Skip to content

Commit bfc09a4

Browse files
committed
Decoupled views from controllers (as much as possible). Put a FindReplaceController in place that works a s a functioning link between the finde replace bar. Should make testing much easier.
1 parent 264a6ef commit bfc09a4

File tree

7 files changed

+327
-108
lines changed

7 files changed

+327
-108
lines changed

src/petab_gui/controllers/file_io_controller.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -283,9 +283,7 @@ def _open_file(self, actionable, file_path, sep, mode):
283283
elif actionable == "visualization":
284284
self.main.visualization_controller.open_table(file_path, sep, mode)
285285
elif actionable == "simulation":
286-
self.main.simulation_table_controller.open_table(
287-
file_path, sep, mode
288-
)
286+
self.main.simulation_controller.open_table(file_path, sep, mode)
289287
elif actionable == "data_matrix":
290288
self.main.measurement_controller.process_data_matrix_file(
291289
file_path, mode, sep
@@ -554,7 +552,7 @@ def open_yaml_and_load_files(self, yaml_path=None, mode="overwrite"):
554552
self.main.visualization_controller.clear_table()
555553

556554
# Simulation should be cleared
557-
self.main.simulation_table_controller.clear_table()
555+
self.main.simulation_controller.clear_table()
558556

559557
self.logger.log_message(
560558
"All files opened successfully from the YAML configuration.",
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
"""Find and Replace Controller.
2+
3+
This controller mediates find/replace operations across multiple table
4+
controllers. It owns the coordination logic for searching, highlighting,
5+
and replacing across multiple tables.
6+
"""
7+
8+
9+
class FindReplaceController:
10+
"""Coordinates find/replace operations across multiple table controllers.
11+
12+
This controller provides a clean interface for the FindReplaceBar view to
13+
search, highlight, focus, and replace text across multiple tables without
14+
knowing about individual table controllers. It works as a mediator
15+
encapsulating the coordination logic between multiple table controllers.
16+
"""
17+
18+
def __init__(self, table_controllers: dict):
19+
"""Initialize the find/replace controller.
20+
21+
Args:
22+
table_controllers: Dictionary mapping table names to their
23+
controllers.
24+
Example: {"Measurement Table": measurement_controller, ...}
25+
"""
26+
self.table_controllers = table_controllers
27+
28+
def get_table_names(self) -> list[str]:
29+
"""Get list of available table names.
30+
31+
Returns:
32+
List of table names that can be searched.
33+
"""
34+
return list(self.table_controllers.keys())
35+
36+
def find_text(
37+
self,
38+
search_text: str,
39+
case_sensitive: bool,
40+
regex: bool,
41+
whole_cell: bool,
42+
selected_table_names: list[str],
43+
) -> list[tuple]:
44+
"""Search for text across selected tables.
45+
46+
Args:
47+
search_text: The text to search for
48+
case_sensitive: Whether search is case-sensitive
49+
regex: Whether to use regex matching
50+
whole_cell: Whether to match whole cell only
51+
selected_table_names: List of table names to search in
52+
53+
Returns:
54+
List of tuples: (row, col, table_name, controller)
55+
Each tuple represents a match with its location and associated
56+
controller.
57+
"""
58+
matches = []
59+
60+
for table_name in selected_table_names:
61+
controller = self.table_controllers.get(table_name)
62+
if controller is None:
63+
continue
64+
65+
# Get matches from this table
66+
table_matches = controller.find_text(
67+
search_text, case_sensitive, regex, whole_cell
68+
)
69+
70+
# Extend with table name and controller reference
71+
for row, col in table_matches:
72+
matches.append((row, col, table_name, controller))
73+
74+
return matches
75+
76+
def focus_match(
77+
self, table_name: str, row: int, col: int, with_focus: bool = False
78+
):
79+
"""Focus on a specific match in a table.
80+
81+
Args:
82+
table_name: Name of the table containing the match
83+
row: Row index of the match
84+
col: Column index of the match
85+
with_focus: Whether to give the table widget focus
86+
"""
87+
controller = self.table_controllers.get(table_name)
88+
if controller:
89+
controller.focus_match((row, col), with_focus=with_focus)
90+
91+
def unfocus_match(self, table_name: str):
92+
"""Remove focus from current match in a table.
93+
94+
Args:
95+
table_name: Name of the table to unfocus
96+
"""
97+
controller = self.table_controllers.get(table_name)
98+
if controller:
99+
controller.focus_match(None)
100+
101+
def replace_text(
102+
self,
103+
table_name: str,
104+
row: int,
105+
col: int,
106+
replace_text: str,
107+
search_text: str,
108+
case_sensitive: bool,
109+
regex: bool,
110+
):
111+
"""Replace text in a specific cell.
112+
113+
Args:
114+
table_name: Name of the table containing the cell
115+
row: Row index
116+
col: Column index
117+
replace_text: Text to replace with
118+
search_text: Original search text (for validation)
119+
case_sensitive: Whether the original search was case-sensitive
120+
regex: Whether the original search used regex
121+
"""
122+
controller = self.table_controllers.get(table_name)
123+
if controller:
124+
controller.replace_text(
125+
row=row,
126+
col=col,
127+
replace_text=replace_text,
128+
search_text=search_text,
129+
case_sensitive=case_sensitive,
130+
regex=regex,
131+
)
132+
133+
def replace_all(
134+
self,
135+
search_text: str,
136+
replace_text: str,
137+
case_sensitive: bool,
138+
regex: bool,
139+
matches: list[tuple],
140+
):
141+
"""Replace all matches across tables.
142+
143+
Args:
144+
search_text: Text to search for
145+
replace_text: Text to replace with
146+
case_sensitive: Whether search is case-sensitive
147+
regex: Whether to use regex
148+
matches: List of match tuples from find_text()
149+
"""
150+
# Group matches by controller
151+
controllers_to_update = {}
152+
for row, col, _, controller in matches:
153+
if controller not in controllers_to_update:
154+
controllers_to_update[controller] = []
155+
controllers_to_update[controller].append((row, col))
156+
157+
# Call replace_all on each unique controller
158+
for controller, positions in controllers_to_update.items():
159+
controller.replace_all(
160+
search_text, replace_text, case_sensitive, regex
161+
)
162+
# Emit dataChanged for each affected cell
163+
for row, col in positions:
164+
controller.model.dataChanged.emit(
165+
controller.model.index(row, col),
166+
controller.model.index(row, col),
167+
)
168+
169+
def cleanse_all_highlights(self):
170+
"""Clear highlights from all tables."""
171+
for controller in self.table_controllers.values():
172+
controller.cleanse_highlighted_cells()
173+
174+
def highlight_matches(self, matches: list[tuple]):
175+
"""Highlight matches in their respective tables.
176+
177+
Args:
178+
matches: List of match tuples from find_text()
179+
"""
180+
# Group matches by controller
181+
by_controller = {}
182+
for row, col, _, controller in matches:
183+
if controller not in by_controller:
184+
by_controller[controller] = []
185+
by_controller[controller].append((row, col))
186+
187+
# Highlight in each table
188+
for controller, positions in by_controller.items():
189+
controller.highlight_text(positions)

src/petab_gui/controllers/mother_controller.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from ..views import TaskBar
4545
from ..views.dialogs import NextStepsPanel
4646
from .file_io_controller import FileIOController
47+
from .find_replace_controller import FindReplaceController
4748
from .logger_controller import LoggerController
4849
from .plot_coordinator import PlotCoordinator
4950
from .sbml_controller import SbmlController
@@ -122,7 +123,7 @@ def __init__(self, view, model: PEtabModel):
122123
self.undo_stack,
123124
self,
124125
)
125-
self.simulation_table_controller = MeasurementController(
126+
self.simulation_controller = MeasurementController(
126127
self.view.simulation_dock,
127128
self.model.simulation,
128129
self.logger,
@@ -139,7 +140,7 @@ def __init__(self, view, model: PEtabModel):
139140
self.condition_controller,
140141
self.sbml_controller,
141142
self.visualization_controller,
142-
self.simulation_table_controller,
143+
self.simulation_controller,
143144
]
144145
# File I/O Controller
145146
self.file_io = FileIOController(self)
@@ -149,6 +150,17 @@ def __init__(self, view, model: PEtabModel):
149150
self.validation = ValidationController(self)
150151
# Simulation Controller
151152
self.simulation = SimulationController(self)
153+
# Find/Replace Controller
154+
self.find_replace_controller = FindReplaceController(
155+
{
156+
"Observable Table": self.observable_controller,
157+
"Condition Table": self.condition_controller,
158+
"Parameter Table": self.parameter_controller,
159+
"Measurement Table": self.measurement_controller,
160+
"Visualization Table": self.visualization_controller,
161+
"Simulation Table": self.simulation_controller,
162+
}
163+
)
152164
# Recent Files
153165
self.recent_files_manager = RecentFilesManager(max_files=10)
154166
# Checkbox states for Find + Replace
@@ -290,11 +302,15 @@ def setup_connections(self):
290302
self.measurement_controller,
291303
self.condition_controller,
292304
self.visualization_controller,
293-
self.simulation_table_controller,
305+
self.simulation_controller,
294306
]:
295307
controller.overwritten_df.connect(
296308
self.plot_coordinator._schedule_plot_update
297309
)
310+
self.view.file_open_requested.connect(
311+
partial(self.file_io.open_file, mode="overwrite")
312+
)
313+
self.view.close_requested.connect(self.maybe_close)
298314

299315
def setup_actions(self):
300316
"""Setup actions for the main controller."""
@@ -646,7 +662,7 @@ def active_controller(self):
646662
if active_widget == self.view.visualization_dock.table_view:
647663
return self.visualization_controller
648664
if active_widget == self.view.simulation_dock.table_view:
649-
return self.simulation_table_controller
665+
return self.simulation_controller
650666
return None
651667

652668
def delete_rows(self):
@@ -710,21 +726,21 @@ def open_settings(self):
710726
"measurement": self.measurement_controller.get_columns(),
711727
"condition": self.condition_controller.get_columns(),
712728
"visualization": self.visualization_controller.get_columns(),
713-
"simulation": self.simulation_table_controller.get_columns(),
729+
"simulation": self.simulation_controller.get_columns(),
714730
}
715731
settings_dialog = SettingsDialog(table_columns, self.view)
716732
settings_dialog.exec()
717733

718734
def find(self):
719735
"""Create a find replace bar if it is non existent."""
720736
if self.view.find_replace_bar is None:
721-
self.view.create_find_replace_bar()
737+
self.view.create_find_replace_bar(self.find_replace_controller)
722738
self.view.toggle_find()
723739

724740
def replace(self):
725741
"""Create a find replace bar if it is non existent."""
726742
if self.view.find_replace_bar is None:
727-
self.view.create_find_replace_bar()
743+
self.view.create_find_replace_bar(self.find_replace_controller)
728744
self.view.toggle_replace()
729745

730746
def _toggle_whats_this_mode(self, on: bool):

src/petab_gui/controllers/plot_coordinator.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def init_plotter(self):
6969
"""
7070
self.view.plot_dock.initialize(
7171
self.main.measurement_controller.proxy_model,
72-
self.main.simulation_table_controller.proxy_model,
72+
self.main.simulation_controller.proxy_model,
7373
self.main.condition_controller.proxy_model,
7474
self.main.visualization_controller.proxy_model,
7575
self.model,
@@ -208,8 +208,8 @@ def _on_plot_point_clicked(self, x, y, label, data_type):
208208
proxy = self.main.measurement_controller.proxy_model
209209
view = self.main.measurement_controller.view.table_view
210210
if data_type == "simulation":
211-
proxy = self.main.simulation_table_controller.proxy_model
212-
view = self.main.simulation_table_controller.view.table_view
211+
proxy = self.main.simulation_controller.proxy_model
212+
view = self.main.simulation_controller.view.table_view
213213
obs = label
214214

215215
x_axis_col = "time"
@@ -259,7 +259,7 @@ def column_index(name):
259259
else:
260260
self.plotter.highlight_from_selection(
261261
[row],
262-
proxy=self.main.simulation_table_controller.proxy_model,
262+
proxy=self.main.simulation_controller.proxy_model,
263263
y_axis_col="simulation",
264264
)
265265

@@ -340,7 +340,7 @@ def _on_simulation_selection_changed(self, selected, deselected):
340340
The newly deselected items.
341341
"""
342342
self._handle_table_selection_changed(
343-
self.main.simulation_table_controller.view.table_view,
344-
proxy=self.main.simulation_table_controller.proxy_model,
343+
self.main.simulation_controller.view.table_view,
344+
proxy=self.main.simulation_controller.proxy_model,
345345
y_axis_col="simulation",
346346
)

src/petab_gui/controllers/simulation_controller.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class SimulationController:
2828
The PEtab model being simulated.
2929
logger : LoggerController
3030
The logger for user feedback.
31-
simulation_table_controller : MeasurementController
31+
simulation_controller : MeasurementController
3232
The controller for the simulation results table.
3333
"""
3434

@@ -126,5 +126,5 @@ def simulate(self):
126126
sim_df = simulator.simulate()
127127

128128
# assign to simulation table
129-
self.main.simulation_table_controller.overwrite_df(sim_df)
130-
self.main.simulation_table_controller.model.reset_invalid_cells()
129+
self.main.simulation_controller.overwrite_df(sim_df)
130+
self.main.simulation_controller.model.reset_invalid_cells()

0 commit comments

Comments
 (0)