def test_open_edit_connection( mock_apply_edit_connections, mock_connections_editor, accept: bool, echoes_game_data, skip_qtbot, ): # Setup window = DataEditorWindow(echoes_game_data, True) skip_qtbot.addWidget(window) editor = mock_connections_editor.return_value editor.exec_.return_value = QDialog.Accepted if accept else QDialog.Rejected # Run window._open_edit_connection() # Assert mock_connections_editor.assert_called_once_with(window, window.resource_database, ANY) if accept: mock_apply_edit_connections.assert_called_once_with( window, window.current_node, window.current_connection_node, editor.final_requirement) else: mock_apply_edit_connections.assert_not_called()
def test_save_database_integrity_failure(tmp_path, echoes_game_data, skip_qtbot, mocker): # Setup mock_find_database_errors = mocker.patch( "randovania.game_description.integrity_check.find_database_errors", return_value=["DB Errors", "Unknown"]) mock_write_human_readable_game = mocker.patch( "randovania.game_description.pretty_print.write_human_readable_game") mock_create_new = mocker.patch( "randovania.gui.lib.scroll_message_box.ScrollMessageBox.create_new") mock_create_new.return_value.exec_.return_value = QtWidgets.QMessageBox.No tmp_path.joinpath("test-game", "game").mkdir(parents=True) tmp_path.joinpath("human-readable").mkdir() db_path = Path(tmp_path.joinpath("test-game", "game")) window = DataEditorWindow(echoes_game_data, db_path, True, True) skip_qtbot.addWidget(window) # Run window._save_as_internal_database() # Assert mock_find_database_errors.assert_called_once_with(window.game_description) mock_write_human_readable_game.assert_not_called() mock_create_new.assert_called_once_with( window, QtWidgets.QMessageBox.Icon.Critical, "Integrity Check", "Database has the following errors:\n\nDB Errors\n\nUnknown\n\nIgnore?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No)
async def test_open_edit_connection( mock_connections_editor: MagicMock, accept: bool, echoes_game_data, skip_qtbot, mocker, ): # Setup execute_dialog = mocker.patch( "randovania.gui.lib.async_dialog.execute_dialog", new_callable=AsyncMock) window = DataEditorWindow(echoes_game_data, None, False, True) window.editor = MagicMock() skip_qtbot.addWidget(window) editor = mock_connections_editor.return_value execute_dialog.return_value = QtWidgets.QDialog.Accepted if accept else QtWidgets.QDialog.Rejected # Run await window._open_edit_connection() # Assert mock_connections_editor.assert_called_once_with(window, window.resource_database, ANY) if accept: window.editor.edit_connections.assert_called_once_with( window.current_area, window.current_node, window.current_connection_node, editor.final_requirement) else: window.editor.edit_connections.assert_not_called()
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 _open_data_editor_prompt(self): from randovania.gui.data_editor import DataEditorWindow 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), database_path, False, True) self._data_editor.show()
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 test_create_new_dock(skip_qtbot, tmp_path, blank_game_data): db_path = Path(tmp_path.joinpath("test-game", "game")) game_data = default_data.read_json_then_binary(RandovaniaGame.BLANK)[1] window = DataEditorWindow(game_data, db_path, True, True) window.set_warning_dialogs_disabled(True) skip_qtbot.addWidget(window) window.focus_on_area_by_name("Back-Only Lock Room") current_area = window.current_area target_area = window.game_description.world_list.area_by_area_location( AreaIdentifier("Intro", "Explosive Depot")) assert current_area.node_with_name("Dock to Explosive Depot") is None assert target_area.node_with_name("Dock to Back-Only Lock Room") is None # Run window._create_new_dock(NodeLocation(0, 0, 0), target_area) # Assert new_node = current_area.node_with_name("Dock to Explosive Depot") assert new_node is not None assert window.current_node is new_node assert target_area.node_with_name( "Dock to Back-Only Lock Room") is not None
def test_create_node_and_save(tmp_path, echoes_game_data, skip_qtbot): # Setup tmp_path.joinpath("test-game", "game").mkdir(parents=True) tmp_path.joinpath("human-readable").mkdir() db_path = Path(tmp_path.joinpath("test-game", "game")) window = DataEditorWindow(echoes_game_data, db_path, True, True) window.set_warning_dialogs_disabled(True) skip_qtbot.addWidget(window) # Run window._do_create_node("Some Node", None) window._save_as_internal_database() # Assert exported_data = data_reader.read_split_file(db_path) exported_game = data_reader.decode_data(exported_data) pretty_print.write_human_readable_game(exported_game, tmp_path.joinpath("human-readable")) new_files = { f.name: f.read_text("utf-8") for f in tmp_path.joinpath("human-readable").glob("*.txt") } existing_files = { f.name: f.read_text("utf-8") for f in tmp_path.joinpath("test-game", "game").glob("*.txt") } assert list(new_files.keys()) == list(existing_files.keys()) assert new_files == existing_files
def show_data_editor(app: QApplication): from randovania.games.prime import default_data from randovania.gui.data_editor import DataEditorWindow app.data_visualizer = DataEditorWindow( default_data.decode_default_prime2(), True) app.data_visualizer.show()
def test_create_node_and_save(mock_prime2_human_readable_path, mock_prime2_json_path, tmpdir, echoes_game_data, skip_qtbot): # Setup mock_prime2_human_readable_path.return_value = Path(tmpdir).joinpath( "human") mock_prime2_json_path.return_value = Path(tmpdir).joinpath("database") window = DataEditorWindow(echoes_game_data, True) skip_qtbot.addWidget(window) # Run window._do_create_node("Some Node") window._save_as_internal_database() # Assert with mock_prime2_json_path.return_value.open() as data_file: exported_data = json.load(data_file) exported_game = data_reader.decode_data(exported_data) output = io.StringIO() data_writer.write_human_readable_world_list(exported_game, output) assert mock_prime2_human_readable_path.return_value.read_text( "utf-8") == output.getvalue()
def test_apply_edit_connections_change( echoes_game_data, qtbot, ): # Setup window = DataEditorWindow(echoes_game_data, True) qtbot.addWidget(window) game = window.game_description landing_site = game.world_list.area_by_asset_id(1655756413) source = landing_site.node_with_name("Save Station") target = landing_site.node_with_name("Door to Service Access") # Run window.world_selector_box.setCurrentIndex( window.world_selector_box.findText("Temple Grounds")) window.area_selector_box.setCurrentIndex( window.area_selector_box.findText(landing_site.name)) window._apply_edit_connections(source, target, RequirementSet.trivial()) # Assert assert landing_site.connections[source][target] == RequirementSet.trivial()
def open_data_visualizer_at( self, world_name: Optional[str], area_name: Optional[str], game: RandovaniaGame = RandovaniaGame.PRIME2, ): self._data_visualizer = DataEditorWindow.open_internal_data( game, 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_visualizer_at( self, world_name: Optional[str], area_name: Optional[str], game: RandovaniaGame = RandovaniaGame.METROID_PRIME_ECHOES, ): from randovania.gui.data_editor import DataEditorWindow data_visualizer = DataEditorWindow.open_internal_data(game, False) self._data_visualizer = data_visualizer if world_name is not None: data_visualizer.focus_on_world_by_name(world_name) if area_name is not None: data_visualizer.focus_on_area_by_name(area_name) self._data_visualizer.show()
def test_select_area_by_name( echoes_game_data, skip_qtbot, ): # Setup window = DataEditorWindow(echoes_game_data, None, False, True) skip_qtbot.addWidget(window) # Run window.focus_on_world_by_name("Torvus Bog") assert window.current_area.name != "Forgotten Bridge" window.focus_on_area_by_name("Forgotten Bridge") # Assert assert window.current_area.name == "Forgotten Bridge"
def test_create_node_and_save(tmpdir, echoes_game_data, skip_qtbot): # Setup db_path = Path(tmpdir.join("game.json")) window = DataEditorWindow(echoes_game_data, db_path, True, True) skip_qtbot.addWidget(window) # Run window._do_create_node("Some Node") window._save_as_internal_database() # Assert with db_path.open() as data_file: exported_data = json.load(data_file) exported_game = data_reader.decode_data(exported_data) output = io.StringIO() randovania.game_description.pretty_print.write_human_readable_world_list( exported_game, output) assert Path( tmpdir.join("game.txt")).read_text("utf-8") == output.getvalue()
def test_on_filters_changed_view_mode(tmp_path, mocker, skip_qtbot): db_path = Path(tmp_path.joinpath("test-game", "game")) game_data = default_data.read_json_then_binary(RandovaniaGame.BLANK)[1] window = DataEditorWindow(game_data, db_path, True, False) skip_qtbot.addWidget(window) # Disable the layer found = False for (trick, trick_check), trick_combo in window.layers_editor.tricks.items(): if trick.long_name == "Combat": trick_check.setChecked(True) trick_combo.setCurrentIndex(1) found = True assert found window.layers_editor.layer_checks[1].setChecked(False) window.focus_on_world_by_name("Intro") window.focus_on_area_by_name("Boss Arena") assert window.current_area.node_with_name("Pickup (Free Loot)") is None
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)
class MainWindow(WindowManager, Ui_MainWindow): options_changed_signal = Signal() _is_preview_mode: bool = False _experimental_games_visible: bool = False menu_new_version: Optional[QtGui.QAction] = None _current_version_url: Optional[str] = None _options: Options _data_visualizer: Optional[QtWidgets.QWidget] = None _map_tracker: QtWidgets.QWidget _preset_manager: PresetManager GameDetailsSignal = Signal(LayoutDescription) InitPostShowSignal = Signal() @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) -> QtWidgets.QMainWindow: return self @property def is_preview_mode(self) -> bool: return self._is_preview_mode def __init__(self, options: Options, preset_manager: PresetManager, network_client, 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.setup_about_text() self.setup_welcome_text() self.browse_racetime_label.setText( self.browse_racetime_label.text().replace("color:#0000ff;", "")) self._preset_manager = preset_manager self.network_client = network_client if preview: debug.set_level(2) if randovania.is_frozen(): self.menu_bar.removeAction(self.menu_edit.menuAction()) # Signals self.options_changed_signal.connect(self.on_options_changed) self.GameDetailsSignal.connect(self._open_game_details) self.InitPostShowSignal.connect(self.initialize_post_show) self.intro_play_now_button.clicked.connect( lambda: self.main_tab_widget.setCurrentWidget(self.tab_play)) self.open_faq_button.clicked.connect(self._open_faq) self.open_database_viewer_button.clicked.connect( partial(self._open_data_visualizer_for_game, RandovaniaGame.METROID_PRIME_ECHOES)) self.import_permalink_button.clicked.connect(self._import_permalink) self.import_game_file_button.clicked.connect(self._import_spoiler_log) self.browse_racetime_button.clicked.connect(self._browse_racetime) self.create_new_seed_button.clicked.connect( lambda: self.main_tab_widget.setCurrentWidget(self.tab_create_seed )) # Menu Bar self.game_menus = [] self.menu_action_edits = [] for game in RandovaniaGame.sorted_all_games(): # Sub-Menu in Open Menu game_menu = QtWidgets.QMenu(self.menu_open) game_menu.setTitle(_t(game.long_name)) game_menu.game = game if game.data.development_state.can_view(False): self.menu_open.addAction(game_menu.menuAction()) self.game_menus.append(game_menu) game_trick_details_menu = QtWidgets.QMenu(game_menu) game_trick_details_menu.setTitle(_t("Trick Details")) self._setup_trick_difficulties_menu_on_show( game_trick_details_menu, game) game_data_visualizer_action = QtGui.QAction(game_menu) game_data_visualizer_action.setText(_t("Data Visualizer")) game_data_visualizer_action.triggered.connect( partial(self._open_data_visualizer_for_game, game)) game_menu.addAction(game_trick_details_menu.menuAction()) game_menu.addAction(game_data_visualizer_action) # Data Editor action = QtGui.QAction(self) action.setText(_t(game.long_name)) self.menu_internal.addAction(action) action.triggered.connect( partial(self._open_data_editor_for_game, game)) self.menu_action_edits.append(action) 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.menu_action_dark_mode.triggered.connect( self._on_menu_action_dark_mode) self.menu_action_experimental_games.triggered.connect( self._on_menu_action_experimental_games) self.menu_action_open_auto_tracker.triggered.connect( self._open_auto_tracker) self.menu_action_previously_generated_games.triggered.connect( self._on_menu_action_previously_generated_games) self.menu_action_log_files_directory.triggered.connect( self._on_menu_action_log_files_directory) self.menu_action_layout_editor.triggered.connect( self._on_menu_action_layout_editor) # 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.main_tab_widget.setCurrentIndex(0) def closeEvent(self, event): self.generate_seed_tab.stop_background_process() super().closeEvent(event) def dragEnterEvent(self, event: QtGui.QDragEnterEvent): from randovania.layout.versioned_preset import VersionedPreset valid_extensions = [ LayoutDescription.file_extension(), VersionedPreset.file_extension(), ] valid_extensions_with_dot = { f".{extension}" for extension in valid_extensions } for url in event.mimeData().urls(): ext = os.path.splitext(url.toLocalFile())[1] if ext in valid_extensions_with_dot: event.acceptProposedAction() return def dropEvent(self, event: QtGui.QDropEvent): from randovania.layout.versioned_preset import VersionedPreset for url in event.mimeData().urls(): path = Path(url.toLocalFile()) if path.suffix == f".{LayoutDescription.file_extension()}": self.open_game_details(LayoutDescription.from_file(path)) return elif path.suffix == f".{VersionedPreset.file_extension()}": self.main_tab_widget.setCurrentWidget(self.tab_create_seed) self.generate_seed_tab.import_preset_file(path) return def showEvent(self, event: QtGui.QShowEvent): self.InitPostShowSignal.emit() # Per-Game elements def refresh_game_list(self): self.games_tab.set_experimental_visible( self.menu_action_experimental_games.isChecked()) if self._experimental_games_visible == self.menu_action_experimental_games.isChecked( ): return self._experimental_games_visible = self.menu_action_experimental_games.isChecked( ) for game_menu in self.game_menus: self.menu_open.removeAction(game_menu.menuAction()) for game_menu, edit_action in zip(self.game_menus, self.menu_action_edits): game: RandovaniaGame = game_menu.game if game.data.development_state.can_view( self.menu_action_experimental_games.isChecked()): self.menu_open.addAction(game_menu.menuAction()) # Delayed Initialization @asyncSlot() async def initialize_post_show(self): self.InitPostShowSignal.disconnect(self.initialize_post_show) logging.info("Will initialize things in post show") await self._initialize_post_show_body() logging.info("Finished initializing post show") async def _initialize_post_show_body(self): logging.info("Will load OnlineInteractions") from randovania.gui.main_online_interaction import OnlineInteractions logging.info("Creating OnlineInteractions...") self.online_interactions = OnlineInteractions(self, self.preset_manager, self.network_client, self, self._options) logging.info("Will load GenerateSeedTab") from randovania.gui.generate_seed_tab import GenerateSeedTab logging.info("Creating GenerateSeedTab...") self.generate_seed_tab = GenerateSeedTab(self, self, self._options) logging.info("Running GenerateSeedTab.setup_ui") self.generate_seed_tab.setup_ui() logging.info("Will update for modified options") with self._options: self.on_options_changed() # Generate Seed def _open_faq(self): self.main_tab_widget.setCurrentWidget(self.games_tab) async def generate_seed_from_permalink(self, permalink: Permalink): from randovania.lib.status_update_lib import ProgressUpdateCallable from randovania.gui.dialog.background_process_dialog import BackgroundProcessDialog def work(progress_update: ProgressUpdateCallable): from randovania.interface_common import simplified_patcher layout = simplified_patcher.generate_layout( progress_update=progress_update, parameters=permalink.parameters, options=self._options) progress_update(f"Success! (Seed hash: {layout.shareable_hash})", 1) return layout new_layout = await BackgroundProcessDialog.open_for_background_task( work, "Creating a game...") if permalink.seed_hash is not None and permalink.seed_hash != new_layout.shareable_hash_bytes: response = await async_dialog.warning( self, "Unexpected hash", "Expected has to be {}. got {}. Do you wish to continue?". format( base64.b32encode(permalink.seed_hash).decode(), new_layout.shareable_hash, ), QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, ) if response != QtWidgets.QMessageBox.Yes: return self.open_game_details(new_layout) @asyncSlot() async def _import_permalink(self): from randovania.gui.dialog.permalink_dialog import PermalinkDialog dialog = PermalinkDialog() result = await async_dialog.execute_dialog(dialog) if result == QtWidgets.QDialog.Accepted: permalink = dialog.get_permalink_from_field() await self.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.open_game_details(layout) @asyncSlot() async def _browse_racetime(self): from randovania.gui.dialog.racetime_browser_dialog import RacetimeBrowserDialog dialog = RacetimeBrowserDialog() if not await dialog.refresh(): return result = await async_dialog.execute_dialog(dialog) if result == QtWidgets.QDialog.Accepted: await self.generate_seed_from_permalink(dialog.permalink) def open_game_details(self, layout: LayoutDescription): self.GameDetailsSignal.emit(layout) def _open_game_details(self, layout: LayoutDescription): from randovania.gui.game_details.game_details_window import GameDetailsWindow details_window = GameDetailsWindow(self, self._options) details_window.update_layout_description(layout) details_window.show() self.track_window(details_window) # Releases info async def request_new_data(self): from randovania.interface_common import github_releases_data await self._on_releases_data(await github_releases_data.get_releases()) async def _on_releases_data(self, releases: Optional[List[dict]]): 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: from randovania.gui.widgets.changelog_widget import ChangeLogWidget changelog_tab = ChangeLogWidget(all_change_logs) self.main_tab_widget.addTab(changelog_tab, "Change Log") if new_change_logs: from randovania.gui.lib.scroll_message_box import ScrollMessageBox message_box = ScrollMessageBox.create_new( self, QtWidgets.QMessageBox.Information, "What's new", "\n".join(new_change_logs), QtWidgets.QMessageBox.Ok, ) message_box.label.setTextFormat(QtCore.Qt.TextFormat.MarkdownText) message_box.scroll_area.setMinimumSize(500, 300) await async_dialog.execute_dialog(message_box) 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 = QtGui.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") QtGui.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.menu_action_dark_mode.setChecked(self._options.dark_mode) self.menu_action_experimental_games.setChecked( self._options.experimental_games) self.refresh_game_list() self.generate_seed_tab.on_options_changed(self._options) theme.set_dark_theme(self._options.dark_mode) # Menu Actions def _open_data_visualizer_for_game(self, game: RandovaniaGame): self.open_data_visualizer_at(None, None, game) def open_data_visualizer_at( self, world_name: Optional[str], area_name: Optional[str], game: RandovaniaGame = RandovaniaGame.METROID_PRIME_ECHOES, ): from randovania.gui.data_editor import DataEditorWindow data_visualizer = DataEditorWindow.open_internal_data(game, False) self._data_visualizer = data_visualizer if world_name is not None: data_visualizer.focus_on_world_by_name(world_name) if area_name is not None: data_visualizer.focus_on_area_by_name(area_name) self._data_visualizer.show() def _open_data_editor_for_game(self, game: RandovaniaGame): from randovania.gui.data_editor import DataEditorWindow self._data_editor = DataEditorWindow.open_internal_data(game, True) self._data_editor.show() def _open_data_editor_prompt(self): from randovania.gui.data_editor import DataEditorWindow 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), database_path, False, True) self._data_editor.show() async def open_map_tracker(self, configuration: "Preset"): from randovania.gui.tracker_window import TrackerWindow, InvalidLayoutForTracker try: self._map_tracker = await TrackerWindow.create_new( self._options.tracker_files_path, configuration) except InvalidLayoutForTracker as e: QtWidgets.QMessageBox.critical( self, "Unsupported configuration for Tracker", str(e)) return self._map_tracker.show() # 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, game, trick: TrickResourceInfo, level: LayoutTrickLevel): from randovania.gui.dialog.trick_details_popup import TrickDetailsPopup self._exec_trick_details( TrickDetailsPopup(self, self, game, trick, level)) def _setup_trick_difficulties_menu_on_show(self, menu: QtWidgets.QMenu, game: RandovaniaGame): def on_show(): menu.aboutToShow.disconnect(on_show) self._setup_difficulties_menu(game, menu) menu.aboutToShow.connect(on_show) def _setup_difficulties_menu(self, game: RandovaniaGame, menu: QtWidgets.QMenu): from randovania.game_description import default_database game = default_database.game_description_for(game) tricks_in_use = used_tricks(game) menu.clear() for trick in sorted(game.resource_database.trick, key=lambda _trick: _trick.long_name): if trick not in tricks_in_use: continue trick_menu = QtWidgets.QMenu(self) trick_menu.setTitle(_t(trick.long_name)) menu.addAction(trick_menu.menuAction()) used_difficulties = difficulties_for_trick(game, trick) for trick_level in enum_lib.iterate_enum(LayoutTrickLevel): if trick_level in used_difficulties: difficulty_action = QtGui.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, game, trick, trick_level)) # ========== @asyncSlot() async 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 = QtWidgets.QMessageBox(self) box.setWindowTitle("Disable validation?") box.setText(_DISABLE_VALIDATION_WARNING) box.setIcon(QtWidgets.QMessageBox.Warning) box.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) box.setDefaultButton(QtWidgets.QMessageBox.No) user_response = await async_dialog.execute_dialog(box) if user_response != QtWidgets.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 _on_menu_action_dark_mode(self): with self._options as options: options.dark_mode = self.menu_action_dark_mode.isChecked() def _on_menu_action_experimental_games(self): with self._options as options: options.experimental_games = self.menu_action_experimental_games.isChecked( ) def _open_auto_tracker(self): from randovania.gui.auto_tracker_window import AutoTrackerWindow self.auto_tracker_window = AutoTrackerWindow( common_qt_lib.get_game_connection(), self._options) self.auto_tracker_window.show() def _on_menu_action_previously_generated_games(self): path = self._options.game_history_path try: open_directory_in_explorer(path) except OSError: box = QtWidgets.QMessageBox( QtWidgets.QMessageBox.Information, "Game History", f"Previously generated games can be found at:\n{path}", QtWidgets.QMessageBox.Ok, self) box.setTextInteractionFlags(Qt.TextSelectableByMouse) box.show() def _on_menu_action_log_files_directory(self): path = self._options.logs_path try: open_directory_in_explorer(path) except OSError: box = QtWidgets.QMessageBox( QtWidgets.QMessageBox.Information, "Logs", f"Randovania logs can be found at:\n{path}", QtWidgets.QMessageBox.Ok, self) box.setTextInteractionFlags(Qt.TextSelectableByMouse) box.show() def _on_menu_action_layout_editor(self): from randovania.gui.corruption_layout_editor import CorruptionLayoutEditor self.corruption_editor = CorruptionLayoutEditor() self.corruption_editor.show() def setup_about_text(self): ABOUT_TEXT = "\n".join([ "# Randovania", "", "<https://github.com/randovania/randovania>", "", "This software is covered by the [GNU General Public License v3 (GPLv3)](https://www.gnu.org/licenses/gpl-3.0.en.html)", "", "{community}", "", "{credits}", ]) about_document: QtGui.QTextDocument = self.about_text_browser.document( ) # Populate from README.md community = get_readme_section("COMMUNITY") credit = get_readme_section("CREDITS") about_document.setMarkdown( ABOUT_TEXT.format(community=community, credits=credit)) # Remove all hardcoded link color about_document.setHtml(about_document.toHtml().replace( "color:#0000ff;", "")) cursor: QtGui.QTextCursor = self.about_text_browser.textCursor() cursor.setPosition(0) self.about_text_browser.setTextCursor(cursor) def setup_welcome_text(self): self.intro_label.setText( self.intro_label.text().format(version=VERSION)) welcome = get_readme_section("WELCOME") supported = get_readme_section("SUPPORTED") experimental = get_readme_section("EXPERIMENTAL") self.games_supported_label.setText(supported) self.games_experimental_label.setText(experimental) self.intro_welcome_label.setText(welcome)
def _open_data_editor_for_game(self, game: RandovaniaGame): from randovania.gui.data_editor import DataEditorWindow self._data_editor = DataEditorWindow.open_internal_data(game, True) self._data_editor.show()
class MainWindow(WindowManager, Ui_MainWindow): newer_version_signal = Signal(str, str) options_changed_signal = Signal() _is_preview_mode: bool = False menu_new_version: Optional[QtWidgets.QAction] = None _current_version_url: Optional[str] = None _options: Options _data_visualizer: Optional[QtWidgets.QWidget] = None _map_tracker: QtWidgets.QWidget _preset_manager: PresetManager GameDetailsSignal = Signal(LayoutDescription) InitPostShowSignal = Signal() @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) -> QtWidgets.QMainWindow: return self @property def is_preview_mode(self) -> bool: return self._is_preview_mode def __init__(self, options: Options, preset_manager: PresetManager, network_client, 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) # Remove all hardcoded link color about_document: QtGui.QTextDocument = self.about_text_browser.document( ) about_document.setHtml(about_document.toHtml().replace( "color:#0000ff;", "")) self.browse_racetime_label.setText( self.browse_racetime_label.text().replace("color:#0000ff;", "")) self.intro_label.setText( self.intro_label.text().format(version=VERSION)) self._preset_manager = preset_manager self.network_client = network_client if preview: debug.set_level(2) # Signals self.newer_version_signal.connect(self.display_new_version) self.options_changed_signal.connect(self.on_options_changed) self.GameDetailsSignal.connect(self._open_game_details) self.InitPostShowSignal.connect(self.initialize_post_show) 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( partial(self._open_data_visualizer_for_game, RandovaniaGame.PRIME2)) for game in RandovaniaGame: self.hint_item_names_game_combo.addItem(game.long_name, game) self.hint_location_game_combo.addItem(game.long_name, game) self.hint_item_names_game_combo.currentIndexChanged.connect( self._update_hints_text) self.hint_location_game_combo.currentIndexChanged.connect( self._update_hint_locations) self.import_permalink_button.clicked.connect(self._import_permalink) self.import_game_file_button.clicked.connect(self._import_spoiler_log) self.browse_racetime_button.clicked.connect(self._browse_racetime) self.create_new_seed_button.clicked.connect( lambda: self.welcome_tab_widget.setCurrentWidget(self. tab_create_seed)) # Menu Bar for action, game in ((self.menu_action_prime_1_data_visualizer, RandovaniaGame.PRIME1), (self.menu_action_prime_2_data_visualizer, RandovaniaGame.PRIME2), (self.menu_action_prime_3_data_visualizer, RandovaniaGame.PRIME3)): action.triggered.connect( partial(self._open_data_visualizer_for_game, game)) for action, game in ((self.menu_action_edit_prime_1, RandovaniaGame.PRIME1), (self.menu_action_edit_prime_2, RandovaniaGame.PRIME2), (self.menu_action_edit_prime_3, RandovaniaGame.PRIME3)): action.triggered.connect( partial(self._open_data_editor_for_game, game)) self.menu_action_item_tracker.triggered.connect( self._open_item_tracker) self.menu_action_map_tracker.triggered.connect( self._on_menu_action_map_tracker) 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.menu_action_dark_mode.triggered.connect( self._on_menu_action_dark_mode) self.menu_action_open_auto_tracker.triggered.connect( self._open_auto_tracker) self.menu_action_previously_generated_games.triggered.connect( self._on_menu_action_previously_generated_games) self.menu_action_layout_editor.triggered.connect( self._on_menu_action_layout_editor) self.menu_prime_1_trick_details.aboutToShow.connect( self._create_trick_details_prime_1) self.menu_prime_2_trick_details.aboutToShow.connect( self._create_trick_details_prime_2) self.menu_prime_3_trick_details.aboutToShow.connect( self._create_trick_details_prime_3) # 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.main_tab_widget.setCurrentIndex(0) def closeEvent(self, event): self.generate_seed_tab.stop_background_process() super().closeEvent(event) def dragEnterEvent(self, event: QtGui.QDragEnterEvent): from randovania.layout.preset_migration import VersionedPreset valid_extensions = [ LayoutDescription.file_extension(), VersionedPreset.file_extension(), ] valid_extensions_with_dot = { f".{extension}" for extension in valid_extensions } for url in event.mimeData().urls(): ext = os.path.splitext(url.toLocalFile())[1] if ext in valid_extensions_with_dot: event.acceptProposedAction() return def dropEvent(self, event: QtGui.QDropEvent): from randovania.layout.preset_migration import VersionedPreset for url in event.mimeData().urls(): path = Path(url.toLocalFile()) if path.suffix == f".{LayoutDescription.file_extension()}": self.open_game_details(LayoutDescription.from_file(path)) return elif path.suffix == f".{VersionedPreset.file_extension()}": self.main_tab_widget.setCurrentWidget(self.welcome_tab) self.welcome_tab_widget.setCurrentWidget(self.tab_create_seed) self.generate_seed_tab.import_preset_file(path) return def showEvent(self, event: QtGui.QShowEvent): self.InitPostShowSignal.emit() # Delayed Initialization @asyncSlot() async def initialize_post_show(self): self.InitPostShowSignal.disconnect(self.initialize_post_show) logging.info("Will initialize things in post show") await self._initialize_post_show_body() logging.info("Finished initializing post show") async def _initialize_post_show_body(self): logging.info("Will load OnlineInteractions") from randovania.gui.main_online_interaction import OnlineInteractions logging.info("Creating OnlineInteractions...") self.online_interactions = OnlineInteractions(self, self.preset_manager, self.network_client, self, self._options) logging.info("Will load GenerateSeedTab") from randovania.gui.generate_seed_tab import GenerateSeedTab logging.info("Creating GenerateSeedTab...") self.generate_seed_tab = GenerateSeedTab(self, self, self._options) logging.info("Running GenerateSeedTab.setup_ui") self.generate_seed_tab.setup_ui() # Update hints text logging.info("Will _update_hints_text") self._update_hints_text() logging.info("Will hide hint locations combo") self.hint_location_game_combo.setVisible(False) self.hint_location_game_combo.setCurrentIndex(1) logging.info("Will update for modified options") with self._options: self.on_options_changed() def _update_hints_text(self): from randovania.gui.lib import hints_text hints_text.update_hints_text( self.hint_item_names_game_combo.currentData(), self.hint_item_names_tree_widget) def _update_hint_locations(self): from randovania.gui.lib import hints_text hints_text.update_hint_locations( self.hint_location_game_combo.currentData(), self.hint_tree_widget) # Generate Seed def _open_faq(self): self.main_tab_widget.setCurrentWidget(self.help_tab) self.help_tab_widget.setCurrentWidget(self.tab_faq) async def generate_seed_from_permalink(self, permalink): from randovania.interface_common.status_update_lib import ProgressUpdateCallable from randovania.gui.dialog.background_process_dialog import BackgroundProcessDialog def work(progress_update: ProgressUpdateCallable): from randovania.interface_common import simplified_patcher layout = simplified_patcher.generate_layout( progress_update=progress_update, permalink=permalink, options=self._options) progress_update(f"Success! (Seed hash: {layout.shareable_hash})", 1) return layout new_layout = await BackgroundProcessDialog.open_for_background_task( work, "Creating a game...") self.open_game_details(new_layout) @asyncSlot() async def _import_permalink(self): from randovania.gui.dialog.permalink_dialog import PermalinkDialog dialog = PermalinkDialog() result = await async_dialog.execute_dialog(dialog) if result == QtWidgets.QDialog.Accepted: permalink = dialog.get_permalink_from_field() await self.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.open_game_details(layout) @asyncSlot() async def _browse_racetime(self): from randovania.gui.dialog.racetime_browser_dialog import RacetimeBrowserDialog dialog = RacetimeBrowserDialog() if not await dialog.refresh(): return result = await async_dialog.execute_dialog(dialog) if result == QtWidgets.QDialog.Accepted: await self.generate_seed_from_permalink(dialog.permalink) def open_game_details(self, layout: LayoutDescription): self.GameDetailsSignal.emit(layout) 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) # Releases info async def request_new_data(self): from randovania.interface_common import github_releases_data await self._on_releases_data(await github_releases_data.get_releases()) async def _on_releases_data(self, releases: Optional[List[dict]]): import markdown 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") for entry in all_change_logs: changelog_label = QtWidgets.QLabel(changelog_scroll_contents) _update_label_on_show(changelog_label, markdown.markdown(entry)) changelog_label.setObjectName("changelog_label") 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: await async_dialog.message_box( self, QtWidgets.QMessageBox.Information, "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 = QtWidgets.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") QtGui.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.menu_action_dark_mode.setChecked(self._options.dark_mode) self.generate_seed_tab.on_options_changed(self._options) theme.set_dark_theme(self._options.dark_mode) # Menu Actions def _open_data_visualizer_for_game(self, game: RandovaniaGame): self.open_data_visualizer_at(None, None, game) def open_data_visualizer_at( self, world_name: Optional[str], area_name: Optional[str], game: RandovaniaGame = RandovaniaGame.PRIME2, ): from randovania.gui.data_editor import DataEditorWindow data_visualizer = DataEditorWindow.open_internal_data(game, False) self._data_visualizer = data_visualizer if world_name is not None: data_visualizer.focus_on_world(world_name) if area_name is not None: data_visualizer.focus_on_area(area_name) self._data_visualizer.show() def _open_data_editor_for_game(self, game: RandovaniaGame): from randovania.gui.data_editor import DataEditorWindow self._data_editor = DataEditorWindow.open_internal_data(game, True) self._data_editor.show() def _open_data_editor_prompt(self): from randovania.gui.data_editor import DataEditorWindow 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), database_path, False, True) self._data_editor.show() @asyncSlot() async def _on_menu_action_map_tracker(self): dialog = QtWidgets.QInputDialog(self) dialog.setWindowTitle("Map Tracker") dialog.setLabelText("Select preset used for the tracker.") dialog.setComboBoxItems( [preset.name for preset in self._preset_manager.all_presets]) dialog.setTextValue(self._options.selected_preset_name) result = await async_dialog.execute_dialog(dialog) if result == QtWidgets.QDialog.Accepted: preset = self._preset_manager.preset_for_name(dialog.textValue()) self.open_map_tracker(preset.get_preset().configuration) def open_map_tracker(self, configuration: "EchoesConfiguration"): from randovania.gui.tracker_window import TrackerWindow, InvalidLayoutForTracker try: self._map_tracker = TrackerWindow(self._options.tracker_files_path, configuration) except InvalidLayoutForTracker as e: QtWidgets.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 = QtWidgets.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, game, trick: TrickResourceInfo, level: LayoutTrickLevel): from randovania.gui.dialog.trick_details_popup import TrickDetailsPopup self._exec_trick_details( TrickDetailsPopup(self, self, game, trick, level)) def _create_trick_details_prime_1(self): self.menu_prime_1_trick_details.aboutToShow.disconnect( self._create_trick_details_prime_1) self._setup_difficulties_menu(RandovaniaGame.PRIME1, self.menu_prime_1_trick_details) def _create_trick_details_prime_2(self): self.menu_prime_2_trick_details.aboutToShow.disconnect( self._create_trick_details_prime_2) self._setup_difficulties_menu(RandovaniaGame.PRIME2, self.menu_prime_2_trick_details) def _create_trick_details_prime_3(self): self.menu_prime_3_trick_details.aboutToShow.disconnect( self._create_trick_details_prime_3) self._setup_difficulties_menu(RandovaniaGame.PRIME3, self.menu_prime_3_trick_details) def _setup_difficulties_menu(self, game: RandovaniaGame, menu: QtWidgets.QMenu): from randovania.game_description import default_database game = default_database.game_description_for(game) tricks_in_use = used_tricks(game) menu.clear() for trick in sorted(game.resource_database.trick, key=lambda _trick: _trick.long_name): if trick not in tricks_in_use: continue trick_menu = QtWidgets.QMenu(self) trick_menu.setTitle(trick.long_name) menu.addAction(trick_menu.menuAction()) used_difficulties = difficulties_for_trick(game, trick) for i, trick_level in enumerate(iterate_enum(LayoutTrickLevel)): if trick_level in used_difficulties: difficulty_action = QtWidgets.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, game, trick, trick_level)) # ========== @asyncSlot() async 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 = QtWidgets.QMessageBox(self) box.setWindowTitle("Disable validation?") box.setText(_DISABLE_VALIDATION_WARNING) box.setIcon(QtWidgets.QMessageBox.Warning) box.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) box.setDefaultButton(QtWidgets.QMessageBox.No) user_response = await async_dialog.execute_dialog(box) if user_response != QtWidgets.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 _on_menu_action_dark_mode(self): with self._options as options: options.dark_mode = self.menu_action_dark_mode.isChecked() def _open_auto_tracker(self): from randovania.gui.auto_tracker_window import AutoTrackerWindow self.auto_tracker_window = AutoTrackerWindow( common_qt_lib.get_game_connection(), self._options) self.auto_tracker_window.show() def _on_menu_action_previously_generated_games(self): path = self._options.data_dir.joinpath("game_history") try: if platform.system() == "Windows": os.startfile(path) elif platform.system() == "Darwin": subprocess.run(["open", path]) else: subprocess.run(["xdg-open", path]) except OSError: print("Exception thrown :)") box = QtWidgets.QMessageBox( QtWidgets.QMessageBox.Information, "Game History", f"Previously generated games can be found at:\n{path}", QtWidgets.QMessageBox.Ok, self) box.setTextInteractionFlags(Qt.TextSelectableByMouse) box.show() def _on_menu_action_layout_editor(self): from randovania.gui.corruption_layout_editor import CorruptionLayoutEditor self.corruption_editor = CorruptionLayoutEditor() self.corruption_editor.show()
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)
def _open_data_editor_default(self): self._data_editor = DataEditorWindow( default_data.decode_default_prime2(), True) self._data_editor.show()
def _open_data_visualizer(self): self._data_visualizer = DataEditorWindow( default_data.decode_default_prime2(), False) self._data_visualizer.show()
def _open_data_editor_for_game(self, game: RandovaniaGame): self._data_editor = DataEditorWindow.open_internal_data(game, True) self._data_editor.show()
class MainWindow(WindowManager, Ui_MainWindow): 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 _map_tracker: TrackerWindow _preset_manager: PresetManager game_session_window: Optional[GameSessionWindow] = None _login_window: Optional[QDialog] = None GameDetailsSignal = Signal(LayoutDescription) @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, network_client: QtNetworkClient, 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) # Remove all hardcoded link color about_document: QtGui.QTextDocument = self.about_text_browser.document( ) about_document.setHtml(about_document.toHtml().replace( "color:#0000ff;", "")) self.browse_racetime_label.setText( self.browse_racetime_label.text().replace("color:#0000ff;", "")) self.intro_label.setText( self.intro_label.text().format(version=VERSION)) self._preset_manager = preset_manager self.network_client = network_client if preview: debug.set_level(2) # Signals self.newer_version_signal.connect(self.display_new_version) self.options_changed_signal.connect(self.on_options_changed) self.GameDetailsSignal.connect(self._open_game_details) 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( partial(self._open_data_visualizer_for_game, RandovaniaGame.PRIME2)) self.import_permalink_button.clicked.connect(self._import_permalink) self.import_game_file_button.clicked.connect(self._import_spoiler_log) self.browse_racetime_button.clicked.connect(self._browse_racetime) self.browse_sessions_button.clicked.connect( self._browse_for_game_session) self.host_new_game_button.clicked.connect(self._host_game_session) self.create_new_seed_button.clicked.connect( lambda: self.welcome_tab_widget.setCurrentWidget(self. tab_create_seed)) # Menu Bar for action, game in ((self.menu_action_visualize_prime_1, RandovaniaGame.PRIME1), (self.menu_action_visualize_prime_2, RandovaniaGame.PRIME2), (self.menu_action_visualize_prime_3, RandovaniaGame.PRIME3)): action.triggered.connect( partial(self._open_data_visualizer_for_game, game)) for action, game in ((self.menu_action_edit_prime_1, RandovaniaGame.PRIME1), (self.menu_action_edit_prime_2, RandovaniaGame.PRIME2), (self.menu_action_edit_prime_3, RandovaniaGame.PRIME3)): action.triggered.connect( partial(self._open_data_editor_for_game, game)) self.menu_action_item_tracker.triggered.connect( self._open_item_tracker) self.menu_action_map_tracker.triggered.connect( self._on_menu_action_map_tracker) 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.menu_action_dark_mode.triggered.connect( self._on_menu_action_dark_mode) self.menu_action_open_auto_tracker.triggered.connect( self._open_auto_tracker) self.menu_action_previously_generated_games.triggered.connect( self._on_menu_action_previously_generated_games) self.menu_action_layout_editor.triggered.connect( self._on_menu_action_layout_editor) self.action_login_window.triggered.connect(self._action_login_window) self.generate_seed_tab = GenerateSeedTab(self, self, options) self.generate_seed_tab.setup_ui() # Needs the GenerateSeedTab 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.generate_seed_tab.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) async def generate_seed_from_permalink(self, permalink): from randovania.interface_common.status_update_lib import ProgressUpdateCallable from randovania.gui.dialog.background_process_dialog import BackgroundProcessDialog def work(progress_update: ProgressUpdateCallable): from randovania.interface_common import simplified_patcher layout = simplified_patcher.generate_layout( progress_update=progress_update, permalink=permalink, options=self._options) progress_update(f"Success! (Seed hash: {layout.shareable_hash})", 1) return layout new_layout = await BackgroundProcessDialog.open_for_background_task( work, "Creating a game...") self.open_game_details(new_layout) @asyncSlot() async def _import_permalink(self): dialog = PermalinkDialog() result = await async_dialog.execute_dialog(dialog) if result == QDialog.Accepted: permalink = dialog.get_permalink_from_field() await self.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.open_game_details(layout) @asyncSlot() async def _browse_racetime(self): from randovania.gui.dialog.racetime_browser_dialog import RacetimeBrowserDialog dialog = RacetimeBrowserDialog() if not await dialog.refresh(): return result = await async_dialog.execute_dialog(dialog) if result == QDialog.Accepted: await self.generate_seed_from_permalink(dialog.permalink) async def _game_session_active(self) -> bool: if self.game_session_window is None or self.game_session_window.has_closed: return False else: await async_dialog.message_box( self, QtWidgets.QMessageBox.Critical, "Game Session in progress", "There's already a game session window open. Please close it first.", QMessageBox.Ok) self.game_session_window.activateWindow() return True async def _ensure_logged_in(self) -> bool: network_client = self.network_client if network_client.connection_state == ConnectionState.Connected: return True if network_client.connection_state.is_disconnected: message_box = QtWidgets.QMessageBox(QtWidgets.QMessageBox.NoIcon, "Connecting", "Connecting to server...", QtWidgets.QMessageBox.Cancel, self) connecting = network_client.connect_to_server() message_box.rejected.connect(connecting.cancel) message_box.show() try: await connecting finally: message_box.close() if network_client.current_user is None: await async_dialog.execute_dialog(LoginPromptDialog(network_client) ) return network_client.current_user is not None @asyncSlot() @handle_network_errors async def _browse_for_game_session(self): if await self._game_session_active(): return if not await self._ensure_logged_in(): return network_client = self.network_client browser = GameSessionBrowserDialog(network_client) await browser.refresh() if await async_dialog.execute_dialog(browser) == browser.Accepted: self.game_session_window = await GameSessionWindow.create_and_update( network_client, common_qt_lib.get_game_connection(), self.preset_manager, self, self._options) if self.game_session_window is not None: self.game_session_window.show() @asyncSlot() @handle_network_errors async def _action_login_window(self): if self._login_window is not None: return self._login_window.show() self._login_window = LoginPromptDialog(self.network_client) try: await async_dialog.execute_dialog(self._login_window) finally: self._login_window = None @asyncSlot() @handle_network_errors async def _host_game_session(self): if await self._game_session_active(): return if not await self._ensure_logged_in(): return dialog = QInputDialog(self) dialog.setModal(True) dialog.setWindowTitle("Enter session name") dialog.setLabelText("Select a name for the session:") if await async_dialog.execute_dialog(dialog) != dialog.Accepted: return await self.network_client.create_new_session(dialog.textValue()) self.game_session_window = await GameSessionWindow.create_and_update( self.network_client, common_qt_lib.get_game_connection(), self.preset_manager, self, self._options) if self.game_session_window is not None: self.game_session_window.show() def open_game_details(self, layout: LayoutDescription): self.GameDetailsSignal.emit(layout) 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) # Releases info async def request_new_data(self): await self._on_releases_data(await github_releases_data.get_releases()) async def _on_releases_data(self, releases: Optional[List[dict]]): 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: await async_dialog.message_box( self, QtWidgets.QMessageBox.Information, "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.menu_action_dark_mode.setChecked(self._options.dark_mode) self.generate_seed_tab.on_options_changed(self._options) theme.set_dark_theme(self._options.dark_mode) # Menu Actions def _open_data_visualizer_for_game(self, game: RandovaniaGame): self.open_data_visualizer_at(None, None, game) def open_data_visualizer_at( self, world_name: Optional[str], area_name: Optional[str], game: RandovaniaGame = RandovaniaGame.PRIME2, ): self._data_visualizer = DataEditorWindow.open_internal_data( game, 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_for_game(self, game: RandovaniaGame): self._data_editor = DataEditorWindow.open_internal_data(game, 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), database_path, False, True) self._data_editor.show() @asyncSlot() async def _on_menu_action_map_tracker(self): dialog = QtWidgets.QInputDialog(self) dialog.setWindowTitle("Map Tracker") dialog.setLabelText("Select preset used for the tracker.") dialog.setComboBoxItems( [preset.name for preset in self._preset_manager.all_presets]) dialog.setTextValue(self._options.selected_preset_name) result = await async_dialog.execute_dialog(dialog) if result == QtWidgets.QDialog.Accepted: preset = self._preset_manager.preset_for_name(dialog.textValue()) self.open_map_tracker(preset.get_preset().layout_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: TrickResourceInfo, level: LayoutTrickLevel): self._exec_trick_details( TrickDetailsPopup( self, self, default_database.default_prime2_game_description(), trick, level, )) def _setup_difficulties_menu(self): game = default_database.default_prime2_game_description() tricks_in_use = used_tricks(game) for trick in sorted(game.resource_database.trick, key=lambda _trick: _trick.long_name): if 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, trick) for i, trick_level in enumerate(iterate_enum(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)) # ========== @asyncSlot() async 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 = await async_dialog.execute_dialog(box) 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 _on_menu_action_dark_mode(self): with self._options as options: options.dark_mode = self.menu_action_dark_mode.isChecked() def _open_auto_tracker(self): from randovania.gui.auto_tracker_window import AutoTrackerWindow self.auto_tracker_window = AutoTrackerWindow( common_qt_lib.get_game_connection(), self._options) self.auto_tracker_window.show() def _on_menu_action_previously_generated_games(self): path = self._options.data_dir.joinpath("game_history") if platform.system() == "Windows": os.startfile(path) else: box = QtWidgets.QMessageBox( QtWidgets.QMessageBox.Information, "Game History", f"Previously generated games can be found at:\n{path}", QtWidgets.QMessageBox.Ok, self) box.setTextInteractionFlags(Qt.TextSelectableByMouse) box.show() def _on_menu_action_layout_editor(self): from randovania.gui.corruption_layout_editor import CorruptionLayoutEditor self.corruption_editor = CorruptionLayoutEditor() self.corruption_editor.show() def _update_hints_text(self): game_description = default_database.default_prime2_game_description() item_database = default_database.default_prime2_item_database() rows = [] for item in item_database.major_items.values(): rows.append(( item.name, item.item_category.hint_details[1], item.item_category.general_details[1], item.broad_category.hint_details[1], )) from randovania.games.prime.echoes_items import DARK_TEMPLE_KEY_NAMES for dark_temple_key in DARK_TEMPLE_KEY_NAMES: rows.append(( dark_temple_key.format("").strip(), ItemCategory.TEMPLE_KEY.hint_details[1], ItemCategory.TEMPLE_KEY.general_details[1], ItemCategory.KEY.hint_details[1], )) rows.append(( "Sky Temple Key", ItemCategory.SKY_TEMPLE_KEY.hint_details[1], ItemCategory.SKY_TEMPLE_KEY.general_details[1], ItemCategory.KEY.hint_details[1], )) for item in item_database.ammo.values(): rows.append(( item.name, ItemCategory.EXPANSION.hint_details[1], ItemCategory.EXPANSION.general_details[1], item.broad_category.hint_details[1], )) self.hint_item_names_tree_widget.setRowCount(len(rows)) for i, elements in enumerate(rows): for j, element in enumerate(elements): self.hint_item_names_tree_widget.setItem( i, j, QtWidgets.QTableWidgetItem(element)) for i in range(4): self.hint_item_names_tree_widget.resizeColumnToContents(i) 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)
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)