Skip to content

Commit b543d8e

Browse files
committed
Merge branch 'major-ic-contextmenu' into major
2 parents 80a0a49 + 222f49c commit b543d8e

13 files changed

Lines changed: 293 additions & 35 deletions

File tree

activity_browser/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def loop(self):
5353
class ABLoader(QtWidgets.QWidget):
5454
def __init__(self):
5555
super().__init__()
56-
self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint)
56+
self.setWindowFlags(Qt.FramelessWindowHint)
5757
self.setWindowTitle("Activity Browser Launcher")
5858
self.setFixedSize(500, 300)
5959

activity_browser/actions/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
from .method.method_duplicate import MethodDuplicate
5656
from .method.method_delete import MethodDelete
5757
from .method.method_open import MethodOpen
58+
from .method.method_rename import MethodRename
59+
from .method.method_meta_modify import MethodMetaModify
5860

5961
from .method.importer.method_importer_ecoinvent import MethodImporterEcoinvent
6062
from .method.importer.method_importer_bw2io import MethodImporterBW2IO

activity_browser/actions/method/method_duplicate.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from activity_browser.mod import bw2data as bd
99
from activity_browser.ui.icons import qicons
1010

11+
from .method_open import MethodOpen
12+
1113
log = getLogger(__name__)
1214

1315

@@ -21,7 +23,7 @@ class MethodDuplicate(ABAction):
2123

2224
@staticmethod
2325
@exception_dialogs
24-
def run(methods: List[tuple], level: str):
26+
def run(methods: List[tuple], level: str = None):
2527
# this action can handle only one selected method for now
2628
selected_method = methods[0]
2729

@@ -57,6 +59,8 @@ def run(methods: List[tuple], level: str):
5759
method.copy(new_name)
5860
log.info(f"Copied method {method.name} into {new_name}")
5961

62+
MethodOpen.run(new_names)
63+
6064

6165
class TupleNameDialog(QtWidgets.QDialog):
6266
def __init__(self, parent=None):
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from typing import List
2+
from logging import getLogger
3+
4+
from qtpy import QtWidgets
5+
6+
from activity_browser import application
7+
from activity_browser.actions.base import ABAction, exception_dialogs
8+
from activity_browser.mod import bw2data as bd
9+
from activity_browser.ui.icons import qicons
10+
11+
log = getLogger(__name__)
12+
13+
14+
class MethodMetaModify(ABAction):
15+
"""
16+
"""
17+
18+
icon = qicons.delete
19+
text = "Modify Impact Category metadata"
20+
21+
@staticmethod
22+
@exception_dialogs
23+
def run(method_name: tuple[str], key: str, value: str):
24+
if method_name not in bd.methods:
25+
log.warning(f"Can't modify metadata for method {method_name} - method not found")
26+
return
27+
28+
bd.methods[method_name][key] = value
29+
bd.methods.flush()
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
from typing import List
2+
from logging import getLogger
3+
4+
from qtpy import QtWidgets
5+
6+
import bw2data as bd
7+
8+
from activity_browser import signals
9+
from activity_browser.ui import widgets
10+
from activity_browser.actions.base import ABAction, exception_dialogs
11+
12+
log = getLogger(__name__)
13+
14+
15+
class MethodRename(ABAction):
16+
"""
17+
Renames an existing method in the Brightway2 data structure.
18+
19+
This method allows renaming a single method by prompting the user for a new name.
20+
It ensures that only one method is renamed at a time, validates the new name, and
21+
updates the method in the Brightway2 database.
22+
23+
Args:
24+
method_name (tuple[str] | list[tuple[str]]): The name of the method to rename.
25+
If a list is provided, it must contain exactly one tuple.
26+
27+
Steps:
28+
- Ensure only one method is being renamed at a time.
29+
- Check if the method exists in the Brightway2 database.
30+
- Open a dialog to prompt the user for the new method name.
31+
- Validate the new name to ensure it is not empty and does not already exist.
32+
- Copy the method to the new name and process it.
33+
- Emit a signal to notify that the method has been renamed.
34+
- Deregister the old method.
35+
36+
Raises:
37+
ValueError: If more than one method is provided for renaming.
38+
RuntimeError: If the method does not exist, the new name is empty, or the new name already exists.
39+
"""
40+
41+
text = "Rename Impact Category"
42+
43+
@staticmethod
44+
@exception_dialogs
45+
def run(method_name: tuple[str] | list[tuple[str]]):
46+
# safeguard: only allow renaming one method at a time
47+
if isinstance(method_name, list):
48+
if len(method_name) != 1 or not isinstance(method_name[0], tuple):
49+
raise ValueError("Can only rename one method at a time.")
50+
method_name = method_name[0]
51+
52+
# check if method exists
53+
if method_name not in bd.methods:
54+
raise RuntimeError(f"Method {method_name} does not exist.")
55+
56+
method = bd.Method(method_name)
57+
58+
# open dialog to get new name
59+
dialog = widgets.ABListEditDialog(method_name)
60+
dialog.exec_()
61+
62+
# if dialog was cancelled, do nothing
63+
if not dialog.result() == QtWidgets.QDialog.Accepted:
64+
return
65+
66+
new_name = dialog.get_data(as_tuple=True)
67+
68+
# check new name validity
69+
if len(new_name) == 0:
70+
raise RuntimeError("Method name cannot be empty.")
71+
72+
if new_name in bd.methods:
73+
raise RuntimeError(f"Method {new_name} already exists.")
74+
75+
# copy method to new name and process
76+
method.copy(new_name).process()
77+
78+
# this should not happen like this, as the model and therefore signals should be handled declaritavely,
79+
# but since method renaming is not native to bw2data we have to do it manually here
80+
signals.method.renamed.emit(method_name, new_name)
81+
82+
# deregister old method
83+
method.deregister()

activity_browser/layouts/pages/impact_category_details/impact_category_details.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from activity_browser.ui import widgets, icons, delegates
99
from activity_browser.bwutils import AB_metadata
1010

11+
from .impact_category_header import ImpactCategoryHeader
12+
1113

1214
class ImpactCategoryDetailsPage(QtWidgets.QWidget):
1315
def __init__(self, name: tuple, parent=None):
@@ -17,54 +19,75 @@ def __init__(self, name: tuple, parent=None):
1719

1820
self.setObjectName(" | ".join(name))
1921

20-
self.model = CharacterizationFactorsModel(self, self.build_df())
22+
self.header = ImpactCategoryHeader(self)
23+
24+
self.model = CharacterizationFactorsModel(self)
2125
self.view = CharacterizationFactorsView(self)
2226
self.view.setModel(self.model)
2327

28+
self.build_layout()
29+
self.connect_signals()
30+
self.sync()
31+
2432
# resizing name and categories columns
2533
self.view.resizeColumnToContents(0)
2634
self.view.resizeColumnToContents(1)
2735

28-
self.build_layout()
29-
self.connect_signals()
30-
3136
def connect_signals(self):
37+
signals.method.renamed.connect(self.on_method_renamed)
3238
signals.method.deleted.connect(self.on_method_deleted)
3339
signals.meta.methods_changed.connect(self.sync)
3440

41+
def on_method_renamed(self, old_name, new_name):
42+
if self.name == old_name:
43+
self.name = new_name
44+
self.setObjectName(" | ".join(new_name))
45+
self.setWindowTitle(" | ".join(new_name))
46+
3547
def on_method_deleted(self, method):
3648
if method.name == self.name:
3749
self.deleteLater()
3850

3951
def sync(self):
52+
if self.name not in bd.methods:
53+
self.deleteLater()
54+
return
55+
4056
self.impact_category = bd.Method(self.name)
4157
self.model.setDataFrame(self.build_df())
58+
self.header.sync()
4259

4360
def build_layout(self):
4461
layout = QtWidgets.QVBoxLayout()
45-
layout.addWidget(widgets.ABLabel.demiBold("Impact Category: " + " - ".join(self.name), self))
62+
layout.addWidget(self.header)
4663
layout.addWidget(widgets.ABHLine(self))
4764
layout.addWidget(self.view)
4865
self.setLayout(layout)
4966

5067
def build_df(self):
51-
df = pd.DataFrame(self.impact_category.load(), columns=["id", "amount"])
68+
df = pd.DataFrame(self.impact_category.load(), columns=["id", "data"])
69+
df["amount"] = df["data"].apply(lambda x: x if isinstance(x, (float, int)) else x.get("amount"))
70+
df["uncertainty"] = df["data"].apply(lambda x: 0 if isinstance(x, (float, int)) else x.get("uncertainty type"))
71+
5272
other = AB_metadata.dataframe[["id", "name", "categories", "database", "unit"]]
5373

54-
df = df.merge(other, left_on="id", right_on="id").rename(columns={"id": "_id"})
74+
df = df.merge(other, left_on="id", right_on="id").rename(columns={"id": "_id", "data": "_cf"})
5575
df["_impact_category_name"] = [self.name for i in range(len(df))]
5676

57-
cols = ["name", "categories", "database", "amount", "unit", "_id", "_impact_category_name"]
77+
cols = ["name", "categories", "database", "amount", "unit", "uncertainty", "_id", "_impact_category_name", "_cf"]
5878
return df[cols]
5979

6080

6181
class CharacterizationFactorsView(widgets.ABTreeView):
6282
defaultColumnDelegates = {
6383
"amount": delegates.FloatDelegate,
6484
"categories": delegates.ListDelegate,
85+
"uncertainty": delegates.UncertaintyDelegate,
6586
}
6687

6788

89+
90+
6891
class ExchangesItem(widgets.ABDataItem):
6992
def flags(self, col: int, key: str):
7093
"""
@@ -78,7 +101,7 @@ def flags(self, col: int, key: str):
78101
QtCore.Qt.ItemFlags: The item flags.
79102
"""
80103
flags = super().flags(col, key)
81-
if key in ["amount"]:
104+
if key in ["amount", "uncertainty"]:
82105
return flags | Qt.ItemFlag.ItemIsEditable
83106
return flags
84107

activity_browser/layouts/pages/impact_category_details/impact_category_header.py

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ def sync(self):
2323
"""
2424
Synchronizes the widget with the current state of the activity.
2525
"""
26+
self.impact_category = self.parent().impact_category
27+
2628
self.clear_layout()
2729
self.layout().addLayout(self.build_grid())
2830

@@ -46,9 +48,12 @@ def build_grid(self) -> QtWidgets.QGridLayout:
4648
grid.setSpacing(10)
4749
grid.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop)
4850

51+
name_label = QtWidgets.QLabel(f"<a href='/'>{' | '.join(self.impact_category.name)}</a>", self)
52+
name_label.linkActivated.connect(lambda: actions.MethodRename.run(self.impact_category.name))
53+
4954
setup = [
50-
("Name:", QtWidgets.QLabel(str(self.impact_category.name)),),
51-
("Unit:", QtWidgets.QLabel(str(self.impact_category.metadata.get("unit", "Undefined"))),),
55+
("Name:", name_label),
56+
("Unit:", ImpactCategoryUnit(self)),
5257
]
5358

5459
# Arrange widgets for display as a grid
@@ -59,25 +64,16 @@ def build_grid(self) -> QtWidgets.QGridLayout:
5964
return grid
6065

6166

62-
class ImpactCategoryName(QtWidgets.QLineEdit):
63-
"""
64-
A widget that displays and edits the name of the activity.
65-
"""
67+
class ImpactCategoryUnit(QtWidgets.QLineEdit):
6668

6769
def __init__(self, parent: ImpactCategoryHeader):
68-
"""
69-
Initializes the ActivityName widget.
70+
name = parent.impact_category.metadata.get("unit", "Undefined")
7071

71-
Args:
72-
parent (ActivityHeader): The parent widget.
73-
"""
74-
super().__init__(str(parent.impact_category.name), parent)
75-
self.editingFinished.connect(self.change_name)
72+
super().__init__(name, parent)
7673

77-
def change_name(self):
78-
"""
79-
Changes the name of the activity if it has been modified.
80-
"""
81-
if self.text() == self.parent().impact_category.name:
74+
self.editingFinished.connect(self.change_unit)
75+
76+
def change_unit(self):
77+
if self.text() == self.parent().impact_category.metadata.get("unit", "Undefined"):
8278
return
83-
# actions.ActivityModify.run(self.parent().activity, "name", self.text())
79+
actions.MethodMetaModify.run(self.parent().impact_category.name, "unit", self.text())

activity_browser/layouts/panes/impact_categories.py

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,14 @@ def load(self):
5252
self.view.setColumnHidden(1, True)
5353
self.view.setColumnHidden(2, True)
5454
self.view.setColumnHidden(3, True)
55+
self.view.sortByColumn(1, Qt.SortOrder.AscendingOrder)
5556

5657
def sync(self):
5758
self.model.setDataFrame(self.build_df())
5859

5960
def build_df(self):
60-
df = pd.DataFrame.from_dict(bd.methods, orient="index")
61-
df.index = df.index.to_flat_index()
62-
df.index.name = "_method_name"
63-
df = df.reset_index()
61+
df = pd.DataFrame(bd.methods.values())
62+
df["_method_name"] = bd.methods.keys()
6463

6564
df["name"] = df["_method_name"].apply(lambda x: x[-1])
6665
df["groups"] = df["_method_name"].apply(lambda x: x[:-1])
@@ -78,6 +77,38 @@ class ImpactCategoriesView(widgets.ABTreeView):
7877
"groups": delegates.ListDelegate,
7978
}
8079

80+
class ContextMenu(widgets.ABMenu):
81+
menuSetup = [
82+
lambda m, p: m.add(actions.MethodOpen, p.selected_impact_categories,
83+
text="Open impact category" if len(p.selected_impact_categories) == 1 else "Open impact categories",
84+
enable=len(p.selected_impact_categories) > 0
85+
),
86+
lambda m, p: m.add(actions.MethodDelete, p.selected_impact_categories,
87+
text="Delete impact category" if len(
88+
p.selected_impact_categories) == 1 else "Delete impact categories",
89+
enable=len(p.selected_impact_categories) > 0
90+
),
91+
lambda m, p: m.add(actions.MethodDuplicate, p.selected_impact_categories,
92+
text="Duplicate impact category",
93+
enable=len(p.selected_impact_categories) == 1
94+
),
95+
lambda m, p: m.add(actions.MethodRename, p.selected_impact_categories,
96+
text="Rename impact category",
97+
enable=len(p.selected_impact_categories) == 1
98+
),
99+
]
100+
101+
@staticmethod
102+
def get_functional_unit_amount(key):
103+
from activity_browser.bwutils import refresh_node
104+
excs = list(refresh_node(key).upstream(["production"]))
105+
exc = excs[0] if len(excs) == 1 else {}
106+
return exc.get("amount", 1.0)
107+
108+
@property
109+
def database_name(self):
110+
return self.parent().parent().database.name
111+
81112
@property
82113
def selected_impact_categories(self):
83114
indices = [i for i in self.selectedIndexes() if i.column() == 0]

activity_browser/signals.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class EdgeSignals(QObject):
2323
class MethodSignals(QObject):
2424
changed: SignalInstance = Signal(object)
2525
deleted: SignalInstance = Signal(object)
26+
renamed: SignalInstance = Signal(tuple, tuple)
2627

2728

2829
class ParameterSignals(QObject):

activity_browser/ui/delegates/uncertainty.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ def createEditor(self, parent, option, index):
3636
elif hasattr(index.internalPointer(), "exchange"):
3737
item = index.internalPointer()
3838
actions.ExchangeUncertaintyModify.run([item.exchange])
39+
elif index.internalPointer()["_impact_category_name"] is not None:
40+
item = index.internalPointer()
41+
actions.CFUncertaintyModify.run(
42+
item["_impact_category_name"], [(item["_id"], item["_cf"]),]
43+
)
3944

4045
def setEditorData(self, editor: QtWidgets.QComboBox, index: QtCore.QModelIndex):
4146
"""Simply use the wizard for updating uncertainties."""

0 commit comments

Comments
 (0)