bump product version to 6.4.0.3
[LibreOffice.git] / vcl / qt5 / Qt5Menu.cxx
blobdd5c9895273e79ec91673da91913312126fa8e2f
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 <vcl/svapp.hxx>
21 #include <sal/log.hxx>
23 #include <strings.hrc>
24 #include <bitmaps.hlst>
26 Qt5Menu::Qt5Menu(bool bMenuBar)
27 : mpVCLMenu(nullptr)
28 , mpParentSalMenu(nullptr)
29 , mpFrame(nullptr)
30 , mbMenuBar(bMenuBar)
31 , mpQMenuBar(nullptr)
32 , mpQMenu(nullptr)
33 , mpCloseButton(nullptr)
37 bool Qt5Menu::VisibleMenuBar() { return true; }
39 void Qt5Menu::InsertMenuItem(Qt5MenuItem* pSalMenuItem, unsigned nPos)
41 sal_uInt16 nId = pSalMenuItem->mnId;
42 OUString aText = mpVCLMenu->GetItemText(nId);
43 NativeItemText(aText);
44 vcl::KeyCode nAccelKey = mpVCLMenu->GetAccelKey(nId);
46 pSalMenuItem->mpAction.reset();
47 pSalMenuItem->mpMenu.reset();
49 if (mbMenuBar)
51 // top-level menu
52 if (mpQMenuBar)
54 QMenu* pQMenu = new QMenu(toQString(aText), nullptr);
55 pSalMenuItem->mpMenu.reset(pQMenu);
57 if ((nPos != MENU_APPEND)
58 && (static_cast<size_t>(nPos) < static_cast<size_t>(mpQMenuBar->actions().size())))
60 mpQMenuBar->insertMenu(mpQMenuBar->actions()[nPos], pQMenu);
62 else
64 mpQMenuBar->addMenu(pQMenu);
67 // correct parent menu for generated menu
68 if (pSalMenuItem->mpSubMenu)
70 pSalMenuItem->mpSubMenu->mpQMenu = pQMenu;
73 connect(pQMenu, &QMenu::aboutToShow, this,
74 [pSalMenuItem] { slotMenuAboutToShow(pSalMenuItem); });
75 connect(pQMenu, &QMenu::aboutToHide, this,
76 [pSalMenuItem] { slotMenuAboutToHide(pSalMenuItem); });
79 else if (mpQMenu)
81 if (pSalMenuItem->mpSubMenu)
83 // submenu
84 QMenu* pQMenu = new QMenu(toQString(aText), nullptr);
85 pSalMenuItem->mpMenu.reset(pQMenu);
87 if ((nPos != MENU_APPEND)
88 && (static_cast<size_t>(nPos) < static_cast<size_t>(mpQMenu->actions().size())))
90 mpQMenu->insertMenu(mpQMenu->actions()[nPos], pQMenu);
92 else
94 mpQMenu->addMenu(pQMenu);
97 // correct parent menu for generated menu
98 pSalMenuItem->mpSubMenu->mpQMenu = pQMenu;
100 ReinitializeActionGroup(nPos);
102 // clear all action groups since menu is recreated
103 pSalMenuItem->mpSubMenu->ResetAllActionGroups();
105 connect(pQMenu, &QMenu::aboutToShow, this,
106 [pSalMenuItem] { slotMenuAboutToShow(pSalMenuItem); });
107 connect(pQMenu, &QMenu::aboutToHide, this,
108 [pSalMenuItem] { slotMenuAboutToHide(pSalMenuItem); });
110 else
112 if (pSalMenuItem->mnType == MenuItemType::SEPARATOR)
114 QAction* pAction = new QAction(nullptr);
115 pSalMenuItem->mpAction.reset(pAction);
116 pAction->setSeparator(true);
118 if ((nPos != MENU_APPEND)
119 && (static_cast<size_t>(nPos) < static_cast<size_t>(mpQMenu->actions().size())))
121 mpQMenu->insertAction(mpQMenu->actions()[nPos], pAction);
123 else
125 mpQMenu->addAction(pAction);
128 ReinitializeActionGroup(nPos);
130 else
132 // leaf menu
133 QAction* pAction = new QAction(toQString(aText), nullptr);
134 pSalMenuItem->mpAction.reset(pAction);
136 if ((nPos != MENU_APPEND)
137 && (static_cast<size_t>(nPos) < static_cast<size_t>(mpQMenu->actions().size())))
139 mpQMenu->insertAction(mpQMenu->actions()[nPos], pAction);
141 else
143 mpQMenu->addAction(pAction);
146 ReinitializeActionGroup(nPos);
148 UpdateActionGroupItem(pSalMenuItem);
150 pAction->setShortcut(toQString(nAccelKey.GetName(GetFrame()->GetWindow())));
152 connect(pAction, &QAction::triggered, this,
153 [pSalMenuItem] { slotMenuTriggered(pSalMenuItem); });
158 QAction* pAction = pSalMenuItem->getAction();
159 if (pAction)
161 pAction->setEnabled(pSalMenuItem->mbEnabled);
162 pAction->setVisible(pSalMenuItem->mbVisible);
166 void Qt5Menu::ReinitializeActionGroup(unsigned nPos)
168 const unsigned nCount = GetItemCount();
170 if (nCount == 0)
172 return;
175 if (nPos == MENU_APPEND)
177 nPos = nCount - 1;
179 else if (nPos >= nCount)
181 return;
184 Qt5MenuItem* pPrevItem = (nPos > 0) ? GetItemAtPos(nPos - 1) : nullptr;
185 Qt5MenuItem* pCurrentItem = GetItemAtPos(nPos);
186 Qt5MenuItem* pNextItem = (nPos < nCount - 1) ? GetItemAtPos(nPos + 1) : nullptr;
188 if (pCurrentItem->mnType == MenuItemType::SEPARATOR)
190 pCurrentItem->mpActionGroup.reset();
192 // if it's inserted into middle of existing group, split it into two groups:
193 // first goes original group, after separator goes new group
194 if (pPrevItem && pPrevItem->mpActionGroup && pNextItem && pNextItem->mpActionGroup
195 && (pPrevItem->mpActionGroup == pNextItem->mpActionGroup))
197 std::shared_ptr<QActionGroup> pFirstActionGroup = pPrevItem->mpActionGroup;
198 std::shared_ptr<QActionGroup> pSecondActionGroup(new QActionGroup(nullptr));
199 pSecondActionGroup->setExclusive(true);
201 auto actions = pFirstActionGroup->actions();
203 for (unsigned idx = nPos + 1; idx < nCount; ++idx)
205 Qt5MenuItem* pModifiedItem = GetItemAtPos(idx);
207 if ((!pModifiedItem) || (!pModifiedItem->mpActionGroup))
209 break;
212 pModifiedItem->mpActionGroup = pSecondActionGroup;
213 auto action = pModifiedItem->getAction();
215 if (actions.contains(action))
217 pFirstActionGroup->removeAction(action);
218 pSecondActionGroup->addAction(action);
223 else
225 if (!pCurrentItem->mpActionGroup)
227 // unless element is inserted between two separators, or a separator and an end of vector, use neighbouring group since it's shared
228 if (pPrevItem && pPrevItem->mpActionGroup)
230 pCurrentItem->mpActionGroup = pPrevItem->mpActionGroup;
232 else if (pNextItem && pNextItem->mpActionGroup)
234 pCurrentItem->mpActionGroup = pNextItem->mpActionGroup;
236 else
238 pCurrentItem->mpActionGroup.reset(new QActionGroup(nullptr));
239 pCurrentItem->mpActionGroup->setExclusive(true);
243 // if there's also a different group after this element, merge it
244 if (pNextItem && pNextItem->mpActionGroup
245 && (pCurrentItem->mpActionGroup != pNextItem->mpActionGroup))
247 auto pFirstCheckedAction = pCurrentItem->mpActionGroup->checkedAction();
248 auto pSecondCheckedAction = pNextItem->mpActionGroup->checkedAction();
249 auto actions = pNextItem->mpActionGroup->actions();
251 // first move all actions from second group to first one, and if first group already has checked action,
252 // and second group also has a checked action, uncheck action from second group
253 for (auto action : actions)
255 pNextItem->mpActionGroup->removeAction(action);
257 if (pFirstCheckedAction && pSecondCheckedAction && (action == pSecondCheckedAction))
259 action->setChecked(false);
262 pCurrentItem->mpActionGroup->addAction(action);
265 // now replace all pointers to second group with pointers to first group
266 for (unsigned idx = nPos + 1; idx < nCount; ++idx)
268 Qt5MenuItem* pModifiedItem = GetItemAtPos(idx);
270 if ((!pModifiedItem) || (!pModifiedItem->mpActionGroup))
272 break;
275 pModifiedItem->mpActionGroup = pCurrentItem->mpActionGroup;
281 void Qt5Menu::ResetAllActionGroups()
283 for (unsigned nItem = 0; nItem < GetItemCount(); ++nItem)
285 Qt5MenuItem* pSalMenuItem = GetItemAtPos(nItem);
286 pSalMenuItem->mpActionGroup.reset();
290 void Qt5Menu::UpdateActionGroupItem(const Qt5MenuItem* pSalMenuItem)
292 QAction* pAction = pSalMenuItem->getAction();
293 if (!pAction)
294 return;
296 bool bChecked = mpVCLMenu->IsItemChecked(pSalMenuItem->mnId);
297 MenuItemBits itemBits = mpVCLMenu->GetItemBits(pSalMenuItem->mnId);
299 if (itemBits & MenuItemBits::RADIOCHECK)
301 pAction->setCheckable(true);
303 if (pSalMenuItem->mpActionGroup)
305 pSalMenuItem->mpActionGroup->addAction(pAction);
308 pAction->setChecked(bChecked);
310 else
312 pAction->setActionGroup(nullptr);
314 if (itemBits & MenuItemBits::CHECKABLE)
316 pAction->setCheckable(true);
317 pAction->setChecked(bChecked);
319 else
321 pAction->setChecked(false);
322 pAction->setCheckable(false);
327 void Qt5Menu::InsertItem(SalMenuItem* pSalMenuItem, unsigned nPos)
329 SolarMutexGuard aGuard;
330 Qt5MenuItem* pItem = static_cast<Qt5MenuItem*>(pSalMenuItem);
332 if (nPos == MENU_APPEND)
333 maItems.push_back(pItem);
334 else
335 maItems.insert(maItems.begin() + nPos, pItem);
337 pItem->mpParentMenu = this;
339 InsertMenuItem(pItem, nPos);
342 void Qt5Menu::RemoveItem(unsigned nPos)
344 SolarMutexGuard aGuard;
346 if (nPos < maItems.size())
348 Qt5MenuItem* pItem = maItems[nPos];
349 pItem->mpAction.reset();
350 pItem->mpMenu.reset();
352 maItems.erase(maItems.begin() + nPos);
354 // Recalculate action groups if necessary:
355 // if separator between two QActionGroups was removed,
356 // it may be needed to merge them
357 if (nPos > 0)
359 ReinitializeActionGroup(nPos - 1);
364 void Qt5Menu::SetSubMenu(SalMenuItem* pSalMenuItem, SalMenu* pSubMenu, unsigned nPos)
366 SolarMutexGuard aGuard;
367 Qt5MenuItem* pItem = static_cast<Qt5MenuItem*>(pSalMenuItem);
368 Qt5Menu* pQSubMenu = static_cast<Qt5Menu*>(pSubMenu);
370 pItem->mpSubMenu = pQSubMenu;
371 // at this point the pointer to parent menu may be outdated, update it too
372 pItem->mpParentMenu = this;
374 if (pQSubMenu != nullptr)
376 pQSubMenu->mpParentSalMenu = this;
377 pQSubMenu->mpQMenu = pItem->mpMenu.get();
380 // if it's not a menu bar item, then convert it to corresponding item if type if necessary.
381 // If submenu is present and it's an action, convert it to menu.
382 // If submenu is not present and it's a menu, convert it to action.
383 // It may be fine to proceed in any case, but by skipping other cases
384 // amount of unneeded actions taken should be reduced.
385 if (pItem->mpParentMenu->mbMenuBar || (pQSubMenu && pItem->mpMenu)
386 || ((!pQSubMenu) && pItem->mpAction))
388 return;
391 InsertMenuItem(pItem, nPos);
394 void Qt5Menu::SetFrame(const SalFrame* pFrame)
396 auto* pSalInst(static_cast<Qt5Instance*>(GetSalData()->m_pInstance));
397 assert(pSalInst);
398 if (!pSalInst->IsMainThread())
400 pSalInst->RunInMainThread([this, pFrame]() { SetFrame(pFrame); });
401 return;
404 SolarMutexGuard aGuard;
405 assert(mbMenuBar);
406 mpFrame = const_cast<Qt5Frame*>(static_cast<const Qt5Frame*>(pFrame));
408 mpFrame->SetMenu(this);
410 Qt5MainWindow* pMainWindow = mpFrame->GetTopLevelWindow();
411 if (pMainWindow)
413 mpQMenuBar = pMainWindow->menuBar();
414 if (mpQMenuBar)
416 mpQMenuBar->clear();
417 QPushButton* pButton
418 = static_cast<QPushButton*>(mpQMenuBar->cornerWidget(Qt::TopRightCorner));
419 if (pButton && ((mpCloseButton != pButton) || !maCloseButtonConnection))
421 maCloseButtonConnection
422 = connect(pButton, &QPushButton::clicked, this, &Qt5Menu::slotCloseDocument);
423 mpCloseButton = pButton;
427 mpQMenu = nullptr;
429 DoFullMenuUpdate(mpVCLMenu);
433 void Qt5Menu::DoFullMenuUpdate(Menu* pMenuBar)
435 // clear action groups since menu is rebuilt
436 ResetAllActionGroups();
437 ShowCloseButton(false);
439 for (sal_Int32 nItem = 0; nItem < static_cast<sal_Int32>(GetItemCount()); nItem++)
441 Qt5MenuItem* pSalMenuItem = GetItemAtPos(nItem);
442 InsertMenuItem(pSalMenuItem, nItem);
443 SetItemImage(nItem, pSalMenuItem, pSalMenuItem->maImage);
445 if (pSalMenuItem->mpSubMenu != nullptr)
447 pMenuBar->HandleMenuActivateEvent(pSalMenuItem->mpSubMenu->GetMenu());
448 pSalMenuItem->mpSubMenu->DoFullMenuUpdate(pMenuBar);
449 pMenuBar->HandleMenuDeActivateEvent(pSalMenuItem->mpSubMenu->GetMenu());
454 void Qt5Menu::ShowItem(unsigned nPos, bool bShow)
456 if (nPos < maItems.size())
458 Qt5MenuItem* pSalMenuItem = GetItemAtPos(nPos);
459 QAction* pAction = pSalMenuItem->getAction();
460 if (pAction)
461 pAction->setVisible(bShow);
462 pSalMenuItem->mbVisible = bShow;
466 void Qt5Menu::SetItemBits(unsigned nPos, MenuItemBits)
468 if (nPos < maItems.size())
470 Qt5MenuItem* pSalMenuItem = GetItemAtPos(nPos);
471 UpdateActionGroupItem(pSalMenuItem);
475 void Qt5Menu::CheckItem(unsigned nPos, bool bChecked)
477 if (nPos < maItems.size())
479 Qt5MenuItem* pSalMenuItem = GetItemAtPos(nPos);
480 QAction* pAction = pSalMenuItem->getAction();
481 if (pAction)
483 pAction->setCheckable(true);
484 pAction->setChecked(bChecked);
489 void Qt5Menu::EnableItem(unsigned nPos, bool bEnable)
491 if (nPos < maItems.size())
493 Qt5MenuItem* pSalMenuItem = GetItemAtPos(nPos);
494 QAction* pAction = pSalMenuItem->getAction();
495 if (pAction)
496 pAction->setEnabled(bEnable);
497 pSalMenuItem->mbEnabled = bEnable;
501 void Qt5Menu::SetItemText(unsigned, SalMenuItem* pItem, const OUString& rText)
503 Qt5MenuItem* pSalMenuItem = static_cast<Qt5MenuItem*>(pItem);
504 QAction* pAction = pSalMenuItem->getAction();
505 if (pAction)
507 OUString aText(rText);
508 NativeItemText(aText);
509 pAction->setText(toQString(aText));
513 void Qt5Menu::SetItemImage(unsigned, SalMenuItem* pItem, const Image& rImage)
515 Qt5MenuItem* pSalMenuItem = static_cast<Qt5MenuItem*>(pItem);
517 // Save new image to use it in DoFullMenuUpdate
518 pSalMenuItem->maImage = rImage;
520 QAction* pAction = pSalMenuItem->getAction();
521 if (!pAction)
522 return;
524 pAction->setIcon(QPixmap::fromImage(toQImage(rImage)));
527 void Qt5Menu::SetAccelerator(unsigned, SalMenuItem* pItem, const vcl::KeyCode&,
528 const OUString& rText)
530 Qt5MenuItem* pSalMenuItem = static_cast<Qt5MenuItem*>(pItem);
531 QAction* pAction = pSalMenuItem->getAction();
532 if (pAction)
533 pAction->setShortcut(QKeySequence(toQString(rText), QKeySequence::PortableText));
536 void Qt5Menu::GetSystemMenuData(SystemMenuData*) {}
538 Qt5Menu* Qt5Menu::GetTopLevel()
540 Qt5Menu* pMenu = this;
541 while (pMenu->mpParentSalMenu)
542 pMenu = pMenu->mpParentSalMenu;
543 return pMenu;
546 void Qt5Menu::ShowMenuBar(bool bVisible)
548 if (mpQMenuBar)
549 mpQMenuBar->setVisible(bVisible);
552 const Qt5Frame* Qt5Menu::GetFrame() const
554 SolarMutexGuard aGuard;
555 const Qt5Menu* pMenu = this;
556 while (pMenu && !pMenu->mpFrame)
557 pMenu = pMenu->mpParentSalMenu;
558 return pMenu ? pMenu->mpFrame : nullptr;
561 void Qt5Menu::slotMenuTriggered(Qt5MenuItem* pQItem)
563 if (pQItem)
565 Qt5Menu* pSalMenu = pQItem->mpParentMenu;
566 Qt5Menu* pTopLevel = pSalMenu->GetTopLevel();
568 Menu* pMenu = pSalMenu->GetMenu();
569 auto mnId = pQItem->mnId;
571 // HACK to allow HandleMenuCommandEvent to "not-set" the checked button
572 // LO expects a signal before an item state change, so reset the check item
573 if (pQItem->mpAction->isCheckable()
574 && (!pQItem->mpActionGroup || pQItem->mpActionGroup->actions().size() <= 1))
575 pQItem->mpAction->setChecked(!pQItem->mpAction->isChecked());
576 pTopLevel->GetMenu()->HandleMenuCommandEvent(pMenu, mnId);
580 void Qt5Menu::slotMenuAboutToShow(Qt5MenuItem* pQItem)
582 if (pQItem)
584 Qt5Menu* pSalMenu = pQItem->mpSubMenu;
585 Qt5Menu* pTopLevel = pSalMenu->GetTopLevel();
587 Menu* pMenu = pSalMenu->GetMenu();
589 // following function may update the menu
590 pTopLevel->GetMenu()->HandleMenuActivateEvent(pMenu);
594 void Qt5Menu::slotMenuAboutToHide(Qt5MenuItem* pQItem)
596 if (pQItem)
598 Qt5Menu* pSalMenu = pQItem->mpSubMenu;
599 Qt5Menu* pTopLevel = pSalMenu->GetTopLevel();
601 Menu* pMenu = pSalMenu->GetMenu();
603 pTopLevel->GetMenu()->HandleMenuDeActivateEvent(pMenu);
607 void Qt5Menu::NativeItemText(OUString& rItemText)
609 // preserve literal '&'s in menu texts
610 rItemText = rItemText.replaceAll("&", "&&");
612 rItemText = rItemText.replace('~', '&');
615 void Qt5Menu::slotCloseDocument()
617 MenuBar* pVclMenuBar = static_cast<MenuBar*>(mpVCLMenu.get());
618 if (pVclMenuBar)
619 Application::PostUserEvent(pVclMenuBar->GetCloseButtonClickHdl());
622 void Qt5Menu::ShowCloseButton(bool bShow)
624 if (!mpQMenuBar)
625 return;
627 QPushButton* pButton = static_cast<QPushButton*>(mpQMenuBar->cornerWidget(Qt::TopRightCorner));
628 if (!pButton)
630 QIcon aIcon;
631 if (QIcon::hasThemeIcon("window-close-symbolic"))
632 aIcon = QIcon::fromTheme("window-close-symbolic");
633 else
634 aIcon = QIcon(
635 QPixmap::fromImage((toQImage(Image(StockImage::Yes, SV_RESID_BITMAP_CLOSEDOC)))));
636 pButton = new QPushButton(mpQMenuBar);
637 pButton->setIcon(aIcon);
638 pButton->setFlat(true);
639 pButton->setFocusPolicy(Qt::NoFocus);
640 pButton->setToolTip(toQString(VclResId(SV_HELPTEXT_CLOSEDOCUMENT)));
641 mpQMenuBar->setCornerWidget(pButton, Qt::TopRightCorner);
642 maCloseButtonConnection
643 = connect(pButton, &QPushButton::clicked, this, &Qt5Menu::slotCloseDocument);
644 mpCloseButton = pButton;
647 if (bShow)
648 pButton->show();
649 else
650 pButton->hide();
653 Qt5MenuItem::Qt5MenuItem(const SalItemParams* pItemData)
654 : mpParentMenu(nullptr)
655 , mpSubMenu(nullptr)
656 , mnId(pItemData->nId)
657 , mnType(pItemData->eType)
658 , mbVisible(true)
659 , mbEnabled(true)
660 , maImage(pItemData->aImage)
664 QAction* Qt5MenuItem::getAction() const
666 if (mpMenu)
667 return mpMenu->menuAction();
668 if (mpAction)
669 return mpAction.get();
670 return nullptr;
673 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */