Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 26 additions & 7 deletions src/mini_eq/window_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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]
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
65 changes: 64 additions & 1 deletion tests/test_mini_eq_window_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = []

Expand Down Expand Up @@ -447,18 +449,79 @@ 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(
[
core.EqBand(core.FILTER_TYPES["Bell"], 1000.0, gain_db=6.0, solo=True),
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)
Expand Down