@@ -160,6 +160,11 @@ CViewerView::CViewerView()
160160 memset (&mStableRgbBufferInfo , 0 , sizeof (BufferInfo));
161161
162162 mRcProgress .SetRectEmpty ();
163+ mRcVolume .SetRectEmpty ();
164+ mRcMute .SetRectEmpty ();
165+ mVolume = 1 .0f ;
166+ mVolumeMuted = false ;
167+ mVolumeDragging = false ;
163168
164169 mMouseMenu .CreatePopupMenu ();
165170 CString str;
@@ -293,39 +298,133 @@ void CViewerView::Initialize(int nFrame, size_t rgbStride, int w, int h, bool pr
293298void CViewerView::ProgressiveDraw (CDC *pDC, CViewerDoc* pDoc, int frameID)
294299{
295300 const int barMargin = MARGIN_PROGESS_BAR;
301+ const int volWidth = 64 ;
302+ const int volGap = 6 ;
303+ const int muteSize = PROGRESS_BAR_H - barMargin * 2 ; // square button inside the band
304+ const int muteGap = 6 ;
296305
297306 int frameMax = pDoc->mFrames - 1 ;
298- int limit =
299- ROUND2I ((mWClient - (barMargin << 1 )) * frameID / (float )frameMax);
300307
301308 CString str;
302309 str.Format (_T (" %d / %d" ), frameID, frameMax);
303310
304311 CRect progressBand (0 , mHCanvas , mWClient , mHClient );
305312 pDC->FillSolidRect (progressBand, Q1UI_COLOR_SURFACE_ALT);
306313
307- CRect barTextRect, barRect;
308- CRect trackRect;
314+ pDC->SelectObject (&mProgressFont );
315+ pDC->SetTextColor (COLOR_PROGRESS_TEXT);
316+ pDC->SetBkMode (TRANSPARENT);
317+
318+ // Reserve a stable slot on the right based on the maximum frame number
319+ // (not the current one) so the volume/mute layout does not shift as the
320+ // counter advances during playback.
321+ CString maxStr;
322+ maxStr.Format (_T (" %d / %d" ), frameMax, frameMax);
323+ int textW = pDC->GetTextExtent (maxStr).cx ;
324+ int rightEnd = mWClient - barMargin;
325+ int textLeft = rightEnd - textW;
326+
327+ bool showVolume = mAudioPlayer .IsOpen ();
328+ int progressRight = textLeft - barMargin;
329+ if (showVolume)
330+ progressRight -= muteSize + muteGap + volWidth + volGap;
309331
310- trackRect.top = mHCanvas + barMargin;
311- trackRect.left = barMargin;
332+ if (progressRight < barMargin + 4 )
333+ progressRight = barMargin + 4 ;
334+
335+ CRect trackRect;
336+ trackRect.top = mHCanvas + barMargin;
337+ trackRect.left = barMargin;
312338 trackRect.bottom = mHClient - barMargin;
313- trackRect.right = mWClient - barMargin ;
339+ trackRect.right = progressRight ;
314340 pDC->FillSolidRect (trackRect, Q1UI_COLOR_BORDER_SOFT);
315341
316- barRect = trackRect;
342+ // Hit-test region for seeks. Extends vertically over the whole band so
343+ // clicks just above/below the slim track still seek, but horizontally
344+ // stays inside the track so frame-counter and volume areas are excluded.
345+ mRcProgress .SetRect (trackRect.left , mHCanvas , trackRect.right , mHClient );
346+
347+ int limit = ROUND2I ((trackRect.Width ()) * frameID / (float )frameMax);
348+ CRect barRect = trackRect;
317349 barRect.right = barRect.left + limit;
318350 pDC->FillSolidRect (&barRect, mBarColor );
319351
320- barTextRect.top = mHCanvas ;
321- barTextRect.left = 0 ;
352+ if (showVolume) {
353+ // Mute button (small square) just before the volume bar.
354+ CRect muteRect;
355+ muteRect.top = trackRect.top ;
356+ muteRect.bottom = trackRect.bottom ;
357+ muteRect.right = textLeft - barMargin - volWidth - volGap;
358+ muteRect.left = muteRect.right - muteSize;
359+ DrawMuteButton (pDC, muteRect);
360+ mRcMute = muteRect;
361+
362+ // Volume bar
363+ CRect volTrack;
364+ volTrack.top = trackRect.top ;
365+ volTrack.bottom = trackRect.bottom ;
366+ volTrack.right = textLeft - barMargin;
367+ volTrack.left = volTrack.right - volWidth;
368+ pDC->FillSolidRect (volTrack, Q1UI_COLOR_BORDER_SOFT);
369+
370+ COLORREF fillColor = mVolumeMuted ? Q1UI_COLOR_WARNING : mBarColor ;
371+ float fillLevel = mVolumeMuted ? 1 .0f : mVolume ;
372+ int fillW = ROUND2I (volTrack.Width () * fillLevel);
373+ CRect volFill = volTrack;
374+ volFill.right = volFill.left + fillW;
375+ pDC->FillSolidRect (&volFill, fillColor);
376+
377+ mRcVolume = volTrack;
378+ } else {
379+ mRcVolume .SetRectEmpty ();
380+ mRcMute .SetRectEmpty ();
381+ }
382+
383+ CRect barTextRect;
384+ barTextRect.top = mHCanvas ;
322385 barTextRect.bottom = mHClient ;
323- barTextRect.right = mWClient - barMargin;
386+ barTextRect.right = rightEnd;
387+ barTextRect.left = 0 ;
388+ pDC->DrawText (str, &barTextRect, DT_SINGLELINE | DT_RIGHT | DT_VCENTER);
389+ }
324390
325- pDC->SelectObject (&mProgressFont );
326- pDC->SetTextColor (COLOR_PROGRESS_TEXT);
327- pDC->SetBkMode (TRANSPARENT);
328- pDC->DrawText (str, &barTextRect, DT_SINGLELINE | DT_RIGHT | DT_VCENTER);
391+ // Draws a small speaker icon centered in |rect|. The icon is rendered from
392+ // GDI primitives (rectangle + triangle) so it works without depending on
393+ // any specific Unicode glyph being available in the progress-band font.
394+ void CViewerView::DrawMuteButton (CDC *pDC, const CRect &rect)
395+ {
396+ COLORREF accent = mVolumeMuted ? Q1UI_COLOR_WARNING : mBarColor ;
397+
398+ // Speaker base (small rectangle on the left) + cone (triangle to the right).
399+ int cx = rect.left + rect.Width () / 2 ;
400+ int cy = rect.top + rect.Height () / 2 ;
401+ int half = rect.Height () / 2 ;
402+ int baseW = max (half / 2 , 2 );
403+
404+ CRect baseRect (cx - baseW, cy - half / 2 , cx, cy + half / 2 );
405+ pDC->FillSolidRect (baseRect, accent);
406+
407+ CBrush brush (accent);
408+ HGDIOBJ oldBrush = pDC->SelectObject (brush);
409+ HGDIOBJ oldPen = pDC->SelectObject (::GetStockObject (NULL_PEN));
410+ POINT cone[3 ] = {
411+ { cx, cy - half + 1 },
412+ { cx + half, cy - half - 1 },
413+ { cx + half, cy + half + 1 },
414+ };
415+ pDC->Polygon (cone, 3 );
416+ pDC->SelectObject (oldBrush);
417+
418+ // When muted, draw a slash through the speaker.
419+ if (mVolumeMuted ) {
420+ CPen slashPen (PS_SOLID, 2 , Q1UI_COLOR_DANGER);
421+ HGDIOBJ oldSlash = pDC->SelectObject (slashPen);
422+ pDC->MoveTo (rect.left + 1 , rect.bottom - 1 );
423+ pDC->LineTo (rect.right - 1 , rect.top + 1 );
424+ pDC->SelectObject (oldSlash);
425+ }
426+
427+ pDC->SelectObject (oldPen);
329428}
330429
331430void CViewerView::_ScaleRgb (BYTE *src, BYTE *dst, int sDst , q1::GridInfo &gi)
@@ -484,8 +583,11 @@ void CViewerView::SetPlayTimer(CViewerDoc* pDoc)
484583 }
485584
486585 double startSec = pDoc->mFps > 0.0 ? pDoc->mCurFrameID / pDoc->mFps : 0.0 ;
487- if (mAudioPlayer .Open (pDoc->mPathName .GetString ()))
586+ if (mAudioPlayer .Open (pDoc->mPathName .GetString ())) {
587+ mAudioPlayer .SetVolume (mVolume );
588+ mAudioPlayer .SetMuted (mVolumeMuted );
488589 mAudioPlayer .Play (startSec);
590+ }
489591}
490592
491593// Timer callbacks only post clock ticks, so pausing does not need to wait for
@@ -763,6 +865,7 @@ void CViewerView::DrawHelpMenu(CDC *pDC)
763865 " B Selected box size\n "
764866 " I Interpolate pixels\n "
765867 " N Next color space\n "
868+ " M Mute or unmute video audio\n "
766869 );
767870 pDC->DrawText (manual, &manualRect, DT_LEFT | DT_TOP);
768871}
@@ -1198,8 +1301,41 @@ BOOL CViewerView::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
11981301 return CView::OnMouseWheel (nFlags, zDelta, pt);
11991302}
12001303
1304+ void CViewerView::UpdateVolumeFromPoint (CPoint point)
1305+ {
1306+ if (mRcVolume .IsRectEmpty ())
1307+ return ;
1308+ int x = point.x - mRcVolume .left ;
1309+ int w = mRcVolume .Width ();
1310+ if (w <= 0 ) return ;
1311+ if (x < 0 ) x = 0 ;
1312+ if (x > w) x = w;
1313+ mVolume = static_cast <float >(x) / static_cast <float >(w);
1314+ mVolumeMuted = false ;
1315+ mAudioPlayer .SetMuted (false );
1316+ mAudioPlayer .SetVolume (mVolume );
1317+ Invalidate (FALSE );
1318+ }
1319+
1320+ void CViewerView::ToggleMute ()
1321+ {
1322+ mVolumeMuted = !mVolumeMuted ;
1323+ mAudioPlayer .SetMuted (mVolumeMuted );
1324+ Invalidate (FALSE );
1325+ }
1326+
12011327void CViewerView::OnLButtonDown (UINT nFlags, CPoint point)
12021328{
1329+ if (!mRcMute .IsRectEmpty () && mRcMute .PtInRect (point)) {
1330+ ToggleMute ();
1331+ return ;
1332+ }
1333+ if (!mRcVolume .IsRectEmpty () && mRcVolume .PtInRect (point)) {
1334+ SetCapture ();
1335+ mVolumeDragging = true ;
1336+ UpdateVolumeFromPoint (point);
1337+ return ;
1338+ }
12031339 if (mRcProgress .PtInRect (point)) {
12041340 CViewerDoc* pDoc = GetDocument ();
12051341 CMainFrame* pMainFrm = static_cast <CMainFrame*>(AfxGetMainWnd ());
@@ -1210,8 +1346,12 @@ void CViewerView::OnLButtonDown(UINT nFlags, CPoint point)
12101346 KillPlayTimerSafe ();
12111347 }
12121348
1213- double R = (point.x + 1 ) / double (mWCanvas );
1349+ int trackW = mRcProgress .Width ();
1350+ double R = trackW > 0
1351+ ? (point.x - mRcProgress .left + 1 ) / double (trackW) : 0.0 ;
12141352 int frameID = max (ROUND2I (R * pDoc->mFrames ) - 1 , 0 );
1353+ int frameMax = static_cast <int >(pDoc->mFrames ) - 1 ;
1354+ if (frameID > frameMax) frameID = frameMax;
12151355 if (pDoc->SeekScene (frameID) >= 0 ) {
12161356 mKeyProcessing = true ;
12171357 Invalidate (FALSE );
@@ -1264,6 +1404,11 @@ void CViewerView::OnLButtonDown(UINT nFlags, CPoint point)
12641404
12651405void CViewerView::OnLButtonUp (UINT nFlags, CPoint point)
12661406{
1407+ if (mVolumeDragging ) {
1408+ mVolumeDragging = false ;
1409+ ::ReleaseCapture ();
1410+ return ;
1411+ }
12671412 if (mSelMode && !(nFlags & MK_CONTROL) && mIsClicked ) {
12681413 CPoint transPointS, transPointE;
12691414 QSelRegion selRegion;
@@ -1330,6 +1475,10 @@ void CViewerView::OnLButtonUp(UINT nFlags, CPoint point)
13301475
13311476void CViewerView::OnMouseMove (UINT nFlags, CPoint point)
13321477{
1478+ if (mVolumeDragging ) {
1479+ UpdateVolumeFromPoint (point);
1480+ return ;
1481+ }
13331482 if (mSelMode && !(nFlags & MK_CONTROL)) {
13341483 CPoint transPointS (int ((-mXDst + mPointS .x ) / mN ), int ((-mYDst + mPointS .y ) / mN ));
13351484 CPoint transPointE (int ((-mXDst + point.x ) / mN ), int ((-mYDst + point.y ) / mN ));
@@ -1886,6 +2035,9 @@ void CViewerView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
18862035 mInterpol = !mInterpol ;
18872036 BroadcastDisplayOptions ();
18882037 break ;
2038+ case ' M' :
2039+ ToggleMute ();
2040+ break ;
18892041 case ' R' :
18902042 pDoc->Rotate90 ();
18912043 {
@@ -1969,6 +2121,12 @@ void CViewerView::OnSize(UINT nType, int cx, int cy)
19692121
19702122void CViewerView::OnRButtonUp (UINT nFlags, CPoint point)
19712123{
2124+ if ((!mRcVolume .IsRectEmpty () && mRcVolume .PtInRect (point)) ||
2125+ (!mRcMute .IsRectEmpty () && mRcMute .PtInRect (point))) {
2126+ ToggleMute ();
2127+ return ;
2128+ }
2129+
19722130 CPoint screenPoint = point;
19732131
19742132 ClientToScreen (&screenPoint);
0 commit comments