diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d08dd8530..ab2edf09a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -38,3 +38,52 @@ jobs: run: | anaconda -t ${{ secrets.CONDA_UPLOAD_TOKEN }} upload \ /usr/share/miniconda/envs/build/conda-bld/noarch/*.tar.bz2 + + + build: + runs-on: ${{ matrix.os }} + permissions: + contents: write + strategy: + fail-fast: false + matrix: + os: [ windows-latest, macos-13 ] + python-version: [ '3.10' ] + defaults: + run: + shell: bash -e {0} + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + # install + - name: Install package + run: | + export GIT_DESCRIBE_TAG=`git describe --tags` + pip install . + - name: Install Build Dependencies + run: | + python -m pip install pyinstaller --user + - name: Build + id: build + run: | + if [ "$RUNNER_OS" == "Windows" ]; then + python scripts/createmsi.py + echo "build=$(ls *.msi)" >> $GITHUB_OUTPUT + elif [ "$RUNNER_OS" == "macOS" ]; then + export PATH=$PATH:/Users/runner/.local/bin + python scripts/create_macos_package.py + echo "build=$(ls *.pkg)" >> $GITHUB_OUTPUT + else + echo "$RUNNER_OS not supported" + exit 1 + fi + shell: bash + - name: Upload + uses: softprops/action-gh-release@v2 + with: + files: ${{ steps.build.outputs.build }} + + diff --git a/.gitignore b/.gitignore index 333a0ae4a..d743b2a66 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ activity_browser/ABSettings.json .logs test/.logs .venv* + +pyinst-tmp \ No newline at end of file diff --git a/LICENSE.rtf b/LICENSE.rtf new file mode 100755 index 000000000..1485cef9d --- /dev/null +++ b/LICENSE.rtf @@ -0,0 +1,170 @@ +{\rtf1\ansi\deff0\nouicompat{\fonttbl{\f0\fnil\fcharset0 Courier New;}} +{\colortbl ;\red0\green0\blue255;} +{\*\generator Riched20 10.0.19041}\viewkind4\uc1 +\pard\f0\fs22\lang1033 GNU LESSER GENERAL PUBLIC LICENSE\par + Version 3, 29 June 2007\par +\par + Copyright (C) 2007 Free Software Foundation, Inc. <{{\field{\*\fldinst{HYPERLINK "https://fsf.org/"}}{\fldrslt{https://fsf.org/\ul0\cf0}}}}\f0\fs22 >\par + Everyone is permitted to copy and distribute verbatim copies\par + of this license document, but changing it is not allowed.\par +\par +\par + This version of the GNU Lesser General Public License incorporates\par +the terms and conditions of version 3 of the GNU General Public\par +License, supplemented by the additional permissions listed below.\par +\par + 0. Additional Definitions.\par +\par + As used herein, "this License" refers to version 3 of the GNU Lesser\par +General Public License, and the "GNU GPL" refers to version 3 of the GNU\par +General Public License.\par +\par + "The Library" refers to a covered work governed by this License,\par +other than an Application or a Combined Work as defined below.\par +\par + An "Application" is any work that makes use of an interface provided\par +by the Library, but which is not otherwise based on the Library.\par +Defining a subclass of a class defined by the Library is deemed a mode\par +of using an interface provided by the Library.\par +\par + A "Combined Work" is a work produced by combining or linking an\par +Application with the Library. The particular version of the Library\par +with which the Combined Work was made is also called the "Linked\par +Version".\par +\par + The "Minimal Corresponding Source" for a Combined Work means the\par +Corresponding Source for the Combined Work, excluding any source code\par +for portions of the Combined Work that, considered in isolation, are\par +based on the Application, and not on the Linked Version.\par +\par + The "Corresponding Application Code" for a Combined Work means the\par +object code and/or source code for the Application, including any data\par +and utility programs needed for reproducing the Combined Work from the\par +Application, but excluding the System Libraries of the Combined Work.\par +\par + 1. Exception to Section 3 of the GNU GPL.\par +\par + You may convey a covered work under sections 3 and 4 of this License\par +without being bound by section 3 of the GNU GPL.\par +\par + 2. Conveying Modified Versions.\par +\par + If you modify a copy of the Library, and, in your modifications, a\par +facility refers to a function or data to be supplied by an Application\par +that uses the facility (other than as an argument passed when the\par +facility is invoked), then you may convey a copy of the modified\par +version:\par +\par + a) under this License, provided that you make a good faith effort to\par + ensure that, in the event an Application does not supply the\par + function or data, the facility still operates, and performs\par + whatever part of its purpose remains meaningful, or\par +\par + b) under the GNU GPL, with none of the additional permissions of\par + this License applicable to that copy.\par +\par + 3. Object Code Incorporating Material from Library Header Files.\par +\par + The object code form of an Application may incorporate material from\par +a header file that is part of the Library. You may convey such object\par +code under terms of your choice, provided that, if the incorporated\par +material is not limited to numerical parameters, data structure\par +layouts and accessors, or small macros, inline functions and templates\par +(ten or fewer lines in length), you do both of the following:\par +\par + a) Give prominent notice with each copy of the object code that the\par + Library is used in it and that the Library and its use are\par + covered by this License.\par +\par + b) Accompany the object code with a copy of the GNU GPL and this license\par + document.\par +\par + 4. Combined Works.\par +\par + You may convey a Combined Work under terms of your choice that,\par +taken together, effectively do not restrict modification of the\par +portions of the Library contained in the Combined Work and reverse\par +engineering for debugging such modifications, if you also do each of\par +the following:\par +\par + a) Give prominent notice with each copy of the Combined Work that\par + the Library is used in it and that the Library and its use are\par + covered by this License.\par +\par + b) Accompany the Combined Work with a copy of the GNU GPL and this license\par + document.\par +\par + c) For a Combined Work that displays copyright notices during\par + execution, include the copyright notice for the Library among\par + these notices, as well as a reference directing the user to the\par + copies of the GNU GPL and this license document.\par +\par + d) Do one of the following:\par +\par + 0) Convey the Minimal Corresponding Source under the terms of this\par + License, and the Corresponding Application Code in a form\par + suitable for, and under terms that permit, the user to\par + recombine or relink the Application with a modified version of\par + the Linked Version to produce a modified Combined Work, in the\par + manner specified by section 6 of the GNU GPL for conveying\par + Corresponding Source.\par +\par + 1) Use a suitable shared library mechanism for linking with the\par + Library. A suitable mechanism is one that (a) uses at run time\par + a copy of the Library already present on the user's computer\par + system, and (b) will operate properly with a modified version\par + of the Library that is interface-compatible with the Linked\par + Version.\par +\par + e) Provide Installation Information, but only if you would otherwise\par + be required to provide such information under section 6 of the\par + GNU GPL, and only to the extent that such information is\par + necessary to install and execute a modified version of the\par + Combined Work produced by recombining or relinking the\par + Application with a modified version of the Linked Version. (If\par + you use option 4d0, the Installation Information must accompany\par + the Minimal Corresponding Source and Corresponding Application\par + Code. If you use option 4d1, you must provide the Installation\par + Information in the manner specified by section 6 of the GNU GPL\par + for conveying Corresponding Source.)\par +\par + 5. Combined Libraries.\par +\par + You may place library facilities that are a work based on the\par +Library side by side in a single library together with other library\par +facilities that are not Applications and are not covered by this\par +License, and convey such a combined library under terms of your\par +choice, if you do both of the following:\par +\par + a) Accompany the combined library with a copy of the same work based\par + on the Library, uncombined with any other library facilities,\par + conveyed under the terms of this License.\par +\par + b) Give prominent notice with the combined library that part of it\par + is a work based on the Library, and explaining where to find the\par + accompanying uncombined form of the same work.\par +\par + 6. Revised Versions of the GNU Lesser General Public License.\par +\par + The Free Software Foundation may publish revised and/or new versions\par +of the GNU Lesser General Public License from time to time. Such new\par +versions will be similar in spirit to the present version, but may\par +differ in detail to address new problems or concerns.\par +\par + Each version is given a distinguishing version number. If the\par +Library as you received it specifies that a certain numbered version\par +of the GNU Lesser General Public License "or any later version"\par +applies to it, you have the option of following the terms and\par +conditions either of that published version or of any later version\par +published by the Free Software Foundation. If the Library as you\par +received it does not specify a version number of the GNU Lesser\par +General Public License, you may choose any version of the GNU Lesser\par +General Public License ever published by the Free Software Foundation.\par +\par + If the Library as you received it specifies that a proxy can decide\par +whether future versions of the GNU Lesser General Public License shall\par +apply, that proxy's public statement of acceptance of any version is\par +permanent authorization for you to choose that version for the\par +Library.\par +} + \ No newline at end of file diff --git a/README.md b/README.md index d515f0cf9..4fb7c8f84 100644 --- a/README.md +++ b/README.md @@ -235,3 +235,12 @@ If you experience problems or are suffering from a specific bug, please [raise a # License You can find the license information for Activity Browser in the [license file](https://github.com/LCA-ActivityBrowser/activity-browser/blob/main/LICENSE.txt). + + +# Build + +```bash +pip install third-party-license-file-generator +pip freeze > requirements.txt +python -m third_party_license_file_generator -r requirements.txt -p .venv/bin/python -c -g +``` \ No newline at end of file diff --git a/activity-browser.spec b/activity-browser.spec new file mode 100644 index 000000000..5767faa0c --- /dev/null +++ b/activity-browser.spec @@ -0,0 +1,99 @@ +# -*- mode: python ; coding: utf-8 -*- +import argparse +import os +import platform + +parser = argparse.ArgumentParser() +parser.add_argument("--debug", action="store_true") +parser.add_argument("--verbose", action="store_true") +parser.add_argument("--console", action="store_true") +options = parser.parse_args() + +python_options = [ + ("W ignore", None, "OPTION"), +] +if options.verbose: + python_options.append(("v", None, "OPTION")) + +EXE_ICON = os.path.join("activity_browser", "static", "icons", "activity-browser.ico") if platform.system().lower() == 'windows' else None + +a = Analysis( + ["run-activity-browser.py"], + pathex=[], + binaries=[], + datas=[], + hiddenimports=["activity_browser", "playhouse"], + hookspath=['scripts/hooks'], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +a.datas += Tree("activity_browser/static", prefix="activity_browser/static") +pyz = PYZ(a.pure) + +if options.debug: + exe = EXE( + pyz, + a.scripts, + python_options, + exclude_binaries=True, + name="activity-browser", + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=options.console, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + ) + coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name="activity-browser", + icon=EXE_ICON + ) + target = coll +else: + exe = EXE( + pyz, + a.scripts, + python_options, + a.binaries, + a.datas, + name="activity-browser", + console=options.console, + icon=EXE_ICON + ) + target = exe + +if platform.system() == "Darwin": + app = BUNDLE( + target, + name="Activity Browser.app", + icon="activity_browser/static/icons/activity-browser.icns", + bundle_identifier="com.github.lca_activity_browser.activity_browser", + version=os.environ.get("VERSION", "0.0.0"), + info_plist={ + "NSPrincipalClass": "NSApplication", + "NSAppleScriptEnabled": False, + "CFBundleDocumentTypes": [ + { + "CFBundleTypeName": "Icon", + "CFBundleTypeIconFile": "activity_browser/static/icons/activity-browser.icns", + "LSItemContentTypes": [ + "com.github.lca_activity_browser.activity_browser" + ], + "LSHandlerRank": "Owner", + } + ], + }, + ) diff --git a/activity_browser/__init__.py b/activity_browser/__init__.py index aceefc824..32ca49e08 100644 --- a/activity_browser/__init__.py +++ b/activity_browser/__init__.py @@ -11,6 +11,7 @@ from .plugin import Plugin from .controllers import * + def load_settings() -> None: if ab_settings.settings: bw2data.projects.switch_dir(ab_settings.current_bw_dir) @@ -19,7 +20,7 @@ def load_settings() -> None: log.info(f"Brightway2 current project: {bw2data.projects.current}") -def run_activity_browser(): +def run_activity_browser(splash=None): log.info(f"Activity Browser version: {version}") if log_file_location: log.info(f"The log file can be found at {log_file_location}") @@ -27,6 +28,8 @@ def run_activity_browser(): application.main_window = MainWindow(application) load_settings() application.show() + if splash: + splash.finish(application.main_window) sys.excepthook = exception_hook diff --git a/activity_browser/application.py b/activity_browser/application.py index b37b9799c..e539c9455 100644 --- a/activity_browser/application.py +++ b/activity_browser/application.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import os +from PySide2 import QtWidgets from PySide2.QtCore import QCoreApplication, QObject, QSysInfo, Qt from PySide2.QtWidgets import QApplication @@ -51,6 +52,8 @@ def deleteLater(self): os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--no-sandbox" log.info("Info: QtWebEngine sandbox disabled") -QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts, True) - -application = ABApplication() +if not QtWidgets.QApplication.instance(): + QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts, True) + application = ABApplication() +else: + application: ABApplication = QtWidgets.QApplication.instance() diff --git a/activity_browser/layouts/panels/right.py b/activity_browser/layouts/panels/right.py index a1747c7fc..98cf0fc41 100644 --- a/activity_browser/layouts/panels/right.py +++ b/activity_browser/layouts/panels/right.py @@ -4,6 +4,7 @@ from activity_browser import log, signals from activity_browser.mod import bw2data as bd +from activity_browser.utils import STATIC_DIR from ...bwutils.commontasks import get_activity_name from ...ui.web import GraphNavigatorWidget, RestrictedWebViewWidget @@ -22,8 +23,7 @@ class RightPanel(ABTab): def __init__(self, *args): super(RightPanel, self).__init__(*args) - package_dir = Path(__file__).resolve().parents[2] - html_file = str(package_dir.joinpath("static", "startscreen", "welcome.html")) + html_file = str(STATIC_DIR.joinpath("startscreen", "welcome.html")) self.tabs = { "Welcome": RestrictedWebViewWidget(html_file=html_file), "Characterization Factors": CharacterizationFactorsTab(self), diff --git a/activity_browser/logger.py b/activity_browser/logger.py index de63d00c8..796efea7c 100644 --- a/activity_browser/logger.py +++ b/activity_browser/logger.py @@ -1,16 +1,19 @@ import inspect +import json import logging import os import sys import threading import time from io import StringIO +from logging.handlers import TimedRotatingFileHandler from traceback import extract_tb from types import TracebackType from typing import TextIO, Type import platformdirs +BUNDLED = getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS') WHITELIST = ["activity_browser", "ab_plugin", "brightway2", "bw2data", "bw2io", "C/C++"] EXTENDED_CONSOLE = os.environ.get("AB_EXTENDED_CONSOLE", False) SIMPLE_CONSOLE = os.environ.get("AB_SIMPLE_CONSOLE", False) @@ -206,7 +209,11 @@ def __init__(self): self.filename = "ab_logs" + self.timestamp() + ".csv" # set dir and create it if it doesn't exist yet - dir_path = str(platformdirs.user_log_dir(appname="ActivityBrowser", appauthor="ActivityBrowser")) + dir_path = str( + platformdirs.user_log_dir( + appname="ActivityBrowser", appauthor="ActivityBrowser" + ) + ) os.makedirs(dir_path, exist_ok=True) # create final filepath of the logfile of this session @@ -279,6 +286,76 @@ def timestamp(self) -> str: return f"-{stmp.tm_year}-{stmp.tm_mon}-{stmp.tm_mday}_{stmp.tm_hour}-{stmp.tm_min}-{stmp.tm_sec}" +class ABTimedFileHandler(TimedRotatingFileHandler): + + def __init__(self, suffix: str, *args, **kwargs): + # set dir and create it if it doesn't exist yet + dir_path = str(platformdirs.user_log_dir(appname="ActivityBrowser", appauthor="ActivityBrowser")) + os.makedirs(dir_path, exist_ok=True) + self.filepath = os.path.join(dir_path, "ab_logs.{}".format(suffix)) + + super().__init__(self.filepath, *args, **kwargs) + + def namer(self, default_name): + base_filename, ext, date = default_name.split(".") + return f"{base_filename}.{date}.{ext}" + + +class JSONFormatter(logging.Formatter): + """ + Formatter that outputs JSON strings after parsing the LogRecord. + + @param dict fmt_dict: Key: logging format attribute pairs. Defaults to {"message": "message"}. + @param str time_format: time.strftime() format string. Default: "%Y-%m-%dT%H:%M:%S" + @param str msec_format: Microsecond formatting. Appended at the end. Default: "%s.%03dZ" + """ + + def __init__(self, fmt_dict: dict = None, time_format: str = "%Y-%m-%dT%H:%M:%S", msec_format: str = "%s.%03dZ"): + self.fmt_dict = fmt_dict if fmt_dict is not None else {"message": "message"} + self.default_time_format = time_format + self.default_msec_format = msec_format + self.datefmt = None + + def usesTime(self) -> bool: + """ + Overwritten to look for the attribute in the format dict values instead of the fmt string. + """ + return "asctime" in self.fmt_dict.values() + + def formatMessage(self, record) -> dict: + """ + Overwritten to return a dictionary of the relevant LogRecord attributes instead of a string. + KeyError is raised if an unknown attribute is provided in the fmt_dict. + """ + return {fmt_key: record.__dict__[fmt_val] for fmt_key, fmt_val in self.fmt_dict.items()} + + def format(self, record) -> str: + """ + Mostly the same as the parent's class method, the difference being that a dict is manipulated and dumped as JSON + instead of a string. + """ + record.message = record.getMessage() + + if self.usesTime(): + record.asctime = self.formatTime(record, self.datefmt) + + message_dict = self.formatMessage(record) + + if record.exc_info: + # Cache the traceback text to avoid converting it multiple times + # (it's constant anyway) + if not record.exc_text: + record.exc_text = self.formatException(record.exc_info) + + if record.exc_text: + message_dict["exc_info"] = record.exc_text + + if record.stack_info: + message_dict["stack_info"] = self.formatStack(record.stack_info) + + return json.dumps(message_dict, default=str) + + class LoggingProxy: """ Official logging documentation states that loggers should be initiated per module using the __name__ attribute of @@ -340,7 +417,7 @@ def log(self, level: int, msg, *args, stack_level: int = 2, exc_info: tuple = No def exception_hook( - error: Type[BaseException], message: BaseException, traceback: TracebackType + error: Type[BaseException], message: BaseException, traceback: TracebackType ): """Exception hook to catch and log exceptions""" exc_info = (error, message, traceback) @@ -361,27 +438,47 @@ def basic_setup(): def advanced_setup(): - # replace the low and high level StdIO's - low_level_stdout = LowLevelStdIO(sys.stdout).start_capture("StdoutCapture") - # low_level_stderr = LowLevelStdIO(sys.stderr).start_capture("StderrCapture") - - sys.stdout = HighLevelStdIO() - # sys.stderr = HighLevelStdIO() - # setting up our own logger root = logging.getLogger() logging.addLevelName(25, "PRINT") - # setting up the console handler - console_handler = ABConsoleHandler(low_level_stdout) - console_handler.addFilter(log_filter) - console_handler.setLevel(LOG_LEVEL) - root.addHandler(console_handler) - - # setting up the file handler - file_handler = ABFileHandler() - file_handler.addFilter(log_filter) - root.addHandler(file_handler) + if not BUNDLED: + # replace the low and high level StdIO's + low_level_stdout = LowLevelStdIO(sys.stdout).start_capture("StdoutCapture") + # low_level_stderr = LowLevelStdIO(sys.stderr).start_capture("StderrCapture") + + sys.stdout = HighLevelStdIO() + # sys.stderr = HighLevelStdIO() + + # setting up the console handler + console_handler = ABConsoleHandler(low_level_stdout) + console_handler.addFilter(log_filter) + console_handler.setLevel(LOG_LEVEL) + root.addHandler(console_handler) + + # setting up the file handler + file_handler = ABFileHandler() + file_handler.addFilter(log_filter) + file_handler.setLevel(LOG_LEVEL) + root.addHandler(file_handler) + else: + file_handler = ABTimedFileHandler("json") + file_handler.addFilter(log_filter) + file_handler.setLevel(LOG_LEVEL) + json_formatter = JSONFormatter({ + "level": "levelname", + "message": "message", + "loggerName": "name", + "processName": "processName", + "processID": "process", + "threadName": "threadName", + "threadID": "thread", + "timestamp": "asctime", + "funcName": "funcName", + "lineno": "lineno" + }) + file_handler.setFormatter(json_formatter) + root.addHandler(file_handler) log = LoggingProxy() diff --git a/activity_browser/static/icons/activity-browser.icns b/activity_browser/static/icons/activity-browser.icns new file mode 100644 index 000000000..66b6030fb Binary files /dev/null and b/activity_browser/static/icons/activity-browser.icns differ diff --git a/activity_browser/static/icons/activity-browser.ico b/activity_browser/static/icons/activity-browser.ico new file mode 100644 index 000000000..86f157e38 Binary files /dev/null and b/activity_browser/static/icons/activity-browser.ico differ diff --git a/activity_browser/ui/icons.py b/activity_browser/ui/icons.py index fb8e3916e..08910385b 100644 --- a/activity_browser/ui/icons.py +++ b/activity_browser/ui/icons.py @@ -3,12 +3,12 @@ from PySide2.QtGui import QIcon -PACKAGE_DIR = Path(__file__).resolve().parents[1] +from activity_browser.utils import STATIC_DIR def create_path(folder: str, filename: str) -> str: """Builds a path to the image file.""" - return str(PACKAGE_DIR.joinpath("static", "icons", folder, filename)) + return str(STATIC_DIR.joinpath("icons", folder, filename)) # CURRENTLY UNUSED ICONS diff --git a/activity_browser/ui/web/base.py b/activity_browser/ui/web/base.py index 4de9746a3..ddbb1ce72 100644 --- a/activity_browser/ui/web/base.py +++ b/activity_browser/ui/web/base.py @@ -38,7 +38,7 @@ def __init__(self, parent=None, css_file: str = "", *args, **kwargs): self.view.loadFinished.connect(self.load_finished_handler) self.view.setContextMenuPolicy(Qt.PreventContextMenu) self.view.page().setWebChannel(self.channel) - self.url = QUrl.fromLocalFile(self.HTML_FILE) + self.url = QUrl.fromLocalFile(str(self.HTML_FILE)) self.css_file = css_file # Various Qt objects diff --git a/activity_browser/ui/web/navigator.py b/activity_browser/ui/web/navigator.py index 780b0fce4..13c68d4ea 100644 --- a/activity_browser/ui/web/navigator.py +++ b/activity_browser/ui/web/navigator.py @@ -10,6 +10,7 @@ from activity_browser import log, signals from activity_browser.mod.bw2data import Database, get_activity +from activity_browser.utils import STATIC_DIR from ...bwutils.commontasks import identify_activity_type from .base import BaseGraph, BaseNavigatorWidget @@ -50,9 +51,7 @@ class GraphNavigatorWidget(BaseNavigatorWidget): NAVIGATION MODE: Click on activities to jump to specific activities (instead of expanding the graph). """ - HTML_FILE = os.path.join( - os.path.abspath(os.path.dirname(__file__)), "../../static/navigator.html" - ) + HTML_FILE = STATIC_DIR.joinpath("navigator.html") def __init__(self, parent=None, key=None): super().__init__(parent, css_file="navigator.css") diff --git a/activity_browser/ui/web/sankey_navigator.py b/activity_browser/ui/web/sankey_navigator.py index 85c3e0a1e..1694c49be 100644 --- a/activity_browser/ui/web/sankey_navigator.py +++ b/activity_browser/ui/web/sankey_navigator.py @@ -17,6 +17,7 @@ from activity_browser import log, signals from activity_browser.mod import bw2data as bd from activity_browser.mod.bw2data.backends import ActivityDataset +from activity_browser.utils import STATIC_DIR from ...bwutils.commontasks import identify_activity_type from ...bwutils.superstructure.graph_traversal_with_scenario import ( @@ -45,9 +46,7 @@ class SankeyNavigatorWidget(BaseNavigatorWidget): Green flows: Avoided impacts """ - HTML_FILE = os.path.join( - os.path.abspath(os.path.dirname(__file__)), "../../static/sankey_navigator.html" - ) + HTML_FILE = STATIC_DIR.joinpath("sankey_navigator.html") def __init__(self, cs_name, parent=None): super().__init__(parent, css_file="sankey_navigator.css") diff --git a/activity_browser/ui/web/webutils.py b/activity_browser/ui/web/webutils.py index 072d4d040..1f61e1761 100644 --- a/activity_browser/ui/web/webutils.py +++ b/activity_browser/ui/web/webutils.py @@ -4,7 +4,7 @@ from PySide2 import QtCore, QtGui, QtWebEngineWidgets, QtWidgets # type "localhost:3999" in Chrome for DevTools of AB web content -from activity_browser.utils import get_base_path +from activity_browser.utils import STATIC_DIR os.environ["QTWEBENGINE_REMOTE_DEBUGGING"] = "3999" @@ -56,8 +56,8 @@ def __init__(self, parent=None, url=None, html_file=None): def get_static_js_path(file_name: str = "") -> str: - return str(get_base_path().joinpath("static", "javascript", file_name)) + return str(STATIC_DIR.joinpath("javascript", file_name)) def get_static_css_path(file_name: str = "") -> str: - return str(get_base_path().joinpath("static", "css", file_name)) + return str(STATIC_DIR.joinpath("css", file_name)) diff --git a/activity_browser/utils.py b/activity_browser/utils.py index 18316db2a..99f69164d 100644 --- a/activity_browser/utils.py +++ b/activity_browser/utils.py @@ -1,4 +1,6 @@ +import importlib import os +import importlib.resources from pathlib import Path from typing import Iterable, Tuple @@ -6,13 +8,17 @@ from bw_processing import safe_filename from PySide2 import QtWidgets +import activity_browser from activity_browser.mod import bw2data as bd from .settings import ab_settings +STATIC_DIR = importlib.resources.files(activity_browser) / "static" -def get_base_path() -> Path: - return Path(__file__).resolve().parents[0] + +def get_package_dir() -> Path: + """Return activity_browser package path""" + return Path(importlib.util.find_spec("activity_browser").origin).parent def read_file_text(file_dir: str) -> str: diff --git a/run-activity-browser.py b/run-activity-browser.py index 5131d1fb9..0b03a810d 100644 --- a/run-activity-browser.py +++ b/run-activity-browser.py @@ -1,5 +1,97 @@ # -*- coding: utf-8 -*- -from activity_browser import run_activity_browser -import logging +import importlib +import multiprocessing +import os +import sys -run_activity_browser() +from PySide2.QtCore import QCoreApplication, QObject, QSysInfo, Qt +from PySide2.QtGui import QPixmap +from PySide2.QtWidgets import QApplication +from PySide2.QtWidgets import QSplashScreen + + +class ABApplication(QApplication): + """Matches the ABApplication definied in application.py""" + + _main_window = None + _controllers = None + + @property + def main_window(self) -> QObject: + """Returns the main_window widget of the Activity Browser""" + if self._main_window: + return self._main_window + raise Exception( + "main_window not yet initialized, did you try to access it during startup?" + ) + + @main_window.setter + def main_window(self, widget): + self._main_window = widget + + def show(self): + self.main_window.showMaximized() + + def close(self): + for child in self.children(): + if hasattr(child, "close"): + child.close() + + def deleteLater(self): + self.main_window.deleteLater() + + +if QSysInfo.productType() == "osx": + # https://bugreports.qt.io/browse/QTBUG-87014 + # https://bugreports.qt.io/browse/QTBUG-85546 + # https://github.com/mapeditor/tiled/issues/2845 + # https://doc.qt.io/qt-5/qoperatingsystemversion.html#MacOSBigSur-var + supported = {"10.10", "10.11", "10.12", "10.13", "10.14", "10.15"} + if QSysInfo.productVersion() not in supported: + os.environ["QT_MAC_WANTS_LAYER"] = "1" + os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu" + +if QSysInfo.productType() in ["arch", "nixos"]: + os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--no-sandbox" + +QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts, True) + + +def show_splash_screen(): + application = ABApplication(sys.argv) + # Create the splash screen + splash = QSplashScreen( + QPixmap( + os.path.join( + os.path.dirname(importlib.util.find_spec("activity_browser").origin), + "static", + "icons", + "main", + "activitybrowser.png", + ) + ).scaledToHeight(480) + ) + splash.setWindowFlag(Qt.WindowStaysOnTopHint) + splash.showMessage( + "