QSkinny 0.8.0
C++/Qt UI toolkit
Loading...
Searching...
No Matches
QskMenu.cpp
1/******************************************************************************
2 * QSkinny - Copyright (C) The authors
3 * SPDX-License-Identifier: BSD-3-Clause
4 *****************************************************************************/
5
6#include "QskMenu.h"
7
8#include "QskGraphicProvider.h"
9#include "QskLabelData.h"
10#include "QskTextOptions.h"
11#include "QskGraphic.h"
12#include "QskColorFilter.h"
13#include "QskSkinlet.h"
14#include "QskEvent.h"
15#include "QskPlatform.h"
16
17#include <qvector.h>
18#include <qvariant.h>
19#include <qeventloop.h>
20
21QSK_QT_PRIVATE_BEGIN
22#include <private/qquickitem_p.h>
23QSK_QT_PRIVATE_END
24
25QSK_SUBCONTROL( QskMenu, Panel )
26QSK_SUBCONTROL( QskMenu, Segment )
27QSK_SUBCONTROL( QskMenu, Cursor )
28QSK_SUBCONTROL( QskMenu, Text )
29QSK_SUBCONTROL( QskMenu, Icon )
30QSK_SUBCONTROL( QskMenu, Separator )
31
32QSK_SYSTEM_STATE( QskMenu, Selected, QskAspect::FirstSystemState << 2 )
33QSK_SYSTEM_STATE( QskMenu, Pressed, QskAspect::FirstSystemState << 3 )
34
35static inline int qskActionIndex( const QVector< int >& actions, int index )
36{
37 if ( index < 0 )
38 return -1;
39
40 auto it = std::lower_bound( actions.constBegin(), actions.constEnd(), index );
41 return it - actions.constBegin();
42}
43
44class QskMenu::PrivateData
45{
46 public:
47 QPointF origin;
48
49 QVector< QskLabelData > options;
50
51 QVector< int > separators;
52 QVector< int > actions;
53
54 int triggeredIndex = -1;
55 int currentIndex = -1;
56
57 bool wrapping = true;
58 bool isPressed = false;
59};
60
61QskMenu::QskMenu( QQuickItem* parent )
62 : Inherited( parent )
63 , m_data( new PrivateData )
64{
65#if 1
66 /*
67 The overlay is clipped from the drop down fading effect.
68 Until it is fixed we simply disable it. TODO ...
69 */
70 setOverlay( false );
71#endif
72 setModal( true );
73
74 setPopupFlag( QskPopup::CloseOnPressOutside, true );
75 setPopupFlag( QskPopup::DeleteOnClose, true );
76
77 setPlacementPolicy( QskPlacementPolicy::Ignore );
78 setSubcontrolProxy( Inherited::Overlay, Overlay );
79
80 initSizePolicy( QskSizePolicy::Fixed, QskSizePolicy::Fixed );
81
82 // we hide the focus indicator while sliding
83 connect( this, &QskPopup::fadingChanged,
85
86 connect( this, &QskPopup::fadingChanged,
87 this, &QQuickItem::setClip );
88
89 connect( this, &QskPopup::opened, this,
90 [this]() { m_data->triggeredIndex = -1; } );
91
92 setAcceptHoverEvents( true );
93}
94
95QskMenu::~QskMenu()
96{
97}
98
99QRectF QskMenu::clipRect() const
100{
101 if ( isFading() )
102 {
103 constexpr qreal d = 1e6;
104 return QRectF( -d, m_data->origin.y() - y(), 2.0 * d, d );
105 }
106
107 return Inherited::clipRect();
108}
109
110bool QskMenu::isWrapping() const
111{
112 return m_data->wrapping;
113}
114
115void QskMenu::setWrapping( bool on )
116{
117 if ( m_data->wrapping != on )
118 {
119 m_data->wrapping = on;
120 Q_EMIT wrappingChanged( on );
121 }
122}
123
124#if 1
125
126// has no effect as we do not offer submenus yet. TODO ...
127bool QskMenu::isCascading() const
128{
129 return flagHint( QskMenu::Panel | QskAspect::Style, qskMaybeDesktopPlatform() );
130}
131
132void QskMenu::setCascading( bool on )
133{
134 if ( setFlagHint( QskMenu::Panel | QskAspect::Style, on ) )
135 Q_EMIT cascadingChanged( on );
136}
137
138void QskMenu::resetCascading()
139{
140 if ( resetSkinHint( QskMenu::Panel | QskAspect::Style ) )
141 Q_EMIT cascadingChanged( isCascading() );
142}
143
144#endif
145
146void QskMenu::setOrigin( const QPointF& origin )
147{
148 if ( origin != m_data->origin )
149 {
150 m_data->origin = origin;
151 Q_EMIT originChanged( origin );
152 }
153}
154
155QPointF QskMenu::origin() const
156{
157 return m_data->origin;
158}
159
160void QskMenu::setTextOptions( const QskTextOptions& textOptions )
161{
162 setTextOptionsHint( Text, textOptions );
163}
164
165QskTextOptions QskMenu::textOptions() const
166{
167 return textOptionsHint( Text );
168}
169
170int QskMenu::addOption( const QString& graphicSource, const QString& text )
171{
172 return addOption( QskLabelData( text, graphicSource ) );
173}
174
175int QskMenu::addOption( const QUrl& graphicSource, const QString& text )
176{
177 return addOption( QskLabelData( text, graphicSource ) );
178}
179
180int QskMenu::addOption( const QskLabelData& option )
181{
182 const int index = m_data->options.count();
183
184 m_data->options += option;
185
186 if ( option.isEmpty() )
187 m_data->separators += index;
188 else
189 m_data->actions += index;
190
192 update();
193
194 if ( isComponentComplete() )
195 Q_EMIT optionsChanged();
196
197 return index;
198}
199
200void QskMenu::setOptions( const QStringList& options )
201{
202 setOptions( qskCreateLabelData( options ) );
203}
204
205void QskMenu::setOptions( const QVector< QskLabelData >& options )
206{
207 m_data->options = options;
208
209 for ( int i = 0; i < options.count(); i++ )
210 {
211 if ( options[i].isEmpty() )
212 m_data->separators += i;
213 else
214 m_data->actions += i;
215 }
216
217 if ( m_data->currentIndex >= 0 )
218 {
219 m_data->currentIndex = -1;
220
221 if ( isComponentComplete() )
222 Q_EMIT currentIndexChanged( m_data->currentIndex );
223 }
224
226 update();
227
228 if ( isComponentComplete() )
229 Q_EMIT optionsChanged();
230}
231
232void QskMenu::clear()
233{
234 setOptions( QVector< QskLabelData >() );
235}
236
237QVector< QskLabelData > QskMenu::options() const
238{
239 return m_data->options;
240}
241
242QskLabelData QskMenu::optionAt( int index ) const
243{
244 return m_data->options.value( index );
245}
246
247void QskMenu::addSeparator()
248{
249 addOption( QskLabelData() );
250}
251
252QVector< int > QskMenu::separators() const
253{
254 return m_data->separators;
255}
256
257QVector< int > QskMenu::actions() const
258{
259 return m_data->actions;
260}
261
262int QskMenu::currentIndex() const
263{
264 return m_data->currentIndex;
265}
266
267void QskMenu::setCurrentIndex( int index )
268{
269 if( index < 0 || index >= m_data->options.count() )
270 {
271 index = -1;
272 }
273 else
274 {
275 if ( m_data->options[index].isEmpty() ) // a seperator
276 index = -1;
277 }
278
279 if( index != m_data->currentIndex )
280 {
281 setPositionHint( Cursor, index );
282
283 m_data->currentIndex = index;
284 update();
285
286 Q_EMIT currentIndexChanged( index );
288 }
289}
290
291QString QskMenu::currentText() const
292{
293 return optionAt( m_data->currentIndex ).text();
294}
295
296int QskMenu::triggeredIndex() const
297{
298 return m_data->triggeredIndex;
299}
300
301QString QskMenu::triggeredText() const
302{
303 return optionAt( m_data->triggeredIndex ).text();
304}
305
306void QskMenu::updateResources()
307{
308 qreal dy = 0.0;
309 if ( isFading() )
310 dy = ( 1.0 - fadingFactor() ) * height();
311
312 setPosition( m_data->origin.x(), m_data->origin.y() - dy );
313
314 Inherited::updateResources();
315}
316
317void QskMenu::updateNode( QSGNode* node )
318{
319 if ( isFading() && clip() )
320 {
321 if ( auto clipNode = QQuickItemPrivate::get( this )->clipNode() )
322 {
323 /*
324 The clipRect is changing while fading. Couldn't
325 find a way how to trigger updates - maybe be enabling/disabling
326 the clip. So we do the updates manually. TODO ...
327 */
328 const auto r = clipRect();
329 if ( r != clipNode->rect() )
330 {
331 clipNode->setRect( r );
332 clipNode->update();
333 }
334 }
335 }
336
337 Inherited::updateNode( node );
338}
339
340void QskMenu::keyPressEvent( QKeyEvent* event )
341{
342 if( m_data->currentIndex < 0 )
343 return;
344
345 switch( event->key() )
346 {
347 case Qt::Key_Up:
348 {
349 traverse( -1 );
350 return;
351 }
352
353 case Qt::Key_Down:
354 {
355 traverse( 1 );
356 return;
357 }
358
359 case Qt::Key_Select:
360 case Qt::Key_Space:
361 case Qt::Key_Return:
362 case Qt::Key_Enter:
363 {
364 m_data->isPressed = true;
365 return;
366 }
367
368 default:
369 {
370 if ( const int steps = qskFocusChainIncrement( event ) )
371 {
372 traverse( steps );
373 return;
374 }
375 }
376 }
377
378 return Inherited::keyPressEvent( event );
379}
380
381void QskMenu::keyReleaseEvent( QKeyEvent* )
382{
383 if( isPressed() )
384 {
385 m_data->isPressed = false;
386
387 if ( m_data->currentIndex >= 0 )
388 {
389 close();
390 trigger( m_data->currentIndex );
391 }
392 }
393}
394
395void QskMenu::hoverEnterEvent( QHoverEvent* event )
396{
397 using A = QskAspect;
398
399 setSkinHint( Segment | Hovered | A::Metric | A::Position, qskHoverPosition( event ) );
400 update();
401}
402
403void QskMenu::hoverMoveEvent( QHoverEvent* event )
404{
405 using A = QskAspect;
406
407 setSkinHint( Segment | Hovered | A::Metric | A::Position, qskHoverPosition( event ) );
408 update();
409}
410
411void QskMenu::hoverLeaveEvent( QHoverEvent* )
412{
413 using A = QskAspect;
414
415 setSkinHint( Segment | Hovered | A::Metric | A::Position, QPointF() );
416 update();
417}
418
419#ifndef QT_NO_WHEELEVENT
420
421void QskMenu::wheelEvent( QWheelEvent* event )
422{
423 const auto steps = qskWheelSteps( event );
424 traverse( -steps );
425}
426
427#endif
428
429void QskMenu::traverse( int steps )
430{
431 const auto& actions = m_data->actions;
432 const auto count = actions.count();
433
434 // -1 -> only one entry ?
435 if ( actions.isEmpty() || ( steps % count == 0 ) )
436 return;
437
438 int action1 = qskActionIndex( actions, m_data->currentIndex );
439 int action2 = action1 + steps;
440
441 if ( !m_data->wrapping )
442 action2 = qBound( 0, action2, count - 1 );
443
444 // when cycling we want to slide in
445 int index1;
446
447 if ( action2 < 0 )
448 index1 = m_data->options.count();
449 else if ( action2 >= count )
450 index1 = -1;
451 else
452 index1 = actions[ action1 ];
453
454 action2 %= count;
455 if ( action2 < 0 )
456 action2 += count;
457
458 const auto index2 = actions[ action2 ];
459
460 movePositionHint( Cursor, index1, index2 );
461 setCurrentIndex( index2 );
462}
463
464void QskMenu::mousePressEvent( QMouseEvent* event )
465{
466 // QGuiApplication::styleHints()->setFocusOnTouchRelease ??
467
468 if ( event->button() == Qt::LeftButton )
469 {
470 const auto index = indexAtPosition( qskMousePosition( event ) );
471 if ( index >= 0 )
472 {
473 setCurrentIndex( index );
474 m_data->isPressed = true;
475 }
476
477 return;
478 }
479
480 Inherited::mousePressEvent( event );
481}
482
483void QskMenu::mouseUngrabEvent()
484{
485 m_data->isPressed = false;
487}
488
489void QskMenu::mouseReleaseEvent( QMouseEvent* event )
490{
491 if ( event->button() == Qt::LeftButton )
492 {
493 if( isPressed() )
494 {
495 m_data->isPressed = false;
496
497 const auto index = m_data->currentIndex;
498
499 if ( ( index >= 0 )
500 && ( index == indexAtPosition( qskMousePosition( event ) ) ) )
501 {
502 close();
503 trigger( m_data->currentIndex );
504 }
505 }
506
507 return;
508 }
509
510 Inherited::mouseReleaseEvent( event );
511}
512
514{
515 setSize( sizeConstraint() );
516
517 if ( m_data->currentIndex < 0 )
518 {
519 if ( !m_data->actions.isEmpty() )
520 setCurrentIndex( m_data->actions.first() );
521 }
522
524}
525
527{
528 if ( isFading() )
529 return QRectF();
530
531 if( currentIndex() >= 0 )
532 {
533 auto actionIndex = qskActionIndex( m_data->actions, currentIndex() );
534
535 return effectiveSkinlet()->sampleRect( this,
536 contentsRect(), Segment, actionIndex );
537 }
538
540}
541
542QRectF QskMenu::cellRect( int index ) const
543{
544 const auto actionIndex = qskActionIndex( m_data->actions, index );
545
546 return effectiveSkinlet()->sampleRect(
547 this, contentsRect(), QskMenu::Segment, actionIndex );
548}
549
550int QskMenu::indexAtPosition( const QPointF& pos ) const
551{
552 const auto index = effectiveSkinlet()->sampleIndexAt(
553 this, contentsRect(), QskMenu::Segment, pos );
554
555 return m_data->actions.value( index, -1 );
556}
557
558bool QskMenu::isPressed() const
559{
560 return m_data->isPressed;
561}
562
563void QskMenu::trigger( int index )
564{
565 if ( index >= 0 && index < m_data->options.count() )
566 {
567 m_data->triggeredIndex = index;
568 Q_EMIT triggered( index );
569 }
570}
571
572QskAspect QskMenu::fadingAspect() const
573{
574 return QskMenu::Panel | QskAspect::Position;
575}
576
577int QskMenu::exec()
578{
579 (void) execPopup();
580 return m_data->triggeredIndex;
581}
582
583#include "moc_QskMenu.cpp"
Lookup key for a QskSkinHintTable.
Definition QskAspect.h:15
@ FirstSystemState
Definition QskAspect.h:115
void focusIndicatorRectChanged()
static const QskAspect::State Hovered
Definition QskControl.h:56
virtual QRectF focusIndicatorRect() const
QRectF contentsRect() const
QSizeF sizeConstraint
Definition QskControl.h:50
void mouseUngrabEvent() override
Definition QskItem.cpp:1044
void resetImplicitSize()
Definition QskItem.cpp:721
void updateNode(QSGNode *) override
Definition QskMenu.cpp:317
void aboutToShow() override
Definition QskMenu.cpp:513
QRectF focusIndicatorRect() const override
Definition QskMenu.cpp:526
void aboutToShow() override
Definition QskPopup.cpp:599
bool resetSkinHint(QskAspect)
Remove a hint from the local hint table.
bool setSkinHint(QskAspect, const QVariant &)
Insert a hint into the local hint table.
const QskSkinlet * effectiveSkinlet() const
bool setFlagHint(QskAspect, int flag)
Sets a flag hint.
virtual void updateNode(QSGNode *)
T flagHint(QskAspect, T=T()) const
Retrieves a flag hint.