QskInputContext improved

This commit is contained in:
Uwe Rathmann 2018-04-20 08:52:26 +02:00
parent 48c897f825
commit 151a73cb0b
8 changed files with 430 additions and 429 deletions

View File

@ -11,7 +11,7 @@ public:
}; };
QskHunspellCompositionModel::QskHunspellCompositionModel( QskInputContext* context ): QskHunspellCompositionModel::QskHunspellCompositionModel( QskInputContext* context ):
Inherited( context ), Inherited( Words, context ),
m_data( new PrivateData() ) m_data( new PrivateData() )
{ {
#if 1 #if 1
@ -28,21 +28,6 @@ QskHunspellCompositionModel::~QskHunspellCompositionModel()
Hunspell_destroy( m_data->hunspellHandle ); Hunspell_destroy( m_data->hunspellHandle );
} }
bool QskHunspellCompositionModel::supportsSuggestions() const
{
return true;
}
void QskHunspellCompositionModel::commitCandidate( int index )
{
if( index < m_data->candidates.count() )
{
// The user usually selects a full word, so we can add the space
QString commitString = candidate( index ) + " ";
commit( commitString );
}
}
int QskHunspellCompositionModel::candidateCount() const int QskHunspellCompositionModel::candidateCount() const
{ {
return m_data->candidates.count(); return m_data->candidates.count();
@ -53,47 +38,38 @@ QString QskHunspellCompositionModel::candidate( int pos ) const
return m_data->candidates[ pos ]; return m_data->candidates[ pos ];
} }
QString QskHunspellCompositionModel::polishPreedit( const QString& preedit ) void QskHunspellCompositionModel::resetCandidates()
{ {
if( preedit.isEmpty() ) if ( !m_data->candidates.isEmpty() )
{ {
// new word: delete suggestions
m_data->candidates.clear(); m_data->candidates.clear();
Q_EMIT candidatesChanged();
} }
else
{
char** suggestions;
QByteArray word = preedit.toUtf8(); // ### do we need to check the encoding (see qtvirtualkeyboard)?
int suggestionCount = Hunspell_suggest(
m_data->hunspellHandle, &suggestions, word.constData() );
QVector< QString > candidates;
candidates.reserve( suggestionCount );
for( int a = 0; a < suggestionCount; ++a )
{
const QString suggestion = QString::fromUtf8( suggestions[a] ); // ### encoding?
if( suggestion.startsWith( preedit ) )
{
candidates.prepend( suggestion );
}
else
{
candidates.append( suggestion );
}
}
Hunspell_free_list( m_data->hunspellHandle, &suggestions, suggestionCount );
m_data->candidates = candidates;
}
Q_EMIT candidatesChanged();
return preedit;
} }
bool QskHunspellCompositionModel::hasIntermediate() const void QskHunspellCompositionModel::requestCandidates( const QString& text )
{ {
return true; char** suggestions;
const QByteArray word = text.toUtf8(); // ### do we need to check the encoding
const int count = Hunspell_suggest(
m_data->hunspellHandle, &suggestions, word.constData() );
QVector< QString > candidates;
candidates.reserve( count );
for( int i = 0; i < count; i++ )
{
const QString suggestion = QString::fromUtf8( suggestions[i] ); // ### encoding?
if( suggestion.startsWith( text ) )
candidates.prepend( suggestion );
else
candidates.append( suggestion );
}
Hunspell_free_list( m_data->hunspellHandle, &suggestions, count );
m_data->candidates = candidates;
Q_EMIT candidatesChanged();
} }

View File

@ -7,6 +7,7 @@
#define QSK_HUNSPELL_COMPOSITION_MODEL_H #define QSK_HUNSPELL_COMPOSITION_MODEL_H
#include "QskInputCompositionModel.h" #include "QskInputCompositionModel.h"
#include <memory>
class QskHunspellCompositionModel : public QskInputCompositionModel class QskHunspellCompositionModel : public QskInputCompositionModel
{ {
@ -16,15 +17,12 @@ public:
QskHunspellCompositionModel( QskInputContext* context ); QskHunspellCompositionModel( QskInputContext* context );
virtual ~QskHunspellCompositionModel() override; virtual ~QskHunspellCompositionModel() override;
virtual bool supportsSuggestions() const override final;
virtual void commitCandidate( int index ) override;
virtual int candidateCount() const override; virtual int candidateCount() const override;
virtual QString candidate( int pos ) const override; virtual QString candidate( int pos ) const override;
protected: protected:
virtual bool hasIntermediate() const override; virtual void requestCandidates( const QString& ) override;
virtual QString polishPreedit( const QString& ) override; virtual void resetCandidates() override;
private: private:
class PrivateData; class PrivateData;

View File

@ -6,45 +6,12 @@
#include "QskInputCompositionModel.h" #include "QskInputCompositionModel.h"
#include "QskInputContext.h" #include "QskInputContext.h"
#include <QGuiApplication> #include <QInputMethodQueryEvent>
#include <QInputMethodEvent>
#include <QTextCharFormat>
static inline QString qskKeyString( int code ) QskInputCompositionModel::QskInputCompositionModel(
{ Attributes attributes, QskInputContext* context ):
// Special case entry codes here, else default to the symbol
switch ( code )
{
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( code );
}
class QskInputCompositionModel::PrivateData
{
public:
QString preedit;
};
QskInputCompositionModel::QskInputCompositionModel( QskInputContext* context ):
QObject( context ), QObject( context ),
m_data( new PrivateData ) m_attributes( attributes )
{ {
} }
@ -57,211 +24,67 @@ QskInputContext* QskInputCompositionModel::context() const
return qobject_cast< QskInputContext* >( parent() ); return qobject_cast< QskInputContext* >( parent() );
} }
bool QskInputCompositionModel::supportsSuggestions() const void QskInputCompositionModel::composeKey( const QString& text, int spaceLeft )
{ {
return false; if ( candidateCount() > 0 )
}
void QskInputCompositionModel::composeKey( int key )
{
/*
* This operation might be expensive (e.g. for Hunspell) and
* should be done asynchronously to be able to run e.g. suggestions
* in a separate thread to not block the UI.
* TODO
*/
const auto queryEvent = context()->queryInputMethod(
Qt::ImSurroundingText | Qt::ImMaximumTextLength | Qt::ImHints );
const auto hints = static_cast< Qt::InputMethodHints >(
queryEvent.value( Qt::ImHints ).toInt() );
const int maxLength = queryEvent.value( Qt::ImMaximumTextLength ).toInt();
const int currentLength = queryEvent.value( Qt::ImSurroundingText ).toString().length();
int spaceLeft = -1;
if ( !( hints & Qt::ImhMultiLine ) && maxLength > 0 )
spaceLeft = maxLength - currentLength;
switch ( key )
{ {
case Qt::Key_Backspace: m_preedit += text;
case Qt::Key_Muhenkan:
{
if ( !m_data->preedit.isEmpty() )
{
m_data->preedit.chop( 1 );
sendPreeditTextEvent( polishPreedit( m_data->preedit ) );
}
else
{
// Backspace one character only if preedit was inactive
sendKeyEvents( Qt::Key_Backspace );
}
return;
}
case Qt::Key_Space:
{
if( !spaceLeft )
{
return;
}
if( !m_data->preedit.isEmpty() ) requestCandidates( m_preedit );
{ context()->sendText( m_preedit, false );
commit( m_data->preedit.left( spaceLeft ) );
}
commit( qskKeyString( key ) );
return;
}
case Qt::Key_Return:
{
if ( !spaceLeft )
return;
// Commit what is in the buffer
if( !m_data->preedit.isEmpty() )
{
commit( m_data->preedit.left( spaceLeft ) );
}
else if( hints & Qt::ImhMultiLine )
{
commit( qskKeyString( key ) );
}
else
{
sendKeyEvents( Qt::Key_Return );
}
return;
}
case Qt::Key_Left:
case Qt::Key_Right:
{
if ( m_data->preedit.isEmpty() )
sendKeyEvents( key );
return;
}
case Qt::Key_Escape:
{
sendKeyEvents( Qt::Key_Escape );
return;
}
default:
{
if ( !spaceLeft )
return;
}
}
if ( hints & Qt::ImhHiddenText )
{
commit( qskKeyString( key ) );
return; return;
} }
const auto firstCandidate = candidateCount() > 0 ? candidate( 0 ) : QString(); requestCandidates( m_preedit );
const auto oldPreedit = m_data->preedit;
m_data->preedit += qskKeyString( key ); QString txt;
auto displayPreedit = polishPreedit( m_data->preedit ); if ( candidateCount() == 0 )
// If there is no intermediate, decide between committing the first candidate and skipping
if ( !hasIntermediate() )
{ {
// Skip preedit phase if there are no candidates/intermediates txt = m_preedit.left( spaceLeft );
if ( firstCandidate.isEmpty() ) spaceLeft -= txt.length();
}
else
{
txt = candidate( 0 );
--spaceLeft;
}
context()->sendText( txt, true );
m_preedit.clear();
resetCandidates();
if ( spaceLeft )
{
m_preedit = text;
requestCandidates( m_preedit );
if ( candidateCount() > 0 )
{ {
commit( oldPreedit.left( spaceLeft ) ); context()->sendText( m_preedit, false );
spaceLeft -= oldPreedit.leftRef( spaceLeft ).length();
} }
else else
{ {
commit( firstCandidate ); context()->sendText( m_preedit, true );
--spaceLeft; m_preedit.clear();
} resetCandidates();
if ( !spaceLeft )
return;
m_data->preedit = qskKeyString( key );
displayPreedit = polishPreedit( m_data->preedit );
if ( !hasIntermediate() )
{
commit( m_data->preedit );
return;
} }
} }
sendPreeditTextEvent( displayPreedit );
} }
void QskInputCompositionModel::clearPreedit() void QskInputCompositionModel::setPreeditText( const QString& text )
{ {
m_data->preedit.clear(); if ( text != m_preedit )
polishPreedit( m_data->preedit ); {
m_preedit = text;
requestCandidates( m_preedit );
}
} }
int QskInputCompositionModel::candidateCount() const void QskInputCompositionModel::reset()
{ {
return 0; m_preedit.clear();
} resetCandidates();
QString QskInputCompositionModel::candidate( int ) const
{
return QString();
}
QString QskInputCompositionModel::polishPreedit( const QString& preedit )
{
return preedit;
}
void QskInputCompositionModel::commit( const QString& text )
{
QInputMethodEvent event;
event.setCommitString( text );
context()->sendEventToInputItem( &event );
clearPreedit();
}
void QskInputCompositionModel::commitCandidate( int index )
{
commit( candidate( index ) );
}
void QskInputCompositionModel::sendPreeditTextEvent( const QString& text )
{
QTextCharFormat format;
format.setFontUnderline( true );
const QInputMethodEvent::Attribute attribute(
QInputMethodEvent::TextFormat, 0, text.length(), format );
QInputMethodEvent event( text, { attribute } );
context()->sendEventToInputItem( &event );
}
void QskInputCompositionModel::sendKeyEvents( int key )
{
auto context = this->context();
QKeyEvent keyPress( QEvent::KeyPress, key, Qt::NoModifier );
context->sendEventToInputItem( &keyPress );
QKeyEvent keyRelease( QEvent::KeyRelease, key, Qt::NoModifier );
context->sendEventToInputItem( &keyRelease );
}
bool QskInputCompositionModel::hasIntermediate() const
{
return false;
} }
#include "moc_QskInputCompositionModel.cpp" #include "moc_QskInputCompositionModel.cpp"

View File

@ -7,7 +7,6 @@
#define QSK_INPUT_COMPOSITION_MODEL_H #define QSK_INPUT_COMPOSITION_MODEL_H
#include <QObject> #include <QObject>
#include <memory>
class QskInputContext; class QskInputContext;
@ -16,36 +15,53 @@ class QskInputCompositionModel : public QObject
Q_OBJECT Q_OBJECT
public: public:
QskInputCompositionModel( QskInputContext* context ); enum Attribute
{
Words = 1 << 0
};
Q_ENUM( Attribute )
Q_DECLARE_FLAGS( Attributes, Attribute )
virtual ~QskInputCompositionModel(); virtual ~QskInputCompositionModel();
// to determine whether to show the suggestion bar: void composeKey( const QString& text, int spaceLeft );
virtual bool supportsSuggestions() const;
void commit( const QString& ); virtual int candidateCount() const = 0;
virtual void commitCandidate( int ); virtual QString candidate( int ) const = 0;
void composeKey( int key ); void reset();
virtual int candidateCount() const; QString preeditText() const;
virtual QString candidate( int ) const; void setPreeditText( const QString& );
Attributes attributes() const;
protected: protected:
virtual bool hasIntermediate() const; QskInputCompositionModel( Attributes, QskInputContext* );
virtual QString polishPreedit( const QString& preedit );
virtual void requestCandidates( const QString& preedit ) = 0;
virtual void resetCandidates() = 0;
QskInputContext* context() const;
Q_SIGNALS: Q_SIGNALS:
void candidatesChanged(); void candidatesChanged();
private: private:
void clearPreedit(); QString m_preedit;
QskInputContext* context() const; const Attributes m_attributes;
void sendPreeditTextEvent( const QString& );
void sendKeyEvents( int key );
class PrivateData;
std::unique_ptr< PrivateData > m_data;
}; };
inline QString QskInputCompositionModel::preeditText() const
{
return m_preedit;
}
inline QskInputCompositionModel::Attributes
QskInputCompositionModel::attributes() const
{
return m_attributes;
}
#endif #endif

View File

@ -18,9 +18,52 @@
#include <QskSetup.h> #include <QskSetup.h>
#include <QskEvent.h> #include <QskEvent.h>
#include <QGuiApplication> #include <QTextCharFormat>
#include <QHash> #include <QHash>
#include <QPointer> #include <QPointer>
#include <QGuiApplication>
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 inline bool qskIsControlKey( int keyCode )
{
switch ( keyCode )
{
case Qt::Key_Backspace:
case Qt::Key_Muhenkan:
case Qt::Key_Return:
case Qt::Key_Left:
case Qt::Key_Right:
case Qt::Key_Escape:
return true;
}
return false;
}
static void qskSetLocale( QQuickItem* inputPanel, const QLocale& locale ) static void qskSetLocale( QQuickItem* inputPanel, const QLocale& locale )
{ {
@ -82,6 +125,11 @@ static void qskSetCandidates( QQuickItem* inputPanel,
} }
} }
static inline uint qskHashLocale( const QLocale& locale )
{
return uint( locale.language() + uint( locale.country() ) << 16 );
}
class QskInputContext::PrivateData class QskInputContext::PrivateData
{ {
public: public:
@ -95,8 +143,7 @@ public:
QskPopup* inputPopup = nullptr; QskPopup* inputPopup = nullptr;
QskWindow* inputWindow = nullptr; QskWindow* inputWindow = nullptr;
QskInputCompositionModel* compositionModel; QHash< uint, QskInputCompositionModel* > compositionModels;
QHash< QLocale, QskInputCompositionModel* > compositionModels;
// the input panel is embedded in a window // the input panel is embedded in a window
bool ownsInputPanelWindow : 1; bool ownsInputPanelWindow : 1;
@ -108,21 +155,15 @@ QskInputContext::QskInputContext():
setObjectName( "InputContext" ); setObjectName( "InputContext" );
#if 1 #if 1
m_data->compositionModel = new QskInputCompositionModel( this ); setCompositionModel( locale(), new QskHunspellCompositionModel( this ) );
#else
m_data->compositionModel = new QskHunspellCompositionModel( this );
#endif #endif
connect( m_data->compositionModel, &QskInputCompositionModel::candidatesChanged,
this, &QskInputContext::handleCandidatesChanged );
connect( qskSetup, &QskSetup::inputPanelChanged,
this, &QskInputContext::setInputPanel );
#if 0 #if 0
setCompositionModel( QLocale::Chinese, new QskPinyinCompositionModel( this ) ); setCompositionModel( QLocale::Chinese, new QskPinyinCompositionModel( this ) );
#endif #endif
connect( qskSetup, &QskSetup::inputPanelChanged,
this, &QskInputContext::setInputPanel );
setInputPanel( qskSetup->inputPanel() ); setInputPanel( qskSetup->inputPanel() );
} }
@ -214,23 +255,33 @@ void QskInputContext::update( Qt::InputMethodQueries queries )
{ {
const auto locale = queryEvent.value( Qt::ImPreferredLanguage ).toLocale(); const auto locale = queryEvent.value( Qt::ImPreferredLanguage ).toLocale();
auto oldModel = compositionModel(); const auto oldModel = compositionModel();
if( m_data->inputPanel ) if( m_data->inputPanel )
qskSetLocale( m_data->inputPanel, locale ); qskSetLocale( m_data->inputPanel, locale );
auto newModel = compositionModel(); auto newModel = compositionModel();
if( oldModel != newModel ) if( newModel && ( oldModel != newModel ) )
{ {
connect( newModel, &QskInputCompositionModel::candidatesChanged, connect( newModel, &QskInputCompositionModel::candidatesChanged,
this, &QskInputContext::handleCandidatesChanged ); this, &QskInputContext::handleCandidatesChanged );
qskSetCandidatesEnabled( m_data->inputPanel,
newModel->supportsSuggestions() );
} }
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::ImMicroFocus
Qt::ImCursorRectangle Qt::ImCursorRectangle
@ -496,14 +547,6 @@ void QskInputContext::setFocusObject( QObject* focusObject )
if ( isAccepted ) if ( isAccepted )
setInputItem( focusItem ); setInputItem( focusItem );
} }
if ( m_data->inputPanel && m_data->inputPanel->isVisible() )
{
if ( m_data->inputItem && focusItem != m_data->inputItem )
qGuiApp->installEventFilter( this );
else
qGuiApp->removeEventFilter( this );
}
} }
void QskInputContext::setCompositionModel( void QskInputContext::setCompositionModel(
@ -511,9 +554,11 @@ void QskInputContext::setCompositionModel(
{ {
auto& models = m_data->compositionModels; auto& models = m_data->compositionModels;
const auto key = qskHashLocale( locale );
if ( model ) if ( model )
{ {
const auto it = models.find( locale ); const auto it = models.find( key );
if ( it != models.end() ) if ( it != models.end() )
{ {
if ( it.value() == model ) if ( it.value() == model )
@ -524,7 +569,7 @@ void QskInputContext::setCompositionModel(
} }
else else
{ {
models.insert( locale, model ); models.insert( key, model );
} }
connect( model, &QskInputCompositionModel::candidatesChanged, connect( model, &QskInputCompositionModel::candidatesChanged,
@ -532,7 +577,7 @@ void QskInputContext::setCompositionModel(
} }
else else
{ {
const auto it = models.find( locale ); const auto it = models.find( key );
if ( it != models.end() ) if ( it != models.end() )
{ {
delete it.value(); delete it.value();
@ -543,23 +588,33 @@ void QskInputContext::setCompositionModel(
QskInputCompositionModel* QskInputContext::compositionModel() const QskInputCompositionModel* QskInputContext::compositionModel() const
{ {
return m_data->compositionModels.value( locale(), m_data->compositionModel ); const auto key = qskHashLocale( locale() );
return m_data->compositionModels.value( key, nullptr );
} }
void QskInputContext::invokeAction( QInputMethod::Action action, int value ) void QskInputContext::invokeAction( QInputMethod::Action action, int value )
{ {
auto model = compositionModel();
switch ( static_cast< int >( action ) ) switch ( static_cast< int >( action ) )
{ {
case QskInputPanel::Compose: case QskInputPanel::Compose:
{ {
model->composeKey( value ); processKey( value );
break; break;
} }
case QskInputPanel::SelectCandidate: case QskInputPanel::SelectCandidate:
{ {
model->commitCandidate( value ); if ( auto model = compositionModel() )
{
auto text = model->candidate( value );
if ( model->attributes() & QskInputCompositionModel::Words )
text += " ";
sendText( text, true );
model->reset();
}
break; break;
} }
case QInputMethod::Click: case QInputMethod::Click:
@ -570,6 +625,130 @@ void QskInputContext::invokeAction( QInputMethod::Action action, int value )
} }
} }
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();
/*
First we have to handle the control keys
*/
switch ( key )
{
case Qt::Key_Backspace:
case Qt::Key_Muhenkan:
{
if ( model )
{
auto preeditText = model->preeditText();
if ( !preeditText.isEmpty() )
{
preeditText.chop( 1 );
sendText( preeditText, false );
model->setPreeditText( preeditText );
return;
}
}
sendKey( Qt::Key_Backspace );
return;
}
case Qt::Key_Return:
{
if ( model )
{
const auto preeditText = model->preeditText();
if ( !preeditText.isEmpty() )
{
if ( spaceLeft )
sendText( preeditText.left( spaceLeft ), true );
model->reset();
return;
}
}
if( !( hints & Qt::ImhMultiLine ) )
{
sendKey( Qt::Key_Return );
return;
}
break;
}
case Qt::Key_Space:
{
if ( model )
{
auto preeditText = model->preeditText();
if ( !preeditText.isEmpty() && spaceLeft)
{
preeditText = preeditText.left( spaceLeft );
sendText( preeditText, true );
spaceLeft -= preeditText.length();
model->reset();
}
}
break;
}
case Qt::Key_Left:
case Qt::Key_Right:
case Qt::Key_Escape:
{
sendKey( key );
return;
}
}
const QString text = qskKeyString( key );
if ( model )
{
model->composeKey( text, spaceLeft );
}
else
{
sendText( text, true );
}
}
void QskInputContext::handleCandidatesChanged() void QskInputContext::handleCandidatesChanged()
{ {
const auto model = compositionModel(); const auto model = compositionModel();
@ -628,11 +807,7 @@ void QskInputContext::setInputPanel( QQuickItem* inputPanel )
this, &QPlatformInputContext::emitLocaleChanged ); this, &QPlatformInputContext::emitLocaleChanged );
} }
if ( model ) qskSetCandidatesEnabled( inputPanel, model != nullptr );
{
qskSetCandidatesEnabled( m_data->inputPanel,
model->supportsSuggestions() );
}
} }
} }
@ -668,8 +843,6 @@ bool QskInputContext::eventFilter( QObject* object, QEvent* event )
} }
case QEvent::DeferredDelete: case QEvent::DeferredDelete:
{ {
object->removeEventFilter( this );
qGuiApp->removeEventFilter( this );
m_data->inputWindow = nullptr; m_data->inputWindow = nullptr;
break; break;
} }
@ -677,45 +850,20 @@ bool QskInputContext::eventFilter( QObject* object, QEvent* event )
break; break;
} }
} }
else else if ( object == m_data->inputPopup )
{ {
switch( static_cast< int >( event->type() ) ) switch( static_cast< int >( event->type() ) )
{ {
case QskEvent::GeometryChange: case QskEvent::GeometryChange:
{ {
if ( object == m_data->inputPanel ) if ( event->type() == QskEvent::GeometryChange )
{ emitKeyboardRectChanged();
if ( event->type() == QskEvent::GeometryChange )
emitKeyboardRectChanged();
}
break; break;
} }
case QEvent::InputMethodQuery:
{
/*
Qt/Quick expects that the item associated with the input context
holds the focus. But this does not work, when a virtual
keyboard is used, where you can navigate and select inside.
So we have to fix the receiver.
*/
if ( ( object != m_data->inputItem )
&& qskIsAncestorOf( m_data->inputPanel, m_data->inputItem ) )
{
sendEventToInputItem( event );
return true;
}
break;
}
case QEvent::DeferredDelete: case QEvent::DeferredDelete:
{ {
if ( object == m_data->inputPopup ) m_data->inputPopup = nullptr;
{
object->removeEventFilter( this );
qGuiApp->removeEventFilter( this );
m_data->inputPopup = nullptr;
}
break; break;
} }
} }
@ -724,10 +872,9 @@ bool QskInputContext::eventFilter( QObject* object, QEvent* event )
return Inherited::eventFilter( object, event ); return Inherited::eventFilter( object, event );
} }
bool QskInputContext::filterEvent( const QEvent* event ) bool QskInputContext::filterEvent( const QEvent* )
{ {
// called from QXcbKeyboard, but what about other platforms // called from QXcbKeyboard, but what about other platforms
Q_UNUSED( event )
return false; return false;
} }
@ -735,15 +882,51 @@ QInputMethodQueryEvent QskInputContext::queryInputMethod(
Qt::InputMethodQueries queries ) const Qt::InputMethodQueries queries ) const
{ {
QInputMethodQueryEvent event( queries ); QInputMethodQueryEvent event( queries );
sendEventToInputItem( &event );
if ( m_data->inputItem )
QCoreApplication::sendEvent( m_data->inputItem, &event );
return event; return event;
} }
void QskInputContext::sendEventToInputItem( QEvent* event ) const void QskInputContext::sendText(
const QString& text, bool isFinal ) const
{ {
if ( m_data->inputItem && event ) if ( m_data->inputItem == nullptr )
QCoreApplication::sendEvent( m_data->inputItem, event ); 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" #include "moc_QskInputContext.cpp"

View File

@ -10,8 +10,12 @@
#include <memory> #include <memory>
class QskInputCompositionModel; class QskInputCompositionModel;
class QQuickItem; class QQuickItem;
class QInputMethodQueryEvent; class QInputMethodQueryEvent;
class QInputMethodEvent;
class QKeyEvent;
class QskInputContext : public QPlatformInputContext class QskInputContext : public QPlatformInputContext
{ {
@ -51,7 +55,12 @@ public:
virtual bool filterEvent( const QEvent* ) override; virtual bool filterEvent( const QEvent* ) override;
QInputMethodQueryEvent queryInputMethod( Qt::InputMethodQueries ) const; QInputMethodQueryEvent queryInputMethod( Qt::InputMethodQueries ) const;
void sendEventToInputItem( QEvent* ) const;
void sendKey( int key ) const;
void sendText( const QString& text, bool isFinal ) const;
Qt::InputMethodHints inputHints() const;
int keysLeft() const;
private Q_SLOTS: private Q_SLOTS:
void handleCandidatesChanged(); void handleCandidatesChanged();
@ -60,6 +69,8 @@ private Q_SLOTS:
virtual bool eventFilter( QObject*, QEvent* ) override; virtual bool eventFilter( QObject*, QEvent* ) override;
private: private:
void processKey( int key );
void setInputItem( QQuickItem* ); void setInputItem( QQuickItem* );
QskInputCompositionModel* compositionModel() const; QskInputCompositionModel* compositionModel() const;

View File

@ -18,7 +18,7 @@ public:
}; };
QskPinyinCompositionModel::QskPinyinCompositionModel( QskInputContext* context ): QskPinyinCompositionModel::QskPinyinCompositionModel( QskInputContext* context ):
Inherited( context ), Inherited( Attributes(), context ),
m_data( new PrivateData ) m_data( new PrivateData )
{ {
#if 1 #if 1
@ -39,11 +39,6 @@ QskPinyinCompositionModel::~QskPinyinCompositionModel()
ime_pinyin::im_close_decoder(); ime_pinyin::im_close_decoder();
} }
bool QskPinyinCompositionModel::supportsSuggestions() const
{
return true;
}
int QskPinyinCompositionModel::candidateCount() const int QskPinyinCompositionModel::candidateCount() const
{ {
return m_data->candidates.count(); return m_data->candidates.count();
@ -57,53 +52,54 @@ QString QskPinyinCompositionModel::candidate( int index ) const
return QString(); return QString();
} }
bool QskPinyinCompositionModel::hasIntermediate() const void QskPinyinCompositionModel::resetCandidates()
{ {
return m_data->candidates.count() > 0; ime_pinyin::im_reset_search();
}
QString QskPinyinCompositionModel::polishPreedit( const QString& preedit ) if ( !m_data->candidates.isEmpty() )
{
if( preedit.isEmpty() )
{ {
ime_pinyin::im_reset_search(); m_data->candidates.clear();
}
QByteArray preeditBuffer = preedit.toLatin1();
size_t numSearchResults = ime_pinyin::im_search(
preeditBuffer.constData(), size_t( preeditBuffer.length() ) );
if( numSearchResults > 0 )
{
QStringList newCandidates;
newCandidates.reserve( 1 );
QVector< QChar > candidateBuffer;
candidateBuffer.resize( ime_pinyin::kMaxSearchSteps + 1 );
// ### numSearchResults is way too big, we should only do this for the first ten results or so
for( unsigned int a = 0; a < numSearchResults; a++ )
{
size_t length = static_cast< size_t >( candidateBuffer.length() - 1 );
bool getCandidate = ime_pinyin::im_get_candidate( a, reinterpret_cast< ime_pinyin::char16* >( candidateBuffer.data() ), length );
QString candidate;
if( getCandidate )
{
candidateBuffer.last() = 0;
candidate = QString( candidateBuffer.data() );
}
Qt::Key key = Qt::Key( candidate.at( 0 ).unicode() );
QString string = QChar( key );
newCandidates.append( string );
}
m_data->candidates = newCandidates;
Q_EMIT candidatesChanged(); Q_EMIT candidatesChanged();
} }
}
return preedit;
void QskPinyinCompositionModel::requestCandidates( const QString& text )
{
const QByteArray bytes = text.toLatin1();
size_t count = ime_pinyin::im_search(
bytes.constData(), size_t( bytes.length() ) );
if( count <= 0 )
return;
const size_t maxCount = 20;
if ( count > maxCount )
count = maxCount;
QVector< QChar > candidateBuffer;
candidateBuffer.resize( ime_pinyin::kMaxSearchSteps + 1 );
QStringList candidates;
candidates.reserve( count );
for( unsigned int i = 0; i < count; i++ )
{
size_t length = static_cast< size_t >( candidateBuffer.length() - 1 );
const auto buf = reinterpret_cast< ime_pinyin::char16* >( candidateBuffer.data() );
const bool found = ime_pinyin::im_get_candidate( i, buf, length );
Q_ASSERT( found );
candidateBuffer.last() = 0;
auto candidate = QString( candidateBuffer.data() );
candidate = QChar( Qt::Key( candidate[0].unicode() ) );
candidates += candidate;
}
m_data->candidates = candidates;
Q_EMIT candidatesChanged();
} }

View File

@ -7,6 +7,7 @@
#define QSK_PINYIN_COMPOSITION_MODEL_H #define QSK_PINYIN_COMPOSITION_MODEL_H
#include "QskInputCompositionModel.h" #include "QskInputCompositionModel.h"
#include <memory>
class QskPinyinCompositionModel : public QskInputCompositionModel class QskPinyinCompositionModel : public QskInputCompositionModel
{ {
@ -16,15 +17,12 @@ public:
QskPinyinCompositionModel( QskInputContext* ); QskPinyinCompositionModel( QskInputContext* );
virtual ~QskPinyinCompositionModel() override; virtual ~QskPinyinCompositionModel() override;
virtual bool supportsSuggestions() const override final;
virtual int candidateCount() const override; virtual int candidateCount() const override;
virtual QString candidate( int ) const override; virtual QString candidate( int ) const override;
protected: protected:
// Used for text composition virtual void requestCandidates( const QString& ) override;
virtual bool hasIntermediate() const override; virtual void resetCandidates() override;
virtual QString polishPreedit( const QString& preedit ) override;
private: private:
class PrivateData; class PrivateData;