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 | |
13 | QT_BEGIN_NAMESPACE |
14 | |
15 | #if QT_CONFIG(shortcut) |
16 | static 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 | |
26 | static 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 | |
46 | 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 | |
60 | QGtk3MenuItem::() |
61 | { |
62 | } |
63 | |
64 | bool QGtk3MenuItem::() const |
65 | { |
66 | return m_invalid; |
67 | } |
68 | |
69 | GtkWidget *QGtk3MenuItem::() |
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 | |
112 | GtkWidget *QGtk3MenuItem::handle() const |
113 | { |
114 | return m_item; |
115 | } |
116 | |
117 | QString QGtk3MenuItem::() const |
118 | { |
119 | return m_text; |
120 | } |
121 | |
122 | static 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 | |
150 | void QGtk3MenuItem::(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 | |
159 | QGtk3Menu *QGtk3MenuItem::() const |
160 | { |
161 | return m_menu; |
162 | } |
163 | |
164 | void QGtk3MenuItem::(QPlatformMenu *) |
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 | |
171 | bool QGtk3MenuItem::() const |
172 | { |
173 | return m_visible; |
174 | } |
175 | |
176 | void QGtk3MenuItem::(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 | |
186 | bool QGtk3MenuItem::() const |
187 | { |
188 | return m_separator; |
189 | } |
190 | |
191 | void QGtk3MenuItem::(bool separator) |
192 | { |
193 | if (m_separator == separator) |
194 | return; |
195 | |
196 | m_invalid = true; |
197 | m_separator = separator; |
198 | } |
199 | |
200 | bool QGtk3MenuItem::() const |
201 | { |
202 | return m_checkable; |
203 | } |
204 | |
205 | void QGtk3MenuItem::(bool checkable) |
206 | { |
207 | if (m_checkable == checkable) |
208 | return; |
209 | |
210 | m_invalid = true; |
211 | m_checkable = checkable; |
212 | } |
213 | |
214 | bool QGtk3MenuItem::() const |
215 | { |
216 | return m_checked; |
217 | } |
218 | |
219 | void QGtk3MenuItem::(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) |
230 | QKeySequence QGtk3MenuItem::() const |
231 | { |
232 | return m_shortcut; |
233 | } |
234 | |
235 | void QGtk3MenuItem::(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 | |
248 | bool QGtk3MenuItem::() const |
249 | { |
250 | return m_enabled; |
251 | } |
252 | |
253 | void QGtk3MenuItem::(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 | |
263 | bool QGtk3MenuItem::() const |
264 | { |
265 | return m_exclusive; |
266 | } |
267 | |
268 | void QGtk3MenuItem::(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 | |
278 | void QGtk3MenuItem::(GtkMenuItem *, void *data) |
279 | { |
280 | QGtk3MenuItem *item = static_cast<QGtk3MenuItem *>(data); |
281 | if (item) |
282 | emit item->hovered(); |
283 | } |
284 | |
285 | void QGtk3MenuItem::(GtkMenuItem *, void *data) |
286 | { |
287 | QGtk3MenuItem *item = static_cast<QGtk3MenuItem *>(data); |
288 | if (item) |
289 | emit item->activated(); |
290 | } |
291 | |
292 | void QGtk3MenuItem::(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 | |
304 | 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 | |
312 | QGtk3Menu::() |
313 | { |
314 | if (GTK_IS_WIDGET(m_menu)) |
315 | gtk_widget_destroy(widget: m_menu); |
316 | } |
317 | |
318 | GtkWidget *QGtk3Menu::handle() const |
319 | { |
320 | return m_menu; |
321 | } |
322 | |
323 | void QGtk3Menu::(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 | |
337 | void QGtk3Menu::(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 | |
348 | void QGtk3Menu::(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 | |
360 | void QGtk3Menu::(bool enable) |
361 | { |
362 | Q_UNUSED(enable); |
363 | } |
364 | |
365 | void QGtk3Menu::(bool enabled) |
366 | { |
367 | gtk_widget_set_sensitive(widget: m_menu, sensitive: enabled); |
368 | } |
369 | |
370 | void QGtk3Menu::(bool visible) |
371 | { |
372 | gtk_widget_set_visible(widget: m_menu, visible); |
373 | } |
374 | |
375 | static void (GtkMenu *, gint *x, gint *y, gboolean *push_in, gpointer data) |
376 | { |
377 | QGtk3Menu * = 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 | |
387 | QPoint QGtk3Menu::() const |
388 | { |
389 | return m_targetPos; |
390 | } |
391 | |
392 | void QGtk3Menu::(const QWindow *parentWindow, const QRect &targetRect, const QPlatformMenuItem *item) |
393 | { |
394 | const QGtk3MenuItem * = 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 | |
407 | void QGtk3Menu::() |
408 | { |
409 | gtk_menu_popdown(GTK_MENU(m_menu)); |
410 | } |
411 | |
412 | QPlatformMenuItem *QGtk3Menu::(int position) const |
413 | { |
414 | return m_items.value(i: position); |
415 | } |
416 | |
417 | QPlatformMenuItem *QGtk3Menu::(quintptr tag) const |
418 | { |
419 | for (QGtk3MenuItem *item : m_items) { |
420 | if (item->tag() == tag) |
421 | return item; |
422 | } |
423 | return nullptr; |
424 | } |
425 | |
426 | QPlatformMenuItem *QGtk3Menu::() const |
427 | { |
428 | return new QGtk3MenuItem; |
429 | } |
430 | |
431 | QPlatformMenu *QGtk3Menu::() const |
432 | { |
433 | return new QGtk3Menu; |
434 | } |
435 | |
436 | void QGtk3Menu::(GtkWidget *, void *data) |
437 | { |
438 | QGtk3Menu * = static_cast<QGtk3Menu *>(data); |
439 | if (menu) |
440 | emit menu->aboutToShow(); |
441 | } |
442 | |
443 | void QGtk3Menu::(GtkWidget *, void *data) |
444 | { |
445 | QGtk3Menu * = static_cast<QGtk3Menu *>(data); |
446 | if (menu) |
447 | emit menu->aboutToHide(); |
448 | } |
449 | |
450 | QT_END_NAMESPACE |
451 | |
452 | #include "moc_qgtk3menu.cpp" |
453 | |