From c25720bf6e0570c7c535fb92a5dac1d4cd4b60ff Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Fri, 1 May 2026 22:00:30 +0200 Subject: [PATCH] fix: shutdown moved to standard functionality --- lib/bds/desktop/main_window.ex | 60 ++++++++++++++++++++------- lib/bds/desktop/shutdown.ex | 43 ++----------------- test/bds/desktop/main_window_test.exs | 23 ++++++++++ test/bds/desktop_test.exs | 30 ++++++++++++++ 4 files changed, 102 insertions(+), 54 deletions(-) diff --git a/lib/bds/desktop/main_window.ex b/lib/bds/desktop/main_window.ex index ffeaf37..5621c2d 100644 --- a/lib/bds/desktop/main_window.ex +++ b/lib/bds/desktop/main_window.ex @@ -6,16 +6,24 @@ defmodule BDS.Desktop.MainWindow do alias Desktop.Window @window_id __MODULE__ + @server_name BDS.Desktop.MainWindow.Watcher @persist_interval_ms 1_000 @default_size {1280, 780} @default_min_size {800, 600} @state_file "window-state.json" def start_link(_opts) do - GenServer.start_link(__MODULE__, :ok) + GenServer.start_link(__MODULE__, :ok, name: @server_name) end def window_id, do: @window_id + def server_name, do: @server_name + + def persist_now(timeout \\ 100) do + GenServer.call(@server_name, :persist_bounds_now, timeout) + catch + :exit, _reason -> :ok + end def window_options(extra_opts \\ []) do desktop_config = Application.get_env(:bds, :desktop, []) @@ -90,6 +98,11 @@ defmodule BDS.Desktop.MainWindow do {:noreply, %{state | last_bounds: next_bounds}} end + @impl true + def handle_call(:persist_bounds_now, _from, state) do + {:reply, :ok, persist_current_bounds(state)} + end + @impl true def terminate(_reason, %{last_bounds: last_bounds}) do if bounds = last_bounds do @@ -103,6 +116,16 @@ defmodule BDS.Desktop.MainWindow do Process.send_after(self(), :persist_bounds, @persist_interval_ms) end + defp persist_current_bounds(%{frame: frame} = state) do + next_bounds = current_bounds(frame) || state.last_bounds + + if next_bounds do + _ = persist_bounds(next_bounds) + end + + %{state | last_bounds: next_bounds} + end + defp apply_restored_bounds(frame) do case restore_bounds() do %{x: x, y: y, width: width, height: height} -> @@ -127,23 +150,30 @@ defmodule BDS.Desktop.MainWindow do defp current_bounds(nil), do: nil defp current_bounds(frame) do - with_wx_env(fn -> - cond do - not :wxWindow.isShown(frame) -> - nil + try do + with_wx_env(fn -> + cond do + not :wxWindow.isShown(frame) -> + nil - :wxTopLevelWindow.isFullScreen(frame) -> - nil + :wxTopLevelWindow.isFullScreen(frame) -> + nil - :wxTopLevelWindow.isMaximized(frame) -> - nil + :wxTopLevelWindow.isMaximized(frame) -> + nil - true -> - {x, y} = :wxWindow.getPosition(frame) - {width, height} = :wxWindow.getSize(frame) - %{x: x, y: y, width: width, height: height} - end - end) + true -> + {x, y} = :wxWindow.getPosition(frame) + {width, height} = :wxWindow.getSize(frame) + %{x: x, y: y, width: width, height: height} + end + end) + rescue + ErlangError -> nil + FunctionClauseError -> nil + catch + :exit, _reason -> nil + end end defp with_wx_env(fun) do diff --git a/lib/bds/desktop/shutdown.ex b/lib/bds/desktop/shutdown.ex index 343bac3..41275ad 100644 --- a/lib/bds/desktop/shutdown.ex +++ b/lib/bds/desktop/shutdown.ex @@ -4,8 +4,6 @@ defmodule BDS.Desktop.Shutdown do alias BDS.Desktop.MainWindow alias Desktop.Window - @stop_delay_ms 100 - @spec install_handlers(term()) :: :ok def install_handlers(frame) do :wx.set_env(Desktop.Env.wx_env()) @@ -17,13 +15,6 @@ defmodule BDS.Desktop.Shutdown do userData: self() ) - _ = :wxFrame.disconnect(frame, :command_menu_selected, id: Desktop.Wx.wxID_EXIT()) - - :wxFrame.connect(frame, :command_menu_selected, - id: Desktop.Wx.wxID_EXIT(), - callback: &__MODULE__.command_menu_selected/2 - ) - :ok rescue _error -> :ok @@ -51,42 +42,16 @@ defmodule BDS.Desktop.Shutdown do request_quit() end - @spec command_menu_selected(tuple(), term()) :: :ok - def command_menu_selected(_event, _command_event) do - request_quit() - end - defp start_shutdown_task do Task.start(fn -> - close_main_window() - Process.sleep(@stop_delay_ms) - System.stop(0) + MainWindow.persist_now() + quit_module().quit() end) :ok end - defp close_main_window do - with frame when not is_nil(frame) <- main_frame() do - :wx.set_env(Desktop.Env.wx_env()) - - if :wxWindow.isShown(frame) do - :wxWindow.hide(frame) - end - - :wxWindow.destroy(frame) - else - _other -> :ok - end - rescue - _error -> :ok - catch - :exit, _reason -> :ok - end - - defp main_frame do - Window.frame(MainWindow.window_id()) - catch - :exit, _reason -> nil + defp quit_module do + Application.get_env(:bds, :desktop_window_quit_module, Window) end end diff --git a/test/bds/desktop/main_window_test.exs b/test/bds/desktop/main_window_test.exs index a78e13f..09fe065 100644 --- a/test/bds/desktop/main_window_test.exs +++ b/test/bds/desktop/main_window_test.exs @@ -44,6 +44,11 @@ defmodule BDS.Desktop.MainWindowTest do assert MainWindow.restore_bounds() == %{x: 120, y: 80, width: 1260, height: 820} end + test "window id and watcher process name do not collide" do + assert MainWindow.window_id() == BDS.Desktop.MainWindow + assert MainWindow.server_name() == BDS.Desktop.MainWindow.Watcher + end + test "window options clamp oversized startup bounds to the visible client area" do desktop = Application.get_env(:bds, :desktop, []) @@ -78,4 +83,22 @@ defmodule BDS.Desktop.MainWindowTest do "height" => 700 } end + + test "persist timer keeps last bounds when the wx frame is already gone", %{path: path} do + bounds = %{x: 166, y: 57, width: 1280, height: 780} + + assert {:noreply, state} = + MainWindow.handle_info(:persist_bounds, %{ + frame: :invalid_wx_frame, + last_bounds: bounds + }) + + assert state.last_bounds == bounds + + refute File.exists?(path) + end + + test "persist now is harmless when the window watcher is not running" do + assert :ok = MainWindow.persist_now() + end end diff --git a/test/bds/desktop_test.exs b/test/bds/desktop_test.exs index 3bd3239..613867c 100644 --- a/test/bds/desktop_test.exs +++ b/test/bds/desktop_test.exs @@ -10,6 +10,13 @@ defmodule BDS.DesktopTest do end end + defmodule FakeWindowQuit do + def quit do + send(Application.fetch_env!(:bds, :desktop_shutdown_test_pid), :window_quit_requested) + :ok + end + end + test "desktop configuration no longer uses a pending adapter" do assert Application.get_env(:bds, BDS.Application)[:desktop_adapter] == :desktop end @@ -138,6 +145,29 @@ defmodule BDS.DesktopTest do assert_receive :quit_requested end + test "cmd-q remains handled by the desktop window quit handler" do + refute function_exported?(BDS.Desktop.Shutdown, :command_menu_selected, 2) + end + + test "app-owned shutdown delegates final termination to the desktop hard quit path" do + previous_module = Application.get_env(:bds, :desktop_shutdown_module) + previous_quit_module = Application.get_env(:bds, :desktop_window_quit_module) + previous_pid = Application.get_env(:bds, :desktop_shutdown_test_pid) + + Application.put_env(:bds, :desktop_shutdown_module, BDS.Desktop.Shutdown) + Application.put_env(:bds, :desktop_window_quit_module, FakeWindowQuit) + Application.put_env(:bds, :desktop_shutdown_test_pid, self()) + + on_exit(fn -> + restore_env(:desktop_shutdown_module, previous_module) + restore_env(:desktop_window_quit_module, previous_quit_module) + restore_env(:desktop_shutdown_test_pid, previous_pid) + end) + + assert :ok = BDS.Desktop.Shutdown.request_quit() + assert_receive :window_quit_requested + end + test "desktop root html is a LiveView shell and references only the live bootstrap assets" do conn = conn(:get, "/?k=#{Desktop.Auth.login_key()}") conn = BDS.Desktop.Endpoint.call(conn, BDS.Desktop.Endpoint.init([]))