diff --git a/.github/workflows/build_installer.yaml b/.github/workflows/build_installer.yaml index 4ccea0b1..961bd3e5 100644 --- a/.github/workflows/build_installer.yaml +++ b/.github/workflows/build_installer.yaml @@ -92,7 +92,6 @@ jobs: run: | conda init bash conda activate rascal2 - conda install conda-forge::expat==2.7.3 if [ ${{ matrix.platform }} == "macos-14" ]; then ARCH="arm64" export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:/Users/runner/hostedtoolcache/MATLAB/2023.2.999/arm64/MATLAB.app/bin/maca64 diff --git a/environment.yaml b/environment.yaml index dd2e4400..8c4b7310 100644 --- a/environment.yaml +++ b/environment.yaml @@ -6,6 +6,7 @@ dependencies: - python=3.10 - pip - llvm-openmp + - expat=2.7.3 - pip: - -r requirements.txt - -r requirements-dev.txt diff --git a/packaging/linux/build_installer.sh b/packaging/linux/build_installer.sh index 2c10dab8..8d540134 100644 --- a/packaging/linux/build_installer.sh +++ b/packaging/linux/build_installer.sh @@ -121,7 +121,7 @@ echo "" wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O "$TMP_DIR/miniconda.sh" bash ./miniconda.sh -b -p ./miniconda ./miniconda/bin/conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main --channel https://repo.anaconda.com/pkgs/r -./miniconda/bin/conda create -n rascal_builder -y python=3.10 +./miniconda/bin/conda create -n rascal_builder -y python=3.10 expat=2.7.3 echo "" echo "Downloading Dependencies" diff --git a/rascal2/app.py b/rascal2/app.py new file mode 100644 index 00000000..7fddba1e --- /dev/null +++ b/rascal2/app.py @@ -0,0 +1,51 @@ +import logging +import multiprocessing +import re +from contextlib import suppress + +from PyQt6 import QtWidgets + +from rascal2.config import MatlabHelper, handle_scaling, setup_logging +from rascal2.paths import IMAGES_PATH, STATIC_PATH +from rascal2.ui.view import MainWindowView + + +def ui_execute(splash): + """Create main window and executes GUI event loop. + + Returns + ------- + exit code : int + QApplication exit code + """ + handle_scaling() + QtWidgets.QApplication.setStyle("Fusion") + app = QtWidgets.QApplication.instance() + with suppress(FileNotFoundError), open(STATIC_PATH / "style.css") as stylesheet: + palette = app.palette() + replacements = { + "@Path": IMAGES_PATH.as_posix(), + "@Window": palette.window().color().name(), + "@Highlight": palette.highlight().color().name(), + "@Midlight": palette.midlight().color().name(), + "@Text": palette.text().color().name(), + } + style = re.sub("|".join(replacements), lambda x: replacements[x.group(0)], stylesheet.read()) + app.setStyleSheet(style) + + window = MainWindowView() + window.show() + splash.finish(window) + + return app.exec() + + +def start_app(splash): + """Start RasCAL app.""" + multiprocessing.set_start_method("spawn", force=True) + setup_logging() + matlab_helper = MatlabHelper() + exit_code = ui_execute(splash) + matlab_helper.close_event.set() + logging.shutdown() + return exit_code diff --git a/rascal2/config.py b/rascal2/config.py index 71becea1..0e31ec3d 100644 --- a/rascal2/config.py +++ b/rascal2/config.py @@ -1,32 +1,12 @@ import logging -import os - -os.environ["DELAY_MATLAB_START"] = "1" import multiprocessing as mp import pathlib import platform -import site import sys +from rascal2.paths import MATLAB_ARCH_FILE from rascal2.settings import Settings, get_global_settings -if getattr(sys, "frozen", False): - # we are running in a bundle - SOURCE_PATH = pathlib.Path(sys.executable).parent.parent - SITE_PATH = SOURCE_PATH / "bin/_internal" - if pathlib.Path(SOURCE_PATH / "MacOS").is_dir(): - SOURCE_PATH = SOURCE_PATH / "Resources" - SITE_PATH = SOURCE_PATH - EXAMPLES_PATH = SOURCE_PATH / "examples" -else: - SOURCE_PATH = pathlib.Path(__file__).parent - SITE_PATH = site.getsitepackages()[-1] - EXAMPLES_PATH = SOURCE_PATH.parent / "examples" - -STATIC_PATH = SOURCE_PATH / "static" -IMAGES_PATH = STATIC_PATH / "images" -MATLAB_ARCH_FILE = pathlib.Path(SITE_PATH) / "matlab/engine/_arch.txt" -EXAMPLES_TEMP_PATH = pathlib.Path(get_global_settings().fileName()).parent / "examples" LOGGER = logging.getLogger("rascal2") SETTINGS = Settings() @@ -39,22 +19,6 @@ def handle_scaling(): windll.user32.SetProcessDPIAware() -def path_for(filename: str): - """Get full path for the given image file. - - Parameters - ---------- - filename : str - basename and extension of image. - - Returns - ------- - full path : str - full path of the image. - """ - return (IMAGES_PATH / filename).as_posix() - - def log_uncaught_exceptions(exc_type, exc_value, exc_traceback): """Qt slots swallows exceptions but this ensures exceptions are logged.""" logging.critical("An unhandled exception occurred!", exc_info=(exc_type, exc_value, exc_traceback)) diff --git a/rascal2/dialogs/about_dialog.py b/rascal2/dialogs/about_dialog.py index 0ee9541e..1bb47158 100644 --- a/rascal2/dialogs/about_dialog.py +++ b/rascal2/dialogs/about_dialog.py @@ -5,7 +5,8 @@ import rascal2 import rascal2.widgets -from rascal2.config import MatlabHelper, path_for +from rascal2.config import MatlabHelper +from rascal2.paths import path_for from rascal2.settings import get_global_settings diff --git a/rascal2/dialogs/custom_file_editor.py b/rascal2/dialogs/custom_file_editor.py index 2b895cc6..b0bd2600 100644 --- a/rascal2/dialogs/custom_file_editor.py +++ b/rascal2/dialogs/custom_file_editor.py @@ -7,8 +7,9 @@ from PyQt6 import Qsci, QtGui, QtWidgets from ratapi.utils.enums import Languages -from rascal2.config import EXAMPLES_PATH, LOGGER, SETTINGS, MatlabHelper +from rascal2.config import LOGGER, SETTINGS, MatlabHelper from rascal2.core.enums import CustomFileType +from rascal2.paths import EXAMPLES_PATH MATLAB_MODEL_TEMPLATE = """function [output, sub_rough] = {0}{1} % RasCAL-2 Layer Model Custom File. diff --git a/rascal2/dialogs/settings_dialog.py b/rascal2/dialogs/settings_dialog.py index be70e4b1..b946e8e4 100644 --- a/rascal2/dialogs/settings_dialog.py +++ b/rascal2/dialogs/settings_dialog.py @@ -5,7 +5,8 @@ from PyQt6 import QtCore, QtWidgets -from rascal2.config import LOGGER, MATLAB_ARCH_FILE, SETTINGS, MatlabHelper +from rascal2.config import LOGGER, SETTINGS, MatlabHelper +from rascal2.paths import MATLAB_ARCH_FILE from rascal2.settings import SettingsGroups from rascal2.widgets.inputs import get_validated_input diff --git a/rascal2/dialogs/startup_dialog.py b/rascal2/dialogs/startup_dialog.py index d61d3a11..832df8e2 100644 --- a/rascal2/dialogs/startup_dialog.py +++ b/rascal2/dialogs/startup_dialog.py @@ -3,8 +3,9 @@ from PyQt6 import QtCore, QtWidgets -from rascal2.config import EXAMPLES_PATH, LOGGER +from rascal2.config import LOGGER from rascal2.core.worker import Worker +from rascal2.paths import EXAMPLES_PATH from rascal2.settings import update_recent_projects # global variable for required project files diff --git a/rascal2/main.py b/rascal2/main.py index b02378c3..6851c4ff 100644 --- a/rascal2/main.py +++ b/rascal2/main.py @@ -1,54 +1,51 @@ -import logging import multiprocessing -import re +import os import sys -from contextlib import suppress - -from PyQt6 import QtGui, QtWidgets - -from rascal2.config import IMAGES_PATH, STATIC_PATH, MatlabHelper, handle_scaling, path_for, setup_logging -from rascal2.ui.view import MainWindowView - - -def ui_execute(): - """Create main window and executes GUI event loop. - - Returns - ------- - exit code : int - QApplication exit code - """ - handle_scaling() - QtWidgets.QApplication.setStyle("Fusion") - app = QtWidgets.QApplication(sys.argv[:1]) - app.setWindowIcon(QtGui.QIcon(path_for("logo.png"))) - with suppress(FileNotFoundError), open(STATIC_PATH / "style.css") as stylesheet: - palette = app.palette() - replacements = { - "@Path": IMAGES_PATH.as_posix(), - "@Window": palette.window().color().name(), - "@Highlight": palette.highlight().color().name(), - "@Midlight": palette.midlight().color().name(), - "@Text": palette.text().color().name(), - } - style = re.sub("|".join(replacements), lambda x: replacements[x.group(0)], stylesheet.read()) - app.setStyleSheet(style) - - window = MainWindowView() - window.show() - return app.exec() + +from PyQt6.QtCore import Qt, QThread +from PyQt6.QtGui import QIcon, QPixmap +from PyQt6.QtWidgets import QApplication, QSplashScreen + +from rascal2.paths import path_for + + +class SplashScreen(QSplashScreen): + """Create splash screen widget.""" + + def __init__(self, *args): + super().__init__(*args) + self.painted = False + + def paintEvent(self, event): + super().paintEvent(event) + self.painted = True def main(): """Entry point function for starting RasCAL.""" multiprocessing.freeze_support() - multiprocessing.set_start_method("spawn", force=True) - setup_logging() - matlab_helper = MatlabHelper() - exit_code = ui_execute() - matlab_helper.close_event.set() - logging.shutdown() - sys.exit(exit_code) + + app = QApplication([]) + app.setWindowIcon(QIcon(path_for("logo.png"))) + + splash = SplashScreen(QPixmap(path_for("splash.png")), Qt.WindowType.WindowStaysOnTopHint) + splash.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose) + splash.show() + splash.raise_() + splash.activateWindow() + app.processEvents() + for _ in range(100): + if splash.painted: + break + # wait for splash to paint on linux + QThread.usleep(100) + app.processEvents() + app.processEvents() + + os.environ["DELAY_MATLAB_START"] = "1" + from rascal2.app import start_app + + sys.exit(start_app(splash)) if __name__ == "__main__": diff --git a/rascal2/paths.py b/rascal2/paths.py new file mode 100644 index 00000000..c4882063 --- /dev/null +++ b/rascal2/paths.py @@ -0,0 +1,39 @@ +import site +import sys +from pathlib import Path + +from rascal2.settings import get_global_settings + +if getattr(sys, "frozen", False): + # we are running in a bundle + SOURCE_PATH = Path(sys.executable).parent.parent + SITE_PATH = SOURCE_PATH / "bin/_internal" + if Path(SOURCE_PATH / "MacOS").is_dir(): + SOURCE_PATH = SOURCE_PATH / "Resources" + SITE_PATH = SOURCE_PATH + EXAMPLES_PATH = SOURCE_PATH / "examples" +else: + SOURCE_PATH = Path(__file__).parent + SITE_PATH = site.getsitepackages()[-1] + EXAMPLES_PATH = SOURCE_PATH.parent / "examples" + +STATIC_PATH = SOURCE_PATH / "static" +IMAGES_PATH = STATIC_PATH / "images" +MATLAB_ARCH_FILE = Path(SITE_PATH) / "matlab/engine/_arch.txt" +EXAMPLES_TEMP_PATH = Path(get_global_settings().fileName()).parent / "examples" + + +def path_for(filename: str): + """Get full path for the given image file. + + Parameters + ---------- + filename : str + basename and extension of image. + + Returns + ------- + full path : str + full path of the image. + """ + return (IMAGES_PATH / filename).as_posix() diff --git a/rascal2/static/images/logo.png b/rascal2/static/images/logo.png index ab7a1a84..095aaa4d 100644 Binary files a/rascal2/static/images/logo.png and b/rascal2/static/images/logo.png differ diff --git a/rascal2/static/images/splash.png b/rascal2/static/images/splash.png new file mode 100644 index 00000000..6293e781 Binary files /dev/null and b/rascal2/static/images/splash.png differ diff --git a/rascal2/ui/model.py b/rascal2/ui/model.py index 56b581a7..563f671e 100644 --- a/rascal2/ui/model.py +++ b/rascal2/ui/model.py @@ -8,7 +8,7 @@ import ratapi.outputs from PyQt6 import QtCore -from rascal2.config import EXAMPLES_PATH, EXAMPLES_TEMP_PATH +from rascal2.paths import EXAMPLES_PATH, EXAMPLES_TEMP_PATH def copy_example_project(load_path): diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index 2420abe0..bdcab93c 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -2,11 +2,12 @@ from PyQt6 import QtCore, QtGui, QtWidgets -from rascal2.config import EXAMPLES_PATH, EXAMPLES_TEMP_PATH, SETTINGS, path_for +from rascal2.config import SETTINGS from rascal2.core.enums import UnsavedReply from rascal2.dialogs.about_dialog import AboutDialog from rascal2.dialogs.settings_dialog import SettingsDialog from rascal2.dialogs.startup_dialog import PROJECT_FILES, LoadDialog, LoadR1Dialog, NewProjectDialog, StartupDialog +from rascal2.paths import EXAMPLES_PATH, EXAMPLES_TEMP_PATH, path_for from rascal2.settings import MDIGeometries, get_global_settings from rascal2.widgets import ControlsWidget, PlotWidget, TerminalWidget from rascal2.widgets.project import ProjectWidget diff --git a/rascal2/widgets/inputs.py b/rascal2/widgets/inputs.py index 6d6d3015..6a7eb969 100644 --- a/rascal2/widgets/inputs.py +++ b/rascal2/widgets/inputs.py @@ -9,7 +9,7 @@ from pydantic.fields import FieldInfo from PyQt6 import QtCore, QtGui, QtWidgets -from rascal2.config import path_for +from rascal2.paths import path_for def get_validated_input(field_info: FieldInfo, parent=None) -> QtWidgets.QWidget: diff --git a/rascal2/widgets/plot.py b/rascal2/widgets/plot.py index 67bd34a3..bbcaa26f 100644 --- a/rascal2/widgets/plot.py +++ b/rascal2/widgets/plot.py @@ -8,7 +8,8 @@ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT from PyQt6 import QtCore, QtGui, QtWidgets -from rascal2.config import SETTINGS, path_for +from rascal2.config import SETTINGS +from rascal2.paths import path_for from rascal2.widgets.inputs import MultiSelectComboBox, ProgressButton diff --git a/rascal2/widgets/project/lists.py b/rascal2/widgets/project/lists.py index 991ca4ed..a39b3914 100644 --- a/rascal2/widgets/project/lists.py +++ b/rascal2/widgets/project/lists.py @@ -9,8 +9,9 @@ from PyQt6 import QtCore, QtGui, QtWidgets from ratapi.utils.enums import BackgroundActions, LayerModels -from rascal2.config import SETTINGS, path_for +from rascal2.config import SETTINGS from rascal2.core.readers import readers +from rascal2.paths import path_for from rascal2.widgets.delegates import ProjectFieldDelegate from rascal2.widgets.inputs import RangeWidget diff --git a/rascal2/widgets/project/project.py b/rascal2/widgets/project/project.py index d0c846f2..bdd2743a 100644 --- a/rascal2/widgets/project/project.py +++ b/rascal2/widgets/project/project.py @@ -11,7 +11,7 @@ from ratapi.utils.custom_errors import custom_pydantic_validation_error from ratapi.utils.enums import Calculations, Geometries, LayerModels -from rascal2.config import path_for +from rascal2.paths import path_for from rascal2.widgets.project.lists import ContrastWidget, DataWidget from rascal2.widgets.project.slider_view import SliderViewWidget from rascal2.widgets.project.tables import ( diff --git a/rascal2/widgets/project/tables.py b/rascal2/widgets/project/tables.py index 38179391..ed6cd9f0 100644 --- a/rascal2/widgets/project/tables.py +++ b/rascal2/widgets/project/tables.py @@ -13,9 +13,10 @@ from ratapi.utils.enums import Calculations, Languages, Procedures, TypeOptions import rascal2.widgets.delegates as delegates -from rascal2.config import LOGGER, SETTINGS, path_for +from rascal2.config import LOGGER, SETTINGS from rascal2.core.enums import CustomFileType from rascal2.dialogs.custom_file_editor import create_new_file, edit_file +from rascal2.paths import path_for class ClassListTableModel(QtCore.QAbstractTableModel): diff --git a/rascal2/widgets/startup.py b/rascal2/widgets/startup.py index 960d2a63..5185edd6 100644 --- a/rascal2/widgets/startup.py +++ b/rascal2/widgets/startup.py @@ -1,7 +1,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets -from rascal2.config import path_for from rascal2.dialogs.startup_dialog import LoadDialog, LoadR1Dialog, NewProjectDialog +from rascal2.paths import path_for class StartUpWidget(QtWidgets.QWidget):