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 <vcl/svapp.hxx>
21 #include <sal/log.hxx>
23 #include <strings.hrc>
24 #include <bitmaps.hlst>
26 Qt5Menu::Qt5Menu(bool bMenuBar
)
28 , mpParentSalMenu(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();
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
);
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
); });
81 if (pSalMenuItem
->mpSubMenu
)
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
);
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
); });
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
);
125 mpQMenu
->addAction(pAction
);
128 ReinitializeActionGroup(nPos
);
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
);
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();
161 pAction
->setEnabled(pSalMenuItem
->mbEnabled
);
162 pAction
->setVisible(pSalMenuItem
->mbVisible
);
166 void Qt5Menu::ReinitializeActionGroup(unsigned nPos
)
168 const unsigned nCount
= GetItemCount();
175 if (nPos
== MENU_APPEND
)
179 else if (nPos
>= nCount
)
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
))
212 pModifiedItem
->mpActionGroup
= pSecondActionGroup
;
213 auto action
= pModifiedItem
->getAction();
215 if (actions
.contains(action
))
217 pFirstActionGroup
->removeAction(action
);
218 pSecondActionGroup
->addAction(action
);
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
;
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
))
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();
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
);
312 pAction
->setActionGroup(nullptr);
314 if (itemBits
& MenuItemBits::CHECKABLE
)
316 pAction
->setCheckable(true);
317 pAction
->setChecked(bChecked
);
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
);
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
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
))
391 InsertMenuItem(pItem
, nPos
);
394 void Qt5Menu::SetFrame(const SalFrame
* pFrame
)
396 auto* pSalInst(static_cast<Qt5Instance
*>(GetSalData()->m_pInstance
));
398 if (!pSalInst
->IsMainThread())
400 pSalInst
->RunInMainThread([this, pFrame
]() { SetFrame(pFrame
); });
404 SolarMutexGuard aGuard
;
406 mpFrame
= const_cast<Qt5Frame
*>(static_cast<const Qt5Frame
*>(pFrame
));
408 mpFrame
->SetMenu(this);
410 Qt5MainWindow
* pMainWindow
= mpFrame
->GetTopLevelWindow();
413 mpQMenuBar
= pMainWindow
->menuBar();
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
;
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();
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();
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();
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();
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();
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();
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
;
546 void Qt5Menu::ShowMenuBar(bool bVisible
)
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
)
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
)
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
)
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());
619 Application::PostUserEvent(pVclMenuBar
->GetCloseButtonClickHdl());
622 void Qt5Menu::ShowCloseButton(bool bShow
)
627 QPushButton
* pButton
= static_cast<QPushButton
*>(mpQMenuBar
->cornerWidget(Qt::TopRightCorner
));
631 if (QIcon::hasThemeIcon("window-close-symbolic"))
632 aIcon
= QIcon::fromTheme("window-close-symbolic");
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
;
653 Qt5MenuItem::Qt5MenuItem(const SalItemParams
* pItemData
)
654 : mpParentMenu(nullptr)
656 , mnId(pItemData
->nId
)
657 , mnType(pItemData
->eType
)
660 , maImage(pItemData
->aImage
)
664 QAction
* Qt5MenuItem::getAction() const
667 return mpMenu
->menuAction();
669 return mpAction
.get();
673 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */