/* ============================================================================== This file is part of the JUCE 6 technical preview. Copyright (c) 2020 - Raw Material Software Limited You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For this technical preview, this file is not subject to commercial licensing. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ namespace juce { class Slider::Pimpl : public AsyncUpdater, // this needs to be public otherwise it will cause an // error when JUCE_DLL_BUILD=1 private Value::Listener { public: Pimpl (Slider& s, SliderStyle sliderStyle, TextEntryBoxPosition textBoxPosition) : owner (s), style (sliderStyle), textBoxPos (textBoxPosition) { rotaryParams.startAngleRadians = MathConstants::pi * 1.2f; rotaryParams.endAngleRadians = MathConstants::pi * 2.8f; rotaryParams.stopAtEnd = true; } ~Pimpl() override { currentValue.removeListener (this); valueMin.removeListener (this); valueMax.removeListener (this); popupDisplay.reset(); } //============================================================================== void registerListeners() { currentValue.addListener (this); valueMin.addListener (this); valueMax.addListener (this); } bool isHorizontal() const noexcept { return style == LinearHorizontal || style == LinearBar || style == TwoValueHorizontal || style == ThreeValueHorizontal; } bool isVertical() const noexcept { return style == LinearVertical || style == LinearBarVertical || style == TwoValueVertical || style == ThreeValueVertical; } bool isRotary() const noexcept { return style == Rotary || style == RotaryHorizontalDrag || style == RotaryVerticalDrag || style == RotaryHorizontalVerticalDrag; } bool isBar() const noexcept { return style == LinearBar || style == LinearBarVertical; } bool isTwoValue() const noexcept { return style == TwoValueHorizontal || style == TwoValueVertical; } bool isThreeValue() const noexcept { return style == ThreeValueHorizontal || style == ThreeValueVertical; } bool incDecDragDirectionIsHorizontal() const noexcept { return incDecButtonMode == incDecButtonsDraggable_Horizontal || (incDecButtonMode == incDecButtonsDraggable_AutoDirection && incDecButtonsSideBySide); } float getPositionOfValue (double value) const { if (isHorizontal() || isVertical()) return getLinearSliderPos (value); jassertfalse; // not a valid call on a slider that doesn't work linearly! return 0.0f; } void updateRange() { // figure out the number of DPs needed to display all values at this // interval setting. numDecimalPlaces = 7; if (normRange.interval != 0.0) { int v = std::abs (roundToInt (normRange.interval * 10000000)); while ((v % 10) == 0 && numDecimalPlaces > 0) { --numDecimalPlaces; v /= 10; } } // keep the current values inside the new range.. if (style != TwoValueHorizontal && style != TwoValueVertical) { setValue (getValue(), dontSendNotification); } else { setMinValue (getMinValue(), dontSendNotification, false); setMaxValue (getMaxValue(), dontSendNotification, false); } updateText(); } void setRange (double newMin, double newMax, double newInt) { normRange = NormalisableRange (newMin, newMax, newInt, normRange.skew, normRange.symmetricSkew); updateRange(); } void setNormalisableRange (NormalisableRange newRange) { normRange = newRange; updateRange(); } double getValue() const { // for a two-value style slider, you should use the getMinValue() and getMaxValue() // methods to get the two values. jassert (style != TwoValueHorizontal && style != TwoValueVertical); return currentValue.getValue(); } void setValue (double newValue, NotificationType notification) { // for a two-value style slider, you should use the setMinValue() and setMaxValue() // methods to set the two values. jassert (style != TwoValueHorizontal && style != TwoValueVertical); newValue = constrainedValue (newValue); if (style == ThreeValueHorizontal || style == ThreeValueVertical) { jassert (static_cast (valueMin.getValue()) <= static_cast (valueMax.getValue())); newValue = jlimit (static_cast (valueMin.getValue()), static_cast (valueMax.getValue()), newValue); } if (newValue != lastCurrentValue) { if (valueBox != nullptr) valueBox->hideEditor (true); lastCurrentValue = newValue; // (need to do this comparison because the Value will use equalsWithSameType to compare // the new and old values, so will generate unwanted change events if the type changes) if (currentValue != newValue) currentValue = newValue; updateText(); owner.repaint(); updatePopupDisplay (newValue); triggerChangeMessage (notification); } } void setMinValue (double newValue, NotificationType notification, bool allowNudgingOfOtherValues) { // The minimum value only applies to sliders that are in two- or three-value mode. jassert (style == TwoValueHorizontal || style == TwoValueVertical || style == ThreeValueHorizontal || style == ThreeValueVertical); newValue = constrainedValue (newValue); if (style == TwoValueHorizontal || style == TwoValueVertical) { if (allowNudgingOfOtherValues && newValue > static_cast (valueMax.getValue())) setMaxValue (newValue, notification, false); newValue = jmin (static_cast (valueMax.getValue()), newValue); } else { if (allowNudgingOfOtherValues && newValue > lastCurrentValue) setValue (newValue, notification); newValue = jmin (lastCurrentValue, newValue); } if (lastValueMin != newValue) { lastValueMin = newValue; valueMin = newValue; owner.repaint(); updatePopupDisplay (newValue); triggerChangeMessage (notification); } } void setMaxValue (double newValue, NotificationType notification, bool allowNudgingOfOtherValues) { // The maximum value only applies to sliders that are in two- or three-value mode. jassert (style == TwoValueHorizontal || style == TwoValueVertical || style == ThreeValueHorizontal || style == ThreeValueVertical); newValue = constrainedValue (newValue); if (style == TwoValueHorizontal || style == TwoValueVertical) { if (allowNudgingOfOtherValues && newValue < static_cast (valueMin.getValue())) setMinValue (newValue, notification, false); newValue = jmax (static_cast (valueMin.getValue()), newValue); } else { if (allowNudgingOfOtherValues && newValue < lastCurrentValue) setValue (newValue, notification); newValue = jmax (lastCurrentValue, newValue); } if (lastValueMax != newValue) { lastValueMax = newValue; valueMax = newValue; owner.repaint(); updatePopupDisplay (valueMax.getValue()); triggerChangeMessage (notification); } } void setMinAndMaxValues (double newMinValue, double newMaxValue, NotificationType notification) { // The maximum value only applies to sliders that are in two- or three-value mode. jassert (style == TwoValueHorizontal || style == TwoValueVertical || style == ThreeValueHorizontal || style == ThreeValueVertical); if (newMaxValue < newMinValue) std::swap (newMaxValue, newMinValue); newMinValue = constrainedValue (newMinValue); newMaxValue = constrainedValue (newMaxValue); if (lastValueMax != newMaxValue || lastValueMin != newMinValue) { lastValueMax = newMaxValue; lastValueMin = newMinValue; valueMin = newMinValue; valueMax = newMaxValue; owner.repaint(); triggerChangeMessage (notification); } } double getMinValue() const { // The minimum value only applies to sliders that are in two- or three-value mode. jassert (style == TwoValueHorizontal || style == TwoValueVertical || style == ThreeValueHorizontal || style == ThreeValueVertical); return valueMin.getValue(); } double getMaxValue() const { // The maximum value only applies to sliders that are in two- or three-value mode. jassert (style == TwoValueHorizontal || style == TwoValueVertical || style == ThreeValueHorizontal || style == ThreeValueVertical); return valueMax.getValue(); } void triggerChangeMessage (NotificationType notification) { if (notification != dontSendNotification) { owner.valueChanged(); if (notification == sendNotificationSync) handleAsyncUpdate(); else triggerAsyncUpdate(); } } void handleAsyncUpdate() override { cancelPendingUpdate(); Component::BailOutChecker checker (&owner); listeners.callChecked (checker, [&] (Slider::Listener& l) { l.sliderValueChanged (&owner); }); if (checker.shouldBailOut()) return; if (owner.onValueChange != nullptr) owner.onValueChange(); } void sendDragStart() { owner.startedDragging(); Component::BailOutChecker checker (&owner); listeners.callChecked (checker, [&] (Slider::Listener& l) { l.sliderDragStarted (&owner); }); if (checker.shouldBailOut()) return; if (owner.onDragStart != nullptr) owner.onDragStart(); } void sendDragEnd() { owner.stoppedDragging(); sliderBeingDragged = -1; Component::BailOutChecker checker (&owner); listeners.callChecked (checker, [&] (Slider::Listener& l) { l.sliderDragEnded (&owner); }); if (checker.shouldBailOut()) return; if (owner.onDragEnd != nullptr) owner.onDragEnd(); } struct DragInProgress { DragInProgress (Pimpl& p) : owner (p) { owner.sendDragStart(); } ~DragInProgress() { owner.sendDragEnd(); } Pimpl& owner; JUCE_DECLARE_NON_COPYABLE (DragInProgress) }; void incrementOrDecrement (double delta) { if (style == IncDecButtons) { auto newValue = owner.snapValue (getValue() + delta, notDragging); if (currentDrag != nullptr) { setValue (newValue, sendNotificationSync); } else { DragInProgress drag (*this); setValue (newValue, sendNotificationSync); } } } void valueChanged (Value& value) override { if (value.refersToSameSourceAs (currentValue)) { if (style != TwoValueHorizontal && style != TwoValueVertical) setValue (currentValue.getValue(), dontSendNotification); } else if (value.refersToSameSourceAs (valueMin)) setMinValue (valueMin.getValue(), dontSendNotification, true); else if (value.refersToSameSourceAs (valueMax)) setMaxValue (valueMax.getValue(), dontSendNotification, true); } void textChanged() { auto newValue = owner.snapValue (owner.getValueFromText (valueBox->getText()), notDragging); if (newValue != static_cast (currentValue.getValue())) { DragInProgress drag (*this); setValue (newValue, sendNotificationSync); } updateText(); // force a clean-up of the text, needed in case setValue() hasn't done this. } void updateText() { if (valueBox != nullptr) { auto newValue = owner.getTextFromValue (currentValue.getValue()); if (newValue != valueBox->getText()) valueBox->setText (newValue, dontSendNotification); } } double constrainedValue (double value) const { return normRange.snapToLegalValue (value); } float getLinearSliderPos (double value) const { double pos; if (normRange.end <= normRange.start) pos = 0.5; else if (value < normRange.start) pos = 0.0; else if (value > normRange.end) pos = 1.0; else pos = owner.valueToProportionOfLength (value); if (isVertical() || style == IncDecButtons) pos = 1.0 - pos; jassert (pos >= 0 && pos <= 1.0); return (float) (sliderRegionStart + pos * sliderRegionSize); } void setSliderStyle (SliderStyle newStyle) { if (style != newStyle) { style = newStyle; owner.repaint(); owner.lookAndFeelChanged(); } } void setVelocityModeParameters (double sensitivity, int threshold, double offset, bool userCanPressKeyToSwapMode, ModifierKeys::Flags newModifierToSwapModes) { velocityModeSensitivity = sensitivity; velocityModeOffset = offset; velocityModeThreshold = threshold; userKeyOverridesVelocity = userCanPressKeyToSwapMode; modifierToSwapModes = newModifierToSwapModes; } void setIncDecButtonsMode (IncDecButtonMode mode) { if (incDecButtonMode != mode) { incDecButtonMode = mode; owner.lookAndFeelChanged(); } } void setTextBoxStyle (TextEntryBoxPosition newPosition, bool isReadOnly, int textEntryBoxWidth, int textEntryBoxHeight) { if (textBoxPos != newPosition || editableText != (! isReadOnly) || textBoxWidth != textEntryBoxWidth || textBoxHeight != textEntryBoxHeight) { textBoxPos = newPosition; editableText = ! isReadOnly; textBoxWidth = textEntryBoxWidth; textBoxHeight = textEntryBoxHeight; owner.repaint(); owner.lookAndFeelChanged(); } } void setTextBoxIsEditable (bool shouldBeEditable) { editableText = shouldBeEditable; updateTextBoxEnablement(); } void showTextBox() { jassert (editableText); // this should probably be avoided in read-only sliders. if (valueBox != nullptr) valueBox->showEditor(); } void hideTextBox (bool discardCurrentEditorContents) { if (valueBox != nullptr) { valueBox->hideEditor (discardCurrentEditorContents); if (discardCurrentEditorContents) updateText(); } } void setTextValueSuffix (const String& suffix) { if (textSuffix != suffix) { textSuffix = suffix; updateText(); } } void updateTextBoxEnablement() { if (valueBox != nullptr) { bool shouldBeEditable = editableText && owner.isEnabled(); if (valueBox->isEditable() != shouldBeEditable) // (to avoid changing the single/double click flags unless we need to) valueBox->setEditable (shouldBeEditable); } } void lookAndFeelChanged (LookAndFeel& lf) { if (textBoxPos != NoTextBox) { auto previousTextBoxContent = (valueBox != nullptr ? valueBox->getText() : owner.getTextFromValue (currentValue.getValue())); valueBox.reset(); valueBox.reset (lf.createSliderTextBox (owner)); owner.addAndMakeVisible (valueBox.get()); valueBox->setWantsKeyboardFocus (false); valueBox->setText (previousTextBoxContent, dontSendNotification); valueBox->setTooltip (owner.getTooltip()); updateTextBoxEnablement(); valueBox->onTextChange = [this] { textChanged(); }; if (style == LinearBar || style == LinearBarVertical) { valueBox->addMouseListener (&owner, false); valueBox->setMouseCursor (MouseCursor::ParentCursor); } } else { valueBox.reset(); } if (style == IncDecButtons) { incButton.reset (lf.createSliderButton (owner, true)); decButton.reset (lf.createSliderButton (owner, false)); owner.addAndMakeVisible (incButton.get()); owner.addAndMakeVisible (decButton.get()); incButton->onClick = [this] { incrementOrDecrement (normRange.interval); }; decButton->onClick = [this] { incrementOrDecrement (-normRange.interval); }; if (incDecButtonMode != incDecButtonsNotDraggable) { incButton->addMouseListener (&owner, false); decButton->addMouseListener (&owner, false); } else { incButton->setRepeatSpeed (300, 100, 20); decButton->setRepeatSpeed (300, 100, 20); } auto tooltip = owner.getTooltip(); incButton->setTooltip (tooltip); decButton->setTooltip (tooltip); } else { incButton.reset(); decButton.reset(); } owner.setComponentEffect (lf.getSliderEffect (owner)); owner.resized(); owner.repaint(); } void showPopupMenu() { PopupMenu m; m.setLookAndFeel (&owner.getLookAndFeel()); m.addItem (1, TRANS ("Velocity-sensitive mode"), true, isVelocityBased); m.addSeparator(); if (isRotary()) { PopupMenu rotaryMenu; rotaryMenu.addItem (2, TRANS ("Use circular dragging"), true, style == Rotary); rotaryMenu.addItem (3, TRANS ("Use left-right dragging"), true, style == RotaryHorizontalDrag); rotaryMenu.addItem (4, TRANS ("Use up-down dragging"), true, style == RotaryVerticalDrag); rotaryMenu.addItem (5, TRANS ("Use left-right/up-down dragging"), true, style == RotaryHorizontalVerticalDrag); m.addSubMenu (TRANS ("Rotary mode"), rotaryMenu); } m.showMenuAsync (PopupMenu::Options(), ModalCallbackFunction::forComponent (sliderMenuCallback, &owner)); } static void sliderMenuCallback (int result, Slider* slider) { if (slider != nullptr) { switch (result) { case 1: slider->setVelocityBasedMode (! slider->getVelocityBasedMode()); break; case 2: slider->setSliderStyle (Rotary); break; case 3: slider->setSliderStyle (RotaryHorizontalDrag); break; case 4: slider->setSliderStyle (RotaryVerticalDrag); break; case 5: slider->setSliderStyle (RotaryHorizontalVerticalDrag); break; default: break; } } } int getThumbIndexAt (const MouseEvent& e) { if (isTwoValue() || isThreeValue()) { auto mousePos = isVertical() ? e.position.y : e.position.x; auto normalPosDistance = std::abs (getLinearSliderPos (currentValue.getValue()) - mousePos); auto minPosDistance = std::abs (getLinearSliderPos (valueMin.getValue()) + (isVertical() ? 0.1f : -0.1f) - mousePos); auto maxPosDistance = std::abs (getLinearSliderPos (valueMax.getValue()) + (isVertical() ? -0.1f : 0.1f) - mousePos); if (isTwoValue()) return maxPosDistance <= minPosDistance ? 2 : 1; if (normalPosDistance >= minPosDistance && maxPosDistance >= minPosDistance) return 1; if (normalPosDistance >= maxPosDistance) return 2; } return 0; } //============================================================================== void handleRotaryDrag (const MouseEvent& e) { auto dx = e.position.x - sliderRect.getCentreX(); auto dy = e.position.y - sliderRect.getCentreY(); if (dx * dx + dy * dy > 25.0f) { auto angle = std::atan2 ((double) dx, (double) -dy); while (angle < 0.0) angle += MathConstants::twoPi; if (rotaryParams.stopAtEnd && e.mouseWasDraggedSinceMouseDown()) { if (std::abs (angle - lastAngle) > MathConstants::pi) { if (angle >= lastAngle) angle -= MathConstants::twoPi; else angle += MathConstants::twoPi; } if (angle >= lastAngle) angle = jmin (angle, (double) jmax (rotaryParams.startAngleRadians, rotaryParams.endAngleRadians)); else angle = jmax (angle, (double) jmin (rotaryParams.startAngleRadians, rotaryParams.endAngleRadians)); } else { while (angle < rotaryParams.startAngleRadians) angle += MathConstants::twoPi; if (angle > rotaryParams.endAngleRadians) { if (smallestAngleBetween (angle, rotaryParams.startAngleRadians) <= smallestAngleBetween (angle, rotaryParams.endAngleRadians)) angle = rotaryParams.startAngleRadians; else angle = rotaryParams.endAngleRadians; } } auto proportion = (angle - rotaryParams.startAngleRadians) / (rotaryParams.endAngleRadians - rotaryParams.startAngleRadians); valueWhenLastDragged = owner.proportionOfLengthToValue (jlimit (0.0, 1.0, proportion)); lastAngle = angle; } } void handleAbsoluteDrag (const MouseEvent& e) { auto mousePos = (isHorizontal() || style == RotaryHorizontalDrag) ? e.position.x : e.position.y; double newPos = 0; if (style == RotaryHorizontalDrag || style == RotaryVerticalDrag || style == IncDecButtons || ((style == LinearHorizontal || style == LinearVertical || style == LinearBar || style == LinearBarVertical) && ! snapsToMousePos)) { auto mouseDiff = (style == RotaryHorizontalDrag || style == LinearHorizontal || style == LinearBar || (style == IncDecButtons && incDecDragDirectionIsHorizontal())) ? e.position.x - mouseDragStartPos.x : mouseDragStartPos.y - e.position.y; newPos = owner.valueToProportionOfLength (valueOnMouseDown) + mouseDiff * (1.0 / pixelsForFullDragExtent); if (style == IncDecButtons) { incButton->setState (mouseDiff < 0 ? Button::buttonNormal : Button::buttonDown); decButton->setState (mouseDiff > 0 ? Button::buttonNormal : Button::buttonDown); } } else if (style == RotaryHorizontalVerticalDrag) { auto mouseDiff = (e.position.x - mouseDragStartPos.x) + (mouseDragStartPos.y - e.position.y); newPos = owner.valueToProportionOfLength (valueOnMouseDown) + mouseDiff * (1.0 / pixelsForFullDragExtent); } else { newPos = (mousePos - sliderRegionStart) / (double) sliderRegionSize; if (isVertical()) newPos = 1.0 - newPos; } newPos = (isRotary() && ! rotaryParams.stopAtEnd) ? newPos - std::floor (newPos) : jlimit (0.0, 1.0, newPos); valueWhenLastDragged = owner.proportionOfLengthToValue (newPos); } void handleVelocityDrag (const MouseEvent& e) { bool hasHorizontalStyle = (isHorizontal() || style == RotaryHorizontalDrag || (style == IncDecButtons && incDecDragDirectionIsHorizontal())); auto mouseDiff = style == RotaryHorizontalVerticalDrag ? (e.position.x - mousePosWhenLastDragged.x) + (mousePosWhenLastDragged.y - e.position.y) : (hasHorizontalStyle ? e.position.x - mousePosWhenLastDragged.x : e.position.y - mousePosWhenLastDragged.y); auto maxSpeed = jmax (200.0, (double) sliderRegionSize); auto speed = jlimit (0.0, maxSpeed, (double) std::abs (mouseDiff)); if (speed != 0.0) { speed = 0.2 * velocityModeSensitivity * (1.0 + std::sin (MathConstants::pi * (1.5 + jmin (0.5, velocityModeOffset + jmax (0.0, (double) (speed - velocityModeThreshold)) / maxSpeed)))); if (mouseDiff < 0) speed = -speed; if (isVertical() || style == RotaryVerticalDrag || (style == IncDecButtons && ! incDecDragDirectionIsHorizontal())) speed = -speed; auto newPos = owner.valueToProportionOfLength (valueWhenLastDragged) + speed; newPos = (isRotary() && ! rotaryParams.stopAtEnd) ? newPos - std::floor (newPos) : jlimit (0.0, 1.0, newPos); valueWhenLastDragged = owner.proportionOfLengthToValue (newPos); e.source.enableUnboundedMouseMovement (true, false); } } void mouseDown (const MouseEvent& e) { incDecDragged = false; useDragEvents = false; mouseDragStartPos = mousePosWhenLastDragged = e.position; currentDrag.reset(); popupDisplay.reset(); if (owner.isEnabled()) { if (e.mods.isPopupMenu() && menuEnabled) { showPopupMenu(); } else if (canDoubleClickToValue() && (singleClickModifiers != ModifierKeys() && e.mods.withoutMouseButtons() == singleClickModifiers)) { mouseDoubleClick(); } else if (normRange.end > normRange.start) { useDragEvents = true; if (valueBox != nullptr) valueBox->hideEditor (true); sliderBeingDragged = getThumbIndexAt (e); minMaxDiff = static_cast (valueMax.getValue()) - static_cast (valueMin.getValue()); if (! isTwoValue()) lastAngle = rotaryParams.startAngleRadians + (rotaryParams.endAngleRadians - rotaryParams.startAngleRadians) * owner.valueToProportionOfLength (currentValue.getValue()); valueWhenLastDragged = (sliderBeingDragged == 2 ? valueMax : (sliderBeingDragged == 1 ? valueMin : currentValue)).getValue(); valueOnMouseDown = valueWhenLastDragged; if (showPopupOnDrag || showPopupOnHover) { showPopupDisplay(); if (popupDisplay != nullptr) popupDisplay->stopTimer(); } currentDrag.reset (new DragInProgress (*this)); mouseDrag (e); } } } void mouseDrag (const MouseEvent& e) { if (useDragEvents && normRange.end > normRange.start && ! ((style == LinearBar || style == LinearBarVertical) && e.mouseWasClicked() && valueBox != nullptr && valueBox->isEditable())) { DragMode dragMode = notDragging; if (style == Rotary) { handleRotaryDrag (e); } else { if (style == IncDecButtons && ! incDecDragged) { if (e.getDistanceFromDragStart() < 10 || ! e.mouseWasDraggedSinceMouseDown()) return; incDecDragged = true; mouseDragStartPos = e.position; } if (isAbsoluteDragMode (e.mods) || (normRange.end - normRange.start) / sliderRegionSize < normRange.interval) { dragMode = absoluteDrag; handleAbsoluteDrag (e); } else { dragMode = velocityDrag; handleVelocityDrag (e); } } valueWhenLastDragged = jlimit (normRange.start, normRange.end, valueWhenLastDragged); if (sliderBeingDragged == 0) { setValue (owner.snapValue (valueWhenLastDragged, dragMode), sendChangeOnlyOnRelease ? dontSendNotification : sendNotificationSync); } else if (sliderBeingDragged == 1) { setMinValue (owner.snapValue (valueWhenLastDragged, dragMode), sendChangeOnlyOnRelease ? dontSendNotification : sendNotificationAsync, true); if (e.mods.isShiftDown()) setMaxValue (getMinValue() + minMaxDiff, dontSendNotification, true); else minMaxDiff = static_cast (valueMax.getValue()) - static_cast (valueMin.getValue()); } else if (sliderBeingDragged == 2) { setMaxValue (owner.snapValue (valueWhenLastDragged, dragMode), sendChangeOnlyOnRelease ? dontSendNotification : sendNotificationAsync, true); if (e.mods.isShiftDown()) setMinValue (getMaxValue() - minMaxDiff, dontSendNotification, true); else minMaxDiff = static_cast (valueMax.getValue()) - static_cast (valueMin.getValue()); } mousePosWhenLastDragged = e.position; } } void mouseUp() { if (owner.isEnabled() && useDragEvents && (normRange.end > normRange.start) && (style != IncDecButtons || incDecDragged)) { restoreMouseIfHidden(); if (sendChangeOnlyOnRelease && valueOnMouseDown != static_cast (currentValue.getValue())) triggerChangeMessage (sendNotificationAsync); currentDrag.reset(); popupDisplay.reset(); if (style == IncDecButtons) { incButton->setState (Button::buttonNormal); decButton->setState (Button::buttonNormal); } } else if (popupDisplay != nullptr) { popupDisplay->startTimer (200); } currentDrag.reset(); } void mouseMove() { // this is a workaround for a bug where the popup display being dismissed triggers // a mouse move causing it to never be hidden auto shouldShowPopup = showPopupOnHover && (Time::getMillisecondCounterHiRes() - lastPopupDismissal) > 250; if (shouldShowPopup && ! isTwoValue() && ! isThreeValue()) { if (owner.isMouseOver (true)) { if (popupDisplay == nullptr) showPopupDisplay(); if (popupDisplay != nullptr && popupHoverTimeout != -1) popupDisplay->startTimer (popupHoverTimeout); } } } void mouseExit() { popupDisplay.reset(); } void showPopupDisplay() { if (style == IncDecButtons) return; if (popupDisplay == nullptr) { popupDisplay.reset (new PopupDisplayComponent (owner, parentForPopupDisplay == nullptr)); if (parentForPopupDisplay != nullptr) parentForPopupDisplay->addChildComponent (popupDisplay.get()); else popupDisplay->addToDesktop (ComponentPeer::windowIsTemporary | ComponentPeer::windowIgnoresKeyPresses | ComponentPeer::windowIgnoresMouseClicks); if (style == SliderStyle::TwoValueHorizontal || style == SliderStyle::TwoValueVertical) { updatePopupDisplay (sliderBeingDragged == 2 ? getMaxValue() : getMinValue()); } else { updatePopupDisplay (getValue()); } popupDisplay->setVisible (true); } } void updatePopupDisplay (double valueToShow) { if (popupDisplay != nullptr) popupDisplay->updatePosition (owner.getTextFromValue (valueToShow)); } bool canDoubleClickToValue() const { return doubleClickToValue && style != IncDecButtons && normRange.start <= doubleClickReturnValue && normRange.end >= doubleClickReturnValue; } void mouseDoubleClick() { if (canDoubleClickToValue()) { DragInProgress drag (*this); setValue (doubleClickReturnValue, sendNotificationSync); } } double getMouseWheelDelta (double value, double wheelAmount) { if (style == IncDecButtons) return normRange.interval * wheelAmount; auto proportionDelta = wheelAmount * 0.15; auto currentPos = owner.valueToProportionOfLength (value); auto newPos = currentPos + proportionDelta; newPos = (isRotary() && ! rotaryParams.stopAtEnd) ? newPos - std::floor (newPos) : jlimit (0.0, 1.0, newPos); return owner.proportionOfLengthToValue (newPos) - value; } bool mouseWheelMove (const MouseEvent& e, const MouseWheelDetails& wheel) { if (scrollWheelEnabled && style != TwoValueHorizontal && style != TwoValueVertical) { // sometimes duplicate wheel events seem to be sent, so since we're going to // bump the value by a minimum of the interval, avoid doing this twice.. if (e.eventTime != lastMouseWheelTime) { lastMouseWheelTime = e.eventTime; if (normRange.end > normRange.start && ! e.mods.isAnyMouseButtonDown()) { if (valueBox != nullptr) valueBox->hideEditor (false); auto value = static_cast (currentValue.getValue()); auto delta = getMouseWheelDelta (value, (std::abs (wheel.deltaX) > std::abs (wheel.deltaY) ? -wheel.deltaX : wheel.deltaY) * (wheel.isReversed ? -1.0f : 1.0f)); if (delta != 0.0) { auto newValue = value + jmax (normRange.interval, std::abs (delta)) * (delta < 0 ? -1.0 : 1.0); DragInProgress drag (*this); setValue (owner.snapValue (newValue, notDragging), sendNotificationSync); } } } return true; } return false; } void modifierKeysChanged (const ModifierKeys& modifiers) { if (style != IncDecButtons && style != Rotary && isAbsoluteDragMode (modifiers)) restoreMouseIfHidden(); } bool isAbsoluteDragMode (ModifierKeys mods) const { return isVelocityBased == (userKeyOverridesVelocity && mods.testFlags (modifierToSwapModes)); } void restoreMouseIfHidden() { for (auto& ms : Desktop::getInstance().getMouseSources()) { if (ms.isUnboundedMouseMovementEnabled()) { ms.enableUnboundedMouseMovement (false); auto pos = sliderBeingDragged == 2 ? getMaxValue() : (sliderBeingDragged == 1 ? getMinValue() : static_cast (currentValue.getValue())); Point mousePos; if (isRotary()) { mousePos = ms.getLastMouseDownPosition(); auto delta = (float) (pixelsForFullDragExtent * (owner.valueToProportionOfLength (valueOnMouseDown) - owner.valueToProportionOfLength (pos))); if (style == RotaryHorizontalDrag) mousePos += Point (-delta, 0.0f); else if (style == RotaryVerticalDrag) mousePos += Point (0.0f, delta); else mousePos += Point (delta / -2.0f, delta / 2.0f); mousePos = owner.getScreenBounds().reduced (4).toFloat().getConstrainedPoint (mousePos); mouseDragStartPos = mousePosWhenLastDragged = owner.getLocalPoint (nullptr, mousePos); valueOnMouseDown = valueWhenLastDragged; } else { auto pixelPos = (float) getLinearSliderPos (pos); mousePos = owner.localPointToGlobal (Point (isHorizontal() ? pixelPos : (owner.getWidth() / 2.0f), isVertical() ? pixelPos : (owner.getHeight() / 2.0f))); } const_cast (ms).setScreenPosition (mousePos); } } } //============================================================================== void paint (Graphics& g, LookAndFeel& lf) { if (style != IncDecButtons) { if (isRotary()) { auto sliderPos = (float) owner.valueToProportionOfLength (lastCurrentValue); jassert (sliderPos >= 0 && sliderPos <= 1.0f); lf.drawRotarySlider (g, sliderRect.getX(), sliderRect.getY(), sliderRect.getWidth(), sliderRect.getHeight(), sliderPos, rotaryParams.startAngleRadians, rotaryParams.endAngleRadians, owner); } else { lf.drawLinearSlider (g, sliderRect.getX(), sliderRect.getY(), sliderRect.getWidth(), sliderRect.getHeight(), getLinearSliderPos (lastCurrentValue), getLinearSliderPos (lastValueMin), getLinearSliderPos (lastValueMax), style, owner); } if ((style == LinearBar || style == LinearBarVertical) && valueBox == nullptr) { g.setColour (owner.findColour (Slider::textBoxOutlineColourId)); g.drawRect (0, 0, owner.getWidth(), owner.getHeight(), 1); } } } //============================================================================== void resized (LookAndFeel& lf) { auto layout = lf.getSliderLayout (owner); sliderRect = layout.sliderBounds; if (valueBox != nullptr) valueBox->setBounds (layout.textBoxBounds); if (isHorizontal()) { sliderRegionStart = layout.sliderBounds.getX(); sliderRegionSize = layout.sliderBounds.getWidth(); } else if (isVertical()) { sliderRegionStart = layout.sliderBounds.getY(); sliderRegionSize = layout.sliderBounds.getHeight(); } else if (style == IncDecButtons) { resizeIncDecButtons(); } } //============================================================================== void resizeIncDecButtons() { auto buttonRect = sliderRect; if (textBoxPos == TextBoxLeft || textBoxPos == TextBoxRight) buttonRect.expand (-2, 0); else buttonRect.expand (0, -2); incDecButtonsSideBySide = buttonRect.getWidth() > buttonRect.getHeight(); if (incDecButtonsSideBySide) { decButton->setBounds (buttonRect.removeFromLeft (buttonRect.getWidth() / 2)); decButton->setConnectedEdges (Button::ConnectedOnRight); incButton->setConnectedEdges (Button::ConnectedOnLeft); } else { decButton->setBounds (buttonRect.removeFromBottom (buttonRect.getHeight() / 2)); decButton->setConnectedEdges (Button::ConnectedOnTop); incButton->setConnectedEdges (Button::ConnectedOnBottom); } incButton->setBounds (buttonRect); } //============================================================================== Slider& owner; SliderStyle style; ListenerList listeners; Value currentValue, valueMin, valueMax; double lastCurrentValue = 0, lastValueMin = 0, lastValueMax = 0; NormalisableRange normRange { 0.0, 10.0 }; double doubleClickReturnValue = 0; double valueWhenLastDragged = 0, valueOnMouseDown = 0, lastAngle = 0; double velocityModeSensitivity = 1.0, velocityModeOffset = 0, minMaxDiff = 0; int velocityModeThreshold = 1; RotaryParameters rotaryParams; Point mouseDragStartPos, mousePosWhenLastDragged; int sliderRegionStart = 0, sliderRegionSize = 1; int sliderBeingDragged = -1; int pixelsForFullDragExtent = 250; Time lastMouseWheelTime; Rectangle sliderRect; std::unique_ptr currentDrag; TextEntryBoxPosition textBoxPos; String textSuffix; int numDecimalPlaces = 7; int textBoxWidth = 80, textBoxHeight = 20; IncDecButtonMode incDecButtonMode = incDecButtonsNotDraggable; ModifierKeys::Flags modifierToSwapModes = ModifierKeys::ctrlAltCommandModifiers; bool editableText = true; bool doubleClickToValue = false; bool isVelocityBased = false; bool userKeyOverridesVelocity = true; bool incDecButtonsSideBySide = false; bool sendChangeOnlyOnRelease = false; bool showPopupOnDrag = false; bool showPopupOnHover = false; bool menuEnabled = false; bool useDragEvents = false; bool incDecDragged = false; bool scrollWheelEnabled = true; bool snapsToMousePos = true; int popupHoverTimeout = 2000; double lastPopupDismissal = 0.0; ModifierKeys singleClickModifiers; std::unique_ptr