AudioDial.cpp
Go to the documentation of this file.
1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
2 
3 /*
4  Sonic Visualiser
5  An audio file viewer and annotation editor.
6  Centre for Digital Music, Queen Mary, University of London.
7 
8  This program is free software; you can redistribute it and/or
9  modify it under the terms of the GNU General Public License as
10  published by the Free Software Foundation; either version 2 of the
11  License, or (at your option) any later version. See the file
12  COPYING included with this distribution for more information.
13 */
14 
38 #include "AudioDial.h"
39 
40 #include "base/RangeMapper.h"
41 
42 #include <cmath>
43 #include <iostream>
44 
45 #include <QTimer>
46 #include <QPainter>
47 #include <QPixmap>
48 #include <QColormap>
49 #include <QMouseEvent>
50 #include <QPaintEvent>
51 #include <QInputDialog>
52 #include <QMenu>
53 
54 #include "base/Profiler.h"
55 
56 #include "MenuTitle.h"
57 
58 
59 
60 
61 
63 
64 
65 //-------------------------------------------------------------------------
66 // AudioDial - Instance knob widget class.
67 //
68 
69 #define AUDIO_DIAL_MIN (0.25 * M_PI)
70 #define AUDIO_DIAL_MAX (1.75 * M_PI)
71 #define AUDIO_DIAL_RANGE (AUDIO_DIAL_MAX - AUDIO_DIAL_MIN)
72 
73 
74 
75 // Constructor.
76 AudioDial::AudioDial(QWidget *parent) :
77  QDial(parent),
78  m_knobColor(Qt::black), // shorthand for "background colour" in paint()
79  m_meterColor(Qt::white), // shorthand for "foreground colour" in paint()
80  m_defaultValue(0),
81  m_defaultMappedValue(0),
82  m_mappedValue(0),
83  m_noMappedUpdate(false),
84  m_showTooltip(true),
85  m_provideContextMenu(true),
86  m_lastContextMenu(nullptr),
87  m_rangeMapper(nullptr)
88 {
89  m_mouseDial = false;
90  m_mousePressed = false;
91  setContextMenuPolicy(Qt::CustomContextMenu);
92  connect(this, SIGNAL(customContextMenuRequested(const QPoint &)),
93  this, SLOT(contextMenuRequested(const QPoint &)));
94 }
95 
96 
97 // Destructor.
99 {
100  delete m_rangeMapper;
101  delete m_lastContextMenu;
102 }
103 
104 void AudioDial::contextMenuRequested(const QPoint &pos)
105 {
106  if (!m_provideContextMenu) {
107  return;
108  }
109 
110  delete m_lastContextMenu;
111  m_lastContextMenu = new QMenu;
112  auto m = m_lastContextMenu;
113 
114  if (m_title == "") {
115  MenuTitle::addTitle(m, tr("Dial"));
116  } else {
118  }
119 
120  m->addAction(tr("&Edit..."), this, SLOT(edit()));
121  m->addAction(tr("&Reset to Default"), this, SLOT(setToDefault()));
122 
123  m->popup(mapToGlobal(pos));
124 }
125 
126 void AudioDial::setRangeMapper(RangeMapper *mapper)
127 {
128  if (m_rangeMapper == mapper) return;
129 
130  if (!m_rangeMapper && mapper) {
131  connect(this, SIGNAL(valueChanged(int)),
132  this, SLOT(updateMappedValue(int)));
133  }
134 
135  delete m_rangeMapper;
136  m_rangeMapper = mapper;
137 
138  updateMappedValue(value());
139 }
140 
141 
142 void AudioDial::paintEvent(QPaintEvent *)
143 {
144  Profiler profiler("AudioDial::paintEvent");
145 
146  QPainter paint;
147 
148  double angle = AUDIO_DIAL_MIN // offset
149  + (AUDIO_DIAL_RANGE *
150  (double(QDial::value() - QDial::minimum()) /
151  (double(QDial::maximum() - QDial::minimum()))));
152  int degrees = int(angle * 180.0 / M_PI);
153 
154  int ns = notchSize();
155  int numTicks = 1 + (maximum() + ns - minimum()) / ns;
156 
157  QColor knobColor(m_knobColor);
158  if (knobColor == Qt::black) {
159  knobColor = palette().window().color().lighter(150);
160  }
161  bool knobIsDark =
162  (knobColor.red() + knobColor.green() + knobColor.blue() <= 384);
163 
164  QColor meterColor(m_meterColor);
165  if (!isEnabled()) {
166  meterColor = palette().mid().color();
167  } else if (m_meterColor == Qt::white) {
168  if (knobIsDark) {
169  meterColor = palette().text().color();
170  } else {
171  meterColor = palette().highlight().color();
172  }
173  }
174 
175  QColor notchColor(palette().dark().color());
176  if (knobIsDark) {
177  notchColor = palette().text().color();
178  }
179 
180  int m_size = width() < height() ? width() : height();
181  int scale = 1;
182  int width = m_size - 2*scale;
183 
184  paint.begin(this);
185  paint.setRenderHint(QPainter::Antialiasing, true);
186  paint.translate(1, 1);
187 
188  QPen pen;
189  QColor c;
190 
191  // Knob body and face...
192 
193  c = knobColor;
194  pen.setColor(knobColor);
195  pen.setWidth(scale * 2);
196  pen.setCapStyle(Qt::FlatCap);
197 
198  paint.setPen(pen);
199  paint.setBrush(c);
200 
201  int indent = (int)(width * 0.15 + 1);
202 
203  paint.drawEllipse(indent-1, indent-1, width-2*indent, width-2*indent);
204 
205  pen.setWidth(3 * scale);
206  int pos = indent-1 + (width-2*indent) / 20;
207  int darkWidth = (width-2*indent) * 3 / 4;
208  while (darkWidth) {
209  if (knobIsDark) {
210  c = c.darker(102);
211  } else {
212  c = c.lighter(102);
213  }
214  pen.setColor(c);
215  paint.setPen(pen);
216  paint.drawEllipse(pos, pos, darkWidth, darkWidth);
217  if (!--darkWidth) break;
218  paint.drawEllipse(pos, pos, darkWidth, darkWidth);
219  if (!--darkWidth) break;
220  paint.drawEllipse(pos, pos, darkWidth, darkWidth);
221  ++pos; --darkWidth;
222  }
223 
224  // Tick notches...
225 
226  if ( notchesVisible() ) {
227  pen.setColor(notchColor);
228  pen.setWidth(scale);
229  paint.setPen(pen);
230  for (int i = 0; i < numTicks; ++i) {
231  int div = numTicks;
232  if (div > 1) --div;
233  drawTick(paint, AUDIO_DIAL_MIN + (AUDIO_DIAL_MAX - AUDIO_DIAL_MIN) * i / div,
234  width, true);
235  }
236  }
237 
238  // The bright metering bit...
239 
240  c = meterColor;
241  pen.setColor(c);
242  pen.setWidth(indent);
243  paint.setPen(pen);
244 
245 // cerr << "degrees " << degrees << ", gives us " << -(degrees - 45) * 16 << endl;
246 
247  int arcLen = -(degrees - 45) * 16;
248  if (arcLen == 0) arcLen = -16;
249 
250  paint.drawArc(indent/2, indent/2,
251  width-indent, width-indent, (180 + 45) * 16, arcLen);
252 
253  paint.setBrush(Qt::NoBrush);
254 
255  // Shadowing...
256 
257  pen.setWidth(scale);
258  paint.setPen(pen);
259 
260  // Knob shadow...
261 
262  int shadowAngle = -720;
263  if (knobIsDark) {
264  c = knobColor.lighter();
265  } else {
266  c = knobColor.darker();
267  }
268  for (int arc = 120; arc < 2880; arc += 240) {
269  pen.setColor(c);
270  paint.setPen(pen);
271  paint.drawArc(indent, indent,
272  width-2*indent, width-2*indent, shadowAngle + arc, 240);
273  paint.drawArc(indent, indent,
274  width-2*indent, width-2*indent, shadowAngle - arc, 240);
275  if (knobIsDark) {
276  c = c.darker(110);
277  } else {
278  c = c.lighter(110);
279  }
280  }
281 
282  // Scale shadow, omitting the bottom part...
283 
284  shadowAngle = 2160;
285  c = palette().shadow().color();
286  for (int i = 0; i < 5; ++i) {
287  pen.setColor(c);
288  paint.setPen(pen);
289  int arc = i * 240 + 120;
290  paint.drawArc(scale/2, scale/2,
291  width-scale, width-scale, shadowAngle + arc, 240);
292  c = c.lighter(110);
293  }
294  c = palette().shadow().color();
295  for (int i = 0; i < 12; ++i) {
296  pen.setColor(c);
297  paint.setPen(pen);
298  int arc = i * 240 + 120;
299  paint.drawArc(scale/2, scale/2,
300  width-scale, width-scale, shadowAngle - arc, 240);
301  c = c.lighter(110);
302  }
303 
304  // Scale ends...
305 
306  if (knobIsDark) {
307  pen.setColor(palette().mid().color());
308  } else {
309  pen.setColor(palette().shadow().color());
310  }
311  pen.setWidth(scale);
312  paint.setPen(pen);
313  for (int i = 0; i < numTicks; ++i) {
314  if (i != 0 && i != numTicks - 1) continue;
315  int div = numTicks;
316  if (div > 1) --div;
317  drawTick(paint, AUDIO_DIAL_MIN + (AUDIO_DIAL_MAX - AUDIO_DIAL_MIN) * i / div,
318  width, false);
319  }
320 
321  // Pointer notch...
322 
323  double hyp = double(width) / 2.0;
324  double len = hyp - indent;
325  --len;
326 
327  double x0 = hyp;
328  double y0 = hyp;
329 
330  double x = hyp - len * sin(angle);
331  double y = hyp + len * cos(angle);
332 
333  c = notchColor;
334  if (isEnabled()) {
335  if (knobIsDark) {
336  c = c.lighter(130);
337  } else {
338  c = c.darker(130);
339  }
340  }
341  pen.setColor(c);
342  pen.setWidth(scale * 2);
343  paint.setPen(pen);
344  paint.drawLine(int(x0), int(y0), int(x), int(y));
345 
346  paint.end();
347 }
348 
349 
350 void AudioDial::drawTick(QPainter &paint,
351  double angle, int size, bool internal)
352 {
353  double hyp = double(size) / 2.0;
354  double x0 = hyp - (hyp - 1) * sin(angle);
355  double y0 = hyp + (hyp - 1) * cos(angle);
356 
357 // cerr << "drawTick: angle " << angle << ", size " << size << ", internal " << internal << endl;
358 
359  if (internal) {
360 
361  double len = hyp / 4;
362  double x1 = hyp - (hyp - len) * sin(angle);
363  double y1 = hyp + (hyp - len) * cos(angle);
364 
365  paint.drawLine(int(x0), int(y0), int(x1), int(y1));
366 
367  } else {
368 
369  double len = hyp / 4;
370  double x1 = hyp - (hyp + len) * sin(angle);
371  double y1 = hyp + (hyp + len) * cos(angle);
372 
373  paint.drawLine(int(x0), int(y0), int(x1), int(y1));
374  }
375 }
376 
377 
378 void AudioDial::setKnobColor(const QColor& color)
379 {
380  m_knobColor = color;
381  update();
382 }
383 
384 
385 void AudioDial::setMeterColor(const QColor& color)
386 {
387  m_meterColor = color;
388  update();
389 }
390 
391 
393 {
395 }
396 
397 
399 {
401  if (m_rangeMapper) {
402  m_defaultMappedValue = m_rangeMapper->getValueForPosition(defaultValue);
403  }
404 }
405 
406 void AudioDial::setValue(int value)
407 {
408  QDial::setValue(value);
409  updateMappedValue(value);
410 }
411 
413 {
414  m_defaultMappedValue = value;
415  if (m_rangeMapper) {
416  m_defaultValue = m_rangeMapper->getPositionForValue(value);
417  }
418 }
419 
421 {
422  if (m_rangeMapper) {
423  int newPosition = m_rangeMapper->getPositionForValue(mappedValue);
424  bool changed = (m_mappedValue != mappedValue);
426  m_noMappedUpdate = true;
427  SVDEBUG << "AudioDial::setMappedValue(" << mappedValue << "): new position is " << newPosition << endl;
428  if (newPosition != value()) {
429  setValue(newPosition);
430  } else if (changed) {
431  emit valueChanged(newPosition);
432  }
433  m_noMappedUpdate = false;
434  } else {
435  setValue(int(mappedValue));
436  }
437 }
438 
439 
441 {
442  m_showTooltip = show;
443  m_noMappedUpdate = true;
444  updateMappedValue(value());
445  m_noMappedUpdate = false;
446 }
447 
448 
450 {
451  m_provideContextMenu = provide;
452 }
453 
454 
456 {
457  if (m_rangeMapper) {
458 // SVDEBUG << "AudioDial::mappedValue(): value = " << value() << ", mappedValue = " << m_mappedValue << endl;
459  return m_mappedValue;
460  }
461  return value();
462 }
463 
464 
466 {
467  if (!m_noMappedUpdate) {
468  if (m_rangeMapper) {
469  m_mappedValue = m_rangeMapper->getValueForPosition(value);
470  } else {
471  m_mappedValue = value;
472  }
473  }
474 
475  QString name = objectName();
476  QString label;
477  if (m_rangeMapper) {
478  label = m_rangeMapper->getLabel(value);
479  }
480  QString text;
481  if (label != "") {
482  if (name != "") {
483  text = tr("%1: %2").arg(name).arg(label);
484  } else {
485  text = label;
486  }
487  } else {
488  QString unit = "";
489  if (m_rangeMapper) {
490  unit = m_rangeMapper->getUnit();
491  }
492  if (name != "") {
493  text = tr("%1: %2%3").arg(name).arg(m_mappedValue).arg(unit);
494  } else {
495  text = tr("%2%3").arg(m_mappedValue).arg(unit);
496  }
497  }
498 
499  m_title = text;
500 
501  if (m_showTooltip) {
502  setToolTip(text);
503  } else {
504  setToolTip("");
505  }
506 }
507 
508 void
510 {
511  if (m_rangeMapper) {
513  return;
514  }
515  int dv = m_defaultValue;
516  if (dv < minimum()) dv = minimum();
517  if (dv > maximum()) dv = maximum();
519 }
520 
521 // Alternate mouse behavior event handlers.
522 void AudioDial::mousePressEvent(QMouseEvent *mouseEvent)
523 {
524  if (m_mouseDial) {
525  QDial::mousePressEvent(mouseEvent);
526  } else if (mouseEvent->button() == Qt::MidButton ||
527  ((mouseEvent->button() == Qt::LeftButton) &&
528  (mouseEvent->modifiers() & Qt::ControlModifier))) {
529  setToDefault();
530  } else if (mouseEvent->button() == Qt::LeftButton) {
531  m_mousePressed = true;
532  m_posMouse = mouseEvent->pos();
533  }
534 }
535 
536 
537 void AudioDial::mouseDoubleClickEvent(QMouseEvent *mouseEvent)
538 {
540 
541  if (m_mouseDial) {
542  QDial::mouseDoubleClickEvent(mouseEvent);
543  } else if (mouseEvent->button() != Qt::LeftButton) {
544  return;
545  }
546 
547  edit();
548 }
549 
551 {
552  bool ok = false;
553 
554  if (m_rangeMapper) {
555 
556  double min = m_rangeMapper->getValueForPosition(minimum());
557  double max = m_rangeMapper->getValueForPosition(maximum());
558 
559  if (min > max) {
560  double tmp = min;
561  min = max;
562  max = tmp;
563  }
564 
565  QString unit = m_rangeMapper->getUnit();
566 
567  QString text;
568  if (objectName() != "") {
569  if (unit != "") {
570  text = tr("New value for %1, from %2 to %3 %4:")
571  .arg(objectName()).arg(min).arg(max).arg(unit);
572  } else {
573  text = tr("New value for %1, from %2 to %3:")
574  .arg(objectName()).arg(min).arg(max);
575  }
576  } else {
577  if (unit != "") {
578  text = tr("Enter a new value from %1 to %2 %3:")
579  .arg(min).arg(max).arg(unit);
580  } else {
581  text = tr("Enter a new value from %1 to %2:")
582  .arg(min).arg(max);
583  }
584  }
585 
586  double newValue = QInputDialog::getDouble
587  (this,
588  tr("Enter new value"),
589  text,
591  min,
592  max,
593  4,
594  &ok);
595 
596  if (ok) {
597  setMappedValue(newValue);
598  }
599 
600  } else {
601 
602  int newPosition = QInputDialog::getInt
603  (this,
604  tr("Enter new value"),
605  tr("Enter a new value from %1 to %2:")
606  .arg(minimum()).arg(maximum()),
607  value(), minimum(), maximum(), singleStep(), &ok);
608 
609  if (ok) {
610  setValue(newPosition);
611  }
612  }
613 }
614 
615 
616 void AudioDial::mouseMoveEvent(QMouseEvent *mouseEvent)
617 {
618  if (m_mouseDial) {
619  QDial::mouseMoveEvent(mouseEvent);
620  } else if (m_mousePressed) {
621  const QPoint& posMouse = mouseEvent->pos();
622  int v = QDial::value()
623  + (posMouse.x() - m_posMouse.x())
624  + (m_posMouse.y() - posMouse.y());
625  if (v > QDial::maximum())
626  v = QDial::maximum();
627  else
628  if (v < QDial::minimum())
629  v = QDial::minimum();
630  m_posMouse = posMouse;
631  QDial::setValue(v);
632  }
633 }
634 
635 
636 void AudioDial::mouseReleaseEvent(QMouseEvent *mouseEvent)
637 {
638  if (m_mouseDial) {
639  QDial::mouseReleaseEvent(mouseEvent);
640  } else if (m_mousePressed) {
641  m_mousePressed = false;
642  }
643 }
644 
645 void
647 {
648  QDial::enterEvent(e);
649  emit mouseEntered();
650 }
651 
652 void
654 {
655  QDial::enterEvent(e);
656  emit mouseLeft();
657 }
658 
void setMappedValue(double mappedValue)
Definition: AudioDial.cpp:420
#define AUDIO_DIAL_MIN
A rotary dial widget.
Definition: AudioDial.cpp:69
QPoint m_posMouse
Definition: AudioDial.h:148
void mouseMoveEvent(QMouseEvent *pMouseEvent) override
Definition: AudioDial.cpp:616
static void addTitle(QMenu *m, QString text)
Definition: MenuTitle.h:29
void mouseDoubleClickEvent(QMouseEvent *pMouseEvent) override
Definition: AudioDial.cpp:537
#define AUDIO_DIAL_MAX
Definition: AudioDial.cpp:70
bool m_mousePressed
Definition: AudioDial.h:147
void setDefaultValue(int defaultValue)
Definition: AudioDial.cpp:398
void setMeterColor(const QColor &color)
Set the colour of the meter (the highlighted area around the knob that shows the current value)...
Definition: AudioDial.cpp:385
double m_mappedValue
Definition: AudioDial.h:142
void setDefaultMappedValue(double mappedValue)
Definition: AudioDial.cpp:412
void mouseReleaseEvent(QMouseEvent *pMouseEvent) override
Definition: AudioDial.cpp:636
void enterEvent(QEvent *) override
Definition: AudioDial.cpp:646
void setMouseDial(bool mouseDial)
Specify that the dial should respond to radial mouse movements in the same way as QDial...
Definition: AudioDial.cpp:392
QColor meterColor
Definition: AudioDial.h:64
void setToDefault()
Definition: AudioDial.cpp:509
void setProvideContextMenu(bool provide)
Definition: AudioDial.cpp:449
void mousePressEvent(QMouseEvent *pMouseEvent) override
Definition: AudioDial.cpp:522
bool m_provideContextMenu
Definition: AudioDial.h:151
QString m_title
Definition: AudioDial.h:153
bool m_mouseDial
Definition: AudioDial.h:146
double mappedValue() const
Definition: AudioDial.cpp:455
QMenu * m_lastContextMenu
Definition: AudioDial.h:155
void leaveEvent(QEvent *) override
Definition: AudioDial.cpp:653
void paintEvent(QPaintEvent *) override
Definition: AudioDial.cpp:142
bool m_showTooltip
Definition: AudioDial.h:150
#define AUDIO_DIAL_RANGE
Definition: AudioDial.cpp:71
bool mouseDial
Definition: AudioDial.h:65
QColor knobColor
Definition: AudioDial.h:63
void setValue(int value)
Definition: AudioDial.cpp:406
int defaultValue() const
Definition: AudioDial.h:79
QColor m_knobColor
Definition: AudioDial.h:137
void mouseEntered()
int m_defaultValue
Definition: AudioDial.h:140
RangeMapper * m_rangeMapper
Definition: AudioDial.h:157
void edit()
Definition: AudioDial.cpp:550
AudioDial(QWidget *parent=0)
Definition: AudioDial.cpp:76
void mouseLeft()
void setKnobColor(const QColor &color)
Set the colour of the knob.
Definition: AudioDial.cpp:378
void setRangeMapper(RangeMapper *mapper)
Definition: AudioDial.cpp:126
void contextMenuRequested(const QPoint &)
Definition: AudioDial.cpp:104
void drawTick(QPainter &paint, double angle, int size, bool internal)
Definition: AudioDial.cpp:350
double m_defaultMappedValue
Definition: AudioDial.h:141
void updateMappedValue(int value)
Definition: AudioDial.cpp:465
QColor m_meterColor
Definition: AudioDial.h:138
bool m_noMappedUpdate
Definition: AudioDial.h:143
void setShowToolTip(bool show)
Definition: AudioDial.cpp:440