# details_window.py # # Copyright 2022-2023 kramo # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # SPDX-License-Identifier: GPL-3.0-or-later import os import shlex from pathlib import Path from time import time from typing import Any, Optional from gi.repository import Adw, Gio, GLib, Gtk from PIL import Image, UnidentifiedImageError from src import shared from src.errors.friendly_error import FriendlyError from src.game import Game from src.game_cover import GameCover from src.store.managers.cover_manager import CoverManager from src.store.managers.sgdb_manager import SGDBManager from src.utils.create_dialog import create_dialog from src.utils.save_cover import convert_cover, save_cover @Gtk.Template(resource_path=shared.PREFIX + "/gtk/details-window.ui") class DetailsWindow(Adw.Window): __gtype_name__ = "DetailsWindow" cover_overlay = Gtk.Template.Child() cover = Gtk.Template.Child() cover_button_edit = Gtk.Template.Child() cover_button_delete_revealer = Gtk.Template.Child() cover_button_delete = Gtk.Template.Child() spinner = Gtk.Template.Child() name = Gtk.Template.Child() developer = Gtk.Template.Child() executable = Gtk.Template.Child() exec_info_label = Gtk.Template.Child() exec_info_popover = Gtk.Template.Child() file_chooser_button = Gtk.Template.Child() apply_button = Gtk.Template.Child() cover_changed: bool = False def __init__(self, game: Optional[Game] = None, **kwargs: Any): super().__init__(**kwargs) self.game: Game = game self.game_cover: GameCover = GameCover({self.cover}) self.set_transient_for(shared.win) if self.game: self.set_title(_("Game Details")) self.name.set_text(self.game.name) if self.game.developer: self.developer.set_text(self.game.developer) self.executable.set_text(self.game.executable) self.apply_button.set_label(_("Apply")) self.game_cover.new_cover(self.game.get_cover_path()) if self.game_cover.get_texture(): self.cover_button_delete_revealer.set_reveal_child(True) else: self.set_title(_("Add New Game")) self.apply_button.set_label(_("Add")) image_filter = Gtk.FileFilter(name=_("Images")) for extension in Image.registered_extensions(): image_filter.add_suffix(extension[1:]) image_filter.add_suffix("svg") # Gdk.Texture supports .svg but PIL doesn't image_filters = Gio.ListStore.new(Gtk.FileFilter) image_filters.append(image_filter) exec_filter = Gtk.FileFilter(name=_("Executables")) exec_filter.add_mime_type("application/x-executable") exec_filters = Gio.ListStore.new(Gtk.FileFilter) exec_filters.append(exec_filter) self.image_file_dialog = Gtk.FileDialog() self.image_file_dialog.set_filters(image_filters) self.image_file_dialog.set_default_filter(image_filter) self.exec_file_dialog = Gtk.FileDialog() self.exec_file_dialog.set_filters(exec_filters) self.exec_file_dialog.set_default_filter(exec_filter) # Translate this string as you would translate "file" file_name = _("file.txt") # As in software exe_name = _("program") if os.name == "nt": exe_name += ".exe" # Translate this string as you would translate "path to {}" exe_path = _("C:\\path\\to\\{}").format(exe_name) # Translate this string as you would translate "path to {}" file_path = _("C:\\path\\to\\{}").format(file_name) command = "start" else: # Translate this string as you would translate "path to {}" exe_path = _("/path/to/{}").format(exe_name) # Translate this string as you would translate "path to {}" file_path = _("/path/to/{}").format(file_name) command = "xdg-open" # pylint: disable=line-too-long exec_info_text = _( 'To launch the executable "{}", use the command:\n\n"{}"\n\nTo open the file "{}" with the default application, use:\n\n{} "{}"\n\nIf the path contains spaces, make sure to wrap it in double quotes!' ).format(exe_name, exe_path, file_name, command, file_path) self.exec_info_label.set_label(exec_info_text) self.exec_info_popover.update_property( (Gtk.AccessibleProperty.LABEL,), ( exec_info_text.replace("", "").replace("", ""), ), # Remove formatting, else the screen reader reads it ) def set_exec_info_a11y_label(*_args: Any) -> None: self.set_focus(self.exec_info_popover) self.exec_info_popover.connect("show", set_exec_info_a11y_label) self.cover_button_delete.connect("clicked", self.delete_pixbuf) self.cover_button_edit.connect("clicked", self.choose_cover) self.file_chooser_button.connect("clicked", self.choose_executable) self.apply_button.connect("clicked", self.apply_preferences) self.name.connect("entry-activated", self.focus_executable) self.developer.connect("entry-activated", self.focus_executable) self.executable.connect("entry-activated", self.apply_preferences) self.set_focus(self.name) self.present() def delete_pixbuf(self, *_args: Any) -> None: self.game_cover.new_cover() self.cover_button_delete_revealer.set_reveal_child(False) self.cover_changed = True def apply_preferences(self, *_args: Any) -> None: final_name = self.name.get_text() final_developer = self.developer.get_text() final_executable = self.executable.get_text() if not self.game: if final_name == "": create_dialog( self, _("Couldn't Add Game"), _("Game title cannot be empty.") ) return if final_executable == "": create_dialog( self, _("Couldn't Add Game"), _("Executable cannot be empty.") ) return # Increment the number after the game id (eg. imported_1, imported_2) source_id = "imported" numbers = [0] game_id: str for game_id in shared.store.source_games.get(source_id, set()): prefix = "imported_" if not game_id.startswith(prefix): continue numbers.append(int(game_id.replace(prefix, "", 1))) game_number = max(numbers) + 1 self.game = Game( { "game_id": f"imported_{game_number}", "hidden": False, "source": source_id, "added": int(time()), } ) else: if final_name == "": create_dialog( self, _("Couldn't Apply Preferences"), _("Game title cannot be empty."), ) return if final_executable == "": create_dialog( self, _("Couldn't Apply Preferences"), _("Executable cannot be empty."), ) return self.game.name = final_name self.game.developer = final_developer or None self.game.executable = final_executable if self.game.game_id in shared.win.game_covers.keys(): shared.win.game_covers[self.game.game_id].animation = None shared.win.game_covers[self.game.game_id] = self.game_cover if self.cover_changed: save_cover( self.game.game_id, self.game_cover.path, ) shared.store.add_game(self.game, {}, run_pipeline=False) self.game.save() self.game.update() # TODO: this is fucked up (less than before) # Get a cover from SGDB if none is present if not self.game_cover.get_texture(): self.game.set_loading(1) sgdb_manager: SGDBManager = shared.store.managers[SGDBManager] sgdb_manager.reset_cancellable() sgdb_manager.process_game(self.game, {}, self.update_cover_callback) self.game_cover.pictures.remove(self.cover) self.close() shared.win.show_details_view(self.game) def update_cover_callback(self, manager: SGDBManager) -> None: # Set the game as not loading self.game.set_loading(-1) self.game.update() # Handle errors that occured for error in manager.collect_errors(): # On auth error, inform the user if isinstance(error, FriendlyError): create_dialog( shared.win, error.title, error.subtitle, "open_preferences", _("Preferences"), ).connect("response", self.update_cover_error_response) def update_cover_error_response(self, _widget: Any, response: str) -> None: if response == "open_preferences": shared.win.get_application().on_preferences_action(page_name="sgdb") def focus_executable(self, *_args: Any) -> None: self.set_focus(self.executable) def toggle_loading(self) -> None: self.apply_button.set_sensitive(not self.apply_button.get_sensitive()) self.spinner.set_spinning(not self.spinner.get_spinning()) self.cover_overlay.set_opacity(not self.cover_overlay.get_opacity()) def set_cover(self, _source: Any, result: Gio.Task, *_args: Any) -> None: try: path = self.image_file_dialog.open_finish(result).get_path() except GLib.GError: return def thread_func() -> None: new_path = None try: with Image.open(path) as image: if getattr(image, "is_animated", False): new_path = convert_cover(path) except UnidentifiedImageError: pass if not new_path: new_path = convert_cover( pixbuf=shared.store.managers[CoverManager].composite_cover( Path(path) ) ) if new_path: self.game_cover.new_cover(new_path) self.cover_button_delete_revealer.set_reveal_child(True) self.cover_changed = True self.toggle_loading() self.toggle_loading() GLib.Thread.new(None, thread_func) def set_executable(self, _source: Any, result: Gio.Task, *_args: Any) -> None: try: path = self.exec_file_dialog.open_finish(result).get_path() except GLib.GError: return self.executable.set_text(shlex.quote(path)) def choose_executable(self, *_args: Any) -> None: self.exec_file_dialog.open(self, None, self.set_executable) def choose_cover(self, *_args: Any) -> None: self.image_file_dialog.open(self, None, self.set_cover)