diff --git a/playground/inputpanel/main.cpp b/playground/inputpanel/main.cpp index 3f6c611b..e05560c0 100644 --- a/playground/inputpanel/main.cpp +++ b/playground/inputpanel/main.cpp @@ -138,7 +138,8 @@ public: textInput3->setSizePolicy( Qt::Horizontal, QskSizePolicy::Preferred ); auto* textInput4 = new QskTextInput( this ); - textInput4->setEchoMode( QskTextInput::PasswordEchoOnEdit ); + textInput4->setEchoMode( QskTextInput::Password ); + textInput4->setPasswordMaskDelay( 1000 ); textInput4->setMaxLength( 8 ); textInput4->setText( "12345678" ); textInput4->setSizePolicy( Qt::Horizontal, QskSizePolicy::Preferred ); @@ -248,8 +249,10 @@ int main( int argc, char* argv[] ) QskObjectCounter counter( true ); #endif +#if 1 qputenv( "QT_IM_MODULE", "skinny" ); qputenv( "QT_PLUGIN_PATH", STRING( PLUGIN_PATH ) ); +#endif QGuiApplication app( argc, argv ); diff --git a/src/controls/QskControl.cpp b/src/controls/QskControl.cpp index 88bca8a4..83e0190b 100644 --- a/src/controls/QskControl.cpp +++ b/src/controls/QskControl.cpp @@ -170,17 +170,14 @@ void qskForceActiveFocus( QQuickItem* item, Qt::FocusReason reason ) } } -void qskUpdateInputMethod( const QQuickItem* item, Qt::InputMethodQueries queries ) +QQuickItem* qskInputContextItem() { - if ( ( item == nullptr ) || - !( item->flags() & QQuickItem::ItemAcceptsInputMethod ) ) - { - return; - } + /* + The item that is connected to the input context. + - often the item having the active focus. + */ - auto inputMethod = QGuiApplication::inputMethod(); - - bool doUpdate = item->hasActiveFocus(); + QQuickItem* inputItem = nullptr; /* We could also get the inputContext from QInputMethodPrivate @@ -197,17 +194,32 @@ void qskUpdateInputMethod( const QQuickItem* item, Qt::InputMethodQueries querie without losing the connected input item */ - QQuickItem* inputItem = nullptr; - - if ( QMetaObject::invokeMethod( inputContext, "inputItem", - Qt::DirectConnection, Q_RETURN_ARG( QQuickItem*, inputItem ) ) ) - { - doUpdate = ( item == inputItem ); - } + QMetaObject::invokeMethod( inputContext, "inputItem", + Qt::DirectConnection, Q_RETURN_ARG( QQuickItem*, inputItem ) ); } + return inputItem; +} + +void qskUpdateInputMethod( const QQuickItem* item, Qt::InputMethodQueries queries ) +{ + if ( item == nullptr ) + return; + +#if 1 + if ( !( item->flags() & QQuickItem::ItemAcceptsInputMethod ) ) + return; +#endif + + bool doUpdate; + + if ( const QQuickItem* inputItem = qskInputContextItem() ) + doUpdate = ( item == inputItem ); + else + doUpdate = item->hasActiveFocus(); + if ( doUpdate ) - inputMethod->update( queries ); + QGuiApplication::inputMethod()->update( queries ); } QList< QQuickItem* > qskPaintOrderChildItems( const QQuickItem* item ) diff --git a/src/controls/QskControl.h b/src/controls/QskControl.h index 3a0810e1..4c68781f 100644 --- a/src/controls/QskControl.h +++ b/src/controls/QskControl.h @@ -265,6 +265,7 @@ QSK_EXPORT void qskForceActiveFocus( QQuickItem*, Qt::FocusReason ); QSK_EXPORT QList< QQuickItem* > qskPaintOrderChildItems( const QQuickItem* ); QSK_EXPORT void qskUpdateInputMethod( const QQuickItem*, Qt::InputMethodQueries ); +QSK_EXPORT QQuickItem* qskInputContextItem(); QSK_EXPORT const QSGNode* qskItemNode( const QQuickItem* ); QSK_EXPORT const QSGNode* qskPaintNode( const QQuickItem* ); diff --git a/src/controls/QskTextInput.cpp b/src/controls/QskTextInput.cpp index 33344186..49e9c952 100644 --- a/src/controls/QskTextInput.cpp +++ b/src/controls/QskTextInput.cpp @@ -62,6 +62,12 @@ static inline void qskBindSignals( const QQuickTextInput* wrappedInput, QObject::connect( wrappedInput, &QQuickTextInput::echoModeChanged, input, [ input ] { input->Q_EMIT echoModeChanged( input->echoMode() ); } ); + QObject::connect( wrappedInput, &QQuickTextInput::passwordCharacterChanged, + input, &QskTextInput::passwordCharacterChanged ); + + QObject::connect( wrappedInput, &QQuickTextInput::passwordMaskDelayChanged, + input, &QskTextInput::passwordMaskDelayChanged ); + QObject::connect( wrappedInput, &QQuickItem::implicitWidthChanged, input, &QskControl::resetImplicitSize ); @@ -381,22 +387,18 @@ void QskTextInput::focusInEvent( QFocusEvent* event ) void QskTextInput::focusOutEvent( QFocusEvent* event ) { -#if 1 - if ( event->reason() != Qt::ActiveWindowFocusReason - && event->reason() != Qt::PopupFocusReason ) + switch( event->reason() ) { - m_data->textInput->deselect(); - } -#endif - - if ( m_data->activationModes & ActivationOnFocus ) - { -#if 0 - if ( !hasFocus() ) + case Qt::ActiveWindowFocusReason: + case Qt::PopupFocusReason: { + break; + } + default: + { + m_data->textInput->deselect(); setEditing( false ); } -#endif } Inherited::focusOutEvent( event ); @@ -655,6 +657,37 @@ void QskTextInput::setEchoMode( EchoMode mode ) } } +QString QskTextInput::passwordCharacter() const +{ + return m_data->textInput->passwordCharacter(); +} + +void QskTextInput::setPasswordCharacter( const QString& text ) +{ + m_data->textInput->setPasswordCharacter( text ); +} + +void QskTextInput::resetPasswordCharacter() +{ + m_data->textInput->setPasswordCharacter( + QGuiApplication::styleHints()->passwordMaskCharacter() ); +} + +int QskTextInput::passwordMaskDelay() const +{ + return m_data->textInput->passwordMaskDelay(); +} + +void QskTextInput::setPasswordMaskDelay( int ms ) +{ + return m_data->textInput->setPasswordMaskDelay( ms ); +} + +void QskTextInput::resetPasswordMaskDelay() +{ + return m_data->textInput->resetPasswordMaskDelay(); +} + QString QskTextInput::displayText() const { return m_data->textInput->displayText(); diff --git a/src/controls/QskTextInput.h b/src/controls/QskTextInput.h index 36753896..76a474a5 100644 --- a/src/controls/QskTextInput.h +++ b/src/controls/QskTextInput.h @@ -31,6 +31,14 @@ class QSK_EXPORT QskTextInput : public QskControl Q_PROPERTY( bool editing READ isEditing WRITE setEditing NOTIFY editingChanged ) + Q_PROPERTY( QString passwordCharacter READ passwordCharacter + WRITE setPasswordCharacter RESET resetPasswordCharacter + NOTIFY passwordCharacterChanged ) + + Q_PROPERTY( int passwordMaskDelay READ passwordMaskDelay + WRITE setPasswordMaskDelay RESET resetPasswordMaskDelay + NOTIFY passwordMaskDelayChanged ) + using Inherited = QskControl; public: @@ -103,8 +111,15 @@ public: EchoMode echoMode() const; void setEchoMode( EchoMode ); - QString displayText() const; + QString passwordCharacter() const; + void setPasswordCharacter( const QString& ); + void resetPasswordCharacter(); + int passwordMaskDelay() const; + void setPasswordMaskDelay( int ); + void resetPasswordMaskDelay(); + + QString displayText() const; QString preeditText() const; #if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0) @@ -152,7 +167,10 @@ Q_SIGNALS: #endif void maximumLengthChanged( int ); + void echoModeChanged( EchoMode ); + void passwordMaskDelayChanged(); + void passwordCharacterChanged(); void validatorChanged(); void inputMaskChanged( const QString& ); diff --git a/src/inputpanel/QskInputContext.cpp b/src/inputpanel/QskInputContext.cpp index 6b54112d..ced9a773 100644 --- a/src/inputpanel/QskInputContext.cpp +++ b/src/inputpanel/QskInputContext.cpp @@ -278,7 +278,9 @@ void QskInputContext::showInputPanel() inputPopup->setOverlay( hasInputProxy ); - if ( !hasInputProxy ) + if ( hasInputProxy ) + box->setMargins( QMarginsF( 5, 5, 5, 5 ) ); + else box->setExtraSpacingAt( Qt::TopEdge | Qt::LeftEdge | Qt::RightEdge ); } diff --git a/src/inputpanel/QskInputEngine.cpp b/src/inputpanel/QskInputEngine.cpp index ac79e398..19e07fed 100644 --- a/src/inputpanel/QskInputEngine.cpp +++ b/src/inputpanel/QskInputEngine.cpp @@ -36,6 +36,16 @@ static inline QString qskKeyString( int keyCode ) return QChar( keyCode ); } +static inline bool qskUsePrediction( Qt::InputMethodHints hints ) +{ + constexpr Qt::InputMethodHints mask = + Qt::ImhNoPredictiveText | Qt::ImhHiddenText + | Qt::ImhDialableCharactersOnly | Qt::ImhEmailCharactersOnly + | Qt::ImhUrlCharactersOnly; + + return ( hints & mask ) == 0; +} + class QskInputEngine::PrivateData { public: @@ -114,7 +124,7 @@ QskInputEngine::Result QskInputEngine::processKey( int key, auto& preedit = m_data->preedit; QskTextPredictor* predictor = nullptr; - if ( !( inputHints & Qt::ImhHiddenText ) ) + if ( qskUsePrediction( inputHints ) ) predictor = m_data->predictor; /* @@ -175,6 +185,7 @@ QskInputEngine::Result QskInputEngine::processKey( int key, { if ( !preedit.isEmpty() && spaceLeft) { + preedit += qskKeyString( key ); preedit = preedit.left( spaceLeft ); result.text = preedit; diff --git a/src/inputpanel/QskInputPanel.cpp b/src/inputpanel/QskInputPanel.cpp index 2c68ee48..d7483951 100644 --- a/src/inputpanel/QskInputPanel.cpp +++ b/src/inputpanel/QskInputPanel.cpp @@ -74,15 +74,59 @@ static inline void qskSendKey( QQuickItem* receiver, int key ) QCoreApplication::sendEvent( receiver, &keyRelease ); } +static inline void qskSyncInputProxy( + QQuickItem* inputItem, QskTextInput* inputProxy ) +{ + int passwordMaskDelay = -1; + QString passwordCharacter; + + if ( auto textInput = qobject_cast< QskTextInput* >( inputItem ) ) + { + passwordMaskDelay = textInput->passwordMaskDelay(); + passwordCharacter = textInput->passwordCharacter(); + + if ( inputProxy->echoMode() == QskTextInput::NoEcho ) + { + /* + Qt::ImhHiddenText does not provide information + to decide between NoEcho/Password + */ + auto mode = textInput->echoMode(); + if ( mode == QskTextInput::Password ) + inputProxy->setEchoMode( mode ); + } + } + + if ( passwordMaskDelay >= 0 ) + inputProxy->setPasswordMaskDelay( passwordMaskDelay ); + else + inputProxy->resetPasswordMaskDelay(); + + if ( !passwordCharacter.isEmpty() ) + inputProxy->setPasswordCharacter( passwordCharacter ); + else + inputProxy->resetPasswordCharacter(); +} + namespace { - class TextInput : public QskTextInput + class TextInput final : public QskTextInput { public: TextInput( QQuickItem* parentItem = nullptr ): QskTextInput( parentItem ) { setObjectName( "InputPanelInputProxy" ); + setFocusPolicy( Qt::NoFocus ); + } + + protected: + virtual void focusInEvent( QFocusEvent* ) override final + { + } + + virtual void focusOutEvent( QFocusEvent* ) override final + { } }; } @@ -198,7 +242,19 @@ void QskInputPanel::attachInputItem( QQuickItem* item ) processInputMethodQueries( queries ); if ( m_data->hasInputProxy ) + { m_data->inputProxy->setEditing( true ); + + // hiding the cursor in the real input item + const QInputMethodEvent::Attribute attribute( + QInputMethodEvent::Cursor, 0, 0, QVariant() ); + + QInputMethodEvent event( QString(), { attribute } ); + QCoreApplication::sendEvent( item, &event ); + + // not all information is available from the input method query + qskSyncInputProxy( item, m_data->inputProxy ); + } } } @@ -351,17 +407,17 @@ void QskInputPanel::processInputMethodQueries( Qt::InputMethodQueries queries ) QInputMethodQueryEvent event( queries ); QCoreApplication::sendEvent( m_data->inputItem, &event ); - if ( queries & Qt::ImHints ) + if ( event.queries() & Qt::ImHints ) { bool hasPrediction = true; - bool hasEchoMode = false; + QskTextInput::EchoMode echoMode = QskTextInput::Normal; const auto hints = static_cast< Qt::InputMethodHints >( event.value( Qt::ImHints ).toInt() ); if ( hints & Qt::ImhHiddenText ) { - hasEchoMode = true; + echoMode = QskTextInput::NoEcho; } if ( hints & Qt::ImhSensitiveData ) @@ -415,11 +471,13 @@ void QskInputPanel::processInputMethodQueries( Qt::InputMethodQueries queries ) if ( hints & Qt::ImhDigitsOnly ) { // using a numpad instead of our virtual keyboard + hasPrediction = false; } if ( hints & Qt::ImhFormattedNumbersOnly ) { // a numpad with decimal point and minus sign + hasPrediction = false; } if ( hints & Qt::ImhUppercaseOnly ) @@ -435,16 +493,19 @@ void QskInputPanel::processInputMethodQueries( Qt::InputMethodQueries queries ) if ( hints & Qt::ImhDialableCharactersOnly ) { // characters suitable for phone dialing + hasPrediction = false; } if ( hints & Qt::ImhEmailCharactersOnly ) { // characters suitable for email addresses + hasPrediction = false; } if ( hints & Qt::ImhUrlCharactersOnly ) { // characters suitable for URLs + hasPrediction = false; } if ( hints & Qt::ImhLatinOnly ) @@ -457,18 +518,17 @@ void QskInputPanel::processInputMethodQueries( Qt::InputMethodQueries queries ) m_data->predictionBar->setVisible( hasPrediction && m_data->engine && m_data->engine->predictor() ); - m_data->inputProxy->setEchoMode( - hasEchoMode ? QskTextInput::PasswordEchoOnEdit : QskTextInput::Normal ); + m_data->inputProxy->setEchoMode( echoMode ); m_data->inputHints = hints; } - if ( queries & Qt::ImPreferredLanguage ) + if ( event.queries() & Qt::ImPreferredLanguage ) { // already handled by the input context } - if ( queries & Qt::ImMaximumTextLength ) + if ( event.queries() & Qt::ImMaximumTextLength ) { // needs to be handled before Qt::ImCursorPosition ! @@ -483,7 +543,7 @@ void QskInputPanel::processInputMethodQueries( Qt::InputMethodQueries queries ) } - if ( queries & Qt::ImSurroundingText ) + if ( event.queries() & Qt::ImSurroundingText ) { if ( m_data->hasInputProxy ) { @@ -492,7 +552,7 @@ void QskInputPanel::processInputMethodQueries( Qt::InputMethodQueries queries ) } } - if ( queries & Qt::ImCursorPosition ) + if ( event.queries() & Qt::ImCursorPosition ) { if ( m_data->hasInputProxy ) { @@ -501,7 +561,7 @@ void QskInputPanel::processInputMethodQueries( Qt::InputMethodQueries queries ) } } - if ( queries & Qt::ImCurrentSelection ) + if ( event.queries() & Qt::ImCurrentSelection ) { #if 0 const auto text = event.value( Qt::ImCurrentSelection ).toString();