Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qiosmenu.mm
Go to the documentation of this file.
1// Copyright (C) 2016 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// Qt-Security score:significant reason:default
4
5#include <qglobal.h>
6#include <qguiapplication.h>
7#include <qpa/qplatformtheme.h>
8
9#include "qiosglobal.h"
10#include "qiosmenu.h"
11#include "qioswindow.h"
15
16#include <algorithm>
17#include <iterator>
18
19// m_currentMenu points to the currently visible menu.
20// Only one menu will be visible at a time, and if a second menu
21// is shown on top of a first, the first one will be told to hide.
22QIOSMenu *QIOSMenu::m_currentMenu = nullptr;
23
24// -------------------------------------------------------------------------
25
26static NSString *const kSelectorPrefix = @"_qtMenuItem_";
27
28@interface QUIMenuController : UIResponder
29@end
30
31@implementation QUIMenuController {
32 QIOSMenuItemList m_visibleMenuItems;
33}
34
35- (instancetype)initWithVisibleMenuItems:(const QIOSMenuItemList &)visibleMenuItems
36{
37 if (self = [super init]) {
38 [self setVisibleMenuItems:visibleMenuItems];
39 [[NSNotificationCenter defaultCenter]
40 addObserver:self
41 selector:@selector(menuClosed)
42 name:UIMenuControllerDidHideMenuNotification object:nil];
43 }
44
45 return self;
46}
47
48- (void)dealloc
49{
50 [[NSNotificationCenter defaultCenter]
51 removeObserver:self
52 name:UIMenuControllerDidHideMenuNotification object:nil];
53 [super dealloc];
54}
55
56- (void)setVisibleMenuItems:(const QIOSMenuItemList &)visibleMenuItems
57{
58 m_visibleMenuItems = visibleMenuItems;
59 NSMutableArray<UIMenuItem *> *menuItemArray = [NSMutableArray<UIMenuItem *> arrayWithCapacity:m_visibleMenuItems.size()];
60 // Create an array of UIMenuItems, one for each visible QIOSMenuItem. Each
61 // UIMenuItem needs a callback assigned, so we assign one of the placeholder methods
62 // added to UIWindow (QIOSMenuActionTargets) below. Each method knows its own index, which
63 // corresponds to the index of the corresponding QIOSMenuItem in m_visibleMenuItems. When
64 // triggered, menuItemActionCallback will end up being called.
65 for (int i = 0; i < m_visibleMenuItems.count(); ++i) {
66 QIOSMenuItem *item = m_visibleMenuItems.at(i);
67 SEL sel = NSSelectorFromString([NSString stringWithFormat:@"%@%i:", kSelectorPrefix, i]);
68 [menuItemArray addObject:[[[UIMenuItem alloc] initWithTitle:item->m_text.toNSString() action:sel] autorelease]];
69 }
70 [UIMenuController sharedMenuController].menuItems = menuItemArray;
71 if ([UIMenuController sharedMenuController].menuVisible)
72 [[UIMenuController sharedMenuController] setMenuVisible:YES animated:NO];
73}
74
75- (void)menuClosed
76{
77 QIOSMenu::currentMenu()->dismiss();
78}
79
80- (id)targetForAction:(SEL)action withSender:(id)sender
81{
82 Q_UNUSED(sender);
83 BOOL containsPrefix = ([NSStringFromSelector(action) rangeOfString:kSelectorPrefix].location != NSNotFound);
84 return containsPrefix ? self : 0;
85}
86
87- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
88{
89 Q_UNUSED(selector);
90 // Just return a dummy signature that NSObject can create an NSInvocation from.
91 // We end up only checking selector in forwardInvocation anyway.
92 return [super methodSignatureForSelector:@selector(methodSignatureForSelector:)];
93}
94
95- (void)forwardInvocation:(NSInvocation *)invocation
96{
97 // Since none of the menu item selector methods actually exist, this function
98 // will end up being called as a final resort. We can then handle the action.
99 NSString *selector = NSStringFromSelector(invocation.selector);
100 NSRange range = NSMakeRange(kSelectorPrefix.length, selector.length - kSelectorPrefix.length - 1);
101 NSInteger selectedIndex = [[selector substringWithRange:range] integerValue];
102 QIOSMenu::currentMenu()->handleItemSelected(m_visibleMenuItems.at(selectedIndex));
103}
104
105@end
106
107// -------------------------------------------------------------------------
108
109@interface QUIPickerView : UIPickerView <UIPickerViewDelegate, UIPickerViewDataSource>
110
111@property(retain) UIToolbar *toolbar;
112
113@end
114
115@implementation QUIPickerView {
116 QIOSMenuItemList m_visibleMenuItems;
117 QPointer<QObject> m_focusObjectWithPickerView;
118 NSInteger m_selectedRow;
119}
120
121- (instancetype)initWithVisibleMenuItems:(const QIOSMenuItemList &)visibleMenuItems selectItem:(const QIOSMenuItem *)selectItem
122{
123 if (self = [super init]) {
124 [self setVisibleMenuItems:visibleMenuItems selectItem:selectItem];
125
126 self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
127 self.toolbar = [[[UIToolbar alloc] init] autorelease];
128 self.toolbar.frame.size = [self.toolbar sizeThatFits:self.bounds.size];
129 self.toolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
130
131 UIBarButtonItem *spaceButton = [[[UIBarButtonItem alloc]
132 initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
133 target:self action:@selector(closeMenu)] autorelease];
134 UIBarButtonItem *cancelButton = [[[UIBarButtonItem alloc]
135 initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
136 target:self action:@selector(cancelMenu)] autorelease];
137 UIBarButtonItem *doneButton = [[[UIBarButtonItem alloc]
138 initWithBarButtonSystemItem:UIBarButtonSystemItemDone
139 target:self action:@selector(closeMenu)] autorelease];
140 [self.toolbar setItems:@[cancelButton, spaceButton, doneButton]];
141
142 [self setDelegate:self];
143 [self setDataSource:self];
144 [self selectRow:m_selectedRow inComponent:0 animated:false];
145 [self listenForKeyboardWillHideNotification:YES];
146 }
147
148 return self;
149}
150
151- (void)setVisibleMenuItems:(const QIOSMenuItemList &)visibleMenuItems selectItem:(const QIOSMenuItem *)selectItem
152{
153 m_visibleMenuItems = visibleMenuItems;
154 m_selectedRow = visibleMenuItems.indexOf(const_cast<QIOSMenuItem *>(selectItem));
155 if (m_selectedRow == -1)
156 m_selectedRow = 0;
157 [self reloadAllComponents];
158}
159
160- (void)listenForKeyboardWillHideNotification:(BOOL)listen
161{
162 if (listen) {
163 [[NSNotificationCenter defaultCenter]
164 addObserver:self
165 selector:@selector(cancelMenu)
166 name:@"UIKeyboardWillHideNotification" object:nil];
167 } else {
168 [[NSNotificationCenter defaultCenter]
169 removeObserver:self
170 name:@"UIKeyboardWillHideNotification" object:nil];
171 }
172}
173
174- (void)dealloc
175{
176 [self listenForKeyboardWillHideNotification:NO];
177 self.toolbar = 0;
178 [super dealloc];
179}
180
181- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component
182{
183 Q_UNUSED(pickerView);
184 Q_UNUSED(component);
185 return m_visibleMenuItems.at(row)->m_text.toNSString();
186}
187
188- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
189{
190 Q_UNUSED(pickerView);
191 return 1;
192}
193
194- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
195{
196 Q_UNUSED(pickerView);
197 Q_UNUSED(component);
198 return m_visibleMenuItems.length();
199}
200
201- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
202{
203 Q_UNUSED(pickerView);
204 Q_UNUSED(component);
205 m_selectedRow = row;
206}
207
208- (void)closeMenu
209{
210 if (!m_visibleMenuItems.isEmpty())
211 QIOSMenu::currentMenu()->handleItemSelected(m_visibleMenuItems.at(m_selectedRow));
212 else
213 QIOSMenu::currentMenu()->dismiss();
214}
215
216- (void)cancelMenu
217{
218 QIOSMenu::currentMenu()->dismiss();
219}
220
221@end
222
223// -------------------------------------------------------------------------
224
225QIOSMenuItem::QIOSMenuItem()
226 : QPlatformMenuItem()
227 , m_visible(true)
228 , m_text(QString())
229 , m_role(MenuRole(0))
230 , m_enabled(true)
231 , m_separator(false)
232 , m_menu(0)
233{
234}
235
236void QIOSMenuItem::setText(const QString &text)
237{
238 m_text = QPlatformTheme::removeMnemonics(text);
239}
240
241void QIOSMenuItem::setMenu(QPlatformMenu *menu)
242{
243 m_menu = static_cast<QIOSMenu *>(menu);
244}
245
246void QIOSMenuItem::setVisible(bool isVisible)
247{
248 m_visible = isVisible;
249}
250
251void QIOSMenuItem::setIsSeparator(bool isSeparator)
252{
253 m_separator = isSeparator;
254}
255
256void QIOSMenuItem::setRole(QPlatformMenuItem::MenuRole role)
257{
258 m_role = role;
259}
260
261#ifndef QT_NO_SHORTCUT
262void QIOSMenuItem::setShortcut(const QKeySequence &sequence)
263{
264 m_shortcut = sequence;
265}
266#endif
267
268void QIOSMenuItem::setEnabled(bool enabled)
269{
270 m_enabled = enabled;
271}
272
273
275 : QPlatformMenu()
276 , m_enabled(true)
277 , m_visible(false)
278 , m_text(QString())
281 , m_parentWindow(0)
282 , m_targetItem(0)
284 , m_pickerView(0)
285{
286}
287
289{
290 dismiss();
291}
292
293void QIOSMenu::insertMenuItem(QPlatformMenuItem *menuItem, QPlatformMenuItem *before)
294{
295 if (!before) {
296 m_menuItems.append(static_cast<QIOSMenuItem *>(menuItem));
297 } else {
298 int index = m_menuItems.indexOf(static_cast<QIOSMenuItem *>(before)) + 1;
299 m_menuItems.insert(index, static_cast<QIOSMenuItem *>(menuItem));
300 }
301 if (m_currentMenu == this)
302 syncMenuItem(menuItem);
303}
304
305void QIOSMenu::removeMenuItem(QPlatformMenuItem *menuItem)
306{
307 m_menuItems.removeOne(static_cast<QIOSMenuItem *>(menuItem));
308 if (m_currentMenu == this)
309 syncMenuItem(menuItem);
310}
311
312void QIOSMenu::syncMenuItem(QPlatformMenuItem *)
313{
314 if (m_currentMenu != this)
315 return;
316
317 switch (m_effectiveMenuType) {
318 case EditMenu:
319 [m_menuController setVisibleMenuItems:filterFirstResponderActions(visibleMenuItems())];
320 break;
321 default:
322 [m_pickerView setVisibleMenuItems:visibleMenuItems() selectItem:m_targetItem];
323 break;
324 }
325}
326
327void QIOSMenu::setText(const QString &text)
328{
329 m_text = text;
330}
331
332void QIOSMenu::setEnabled(bool enabled)
333{
334 m_enabled = enabled;
335}
336
337void QIOSMenu::setVisible(bool visible)
338{
339 m_visible = visible;
340}
341
342void QIOSMenu::setMenuType(QPlatformMenu::MenuType type)
343{
344 m_menuType = type;
345}
346
348{
349 emit menuItem->activated();
350 dismiss();
351
352 if (QIOSMenu *menu = menuItem->m_menu) {
353 menu->setMenuType(m_effectiveMenuType);
354 menu->showPopup(m_parentWindow, m_targetRect, 0);
355 }
356}
357
358void QIOSMenu::showPopup(const QWindow *parentWindow, const QRect &targetRect, const QPlatformMenuItem *item)
359{
360 if (m_currentMenu == this || !parentWindow)
361 return;
362
363 emit aboutToShow();
364
365 m_parentWindow = const_cast<QWindow *>(parentWindow);
366 m_targetRect = targetRect;
367 m_targetItem = static_cast<const QIOSMenuItem *>(item);
368
369 if (!m_parentWindow->isActive())
370 m_parentWindow->requestActivate();
371
372 if (m_currentMenu && m_currentMenu != this)
373 m_currentMenu->dismiss();
374
375 m_currentMenu = this;
376 m_effectiveMenuType = m_menuType;
377 connect(qGuiApp, &QGuiApplication::focusObjectChanged, this, &QIOSMenu::dismiss);
378
379 switch (m_effectiveMenuType) {
380 case EditMenu:
381 toggleShowUsingUIMenuController(true);
382 break;
383 default:
384 toggleShowUsingUIPickerView(true);
385 break;
386 }
387
388 m_visible = true;
389}
390
392{
393 if (m_currentMenu != this)
394 return;
395
396 emit aboutToHide();
397
398 disconnect(qGuiApp, &QGuiApplication::focusObjectChanged, this, &QIOSMenu::dismiss);
399
400 switch (m_effectiveMenuType) {
401 case EditMenu:
402 toggleShowUsingUIMenuController(false);
403 break;
404 default:
405 toggleShowUsingUIPickerView(false);
406 break;
407 }
408
409 m_currentMenu = nullptr;
410 m_visible = false;
411}
412
413void QIOSMenu::toggleShowUsingUIMenuController(bool show)
414{
415 if (show) {
416 Q_ASSERT(!m_menuController);
417 m_menuController = [[QUIMenuController alloc] initWithVisibleMenuItems:filterFirstResponderActions(visibleMenuItems())];
418 repositionMenu();
419 connect(qGuiApp->inputMethod(), &QInputMethod::keyboardRectangleChanged, this, &QIOSMenu::repositionMenu);
420 } else {
421 disconnect(qGuiApp->inputMethod(), &QInputMethod::keyboardRectangleChanged, this, &QIOSMenu::repositionMenu);
422
423 Q_ASSERT(m_menuController);
424 [[UIMenuController sharedMenuController] setMenuVisible:NO animated:YES];
425 [m_menuController release];
426 m_menuController = nullptr;
427 }
428}
429
430void QIOSMenu::toggleShowUsingUIPickerView(bool show)
431{
432 static QObject *focusObjectWithPickerView = nullptr;
433
434 if (show) {
435 Q_ASSERT(!m_pickerView);
436 m_pickerView = [[QUIPickerView alloc] initWithVisibleMenuItems:visibleMenuItems() selectItem:m_targetItem];
437
438 Q_ASSERT(!focusObjectWithPickerView);
439 focusObjectWithPickerView = qApp->focusWindow()->focusObject();
440 focusObjectWithPickerView->installEventFilter(this);
441 qApp->inputMethod()->update(Qt::ImEnabled | Qt::ImPlatformData);
442 } else {
443 Q_ASSERT(focusObjectWithPickerView);
444 focusObjectWithPickerView->removeEventFilter(this);
445 focusObjectWithPickerView = nullptr;
446
447 Q_ASSERT(m_pickerView);
448 [m_pickerView listenForKeyboardWillHideNotification:NO];
449 [m_pickerView release];
450 m_pickerView = nullptr;
451
452 qApp->inputMethod()->update(Qt::ImEnabled | Qt::ImPlatformData);
453 }
454}
455
456bool QIOSMenu::eventFilter(QObject *obj, QEvent *event)
457{
458 if (event->type() == QEvent::InputMethodQuery) {
459 QInputMethodQueryEvent *queryEvent = static_cast<QInputMethodQueryEvent *>(event);
460 if (queryEvent->queries() & Qt::ImPlatformData) {
461 // Let object fill inn default query results
462 obj->event(queryEvent);
463
464 QVariantMap imPlatformData = queryEvent->value(Qt::ImPlatformData).toMap();
465 imPlatformData.insert(kImePlatformDataInputView, QVariant::fromValue(static_cast<void *>(m_pickerView)));
466 imPlatformData.insert(kImePlatformDataInputAccessoryView, QVariant::fromValue(static_cast<void *>(m_pickerView.toolbar)));
467 imPlatformData.insert(kImePlatformDataHideShortcutsBar, true);
468 queryEvent->setValue(Qt::ImPlatformData, imPlatformData);
469 queryEvent->setValue(Qt::ImEnabled, true);
470
471 return true;
472 }
473 }
474
475 return QObject::eventFilter(obj, event);
476}
477
478QIOSMenuItemList QIOSMenu::visibleMenuItems() const
479{
480 QIOSMenuItemList visibleMenuItems;
481 visibleMenuItems.reserve(m_menuItems.size());
482 std::copy_if(m_menuItems.begin(), m_menuItems.end(), std::back_inserter(visibleMenuItems),
483 [](QIOSMenuItem *item) { return item->m_enabled && item->m_visible && !item->m_separator; });
484 return visibleMenuItems;
485}
486
487QIOSMenuItemList QIOSMenu::filterFirstResponderActions(const QIOSMenuItemList &menuItems)
488{
489 // UIResponderStandardEditActions found in first responder will be prepended to the edit
490 // menu automatically (or e.g made available as buttons on the virtual keyboard). So we
491 // filter them out to avoid duplicates, and let first responder handle the actions instead.
492 // In case of QIOSTextResponder, edit actions will be converted to key events that ends up
493 // triggering the shortcuts of the filtered menu items.
494 QIOSMenuItemList filteredMenuItems;
495 UIResponder *responder = [UIResponder qt_currentFirstResponder];
496
497 for (int i = 0; i < menuItems.count(); ++i) {
498 QIOSMenuItem *menuItem = menuItems.at(i);
499#ifndef QT_NO_SHORTCUT
500 QKeySequence shortcut = menuItem->m_shortcut;
501 if ((shortcut == QKeySequence::Cut && [responder canPerformAction:@selector(cut:) withSender:nil])
502 || (shortcut == QKeySequence::Copy && [responder canPerformAction:@selector(copy:) withSender:nil])
503 || (shortcut == QKeySequence::Paste && [responder canPerformAction:@selector(paste:) withSender:nil])
504 || (shortcut == QKeySequence::Delete && [responder canPerformAction:@selector(delete:) withSender:nil])
505 || (shortcut == QKeySequence::SelectAll && [responder canPerformAction:@selector(selectAll:) withSender:nil])
506 || (shortcut == QKeySequence::Undo && [responder canPerformAction:@selector(undo) withSender:nil])
507 || (shortcut == QKeySequence::Redo && [responder canPerformAction:@selector(redo) withSender:nil])
508 || (shortcut == QKeySequence::Bold && [responder canPerformAction:@selector(toggleBoldface:) withSender:nil])
509 || (shortcut == QKeySequence::Italic && [responder canPerformAction:@selector(toggleItalics:) withSender:nil])
510 || (shortcut == QKeySequence::Underline && [responder canPerformAction:@selector(toggleUnderline:) withSender:nil])) {
511 continue;
512 }
513#endif
514 filteredMenuItems.append(menuItem);
515 }
516 return filteredMenuItems;
517}
518
519void QIOSMenu::repositionMenu()
520{
521 switch (m_effectiveMenuType) {
522 case EditMenu: {
523 UIView *view = reinterpret_cast<UIView *>(m_parentWindow->winId());
524 [[UIMenuController sharedMenuController] setTargetRect:m_targetRect.toCGRect() inView:view];
525 [[UIMenuController sharedMenuController] setMenuVisible:YES animated:YES];
526 break; }
527 default:
528 break;
529 }
530}
531
533{
534 if (position < 0 || position >= m_menuItems.size())
535 return 0;
536 return m_menuItems.at(position);
537}
538
540{
541 for (int i = 0; i < m_menuItems.size(); ++i) {
542 QPlatformMenuItem *item = m_menuItems.at(i);
543 if (item->tag() == tag)
544 return item;
545 }
546 return 0;
547}
void setText(const QString &text) override
Definition qiosmenu.mm:236
void setIsSeparator(bool) override
Definition qiosmenu.mm:251
void setVisible(bool isVisible) override
Definition qiosmenu.mm:246
bool m_enabled
Definition qiosmenu.h:44
bool m_visible
Definition qiosmenu.h:41
bool m_separator
Definition qiosmenu.h:45
void setShortcut(const QKeySequence &) override
Definition qiosmenu.mm:262
void setRole(MenuRole role) override
Definition qiosmenu.mm:256
void setEnabled(bool enabled) override
Definition qiosmenu.mm:268
void setMenu(QPlatformMenu *) override
Definition qiosmenu.mm:241
void dismiss() override
Definition qiosmenu.mm:391
QPlatformMenuItem * menuItemAt(int position) const override
Definition qiosmenu.mm:532
void insertMenuItem(QPlatformMenuItem *menuItem, QPlatformMenuItem *before) override
Definition qiosmenu.mm:293
void removeMenuItem(QPlatformMenuItem *menuItem) override
Definition qiosmenu.mm:305
void setText(const QString &) override
Definition qiosmenu.mm:327
QPlatformMenuItem * menuItemForTag(quintptr tag) const override
Definition qiosmenu.mm:539
void setVisible(bool visible) override
Definition qiosmenu.mm:337
bool eventFilter(QObject *obj, QEvent *event) override
Filters events if this object has been installed as an event filter for the watched object.
Definition qiosmenu.mm:456
void syncMenuItem(QPlatformMenuItem *) override
Definition qiosmenu.mm:312
void handleItemSelected(QIOSMenuItem *menuItem)
Definition qiosmenu.mm:347
void setEnabled(bool enabled) override
Definition qiosmenu.mm:332
long NSInteger
Q_FORWARD_DECLARE_OBJC_CLASS(NSString)
#define qApp
#define qGuiApp
const char kImePlatformDataHideShortcutsBar[]
QList< QIOSMenuItem * > QIOSMenuItemList
Definition qiosmenu.h:50
static NSString *const kSelectorPrefix
Definition qiosmenu.mm:26