Esempio n. 1
0
    def _open_existing_seed_details(self):
        json_path = prompt_user_for_seed_log(self)
        if json_path is None:
            return

        self._seed_details = SeedDetailsWindow(json_path)
        self._seed_details.show()
Esempio n. 2
0
async def test_show_dialog_for_prime3_layout(skip_qtbot, mocker,
                                             corruption_game_description):
    mock_execute_dialog = mocker.patch(
        "randovania.gui.lib.async_dialog.execute_dialog",
        new_callable=AsyncMock)
    mock_clipboard: MagicMock = mocker.patch(
        "PySide2.QtWidgets.QApplication.clipboard")

    window = SeedDetailsWindow(None, MagicMock())
    window.player_index_combo.addItem("Current", 0)
    skip_qtbot.addWidget(window)

    collections.namedtuple("MockPickup", ["name"])
    target = MagicMock()
    target.pickup.name = "Boost Ball"

    patches = corruption_game_description.create_game_patches()
    for i in range(100):
        patches.pickup_assignment[PickupIndex(i)] = target

    window.layout_description = MagicMock()
    window.layout_description.all_patches = {0: patches}

    # Run
    await window._show_dialog_for_prime3_layout()

    # Assert
    mock_execute_dialog.assert_awaited_once()
    mock_clipboard.return_value.setText.assert_called_once()
Esempio n. 3
0
def test_update_layout_description_no_spoiler(skip_qtbot, mocker):
    # Setup
    mock_describer = mocker.patch(
        "randovania.gui.lib.preset_describer.describe",
        return_value=["a", "b", "c", "d"])
    mock_merge = mocker.patch(
        "randovania.gui.lib.preset_describer.merge_categories",
        return_value="<description>")

    options = MagicMock()
    description = MagicMock()
    description.permalink.player_count = 1
    description.permalink.as_base64_str = "<permalink>"
    description.permalink.spoiler = False

    window = SeedDetailsWindow(None, options)
    skip_qtbot.addWidget(window)

    # Run
    window.update_layout_description(description)

    # Assert
    mock_describer.assert_called_once_with(
        description.permalink.get_preset.return_value)
    mock_merge.assert_has_calls([
        call(["a", "c"]),
        call(["b", "d"]),
    ])
Esempio n. 4
0
    def __init__(self, options: Options, preset_manager: PresetManager, preview: bool):
        super().__init__()
        self.setupUi(self)
        self.setWindowTitle("Randovania {}".format(VERSION))
        self.is_preview_mode = preview
        self.setAcceptDrops(True)
        common_qt_lib.set_default_window_icon(self)

        self.intro_label.setText(self.intro_label.text().format(version=VERSION))

        self._preset_manager = preset_manager

        if preview:
            debug.set_level(2)

        # Signals
        self.newer_version_signal.connect(self.display_new_version)
        self.background_tasks_button_lock_signal.connect(self.enable_buttons_with_background_tasks)
        self.progress_update_signal.connect(self.update_progress)
        self.stop_background_process_button.clicked.connect(self.stop_background_process)
        self.options_changed_signal.connect(self.on_options_changed)

        self.intro_play_now_button.clicked.connect(lambda: self.welcome_tab_widget.setCurrentWidget(self.tab_play))
        self.open_faq_button.clicked.connect(self._open_faq)
        self.open_database_viewer_button.clicked.connect(self._open_data_visualizer)

        self.import_permalink_button.clicked.connect(self._import_permalink)
        self.create_new_seed_button.clicked.connect(
            lambda: self.welcome_tab_widget.setCurrentWidget(self.tab_create_seed))

        # Menu Bar
        self.menu_action_data_visualizer.triggered.connect(self._open_data_visualizer)
        self.menu_action_item_tracker.triggered.connect(self._open_item_tracker)
        self.menu_action_edit_new_database.triggered.connect(self._open_data_editor_default)
        self.menu_action_edit_existing_database.triggered.connect(self._open_data_editor_prompt)
        self.menu_action_validate_seed_after.triggered.connect(self._on_validate_seed_change)
        self.menu_action_timeout_generation_after_a_time_limit.triggered.connect(self._on_generate_time_limit_change)

        self.generate_seed_tab = GenerateSeedTab(self, self, self, options)
        self.generate_seed_tab.setup_ui()
        self._details_window = SeedDetailsWindow(self, self, options)
        self._details_window.added_to_tab = False

        # Needs the GenerateSeedTab
        self._create_open_map_tracker_actions()

        # Setting this event only now, so all options changed trigger only once
        options.on_options_changed = self.options_changed_signal.emit
        self._options = options
        with options:
            self.on_options_changed()

        self.main_tab_widget.setCurrentIndex(0)

        # Update hints text
        self._update_hints_text()
Esempio n. 5
0
def test_update_layout_description_actual_seed(skip_qtbot, test_files_dir):
    description = LayoutDescription.from_file(
        test_files_dir.joinpath("log_files", "seed_a.rdvgame"))

    # Run
    window = SeedDetailsWindow(None, MagicMock())
    skip_qtbot.addWidget(window)
    window.update_layout_description(description)

    # Assert
    assert len(window.pickup_spoiler_buttons) == 119
    assert window.pickup_spoiler_show_all_button.text() == "Show All"
    skip_qtbot.mouseClick(window.pickup_spoiler_show_all_button,
                          QtCore.Qt.LeftButton)
    assert window.pickup_spoiler_show_all_button.text() == "Hide All"
Esempio n. 6
0
async def test_export_iso(skip_qtbot, mocker):
    # Setup
    mock_execute_dialog = mocker.patch(
        "randovania.gui.lib.async_dialog.execute_dialog",
        new_callable=AsyncMock,
        return_value=QtWidgets.QDialog.Accepted)

    options = MagicMock()
    options.output_directory = None

    window = SeedDetailsWindow(None, options)
    window.layout_description = MagicMock()
    window._player_names = {}
    window.run_in_background_async = AsyncMock()

    # Run
    await window._export_iso()

    # Assert
    mock_execute_dialog.assert_awaited_once()
    window.run_in_background_async.assert_awaited_once()
async def test_export_iso(skip_qtbot, mocker):
    # Setup
    mock_input_dialog = mocker.patch(
        "randovania.gui.seed_details_window.GameInputDialog")
    mock_execute_dialog = mocker.patch(
        "randovania.gui.lib.async_dialog.execute_dialog",
        new_callable=AsyncMock,
        return_value=QtWidgets.QDialog.Accepted)

    options = MagicMock()
    options.output_directory = None

    window_manager = MagicMock()
    patcher_provider = window_manager.patcher_provider
    patcher = patcher_provider.patcher_for_game.return_value
    patcher.is_busy = False

    window = SeedDetailsWindow(window_manager, options)
    window.layout_description = MagicMock()
    window._player_names = {}

    players_config = PlayersConfiguration(
        player_index=window.current_player_index,
        player_names=window._player_names,
    )

    # Run
    await window._export_iso()

    # Assert
    mock_execute_dialog.assert_awaited_once()
    patcher.create_patch_data.assert_called_once_with(
        window.layout_description, players_config, options.cosmetic_patches)
    patcher.patch_game.assert_called_once_with(
        mock_input_dialog.return_value.input_file,
        mock_input_dialog.return_value.output_file,
        patcher.create_patch_data.return_value,
        window._options.game_files_path,
        progress_update=ANY,
    )
Esempio n. 8
0
def show_game_details(app: QApplication, options, game: Path):
    from randovania.layout.layout_description import LayoutDescription
    from randovania.gui.seed_details_window import SeedDetailsWindow

    layout = LayoutDescription.from_file(game)
    details_window = SeedDetailsWindow(None, options)
    details_window.update_layout_description(layout)
    details_window.show()
    app.details_window = details_window
Esempio n. 9
0
 def _open_game_details(self, layout: LayoutDescription):
     from randovania.gui.seed_details_window import SeedDetailsWindow
     details_window = SeedDetailsWindow(self, self._options)
     details_window.update_layout_description(layout)
     details_window.show()
     self.track_window(details_window)
Esempio n. 10
0
class MainWindow(QMainWindow, Ui_MainWindow, TabService, BackgroundTaskMixin):
    newer_version_signal = Signal(str, str)
    options_changed_signal = Signal()
    is_preview_mode: bool = False

    menu_new_version: Optional[QAction] = None
    _current_version_url: Optional[str] = None
    _options: Options
    _data_visualizer: Optional[DataEditorWindow] = None

    @property
    def _tab_widget(self):
        return self.tabWidget

    def __init__(self, options: Options, preview: bool):
        super().__init__()
        self.setupUi(self)
        self.setWindowTitle("Randovania {}".format(VERSION))
        self.is_preview_mode = preview
        self.setAcceptDrops(True)
        set_default_window_icon(self)

        self.intro_label.setText(
            self.intro_label.text().format(version=VERSION))

        if preview:
            debug.set_level(2)

        # Signals
        self.newer_version_signal.connect(self.display_new_version)
        self.background_tasks_button_lock_signal.connect(
            self.enable_buttons_with_background_tasks)
        self.progress_update_signal.connect(self.update_progress)
        self.stop_background_process_button.clicked.connect(
            self.stop_background_process)
        self.options_changed_signal.connect(self.on_options_changed)

        # Menu Bar
        self.menu_action_data_visualizer.triggered.connect(
            self._open_data_visualizer)
        self.menu_action_existing_seed_details.triggered.connect(
            self._open_existing_seed_details)
        self.menu_action_tracker.triggered.connect(self._open_tracker)
        self.menu_action_edit_new_database.triggered.connect(
            self._open_data_editor_default)
        self.menu_action_edit_existing_database.triggered.connect(
            self._open_data_editor_prompt)
        self.menu_action_export_iso.triggered.connect(self._export_iso)
        self.menu_action_validate_seed_after.triggered.connect(
            self._on_validate_seed_change)
        self.menu_action_timeout_generation_after_a_time_limit.triggered.connect(
            self._on_generate_time_limit_change)

        self.menu_action_export_iso.setEnabled(False)

        _translate = QtCore.QCoreApplication.translate
        self.tabs = []

        from randovania.gui.game_patches_window import GamePatchesWindow
        from randovania.gui.iso_management_window import ISOManagementWindow
        from randovania.gui.logic_settings_window import LogicSettingsWindow
        from randovania.gui.cosmetic_window import CosmeticWindow
        from randovania.gui.main_rules import MainRulesWindow

        self.tab_windows = [
            (ISOManagementWindow, "ROM Settings"),
            (GamePatchesWindow, "Game Patches"),
            (MainRulesWindow, "Main Rules"),
            (LogicSettingsWindow, "Logic Settings"),
            (CosmeticWindow, "Cosmetic"),
        ]

        for i, tab in enumerate(self.tab_windows):
            self.windows.append(tab[0](self, self, options))
            self.tabs.append(self.windows[i].centralWidget)
            self.tabWidget.insertTab(i + 1, self.tabs[i],
                                     _translate("MainWindow", tab[1]))

        # Setting this event only now, so all options changed trigger only once
        options.on_options_changed = self.options_changed_signal.emit
        self._options = options
        with options:
            self.on_options_changed()

        self.tabWidget.setCurrentIndex(0)

        # Update hints text
        self._update_hints_text()

    def closeEvent(self, event):
        self.stop_background_process()
        for window in self.windows:
            window.closeEvent(event)
        super().closeEvent(event)

    def dragEnterEvent(self, event):
        for url in event.mimeData().urls():
            if os.path.splitext(url.toLocalFile())[1] == ".iso":
                event.acceptProposedAction()
                return

    def dropEvent(self, event):
        from randovania.gui.iso_management_window import ISOManagementWindow

        for url in event.mimeData().urls():
            iso_path = url.toLocalFile()
            if os.path.splitext(iso_path)[1] == ".iso":
                self.get_tab(ISOManagementWindow).load_game(Path(iso_path))
                return

    # Releases info
    def request_new_data(self):
        asyncio.get_event_loop().create_task(
            github_releases_data.get_releases()).add_done_callback(
                self._on_releases_data)

    def _on_releases_data(self, task: asyncio.Task):
        releases = task.result()
        current_version = update_checker.strict_current_version()
        last_changelog = self._options.last_changelog_displayed

        all_change_logs, new_change_logs, version_to_display = update_checker.versions_to_display_for_releases(
            current_version, last_changelog, releases)

        if version_to_display is not None:
            self.display_new_version(version_to_display)

        if all_change_logs:
            changelog_tab = QtWidgets.QWidget()
            changelog_tab.setObjectName("changelog_tab")
            changelog_tab_layout = QtWidgets.QVBoxLayout(changelog_tab)
            changelog_tab_layout.setObjectName("changelog_tab_layout")
            changelog_scroll_area = QtWidgets.QScrollArea(changelog_tab)
            changelog_scroll_area.setWidgetResizable(True)
            changelog_scroll_area.setObjectName("changelog_scroll_area")
            changelog_scroll_contents = QtWidgets.QWidget()
            changelog_scroll_contents.setGeometry(QtCore.QRect(0, 0, 489, 337))
            changelog_scroll_contents.setObjectName(
                "changelog_scroll_contents")
            changelog_scroll_layout = QtWidgets.QVBoxLayout(
                changelog_scroll_contents)
            changelog_scroll_layout.setObjectName("changelog_scroll_layout")
            changelog_label = QtWidgets.QLabel(changelog_scroll_contents)
            changelog_label.setObjectName("changelog_label")
            changelog_label.setText(
                markdown.markdown("\n".join(all_change_logs)))
            changelog_scroll_layout.addWidget(changelog_label)
            changelog_scroll_area.setWidget(changelog_scroll_contents)
            changelog_tab_layout.addWidget(changelog_scroll_area)
            self.welcome_tab_widget.addTab(changelog_tab, "Change Log")

        if new_change_logs:
            QMessageBox.information(
                self, "What's new",
                markdown.markdown("\n".join(new_change_logs)))
            with self._options as options:
                options.last_changelog_displayed = current_version

    def display_new_version(self, version: update_checker.VersionDescription):
        if self.menu_new_version is None:
            self.menu_new_version = QAction("", self)
            self.menu_new_version.triggered.connect(self.open_version_link)
            self.menu_bar.addAction(self.menu_new_version)

        self.menu_new_version.setText("New version available: {}".format(
            version.tag_name))
        self._current_version_url = version.html_url

    def open_version_link(self):
        if self._current_version_url is None:
            raise RuntimeError(
                "Called open_version_link, but _current_version_url is None")

        QDesktopServices.openUrl(QUrl(self._current_version_url))

    # Options
    def on_options_changed(self):
        for window in self.windows:
            window.on_options_changed(self._options)

        self.menu_action_validate_seed_after.setChecked(
            self._options.advanced_validate_seed_after)
        self.menu_action_timeout_generation_after_a_time_limit.setChecked(
            self._options.advanced_timeout_during_generation)

    # Menu Actions
    def _open_data_visualizer(self):
        self.open_data_visualizer_at(None, None)

    def open_data_visualizer_at(
        self,
        world_name: Optional[str],
        area_name: Optional[str],
    ):
        self._data_visualizer = DataEditorWindow(
            default_data.decode_default_prime2(), False)

        if world_name is not None:
            self._data_visualizer.focus_on_world(world_name)

        if area_name is not None:
            self._data_visualizer.focus_on_area(area_name)

        self._data_visualizer.show()

    def _open_data_editor_default(self):
        self._data_editor = DataEditorWindow(
            default_data.decode_default_prime2(), True)
        self._data_editor.show()

    def _open_data_editor_prompt(self):
        database_path = prompt_user_for_database_file(self)
        if database_path is None:
            return

        with database_path.open("r") as database_file:
            self._data_editor = DataEditorWindow(json.load(database_file),
                                                 True)
            self._data_editor.show()

    def _open_existing_seed_details(self):
        json_path = prompt_user_for_seed_log(self)
        if json_path is None:
            return

        self._seed_details = SeedDetailsWindow(json_path)
        self._seed_details.show()

    def _open_tracker(self):
        try:
            self._tracker = TrackerWindow(self._options.tracker_files_path,
                                          self._options.layout_configuration)
        except InvalidLayoutForTracker as e:
            QMessageBox.critical(self, "Unsupported configuration for Tracker",
                                 str(e))
            return

        self._tracker.show()

    def _export_iso(self):
        pass

    def _on_validate_seed_change(self):
        old_value = self._options.advanced_validate_seed_after
        new_value = self.menu_action_validate_seed_after.isChecked()

        if old_value and not new_value:
            box = QMessageBox(self)
            box.setWindowTitle("Disable validation?")
            box.setText(_DISABLE_VALIDATION_WARNING)
            box.setIcon(QMessageBox.Warning)
            box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
            box.setDefaultButton(QMessageBox.No)
            user_response = box.exec_()
            if user_response != QMessageBox.Yes:
                self.menu_action_validate_seed_after.setChecked(True)
                return

        with self._options as options:
            options.advanced_validate_seed_after = new_value

    def _on_generate_time_limit_change(self):
        is_checked = self.menu_action_timeout_generation_after_a_time_limit.isChecked(
        )
        with self._options as options:
            options.advanced_timeout_during_generation = is_checked

    def _update_hints_text(self):
        game_description = default_database.default_prime2_game_description()

        number_for_hint_type = {
            hint_type: i + 1
            for i, hint_type in enumerate(LoreType)
        }
        used_hint_types = set()

        self.hint_tree_widget.setSortingEnabled(False)

        # TODO: This ignores the Dark World names. But there's currently no logbook nodes in Dark World.
        for world in game_description.world_list.worlds:

            world_item = QtWidgets.QTreeWidgetItem(self.hint_tree_widget)
            world_item.setText(0, world.name)
            world_item.setExpanded(True)

            for area in world.areas:
                hint_types = {}

                for node in area.nodes:
                    if isinstance(node, LogbookNode):
                        if node.required_translator is not None:
                            hint_types[
                                node.
                                lore_type] = node.required_translator.short_name
                        else:
                            hint_types[node.lore_type] = "✓"

                if hint_types:
                    area_item = QtWidgets.QTreeWidgetItem(world_item)
                    area_item.setText(0, area.name)

                    for hint_type, text in hint_types.items():
                        area_item.setText(number_for_hint_type[hint_type],
                                          text)
                        used_hint_types.add(hint_type)

        self.hint_tree_widget.resizeColumnToContents(0)
        self.hint_tree_widget.setSortingEnabled(True)
        self.hint_tree_widget.sortByColumn(0, QtCore.Qt.AscendingOrder)

        for hint_type in used_hint_types:
            self.hint_tree_widget.headerItem().setText(
                number_for_hint_type[hint_type], hint_type.long_name)

    # Background Process

    def enable_buttons_with_background_tasks(self, value: bool):
        self.stop_background_process_button.setEnabled(not value)

    def update_progress(self, message: str, percentage: int):
        self.progress_label.setText(message)
        if "Aborted" in message:
            percentage = 0
        if percentage >= 0:
            self.progress_bar.setRange(0, 100)
            self.progress_bar.setValue(percentage)
        else:
            self.progress_bar.setRange(0, 0)
Esempio n. 11
0
class MainWindow(QMainWindow, Ui_MainWindow, WindowManager,
                 BackgroundTaskMixin):
    newer_version_signal = Signal(str, str)
    options_changed_signal = Signal()
    _is_preview_mode: bool = False

    menu_new_version: Optional[QAction] = None
    _current_version_url: Optional[str] = None
    _options: Options
    _data_visualizer: Optional[DataEditorWindow] = None
    _details_window: SeedDetailsWindow
    _map_tracker: TrackerWindow
    _preset_manager: PresetManager

    @property
    def _tab_widget(self):
        return self.main_tab_widget

    @property
    def preset_manager(self) -> PresetManager:
        return self._preset_manager

    @property
    def main_window(self) -> QMainWindow:
        return self

    @property
    def is_preview_mode(self) -> bool:
        return self._is_preview_mode

    def __init__(self, options: Options, preset_manager: PresetManager,
                 preview: bool):
        super().__init__()
        self.setupUi(self)
        self.setWindowTitle("Randovania {}".format(VERSION))
        self._is_preview_mode = preview
        self.setAcceptDrops(True)
        common_qt_lib.set_default_window_icon(self)

        self.intro_label.setText(
            self.intro_label.text().format(version=VERSION))

        self._preset_manager = preset_manager

        if preview:
            debug.set_level(2)

        # Signals
        self.newer_version_signal.connect(self.display_new_version)
        self.background_tasks_button_lock_signal.connect(
            self.enable_buttons_with_background_tasks)
        self.progress_update_signal.connect(self.update_progress)
        self.stop_background_process_button.clicked.connect(
            self.stop_background_process)
        self.options_changed_signal.connect(self.on_options_changed)

        self.intro_play_now_button.clicked.connect(
            lambda: self.welcome_tab_widget.setCurrentWidget(self.tab_play))
        self.open_faq_button.clicked.connect(self._open_faq)
        self.open_database_viewer_button.clicked.connect(
            self._open_data_visualizer)

        self.import_permalink_button.clicked.connect(self._import_permalink)
        self.import_game_file_button.clicked.connect(self._import_spoiler_log)
        self.create_new_seed_button.clicked.connect(
            lambda: self.welcome_tab_widget.setCurrentWidget(self.
                                                             tab_create_seed))

        # Menu Bar
        self.menu_action_data_visualizer.triggered.connect(
            self._open_data_visualizer)
        self.menu_action_item_tracker.triggered.connect(
            self._open_item_tracker)
        self.menu_action_edit_new_database.triggered.connect(
            self._open_data_editor_default)
        self.menu_action_edit_existing_database.triggered.connect(
            self._open_data_editor_prompt)
        self.menu_action_validate_seed_after.triggered.connect(
            self._on_validate_seed_change)
        self.menu_action_timeout_generation_after_a_time_limit.triggered.connect(
            self._on_generate_time_limit_change)

        self.generate_seed_tab = GenerateSeedTab(self, self, self, options)
        self.generate_seed_tab.setup_ui()
        self._details_window = SeedDetailsWindow(self, self, options)
        self._details_window.added_to_tab = False

        # Needs the GenerateSeedTab
        self._create_open_map_tracker_actions()
        self._setup_difficulties_menu()

        # Setting this event only now, so all options changed trigger only once
        options.on_options_changed = self.options_changed_signal.emit
        self._options = options
        with options:
            self.on_options_changed()

        self.main_tab_widget.setCurrentIndex(0)

        # Update hints text
        self._update_hints_text()

    def closeEvent(self, event):
        self.stop_background_process()
        super().closeEvent(event)

    # Generate Seed
    def _open_faq(self):
        self.main_tab_widget.setCurrentWidget(self.help_tab)
        self.help_tab_widget.setCurrentWidget(self.tab_faq)

    def _import_permalink(self):
        dialog = PermalinkDialog()
        result = dialog.exec_()
        if result == QDialog.Accepted:
            permalink = dialog.get_permalink_from_field()
            self.generate_seed_tab.generate_seed_from_permalink(permalink)

    def _import_spoiler_log(self):
        json_path = common_qt_lib.prompt_user_for_input_game_log(self)
        if json_path is not None:
            layout = LayoutDescription.from_file(json_path)
            self.show_seed_tab(layout)

    def show_seed_tab(self, layout: LayoutDescription):
        self._details_window.update_layout_description(layout)
        if not self._details_window.added_to_tab:
            self.welcome_tab_widget.addTab(self._details_window.centralWidget,
                                           "Game Details")
            self._details_window.added_to_tab = True
        self.welcome_tab_widget.setCurrentWidget(
            self._details_window.centralWidget)

    # Releases info
    def request_new_data(self):
        asyncio.get_event_loop().create_task(
            github_releases_data.get_releases()).add_done_callback(
                self._on_releases_data)

    def _on_releases_data(self, task: asyncio.Task):
        releases = task.result()
        current_version = update_checker.strict_current_version()
        last_changelog = self._options.last_changelog_displayed

        all_change_logs, new_change_logs, version_to_display = update_checker.versions_to_display_for_releases(
            current_version, last_changelog, releases)

        if version_to_display is not None:
            self.display_new_version(version_to_display)

        if all_change_logs:
            changelog_tab = QtWidgets.QWidget()
            changelog_tab.setObjectName("changelog_tab")
            changelog_tab_layout = QtWidgets.QVBoxLayout(changelog_tab)
            changelog_tab_layout.setContentsMargins(0, 0, 0, 0)
            changelog_tab_layout.setObjectName("changelog_tab_layout")
            changelog_scroll_area = QtWidgets.QScrollArea(changelog_tab)
            changelog_scroll_area.setWidgetResizable(True)
            changelog_scroll_area.setObjectName("changelog_scroll_area")
            changelog_scroll_contents = QtWidgets.QWidget()
            changelog_scroll_contents.setGeometry(QtCore.QRect(0, 0, 489, 337))
            changelog_scroll_contents.setObjectName(
                "changelog_scroll_contents")
            changelog_scroll_layout = QtWidgets.QVBoxLayout(
                changelog_scroll_contents)
            changelog_scroll_layout.setObjectName("changelog_scroll_layout")
            changelog_label = QtWidgets.QLabel(changelog_scroll_contents)
            changelog_label.setObjectName("changelog_label")
            changelog_label.setText(
                markdown.markdown("\n".join(all_change_logs)))
            changelog_label.setWordWrap(True)
            changelog_scroll_layout.addWidget(changelog_label)
            changelog_scroll_area.setWidget(changelog_scroll_contents)
            changelog_tab_layout.addWidget(changelog_scroll_area)
            self.help_tab_widget.addTab(changelog_tab, "Change Log")

        if new_change_logs:
            QMessageBox.information(
                self, "What's new",
                markdown.markdown("\n".join(new_change_logs)))
            with self._options as options:
                options.last_changelog_displayed = current_version

    def display_new_version(self, version: update_checker.VersionDescription):
        if self.menu_new_version is None:
            self.menu_new_version = QAction("", self)
            self.menu_new_version.triggered.connect(self.open_version_link)
            self.menu_bar.addAction(self.menu_new_version)

        self.menu_new_version.setText("New version available: {}".format(
            version.tag_name))
        self._current_version_url = version.html_url

    def open_version_link(self):
        if self._current_version_url is None:
            raise RuntimeError(
                "Called open_version_link, but _current_version_url is None")

        QDesktopServices.openUrl(QUrl(self._current_version_url))

    # Options
    def on_options_changed(self):
        self.menu_action_validate_seed_after.setChecked(
            self._options.advanced_validate_seed_after)
        self.menu_action_timeout_generation_after_a_time_limit.setChecked(
            self._options.advanced_timeout_during_generation)

        self.generate_seed_tab.on_options_changed(self._options)

    # Menu Actions
    def _open_data_visualizer(self):
        self.open_data_visualizer_at(None, None)

    def open_data_visualizer_at(
        self,
        world_name: Optional[str],
        area_name: Optional[str],
    ):
        self._data_visualizer = DataEditorWindow(
            default_data.decode_default_prime2(), False)

        if world_name is not None:
            self._data_visualizer.focus_on_world(world_name)

        if area_name is not None:
            self._data_visualizer.focus_on_area(area_name)

        self._data_visualizer.show()

    def _open_data_editor_default(self):
        self._data_editor = DataEditorWindow(
            default_data.decode_default_prime2(), True)
        self._data_editor.show()

    def _open_data_editor_prompt(self):
        database_path = common_qt_lib.prompt_user_for_database_file(self)
        if database_path is None:
            return

        with database_path.open("r") as database_file:
            self._data_editor = DataEditorWindow(json.load(database_file),
                                                 True)
            self._data_editor.show()

    def _create_open_map_tracker_actions(self):
        base_layout = self.preset_manager.default_preset.layout_configuration

        for trick_level in LayoutTrickLevel:
            if trick_level != LayoutTrickLevel.MINIMAL_RESTRICTIONS:
                action = QtWidgets.QAction(self)
                action.setText(trick_level.long_name)
                self.menu_map_tracker.addAction(action)

                configuration = dataclasses.replace(
                    base_layout,
                    trick_level_configuration=TrickLevelConfiguration(
                        trick_level, {}))
                action.triggered.connect(
                    partial(self.open_map_tracker, configuration))

    def open_map_tracker(self, configuration: LayoutConfiguration):
        try:
            self._map_tracker = TrackerWindow(self._options.tracker_files_path,
                                              configuration)
        except InvalidLayoutForTracker as e:
            QMessageBox.critical(self, "Unsupported configuration for Tracker",
                                 str(e))
            return

        self._map_tracker.show()

    def _open_item_tracker(self):
        # Importing this at root level seems to crash linux tests :(
        from PySide2.QtWebEngineWidgets import QWebEngineView

        tracker_window = QMainWindow()
        tracker_window.setWindowTitle("Item Tracker")
        tracker_window.resize(370, 380)

        web_view = QWebEngineView(tracker_window)
        tracker_window.setCentralWidget(web_view)

        self.web_view = web_view

        def update_window_icon():
            tracker_window.setWindowIcon(web_view.icon())

        web_view.iconChanged.connect(update_window_icon)
        web_view.load(
            QUrl("https://spaghettitoastbook.github.io/echoes/tracker/"))

        tracker_window.show()
        self._item_tracker_window = tracker_window

    # Difficulties stuff

    def _exec_trick_details(self, popup: TrickDetailsPopup):
        self._trick_details_popup = popup
        self._trick_details_popup.setWindowModality(Qt.WindowModal)
        self._trick_details_popup.open()

    def _open_trick_details_popup(self, trick: SimpleResourceInfo,
                                  level: LayoutTrickLevel):
        self._exec_trick_details(
            TrickDetailsPopup(
                self,
                self,
                default_database.default_prime2_game_description(),
                trick,
                level,
            ))

    def _open_difficulty_details_popup(self, difficulty: LayoutTrickLevel):
        self._exec_trick_details(
            TrickDetailsPopup(
                self,
                self,
                default_database.default_prime2_game_description(),
                None,
                difficulty,
            ))

    def _setup_difficulties_menu(self):
        game = default_database.default_prime2_game_description()
        for i, trick_level in enumerate(LayoutTrickLevel):
            if trick_level not in {
                    LayoutTrickLevel.NO_TRICKS,
                    LayoutTrickLevel.MINIMAL_RESTRICTIONS
            }:
                difficulty_action = QAction(self)
                difficulty_action.setText(trick_level.long_name)
                self.menu_difficulties.addAction(difficulty_action)
                difficulty_action.triggered.connect(
                    functools.partial(self._open_difficulty_details_popup,
                                      trick_level))

        configurable_tricks = TrickLevelConfiguration.all_possible_tricks()
        tricks_in_use = used_tricks(game.world_list)

        for trick in sorted(game.resource_database.trick,
                            key=lambda _trick: _trick.long_name):
            if trick.index not in configurable_tricks or trick not in tricks_in_use:
                continue

            trick_menu = QMenu(self)
            trick_menu.setTitle(trick.long_name)
            self.menu_trick_details.addAction(trick_menu.menuAction())

            used_difficulties = difficulties_for_trick(game.world_list, trick)
            for i, trick_level in enumerate(LayoutTrickLevel):
                if trick_level in used_difficulties:
                    difficulty_action = QAction(self)
                    difficulty_action.setText(trick_level.long_name)
                    trick_menu.addAction(difficulty_action)
                    difficulty_action.triggered.connect(
                        functools.partial(self._open_trick_details_popup,
                                          trick, trick_level))

    # ==========

    def _on_validate_seed_change(self):
        old_value = self._options.advanced_validate_seed_after
        new_value = self.menu_action_validate_seed_after.isChecked()

        if old_value and not new_value:
            box = QMessageBox(self)
            box.setWindowTitle("Disable validation?")
            box.setText(_DISABLE_VALIDATION_WARNING)
            box.setIcon(QMessageBox.Warning)
            box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
            box.setDefaultButton(QMessageBox.No)
            user_response = box.exec_()
            if user_response != QMessageBox.Yes:
                self.menu_action_validate_seed_after.setChecked(True)
                return

        with self._options as options:
            options.advanced_validate_seed_after = new_value

    def _on_generate_time_limit_change(self):
        is_checked = self.menu_action_timeout_generation_after_a_time_limit.isChecked(
        )
        with self._options as options:
            options.advanced_timeout_during_generation = is_checked

    def _update_hints_text(self):
        game_description = default_database.default_prime2_game_description()

        number_for_hint_type = {
            hint_type: i + 1
            for i, hint_type in enumerate(LoreType)
        }
        used_hint_types = set()

        self.hint_tree_widget.setSortingEnabled(False)

        # TODO: This ignores the Dark World names. But there's currently no logbook nodes in Dark World.
        for world in game_description.world_list.worlds:

            world_item = QtWidgets.QTreeWidgetItem(self.hint_tree_widget)
            world_item.setText(0, world.name)
            world_item.setExpanded(True)

            for area in world.areas:
                hint_types = {}

                for node in area.nodes:
                    if isinstance(node, LogbookNode):
                        if node.required_translator is not None:
                            hint_types[
                                node.
                                lore_type] = node.required_translator.short_name
                        else:
                            hint_types[node.lore_type] = "✓"

                if hint_types:
                    area_item = QtWidgets.QTreeWidgetItem(world_item)
                    area_item.setText(0, area.name)

                    for hint_type, text in hint_types.items():
                        area_item.setText(number_for_hint_type[hint_type],
                                          text)
                        used_hint_types.add(hint_type)

        self.hint_tree_widget.resizeColumnToContents(0)
        self.hint_tree_widget.setSortingEnabled(True)
        self.hint_tree_widget.sortByColumn(0, QtCore.Qt.AscendingOrder)

        for hint_type in used_hint_types:
            self.hint_tree_widget.headerItem().setText(
                number_for_hint_type[hint_type], hint_type.long_name)

    # Background Process

    def enable_buttons_with_background_tasks(self, value: bool):
        self.stop_background_process_button.setEnabled(not value)

    def update_progress(self, message: str, percentage: int):
        self.progress_label.setText(message)
        if "Aborted" in message:
            percentage = 0
        if percentage >= 0:
            self.progress_bar.setRange(0, 100)
            self.progress_bar.setValue(percentage)
        else:
            self.progress_bar.setRange(0, 0)
Esempio n. 12
0
class MainWindow(QMainWindow, Ui_MainWindow, TabService, BackgroundTaskMixin):
    newer_version_signal = Signal(str, str)
    options_changed_signal = Signal()
    is_preview_mode: bool = False

    menu_new_version: Optional[QAction] = None
    _current_version_url: Optional[str] = None
    _options: Options

    @property
    def _tab_widget(self):
        return self.tabWidget

    def __init__(self, options: Options, preview: bool):
        super().__init__()
        self.setupUi(self)
        self.setWindowTitle("Randovania {}".format(VERSION))
        self.is_preview_mode = preview
        self.setAcceptDrops(True)
        set_default_window_icon(self)

        if preview:
            debug._DEBUG_LEVEL = 2

        # Signals
        self.newer_version_signal.connect(self.display_new_version)
        self.background_tasks_button_lock_signal.connect(
            self.enable_buttons_with_background_tasks)
        self.progress_update_signal.connect(self.update_progress)
        self.stop_background_process_button.clicked.connect(
            self.stop_background_process)
        self.options_changed_signal.connect(self.on_options_changed)

        # Menu Bar
        self.menu_action_data_visualizer.triggered.connect(
            self._open_data_visualizer)
        self.menu_action_existing_seed_details.triggered.connect(
            self._open_existing_seed_details)
        self.menu_action_tracker.triggered.connect(self._open_tracker)
        self.menu_action_edit_new_database.triggered.connect(
            self._open_data_editor_default)
        self.menu_action_edit_existing_database.triggered.connect(
            self._open_data_editor_prompt)

        _translate = QtCore.QCoreApplication.translate
        self.tabs = []

        self.tab_windows = [
            (ISOManagementWindow, "ROM Settings"),
            (LogicSettingsWindow, "Logic Settings"),
            (ItemQuantitiesWindow, "Item Quantities"),
        ]

        for i, tab in enumerate(self.tab_windows):
            self.windows.append(tab[0](self, self, options))
            self.tabs.append(self.windows[i].centralWidget)
            self.tabWidget.insertTab(i, self.tabs[i],
                                     _translate("MainWindow", tab[1]))

        # Setting this event only now, so all options changed trigger only once
        options.on_options_changed = self.options_changed_signal.emit
        self._options = options
        self.on_options_changed()

        self.tabWidget.setCurrentIndex(0)
        get_latest_version(self.newer_version_signal.emit)

    def closeEvent(self, event):
        self.stop_background_process()
        for window in self.windows:
            window.closeEvent(event)
        super().closeEvent(event)

    def dragEnterEvent(self, event):
        for url in event.mimeData().urls():
            if os.path.splitext(url.toLocalFile())[1] == ".iso":
                event.acceptProposedAction()
                return

    def dropEvent(self, event):
        for url in event.mimeData().urls():
            iso_path = url.toLocalFile()
            if os.path.splitext(iso_path)[1] == ".iso":
                self.get_tab(ISOManagementWindow).load_game(Path(iso_path))
                return

    def display_new_version(self, new_version: str, new_version_url: str):
        if self.menu_new_version is None:
            self.menu_new_version = QAction("", self)
            self.menu_new_version.triggered.connect(self.open_version_link)
            self.menu_bar.addAction(self.menu_new_version)

        self.menu_new_version.setText(
            "New version available: {}".format(new_version))
        self._current_version_url = new_version_url

    def open_version_link(self):
        if self._current_version_url is None:
            raise RuntimeError(
                "Called open_version_link, but _current_version_url is None")

        QDesktopServices.openUrl(QUrl(self._current_version_url))

    # Options
    def on_options_changed(self):
        for window in self.windows:
            window.on_options_changed()

    # Menu Actions
    def _open_data_visualizer(self):
        self._data_visualizer = DataEditorWindow(
            default_data.decode_default_prime2(), False)
        self._data_visualizer.show()

    def _open_data_editor_default(self):
        self._data_editor = DataEditorWindow(
            default_data.decode_default_prime2(), True)
        self._data_editor.show()

    def _open_data_editor_prompt(self):
        database_path = prompt_user_for_database_file(self)
        if database_path is None:
            return

        with database_path.open("r") as database_file:
            self._data_editor = DataEditorWindow(json.load(database_file),
                                                 True)
            self._data_editor.show()

    def _open_existing_seed_details(self):
        json_path = prompt_user_for_seed_log(self)
        if json_path is None:
            return

        self._seed_details = SeedDetailsWindow(json_path)
        self._seed_details.show()

    def _open_tracker(self):
        self._tracker = TrackerWindow(self._options.layout_configuration)
        self._tracker.show()

    # Background Process

    def enable_buttons_with_background_tasks(self, value: bool):
        self.stop_background_process_button.setEnabled(not value)

    def update_progress(self, message: str, percentage: int):
        self.progress_label.setText(message)
        if "Aborted" in message:
            percentage = 0
        if percentage >= 0:
            self.progress_bar.setRange(0, 100)
            self.progress_bar.setValue(percentage)
        else:
            self.progress_bar.setRange(0, 0)