1 /**********************************************************************
2 
3 Audacity: A Digital Audio Editor
4 
5 TrackInfo.cpp
6 
7 Paul Licameli split from TrackPanel.cpp
8 
9 ********************************************************************//*!
10 
11 \namespace TrackInfo
12 \brief
13   Functions for drawing the track control panel, which is shown to the side
14   of a track
15   It has the menus, pan and gain controls displayed in it.
16   So "Info" is somewhat a misnomer. Should possibly be "TrackControls".
17 
18   It maintains global slider widget instances that are reparented and
19   repositioned as needed for drawing and interaction with the user,
20   interoperating with the custom panel subdivision implemented in CellularPanel
21   and avoiding wxWidgets sizers
22 
23   If we'd instead coded it as a wxWindow, we would have an instance
24   of this class for each track displayed.
25 
26 **********************************************************************/
27 
28 
29 #include "TrackInfo.h"
30 
31 #include <wx/app.h>
32 #include <wx/dc.h>
33 #include <wx/frame.h>
34 
35 #include "AColor.h"
36 #include "AllThemeResources.h"
37 #include "Prefs.h"
38 #include "Project.h"
39 #include "Theme.h"
40 #include "Track.h"
41 #include "TrackPanelDrawingContext.h"
42 #include "ViewInfo.h"
43 #include "prefs/TracksBehaviorsPrefs.h"
44 #include "tracks/ui/TrackView.h"
45 
46 // Subscribe to preference changes to update static variables
47 struct Settings : PrefsListener {
48    wxString gSoloPref;
49    wxFont gFont;
50 
51    bool mInitialized{ false };
52 
UpdatePrefsSettings53    void UpdatePrefs() override
54    {
55       gSoloPref = TracksBehaviorsSolo.Read();
56 
57       // Calculation of best font size depends on language, so it should be redone in case
58       // the language preference changed.
59 
60       // wxWidgets seems to need a window to do this portably.
61       if ( !wxTheApp )
62          return;
63       auto window = wxTheApp->GetTopWindow();
64       if ( !window )
65          return;
66 
67       int fontSize = 10;
68       gFont.Create(fontSize, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_NORMAL);
69 
70       int allowableWidth =
71          // PRL:  was it correct to include the margin?
72          ( kTrackInfoWidth + kLeftMargin )
73             - 2; // 2 to allow for left/right borders
74       int textWidth;
75       do {
76          gFont.SetPointSize(fontSize);
77          window->GetTextExtent(_("Stereo, 999999Hz"),
78             &textWidth, nullptr, nullptr, nullptr, &gFont);
79          fontSize--;
80       } while (textWidth >= allowableWidth);
81 
82       mInitialized = true;
83    }
84 };
85 
settings()86 static Settings &settings()
87 {
88    static Settings theSettings;
89    if ( !theSettings.mInitialized )
90       theSettings.UpdatePrefs();
91    return theSettings;
92 }
93 
HasSoloButton()94 bool TrackInfo::HasSoloButton()
95 {
96    return settings().gSoloPref != wxT("None");
97 }
98 
99 #define RANGE(array) (array), (array) + sizeof(array)/sizeof(*(array))
100 using TCPLine = TrackInfo::TCPLine;
101 using TCPLines = TrackInfo::TCPLines;
102 
commonTrackTCPLines()103 static const TCPLines &commonTrackTCPLines()
104 {
105    static const TCPLines theLines{
106 #ifdef EXPERIMENTAL_DA
107 
108       { TCPLine::kItemBarButtons, kTrackInfoBtnSize, 4,
109         &TrackInfo::CloseTitleDrawFunction },
110 
111 #else
112 
113       { TCPLine::kItemBarButtons, kTrackInfoBtnSize, 0,
114         &TrackInfo::CloseTitleDrawFunction },
115 
116 #endif
117    };
118    return theLines;
119 }
120 
121 #include "tracks/ui/CommonTrackControls.h"
StaticTCPLines()122 const TCPLines &CommonTrackControls::StaticTCPLines()
123 {
124    return commonTrackTCPLines();
125 }
126 
127 namespace {
128 
totalTCPLines(const TCPLines & lines,bool omitLastExtra)129 int totalTCPLines( const TCPLines &lines, bool omitLastExtra )
130 {
131    int total = 0;
132    int lastExtra = 0;
133    for ( const auto line : lines ) {
134       lastExtra = line.extraSpace;
135       total += line.height + lastExtra;
136    }
137    if (omitLastExtra)
138       total -= lastExtra;
139    return total;
140 }
141 }
142 
143 // return y value and height
144 std::pair< int, int >
CalcItemY(const TCPLines & lines,unsigned iItem)145 TrackInfo::CalcItemY( const TCPLines &lines, unsigned iItem )
146 {
147    int y = 0;
148    auto pLines = lines.begin();
149    while ( pLines != lines.end() &&
150            0 == (pLines->items & iItem) ) {
151       y += pLines->height + pLines->extraSpace;
152       ++pLines;
153    }
154    int height = 0;
155    if ( pLines != lines.end() )
156       height = pLines->height;
157    return { y, height };
158 }
159 
160 namespace {
161 
162 // Items for the bottom of the panel, listed bottom-upwards
163 // As also with the top items, the extra space is below the item
164 const TrackInfo::TCPLine defaultCommonTrackTCPBottomLines[] = {
165    // The '0' avoids impinging on bottom line of TCP
166    // Use -1 if you do want to do so.
167    { TCPLine::kItemSyncLock | TCPLine::kItemMinimize, kTrackInfoBtnSize, 0,
168      &TrackInfo::MinimizeSyncLockDrawFunction },
169 };
170 TCPLines commonTrackTCPBottomLines{ RANGE(defaultCommonTrackTCPBottomLines) };
171 
172 // return y value and height
CalcBottomItemY(const TCPLines & lines,unsigned iItem,int height)173 std::pair< int, int > CalcBottomItemY
174    ( const TCPLines &lines, unsigned iItem, int height )
175 {
176    int y = height;
177    auto pLines = lines.begin();
178    while ( pLines != lines.end() &&
179            0 == (pLines->items & iItem) ) {
180       y -= pLines->height + pLines->extraSpace;
181       ++pLines;
182    }
183    if (pLines != lines.end())
184       y -= (pLines->height + pLines->extraSpace );
185    return { y, pLines->height };
186 }
187 
188 }
189 
GetTCPLines() const190 const TCPLines &CommonTrackControls::GetTCPLines() const
191 {
192    return commonTrackTCPLines();
193 }
194 
MinimumTrackHeight()195 unsigned TrackInfo::MinimumTrackHeight()
196 {
197    unsigned height = 0;
198    if (!commonTrackTCPLines().empty())
199       height += commonTrackTCPLines().front().height;
200    if (!commonTrackTCPBottomLines.empty())
201       height += commonTrackTCPBottomLines.front().height;
202    // + 1 prevents the top item from disappearing for want of enough space,
203    // according to the rules in HideTopItem.
204    return height + kVerticalPadding + 1;
205 }
206 
HideTopItem(const wxRect & rect,const wxRect & subRect,int allowance)207 bool TrackInfo::HideTopItem( const wxRect &rect, const wxRect &subRect,
208                  int allowance ) {
209    auto limit = CalcBottomItemY
210    ( commonTrackTCPBottomLines, TCPLine::kHighestBottomItem, rect.height).first;
211    // Return true if the rectangle is even touching the limit
212    // without an overlap.  That was the behavior as of 2.1.3.
213    return subRect.y + subRect.height - allowance >= rect.y + limit;
214 }
215 
DrawItems(TrackPanelDrawingContext & context,const wxRect & rect,const Track & track)216 void TrackInfo::DrawItems
217 ( TrackPanelDrawingContext &context,
218   const wxRect &rect, const Track &track  )
219 {
220    auto &trackControl = static_cast<const CommonTrackControls&>(
221       TrackControls::Get( track ) );
222    const auto &topLines = trackControl.GetTCPLines();
223    const auto &bottomLines = commonTrackTCPBottomLines;
224    DrawItems
225       ( context, rect, &track, topLines, bottomLines );
226 }
227 
DrawItems(TrackPanelDrawingContext & context,const wxRect & rect,const Track * pTrack,const std::vector<TCPLine> & topLines,const std::vector<TCPLine> & bottomLines)228 void TrackInfo::DrawItems
229 ( TrackPanelDrawingContext &context,
230   const wxRect &rect, const Track *pTrack,
231   const std::vector<TCPLine> &topLines, const std::vector<TCPLine> &bottomLines )
232 {
233    auto dc = &context.dc;
234    TrackInfo::SetTrackInfoFont(dc);
235    dc->SetTextForeground(theTheme.Colour(clrTrackPanelText));
236 
237    {
238       int yy = 0;
239       for ( const auto &line : topLines ) {
240          wxRect itemRect{
241             rect.x, rect.y + yy,
242             rect.width, line.height
243          };
244          if ( !TrackInfo::HideTopItem( rect, itemRect ) &&
245               line.drawFunction )
246             line.drawFunction( context, itemRect, pTrack );
247          yy += line.height + line.extraSpace;
248       }
249    }
250    {
251       int yy = rect.height;
252       for ( const auto &line : bottomLines ) {
253          yy -= line.height + line.extraSpace;
254          if ( line.drawFunction ) {
255             wxRect itemRect{
256                rect.x, rect.y + yy,
257                rect.width, line.height
258             };
259             line.drawFunction( context, itemRect, pTrack );
260          }
261       }
262    }
263 }
264 
265 #include "tracks/ui/TrackButtonHandles.h"
DrawCloseButton(TrackPanelDrawingContext & context,const wxRect & bev,const Track * pTrack,ButtonHandle * target)266 void TrackInfo::DrawCloseButton(
267    TrackPanelDrawingContext &context, const wxRect &bev,
268    const Track *pTrack, ButtonHandle *target )
269 {
270    auto dc = &context.dc;
271    bool selected = pTrack ? pTrack->GetSelected() : true;
272    bool hit = target && target->GetTrack().get() == pTrack;
273    bool captured = hit && target->IsClicked();
274    bool down = captured && bev.Contains( context.lastState.GetPosition());
275    AColor::Bevel2(*dc, !down, bev, selected, hit );
276 
277 #ifdef EXPERIMENTAL_THEMING
278    wxPen pen( theTheme.Colour( clrTrackPanelText ));
279    dc->SetPen( pen );
280 #else
281    dc->SetPen(*wxBLACK_PEN);
282 #endif
283    bev.Inflate( -1, -1 );
284    // Draw the "X"
285    const int s = 6;
286 
287    int ls = bev.x + ((bev.width - s) / 2);
288    int ts = bev.y + ((bev.height - s) / 2);
289    int rs = ls + s;
290    int bs = ts + s;
291 
292    AColor::Line(*dc, ls,     ts, rs,     bs);
293    AColor::Line(*dc, ls + 1, ts, rs + 1, bs);
294    AColor::Line(*dc, rs,     ts, ls,     bs);
295    AColor::Line(*dc, rs + 1, ts, ls + 1, bs);
296    //   bev.Inflate(-1, -1);
297 }
298 
CloseTitleDrawFunction(TrackPanelDrawingContext & context,const wxRect & rect,const Track * pTrack)299 void TrackInfo::CloseTitleDrawFunction
300 ( TrackPanelDrawingContext &context,
301   const wxRect &rect, const Track *pTrack )
302 {
303    auto dc = &context.dc;
304    bool selected = pTrack ? pTrack->GetSelected() : true;
305    {
306       wxRect bev = rect;
307       GetCloseBoxHorizontalBounds( rect, bev );
308       auto target = dynamic_cast<CloseButtonHandle*>( context.target.get() );
309       DrawCloseButton( context, bev, pTrack, target );
310    }
311 
312    {
313       wxRect bev = rect;
314       GetTitleBarHorizontalBounds( rect, bev );
315       auto target = dynamic_cast<MenuButtonHandle*>( context.target.get() );
316       bool hit = target && target->GetTrack().get() == pTrack;
317       bool captured = hit && target->IsClicked();
318       bool down = captured && bev.Contains( context.lastState.GetPosition());
319       wxString titleStr =
320          pTrack ? pTrack->GetName() : _("Name");
321 
322       //bev.Inflate(-1, -1);
323       AColor::Bevel2(*dc, !down, bev, selected, hit);
324 
325       // Draw title text
326       SetTrackInfoFont(dc);
327 
328       // Bug 1660 The 'k' of 'Audio Track' was being truncated.
329       // Constant of 32 found by counting pixels on a windows machine.
330       // I believe it's the size of the X close button + the size of the
331       // drop down arrow.
332       int allowableWidth = rect.width - 32;
333 
334       wxCoord textWidth, textHeight;
335       dc->GetTextExtent(titleStr, &textWidth, &textHeight);
336       while (textWidth > allowableWidth) {
337          titleStr = titleStr.Left(titleStr.length() - 1);
338          dc->GetTextExtent(titleStr, &textWidth, &textHeight);
339       }
340 
341       // Pop-up triangle
342    #ifdef EXPERIMENTAL_THEMING
343       wxColour c = theTheme.Colour( clrTrackPanelText );
344    #else
345       wxColour c = *wxBLACK;
346    #endif
347 
348       // wxGTK leaves little scraps (antialiasing?) of the
349       // characters if they are repeatedly drawn.  This
350       // happens when holding down mouse button and moving
351       // in and out of the title bar.  So clear it first.
352    //   AColor::MediumTrackInfo(dc, t->GetSelected());
353    //   dc->DrawRectangle(bev);
354 
355       dc->SetTextForeground( c );
356       dc->SetTextBackground( wxTRANSPARENT );
357       dc->DrawText(titleStr, bev.x + 2, bev.y + (bev.height - textHeight) / 2);
358 
359 
360 
361       dc->SetPen(c);
362       dc->SetBrush(c);
363 
364       int s = 10; // Width of dropdown arrow...height is half of width
365       AColor::Arrow(*dc,
366                     bev.GetRight() - s - 3, // 3 to offset from right border
367                     bev.y + ((bev.height - (s / 2)) / 2),
368                     s);
369 
370    }
371 }
372 
MinimizeSyncLockDrawFunction(TrackPanelDrawingContext & context,const wxRect & rect,const Track * pTrack)373 void TrackInfo::MinimizeSyncLockDrawFunction
374 ( TrackPanelDrawingContext &context,
375   const wxRect &rect, const Track *pTrack )
376 {
377    auto dc = &context.dc;
378    bool selected = pTrack ? pTrack->GetSelected() : true;
379    bool syncLockSelected = pTrack ? pTrack->IsSyncLockSelected() : true;
380    bool minimized =
381       pTrack ? TrackView::Get( *pTrack ).GetMinimized() : false;
382    {
383       wxRect bev = rect;
384       GetMinimizeHorizontalBounds(rect, bev);
385       auto target = dynamic_cast<MinimizeButtonHandle*>( context.target.get() );
386       bool hit = target && target->GetTrack().get() == pTrack;
387       bool captured = hit && target->IsClicked();
388       bool down = captured && bev.Contains( context.lastState.GetPosition());
389 
390       // Clear background to get rid of previous arrow
391       //AColor::MediumTrackInfo(dc, t->GetSelected());
392       //dc->DrawRectangle(bev);
393 
394       AColor::Bevel2(*dc, !down, bev, selected, hit);
395 
396 #ifdef EXPERIMENTAL_THEMING
397       wxColour c = theTheme.Colour(clrTrackPanelText);
398       dc->SetBrush(c);
399       dc->SetPen(c);
400 #else
401       AColor::Dark(dc, selected);
402 #endif
403 
404       AColor::Arrow(*dc,
405                     bev.x - 5 + bev.width / 2,
406                     bev.y - 2 + bev.height / 2,
407                     10,
408                     minimized);
409    }
410 
411    {
412       wxRect bev = rect;
413       GetSelectButtonHorizontalBounds(rect, bev);
414       auto target = dynamic_cast<SelectButtonHandle*>( context.target.get() );
415       bool hit = target && target->GetTrack().get() == pTrack;
416       bool captured = hit && target->IsClicked();
417       bool down = captured && bev.Contains( context.lastState.GetPosition());
418 
419       AColor::Bevel2(*dc, !down, bev, selected, hit);
420 
421 #ifdef EXPERIMENTAL_THEMING
422       wxColour c = theTheme.Colour(clrTrackPanelText);
423       dc->SetBrush(c);
424       dc->SetPen(c);
425 #else
426       AColor::Dark(dc, selected);
427 #endif
428 
429       wxString str = _("Select");
430       wxCoord textWidth;
431       wxCoord textHeight;
432       SetTrackInfoFont(dc);
433       dc->GetTextExtent(str, &textWidth, &textHeight);
434 
435       dc->SetTextForeground( c );
436       dc->SetTextBackground( wxTRANSPARENT );
437       dc->DrawText(str, bev.x + 2 + (bev.width-textWidth)/2, bev.y + (bev.height - textHeight) / 2);
438    }
439 
440 
441    // Draw the sync-lock indicator if this track is in a sync-lock selected group.
442    if (syncLockSelected)
443    {
444       wxRect syncLockIconRect = rect;
445 
446       GetSyncLockHorizontalBounds( rect, syncLockIconRect );
447       wxBitmap syncLockBitmap(theTheme.Image(bmpSyncLockIcon));
448       // Icon is 12x12 and syncLockIconRect is 16x16.
449       dc->DrawBitmap(syncLockBitmap,
450                      syncLockIconRect.x + 3,
451                      syncLockIconRect.y + 2,
452                      true);
453    }
454 }
455 
GetCloseBoxHorizontalBounds(const wxRect & rect,wxRect & dest)456 void TrackInfo::GetCloseBoxHorizontalBounds( const wxRect & rect, wxRect &dest )
457 {
458    dest.x = rect.x;
459    dest.width = kTrackInfoBtnSize;
460 }
461 
GetCloseBoxRect(const wxRect & rect,wxRect & dest)462 void TrackInfo::GetCloseBoxRect(const wxRect & rect, wxRect & dest)
463 {
464    GetCloseBoxHorizontalBounds( rect, dest );
465    auto results = CalcItemY( commonTrackTCPLines(), TCPLine::kItemBarButtons );
466    dest.y = rect.y + results.first;
467    dest.height = results.second;
468 }
469 
GetTitleBarHorizontalBounds(const wxRect & rect,wxRect & dest)470 void TrackInfo::GetTitleBarHorizontalBounds( const wxRect & rect, wxRect &dest )
471 {
472    // to right of CloseBoxRect, plus a little more
473    wxRect closeRect;
474    GetCloseBoxHorizontalBounds( rect, closeRect );
475    dest.x = rect.x + closeRect.width + 1;
476    dest.width = rect.x + rect.width - dest.x + TitleSoloBorderOverlap;
477 }
478 
GetTitleBarRect(const wxRect & rect,wxRect & dest)479 void TrackInfo::GetTitleBarRect(const wxRect & rect, wxRect & dest)
480 {
481    GetTitleBarHorizontalBounds( rect, dest );
482    auto results = CalcItemY( commonTrackTCPLines(), TCPLine::kItemBarButtons );
483    dest.y = rect.y + results.first;
484    dest.height = results.second;
485 }
486 
GetSliderHorizontalBounds(const wxPoint & topleft,wxRect & dest)487 void TrackInfo::GetSliderHorizontalBounds( const wxPoint &topleft, wxRect &dest )
488 {
489    dest.x = topleft.x + 6;
490    dest.width = kTrackInfoSliderWidth;
491 }
492 
GetMinimizeHorizontalBounds(const wxRect & rect,wxRect & dest)493 void TrackInfo::GetMinimizeHorizontalBounds( const wxRect &rect, wxRect &dest )
494 {
495    const int space = 0;// was 3.
496    dest.x = rect.x + space;
497 
498    wxRect syncLockRect;
499    GetSyncLockHorizontalBounds( rect, syncLockRect );
500 
501    // Width is rect.width less space on left for track select
502    // and on right for sync-lock icon.
503    dest.width = kTrackInfoBtnSize;
504 // rect.width - (space + syncLockRect.width);
505 }
506 
GetMinimizeRect(const wxRect & rect,wxRect & dest)507 void TrackInfo::GetMinimizeRect(const wxRect & rect, wxRect &dest)
508 {
509    GetMinimizeHorizontalBounds( rect, dest );
510    auto results = CalcBottomItemY
511       ( commonTrackTCPBottomLines, TCPLine::kItemMinimize, rect.height);
512    dest.y = rect.y + results.first;
513    dest.height = results.second;
514 }
515 
GetSelectButtonHorizontalBounds(const wxRect & rect,wxRect & dest)516 void TrackInfo::GetSelectButtonHorizontalBounds( const wxRect &rect, wxRect &dest )
517 {
518    const int space = 0;// was 3.
519    dest.x = rect.x + space;
520 
521    wxRect syncLockRect;
522    GetSyncLockHorizontalBounds( rect, syncLockRect );
523    wxRect minimizeRect;
524    GetMinimizeHorizontalBounds( rect, minimizeRect );
525 
526    dest.x = dest.x + space + minimizeRect.width;
527    // Width is rect.width less space on left for track select
528    // and on right for sync-lock icon.
529    dest.width = rect.width - (space + syncLockRect.width) - (space + minimizeRect.width);
530 }
531 
532 
GetSelectButtonRect(const wxRect & rect,wxRect & dest)533 void TrackInfo::GetSelectButtonRect(const wxRect & rect, wxRect &dest)
534 {
535    GetSelectButtonHorizontalBounds( rect, dest );
536    auto results = CalcBottomItemY
537       ( commonTrackTCPBottomLines, TCPLine::kItemMinimize, rect.height);
538    dest.y = rect.y + results.first;
539    dest.height = results.second;
540 }
541 
GetSyncLockHorizontalBounds(const wxRect & rect,wxRect & dest)542 void TrackInfo::GetSyncLockHorizontalBounds( const wxRect &rect, wxRect &dest )
543 {
544    dest.width = kTrackInfoBtnSize;
545    dest.x = rect.x + rect.width - dest.width;
546 }
547 
GetSyncLockIconRect(const wxRect & rect,wxRect & dest)548 void TrackInfo::GetSyncLockIconRect(const wxRect & rect, wxRect &dest)
549 {
550    GetSyncLockHorizontalBounds( rect, dest );
551    auto results = CalcBottomItemY
552       ( commonTrackTCPBottomLines, TCPLine::kItemSyncLock, rect.height);
553    dest.y = rect.y + results.first;
554    dest.height = results.second;
555 }
556 
557 /// \todo Probably should move to 'Utils.cpp'.
SetTrackInfoFont(wxDC * dc)558 void TrackInfo::SetTrackInfoFont(wxDC * dc)
559 {
560    dc->SetFont(settings().gFont);
561 }
562 
563 //#define USE_BEVELS
564 
DefaultTrackHeight(const TCPLines & topLines)565 unsigned TrackInfo::DefaultTrackHeight( const TCPLines &topLines )
566 {
567    int needed =
568       kVerticalPadding +
569       totalTCPLines( topLines, true ) +
570       totalTCPLines( commonTrackTCPBottomLines, false ) + 1;
571    return (unsigned) std::max( needed, (int) TrackView::DefaultHeight );
572 }
573