Backed out changeset 009ae35b0c67 (bug 806819) Backed out changeset 5a57f87f5061 (bug 806819) Backed out changeset f06cd735b5b3 (bug 806819) Backed out changeset e25a2a8d4af4 (bug 806819) Backed out changeset 70a167982c3f (bug 806819)
1588 lines
53 KiB
C++
1588 lines
53 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
|
/* vim: set ts=4 et sw=4 tw=80: */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#ifdef MOZ_LOGGING
|
|
#define FORCE_PR_LOG /* Allow logging in the release build */
|
|
#endif // MOZ_LOGGING
|
|
#include "prlog.h"
|
|
#include "prtime.h"
|
|
|
|
#include "nsGtkIMModule.h"
|
|
#include "nsWindow.h"
|
|
#include "mozilla/Likely.h"
|
|
#include "mozilla/MiscEvents.h"
|
|
#include "mozilla/Preferences.h"
|
|
#include "mozilla/TextEvents.h"
|
|
|
|
using namespace mozilla;
|
|
using namespace mozilla::widget;
|
|
|
|
#ifdef PR_LOGGING
|
|
PRLogModuleInfo* gGtkIMLog = nullptr;
|
|
|
|
static const char*
|
|
GetRangeTypeName(uint32_t aRangeType)
|
|
{
|
|
switch (aRangeType) {
|
|
case NS_TEXTRANGE_RAWINPUT:
|
|
return "NS_TEXTRANGE_RAWINPUT";
|
|
case NS_TEXTRANGE_CONVERTEDTEXT:
|
|
return "NS_TEXTRANGE_CONVERTEDTEXT";
|
|
case NS_TEXTRANGE_SELECTEDRAWTEXT:
|
|
return "NS_TEXTRANGE_SELECTEDRAWTEXT";
|
|
case NS_TEXTRANGE_SELECTEDCONVERTEDTEXT:
|
|
return "NS_TEXTRANGE_SELECTEDCONVERTEDTEXT";
|
|
case NS_TEXTRANGE_CARETPOSITION:
|
|
return "NS_TEXTRANGE_CARETPOSITION";
|
|
default:
|
|
return "UNKNOWN SELECTION TYPE!!";
|
|
}
|
|
}
|
|
|
|
static const char*
|
|
GetEnabledStateName(uint32_t aState)
|
|
{
|
|
switch (aState) {
|
|
case IMEState::DISABLED:
|
|
return "DISABLED";
|
|
case IMEState::ENABLED:
|
|
return "ENABLED";
|
|
case IMEState::PASSWORD:
|
|
return "PASSWORD";
|
|
case IMEState::PLUGIN:
|
|
return "PLUG_IN";
|
|
default:
|
|
return "UNKNOWN ENABLED STATUS!!";
|
|
}
|
|
}
|
|
#endif
|
|
|
|
const static bool kUseSimpleContextDefault = MOZ_WIDGET_GTK == 2;
|
|
|
|
nsGtkIMModule* nsGtkIMModule::sLastFocusedModule = nullptr;
|
|
bool nsGtkIMModule::sUseSimpleContext;
|
|
|
|
nsGtkIMModule::nsGtkIMModule(nsWindow* aOwnerWindow)
|
|
: mOwnerWindow(aOwnerWindow)
|
|
, mLastFocusedWindow(nullptr)
|
|
, mContext(nullptr)
|
|
, mSimpleContext(nullptr)
|
|
, mDummyContext(nullptr)
|
|
, mCompositionStart(UINT32_MAX)
|
|
, mProcessingKeyEvent(nullptr)
|
|
, mCompositionTargetOffset(UINT32_MAX)
|
|
, mCompositionState(eCompositionState_NotComposing)
|
|
, mIsIMFocused(false)
|
|
{
|
|
#ifdef PR_LOGGING
|
|
if (!gGtkIMLog) {
|
|
gGtkIMLog = PR_NewLogModule("nsGtkIMModuleWidgets");
|
|
}
|
|
#endif
|
|
static bool sFirstInstance = true;
|
|
if (sFirstInstance) {
|
|
sFirstInstance = false;
|
|
sUseSimpleContext =
|
|
Preferences::GetBool(
|
|
"intl.ime.use_simple_context_on_password_field",
|
|
kUseSimpleContextDefault);
|
|
}
|
|
Init();
|
|
}
|
|
|
|
void
|
|
nsGtkIMModule::Init()
|
|
{
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): Init, mOwnerWindow=%p",
|
|
this, mOwnerWindow));
|
|
|
|
MozContainer* container = mOwnerWindow->GetMozContainer();
|
|
NS_PRECONDITION(container, "container is null");
|
|
GdkWindow* gdkWindow = gtk_widget_get_window(GTK_WIDGET(container));
|
|
|
|
// NOTE: gtk_im_*_new() abort (kill) the whole process when it fails.
|
|
// So, we don't need to check the result.
|
|
|
|
// Normal context.
|
|
mContext = gtk_im_multicontext_new();
|
|
gtk_im_context_set_client_window(mContext, gdkWindow);
|
|
g_signal_connect(mContext, "preedit_changed",
|
|
G_CALLBACK(nsGtkIMModule::OnChangeCompositionCallback),
|
|
this);
|
|
g_signal_connect(mContext, "retrieve_surrounding",
|
|
G_CALLBACK(nsGtkIMModule::OnRetrieveSurroundingCallback),
|
|
this);
|
|
g_signal_connect(mContext, "delete_surrounding",
|
|
G_CALLBACK(nsGtkIMModule::OnDeleteSurroundingCallback),
|
|
this);
|
|
g_signal_connect(mContext, "commit",
|
|
G_CALLBACK(nsGtkIMModule::OnCommitCompositionCallback),
|
|
this);
|
|
g_signal_connect(mContext, "preedit_start",
|
|
G_CALLBACK(nsGtkIMModule::OnStartCompositionCallback),
|
|
this);
|
|
g_signal_connect(mContext, "preedit_end",
|
|
G_CALLBACK(nsGtkIMModule::OnEndCompositionCallback),
|
|
this);
|
|
|
|
// Simple context
|
|
if (sUseSimpleContext) {
|
|
mSimpleContext = gtk_im_context_simple_new();
|
|
gtk_im_context_set_client_window(mSimpleContext, gdkWindow);
|
|
g_signal_connect(mSimpleContext, "preedit_changed",
|
|
G_CALLBACK(&nsGtkIMModule::OnChangeCompositionCallback),
|
|
this);
|
|
g_signal_connect(mSimpleContext, "retrieve_surrounding",
|
|
G_CALLBACK(&nsGtkIMModule::OnRetrieveSurroundingCallback),
|
|
this);
|
|
g_signal_connect(mSimpleContext, "delete_surrounding",
|
|
G_CALLBACK(&nsGtkIMModule::OnDeleteSurroundingCallback),
|
|
this);
|
|
g_signal_connect(mSimpleContext, "commit",
|
|
G_CALLBACK(&nsGtkIMModule::OnCommitCompositionCallback),
|
|
this);
|
|
g_signal_connect(mSimpleContext, "preedit_start",
|
|
G_CALLBACK(nsGtkIMModule::OnStartCompositionCallback),
|
|
this);
|
|
g_signal_connect(mSimpleContext, "preedit_end",
|
|
G_CALLBACK(nsGtkIMModule::OnEndCompositionCallback),
|
|
this);
|
|
}
|
|
|
|
// Dummy context
|
|
mDummyContext = gtk_im_multicontext_new();
|
|
gtk_im_context_set_client_window(mDummyContext, gdkWindow);
|
|
}
|
|
|
|
nsGtkIMModule::~nsGtkIMModule()
|
|
{
|
|
if (this == sLastFocusedModule) {
|
|
sLastFocusedModule = nullptr;
|
|
}
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p) was gone", this));
|
|
}
|
|
|
|
void
|
|
nsGtkIMModule::OnDestroyWindow(nsWindow* aWindow)
|
|
{
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): OnDestroyWindow, aWindow=%p, mLastFocusedWindow=%p, mOwnerWindow=%p, mLastFocusedModule=%p",
|
|
this, aWindow, mLastFocusedWindow, mOwnerWindow, sLastFocusedModule));
|
|
|
|
NS_PRECONDITION(aWindow, "aWindow must not be null");
|
|
|
|
if (mLastFocusedWindow == aWindow) {
|
|
EndIMEComposition(aWindow);
|
|
if (mIsIMFocused) {
|
|
Blur();
|
|
}
|
|
mLastFocusedWindow = nullptr;
|
|
}
|
|
|
|
if (mOwnerWindow != aWindow) {
|
|
return;
|
|
}
|
|
|
|
if (sLastFocusedModule == this) {
|
|
sLastFocusedModule = nullptr;
|
|
}
|
|
|
|
/**
|
|
* NOTE:
|
|
* The given window is the owner of this, so, we must release the
|
|
* contexts now. But that might be referred from other nsWindows
|
|
* (they are children of this. But we don't know why there are the
|
|
* cases). So, we need to clear the pointers that refers to contexts
|
|
* and this if the other referrers are still alive. See bug 349727.
|
|
*/
|
|
if (mContext) {
|
|
PrepareToDestroyContext(mContext);
|
|
gtk_im_context_set_client_window(mContext, nullptr);
|
|
g_object_unref(mContext);
|
|
mContext = nullptr;
|
|
}
|
|
|
|
if (mSimpleContext) {
|
|
gtk_im_context_set_client_window(mSimpleContext, nullptr);
|
|
g_object_unref(mSimpleContext);
|
|
mSimpleContext = nullptr;
|
|
}
|
|
|
|
if (mDummyContext) {
|
|
// mContext and mDummyContext have the same slaveType and signal_data
|
|
// so no need for another workaround_gtk_im_display_closed.
|
|
gtk_im_context_set_client_window(mDummyContext, nullptr);
|
|
g_object_unref(mDummyContext);
|
|
mDummyContext = nullptr;
|
|
}
|
|
|
|
mOwnerWindow = nullptr;
|
|
mLastFocusedWindow = nullptr;
|
|
mInputContext.mIMEState.mEnabled = IMEState::DISABLED;
|
|
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" SUCCEEDED, Completely destroyed"));
|
|
}
|
|
|
|
// Work around gtk bug http://bugzilla.gnome.org/show_bug.cgi?id=483223:
|
|
// (and the similar issue of GTK+ IIIM)
|
|
// The GTK+ XIM and IIIM modules register handlers for the "closed" signal
|
|
// on the display, but:
|
|
// * The signal handlers are not disconnected when the module is unloaded.
|
|
//
|
|
// The GTK+ XIM module has another problem:
|
|
// * When the signal handler is run (with the module loaded) it tries
|
|
// XFree (and fails) on a pointer that did not come from Xmalloc.
|
|
//
|
|
// To prevent these modules from being unloaded, use static variables to
|
|
// hold ref of GtkIMContext class.
|
|
// For GTK+ XIM module, to prevent the signal handler from being run,
|
|
// find the signal handlers and remove them.
|
|
//
|
|
// GtkIMContextXIMs share XOpenIM connections and display closed signal
|
|
// handlers (where possible).
|
|
|
|
void
|
|
nsGtkIMModule::PrepareToDestroyContext(GtkIMContext *aContext)
|
|
{
|
|
#if (MOZ_WIDGET_GTK == 2)
|
|
GtkIMMulticontext *multicontext = GTK_IM_MULTICONTEXT(aContext);
|
|
GtkIMContext *slave = multicontext->slave;
|
|
#else
|
|
GtkIMContext *slave = nullptr; //TODO GTK3
|
|
#endif
|
|
if (!slave) {
|
|
return;
|
|
}
|
|
|
|
GType slaveType = G_TYPE_FROM_INSTANCE(slave);
|
|
const gchar *im_type_name = g_type_name(slaveType);
|
|
if (strcmp(im_type_name, "GtkIMContextIIIM") == 0) {
|
|
// Add a reference to prevent the IIIM module from being unloaded
|
|
static gpointer gtk_iiim_context_class =
|
|
g_type_class_ref(slaveType);
|
|
// Mute unused variable warning:
|
|
(void)gtk_iiim_context_class;
|
|
}
|
|
}
|
|
|
|
void
|
|
nsGtkIMModule::OnFocusWindow(nsWindow* aWindow)
|
|
{
|
|
if (MOZ_UNLIKELY(IsDestroyed())) {
|
|
return;
|
|
}
|
|
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): OnFocusWindow, aWindow=%p, mLastFocusedWindow=%p",
|
|
this, aWindow, mLastFocusedWindow));
|
|
mLastFocusedWindow = aWindow;
|
|
Focus();
|
|
}
|
|
|
|
void
|
|
nsGtkIMModule::OnBlurWindow(nsWindow* aWindow)
|
|
{
|
|
if (MOZ_UNLIKELY(IsDestroyed())) {
|
|
return;
|
|
}
|
|
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): OnBlurWindow, aWindow=%p, mLastFocusedWindow=%p, mIsIMFocused=%s",
|
|
this, aWindow, mLastFocusedWindow, mIsIMFocused ? "YES" : "NO"));
|
|
|
|
if (!mIsIMFocused || mLastFocusedWindow != aWindow) {
|
|
return;
|
|
}
|
|
|
|
Blur();
|
|
}
|
|
|
|
bool
|
|
nsGtkIMModule::OnKeyEvent(nsWindow* aCaller, GdkEventKey* aEvent,
|
|
bool aKeyDownEventWasSent /* = false */)
|
|
{
|
|
NS_PRECONDITION(aEvent, "aEvent must be non-null");
|
|
|
|
if (!IsEditable() || MOZ_UNLIKELY(IsDestroyed())) {
|
|
return false;
|
|
}
|
|
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): OnKeyEvent, aCaller=%p, aKeyDownEventWasSent=%s",
|
|
this, aCaller, aKeyDownEventWasSent ? "TRUE" : "FALSE"));
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" aEvent: type=%s, keyval=%s, unicode=0x%X",
|
|
aEvent->type == GDK_KEY_PRESS ? "GDK_KEY_PRESS" :
|
|
aEvent->type == GDK_KEY_RELEASE ? "GDK_KEY_RELEASE" : "Unknown",
|
|
gdk_keyval_name(aEvent->keyval),
|
|
gdk_keyval_to_unicode(aEvent->keyval)));
|
|
|
|
if (aCaller != mLastFocusedWindow) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, the caller isn't focused window, mLastFocusedWindow=%p",
|
|
mLastFocusedWindow));
|
|
return false;
|
|
}
|
|
|
|
GtkIMContext* im = GetContext();
|
|
if (MOZ_UNLIKELY(!im)) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, there are no context"));
|
|
return false;
|
|
}
|
|
|
|
mKeyDownEventWasSent = aKeyDownEventWasSent;
|
|
mFilterKeyEvent = true;
|
|
mProcessingKeyEvent = aEvent;
|
|
gboolean isFiltered = gtk_im_context_filter_keypress(im, aEvent);
|
|
mProcessingKeyEvent = nullptr;
|
|
|
|
// We filter the key event if the event was not committed (because
|
|
// it's probably part of a composition) or if the key event was
|
|
// committed _and_ changed. This way we still let key press
|
|
// events go through as simple key press events instead of
|
|
// composed characters.
|
|
bool filterThisEvent = isFiltered && mFilterKeyEvent;
|
|
|
|
if (IsComposing() && !isFiltered) {
|
|
if (aEvent->type == GDK_KEY_PRESS) {
|
|
if (!mDispatchedCompositionString.IsEmpty()) {
|
|
// If there is composition string, we shouldn't dispatch
|
|
// any keydown events during composition.
|
|
filterThisEvent = true;
|
|
} else {
|
|
// A Hangul input engine for SCIM doesn't emit preedit_end
|
|
// signal even when composition string becomes empty. On the
|
|
// other hand, we should allow to make composition with empty
|
|
// string for other languages because there *might* be such
|
|
// IM. For compromising this issue, we should dispatch
|
|
// compositionend event, however, we don't need to reset IM
|
|
// actually.
|
|
CommitCompositionBy(EmptyString());
|
|
filterThisEvent = false;
|
|
}
|
|
} else {
|
|
// Key release event may not be consumed by IM, however, we
|
|
// shouldn't dispatch any keyup event during composition.
|
|
filterThisEvent = true;
|
|
}
|
|
}
|
|
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" filterThisEvent=%s (isFiltered=%s, mFilterKeyEvent=%s)",
|
|
filterThisEvent ? "TRUE" : "FALSE", isFiltered ? "YES" : "NO",
|
|
mFilterKeyEvent ? "YES" : "NO"));
|
|
|
|
return filterThisEvent;
|
|
}
|
|
|
|
void
|
|
nsGtkIMModule::OnFocusChangeInGecko(bool aFocus)
|
|
{
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): OnFocusChangeInGecko, aFocus=%s, "
|
|
"mCompositionState=%s, mIsIMFocused=%s",
|
|
this, aFocus ? "YES" : "NO", GetCompositionStateName(),
|
|
mIsIMFocused ? "YES" : "NO"));
|
|
|
|
// We shouldn't carry over the removed string to another editor.
|
|
mSelectedString.Truncate();
|
|
}
|
|
|
|
void
|
|
nsGtkIMModule::ResetIME()
|
|
{
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): ResetIME, mCompositionState=%s, mIsIMFocused=%s",
|
|
this, GetCompositionStateName(), mIsIMFocused ? "YES" : "NO"));
|
|
|
|
GtkIMContext *im = GetContext();
|
|
if (MOZ_UNLIKELY(!im)) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, there are no context"));
|
|
return;
|
|
}
|
|
|
|
gtk_im_context_reset(im);
|
|
}
|
|
|
|
nsresult
|
|
nsGtkIMModule::EndIMEComposition(nsWindow* aCaller)
|
|
{
|
|
if (MOZ_UNLIKELY(IsDestroyed())) {
|
|
return NS_OK;
|
|
}
|
|
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): EndIMEComposition, aCaller=%p, "
|
|
"mCompositionState=%s",
|
|
this, aCaller, GetCompositionStateName()));
|
|
|
|
if (aCaller != mLastFocusedWindow) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" WARNING: the caller isn't focused window, mLastFocusedWindow=%p",
|
|
mLastFocusedWindow));
|
|
return NS_OK;
|
|
}
|
|
|
|
if (!IsComposing()) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// Currently, GTK has API neither to commit nor to cancel composition
|
|
// forcibly. Therefore, TextComposition will recompute commit string for
|
|
// the request even if native IME will cause unexpected commit string.
|
|
// So, we don't need to emulate commit or cancel composition with
|
|
// proper composition events and a text event.
|
|
// XXX ResetIME() might not enough for finishing compositoin on some
|
|
// environments. We should emulate focus change too because some IMEs
|
|
// may commit or cancel composition at blur.
|
|
ResetIME();
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void
|
|
nsGtkIMModule::OnUpdateComposition(void)
|
|
{
|
|
if (MOZ_UNLIKELY(IsDestroyed())) {
|
|
return;
|
|
}
|
|
|
|
SetCursorPosition(mCompositionTargetOffset);
|
|
}
|
|
|
|
void
|
|
nsGtkIMModule::SetInputContext(nsWindow* aCaller,
|
|
const InputContext* aContext,
|
|
const InputContextAction* aAction)
|
|
{
|
|
if (MOZ_UNLIKELY(IsDestroyed())) {
|
|
return;
|
|
}
|
|
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): SetInputContext, aCaller=%p, aState=%s mHTMLInputType=%s",
|
|
this, aCaller, GetEnabledStateName(aContext->mIMEState.mEnabled),
|
|
NS_ConvertUTF16toUTF8(aContext->mHTMLInputType).get()));
|
|
|
|
if (aCaller != mLastFocusedWindow) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, the caller isn't focused window, mLastFocusedWindow=%p",
|
|
mLastFocusedWindow));
|
|
return;
|
|
}
|
|
|
|
if (!mContext) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, there are no context"));
|
|
return;
|
|
}
|
|
|
|
|
|
if (sLastFocusedModule != this) {
|
|
mInputContext = *aContext;
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" SUCCEEDED, but we're not active"));
|
|
return;
|
|
}
|
|
|
|
bool changingEnabledState =
|
|
aContext->mIMEState.mEnabled != mInputContext.mIMEState.mEnabled ||
|
|
aContext->mHTMLInputType != mInputContext.mHTMLInputType;
|
|
|
|
// Release current IME focus if IME is enabled.
|
|
if (changingEnabledState && IsEditable()) {
|
|
EndIMEComposition(mLastFocusedWindow);
|
|
Blur();
|
|
}
|
|
|
|
mInputContext = *aContext;
|
|
|
|
if (changingEnabledState) {
|
|
#if (MOZ_WIDGET_GTK == 3)
|
|
static bool sInputPurposeSupported = !gtk_check_version(3, 6, 0);
|
|
if (sInputPurposeSupported && IsEditable()) {
|
|
GtkIMContext* context = GetContext();
|
|
if (context) {
|
|
GtkInputPurpose purpose = GTK_INPUT_PURPOSE_FREE_FORM;
|
|
const nsString& inputType = mInputContext.mHTMLInputType;
|
|
// Password case has difficult issue. Desktop IMEs disable
|
|
// composition if input-purpose is password.
|
|
// For disabling IME on |ime-mode: disabled;|, we need to check
|
|
// mEnabled value instead of inputType value. This hack also
|
|
// enables composition on
|
|
// <input type="password" style="ime-mode: enabled;">.
|
|
// This is right behavior of ime-mode on desktop.
|
|
//
|
|
// On the other hand, IME for tablet devices may provide a
|
|
// specific software keyboard for password field. If so,
|
|
// the behavior might look strange on both:
|
|
// <input type="text" style="ime-mode: disabled;">
|
|
// <input type="password" style="ime-mode: enabled;">
|
|
//
|
|
// Temporarily, we should focus on desktop environment for now.
|
|
// I.e., let's ignore tablet devices for now. When somebody
|
|
// reports actual trouble on tablet devices, we should try to
|
|
// look for a way to solve actual problem.
|
|
if (mInputContext.mIMEState.mEnabled == IMEState::PASSWORD) {
|
|
purpose = GTK_INPUT_PURPOSE_PASSWORD;
|
|
} else if (inputType.EqualsLiteral("email")) {
|
|
purpose = GTK_INPUT_PURPOSE_EMAIL;
|
|
} else if (inputType.EqualsLiteral("url")) {
|
|
purpose = GTK_INPUT_PURPOSE_URL;
|
|
} else if (inputType.EqualsLiteral("tel")) {
|
|
purpose = GTK_INPUT_PURPOSE_PHONE;
|
|
} else if (inputType.EqualsLiteral("number")) {
|
|
purpose = GTK_INPUT_PURPOSE_NUMBER;
|
|
}
|
|
|
|
g_object_set(context, "input-purpose", purpose, nullptr);
|
|
}
|
|
}
|
|
#endif // #if (MOZ_WIDGET_GTK == 3)
|
|
|
|
// Even when aState is not enabled state, we need to set IME focus.
|
|
// Because some IMs are updating the status bar of them at this time.
|
|
// Be aware, don't use aWindow here because this method shouldn't move
|
|
// focus actually.
|
|
Focus();
|
|
|
|
// XXX Should we call Blur() when it's not editable? E.g., it might be
|
|
// better to close VKB automatically.
|
|
}
|
|
}
|
|
|
|
InputContext
|
|
nsGtkIMModule::GetInputContext()
|
|
{
|
|
mInputContext.mIMEState.mOpen = IMEState::OPEN_STATE_NOT_SUPPORTED;
|
|
return mInputContext;
|
|
}
|
|
|
|
/* static */
|
|
bool
|
|
nsGtkIMModule::IsVirtualKeyboardOpened()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
GtkIMContext*
|
|
nsGtkIMModule::GetContext()
|
|
{
|
|
if (IsEnabled()) {
|
|
return mContext;
|
|
}
|
|
if (mInputContext.mIMEState.mEnabled == IMEState::PASSWORD) {
|
|
return mSimpleContext;
|
|
}
|
|
return mDummyContext;
|
|
}
|
|
|
|
bool
|
|
nsGtkIMModule::IsEnabled()
|
|
{
|
|
return mInputContext.mIMEState.mEnabled == IMEState::ENABLED ||
|
|
mInputContext.mIMEState.mEnabled == IMEState::PLUGIN ||
|
|
(!sUseSimpleContext &&
|
|
mInputContext.mIMEState.mEnabled == IMEState::PASSWORD);
|
|
}
|
|
|
|
bool
|
|
nsGtkIMModule::IsEditable()
|
|
{
|
|
return mInputContext.mIMEState.mEnabled == IMEState::ENABLED ||
|
|
mInputContext.mIMEState.mEnabled == IMEState::PLUGIN ||
|
|
mInputContext.mIMEState.mEnabled == IMEState::PASSWORD;
|
|
}
|
|
|
|
void
|
|
nsGtkIMModule::Focus()
|
|
{
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): Focus, sLastFocusedModule=%p",
|
|
this, sLastFocusedModule));
|
|
|
|
if (mIsIMFocused) {
|
|
NS_ASSERTION(sLastFocusedModule == this,
|
|
"We're not active, but the IM was focused?");
|
|
return;
|
|
}
|
|
|
|
GtkIMContext *im = GetContext();
|
|
if (!im) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, there are no context"));
|
|
return;
|
|
}
|
|
|
|
if (sLastFocusedModule && sLastFocusedModule != this) {
|
|
sLastFocusedModule->Blur();
|
|
}
|
|
|
|
sLastFocusedModule = this;
|
|
|
|
gtk_im_context_focus_in(im);
|
|
mIsIMFocused = true;
|
|
|
|
if (!IsEnabled()) {
|
|
// We should release IME focus for uim and scim.
|
|
// These IMs are using snooper that is released at losing focus.
|
|
Blur();
|
|
}
|
|
}
|
|
|
|
void
|
|
nsGtkIMModule::Blur()
|
|
{
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): Blur, mIsIMFocused=%s",
|
|
this, mIsIMFocused ? "YES" : "NO"));
|
|
|
|
if (!mIsIMFocused) {
|
|
return;
|
|
}
|
|
|
|
GtkIMContext *im = GetContext();
|
|
if (!im) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, there are no context"));
|
|
return;
|
|
}
|
|
|
|
gtk_im_context_focus_out(im);
|
|
mIsIMFocused = false;
|
|
}
|
|
|
|
void
|
|
nsGtkIMModule::OnSelectionChange(nsWindow* aCaller)
|
|
{
|
|
if (MOZ_UNLIKELY(IsDestroyed())) {
|
|
return;
|
|
}
|
|
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): OnSelectionChange(aCaller=0x%p), "
|
|
"mCompositionState=%s",
|
|
this, aCaller, GetCompositionStateName()));
|
|
|
|
if (aCaller != mLastFocusedWindow) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" WARNING: the caller isn't focused window, "
|
|
"mLastFocusedWindow=%p",
|
|
mLastFocusedWindow));
|
|
return;
|
|
}
|
|
|
|
ResetIME();
|
|
}
|
|
|
|
/* static */
|
|
void
|
|
nsGtkIMModule::OnStartCompositionCallback(GtkIMContext *aContext,
|
|
nsGtkIMModule* aModule)
|
|
{
|
|
aModule->OnStartCompositionNative(aContext);
|
|
}
|
|
|
|
void
|
|
nsGtkIMModule::OnStartCompositionNative(GtkIMContext *aContext)
|
|
{
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): OnStartCompositionNative, aContext=%p",
|
|
this, aContext));
|
|
|
|
// See bug 472635, we should do nothing if IM context doesn't match.
|
|
if (GetContext() != aContext) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, given context doesn't match, GetContext()=%p",
|
|
GetContext()));
|
|
return;
|
|
}
|
|
|
|
if (!DispatchCompositionStart()) {
|
|
return;
|
|
}
|
|
mCompositionTargetOffset = mCompositionStart;
|
|
}
|
|
|
|
/* static */
|
|
void
|
|
nsGtkIMModule::OnEndCompositionCallback(GtkIMContext *aContext,
|
|
nsGtkIMModule* aModule)
|
|
{
|
|
aModule->OnEndCompositionNative(aContext);
|
|
}
|
|
|
|
void
|
|
nsGtkIMModule::OnEndCompositionNative(GtkIMContext *aContext)
|
|
{
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): OnEndCompositionNative, aContext=%p",
|
|
this, aContext));
|
|
|
|
// See bug 472635, we should do nothing if IM context doesn't match.
|
|
if (GetContext() != aContext) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, given context doesn't match, GetContext()=%p",
|
|
GetContext()));
|
|
return;
|
|
}
|
|
|
|
if (!IsComposing()) {
|
|
// If we already handled the commit event, we should do nothing here.
|
|
return;
|
|
}
|
|
|
|
// Be aware, widget can be gone
|
|
DispatchCompositionEnd();
|
|
}
|
|
|
|
/* static */
|
|
void
|
|
nsGtkIMModule::OnChangeCompositionCallback(GtkIMContext *aContext,
|
|
nsGtkIMModule* aModule)
|
|
{
|
|
aModule->OnChangeCompositionNative(aContext);
|
|
}
|
|
|
|
void
|
|
nsGtkIMModule::OnChangeCompositionNative(GtkIMContext *aContext)
|
|
{
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): OnChangeCompositionNative, aContext=%p",
|
|
this, aContext));
|
|
|
|
// See bug 472635, we should do nothing if IM context doesn't match.
|
|
if (GetContext() != aContext) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, given context doesn't match, GetContext()=%p",
|
|
GetContext()));
|
|
return;
|
|
}
|
|
|
|
nsAutoString compositionString;
|
|
GetCompositionString(compositionString);
|
|
if (!IsComposing() && compositionString.IsEmpty()) {
|
|
mDispatchedCompositionString.Truncate();
|
|
return; // Don't start the composition with empty string.
|
|
}
|
|
|
|
// Be aware, widget can be gone
|
|
DispatchTextEvent(compositionString, false);
|
|
}
|
|
|
|
/* static */
|
|
gboolean
|
|
nsGtkIMModule::OnRetrieveSurroundingCallback(GtkIMContext *aContext,
|
|
nsGtkIMModule *aModule)
|
|
{
|
|
return aModule->OnRetrieveSurroundingNative(aContext);
|
|
}
|
|
|
|
gboolean
|
|
nsGtkIMModule::OnRetrieveSurroundingNative(GtkIMContext *aContext)
|
|
{
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): OnRetrieveSurroundingNative, aContext=%p, current context=%p",
|
|
this, aContext, GetContext()));
|
|
|
|
if (GetContext() != aContext) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, given context doesn't match, GetContext()=%p",
|
|
GetContext()));
|
|
return FALSE;
|
|
}
|
|
|
|
nsAutoString uniStr;
|
|
uint32_t cursorPos;
|
|
if (NS_FAILED(GetCurrentParagraph(uniStr, cursorPos))) {
|
|
return FALSE;
|
|
}
|
|
|
|
NS_ConvertUTF16toUTF8 utf8Str(nsDependentSubstring(uniStr, 0, cursorPos));
|
|
uint32_t cursorPosInUTF8 = utf8Str.Length();
|
|
AppendUTF16toUTF8(nsDependentSubstring(uniStr, cursorPos), utf8Str);
|
|
gtk_im_context_set_surrounding(aContext, utf8Str.get(), utf8Str.Length(),
|
|
cursorPosInUTF8);
|
|
return TRUE;
|
|
}
|
|
|
|
/* static */
|
|
gboolean
|
|
nsGtkIMModule::OnDeleteSurroundingCallback(GtkIMContext *aContext,
|
|
gint aOffset,
|
|
gint aNChars,
|
|
nsGtkIMModule *aModule)
|
|
{
|
|
return aModule->OnDeleteSurroundingNative(aContext, aOffset, aNChars);
|
|
}
|
|
|
|
gboolean
|
|
nsGtkIMModule::OnDeleteSurroundingNative(GtkIMContext *aContext,
|
|
gint aOffset,
|
|
gint aNChars)
|
|
{
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): OnDeleteSurroundingNative, aContext=%p, current context=%p",
|
|
this, aContext, GetContext()));
|
|
|
|
if (GetContext() != aContext) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, given context doesn't match, GetContext()=%p",
|
|
GetContext()));
|
|
return FALSE;
|
|
}
|
|
|
|
if (NS_SUCCEEDED(DeleteText(aOffset, (uint32_t)aNChars))) {
|
|
return TRUE;
|
|
}
|
|
|
|
// failed
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, cannot delete text"));
|
|
return FALSE;
|
|
}
|
|
|
|
/* static */
|
|
void
|
|
nsGtkIMModule::OnCommitCompositionCallback(GtkIMContext *aContext,
|
|
const gchar *aString,
|
|
nsGtkIMModule* aModule)
|
|
{
|
|
aModule->OnCommitCompositionNative(aContext, aString);
|
|
}
|
|
|
|
void
|
|
nsGtkIMModule::OnCommitCompositionNative(GtkIMContext *aContext,
|
|
const gchar *aUTF8Char)
|
|
{
|
|
const gchar emptyStr = 0;
|
|
const gchar *commitString = aUTF8Char ? aUTF8Char : &emptyStr;
|
|
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): OnCommitCompositionNative, aContext=%p, current context=%p, commitString=\"%s\"",
|
|
this, aContext, GetContext(), commitString));
|
|
|
|
// See bug 472635, we should do nothing if IM context doesn't match.
|
|
if (GetContext() != aContext) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, given context doesn't match, GetContext()=%p",
|
|
GetContext()));
|
|
return;
|
|
}
|
|
|
|
// If we are not in composition and committing with empty string,
|
|
// we need to do nothing because if we continued to handle this
|
|
// signal, we would dispatch compositionstart, text, compositionend
|
|
// events with empty string. Of course, they are unnecessary events
|
|
// for Web applications and our editor.
|
|
if (!IsComposing() && !commitString[0]) {
|
|
return;
|
|
}
|
|
|
|
// If IME doesn't change their keyevent that generated this commit,
|
|
// don't send it through XIM - just send it as a normal key press
|
|
// event.
|
|
if (!IsComposing() && mProcessingKeyEvent) {
|
|
char keyval_utf8[8]; /* should have at least 6 bytes of space */
|
|
gint keyval_utf8_len;
|
|
guint32 keyval_unicode;
|
|
|
|
keyval_unicode = gdk_keyval_to_unicode(mProcessingKeyEvent->keyval);
|
|
keyval_utf8_len = g_unichar_to_utf8(keyval_unicode, keyval_utf8);
|
|
keyval_utf8[keyval_utf8_len] = '\0';
|
|
|
|
if (!strcmp(commitString, keyval_utf8)) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): OnCommitCompositionNative, we'll send normal key event",
|
|
this));
|
|
mFilterKeyEvent = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
NS_ConvertUTF8toUTF16 str(commitString);
|
|
CommitCompositionBy(str); // Be aware, widget can be gone
|
|
}
|
|
|
|
bool
|
|
nsGtkIMModule::CommitCompositionBy(const nsAString& aString)
|
|
{
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): CommitCompositionBy, aString=\"%s\", "
|
|
"mDispatchedCompositionString=\"%s\"",
|
|
this, NS_ConvertUTF16toUTF8(aString).get(),
|
|
NS_ConvertUTF16toUTF8(mDispatchedCompositionString).get()));
|
|
|
|
if (!DispatchTextEvent(aString, true)) {
|
|
return false;
|
|
}
|
|
// We should dispatch the compositionend event here because some IMEs
|
|
// might not fire "preedit_end" native event.
|
|
return DispatchCompositionEnd(); // Be aware, widget can be gone
|
|
}
|
|
|
|
void
|
|
nsGtkIMModule::GetCompositionString(nsAString &aCompositionString)
|
|
{
|
|
gchar *preedit_string;
|
|
gint cursor_pos;
|
|
PangoAttrList *feedback_list;
|
|
gtk_im_context_get_preedit_string(GetContext(), &preedit_string,
|
|
&feedback_list, &cursor_pos);
|
|
if (preedit_string && *preedit_string) {
|
|
CopyUTF8toUTF16(preedit_string, aCompositionString);
|
|
} else {
|
|
aCompositionString.Truncate();
|
|
}
|
|
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): GetCompositionString, result=\"%s\"",
|
|
this, preedit_string));
|
|
|
|
pango_attr_list_unref(feedback_list);
|
|
g_free(preedit_string);
|
|
}
|
|
|
|
bool
|
|
nsGtkIMModule::DispatchCompositionStart()
|
|
{
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): DispatchCompositionStart", this));
|
|
|
|
if (IsComposing()) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" WARNING, we're already in composition"));
|
|
return true;
|
|
}
|
|
|
|
if (!mLastFocusedWindow) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, there are no focused window in this module"));
|
|
return false;
|
|
}
|
|
|
|
nsEventStatus status;
|
|
WidgetQueryContentEvent selection(true, NS_QUERY_SELECTED_TEXT,
|
|
mLastFocusedWindow);
|
|
InitEvent(selection);
|
|
mLastFocusedWindow->DispatchEvent(&selection, status);
|
|
|
|
if (!selection.mSucceeded || selection.mReply.mOffset == UINT32_MAX) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, cannot query the selection offset"));
|
|
return false;
|
|
}
|
|
|
|
// XXX The composition start point might be changed by composition events
|
|
// even though we strongly hope it doesn't happen.
|
|
// Every composition event should have the start offset for the result
|
|
// because it may high cost if we query the offset every time.
|
|
mCompositionStart = selection.mReply.mOffset;
|
|
mDispatchedCompositionString.Truncate();
|
|
|
|
if (mProcessingKeyEvent && !mKeyDownEventWasSent &&
|
|
mProcessingKeyEvent->type == GDK_KEY_PRESS) {
|
|
// If this composition is started by a native keydown event, we need to
|
|
// dispatch our keydown event here (before composition start).
|
|
nsCOMPtr<nsIWidget> kungFuDeathGrip = mLastFocusedWindow;
|
|
bool isCancelled;
|
|
mLastFocusedWindow->DispatchKeyDownEvent(mProcessingKeyEvent,
|
|
&isCancelled);
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" keydown event is dispatched"));
|
|
if (static_cast<nsWindow*>(kungFuDeathGrip.get())->IsDestroyed() ||
|
|
kungFuDeathGrip != mLastFocusedWindow) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" NOTE, the focused widget was destroyed/changed by keydown event"));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" mCompositionStart=%u", mCompositionStart));
|
|
mCompositionState = eCompositionState_CompositionStartDispatched;
|
|
WidgetCompositionEvent compEvent(true, NS_COMPOSITION_START,
|
|
mLastFocusedWindow);
|
|
InitEvent(compEvent);
|
|
nsCOMPtr<nsIWidget> kungFuDeathGrip = mLastFocusedWindow;
|
|
mLastFocusedWindow->DispatchEvent(&compEvent, status);
|
|
if (static_cast<nsWindow*>(kungFuDeathGrip.get())->IsDestroyed() ||
|
|
kungFuDeathGrip != mLastFocusedWindow) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" NOTE, the focused widget was destroyed/changed by compositionstart event"));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
nsGtkIMModule::DispatchCompositionEnd()
|
|
{
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): DispatchCompositionEnd, "
|
|
"mDispatchedCompositionString=\"%s\"",
|
|
this, NS_ConvertUTF16toUTF8(mDispatchedCompositionString).get()));
|
|
|
|
if (!IsComposing()) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" WARNING, we have alrady finished the composition"));
|
|
return false;
|
|
}
|
|
|
|
if (!mLastFocusedWindow) {
|
|
mDispatchedCompositionString.Truncate();
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, there are no focused window in this module"));
|
|
return false;
|
|
}
|
|
|
|
WidgetCompositionEvent compEvent(true, NS_COMPOSITION_END,
|
|
mLastFocusedWindow);
|
|
InitEvent(compEvent);
|
|
compEvent.data = mDispatchedCompositionString;
|
|
nsEventStatus status;
|
|
nsCOMPtr<nsIWidget> kungFuDeathGrip = mLastFocusedWindow;
|
|
mLastFocusedWindow->DispatchEvent(&compEvent, status);
|
|
mCompositionState = eCompositionState_NotComposing;
|
|
mCompositionStart = UINT32_MAX;
|
|
mCompositionTargetOffset = UINT32_MAX;
|
|
mDispatchedCompositionString.Truncate();
|
|
if (static_cast<nsWindow*>(kungFuDeathGrip.get())->IsDestroyed() ||
|
|
kungFuDeathGrip != mLastFocusedWindow) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" NOTE, the focused widget was destroyed/changed by compositionend event"));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
nsGtkIMModule::DispatchTextEvent(const nsAString &aCompositionString,
|
|
bool aIsCommit)
|
|
{
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): DispatchTextEvent, aIsCommit=%s",
|
|
this, aIsCommit ? "TRUE" : "FALSE"));
|
|
|
|
if (!mLastFocusedWindow) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, there are no focused window in this module"));
|
|
return false;
|
|
}
|
|
|
|
if (!IsComposing()) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" The composition wasn't started, force starting..."));
|
|
nsCOMPtr<nsIWidget> kungFuDeathGrip = mLastFocusedWindow;
|
|
if (!DispatchCompositionStart()) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
nsEventStatus status;
|
|
nsRefPtr<nsWindow> lastFocusedWindow = mLastFocusedWindow;
|
|
|
|
// Store the selected string which will be removed by following text event.
|
|
if (mCompositionState == eCompositionState_CompositionStartDispatched) {
|
|
// XXX We should assume, for now, any web applications don't change
|
|
// selection at handling this text event.
|
|
WidgetQueryContentEvent querySelectedTextEvent(true,
|
|
NS_QUERY_SELECTED_TEXT,
|
|
mLastFocusedWindow);
|
|
mLastFocusedWindow->DispatchEvent(&querySelectedTextEvent, status);
|
|
if (querySelectedTextEvent.mSucceeded) {
|
|
mSelectedString = querySelectedTextEvent.mReply.mString;
|
|
mCompositionStart = querySelectedTextEvent.mReply.mOffset;
|
|
}
|
|
}
|
|
|
|
WidgetTextEvent textEvent(true, NS_TEXT_TEXT, mLastFocusedWindow);
|
|
InitEvent(textEvent);
|
|
|
|
uint32_t targetOffset = mCompositionStart;
|
|
|
|
textEvent.theText = mDispatchedCompositionString = aCompositionString;
|
|
|
|
if (!aIsCommit) {
|
|
// NOTE: SetTextRangeList() assumes that mDispatchedCompositionString
|
|
// has been updated already.
|
|
textEvent.mRanges = CreateTextRangeArray();
|
|
targetOffset += textEvent.mRanges->TargetClauseOffset();
|
|
}
|
|
|
|
mCompositionState = aIsCommit ?
|
|
eCompositionState_CommitTextEventDispatched :
|
|
eCompositionState_TextEventDispatched;
|
|
|
|
mLastFocusedWindow->DispatchEvent(&textEvent, status);
|
|
if (lastFocusedWindow->IsDestroyed() ||
|
|
lastFocusedWindow != mLastFocusedWindow) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" NOTE, the focused widget was destroyed/changed by text event"));
|
|
return false;
|
|
}
|
|
|
|
// We cannot call SetCursorPosition for e10s-aware.
|
|
// DispatchEvent is async on e10s, so composition rect isn't updated now
|
|
// on tab parent.
|
|
mCompositionTargetOffset = targetOffset;
|
|
|
|
return true;
|
|
}
|
|
|
|
already_AddRefed<TextRangeArray>
|
|
nsGtkIMModule::CreateTextRangeArray()
|
|
{
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): CreateTextRangeArray", this));
|
|
|
|
nsRefPtr<TextRangeArray> textRangeArray = new TextRangeArray();
|
|
|
|
gchar *preedit_string;
|
|
gint cursor_pos;
|
|
PangoAttrList *feedback_list;
|
|
gtk_im_context_get_preedit_string(GetContext(), &preedit_string,
|
|
&feedback_list, &cursor_pos);
|
|
if (!preedit_string || !*preedit_string) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" preedit_string is null"));
|
|
pango_attr_list_unref(feedback_list);
|
|
g_free(preedit_string);
|
|
return textRangeArray.forget();
|
|
}
|
|
|
|
PangoAttrIterator* iter;
|
|
iter = pango_attr_list_get_iterator(feedback_list);
|
|
if (!iter) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, iterator couldn't be allocated"));
|
|
pango_attr_list_unref(feedback_list);
|
|
g_free(preedit_string);
|
|
return textRangeArray.forget();
|
|
}
|
|
|
|
/*
|
|
* Depend on gtk2's implementation on XIM support.
|
|
* In aFeedback got from gtk2, there are only three types of data:
|
|
* PANGO_ATTR_UNDERLINE, PANGO_ATTR_FOREGROUND, PANGO_ATTR_BACKGROUND.
|
|
* Corresponding to XIMUnderline, XIMReverse.
|
|
* Don't take PANGO_ATTR_BACKGROUND into account, since
|
|
* PANGO_ATTR_BACKGROUND and PANGO_ATTR_FOREGROUND are always
|
|
* a couple.
|
|
*/
|
|
do {
|
|
PangoAttribute* attrUnderline =
|
|
pango_attr_iterator_get(iter, PANGO_ATTR_UNDERLINE);
|
|
PangoAttribute* attrForeground =
|
|
pango_attr_iterator_get(iter, PANGO_ATTR_FOREGROUND);
|
|
if (!attrUnderline && !attrForeground) {
|
|
continue;
|
|
}
|
|
|
|
// Get the range of the current attribute(s)
|
|
gint start, end;
|
|
pango_attr_iterator_range(iter, &start, &end);
|
|
|
|
TextRange range;
|
|
// XIMReverse | XIMUnderline
|
|
if (attrUnderline && attrForeground) {
|
|
range.mRangeType = NS_TEXTRANGE_SELECTEDCONVERTEDTEXT;
|
|
}
|
|
// XIMUnderline
|
|
else if (attrUnderline) {
|
|
range.mRangeType = NS_TEXTRANGE_CONVERTEDTEXT;
|
|
}
|
|
// XIMReverse
|
|
else if (attrForeground) {
|
|
range.mRangeType = NS_TEXTRANGE_SELECTEDRAWTEXT;
|
|
} else {
|
|
range.mRangeType = NS_TEXTRANGE_RAWINPUT;
|
|
}
|
|
|
|
gunichar2* uniStr = nullptr;
|
|
if (start == 0) {
|
|
range.mStartOffset = 0;
|
|
} else {
|
|
glong uniStrLen;
|
|
uniStr = g_utf8_to_utf16(preedit_string, start,
|
|
nullptr, &uniStrLen, nullptr);
|
|
if (uniStr) {
|
|
range.mStartOffset = uniStrLen;
|
|
g_free(uniStr);
|
|
uniStr = nullptr;
|
|
}
|
|
}
|
|
|
|
glong uniStrLen;
|
|
uniStr = g_utf8_to_utf16(preedit_string + start, end - start,
|
|
nullptr, &uniStrLen, nullptr);
|
|
if (!uniStr) {
|
|
range.mEndOffset = range.mStartOffset;
|
|
} else {
|
|
range.mEndOffset = range.mStartOffset + uniStrLen;
|
|
g_free(uniStr);
|
|
uniStr = nullptr;
|
|
}
|
|
|
|
textRangeArray->AppendElement(range);
|
|
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" mStartOffset=%u, mEndOffset=%u, mRangeType=%s",
|
|
range.mStartOffset, range.mEndOffset,
|
|
GetRangeTypeName(range.mRangeType)));
|
|
} while (pango_attr_iterator_next(iter));
|
|
|
|
TextRange range;
|
|
if (cursor_pos < 0) {
|
|
range.mStartOffset = 0;
|
|
} else if (uint32_t(cursor_pos) > mDispatchedCompositionString.Length()) {
|
|
range.mStartOffset = mDispatchedCompositionString.Length();
|
|
} else {
|
|
range.mStartOffset = uint32_t(cursor_pos);
|
|
}
|
|
range.mEndOffset = range.mStartOffset;
|
|
range.mRangeType = NS_TEXTRANGE_CARETPOSITION;
|
|
textRangeArray->AppendElement(range);
|
|
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" mStartOffset=%u, mEndOffset=%u, mRangeType=%s",
|
|
range.mStartOffset, range.mEndOffset,
|
|
GetRangeTypeName(range.mRangeType)));
|
|
|
|
pango_attr_iterator_destroy(iter);
|
|
pango_attr_list_unref(feedback_list);
|
|
g_free(preedit_string);
|
|
|
|
return textRangeArray.forget();
|
|
}
|
|
|
|
void
|
|
nsGtkIMModule::SetCursorPosition(uint32_t aTargetOffset)
|
|
{
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): SetCursorPosition, aTargetOffset=%u",
|
|
this, aTargetOffset));
|
|
|
|
if (aTargetOffset == UINT32_MAX) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, aTargetOffset is wrong offset"));
|
|
return;
|
|
}
|
|
|
|
if (!mLastFocusedWindow) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, there are no focused window"));
|
|
return;
|
|
}
|
|
|
|
GtkIMContext *im = GetContext();
|
|
if (!im) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, there are no context"));
|
|
return;
|
|
}
|
|
|
|
WidgetQueryContentEvent charRect(true, NS_QUERY_TEXT_RECT,
|
|
mLastFocusedWindow);
|
|
charRect.InitForQueryTextRect(aTargetOffset, 1);
|
|
InitEvent(charRect);
|
|
nsEventStatus status;
|
|
mLastFocusedWindow->DispatchEvent(&charRect, status);
|
|
if (!charRect.mSucceeded) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, NS_QUERY_TEXT_RECT was failed"));
|
|
return;
|
|
}
|
|
nsWindow* rootWindow =
|
|
static_cast<nsWindow*>(mLastFocusedWindow->GetTopLevelWidget());
|
|
|
|
// Get the position of the rootWindow in screen.
|
|
gint rootX, rootY;
|
|
gdk_window_get_origin(rootWindow->GetGdkWindow(), &rootX, &rootY);
|
|
|
|
// Get the position of IM context owner window in screen.
|
|
gint ownerX, ownerY;
|
|
gdk_window_get_origin(mOwnerWindow->GetGdkWindow(), &ownerX, &ownerY);
|
|
|
|
// Compute the caret position in the IM owner window.
|
|
GdkRectangle area;
|
|
area.x = charRect.mReply.mRect.x + rootX - ownerX;
|
|
area.y = charRect.mReply.mRect.y + rootY - ownerY;
|
|
area.width = 0;
|
|
area.height = charRect.mReply.mRect.height;
|
|
|
|
gtk_im_context_set_cursor_location(im, &area);
|
|
}
|
|
|
|
nsresult
|
|
nsGtkIMModule::GetCurrentParagraph(nsAString& aText, uint32_t& aCursorPos)
|
|
{
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): GetCurrentParagraph, mCompositionState=%s",
|
|
this, GetCompositionStateName()));
|
|
|
|
if (!mLastFocusedWindow) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, there are no focused window in this module"));
|
|
return NS_ERROR_NULL_POINTER;
|
|
}
|
|
|
|
nsEventStatus status;
|
|
|
|
uint32_t selOffset = mCompositionStart;
|
|
uint32_t selLength = mSelectedString.Length();
|
|
|
|
// If focused editor doesn't have composition string, we should use
|
|
// current selection.
|
|
if (!EditorHasCompositionString()) {
|
|
// Query cursor position & selection
|
|
WidgetQueryContentEvent querySelectedTextEvent(true,
|
|
NS_QUERY_SELECTED_TEXT,
|
|
mLastFocusedWindow);
|
|
mLastFocusedWindow->DispatchEvent(&querySelectedTextEvent, status);
|
|
NS_ENSURE_TRUE(querySelectedTextEvent.mSucceeded, NS_ERROR_FAILURE);
|
|
|
|
selOffset = querySelectedTextEvent.mReply.mOffset;
|
|
selLength = querySelectedTextEvent.mReply.mString.Length();
|
|
}
|
|
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" selOffset=%u, selLength=%u",
|
|
selOffset, selLength));
|
|
|
|
// XXX nsString::Find and nsString::RFind take int32_t for offset, so,
|
|
// we cannot support this request when the current offset is larger
|
|
// than INT32_MAX.
|
|
if (selOffset > INT32_MAX || selLength > INT32_MAX ||
|
|
selOffset + selLength > INT32_MAX) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, The selection is out of range"));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
// Get all text contents of the focused editor
|
|
WidgetQueryContentEvent queryTextContentEvent(true,
|
|
NS_QUERY_TEXT_CONTENT,
|
|
mLastFocusedWindow);
|
|
queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX);
|
|
mLastFocusedWindow->DispatchEvent(&queryTextContentEvent, status);
|
|
NS_ENSURE_TRUE(queryTextContentEvent.mSucceeded, NS_ERROR_FAILURE);
|
|
|
|
nsAutoString textContent(queryTextContentEvent.mReply.mString);
|
|
if (selOffset + selLength > textContent.Length()) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, The selection is invalid, textContent.Length()=%u",
|
|
textContent.Length()));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
// Remove composing string and restore the selected string because
|
|
// GtkEntry doesn't remove selected string until committing, however,
|
|
// our editor does it. We should emulate the behavior for IME.
|
|
if (EditorHasCompositionString() &&
|
|
mDispatchedCompositionString != mSelectedString) {
|
|
textContent.Replace(mCompositionStart,
|
|
mDispatchedCompositionString.Length(), mSelectedString);
|
|
}
|
|
|
|
// Get only the focused paragraph, by looking for newlines
|
|
int32_t parStart = (selOffset == 0) ? 0 :
|
|
textContent.RFind("\n", false, selOffset - 1, -1) + 1;
|
|
int32_t parEnd = textContent.Find("\n", false, selOffset + selLength, -1);
|
|
if (parEnd < 0) {
|
|
parEnd = textContent.Length();
|
|
}
|
|
aText = nsDependentSubstring(textContent, parStart, parEnd - parStart);
|
|
aCursorPos = selOffset - uint32_t(parStart);
|
|
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" aText=%s, aText.Length()=%u, aCursorPos=%u",
|
|
NS_ConvertUTF16toUTF8(aText).get(),
|
|
aText.Length(), aCursorPos));
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
nsGtkIMModule::DeleteText(const int32_t aOffset, const uint32_t aNChars)
|
|
{
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
("GtkIMModule(%p): DeleteText, aOffset=%d, aNChars=%d, "
|
|
"mCompositionState=%s",
|
|
this, aOffset, aNChars, GetCompositionStateName()));
|
|
|
|
if (!mLastFocusedWindow) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, there are no focused window in this module"));
|
|
return NS_ERROR_NULL_POINTER;
|
|
}
|
|
|
|
if (!aNChars) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, aNChars must not be zero"));
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
|
|
nsRefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
|
|
nsEventStatus status;
|
|
|
|
// First, we should cancel current composition because editor cannot
|
|
// handle changing selection and deleting text.
|
|
uint32_t selOffset;
|
|
bool wasComposing = IsComposing();
|
|
bool editorHadCompositionString = EditorHasCompositionString();
|
|
if (wasComposing) {
|
|
selOffset = mCompositionStart;
|
|
if (editorHadCompositionString &&
|
|
!DispatchTextEvent(mSelectedString, false)) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, quitting from DeletText"));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
if (!DispatchCompositionEnd()) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, quitting from DeletText"));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
} else {
|
|
// Query cursor position & selection
|
|
WidgetQueryContentEvent querySelectedTextEvent(true,
|
|
NS_QUERY_SELECTED_TEXT,
|
|
mLastFocusedWindow);
|
|
lastFocusedWindow->DispatchEvent(&querySelectedTextEvent, status);
|
|
NS_ENSURE_TRUE(querySelectedTextEvent.mSucceeded, NS_ERROR_FAILURE);
|
|
|
|
selOffset = querySelectedTextEvent.mReply.mOffset;
|
|
}
|
|
|
|
// Get all text contents of the focused editor
|
|
WidgetQueryContentEvent queryTextContentEvent(true,
|
|
NS_QUERY_TEXT_CONTENT,
|
|
mLastFocusedWindow);
|
|
queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX);
|
|
mLastFocusedWindow->DispatchEvent(&queryTextContentEvent, status);
|
|
NS_ENSURE_TRUE(queryTextContentEvent.mSucceeded, NS_ERROR_FAILURE);
|
|
if (queryTextContentEvent.mReply.mString.IsEmpty()) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, there is no contents"));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
NS_ConvertUTF16toUTF8 utf8Str(
|
|
nsDependentSubstring(queryTextContentEvent.mReply.mString,
|
|
0, selOffset));
|
|
glong offsetInUTF8Characters =
|
|
g_utf8_strlen(utf8Str.get(), utf8Str.Length()) + aOffset;
|
|
if (offsetInUTF8Characters < 0) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, aOffset is too small for current cursor pos "
|
|
"(computed offset: %d)",
|
|
offsetInUTF8Characters));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
AppendUTF16toUTF8(
|
|
nsDependentSubstring(queryTextContentEvent.mReply.mString, selOffset),
|
|
utf8Str);
|
|
glong countOfCharactersInUTF8 =
|
|
g_utf8_strlen(utf8Str.get(), utf8Str.Length());
|
|
glong endInUTF8Characters =
|
|
offsetInUTF8Characters + aNChars;
|
|
if (countOfCharactersInUTF8 < endInUTF8Characters) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, aNChars is too large for current contents "
|
|
"(content length: %d, computed end offset: %d)",
|
|
countOfCharactersInUTF8, endInUTF8Characters));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
gchar* charAtOffset =
|
|
g_utf8_offset_to_pointer(utf8Str.get(), offsetInUTF8Characters);
|
|
gchar* charAtEnd =
|
|
g_utf8_offset_to_pointer(utf8Str.get(), endInUTF8Characters);
|
|
|
|
// Set selection to delete
|
|
WidgetSelectionEvent selectionEvent(true, NS_SELECTION_SET,
|
|
mLastFocusedWindow);
|
|
|
|
nsDependentCSubstring utf8StrBeforeOffset(utf8Str, 0,
|
|
charAtOffset - utf8Str.get());
|
|
selectionEvent.mOffset =
|
|
NS_ConvertUTF8toUTF16(utf8StrBeforeOffset).Length();
|
|
|
|
nsDependentCSubstring utf8DeletingStr(utf8Str,
|
|
utf8StrBeforeOffset.Length(),
|
|
charAtEnd - charAtOffset);
|
|
selectionEvent.mLength =
|
|
NS_ConvertUTF8toUTF16(utf8DeletingStr).Length();
|
|
|
|
selectionEvent.mReversed = false;
|
|
selectionEvent.mExpandToClusterBoundary = false;
|
|
lastFocusedWindow->DispatchEvent(&selectionEvent, status);
|
|
|
|
if (!selectionEvent.mSucceeded ||
|
|
lastFocusedWindow != mLastFocusedWindow ||
|
|
lastFocusedWindow->Destroyed()) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, setting selection caused focus change "
|
|
"or window destroyed"));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
// Delete the selection
|
|
WidgetContentCommandEvent contentCommandEvent(true,
|
|
NS_CONTENT_COMMAND_DELETE,
|
|
mLastFocusedWindow);
|
|
mLastFocusedWindow->DispatchEvent(&contentCommandEvent, status);
|
|
|
|
if (!contentCommandEvent.mSucceeded ||
|
|
lastFocusedWindow != mLastFocusedWindow ||
|
|
lastFocusedWindow->Destroyed()) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, deleting the selection caused focus change "
|
|
"or window destroyed"));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
if (!wasComposing) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// Restore the composition at new caret position.
|
|
if (!DispatchCompositionStart()) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, resterting composition start"));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
if (!editorHadCompositionString) {
|
|
return NS_OK;
|
|
}
|
|
|
|
nsAutoString compositionString;
|
|
GetCompositionString(compositionString);
|
|
if (!DispatchTextEvent(compositionString, true)) {
|
|
PR_LOG(gGtkIMLog, PR_LOG_ALWAYS,
|
|
(" FAILED, restoring composition string"));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void
|
|
nsGtkIMModule::InitEvent(WidgetGUIEvent& aEvent)
|
|
{
|
|
aEvent.time = PR_Now() / 1000;
|
|
}
|