defmodule ExSni.Menu do
require Logger
alias ExSni.Menu.Item
alias ExSni.Ref
defstruct __ref__: %Ref{path: "/MenuBar", interface: "com.canonical.dbusmenu"},
version: 1,
text_direction: "ltr",
icon_theme_path: [""],
status: "normal",
root: nil,
last_id: 0,
callbacks: [],
item_cache: %{}
@type fn_callback() :: (... -> any())
@type callback() :: {atom(), fn_callback()}
@type t() :: %__MODULE__{
__ref__: Ref.t() | nil,
version: integer(),
text_direction: String.t(),
icon_theme_path: list(String.t()),
status: String.t(),
root: Item.t(),
last_id: non_neg_integer(),
callbacks: list(callback()),
item_cache: map()
}
@dbus_menu_item_type {:struct, [:int32, {:dict, :string, :variant}, {:array, :variant}]}
@spec get_layout(t(), integer(), list(String.t())) ::
{:ok, list(), list()} | {:error, binary(), binary()}
def get_layout(%__MODULE__{} = menu, depth, properties) do
get_layout(menu, 0, depth, properties)
end
@spec get_layout(t(), non_neg_integer(), integer(), list(String.t())) ::
{:ok, list(), list()} | {:error, binary(), binary()}
def get_layout(%__MODULE__{root: %Item{} = root, version: version}, 0, depth, properties) do
{_, _, root_layout} = Item.get_layout(root, depth, properties)
{:ok, [:uint32, @dbus_menu_item_type], [version, root_layout]}
end
def get_layout(%__MODULE__{root: %Item{children: children}}, parentId, depth, properties) do
case find_child(children, parentId) do
%Item{} = child ->
{_, _, child_layout} = Item.get_layout(child, depth, properties)
{:ok, [:uint32, @dbus_menu_item_type], [parentId, child_layout]}
_ ->
{:error, "org.freedesktop.DBus.Error.Failed", "No such menu item"}
end
end
def get_layout(%__MODULE__{root: _, version: version}, parentId, _, _) do
{:ok, [:uint32, @dbus_menu_item_type], [version, [parentId, [], []]]}
end
def get_layout(_, _, _, _) do
{:error, "org.freedesktop.DBus.Error.Failed", "No such menu item"}
end
def get_group_properties(%__MODULE__{} = menu, :all, properties) do
ids =
menu
|> get_children()
|> Enum.map(&Map.get(&1, :id))
get_group_properties(menu, ids, properties)
end
def get_group_properties(%__MODULE__{} = menu, ids, properties) do
ids
|> Enum.map(&find_item(menu, &1))
|> Enum.reject(&(&1 == nil))
|> Enum.map(fn item ->
values = ExSni.DbusProtocol.get_properties(item, properties)
[item.id, values]
end)
end
def get_group_properties(_, _, _) do
[]
end
def get_group_properties_per_item(%__MODULE__{} = menu, mapping) do
Enum.reduce(mapping, [], fn {id, properties}, acc ->
case find_item(menu, id) do
nil ->
acc
item ->
values = ExSni.DbusProtocol.get_properties(item, properties)
[id, values]
end
end)
end
def get_last_id(%__MODULE__{last_id: last_id}) do
last_id
end
def get_last_id(_) do
0
end
def get_children(%__MODULE__{root: %{} = root}) do
get_children(root)
end
def get_children(%{children: []}) do
[]
end
def get_children(%{children: children}) when is_list(children) do
Enum.reduce(children, [], fn child, acc ->
children = get_children(child)
[child | children] ++ acc
end)
end
def get_children(_) do
[]
end
def find_item(%__MODULE__{root: %Item{} = root, item_cache: items}, id) do
case Item.find_item(root, id) do
nil ->
case Enum.find(items, fn {item, _} -> item.id == id end) do
nil -> nil
{item, _} -> item
end
other ->
other
end
end
def find_item(_, _) do
nil
end
@spec onAboutToShow(t(), id :: non_neg_integer()) :: boolean()
def onAboutToShow(%__MODULE__{callbacks: callbacks}, 0) do
run_aboutToShow(callbacks)
end
def onAboutToShow(%__MODULE__{} = menu, id) do
case find_item(menu, id) do
%Item{callbacks: callbacks} ->
run_aboutToShow(callbacks)
_ ->
Logger.debug("Menu.onEvent:: Failed to find menu item by id [#{id}] (AboutToShow})")
false
end
end
def onAboutToShow(nil, _) do
false
end
@spec onEvent(
t(),
eventId :: String.t(),
menuId :: non_neg_integer(),
data :: any(),
timestamp :: any()
) :: any()
def onEvent(%__MODULE__{callbacks: callbacks}, eventId, 0, data, timestamp) do
run_events(callbacks, eventId, data, timestamp)
end
def onEvent(%__MODULE__{} = menu, eventId, id, data, timestamp) do
case find_item(menu, id) do
%Item{callbacks: callbacks} ->
run_events(callbacks, eventId, data, timestamp)
_ ->
Logger.debug(
"Menu.onEvent:: Failed to find menu item by id [#{id}] (eventId: #{eventId})"
)
nil
end
end
def onEvent(nil, _, _, _, _) do
nil
end
defp run_events(callbacks, eventId, data, timestamp) do
callbacks
|> get_callbacks(eventId)
|> Enum.reduce(nil, fn func, _ ->
try do
func.(data, timestamp)
rescue
_ -> nil
end
end)
end
defp run_aboutToShow(callbacks) do
callbacks
|> get_callbacks(:show)
|> Enum.reduce(false, fn func, acc ->
try do
func.()
rescue
_ -> acc
else
v -> v || acc
end
end)
end
defp get_callbacks([], _eventId) do
[]
end
defp get_callbacks(callbacks, eventId) do
callbacks
# |> Enum.filter(&is_tuple/1)
# |> Enum.filter(&(elem(&1, 0) == eventId))
|> Enum.filter(&has_event_id(&1, eventId))
|> Enum.map(&elem(&1, 1))
|> Enum.filter(&is_function(&1))
end
defp has_event_id(item, eventId) when is_tuple(item) do
elem(item, 0) == eventId
end
defp has_event_id(_, _) do
false
end
@spec find_child(list(Item.t()), non_neg_integer()) :: nil | Item.t()
defp find_child([], _) do
nil
end
defp find_child([%Item{id: id} = item | _], id) do
item
end
defp find_child([_ | items], id) do
find_child(items, id)
end
end
defimpl ExSni.DbusProtocol, for: ExSni.Menu do
def get_property(%{version: version}, "Version") do
{:ok, version}
end
def get_property(%{text_direction: text_direction}, "TextDirection") do
{:ok, text_direction}
end
def get_property(%{status: status}, "Status") do
{:ok, status}
end
def get_property(%{icon_theme_path: icon_theme_path}, "IconThemePath")
when is_binary(icon_theme_path) do
{:ok, [icon_theme_path]}
end
def get_property(%{icon_theme_path: icon_theme_path}, "IconThemePath")
when is_list(icon_theme_path) do
{:ok, icon_theme_path}
end
def get_property(_, _) do
{:error, "org.freedesktop.DBus.Error.UnknownProperty", "Invalid property"}
end
def get_property(item, property, _) do
get_property(item, property)
end
def get_properties(item, []) do
get_properties(item, [
"Version",
"TextDirection",
"Status",
"IconThemePath"
])
end
def get_properties(item, properties) do
get_properties(item, properties, [])
end
def get_properties(item, properties, options) do
properties
|> Enum.reduce([], fn property, acc ->
case get_property(item, property, options) do
{:ok, value} -> [{property, value} | acc]
_ -> acc
end
end)
end
end