| /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ |
| /* |
| * This file is part of the Collabora Office project. |
| * |
| * This Source Code Form is subject to the terms of the Mozilla Public |
| * License, v. 2.0. If a copy of the MPL was not distributed with this |
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. |
| */ |
| |
| #include <QtCustomStyle.hxx> |
| #include <vcl/themecolors.hxx> |
| #include <QtMenu.hxx> |
| #include <QtMenu.moc> |
| |
| #include <QtFrame.hxx> |
| #include <QtInstance.hxx> |
| #include <QtMainWindow.hxx> |
| |
| #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) |
| #include <QtWidgets/QActionGroup> |
| #else |
| #include <QtGui/QActionGroup> |
| #endif |
| |
| #include <QtWidgets/QButtonGroup> |
| #include <QtWidgets/QHBoxLayout> |
| #include <QtWidgets/QMenuBar> |
| #include <QtWidgets/QPushButton> |
| #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) |
| #include <QtGui/QShortcut> |
| #else |
| #include <QtWidgets/QShortcut> |
| #endif |
| #include <QtWidgets/QStyle> |
| |
| #include <o3tl/safeint.hxx> |
| #include <vcl/svapp.hxx> |
| |
| #include <strings.hrc> |
| #include <bitmaps.hlst> |
| |
| #include <vcl/qt/QtUtils.hxx> |
| #include <vcl/toolkit/floatwin.hxx> |
| #include <window.h> |
| |
| // LO SalMenuButtonItem::mnId is sal_uInt16, so we go with -2, as -1 has a special meaning as automatic id |
| constexpr int CLOSE_BUTTON_ID = -2; |
| const QString gButtonGroupKey("QtMenu::ButtonGroup"); |
| |
| static inline void lcl_force_menubar_layout_update(QMenuBar& rMenuBar) |
| { |
| // just exists as a function to not comment it everywhere: forces reposition of the |
| // corner widget after its layout changes, which will otherwise just happen on resize. |
| // it unfortunatly has additional side effects; see QtMenu::GetMenuBarButtonRectPixel. |
| rMenuBar.adjustSize(); |
| } |
| |
| OUString QtMenu::m_sCurrentHelpId = u""_ustr; |
| |
| QtMenu::QtMenu(bool bMenuBar) |
| : mpVCLMenu(nullptr) |
| , mpParentSalMenu(nullptr) |
| , mpFrame(nullptr) |
| , mbMenuBar(bMenuBar) |
| , mpQMenuBar(nullptr) |
| , mpQMenu(nullptr) |
| , m_pButtonGroup(nullptr) |
| { |
| } |
| |
| bool QtMenu::VisibleMenuBar() { return true; } |
| |
| void QtMenu::InsertMenuItem(QtMenuItem* pSalMenuItem, unsigned nPos) |
| { |
| SolarMutexGuard g; |
| GetQtInstance().RunInMainThread([&] { |
| sal_uInt16 nId = pSalMenuItem->mnId; |
| const QString aText = vclToQtStringWithAccelerator(mpVCLMenu->GetItemText(nId)); |
| vcl::KeyCode nAccelKey = mpVCLMenu->GetAccelKey(nId); |
| |
| pSalMenuItem->mpAction.reset(); |
| pSalMenuItem->mpMenu.reset(); |
| |
| if (mbMenuBar) |
| { |
| // top-level menu |
| if (validateQMenuBar()) |
| { |
| QMenu* pQMenu = new QMenu(aText, nullptr); |
| connectHelpSignalSlots(pQMenu, pSalMenuItem); |
| pSalMenuItem->mpMenu.reset(pQMenu); |
| |
| if ((nPos != MENU_APPEND) |
| && (static_cast<size_t>(nPos) |
| < o3tl::make_unsigned(mpQMenuBar->actions().size()))) |
| { |
| mpQMenuBar->insertMenu(mpQMenuBar->actions()[nPos], pQMenu); |
| } |
| else |
| { |
| mpQMenuBar->addMenu(pQMenu); |
| } |
| |
| // correct parent menu for generated menu |
| if (pSalMenuItem->mpSubMenu) |
| { |
| pSalMenuItem->mpSubMenu->mpQMenu = pQMenu; |
| } |
| |
| connect(pQMenu, &QMenu::aboutToShow, this, |
| [pSalMenuItem] { slotMenuAboutToShow(pSalMenuItem); }); |
| connect(pQMenu, &QMenu::aboutToHide, this, |
| [pSalMenuItem] { slotMenuAboutToHide(pSalMenuItem); }); |
| } |
| } |
| else |
| { |
| if (!mpQMenu) |
| { |
| // no QMenu set, instantiate own one |
| mpOwnedQMenu.reset(new QMenu); |
| mpQMenu = mpOwnedQMenu.get(); |
| connectHelpSignalSlots(mpQMenu, pSalMenuItem); |
| } |
| |
| if (pSalMenuItem->mpSubMenu) |
| { |
| // submenu |
| QMenu* pQMenu = new QMenu(aText, nullptr); |
| connectHelpSignalSlots(pQMenu, pSalMenuItem); |
| pSalMenuItem->mpMenu.reset(pQMenu); |
| |
| if ((nPos != MENU_APPEND) |
| && (static_cast<size_t>(nPos) < o3tl::make_unsigned(mpQMenu->actions().size()))) |
| { |
| mpQMenu->insertMenu(mpQMenu->actions()[nPos], pQMenu); |
| } |
| else |
| { |
| mpQMenu->addMenu(pQMenu); |
| } |
| |
| // correct parent menu for generated menu |
| pSalMenuItem->mpSubMenu->mpQMenu = pQMenu; |
| |
| ReinitializeActionGroup(nPos); |
| |
| // clear all action groups since menu is recreated |
| pSalMenuItem->mpSubMenu->ResetAllActionGroups(); |
| |
| connect(pQMenu, &QMenu::aboutToShow, this, |
| [pSalMenuItem] { slotMenuAboutToShow(pSalMenuItem); }); |
| connect(pQMenu, &QMenu::aboutToHide, this, |
| [pSalMenuItem] { slotMenuAboutToHide(pSalMenuItem); }); |
| } |
| else |
| { |
| if (pSalMenuItem->mnType == MenuItemType::SEPARATOR) |
| { |
| QAction* pAction = new QAction(nullptr); |
| pSalMenuItem->mpAction.reset(pAction); |
| pAction->setSeparator(true); |
| |
| if ((nPos != MENU_APPEND) |
| && (static_cast<size_t>(nPos) |
| < o3tl::make_unsigned(mpQMenu->actions().size()))) |
| { |
| mpQMenu->insertAction(mpQMenu->actions()[nPos], pAction); |
| } |
| else |
| { |
| mpQMenu->addAction(pAction); |
| } |
| |
| ReinitializeActionGroup(nPos); |
| } |
| else |
| { |
| // leaf menu |
| QAction* pAction = new QAction(aText, nullptr); |
| pAction->setToolTip(toQString(mpVCLMenu->GetTipHelpText(nId))); |
| pSalMenuItem->mpAction.reset(pAction); |
| |
| if ((nPos != MENU_APPEND) |
| && (static_cast<size_t>(nPos) |
| < o3tl::make_unsigned(mpQMenu->actions().size()))) |
| { |
| mpQMenu->insertAction(mpQMenu->actions()[nPos], pAction); |
| } |
| else |
| { |
| mpQMenu->addAction(pAction); |
| } |
| |
| ReinitializeActionGroup(nPos); |
| |
| UpdateActionGroupItem(pSalMenuItem); |
| |
| pAction->setShortcut(toQString(nAccelKey.GetName())); |
| |
| connect(pAction, &QAction::triggered, this, |
| [pSalMenuItem] { slotMenuTriggered(pSalMenuItem); }); |
| connect(pAction, &QAction::hovered, this, |
| [pSalMenuItem] { slotMenuHovered(pSalMenuItem); }); |
| } |
| } |
| } |
| |
| QAction* pAction = pSalMenuItem->getAction(); |
| if (pAction) |
| { |
| pAction->setEnabled(pSalMenuItem->mbEnabled); |
| pAction->setVisible(pSalMenuItem->mbVisible); |
| } |
| }); |
| } |
| |
| void QtMenu::ReinitializeActionGroup(unsigned nPos) |
| { |
| const unsigned nCount = GetItemCount(); |
| |
| if (nCount == 0) |
| { |
| return; |
| } |
| |
| if (nPos == MENU_APPEND) |
| { |
| nPos = nCount - 1; |
| } |
| else if (nPos >= nCount) |
| { |
| return; |
| } |
| |
| QtMenuItem* pPrevItem = (nPos > 0) ? GetItemAtPos(nPos - 1) : nullptr; |
| QtMenuItem* pCurrentItem = GetItemAtPos(nPos); |
| QtMenuItem* pNextItem = (nPos < nCount - 1) ? GetItemAtPos(nPos + 1) : nullptr; |
| |
| if (pCurrentItem->mnType == MenuItemType::SEPARATOR) |
| { |
| pCurrentItem->mpActionGroup.reset(); |
| |
| // if it's inserted into middle of existing group, split it into two groups: |
| // first goes original group, after separator goes new group |
| if (pPrevItem && pPrevItem->mpActionGroup && pNextItem && pNextItem->mpActionGroup |
| && (pPrevItem->mpActionGroup == pNextItem->mpActionGroup)) |
| { |
| std::shared_ptr<QActionGroup> pFirstActionGroup = pPrevItem->mpActionGroup; |
| auto pSecondActionGroup = std::make_shared<QActionGroup>(nullptr); |
| pSecondActionGroup->setExclusive(true); |
| |
| auto actions = pFirstActionGroup->actions(); |
| |
| for (unsigned idx = nPos + 1; idx < nCount; ++idx) |
| { |
| QtMenuItem* pModifiedItem = GetItemAtPos(idx); |
| |
| if ((!pModifiedItem) || (!pModifiedItem->mpActionGroup)) |
| { |
| break; |
| } |
| |
| pModifiedItem->mpActionGroup = pSecondActionGroup; |
| auto action = pModifiedItem->getAction(); |
| |
| if (actions.contains(action)) |
| { |
| pFirstActionGroup->removeAction(action); |
| pSecondActionGroup->addAction(action); |
| } |
| } |
| } |
| } |
| else |
| { |
| if (!pCurrentItem->mpActionGroup) |
| { |
| // unless element is inserted between two separators, or a separator and an end of vector, use neighbouring group since it's shared |
| if (pPrevItem && pPrevItem->mpActionGroup) |
| { |
| pCurrentItem->mpActionGroup = pPrevItem->mpActionGroup; |
| } |
| else if (pNextItem && pNextItem->mpActionGroup) |
| { |
| pCurrentItem->mpActionGroup = pNextItem->mpActionGroup; |
| } |
| else |
| { |
| pCurrentItem->mpActionGroup = std::make_shared<QActionGroup>(nullptr); |
| pCurrentItem->mpActionGroup->setExclusive(true); |
| } |
| } |
| |
| // if there's also a different group after this element, merge it |
| if (pNextItem && pNextItem->mpActionGroup |
| && (pCurrentItem->mpActionGroup != pNextItem->mpActionGroup)) |
| { |
| auto pFirstCheckedAction = pCurrentItem->mpActionGroup->checkedAction(); |
| auto pSecondCheckedAction = pNextItem->mpActionGroup->checkedAction(); |
| auto actions = pNextItem->mpActionGroup->actions(); |
| |
| // first move all actions from second group to first one, and if first group already has checked action, |
| // and second group also has a checked action, uncheck action from second group |
| for (auto action : actions) |
| { |
| pNextItem->mpActionGroup->removeAction(action); |
| |
| if (pFirstCheckedAction && pSecondCheckedAction && (action == pSecondCheckedAction)) |
| { |
| action->setChecked(false); |
| } |
| |
| pCurrentItem->mpActionGroup->addAction(action); |
| } |
| |
| // now replace all pointers to second group with pointers to first group |
| for (unsigned idx = nPos + 1; idx < nCount; ++idx) |
| { |
| QtMenuItem* pModifiedItem = GetItemAtPos(idx); |
| |
| if ((!pModifiedItem) || (!pModifiedItem->mpActionGroup)) |
| { |
| break; |
| } |
| |
| pModifiedItem->mpActionGroup = pCurrentItem->mpActionGroup; |
| } |
| } |
| } |
| } |
| |
| void QtMenu::ResetAllActionGroups() |
| { |
| for (unsigned nItem = 0; nItem < GetItemCount(); ++nItem) |
| { |
| QtMenuItem* pSalMenuItem = GetItemAtPos(nItem); |
| pSalMenuItem->mpActionGroup.reset(); |
| } |
| } |
| |
| void QtMenu::UpdateActionGroupItem(const QtMenuItem* pSalMenuItem) |
| { |
| SolarMutexGuard g; |
| GetQtInstance().RunInMainThread([&] { |
| QAction* pAction = pSalMenuItem->getAction(); |
| if (!pAction) |
| return; |
| |
| bool bChecked = mpVCLMenu->IsItemChecked(pSalMenuItem->mnId); |
| MenuItemBits itemBits = mpVCLMenu->GetItemBits(pSalMenuItem->mnId); |
| |
| if (itemBits & MenuItemBits::RADIOCHECK) |
| { |
| pAction->setCheckable(true); |
| |
| if (pSalMenuItem->mpActionGroup) |
| { |
| pSalMenuItem->mpActionGroup->addAction(pAction); |
| } |
| |
| pAction->setChecked(bChecked); |
| } |
| else |
| { |
| pAction->setActionGroup(nullptr); |
| |
| if (itemBits & MenuItemBits::CHECKABLE) |
| { |
| pAction->setCheckable(true); |
| pAction->setChecked(bChecked); |
| } |
| else |
| { |
| pAction->setChecked(false); |
| pAction->setCheckable(false); |
| } |
| } |
| }); |
| } |
| |
| void QtMenu::InsertItem(SalMenuItem* pSalMenuItem, unsigned nPos) |
| { |
| SolarMutexGuard aGuard; |
| QtMenuItem* pItem = static_cast<QtMenuItem*>(pSalMenuItem); |
| |
| if (nPos == MENU_APPEND) |
| maItems.push_back(pItem); |
| else |
| maItems.insert(maItems.begin() + nPos, pItem); |
| |
| pItem->mpParentMenu = this; |
| |
| InsertMenuItem(pItem, nPos); |
| } |
| |
| void QtMenu::RemoveItem(unsigned nPos) |
| { |
| SolarMutexGuard g; |
| GetQtInstance().RunInMainThread([&] { |
| if (nPos >= maItems.size()) |
| return; |
| |
| QtMenuItem* pItem = maItems[nPos]; |
| pItem->mpAction.reset(); |
| pItem->mpMenu.reset(); |
| |
| maItems.erase(maItems.begin() + nPos); |
| |
| // Recalculate action groups if necessary: |
| // if separator between two QActionGroups was removed, |
| // it may be needed to merge them |
| if (nPos > 0) |
| { |
| ReinitializeActionGroup(nPos - 1); |
| } |
| }); |
| } |
| |
| void QtMenu::SetSubMenu(SalMenuItem* pSalMenuItem, SalMenu* pSubMenu, unsigned nPos) |
| { |
| SolarMutexGuard aGuard; |
| QtMenuItem* pItem = static_cast<QtMenuItem*>(pSalMenuItem); |
| QtMenu* pQSubMenu = static_cast<QtMenu*>(pSubMenu); |
| |
| pItem->mpSubMenu = pQSubMenu; |
| // at this point the pointer to parent menu may be outdated, update it too |
| pItem->mpParentMenu = this; |
| |
| if (pQSubMenu != nullptr) |
| { |
| pQSubMenu->mpParentSalMenu = this; |
| pQSubMenu->mpQMenu = pItem->mpMenu.get(); |
| } |
| |
| // if it's not a menu bar item, then convert it to corresponding item if type if necessary. |
| // If submenu is present and it's an action, convert it to menu. |
| // If submenu is not present and it's a menu, convert it to action. |
| // It may be fine to proceed in any case, but by skipping other cases |
| // amount of unneeded actions taken should be reduced. |
| if (pItem->mpParentMenu->mbMenuBar || (pQSubMenu && pItem->mpMenu) |
| || ((!pQSubMenu) && pItem->mpAction)) |
| { |
| return; |
| } |
| |
| InsertMenuItem(pItem, nPos); |
| } |
| |
| void QtMenu::SetFrame(const SalFrame* pFrame) |
| { |
| QtInstance& rQtInstance = GetQtInstance(); |
| if (!rQtInstance.IsMainThread()) |
| { |
| rQtInstance.RunInMainThread([this, pFrame]() { SetFrame(pFrame); }); |
| return; |
| } |
| |
| SolarMutexGuard aGuard; |
| assert(mbMenuBar); |
| mpFrame = const_cast<QtFrame*>(static_cast<const QtFrame*>(pFrame)); |
| |
| mpFrame->SetMenu(this); |
| |
| QtMainWindow* pMainWindow = mpFrame->GetTopLevelWindow(); |
| if (!pMainWindow) |
| return; |
| |
| mpQMenuBar = new QMenuBar(); |
| mpQMenuBar->installEventFilter(this); |
| pMainWindow->setMenuBar(mpQMenuBar); |
| |
| // open menu bar on F10, as is common in KF 6 and other toolkits: |
| // https://wordsmith.social/felix-ernst/f10-for-accessibility-in-kf6 |
| QShortcut* pQShortcut = new QShortcut(QKeySequence(Qt::Key_F10), mpQMenuBar->window()); |
| connect(pQShortcut, &QShortcut::activated, this, &QtMenu::slotShortcutF10); |
| |
| QWidget* pWidget = mpQMenuBar->cornerWidget(Qt::TopRightCorner); |
| if (pWidget) |
| { |
| m_pButtonGroup = pWidget->findChild<QButtonGroup*>(gButtonGroupKey); |
| assert(m_pButtonGroup); |
| connect(m_pButtonGroup, QOverload<QAbstractButton*>::of(&QButtonGroup::buttonClicked), this, |
| &QtMenu::slotMenuBarButtonClicked); |
| QPushButton* pButton = static_cast<QPushButton*>(m_pButtonGroup->button(CLOSE_BUTTON_ID)); |
| if (pButton) |
| connect(pButton, &QPushButton::clicked, this, &QtMenu::slotCloseDocument); |
| } |
| else |
| m_pButtonGroup = nullptr; |
| mpQMenu = nullptr; |
| |
| DoFullMenuUpdate(mpVCLMenu); |
| } |
| |
| void QtMenu::DoFullMenuUpdate(Menu* pMenuBar) |
| { |
| if (mpQMenuBar && ThemeColors::VclPluginCanUseThemeColors()) |
| mpQMenuBar->setPalette(QtCustomStyle::GetMenuBarPalette()); |
| |
| if (mpQMenu && ThemeColors::VclPluginCanUseThemeColors()) |
| mpQMenu->setPalette(QtCustomStyle::GetMenuPalette()); |
| |
| // clear action groups since menu is rebuilt |
| ResetAllActionGroups(); |
| ShowCloseButton(false); |
| |
| for (sal_Int32 nItem = 0; nItem < static_cast<sal_Int32>(GetItemCount()); nItem++) |
| { |
| QtMenuItem* pSalMenuItem = GetItemAtPos(nItem); |
| InsertMenuItem(pSalMenuItem, nItem); |
| SetItemImage(nItem, pSalMenuItem, pSalMenuItem->maImage); |
| const bool bShowDisabled |
| = bool(pMenuBar->GetMenuFlags() & MenuFlags::AlwaysShowDisabledEntries) |
| || !bool(pMenuBar->GetMenuFlags() & MenuFlags::HideDisabledEntries); |
| const bool bVisible = pSalMenuItem->mbVisible |
| && (bShowDisabled || mpVCLMenu->IsItemEnabled(pSalMenuItem->mnId)); |
| pSalMenuItem->getAction()->setVisible(bVisible); |
| |
| if (pSalMenuItem->mpSubMenu != nullptr) |
| { |
| pMenuBar->HandleMenuActivateEvent(pSalMenuItem->mpSubMenu->GetMenu()); |
| pSalMenuItem->mpSubMenu->DoFullMenuUpdate(pMenuBar); |
| pMenuBar->HandleMenuDeActivateEvent(pSalMenuItem->mpSubMenu->GetMenu()); |
| } |
| } |
| } |
| |
| void QtMenu::ShowItem(unsigned nPos, bool bShow) |
| { |
| SolarMutexGuard g; |
| GetQtInstance().RunInMainThread([&] { |
| if (nPos < maItems.size()) |
| { |
| QtMenuItem* pSalMenuItem = GetItemAtPos(nPos); |
| QAction* pAction = pSalMenuItem->getAction(); |
| if (pAction) |
| pAction->setVisible(bShow); |
| pSalMenuItem->mbVisible = bShow; |
| } |
| }); |
| } |
| |
| void QtMenu::SetItemBits(unsigned nPos, MenuItemBits) |
| { |
| if (nPos < maItems.size()) |
| { |
| QtMenuItem* pSalMenuItem = GetItemAtPos(nPos); |
| UpdateActionGroupItem(pSalMenuItem); |
| } |
| } |
| |
| void QtMenu::CheckItem(unsigned nPos, bool bChecked) |
| { |
| SolarMutexGuard g; |
| GetQtInstance().RunInMainThread([&] { |
| if (nPos < maItems.size()) |
| { |
| QtMenuItem* pSalMenuItem = GetItemAtPos(nPos); |
| QAction* pAction = pSalMenuItem->getAction(); |
| if (pAction) |
| { |
| pAction->setCheckable(true); |
| pAction->setChecked(bChecked); |
| } |
| } |
| }); |
| } |
| |
| void QtMenu::EnableItem(unsigned nPos, bool bEnable) |
| { |
| SolarMutexGuard g; |
| GetQtInstance().RunInMainThread([&] { |
| if (nPos < maItems.size()) |
| { |
| QtMenuItem* pSalMenuItem = GetItemAtPos(nPos); |
| QAction* pAction = pSalMenuItem->getAction(); |
| if (pAction) |
| pAction->setEnabled(bEnable); |
| pSalMenuItem->mbEnabled = bEnable; |
| } |
| }); |
| } |
| |
| void QtMenu::SetItemText(unsigned, SalMenuItem* pItem, const OUString& rText) |
| { |
| SolarMutexGuard g; |
| GetQtInstance().RunInMainThread([&] { |
| QtMenuItem* pSalMenuItem = static_cast<QtMenuItem*>(pItem); |
| QAction* pAction = pSalMenuItem->getAction(); |
| if (pAction) |
| pAction->setText(vclToQtStringWithAccelerator(rText)); |
| }); |
| } |
| |
| void QtMenu::SetItemImage(unsigned, SalMenuItem* pItem, const Image& rImage) |
| { |
| SolarMutexGuard g; |
| GetQtInstance().RunInMainThread([&] { |
| QtMenuItem* pSalMenuItem = static_cast<QtMenuItem*>(pItem); |
| |
| // Save new image to use it in DoFullMenuUpdate |
| pSalMenuItem->maImage = rImage; |
| |
| QAction* pAction = pSalMenuItem->getAction(); |
| if (!pAction) |
| return; |
| |
| pAction->setIcon(QPixmap::fromImage(toQImage(rImage))); |
| }); |
| } |
| |
| void QtMenu::SetItemTooltip(SalMenuItem* pItem, const OUString& rTooltip) |
| { |
| SolarMutexGuard g; |
| GetQtInstance().RunInMainThread([&] { |
| QtMenuItem* pSalMenuItem = static_cast<QtMenuItem*>(pItem); |
| |
| if (QAction* pAction = pSalMenuItem->getAction()) |
| pAction->setToolTip(toQString(rTooltip)); |
| }); |
| } |
| |
| void QtMenu::SetAccelerator(unsigned, SalMenuItem* pItem, const vcl::KeyCode&, |
| const OUString& rText) |
| { |
| SolarMutexGuard g; |
| GetQtInstance().RunInMainThread([&] { |
| QtMenuItem* pSalMenuItem = static_cast<QtMenuItem*>(pItem); |
| QAction* pAction = pSalMenuItem->getAction(); |
| if (pAction) |
| pAction->setShortcut(QKeySequence(toQString(rText), QKeySequence::PortableText)); |
| }); |
| } |
| |
| QtMenu* QtMenu::GetTopLevel() |
| { |
| QtMenu* pMenu = this; |
| while (pMenu->mpParentSalMenu) |
| pMenu = pMenu->mpParentSalMenu; |
| return pMenu; |
| } |
| |
| bool QtMenu::validateQMenuBar() const |
| { |
| if (!mpQMenuBar) |
| return false; |
| assert(mpFrame); |
| QtMainWindow* pMainWindow = mpFrame->GetTopLevelWindow(); |
| assert(pMainWindow); |
| const bool bValid = mpQMenuBar == pMainWindow->menuBar(); |
| if (!bValid) |
| { |
| QtMenu* thisPtr = const_cast<QtMenu*>(this); |
| thisPtr->mpQMenuBar = nullptr; |
| } |
| return bValid; |
| } |
| |
| void QtMenu::ShowMenuBar(bool bVisible) |
| { |
| SolarMutexGuard g; |
| GetQtInstance().RunInMainThread([&] { |
| if (!validateQMenuBar()) |
| return; |
| |
| mpQMenuBar->setVisible(bVisible); |
| if (bVisible) |
| lcl_force_menubar_layout_update(*mpQMenuBar); |
| }); |
| } |
| |
| void QtMenu::slotMenuHovered(QtMenuItem* pItem) |
| { |
| const OUString sHelpId = pItem->mpParentMenu->GetMenu()->GetHelpId(pItem->mnId); |
| m_sCurrentHelpId = sHelpId; |
| } |
| |
| void QtMenu::slotShowHelp() |
| { |
| SolarMutexGuard aGuard; |
| Help* pHelp = Application::GetHelp(); |
| if (pHelp && !m_sCurrentHelpId.isEmpty()) |
| { |
| pHelp->Start(m_sCurrentHelpId); |
| } |
| } |
| |
| void QtMenu::slotMenuTriggered(QtMenuItem* pQItem) |
| { |
| if (!pQItem) |
| return; |
| |
| QtMenu* pSalMenu = pQItem->mpParentMenu; |
| QtMenu* pTopLevel = pSalMenu->GetTopLevel(); |
| |
| Menu* pMenu = pSalMenu->GetMenu(); |
| auto mnId = pQItem->mnId; |
| |
| // HACK to allow HandleMenuCommandEvent to "not-set" the checked button |
| // LO expects a signal before an item state change, so reset the check item |
| if (pQItem->mpAction->isCheckable() |
| && (!pQItem->mpActionGroup || pQItem->mpActionGroup->actions().size() <= 1)) |
| pQItem->mpAction->setChecked(!pQItem->mpAction->isChecked()); |
| pTopLevel->GetMenu()->HandleMenuCommandEvent(pMenu, mnId); |
| } |
| |
| void QtMenu::slotMenuAboutToShow(QtMenuItem* pQItem) |
| { |
| if (pQItem) |
| { |
| QtMenu* pSalMenu = pQItem->mpSubMenu; |
| QtMenu* pTopLevel = pSalMenu->GetTopLevel(); |
| |
| Menu* pMenu = pSalMenu->GetMenu(); |
| |
| // following function may update the menu |
| pTopLevel->GetMenu()->HandleMenuActivateEvent(pMenu); |
| } |
| } |
| |
| void QtMenu::slotMenuAboutToHide(QtMenuItem* pQItem) |
| { |
| if (pQItem) |
| { |
| QtMenu* pSalMenu = pQItem->mpSubMenu; |
| QtMenu* pTopLevel = pSalMenu->GetTopLevel(); |
| |
| Menu* pMenu = pSalMenu->GetMenu(); |
| |
| pTopLevel->GetMenu()->HandleMenuDeActivateEvent(pMenu); |
| } |
| } |
| |
| void QtMenu::slotCloseDocument() |
| { |
| MenuBar* pVclMenuBar = static_cast<MenuBar*>(mpVCLMenu.get()); |
| if (pVclMenuBar) |
| Application::PostUserEvent(pVclMenuBar->GetCloseButtonClickHdl()); |
| } |
| |
| void QtMenu::slotMenuBarButtonClicked(QAbstractButton* pButton) |
| { |
| MenuBar* pVclMenuBar = static_cast<MenuBar*>(mpVCLMenu.get()); |
| if (pVclMenuBar) |
| { |
| SolarMutexGuard aGuard; |
| pVclMenuBar->HandleMenuButtonEvent(m_pButtonGroup->id(pButton)); |
| } |
| } |
| |
| void QtMenu::slotShortcutF10() |
| { |
| SolarMutexGuard aGuard; |
| |
| // focus menu bar and select first item |
| if (mpQMenuBar && !mpQMenuBar->actions().empty()) |
| mpQMenuBar->setActiveAction(mpQMenuBar->actions().at(0)); |
| } |
| |
| QPushButton* QtMenu::ImplAddMenuBarButton(const QIcon& rIcon, const QString& rToolTip, int nId) |
| { |
| if (!validateQMenuBar()) |
| return nullptr; |
| |
| QWidget* pWidget = mpQMenuBar->cornerWidget(Qt::TopRightCorner); |
| QHBoxLayout* pLayout; |
| if (!pWidget) |
| { |
| assert(!m_pButtonGroup); |
| pWidget = GetQtInstance().EmscriptenLightweightRunInMainThread( |
| [this] { return new QWidget(mpQMenuBar); }); |
| assert(!pWidget->layout()); |
| pLayout = GetQtInstance().EmscriptenLightweightRunInMainThread( |
| [] { return new QHBoxLayout(); }); |
| pLayout->setContentsMargins(QMargins()); |
| pLayout->setSpacing(0); |
| pWidget->setLayout(pLayout); |
| m_pButtonGroup = GetQtInstance().EmscriptenLightweightRunInMainThread( |
| [pLayout] { return new QButtonGroup(pLayout); }); |
| m_pButtonGroup->setObjectName(gButtonGroupKey); |
| m_pButtonGroup->setExclusive(false); |
| connect(m_pButtonGroup, QOverload<QAbstractButton*>::of(&QButtonGroup::buttonClicked), this, |
| &QtMenu::slotMenuBarButtonClicked); |
| GetQtInstance().EmscriptenLightweightRunInMainThread([this, pWidget] { |
| pWidget->show(); |
| mpQMenuBar->setCornerWidget(pWidget, Qt::TopRightCorner); |
| }); |
| } |
| else |
| pLayout = static_cast<QHBoxLayout*>(pWidget->layout()); |
| assert(m_pButtonGroup); |
| assert(pLayout); |
| |
| QPushButton* pButton = static_cast<QPushButton*>(m_pButtonGroup->button(nId)); |
| if (pButton) |
| RemoveMenuBarButton(nId); |
| |
| pButton |
| = GetQtInstance().EmscriptenLightweightRunInMainThread([] { return new QPushButton(); }); |
| // we don't want the button to increase the QMenuBar height, so a fixed size square it is |
| const int nFixedLength |
| = mpQMenuBar->height() - 2 * mpQMenuBar->style()->pixelMetric(QStyle::PM_MenuBarVMargin); |
| pButton->setFixedSize(nFixedLength, nFixedLength); |
| pButton->setIcon(rIcon); |
| pButton->setFlat(true); |
| pButton->setFocusPolicy(Qt::NoFocus); |
| pButton->setToolTip(rToolTip); |
| |
| m_pButtonGroup->addButton(pButton, nId); |
| int nPos = pLayout->count(); |
| if (m_pButtonGroup->button(CLOSE_BUTTON_ID)) |
| nPos--; |
| pLayout->insertWidget(nPos, pButton, 0, Qt::AlignCenter); |
| // show must happen after adding the button to the layout, otherwise the button is |
| // shown, but not correct in the layout, if at all! Some times the layout ignores it. |
| pButton->show(); |
| |
| lcl_force_menubar_layout_update(*mpQMenuBar); |
| |
| return pButton; |
| } |
| |
| bool QtMenu::AddMenuBarButton(const SalMenuButtonItem& rItem) |
| { |
| if (!validateQMenuBar()) |
| return false; |
| return !!ImplAddMenuBarButton(QIcon(QPixmap::fromImage(toQImage(rItem.maImage))), |
| toQString(rItem.maToolTipText), rItem.mnId); |
| } |
| |
| void QtMenu::connectHelpShortcut(QMenu* pMenu) |
| { |
| assert(pMenu); |
| QKeySequence sequence(QKeySequence::HelpContents); |
| QShortcut* pQShortcut; |
| pQShortcut = GetQtInstance().EmscriptenLightweightRunInMainThread( |
| [&sequence, pMenu] { return new QShortcut(sequence, pMenu); }); |
| connect(pQShortcut, &QShortcut::activated, this, QtMenu::slotShowHelp); |
| connect(pQShortcut, &QShortcut::activatedAmbiguously, this, QtMenu::slotShowHelp); |
| } |
| |
| void QtMenu::connectHelpSignalSlots(QMenu* pMenu, QtMenuItem* pSalMenuItem) |
| { |
| // connect hovered signal of the menu's own action |
| QAction* pAction = pMenu->menuAction(); |
| assert(pAction); |
| connect(pAction, &QAction::hovered, this, [pSalMenuItem] { slotMenuHovered(pSalMenuItem); }); |
| |
| // connect slot to handle Help key (F1) |
| connectHelpShortcut(pMenu); |
| |
| // enable tooltips, Qt doesn't show them for menu entries by default |
| pMenu->setToolTipsVisible(true); |
| } |
| |
| void QtMenu::RemoveMenuBarButton(sal_uInt16 nId) |
| { |
| SolarMutexGuard g; |
| GetQtInstance().RunInMainThread([&] { |
| if (!validateQMenuBar()) |
| return; |
| |
| assert(m_pButtonGroup); |
| auto* pButton = m_pButtonGroup->button(nId); |
| assert(pButton); |
| QWidget* pWidget = mpQMenuBar->cornerWidget(Qt::TopRightCorner); |
| assert(pWidget); |
| QLayout* pLayout = pWidget->layout(); |
| m_pButtonGroup->removeButton(pButton); |
| pLayout->removeWidget(pButton); |
| delete pButton; |
| |
| lcl_force_menubar_layout_update(*mpQMenuBar); |
| }); |
| } |
| |
| tools::Rectangle QtMenu::GetMenuBarButtonRectPixel(sal_uInt16 nId, SalFrame* pFrame) |
| { |
| #ifdef NDEBUG |
| Q_UNUSED(pFrame); |
| #endif |
| if (!validateQMenuBar()) |
| return tools::Rectangle(); |
| |
| assert(mpFrame == static_cast<QtFrame*>(pFrame)); |
| assert(m_pButtonGroup); |
| auto* pButton = static_cast<QPushButton*>(m_pButtonGroup->button(nId)); |
| assert(pButton); |
| |
| // unfortunatly, calling lcl_force_menubar_layout_update results in a temporary wrong menubar size, |
| // but it's the correct minimal size AFAIK and the layout seems correct, so just adjust the width. |
| QPoint aPos = pButton->mapTo(mpFrame->asChild(), QPoint()); |
| aPos.rx() += (mpFrame->asChild()->width() - mpQMenuBar->width()); |
| return tools::Rectangle(toPoint(aPos), toSize(pButton->size())); |
| } |
| |
| void QtMenu::ShowCloseButton(bool bShow) |
| { |
| SolarMutexGuard g; |
| GetQtInstance().RunInMainThread([&] { |
| if (!validateQMenuBar()) |
| return; |
| |
| if (!bShow && !m_pButtonGroup) |
| return; |
| |
| QPushButton* pButton = nullptr; |
| if (m_pButtonGroup) |
| pButton = static_cast<QPushButton*>(m_pButtonGroup->button(CLOSE_BUTTON_ID)); |
| if (!bShow && !pButton) |
| return; |
| |
| if (!pButton) |
| { |
| QIcon aIcon; |
| if (QIcon::hasThemeIcon("window-close-symbolic")) |
| aIcon = QIcon::fromTheme("window-close-symbolic"); |
| else |
| aIcon = QIcon( |
| QPixmap::fromImage(toQImage(Image(StockImage::Yes, SV_RESID_BITMAP_CLOSEDOC)))); |
| pButton = ImplAddMenuBarButton(aIcon, toQString(VclResId(SV_HELPTEXT_CLOSEDOCUMENT)), |
| CLOSE_BUTTON_ID); |
| connect(pButton, &QPushButton::clicked, this, &QtMenu::slotCloseDocument); |
| } |
| |
| if (bShow) |
| pButton->show(); |
| else |
| pButton->hide(); |
| |
| lcl_force_menubar_layout_update(*mpQMenuBar); |
| }); |
| } |
| |
| bool QtMenu::ShowNativePopupMenu(FloatingWindow* pWin, const tools::Rectangle& rRect, |
| FloatWinPopupFlags nFlags) |
| { |
| assert(mpQMenu); |
| DoFullMenuUpdate(mpVCLMenu); |
| mpQMenu->setTearOffEnabled(bool(nFlags & FloatWinPopupFlags::AllowTearOff)); |
| |
| const VclPtr<vcl::Window> xParent = pWin->ImplGetWindowImpl()->mpRealParent; |
| AbsoluteScreenPixelRectangle aFloatRect = FloatingWindow::ImplConvertToAbsPos(xParent, rRect); |
| |
| QtFrame* pFrame = static_cast<QtFrame*>(pWin->ImplGetFrame()); |
| assert(pFrame); |
| |
| const QRect aRect = toQRect(aFloatRect, 1 / pFrame->devicePixelRatioF()); |
| mpQMenu->exec(aRect.bottomLeft()); |
| |
| return true; |
| } |
| |
| int QtMenu::GetMenuBarHeight() const |
| { |
| if (!validateQMenuBar() || mpQMenuBar->isHidden()) |
| return 0; |
| |
| return mpQMenuBar->height(); |
| } |
| |
| QtMenu::~QtMenu() |
| { |
| SolarMutexGuard g; |
| GetQtInstance().RunInMainThread([&] { mpOwnedQMenu.reset(); }); |
| } |
| |
| QtMenuItem::QtMenuItem(const SalItemParams* pItemData) |
| : mpParentMenu(nullptr) |
| , mpSubMenu(nullptr) |
| , mnId(pItemData->nId) |
| , mnType(pItemData->eType) |
| , mbVisible(true) |
| , mbEnabled(true) |
| , maImage(pItemData->aImage) |
| { |
| } |
| |
| QAction* QtMenuItem::getAction() const |
| { |
| if (mpMenu) |
| return mpMenu->menuAction(); |
| if (mpAction) |
| return mpAction.get(); |
| return nullptr; |
| } |
| |
| /* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */ |