Skip to content

Commit 84bd1de

Browse files
author
Patrick Smith
committed
Add "Replace" action to File menu for swapping MDF files with preserved display config
When working with multiple MDF files that share the same signal layout, switching files while keeping the current display configuration (plot windows, selected channels, etc.) previously required multiple manual steps. The new File > Replace action (Ctrl+Shift+O) captures the current display config via to_config(), closes the old tab, and opens the selected file with display_file=config to restore the layout automatically.
1 parent 3f9abd6 commit 84bd1de

File tree

2 files changed

+243
-0
lines changed

2 files changed

+243
-0
lines changed

src/asammdf/gui/widgets/main.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ def __init__(self, files=None, *args, **kwargs):
114114
action.triggered.connect(self.open_folder)
115115
open_group.addAction(action)
116116

117+
action = QtGui.QAction(icon, "Replace", menu)
118+
action.triggered.connect(self.replace_file)
119+
action.setShortcut(QtGui.QKeySequence("Ctrl+Shift+O"))
120+
open_group.addAction(action)
121+
117122
menu.addActions(open_group.actions())
118123

119124
menu.addSeparator()
@@ -1481,6 +1486,85 @@ def open_folder(self, event):
14811486
self.batch._ignore = False
14821487
self.batch.update_channel_tree()
14831488

1489+
def replace_file(self, event=None):
1490+
# Only works in single-file mode with at least one tab open
1491+
if self.stackedWidget.currentIndex() != 0:
1492+
return
1493+
1494+
current_index = self.files.currentIndex()
1495+
if current_index < 0:
1496+
return
1497+
1498+
current_widget = self.files.widget(current_index)
1499+
if current_widget is None:
1500+
return
1501+
1502+
# Capture current display config before closing
1503+
config = current_widget.to_config()
1504+
1505+
# Show single-file open dialog
1506+
system = platform.system().lower()
1507+
if system == "linux":
1508+
file_name, _ = QtWidgets.QFileDialog.getOpenFileName(
1509+
self,
1510+
"Select replacement measurement file",
1511+
self._settings.value("last_opened_path", "", str),
1512+
"CSV (*.csv);;MDF v3 (*.dat *.mdf);;MDF v4(*.mf4 *.mf4z);;DL3/ERG files (*.dl3 *.erg);;All files (*.csv *.dat *.mdf *.mf4 *.mf4z *.dl3 *.erg)",
1513+
"All files (*.csv *.dat *.mdf *.mf4 *.mf4z *.dl3 *.erg)",
1514+
options=QtWidgets.QFileDialog.Option.DontUseNativeDialog,
1515+
)
1516+
else:
1517+
file_name, _ = QtWidgets.QFileDialog.getOpenFileName(
1518+
self,
1519+
"Select replacement measurement file",
1520+
self._settings.value("last_opened_path", "", str),
1521+
"CSV (*.csv);;MDF v3 (*.dat *.mdf);;MDF v4(*.mf4 *.mf4z);;DL3/ERG files (*.dl3 *.erg);;All files (*.csv *.dat *.mdf *.mf4 *.mf4z *.dl3 *.erg)",
1522+
"All files (*.csv *.dat *.mdf *.mf4 *.mf4z *.dl3 *.erg)",
1523+
)
1524+
1525+
if not file_name:
1526+
return
1527+
1528+
self._settings.setValue("last_opened_path", file_name)
1529+
file_name = Path(file_name)
1530+
1531+
# Close old tab
1532+
current_widget.close()
1533+
current_widget.setParent(None)
1534+
current_widget.deleteLater()
1535+
1536+
gc.collect()
1537+
1538+
# Open new file with the captured display config
1539+
try:
1540+
widget = FileWidget(
1541+
file_name,
1542+
self.with_dots,
1543+
self.subplots,
1544+
self.subplots_link,
1545+
self.ignore_value2text_conversions,
1546+
self.display_cg_name,
1547+
self.line_interconnect,
1548+
None,
1549+
False,
1550+
False,
1551+
self,
1552+
ignore_invalidation_bits=self.ignore_invalidation_bits,
1553+
display_file=config,
1554+
)
1555+
except:
1556+
raise
1557+
else:
1558+
widget.mdf.configure(integer_interpolation=self.integer_interpolation)
1559+
self.files.insertTab(current_index, widget, file_name.name)
1560+
self.files.setTabToolTip(current_index, str(file_name))
1561+
self.files.setCurrentIndex(current_index)
1562+
widget.open_new_files.connect(self._open_file)
1563+
widget.full_screen_toggled.connect(self.toggle_fullscreen)
1564+
self.edit_cursor_options()
1565+
1566+
widget.finalize_init()
1567+
14841568
def close_file(self, index):
14851569
widget = self.files.widget(index)
14861570
if widget:

test/asammdf/gui/widgets/Shortcuts/test_MainWindow_Shortcuts.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#!/usr/bin/env python
22
from pathlib import Path
3+
import shutil
34
from unittest import mock
45

6+
from PySide6 import QtCore
57
from PySide6.QtGui import QKeySequence
68
from PySide6.QtTest import QTest
79
from PySide6.QtWidgets import QApplication, QTreeWidgetItemIterator
@@ -216,3 +218,160 @@ def test_create_plot__numeric__tabular_sub_window_shortcut(self):
216218
self.assertEqual(len(matrix_items), signals.columnHeader.model().columnCount() - 1)
217219
for key in matrix_items.values():
218220
self.assertIn(key, signals.pgdf.df.keys())
221+
222+
223+
class TestReplaceFile(TestBase):
224+
@safe_setup
225+
def setUp(self):
226+
super().setUp()
227+
# Create two copies of the test file so we have distinct paths
228+
self.measurement_file = str(
229+
shutil.copy(Path(TestBase.resource, "ASAP2_Demo_V171.mf4"), Path(self.test_workspace, "original.mf4"))
230+
)
231+
self.replacement_file = str(
232+
shutil.copy(Path(TestBase.resource, "ASAP2_Demo_V171.mf4"), Path(self.test_workspace, "replacement.mf4"))
233+
)
234+
self.mw = None
235+
236+
def tearDown(self):
237+
if self.mw:
238+
self.mw.close()
239+
self.mw.deleteLater()
240+
super().tearDown()
241+
242+
def test_replace_file_preserves_tab_position(self):
243+
"""
244+
Test scope:
245+
Ensure that File > Replace replaces the current file tab in place.
246+
Events:
247+
- Open MainWindow with a measurement file
248+
- Trigger Replace via Ctrl+Shift+O with mocked file dialog
249+
Evaluate:
250+
- Tab count remains 1
251+
- Tab label matches replacement file name
252+
- Tab tooltip matches replacement file path
253+
- FileWidget is valid
254+
"""
255+
self.mw = MainWindow(files=(self.measurement_file,))
256+
self.mw.showNormal()
257+
self.processEvents(1)
258+
259+
self.assertEqual(self.mw.files.count(), 1)
260+
self.assertEqual(self.mw.files.tabText(0), "original.mf4")
261+
262+
with mock.patch("asammdf.gui.widgets.main.QtWidgets.QFileDialog.getOpenFileName") as mo_dialog:
263+
mo_dialog.return_value = self.replacement_file, None
264+
QTest.keySequence(self.mw, QKeySequence("Ctrl+Shift+O"))
265+
self.processEvents(1)
266+
267+
# Tab count should still be 1
268+
self.assertEqual(self.mw.files.count(), 1)
269+
# Tab label and tooltip should reflect the new file
270+
self.assertEqual(self.mw.files.tabText(0), "replacement.mf4")
271+
self.assertIn("replacement.mf4", self.mw.files.tabToolTip(0))
272+
# Widget should be a valid FileWidget
273+
self.assertIsInstance(self.mw.files.widget(0), FileWidget)
274+
275+
def test_replace_file_preserves_display_config(self):
276+
"""
277+
Test scope:
278+
Ensure that File > Replace preserves the display configuration (sub-windows)
279+
from the old file and applies it to the new file.
280+
Events:
281+
- Open MainWindow with a measurement file
282+
- Create a Plot sub-window with selected channels
283+
- Trigger Replace via Ctrl+Shift+O with mocked file dialog
284+
Evaluate:
285+
- New file has the same number of sub-windows
286+
- Sub-window type is Plot
287+
- Plot contains the same channels
288+
"""
289+
self.mw = MainWindow(files=(self.measurement_file,))
290+
self.mw.showNormal()
291+
self.processEvents(1)
292+
293+
file_widget = self.mw.files.widget(0)
294+
295+
# Select some channels and create a Plot window
296+
channel_names = []
297+
iterator = QTreeWidgetItemIterator(file_widget.channels_tree)
298+
count = 0
299+
while item := iterator.value():
300+
if item.parent() is not None and count < 3:
301+
item.setCheckState(0, QtCore.Qt.CheckState.Checked)
302+
channel_names.append(item.text(0))
303+
count += 1
304+
iterator += 1
305+
306+
self.assertGreater(len(channel_names), 0, "No channels found to select")
307+
308+
with mock.patch("asammdf.gui.widgets.file.WindowSelectionDialog") as mc_WindowSelectionDialog:
309+
mc_WindowSelectionDialog.return_value.result.return_value = True
310+
mc_WindowSelectionDialog.return_value.selected_type.return_value = "Plot"
311+
QTest.keySequence(self.mw, QKeySequence("F2"))
312+
self.processEvents(0.5)
313+
314+
self.assertEqual(len(file_widget.mdi_area.subWindowList()), 1)
315+
316+
with mock.patch("asammdf.gui.widgets.main.QtWidgets.QFileDialog.getOpenFileName") as mo_dialog:
317+
mo_dialog.return_value = self.replacement_file, None
318+
QTest.keySequence(self.mw, QKeySequence("Ctrl+Shift+O"))
319+
self.processEvents(1)
320+
321+
new_widget = self.mw.files.widget(0)
322+
self.assertIsInstance(new_widget, FileWidget)
323+
# Verify sub-windows were recreated
324+
sub_windows = new_widget.mdi_area.subWindowList()
325+
self.assertEqual(len(sub_windows), 1)
326+
self.assertIsInstance(sub_windows[0].widget(), plot.Plot)
327+
328+
def test_replace_file_no_op_when_no_file_open(self):
329+
"""
330+
Test scope:
331+
Ensure that Replace does nothing when no file tab is open.
332+
Events:
333+
- Open MainWindow with no files
334+
- Trigger Replace via Ctrl+Shift+O
335+
Evaluate:
336+
- Tab count remains 0
337+
- No errors raised
338+
"""
339+
self.mw = MainWindow()
340+
self.mw.showNormal()
341+
self.processEvents(0.5)
342+
343+
self.assertEqual(self.mw.files.count(), 0)
344+
345+
with mock.patch("asammdf.gui.widgets.main.QtWidgets.QFileDialog.getOpenFileName") as mo_dialog:
346+
QTest.keySequence(self.mw, QKeySequence("Ctrl+Shift+O"))
347+
self.processEvents(0.5)
348+
# Dialog should never be shown
349+
mo_dialog.assert_not_called()
350+
351+
self.assertEqual(self.mw.files.count(), 0)
352+
353+
def test_replace_file_no_op_when_dialog_cancelled(self):
354+
"""
355+
Test scope:
356+
Ensure that Replace does nothing when the user cancels the file dialog.
357+
Events:
358+
- Open MainWindow with a measurement file
359+
- Trigger Replace but return empty string from dialog (user cancelled)
360+
Evaluate:
361+
- Original file tab is unchanged
362+
"""
363+
self.mw = MainWindow(files=(self.measurement_file,))
364+
self.mw.showNormal()
365+
self.processEvents(1)
366+
367+
self.assertEqual(self.mw.files.count(), 1)
368+
self.assertEqual(self.mw.files.tabText(0), "original.mf4")
369+
370+
with mock.patch("asammdf.gui.widgets.main.QtWidgets.QFileDialog.getOpenFileName") as mo_dialog:
371+
mo_dialog.return_value = "", None
372+
QTest.keySequence(self.mw, QKeySequence("Ctrl+Shift+O"))
373+
self.processEvents(0.5)
374+
375+
# Original tab should be unchanged
376+
self.assertEqual(self.mw.files.count(), 1)
377+
self.assertEqual(self.mw.files.tabText(0), "original.mf4")

0 commit comments

Comments
 (0)