diff --git a/inputcontext/QskHunspellCompositionModel.cpp b/inputcontext/QskHunspellTextPredictor.cpp similarity index 71% rename from inputcontext/QskHunspellCompositionModel.cpp rename to inputcontext/QskHunspellTextPredictor.cpp index 69c7e0a3..92e66c44 100644 --- a/inputcontext/QskHunspellCompositionModel.cpp +++ b/inputcontext/QskHunspellTextPredictor.cpp @@ -1,16 +1,16 @@ -#include "QskHunspellCompositionModel.h" +#include "QskHunspellTextPredictor.h" #include #include "hunspell.h" -class QskHunspellCompositionModel::PrivateData +class QskHunspellTextPredictor::PrivateData { public: Hunhandle* hunspellHandle; QVector< QString > candidates; }; -QskHunspellCompositionModel::QskHunspellCompositionModel( QObject* object ): +QskHunspellTextPredictor::QskHunspellTextPredictor( QObject* object ): Inherited( Words, object ), m_data( new PrivateData() ) { @@ -23,31 +23,31 @@ QskHunspellCompositionModel::QskHunspellCompositionModel( QObject* object ): #endif } -QskHunspellCompositionModel::~QskHunspellCompositionModel() +QskHunspellTextPredictor::~QskHunspellTextPredictor() { Hunspell_destroy( m_data->hunspellHandle ); } -int QskHunspellCompositionModel::candidateCount() const +int QskHunspellTextPredictor::candidateCount() const { return m_data->candidates.count(); } -QString QskHunspellCompositionModel::candidate( int pos ) const +QString QskHunspellTextPredictor::candidate( int pos ) const { return m_data->candidates[ pos ]; } -void QskHunspellCompositionModel::resetCandidates() +void QskHunspellTextPredictor::reset() { if ( !m_data->candidates.isEmpty() ) { m_data->candidates.clear(); - Q_EMIT candidatesChanged(); + Q_EMIT predictionChanged(); } } -void QskHunspellCompositionModel::requestCandidates( const QString& text ) +void QskHunspellTextPredictor::request( const QString& text ) { char** suggestions; const QByteArray word = text.toUtf8(); // ### do we need to check the encoding @@ -71,5 +71,5 @@ void QskHunspellCompositionModel::requestCandidates( const QString& text ) Hunspell_free_list( m_data->hunspellHandle, &suggestions, count ); m_data->candidates = candidates; - Q_EMIT candidatesChanged(); + Q_EMIT predictionChanged(); } diff --git a/inputcontext/QskHunspellCompositionModel.h b/inputcontext/QskHunspellTextPredictor.h similarity index 54% rename from inputcontext/QskHunspellCompositionModel.h rename to inputcontext/QskHunspellTextPredictor.h index 470cfaa7..01e5832d 100644 --- a/inputcontext/QskHunspellCompositionModel.h +++ b/inputcontext/QskHunspellTextPredictor.h @@ -3,26 +3,26 @@ * This file may be used under the terms of the QSkinny License, Version 1.0 *****************************************************************************/ -#ifndef QSK_HUNSPELL_COMPOSITION_MODEL_H -#define QSK_HUNSPELL_COMPOSITION_MODEL_H +#ifndef QSK_HUNSPELL_TEXT_PREDICTOR_H +#define QSK_HUNSPELL_TEXT_PREDICTOR_H -#include "QskInputCompositionModel.h" +#include "QskTextPredictor.h" #include -class QskHunspellCompositionModel : public QskInputCompositionModel +class QSK_EXPORT QskHunspellTextPredictor : public QskTextPredictor { - using Inherited = QskInputCompositionModel; + using Inherited = QskTextPredictor; public: - QskHunspellCompositionModel( QObject* ); - virtual ~QskHunspellCompositionModel() override; + QskHunspellTextPredictor( QObject* ); + virtual ~QskHunspellTextPredictor() override; virtual int candidateCount() const override; virtual QString candidate( int pos ) const override; protected: - virtual void requestCandidates( const QString& ) override; - virtual void resetCandidates() override; + virtual void request( const QString& ) override; + virtual void reset() override; private: class PrivateData; diff --git a/inputcontext/QskInputContextPlugin.cpp b/inputcontext/QskInputContextPlugin.cpp index 69a2accc..8fdc8204 100644 --- a/inputcontext/QskInputContextPlugin.cpp +++ b/inputcontext/QskInputContextPlugin.cpp @@ -6,8 +6,8 @@ #include #include "QskInputContext.h" -#include "QskPinyinCompositionModel.h" -#include "QskHunspellCompositionModel.h" +#include "QskPinyinTextPredictor.h" +#include "QskHunspellTextPredictor.h" #include @@ -25,13 +25,13 @@ public: auto context = new QskInputContext(); #if 0 - context->setCompositionModel( QLocale(), - new QskHunspellCompositionModel( this ) ); + context->registerPredictor( QLocale(), + new QskHunspellTextPredictor( this ) ); #endif #if 0 - context->setCompositionModel( - QLocale::Chinese, new QskPinyinCompositionModel( this ) ); + context->registerPredictor( + QLocale::Chinese, new QskPinyinTextPredictor( this ) ); #endif return context; diff --git a/inputcontext/QskPinyinCompositionModel.cpp b/inputcontext/QskPinyinTextPredictor.cpp similarity index 80% rename from inputcontext/QskPinyinCompositionModel.cpp rename to inputcontext/QskPinyinTextPredictor.cpp index 469b5745..f2605dcd 100644 --- a/inputcontext/QskPinyinCompositionModel.cpp +++ b/inputcontext/QskPinyinTextPredictor.cpp @@ -3,7 +3,7 @@ * This file may be used under the terms of the QSkinny License, Version 1.0 *****************************************************************************/ -#include "QskPinyinCompositionModel.h" +#include "QskPinyinTextPredictor.h" #include "QskInputContext.h" #include "pinyinime.h" @@ -11,13 +11,13 @@ #include #include -class QskPinyinCompositionModel::PrivateData +class QskPinyinTextPredictor::PrivateData { public: QStringList candidates; }; -QskPinyinCompositionModel::QskPinyinCompositionModel( QObject* parent ): +QskPinyinTextPredictor::QskPinyinTextPredictor( QObject* parent ): Inherited( Attributes(), parent ), m_data( new PrivateData ) { @@ -34,17 +34,17 @@ QskPinyinCompositionModel::QskPinyinCompositionModel( QObject* parent ): } } -QskPinyinCompositionModel::~QskPinyinCompositionModel() +QskPinyinTextPredictor::~QskPinyinTextPredictor() { ime_pinyin::im_close_decoder(); } -int QskPinyinCompositionModel::candidateCount() const +int QskPinyinTextPredictor::candidateCount() const { return m_data->candidates.count(); } -QString QskPinyinCompositionModel::candidate( int index ) const +QString QskPinyinTextPredictor::candidate( int index ) const { if ( ( index >= 0 ) && ( index < m_data->candidates.count() ) ) return m_data->candidates[ index ]; @@ -52,18 +52,18 @@ QString QskPinyinCompositionModel::candidate( int index ) const return QString(); } -void QskPinyinCompositionModel::resetCandidates() +void QskPinyinTextPredictor::reset() { ime_pinyin::im_reset_search(); if ( !m_data->candidates.isEmpty() ) { m_data->candidates.clear(); - Q_EMIT candidatesChanged(); + Q_EMIT predictionChanged(); } } -void QskPinyinCompositionModel::requestCandidates( const QString& text ) +void QskPinyinTextPredictor::request( const QString& text ) { const QByteArray bytes = text.toLatin1(); @@ -101,5 +101,5 @@ void QskPinyinCompositionModel::requestCandidates( const QString& text ) } m_data->candidates = candidates; - Q_EMIT candidatesChanged(); + Q_EMIT predictionChanged(); } diff --git a/inputcontext/QskPinyinCompositionModel.h b/inputcontext/QskPinyinTextPredictor.h similarity index 54% rename from inputcontext/QskPinyinCompositionModel.h rename to inputcontext/QskPinyinTextPredictor.h index 487f27a6..586505db 100644 --- a/inputcontext/QskPinyinCompositionModel.h +++ b/inputcontext/QskPinyinTextPredictor.h @@ -3,26 +3,26 @@ * This file may be used under the terms of the QSkinny License, Version 1.0 *****************************************************************************/ -#ifndef QSK_PINYIN_COMPOSITION_MODEL_H -#define QSK_PINYIN_COMPOSITION_MODEL_H +#ifndef QSK_PINYIN_TEXT_PREDICTOR_H +#define QSK_PINYIN_TEXT_PREDICTOR_H -#include "QskInputCompositionModel.h" +#include "QskTextPredictor.h" #include -class QskPinyinCompositionModel : public QskInputCompositionModel +class QSK_EXPORT QskPinyinTextPredictor : public QskTextPredictor { - using Inherited = QskInputCompositionModel; + using Inherited = QskTextPredictor; public: - QskPinyinCompositionModel( QObject* ); - virtual ~QskPinyinCompositionModel() override; + QskPinyinTextPredictor( QObject* ); + virtual ~QskPinyinTextPredictor() override; virtual int candidateCount() const override; virtual QString candidate( int ) const override; protected: - virtual void requestCandidates( const QString& ) override; - virtual void resetCandidates() override; + virtual void request( const QString& ) override; + virtual void reset() override; private: class PrivateData; diff --git a/inputcontext/inputcontext.pro b/inputcontext/inputcontext.pro index ac7cd013..f546614f 100644 --- a/inputcontext/inputcontext.pro +++ b/inputcontext/inputcontext.pro @@ -81,10 +81,10 @@ pinyin { $${QSK_PINYIN_DIR}/include/utf16reader.h HEADERS += \ - QskPinyinCompositionModel.h + QskPinyinTextPredictor.h SOURCES += \ - QskPinyinCompositionModel.cpp + QskPinyinTextPredictor.cpp } hunspell { @@ -124,10 +124,10 @@ hunspell { $${QSK_HUNSPELL_DIR}/suggestmgr.cxx HEADERS += \ - QskHunspellCompositionModel.h + QskHunspellTextPredictor.h SOURCES += \ - QskHunspellCompositionModel.cpp + QskHunspellTextPredictor.cpp OTHER_FILES +=\ $${QSK_HUNSPELL_DIR}/license.hunspell \ diff --git a/playground/inputpanel/main.cpp b/playground/inputpanel/main.cpp index cdbe0922..50b78146 100644 --- a/playground/inputpanel/main.cpp +++ b/playground/inputpanel/main.cpp @@ -26,6 +26,93 @@ #define STRINGIFY(x) #x #define STRING(x) STRINGIFY(x) +static inline QString nativeLocaleString( const QLocale& locale ) +{ + switch( locale.language() ) + { + case QLocale::Bulgarian: + return QStringLiteral( "български език" ); + + case QLocale::Czech: + return QStringLiteral( "Čeština" ); + + case QLocale::German: + return QStringLiteral( "Deutsch" ); + + case QLocale::Danish: + return QStringLiteral( "Dansk" ); + + case QLocale::Greek: + return QStringLiteral( "Eλληνικά" ); + + case QLocale::English: + { + switch( locale.country() ) + { + case QLocale::Canada: + case QLocale::UnitedStates: + case QLocale::UnitedStatesMinorOutlyingIslands: + case QLocale::UnitedStatesVirginIslands: + return QStringLiteral( "English (US)" ); + + default: + return QStringLiteral( "English (UK)" ); + } + } + + case QLocale::Spanish: + return QStringLiteral( "Español" ); + + case QLocale::Finnish: + return QStringLiteral( "Suomi" ); + + case QLocale::French: + return QStringLiteral( "Français" ); + + case QLocale::Hungarian: + return QStringLiteral( "Magyar" ); + + case QLocale::Italian: + return QStringLiteral( "Italiano" ); + + case QLocale::Japanese: + return QStringLiteral( "日本語" ); + + case QLocale::Latvian: + return QStringLiteral( "Latviešu" ); + + case QLocale::Lithuanian: + return QStringLiteral( "Lietuvių" ); + + case QLocale::Dutch: + return QStringLiteral( "Nederlands" ); + + case QLocale::Portuguese: + return QStringLiteral( "Português" ); + + case QLocale::Romanian: + return QStringLiteral( "Română" ); + + case QLocale::Russia: + return QStringLiteral( "Русский" ); + + case QLocale::Slovenian: + return QStringLiteral( "Slovenščina" ); + + case QLocale::Slovak: + return QStringLiteral( "Slovenčina" ); + + case QLocale::Turkish: + return QStringLiteral( "Türkçe" ); + + case QLocale::Chinese: + return QStringLiteral( "中文" ); + + default: + return QLocale::languageToString( locale.language() ); + } +} + class InputBox : public QskLinearBox { public: @@ -149,7 +236,7 @@ public: private: inline void append( const QLocale& locale ) { - m_values += qMakePair( qskNativeLocaleString( locale ), locale ); + m_values += qMakePair( nativeLocaleString( locale ), locale ); } QVector< QPair< QString, QLocale > > m_values; diff --git a/src/inputpanel/QskInputContext.cpp b/src/inputpanel/QskInputContext.cpp index 36383e32..44f1193e 100644 --- a/src/inputpanel/QskInputContext.cpp +++ b/src/inputpanel/QskInputContext.cpp @@ -4,113 +4,73 @@ *****************************************************************************/ #include "QskInputContext.h" -#include "QskInputCompositionModel.h" +#include "QskTextPredictor.h" #include "QskInputPanel.h" +#include "QskInputEngine.h" #include "QskLinearBox.h" #include #include #include -#include #include #include -#include #include #include #include -static inline QString qskKeyString( int keyCode ) -{ - // Special case entry codes here, else default to the symbol - switch ( keyCode ) - { - case Qt::Key_Shift: - case Qt::Key_CapsLock: - case Qt::Key_Mode_switch: - case Qt::Key_Backspace: - case Qt::Key_Muhenkan: - return QString(); - - case Qt::Key_Return: - case Qt::Key_Kanji: - return QChar( QChar::CarriageReturn ); - - case Qt::Key_Space: - return QChar( QChar::Space ); - - default: - break; - } - - return QChar( keyCode ); -} - -static void qskSetLocale( QQuickItem* inputPanel, const QLocale& locale ) -{ - if ( auto control = qobject_cast< QskControl* >( inputPanel ) ) - { - control->setLocale( locale ); - } - else - { - const auto mo = inputPanel->metaObject(); - - const auto property = mo->property( mo->indexOfProperty( "locale" ) ); - if ( property.isWritable() ) - property.write( inputPanel, locale ); - } -} - -static QLocale qskLocale( const QQuickItem* inputPanel ) -{ - if ( inputPanel == nullptr ) - return QLocale(); - - if ( auto control = qobject_cast< const QskControl* >( inputPanel ) ) - return control->locale(); - - return inputPanel->property( "locale" ).toLocale(); -} - -static void qskSetCandidatesEnabled( QQuickItem* inputPanel, bool on ) -{ - if ( inputPanel == nullptr ) - return; - - if ( auto panel = qobject_cast< QskInputPanel* >( inputPanel ) ) - { - panel->setCandidatesEnabled( on ); - } - else - { - QMetaObject::invokeMethod( inputPanel, "setCandidatesEnabled", - Qt::DirectConnection, Q_ARG( bool, on ) ); - } -} - -static void qskSetCandidates( QQuickItem* inputPanel, - const QVector< QString >& candidates ) -{ - if ( inputPanel == nullptr ) - return; - - if ( auto panel = qobject_cast< QskInputPanel* >( inputPanel ) ) - { - panel->setCandidates( candidates ); - } - else - { - QMetaObject::invokeMethod( inputPanel, "setCandidates", - Qt::DirectConnection, Q_ARG( QVector< QString >, candidates ) ); - } -} - static inline uint qskHashLocale( const QLocale& locale ) { return uint( locale.language() + ( uint( locale.country() ) << 16 ) ); } +namespace +{ + class PredictorTable + { + public: + void replace( const QLocale& locale, QskTextPredictor* predictor ) + { + const auto key = qskHashLocale( locale ); + + if ( predictor ) + { + const auto it = hashTab.find( key ); + if ( it != hashTab.end() ) + { + if ( it.value() == predictor ) + return; + + delete it.value(); + *it = predictor; + } + else + { + hashTab.insert( key, predictor ); + } + } + else + { + const auto it = hashTab.find( key ); + if ( it != hashTab.end() ) + { + delete it.value(); + hashTab.erase( it ); + } + } + } + + QskTextPredictor* find( const QLocale& locale ) + { + const auto key = qskHashLocale( locale ); + return hashTab.value( key, nullptr ); + } + + private: + QHash< uint, QskTextPredictor* > hashTab; + }; +} + class QskInputContext::PrivateData { public: @@ -124,9 +84,9 @@ public: QskPopup* inputPopup = nullptr; QskWindow* inputWindow = nullptr; - QHash< uint, QskInputCompositionModel* > compositionModels; + PredictorTable predictorTable; - QString preedit; + QskInputEngine* engine = nullptr; // the input panel is embedded in a window bool ownsInputPanelWindow : 1; @@ -137,6 +97,8 @@ QskInputContext::QskInputContext(): { setObjectName( "InputContext" ); + m_data->engine = new QskInputEngine( this ); + connect( qskSetup, &QskSetup::inputPanelChanged, this, &QskInputContext::setInputPanel ); @@ -168,114 +130,48 @@ void QskInputContext::setInputItem( QQuickItem* item ) if ( m_data->inputItem == item ) return; - m_data->inputItem = item; + auto panel = qobject_cast< QskInputPanel* >( m_data->inputPanel ); - if ( item ) - update( Qt::ImQueryAll ); + if ( isInputPanelVisible() ) + { + if ( item == nullptr ) + { + hideInputPanel(); + } + else + { + if ( panel ) + panel->attachInputItem( item ); + + update( Qt::ImQueryAll ); + } + } else - hideInputPanel(); + { + // no need for updates + if ( panel ) + panel->attachInputItem( nullptr ); + } + + m_data->inputItem = item; } void QskInputContext::update( Qt::InputMethodQueries queries ) { - if ( m_data->inputItem == nullptr || m_data->inputPanel == nullptr ) - return; - - const auto queryEvent = queryInputMethod( queries ); - - if ( queryEvent.queries() & Qt::ImEnabled ) + if ( queries & Qt::ImEnabled ) { - if ( !queryEvent.value( Qt::ImEnabled ).toBool() ) + QInputMethodQueryEvent event( Qt::ImEnabled ); + QCoreApplication::sendEvent( m_data->inputItem, &event ); + + if ( !event.value( Qt::ImEnabled ).toBool() ) { hideInputPanel(); return; } } - if ( queryEvent.queries() & Qt::ImHints ) - { - /* - ImhHiddenText = 0x1, // might need to disable certain checks - ImhSensitiveData = 0x2, // shouldn't change anything - ImhNoAutoUppercase = 0x4, // if we support auto uppercase, disable it - ImhPreferNumbers = 0x8, // default to number keyboard - ImhPreferUppercase = 0x10, // start with shift on - ImhPreferLowercase = 0x20, // start with shift off - ImhNoPredictiveText = 0x40, // not use predictive text - - ImhDate = 0x80, // ignored for now (no date keyboard) - ImhTime = 0x100, // ignored for know (no time keyboard) - - ImhPreferLatin = 0x200, // can be used to launch chinese kb in english mode - - ImhMultiLine = 0x400, // not useful? - - ImhDigitsOnly // default to number keyboard, disable other keys - ImhFormattedNumbersOnly // hard to say - ImhUppercaseOnly // caps-lock, disable shift - ImhLowercaseOnly // disable shift - ImhDialableCharactersOnly // dial pad (calculator?) - ImhEmailCharactersOnly // disable certain symbols (email-only kb?) - ImhUrlCharactersOnly // disable certain symbols (url-only kb?) - ImhLatinOnly // disable chinese input - */ - -#if 0 - const auto hints = static_cast< Qt::InputMethodHints >( - queryEvent.value( Qt::ImHints ).toInt() ); - -#endif - } - - if ( queryEvent.queries() & Qt::ImPreferredLanguage ) - { - const auto locale = queryEvent.value( Qt::ImPreferredLanguage ).toLocale(); - - const auto oldModel = compositionModel(); - - if( m_data->inputPanel ) - qskSetLocale( m_data->inputPanel, locale ); - - auto newModel = compositionModel(); - - if( newModel && ( oldModel != newModel ) ) - { - connect( newModel, &QskInputCompositionModel::candidatesChanged, - this, &QskInputContext::handleCandidatesChanged ); - } - - qskSetCandidatesEnabled( m_data->inputPanel, newModel != nullptr ); - } - -#if 0 - if ( queryEvent.queries() & Qt::ImTextBeforeCursor - && queryEvent.queries() & Qt::ImTextAfterCursor ) - { - const auto text1 = queryEvent.value( Qt::ImTextBeforeCursor ).toString(); - const auto text2 = queryEvent.value( Qt::ImTextAfterCursor ).toString(); - - qDebug() << text1 << text2; - } -#endif - - /* - Qt::ImMicroFocus - Qt::ImCursorRectangle - Qt::ImFont - Qt::ImCursorPosition - Qt::ImSurroundingText // important for chinese input - Qt::ImCurrentSelection // important for prediction - Qt::ImMaximumTextLength // should be monitored - Qt::ImAnchorPosition - - Qt::ImAbsolutePosition - Qt::ImTextBeforeCursor // important for chinese - Qt::ImTextAfterCursor // important for chinese - Qt::ImPlatformData // hard to say... - Qt::ImEnterKeyType - Qt::ImAnchorRectangle - Qt::ImInputItemClipRectangle // could be used for the geometry of the panel - */ + if ( auto panel = qobject_cast< QskInputPanel* >( m_data->inputPanel ) ) + panel->processInputMethodQueries( queries ); } QRectF QskInputContext::keyboardRect() const @@ -417,6 +313,11 @@ void QskInputContext::showInputPanel() connect( inputPanel->window(), &QskWindow::visibleChanged, this, &QskInputContext::emitInputPanelVisibleChanged ); + + updateInputPanel( m_data->inputItem ); + + m_data->engine->setPredictor( + m_data->predictorTable.find( locale() ) ); } void QskInputContext::hideInputPanel() @@ -425,6 +326,8 @@ void QskInputContext::hideInputPanel() { // to get rid of the scene graph nodes m_data->inputPanel->setVisible( false ); + if ( auto panel = qobject_cast< QskInputPanel* >( m_data->inputPanel ) ) + panel->setEngine( nullptr ); } if ( m_data->inputPopup == m_data->inputPanel ) @@ -465,9 +368,19 @@ void QskInputContext::hideInputPanel() qGuiApp->removeEventFilter( this ); - m_data->preedit.clear(); - if ( auto model = compositionModel() ) - model->resetCandidates(); + updateInputPanel( nullptr ); +} + +void QskInputContext::updateInputPanel( QQuickItem* inputItem ) +{ + auto panel = qobject_cast< QskInputPanel* >( m_data->inputPanel ); + if ( panel == nullptr ) + return; + + panel->setLocale( locale() ); + panel->attachInputItem( inputItem ); + + panel->setEngine( inputItem ? m_data->engine : nullptr ); } bool QskInputContext::isInputPanelVisible() const @@ -480,7 +393,15 @@ bool QskInputContext::isInputPanelVisible() const QLocale QskInputContext::locale() const { - return qskLocale( m_data->inputPanel ); + if ( m_data->inputItem ) + { + QInputMethodQueryEvent event( Qt::ImPreferredLanguage ); + QCoreApplication::sendEvent( m_data->inputItem, &event ); + + return event.value( Qt::ImPreferredLanguage ).toLocale(); + } + + return QLocale(); } Qt::LayoutDirection QskInputContext::inputDirection() const @@ -535,235 +456,32 @@ void QskInputContext::setFocusObject( QObject* focusObject ) } } -void QskInputContext::setCompositionModel( - const QLocale& locale, QskInputCompositionModel* model ) +void QskInputContext::registerPredictor( + const QLocale& locale, QskTextPredictor* predictor ) { - auto& models = m_data->compositionModels; - - const auto key = qskHashLocale( locale ); - - if ( model ) - { - const auto it = models.find( key ); - if ( it != models.end() ) - { - if ( it.value() == model ) - return; - - delete it.value(); - *it = model; - } - else - { - models.insert( key, model ); - } - - connect( model, &QskInputCompositionModel::candidatesChanged, - this, &QskInputContext::handleCandidatesChanged ); - } - else - { - const auto it = models.find( key ); - if ( it != models.end() ) - { - delete it.value(); - models.erase( it ); - } - } -} - -QskInputCompositionModel* QskInputContext::compositionModel() const -{ - const auto key = qskHashLocale( locale() ); - return m_data->compositionModels.value( key, nullptr ); -} - -void QskInputContext::invokeAction( QInputMethod::Action action, int value ) -{ - switch ( static_cast< int >( action ) ) - { - case QskInputPanel::Compose: - { - processKey( value ); - break; - } - case QskInputPanel::SelectCandidate: - { - if ( auto model = compositionModel() ) - { - auto text = model->candidate( value ); - - if ( model->attributes() & QskInputCompositionModel::Words ) - text += " "; - - sendText( text, true ); - - m_data->preedit.clear(); - model->resetCandidates(); - } - - break; - } - case QInputMethod::Click: - case QInputMethod::ContextMenu: - { - break; - } - } -} - -int QskInputContext::keysLeft() const -{ - const auto event = queryInputMethod( - Qt::ImSurroundingText | Qt::ImMaximumTextLength | Qt::ImHints ); - - const auto hints = static_cast< Qt::InputMethodHints >( - event.value( Qt::ImHints ).toInt() ); - - if ( !( hints & Qt::ImhMultiLine ) ) - { - const int max = event.value( Qt::ImMaximumTextLength ).toInt(); - - if ( max > 0 ) - { - const auto text = event.value( Qt::ImSurroundingText ).toString(); - return max - text.length(); - } - } - - return -1; // unlimited -} - -Qt::InputMethodHints QskInputContext::inputHints() const -{ - const auto e = queryInputMethod( Qt::ImHints ); - return static_cast< Qt::InputMethodHints >( e.value( Qt::ImHints ).toInt() ); -} - -void QskInputContext::processKey( int key ) -{ - const auto hints = inputHints(); - - auto spaceLeft = keysLeft(); - - QskInputCompositionModel* model = nullptr; - - if ( !( hints & Qt::ImhHiddenText ) ) - model = compositionModel(); - - auto& preedit = m_data->preedit; - /* - First we have to handle the control keys - */ - switch ( key ) - { - case Qt::Key_Backspace: - case Qt::Key_Muhenkan: - { - if ( model ) - { - if ( !preedit.isEmpty() ) - { - preedit.chop( 1 ); - sendText( preedit, false ); - - model->requestCandidates( preedit ); - return; - } - } - - sendKey( Qt::Key_Backspace ); - return; - } - case Qt::Key_Return: - { - if ( model ) - { - if ( !preedit.isEmpty() ) - { - if ( spaceLeft ) - sendText( preedit.left( spaceLeft ), true ); - - preedit.clear(); - model->resetCandidates(); - - return; - } - } - - if( !( hints & Qt::ImhMultiLine ) ) - { - sendKey( Qt::Key_Return ); - return; - } - - break; - } - case Qt::Key_Space: - { - if ( model ) - { - if ( !preedit.isEmpty() && spaceLeft) - { - preedit = preedit.left( spaceLeft ); - sendText( preedit, true ); - spaceLeft -= preedit.length(); - - preedit.clear(); - model->resetCandidates(); - } - } - - break; - } - case Qt::Key_Left: - case Qt::Key_Right: - case Qt::Key_Escape: - { - sendKey( key ); - return; - } - } - - const QString text = qskKeyString( key ); - - if ( model ) - { - preedit += text; - - model->requestCandidates( preedit ); - - if ( model->candidateCount() > 0 ) - { - sendText( preedit, false ); - } - else - { - sendText( preedit.left( spaceLeft ), true ); - preedit.clear(); - } - } - else - { - sendText( text, true ); - } -} - -void QskInputContext::handleCandidatesChanged() -{ - const auto model = compositionModel(); - if ( model == nullptr || m_data->inputPanel == nullptr ) + auto oldPredictor = m_data->predictorTable.find( locale ); + if ( predictor == oldPredictor ) return; - const auto count = model->candidateCount(); + if ( predictor ) + predictor->setParent( this ); - QVector< QString > candidates; - candidates.reserve( count ); + m_data->predictorTable.replace( locale, predictor ); - for( int i = 0; i < count; i++ ) - candidates += model->candidate( i ); + if ( oldPredictor ) + delete oldPredictor; - qskSetCandidates( m_data->inputPanel, candidates ); + if ( qskHashLocale( locale ) == qskHashLocale( this->locale() ) ) + m_data->engine->setPredictor( predictor ); +} + +QskTextPredictor* QskInputContext::registeredPredictor( const QLocale& locale ) +{ + return m_data->predictorTable.find( locale ); +} + +void QskInputContext::invokeAction( QInputMethod::Action, int ) +{ } void QskInputContext::setInputPanel( QQuickItem* inputPanel ) @@ -771,8 +489,6 @@ void QskInputContext::setInputPanel( QQuickItem* inputPanel ) if ( m_data->inputPanel == inputPanel ) return; - auto model = compositionModel(); - if ( m_data->inputPanel ) { m_data->inputPanel->disconnect( this ); @@ -785,9 +501,6 @@ void QskInputContext::setInputPanel( QQuickItem* inputPanel ) { m_data->inputPanel->setParentItem( nullptr ); } - - if ( model ) - model->disconnect( m_data->inputPanel ); } m_data->inputPanel = inputPanel; @@ -806,8 +519,6 @@ void QskInputContext::setInputPanel( QQuickItem* inputPanel ) connect( control, &QskControl::localeChanged, this, &QPlatformInputContext::emitLocaleChanged ); } - - qskSetCandidatesEnabled( inputPanel, model != nullptr ); } } @@ -817,6 +528,7 @@ void QskInputContext::reset() void QskInputContext::commit() { + // called on focus changes } bool QskInputContext::eventFilter( QObject* object, QEvent* event ) @@ -874,54 +586,4 @@ bool QskInputContext::filterEvent( const QEvent* ) return false; } -QInputMethodQueryEvent QskInputContext::queryInputMethod( - Qt::InputMethodQueries queries ) const -{ - QInputMethodQueryEvent event( queries ); - - if ( m_data->inputItem ) - QCoreApplication::sendEvent( m_data->inputItem, &event ); - - return event; -} - -void QskInputContext::sendText( - const QString& text, bool isFinal ) const -{ - if ( m_data->inputItem == nullptr ) - return; - - if ( isFinal ) - { - QInputMethodEvent event; - event.setCommitString( text ); - - QCoreApplication::sendEvent( m_data->inputItem, &event ); - } - else - { - QTextCharFormat format; - format.setFontUnderline( true ); - - const QInputMethodEvent::Attribute attribute( - QInputMethodEvent::TextFormat, 0, text.length(), format ); - - QInputMethodEvent event( text, { attribute } ); - - QCoreApplication::sendEvent( m_data->inputItem, &event ); - } -} - -void QskInputContext::sendKey( int key ) const -{ - if ( m_data->inputItem == nullptr ) - return; - - QKeyEvent keyPress( QEvent::KeyPress, key, Qt::NoModifier ); - QCoreApplication::sendEvent( m_data->inputItem, &keyPress ); - - QKeyEvent keyRelease( QEvent::KeyRelease, key, Qt::NoModifier ); - QCoreApplication::sendEvent( m_data->inputItem, &keyRelease ); -} - #include "moc_QskInputContext.cpp" diff --git a/src/inputpanel/QskInputContext.h b/src/inputpanel/QskInputContext.h index c00644f2..663961c3 100644 --- a/src/inputpanel/QskInputContext.h +++ b/src/inputpanel/QskInputContext.h @@ -6,18 +6,14 @@ #ifndef QSK_INPUT_CONTEXT_H #define QSK_INPUT_CONTEXT_H +#include "QskGlobal.h" #include #include -class QskInputCompositionModel; - +class QskTextPredictor; class QQuickItem; -class QInputMethodQueryEvent; -class QInputMethodEvent; -class QKeyEvent; - -class QskInputContext : public QPlatformInputContext +class QSK_EXPORT QskInputContext : public QPlatformInputContext { Q_OBJECT @@ -48,31 +44,23 @@ public: virtual QLocale locale() const override; virtual Qt::LayoutDirection inputDirection() const override; - void setCompositionModel( const QLocale&, QskInputCompositionModel* ); + void registerPredictor( const QLocale&, QskTextPredictor* ); + QskTextPredictor* registeredPredictor( const QLocale& ); Q_INVOKABLE QQuickItem* inputItem(); virtual bool filterEvent( const QEvent* ) override; - QInputMethodQueryEvent queryInputMethod( Qt::InputMethodQueries ) const; - - void sendKey( int key ) const; - void sendText( const QString& text, bool isFinal ) const; - - Qt::InputMethodHints inputHints() const; - int keysLeft() const; +protected: + virtual void updateInputPanel( QQuickItem* inputItem ); private Q_SLOTS: - void handleCandidatesChanged(); void setInputPanel( QQuickItem* ); virtual bool eventFilter( QObject*, QEvent* ) override; private: - void processKey( int key ); - void setInputItem( QQuickItem* ); - QskInputCompositionModel* compositionModel() const; class PrivateData; std::unique_ptr< PrivateData > m_data; diff --git a/src/inputpanel/QskInputEngine.cpp b/src/inputpanel/QskInputEngine.cpp new file mode 100644 index 00000000..ac79e398 --- /dev/null +++ b/src/inputpanel/QskInputEngine.cpp @@ -0,0 +1,246 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the QSkinny License, Version 1.0 + *****************************************************************************/ + +#include "QskInputEngine.h" +#include "QskTextPredictor.h" + +#include +#include +#include + +static inline QString qskKeyString( int keyCode ) +{ + // Special case entry codes here, else default to the symbol + switch ( keyCode ) + { + case Qt::Key_Shift: + case Qt::Key_CapsLock: + case Qt::Key_Mode_switch: + case Qt::Key_Backspace: + case Qt::Key_Muhenkan: + return QString(); + + case Qt::Key_Return: + case Qt::Key_Kanji: + return QChar( QChar::CarriageReturn ); + + case Qt::Key_Space: + return QChar( QChar::Space ); + + default: + break; + } + + return QChar( keyCode ); +} + +class QskInputEngine::PrivateData +{ +public: + QPointer< QskTextPredictor > predictor; + QString preedit; +}; + +QskInputEngine::QskInputEngine( QObject* parent ): + QObject( parent ), + m_data( new PrivateData() ) +{ +} + +QskInputEngine::~QskInputEngine() +{ +} + +void QskInputEngine::setPredictor( QskTextPredictor* predictor ) +{ + if ( predictor == m_data->predictor ) + return; + + reset(); + + if ( m_data->predictor ) + { + if ( m_data->predictor->parent() == this ) + { + delete m_data->predictor; + } + else + { + m_data->predictor->disconnect( this ); + m_data->predictor = nullptr; + } + } + + if ( predictor ) + { + if ( predictor->parent() == nullptr ) + predictor->setParent( this ); + + connect( predictor, &QskTextPredictor::predictionChanged, + this, &QskInputEngine::predictionChanged ); + } + + m_data->predictor = predictor; +} + +QskTextPredictor* QskInputEngine::predictor() const +{ + return m_data->predictor; +} + +QVector< QString > QskInputEngine::prediction() const +{ + QVector< QString > candidates; + + if ( const auto predictor = m_data->predictor ) + { + const auto count = predictor->candidateCount(); + candidates.reserve( count ); + + for( int i = 0; i < count; i++ ) + candidates += predictor->candidate( i ); + } + + return candidates; +} + +QskInputEngine::Result QskInputEngine::processKey( int key, + Qt::InputMethodHints inputHints, int spaceLeft ) +{ + QskInputEngine::Result result; + + auto& preedit = m_data->preedit; + + QskTextPredictor* predictor = nullptr; + if ( !( inputHints & Qt::ImhHiddenText ) ) + predictor = m_data->predictor; + + /* + First we have to handle the control keys + */ + switch ( key ) + { + case Qt::Key_Backspace: + case Qt::Key_Muhenkan: + { + if ( predictor ) + { + if ( !preedit.isEmpty() ) + { + preedit.chop( 1 ); + + result.text = preedit; + result.isFinal = false; + + predictor->request( preedit ); + + return result; + } + } + + result.key = Qt::Key_Backspace; + return result; + } + case Qt::Key_Return: + { + if ( predictor ) + { + if ( !preedit.isEmpty() ) + { + if ( spaceLeft ) + { + result.text = preedit.left( spaceLeft ); + result.isFinal = true; + } + + reset(); + + return result; + } + } + + if( !( inputHints & Qt::ImhMultiLine ) ) + { + result.key = Qt::Key_Return; + return result; + } + + break; + } + case Qt::Key_Space: + { + if ( predictor ) + { + if ( !preedit.isEmpty() && spaceLeft) + { + preedit = preedit.left( spaceLeft ); + + result.text = preedit; + result.isFinal = true; + + reset(); + + return result; + } + } + + break; + } + case Qt::Key_Left: + case Qt::Key_Right: + case Qt::Key_Escape: + { + result.key = key; + return result; + } + } + + const QString text = qskKeyString( key ); + + if ( predictor ) + { + preedit += text; + + predictor->request( preedit ); + + if ( predictor->candidateCount() > 0 ) + { + result.text = preedit; + result.isFinal = false; + } + else + { + result.text = preedit.left( spaceLeft ); + result.isFinal = true; + + preedit.clear(); + } + } + else + { + result.text = text; + result.isFinal = true; + } + + return result; +} + +QString QskInputEngine::predictiveText( int index ) const +{ + if ( m_data->predictor ) + return m_data->predictor->candidate( index ); + + return QString(); +} + +void QskInputEngine::reset() +{ + if ( m_data->predictor ) + m_data->predictor->reset(); + + m_data->preedit.clear(); +} + +#include "moc_QskInputEngine.cpp" diff --git a/src/inputpanel/QskInputEngine.h b/src/inputpanel/QskInputEngine.h new file mode 100644 index 00000000..1c468811 --- /dev/null +++ b/src/inputpanel/QskInputEngine.h @@ -0,0 +1,54 @@ +/****************************************************************************** + * QSkinny - Copyright (C) 2016 Uwe Rathmann + * This file may be used under the terms of the QSkinny License, Version 1.0 + *****************************************************************************/ + +#ifndef QSK_INPUT_ENGINE_H +#define QSK_INPUT_ENGINE_H + +#include "QskGlobal.h" +#include +#include + +class QskTextPredictor; +class QQuickItem; + +class QSK_EXPORT QskInputEngine : public QObject +{ + Q_OBJECT + + using Inherited = QObject; + +public: + class Result + { + public: + int key = 0; + + QString text; + bool isFinal = true; + }; + + QskInputEngine( QObject* parent = nullptr ); + virtual ~QskInputEngine(); + + void setPredictor( QskTextPredictor* ); + QskTextPredictor* predictor() const; + + virtual Result processKey( int key, + Qt::InputMethodHints, int spaceLeft = -1 ); + + QString predictiveText( int ) const; + QVector< QString > prediction() const; + + void reset(); + +Q_SIGNALS: + void predictionChanged(); + +private: + class PrivateData; + std::unique_ptr< PrivateData > m_data; +}; + +#endif diff --git a/src/inputpanel/QskInputPanel.cpp b/src/inputpanel/QskInputPanel.cpp index c167c576..442d51f3 100644 --- a/src/inputpanel/QskInputPanel.cpp +++ b/src/inputpanel/QskInputPanel.cpp @@ -4,8 +4,9 @@ *****************************************************************************/ #include "QskInputPanel.h" +#include "QskInputEngine.h" #include "QskVirtualKeyboard.h" -#include "QskInputSuggestionBar.h" +#include "QskInputPredictionBar.h" #include "QskTextInput.h" #include "QskTextLabel.h" #include "QskLinearBox.h" @@ -13,92 +14,47 @@ #include #include #include +#include +#include +#include -QString qskNativeLocaleString( const QLocale& locale ) +static inline void qskSendText( QQuickItem* inputItem, + const QString& text, bool isFinal ) { - switch( locale.language() ) + if ( inputItem == nullptr ) + return; + + if ( isFinal ) { - case QLocale::Bulgarian: - return QStringLiteral( "български език" ); + QInputMethodEvent event; + event.setCommitString( text ); - case QLocale::Czech: - return QStringLiteral( "Čeština" ); - - case QLocale::German: - return QStringLiteral( "Deutsch" ); - - case QLocale::Danish: - return QStringLiteral( "Dansk" ); - - case QLocale::Greek: - return QStringLiteral( "Eλληνικά" ); - - case QLocale::English: - { - switch( locale.country() ) - { - case QLocale::Canada: - case QLocale::UnitedStates: - case QLocale::UnitedStatesMinorOutlyingIslands: - case QLocale::UnitedStatesVirginIslands: - return QStringLiteral( "English (US)" ); - - default: - return QStringLiteral( "English (UK)" ); - } - } - - case QLocale::Spanish: - return QStringLiteral( "Español" ); - - case QLocale::Finnish: - return QStringLiteral( "Suomi" ); - - case QLocale::French: - return QStringLiteral( "Français" ); - - case QLocale::Hungarian: - return QStringLiteral( "Magyar" ); - - case QLocale::Italian: - return QStringLiteral( "Italiano" ); - - case QLocale::Japanese: - return QStringLiteral( "日本語" ); - - case QLocale::Latvian: - return QStringLiteral( "Latviešu" ); - - case QLocale::Lithuanian: - return QStringLiteral( "Lietuvių" ); - - case QLocale::Dutch: - return QStringLiteral( "Nederlands" ); - - case QLocale::Portuguese: - return QStringLiteral( "Português" ); - - case QLocale::Romanian: - return QStringLiteral( "Română" ); - - case QLocale::Russia: - return QStringLiteral( "Русский" ); - - case QLocale::Slovenian: - return QStringLiteral( "Slovenščina" ); - - case QLocale::Slovak: - return QStringLiteral( "Slovenčina" ); - - case QLocale::Turkish: - return QStringLiteral( "Türkçe" ); - - case QLocale::Chinese: - return QStringLiteral( "中文" ); - - default: - return QLocale::languageToString( locale.language() ); + QCoreApplication::sendEvent( inputItem, &event ); } + else + { + QTextCharFormat format; + format.setFontUnderline( true ); + + const QInputMethodEvent::Attribute attribute( + QInputMethodEvent::TextFormat, 0, text.length(), format ); + + QInputMethodEvent event( text, { attribute } ); + + QCoreApplication::sendEvent( inputItem, &event ); + } +} + +static inline void qskSendKey( QQuickItem* inputItem, int key ) +{ + if ( inputItem == nullptr ) + return; + + QKeyEvent keyPress( QEvent::KeyPress, key, Qt::NoModifier ); + QCoreApplication::sendEvent( inputItem, &keyPress ); + + QKeyEvent keyRelease( QEvent::KeyRelease, key, Qt::NoModifier ); + QCoreApplication::sendEvent( inputItem, &keyRelease ); } namespace @@ -123,10 +79,13 @@ public: { } + QPointer< QskInputEngine > engine; + QPointer< QQuickItem > inputItem; + QskLinearBox* layout; QskTextLabel* prompt; TextInput* inputProxy; - QskInputSuggestionBar* suggestionBar; + QskInputPredictionBar* predictionBar; QskVirtualKeyboard* keyboard; bool hasInputProxy : 1; @@ -145,8 +104,8 @@ QskInputPanel::QskInputPanel( QQuickItem* parent ): m_data->inputProxy = new TextInput(); m_data->inputProxy->setVisible( m_data->hasInputProxy ); - m_data->suggestionBar = new QskInputSuggestionBar(); - m_data->suggestionBar->setVisible( false ); + m_data->predictionBar = new QskInputPredictionBar(); + m_data->predictionBar->setVisible( false ); m_data->keyboard = new QskVirtualKeyboard(); @@ -155,13 +114,13 @@ QskInputPanel::QskInputPanel( QQuickItem* parent ): layout->addItem( m_data->prompt, Qt::AlignLeft | Qt::AlignHCenter ); layout->addItem( m_data->inputProxy, Qt::AlignLeft | Qt::AlignHCenter ); layout->addStretch( 10 ); - layout->addItem( m_data->suggestionBar ); + layout->addItem( m_data->predictionBar ); layout->addItem( m_data->keyboard ); m_data->layout = layout; - connect( m_data->suggestionBar, &QskInputSuggestionBar::suggested, - this, &QskInputPanel::commitCandidate ); + connect( m_data->predictionBar, &QskInputPredictionBar::predictiveTextSelected, + this, &QskInputPanel::commitPredictiveText ); connect( m_data->keyboard, &QskVirtualKeyboard::keySelected, this, &QskInputPanel::commitKey ); @@ -171,6 +130,60 @@ QskInputPanel::~QskInputPanel() { } +void QskInputPanel::setEngine( QskInputEngine* engine ) +{ + if ( engine == m_data->engine ) + return; + + if ( m_data->engine ) + m_data->engine->disconnect( this ); + + m_data->engine = engine; + + if ( engine ) + { + connect( engine, &QskInputEngine::predictionChanged, + this, &QskInputPanel::updatePredictionBar ); + } + + m_data->predictionBar->setVisible( engine && engine->predictor() ); +} + +void QskInputPanel::attachInputItem( QQuickItem* item ) +{ + if ( item == m_data->inputItem ) + return; + + m_data->inputItem = item; + + if ( m_data->engine ) + m_data->engine->reset(); + + if ( item ) + { + Qt::InputMethodQueries queries = Qt::ImQueryAll; + queries &= ~Qt::ImEnabled; + + processInputMethodQueries( queries ); + } +} + +QQuickItem* QskInputPanel::attachedInputItem() const +{ + return m_data->inputItem; +} + +void QskInputPanel::updatePredictionBar() +{ + m_data->predictionBar->setPrediction( + m_data->engine->prediction() ); +} + +QskInputEngine* QskInputPanel::engine() +{ + return m_data->engine; +} + QskAspect::Subcontrol QskInputPanel::effectiveSubcontrol( QskAspect::Subcontrol subControl ) const { @@ -180,73 +193,6 @@ QskAspect::Subcontrol QskInputPanel::effectiveSubcontrol( return subControl; } -qreal QskInputPanel::heightForWidth( qreal width ) const -{ - /* - This code looks like as it could be generalized - and moved to QskLinearBox. TODO ... - */ - - const auto margins = this->margins(); - - width -= margins.left() + margins.right(); - - const auto padding = innerPadding( - Panel, QSizeF( width, width ) ); - - width -= padding.left() + padding.right(); - - qreal height = m_data->keyboard->heightForWidth( width ); - - const QskControl* controls[] = - { m_data->prompt, m_data->inputProxy, m_data->suggestionBar }; - - for ( auto control : controls ) - { - if ( control->isVisible() ) - { - height += m_data->layout->spacing(); - height += control->sizeHint().height(); - } - } - - height += padding.top() + padding.bottom(); - height += margins.top() + margins.bottom(); - - return height; -} - -qreal QskInputPanel::widthForHeight( qreal height ) const -{ - const auto margins = this->margins(); - - height -= margins.top() + margins.bottom(); - - const auto padding = innerPadding( - Panel, QSizeF( height, height ) ); - - height -= padding.top() + padding.bottom(); - - const QskControl* controls[] = - { m_data->prompt, m_data->inputProxy, m_data->suggestionBar }; - - for ( auto control : controls ) - { - if ( control->isVisible() ) - { - height -= m_data->layout->spacing(); - height -= control->sizeHint().height(); - } - } - - qreal width = m_data->keyboard->widthForHeight( height ); - - width += padding.left() + padding.right(); - width += margins.left() + margins.right(); - - return width; -} - QString QskInputPanel::inputPrompt() const { return m_data->prompt->text(); @@ -338,38 +284,145 @@ void QskInputPanel::updateInputProxy( const QQuickItem* inputItem ) } } -bool QskInputPanel::isCandidatesEnabled() const +void QskInputPanel::commitPredictiveText( int index ) { - return m_data->suggestionBar->isVisible(); -} + m_data->predictionBar->setPrediction( QVector< QString >() ); -QVector< QString > QskInputPanel::candidates() const -{ - return m_data->suggestionBar->candidates(); -} + if ( m_data->engine ) + { + const QString text = m_data->engine->predictiveText( index ); + m_data->engine->reset(); -void QskInputPanel::setCandidatesEnabled( bool on ) -{ - m_data->suggestionBar->setVisible( on ); -} - -void QskInputPanel::setCandidates( const QVector< QString >& candidates ) -{ - m_data->suggestionBar->setCandidates( candidates ); -} - -void QskInputPanel::commitCandidate( int index ) -{ - m_data->suggestionBar->setCandidates( QVector< QString >() ); - - QGuiApplication::inputMethod()->invokeAction( - static_cast< QInputMethod::Action >( SelectCandidate ), index ); + qskSendText( m_data->inputItem, text, true ); + } } void QskInputPanel::commitKey( int key ) { - QGuiApplication::inputMethod()->invokeAction( - static_cast< QInputMethod::Action >( Compose ), key ); + if ( m_data->engine == nullptr || m_data->inputItem == nullptr ) + return; + + auto engine = m_data->engine; + auto inputItem = m_data->inputItem; + + QInputMethodQueryEvent event( Qt::ImHints ); + QCoreApplication::sendEvent( inputItem, &event ); + + const auto inputHints = static_cast< Qt::InputMethodHints >( + event.value( Qt::ImHints ).toInt() ); + + int spaceLeft = -1; + + if ( !( inputHints & Qt::ImhMultiLine ) ) + { + QInputMethodQueryEvent event( + Qt::ImSurroundingText | Qt::ImMaximumTextLength ); + + QCoreApplication::sendEvent( inputItem, &event ); + + const int max = event.value( Qt::ImMaximumTextLength ).toInt(); + + if ( max > 0 ) + { + const auto text = event.value( Qt::ImSurroundingText ).toString(); + spaceLeft = max - text.length(); + } + } + + processKey( key, inputHints, spaceLeft ); +} + +void QskInputPanel::processKey( int key, + Qt::InputMethodHints inputHints, int spaceLeft ) +{ + const auto result = m_data->engine->processKey( key, inputHints, spaceLeft ); + + auto inputItem = m_data->inputItem; + + if ( result.key ) + { + // sending a control key + qskSendKey( inputItem, result.key ); + } + else if ( !result.text.isEmpty() ) + { + // changing the current text + qskSendText( inputItem, result.text, result.isFinal ); + } +} + +void QskInputPanel::processInputMethodQueries( Qt::InputMethodQueries queries ) +{ + if ( m_data->inputItem == nullptr ) + return; + + /* + adjust the input panel to information provided from the input item + */ + + QInputMethodQueryEvent queryEvent( queries ); + QCoreApplication::sendEvent( m_data->inputItem, &queryEvent ); + + if ( queryEvent.queries() & Qt::ImHints ) + { + /* + ImhHiddenText = 0x1, // might need to disable certain checks + ImhSensitiveData = 0x2, // shouldn't change anything + ImhNoAutoUppercase = 0x4, // if we support auto uppercase, disable it + ImhPreferNumbers = 0x8, // default to number keyboard + ImhPreferUppercase = 0x10, // start with shift on + ImhPreferLowercase = 0x20, // start with shift off + ImhNoPredictiveText = 0x40, // not use predictive text + + ImhDate = 0x80, // ignored for now (no date keyboard) + ImhTime = 0x100, // ignored for know (no time keyboard) + + ImhPreferLatin = 0x200, // can be used to launch chinese kb in english mode + + ImhMultiLine = 0x400, // not useful? + + ImhDigitsOnly // default to number keyboard, disable other keys + ImhFormattedNumbersOnly // hard to say + ImhUppercaseOnly // caps-lock, disable shift + ImhLowercaseOnly // disable shift + ImhDialableCharactersOnly // dial pad (calculator?) + ImhEmailCharactersOnly // disable certain symbols (email-only kb?) + ImhUrlCharactersOnly // disable certain symbols (url-only kb?) + ImhLatinOnly // disable chinese input + */ + +#if 0 + const auto hints = static_cast< Qt::InputMethodHints >( + queryEvent.value( Qt::ImHints ).toInt() ); + +#endif + } + +#if 0 + if ( queryEvent.queries() & Qt::ImPreferredLanguage ) + { + } +#endif + + /* + Qt::ImMicroFocus + Qt::ImCursorRectangle + Qt::ImFont + Qt::ImCursorPosition + Qt::ImSurroundingText // important for chinese input + Qt::ImCurrentSelection // important for prediction + Qt::ImMaximumTextLength // should be monitored + Qt::ImAnchorPosition + + Qt::ImAbsolutePosition + Qt::ImTextBeforeCursor // important for chinese + Qt::ImTextAfterCursor // important for chinese + Qt::ImPlatformData // hard to say... + Qt::ImEnterKeyType + Qt::ImAnchorRectangle + Qt::ImInputItemClipRectangle // could be used for the geometry of the panel + */ + } void QskInputPanel::keyPressEvent( QKeyEvent* event ) @@ -401,4 +454,71 @@ void QskInputPanel::keyReleaseEvent( QKeyEvent* event ) return Inherited::keyReleaseEvent( event ); } +qreal QskInputPanel::heightForWidth( qreal width ) const +{ + /* + This code looks like as it could be generalized + and moved to QskLinearBox. TODO ... + */ + + const auto margins = this->margins(); + + width -= margins.left() + margins.right(); + + const auto padding = innerPadding( + Panel, QSizeF( width, width ) ); + + width -= padding.left() + padding.right(); + + qreal height = m_data->keyboard->heightForWidth( width ); + + const QskControl* controls[] = + { m_data->prompt, m_data->inputProxy, m_data->predictionBar }; + + for ( auto control : controls ) + { + if ( control->isVisible() ) + { + height += m_data->layout->spacing(); + height += control->sizeHint().height(); + } + } + + height += padding.top() + padding.bottom(); + height += margins.top() + margins.bottom(); + + return height; +} + +qreal QskInputPanel::widthForHeight( qreal height ) const +{ + const auto margins = this->margins(); + + height -= margins.top() + margins.bottom(); + + const auto padding = innerPadding( + Panel, QSizeF( height, height ) ); + + height -= padding.top() + padding.bottom(); + + const QskControl* controls[] = + { m_data->prompt, m_data->inputProxy, m_data->predictionBar }; + + for ( auto control : controls ) + { + if ( control->isVisible() ) + { + height -= m_data->layout->spacing(); + height -= control->sizeHint().height(); + } + } + + qreal width = m_data->keyboard->widthForHeight( height ); + + width += padding.left() + padding.right(); + width += margins.left() + margins.right(); + + return width; +} + #include "moc_QskInputPanel.cpp" diff --git a/src/inputpanel/QskInputPanel.h b/src/inputpanel/QskInputPanel.h index 1c9a4b50..3c005022 100644 --- a/src/inputpanel/QskInputPanel.h +++ b/src/inputpanel/QskInputPanel.h @@ -9,6 +9,8 @@ #include "QskGlobal.h" #include "QskBox.h" +class QskInputEngine; + class QString; class QLocale; @@ -26,26 +28,21 @@ class QSK_EXPORT QskInputPanel: public QskBox Q_PROPERTY( QString inputPrompt READ inputPrompt WRITE setInputPrompt NOTIFY inputPromptChanged ) - public: QSK_SUBCONTROLS( Panel ) - enum Action - { - Compose = 0x10, - SelectCandidate = 0x11 - }; - Q_ENUM( Action ) - QskInputPanel( QQuickItem* parent = nullptr ); virtual ~QskInputPanel() override; + void attachInputItem( QQuickItem* ); + QQuickItem* attachedInputItem() const; + + void setEngine( QskInputEngine* ); + QskInputEngine* engine(); + bool hasInputProxy() const; QString inputPrompt() const; - bool isCandidatesEnabled() const; - QVector< QString > candidates() const; - virtual qreal heightForWidth( qreal width ) const override; virtual qreal widthForHeight( qreal height ) const override; @@ -53,6 +50,7 @@ public: QskAspect::Subcontrol ) const override; void updateInputProxy( const QQuickItem* ); + virtual void processInputMethodQueries( Qt::InputMethodQueries ); Q_SIGNALS: void inputProxyChanged( bool ); @@ -61,21 +59,21 @@ Q_SIGNALS: public Q_SLOTS: void setInputPrompt( const QString& ); void setInputProxy( bool ); - void setCandidatesEnabled( bool ); - void setCandidates( const QVector< QString >& ); protected: virtual void keyPressEvent( QKeyEvent* ) override; virtual void keyReleaseEvent( QKeyEvent* ) override; + virtual void processKey( int key, + Qt::InputMethodHints, int spaceLeft ); + private: + void updatePredictionBar(); void commitKey( int key ); - void commitCandidate( int ); + void commitPredictiveText( int ); class PrivateData; std::unique_ptr< PrivateData > m_data; }; -QSK_EXPORT QString qskNativeLocaleString( const QLocale& ); - #endif diff --git a/src/inputpanel/QskInputSuggestionBar.cpp b/src/inputpanel/QskInputPredictionBar.cpp similarity index 72% rename from src/inputpanel/QskInputSuggestionBar.cpp rename to src/inputpanel/QskInputPredictionBar.cpp index 9bba6967..da12e9f2 100644 --- a/src/inputpanel/QskInputSuggestionBar.cpp +++ b/src/inputpanel/QskInputPredictionBar.cpp @@ -3,7 +3,7 @@ * This file may be used under the terms of the QSkinny License, Version 1.0 *****************************************************************************/ -#include "QskInputSuggestionBar.h" +#include "QskInputPredictionBar.h" #include "QskPushButton.h" #include "QskLinearBox.h" #include "QskTextOptions.h" @@ -11,9 +11,9 @@ #include #include -QSK_SUBCONTROL( QskInputSuggestionBar, Panel ) -QSK_SUBCONTROL( QskInputSuggestionBar, ButtonPanel ) -QSK_SUBCONTROL( QskInputSuggestionBar, ButtonText ) +QSK_SUBCONTROL( QskInputPredictionBar, Panel ) +QSK_SUBCONTROL( QskInputPredictionBar, ButtonPanel ) +QSK_SUBCONTROL( QskInputPredictionBar, ButtonText ) namespace { @@ -46,27 +46,27 @@ namespace QskAspect::Subcontrol subControl ) const override final { if( subControl == QskPushButton::Panel ) - return QskInputSuggestionBar::ButtonPanel; + return QskInputPredictionBar::ButtonPanel; if( subControl == QskPushButton::Text ) - return QskInputSuggestionBar::ButtonText; + return QskInputPredictionBar::ButtonText; return subControl; } }; } -class QskInputSuggestionBar::PrivateData +class QskInputPredictionBar::PrivateData { public: QskLinearBox* layoutBox; QVector< QString > candidates; - int candidateOffset = 0; + int scrollOffset = 0; const int buttonCount = 12; }; -QskInputSuggestionBar::QskInputSuggestionBar( QQuickItem* parent ): +QskInputPredictionBar::QskInputPredictionBar( QQuickItem* parent ): Inherited( parent ), m_data( new PrivateData ) { @@ -82,7 +82,7 @@ QskInputSuggestionBar::QskInputSuggestionBar( QQuickItem* parent ): button->setSizePolicy( Qt::Horizontal, QskSizePolicy::Maximum ); connect( button, &QskPushButton::clicked, - this, &QskInputSuggestionBar::candidateClicked ); + this, &QskInputPredictionBar::buttonClicked ); if ( i == 0 ) { @@ -90,44 +90,43 @@ QskInputSuggestionBar::QskInputSuggestionBar( QQuickItem* parent ): m_data->layoutBox->setRetainSizeWhenHidden( button, true ); } } - } -QskInputSuggestionBar::~QskInputSuggestionBar() +QskInputPredictionBar::~QskInputPredictionBar() { } -QskAspect::Subcontrol QskInputSuggestionBar::effectiveSubcontrol( +QskAspect::Subcontrol QskInputPredictionBar::effectiveSubcontrol( QskAspect::Subcontrol subControl ) const { if( subControl == QskBox::Panel ) - return QskInputSuggestionBar::Panel; + return QskInputPredictionBar::Panel; return subControl; } -void QskInputSuggestionBar::setCandidates( const QVector< QString >& candidates ) +void QskInputPredictionBar::setPrediction( const QVector< QString >& candidates ) { if( m_data->candidates != candidates ) { m_data->candidates = candidates; - setCandidateOffset( 0 ); + setScrollOffset( 0 ); } } -QVector< QString > QskInputSuggestionBar::candidates() const +QVector< QString > QskInputPredictionBar::candidates() const { return m_data->candidates; } -void QskInputSuggestionBar::setCandidateOffset( int offset ) +void QskInputPredictionBar::setScrollOffset( int offset ) { - m_data->candidateOffset = offset; + m_data->scrollOffset = offset; const auto candidateCount = m_data->candidates.length(); const auto count = std::min( candidateCount, m_data->buttonCount ); - const bool continueLeft = m_data->candidateOffset > 0; - const bool continueRight = ( candidateCount - m_data->candidateOffset ) > count; + const bool continueLeft = m_data->scrollOffset > 0; + const bool continueRight = ( candidateCount - m_data->scrollOffset ) > count; for( int i = 0; i < count; i++ ) { @@ -144,7 +143,7 @@ void QskInputSuggestionBar::setCandidateOffset( int offset ) } else { - const int index = i + m_data->candidateOffset; + const int index = i + m_data->scrollOffset; button->setText( m_data->candidates[index] ); } @@ -155,18 +154,18 @@ void QskInputSuggestionBar::setCandidateOffset( int offset ) m_data->layoutBox->itemAtIndex( i )->setVisible( false ); } -void QskInputSuggestionBar::candidateClicked() +void QskInputPredictionBar::buttonClicked() { const int index = m_data->layoutBox->indexOf( qobject_cast< QQuickItem* > ( sender() ) ); - const int offset = m_data->candidateOffset; + const int offset = m_data->scrollOffset; if ( index == 0 ) { if ( offset > 0 ) { - setCandidateOffset( offset - 1 ); + setScrollOffset( offset - 1 ); return; } } @@ -174,12 +173,12 @@ void QskInputSuggestionBar::candidateClicked() { if ( m_data->candidates.count() - offset > m_data->buttonCount ) { - setCandidateOffset( offset + 1 ); + setScrollOffset( offset + 1 ); return; } } - Q_EMIT suggested( offset + index ); + Q_EMIT predictiveTextSelected( offset + index ); } -#include "moc_QskInputSuggestionBar.cpp" +#include "moc_QskInputPredictionBar.cpp" diff --git a/src/inputpanel/QskInputSuggestionBar.h b/src/inputpanel/QskInputPredictionBar.h similarity index 64% rename from src/inputpanel/QskInputSuggestionBar.h rename to src/inputpanel/QskInputPredictionBar.h index ac678906..3b1a45fd 100644 --- a/src/inputpanel/QskInputSuggestionBar.h +++ b/src/inputpanel/QskInputPredictionBar.h @@ -3,12 +3,12 @@ * This file may be used under the terms of the QSkinny License, Version 1.0 *****************************************************************************/ -#ifndef QSK_INPUT_SUGGESTION_BAR_H -#define QSK_INPUT_SUGGESTION_BAR_H +#ifndef QSK_INPUT_PREDICTION_BAR_H +#define QSK_INPUT_PREDICTION_BAR_H #include "QskBox.h" -class QSK_EXPORT QskInputSuggestionBar : public QskBox +class QSK_EXPORT QskInputPredictionBar : public QskBox { Q_OBJECT @@ -17,8 +17,8 @@ class QSK_EXPORT QskInputSuggestionBar : public QskBox public: QSK_SUBCONTROLS( Panel, ButtonPanel, ButtonText ) - QskInputSuggestionBar( QQuickItem* parent = nullptr ); - virtual ~QskInputSuggestionBar(); + QskInputPredictionBar( QQuickItem* parent = nullptr ); + virtual ~QskInputPredictionBar(); virtual QskAspect::Subcontrol effectiveSubcontrol( QskAspect::Subcontrol subControl ) const override; @@ -26,14 +26,14 @@ public: QVector< QString > candidates() const; Q_SIGNALS: - void suggested( int ); + void predictiveTextSelected( int ); public Q_SLOTS: - void setCandidates( const QVector< QString >& ); + void setPrediction( const QVector< QString >& ); private: - void candidateClicked(); - void setCandidateOffset( int ); + void buttonClicked(); + void setScrollOffset( int ); class PrivateData; std::unique_ptr< PrivateData > m_data; diff --git a/src/inputpanel/QskInputCompositionModel.cpp b/src/inputpanel/QskTextPredictor.cpp similarity index 61% rename from src/inputpanel/QskInputCompositionModel.cpp rename to src/inputpanel/QskTextPredictor.cpp index 90879e85..6ff308c3 100644 --- a/src/inputpanel/QskInputCompositionModel.cpp +++ b/src/inputpanel/QskTextPredictor.cpp @@ -3,22 +3,22 @@ * This file may be used under the terms of the QSkinny License, Version 1.0 *****************************************************************************/ -#include "QskInputCompositionModel.h" +#include "QskTextPredictor.h" -QskInputCompositionModel::QskInputCompositionModel( +QskTextPredictor::QskTextPredictor( Attributes attributes, QObject* parent ): QObject( parent ), m_attributes( attributes ) { } -QskInputCompositionModel::~QskInputCompositionModel() +QskTextPredictor::~QskTextPredictor() { } -QskInputCompositionModel::Attributes QskInputCompositionModel::attributes() const +QskTextPredictor::Attributes QskTextPredictor::attributes() const { return m_attributes; } -#include "moc_QskInputCompositionModel.cpp" +#include "moc_QskTextPredictor.cpp" diff --git a/src/inputpanel/QskInputCompositionModel.h b/src/inputpanel/QskTextPredictor.h similarity index 62% rename from src/inputpanel/QskInputCompositionModel.h rename to src/inputpanel/QskTextPredictor.h index 7d9e9930..0b9552b6 100644 --- a/src/inputpanel/QskInputCompositionModel.h +++ b/src/inputpanel/QskTextPredictor.h @@ -3,12 +3,15 @@ * This file may be used under the terms of the QSkinny License, Version 1.0 *****************************************************************************/ -#ifndef QSK_INPUT_COMPOSITION_MODEL_H -#define QSK_INPUT_COMPOSITION_MODEL_H +#ifndef QSK_TEXT_PREDICTOR_H +#define QSK_TEXT_PREDICTOR_H +#include #include -class QskInputCompositionModel : public QObject +// abstract base class for input methods for retrieving predictive text + +class QSK_EXPORT QskTextPredictor : public QObject { Q_OBJECT @@ -21,10 +24,10 @@ public: Q_ENUM( Attribute ) Q_DECLARE_FLAGS( Attributes, Attribute ) - virtual ~QskInputCompositionModel(); + virtual ~QskTextPredictor(); - virtual void requestCandidates( const QString& preedit ) = 0; - virtual void resetCandidates() = 0; + virtual void request( const QString& text ) = 0; + virtual void reset() = 0; virtual int candidateCount() const = 0; virtual QString candidate( int ) const = 0; @@ -32,10 +35,10 @@ public: Attributes attributes() const; Q_SIGNALS: - void candidatesChanged(); + void predictionChanged(); protected: - QskInputCompositionModel( Attributes, QObject* ); + QskTextPredictor( Attributes, QObject* ); private: const Attributes m_attributes; diff --git a/src/src.pro b/src/src.pro index d734dcc7..174dc273 100644 --- a/src/src.pro +++ b/src/src.pro @@ -298,15 +298,17 @@ SOURCES += \ dialogs/QskSelectionWindow.cpp SOURCES += \ - inputpanel/QskInputCompositionModel.cpp \ + inputpanel/QskTextPredictor.cpp \ inputpanel/QskInputContext.cpp \ + inputpanel/QskInputEngine.cpp \ inputpanel/QskInputPanel.cpp \ - inputpanel/QskInputSuggestionBar.cpp \ + inputpanel/QskInputPredictionBar.cpp \ inputpanel/QskVirtualKeyboard.cpp HEADERS += \ - inputpanel/QskInputCompositionModel.h \ + inputpanel/QskTextPredictor.h \ inputpanel/QskInputContext.h \ + inputpanel/QskInputEngine.h \ inputpanel/QskInputPanel.h \ - inputpanel/QskInputSuggestionBar.h \ + inputpanel/QskInputPredictionBar.h \ inputpanel/QskVirtualKeyboard.h