Native context menu on macOS
-
Hi, I am new to this forum and I want to bring you this thing regarding the context menus.
I wanted to use native menus on macOS. Just I prefer context menus with native look and feel.
Currently up to 6.7 the only widget that has implemented this capability is QComboBox, using a proxy style and QStyle::SH_ComboBox_UseNativePopup, it works perfectly!
I went to look at the Qt source to figure it. One problem is documented in the source, native menus (NSMenu) alter Qt signals and events. I was able to implement native menus using Objective-C in cpp, with few calls to the native API.
So I managed to find a compromise. The goal was to use native menus without disrupt my code, cross-platform. Unfortunately the matter becomes more complicated for example if you want to use the native context menu also on QLineEdit selection, or in widgets with the persistent editor.
My intent was to use native menus in any widget where menu appears.
I have a QTreeWidget with Drag and Drop and the native context menu interferes. Until version 6.6 it was sufficient to send a native mouse release event to make it just work. Since version 6.7 there is no way to stop them interfering.
I tried several things, the Qt::WA_TransparentForMouseEvents attribute, different types of signals sent to QCoreApplication, tried using QObject::blockSignals, also disabling updates for QTreeWidget using QWidget::setUpdatesEnabled set to false (in the latter case QTreeWidget disappears completely).
The only solution that seems to work consist to disable the widget and re-enable it after the native context menu popup. But this is not a really usable solution, the widget undergoes all updates, is visually disabled, with many repaints.
Currently I disable Drag and Drop when QWidget is QAbstractItemView, from which QTreeWidget and QListWidget descend. A smelly solution.
Do you have any idea to get around the problem?
This is a code sample:
#import <AppKit/AppKit.h> #include <QApplication> #include <QWidget> #include <QWindow> #include <QMenu> #include <QTimer> #include <QGridLayout> #include <QTreeWidget> #include <QList> int main (int argc, char *argv[]) { QApplication *app = new QApplication(argc, argv); QWidget *mwid = new QWidget; QGridLayout *frm = new QGridLayout(mwid); QTreeWidget *tree = new QTreeWidget(); tree->setUniformRowHeights(true); tree->setSelectionBehavior(QTreeWidget::SelectRows); tree->setSelectionMode(QTreeWidget::ExtendedSelection); tree->setItemsExpandable(false); tree->setExpandsOnDoubleClick(false); tree->setDragDropMode(QTreeWidget::InternalMove); tree->setDefaultDropAction(Qt::MoveAction); tree->setDropIndicatorShown(true); tree->setEditTriggers(QTreeWidget::NoEditTriggers); tree->setRootIsDecorated(false); tree->setContextMenuPolicy(Qt::CustomContextMenu); tree->setHeaderLabels({"Col 1", "Col 2"}); QList<QTreeWidgetItem*> items; for (int i = 0; i < 26; i++) { QTreeWidgetItem *item = new QTreeWidgetItem(QStringList({QString("item %1").arg(QChar(i + 65)), "1901-01-01"})); item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemNeverHasChildren); items.append(item); } tree->addTopLevelItems(items); frm->addWidget(tree); QWidget *widget = tree; QMenu* menu = new QMenu(); menu->addAction("&Edit"); menu->addSeparator(); menu->addAction("Cu&t", QKeySequence::Cut); menu->addAction("&Copy", QKeySequence::Copy); menu->addAction("&Paste", QKeySequence::Paste); // reusable function QApplication::connect(tree, &QTreeWidget::customContextMenuRequested, [=](QPoint pos) { QWidget *top = widget->window(); QWindow *tlw = top->windowHandle(); // get the main native NSView (qnsview) NSView *view = (NSView*)top->winId(); // get a native menu NSMenu NSMenu *nsMenu = menu->toNSMenu(); // need to convert position QPoint globalPos = widget->mapTo(top, pos); NSPoint nsPos = NSMakePoint(globalPos.x(), globalPos.y()); nsPos = [view convertPoint:nsPos toView:nil]; // context menu interfering with Drag and Drop // in Qt >= 6.7 // temp workaround to disallow DND #if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) struct dnd { bool dragEnabled = false; QAbstractItemView::DragDropMode dragDropMode = QAbstractItemView::NoDragDrop; bool showDropIndicator = false; bool acceptDrops = false; } dndState; if (QAbstractItemView *wid = qobject_cast<QAbstractItemView*>(widget)) { dndState.dragEnabled = wid->dragEnabled(); dndState.dragDropMode = wid->dragDropMode(); dndState.showDropIndicator = wid->showDropIndicator(); dndState.acceptDrops = wid->acceptDrops(); wid->setDragEnabled(false); wid->setDragDropMode(QAbstractItemView::NoDragDrop); wid->setDropIndicatorShown(false); wid->setAcceptDrops(false); } #endif // signal emitter menu->aboutToShow(); NSEvent *nsEventMenuShow = [NSEvent mouseEventWithType:NSEventTypeRightMouseDown location:nsPos modifierFlags:0 timestamp:0 windowNumber:view ? view.window.windowNumber : 0 context:nil eventNumber:0 clickCount:1 pressure:1.0 ]; // inner items disabled when modal // a workaround to set enabled state on each item if (tlw != nullptr && tlw->type() != Qt::Window) { [nsMenu setAutoenablesItems:FALSE]; int i = 0; for (auto & item : menu->actions()) { NSMenuItem *nsMenuItem = [nsMenu itemAtIndex:i++]; [nsMenuItem setEnabled:(item->isEnabled())]; } } // native menu is blocking [NSMenu popUpContextMenu:nsMenu withEvent:nsEventMenuShow forView:view]; // native menu alters Qt QApplication mouse events // send mouse release with native events NSEvent *nsEventMouseRelease = [NSEvent mouseEventWithType:NSEventTypeRightMouseUp location:nsPos modifierFlags:0 timestamp:0 windowNumber:view ? view.window.windowNumber : 0 context:nil eventNumber:0 clickCount:1 pressure:1.0 ]; [view mouseUp:nsEventMouseRelease]; // signal emitter menu->aboutToHide(); // context menu interfering with Drag and Drop // in Qt >= 6.7 // temp workaround to re-allow DND #if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) if (QAbstractItemView *wid = qobject_cast<QAbstractItemView*>(widget)) { // delay to zero needed QTimer::singleShot(0, [=]() { wid->setDragEnabled(dndState.dragEnabled); wid->setDragDropMode(dndState.dragDropMode); wid->setDropIndicatorShown(dndState.showDropIndicator); wid->setAcceptDrops(dndState.acceptDrops); }); } #endif }); mwid->show(); return app->exec(); }