diff --git a/CMakeLists.txt b/CMakeLists.txt index 2d4a0abd2..c57b0cec3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -124,6 +124,14 @@ if(DESKTOPSWITCH_PLUGIN) add_subdirectory(plugin-desktopswitch) endif() +setByDefault(FANCYMENU_PLUGIN Yes) +if(FANCYMENU_PLUGIN) + list(APPEND STATIC_PLUGINS "fancymenu") + add_definitions(-DWITH_FANCYMENU_PLUGIN) + list(APPEND ENABLED_PLUGINS "Application fancy menu") + add_subdirectory(plugin-fancymenu) +endif() + setByDefault(KBINDICATOR_PLUGIN Yes) if(KBINDICATOR_PLUGIN) list(APPEND ENABLED_PLUGINS "Keyboard Indicator") diff --git a/panel/plugin.cpp b/panel/plugin.cpp index 271ac0dc4..d5c8c992e 100644 --- a/panel/plugin.cpp +++ b/panel/plugin.cpp @@ -57,6 +57,10 @@ #include "../plugin-desktopswitch/desktopswitch.h" // desktopswitch extern void * loadPluginTranslation_desktopswitch_helper; #endif +#if defined(WITH_FANCYMENU_PLUGIN) +#include "../plugin-fancymenu/lxqtfancymenu.h" // fancymenu +extern void * loadPluginTranslation_fancymenu_helper; +#endif #if defined(WITH_MAINMENU_PLUGIN) #include "../plugin-mainmenu/lxqtmainmenu.h" // mainmenu extern void * loadPluginTranslation_mainmenu_helper; @@ -218,6 +222,9 @@ namespace #if defined(WITH_DESKTOPSWITCH_PLUGIN) std::make_tuple(QLatin1String("desktopswitch"), plugin_ptr_t{new DesktopSwitchPluginLibrary}, loadPluginTranslation_desktopswitch_helper),// desktopswitch #endif +#if defined(WITH_FANCYMENU_PLUGIN) + std::make_tuple(QLatin1String("fancymenu"), plugin_ptr_t{new LXQtFancyMenuPluginLibrary}, loadPluginTranslation_fancymenu_helper),// fancymenu +#endif #if defined(WITH_MAINMENU_PLUGIN) std::make_tuple(QLatin1String("mainmenu"), plugin_ptr_t{new LXQtMainMenuPluginLibrary}, loadPluginTranslation_mainmenu_helper),// mainmenu #endif diff --git a/plugin-fancymenu/CMakeLists.txt b/plugin-fancymenu/CMakeLists.txt new file mode 100644 index 000000000..dc4a46af8 --- /dev/null +++ b/plugin-fancymenu/CMakeLists.txt @@ -0,0 +1,52 @@ +set(PLUGIN "fancymenu") + +set(HEADERS + lxqtfancymenu.h + lxqtfancymenuconfiguration.h + lxqtfancymenuwindow.h + lxqtfancymenuappmap.h + lxqtfancymenuappmodel.h + lxqtfancymenucategoriesmodel.h + lxqtfancymenutypes.h +) + +set(SOURCES + lxqtfancymenu.cpp + lxqtfancymenuconfiguration.cpp + lxqtfancymenuwindow.cpp + lxqtfancymenuappmap.cpp + lxqtfancymenuappmodel.cpp + lxqtfancymenucategoriesmodel.cpp +) + +set(UIS + lxqtfancymenuconfiguration.ui +) + + +# optionally use libmenu-cache to generate the application menu +if(USE_MENU_CACHE) + find_package(MenuCache "0.3.3") +endif() + +set(LIBRARIES + lxqt + lxqt-globalkeys + lxqt-globalkeys-ui +) + +if(MENUCACHE_FOUND) + list(APPEND SOURCES xdgcachedmenu.cpp) + list(APPEND MOCS xdgcachedmenu.h) + + include_directories(${MENUCACHE_INCLUDE_DIRS}) + list(APPEND LIBRARIES ${MENUCACHE_LIBRARIES}) + add_definitions(-DHAVE_MENU_CACHE=1) + +endif() + + +set(QT_USE_QTXML 1) +set(QT_USE_QTDBUS 1) + +BUILD_LXQT_PLUGIN(${PLUGIN}) diff --git a/plugin-fancymenu/lxqtfancymenu.cpp b/plugin-fancymenu/lxqtfancymenu.cpp new file mode 100644 index 000000000..8627a3778 --- /dev/null +++ b/plugin-fancymenu/lxqtfancymenu.cpp @@ -0,0 +1,384 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * 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 "lxqtfancymenu.h" +#include "lxqtfancymenuconfiguration.h" +#include "lxqtfancymenuwindow.h" +#include "../panel/lxqtpanel.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include + +#define DEFAULT_SHORTCUT "Alt+F1" + +LXQtFancyMenu::LXQtFancyMenu(const ILXQtPanelPluginStartupInfo &startupInfo): + QObject(), + ILXQtPanelPlugin(startupInfo), + mWindow(nullptr), + mShortcut(nullptr), + mFilterClear(false) +{ + mWindow = new LXQtFancyMenuWindow(&mButton); + mWindow->setObjectName(QStringLiteral("TopLevelFancyMenu")); + mWindow->installEventFilter(this); + connect(mWindow, &LXQtFancyMenuWindow::aboutToHide, &mHideTimer, QOverload<>::of(&QTimer::start)); + connect(mWindow, &LXQtFancyMenuWindow::aboutToShow, &mHideTimer, &QTimer::stop); + connect(mWindow, &LXQtFancyMenuWindow::favoritesChanged, this, &LXQtFancyMenu::saveFavorites); + + mDelayedPopup.setSingleShot(true); + mDelayedPopup.setInterval(200); + connect(&mDelayedPopup, &QTimer::timeout, this, &LXQtFancyMenu::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, mButton.parentWidget(), [this] { + Q_ASSERT(mButton.parentWidget()); + mButton.parentWidget()->installEventFilter(this); + }); + + connect(&mButton, &QToolButton::clicked, this, &LXQtFancyMenu::showHideMenu); + + QTimer::singleShot(0, this, [this] { + settingsChanged(); + }); + + mShortcut = GlobalKeyShortcut::Client::instance()->addAction(QString{}, QStringLiteral("/panel/%1/show_hide").arg(settings()->group()), LXQtFancyMenu::tr("Show/hide main menu"), this); + if (mShortcut) + { + connect(mShortcut, &GlobalKeyShortcut::Action::shortcutChanged, this, [this](const QString &, const QString & shortcut) { + mShortcutSeq = shortcut; + }); + connect(mShortcut, &GlobalKeyShortcut::Action::registrationFinished, this, [this] { + if (mShortcut->shortcut().isEmpty()) + mShortcut->changeShortcut(QStringLiteral(DEFAULT_SHORTCUT)); + else + mShortcutSeq = mShortcut->shortcut(); + }); + connect(mShortcut, &GlobalKeyShortcut::Action::activated, this, [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(); + }); + } +} + + +/************************************************ + + ************************************************/ +LXQtFancyMenu::~LXQtFancyMenu() +{ + mButton.parentWidget()->removeEventFilter(this); + + delete mWindow; +} + + +/************************************************ + + ************************************************/ +void LXQtFancyMenu::showHideMenu() +{ + if(mWindow && mWindow->isVisible()) + mWindow->hide(); + else + showMenu(); +} + +/************************************************ + + ************************************************/ +void LXQtFancyMenu::showMenu() +{ + if (!mWindow) + return; + + willShowWindow(mWindow); + // Just using Qt`s activateWindow() won't work on some WMs like Kwin. + // Solution is to execute menu 1ms later using timer + mWindow->move(calculatePopupWindowPos(mWindow->sizeHint()).topLeft()); + + emit mWindow->aboutToShow(); + mWindow->show(); + mWindow->setSearchEditFocus(); +} + +/************************************************ + + ************************************************/ +void LXQtFancyMenu::settingsChanged() +{ + setButtonIcon(); + if (settings()->value(QStringLiteral("showText"), false).toBool()) + { + mButton.setText(settings()->value(QStringLiteral("text"), QStringLiteral("Start")).toString()); + mButton.setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + } + else + { + mButton.setText(QLatin1String("")); + mButton.setToolButtonStyle(Qt::ToolButtonIconOnly); + } + + mLogDir = settings()->value(QStringLiteral("log_dir"), QString()).toString(); + + QString menu_file = settings()->value(QStringLiteral("menu_file"), QString()).toString(); + if (menu_file.isEmpty()) + menu_file = XdgMenu::getMenuFileName(); + + if (mMenuFile != menu_file) + { + mMenuFile = menu_file; + mXdgMenu.setEnvironments(QStringList() << QStringLiteral("X-LXQT") << QStringLiteral("LXQt")); + mXdgMenu.setLogDir(mLogDir); + + bool res = mXdgMenu.read(mMenuFile); + connect(&mXdgMenu, &XdgMenu::changed, this, &LXQtFancyMenu::buildMenu); + if (res) + { + QTimer::singleShot(1000, this, &LXQtFancyMenu::buildMenu); + } + else + { + QMessageBox::warning(nullptr, QStringLiteral("Parse error"), mXdgMenu.errorString()); + return; + } + } + + loadFavorites(); + setMenuFontSize(); + + //clear the search to not leaving the menu in wrong state + mFilterClear = settings()->value(QStringLiteral("filterClear"), false).toBool(); + mWindow->setFilterClear(mFilterClear); + + bool buttonsAtTop = settings()->value(QStringLiteral("buttonsAtTop"), false).toBool(); + mWindow->setButtonPosition(buttonsAtTop ? LXQtFancyMenuButtonPosition::Top : LXQtFancyMenuButtonPosition::Bottom); + + bool categoriesAtRight = settings()->value(QStringLiteral("categoriesAtRight"), true).toBool(); + mWindow->setCategoryPosition(categoriesAtRight ? LXQtFancyMenuCategoryPosition::Right : LXQtFancyMenuCategoryPosition::Left); + + realign(); +} + +/************************************************ + + ************************************************/ +void LXQtFancyMenu::buildMenu() +{ + mWindow->rebuildMenu(mXdgMenu); + + mWindow->doSearch(); + setMenuFontSize(); +} + +void LXQtFancyMenu::loadFavorites() +{ + bool listChanged = false; + + const QList > list = settings()->readArray(QStringLiteral("favorites")); + QStringList fileList; + for(const QMap& item : list) + { + QString file = item.value(QStringLiteral("desktopFile")).toString(); + if(file.isEmpty()) + { + listChanged = true; + continue; + } + + QString canonicalPath = QDir(file).canonicalPath(); + if(canonicalPath != file) + listChanged = true; + + if(canonicalPath.isEmpty()) + continue; + + if(fileList.contains(canonicalPath)) + { + // Don't add duplicates + listChanged = true; + continue; + } + + fileList.append(canonicalPath); + } + + mWindow->setFavorites(fileList); + + if(listChanged) + saveFavorites(); +} + +void LXQtFancyMenu::saveFavorites() +{ + const QStringList fileList = mWindow->favorites(); + + QList > list; + for(const QString& file : fileList) + { + QMap item; + item.insert(QStringLiteral("desktopFile"), file); + list.append(item); + } + + settings()->setArray(QStringLiteral("favorites"), list); +} + +/************************************************ + + ************************************************/ +void LXQtFancyMenu::setMenuFontSize() +{ + if (!mWindow) + return; + + QFont menuFont = mButton.font(); + bool customFont = settings()->value(QStringLiteral("customFont"), false).toBool(); + int customFontSize = settings()->value(QStringLiteral("customFontSize")).toInt(); + + if(customFont) + { + menuFont = mWindow->font(); + menuFont.setPointSize(customFontSize); + } + + mWindow->setCustomFont(menuFont); +} + +/************************************************ + + ************************************************/ +void LXQtFancyMenu::setButtonIcon() +{ + if (settings()->value(QStringLiteral("ownIcon"), false).toBool()) + { + mButton.setStyleSheet(QStringLiteral("#FancyMenu { qproperty-icon: url(%1); }") + .arg(settings()->value(QLatin1String("icon"), QLatin1String(LXQT_GRAPHICS_DIR"/helix.svg")).toString())); + } else + { + mButton.setStyleSheet(QString()); + } +} + +/************************************************ + + ************************************************/ +QDialog *LXQtFancyMenu::configureDialog() +{ + return new LXQtFancyMenuConfiguration(settings(), mShortcut, QStringLiteral(DEFAULT_SHORTCUT)); +} + +/************************************************ + + ************************************************/ +bool LXQtFancyMenu::eventFilter(QObject *obj, QEvent *event) +{ + if(obj == mButton.parentWidget()) + { + // the application is given a new QStyle + if(event->type() == QEvent::StyleChange) + { + setMenuFontSize(); + setButtonIcon(); + mWindow->updateButtonIconSize(); + } + } + else if(obj == mWindow) + { + if(event->type() == QEvent::KeyRelease) + { + static const auto key_meta = QMetaEnum::fromType(); + // if our shortcut key is pressed while the menu is open, close the menu + QKeyEvent* keyEvent = static_cast(event); + QFlags mod = keyEvent->modifiers(); + switch (keyEvent->key()) + { + case Qt::Key_Alt: + mod &= ~Qt::AltModifier; + break; + case Qt::Key_Control: + mod &= ~Qt::ControlModifier; + break; + case Qt::Key_Shift: + mod &= ~Qt::ShiftModifier; + break; + case Qt::Key_Super_L: + case Qt::Key_Super_R: + mod &= ~Qt::MetaModifier; + break; + } + const QString press = QKeySequence{static_cast(mod)}.toString() % QString::fromLatin1(key_meta.valueToKey(keyEvent->key())).remove(0, 4); + if (press == mShortcutSeq) + { + //TODO: isn't timer already fired by hide() ??? + mHideTimer.start(); + mWindow->hide(); // close the app menu + return true; + } + //TODO: go to item which starts with pressed letter + } + else if (event->type() == QEvent::Resize) + { + QResizeEvent *e = static_cast(event); + if (e->oldSize().isValid() && e->oldSize() != e->size()) + { + mWindow->move(calculatePopupWindowPos(e->size()).topLeft()); + } + } + } + return false; +} + +#undef DEFAULT_SHORTCUT diff --git a/plugin-fancymenu/lxqtfancymenu.h b/plugin-fancymenu/lxqtfancymenu.h new file mode 100644 index 000000000..d798f3226 --- /dev/null +++ b/plugin-fancymenu/lxqtfancymenu.h @@ -0,0 +1,113 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * 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 */ + + +#ifndef LXQT_FANCYMENU_H +#define LXQT_FANCYMENU_H + +#include "../panel/ilxqtpanelplugin.h" +#include + +#include +#include +#include +#include +#include +#include + +class LXQtFancyMenuWindow; +class LXQtBar; + +namespace LXQt { +class PowerManager; +class ScreenSaver; +} + +namespace GlobalKeyShortcut +{ +class Action; +} + +class LXQtFancyMenu : public QObject, public ILXQtPanelPlugin +{ + Q_OBJECT +public: + LXQtFancyMenu(const ILXQtPanelPluginStartupInfo &startupInfo); + ~LXQtFancyMenu(); + + QString themeId() const { return QStringLiteral("FancyMenu"); } + virtual ILXQtPanelPlugin::Flags flags() const { return HaveConfigDialog ; } + + QWidget *widget() { return &mButton; } + QDialog *configureDialog(); + + bool isSeparate() const { return true; } + +protected: + bool eventFilter(QObject *obj, QEvent *event); + +private: + void setMenuFontSize(); + void setButtonIcon(); + +private: + QToolButton mButton; + QString mLogDir; + LXQtFancyMenuWindow *mWindow; + GlobalKeyShortcut::Action *mShortcut; + bool mFilterClear; //!< search field should be cleared upon showing the menu + + XdgMenu mXdgMenu; + + QTimer mDelayedPopup; + QTimer mHideTimer; + QString mShortcutSeq; + QString mMenuFile; + +protected slots: + + virtual void settingsChanged(); + void buildMenu(); + + void loadFavorites(); + void saveFavorites(); + +private slots: + void showMenu(); + void showHideMenu(); +}; + +class LXQtFancyMenuPluginLibrary: public QObject, public ILXQtPanelPluginLibrary +{ + Q_OBJECT + // Q_PLUGIN_METADATA(IID "lxqt.org/Panel/PluginInterface/3.0") + Q_INTERFACES(ILXQtPanelPluginLibrary) +public: + ILXQtPanelPlugin *instance(const ILXQtPanelPluginStartupInfo &startupInfo) const { return new LXQtFancyMenu(startupInfo);} +}; + +#endif diff --git a/plugin-fancymenu/lxqtfancymenuappmap.cpp b/plugin-fancymenu/lxqtfancymenuappmap.cpp new file mode 100644 index 000000000..77dc0939b --- /dev/null +++ b/plugin-fancymenu/lxqtfancymenuappmap.cpp @@ -0,0 +1,351 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * 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 "lxqtfancymenuappmap.h" + +#include +#include + +#include + +class LXQtFancyMenuAppMapStrings +{ + Q_DECLARE_TR_FUNCTIONS(LXQtFancyMenuAppMapStrings) +}; + + +LXQtFancyMenuAppMap::LXQtFancyMenuAppMap() +{ + mCachedIndex = -1; + mCachedIterator = mAppSortedByName.constEnd(); + + //Add Favorites category + Category favorites; + favorites.menuTitle = LXQtFancyMenuAppMapStrings::tr("Favorites"); + favorites.icon = XdgIcon::fromTheme(QLatin1String("bookmarks")); + mCategories.append(favorites); + + //Add All Apps category + Category allAppsCategory; + allAppsCategory.menuTitle = LXQtFancyMenuAppMapStrings::tr("All Applications"); + allAppsCategory.icon = XdgIcon::fromTheme(QLatin1String("folder")); + mCategories.append(allAppsCategory); + + //Add separator + Category sepatorCategory; + sepatorCategory.type = LXQtFancyMenuItemType::SeparatorItem; + mCategories.append(sepatorCategory); +} + +LXQtFancyMenuAppMap::~LXQtFancyMenuAppMap() +{ + clear(); + clearFavorites(); +} + +void LXQtFancyMenuAppMap::clear() +{ + // Keep Favorites, All Applications and separator + mCategories.erase(mCategories.begin() + 3, mCategories.end()); + + mAppSortedByName.clear(); + qDeleteAll(mAppSortedByDesktopFile); + mAppSortedByDesktopFile.clear(); + + mCachedIndex = -1; + mCachedIterator = mAppSortedByName.constEnd(); +} + +void LXQtFancyMenuAppMap::clearFavorites() +{ + Category& favoritesCatRef = mCategories[0]; + for(Category::Item& item : favoritesCatRef.apps) + { + if(item.appItem) + { + delete item.appItem; + item.appItem = nullptr; + } + } + favoritesCatRef.apps.clear(); +} + +bool LXQtFancyMenuAppMap::rebuildModel(const XdgMenu &menu) +{ + clear(); + + QDomElement rootMenu = menu.xml().documentElement(); + parseMenu(rootMenu, QString()); + + mCategories.squeeze(); + + return true; +} + +void LXQtFancyMenuAppMap::setFavorites(const QStringList &favorites) +{ + clearFavorites(); + + Category& favoritesCatRef = mCategories[0]; + + for(const QString& desktopFile : favorites) + { + Category::Item item; + item.type = LXQtFancyMenuItemType::AppItem; + item.appItem = loadAppItem(desktopFile); + if(!item.appItem) + continue; + favoritesCatRef.apps.append(item); + } +} + +bool LXQtFancyMenuAppMap::isFavorite(const QString &desktopFile) const +{ + const Category& favoritesCat = mCategories.at(0); + for(const Category::Item& item : favoritesCat.apps) + { + if(item.appItem && item.appItem->desktopFile == desktopFile) + return true; + } + + return false; +} + +void LXQtFancyMenuAppMap::addToFavorites(const QString &desktopFile) +{ + if(isFavorite(desktopFile)) + return; + + Category::Item item; + item.type = LXQtFancyMenuItemType::AppItem; + item.appItem = loadAppItem(desktopFile); + if(!item.appItem) + return; + + Category& favoritesCatRef = mCategories[0]; + favoritesCatRef.apps.append(item); +} + +void LXQtFancyMenuAppMap::removeFromFavorites(const QString &desktopFile) +{ + if(!isFavorite(desktopFile)) + return; + + Category& favoritesCatRef = mCategories[0]; + for(auto it = favoritesCatRef.apps.begin(); it != favoritesCatRef.apps.end(); it++) + { + AppItem *appItem = (*it).appItem; + if(appItem && appItem->desktopFile == desktopFile) + { + favoritesCatRef.apps.erase(it); + delete appItem; + return; + } + } +} + +LXQtFancyMenuAppMap::AppItem *LXQtFancyMenuAppMap::getAppAt(int index) +{ + if(index < 0 || index >= getTotalAppCount()) + return nullptr; + + if(mCachedIndex != -1) + { + if(index == mCachedIndex + 1) + { + //Fast case, go to next row + mCachedIndex++; + mCachedIterator++; + } + + if(index == mCachedIndex) + return *mCachedIterator; + + int dist1 = qAbs(mCachedIndex - index); + if(dist1 < index) + { + std::advance(mCachedIterator, index - mCachedIndex); + mCachedIndex = index; + return *mCachedIterator; + } + } + + // Recalculate cached iterator + mCachedIterator = mAppSortedByName.constBegin(); + std::advance(mCachedIterator, index); + mCachedIndex = index; + return *mCachedIterator; +} + +QVector LXQtFancyMenuAppMap::getMatchingApps(const QString &query) const +{ + QVector byName; + QVector byKeyword; + + //TODO: implement some kind of score to get better matches on top + + for(const AppItem *app : qAsConst(mAppSortedByName)) + { + if(app->title.contains(query)) + { + byName.append(app); + continue; + } + + if(app->comment.contains(query)) + { + byKeyword.append(app); + continue; + } + + for(const QString& key : app->keywords) + { + if(key.startsWith(query)) + { + byKeyword.append(app); + break; + } + } + } + + // Give priority to title matches + byName += byKeyword; + + return byName; +} + +void LXQtFancyMenuAppMap::parseMenu(const QDomElement &menu, const QString& topLevelCategory) +{ + QDomElement e = menu.firstChildElement(); + while(!e.isNull()) + { + if(e.tagName() == QLatin1String("Menu")) + { + if(topLevelCategory.isEmpty()) + { + //This is a top level menu + Category item; + item.type = LXQtFancyMenuItemType::CategoryItem; + item.menuName = e.attribute(QLatin1String("name")); + item.menuTitle = e.attribute(QLatin1Literal("title"), item.menuName); + QString iconName = e.attribute(QLatin1String("icon")); + item.icon = XdgIcon::fromTheme(iconName); + mCategories.append(item); + + //Merge sub menu to parent + parseMenu(e, item.menuName); + } + else + { + //Merge sub menu to parent + parseMenu(e, topLevelCategory); + } + } + else if(!topLevelCategory.isEmpty()) + { + if(e.tagName() == QLatin1String("AppLink")) + parseAppLink(e, topLevelCategory); + else if(e.tagName() == QLatin1String("Separator")) + parseSeparator(e, topLevelCategory); + } + + e = e.nextSiblingElement(); + } +} + +void LXQtFancyMenuAppMap::parseAppLink(const QDomElement &app, const QString& topLevelCategory) +{ + QString desktopFile = app.attribute(QLatin1String("desktopFile")); + + // Check if already added + AppItem *appItem = mAppSortedByDesktopFile.value(desktopFile, nullptr); + if(!appItem) + { + // Add new app + appItem = loadAppItem(desktopFile); + if(!appItem) + return; // Invalid app + + mAppSortedByDesktopFile.insert(appItem->desktopFile, appItem); + mAppSortedByName.insert(appItem->title, appItem); + } + + // Now add app to category + for(Category &category : mCategories) + { + if(category.menuName == topLevelCategory) + { + Category::Item item; + item.appItem = appItem; + item.type = LXQtFancyMenuItemType::AppItem; + category.apps.append(item); + break; + } + } +} + +void LXQtFancyMenuAppMap::parseSeparator(const QDomElement &sep, const QString& topLevelCategory) +{ + Q_UNUSED(sep) + + // Find category + for(Category &category : mCategories) + { + if(category.menuName != topLevelCategory) + continue; + + // XdgMenu already cares of removing consecutive separators + // Or separators put as first or last items + Category::Item item; + item.type = LXQtFancyMenuItemType::SeparatorItem; + category.apps.append(item); + + break; + } +} + +LXQtFancyMenuAppMap::AppItem *LXQtFancyMenuAppMap::loadAppItem(const QString &desktopFile) +{ + XdgDesktopFile f; + if(!f.load(desktopFile)) + return nullptr; // Invalid App + + AppItem *item = new AppItem; + item->desktopFile = desktopFile; + item->title = f.name(); + item->comment = f.comment(); + if(item->comment.isEmpty()) + item->comment = f.localizedValue(QLatin1String("GenericName")).toString(); + item->icon = f.icon(); + item->desktopFileCache = f; + + item->keywords << f.localizedValue(QLatin1String("Keywords")).toString().toLower().split(QLatin1Char(';')); + item->keywords.append(item->title.toLower().split(QLatin1Char(' '))); + item->keywords.append(item->comment.toLower().split(QLatin1Char(' '))); + return item; +} diff --git a/plugin-fancymenu/lxqtfancymenuappmap.h b/plugin-fancymenu/lxqtfancymenuappmap.h new file mode 100644 index 000000000..aeb8ae534 --- /dev/null +++ b/plugin-fancymenu/lxqtfancymenuappmap.h @@ -0,0 +1,120 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * 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 */ + + +#ifndef LXQTFANCYMENUAPPMAP_H +#define LXQTFANCYMENUAPPMAP_H + +#include +#include +#include +#include + +#include + +#include "lxqtfancymenutypes.h" + +class XdgMenu; +class QDomElement; + +struct LXQtFancyMenuAppItem +{ + QString desktopFile; + QString title; + QString comment; + QStringList keywords; + QIcon icon; + XdgDesktopFile desktopFileCache; +}; + +class LXQtFancyMenuAppMap +{ +public: + enum SpecialCategory + { + FavoritesCategory = 0, + AllAppsCategory = 1 + }; + + typedef LXQtFancyMenuAppItem AppItem; + + struct Category + { + QString menuName; + QString menuTitle; + QIcon icon; + + struct Item + { + AppItem *appItem = nullptr; + LXQtFancyMenuItemType type = LXQtFancyMenuItemType::AppItem; + }; + + QVector apps; + LXQtFancyMenuItemType type; + }; + + LXQtFancyMenuAppMap(); + ~LXQtFancyMenuAppMap(); + + void clear(); + void clearFavorites(); + bool rebuildModel(const XdgMenu &menu); + + void setFavorites(const QStringList& favorites); + bool isFavorite(const QString& desktopFile) const; + void addToFavorites(const QString& desktopFile); + void removeFromFavorites(const QString& desktopFile); + + inline int getCategoriesCount() const { return mCategories.size(); } + inline const Category& getCategoryAt(int index) { return mCategories.at(index); } + + inline int getTotalAppCount() const { return mAppSortedByName.size(); } + + AppItem *getAppAt(int index); + + QVector getMatchingApps(const QString& query) const; + +private: + void parseMenu(const QDomElement& menu, const QString &topLevelCategory); + void parseAppLink(const QDomElement& app, const QString &topLevelCategory); + void parseSeparator(const QDomElement &sep, const QString &topLevelCategory); + + AppItem *loadAppItem(const QString& desktopFile); + +private: + typedef QMap AppMap; + AppMap mAppSortedByDesktopFile; + AppMap mAppSortedByName; + QVector mCategories; + + // Cache sort by name map access + AppMap::const_iterator mCachedIterator; + int mCachedIndex; +}; + +#endif // LXQTFANCYMENUAPPMAP_H diff --git a/plugin-fancymenu/lxqtfancymenuappmodel.cpp b/plugin-fancymenu/lxqtfancymenuappmodel.cpp new file mode 100644 index 000000000..dd6385975 --- /dev/null +++ b/plugin-fancymenu/lxqtfancymenuappmodel.cpp @@ -0,0 +1,205 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * 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 "lxqtfancymenuappmodel.h" +#include "lxqtfancymenuappmap.h" + +#include +#include + +LXQtFancyMenuAppModel::LXQtFancyMenuAppModel(QObject *parent) + : QAbstractListModel(parent) + , mCurrentCategory(0) + , mInSearch(false) +{ +} + +int LXQtFancyMenuAppModel::rowCount(const QModelIndex &p) const +{ + if(!mAppMap || p.isValid() || mCurrentCategory < 0 || mCurrentCategory >= mAppMap->getCategoriesCount()) + return 0; + + if(mInSearch) + return mSearchMatches.size(); + + if(mCurrentCategory == LXQtFancyMenuAppMap::AllAppsCategory) + return mAppMap->getTotalAppCount(); //Special "All Applications" category + + return mAppMap->getCategoryAt(mCurrentCategory).apps.size(); +} + +QVariant LXQtFancyMenuAppModel::data(const QModelIndex &idx, int role) const +{ + if(!idx.isValid()) + return QVariant(); + + const LXQtFancyMenuAppMap::AppItem* item = getAppAt(idx.row()); + LXQtFancyMenuItemType type = getItemTypeAt(idx.row()); + if(!item && type == LXQtFancyMenuItemType::AppItem) + return QVariant(); + + if(!item) + { + if(role == LXQtFancyMenuItemIsSeparatorRole) + return 1; + return QVariant(); + } + + switch (role) + { + case Qt::DisplayRole: + return item->title; + case Qt::EditRole: + return item->desktopFile; + case Qt::DecorationRole: + return item->icon; + case Qt::ToolTipRole: + { + return item->comment; + } + default: + break; + } + + return QVariant(); +} + +Qt::ItemFlags LXQtFancyMenuAppModel::flags(const QModelIndex &idx) const +{ + const LXQtFancyMenuAppMap::AppItem* item = getAppAt(idx.row()); + LXQtFancyMenuItemType type = getItemTypeAt(idx.row()); + if(!item || type == LXQtFancyMenuItemType::SeparatorItem) + return Qt::NoItemFlags; + + Qt::ItemFlags f = QAbstractListModel::flags(idx); + if (idx.isValid()) + f |= Qt::ItemIsDragEnabled; + return f; +} + +QMimeData *LXQtFancyMenuAppModel::mimeData(const QModelIndexList &indexes) const +{ + QList urls; + + for(const QModelIndex& idx : indexes) + { + const LXQtFancyMenuAppMap::AppItem* item = getAppAt(idx.row()); + if(!item) + continue; + urls << QUrl::fromLocalFile(item->desktopFile); + } + + QMimeData *mimeData = new QMimeData(); + mimeData->setUrls(urls); + return mimeData; +} + +Qt::DropActions LXQtFancyMenuAppModel::supportedDragActions() const +{ + return Qt::CopyAction | Qt::LinkAction; +} + +void LXQtFancyMenuAppModel::reloadAppMap(bool end) +{ + if(!end) + beginResetModel(); + else + endResetModel(); +} + +void LXQtFancyMenuAppModel::setCurrentCategory(int category) +{ + beginResetModel(); + mCurrentCategory = category; + endResetModel(); +} + +void LXQtFancyMenuAppModel::showSearchResults(const QVector &matches) +{ + beginResetModel(); + mSearchMatches = matches; + mInSearch = true; + endResetModel(); +} + +void LXQtFancyMenuAppModel::endSearch() +{ + beginResetModel(); + mSearchMatches.clear(); + mSearchMatches.squeeze(); + mInSearch = false; + endResetModel(); +} + +LXQtFancyMenuAppMap *LXQtFancyMenuAppModel::appMap() const +{ + return mAppMap; +} + +void LXQtFancyMenuAppModel::setAppMap(LXQtFancyMenuAppMap *newAppMap) +{ + mAppMap = newAppMap; +} + +const LXQtFancyMenuAppItem *LXQtFancyMenuAppModel::getAppAt(int idx) const +{ + if(!mAppMap || idx < 0 || mCurrentCategory < 0 || mCurrentCategory >= mAppMap->getCategoriesCount()) + return nullptr; + + if(mInSearch) + return mSearchMatches.value(idx, nullptr); + + if(mCurrentCategory == LXQtFancyMenuAppMap::AllAppsCategory) + return mAppMap->getAppAt(idx); //Special "All Applications" category + + const LXQtFancyMenuAppMap::Category& cat = mAppMap->getCategoryAt(mCurrentCategory); + if(idx >= cat.apps.size()) + return nullptr; + + const LXQtFancyMenuAppMap::Category::Item& item = cat.apps.at(idx); + return item.appItem; +} + +LXQtFancyMenuItemType LXQtFancyMenuAppModel::getItemTypeAt(int idx) const +{ + if(!mAppMap || idx < 0 || mCurrentCategory < 0 || mCurrentCategory >= mAppMap->getCategoriesCount()) + return LXQtFancyMenuItemType::AppItem; + + if(mInSearch) + return LXQtFancyMenuItemType::AppItem; + + if(mCurrentCategory == LXQtFancyMenuAppMap::AllAppsCategory) + return LXQtFancyMenuItemType::AppItem; //Special "All Applications" category + + const LXQtFancyMenuAppMap::Category& cat = mAppMap->getCategoryAt(mCurrentCategory); + if(idx >= cat.apps.size()) + return LXQtFancyMenuItemType::AppItem; + + const LXQtFancyMenuAppMap::Category::Item& item = cat.apps.at(idx); + return item.type; +} diff --git a/plugin-fancymenu/lxqtfancymenuappmodel.h b/plugin-fancymenu/lxqtfancymenuappmodel.h new file mode 100644 index 000000000..ad1fdde35 --- /dev/null +++ b/plugin-fancymenu/lxqtfancymenuappmodel.h @@ -0,0 +1,75 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * 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 */ + + +#ifndef LXQTFANCYMENUAPPMODEL_H +#define LXQTFANCYMENUAPPMODEL_H + +#include + +#include "lxqtfancymenutypes.h" + +class LXQtFancyMenuAppMap; +class LXQtFancyMenuAppItem; + +class LXQtFancyMenuAppModel : public QAbstractListModel +{ + Q_OBJECT + +public: + explicit LXQtFancyMenuAppModel(QObject *parent = nullptr); + + // Basic functionality: + int rowCount(const QModelIndex &p = QModelIndex()) const override; + + QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override; + + // Drag support + Qt::ItemFlags flags(const QModelIndex &idx) const override; + virtual QMimeData *mimeData(const QModelIndexList &indexes) const; + virtual Qt::DropActions supportedDragActions() const; + + void reloadAppMap(bool end); + void setCurrentCategory(int category); + void showSearchResults(const QVector &matches); + void endSearch(); + + LXQtFancyMenuAppMap *appMap() const; + void setAppMap(LXQtFancyMenuAppMap *newAppMap); + + const LXQtFancyMenuAppItem *getAppAt(int idx) const; + LXQtFancyMenuItemType getItemTypeAt(int idx) const; + +private: + LXQtFancyMenuAppMap *mAppMap; + int mCurrentCategory; + + QVector mSearchMatches; + bool mInSearch; +}; + +#endif // LXQTFANCYMENUAPPMODEL_H diff --git a/plugin-fancymenu/lxqtfancymenucategoriesmodel.cpp b/plugin-fancymenu/lxqtfancymenucategoriesmodel.cpp new file mode 100644 index 000000000..acf899bab --- /dev/null +++ b/plugin-fancymenu/lxqtfancymenucategoriesmodel.cpp @@ -0,0 +1,100 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * 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 "lxqtfancymenucategoriesmodel.h" +#include "lxqtfancymenuappmap.h" + +LXQtFancyMenuCategoriesModel::LXQtFancyMenuCategoriesModel(QObject *parent) + : QAbstractListModel(parent) + , mAppMap(nullptr) +{ +} + +int LXQtFancyMenuCategoriesModel::rowCount(const QModelIndex &p) const +{ + if(!mAppMap || p.isValid()) + return 0; + + return mAppMap->getCategoriesCount(); +} + +QVariant LXQtFancyMenuCategoriesModel::data(const QModelIndex &idx, int role) const +{ + if (!mAppMap || !idx.isValid() || idx.row() >= mAppMap->getCategoriesCount()) + return QVariant(); + + const LXQtFancyMenuAppMap::Category& item = mAppMap->getCategoryAt(idx.row()); + + switch (role) + { + case Qt::DisplayRole: + case Qt::ToolTipRole: + return item.menuTitle; + case Qt::EditRole: + return item.menuName; + case Qt::DecorationRole: + return item.icon; + case LXQtFancyMenuItemIsSeparatorRole: + if(item.type == LXQtFancyMenuItemType::SeparatorItem) + return 1; + default: + break; + } + + return QVariant(); +} + +Qt::ItemFlags LXQtFancyMenuCategoriesModel::flags(const QModelIndex &idx) const +{ + if (!mAppMap || !idx.isValid() || idx.row() >= mAppMap->getCategoriesCount()) + return Qt::NoItemFlags; + + const LXQtFancyMenuAppMap::Category& item = mAppMap->getCategoryAt(idx.row()); + if(item.type == LXQtFancyMenuItemType::SeparatorItem) + return Qt::NoItemFlags; + + return QAbstractListModel::flags(idx); +} + +void LXQtFancyMenuCategoriesModel::reloadAppMap(bool end) +{ + if(!end) + beginResetModel(); + else + endResetModel(); +} + +LXQtFancyMenuAppMap *LXQtFancyMenuCategoriesModel::appMap() const +{ + return mAppMap; +} + +void LXQtFancyMenuCategoriesModel::setAppMap(LXQtFancyMenuAppMap *newAppMap) +{ + mAppMap = newAppMap; +} diff --git a/plugin-fancymenu/lxqtfancymenucategoriesmodel.h b/plugin-fancymenu/lxqtfancymenucategoriesmodel.h new file mode 100644 index 000000000..36f1c8029 --- /dev/null +++ b/plugin-fancymenu/lxqtfancymenucategoriesmodel.h @@ -0,0 +1,60 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * 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 */ + + +#ifndef LXQTFANCYMENUCATEGORIESMODEL_H +#define LXQTFANCYMENUCATEGORIESMODEL_H + +#include + +class LXQtFancyMenuAppMap; + +class LXQtFancyMenuCategoriesModel : public QAbstractListModel +{ + Q_OBJECT + +public: + explicit LXQtFancyMenuCategoriesModel(QObject *parent = nullptr); + + // Basic functionality: + int rowCount(const QModelIndex &p = QModelIndex()) const override; + + QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override; + + // Separator support: + Qt::ItemFlags flags(const QModelIndex &idx) const override; + + void reloadAppMap(bool end); + + LXQtFancyMenuAppMap *appMap() const; + void setAppMap(LXQtFancyMenuAppMap *newAppMap); + +private: + LXQtFancyMenuAppMap *mAppMap; +}; + +#endif // LXQTFANCYMENUCATEGORIESMODEL_H diff --git a/plugin-fancymenu/lxqtfancymenuconfiguration.cpp b/plugin-fancymenu/lxqtfancymenuconfiguration.cpp new file mode 100644 index 000000000..b8cb26f4c --- /dev/null +++ b/plugin-fancymenu/lxqtfancymenuconfiguration.cpp @@ -0,0 +1,238 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * 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 "lxqtfancymenuconfiguration.h" +#include "ui_lxqtfancymenuconfiguration.h" +#include +#include +#include +#include + +#include +#include + +#include "lxqtfancymenutypes.h" + +LXQtFancyMenuConfiguration::LXQtFancyMenuConfiguration(PluginSettings *settings, GlobalKeyShortcut::Action * shortcut, const QString &defaultShortcut, QWidget *parent) : + LXQtPanelPluginConfigDialog(settings, parent), + ui(new Ui::LXQtFancyMenuConfiguration), + mDefaultShortcut(defaultShortcut), + mShortcut(shortcut), + mLockSettingChanges(false) +{ + setAttribute(Qt::WA_DeleteOnClose); + setObjectName(QStringLiteral("FancyMenuConfigurationWindow")); + ui->setupUi(this); + + fillButtonPositionComboBox(); + fillCategoryPositionComboBox(); + + QIcon folder{XdgIcon::fromTheme(QStringLiteral("folder"))}; + ui->chooseMenuFilePB->setIcon(folder); + ui->iconPB->setIcon(folder); + + connect(ui->buttons, &QDialogButtonBox::clicked, this, &LXQtFancyMenuConfiguration::dialogButtonsAction); + + loadSettings(); + + connect(ui->showTextCB, &QAbstractButton::toggled, this, &LXQtFancyMenuConfiguration::showTextChanged); + connect(ui->textLE, &QLineEdit::textEdited, this, &LXQtFancyMenuConfiguration::textButtonChanged); + connect(ui->chooseMenuFilePB, &QAbstractButton::clicked, this, &LXQtFancyMenuConfiguration::chooseMenuFile); + connect(ui->menuFilePathLE, &QLineEdit::textChanged, this, [&] (QString const & file) { + if (!mLockSettingChanges) + this->settings().setValue(QLatin1String("menu_file"), file); + }); + connect(ui->iconCB, &QCheckBox::toggled, this, [this] (bool value) { + if (!mLockSettingChanges) + this->settings().setValue(QStringLiteral("ownIcon"), value); + }); + connect(ui->iconPB, &QAbstractButton::clicked, this, &LXQtFancyMenuConfiguration::chooseIcon); + connect(ui->iconLE, &QLineEdit::textChanged, this, [&] (QString const & path) { + if (!mLockSettingChanges) + this->settings().setValue(QLatin1String("icon"), path); + }); + + connect(ui->shortcutEd, &ShortcutSelector::shortcutGrabbed, this, &LXQtFancyMenuConfiguration::shortcutChanged); + connect(ui->shortcutEd->addMenuAction(tr("Reset")), &QAction::triggered, this, &LXQtFancyMenuConfiguration::shortcutReset); + + connect(ui->customFontCB, &QAbstractButton::toggled, this, &LXQtFancyMenuConfiguration::customFontChanged); + connect(ui->customFontSizeSB, static_cast(&QSpinBox::valueChanged), this, &LXQtFancyMenuConfiguration::customFontSizeChanged); + + connect(mShortcut, &GlobalKeyShortcut::Action::shortcutChanged, this, &LXQtFancyMenuConfiguration::globalShortcutChanged); + + connect(ui->filterClearCB, &QCheckBox::toggled, this, [this] (bool value) { + if (!mLockSettingChanges) + this->settings().setValue(QStringLiteral("filterClear"), value); + }); + + connect(ui->buttRowPosCB, QOverload::of(&QComboBox::activated), this, &LXQtFancyMenuConfiguration::buttonRowPositionChanged); + connect(ui->categoryViewPosCB, QOverload::of(&QComboBox::activated), this, &LXQtFancyMenuConfiguration::categoryPositionChanged); +} + +LXQtFancyMenuConfiguration::~LXQtFancyMenuConfiguration() +{ + delete ui; +} + +void LXQtFancyMenuConfiguration::fillButtonPositionComboBox() +{ + ui->buttRowPosCB->addItem(tr("Bottom"), LXQtFancyMenuButtonPosition::Bottom); + ui->buttRowPosCB->addItem(tr("Top"), LXQtFancyMenuButtonPosition::Top); +} + +void LXQtFancyMenuConfiguration::fillCategoryPositionComboBox() +{ + ui->categoryViewPosCB->addItem(tr("Left"), LXQtFancyMenuCategoryPosition::Left); + ui->categoryViewPosCB->addItem(tr("Right"), LXQtFancyMenuCategoryPosition::Right); +} + +void LXQtFancyMenuConfiguration::loadSettings() +{ + mLockSettingChanges = true; + + ui->iconCB->setChecked(settings().value(QStringLiteral("ownIcon"), false).toBool()); + ui->iconLE->setText(settings().value(QStringLiteral("icon"), QLatin1String(LXQT_GRAPHICS_DIR"/helix.svg")).toString()); + ui->showTextCB->setChecked(settings().value(QStringLiteral("showText"), false).toBool()); + ui->textLE->setText(settings().value(QStringLiteral("text"), QString()).toString()); + + QString menuFile = settings().value(QStringLiteral("menu_file"), QString()).toString(); + if (menuFile.isEmpty()) + { + menuFile = XdgMenu::getMenuFileName(); + } + ui->menuFilePathLE->setText(menuFile); + ui->shortcutEd->setText(nullptr != mShortcut ? mShortcut->shortcut() : mDefaultShortcut); + + ui->customFontCB->setChecked(settings().value(QStringLiteral("customFont"), false).toBool()); + LXQt::Settings lxqtSettings(QStringLiteral("lxqt")); //load system font size as init value + QFont systemFont; + lxqtSettings.beginGroup(QLatin1String("Qt")); + systemFont.fromString(lxqtSettings.value(QStringLiteral("font"), this->font()).toString()); + lxqtSettings.endGroup(); + ui->customFontSizeSB->setValue(settings().value(QStringLiteral("customFontSize"), systemFont.pointSize()).toInt()); + ui->filterClearCB->setChecked(settings().value(QStringLiteral("filterClear"), false).toBool()); + + bool buttonsAtTop = settings().value(QStringLiteral("buttonsAtTop"), false).toBool(); + int buttRowPosIdx = ui->buttRowPosCB->findData(buttonsAtTop ? LXQtFancyMenuButtonPosition::Top : LXQtFancyMenuButtonPosition::Bottom); + ui->buttRowPosCB->setCurrentIndex(buttRowPosIdx); + + bool categoriesAtRight = settings().value(QStringLiteral("categoriesAtRight"), true).toBool(); + int categoryPosIdx = ui->categoryViewPosCB->findData(categoriesAtRight ? LXQtFancyMenuCategoryPosition::Right : LXQtFancyMenuCategoryPosition::Left); + ui->categoryViewPosCB->setCurrentIndex(categoryPosIdx); + + mLockSettingChanges = false; +} + + +void LXQtFancyMenuConfiguration::textButtonChanged(const QString &value) +{ + if (!mLockSettingChanges) + settings().setValue(QStringLiteral("text"), value); +} + +void LXQtFancyMenuConfiguration::showTextChanged(bool value) +{ + if (!mLockSettingChanges) + settings().setValue(QStringLiteral("showText"), value); +} + +void LXQtFancyMenuConfiguration::chooseIcon() +{ + QFileInfo f{ui->iconLE->text()}; + QDir dir = f.dir(); + QFileDialog *d = new QFileDialog(this, + tr("Choose icon file"), + !f.filePath().isEmpty() && dir.exists() ? dir.path() : QLatin1String(LXQT_GRAPHICS_DIR), + tr("Images (*.svg *.png)")); + d->setWindowModality(Qt::WindowModal); + d->setAttribute(Qt::WA_DeleteOnClose); + connect(d, &QFileDialog::fileSelected, this, [&] (const QString &icon) { + ui->iconLE->setText(icon); + }); + d->show(); +} + +void LXQtFancyMenuConfiguration::chooseMenuFile() +{ + QFileDialog *d = new QFileDialog(this, + tr("Choose menu file"), + QLatin1String("/etc/xdg/menus"), + tr("Menu files (*.menu)")); + d->setWindowModality(Qt::WindowModal); + d->setAttribute(Qt::WA_DeleteOnClose); + connect(d, &QFileDialog::fileSelected, this, [&] (const QString &file) { + ui->menuFilePathLE->setText(file); + }); + d->show(); +} + +void LXQtFancyMenuConfiguration::globalShortcutChanged(const QString &/*oldShortcut*/, const QString &newShortcut) +{ + ui->shortcutEd->setText(newShortcut); +} + +void LXQtFancyMenuConfiguration::shortcutChanged(const QString &value) +{ + if (mShortcut) + mShortcut->changeShortcut(value); +} + +void LXQtFancyMenuConfiguration::shortcutReset() +{ + shortcutChanged(mDefaultShortcut); +} + +void LXQtFancyMenuConfiguration::customFontChanged(bool value) +{ + if (!mLockSettingChanges) + settings().setValue(QStringLiteral("customFont"), value); +} + +void LXQtFancyMenuConfiguration::customFontSizeChanged(int value) +{ + if (!mLockSettingChanges) + settings().setValue(QStringLiteral("customFontSize"), value); +} + +void LXQtFancyMenuConfiguration::buttonRowPositionChanged(int idx) +{ + if (mLockSettingChanges) + return; + LXQtFancyMenuButtonPosition pos = LXQtFancyMenuButtonPosition(this->ui->buttRowPosCB->itemData(idx).toInt()); + bool value = (pos == LXQtFancyMenuButtonPosition::Top); + this->settings().setValue(QStringLiteral("buttonsAtTop"), value); +} + +void LXQtFancyMenuConfiguration::categoryPositionChanged(int idx) +{ + if (mLockSettingChanges) + return; + LXQtFancyMenuCategoryPosition pos = LXQtFancyMenuCategoryPosition(this->ui->categoryViewPosCB->itemData(idx).toInt()); + bool value = (pos == LXQtFancyMenuCategoryPosition::Right); + this->settings().setValue(QStringLiteral("categoriesAtRight"), value); +} diff --git a/plugin-fancymenu/lxqtfancymenuconfiguration.h b/plugin-fancymenu/lxqtfancymenuconfiguration.h new file mode 100644 index 000000000..03bec4b79 --- /dev/null +++ b/plugin-fancymenu/lxqtfancymenuconfiguration.h @@ -0,0 +1,84 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * 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 */ + + +#ifndef LXQTFANCYMENUCONFIGURATION_H +#define LXQTFANCYMENUCONFIGURATION_H + +#include "../panel/lxqtpanelpluginconfigdialog.h" +#include "../panel/pluginsettings.h" + +class QAbstractButton; + +namespace Ui { + class LXQtFancyMenuConfiguration; +} + +namespace GlobalKeyShortcut { + class Action; +} + +class LXQtFancyMenuConfiguration : public LXQtPanelPluginConfigDialog +{ + Q_OBJECT + +public: + explicit LXQtFancyMenuConfiguration(PluginSettings *settings, + GlobalKeyShortcut::Action *shortcut, + const QString &defaultShortcut, + QWidget *parent = nullptr); + ~LXQtFancyMenuConfiguration(); + +private: + void fillButtonPositionComboBox(); + void fillCategoryPositionComboBox(); + +private: + Ui::LXQtFancyMenuConfiguration *ui; + QString mDefaultShortcut; + GlobalKeyShortcut::Action * mShortcut; + bool mLockSettingChanges; + +private slots: + void globalShortcutChanged(const QString &oldShortcut, const QString &newShortcut); + void shortcutChanged(const QString &value); + /* + Saves settings in conf file. + */ + void loadSettings(); + void textButtonChanged(const QString &value); + void showTextChanged(bool value); + void chooseIcon(); + void chooseMenuFile(); + void shortcutReset(); + void customFontChanged(bool value); + void customFontSizeChanged(int value); + void buttonRowPositionChanged(int idx); + void categoryPositionChanged(int idx); +}; + +#endif // LXQTFANCYMENUCONFIGURATION_H diff --git a/plugin-fancymenu/lxqtfancymenuconfiguration.ui b/plugin-fancymenu/lxqtfancymenuconfiguration.ui new file mode 100644 index 000000000..49189bc87 --- /dev/null +++ b/plugin-fancymenu/lxqtfancymenuconfiguration.ui @@ -0,0 +1,297 @@ + + + LXQtFancyMenuConfiguration + + + + 0 + 0 + 537 + 544 + + + + Fancy Menu settings + + + + + + General + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + false + + + false + + + + + + Icon: + + + + + + + false + + + + + + + false + + + + + + + + + + Button text: + + + + + + + false + + + + + + + true + + + Custom font size: + + + + + + + false + + + pt + + + 1 + + + 11 + + + + + + + + + + true + + + Menu file + + + + + + Menu file: + + + + + + + + + + + + + + + + Keyboard Shortcut + + + + + + + 200 + 0 + + + + + + + + + + + Click the button to record shortcut: + + + + + + + + + + Search + + + + + + Clear search upon showing menu + + + + + + + + + + Popup + + + + + + + + + Buttons row position + + + + + + + Categories position + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 41 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close|QDialogButtonBox::Reset + + + + + + + + ShortcutSelector + QToolButton +
LXQtGlobalKeysUi/ShortcutSelector
+
+
+ + + + customFontCB + toggled(bool) + customFontSizeSB + setEnabled(bool) + + + 60 + 249 + + + 280 + 277 + + + + + showTextCB + toggled(bool) + textLE + setEnabled(bool) + + + 239 + 39 + + + 313 + 68 + + + + + iconCB + toggled(bool) + iconLE + setEnabled(bool) + + + 91 + 53 + + + 284 + 53 + + + + + iconCB + toggled(bool) + iconPB + setEnabled(bool) + + + 91 + 53 + + + 431 + 53 + + + + +
diff --git a/plugin-fancymenu/lxqtfancymenutypes.h b/plugin-fancymenu/lxqtfancymenutypes.h new file mode 100644 index 000000000..2d0c882be --- /dev/null +++ b/plugin-fancymenu/lxqtfancymenutypes.h @@ -0,0 +1,55 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * 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 */ + + +#ifndef LXQTFANCYMENUTYPES_H +#define LXQTFANCYMENUTYPES_H + +#include + +enum LXQtFancyMenuButtonPosition : bool +{ + Bottom = 0, + Top = 1 +}; + +enum LXQtFancyMenuCategoryPosition : bool +{ + Left = 0, + Right = 1 +}; + +enum class LXQtFancyMenuItemType +{ + AppItem = 0, + CategoryItem, + SeparatorItem +}; + +static constexpr const int LXQtFancyMenuItemIsSeparatorRole = Qt::UserRole + 1; + +#endif // LXQTFANCYMENUTYPES_H diff --git a/plugin-fancymenu/lxqtfancymenuwindow.cpp b/plugin-fancymenu/lxqtfancymenuwindow.cpp new file mode 100644 index 000000000..f3c34eb57 --- /dev/null +++ b/plugin-fancymenu/lxqtfancymenuwindow.cpp @@ -0,0 +1,521 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * 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 "lxqtfancymenuwindow.h" + +#include "lxqtfancymenuappmap.h" +#include "lxqtfancymenuappmodel.h" +#include "lxqtfancymenucategoriesmodel.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include + +#include + +#include +#include + +#include +#include + +namespace +{ +class SingleActivateStyle : public QProxyStyle +{ +public: + using QProxyStyle::QProxyStyle; + int styleHint(StyleHint hint, const QStyleOption * option = nullptr, const QWidget * widget = nullptr, QStyleHintReturn * returnData = nullptr) const override + { + if(hint == QStyle::SH_ItemView_ActivateItemOnSingleClick) + return 1; + return QProxyStyle::styleHint(hint, option, widget, returnData); + + } +}; + +class SeparatorDelegate : public QStyledItemDelegate +{ +public: + SeparatorDelegate(QObject *parent) : QStyledItemDelegate(parent) {} + + static bool isSeparator(const QModelIndex &index) + { + return index.data(LXQtFancyMenuItemIsSeparatorRole).toInt() == 1; + } + +protected: + void paint(QPainter *painter, + const QStyleOptionViewItem &option, + const QModelIndex &index) const override + { + if (isSeparator(index)) + { + QRect rect = option.rect; + if (const QAbstractItemView *view = qobject_cast(option.widget)) + rect.setWidth(view->viewport()->width()); + QStyleOption opt; + opt.rect = rect; + option.widget->style()->drawPrimitive(QStyle::PE_IndicatorToolBarSeparator, &opt, painter, option.widget); + } + else + { + QStyledItemDelegate::paint(painter, option, index); + } + } + + QSize sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const override + { + if (isSeparator(index)) + { + int pm = option.widget->style()->pixelMetric(QStyle::PM_DefaultFrameWidth, + nullptr, + option.widget); + return QSize(pm, pm); + } + + return QStyledItemDelegate::sizeHint(option, index); + } +}; + +} + +LXQtFancyMenuWindow::LXQtFancyMenuWindow(QWidget *parent) + : QWidget{parent, Qt::Popup} +{ + SingleActivateStyle *s = new SingleActivateStyle; + s->setParent(this); + setStyle(s); + + mSearchTimer.setSingleShot(true); + connect(&mSearchTimer, &QTimer::timeout, this, &LXQtFancyMenuWindow::doSearch); + mSearchTimer.setInterval(350); // typing speed (not very fast) + + mSearchEdit = new QLineEdit; + mSearchEdit->setPlaceholderText(tr("Search...")); + mSearchEdit->setClearButtonEnabled(true); + connect(mSearchEdit, &QLineEdit::textEdited, &mSearchTimer, qOverload<>(&QTimer::start)); + connect(mSearchEdit, &QLineEdit::returnPressed, this, &LXQtFancyMenuWindow::activateCurrentApp); + + mSettingsButton = new QToolButton; + mSettingsButton->setIcon(XdgIcon::fromTheme(QStringLiteral("preferences-desktop"))); //TODO: preferences-system? + mSettingsButton->setText(tr("Settings")); + mSettingsButton->setToolTip(mSettingsButton->text()); + connect(mSettingsButton, &QToolButton::clicked, this, &LXQtFancyMenuWindow::runSystemConfigDialog); + + mPowerButton = new QToolButton; + mPowerButton->setIcon(XdgIcon::fromTheme(QStringLiteral("system-shutdown"))); + mPowerButton->setText(tr("Leave")); + mPowerButton->setToolTip(mPowerButton->text()); + connect(mPowerButton, &QToolButton::clicked, this, &LXQtFancyMenuWindow::runPowerDialog); + + mAppView = new QListView; + mAppView->setSelectionMode(QListView::SingleSelection); + mAppView->setDragEnabled(true); + mAppView->setContextMenuPolicy(Qt::CustomContextMenu); + mAppView->setItemDelegate(new SeparatorDelegate(this)); + + mCategoryView = new QListView; + mCategoryView->setSelectionMode(QListView::SingleSelection); + mCategoryView->setItemDelegate(new SeparatorDelegate(this)); + + // Meld category view with whole popup window + // So remove the frame and set same background as the window + mCategoryView->setFrameShape(QFrame::NoFrame); + mCategoryView->viewport()->setBackgroundRole(QPalette::Window); + + mAppMap = new LXQtFancyMenuAppMap; + + mAppModel = new LXQtFancyMenuAppModel(this); + mAppModel->setAppMap(mAppMap); + mAppView->setModel(mAppModel); + + mCategoryModel = new LXQtFancyMenuCategoriesModel(this); + mCategoryModel->setAppMap(mAppMap); + mCategoryView->setModel(mCategoryModel); + + connect(mAppView, &QListView::activated, this, &LXQtFancyMenuWindow::activateAppAtIndex); + connect(mAppView, &QListView::customContextMenuRequested, this, &LXQtFancyMenuWindow::onAppViewCustomMenu); + connect(mCategoryView, &QListView::activated, this, &LXQtFancyMenuWindow::activateCategory); + + mMainLayout = new QVBoxLayout(this); + + mMainLayout->addWidget(mSearchEdit); + + mViewLayout = new QHBoxLayout; + mViewLayout->addWidget(mAppView, APP_VIEW_STRETCH); + mViewLayout->addWidget(mCategoryView, CAT_VIEW_STRETCH); + mMainLayout->addLayout(mViewLayout); + + mButtonsLayout = new QHBoxLayout; + mButtonsLayout->addStretch(); + mButtonsLayout->addWidget(mSettingsButton); + mButtonsLayout->addWidget(mPowerButton); + mMainLayout->addLayout(mButtonsLayout); + + updateButtonIconSize(); + + setMinimumHeight(500); + + // Ensure all key presses go to search box + setFocusProxy(mSearchEdit); + mAppView->setFocusProxy(mSearchEdit); + mCategoryView->setFocusProxy(mSearchEdit); + + // Filter navigation keys + mSearchEdit->installEventFilter(this); +} + +LXQtFancyMenuWindow::~LXQtFancyMenuWindow() +{ + mAppModel->setAppMap(nullptr); + mCategoryModel->setAppMap(nullptr); + delete mAppMap; + mAppMap = nullptr; +} + +QSize LXQtFancyMenuWindow::sizeHint() const +{ + return QSize(450, 550); +} + +bool LXQtFancyMenuWindow::rebuildMenu(const XdgMenu &menu) +{ + mAppModel->reloadAppMap(false); + mCategoryModel->reloadAppMap(false); + mAppMap->rebuildModel(menu); + mAppModel->reloadAppMap(true); + mCategoryModel->reloadAppMap(true); + + setCurrentCategory(LXQtFancyMenuAppMap::FavoritesCategory); + + return true; +} + +void LXQtFancyMenuWindow::activateCategory(const QModelIndex &idx) +{ + setCurrentCategory(idx.row()); +} + +void LXQtFancyMenuWindow::activateAppAtIndex(const QModelIndex &idx) +{ + if(!idx.isValid()) + return; + + auto *app = mAppModel->getAppAt(idx.row()); + if(!app) + return; + + app->desktopFileCache.startDetached(); + hide(); +} + +void LXQtFancyMenuWindow::activateCurrentApp() +{ + activateAppAtIndex(mAppView->currentIndex()); +} + +void LXQtFancyMenuWindow::runPowerDialog() +{ + runCommandHelper(QLatin1String("lxqt-leave")); +} + +void LXQtFancyMenuWindow::runSystemConfigDialog() +{ + runCommandHelper(QLatin1String("lxqt-config")); +} + +void LXQtFancyMenuWindow::onAppViewCustomMenu(const QPoint& p) +{ + QModelIndex idx = mAppView->indexAt(p); + auto item = mAppModel->getAppAt(idx.row()); + if(!item) + return; + + XdgDesktopFile df = item->desktopFileCache; + QString file = df.fileName(); + + QMenu menu; + QAction *a; + + if (df.actions().count() > 0 && df.type() == XdgDesktopFile::Type::ApplicationType) + { + for (int i = 0; i < df.actions().count(); ++i) + { + QString actionString(df.actions().at(i)); + a = menu.addAction(df.actionIcon(actionString), df.actionName(actionString)); + connect(a, &QAction::triggered, this, [this, df, actionString] { + df.actionActivate(actionString, QStringList()); + hide(); + }); + } + menu.addSeparator(); + } + + a = menu.addAction(XdgIcon::fromTheme(QLatin1String("desktop")), tr("Add to desktop")); + connect(a, &QAction::triggered, [file] { + QString desktop = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); + QString desktopFile = desktop + QStringLiteral("/") + file.section(QStringLiteral("/"), -1); + if (QFile::exists(desktopFile)) + { + QMessageBox::StandardButton btn = + QMessageBox::question(nullptr, + tr("Question"), + tr("A file with the same name already exists.\nDo you want to overwrite it?")); + if (btn == QMessageBox::No) + return; + if (!QFile::remove(desktopFile)) + { + QMessageBox::warning(nullptr, + tr("Warning"), + tr("The file cannot be overwritten.")); + return; + } + } + QFile::copy(file, desktopFile); + }); + + a = menu.addAction(XdgIcon::fromTheme(QLatin1String("edit-copy")), tr("Copy")); + connect(a, &QAction::triggered, this, [file] { + QClipboard* clipboard = QApplication::clipboard(); + QMimeData* data = new QMimeData(); + data->setUrls({QUrl::fromLocalFile(file)}); + clipboard->setMimeData(data); + }); + + menu.addSeparator(); + + QString canonicalFile = QDir(file).canonicalPath(); + if(mAppMap->isFavorite(canonicalFile)) + { + a = menu.addAction(XdgIcon::fromTheme(QLatin1String("bookmark-remove")), tr("Remove from Favorites")); + connect(a, &QAction::triggered, this, [this, canonicalFile] { + removeFromFavorites(canonicalFile); + }); + } + else + { + a = menu.addAction(XdgIcon::fromTheme(QLatin1String("bookmark-new")), tr("Add to Favorites")); + connect(a, &QAction::triggered, this, [this, canonicalFile] { + addToFavorites(canonicalFile); + }); + } + + QPoint globalPos = mAppView->mapToGlobal(p); + menu.exec(globalPos); +} + +void LXQtFancyMenuWindow::setCurrentCategory(int cat) +{ + QModelIndex idx = mCategoryModel->index(cat, 0); + mCategoryView->setCurrentIndex(idx); + mCategoryView->selectionModel()->select(idx, QItemSelectionModel::ClearAndSelect); + mAppModel->setCurrentCategory(cat); + + // If user clicked elsewhere, reset search + if(cat != LXQtFancyMenuAppMap::AllAppsCategory) + setSearchQuery(QString()); +} + +bool LXQtFancyMenuWindow::eventFilter(QObject *watched, QEvent *e) +{ + if(watched == mSearchEdit && e->type() == QEvent::KeyPress) + { + QKeyEvent *ev = static_cast(e); + if(ev->key() == Qt::Key_Up || ev->key() == Qt::Key_Down) + { + // Use Up/Down arrows to navigate app view + QCoreApplication::sendEvent(mAppView, ev); + return true; + } + } + + return QWidget::eventFilter(watched, e); +} + +void LXQtFancyMenuWindow::doSearch() +{ + setSearchQuery(mSearchEdit->text()); +} + +void LXQtFancyMenuWindow::setSearchQuery(const QString &text) +{ + QSignalBlocker blk(mSearchEdit); + mSearchEdit->setText(text); + + if(text.isEmpty()) + { + mAppModel->endSearch(); + return; + } + + setCurrentCategory(LXQtFancyMenuAppMap::AllAppsCategory); + + auto apps = mAppMap->getMatchingApps(text); + mAppModel->showSearchResults(apps); +} + +void LXQtFancyMenuWindow::hideEvent(QHideEvent *e) +{ + emit aboutToHide(); + + if(mFilterClear) + setSearchQuery(QString()); // Clear search on hide + + // If search is not active, switch to Favorites + if(mSearchEdit->text().isEmpty()) + setCurrentCategory(LXQtFancyMenuAppMap::FavoritesCategory); + + QWidget::hideEvent(e); +} + +void LXQtFancyMenuWindow::keyPressEvent(QKeyEvent *e) +{ + // If search edit is not empty, clear it instead of closing popup + if(!mSearchEdit->text().isEmpty() && e->matches(QKeySequence::Cancel)) + { + mSearchEdit->clear(); + e->accept(); + return; + } + + QWidget::keyPressEvent(e); +} + +void LXQtFancyMenuWindow::runCommandHelper(const QString &cmd) +{ + if(QProcess::startDetached(cmd, QStringList())) + { + hide(); + } + else + { + QMessageBox::warning(this, tr("No Executable"), + tr("Cannot find %1 executable.").arg(cmd)); + } +} + +void LXQtFancyMenuWindow::addToFavorites(const QString &desktopFile) +{ + mFavorites.append(desktopFile); + + mAppModel->reloadAppMap(false); + mAppMap->addToFavorites(desktopFile); + mAppModel->reloadAppMap(true); + + emit favoritesChanged(); +} + +void LXQtFancyMenuWindow::removeFromFavorites(const QString &desktopFile) +{ + mFavorites.removeOne(desktopFile); + mAppModel->reloadAppMap(false); + mAppMap->removeFromFavorites(desktopFile); + mAppModel->reloadAppMap(true); + + emit favoritesChanged(); +} + +void LXQtFancyMenuWindow::setFilterClear(bool newFilterClear) +{ + mFilterClear = newFilterClear; + + if(mFilterClear && !isVisible()) + { + // Apply immediately + setSearchQuery(QString()); + } +} + +void LXQtFancyMenuWindow::setButtonPosition(LXQtFancyMenuButtonPosition pos) +{ + mMainLayout->removeItem(mButtonsLayout); + int idx = 0; + if(pos == LXQtFancyMenuButtonPosition::Bottom) + idx = -1; + + mMainLayout->insertLayout(idx, mButtonsLayout); +} + +void LXQtFancyMenuWindow::setCategoryPosition(LXQtFancyMenuCategoryPosition pos) +{ + mViewLayout->removeWidget(mCategoryView); + int idx = 0; + if(pos == LXQtFancyMenuCategoryPosition::Right) + idx = -1; + + mViewLayout->insertWidget(idx, mCategoryView, CAT_VIEW_STRETCH); +} + +void LXQtFancyMenuWindow::updateButtonIconSize() +{ + int sz = style()->pixelMetric(QStyle::PM_LargeIconSize, nullptr, mSettingsButton); + const QSize iconSize(sz, sz); + mSettingsButton->setIconSize(iconSize); + mPowerButton->setIconSize(iconSize); +} + +void LXQtFancyMenuWindow::setSearchEditFocus() +{ + mSearchEdit->setFocus(); +} + +void LXQtFancyMenuWindow::setCustomFont(const QFont &f) +{ + mAppView->setFont(f); + mCategoryView->setFont(f); + mSearchEdit->setFont(f); +} + +QStringList LXQtFancyMenuWindow::favorites() const +{ + return mFavorites; +} + +void LXQtFancyMenuWindow::setFavorites(const QStringList &newFavorites) +{ + mFavorites = newFavorites; + mAppModel->reloadAppMap(false); + mAppMap->setFavorites(mFavorites); + mAppModel->reloadAppMap(true); +} diff --git a/plugin-fancymenu/lxqtfancymenuwindow.h b/plugin-fancymenu/lxqtfancymenuwindow.h new file mode 100644 index 000000000..6316ae882 --- /dev/null +++ b/plugin-fancymenu/lxqtfancymenuwindow.h @@ -0,0 +1,134 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * LXQt - a lightweight, Qt based, desktop toolset + * https://lxqt.org + * + * Copyright: 2023 LXQt team + * Authors: + * Filippo Gentile + * + * 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 */ + + +#ifndef LXQTFANCYMENUWINDOW_H +#define LXQTFANCYMENUWINDOW_H + +#include +#include + +#include "lxqtfancymenutypes.h" + +class QLineEdit; +class QToolButton; +class QListView; +class QModelIndex; + +class QHBoxLayout; +class QVBoxLayout; + +class XdgMenu; + +class LXQtFancyMenuAppMap; +class LXQtFancyMenuAppModel; +class LXQtFancyMenuCategoriesModel; + +class LXQtFancyMenuWindow : public QWidget +{ + Q_OBJECT +public: + explicit LXQtFancyMenuWindow(QWidget *parent = nullptr); + ~LXQtFancyMenuWindow(); + + virtual QSize sizeHint() const override; + + bool rebuildMenu(const XdgMenu &menu); + + void setCurrentCategory(int cat); + + bool eventFilter(QObject *watched, QEvent *e) override; + + QStringList favorites() const; + void setFavorites(const QStringList &newFavorites); + + void setFilterClear(bool newFilterClear); + + void setButtonPosition(LXQtFancyMenuButtonPosition pos); + void setCategoryPosition(LXQtFancyMenuCategoryPosition pos); + + void updateButtonIconSize(); + + void setSearchEditFocus(); + + void setCustomFont(const QFont& f); + +signals: + void aboutToShow(); + void aboutToHide(); + void favoritesChanged(); + +public slots: + void doSearch(); + void setSearchQuery(const QString& text); + +protected: + void hideEvent(QHideEvent *e); + void keyPressEvent(QKeyEvent *e); + +private slots: + void activateCategory(const QModelIndex& idx); + void activateAppAtIndex(const QModelIndex& idx); + void activateCurrentApp(); + + void runPowerDialog(); + void runSystemConfigDialog(); + + void onAppViewCustomMenu(const QPoint &p); + +private: + void runCommandHelper(const QString& cmd); + + void addToFavorites(const QString& desktopFile); + void removeFromFavorites(const QString& desktopFile); + +private: + // Use 3:2 stretch factors so app view is slightly wider than category view + static const int APP_VIEW_STRETCH = 3; + static const int CAT_VIEW_STRETCH = 2; + + QStringList mFavorites; + + QVBoxLayout *mMainLayout; + QHBoxLayout *mButtonsLayout; + QHBoxLayout *mViewLayout; + + QToolButton *mSettingsButton; + QToolButton *mPowerButton; + QLineEdit *mSearchEdit; + QListView *mAppView; + QListView *mCategoryView; + + LXQtFancyMenuAppMap *mAppMap; + LXQtFancyMenuAppModel *mAppModel; + LXQtFancyMenuCategoriesModel *mCategoryModel; + + QTimer mSearchTimer; + bool mFilterClear = false; +}; + +#endif // LXQTFANCYMENUWINDOW_H diff --git a/plugin-fancymenu/resources/fancymenu.desktop.in b/plugin-fancymenu/resources/fancymenu.desktop.in new file mode 100644 index 000000000..005895f11 --- /dev/null +++ b/plugin-fancymenu/resources/fancymenu.desktop.in @@ -0,0 +1,6 @@ +[Desktop Entry] +Type=Service +ServiceTypes=LXQtPanel/Plugin +Icon=start-here-lxqt + +#TRANSLATIONS_DIR=../translations diff --git a/plugin-fancymenu/translations/fancymenu.desktop.yaml b/plugin-fancymenu/translations/fancymenu.desktop.yaml new file mode 100644 index 000000000..efc41ebe9 --- /dev/null +++ b/plugin-fancymenu/translations/fancymenu.desktop.yaml @@ -0,0 +1,2 @@ +Desktop Entry/Name: "Fancy Application Menu" +Desktop Entry/Comment: "A menu of all your applications with favorites"