diff --git a/src/mini_eq/window_graph.py b/src/mini_eq/window_graph.py index 2a6c6ad..72a9e89 100644 --- a/src/mini_eq/window_graph.py +++ b/src/mini_eq/window_graph.py @@ -45,6 +45,8 @@ GRAPH_PLOT_TOP = 26.0 GRAPH_PLOT_BOTTOM = 34.0 SELECTED_BAND_PLACEHOLDER_FREQUENCY_HZ = 1000.0 +GRAPH_DRAG_EDIT_THRESHOLD_PX = 3.0 +GRAPH_POINT_HIT_RADIUS_PX = 32.0 def rounded_rectangle_path(cr, x: float, y: float, width: float, height: float, radius: float) -> None: @@ -352,6 +354,7 @@ def on_graph_pressed(self, gesture: Gtk.GestureClick, _press_count: int, x: floa self.select_band(target) def on_graph_drag_begin(self, gesture: Gtk.GestureDrag, start_x: float, start_y: float) -> None: + self.clear_graph_drag_state() width = self.graph_area.get_allocated_width() height = self.graph_area.get_allocated_height() if width <= 0 or height <= 0: @@ -366,6 +369,8 @@ def on_graph_drag_begin(self, gesture: Gtk.GestureDrag, start_x: float, start_y: best_index = -1 min_dist = float("inf") + best_point_x = 0.0 + best_point_y = 0.0 for index in active: band = self.controller.bands[index] @@ -380,19 +385,31 @@ def on_graph_drag_begin(self, gesture: Gtk.GestureDrag, start_x: float, start_y: if dist < min_dist: min_dist = dist best_index = index + best_point_x = bx + best_point_y = by - if min_dist < 32.0: + if min_dist < GRAPH_POINT_HIT_RADIUS_PX: self.drag_band_index = best_index self.drag_start_q = self.controller.bands[best_index].q + self.drag_start_point_x = best_point_x + self.drag_start_point_y = best_point_y self.select_band(best_index) - else: - self.drag_band_index = None - self.drag_start_q = None + + def clear_graph_drag_state(self) -> None: + self.drag_band_index = None + self.drag_start_q = None + self.drag_start_point_x = None + self.drag_start_point_y = None + + def graph_drag_edit_threshold(self) -> float: + return GRAPH_DRAG_EDIT_THRESHOLD_PX def on_graph_drag_update(self, gesture: Gtk.GestureDrag, offset_x: float, offset_y: float) -> None: drag_index = getattr(self, "drag_band_index", None) if drag_index is None: return + if math.hypot(offset_x, offset_y) < self.graph_drag_edit_threshold(): + return width = self.graph_area.get_allocated_width() height = self.graph_area.get_allocated_height() @@ -422,13 +439,15 @@ def on_graph_drag_update(self, gesture: Gtk.GestureDrag, offset_x: float, offset changed_q = self.controller.set_band_q(drag_index, new_q, apply=False) else: # No shift -> Frequency and Gain adjustment - curr_x = start_x + offset_x + drag_start_point_x = getattr(self, "drag_start_point_x", None) + curr_x = (drag_start_point_x if drag_start_point_x is not None else start_x) + offset_x freq = self.x_to_frequency(curr_x, width_f, left, right) changed_f = self.controller.set_band_frequency(drag_index, freq, apply=False) # Gain adjustment (only for gain-capable filters) if band.filter_type in {FILTER_TYPES["Bell"], FILTER_TYPES["Hi-shelf"], FILTER_TYPES["Lo-shelf"]}: - curr_y = start_y + offset_y + drag_start_point_y = getattr(self, "drag_start_point_y", None) + curr_y = (drag_start_point_y if drag_start_point_y is not None else start_y) + offset_y target_db = self.y_to_db(curr_y, height_f, top, bottom) # To make the point stay under the mouse on the combined curve, we calculate @@ -474,7 +493,7 @@ def on_graph_drag_update(self, gesture: Gtk.GestureDrag, offset_x: float, offset self.schedule_curve_metadata_refresh() def on_graph_drag_end(self, _gesture: Gtk.GestureDrag, _offset_x: float, _offset_y: float) -> None: - self.drag_band_index = None + self.clear_graph_drag_state() def on_preamp_changed(self, scale: Gtk.Scale) -> None: value = scale.get_value() diff --git a/tests/test_mini_eq_window_graph.py b/tests/test_mini_eq_window_graph.py index e3c5eee..29d15bf 100644 --- a/tests/test_mini_eq_window_graph.py +++ b/tests/test_mini_eq_window_graph.py @@ -160,6 +160,8 @@ def __init__(self, bands: list[core.EqBand]) -> None: self.updating_ui = False self.drag_band_index = None self.drag_start_q = None + self.drag_start_point_x = None + self.drag_start_point_y = None self.engine_updates: list[int] = [] self.ui_updates: list[object] = [] @@ -447,6 +449,66 @@ def x_to_frequency(x: float, width: float, left: float, right: float) -> float: assert window.selected_band_index == 1 +def test_graph_zero_offset_drag_selects_without_modifying_band() -> None: + window = GraphInteractionWindow( + [ + core.EqBand(core.FILTER_TYPES["Bell"], 1000.0, gain_db=3.0), + ] + ) + start_x, start_y = window.band_point(0) + + window.on_graph_drag_begin(FakeGraphDragGesture(start_x, start_y), start_x, start_y) + window.on_graph_drag_update(FakeGraphDragGesture(start_x, start_y), 0.0, 0.0) + + assert window.selected_band_index == 0 + assert window.controller.frequency_updates == [] + assert window.controller.gain_updates == [] + assert window.controller.q_updates == [] + assert window.engine_updates == [] + assert window.ui_updates == [] + + +def test_graph_drag_waits_for_edit_threshold_before_modifying_band() -> None: + window = GraphInteractionWindow( + [ + core.EqBand(core.FILTER_TYPES["Bell"], 1000.0, gain_db=3.0), + ] + ) + window.graph_drag_edit_threshold = lambda: 8.0 + start_x, start_y = window.band_point(0) + + window.on_graph_drag_begin(FakeGraphDragGesture(start_x, start_y), start_x, start_y) + window.on_graph_drag_update(FakeGraphDragGesture(start_x, start_y), 4.0, 3.0) + + assert window.controller.frequency_updates == [] + assert window.controller.gain_updates == [] + assert window.engine_updates == [] + + +def test_graph_drag_uses_band_point_as_anchor_to_avoid_click_jump() -> None: + window = GraphInteractionWindow( + [ + core.EqBand(core.FILTER_TYPES["Hi-pass"], 1000.0, gain_db=3.0), + ] + ) + window.graph_drag_edit_threshold = lambda: 4.0 + point_x, point_y = window.band_point(0) + captured_x: list[float] = [] + + def x_to_frequency(x: float, _width: float, _left: float, _right: float) -> float: + captured_x.append(x) + return 1200.0 + + window.x_to_frequency = x_to_frequency + + window.on_graph_drag_begin(FakeGraphDragGesture(point_x + 20.0, point_y), point_x + 20.0, point_y) + window.on_graph_drag_update(FakeGraphDragGesture(point_x + 20.0, point_y), 10.0, 0.0) + + assert captured_x == [pytest.approx(point_x + 10.0)] + assert window.controller.frequency_updates == [(0, 1200.0)] + assert window.controller.gain_updates == [] + + def test_graph_drag_preserves_solo_context_when_calculating_other_response() -> None: window = GraphInteractionWindow( [ @@ -454,11 +516,12 @@ def test_graph_drag_preserves_solo_context_when_calculating_other_response() -> core.EqBand(core.FILTER_TYPES["Bell"], 1000.0, gain_db=6.0), ] ) + window.graph_drag_edit_threshold = lambda: 4.0 start_x, start_y = window.band_point(0) window.drag_band_index = 0 window.drag_start_q = window.controller.bands[0].q - window.on_graph_drag_update(FakeGraphDragGesture(start_x, start_y), 0.0, 0.0) + window.on_graph_drag_update(FakeGraphDragGesture(start_x, start_y), 12.0, 0.0) assert window.controller.gain_updates assert window.controller.bands[0].gain_db == pytest.approx(6.0)