QskInputContext improved
This commit is contained in:
parent
48c897f825
commit
151a73cb0b
@ -11,7 +11,7 @@ public:
|
||||
};
|
||||
|
||||
QskHunspellCompositionModel::QskHunspellCompositionModel( QskInputContext* context ):
|
||||
Inherited( context ),
|
||||
Inherited( Words, context ),
|
||||
m_data( new PrivateData() )
|
||||
{
|
||||
#if 1
|
||||
@ -28,21 +28,6 @@ QskHunspellCompositionModel::~QskHunspellCompositionModel()
|
||||
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
|
||||
{
|
||||
return m_data->candidates.count();
|
||||
@ -53,47 +38,38 @@ QString QskHunspellCompositionModel::candidate( int pos ) const
|
||||
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();
|
||||
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();
|
||||
}
|
||||
|
@ -7,6 +7,7 @@
|
||||
#define QSK_HUNSPELL_COMPOSITION_MODEL_H
|
||||
|
||||
#include "QskInputCompositionModel.h"
|
||||
#include <memory>
|
||||
|
||||
class QskHunspellCompositionModel : public QskInputCompositionModel
|
||||
{
|
||||
@ -16,15 +17,12 @@ public:
|
||||
QskHunspellCompositionModel( QskInputContext* context );
|
||||
virtual ~QskHunspellCompositionModel() override;
|
||||
|
||||
virtual bool supportsSuggestions() const override final;
|
||||
|
||||
virtual void commitCandidate( int index ) override;
|
||||
virtual int candidateCount() const override;
|
||||
virtual QString candidate( int pos ) const override;
|
||||
|
||||
protected:
|
||||
virtual bool hasIntermediate() const override;
|
||||
virtual QString polishPreedit( const QString& ) override;
|
||||
virtual void requestCandidates( const QString& ) override;
|
||||
virtual void resetCandidates() override;
|
||||
|
||||
private:
|
||||
class PrivateData;
|
||||
|
@ -6,45 +6,12 @@
|
||||
#include "QskInputCompositionModel.h"
|
||||
#include "QskInputContext.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QInputMethodEvent>
|
||||
#include <QTextCharFormat>
|
||||
#include <QInputMethodQueryEvent>
|
||||
|
||||
static inline QString qskKeyString( int code )
|
||||
{
|
||||
// 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 ):
|
||||
QskInputCompositionModel::QskInputCompositionModel(
|
||||
Attributes attributes, QskInputContext* context ):
|
||||
QObject( context ),
|
||||
m_data( new PrivateData )
|
||||
m_attributes( attributes )
|
||||
{
|
||||
}
|
||||
|
||||
@ -57,211 +24,67 @@ QskInputContext* QskInputCompositionModel::context() const
|
||||
return qobject_cast< QskInputContext* >( parent() );
|
||||
}
|
||||
|
||||
bool QskInputCompositionModel::supportsSuggestions() const
|
||||
void QskInputCompositionModel::composeKey( const QString& text, int spaceLeft )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
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 )
|
||||
if ( candidateCount() > 0 )
|
||||
{
|
||||
case Qt::Key_Backspace:
|
||||
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;
|
||||
}
|
||||
m_preedit += text;
|
||||
|
||||
if( !m_data->preedit.isEmpty() )
|
||||
{
|
||||
commit( m_data->preedit.left( spaceLeft ) );
|
||||
}
|
||||
requestCandidates( m_preedit );
|
||||
context()->sendText( m_preedit, false );
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const auto firstCandidate = candidateCount() > 0 ? candidate( 0 ) : QString();
|
||||
const auto oldPreedit = m_data->preedit;
|
||||
requestCandidates( m_preedit );
|
||||
|
||||
m_data->preedit += qskKeyString( key );
|
||||
auto displayPreedit = polishPreedit( m_data->preedit );
|
||||
|
||||
// If there is no intermediate, decide between committing the first candidate and skipping
|
||||
if ( !hasIntermediate() )
|
||||
QString txt;
|
||||
if ( candidateCount() == 0 )
|
||||
{
|
||||
// Skip preedit phase if there are no candidates/intermediates
|
||||
if ( firstCandidate.isEmpty() )
|
||||
txt = m_preedit.left( spaceLeft );
|
||||
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 ) );
|
||||
spaceLeft -= oldPreedit.leftRef( spaceLeft ).length();
|
||||
context()->sendText( m_preedit, false );
|
||||
}
|
||||
else
|
||||
{
|
||||
commit( firstCandidate );
|
||||
--spaceLeft;
|
||||
}
|
||||
|
||||
if ( !spaceLeft )
|
||||
return;
|
||||
|
||||
m_data->preedit = qskKeyString( key );
|
||||
displayPreedit = polishPreedit( m_data->preedit );
|
||||
|
||||
if ( !hasIntermediate() )
|
||||
{
|
||||
commit( m_data->preedit );
|
||||
return;
|
||||
context()->sendText( m_preedit, true );
|
||||
m_preedit.clear();
|
||||
resetCandidates();
|
||||
}
|
||||
}
|
||||
|
||||
sendPreeditTextEvent( displayPreedit );
|
||||
}
|
||||
|
||||
void QskInputCompositionModel::clearPreedit()
|
||||
void QskInputCompositionModel::setPreeditText( const QString& text )
|
||||
{
|
||||
m_data->preedit.clear();
|
||||
polishPreedit( m_data->preedit );
|
||||
if ( text != m_preedit )
|
||||
{
|
||||
m_preedit = text;
|
||||
requestCandidates( m_preedit );
|
||||
}
|
||||
}
|
||||
|
||||
int QskInputCompositionModel::candidateCount() const
|
||||
void QskInputCompositionModel::reset()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
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;
|
||||
m_preedit.clear();
|
||||
resetCandidates();
|
||||
}
|
||||
|
||||
#include "moc_QskInputCompositionModel.cpp"
|
||||
|
@ -7,7 +7,6 @@
|
||||
#define QSK_INPUT_COMPOSITION_MODEL_H
|
||||
|
||||
#include <QObject>
|
||||
#include <memory>
|
||||
|
||||
class QskInputContext;
|
||||
|
||||
@ -16,36 +15,53 @@ class QskInputCompositionModel : public QObject
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
QskInputCompositionModel( QskInputContext* context );
|
||||
enum Attribute
|
||||
{
|
||||
Words = 1 << 0
|
||||
};
|
||||
|
||||
Q_ENUM( Attribute )
|
||||
Q_DECLARE_FLAGS( Attributes, Attribute )
|
||||
|
||||
virtual ~QskInputCompositionModel();
|
||||
|
||||
// to determine whether to show the suggestion bar:
|
||||
virtual bool supportsSuggestions() const;
|
||||
void composeKey( const QString& text, int spaceLeft );
|
||||
|
||||
void commit( const QString& );
|
||||
virtual void commitCandidate( int );
|
||||
virtual int candidateCount() const = 0;
|
||||
virtual QString candidate( int ) const = 0;
|
||||
|
||||
void composeKey( int key );
|
||||
void reset();
|
||||
|
||||
virtual int candidateCount() const;
|
||||
virtual QString candidate( int ) const;
|
||||
QString preeditText() const;
|
||||
void setPreeditText( const QString& );
|
||||
|
||||
Attributes attributes() const;
|
||||
|
||||
protected:
|
||||
virtual bool hasIntermediate() const;
|
||||
virtual QString polishPreedit( const QString& preedit );
|
||||
QskInputCompositionModel( Attributes, QskInputContext* );
|
||||
|
||||
virtual void requestCandidates( const QString& preedit ) = 0;
|
||||
virtual void resetCandidates() = 0;
|
||||
|
||||
QskInputContext* context() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void candidatesChanged();
|
||||
|
||||
private:
|
||||
void clearPreedit();
|
||||
QskInputContext* context() const;
|
||||
|
||||
void sendPreeditTextEvent( const QString& );
|
||||
void sendKeyEvents( int key );
|
||||
|
||||
class PrivateData;
|
||||
std::unique_ptr< PrivateData > m_data;
|
||||
QString m_preedit;
|
||||
const Attributes m_attributes;
|
||||
};
|
||||
|
||||
inline QString QskInputCompositionModel::preeditText() const
|
||||
{
|
||||
return m_preedit;
|
||||
}
|
||||
|
||||
inline QskInputCompositionModel::Attributes
|
||||
QskInputCompositionModel::attributes() const
|
||||
{
|
||||
return m_attributes;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
@ -18,9 +18,52 @@
|
||||
#include <QskSetup.h>
|
||||
#include <QskEvent.h>
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QTextCharFormat>
|
||||
#include <QHash>
|
||||
#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 )
|
||||
{
|
||||
@ -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
|
||||
{
|
||||
public:
|
||||
@ -95,8 +143,7 @@ public:
|
||||
QskPopup* inputPopup = nullptr;
|
||||
QskWindow* inputWindow = nullptr;
|
||||
|
||||
QskInputCompositionModel* compositionModel;
|
||||
QHash< QLocale, QskInputCompositionModel* > compositionModels;
|
||||
QHash< uint, QskInputCompositionModel* > compositionModels;
|
||||
|
||||
// the input panel is embedded in a window
|
||||
bool ownsInputPanelWindow : 1;
|
||||
@ -108,21 +155,15 @@ QskInputContext::QskInputContext():
|
||||
setObjectName( "InputContext" );
|
||||
|
||||
#if 1
|
||||
m_data->compositionModel = new QskInputCompositionModel( this );
|
||||
#else
|
||||
m_data->compositionModel = new QskHunspellCompositionModel( this );
|
||||
setCompositionModel( locale(), new QskHunspellCompositionModel( this ) );
|
||||
#endif
|
||||
|
||||
connect( m_data->compositionModel, &QskInputCompositionModel::candidatesChanged,
|
||||
this, &QskInputContext::handleCandidatesChanged );
|
||||
|
||||
connect( qskSetup, &QskSetup::inputPanelChanged,
|
||||
this, &QskInputContext::setInputPanel );
|
||||
|
||||
#if 0
|
||||
setCompositionModel( QLocale::Chinese, new QskPinyinCompositionModel( this ) );
|
||||
#endif
|
||||
|
||||
connect( qskSetup, &QskSetup::inputPanelChanged,
|
||||
this, &QskInputContext::setInputPanel );
|
||||
|
||||
setInputPanel( qskSetup->inputPanel() );
|
||||
}
|
||||
|
||||
@ -214,23 +255,33 @@ void QskInputContext::update( Qt::InputMethodQueries queries )
|
||||
{
|
||||
const auto locale = queryEvent.value( Qt::ImPreferredLanguage ).toLocale();
|
||||
|
||||
auto oldModel = compositionModel();
|
||||
const auto oldModel = compositionModel();
|
||||
|
||||
if( m_data->inputPanel )
|
||||
qskSetLocale( m_data->inputPanel, locale );
|
||||
|
||||
auto newModel = compositionModel();
|
||||
|
||||
if( oldModel != newModel )
|
||||
if( newModel && ( oldModel != newModel ) )
|
||||
{
|
||||
connect( newModel, &QskInputCompositionModel::candidatesChanged,
|
||||
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::ImCursorRectangle
|
||||
@ -496,14 +547,6 @@ void QskInputContext::setFocusObject( QObject* focusObject )
|
||||
if ( isAccepted )
|
||||
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(
|
||||
@ -511,9 +554,11 @@ void QskInputContext::setCompositionModel(
|
||||
{
|
||||
auto& models = m_data->compositionModels;
|
||||
|
||||
const auto key = qskHashLocale( locale );
|
||||
|
||||
if ( model )
|
||||
{
|
||||
const auto it = models.find( locale );
|
||||
const auto it = models.find( key );
|
||||
if ( it != models.end() )
|
||||
{
|
||||
if ( it.value() == model )
|
||||
@ -524,7 +569,7 @@ void QskInputContext::setCompositionModel(
|
||||
}
|
||||
else
|
||||
{
|
||||
models.insert( locale, model );
|
||||
models.insert( key, model );
|
||||
}
|
||||
|
||||
connect( model, &QskInputCompositionModel::candidatesChanged,
|
||||
@ -532,7 +577,7 @@ void QskInputContext::setCompositionModel(
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto it = models.find( locale );
|
||||
const auto it = models.find( key );
|
||||
if ( it != models.end() )
|
||||
{
|
||||
delete it.value();
|
||||
@ -543,23 +588,33 @@ void QskInputContext::setCompositionModel(
|
||||
|
||||
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 )
|
||||
{
|
||||
auto model = compositionModel();
|
||||
|
||||
switch ( static_cast< int >( action ) )
|
||||
{
|
||||
case QskInputPanel::Compose:
|
||||
{
|
||||
model->composeKey( value );
|
||||
processKey( value );
|
||||
break;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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()
|
||||
{
|
||||
const auto model = compositionModel();
|
||||
@ -628,11 +807,7 @@ void QskInputContext::setInputPanel( QQuickItem* inputPanel )
|
||||
this, &QPlatformInputContext::emitLocaleChanged );
|
||||
}
|
||||
|
||||
if ( model )
|
||||
{
|
||||
qskSetCandidatesEnabled( m_data->inputPanel,
|
||||
model->supportsSuggestions() );
|
||||
}
|
||||
qskSetCandidatesEnabled( inputPanel, model != nullptr );
|
||||
}
|
||||
}
|
||||
|
||||
@ -668,8 +843,6 @@ bool QskInputContext::eventFilter( QObject* object, QEvent* event )
|
||||
}
|
||||
case QEvent::DeferredDelete:
|
||||
{
|
||||
object->removeEventFilter( this );
|
||||
qGuiApp->removeEventFilter( this );
|
||||
m_data->inputWindow = nullptr;
|
||||
break;
|
||||
}
|
||||
@ -677,45 +850,20 @@ bool QskInputContext::eventFilter( QObject* object, QEvent* event )
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
else if ( object == m_data->inputPopup )
|
||||
{
|
||||
switch( static_cast< int >( event->type() ) )
|
||||
{
|
||||
case QskEvent::GeometryChange:
|
||||
{
|
||||
if ( object == m_data->inputPanel )
|
||||
{
|
||||
if ( event->type() == QskEvent::GeometryChange )
|
||||
emitKeyboardRectChanged();
|
||||
}
|
||||
if ( event->type() == QskEvent::GeometryChange )
|
||||
emitKeyboardRectChanged();
|
||||
|
||||
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:
|
||||
{
|
||||
if ( object == m_data->inputPopup )
|
||||
{
|
||||
object->removeEventFilter( this );
|
||||
qGuiApp->removeEventFilter( this );
|
||||
m_data->inputPopup = nullptr;
|
||||
}
|
||||
m_data->inputPopup = nullptr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -724,10 +872,9 @@ bool QskInputContext::eventFilter( QObject* object, QEvent* 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
|
||||
Q_UNUSED( event )
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -735,15 +882,51 @@ QInputMethodQueryEvent QskInputContext::queryInputMethod(
|
||||
Qt::InputMethodQueries queries ) const
|
||||
{
|
||||
QInputMethodQueryEvent event( queries );
|
||||
sendEventToInputItem( &event );
|
||||
|
||||
if ( m_data->inputItem )
|
||||
QCoreApplication::sendEvent( m_data->inputItem, &event );
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
void QskInputContext::sendEventToInputItem( QEvent* event ) const
|
||||
void QskInputContext::sendText(
|
||||
const QString& text, bool isFinal ) const
|
||||
{
|
||||
if ( m_data->inputItem && event )
|
||||
QCoreApplication::sendEvent( m_data->inputItem, event );
|
||||
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"
|
||||
|
@ -10,8 +10,12 @@
|
||||
#include <memory>
|
||||
|
||||
class QskInputCompositionModel;
|
||||
|
||||
class QQuickItem;
|
||||
|
||||
class QInputMethodQueryEvent;
|
||||
class QInputMethodEvent;
|
||||
class QKeyEvent;
|
||||
|
||||
class QskInputContext : public QPlatformInputContext
|
||||
{
|
||||
@ -51,7 +55,12 @@ public:
|
||||
virtual bool filterEvent( const QEvent* ) override;
|
||||
|
||||
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:
|
||||
void handleCandidatesChanged();
|
||||
@ -60,6 +69,8 @@ private Q_SLOTS:
|
||||
virtual bool eventFilter( QObject*, QEvent* ) override;
|
||||
|
||||
private:
|
||||
void processKey( int key );
|
||||
|
||||
void setInputItem( QQuickItem* );
|
||||
QskInputCompositionModel* compositionModel() const;
|
||||
|
||||
|
@ -18,7 +18,7 @@ public:
|
||||
};
|
||||
|
||||
QskPinyinCompositionModel::QskPinyinCompositionModel( QskInputContext* context ):
|
||||
Inherited( context ),
|
||||
Inherited( Attributes(), context ),
|
||||
m_data( new PrivateData )
|
||||
{
|
||||
#if 1
|
||||
@ -39,11 +39,6 @@ QskPinyinCompositionModel::~QskPinyinCompositionModel()
|
||||
ime_pinyin::im_close_decoder();
|
||||
}
|
||||
|
||||
bool QskPinyinCompositionModel::supportsSuggestions() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
int QskPinyinCompositionModel::candidateCount() const
|
||||
{
|
||||
return m_data->candidates.count();
|
||||
@ -57,53 +52,54 @@ QString QskPinyinCompositionModel::candidate( int index ) const
|
||||
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( preedit.isEmpty() )
|
||||
if ( !m_data->candidates.isEmpty() )
|
||||
{
|
||||
ime_pinyin::im_reset_search();
|
||||
}
|
||||
|
||||
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;
|
||||
m_data->candidates.clear();
|
||||
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();
|
||||
}
|
||||
|
@ -7,6 +7,7 @@
|
||||
#define QSK_PINYIN_COMPOSITION_MODEL_H
|
||||
|
||||
#include "QskInputCompositionModel.h"
|
||||
#include <memory>
|
||||
|
||||
class QskPinyinCompositionModel : public QskInputCompositionModel
|
||||
{
|
||||
@ -16,15 +17,12 @@ public:
|
||||
QskPinyinCompositionModel( QskInputContext* );
|
||||
virtual ~QskPinyinCompositionModel() override;
|
||||
|
||||
virtual bool supportsSuggestions() const override final;
|
||||
|
||||
virtual int candidateCount() const override;
|
||||
virtual QString candidate( int ) const override;
|
||||
|
||||
protected:
|
||||
// Used for text composition
|
||||
virtual bool hasIntermediate() const override;
|
||||
virtual QString polishPreedit( const QString& preedit ) override;
|
||||
virtual void requestCandidates( const QString& ) override;
|
||||
virtual void resetCandidates() override;
|
||||
|
||||
private:
|
||||
class PrivateData;
|
||||
|
Loading…
x
Reference in New Issue
Block a user