1 /*
2   ==============================================================================
3 
4    This file is part of the JUCE library.
5    Copyright (c) 2020 - Raw Material Software Limited
6 
7    JUCE is an open source library subject to commercial or open-source
8    licensing.
9 
10    By using JUCE, you agree to the terms of both the JUCE 6 End-User License
11    Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
12 
13    End User License Agreement: www.juce.com/juce-6-licence
14    Privacy Policy: www.juce.com/juce-privacy-policy
15 
16    Or: You may also use this code under the terms of the GPL v3 (see
17    www.gnu.org/licenses).
18 
19    JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20    EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21    DISCLAIMED.
22 
23   ==============================================================================
24 */
25 
26 namespace juce
27 {
28 
29 class Slider::Pimpl   : public AsyncUpdater, // this needs to be public otherwise it will cause an
30                                              // error when JUCE_DLL_BUILD=1
31                         private Value::Listener
32 {
33 public:
Pimpl(Slider & s,SliderStyle sliderStyle,TextEntryBoxPosition textBoxPosition)34     Pimpl (Slider& s, SliderStyle sliderStyle, TextEntryBoxPosition textBoxPosition)
35       : owner (s),
36         style (sliderStyle),
37         textBoxPos (textBoxPosition)
38     {
39         rotaryParams.startAngleRadians = MathConstants<float>::pi * 1.2f;
40         rotaryParams.endAngleRadians   = MathConstants<float>::pi * 2.8f;
41         rotaryParams.stopAtEnd = true;
42     }
43 
~Pimpl()44     ~Pimpl() override
45     {
46         currentValue.removeListener (this);
47         valueMin.removeListener (this);
48         valueMax.removeListener (this);
49         popupDisplay.reset();
50     }
51 
52     //==============================================================================
registerListeners()53     void registerListeners()
54     {
55         currentValue.addListener (this);
56         valueMin.addListener (this);
57         valueMax.addListener (this);
58     }
59 
isHorizontal() const60     bool isHorizontal() const noexcept
61     {
62         return style == LinearHorizontal
63             || style == LinearBar
64             || style == TwoValueHorizontal
65             || style == ThreeValueHorizontal;
66     }
67 
isVertical() const68     bool isVertical() const noexcept
69     {
70         return style == LinearVertical
71             || style == LinearBarVertical
72             || style == TwoValueVertical
73             || style == ThreeValueVertical;
74     }
75 
isRotary() const76     bool isRotary() const noexcept
77     {
78         return style == Rotary
79             || style == RotaryHorizontalDrag
80             || style == RotaryVerticalDrag
81             || style == RotaryHorizontalVerticalDrag;
82     }
83 
isBar() const84     bool isBar() const noexcept
85     {
86         return style == LinearBar
87             || style == LinearBarVertical;
88     }
89 
isTwoValue() const90     bool isTwoValue() const noexcept
91     {
92         return style == TwoValueHorizontal
93             || style == TwoValueVertical;
94     }
95 
isThreeValue() const96     bool isThreeValue() const noexcept
97     {
98         return style == ThreeValueHorizontal
99             || style == ThreeValueVertical;
100     }
101 
incDecDragDirectionIsHorizontal() const102     bool incDecDragDirectionIsHorizontal() const noexcept
103     {
104         return incDecButtonMode == incDecButtonsDraggable_Horizontal
105                 || (incDecButtonMode == incDecButtonsDraggable_AutoDirection && incDecButtonsSideBySide);
106     }
107 
getPositionOfValue(double value) const108     float getPositionOfValue (double value) const
109     {
110         if (isHorizontal() || isVertical())
111             return getLinearSliderPos (value);
112 
113         jassertfalse; // not a valid call on a slider that doesn't work linearly!
114         return 0.0f;
115     }
116 
updateRange()117     void updateRange()
118     {
119         // figure out the number of DPs needed to display all values at this
120         // interval setting.
121         numDecimalPlaces = 7;
122 
123         if (normRange.interval != 0.0)
124         {
125             int v = std::abs (roundToInt (normRange.interval * 10000000));
126 
127             while ((v % 10) == 0 && numDecimalPlaces > 0)
128             {
129                 --numDecimalPlaces;
130                 v /= 10;
131             }
132         }
133 
134         // keep the current values inside the new range..
135         if (style != TwoValueHorizontal && style != TwoValueVertical)
136         {
137             setValue (getValue(), dontSendNotification);
138         }
139         else
140         {
141             setMinValue (getMinValue(), dontSendNotification, false);
142             setMaxValue (getMaxValue(), dontSendNotification, false);
143         }
144 
145         updateText();
146     }
147 
setRange(double newMin,double newMax,double newInt)148     void setRange (double newMin, double newMax, double newInt)
149     {
150         normRange = NormalisableRange<double> (newMin, newMax, newInt,
151                                                normRange.skew, normRange.symmetricSkew);
152         updateRange();
153     }
154 
setNormalisableRange(NormalisableRange<double> newRange)155     void setNormalisableRange (NormalisableRange<double> newRange)
156     {
157         normRange = newRange;
158         updateRange();
159     }
160 
getValue() const161     double getValue() const
162     {
163         // for a two-value style slider, you should use the getMinValue() and getMaxValue()
164         // methods to get the two values.
165         jassert (style != TwoValueHorizontal && style != TwoValueVertical);
166 
167         return currentValue.getValue();
168     }
169 
setValue(double newValue,NotificationType notification)170     void setValue (double newValue, NotificationType notification)
171     {
172         // for a two-value style slider, you should use the setMinValue() and setMaxValue()
173         // methods to set the two values.
174         jassert (style != TwoValueHorizontal && style != TwoValueVertical);
175 
176         newValue = constrainedValue (newValue);
177 
178         if (style == ThreeValueHorizontal || style == ThreeValueVertical)
179         {
180             jassert (static_cast<double> (valueMin.getValue()) <= static_cast<double> (valueMax.getValue()));
181 
182             newValue = jlimit (static_cast<double> (valueMin.getValue()),
183                                static_cast<double> (valueMax.getValue()),
184                                newValue);
185         }
186 
187         if (newValue != lastCurrentValue)
188         {
189             if (valueBox != nullptr)
190                 valueBox->hideEditor (true);
191 
192             lastCurrentValue = newValue;
193 
194             // (need to do this comparison because the Value will use equalsWithSameType to compare
195             // the new and old values, so will generate unwanted change events if the type changes)
196             if (currentValue != newValue)
197                 currentValue = newValue;
198 
199             updateText();
200             owner.repaint();
201             updatePopupDisplay (newValue);
202 
203             triggerChangeMessage (notification);
204         }
205     }
206 
setMinValue(double newValue,NotificationType notification,bool allowNudgingOfOtherValues)207     void setMinValue (double newValue, NotificationType notification, bool allowNudgingOfOtherValues)
208     {
209         // The minimum value only applies to sliders that are in two- or three-value mode.
210         jassert (style == TwoValueHorizontal || style == TwoValueVertical
211                   || style == ThreeValueHorizontal || style == ThreeValueVertical);
212 
213         newValue = constrainedValue (newValue);
214 
215         if (style == TwoValueHorizontal || style == TwoValueVertical)
216         {
217             if (allowNudgingOfOtherValues && newValue > static_cast<double> (valueMax.getValue()))
218                 setMaxValue (newValue, notification, false);
219 
220             newValue = jmin (static_cast<double> (valueMax.getValue()), newValue);
221         }
222         else
223         {
224             if (allowNudgingOfOtherValues && newValue > lastCurrentValue)
225                 setValue (newValue, notification);
226 
227             newValue = jmin (lastCurrentValue, newValue);
228         }
229 
230         if (lastValueMin != newValue)
231         {
232             lastValueMin = newValue;
233             valueMin = newValue;
234             owner.repaint();
235             updatePopupDisplay (newValue);
236 
237             triggerChangeMessage (notification);
238         }
239     }
240 
setMaxValue(double newValue,NotificationType notification,bool allowNudgingOfOtherValues)241     void setMaxValue (double newValue, NotificationType notification, bool allowNudgingOfOtherValues)
242     {
243         // The maximum value only applies to sliders that are in two- or three-value mode.
244         jassert (style == TwoValueHorizontal || style == TwoValueVertical
245                   || style == ThreeValueHorizontal || style == ThreeValueVertical);
246 
247         newValue = constrainedValue (newValue);
248 
249         if (style == TwoValueHorizontal || style == TwoValueVertical)
250         {
251             if (allowNudgingOfOtherValues && newValue < static_cast<double> (valueMin.getValue()))
252                 setMinValue (newValue, notification, false);
253 
254             newValue = jmax (static_cast<double> (valueMin.getValue()), newValue);
255         }
256         else
257         {
258             if (allowNudgingOfOtherValues && newValue < lastCurrentValue)
259                 setValue (newValue, notification);
260 
261             newValue = jmax (lastCurrentValue, newValue);
262         }
263 
264         if (lastValueMax != newValue)
265         {
266             lastValueMax = newValue;
267             valueMax = newValue;
268             owner.repaint();
269             updatePopupDisplay (valueMax.getValue());
270 
271             triggerChangeMessage (notification);
272         }
273     }
274 
setMinAndMaxValues(double newMinValue,double newMaxValue,NotificationType notification)275     void setMinAndMaxValues (double newMinValue, double newMaxValue, NotificationType notification)
276     {
277         // The maximum value only applies to sliders that are in two- or three-value mode.
278         jassert (style == TwoValueHorizontal || style == TwoValueVertical
279                   || style == ThreeValueHorizontal || style == ThreeValueVertical);
280 
281         if (newMaxValue < newMinValue)
282             std::swap (newMaxValue, newMinValue);
283 
284         newMinValue = constrainedValue (newMinValue);
285         newMaxValue = constrainedValue (newMaxValue);
286 
287         if (lastValueMax != newMaxValue || lastValueMin != newMinValue)
288         {
289             lastValueMax = newMaxValue;
290             lastValueMin = newMinValue;
291             valueMin = newMinValue;
292             valueMax = newMaxValue;
293             owner.repaint();
294 
295             triggerChangeMessage (notification);
296         }
297     }
298 
getMinValue() const299     double getMinValue() const
300     {
301         // The minimum value only applies to sliders that are in two- or three-value mode.
302         jassert (style == TwoValueHorizontal || style == TwoValueVertical
303                   || style == ThreeValueHorizontal || style == ThreeValueVertical);
304 
305         return valueMin.getValue();
306     }
307 
getMaxValue() const308     double getMaxValue() const
309     {
310         // The maximum value only applies to sliders that are in two- or three-value mode.
311         jassert (style == TwoValueHorizontal || style == TwoValueVertical
312                   || style == ThreeValueHorizontal || style == ThreeValueVertical);
313 
314         return valueMax.getValue();
315     }
316 
triggerChangeMessage(NotificationType notification)317     void triggerChangeMessage (NotificationType notification)
318     {
319         if (notification != dontSendNotification)
320         {
321             owner.valueChanged();
322 
323             if (notification == sendNotificationSync)
324                 handleAsyncUpdate();
325             else
326                 triggerAsyncUpdate();
327         }
328     }
329 
handleAsyncUpdate()330     void handleAsyncUpdate() override
331     {
332         cancelPendingUpdate();
333 
334         Component::BailOutChecker checker (&owner);
335         listeners.callChecked (checker, [&] (Slider::Listener& l) { l.sliderValueChanged (&owner); });
336 
337         if (checker.shouldBailOut())
338             return;
339 
340         if (owner.onValueChange != nullptr)
341             owner.onValueChange();
342     }
343 
sendDragStart()344     void sendDragStart()
345     {
346         owner.startedDragging();
347 
348         Component::BailOutChecker checker (&owner);
349         listeners.callChecked (checker, [&] (Slider::Listener& l) { l.sliderDragStarted (&owner); });
350 
351         if (checker.shouldBailOut())
352             return;
353 
354         if (owner.onDragStart != nullptr)
355             owner.onDragStart();
356     }
357 
sendDragEnd()358     void sendDragEnd()
359     {
360         owner.stoppedDragging();
361         sliderBeingDragged = -1;
362 
363         Component::BailOutChecker checker (&owner);
364         listeners.callChecked (checker, [&] (Slider::Listener& l) { l.sliderDragEnded (&owner); });
365 
366         if (checker.shouldBailOut())
367             return;
368 
369         if (owner.onDragEnd != nullptr)
370             owner.onDragEnd();
371     }
372 
373     struct DragInProgress
374     {
DragInProgressjuce::Slider::Pimpl::DragInProgress375         DragInProgress (Pimpl& p)  : owner (p)      { owner.sendDragStart(); }
~DragInProgressjuce::Slider::Pimpl::DragInProgress376         ~DragInProgress()                           { owner.sendDragEnd(); }
377 
378         Pimpl& owner;
379 
380         JUCE_DECLARE_NON_COPYABLE (DragInProgress)
381     };
382 
incrementOrDecrement(double delta)383     void incrementOrDecrement (double delta)
384     {
385         if (style == IncDecButtons)
386         {
387             auto newValue = owner.snapValue (getValue() + delta, notDragging);
388 
389             if (currentDrag != nullptr)
390             {
391                 setValue (newValue, sendNotificationSync);
392             }
393             else
394             {
395                 DragInProgress drag (*this);
396                 setValue (newValue, sendNotificationSync);
397             }
398         }
399     }
400 
valueChanged(Value & value)401     void valueChanged (Value& value) override
402     {
403         if (value.refersToSameSourceAs (currentValue))
404         {
405             if (style != TwoValueHorizontal && style != TwoValueVertical)
406                 setValue (currentValue.getValue(), dontSendNotification);
407         }
408         else if (value.refersToSameSourceAs (valueMin))
409             setMinValue (valueMin.getValue(), dontSendNotification, true);
410         else if (value.refersToSameSourceAs (valueMax))
411             setMaxValue (valueMax.getValue(), dontSendNotification, true);
412     }
413 
textChanged()414     void textChanged()
415     {
416         auto newValue = owner.snapValue (owner.getValueFromText (valueBox->getText()), notDragging);
417 
418         if (newValue != static_cast<double> (currentValue.getValue()))
419         {
420             DragInProgress drag (*this);
421             setValue (newValue, sendNotificationSync);
422         }
423 
424         updateText(); // force a clean-up of the text, needed in case setValue() hasn't done this.
425     }
426 
updateText()427     void updateText()
428     {
429         if (valueBox != nullptr)
430         {
431             auto newValue = owner.getTextFromValue (currentValue.getValue());
432 
433             if (newValue != valueBox->getText())
434                 valueBox->setText (newValue, dontSendNotification);
435         }
436     }
437 
constrainedValue(double value) const438     double constrainedValue (double value) const
439     {
440         return normRange.snapToLegalValue (value);
441     }
442 
getLinearSliderPos(double value) const443     float getLinearSliderPos (double value) const
444     {
445         double pos;
446 
447         if (normRange.end <= normRange.start)
448             pos = 0.5;
449         else if (value < normRange.start)
450             pos = 0.0;
451         else if (value > normRange.end)
452             pos = 1.0;
453         else
454             pos = owner.valueToProportionOfLength (value);
455 
456         if (isVertical() || style == IncDecButtons)
457             pos = 1.0 - pos;
458 
459         jassert (pos >= 0 && pos <= 1.0);
460         return (float) (sliderRegionStart + pos * sliderRegionSize);
461     }
462 
setSliderStyle(SliderStyle newStyle)463     void setSliderStyle (SliderStyle newStyle)
464     {
465         if (style != newStyle)
466         {
467             style = newStyle;
468             owner.repaint();
469             owner.lookAndFeelChanged();
470         }
471     }
472 
setVelocityModeParameters(double sensitivity,int threshold,double offset,bool userCanPressKeyToSwapMode,ModifierKeys::Flags newModifierToSwapModes)473     void setVelocityModeParameters (double sensitivity, int threshold,
474                                     double offset, bool userCanPressKeyToSwapMode,
475                                     ModifierKeys::Flags newModifierToSwapModes)
476     {
477         velocityModeSensitivity = sensitivity;
478         velocityModeOffset = offset;
479         velocityModeThreshold = threshold;
480         userKeyOverridesVelocity = userCanPressKeyToSwapMode;
481         modifierToSwapModes = newModifierToSwapModes;
482     }
483 
setIncDecButtonsMode(IncDecButtonMode mode)484     void setIncDecButtonsMode (IncDecButtonMode mode)
485     {
486         if (incDecButtonMode != mode)
487         {
488             incDecButtonMode = mode;
489             owner.lookAndFeelChanged();
490         }
491     }
492 
setTextBoxStyle(TextEntryBoxPosition newPosition,bool isReadOnly,int textEntryBoxWidth,int textEntryBoxHeight)493     void setTextBoxStyle (TextEntryBoxPosition newPosition,
494                           bool isReadOnly,
495                           int textEntryBoxWidth,
496                           int textEntryBoxHeight)
497     {
498         if (textBoxPos != newPosition
499              || editableText != (! isReadOnly)
500              || textBoxWidth != textEntryBoxWidth
501              || textBoxHeight != textEntryBoxHeight)
502         {
503             textBoxPos = newPosition;
504             editableText = ! isReadOnly;
505             textBoxWidth = textEntryBoxWidth;
506             textBoxHeight = textEntryBoxHeight;
507 
508             owner.repaint();
509             owner.lookAndFeelChanged();
510         }
511     }
512 
setTextBoxIsEditable(bool shouldBeEditable)513     void setTextBoxIsEditable (bool shouldBeEditable)
514     {
515         editableText = shouldBeEditable;
516         updateTextBoxEnablement();
517     }
518 
showTextBox()519     void showTextBox()
520     {
521         jassert (editableText); // this should probably be avoided in read-only sliders.
522 
523         if (valueBox != nullptr)
524             valueBox->showEditor();
525     }
526 
hideTextBox(bool discardCurrentEditorContents)527     void hideTextBox (bool discardCurrentEditorContents)
528     {
529         if (valueBox != nullptr)
530         {
531             valueBox->hideEditor (discardCurrentEditorContents);
532 
533             if (discardCurrentEditorContents)
534                 updateText();
535         }
536     }
537 
setTextValueSuffix(const String & suffix)538     void setTextValueSuffix (const String& suffix)
539     {
540         if (textSuffix != suffix)
541         {
542             textSuffix = suffix;
543             updateText();
544         }
545     }
546 
updateTextBoxEnablement()547     void updateTextBoxEnablement()
548     {
549         if (valueBox != nullptr)
550         {
551             bool shouldBeEditable = editableText && owner.isEnabled();
552 
553             if (valueBox->isEditable() != shouldBeEditable) // (to avoid changing the single/double click flags unless we need to)
554                 valueBox->setEditable (shouldBeEditable);
555         }
556     }
557 
lookAndFeelChanged(LookAndFeel & lf)558     void lookAndFeelChanged (LookAndFeel& lf)
559     {
560         if (textBoxPos != NoTextBox)
561         {
562             auto previousTextBoxContent = (valueBox != nullptr ? valueBox->getText()
563                                                                : owner.getTextFromValue (currentValue.getValue()));
564 
565             valueBox.reset();
566             valueBox.reset (lf.createSliderTextBox (owner));
567             owner.addAndMakeVisible (valueBox.get());
568 
569             valueBox->setWantsKeyboardFocus (false);
570             valueBox->setText (previousTextBoxContent, dontSendNotification);
571             valueBox->setTooltip (owner.getTooltip());
572             updateTextBoxEnablement();
573             valueBox->onTextChange = [this] { textChanged(); };
574 
575             if (style == LinearBar || style == LinearBarVertical)
576             {
577                 valueBox->addMouseListener (&owner, false);
578                 valueBox->setMouseCursor (MouseCursor::ParentCursor);
579             }
580         }
581         else
582         {
583             valueBox.reset();
584         }
585 
586         if (style == IncDecButtons)
587         {
588             incButton.reset (lf.createSliderButton (owner, true));
589             decButton.reset (lf.createSliderButton (owner, false));
590 
591             owner.addAndMakeVisible (incButton.get());
592             owner.addAndMakeVisible (decButton.get());
593 
594             incButton->onClick = [this] { incrementOrDecrement (normRange.interval); };
595             decButton->onClick = [this] { incrementOrDecrement (-normRange.interval); };
596 
597             if (incDecButtonMode != incDecButtonsNotDraggable)
598             {
599                 incButton->addMouseListener (&owner, false);
600                 decButton->addMouseListener (&owner, false);
601             }
602             else
603             {
604                 incButton->setRepeatSpeed (300, 100, 20);
605                 decButton->setRepeatSpeed (300, 100, 20);
606             }
607 
608             auto tooltip = owner.getTooltip();
609             incButton->setTooltip (tooltip);
610             decButton->setTooltip (tooltip);
611         }
612         else
613         {
614             incButton.reset();
615             decButton.reset();
616         }
617 
618         owner.setComponentEffect (lf.getSliderEffect (owner));
619 
620         owner.resized();
621         owner.repaint();
622     }
623 
showPopupMenu()624     void showPopupMenu()
625     {
626         PopupMenu m;
627         m.setLookAndFeel (&owner.getLookAndFeel());
628         m.addItem (1, TRANS ("Velocity-sensitive mode"), true, isVelocityBased);
629         m.addSeparator();
630 
631         if (isRotary())
632         {
633             PopupMenu rotaryMenu;
634             rotaryMenu.addItem (2, TRANS ("Use circular dragging"),           true, style == Rotary);
635             rotaryMenu.addItem (3, TRANS ("Use left-right dragging"),         true, style == RotaryHorizontalDrag);
636             rotaryMenu.addItem (4, TRANS ("Use up-down dragging"),            true, style == RotaryVerticalDrag);
637             rotaryMenu.addItem (5, TRANS ("Use left-right/up-down dragging"), true, style == RotaryHorizontalVerticalDrag);
638 
639             m.addSubMenu (TRANS ("Rotary mode"), rotaryMenu);
640         }
641 
642         m.showMenuAsync (PopupMenu::Options(),
643                          ModalCallbackFunction::forComponent (sliderMenuCallback, &owner));
644     }
645 
sliderMenuCallback(int result,Slider * slider)646     static void sliderMenuCallback (int result, Slider* slider)
647     {
648         if (slider != nullptr)
649         {
650             switch (result)
651             {
652                 case 1:   slider->setVelocityBasedMode (! slider->getVelocityBasedMode()); break;
653                 case 2:   slider->setSliderStyle (Rotary); break;
654                 case 3:   slider->setSliderStyle (RotaryHorizontalDrag); break;
655                 case 4:   slider->setSliderStyle (RotaryVerticalDrag); break;
656                 case 5:   slider->setSliderStyle (RotaryHorizontalVerticalDrag); break;
657                 default:  break;
658             }
659         }
660     }
661 
getThumbIndexAt(const MouseEvent & e)662     int getThumbIndexAt (const MouseEvent& e)
663     {
664         if (isTwoValue() || isThreeValue())
665         {
666             auto mousePos = isVertical() ? e.position.y : e.position.x;
667 
668             auto normalPosDistance = std::abs (getLinearSliderPos (currentValue.getValue()) - mousePos);
669             auto minPosDistance    = std::abs (getLinearSliderPos (valueMin.getValue()) + (isVertical() ? 0.1f : -0.1f) - mousePos);
670             auto maxPosDistance    = std::abs (getLinearSliderPos (valueMax.getValue()) + (isVertical() ? -0.1f : 0.1f) - mousePos);
671 
672             if (isTwoValue())
673                 return maxPosDistance <= minPosDistance ? 2 : 1;
674 
675             if (normalPosDistance >= minPosDistance && maxPosDistance >= minPosDistance)
676                 return 1;
677 
678             if (normalPosDistance >= maxPosDistance)
679                 return 2;
680         }
681 
682         return 0;
683     }
684 
685     //==============================================================================
handleRotaryDrag(const MouseEvent & e)686     void handleRotaryDrag (const MouseEvent& e)
687     {
688         auto dx = e.position.x - (float) sliderRect.getCentreX();
689         auto dy = e.position.y - (float) sliderRect.getCentreY();
690 
691         if (dx * dx + dy * dy > 25.0f)
692         {
693             auto angle = std::atan2 ((double) dx, (double) -dy);
694 
695             while (angle < 0.0)
696                 angle += MathConstants<double>::twoPi;
697 
698             if (rotaryParams.stopAtEnd && e.mouseWasDraggedSinceMouseDown())
699             {
700                 if (std::abs (angle - lastAngle) > MathConstants<double>::pi)
701                 {
702                     if (angle >= lastAngle)
703                         angle -= MathConstants<double>::twoPi;
704                     else
705                         angle += MathConstants<double>::twoPi;
706                 }
707 
708                 if (angle >= lastAngle)
709                     angle = jmin (angle, (double) jmax (rotaryParams.startAngleRadians, rotaryParams.endAngleRadians));
710                 else
711                     angle = jmax (angle, (double) jmin (rotaryParams.startAngleRadians, rotaryParams.endAngleRadians));
712             }
713             else
714             {
715                 while (angle < rotaryParams.startAngleRadians)
716                     angle += MathConstants<double>::twoPi;
717 
718                 if (angle > rotaryParams.endAngleRadians)
719                 {
720                     if (smallestAngleBetween (angle, rotaryParams.startAngleRadians)
721                          <= smallestAngleBetween (angle, rotaryParams.endAngleRadians))
722                         angle = rotaryParams.startAngleRadians;
723                     else
724                         angle = rotaryParams.endAngleRadians;
725                 }
726             }
727 
728             auto proportion = (angle - rotaryParams.startAngleRadians) / (rotaryParams.endAngleRadians - rotaryParams.startAngleRadians);
729             valueWhenLastDragged = owner.proportionOfLengthToValue (jlimit (0.0, 1.0, proportion));
730             lastAngle = angle;
731         }
732     }
733 
handleAbsoluteDrag(const MouseEvent & e)734     void handleAbsoluteDrag (const MouseEvent& e)
735     {
736         auto mousePos = (isHorizontal() || style == RotaryHorizontalDrag) ? e.position.x : e.position.y;
737         double newPos = 0;
738 
739         if (style == RotaryHorizontalDrag
740             || style == RotaryVerticalDrag
741             || style == IncDecButtons
742             || ((style == LinearHorizontal || style == LinearVertical || style == LinearBar || style == LinearBarVertical)
743                 && ! snapsToMousePos))
744         {
745             auto mouseDiff = (style == RotaryHorizontalDrag
746                                 || style == LinearHorizontal
747                                 || style == LinearBar
748                                 || (style == IncDecButtons && incDecDragDirectionIsHorizontal()))
749                               ? e.position.x - mouseDragStartPos.x
750                               : mouseDragStartPos.y - e.position.y;
751 
752             newPos = owner.valueToProportionOfLength (valueOnMouseDown)
753                        + mouseDiff * (1.0 / pixelsForFullDragExtent);
754 
755             if (style == IncDecButtons)
756             {
757                 incButton->setState (mouseDiff < 0 ? Button::buttonNormal : Button::buttonDown);
758                 decButton->setState (mouseDiff > 0 ? Button::buttonNormal : Button::buttonDown);
759             }
760         }
761         else if (style == RotaryHorizontalVerticalDrag)
762         {
763             auto mouseDiff = (e.position.x - mouseDragStartPos.x)
764                                + (mouseDragStartPos.y - e.position.y);
765 
766             newPos = owner.valueToProportionOfLength (valueOnMouseDown)
767                        + mouseDiff * (1.0 / pixelsForFullDragExtent);
768         }
769         else
770         {
771             newPos = (mousePos - (float) sliderRegionStart) / (double) sliderRegionSize;
772 
773             if (isVertical())
774                 newPos = 1.0 - newPos;
775         }
776 
777         newPos = (isRotary() && ! rotaryParams.stopAtEnd) ? newPos - std::floor (newPos)
778                                                           : jlimit (0.0, 1.0, newPos);
779         valueWhenLastDragged = owner.proportionOfLengthToValue (newPos);
780     }
781 
handleVelocityDrag(const MouseEvent & e)782     void handleVelocityDrag (const MouseEvent& e)
783     {
784         bool hasHorizontalStyle =
785             (isHorizontal() ||  style == RotaryHorizontalDrag
786                             || (style == IncDecButtons && incDecDragDirectionIsHorizontal()));
787 
788         auto mouseDiff = style == RotaryHorizontalVerticalDrag
789                             ? (e.position.x - mousePosWhenLastDragged.x) + (mousePosWhenLastDragged.y - e.position.y)
790                             : (hasHorizontalStyle ? e.position.x - mousePosWhenLastDragged.x
791                                                   : e.position.y - mousePosWhenLastDragged.y);
792 
793         auto maxSpeed = jmax (200.0, (double) sliderRegionSize);
794         auto speed = jlimit (0.0, maxSpeed, (double) std::abs (mouseDiff));
795 
796         if (speed != 0.0)
797         {
798             speed = 0.2 * velocityModeSensitivity
799                       * (1.0 + std::sin (MathConstants<double>::pi * (1.5 + jmin (0.5, velocityModeOffset
800                                                                                          + jmax (0.0, (double) (speed - velocityModeThreshold))
801                                                                                             / maxSpeed))));
802 
803             if (mouseDiff < 0)
804                 speed = -speed;
805 
806             if (isVertical() || style == RotaryVerticalDrag
807                  || (style == IncDecButtons && ! incDecDragDirectionIsHorizontal()))
808                 speed = -speed;
809 
810             auto newPos = owner.valueToProportionOfLength (valueWhenLastDragged) + speed;
811             newPos = (isRotary() && ! rotaryParams.stopAtEnd) ? newPos - std::floor (newPos)
812                                                               : jlimit (0.0, 1.0, newPos);
813             valueWhenLastDragged = owner.proportionOfLengthToValue (newPos);
814 
815             e.source.enableUnboundedMouseMovement (true, false);
816         }
817     }
818 
mouseDown(const MouseEvent & e)819     void mouseDown (const MouseEvent& e)
820     {
821         incDecDragged = false;
822         useDragEvents = false;
823         mouseDragStartPos = mousePosWhenLastDragged = e.position;
824         currentDrag.reset();
825         popupDisplay.reset();
826 
827         if (owner.isEnabled())
828         {
829             if (e.mods.isPopupMenu() && menuEnabled)
830             {
831                 showPopupMenu();
832             }
833             else if (canDoubleClickToValue()
834                      && (singleClickModifiers != ModifierKeys() && e.mods.withoutMouseButtons() == singleClickModifiers))
835             {
836                 mouseDoubleClick();
837             }
838             else if (normRange.end > normRange.start)
839             {
840                 useDragEvents = true;
841 
842                 if (valueBox != nullptr)
843                     valueBox->hideEditor (true);
844 
845                 sliderBeingDragged = getThumbIndexAt (e);
846 
847                 minMaxDiff = static_cast<double> (valueMax.getValue()) - static_cast<double> (valueMin.getValue());
848 
849                 if (! isTwoValue())
850                     lastAngle = rotaryParams.startAngleRadians
851                                     + (rotaryParams.endAngleRadians - rotaryParams.startAngleRadians)
852                                          * owner.valueToProportionOfLength (currentValue.getValue());
853 
854                 valueWhenLastDragged = (sliderBeingDragged == 2 ? valueMax
855                                                                 : (sliderBeingDragged == 1 ? valueMin
856                                                                                            : currentValue)).getValue();
857                 valueOnMouseDown = valueWhenLastDragged;
858 
859                 if (showPopupOnDrag || showPopupOnHover)
860                 {
861                     showPopupDisplay();
862 
863                     if (popupDisplay != nullptr)
864                         popupDisplay->stopTimer();
865                 }
866 
867                 currentDrag.reset (new DragInProgress (*this));
868                 mouseDrag (e);
869             }
870         }
871     }
872 
mouseDrag(const MouseEvent & e)873     void mouseDrag (const MouseEvent& e)
874     {
875         if (useDragEvents && normRange.end > normRange.start
876              && ! ((style == LinearBar || style == LinearBarVertical)
877                     && e.mouseWasClicked() && valueBox != nullptr && valueBox->isEditable()))
878         {
879             DragMode dragMode = notDragging;
880 
881             if (style == Rotary)
882             {
883                 handleRotaryDrag (e);
884             }
885             else
886             {
887                 if (style == IncDecButtons && ! incDecDragged)
888                 {
889                     if (e.getDistanceFromDragStart() < 10 || ! e.mouseWasDraggedSinceMouseDown())
890                         return;
891 
892                     incDecDragged = true;
893                     mouseDragStartPos = e.position;
894                 }
895 
896                 if (isAbsoluteDragMode (e.mods) || (normRange.end - normRange.start) / sliderRegionSize < normRange.interval)
897                 {
898                     dragMode = absoluteDrag;
899                     handleAbsoluteDrag (e);
900                 }
901                 else
902                 {
903                     dragMode = velocityDrag;
904                     handleVelocityDrag (e);
905                 }
906             }
907 
908             valueWhenLastDragged = jlimit (normRange.start, normRange.end, valueWhenLastDragged);
909 
910             if (sliderBeingDragged == 0)
911             {
912                 setValue (owner.snapValue (valueWhenLastDragged, dragMode),
913                           sendChangeOnlyOnRelease ? dontSendNotification : sendNotificationSync);
914             }
915             else if (sliderBeingDragged == 1)
916             {
917                 setMinValue (owner.snapValue (valueWhenLastDragged, dragMode),
918                              sendChangeOnlyOnRelease ? dontSendNotification : sendNotificationAsync, true);
919 
920                 if (e.mods.isShiftDown())
921                     setMaxValue (getMinValue() + minMaxDiff, dontSendNotification, true);
922                 else
923                     minMaxDiff = static_cast<double> (valueMax.getValue()) - static_cast<double> (valueMin.getValue());
924             }
925             else if (sliderBeingDragged == 2)
926             {
927                 setMaxValue (owner.snapValue (valueWhenLastDragged, dragMode),
928                              sendChangeOnlyOnRelease ? dontSendNotification : sendNotificationAsync, true);
929 
930                 if (e.mods.isShiftDown())
931                     setMinValue (getMaxValue() - minMaxDiff, dontSendNotification, true);
932                 else
933                     minMaxDiff = static_cast<double> (valueMax.getValue()) - static_cast<double> (valueMin.getValue());
934             }
935 
936             mousePosWhenLastDragged = e.position;
937         }
938     }
939 
mouseUp()940     void mouseUp()
941     {
942         if (owner.isEnabled()
943              && useDragEvents
944              && (normRange.end > normRange.start)
945              && (style != IncDecButtons || incDecDragged))
946         {
947             restoreMouseIfHidden();
948 
949             if (sendChangeOnlyOnRelease && valueOnMouseDown != static_cast<double> (currentValue.getValue()))
950                 triggerChangeMessage (sendNotificationAsync);
951 
952             currentDrag.reset();
953             popupDisplay.reset();
954 
955             if (style == IncDecButtons)
956             {
957                 incButton->setState (Button::buttonNormal);
958                 decButton->setState (Button::buttonNormal);
959             }
960         }
961         else if (popupDisplay != nullptr)
962         {
963             popupDisplay->startTimer (200);
964         }
965 
966         currentDrag.reset();
967     }
968 
mouseMove()969     void mouseMove()
970     {
971         // this is a workaround for a bug where the popup display being dismissed triggers
972         // a mouse move causing it to never be hidden
973         auto shouldShowPopup = showPopupOnHover
974                                 && (Time::getMillisecondCounterHiRes() - lastPopupDismissal) > 250;
975 
976         if (shouldShowPopup
977              && ! isTwoValue()
978              && ! isThreeValue())
979         {
980             if (owner.isMouseOver (true))
981             {
982                 if (popupDisplay == nullptr)
983                     showPopupDisplay();
984 
985                 if (popupDisplay != nullptr && popupHoverTimeout != -1)
986                     popupDisplay->startTimer (popupHoverTimeout);
987             }
988         }
989     }
990 
mouseExit()991     void mouseExit()
992     {
993         popupDisplay.reset();
994     }
995 
showPopupDisplay()996     void showPopupDisplay()
997     {
998         if (style == IncDecButtons)
999             return;
1000 
1001         if (popupDisplay == nullptr)
1002         {
1003             popupDisplay.reset (new PopupDisplayComponent (owner, parentForPopupDisplay == nullptr));
1004 
1005             if (parentForPopupDisplay != nullptr)
1006                 parentForPopupDisplay->addChildComponent (popupDisplay.get());
1007             else
1008                 popupDisplay->addToDesktop (ComponentPeer::windowIsTemporary
1009                                             | ComponentPeer::windowIgnoresKeyPresses
1010                                             | ComponentPeer::windowIgnoresMouseClicks);
1011 
1012             if (style == SliderStyle::TwoValueHorizontal
1013                 || style == SliderStyle::TwoValueVertical)
1014             {
1015                 updatePopupDisplay (sliderBeingDragged == 2 ? getMaxValue()
1016                                                             : getMinValue());
1017             }
1018             else
1019             {
1020                 updatePopupDisplay (getValue());
1021             }
1022 
1023             popupDisplay->setVisible (true);
1024         }
1025     }
1026 
updatePopupDisplay(double valueToShow)1027     void updatePopupDisplay (double valueToShow)
1028     {
1029         if (popupDisplay != nullptr)
1030             popupDisplay->updatePosition (owner.getTextFromValue (valueToShow));
1031     }
1032 
canDoubleClickToValue() const1033     bool canDoubleClickToValue() const
1034     {
1035         return doubleClickToValue
1036                 && style != IncDecButtons
1037                 && normRange.start <= doubleClickReturnValue
1038                 && normRange.end >= doubleClickReturnValue;
1039     }
1040 
mouseDoubleClick()1041     void mouseDoubleClick()
1042     {
1043         if (canDoubleClickToValue())
1044         {
1045             DragInProgress drag (*this);
1046             setValue (doubleClickReturnValue, sendNotificationSync);
1047         }
1048     }
1049 
getMouseWheelDelta(double value,double wheelAmount)1050     double getMouseWheelDelta (double value, double wheelAmount)
1051     {
1052         if (style == IncDecButtons)
1053             return normRange.interval * wheelAmount;
1054 
1055         auto proportionDelta = wheelAmount * 0.15;
1056         auto currentPos = owner.valueToProportionOfLength (value);
1057         auto newPos = currentPos + proportionDelta;
1058         newPos = (isRotary() && ! rotaryParams.stopAtEnd) ? newPos - std::floor (newPos)
1059                                                           : jlimit (0.0, 1.0, newPos);
1060         return owner.proportionOfLengthToValue (newPos) - value;
1061     }
1062 
mouseWheelMove(const MouseEvent & e,const MouseWheelDetails & wheel)1063     bool mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel)
1064     {
1065         if (scrollWheelEnabled
1066              && style != TwoValueHorizontal
1067              && style != TwoValueVertical)
1068         {
1069             // sometimes duplicate wheel events seem to be sent, so since we're going to
1070             // bump the value by a minimum of the interval, avoid doing this twice..
1071             if (e.eventTime != lastMouseWheelTime)
1072             {
1073                 lastMouseWheelTime = e.eventTime;
1074 
1075                 if (normRange.end > normRange.start && ! e.mods.isAnyMouseButtonDown())
1076                 {
1077                     if (valueBox != nullptr)
1078                         valueBox->hideEditor (false);
1079 
1080                     auto value = static_cast<double> (currentValue.getValue());
1081                     auto delta = getMouseWheelDelta (value, (std::abs (wheel.deltaX) > std::abs (wheel.deltaY)
1082                                                                   ? -wheel.deltaX : wheel.deltaY)
1083                                                                * (wheel.isReversed ? -1.0f : 1.0f));
1084                     if (delta != 0.0)
1085                     {
1086                         auto newValue = value + jmax (normRange.interval, std::abs (delta)) * (delta < 0 ? -1.0 : 1.0);
1087 
1088                         DragInProgress drag (*this);
1089                         setValue (owner.snapValue (newValue, notDragging), sendNotificationSync);
1090                     }
1091                 }
1092             }
1093 
1094             return true;
1095         }
1096 
1097         return false;
1098     }
1099 
modifierKeysChanged(const ModifierKeys & modifiers)1100     void modifierKeysChanged (const ModifierKeys& modifiers)
1101     {
1102         if (style != IncDecButtons && style != Rotary && isAbsoluteDragMode (modifiers))
1103             restoreMouseIfHidden();
1104     }
1105 
isAbsoluteDragMode(ModifierKeys mods) const1106     bool isAbsoluteDragMode (ModifierKeys mods) const
1107     {
1108         return isVelocityBased == (userKeyOverridesVelocity && mods.testFlags (modifierToSwapModes));
1109     }
1110 
restoreMouseIfHidden()1111     void restoreMouseIfHidden()
1112     {
1113         for (auto& ms : Desktop::getInstance().getMouseSources())
1114         {
1115             if (ms.isUnboundedMouseMovementEnabled())
1116             {
1117                 ms.enableUnboundedMouseMovement (false);
1118 
1119                 auto pos = sliderBeingDragged == 2 ? getMaxValue()
1120                                                    : (sliderBeingDragged == 1 ? getMinValue()
1121                                                                               : static_cast<double> (currentValue.getValue()));
1122                 Point<float> mousePos;
1123 
1124                 if (isRotary())
1125                 {
1126                     mousePos = ms.getLastMouseDownPosition();
1127 
1128                     auto delta = (float) (pixelsForFullDragExtent * (owner.valueToProportionOfLength (valueOnMouseDown)
1129                                                                        - owner.valueToProportionOfLength (pos)));
1130 
1131                     if (style == RotaryHorizontalDrag)      mousePos += Point<float> (-delta, 0.0f);
1132                     else if (style == RotaryVerticalDrag)   mousePos += Point<float> (0.0f, delta);
1133                     else                                    mousePos += Point<float> (delta / -2.0f, delta / 2.0f);
1134 
1135                     mousePos = owner.getScreenBounds().reduced (4).toFloat().getConstrainedPoint (mousePos);
1136                     mouseDragStartPos = mousePosWhenLastDragged = owner.getLocalPoint (nullptr, mousePos);
1137                     valueOnMouseDown = valueWhenLastDragged;
1138                 }
1139                 else
1140                 {
1141                     auto pixelPos = (float) getLinearSliderPos (pos);
1142 
1143                     mousePos = owner.localPointToGlobal (Point<float> (isHorizontal() ? pixelPos : ((float) owner.getWidth()  / 2.0f),
1144                                                                        isVertical()   ? pixelPos : ((float) owner.getHeight() / 2.0f)));
1145                 }
1146 
1147                 const_cast <MouseInputSource&> (ms).setScreenPosition (mousePos);
1148             }
1149         }
1150     }
1151 
1152     //==============================================================================
paint(Graphics & g,LookAndFeel & lf)1153     void paint (Graphics& g, LookAndFeel& lf)
1154     {
1155         if (style != IncDecButtons)
1156         {
1157             if (isRotary())
1158             {
1159                 auto sliderPos = (float) owner.valueToProportionOfLength (lastCurrentValue);
1160                 jassert (sliderPos >= 0 && sliderPos <= 1.0f);
1161 
1162                 lf.drawRotarySlider (g,
1163                                      sliderRect.getX(), sliderRect.getY(),
1164                                      sliderRect.getWidth(), sliderRect.getHeight(),
1165                                      sliderPos, rotaryParams.startAngleRadians,
1166                                      rotaryParams.endAngleRadians, owner);
1167             }
1168             else
1169             {
1170                 lf.drawLinearSlider (g,
1171                                      sliderRect.getX(), sliderRect.getY(),
1172                                      sliderRect.getWidth(), sliderRect.getHeight(),
1173                                      getLinearSliderPos (lastCurrentValue),
1174                                      getLinearSliderPos (lastValueMin),
1175                                      getLinearSliderPos (lastValueMax),
1176                                      style, owner);
1177             }
1178 
1179             if ((style == LinearBar || style == LinearBarVertical) && valueBox == nullptr)
1180             {
1181                 g.setColour (owner.findColour (Slider::textBoxOutlineColourId));
1182                 g.drawRect (0, 0, owner.getWidth(), owner.getHeight(), 1);
1183             }
1184         }
1185     }
1186 
1187     //==============================================================================
resized(LookAndFeel & lf)1188     void resized (LookAndFeel& lf)
1189     {
1190         auto layout = lf.getSliderLayout (owner);
1191         sliderRect = layout.sliderBounds;
1192 
1193         if (valueBox != nullptr)
1194             valueBox->setBounds (layout.textBoxBounds);
1195 
1196         if (isHorizontal())
1197         {
1198             sliderRegionStart = layout.sliderBounds.getX();
1199             sliderRegionSize = layout.sliderBounds.getWidth();
1200         }
1201         else if (isVertical())
1202         {
1203             sliderRegionStart = layout.sliderBounds.getY();
1204             sliderRegionSize = layout.sliderBounds.getHeight();
1205         }
1206         else if (style == IncDecButtons)
1207         {
1208             resizeIncDecButtons();
1209         }
1210     }
1211 
1212     //==============================================================================
1213 
resizeIncDecButtons()1214     void resizeIncDecButtons()
1215     {
1216         auto buttonRect = sliderRect;
1217 
1218         if (textBoxPos == TextBoxLeft || textBoxPos == TextBoxRight)
1219             buttonRect.expand (-2, 0);
1220         else
1221             buttonRect.expand (0, -2);
1222 
1223         incDecButtonsSideBySide = buttonRect.getWidth() > buttonRect.getHeight();
1224 
1225         if (incDecButtonsSideBySide)
1226         {
1227             decButton->setBounds (buttonRect.removeFromLeft (buttonRect.getWidth() / 2));
1228             decButton->setConnectedEdges (Button::ConnectedOnRight);
1229             incButton->setConnectedEdges (Button::ConnectedOnLeft);
1230         }
1231         else
1232         {
1233             decButton->setBounds (buttonRect.removeFromBottom (buttonRect.getHeight() / 2));
1234             decButton->setConnectedEdges (Button::ConnectedOnTop);
1235             incButton->setConnectedEdges (Button::ConnectedOnBottom);
1236         }
1237 
1238         incButton->setBounds (buttonRect);
1239     }
1240 
1241     //==============================================================================
1242     Slider& owner;
1243     SliderStyle style;
1244 
1245     ListenerList<Slider::Listener> listeners;
1246     Value currentValue, valueMin, valueMax;
1247     double lastCurrentValue = 0, lastValueMin = 0, lastValueMax = 0;
1248     NormalisableRange<double> normRange { 0.0, 10.0 };
1249     double doubleClickReturnValue = 0;
1250     double valueWhenLastDragged = 0, valueOnMouseDown = 0, lastAngle = 0;
1251     double velocityModeSensitivity = 1.0, velocityModeOffset = 0, minMaxDiff = 0;
1252     int velocityModeThreshold = 1;
1253     RotaryParameters rotaryParams;
1254     Point<float> mouseDragStartPos, mousePosWhenLastDragged;
1255     int sliderRegionStart = 0, sliderRegionSize = 1;
1256     int sliderBeingDragged = -1;
1257     int pixelsForFullDragExtent = 250;
1258     Time lastMouseWheelTime;
1259     Rectangle<int> sliderRect;
1260     std::unique_ptr<DragInProgress> currentDrag;
1261 
1262     TextEntryBoxPosition textBoxPos;
1263     String textSuffix;
1264     int numDecimalPlaces = 7;
1265     int textBoxWidth = 80, textBoxHeight = 20;
1266     IncDecButtonMode incDecButtonMode = incDecButtonsNotDraggable;
1267     ModifierKeys::Flags modifierToSwapModes = ModifierKeys::ctrlAltCommandModifiers;
1268 
1269     bool editableText = true;
1270     bool doubleClickToValue = false;
1271     bool isVelocityBased = false;
1272     bool userKeyOverridesVelocity = true;
1273     bool incDecButtonsSideBySide = false;
1274     bool sendChangeOnlyOnRelease = false;
1275     bool showPopupOnDrag = false;
1276     bool showPopupOnHover = false;
1277     bool menuEnabled = false;
1278     bool useDragEvents = false;
1279     bool incDecDragged = false;
1280     bool scrollWheelEnabled = true;
1281     bool snapsToMousePos = true;
1282 
1283     int popupHoverTimeout = 2000;
1284     double lastPopupDismissal = 0.0;
1285 
1286     ModifierKeys singleClickModifiers;
1287 
1288     std::unique_ptr<Label> valueBox;
1289     std::unique_ptr<Button> incButton, decButton;
1290 
1291     //==============================================================================
1292     struct PopupDisplayComponent  : public BubbleComponent,
1293                                     public Timer
1294     {
PopupDisplayComponentjuce::Slider::Pimpl::PopupDisplayComponent1295         PopupDisplayComponent (Slider& s, bool isOnDesktop)
1296             : owner (s),
1297               font (s.getLookAndFeel().getSliderPopupFont (s))
1298         {
1299             if (isOnDesktop)
1300                 setTransform (AffineTransform::scale (Component::getApproximateScaleFactorForComponent (&s)));
1301 
1302             setAlwaysOnTop (true);
1303             setAllowedPlacement (owner.getLookAndFeel().getSliderPopupPlacement (s));
1304             setLookAndFeel (&s.getLookAndFeel());
1305         }
1306 
~PopupDisplayComponentjuce::Slider::Pimpl::PopupDisplayComponent1307         ~PopupDisplayComponent() override
1308         {
1309             if (owner.pimpl != nullptr)
1310                 owner.pimpl->lastPopupDismissal = Time::getMillisecondCounterHiRes();
1311         }
1312 
paintContentjuce::Slider::Pimpl::PopupDisplayComponent1313         void paintContent (Graphics& g, int w, int h) override
1314         {
1315             g.setFont (font);
1316             g.setColour (owner.findColour (TooltipWindow::textColourId, true));
1317             g.drawFittedText (text, Rectangle<int> (w, h), Justification::centred, 1);
1318         }
1319 
getContentSizejuce::Slider::Pimpl::PopupDisplayComponent1320         void getContentSize (int& w, int& h) override
1321         {
1322             w = font.getStringWidth (text) + 18;
1323             h = (int) (font.getHeight() * 1.6f);
1324         }
1325 
updatePositionjuce::Slider::Pimpl::PopupDisplayComponent1326         void updatePosition (const String& newText)
1327         {
1328             text = newText;
1329             BubbleComponent::setPosition (&owner);
1330             repaint();
1331         }
1332 
timerCallbackjuce::Slider::Pimpl::PopupDisplayComponent1333         void timerCallback() override
1334         {
1335             stopTimer();
1336             owner.pimpl->popupDisplay.reset();
1337         }
1338 
1339     private:
1340         //==============================================================================
1341         Slider& owner;
1342         Font font;
1343         String text;
1344 
1345         JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PopupDisplayComponent)
1346     };
1347 
1348     std::unique_ptr<PopupDisplayComponent> popupDisplay;
1349     Component* parentForPopupDisplay = nullptr;
1350 
1351     //==============================================================================
smallestAngleBetween(double a1,double a2)1352     static double smallestAngleBetween (double a1, double a2) noexcept
1353     {
1354         return jmin (std::abs (a1 - a2),
1355                      std::abs (a1 + MathConstants<double>::twoPi - a2),
1356                      std::abs (a2 + MathConstants<double>::twoPi - a1));
1357     }
1358 };
1359 
1360 
1361 //==============================================================================
Slider()1362 Slider::Slider()
1363 {
1364     init (LinearHorizontal, TextBoxLeft);
1365 }
1366 
Slider(const String & name)1367 Slider::Slider (const String& name)  : Component (name)
1368 {
1369     init (LinearHorizontal, TextBoxLeft);
1370 }
1371 
Slider(SliderStyle style,TextEntryBoxPosition textBoxPos)1372 Slider::Slider (SliderStyle style, TextEntryBoxPosition textBoxPos)
1373 {
1374     init (style, textBoxPos);
1375 }
1376 
init(SliderStyle style,TextEntryBoxPosition textBoxPos)1377 void Slider::init (SliderStyle style, TextEntryBoxPosition textBoxPos)
1378 {
1379     setWantsKeyboardFocus (false);
1380     setRepaintsOnMouseActivity (true);
1381 
1382     pimpl.reset (new Pimpl (*this, style, textBoxPos));
1383 
1384     Slider::lookAndFeelChanged();
1385     updateText();
1386 
1387     pimpl->registerListeners();
1388 }
1389 
~Slider()1390 Slider::~Slider() {}
1391 
1392 //==============================================================================
addListener(Listener * l)1393 void Slider::addListener (Listener* l)       { pimpl->listeners.add (l); }
removeListener(Listener * l)1394 void Slider::removeListener (Listener* l)    { pimpl->listeners.remove (l); }
1395 
1396 //==============================================================================
getSliderStyle() const1397 Slider::SliderStyle Slider::getSliderStyle() const noexcept     { return pimpl->style; }
setSliderStyle(SliderStyle newStyle)1398 void Slider::setSliderStyle (SliderStyle newStyle)              { pimpl->setSliderStyle (newStyle); }
1399 
setRotaryParameters(RotaryParameters p)1400 void Slider::setRotaryParameters (RotaryParameters p) noexcept
1401 {
1402     // make sure the values are sensible..
1403     jassert (p.startAngleRadians >= 0 && p.endAngleRadians >= 0);
1404     jassert (p.startAngleRadians < MathConstants<float>::pi * 4.0f
1405               && p.endAngleRadians < MathConstants<float>::pi * 4.0f);
1406 
1407     pimpl->rotaryParams = p;
1408 }
1409 
setRotaryParameters(float startAngleRadians,float endAngleRadians,bool stopAtEnd)1410 void Slider::setRotaryParameters (float startAngleRadians, float endAngleRadians, bool stopAtEnd) noexcept
1411 {
1412     setRotaryParameters ({ startAngleRadians, endAngleRadians, stopAtEnd });
1413 }
1414 
getRotaryParameters() const1415 Slider::RotaryParameters Slider::getRotaryParameters() const noexcept
1416 {
1417     return pimpl->rotaryParams;
1418 }
1419 
setVelocityBasedMode(bool vb)1420 void Slider::setVelocityBasedMode (bool vb)                 { pimpl->isVelocityBased = vb; }
getVelocityBasedMode() const1421 bool Slider::getVelocityBasedMode() const noexcept          { return pimpl->isVelocityBased; }
getVelocityModeIsSwappable() const1422 bool Slider::getVelocityModeIsSwappable() const noexcept    { return pimpl->userKeyOverridesVelocity; }
getVelocityThreshold() const1423 int Slider::getVelocityThreshold() const noexcept           { return pimpl->velocityModeThreshold; }
getVelocitySensitivity() const1424 double Slider::getVelocitySensitivity() const noexcept      { return pimpl->velocityModeSensitivity; }
getVelocityOffset() const1425 double Slider::getVelocityOffset() const noexcept           { return pimpl->velocityModeOffset; }
1426 
setVelocityModeParameters(double sensitivity,int threshold,double offset,bool userCanPressKeyToSwapMode,ModifierKeys::Flags modifierToSwapModes)1427 void Slider::setVelocityModeParameters (double sensitivity, int threshold,
1428                                         double offset, bool userCanPressKeyToSwapMode,
1429                                         ModifierKeys::Flags modifierToSwapModes)
1430 {
1431     jassert (threshold >= 0);
1432     jassert (sensitivity > 0);
1433     jassert (offset >= 0);
1434 
1435     pimpl->setVelocityModeParameters (sensitivity, threshold, offset,
1436                                       userCanPressKeyToSwapMode, modifierToSwapModes);
1437 }
1438 
getSkewFactor() const1439 double Slider::getSkewFactor() const noexcept               { return pimpl->normRange.skew; }
isSymmetricSkew() const1440 bool Slider::isSymmetricSkew() const noexcept               { return pimpl->normRange.symmetricSkew; }
1441 
setSkewFactor(double factor,bool symmetricSkew)1442 void Slider::setSkewFactor (double factor, bool symmetricSkew)
1443 {
1444     pimpl->normRange.skew = factor;
1445     pimpl->normRange.symmetricSkew = symmetricSkew;
1446 }
1447 
setSkewFactorFromMidPoint(double sliderValueToShowAtMidPoint)1448 void Slider::setSkewFactorFromMidPoint (double sliderValueToShowAtMidPoint)
1449 {
1450     pimpl->normRange.setSkewForCentre (sliderValueToShowAtMidPoint);
1451 }
1452 
getMouseDragSensitivity() const1453 int Slider::getMouseDragSensitivity() const noexcept        { return pimpl->pixelsForFullDragExtent; }
1454 
setMouseDragSensitivity(int distanceForFullScaleDrag)1455 void Slider::setMouseDragSensitivity (int distanceForFullScaleDrag)
1456 {
1457     jassert (distanceForFullScaleDrag > 0);
1458 
1459     pimpl->pixelsForFullDragExtent = distanceForFullScaleDrag;
1460 }
1461 
setIncDecButtonsMode(IncDecButtonMode mode)1462 void Slider::setIncDecButtonsMode (IncDecButtonMode mode)                   { pimpl->setIncDecButtonsMode (mode); }
1463 
getTextBoxPosition() const1464 Slider::TextEntryBoxPosition Slider::getTextBoxPosition() const noexcept    { return pimpl->textBoxPos; }
getTextBoxWidth() const1465 int Slider::getTextBoxWidth() const noexcept                                { return pimpl->textBoxWidth; }
getTextBoxHeight() const1466 int Slider::getTextBoxHeight() const noexcept                               { return pimpl->textBoxHeight; }
1467 
setTextBoxStyle(TextEntryBoxPosition newPosition,bool isReadOnly,int textEntryBoxWidth,int textEntryBoxHeight)1468 void Slider::setTextBoxStyle (TextEntryBoxPosition newPosition, bool isReadOnly, int textEntryBoxWidth, int textEntryBoxHeight)
1469 {
1470     pimpl->setTextBoxStyle (newPosition, isReadOnly, textEntryBoxWidth, textEntryBoxHeight);
1471 }
1472 
isTextBoxEditable() const1473 bool Slider::isTextBoxEditable() const noexcept                     { return pimpl->editableText; }
setTextBoxIsEditable(const bool shouldBeEditable)1474 void Slider::setTextBoxIsEditable (const bool shouldBeEditable)     { pimpl->setTextBoxIsEditable (shouldBeEditable); }
showTextBox()1475 void Slider::showTextBox()                                          { pimpl->showTextBox(); }
hideTextBox(bool discardCurrentEditorContents)1476 void Slider::hideTextBox (bool discardCurrentEditorContents)        { pimpl->hideTextBox (discardCurrentEditorContents); }
1477 
setChangeNotificationOnlyOnRelease(bool onlyNotifyOnRelease)1478 void Slider::setChangeNotificationOnlyOnRelease (bool onlyNotifyOnRelease)
1479 {
1480     pimpl->sendChangeOnlyOnRelease = onlyNotifyOnRelease;
1481 }
1482 
getSliderSnapsToMousePosition() const1483 bool Slider::getSliderSnapsToMousePosition() const noexcept           { return pimpl->snapsToMousePos; }
setSliderSnapsToMousePosition(bool shouldSnapToMouse)1484 void Slider::setSliderSnapsToMousePosition (bool shouldSnapToMouse)   { pimpl->snapsToMousePos = shouldSnapToMouse; }
1485 
setPopupDisplayEnabled(bool showOnDrag,bool showOnHover,Component * parent,int hoverTimeout)1486 void Slider::setPopupDisplayEnabled (bool showOnDrag, bool showOnHover, Component* parent, int hoverTimeout)
1487 {
1488     pimpl->showPopupOnDrag = showOnDrag;
1489     pimpl->showPopupOnHover = showOnHover;
1490     pimpl->parentForPopupDisplay = parent;
1491     pimpl->popupHoverTimeout = hoverTimeout;
1492 }
1493 
getCurrentPopupDisplay() const1494 Component* Slider::getCurrentPopupDisplay() const noexcept      { return pimpl->popupDisplay.get(); }
1495 
1496 //==============================================================================
colourChanged()1497 void Slider::colourChanged()        { lookAndFeelChanged(); }
lookAndFeelChanged()1498 void Slider::lookAndFeelChanged()   { pimpl->lookAndFeelChanged (getLookAndFeel()); }
enablementChanged()1499 void Slider::enablementChanged()    { repaint(); pimpl->updateTextBoxEnablement(); }
1500 
1501 //==============================================================================
getRange() const1502 Range<double> Slider::getRange() const noexcept  { return { pimpl->normRange.start, pimpl->normRange.end }; }
getMaximum() const1503 double Slider::getMaximum() const noexcept       { return pimpl->normRange.end; }
getMinimum() const1504 double Slider::getMinimum() const noexcept       { return pimpl->normRange.start; }
getInterval() const1505 double Slider::getInterval() const noexcept      { return pimpl->normRange.interval; }
1506 
setRange(double newMin,double newMax,double newInt)1507 void Slider::setRange (double newMin, double newMax, double newInt)      { pimpl->setRange (newMin, newMax, newInt); }
setRange(Range<double> newRange,double newInt)1508 void Slider::setRange (Range<double> newRange, double newInt)            { pimpl->setRange (newRange.getStart(), newRange.getEnd(), newInt); }
setNormalisableRange(NormalisableRange<double> newRange)1509 void Slider::setNormalisableRange (NormalisableRange<double> newRange)   { pimpl->setNormalisableRange (newRange); }
1510 
getValue() const1511 double Slider::getValue() const                  { return pimpl->getValue(); }
getValueObject()1512 Value& Slider::getValueObject() noexcept         { return pimpl->currentValue; }
getMinValueObject()1513 Value& Slider::getMinValueObject() noexcept      { return pimpl->valueMin; }
getMaxValueObject()1514 Value& Slider::getMaxValueObject() noexcept      { return pimpl->valueMax; }
1515 
setValue(double newValue,NotificationType notification)1516 void Slider::setValue (double newValue, NotificationType notification)
1517 {
1518     pimpl->setValue (newValue, notification);
1519 }
1520 
getMinValue() const1521 double Slider::getMinValue() const      { return pimpl->getMinValue(); }
getMaxValue() const1522 double Slider::getMaxValue() const      { return pimpl->getMaxValue(); }
1523 
setMinValue(double newValue,NotificationType notification,bool allowNudgingOfOtherValues)1524 void Slider::setMinValue (double newValue, NotificationType notification, bool allowNudgingOfOtherValues)
1525 {
1526     pimpl->setMinValue (newValue, notification, allowNudgingOfOtherValues);
1527 }
1528 
setMaxValue(double newValue,NotificationType notification,bool allowNudgingOfOtherValues)1529 void Slider::setMaxValue (double newValue, NotificationType notification, bool allowNudgingOfOtherValues)
1530 {
1531     pimpl->setMaxValue (newValue, notification, allowNudgingOfOtherValues);
1532 }
1533 
setMinAndMaxValues(double newMinValue,double newMaxValue,NotificationType notification)1534 void Slider::setMinAndMaxValues (double newMinValue, double newMaxValue, NotificationType notification)
1535 {
1536     pimpl->setMinAndMaxValues (newMinValue, newMaxValue, notification);
1537 }
1538 
setDoubleClickReturnValue(bool isDoubleClickEnabled,double valueToSetOnDoubleClick,ModifierKeys mods)1539 void Slider::setDoubleClickReturnValue (bool isDoubleClickEnabled,  double valueToSetOnDoubleClick, ModifierKeys mods)
1540 {
1541     pimpl->doubleClickToValue = isDoubleClickEnabled;
1542     pimpl->doubleClickReturnValue = valueToSetOnDoubleClick;
1543     pimpl->singleClickModifiers = mods;
1544 }
1545 
getDoubleClickReturnValue() const1546 double Slider::getDoubleClickReturnValue() const noexcept       { return pimpl->doubleClickReturnValue; }
isDoubleClickReturnEnabled() const1547 bool Slider::isDoubleClickReturnEnabled() const noexcept        { return pimpl->doubleClickToValue; }
1548 
updateText()1549 void Slider::updateText()
1550 {
1551     pimpl->updateText();
1552 }
1553 
setTextValueSuffix(const String & suffix)1554 void Slider::setTextValueSuffix (const String& suffix)
1555 {
1556     pimpl->setTextValueSuffix (suffix);
1557 }
1558 
getTextValueSuffix() const1559 String Slider::getTextValueSuffix() const
1560 {
1561     return pimpl->textSuffix;
1562 }
1563 
getTextFromValue(double v)1564 String Slider::getTextFromValue (double v)
1565 {
1566     auto getText = [this] (double val)
1567     {
1568         if (textFromValueFunction != nullptr)
1569             return textFromValueFunction (val);
1570 
1571         if (getNumDecimalPlacesToDisplay() > 0)
1572             return String (val, getNumDecimalPlacesToDisplay());
1573 
1574         return String (roundToInt (val));
1575     };
1576 
1577     return getText (v) + getTextValueSuffix();
1578 }
1579 
getValueFromText(const String & text)1580 double Slider::getValueFromText (const String& text)
1581 {
1582     auto t = text.trimStart();
1583 
1584     if (t.endsWith (getTextValueSuffix()))
1585         t = t.substring (0, t.length() - getTextValueSuffix().length());
1586 
1587     if (valueFromTextFunction != nullptr)
1588         return valueFromTextFunction (t);
1589 
1590     while (t.startsWithChar ('+'))
1591         t = t.substring (1).trimStart();
1592 
1593     return t.initialSectionContainingOnly ("0123456789.,-")
1594             .getDoubleValue();
1595 }
1596 
proportionOfLengthToValue(double proportion)1597 double Slider::proportionOfLengthToValue (double proportion)
1598 {
1599     return pimpl->normRange.convertFrom0to1 (proportion);
1600 }
1601 
valueToProportionOfLength(double value)1602 double Slider::valueToProportionOfLength (double value)
1603 {
1604     return pimpl->normRange.convertTo0to1 (value);
1605 }
1606 
snapValue(double attemptedValue,DragMode)1607 double Slider::snapValue (double attemptedValue, DragMode)
1608 {
1609     return attemptedValue;
1610 }
1611 
getNumDecimalPlacesToDisplay() const1612 int Slider::getNumDecimalPlacesToDisplay() const noexcept   { return pimpl->numDecimalPlaces; }
1613 
setNumDecimalPlacesToDisplay(int decimalPlacesToDisplay)1614 void Slider::setNumDecimalPlacesToDisplay (int decimalPlacesToDisplay)
1615 {
1616     pimpl->numDecimalPlaces = decimalPlacesToDisplay;
1617     updateText();
1618 }
1619 
1620 //==============================================================================
getThumbBeingDragged() const1621 int Slider::getThumbBeingDragged() const noexcept           { return pimpl->sliderBeingDragged; }
startedDragging()1622 void Slider::startedDragging() {}
stoppedDragging()1623 void Slider::stoppedDragging() {}
valueChanged()1624 void Slider::valueChanged() {}
1625 
1626 //==============================================================================
setPopupMenuEnabled(bool menuEnabled)1627 void Slider::setPopupMenuEnabled (bool menuEnabled)         { pimpl->menuEnabled = menuEnabled; }
setScrollWheelEnabled(bool enabled)1628 void Slider::setScrollWheelEnabled (bool enabled)           { pimpl->scrollWheelEnabled = enabled; }
1629 
isHorizontal() const1630 bool Slider::isHorizontal() const noexcept                  { return pimpl->isHorizontal(); }
isVertical() const1631 bool Slider::isVertical() const noexcept                    { return pimpl->isVertical(); }
isRotary() const1632 bool Slider::isRotary() const noexcept                      { return pimpl->isRotary(); }
isBar() const1633 bool Slider::isBar() const noexcept                         { return pimpl->isBar(); }
isTwoValue() const1634 bool Slider::isTwoValue() const noexcept                    { return pimpl->isTwoValue(); }
isThreeValue() const1635 bool Slider::isThreeValue() const noexcept                  { return pimpl->isThreeValue(); }
1636 
getPositionOfValue(double value) const1637 float Slider::getPositionOfValue (double value) const       { return pimpl->getPositionOfValue (value); }
1638 
1639 //==============================================================================
paint(Graphics & g)1640 void Slider::paint (Graphics& g)        { pimpl->paint (g, getLookAndFeel()); }
resized()1641 void Slider::resized()                  { pimpl->resized (getLookAndFeel()); }
1642 
focusOfChildComponentChanged(FocusChangeType)1643 void Slider::focusOfChildComponentChanged (FocusChangeType)     { repaint(); }
1644 
mouseDown(const MouseEvent & e)1645 void Slider::mouseDown (const MouseEvent& e)    { pimpl->mouseDown (e); }
mouseUp(const MouseEvent &)1646 void Slider::mouseUp   (const MouseEvent&)      { pimpl->mouseUp(); }
mouseMove(const MouseEvent &)1647 void Slider::mouseMove (const MouseEvent&)      { pimpl->mouseMove(); }
mouseExit(const MouseEvent &)1648 void Slider::mouseExit (const MouseEvent&)      { pimpl->mouseExit(); }
1649 
1650 // If popup display is enabled and set to show on mouse hover, this makes sure
1651 // it is shown when dragging the mouse over a slider and releasing
mouseEnter(const MouseEvent &)1652 void Slider::mouseEnter (const MouseEvent&)     { pimpl->mouseMove(); }
1653 
modifierKeysChanged(const ModifierKeys & modifiers)1654 void Slider::modifierKeysChanged (const ModifierKeys& modifiers)
1655 {
1656     if (isEnabled())
1657         pimpl->modifierKeysChanged (modifiers);
1658 }
1659 
mouseDrag(const MouseEvent & e)1660 void Slider::mouseDrag (const MouseEvent& e)
1661 {
1662     if (isEnabled())
1663         pimpl->mouseDrag (e);
1664 }
1665 
mouseDoubleClick(const MouseEvent &)1666 void Slider::mouseDoubleClick (const MouseEvent&)
1667 {
1668     if (isEnabled())
1669         pimpl->mouseDoubleClick();
1670 }
1671 
mouseWheelMove(const MouseEvent & e,const MouseWheelDetails & wheel)1672 void Slider::mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel)
1673 {
1674     if (! (isEnabled() && pimpl->mouseWheelMove (e, wheel)))
1675         Component::mouseWheelMove (e, wheel);
1676 }
1677 
1678 } // namespace juce
1679