Move DetailsWindow to its own class

This commit is contained in:
kramo
2023-04-20 01:53:22 +02:00
parent 5fd0cdf416
commit 1e3df843ba
9 changed files with 390 additions and 368 deletions

236
src/details_window.py Normal file
View File

@@ -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 <http://www.gnu.org/licenses/>.
#
# 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<tt>"{}"</tt>\n\nTo open the file "{}" with the default application, use:\n\n<tt>{} "{}"</tt>\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)

View File

@@ -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)

View File

@@ -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'
]

View File

@@ -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 <http://www.gnu.org/licenses/>.
#
# 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<tt>"{}"</tt>\n\nTo open the file "{}" with the default application, use:\n\n<tt>{} "{}"</tt>\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()

View File

@@ -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)

View File

@@ -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
):