/****************************************************************************** * Qsk Controls * Copyright (C) 2016 Uwe Rathmann * * This file may be used under the terms of the 3-clause BSD License *****************************************************************************/ #include "QskInputPanel.h" #include "QskAspect.h" #include #include #include #include #include #include QSK_SUBCONTROL( QskInputPanel, Panel ) QSK_SUBCONTROL( QskInputPanel, KeyFrame ) QSK_SUBCONTROL( QskInputPanel, KeyGlyph ) QSK_STATE( QskInputPanel, Checked, QskAspect::LastSystemState >> 3 ) QSK_STATE( QskInputPanel, Pressed, QskAspect::LastSystemState >> 2 ) namespace { struct KeyTable { using Row = QskInputPanel::KeyData[ QskInputPanel::KeyCount ]; Row data[ QskInputPanel::RowCount ]; int indexOf( const QskInputPanel::KeyData* value ) const { return int( intptr_t( value - data[0] ) ); } }; QskInputPanel::KeyData* qskKeyDataAt( KeyTable& table, qreal x, qreal y ) { if ( x < 0 || x > 1 || y < 0 || y > 1 ) return nullptr; auto rowIndex = size_t( std::floor( y * QskInputPanel::RowCount ) ); auto columnIndex = size_t( std::floor( x * QskInputPanel::KeyCount ) ); Q_FOREVER { const auto rect = table.data[ rowIndex ][ columnIndex ].rect; if ( rect.contains( x, y ) ) return &table.data[ rowIndex ][ columnIndex ]; // Look up/down if ( y < rect.top() ) { if ( rowIndex == 0 ) break; rowIndex -= 1; continue; } else if ( y > rect.bottom() ) { if ( rowIndex == QskInputPanel::RowCount - 1 ) break; rowIndex += 1; continue; } // Look left/right if ( x < rect.left() ) { if ( columnIndex == 0 ) break; columnIndex -= 1; continue; } else if ( x > rect.right() ) { if ( columnIndex == QskInputPanel::KeyCount - 1 ) break; columnIndex += 1; continue; } } return nullptr; } } struct QskInputPanelLayouts { struct KeyCodes { using Row = Qt::Key[ QskInputPanel::KeyCount ]; Row data[ QskInputPanel::RowCount ]; }; using Layout = KeyCodes[ QskInputPanel::ModeCount ]; Layout bg; // Bulgarian Layout cs; // Czech Layout de; // German Layout da; // Danish Layout el; // Greek Layout en_GB; // English (GB) Layout en_US; // English (US) Layout es; // Spanish Layout fi; // Finnish Layout fr; // French Layout hu; // Hungarian Layout it; // Italian Layout ja; // Japanese Layout lv; // Latvian Layout lt; // Lithuanian Layout nl; // Dutch Layout pt; // Portuguese Layout ro; // Romanian Layout ru; // Russian Layout sl; // Slovene Layout sk; // Slovak Layout tr; // Turkish Layout zh; // Chinese Q_GADGET }; #define LOWER(x) Qt::Key(x + 32) // Convert an uppercase key to lowercase static constexpr const QskInputPanelLayouts qskInputPanelLayouts = { #include "QskInputPanelLayouts.cpp" }; #undef LOWER QSK_DECLARE_OPERATORS_FOR_FLAGS( Qt::Key ) // Must appear after the LOWER macro static const Qt::Key KeyPressed = static_cast< Qt::Key >( Qt::ShiftModifier ); static const Qt::Key KeyLocked = static_cast< Qt::Key >( Qt::ControlModifier ); static const Qt::Key KeyFocused = static_cast< Qt::Key >( Qt::MetaModifier ); static const Qt::Key KeyStates = static_cast< Qt::Key >( Qt::KeyboardModifierMask ); static qreal qskKeyStretch( Qt::Key key ) { switch ( key ) { case Qt::Key_Backspace: case Qt::Key_Shift: case Qt::Key_CapsLock: return 1.5; case Qt::Key_Space: return 3.5; case Qt::Key_Return: case Qt::Key_Mode_switch: // Possibly smaller default: break; } return 1.0; } static qreal qskRowStretch( const QskInputPanel::KeyRow& keyRow ) { qreal stretch = 0; for ( const auto& key : keyRow ) { if ( !key ) continue; stretch += qskKeyStretch( key ); } return stretch; } static inline int qskKeyCount( const QskInputPanel::KeyDataRow& keyRow ) { int keyCount = QskInputPanel::KeyCount; while ( !keyRow[ keyCount - 1 ].key ) --keyCount; return keyCount; } struct KeyCounter { int keyIndex; int count; }; class QskInputPanel::PrivateData { public: PrivateData(): currentLayout( nullptr ), mode( QskInputPanel::LowercaseMode ), focusKeyIndex( -1 ), selectedGroup( -1 ), candidateOffset( 0 ), repeatKeyTimerId( -1 ) { } public: const QskInputPanelLayouts::Layout* currentLayout; QskInputPanel::Mode mode; qint16 focusKeyIndex; qint16 selectedGroup; qint32 candidateOffset; int repeatKeyTimerId; QLocale locale; QVector< Qt::Key > groups; QVector< Qt::Key > candidates; std::unordered_map< int, KeyCounter > activeKeys; KeyTable keyTable[ ModeCount ]; }; QskInputPanel::QskInputPanel( QQuickItem* parent ): QskControl( parent ), m_data( new PrivateData ) { setFlag( ItemHasContents ); setAcceptedMouseButtons( Qt::MouseButtonMask ); initSizePolicy( QskSizePolicy::Expanding, QskSizePolicy::Expanding ); updateLocale( locale() ); QObject::connect( this, &QskControl::localeChanged, this, &QskInputPanel::updateLocale ); } QskInputPanel::~QskInputPanel() { } QskInputPanel::Mode QskInputPanel::mode() const { return m_data->mode; } const QskInputPanel::KeyDataSet& QskInputPanel::keyData( Mode mode ) const { mode = mode == CurrentMode ? m_data->mode : mode; Q_ASSERT( mode >= 0 && mode < ModeCount ); return m_data->keyTable[ mode ].data; } QString QskInputPanel::textForKey( Qt::Key key ) const { key &= ~KeyStates; // Special cases switch ( key ) { case Qt::Key_Backspace: case Qt::Key_Muhenkan: return QChar( 0x232B ); case Qt::Key_CapsLock: case Qt::Key_Kana_Lock: return QChar( 0x21E7 ); case Qt::Key_Shift: case Qt::Key_Kana_Shift: return QChar( 0x2B06 ); case Qt::Key_Mode_switch: return QChar( 0x2026 ); case Qt::Key_Return: case Qt::Key_Kanji: return QChar( 0x23CE ); case Qt::Key_Left: return QChar( 0x2190 ); case Qt::Key_Right: return QChar( 0x2192 ); case Qt::Key_ApplicationLeft: return QChar( 0x2B05 ); case Qt::Key_ApplicationRight: return QChar( 0x27A1 ); default: break; } // TODO: possibly route through locale for custom strings // Default to direct key mapping return QChar( key ); } QString QskInputPanel::displayLanguageName() const { const auto locale = this->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)" ); } break; } 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: break; } return QLocale::languageToString( locale.language() ); } void QskInputPanel::setPreeditGroups( const QVector< Qt::Key >& groups ) { auto& topRow = m_data->keyTable[ LowercaseMode ].data[ 0 ]; for ( const auto& group : groups ) { auto& keyData = topRow[ &group - groups.data() ]; keyData.key = group; } m_data->groups = groups; selectGroup( -1 ); if ( m_data->mode == LowercaseMode ) update(); } void QskInputPanel::setPreeditCandidates( const QVector< Qt::Key >& candidates ) { if ( m_data->candidates == candidates ) return; m_data->candidates = candidates; setCandidateOffset( 0 ); } bool QskInputPanel::advanceFocus( bool forward ) { deactivateFocusKey(); auto offset = forward ? 1 : -1; auto focusKeyIndex = m_data->focusKeyIndex; Q_FOREVER { focusKeyIndex += offset; if ( focusKeyIndex < 0 || focusKeyIndex >= RowCount * KeyCount ) { clearFocusKey(); return false; } const auto key = keyDataAt( focusKeyIndex ).key; if ( key && key != Qt::Key_unknown ) break; } if ( m_data->focusKeyIndex >= 0 ) keyDataAt( m_data->focusKeyIndex ).key &= ~KeyFocused; m_data->focusKeyIndex = focusKeyIndex; keyDataAt( m_data->focusKeyIndex ).key |= KeyFocused; update(); return true; } bool QskInputPanel::activateFocusKey() { if ( m_data->focusKeyIndex > 0 && m_data->focusKeyIndex < RowCount * KeyCount ) { auto& keyData = keyDataAt( m_data->focusKeyIndex ); if ( keyData.key & KeyPressed ) handleKey( m_data->focusKeyIndex ); else keyData.key |= KeyPressed; update(); return true; } return false; } bool QskInputPanel::deactivateFocusKey() { if ( m_data->focusKeyIndex > 0 && m_data->focusKeyIndex < RowCount * KeyCount ) { auto& keyData = keyDataAt( m_data->focusKeyIndex ); if ( keyData.key & KeyPressed ) { keyData.key &= ~KeyPressed; handleKey( m_data->focusKeyIndex ); } update(); return true; } return true; } void QskInputPanel::clearFocusKey() { if ( m_data->focusKeyIndex > 0 && m_data->focusKeyIndex < RowCount * KeyCount ) { keyDataAt( m_data->focusKeyIndex ).key &= ~KeyFocused; update(); } m_data->focusKeyIndex = -1; } void QskInputPanel::setCandidateOffset( int candidateOffset ) { m_data->candidateOffset = candidateOffset; auto& topRow = m_data->keyTable[ LowercaseMode ].data[ 0 ]; const auto groupCount = m_data->groups.length(); const auto candidateCount = m_data->candidates.length(); const auto count = std::min( candidateCount, KeyCount - groupCount ); const bool continueLeft = m_data->candidateOffset > 0; const bool continueRight = ( candidateCount - m_data->candidateOffset ) > count; for ( int i = 0; i < count; ++i ) { auto& keyData = topRow[ i + groupCount ]; if ( continueLeft && i == 0 ) keyData.key = Qt::Key_ApplicationLeft; else if ( continueRight && ( i == KeyCount - groupCount - 1 ) ) keyData.key = Qt::Key_ApplicationRight; else keyData.key = m_data->candidates.at( i + m_data->candidateOffset ); } for ( int i = count; i < KeyCount - groupCount; ++i ) { auto& keyData = topRow[ i + groupCount ]; keyData.key = Qt::Key_unknown; } if ( m_data->mode == LowercaseMode ) update(); } QRectF QskInputPanel::keyboardRect() const { auto keyboardRect = boundingRect(); // ### margins? would eliminate this thing below if ( QskDialog::instance()->policy() != QskDialog::TopLevelWindow ) keyboardRect.adjust( 0, keyboardRect.height() * 0.5, 0, 0 ); return keyboardRect; } void QskInputPanel::geometryChanged( const QRectF& newGeometry, const QRectF& oldGeometry ) { Inherited::geometryChanged( newGeometry, oldGeometry ); Q_EMIT keyboardRectChanged(); } void QskInputPanel::mousePressEvent( QMouseEvent* e ) { if ( !keyboardRect().contains( e->pos() ) ) { e->ignore(); return; } QTouchEvent::TouchPoint touchPoint( 0 ); touchPoint.setPos( e->pos() ); touchPoint.setState( Qt::TouchPointPressed ); QTouchEvent touchEvent( QTouchEvent::TouchBegin, nullptr, e->modifiers(), Qt::TouchPointPressed, { touchPoint } ); QCoreApplication::sendEvent( this, &touchEvent ); e->setAccepted( touchEvent.isAccepted() ); } void QskInputPanel::mouseMoveEvent( QMouseEvent* e ) { QTouchEvent::TouchPoint touchPoint( 0 ); touchPoint.setPos( e->pos() ); touchPoint.setState( Qt::TouchPointMoved ); QTouchEvent touchEvent( QTouchEvent::TouchUpdate, nullptr, e->modifiers(), Qt::TouchPointMoved, { touchPoint } ); QCoreApplication::sendEvent( this, &touchEvent ); e->setAccepted( touchEvent.isAccepted() ); } void QskInputPanel::mouseReleaseEvent( QMouseEvent* e ) { QTouchEvent::TouchPoint touchPoint( 0 ); touchPoint.setPos( e->pos() ); touchPoint.setState( Qt::TouchPointReleased ); QTouchEvent touchEvent( QTouchEvent::TouchEnd, nullptr, e->modifiers(), Qt::TouchPointReleased, { touchPoint } ); QCoreApplication::sendEvent( this, &touchEvent ); e->setAccepted( touchEvent.isAccepted() ); } // Try to handle touch-specific details here; once touch is resolved, send to handleKey() void QskInputPanel::touchEvent( QTouchEvent* e ) { if ( e->type() == QEvent::TouchCancel ) { for ( auto& it : m_data->activeKeys ) keyDataAt( it.second.keyIndex ).key &= ~KeyPressed; m_data->activeKeys.clear(); return; } const auto rect = keyboardRect(); for ( const auto& tp : e->touchPoints() ) { const auto pos = tp.pos(); const auto x = ( pos.x() - rect.x() ) / rect.width(); const auto y = ( pos.y() - rect.y() ) / rect.height(); auto keyData = qskKeyDataAt( m_data->keyTable[ m_data->mode ], x, y ); if ( !keyData || ( !keyData->key || keyData->key == Qt::Key_unknown ) ) { auto it = m_data->activeKeys.find( tp.id() ); if ( it == m_data->activeKeys.cend() ) continue; keyDataAt( it->second.keyIndex ).key &= ~KeyPressed; m_data->activeKeys.erase( it ); continue; } const auto keyIndex = m_data->keyTable[ m_data->mode ].indexOf( keyData ); auto it = m_data->activeKeys.find( tp.id() ); if ( tp.state() == Qt::TouchPointReleased ) { const int repeatCount = it->second.count; auto it = m_data->activeKeys.find( tp.id() ); keyDataAt( it->second.keyIndex ).key &= ~KeyPressed; m_data->activeKeys.erase( it ); if ( repeatCount < 0 ) continue; // Don't compose an accepted held key handleKey( keyIndex ); continue; } if ( it == m_data->activeKeys.end() ) { m_data->activeKeys.emplace( tp.id(), KeyCounter { keyIndex, 0 } ); } else { if ( it->second.keyIndex != keyIndex ) { keyDataAt( it->second.keyIndex ).key &= ~KeyPressed; it->second.count = 0; } it->second.keyIndex = keyIndex; } keyDataAt( keyIndex ).key |= KeyPressed; } // Now start an update timer based on active keys if ( m_data->activeKeys.empty() && m_data->repeatKeyTimerId >= 0 ) { killTimer( m_data->repeatKeyTimerId ); m_data->repeatKeyTimerId = -1; } else if ( m_data->repeatKeyTimerId < 0 ) { m_data->repeatKeyTimerId = startTimer( 1000 / QGuiApplication::styleHints()->keyboardAutoRepeatRate() ); } /* else timer is already running as it should be */ update(); } void QskInputPanel::timerEvent( QTimerEvent* e ) { if ( e->timerId() == m_data->repeatKeyTimerId ) { for ( auto it = m_data->activeKeys.begin(); it != m_data->activeKeys.end(); ) { if ( it->second.count >= 0 && it->second.count++ > 20 ) // ### use platform long-press hint { const auto key = keyDataAt( it->second.keyIndex ).key & ~KeyStates; if ( !key || key == Qt::Key_unknown ) { it = m_data->activeKeys.erase( it ); continue; } if ( key == Qt::Key_ApplicationLeft || key == Qt::Key_ApplicationRight ) { setCandidateOffset( m_data->candidateOffset + ( key == Qt::Key_ApplicationLeft ? -1 : 1 ) ); } else if ( !( key & KeyLocked ) ) // do not repeat locked keys { // long press events could be emitted from here compose( key & ~KeyStates ); } } ++it; } } } QskInputPanel::KeyData& QskInputPanel::keyDataAt( int keyIndex ) { const auto row = keyIndex / KeyCount; const auto col = keyIndex % KeyCount; return m_data->keyTable[ m_data->mode ].data[ row ][ col ]; } void QskInputPanel::handleKey( int keyIndex ) { const auto key = keyDataAt( keyIndex ).key & ~KeyStates; // Preedit keys const auto row = keyIndex / KeyCount; const auto column = keyIndex % KeyCount; if ( m_data->mode == LowercaseMode && !m_data->groups.isEmpty() && row == 0 ) { if ( key == Qt::Key_ApplicationLeft || key == Qt::Key_ApplicationRight ) { setCandidateOffset( m_data->candidateOffset + ( key == Qt::Key_ApplicationLeft ? -1 : 1 ) ); return; } const auto groupCount = m_data->groups.length(); if ( column < groupCount ) selectGroup( column ); else if ( column < KeyCount ) selectCandidate( column - groupCount + m_data->candidateOffset ); else Q_UNREACHABLE(); // Handle the final key... return; } // Mode-switching keys switch ( key ) { case Qt::Key_CapsLock: case Qt::Key_Kana_Lock: setMode( UppercaseMode ); // Lock caps return; case Qt::Key_Shift: case Qt::Key_Kana_Shift: setMode( LowercaseMode ); // Unlock caps return; case Qt::Key_Mode_switch: // Cycle through modes, but skip caps setMode( static_cast< QskInputPanel::Mode >( m_data->mode ? ( ( m_data->mode + 1 ) % QskInputPanel::ModeCount ) : SpecialCharacterMode ) ); return; default: break; } // Normal keys compose( key ); } void QskInputPanel::compose( Qt::Key key ) { QGuiApplication::inputMethod()->invokeAction( static_cast< QInputMethod::Action >( Compose ), key ); } void QskInputPanel::selectGroup( int index ) { auto& topRow = m_data->keyTable[ m_data->mode ].data[ 0 ]; if ( m_data->selectedGroup >= 0 ) topRow[ m_data->selectedGroup ].key &= ~KeyLocked; if ( m_data->selectedGroup == index ) index = -1; // clear selection m_data->selectedGroup = index; QGuiApplication::inputMethod()->invokeAction( static_cast< QInputMethod::Action >( SelectGroup ), m_data->selectedGroup + 1 ); if ( m_data->selectedGroup < 0 ) return; topRow[ m_data->selectedGroup ].key |= KeyLocked; } void QskInputPanel::selectCandidate( int index ) { QGuiApplication::inputMethod()->invokeAction( static_cast< QInputMethod::Action >( SelectCandidate ), index ); } void QskInputPanel::updateLocale( const QLocale& locale ) { switch ( locale.language() ) { case QLocale::Bulgarian: m_data->currentLayout = &qskInputPanelLayouts.bg; break; case QLocale::Czech: m_data->currentLayout = &qskInputPanelLayouts.cs; break; case QLocale::German: m_data->currentLayout = &qskInputPanelLayouts.de; break; case QLocale::Danish: m_data->currentLayout = &qskInputPanelLayouts.da; break; case QLocale::Greek: m_data->currentLayout = &qskInputPanelLayouts.el; break; case QLocale::English: { switch ( locale.country() ) { case QLocale::Canada: case QLocale::UnitedStates: case QLocale::UnitedStatesMinorOutlyingIslands: case QLocale::UnitedStatesVirginIslands: m_data->currentLayout = &qskInputPanelLayouts.en_US; break; default: m_data->currentLayout = &qskInputPanelLayouts.en_GB; break; } break; } case QLocale::Spanish: m_data->currentLayout = &qskInputPanelLayouts.es; break; case QLocale::Finnish: m_data->currentLayout = &qskInputPanelLayouts.fi; break; case QLocale::French: m_data->currentLayout = &qskInputPanelLayouts.fr; break; case QLocale::Hungarian: m_data->currentLayout = &qskInputPanelLayouts.hu; break; case QLocale::Italian: m_data->currentLayout = &qskInputPanelLayouts.it; break; case QLocale::Japanese: m_data->currentLayout = &qskInputPanelLayouts.ja; break; case QLocale::Latvian: m_data->currentLayout = &qskInputPanelLayouts.lv; break; case QLocale::Lithuanian: m_data->currentLayout = &qskInputPanelLayouts.lt; break; case QLocale::Dutch: m_data->currentLayout = &qskInputPanelLayouts.nl; break; case QLocale::Portuguese: m_data->currentLayout = &qskInputPanelLayouts.pt; break; case QLocale::Romanian: m_data->currentLayout = &qskInputPanelLayouts.ro; break; case QLocale::Russia: m_data->currentLayout = &qskInputPanelLayouts.ru; break; case QLocale::Slovenian: m_data->currentLayout = &qskInputPanelLayouts.sl; break; case QLocale::Slovak: m_data->currentLayout = &qskInputPanelLayouts.sk; break; case QLocale::Turkish: m_data->currentLayout = &qskInputPanelLayouts.tr; break; case QLocale::Chinese: m_data->currentLayout = &qskInputPanelLayouts.zh; break; default: qWarning() << "QskInputPanel: unsupported locale:" << locale; case QLocale::C: m_data->currentLayout = &qskInputPanelLayouts.en_US; break; } Q_EMIT displayLanguageName(); updateKeyData(); setMode( LowercaseMode ); } void QskInputPanel::updateKeyData() { // Key data is in normalized coordinates const auto keyHeight = 1.0f / RowCount; for ( const auto& keyLayout : *m_data->currentLayout ) { auto& keyDataLayout = m_data->keyTable[ &keyLayout - *m_data->currentLayout ]; qreal yPos = 0; for ( int i = 0; i < RowCount; i++ ) { auto& row = keyLayout.data[i]; auto& keyDataRow = keyDataLayout.data[ i ]; const auto baseKeyWidth = 1.0f / qskRowStretch( row ); qreal xPos = 0; qreal keyWidth = baseKeyWidth; for ( const auto& key : row ) { auto& keyData = keyDataRow[ &key - row ]; keyData.key = key; keyWidth = baseKeyWidth * qskKeyStretch( key ); keyData.rect = { xPos, yPos, keyWidth, keyHeight }; xPos += keyWidth; } yPos += keyHeight; } } } void QskInputPanel::setMode( QskInputPanel::Mode mode ) { m_data->mode = mode; update(); } #include "QskInputPanel.moc" #include "moc_QskInputPanel.cpp"