Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 34 additions & 7 deletions lib/desktop/fallback.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ defmodule Desktop.Fallback do
require Logger
alias Desktop.{Wx, OS}

@notification_show_failed """
wxWidgets failed to show a desktop notification (wxNotificationMessage:show/2 returned false). \
On macOS, ensure notifications are enabled for the app in System Settings, use a packaged .app \
with matching bundle id / Info.plist for the Erlang VM, and see \
https://github.com/elixir-desktop/desktop/issues/38 \
"""

@moduledoc """
Fallback handles version differences in the :wx modules needed for showing the
WebView and Desktop notifications and it uses the highest available
Expand Down Expand Up @@ -170,7 +177,7 @@ defmodule Desktop.Fallback do
webview
end

def notification_new(title, type) do
def notification_new(title, type, parent \\ nil) do
if module?(:wxNotificationMessage) do
flag =
case type do
Expand All @@ -180,8 +187,9 @@ defmodule Desktop.Fallback do
end

notification = call(:wxNotificationMessage, :new, [title, [flags: flag]])
notification_set_parent(notification, parent)

if notification_events_available?() do
if notification != nil and notification_events_available?() do
for event <- [
:notification_message_click,
:notification_message_dismissed,
Expand All @@ -190,9 +198,11 @@ defmodule Desktop.Fallback do
call(:wxNotificationMessage, :connect, [notification, event])
end
else
Logger.warning(
"Missing support for wxNotificationMessage Events - upgrade to wxWidgets 3.1 - messages won't be clickable"
)
if notification != nil do
Logger.warning(
"Missing support for wxNotificationMessage Events - upgrade to wxWidgets 3.1 - messages won't be clickable"
)
end
end

notification
Expand All @@ -204,18 +214,27 @@ defmodule Desktop.Fallback do
end

def notification_show(notification, message, timeout, title \\ nil) do
if module?(:wxNotificationMessage) do
if module?(:wxNotificationMessage) and notification != nil do
if title != nil do
call(:wxNotificationMessage, :setTitle, [notification, to_charlist(title)])
end

call(:wxNotificationMessage, :setMessage, [notification, to_charlist(message)])
call(:wxNotificationMessage, :show, [notification, [timeout: timeout]])

case call(:wxNotificationMessage, :show, [notification, [timeout: timeout]]) do
false ->
Logger.warning(@notification_show_failed)

_ ->
:ok
end
else
Logger.notice("NOTIFICATION: #{title}: #{message}")
end
end

def notification_close(nil), do: :ok

def notification_close(notification) do
call(:wxNotificationMessage, :close, [notification])
end
Expand Down Expand Up @@ -244,6 +263,14 @@ defmodule Desktop.Fallback do
end
end

defp notification_set_parent(notification, parent) do
if notification != nil and parent != nil and OS.macos?() and
module?(:wxNotificationMessage) and
Kernel.function_exported?(:wxNotificationMessage, :setParent, 2) do
call(:wxNotificationMessage, :setParent, [notification, parent])
end
end

defp call(module, method, args \\ []) do
if System.get_env("NO_WX") == nil and Code.ensure_loaded?(module) and
Kernel.function_exported?(module, method, length(args)) do
Expand Down
12 changes: 12 additions & 0 deletions lib/desktop/os.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ defmodule Desktop.OS do
- Windows
- Linux

`ELIXIR_DESKTOP_OS` can be set to `android`, `ios`, or `macos` to force the
corresponding type (useful in tests). On real devices, omit it and the OS is
detected from `:os.type()`.

"""

@doc """
Expand Down Expand Up @@ -45,6 +49,11 @@ defmodule Desktop.OS do
"ios" ->
IOS

# Lets CI and Linux developers run macOS-specific branches (e.g. notification wiring)
# without a Darwin host. Not used in production builds.
"macos" ->
MacOS

_ ->
case :os.type() do
{:unix, :darwin} -> MacOS
Expand Down Expand Up @@ -93,6 +102,9 @@ defmodule Desktop.OS do
end
end

@doc false
def macos?(), do: type() == MacOS

defp kill_heart() do
heart = Process.whereis(:heart)

Expand Down
24 changes: 17 additions & 7 deletions lib/desktop/window.ex
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ defmodule Desktop.Window do
wx_menubar
end

if OS.type() == MacOS do
if OS.macos?() do
update_apple_menu(window_title, frame, wx_menubar || :wxMenuBar.new())
end

Expand Down Expand Up @@ -449,7 +449,7 @@ defmodule Desktop.Window do
* `:type` - One of `:info` `:error` `:warn` these will change
how the notification will be displayed. The default is `:info`

* `:title` - An alternative title for the notificaion,
* `:title` - An alternative title for the notification,
when none is provided the current window title is used.

* `:timeout` - A timeout hint specifying how long the notification
Expand All @@ -468,6 +468,12 @@ defmodule Desktop.Window do
* `:callback` - A function to be executed when the user clicks on the
notification.

On macOS, notifications are associated with the main `wxFrame` via
`wxNotificationMessage:setParent/2` so they follow the same app identity as
the visible window. If nothing appears, check System Settings → Notifications
and (for distributed apps) bundle id / Info.plist alignment for the VM
(see GitHub issue #38).

## Examples

iex> Desktop.Window.show_notification(pid, "Hello, world!")
Expand Down Expand Up @@ -548,8 +554,6 @@ defmodule Desktop.Window do

if OS.type() == Linux do
notification(ui, obj, :action)
else
notification(ui, obj, :dismiss)
end

{:noreply, ui}
Expand Down Expand Up @@ -653,13 +657,19 @@ defmodule Desktop.Window do

def handle_cast(
{:show_notification, message, id, type, title, callback, timeout},
ui = %Window{notifications: noties, title: window_title}
ui = %Window{notifications: noties, title: window_title, frame: frame}
) do
{n, _} =
note =
case Map.get(noties, id, nil) do
nil -> {Fallback.notification_new(title || window_title, type), callback}
{note, _} -> {note, callback}
nil ->
{Fallback.notification_new(title || window_title, type, frame), callback}

{nil, _} ->
{Fallback.notification_new(title || window_title, type, frame), callback}

{note, _} ->
{note, callback}
end

Fallback.notification_show(n, message, timeout, title || window_title)
Expand Down
4 changes: 2 additions & 2 deletions lib/mix/tasks/desktop.install.ex
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ if Code.ensure_loaded?(Igniter.Mix.Task) do
menu = Igniter.Project.Module.module_name(igniter, "Menu")
menubar = Igniter.Project.Module.module_name(igniter, "MenuBar")
gettext = Igniter.Libs.Phoenix.web_module_name(igniter, "Gettext")
main_window = Igniter.Project.Module.module_name(igniter, MainWindow)
main_window = Igniter.Project.Module.module_name(igniter, "MainWindow")

igniter
|> Igniter.compose_task("igniter.add", ["desktop"])
Expand All @@ -63,7 +63,7 @@ if Code.ensure_loaded?(Igniter.Mix.Task) do
quote do
[
app: unquote(app),
id: unquote(Igniter.Project.Module.module_name(igniter, MainWindow)),
id: unquote(Igniter.Project.Module.module_name(igniter, "MainWindow")),
title: unquote(to_string(app)),
size: {600, 500},
menubar: unquote(menubar),
Expand Down
25 changes: 25 additions & 0 deletions test/fallback_notification_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
defmodule Desktop.FallbackNotificationTest do
use ExUnit.Case

describe "notification_new/3" do
test "parent option does not crash when wx is disabled (NO_WX)" do
old_no_wx = System.get_env("NO_WX")
old_os = System.get_env("ELIXIR_DESKTOP_OS")

on_exit(fn ->
restore_env("NO_WX", old_no_wx)
restore_env("ELIXIR_DESKTOP_OS", old_os)
end)

System.put_env("NO_WX", "1")
System.put_env("ELIXIR_DESKTOP_OS", "macos")

assert Desktop.OS.macos?()
# Parent is ignored when notification object cannot be created; must not raise.
assert Desktop.Fallback.notification_new("Title", :info, :not_a_wx_window) == nil
end
end

defp restore_env(key, nil), do: System.delete_env(key)
defp restore_env(key, value), do: System.put_env(key, value)
end
20 changes: 20 additions & 0 deletions test/os_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
defmodule Desktop.OSTest do
use ExUnit.Case

describe "type/0 and ELIXIR_DESKTOP_OS" do
test "macos override forces MacOS for tests and CI" do
old = System.get_env("ELIXIR_DESKTOP_OS")

try do
System.put_env("ELIXIR_DESKTOP_OS", "macos")
assert Desktop.OS.type() == MacOS
after
if old do
System.put_env("ELIXIR_DESKTOP_OS", old)
else
System.delete_env("ELIXIR_DESKTOP_OS")
end
end
end
end
end
Loading