From ac8a1787eb74d374a346ae5982d0ea361747729e Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 May 2016 14:51:55 +0200 Subject: Implement QPA menu for GTK+ 3.x QGtk3Menu provides native stand-alone context menus, which is not covered by the recently introduced QDBusPlatformMenu (limited to global menubars and system trays). Change-Id: Ida42ebaf69444e6b3150dc0ccf9b44e80cf71c20 Reviewed-by: Frederik Gladhorn --- src/plugins/platformthemes/gtk3/gtk3.pro | 2 + src/plugins/platformthemes/gtk3/qgtk3menu.cpp | 489 +++++++++++++++++++++++++ src/plugins/platformthemes/gtk3/qgtk3menu.h | 162 ++++++++ src/plugins/platformthemes/gtk3/qgtk3theme.cpp | 11 + src/plugins/platformthemes/gtk3/qgtk3theme.h | 3 + 5 files changed, 667 insertions(+) create mode 100644 src/plugins/platformthemes/gtk3/qgtk3menu.cpp create mode 100644 src/plugins/platformthemes/gtk3/qgtk3menu.h (limited to 'src/plugins/platformthemes') diff --git a/src/plugins/platformthemes/gtk3/gtk3.pro b/src/plugins/platformthemes/gtk3/gtk3.pro index 557918c5a4..72a33efeac 100644 --- a/src/plugins/platformthemes/gtk3/gtk3.pro +++ b/src/plugins/platformthemes/gtk3/gtk3.pro @@ -13,9 +13,11 @@ LIBS += $$QMAKE_LIBS_GTK3 HEADERS += \ qgtk3dialoghelpers.h \ + qgtk3menu.h \ qgtk3theme.h SOURCES += \ main.cpp \ qgtk3dialoghelpers.cpp \ + qgtk3menu.cpp \ qgtk3theme.cpp diff --git a/src/plugins/platformthemes/gtk3/qgtk3menu.cpp b/src/plugins/platformthemes/gtk3/qgtk3menu.cpp new file mode 100644 index 0000000000..288978ae84 --- /dev/null +++ b/src/plugins/platformthemes/gtk3/qgtk3menu.cpp @@ -0,0 +1,489 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the plugins of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qgtk3menu.h" + +#include +#include + +#undef signals +#include + +QT_BEGIN_NAMESPACE + +static guint qt_gdkKey(const QKeySequence &shortcut) +{ + if (shortcut.isEmpty()) + return 0; + + // TODO: proper mapping + Qt::KeyboardModifiers mods = Qt::ShiftModifier | Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier; + return (shortcut[0] ^ mods) & shortcut[0]; +} + +static GdkModifierType qt_gdkModifiers(const QKeySequence &shortcut) +{ + if (shortcut.isEmpty()) + return GdkModifierType(0); + + guint mods = 0; + int m = shortcut[0]; + if (m & Qt::ShiftModifier) + mods |= GDK_SHIFT_MASK; + if (m & Qt::ControlModifier) + mods |= GDK_CONTROL_MASK; + if (m & Qt::AltModifier) + mods |= GDK_MOD1_MASK; + if (m & Qt::MetaModifier) + mods |= GDK_META_MASK; + + return static_cast(mods); +} + +QGtk3MenuItem::QGtk3MenuItem() + : m_visible(true), + m_separator(false), + m_checkable(false), + m_checked(false), + m_enabled(true), + m_underline(false), + m_invalid(true), + m_tag(reinterpret_cast(this)), + m_menu(nullptr), + m_item(nullptr) +{ +} + +QGtk3MenuItem::~QGtk3MenuItem() +{ +} + +bool QGtk3MenuItem::isInvalid() const +{ + return m_invalid; +} + +GtkWidget *QGtk3MenuItem::create() +{ + if (m_invalid) { + if (m_item) { + gtk_widget_destroy(m_item); + m_item = nullptr; + } + m_invalid = false; + } + + if (!m_item) { + if (m_separator) { + m_item = gtk_separator_menu_item_new(); + } else { + if (m_checkable) { + m_item = gtk_check_menu_item_new(); + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(m_item), m_checked); + g_signal_connect(m_item, "toggled", G_CALLBACK(onToggle), this); + } else { + m_item = gtk_menu_item_new(); + g_signal_connect(m_item, "activate", G_CALLBACK(onActivate), this); + } + gtk_menu_item_set_label(GTK_MENU_ITEM(m_item), m_text.toUtf8()); + gtk_menu_item_set_use_underline(GTK_MENU_ITEM(m_item), m_underline); + if (m_menu) + gtk_menu_item_set_submenu(GTK_MENU_ITEM(m_item), m_menu->handle()); + g_signal_connect(m_item, "select", G_CALLBACK(onSelect), this); + if (!m_shortcut.isEmpty()) { + GtkWidget *label = gtk_bin_get_child(GTK_BIN(m_item)); + gtk_accel_label_set_accel(GTK_ACCEL_LABEL(label), qt_gdkKey(m_shortcut), qt_gdkModifiers(m_shortcut)); + } + } + gtk_widget_set_sensitive(m_item, m_enabled); + gtk_widget_set_visible(m_item, m_visible); + if (GTK_IS_CHECK_MENU_ITEM(m_item)) + g_object_set(m_item, "draw-as-radio", m_exclusive, NULL); + } + + return m_item; +} + +GtkWidget *QGtk3MenuItem::handle() const +{ + return m_item; +} + +quintptr QGtk3MenuItem::tag() const +{ + return m_tag; +} + +void QGtk3MenuItem::setTag(quintptr tag) +{ + m_tag = tag; +} + +QString QGtk3MenuItem::text() const +{ + return m_text; +} + +static QString convertMnemonics(QString text, bool *found) +{ + *found = false; + + int i = text.length() - 1; + while (i >= 0) { + const QChar c = text.at(i); + if (c == QLatin1Char('&')) { + if (i == 0 || text.at(i - 1) != QLatin1Char('&')) { + // convert Qt to GTK mnemonic + if (i < text.length() - 1 && !text.at(i + 1).isSpace()) { + text.replace(i, 1, QLatin1Char('_')); + *found = true; + } + } else if (text.at(i - 1) == QLatin1Char('&')) { + // unescape ampersand + text.replace(--i, 2, QLatin1Char('&')); + } + } else if (c == QLatin1Char('_')) { + // escape GTK mnemonic + text.insert(i, QLatin1Char('_')); + } + --i; + } + + return text; +} + +void QGtk3MenuItem::setText(const QString &text) +{ + m_text = convertMnemonics(text, &m_underline); + if (GTK_IS_MENU_ITEM(m_item)) { + gtk_menu_item_set_label(GTK_MENU_ITEM(m_item), m_text.toUtf8()); + gtk_menu_item_set_use_underline(GTK_MENU_ITEM(m_item), m_underline); + } +} + +QGtk3Menu *QGtk3MenuItem::menu() const +{ + return m_menu; +} + +void QGtk3MenuItem::setMenu(QPlatformMenu *menu) +{ + m_menu = qobject_cast(menu); + if (GTK_IS_MENU_ITEM(m_item)) + gtk_menu_item_set_submenu(GTK_MENU_ITEM(m_item), m_menu ? m_menu->handle() : NULL); +} + +bool QGtk3MenuItem::isVisible() const +{ + return m_visible; +} + +void QGtk3MenuItem::setVisible(bool visible) +{ + if (m_visible == visible) + return; + + m_visible = visible; + if (GTK_IS_MENU_ITEM(m_item)) + gtk_widget_set_visible(m_item, visible); +} + +bool QGtk3MenuItem::isSeparator() const +{ + return m_separator; +} + +void QGtk3MenuItem::setIsSeparator(bool separator) +{ + if (m_separator == separator) + return; + + m_invalid = true; + m_separator = separator; +} + +bool QGtk3MenuItem::isCheckable() const +{ + return m_checkable; +} + +void QGtk3MenuItem::setCheckable(bool checkable) +{ + if (m_checkable == checkable) + return; + + m_invalid = true; + m_checkable = checkable; +} + +bool QGtk3MenuItem::isChecked() const +{ + return m_checked; +} + +void QGtk3MenuItem::setChecked(bool checked) +{ + if (m_checked == checked) + return; + + m_checked = checked; + if (GTK_IS_CHECK_MENU_ITEM(m_item)) + gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(m_item), checked); +} + +QKeySequence QGtk3MenuItem::shortcut() const +{ + return m_shortcut; +} + +void QGtk3MenuItem::setShortcut(const QKeySequence& shortcut) +{ + if (m_shortcut == shortcut) + return; + + m_shortcut = shortcut; + if (GTK_IS_MENU_ITEM(m_item)) { + GtkWidget *label = gtk_bin_get_child(GTK_BIN(m_item)); + gtk_accel_label_set_accel(GTK_ACCEL_LABEL(label), qt_gdkKey(m_shortcut), qt_gdkModifiers(m_shortcut)); + } +} + +bool QGtk3MenuItem::isEnabled() const +{ + return m_enabled; +} + +void QGtk3MenuItem::setEnabled(bool enabled) +{ + if (m_enabled == enabled) + return; + + m_enabled = enabled; + if (m_item) + gtk_widget_set_sensitive(m_item, enabled); +} + +bool QGtk3MenuItem::hasExclusiveGroup() const +{ + return m_exclusive; +} + +void QGtk3MenuItem::setHasExclusiveGroup(bool exclusive) +{ + if (m_exclusive == exclusive) + return; + + m_exclusive = exclusive; + if (GTK_IS_CHECK_MENU_ITEM(m_item)) + g_object_set(m_item, "draw-as-radio", exclusive, NULL); +} + +void QGtk3MenuItem::onSelect(GtkMenuItem *, void *data) +{ + QGtk3MenuItem *item = static_cast(data); + if (item) + emit item->hovered(); +} + +void QGtk3MenuItem::onActivate(GtkMenuItem *, void *data) +{ + QGtk3MenuItem *item = static_cast(data); + if (item) + emit item->activated(); +} + +void QGtk3MenuItem::onToggle(GtkCheckMenuItem *check, void *data) +{ + QGtk3MenuItem *item = static_cast(data); + if (item) { + bool active = gtk_check_menu_item_get_active(check); + if (active != item->isChecked()) { + item->setChecked(active); + emit item->activated(); + } + } +} + +QGtk3Menu::QGtk3Menu() + : m_tag(reinterpret_cast(this)) +{ + m_menu = gtk_menu_new(); + + g_signal_connect(m_menu, "show", G_CALLBACK(onShow), this); + g_signal_connect(m_menu, "hide", G_CALLBACK(onHide), this); +} + +QGtk3Menu::~QGtk3Menu() +{ + if (GTK_IS_WIDGET(m_menu)) + gtk_widget_destroy(m_menu); +} + +GtkWidget *QGtk3Menu::handle() const +{ + return m_menu; +} + +void QGtk3Menu::insertMenuItem(QPlatformMenuItem *item, QPlatformMenuItem *before) +{ + QGtk3MenuItem *gitem = static_cast(item); + if (!gitem || m_items.contains(gitem)) + return; + + GtkWidget *handle = gitem->create(); + int index = m_items.indexOf(static_cast(before)); + if (index < 0) + index = m_items.count(); + m_items.insert(index, gitem); + gtk_menu_shell_insert(GTK_MENU_SHELL(m_menu), handle, index); +} + +void QGtk3Menu::removeMenuItem(QPlatformMenuItem *item) +{ + QGtk3MenuItem *gitem = static_cast(item); + if (!gitem && !m_items.removeOne(gitem)) + return; + + GtkWidget *handle = gitem->handle(); + if (handle) + gtk_container_remove(GTK_CONTAINER(m_menu), handle); +} + +void QGtk3Menu::syncMenuItem(QPlatformMenuItem *item) +{ + QGtk3MenuItem *gitem = static_cast(item); + int index = m_items.indexOf(gitem); + if (index == -1 || !gitem->isInvalid()) + return; + + GtkWidget *handle = gitem->create(); + if (handle) + gtk_menu_shell_insert(GTK_MENU_SHELL(m_menu), handle, index); +} + +void QGtk3Menu::syncSeparatorsCollapsible(bool enable) +{ + Q_UNUSED(enable); +} + +quintptr QGtk3Menu::tag() const +{ + return m_tag; +} + +void QGtk3Menu::setTag(quintptr tag) +{ + m_tag = tag; +} + +void QGtk3Menu::setEnabled(bool enabled) +{ + gtk_widget_set_sensitive(m_menu, enabled); +} + +void QGtk3Menu::setVisible(bool visible) +{ + gtk_widget_set_visible(m_menu, visible); +} + +static void qt_gtk_menu_position_func(GtkMenu *, gint *x, gint *y, gboolean *push_in, gpointer data) +{ + QGtk3Menu *menu = static_cast(data); + QPoint targetPos = menu->targetPos(); + *x = targetPos.x(); + *y = targetPos.y(); + *push_in = true; +} + +QPoint QGtk3Menu::targetPos() const +{ + return m_targetPos; +} + +void QGtk3Menu::showPopup(const QWindow *parentWindow, const QRect &targetRect, const QPlatformMenuItem *item) +{ + int index = m_items.indexOf(static_cast(const_cast(item))); + if (index != -1) + gtk_menu_set_active(GTK_MENU(m_menu), index); + + m_targetPos = targetRect.bottomLeft(); + if (parentWindow) + m_targetPos = parentWindow->mapToGlobal(m_targetPos); + + gtk_menu_popup(GTK_MENU(m_menu), NULL, NULL, qt_gtk_menu_position_func, this, 0, gtk_get_current_event_time()); +} + +void QGtk3Menu::dismiss() +{ + gtk_menu_popdown(GTK_MENU(m_menu)); +} + +QPlatformMenuItem *QGtk3Menu::menuItemAt(int position) const +{ + return m_items.value(position); +} + +QPlatformMenuItem *QGtk3Menu::menuItemForTag(quintptr tag) const +{ + for (QGtk3MenuItem *item : m_items) { + if (item->tag() == tag) + return item; + } + return nullptr; +} + +QPlatformMenuItem *QGtk3Menu::createMenuItem() const +{ + return new QGtk3MenuItem; +} + +QPlatformMenu *QGtk3Menu::createSubMenu() const +{ + return new QGtk3Menu; +} + +void QGtk3Menu::onShow(GtkWidget *, void *data) +{ + QGtk3Menu *menu = static_cast(data); + if (menu) + emit menu->aboutToShow(); +} + +void QGtk3Menu::onHide(GtkWidget *, void *data) +{ + QGtk3Menu *menu = static_cast(data); + if (menu) + emit menu->aboutToHide(); +} + +QT_END_NAMESPACE diff --git a/src/plugins/platformthemes/gtk3/qgtk3menu.h b/src/plugins/platformthemes/gtk3/qgtk3menu.h new file mode 100644 index 0000000000..ad108f1218 --- /dev/null +++ b/src/plugins/platformthemes/gtk3/qgtk3menu.h @@ -0,0 +1,162 @@ +/**************************************************************************** +** +** Copyright (C) 2016 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the plugins of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** As a special exception, The Qt Company gives you certain additional +** rights. These rights are described in The Qt Company LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QGTK3MENU_H +#define QGTK3MENU_H + +#include + +typedef struct _GtkWidget GtkWidget; +typedef struct _GtkMenuItem GtkMenuItem; +typedef struct _GtkCheckMenuItem GtkCheckMenuItem; + +QT_BEGIN_NAMESPACE + +class QGtk3Menu; + +class QGtk3MenuItem: public QPlatformMenuItem +{ +public: + QGtk3MenuItem(); + ~QGtk3MenuItem(); + + bool isInvalid() const; + + GtkWidget *create(); + GtkWidget *handle() const; + + quintptr tag() const; + void setTag(quintptr tag) override; + + QString text() const; + void setText(const QString &text) override; + + QGtk3Menu *menu() const; + void setMenu(QPlatformMenu *menu) override; + + bool isVisible() const; + void setVisible(bool visible) override; + + bool isSeparator() const; + void setIsSeparator(bool separator) override; + + bool isCheckable() const; + void setCheckable(bool checkable) override; + + bool isChecked() const; + void setChecked(bool checked) override; + + QKeySequence shortcut() const; + void setShortcut(const QKeySequence &shortcut) override; + + bool isEnabled() const; + void setEnabled(bool enabled) override; + + bool hasExclusiveGroup() const; + void setHasExclusiveGroup(bool exclusive) override; + + void setRole(MenuRole role) override { Q_UNUSED(role); } + void setFont(const QFont &font) override { Q_UNUSED(font); } + void setIcon(const QIcon &icon) override { Q_UNUSED(icon); } + void setIconSize(int size) override { Q_UNUSED(size); } + +protected: + static void onSelect(GtkMenuItem *item, void *data); + static void onActivate(GtkMenuItem *item, void *data); + static void onToggle(GtkCheckMenuItem *item, void *data); + +private: + bool m_visible; + bool m_separator; + bool m_checkable; + bool m_checked; + bool m_enabled; + bool m_exclusive; + bool m_underline; + bool m_invalid; + quintptr m_tag; + QGtk3Menu *m_menu; + GtkWidget *m_item; + QString m_text; + QKeySequence m_shortcut; +}; + +class QGtk3Menu : public QPlatformMenu +{ + Q_OBJECT + +public: + QGtk3Menu(); + ~QGtk3Menu(); + + GtkWidget *handle() const; + + void insertMenuItem(QPlatformMenuItem *item, QPlatformMenuItem *before) override; + void removeMenuItem(QPlatformMenuItem *item) override; + void syncMenuItem(QPlatformMenuItem *item) override; + void syncSeparatorsCollapsible(bool enable) override; + + quintptr tag() const override; + void setTag(quintptr tag) override; + + void setEnabled(bool enabled) override; + void setVisible(bool visible) override; + + void setIcon(const QIcon &icon) override { Q_UNUSED(icon); } + void setText(const QString &text) override { Q_UNUSED(text); } + + QPoint targetPos() const; + + void showPopup(const QWindow *parentWindow, const QRect &targetRect, const QPlatformMenuItem *item) override; + void dismiss() override; + + QPlatformMenuItem *menuItemAt(int position) const override; + QPlatformMenuItem *menuItemForTag(quintptr tag) const override; + + QPlatformMenuItem *createMenuItem() const override; + QPlatformMenu *createSubMenu() const override; + +protected: + static void onShow(GtkWidget *menu, void *data); + static void onHide(GtkWidget *menu, void *data); + +private: + quintptr m_tag; + GtkWidget *m_menu; + QPoint m_targetPos; + QVector m_items; +}; + +QT_END_NAMESPACE + +#endif // QGTK3MENU_H diff --git a/src/plugins/platformthemes/gtk3/qgtk3theme.cpp b/src/plugins/platformthemes/gtk3/qgtk3theme.cpp index 65bec3dbd1..6df631bff3 100644 --- a/src/plugins/platformthemes/gtk3/qgtk3theme.cpp +++ b/src/plugins/platformthemes/gtk3/qgtk3theme.cpp @@ -39,6 +39,7 @@ #include "qgtk3theme.h" #include "qgtk3dialoghelpers.h" +#include "qgtk3menu.h" #include #undef signals @@ -142,4 +143,14 @@ QPlatformDialogHelper *QGtk3Theme::createPlatformDialogHelper(DialogType type) c } } +QPlatformMenu* QGtk3Theme::createPlatformMenu() const +{ + return new QGtk3Menu; +} + +QPlatformMenuItem* QGtk3Theme::createPlatformMenuItem() const +{ + return new QGtk3MenuItem; +} + QT_END_NAMESPACE diff --git a/src/plugins/platformthemes/gtk3/qgtk3theme.h b/src/plugins/platformthemes/gtk3/qgtk3theme.h index 8bb9adeab8..52036680c6 100644 --- a/src/plugins/platformthemes/gtk3/qgtk3theme.h +++ b/src/plugins/platformthemes/gtk3/qgtk3theme.h @@ -55,6 +55,9 @@ public: bool usePlatformNativeDialog(DialogType type) const Q_DECL_OVERRIDE; QPlatformDialogHelper *createPlatformDialogHelper(DialogType type) const Q_DECL_OVERRIDE; + QPlatformMenu* createPlatformMenu() const Q_DECL_OVERRIDE; + QPlatformMenuItem* createPlatformMenuItem() const Q_DECL_OVERRIDE; + static const char *name; }; -- cgit v1.2.3