File indexing completed on 2024-05-26 05:38:20
0001 /* 0002 SPDX-FileCopyrightText: 2016 Kai Uwe Broulik <kde@privat.broulik.de> 0003 0004 SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0005 */ 0006 0007 #include "appmenuapplet.h" 0008 #include "../plugin/appmenumodel.h" 0009 0010 #include <QAction> 0011 #include <QDBusConnection> 0012 #include <QDBusConnectionInterface> 0013 #include <QKeyEvent> 0014 #include <QMenu> 0015 #include <QMouseEvent> 0016 #include <QQuickItem> 0017 #include <QQuickWindow> 0018 #include <QScreen> 0019 #include <QTimer> 0020 0021 int AppMenuApplet::s_refs = 0; 0022 namespace 0023 { 0024 QString viewService() 0025 { 0026 return QStringLiteral("org.kde.kappmenuview"); 0027 } 0028 } 0029 0030 AppMenuApplet::AppMenuApplet(QObject *parent, const KPluginMetaData &data, const QVariantList &args) 0031 : Plasma::Applet(parent, data, args) 0032 { 0033 ++s_refs; 0034 // if we're the first, register the service 0035 if (s_refs == 1) { 0036 QDBusConnection::sessionBus().interface()->registerService(viewService(), 0037 QDBusConnectionInterface::QueueService, 0038 QDBusConnectionInterface::DontAllowReplacement); 0039 } 0040 /*it registers or unregisters the service when the destroyed value of the applet change, 0041 and not in the dtor, because: 0042 when we "delete" an applet, it just hides it for about a minute setting its status 0043 to destroyed, in order to be able to do a clean undo: if we undo, there will be 0044 another destroyedchanged and destroyed will be false. 0045 When this happens, if we are the only appmenu applet existing, the dbus interface 0046 will have to be registered again*/ 0047 connect(this, &Applet::destroyedChanged, this, [](bool destroyed) { 0048 if (destroyed) { 0049 // if we were the last, unregister 0050 if (--s_refs == 0) { 0051 QDBusConnection::sessionBus().interface()->unregisterService(viewService()); 0052 } 0053 } else { 0054 // if we're the first, register the service 0055 if (++s_refs == 1) { 0056 QDBusConnection::sessionBus().interface()->registerService(viewService(), 0057 QDBusConnectionInterface::QueueService, 0058 QDBusConnectionInterface::DontAllowReplacement); 0059 } 0060 } 0061 }); 0062 } 0063 0064 AppMenuApplet::~AppMenuApplet() = default; 0065 0066 void AppMenuApplet::init() 0067 { 0068 } 0069 0070 QAbstractItemModel *AppMenuApplet::model() const 0071 { 0072 return m_model; 0073 } 0074 0075 void AppMenuApplet::setModel(QAbstractItemModel *model) 0076 { 0077 if (m_model != model) { 0078 m_model = model; 0079 Q_EMIT modelChanged(); 0080 } 0081 } 0082 0083 int AppMenuApplet::view() const 0084 { 0085 return m_viewType; 0086 } 0087 0088 void AppMenuApplet::setView(int type) 0089 { 0090 if (m_viewType != type) { 0091 m_viewType = type; 0092 Q_EMIT viewChanged(); 0093 } 0094 } 0095 0096 int AppMenuApplet::currentIndex() const 0097 { 0098 return m_currentIndex; 0099 } 0100 0101 void AppMenuApplet::setCurrentIndex(int currentIndex) 0102 { 0103 if (m_currentIndex != currentIndex) { 0104 m_currentIndex = currentIndex; 0105 Q_EMIT currentIndexChanged(); 0106 } 0107 } 0108 0109 QQuickItem *AppMenuApplet::buttonGrid() const 0110 { 0111 return m_buttonGrid; 0112 } 0113 0114 void AppMenuApplet::setButtonGrid(QQuickItem *buttonGrid) 0115 { 0116 if (m_buttonGrid != buttonGrid) { 0117 m_buttonGrid = buttonGrid; 0118 Q_EMIT buttonGridChanged(); 0119 } 0120 } 0121 0122 QMenu *AppMenuApplet::createMenu(int idx) const 0123 { 0124 QMenu *menu = nullptr; 0125 0126 if (view() == CompactView) { 0127 if (QAction *menuAction = m_model->data(QModelIndex(), AppMenuModel::ActionRole).value<QAction *>()) { 0128 menu = menuAction->menu(); 0129 } 0130 } else if (view() == FullView) { 0131 const QModelIndex index = m_model->index(idx, 0); 0132 if (QAction *action = m_model->data(index, AppMenuModel::ActionRole).value<QAction *>()) { 0133 menu = action->menu(); 0134 } 0135 } 0136 0137 return menu; 0138 } 0139 0140 void AppMenuApplet::onMenuAboutToHide() 0141 { 0142 setCurrentIndex(-1); 0143 } 0144 0145 Qt::Edges edgeFromLocation(Plasma::Types::Location location) 0146 { 0147 switch (location) { 0148 case Plasma::Types::TopEdge: 0149 return Qt::TopEdge; 0150 case Plasma::Types::BottomEdge: 0151 return Qt::BottomEdge; 0152 case Plasma::Types::LeftEdge: 0153 return Qt::LeftEdge; 0154 case Plasma::Types::RightEdge: 0155 return Qt::RightEdge; 0156 case Plasma::Types::Floating: 0157 case Plasma::Types::Desktop: 0158 case Plasma::Types::FullScreen: 0159 break; 0160 } 0161 return Qt::Edges(); 0162 } 0163 0164 void AppMenuApplet::trigger(QQuickItem *ctx, int idx) 0165 { 0166 if (m_currentIndex == idx) { 0167 return; 0168 } 0169 0170 if (!ctx || !ctx->window() || !ctx->window()->screen()) { 0171 return; 0172 } 0173 0174 QMenu *actionMenu = createMenu(idx); 0175 if (actionMenu) { 0176 // this is a workaround where Qt will fail to realize a mouse has been released 0177 // this happens if a window which does not accept focus spawns a new window that takes focus and X grab 0178 // whilst the mouse is depressed 0179 // https://bugreports.qt.io/browse/QTBUG-59044 0180 // this causes the next click to go missing 0181 0182 // by releasing manually we avoid that situation 0183 auto ungrabMouseHack = [ctx]() { 0184 if (ctx && ctx->window() && ctx->window()->mouseGrabberItem()) { 0185 // FIXME event forge thing enters press and hold move mode :/ 0186 ctx->window()->mouseGrabberItem()->ungrabMouse(); 0187 } 0188 }; 0189 0190 QTimer::singleShot(0, ctx, ungrabMouseHack); 0191 // end workaround 0192 0193 const auto &geo = ctx->window()->screen()->availableVirtualGeometry(); 0194 0195 QPoint pos = ctx->window()->mapToGlobal(ctx->mapToScene(QPointF()).toPoint()); 0196 0197 const Qt::Edges edges = edgeFromLocation(location()); 0198 actionMenu->setProperty("_breeze_menu_seamless_edges", QVariant::fromValue(edges)); 0199 0200 if (location() == Plasma::Types::TopEdge) { 0201 pos.setY(pos.y() + ctx->height()); 0202 } 0203 0204 actionMenu->adjustSize(); 0205 0206 pos = QPoint(qBound(geo.x(), pos.x(), geo.x() + geo.width() - actionMenu->width()), 0207 qBound(geo.y(), pos.y(), geo.y() + geo.height() - actionMenu->height())); 0208 0209 if (view() == FullView) { 0210 actionMenu->installEventFilter(this); 0211 } 0212 0213 actionMenu->winId(); // create window handle 0214 actionMenu->windowHandle()->setTransientParent(ctx->window()); 0215 0216 // hide the old menu only after showing the new one to avoid brief focus flickering on X11. 0217 // on wayland, you can't have more than one grabbing popup at a time so we show it after 0218 // the menu has hidden. thankfully, wayland doesn't have this flickering. 0219 if (!KWindowSystem::isPlatformWayland()) { 0220 actionMenu->popup(pos); 0221 } 0222 0223 if (view() == FullView) { 0224 QMenu *oldMenu = m_currentMenu; 0225 m_currentMenu = actionMenu; 0226 if (oldMenu && oldMenu != actionMenu) { 0227 // don't initialize the currentIndex when another menu is already shown 0228 disconnect(oldMenu, &QMenu::aboutToHide, this, &AppMenuApplet::onMenuAboutToHide); 0229 oldMenu->hide(); 0230 } 0231 } 0232 0233 if (KWindowSystem::isPlatformWayland()) { 0234 actionMenu->popup(pos); 0235 } 0236 0237 setCurrentIndex(idx); 0238 0239 // FIXME TODO connect only once 0240 connect(actionMenu, &QMenu::aboutToHide, this, &AppMenuApplet::onMenuAboutToHide, Qt::UniqueConnection); 0241 } else { // is it just an action without a menu? 0242 if (QAction *action = m_model->index(idx, 0).data(AppMenuModel::ActionRole).value<QAction *>()) { 0243 Q_ASSERT(!action->menu()); 0244 action->trigger(); 0245 } 0246 } 0247 } 0248 0249 // FIXME TODO doesn't work on submenu 0250 bool AppMenuApplet::eventFilter(QObject *watched, QEvent *event) 0251 { 0252 auto *menu = qobject_cast<QMenu *>(watched); 0253 if (!menu) { 0254 return false; 0255 } 0256 0257 if (event->type() == QEvent::KeyPress) { 0258 auto *e = static_cast<QKeyEvent *>(event); 0259 0260 // TODO right to left languages 0261 if (e->key() == Qt::Key_Left) { 0262 int desiredIndex = m_currentIndex - 1; 0263 Q_EMIT requestActivateIndex(desiredIndex); 0264 return true; 0265 } else if (e->key() == Qt::Key_Right) { 0266 if (menu->activeAction() && menu->activeAction()->menu()) { 0267 return false; 0268 } 0269 0270 int desiredIndex = m_currentIndex + 1; 0271 Q_EMIT requestActivateIndex(desiredIndex); 0272 return true; 0273 } 0274 0275 } else if (event->type() == QEvent::MouseMove) { 0276 auto *e = static_cast<QMouseEvent *>(event); 0277 0278 if (!m_buttonGrid || !m_buttonGrid->window()) { 0279 return false; 0280 } 0281 0282 // FIXME the panel margin breaks Fitt's law :( 0283 const QPointF &windowLocalPos = m_buttonGrid->window()->mapFromGlobal(e->globalPosition()); 0284 const QPointF &buttonGridLocalPos = m_buttonGrid->mapFromScene(windowLocalPos); 0285 auto *item = m_buttonGrid->childAt(buttonGridLocalPos.x(), buttonGridLocalPos.y()); 0286 if (!item) { 0287 return false; 0288 } 0289 0290 bool ok; 0291 const int buttonIndex = item->property("buttonIndex").toInt(&ok); 0292 if (!ok) { 0293 return false; 0294 } 0295 0296 Q_EMIT requestActivateIndex(buttonIndex); 0297 } 0298 0299 return false; 0300 } 0301 0302 K_PLUGIN_CLASS(AppMenuApplet) 0303 0304 #include "appmenuapplet.moc" 0305 #include "moc_appmenuapplet.cpp"