From 5e30eaae9cea9d4d4db30984b573ac47d149ec8e Mon Sep 17 00:00:00 2001 From: William Edwards Date: Sat, 27 Sep 2025 20:31:41 -0700 Subject: [PATCH] fix(Focus): refactor window focus logic --- core/global/launch_manager.gd | 38 +-- core/systems/effects/fade_effect.gd | 1 + core/systems/launcher/running_app.gd | 312 ++++++------------ core/ui/card_ui/launch/game_launch_menu.gd | 1 + .../card_ui/navigation/running_game_card.gd | 88 +++-- core/ui/common/game/game_loading.gd | 6 +- core/ui/common/game/game_loading.tscn | 27 +- 7 files changed, 185 insertions(+), 288 deletions(-) diff --git a/core/global/launch_manager.gd b/core/global/launch_manager.gd index e2b943be2..83389a75d 100644 --- a/core/global/launch_manager.gd +++ b/core/global/launch_manager.gd @@ -64,7 +64,7 @@ var _persist_path: String = "/".join([_data_dir, "launcher.json"]) var _persist_data: Dictionary = {"version": 1} var _ogui_window_id := 0 var should_manage_overlay := true -var logger := Log.get_logger("LaunchManager", Log.LEVEL.INFO) +var logger := Log.get_logger("LaunchManager", Log.LEVEL.DEBUG) var _focused_app_id := 0 @@ -126,19 +126,6 @@ func _init() -> void: return logger.debug("Focusable apps changed from", from, "to", to) self.check_running.call_deferred() - # If focusable apps has changed and the currently focused app no longer exists, - # remove the manual focus - const keep_app_ids := [GamescopeInstance.EXTRA_UNKNOWN_GAME_ID, GamescopeInstance.OVERLAY_GAME_ID] - var baselayer_apps := _xwayland_primary.baselayer_apps - var new_baselayer_apps := PackedInt64Array() - for app_id in baselayer_apps: - if app_id in keep_app_ids: - new_baselayer_apps.push_back(app_id) - continue - if app_id in to: - new_baselayer_apps.push_back(app_id) - if new_baselayer_apps != baselayer_apps: - _xwayland_primary.baselayer_apps = new_baselayer_apps _xwayland_primary.focusable_apps_updated.connect(on_focusable_apps_changed) # Listen for when focusable windows change @@ -169,7 +156,7 @@ func _init() -> void: if _xwayland_ogui: logger.debug("Enabling STEAM_OVERLAY atom") - _xwayland_ogui.set_overlay(_ogui_window_id, 0) + _xwayland_ogui.set_overlay(_ogui_window_id, 1) var on_game_state_exited := func(_to: State): # Set the gamepad profile to the global profile @@ -221,11 +208,10 @@ func launch(app: LibraryLaunchItem) -> RunningApp: # Add the running app to our list and change to the IN_GAME state _add_running(running_app) state_machine.set_state([in_game_state]) - _update_recent_apps(app) + update_recent_apps(app) # Execute any pre-launch hooks and start the app await _execute_hooks(app, AppLifecycleHook.TYPE.PRE_LAUNCH) - running_app.start() # Call any hooks at different points in the app's lifecycle var on_app_state_changed := func(_from: RunningApp.STATE, to: RunningApp.STATE): @@ -251,13 +237,17 @@ func launch(app: LibraryLaunchItem) -> RunningApp: var focused_window := _xwayland_primary.baselayer_window if not focused_window in old_windows: return - if new_windows.is_empty(): + if not focused_window in new_windows: _xwayland_primary.remove_baselayer_window() return + # TODO: Use the most recently created window instead of just the first in the list var new_window := new_windows[0] running_app.switch_window(new_window) running_app.window_ids_changed.connect(remove_focus) + # Run the application + running_app.start() + return running_app @@ -400,7 +390,7 @@ func get_recent_apps() -> Array: # Updates our list of recently launched apps -func _update_recent_apps(app: LibraryLaunchItem) -> void: +func update_recent_apps(app: LibraryLaunchItem) -> void: if not "recent" in _persist_data: _persist_data["recent"] = [] var recent: Array = _persist_data["recent"] @@ -619,24 +609,22 @@ func check_running() -> void: _update_pids(all_valid_windows) # Update the state of all running apps + var windows_with_app := PackedInt64Array() + var orphan_windows := PackedInt64Array() for app in _running: var app_pids := PackedInt64Array() if app.ogui_id in ogui_id_to_pids: app_pids = ogui_id_to_pids[app.ogui_id] app.update(all_valid_windows, app_pids) + windows_with_app.append_array(app.window_ids.duplicate()) for app in _running_background: var app_pids := PackedInt64Array() if app.ogui_id in ogui_id_to_pids: app_pids = ogui_id_to_pids[app.ogui_id] app.update(all_valid_windows, app_pids) + windows_with_app.append_array(app.window_ids.duplicate()) # Look for orphan windows - var windows_with_app := PackedInt64Array() - var orphan_windows := PackedInt64Array() - for app in _running: - windows_with_app.append_array(app.window_ids.duplicate()) - for app in _running_background: - windows_with_app.append_array(app.window_ids.duplicate()) for window in all_valid_windows: if window in windows_with_app: continue diff --git a/core/systems/effects/fade_effect.gd b/core/systems/effects/fade_effect.gd index eed6b54b8..05e8c86f2 100644 --- a/core/systems/effects/fade_effect.gd +++ b/core/systems/effects/fade_effect.gd @@ -62,6 +62,7 @@ func fade_out() -> void: tween.tween_property(target, "visible", false, 0) var on_finished := func(): fade_out_finished.emit() + target.modulate = Color(1, 1, 1, 1) tween.tween_callback(on_finished) diff --git a/core/systems/launcher/running_app.gd b/core/systems/launcher/running_app.gd index a0b36a61f..39adaeb5e 100644 --- a/core/systems/launcher/running_app.gd +++ b/core/systems/launcher/running_app.gd @@ -37,6 +37,13 @@ enum STATE { STOPPING, ## App is being killed gracefully STOPPED, ## App is no longer running } +const STATE_STR := { + STATE.STARTED: "started", + STATE.RUNNING: "running", + STATE.MISSING_WINDOW: "no window", + STATE.STOPPING: "stopping", + STATE.STOPPED: "stopped" +} enum APP_TYPE { UNKNOWN, @@ -61,7 +68,8 @@ var state: STATE = STATE.STARTED: state = v if old_state != state: state_changed.emit(old_state, state) -var state_steam: STATE = STATE.MISSING_WINDOW +var state_steam: STATE = STATE.STARTED +var steam_missing_window_timestamp: int ## Whether or not the running app is suspended var is_suspended := false: set(v): @@ -71,11 +79,6 @@ var is_suspended := false: suspended.emit(is_suspended) ## Time in milliseconds when the app started var start_time := Time.get_ticks_msec() -## The currently detected window ID of the application -var window_id: int: - set(v): - window_id = v - window_id_changed.emit() ## A list of all detected window IDs related to the application var window_ids: PackedInt64Array = PackedInt64Array(): set(v): @@ -84,6 +87,8 @@ var window_ids: PackedInt64Array = PackedInt64Array(): window_ids.sort() if old_windows != window_ids: window_ids_changed.emit(old_windows, window_ids) +## The window id of the last focused window +var last_focused_window_id: int ## The identifier that is set as the OGUI_ID environment variable var ogui_id: String ## The current app ID of the application @@ -110,13 +115,10 @@ var created_window := false var num_created_windows := 0 ## Number of times this app has failed its "is_running" check var not_running_count := 0 -## When a steam-launched app has no window, count a few tries before trying -## to close Steam -var steam_close_tries := 0 ## Flag for if OGUI should manage this app. Set to false if app is launched ## outside OGUI and we just want to track it. var is_ogui_managed: bool = true -var logger := Log.get_logger("RunningApp", Log.LEVEL.DEBUG) +var logger := Log.get_logger("RunningApp", Log.LEVEL.TRACE) func _init(item: LibraryLaunchItem, dsp: String) -> void: @@ -166,26 +168,26 @@ func start() -> void: ## Updates the state of the running app and fires signals using the given ## list of window ids from XWayland and list of process ids that match ## the OGUI_ID of this application. -func update(all_windows: PackedInt64Array, pids_with_ogui_id: PackedInt64Array) -> void: +func update(all_windows: PackedInt64Array, app_pids: PackedInt64Array) -> void: # Updating the app state differs depending on whether it is running as an # X11 app or a Wayland app. When an app is first started, the app type is # unknown and detection will occur until we find out the type of app it is. match self.app_type: APP_TYPE.UNKNOWN: - self.app_type = discover_app_type(all_windows, pids_with_ogui_id) + self.app_type = discover_app_type(all_windows, app_pids) if self.app_type != APP_TYPE.UNKNOWN: app_type_detected.emit() - self.update(all_windows, pids_with_ogui_id) + self.update(all_windows, app_pids) APP_TYPE.X11: - update_xwayland_app(all_windows, pids_with_ogui_id) + update_xwayland_app(all_windows, app_pids) APP_TYPE.WAYLAND: - update_wayland_app(pids_with_ogui_id) + update_wayland_app(app_pids) ## Tries to discover if the launched app is an X11 or Wayland application -func discover_app_type(all_windows: PackedInt64Array, pids_with_ogui_id: PackedInt64Array) -> APP_TYPE: +func discover_app_type(all_windows: PackedInt64Array, app_pids: PackedInt64Array) -> APP_TYPE: # Update all the windows - self.window_ids = self.get_all_window_ids(all_windows, pids_with_ogui_id) + self.window_ids = self.get_all_window_ids(all_windows, app_pids) # Check to see if running app's app_id exists in focusable_apps var xwayland_primary := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY) @@ -202,7 +204,7 @@ func discover_app_type(all_windows: PackedInt64Array, pids_with_ogui_id: PackedI return APP_TYPE.UNKNOWN -func update_wayland_app(pids_with_ogui_id: PackedInt64Array) -> void: +func update_wayland_app(app_pids: PackedInt64Array) -> void: # Check if the app, or any of its children, are still running var running := is_running() if not running: @@ -214,7 +216,6 @@ func update_wayland_app(pids_with_ogui_id: PackedInt64Array) -> void: app_killed.emit() elif state == STATE.STARTED: state = STATE.RUNNING - #grab_focus() # How can we grab wayland window focus? # Update the focus state of the app focused = is_focused() @@ -232,37 +233,20 @@ func update_wayland_app(pids_with_ogui_id: PackedInt64Array) -> void: # so we can kill Steam gracefully if is_steam_app() and state == STATE.STOPPED and is_ogui_managed: logger.trace(launch_item.name + " is a Steam game and has no valid window ID. It may have closed.") - # Don't try closing Steam immediately. Wait a few more ticks before attempting - # to close Steam. - if steam_close_tries < 4: - steam_close_tries += 1 - return - var steam_pid := find_steam(pids_with_ogui_id) + var steam_pid := find_steam(app_pids) if steam_pid > 0: logger.info("Trying to stop steam with pid: " + str(steam_pid)) var cmd := Command.create("kill", ["-15", str(steam_pid)]) cmd.execute() -func update_xwayland_app(all_windows: PackedInt64Array, pids_with_ogui_id: PackedInt64Array) -> void: +func update_xwayland_app(all_windows: PackedInt64Array, app_pids: PackedInt64Array) -> void: # Update all windows related to the app's PID - self.window_ids = self.get_all_window_ids(all_windows, pids_with_ogui_id) + self.window_ids = self.get_all_window_ids(all_windows, app_pids) # Ensure that all windows related to the app have an app ID set _ensure_app_id() - # Ensure that the running app has a corresponding window ID - var has_valid_window := false - if needs_window_id(): - logger.trace("App needs a valid window id") - var id := _discover_window_id() - if id > 0 and window_id != id: - logger.trace("Setting window ID " + str(id) + " for " + launch_item.name) - window_id = id - has_valid_window = true - else: - has_valid_window = true - # Update the focus state of the app focused = is_focused() @@ -271,62 +255,64 @@ func update_xwayland_app(all_windows: PackedInt64Array, pids_with_ogui_id: Packe if not running: not_running_count += 1 - var state_str := { - STATE.STARTED: "started", - STATE.RUNNING: "running", - STATE.MISSING_WINDOW: "no window", - STATE.STOPPING: "stopping", - STATE.STOPPED: "stopped" - } - # Update the running app's state if not_running_count > 3: state = STATE.STOPPED app_killed.emit() - elif state == STATE.STARTED and has_valid_window: + elif state in [STATE.STARTED, STATE.MISSING_WINDOW] and self.window_ids.size() > 0: state = STATE.RUNNING - if is_steam_app(): - state_steam = STATE.STARTED - grab_focus() - elif state == STATE.RUNNING and not has_valid_window: + elif state in [STATE.RUNNING, STATE.STARTED] and self.window_ids.is_empty(): state = STATE.MISSING_WINDOW + # Update steam state if this is a steam app + update_steam_xwayland_app(app_pids) + + +func update_steam_xwayland_app(app_pids: PackedInt64Array) -> void: + if not is_steam_app(): + return + # If this is a Steam app, we also need to wait until Steam starts the game - if is_steam_app() and state_steam == STATE.STARTED: + if state_steam == STATE.STARTED: for window in self.window_ids: if self.is_steam_window(window): continue logger.debug("Steam game appears to be running") state_steam = STATE.RUNNING - self.window_id = window - grab_focus() break - logger.trace(launch_item.name + " current steam state: " + state_str[state_steam]) + logger.trace(launch_item.name + " current steam state: " + STATE_STR[state_steam]) + return # If the Steam game started, but now only Steam windows remain, indicate # that Steam should stop. - if is_steam_app() and state_steam == STATE.RUNNING: + if state_steam in [STATE.RUNNING, STATE.MISSING_WINDOW]: var only_steam_running := true for window in self.window_ids: if not self.is_steam_window(window): only_steam_running = false + if state_steam == STATE.MISSING_WINDOW: + state_steam = STATE.RUNNING break - if only_steam_running: - state_steam = STATE.STOPPING + if only_steam_running and state_steam == STATE.RUNNING: + state_steam = STATE.MISSING_WINDOW + steam_missing_window_timestamp = Time.get_ticks_msec() + return - logger.trace(launch_item.name + " current state: " + state_str[state]) + # Handle cases where the game window has been missing + if only_steam_running and state_steam == STATE.MISSING_WINDOW: + var time_elapsed := Time.get_ticks_msec() - steam_missing_window_timestamp + if time_elapsed > 4000: + logger.debug("Steam game missing window for 5 seconds. Assuming game closed.") + state_steam = STATE.STOPPING + + logger.trace(launch_item.name + " current state: " + STATE_STR[state]) # TODO: Check all windows for STEAM_GAME prop # If this was launched by Steam, try and detect if the game closed # so we can kill Steam gracefully - if is_steam_app() and state_steam == STATE.STOPPING and is_ogui_managed: + if state_steam == STATE.STOPPING and is_ogui_managed: logger.trace(launch_item.name + " is a Steam game and has no valid window ID. It may have closed.") - # Don't try closing Steam immediately. Wait a few more ticks before attempting - # to close Steam. - if steam_close_tries < 4: - steam_close_tries += 1 - return - var steam_pid := find_steam(pids_with_ogui_id) + var steam_pid := find_steam(app_pids) if steam_pid > 0: logger.info("Trying to stop steam with pid: " + str(steam_pid)) OS.execute("kill", ["-15", str(steam_pid)]) @@ -368,13 +354,11 @@ func get_window_id_from_pid() -> int: ## Attempt to discover all window IDs from the PID of the given application and ## the PIDs of all processes in the same process group. -func get_all_window_ids(all_windows: PackedInt64Array, pids_with_ogui_id: PackedInt64Array) -> PackedInt64Array: +func get_all_window_ids(all_windows: PackedInt64Array, app_pids: PackedInt64Array) -> PackedInt64Array: var app_name := launch_item.name var window_ids := PackedInt64Array() - var pids := get_child_pids(pids_with_ogui_id) var xwayland := gamescope.get_xwayland_by_name(display) - pids.append(pid) - logger.trace(app_name + " found related PIDs: " + str(pids)) + logger.trace(app_name + " found related PIDs: " + str(app_pids)) # Loop through all windows and check if the window belongs to one of our # processes @@ -384,7 +368,7 @@ func get_all_window_ids(all_windows: PackedInt64Array, pids_with_ogui_id: Packed continue var window_pids := xwayland.get_pids_for_window(window_id) for window_pid in window_pids: - if window_pid in pids: + if window_pid in app_pids: #logger.trace("Found window for pid", window_pid, ":", window_id) window_ids.append(window_id) @@ -400,50 +384,8 @@ func is_running() -> bool: return OS.is_process_running(pid) -## Return a list of child PIDs. When launching apps with [Reaper], PR_SET_CHILD_SUBREAPER -## is set to prevent processes from re-parenting themselves to other processes. -func get_child_pids(pids_with_ogui_id: PackedInt64Array) -> PackedInt64Array: - var pids := pids_with_ogui_id.duplicate() - - # Find any child processes that are not in the list of PIDs - var child_pids := Reaper.pstree(pid) - for child_pid in child_pids: - if child_pid in pids: - continue - pids.append(child_pid) - - # Get all PIDs that share the running app's process ID group - var pids_in_group := PackedInt64Array() - for process_id in Reaper.get_pids(): - if process_id in pids_in_group or process_id in pids: - continue - var pgid := Reaper.get_pid_group(process_id) - if pgid == pid: - pids_in_group.append(process_id) - - # Get all the children of THOSE pids as well - for process_id in pids_in_group: - var subchildren := Reaper.pstree(process_id) - if not process_id in pids: - pids.append(process_id) - for subpid in subchildren: - if subpid in pids: - continue - pids.append(subpid) - - # Recursively return all child PIDs of the process - return pids - - -## Returns whether or not the app can be switched to/focused -func can_focus() -> bool: - return self.app_id > 0 - - ## Return true if the currently running app is focused func is_focused() -> bool: - if not can_focus(): - return false var xwayland_primary := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY) if not xwayland_primary: return false @@ -453,11 +395,12 @@ func is_focused() -> bool: ## Focuses to the app's window func grab_focus() -> void: - if not can_focus(): - return var xwayland_primary := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY) if not xwayland_primary: return + if self.window_ids.is_empty(): + logger.warn("No windows to grab focus on") + return # Set the baselayer app id var focused_apps := xwayland_primary.baselayer_apps @@ -478,8 +421,11 @@ func grab_focus() -> void: xwayland_primary.baselayer_apps = new_focused_apps # Set the baselayer window id - xwayland_primary.baselayer_window = self.window_id - focused = true + var focus_window := self.last_focused_window_id + if not focus_window in self.window_ids: + focus_window = self.window_ids[0] + xwayland_primary.baselayer_window = focus_window + self.focused = true ## Switches the app window to the given window ID. Returns an error if unable @@ -489,20 +435,15 @@ func switch_window(win_id: int, focus: bool = true) -> int: # Error if the window does not belong to the running app # TODO: Look into how window switching can work with Wayland windows - if not win_id in window_ids: + if not win_id in self.window_ids: logger.debug("Failed to switch window: window id does not exist") return ERR_DOES_NOT_EXIST - # Get the primary XWayland instance - var xwayland_primary := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY) - if not xwayland_primary: - logger.debug("Failed to switch window: unable to find primary XWayland") - return ERR_UNAVAILABLE - # Update the window ID and optionally grab focus - window_id = win_id + self.last_focused_window_id = win_id if focus: grab_focus() + return OK @@ -518,119 +459,46 @@ func kill(sig: Reaper.SIG = Reaper.SIG.TERM) -> void: func _ensure_app_id() -> void: # Don't mess with app IDs if this was not spawned by ogui if not is_ogui_managed: + logger.trace("App is not managed by OGUI. Skipping ensuring app id.") return # Get the xwayland instance this app is running on var xwayland := gamescope.get_xwayland_by_name(display) if not xwayland: + logger.error("Unable to identify xwayland instance for running app.") return # Get all windows associated with the running app - var possible_windows := window_ids.duplicate() + var possible_windows := self.window_ids.duplicate() + + # Ensure the app id doesn't collide with the overlay + if self.app_id == gamescope.OVERLAY_GAME_ID: + self.app_id = gamescope.OVERLAY_GAME_ID + 1 # Try setting the app ID on each possible Window. If they are valid windows, # gamescope will make these windows available as focusable windows. for window in possible_windows: var window_app_id := xwayland.get_app_id(window) - if window_app_id != gamescope.OVERLAY_GAME_ID and window_app_id >= 0: + if window_app_id == self.app_id: continue - # If the window is using the OVERLAY_GAME_ID (769), set it to something different - # so it does not fight with OGUI for focus. - if window_app_id == gamescope.OVERLAY_GAME_ID: - var new_app_id := self.app_id - if is_steam_app() and has_meta("steam_app_id"): - new_app_id = get_meta("steam_app_id") - if new_app_id != self.app_id: - self.app_id = new_app_id - if self.app_id == gamescope.OVERLAY_GAME_ID: - self.app_id += 1 - xwayland.set_app_id(window, self.app_id) -## Returns whether or not the window id of the running app needs to be discovered -func needs_window_id() -> bool: - var xwayland_primary := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY) - if not xwayland_primary: - return false - - if window_id <= 0: - logger.trace(launch_item.name + " has a bad window ID: " + str(window_id)) - return true - - var focusable_windows := xwayland_primary.focusable_windows - if not window_id in focusable_windows: - logger.trace(str(window_id) + " is not in the list of focusable windows") - return true - - var xwayland := gamescope.get_xwayland_by_name(display) - if not xwayland: - return false - - # Check if the current window ID exists in the list of open windows - var root_window := xwayland.root_window_id - var all_windows := xwayland.get_all_windows(root_window) - if not window_id in all_windows: - logger.trace(str(window_id) + " is not in the list of all windows") - return true - - # If this is a Steam app, the only acceptable window will have its STEAM_GAME - # property set. - if is_steam_app(): - var steam_app_id := get_meta("steam_app_id") as int - if not xwayland.has_app_id(window_id): - logger.trace(str(window_id) + " does not have an app ID already set by Steam") - return true - if xwayland.get_app_id(window_id) != steam_app_id: - logger.trace(str(window_id) + " has an app ID but it does not match " + str(steam_app_id)) - return true - - # Track that a window has been successfully detected at least once. - if not created_window: - created_window = true - num_created_windows += 1 - - return false - - -## Tries to discover the window ID of the running app -func _discover_window_id() -> int: - # If there's a window directly associated with the PID, return that - var win_id := get_window_id_from_pid() - if win_id > 0: - logger.trace("Found window ID for {0} from PID: {1}".format([launch_item.name, window_id])) - return win_id - - # Get all windows associated with the running app - var possible_windows := window_ids.duplicate() - - # Get the primary XWayland instance - var xwayland_primary := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY) - if not xwayland_primary: - return -1 - - # Look for the app window in the list of focusable windows - var focusable := xwayland_primary.focusable_windows - for window in possible_windows: - if window in focusable: - return window - - return -1 - - ## Returns true if the running app was launched through Steam func is_steam_app() -> bool: if has_meta("is_steam_app"): return get_meta("is_steam_app") var args := launch_item.args for arg in args: - if arg.contains("steam://rungameid/"): - set_meta("is_steam_app", true) - var steam_app_id := arg.split("/")[-1] - if steam_app_id.is_valid_int(): - set_meta("steam_app_id", steam_app_id.to_int()) + if not arg.contains("steam://rungameid/"): + continue + set_meta("is_steam_app", true) + var steam_app_id := arg.split("/")[-1] + if steam_app_id.is_valid_int(): + set_meta("steam_app_id", steam_app_id.to_int()) return true + return false set_meta("is_steam_app", false) return false @@ -642,7 +510,7 @@ func is_steam_window(window_id: int) -> bool: return false var window_name := xwayland.get_window_name(window_id) - var steam_window_names := ["Steam Big Picture Mode", "Steam", "steamwebhelper"] + var steam_window_names := ["Steam Big Picture Mode", "Steam", "steam", "steamwebhelper"] if window_name in steam_window_names: return true @@ -659,9 +527,17 @@ func is_steam_window(window_id: int) -> bool: ## Finds the steam process so it can be killed when a game closes -func find_steam(pids_with_ogui_id: PackedInt64Array) -> int: - var child_pids := get_child_pids(pids_with_ogui_id) - for child_pid in child_pids: +func find_steam(app_pids: PackedInt64Array) -> int: + ## Check registry.vdf for a PID + #var registry_path := "/".join([OS.get_environment("HOME"), ".steam", "registry.vdf"]) + #if FileAccess.file_exists(registry_path): + #var content := FileAccess.get_file_as_string(registry_path) + #var vdf := Vdf.new() + #if vdf.parse(content) == OK: + #pass + + var pids := app_pids.duplicate() + for child_pid in pids: var pid_info := Reaper.get_pid_status(child_pid) if not "Name" in pid_info: continue diff --git a/core/ui/card_ui/launch/game_launch_menu.gd b/core/ui/card_ui/launch/game_launch_menu.gd index f6f476a1f..6a59c817c 100644 --- a/core/ui/card_ui/launch/game_launch_menu.gd +++ b/core/ui/card_ui/launch/game_launch_menu.gd @@ -168,6 +168,7 @@ func _on_install() -> void: # Do nothing if we're already installing if InstallManager.is_queued_or_installing(launch_item): return + LaunchManager.update_recent_apps(launch_item) # Get the library provider for this launch item var provider := LibraryManager.get_library_by_id(launch_item._provider_id) diff --git a/core/ui/card_ui/navigation/running_game_card.gd b/core/ui/card_ui/navigation/running_game_card.gd index 41191cba2..b93861051 100644 --- a/core/ui/card_ui/navigation/running_game_card.gd +++ b/core/ui/card_ui/navigation/running_game_card.gd @@ -41,6 +41,7 @@ var gamepad_state := load("res://assets/state/states/gamepad_settings.tres") as @onready var focus_group := $%FocusGroup as FocusGroup @onready var gamepad_button := $%GamepadButton +var _xwayland: GamescopeXWayland var tween: Tween var running_app: RunningApp var window_buttons := {} @@ -114,43 +115,57 @@ func set_running_app(app: RunningApp): else: game_label.visible = true game_label.text = item.name - + _xwayland = gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY) + # Connect to app signals to allow switching between app windows - var on_windows_changed := func(_from: PackedInt64Array, to: PackedInt64Array): - var xwayland := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_PRIMARY) - var xwayland_game := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_GAME) - var focusable_windows := xwayland.get_focusable_windows() - # Add a button to switch to a given window - for window_id in to: - # A button already exists for this window - if window_id in window_buttons: - continue - if not window_id in focusable_windows: - continue - var window_name := xwayland_game.get_window_name(window_id) - if window_name == "": - window_name = "Window (" + str(window_id) + ")" - var button := button_scene.instantiate() as CardButton - button.text = window_name - button.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - window_buttons[window_id] = button - content_container.add_child(button) - content_container.move_child(button, 1) - - # Switch app window when the button is pressed - var on_pressed := func(): - app.switch_window(window_id) - button.button_up.connect(on_pressed) + _on_windows_changed(PackedInt64Array(), app.window_ids) + app.window_ids_changed.connect(_on_windows_changed) + + # Listen for focusable window changes + _xwayland.focusable_windows_updated.connect(_on_focusable_windows_changed) + + +func _on_windows_changed(_from: PackedInt64Array, to: PackedInt64Array) -> void: + logger.trace("App windows change to:", to) + var xwayland_game := gamescope.get_xwayland(gamescope.XWAYLAND_TYPE_GAME) + var focusable_windows := _xwayland.get_focusable_windows() + # Add a button to switch to a given window + for window_id in to: + var window_name := xwayland_game.get_window_name(window_id) + if window_name == "": + window_name = "Window (" + str(window_id) + ")" + # A button already exists for this window + if window_id in window_buttons: + logger.trace("Window button already exists:", window_id) + window_buttons[window_id].text = window_name + continue + if not window_id in focusable_windows: + logger.trace("Window is not focusable. Skipping:", window_id) + continue + logger.trace("Creating button for window id", window_id, ":", window_name) + var button := button_scene.instantiate() as CardButton + button.text = window_name + button.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + window_buttons[window_id] = button + content_container.add_child(button) + content_container.move_child(button, 1) - # Remove buttons for windows that don't exist anymore - for window_id in window_buttons.keys(): - if window_id in to: - continue - var button := window_buttons[window_id] as Control - button.queue_free() - window_buttons.erase(window_id) + # Switch app window when the button is pressed + var on_pressed := func(): + running_app.switch_window(window_id) + button.button_up.connect(on_pressed) + + # Remove buttons for windows that don't exist anymore + for window_id in window_buttons.keys(): + if window_id in to: + continue + var button := window_buttons[window_id] as Control + button.queue_free() + window_buttons.erase(window_id) - app.window_ids_changed.connect(on_windows_changed) + +func _on_focusable_windows_changed(_from: PackedInt64Array, _to: PackedInt64Array): + _on_windows_changed.call_deferred(PackedInt64Array(), running_app.window_ids) func _on_focus() -> void: @@ -223,3 +238,8 @@ func _on_gampad_button_pressed() -> void: if running_app and running_app.launch_item: library_item = LibraryItem.new_from_launch_item(running_app.launch_item) gamepad_state.set_meta("item", library_item) + + +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + _xwayland.focusable_windows_updated.disconnect(_on_focusable_windows_changed) diff --git a/core/ui/common/game/game_loading.gd b/core/ui/common/game/game_loading.gd index 80f053601..de84a78d7 100644 --- a/core/ui/common/game/game_loading.gd +++ b/core/ui/common/game/game_loading.gd @@ -1,5 +1,7 @@ extends Control +signal load_started + var launch_manager := load("res://core/global/launch_manager.tres") as LaunchManager var state_machine := ( preload("res://assets/state/state_machines/global_state_machine.tres") as StateMachine @@ -9,6 +11,7 @@ var popup_state := preload("res://assets/state/states/popup.tres") as State var in_game_state := preload("res://assets/state/states/in_game.tres") as State var game_launching := false +@onready var fade_effect := $FadeEffect as FadeEffect @onready var label := %RichTextLabel as RichTextLabel @onready var progress_bar := %ProgressBar as ProgressBar @@ -37,7 +40,8 @@ func _on_app_launched(app: RunningApp): func _on_window_created() -> void: game_launching = false - visible = false + await get_tree().create_timer(0.5).timeout + fade_effect.fade_out() ## Executed if a pre-launch app lifecycle hook is running and wants to display diff --git a/core/ui/common/game/game_loading.tscn b/core/ui/common/game/game_loading.tscn index 316ae9e60..83c997e43 100644 --- a/core/ui/common/game/game_loading.tscn +++ b/core/ui/common/game/game_loading.tscn @@ -1,7 +1,8 @@ -[gd_scene load_steps=3 format=3 uid="uid://b1kist0rarpcy"] +[gd_scene load_steps=4 format=3 uid="uid://b1kist0rarpcy"] [ext_resource type="Script" uid="uid://b7rbyufflei7k" path="res://core/ui/common/game/game_loading.gd" id="1_1ucmh"] [ext_resource type="PackedScene" uid="uid://2tdbi1v6qb6h" path="res://core/ui/components/loading02.tscn" id="1_jduoa"] +[ext_resource type="PackedScene" uid="uid://bw8113ocotx2r" path="res://core/systems/effects/fade_effect.tscn" id="2_c8n55"] [node name="GameLoading" type="Control"] layout_mode = 3 @@ -14,6 +15,21 @@ size_flags_horizontal = 3 size_flags_vertical = 3 script = ExtResource("1_1ucmh") +[node name="FadeEffect" parent="." node_paths=PackedStringArray("target") instance=ExtResource("2_c8n55")] +target = NodePath("..") +fade_speed = 0.3 +on_signal = "load_started" +on_signal = "load_started" + +[node name="ColorRect" type="ColorRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +color = Color(0, 0, 0, 1) + [node name="VBoxContainer" type="VBoxContainer" parent="."] layout_mode = 1 anchors_preset = 15 @@ -71,15 +87,6 @@ layout_mode = 2 size_flags_horizontal = 3 mouse_filter = 2 -[node name="ColorRect" type="ColorRect" parent="."] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -color = Color(0, 0, 0, 0.501961) - [node name="Loading02" parent="." instance=ExtResource("1_jduoa")] layout_mode = 1 offset_left = -128.0