From 3e4c7fa99de1cbdf7548b447555d43957d896ee2 Mon Sep 17 00:00:00 2001 From: Robbert van der Helm Date: Fri, 18 Nov 2022 17:03:13 +0100 Subject: [PATCH] Add Alt+click text entry for X-Y pad --- plugins/diopser/src/editor/theme.css | 20 +++ plugins/diopser/src/editor/xy_pad.rs | 186 +++++++++++++++++++-------- 2 files changed, 153 insertions(+), 53 deletions(-) diff --git a/plugins/diopser/src/editor/theme.css b/plugins/diopser/src/editor/theme.css index 7ed44f53..976f19f4 100644 --- a/plugins/diopser/src/editor/theme.css +++ b/plugins/diopser/src/editor/theme.css @@ -58,6 +58,26 @@ xy-pad:hover .xy-pad__tooltip { transition: opacity 0.1 0; } +/* This is a textbox, we want it to somewhat resemble the tooltip */ +xy-pad .xy-pad__value-entry { + background-color: #e5e5e5; + border-color: #0a0a0a; + border-width: 1px; + child-right: 5px; + child-left: 5px; +} +xy-pad .xy-pad__value-entry .textbox_container { + overflow: visible; + text-wrap: false; + width: auto; +} +xy-pad .xy-pad__value-entry .caret { + background-color: #0a0a0a; +} +xy-pad .xy-pad__value-entry .selection { + background-color: #0a0a0a30; +} + xy-pad__handle { background-color: #e5e5e5; border-color: #0a0a0a; diff --git a/plugins/diopser/src/editor/xy_pad.rs b/plugins/diopser/src/editor/xy_pad.rs index ae7c691c..b5e1e97d 100644 --- a/plugins/diopser/src/editor/xy_pad.rs +++ b/plugins/diopser/src/editor/xy_pad.rs @@ -30,8 +30,6 @@ const HANDLE_WIDTH_PX: f32 = 20.0; /// An X-Y pad that controlers two parameters at the same time by binding them to one of the two /// axes. This specific implementation has a tooltip for the X-axis parmaeter and allows /// Alt+clicking to enter a specific value. -// -// TODO: Text entry for the x-parameter #[derive(Lens)] pub struct XyPad { x_param_base: ParamWidgetBase, @@ -42,6 +40,9 @@ pub struct XyPad { /// NOTE: This is hardcoded to work with the filter frequency parameter. frequency_range: FloatRange, + /// Will be set to `true` when the X-Y pad gets Alt+Click'ed. This will replace the handle with + /// a text input box. + text_input_active: bool, /// Will be set to `true` if we're dragging the parameter. Resetting the parameter or entering a /// text value should not initiate a drag. drag_active: bool, @@ -74,6 +75,10 @@ pub struct GranularDragStatus { enum XyPadEvent { /// The tooltip's size has changed. This causes us to recompute the tooltip position. TooltipWidthChanged, + /// Text input has been cancelled without submitting a new value. + CancelTextInput, + /// A new value has been sent by the text input dialog after pressing Enter. + TextInput(String), } impl XyPad { @@ -100,6 +105,7 @@ impl XyPad { frequency_range: crate::filter_frequency_range(), + text_input_active: false, drag_active: false, granular_drag_status: None, @@ -142,45 +148,25 @@ impl XyPad { ) }); - XyPadHandle::new(cx) - .position_type(PositionType::SelfDirected) - .top(y_position_lens) - .left(x_position_lens) - // TODO: It would be much nicer if this could be set in the - // stylesheet, but Vizia doesn't support that right now - .translate((-(HANDLE_WIDTH_PX / 2.0), -(HANDLE_WIDTH_PX / 2.0))) - .width(Pixels(HANDLE_WIDTH_PX)) - .height(Pixels(HANDLE_WIDTH_PX)) - .hoverable(false); - - // The stylesheet makes the tooltip visible when hovering over the X-Y - // pad. Its position is set to the mouse coordinate in the event - // handler. If there's enough space, the tooltip is drawn at the top - // right of the mouse cursor. - VStack::new(cx, move |cx| { - // The X-parameter is the 'important' one, so we'll display that at - // the bottom since it's closer to the mouse cursor. We'll also - // hardcode the `Q: ` prefix for now to make it a bit clearer and to - // reduce the length difference between the lines a bit. - Label::new( - cx, - y_display_value_lens.map(|value| format!("Q: {value}")), - ); - Label::new(cx, x_display_value_lens); - }) - .class("xy-pad__tooltip") - .left(XyPad::tooltip_pos_x) - .top(XyPad::tooltip_pos_y) - .position_type(PositionType::SelfDirected) - .on_geo_changed(|cx, change_flags| { - // When a new parameter value causes the width of the tooltip to - // change, we must recompute its position so it stays anchored to - // the mouse cursor - if change_flags.intersects(GeometryChanged::WIDTH_CHANGED) { - cx.emit(XyPadEvent::TooltipWidthChanged); - } - }) - .hoverable(false); + // When the X-Y pad gets Alt+clicked, we'll replace it with a text input + // box for the frequency parameter + Binding::new( + cx, + XyPad::text_input_active, + move |cx, text_input_active| { + if text_input_active.get(cx) { + Self::text_input_view(cx, x_display_value_lens.clone()); + } else { + Self::xy_pad_handle_view( + cx, + x_position_lens.clone(), + y_position_lens.clone(), + x_display_value_lens.clone(), + y_display_value_lens.clone(), + ); + } + }, + ); }, ); }, @@ -188,6 +174,71 @@ impl XyPad { ) } + /// Create a text input that's shown in place of the X-Y pad's handle. + fn text_input_view(cx: &mut Context, x_display_value_lens: impl Lens) { + Textbox::new(cx, x_display_value_lens) + .class("xy-pad__value-entry") + .on_submit(|cx, string, success| { + if success { + cx.emit(XyPadEvent::TextInput(string)) + } else { + cx.emit(XyPadEvent::CancelTextInput); + } + }) + .on_build(|cx| { + cx.emit(TextEvent::StartEdit); + cx.emit(TextEvent::SelectAll); + }) + .class("align_center") + .space(Stretch(1.0)); + } + + /// Draws the X-Y pad's handle and the tooltip. + fn xy_pad_handle_view( + cx: &mut Context, + x_position_lens: impl Lens, + y_position_lens: impl Lens, + x_display_value_lens: impl Lens, + y_display_value_lens: impl Lens, + ) { + XyPadHandle::new(cx) + .position_type(PositionType::SelfDirected) + .top(y_position_lens) + .left(x_position_lens) + // TODO: It would be much nicer if this could be set in the + // stylesheet, but Vizia doesn't support that right now + .translate((-(HANDLE_WIDTH_PX / 2.0), -(HANDLE_WIDTH_PX / 2.0))) + .width(Pixels(HANDLE_WIDTH_PX)) + .height(Pixels(HANDLE_WIDTH_PX)) + .hoverable(false); + + // The stylesheet makes the tooltip visible when hovering over the X-Y + // pad. Its position is set to the mouse coordinate in the event + // handler. If there's enough space, the tooltip is drawn at the top + // right of the mouse cursor. + VStack::new(cx, move |cx| { + // The X-parameter is the 'important' one, so we'll display that at + // the bottom since it's closer to the mouse cursor. We'll also + // hardcode the `Q: ` prefix for now to make it a bit clearer and to + // reduce the length difference between the lines a bit. + Label::new(cx, y_display_value_lens.map(|value| format!("Q: {value}"))); + Label::new(cx, x_display_value_lens); + }) + .class("xy-pad__tooltip") + .left(XyPad::tooltip_pos_x) + .top(XyPad::tooltip_pos_y) + .position_type(PositionType::SelfDirected) + .on_geo_changed(|cx, change_flags| { + // When a new parameter value causes the width of the tooltip to + // change, we must recompute its position so it stays anchored to + // the mouse cursor + if change_flags.intersects(GeometryChanged::WIDTH_CHANGED) { + cx.emit(XyPadEvent::TooltipWidthChanged); + } + }) + .hoverable(false); + } + /// Should be called at the start of a drag operation. fn begin_set_parameters(&self, cx: &mut EventContext) { // NOTE: Since the X-parameter is the main parmaeter, we'll always modify this parameter @@ -224,6 +275,7 @@ impl XyPad { let mut x_value = util::remap_current_entity_x_coordinate(cx, x_pos); if snap_to_whole_notes { let x_freq = self.frequency_range.unnormalize(x_value); + let fractional_note = nih_plug::util::freq_to_midi_note(x_freq); let note = fractional_note.round(); let note_freq = nih_plug::util::f32_midi_note_to_freq(note); @@ -257,10 +309,15 @@ impl XyPad { // If there's not enough space at the top right, we'll move the tooltip to the // bottom and/or the left - let tooltip_entity = cx + // NOTE: This is hardcoded to find the tooltip. The Binding also counts as a child. + let binding_entity = cx .tree .get_last_child(cx.current()) .expect("Missing child view in X-Y pad"); + let tooltip_entity = cx + .tree + .get_last_child(binding_entity) + .expect("Missing child view in X-Y pad binding"); let tooltip_bounds = cx.cache.get_bounds(tooltip_entity); // NOTE: The width can vary drastically depending on the frequency value, so we'll // hardcode a minimum width in this comparison to avoid this from jumping @@ -298,16 +355,36 @@ impl View for XyPad { fn event(&mut self, cx: &mut EventContext, event: &mut Event) { event.map(|window_event, meta| { - // With an `if let` clippy complains about the irrefutable match, but in case we add - // more events it's a good idea to prevent this from acting as a wildcard. - let XyPadEvent::TooltipWidthChanged = window_event; + match window_event { + XyPadEvent::TooltipWidthChanged => { + // The tooltip tracks the mouse position, but it also needs to be recomputed when + // the parameter changes while the tooltip is still visible. Without this the + // position maya be off when the parameter is automated, or because of the samll + // delay between interacting with a parameter and the parameter changing. + if cx.hovered() == cx.current() { + self.update_tooltip_pos(cx); + } + } + XyPadEvent::CancelTextInput => { + self.text_input_active = false; + cx.set_active(false); - // The tooltip tracks the mouse position, but it also needs to be recomputed when - // the parameter changes while the tooltip is still visible. Without this the - // position maya be off when the parameter is automated, or because of the samll - // delay between interacting with a parameter and the parameter changing. - if cx.hovered() == cx.current() { - self.update_tooltip_pos(cx); + meta.consume(); + } + XyPadEvent::TextInput(string) => { + // This controls the X-parameter directly + if let Some(normalized_value) = + self.x_param_base.string_to_normalized_value(string) + { + self.x_param_base.begin_set_parameter(cx); + self.x_param_base.set_normalized_value(cx, normalized_value); + self.x_param_base.end_set_parameter(cx); + } + + self.text_input_active = false; + + meta.consume(); + } } meta.consume(); @@ -316,8 +393,11 @@ impl View for XyPad { event.map(|window_event, meta| match window_event { WindowEvent::MouseDown(MouseButton::Left) | WindowEvent::MouseTripleClick(MouseButton::Left) => { - // TODO: Alt+click text entry - if cx.modifiers.command() { + if cx.modifiers.alt() { + // ALt+Click brings up a text entry dialog + self.text_input_active = true; + cx.set_active(true); + } else if cx.modifiers.command() { // Ctrl+Click and double click should reset the parameter instead of initiating // a drag operation self.begin_set_parameters(cx);