lib/desktop/wx/task_bar_icon.ex

defmodule Desktop.Wx.TaskBarIcon do
  @moduledoc """
  TaskBarIcon module for Desktop, based on :wxTaskBarIcon
  """
  require Logger

  alias Desktop.OS

  defstruct fn_create_popup: nil,
            skip_popup_menu?: true,
            wx_taskbar_icon: nil

  @type wx_menu() :: any()
  @type wx_taskbar_icon() :: any()
  @type create_popup() :: (() -> wx_menu())
  @type t() :: %__MODULE__{
          fn_create_popup: nil | create_popup(),
          skip_popup_menu?: boolean(),
          wx_taskbar_icon: wx_taskbar_icon()
        }

  @spec create(fn_create_popup :: create_popup()) :: {:ok, t()} | {:error, any()}
  def create(fn_create_popup) when is_function(fn_create_popup) do
    taskbar_icon = %__MODULE__{
      fn_create_popup: fn_create_popup,
      skip_popup_menu?: true
    }

    with {:ok, taskbar_icon = %{wx_taskbar_icon: wx_taskbar_icon}} <-
           create(taskbar_icon, OS.type()) do
      OnCrash.call(fn ->
        :wx.set_env(Desktop.Env.wx_env())
        :wxTaskBarIcon.removeIcon(wx_taskbar_icon)
        :wxTaskBarIcon.destroy(wx_taskbar_icon)
      end)

      {:ok, taskbar_icon}
    end
  end

  @spec create(t(), os_type :: atom()) :: {:ok, t()} | {:error, any()}
  def create(taskbar_icon = %__MODULE__{fn_create_popup: fn_create_popup}, MacOS)
      when is_function(fn_create_popup) do
    with {:ok, wx_taskbar_icon} <- create_taskbar_icon_osx(fn_create_popup) do
      {:ok,
       %{
         taskbar_icon
         | wx_taskbar_icon: wx_taskbar_icon,
           # On MacOS we always skip using popup_menu() method
           skip_popup_menu?: true
       }}
    end
  end

  def create(taskbar_icon = %__MODULE__{fn_create_popup: fn_create_popup}, _) do
    case create_taskbar_icon(createPopupMenu: fn_create_popup) do
      {:ok, wx_taskbar_icon} ->
        {:ok, %{taskbar_icon | wx_taskbar_icon: wx_taskbar_icon}}

      _ ->
        # Failed to create with :wxTaskBarIcon.new/1
        # Fallback to :wxTaskBarIcon.new/0
        case create_taskbar_icon_fallback() do
          {:ok, wx_taskbar_icon} ->
            # When using the fallback, enable popup_menu()
            # by setting skip_popup_menu? to false
            {:ok, %{taskbar_icon | wx_taskbar_icon: wx_taskbar_icon, skip_popup_menu?: false}}

          error ->
            error
        end
    end
  end

  @spec popup_menu(t()) :: boolean()
  @spec popup_menu(t(), atom()) :: boolean()
  def popup_menu(taskbar_icon, event \\ :taskbar_left_down)

  def popup_menu(
        %__MODULE__{
          wx_taskbar_icon: wx_taskbar_icon,
          fn_create_popup: fn_create_popup,
          skip_popup_menu?: true
        },
        :taskbar_left_down
      )
      when is_function(fn_create_popup) do
    menu = fn_create_popup.()
    :wxTaskBarIcon.popupMenu(wx_taskbar_icon, menu)
  end

  def popup_menu(
        %__MODULE__{
          wx_taskbar_icon: wx_taskbar_icon,
          fn_create_popup: fn_create_popup,
          skip_popup_menu?: false
        },
        _
      )
      when is_function(fn_create_popup) do
    menu = fn_create_popup.()
    :wxTaskBarIcon.popupMenu(wx_taskbar_icon, menu)
  end

  def popup_menu(
        %__MODULE__{
          fn_create_popup: fn_create_popup
        },
        _
      )
      when is_function(fn_create_popup) do
    true
  end

  def popup_menu(_, _) do
    false
  end

  def set_icon(%__MODULE__{wx_taskbar_icon: wx_taskbar_icon}, icon) do
    :wxTaskBarIcon.setIcon(wx_taskbar_icon, icon)
  end

  def set_icon(%__MODULE__{wx_taskbar_icon: wx_taskbar_icon}, icon, options) do
    :wxTaskBarIcon.setIcon(wx_taskbar_icon, icon, options)
  end

  def remove_icon(%__MODULE__{wx_taskbar_icon: wx_taskbar_icon}) do
    :wxTaskBarIcon.removeIcon(wx_taskbar_icon)
  end

  def connect(%__MODULE__{wx_taskbar_icon: wx_taskbar_icon}) do
    :wxTaskBarIcon.connect(wx_taskbar_icon, :taskbar_left_down, skip: true)
    :wxTaskBarIcon.connect(wx_taskbar_icon, :taskbar_right_down, skip: true)
  end

  defp create_taskbar_icon_osx(fn_create_popup) do
    if supports_method?(:new, 1) do
      # Supports :wxTaskBarIcon.new(opts)

      if is_module?(:wxWebView) do
        # Proper OTP24 release
        create_taskbar_icon(createPopupMenu: fn_create_popup)
      else
        # Pre-OTP24 custom version for backwards compat.
        try do
          :wxTaskBarIcon.new(fn_create_popup)
        catch
          :error, :function_clause ->
            Logger.error("No MacOS compatible :wxTaskBarIcon found! Please use at least OTP24")

            create_taskbar_icon_fallback()
        else
          wx_taskbar_icon -> {:ok, wx_taskbar_icon}
        end
      end
    else
      # Does not support constructor with 1 argument
      create_taskbar_icon_fallback()
    end
  end

  defp create_taskbar_icon(opts) do
    if supports_method?(:new, 1) do
      try do
        :wxTaskBarIcon.new(opts)
      rescue
        e -> {:error, e}
      else
        wx_taskbar_icon -> {:ok, wx_taskbar_icon}
      end
    else
      {:error, ":wxTaskBarIcon.new/1 not available"}
    end
  end

  defp create_taskbar_icon_fallback() do
    if supports_method?(:new, 0) do
      try do
        :wxTaskBarIcon.new()
      rescue
        e -> {:error, e}
      else
        wx_taskbar_icon -> {:ok, wx_taskbar_icon}
      end
    else
      {:error, ":wxTaskBarIcon.new/0 not available"}
    end
  end

  defp supports_method?(method, arity) do
    if is_module?(:wxTaskBarIcon) do
      Kernel.function_exported?(:wxTaskBarIcon, method, arity)
    else
      false
    end
  end

  defp is_module?(module) do
    Code.ensure_loaded?(module)
  end
end