QSkinny 0.8.0
C++/Qt UI toolkit
Loading...
Searching...
No Matches
QskInputContext.cpp
1/******************************************************************************
2 * QSkinny - Copyright (C) The authors
3 * SPDX-License-Identifier: BSD-3-Clause
4 *****************************************************************************/
5
6#include "QskInputContext.h"
7#include "QskInputPanel.h"
8#include "QskInputPanelBox.h"
9
10#include "QskDialog.h"
11#include "QskLinearBox.h"
12#include "QskPopup.h"
13#include "QskQuick.h"
14#include "QskTextPredictor.h"
15#include "QskWindow.h"
16#include "QskPlatform.h"
17
18#include <qguiapplication.h>
19#include <qmap.h>
20#include <qpointer.h>
21#include <qthread.h>
22
23#include <qpa/qplatforminputcontext.h>
24#include <qpa/qplatformintegration.h>
25
26#if HUNSPELL
27#include "QskHunspellTextPredictor.h"
28#endif
29
30namespace
31{
32 class Panel final : public QskInputPanel
33 {
34 public:
35 Panel( QQuickItem* parent = nullptr )
36 : QskInputPanel( parent )
37 {
39 setLayoutAlignmentHint( Qt::AlignHCenter | Qt::AlignBottom );
40
41 m_box = new QskInputPanelBox( this );
42
43 connect( m_box, &QskInputPanelBox::keySelected,
44 this, &QskInputPanel::keySelected );
45
46 connect( m_box, &QskInputPanelBox::predictiveTextSelected,
47 this, &QskInputPanel::predictiveTextSelected );
48 }
49
50 void attachItem( QQuickItem* item ) override
51 {
52 m_box->attachInputItem( item );
53 }
54
55 QQuickItem* inputProxy() const override
56 {
57 return m_box->inputProxy();
58 }
59
60 void setPrompt( const QString& prompt ) override
61 {
62 m_box->setInputPrompt( prompt );
63 }
64
65 void setPredictionEnabled( bool on ) override
66 {
67 m_box->setPanelHint( QskInputPanelBox::Prediction, on );
68 }
69
70 void setPrediction( const QStringList& prediction ) override
71 {
72 QskInputPanel::setPrediction( prediction );
73 m_box->setPrediction( prediction );
74 }
75
76 private:
77 QskInputPanelBox* m_box;
78 };
79
80 class Channel
81 {
82 public:
83 // item receiving the input
84 QPointer< QQuickItem > item;
85
86 // panel for inserting the input
87 QPointer< QskInputPanel > panel;
88
89 // popup or window embedding the panel
90 QPointer< QskPopup > popup;
91 QPointer< QskWindow > window;
92 };
93
94 class ChannelTable
95 {
96 public:
97 inline Channel* currentChannel() const
98 {
99 const auto object = QGuiApplication::focusObject();
100 return channel( qobject_cast< const QQuickItem* >( object ) );
101 }
102
103 inline Channel* channel( const QQuickWindow* window ) const
104 {
105 if ( window )
106 {
107 auto it = m_map.constFind( window );
108 if ( it != m_map.constEnd() )
109 return const_cast< Channel* >( &it.value() );
110 }
111
112 return nullptr;
113 }
114
115 inline Channel* channel( const QskInputPanel* panel ) const
116 {
117 if ( panel )
118 {
119 QMap< const QQuickWindow*, Channel >::const_iterator it;
120
121 for( it = m_map.constBegin(); it != m_map.constEnd(); ++it )
122 {
123 if( it.value().panel == panel )
124 return const_cast< Channel* >( &it.value() );
125 }
126 }
127
128 return nullptr;
129 }
130
131 inline Channel* channel( const QQuickItem* item ) const
132 {
133 if ( item )
134 {
135 // item->window() might already been gone
136 QMap< const QQuickWindow*, Channel >::const_iterator it;
137
138 for( it = m_map.constBegin(); it != m_map.constEnd(); ++it )
139 {
140 if( it.value().item == item )
141 return const_cast< Channel* >( &it.value() );
142 }
143 }
144
145 return nullptr;
146 }
147
148 inline Channel* ancestorChannel( const QQuickItem* item ) const
149 {
150 for ( auto it = m_map.constBegin();
151 it != m_map.constEnd(); ++it )
152 {
153 if ( const auto panel = it.value().panel )
154 {
155 if ( ( item == panel ) || qskIsAncestorOf( panel, item ) )
156 {
157 return const_cast< Channel* >( &it.value() );
158 }
159 }
160 }
161
162 return nullptr;
163 }
164
165 inline Channel* insert( const QQuickWindow* window )
166 {
167 return &m_map[ window ];
168 }
169
170 inline void remove( const QQuickWindow* window )
171 {
172 m_map.remove( window );
173 }
174
175 private:
176 QMap< const QQuickWindow*, Channel > m_map;
177 };
178}
179
180static QPointer< QskInputContext > qskInputContext;
181
182static void qskSendToPlatformContext( QEvent::Type type )
183{
184 const auto integration = qskPlatformIntegration();
185
186 if ( const auto inputContext = integration->inputContext() )
187 {
188 QEvent event( type );
189 QCoreApplication::sendEvent( inputContext, &event );
190 }
191}
192
193static void qskInputContextHook()
194{
195 qAddPostRoutine( [] { delete qskInputContext; } );
196}
197
198Q_COREAPP_STARTUP_FUNCTION( qskInputContextHook )
199
200#if QT_VERSION >= QT_VERSION_CHECK( 6, 2, 0 )
201 static inline QLocale::Territory qskTerritory( const QLocale& locale )
202 { return locale.territory(); }
203#else
204 static inline QLocale::Country qskTerritory( const QLocale& locale )
205 { return locale.country(); }
206#endif
207
208void QskInputContext::setInstance( QskInputContext* inputContext )
209{
210 if ( inputContext != qskInputContext )
211 {
212 const auto oldContext = qskInputContext;
213 qskInputContext = inputContext;
214
215 if ( oldContext && oldContext->parent() == nullptr )
216 {
217 delete oldContext;
218
219 // still needed with > Qt 5.15 ?
220 qskSendToPlatformContext( QEvent::PlatformPanel );
221 }
222 }
223}
224
225QskInputContext* QskInputContext::instance()
226{
227 return qskInputContext;
228}
229
230class QskInputContext::PrivateData
231{
232 public:
233 inline QskInputPanel* createPanel( QskInputContext* context ) const
234 {
235 QskInputPanel* panel = nullptr;
236
237 if ( this->factory )
238 panel = this->factory->createPanel();
239
240 if ( panel == nullptr )
241 panel = new Panel();
242
243 connect( panel, &QskInputPanel::visibleChanged,
244 context, &QskInputContext::activeChanged );
245
246 connect( panel, &QskInputPanel::localeChanged,
247 context, [] { qskSendToPlatformContext( QEvent::LocaleChange ); } );
248
249 connect( panel, &QskInputPanel::inputItemDestroyed,
250 context, [ context, panel ] { context->hideChannel( panel ); } );
251
252 return panel;
253 }
254
255 inline QskPopup* createPopup( QskInputPanel* panel ) const
256 {
257 auto popup = new QskPopup();
258
259 popup->setAutoLayoutChildren( true );
260 popup->setPlacementPolicy( QskPlacementPolicy() );
261 popup->setMargins( 5 );
262 popup->setModal( true );
263
264 panel->setParentItem( popup );
265 if ( panel->parent() == nullptr )
266 panel->setParent( popup );
267
268 return popup;
269 }
270
271 inline QskWindow* createWindow( QskInputPanel* panel ) const
272 {
273 auto window = new QskWindow();
274
275 window->setFlags( window->flags() & Qt::Dialog );
276 // window->setModality( Qt::ApplicationModal );
277 window->setAutoLayoutChildren( true );
278#if 0
279 window->setFlags( Qt::Tool | Qt::WindowDoesNotAcceptFocus );
280#endif
281
282 panel->setParentItem( window->contentItem() );
283
284 return window;
285 }
286
287 void closeChannel( Channel* channel )
288 {
289 if ( channel->popup )
290 channel->popup->close(); // deleteOnClose is set
291
292 if ( channel->window )
293 channel->window->close(); // deleteOnClose is set
294 }
295
296 ChannelTable channels;
297 QPointer< QskInputContextFactory > factory;
298};
299
300QskInputContext::QskInputContext()
301 : m_data( new PrivateData() )
302{
303 setObjectName( QStringLiteral( "InputContext" ) );
304}
305
306QskInputContext::~QskInputContext()
307{
308}
309
310void QskInputContext::setFactory( QskInputContextFactory* factory )
311{
312 if ( m_data->factory == factory )
313 return;
314
315 if ( m_data->factory && m_data->factory->parent() == this )
316 delete m_data->factory;
317
318 m_data->factory = factory;
319
320 if ( factory && factory->parent() == nullptr )
321 factory->setParent( this );
322}
323
324QskInputContextFactory* QskInputContext::factory() const
325{
326 return m_data->factory;
327}
328
329std::shared_ptr< QskTextPredictor > QskInputContext::textPredictor( const QLocale& locale )
330{
331 if ( m_data->factory )
332 return m_data->factory->setupPredictor( locale );
333
334 return nullptr;
335}
336
337void QskInputContext::update( const QQuickItem* item, Qt::InputMethodQueries queries )
338{
339 if ( item == nullptr )
340 {
341 // those are coming from QQuickWindow based on focus changes
342 item = qobject_cast< QQuickItem* >( QGuiApplication::focusObject() );
343 }
344
345 auto channel = m_data->channels.channel( item );
346 if ( channel == nullptr )
347 return;
348
349 if ( queries & Qt::ImEnabled )
350 {
351 QInputMethodQueryEvent event( Qt::ImEnabled );
352 QCoreApplication::sendEvent( channel->item, &event );
353
354 if ( !event.value( Qt::ImEnabled ).toBool() )
355 {
356 hidePanel( item );
357 return;
358 }
359 }
360
361 channel->panel->updateInputPanel( queries );
362}
363
364QRectF QskInputContext::panelRect() const
365{
366 /*
367 As we can have more than panel at the same time we
368 better don't return any geometry
369 */
370
371 return QRectF();
372}
373
374void QskInputContext::showPanel( const QQuickItem* item )
375{
376 if ( item == nullptr )
377 return;
378
379 if ( m_data->channels.ancestorChannel( item ) )
380 {
381 // We are inside of an existing panel
382 return;
383 }
384
385 if ( auto channel = m_data->channels.channel( item->window() ) )
386 {
387 if ( channel->item == item )
388 return;
389
390 hidePanel( channel->item );
391 }
392
393 auto panel = m_data->createPanel( this );
394
395 auto channel = m_data->channels.insert( item->window() );
396 channel->item = const_cast< QQuickItem* >( item );
397 channel->panel = panel;
398
399 if ( QskDialog::instance()->policy() == QskDialog::TopLevelWindow )
400 {
401 // The input panel is embedded in a top level window
402
403 auto window = m_data->createWindow( panel );
404
405 QSize size = window->sizeConstraint();
406 if ( size.isEmpty() )
407 {
408 // no idea, may be something based on the screen size
409 size = QSize( 800, 240 );
410 }
411
412 window->resize( size );
413 window->show();
414
415 window->setDeleteOnClose( true );
416
417 channel->window = window;
418 }
419 else
420 {
421 // The input panel is embedded in a popup
422
423 auto popup = m_data->createPopup( panel );
424
425 popup->setPopupFlag( QskPopup::DeleteOnClose, true );
426 popup->setParentItem( item->window()->contentItem() );
427 popup->setParent( this );
428
429 channel->popup = popup;
430
431 popup->open();
432 }
433
434 panel->attachInputItem( const_cast< QQuickItem* >( item ) );
435}
436
437void QskInputContext::hidePanel( const QQuickItem* item )
438{
439 if ( item == nullptr )
440 return;
441
442 if ( auto channel = m_data->channels.channel( item ) )
443 {
444 m_data->closeChannel( channel );
445 m_data->channels.remove( item->window() );
446 }
447}
448
449void QskInputContext::hideChannel( const QskInputPanel* panel )
450{
451 if ( auto channel = m_data->channels.channel( panel ) )
452 {
453 // channel->item is already dead here
454 m_data->closeChannel( channel );
455 m_data->channels.remove( panel->window() );
456 }
457}
458
459void QskInputContext::setInputPanelVisible( const QQuickItem* item, bool on )
460{
461 // called from inside the controls
462
463 if ( item == nullptr )
464 item = qobject_cast< QQuickItem* >( QGuiApplication::focusObject() );
465
466 if ( item )
467 {
468 if ( on )
469 showPanel( item );
470 else
471 hidePanel( item );
472 }
473}
474
475bool QskInputContext::isInputPanelVisible() const
476{
477 return m_data->channels.currentChannel() != nullptr;
478}
479
480QLocale QskInputContext::locale() const
481{
482 if ( auto channel = m_data->channels.currentChannel() )
483 {
484 if ( channel->panel )
485 return channel->panel->locale();
486 }
487
488 return QLocale();
489}
490
491void QskInputContext::setFocusObject( QObject* )
492{
493}
494
495void QskInputContext::invokeAction(
496 QInputMethod::Action, int cursorPosition )
497{
498 // called from qquicktextinput/qquicktextedit
499 Q_UNUSED( cursorPosition )
500}
501
502void QskInputContext::commitPrediction( bool )
503{
504 /*
505 called, when the input item loses the focus.
506 As it it should be possible to navigate inside of the
507 panel what should we do here ?
508 */
509}
510
511class QskInputContextFactory::PrivateData
512{
513 public:
514 QThread* thread = nullptr;
515 std::shared_ptr< QskTextPredictor > predictor;
516 QLocale predictorLocale;
517};
518
519QskInputContextFactory::QskInputContextFactory( QObject* parent )
520 : QObject( parent )
521 , m_data( new PrivateData() )
522{
523}
524
525QskInputContextFactory::~QskInputContextFactory()
526{
527 if( m_data->thread )
528 {
529 m_data->thread->quit();
530 connect( m_data->thread, &QThread::finished, m_data->thread, &QObject::deleteLater );
531 }
532}
533
534std::shared_ptr< QskTextPredictor > QskInputContextFactory::setupPredictor( const QLocale& locale )
535{
536 if( !m_data->predictor
537 || m_data->predictorLocale.language() != locale.language()
538 || qskTerritory( m_data->predictorLocale ) != qskTerritory( locale ) )
539 {
540 m_data->predictor = std::shared_ptr< QskTextPredictor >( createPredictor( locale ) );
541 m_data->predictorLocale = QLocale( locale.language(), qskTerritory( locale ) );
542
543 if( m_data->predictor )
544 {
545 if( !m_data->thread )
546 {
547 m_data->thread = new QThread();
548 m_data->thread->start();
549 }
550
551 m_data->predictor->moveToThread( m_data->thread );
552 }
553 }
554
555 return m_data->predictor;
556}
557
558QskTextPredictor* QskInputContextFactory::createPredictor( const QLocale& locale )
559{
560#if HUNSPELL
561 return new QskHunspellTextPredictor( locale );
562#else
563 Q_UNUSED( locale )
564#endif
565
566 return nullptr;
567}
568
569QskInputPanel* QskInputContextFactory::createPanel() const
570{
571 return new Panel();
572}
573
574#include "moc_QskInputContext.cpp"
void localeChanged(const QLocale &)
void setAutoLayoutChildren(bool)
void setLayoutAlignmentHint(Qt::Alignment)