1 /**********************************************************************
2 
3   Audacity: A Digital Audio Editor
4 
5   EnvelopeEditor.cpp
6 
7   Paul Licameli split this from Envelope.cpp
8 
9 **********************************************************************/
10 
11 
12 #include "EnvelopeEditor.h"
13 
14 
15 
16 #include <wx/dc.h>
17 #include <wx/event.h>
18 
19 #include "AColor.h"
20 #include "Envelope.h"
21 #include "TrackArtist.h"
22 #include "TrackPanelDrawingContext.h"
23 #include "ViewInfo.h"
24 
25 namespace {
DrawPoint(wxDC & dc,const wxRect & r,int x,int y,bool top)26 void DrawPoint(wxDC & dc, const wxRect & r, int x, int y, bool top)
27 {
28    if (y >= 0 && y <= r.height) {
29       wxRect circle(r.x + x, r.y + (top ? y - 1: y - 2), 4, 4);
30       dc.DrawEllipse(circle);
31    }
32 }
33 }
34 
DrawPoints(const Envelope & env,TrackPanelDrawingContext & context,const wxRect & r,bool dB,double dBRange,float zoomMin,float zoomMax,bool mirrored,int origin)35 void EnvelopeEditor::DrawPoints(const Envelope &env,
36  TrackPanelDrawingContext &context, const wxRect & r,
37  bool dB, double dBRange,
38  float zoomMin, float zoomMax, bool mirrored, int origin)
39 {
40    auto &dc = context.dc;
41    const auto artist = TrackArtist::Get( context );
42    const auto &zoomInfo = *artist->pZoomInfo;
43 
44    bool highlight = false;
45 #ifdef EXPERIMENTAL_TRACK_PANEL_HIGHLIGHTING
46    auto target = dynamic_cast<EnvelopeHandle*>(context.target.get());
47    highlight = target && target->GetEnvelope() == this;
48 #endif
49    wxPen &pen = highlight ? AColor::uglyPen : AColor::envelopePen;
50    dc.SetPen( pen );
51    dc.SetBrush(*wxWHITE_BRUSH);
52 
53    for (int i = 0; i < (int)env.GetNumberOfPoints(); i++) {
54       const double time = env[i].GetT() + env.GetOffset();
55       const wxInt64 position = zoomInfo.TimeToPosition(time, origin);
56       if (position >= 0 && position < r.width) {
57          // Change colour if this is the draggable point...
58          if (i == env.GetDragPoint()) {
59             dc.SetPen( pen );
60             dc.SetBrush(AColor::envelopeBrush);
61          }
62 
63          double v = env[i].GetVal();
64          int x = (int)(position);
65          int y, y2;
66 
67          y = GetWaveYPos(v, zoomMin, zoomMax, r.height, dB,
68             true, dBRange, false);
69          if (!mirrored) {
70             DrawPoint(dc, r, x, y, true);
71          }
72          else {
73             y2 = GetWaveYPos(-v-.000000001, zoomMin, zoomMax, r.height, dB,
74                true, dBRange, false);
75 
76             // This follows the same logic as the envelop drawing in
77             // TrackArt::DrawEnvelope().
78             // TODO: make this calculation into a reusable function.
79             if (y2 - y < 9) {
80                int value = (int)((zoomMax / (zoomMax - zoomMin)) * r.height);
81                y = value - 4;
82                y2 = value + 4;
83             }
84 
85             DrawPoint(dc, r, x, y, true);
86             DrawPoint(dc, r, x, y2, false);
87 
88             // Contour
89             y = GetWaveYPos(v, zoomMin, zoomMax, r.height, dB,
90                false, dBRange, false);
91             y2 = GetWaveYPos(-v-.000000001, zoomMin, zoomMax, r.height, dB,
92                false, dBRange, false);
93             if (y <= y2) {
94                DrawPoint(dc, r, x, y, true);
95                DrawPoint(dc, r, x, y2, false);
96             }
97          }
98 
99          // Change colour back again if was the draggable point.
100          if (i == env.GetDragPoint()) {
101             dc.SetPen( pen );
102             dc.SetBrush(*wxWHITE_BRUSH);
103          }
104       }
105    }
106 }
107 
EnvelopeEditor(Envelope & envelope,bool mirrored)108 EnvelopeEditor::EnvelopeEditor(Envelope &envelope, bool mirrored)
109    : mEnvelope(envelope)
110    , mMirrored(mirrored)
111    , mContourOffset(-1)
112    // , mInitialVal(-1.0)
113    // , mInitialY(-1)
114    , mUpper(false)
115    , mButton(wxMOUSE_BTN_NONE)
116    , mDirty(false)
117 {
118 }
119 
~EnvelopeEditor()120 EnvelopeEditor::~EnvelopeEditor()
121 {
122 }
123 
124 namespace
125 {
SQR(int x)126 inline int SQR(int x) { return x * x; }
127 }
128 
129 /// ValueOfPixel() converts a y position on screen to an envelope value.
130 /// @param y - y position, usually of the mouse.relative to the clip.
131 /// @param height - height of the rectangle we are in.
132 /// @upper - true if we are on the upper line, false if on lower.
133 /// @dB - display mode either linear or log.
134 /// @zoomMin - vertical scale, typically -1.0
135 /// @zoomMax - vertical scale, typically +1.0
ValueOfPixel(int y,int height,bool upper,bool dB,double dBRange,float zoomMin,float zoomMax)136 float EnvelopeEditor::ValueOfPixel( int y, int height, bool upper,
137                               bool dB, double dBRange,
138                               float zoomMin, float zoomMax)
139 {
140    float v = ::ValueOfPixel(y, height, 0 != mContourOffset, dB, dBRange, zoomMin, zoomMax);
141 
142    // MB: this is mostly equivalent to what the old code did, I'm not sure
143    // if anything special is needed for asymmetric ranges
144    if(upper)
145       return mEnvelope.ClampValue(v);
146    else
147       return mEnvelope.ClampValue(-v);
148 }
149 
150 /// HandleMouseButtonDown either finds an existing control point or adds a NEW one
151 /// which is then recorded as the point to drag.
152 /// This is slightly complicated by there possibly being four control points for
153 /// a given time value:
154 /// We have an upper and lower envelope line.
155 /// Also we may be showing an inner envelope (at 0.5 the range).
HandleMouseButtonDown(const wxMouseEvent & event,wxRect & r,const ZoomInfo & zoomInfo,bool dB,double dBRange,float zoomMin,float zoomMax)156 bool EnvelopeEditor::HandleMouseButtonDown(const wxMouseEvent & event, wxRect & r,
157                                      const ZoomInfo &zoomInfo,
158                                      bool dB, double dBRange,
159                                      float zoomMin, float zoomMax)
160 {
161    int ctr = (int)(r.height * zoomMax / (zoomMax - zoomMin));
162    bool upper = !mMirrored || (zoomMin >= 0.0) || (event.m_y - r.y < ctr);
163 
164    int clip_y = event.m_y - r.y;
165    if(clip_y < 0) clip_y = 0; //keeps point in rect r, even if mouse isn't
166    if(clip_y > r.GetBottom()) clip_y = r.GetBottom();
167 
168    int bestNum = -1;
169    int bestDistSqr = 100; // Must be within 10 pixel radius.
170 
171    // Member variables hold state that will be needed in dragging.
172    mButton        = event.GetButton();
173    mContourOffset = false;
174 
175    //   wxLogDebug(wxT("Y:%i Height:%i Offset:%i"), y, height, mContourOffset );
176    int len = mEnvelope.GetNumberOfPoints();
177 
178    // TODO: extract this into a function FindNearestControlPoint()
179    // TODO: also fix it so that we can drag the last point on an envelope.
180    for (int i = 0; i < len; i++) { //search for control point nearest click
181       const double time = mEnvelope[i].GetT() + mEnvelope.GetOffset();
182       const wxInt64 position = zoomInfo.TimeToPosition(time);
183       if (position >= 0 && position < r.width) {
184 
185          int x = (int)(position);
186          int y[4];
187          int numControlPoints;
188 
189          // Outer control points
190          double value = mEnvelope[i].GetVal();
191          y[0] = GetWaveYPos(value, zoomMin, zoomMax, r.height,
192                                 dB, true, dBRange, false);
193          y[1] = GetWaveYPos(-value, zoomMin, zoomMax, r.height,
194                                 dB, true, dBRange, false);
195 
196          // Inner control points(contour)
197          y[2] = GetWaveYPos(value, zoomMin, zoomMax, r.height,
198                                 dB, false, dBRange, false);
199          y[3] = GetWaveYPos(-value -.00000001, zoomMin, zoomMax,
200                                 r.height, dB, false, dBRange, false);
201 
202          numControlPoints = 4;
203 
204          if (y[2] > y[3])
205             numControlPoints = 2;
206 
207          if (!mMirrored)
208             numControlPoints = 1;
209 
210          const int deltaXSquared = SQR(x - (event.m_x - r.x));
211          for(int j=0; j<numControlPoints; j++){
212 
213             const int dSqr = deltaXSquared + SQR(y[j] - (event.m_y - r.y));
214             if (dSqr < bestDistSqr) {
215                bestNum = i;
216                bestDistSqr = dSqr;
217                mContourOffset = (bool)(j > 1);
218             }
219          }
220       }
221    }
222 
223    if (bestNum >= 0) {
224       mEnvelope.SetDragPoint(bestNum);
225    }
226    else {
227       // TODO: Extract this into a function CreateNewPoint
228       const double when = zoomInfo.PositionToTime(event.m_x, r.x);
229 
230       //      if (when <= 0 || when >= mTrackLen)
231       //         return false;
232 
233       const double v = mEnvelope.GetValue( when );
234 
235       int ct = GetWaveYPos( v, zoomMin, zoomMax, r.height, dB,
236                                false, dBRange, false) ;
237       int cb = GetWaveYPos( -v-.000000001, zoomMin, zoomMax, r.height, dB,
238                                false, dBRange, false) ;
239       if (ct <= cb || !mMirrored) {
240          int t = GetWaveYPos( v, zoomMin, zoomMax, r.height, dB,
241                                  true, dBRange, false) ;
242          int b = GetWaveYPos( -v, zoomMin, zoomMax, r.height, dB,
243                                  true, dBRange, false) ;
244 
245          ct = (t + ct) / 2;
246          cb = (b + cb) / 2;
247 
248          if (mMirrored &&
249             (event.m_y - r.y) > ct &&
250             ((event.m_y - r.y) < cb))
251             mContourOffset = true;
252          else
253             mContourOffset = false;
254       }
255 
256       double newVal = ValueOfPixel(clip_y, r.height, upper, dB, dBRange,
257                                    zoomMin, zoomMax);
258 
259       mEnvelope.SetDragPoint(mEnvelope.InsertOrReplace(when, newVal));
260       mDirty = true;
261    }
262 
263    mUpper = upper;
264 
265    // const int dragPoint = mEnvelope.GetDragPoint();
266    // mInitialVal = mEnvelope[dragPoint].GetVal();
267    // mInitialY = event.m_y+mContourOffset;
268 
269    return true;
270 }
271 
MoveDragPoint(const wxMouseEvent & event,wxRect & r,const ZoomInfo & zoomInfo,bool dB,double dBRange,float zoomMin,float zoomMax)272 void EnvelopeEditor::MoveDragPoint(const wxMouseEvent & event, wxRect & r,
273                                const ZoomInfo &zoomInfo, bool dB, double dBRange,
274                                float zoomMin, float zoomMax)
275 {
276    int clip_y = event.m_y - r.y;
277    if(clip_y < 0) clip_y = 0;
278    if(clip_y > r.height) clip_y = r.height;
279    double newVal = ValueOfPixel(clip_y, r.height, mUpper, dB, dBRange,
280                                 zoomMin, zoomMax);
281 
282    // We no longer tolerate multiple envelope points at the same t.
283    // epsilon is less than the time offset of a single sample
284    // TODO: However because mTrackEpsilon assumes 200KHz this use
285    // of epsilon is a tad bogus.  What we need to do instead is DELETE
286    // a duplicated point on a mouse up.
287    double newWhen = zoomInfo.PositionToTime(event.m_x, r.x) - mEnvelope.GetOffset();
288    mEnvelope.MoveDragPoint(newWhen, newVal);
289 }
290 
HandleDragging(const wxMouseEvent & event,wxRect & r,const ZoomInfo & zoomInfo,bool dB,double dBRange,float zoomMin,float zoomMax,float WXUNUSED (eMin),float WXUNUSED (eMax))291 bool EnvelopeEditor::HandleDragging(const wxMouseEvent & event, wxRect & r,
292                                const ZoomInfo &zoomInfo, bool dB, double dBRange,
293                                float zoomMin, float zoomMax,
294                                float WXUNUSED(eMin), float WXUNUSED(eMax))
295 {
296    mDirty = true;
297 
298    wxRect larger = r;
299    larger.Inflate(10, 10);
300 
301    if (larger.Contains(event.m_x, event.m_y))
302    {
303       // IF we're in the rect THEN we're not deleting this point (anymore).
304       // ...we're dragging it.
305       MoveDragPoint( event, r, zoomInfo, dB, dBRange, zoomMin, zoomMax);
306       return true;
307    }
308 
309    if(!mEnvelope.GetDragPointValid())
310       // IF we already know we're deleting THEN no envelope point to update.
311       return false;
312 
313    // Invalidate the point
314    mEnvelope.SetDragPointValid(false);
315    return true;
316 }
317 
318 // Exit dragging mode and delete dragged point if necessary.
HandleMouseButtonUp()319 bool EnvelopeEditor::HandleMouseButtonUp()
320 {
321    mEnvelope.ClearDragPoint();
322    mButton = wxMOUSE_BTN_NONE;
323    return true;
324 }
325 
326 // Returns true if parent needs to be redrawn
MouseEvent(const wxMouseEvent & event,wxRect & r,const ZoomInfo & zoomInfo,bool dB,double dBRange,float zoomMin,float zoomMax)327 bool EnvelopeEditor::MouseEvent(const wxMouseEvent & event, wxRect & r,
328                           const ZoomInfo &zoomInfo, bool dB, double dBRange,
329                           float zoomMin, float zoomMax)
330 {
331    if (event.ButtonDown() && mButton == wxMOUSE_BTN_NONE)
332       return HandleMouseButtonDown( event, r, zoomInfo, dB, dBRange,
333                                     zoomMin, zoomMax);
334    if (event.Dragging() && mEnvelope.GetDragPoint() >= 0)
335       return HandleDragging( event, r, zoomInfo, dB, dBRange,
336                              zoomMin, zoomMax);
337    if (event.ButtonUp() && event.GetButton() == mButton)
338       return HandleMouseButtonUp();
339    return false;
340 }
341