lib/desktop/menu/adapters/dbus.ex

defmodule Desktop.Menu.Adapter.DBus do
  @moduledoc """
  DBus Menu adapter that creates and handles
  menus and tray icons created over DBus.
  """

  require Logger

  alias ExSni.Icon
  alias ExSni.Icon.Info
  alias ExSni.Icon.Tooltip
  alias ExSni.Menu
  alias ExSni.Menu.Item
  alias Desktop.Pixmap

  defstruct [:menu_pid, :sni, :icon, :menubar]

  @type t() :: %__MODULE__{
          menu_pid: pid() | nil,
          sni: pid() | nil,
          icon: ExSni.Icon.t() | nil,
          menubar: ExSni.Menu.t() | nil
        }

  def new(opts) do
    %__MODULE__{
      menu_pid: Keyword.get(opts, :menu_pid),
      sni: Keyword.get(opts, :sni),
      icon: Keyword.get(opts, :icon),
      menubar: nil
    }
  end

  def create(adapter = %__MODULE__{sni: sni, menubar: menubar, icon: icon}, _dom) do
    if menubar != nil do
      ExSni.set_menu(sni, menubar)
    end

    if icon != nil do
      set_sni_icon(sni, icon)
    end

    adapter
  end

  def update_dom(adapter = %__MODULE__{menubar: _menubar, sni: sni}, dom) do
    {children, _} = create_menu_items(1, dom, adapter)

    root = %Item{id: 0, children: children}

    callbacks = [
      {:show,
       fn ->
         # ABOUT TO SHOW menubar
         # Return "needUpdate" = false
         false
       end}
    ]

    menubar = %Menu{root: root, callbacks: callbacks}

    ExSni.update_menu(sni, nil, menubar)

    %{adapter | menubar: menubar}
  end

  def popup_menu(adapter = %__MODULE__{}) do
    adapter
  end

  def recreate_menu(adapter = %__MODULE__{}, _dom) do
    adapter
  end

  def menubar(%__MODULE__{menubar: menubar}) do
    menubar
  end

  def get_icon(%__MODULE__{icon: icon}) do
    icon
  end

  def set_icon(adapter = %__MODULE__{}, nil) do
    {:ok, %{adapter | icon: nil}}
  end

  def set_icon(adapter = %__MODULE__{sni: sni}, icon) do
    with {:ok, _} <- set_sni_icon(sni, icon) do
      {:ok, %{adapter | icon: icon}}
    end
  end

  # Private functions

  defp set_sni_icon(sni, icon) do
    with {:ok, dbus_icon} <- generate_icon(icon),
         {:ok, icon} <- ExSni.update_icon(sni, dbus_icon) do
      {:ok, icon}
    end
  end

  defp create_menu_items(next_id, [], _adapter) do
    {[], next_id}
  end

  defp create_menu_items(next_id, {:menu, _, children}, adapter) do
    create_menu_items(next_id, children, adapter)
  end

  defp create_menu_items(next_id, [child | children], adapter) do
    {item, last_id} = create_menu_item(next_id, child, adapter)
    {items, last_id} = create_menu_items(last_id + 1, children, adapter)
    {[item | items], last_id}
  end

  defp create_menu_item(id, {:item, params, [label]}, adapter) do
    {create_standard_item(id, label, params, adapter), id}
  end

  defp create_menu_item(id, {:hr, _, _}, _opts) do
    {%Item{id: id, type: "separator"}, id}
  end

  defp create_menu_item(id, {:menu, params, children}, adapter) do
    item = create_standard_item(id, Map.get(params, :label, ""), params, adapter)
    {children, last_id} = create_menu_items(id + 1, children, adapter)
    item = %{item | children: children}
    {item, last_id}
  end

  defp create_menu_item(id, {:menubar, _params, children}, adapter) do
    create_menu_items(id, children, adapter)
  end

  defp create_standard_item(id, label, params, %{menu_pid: menu_pid}) do
    toggle_type =
      case Map.get(params, :type, nil) do
        "checkbox" -> :checkmark
        "radio" -> :radio
        _ -> nil
      end

    toggle_state =
      case Map.get(params, :checked, nil) do
        "checked" -> :on
        _ -> :off
      end

    callbacks =
      if menu_pid != nil do
        Enum.reduce(params, [], fn param, acc ->
          case param do
            {:onclick, event} ->
              [
                {"clicked",
                 fn _data, _timestamp ->
                   spawn_link(Desktop.Menu, :trigger_event, [menu_pid, event])
                 end}
                | acc
              ]

            _ ->
              acc
          end
        end)
      end

    %Item{
      id: id,
      label: label,
      toggle_type: toggle_type,
      toggle_state: toggle_state,
      callbacks: callbacks
    }
  end

  defp generate_icon(nil) do
    {:ok, nil}
  end

  defp generate_icon(icon = %Icon{}) do
    {:ok, icon}
  end

  defp generate_icon(icon_name) when is_binary(icon_name) do
    icon = %{
      generate_sni_icon()
      | icon: %Info{
          name: icon_name
        }
    }

    {:ok, icon}
  end

  defp generate_icon(wx_icon) do
    :wxIcon = :wx.getObjectType(wx_icon)

    case Pixmap.from_wx_icon(wx_icon,
           env: Desktop.Env.wx_env(),
           rescale: true,
           width: 22,
           height: 22
         ) do
      {:ok, pixmap} ->
        icon = %{
          generate_sni_icon()
          | icon: %Info{
              data: {:pixmap, [pixmap]}
            }
        }

        {:ok, icon}

      error ->
        Logger.warn(error)
        error
    end
  end

  defp generate_sni_icon() do
    %Icon{
      category: :application_status,
      id: "1",
      title: "",
      menu: "/MenuBar",
      status: :active,
      tooltip: %Tooltip{
        name: "",
        title: "",
        description: ""
      }
    }
  end
end