1// Copyright (C) 2017 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qgtk3menu.h"
5
6#include <QtGui/qwindow.h>
7#include <QtGui/qpa/qplatformtheme.h>
8#include <QtGui/qpa/qplatformwindow.h>
9
10#undef signals
11#include <gtk/gtk.h>
12
13QT_BEGIN_NAMESPACE
14
15#if QT_CONFIG(shortcut)
16static guint qt_gdkKey(const QKeySequence &shortcut)
17{
18 if (shortcut.isEmpty())
19 return 0;
20
21 // TODO: proper mapping
22 Qt::KeyboardModifiers mods = Qt::ShiftModifier | Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier;
23 return (shortcut[0].toCombined() ^ mods) & shortcut[0].toCombined();
24}
25
26static GdkModifierType qt_gdkModifiers(const QKeySequence &shortcut)
27{
28 if (shortcut.isEmpty())
29 return GdkModifierType(0);
30
31 guint mods = 0;
32 Qt::KeyboardModifiers m = shortcut[0].keyboardModifiers();
33 if (m & Qt::ShiftModifier)
34 mods |= GDK_SHIFT_MASK;
35 if (m & Qt::ControlModifier)
36 mods |= GDK_CONTROL_MASK;
37 if (m & Qt::AltModifier)
38 mods |= GDK_MOD1_MASK;
39 if (m & Qt::MetaModifier)
40 mods |= GDK_META_MASK;
41
42 return static_cast<GdkModifierType>(mods);
43}
44#endif
45
46QGtk3MenuItem::QGtk3MenuItem()
47 : m_visible(true),
48 m_separator(false),
49 m_checkable(false),
50 m_checked(false),
51 m_enabled(true),
52 m_exclusive(false),
53 m_underline(false),
54 m_invalid(true),
55 m_menu(nullptr),
56 m_item(nullptr)
57{
58}
59
60QGtk3MenuItem::~QGtk3MenuItem()
61{
62}
63
64bool QGtk3MenuItem::isInvalid() const
65{
66 return m_invalid;
67}
68
69GtkWidget *QGtk3MenuItem::create()
70{
71 if (m_invalid) {
72 if (m_item) {
73 gtk_widget_destroy(widget: m_item);
74 m_item = nullptr;
75 }
76 m_invalid = false;
77 }
78
79 if (!m_item) {
80 if (m_separator) {
81 m_item = gtk_separator_menu_item_new();
82 } else {
83 if (m_checkable) {
84 m_item = gtk_check_menu_item_new();
85 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(m_item), is_active: m_checked);
86 g_signal_connect(m_item, "toggled", G_CALLBACK(onToggle), this);
87 } else {
88 m_item = gtk_menu_item_new();
89 g_signal_connect(m_item, "activate", G_CALLBACK(onActivate), this);
90 }
91 gtk_menu_item_set_label(GTK_MENU_ITEM(m_item), label: m_text.toUtf8());
92 gtk_menu_item_set_use_underline(GTK_MENU_ITEM(m_item), setting: m_underline);
93 if (m_menu)
94 gtk_menu_item_set_submenu(GTK_MENU_ITEM(m_item), submenu: m_menu->handle());
95 g_signal_connect(m_item, "select", G_CALLBACK(onSelect), this);
96#if QT_CONFIG(shortcut)
97 if (!m_shortcut.isEmpty()) {
98 GtkWidget *label = gtk_bin_get_child(GTK_BIN(m_item));
99 gtk_accel_label_set_accel(GTK_ACCEL_LABEL(label), accelerator_key: qt_gdkKey(shortcut: m_shortcut), accelerator_mods: qt_gdkModifiers(shortcut: m_shortcut));
100 }
101#endif
102 }
103 gtk_widget_set_sensitive(widget: m_item, sensitive: m_enabled);
104 gtk_widget_set_visible(widget: m_item, visible: m_visible);
105 if (GTK_IS_CHECK_MENU_ITEM(m_item))
106 g_object_set(object: m_item, first_property_name: "draw-as-radio", m_exclusive, NULL);
107 }
108
109 return m_item;
110}
111
112GtkWidget *QGtk3MenuItem::handle() const
113{
114 return m_item;
115}
116
117QString QGtk3MenuItem::text() const
118{
119 return m_text;
120}
121
122static QString convertMnemonics(QString text, bool *found)
123{
124 *found = false;
125
126 qsizetype i = text.size() - 1;
127 while (i >= 0) {
128 const QChar c = text.at(i);
129 if (c == u'&') {
130 if (i == 0 || text.at(i: i - 1) != u'&') {
131 // convert Qt to GTK mnemonic
132 if (i < text.size() - 1 && !text.at(i: i + 1).isSpace()) {
133 text.replace(i, len: 1, after: u'_');
134 *found = true;
135 }
136 } else if (text.at(i: i - 1) == u'&') {
137 // unescape ampersand
138 text.replace(i: --i, len: 2, after: u'&');
139 }
140 } else if (c == u'_') {
141 // escape GTK mnemonic
142 text.insert(i, c: u'_');
143 }
144 --i;
145 }
146
147 return text;
148}
149
150void QGtk3MenuItem::setText(const QString &text)
151{
152 m_text = convertMnemonics(text, found: &m_underline);
153 if (GTK_IS_MENU_ITEM(m_item)) {
154 gtk_menu_item_set_label(GTK_MENU_ITEM(m_item), label: m_text.toUtf8());
155 gtk_menu_item_set_use_underline(GTK_MENU_ITEM(m_item), setting: m_underline);
156 }
157}
158
159QGtk3Menu *QGtk3MenuItem::menu() const
160{
161 return m_menu;
162}
163
164void QGtk3MenuItem::setMenu(QPlatformMenu *menu)
165{
166 m_menu = qobject_cast<QGtk3Menu *>(object: menu);
167 if (GTK_IS_MENU_ITEM(m_item))
168 gtk_menu_item_set_submenu(GTK_MENU_ITEM(m_item), submenu: m_menu ? m_menu->handle() : nullptr);
169}
170
171bool QGtk3MenuItem::isVisible() const
172{
173 return m_visible;
174}
175
176void QGtk3MenuItem::setVisible(bool visible)
177{
178 if (m_visible == visible)
179 return;
180
181 m_visible = visible;
182 if (GTK_IS_MENU_ITEM(m_item))
183 gtk_widget_set_visible(widget: m_item, visible);
184}
185
186bool QGtk3MenuItem::isSeparator() const
187{
188 return m_separator;
189}
190
191void QGtk3MenuItem::setIsSeparator(bool separator)
192{
193 if (m_separator == separator)
194 return;
195
196 m_invalid = true;
197 m_separator = separator;
198}
199
200bool QGtk3MenuItem::isCheckable() const
201{
202 return m_checkable;
203}
204
205void QGtk3MenuItem::setCheckable(bool checkable)
206{
207 if (m_checkable == checkable)
208 return;
209
210 m_invalid = true;
211 m_checkable = checkable;
212}
213
214bool QGtk3MenuItem::isChecked() const
215{
216 return m_checked;
217}
218
219void QGtk3MenuItem::setChecked(bool checked)
220{
221 if (m_checked == checked)
222 return;
223
224 m_checked = checked;
225 if (GTK_IS_CHECK_MENU_ITEM(m_item))
226 gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(m_item), is_active: checked);
227}
228
229#if QT_CONFIG(shortcut)
230QKeySequence QGtk3MenuItem::shortcut() const
231{
232 return m_shortcut;
233}
234
235void QGtk3MenuItem::setShortcut(const QKeySequence& shortcut)
236{
237 if (m_shortcut == shortcut)
238 return;
239
240 m_shortcut = shortcut;
241 if (GTK_IS_MENU_ITEM(m_item)) {
242 GtkWidget *label = gtk_bin_get_child(GTK_BIN(m_item));
243 gtk_accel_label_set_accel(GTK_ACCEL_LABEL(label), accelerator_key: qt_gdkKey(shortcut: m_shortcut), accelerator_mods: qt_gdkModifiers(shortcut: m_shortcut));
244 }
245}
246#endif
247
248bool QGtk3MenuItem::isEnabled() const
249{
250 return m_enabled;
251}
252
253void QGtk3MenuItem::setEnabled(bool enabled)
254{
255 if (m_enabled == enabled)
256 return;
257
258 m_enabled = enabled;
259 if (m_item)
260 gtk_widget_set_sensitive(widget: m_item, sensitive: enabled);
261}
262
263bool QGtk3MenuItem::hasExclusiveGroup() const
264{
265 return m_exclusive;
266}
267
268void QGtk3MenuItem::setHasExclusiveGroup(bool exclusive)
269{
270 if (m_exclusive == exclusive)
271 return;
272
273 m_exclusive = exclusive;
274 if (GTK_IS_CHECK_MENU_ITEM(m_item))
275 g_object_set(object: m_item, first_property_name: "draw-as-radio", exclusive, NULL);
276}
277
278void QGtk3MenuItem::onSelect(GtkMenuItem *, void *data)
279{
280 QGtk3MenuItem *item = static_cast<QGtk3MenuItem *>(data);
281 if (item)
282 emit item->hovered();
283}
284
285void QGtk3MenuItem::onActivate(GtkMenuItem *, void *data)
286{
287 QGtk3MenuItem *item = static_cast<QGtk3MenuItem *>(data);
288 if (item)
289 emit item->activated();
290}
291
292void QGtk3MenuItem::onToggle(GtkCheckMenuItem *check, void *data)
293{
294 QGtk3MenuItem *item = static_cast<QGtk3MenuItem *>(data);
295 if (item) {
296 bool active = gtk_check_menu_item_get_active(check_menu_item: check);
297 if (active != item->isChecked()) {
298 item->setChecked(active);
299 emit item->activated();
300 }
301 }
302}
303
304QGtk3Menu::QGtk3Menu()
305{
306 m_menu = gtk_menu_new();
307
308 g_signal_connect(m_menu, "show", G_CALLBACK(onShow), this);
309 g_signal_connect(m_menu, "hide", G_CALLBACK(onHide), this);
310}
311
312QGtk3Menu::~QGtk3Menu()
313{
314 if (GTK_IS_WIDGET(m_menu))
315 gtk_widget_destroy(widget: m_menu);
316}
317
318GtkWidget *QGtk3Menu::handle() const
319{
320 return m_menu;
321}
322
323void QGtk3Menu::insertMenuItem(QPlatformMenuItem *item, QPlatformMenuItem *before)
324{
325 QGtk3MenuItem *gitem = static_cast<QGtk3MenuItem *>(item);
326 if (!gitem || m_items.contains(t: gitem))
327 return;
328
329 GtkWidget *handle = gitem->create();
330 int index = m_items.indexOf(t: static_cast<QGtk3MenuItem *>(before));
331 if (index < 0)
332 index = m_items.size();
333 m_items.insert(i: index, t: gitem);
334 gtk_menu_shell_insert(GTK_MENU_SHELL(m_menu), child: handle, position: index);
335}
336
337void QGtk3Menu::removeMenuItem(QPlatformMenuItem *item)
338{
339 QGtk3MenuItem *gitem = static_cast<QGtk3MenuItem *>(item);
340 if (!gitem || !m_items.removeOne(t: gitem))
341 return;
342
343 GtkWidget *handle = gitem->handle();
344 if (handle)
345 gtk_container_remove(GTK_CONTAINER(m_menu), widget: handle);
346}
347
348void QGtk3Menu::syncMenuItem(QPlatformMenuItem *item)
349{
350 QGtk3MenuItem *gitem = static_cast<QGtk3MenuItem *>(item);
351 int index = m_items.indexOf(t: gitem);
352 if (index == -1 || !gitem->isInvalid())
353 return;
354
355 GtkWidget *handle = gitem->create();
356 if (handle)
357 gtk_menu_shell_insert(GTK_MENU_SHELL(m_menu), child: handle, position: index);
358}
359
360void QGtk3Menu::syncSeparatorsCollapsible(bool enable)
361{
362 Q_UNUSED(enable);
363}
364
365void QGtk3Menu::setEnabled(bool enabled)
366{
367 gtk_widget_set_sensitive(widget: m_menu, sensitive: enabled);
368}
369
370void QGtk3Menu::setVisible(bool visible)
371{
372 gtk_widget_set_visible(widget: m_menu, visible);
373}
374
375static void qt_gtk_menu_position_func(GtkMenu *, gint *x, gint *y, gboolean *push_in, gpointer data)
376{
377 QGtk3Menu *menu = static_cast<QGtk3Menu *>(data);
378 QPoint targetPos = menu->targetPos();
379#if GTK_CHECK_VERSION(3, 10, 0)
380 targetPos /= gtk_widget_get_scale_factor(widget: menu->handle());
381#endif
382 *x = targetPos.x();
383 *y = targetPos.y();
384 *push_in = true;
385}
386
387QPoint QGtk3Menu::targetPos() const
388{
389 return m_targetPos;
390}
391
392void QGtk3Menu::showPopup(const QWindow *parentWindow, const QRect &targetRect, const QPlatformMenuItem *item)
393{
394 const QGtk3MenuItem *menuItem = static_cast<const QGtk3MenuItem *>(item);
395 if (menuItem)
396 gtk_menu_shell_select_item(GTK_MENU_SHELL(m_menu), menu_item: menuItem->handle());
397
398 m_targetPos = QPoint(targetRect.x(), targetRect.y() + targetRect.height());
399
400 QPlatformWindow *pw = parentWindow ? parentWindow->handle() : nullptr;
401 if (pw)
402 m_targetPos = pw->mapToGlobal(pos: m_targetPos);
403
404 gtk_menu_popup(GTK_MENU(m_menu), parent_menu_shell: nullptr, parent_menu_item: nullptr, func: qt_gtk_menu_position_func, data: this, button: 0, activate_time: gtk_get_current_event_time());
405}
406
407void QGtk3Menu::dismiss()
408{
409 gtk_menu_popdown(GTK_MENU(m_menu));
410}
411
412QPlatformMenuItem *QGtk3Menu::menuItemAt(int position) const
413{
414 return m_items.value(i: position);
415}
416
417QPlatformMenuItem *QGtk3Menu::menuItemForTag(quintptr tag) const
418{
419 for (QGtk3MenuItem *item : m_items) {
420 if (item->tag() == tag)
421 return item;
422 }
423 return nullptr;
424}
425
426QPlatformMenuItem *QGtk3Menu::createMenuItem() const
427{
428 return new QGtk3MenuItem;
429}
430
431QPlatformMenu *QGtk3Menu::createSubMenu() const
432{
433 return new QGtk3Menu;
434}
435
436void QGtk3Menu::onShow(GtkWidget *, void *data)
437{
438 QGtk3Menu *menu = static_cast<QGtk3Menu *>(data);
439 if (menu)
440 emit menu->aboutToShow();
441}
442
443void QGtk3Menu::onHide(GtkWidget *, void *data)
444{
445 QGtk3Menu *menu = static_cast<QGtk3Menu *>(data);
446 if (menu)
447 emit menu->aboutToHide();
448}
449
450QT_END_NAMESPACE
451
452#include "moc_qgtk3menu.cpp"
453

source code of qtbase/src/plugins/platformthemes/gtk3/qgtk3menu.cpp