|
2 | 2 |
|
3 | 3 | from types import SimpleNamespace |
4 | 4 |
|
| 5 | +import pytest |
| 6 | + |
5 | 7 | from tests._mini_eq_imports import import_mini_eq_module |
6 | 8 |
|
7 | 9 | core = import_mini_eq_module("core") |
@@ -94,6 +96,117 @@ def get_value(self) -> float: |
94 | 96 | return self.value |
95 | 97 |
|
96 | 98 |
|
| 99 | +class FakeGraphArea: |
| 100 | + def __init__(self, width: int = 640, height: int = 240) -> None: |
| 101 | + self.width = width |
| 102 | + self.height = height |
| 103 | + |
| 104 | + def get_allocated_width(self) -> int: |
| 105 | + return self.width |
| 106 | + |
| 107 | + def get_allocated_height(self) -> int: |
| 108 | + return self.height |
| 109 | + |
| 110 | + |
| 111 | +class FakeGraphDragGesture: |
| 112 | + def __init__(self, start_x: float, start_y: float, state=0) -> None: |
| 113 | + self.start_x = start_x |
| 114 | + self.start_y = start_y |
| 115 | + self.state = state |
| 116 | + |
| 117 | + def get_start_point(self) -> tuple[bool, float, float]: |
| 118 | + return True, self.start_x, self.start_y |
| 119 | + |
| 120 | + def get_current_event_state(self): |
| 121 | + return self.state |
| 122 | + |
| 123 | + |
| 124 | +class FakeGraphController: |
| 125 | + def __init__(self, bands: list[core.EqBand], preamp_db: float = 0.0) -> None: |
| 126 | + self.bands = bands |
| 127 | + self.preamp_db = preamp_db |
| 128 | + self.frequency_updates: list[tuple[int, float]] = [] |
| 129 | + self.gain_updates: list[tuple[int, float]] = [] |
| 130 | + self.q_updates: list[tuple[int, float]] = [] |
| 131 | + |
| 132 | + def set_band_frequency(self, index: int, frequency: float, *, apply: bool = True) -> bool: |
| 133 | + self.frequency_updates.append((index, frequency)) |
| 134 | + if self.bands[index].frequency == frequency: |
| 135 | + return False |
| 136 | + self.bands[index].frequency = frequency |
| 137 | + return True |
| 138 | + |
| 139 | + def set_band_gain(self, index: int, gain_db: float, *, apply: bool = True) -> bool: |
| 140 | + self.gain_updates.append((index, gain_db)) |
| 141 | + if self.bands[index].gain_db == gain_db: |
| 142 | + return False |
| 143 | + self.bands[index].gain_db = gain_db |
| 144 | + return True |
| 145 | + |
| 146 | + def set_band_q(self, index: int, q_value: float, *, apply: bool = True) -> bool: |
| 147 | + self.q_updates.append((index, q_value)) |
| 148 | + if self.bands[index].q == q_value: |
| 149 | + return False |
| 150 | + self.bands[index].q = q_value |
| 151 | + return True |
| 152 | + |
| 153 | + |
| 154 | +class GraphInteractionWindow(window_graph.MiniEqWindowGraphMixin): |
| 155 | + def __init__(self, bands: list[core.EqBand]) -> None: |
| 156 | + self.controller = FakeGraphController(bands) |
| 157 | + self.graph_area = FakeGraphArea() |
| 158 | + self.visible_band_count = len(bands) |
| 159 | + self.selected_band_index = None |
| 160 | + self.updating_ui = False |
| 161 | + self.drag_band_index = None |
| 162 | + self.drag_start_q = None |
| 163 | + self.engine_updates: list[int] = [] |
| 164 | + self.ui_updates: list[object] = [] |
| 165 | + |
| 166 | + def select_band(self, index: int) -> None: |
| 167 | + self.selected_band_index = index |
| 168 | + |
| 169 | + def schedule_band_engine_update(self, index: int) -> None: |
| 170 | + self.engine_updates.append(index) |
| 171 | + |
| 172 | + def update_band_fader(self, index: int, solo_active: bool | None = None) -> None: |
| 173 | + self.ui_updates.append(("fader", index)) |
| 174 | + |
| 175 | + def update_focus_summary(self) -> None: |
| 176 | + self.ui_updates.append("focus") |
| 177 | + |
| 178 | + def update_selected_band_editor(self) -> None: |
| 179 | + self.ui_updates.append("editor") |
| 180 | + |
| 181 | + def invalidate_graph_response_cache(self) -> None: |
| 182 | + self.ui_updates.append("invalidate") |
| 183 | + |
| 184 | + def queue_response_draw(self) -> None: |
| 185 | + self.ui_updates.append("draw") |
| 186 | + |
| 187 | + def schedule_curve_metadata_refresh(self) -> None: |
| 188 | + self.ui_updates.append("metadata") |
| 189 | + |
| 190 | + def band_point(self, index: int) -> tuple[float, float]: |
| 191 | + width = self.graph_area.get_allocated_width() |
| 192 | + height = self.graph_area.get_allocated_height() |
| 193 | + width_f, height_f, left, right, top, bottom = self.graph_plot_bounds(width, height) |
| 194 | + band = self.controller.bands[index] |
| 195 | + x = self.frequency_to_x(band.frequency, width_f, left, right) |
| 196 | + y = self.db_to_y( |
| 197 | + window_graph.total_response_db( |
| 198 | + self.controller.bands, |
| 199 | + self.controller.preamp_db, |
| 200 | + core.SAMPLE_RATE, |
| 201 | + band.frequency, |
| 202 | + ), |
| 203 | + height_f, |
| 204 | + top, |
| 205 | + bottom, |
| 206 | + ) |
| 207 | + return x, y |
| 208 | + |
| 209 | + |
97 | 210 | class FocusSummaryWindow(window_graph.MiniEqWindowGraphMixin): |
98 | 211 | def __init__( |
99 | 212 | self, |
@@ -306,3 +419,87 @@ def test_curve_metadata_refresh_idle_notifies_control_clients() -> None: |
306 | 419 | assert keep_source is False |
307 | 420 | assert test_window.curve_metadata_refresh_source_id == 0 |
308 | 421 | assert calls == ["status", "preset-state", "control-state"] |
| 422 | + |
| 423 | + |
| 424 | +def test_graph_press_uses_shared_plot_bounds_for_frequency_mapping() -> None: |
| 425 | + window = GraphInteractionWindow( |
| 426 | + [ |
| 427 | + core.EqBand(core.FILTER_TYPES["Bell"], 100.0), |
| 428 | + core.EqBand(core.FILTER_TYPES["Bell"], 1000.0), |
| 429 | + ] |
| 430 | + ) |
| 431 | + calls: list[tuple[float, float, float, float]] = [] |
| 432 | + |
| 433 | + def graph_plot_bounds(width: int, height: int) -> tuple[float, float, float, float, float, float]: |
| 434 | + assert (width, height) == (640, 240) |
| 435 | + return 640.0, 240.0, 11.0, 77.0, 13.0, 17.0 |
| 436 | + |
| 437 | + def x_to_frequency(x: float, width: float, left: float, right: float) -> float: |
| 438 | + calls.append((x, width, left, right)) |
| 439 | + return 1000.0 |
| 440 | + |
| 441 | + window.graph_plot_bounds = graph_plot_bounds |
| 442 | + window.x_to_frequency = x_to_frequency |
| 443 | + |
| 444 | + window.on_graph_pressed(None, 1, 240.0, 120.0) |
| 445 | + |
| 446 | + assert calls == [(240.0, 640.0, 11.0, 77.0)] |
| 447 | + assert window.selected_band_index == 1 |
| 448 | + |
| 449 | + |
| 450 | +def test_graph_drag_preserves_solo_context_when_calculating_other_response() -> None: |
| 451 | + window = GraphInteractionWindow( |
| 452 | + [ |
| 453 | + core.EqBand(core.FILTER_TYPES["Bell"], 1000.0, gain_db=6.0, solo=True), |
| 454 | + core.EqBand(core.FILTER_TYPES["Bell"], 1000.0, gain_db=6.0), |
| 455 | + ] |
| 456 | + ) |
| 457 | + start_x, start_y = window.band_point(0) |
| 458 | + window.drag_band_index = 0 |
| 459 | + window.drag_start_q = window.controller.bands[0].q |
| 460 | + |
| 461 | + window.on_graph_drag_update(FakeGraphDragGesture(start_x, start_y), 0.0, 0.0) |
| 462 | + |
| 463 | + assert window.controller.gain_updates |
| 464 | + assert window.controller.bands[0].gain_db == pytest.approx(6.0) |
| 465 | + |
| 466 | + |
| 467 | +def test_graph_drag_does_not_change_gain_for_non_gain_filters() -> None: |
| 468 | + window = GraphInteractionWindow( |
| 469 | + [ |
| 470 | + core.EqBand(core.FILTER_TYPES["Hi-pass"], 1000.0, gain_db=3.0), |
| 471 | + ] |
| 472 | + ) |
| 473 | + start_x, start_y = window.band_point(0) |
| 474 | + window.drag_band_index = 0 |
| 475 | + window.drag_start_q = window.controller.bands[0].q |
| 476 | + |
| 477 | + window.on_graph_drag_update(FakeGraphDragGesture(start_x, start_y), 40.0, 80.0) |
| 478 | + |
| 479 | + assert window.controller.frequency_updates |
| 480 | + assert window.controller.gain_updates == [] |
| 481 | + assert window.controller.bands[0].gain_db == 3.0 |
| 482 | + |
| 483 | + |
| 484 | +def test_graph_shift_drag_changes_q_without_changing_frequency_or_gain() -> None: |
| 485 | + window = GraphInteractionWindow( |
| 486 | + [ |
| 487 | + core.EqBand(core.FILTER_TYPES["Bell"], 1000.0, gain_db=3.0, q=1.0), |
| 488 | + ] |
| 489 | + ) |
| 490 | + start_x, start_y = window.band_point(0) |
| 491 | + window.drag_band_index = 0 |
| 492 | + window.drag_start_q = 1.0 |
| 493 | + |
| 494 | + window.on_graph_drag_update( |
| 495 | + FakeGraphDragGesture(start_x, start_y, window_graph.Gdk.ModifierType.SHIFT_MASK), |
| 496 | + 90.0, |
| 497 | + -100.0, |
| 498 | + ) |
| 499 | + |
| 500 | + assert window.controller.frequency_updates == [] |
| 501 | + assert window.controller.gain_updates == [] |
| 502 | + assert window.controller.q_updates == [(0, pytest.approx(1.5))] |
| 503 | + assert window.controller.bands[0].frequency == 1000.0 |
| 504 | + assert window.controller.bands[0].gain_db == 3.0 |
| 505 | + assert window.controller.bands[0].q == pytest.approx(1.5) |
0 commit comments