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/.
10 #include <Qt5Menu.hxx>
11 #include <Qt5Menu.moc>
13 #include <Qt5Frame.hxx>
14 #include <Qt5Instance.hxx>
15 #include <Qt5MainWindow.hxx>
17 #include <QtWidgets/QMenuBar>
18 #include <QtWidgets/QPushButton>
20 #include <o3tl/safeint.hxx>
21 #include <vcl/svapp.hxx>
22 #include <sal/log.hxx>
24 #include <strings.hrc>
25 #include <bitmaps.hlst>
27 #include <vcl/floatwin.hxx>
30 Qt5Menu::Qt5Menu(bool bMenuBar
)
32 , mpParentSalMenu(nullptr)
37 , mpCloseButton(nullptr)
41 bool Qt5Menu::VisibleMenuBar() { return true; }
43 void Qt5Menu::InsertMenuItem(Qt5MenuItem
* pSalMenuItem
, unsigned nPos
)
45 sal_uInt16 nId
= pSalMenuItem
->mnId
;
46 OUString aText
= mpVCLMenu
->GetItemText(nId
);
47 NativeItemText(aText
);
48 vcl::KeyCode nAccelKey
= mpVCLMenu
->GetAccelKey(nId
);
50 pSalMenuItem
->mpAction
.reset();
51 pSalMenuItem
->mpMenu
.reset();
58 QMenu
* pQMenu
= new QMenu(toQString(aText
), nullptr);
59 pSalMenuItem
->mpMenu
.reset(pQMenu
);
61 if ((nPos
!= MENU_APPEND
)
62 && (static_cast<size_t>(nPos
) < o3tl::make_unsigned(mpQMenuBar
->actions().size())))
64 mpQMenuBar
->insertMenu(mpQMenuBar
->actions()[nPos
], pQMenu
);
68 mpQMenuBar
->addMenu(pQMenu
);
71 // correct parent menu for generated menu
72 if (pSalMenuItem
->mpSubMenu
)
74 pSalMenuItem
->mpSubMenu
->mpQMenu
= pQMenu
;
77 connect(pQMenu
, &QMenu::aboutToShow
, this,
78 [pSalMenuItem
] { slotMenuAboutToShow(pSalMenuItem
); });
79 connect(pQMenu
, &QMenu::aboutToHide
, this,
80 [pSalMenuItem
] { slotMenuAboutToHide(pSalMenuItem
); });
87 // no QMenu set, instantiate own one
88 mpOwnedQMenu
.reset(new QMenu
);
89 mpQMenu
= mpOwnedQMenu
.get();
92 if (pSalMenuItem
->mpSubMenu
)
95 QMenu
* pQMenu
= new QMenu(toQString(aText
), nullptr);
96 pSalMenuItem
->mpMenu
.reset(pQMenu
);
98 if ((nPos
!= MENU_APPEND
)
99 && (static_cast<size_t>(nPos
) < o3tl::make_unsigned(mpQMenu
->actions().size())))
101 mpQMenu
->insertMenu(mpQMenu
->actions()[nPos
], pQMenu
);
105 mpQMenu
->addMenu(pQMenu
);
108 // correct parent menu for generated menu
109 pSalMenuItem
->mpSubMenu
->mpQMenu
= pQMenu
;
111 ReinitializeActionGroup(nPos
);
113 // clear all action groups since menu is recreated
114 pSalMenuItem
->mpSubMenu
->ResetAllActionGroups();
116 connect(pQMenu
, &QMenu::aboutToShow
, this,
117 [pSalMenuItem
] { slotMenuAboutToShow(pSalMenuItem
); });
118 connect(pQMenu
, &QMenu::aboutToHide
, this,
119 [pSalMenuItem
] { slotMenuAboutToHide(pSalMenuItem
); });
123 if (pSalMenuItem
->mnType
== MenuItemType::SEPARATOR
)
125 QAction
* pAction
= new QAction(nullptr);
126 pSalMenuItem
->mpAction
.reset(pAction
);
127 pAction
->setSeparator(true);
129 if ((nPos
!= MENU_APPEND
)
130 && (static_cast<size_t>(nPos
) < o3tl::make_unsigned(mpQMenu
->actions().size())))
132 mpQMenu
->insertAction(mpQMenu
->actions()[nPos
], pAction
);
136 mpQMenu
->addAction(pAction
);
139 ReinitializeActionGroup(nPos
);
144 QAction
* pAction
= new QAction(toQString(aText
), nullptr);
145 pSalMenuItem
->mpAction
.reset(pAction
);
147 if ((nPos
!= MENU_APPEND
)
148 && (static_cast<size_t>(nPos
) < o3tl::make_unsigned(mpQMenu
->actions().size())))
150 mpQMenu
->insertAction(mpQMenu
->actions()[nPos
], pAction
);
154 mpQMenu
->addAction(pAction
);
157 ReinitializeActionGroup(nPos
);
159 UpdateActionGroupItem(pSalMenuItem
);
161 const Qt5Frame
* pFrame
= GetFrame();
163 pAction
->setShortcut(toQString(nAccelKey
.GetName(pFrame
->GetWindow())));
165 connect(pAction
, &QAction::triggered
, this,
166 [pSalMenuItem
] { slotMenuTriggered(pSalMenuItem
); });
171 QAction
* pAction
= pSalMenuItem
->getAction();
174 pAction
->setEnabled(pSalMenuItem
->mbEnabled
);
175 pAction
->setVisible(pSalMenuItem
->mbVisible
);
179 void Qt5Menu::ReinitializeActionGroup(unsigned nPos
)
181 const unsigned nCount
= GetItemCount();
188 if (nPos
== MENU_APPEND
)
192 else if (nPos
>= nCount
)
197 Qt5MenuItem
* pPrevItem
= (nPos
> 0) ? GetItemAtPos(nPos
- 1) : nullptr;
198 Qt5MenuItem
* pCurrentItem
= GetItemAtPos(nPos
);
199 Qt5MenuItem
* pNextItem
= (nPos
< nCount
- 1) ? GetItemAtPos(nPos
+ 1) : nullptr;
201 if (pCurrentItem
->mnType
== MenuItemType::SEPARATOR
)
203 pCurrentItem
->mpActionGroup
.reset();
205 // if it's inserted into middle of existing group, split it into two groups:
206 // first goes original group, after separator goes new group
207 if (pPrevItem
&& pPrevItem
->mpActionGroup
&& pNextItem
&& pNextItem
->mpActionGroup
208 && (pPrevItem
->mpActionGroup
== pNextItem
->mpActionGroup
))
210 std::shared_ptr
<QActionGroup
> pFirstActionGroup
= pPrevItem
->mpActionGroup
;
211 auto pSecondActionGroup
= std::make_shared
<QActionGroup
>(nullptr);
212 pSecondActionGroup
->setExclusive(true);
214 auto actions
= pFirstActionGroup
->actions();
216 for (unsigned idx
= nPos
+ 1; idx
< nCount
; ++idx
)
218 Qt5MenuItem
* pModifiedItem
= GetItemAtPos(idx
);
220 if ((!pModifiedItem
) || (!pModifiedItem
->mpActionGroup
))
225 pModifiedItem
->mpActionGroup
= pSecondActionGroup
;
226 auto action
= pModifiedItem
->getAction();
228 if (actions
.contains(action
))
230 pFirstActionGroup
->removeAction(action
);
231 pSecondActionGroup
->addAction(action
);
238 if (!pCurrentItem
->mpActionGroup
)
240 // unless element is inserted between two separators, or a separator and an end of vector, use neighbouring group since it's shared
241 if (pPrevItem
&& pPrevItem
->mpActionGroup
)
243 pCurrentItem
->mpActionGroup
= pPrevItem
->mpActionGroup
;
245 else if (pNextItem
&& pNextItem
->mpActionGroup
)
247 pCurrentItem
->mpActionGroup
= pNextItem
->mpActionGroup
;
251 pCurrentItem
->mpActionGroup
= std::make_shared
<QActionGroup
>(nullptr);
252 pCurrentItem
->mpActionGroup
->setExclusive(true);
256 // if there's also a different group after this element, merge it
257 if (pNextItem
&& pNextItem
->mpActionGroup
258 && (pCurrentItem
->mpActionGroup
!= pNextItem
->mpActionGroup
))
260 auto pFirstCheckedAction
= pCurrentItem
->mpActionGroup
->checkedAction();
261 auto pSecondCheckedAction
= pNextItem
->mpActionGroup
->checkedAction();
262 auto actions
= pNextItem
->mpActionGroup
->actions();
264 // first move all actions from second group to first one, and if first group already has checked action,
265 // and second group also has a checked action, uncheck action from second group
266 for (auto action
: actions
)
268 pNextItem
->mpActionGroup
->removeAction(action
);
270 if (pFirstCheckedAction
&& pSecondCheckedAction
&& (action
== pSecondCheckedAction
))
272 action
->setChecked(false);
275 pCurrentItem
->mpActionGroup
->addAction(action
);
278 // now replace all pointers to second group with pointers to first group
279 for (unsigned idx
= nPos
+ 1; idx
< nCount
; ++idx
)
281 Qt5MenuItem
* pModifiedItem
= GetItemAtPos(idx
);
283 if ((!pModifiedItem
) || (!pModifiedItem
->mpActionGroup
))
288 pModifiedItem
->mpActionGroup
= pCurrentItem
->mpActionGroup
;
294 void Qt5Menu::ResetAllActionGroups()
296 for (unsigned nItem
= 0; nItem
< GetItemCount(); ++nItem
)
298 Qt5MenuItem
* pSalMenuItem
= GetItemAtPos(nItem
);
299 pSalMenuItem
->mpActionGroup
.reset();
303 void Qt5Menu::UpdateActionGroupItem(const Qt5MenuItem
* pSalMenuItem
)
305 QAction
* pAction
= pSalMenuItem
->getAction();
309 bool bChecked
= mpVCLMenu
->IsItemChecked(pSalMenuItem
->mnId
);
310 MenuItemBits itemBits
= mpVCLMenu
->GetItemBits(pSalMenuItem
->mnId
);
312 if (itemBits
& MenuItemBits::RADIOCHECK
)
314 pAction
->setCheckable(true);
316 if (pSalMenuItem
->mpActionGroup
)
318 pSalMenuItem
->mpActionGroup
->addAction(pAction
);
321 pAction
->setChecked(bChecked
);
325 pAction
->setActionGroup(nullptr);
327 if (itemBits
& MenuItemBits::CHECKABLE
)
329 pAction
->setCheckable(true);
330 pAction
->setChecked(bChecked
);
334 pAction
->setChecked(false);
335 pAction
->setCheckable(false);
340 void Qt5Menu::InsertItem(SalMenuItem
* pSalMenuItem
, unsigned nPos
)
342 SolarMutexGuard aGuard
;
343 Qt5MenuItem
* pItem
= static_cast<Qt5MenuItem
*>(pSalMenuItem
);
345 if (nPos
== MENU_APPEND
)
346 maItems
.push_back(pItem
);
348 maItems
.insert(maItems
.begin() + nPos
, pItem
);
350 pItem
->mpParentMenu
= this;
352 InsertMenuItem(pItem
, nPos
);
355 void Qt5Menu::RemoveItem(unsigned nPos
)
357 SolarMutexGuard aGuard
;
359 if (nPos
>= maItems
.size())
362 Qt5MenuItem
* pItem
= maItems
[nPos
];
363 pItem
->mpAction
.reset();
364 pItem
->mpMenu
.reset();
366 maItems
.erase(maItems
.begin() + nPos
);
368 // Recalculate action groups if necessary:
369 // if separator between two QActionGroups was removed,
370 // it may be needed to merge them
373 ReinitializeActionGroup(nPos
- 1);
377 void Qt5Menu::SetSubMenu(SalMenuItem
* pSalMenuItem
, SalMenu
* pSubMenu
, unsigned nPos
)
379 SolarMutexGuard aGuard
;
380 Qt5MenuItem
* pItem
= static_cast<Qt5MenuItem
*>(pSalMenuItem
);
381 Qt5Menu
* pQSubMenu
= static_cast<Qt5Menu
*>(pSubMenu
);
383 pItem
->mpSubMenu
= pQSubMenu
;
384 // at this point the pointer to parent menu may be outdated, update it too
385 pItem
->mpParentMenu
= this;
387 if (pQSubMenu
!= nullptr)
389 pQSubMenu
->mpParentSalMenu
= this;
390 pQSubMenu
->mpQMenu
= pItem
->mpMenu
.get();
393 // if it's not a menu bar item, then convert it to corresponding item if type if necessary.
394 // If submenu is present and it's an action, convert it to menu.
395 // If submenu is not present and it's a menu, convert it to action.
396 // It may be fine to proceed in any case, but by skipping other cases
397 // amount of unneeded actions taken should be reduced.
398 if (pItem
->mpParentMenu
->mbMenuBar
|| (pQSubMenu
&& pItem
->mpMenu
)
399 || ((!pQSubMenu
) && pItem
->mpAction
))
404 InsertMenuItem(pItem
, nPos
);
407 void Qt5Menu::SetFrame(const SalFrame
* pFrame
)
409 auto* pSalInst(static_cast<Qt5Instance
*>(GetSalData()->m_pInstance
));
411 if (!pSalInst
->IsMainThread())
413 pSalInst
->RunInMainThread([this, pFrame
]() { SetFrame(pFrame
); });
417 SolarMutexGuard aGuard
;
419 mpFrame
= const_cast<Qt5Frame
*>(static_cast<const Qt5Frame
*>(pFrame
));
421 mpFrame
->SetMenu(this);
423 Qt5MainWindow
* pMainWindow
= mpFrame
->GetTopLevelWindow();
427 mpQMenuBar
= pMainWindow
->menuBar();
432 = static_cast<QPushButton
*>(mpQMenuBar
->cornerWidget(Qt::TopRightCorner
));
433 if (pButton
&& ((mpCloseButton
!= pButton
) || !maCloseButtonConnection
))
435 maCloseButtonConnection
436 = connect(pButton
, &QPushButton::clicked
, this, &Qt5Menu::slotCloseDocument
);
437 mpCloseButton
= pButton
;
443 DoFullMenuUpdate(mpVCLMenu
);
446 void Qt5Menu::DoFullMenuUpdate(Menu
* pMenuBar
)
448 // clear action groups since menu is rebuilt
449 ResetAllActionGroups();
450 ShowCloseButton(false);
452 for (sal_Int32 nItem
= 0; nItem
< static_cast<sal_Int32
>(GetItemCount()); nItem
++)
454 Qt5MenuItem
* pSalMenuItem
= GetItemAtPos(nItem
);
455 InsertMenuItem(pSalMenuItem
, nItem
);
456 SetItemImage(nItem
, pSalMenuItem
, pSalMenuItem
->maImage
);
457 const bool bShowDisabled
458 = bool(pMenuBar
->GetMenuFlags() & MenuFlags::AlwaysShowDisabledEntries
)
459 || !bool(pMenuBar
->GetMenuFlags() & MenuFlags::HideDisabledEntries
);
460 const bool bVisible
= bShowDisabled
|| mpVCLMenu
->IsItemEnabled(pSalMenuItem
->mnId
);
461 pSalMenuItem
->getAction()->setVisible(bVisible
);
463 if (pSalMenuItem
->mpSubMenu
!= nullptr)
465 pMenuBar
->HandleMenuActivateEvent(pSalMenuItem
->mpSubMenu
->GetMenu());
466 pSalMenuItem
->mpSubMenu
->DoFullMenuUpdate(pMenuBar
);
467 pMenuBar
->HandleMenuDeActivateEvent(pSalMenuItem
->mpSubMenu
->GetMenu());
472 void Qt5Menu::ShowItem(unsigned nPos
, bool bShow
)
474 if (nPos
< maItems
.size())
476 Qt5MenuItem
* pSalMenuItem
= GetItemAtPos(nPos
);
477 QAction
* pAction
= pSalMenuItem
->getAction();
479 pAction
->setVisible(bShow
);
480 pSalMenuItem
->mbVisible
= bShow
;
484 void Qt5Menu::SetItemBits(unsigned nPos
, MenuItemBits
)
486 if (nPos
< maItems
.size())
488 Qt5MenuItem
* pSalMenuItem
= GetItemAtPos(nPos
);
489 UpdateActionGroupItem(pSalMenuItem
);
493 void Qt5Menu::CheckItem(unsigned nPos
, bool bChecked
)
495 if (nPos
< maItems
.size())
497 Qt5MenuItem
* pSalMenuItem
= GetItemAtPos(nPos
);
498 QAction
* pAction
= pSalMenuItem
->getAction();
501 pAction
->setCheckable(true);
502 pAction
->setChecked(bChecked
);
507 void Qt5Menu::EnableItem(unsigned nPos
, bool bEnable
)
509 if (nPos
< maItems
.size())
511 Qt5MenuItem
* pSalMenuItem
= GetItemAtPos(nPos
);
512 QAction
* pAction
= pSalMenuItem
->getAction();
514 pAction
->setEnabled(bEnable
);
515 pSalMenuItem
->mbEnabled
= bEnable
;
519 void Qt5Menu::SetItemText(unsigned, SalMenuItem
* pItem
, const OUString
& rText
)
521 Qt5MenuItem
* pSalMenuItem
= static_cast<Qt5MenuItem
*>(pItem
);
522 QAction
* pAction
= pSalMenuItem
->getAction();
525 OUString
aText(rText
);
526 NativeItemText(aText
);
527 pAction
->setText(toQString(aText
));
531 void Qt5Menu::SetItemImage(unsigned, SalMenuItem
* pItem
, const Image
& rImage
)
533 Qt5MenuItem
* pSalMenuItem
= static_cast<Qt5MenuItem
*>(pItem
);
535 // Save new image to use it in DoFullMenuUpdate
536 pSalMenuItem
->maImage
= rImage
;
538 QAction
* pAction
= pSalMenuItem
->getAction();
542 pAction
->setIcon(QPixmap::fromImage(toQImage(rImage
)));
545 void Qt5Menu::SetAccelerator(unsigned, SalMenuItem
* pItem
, const vcl::KeyCode
&,
546 const OUString
& rText
)
548 Qt5MenuItem
* pSalMenuItem
= static_cast<Qt5MenuItem
*>(pItem
);
549 QAction
* pAction
= pSalMenuItem
->getAction();
551 pAction
->setShortcut(QKeySequence(toQString(rText
), QKeySequence::PortableText
));
554 void Qt5Menu::GetSystemMenuData(SystemMenuData
*) {}
556 Qt5Menu
* Qt5Menu::GetTopLevel()
558 Qt5Menu
* pMenu
= this;
559 while (pMenu
->mpParentSalMenu
)
560 pMenu
= pMenu
->mpParentSalMenu
;
564 void Qt5Menu::ShowMenuBar(bool bVisible
)
567 mpQMenuBar
->setVisible(bVisible
);
570 const Qt5Frame
* Qt5Menu::GetFrame() const
572 SolarMutexGuard aGuard
;
573 const Qt5Menu
* pMenu
= this;
574 while (pMenu
&& !pMenu
->mpFrame
)
575 pMenu
= pMenu
->mpParentSalMenu
;
576 return pMenu
? pMenu
->mpFrame
: nullptr;
579 void Qt5Menu::slotMenuTriggered(Qt5MenuItem
* pQItem
)
584 Qt5Menu
* pSalMenu
= pQItem
->mpParentMenu
;
585 Qt5Menu
* pTopLevel
= pSalMenu
->GetTopLevel();
587 Menu
* pMenu
= pSalMenu
->GetMenu();
588 auto mnId
= pQItem
->mnId
;
590 // HACK to allow HandleMenuCommandEvent to "not-set" the checked button
591 // LO expects a signal before an item state change, so reset the check item
592 if (pQItem
->mpAction
->isCheckable()
593 && (!pQItem
->mpActionGroup
|| pQItem
->mpActionGroup
->actions().size() <= 1))
594 pQItem
->mpAction
->setChecked(!pQItem
->mpAction
->isChecked());
595 pTopLevel
->GetMenu()->HandleMenuCommandEvent(pMenu
, mnId
);
598 void Qt5Menu::slotMenuAboutToShow(Qt5MenuItem
* pQItem
)
602 Qt5Menu
* pSalMenu
= pQItem
->mpSubMenu
;
603 Qt5Menu
* pTopLevel
= pSalMenu
->GetTopLevel();
605 Menu
* pMenu
= pSalMenu
->GetMenu();
607 // following function may update the menu
608 pTopLevel
->GetMenu()->HandleMenuActivateEvent(pMenu
);
612 void Qt5Menu::slotMenuAboutToHide(Qt5MenuItem
* pQItem
)
616 Qt5Menu
* pSalMenu
= pQItem
->mpSubMenu
;
617 Qt5Menu
* pTopLevel
= pSalMenu
->GetTopLevel();
619 Menu
* pMenu
= pSalMenu
->GetMenu();
621 pTopLevel
->GetMenu()->HandleMenuDeActivateEvent(pMenu
);
625 void Qt5Menu::NativeItemText(OUString
& rItemText
)
627 // preserve literal '&'s in menu texts
628 rItemText
= rItemText
.replaceAll("&", "&&");
630 rItemText
= rItemText
.replace('~', '&');
633 void Qt5Menu::slotCloseDocument()
635 MenuBar
* pVclMenuBar
= static_cast<MenuBar
*>(mpVCLMenu
.get());
637 Application::PostUserEvent(pVclMenuBar
->GetCloseButtonClickHdl());
640 void Qt5Menu::ShowCloseButton(bool bShow
)
645 QPushButton
* pButton
= static_cast<QPushButton
*>(mpQMenuBar
->cornerWidget(Qt::TopRightCorner
));
649 if (QIcon::hasThemeIcon("window-close-symbolic"))
650 aIcon
= QIcon::fromTheme("window-close-symbolic");
653 QPixmap::fromImage((toQImage(Image(StockImage::Yes
, SV_RESID_BITMAP_CLOSEDOC
)))));
654 pButton
= new QPushButton(mpQMenuBar
);
655 pButton
->setIcon(aIcon
);
656 pButton
->setFlat(true);
657 pButton
->setFocusPolicy(Qt::NoFocus
);
658 pButton
->setToolTip(toQString(VclResId(SV_HELPTEXT_CLOSEDOCUMENT
)));
659 mpQMenuBar
->setCornerWidget(pButton
, Qt::TopRightCorner
);
660 maCloseButtonConnection
661 = connect(pButton
, &QPushButton::clicked
, this, &Qt5Menu::slotCloseDocument
);
662 mpCloseButton
= pButton
;
671 bool Qt5Menu::ShowNativePopupMenu(FloatingWindow
*, const tools::Rectangle
&,
672 FloatWinPopupFlags nFlags
)
675 DoFullMenuUpdate(mpVCLMenu
);
676 mpQMenu
->setTearOffEnabled(bool(nFlags
& FloatWinPopupFlags::AllowTearOff
));
678 const QPoint aPos
= QCursor::pos();
684 Qt5MenuItem::Qt5MenuItem(const SalItemParams
* pItemData
)
685 : mpParentMenu(nullptr)
687 , mnId(pItemData
->nId
)
688 , mnType(pItemData
->eType
)
691 , maImage(pItemData
->aImage
)
695 QAction
* Qt5MenuItem::getAction() const
698 return mpMenu
->menuAction();
700 return mpAction
.get();
704 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */