fix: shutdown moved to standard functionality

This commit is contained in:
2026-05-01 22:00:30 +02:00
parent e4db1d6d62
commit c25720bf6e
4 changed files with 102 additions and 54 deletions

View File

@@ -6,16 +6,24 @@ defmodule BDS.Desktop.MainWindow do
alias Desktop.Window alias Desktop.Window
@window_id __MODULE__ @window_id __MODULE__
@server_name BDS.Desktop.MainWindow.Watcher
@persist_interval_ms 1_000 @persist_interval_ms 1_000
@default_size {1280, 780} @default_size {1280, 780}
@default_min_size {800, 600} @default_min_size {800, 600}
@state_file "window-state.json" @state_file "window-state.json"
def start_link(_opts) do def start_link(_opts) do
GenServer.start_link(__MODULE__, :ok) GenServer.start_link(__MODULE__, :ok, name: @server_name)
end end
def window_id, do: @window_id 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 def window_options(extra_opts \\ []) do
desktop_config = Application.get_env(:bds, :desktop, []) desktop_config = Application.get_env(:bds, :desktop, [])
@@ -90,6 +98,11 @@ defmodule BDS.Desktop.MainWindow do
{:noreply, %{state | last_bounds: next_bounds}} {:noreply, %{state | last_bounds: next_bounds}}
end end
@impl true
def handle_call(:persist_bounds_now, _from, state) do
{:reply, :ok, persist_current_bounds(state)}
end
@impl true @impl true
def terminate(_reason, %{last_bounds: last_bounds}) do def terminate(_reason, %{last_bounds: last_bounds}) do
if 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) Process.send_after(self(), :persist_bounds, @persist_interval_ms)
end 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 defp apply_restored_bounds(frame) do
case restore_bounds() do case restore_bounds() do
%{x: x, y: y, width: width, height: height} -> %{x: x, y: y, width: width, height: height} ->
@@ -127,6 +150,7 @@ defmodule BDS.Desktop.MainWindow do
defp current_bounds(nil), do: nil defp current_bounds(nil), do: nil
defp current_bounds(frame) do defp current_bounds(frame) do
try do
with_wx_env(fn -> with_wx_env(fn ->
cond do cond do
not :wxWindow.isShown(frame) -> not :wxWindow.isShown(frame) ->
@@ -144,6 +168,12 @@ defmodule BDS.Desktop.MainWindow do
%{x: x, y: y, width: width, height: height} %{x: x, y: y, width: width, height: height}
end end
end) end)
rescue
ErlangError -> nil
FunctionClauseError -> nil
catch
:exit, _reason -> nil
end
end end
defp with_wx_env(fun) do defp with_wx_env(fun) do

View File

@@ -4,8 +4,6 @@ defmodule BDS.Desktop.Shutdown do
alias BDS.Desktop.MainWindow alias BDS.Desktop.MainWindow
alias Desktop.Window alias Desktop.Window
@stop_delay_ms 100
@spec install_handlers(term()) :: :ok @spec install_handlers(term()) :: :ok
def install_handlers(frame) do def install_handlers(frame) do
:wx.set_env(Desktop.Env.wx_env()) :wx.set_env(Desktop.Env.wx_env())
@@ -17,13 +15,6 @@ defmodule BDS.Desktop.Shutdown do
userData: self() 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 :ok
rescue rescue
_error -> :ok _error -> :ok
@@ -51,42 +42,16 @@ defmodule BDS.Desktop.Shutdown do
request_quit() request_quit()
end end
@spec command_menu_selected(tuple(), term()) :: :ok
def command_menu_selected(_event, _command_event) do
request_quit()
end
defp start_shutdown_task do defp start_shutdown_task do
Task.start(fn -> Task.start(fn ->
close_main_window() MainWindow.persist_now()
Process.sleep(@stop_delay_ms) quit_module().quit()
System.stop(0)
end) end)
:ok :ok
end end
defp close_main_window do defp quit_module do
with frame when not is_nil(frame) <- main_frame() do Application.get_env(:bds, :desktop_window_quit_module, Window)
: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
end end
end end

View File

@@ -44,6 +44,11 @@ defmodule BDS.Desktop.MainWindowTest do
assert MainWindow.restore_bounds() == %{x: 120, y: 80, width: 1260, height: 820} assert MainWindow.restore_bounds() == %{x: 120, y: 80, width: 1260, height: 820}
end 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 test "window options clamp oversized startup bounds to the visible client area" do
desktop = Application.get_env(:bds, :desktop, []) desktop = Application.get_env(:bds, :desktop, [])
@@ -78,4 +83,22 @@ defmodule BDS.Desktop.MainWindowTest do
"height" => 700 "height" => 700
} }
end 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 end

View File

@@ -10,6 +10,13 @@ defmodule BDS.DesktopTest do
end end
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 test "desktop configuration no longer uses a pending adapter" do
assert Application.get_env(:bds, BDS.Application)[:desktop_adapter] == :desktop assert Application.get_env(:bds, BDS.Application)[:desktop_adapter] == :desktop
end end
@@ -138,6 +145,29 @@ defmodule BDS.DesktopTest do
assert_receive :quit_requested assert_receive :quit_requested
end 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 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 = conn(:get, "/?k=#{Desktop.Auth.login_key()}")
conn = BDS.Desktop.Endpoint.call(conn, BDS.Desktop.Endpoint.init([])) conn = BDS.Desktop.Endpoint.call(conn, BDS.Desktop.Endpoint.init([]))