Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions .github/workflows/testing.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ jobs:
steps:
- uses: actions/checkout@v4

# general Python setup
- name: Set up Python ${{ matrix.py-version }}
uses: actions/setup-python@v5
with:
Expand All @@ -37,11 +36,9 @@ jobs:
python -VV
python -m pip install --upgrade pip setuptools wheel

# install testing
- name: Install package and test deps
run: |
pip install .[testing] # install the package and the testing deps
pip install .[testing]

- name: Test with pytest
run: |
pytest -s --no-header --no-summary -q --no-cov
run: pytest -q --no-cov -o log_cli=false
2 changes: 1 addition & 1 deletion activity_browser/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def apply_settings(self, load=False):
else:
hint = QtCore.Qt.ColorScheme.Unknown

app.application.styleHints().setColorScheme(hint)
app.application.apply_color_scheme(hint)

# apply pane tab position
position = app.settings["appearance"]["pane_tab_position"]
Expand Down
5 changes: 2 additions & 3 deletions activity_browser/app/menu_bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from qtpy.QtCore import QSize, QUrl, Qt

from activity_browser import app
from activity_browser.bwutils.commontasks import get_templates
from activity_browser.bwutils.commontasks import fetch_remote_projects, get_templates

from ..ui.icons import qicons

Expand Down Expand Up @@ -111,8 +111,7 @@ def __init__(self, parent=None):

def get_projects(self):
if not self.remote_projects:
from bw2io.remote import get_projects
ProjectNewTemplateMenu.remote_projects = get_projects()
ProjectNewTemplateMenu.remote_projects = fetch_remote_projects()
return self.remote_projects


Expand Down
2 changes: 1 addition & 1 deletion activity_browser/app/pages/lca_results/LCA_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ def __init__(self, parent=None):
self.layout = QtWidgets.QVBoxLayout()
self.setLayout(self.layout)
configure_lca_tab_layout(self.layout)
app.application.theme_changed.connect(self.update_tab)

def add_tab_header(
self, title: str, help_tooltip: Optional[str] = None
Expand All @@ -277,7 +278,6 @@ def build_tab_body(
QtWidgets.QSizePolicy.Policy.Expanding,
)
widget.setMinimumWidth(0)
widget.setStyleSheet("background-color: white;")
self.pt_layout.setContentsMargins(0, 0, 0, 0)
self.pt_layout.setAlignment(alignment)
widget.setLayout(self.pt_layout)
Expand Down
10 changes: 6 additions & 4 deletions activity_browser/app/pages/lca_results/plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from matplotlib.gridspec import GridSpec
from matplotlib.patches import Patch
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from bw2data import methods
Expand Down Expand Up @@ -318,13 +319,14 @@ def _prepare_data(self, df: pd.DataFrame, unit: str | None) -> _ContributionFram

@staticmethod
def _draw_net_markers(ax, dfp: pd.DataFrame, *, horizontal: bool) -> None:
"""Black dot at bar tip when positive and negative stacks do not cancel."""
"""Dot at bar tip when positive and negative stacks do not cancel."""
edge_color = plt.rcParams["axes.edgecolor"]
kw = dict(
markersize=5,
marker="o",
linestyle="none",
markerfacecolor="black",
markeredgecolor="black",
markerfacecolor=edge_color,
markeredgecolor=edge_color,
)
for i in range(dfp.shape[1]):
s = dfp.iloc[:, i]
Expand Down Expand Up @@ -480,7 +482,7 @@ def _render(self) -> None:
Patch(color=self.gsa_type_color(t), label=t)
for t in dfp[GSA_TYPE_COLUMN].drop_duplicates()
]
err_kw = dict(capsize=3, ecolor="#333333")
err_kw = dict(capsize=3, ecolor=plt.rcParams["axes.edgecolor"])
legend_ratio = 0.18 if handles else 0.0
value_label = "Delta sensitivity index"

Expand Down
137 changes: 68 additions & 69 deletions activity_browser/app/pages/settings/appearance.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
# -*- coding: utf-8 -*-
from loguru import logger
from qtpy import QtWidgets

from activity_browser.app import settings
from activity_browser.app.pages.settings.base import BaseSettingsChapter
from activity_browser.ui.widgets.plot import DEFAULT_PLOT_PALETTE, PLOT_PALETTES


def _labeled_group(title: str, rows: list[tuple[str, QtWidgets.QWidget]]) -> QtWidgets.QGroupBox:
group = QtWidgets.QGroupBox(title)
grid = QtWidgets.QGridLayout()
for i, (label, widget) in enumerate(rows):
grid.addWidget(QtWidgets.QLabel(label), i, 0)
grid.addWidget(widget, i, 1)
group.setLayout(grid)
return group


class AppearanceSettingsChapter(BaseSettingsChapter):
Expand All @@ -14,106 +24,95 @@ class AppearanceSettingsChapter(BaseSettingsChapter):
"light": "Light theme",
"dark": "Dark theme",
}

pane_tab_position_map = {
"top": "Top",
"bottom": "Bottom",
"left": "Left",
"right": "Right",
}

def __init__(self, parent=None):
super().__init__(parent)

# Theme selector
self.theme_combo = QtWidgets.QComboBox()

# Pane tab position selector
self.palette_combo = QtWidgets.QComboBox()
self.pane_tab_position_combo = QtWidgets.QComboBox()

# Database products as cards checkbox
self.database_products_as_cards = QtWidgets.QCheckBox("Show database contents as cards")
self.database_products_as_cards.setToolTip(
"When enabled, the database process list uses a card layout instead of a detailed table."
)

self.build_layout()
self.connect_signals()
self.reset()

def connect_signals(self):
"""Connect signals and slots."""
# Emit changed signal when settings change
self.theme_combo.currentTextChanged.connect(lambda: self.changed.emit())
self.pane_tab_position_combo.currentTextChanged.connect(lambda: self.changed.emit())
for widget in (
self.theme_combo,
self.palette_combo,
self.pane_tab_position_combo,
):
widget.currentTextChanged.connect(lambda: self.changed.emit())
self.database_products_as_cards.checkStateChanged.connect(lambda _: self.changed.emit())

def build_layout(self):
"""Build the chapter layout."""
layout = QtWidgets.QVBoxLayout()

# Theme section
theme_group = QtWidgets.QGroupBox("Theme")
theme_layout = QtWidgets.QGridLayout()
theme_layout.addWidget(QtWidgets.QLabel("Theme:"), 0, 0)
theme_layout.addWidget(self.theme_combo, 0, 1)
theme_group.setLayout(theme_layout)

# Pane tab position section
pane_tab_group = QtWidgets.QGroupBox("Pane Tab Position")
pane_tab_layout = QtWidgets.QGridLayout()
pane_tab_layout.addWidget(QtWidgets.QLabel("Position:"), 0, 0)
pane_tab_layout.addWidget(self.pane_tab_position_combo, 0, 1)
pane_tab_group.setLayout(pane_tab_layout)

layout.addWidget(_labeled_group("Theme", [("Theme:", self.theme_combo)]))
layout.addWidget(_labeled_group("Plots", [("Palette:", self.palette_combo)]))
layout.addWidget(
_labeled_group("Pane Tab Position", [("Position:", self.pane_tab_position_combo)])
)
database_group = QtWidgets.QGroupBox("Database pane")
database_layout = QtWidgets.QVBoxLayout()
database_layout.addWidget(self.database_products_as_cards)
database_group.setLayout(database_layout)

layout.addWidget(theme_group)
layout.addWidget(pane_tab_group)
db_layout = QtWidgets.QVBoxLayout()
db_layout.addWidget(self.database_products_as_cards)
database_group.setLayout(db_layout)
layout.addWidget(database_group)
layout.addStretch()

self.setLayout(layout)

# --- Settings management methods --- #

@staticmethod
def _saved_palette() -> str:
name = settings["appearance"].get("plot_palette", DEFAULT_PLOT_PALETTE)
return name if name in PLOT_PALETTES else DEFAULT_PLOT_PALETTE

def reset(self):
"""(Re)set to initial values."""
appearance = settings["appearance"]
self.theme_combo.clear()
self.theme_combo.addItems(self.theme_map.values())
self.theme_combo.setCurrentText(self.theme_map.get(settings["appearance"]["theme"], "System default"))

self.theme_combo.setCurrentText(self.theme_map.get(appearance["theme"], "System default"))
self.palette_combo.clear()
self.palette_combo.addItems(PLOT_PALETTES)
self.palette_combo.setCurrentText(self._saved_palette())
self.pane_tab_position_combo.clear()
self.pane_tab_position_combo.addItems(self.pane_tab_position_map.values())
self.pane_tab_position_combo.setCurrentText(self.pane_tab_position_map.get(settings["appearance"]["pane_tab_position"], "Bottom"))

self.database_products_as_cards.setChecked(
bool(settings["appearance"].get("database_products_as_cards", False))
self.pane_tab_position_combo.setCurrentText(
self.pane_tab_position_map.get(appearance["pane_tab_position"], "Bottom")
)
self.database_products_as_cards.setChecked(bool(appearance.get("database_products_as_cards", False)))

def has_changes(self):
"""Check if there are unsaved changes."""
current_state = {
'theme': self.theme_combo.currentText(),
'pane_tab_position': self.pane_tab_position_combo.currentText(),
'database_products_as_cards': self.database_products_as_cards.isChecked(),
appearance = settings["appearance"]
return {
"theme": self.theme_combo.currentText(),
"plot_palette": self.palette_combo.currentText(),
"pane_tab_position": self.pane_tab_position_combo.currentText(),
"database_products_as_cards": self.database_products_as_cards.isChecked(),
} != {
"theme": self.theme_map.get(appearance["theme"], "System default"),
"plot_palette": self._saved_palette(),
"pane_tab_position": self.pane_tab_position_map.get(
appearance["pane_tab_position"], "Bottom"
),
"database_products_as_cards": bool(appearance.get("database_products_as_cards", False)),
}
initial_state = {
'theme': self.theme_map.get(settings["appearance"]["theme"], "System default"),
'pane_tab_position': self.pane_tab_position_map.get(settings["appearance"]["pane_tab_position"], "Bottom"),
'database_products_as_cards': bool(settings["appearance"].get("database_products_as_cards", False)),
}
return current_state != initial_state

def set_settings(self):
"""Save appearance settings."""
new_theme = self.theme_combo.currentText()
settings["appearance"]["theme"] = [key for key, value in self.theme_map.items() if value == new_theme][0]

new_pane_position = self.pane_tab_position_combo.currentText()
settings["appearance"]["pane_tab_position"] = [key for key, value in self.pane_tab_position_map.items() if value == new_pane_position][0]

settings["appearance"]["database_products_as_cards"] = self.database_products_as_cards.isChecked()

def set_settings(self):
appearance = settings["appearance"]
appearance["theme"] = next(
k for k, v in self.theme_map.items() if v == self.theme_combo.currentText()
)
appearance["plot_palette"] = self.palette_combo.currentText()
appearance["pane_tab_position"] = next(
k
for k, v in self.pane_tab_position_map.items()
if v == self.pane_tab_position_combo.currentText()
)
appearance["database_products_as_cards"] = self.database_products_as_cards.isChecked()
5 changes: 2 additions & 3 deletions activity_browser/app/pages/settings/project_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@
from qtpy import QtWidgets, QtGui

import bw2data as bd
from bw2io import remote

from activity_browser import app, ui
from activity_browser.bwutils.commontasks import get_templates
from activity_browser.bwutils.commontasks import fetch_remote_projects, get_templates
from activity_browser.ui import widgets, core

from .base import BaseSettingsChapter
Expand Down Expand Up @@ -94,7 +93,7 @@ def build_template_df(self) -> pd.DataFrame:
data = []

templates = get_templates()
remote_templates = remote.get_projects()
remote_templates = fetch_remote_projects()

for name in sorted(templates):
data.append({
Expand Down
19 changes: 18 additions & 1 deletion activity_browser/app/pages/welcome.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,24 @@ def __init__(self, parent=None):
self.setLayout(self.vl)

self.bridge.ready.connect(self.update_welcome)
app.signals.project.changed.connect(lambda: self.page.load(self.url))
app.application.theme_changed.connect(self._reload_page)
app.signals.project.changed.connect(self._reload_page)
self.page.loadFinished.connect(self._on_load_finished)

def _reload_page(self, *_args) -> None:
self.page.load(self.url)

def _on_load_finished(self, ok: bool) -> None:
if not ok:
return
scheme = (
"dark"
if app.application.styleHints().colorScheme() == QtCore.Qt.ColorScheme.Dark
else "light"
)
self.page.runJavaScript(
f'document.documentElement.style.colorScheme = "{scheme}";'
)

def update_welcome(self):
projects = projects_by_last_opened()
Expand Down
14 changes: 14 additions & 0 deletions activity_browser/bwutils/commontasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,20 @@ def get_templates() -> dict:

return collection


def fetch_remote_projects() -> dict:
"""Remote template catalogue from ``bw2io``; empty dict if unreachable."""
try:
from bw2io.remote import get_projects

return get_projects() or {}
except Exception as exc:
from loguru import logger

logger.warning(f"Could not fetch remote project templates: {exc}")
return {}


def nodes_to_excel(nodes: list[tuple | int | bd.Node]) -> str:
"""Convert a list of nodes to an HTML table suitable for Excel."""
from .exporters import ABCSVFormatter
Expand Down
1 change: 1 addition & 0 deletions activity_browser/bwutils/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"theme": "default",
"pane_tab_position": "bottom",
"database_products_as_cards": False,
"plot_palette": "tab20",
},
"metadatastore": {
"caching_enabled": True,
Expand Down
Loading
Loading