From 76bdc96214f38bf3d3e35ef8a3607133170a0790 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 11:23:53 +0000 Subject: [PATCH 01/10] Initial plan From 3486a59a7a5d18f0ed97e50b791e37049d256b81 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 11:35:31 +0000 Subject: [PATCH 02/10] Add editable checkbox and context menu for impact category modifications - Added editable checkbox to impact category header to control editing state - Implemented context menu with delete option for characterization factors - Added drag-and-drop support for adding biosphere flows to impact categories - Conditional editing: amount and uncertainty fields only editable when checkbox is checked - All functionality gated behind editable state to prevent accidental modifications Co-authored-by: mrvisscher <103424764+mrvisscher@users.noreply.github.com> --- .../impact_category_details.py | 72 +++++++++++++++++++ .../impact_category_header.py | 17 ++++- 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/activity_browser/layouts/pages/impact_category_details/impact_category_details.py b/activity_browser/layouts/pages/impact_category_details/impact_category_details.py index ef7332726..f35c8cb8c 100644 --- a/activity_browser/layouts/pages/impact_category_details/impact_category_details.py +++ b/activity_browser/layouts/pages/impact_category_details/impact_category_details.py @@ -16,6 +16,7 @@ def __init__(self, name: tuple, parent=None): super().__init__(parent) self.name = name self.impact_category = bd.Method(name) + self.is_editable = False self.setObjectName(" | ".join(name)) @@ -57,6 +58,16 @@ def sync(self): self.model.setDataFrame(self.build_df()) self.header.sync() + def on_editable_changed(self, is_editable): + """ + Called when the editable checkbox state changes. + Updates the editable state and refreshes the model. + """ + self.is_editable = is_editable + # Trigger a model reset to update the item flags + self.model.beginResetModel() + self.model.endResetModel() + def build_layout(self): layout = QtWidgets.QVBoxLayout() layout.addWidget(self.header) @@ -85,6 +96,57 @@ class CharacterizationFactorsView(widgets.ABTreeView): "uncertainty": delegates.UncertaintyDelegate, } + class ContextMenu(widgets.ABMenu): + menuSetup = [ + lambda m: m.add(actions.CFRemove, m.impact_category_name, m.char_factors, + enable=bool(m.char_factors) and m.is_editable, + text="Remove characterization factor(s)"), + ] + + @property + def is_editable(self): + return self.parent().parent().is_editable + + @property + def impact_category_name(self): + return self.parent().parent().name + + @property + def char_factors(self): + indexes = self.parent().selectedIndexes() + return [(idx.internalPointer()["_id"], idx.internalPointer()["_cf"]) + for idx in indexes if idx.isValid() and idx.column() == 0] + + def __init__(self, parent): + super().__init__(parent) + self.setAcceptDrops(True) + self.setSortingEnabled(True) + + def dragEnterEvent(self, event): + """Handle drag enter event for biosphere flows.""" + if not self.parent().is_editable: + return + + if event.mimeData().hasFormat("application/bw-nodekeylist"): + event.accept() + + def dropEvent(self, event): + """Handle drop event to add new characterization factors.""" + keys = event.mimeData().retrievePickleData("application/bw-nodekeylist") + + # Filter to only biosphere flows + biosphere_keys = [] + for key in keys: + try: + node = bd.get_node(id=key) + if node.get("type") in ["emission", "natural resource", "inventory indicator", "economic", "social"]: + biosphere_keys.append(key) + except: + pass + + if biosphere_keys: + actions.CFNew.run(self.parent().name, biosphere_keys) + @@ -101,6 +163,16 @@ def flags(self, col: int, key: str): QtCore.Qt.ItemFlags: The item flags. """ flags = super().flags(col, key) + + # Get the parent page to check if it's editable + try: + parent_page = self.model.parent() + if parent_page and hasattr(parent_page, 'is_editable'): + if not parent_page.is_editable: + return flags + except: + pass + if key in ["amount", "uncertainty"]: return flags | Qt.ItemFlag.ItemIsEditable return flags diff --git a/activity_browser/layouts/pages/impact_category_details/impact_category_header.py b/activity_browser/layouts/pages/impact_category_details/impact_category_header.py index 716d06f98..01e086d6f 100644 --- a/activity_browser/layouts/pages/impact_category_details/impact_category_header.py +++ b/activity_browser/layouts/pages/impact_category_details/impact_category_header.py @@ -14,6 +14,7 @@ def __init__(self, parent: QtWidgets.QWidget): """ super().__init__(parent) self.impact_category = parent.impact_category + self.editable_checkbox = None layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) @@ -51,18 +52,32 @@ def build_grid(self) -> QtWidgets.QGridLayout: name_label = QtWidgets.QLabel(f"{' | '.join(self.impact_category.name)}", self) name_label.linkActivated.connect(lambda: actions.MethodRename.run(self.impact_category.name)) + # Create editable checkbox + self.editable_checkbox = QtWidgets.QCheckBox("Editable", self) + self.editable_checkbox.setChecked(False) + self.editable_checkbox.stateChanged.connect(self.on_editable_changed) + setup = [ ("Name:", name_label), ("Unit:", ImpactCategoryUnit(self)), + ("", self.editable_checkbox), ] # Arrange widgets for display as a grid for i, (title, widget) in enumerate(setup): - grid.addWidget(widgets.ABLabel.demiBold(title, self), i, 1) + if title: + grid.addWidget(widgets.ABLabel.demiBold(title, self), i, 1) grid.addWidget(widget, i, 2, 1, 4) return grid + def on_editable_changed(self): + """ + Called when the editable checkbox state changes. + Notifies the parent page to update the view accordingly. + """ + self.parent().on_editable_changed(self.editable_checkbox.isChecked()) + class ImpactCategoryUnit(QtWidgets.QLineEdit): From 8b0648081936db610f3df0c76a9312ea0bec2658 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Oct 2025 11:38:03 +0000 Subject: [PATCH 03/10] Improve drag-and-drop filtering using is_node_biosphere utility - Use is_node_biosphere utility function for cleaner biosphere flow filtering - Simplify dropEvent implementation with list comprehension Co-authored-by: mrvisscher <103424764+mrvisscher@users.noreply.github.com> --- .../impact_category_details.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/activity_browser/layouts/pages/impact_category_details/impact_category_details.py b/activity_browser/layouts/pages/impact_category_details/impact_category_details.py index f35c8cb8c..317a6d17a 100644 --- a/activity_browser/layouts/pages/impact_category_details/impact_category_details.py +++ b/activity_browser/layouts/pages/impact_category_details/impact_category_details.py @@ -6,7 +6,7 @@ from activity_browser import actions, signals from activity_browser.ui import widgets, icons, delegates -from activity_browser.bwutils import AB_metadata +from activity_browser.bwutils import AB_metadata, is_node_biosphere from .impact_category_header import ImpactCategoryHeader @@ -135,14 +135,7 @@ def dropEvent(self, event): keys = event.mimeData().retrievePickleData("application/bw-nodekeylist") # Filter to only biosphere flows - biosphere_keys = [] - for key in keys: - try: - node = bd.get_node(id=key) - if node.get("type") in ["emission", "natural resource", "inventory indicator", "economic", "social"]: - biosphere_keys.append(key) - except: - pass + biosphere_keys = [key for key in keys if is_node_biosphere(key)] if biosphere_keys: actions.CFNew.run(self.parent().name, biosphere_keys) From e8d82dab8d667ef716f5c64ad9b7d60c1b10b0e1 Mon Sep 17 00:00:00 2001 From: Marin Visscher Date: Thu, 16 Oct 2025 15:20:57 +0200 Subject: [PATCH 04/10] Fixes to the header --- .../impact_category_details.py | 61 ++++++++++--------- .../impact_category_header.py | 32 ++++++---- 2 files changed, 52 insertions(+), 41 deletions(-) diff --git a/activity_browser/layouts/pages/impact_category_details/impact_category_details.py b/activity_browser/layouts/pages/impact_category_details/impact_category_details.py index 317a6d17a..69bd5c207 100644 --- a/activity_browser/layouts/pages/impact_category_details/impact_category_details.py +++ b/activity_browser/layouts/pages/impact_category_details/impact_category_details.py @@ -58,16 +58,6 @@ def sync(self): self.model.setDataFrame(self.build_df()) self.header.sync() - def on_editable_changed(self, is_editable): - """ - Called when the editable checkbox state changes. - Updates the editable state and refreshes the model. - """ - self.is_editable = is_editable - # Trigger a model reset to update the item flags - self.model.beginResetModel() - self.model.endResetModel() - def build_layout(self): layout = QtWidgets.QVBoxLayout() layout.addWidget(self.header) @@ -84,8 +74,9 @@ def build_df(self): df = df.merge(other, left_on="id", right_on="id").rename(columns={"id": "_id", "data": "_cf"}) df["_impact_category_name"] = [self.name for i in range(len(df))] + df["_editable"] = self.is_editable - cols = ["name", "categories", "database", "amount", "unit", "uncertainty", "_id", "_impact_category_name", "_cf"] + cols = ["name", "categories", "database", "amount", "unit", "uncertainty", "_id", "_impact_category_name", "_cf", "_editable"] return df[cols] @@ -121,19 +112,44 @@ def __init__(self, parent): super().__init__(parent) self.setAcceptDrops(True) self.setSortingEnabled(True) + self.overlay = None def dragEnterEvent(self, event): - """Handle drag enter event for biosphere flows.""" + """ + Handles the drag enter event. + + Args: + event: The drag enter event. + """ if not self.parent().is_editable: return if event.mimeData().hasFormat("application/bw-nodekeylist"): + self.overlay = widgets.ABDropOverlay(self) + self.overlay.show() event.accept() + def dragLeaveEvent(self, event): + """ + Handles the drag leave event. + + Args: + event: The drag leave event. + """ + # Reset the palette on drag leave + self.overlay.deleteLater() + def dropEvent(self, event): - """Handle drop event to add new characterization factors.""" - keys = event.mimeData().retrievePickleData("application/bw-nodekeylist") - + """ + Handles the drop event. + + Args: + event: The drop event. + """ + self.overlay.deleteLater() + + keys: list = event.mimeData().retrievePickleData("application/bw-nodekeylist") + # Filter to only biosphere flows biosphere_keys = [key for key in keys if is_node_biosphere(key)] @@ -141,8 +157,6 @@ def dropEvent(self, event): actions.CFNew.run(self.parent().name, biosphere_keys) - - class ExchangesItem(widgets.ABDataItem): def flags(self, col: int, key: str): """ @@ -156,17 +170,8 @@ def flags(self, col: int, key: str): QtCore.Qt.ItemFlags: The item flags. """ flags = super().flags(col, key) - - # Get the parent page to check if it's editable - try: - parent_page = self.model.parent() - if parent_page and hasattr(parent_page, 'is_editable'): - if not parent_page.is_editable: - return flags - except: - pass - - if key in ["amount", "uncertainty"]: + + if key in ["amount", "uncertainty"] and self["_editable"]: return flags | Qt.ItemFlag.ItemIsEditable return flags diff --git a/activity_browser/layouts/pages/impact_category_details/impact_category_header.py b/activity_browser/layouts/pages/impact_category_details/impact_category_header.py index 01e086d6f..5e6f072c8 100644 --- a/activity_browser/layouts/pages/impact_category_details/impact_category_header.py +++ b/activity_browser/layouts/pages/impact_category_details/impact_category_header.py @@ -14,7 +14,6 @@ def __init__(self, parent: QtWidgets.QWidget): """ super().__init__(parent) self.impact_category = parent.impact_category - self.editable_checkbox = None layout = QtWidgets.QVBoxLayout() layout.setContentsMargins(0, 0, 0, 0) @@ -49,25 +48,31 @@ def build_grid(self) -> QtWidgets.QGridLayout: grid.setSpacing(10) grid.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) - name_label = QtWidgets.QLabel(f"{' | '.join(self.impact_category.name)}", self) - name_label.linkActivated.connect(lambda: actions.MethodRename.run(self.impact_category.name)) + # check if the method is editable + editable = self.parent().is_editable + if editable: + name_label = QtWidgets.QLabel(f"{' | '.join(self.impact_category.name)}", self) + name_label.linkActivated.connect(lambda: actions.MethodRename.run(self.impact_category.name)) + unit = ImpactCategoryUnit(parent=self) + else: + name_label = QtWidgets.QLabel(" | ".join(self.impact_category.name), self) + unit = QtWidgets.QLabel(self.impact_category.metadata.get("unit", "Undefined"), self) - # Create editable checkbox - self.editable_checkbox = QtWidgets.QCheckBox("Editable", self) - self.editable_checkbox.setChecked(False) - self.editable_checkbox.stateChanged.connect(self.on_editable_changed) + # create edit button + editable_button = QtWidgets.QPushButton("Lock" if editable else "Unlock", self) + editable_button.clicked.connect(self.on_editable_changed) setup = [ ("Name:", name_label), - ("Unit:", ImpactCategoryUnit(self)), - ("", self.editable_checkbox), + ("Unit:", unit), ] + grid.addWidget(editable_button, 0, 8, len(setup), 1, QtCore.Qt.AlignmentFlag.AlignTop) + # Arrange widgets for display as a grid for i, (title, widget) in enumerate(setup): - if title: - grid.addWidget(widgets.ABLabel.demiBold(title, self), i, 1) - grid.addWidget(widget, i, 2, 1, 4) + grid.addWidget(widgets.ABLabel.demiBold(title, self), i, 1, 1, 2) + grid.addWidget(widget, i, 2, 1, 5) return grid @@ -76,7 +81,8 @@ def on_editable_changed(self): Called when the editable checkbox state changes. Notifies the parent page to update the view accordingly. """ - self.parent().on_editable_changed(self.editable_checkbox.isChecked()) + self.parent().is_editable = not self.parent().is_editable + self.parent().sync() class ImpactCategoryUnit(QtWidgets.QLineEdit): From b9aa47aacf51d13ee5ef3e21d5035e5169bbcbc2 Mon Sep 17 00:00:00 2001 From: Marin Visscher Date: Thu, 16 Oct 2025 15:21:26 +0200 Subject: [PATCH 05/10] Seperating drop overlay --- .../pages/activity_details/exchanges_tab.py | 24 +------------------ activity_browser/ui/widgets/__init__.py | 1 + activity_browser/ui/widgets/drop_overlay.py | 24 +++++++++++++++++++ 3 files changed, 26 insertions(+), 23 deletions(-) create mode 100644 activity_browser/ui/widgets/drop_overlay.py diff --git a/activity_browser/layouts/pages/activity_details/exchanges_tab.py b/activity_browser/layouts/pages/activity_details/exchanges_tab.py index 90d45ff89..41bbe6339 100644 --- a/activity_browser/layouts/pages/activity_details/exchanges_tab.py +++ b/activity_browser/layouts/pages/activity_details/exchanges_tab.py @@ -195,7 +195,7 @@ def dragEnterEvent(self, event): return if event.mimeData().hasFormat("application/bw-nodekeylist"): - self.overlay = DropOverlay(self) + self.overlay = widgets.ABDropOverlay(self) self.overlay.show() event.accept() @@ -239,28 +239,6 @@ def get_exchange_type(activity_key: tuple) -> str | None: return None -class DropOverlay(QtWidgets.QWidget): - def __init__(self, parent=None): - super().__init__(parent) - self.setAttribute(Qt.WA_TransparentForMouseEvents) - self.setAttribute(Qt.WA_NoSystemBackground) - self.setAttribute(Qt.WA_TranslucentBackground) - self.setAutoFillBackground(False) - self.resize(parent.size()) - - def paintEvent(self, event): - painter = QtGui.QPainter(self) - painter.setRenderHint(QtGui.QPainter.Antialiasing) - painter.fillRect(self.rect(), QtGui.QColor(0, 100, 255, 200)) # Semi-transparent blue - painter.setPen(Qt.white) - - font = self.font() - font.setBold(True) - - painter.setFont(font) - painter.drawText(self.rect(), Qt.AlignCenter, "Drop here to create new exchanges") - - class RelinkDelegate(delegates.StringDelegate): matched: pd.DataFrame column: str diff --git a/activity_browser/ui/widgets/__init__.py b/activity_browser/ui/widgets/__init__.py index e4aee7b86..9031f6e35 100644 --- a/activity_browser/ui/widgets/__init__.py +++ b/activity_browser/ui/widgets/__init__.py @@ -22,3 +22,4 @@ from .central import CentralTabWidget from .menu import ABMenu from .list_edit_dialog import ABListEditDialog +from .drop_overlay import ABDropOverlay \ No newline at end of file diff --git a/activity_browser/ui/widgets/drop_overlay.py b/activity_browser/ui/widgets/drop_overlay.py new file mode 100644 index 000000000..326b2e2d8 --- /dev/null +++ b/activity_browser/ui/widgets/drop_overlay.py @@ -0,0 +1,24 @@ +from qtpy import QtWidgets, QtGui +from qtpy.QtCore import Qt + + +class ABDropOverlay(QtWidgets.QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setAttribute(Qt.WA_TransparentForMouseEvents) + self.setAttribute(Qt.WA_NoSystemBackground) + self.setAttribute(Qt.WA_TranslucentBackground) + self.setAutoFillBackground(False) + self.resize(parent.size()) + + def paintEvent(self, event): + painter = QtGui.QPainter(self) + painter.setRenderHint(QtGui.QPainter.Antialiasing) + painter.fillRect(self.rect(), QtGui.QColor(0, 100, 255, 200)) # Semi-transparent blue + painter.setPen(Qt.white) + + font = self.font() + font.setBold(True) + + painter.setFont(font) + painter.drawText(self.rect(), Qt.AlignCenter, "Drop here to create new exchanges") From 82aeb6e23734de13845a5812470083f3f94115d8 Mon Sep 17 00:00:00 2001 From: Marin Visscher Date: Thu, 16 Oct 2025 15:21:34 +0200 Subject: [PATCH 06/10] Seperating drop overlay --- activity_browser/layouts/pages/activity_details/graph_tab.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/activity_browser/layouts/pages/activity_details/graph_tab.py b/activity_browser/layouts/pages/activity_details/graph_tab.py index c4196bbd5..b25759b80 100644 --- a/activity_browser/layouts/pages/activity_details/graph_tab.py +++ b/activity_browser/layouts/pages/activity_details/graph_tab.py @@ -9,7 +9,8 @@ import bw_functional as bf from activity_browser import static, bwutils, actions -from .exchanges_tab import DropOverlay, get_exchange_type +from activity_browser.ui import widgets +from .exchanges_tab import get_exchange_type log = getLogger(__name__) @@ -208,7 +209,7 @@ def dragEnterEvent(self, event): return if event.mimeData().hasFormat("application/bw-nodekeylist"): - self.overlay = DropOverlay(self) + self.overlay = widgets.ABDropOverlay(self) self.overlay.show() event.accept() From 4316f375a42683631e33f7058495de588619ea70 Mon Sep 17 00:00:00 2001 From: Marin Visscher Date: Thu, 23 Oct 2025 12:25:50 +0200 Subject: [PATCH 07/10] Fix button placement --- .../impact_category_header.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/activity_browser/layouts/pages/impact_category_details/impact_category_header.py b/activity_browser/layouts/pages/impact_category_details/impact_category_header.py index 5e6f072c8..c81c79ec2 100644 --- a/activity_browser/layouts/pages/impact_category_details/impact_category_header.py +++ b/activity_browser/layouts/pages/impact_category_details/impact_category_header.py @@ -46,6 +46,8 @@ def build_grid(self) -> QtWidgets.QGridLayout: grid = QtWidgets.QGridLayout(self) grid.setContentsMargins(0, 5, 0, 5) grid.setSpacing(10) + grid.setColumnStretch(0, 0) # Column 0 doesn't stretch + grid.setColumnStretch(1, 1) # Column 1 takes remaining space grid.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) # check if the method is editable @@ -59,7 +61,7 @@ def build_grid(self) -> QtWidgets.QGridLayout: unit = QtWidgets.QLabel(self.impact_category.metadata.get("unit", "Undefined"), self) # create edit button - editable_button = QtWidgets.QPushButton("Lock" if editable else "Unlock", self) + editable_button = QtWidgets.QPushButton("Done editing" if editable else "Edit Impact Category", self) editable_button.clicked.connect(self.on_editable_changed) setup = [ @@ -67,12 +69,12 @@ def build_grid(self) -> QtWidgets.QGridLayout: ("Unit:", unit), ] - grid.addWidget(editable_button, 0, 8, len(setup), 1, QtCore.Qt.AlignmentFlag.AlignTop) - # Arrange widgets for display as a grid for i, (title, widget) in enumerate(setup): - grid.addWidget(widgets.ABLabel.demiBold(title, self), i, 1, 1, 2) - grid.addWidget(widget, i, 2, 1, 5) + grid.addWidget(widgets.ABLabel.demiBold(title, self), i, 0) + grid.addWidget(widget, i, 1) + + grid.addWidget(editable_button, 0, 1, QtCore.Qt.AlignmentFlag.AlignRight) return grid From 8233f39624ea22b08c42fdc3fd716313b07cfbdf Mon Sep 17 00:00:00 2001 From: Marin Visscher Date: Thu, 23 Oct 2025 12:45:41 +0200 Subject: [PATCH 08/10] Fix drag drop in IC details --- .../impact_category_details.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/activity_browser/layouts/pages/impact_category_details/impact_category_details.py b/activity_browser/layouts/pages/impact_category_details/impact_category_details.py index 103fd50ed..53d0dec4c 100644 --- a/activity_browser/layouts/pages/impact_category_details/impact_category_details.py +++ b/activity_browser/layouts/pages/impact_category_details/impact_category_details.py @@ -123,12 +123,26 @@ def dragEnterEvent(self, event): event: The drag enter event. """ if not self.parent().is_editable: + event.ignore() return if event.mimeData().hasFormat("application/bw-nodekeylist"): self.overlay = widgets.ABDropOverlay(self) self.overlay.show() event.accept() + else: + event.ignore() + + def dragMoveEvent(self, event): + """Handles the drag move event - required for proper drop indicator.""" + if not self.parent().is_editable: + event.ignore() + return + + if event.mimeData().hasFormat("application/bw-nodekeylist"): + event.accept() + else: + event.ignore() def dragLeaveEvent(self, event): """ @@ -137,8 +151,9 @@ def dragLeaveEvent(self, event): Args: event: The drag leave event. """ - # Reset the palette on drag leave - self.overlay.deleteLater() + if not self.overlay is None: + # Reset the palette on drag leave + self.overlay.deleteLater() def dropEvent(self, event): """ From d8212b82186fa4bde0af1453c7ea35960dd53fc6 Mon Sep 17 00:00:00 2001 From: Marin Visscher Date: Thu, 23 Oct 2025 12:49:59 +0200 Subject: [PATCH 09/10] Fix overlay throwing errors --- .../pages/impact_category_details/impact_category_details.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/activity_browser/layouts/pages/impact_category_details/impact_category_details.py b/activity_browser/layouts/pages/impact_category_details/impact_category_details.py index 53d0dec4c..7f11a6be7 100644 --- a/activity_browser/layouts/pages/impact_category_details/impact_category_details.py +++ b/activity_browser/layouts/pages/impact_category_details/impact_category_details.py @@ -154,6 +154,7 @@ def dragLeaveEvent(self, event): if not self.overlay is None: # Reset the palette on drag leave self.overlay.deleteLater() + self.overlay = None def dropEvent(self, event): """ @@ -163,6 +164,7 @@ def dropEvent(self, event): event: The drop event. """ self.overlay.deleteLater() + self.overlay = None keys: list = event.mimeData().retrievePickleData("application/bw-nodekeylist") From c647c36dfacbdc0a71b0d84847e285899546f959 Mon Sep 17 00:00:00 2001 From: Marin Visscher Date: Thu, 23 Oct 2025 12:58:19 +0200 Subject: [PATCH 10/10] Refactor ICHeader for maintainability --- .../impact_category_header.py | 208 ++++++++++++------ 1 file changed, 145 insertions(+), 63 deletions(-) diff --git a/activity_browser/layouts/pages/impact_category_details/impact_category_header.py b/activity_browser/layouts/pages/impact_category_details/impact_category_header.py index c81c79ec2..c853bdc0d 100644 --- a/activity_browser/layouts/pages/impact_category_details/impact_category_header.py +++ b/activity_browser/layouts/pages/impact_category_details/impact_category_header.py @@ -7,7 +7,8 @@ class ImpactCategoryHeader(QtWidgets.QWidget): def __init__(self, parent: QtWidgets.QWidget): """ - Initializes the ActivityHeader widget. + Initializes the ImpactCategoryHeader widget with a stack layout + that switches between editable and view-only headers. Args: parent (QtWidgets.QWidget): The parent widget. @@ -15,88 +16,169 @@ def __init__(self, parent: QtWidgets.QWidget): super().__init__(parent) self.impact_category = parent.impact_category - layout = QtWidgets.QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(layout) + # Set size policy to only take needed vertical space + self.setSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Maximum + ) + + # Create stack layout to hold both header types + self.stack = QtWidgets.QStackedLayout() + self.stack.setContentsMargins(0, 0, 0, 0) + self.stack.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinimumSize) + + # Create both header widgets + self.view_only_header = ViewOnlyHeader(self) + self.editable_header = EditableHeader(self) + + # Add headers to stack + self.stack.addWidget(self.view_only_header) # Index 0 + self.stack.addWidget(self.editable_header) # Index 1 + + self.setLayout(self.stack) def sync(self): """ - Synchronizes the widget with the current state of the activity. + Synchronizes the widget with the current state of the impact category. + Switches between editable and view-only headers based on edit mode. """ self.impact_category = self.parent().impact_category - - self.clear_layout() - self.layout().addLayout(self.build_grid()) - - def clear_layout(self, layout: QtWidgets.QLayout = None): - layout = layout or self.layout() - - if layout is None: - return - - while layout.count(): - item = layout.takeAt(0) - widget = item.widget() - if widget is not None: - widget.deleteLater() - elif item.layout() is not None: - self.clear_layout(item.layout()) - - def build_grid(self) -> QtWidgets.QGridLayout: - grid = QtWidgets.QGridLayout(self) - grid.setContentsMargins(0, 5, 0, 5) - grid.setSpacing(10) - grid.setColumnStretch(0, 0) # Column 0 doesn't stretch - grid.setColumnStretch(1, 1) # Column 1 takes remaining space - grid.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) - - # check if the method is editable - editable = self.parent().is_editable - if editable: - name_label = QtWidgets.QLabel(f"{' | '.join(self.impact_category.name)}", self) - name_label.linkActivated.connect(lambda: actions.MethodRename.run(self.impact_category.name)) - unit = ImpactCategoryUnit(parent=self) - else: - name_label = QtWidgets.QLabel(" | ".join(self.impact_category.name), self) - unit = QtWidgets.QLabel(self.impact_category.metadata.get("unit", "Undefined"), self) - - # create edit button - editable_button = QtWidgets.QPushButton("Done editing" if editable else "Edit Impact Category", self) - editable_button.clicked.connect(self.on_editable_changed) - - setup = [ - ("Name:", name_label), - ("Unit:", unit), - ] - - # Arrange widgets for display as a grid - for i, (title, widget) in enumerate(setup): - grid.addWidget(widgets.ABLabel.demiBold(title, self), i, 0) - grid.addWidget(widget, i, 1) - grid.addWidget(editable_button, 0, 1, QtCore.Qt.AlignmentFlag.AlignRight) - - return grid + # Update both headers with current data + self.view_only_header.sync() + self.editable_header.sync() + + # Switch to appropriate header based on edit mode + if self.parent().is_editable: + self.stack.setCurrentIndex(1) # Show editable header + else: + self.stack.setCurrentIndex(0) # Show view-only header def on_editable_changed(self): """ - Called when the editable checkbox state changes. + Called when the edit button is clicked. Notifies the parent page to update the view accordingly. """ self.parent().is_editable = not self.parent().is_editable self.parent().sync() -class ImpactCategoryUnit(QtWidgets.QLineEdit): +class ViewOnlyHeader(QtWidgets.QWidget): + """ + A read-only header widget that displays impact category information. + """ + + def __init__(self, parent: ImpactCategoryHeader): + """ + Initializes the view-only header. + + Args: + parent (ImpactCategoryHeader): The parent header widget. + """ + super().__init__(parent) + self.grid = QtWidgets.QGridLayout() + self.grid.setContentsMargins(0, 5, 0, 5) + self.grid.setSpacing(10) + self.grid.setColumnStretch(0, 0) # Column 0 doesn't stretch + self.grid.setColumnStretch(1, 1) # Column 1 takes remaining space + self.grid.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) + self.setLayout(self.grid) + + # Create widgets + self.name_label = QtWidgets.QLabel(self) + self.unit_label = QtWidgets.QLabel(self) + self.edit_button = QtWidgets.QPushButton("Edit Impact Category", self) + self.edit_button.clicked.connect(parent.on_editable_changed) + + # Layout widgets + self.grid.addWidget(widgets.ABLabel.demiBold("Name:", self), 0, 0) + self.grid.addWidget(self.name_label, 0, 1) + self.grid.addWidget(self.edit_button, 0, 1, QtCore.Qt.AlignmentFlag.AlignRight) + self.grid.addWidget(widgets.ABLabel.demiBold("Unit:", self), 1, 0) + self.grid.addWidget(self.unit_label, 1, 1) + + def sync(self): + """ + Updates the displayed information from the current impact category. + """ + impact_category = self.parent().impact_category + self.name_label.setText(" | ".join(impact_category.name)) + self.unit_label.setText(impact_category.metadata.get("unit", "Undefined")) + + +class EditableHeader(QtWidgets.QWidget): + """ + An editable header widget that allows modifying impact category information. + """ def __init__(self, parent: ImpactCategoryHeader): - name = parent.impact_category.metadata.get("unit", "Undefined") + """ + Initializes the editable header. + + Args: + parent (ImpactCategoryHeader): The parent header widget. + """ + super().__init__(parent) + self.grid = QtWidgets.QGridLayout() + self.grid.setContentsMargins(0, 5, 0, 5) + self.grid.setSpacing(10) + self.grid.setColumnStretch(0, 0) # Column 0 doesn't stretch + self.grid.setColumnStretch(1, 1) # Column 1 takes remaining space + self.grid.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) + self.setLayout(self.grid) + + # Create widgets + self.name_label = QtWidgets.QLabel(self) + self.name_label.linkActivated.connect(self._rename_method) + self.unit_edit = ImpactCategoryUnit(self) + self.done_button = QtWidgets.QPushButton("Done editing", self) + self.done_button.clicked.connect(parent.on_editable_changed) + + # Layout widgets + self.grid.addWidget(widgets.ABLabel.demiBold("Name:", self), 0, 0) + self.grid.addWidget(self.name_label, 0, 1) + self.grid.addWidget(self.done_button, 0, 1, QtCore.Qt.AlignmentFlag.AlignRight) + self.grid.addWidget(widgets.ABLabel.demiBold("Unit:", self), 1, 0) + self.grid.addWidget(self.unit_edit, 1, 1) - super().__init__(name, parent) + def sync(self): + """ + Updates the displayed information from the current impact category. + """ + impact_category = self.parent().impact_category + self.name_label.setText(f"{' | '.join(impact_category.name)}") + self.unit_edit.setText(impact_category.metadata.get("unit", "Undefined")) + def _rename_method(self): + """ + Triggers the method rename action. + """ + actions.MethodRename.run(self.parent().impact_category.name) + + +class ImpactCategoryUnit(QtWidgets.QLineEdit): + """ + A line edit widget for editing the impact category unit. + """ + + def __init__(self, parent: EditableHeader): + """ + Initializes the unit edit widget. + + Args: + parent (EditableHeader): The parent editable header widget. + """ + super().__init__(parent) self.editingFinished.connect(self.change_unit) def change_unit(self): - if self.text() == self.parent().impact_category.metadata.get("unit", "Undefined"): + """ + Updates the impact category unit when editing is finished. + """ + impact_category = self.parent().parent().impact_category + current_unit = impact_category.metadata.get("unit", "Undefined") + + if self.text() == current_unit: return - actions.MethodMetaModify.run(self.parent().impact_category.name, "unit", self.text()) \ No newline at end of file + + actions.MethodMetaModify.run(impact_category.name, "unit", self.text()) \ No newline at end of file