From a0a4ab7fb027b6bb22401fb72b9aabf2493b01e6 Mon Sep 17 00:00:00 2001 From: tangkong Date: Tue, 28 May 2024 10:17:45 -0700 Subject: [PATCH 01/13] BUG: unify usage of `create_tree_from_file`, cleaning up loose ends from previous refactor --- atef/widgets/config/data_active.py | 5 ++--- atef/widgets/config/run_active.py | 10 +++++----- atef/widgets/config/run_base.py | 6 ++++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/atef/widgets/config/data_active.py b/atef/widgets/config/data_active.py index c195fd81..e38a90ee 100644 --- a/atef/widgets/config/data_active.py +++ b/atef/widgets/config/data_active.py @@ -39,7 +39,7 @@ from atef.widgets.config.data_base import DataWidget, SimpleRowWidget from atef.widgets.config.run_base import create_tree_from_file from atef.widgets.config.utils import (ConfigTreeModel, MultiInputDialog, - TableWidgetWithAddRow, TreeItem) + TableWidgetWithAddRow) from atef.widgets.core import DesignerDisplay from atef.widgets.happi import HappiDeviceComponentWidget from atef.widgets.ophyd import OphydAttributeData @@ -517,8 +517,7 @@ def setup_tree(self, config_file: ConfigurationFile) -> None: Passive checkout configuration file dataclass """ # tree data - root_item = TreeItem(data=config_file) - create_tree_from_file(data=config_file.root, parent=root_item) + root_item = create_tree_from_file(data=config_file) model = ConfigTreeModel(data=root_item) diff --git a/atef/widgets/config/run_active.py b/atef/widgets/config/run_active.py index 1415d15e..b4753e49 100644 --- a/atef/widgets/config/run_active.py +++ b/atef/widgets/config/run_active.py @@ -17,7 +17,7 @@ PreparedSetValueStep, PreparedValueToSignal) from atef.widgets.config.data_base import DataWidget from atef.widgets.config.run_base import ResultStatus, create_tree_from_file -from atef.widgets.config.utils import ConfigTreeModel, TreeItem +from atef.widgets.config.utils import ConfigTreeModel from atef.widgets.core import DesignerDisplay from atef.widgets.utils import insert_widget @@ -57,11 +57,11 @@ def __init__(self, *args, data: PreparedPassiveStep, **kwargs): def setup_tree(self): """Sets up ConfigTreeModel with the data from the ConfigurationFile""" - root_item = TreeItem( - data=self.config_file, prepared_data=self.prepared_config + + root_item = create_tree_from_file( + data=self.config_file, + prepared_file=self.prepared_config ) - create_tree_from_file(data=self.config_file.root, parent=root_item, - prepared_file=self.prepared_config) model = ConfigTreeModel(data=root_item) self.tree_view.setModel(model) diff --git a/atef/widgets/config/run_base.py b/atef/widgets/config/run_base.py index ce084d26..e1a08547 100644 --- a/atef/widgets/config/run_base.py +++ b/atef/widgets/config/run_base.py @@ -430,7 +430,7 @@ def __init__(self, *args, **kwargs): def create_tree_from_file( data: Union[ConfigurationFile, ProcedureFile], - prepared_file: Union[PreparedFile, PreparedProcedureFile] + prepared_file: Optional[Union[PreparedFile, PreparedProcedureFile]] = None, ) -> TreeItem: """ Create a TreeItem Tree with items linked to original and prepared dataclasses @@ -463,12 +463,14 @@ def create_tree_from_file( else: raise TypeError("Data was not a passive or active checkout file") - def create_tree(data, parent: TreeItem, prepared_data): + def create_tree(data, parent: TreeItem, prepared_data=None): if not hasattr(data, 'children'): return for child_data in data.children(): if prepared_data: prepared_subset = gather_fn(prepared_data, child_data) + else: + prepared_subset = None item = TreeItem(child_data, prepared_data=prepared_subset) create_tree(child_data, item, prepared_data) parent.addChild(item) From f3a0c43eb197fa5e471ec316e76915255c8a6604 Mon Sep 17 00:00:00 2001 From: tangkong Date: Wed, 29 May 2024 14:18:20 -0700 Subject: [PATCH 02/13] MNT: allow header resizing in checkout tree views --- atef/widgets/config/data_active.py | 2 -- atef/widgets/config/run_active.py | 3 --- 2 files changed, 5 deletions(-) diff --git a/atef/widgets/config/data_active.py b/atef/widgets/config/data_active.py index e38a90ee..dfb47c40 100644 --- a/atef/widgets/config/data_active.py +++ b/atef/widgets/config/data_active.py @@ -522,8 +522,6 @@ def setup_tree(self, config_file: ConfigurationFile) -> None: model = ConfigTreeModel(data=root_item) self.tree_view.setModel(model) - header = self.tree_view.header() - header.setSectionResizeMode(header.ResizeToContents) # Hide the irrelevant status column self.tree_view.setColumnHidden(1, True) self.tree_view.expandAll() diff --git a/atef/widgets/config/run_active.py b/atef/widgets/config/run_active.py index b4753e49..d00f32f4 100644 --- a/atef/widgets/config/run_active.py +++ b/atef/widgets/config/run_active.py @@ -66,9 +66,6 @@ def setup_tree(self): model = ConfigTreeModel(data=root_item) self.tree_view.setModel(model) - # Customize the look of the table - header = self.tree_view.header() - header.setSectionResizeMode(header.ResizeToContents) self.tree_view.header().swapSections(0, 1) self.tree_view.expandAll() From bf97916b3259e4769ec8c5dbecc80301f4349381 Mon Sep 17 00:00:00 2001 From: tangkong Date: Thu, 30 May 2024 10:42:05 -0700 Subject: [PATCH 03/13] GUI: adjust .ui files for refactor, adding staging step --- atef/ui/fill_template_page.ui | 321 ++++++++++++++++------------ atef/ui/template_edit_row_widget.ui | 4 +- 2 files changed, 181 insertions(+), 144 deletions(-) diff --git a/atef/ui/fill_template_page.ui b/atef/ui/fill_template_page.ui index 8b31b004..d25e817b 100644 --- a/atef/ui/fill_template_page.ui +++ b/atef/ui/fill_template_page.ui @@ -6,8 +6,8 @@ 0 0 - 750 - 481 + 702 + 610 @@ -40,154 +40,198 @@ Qt::Vertical - - false - - - - - - - - 0 - 0 - - - - Devices in template - - - - - - - Devices and Signals found in the original checkout - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::NoSelection - - - 2 - - - false - - - - Devices - - - - - Signals - - - - - - - + Qt::Horizontal - - false - - - - - 0 - 0 - - - - - 50 - 0 - - - - QFrame::StyledPanel - - - QFrame::Raised - - + + + QAbstractItemView::NoEditTriggers + + + + - 0 - - - 0 - - - 0 - - - 0 + 5 - - - Open a file to get started! + + + + 0 + 0 + - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + Devices in template - - - - - - 0 - 0 - - - - - 50 - 0 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - 0 - - - 0 - - - 0 - - - 0 - - - - Edit Details + + + Devices and Signals found in the original checkout + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection - - Qt::AlignCenter + + 2 + + false + + + + Devices + + + + + Signals + + - - - + + + + 0 + 0 + + + + + 50 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Open a file to get started! + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + + + 0 + 0 + + + + + 50 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Edit Details + + + Qt::AlignCenter + + + + + + + + + + Stage All + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 1 + + + 0 + + + + + Staged Edits + + + Qt::AlignCenter + + + + + + + + @@ -198,28 +242,21 @@ - Open File - - - - - - - Apply All + Select File - Verify + Validate - Save As... + Apply Staged, Save As... diff --git a/atef/ui/template_edit_row_widget.ui b/atef/ui/template_edit_row_widget.ui index 12caaa36..8e6335f5 100644 --- a/atef/ui/template_edit_row_widget.ui +++ b/atef/ui/template_edit_row_widget.ui @@ -36,7 +36,7 @@ - QDialogButtonBox::Apply|QDialogButtonBox::Retry + QDialogButtonBox::Retry true @@ -76,7 +76,7 @@ - + 20 From 19abf9496d89146835a6b0871e7178dd3964b0ae Mon Sep 17 00:00:00 2001 From: tangkong Date: Thu, 30 May 2024 11:07:45 -0700 Subject: [PATCH 04/13] MNT/REF: add staged_actions GUI logic, replacing all_actions. Update FillTemplatePage to reflect new ui layout --- atef/widgets/config/find_replace.py | 115 ++++++++++++++++++++-------- 1 file changed, 82 insertions(+), 33 deletions(-) diff --git a/atef/widgets/config/find_replace.py b/atef/widgets/config/find_replace.py index e882a2ec..e5bf2d69 100644 --- a/atef/widgets/config/find_replace.py +++ b/atef/widgets/config/find_replace.py @@ -370,17 +370,19 @@ class FillTemplatePage(DesignerDisplay, QtWidgets.QWidget): file_name_label: QtWidgets.QLabel type_label: QtWidgets.QLabel + tree_view: QtWidgets.QTreeView device_table: QtWidgets.QTableWidget # TODO?: filter by device type? look at specific device types? vert_splitter: QtWidgets.QSplitter - horiz_splitter: QtWidgets.QSplitter + overview_splitter: QtWidgets.QSplitter details_list: QtWidgets.QListWidget edits_table: TableWidgetWithAddRow edits_table_placeholder: QtWidgets.QWidget + staged_list: QtWidgets.QListWidget # TODO?: smart initialization? Choosing edits by clicking on devices? # TODO?: starting device / string, happi selector for replace? - apply_all_button: QtWidgets.QPushButton + stage_all_button: QtWidgets.QPushButton open_button: QtWidgets.QPushButton save_button: QtWidgets.QPushButton verify_button: QtWidgets.QPushButton @@ -399,7 +401,7 @@ def __init__( super().__init__(*args, **kwargs) self._window = window self.fp = filepath - self.all_actions: List[FindReplaceAction] = [] + self.staged_actions: List[FindReplaceAction] = [] self._signals: List[str] = [] self._devices: List[str] = [] self.busy_thread = None @@ -411,11 +413,11 @@ def __init__( def setup_ui(self) -> None: self.open_button.clicked.connect(self.open_file) self.save_button.clicked.connect(self.save_file) - self.apply_all_button.clicked.connect(self.apply_all) + self.stage_all_button.clicked.connect(self.stage_all) self.verify_button.clicked.connect(self.verify_changes) - self.horiz_splitter.setSizes([375, 375]) # in pixels, a good first shot - self.vert_splitter.setSizes([175, 375]) + self.overview_splitter.setSizes([375, 375]) # in pixels, a good first shot + self.vert_splitter.setSizes([200, 200, 200, 200,]) horiz_header = self.device_table.horizontalHeader() horiz_header.setSectionResizeMode(horiz_header.Stretch) @@ -427,9 +429,6 @@ def setup_edits_table(self) -> None: row_widget_cls=partial(TemplateEditRowWidget, orig_file=self.orig_file) ) insert_widget(self.edits_table, self.edits_table_placeholder) - self.edits_table.table_updated.connect( - self.update_change_list - ) self.edits_table.setSelectionMode(self.edits_table.SingleSelection) self.edits_table.setSelectionBehavior(self.edits_table.SelectRows) @@ -569,26 +568,21 @@ def save_file(self) -> None: 'File saved successfully' ) - def apply_all(self) -> None: - self.prompt_apply() - self.update_title() - def prompt_apply(self) -> None: # message box with details on remaining changes - self.update_change_list() - if len(self.all_actions) <= 0: + if len(self.staged_actions) <= 0: return reply = QtWidgets.QMessageBox.question( self, - 'Apply remaning edits?', + 'Apply staged edits?', ( 'Would you like to apply the remaining ' - f'({len(self.all_actions)}) edits?' + f'({len(self.staged_actions)}) staged edits?' ) ) if reply == QtWidgets.QMessageBox.Yes: - for action in self.all_actions: + for action in self.staged_actions: action.apply() # clear all rows @@ -597,29 +591,17 @@ def prompt_apply(self) -> None: def update_title(self) -> None: """ - Update the title. Will be the name and the number of unapplied edits + Update the title. Will be the name and the number of staged edits """ if self.fp is None: return file_name = os.path.basename(self.fp) - if len(self.all_actions) > 0: - file_name += f'[{len(self.all_actions)}]' + if len(self.staged_actions) > 0: + file_name += f'[{len(self.staged_actions)}]' self.file_name_label.setText(file_name) self.type_label.setText(type(self.orig_file).__name__) - def update_change_list(self) -> None: - """ - update the global change list, gathering all ``FindReplaceAction``'s - """ - # walk through edits_table, gather list of list of paths - self.all_actions = [] - for row_idx in range(self.edits_table.rowCount()): - template_widget = self.edits_table.cellWidget(row_idx, 0) - self.all_actions.extend(template_widget.get_actions()) - - self.update_title() - def show_changes_from_edit(self, *args, **kwargs) -> None: """ Populate the details_list with each action from the selected edit. @@ -663,10 +645,77 @@ def show_changes_from_edit(self, *args, **kwargs) -> None: ) self._partial_slots.append(remove_slot) + # Disconnect existing apply slot, replace with stage slot + row_widget.button_box.accepted.disconnect(row_widget.apply_action) + stage_slot = WeakPartialMethodSlot( + row_widget, row_widget.button_box.accepted, + self.stage_item_from_details, row_widget.data, l_item, + ) + self._partial_slots.append(stage_slot) + def remove_item_from_details(self, item: QtWidgets.QListWidgetItem) -> None: """remove an item from the details list""" self.details_list.takeItem(self.details_list.row(item)) + def remove_item_from_staged(self, item: QtWidgets.QListWidgetItem) -> None: + """remove an item from the staged list, GUI and internal""" + data = self.staged_list.itemWidget(item).data + self.staged_actions.remove(data) + self.staged_list.takeItem(self.staged_list.row(item)) + self.update_title() + + def stage_item_from_details( + self, + data: FindReplaceAction, + item: QtWidgets.QListWidgetItem + ) -> None: + """stage an item from the details list""" + if any([data.same_path(action.path) for action in self.staged_actions]): + QtWidgets.QMessageBox.information( + self, + 'Duplicate Edit Not Staged', + 'Edit was not staged, had a path matching an already staged path' + ) + return + # Add data to staged list + self.stage_edit(data) + self.refresh_staged_table() + self.remove_item_from_details(item) + + def stage_all(self) -> None: + """Move actions from edit details to staged_actions and refresh table""" + for _ in range(self.details_list.count()): + l_item = self.details_list.item(0) + data = self.details_list.itemWidget(l_item).data + self.stage_item_from_details(data, item=l_item) + self.details_list.takeItem(0) + + def stage_edit(self, edit: FindReplaceAction) -> None: + """Add ``edit`` to the staging list, do nothing to the GUI""" + self.staged_actions.append(edit) + + def refresh_staged_table(self) -> None: + """Re-populate staged edits table""" + self.staged_list.clear() + for action in self.staged_actions: + l_item = QtWidgets.QListWidgetItem() + row_widget = FindReplaceRow(data=action) + l_item.setSizeHint(QtCore.QSize(row_widget.width(), row_widget.height())) + self.staged_list.addItem(l_item) + self.staged_list.setItemWidget(l_item, row_widget) + + remove_slot = WeakPartialMethodSlot( + row_widget, row_widget.remove_item, + self.remove_item_from_staged, l_item + ) + self._partial_slots.append(remove_slot) + + # Hide ok button + ok_button = row_widget.button_box.button(QtWidgets.QDialogButtonBox.Ok) + row_widget.button_box.removeButton(ok_button) + + self.update_title() + class TemplateEditRowWidget(DesignerDisplay, QtWidgets.QWidget): """ From f195af1a5a6eede848b2b6e26820d54904b8f96e Mon Sep 17 00:00:00 2001 From: tangkong Date: Thu, 30 May 2024 11:09:19 -0700 Subject: [PATCH 05/13] MNT: add FindReplaceAction.same_path comparison method --- atef/find_replace.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/atef/find_replace.py b/atef/find_replace.py index d922ad76..2c9ef671 100644 --- a/atef/find_replace.py +++ b/atef/find_replace.py @@ -386,3 +386,8 @@ def apply( return False return True + + def same_path(self, path: List[Tuple[Any, Any]]) -> bool: + """Checks if this FindReplaceAction's path matches ``path``, ignoring objects""" + return all([own_step[1] == other_step[1] + for own_step, other_step in zip(self.path, path)]) From 06f96bfd60771b71f6c332fe40b7f636a804ab02 Mon Sep 17 00:00:00 2001 From: tangkong Date: Thu, 30 May 2024 11:10:44 -0700 Subject: [PATCH 06/13] MNT: remove apply button from TemplateEditRowWidget --- atef/widgets/config/find_replace.py | 30 ----------------------------- 1 file changed, 30 deletions(-) diff --git a/atef/widgets/config/find_replace.py b/atef/widgets/config/find_replace.py index e5bf2d69..91c22c53 100644 --- a/atef/widgets/config/find_replace.py +++ b/atef/widgets/config/find_replace.py @@ -761,12 +761,6 @@ def setup_ui(self): refresh_button.setToolTip('refresh edit details') refresh_button.setIcon(qta.icon('ei.refresh')) - apply_button = self.button_box.button(QtWidgets.QDialogButtonBox.Apply) - apply_button.clicked.connect(self.apply_edits) - apply_button.setText('') - apply_button.setToolTip('apply all changes') - apply_button.setIcon(qta.icon('ei.check')) - # settings menu (regex, case) self.setting_widget = QtWidgets.QWidget() self.setting_layout = QtWidgets.QHBoxLayout() @@ -812,30 +806,6 @@ def update_match_fn(self, *args, **kwargs) -> None: match_fn = get_default_match_fn(self._search_regex) self._match_fn = match_fn - def apply_edits(self) -> None: - """Apply all the actions corresponding to this edit""" - self.refresh_paths() - if len(self.actions) <= 0: - return - - reply = QtWidgets.QMessageBox.question( - self, - 'Apply remaning edits?', - ( - 'Would you like to apply the remaining ' - f'({len(self.actions)}) edits?' - ) - ) - if reply == QtWidgets.QMessageBox.Yes: - for action in self.actions: - success = action.apply() - if not success: - logger.warning(f'action failed {action}') - - self.actions.clear() - - self.refresh_paths() - def refresh_paths(self) -> None: """ Refresh the paths generated by the edit and create FindReplaceActions From 90f9ab935fd60ccfeeea9767576a10aa729d738c Mon Sep 17 00:00:00 2001 From: tangkong Date: Thu, 30 May 2024 11:11:37 -0700 Subject: [PATCH 07/13] ENH: add tree view logic, verify changes on a temp file --- atef/widgets/config/find_replace.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/atef/widgets/config/find_replace.py b/atef/widgets/config/find_replace.py index 91c22c53..02187193 100644 --- a/atef/widgets/config/find_replace.py +++ b/atef/widgets/config/find_replace.py @@ -26,7 +26,8 @@ walk_find_match) from atef.procedure import PreparedProcedureFile, ProcedureFile from atef.util import get_happi_client -from atef.widgets.config.utils import TableWidgetWithAddRow +from atef.widgets.config.run_base import create_tree_from_file +from atef.widgets.config.utils import ConfigTreeModel, TableWidgetWithAddRow from atef.widgets.core import DesignerDisplay from atef.widgets.utils import BusyCursorThread, insert_widget @@ -56,7 +57,7 @@ def verify_file_and_notify( bool the verification success """ - verified, msg = file.verify() + verified, msg = file.validate() if not verified: QtWidgets.QMessageBox.warning( @@ -452,6 +453,7 @@ def open_file(self, *args, filename: Optional[str] = None, **kwargs) -> None: def finish_setup(): self.details_list.clear() self.setup_edits_table() + self.setup_tree_view() self.setup_devices_list() self.update_title() @@ -509,9 +511,25 @@ def _fill_devices_list(self) -> None: for i, dev in enumerate(self._devices): self.device_table.setItem(i, 0, QtWidgets.QTableWidgetItem(dev)) + def setup_tree_view(self) -> None: + """Populate tree view with preview of loaded file""" + root_item = create_tree_from_file(data=self.orig_file) + + model = ConfigTreeModel(data=root_item) + + self.tree_view.setModel(model) + # Hide the irrelevant status column + self.tree_view.setColumnHidden(1, True) + self.tree_view.expandAll() + def verify_changes(self) -> None: + """Apply staged changes and validate copy of file""" if self.orig_file is not None: - verify_file_and_notify(self.orig_file, self) + temp_file = copy.deepcopy(self.orig_file) + for action in self.staged_actions: + action.apply(target=temp_file) + + verify_file_and_notify(temp_file, self) def save_file(self) -> None: if self.orig_file is None: From ed3f93525afbd3606f1be9f0d3c0c03ddd8da4c7 Mon Sep 17 00:00:00 2001 From: tangkong Date: Thu, 30 May 2024 11:19:03 -0700 Subject: [PATCH 08/13] BUG: avoid double-removing item from details list during stage_all --- atef/widgets/config/find_replace.py | 1 - 1 file changed, 1 deletion(-) diff --git a/atef/widgets/config/find_replace.py b/atef/widgets/config/find_replace.py index 02187193..7bed8fe1 100644 --- a/atef/widgets/config/find_replace.py +++ b/atef/widgets/config/find_replace.py @@ -706,7 +706,6 @@ def stage_all(self) -> None: l_item = self.details_list.item(0) data = self.details_list.itemWidget(l_item).data self.stage_item_from_details(data, item=l_item) - self.details_list.takeItem(0) def stage_edit(self, edit: FindReplaceAction) -> None: """Add ``edit`` to the staging list, do nothing to the GUI""" From f25afa341b1eb3618642bd49eeb91e68493edc58 Mon Sep 17 00:00:00 2001 From: tangkong Date: Thu, 30 May 2024 12:55:45 -0700 Subject: [PATCH 09/13] ENH/REF: reveal tree item when details clicked or row selected. move walk_tree_items to avoid circular imports --- atef/widgets/config/find_replace.py | 57 ++++++++++++++++++++++++++++- atef/widgets/config/page.py | 24 +----------- atef/widgets/config/utils.py | 20 ++++++++++ atef/widgets/config/window.py | 5 ++- 4 files changed, 81 insertions(+), 25 deletions(-) diff --git a/atef/widgets/config/find_replace.py b/atef/widgets/config/find_replace.py index 7bed8fe1..6fcf5fb2 100644 --- a/atef/widgets/config/find_replace.py +++ b/atef/widgets/config/find_replace.py @@ -27,7 +27,8 @@ from atef.procedure import PreparedProcedureFile, ProcedureFile from atef.util import get_happi_client from atef.widgets.config.run_base import create_tree_from_file -from atef.widgets.config.utils import ConfigTreeModel, TableWidgetWithAddRow +from atef.widgets.config.utils import (ConfigTreeModel, TableWidgetWithAddRow, + walk_tree_items) from atef.widgets.core import DesignerDisplay from atef.widgets.utils import BusyCursorThread, insert_widget @@ -440,6 +441,20 @@ def setup_edits_table(self) -> None: # on a row different from the currently selected one self.edits_table.row_interacted.connect(self.show_changes_from_edit) + # reveal in tree when detail is highlighted. + # (These may not need to be WPMS but I'll be safe) + reveal_details_slot = WeakPartialMethodSlot( + self.details_list, self.details_list.itemSelectionChanged, + self.reveal_tree_item, self.details_list, + ) + self._partial_slots.append(reveal_details_slot) + + reveal_staged_slot = WeakPartialMethodSlot( + self.staged_list, self.staged_list.itemSelectionChanged, + self.reveal_tree_item, self.staged_list, + ) + self._partial_slots.append(reveal_staged_slot) + def open_file(self, *args, filename: Optional[str] = None, **kwargs) -> None: if filename is None: filename, _ = QtWidgets.QFileDialog.getOpenFileName( @@ -522,6 +537,32 @@ def setup_tree_view(self) -> None: self.tree_view.setColumnHidden(1, True) self.tree_view.expandAll() + def reveal_tree_item( + self, + this_list: QtWidgets.QListWidget, + action: Optional[FindReplaceAction] = None + ) -> None: + """Reveal and highlight the tree-item referenced by ``action``""" + if not action: + curr_widget = this_list.itemWidget(this_list.currentItem()) + if curr_widget is None: # selection has likely been removed + return + + action: FindReplaceAction = curr_widget.data + + model: ConfigTreeModel = self.tree_view.model() + + closest_index = None + # Gather objects in path, ignoring steps that jump into lists etc + path_objs = [part[0] for part in action.path if not isinstance(part[0], str)] + for tree_item in walk_tree_items(model.root_item): + if tree_item.orig_data in path_objs: + closest_index = model.index_from_item(tree_item) + + if closest_index: + self.tree_view.setCurrentIndex(closest_index) + self.tree_view.scrollTo(closest_index) + def verify_changes(self) -> None: """Apply staged changes and validate copy of file""" if self.orig_file is not None: @@ -671,6 +712,13 @@ def show_changes_from_edit(self, *args, **kwargs) -> None: ) self._partial_slots.append(stage_slot) + # reveal tree when deails selected + reveal_slot = WeakPartialMethodSlot( + row_widget, row_widget.details_button.pressed, + self.reveal_tree_item, self.details_list, action=row_widget.data + ) + self._partial_slots.append(reveal_slot) + def remove_item_from_details(self, item: QtWidgets.QListWidgetItem) -> None: """remove an item from the details list""" self.details_list.takeItem(self.details_list.row(item)) @@ -727,6 +775,13 @@ def refresh_staged_table(self) -> None: ) self._partial_slots.append(remove_slot) + # reveal tree when deails selected + reveal_slot = WeakPartialMethodSlot( + row_widget, row_widget.details_button.pressed, + self.reveal_tree_item, self.staged_list, action=row_widget.data + ) + self._partial_slots.append(reveal_slot) + # Hide ok button ok_button = row_widget.button_box.button(QtWidgets.QDialogButtonBox.Ok) row_widget.button_box.removeButton(ok_button) diff --git a/atef/widgets/config/page.py b/atef/widgets/config/page.py index fa63a85a..b17bcb18 100644 --- a/atef/widgets/config/page.py +++ b/atef/widgets/config/page.py @@ -20,8 +20,8 @@ import logging from collections import OrderedDict from functools import partial -from typing import (TYPE_CHECKING, Any, ClassVar, Dict, Generator, List, - Optional, Tuple, Type, Union) +from typing import (TYPE_CHECKING, Any, ClassVar, Dict, List, Optional, Tuple, + Type, Union) from weakref import WeakValueDictionary from pcdsutils.qt.callbacks import WeakPartialMethodSlot @@ -75,26 +75,6 @@ logger = logging.getLogger(__name__) -def walk_tree_items(item: TreeItem) -> Generator[TreeItem, None, None]: - """ - Walk the tree depth first, starting at `item`. - - Parameters - ---------- - item : TreeItem - the root node of the tree to walk - - Yields - ------ - Generator[TreeItem, None, None] - Yields TreeItem from the the tree. - """ - yield item - - for child_idx in range(item.childCount()): - yield from walk_tree_items(item.child(child_idx)) - - def setup_multi_mode_edit_widget( page: PageWidget, target_widget: QWidget, diff --git a/atef/widgets/config/utils.py b/atef/widgets/config/utils.py index 7a4861f0..f7783946 100644 --- a/atef/widgets/config/utils.py +++ b/atef/widgets/config/utils.py @@ -2259,3 +2259,23 @@ def gather_relevant_identifiers( identifiers.append(signal.pvname) return identifiers + + +def walk_tree_items(item: TreeItem) -> Generator[TreeItem, None, None]: + """ + Walk the tree depth first, starting at `item`. + + Parameters + ---------- + item : TreeItem + the root node of the tree to walk + + Yields + ------ + Generator[TreeItem, None, None] + Yields TreeItem from the the tree. + """ + yield item + + for child_idx in range(item.childCount()): + yield from walk_tree_items(item.child(child_idx)) diff --git a/atef/widgets/config/window.py b/atef/widgets/config/window.py index 7f46f585..55b0cf97 100644 --- a/atef/widgets/config/window.py +++ b/atef/widgets/config/window.py @@ -38,10 +38,11 @@ from ..archive_viewer import get_archive_viewer from ..core import DesignerDisplay -from .page import PAGE_MAP, FailPage, PageWidget, RunStepPage, walk_tree_items +from .page import PAGE_MAP, FailPage, PageWidget, RunStepPage from .result_summary import ResultsSummaryWidget from .run_base import create_tree_from_file, make_run_page -from .utils import ConfigTreeModel, MultiInputDialog, Toggle, TreeItem +from .utils import (ConfigTreeModel, MultiInputDialog, Toggle, TreeItem, + walk_tree_items) logger = logging.getLogger(__name__) From a9d61ff9f2babb482f12bab614e81832a364d2e0 Mon Sep 17 00:00:00 2001 From: tangkong Date: Thu, 30 May 2024 13:06:50 -0700 Subject: [PATCH 10/13] DOC: pre-release notes --- .../239-enh_ref_fill_template_page.rst | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 docs/source/upcoming_release_notes/239-enh_ref_fill_template_page.rst diff --git a/docs/source/upcoming_release_notes/239-enh_ref_fill_template_page.rst b/docs/source/upcoming_release_notes/239-enh_ref_fill_template_page.rst new file mode 100644 index 00000000..60e3d176 --- /dev/null +++ b/docs/source/upcoming_release_notes/239-enh_ref_fill_template_page.rst @@ -0,0 +1,22 @@ +239 enh_ref_fill_template_page +############################## + +API Breaks +---------- +- N/A + +Features +-------- +- Refines the flow of FillTemplatePage, adding a clear staging area and tree-view for added clarity + +Bugfixes +-------- +- Unifies usage of ``create_tree_from_file`` + +Maintenance +----------- +- N/A + +Contributors +------------ +- tangkong From b40304f92d9c4ef222d0270a58db45c3516cf38f Mon Sep 17 00:00:00 2001 From: tangkong Date: Fri, 31 May 2024 12:11:34 -0700 Subject: [PATCH 11/13] MNT: review comments, add top level select-file button --- atef/ui/fill_template_page.ui | 96 ++++++++++++++++++++++++----- atef/widgets/config/find_replace.py | 18 +++++- 2 files changed, 96 insertions(+), 18 deletions(-) diff --git a/atef/ui/fill_template_page.ui b/atef/ui/fill_template_page.ui index d25e817b..6f762390 100644 --- a/atef/ui/fill_template_page.ui +++ b/atef/ui/fill_template_page.ui @@ -26,6 +26,19 @@ + + + + + 75 + true + + + + Select File + + + @@ -37,9 +50,15 @@ + + 5 + Qt::Vertical + + 10 + Qt::Horizontal @@ -91,7 +110,7 @@ - Signals + PVs @@ -108,7 +127,7 @@ - 50 + 200 0 @@ -162,7 +181,7 @@ QFrame::Raised - + 0 @@ -176,24 +195,54 @@ 0 - - - Edit Details - - - Qt::AlignCenter + + + 0 - - - - + + + + Edit Details + + + Qt::AlignCenter + + + + + + + - - - Stage All + + + 0 - + + + + Stage All + + + + + + + + + + 5.000000000000000 + + + 90.000000000000000 + + + true + + + + @@ -241,6 +290,12 @@ + + + 75 + true + + Select File @@ -264,6 +319,13 @@ + + + PyDMDrawingLine + QWidget +
pydm.widgets.drawing
+
+
diff --git a/atef/widgets/config/find_replace.py b/atef/widgets/config/find_replace.py index 6fcf5fb2..2a05dd8f 100644 --- a/atef/widgets/config/find_replace.py +++ b/atef/widgets/config/find_replace.py @@ -386,6 +386,7 @@ class FillTemplatePage(DesignerDisplay, QtWidgets.QWidget): stage_all_button: QtWidgets.QPushButton open_button: QtWidgets.QPushButton + top_open_button: QtWidgets.QPushButton save_button: QtWidgets.QPushButton verify_button: QtWidgets.QPushButton @@ -408,12 +409,15 @@ def __init__( self._devices: List[str] = [] self.busy_thread = None self._partial_slots: list[WeakPartialMethodSlot] = [] + self.file_name_label.hide() + self.type_label.hide() if filepath: self.open_file(filename=filepath) self.setup_ui() def setup_ui(self) -> None: self.open_button.clicked.connect(self.open_file) + self.top_open_button.clicked.connect(self.open_file) self.save_button.clicked.connect(self.save_file) self.stage_all_button.clicked.connect(self.stage_all) self.verify_button.clicked.connect(self.verify_changes) @@ -471,6 +475,7 @@ def finish_setup(): self.setup_tree_view() self.setup_devices_list() self.update_title() + self.vert_splitter.setSizes([200, 200, 200, 200,]) self.busy_thread = BusyCursorThread( func=partial(self.load_file, filepath=filename) @@ -653,8 +658,15 @@ def update_title(self) -> None: Update the title. Will be the name and the number of staged edits """ if self.fp is None: + self.type_label.hide() + self.file_name_label.hide() + self.top_open_button.show() return + self.type_label.show() + self.file_name_label.show() + self.top_open_button.hide() + file_name = os.path.basename(self.fp) if len(self.staged_actions) > 0: file_name += f'[{len(self.staged_actions)}]' @@ -752,7 +764,11 @@ def stage_all(self) -> None: """Move actions from edit details to staged_actions and refresh table""" for _ in range(self.details_list.count()): l_item = self.details_list.item(0) - data = self.details_list.itemWidget(l_item).data + widget = self.details_list.itemWidget(l_item) + if widget is None: + return # no details loaded, simple help text item + + data = widget.data self.stage_item_from_details(data, item=l_item) def stage_edit(self, edit: FindReplaceAction) -> None: From 311748870509cb8ed98074792201131db5cf380e Mon Sep 17 00:00:00 2001 From: tangkong Date: Fri, 31 May 2024 15:03:51 -0700 Subject: [PATCH 12/13] MNT: capitalize edits --- atef/widgets/config/find_replace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/atef/widgets/config/find_replace.py b/atef/widgets/config/find_replace.py index 2a05dd8f..021c6fd0 100644 --- a/atef/widgets/config/find_replace.py +++ b/atef/widgets/config/find_replace.py @@ -431,7 +431,7 @@ def setup_ui(self) -> None: def setup_edits_table(self) -> None: # set up add row widget for edits self.edits_table = TableWidgetWithAddRow( - add_row_text='add edit', title_text='edits', + add_row_text='add edit', title_text='Edits', row_widget_cls=partial(TemplateEditRowWidget, orig_file=self.orig_file) ) insert_widget(self.edits_table, self.edits_table_placeholder) From a667b5254b465b1ad597c47a7e29c0e676fb05fb Mon Sep 17 00:00:00 2001 From: tangkong Date: Mon, 3 Jun 2024 08:17:51 -0700 Subject: [PATCH 13/13] DOC: update create_tree_from_file docstring --- atef/widgets/config/run_base.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/atef/widgets/config/run_base.py b/atef/widgets/config/run_base.py index e1a08547..4ae1add0 100644 --- a/atef/widgets/config/run_base.py +++ b/atef/widgets/config/run_base.py @@ -442,8 +442,9 @@ def create_tree_from_file( ---------- data : Union[ConfigurationFile, ProcedureFile] The "original" file (edit-mode, un-prepared) - prepared_file : Union[PreparedFile, PreparedProcedureFile] - The "prepared" file (run-mode, prepared) + prepared_file : Optional[Union[PreparedFile, PreparedProcedureFile]], optional + The "prepared" file (run-mode, prepared), by default None. + If no prepared file is provided, tree will not include gathered prepared data Returns -------