1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * This file is part of the LibreOffice project.
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
13 #include <QtFrame.hxx>
14 #include <QtInstance.hxx>
15 #include <QtMainWindow.hxx>
17 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
18 #include <QtWidgets/QActionGroup>
20 #include <QtGui/QActionGroup>
23 #include <QtWidgets/QMenuBar>
24 #include <QtWidgets/QPushButton>
26 #include <o3tl/safeint.hxx>
27 #include <vcl/svapp.hxx>
28 #include <sal/log.hxx>
30 #include <strings.hrc>
31 #include <bitmaps.hlst>
33 #include <vcl/toolkit/floatwin.hxx>
36 QtMenu::QtMenu(bool bMenuBar
)
38 , mpParentSalMenu(nullptr)
46 bool QtMenu::VisibleMenuBar() { return true; }
48 void QtMenu::InsertMenuItem(QtMenuItem
* pSalMenuItem
, unsigned nPos
)
50 sal_uInt16 nId
= pSalMenuItem
->mnId
;
51 OUString aText
= mpVCLMenu
->GetItemText(nId
);
52 NativeItemText(aText
);
53 vcl::KeyCode nAccelKey
= mpVCLMenu
->GetAccelKey(nId
);
55 pSalMenuItem
->mpAction
.reset();
56 pSalMenuItem
->mpMenu
.reset();
61 if (validateQMenuBar())
63 QMenu
* pQMenu
= new QMenu(toQString(aText
), nullptr);
64 pSalMenuItem
->mpMenu
.reset(pQMenu
);
66 if ((nPos
!= MENU_APPEND
)
67 && (static_cast<size_t>(nPos
) < o3tl::make_unsigned(mpQMenuBar
->actions().size())))
69 mpQMenuBar
->insertMenu(mpQMenuBar
->actions()[nPos
], pQMenu
);
73 mpQMenuBar
->addMenu(pQMenu
);
76 // correct parent menu for generated menu
77 if (pSalMenuItem
->mpSubMenu
)
79 pSalMenuItem
->mpSubMenu
->mpQMenu
= pQMenu
;
82 connect(pQMenu
, &QMenu::aboutToShow
, this,
83 [pSalMenuItem
] { slotMenuAboutToShow(pSalMenuItem
); });
84 connect(pQMenu
, &QMenu::aboutToHide
, this,
85 [pSalMenuItem
] { slotMenuAboutToHide(pSalMenuItem
); });
92 // no QMenu set, instantiate own one
93 mpOwnedQMenu
.reset(new QMenu
);
94 mpQMenu
= mpOwnedQMenu
.get();
97 if (pSalMenuItem
->mpSubMenu
)
100 QMenu
* pQMenu
= new QMenu(toQString(aText
), nullptr);
101 pSalMenuItem
->mpMenu
.reset(pQMenu
);
103 if ((nPos
!= MENU_APPEND
)
104 && (static_cast<size_t>(nPos
) < o3tl::make_unsigned(mpQMenu
->actions().size())))
106 mpQMenu
->insertMenu(mpQMenu
->actions()[nPos
], pQMenu
);
110 mpQMenu
->addMenu(pQMenu
);
113 // correct parent menu for generated menu
114 pSalMenuItem
->mpSubMenu
->mpQMenu
= pQMenu
;
116 ReinitializeActionGroup(nPos
);
118 // clear all action groups since menu is recreated
119 pSalMenuItem
->mpSubMenu
->ResetAllActionGroups();
121 connect(pQMenu
, &QMenu::aboutToShow
, this,
122 [pSalMenuItem
] { slotMenuAboutToShow(pSalMenuItem
); });
123 connect(pQMenu
, &QMenu::aboutToHide
, this,
124 [pSalMenuItem
] { slotMenuAboutToHide(pSalMenuItem
); });
128 if (pSalMenuItem
->mnType
== MenuItemType::SEPARATOR
)
130 QAction
* pAction
= new QAction(nullptr);
131 pSalMenuItem
->mpAction
.reset(pAction
);
132 pAction
->setSeparator(true);
134 if ((nPos
!= MENU_APPEND
)
135 && (static_cast<size_t>(nPos
) < o3tl::make_unsigned(mpQMenu
->actions().size())))
137 mpQMenu
->insertAction(mpQMenu
->actions()[nPos
], pAction
);
141 mpQMenu
->addAction(pAction
);
144 ReinitializeActionGroup(nPos
);
149 QAction
* pAction
= new QAction(toQString(aText
), nullptr);
150 pSalMenuItem
->mpAction
.reset(pAction
);
152 if ((nPos
!= MENU_APPEND
)
153 && (static_cast<size_t>(nPos
) < o3tl::make_unsigned(mpQMenu
->actions().size())))
155 mpQMenu
->insertAction(mpQMenu
->actions()[nPos
], pAction
);
159 mpQMenu
->addAction(pAction
);
162 ReinitializeActionGroup(nPos
);
164 UpdateActionGroupItem(pSalMenuItem
);
166 pAction
->setShortcut(toQString(nAccelKey
.GetName()));
168 connect(pAction
, &QAction::triggered
, this,
169 [pSalMenuItem
] { slotMenuTriggered(pSalMenuItem
); });
174 QAction
* pAction
= pSalMenuItem
->getAction();
177 pAction
->setEnabled(pSalMenuItem
->mbEnabled
);
178 pAction
->setVisible(pSalMenuItem
->mbVisible
);
182 void QtMenu::ReinitializeActionGroup(unsigned nPos
)
184 const unsigned nCount
= GetItemCount();
191 if (nPos
== MENU_APPEND
)
195 else if (nPos
>= nCount
)
200 QtMenuItem
* pPrevItem
= (nPos
> 0) ? GetItemAtPos(nPos
- 1) : nullptr;
201 QtMenuItem
* pCurrentItem
= GetItemAtPos(nPos
);
202 QtMenuItem
* pNextItem
= (nPos
< nCount
- 1) ? GetItemAtPos(nPos
+ 1) : nullptr;
204 if (pCurrentItem
->mnType
== MenuItemType::SEPARATOR
)
206 pCurrentItem
->mpActionGroup
.reset();
208 // if it's inserted into middle of existing group, split it into two groups:
209 // first goes original group, after separator goes new group
210 if (pPrevItem
&& pPrevItem
->mpActionGroup
&& pNextItem
&& pNextItem
->mpActionGroup
211 && (pPrevItem
->mpActionGroup
== pNextItem
->mpActionGroup
))
213 std::shared_ptr
<QActionGroup
> pFirstActionGroup
= pPrevItem
->mpActionGroup
;
214 auto pSecondActionGroup
= std::make_shared
<QActionGroup
>(nullptr);
215 pSecondActionGroup
->setExclusive(true);
217 auto actions
= pFirstActionGroup
->actions();
219 for (unsigned idx
= nPos
+ 1; idx
< nCount
; ++idx
)
221 QtMenuItem
* pModifiedItem
= GetItemAtPos(idx
);
223 if ((!pModifiedItem
) || (!pModifiedItem
->mpActionGroup
))
228 pModifiedItem
->mpActionGroup
= pSecondActionGroup
;
229 auto action
= pModifiedItem
->getAction();
231 if (actions
.contains(action
))
233 pFirstActionGroup
->removeAction(action
);
234 pSecondActionGroup
->addAction(action
);
241 if (!pCurrentItem
->mpActionGroup
)
243 // unless element is inserted between two separators, or a separator and an end of vector, use neighbouring group since it's shared
244 if (pPrevItem
&& pPrevItem
->mpActionGroup
)
246 pCurrentItem
->mpActionGroup
= pPrevItem
->mpActionGroup
;
248 else if (pNextItem
&& pNextItem
->mpActionGroup
)
250 pCurrentItem
->mpActionGroup
= pNextItem
->mpActionGroup
;
254 pCurrentItem
->mpActionGroup
= std::make_shared
<QActionGroup
>(nullptr);
255 pCurrentItem
->mpActionGroup
->setExclusive(true);
259 // if there's also a different group after this element, merge it
260 if (pNextItem
&& pNextItem
->mpActionGroup
261 && (pCurrentItem
->mpActionGroup
!= pNextItem
->mpActionGroup
))
263 auto pFirstCheckedAction
= pCurrentItem
->mpActionGroup
->checkedAction();
264 auto pSecondCheckedAction
= pNextItem
->mpActionGroup
->checkedAction();
265 auto actions
= pNextItem
->mpActionGroup
->actions();
267 // first move all actions from second group to first one, and if first group already has checked action,
268 // and second group also has a checked action, uncheck action from second group
269 for (auto action
: actions
)
271 pNextItem
->mpActionGroup
->removeAction(action
);
273 if (pFirstCheckedAction
&& pSecondCheckedAction
&& (action
== pSecondCheckedAction
))
275 action
->setChecked(false);
278 pCurrentItem
->mpActionGroup
->addAction(action
);
281 // now replace all pointers to second group with pointers to first group
282 for (unsigned idx
= nPos
+ 1; idx
< nCount
; ++idx
)
284 QtMenuItem
* pModifiedItem
= GetItemAtPos(idx
);
286 if ((!pModifiedItem
) || (!pModifiedItem
->mpActionGroup
))
291 pModifiedItem
->mpActionGroup
= pCurrentItem
->mpActionGroup
;
297 void QtMenu::ResetAllActionGroups()
299 for (unsigned nItem
= 0; nItem
< GetItemCount(); ++nItem
)
301 QtMenuItem
* pSalMenuItem
= GetItemAtPos(nItem
);
302 pSalMenuItem
->mpActionGroup
.reset();
306 void QtMenu::UpdateActionGroupItem(const QtMenuItem
* pSalMenuItem
)
308 QAction
* pAction
= pSalMenuItem
->getAction();
312 bool bChecked
= mpVCLMenu
->IsItemChecked(pSalMenuItem
->mnId
);
313 MenuItemBits itemBits
= mpVCLMenu
->GetItemBits(pSalMenuItem
->mnId
);
315 if (itemBits
& MenuItemBits::RADIOCHECK
)
317 pAction
->setCheckable(true);
319 if (pSalMenuItem
->mpActionGroup
)
321 pSalMenuItem
->mpActionGroup
->addAction(pAction
);
324 pAction
->setChecked(bChecked
);
328 pAction
->setActionGroup(nullptr);
330 if (itemBits
& MenuItemBits::CHECKABLE
)
332 pAction
->setCheckable(true);
333 pAction
->setChecked(bChecked
);
337 pAction
->setChecked(false);
338 pAction
->setCheckable(false);
343 void QtMenu::InsertItem(SalMenuItem
* pSalMenuItem
, unsigned nPos
)
345 SolarMutexGuard aGuard
;
346 QtMenuItem
* pItem
= static_cast<QtMenuItem
*>(pSalMenuItem
);
348 if (nPos
== MENU_APPEND
)
349 maItems
.push_back(pItem
);
351 maItems
.insert(maItems
.begin() + nPos
, pItem
);
353 pItem
->mpParentMenu
= this;
355 InsertMenuItem(pItem
, nPos
);
358 void QtMenu::RemoveItem(unsigned nPos
)
360 SolarMutexGuard aGuard
;
362 if (nPos
>= maItems
.size())
365 QtMenuItem
* pItem
= maItems
[nPos
];
366 pItem
->mpAction
.reset();
367 pItem
->mpMenu
.reset();
369 maItems
.erase(maItems
.begin() + nPos
);
371 // Recalculate action groups if necessary:
372 // if separator between two QActionGroups was removed,
373 // it may be needed to merge them
376 ReinitializeActionGroup(nPos
- 1);
380 void QtMenu::SetSubMenu(SalMenuItem
* pSalMenuItem
, SalMenu
* pSubMenu
, unsigned nPos
)
382 SolarMutexGuard aGuard
;
383 QtMenuItem
* pItem
= static_cast<QtMenuItem
*>(pSalMenuItem
);
384 QtMenu
* pQSubMenu
= static_cast<QtMenu
*>(pSubMenu
);
386 pItem
->mpSubMenu
= pQSubMenu
;
387 // at this point the pointer to parent menu may be outdated, update it too
388 pItem
->mpParentMenu
= this;
390 if (pQSubMenu
!= nullptr)
392 pQSubMenu
->mpParentSalMenu
= this;
393 pQSubMenu
->mpQMenu
= pItem
->mpMenu
.get();
396 // if it's not a menu bar item, then convert it to corresponding item if type if necessary.
397 // If submenu is present and it's an action, convert it to menu.
398 // If submenu is not present and it's a menu, convert it to action.
399 // It may be fine to proceed in any case, but by skipping other cases
400 // amount of unneeded actions taken should be reduced.
401 if (pItem
->mpParentMenu
->mbMenuBar
|| (pQSubMenu
&& pItem
->mpMenu
)
402 || ((!pQSubMenu
) && pItem
->mpAction
))
407 InsertMenuItem(pItem
, nPos
);
410 void QtMenu::SetFrame(const SalFrame
* pFrame
)
412 auto* pSalInst(static_cast<QtInstance
*>(GetSalData()->m_pInstance
));
414 if (!pSalInst
->IsMainThread())
416 pSalInst
->RunInMainThread([this, pFrame
]() { SetFrame(pFrame
); });
420 SolarMutexGuard aGuard
;
422 mpFrame
= const_cast<QtFrame
*>(static_cast<const QtFrame
*>(pFrame
));
424 mpFrame
->SetMenu(this);
426 QtMainWindow
* pMainWindow
= mpFrame
->GetTopLevelWindow();
430 mpQMenuBar
= new QMenuBar();
431 pMainWindow
->setMenuBar(mpQMenuBar
);
433 QPushButton
* pButton
= static_cast<QPushButton
*>(mpQMenuBar
->cornerWidget(Qt::TopRightCorner
));
435 connect(pButton
, &QPushButton::clicked
, this, &QtMenu::slotCloseDocument
);
438 DoFullMenuUpdate(mpVCLMenu
);
441 void QtMenu::DoFullMenuUpdate(Menu
* pMenuBar
)
443 // clear action groups since menu is rebuilt
444 ResetAllActionGroups();
445 ShowCloseButton(false);
447 for (sal_Int32 nItem
= 0; nItem
< static_cast<sal_Int32
>(GetItemCount()); nItem
++)
449 QtMenuItem
* pSalMenuItem
= GetItemAtPos(nItem
);
450 InsertMenuItem(pSalMenuItem
, nItem
);
451 SetItemImage(nItem
, pSalMenuItem
, pSalMenuItem
->maImage
);
452 const bool bShowDisabled
453 = bool(pMenuBar
->GetMenuFlags() & MenuFlags::AlwaysShowDisabledEntries
)
454 || !bool(pMenuBar
->GetMenuFlags() & MenuFlags::HideDisabledEntries
);
455 const bool bVisible
= bShowDisabled
|| mpVCLMenu
->IsItemEnabled(pSalMenuItem
->mnId
);
456 pSalMenuItem
->getAction()->setVisible(bVisible
);
458 if (pSalMenuItem
->mpSubMenu
!= nullptr)
460 pMenuBar
->HandleMenuActivateEvent(pSalMenuItem
->mpSubMenu
->GetMenu());
461 pSalMenuItem
->mpSubMenu
->DoFullMenuUpdate(pMenuBar
);
462 pMenuBar
->HandleMenuDeActivateEvent(pSalMenuItem
->mpSubMenu
->GetMenu());
467 void QtMenu::ShowItem(unsigned nPos
, bool bShow
)
469 if (nPos
< maItems
.size())
471 QtMenuItem
* pSalMenuItem
= GetItemAtPos(nPos
);
472 QAction
* pAction
= pSalMenuItem
->getAction();
474 pAction
->setVisible(bShow
);
475 pSalMenuItem
->mbVisible
= bShow
;
479 void QtMenu::SetItemBits(unsigned nPos
, MenuItemBits
)
481 if (nPos
< maItems
.size())
483 QtMenuItem
* pSalMenuItem
= GetItemAtPos(nPos
);
484 UpdateActionGroupItem(pSalMenuItem
);
488 void QtMenu::CheckItem(unsigned nPos
, bool bChecked
)
490 if (nPos
< maItems
.size())
492 QtMenuItem
* pSalMenuItem
= GetItemAtPos(nPos
);
493 QAction
* pAction
= pSalMenuItem
->getAction();
496 pAction
->setCheckable(true);
497 pAction
->setChecked(bChecked
);
502 void QtMenu::EnableItem(unsigned nPos
, bool bEnable
)
504 if (nPos
< maItems
.size())
506 QtMenuItem
* pSalMenuItem
= GetItemAtPos(nPos
);
507 QAction
* pAction
= pSalMenuItem
->getAction();
509 pAction
->setEnabled(bEnable
);
510 pSalMenuItem
->mbEnabled
= bEnable
;
514 void QtMenu::SetItemText(unsigned, SalMenuItem
* pItem
, const OUString
& rText
)
516 QtMenuItem
* pSalMenuItem
= static_cast<QtMenuItem
*>(pItem
);
517 QAction
* pAction
= pSalMenuItem
->getAction();
520 OUString
aText(rText
);
521 NativeItemText(aText
);
522 pAction
->setText(toQString(aText
));
526 void QtMenu::SetItemImage(unsigned, SalMenuItem
* pItem
, const Image
& rImage
)
528 QtMenuItem
* pSalMenuItem
= static_cast<QtMenuItem
*>(pItem
);
530 // Save new image to use it in DoFullMenuUpdate
531 pSalMenuItem
->maImage
= rImage
;
533 QAction
* pAction
= pSalMenuItem
->getAction();
537 pAction
->setIcon(QPixmap::fromImage(toQImage(rImage
)));
540 void QtMenu::SetAccelerator(unsigned, SalMenuItem
* pItem
, const vcl::KeyCode
&,
541 const OUString
& rText
)
543 QtMenuItem
* pSalMenuItem
= static_cast<QtMenuItem
*>(pItem
);
544 QAction
* pAction
= pSalMenuItem
->getAction();
546 pAction
->setShortcut(QKeySequence(toQString(rText
), QKeySequence::PortableText
));
549 void QtMenu::GetSystemMenuData(SystemMenuData
*) {}
551 QtMenu
* QtMenu::GetTopLevel()
553 QtMenu
* pMenu
= this;
554 while (pMenu
->mpParentSalMenu
)
555 pMenu
= pMenu
->mpParentSalMenu
;
559 bool QtMenu::validateQMenuBar()
564 QtMainWindow
* pMainWindow
= mpFrame
->GetTopLevelWindow();
566 const bool bValid
= mpQMenuBar
== pMainWindow
->menuBar();
568 mpQMenuBar
= nullptr;
572 void QtMenu::ShowMenuBar(bool bVisible
)
574 if (validateQMenuBar())
575 mpQMenuBar
->setVisible(bVisible
);
578 const QtFrame
* QtMenu::GetFrame() const
580 SolarMutexGuard aGuard
;
581 const QtMenu
* pMenu
= this;
582 while (pMenu
&& !pMenu
->mpFrame
)
583 pMenu
= pMenu
->mpParentSalMenu
;
584 return pMenu
? pMenu
->mpFrame
: nullptr;
587 void QtMenu::slotMenuTriggered(QtMenuItem
* pQItem
)
592 QtMenu
* pSalMenu
= pQItem
->mpParentMenu
;
593 QtMenu
* pTopLevel
= pSalMenu
->GetTopLevel();
595 Menu
* pMenu
= pSalMenu
->GetMenu();
596 auto mnId
= pQItem
->mnId
;
598 // HACK to allow HandleMenuCommandEvent to "not-set" the checked button
599 // LO expects a signal before an item state change, so reset the check item
600 if (pQItem
->mpAction
->isCheckable()
601 && (!pQItem
->mpActionGroup
|| pQItem
->mpActionGroup
->actions().size() <= 1))
602 pQItem
->mpAction
->setChecked(!pQItem
->mpAction
->isChecked());
603 pTopLevel
->GetMenu()->HandleMenuCommandEvent(pMenu
, mnId
);
606 void QtMenu::slotMenuAboutToShow(QtMenuItem
* pQItem
)
610 QtMenu
* pSalMenu
= pQItem
->mpSubMenu
;
611 QtMenu
* pTopLevel
= pSalMenu
->GetTopLevel();
613 Menu
* pMenu
= pSalMenu
->GetMenu();
615 // following function may update the menu
616 pTopLevel
->GetMenu()->HandleMenuActivateEvent(pMenu
);
620 void QtMenu::slotMenuAboutToHide(QtMenuItem
* pQItem
)
624 QtMenu
* pSalMenu
= pQItem
->mpSubMenu
;
625 QtMenu
* pTopLevel
= pSalMenu
->GetTopLevel();
627 Menu
* pMenu
= pSalMenu
->GetMenu();
629 pTopLevel
->GetMenu()->HandleMenuDeActivateEvent(pMenu
);
633 void QtMenu::NativeItemText(OUString
& rItemText
)
635 // preserve literal '&'s in menu texts
636 rItemText
= rItemText
.replaceAll("&", "&&");
638 rItemText
= rItemText
.replace('~', '&');
641 void QtMenu::slotCloseDocument()
643 MenuBar
* pVclMenuBar
= static_cast<MenuBar
*>(mpVCLMenu
.get());
645 Application::PostUserEvent(pVclMenuBar
->GetCloseButtonClickHdl());
648 void QtMenu::ShowCloseButton(bool bShow
)
650 if (!validateQMenuBar())
653 QPushButton
* pButton
= static_cast<QPushButton
*>(mpQMenuBar
->cornerWidget(Qt::TopRightCorner
));
654 if (!pButton
&& !bShow
)
659 if (QIcon::hasThemeIcon("window-close-symbolic"))
660 aIcon
= QIcon::fromTheme("window-close-symbolic");
663 QPixmap::fromImage(toQImage(Image(StockImage::Yes
, SV_RESID_BITMAP_CLOSEDOC
))));
664 pButton
= new QPushButton(mpQMenuBar
);
665 pButton
->setIcon(aIcon
);
666 pButton
->setFlat(true);
667 pButton
->setFocusPolicy(Qt::NoFocus
);
668 pButton
->setToolTip(toQString(VclResId(SV_HELPTEXT_CLOSEDOCUMENT
)));
669 mpQMenuBar
->setCornerWidget(pButton
, Qt::TopRightCorner
);
670 connect(pButton
, &QPushButton::clicked
, this, &QtMenu::slotCloseDocument
);
679 bool QtMenu::ShowNativePopupMenu(FloatingWindow
* pWin
, const tools::Rectangle
& rRect
,
680 FloatWinPopupFlags nFlags
)
683 DoFullMenuUpdate(mpVCLMenu
);
684 mpQMenu
->setTearOffEnabled(bool(nFlags
& FloatWinPopupFlags::AllowTearOff
));
686 const VclPtr
<vcl::Window
> xParent
= pWin
->ImplGetWindowImpl()->mpRealParent
;
687 const QtFrame
* pFrame
= static_cast<QtFrame
*>(xParent
->ImplGetFrame());
689 const tools::Rectangle aFloatRect
= FloatingWindow::ImplConvertToAbsPos(xParent
, rRect
);
690 const QRect aRect
= toQRect(aFloatRect
, 1 / pFrame
->devicePixelRatioF());
691 mpQMenu
->exec(aRect
.topLeft());
696 QtMenuItem::QtMenuItem(const SalItemParams
* pItemData
)
697 : mpParentMenu(nullptr)
699 , mnId(pItemData
->nId
)
700 , mnType(pItemData
->eType
)
703 , maImage(pItemData
->aImage
)
707 QAction
* QtMenuItem::getAction() const
710 return mpMenu
->menuAction();
712 return mpAction
.get();
716 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */