/* * Copyright 2021-2023 Robert Tari * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License version 3, as published * by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranties of * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ #include #include #include #include #include "service.h" #define BUS_NAME "org.ayatana.indicator.keyboard" #define BUS_PATH "/org/ayatana/indicator/keyboard" #define ICON_DEFAULT "input-keyboard" static guint m_nSignal = 0; static void *m_pLibHandle = NULL; static Keyboard* (*m_fnKeyboardNew)(); static void (*m_fnKeyboardAddSource)(Keyboard *pKeyboard); static guint (*m_fnKeyboardGetNumLayouts)(Keyboard *pKeyboard); static guint (*m_fnKeyboardGetLayoutIndex)(Keyboard *pKeyboard); static void (*m_fnKeyboardGetLayout)(Keyboard *pKeyboard, gint nLayout, gchar **pLanguage, gchar **pDescription); static void (*m_fnKeyboardSetLayout)(Keyboard *pKeyboard, gint nLayout); enum { SECTION_HEADER = (1 << 0), SECTION_LAYOUTS = (1 << 1), SECTION_DISPLAY = (1 << 2), SECTION_SETTINGS = (1 << 3) }; enum { PROFILE_PHONE, PROFILE_DESKTOP, PROFILE_GREETER, N_PROFILES }; static const char * const m_lMenuNames[N_PROFILES] = { "phone", "desktop", "greeter" }; struct ProfileMenuInfo { GMenu *pMenu; GMenu *pSubmenu; guint nExportId; GSimpleAction *pHeaderAction; }; struct _IndicatorKeyboardServicePrivate { GCancellable *pCancellable; guint nOwnId; guint nActionsId; GDBusConnection *pConnection; gboolean bMenusBuilt; struct ProfileMenuInfo lMenus[N_PROFILES]; GSimpleActionGroup *pActionGroup; GSimpleAction *pSettingsAction; GSimpleAction *pDisplayAction; GSimpleAction *pLayoutAction; GMenu *pLayoutSection; Keyboard *pKeyboard; GSettings *pSettings; }; typedef IndicatorKeyboardServicePrivate priv_t; G_DEFINE_TYPE_WITH_PRIVATE(IndicatorKeyboardService, indicator_keyboard_service, G_TYPE_OBJECT) static GVariant* createHeaderState(IndicatorKeyboardService *self, int nProfile) { GVariantBuilder cBuilder; g_variant_builder_init(&cBuilder, G_VARIANT_TYPE("a{sv}")); g_variant_builder_add(&cBuilder, "{sv}", "title", g_variant_new_string(_("Keyboard"))); g_variant_builder_add(&cBuilder, "{sv}", "tooltip", g_variant_new_string(_("Keyboard layout switcher and settings"))); g_variant_builder_add(&cBuilder, "{sv}", "visible", g_variant_new_boolean(TRUE)); gchar *sKey = NULL; if (nProfile == PROFILE_DESKTOP) { sKey = "language-icon-desktop"; } else if (nProfile == PROFILE_PHONE) { sKey = "language-icon-phone"; } else if (nProfile == PROFILE_GREETER) { sKey = "language-icon-greeter"; } gboolean bLayout = g_settings_get_boolean (self->pPrivate->pSettings, sKey); GIcon *pIcon = NULL; if (bLayout == FALSE) { pIcon = g_themed_icon_new_with_default_fallbacks(ICON_DEFAULT); } else { gchar *sLanguage; m_fnKeyboardGetLayout(self->pPrivate->pKeyboard, -1, &sLanguage, NULL); gchar *sIcon = g_strconcat("ayatana-indicator-keyboard-", sLanguage, NULL); g_free(sLanguage); pIcon = g_themed_icon_new_with_default_fallbacks(sIcon); g_free(sIcon); } g_variant_builder_add(&cBuilder, "{sv}", "accessible-desc", g_variant_new_string(_("Current keyboard layout"))); if (pIcon) { GVariant *pSerialized = g_icon_serialize(pIcon); if (pSerialized != NULL) { g_variant_builder_add(&cBuilder, "{sv}", "icon", pSerialized); g_variant_unref(pSerialized); } g_object_unref(pIcon); } return g_variant_builder_end(&cBuilder); } static GMenuModel* createLayoutSection(IndicatorKeyboardService *self) { self->pPrivate->pLayoutSection = g_menu_new(); guint nLayouts = m_fnKeyboardGetNumLayouts(self->pPrivate->pKeyboard); for (guint nLayout = 0; nLayout < nLayouts; nLayout++) { gchar *sLanguage; gchar *sDescription; m_fnKeyboardGetLayout(self->pPrivate->pKeyboard, nLayout, &sLanguage, &sDescription); GMenuItem *pItem = g_menu_item_new(sDescription, NULL); g_free(sDescription); g_menu_item_set_action_and_target_value(pItem, "indicator.layout", g_variant_new_byte(nLayout)); g_menu_item_set_attribute_value(pItem, "x-ayatana-layout", g_variant_new_byte(nLayout)); gchar *sIcon = g_strconcat("ayatana-indicator-keyboard-", sLanguage, NULL); g_free(sLanguage); GIcon *pIcon = g_themed_icon_new_with_default_fallbacks(sIcon); g_free(sIcon); GVariant *pSerialized = g_icon_serialize(pIcon); if (pSerialized != NULL) { g_menu_item_set_attribute_value(pItem, G_MENU_ATTRIBUTE_ICON, pSerialized); g_variant_unref(pSerialized); } g_object_unref(pIcon); g_menu_append_item(self->pPrivate->pLayoutSection, pItem); g_object_unref(pItem); } return G_MENU_MODEL(self->pPrivate->pLayoutSection); } static GMenuModel* createSettingsSection(IndicatorKeyboardService *self) { GMenu * pMenu = g_menu_new(); g_menu_append(pMenu, _("Keyboard Settingsā€¦"), "indicator.settings"); return G_MENU_MODEL(pMenu); } static GMenuModel* createDisplaySection (IndicatorKeyboardService *self) { GMenu * pMenu = g_menu_new (); g_menu_append (pMenu, _("Show Current Layout"), "indicator.display"); return G_MENU_MODEL (pMenu); } static void rebuildSection(GMenu *pMenu, int nPos, GMenuModel *pModel) { g_menu_remove(pMenu, nPos); g_menu_insert_section(pMenu, nPos, NULL, pModel); g_object_unref(pModel); } static void rebuildNow(IndicatorKeyboardService *self, guint nSections) { struct ProfileMenuInfo *pInfoDesktop = &self->pPrivate->lMenus[PROFILE_DESKTOP]; struct ProfileMenuInfo *pInfoPhone = &self->pPrivate->lMenus[PROFILE_PHONE]; struct ProfileMenuInfo *pInfoGreeter = &self->pPrivate->lMenus[PROFILE_GREETER]; if (nSections & SECTION_HEADER) { g_simple_action_set_state(pInfoDesktop->pHeaderAction, createHeaderState(self, PROFILE_DESKTOP)); g_simple_action_set_state(pInfoPhone->pHeaderAction, createHeaderState(self, PROFILE_PHONE)); g_simple_action_set_state(pInfoGreeter->pHeaderAction, createHeaderState(self, PROFILE_GREETER)); } if (!self->pPrivate->bMenusBuilt) { return; } if (nSections & SECTION_LAYOUTS) { rebuildSection(pInfoDesktop->pSubmenu, 0, createLayoutSection(self)); rebuildSection(pInfoPhone->pSubmenu, 0, createLayoutSection(self)); rebuildSection(pInfoGreeter->pSubmenu, 0, createLayoutSection(self)); } if (nSections & SECTION_DISPLAY) { rebuildSection (pInfoDesktop->pSubmenu, 1, createDisplaySection (self)); } if (nSections & SECTION_SETTINGS) { rebuildSection(pInfoDesktop->pSubmenu, 2, createSettingsSection(self)); rebuildSection(pInfoPhone->pSubmenu, 2, createSettingsSection(self)); } } static void createMenu(IndicatorKeyboardService *self, int nProfile) { GMenu *pMenu; GMenu *pSubmenu; GMenuItem *pItem; GMenuModel *lSections[16]; guint nSection = 0; g_assert(0 <= nProfile && nProfile < N_PROFILES); g_assert(self->pPrivate->lMenus[nProfile].pMenu == NULL); // Build the sections if (nProfile == PROFILE_PHONE) { lSections[nSection++] = createLayoutSection(self); lSections[nSection++] = createSettingsSection(self); } else if (nProfile == PROFILE_DESKTOP) { lSections[nSection++] = createLayoutSection(self); lSections[nSection++] = createDisplaySection(self); lSections[nSection++] = createSettingsSection(self); } else if (nProfile == PROFILE_GREETER) { lSections[nSection++] = createLayoutSection(self); } // Add sections to the submenu pSubmenu = g_menu_new(); for (guint i = 0; i < nSection; ++i) { g_menu_append_section(pSubmenu, NULL, lSections[i]); g_object_unref(lSections[i]); } // Add submenu to the header gchar *sName = g_strdup_printf ("indicator._header-%s", m_lMenuNames[nProfile]); pItem = g_menu_item_new(NULL, sName); g_free (sName); g_menu_item_set_attribute(pItem, "x-ayatana-type", "s", "org.ayatana.indicator.root"); g_menu_item_set_submenu(pItem, G_MENU_MODEL(pSubmenu)); g_object_unref(pSubmenu); // Add header to the menu pMenu = g_menu_new(); g_menu_append_item(pMenu, pItem); g_object_unref(pItem); self->pPrivate->lMenus[nProfile].pMenu = pMenu; self->pPrivate->lMenus[nProfile].pSubmenu = pSubmenu; } static void onLayoutChanged(Keyboard *pKeyboard, gpointer pData) { IndicatorKeyboardService *self = INDICATOR_KEYBOARD_SERVICE(pData); rebuildNow(self, SECTION_HEADER); } static void onConfigChanged(Keyboard *pKeyboard, gpointer pData) { IndicatorKeyboardService *self = INDICATOR_KEYBOARD_SERVICE(pData); rebuildNow(self, SECTION_LAYOUTS); } static void onLayoutSelected(GSimpleAction *pAction, GVariant *pVariant, gpointer pData) { IndicatorKeyboardService *self = INDICATOR_KEYBOARD_SERVICE(pData); const guint8 nLayout = g_variant_get_byte(pVariant); m_fnKeyboardSetLayout(self->pPrivate->pKeyboard, nLayout); } static void onSettings(GSimpleAction *pAction, GVariant *pVariant, gpointer pUserData) { if (ayatana_common_utils_is_mate()) { ayatana_common_utils_execute_command("mate-keyboard-properties"); } else if (ayatana_common_utils_is_lomiri()) { ayatana_common_utils_open_url("settings:///system/hw-keyboard-layouts"); } } static void onDisplay (GSimpleAction *pAction, GVariant *pVariant, gpointer pData) { IndicatorKeyboardService *self = INDICATOR_KEYBOARD_SERVICE (pData); guint nLayout = m_fnKeyboardGetLayoutIndex (self->pPrivate->pKeyboard); gchar *sProgram = NULL; if (ayatana_common_utils_is_mate ()) { sProgram = "matekbd-keyboard-display"; } else { sProgram = "gkbd-keyboard-display"; } gchar *sArgs = g_strdup_printf ("-g %i", nLayout + 1); ayatana_common_utils_execute_command_warn (sProgram, sArgs); g_free (sArgs); } static void initActions(IndicatorKeyboardService *self) { GSimpleAction *pAction; self->pPrivate->pActionGroup = g_simple_action_group_new(); for (int nProfile = 0; nProfile < N_PROFILES; ++nProfile) { gchar *sName = g_strdup_printf ("_header-%s", m_lMenuNames[nProfile]); pAction = g_simple_action_new_stateful(sName, NULL, createHeaderState(self, nProfile)); g_free (sName); g_action_map_add_action(G_ACTION_MAP(self->pPrivate->pActionGroup), G_ACTION(pAction)); self->pPrivate->lMenus[nProfile].pHeaderAction = pAction; } pAction = g_simple_action_new("layout", G_VARIANT_TYPE_BYTE); g_action_map_add_action(G_ACTION_MAP(self->pPrivate->pActionGroup), G_ACTION(pAction)); self->pPrivate->pLayoutAction = pAction; g_signal_connect(pAction, "activate", G_CALLBACK(onLayoutSelected), self); pAction = g_simple_action_new("settings", NULL); g_action_map_add_action(G_ACTION_MAP(self->pPrivate->pActionGroup), G_ACTION(pAction)); self->pPrivate->pSettingsAction = pAction; g_signal_connect(pAction, "activate", G_CALLBACK(onSettings), self); pAction = g_simple_action_new ("display", NULL); g_action_map_add_action (G_ACTION_MAP (self->pPrivate->pActionGroup), G_ACTION (pAction)); self->pPrivate->pDisplayAction = pAction; g_signal_connect (pAction, "activate", G_CALLBACK (onDisplay), self); } static void onBusAcquired(GDBusConnection *pConnection, const gchar *sName, gpointer pData) { IndicatorKeyboardService *self = INDICATOR_KEYBOARD_SERVICE(pData); g_debug("bus acquired: %s", sName); self->pPrivate->pConnection = (GDBusConnection*)g_object_ref(G_OBJECT (pConnection)); guint nId; GError *pError = NULL; // Export the actions if ((nId = g_dbus_connection_export_action_group(pConnection, BUS_PATH, G_ACTION_GROUP(self->pPrivate->pActionGroup), &pError))) { self->pPrivate->nActionsId = nId; } else { g_warning("cannot export action group: %s", pError->message); g_clear_error(&pError); } GString *pPath = g_string_new(NULL); // Export the menus for (int nProfile = 0; nProfile < N_PROFILES; ++nProfile) { struct ProfileMenuInfo *pInfo = &self->pPrivate->lMenus[nProfile]; g_string_printf(pPath, "%s/%s", BUS_PATH, m_lMenuNames[nProfile]); if ((nId = g_dbus_connection_export_menu_model(pConnection, pPath->str, G_MENU_MODEL(pInfo->pMenu), &pError))) { pInfo->nExportId = nId; } else { g_warning("cannot export %s menu: %s", pPath->str, pError->message); g_clear_error (&pError); } } g_string_free(pPath, TRUE); } static void unexport(IndicatorKeyboardService *self) { // Unexport the menus for (int nProfile = 0; nProfile < N_PROFILES; ++nProfile) { guint *nId = &self->pPrivate->lMenus[nProfile].nExportId; if (*nId) { g_dbus_connection_unexport_menu_model(self->pPrivate->pConnection, *nId); *nId = 0; } } // Unexport the actions if (self->pPrivate->nActionsId) { g_dbus_connection_unexport_action_group(self->pPrivate->pConnection, self->pPrivate->nActionsId); self->pPrivate->nActionsId = 0; } } static void onNameLost(GDBusConnection *pConnection, const gchar *sName, gpointer pData) { IndicatorKeyboardService *self = INDICATOR_KEYBOARD_SERVICE(pData); g_debug("%s %s name lost %s", G_STRLOC, G_STRFUNC, sName); unexport(self); } static void onDispose(GObject *pObject) { IndicatorKeyboardService *self = INDICATOR_KEYBOARD_SERVICE(pObject); if (self->pPrivate->pSettings != NULL) { g_signal_handlers_disconnect_by_data (self->pPrivate->pSettings, self); g_clear_object (&self->pPrivate->pSettings); } if (self->pPrivate->pKeyboard != NULL) { g_object_unref(G_OBJECT(self->pPrivate->pKeyboard)); self->pPrivate->pKeyboard = NULL; } if (self->pPrivate->nOwnId) { g_bus_unown_name(self->pPrivate->nOwnId); self->pPrivate->nOwnId = 0; } unexport(self); if (self->pPrivate->pCancellable != NULL) { g_cancellable_cancel(self->pPrivate->pCancellable); g_clear_object(&self->pPrivate->pCancellable); } g_clear_object (&self->pPrivate->pSettingsAction); g_clear_object (&self->pPrivate->pDisplayAction); g_clear_object (&self->pPrivate->pLayoutAction); for (int nProfile = 0; nProfile < N_PROFILES; ++nProfile) { g_clear_object (&self->pPrivate->lMenus[nProfile].pHeaderAction); } g_clear_object (&self->pPrivate->pActionGroup); g_clear_object (&self->pPrivate->pConnection); if (m_pLibHandle) { dlclose(m_pLibHandle); m_pLibHandle = NULL; } G_OBJECT_CLASS(indicator_keyboard_service_parent_class)->dispose(pObject); } static void onSettingsChanged(GSettings *pSettings, gchar *sKey, gpointer pData) { IndicatorKeyboardService *self = INDICATOR_KEYBOARD_SERVICE(pData); rebuildNow(self, SECTION_HEADER); } static void indicator_keyboard_service_init(IndicatorKeyboardService *self) { gchar *sLib = "libayatana-keyboard-x11.so.0"; if (ayatana_common_utils_is_lomiri()) { sLib = "libayatana-keyboard-lomiri.so.0"; } m_pLibHandle = dlopen(sLib, RTLD_NOW); if (!m_pLibHandle) { g_error("%s", dlerror()); } m_fnKeyboardNew = dlsym(m_pLibHandle, "keyboard_new"); gchar *sError = dlerror(); if (sError != NULL) { g_error("%s", sError); } m_fnKeyboardAddSource = dlsym(m_pLibHandle, "keyboard_AddSource"); m_fnKeyboardGetNumLayouts = dlsym(m_pLibHandle, "keyboard_GetNumLayouts"); m_fnKeyboardGetLayoutIndex = dlsym(m_pLibHandle, "keyboard_GetLayoutIndex"); m_fnKeyboardGetLayout = dlsym(m_pLibHandle, "keyboard_GetLayout"); m_fnKeyboardSetLayout = dlsym(m_pLibHandle, "keyboard_SetLayout"); self->pPrivate = indicator_keyboard_service_get_instance_private(self); self->pPrivate->pCancellable = g_cancellable_new(); self->pPrivate->pSettings = g_settings_new ("org.ayatana.indicator.keyboard"); g_signal_connect(self->pPrivate->pSettings, "changed", G_CALLBACK(onSettingsChanged), self); self->pPrivate->pKeyboard = m_fnKeyboardNew(); g_signal_connect(self->pPrivate->pKeyboard, KEYBOARD_LAYOUT_CHANGED, G_CALLBACK(onLayoutChanged), self); g_signal_connect(self->pPrivate->pKeyboard, KEYBOARD_CONFIG_CHANGED, G_CALLBACK(onConfigChanged), self); initActions(self); for (int nProfile = 0; nProfile < N_PROFILES; ++nProfile) { createMenu(self, nProfile); } self->pPrivate->bMenusBuilt = TRUE; self->pPrivate->nOwnId = g_bus_own_name(G_BUS_TYPE_SESSION, BUS_NAME, G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT, onBusAcquired, NULL, onNameLost, self, NULL); } static void indicator_keyboard_service_class_init(IndicatorKeyboardServiceClass *klass) { GObjectClass *pClass = G_OBJECT_CLASS(klass); pClass->dispose = onDispose; m_nSignal = g_signal_new("name-lost", G_TYPE_FROM_CLASS(klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET(IndicatorKeyboardServiceClass, pNameLost), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); } IndicatorKeyboardService *indicator_keyboard_service_new() { GObject *pObject = g_object_new(INDICATOR_TYPE_KEYBOARD_SERVICE, NULL); return INDICATOR_KEYBOARD_SERVICE(pObject); } void indicator_keyboard_service_AddKeyboardSource(IndicatorKeyboardService *self) { m_fnKeyboardAddSource(self->pPrivate->pKeyboard); }