/* BEGIN_COMMON_COPYRIGHT_HEADER * (c)LGPL2+ * * LXDE-Qt - a lightweight, Qt based, desktop toolset * http://razor-qt.org * * Copyright: 2010-2011 Razor team * Authors: * Alexander Sokoloff * * This program or library is free software; you can redistribute it * and/or modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * You should have received a copy of the GNU Lesser General * Public License along with this library; if not, write to the * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301 USA * * END_COMMON_COPYRIGHT_HEADER */ #include "lxqtmainmenu.h" #include "lxqtmainmenuconfiguration.h" #include "../panel/lxqtpanel.h" #include "actionview.h" #include #include #include #include #include #include #include #include #include #include // for find_if() #include #include #include #ifdef HAVE_MENU_CACHE #include "xdgcachedmenu.h" #endif #define DEFAULT_SHORTCUT "Alt+F1" LXQtMainMenu::LXQtMainMenu(const ILXQtPanelPluginStartupInfo &startupInfo): QObject(), ILXQtPanelPlugin(startupInfo), mMenu(0), mShortcut(0), mSearchEditAction{new QWidgetAction{this}}, mSearchViewAction{new QWidgetAction{this}}, mMakeDirtyAction{new QAction{this}}, mFilterMenu(true), mFilterShow(true), mFilterShowHideMenu(true), mHeavyMenuChanges(false) { #ifdef HAVE_MENU_CACHE mMenuCache = NULL; mMenuCacheNotify = 0; #endif mDelayedPopup.setSingleShot(true); mDelayedPopup.setInterval(200); connect(&mDelayedPopup, &QTimer::timeout, this, &LXQtMainMenu::showHideMenu); mHideTimer.setSingleShot(true); mHideTimer.setInterval(250); mButton.setAutoRaise(true); mButton.setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); //Notes: //1. installing event filter to parent widget to avoid infinite loop // (while setting icon we also need to set the style) //2. delaying of installEventFilter because in c-tor mButton has no parent widget // (parent is assigned in panel's logic after widget() call) QTimer::singleShot(0, [this] { Q_ASSERT(mButton.parentWidget()); mButton.parentWidget()->installEventFilter(this); }); connect(&mButton, &QToolButton::clicked, this, &LXQtMainMenu::showHideMenu); mSearchView = new ActionView; mSearchView->setVisible(false); connect(mSearchView, &QAbstractItemView::activated, this, &LXQtMainMenu::showHideMenu); mSearchViewAction->setDefaultWidget(mSearchView); mSearchEdit = new QLineEdit; mSearchEdit->setClearButtonEnabled(true); mSearchEdit->setPlaceholderText(LXQtMainMenu::tr("Search...")); connect(mSearchEdit, &QLineEdit::textChanged, this, &LXQtMainMenu::searchTextChanged); connect(mSearchEdit, &QLineEdit::returnPressed, mSearchView, &ActionView::activateCurrent); mSearchEditAction->setDefaultWidget(mSearchEdit); QTimer::singleShot(0, [this] { settingsChanged(); }); mShortcut = GlobalKeyShortcut::Client::instance()->addAction(QString{}, QString("/panel/%1/show_hide").arg(settings()->group()), LXQtMainMenu::tr("Show/hide main menu"), this); if (mShortcut) { connect(mShortcut, &GlobalKeyShortcut::Action::registrationFinished, [this] { if (mShortcut->shortcut().isEmpty()) mShortcut->changeShortcut(DEFAULT_SHORTCUT); }); connect(mShortcut, &GlobalKeyShortcut::Action::activated, [this] { if (!mHideTimer.isActive()) // Delay this a little -- if we don't do this, search field // won't be able to capture focus // See and // mDelayedPopup.start(); }); } } /************************************************ ************************************************/ LXQtMainMenu::~LXQtMainMenu() { mButton.parentWidget()->removeEventFilter(this); if (mMenu) { mMenu->removeAction(mSearchEditAction); mMenu->removeAction(mSearchViewAction); } #ifdef HAVE_MENU_CACHE if(mMenuCache) { menu_cache_remove_reload_notify(mMenuCache, mMenuCacheNotify); menu_cache_unref(mMenuCache); } #endif } /************************************************ ************************************************/ void LXQtMainMenu::showHideMenu() { if (mMenu && mMenu->isVisible()) mMenu->hide(); else showMenu(); } /************************************************ ************************************************/ void LXQtMainMenu::showMenu() { if (!mMenu) return; willShowWindow(mMenu); // Just using Qt`s activateWindow() won't work on some WMs like Kwin. // Solution is to execute menu 1ms later using timer mMenu->popup(calculatePopupWindowPos(mMenu->sizeHint()).topLeft()); if (mFilterMenu || mFilterShow) { //Note: part of the workadound for https://bugreports.qt.io/browse/QTBUG-52021 mSearchEdit->setReadOnly(false); //the setReadOnly also changes the cursor, override it back to normal mSearchEdit->unsetCursor(); mSearchEdit->setFocus(); } } #ifdef HAVE_MENU_CACHE // static void LXQtMainMenu::menuCacheReloadNotify(MenuCache* cache, gpointer user_data) { reinterpret_cast(user_data)->buildMenu(); } #endif /************************************************ ************************************************/ void LXQtMainMenu::settingsChanged() { setButtonIcon(); if (settings()->value("showText", false).toBool()) { mButton.setText(settings()->value("text", "Start").toString()); mButton.setToolButtonStyle(Qt::ToolButtonTextBesideIcon); } else { mButton.setText(""); mButton.setToolButtonStyle(Qt::ToolButtonIconOnly); } mLogDir = settings()->value("log_dir", "").toString(); QString menu_file = settings()->value("menu_file", "").toString(); if (menu_file.isEmpty()) menu_file = XdgMenu::getMenuFileName(); if (mMenuFile != menu_file) { mMenuFile = menu_file; #ifdef HAVE_MENU_CACHE menu_cache_init(0); if(mMenuCache) { menu_cache_remove_reload_notify(mMenuCache, mMenuCacheNotify); menu_cache_unref(mMenuCache); } mMenuCache = menu_cache_lookup(mMenuFile.toLocal8Bit()); if (menu_cache_get_root_dir(mMenuCache)) buildMenu(); mMenuCacheNotify = menu_cache_add_reload_notify(mMenuCache, (MenuCacheReloadNotify)menuCacheReloadNotify, this); #else mXdgMenu.setEnvironments(QStringList() << "X-LXQT" << "LXQt"); mXdgMenu.setLogDir(mLogDir); bool res = mXdgMenu.read(mMenuFile); connect(&mXdgMenu, SIGNAL(changed()), this, SLOT(buildMenu())); if (res) { QTimer::singleShot(1000, this, SLOT(buildMenu())); } else { QMessageBox::warning(0, "Parse error", mXdgMenu.errorString()); return; } #endif } setMenuFontSize(); //clear the search to not leaving the menu in wrong state mSearchEdit->setText(QString{}); mFilterMenu = settings()->value("filterMenu", true).toBool(); mFilterShow = settings()->value("filterShow", true).toBool(); mFilterShowHideMenu = settings()->value("filterShowHideMenu", true).toBool(); if (mMenu) { mSearchEdit->setVisible(mFilterMenu || mFilterShow); mSearchEditAction->setVisible(mFilterMenu || mFilterShow); } mSearchView->setMaxItemsToShow(settings()->value("filterShowMaxItems", 10).toInt()); mSearchView->setMaxItemWidth(settings()->value("filterShowMaxWidth", 300).toInt()); realign(); } static bool filterMenu(QMenu * menu, QString const & filter) { bool has_visible = false; for (auto const & action : menu->actions()) { if (QMenu * sub_menu = action->menu()) { action->setVisible(filterMenu(sub_menu, filter)/*recursion*/); has_visible |= action->isVisible(); } else if (nullptr != qobject_cast(action)) { //our searching widget has_visible = true; } else if (!action->isSeparator()) { //real menu action -> app action->setVisible(filter.isEmpty() || action->text().contains(filter, Qt::CaseInsensitive) || action->toolTip().contains(filter, Qt::CaseInsensitive)); has_visible |= action->isVisible(); } } return has_visible; } static void showHideMenuEntries(QMenu * menu, bool show) { //show/hide the top menu entries for (auto const & action : menu->actions()) { if (nullptr == qobject_cast(action)) { action->setVisible(show); } } } /************************************************ ************************************************/ void LXQtMainMenu::searchTextChanged(QString const & text) { if (mFilterShow) { mHeavyMenuChanges = true; const bool shown = !text.isEmpty(); if (mFilterShowHideMenu) showHideMenuEntries(mMenu, !shown); if (shown) mSearchView->setFilter(text); mSearchView->setVisible(shown); mSearchViewAction->setVisible(shown); //TODO: how to force the menu to recalculate it's size in a more elegant way? mMenu->addAction(mMakeDirtyAction); mMenu->removeAction(mMakeDirtyAction); mHeavyMenuChanges = false; } if (mFilterMenu && !(mFilterShow && mFilterShowHideMenu)) filterMenu(mMenu, text); } /************************************************ ************************************************/ void LXQtMainMenu::setSearchFocus(QAction *action) { if (mFilterMenu || mFilterShow) { if(action == mSearchEditAction) mSearchEdit->setFocus(); else mSearchEdit->clearFocus(); } } /************************************************ ************************************************/ void LXQtMainMenu::buildMenu() { if(mMenu) { mMenu->removeAction(mSearchEditAction); mMenu->removeAction(mSearchViewAction); delete mMenu; } #ifdef HAVE_MENU_CACHE mMenu = new XdgCachedMenu(mMenuCache, &mButton); #else mMenu = new XdgMenuWidget(mXdgMenu, "", &mButton); #endif mMenu->setObjectName("TopLevelMainMenu"); // Note: the QWidget::ensurePolished() workarounds problem with transparent // QLineEdit (mSearchEditAction) in menu with Breeze style // https://bugs.kde.org/show_bug.cgi?id=368048 mMenu->ensurePolished(); mMenu->setStyle(&mTopMenuStyle); mMenu->addSeparator(); Q_FOREACH(QAction* action, mMenu->actions()) { if (action->menu()) action->menu()->installEventFilter(this); } mMenu->installEventFilter(this); connect(mMenu, &QMenu::aboutToHide, &mHideTimer, static_cast(&QTimer::start)); connect(mMenu, &QMenu::aboutToShow, &mHideTimer, &QTimer::stop); mMenu->addSeparator(); mMenu->addAction(mSearchViewAction); mMenu->addAction(mSearchEditAction); connect(mMenu, &QMenu::hovered, this, &LXQtMainMenu::setSearchFocus); //Note: setting readOnly to true to avoid wake-ups upon the Qt's internal "blink" cursor timer //(if the readOnly is not set, the "blink" timer is active also in case the menu is not shown -> //QWidgetLineControl::updateNeeded is performed w/o any need) //https://bugreports.qt.io/browse/QTBUG-52021 connect(mMenu, &QMenu::aboutToHide, [this] { mSearchEdit->setReadOnly(true); }); mSearchEdit->setVisible(mFilterMenu || mFilterShow); mSearchEditAction->setVisible(mFilterMenu || mFilterShow); mSearchView->fillActions(mMenu); searchTextChanged(mSearchEdit->text()); setMenuFontSize(); } /************************************************ ************************************************/ void LXQtMainMenu::setMenuFontSize() { if (!mMenu) return; QFont menuFont = mButton.font(); if(settings()->value("customFont", false).toBool()) { menuFont = mMenu->font(); menuFont.setPointSize(settings()->value("customFontSize").toInt()); } if (mMenu->font() != menuFont) { mMenu->setFont(menuFont); QList subMenuList = mMenu->findChildren(); foreach (QMenu* subMenu, subMenuList) { subMenu->setFont(menuFont); } mSearchEdit->setFont(menuFont); mSearchView->setFont(menuFont); } //icon size the same as the font height const int icon_size = QFontMetrics(menuFont).height(); mTopMenuStyle.setIconSize(icon_size); mSearchView->setIconSize(QSize{icon_size, icon_size}); } /************************************************ ************************************************/ void LXQtMainMenu::setButtonIcon() { if (settings()->value("ownIcon", false).toBool()) { mButton.setStyleSheet(QString("#MainMenu { qproperty-icon: url(%1); }") .arg(settings()->value(QLatin1String("icon"), QLatin1String(LXQT_GRAPHICS_DIR"/helix.svg")).toString())); } else { mButton.setStyleSheet(QString()); } } /************************************************ ************************************************/ QDialog *LXQtMainMenu::configureDialog() { return new LXQtMainMenuConfiguration(settings(), mShortcut, DEFAULT_SHORTCUT); } /************************************************ ************************************************/ // functor used to match a QAction by prefix struct MatchAction { MatchAction(QString key):key_(key) {} bool operator()(QAction* action) { return action->text().startsWith(key_, Qt::CaseInsensitive); } QString key_; }; bool LXQtMainMenu::eventFilter(QObject *obj, QEvent *event) { if(obj == mButton.parentWidget()) { // the application is given a new QStyle if(event->type() == QEvent::StyleChange) { setMenuFontSize(); setButtonIcon(); } } else if(QMenu* menu = qobject_cast(obj)) { if(event->type() == QEvent::KeyPress) { // if our shortcut key is pressed while the menu is open, close the menu QKeyEvent* keyEvent = static_cast(event); if (keyEvent->modifiers() & ~Qt::ShiftModifier) { mHideTimer.start(); mMenu->hide(); // close the app menu return true; } else // go to the menu item starts with the pressed key { QString key = keyEvent->text(); if(key.isEmpty()) return false; QAction* action = menu->activeAction(); QList actions = menu->actions(); QList::iterator it = qFind(actions.begin(), actions.end(), action); it = std::find_if(it + 1, actions.end(), MatchAction(key)); if(it == actions.end()) it = std::find_if(actions.begin(), it, MatchAction(key)); if(it != actions.end()) menu->setActiveAction(*it); } } if (obj == mMenu) { if (event->type() == QEvent::Resize) { QResizeEvent * e = dynamic_cast(event); if (e->oldSize().isValid() && e->oldSize() != e->size()) { mMenu->move(calculatePopupWindowPos(e->size()).topLeft()); } } else if (event->type() == QEvent::KeyPress) { QKeyEvent * e = dynamic_cast(event); if (Qt::Key_Escape == e->key()) { if (!mSearchEdit->text().isEmpty()) { mSearchEdit->setText(QString{}); //filter out this to not close the menu return true; } } } else if (QEvent::ActionChanged == event->type() || QEvent::ActionAdded == event->type()) { //filter this if we are performing heavy changes to reduce flicker if (mHeavyMenuChanges) return true; } } } return false; } #undef DEFAULT_SHORTCUT