From 1e3df843ba7562fd51bfe42aab9df591211dba83 Mon Sep 17 00:00:00 2001 From: kramo <93832451+kra-mo@users.noreply.github.com> Date: Thu, 20 Apr 2023 01:53:22 +0200 Subject: [PATCH] Move DetailsWindow to its own class --- data/cartridges.gresource.xml | 1 + data/gtk/details_window.blp | 136 +++++++++++ data/meson.build | 3 +- src/details_window.py | 236 +++++++++++++++++++ src/main.py | 6 +- src/meson.build | 2 +- src/utils/create_details_window.py | 351 ----------------------------- src/utils/importer.py | 4 +- src/utils/steamgriddb.py | 19 +- 9 files changed, 390 insertions(+), 368 deletions(-) create mode 100644 data/gtk/details_window.blp create mode 100644 src/details_window.py delete mode 100644 src/utils/create_details_window.py diff --git a/data/cartridges.gresource.xml b/data/cartridges.gresource.xml index bdbd1b0..afa550b 100644 --- a/data/cartridges.gresource.xml +++ b/data/cartridges.gresource.xml @@ -5,6 +5,7 @@ gtk/help-overlay.ui gtk/game.ui gtk/preferences.ui + gtk/details_window.ui gtk/style.css gtk/style-dark.css library_placeholder.svg diff --git a/data/gtk/details_window.blp b/data/gtk/details_window.blp new file mode 100644 index 0000000..3efd2df --- /dev/null +++ b/data/gtk/details_window.blp @@ -0,0 +1,136 @@ +using Gtk 4.0; +using Adw 1; + +template DetailsWindow : Adw.Window { + default-width: 500; + default-height: -1; + modal: true; + + ShortcutController { + Shortcut { + trigger: "Escape"; + action: "action(window.close)"; + } + } + + Box { + orientation: vertical; + + Adw.HeaderBar HeaderBar { + show-start-title-buttons: false; + show-end-title-buttons: false; + + [start] + Button cancel_button { + label: _("Cancel"); + action-name: "window.close"; + } + + [end] + Button apply_button { + styles [ + "suggested-action" + ] + } + } + + Adw.PreferencesPage { + vexpand: true; + + Adw.PreferencesGroup cover_group { + Adw.Clamp cover_clamp { + maximum-size: 200; + Overlay cover_overlay { + halign: center; + valign: center; + + [overlay] + Button cover_button_edit { + icon-name: "document-edit-symbolic"; + halign: end; + valign: end; + margin-bottom: 6; + margin-end: 6; + + styles [ + "circular", "osd" + ] + } + + [overlay] + Revealer cover_button_delete_revealer { + transition-type: crossfade; + margin-end: 40; + + Button cover_button_delete { + icon-name: "user-trash-symbolic"; + halign: end; + valign: end; + margin-bottom: 6; + margin-end: 6; + + styles [ + "circular", "osd" + ] + } + } + + Picture cover { + width-request: 200; + height-request: 300; + + styles [ + "card" + ] + } + } + } + } + + Adw.PreferencesGroup title_group { + title: _("Title"); + description: _("The title of the game"); + + Entry name {} + } + + Adw.PreferencesGroup developer_group { + title: _("Developer"); + description: _("The developer or publisher (optional)"); + + Entry developer {} + } + + Adw.PreferencesGroup exec_group { + title: _("Executable"); + description: _("File to open or command to run when launching the game"); + + [header-suffix] + Gtk.MenuButton exec-info-button { + valign: center; + icon-name: "help-about-symbolic"; + + popover: Popover { + visible: bind exec-info-button.active bidirectional; + + Label exec_info_label { + use-markup: true; + wrap: true; + max-width-chars: 30; + margin-top: 6; + margin-bottom: 12; + margin-start: 6; + margin-end: 6; + } + }; + + styles [ + "flat" + ] + } + + Entry executable {} + } + } + } +} \ No newline at end of file diff --git a/data/meson.build b/data/meson.build index b57150e..df0f8b8 100644 --- a/data/meson.build +++ b/data/meson.build @@ -3,7 +3,8 @@ blueprints = custom_target('blueprints', 'gtk/help-overlay.blp', 'gtk/window.blp', 'gtk/game.blp', - 'gtk/preferences.blp' + 'gtk/preferences.blp', + 'gtk/details_window.blp' ), output: '.', command: [find_program('blueprint-compiler'), 'batch-compile', '@OUTPUT@', '@CURRENT_SOURCE_DIR@', '@INPUT@'], diff --git a/src/details_window.py b/src/details_window.py new file mode 100644 index 0000000..af273aa --- /dev/null +++ b/src/details_window.py @@ -0,0 +1,236 @@ +# 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 time import time + +from gi.repository import Adw, Gio, GLib, Gtk +from PIL import Image + +from .create_dialog import create_dialog +from .game import Game +from .game_cover import GameCover +from .save_cover import resize_cover, save_cover +from .steamgriddb import SGDBSave + + +@Gtk.Template(resource_path="/hu/kramo/Cartridges/gtk/details_window.ui") +class DetailsWindow(Adw.Window): + __gtype_name__ = "DetailsWindow" + + cover = Gtk.Template.Child() + cover_button_edit = Gtk.Template.Child() + cover_button_delete_revealer = Gtk.Template.Child() + cover_button_delete = Gtk.Template.Child() + + name = Gtk.Template.Child() + developer = Gtk.Template.Child() + executable = Gtk.Template.Child() + + exec_info_label = Gtk.Template.Child() + + apply_button = Gtk.Template.Child() + + cover_changed = False + + def delete_pixbuf(self, *_args): + self.game_cover.new_cover() + + self.cover_button_delete_revealer.set_reveal_child(False) + self.cover_changed = True + + def __init__(self, win, game=None, **kwargs): + super().__init__(**kwargs) + self.set_transient_for(win) + + self.win = win + self.game = game + self.game_cover = GameCover({self.cover}) + + if self.game: + self.set_title(_("Edit Game Details")) + self.name.set_text(self.game.name) + self.developer.set_text(self.game.developer) + self.executable.set_text(shlex.join(self.game.executable)) + self.apply_button.set_label(_("Apply")) + + self.game_cover.new_cover(self.game.get_cover_path()) + if self.game_cover.get_pixbuf(): + self.cover_button_delete_revealer.set_reveal_child(True) + else: + self.set_title(_("Add New Game")) + self.apply_button.set_label(_("Confirm")) + + image_filter = Gtk.FileFilter(name=_("Images")) + for extension in Image.registered_extensions(): + image_filter.add_suffix(extension[1:]) + + file_filters = Gio.ListStore.new(Gtk.FileFilter) + file_filters.append(image_filter) + self.file_dialog = Gtk.FileDialog() + self.file_dialog.set_filters(file_filters) + + # 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" + + 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.cover_button_delete.connect("clicked", self.delete_pixbuf) + self.cover_button_edit.connect("clicked", self.choose_cover) + self.apply_button.connect("clicked", self.apply_preferences) + + self.name.connect("activate", self.focus_executable) + self.developer.connect("activate", self.focus_executable) + self.executable.connect("activate", self.apply_preferences) + + self.set_focus(self.name) + self.present() + + def apply_preferences(self, *_args): + final_name = self.name.get_text() + final_developer = self.developer.get_text() + final_executable = self.executable.get_text() + + try: + # Attempt to parse using shell parsing rules (doesn't verify executable existence). + final_executable_split = shlex.split( + final_executable, comments=False, posix=True + ) + except ValueError as exception: + create_dialog( + self, + _("Couldn't Add Game") + if not self.game + else _("Couldn't Apply Preferences"), + f'{_("Executable")}: {exception}.', + ) + return + + 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) + + numbers = [0] + + for current_game in self.win.games: + if "imported_" in current_game: + numbers.append(int(current_game.replace("imported_", ""))) + + self.game = Game( + self.win, + { + "game_id": f"imported_{str(max(numbers) + 1)}", + "hidden": False, + "source": "imported", + "added": int(time()), + "last_played": 0, + }, + ) + + 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_split + + if self.game.game_id in self.win.game_covers.keys(): + self.win.game_covers[self.game.game_id].animation = None + + self.win.game_covers[self.game.game_id] = self.game_cover + + if self.cover_changed: + save_cover( + self.win, + self.game.game_id, + self.game_cover.path, + ) + + self.game.save() + + if not self.game_cover.get_pixbuf(): + SGDBSave(self.win, {self.game}) + + self.game_cover.pictures.remove(self.cover) + + self.close() + self.win.show_details_view(self.game) + + def focus_executable(self): + self.set_focus(self.executable) + + def set_cover(self, _source, result, *_args): + try: + path = self.file_dialog.open_finish(result).get_path() + except GLib.GError: + return + + self.cover_button_delete_revealer.set_reveal_child(True) + self.cover_changed = True + + self.game_cover.new_cover(resize_cover(self.win, path)) + + def choose_cover(self, *_args): + self.file_dialog.open(self, None, self.set_cover) diff --git a/src/main.py b/src/main.py index c6f7437..8d10c95 100644 --- a/src/main.py +++ b/src/main.py @@ -28,7 +28,7 @@ gi.require_version("Adw", "1") from gi.repository import Adw, Gio, GLib, Gtk from .bottles_importer import bottles_importer -from .create_details_window import create_details_window +from .details_window import DetailsWindow from .heroic_importer import heroic_importer from .importer import Importer from .itch_importer import itch_importer @@ -143,10 +143,10 @@ class CartridgesApplication(Adw.Application): self.win.active_game.toggle_hidden() def on_edit_game_action(self, *_args): - create_details_window(self.win, self.win.active_game) + DetailsWindow(self.win, self.win.active_game) def on_add_game_action(self, *_args): - create_details_window(self.win) + DetailsWindow(self.win) def on_import_action(self, *_args): self.win.importer = Importer(self.win) diff --git a/src/meson.build b/src/meson.build index 409003a..cfb6769 100644 --- a/src/meson.build +++ b/src/meson.build @@ -21,6 +21,7 @@ cartridges_sources = [ 'main.py', 'window.py', 'preferences.py', + 'details_window.py', 'game.py', 'game_cover.py', 'importers/steam_importer.py', @@ -32,7 +33,6 @@ cartridges_sources = [ 'utils/steamgriddb.py', 'utils/save_cover.py', 'utils/create_dialog.py', - 'utils/create_details_window.py', 'utils/check_install.py' ] diff --git a/src/utils/create_details_window.py b/src/utils/create_details_window.py deleted file mode 100644 index d93f430..0000000 --- a/src/utils/create_details_window.py +++ /dev/null @@ -1,351 +0,0 @@ -# create_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 time import time - -from gi.repository import Adw, Gio, GLib, GObject, Gtk -from PIL import Image - -from .create_dialog import create_dialog -from .game import Game -from .game_cover import GameCover -from .save_cover import resize_cover, save_cover -from .steamgriddb import SGDBSave - - -def create_details_window(win, game=None): - window = Adw.Window( - modal=True, default_width=500, default_height=-1, transient_for=win - ) - - cover_changed = False - - cover_button_edit = Gtk.Button( - icon_name="document-edit-symbolic", - halign=Gtk.Align.END, - valign=Gtk.Align.END, - margin_bottom=6, - margin_end=6, - css_classes=["circular", "osd"], - ) - - cover_button_delete = Gtk.Button( - icon_name="user-trash-symbolic", - css_classes=["circular", "osd"], - halign=Gtk.Align.END, - valign=Gtk.Align.END, - margin_bottom=6, - margin_end=6, - ) - - def delete_pixbuf(*_args): - nonlocal game_cover - nonlocal cover_changed - - game_cover.new_cover() - - cover_button_delete_revealer.set_reveal_child(False) - cover_changed = True - - cover_button_delete.connect("clicked", delete_pixbuf) - - cover_button_delete_revealer = Gtk.Revealer( - child=cover_button_delete, - transition_type=Gtk.RevealerTransitionType.CROSSFADE, - margin_end=40, - ) - - cover = Gtk.Picture.new() - game_cover = GameCover({cover}) - - if game: - window.set_title(_("Edit Game Details")) - developer = Gtk.Entry.new_with_buffer(Gtk.EntryBuffer.new(game.developer, -1)) - name = Gtk.Entry.new_with_buffer(Gtk.EntryBuffer.new(game.name, -1)) - executable = Gtk.Entry.new_with_buffer( - Gtk.EntryBuffer.new(shlex.join(game.executable), -1) - ) - apply_button = Gtk.Button.new_with_label(_("Apply")) - - game_cover.new_cover(game.get_cover_path()) - if game_cover.pixbuf: - cover_button_delete_revealer.set_reveal_child(True) - else: - window.set_title(_("Add New Game")) - name = Gtk.Entry() - developer = Gtk.Entry() - executable = Gtk.Entry() - apply_button = Gtk.Button.new_with_label(_("Confirm")) - - image_filter = Gtk.FileFilter(name=_("Images")) - for extension in Image.registered_extensions(): - image_filter.add_suffix(extension[1:]) - - file_filters = Gio.ListStore.new(Gtk.FileFilter) - file_filters.append(image_filter) - filechooser = Gtk.FileDialog() - filechooser.set_filters(file_filters) - - cover.add_css_class("card") - cover.set_size_request(200, 300) - - cover_overlay = Gtk.Overlay( - child=cover, - halign=Gtk.Align.CENTER, - valign=Gtk.Align.CENTER, - ) - - cover_overlay.add_overlay(cover_button_edit) - cover_overlay.add_overlay(cover_button_delete_revealer) - - cover_clamp = Adw.Clamp( - maximum_size=200, - child=cover_overlay, - ) - - cover_group = Adw.PreferencesGroup() - cover_group.add(cover_clamp) - - title_group = Adw.PreferencesGroup( - title=_("Title"), - description=_("The title of the game"), - ) - title_group.add(name) - - developer_group = Adw.PreferencesGroup( - title=_("Developer"), - description=_("The developer or publisher (optional)"), - ) - developer_group.add(developer) - - exec_info_button = Gtk.ToggleButton( - icon_name="help-about-symbolic", - valign=Gtk.Align.CENTER, - css_classes=["flat"], - ) - - # 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" - - 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) - - exec_info_label = Gtk.Label( - label=exec_info_text, - use_markup=True, - wrap=True, - max_width_chars=30, - margin_top=6, - margin_bottom=12, - margin_start=6, - margin_end=6, - ) - - exec_info_popover = Gtk.Popover( - position=Gtk.PositionType.TOP, child=exec_info_label - ) - - exec_info_popover.bind_property( - "visible", exec_info_button, "active", GObject.BindingFlags.BIDIRECTIONAL - ) - - exec_group = Adw.PreferencesGroup( - title=_("Executable"), - description=_("File to open or command to run when launching the game"), - header_suffix=exec_info_button, - ) - exec_info_popover.set_parent(exec_group.get_header_suffix()) - exec_group.add(executable) - - general_page = Adw.PreferencesPage(vexpand=True) - general_page.add(cover_group) - general_page.add(title_group) - general_page.add(developer_group) - general_page.add(exec_group) - - cancel_button = Gtk.Button.new_with_label(_("Cancel")) - - apply_button.add_css_class("suggested-action") - - header_bar = Adw.HeaderBar( - show_start_title_buttons=False, - show_end_title_buttons=False, - ) - header_bar.pack_start(cancel_button) - header_bar.pack_end(apply_button) - - main_box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) - main_box.append(header_bar) - main_box.append(general_page) - window.set_content(main_box) - - def choose_cover(*_args): - filechooser.open(window, None, set_cover, None) - - def set_cover(_source, result, *_args): - nonlocal game_cover - nonlocal cover_changed - - try: - path = filechooser.open_finish(result).get_path() - except GLib.GError: - return - - cover_button_delete_revealer.set_reveal_child(True) - cover_changed = True - - game_cover.new_cover(resize_cover(win, path)) - - def close_window(*_args): - window.close() - - def apply_preferences(*_args): - nonlocal cover_changed - nonlocal game - nonlocal cover - - final_name = name.get_buffer().get_text() - final_developer = developer.get_buffer().get_text() - final_executable = executable.get_buffer().get_text() - - try: - # Attempt to parse using shell parsing rules (doesn't verify executable existence). - final_executable_split = shlex.split( - final_executable, comments=False, posix=True - ) - except ValueError as exception: - create_dialog( - window, - _("Couldn't Add Game") if not game else _("Couldn't Apply Preferences"), - f'{_("Executable")}: {exception}.', - ) - return - - if not game: - if final_name == "": - create_dialog( - window, _("Couldn't Add Game"), _("Game title cannot be empty.") - ) - return - - if final_executable == "": - create_dialog( - window, _("Couldn't Add Game"), _("Executable cannot be empty.") - ) - return - - # Increment the number after the game id (eg. imported_1, imported_2) - - numbers = [0] - - for current_game in win.games: - if "imported_" in current_game: - numbers.append(int(current_game.replace("imported_", ""))) - - game = Game( - win, - { - "game_id": f"imported_{str(max(numbers) + 1)}", - "hidden": False, - "source": "imported", - "added": int(time()), - "last_played": 0, - }, - ) - - else: - if final_name == "": - create_dialog( - window, - _("Couldn't Apply Preferences"), - _("Game title cannot be empty."), - ) - return - - if final_executable == "": - create_dialog( - window, - _("Couldn't Apply Preferences"), - _("Executable cannot be empty."), - ) - return - - game.name = final_name - game.developer = final_developer or None - game.executable = final_executable_split - - win.game_covers[game.game_id] = game_cover - - if cover_changed: - save_cover( - win, - game.game_id, - game_cover.path, - ) - - game.save() - - if not game_cover.pixbuf: - SGDBSave(win, {game}) - - game.game_cover.pictures.remove(cover) - - window.close() - win.show_details_view(game) - - def focus_executable(*_args): - window.set_focus(executable) - - cover_button_edit.connect("clicked", choose_cover) - cancel_button.connect("clicked", close_window) - apply_button.connect("clicked", apply_preferences) - name.connect("activate", focus_executable) - developer.connect("activate", focus_executable) - executable.connect("activate", apply_preferences) - - shortcut_controller = Gtk.ShortcutController() - shortcut_controller.add_shortcut( - Gtk.Shortcut.new( - Gtk.ShortcutTrigger.parse_string("Escape"), - Gtk.CallbackAction.new(close_window), - ) - ) - - window.add_controller(shortcut_controller) - window.set_focus(name) - window.present() diff --git a/src/utils/importer.py b/src/utils/importer.py index 736adfb..e64648d 100644 --- a/src/utils/importer.py +++ b/src/utils/importer.py @@ -24,7 +24,7 @@ from gi.repository import Adw, Gtk from .create_dialog import create_dialog from .game import Game from .save_cover import resize_cover, save_cover -from .steamgriddb import SGDBSave, needs_cover +from .steamgriddb import SGDBSave class Importer: @@ -58,7 +58,7 @@ class Importer: if values: game = Game(self.win, values) - if not needs_cover(self.win.schema, cover_path): + if save_cover: save_cover(self.win, game.game_id, resize_cover(self.win, cover_path)) self.games.add(game) diff --git a/src/utils/steamgriddb.py b/src/utils/steamgriddb.py index 1ea929e..b454aba 100644 --- a/src/utils/steamgriddb.py +++ b/src/utils/steamgriddb.py @@ -7,12 +7,6 @@ from .create_dialog import create_dialog from .save_cover import save_cover, resize_cover -def needs_cover(schema, previous): - return schema.get_boolean("sgdb") and ( - (schema.get_boolean("sgdb-prefer")) or not previous - ) - - class SGDBSave: def __init__(self, win, games, importer=None): self.win = win @@ -34,10 +28,15 @@ class SGDBSave: def update_cover(self, task, game): if ( - not needs_cover( - self.win.schema, - (self.win.covers_dir / f"{game.game_id}.gif").is_file() - or (self.win.covers_dir / f"{game.game_id}.tiff").is_file(), + not ( + self.win.schema.get_boolean("sgdb") + and ( + (self.win.schema.get_boolean("sgdb-prefer")) + or not ( + (self.win.covers_dir / f"{game.game_id}.gif").is_file() + or (self.win.covers_dir / f"{game.game_id}.tiff").is_file() + ) + ) ) or game.blacklisted ):