QSkinny 0.8.0
C++/Qt UI toolkit
Loading...
Searching...
No Matches
QskMenuSkinlet.cpp
1/******************************************************************************
2 * QSkinny - Copyright (C) The authors
3 * SPDX-License-Identifier: BSD-3-Clause
4 *****************************************************************************/
5
6#include "QskMenuSkinlet.h"
7#include "QskMenu.h"
8
9#include "QskGraphic.h"
10#include "QskColorFilter.h"
11#include "QskTextOptions.h"
12#include "QskFunctions.h"
13#include "QskMargins.h"
14#include "QskFunctions.h"
15#include "QskLabelData.h"
16
17#include "QskSGNode.h"
18
19#include <qfontmetrics.h>
20#include <qmath.h>
21
22static inline int qskActionIndex( const QskMenu* menu, int optionIndex )
23{
24 if ( optionIndex < 0 )
25 return -1;
26
27 const auto& actions = menu->actions();
28
29 auto it = std::lower_bound(
30 actions.constBegin(), actions.constEnd(), optionIndex );
31
32 return it - actions.constBegin();
33}
34
35static inline qreal qskPaddedSeparatorHeight( const QskMenu* menu )
36{
37 using Q = QskMenu;
38
39 const auto margins = menu->marginHint( Q::Separator );
40
41 return menu->metric( Q::Separator | QskAspect::Size )
42 + margins.top() + margins.bottom();
43}
44
45class QskMenuSkinlet::PrivateData
46{
47 public:
48 class CacheGuard
49 {
50 public:
51 CacheGuard( PrivateData* data )
52 : m_data( data )
53 {
54 m_data->enableCache( true );
55 }
56
57 ~CacheGuard()
58 {
59 m_data->enableCache( false );
60 }
61
62 private:
63 PrivateData* m_data;
64 };
65
66 void enableCache( bool on )
67 {
68 m_isCaching = on;
69 m_segmentHeight = m_segmentWidth = m_graphicWidth = m_textWidth = -1.0;
70 }
71
72 inline qreal graphicWidth( const QskMenu* menu ) const
73 {
74 if ( m_isCaching )
75 {
76 if ( m_graphicWidth < 0.0 )
77 m_graphicWidth = graphicWidthInternal( menu );
78
79 return m_graphicWidth;
80 }
81
82 return graphicWidthInternal( menu );
83 }
84
85 inline qreal textWidth( const QskMenu* menu ) const
86 {
87 if ( m_isCaching )
88 {
89 if ( m_textWidth < 0.0 )
90 m_textWidth = textWidthInternal( menu );
91
92 return m_textWidth;
93 }
94
95 return textWidthInternal( menu );
96 }
97
98 inline qreal segmentWidth( const QskMenu* menu ) const
99 {
100 if ( m_isCaching )
101 {
102 if ( m_segmentWidth < 0.0 )
103 m_segmentWidth = segmentWidthInternal( menu );
104
105 return m_segmentWidth;
106 }
107
108 return segmentWidthInternal( menu );
109 }
110
111 inline qreal segmentHeight( const QskMenu* menu ) const
112 {
113 if ( m_isCaching )
114 {
115 if ( m_segmentHeight < 0.0 )
116 m_segmentHeight = segmentHeightInternal( menu );
117
118 return m_segmentHeight;
119 }
120
121 return segmentHeightInternal( menu );
122 }
123
124 private:
125 qreal graphicWidthInternal( const QskMenu* menu ) const
126 {
127 const auto hint = menu->strutSizeHint( QskMenu::Icon );
128 const qreal textHeight = menu->effectiveFontHeight( QskMenu::Text );
129
130 const auto h = qMax( hint.height(), textHeight );
131
132 qreal maxW = 0.0;
133
134 const auto options = menu->options();
135 for ( auto& option : options )
136 {
137 const auto graphic = option.icon().graphic();
138 if ( !graphic.isNull() )
139 {
140 const auto w = graphic.widthForHeight( h );
141 if( w > maxW )
142 maxW = w;
143 }
144 }
145
146 return qMax( hint.width(), maxW );
147 }
148
149 qreal textWidthInternal( const QskMenu* menu ) const
150 {
151 const QFontMetricsF fm( menu->effectiveFont( QskMenu::Text ) );
152
153 auto maxWidth = 0.0;
154
155 const auto options = menu->options();
156 for ( auto& option : options )
157 {
158 if( !option.text().isEmpty() )
159 {
160 const auto w = qskHorizontalAdvance( fm, option.text() );
161 if( w > maxWidth )
162 maxWidth = w;
163 }
164 }
165
166 return maxWidth;
167 }
168
169 qreal segmentWidthInternal( const QskMenu* menu ) const
170 {
171 using Q = QskMenu;
172
173 const auto spacing = menu->spacingHint( Q::Segment );
174 const auto padding = menu->paddingHint( Q::Segment );
175
176 auto w = graphicWidth( menu ) + spacing + textWidth( menu );
177
178 w += padding.left() + padding.right();
179
180 const auto minWidth = menu->strutSizeHint( Q::Segment ).width();
181 return qMax( w, minWidth );
182 }
183
184 qreal segmentHeightInternal( const QskMenu* menu ) const
185 {
186 using Q = QskMenu;
187
188 const auto graphicHeight = menu->strutSizeHint( Q::Icon ).height();
189 const auto textHeight = menu->effectiveFontHeight( Q::Text );
190 const auto padding = menu->paddingHint( Q::Segment );
191
192 qreal h = qMax( graphicHeight, textHeight );
193 h += padding.top() + padding.bottom();
194
195 const auto minHeight = menu->strutSizeHint( Q::Segment ).height();
196 h = qMax( h, minHeight );
197
198 return h;
199 }
200
201 bool m_isCaching;
202
203 mutable qreal m_graphicWidth = -1.0;
204 mutable qreal m_textWidth = -1.0;
205 mutable qreal m_segmentHeight = -1.0;
206 mutable qreal m_segmentWidth = -1.0;
207};
208
209QskMenuSkinlet::QskMenuSkinlet( QskSkin* skin )
210 : Inherited( skin )
211 , m_data( new PrivateData() )
212{
213 appendNodeRoles( { ContentsRole, PanelRole } );
214}
215
216QskMenuSkinlet::~QskMenuSkinlet() = default;
217
218QSGNode* QskMenuSkinlet::updateSubNode(
219 const QskSkinnable* skinnable, quint8 nodeRole, QSGNode* node ) const
220{
221 switch ( nodeRole )
222 {
223 case ContentsRole:
224 {
225 const auto popup = static_cast< const QskPopup* >( skinnable );
226
227 auto rect = popup->contentsRect();
228 if ( rect.isEmpty() )
229 return nullptr;
230
231 return updateContentsNode( popup, node );
232 }
233 }
234
235 return Inherited::updateSubNode( skinnable, nodeRole, node );
236}
237
238QRectF QskMenuSkinlet::cursorRect(
239 const QskSkinnable* skinnable, const QRectF& contentsRect, int index ) const
240{
241 using Q = QskMenu;
242
243 const auto menu = static_cast< const QskMenu* >( skinnable );
244 const auto actions = menu->actions();
245
246 index = qskActionIndex( menu, index );
247
248 QRectF rect;
249
250 if ( index < 0 )
251 {
252 rect = sampleRect( skinnable, contentsRect, Q::Segment, 0 );
253 rect.setBottom( rect.top() );
254 }
255 else if ( index >= actions.count() )
256 {
257 rect = sampleRect( skinnable, contentsRect, Q::Segment, actions.count() - 1 );
258 rect.setTop( rect.bottom() );
259 }
260 else
261 {
262 rect = sampleRect( skinnable, contentsRect, Q::Segment, index );
263 }
264
265 return rect;
266}
267
268QRectF QskMenuSkinlet::subControlRect(
269 const QskSkinnable* skinnable, const QRectF& contentsRect,
270 QskAspect::Subcontrol subControl ) const
271{
272 using Q = QskMenu;
273
274 const auto menu = static_cast< const QskMenu* >( skinnable );
275
276 if( subControl == Q::Panel )
277 {
278 return contentsRect;
279 }
280
281 if( subControl == Q::Cursor )
282 {
283 if ( menu->currentIndex() < 0 )
284 return QRectF();
285
286 const qreal pos = menu->positionHint( Q::Cursor );
287
288 const int pos1 = qFloor( pos );
289 const int pos2 = qCeil( pos );
290
291 auto rect = cursorRect( skinnable, contentsRect, pos1 );
292
293 if ( pos1 != pos2 )
294 {
295 const auto r = cursorRect( skinnable, contentsRect, pos2 );
296
297 const qreal ratio = ( pos - pos1 ) / ( pos2 - pos1 );
298 rect = qskInterpolatedRect( rect, r, ratio );
299 }
300
301 return rect;
302 }
303
304 return Inherited::subControlRect( skinnable, contentsRect, subControl );
305}
306
307QRectF QskMenuSkinlet::sampleRect(
308 const QskSkinnable* skinnable, const QRectF& contentsRect,
309 QskAspect::Subcontrol subControl, int index ) const
310{
311 using Q = QskMenu;
312
313 const auto menu = static_cast< const QskMenu* >( skinnable );
314
315 if ( subControl == Q::Segment )
316 {
317 const auto h = m_data->segmentHeight( menu );
318
319 auto dy = index * h;
320
321 if ( const auto n = menu->actions()[ index ] - index )
322 dy += n * qskPaddedSeparatorHeight( menu );
323
324 const auto r = menu->subControlContentsRect( Q::Panel );
325 return QRectF( r.x(), r.y() + dy, r.width(), h );
326 }
327
328 if ( subControl == QskMenu::Icon || subControl == QskMenu::Text )
329 {
330 const auto r = sampleRect( menu, contentsRect, Q::Segment, index );
331 const auto graphicWidth = m_data->graphicWidth( menu );
332
333 if ( subControl == QskMenu::Icon )
334 {
335 auto graphicRect = r;
336 graphicRect.setWidth( graphicWidth );
337 const auto padding = menu->paddingHint( QskMenu::Icon );
338 graphicRect = graphicRect.marginsRemoved( padding );
339
340 return graphicRect;
341 }
342 else
343 {
344 auto textRect = r;
345
346 if ( graphicWidth > 0.0 )
347 {
348 const auto spacing = skinnable->spacingHint( Q::Segment );
349 textRect.setX( r.x() + graphicWidth + spacing );
350 }
351
352 return textRect;
353 }
354 }
355
356 if ( subControl == QskMenu::Separator )
357 {
358 const auto separators = menu->separators();
359 if ( index >= separators.count() )
360 return QRectF();
361
362 const auto h = qskPaddedSeparatorHeight( menu );
363
364 auto y = index * h;
365
366 if ( const auto n = qskActionIndex( menu, separators[ index ] ) )
367 y += n * m_data->segmentHeight( menu );
368
369 const auto r = menu->subControlContentsRect( Q::Panel );
370 return QRectF( r.left(), y, r.width(), h );
371 }
372
373 return Inherited::sampleRect(
374 skinnable, contentsRect, subControl, index );
375}
376
377int QskMenuSkinlet::sampleIndexAt(
378 const QskSkinnable* skinnable, const QRectF& contentsRect,
379 QskAspect::Subcontrol subControl, const QPointF& pos ) const
380{
381 const PrivateData::CacheGuard guard( m_data.get() );
382 return Inherited::sampleIndexAt( skinnable, contentsRect, subControl, pos );
383}
384
385int QskMenuSkinlet::sampleCount(
386 const QskSkinnable* skinnable, QskAspect::Subcontrol subControl ) const
387{
388 using Q = QskMenu;
389
390 if ( subControl == Q::Segment || subControl == Q::Icon || subControl == Q::Text )
391 {
392 const auto menu = static_cast< const QskMenu* >( skinnable );
393 return menu->actions().count();
394 }
395
396 if ( subControl == Q::Separator )
397 {
398 const auto menu = static_cast< const QskMenu* >( skinnable );
399 return menu->separators().count();
400 }
401
402 return Inherited::sampleCount( skinnable, subControl );
403}
404
405QskAspect::States QskMenuSkinlet::sampleStates(
406 const QskSkinnable* skinnable, QskAspect::Subcontrol subControl, int index ) const
407{
408 using Q = QskMenu;
409 using A = QskAspect;
410
411 auto states = Inherited::sampleStates( skinnable, subControl, index );
412
413 if ( subControl == Q::Segment || subControl == Q::Icon || subControl == Q::Text )
414 {
415 const auto menu = static_cast< const QskMenu* >( skinnable );
416
417 if ( menu->currentIndex() == menu->actions()[ index ] )
418 {
419 states |= Q::Selected;
420
421 if( menu->isPressed() )
422 {
423 states |= Q::Pressed;
424 }
425 else
426 {
427 states &= ~Q::Pressed;
428 }
429 }
430
431 const auto cursorPos = menu->effectiveSkinHint(
432 Q::Segment | Q::Hovered | A::Metric | A::Position ).toPointF();
433
434 if( !cursorPos.isNull() && menu->indexAtPosition( cursorPos ) == index )
435 {
436 states |= Q::Hovered;
437 }
438 else
439 {
440 states &= ~Q::Hovered;
441 }
442 }
443
444 return states;
445}
446
447QVariant QskMenuSkinlet::sampleAt( const QskSkinnable* skinnable,
448 QskAspect::Subcontrol subControl, int index ) const
449{
450 using Q = QskMenu;
451
452 if ( subControl == Q::Icon || subControl == Q::Text )
453 {
454 const auto menu = static_cast< const QskMenu* >( skinnable );
455
456 const auto option = menu->optionAt( index );
457
458 if ( subControl == Q::Icon )
459 return QVariant::fromValue( option.icon().graphic() );
460 else
461 return QVariant::fromValue( option.text() );
462 }
463
464 return Inherited::sampleAt( skinnable, subControl, index );
465}
466
467QSGNode* QskMenuSkinlet::updateContentsNode(
468 const QskPopup* popup, QSGNode* contentsNode ) const
469{
470 const PrivateData::CacheGuard guard( m_data.get() );
471 return updateMenuNode( popup, contentsNode );
472}
473
474QSGNode* QskMenuSkinlet::updateMenuNode(
475 const QskSkinnable* skinnable, QSGNode* contentsNode ) const
476{
477 enum { Panel, Segment, Cursor, Icon, Text, Separator };
478 static QVector< quint8 > roles = { Panel, Separator, Segment, Cursor, Icon, Text };
479
480 if ( contentsNode == nullptr )
481 contentsNode = new QSGNode();
482
483 for ( const auto role : roles )
484 {
485 auto oldNode = QskSGNode::findChildNode( contentsNode, role );
486
487 QSGNode* newNode = nullptr;
488
489 switch( role )
490 {
491 case Panel:
492 {
493 newNode = updateBoxNode( skinnable, oldNode, QskMenu::Panel );
494 break;
495 }
496 case Segment:
497 {
498 newNode = updateSeriesNode( skinnable, QskMenu::Segment, oldNode );
499 break;
500 }
501 case Cursor:
502 {
503 newNode = updateBoxNode( skinnable, oldNode, QskMenu::Cursor );
504 break;
505 }
506 case Icon:
507 {
508 newNode = updateSeriesNode( skinnable, QskMenu::Icon, oldNode );
509 break;
510 }
511 case Text:
512 {
513 newNode = updateSeriesNode( skinnable, QskMenu::Text, oldNode );
514 break;
515 }
516 case Separator:
517 {
518 newNode = updateSeriesNode( skinnable, QskMenu::Separator, oldNode );
519 break;
520 }
521 }
522
523 QskSGNode::replaceChildNode( roles, role, contentsNode, oldNode, newNode );
524 }
525
526 return contentsNode;
527}
528
529QSGNode* QskMenuSkinlet::updateSampleNode( const QskSkinnable* skinnable,
530 QskAspect::Subcontrol subControl, int index, QSGNode* node ) const
531{
532 using Q = QskMenu;
533
534 auto menu = static_cast< const QskMenu* >( skinnable );
535
536 const auto rect = sampleRect( menu, menu->contentsRect(), subControl, index );
537
538 if ( subControl == Q::Segment )
539 {
540 return updateBoxNode( menu, node, rect, subControl );
541 }
542
543 if ( subControl == Q::Icon )
544 {
545 index = menu->actions()[ index ];
546
547 const auto graphic = menu->optionAt( index ).icon().graphic();
548 if ( graphic.isNull() )
549 return nullptr;
550
551 const auto alignment = menu->alignmentHint( subControl, Qt::AlignCenter );
552 const auto filter = menu->effectiveGraphicFilter( subControl );
553
554 return QskSkinlet::updateGraphicNode(
555 menu, node, graphic, filter, rect, alignment );
556 }
557
558 if ( subControl == Q::Text )
559 {
560 index = menu->actions()[ index ];
561
562 const auto text = menu->optionAt( index ).text();
563 if ( text.isEmpty() )
564 return nullptr;
565
566 const auto alignment = menu->alignmentHint(
567 subControl, Qt::AlignVCenter | Qt::AlignLeft );
568
569 return QskSkinlet::updateTextNode( menu, node, rect,
570 alignment, text, Q::Text );
571 }
572
573 if ( subControl == Q::Separator )
574 {
575 auto gradient = menu->gradientHint( subControl );
576 if ( ( gradient.type() == QskGradient::Stops ) && !gradient.isMonochrome() )
577 gradient.setLinearDirection( Qt::Vertical );
578
579 return updateBoxNode( menu, node, rect, gradient, subControl );
580 }
581
582 return nullptr;
583}
584
585QSizeF QskMenuSkinlet::sizeHint( const QskSkinnable* skinnable,
586 Qt::SizeHint which, const QSizeF& ) const
587{
588 if ( which != Qt::PreferredSize )
589 return QSizeF();
590
591 using Q = QskMenu;
592 const auto menu = static_cast< const QskMenu* >( skinnable );
593
594 const PrivateData::CacheGuard guard( m_data.get() );
595
596 qreal w = 0.0;
597 qreal h = 0.0;
598
599 if ( const auto count = sampleCount( skinnable, Q::Segment ) )
600 {
601 w = m_data->segmentWidth( menu );
602 h = count * m_data->segmentHeight( menu );
603 }
604
605 if ( const auto count = sampleCount( skinnable, Q::Separator ) )
606 {
607 h += count * qskPaddedSeparatorHeight( menu );
608 }
609
610 auto hint = skinnable->outerBoxSize( QskMenu::Panel, QSizeF( w, h ) );
611 hint = hint.expandedTo( skinnable->strutSizeHint( QskMenu::Panel ) );
612
613 return hint;
614}
615
616#include "moc_QskMenuSkinlet.cpp"
Lookup key for a QskSkinHintTable.
Definition QskAspect.h:15
Subcontrol
For use within the rendering or lay-outing of a specific QskSkinnable.
Definition QskAspect.h:104
static const QskAspect::State Hovered
Definition QskControl.h:56
QRectF subControlContentsRect(QskAspect::Subcontrol) const
QRectF contentsRect() const
QVariant effectiveSkinHint(QskAspect, QskSkinHintStatus *=nullptr) const
Find the value for a specific aspect.
qreal spacingHint(QskAspect, QskSkinHintStatus *=nullptr) const
Retrieves a spacing hint.
QMarginsF paddingHint(QskAspect, QskSkinHintStatus *=nullptr) const
Retrieves a padding hint.
QMarginsF marginHint(QskAspect, QskSkinHintStatus *=nullptr) const
Retrieves a margin hint.
QskGradient gradientHint(QskAspect, QskSkinHintStatus *=nullptr) const
Retrieves a color hint as gradient.
QFont effectiveFont(QskAspect) const
QSizeF strutSizeHint(QskAspect, QskSkinHintStatus *=nullptr) const
Retrieves a strut size hint.
QSizeF outerBoxSize(QskAspect, const QSizeF &innerBoxSize) const
Calculate the size, when being expanded by paddings, indentations.
QskColorFilter effectiveGraphicFilter(QskAspect::Subcontrol) const
qreal metric(QskAspect, QskSkinHintStatus *=nullptr) const
Retrieves a metric hint.
Qt::Alignment alignmentHint(QskAspect, Qt::Alignment=Qt::Alignment()) const
Retrieves an alignment hint.