QSkinny 0.8.0
C++/Qt UI toolkit
Loading...
Searching...
No Matches
QskInputPanel.cpp
1/******************************************************************************
2 * QSkinny - Copyright (C) The authors
3 * SPDX-License-Identifier: BSD-3-Clause
4 *****************************************************************************/
5
6#include "QskInputPanel.h"
7#include "QskInputContext.h"
8#include "QskTextPredictor.h"
9
10#include <qpointer.h>
11#include <qtextformat.h>
12
13namespace
14{
15 struct Result
16 {
17 int key = 0;
18
19 QString text;
20 bool isFinal = true;
21 };
22}
23
24static void qskRegisterInputPanel()
25{
26 qRegisterMetaType< Result >( "Result" );
27}
28
29Q_CONSTRUCTOR_FUNCTION( qskRegisterInputPanel )
30
31static inline QQuickItem* qskReceiverItem( const QskInputPanel* panel )
32{
33 if ( auto item = panel->inputProxy() )
34 return item;
35
36 return panel->inputItem();
37}
38
39static inline bool qskUsePrediction( Qt::InputMethodHints hints )
40{
41 constexpr Qt::InputMethodHints mask =
42 Qt::ImhNoPredictiveText | Qt::ImhExclusiveInputMask | Qt::ImhHiddenText;
43
44 return ( hints & mask ) == 0;
45}
46
47static inline void qskSendText(
48 QQuickItem* receiver, const QString& text, bool isFinal )
49{
50 if ( receiver == nullptr )
51 return;
52
53 if ( isFinal )
54 {
55 QInputMethodEvent event;
56
57 /*
58 QQuickTextInput is buggy when receiving empty commit strings.
59 We need to send a wrong replaceLength to work around it.
60 See QTBUG: 68874
61 */
62 if ( text.isEmpty() )
63 event.setCommitString( text, 0, 1 );
64 else
65 event.setCommitString( text );
66
67 QCoreApplication::sendEvent( receiver, &event );
68 }
69 else
70 {
71 QTextCharFormat format;
72 format.setFontUnderline( true );
73
74 const QInputMethodEvent::Attribute attribute(
75 QInputMethodEvent::TextFormat, 0, text.length(), format );
76
77 QInputMethodEvent event( text, { attribute } );
78
79 QCoreApplication::sendEvent( receiver, &event );
80 }
81}
82
83static inline void qskSendReplaceText( QQuickItem* receiver, const QString& text )
84{
85 if ( receiver == nullptr )
86 return;
87
88 QInputMethodEvent::Attribute attribute(
89 QInputMethodEvent::Selection, 0, 32767, QVariant() );
90
91 QInputMethodEvent event( QString(), { attribute } );
92 QCoreApplication::sendEvent( receiver, &event );
93
94 qskSendText( receiver, text, true );
95}
96
97static inline void qskSendKey( QQuickItem* receiver, int key )
98{
99 if ( receiver == nullptr )
100 return;
101
102 QKeyEvent keyPress( QEvent::KeyPress, key, Qt::NoModifier );
103 QCoreApplication::sendEvent( receiver, &keyPress );
104
105 QKeyEvent keyRelease( QEvent::KeyRelease, key, Qt::NoModifier );
106 QCoreApplication::sendEvent( receiver, &keyRelease );
107}
108
109namespace
110{
111 class KeyProcessor : public QObject
112 {
113 Q_OBJECT
114
115 public:
116 QString preedit() const
117 {
118 return m_preedit;
119 }
120
121 void processKey(
122 int key, Qt::InputMethodHints inputHints, QskInputPanel* panel,
123 QskTextPredictor* predictor, int spaceLeft )
124 {
125 // reset:
126 m_currentResult.isFinal = true;
127 m_currentResult.text.clear();
128 m_currentResult.key = 0;
129
130 m_predictor = predictor;
131 m_spaceLeft = spaceLeft;
132
133 // First we have to handle the control keys
134
135 switch ( key )
136 {
137 case Qt::Key_Backspace:
138 case Qt::Key_Muhenkan:
139 {
140 if ( predictor && !m_preedit.isEmpty() )
141 {
142 m_preedit.chop( 1 );
143
144 m_currentResult.text = m_preedit;
145 m_currentResult.isFinal = false;
146
147 Q_EMIT panel->predictionRequested( m_preedit );
148 // Let the input field update right away, otherwise
149 // we'll get weird effects with fast backspace presses:
150 Q_EMIT keyProcessingFinished( m_currentResult );
151 }
152 else
153 {
154 m_currentResult.key = Qt::Key_Backspace;
155 Q_EMIT keyProcessingFinished( m_currentResult );
156 }
157
158 return;
159 }
160 case Qt::Key_Return:
161 {
162 if ( predictor )
163 {
164 if ( !m_preedit.isEmpty() )
165 {
166 if ( spaceLeft )
167 {
168 m_currentResult.text = m_preedit.left( spaceLeft );
169 m_currentResult.isFinal = true;
170 }
171
172 reset();
173 Q_EMIT keyProcessingFinished( m_currentResult );
174 return;
175 }
176 }
177
178 if ( !( inputHints & Qt::ImhMultiLine ) )
179 {
180 m_currentResult.key = Qt::Key_Return;
181 Q_EMIT keyProcessingFinished( m_currentResult );
182 return;
183 }
184
185 break;
186 }
187 case Qt::Key_Space:
188 {
189 if ( predictor )
190 {
191 if ( !m_preedit.isEmpty() && spaceLeft )
192 {
193 m_preedit += keyString( key );
194 m_preedit = m_preedit.left( spaceLeft );
195
196 m_currentResult.text = m_preedit;
197 m_currentResult.isFinal = true;
198
199 reset();
200
201 Q_EMIT keyProcessingFinished( m_currentResult );
202 return;
203 }
204 }
205
206 break;
207 }
208 case Qt::Key_Left:
209 case Qt::Key_Right:
210 case Qt::Key_Escape:
211 case Qt::Key_Cancel:
212 {
213 m_currentResult.key = key;
214 Q_EMIT keyProcessingFinished( m_currentResult );
215 return;
216 }
217 }
218
219 const QString text = keyString( key );
220
221 if ( predictor )
222 {
223 m_preedit += text;
224 Q_EMIT panel->predictionRequested( m_preedit );
225 }
226 else
227 {
228 m_currentResult.text = text;
229 m_currentResult.isFinal = true;
230 Q_EMIT keyProcessingFinished( m_currentResult );
231 }
232 }
233
234 void reset()
235 {
236 m_preedit.clear();
237 }
238
239 void continueProcessingKey( const QStringList& candidates )
240 {
241 if ( m_predictor )
242 {
243 if ( candidates.count() > 0 )
244 {
245 m_currentResult.text = m_preedit;
246 m_currentResult.isFinal = false;
247 }
248 else
249 {
250 m_currentResult.text = m_preedit.left( m_spaceLeft );
251 m_currentResult.isFinal = true;
252
253 m_preedit.clear();
254 }
255 }
256
257 Q_EMIT keyProcessingFinished( m_currentResult );
258 }
259
260 Q_SIGNALS:
261 void keyProcessingFinished( const Result& );
262
263 private:
264 inline QString keyString( int keyCode ) const
265 {
266 // Special case entry codes here, else default to the symbol
267 switch ( keyCode )
268 {
269 case Qt::Key_Shift:
270 case Qt::Key_CapsLock:
271 case Qt::Key_Mode_switch:
272 case Qt::Key_Backspace:
273 case Qt::Key_Muhenkan:
274 return QString();
275
276 case Qt::Key_Return:
277 case Qt::Key_Kanji:
278 return QChar( QChar::CarriageReturn );
279
280 case Qt::Key_Space:
281 return QChar( QChar::Space );
282
283 default:
284 break;
285 }
286
287 return QChar( keyCode );
288 }
289
290 QString m_preedit;
291 int m_spaceLeft = -1;
292 QskTextPredictor* m_predictor = nullptr;
293 Result m_currentResult;
294 };
295}
296
297class QskInputPanel::PrivateData
298{
299 public:
300 PrivateData( QskInputPanel* panel )
301 : panel( panel )
302 {
303 }
304
305 KeyProcessor keyProcessor;
306 QPointer< QQuickItem > inputItem;
307
308 QLocale predictorLocale;
309 std::shared_ptr< QskTextPredictor > predictor;
310 QStringList candidates;
311
312 Qt::InputMethodHints inputHints;
313 bool hasPredictorLocale = false;
314 const QskInputPanel* panel;
315
316 void handleKeyProcessingFinished( const Result& result )
317 {
318 switch ( result.key )
319 {
320 case 0:
321 {
322 qskSendText( qskReceiverItem( panel ),
323 result.text, result.isFinal );
324 break;
325 }
326 case Qt::Key_Return:
327 {
328 if ( auto proxy = panel->inputProxy() )
329 {
330 // using input method query instead
331 const auto value = proxy->property( "text" );
332 if ( value.canConvert< QString >() )
333 {
334 qskSendReplaceText( inputItem, value.toString() );
335 }
336 }
337
338 qskSendKey( inputItem, result.key );
339 break;
340 }
341 case Qt::Key_Escape:
342 case Qt::Key_Cancel:
343 {
344 qskSendKey( inputItem, result.key );
345 break;
346 }
347 default:
348 {
349 qskSendKey( qskReceiverItem( panel ), result.key );
350 }
351 }
352 }
353};
354
355QskInputPanel::QskInputPanel( QQuickItem* parent )
356 : Inherited( parent )
357 , m_data( new PrivateData( this ) )
358{
359 setAutoLayoutChildren( true );
360 initSizePolicy( QskSizePolicy::Expanding, QskSizePolicy::Constrained );
361
362 connect( this, &QskInputPanel::keySelected,
363 this, &QskInputPanel::commitKey );
364
365 connect( this, &QskInputPanel::predictiveTextSelected,
366 this, &QskInputPanel::commitPredictiveText );
367
368 connect( this, &QskControl::localeChanged,
369 this, &QskInputPanel::updateLocale );
370
371 connect( &m_data->keyProcessor, &KeyProcessor::keyProcessingFinished,
372 this, [this]( const Result& result ) { m_data->handleKeyProcessingFinished( result ); } );
373
374 updateLocale( locale() );
375}
376
377QskInputPanel::~QskInputPanel()
378{
379}
380
381void QskInputPanel::attachInputItem( QQuickItem* item )
382{
383 if ( item == m_data->inputItem )
384 return;
385
386 if ( m_data->inputItem )
387 {
388 disconnect( m_data->inputItem, &QObject::destroyed,
389 this, &QskInputPanel::inputItemDestroyed );
390 }
391
392 m_data->inputItem = item;
393
394 if ( item )
395 {
396 if ( m_data->predictor )
397 Q_EMIT predictionReset();
398
399 m_data->keyProcessor.reset();
400 m_data->inputHints = Qt::InputMethodHints();
401
402 attachItem( item );
403
404 Qt::InputMethodQueries queries = Qt::ImQueryAll;
405 queries &= ~Qt::ImEnabled;
406
407 updateInputPanel( queries );
408
409 if ( inputProxy() )
410 {
411 /*
412 Hiding the cursor in item. We use postEvent
413 so that everything on the item is done,
414 when receiving the event.
415 */
416 const QInputMethodEvent::Attribute attribute(
417 QInputMethodEvent::Cursor, 0, 0, QVariant() );
418
419 QCoreApplication::postEvent( item,
420 new QInputMethodEvent( QString(), { attribute } ) );
421 }
422
423 connect( item, &QObject::destroyed,
424 this, &QskInputPanel::inputItemDestroyed,
425 Qt::UniqueConnection );
426 }
427 else
428 {
429 attachItem( nullptr );
430 }
431}
432
433void QskInputPanel::updateInputPanel( Qt::InputMethodQueries queries )
434{
435 if ( m_data->inputItem == nullptr )
436 return;
437
438 QInputMethodQueryEvent event( queries );
439 QCoreApplication::sendEvent( m_data->inputItem, &event );
440
441 if ( queries & Qt::ImHints )
442 {
443 m_data->inputHints = static_cast< Qt::InputMethodHints >(
444 event.value( Qt::ImHints ).toInt() );
445
446 setPredictionEnabled(
447 m_data->predictor && qskUsePrediction( m_data->inputHints ) );
448 }
449
450 if ( queries & Qt::ImPreferredLanguage )
451 {
452 setLocale( event.value( Qt::ImPreferredLanguage ).toLocale() );
453 }
454
455 if ( queries & Qt::ImInputItemClipRectangle )
456 {
457 /*
458 Could be used to move the panel,
459 so that it does not hide the item.
460 */
461 }
462}
463
464void QskInputPanel::updateLocale( const QLocale& locale )
465{
466 if ( !m_data->hasPredictorLocale || locale != m_data->predictorLocale )
467 {
468 m_data->hasPredictorLocale = true;
469 m_data->predictorLocale = locale;
470
471 resetPredictor( locale );
472 m_data->keyProcessor.reset();
473 }
474}
475
476void QskInputPanel::resetPredictor( const QLocale& locale )
477{
478 auto predictor = QskInputContext::instance()->textPredictor( locale );
479
480 if ( predictor == m_data->predictor )
481 return;
482
483 m_data->predictor = predictor;
484
485 if ( predictor )
486 {
487 // text predictor lives in another thread, so these will all be QueuedConnections:
488 connect( this, &QskInputPanel::predictionReset,
489 predictor.get(), &QskTextPredictor::reset );
490 connect( this, &QskInputPanel::predictionRequested,
491 predictor.get(), &QskTextPredictor::request );
492
493 connect( predictor.get(), &QskTextPredictor::predictionChanged,
494 this, &QskInputPanel::updatePrediction );
495 }
496
497 setPredictionEnabled(
498 predictor && qskUsePrediction( m_data->inputHints ) );
499}
500
501void QskInputPanel::commitPredictiveText( int index )
502{
503 QString text;
504
505 if ( m_data->predictor )
506 {
507 text = m_data->candidates.at( index );
508 Q_EMIT predictionReset();
509 }
510
511 m_data->keyProcessor.reset();
512
513 setPrediction( {} );
514
515 qskSendText( qskReceiverItem( this ), text, true );
516}
517
518void QskInputPanel::updatePrediction( const QString& text, const QStringList& candidates )
519{
520 if ( m_data->predictor )
521 {
522 if( m_data->keyProcessor.preedit() != text )
523 {
524 // This must be for another input panel
525 return;
526 }
527
528 setPrediction( candidates );
529 m_data->keyProcessor.continueProcessingKey( candidates );
530 }
531 else
532 {
533 qWarning() << "got prediction update, but no predictor. Something is wrong";
534 }
535}
536
537QQuickItem* QskInputPanel::inputProxy() const
538{
539 return nullptr;
540}
541
542QQuickItem* QskInputPanel::inputItem() const
543{
544 return m_data->inputItem;
545}
546
547void QskInputPanel::setPrompt( const QString& )
548{
549}
550
551void QskInputPanel::setPredictionEnabled( bool )
552{
553}
554
555void QskInputPanel::setPrediction(const QStringList& candidates )
556{
557 m_data->candidates = candidates;
558}
559
560Qt::Alignment QskInputPanel::alignment() const
561{
562 /*
563 When we have an input proxy, we don't care if
564 the input item becomes hidden
565 */
566
567 return inputProxy() ? Qt::AlignVCenter : Qt::AlignBottom;
568}
569
570QStringList QskInputPanel::candidates() const
571{
572 return m_data->candidates;
573}
574
575void QskInputPanel::commitKey( int key )
576{
577 if ( m_data->inputItem == nullptr )
578 return;
579
580 int spaceLeft = -1;
581
582 if ( !( m_data->inputHints & Qt::ImhMultiLine ) )
583 {
584 QInputMethodQueryEvent event1( Qt::ImMaximumTextLength );
585 QCoreApplication::sendEvent( m_data->inputItem, &event1 );
586
587 const int maxChars = event1.value( Qt::ImMaximumTextLength ).toInt();
588 if ( maxChars >= 0 )
589 {
590 QInputMethodQueryEvent event2( Qt::ImSurroundingText );
591 QCoreApplication::sendEvent( qskReceiverItem( this ), &event2 );
592
593 const auto text = event2.value( Qt::ImSurroundingText ).toString();
594 spaceLeft = maxChars - text.length();
595 }
596 }
597
598 QskTextPredictor* predictor = nullptr;
599 if ( qskUsePrediction( m_data->inputHints ) )
600 predictor = m_data->predictor.get(); // ### we could also make the predictor member of keyProcessor a shared ptr?
601
602 m_data->keyProcessor.processKey(
603 key, m_data->inputHints, this, predictor, spaceLeft );
604}
605
606#include "moc_QskInputPanel.cpp"
607#include "QskInputPanel.moc"
void setLocale(const QLocale &)
void localeChanged(const QLocale &)
QLocale locale
Definition QskControl.h:27