| @@ -13,14 +13,19 @@ struct Knob : ParamWidget { | |||||
| struct Internal; | struct Internal; | ||||
| Internal* internal; | Internal* internal; | ||||
| /** Multiplier for mouse movement to adjust knob value */ | |||||
| float speed = 1.0; | |||||
| /** Drag horizontally instead of vertically. */ | /** Drag horizontally instead of vertically. */ | ||||
| bool horizontal = false; | bool horizontal = false; | ||||
| /** Enables per-sample value smoothing while dragging. */ | /** Enables per-sample value smoothing while dragging. */ | ||||
| bool smooth = true; | bool smooth = true; | ||||
| /** DEPRECATED. Use `ParamQuantity::snapEnabled`. */ | /** DEPRECATED. Use `ParamQuantity::snapEnabled`. */ | ||||
| bool snap = false; | bool snap = false; | ||||
| /** Multiplier for mouse movement to adjust knob value */ | |||||
| float speed = 1.f; | |||||
| /** Force dragging to linear, e.g. for sliders. */ | |||||
| bool forceLinear = false; | |||||
| /** Angles in radians. */ | |||||
| float minAngle = -M_PI; | |||||
| float maxAngle = M_PI; | |||||
| Knob(); | Knob(); | ||||
| ~Knob(); | ~Knob(); | ||||
| @@ -30,6 +35,7 @@ struct Knob : ParamWidget { | |||||
| void onDragStart(const event::DragStart& e) override; | void onDragStart(const event::DragStart& e) override; | ||||
| void onDragEnd(const event::DragEnd& e) override; | void onDragEnd(const event::DragEnd& e) override; | ||||
| void onDragMove(const event::DragMove& e) override; | void onDragMove(const event::DragMove& e) override; | ||||
| void onDragLeave(const event::DragLeave& e) override; | |||||
| }; | }; | ||||
| @@ -8,13 +8,10 @@ namespace app { | |||||
| struct SliderKnob : Knob { | struct SliderKnob : Knob { | ||||
| // Bypass Knob's circular hitbox detection | |||||
| void onHover(const event::Hover& e) override { | |||||
| ParamWidget::onHover(e); | |||||
| } | |||||
| void onButton(const event::Button& e) override { | |||||
| ParamWidget::onButton(e); | |||||
| } | |||||
| SliderKnob(); | |||||
| void onHover(const event::Hover& e) override; | |||||
| void onButton(const event::Button& e) override; | |||||
| }; | }; | ||||
| @@ -17,9 +17,6 @@ struct SvgKnob : Knob { | |||||
| CircularShadow* shadow; | CircularShadow* shadow; | ||||
| widget::TransformWidget* tw; | widget::TransformWidget* tw; | ||||
| widget::SvgWidget* sw; | widget::SvgWidget* sw; | ||||
| /** Angles in radians */ | |||||
| float minAngle = 0.f; | |||||
| float maxAngle = M_PI; | |||||
| SvgKnob(); | SvgKnob(); | ||||
| void setSvg(std::shared_ptr<Svg> svg); | void setSvg(std::shared_ptr<Svg> svg); | ||||
| @@ -28,12 +28,12 @@ extern bool invertZoom; | |||||
| extern float cableOpacity; | extern float cableOpacity; | ||||
| extern float cableTension; | extern float cableTension; | ||||
| enum KnobMode { | enum KnobMode { | ||||
| KNOB_MODE_LINEAR_LOCKED = 0, | |||||
| KNOB_MODE_LINEAR, | KNOB_MODE_LINEAR, | ||||
| KNOB_MODE_LINEAR_UNLOCKED, | |||||
| KNOB_MODE_LINEAR_SPEED, | |||||
| KNOB_MODE_LINEAR_SPEED_UNLOCKED, | |||||
| KNOB_MODE_CIRCULAR_ABSOLUTE, | |||||
| KNOB_MODE_CIRCULAR_RELATIVE, | |||||
| KNOB_MODE_SCALED_LINEAR_LOCKED = 100, | |||||
| KNOB_MODE_SCALED_LINEAR, | |||||
| KNOB_MODE_ROTARY_ABSOLUTE = 200, | |||||
| KNOB_MODE_ROTARY_RELATIVE, | |||||
| }; | }; | ||||
| extern KnobMode knobMode; | extern KnobMode knobMode; | ||||
| extern float sampleRate; | extern float sampleRate; | ||||
| @@ -3,20 +3,26 @@ | |||||
| #include <app/Scene.hpp> | #include <app/Scene.hpp> | ||||
| #include <random.hpp> | #include <random.hpp> | ||||
| #include <history.hpp> | #include <history.hpp> | ||||
| #include <settings.hpp> | |||||
| namespace rack { | namespace rack { | ||||
| namespace app { | namespace app { | ||||
| static const float KNOB_SENSITIVITY = 0.0015f; | |||||
| struct Knob::Internal { | struct Knob::Internal { | ||||
| /** Value of the knob before dragging. */ | /** Value of the knob before dragging. */ | ||||
| float oldValue = 0.f; | float oldValue = 0.f; | ||||
| /** Fractional value between the param's value and the dragged knob position. */ | |||||
| /** Fractional value between the param's value and the dragged knob position. | |||||
| Using a "snapValue" variable and rounding is insufficient because the mouse needs to reach 1.0, not 0.5 to obtain the first increment. | |||||
| */ | |||||
| float snapDelta = 0.f; | float snapDelta = 0.f; | ||||
| /** Speed multiplier in speed knob mode */ | |||||
| float linearScale = 1.f; | |||||
| /** The mouse has once escaped from the knob while dragging. */ | |||||
| bool rotaryDragEnabled = false; | |||||
| float dragAngle = NAN; | |||||
| }; | }; | ||||
| @@ -38,6 +44,7 @@ void Knob::initParamQuantity() { | |||||
| } | } | ||||
| void Knob::onHover(const event::Hover& e) { | void Knob::onHover(const event::Hover& e) { | ||||
| // Only call super if mouse position is in the circle | |||||
| math::Vec c = box.size.div(2); | math::Vec c = box.size.div(2); | ||||
| float dist = e.pos.minus(c).norm(); | float dist = e.pos.minus(c).norm(); | ||||
| if (dist <= c.x) { | if (dist <= c.x) { | ||||
| @@ -63,14 +70,27 @@ void Knob::onDragStart(const event::DragStart& e) { | |||||
| internal->snapDelta = 0.f; | internal->snapDelta = 0.f; | ||||
| } | } | ||||
| APP->window->cursorLock(); | |||||
| settings::KnobMode km = settings::knobMode; | |||||
| if (km == settings::KNOB_MODE_LINEAR_LOCKED || km == settings::KNOB_MODE_SCALED_LINEAR_LOCKED) { | |||||
| APP->window->cursorLock(); | |||||
| } | |||||
| // Only changed for KNOB_MODE_LINEAR_*. | |||||
| internal->linearScale = 1.f; | |||||
| // Only used for KNOB_MODE_ROTARY_*. | |||||
| internal->rotaryDragEnabled = false; | |||||
| internal->dragAngle = NAN; | |||||
| ParamWidget::onDragStart(e); | |||||
| } | } | ||||
| void Knob::onDragEnd(const event::DragEnd& e) { | void Knob::onDragEnd(const event::DragEnd& e) { | ||||
| if (e.button != GLFW_MOUSE_BUTTON_LEFT) | if (e.button != GLFW_MOUSE_BUTTON_LEFT) | ||||
| return; | return; | ||||
| APP->window->cursorUnlock(); | |||||
| settings::KnobMode km = settings::knobMode; | |||||
| if (km == settings::KNOB_MODE_LINEAR_LOCKED || km == settings::KNOB_MODE_SCALED_LINEAR_LOCKED) { | |||||
| APP->window->cursorUnlock(); | |||||
| } | |||||
| engine::ParamQuantity* pq = getParamQuantity(); | engine::ParamQuantity* pq = getParamQuantity(); | ||||
| if (pq) { | if (pq) { | ||||
| @@ -86,56 +106,123 @@ void Knob::onDragEnd(const event::DragEnd& e) { | |||||
| APP->history->push(h); | APP->history->push(h); | ||||
| } | } | ||||
| } | } | ||||
| ParamWidget::onDragEnd(e); | |||||
| } | } | ||||
| void Knob::onDragMove(const event::DragMove& e) { | void Knob::onDragMove(const event::DragMove& e) { | ||||
| if (e.button != GLFW_MOUSE_BUTTON_LEFT) | if (e.button != GLFW_MOUSE_BUTTON_LEFT) | ||||
| return; | return; | ||||
| settings::KnobMode km = settings::knobMode; | |||||
| bool linearMode = (km < settings::KNOB_MODE_ROTARY_ABSOLUTE) || forceLinear; | |||||
| engine::ParamQuantity* pq = getParamQuantity(); | engine::ParamQuantity* pq = getParamQuantity(); | ||||
| if (pq) { | if (pq) { | ||||
| float range; | |||||
| if (pq->isBounded()) { | |||||
| range = pq->getRange(); | |||||
| } | |||||
| else { | |||||
| // Continuous encoders scale as if their limits are +/-1 | |||||
| range = 2.f; | |||||
| } | |||||
| float delta = (horizontal ? e.mouseDelta.x : -e.mouseDelta.y); | |||||
| delta *= KNOB_SENSITIVITY; | |||||
| delta *= speed; | |||||
| delta *= range; | |||||
| float value = smooth ? pq->getSmoothValue() : pq->getValue(); | |||||
| // Drag slower if Mod is held | |||||
| // Scale by mod keys | |||||
| float modScale = 1.f; | |||||
| // Drag slower if Ctrl is held | |||||
| int mods = APP->window->getMods(); | int mods = APP->window->getMods(); | ||||
| if ((mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { | if ((mods & RACK_MOD_MASK) == RACK_MOD_CTRL) { | ||||
| delta /= 16.f; | |||||
| modScale /= 16.f; | |||||
| } | } | ||||
| // Drag even slower if Mod-Shift is held | |||||
| // Drag even slower if Ctrl-Shift is held | |||||
| if ((mods & RACK_MOD_MASK) == (RACK_MOD_CTRL | GLFW_MOD_SHIFT)) { | if ((mods & RACK_MOD_MASK) == (RACK_MOD_CTRL | GLFW_MOD_SHIFT)) { | ||||
| delta /= 256.f; | |||||
| modScale /= 256.f; | |||||
| } | } | ||||
| if (pq->snapEnabled) { | |||||
| // Replace delta with an accumulated delta since the last integer knob. | |||||
| internal->snapDelta += delta; | |||||
| delta = std::trunc(internal->snapDelta); | |||||
| internal->snapDelta -= delta; | |||||
| // Ratio between parameter value scale / (angle range / 2*pi) | |||||
| float rangeRatio; | |||||
| if (pq->isBounded()) { | |||||
| rangeRatio = pq->getRange(); | |||||
| rangeRatio /= (maxAngle - minAngle) / float(2 * M_PI); | |||||
| } | |||||
| else { | |||||
| rangeRatio = 1.f; | |||||
| } | } | ||||
| // Set value | |||||
| if (smooth) { | |||||
| pq->setSmoothValue(pq->getSmoothValue() + delta); | |||||
| if (linearMode) { | |||||
| float delta = (horizontal ? e.mouseDelta.x : -e.mouseDelta.y); | |||||
| // TODO Put this in settings | |||||
| const float linearSensitivity = 1 / 1000.f; | |||||
| delta *= linearSensitivity; | |||||
| delta *= modScale; | |||||
| delta *= rangeRatio; | |||||
| // Scale delta if in scaled linear knob mode | |||||
| if (km == settings::KNOB_MODE_SCALED_LINEAR_LOCKED || km == settings::KNOB_MODE_SCALED_LINEAR) { | |||||
| float deltaY = (horizontal ? -e.mouseDelta.y : -e.mouseDelta.x); | |||||
| const float pixelTau = 200.f; | |||||
| internal->linearScale *= std::pow(2.f, deltaY / pixelTau); | |||||
| delta *= internal->linearScale; | |||||
| } | |||||
| // Handle value snapping | |||||
| if (pq->snapEnabled) { | |||||
| // Replace delta with an accumulated delta since the last integer knob. | |||||
| internal->snapDelta += delta; | |||||
| delta = std::trunc(internal->snapDelta); | |||||
| internal->snapDelta -= delta; | |||||
| } | |||||
| value += delta; | |||||
| } | } | ||||
| else { | |||||
| pq->setValue(pq->getValue() + delta); | |||||
| else if (internal->rotaryDragEnabled) { | |||||
| math::Vec origin = getAbsoluteOffset(box.size.div(2)); | |||||
| math::Vec deltaPos = APP->scene->mousePos.minus(origin); | |||||
| float angle = deltaPos.arg() + float(M_PI) / 2; | |||||
| bool absoluteRotaryMode = (km == settings::KNOB_MODE_ROTARY_ABSOLUTE) && pq->isBounded(); | |||||
| if (absoluteRotaryMode) { | |||||
| // Find angle closest to midpoint of angle range, mod 2*pi | |||||
| float midAngle = (minAngle + maxAngle) / 2; | |||||
| angle = math::eucMod(angle - midAngle + float(M_PI), float(2 * M_PI)) + midAngle - float(M_PI); | |||||
| value = math::rescale(angle, minAngle, maxAngle, pq->getMinValue(), pq->getMaxValue()); | |||||
| } | |||||
| else { | |||||
| if (!std::isfinite(internal->dragAngle)) { | |||||
| // Set the starting angle | |||||
| internal->dragAngle = angle; | |||||
| } | |||||
| // Find angle closest to last angle, mod 2*pi | |||||
| float deltaAngle = math::eucMod(angle - internal->dragAngle + float(M_PI), float(2 * M_PI)) - float(M_PI); | |||||
| internal->dragAngle = angle; | |||||
| float delta = deltaAngle / float(2 * M_PI) * rangeRatio; | |||||
| delta *= modScale; | |||||
| // Handle value snapping | |||||
| if (pq->snapEnabled) { | |||||
| // Replace delta with an accumulated delta since the last integer knob. | |||||
| internal->snapDelta += delta; | |||||
| delta = std::trunc(internal->snapDelta); | |||||
| internal->snapDelta -= delta; | |||||
| } | |||||
| value += delta; | |||||
| } | |||||
| } | } | ||||
| // Set value | |||||
| if (smooth) | |||||
| pq->setSmoothValue(value); | |||||
| else | |||||
| pq->setValue(value); | |||||
| } | } | ||||
| ParamWidget::onDragMove(e); | ParamWidget::onDragMove(e); | ||||
| } | } | ||||
| void Knob::onDragLeave(const event::DragLeave& e) { | |||||
| if (e.origin == this) { | |||||
| internal->rotaryDragEnabled = true; | |||||
| } | |||||
| ParamWidget::onDragLeave(e); | |||||
| } | |||||
| } // namespace app | } // namespace app | ||||
| } // namespace rack | } // namespace rack | ||||
| @@ -19,6 +19,7 @@ | |||||
| #include <updater.hpp> | #include <updater.hpp> | ||||
| #include <osdialog.h> | #include <osdialog.h> | ||||
| #include <thread> | #include <thread> | ||||
| #include <utility> | |||||
| namespace rack { | namespace rack { | ||||
| @@ -327,19 +328,19 @@ struct KnobModeItem : ui::MenuItem { | |||||
| ui::Menu* createChildMenu() override { | ui::Menu* createChildMenu() override { | ||||
| ui::Menu* menu = new ui::Menu; | ui::Menu* menu = new ui::Menu; | ||||
| static const std::string knobModeNames[] = { | |||||
| "Linear (locked cursor)", | |||||
| "Linear", | |||||
| "Adjustable linear (locked cursor)", | |||||
| "Adjustable linear", | |||||
| "Absolute rotary", | |||||
| "Relative rotary", | |||||
| static const std::vector<std::pair<settings::KnobMode, std::string>> knobModes = { | |||||
| {settings::KNOB_MODE_LINEAR_LOCKED, "Linear (locked cursor)"}, | |||||
| {settings::KNOB_MODE_LINEAR, "Linear"}, | |||||
| {settings::KNOB_MODE_SCALED_LINEAR_LOCKED, "Scaled linear (locked cursor)"}, | |||||
| {settings::KNOB_MODE_SCALED_LINEAR, "Scaled linear"}, | |||||
| {settings::KNOB_MODE_ROTARY_ABSOLUTE, "Absolute rotary"}, | |||||
| {settings::KNOB_MODE_ROTARY_RELATIVE, "Relative rotary"}, | |||||
| }; | }; | ||||
| for (int i = 0; i < (int) LENGTHOF(knobModeNames); i++) { | |||||
| for (const auto& pair : knobModes) { | |||||
| KnobModeValueItem* item = new KnobModeValueItem; | KnobModeValueItem* item = new KnobModeValueItem; | ||||
| item->knobMode = (settings::KnobMode) i; | |||||
| item->text = knobModeNames[i]; | |||||
| item->rightText = CHECKMARK(settings::knobMode == i); | |||||
| item->knobMode = pair.first; | |||||
| item->text = pair.second; | |||||
| item->rightText = CHECKMARK(settings::knobMode == pair.first); | |||||
| menu->addChild(item); | menu->addChild(item); | ||||
| } | } | ||||
| return menu; | return menu; | ||||
| @@ -0,0 +1,23 @@ | |||||
| #include <app/SliderKnob.hpp> | |||||
| namespace rack { | |||||
| namespace app { | |||||
| SliderKnob::SliderKnob() { | |||||
| forceLinear = true; | |||||
| } | |||||
| void SliderKnob::onHover(const event::Hover& e) { | |||||
| // Bypass Knob's circular hitbox detection | |||||
| ParamWidget::onHover(e); | |||||
| } | |||||
| void SliderKnob::onButton(const event::Button& e) { | |||||
| ParamWidget::onButton(e); | |||||
| } | |||||
| } // namespace app | |||||
| } // namespace rack | |||||
| @@ -38,14 +38,15 @@ void SvgKnob::onChange(const event::Change& e) { | |||||
| float value = pq->getSmoothValue(); | float value = pq->getSmoothValue(); | ||||
| float angle; | float angle; | ||||
| if (!pq->isBounded()) { | if (!pq->isBounded()) { | ||||
| // Center unbounded knobs | |||||
| angle = math::rescale(value, -1.f, 1.f, minAngle, maxAngle); | |||||
| // Number of rotations equals value for unbounded range | |||||
| angle = value * (2 * M_PI); | |||||
| } | } | ||||
| else if (pq->getMinValue() == pq->getMaxValue()) { | |||||
| // Center 0 range | |||||
| angle = math::rescale(0.f, -1.f, 1.f, minAngle, maxAngle); | |||||
| else if (pq->getRange() == 0.f) { | |||||
| // Center angle for zero range | |||||
| angle = (minAngle + maxAngle) / 2.f; | |||||
| } | } | ||||
| else { | else { | ||||
| // Proportional angle for finite range | |||||
| angle = math::rescale(value, pq->getMinValue(), pq->getMaxValue(), minAngle, maxAngle); | angle = math::rescale(value, pq->getMinValue(), pq->getMaxValue(), minAngle, maxAngle); | ||||
| } | } | ||||
| angle = std::fmod(angle, 2 * M_PI); | angle = std::fmod(angle, 2 * M_PI); | ||||
| @@ -233,8 +233,9 @@ bool State::handleHover(math::Vec pos, math::Vec mouseDelta) { | |||||
| bool State::handleLeave() { | bool State::handleLeave() { | ||||
| heldKeys.clear(); | heldKeys.clear(); | ||||
| setDragHovered(NULL); | |||||
| setHovered(NULL); | |||||
| // When leaving the window, don't un-hover widgets because the mouse might be dragging. | |||||
| // setDragHovered(NULL); | |||||
| // setHovered(NULL); | |||||
| return true; | return true; | ||||
| } | } | ||||
| @@ -22,7 +22,7 @@ float zoom = 0.0; | |||||
| bool invertZoom = false; | bool invertZoom = false; | ||||
| float cableOpacity = 0.5; | float cableOpacity = 0.5; | ||||
| float cableTension = 0.5; | float cableTension = 0.5; | ||||
| KnobMode knobMode = KNOB_MODE_LINEAR; | |||||
| KnobMode knobMode = KNOB_MODE_LINEAR_LOCKED; | |||||
| float sampleRate = 44100.0; | float sampleRate = 44100.0; | ||||
| int threadCount = 1; | int threadCount = 1; | ||||
| bool paramTooltip = false; | bool paramTooltip = false; | ||||
| @@ -145,7 +145,7 @@ void fromJson(json_t* rootJ) { | |||||
| json_t* allowCursorLockJ = json_object_get(rootJ, "allowCursorLock"); | json_t* allowCursorLockJ = json_object_get(rootJ, "allowCursorLock"); | ||||
| if (allowCursorLockJ) { | if (allowCursorLockJ) { | ||||
| if (json_is_false(allowCursorLockJ)) | if (json_is_false(allowCursorLockJ)) | ||||
| knobMode = KNOB_MODE_LINEAR_UNLOCKED; | |||||
| knobMode = KNOB_MODE_LINEAR; | |||||
| } | } | ||||
| json_t* knobModeJ = json_object_get(rootJ, "knobMode"); | json_t* knobModeJ = json_object_get(rootJ, "knobMode"); | ||||