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