diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 466536a8..505ff5d1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,52 +9,106 @@ on: - main paths: - 'constructor-manager/**' - - 'constructor-manager-cli/**' + - 'constructor-manager-api/**' + - 'constructor-manager-ui/**' workflow_dispatch: jobs: test: name: ${{ matrix.platform }} py${{ matrix.python-version }} runs-on: ${{ matrix.platform }} + defaults: + run: + shell: bash -el {0} strategy: + fail-fast: false matrix: platform: [ubuntu-latest, windows-latest, macos-latest] python-version: ['3.8', '3.9', '3.10'] - + env: + DISPLAY: ':99.0' steps: + - name: Install OS dependencies + if: contains(matrix.platform, 'ubuntu') + run: | + sudo apt-get update --fix-missing + sudo apt install xvfb + + - uses: tlambert03/setup-qt-libs@v1 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: conda-incubator/setup-miniconda@v2 with: + activate-environment: "constructor-manager" python-version: ${{ matrix.python-version }} + auto-activate-base: true + run-post: false - - name: Install dependencies constructor-manager-cli + - name: Install dependencies on base environment run: | - python -m pip install --upgrade pip - python -m pip install setuptools tox tox-gh-actions - cd constructor-manager-cli - pip install -e . - pip list + conda install -n base conda-lock mamba -c conda-forge --quiet + conda list - - name: Test constructor-manager-cli + - name: create constructor-manager-environment run: | - cd constructor-manager-cli - python -m tox - env: - PLATFORM: ${{ matrix.platform }} + conda install -n constructor-manager conda packaging requests pyyaml -c conda-forge --quiet + # List installed packages + conda list - - name: Install dependencies constructor-manager + - name: Install constructor-manager run: | - cd constructor-manager-cli - pip install -e . - pip list - env: - PLATFORM: ${{ matrix.platform }} + # Install constructor manager + git clone https://github.com/goanpeca/packaging.git packaging_clone + cd packaging_clone + git checkout constructor-updater + cd constructor-manager + pip install -e . --no-deps - - name: Test constructor-manager + # Install test deps + conda install -n constructor-manager pytest pytest-cov pytest-qt -c conda-forge --quiet + + # List installed packages + conda list + + - name: Install constructor-manager-api run: | - cd constructor-manager - python -m tox - env: - PLATFORM: ${{ matrix.platform }} + # Install dependencies + conda install -n constructor-manager qtpy pyqt -c conda-forge --quiet + + # Install constructor manager + cd packaging_clone + git checkout constructor-cli + cd constructor-manager-api + pip install -e . --no-deps + + # List installed packages + conda list + + - name: Install constructor-manager-ui + run: | + # Install constructor manager ui + cd constructor-manager-ui + pip install -e . --no-deps + + # List installed packages + conda list + + - name: List installed packages + run: | + conda list + + - name: Test constructor-manager-ui (linux) + if: contains(matrix.platform, 'ubuntu') + run: | + # Run Tests + cd constructor-manager-ui/src + xvfb-run pytest constructor_manager_ui --cov=constructor_manager_ui + + - name: Test constructor-manager-ui (other) + if: "!contains(matrix.platform, 'ubuntu')" + run: | + # Run Tests + cd constructor-manager-ui/src + pytest constructor_manager_ui --cov=constructor_manager_ui diff --git a/.github/workflows/tests_ui.yml b/.github/workflows/tests_ui.yml deleted file mode 100644 index 11743e03..00000000 --- a/.github/workflows/tests_ui.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: tests_ui - -on: - push: - branches: - - main - pull_request: - branches: - - main - paths: - - 'constructor-manager-ui/**' - workflow_dispatch: - -jobs: - test: - name: ${{ matrix.platform }} py${{ matrix.python-version }} - runs-on: ${{ matrix.platform }} - strategy: - matrix: - platform: [ubuntu-latest, windows-latest, macos-latest] - python-version: ['3.8', '3.9', '3.10'] - env: - DISPLAY: ':99.0' - steps: - - uses: actions/checkout@v3 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - - name: Install OS dependencies - if: contains(matrix.platform, 'ubuntu') - run: | - sudo apt-get update --fix-missing - sudo apt-get install -qq pyqt5-dev-tools libxcb-xinerama0 xterm --fix-missing - - - name: Install dependencies UI - run: | - cd constructor-manager-ui - python -m pip install --upgrade pip - python -m pip install setuptools tox tox-gh-actions - pip list - # this runs the platform-specific tests declared in tox.ini - - name: Test with tox - run: | - cd constructor-manager-ui - python -m tox - env: - PLATFORM: ${{ matrix.platform }} diff --git a/constructor-manager-ui/README.md b/constructor-manager-ui/README.md index f9f6ff0c..b0fe13d7 100644 --- a/constructor-manager-ui/README.md +++ b/constructor-manager-ui/README.md @@ -46,3 +46,12 @@ constructor-manager-ui-qrc ``` You need to have pyqt5 installed. + +### Example commands + +```bash +constructor-manager-ui napari --current-version 0.4.16 --build-string pyside --plugins-url https://api.napari-hub.org/plugins --channel conda-forge +constructor-manager-ui napari --build-string pyside --plugins-url https://api.napari-hub.org/plugins --channel conda-forge --channel napari +constructor-manager-ui napari --build-string pyside --plugins-url https://api.napari-hub.org/plugins --channel conda-forge --channel napari --dev +constructor-manager-ui napari --build-string pyside --plugins-url https://api.napari-hub.org/plugins --channel conda-forge --channel napari -cv 0.4.17 +``` diff --git a/constructor-manager-ui/setup.cfg b/constructor-manager-ui/setup.cfg index 42209548..7959ffd1 100644 --- a/constructor-manager-ui/setup.cfg +++ b/constructor-manager-ui/setup.cfg @@ -39,7 +39,7 @@ where = src [options.entry_points] console_scripts = - constructor-manager-ui = constructor_manager_ui.cli:run + constructor-manager-ui = constructor_manager_ui.main:run constructor-manager-ui-qrc = constructor_manager_ui.style.utils:generate_resource_file [options.extras_require] diff --git a/constructor-manager-ui/src/constructor_manager_ui/__init__.py b/constructor-manager-ui/src/constructor_manager_ui/__init__.py index 96e31bb9..e69de29b 100644 --- a/constructor-manager-ui/src/constructor_manager_ui/__init__.py +++ b/constructor-manager-ui/src/constructor_manager_ui/__init__.py @@ -1 +0,0 @@ -"""Constructor manager.""" diff --git a/constructor-manager-ui/src/constructor_manager_ui/cli.py b/constructor-manager-ui/src/constructor_manager_ui/cli.py index 1776fb71..437f6e18 100644 --- a/constructor-manager-ui/src/constructor_manager_ui/cli.py +++ b/constructor-manager-ui/src/constructor_manager_ui/cli.py @@ -2,15 +2,39 @@ import argparse -from constructor_manager_ui.main import main - -def run(): +def create_parser(): parser = argparse.ArgumentParser() parser.add_argument("package", type=str) - args = parser.parse_args() - main(args.package) - -if __name__ == "__main__": - run() + parser.add_argument( + "--current-version", + "-cv", + type=str, + default=None, + ) + parser.add_argument( + "--build-string", + help="increase output verbosity", + type=str, + default=None, + ) + parser.add_argument( + "--plugins-url", + "-pu", + type=str, + default=None, + ) + parser.add_argument( + "--channel", + "-c", + action="append", + default=None, + ) + parser.add_argument( + "--log", + default="WARNING", + choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], + ) + parser.add_argument("--dev", "-d", action="store_true") + return parser diff --git a/constructor-manager-ui/src/constructor_manager_ui/main.py b/constructor-manager-ui/src/constructor_manager_ui/main.py index 20d7b7cb..cdb3bad6 100644 --- a/constructor-manager-ui/src/constructor_manager_ui/main.py +++ b/constructor-manager-ui/src/constructor_manager_ui/main.py @@ -1,434 +1,46 @@ -"""Constructor manager main interface.""" - +import logging import sys -from typing import Optional +from typing import Any, Tuple -from qtpy.QtCore import QSize, Qt, QTimer, Signal -from qtpy.QtGui import QBrush, QMovie -from qtpy.QtWidgets import ( - QAbstractItemView, - QApplication, - QCheckBox, - QDialog, - QFrame, - QGridLayout, - QGroupBox, - QHBoxLayout, - QHeaderView, - QLabel, - QPushButton, - QStackedWidget, - QTableWidget, - QTableWidgetItem, - QVBoxLayout, - QWidget, -) +from qtpy.QtWidgets import QApplication -# To get mock data -from constructor_manager_ui.data import ( - INSTALL_INFORMATION, - PACKAGES, - UPDATE_AVAILABLE_VERSION, -) +from constructor_manager_ui.style.utils import update_styles +from constructor_manager_ui.widgets.dialog import InstallationManagerDialog +from constructor_manager_ui.cli import create_parser +from constructor_manager_api.utils.settings import load_settings # type: ignore # To setup image resources for .qss file from constructor_manager_ui.style import images # noqa -from constructor_manager_ui.style.utils import update_styles - -# Packages table constants -RELATED_PACKAGES = 0 -ALL_PACKAGES = 1 - - -class SpinnerWidget(QWidget): - def __init__(self, text: str, parent: Optional[QWidget] = None): - super().__init__(parent=parent) - - # Widgets for text and loading gif - self.text_label = QLabel(text) - spinner_label = QLabel() - self.spinner_movie = QMovie(":/images/loading.gif") - self.spinner_movie.setScaledSize(QSize(18, 18)) - spinner_label.setMovie(self.spinner_movie) - - # Set layout for text + loading indicator - layout = QHBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.text_label) - layout.addWidget(spinner_label) - layout.addStretch(1) - self.setLayout(layout) - self.spinner_movie.start() - - def set_text(self, text: str): - self.text_label.setText(text) - - def show(self): - self.spinner_movie.start() - super().show() - - def hide(self): - self.spinner_movie.stop() - super().hide() - - -class UpdateWidget(QWidget): - - install_version = Signal(str) - skip_version = Signal(str) - - def __init__(self, package_name: str, parent: Optional[QWidget] = None): - super().__init__(parent=parent) - self.package_name = package_name - self.update_available_version = None - - # Setup widgets - self.checking_update_widget = SpinnerWidget( - "Checking for updates...", parent=self - ) - self.up_to_date_widget = QWidget(self) - self._initialize_up_to_date_widget() - self.update_available_widget = QWidget(self) - self._initialize_update_available_widget() - - # Stack widgets to show one at a time and set layout - update_widget_layout = QHBoxLayout() - self.update_widgets = QStackedWidget(self) - self.update_widgets.addWidget(self.checking_update_widget) - self.update_widgets.addWidget(self.up_to_date_widget) - self.update_widgets.addWidget(self.update_available_widget) - update_widget_layout.addWidget(self.update_widgets) - self.setLayout(update_widget_layout) - - # Start showing checking updates widget - self.show_checking_updates_message() - - def _initialize_up_to_date_widget(self): - up_to_date_layout = QVBoxLayout() - update_msg_label = QLabel(f"Your {self.package_name} is up to date.") - up_to_date_layout.addWidget(update_msg_label) - - self.up_to_date_widget.setLayout(up_to_date_layout) - - def _initialize_update_available_widget(self): - new_version_layout = QVBoxLayout() - update_msg_label_layout = QHBoxLayout() - update_msg_label = QLabel( - f"A newer version of {self.package_name} is available!" - ) - update_msg_label_layout.addSpacing(15) - update_msg_label_layout.addWidget(update_msg_label) - - update_actions_layout = QHBoxLayout() - new_version_label = QLabel(self.update_available_version) - skip_version_button = QPushButton("Skip This Version") - install_version_button = QPushButton("Install This Version") - install_version_button.setObjectName("install_button") - update_actions_layout.addSpacing(20) - update_actions_layout.addWidget(new_version_label) - update_actions_layout.addSpacing(20) - update_actions_layout.addWidget(skip_version_button) - update_actions_layout.addSpacing(20) - update_actions_layout.addWidget(install_version_button) - update_actions_layout.addStretch(1) - new_version_layout.addLayout(update_msg_label_layout) - new_version_layout.addLayout(update_actions_layout) - - self.update_available_widget.setLayout(new_version_layout) - - # Connect buttons signals to parent class signals - skip_version_button.clicked.connect( - lambda checked: self.skip_version.emit(self.update_available_version) - ) - install_version_button.clicked.connect( - lambda checked: self.install_version.emit(self.update_available_version) - ) - - def show_checking_updates_message(self): - self.update_widgets.setCurrentWidget(self.checking_update_widget) - - def show_up_to_date_message(self): - self.update_widgets.setCurrentWidget(self.up_to_date_widget) - - def show_update_available_message(self, update_available_version): - self.update_available_version = update_available_version - if update_available_version: - self.update_widgets.setCurrentWidget(self.update_available_widget) - - -class PackagesTable(QTableWidget): - def __init__(self, packages, visible_packages=RELATED_PACKAGES, parent=None): - super().__init__(parent=parent) - self.packages = packages - self.visible_packages = visible_packages - self.setup() - - def _create_item(self, text: str, related_package: bool): - item = QTableWidgetItem(text) - if related_package: - background_brush = QBrush(Qt.GlobalColor.black) - else: - background_brush = QBrush(Qt.GlobalColor.darkGray) - item.setBackground(background_brush) - if not related_package: - foreground_brush = QBrush(Qt.GlobalColor.black) - item.setForeground(foreground_brush) - return item - - def setup(self): - # Set columns number and headers - self.setColumnCount(4) - self.setHorizontalHeaderLabels(["Name", "Version", "Source", "Build"]) - self.verticalHeader().setVisible(False) - - # Set horizontal headers alignment and config - self.horizontalHeader().setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter) - self.horizontalHeader().setStretchLastSection(True) - self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) - - # Hide table items borders - self.setShowGrid(False) - - # Set table selection to row - self.setSelectionBehavior(QAbstractItemView.SelectRows) - - def set_data(self, packages): - self.packages = packages - - # Populate table with data available - for name, version, source, build, related_package in self.packages: - self.insertRow(self.rowCount()) - package_row = self.rowCount() - 1 - self.setItem(package_row, 0, self._create_item(name, related_package)) - self.setItem(package_row, 1, self._create_item(version, related_package)) - self.setItem(package_row, 2, self._create_item(source, related_package)) - self.setItem(package_row, 3, self._create_item(build, related_package)) - if self.visible_packages == RELATED_PACKAGES and not related_package: - self.hideRow(package_row) - - def change_visible_packages(self, toggled_option): - if self.packages: - self.visible_packages = toggled_option - if toggled_option == RELATED_PACKAGES: - for idx, package in enumerate(self.packages): - name, version, source, build, related_package = package - if not related_package: - self.hideRow(idx) - else: - for idx, _ in enumerate(self.packages): - self.showRow(idx) - else: - self.visible_packages = toggled_option - - def change_detailed_info_visibility(self, state): - if state > Qt.Unchecked: - self.showColumn(2) - self.showColumn(3) - self.change_visible_packages(ALL_PACKAGES) - else: - self.hideColumn(2) - self.hideColumn(3) - self.change_visible_packages(RELATED_PACKAGES) -class InstallationManagerDialog(QDialog): - def __init__( - self, - package_name: str, - install_information, - parent: Optional[QWidget] = None, - ): - super().__init__(parent=parent) - self.package_name = package_name - self.current_version = install_information["current_version"] - self.snapshot_version = install_information["snapshot_version"] - self.updates_widget = None - self.packages_tablewidget = None - self.setWindowTitle(f"{package_name} installation manager") - self.setMinimumSize(QSize(500, 500)) - self.setup_layout() +logger = logging.getLogger(__name__) - def _create_install_information_group(self): - install_information_group = QGroupBox("Install information") - install_information_layout = QVBoxLayout() - current_version_layout = QHBoxLayout() - # Current version labels and button - current_version_label = QLabel( - f"{self.package_name} {self.current_version['version']}" - ) - last_modified_version_label = QLabel( - f"Last modified {self.current_version['last_modified']}" - ) - current_version_open_button = QPushButton("Open") - current_version_open_button.setObjectName("open_button") - current_version_layout.addWidget(current_version_label) - current_version_layout.addSpacing(10) - current_version_layout.addWidget(last_modified_version_label) - current_version_layout.addSpacing(10) - current_version_layout.addWidget(current_version_open_button) - current_version_layout.addStretch(1) - install_information_layout.addLayout(current_version_layout) +def dedup(items: Tuple[Any, ...]) -> Tuple[Any, ...]: + """Deduplicate an list of items.""" + new_items: Tuple[Any, ...] = () + for item in items: + if item not in new_items: + new_items += (item,) - # Line to divide current section and update section - install_version_line = QFrame() - install_version_line.setObjectName("separator") - install_version_line.setFrameShape(QFrame.HLine) - install_version_line.setFrameShadow(QFrame.Sunken) - install_version_line.setLineWidth(1) - install_information_layout.addWidget(install_version_line) + return new_items - # Update information widget - self.updates_widget = UpdateWidget(self.package_name, parent=self) - install_information_layout.addWidget(self.updates_widget) - install_information_group.setLayout(install_information_layout) - # Signals - # Open button signal - current_version_open_button.clicked.connect(self.open_installed) - # Update widget signals - self.updates_widget.install_version.connect(self.install_version) - self.updates_widget.skip_version.connect(self.skip_version) +def _configure_logging(log_level="WARNING"): + """Configure logging.""" + import constructor_manager_api # type: ignore - return install_information_group + log_level = getattr(logging, log_level.upper()) + log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + log_format = "%(levelname)s - %(message)s" + logging.basicConfig(format=log_format, level=log_level) - def _create_packages_group(self): - packages_group = QGroupBox("Packages") - packages_layout = QVBoxLayout() + # Set logging level for libraries used + api_logger = logging.getLogger(constructor_manager_api.__name__) + api_logger.setLevel(log_level) - packages_filter_layout = QHBoxLayout() - packages_filter_label = QLabel("Show:") - self.packages_spinner_label = SpinnerWidget("Loading packages...", parent=self) - show_detailed_view_checkbox = QCheckBox("Detailed view") - show_detailed_view_checkbox.setChecked(False) - packages_filter_layout.addWidget(packages_filter_label) - packages_filter_layout.addWidget(show_detailed_view_checkbox) - packages_filter_layout.addStretch(1) - packages_filter_layout.addWidget(self.packages_spinner_label) - - self.packages_tablewidget = PackagesTable(None, parent=self) - packages_layout.addLayout(packages_filter_layout) - packages_layout.addWidget(self.packages_tablewidget) - packages_group.setLayout(packages_layout) - - show_detailed_view_checkbox.stateChanged.connect( - self.packages_tablewidget.change_detailed_info_visibility - ) - self.packages_tablewidget.change_detailed_info_visibility( - show_detailed_view_checkbox.checkState() - ) - - return packages_group - - def _create_installation_actions_group(self): - installation_actions_group = QGroupBox("Installation Actions") - installation_actions_layout = QGridLayout() - - # Revert action - revert_button = QPushButton("Revert Installation") - revert_label = QLabel( - "Rollback installation to the latest snapshot: " - f"{self.snapshot_version['version']} " - f"({self.snapshot_version['last_modified']})" - ) - installation_actions_layout.addWidget(revert_button, 0, 0) - installation_actions_layout.addWidget(revert_label, 0, 1) - - # Reset action - reset_button = QPushButton("Reset Installation") - reset_label = QLabel( - "Reset the installation to clear " - "preferences, plugins, and other packages" - ) - installation_actions_layout.addWidget(reset_button, 1, 0) - installation_actions_layout.addWidget(reset_label, 1, 1) - - # Uninstall action - uninstall_button = QPushButton("Uninstall") - uninstall_button.setObjectName("uninstall_button") - uninstall_label = QLabel( - f"Remove the {self.package_name} Bundled App " - "and Installation Manager from your computer" - ) - installation_actions_layout.addWidget(uninstall_button, 2, 0) - installation_actions_layout.addWidget(uninstall_label, 2, 1) - - installation_actions_group.setLayout(installation_actions_layout) - - # Signals - revert_button.clicked.connect(self.revert_installation) - reset_button.clicked.connect(self.reset_installation) - uninstall_button.clicked.connect(self.uninstall) - - return installation_actions_group - - def setup_layout(self): - main_layout = QVBoxLayout(self) - - # Install information - install_information_group = self._create_install_information_group() - main_layout.addWidget(install_information_group) - - # Packages - packages_group = self._create_packages_group() - main_layout.addWidget(packages_group, stretch=1) - - # Installation Actions - installation_actions_group = self._create_installation_actions_group() - main_layout.addWidget(installation_actions_group) - - # Layout - self.setLayout(main_layout) - - def open_installed(self): - # TODO: To be handled with the backend. - # Maybe this needs to be a signal - print(self.current_version) - - def show_checking_updates_message(self): - self.updates_widget.show_checking_updates_message() - - def show_up_to_date_message(self): - self.updates_widget.show_up_to_date_message() - - def show_update_available_message(self, update_available_version): - self.updates_widget.show_update_available_message(update_available_version) - - def install_version(self, update_version): - # TODO: To be handled with the backend. - # Maybe this needs to be a signal - print(update_version) - - def skip_version(self, skip_version): - # TODO: To be handled with the backend. - # Maybe this needs to be a signal - print(skip_version) - - def set_packages(self, packages): - self.packages_spinner_label.show() - self.packages = packages - if self.packages_tablewidget: - self.packages_tablewidget.set_data(self.packages) - self.packages_spinner_label.hide() - - def revert_installation(self): - # TODO: To be handled with the backend. - # Maybe this needs to be a signal - print("Revert installation") - - def reset_installation(self): - # TODO: To be handled with the backend. - # Maybe this needs to be a signal - print("Reset installation") - - def uninstall(self): - # TODO: To be handled with the backend. - # Maybe this needs to be a signal - print("Uninstall") - - -def main(package_name: str): +def run(): """Run the main interface. Parameters @@ -436,23 +48,45 @@ def main(package_name: str): package_name : str Name of the package that the installation manager is handling. """ + parser = create_parser() + args = parser.parse_args() + if "channel" in args: + if args.channel: + args.channel = dedup(args.channel) + + settings = load_settings(args.package) + _configure_logging(settings["log"]) + + # TODO: Need to add a lock to avoid multiple instances app = QApplication([]) update_styles(app) + print(args) + + if "current_version" in args: + current_version = args.current_version or settings["current_version"] + + if "build_string" in args: + build_string = args.build_string or settings["build_string"] + + if "plugins_url" in args: + plugins_url = args.plugins_url or settings["plugins_url"] + + if "channel" in args: + channels = args.channel or dedup(settings["channels"]) + + dev = settings["dev"] if settings["dev"] is not None else args.dev + log = settings["log"] if settings["log"] is not None else args.log + # Installation manager dialog instance installation_manager_dlg = InstallationManagerDialog( - package_name, - INSTALL_INFORMATION, + args.package, + current_version=current_version, + build_string=build_string, + plugins_url=plugins_url, + channels=channels, + dev=dev, + log=log, ) installation_manager_dlg.show() - - # Mock data initialization loading. - # Change commented lines to check different UI update widget states - def data_initialization(): - installation_manager_dlg.set_packages(PACKAGES) - installation_manager_dlg.show_update_available_message(UPDATE_AVAILABLE_VERSION) - # installation_manager_dlg.show_up_to_date_message() - - QTimer.singleShot(5000, data_initialization) - sys.exit(app.exec_()) diff --git a/constructor-manager-ui/src/constructor_manager_ui/style/base.qss b/constructor-manager-ui/src/constructor_manager_ui/style/base.qss index 662c1bbc..de0cfe82 100644 --- a/constructor-manager-ui/src/constructor_manager_ui/style/base.qss +++ b/constructor-manager-ui/src/constructor_manager_ui/style/base.qss @@ -336,6 +336,12 @@ QHeaderView::section { /* ----------------- Buttons -------------------- */ +QPushButton:disabled { + background-color: @background-color13; + background-color: @background-selection-color01; + background-color: #758193; +} + QPushButton#open_button, QPushButton#install_button { background-color: @background-color08; } @@ -364,6 +370,10 @@ QPushButton#uninstall_button:pressed { background-color: @background-color016; } +QPushButton#open_button:disabled, QPushButton#install_button:disabled { + background-color: @background-color13; +} + /* ----------------- Separator -------------------- */ QFrame#separator { diff --git a/constructor-manager-ui/src/constructor_manager_ui/style/images.py b/constructor-manager-ui/src/constructor_manager_ui/style/images.py index 51883bd1..15631198 100644 --- a/constructor-manager-ui/src/constructor_manager_ui/style/images.py +++ b/constructor-manager-ui/src/constructor_manager_ui/style/images.py @@ -9,106 +9,106 @@ from qtpy import QtCore qt_resource_data = b"\ -\x00\x00\x06\x1c\ +\x00\x00\x06\x15\ \x3c\ \x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ \x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ \x2d\x38\x22\x20\x73\x74\x61\x6e\x64\x61\x6c\x6f\x6e\x65\x3d\x22\ -\x6e\x6f\x22\x3f\x3e\x0a\x3c\x21\x2d\x2d\x20\x47\x65\x6e\x65\x72\ -\x61\x74\x6f\x72\x3a\x20\x41\x64\x6f\x62\x65\x20\x49\x6c\x6c\x75\ -\x73\x74\x72\x61\x74\x6f\x72\x20\x32\x33\x2e\x30\x2e\x36\x2c\x20\ -\x53\x56\x47\x20\x45\x78\x70\x6f\x72\x74\x20\x50\x6c\x75\x67\x2d\ -\x49\x6e\x20\x2e\x20\x53\x56\x47\x20\x56\x65\x72\x73\x69\x6f\x6e\ -\x3a\x20\x36\x2e\x30\x30\x20\x42\x75\x69\x6c\x64\x20\x30\x29\x20\ -\x20\x2d\x2d\x3e\x0a\x0a\x3c\x73\x76\x67\x0a\x20\x20\x20\x76\x65\ +\x6e\x6f\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x0a\x20\x20\x20\x76\x65\ \x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x31\x22\x0a\x20\x20\x20\x69\ -\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\x0a\x20\x20\x20\x78\ -\x3d\x22\x30\x70\x78\x22\x0a\x20\x20\x20\x79\x3d\x22\x30\x70\x78\ -\x22\x0a\x20\x20\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\ -\x30\x20\x31\x30\x30\x20\x31\x30\x30\x22\x0a\x20\x20\x20\x73\x74\ -\x79\x6c\x65\x3d\x22\x65\x6e\x61\x62\x6c\x65\x2d\x62\x61\x63\x6b\ -\x67\x72\x6f\x75\x6e\x64\x3a\x6e\x65\x77\x20\x30\x20\x30\x20\x31\ -\x30\x30\x20\x31\x30\x30\x3b\x22\x0a\x20\x20\x20\x78\x6d\x6c\x3a\ -\x73\x70\x61\x63\x65\x3d\x22\x70\x72\x65\x73\x65\x72\x76\x65\x22\ -\x0a\x20\x20\x20\x73\x6f\x64\x69\x70\x6f\x64\x69\x3a\x64\x6f\x63\ -\x6e\x61\x6d\x65\x3d\x22\x6c\x65\x66\x74\x5f\x61\x72\x72\x6f\x77\ -\x2e\x73\x76\x67\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\ -\x65\x3a\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x32\x2e\x31\ -\x20\x28\x39\x63\x36\x64\x34\x31\x65\x34\x31\x30\x2c\x20\x32\x30\ -\x32\x32\x2d\x30\x37\x2d\x31\x34\x29\x22\x0a\x20\x20\x20\x78\x6d\ -\x6c\x6e\x73\x3a\x69\x6e\x6b\x73\x63\x61\x70\x65\x3d\x22\x68\x74\ -\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\ -\x65\x2e\x6f\x72\x67\x2f\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\x73\ -\x2f\x69\x6e\x6b\x73\x63\x61\x70\x65\x22\x0a\x20\x20\x20\x78\x6d\ -\x6c\x6e\x73\x3a\x73\x6f\x64\x69\x70\x6f\x64\x69\x3d\x22\x68\x74\ -\x74\x70\x3a\x2f\x2f\x73\x6f\x64\x69\x70\x6f\x64\x69\x2e\x73\x6f\ -\x75\x72\x63\x65\x66\x6f\x72\x67\x65\x2e\x6e\x65\x74\x2f\x44\x54\ -\x44\x2f\x73\x6f\x64\x69\x70\x6f\x64\x69\x2d\x30\x2e\x64\x74\x64\ -\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ -\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ -\x30\x30\x2f\x73\x76\x67\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\ -\x3a\x73\x76\x67\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\ -\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\ -\x22\x3e\x3c\x64\x65\x66\x73\x0a\x20\x20\x20\x69\x64\x3d\x22\x64\ -\x65\x66\x73\x37\x22\x20\x2f\x3e\x3c\x73\x6f\x64\x69\x70\x6f\x64\ -\x69\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x0a\x20\x20\x20\x69\ -\x64\x3d\x22\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x35\x22\x0a\x20\ -\x20\x20\x70\x61\x67\x65\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x66\x66\ -\x66\x66\x66\x66\x22\x0a\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x63\ -\x6f\x6c\x6f\x72\x3d\x22\x23\x30\x30\x30\x30\x30\x30\x22\x0a\x20\ -\x20\x20\x62\x6f\x72\x64\x65\x72\x6f\x70\x61\x63\x69\x74\x79\x3d\ -\x22\x30\x2e\x32\x35\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\ -\x70\x65\x3a\x73\x68\x6f\x77\x70\x61\x67\x65\x73\x68\x61\x64\x6f\ -\x77\x3d\x22\x32\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\ -\x65\x3a\x70\x61\x67\x65\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\ -\x2e\x30\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\ -\x70\x61\x67\x65\x63\x68\x65\x63\x6b\x65\x72\x62\x6f\x61\x72\x64\ -\x3d\x22\x30\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\ -\x3a\x64\x65\x73\x6b\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x64\x31\x64\ -\x31\x64\x31\x22\x0a\x20\x20\x20\x73\x68\x6f\x77\x67\x72\x69\x64\ -\x3d\x22\x66\x61\x6c\x73\x65\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\ -\x63\x61\x70\x65\x3a\x7a\x6f\x6f\x6d\x3d\x22\x35\x2e\x32\x35\x22\ -\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x78\x3d\ -\x22\x35\x30\x2e\x30\x39\x35\x32\x33\x38\x22\x0a\x20\x20\x20\x69\ -\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x79\x3d\x22\x35\x30\x2e\x30\ -\x39\x35\x32\x33\x38\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\ -\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x77\x69\x64\x74\x68\x3d\ -\x22\x31\x33\x36\x36\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\ -\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x68\x65\x69\x67\x68\x74\ -\x3d\x22\x37\x30\x35\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\ -\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x78\x3d\x22\x2d\x38\x22\ -\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\ -\x64\x6f\x77\x2d\x79\x3d\x22\x2d\x38\x22\x0a\x20\x20\x20\x69\x6e\ -\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x6d\x61\ -\x78\x69\x6d\x69\x7a\x65\x64\x3d\x22\x31\x22\x0a\x20\x20\x20\x69\ -\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x75\x72\x72\x65\x6e\x74\x2d\ -\x6c\x61\x79\x65\x72\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\x20\ -\x2f\x3e\x0a\x3c\x70\x6f\x6c\x79\x67\x6f\x6e\x0a\x20\x20\x20\x70\ -\x6f\x69\x6e\x74\x73\x3d\x22\x32\x30\x2e\x39\x2c\x35\x30\x20\x37\ -\x39\x2e\x31\x2c\x39\x37\x2e\x34\x20\x37\x39\x2e\x31\x2c\x32\x2e\ -\x36\x20\x22\x0a\x20\x20\x20\x69\x64\x3d\x22\x70\x6f\x6c\x79\x67\ -\x6f\x6e\x32\x22\x20\x2f\x3e\x0a\x3c\x70\x61\x74\x68\x0a\x20\x20\ -\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\x3a\x23\x66\x66\ -\x66\x66\x66\x66\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\ -\x68\x3a\x30\x2e\x31\x39\x30\x34\x37\x36\x22\x0a\x20\x20\x20\x64\ -\x3d\x22\x4d\x20\x35\x30\x2e\x30\x39\x35\x32\x33\x38\x2c\x37\x33\ -\x2e\x36\x31\x39\x30\x35\x35\x20\x43\x20\x33\x34\x2e\x32\x32\x33\ -\x38\x31\x2c\x36\x30\x2e\x36\x39\x33\x36\x37\x34\x20\x32\x31\x2e\ -\x32\x35\x38\x30\x31\x39\x2c\x35\x30\x2e\x30\x34\x38\x38\x37\x34\ -\x20\x32\x31\x2e\x32\x38\x32\x33\x37\x2c\x34\x39\x2e\x39\x36\x33\ -\x39\x34\x33\x20\x32\x31\x2e\x33\x30\x36\x37\x32\x31\x2c\x34\x39\ -\x2e\x38\x37\x39\x30\x31\x33\x20\x33\x34\x2e\x32\x39\x32\x34\x33\ -\x36\x2c\x33\x39\x2e\x32\x35\x30\x34\x33\x38\x20\x35\x30\x2e\x31\ -\x33\x39\x35\x31\x32\x2c\x32\x36\x2e\x33\x34\x34\x38\x38\x38\x20\ -\x4c\x20\x37\x38\x2e\x39\x35\x32\x33\x38\x31\x2c\x32\x2e\x38\x38\ -\x30\x32\x35\x33\x32\x20\x37\x39\x2e\x30\x30\x30\x37\x34\x35\x2c\ -\x32\x36\x2e\x35\x30\x33\x30\x35\x33\x20\x63\x20\x30\x2e\x30\x32\ -\x36\x36\x2c\x31\x32\x2e\x39\x39\x32\x35\x34\x31\x20\x30\x2e\x30\ -\x32\x36\x36\x2c\x33\x34\x2e\x31\x39\x36\x34\x32\x37\x20\x30\x2c\ -\x34\x37\x2e\x31\x31\x39\x37\x34\x37\x20\x6c\x20\x2d\x30\x2e\x30\ -\x34\x38\x33\x36\x2c\x32\x33\x2e\x34\x39\x36\x39\x34\x37\x20\x7a\ -\x22\x0a\x20\x20\x20\x69\x64\x3d\x22\x70\x61\x74\x68\x31\x38\x32\ -\x22\x20\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\x0a\ +\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\x0a\x20\x20\x20\x76\ +\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x31\x32\x20\x31\ +\x36\x22\x0a\x20\x20\x20\x73\x6f\x64\x69\x70\x6f\x64\x69\x3a\x64\ +\x6f\x63\x6e\x61\x6d\x65\x3d\x22\x63\x68\x65\x63\x6b\x2e\x73\x76\ +\x67\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x76\ +\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x32\x2e\x31\x20\x28\x39\ +\x63\x36\x64\x34\x31\x65\x34\x31\x30\x2c\x20\x32\x30\x32\x32\x2d\ +\x30\x37\x2d\x31\x34\x29\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\ +\x3a\x69\x6e\x6b\x73\x63\x61\x70\x65\x3d\x22\x68\x74\x74\x70\x3a\ +\x2f\x2f\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\x2e\x6f\ +\x72\x67\x2f\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\x73\x2f\x69\x6e\ +\x6b\x73\x63\x61\x70\x65\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\ +\x3a\x73\x6f\x64\x69\x70\x6f\x64\x69\x3d\x22\x68\x74\x74\x70\x3a\ +\x2f\x2f\x73\x6f\x64\x69\x70\x6f\x64\x69\x2e\x73\x6f\x75\x72\x63\ +\x65\x66\x6f\x72\x67\x65\x2e\x6e\x65\x74\x2f\x44\x54\x44\x2f\x73\ +\x6f\x64\x69\x70\x6f\x64\x69\x2d\x30\x2e\x64\x74\x64\x22\x0a\x20\ +\x20\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\ +\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\ +\x73\x76\x67\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x76\ +\x67\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\ +\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x3e\x0a\ +\x20\x20\x3c\x64\x65\x66\x73\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\ +\x22\x64\x65\x66\x73\x39\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x73\x6f\ +\x64\x69\x70\x6f\x64\x69\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\ +\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x6e\x61\x6d\x65\x64\x76\ +\x69\x65\x77\x37\x22\x0a\x20\x20\x20\x20\x20\x70\x61\x67\x65\x63\ +\x6f\x6c\x6f\x72\x3d\x22\x23\x66\x66\x66\x66\x66\x66\x22\x0a\x20\ +\x20\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x63\x6f\x6c\x6f\x72\x3d\ +\x22\x23\x30\x30\x30\x30\x30\x30\x22\x0a\x20\x20\x20\x20\x20\x62\ +\x6f\x72\x64\x65\x72\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\x2e\ +\x32\x35\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\ +\x65\x3a\x73\x68\x6f\x77\x70\x61\x67\x65\x73\x68\x61\x64\x6f\x77\ +\x3d\x22\x32\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\ +\x70\x65\x3a\x70\x61\x67\x65\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\ +\x30\x2e\x30\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\ +\x70\x65\x3a\x70\x61\x67\x65\x63\x68\x65\x63\x6b\x65\x72\x62\x6f\ +\x61\x72\x64\x3d\x22\x30\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\ +\x73\x63\x61\x70\x65\x3a\x64\x65\x73\x6b\x63\x6f\x6c\x6f\x72\x3d\ +\x22\x23\x64\x31\x64\x31\x64\x31\x22\x0a\x20\x20\x20\x20\x20\x73\ +\x68\x6f\x77\x67\x72\x69\x64\x3d\x22\x66\x61\x6c\x73\x65\x22\x0a\ +\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x7a\x6f\ +\x6f\x6d\x3d\x22\x33\x32\x2e\x38\x31\x32\x35\x22\x0a\x20\x20\x20\ +\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x78\x3d\x22\x36\ +\x2e\x30\x30\x33\x38\x30\x39\x35\x22\x0a\x20\x20\x20\x20\x20\x69\ +\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x79\x3d\x22\x38\x2e\x30\x31\ +\x35\x32\x33\x38\x31\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\ +\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x77\x69\x64\x74\ +\x68\x3d\x22\x31\x33\x36\x36\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\ +\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x68\x65\ +\x69\x67\x68\x74\x3d\x22\x37\x30\x35\x22\x0a\x20\x20\x20\x20\x20\ +\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\ +\x78\x3d\x22\x2d\x38\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\ +\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x79\x3d\x22\x2d\ +\x38\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\ +\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x6d\x61\x78\x69\x6d\x69\x7a\x65\ +\x64\x3d\x22\x31\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\ +\x61\x70\x65\x3a\x63\x75\x72\x72\x65\x6e\x74\x2d\x6c\x61\x79\x65\ +\x72\x3d\x22\x67\x34\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x67\x0a\x20\ +\x20\x20\x20\x20\x69\x64\x3d\x22\x67\x34\x22\x3e\x0a\x20\x20\x20\ +\x20\x3c\x70\x61\x74\x68\x0a\x20\x20\x20\x20\x20\x20\x20\x64\x3d\ +\x22\x4d\x31\x32\x20\x35\x6c\x2d\x38\x20\x38\x2d\x34\x2d\x34\x20\ +\x31\x2e\x35\x2d\x31\x2e\x35\x4c\x34\x20\x31\x30\x6c\x36\x2e\x35\ +\x2d\x36\x2e\x35\x4c\x31\x32\x20\x35\x7a\x22\x0a\x20\x20\x20\x20\ +\x20\x20\x20\x69\x64\x3d\x22\x70\x61\x74\x68\x32\x22\x20\x2f\x3e\ +\x0a\x20\x20\x20\x20\x3c\x70\x61\x74\x68\x0a\x20\x20\x20\x20\x20\ +\x20\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\x3a\x23\x66\ +\x66\x66\x66\x66\x66\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\ +\x74\x68\x3a\x30\x2e\x30\x33\x30\x34\x37\x36\x32\x22\x0a\x20\x20\ +\x20\x20\x20\x20\x20\x64\x3d\x22\x4d\x20\x32\x2e\x30\x30\x33\x36\ +\x34\x38\x36\x2c\x31\x30\x2e\x39\x37\x38\x38\x38\x37\x20\x30\x2e\ +\x30\x33\x30\x33\x33\x30\x31\x34\x2c\x39\x2e\x30\x30\x35\x33\x39\ +\x33\x37\x20\x30\x2e\x37\x36\x39\x36\x33\x34\x37\x37\x2c\x38\x2e\ +\x32\x36\x36\x35\x35\x31\x32\x20\x31\x2e\x35\x30\x38\x39\x33\x39\ +\x34\x2c\x37\x2e\x35\x32\x37\x37\x30\x38\x33\x20\x32\x2e\x37\x35\ +\x38\x31\x38\x32\x39\x2c\x38\x2e\x37\x37\x37\x32\x33\x30\x36\x20\ +\x34\x2e\x30\x30\x37\x34\x32\x36\x34\x2c\x31\x30\x2e\x30\x32\x36\ +\x37\x35\x33\x20\x37\x2e\x32\x35\x33\x33\x38\x32\x37\x2c\x36\x2e\ +\x37\x38\x30\x39\x30\x33\x20\x31\x30\x2e\x34\x39\x39\x33\x33\x39\ +\x2c\x33\x2e\x35\x33\x35\x30\x35\x33\x31\x20\x31\x31\x2e\x32\x33\ +\x30\x34\x38\x31\x2c\x34\x2e\x32\x36\x36\x36\x37\x31\x34\x20\x31\ +\x31\x2e\x39\x36\x31\x36\x32\x33\x2c\x34\x2e\x39\x39\x38\x32\x38\ +\x39\x36\x20\x37\x2e\x39\x38\x34\x36\x36\x34\x37\x2c\x38\x2e\x39\ +\x37\x35\x33\x33\x35\x33\x20\x43\x20\x35\x2e\x37\x39\x37\x33\x33\ +\x37\x39\x2c\x31\x31\x2e\x31\x36\x32\x37\x31\x31\x20\x34\x2e\x30\ +\x30\x30\x37\x39\x30\x32\x2c\x31\x32\x2e\x39\x35\x32\x33\x38\x31\ +\x20\x33\x2e\x39\x39\x32\x33\x33\x36\x38\x2c\x31\x32\x2e\x39\x35\ +\x32\x33\x38\x31\x20\x63\x20\x2d\x30\x2e\x30\x30\x38\x34\x35\x2c\ +\x30\x20\x2d\x30\x2e\x39\x30\x33\x33\x36\x33\x31\x2c\x2d\x30\x2e\ +\x38\x38\x38\x30\x37\x32\x20\x2d\x31\x2e\x39\x38\x38\x36\x38\x38\ +\x32\x2c\x2d\x31\x2e\x39\x37\x33\x34\x39\x34\x20\x7a\x22\x0a\x20\ +\x20\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x70\x61\x74\x68\x33\x30\ +\x37\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x2f\x67\x3e\x0a\x3c\x2f\x73\ +\x76\x67\x3e\x0a\ \x00\x05\x84\x90\ \x47\ \x49\x46\x38\x39\x61\x00\x01\x00\x01\xf7\xff\x00\xf9\xfa\xfa\x72\ @@ -22803,106 +22803,6 @@ \x2e\x34\x32\x38\x35\x37\x31\x34\x20\x5a\x22\x0a\x20\x20\x20\x69\ \x64\x3d\x22\x70\x61\x74\x68\x31\x32\x38\x22\x20\x2f\x3e\x3c\x2f\ \x73\x76\x67\x3e\x0a\ -\x00\x00\x06\x15\ -\x3c\ -\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ -\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ -\x2d\x38\x22\x20\x73\x74\x61\x6e\x64\x61\x6c\x6f\x6e\x65\x3d\x22\ -\x6e\x6f\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x0a\x20\x20\x20\x76\x65\ -\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x31\x22\x0a\x20\x20\x20\x69\ -\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\x0a\x20\x20\x20\x76\ -\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x31\x32\x20\x31\ -\x36\x22\x0a\x20\x20\x20\x73\x6f\x64\x69\x70\x6f\x64\x69\x3a\x64\ -\x6f\x63\x6e\x61\x6d\x65\x3d\x22\x63\x68\x65\x63\x6b\x2e\x73\x76\ -\x67\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x76\ -\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x32\x2e\x31\x20\x28\x39\ -\x63\x36\x64\x34\x31\x65\x34\x31\x30\x2c\x20\x32\x30\x32\x32\x2d\ -\x30\x37\x2d\x31\x34\x29\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\ -\x3a\x69\x6e\x6b\x73\x63\x61\x70\x65\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\x2e\x6f\ -\x72\x67\x2f\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\x73\x2f\x69\x6e\ -\x6b\x73\x63\x61\x70\x65\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\ -\x3a\x73\x6f\x64\x69\x70\x6f\x64\x69\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x73\x6f\x64\x69\x70\x6f\x64\x69\x2e\x73\x6f\x75\x72\x63\ -\x65\x66\x6f\x72\x67\x65\x2e\x6e\x65\x74\x2f\x44\x54\x44\x2f\x73\ -\x6f\x64\x69\x70\x6f\x64\x69\x2d\x30\x2e\x64\x74\x64\x22\x0a\x20\ -\x20\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\ -\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\ -\x73\x76\x67\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x76\ -\x67\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\ -\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x3e\x0a\ -\x20\x20\x3c\x64\x65\x66\x73\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\ -\x22\x64\x65\x66\x73\x39\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x73\x6f\ -\x64\x69\x70\x6f\x64\x69\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\ -\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x6e\x61\x6d\x65\x64\x76\ -\x69\x65\x77\x37\x22\x0a\x20\x20\x20\x20\x20\x70\x61\x67\x65\x63\ -\x6f\x6c\x6f\x72\x3d\x22\x23\x66\x66\x66\x66\x66\x66\x22\x0a\x20\ -\x20\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x63\x6f\x6c\x6f\x72\x3d\ -\x22\x23\x30\x30\x30\x30\x30\x30\x22\x0a\x20\x20\x20\x20\x20\x62\ -\x6f\x72\x64\x65\x72\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\x2e\ -\x32\x35\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\ -\x65\x3a\x73\x68\x6f\x77\x70\x61\x67\x65\x73\x68\x61\x64\x6f\x77\ -\x3d\x22\x32\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\ -\x70\x65\x3a\x70\x61\x67\x65\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\ -\x30\x2e\x30\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\ -\x70\x65\x3a\x70\x61\x67\x65\x63\x68\x65\x63\x6b\x65\x72\x62\x6f\ -\x61\x72\x64\x3d\x22\x30\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\ -\x73\x63\x61\x70\x65\x3a\x64\x65\x73\x6b\x63\x6f\x6c\x6f\x72\x3d\ -\x22\x23\x64\x31\x64\x31\x64\x31\x22\x0a\x20\x20\x20\x20\x20\x73\ -\x68\x6f\x77\x67\x72\x69\x64\x3d\x22\x66\x61\x6c\x73\x65\x22\x0a\ -\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x7a\x6f\ -\x6f\x6d\x3d\x22\x33\x32\x2e\x38\x31\x32\x35\x22\x0a\x20\x20\x20\ -\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x78\x3d\x22\x36\ -\x2e\x30\x30\x33\x38\x30\x39\x35\x22\x0a\x20\x20\x20\x20\x20\x69\ -\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x79\x3d\x22\x38\x2e\x30\x31\ -\x35\x32\x33\x38\x31\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\ -\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x77\x69\x64\x74\ -\x68\x3d\x22\x31\x33\x36\x36\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\ -\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x68\x65\ -\x69\x67\x68\x74\x3d\x22\x37\x30\x35\x22\x0a\x20\x20\x20\x20\x20\ -\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\ -\x78\x3d\x22\x2d\x38\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\ -\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x79\x3d\x22\x2d\ -\x38\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\ -\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x6d\x61\x78\x69\x6d\x69\x7a\x65\ -\x64\x3d\x22\x31\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\ -\x61\x70\x65\x3a\x63\x75\x72\x72\x65\x6e\x74\x2d\x6c\x61\x79\x65\ -\x72\x3d\x22\x67\x34\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x67\x0a\x20\ -\x20\x20\x20\x20\x69\x64\x3d\x22\x67\x34\x22\x3e\x0a\x20\x20\x20\ -\x20\x3c\x70\x61\x74\x68\x0a\x20\x20\x20\x20\x20\x20\x20\x64\x3d\ -\x22\x4d\x31\x32\x20\x35\x6c\x2d\x38\x20\x38\x2d\x34\x2d\x34\x20\ -\x31\x2e\x35\x2d\x31\x2e\x35\x4c\x34\x20\x31\x30\x6c\x36\x2e\x35\ -\x2d\x36\x2e\x35\x4c\x31\x32\x20\x35\x7a\x22\x0a\x20\x20\x20\x20\ -\x20\x20\x20\x69\x64\x3d\x22\x70\x61\x74\x68\x32\x22\x20\x2f\x3e\ -\x0a\x20\x20\x20\x20\x3c\x70\x61\x74\x68\x0a\x20\x20\x20\x20\x20\ -\x20\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\x3a\x23\x66\ -\x66\x66\x66\x66\x66\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\ -\x74\x68\x3a\x30\x2e\x30\x33\x30\x34\x37\x36\x32\x22\x0a\x20\x20\ -\x20\x20\x20\x20\x20\x64\x3d\x22\x4d\x20\x32\x2e\x30\x30\x33\x36\ -\x34\x38\x36\x2c\x31\x30\x2e\x39\x37\x38\x38\x38\x37\x20\x30\x2e\ -\x30\x33\x30\x33\x33\x30\x31\x34\x2c\x39\x2e\x30\x30\x35\x33\x39\ -\x33\x37\x20\x30\x2e\x37\x36\x39\x36\x33\x34\x37\x37\x2c\x38\x2e\ -\x32\x36\x36\x35\x35\x31\x32\x20\x31\x2e\x35\x30\x38\x39\x33\x39\ -\x34\x2c\x37\x2e\x35\x32\x37\x37\x30\x38\x33\x20\x32\x2e\x37\x35\ -\x38\x31\x38\x32\x39\x2c\x38\x2e\x37\x37\x37\x32\x33\x30\x36\x20\ -\x34\x2e\x30\x30\x37\x34\x32\x36\x34\x2c\x31\x30\x2e\x30\x32\x36\ -\x37\x35\x33\x20\x37\x2e\x32\x35\x33\x33\x38\x32\x37\x2c\x36\x2e\ -\x37\x38\x30\x39\x30\x33\x20\x31\x30\x2e\x34\x39\x39\x33\x33\x39\ -\x2c\x33\x2e\x35\x33\x35\x30\x35\x33\x31\x20\x31\x31\x2e\x32\x33\ -\x30\x34\x38\x31\x2c\x34\x2e\x32\x36\x36\x36\x37\x31\x34\x20\x31\ -\x31\x2e\x39\x36\x31\x36\x32\x33\x2c\x34\x2e\x39\x39\x38\x32\x38\ -\x39\x36\x20\x37\x2e\x39\x38\x34\x36\x36\x34\x37\x2c\x38\x2e\x39\ -\x37\x35\x33\x33\x35\x33\x20\x43\x20\x35\x2e\x37\x39\x37\x33\x33\ -\x37\x39\x2c\x31\x31\x2e\x31\x36\x32\x37\x31\x31\x20\x34\x2e\x30\ -\x30\x30\x37\x39\x30\x32\x2c\x31\x32\x2e\x39\x35\x32\x33\x38\x31\ -\x20\x33\x2e\x39\x39\x32\x33\x33\x36\x38\x2c\x31\x32\x2e\x39\x35\ -\x32\x33\x38\x31\x20\x63\x20\x2d\x30\x2e\x30\x30\x38\x34\x35\x2c\ -\x30\x20\x2d\x30\x2e\x39\x30\x33\x33\x36\x33\x31\x2c\x2d\x30\x2e\ -\x38\x38\x38\x30\x37\x32\x20\x2d\x31\x2e\x39\x38\x38\x36\x38\x38\ -\x32\x2c\x2d\x31\x2e\x39\x37\x33\x34\x39\x34\x20\x7a\x22\x0a\x20\ -\x20\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x70\x61\x74\x68\x33\x30\ -\x37\x22\x20\x2f\x3e\x0a\x20\x20\x3c\x2f\x67\x3e\x0a\x3c\x2f\x73\ -\x76\x67\x3e\x0a\ \x00\x00\x06\x28\ \x3c\ \x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ @@ -23004,6 +22904,106 @@ \x20\x32\x2e\x38\x31\x31\x32\x35\x32\x35\x20\x5a\x22\x0a\x20\x20\ \x20\x69\x64\x3d\x22\x70\x61\x74\x68\x31\x38\x32\x22\x20\x2f\x3e\ \x3c\x2f\x73\x76\x67\x3e\x0a\ +\x00\x00\x06\x1c\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x20\x73\x74\x61\x6e\x64\x61\x6c\x6f\x6e\x65\x3d\x22\ +\x6e\x6f\x22\x3f\x3e\x0a\x3c\x21\x2d\x2d\x20\x47\x65\x6e\x65\x72\ +\x61\x74\x6f\x72\x3a\x20\x41\x64\x6f\x62\x65\x20\x49\x6c\x6c\x75\ +\x73\x74\x72\x61\x74\x6f\x72\x20\x32\x33\x2e\x30\x2e\x36\x2c\x20\ +\x53\x56\x47\x20\x45\x78\x70\x6f\x72\x74\x20\x50\x6c\x75\x67\x2d\ +\x49\x6e\x20\x2e\x20\x53\x56\x47\x20\x56\x65\x72\x73\x69\x6f\x6e\ +\x3a\x20\x36\x2e\x30\x30\x20\x42\x75\x69\x6c\x64\x20\x30\x29\x20\ +\x20\x2d\x2d\x3e\x0a\x0a\x3c\x73\x76\x67\x0a\x20\x20\x20\x76\x65\ +\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x31\x22\x0a\x20\x20\x20\x69\ +\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\x0a\x20\x20\x20\x78\ +\x3d\x22\x30\x70\x78\x22\x0a\x20\x20\x20\x79\x3d\x22\x30\x70\x78\ +\x22\x0a\x20\x20\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\ +\x30\x20\x31\x30\x30\x20\x31\x30\x30\x22\x0a\x20\x20\x20\x73\x74\ +\x79\x6c\x65\x3d\x22\x65\x6e\x61\x62\x6c\x65\x2d\x62\x61\x63\x6b\ +\x67\x72\x6f\x75\x6e\x64\x3a\x6e\x65\x77\x20\x30\x20\x30\x20\x31\ +\x30\x30\x20\x31\x30\x30\x3b\x22\x0a\x20\x20\x20\x78\x6d\x6c\x3a\ +\x73\x70\x61\x63\x65\x3d\x22\x70\x72\x65\x73\x65\x72\x76\x65\x22\ +\x0a\x20\x20\x20\x73\x6f\x64\x69\x70\x6f\x64\x69\x3a\x64\x6f\x63\ +\x6e\x61\x6d\x65\x3d\x22\x6c\x65\x66\x74\x5f\x61\x72\x72\x6f\x77\ +\x2e\x73\x76\x67\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\ +\x65\x3a\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x32\x2e\x31\ +\x20\x28\x39\x63\x36\x64\x34\x31\x65\x34\x31\x30\x2c\x20\x32\x30\ +\x32\x32\x2d\x30\x37\x2d\x31\x34\x29\x22\x0a\x20\x20\x20\x78\x6d\ +\x6c\x6e\x73\x3a\x69\x6e\x6b\x73\x63\x61\x70\x65\x3d\x22\x68\x74\ +\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\ +\x65\x2e\x6f\x72\x67\x2f\x6e\x61\x6d\x65\x73\x70\x61\x63\x65\x73\ +\x2f\x69\x6e\x6b\x73\x63\x61\x70\x65\x22\x0a\x20\x20\x20\x78\x6d\ +\x6c\x6e\x73\x3a\x73\x6f\x64\x69\x70\x6f\x64\x69\x3d\x22\x68\x74\ +\x74\x70\x3a\x2f\x2f\x73\x6f\x64\x69\x70\x6f\x64\x69\x2e\x73\x6f\ +\x75\x72\x63\x65\x66\x6f\x72\x67\x65\x2e\x6e\x65\x74\x2f\x44\x54\ +\x44\x2f\x73\x6f\x64\x69\x70\x6f\x64\x69\x2d\x30\x2e\x64\x74\x64\ +\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ +\x30\x30\x2f\x73\x76\x67\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\ +\x3a\x73\x76\x67\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\ +\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\ +\x22\x3e\x3c\x64\x65\x66\x73\x0a\x20\x20\x20\x69\x64\x3d\x22\x64\ +\x65\x66\x73\x37\x22\x20\x2f\x3e\x3c\x73\x6f\x64\x69\x70\x6f\x64\ +\x69\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x0a\x20\x20\x20\x69\ +\x64\x3d\x22\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x35\x22\x0a\x20\ +\x20\x20\x70\x61\x67\x65\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x66\x66\ +\x66\x66\x66\x66\x22\x0a\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x63\ +\x6f\x6c\x6f\x72\x3d\x22\x23\x30\x30\x30\x30\x30\x30\x22\x0a\x20\ +\x20\x20\x62\x6f\x72\x64\x65\x72\x6f\x70\x61\x63\x69\x74\x79\x3d\ +\x22\x30\x2e\x32\x35\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\ +\x70\x65\x3a\x73\x68\x6f\x77\x70\x61\x67\x65\x73\x68\x61\x64\x6f\ +\x77\x3d\x22\x32\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\ +\x65\x3a\x70\x61\x67\x65\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\ +\x2e\x30\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\ +\x70\x61\x67\x65\x63\x68\x65\x63\x6b\x65\x72\x62\x6f\x61\x72\x64\ +\x3d\x22\x30\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\ +\x3a\x64\x65\x73\x6b\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x64\x31\x64\ +\x31\x64\x31\x22\x0a\x20\x20\x20\x73\x68\x6f\x77\x67\x72\x69\x64\ +\x3d\x22\x66\x61\x6c\x73\x65\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\ +\x63\x61\x70\x65\x3a\x7a\x6f\x6f\x6d\x3d\x22\x35\x2e\x32\x35\x22\ +\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x78\x3d\ +\x22\x35\x30\x2e\x30\x39\x35\x32\x33\x38\x22\x0a\x20\x20\x20\x69\ +\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x79\x3d\x22\x35\x30\x2e\x30\ +\x39\x35\x32\x33\x38\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\ +\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x77\x69\x64\x74\x68\x3d\ +\x22\x31\x33\x36\x36\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\ +\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x68\x65\x69\x67\x68\x74\ +\x3d\x22\x37\x30\x35\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\ +\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x78\x3d\x22\x2d\x38\x22\ +\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\ +\x64\x6f\x77\x2d\x79\x3d\x22\x2d\x38\x22\x0a\x20\x20\x20\x69\x6e\ +\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x6d\x61\ +\x78\x69\x6d\x69\x7a\x65\x64\x3d\x22\x31\x22\x0a\x20\x20\x20\x69\ +\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\x75\x72\x72\x65\x6e\x74\x2d\ +\x6c\x61\x79\x65\x72\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\x20\ +\x2f\x3e\x0a\x3c\x70\x6f\x6c\x79\x67\x6f\x6e\x0a\x20\x20\x20\x70\ +\x6f\x69\x6e\x74\x73\x3d\x22\x32\x30\x2e\x39\x2c\x35\x30\x20\x37\ +\x39\x2e\x31\x2c\x39\x37\x2e\x34\x20\x37\x39\x2e\x31\x2c\x32\x2e\ +\x36\x20\x22\x0a\x20\x20\x20\x69\x64\x3d\x22\x70\x6f\x6c\x79\x67\ +\x6f\x6e\x32\x22\x20\x2f\x3e\x0a\x3c\x70\x61\x74\x68\x0a\x20\x20\ +\x20\x73\x74\x79\x6c\x65\x3d\x22\x66\x69\x6c\x6c\x3a\x23\x66\x66\ +\x66\x66\x66\x66\x3b\x73\x74\x72\x6f\x6b\x65\x2d\x77\x69\x64\x74\ +\x68\x3a\x30\x2e\x31\x39\x30\x34\x37\x36\x22\x0a\x20\x20\x20\x64\ +\x3d\x22\x4d\x20\x35\x30\x2e\x30\x39\x35\x32\x33\x38\x2c\x37\x33\ +\x2e\x36\x31\x39\x30\x35\x35\x20\x43\x20\x33\x34\x2e\x32\x32\x33\ +\x38\x31\x2c\x36\x30\x2e\x36\x39\x33\x36\x37\x34\x20\x32\x31\x2e\ +\x32\x35\x38\x30\x31\x39\x2c\x35\x30\x2e\x30\x34\x38\x38\x37\x34\ +\x20\x32\x31\x2e\x32\x38\x32\x33\x37\x2c\x34\x39\x2e\x39\x36\x33\ +\x39\x34\x33\x20\x32\x31\x2e\x33\x30\x36\x37\x32\x31\x2c\x34\x39\ +\x2e\x38\x37\x39\x30\x31\x33\x20\x33\x34\x2e\x32\x39\x32\x34\x33\ +\x36\x2c\x33\x39\x2e\x32\x35\x30\x34\x33\x38\x20\x35\x30\x2e\x31\ +\x33\x39\x35\x31\x32\x2c\x32\x36\x2e\x33\x34\x34\x38\x38\x38\x20\ +\x4c\x20\x37\x38\x2e\x39\x35\x32\x33\x38\x31\x2c\x32\x2e\x38\x38\ +\x30\x32\x35\x33\x32\x20\x37\x39\x2e\x30\x30\x30\x37\x34\x35\x2c\ +\x32\x36\x2e\x35\x30\x33\x30\x35\x33\x20\x63\x20\x30\x2e\x30\x32\ +\x36\x36\x2c\x31\x32\x2e\x39\x39\x32\x35\x34\x31\x20\x30\x2e\x30\ +\x32\x36\x36\x2c\x33\x34\x2e\x31\x39\x36\x34\x32\x37\x20\x30\x2c\ +\x34\x37\x2e\x31\x31\x39\x37\x34\x37\x20\x6c\x20\x2d\x30\x2e\x30\ +\x34\x38\x33\x36\x2c\x32\x33\x2e\x34\x39\x36\x39\x34\x37\x20\x7a\ +\x22\x0a\x20\x20\x20\x69\x64\x3d\x22\x70\x61\x74\x68\x31\x38\x32\ +\x22\x20\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\x0a\ \x00\x00\x06\x5a\ \x3c\ \x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ @@ -23218,10 +23218,10 @@ \x07\x03\x7d\xc3\ \x00\x69\ \x00\x6d\x00\x61\x00\x67\x00\x65\x00\x73\ -\x00\x0e\ -\x0e\xde\xf7\x47\ -\x00\x6c\ -\x00\x65\x00\x66\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x09\ +\x0b\x9e\x89\x07\ +\x00\x63\ +\x00\x68\x00\x65\x00\x63\x00\x6b\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x0b\ \x00\xb0\xe2\x96\ \x00\x6c\ @@ -23230,14 +23230,14 @@ \x05\xc6\xb2\xc7\ \x00\x6d\ \x00\x69\x00\x6e\x00\x75\x00\x73\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x09\ -\x0b\x9e\x89\x07\ -\x00\x63\ -\x00\x68\x00\x65\x00\x63\x00\x6b\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x0c\ \x06\xe6\xeb\xe7\ \x00\x75\ \x00\x70\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0e\ +\x0e\xde\xf7\x47\ +\x00\x6c\ +\x00\x65\x00\x66\x00\x74\x00\x5f\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x0f\ \x02\x9f\x08\x07\ \x00\x72\ @@ -23251,13 +23251,13 @@ qt_resource_struct_v1 = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x07\x00\x00\x00\x02\ -\x00\x00\x00\x34\x00\x00\x00\x00\x00\x01\x00\x00\x06\x20\ +\x00\x00\x00\x2a\x00\x00\x00\x00\x00\x01\x00\x00\x06\x19\ \x00\x00\x00\x9e\x00\x00\x00\x00\x00\x01\x00\x05\x9c\x83\ \x00\x00\x00\xc2\x00\x00\x00\x00\x00\x01\x00\x05\xa2\xe1\ -\x00\x00\x00\x50\x00\x00\x00\x00\x00\x01\x00\x05\x8a\xb4\ -\x00\x00\x00\x80\x00\x00\x00\x00\x00\x01\x00\x05\x96\x57\ -\x00\x00\x00\x68\x00\x00\x00\x00\x00\x01\x00\x05\x90\x3e\ +\x00\x00\x00\x46\x00\x00\x00\x00\x00\x01\x00\x05\x8a\xad\ +\x00\x00\x00\x5e\x00\x00\x00\x00\x00\x01\x00\x05\x90\x37\ \x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x00\x7c\x00\x00\x00\x00\x00\x01\x00\x05\x96\x63\ " qt_resource_struct_v2 = b"\ @@ -23265,20 +23265,20 @@ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x07\x00\x00\x00\x02\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x34\x00\x00\x00\x00\x00\x01\x00\x00\x06\x20\ +\x00\x00\x00\x2a\x00\x00\x00\x00\x00\x01\x00\x00\x06\x19\ \x00\x00\x01\x84\xbb\xb5\x66\x03\ \x00\x00\x00\x9e\x00\x00\x00\x00\x00\x01\x00\x05\x9c\x83\ \x00\x00\x01\x84\xbb\xb5\x66\x03\ \x00\x00\x00\xc2\x00\x00\x00\x00\x00\x01\x00\x05\xa2\xe1\ \x00\x00\x01\x84\xbb\xb5\x66\x00\ -\x00\x00\x00\x50\x00\x00\x00\x00\x00\x01\x00\x05\x8a\xb4\ +\x00\x00\x00\x46\x00\x00\x00\x00\x00\x01\x00\x05\x8a\xad\ \x00\x00\x01\x84\xbb\xb5\x66\x03\ -\x00\x00\x00\x80\x00\x00\x00\x00\x00\x01\x00\x05\x96\x57\ +\x00\x00\x00\x5e\x00\x00\x00\x00\x00\x01\x00\x05\x90\x37\ \x00\x00\x01\x84\xbb\xb5\x66\x03\ -\x00\x00\x00\x68\x00\x00\x00\x00\x00\x01\x00\x05\x90\x3e\ -\x00\x00\x01\x84\xbb\xb5\x66\x00\ \x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ \x00\x00\x01\x84\xbb\xb5\x66\x00\ +\x00\x00\x00\x7c\x00\x00\x00\x00\x00\x01\x00\x05\x96\x63\ +\x00\x00\x01\x84\xbb\xb5\x66\x00\ " qt_version = [int(v) for v in QtCore.qVersion().split('.')] diff --git a/constructor-manager-ui/src/constructor_manager_ui/tests/test_constructor_manager_ui.py b/constructor-manager-ui/src/constructor_manager_ui/tests/test_constructor_manager_ui.py index 7d05cc66..9dfe2415 100644 --- a/constructor-manager-ui/src/constructor_manager_ui/tests/test_constructor_manager_ui.py +++ b/constructor-manager-ui/src/constructor_manager_ui/tests/test_constructor_manager_ui.py @@ -1,28 +1,5 @@ """Tests for the constructor manager UI.""" -import pytest # type: ignore -from constructor_manager_ui.data import ( - INSTALL_INFORMATION, - PACKAGE_NAME, - PACKAGES, - UPDATE_AVAILABLE_VERSION, -) -from constructor_manager_ui.main import InstallationManagerDialog - - -@pytest.fixture -def installation_manager_dlg(qtbot): - # Initialize installation manager with mock data - installation_manager_dlg = InstallationManagerDialog( - PACKAGE_NAME, - INSTALL_INFORMATION, - ) - qtbot.addWidget(installation_manager_dlg) - yield installation_manager_dlg - - -def test_installation_manager_dialog(installation_manager_dlg): - installation_manager_dlg.show() - installation_manager_dlg.set_packages(PACKAGES) - installation_manager_dlg.show_update_available_message(UPDATE_AVAILABLE_VERSION) +def test_installation_manager_dialog(qtbot): + pass diff --git a/constructor-manager-ui/src/constructor_manager_ui/widgets/__init__.py b/constructor-manager-ui/src/constructor_manager_ui/widgets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/constructor-manager-ui/src/constructor_manager_ui/widgets/dialog.py b/constructor-manager-ui/src/constructor_manager_ui/widgets/dialog.py new file mode 100644 index 00000000..f2393b17 --- /dev/null +++ b/constructor-manager-ui/src/constructor_manager_ui/widgets/dialog.py @@ -0,0 +1,465 @@ +"""Constructor manager main dialog.""" + +from typing import Optional, List +import logging + +from qtpy.QtCore import QProcess, QSize, QTimer, Qt +from qtpy.QtGui import QCloseEvent +from qtpy.QtWidgets import ( + QCheckBox, + QDialog, + QFrame, + QGridLayout, + QGroupBox, + QHBoxLayout, + QLabel, + QPushButton, + QVBoxLayout, + QWidget, + QTextEdit, + QMessageBox, +) + +from constructor_manager_api import api # type: ignore + +# To get mock data +from constructor_manager_ui.data import PackageData +from constructor_manager_ui.widgets.spinner import SpinnerWidget +from constructor_manager_ui.widgets.table import PackagesTable +from constructor_manager_ui.widgets.update import UpdateWidget + +# Packages table constants +RELATED_PACKAGES = 0 +ALL_PACKAGES = 1 + + +logger = logging.getLogger(__name__) + + +class InstallationManagerDialog(QDialog): + def __init__( + self, + package_name: str, + current_version: Optional[str] = None, + build_string: Optional[str] = None, + plugins_url: Optional[str] = None, + channels: Optional[List[str]] = None, + dev: bool = False, + log: str = "WARNING", + parent: Optional[QWidget] = None, + ): + super().__init__(parent=parent) + self.package_name = package_name + self.current_version = current_version + self.plugins_url = plugins_url + self.build_string = build_string + self.channels = channels + self.dev = dev + self.log = log + self._busy = False + self.snapshot_version = None + self.updates_widget = None + self.packages_tablewidget = None + self._worker_version = None + + # Setup + self.setWindowTitle(f"{package_name} installation manager") + self.setMinimumSize(QSize(500, 750)) + self.setup_layout() + self._refresh() + + def _create_install_information_group(self): + install_information_group = QGroupBox("Install information") + install_information_layout = QVBoxLayout() + current_version_layout = QHBoxLayout() + + # Current version labels and button + if self.current_version: + text = f"{self.package_name} v{self.current_version}" + else: + text = self.package_name + + self.current_version_label = QLabel(text) + last_modified_version_label = QLabel() + # f"Last modified {self.current_version['last_modified']}" + # ) + self.current_version_open_button = QPushButton("Open") + self.current_version_open_button.setObjectName("open_button") + self.refresh_button = QPushButton("Refresh") + current_version_layout.addWidget(self.current_version_label) + current_version_layout.addSpacing(10) + current_version_layout.addWidget(last_modified_version_label) + current_version_layout.addSpacing(10) + current_version_layout.addWidget(self.current_version_open_button) + current_version_layout.addStretch(1) + current_version_layout.addWidget(self.refresh_button) + install_information_layout.addLayout(current_version_layout) + + # Line to divide current section and update section + install_version_line = QFrame() + install_version_line.setObjectName("separator") + install_version_line.setFrameShape(QFrame.HLine) + install_version_line.setFrameShadow(QFrame.Sunken) + install_version_line.setLineWidth(1) + install_information_layout.addWidget(install_version_line) + + # Update information widget + self.updates_widget = UpdateWidget(self.package_name, parent=self) + install_information_layout.addWidget(self.updates_widget) + install_information_group.setLayout(install_information_layout) + + # Signals + # Open button signal + self.current_version_open_button.clicked.connect(self.open_installed) + + # Update widget signals + self.updates_widget.install_version.connect(self.install_version) + self.updates_widget.skip_version.connect(self.skip_version) + self.refresh_button.clicked.connect(self._refresh) + self.refresh_button.setVisible(False) + + return install_information_group + + def _create_packages_group(self): + packages_group = QGroupBox("Packages") + packages_layout = QVBoxLayout() + + packages_filter_layout = QHBoxLayout() + packages_filter_label = QLabel("Show:") + self.packages_spinner_label = SpinnerWidget("Loading packages...", parent=self) + show_detailed_view_checkbox = QCheckBox("Detailed view") + show_detailed_view_checkbox.setChecked(False) + + packages_filter_layout.addWidget(packages_filter_label) + packages_filter_layout.addWidget(show_detailed_view_checkbox) + packages_filter_layout.addStretch(1) + packages_filter_layout.addWidget(self.packages_spinner_label) + + self.packages_tablewidget = PackagesTable(None, parent=self) + packages_layout.addLayout(packages_filter_layout) + packages_layout.addWidget(self.packages_tablewidget) + packages_group.setLayout(packages_layout) + + show_detailed_view_checkbox.stateChanged.connect( + self.packages_tablewidget.change_detailed_info_visibility + ) + self.packages_tablewidget.change_detailed_info_visibility( + show_detailed_view_checkbox.checkState() + ) + + return packages_group + + def _create_installation_actions_group(self): + installation_actions_group = QGroupBox("Installation Actions") + installation_actions_layout = QGridLayout() + + self.spinner_installation_actions = SpinnerWidget("", parent=self) + self.spinner_installation_actions.setVisible(False) + installation_actions_layout.addWidget(QLabel(""), 0, 0) + installation_actions_layout.addWidget( + self.spinner_installation_actions, 0, 1, alignment=Qt.AlignRight + ) + + # Restore action + self._restore_button = QPushButton("Restore Installation") + self.restore_label = QLabel( + "Restore installation to the latest snapshot of the current version: " + # f"{self.snapshot_version['version']} " + # f"({self.snapshot_version['last_modified']})" + ) + installation_actions_layout.addWidget(self._restore_button, 1, 0) + installation_actions_layout.addWidget(self.restore_label, 1, 1) + + # Revert action + self._revert_button = QPushButton("Revert Installation") + self.revert_label = QLabel( + "Rollback installation to the latest snapshot of the previous version: " + # f"{self.snapshot_version['version']} " + # f"({self.snapshot_version['last_modified']})" + ) + installation_actions_layout.addWidget(self._revert_button, 2, 0) + installation_actions_layout.addWidget(self.revert_label, 2, 1) + + # Reset action + self._reset_button = QPushButton("Reset Installation") + self.reset_label = QLabel( + "Reset the current installation to clear " + "preferences, plugins, and other packages" + ) + installation_actions_layout.addWidget(self._reset_button, 3, 0) + installation_actions_layout.addWidget(self.reset_label, 3, 1) + + # Uninstall action + self.uninstall_button = QPushButton("Uninstall") + self.uninstall_button.setObjectName("uninstall_button") + self.uninstall_label = QLabel( + f"Remove the {self.package_name} Bundled App " + "and Installation Manager from your computer" + ) + self.uninstall_button.setVisible(False) + self.uninstall_label.setVisible(False) + # TODO: to be enabled later on + # installation_actions_layout.addWidget(self.uninstall_button, 3, 0) + # installation_actions_layout.addWidget(uninstall_label, 3, 1) + + installation_actions_group.setLayout(installation_actions_layout) + + # Signals + self._restore_button.clicked.connect(self.restore_installation) + self._revert_button.clicked.connect(self.revert_installation) + self._reset_button.clicked.connect(self.reset_installation) + self.uninstall_button.clicked.connect(self.uninstall) + + return installation_actions_group + + def _create_feedback_group(self): + feedback_group = QGroupBox("Feedback") + self._status_data = QTextEdit(self) + self._status_error = QTextEdit(self) + + self._status_data.setReadOnly(True) + self._status_error.setReadOnly(True) + + feedback_layout = QVBoxLayout() + feedback_status_layout = QHBoxLayout() + feedback_status_layout.addWidget(self._status_data) + feedback_status_layout.addWidget(self._status_error) + + feedback_layout.addLayout(feedback_status_layout) + feedback_group.setLayout(feedback_layout) + return feedback_group + + def setup_layout(self): + main_layout = QVBoxLayout(self) + + # Install information + install_information_group = self._create_install_information_group() + main_layout.addWidget(install_information_group, stretch=2) + + # Packages + packages_group = self._create_packages_group() + main_layout.addWidget(packages_group, stretch=2) + + # Installation Actions + installation_actions_group = self._create_installation_actions_group() + main_layout.addWidget(installation_actions_group, stretch=2) + + # Installation Actions + feedback_group = self._create_feedback_group() + main_layout.addWidget(feedback_group, stretch=1) + print(self.log) + feedback_group.setVisible(self.log == "DEBUG") + + # Layout + self.setLayout(main_layout) + + self.set_version_actions_enabled(False) + + def _refresh(self): + self.set_busy(True) + self.refresh_button.setVisible(False) + self.packages_spinner_label.show() + self._worker_version = api.check_version(self.package_name) + self._worker_version.finished.connect(self._update_version) + self._worker_version.start() + self.show_checking_updates_message() + + def _refresh_after_version(self): + self._worker_packages = api.check_packages( + self.package_name, + version=self.current_version, + plugins_url=self.plugins_url, + ) + self._worker_packages.finished.connect(self._update_packages) + self._worker_packages.start() + + self._worker_updates = api.check_updates( + self.package_name, + current_version=self.current_version, + build_string=self.build_string, + channels=self.channels, + dev=self.dev, + ) + self._worker_updates.finished.connect(self._update_widget) + self._worker_updates.start() + + self.show_checking_updates_message() + + def _update_version(self, result): + self._handle_finished(result) + if result.get("exit_code") == 0: + data = result["data"] + self.current_version = data.get("version", "") + + if self.current_version: + text = f"v{self.current_version}" + else: + text = "(Not installed!)" + + self.current_version_open_button.setEnabled(bool(self.current_version)) + + self.current_version_label.setText(f"{self.package_name} {text}") + self.current_version_open_button.setVisible(True) + + self._refresh_after_version() + + def _update_packages(self, result): + self._handle_finished(result) + data = result["data"] + package_data = [] + if isinstance(data, dict): + packages = data.get("packages", []) + for pkg in packages: + package_data.append( + PackageData( + pkg["name"], + pkg["version"], + pkg["source"], + pkg["build_string"], + pkg["is_plugin"], + ) + ) + + self.set_packages(package_data) + self.set_busy(False) + self.refresh_button.setVisible(True) + + def _update_widget(self, result): + self._handle_finished(result) + if result.get("exit_code") == 0: + data = result["data"] + if data.get("update"): + self.show_update_available_message(data.get("latest_version")) + else: + self.show_up_to_date_message() + + states = data.get("states", []) + if states: + print(states) + + # Qt Overrides + def closeEvent(self, event: QCloseEvent) -> None: + """Override Qt method.""" + if self._busy: + QMessageBox.warning( + self, "Busy", "Please wait until the current action finishes!" + ) + event.ignore() + return + + # while ConstructorManagerWorker._WORKERS: + # worker = ConstructorManagerWorker._WORKERS.pop() + # self._terminate_worker(worker) + + return super().closeEvent(event) + + # Helpers + def _handle_finished(self, result): + self._status_data.append("
") + self._status_error.append("
") + data = result.get("data", {}) + error = result.get("error", "") + self._status_data.append(str(data)) + self._status_error.append(str(error)) + + def _terminate_worker(self, worker): + not_running = QProcess.ProcessState.NotRunning + if worker and worker.state() != not_running: + self._worker_version.terminate() + + def handle_finished(self, result): + self._handle_finished(result) + self.spinner_installation_actions.hide() + self._refresh() + + def show_checking_updates_message(self): + self.updates_widget.show_checking_updates_message() + + def show_up_to_date_message(self): + self.updates_widget.show_up_to_date_message() + + def show_update_available_message(self, update_available_version): + self.updates_widget.show_update_available_message(update_available_version) + + def set_packages(self, packages): + self.packages = packages + if self.packages_tablewidget: + self.packages_tablewidget.set_data(self.packages) + self.packages_spinner_label.hide() + + def set_busy(self, value): + self._restore_button.setDisabled(value) + self._revert_button.setDisabled(value) + self._reset_button.setDisabled(value) + self._busy = value + + def set_version_actions_enabled(self, value): + self._restore_button.setEnabled(value) + self._revert_button.setEnabled(value) + self._reset_button.setEnabled(value) + self.current_version_open_button.setVisible(value) + + # Actions + def open_installed(self): + self._open_worker = api.open_application( + self.package_name, self.current_version + ) + self._open_worker.start() + self.current_version_open_button.setEnabled(False) + + # Disable open button for a bit + self._timer_open_button = QTimer() + self._timer_open_button.timeout.connect( + lambda: self.current_version_open_button.setEnabled(True) + ) + self._timer_open_button.setSingleShot(True) + self._timer_open_button.start(10000) + + def install_version(self, update_version): + print("Update version") + worker = api.update( + self.package_name, + self.current_version, + build_string=self.build_string, + channels=self.channels, + plugins_url=self.plugins_url, + dev=self.dev, + delayed=False, + ) + worker.finished.connect(self.handle_finished) + worker.start() + self.set_busy(True) + + def restore_installation(self): + self.spinner_installation_actions.set_text("Restoring installation...") + self.spinner_installation_actions.show() + worker = api.restore(self.package_name) + worker.finished.connect(self.handle_finished) + worker.start() + self.set_busy(True) + + def revert_installation(self): + self.spinner_installation_actions.set_text("Reverting installation...") + self.spinner_installation_actions.show() + worker = api.revert(self.package_name, self.current_version) + worker.finished.connect(self.handle_finished) + worker.start() + self.set_busy(True) + + def reset_installation(self): + self.spinner_installation_actions.set_text("Reseting installation...") + self.spinner_installation_actions.show() + self._worker = api.reset( + package_name=self.package_name, + current_version=self.current_version, + channels=self.channels, + ) + self._worker.finished.connect(self.handle_finished) + self._worker.start() + self.set_busy(True) + + def skip_version(self, skip_version): + pass + + def uninstall(self): + pass diff --git a/constructor-manager-ui/src/constructor_manager_ui/widgets/spinner.py b/constructor-manager-ui/src/constructor_manager_ui/widgets/spinner.py new file mode 100644 index 00000000..a7664d72 --- /dev/null +++ b/constructor-manager-ui/src/constructor_manager_ui/widgets/spinner.py @@ -0,0 +1,49 @@ +"""Constructor manager main interface.""" + +from typing import Optional +import logging + +from qtpy.QtCore import QSize +from qtpy.QtGui import QMovie +from qtpy.QtWidgets import QHBoxLayout, QLabel, QWidget + + +logger = logging.getLogger(__name__) + + +class SpinnerWidget(QWidget): + def __init__(self, text: str, parent: Optional[QWidget] = None): + super().__init__(parent=parent) + + # Widgets for text and loading gif + self.text_label = QLabel(text) + spinner_label = QLabel() + self.spinner_movie = QMovie(":/images/loading.gif") + self.spinner_movie.setScaledSize(QSize(18, 18)) + spinner_label.setMovie(self.spinner_movie) + + # Set layout for text + loading indicator + layout = QHBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(self.text_label) + layout.addWidget(spinner_label) + layout.addStretch(1) + self.setLayout(layout) + self.spinner_movie.start() + + def set_text(self, text: str): + self.text_label.setText(text) + + def show(self): + try: + self.spinner_movie.start() + except RuntimeError: + pass + super().show() + + def hide(self): + try: + self.spinner_movie.stop() + except RuntimeError: + pass + super().hide() diff --git a/constructor-manager-ui/src/constructor_manager_ui/widgets/table.py b/constructor-manager-ui/src/constructor_manager_ui/widgets/table.py new file mode 100644 index 00000000..01eb79be --- /dev/null +++ b/constructor-manager-ui/src/constructor_manager_ui/widgets/table.py @@ -0,0 +1,100 @@ +"""Constructor manager main interface.""" + +import logging + +from qtpy.QtCore import Qt +from qtpy.QtGui import QBrush +from qtpy.QtWidgets import ( + QAbstractItemView, + QHeaderView, + QTableWidget, + QTableWidgetItem, +) + + +# Packages table constants +RELATED_PACKAGES = 0 +ALL_PACKAGES = 1 + + +logger = logging.getLogger(__name__) + + +class PackagesTable(QTableWidget): + def __init__(self, packages, visible_packages=RELATED_PACKAGES, parent=None): + super().__init__(parent=parent) + self.packages = packages + self.visible_packages = visible_packages + self.setup() + + def _create_item(self, text: str, related_package: bool): + item = QTableWidgetItem(text) + if related_package: + background_brush = QBrush(Qt.GlobalColor.black) + else: + background_brush = QBrush(Qt.GlobalColor.darkGray) + + item.setBackground(background_brush) + if not related_package: + foreground_brush = QBrush(Qt.GlobalColor.black) + item.setForeground(foreground_brush) + + item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) # type: ignore + return item + + def setup(self): + # Set columns number and headers + self.setColumnCount(4) + self.setHorizontalHeaderLabels(["Name", "Version", "Source", "Build"]) + self.verticalHeader().setVisible(False) + + # Set horizontal headers alignment and config + self.horizontalHeader().setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter) + self.horizontalHeader().setStretchLastSection(True) + self.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) + + # Hide table items borders + self.setShowGrid(False) + + # Set table selection to row + self.setSelectionBehavior(QAbstractItemView.SelectRows) + + def set_data(self, packages): + self.clearContents() + self.setRowCount(0) + self.packages = packages + + # Populate table with data available + for name, version, source, build, related_package in self.packages: + self.insertRow(self.rowCount()) + package_row = self.rowCount() - 1 + self.setItem(package_row, 0, self._create_item(name, related_package)) + self.setItem(package_row, 1, self._create_item(version, related_package)) + self.setItem(package_row, 2, self._create_item(source, related_package)) + self.setItem(package_row, 3, self._create_item(build, related_package)) + if self.visible_packages == RELATED_PACKAGES and not related_package: + self.hideRow(package_row) + + def change_visible_packages(self, toggled_option): + if self.packages: + self.visible_packages = toggled_option + if toggled_option == RELATED_PACKAGES: + for idx, package in enumerate(self.packages): + name, version, source, build, related_package = package + if not related_package: + self.hideRow(idx) + else: + for idx, _ in enumerate(self.packages): + self.showRow(idx) + else: + self.visible_packages = toggled_option + + def change_detailed_info_visibility(self, state): + if state > Qt.Unchecked: + self.showColumn(2) + self.showColumn(3) + self.change_visible_packages(ALL_PACKAGES) + else: + self.hideColumn(2) + self.hideColumn(3) + self.change_visible_packages(RELATED_PACKAGES) diff --git a/constructor-manager-ui/src/constructor_manager_ui/widgets/update.py b/constructor-manager-ui/src/constructor_manager_ui/widgets/update.py new file mode 100644 index 00000000..8f7984f7 --- /dev/null +++ b/constructor-manager-ui/src/constructor_manager_ui/widgets/update.py @@ -0,0 +1,105 @@ +"""Constructor manager update section widgets.""" + +from typing import Optional +import logging + +from qtpy.QtCore import Signal +from qtpy.QtWidgets import ( + QHBoxLayout, + QLabel, + QPushButton, + QStackedWidget, + QVBoxLayout, + QWidget, +) + + +from constructor_manager_ui.widgets.spinner import SpinnerWidget + + +logger = logging.getLogger(__name__) + + +class UpdateWidget(QWidget): + + install_version = Signal(str) + skip_version = Signal(str) + + def __init__(self, package_name: str, parent: Optional[QWidget] = None): + super().__init__(parent=parent) + self.package_name = package_name + self.update_available_version = None + + # Setup widgets + self.checking_update_widget = SpinnerWidget( + "Checking for updates...", parent=self + ) + self.up_to_date_widget = QWidget(self) + self._initialize_up_to_date_widget() + self.update_available_widget = QWidget(self) + self._initialize_update_available_widget() + + # Stack widgets to show one at a time and set layout + update_widget_layout = QHBoxLayout() + self.update_widgets = QStackedWidget(self) + self.update_widgets.addWidget(self.checking_update_widget) + self.update_widgets.addWidget(self.up_to_date_widget) + self.update_widgets.addWidget(self.update_available_widget) + update_widget_layout.addWidget(self.update_widgets) + self.setLayout(update_widget_layout) + + # Start showing checking updates widget + self.show_checking_updates_message() + + def _initialize_up_to_date_widget(self): + up_to_date_layout = QVBoxLayout() + update_msg_label = QLabel(f"Your {self.package_name} is up to date.") + up_to_date_layout.addWidget(update_msg_label) + + self.up_to_date_widget.setLayout(up_to_date_layout) + + def _initialize_update_available_widget(self): + new_version_layout = QVBoxLayout() + update_msg_label_layout = QHBoxLayout() + update_msg_label = QLabel( + f"A newer version of {self.package_name} is available!" + ) + update_msg_label_layout.addSpacing(15) + update_msg_label_layout.addWidget(update_msg_label) + + update_actions_layout = QHBoxLayout() + new_version_label = QLabel(self.update_available_version) + self.skip_version_button = QPushButton("Skip This Version") + self.install_version_button = QPushButton("Install This Version") + self.install_version_button.setObjectName("install_button") + update_actions_layout.addSpacing(20) + update_actions_layout.addWidget(new_version_label) + update_actions_layout.addSpacing(20) + update_actions_layout.addWidget(self.skip_version_button) + update_actions_layout.addSpacing(20) + update_actions_layout.addWidget(self.install_version_button) + update_actions_layout.addStretch(1) + new_version_layout.addLayout(update_msg_label_layout) + new_version_layout.addLayout(update_actions_layout) + self.skip_version_button.setVisible(False) + + self.update_available_widget.setLayout(new_version_layout) + + # Connect buttons signals to parent class signals + self.skip_version_button.clicked.connect( + lambda checked: self.skip_version.emit(self.update_available_version) + ) + self.install_version_button.clicked.connect( + lambda checked: self.install_version.emit(self.update_available_version) + ) + + def show_checking_updates_message(self): + self.update_widgets.setCurrentWidget(self.checking_update_widget) + + def show_up_to_date_message(self): + self.update_widgets.setCurrentWidget(self.up_to_date_widget) + + def show_update_available_message(self, update_available_version): + self.update_available_version = update_available_version + if update_available_version: + self.update_widgets.setCurrentWidget(self.update_available_widget) diff --git a/constructor-manager-ui/tox.ini b/constructor-manager-ui/tox.ini index 5d20d538..7165316d 100644 --- a/constructor-manager-ui/tox.ini +++ b/constructor-manager-ui/tox.ini @@ -25,7 +25,8 @@ platform = passenv = CI GITHUB_ACTIONS - DISPLAY XAUTHORITY + DISPLAY + XAUTHORITY NUMPY_EXPERIMENTAL_ARRAY_FUNCTION PYVISTA_OFF_SCREEN extras =