diff --git a/docs/reference/gtk/input-handling.md b/docs/reference/gtk/input-handling.md index 5fdfeea282..db54c4b350 100644 --- a/docs/reference/gtk/input-handling.md +++ b/docs/reference/gtk/input-handling.md @@ -137,6 +137,14 @@ capture phase, and key bindings locally, during the target phase. Under the hood, all shortcuts are represented as instances of `GtkShortcut`, and they are managed by `GtkShortcutController`. +## Text input + +When actual text input is needed (i.e. not just keyboard shortcuts), +input method support can be added to a widget by connecting an input +method context and listening to its `::commit` signal. To create a new +input method context, use gtk_im_multicontext_new(), to provide it with +input, use gtk_event_controller_key_set_im_context(). + ## Event controllers and gestures Event controllers are standalone objects that can perform diff --git a/gtk/gtkimcontext.c b/gtk/gtkimcontext.c index 10e883e6ca..c0796612b2 100644 --- a/gtk/gtkimcontext.c +++ b/gtk/gtkimcontext.c @@ -31,27 +31,16 @@ * `GtkIMContext` is used by GTK text input widgets like `GtkText` * to map from key events to Unicode character strings. * - * By default, GTK uses a platform-dependent default input method. - * On Windows, the default implementation is IME-based and on Wayland, - * it is using the Wayland text protocol. The choice can be overridden - * programmatically via the [property@Gtk.Settings:gtk-im-module] setting. - * Users may set the `GTK_IM_MODULE` environment variable to override the - * default. - * - * Text widgets have a :im-module property (e.g. [property@Gtk.TextView:im-module]) - * that may also be used to set input methods for specific widget instances. - * For instance, a certain entry widget might be expected to contain - * certain characters which would be easier to input with a specific - * input method. - * * An input method may consume multiple key events in sequence before finally * outputting the composed result. This is called *preediting*, and an input * method may provide feedback about this process by displaying the intermediate - * composition states as preedit text. + * composition states as preedit text. To do so, the `GtkIMContext` will emit + * [signal@Gtk.IMContext::preedit-start], [signal@Gtk.IMContext::preedit-changed] + * and [signal@Gtk.IMContext::preedit-end] signals. * - * For instance, the built-in GTK input method `GtkIMContextSimple` implements - * the input of arbitrary Unicode code points by holding down the - * Control and Shift keys and then typing U + * For instance, the built-in GTK input method [class@Gtk.IMContextSimple] + * implements the input of arbitrary Unicode code points by holding down the + * Control and Shift keys and then typing u * followed by the hexadecimal digits of the code point. When releasing the * Control and Shift keys, preediting ends and the * character is inserted as text. For example, @@ -63,6 +52,9 @@ * Additional input methods can be made available for use by GTK widgets as * loadable modules. An input method module is a small shared library which * provides a `GIOExtension` for the extension point named "gtk-im-module". + * + * To connect a widget to the users preferred input method, you should use + * [class@Gtk.IMMulticontext]. */ enum { @@ -123,72 +115,74 @@ G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GtkIMContext, gtk_im_context, G_TYPE_OBJECT /** * GtkIMContextClass: - * @preedit_start: Default handler of the `GtkIMContext::preedit-start` signal. - * @preedit_end: Default handler of the `GtkIMContext::preedit-end` signal. - * @preedit_changed: Default handler of the `GtkIMContext::preedit-changed` + * @preedit_start: Default handler of the [signal@Gtk.IMContext::preedit-start] signal. + * @preedit_end: Default handler of the [signal@Gtk.IMContext::preedit-end] signal. + * @preedit_changed: Default handler of the [signal@Gtk.IMContext::preedit-changed] * signal. - * @commit: Default handler of the `GtkIMContext::commit` signal. + * @commit: Default handler of the [signal@Gtk.IMContext::commit] signal. * @retrieve_surrounding: Default handler of the - * `GtkIMContext::retrieve-surrounding` signal. + * [signal@Gtk.IMContext::retrieve-surrounding] signal. * @delete_surrounding: Default handler of the - * `GtkIMContext::delete-surrounding` signal. - * @set_client_widget: Called via gtk_im_context_set_client_widget() when the - * input window where the entered text will appear changes. Override this to - * keep track of the current input window, for instance for the purpose of + * [signal@Gtk.IMContext::delete-surrounding] signal. + * @set_client_widget: Called via [method@Gtk.IMContext.set_client_widget] when + * the input window where the entered text will appear changes. Override this + * to keep track of the current input window, for instance for the purpose of * positioning a status display of your input method. - * @get_preedit_string: Called via gtk_im_context_get_preedit_string() to - * retrieve the text currently being preedited for display at the cursor + * @get_preedit_string: Called via [method@Gtk.IMContext.get_preedit_string] + * to retrieve the text currently being preedited for display at the cursor * position. Any input method which composes complex characters or any * other compositions from multiple sequential key presses should override * this method to provide feedback. - * @filter_keypress: Called via gtk_im_context_filter_keypress() on every + * @filter_keypress: Called via [method@Gtk.IMContext.filter_keypress] on every * key press or release event. Every non-trivial input method needs to * override this in order to implement the mapping from key events to text. * A return value of %TRUE indicates to the caller that the event was - * consumed by the input method. In that case, the `GtkIMContext::commit` + * consumed by the input method. In that case, the [signal@Gtk.IMContext::commit] * signal should be emitted upon completion of a key sequence to pass the * resulting text back to the input widget. Alternatively, %FALSE may be * returned to indicate that the event wasn’t handled by the input method. * If a builtin mapping exists for the key, it is used to produce a * character. - * @focus_in: Called via gtk_im_context_focus_in() when the input widget + * @focus_in: Called via [method@Gtk.IMContext.focus_in] when the input widget * has gained focus. May be overridden to keep track of the current focus. - * @focus_out: Called via gtk_im_context_focus_out() when the input widget + * @focus_out: Called via [method@Gtk.IMContext.focus_out] when the input widget * has lost focus. May be overridden to keep track of the current focus. - * @reset: Called via gtk_im_context_reset() to signal a change such as a + * @reset: Called via [method@Gtk.IMContext.reset] to signal a change such as a * change in cursor position. An input method that implements preediting * should override this method to clear the preedit state on reset. - * @set_cursor_location: Called via gtk_im_context_set_cursor_location() + * @set_cursor_location: Called via [method@Gtk.IMContext.set_cursor_location] * to inform the input method of the current cursor location relative to * the client window. May be overridden to implement the display of popup * windows at the cursor position. - * @set_use_preedit: Called via gtk_im_context_set_use_preedit() to control + * @set_use_preedit: Called via [method@Gtk.IMContext.set_use_preedit] to control * the use of the preedit string. Override this to display feedback by some * other means if turned off. - * @set_surrounding: Called via gtk_im_context_set_surrounding() in response - * to signal `GtkIMContext::retrieve-surrounding` to update the input + * @set_surrounding: Called via [method@Gtk.IMContext.set_surrounding] in + * response to [signal@Gtk.IMContext::retrieve-surrounding] signal to update + * the input method’s idea of the context around the cursor. It is not necessary + * to override this method even with input methods which implement + * context-dependent behavior. The base implementation is sufficient for + * [method@Gtk.IMContext.get_surrounding] to work. + * @get_surrounding: Called via [method@Gtk.IMContext.get_surrounding] to update + * the context around the cursor location. It is not necessary to override this + * method even with input methods which implement context-dependent behavior. + * The base implementation emits [signal@Gtk.IMContext::retrieve-surrounding] + * and records the context received by the subsequent invocation of + * [vfunc@Gtk.IMContext.get_surrounding]. + * @set_surrounding_with_selection: Called via + * [method@Gtk.IMContext.set_surrounding_with_selection] in response to the + * [signal@Gtk.IMContext::retrieve-surrounding] signal to update the input * method’s idea of the context around the cursor. It is not necessary to * override this method even with input methods which implement * context-dependent behavior. The base implementation is sufficient for - * gtk_im_context_get_surrounding() to work. - * @get_surrounding: Called via gtk_im_context_get_surrounding() to update - * the context around the cursor location. It is not necessary to override + * [method@Gtk.IMContext.get_surrounding] to work. + * @get_surrounding_with_selection: Called via + * [method@Gtk.IMContext.get_surrounding_with_selection] to update the + * context around the cursor location. It is not necessary to override * this method even with input methods which implement context-dependent * behavior. The base implementation emits - * `GtkIMContext::retrieve-surrounding` and records the context received - * by the subsequent invocation of @get_surrounding. - * @set_surrounding_with_selection: Called via gtk_im_context_set_surrounding_with_selection() - * in response to signal `GtkIMContext::retrieve-surrounding` to update the input - * method’s idea of the context around the cursor. It is not necessary to - * override this method even with input methods which implement - * context-dependent behavior. The base implementation is sufficient for - * gtk_im_context_get_surrounding() to work. - * @get_surrounding_with_selection: Called via gtk_im_context_get_surrounding_with_selection() - * to update the context around the cursor location. It is not necessary to override - * this method even with input methods which implement context-dependent - * behavior. The base implementation emits - * `GtkIMContext::retrieve-surrounding` and records the context received - * by the subsequent invocation of @get_surrounding. + * [signal@Gtk.IMContext::retrieve-surrounding] and records the context + * received by the subsequent invocation of [vfunc@Gtk.IMContext.get_surrounding]. */ static void gtk_im_context_class_init (GtkIMContextClass *klass) @@ -218,6 +212,7 @@ gtk_im_context_class_init (GtkIMContextClass *klass) NULL, NULL, NULL, G_TYPE_NONE, 0); + /** * GtkIMContext::preedit-end: * @context: the object on which the signal is emitted @@ -233,14 +228,16 @@ gtk_im_context_class_init (GtkIMContextClass *klass) NULL, NULL, NULL, G_TYPE_NONE, 0); + /** * GtkIMContext::preedit-changed: * @context: the object on which the signal is emitted * * The ::preedit-changed signal is emitted whenever the preedit sequence - * currently being entered has changed. It is also emitted at the end of - * a preedit sequence, in which case - * gtk_im_context_get_preedit_string() returns the empty string. + * currently being entered has changed. + * + * It is also emitted at the end of a preedit sequence, in which case + * [method@Gtk.IMContext.get_preedit_string] returns the empty string. */ im_context_signals[PREEDIT_CHANGED] = g_signal_new (I_("preedit-changed"), @@ -250,14 +247,20 @@ gtk_im_context_class_init (GtkIMContextClass *klass) NULL, NULL, NULL, G_TYPE_NONE, 0); + /** * GtkIMContext::commit: * @context: the object on which the signal is emitted * @str: the completed character(s) entered by the user * * The ::commit signal is emitted when a complete input sequence - * has been entered by the user. This can be a single character - * immediately after a key press or the final result of preediting. + * has been entered by the user. + * + * If the commit comes after a preediting sequence, the + * ::commit signal is emitted after ::preedit-end. + * + * This can be a single character immediately after a key press or + * the final result of preediting. */ im_context_signals[COMMIT] = g_signal_new (I_("commit"), @@ -268,14 +271,16 @@ gtk_im_context_class_init (GtkIMContextClass *klass) NULL, G_TYPE_NONE, 1, G_TYPE_STRING); + /** * GtkIMContext::retrieve-surrounding: * @context: the object on which the signal is emitted * * The ::retrieve-surrounding signal is emitted when the input method - * requires the context surrounding the cursor. The callback should set - * the input method surrounding context by calling the - * gtk_im_context_set_surrounding() method. + * requires the context surrounding the cursor. + * + * The callback should set the input method surrounding context by + * calling the [method@Gtk.IMContext.set_surrounding] method. * * Returns: %TRUE if the signal was handled. */ @@ -290,12 +295,13 @@ gtk_im_context_class_init (GtkIMContextClass *klass) g_signal_set_va_marshaller (im_context_signals[RETRIEVE_SURROUNDING], G_TYPE_FROM_CLASS (klass), _gtk_marshal_BOOLEAN__VOIDv); + /** * GtkIMContext::delete-surrounding: * @context: the object on which the signal is emitted - * @offset: the character offset from the cursor position of the text - * to be deleted. A negative value indicates a position before - * the cursor. + * @offset: the character offset from the cursor position of the text + * to be deleted. A negative value indicates a position before + * the cursor. * @n_chars: the number of characters to be deleted * * The ::delete-surrounding signal is emitted when the input method @@ -317,6 +323,14 @@ gtk_im_context_class_init (GtkIMContextClass *klass) G_TYPE_FROM_CLASS (klass), _gtk_marshal_BOOLEAN__INT_INTv); + /** + * GtkIMContext:input-purpose: + * + * The purpose of the text field that the `GtkIMContext is connected to. + * + * This property can be used by on-screen keyboards and other input + * methods to adjust their behaviour. + */ properties[PROP_INPUT_PURPOSE] = g_param_spec_enum ("input-purpose", P_("Purpose"), @@ -325,6 +339,12 @@ gtk_im_context_class_init (GtkIMContextClass *klass) GTK_INPUT_PURPOSE_FREE_FORM, G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY); + /** + * GtkIMContext:input-hints: + * + * Additional hints that allow input methods to fine-tune + * their behaviour. + */ properties[PROP_INPUT_HINTS] = g_param_spec_flags ("input-hints", P_("hints"), @@ -465,9 +485,9 @@ gtk_im_context_set_client_widget (GtkIMContext *context, * string. The string retrieved must be freed with g_free(). * @attrs: (out) (transfer full): location to store the retrieved * attribute list. When you are done with this list, you - * must unreference it with pango_attr_list_unref(). - * @cursor_pos: (out): location to store position of cursor (in characters) - * within the preedit string. + * must unreference it with [method@Pango.AttrList.unref]. + * @cursor_pos: (out): location to store position of cursor + * (in characters) within the preedit string. * * Retrieve the current preedit string for the input context, * and a list of attributes to apply to the string. @@ -527,7 +547,7 @@ gtk_im_context_filter_keypress (GtkIMContext *context, * @group: the active keyboard group for the event * * Allow an input method to forward key press and release events - * to another input methodm without necessarily having a `GdkEvent` + * to another input method without necessarily having a `GdkEvent` * available. * * Returns: %TRUE if the input method handled the key event. @@ -673,7 +693,7 @@ gtk_im_context_reset (GtkIMContext *context) * Notify the input method that a change in cursor * position has been made. * - * The location is relative to the client window. + * The location is relative to the client widget. */ void gtk_im_context_set_cursor_location (GtkIMContext *context, @@ -750,8 +770,8 @@ gtk_im_context_set_surrounding (GtkIMContext *context, * * Sets surrounding context around the insertion point and preedit * string. This function is expected to be called in response to the - * GtkIMContext::retrieve_surrounding signal, and will likely have no - * effect if called at other times. + * [signal@Gtk.IMContext::retrieve_surrounding] signal, and will likely + * have no effect if called at other times. * * Since: 4.2 */ @@ -902,24 +922,24 @@ gtk_im_context_get_surrounding_with_selection (GtkIMContext *context, * * Asks the widget that the input context is attached to delete * characters around the cursor position by emitting the - * GtkIMContext::delete_surrounding signal. + * `::delete_surrounding` signal. * * Note that @offset and @n_chars are in characters not in bytes * which differs from the usage other places in `GtkIMContext`. * * In order to use this function, you should first call - * gtk_im_context_get_surrounding() to get the current context, and - * call this function immediately afterwards to make sure that you + * [method@Gtk.IMContext.get_surrounding] to get the current context, + * and call this function immediately afterwards to make sure that you * know what you are deleting. You should also account for the fact * that even if the signal was handled, the input context might not * have deleted all the characters that were requested to be deleted. * * This function is used by an input method that wants to make - * subsitutions in the existing text in response to new input. It is - * not useful for applications. - * + * subsitutions in the existing text in response to new input. + * It is not useful for applications. + * * Returns: %TRUE if the signal was handled. - **/ + */ gboolean gtk_im_context_delete_surrounding (GtkIMContext *context, int offset, diff --git a/gtk/gtkimmulticontext.c b/gtk/gtkimmulticontext.c index f26f1dd05c..47347d299b 100644 --- a/gtk/gtkimmulticontext.c +++ b/gtk/gtkimmulticontext.c @@ -32,8 +32,8 @@ /** * GtkIMMulticontext: * - * `GtkIMMulticontext` is input method supporting multiple, switchable input - * methods. + * `GtkIMMulticontext` is an input method context supporting multiple, + * switchable input methods. * * Text widgets such as `GtkText` or `GtkTextView` use a `GtkIMMultiContext` * to implement their `im-module` property for switching between different @@ -587,12 +587,16 @@ gtk_im_multicontext_get_context_id (GtkIMMulticontext *context) /** * gtk_im_multicontext_set_context_id: * @context: a `GtkIMMulticontext` - * @context_id: the id to use + * @context_id: (nullable): the id to use * * Sets the context id for @context. * * This causes the currently active delegate of @context to be * replaced by the delegate corresponding to the new context id. + * + * Setting this to a non-%NULL value overrides the system-wide + * IM module setting. See the [property@Gtk.Settings:gtk-im-module] + * property. */ void gtk_im_multicontext_set_context_id (GtkIMMulticontext *context, diff --git a/gtk/gtktext.c b/gtk/gtktext.c index 1cb086dd1f..52cbf56a09 100644 --- a/gtk/gtktext.c +++ b/gtk/gtktext.c @@ -867,7 +867,7 @@ gtk_text_class_init (GtkTextClass *class) * * Which IM (input method) module should be used for this self. * - * See [class@Gtk.IMContext]. + * See [class@Gtk.IMMulticontext]. * * Setting this to a non-%NULL value overrides the system-wide * IM module setting. See the [property@Gtk.Settings:gtk-im-module] diff --git a/gtk/gtktextview.c b/gtk/gtktextview.c index 6e1a922e37..fbb00a685b 100644 --- a/gtk/gtktextview.c +++ b/gtk/gtktextview.c @@ -1077,7 +1077,7 @@ gtk_text_view_class_init (GtkTextViewClass *klass) * * Which IM (input method) module should be used for this text_view. * - * See [class@Gtk.IMContext]. + * See [class@Gtk.IMMulticontext]. * * Setting this to a non-%NULL value overrides the system-wide IM module * setting. See the GtkSettings [property@Gtk.Settings:gtk-im-module] property. diff --git a/tests/input.c b/tests/input.c new file mode 100644 index 0000000000..01099d2129 --- /dev/null +++ b/tests/input.c @@ -0,0 +1,153 @@ +/* Demonstrates hooking up an input method context to a custom widget. + */ +#include + +G_DECLARE_FINAL_TYPE (DemoWidget, demo_widget, DEMO, WIDGET, GtkWidget) + +struct _DemoWidget +{ + GtkWidget parent_instance; + + GtkIMContext *im_context; + PangoLayout *layout; +}; + +struct _DemoWidgetClass +{ + GtkWidgetClass parent_class; +}; + +G_DEFINE_TYPE (DemoWidget, demo_widget, GTK_TYPE_WIDGET) + +static void +commit_cb (GtkIMContext *context, + const char *str, + DemoWidget *demo) +{ + pango_layout_set_text (demo->layout, str, -1); + pango_layout_set_attributes (demo->layout, NULL); + gtk_widget_queue_draw (GTK_WIDGET (demo)); +} + +static void +preedit_changed_cb (GtkIMContext *context, + DemoWidget *demo) +{ + char *str; + PangoAttrList *attrs; + int cursor_pos; + + gtk_im_context_get_preedit_string (context, &str, &attrs, &cursor_pos); + pango_layout_set_text (demo->layout, str, -1); + pango_layout_set_attributes (demo->layout, attrs); + g_free (str); + pango_attr_list_unref (attrs); + + gtk_widget_queue_draw (GTK_WIDGET (demo)); +} + +static gboolean +key_pressed_cb (GtkEventControllerKey *controller, + guint keyval, + guint keycode, + GdkModifierType state, + DemoWidget *demo) +{ + if (keyval == GDK_KEY_BackSpace) + { + pango_layout_set_text (demo->layout, "", -1); + pango_layout_set_attributes (demo->layout, NULL); + gtk_widget_queue_draw (GTK_WIDGET (demo)); + + return TRUE; + } + + return FALSE; +} + +static void +demo_widget_init (DemoWidget *demo) +{ + GtkEventController *controller; + + gtk_widget_set_focusable (GTK_WIDGET (demo), TRUE); + + demo->layout = gtk_widget_create_pango_layout (GTK_WIDGET (demo), ""); + + demo->im_context = gtk_im_multicontext_new (); + + g_signal_connect (demo->im_context, "commit", G_CALLBACK (commit_cb), demo); + g_signal_connect (demo->im_context, "preedit-changed", G_CALLBACK (preedit_changed_cb), demo); + + controller = gtk_event_controller_key_new (); + gtk_event_controller_key_set_im_context (GTK_EVENT_CONTROLLER_KEY (controller), + demo->im_context); + + g_signal_connect (controller, "key-pressed", G_CALLBACK (key_pressed_cb), demo); + + gtk_widget_add_controller (GTK_WIDGET (demo), controller); +} + +static void +demo_widget_dispose (GObject *object) +{ + DemoWidget *demo = DEMO_WIDGET (object); + + g_clear_object (&demo->layout); + g_clear_object (&demo->im_context); + + G_OBJECT_CLASS (demo_widget_parent_class)->dispose (object); +} + +static void +demo_widget_snapshot (GtkWidget *widget, + GtkSnapshot *snapshot) +{ + DemoWidget *demo = DEMO_WIDGET (widget); + + gtk_snapshot_render_layout (snapshot, + gtk_widget_get_style_context (widget), + 0, 0, + demo->layout); +} + +static void +demo_widget_class_init (DemoWidgetClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class); + + object_class->dispose = demo_widget_dispose; + + widget_class->snapshot = demo_widget_snapshot; +} + +static GtkWidget * +demo_widget_new (void) +{ + return g_object_new (demo_widget_get_type (), NULL); +} + +int +main (int argc, char *argv[]) +{ + GtkWindow *window; + GtkWidget *demo; + + gtk_init (); + + window = GTK_WINDOW (gtk_window_new ()); + + demo = demo_widget_new (); + + gtk_window_set_child (window, demo); + + gtk_window_present (window); + + gtk_widget_grab_focus (demo); + + while (g_list_model_get_n_items (gtk_window_get_toplevels ()) > 0) + g_main_context_iteration (NULL, TRUE); + + return 0; +} diff --git a/tests/meson.build b/tests/meson.build index b548db4e7a..9acf884231 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -1,5 +1,6 @@ gtk_tests = [ # testname, optional extra sources + ['input'], ['testpopup'], ['testupload'], ['testtransform'],