# library_entry.py # # Copyright 2025 The Bottles Contributors # # 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, in version 3 of the License. # # 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 . # from gettext import gettext as _ from gi.repository import Gtk, Gdk from bottles.backend.logger import Logger from bottles.backend.managers.library import LibraryManager from bottles.backend.managers.thumbnail import ThumbnailManager from bottles.backend.models.result import Result from bottles.backend.utils.threading import RunAsync from bottles.backend.wine.executor import WineExecutor from bottles.backend.wine.winedbg import WineDbg from bottles.frontend.gtk import GtkUtils logging = Logger() @Gtk.Template(resource_path="/com/usebottles/bottles/library-entry.ui") class LibraryEntry(Gtk.Box): __gtype_name__ = "LibraryEntry" # region Widgets btn_run = Gtk.Template.Child() btn_stop = Gtk.Template.Child() btn_launch_steam = Gtk.Template.Child() btn_remove = Gtk.Template.Child() label_name = Gtk.Template.Child() label_bottle = Gtk.Template.Child() label_no_cover = Gtk.Template.Child() img_cover = Gtk.Template.Child() revealer_run = Gtk.Template.Child() revealer_details = Gtk.Template.Child() overlay = Gtk.Template.Child() # endregion def __init__(self, library, uuid, entry, *args, **kwargs): super().__init__(*args, **kwargs) self.library = library self.window = library.window self.manager = library.window.manager self.name = entry["name"] self.uuid = uuid self.entry = entry self.config = self.__get_config() # This happens when a Library entry is an "orphan" (no bottles associated) if self.config is None: library_manager = LibraryManager() library_manager.remove_from_library(self.uuid) raise Exception self.program = self.__get_program() if len(entry["name"]) >= 15: name = entry["name"][:13] + "…" else: name = entry["name"] self.label_name.set_text(name) self.label_bottle.set_text(entry["bottle"]["name"]) self.label_no_cover.set_label(self.name) if entry.get("thumbnail"): path = ThumbnailManager.get_path(self.config, entry["thumbnail"]) if path is None: # redownloading *should* never fail as it was successfully downloaded before logging.info("Redownloading grid image...") library_manager = LibraryManager() result = library_manager.download_thumbnail(self.uuid, self.config) if result: entry = library_manager.get_library().get(uuid) path = ThumbnailManager.get_path(self.config, entry["thumbnail"]) if path is not None: # Gtk.Picture.set_pixbuf deprecated in GTK 4.12 texture = Gdk.Texture.new_from_filename(path) self.img_cover.set_paintable(texture) self.img_cover.set_visible(True) self.label_no_cover.set_visible(False) motion_ctrl = Gtk.EventControllerMotion.new() motion_ctrl.connect("enter", self.__on_motion_enter) motion_ctrl.connect("leave", self.__on_motion_leave) self.overlay.add_controller(motion_ctrl) self.btn_run.connect("clicked", self.run_executable) self.btn_launch_steam.connect("clicked", self.run_steam) self.btn_stop.connect("clicked", self.stop_process) self.btn_remove.connect("clicked", self.__remove_entry) def __get_config(self): bottles = self.manager.local_bottles if self.entry["bottle"]["name"] in bottles: return bottles[self.entry["bottle"]["name"]] parent = self.get_parent() if parent: parent.remove(self) # TODO: Remove from list def __get_program(self): programs = self.manager.get_programs(self.config) programs = [ p for p in programs if p["id"] == self.entry["id"] or p["name"] == self.entry["name"] ] if len(programs) == 0: return None # TODO: remove entry from library return programs[0] @GtkUtils.run_in_main_loop def __reset_buttons(self, result: Result | bool = None, error=False): match result: case Result(): status = result.status case bool(): status = result case _: logging.error( f"result should be Result or bool, but it was {type(result)}" ) status = False self.btn_remove.set_visible(status) self.btn_stop.set_visible(not status) self.btn_run.set_visible(status) def __is_alive(self): winedbg = WineDbg(self.config) @GtkUtils.run_in_main_loop def set_watcher(result=False, error=False): nonlocal winedbg self.__reset_buttons() RunAsync( winedbg.wait_for_process, callback=self.__reset_buttons, name=self.program["executable"], timeout=5, ) RunAsync( winedbg.is_process_alive, callback=set_watcher, name=self.program["executable"], ) def __remove_entry(self, *args): self.library.remove_entry(self) def run_executable(self, widget, with_terminal=False): self.window.show_toast(_('Launching "{0}"…').format(self.program["name"])) RunAsync( WineExecutor.run_program, callback=self.__reset_buttons, config=self.config, program=self.program, ) self.__reset_buttons() def run_steam(self, widget): self.manager.steam_manager.launch_app(self.config.CompatData) def stop_process(self, widget): self.window.show_toast(_('Stopping "{0}"…').format(self.program["name"])) winedbg = WineDbg(self.config) winedbg.kill_process(name=self.program["executable"]) self.__reset_buttons(True) def __on_motion_enter(self, *args): self.revealer_run.set_reveal_child(True) self.revealer_details.set_reveal_child(True) def __on_motion_leave(self, *args): self.revealer_run.set_reveal_child(False) self.revealer_details.set_reveal_child(False) # hide() and show() are essentialy workarounds to avoid keeping # the empty space of the hidden entry in the GtkFlowBox def hide(self): self.get_parent().set_visible(False) def show(self): self.get_parent().set_visible(True)