зеркало из https://github.com/mozilla/gecko-dev.git
2360 строки
83 KiB
C++
2360 строки
83 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/. */
|
|
|
|
#include "mozilla/Logging.h"
|
|
#include "prtime.h"
|
|
|
|
#include "IMContextWrapper.h"
|
|
#include "nsGtkKeyUtils.h"
|
|
#include "nsWindow.h"
|
|
#include "mozilla/AutoRestore.h"
|
|
#include "mozilla/Likely.h"
|
|
#include "mozilla/MiscEvents.h"
|
|
#include "mozilla/Preferences.h"
|
|
#include "mozilla/TextEventDispatcher.h"
|
|
#include "mozilla/TextEvents.h"
|
|
#include "WritingModes.h"
|
|
|
|
namespace mozilla {
|
|
namespace widget {
|
|
|
|
LazyLogModule gGtkIMLog("nsGtkIMModuleWidgets");
|
|
|
|
static inline const char*
|
|
ToChar(bool aBool)
|
|
{
|
|
return aBool ? "true" : "false";
|
|
}
|
|
|
|
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!!";
|
|
}
|
|
}
|
|
|
|
static const char*
|
|
GetEventType(GdkEventKey* aKeyEvent)
|
|
{
|
|
switch (aKeyEvent->type) {
|
|
case GDK_KEY_PRESS:
|
|
return "GDK_KEY_PRESS";
|
|
case GDK_KEY_RELEASE:
|
|
return "GDK_KEY_RELEASE";
|
|
default:
|
|
return "Unknown";
|
|
}
|
|
}
|
|
|
|
class GetWritingModeName : public nsAutoCString
|
|
{
|
|
public:
|
|
explicit GetWritingModeName(const WritingMode& aWritingMode)
|
|
{
|
|
if (!aWritingMode.IsVertical()) {
|
|
AssignLiteral("Horizontal");
|
|
return;
|
|
}
|
|
if (aWritingMode.IsVerticalLR()) {
|
|
AssignLiteral("Vertical (LTR)");
|
|
return;
|
|
}
|
|
AssignLiteral("Vertical (RTL)");
|
|
}
|
|
virtual ~GetWritingModeName() {}
|
|
};
|
|
|
|
class GetTextRangeStyleText final : public nsAutoCString
|
|
{
|
|
public:
|
|
explicit GetTextRangeStyleText(const TextRangeStyle& aStyle)
|
|
{
|
|
if (!aStyle.IsDefined()) {
|
|
AssignLiteral("{ IsDefined()=false }");
|
|
return;
|
|
}
|
|
|
|
if (aStyle.IsLineStyleDefined()) {
|
|
AppendLiteral("{ mLineStyle=");
|
|
AppendLineStyle(aStyle.mLineStyle);
|
|
if (aStyle.IsUnderlineColorDefined()) {
|
|
AppendLiteral(", mUnderlineColor=");
|
|
AppendColor(aStyle.mUnderlineColor);
|
|
} else {
|
|
AppendLiteral(", IsUnderlineColorDefined=false");
|
|
}
|
|
} else {
|
|
AppendLiteral("{ IsLineStyleDefined()=false");
|
|
}
|
|
|
|
if (aStyle.IsForegroundColorDefined()) {
|
|
AppendLiteral(", mForegroundColor=");
|
|
AppendColor(aStyle.mForegroundColor);
|
|
} else {
|
|
AppendLiteral(", IsForegroundColorDefined()=false");
|
|
}
|
|
|
|
if (aStyle.IsBackgroundColorDefined()) {
|
|
AppendLiteral(", mBackgroundColor=");
|
|
AppendColor(aStyle.mBackgroundColor);
|
|
} else {
|
|
AppendLiteral(", IsBackgroundColorDefined()=false");
|
|
}
|
|
|
|
AppendLiteral(" }");
|
|
}
|
|
void AppendLineStyle(uint8_t aLineStyle)
|
|
{
|
|
switch (aLineStyle) {
|
|
case TextRangeStyle::LINESTYLE_NONE:
|
|
AppendLiteral("LINESTYLE_NONE");
|
|
break;
|
|
case TextRangeStyle::LINESTYLE_SOLID:
|
|
AppendLiteral("LINESTYLE_SOLID");
|
|
break;
|
|
case TextRangeStyle::LINESTYLE_DOTTED:
|
|
AppendLiteral("LINESTYLE_DOTTED");
|
|
break;
|
|
case TextRangeStyle::LINESTYLE_DASHED:
|
|
AppendLiteral("LINESTYLE_DASHED");
|
|
break;
|
|
case TextRangeStyle::LINESTYLE_DOUBLE:
|
|
AppendLiteral("LINESTYLE_DOUBLE");
|
|
break;
|
|
case TextRangeStyle::LINESTYLE_WAVY:
|
|
AppendLiteral("LINESTYLE_WAVY");
|
|
break;
|
|
default:
|
|
AppendPrintf("Invalid(0x%02X)", aLineStyle);
|
|
break;
|
|
}
|
|
}
|
|
void AppendColor(nscolor aColor)
|
|
{
|
|
AppendPrintf("{ R=0x%02X, G=0x%02X, B=0x%02X, A=0x%02X }",
|
|
NS_GET_R(aColor), NS_GET_G(aColor), NS_GET_B(aColor),
|
|
NS_GET_A(aColor));
|
|
}
|
|
virtual ~GetTextRangeStyleText() {};
|
|
};
|
|
|
|
const static bool kUseSimpleContextDefault = MOZ_WIDGET_GTK == 2;
|
|
|
|
/******************************************************************************
|
|
* IMContextWrapper
|
|
******************************************************************************/
|
|
|
|
IMContextWrapper* IMContextWrapper::sLastFocusedContext = nullptr;
|
|
bool IMContextWrapper::sUseSimpleContext;
|
|
|
|
NS_IMPL_ISUPPORTS(IMContextWrapper,
|
|
TextEventDispatcherListener,
|
|
nsISupportsWeakReference)
|
|
|
|
IMContextWrapper::IMContextWrapper(nsWindow* aOwnerWindow)
|
|
: mOwnerWindow(aOwnerWindow)
|
|
, mLastFocusedWindow(nullptr)
|
|
, mContext(nullptr)
|
|
, mSimpleContext(nullptr)
|
|
, mDummyContext(nullptr)
|
|
, mComposingContext(nullptr)
|
|
, mCompositionStart(UINT32_MAX)
|
|
, mProcessingKeyEvent(nullptr)
|
|
, mCompositionState(eCompositionState_NotComposing)
|
|
, mIsIMFocused(false)
|
|
, mIsDeletingSurrounding(false)
|
|
, mLayoutChanged(false)
|
|
, mSetCursorPositionOnKeyEvent(true)
|
|
, mPendingResettingIMContext(false)
|
|
, mRetrieveSurroundingSignalReceived(false)
|
|
{
|
|
static bool sFirstInstance = true;
|
|
if (sFirstInstance) {
|
|
sFirstInstance = false;
|
|
sUseSimpleContext =
|
|
Preferences::GetBool(
|
|
"intl.ime.use_simple_context_on_password_field",
|
|
kUseSimpleContextDefault);
|
|
}
|
|
Init();
|
|
}
|
|
|
|
void
|
|
IMContextWrapper::Init()
|
|
{
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p Init(), mOwnerWindow=0x%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(IMContextWrapper::OnChangeCompositionCallback), this);
|
|
g_signal_connect(mContext, "retrieve_surrounding",
|
|
G_CALLBACK(IMContextWrapper::OnRetrieveSurroundingCallback), this);
|
|
g_signal_connect(mContext, "delete_surrounding",
|
|
G_CALLBACK(IMContextWrapper::OnDeleteSurroundingCallback), this);
|
|
g_signal_connect(mContext, "commit",
|
|
G_CALLBACK(IMContextWrapper::OnCommitCompositionCallback), this);
|
|
g_signal_connect(mContext, "preedit_start",
|
|
G_CALLBACK(IMContextWrapper::OnStartCompositionCallback), this);
|
|
g_signal_connect(mContext, "preedit_end",
|
|
G_CALLBACK(IMContextWrapper::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(&IMContextWrapper::OnChangeCompositionCallback),
|
|
this);
|
|
g_signal_connect(mSimpleContext, "retrieve_surrounding",
|
|
G_CALLBACK(&IMContextWrapper::OnRetrieveSurroundingCallback),
|
|
this);
|
|
g_signal_connect(mSimpleContext, "delete_surrounding",
|
|
G_CALLBACK(&IMContextWrapper::OnDeleteSurroundingCallback),
|
|
this);
|
|
g_signal_connect(mSimpleContext, "commit",
|
|
G_CALLBACK(&IMContextWrapper::OnCommitCompositionCallback),
|
|
this);
|
|
g_signal_connect(mSimpleContext, "preedit_start",
|
|
G_CALLBACK(IMContextWrapper::OnStartCompositionCallback),
|
|
this);
|
|
g_signal_connect(mSimpleContext, "preedit_end",
|
|
G_CALLBACK(IMContextWrapper::OnEndCompositionCallback),
|
|
this);
|
|
}
|
|
|
|
// Dummy context
|
|
mDummyContext = gtk_im_multicontext_new();
|
|
gtk_im_context_set_client_window(mDummyContext, gdkWindow);
|
|
}
|
|
|
|
IMContextWrapper::~IMContextWrapper()
|
|
{
|
|
if (this == sLastFocusedContext) {
|
|
sLastFocusedContext = nullptr;
|
|
}
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p ~IMContextWrapper()", this));
|
|
}
|
|
|
|
NS_IMETHODIMP
|
|
IMContextWrapper::NotifyIME(TextEventDispatcher* aTextEventDispatcher,
|
|
const IMENotification& aNotification)
|
|
{
|
|
switch (aNotification.mMessage) {
|
|
case REQUEST_TO_COMMIT_COMPOSITION:
|
|
case REQUEST_TO_CANCEL_COMPOSITION: {
|
|
nsWindow* window =
|
|
static_cast<nsWindow*>(aTextEventDispatcher->GetWidget());
|
|
return EndIMEComposition(window);
|
|
}
|
|
case NOTIFY_IME_OF_FOCUS:
|
|
OnFocusChangeInGecko(true);
|
|
return NS_OK;
|
|
case NOTIFY_IME_OF_BLUR:
|
|
OnFocusChangeInGecko(false);
|
|
return NS_OK;
|
|
case NOTIFY_IME_OF_POSITION_CHANGE:
|
|
OnLayoutChange();
|
|
return NS_OK;
|
|
case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED:
|
|
OnUpdateComposition();
|
|
return NS_OK;
|
|
case NOTIFY_IME_OF_SELECTION_CHANGE: {
|
|
nsWindow* window =
|
|
static_cast<nsWindow*>(aTextEventDispatcher->GetWidget());
|
|
OnSelectionChange(window, aNotification);
|
|
return NS_OK;
|
|
}
|
|
default:
|
|
return NS_ERROR_NOT_IMPLEMENTED;
|
|
}
|
|
}
|
|
|
|
NS_IMETHODIMP_(void)
|
|
IMContextWrapper::OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher)
|
|
{
|
|
// XXX When input transaction is being stolen by add-on, what should we do?
|
|
}
|
|
|
|
NS_IMETHODIMP_(void)
|
|
IMContextWrapper::WillDispatchKeyboardEvent(
|
|
TextEventDispatcher* aTextEventDispatcher,
|
|
WidgetKeyboardEvent& aKeyboardEvent,
|
|
uint32_t aIndexOfKeypress,
|
|
void* aData)
|
|
{
|
|
KeymapWrapper::WillDispatchKeyboardEvent(aKeyboardEvent,
|
|
static_cast<GdkEventKey*>(aData));
|
|
}
|
|
|
|
TextEventDispatcher*
|
|
IMContextWrapper::GetTextEventDispatcher()
|
|
{
|
|
if (NS_WARN_IF(!mLastFocusedWindow)) {
|
|
return nullptr;
|
|
}
|
|
TextEventDispatcher* dispatcher =
|
|
mLastFocusedWindow->GetTextEventDispatcher();
|
|
// nsIWidget::GetTextEventDispatcher() shouldn't return nullptr.
|
|
MOZ_RELEASE_ASSERT(dispatcher);
|
|
return dispatcher;
|
|
}
|
|
|
|
nsIMEUpdatePreference
|
|
IMContextWrapper::GetIMEUpdatePreference() const
|
|
{
|
|
// While a plugin has focus, IMContextWrapper doesn't need any
|
|
// notifications.
|
|
if (mInputContext.mIMEState.mEnabled == IMEState::PLUGIN) {
|
|
return nsIMEUpdatePreference();
|
|
}
|
|
|
|
nsIMEUpdatePreference::Notifications notifications =
|
|
nsIMEUpdatePreference::NOTIFY_NOTHING;
|
|
// If it's not enabled, we don't need position change notification.
|
|
if (IsEnabled()) {
|
|
notifications |= nsIMEUpdatePreference::NOTIFY_POSITION_CHANGE;
|
|
}
|
|
nsIMEUpdatePreference updatePreference(notifications);
|
|
return updatePreference;
|
|
}
|
|
|
|
void
|
|
IMContextWrapper::OnDestroyWindow(nsWindow* aWindow)
|
|
{
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p OnDestroyWindow(aWindow=0x%p), mLastFocusedWindow=0x%p, "
|
|
"mOwnerWindow=0x%p, mLastFocusedModule=0x%p",
|
|
this, aWindow, mLastFocusedWindow, mOwnerWindow, sLastFocusedContext));
|
|
|
|
NS_PRECONDITION(aWindow, "aWindow must not be null");
|
|
|
|
if (mLastFocusedWindow == aWindow) {
|
|
EndIMEComposition(aWindow);
|
|
if (mIsIMFocused) {
|
|
Blur();
|
|
}
|
|
mLastFocusedWindow = nullptr;
|
|
}
|
|
|
|
if (mOwnerWindow != aWindow) {
|
|
return;
|
|
}
|
|
|
|
if (sLastFocusedContext == this) {
|
|
sLastFocusedContext = 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;
|
|
}
|
|
|
|
if (NS_WARN_IF(mComposingContext)) {
|
|
g_object_unref(mComposingContext);
|
|
mComposingContext = nullptr;
|
|
}
|
|
|
|
mOwnerWindow = nullptr;
|
|
mLastFocusedWindow = nullptr;
|
|
mInputContext.mIMEState.mEnabled = IMEState::DISABLED;
|
|
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Debug,
|
|
("0x%p OnDestroyWindow(), succeeded, Completely destroyed",
|
|
this));
|
|
}
|
|
|
|
// 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
|
|
IMContextWrapper::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
|
|
IMContextWrapper::OnFocusWindow(nsWindow* aWindow)
|
|
{
|
|
if (MOZ_UNLIKELY(IsDestroyed())) {
|
|
return;
|
|
}
|
|
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p OnFocusWindow(aWindow=0x%p), mLastFocusedWindow=0x%p",
|
|
this, aWindow, mLastFocusedWindow));
|
|
mLastFocusedWindow = aWindow;
|
|
Focus();
|
|
}
|
|
|
|
void
|
|
IMContextWrapper::OnBlurWindow(nsWindow* aWindow)
|
|
{
|
|
if (MOZ_UNLIKELY(IsDestroyed())) {
|
|
return;
|
|
}
|
|
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p OnBlurWindow(aWindow=0x%p), mLastFocusedWindow=0x%p, "
|
|
"mIsIMFocused=%s",
|
|
this, aWindow, mLastFocusedWindow, ToChar(mIsIMFocused)));
|
|
|
|
if (!mIsIMFocused || mLastFocusedWindow != aWindow) {
|
|
return;
|
|
}
|
|
|
|
Blur();
|
|
}
|
|
|
|
bool
|
|
IMContextWrapper::OnKeyEvent(nsWindow* aCaller,
|
|
GdkEventKey* aEvent,
|
|
bool aKeyDownEventWasSent /* = false */)
|
|
{
|
|
NS_PRECONDITION(aEvent, "aEvent must be non-null");
|
|
|
|
if (!mInputContext.mIMEState.MaybeEditable() ||
|
|
MOZ_UNLIKELY(IsDestroyed())) {
|
|
return false;
|
|
}
|
|
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p OnKeyEvent(aCaller=0x%p, aKeyDownEventWasSent=%s), "
|
|
"mCompositionState=%s, current context=0x%p, active context=0x%p, "
|
|
"aEvent(0x%p): { type=%s, keyval=%s, unicode=0x%X }",
|
|
this, aCaller, ToChar(aKeyDownEventWasSent),
|
|
GetCompositionStateName(), GetCurrentContext(), GetActiveContext(),
|
|
aEvent, GetEventType(aEvent), gdk_keyval_name(aEvent->keyval),
|
|
gdk_keyval_to_unicode(aEvent->keyval)));
|
|
|
|
if (aCaller != mLastFocusedWindow) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p OnKeyEvent(), FAILED, the caller isn't focused "
|
|
"window, mLastFocusedWindow=0x%p",
|
|
this, mLastFocusedWindow));
|
|
return false;
|
|
}
|
|
|
|
// Even if old IM context has composition, key event should be sent to
|
|
// current context since the user expects so.
|
|
GtkIMContext* currentContext = GetCurrentContext();
|
|
if (MOZ_UNLIKELY(!currentContext)) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p OnKeyEvent(), FAILED, there are no context",
|
|
this));
|
|
return false;
|
|
}
|
|
|
|
if (mSetCursorPositionOnKeyEvent) {
|
|
SetCursorPosition(currentContext);
|
|
mSetCursorPositionOnKeyEvent = false;
|
|
}
|
|
|
|
mKeyDownEventWasSent = aKeyDownEventWasSent;
|
|
mFilterKeyEvent = true;
|
|
mProcessingKeyEvent = aEvent;
|
|
gboolean isFiltered =
|
|
gtk_im_context_filter_keypress(currentContext, 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 (IsComposingOnCurrentContext() && !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.
|
|
DispatchCompositionCommitEvent(currentContext, &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;
|
|
}
|
|
}
|
|
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Debug,
|
|
("0x%p OnKeyEvent(), succeeded, filterThisEvent=%s "
|
|
"(isFiltered=%s, mFilterKeyEvent=%s), mCompositionState=%s",
|
|
this, ToChar(filterThisEvent), ToChar(isFiltered),
|
|
ToChar(mFilterKeyEvent), GetCompositionStateName()));
|
|
|
|
return filterThisEvent;
|
|
}
|
|
|
|
void
|
|
IMContextWrapper::OnFocusChangeInGecko(bool aFocus)
|
|
{
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p OnFocusChangeInGecko(aFocus=%s), "
|
|
"mCompositionState=%s, mIsIMFocused=%s",
|
|
this, ToChar(aFocus), GetCompositionStateName(),
|
|
ToChar(mIsIMFocused)));
|
|
|
|
// We shouldn't carry over the removed string to another editor.
|
|
mSelectedString.Truncate();
|
|
mSelection.Clear();
|
|
}
|
|
|
|
void
|
|
IMContextWrapper::ResetIME()
|
|
{
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p ResetIME(), mCompositionState=%s, mIsIMFocused=%s",
|
|
this, GetCompositionStateName(), ToChar(mIsIMFocused)));
|
|
|
|
GtkIMContext* activeContext = GetActiveContext();
|
|
if (MOZ_UNLIKELY(!activeContext)) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p ResetIME(), FAILED, there are no context",
|
|
this));
|
|
return;
|
|
}
|
|
|
|
RefPtr<IMContextWrapper> kungFuDeathGrip(this);
|
|
RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
|
|
|
|
mPendingResettingIMContext = false;
|
|
gtk_im_context_reset(activeContext);
|
|
|
|
// The last focused window might have been destroyed by a DOM event handler
|
|
// which was called by us during a call of gtk_im_context_reset().
|
|
if (!lastFocusedWindow ||
|
|
NS_WARN_IF(lastFocusedWindow != mLastFocusedWindow) ||
|
|
lastFocusedWindow->Destroyed()) {
|
|
return;
|
|
}
|
|
|
|
nsAutoString compositionString;
|
|
GetCompositionString(activeContext, compositionString);
|
|
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Debug,
|
|
("0x%p ResetIME() called gtk_im_context_reset(), "
|
|
"activeContext=0x%p, mCompositionState=%s, compositionString=%s, "
|
|
"mIsIMFocused=%s",
|
|
this, activeContext, GetCompositionStateName(),
|
|
NS_ConvertUTF16toUTF8(compositionString).get(),
|
|
ToChar(mIsIMFocused)));
|
|
|
|
// XXX IIIMF (ATOK X3 which is one of the Language Engine of it is still
|
|
// used in Japan!) sends only "preedit_changed" signal with empty
|
|
// composition string synchronously. Therefore, if composition string
|
|
// is now empty string, we should assume that the IME won't send
|
|
// "commit" signal.
|
|
if (IsComposing() && compositionString.IsEmpty()) {
|
|
// WARNING: The widget might have been gone after this.
|
|
DispatchCompositionCommitEvent(activeContext, &EmptyString());
|
|
}
|
|
}
|
|
|
|
nsresult
|
|
IMContextWrapper::EndIMEComposition(nsWindow* aCaller)
|
|
{
|
|
if (MOZ_UNLIKELY(IsDestroyed())) {
|
|
return NS_OK;
|
|
}
|
|
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p EndIMEComposition(aCaller=0x%p), "
|
|
"mCompositionState=%s",
|
|
this, aCaller, GetCompositionStateName()));
|
|
|
|
if (aCaller != mLastFocusedWindow) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p EndIMEComposition(), FAILED, the caller isn't "
|
|
"focused window, mLastFocusedWindow=0x%p",
|
|
this, 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.
|
|
// 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
|
|
IMContextWrapper::OnLayoutChange()
|
|
{
|
|
if (MOZ_UNLIKELY(IsDestroyed())) {
|
|
return;
|
|
}
|
|
|
|
if (IsComposing()) {
|
|
SetCursorPosition(GetActiveContext());
|
|
} else {
|
|
// If not composing, candidate window position is updated before key
|
|
// down
|
|
mSetCursorPositionOnKeyEvent = true;
|
|
}
|
|
mLayoutChanged = true;
|
|
}
|
|
|
|
void
|
|
IMContextWrapper::OnUpdateComposition()
|
|
{
|
|
if (MOZ_UNLIKELY(IsDestroyed())) {
|
|
return;
|
|
}
|
|
|
|
if (!IsComposing()) {
|
|
// Composition has been committed. So we need update selection for
|
|
// caret later
|
|
mSelection.Clear();
|
|
EnsureToCacheSelection();
|
|
mSetCursorPositionOnKeyEvent = true;
|
|
}
|
|
|
|
// If we've already set candidate window position, we don't need to update
|
|
// the position with update composition notification.
|
|
if (!mLayoutChanged) {
|
|
SetCursorPosition(GetActiveContext());
|
|
}
|
|
}
|
|
|
|
void
|
|
IMContextWrapper::SetInputContext(nsWindow* aCaller,
|
|
const InputContext* aContext,
|
|
const InputContextAction* aAction)
|
|
{
|
|
if (MOZ_UNLIKELY(IsDestroyed())) {
|
|
return;
|
|
}
|
|
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p SetInputContext(aCaller=0x%p, aContext={ mIMEState={ "
|
|
"mEnabled=%s }, mHTMLInputType=%s })",
|
|
this, aCaller, GetEnabledStateName(aContext->mIMEState.mEnabled),
|
|
NS_ConvertUTF16toUTF8(aContext->mHTMLInputType).get()));
|
|
|
|
if (aCaller != mLastFocusedWindow) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p SetInputContext(), FAILED, "
|
|
"the caller isn't focused window, mLastFocusedWindow=0x%p",
|
|
this, mLastFocusedWindow));
|
|
return;
|
|
}
|
|
|
|
if (!mContext) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p SetInputContext(), FAILED, "
|
|
"there are no context",
|
|
this));
|
|
return;
|
|
}
|
|
|
|
|
|
if (sLastFocusedContext != this) {
|
|
mInputContext = *aContext;
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Debug,
|
|
("0x%p SetInputContext(), succeeded, "
|
|
"but we're not active",
|
|
this));
|
|
return;
|
|
}
|
|
|
|
bool changingEnabledState =
|
|
aContext->mIMEState.mEnabled != mInputContext.mIMEState.mEnabled ||
|
|
aContext->mHTMLInputType != mInputContext.mHTMLInputType;
|
|
|
|
// Release current IME focus if IME is enabled.
|
|
if (changingEnabledState && mInputContext.mIMEState.MaybeEditable()) {
|
|
EndIMEComposition(mLastFocusedWindow);
|
|
Blur();
|
|
}
|
|
|
|
mInputContext = *aContext;
|
|
|
|
if (changingEnabledState) {
|
|
#if (MOZ_WIDGET_GTK == 3)
|
|
static bool sInputPurposeSupported = !gtk_check_version(3, 6, 0);
|
|
if (sInputPurposeSupported && mInputContext.mIMEState.MaybeEditable()) {
|
|
GtkIMContext* currentContext = GetCurrentContext();
|
|
if (currentContext) {
|
|
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(currentContext, "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
|
|
IMContextWrapper::GetInputContext()
|
|
{
|
|
mInputContext.mIMEState.mOpen = IMEState::OPEN_STATE_NOT_SUPPORTED;
|
|
return mInputContext;
|
|
}
|
|
|
|
GtkIMContext*
|
|
IMContextWrapper::GetCurrentContext() const
|
|
{
|
|
if (IsEnabled()) {
|
|
return mContext;
|
|
}
|
|
if (mInputContext.mIMEState.mEnabled == IMEState::PASSWORD) {
|
|
return mSimpleContext;
|
|
}
|
|
return mDummyContext;
|
|
}
|
|
|
|
bool
|
|
IMContextWrapper::IsValidContext(GtkIMContext* aContext) const
|
|
{
|
|
if (!aContext) {
|
|
return false;
|
|
}
|
|
return aContext == mContext ||
|
|
aContext == mSimpleContext ||
|
|
aContext == mDummyContext;
|
|
}
|
|
|
|
bool
|
|
IMContextWrapper::IsEnabled() const
|
|
{
|
|
return mInputContext.mIMEState.mEnabled == IMEState::ENABLED ||
|
|
mInputContext.mIMEState.mEnabled == IMEState::PLUGIN ||
|
|
(!sUseSimpleContext &&
|
|
mInputContext.mIMEState.mEnabled == IMEState::PASSWORD);
|
|
}
|
|
|
|
void
|
|
IMContextWrapper::Focus()
|
|
{
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p Focus(), sLastFocusedContext=0x%p",
|
|
this, sLastFocusedContext));
|
|
|
|
if (mIsIMFocused) {
|
|
NS_ASSERTION(sLastFocusedContext == this,
|
|
"We're not active, but the IM was focused?");
|
|
return;
|
|
}
|
|
|
|
GtkIMContext* currentContext = GetCurrentContext();
|
|
if (!currentContext) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p Focus(), FAILED, there are no context",
|
|
this));
|
|
return;
|
|
}
|
|
|
|
if (sLastFocusedContext && sLastFocusedContext != this) {
|
|
sLastFocusedContext->Blur();
|
|
}
|
|
|
|
sLastFocusedContext = this;
|
|
|
|
gtk_im_context_focus_in(currentContext);
|
|
mIsIMFocused = true;
|
|
mSetCursorPositionOnKeyEvent = 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
|
|
IMContextWrapper::Blur()
|
|
{
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p Blur(), mIsIMFocused=%s",
|
|
this, ToChar(mIsIMFocused)));
|
|
|
|
if (!mIsIMFocused) {
|
|
return;
|
|
}
|
|
|
|
GtkIMContext* currentContext = GetCurrentContext();
|
|
if (!currentContext) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p Blur(), FAILED, there are no context",
|
|
this));
|
|
return;
|
|
}
|
|
|
|
gtk_im_context_focus_out(currentContext);
|
|
mIsIMFocused = false;
|
|
}
|
|
|
|
void
|
|
IMContextWrapper::OnSelectionChange(nsWindow* aCaller,
|
|
const IMENotification& aIMENotification)
|
|
{
|
|
mSelection.Assign(aIMENotification);
|
|
bool retrievedSurroundingSignalReceived =
|
|
mRetrieveSurroundingSignalReceived;
|
|
mRetrieveSurroundingSignalReceived = false;
|
|
|
|
if (MOZ_UNLIKELY(IsDestroyed())) {
|
|
return;
|
|
}
|
|
|
|
const IMENotification::SelectionChangeDataBase& selectionChangeData =
|
|
aIMENotification.mSelectionChangeData;
|
|
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p OnSelectionChange(aCaller=0x%p, aIMENotification={ "
|
|
"mSelectionChangeData={ mOffset=%u, Length()=%u, mReversed=%s, "
|
|
"mWritingMode=%s, mCausedByComposition=%s, "
|
|
"mCausedBySelectionEvent=%s, mOccurredDuringComposition=%s "
|
|
"} }), mCompositionState=%s, mIsDeletingSurrounding=%s, "
|
|
"mRetrieveSurroundingSignalReceived=%s",
|
|
this, aCaller, selectionChangeData.mOffset,
|
|
selectionChangeData.Length(),
|
|
ToChar(selectionChangeData.mReversed),
|
|
GetWritingModeName(selectionChangeData.GetWritingMode()).get(),
|
|
ToChar(selectionChangeData.mCausedByComposition),
|
|
ToChar(selectionChangeData.mCausedBySelectionEvent),
|
|
ToChar(selectionChangeData.mOccurredDuringComposition),
|
|
GetCompositionStateName(), ToChar(mIsDeletingSurrounding),
|
|
ToChar(retrievedSurroundingSignalReceived)));
|
|
|
|
if (aCaller != mLastFocusedWindow) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p OnSelectionChange(), FAILED, "
|
|
"the caller isn't focused window, mLastFocusedWindow=0x%p",
|
|
this, mLastFocusedWindow));
|
|
return;
|
|
}
|
|
|
|
if (!IsComposing()) {
|
|
// Now we have no composition (mostly situation on calling this method)
|
|
// If we have it, it will set by
|
|
// NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED.
|
|
mSetCursorPositionOnKeyEvent = true;
|
|
}
|
|
|
|
// The focused editor might have placeholder text with normal text node.
|
|
// In such case, the text node must be removed from a compositionstart
|
|
// event handler. So, we're dispatching eCompositionStart,
|
|
// we should ignore selection change notification.
|
|
if (mCompositionState == eCompositionState_CompositionStartDispatched) {
|
|
if (NS_WARN_IF(!mSelection.IsValid())) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p OnSelectionChange(), FAILED, "
|
|
"new offset is too large, cannot keep composing",
|
|
this));
|
|
} else {
|
|
// Modify the selection start offset with new offset.
|
|
mCompositionStart = mSelection.mOffset;
|
|
// XXX We should modify mSelectedString? But how?
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Debug,
|
|
("0x%p OnSelectionChange(), ignored, mCompositionStart "
|
|
"is updated to %u, the selection change doesn't cause "
|
|
"resetting IM context",
|
|
this, mCompositionStart));
|
|
// And don't reset the IM context.
|
|
return;
|
|
}
|
|
// Otherwise, reset the IM context due to impossible to keep composing.
|
|
}
|
|
|
|
// If the selection change is caused by deleting surrounding text,
|
|
// we shouldn't need to notify IME of selection change.
|
|
if (mIsDeletingSurrounding) {
|
|
return;
|
|
}
|
|
|
|
bool occurredBeforeComposition =
|
|
IsComposing() && !selectionChangeData.mOccurredDuringComposition &&
|
|
!selectionChangeData.mCausedByComposition;
|
|
if (occurredBeforeComposition) {
|
|
mPendingResettingIMContext = true;
|
|
}
|
|
|
|
// When the selection change is caused by dispatching composition event,
|
|
// selection set event and/or occurred before starting current composition,
|
|
// we shouldn't notify IME of that and commit existing composition.
|
|
if (!selectionChangeData.mCausedByComposition &&
|
|
!selectionChangeData.mCausedBySelectionEvent &&
|
|
!occurredBeforeComposition) {
|
|
// Hack for ibus-pinyin. ibus-pinyin will synthesize a set of
|
|
// composition which commits with empty string after calling
|
|
// gtk_im_context_reset(). Therefore, selecting text causes
|
|
// unexpectedly removing it. For preventing it but not breaking the
|
|
// other IMEs which use surrounding text, we should call it only when
|
|
// surrounding text has been retrieved after last selection range was
|
|
// set. If it's not retrieved, that means that current IME doesn't
|
|
// have any content cache, so, it must not need the notification of
|
|
// selection change.
|
|
if (IsComposing() || retrievedSurroundingSignalReceived) {
|
|
ResetIME();
|
|
}
|
|
}
|
|
}
|
|
|
|
/* static */
|
|
void
|
|
IMContextWrapper::OnStartCompositionCallback(GtkIMContext* aContext,
|
|
IMContextWrapper* aModule)
|
|
{
|
|
aModule->OnStartCompositionNative(aContext);
|
|
}
|
|
|
|
void
|
|
IMContextWrapper::OnStartCompositionNative(GtkIMContext* aContext)
|
|
{
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p OnStartCompositionNative(aContext=0x%p), "
|
|
"current context=0x%p",
|
|
this, aContext, GetCurrentContext()));
|
|
|
|
// See bug 472635, we should do nothing if IM context doesn't match.
|
|
if (GetCurrentContext() != aContext) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p OnStartCompositionNative(), FAILED, "
|
|
"given context doesn't match",
|
|
this));
|
|
return;
|
|
}
|
|
|
|
mComposingContext = static_cast<GtkIMContext*>(g_object_ref(aContext));
|
|
|
|
if (!DispatchCompositionStart(aContext)) {
|
|
return;
|
|
}
|
|
mCompositionTargetRange.mOffset = mCompositionStart;
|
|
mCompositionTargetRange.mLength = 0;
|
|
}
|
|
|
|
/* static */
|
|
void
|
|
IMContextWrapper::OnEndCompositionCallback(GtkIMContext* aContext,
|
|
IMContextWrapper* aModule)
|
|
{
|
|
aModule->OnEndCompositionNative(aContext);
|
|
}
|
|
|
|
void
|
|
IMContextWrapper::OnEndCompositionNative(GtkIMContext* aContext)
|
|
{
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p OnEndCompositionNative(aContext=0x%p)",
|
|
this, aContext));
|
|
|
|
// See bug 472635, we should do nothing if IM context doesn't match.
|
|
// Note that if this is called after focus move, the context may different
|
|
// from any our owning context.
|
|
if (!IsValidContext(aContext)) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p OnEndCompositionNative(), FAILED, "
|
|
"given context doesn't match with any context",
|
|
this));
|
|
return;
|
|
}
|
|
|
|
g_object_unref(mComposingContext);
|
|
mComposingContext = nullptr;
|
|
|
|
// If we already handled the commit event, we should do nothing here.
|
|
if (IsComposing()) {
|
|
if (!DispatchCompositionCommitEvent(aContext)) {
|
|
// If the widget is destroyed, we should do nothing anymore.
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (mPendingResettingIMContext) {
|
|
ResetIME();
|
|
}
|
|
}
|
|
|
|
/* static */
|
|
void
|
|
IMContextWrapper::OnChangeCompositionCallback(GtkIMContext* aContext,
|
|
IMContextWrapper* aModule)
|
|
{
|
|
aModule->OnChangeCompositionNative(aContext);
|
|
}
|
|
|
|
void
|
|
IMContextWrapper::OnChangeCompositionNative(GtkIMContext* aContext)
|
|
{
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p OnChangeCompositionNative(aContext=0x%p)",
|
|
this, aContext));
|
|
|
|
// See bug 472635, we should do nothing if IM context doesn't match.
|
|
// Note that if this is called after focus move, the context may different
|
|
// from any our owning context.
|
|
if (!IsValidContext(aContext)) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p OnChangeCompositionNative(), FAILED, "
|
|
"given context doesn't match with any context",
|
|
this));
|
|
return;
|
|
}
|
|
|
|
nsAutoString compositionString;
|
|
GetCompositionString(aContext, compositionString);
|
|
if (!IsComposing() && compositionString.IsEmpty()) {
|
|
mDispatchedCompositionString.Truncate();
|
|
return; // Don't start the composition with empty string.
|
|
}
|
|
|
|
// Be aware, widget can be gone
|
|
DispatchCompositionChangeEvent(aContext, compositionString);
|
|
}
|
|
|
|
/* static */
|
|
gboolean
|
|
IMContextWrapper::OnRetrieveSurroundingCallback(GtkIMContext* aContext,
|
|
IMContextWrapper* aModule)
|
|
{
|
|
return aModule->OnRetrieveSurroundingNative(aContext);
|
|
}
|
|
|
|
gboolean
|
|
IMContextWrapper::OnRetrieveSurroundingNative(GtkIMContext* aContext)
|
|
{
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p OnRetrieveSurroundingNative(aContext=0x%p), "
|
|
"current context=0x%p",
|
|
this, aContext, GetCurrentContext()));
|
|
|
|
// See bug 472635, we should do nothing if IM context doesn't match.
|
|
if (GetCurrentContext() != aContext) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p OnRetrieveSurroundingNative(), FAILED, "
|
|
"given context doesn't match",
|
|
this));
|
|
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);
|
|
mRetrieveSurroundingSignalReceived = true;
|
|
return TRUE;
|
|
}
|
|
|
|
/* static */
|
|
gboolean
|
|
IMContextWrapper::OnDeleteSurroundingCallback(GtkIMContext* aContext,
|
|
gint aOffset,
|
|
gint aNChars,
|
|
IMContextWrapper* aModule)
|
|
{
|
|
return aModule->OnDeleteSurroundingNative(aContext, aOffset, aNChars);
|
|
}
|
|
|
|
gboolean
|
|
IMContextWrapper::OnDeleteSurroundingNative(GtkIMContext* aContext,
|
|
gint aOffset,
|
|
gint aNChars)
|
|
{
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p OnDeleteSurroundingNative(aContext=0x%p, aOffset=%d, "
|
|
"aNChar=%d), current context=0x%p",
|
|
this, aContext, aOffset, aNChars, GetCurrentContext()));
|
|
|
|
// See bug 472635, we should do nothing if IM context doesn't match.
|
|
if (GetCurrentContext() != aContext) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p OnDeleteSurroundingNative(), FAILED, "
|
|
"given context doesn't match",
|
|
this));
|
|
return FALSE;
|
|
}
|
|
|
|
AutoRestore<bool> saveDeletingSurrounding(mIsDeletingSurrounding);
|
|
mIsDeletingSurrounding = true;
|
|
if (NS_SUCCEEDED(DeleteText(aContext, aOffset, (uint32_t)aNChars))) {
|
|
return TRUE;
|
|
}
|
|
|
|
// failed
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p OnDeleteSurroundingNative(), FAILED, "
|
|
"cannot delete text",
|
|
this));
|
|
return FALSE;
|
|
}
|
|
|
|
/* static */
|
|
void
|
|
IMContextWrapper::OnCommitCompositionCallback(GtkIMContext* aContext,
|
|
const gchar* aString,
|
|
IMContextWrapper* aModule)
|
|
{
|
|
aModule->OnCommitCompositionNative(aContext, aString);
|
|
}
|
|
|
|
void
|
|
IMContextWrapper::OnCommitCompositionNative(GtkIMContext* aContext,
|
|
const gchar* aUTF8Char)
|
|
{
|
|
const gchar emptyStr = 0;
|
|
const gchar *commitString = aUTF8Char ? aUTF8Char : &emptyStr;
|
|
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p OnCommitCompositionNative(aContext=0x%p), "
|
|
"current context=0x%p, active context=0x%p, commitString=\"%s\", "
|
|
"mProcessingKeyEvent=0x%p, IsComposingOn(aContext)=%s",
|
|
this, aContext, GetCurrentContext(), GetActiveContext(), commitString,
|
|
mProcessingKeyEvent, ToChar(IsComposingOn(aContext))));
|
|
|
|
// See bug 472635, we should do nothing if IM context doesn't match.
|
|
if (!IsValidContext(aContext)) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p OnCommitCompositionNative(), FAILED, "
|
|
"given context doesn't match",
|
|
this));
|
|
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 (!IsComposingOn(aContext) && !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.
|
|
// NOTE: While a key event is being handled, this might be caused on
|
|
// current context. Otherwise, this may be caused on active context.
|
|
if (!IsComposingOn(aContext) && mProcessingKeyEvent &&
|
|
aContext == GetCurrentContext()) {
|
|
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)) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p OnCommitCompositionNative(), "
|
|
"we'll send normal key event",
|
|
this));
|
|
mFilterKeyEvent = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
NS_ConvertUTF8toUTF16 str(commitString);
|
|
// Be aware, widget can be gone
|
|
DispatchCompositionCommitEvent(aContext, &str);
|
|
}
|
|
|
|
void
|
|
IMContextWrapper::GetCompositionString(GtkIMContext* aContext,
|
|
nsAString& aCompositionString)
|
|
{
|
|
gchar *preedit_string;
|
|
gint cursor_pos;
|
|
PangoAttrList *feedback_list;
|
|
gtk_im_context_get_preedit_string(aContext, &preedit_string,
|
|
&feedback_list, &cursor_pos);
|
|
if (preedit_string && *preedit_string) {
|
|
CopyUTF8toUTF16(preedit_string, aCompositionString);
|
|
} else {
|
|
aCompositionString.Truncate();
|
|
}
|
|
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p GetCompositionString(aContext=0x%p), "
|
|
"aCompositionString=\"%s\"",
|
|
this, aContext, preedit_string));
|
|
|
|
pango_attr_list_unref(feedback_list);
|
|
g_free(preedit_string);
|
|
}
|
|
|
|
bool
|
|
IMContextWrapper::DispatchCompositionStart(GtkIMContext* aContext)
|
|
{
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p DispatchCompositionStart(aContext=0x%p)",
|
|
this, aContext));
|
|
|
|
if (IsComposing()) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DispatchCompositionStart(), FAILED, "
|
|
"we're already in composition",
|
|
this));
|
|
return true;
|
|
}
|
|
|
|
if (!mLastFocusedWindow) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DispatchCompositionStart(), FAILED, "
|
|
"there are no focused window in this module",
|
|
this));
|
|
return false;
|
|
}
|
|
|
|
if (NS_WARN_IF(!EnsureToCacheSelection())) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DispatchCompositionStart(), FAILED, "
|
|
"cannot query the selection offset",
|
|
this));
|
|
return false;
|
|
}
|
|
|
|
// Keep the last focused window alive
|
|
RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
|
|
|
|
// 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 = mSelection.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).
|
|
bool isCancelled;
|
|
mLastFocusedWindow->DispatchKeyDownEvent(mProcessingKeyEvent,
|
|
&isCancelled);
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Debug,
|
|
("0x%p DispatchCompositionStart(), FAILED, keydown event "
|
|
"is dispatched",
|
|
this));
|
|
if (lastFocusedWindow->IsDestroyed() ||
|
|
lastFocusedWindow != mLastFocusedWindow) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DispatchCompositionStart(), FAILED, the focused "
|
|
"widget was destroyed/changed by keydown event",
|
|
this));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
|
|
nsresult rv = dispatcher->BeginNativeInputTransaction();
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DispatchCompositionStart(), FAILED, "
|
|
"due to BeginNativeInputTransaction() failure",
|
|
this));
|
|
return false;
|
|
}
|
|
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Debug,
|
|
("0x%p DispatchCompositionStart(), dispatching "
|
|
"compositionstart... (mCompositionStart=%u)",
|
|
this, mCompositionStart));
|
|
mCompositionState = eCompositionState_CompositionStartDispatched;
|
|
nsEventStatus status;
|
|
dispatcher->StartComposition(status);
|
|
if (lastFocusedWindow->IsDestroyed() ||
|
|
lastFocusedWindow != mLastFocusedWindow) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DispatchCompositionStart(), FAILED, the focused "
|
|
"widget was destroyed/changed by compositionstart event",
|
|
this));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
IMContextWrapper::DispatchCompositionChangeEvent(
|
|
GtkIMContext* aContext,
|
|
const nsAString& aCompositionString)
|
|
{
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p DispatchCompositionChangeEvent(aContext=0x%p)",
|
|
this, aContext));
|
|
|
|
if (!mLastFocusedWindow) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DispatchCompositionChangeEvent(), FAILED, "
|
|
"there are no focused window in this module",
|
|
this));
|
|
return false;
|
|
}
|
|
|
|
if (!IsComposing()) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Debug,
|
|
("0x%p DispatchCompositionChangeEvent(), the composition "
|
|
"wasn't started, force starting...",
|
|
this));
|
|
if (!DispatchCompositionStart(aContext)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
|
|
nsresult rv = dispatcher->BeginNativeInputTransaction();
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DispatchCompositionChangeEvent(), FAILED, "
|
|
"due to BeginNativeInputTransaction() failure",
|
|
this));
|
|
return false;
|
|
}
|
|
|
|
// Store the selected string which will be removed by following
|
|
// compositionchange event.
|
|
if (mCompositionState == eCompositionState_CompositionStartDispatched) {
|
|
if (NS_WARN_IF(!EnsureToCacheSelection(&mSelectedString))) {
|
|
// XXX How should we behave in this case??
|
|
} else {
|
|
// XXX We should assume, for now, any web applications don't change
|
|
// selection at handling this compositionchange event.
|
|
mCompositionStart = mSelection.mOffset;
|
|
}
|
|
}
|
|
|
|
RefPtr<TextRangeArray> rangeArray =
|
|
CreateTextRangeArray(aContext, aCompositionString);
|
|
|
|
rv = dispatcher->SetPendingComposition(aCompositionString, rangeArray);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DispatchCompositionChangeEvent(), FAILED, "
|
|
"due to SetPendingComposition() failure",
|
|
this));
|
|
return false;
|
|
}
|
|
|
|
mCompositionState = eCompositionState_CompositionChangeEventDispatched;
|
|
|
|
// We cannot call SetCursorPosition for e10s-aware.
|
|
// DispatchEvent is async on e10s, so composition rect isn't updated now
|
|
// on tab parent.
|
|
mDispatchedCompositionString = aCompositionString;
|
|
mLayoutChanged = false;
|
|
mCompositionTargetRange.mOffset =
|
|
mCompositionStart + rangeArray->TargetClauseOffset();
|
|
mCompositionTargetRange.mLength = rangeArray->TargetClauseLength();
|
|
|
|
RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
|
|
nsEventStatus status;
|
|
rv = dispatcher->FlushPendingComposition(status);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DispatchCompositionChangeEvent(), FAILED, "
|
|
"due to FlushPendingComposition() failure",
|
|
this));
|
|
return false;
|
|
}
|
|
|
|
if (lastFocusedWindow->IsDestroyed() ||
|
|
lastFocusedWindow != mLastFocusedWindow) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DispatchCompositionChangeEvent(), FAILED, the "
|
|
"focused widget was destroyed/changed by "
|
|
"compositionchange event",
|
|
this));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
IMContextWrapper::DispatchCompositionCommitEvent(
|
|
GtkIMContext* aContext,
|
|
const nsAString* aCommitString)
|
|
{
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p DispatchCompositionCommitEvent(aContext=0x%p, "
|
|
"aCommitString=0x%p, (\"%s\"))",
|
|
this, aContext, aCommitString,
|
|
aCommitString ? NS_ConvertUTF16toUTF8(*aCommitString).get() : ""));
|
|
|
|
if (!mLastFocusedWindow) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DispatchCompositionCommitEvent(), FAILED, "
|
|
"there are no focused window in this module",
|
|
this));
|
|
return false;
|
|
}
|
|
|
|
if (!IsComposing()) {
|
|
if (!aCommitString || aCommitString->IsEmpty()) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DispatchCompositionCommitEvent(), FAILED, "
|
|
"there is no composition and empty commit string",
|
|
this));
|
|
return true;
|
|
}
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Debug,
|
|
("0x%p DispatchCompositionCommitEvent(), "
|
|
"the composition wasn't started, force starting...",
|
|
this));
|
|
if (!DispatchCompositionStart(aContext)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
RefPtr<TextEventDispatcher> dispatcher = GetTextEventDispatcher();
|
|
nsresult rv = dispatcher->BeginNativeInputTransaction();
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DispatchCompositionCommitEvent(), FAILED, "
|
|
"due to BeginNativeInputTransaction() failure",
|
|
this));
|
|
return false;
|
|
}
|
|
|
|
RefPtr<nsWindow> lastFocusedWindow(mLastFocusedWindow);
|
|
|
|
mCompositionState = eCompositionState_NotComposing;
|
|
mCompositionStart = UINT32_MAX;
|
|
mCompositionTargetRange.Clear();
|
|
mDispatchedCompositionString.Truncate();
|
|
|
|
nsEventStatus status;
|
|
rv = dispatcher->CommitComposition(status, aCommitString);
|
|
if (NS_WARN_IF(NS_FAILED(rv))) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DispatchCompositionChangeEvent(), FAILED, "
|
|
"due to CommitComposition() failure",
|
|
this));
|
|
return false;
|
|
}
|
|
|
|
if (lastFocusedWindow->IsDestroyed() ||
|
|
lastFocusedWindow != mLastFocusedWindow) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DispatchCompositionCommitEvent(), FAILED, "
|
|
"the focused widget was destroyed/changed by "
|
|
"compositioncommit event",
|
|
this));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
already_AddRefed<TextRangeArray>
|
|
IMContextWrapper::CreateTextRangeArray(GtkIMContext* aContext,
|
|
const nsAString& aCompositionString)
|
|
{
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p CreateTextRangeArray(aContext=0x%p, "
|
|
"aCompositionString=\"%s\" (Length()=%u))",
|
|
this, aContext, NS_ConvertUTF16toUTF8(aCompositionString).get(),
|
|
aCompositionString.Length()));
|
|
|
|
RefPtr<TextRangeArray> textRangeArray = new TextRangeArray();
|
|
|
|
gchar *preedit_string;
|
|
gint cursor_pos_in_chars;
|
|
PangoAttrList *feedback_list;
|
|
gtk_im_context_get_preedit_string(aContext, &preedit_string,
|
|
&feedback_list, &cursor_pos_in_chars);
|
|
if (!preedit_string || !*preedit_string) {
|
|
if (!aCompositionString.IsEmpty()) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p CreateTextRangeArray(), FAILED, due to "
|
|
"preedit_string is null",
|
|
this));
|
|
}
|
|
pango_attr_list_unref(feedback_list);
|
|
g_free(preedit_string);
|
|
return textRangeArray.forget();
|
|
}
|
|
|
|
// Convert caret offset from offset in characters to offset in UTF-16
|
|
// string. If we couldn't proper offset in UTF-16 string, we should
|
|
// assume that the caret is at the end of the composition string.
|
|
uint32_t caretOffsetInUTF16 = aCompositionString.Length();
|
|
if (NS_WARN_IF(cursor_pos_in_chars < 0)) {
|
|
// Note that this case is undocumented. We should assume that the
|
|
// caret is at the end of the composition string.
|
|
} else if (cursor_pos_in_chars == 0) {
|
|
caretOffsetInUTF16 = 0;
|
|
} else {
|
|
gchar* charAfterCaret =
|
|
g_utf8_offset_to_pointer(preedit_string, cursor_pos_in_chars);
|
|
if (NS_WARN_IF(!charAfterCaret)) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Warning,
|
|
("0x%p CreateTextRangeArray(), failed to get UTF-8 "
|
|
"string before the caret (cursor_pos_in_chars=%d)",
|
|
this, cursor_pos_in_chars));
|
|
} else {
|
|
glong caretOffset = 0;
|
|
gunichar2* utf16StrBeforeCaret =
|
|
g_utf8_to_utf16(preedit_string, charAfterCaret - preedit_string,
|
|
nullptr, &caretOffset, nullptr);
|
|
if (NS_WARN_IF(!utf16StrBeforeCaret) ||
|
|
NS_WARN_IF(caretOffset < 0)) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Warning,
|
|
("0x%p CreateTextRangeArray(), WARNING, failed to "
|
|
"convert to UTF-16 string before the caret "
|
|
"(cursor_pos_in_chars=%d, caretOffset=%d)",
|
|
this, cursor_pos_in_chars, caretOffset));
|
|
} else {
|
|
caretOffsetInUTF16 = static_cast<uint32_t>(caretOffset);
|
|
uint32_t compositionStringLength = aCompositionString.Length();
|
|
if (NS_WARN_IF(caretOffsetInUTF16 > compositionStringLength)) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Warning,
|
|
("0x%p CreateTextRangeArray(), WARNING, "
|
|
"caretOffsetInUTF16=%u is larger than "
|
|
"compositionStringLength=%u",
|
|
this, caretOffsetInUTF16, compositionStringLength));
|
|
caretOffsetInUTF16 = compositionStringLength;
|
|
}
|
|
}
|
|
if (utf16StrBeforeCaret) {
|
|
g_free(utf16StrBeforeCaret);
|
|
}
|
|
}
|
|
}
|
|
|
|
PangoAttrIterator* iter;
|
|
iter = pango_attr_list_get_iterator(feedback_list);
|
|
if (!iter) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p CreateTextRangeArray(), FAILED, iterator couldn't "
|
|
"be allocated",
|
|
this));
|
|
pango_attr_list_unref(feedback_list);
|
|
g_free(preedit_string);
|
|
return textRangeArray.forget();
|
|
}
|
|
|
|
uint32_t minOffsetOfClauses = aCompositionString.Length();
|
|
do {
|
|
TextRange range;
|
|
if (!SetTextRange(iter, preedit_string, caretOffsetInUTF16, range)) {
|
|
continue;
|
|
}
|
|
MOZ_ASSERT(range.Length());
|
|
minOffsetOfClauses = std::min(minOffsetOfClauses, range.mStartOffset);
|
|
textRangeArray->AppendElement(range);
|
|
} while (pango_attr_iterator_next(iter));
|
|
|
|
// If the IME doesn't define clause from the start of the composition,
|
|
// we should insert dummy clause information since TextRangeArray assumes
|
|
// that there must be a clause whose start is 0 when there is one or
|
|
// more clauses.
|
|
if (minOffsetOfClauses) {
|
|
TextRange dummyClause;
|
|
dummyClause.mStartOffset = 0;
|
|
dummyClause.mEndOffset = minOffsetOfClauses;
|
|
dummyClause.mRangeType = TextRangeType::eRawClause;
|
|
textRangeArray->InsertElementAt(0, dummyClause);
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Warning,
|
|
("0x%p CreateTextRangeArray(), inserting a dummy clause "
|
|
"at the beginning of the composition string mStartOffset=%u, "
|
|
"mEndOffset=%u, mRangeType=%s",
|
|
this, dummyClause.mStartOffset, dummyClause.mEndOffset,
|
|
ToChar(dummyClause.mRangeType)));
|
|
}
|
|
|
|
TextRange range;
|
|
range.mStartOffset = range.mEndOffset = caretOffsetInUTF16;
|
|
range.mRangeType = TextRangeType::eCaret;
|
|
textRangeArray->AppendElement(range);
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Debug,
|
|
("0x%p CreateTextRangeArray(), mStartOffset=%u, "
|
|
"mEndOffset=%u, mRangeType=%s",
|
|
this, range.mStartOffset, range.mEndOffset,
|
|
ToChar(range.mRangeType)));
|
|
|
|
pango_attr_iterator_destroy(iter);
|
|
pango_attr_list_unref(feedback_list);
|
|
g_free(preedit_string);
|
|
|
|
return textRangeArray.forget();
|
|
}
|
|
|
|
/* static */
|
|
nscolor
|
|
IMContextWrapper::ToNscolor(PangoAttrColor* aPangoAttrColor)
|
|
{
|
|
PangoColor& pangoColor = aPangoAttrColor->color;
|
|
uint8_t r = pangoColor.red / 0x100;
|
|
uint8_t g = pangoColor.green / 0x100;
|
|
uint8_t b = pangoColor.blue / 0x100;
|
|
return NS_RGB(r, g, b);
|
|
}
|
|
|
|
bool
|
|
IMContextWrapper::SetTextRange(PangoAttrIterator* aPangoAttrIter,
|
|
const gchar* aUTF8CompositionString,
|
|
uint32_t aUTF16CaretOffset,
|
|
TextRange& aTextRange) const
|
|
{
|
|
// Set the range offsets in UTF-16 string.
|
|
gint utf8ClauseStart, utf8ClauseEnd;
|
|
pango_attr_iterator_range(aPangoAttrIter, &utf8ClauseStart, &utf8ClauseEnd);
|
|
if (utf8ClauseStart == utf8ClauseEnd) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p SetTextRange(), FAILED, due to collapsed range",
|
|
this));
|
|
return false;
|
|
}
|
|
|
|
if (!utf8ClauseStart) {
|
|
aTextRange.mStartOffset = 0;
|
|
} else {
|
|
glong utf16PreviousClausesLength;
|
|
gunichar2* utf16PreviousClausesString =
|
|
g_utf8_to_utf16(aUTF8CompositionString, utf8ClauseStart, nullptr,
|
|
&utf16PreviousClausesLength, nullptr);
|
|
|
|
if (NS_WARN_IF(!utf16PreviousClausesString)) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p SetTextRange(), FAILED, due to g_utf8_to_utf16() "
|
|
"failure (retrieving previous string of current clause)",
|
|
this));
|
|
return false;
|
|
}
|
|
|
|
aTextRange.mStartOffset = utf16PreviousClausesLength;
|
|
g_free(utf16PreviousClausesString);
|
|
}
|
|
|
|
glong utf16CurrentClauseLength;
|
|
gunichar2* utf16CurrentClauseString =
|
|
g_utf8_to_utf16(aUTF8CompositionString + utf8ClauseStart,
|
|
utf8ClauseEnd - utf8ClauseStart,
|
|
nullptr, &utf16CurrentClauseLength, nullptr);
|
|
|
|
if (NS_WARN_IF(!utf16CurrentClauseString)) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p SetTextRange(), FAILED, due to g_utf8_to_utf16() "
|
|
"failure (retrieving current clause)",
|
|
this));
|
|
return false;
|
|
}
|
|
|
|
// iBus Chewing IME tells us that there is an empty clause at the end of
|
|
// the composition string but we should ignore it since our code doesn't
|
|
// assume that there is an empty clause.
|
|
if (!utf16CurrentClauseLength) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Warning,
|
|
("0x%p SetTextRange(), FAILED, due to current clause length "
|
|
"is 0",
|
|
this));
|
|
return false;
|
|
}
|
|
|
|
aTextRange.mEndOffset = aTextRange.mStartOffset + utf16CurrentClauseLength;
|
|
g_free(utf16CurrentClauseString);
|
|
utf16CurrentClauseString = nullptr;
|
|
|
|
// Set styles
|
|
TextRangeStyle& style = aTextRange.mRangeStyle;
|
|
|
|
// Underline
|
|
PangoAttrInt* attrUnderline =
|
|
reinterpret_cast<PangoAttrInt*>(
|
|
pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_UNDERLINE));
|
|
if (attrUnderline) {
|
|
switch (attrUnderline->value) {
|
|
case PANGO_UNDERLINE_NONE:
|
|
style.mLineStyle = TextRangeStyle::LINESTYLE_NONE;
|
|
break;
|
|
case PANGO_UNDERLINE_DOUBLE:
|
|
style.mLineStyle = TextRangeStyle::LINESTYLE_DOUBLE;
|
|
break;
|
|
case PANGO_UNDERLINE_ERROR:
|
|
style.mLineStyle = TextRangeStyle::LINESTYLE_WAVY;
|
|
break;
|
|
case PANGO_UNDERLINE_SINGLE:
|
|
case PANGO_UNDERLINE_LOW:
|
|
style.mLineStyle = TextRangeStyle::LINESTYLE_SOLID;
|
|
break;
|
|
default:
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Warning,
|
|
("0x%p SetTextRange(), retrieved unknown underline "
|
|
"style: %d",
|
|
this, attrUnderline->value));
|
|
style.mLineStyle = TextRangeStyle::LINESTYLE_SOLID;
|
|
break;
|
|
}
|
|
style.mDefinedStyles |= TextRangeStyle::DEFINED_LINESTYLE;
|
|
|
|
// Underline color
|
|
PangoAttrColor* attrUnderlineColor =
|
|
reinterpret_cast<PangoAttrColor*>(
|
|
pango_attr_iterator_get(aPangoAttrIter,
|
|
PANGO_ATTR_UNDERLINE_COLOR));
|
|
if (attrUnderlineColor) {
|
|
style.mUnderlineColor = ToNscolor(attrUnderlineColor);
|
|
style.mDefinedStyles |= TextRangeStyle::DEFINED_UNDERLINE_COLOR;
|
|
}
|
|
} else {
|
|
style.mLineStyle = TextRangeStyle::LINESTYLE_NONE;
|
|
style.mDefinedStyles |= TextRangeStyle::DEFINED_LINESTYLE;
|
|
}
|
|
|
|
// Don't set colors if they are not specified. They should be computed by
|
|
// textframe if only one of the colors are specified.
|
|
|
|
// Foreground color (text color)
|
|
PangoAttrColor* attrForeground =
|
|
reinterpret_cast<PangoAttrColor*>(
|
|
pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_FOREGROUND));
|
|
if (attrForeground) {
|
|
style.mForegroundColor = ToNscolor(attrForeground);
|
|
style.mDefinedStyles |= TextRangeStyle::DEFINED_FOREGROUND_COLOR;
|
|
}
|
|
|
|
// Background color
|
|
PangoAttrColor* attrBackground =
|
|
reinterpret_cast<PangoAttrColor*>(
|
|
pango_attr_iterator_get(aPangoAttrIter, PANGO_ATTR_BACKGROUND));
|
|
if (attrBackground) {
|
|
style.mBackgroundColor = ToNscolor(attrBackground);
|
|
style.mDefinedStyles |= TextRangeStyle::DEFINED_BACKGROUND_COLOR;
|
|
}
|
|
|
|
/**
|
|
* We need to judge the meaning of the clause for a11y. Before we support
|
|
* IME specific composition string style, we used following rules:
|
|
*
|
|
* 1: If attrUnderline and attrForground are specified, we assumed the
|
|
* clause is TextRangeType::eSelectedClause.
|
|
* 2: If only attrUnderline is specified, we assumed the clause is
|
|
* TextRangeType::eConvertedClause.
|
|
* 3: If only attrForground is specified, we assumed the clause is
|
|
* TextRangeType::eSelectedRawClause.
|
|
* 4: If neither attrUnderline nor attrForeground is specified, we assumed
|
|
* the clause is TextRangeType::eRawClause.
|
|
*
|
|
* However, this rules are odd since there can be two or more selected
|
|
* clauses. Additionally, our old rules caused that IME developers/users
|
|
* cannot specify composition string style as they want.
|
|
*
|
|
* So, we shouldn't guess the meaning from its visual style.
|
|
*/
|
|
|
|
if (!attrUnderline && !attrForeground && !attrBackground) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Warning,
|
|
("0x%p SetTextRange(), FAILED, due to no attr, "
|
|
"aTextRange= { mStartOffset=%u, mEndOffset=%u }",
|
|
this, aTextRange.mStartOffset, aTextRange.mEndOffset));
|
|
return false;
|
|
}
|
|
|
|
// If the range covers whole of composition string and the caret is at
|
|
// the end of the composition string, the range is probably not converted.
|
|
if (!utf8ClauseStart &&
|
|
utf8ClauseEnd == static_cast<gint>(strlen(aUTF8CompositionString)) &&
|
|
aTextRange.mEndOffset == aUTF16CaretOffset) {
|
|
aTextRange.mRangeType = TextRangeType::eRawClause;
|
|
}
|
|
// Typically, the caret is set at the start of the selected clause.
|
|
// So, if the caret is in the clause, we can assume that the clause is
|
|
// selected.
|
|
else if (aTextRange.mStartOffset <= aUTF16CaretOffset &&
|
|
aTextRange.mEndOffset > aUTF16CaretOffset) {
|
|
aTextRange.mRangeType = TextRangeType::eSelectedClause;
|
|
}
|
|
// Otherwise, we should assume that the clause is converted but not
|
|
// selected.
|
|
else {
|
|
aTextRange.mRangeType = TextRangeType::eConvertedClause;
|
|
}
|
|
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Debug,
|
|
("0x%p SetTextRange(), succeeded, aTextRange= { "
|
|
"mStartOffset=%u, mEndOffset=%u, mRangeType=%s, mRangeStyle=%s }",
|
|
this, aTextRange.mStartOffset, aTextRange.mEndOffset,
|
|
ToChar(aTextRange.mRangeType),
|
|
GetTextRangeStyleText(aTextRange.mRangeStyle).get()));
|
|
|
|
return true;
|
|
}
|
|
|
|
void
|
|
IMContextWrapper::SetCursorPosition(GtkIMContext* aContext)
|
|
{
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p SetCursorPosition(aContext=0x%p), "
|
|
"mCompositionTargetRange={ mOffset=%u, mLength=%u }"
|
|
"mSelection={ mOffset=%u, mLength=%u, mWritingMode=%s }",
|
|
this, aContext, mCompositionTargetRange.mOffset,
|
|
mCompositionTargetRange.mLength,
|
|
mSelection.mOffset, mSelection.mLength,
|
|
GetWritingModeName(mSelection.mWritingMode).get()));
|
|
|
|
bool useCaret = false;
|
|
if (!mCompositionTargetRange.IsValid()) {
|
|
if (!mSelection.IsValid()) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p SetCursorPosition(), FAILED, "
|
|
"mCompositionTargetRange and mSelection are invalid",
|
|
this));
|
|
return;
|
|
}
|
|
useCaret = true;
|
|
}
|
|
|
|
if (!mLastFocusedWindow) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p SetCursorPosition(), FAILED, due to no focused "
|
|
"window",
|
|
this));
|
|
return;
|
|
}
|
|
|
|
if (MOZ_UNLIKELY(!aContext)) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p SetCursorPosition(), FAILED, due to no context",
|
|
this));
|
|
return;
|
|
}
|
|
|
|
WidgetQueryContentEvent charRect(true,
|
|
useCaret ? eQueryCaretRect :
|
|
eQueryTextRect,
|
|
mLastFocusedWindow);
|
|
if (useCaret) {
|
|
charRect.InitForQueryCaretRect(mSelection.mOffset);
|
|
} else {
|
|
if (mSelection.mWritingMode.IsVertical()) {
|
|
// For preventing the candidate window to overlap the target
|
|
// clause, we should set fake (typically, very tall) caret rect.
|
|
uint32_t length = mCompositionTargetRange.mLength ?
|
|
mCompositionTargetRange.mLength : 1;
|
|
charRect.InitForQueryTextRect(mCompositionTargetRange.mOffset,
|
|
length);
|
|
} else {
|
|
charRect.InitForQueryTextRect(mCompositionTargetRange.mOffset, 1);
|
|
}
|
|
}
|
|
InitEvent(charRect);
|
|
nsEventStatus status;
|
|
mLastFocusedWindow->DispatchEvent(&charRect, status);
|
|
if (!charRect.mSucceeded) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p SetCursorPosition(), FAILED, %s was failed",
|
|
this, useCaret ? "eQueryCaretRect" : "eQueryTextRect"));
|
|
return;
|
|
}
|
|
|
|
nsWindow* rootWindow =
|
|
static_cast<nsWindow*>(mLastFocusedWindow->GetTopLevelWidget());
|
|
|
|
// Get the position of the rootWindow in screen.
|
|
LayoutDeviceIntPoint root = rootWindow->WidgetToScreenOffset();
|
|
|
|
// Get the position of IM context owner window in screen.
|
|
LayoutDeviceIntPoint owner = mOwnerWindow->WidgetToScreenOffset();
|
|
|
|
// Compute the caret position in the IM owner window.
|
|
LayoutDeviceIntRect rect = charRect.mReply.mRect + root - owner;
|
|
rect.width = 0;
|
|
GdkRectangle area = rootWindow->DevicePixelsToGdkRectRoundOut(rect);
|
|
|
|
gtk_im_context_set_cursor_location(aContext, &area);
|
|
}
|
|
|
|
nsresult
|
|
IMContextWrapper::GetCurrentParagraph(nsAString& aText,
|
|
uint32_t& aCursorPos)
|
|
{
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p GetCurrentParagraph(), mCompositionState=%s",
|
|
this, GetCompositionStateName()));
|
|
|
|
if (!mLastFocusedWindow) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p GetCurrentParagraph(), FAILED, there are no "
|
|
"focused window in this module",
|
|
this));
|
|
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
|
|
if (NS_WARN_IF(!EnsureToCacheSelection())) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p GetCurrentParagraph(), FAILED, due to no "
|
|
"valid selection information",
|
|
this));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
selOffset = mSelection.mOffset;
|
|
selLength = mSelection.mLength;
|
|
}
|
|
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Debug,
|
|
("0x%p GetCurrentParagraph(), selOffset=%u, selLength=%u",
|
|
this, 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) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p GetCurrentParagraph(), FAILED, The selection is "
|
|
"out of range",
|
|
this));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
// Get all text contents of the focused editor
|
|
WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent,
|
|
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()) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p GetCurrentParagraph(), FAILED, The selection is "
|
|
"invalid, textContent.Length()=%u",
|
|
this, 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);
|
|
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Debug,
|
|
("0x%p GetCurrentParagraph(), succeeded, aText=%s, "
|
|
"aText.Length()=%u, aCursorPos=%u",
|
|
this, NS_ConvertUTF16toUTF8(aText).get(),
|
|
aText.Length(), aCursorPos));
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
nsresult
|
|
IMContextWrapper::DeleteText(GtkIMContext* aContext,
|
|
int32_t aOffset,
|
|
uint32_t aNChars)
|
|
{
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Info,
|
|
("0x%p DeleteText(aContext=0x%p, aOffset=%d, aNChars=%u), "
|
|
"mCompositionState=%s",
|
|
this, aContext, aOffset, aNChars, GetCompositionStateName()));
|
|
|
|
if (!mLastFocusedWindow) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DeleteText(), FAILED, there are no focused window "
|
|
"in this module",
|
|
this));
|
|
return NS_ERROR_NULL_POINTER;
|
|
}
|
|
|
|
if (!aNChars) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DeleteText(), FAILED, aNChars must not be zero",
|
|
this));
|
|
return NS_ERROR_INVALID_ARG;
|
|
}
|
|
|
|
RefPtr<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 (!DispatchCompositionCommitEvent(aContext, &mSelectedString)) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DeleteText(), FAILED, quitting from DeletText",
|
|
this));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
} else {
|
|
if (NS_WARN_IF(!EnsureToCacheSelection())) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DeleteText(), FAILED, due to no valid selection "
|
|
"information",
|
|
this));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
selOffset = mSelection.mOffset;
|
|
}
|
|
|
|
// Get all text contents of the focused editor
|
|
WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent,
|
|
mLastFocusedWindow);
|
|
queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX);
|
|
mLastFocusedWindow->DispatchEvent(&queryTextContentEvent, status);
|
|
NS_ENSURE_TRUE(queryTextContentEvent.mSucceeded, NS_ERROR_FAILURE);
|
|
if (queryTextContentEvent.mReply.mString.IsEmpty()) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DeleteText(), FAILED, there is no contents",
|
|
this));
|
|
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) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DeleteText(), FAILED, aOffset is too small for "
|
|
"current cursor pos (computed offset: %d)",
|
|
this, 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) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DeleteText(), FAILED, aNChars is too large for "
|
|
"current contents (content length: %d, computed end offset: %d)",
|
|
this, 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, eSetSelection,
|
|
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()) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DeleteText(), FAILED, setting selection caused "
|
|
"focus change or window destroyed",
|
|
this));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
// Delete the selection
|
|
WidgetContentCommandEvent contentCommandEvent(true, eContentCommandDelete,
|
|
mLastFocusedWindow);
|
|
mLastFocusedWindow->DispatchEvent(&contentCommandEvent, status);
|
|
|
|
if (!contentCommandEvent.mSucceeded ||
|
|
lastFocusedWindow != mLastFocusedWindow ||
|
|
lastFocusedWindow->Destroyed()) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DeleteText(), FAILED, deleting the selection caused "
|
|
"focus change or window destroyed",
|
|
this));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
if (!wasComposing) {
|
|
return NS_OK;
|
|
}
|
|
|
|
// Restore the composition at new caret position.
|
|
if (!DispatchCompositionStart(aContext)) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DeleteText(), FAILED, resterting composition start",
|
|
this));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
if (!editorHadCompositionString) {
|
|
return NS_OK;
|
|
}
|
|
|
|
nsAutoString compositionString;
|
|
GetCompositionString(aContext, compositionString);
|
|
if (!DispatchCompositionChangeEvent(aContext, compositionString)) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p DeleteText(), FAILED, restoring composition string",
|
|
this));
|
|
return NS_ERROR_FAILURE;
|
|
}
|
|
|
|
return NS_OK;
|
|
}
|
|
|
|
void
|
|
IMContextWrapper::InitEvent(WidgetGUIEvent& aEvent)
|
|
{
|
|
aEvent.mTime = PR_Now() / 1000;
|
|
}
|
|
|
|
bool
|
|
IMContextWrapper::EnsureToCacheSelection(nsAString* aSelectedString)
|
|
{
|
|
if (aSelectedString) {
|
|
aSelectedString->Truncate();
|
|
}
|
|
|
|
if (mSelection.IsValid() &&
|
|
(!mSelection.Collapsed() || !aSelectedString)) {
|
|
return true;
|
|
}
|
|
|
|
if (NS_WARN_IF(!mLastFocusedWindow)) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p EnsureToCacheSelection(), FAILED, due to "
|
|
"no focused window",
|
|
this));
|
|
return false;
|
|
}
|
|
|
|
nsEventStatus status;
|
|
WidgetQueryContentEvent selection(true, eQuerySelectedText,
|
|
mLastFocusedWindow);
|
|
InitEvent(selection);
|
|
mLastFocusedWindow->DispatchEvent(&selection, status);
|
|
if (NS_WARN_IF(!selection.mSucceeded)) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p EnsureToCacheSelection(), FAILED, due to "
|
|
"failure of query selection event",
|
|
this));
|
|
return false;
|
|
}
|
|
|
|
mSelection.Assign(selection);
|
|
if (!mSelection.IsValid()) {
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Error,
|
|
("0x%p EnsureToCacheSelection(), FAILED, due to "
|
|
"failure of query selection event (invalid result)",
|
|
this));
|
|
return false;
|
|
}
|
|
|
|
if (!mSelection.Collapsed() && aSelectedString) {
|
|
aSelectedString->Assign(selection.mReply.mString);
|
|
}
|
|
|
|
MOZ_LOG(gGtkIMLog, LogLevel::Debug,
|
|
("0x%p EnsureToCacheSelection(), Succeeded, mSelection="
|
|
"{ mOffset=%u, mLength=%u, mWritingMode=%s }",
|
|
this, mSelection.mOffset, mSelection.mLength,
|
|
GetWritingModeName(mSelection.mWritingMode).get()));
|
|
return true;
|
|
}
|
|
|
|
/******************************************************************************
|
|
* IMContextWrapper::Selection
|
|
******************************************************************************/
|
|
|
|
void
|
|
IMContextWrapper::Selection::Assign(const IMENotification& aIMENotification)
|
|
{
|
|
MOZ_ASSERT(aIMENotification.mMessage == NOTIFY_IME_OF_SELECTION_CHANGE);
|
|
mOffset = aIMENotification.mSelectionChangeData.mOffset;
|
|
mLength = aIMENotification.mSelectionChangeData.Length();
|
|
mWritingMode = aIMENotification.mSelectionChangeData.GetWritingMode();
|
|
}
|
|
|
|
void
|
|
IMContextWrapper::Selection::Assign(const WidgetQueryContentEvent& aEvent)
|
|
{
|
|
MOZ_ASSERT(aEvent.mMessage == eQuerySelectedText);
|
|
MOZ_ASSERT(aEvent.mSucceeded);
|
|
mOffset = aEvent.mReply.mOffset;
|
|
mLength = aEvent.mReply.mString.Length();
|
|
mWritingMode = aEvent.GetWritingMode();
|
|
}
|
|
|
|
} // namespace widget
|
|
} // namespace mozilla
|