qskinny/src/inputpanel/QskInputPanel.cpp

610 lines
16 KiB
C++

/******************************************************************************
* QSkinny - Copyright (C) 2016 Uwe Rathmann
* This file may be used under the terms of the QSkinny License, Version 1.0
*****************************************************************************/
#include "QskInputPanel.h"
#include "QskInputContext.h"
#include "QskTextPredictor.h"
#include <qpointer.h>
#include <qtextformat.h>
namespace
{
struct Result
{
int key = 0;
QString text;
bool isFinal = true;
};
}
static void qskRegisterInputPanel()
{
qRegisterMetaType< Result >( "Result" );
}
Q_CONSTRUCTOR_FUNCTION( qskRegisterInputPanel )
static inline QQuickItem* qskReceiverItem( const QskInputPanel* panel )
{
if ( auto item = panel->inputProxy() )
return item;
return panel->inputItem();
}
static inline bool qskUsePrediction( Qt::InputMethodHints hints )
{
constexpr Qt::InputMethodHints mask =
Qt::ImhNoPredictiveText | Qt::ImhExclusiveInputMask | Qt::ImhHiddenText;
return ( hints & mask ) == 0;
}
static inline void qskSendText(
QQuickItem* receiver, const QString& text, bool isFinal )
{
if ( receiver == nullptr )
return;
if ( isFinal )
{
QInputMethodEvent event;
/*
QQuickTextInput is buggy when receiving empty commit strings.
We need to send a wrong replaceLength to work around it.
See QTBUG: 68874
*/
if ( text.isEmpty() )
event.setCommitString( text, 0, 1 );
else
event.setCommitString( text );
QCoreApplication::sendEvent( receiver, &event );
}
else
{
QTextCharFormat format;
format.setFontUnderline( true );
const QInputMethodEvent::Attribute attribute(
QInputMethodEvent::TextFormat, 0, text.length(), format );
QInputMethodEvent event( text, { attribute } );
QCoreApplication::sendEvent( receiver, &event );
}
}
static inline void qskSendReplaceText( QQuickItem* receiver, const QString& text )
{
if ( receiver == nullptr )
return;
QInputMethodEvent::Attribute attribute(
QInputMethodEvent::Selection, 0, 32767, QVariant() );
QInputMethodEvent event( QString(), { attribute } );
QCoreApplication::sendEvent( receiver, &event );
qskSendText( receiver, text, true );
}
static inline void qskSendKey( QQuickItem* receiver, int key )
{
if ( receiver == nullptr )
return;
QKeyEvent keyPress( QEvent::KeyPress, key, Qt::NoModifier );
QCoreApplication::sendEvent( receiver, &keyPress );
QKeyEvent keyRelease( QEvent::KeyRelease, key, Qt::NoModifier );
QCoreApplication::sendEvent( receiver, &keyRelease );
}
namespace
{
class KeyProcessor : public QObject
{
Q_OBJECT
public:
QString preedit() const
{
return m_preedit;
}
void processKey(
int key, Qt::InputMethodHints inputHints, QskInputPanel* panel,
QskTextPredictor* predictor, int spaceLeft )
{
// reset:
m_currentResult.isFinal = true;
m_currentResult.text.clear();
m_currentResult.key = 0;
m_predictor = predictor;
m_spaceLeft = spaceLeft;
// First we have to handle the control keys
switch ( key )
{
case Qt::Key_Backspace:
case Qt::Key_Muhenkan:
{
if ( predictor && !m_preedit.isEmpty() )
{
m_preedit.chop( 1 );
m_currentResult.text = m_preedit;
m_currentResult.isFinal = false;
Q_EMIT panel->predictionRequested( m_preedit );
// Let the input field update right away, otherwise
// we'll get weird effects with fast backspace presses:
Q_EMIT keyProcessingFinished( m_currentResult );
return;
}
else
{
m_currentResult.key = Qt::Key_Backspace;
Q_EMIT keyProcessingFinished( m_currentResult );
return;
}
break;
}
case Qt::Key_Return:
{
if ( predictor )
{
if ( !m_preedit.isEmpty() )
{
if ( spaceLeft )
{
m_currentResult.text = m_preedit.left( spaceLeft );
m_currentResult.isFinal = true;
}
reset();
Q_EMIT keyProcessingFinished( m_currentResult );
return;
}
}
if ( !( inputHints & Qt::ImhMultiLine ) )
{
m_currentResult.key = Qt::Key_Return;
Q_EMIT keyProcessingFinished( m_currentResult );
return;
}
break;
}
case Qt::Key_Space:
{
if ( predictor )
{
if ( !m_preedit.isEmpty() && spaceLeft )
{
m_preedit += keyString( key );
m_preedit = m_preedit.left( spaceLeft );
m_currentResult.text = m_preedit;
m_currentResult.isFinal = true;
reset();
Q_EMIT keyProcessingFinished( m_currentResult );
return;
}
}
break;
}
case Qt::Key_Left:
case Qt::Key_Right:
case Qt::Key_Escape:
case Qt::Key_Cancel:
{
m_currentResult.key = key;
Q_EMIT keyProcessingFinished( m_currentResult );
return;
}
}
const QString text = keyString( key );
if ( predictor )
{
m_preedit += text;
Q_EMIT panel->predictionRequested( m_preedit );
}
else
{
m_currentResult.text = text;
m_currentResult.isFinal = true;
Q_EMIT keyProcessingFinished( m_currentResult );
}
}
void reset()
{
m_preedit.clear();
}
void continueProcessingKey( const QStringList& candidates )
{
if ( m_predictor )
{
if ( candidates.count() > 0 )
{
m_currentResult.text = m_preedit;
m_currentResult.isFinal = false;
}
else
{
m_currentResult.text = m_preedit.left( m_spaceLeft );
m_currentResult.isFinal = true;
m_preedit.clear();
}
}
Q_EMIT keyProcessingFinished( m_currentResult );
}
Q_SIGNALS:
void keyProcessingFinished( const Result& );
private:
inline QString keyString( int keyCode ) const
{
// 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 );
}
QString m_preedit;
int m_spaceLeft = -1;
QskTextPredictor* m_predictor = nullptr;
Result m_currentResult;
};
}
class QskInputPanel::PrivateData
{
public:
PrivateData( QskInputPanel* panel )
: panel( panel )
{
}
KeyProcessor keyProcessor;
QPointer< QQuickItem > inputItem;
QLocale predictorLocale;
std::shared_ptr< QskTextPredictor > predictor;
QStringList candidates;
Qt::InputMethodHints inputHints;
bool hasPredictorLocale = false;
const QskInputPanel* panel;
void handleKeyProcessingFinished( const Result& result )
{
switch ( result.key )
{
case 0:
{
qskSendText( qskReceiverItem( panel ),
result.text, result.isFinal );
break;
}
case Qt::Key_Return:
{
if ( auto proxy = panel->inputProxy() )
{
// using input method query instead
const auto value = proxy->property( "text" );
if ( value.canConvert< QString >() )
{
qskSendReplaceText( inputItem, value.toString() );
}
}
qskSendKey( inputItem, result.key );
break;
}
case Qt::Key_Escape:
case Qt::Key_Cancel:
{
qskSendKey( inputItem, result.key );
break;
}
default:
{
qskSendKey( qskReceiverItem( panel ), result.key );
}
}
}
};
QskInputPanel::QskInputPanel( QQuickItem* parent )
: Inherited( parent )
, m_data( new PrivateData( this ) )
{
setAutoLayoutChildren( true );
initSizePolicy( QskSizePolicy::Expanding, QskSizePolicy::Constrained );
connect( this, &QskInputPanel::keySelected,
this, &QskInputPanel::commitKey );
connect( this, &QskInputPanel::predictiveTextSelected,
this, &QskInputPanel::commitPredictiveText );
connect( this, &QskControl::localeChanged,
this, &QskInputPanel::updateLocale );
connect( &m_data->keyProcessor, &KeyProcessor::keyProcessingFinished,
this, [this]( const Result& result ) { m_data->handleKeyProcessingFinished( result ); } );
updateLocale( locale() );
}
QskInputPanel::~QskInputPanel()
{
}
void QskInputPanel::attachInputItem( QQuickItem* item )
{
if ( item == m_data->inputItem )
return;
if ( m_data->inputItem )
{
disconnect( m_data->inputItem, &QObject::destroyed,
this, &QskInputPanel::inputItemDestroyed );
}
m_data->inputItem = item;
if ( item )
{
if ( m_data->predictor )
Q_EMIT predictionReset();
m_data->keyProcessor.reset();
m_data->inputHints = Qt::InputMethodHints();
attachItem( item );
Qt::InputMethodQueries queries = Qt::ImQueryAll;
queries &= ~Qt::ImEnabled;
updateInputPanel( queries );
if ( inputProxy() )
{
/*
Hiding the cursor in item. We use postEvent
so that everything on the item is done,
when receiving the event.
*/
const QInputMethodEvent::Attribute attribute(
QInputMethodEvent::Cursor, 0, 0, QVariant() );
QCoreApplication::postEvent( item,
new QInputMethodEvent( QString(), { attribute } ) );
}
connect( item, &QObject::destroyed,
this, &QskInputPanel::inputItemDestroyed,
Qt::UniqueConnection );
}
else
{
attachItem( nullptr );
}
}
void QskInputPanel::updateInputPanel( Qt::InputMethodQueries queries )
{
if ( m_data->inputItem == nullptr )
return;
QInputMethodQueryEvent event( queries );
QCoreApplication::sendEvent( m_data->inputItem, &event );
if ( queries & Qt::ImHints )
{
m_data->inputHints = static_cast< Qt::InputMethodHints >(
event.value( Qt::ImHints ).toInt() );
setPredictionEnabled(
m_data->predictor && qskUsePrediction( m_data->inputHints ) );
}
if ( queries & Qt::ImPreferredLanguage )
{
setLocale( event.value( Qt::ImPreferredLanguage ).toLocale() );
}
if ( queries & Qt::ImInputItemClipRectangle )
{
/*
Could be used to move the panel,
so that it does not hide the item.
*/
}
}
void QskInputPanel::updateLocale( const QLocale& locale )
{
if ( !m_data->hasPredictorLocale || locale != m_data->predictorLocale )
{
m_data->hasPredictorLocale = true;
m_data->predictorLocale = locale;
resetPredictor( locale );
m_data->keyProcessor.reset();
}
}
void QskInputPanel::resetPredictor( const QLocale& locale )
{
auto predictor = QskInputContext::instance()->textPredictor( locale );
if ( predictor == m_data->predictor )
return;
m_data->predictor = predictor;
if ( predictor )
{
// text predictor lives in another thread, so these will all be QueuedConnections:
connect( this, &QskInputPanel::predictionReset,
predictor.get(), &QskTextPredictor::reset );
connect( this, &QskInputPanel::predictionRequested,
predictor.get(), &QskTextPredictor::request );
connect( predictor.get(), &QskTextPredictor::predictionChanged,
this, &QskInputPanel::updatePrediction );
}
setPredictionEnabled(
predictor && qskUsePrediction( m_data->inputHints ) );
}
void QskInputPanel::commitPredictiveText( int index )
{
QString text;
if ( m_data->predictor )
{
text = m_data->candidates.at( index );
Q_EMIT predictionReset();
}
m_data->keyProcessor.reset();
setPrediction( {} );
qskSendText( qskReceiverItem( this ), text, true );
}
void QskInputPanel::updatePrediction( const QString& text, const QStringList& candidates )
{
if ( m_data->predictor )
{
if( m_data->keyProcessor.preedit() != text )
{
// This must be for another input panel
return;
}
setPrediction( candidates );
m_data->keyProcessor.continueProcessingKey( candidates );
}
else
{
qWarning() << "got prediction update, but no predictor. Something is wrong";
}
}
QQuickItem* QskInputPanel::inputProxy() const
{
return nullptr;
}
QQuickItem* QskInputPanel::inputItem() const
{
return m_data->inputItem;
}
void QskInputPanel::setPrompt( const QString& )
{
}
void QskInputPanel::setPredictionEnabled( bool )
{
}
void QskInputPanel::setPrediction(const QStringList& candidates )
{
m_data->candidates = candidates;
}
Qt::Alignment QskInputPanel::alignment() const
{
/*
When we have an input proxy, we don't care if
the input item becomes hidden
*/
return inputProxy() ? Qt::AlignVCenter : Qt::AlignBottom;
}
QStringList QskInputPanel::candidates() const
{
return m_data->candidates;
}
void QskInputPanel::commitKey( int key )
{
if ( m_data->inputItem == nullptr )
return;
int spaceLeft = -1;
if ( !( m_data->inputHints & Qt::ImhMultiLine ) )
{
QInputMethodQueryEvent event1( Qt::ImMaximumTextLength );
QCoreApplication::sendEvent( m_data->inputItem, &event1 );
const int maxChars = event1.value( Qt::ImMaximumTextLength ).toInt();
if ( maxChars >= 0 )
{
QInputMethodQueryEvent event2( Qt::ImSurroundingText );
QCoreApplication::sendEvent( qskReceiverItem( this ), &event2 );
const auto text = event2.value( Qt::ImSurroundingText ).toString();
spaceLeft = maxChars - text.length();
}
}
QskTextPredictor* predictor = nullptr;
if ( qskUsePrediction( m_data->inputHints ) )
predictor = m_data->predictor.get(); // ### we could also make the predictor member of keyProcessor a shared ptr?
m_data->keyProcessor.processKey(
key, m_data->inputHints, this, predictor, spaceLeft );
}
#include "moc_QskInputPanel.cpp"
#include "QskInputPanel.moc"