/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2022 - Raw Material Software Limited JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 7 End-User License Agreement and JUCE Privacy Policy. End User License Agreement: www.juce.com/juce-7-licence Privacy Policy: www.juce.com/juce-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). 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 { static double getStepSize (const Slider& slider) { const auto interval = slider.getInterval(); return interval != 0.0 ? interval : slider.getRange().getLength() * 0.01; } 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. // Cast to double before comparing, to prevent comparing as another type (e.g. String). if (static_cast (currentValue.getValue()) != 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(); if (auto* handler = owner.getAccessibilityHandler()) handler->notifyAccessibilityEvent (AccessibilityEvent::valueChanged); } 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(); } void incrementOrDecrement (double delta) { if (style == IncDecButtons) { auto newValue = owner.snapValue (getValue() + delta, notDragging); if (currentDrag != nullptr) { setValue (newValue, sendNotificationSync); } else { ScopedDragNotification drag (owner); 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())) { ScopedDragNotification drag (owner); 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(); owner.invalidateAccessibilityHandler(); } } 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)); auto tooltip = owner.getTooltip(); auto setupButton = [&] (Button& b, bool isIncrement) { owner.addAndMakeVisible (b); b.onClick = [this, isIncrement] { incrementOrDecrement (isIncrement ? normRange.interval : -normRange.interval); }; if (incDecButtonMode != incDecButtonsNotDraggable) b.addMouseListener (&owner, false); else b.setRepeatSpeed (300, 100, 20); b.setTooltip (tooltip); b.setAccessible (false); }; setupButton (*incButton, true); setupButton (*decButton, false); } 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 - (float) sliderRect.getCentreX(); auto dy = e.position.y - (float) 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 - (float) 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 = std::make_unique (owner); 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(); } bool keyPressed (const KeyPress& key) { if (key.getModifiers().isAnyModifierKeyDown()) return false; const auto getInterval = [this] { if (auto* accessibility = owner.getAccessibilityHandler()) if (auto* valueInterface = accessibility->getValueInterface()) return valueInterface->getRange().getInterval(); return getStepSize (owner); }; const auto valueChange = [&] { if (key == KeyPress::rightKey || key == KeyPress::upKey) return getInterval(); if (key == KeyPress::leftKey || key == KeyPress::downKey) return -getInterval(); return 0.0; }(); if (valueChange == 0.0) return false; setValue (getValue() + valueChange, sendNotificationSync); return true; } 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()) { ScopedDragNotification drag (owner); 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); ScopedDragNotification drag (owner); 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 : ((float) owner.getWidth() / 2.0f), isVertical() ? pixelPos : ((float) 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