1 /*
2 SPDX-FileCopyrightText: 2016 Kai Uwe Broulik <kde@privat.broulik.de>
3
4 SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
5 */
6
7 #include "appmenuapplet.h"
8 #include "../plugin/appmenumodel.h"
9
10 #include <QAction>
11 #include <QDBusConnection>
12 #include <QDBusConnectionInterface>
13 #include <QKeyEvent>
14 #include <QMenu>
15 #include <QMouseEvent>
16 #include <QQuickItem>
17 #include <QQuickWindow>
18 #include <QScreen>
19 #include <QTimer>
20
21 int AppMenuApplet::s_refs = 0;
22 namespace
23 {
viewService()24 QString viewService()
25 {
26 return QStringLiteral("org.kde.kappmenuview");
27 }
28 }
29
AppMenuApplet(QObject * parent,const QVariantList & data)30 AppMenuApplet::AppMenuApplet(QObject *parent, const QVariantList &data)
31 : Plasma::Applet(parent, data)
32 {
33 ++s_refs;
34 // if we're the first, register the service
35 if (s_refs == 1) {
36 QDBusConnection::sessionBus().interface()->registerService(viewService(),
37 QDBusConnectionInterface::QueueService,
38 QDBusConnectionInterface::DontAllowReplacement);
39 }
40 /*it registers or unregisters the service when the destroyed value of the applet change,
41 and not in the dtor, because:
42 when we "delete" an applet, it just hides it for about a minute setting its status
43 to destroyed, in order to be able to do a clean undo: if we undo, there will be
44 another destroyedchanged and destroyed will be false.
45 When this happens, if we are the only appmenu applet existing, the dbus interface
46 will have to be registered again*/
47 connect(this, &Applet::destroyedChanged, this, [](bool destroyed) {
48 if (destroyed) {
49 // if we were the last, unregister
50 if (--s_refs == 0) {
51 QDBusConnection::sessionBus().interface()->unregisterService(viewService());
52 }
53 } else {
54 // if we're the first, register the service
55 if (++s_refs == 1) {
56 QDBusConnection::sessionBus().interface()->registerService(viewService(),
57 QDBusConnectionInterface::QueueService,
58 QDBusConnectionInterface::DontAllowReplacement);
59 }
60 }
61 });
62 }
63
64 AppMenuApplet::~AppMenuApplet() = default;
65
init()66 void AppMenuApplet::init()
67 {
68 }
69
model() const70 AppMenuModel *AppMenuApplet::model() const
71 {
72 return m_model;
73 }
74
setModel(AppMenuModel * model)75 void AppMenuApplet::setModel(AppMenuModel *model)
76 {
77 if (m_model != model) {
78 m_model = model;
79 emit modelChanged();
80 }
81 }
82
view() const83 int AppMenuApplet::view() const
84 {
85 return m_viewType;
86 }
87
setView(int type)88 void AppMenuApplet::setView(int type)
89 {
90 if (m_viewType != type) {
91 m_viewType = type;
92 emit viewChanged();
93 }
94 }
95
currentIndex() const96 int AppMenuApplet::currentIndex() const
97 {
98 return m_currentIndex;
99 }
100
setCurrentIndex(int currentIndex)101 void AppMenuApplet::setCurrentIndex(int currentIndex)
102 {
103 if (m_currentIndex != currentIndex) {
104 m_currentIndex = currentIndex;
105 emit currentIndexChanged();
106 }
107 }
108
buttonGrid() const109 QQuickItem *AppMenuApplet::buttonGrid() const
110 {
111 return m_buttonGrid;
112 }
113
setButtonGrid(QQuickItem * buttonGrid)114 void AppMenuApplet::setButtonGrid(QQuickItem *buttonGrid)
115 {
116 if (m_buttonGrid != buttonGrid) {
117 m_buttonGrid = buttonGrid;
118 emit buttonGridChanged();
119 }
120 }
121
createMenu(int idx) const122 QMenu *AppMenuApplet::createMenu(int idx) const
123 {
124 QMenu *menu = nullptr;
125 QAction *action = nullptr;
126
127 if (view() == CompactView) {
128 menu = new QMenu();
129 for (int i = 0; i < m_model->rowCount(); i++) {
130 const QModelIndex index = m_model->index(i, 0);
131 const QVariant data = m_model->data(index, AppMenuModel::ActionRole);
132 action = (QAction *)data.value<void *>();
133 menu->addAction(action);
134 }
135 menu->setAttribute(Qt::WA_DeleteOnClose);
136 } else if (view() == FullView) {
137 const QModelIndex index = m_model->index(idx, 0);
138 const QVariant data = m_model->data(index, AppMenuModel::ActionRole);
139 action = (QAction *)data.value<void *>();
140 if (action) {
141 menu = action->menu();
142 }
143 }
144
145 return menu;
146 }
147
onMenuAboutToHide()148 void AppMenuApplet::onMenuAboutToHide()
149 {
150 setCurrentIndex(-1);
151 }
152
trigger(QQuickItem * ctx,int idx)153 void AppMenuApplet::trigger(QQuickItem *ctx, int idx)
154 {
155 if (m_currentIndex == idx) {
156 return;
157 }
158
159 if (!ctx || !ctx->window() || !ctx->window()->screen()) {
160 return;
161 }
162
163 QMenu *actionMenu = createMenu(idx);
164 if (actionMenu) {
165 // this is a workaround where Qt will fail to realize a mouse has been released
166 // this happens if a window which does not accept focus spawns a new window that takes focus and X grab
167 // whilst the mouse is depressed
168 // https://bugreports.qt.io/browse/QTBUG-59044
169 // this causes the next click to go missing
170
171 // by releasing manually we avoid that situation
172 auto ungrabMouseHack = [ctx]() {
173 if (ctx && ctx->window() && ctx->window()->mouseGrabberItem()) {
174 // FIXME event forge thing enters press and hold move mode :/
175 ctx->window()->mouseGrabberItem()->ungrabMouse();
176 }
177 };
178
179 QTimer::singleShot(0, ctx, ungrabMouseHack);
180 // end workaround
181
182 const auto &geo = ctx->window()->screen()->availableVirtualGeometry();
183
184 QPoint pos = ctx->window()->mapToGlobal(ctx->mapToScene(QPointF()).toPoint());
185 if (location() == Plasma::Types::TopEdge) {
186 actionMenu->setProperty("_breeze_menu_is_top", true);
187 pos.setY(pos.y() + ctx->height());
188 }
189
190 actionMenu->adjustSize();
191
192 pos = QPoint(qBound(geo.x(), pos.x(), geo.x() + geo.width() - actionMenu->width()),
193 qBound(geo.y(), pos.y(), geo.y() + geo.height() - actionMenu->height()));
194
195 if (view() == FullView) {
196 actionMenu->installEventFilter(this);
197 }
198
199 actionMenu->winId(); // create window handle
200 actionMenu->windowHandle()->setTransientParent(ctx->window());
201
202 // hide the old menu only after showing the new one to avoid brief focus flickering on X11.
203 // on wayland, you can't have more than one grabbing popup at a time so we show it after
204 // the menu has hidden. thankfully, wayland doesn't have this flickering.
205 if (!KWindowSystem::isPlatformWayland()) {
206 actionMenu->popup(pos);
207 }
208
209 if (view() == FullView) {
210 QMenu *oldMenu = m_currentMenu;
211 m_currentMenu = actionMenu;
212 if (oldMenu && oldMenu != actionMenu) {
213 // don't initialize the currentIndex when another menu is already shown
214 disconnect(oldMenu, &QMenu::aboutToHide, this, &AppMenuApplet::onMenuAboutToHide);
215 oldMenu->hide();
216 }
217 }
218
219 if (KWindowSystem::isPlatformWayland()) {
220 actionMenu->popup(pos);
221 }
222
223 setCurrentIndex(idx);
224
225 // FIXME TODO connect only once
226 connect(actionMenu, &QMenu::aboutToHide, this, &AppMenuApplet::onMenuAboutToHide, Qt::UniqueConnection);
227 } else { // is it just an action without a menu?
228 const QVariant data = m_model->index(idx, 0).data(AppMenuModel::ActionRole);
229 QAction *action = static_cast<QAction *>(data.value<void *>());
230 if (action) {
231 Q_ASSERT(!action->menu());
232 action->trigger();
233 }
234 }
235 }
236
237 // FIXME TODO doesn't work on submenu
eventFilter(QObject * watched,QEvent * event)238 bool AppMenuApplet::eventFilter(QObject *watched, QEvent *event)
239 {
240 auto *menu = qobject_cast<QMenu *>(watched);
241 if (!menu) {
242 return false;
243 }
244
245 if (event->type() == QEvent::KeyPress) {
246 auto *e = static_cast<QKeyEvent *>(event);
247
248 // TODO right to left languages
249 if (e->key() == Qt::Key_Left) {
250 int desiredIndex = m_currentIndex - 1;
251 emit requestActivateIndex(desiredIndex);
252 return true;
253 } else if (e->key() == Qt::Key_Right) {
254 if (menu->activeAction() && menu->activeAction()->menu()) {
255 return false;
256 }
257
258 int desiredIndex = m_currentIndex + 1;
259 emit requestActivateIndex(desiredIndex);
260 return true;
261 }
262
263 } else if (event->type() == QEvent::MouseMove) {
264 auto *e = static_cast<QMouseEvent *>(event);
265
266 if (!m_buttonGrid || !m_buttonGrid->window()) {
267 return false;
268 }
269
270 // FIXME the panel margin breaks Fitt's law :(
271 const QPointF &windowLocalPos = m_buttonGrid->window()->mapFromGlobal(e->globalPos());
272 const QPointF &buttonGridLocalPos = m_buttonGrid->mapFromScene(windowLocalPos);
273 auto *item = m_buttonGrid->childAt(buttonGridLocalPos.x(), buttonGridLocalPos.y());
274 if (!item) {
275 return false;
276 }
277
278 bool ok;
279 const int buttonIndex = item->property("buttonIndex").toInt(&ok);
280 if (!ok) {
281 return false;
282 }
283
284 emit requestActivateIndex(buttonIndex);
285 }
286
287 return false;
288 }
289
290 K_PLUGIN_CLASS_WITH_JSON(AppMenuApplet, "metadata.json")
291
292 #include "appmenuapplet.moc"
293