1 /****************************************************************************
2 **
3 ** Copyright (C) 2017 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the QtGui module of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:LGPL$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU Lesser General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU Lesser
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
21 ** packaging of this file. Please review the following information to
22 ** ensure the GNU Lesser General Public License version 3 requirements
23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 2.0 or (at your option) the GNU General
28 ** Public license version 3 or any later version approved by the KDE Free
29 ** Qt Foundation. The licenses are as published by the Free Software
30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31 ** included in the packaging of this file. Please review the following
32 ** information to ensure the GNU General Public License requirements will
33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34 ** https://www.gnu.org/licenses/gpl-3.0.html.
35 **
36 ** $QT_END_LICENSE$
37 **
38 ****************************************************************************/
39 
40 #include "qgtk3menu.h"
41 
42 #include <QtGui/qwindow.h>
43 #include <QtGui/qpa/qplatformtheme.h>
44 #include <QtGui/qpa/qplatformwindow.h>
45 
46 #undef signals
47 #include <gtk/gtk.h>
48 
49 QT_BEGIN_NAMESPACE
50 
51 #if QT_CONFIG(shortcut)
qt_gdkKey(const QKeySequence & shortcut)52 static guint qt_gdkKey(const QKeySequence &shortcut)
53 {
54     if (shortcut.isEmpty())
55         return 0;
56 
57     // TODO: proper mapping
58     Qt::KeyboardModifiers mods = Qt::ShiftModifier | Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier;
59     return (shortcut[0] ^ mods) & shortcut[0];
60 }
61 
qt_gdkModifiers(const QKeySequence & shortcut)62 static GdkModifierType qt_gdkModifiers(const QKeySequence &shortcut)
63 {
64     if (shortcut.isEmpty())
65         return GdkModifierType(0);
66 
67     guint mods = 0;
68     int m = shortcut[0];
69     if (m & Qt::ShiftModifier)
70         mods |= GDK_SHIFT_MASK;
71     if (m & Qt::ControlModifier)
72         mods |= GDK_CONTROL_MASK;
73     if (m & Qt::AltModifier)
74         mods |= GDK_MOD1_MASK;
75     if (m & Qt::MetaModifier)
76         mods |= GDK_META_MASK;
77 
78     return static_cast<GdkModifierType>(mods);
79 }
80 #endif
81 
QGtk3MenuItem()82 QGtk3MenuItem::QGtk3MenuItem()
83     : m_visible(true),
84       m_separator(false),
85       m_checkable(false),
86       m_checked(false),
87       m_enabled(true),
88       m_underline(false),
89       m_invalid(true),
90       m_menu(nullptr),
91       m_item(nullptr)
92 {
93 }
94 
~QGtk3MenuItem()95 QGtk3MenuItem::~QGtk3MenuItem()
96 {
97 }
98 
isInvalid() const99 bool QGtk3MenuItem::isInvalid() const
100 {
101     return m_invalid;
102 }
103 
create()104 GtkWidget *QGtk3MenuItem::create()
105 {
106     if (m_invalid) {
107         if (m_item) {
108             gtk_widget_destroy(m_item);
109             m_item = nullptr;
110         }
111         m_invalid = false;
112     }
113 
114     if (!m_item) {
115         if (m_separator) {
116             m_item = gtk_separator_menu_item_new();
117         } else {
118             if (m_checkable) {
119                 m_item = gtk_check_menu_item_new();
120                 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(m_item), m_checked);
121                 g_signal_connect(m_item, "toggled", G_CALLBACK(onToggle), this);
122             } else {
123                 m_item = gtk_menu_item_new();
124                 g_signal_connect(m_item, "activate", G_CALLBACK(onActivate), this);
125             }
126             gtk_menu_item_set_label(GTK_MENU_ITEM(m_item), m_text.toUtf8());
127             gtk_menu_item_set_use_underline(GTK_MENU_ITEM(m_item), m_underline);
128             if (m_menu)
129                 gtk_menu_item_set_submenu(GTK_MENU_ITEM(m_item), m_menu->handle());
130             g_signal_connect(m_item, "select", G_CALLBACK(onSelect), this);
131 #if QT_CONFIG(shortcut)
132             if (!m_shortcut.isEmpty()) {
133                 GtkWidget *label = gtk_bin_get_child(GTK_BIN(m_item));
134                 gtk_accel_label_set_accel(GTK_ACCEL_LABEL(label), qt_gdkKey(m_shortcut), qt_gdkModifiers(m_shortcut));
135             }
136 #endif
137         }
138         gtk_widget_set_sensitive(m_item, m_enabled);
139         gtk_widget_set_visible(m_item, m_visible);
140         if (GTK_IS_CHECK_MENU_ITEM(m_item))
141             g_object_set(m_item, "draw-as-radio", m_exclusive, NULL);
142     }
143 
144     return m_item;
145 }
146 
handle() const147 GtkWidget *QGtk3MenuItem::handle() const
148 {
149     return m_item;
150 }
151 
text() const152 QString QGtk3MenuItem::text() const
153 {
154     return m_text;
155 }
156 
convertMnemonics(QString text,bool * found)157 static QString convertMnemonics(QString text, bool *found)
158 {
159     *found = false;
160 
161     int i = text.length() - 1;
162     while (i >= 0) {
163         const QChar c = text.at(i);
164         if (c == QLatin1Char('&')) {
165             if (i == 0 || text.at(i - 1) != QLatin1Char('&')) {
166                 // convert Qt to GTK mnemonic
167                 if (i < text.length() - 1 && !text.at(i + 1).isSpace()) {
168                     text.replace(i, 1, QLatin1Char('_'));
169                     *found = true;
170                 }
171             } else if (text.at(i - 1) == QLatin1Char('&')) {
172                 // unescape ampersand
173                 text.replace(--i, 2, QLatin1Char('&'));
174             }
175         } else if (c == QLatin1Char('_')) {
176             // escape GTK mnemonic
177             text.insert(i, QLatin1Char('_'));
178         }
179         --i;
180     }
181 
182     return text;
183 }
184 
setText(const QString & text)185 void QGtk3MenuItem::setText(const QString &text)
186 {
187     m_text = convertMnemonics(text, &m_underline);
188     if (GTK_IS_MENU_ITEM(m_item)) {
189         gtk_menu_item_set_label(GTK_MENU_ITEM(m_item), m_text.toUtf8());
190         gtk_menu_item_set_use_underline(GTK_MENU_ITEM(m_item), m_underline);
191     }
192 }
193 
menu() const194 QGtk3Menu *QGtk3MenuItem::menu() const
195 {
196     return m_menu;
197 }
198 
setMenu(QPlatformMenu * menu)199 void QGtk3MenuItem::setMenu(QPlatformMenu *menu)
200 {
201     m_menu = qobject_cast<QGtk3Menu *>(menu);
202     if (GTK_IS_MENU_ITEM(m_item))
203         gtk_menu_item_set_submenu(GTK_MENU_ITEM(m_item), m_menu ? m_menu->handle() : nullptr);
204 }
205 
isVisible() const206 bool QGtk3MenuItem::isVisible() const
207 {
208     return m_visible;
209 }
210 
setVisible(bool visible)211 void QGtk3MenuItem::setVisible(bool visible)
212 {
213     if (m_visible == visible)
214         return;
215 
216     m_visible = visible;
217     if (GTK_IS_MENU_ITEM(m_item))
218         gtk_widget_set_visible(m_item, visible);
219 }
220 
isSeparator() const221 bool QGtk3MenuItem::isSeparator() const
222 {
223     return m_separator;
224 }
225 
setIsSeparator(bool separator)226 void QGtk3MenuItem::setIsSeparator(bool separator)
227 {
228     if (m_separator == separator)
229         return;
230 
231     m_invalid = true;
232     m_separator = separator;
233 }
234 
isCheckable() const235 bool QGtk3MenuItem::isCheckable() const
236 {
237     return m_checkable;
238 }
239 
setCheckable(bool checkable)240 void QGtk3MenuItem::setCheckable(bool checkable)
241 {
242     if (m_checkable == checkable)
243         return;
244 
245     m_invalid = true;
246     m_checkable = checkable;
247 }
248 
isChecked() const249 bool QGtk3MenuItem::isChecked() const
250 {
251     return m_checked;
252 }
253 
setChecked(bool checked)254 void QGtk3MenuItem::setChecked(bool checked)
255 {
256     if (m_checked == checked)
257         return;
258 
259     m_checked = checked;
260     if (GTK_IS_CHECK_MENU_ITEM(m_item))
261         gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(m_item), checked);
262 }
263 
264 #if QT_CONFIG(shortcut)
shortcut() const265 QKeySequence QGtk3MenuItem::shortcut() const
266 {
267     return m_shortcut;
268 }
269 
setShortcut(const QKeySequence & shortcut)270 void QGtk3MenuItem::setShortcut(const QKeySequence& shortcut)
271 {
272     if (m_shortcut == shortcut)
273         return;
274 
275     m_shortcut = shortcut;
276     if (GTK_IS_MENU_ITEM(m_item)) {
277         GtkWidget *label = gtk_bin_get_child(GTK_BIN(m_item));
278         gtk_accel_label_set_accel(GTK_ACCEL_LABEL(label), qt_gdkKey(m_shortcut), qt_gdkModifiers(m_shortcut));
279     }
280 }
281 #endif
282 
isEnabled() const283 bool QGtk3MenuItem::isEnabled() const
284 {
285     return m_enabled;
286 }
287 
setEnabled(bool enabled)288 void QGtk3MenuItem::setEnabled(bool enabled)
289 {
290     if (m_enabled == enabled)
291         return;
292 
293     m_enabled = enabled;
294     if (m_item)
295         gtk_widget_set_sensitive(m_item, enabled);
296 }
297 
hasExclusiveGroup() const298 bool QGtk3MenuItem::hasExclusiveGroup() const
299 {
300     return m_exclusive;
301 }
302 
setHasExclusiveGroup(bool exclusive)303 void QGtk3MenuItem::setHasExclusiveGroup(bool exclusive)
304 {
305     if (m_exclusive == exclusive)
306         return;
307 
308     m_exclusive = exclusive;
309     if (GTK_IS_CHECK_MENU_ITEM(m_item))
310         g_object_set(m_item, "draw-as-radio", exclusive, NULL);
311 }
312 
onSelect(GtkMenuItem *,void * data)313 void QGtk3MenuItem::onSelect(GtkMenuItem *, void *data)
314 {
315     QGtk3MenuItem *item = static_cast<QGtk3MenuItem *>(data);
316     if (item)
317         emit item->hovered();
318 }
319 
onActivate(GtkMenuItem *,void * data)320 void QGtk3MenuItem::onActivate(GtkMenuItem *, void *data)
321 {
322     QGtk3MenuItem *item = static_cast<QGtk3MenuItem *>(data);
323     if (item)
324         emit item->activated();
325 }
326 
onToggle(GtkCheckMenuItem * check,void * data)327 void QGtk3MenuItem::onToggle(GtkCheckMenuItem *check, void *data)
328 {
329     QGtk3MenuItem *item = static_cast<QGtk3MenuItem *>(data);
330     if (item) {
331         bool active = gtk_check_menu_item_get_active(check);
332         if (active != item->isChecked()) {
333             item->setChecked(active);
334             emit item->activated();
335         }
336     }
337 }
338 
QGtk3Menu()339 QGtk3Menu::QGtk3Menu()
340 {
341     m_menu = gtk_menu_new();
342 
343     g_signal_connect(m_menu, "show", G_CALLBACK(onShow), this);
344     g_signal_connect(m_menu, "hide", G_CALLBACK(onHide), this);
345 }
346 
~QGtk3Menu()347 QGtk3Menu::~QGtk3Menu()
348 {
349     if (GTK_IS_WIDGET(m_menu))
350         gtk_widget_destroy(m_menu);
351 }
352 
handle() const353 GtkWidget *QGtk3Menu::handle() const
354 {
355     return m_menu;
356 }
357 
insertMenuItem(QPlatformMenuItem * item,QPlatformMenuItem * before)358 void QGtk3Menu::insertMenuItem(QPlatformMenuItem *item, QPlatformMenuItem *before)
359 {
360     QGtk3MenuItem *gitem = static_cast<QGtk3MenuItem *>(item);
361     if (!gitem || m_items.contains(gitem))
362         return;
363 
364     GtkWidget *handle = gitem->create();
365     int index = m_items.indexOf(static_cast<QGtk3MenuItem *>(before));
366     if (index < 0)
367         index = m_items.count();
368     m_items.insert(index, gitem);
369     gtk_menu_shell_insert(GTK_MENU_SHELL(m_menu), handle, index);
370 }
371 
removeMenuItem(QPlatformMenuItem * item)372 void QGtk3Menu::removeMenuItem(QPlatformMenuItem *item)
373 {
374     QGtk3MenuItem *gitem = static_cast<QGtk3MenuItem *>(item);
375     if (!gitem || !m_items.removeOne(gitem))
376         return;
377 
378     GtkWidget *handle = gitem->handle();
379     if (handle)
380         gtk_container_remove(GTK_CONTAINER(m_menu), handle);
381 }
382 
syncMenuItem(QPlatformMenuItem * item)383 void QGtk3Menu::syncMenuItem(QPlatformMenuItem *item)
384 {
385     QGtk3MenuItem *gitem = static_cast<QGtk3MenuItem *>(item);
386     int index = m_items.indexOf(gitem);
387     if (index == -1 || !gitem->isInvalid())
388         return;
389 
390     GtkWidget *handle = gitem->create();
391     if (handle)
392         gtk_menu_shell_insert(GTK_MENU_SHELL(m_menu), handle, index);
393 }
394 
syncSeparatorsCollapsible(bool enable)395 void QGtk3Menu::syncSeparatorsCollapsible(bool enable)
396 {
397     Q_UNUSED(enable);
398 }
399 
setEnabled(bool enabled)400 void QGtk3Menu::setEnabled(bool enabled)
401 {
402     gtk_widget_set_sensitive(m_menu, enabled);
403 }
404 
setVisible(bool visible)405 void QGtk3Menu::setVisible(bool visible)
406 {
407     gtk_widget_set_visible(m_menu, visible);
408 }
409 
qt_gtk_menu_position_func(GtkMenu *,gint * x,gint * y,gboolean * push_in,gpointer data)410 static void qt_gtk_menu_position_func(GtkMenu *, gint *x, gint *y, gboolean *push_in, gpointer data)
411 {
412     QGtk3Menu *menu = static_cast<QGtk3Menu *>(data);
413     QPoint targetPos = menu->targetPos();
414 #if GTK_CHECK_VERSION(3, 10, 0)
415     targetPos /= gtk_widget_get_scale_factor(menu->handle());
416 #endif
417     *x = targetPos.x();
418     *y = targetPos.y();
419     *push_in = true;
420 }
421 
targetPos() const422 QPoint QGtk3Menu::targetPos() const
423 {
424     return m_targetPos;
425 }
426 
showPopup(const QWindow * parentWindow,const QRect & targetRect,const QPlatformMenuItem * item)427 void QGtk3Menu::showPopup(const QWindow *parentWindow, const QRect &targetRect, const QPlatformMenuItem *item)
428 {
429     const QGtk3MenuItem *menuItem = static_cast<const QGtk3MenuItem *>(item);
430     if (menuItem)
431         gtk_menu_shell_select_item(GTK_MENU_SHELL(m_menu), menuItem->handle());
432 
433     m_targetPos = QPoint(targetRect.x(), targetRect.y() + targetRect.height());
434 
435     QPlatformWindow *pw = parentWindow ? parentWindow->handle() : nullptr;
436     if (pw)
437         m_targetPos = pw->mapToGlobal(m_targetPos);
438 
439     gtk_menu_popup(GTK_MENU(m_menu), nullptr, nullptr, qt_gtk_menu_position_func, this, 0, gtk_get_current_event_time());
440 }
441 
dismiss()442 void QGtk3Menu::dismiss()
443 {
444     gtk_menu_popdown(GTK_MENU(m_menu));
445 }
446 
menuItemAt(int position) const447 QPlatformMenuItem *QGtk3Menu::menuItemAt(int position) const
448 {
449     return m_items.value(position);
450 }
451 
menuItemForTag(quintptr tag) const452 QPlatformMenuItem *QGtk3Menu::menuItemForTag(quintptr tag) const
453 {
454     for (QGtk3MenuItem *item : m_items) {
455         if (item->tag() == tag)
456             return item;
457     }
458     return nullptr;
459 }
460 
createMenuItem() const461 QPlatformMenuItem *QGtk3Menu::createMenuItem() const
462 {
463     return new QGtk3MenuItem;
464 }
465 
createSubMenu() const466 QPlatformMenu *QGtk3Menu::createSubMenu() const
467 {
468     return new QGtk3Menu;
469 }
470 
onShow(GtkWidget *,void * data)471 void QGtk3Menu::onShow(GtkWidget *, void *data)
472 {
473     QGtk3Menu *menu = static_cast<QGtk3Menu *>(data);
474     if (menu)
475         emit menu->aboutToShow();
476 }
477 
onHide(GtkWidget *,void * data)478 void QGtk3Menu::onHide(GtkWidget *, void *data)
479 {
480     QGtk3Menu *menu = static_cast<QGtk3Menu *>(data);
481     if (menu)
482         emit menu->aboutToHide();
483 }
484 
485 QT_END_NAMESPACE
486