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
49QT_BEGIN_NAMESPACE
50
51#if QT_CONFIG(shortcut)
52static 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].toCombined() ^ mods) & shortcut[0].toCombined();
60}
61
62static GdkModifierType qt_gdkModifiers(const QKeySequence &shortcut)
63{
64 if (shortcut.isEmpty())
65 return GdkModifierType(0);
66
67 guint mods = 0;
68 Qt::KeyboardModifiers m = shortcut[0].keyboardModifiers();
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
82QGtk3MenuItem::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
95QGtk3MenuItem::~QGtk3MenuItem()
96{
97}
98
99bool QGtk3MenuItem::isInvalid() const
100{
101 return m_invalid;
102}
103
104GtkWidget *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
147GtkWidget *QGtk3MenuItem::handle() const
148{
149 return m_item;
150}
151
152QString QGtk3MenuItem::text() const
153{
154 return m_text;
155}
156
157static 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
185void 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
194QGtk3Menu *QGtk3MenuItem::menu() const
195{
196 return m_menu;
197}
198
199void 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
206bool QGtk3MenuItem::isVisible() const
207{
208 return m_visible;
209}
210
211void 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
221bool QGtk3MenuItem::isSeparator() const
222{
223 return m_separator;
224}
225
226void QGtk3MenuItem::setIsSeparator(bool separator)
227{
228 if (m_separator == separator)
229 return;
230
231 m_invalid = true;
232 m_separator = separator;
233}
234
235bool QGtk3MenuItem::isCheckable() const
236{
237 return m_checkable;
238}
239
240void QGtk3MenuItem::setCheckable(bool checkable)
241{
242 if (m_checkable == checkable)
243 return;
244
245 m_invalid = true;
246 m_checkable = checkable;
247}
248
249bool QGtk3MenuItem::isChecked() const
250{
251 return m_checked;
252}
253
254void 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)
265QKeySequence QGtk3MenuItem::shortcut() const
266{
267 return m_shortcut;
268}
269
270void 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
283bool QGtk3MenuItem::isEnabled() const
284{
285 return m_enabled;
286}
287
288void 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
298bool QGtk3MenuItem::hasExclusiveGroup() const
299{
300 return m_exclusive;
301}
302
303void 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
313void QGtk3MenuItem::onSelect(GtkMenuItem *, void *data)
314{
315 QGtk3MenuItem *item = static_cast<QGtk3MenuItem *>(data);
316 if (item)
317 emit item->hovered();
318}
319
320void QGtk3MenuItem::onActivate(GtkMenuItem *, void *data)
321{
322 QGtk3MenuItem *item = static_cast<QGtk3MenuItem *>(data);
323 if (item)
324 emit item->activated();
325}
326
327void 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
339QGtk3Menu::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
347QGtk3Menu::~QGtk3Menu()
348{
349 if (GTK_IS_WIDGET(m_menu))
350 gtk_widget_destroy(m_menu);
351}
352
353GtkWidget *QGtk3Menu::handle() const
354{
355 return m_menu;
356}
357
358void 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
372void 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
383void 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
395void QGtk3Menu::syncSeparatorsCollapsible(bool enable)
396{
397 Q_UNUSED(enable);
398}
399
400void QGtk3Menu::setEnabled(bool enabled)
401{
402 gtk_widget_set_sensitive(m_menu, enabled);
403}
404
405void QGtk3Menu::setVisible(bool visible)
406{
407 gtk_widget_set_visible(m_menu, visible);
408}
409
410static 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
422QPoint QGtk3Menu::targetPos() const
423{
424 return m_targetPos;
425}
426
427void 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
442void QGtk3Menu::dismiss()
443{
444 gtk_menu_popdown(GTK_MENU(m_menu));
445}
446
447QPlatformMenuItem *QGtk3Menu::menuItemAt(int position) const
448{
449 return m_items.value(position);
450}
451
452QPlatformMenuItem *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
461QPlatformMenuItem *QGtk3Menu::createMenuItem() const
462{
463 return new QGtk3MenuItem;
464}
465
466QPlatformMenu *QGtk3Menu::createSubMenu() const
467{
468 return new QGtk3Menu;
469}
470
471void QGtk3Menu::onShow(GtkWidget *, void *data)
472{
473 QGtk3Menu *menu = static_cast<QGtk3Menu *>(data);
474 if (menu)
475 emit menu->aboutToShow();
476}
477
478void QGtk3Menu::onHide(GtkWidget *, void *data)
479{
480 QGtk3Menu *menu = static_cast<QGtk3Menu *>(data);
481 if (menu)
482 emit menu->aboutToHide();
483}
484
485QT_END_NAMESPACE
486