1
/* * This file is part of Maliit framework *
3
* Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
4
* Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
5
* Copyright (C) 2013 Jolla Ltd.
9
* Contact: maliit-discuss@lists.maliit.org
11
* This library is free software; you can redistribute it and/or
12
* modify it under the terms of the GNU Lesser General Public
13
* License version 2.1 as published by the Free Software Foundation
14
* and appearing in the file LICENSE.LGPL included in the packaging
19
#include "minputcontext.h"
21
#include <QGuiApplication>
23
#include <QTextFormat>
29
#include <QSharedDataPointer>
34
const int SoftwareInputPanelHideTimer = 100;
35
const char * const InputContextName = "MInputContext";
37
int orientationAngle(Qt::ScreenOrientation orientation)
39
switch (orientation) {
40
case Qt::PrimaryOrientation: // Urgh.
41
case Qt::PortraitOrientation:
42
return MInputContext::Angle270;
43
case Qt::LandscapeOrientation:
44
return MInputContext::Angle0;
45
case Qt::InvertedPortraitOrientation:
46
return MInputContext::Angle90;
47
case Qt::InvertedLandscapeOrientation:
48
return MInputContext::Angle180;
50
return MInputContext::Angle0;
54
bool MInputContext::debug = false;
57
MInputContext::MInputContext()
60
inputPanelState(InputPanelHidden),
64
QByteArray debugEnvVar = qgetenv("MALIIT_DEBUG");
65
if (!debugEnvVar.isEmpty() && debugEnvVar != "0") {
66
qDebug() << "Creating Maliit input context";
70
QSharedPointer<Maliit::InputContext::DBus::Address> address(new Maliit::InputContext::DBus::DynamicAddress);
71
imServer = new DBusServerConnection(address);
73
sipHideTimer.setSingleShot(true);
74
sipHideTimer.setInterval(SoftwareInputPanelHideTimer);
75
connect(&sipHideTimer, SIGNAL(timeout()), SLOT(sendHideInputMethod()));
77
connectInputMethodServer();
80
MInputContext::~MInputContext()
85
void MInputContext::connectInputMethodServer()
87
connect(imServer, SIGNAL(connected()), this, SLOT(onDBusConnection()));
88
connect(imServer, SIGNAL(disconnected()), this, SLOT(onDBusDisconnection()));
90
// Hook up incoming communication from input method server
91
connect(imServer, SIGNAL(activationLostEvent()), this, SLOT(activationLostEvent()));
93
connect(imServer, SIGNAL(imInitiatedHide()), this, SLOT(imInitiatedHide()));
95
connect(imServer, SIGNAL(commitString(QString,int,int,int)),
96
this, SLOT(commitString(QString,int,int,int)));
98
connect(imServer, SIGNAL(updatePreedit(QString,QList<Maliit::PreeditTextFormat>,int,int,int)),
99
this, SLOT(updatePreedit(QString,QList<Maliit::PreeditTextFormat>,int,int,int)));
101
connect(imServer, SIGNAL(keyEvent(int,int,int,QString,bool,int,Maliit::EventRequestType)),
102
this, SLOT(keyEvent(int,int,int,QString,bool,int,Maliit::EventRequestType)));
104
connect(imServer, SIGNAL(updateInputMethodArea(QRect)),
105
this, SLOT(updateInputMethodArea(QRect)));
107
connect(imServer, SIGNAL(setGlobalCorrectionEnabled(bool)),
108
this, SLOT(setGlobalCorrectionEnabled(bool)));
110
connect(imServer, SIGNAL(getPreeditRectangle(QRect&,bool&)),
111
this, SLOT(getPreeditRectangle(QRect&,bool&)));
113
connect(imServer, SIGNAL(invokeAction(QString,QKeySequence)), this, SLOT(onInvokeAction(QString,QKeySequence)));
115
connect(imServer, SIGNAL(setRedirectKeys(bool)), this, SLOT(setRedirectKeys(bool)));
117
connect(imServer, SIGNAL(setDetectableAutoRepeat(bool)),
118
this, SLOT(setDetectableAutoRepeat(bool)));
120
connect(imServer, SIGNAL(setSelection(int,int)),
121
this, SLOT(setSelection(int,int)));
123
connect(imServer, SIGNAL(getSelection(QString&,bool&)),
124
this, SLOT(getSelection(QString&, bool&)));
126
connect(imServer, SIGNAL(setLanguage(QString)),
127
this, SLOT(setLanguage(QString)));
131
bool MInputContext::isValid() const
136
void MInputContext::setLanguage(const QString &language)
138
QLocale newLocale(language);
139
Qt::LayoutDirection oldDirection = inputDirection();
141
if (newLocale != inputLocale) {
142
inputLocale = newLocale;
146
Qt::LayoutDirection newDirection = inputDirection();
147
if (newDirection != oldDirection) {
148
emitInputDirectionChanged(newDirection);
152
void MInputContext::reset()
154
if (debug) qDebug() << InputContextName << "in" << __PRETTY_FUNCTION__;
156
const bool hadPreedit = !preedit.isEmpty();
158
// reset input method server, preedit requires synchronization.
159
// rationale: input method might be autocommitting existing preedit without
161
imServer->reset(hadPreedit);
164
void MInputContext::commit()
166
if (debug) qDebug() << InputContextName << "in" << __PRETTY_FUNCTION__;
168
const bool hadPreedit = !preedit.isEmpty();
171
QList<QInputMethodEvent::Attribute> attributes;
172
if (preeditCursorPos >= 0) {
174
int start = cursorStartPosition(&valid);
176
attributes << QInputMethodEvent::Attribute(QInputMethodEvent::Selection,
177
start + preeditCursorPos, 0, QVariant());
181
QInputMethodEvent event("", attributes);
182
event.setCommitString(preedit);
183
QGuiApplication::sendEvent(qGuiApp->focusObject(), &event);
186
preeditCursorPos = -1;
189
imServer->reset(hadPreedit);
192
void MInputContext::invokeAction(QInputMethod::Action action, int x)
194
if (debug) qDebug() << InputContextName << "in" << __PRETTY_FUNCTION__;
196
if (!inputMethodAccepted())
199
if (action == QInputMethod::Click) {
200
if (x < 0 || x >= preedit.length()) {
205
// To preserve the wire protocol, the "x" argument gets
206
// transferred in widget state instead of being an extra
207
// argument to mouseClickedOnPreedit().
208
QMap<QString, QVariant> stateInformation = getStateInformation();
209
stateInformation["preeditClickPos"] = x;
210
imServer->updateWidgetInformation(stateInformation, false);
212
// FIXME: proper positions and preeditClickPos
215
imServer->mouseClickedOnPreedit(globalPos, preeditRect);
218
QPlatformInputContext::invokeAction(action, x);
222
void MInputContext::update(Qt::InputMethodQueries queries)
224
if (debug) qDebug() << InputContextName << "in" << __PRETTY_FUNCTION__;
226
Q_UNUSED(queries) // fetching everything
228
if (queries & Qt::ImPlatformData) {
229
updateInputMethodExtensions();
232
// get the state information of currently focused widget, and pass it to input method server
233
QMap<QString, QVariant> stateInformation = getStateInformation();
234
imServer->updateWidgetInformation(stateInformation, false);
237
void MInputContext::updateServerOrientation(Qt::ScreenOrientation orientation)
240
imServer->appOrientationChanged(orientationAngle(orientation));
245
void MInputContext::setFocusObject(QObject *focused)
247
if (debug) qDebug() << InputContextName << "in" << __PRETTY_FUNCTION__ << focused;
249
updateInputMethodExtensions();
251
QWindow *newFocusWindow = qGuiApp->focusWindow();
252
if (newFocusWindow != window.data()) {
254
disconnect(window.data(), SIGNAL(contentOrientationChanged(Qt::ScreenOrientation)),
255
this, SLOT(updateServerOrientation(Qt::ScreenOrientation)));
258
window = newFocusWindow;
260
connect(window.data(), SIGNAL(contentOrientationChanged(Qt::ScreenOrientation)),
261
this, SLOT(updateServerOrientation(Qt::ScreenOrientation)));
265
if (focused && !active) {
266
imServer->activateContext();
268
updateServerOrientation(newFocusWindow->contentOrientation());
271
const QMap<QString, QVariant> stateInformation = getStateInformation();
272
imServer->updateWidgetInformation(stateInformation, true);
274
if (inputPanelState == InputPanelShowPending && focused) {
276
imServer->showInputMethod();
277
inputPanelState = InputPanelShown;
281
bool MInputContext::filterEvent(const QEvent *event)
285
switch (event->type()) {
287
case QEvent::KeyPress:
288
case QEvent::KeyRelease:
289
if (!inputMethodAccepted()) {
294
const QKeyEvent *key = static_cast<const QKeyEvent *>(event);
295
imServer->processKeyEvent(key->type(), static_cast<Qt::Key>(key->key()),
296
key->modifiers(), key->text(), key->isAutoRepeat(),
297
key->count(), key->nativeScanCode(),
298
key->nativeModifiers(), 0 /* currentKeyEventTime */);
310
QRectF MInputContext::keyboardRect() const
312
return keyboardRectangle;
315
bool MInputContext::isAnimating() const
317
return false; // don't know here when input method server is actually doing transitions
320
void MInputContext::showInputPanel()
322
if (debug) qDebug() << __PRETTY_FUNCTION__;
324
if (inputMethodAccepted()) {
328
if (!active || !inputMethodAccepted()) {
329
// in case SIP request comes without a properly focused widget, we
330
// don't ask input method server to be shown. It's done when the next widget
331
// is focused, so the widget state information can be set.
332
inputPanelState = InputPanelShowPending;
335
// note: could do this also if panel was hidden
337
imServer->showInputMethod();
338
inputPanelState = InputPanelShown;
342
void MInputContext::hideInputPanel()
344
if (debug) qDebug() << __PRETTY_FUNCTION__;
345
sipHideTimer.start();
348
bool MInputContext::isInputPanelVisible() const
350
return !keyboardRectangle.isEmpty();
353
QLocale MInputContext::locale() const
358
Qt::LayoutDirection MInputContext::inputDirection() const
360
return inputLocale.textDirection();
363
void MInputContext::sendHideInputMethod()
365
imServer->hideInputMethod();
367
inputPanelState = InputPanelHidden;
370
void MInputContext::activationLostEvent()
372
// This method is called when activation was gracefully lost.
373
// There is similar cleaning up done in onDBusDisconnection.
375
inputPanelState = InputPanelHidden;
379
void MInputContext::imInitiatedHide()
381
if (debug) qDebug() << InputContextName << "in" << __PRETTY_FUNCTION__;
383
inputPanelState = InputPanelHidden;
385
// remove focus on QtQuick2
386
QQuickItem *inputItem = qobject_cast<QQuickItem*>(QGuiApplication::focusObject());
388
inputItem->setFocus(false);
393
void MInputContext::commitString(const QString &string, int replacementStart,
394
int replacementLength, int cursorPos)
396
if (debug) qDebug() << InputContextName << "in" << __PRETTY_FUNCTION__;
398
if (imServer->pendingResets()) {
403
preeditCursorPos = -1;
406
if (cursorPos >= 0) {
408
int currentStart = cursorStartPosition(&valid);
410
start = cursorPos + currentStart + replacementStart;
415
QList<QInputMethodEvent::Attribute> attributes;
416
attributes << QInputMethodEvent::Attribute(QInputMethodEvent::Selection, start, 0, QVariant());
417
QInputMethodEvent event("", attributes);
418
event.setCommitString(string, replacementStart, replacementLength);
419
QGuiApplication::sendEvent(qGuiApp->focusObject(), &event);
422
QInputMethodEvent event;
423
event.setCommitString(string, replacementStart, replacementLength);
424
QGuiApplication::sendEvent(qGuiApp->focusObject(), &event);
429
void MInputContext::updatePreedit(const QString &string,
430
const QList<Maliit::PreeditTextFormat> &preeditFormats,
431
int replacementStart, int replacementLength, int cursorPos)
434
qDebug() << InputContextName << "in" << __PRETTY_FUNCTION__ << "preedit:" << string
435
<< ", replacementStart:" << replacementStart
436
<< ", replacementLength:" << replacementLength
437
<< ", cursorPos:" << cursorPos;
440
if (imServer->pendingResets()) {
444
updatePreeditInternally(string, preeditFormats, replacementStart, replacementLength, cursorPos);
447
void MInputContext::updatePreeditInternally(const QString &string,
448
const QList<Maliit::PreeditTextFormat> &preeditFormats,
449
int replacementStart, int replacementLength, int cursorPos)
452
preeditCursorPos = cursorPos;
454
QList<QInputMethodEvent::Attribute> attributes;
455
Q_FOREACH (const Maliit::PreeditTextFormat &preeditFormat, preeditFormats) {
457
QTextCharFormat format;
460
switch (preeditFormat.preeditFace) {
461
case Maliit::PreeditNoCandidates:
462
format.setUnderlineStyle(QTextCharFormat::SpellCheckUnderline);
463
format.setUnderlineColor(Qt::red);
465
case Maliit::PreeditUnconvertible:
466
format.setForeground(QBrush(QColor(128, 128, 128)));
468
case Maliit::PreeditActive:
469
format.setForeground(QBrush(QColor(153, 50, 204)));
470
format.setFontWeight(QFont::Bold);
472
case Maliit::PreeditKeyPress:
473
case Maliit::PreeditDefault:
474
format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
475
format.setUnderlineColor(QColor(0, 0, 0));
479
attributes << QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat,
481
preeditFormat.length, format);
484
if (cursorPos >= 0) {
485
attributes << QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, cursorPos, 1, QVariant());
488
QInputMethodEvent event(string, attributes);
489
if (replacementStart || replacementLength) {
490
event.setCommitString("", replacementStart, replacementLength);
493
QGuiApplication::sendEvent(qGuiApp->focusObject(), &event);
496
void MInputContext::keyEvent(int type, int key, int modifiers, const QString &text,
497
bool autoRepeat, int count,
498
Maliit::EventRequestType requestType)
500
if (debug) qDebug() << InputContextName << "in" << __PRETTY_FUNCTION__;
502
if (qGuiApp->focusWindow() != 0 && requestType != Maliit::EventRequestSignalOnly) {
503
QEvent::Type eventType = static_cast<QEvent::Type>(type);
504
QKeyEvent event(eventType, key, static_cast<Qt::KeyboardModifiers>(modifiers), text, autoRepeat, count);
505
// need window instead of focus item so e.g. QQuickItem::keyPressEvent() gets properly called
506
QGuiApplication::sendEvent(qGuiApp->focusWindow(), &event);
511
void MInputContext::updateInputMethodArea(const QRect &rect)
513
bool wasVisible = isInputPanelVisible();
515
if (rect != keyboardRectangle) {
516
keyboardRectangle = rect;
517
emitKeyboardRectChanged();
519
if (wasVisible != isInputPanelVisible()) {
520
emitInputPanelVisibleChanged();
526
void MInputContext::setGlobalCorrectionEnabled(bool enabled)
529
// not handling preedit injections, ignored
533
void MInputContext::getPreeditRectangle(QRect &rectangle, bool &valid) const
542
void MInputContext::onInvokeAction(const QString &action, const QKeySequence &sequence)
544
if (debug) qDebug() << InputContextName << __PRETTY_FUNCTION__ << "action" << action;
546
// NOTE: currently not trying to trigger action directly
547
static const Qt::KeyboardModifiers AllModifiers = Qt::ShiftModifier | Qt::ControlModifier | Qt::AltModifier
548
| Qt::MetaModifier | Qt::KeypadModifier;
550
for (int i = 0; i < sequence.count(); i++) {
551
const int key = sequence[i] & ~AllModifiers;
552
const int modifiers = sequence[i] & AllModifiers;
554
if (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier) {
557
keyEvent(QEvent::KeyPress, key, modifiers, text, false, 1);
558
keyEvent(QEvent::KeyRelease, key, modifiers, text, false, 1);
562
void MInputContext::onDBusDisconnection()
564
if (debug) qDebug() << __PRETTY_FUNCTION__;
567
redirectKeys = false;
569
updateInputMethodArea(QRect());
572
void MInputContext::onDBusConnection()
574
if (debug) qDebug() << __PRETTY_FUNCTION__;
576
// using one attribute extension for everything
577
imServer->registerAttributeExtension(0, QString());
579
// Force activation, since setFocusWidget may have been called after
580
// onDBusDisconnection set active to false or before the dbus connection.
583
if (inputMethodAccepted()) {
584
setFocusObject(QGuiApplication::focusObject());
585
if (inputPanelState != InputPanelHidden) {
586
imServer->showInputMethod();
587
inputPanelState = InputPanelShown;
592
void MInputContext::notifyOrientationAboutToChange(MInputContext::OrientationAngle angle)
594
// can get called from signal so cannot be sure we are really currently active
596
imServer->appOrientationAboutToChange(static_cast<int>(angle));
600
void MInputContext::notifyOrientationChanged(MInputContext::OrientationAngle angle)
602
// can get called from signal so cannot be sure we are really currently active
604
imServer->appOrientationChanged(static_cast<int>(angle));
608
Maliit::TextContentType MInputContext::contentType(Qt::InputMethodHints hints) const
610
Maliit::TextContentType type = Maliit::FreeTextContentType;
611
hints &= Qt::ImhExclusiveInputMask;
613
if (hints == Qt::ImhFormattedNumbersOnly || hints == Qt::ImhDigitsOnly) {
614
type = Maliit::NumberContentType;
615
} else if (hints == Qt::ImhDialableCharactersOnly) {
616
type = Maliit::PhoneNumberContentType;
617
} else if (hints == Qt::ImhEmailCharactersOnly) {
618
type = Maliit::EmailContentType;
619
} else if (hints == Qt::ImhUrlCharactersOnly) {
620
type = Maliit::UrlContentType;
626
void MInputContext::setRedirectKeys(bool enabled)
628
redirectKeys = enabled;
631
void MInputContext::setDetectableAutoRepeat(bool enabled)
634
qWarning() << "Detectable autorepeat not supported.";
637
QMap<QString, QVariant> MInputContext::getStateInformation() const
639
QMap<QString, QVariant> stateInformation;
641
stateInformation["focusState"] = inputMethodAccepted();
643
if (!inputMethodAccepted()) {
644
return stateInformation;
647
QInputMethodQueryEvent query(Qt::ImQueryAll);
648
QGuiApplication::sendEvent(qGuiApp->focusObject(), &query);
650
QVariant queryResult;
652
queryResult = query.value(Qt::ImSurroundingText);
653
if (queryResult.isValid()) {
654
stateInformation["surroundingText"] = queryResult.toString();
657
queryResult = query.value(Qt::ImCursorPosition);
658
if (queryResult.isValid()) {
659
stateInformation["cursorPosition"] = queryResult.toInt();
662
queryResult = query.value(Qt::ImAnchorPosition);
663
if (queryResult.isValid()) {
664
stateInformation["anchorPosition"] = queryResult.toInt();
667
queryResult = query.value(Qt::ImHints);
668
Qt::InputMethodHints hints = static_cast<Qt::InputMethodHints>(queryResult.toUInt());
670
// content type value
671
// Deprecated, replaced by just transmitting all hints (see below):
672
// FIXME: Remove once MAbstractInputMethod API for this got deprecated/removed.
673
stateInformation["contentType"] = contentType(hints);
675
stateInformation["autocapitalizationEnabled"] = !(hints & Qt::ImhNoAutoUppercase);
676
stateInformation["hiddenText"] = static_cast<bool>(hints & Qt::ImhHiddenText);
677
stateInformation["predictionEnabled"] = ! static_cast<bool>(hints & Qt::ImhNoPredictiveText);
679
stateInformation["maliit-inputmethod-hints"] = QVariant(static_cast<qint64>(hints));
682
queryResult = query.value(Qt::ImCurrentSelection);
683
if (queryResult.isValid()) {
684
stateInformation["hasSelection"] = !(queryResult.toString().isEmpty());
687
QWindow *window = qGuiApp->focusWindow();
689
stateInformation["winId"] = static_cast<qulonglong>(window->winId());
692
queryResult = query.value(Qt::ImCursorRectangle);
693
if (queryResult.isValid()) {
694
QRect rect = queryResult.toRect();
695
rect = qGuiApp->inputMethod()->inputItemTransform().mapRect(rect);
697
stateInformation["cursorRectangle"] = QRect(window->mapToGlobal(rect.topLeft()), rect.size());
701
stateInformation["toolbarId"] = 0; // Global extension id. And bad state parameter name for it.
703
return stateInformation;
706
void MInputContext::setSelection(int start, int length)
708
if (!inputMethodAccepted())
711
QList<QInputMethodEvent::Attribute> attributes;
712
attributes << QInputMethodEvent::Attribute(QInputMethodEvent::Selection, start,
714
QInputMethodEvent event("", attributes);
715
QGuiApplication::sendEvent(qGuiApp->focusObject(), &event);
718
void MInputContext::getSelection(QString &selection, bool &valid) const
722
QString selectionText;
725
if (!inputMethodAccepted()) {
729
QInputMethodQueryEvent query(Qt::ImCurrentSelection);
730
QGuiApplication::sendEvent(qGuiApp->focusObject(), &query);
732
QVariant queryResult = query.value(Qt::ImCurrentSelection);
733
valid = queryResult.isValid();
734
selectionText = queryResult.toString();
736
selection = selectionText;
739
int MInputContext::cursorStartPosition(bool *valid)
746
if (!inputMethodAccepted()) {
750
QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition);
751
QGuiApplication::sendEvent(qGuiApp->focusObject(), &query);
753
// obtain the cursor absolute position
754
QVariant queryResult = query.value(Qt::ImCursorPosition);
755
if (queryResult.isValid()) {
756
int absCursorPos = queryResult.toInt();
758
// Fetch anchor position too but don't require it.
759
queryResult = query.value(Qt::ImAnchorPosition);
760
int absAnchorPos = queryResult.isValid() ? queryResult.toInt() : absCursorPos;
762
// In case of selection, base cursorPos on start of it.
763
start = qMin<int>(absCursorPos, absAnchorPos);
770
void MInputContext::updateInputMethodExtensions()
772
if (!inputMethodAccepted()) {
775
if (debug) qDebug() << InputContextName << __PRETTY_FUNCTION__;
777
QVariantMap extensions = qGuiApp->focusObject()->property("__inputMethodExtensions").toMap();
779
value = extensions.value("enterKeyIconSource");
780
imServer->setExtendedAttribute(0, "/keys", "actionKey", "icon", QVariant(value.toUrl().toString()));
782
value = extensions.value("enterKeyText");
783
imServer->setExtendedAttribute(0, "/keys", "actionKey", "label", QVariant(value.toString()));
785
value = extensions.value("enterKeyEnabled");
786
imServer->setExtendedAttribute(0, "/keys", "actionKey", "enabled", value.isValid() ? value.toBool() : true);
788
value = extensions.value("enterKeyHighlighted");
789
imServer->setExtendedAttribute(0, "/keys", "actionKey", "highlighted", value.isValid() ? value.toBool() : false);