VirtualBox

source: vbox/trunk/src/VBox/Frontends/VirtualBox/src/manager/chooser/UIChooserItem.cpp

Last change on this file was 106061, checked in by vboxsync, 4 weeks ago

Copyright year updates by scm.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 22.2 KB
Line 
1/* $Id: UIChooserItem.cpp 106061 2024-09-16 14:03:52Z vboxsync $ */
2/** @file
3 * VBox Qt GUI - UIChooserItem class definition.
4 */
5
6/*
7 * Copyright (C) 2012-2024 Oracle and/or its affiliates.
8 *
9 * This file is part of VirtualBox base platform packages, as
10 * available from https://www.virtualbox.org.
11 *
12 * This program is free software; you can redistribute it and/or
13 * modify it under the terms of the GNU General Public License
14 * as published by the Free Software Foundation, in version 3 of the
15 * License.
16 *
17 * This program is distributed in the hope that it will be useful, but
18 * WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
20 * General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with this program; if not, see <https://www.gnu.org/licenses>.
24 *
25 * SPDX-License-Identifier: GPL-3.0-only
26 */
27
28/* Qt includes: */
29#include <QAccessibleObject>
30#include <QApplication>
31#include <QStyle>
32#include <QPainter>
33#include <QGraphicsScene>
34#include <QStyleOptionFocusRect>
35#include <QGraphicsSceneMouseEvent>
36#include <QStateMachine>
37#include <QPropertyAnimation>
38#include <QSignalTransition>
39#include <QDrag>
40
41/* GUI includes: */
42#include "UIChooserItem.h"
43#include "UIChooserItemGroup.h"
44#include "UIChooserItemGlobal.h"
45#include "UIChooserItemMachine.h"
46#include "UIChooserView.h"
47#include "UIChooserModel.h"
48#include "UIChooserNode.h"
49#include "UIDesktopWidgetWatchdog.h"
50#include "UIImageTools.h"
51
52/* Other VBox includes: */
53#include "iprt/assert.h"
54
55
56/** QAccessibleObject extension used as an accessibility interface for Chooser-view items. */
57class UIAccessibilityInterfaceForUIChooserItem : public QAccessibleObject
58{
59public:
60
61 /** Returns an accessibility interface for passed @a strClassname and @a pObject. */
62 static QAccessibleInterface *pFactory(const QString &strClassname, QObject *pObject)
63 {
64 /* Creating Chooser-view accessibility interface: */
65 if (pObject && strClassname == QLatin1String("UIChooserItem"))
66 return new UIAccessibilityInterfaceForUIChooserItem(pObject);
67
68 /* Null by default: */
69 return 0;
70 }
71
72 /** Constructs an accessibility interface passing @a pObject to the base-class. */
73 UIAccessibilityInterfaceForUIChooserItem(QObject *pObject)
74 : QAccessibleObject(pObject)
75 {}
76
77 /** Returns the parent. */
78 virtual QAccessibleInterface *parent() const RT_OVERRIDE
79 {
80 /* Make sure item still alive: */
81 AssertPtrReturn(item(), 0);
82
83 /* Return the parent: */
84 return QAccessible::queryAccessibleInterface(item()->model()->view());
85 }
86
87 /** Returns the number of children. */
88 virtual int childCount() const RT_OVERRIDE
89 {
90 /* Make sure item still alive: */
91 AssertPtrReturn(item(), 0);
92
93 /* Return the number of group children: */
94 if (item()->type() == UIChooserNodeType_Group)
95 return item()->items().size();
96
97 /* Zero by default: */
98 return 0;
99 }
100
101 /** Returns the child with the passed @a iIndex. */
102 virtual QAccessibleInterface *child(int iIndex) const RT_OVERRIDE
103 {
104 /* Make sure item still alive: */
105 AssertPtrReturn(item(), 0);
106 /* Make sure index is valid: */
107 AssertReturn(iIndex >= 0 && iIndex < childCount(), 0);
108
109 /* Return the child with the passed iIndex: */
110 return QAccessible::queryAccessibleInterface(item()->items().at(iIndex));
111 }
112
113 /** Returns the index of the passed @a pChild. */
114 virtual int indexOfChild(const QAccessibleInterface *pChild) const RT_OVERRIDE
115 {
116 /* Search for corresponding child: */
117 for (int i = 0; i < childCount(); ++i)
118 if (child(i) == pChild)
119 return i;
120
121 /* -1 by default: */
122 return -1;
123 }
124
125 /** Returns the rect. */
126 virtual QRect rect() const RT_OVERRIDE
127 {
128 /* Now goes the mapping: */
129 const QSize itemSize = item()->size().toSize();
130 const QPointF itemPosInScene = item()->mapToScene(QPointF(0, 0));
131 const QPoint itemPosInView = item()->model()->view()->mapFromScene(itemPosInScene);
132 const QPoint itemPosInScreen = item()->model()->view()->mapToGlobal(itemPosInView);
133 const QRect itemRectInScreen = QRect(itemPosInScreen, itemSize);
134 return itemRectInScreen;
135 }
136
137 /** Returns a text for the passed @a enmTextRole. */
138 virtual QString text(QAccessible::Text enmTextRole) const RT_OVERRIDE
139 {
140 /* Make sure item still alive: */
141 AssertPtrReturn(item(), QString());
142
143 switch (enmTextRole)
144 {
145 case QAccessible::Name: return item()->name();
146 case QAccessible::Description: return item()->description();
147 default: break;
148 }
149
150 /* Null-string by default: */
151 return QString();
152 }
153
154 /** Returns the role. */
155 virtual QAccessible::Role role() const RT_OVERRIDE
156 {
157 /* Make sure item still alive: */
158 AssertPtrReturn(item(), QAccessible::NoRole);
159
160 /* Return the role of group: */
161 if (item()->type() == UIChooserNodeType_Group)
162 return QAccessible::List;
163
164 /* ListItem by default: */
165 return QAccessible::ListItem;
166 }
167
168 /** Returns the state. */
169 virtual QAccessible::State state() const RT_OVERRIDE
170 {
171 /* Make sure item still alive: */
172 AssertPtrReturn(item(), QAccessible::State());
173
174 /* Compose the state: */
175 QAccessible::State state;
176 state.focusable = true;
177 state.selectable = true;
178
179 /* Compose the state of first selected-item: */
180 if (item() && item() == item()->model()->firstSelectedItem())
181 {
182 state.active = true;
183 state.focused = true;
184 state.selected = true;
185 }
186
187 /* Compose the state of group: */
188 if (item()->type() == UIChooserNodeType_Group)
189 {
190 state.expandable = true;
191 if (!item()->toGroupItem()->isClosed())
192 state.expanded = true;
193 }
194
195 /* Return the state: */
196 return state;
197 }
198
199private:
200
201 /** Returns corresponding Chooser-view item. */
202 UIChooserItem *item() const { return qobject_cast<UIChooserItem*>(object()); }
203};
204
205
206/*********************************************************************************************************************************
207* Class UIChooserDisabledItemEffect implementation. *
208*********************************************************************************************************************************/
209
210UIChooserDisabledItemEffect::UIChooserDisabledItemEffect(int iBlurRadius, QObject *pParent /* = 0 */)
211 : QGraphicsEffect(pParent)
212 , m_iBlurRadius(iBlurRadius)
213{
214}
215
216void UIChooserDisabledItemEffect::draw(QPainter *pPainter)
217{
218 QPoint offset;
219 QPixmap pixmap;
220 /* Get the original pixmap: */
221 pixmap = sourcePixmap(Qt::LogicalCoordinates, &offset);
222 QImage resultImage;
223 /* Apply our blur and grayscale filters to the original pixmap: */
224 UIImageTools::blurImage(pixmap.toImage(), resultImage, m_iBlurRadius);
225 pixmap.convertFromImage(UIImageTools::toGray(resultImage));
226 QWidget *pParentWidget = qobject_cast<QWidget*>(parent());
227 pixmap.setDevicePixelRatio( pParentWidget
228 ? UIDesktopWidgetWatchdog::devicePixelRatioActual(pParentWidget)
229 : UIDesktopWidgetWatchdog::devicePixelRatioActual());
230 /* Use the filtered pixmap: */
231 pPainter->drawPixmap(offset, pixmap);
232}
233
234
235/*********************************************************************************************************************************
236* Class UIChooserItem implementation. *
237*********************************************************************************************************************************/
238
239UIChooserItem::UIChooserItem(UIChooserItem *pParent, UIChooserNode *pNode,
240 int iDefaultValue /* = 0 */, int iHoveredValue /* = 100 */)
241 : QIGraphicsWidget(pParent)
242 , m_pParent(pParent)
243 , m_pNode(pNode)
244 , m_fHovered(false)
245 , m_fSelected(false)
246 , m_pHoveringMachine(0)
247 , m_pHoveringAnimationForward(0)
248 , m_pHoveringAnimationBackward(0)
249 , m_iAnimationDuration(400)
250 , m_iDefaultValue(iDefaultValue)
251 , m_iHoveredValue(iHoveredValue)
252 , m_iAnimatedValue(m_iDefaultValue)
253 , m_pDisabledEffect(0)
254 , m_enmDragTokenPlace(UIChooserItemDragToken_Off)
255 , m_iDragTokenDarkness(110)
256{
257 /* Install Chooser-view item accessibility interface factory: */
258 QAccessible::installFactory(UIAccessibilityInterfaceForUIChooserItem::pFactory);
259
260 /* Assign item for passed node: */
261 node()->setItem(this);
262
263 /* Basic item setup: */
264 setOwnedByLayout(false);
265 setAcceptDrops(true);
266 setFocusPolicy(Qt::NoFocus);
267 setFlag(QGraphicsItem::ItemIsSelectable, false);
268 setAcceptHoverEvents(!isRoot());
269
270 /* Non-root item? */
271 if (!isRoot())
272 {
273 /* Create hovering animation machine: */
274 m_pHoveringMachine = new QStateMachine(this);
275 if (m_pHoveringMachine)
276 {
277 /* Create 'default' state: */
278 QState *pStateDefault = new QState(m_pHoveringMachine);
279 /* Create 'hovered' state: */
280 QState *pStateHovered = new QState(m_pHoveringMachine);
281
282 /* Configure 'default' state: */
283 if (pStateDefault)
284 {
285 /* When we entering default state => we assigning animatedValue to m_iDefaultValue: */
286 pStateDefault->assignProperty(this, "animatedValue", m_iDefaultValue);
287
288 /* Add state transitions: */
289 QSignalTransition *pDefaultToHovered = pStateDefault->addTransition(this, SIGNAL(sigHoverEnter()), pStateHovered);
290 if (pDefaultToHovered)
291 {
292 /* Create forward animation: */
293 m_pHoveringAnimationForward = new QPropertyAnimation(this, "animatedValue", this);
294 if (m_pHoveringAnimationForward)
295 {
296 m_pHoveringAnimationForward->setDuration(m_iAnimationDuration);
297 m_pHoveringAnimationForward->setStartValue(m_iDefaultValue);
298 m_pHoveringAnimationForward->setEndValue(m_iHoveredValue);
299
300 /* Add to transition: */
301 pDefaultToHovered->addAnimation(m_pHoveringAnimationForward);
302 }
303 }
304 }
305
306 /* Configure 'hovered' state: */
307 if (pStateHovered)
308 {
309 /* When we entering hovered state => we assigning animatedValue to m_iHoveredValue: */
310 pStateHovered->assignProperty(this, "animatedValue", m_iHoveredValue);
311
312 /* Add state transitions: */
313 QSignalTransition *pHoveredToDefault = pStateHovered->addTransition(this, SIGNAL(sigHoverLeave()), pStateDefault);
314 if (pHoveredToDefault)
315 {
316 /* Create backward animation: */
317 m_pHoveringAnimationBackward = new QPropertyAnimation(this, "animatedValue", this);
318 if (m_pHoveringAnimationBackward)
319 {
320 m_pHoveringAnimationBackward->setDuration(m_iAnimationDuration);
321 m_pHoveringAnimationBackward->setStartValue(m_iHoveredValue);
322 m_pHoveringAnimationBackward->setEndValue(m_iDefaultValue);
323
324 /* Add to transition: */
325 pHoveredToDefault->addAnimation(m_pHoveringAnimationBackward);
326 }
327 }
328 }
329
330 /* Initial state is 'default': */
331 m_pHoveringMachine->setInitialState(pStateDefault);
332 /* Start state-machine: */
333 m_pHoveringMachine->start();
334 }
335
336 /* Allocate the effect instance which we use when the item is marked as disabled: */
337 m_pDisabledEffect = new UIChooserDisabledItemEffect(1 /* Blur Radius */, model()->view());
338 if (m_pDisabledEffect)
339 {
340 setGraphicsEffect(m_pDisabledEffect);
341 m_pDisabledEffect->setEnabled(node()->isDisabled());
342 }
343 }
344}
345
346UIChooserItemGroup *UIChooserItem::toGroupItem()
347{
348 UIChooserItemGroup *pItem = qgraphicsitem_cast<UIChooserItemGroup*>(this);
349 AssertMsg(pItem, ("Trying to cast invalid item type to UIChooserItemGroup!"));
350 return pItem;
351}
352
353UIChooserItemGlobal *UIChooserItem::toGlobalItem()
354{
355 UIChooserItemGlobal *pItem = qgraphicsitem_cast<UIChooserItemGlobal*>(this);
356 AssertMsg(pItem, ("Trying to cast invalid item type to UIChooserItemGlobal!"));
357 return pItem;
358}
359
360UIChooserItemMachine *UIChooserItem::toMachineItem()
361{
362 UIChooserItemMachine *pItem = qgraphicsitem_cast<UIChooserItemMachine*>(this);
363 AssertMsg(pItem, ("Trying to cast invalid item type to UIChooserItemMachine!"));
364 return pItem;
365}
366
367UIChooserModel *UIChooserItem::model() const
368{
369 UIChooserModel *pModel = qobject_cast<UIChooserModel*>(QIGraphicsWidget::scene()->parent());
370 AssertMsg(pModel, ("Incorrect graphics scene parent set!"));
371 return pModel;
372}
373
374bool UIChooserItem::isRoot() const
375{
376 return node()->isRoot();
377}
378
379QString UIChooserItem::name() const
380{
381 return node()->name();
382}
383
384QString UIChooserItem::fullName() const
385{
386 return node()->fullName();
387}
388
389QString UIChooserItem::description() const
390{
391 return node()->description();
392}
393
394QString UIChooserItem::definition() const
395{
396 return node()->definition();
397}
398
399bool UIChooserItem::isFavorite() const
400{
401 return node()->isFavorite();
402}
403
404void UIChooserItem::setFavorite(bool fFavorite)
405{
406 node()->setFavorite(fFavorite);
407 if (m_pParent)
408 m_pParent->toGroupItem()->updateFavorites();
409}
410
411int UIChooserItem::position() const
412{
413 return node()->position();
414}
415
416bool UIChooserItem::isHovered() const
417{
418 return m_fHovered;
419}
420
421bool UIChooserItem::isSelected() const
422{
423 return m_fSelected;
424}
425
426void UIChooserItem::setSelected(bool fSelected)
427{
428 m_fSelected = fSelected;
429}
430
431void UIChooserItem::setDisabledEffect(bool fOn)
432{
433 if (m_pDisabledEffect)
434 m_pDisabledEffect->setEnabled(fOn);
435}
436
437void UIChooserItem::updateGeometry()
438{
439 /* Call to base-class: */
440 QIGraphicsWidget::updateGeometry();
441
442 /* Update parent's geometry: */
443 if (parentItem())
444 parentItem()->updateGeometry();
445}
446
447void UIChooserItem::makeSureItsVisible()
448{
449 /* Get parrent item: */
450 UIChooserItemGroup *pParentItem = parentItem()->toGroupItem();
451 if (!pParentItem)
452 return;
453 /* If item is not visible. That is all the parent group(s) are opened (expanded): */
454 if (!isVisible())
455 {
456 /* We should make parent visible: */
457 pParentItem->makeSureItsVisible();
458 /* And make sure its opened: */
459 if (pParentItem->isClosed())
460 pParentItem->open(false);
461 }
462}
463
464UIChooserItemDragToken UIChooserItem::dragTokenPlace() const
465{
466 return m_enmDragTokenPlace;
467}
468
469void UIChooserItem::setDragTokenPlace(UIChooserItemDragToken enmPlace)
470{
471 /* Something changed? */
472 if (m_enmDragTokenPlace != enmPlace)
473 {
474 m_enmDragTokenPlace = enmPlace;
475 update();
476 }
477}
478
479void UIChooserItem::hoverMoveEvent(QGraphicsSceneHoverEvent *)
480{
481 if (!m_fHovered)
482 {
483 m_fHovered = true;
484 emit sigHoverEnter();
485 }
486 update();
487}
488
489void UIChooserItem::hoverLeaveEvent(QGraphicsSceneHoverEvent *)
490{
491 if (m_fHovered)
492 {
493 m_fHovered = false;
494 emit sigHoverLeave();
495 update();
496 }
497}
498
499void UIChooserItem::mousePressEvent(QGraphicsSceneMouseEvent *pEvent)
500{
501 /* By default, non-moveable and non-selectable items
502 * can't grab mouse-press events which is required
503 * to grab further mouse-move events which we wants... */
504 if (isRoot())
505 pEvent->ignore();
506 else
507 pEvent->accept();
508}
509
510void UIChooserItem::mouseMoveEvent(QGraphicsSceneMouseEvent *pEvent)
511{
512 /* Make sure item is really dragged: */
513 if (QLineF(pEvent->screenPos(),
514 pEvent->buttonDownScreenPos(Qt::LeftButton)).length() <
515 QApplication::startDragDistance())
516 return;
517
518 /* Initialize dragging: */
519 QDrag *pDrag = new QDrag(pEvent->widget());
520 model()->setCurrentDragObject(pDrag);
521 pDrag->setPixmap(toPixmap());
522 pDrag->setMimeData(createMimeData());
523 pDrag->exec(Qt::MoveAction | Qt::CopyAction, Qt::MoveAction);
524}
525
526void UIChooserItem::dragMoveEvent(QGraphicsSceneDragDropEvent *pEvent)
527{
528 /* Make sure we are non-root: */
529 if (!isRoot())
530 {
531 /* Allow drag tokens only for the same item type as current: */
532 bool fAllowDragToken = false;
533 if ((type() == UIChooserNodeType_Group &&
534 pEvent->mimeData()->hasFormat(UIChooserItemGroup::className())) ||
535 (type() == UIChooserNodeType_Machine &&
536 pEvent->mimeData()->hasFormat(UIChooserItemMachine::className())))
537 fAllowDragToken = true;
538 /* Do we need a drag-token? */
539 if (fAllowDragToken)
540 {
541 QPoint p = pEvent->pos().toPoint();
542 if (p.y() < 10)
543 setDragTokenPlace(UIChooserItemDragToken_Up);
544 else if (p.y() > minimumSizeHint().toSize().height() - 10)
545 setDragTokenPlace(UIChooserItemDragToken_Down);
546 else
547 setDragTokenPlace(UIChooserItemDragToken_Off);
548 }
549 }
550 /* Check if drop is allowed: */
551 pEvent->setAccepted(isDropAllowed(pEvent, dragTokenPlace()));
552}
553
554void UIChooserItem::dragLeaveEvent(QGraphicsSceneDragDropEvent *)
555{
556 resetDragToken();
557}
558
559void UIChooserItem::dropEvent(QGraphicsSceneDragDropEvent *pEvent)
560{
561 /* Do we have token active? */
562 switch (dragTokenPlace())
563 {
564 case UIChooserItemDragToken_Off:
565 {
566 /* Its our drop, processing: */
567 processDrop(pEvent);
568 break;
569 }
570 default:
571 {
572 /* Its parent drop, passing: */
573 parentItem()->processDrop(pEvent, this, dragTokenPlace());
574 break;
575 }
576 }
577}
578
579/* static */
580QSize UIChooserItem::textSize(const QFont &font, QPaintDevice *pPaintDevice, const QString &strText)
581{
582 /* Make sure text is not empty: */
583 if (strText.isEmpty())
584 return QSize(0, 0);
585
586 /* Return text size, based on font-metrics: */
587 const QFontMetrics fm(font, pPaintDevice);
588 return QSize(fm.horizontalAdvance(strText), fm.height());
589}
590
591/* static */
592int UIChooserItem::textWidth(const QFont &font, QPaintDevice *pPaintDevice, int iCount)
593{
594 /* Return text width: */
595 const QFontMetrics fm(font, pPaintDevice);
596 QString strString;
597 strString.fill('_', iCount);
598 return fm.horizontalAdvance(strString);
599}
600
601/* static */
602QString UIChooserItem::compressText(const QFont &font, QPaintDevice *pPaintDevice, QString strText, int iWidth)
603{
604 /* Check if passed text is empty: */
605 if (strText.isEmpty())
606 return strText;
607
608 /* Check if passed text fits maximum width: */
609 const QFontMetrics fm(font, pPaintDevice);
610 if (fm.horizontalAdvance(strText) <= iWidth)
611 return strText;
612
613 /* Truncate otherwise: */
614 const QString strEllipsis = QString("...");
615 const int iEllipsisWidth = fm.horizontalAdvance(strEllipsis + " ");
616 while (!strText.isEmpty() && fm.horizontalAdvance(strText) + iEllipsisWidth > iWidth)
617 strText.truncate(strText.size() - 1);
618 return strText + strEllipsis;
619}
620
621/* static */
622void UIChooserItem::paintFrameRect(QPainter *pPainter, bool fIsSelected, int iRadius,
623 const QRect &rectangle)
624{
625 pPainter->save();
626 QPalette pal = QApplication::palette();
627 QColor base = pal.color(QPalette::Active, fIsSelected ? QPalette::Highlight : QPalette::Window);
628 pPainter->setPen(base.darker(160));
629 if (iRadius)
630 pPainter->drawRoundedRect(rectangle, iRadius, iRadius);
631 else
632 pPainter->drawRect(rectangle);
633 pPainter->restore();
634}
635
636/* static */
637void UIChooserItem::paintPixmap(QPainter *pPainter, const QPoint &point,
638 const QPixmap &pixmap)
639{
640 pPainter->drawPixmap(point, pixmap);
641}
642
643/* static */
644void UIChooserItem::paintText(QPainter *pPainter, QPoint point,
645 const QFont &font, QPaintDevice *pPaintDevice,
646 const QString &strText)
647{
648 /* Prepare variables: */
649 QFontMetrics fm(font, pPaintDevice);
650 point += QPoint(0, fm.ascent());
651
652 /* Draw text: */
653 pPainter->save();
654 pPainter->setFont(font);
655 pPainter->drawText(point, strText);
656 pPainter->restore();
657}
658
659/* static */
660void UIChooserItem::paintFlatButton(QPainter *pPainter, const QRect &rectangle, const QPoint &cursorPosition)
661{
662 /* Save painter: */
663 pPainter->save();
664
665 /* Prepare colors: */
666 const QColor color = QApplication::palette().color(QPalette::Active, QPalette::Button);
667
668 /* Prepare pen: */
669 QPen pen;
670 pen.setColor(color);
671 pen.setWidth(0);
672 pPainter->setPen(pen);
673
674 /* Apply clipping path: */
675 QPainterPath path;
676 path.addRect(rectangle);
677 pPainter->setClipPath(path);
678
679 /* Paint active background: */
680 QRadialGradient grad(rectangle.center(), rectangle.width(), cursorPosition);
681 QColor color1 = color;
682 color1.setAlpha(50);
683 QColor color2 = color;
684 color2.setAlpha(250);
685 grad.setColorAt(0, color1);
686 grad.setColorAt(1, color2);
687 pPainter->fillRect(rectangle.adjusted(0, 0, -1, -1), grad);
688
689 /* Paint frame: */
690 pPainter->drawRect(rectangle.adjusted(0, 0, -1, -1));
691
692 /* Restore painter: */
693 pPainter->restore();
694}
695
696
697/*********************************************************************************************************************************
698* Class UIChooserItemMimeData implementation. *
699*********************************************************************************************************************************/
700
701UIChooserItemMimeData::UIChooserItemMimeData(UIChooserItem *pItem)
702 : m_pItem(pItem)
703{
704}
705
706bool UIChooserItemMimeData::hasFormat(const QString &strMimeType) const
707{
708 if (strMimeType == QString(m_pItem->metaObject()->className()))
709 return true;
710 return false;
711}
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette