Audacity 3.2.0
TrackArt.cpp
Go to the documentation of this file.
1/**********************************************************************
2
3 Audacity: A Digital Audio Editor
4
5 @file TrackArt.cpp
6
7 Paul Licameli split from TrackArtist.cpp
8
9**********************************************************************/
10
11#include "TrackArt.h"
12
13#include "AColor.h"
14#include "AllThemeResources.h"
15#include "SelectedRegion.h"
16#include "SyncLock.h"
17#include "Theme.h"
18#include "Track.h"
19#include "TrackArtist.h"
21#include "ZoomInfo.h"
22#include <wx/app.h>
23#include <wx/dc.h>
24
25static constexpr int ClipSelectionStrokeSize{ 1 };//px
26
27namespace
28{
29 //Helper function that takes affordance rectangle as
30 //argument and returns rectangle to be used for title
31 //drawings
32 wxRect GetAffordanceTitleRect(const wxRect& rect)
33 {
34 constexpr int FrameThickness{ 1 };
35 return wxRect(
36 rect.GetLeft() + TrackArt::ClipFrameRadius,
37 rect.GetTop() + ClipSelectionStrokeSize + FrameThickness,
38 rect.GetWidth() - TrackArt::ClipFrameRadius * 2,
39 rect.GetHeight() - ClipSelectionStrokeSize - FrameThickness);
40 }
41
42}
43
46int GetWaveYPos(float value, float min, float max,
47 int height, bool dB, bool outer,
48 float dBr, bool clip)
49{
50 if (dB) {
51 if (height == 0) {
52 return 0;
53 }
54
55 float sign = (value >= 0 ? 1 : -1);
56
57 if (value != 0.) {
58 float db = LINEAR_TO_DB(fabs(value));
59 value = (db + dBr) / dBr;
60 if (!outer) {
61 value -= 0.5;
62 }
63 if (value < 0.0) {
64 value = 0.0;
65 }
66 value *= sign;
67 }
68 }
69 else {
70 if (!outer) {
71 if (value >= 0.0) {
72 value -= 0.5;
73 }
74 else {
75 value += 0.5;
76 }
77 }
78 }
79
80 if (clip) {
81 if (value < min) {
82 value = min;
83 }
84 if (value > max) {
85 value = max;
86 }
87 }
88
89 value = (max - value) / (max - min);
90 return (int) (value * (height - 1) + 0.5);
91}
92
93float FromDB(float value, double dBRange)
94{
95 if (value == 0)
96 return 0;
97
98 double sign = (value >= 0 ? 1 : -1);
99 return DB_TO_LINEAR((fabs(value) * dBRange) - dBRange) * sign;
100}
101
102float ValueOfPixel(int yy, int height, bool offset,
103 bool dB, double dBRange, float zoomMin, float zoomMax)
104{
105 wxASSERT(height > 0);
106 // Map 0 to max and height - 1 (not height) to min
107 float v =
108 height == 1 ? (zoomMin + zoomMax) / 2 :
109 zoomMax - (yy / (float)(height - 1)) * (zoomMax - zoomMin);
110 if (offset) {
111 if (v > 0.0)
112 v += .5;
113 else
114 v -= .5;
115 }
116
117 if (dB)
118 v = FromDB(v, dBRange);
119
120 return v;
121}
122
124 TrackPanelDrawingContext &context, const wxRect &rect )
125{
126 auto &dc = context.dc;
127
128 // Draws two black arrows on the left side of the track to
129 // indicate the user that the track has been time-shifted
130 // to the left beyond t=0.0.
131
132 dc.SetPen(*wxBLACK_PEN);
133 AColor::Line(dc,
134 rect.x + 2, rect.y + 6,
135 rect.x + 8, rect.y + 6);
136 AColor::Line(dc,
137 rect.x + 2, rect.y + 6,
138 rect.x + 6, rect.y + 2);
139 AColor::Line(dc,
140 rect.x + 2, rect.y + 6,
141 rect.x + 6, rect.y + 10);
142 AColor::Line(dc,
143 rect.x + 2, rect.y + rect.height - 8,
144 rect.x + 8, rect.y + rect.height - 8);
145 AColor::Line(dc,
146 rect.x + 2, rect.y + rect.height - 8,
147 rect.x + 6, rect.y + rect.height - 4);
148 AColor::Line(dc,
149 rect.x + 2, rect.y + rect.height - 8,
150 rect.x + 6, rect.y + rect.height - 12);
151}
152
153wxString TrackArt::TruncateText(wxDC& dc, const wxString& text, const int maxWidth)
154{
155 static const wxString ellipsis = "\u2026";
156
157 if (dc.GetTextExtent(text).GetWidth() <= maxWidth)
158 return text;
159
160 auto left = 0;
161 //no need to check text + '...'
162 auto right = static_cast<int>(text.Length() - 2);
163
164 while (left <= right)
165 {
166 auto middle = (left + right) / 2;
167 auto str = text.SubString(0, middle).Trim() + ellipsis;
168 auto strWidth = dc.GetTextExtent(str).GetWidth();
169 if (strWidth < maxWidth)
170 //if left == right (== middle), then exit loop
171 //with right equals to the last known index for which
172 //strWidth < maxWidth
173 left = middle + 1;
174 else if (strWidth > maxWidth)
175 //if right == left (== middle), then exit loop with
176 //right equals to (left - 1), which is the last known
177 //index for which (strWidth < maxWidth) or -1
178 right = middle - 1;
179 else
180 return str;
181 }
182 if (right >= 0)
183 return text.SubString(0, right).Trim() + ellipsis;
184
185 return wxEmptyString;
186}
187
188wxRect TrackArt::DrawClipAffordance(wxDC& dc, const wxRect& rect, bool highlight, bool selected)
189{
190 //To make sure that roundings do not overlap each other
191 auto clipFrameRadius = std::min(ClipFrameRadius, rect.width / 2);
192
193 wxRect clipRect;
194 bool hasClipRect = dc.GetClippingBox(clipRect);
195 //Fix #1689: visual glitches appear on attempt to draw a rectangle
196 //larger than 0x7FFFFFF pixels wide (value was discovered
197 //by manual testing, and maybe depends on OS being used), but
198 //it's very unlikely that such huge rectangle will be ever fully visible
199 //on the screen, so we can safely reduce its size to be slightly larger than
200 //clipping rectangle, and avoid that problem
201 auto drawingRect = rect;
202 if (hasClipRect)
203 {
204 //to make sure that rounding happends outside the clipping rectangle
205 drawingRect.SetLeft(std::max(rect.GetLeft(), clipRect.GetLeft() - clipFrameRadius - 1));
206 drawingRect.SetRight(std::min(rect.GetRight(), clipRect.GetRight() + clipFrameRadius + 1));
207 }
208
209 if (selected)
210 {
211 wxRect strokeRect{
212 drawingRect.x - ClipSelectionStrokeSize,
213 drawingRect.y,
214 drawingRect.width + ClipSelectionStrokeSize * 2,
215 drawingRect.height + clipFrameRadius };
216 dc.SetBrush(*wxTRANSPARENT_BRUSH);
217 AColor::UseThemeColour(&dc, clrClipAffordanceStroke, clrClipAffordanceStroke);
218 dc.DrawRoundedRectangle(strokeRect, clipFrameRadius);
219 }
220
221 AColor::UseThemeColour(&dc, highlight ? clrClipAffordanceActiveBrush : clrClipAffordanceInactiveBrush, clrClipAffordanceOutlinePen);
222 dc.DrawRoundedRectangle(
223 wxRect(
224 drawingRect.x,
225 drawingRect.y + ClipSelectionStrokeSize,
226 drawingRect.width,
227 drawingRect.height + clipFrameRadius
228 ), clipFrameRadius
229 );
230
231 auto titleRect = hasClipRect ?
232 //avoid drawing text outside the clipping rectangle if possible
233 GetAffordanceTitleRect(rect.Intersect(clipRect)) :
235
236 return titleRect;
237}
238
239bool TrackArt::DrawClipTitle(wxDC &dc, const wxRect &titleRect, const wxString &title)
240{
241 if(titleRect.IsEmpty())
242 return false;
243 if(title.empty())
244 return true;
245
246 auto truncatedTitle = TrackArt::TruncateText(dc, title, titleRect.GetWidth());
247 if (!truncatedTitle.empty())
248 {
249 auto hAlign = wxTheApp->GetLayoutDirection() == wxLayout_RightToLeft ? wxALIGN_RIGHT : wxALIGN_LEFT;
250 dc.DrawLabel(truncatedTitle, titleRect, hAlign | wxALIGN_CENTER_VERTICAL);
251 return true;
252 }
253 return false;
254}
255
256void TrackArt::DrawClipEdges(wxDC& dc, const wxRect& clipRect, bool selected)
257{
258 dc.SetBrush(*wxTRANSPARENT_BRUSH);
259 {
260 AColor::UseThemeColour(&dc, -1, clrClipAffordanceOutlinePen);
261 AColor::Line(dc,
262 clipRect.GetLeft(), clipRect.GetTop(),
263 clipRect.GetLeft(), clipRect.GetBottom());
264 AColor::Line(dc,
265 clipRect.GetRight(), clipRect.GetTop(),
266 clipRect.GetRight(), clipRect.GetBottom());
267 }
268 if(selected)
269 {
270 if constexpr (ClipSelectionStrokeSize == 1)
271 {
272 AColor::UseThemeColour(&dc, -1, clrClipAffordanceStroke);
273 AColor::Line(dc,
274 clipRect.GetLeft() - ClipSelectionStrokeSize, clipRect.GetTop(),
275 clipRect.GetLeft() - ClipSelectionStrokeSize, clipRect.GetBottom());
276 AColor::Line(dc,
277 clipRect.GetRight() + ClipSelectionStrokeSize, clipRect.GetTop(),
278 clipRect.GetRight() + ClipSelectionStrokeSize, clipRect.GetBottom());
279 }
280 else if constexpr (ClipSelectionStrokeSize > 1)
281 {
282 AColor::UseThemeColour(&dc, clrClipAffordanceStroke, clrClipAffordanceStroke);
283 dc.DrawRectangle(wxRect(
284 clipRect.GetLeft() - ClipSelectionStrokeSize, clipRect.GetTop(),
285 ClipSelectionStrokeSize, clipRect.GetHeight()));
286 dc.DrawRectangle(wxRect(
287 clipRect.GetRight() + 1, clipRect.GetTop(),
288 ClipSelectionStrokeSize, clipRect.GetHeight()));
289 }
290 }
291}
292
293void TrackArt::DrawClipFolded(wxDC& dc, const wxRect& rect)
294{
295 AColor::UseThemeColour(&dc, clrClipAffordanceOutlinePen);
296 dc.DrawRectangle(rect);
297}
298
299// Draws the sync-lock bitmap, tiled; always draws stationary relative to the DC
300//
301// AWD: now that the tiles don't link together, we're drawing a tilted grid, at
302// two steps down for every one across. This creates a pattern that repeats in
303// 5-step by 5-step boxes. Because we're only drawing in 5/25 possible positions
304// we have a grid spacing somewhat smaller than the image dimensions. Thus we
305// achieve lower density than with a square grid and eliminate edge cases where
306// no tiles are displayed.
307//
308// The pattern draws in tiles at (0,0), (2,1), (4,2), (1,3), and (3,4) in each
309// 5x5 box.
310//
311// There may be a better way to do this, or a more appealing pattern.
313 TrackPanelDrawingContext &context, const wxRect &rect )
314{
315 const auto dc = &context.dc;
316
317 wxBitmap syncLockBitmap(theTheme.Image(bmpSyncLockSelTile));
318
319 // Grid spacing is a bit smaller than actual image size
320 int gridW = syncLockBitmap.GetWidth() - 6;
321 int gridH = syncLockBitmap.GetHeight() - 8;
322
323 // Horizontal position within the grid, modulo its period
324 int blockX = (rect.x / gridW) % 5;
325
326 // Amount to offset drawing of first column
327 int xOffset = rect.x % gridW;
328 if (xOffset < 0) xOffset += gridW;
329
330 // Check if we're missing an extra column to the left (this can happen
331 // because the tiles are bigger than the grid spacing)
332 bool extraCol = false;
333 if (syncLockBitmap.GetWidth() - gridW > xOffset) {
334 extraCol = true;
335 xOffset += gridW;
336 blockX = (blockX - 1) % 5;
337 }
338 // Make sure blockX is non-negative
339 if (blockX < 0) blockX += 5;
340
341 int xx = 0;
342 while (xx < rect.width) {
343 int width = syncLockBitmap.GetWidth() - xOffset;
344 if (xx + width > rect.width)
345 width = rect.width - xx;
346
347 //
348 // Draw each row in this column
349 //
350
351 // Vertical position in the grid, modulo its period
352 int blockY = (rect.y / gridH) % 5;
353
354 // Amount to offset drawing of first row
355 int yOffset = rect.y % gridH;
356 if (yOffset < 0) yOffset += gridH;
357
358 // Check if we're missing an extra row on top (this can happen because
359 // the tiles are bigger than the grid spacing)
360 bool extraRow = false;
361 if (syncLockBitmap.GetHeight() - gridH > yOffset) {
362 extraRow = true;
363 yOffset += gridH;
364 blockY = (blockY - 1) % 5;
365 }
366 // Make sure blockY is non-negative
367 if (blockY < 0) blockY += 5;
368
369 int yy = 0;
370 while (yy < rect.height)
371 {
372 int height = syncLockBitmap.GetHeight() - yOffset;
373 if (yy + height > rect.height)
374 height = rect.height - yy;
375
376 // AWD: draw blocks according to our pattern
377 if ((blockX == 0 && blockY == 0) || (blockX == 2 && blockY == 1) ||
378 (blockX == 4 && blockY == 2) || (blockX == 1 && blockY == 3) ||
379 (blockX == 3 && blockY == 4))
380 {
381
382 // Do we need to get a sub-bitmap?
383 if (width != syncLockBitmap.GetWidth() || height != syncLockBitmap.GetHeight()) {
384 wxBitmap subSyncLockBitmap =
385 syncLockBitmap.GetSubBitmap(wxRect(xOffset, yOffset, width, height));
386 dc->DrawBitmap(subSyncLockBitmap, rect.x + xx, rect.y + yy, true);
387 }
388 else {
389 dc->DrawBitmap(syncLockBitmap, rect.x + xx, rect.y + yy, true);
390 }
391 }
392
393 // Updates for next row
394 if (extraRow) {
395 // Second offset row, still at y = 0; no more extra rows
396 yOffset -= gridH;
397 extraRow = false;
398 }
399 else {
400 // Move on in y, no more offset rows
401 yy += gridH - yOffset;
402 yOffset = 0;
403 }
404 blockY = (blockY + 1) % 5;
405 }
406
407 // Updates for next column
408 if (extraCol) {
409 // Second offset column, still at x = 0; no more extra columns
410 xOffset -= gridW;
411 extraCol = false;
412 }
413 else {
414 // Move on in x, no more offset rows
415 xx += gridW - xOffset;
416 xOffset = 0;
417 }
418 blockX = (blockX + 1) % 5;
419 }
420}
421
423 TrackPanelDrawingContext &context, const wxRect &rect,
424 const Track *track, const wxBrush &selBrush, const wxBrush &unselBrush,
425 bool useSelection)
426{
427 const auto dc = &context.dc;
428 const auto artist = TrackArtist::Get( context );
429 const auto &selectedRegion = *artist->pSelectedRegion;
430 const auto &zoomInfo = *artist->pZoomInfo;
431
432 //MM: Draw background. We should optimize that a bit more.
433 const double sel0 = useSelection ? selectedRegion.t0() : 0.0;
434 const double sel1 = useSelection ? selectedRegion.t1() : 0.0;
435
436 dc->SetPen(*wxTRANSPARENT_PEN);
438 {
439 // Rectangles before, within, after the selection
440 wxRect before = rect;
441 wxRect within = rect;
442 wxRect after = rect;
443
444 before.width = (int)(zoomInfo.TimeToPosition(sel0) );
445 if (before.GetRight() > rect.GetRight()) {
446 before.width = rect.width;
447 }
448
449 if (before.width > 0) {
450 dc->SetBrush(unselBrush);
451 dc->DrawRectangle(before);
452
453 within.x = 1 + before.GetRight();
454 }
455 within.width = rect.x + (int)(zoomInfo.TimeToPosition(sel1) ) - within.x -1;
456
457 if (within.GetRight() > rect.GetRight()) {
458 within.width = 1 + rect.GetRight() - within.x;
459 }
460
461 // Bug 2389 - Selection can disappear
462 // This handles case where no waveform is visible.
463 if (within.width < 1)
464 {
465 within.width = 1;
466 }
467
468 if (within.width > 0) {
469 if (track->GetSelected()) {
470 dc->SetBrush(selBrush);
471 dc->DrawRectangle(within);
472 }
473 else {
474 // Per condition above, track must be sync-lock selected
475 dc->SetBrush(unselBrush);
476 dc->DrawRectangle(within);
477 DrawSyncLockTiles( context, within );
478 }
479
480 after.x = 1 + within.GetRight();
481 }
482 else {
483 // `within` not drawn; start where it would have gone
484 after.x = within.x;
485 }
486
487 after.width = 1 + rect.GetRight() - after.x;
488 if (after.width > 0) {
489 dc->SetBrush(unselBrush);
490 dc->DrawRectangle(after);
491 }
492 }
493 else
494 {
495 // Track not selected; just draw background
496 dc->SetBrush(unselBrush);
497 dc->DrawRectangle(rect);
498 }
499}
500
502 const wxRect& rect, const Track* track)
503{
504 const auto dc = &context.dc;
505 const auto artist = TrackArtist::Get(context);
506 const auto& selectedRegion = *artist->pSelectedRegion;
507
508 if (selectedRegion.isPoint())
509 {
510 const auto& zoomInfo = *artist->pZoomInfo;
511 auto x = static_cast<int>(zoomInfo.TimeToPosition(selectedRegion.t0(), rect.x));
512 if (x >= rect.GetLeft() && x <= rect.GetRight())
513 {
515 AColor::Line(*dc, x, rect.GetTop(), x, rect.GetBottom());
516 }
517 }
518}
static const wxPoint2DDouble outer[]
Definition: ASlider.cpp:397
int min(int a, int b)
#define str(a)
#define LINEAR_TO_DB(x)
Definition: MemoryX.h:561
#define DB_TO_LINEAR(x)
Definition: MemoryX.h:560
static const auto title
THEME_API Theme theTheme
Definition: Theme.cpp:82
declares abstract base class Track, TrackList, and iterators over TrackList
int GetWaveYPos(float value, float min, float max, int height, bool dB, bool outer, float dBr, bool clip)
Definition: TrackArt.cpp:46
float FromDB(float value, double dBRange)
Definition: TrackArt.cpp:93
float ValueOfPixel(int yy, int height, bool offset, bool dB, double dBRange, float zoomMin, float zoomMax)
Definition: TrackArt.cpp:102
static constexpr int ClipSelectionStrokeSize
Definition: TrackArt.cpp:25
bool within(A a, B b, DIST d)
Definition: TrackPanel.cpp:167
static void Line(wxDC &dc, wxCoord x1, wxCoord y1, wxCoord x2, wxCoord y2)
Definition: AColor.cpp:187
static void CursorColor(wxDC *dc)
Definition: AColor.cpp:463
static void UseThemeColour(wxDC *dc, int iBrush, int iPen=-1, int alpha=255)
Definition: AColor.cpp:372
static bool IsSelectedOrSyncLockSelected(const Track *pTrack)
Definition: SyncLock.cpp:112
wxImage & Image(int iIndex)
static TrackArtist * Get(TrackPanelDrawingContext &)
Definition: TrackArtist.cpp:69
Abstract base class for an object holding data associated with points on a time axis.
Definition: Track.h:226
bool GetSelected() const
Definition: Track.h:470
AUDACITY_DLL_API void DrawSyncLockTiles(TrackPanelDrawingContext &context, const wxRect &rect)
Definition: TrackArt.cpp:312
AUDACITY_DLL_API wxString TruncateText(wxDC &dc, const wxString &text, const int maxWidth)
Definition: TrackArt.cpp:153
AUDACITY_DLL_API void DrawClipFolded(wxDC &dc, const wxRect &rect)
Definition: TrackArt.cpp:293
AUDACITY_DLL_API void DrawClipEdges(wxDC &dc, const wxRect &clipRect, bool selected=false)
Definition: TrackArt.cpp:256
AUDACITY_DLL_API wxRect DrawClipAffordance(wxDC &dc, const wxRect &affordanceRect, bool highlight=false, bool selected=false)
Definition: TrackArt.cpp:188
AUDACITY_DLL_API void DrawBackgroundWithSelection(TrackPanelDrawingContext &context, const wxRect &rect, const Track *track, const wxBrush &selBrush, const wxBrush &unselBrush, bool useSelection=true)
Definition: TrackArt.cpp:422
AUDACITY_DLL_API void DrawCursor(TrackPanelDrawingContext &context, const wxRect &rect, const Track *track)
Definition: TrackArt.cpp:501
AUDACITY_DLL_API void DrawNegativeOffsetTrackArrows(TrackPanelDrawingContext &context, const wxRect &rect)
Definition: TrackArt.cpp:123
static constexpr int ClipFrameRadius
Definition: TrackArt.h:22
AUDACITY_DLL_API bool DrawClipTitle(wxDC &dc, const wxRect &titleRect, const wxString &title)
Definition: TrackArt.cpp:239
wxRect GetAffordanceTitleRect(const wxRect &rect)
Definition: TrackArt.cpp:32