OInterfaceContainerHelper3 needs to be thread-safe
[LibreOffice.git] / vcl / qt5 / Qt5Menu.cxx
blobf7c2e480118c2c60622d76fce16d75167cc39d5c
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
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/.
8 */
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>
28 #include <window.h>
30 Qt5Menu::Qt5Menu(bool bMenuBar)
31 : mpVCLMenu(nullptr)
32 , mpParentSalMenu(nullptr)
33 , mpFrame(nullptr)
34 , mbMenuBar(bMenuBar)
35 , mpQMenuBar(nullptr)
36 , mpQMenu(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();
53 if (mbMenuBar)
55 // top-level menu
56 if (mpQMenuBar)
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);
66 else
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); });
83 else
85 if (!mpQMenu)
87 // no QMenu set, instantiate own one
88 mpOwnedQMenu.reset(new QMenu);
89 mpQMenu = mpOwnedQMenu.get();
92 if (pSalMenuItem->mpSubMenu)
94 // submenu
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);
103 else
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); });
121 else
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);
134 else
136 mpQMenu->addAction(pAction);
139 ReinitializeActionGroup(nPos);
141 else
143 // leaf menu
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);
152 else
154 mpQMenu->addAction(pAction);
157 ReinitializeActionGroup(nPos);
159 UpdateActionGroupItem(pSalMenuItem);
161 const Qt5Frame* pFrame = GetFrame();
162 if (pFrame)
163 pAction->setShortcut(toQString(nAccelKey.GetName(pFrame->GetWindow())));
165 connect(pAction, &QAction::triggered, this,
166 [pSalMenuItem] { slotMenuTriggered(pSalMenuItem); });
171 QAction* pAction = pSalMenuItem->getAction();
172 if (pAction)
174 pAction->setEnabled(pSalMenuItem->mbEnabled);
175 pAction->setVisible(pSalMenuItem->mbVisible);
179 void Qt5Menu::ReinitializeActionGroup(unsigned nPos)
181 const unsigned nCount = GetItemCount();
183 if (nCount == 0)
185 return;
188 if (nPos == MENU_APPEND)
190 nPos = nCount - 1;
192 else if (nPos >= nCount)
194 return;
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))
222 break;
225 pModifiedItem->mpActionGroup = pSecondActionGroup;
226 auto action = pModifiedItem->getAction();
228 if (actions.contains(action))
230 pFirstActionGroup->removeAction(action);
231 pSecondActionGroup->addAction(action);
236 else
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;
249 else
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))
285 break;
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();
306 if (!pAction)
307 return;
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);
323 else
325 pAction->setActionGroup(nullptr);
327 if (itemBits & MenuItemBits::CHECKABLE)
329 pAction->setCheckable(true);
330 pAction->setChecked(bChecked);
332 else
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);
347 else
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())
360 return;
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
371 if (nPos > 0)
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))
401 return;
404 InsertMenuItem(pItem, nPos);
407 void Qt5Menu::SetFrame(const SalFrame* pFrame)
409 auto* pSalInst(static_cast<Qt5Instance*>(GetSalData()->m_pInstance));
410 assert(pSalInst);
411 if (!pSalInst->IsMainThread())
413 pSalInst->RunInMainThread([this, pFrame]() { SetFrame(pFrame); });
414 return;
417 SolarMutexGuard aGuard;
418 assert(mbMenuBar);
419 mpFrame = const_cast<Qt5Frame*>(static_cast<const Qt5Frame*>(pFrame));
421 mpFrame->SetMenu(this);
423 Qt5MainWindow* pMainWindow = mpFrame->GetTopLevelWindow();
424 if (!pMainWindow)
425 return;
427 mpQMenuBar = pMainWindow->menuBar();
428 if (mpQMenuBar)
430 mpQMenuBar->clear();
431 QPushButton* pButton
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;
441 mpQMenu = nullptr;
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();
478 if (pAction)
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();
499 if (pAction)
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();
513 if (pAction)
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();
523 if (pAction)
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();
539 if (!pAction)
540 return;
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();
550 if (pAction)
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;
561 return pMenu;
564 void Qt5Menu::ShowMenuBar(bool bVisible)
566 if (mpQMenuBar)
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)
581 if (!pQItem)
582 return;
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)
600 if (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)
614 if (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());
636 if (pVclMenuBar)
637 Application::PostUserEvent(pVclMenuBar->GetCloseButtonClickHdl());
640 void Qt5Menu::ShowCloseButton(bool bShow)
642 if (!mpQMenuBar)
643 return;
645 QPushButton* pButton = static_cast<QPushButton*>(mpQMenuBar->cornerWidget(Qt::TopRightCorner));
646 if (!pButton)
648 QIcon aIcon;
649 if (QIcon::hasThemeIcon("window-close-symbolic"))
650 aIcon = QIcon::fromTheme("window-close-symbolic");
651 else
652 aIcon = QIcon(
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;
665 if (bShow)
666 pButton->show();
667 else
668 pButton->hide();
671 bool Qt5Menu::ShowNativePopupMenu(FloatingWindow*, const tools::Rectangle&,
672 FloatWinPopupFlags nFlags)
674 assert(mpQMenu);
675 DoFullMenuUpdate(mpVCLMenu);
676 mpQMenu->setTearOffEnabled(bool(nFlags & FloatWinPopupFlags::AllowTearOff));
678 const QPoint aPos = QCursor::pos();
679 mpQMenu->exec(aPos);
681 return true;
684 Qt5MenuItem::Qt5MenuItem(const SalItemParams* pItemData)
685 : mpParentMenu(nullptr)
686 , mpSubMenu(nullptr)
687 , mnId(pItemData->nId)
688 , mnType(pItemData->eType)
689 , mbVisible(true)
690 , mbEnabled(true)
691 , maImage(pItemData->aImage)
695 QAction* Qt5MenuItem::getAction() const
697 if (mpMenu)
698 return mpMenu->menuAction();
699 if (mpAction)
700 return mpAction.get();
701 return nullptr;
704 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */