michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "mozilla/ArrayUtils.h" michael@0: #include "mozilla/MathAlgorithms.h" michael@0: #include "mozilla/TextEvents.h" michael@0: michael@0: #include "NativeKeyBindings.h" michael@0: #include "nsString.h" michael@0: #include "nsMemory.h" michael@0: #include "nsGtkKeyUtils.h" michael@0: michael@0: #include michael@0: #include michael@0: #include michael@0: michael@0: namespace mozilla { michael@0: namespace widget { michael@0: michael@0: static nsIWidget::DoCommandCallback gCurrentCallback; michael@0: static void *gCurrentCallbackData; michael@0: static bool gHandled; michael@0: michael@0: // Common GtkEntry and GtkTextView signals michael@0: static void michael@0: copy_clipboard_cb(GtkWidget *w, gpointer user_data) michael@0: { michael@0: gCurrentCallback(CommandCopy, gCurrentCallbackData); michael@0: g_signal_stop_emission_by_name(w, "copy_clipboard"); michael@0: gHandled = true; michael@0: } michael@0: michael@0: static void michael@0: cut_clipboard_cb(GtkWidget *w, gpointer user_data) michael@0: { michael@0: gCurrentCallback(CommandCut, gCurrentCallbackData); michael@0: g_signal_stop_emission_by_name(w, "cut_clipboard"); michael@0: gHandled = true; michael@0: } michael@0: michael@0: // GTK distinguishes between display lines (wrapped, as they appear on the michael@0: // screen) and paragraphs, which are runs of text terminated by a newline. michael@0: // We don't have this distinction, so we always use editor's notion of michael@0: // lines, which are newline-terminated. michael@0: michael@0: static const Command sDeleteCommands[][2] = { michael@0: // backward, forward michael@0: { CommandDeleteCharBackward, CommandDeleteCharForward }, // CHARS michael@0: { CommandDeleteWordBackward, CommandDeleteWordForward }, // WORD_ENDS michael@0: { CommandDeleteWordBackward, CommandDeleteWordForward }, // WORDS michael@0: { CommandDeleteToBeginningOfLine, CommandDeleteToEndOfLine }, // LINES michael@0: { CommandDeleteToBeginningOfLine, CommandDeleteToEndOfLine }, // LINE_ENDS michael@0: { CommandDeleteToBeginningOfLine, CommandDeleteToEndOfLine }, // PARAGRAPH_ENDS michael@0: { CommandDeleteToBeginningOfLine, CommandDeleteToEndOfLine }, // PARAGRAPHS michael@0: // This deletes from the end of the previous word to the beginning of the michael@0: // next word, but only if the caret is not in a word. michael@0: // XXX need to implement in editor michael@0: { CommandDoNothing, CommandDoNothing } // WHITESPACE michael@0: }; michael@0: michael@0: static void michael@0: delete_from_cursor_cb(GtkWidget *w, GtkDeleteType del_type, michael@0: gint count, gpointer user_data) michael@0: { michael@0: g_signal_stop_emission_by_name(w, "delete_from_cursor"); michael@0: gHandled = true; michael@0: michael@0: bool forward = count > 0; michael@0: if (uint32_t(del_type) >= ArrayLength(sDeleteCommands)) { michael@0: // unsupported deletion type michael@0: return; michael@0: } michael@0: michael@0: if (del_type == GTK_DELETE_WORDS) { michael@0: // This works like word_ends, except we first move the caret to the michael@0: // beginning/end of the current word. michael@0: if (forward) { michael@0: gCurrentCallback(CommandWordNext, gCurrentCallbackData); michael@0: gCurrentCallback(CommandWordPrevious, gCurrentCallbackData); michael@0: } else { michael@0: gCurrentCallback(CommandWordPrevious, gCurrentCallbackData); michael@0: gCurrentCallback(CommandWordNext, gCurrentCallbackData); michael@0: } michael@0: } else if (del_type == GTK_DELETE_DISPLAY_LINES || michael@0: del_type == GTK_DELETE_PARAGRAPHS) { michael@0: michael@0: // This works like display_line_ends, except we first move the caret to the michael@0: // beginning/end of the current line. michael@0: if (forward) { michael@0: gCurrentCallback(CommandBeginLine, gCurrentCallbackData); michael@0: } else { michael@0: gCurrentCallback(CommandEndLine, gCurrentCallbackData); michael@0: } michael@0: } michael@0: michael@0: Command command = sDeleteCommands[del_type][forward]; michael@0: if (!command) { michael@0: return; // unsupported command michael@0: } michael@0: michael@0: unsigned int absCount = Abs(count); michael@0: for (unsigned int i = 0; i < absCount; ++i) { michael@0: gCurrentCallback(command, gCurrentCallbackData); michael@0: } michael@0: } michael@0: michael@0: static const Command sMoveCommands[][2][2] = { michael@0: // non-extend { backward, forward }, extend { backward, forward } michael@0: // GTK differentiates between logical position, which is prev/next, michael@0: // and visual position, which is always left/right. michael@0: // We should fix this to work the same way for RTL text input. michael@0: { // LOGICAL_POSITIONS michael@0: { CommandCharPrevious, CommandCharNext }, michael@0: { CommandSelectCharPrevious, CommandSelectCharNext } michael@0: }, michael@0: { // VISUAL_POSITIONS michael@0: { CommandCharPrevious, CommandCharNext }, michael@0: { CommandSelectCharPrevious, CommandSelectCharNext } michael@0: }, michael@0: { // WORDS michael@0: { CommandWordPrevious, CommandWordNext }, michael@0: { CommandSelectWordPrevious, CommandSelectWordNext } michael@0: }, michael@0: { // DISPLAY_LINES michael@0: { CommandLinePrevious, CommandLineNext }, michael@0: { CommandSelectLinePrevious, CommandSelectLineNext } michael@0: }, michael@0: { // DISPLAY_LINE_ENDS michael@0: { CommandBeginLine, CommandEndLine }, michael@0: { CommandSelectBeginLine, CommandSelectEndLine } michael@0: }, michael@0: { // PARAGRAPHS michael@0: { CommandLinePrevious, CommandLineNext }, michael@0: { CommandSelectLinePrevious, CommandSelectLineNext } michael@0: }, michael@0: { // PARAGRAPH_ENDS michael@0: { CommandBeginLine, CommandEndLine }, michael@0: { CommandSelectBeginLine, CommandSelectEndLine } michael@0: }, michael@0: { // PAGES michael@0: { CommandMovePageUp, CommandMovePageDown }, michael@0: { CommandSelectPageUp, CommandSelectPageDown } michael@0: }, michael@0: { // BUFFER_ENDS michael@0: { CommandMoveTop, CommandMoveBottom }, michael@0: { CommandSelectTop, CommandSelectBottom } michael@0: }, michael@0: { // HORIZONTAL_PAGES (unsupported) michael@0: { CommandDoNothing, CommandDoNothing }, michael@0: { CommandDoNothing, CommandDoNothing } michael@0: } michael@0: }; michael@0: michael@0: static void michael@0: move_cursor_cb(GtkWidget *w, GtkMovementStep step, gint count, michael@0: gboolean extend_selection, gpointer user_data) michael@0: { michael@0: g_signal_stop_emission_by_name(w, "move_cursor"); michael@0: gHandled = true; michael@0: bool forward = count > 0; michael@0: if (uint32_t(step) >= ArrayLength(sMoveCommands)) { michael@0: // unsupported movement type michael@0: return; michael@0: } michael@0: michael@0: Command command = sMoveCommands[step][extend_selection][forward]; michael@0: if (!command) { michael@0: return; // unsupported command michael@0: } michael@0: michael@0: unsigned int absCount = Abs(count); michael@0: for (unsigned int i = 0; i < absCount; ++i) { michael@0: gCurrentCallback(command, gCurrentCallbackData); michael@0: } michael@0: } michael@0: michael@0: static void michael@0: paste_clipboard_cb(GtkWidget *w, gpointer user_data) michael@0: { michael@0: gCurrentCallback(CommandPaste, gCurrentCallbackData); michael@0: g_signal_stop_emission_by_name(w, "paste_clipboard"); michael@0: gHandled = true; michael@0: } michael@0: michael@0: // GtkTextView-only signals michael@0: static void michael@0: select_all_cb(GtkWidget *w, gboolean select, gpointer user_data) michael@0: { michael@0: gCurrentCallback(CommandSelectAll, gCurrentCallbackData); michael@0: g_signal_stop_emission_by_name(w, "select_all"); michael@0: gHandled = true; michael@0: } michael@0: michael@0: NativeKeyBindings* NativeKeyBindings::sInstanceForSingleLineEditor = nullptr; michael@0: NativeKeyBindings* NativeKeyBindings::sInstanceForMultiLineEditor = nullptr; michael@0: michael@0: // static michael@0: NativeKeyBindings* michael@0: NativeKeyBindings::GetInstance(NativeKeyBindingsType aType) michael@0: { michael@0: switch (aType) { michael@0: case nsIWidget::NativeKeyBindingsForSingleLineEditor: michael@0: if (!sInstanceForSingleLineEditor) { michael@0: sInstanceForSingleLineEditor = new NativeKeyBindings(); michael@0: sInstanceForSingleLineEditor->Init(aType); michael@0: } michael@0: return sInstanceForSingleLineEditor; michael@0: michael@0: default: michael@0: // fallback to multiline editor case in release build michael@0: MOZ_ASSERT(false, "aType is invalid or not yet implemented"); michael@0: case nsIWidget::NativeKeyBindingsForMultiLineEditor: michael@0: case nsIWidget::NativeKeyBindingsForRichTextEditor: michael@0: if (!sInstanceForMultiLineEditor) { michael@0: sInstanceForMultiLineEditor = new NativeKeyBindings(); michael@0: sInstanceForMultiLineEditor->Init(aType); michael@0: } michael@0: return sInstanceForMultiLineEditor; michael@0: } michael@0: } michael@0: michael@0: // static michael@0: void michael@0: NativeKeyBindings::Shutdown() michael@0: { michael@0: delete sInstanceForSingleLineEditor; michael@0: sInstanceForSingleLineEditor = nullptr; michael@0: delete sInstanceForMultiLineEditor; michael@0: sInstanceForMultiLineEditor = nullptr; michael@0: } michael@0: michael@0: void michael@0: NativeKeyBindings::Init(NativeKeyBindingsType aType) michael@0: { michael@0: switch (aType) { michael@0: case nsIWidget::NativeKeyBindingsForSingleLineEditor: michael@0: mNativeTarget = gtk_entry_new(); michael@0: break; michael@0: default: michael@0: mNativeTarget = gtk_text_view_new(); michael@0: if (gtk_major_version > 2 || michael@0: (gtk_major_version == 2 && (gtk_minor_version > 2 || michael@0: (gtk_minor_version == 2 && michael@0: gtk_micro_version >= 2)))) { michael@0: // select_all only exists in gtk >= 2.2.2. Prior to that, michael@0: // ctrl+a is bound to (move to beginning, select to end). michael@0: g_signal_connect(mNativeTarget, "select_all", michael@0: G_CALLBACK(select_all_cb), this); michael@0: } michael@0: break; michael@0: } michael@0: michael@0: g_object_ref_sink(mNativeTarget); michael@0: michael@0: g_signal_connect(mNativeTarget, "copy_clipboard", michael@0: G_CALLBACK(copy_clipboard_cb), this); michael@0: g_signal_connect(mNativeTarget, "cut_clipboard", michael@0: G_CALLBACK(cut_clipboard_cb), this); michael@0: g_signal_connect(mNativeTarget, "delete_from_cursor", michael@0: G_CALLBACK(delete_from_cursor_cb), this); michael@0: g_signal_connect(mNativeTarget, "move_cursor", michael@0: G_CALLBACK(move_cursor_cb), this); michael@0: g_signal_connect(mNativeTarget, "paste_clipboard", michael@0: G_CALLBACK(paste_clipboard_cb), this); michael@0: } michael@0: michael@0: NativeKeyBindings::~NativeKeyBindings() michael@0: { michael@0: gtk_widget_destroy(mNativeTarget); michael@0: g_object_unref(mNativeTarget); michael@0: } michael@0: michael@0: bool michael@0: NativeKeyBindings::Execute(const WidgetKeyboardEvent& aEvent, michael@0: DoCommandCallback aCallback, michael@0: void* aCallbackData) michael@0: { michael@0: // If the native key event is set, it must be synthesized for tests. michael@0: // We just ignore such events because this behavior depends on system michael@0: // settings. michael@0: if (!aEvent.mNativeKeyEvent) { michael@0: // It must be synthesized event or dispatched DOM event from chrome. michael@0: return false; michael@0: } michael@0: michael@0: guint keyval; michael@0: michael@0: if (aEvent.charCode) { michael@0: keyval = gdk_unicode_to_keyval(aEvent.charCode); michael@0: } else { michael@0: keyval = michael@0: static_cast(aEvent.mNativeKeyEvent)->keyval; michael@0: } michael@0: michael@0: if (ExecuteInternal(aEvent, aCallback, aCallbackData, keyval)) { michael@0: return true; michael@0: } michael@0: michael@0: for (uint32_t i = 0; i < aEvent.alternativeCharCodes.Length(); ++i) { michael@0: uint32_t ch = aEvent.IsShift() ? michael@0: aEvent.alternativeCharCodes[i].mShiftedCharCode : michael@0: aEvent.alternativeCharCodes[i].mUnshiftedCharCode; michael@0: if (ch && ch != aEvent.charCode) { michael@0: keyval = gdk_unicode_to_keyval(ch); michael@0: if (ExecuteInternal(aEvent, aCallback, aCallbackData, keyval)) { michael@0: return true; michael@0: } michael@0: } michael@0: } michael@0: michael@0: /* michael@0: gtk_bindings_activate_event is preferable, but it has unresolved bug: michael@0: http://bugzilla.gnome.org/show_bug.cgi?id=162726 michael@0: The bug was already marked as FIXED. However, somebody reports that the michael@0: bug still exists. michael@0: Also gtk_bindings_activate may work with some non-shortcuts operations michael@0: (todo: check it). See bug 411005 and bug 406407. michael@0: michael@0: Code, which should be used after fixing GNOME bug 162726: michael@0: michael@0: gtk_bindings_activate_event(GTK_OBJECT(mNativeTarget), michael@0: static_cast(aEvent.mNativeKeyEvent)); michael@0: */ michael@0: michael@0: return false; michael@0: } michael@0: michael@0: bool michael@0: NativeKeyBindings::ExecuteInternal(const WidgetKeyboardEvent& aEvent, michael@0: DoCommandCallback aCallback, michael@0: void* aCallbackData, michael@0: guint aKeyval) michael@0: { michael@0: guint modifiers = michael@0: static_cast(aEvent.mNativeKeyEvent)->state; michael@0: michael@0: gCurrentCallback = aCallback; michael@0: gCurrentCallbackData = aCallbackData; michael@0: michael@0: gHandled = false; michael@0: #if (MOZ_WIDGET_GTK == 2) michael@0: gtk_bindings_activate(GTK_OBJECT(mNativeTarget), michael@0: aKeyval, GdkModifierType(modifiers)); michael@0: #else michael@0: gtk_bindings_activate(G_OBJECT(mNativeTarget), michael@0: aKeyval, GdkModifierType(modifiers)); michael@0: #endif michael@0: michael@0: gCurrentCallback = nullptr; michael@0: gCurrentCallbackData = nullptr; michael@0: michael@0: return gHandled; michael@0: } michael@0: michael@0: } // namespace widget michael@0: } // namespace mozilla