diff --git a/po/POTFILES.in b/po/POTFILES.in
index e3e9fb5d094c43af52a7ac252ef26e974a3af044..4f116b2111a73526f81906e54c91860fc707600c 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -24,6 +24,7 @@ src/ui/ms-features-panel.ui
src/ui/ms-feedback-panel.ui
src/ui/ms-lockscreen-panel.ui
src/ui/ms-osk-add-layout-dialog.ui
+src/ui/ms-osk-add-shortcut-dialog.ui
src/ui/ms-osk-layout-prefs.ui
src/ui/ms-osk-layout-row.ui
src/ui/ms-osk-panel.ui
diff --git a/src/meson.build b/src/meson.build
index fc25cee096351bd34f9e49aae96516cdca937e9d..100b924321cc230c47319ede1cdefc18ee0ea2cd 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -50,6 +50,8 @@ mobile_settings_sources = [
'ms-main.h',
'ms-osk-add-layout-dialog.c',
'ms-osk-add-layout-dialog.h',
+ 'ms-osk-add-shortcut-dialog.c',
+ 'ms-osk-add-shortcut-dialog.h',
'ms-osk-layout.c',
'ms-osk-layout.h',
'ms-osk-layout-prefs.c',
diff --git a/src/mobile-settings.gresource.xml b/src/mobile-settings.gresource.xml
index 2f64cd8c98e6a2d0c51b4f7d9ee455e21c8fa320..6fad4a9a102eff50da0558abd9d0cabe269805d1 100644
--- a/src/mobile-settings.gresource.xml
+++ b/src/mobile-settings.gresource.xml
@@ -12,6 +12,7 @@
ui/ms-feedback-row.ui
ui/ms-lockscreen-panel.ui
ui/ms-osk-add-layout-dialog.ui
+ ui/ms-osk-add-shortcut-dialog.ui
ui/ms-osk-layout-prefs.ui
ui/ms-osk-layout-row.ui
ui/ms-osk-panel.ui
diff --git a/src/ms-osk-add-shortcut-dialog.c b/src/ms-osk-add-shortcut-dialog.c
new file mode 100644
index 0000000000000000000000000000000000000000..8d089bbad33ca9cd61d0f3f333df57faf938a7b1
--- /dev/null
+++ b/src/ms-osk-add-shortcut-dialog.c
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2025 Tether Operations Limited
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * Authors: Gotam Gorabh
+ */
+
+#define G_LOG_DOMAIN "ms-osk-add-shortcut-dialog"
+
+#include "mobile-settings-config.h"
+
+#include "ms-osk-add-shortcut-dialog.h"
+
+#define PHOSH_OSK_TERMINAL_SETTINGS "sm.puri.phosh.osk.Terminal"
+#define SHORTCUTS_KEY "shortcuts"
+
+/**
+ * MsOskAddShortcutDialog:
+ *
+ * Dialog to add an OSK shortcut
+ */
+
+typedef enum {
+ MODIFIER_CTRL,
+ MODIFIER_ALT,
+ MODIFIER_SHIFT,
+ MODIFIER_SUPER,
+ MODIFIER_LAST
+} ShortcutModifiers;
+
+static const char * const shortcut_modifiers_names[] = {
+ "ctrl",
+ "alt",
+ "shift",
+ "super",
+ NULL
+};
+
+typedef enum {
+ KEY_UP,
+ KEY_DOWN,
+ KEY_LEFT,
+ KEY_RIGHT,
+ KEY_F1,
+ KEY_F2,
+ KEY_F3,
+ KEY_F4,
+ KEY_F5,
+ KEY_F6,
+ KEY_F7,
+ KEY_F8,
+ KEY_F9,
+ KEY_F10,
+ KEY_F11,
+ KEY_F12,
+ KEY_TAB,
+ KEY_DELETE,
+ KEY_LAST
+}ShortcutKeys;
+
+static const char * const shortcut_keys_names[] = {
+ "Up",
+ "Down",
+ "Left",
+ "Right",
+ "F1",
+ "F2",
+ "F3",
+ "F4",
+ "F5",
+ "F6",
+ "F7",
+ "F8",
+ "F9",
+ "F10",
+ "F11",
+ "F12",
+ "Tab",
+ "Delete",
+ NULL
+};
+
+struct _MsOskAddShortcutDialog {
+ AdwDialog parent;
+
+ GSettings *pos_terminal_settings;
+
+ AdwToastOverlay *toast_overlay;
+ GtkWidget *add_button;
+
+ GtkCheckButton *shortcut_modifiers[MODIFIER_LAST];
+
+ AdwEntryRow *shortcut_key_entry;
+ GtkFlowBox *key_flowbox;
+ GtkFlowBox *preview_flowbox;
+};
+G_DEFINE_TYPE (MsOskAddShortcutDialog, ms_osk_add_shortcut_dialog, ADW_TYPE_DIALOG)
+
+
+static void
+is_valid_shortcut (MsOskAddShortcutDialog *self)
+{
+ GtkFlowBoxChild* flow_child = gtk_flow_box_get_child_at_index (self->preview_flowbox, 0);
+ GtkWidget *shortcut_label = gtk_widget_get_first_child (GTK_WIDGET (flow_child));
+ GtkWidget *label_child = gtk_widget_get_first_child (shortcut_label);
+
+ /* If a shortcut is valid then GtkShortcutLabel has one or more GtkLabel */
+ if (!label_child) {
+ GtkWidget *invalid_label = gtk_label_new ("Invalid Shortcut");
+ gtk_widget_add_css_class (invalid_label, "error");
+
+ for (int i = 0; i < MODIFIER_LAST; i++)
+ gtk_check_button_set_active (self->shortcut_modifiers[i], FALSE);
+
+ gtk_flow_box_remove_all (self->preview_flowbox);
+ gtk_flow_box_append (self->preview_flowbox, invalid_label);
+ gtk_editable_set_text (GTK_EDITABLE (self->shortcut_key_entry), "");
+ gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), FALSE);
+ } else {
+ gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), TRUE);
+ }
+}
+
+
+static GStrv
+shortcut_append (const char *const *shortcuts, const char *shortcut)
+{
+ g_autoptr (GStrvBuilder) builder = g_strv_builder_new ();
+
+ if (g_strv_contains (shortcuts, shortcut))
+ return g_strdupv ((GStrv)shortcuts);
+
+ for (int i = 0; shortcuts[i]; i++)
+ g_strv_builder_add (builder, shortcuts[i]);
+
+ g_strv_builder_add (builder, shortcut);
+
+ return g_strv_builder_end (builder);
+}
+
+
+static const char *
+get_current_preview_shortcut (MsOskAddShortcutDialog *self)
+{
+ GtkFlowBoxChild* child = gtk_flow_box_get_child_at_index (self->preview_flowbox, 0);
+ GtkWidget *shortcut_label;
+
+ if (!child)
+ return "";
+
+ shortcut_label = gtk_widget_get_first_child (GTK_WIDGET (child));
+
+ /* when users continue to choose shortcut without clearing preview */
+ if (GTK_IS_LABEL (shortcut_label)) {
+ gtk_flow_box_remove_all (self->preview_flowbox);
+ return "";
+ }
+ return gtk_shortcut_label_get_accelerator (GTK_SHORTCUT_LABEL (shortcut_label));
+}
+
+
+static void
+on_add_clicked (MsOskAddShortcutDialog *self)
+{
+ const char *curr_shortcut = get_current_preview_shortcut (self);
+ g_auto (GStrv) pos_terminal_shortcuts = NULL;
+ g_auto (GStrv) shortcut = NULL;
+
+ pos_terminal_shortcuts = g_settings_get_strv (self->pos_terminal_settings, SHORTCUTS_KEY);
+ shortcut = shortcut_append ((const char * const *) pos_terminal_shortcuts, curr_shortcut);
+
+ g_settings_set_strv (self->pos_terminal_settings, SHORTCUTS_KEY, (const char * const *)shortcut);
+ adw_dialog_close (ADW_DIALOG (self));
+}
+
+
+static void
+on_modifiers_toggled (MsOskAddShortcutDialog *self)
+{
+ g_autofree char *current_modifers = g_strdup (get_current_preview_shortcut (self));
+
+ for (int i = 0; i < MODIFIER_LAST; i++) {
+ GtkWidget *check_button_child = gtk_check_button_get_child (self->shortcut_modifiers[i]);
+ const char *mod_label = gtk_shortcut_label_get_accelerator (GTK_SHORTCUT_LABEL (check_button_child));
+ gboolean active = gtk_check_button_get_active (self->shortcut_modifiers[i]);
+ char *contain = g_strrstr (current_modifers, mod_label);
+ g_autofree const char *accltr = NULL;
+ GtkWidget *shortcut_label;
+ if (active && contain == NULL) {
+ accltr = g_strconcat (current_modifers, mod_label, NULL);
+ shortcut_label = gtk_shortcut_label_new (accltr);
+ gtk_flow_box_remove_all (self->preview_flowbox);
+ gtk_flow_box_append (self->preview_flowbox, shortcut_label);
+ is_valid_shortcut (self);
+ } else if (!active && contain) {
+ g_auto (GStrv) parts = g_strsplit (current_modifers, mod_label, -1);
+ accltr = g_strjoinv ("", parts);
+ shortcut_label = gtk_shortcut_label_new (accltr);
+ gtk_flow_box_remove_all (self->preview_flowbox);
+ gtk_flow_box_append (self->preview_flowbox, shortcut_label);
+ is_valid_shortcut (self);
+ }
+ }
+}
+
+
+static void
+on_shortcut_key_apply (MsOskAddShortcutDialog *self)
+{
+ const char *modifiers = get_current_preview_shortcut (self);
+ const char *key = gtk_editable_get_text (GTK_EDITABLE (self->shortcut_key_entry));
+ const char *joined = g_strconcat (modifiers, key, NULL);
+ GtkWidget *shortcut_label = gtk_shortcut_label_new (joined);
+ gtk_flow_box_remove_all (self->preview_flowbox);
+ gtk_flow_box_append (self->preview_flowbox, shortcut_label);
+ is_valid_shortcut (self);
+}
+
+
+static void
+on_key_selected (GtkFlowBox *box,
+ GtkFlowBoxChild *child,
+ gpointer user_data)
+{
+ MsOskAddShortcutDialog *self = user_data;
+ GtkWidget *shortcut_label_child = gtk_widget_get_first_child (GTK_WIDGET (child));
+ const char *modifiers = get_current_preview_shortcut (self);
+ const char *box_key = gtk_shortcut_label_get_accelerator (GTK_SHORTCUT_LABEL (shortcut_label_child));
+ const char *joined = g_strconcat (modifiers, box_key, NULL);
+ GtkWidget *shortcut_label = gtk_shortcut_label_new (joined);
+ gtk_flow_box_remove_all (self->preview_flowbox);
+ gtk_flow_box_append (self->preview_flowbox, shortcut_label);
+ is_valid_shortcut (self);
+}
+
+
+static void
+on_preview_clear_clicked (MsOskAddShortcutDialog *self)
+{
+ for (int i = 0; i < MODIFIER_LAST; i++)
+ gtk_check_button_set_active (self->shortcut_modifiers[i], FALSE);
+
+ gtk_flow_box_remove_all (self->preview_flowbox);
+ gtk_editable_set_text (GTK_EDITABLE (self->shortcut_key_entry), "");
+ gtk_widget_set_sensitive (GTK_WIDGET (self->add_button), FALSE);
+}
+
+
+static void
+ms_osk_add_shortcut_dialog_dispose (GObject *object)
+{
+ MsOskAddShortcutDialog *self = MS_OSK_ADD_SHORTCUT_DIALOG (object);
+
+ g_clear_object (&self->pos_terminal_settings);
+
+ G_OBJECT_CLASS (ms_osk_add_shortcut_dialog_parent_class)->dispose (object);
+}
+
+
+static void
+ms_osk_add_shortcut_dialog_class_init (MsOskAddShortcutDialogClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = ms_osk_add_shortcut_dialog_dispose;
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/mobi/phosh/MobileSettings/"
+ "ui/ms-osk-add-shortcut-dialog.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, MsOskAddShortcutDialog, toast_overlay);
+ gtk_widget_class_bind_template_child (widget_class, MsOskAddShortcutDialog, add_button);
+
+ for (int i = 0; i < MODIFIER_LAST; i++) {
+ g_autofree char *widget_name = g_strdup_printf ("%s_modifier", shortcut_modifiers_names[i]);
+ gtk_widget_class_bind_template_child_full (widget_class,
+ widget_name,
+ FALSE,
+ G_STRUCT_OFFSET (MsOskAddShortcutDialog, shortcut_modifiers[i]));
+ }
+
+ gtk_widget_class_bind_template_child (widget_class, MsOskAddShortcutDialog, shortcut_key_entry);
+ gtk_widget_class_bind_template_child (widget_class, MsOskAddShortcutDialog, key_flowbox);
+ gtk_widget_class_bind_template_child (widget_class, MsOskAddShortcutDialog, preview_flowbox);
+
+ gtk_widget_class_bind_template_callback (widget_class, on_add_clicked);
+ gtk_widget_class_bind_template_callback (widget_class, on_modifiers_toggled);
+ gtk_widget_class_bind_template_callback (widget_class, on_shortcut_key_apply);
+ gtk_widget_class_bind_template_callback (widget_class, on_key_selected);
+ gtk_widget_class_bind_template_callback (widget_class, on_preview_clear_clicked);
+}
+
+
+static void
+ms_osk_add_shortcut_dialog_init (MsOskAddShortcutDialog *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->pos_terminal_settings = g_settings_new (PHOSH_OSK_TERMINAL_SETTINGS);
+
+ for (int i = 0; i < KEY_LAST; i++) {
+ GtkWidget *key_shortcut_label = gtk_shortcut_label_new (shortcut_keys_names[i]);
+ gtk_flow_box_append (self->key_flowbox, key_shortcut_label);
+ }
+}
+
+
+GtkWidget *
+ms_osk_add_shortcut_dialog_new (void)
+{
+ return g_object_new (MS_TYPE_OSK_ADD_SHORTCUT_DIALOG, NULL);
+}
diff --git a/src/ms-osk-add-shortcut-dialog.h b/src/ms-osk-add-shortcut-dialog.h
new file mode 100644
index 0000000000000000000000000000000000000000..91288c0cb8354d686f774fe3f3d60626d711fbf0
--- /dev/null
+++ b/src/ms-osk-add-shortcut-dialog.h
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2025 Tether Operations Limited
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include
+
+G_BEGIN_DECLS
+
+#define MS_TYPE_OSK_ADD_SHORTCUT_DIALOG (ms_osk_add_shortcut_dialog_get_type ())
+
+G_DECLARE_FINAL_TYPE (MsOskAddShortcutDialog, ms_osk_add_shortcut_dialog,
+ MS, OSK_ADD_SHORTCUT_DIALOG, AdwDialog)
+
+GtkWidget *ms_osk_add_shortcut_dialog_new (void);
+
+G_END_DECLS
diff --git a/src/ms-osk-panel.c b/src/ms-osk-panel.c
index e3bf3a9ebf4ecfec98cc202e6376c2c61cf26f1d..b118a606f368b08de1ccabaf60839e906dcd3e34 100644
--- a/src/ms-osk-panel.c
+++ b/src/ms-osk-panel.c
@@ -13,6 +13,7 @@
#include "ms-completer-info.h"
#include "ms-enum-types.h"
#include "ms-osk-layout-prefs.h"
+#include "ms-osk-add-shortcut-dialog.h"
#include "ms-osk-panel.h"
#include "ms-util.h"
@@ -191,31 +192,109 @@ on_drop (GtkDropTarget *drop_target, const GValue *value, double x, double y, g
}
+struct ShortcutData {
+ MsOskPanel *self;
+ char *shortcut_string;
+};
+
+
+static void
+shortcut_data_free (struct ShortcutData *data)
+{
+ if (!data)
+ return;
+ g_object_unref (data->self);
+ g_free (data->shortcut_string);
+ g_free (data);
+}
+
+
+static GStrv
+shortcut_remove (const char *const *shortcuts, const char *shortcut)
+{
+ g_autoptr (GStrvBuilder) builder = g_strv_builder_new ();
+
+ if (!shortcuts)
+ return NULL;
+
+ for (int i = 0; shortcuts[i]; i++) {
+ if (g_str_equal (shortcut, shortcuts[i]))
+ continue;
+ g_strv_builder_add (builder, shortcuts[i]);
+ }
+
+ return g_strv_builder_end (builder);
+}
+
+
+static void
+on_remove_button_clicked (gpointer user_data)
+{
+ struct ShortcutData *data = user_data;
+ MsOskPanel *self = MS_OSK_PANEL (data->self);
+ g_auto (GStrv) pos_terminal_shortcuts = NULL;
+ g_auto (GStrv) shortcuts = NULL;
+
+ pos_terminal_shortcuts = g_settings_get_strv (self->pos_terminal_settings, SHORTCUTS_KEY);
+ shortcuts = shortcut_remove ((const char * const *) pos_terminal_shortcuts, data->shortcut_string);
+
+ g_settings_set_strv (self->pos_terminal_settings, SHORTCUTS_KEY, (const char * const *)shortcuts);
+
+ shortcut_data_free (data);
+}
+
+
static GtkWidget *
create_shortcuts_row (gpointer item, gpointer user_data)
{
MsOskPanel *self = MS_OSK_PANEL (user_data);
GtkStringObject *string = GTK_STRING_OBJECT (item);
- GtkWidget *label = gtk_shortcut_label_new (gtk_string_object_get_string (string));
- GtkDragSource *drag_source = gtk_drag_source_new ();
- g_autoptr (GdkContentProvider) type = NULL;
- GtkDropTarget *target = gtk_drop_target_new (G_TYPE_INVALID, GDK_ACTION_COPY);
+ const char *shortcut_string = gtk_string_object_get_string (string);
+ GtkWidget *label = gtk_shortcut_label_new (shortcut_string);
+
+ GtkWidget *row_box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
+ GtkWidget *remove_btn = gtk_button_new_from_icon_name ("window-close-symbolic");
+
GType targets[] = { GTK_TYPE_STRING_OBJECT };
+ g_autoptr (GdkContentProvider) type = NULL;
+ GtkDragSource *drag_source;
+ GtkDropTarget *target;
+ struct ShortcutData *data;
+
+ data = g_new (struct ShortcutData, 1);
+ data->self = g_object_ref (self);
+ data->shortcut_string = g_strdup (shortcut_string);
+
+ gtk_widget_add_css_class (row_box, "shortcut-row");
+
+ gtk_box_append (GTK_BOX (row_box), label);
+ gtk_widget_add_css_class (remove_btn, "flat");
+ gtk_widget_add_css_class (remove_btn, "circular");
+
+ gtk_widget_set_hexpand (remove_btn, TRUE);
+ gtk_widget_set_halign (remove_btn, GTK_ALIGN_END);
+
+ gtk_box_append (GTK_BOX (row_box), remove_btn);
+
+ g_signal_connect_swapped (remove_btn, "clicked", G_CALLBACK (on_remove_button_clicked), data);
+
+ drag_source = gtk_drag_source_new ();
+ target = gtk_drop_target_new (G_TYPE_INVALID, GDK_ACTION_COPY);
type = gdk_content_provider_new_typed (GTK_TYPE_STRING_OBJECT, string);
/* drag */
gtk_drag_source_set_content (drag_source, type);
- g_signal_connect (drag_source, "drag-begin", G_CALLBACK (on_drag_begin), label);
- gtk_widget_add_controller (label, GTK_EVENT_CONTROLLER (drag_source));
+ g_signal_connect (drag_source, "drag-begin", G_CALLBACK (on_drag_begin), row_box);
+ gtk_widget_add_controller (row_box, GTK_EVENT_CONTROLLER (drag_source));
/* drop */
gtk_drop_target_set_gtypes (target, targets, G_N_ELEMENTS (targets));
g_signal_connect (target, "drop", G_CALLBACK (on_drop), self);
/* No ref as we just use it for string comparison */
g_object_set_data (G_OBJECT (target), "ms-str", string);
- gtk_widget_add_controller (label, GTK_EVENT_CONTROLLER (target));
+ gtk_widget_add_controller (row_box, GTK_EVENT_CONTROLLER (target));
- return label;
+ return row_box;
}
@@ -420,6 +499,16 @@ is_osk_app (void)
}
+static void
+on_new_shortcut_clicked (MsOskPanel *self)
+{
+ GtkWidget *dialog;
+
+ dialog = ms_osk_add_shortcut_dialog_new ();
+ adw_dialog_present (ADW_DIALOG (dialog), GTK_WIDGET (self));
+}
+
+
static void
ms_osk_panel_finalize (GObject *object)
{
@@ -466,6 +555,7 @@ ms_osk_panel_class_init (MsOskPanelClass *klass)
/* Terminal layout group */
gtk_widget_class_bind_template_child (widget_class, MsOskPanel, terminal_layout_group);
gtk_widget_class_bind_template_child (widget_class, MsOskPanel, shortcuts_box);
+ gtk_widget_class_bind_template_callback (widget_class, on_new_shortcut_clicked);
/* Stevia scaling */
gtk_widget_class_bind_template_child (widget_class, MsOskPanel, osk_scaling_group);
diff --git a/src/ui/ms-osk-add-shortcut-dialog.ui b/src/ui/ms-osk-add-shortcut-dialog.ui
new file mode 100644
index 0000000000000000000000000000000000000000..4359ba88fe353ca69297646075c769e2b18982a2
--- /dev/null
+++ b/src/ui/ms-osk-add-shortcut-dialog.ui
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+ Add Shortcut
+
+
+
+
+
diff --git a/src/ui/ms-osk-panel.ui b/src/ui/ms-osk-panel.ui
index c921526e80d7918e9eaf47033ba924c6e9900d4f..aa33910edfaf731f091802c9535cb277420815fb 100644
--- a/src/ui/ms-osk-panel.ui
+++ b/src/ui/ms-osk-panel.ui
@@ -151,6 +151,21 @@
Terminal Layout
False
+
+
+
+
+ list-add-symbolic
+
+ Add Shortcut
+
+
+
+
+
+
Shortcuts