blob: 74a77de7574b12c0d1a8a1a778af208629f8ab7b [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the plugins of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** 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 https://www.qt.io/terms-conditions. For further
** information use the contact form at https://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 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include <qglobal.h>
#include <qguiapplication.h>
#include <qpa/qplatformtheme.h>
#include "qiosglobal.h"
#include "qiosmenu.h"
#include "qioswindow.h"
#include "qiosinputcontext.h"
#include "qiosintegration.h"
#include "qiostextresponder.h"
#include <algorithm>
#include <iterator>
// m_currentMenu points to the currently visible menu.
// Only one menu will be visible at a time, and if a second menu
// is shown on top of a first, the first one will be told to hide.
QIOSMenu *QIOSMenu::m_currentMenu = 0;
// -------------------------------------------------------------------------
static NSString *const kSelectorPrefix = @"_qtMenuItem_";
@interface QUIMenuController : UIResponder
@end
@implementation QUIMenuController {
QIOSMenuItemList m_visibleMenuItems;
}
- (instancetype)initWithVisibleMenuItems:(const QIOSMenuItemList &)visibleMenuItems
{
if (self = [super init]) {
[self setVisibleMenuItems:visibleMenuItems];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(menuClosed)
name:UIMenuControllerDidHideMenuNotification object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter]
removeObserver:self
name:UIMenuControllerDidHideMenuNotification object:nil];
[super dealloc];
}
- (void)setVisibleMenuItems:(const QIOSMenuItemList &)visibleMenuItems
{
m_visibleMenuItems = visibleMenuItems;
NSMutableArray<UIMenuItem *> *menuItemArray = [NSMutableArray<UIMenuItem *> arrayWithCapacity:m_visibleMenuItems.size()];
// Create an array of UIMenuItems, one for each visible QIOSMenuItem. Each
// UIMenuItem needs a callback assigned, so we assign one of the placeholder methods
// added to UIWindow (QIOSMenuActionTargets) below. Each method knows its own index, which
// corresponds to the index of the corresponding QIOSMenuItem in m_visibleMenuItems. When
// triggered, menuItemActionCallback will end up being called.
for (int i = 0; i < m_visibleMenuItems.count(); ++i) {
QIOSMenuItem *item = m_visibleMenuItems.at(i);
SEL sel = NSSelectorFromString([NSString stringWithFormat:@"%@%i:", kSelectorPrefix, i]);
[menuItemArray addObject:[[[UIMenuItem alloc] initWithTitle:item->m_text.toNSString() action:sel] autorelease]];
}
[UIMenuController sharedMenuController].menuItems = menuItemArray;
if ([UIMenuController sharedMenuController].menuVisible)
[[UIMenuController sharedMenuController] setMenuVisible:YES animated:NO];
}
- (void)menuClosed
{
QIOSMenu::currentMenu()->dismiss();
}
- (id)targetForAction:(SEL)action withSender:(id)sender
{
Q_UNUSED(sender);
BOOL containsPrefix = ([NSStringFromSelector(action) rangeOfString:kSelectorPrefix].location != NSNotFound);
return containsPrefix ? self : 0;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
Q_UNUSED(selector);
// Just return a dummy signature that NSObject can create an NSInvocation from.
// We end up only checking selector in forwardInvocation anyway.
return [super methodSignatureForSelector:@selector(methodSignatureForSelector:)];
}
- (void)forwardInvocation:(NSInvocation *)invocation
{
// Since none of the menu item selector methods actually exist, this function
// will end up being called as a final resort. We can then handle the action.
NSString *selector = NSStringFromSelector(invocation.selector);
NSRange range = NSMakeRange(kSelectorPrefix.length, selector.length - kSelectorPrefix.length - 1);
NSInteger selectedIndex = [[selector substringWithRange:range] integerValue];
QIOSMenu::currentMenu()->handleItemSelected(m_visibleMenuItems.at(selectedIndex));
}
@end
// -------------------------------------------------------------------------
@interface QUIPickerView : UIPickerView <UIPickerViewDelegate, UIPickerViewDataSource>
@property(retain) UIToolbar *toolbar;
@end
@implementation QUIPickerView {
QIOSMenuItemList m_visibleMenuItems;
QPointer<QObject> m_focusObjectWithPickerView;
NSInteger m_selectedRow;
}
- (instancetype)initWithVisibleMenuItems:(const QIOSMenuItemList &)visibleMenuItems selectItem:(const QIOSMenuItem *)selectItem
{
if (self = [super init]) {
[self setVisibleMenuItems:visibleMenuItems selectItem:selectItem];
self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
self.toolbar = [[[UIToolbar alloc] init] autorelease];
self.toolbar.frame.size = [self.toolbar sizeThatFits:self.bounds.size];
self.toolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
UIBarButtonItem *spaceButton = [[[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
target:self action:@selector(closeMenu)] autorelease];
UIBarButtonItem *cancelButton = [[[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
target:self action:@selector(cancelMenu)] autorelease];
UIBarButtonItem *doneButton = [[[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
target:self action:@selector(closeMenu)] autorelease];
[self.toolbar setItems:@[cancelButton, spaceButton, doneButton]];
[self setDelegate:self];
[self setDataSource:self];
[self selectRow:m_selectedRow inComponent:0 animated:false];
[self listenForKeyboardWillHideNotification:YES];
}
return self;
}
- (void)setVisibleMenuItems:(const QIOSMenuItemList &)visibleMenuItems selectItem:(const QIOSMenuItem *)selectItem
{
m_visibleMenuItems = visibleMenuItems;
m_selectedRow = visibleMenuItems.indexOf(const_cast<QIOSMenuItem *>(selectItem));
if (m_selectedRow == -1)
m_selectedRow = 0;
[self reloadAllComponents];
}
- (void)listenForKeyboardWillHideNotification:(BOOL)listen
{
if (listen) {
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(cancelMenu)
name:@"UIKeyboardWillHideNotification" object:nil];
} else {
[[NSNotificationCenter defaultCenter]
removeObserver:self
name:@"UIKeyboardWillHideNotification" object:nil];
}
}
- (void)dealloc
{
[self listenForKeyboardWillHideNotification:NO];
self.toolbar = 0;
[super dealloc];
}
- (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component
{
Q_UNUSED(pickerView);
Q_UNUSED(component);
return m_visibleMenuItems.at(row)->m_text.toNSString();
}
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
{
Q_UNUSED(pickerView);
return 1;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
Q_UNUSED(pickerView);
Q_UNUSED(component);
return m_visibleMenuItems.length();
}
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
Q_UNUSED(pickerView);
Q_UNUSED(component);
m_selectedRow = row;
}
- (void)closeMenu
{
if (!m_visibleMenuItems.isEmpty())
QIOSMenu::currentMenu()->handleItemSelected(m_visibleMenuItems.at(m_selectedRow));
else
QIOSMenu::currentMenu()->dismiss();
}
- (void)cancelMenu
{
QIOSMenu::currentMenu()->dismiss();
}
@end
// -------------------------------------------------------------------------
QIOSMenuItem::QIOSMenuItem()
: QPlatformMenuItem()
, m_visible(true)
, m_text(QString())
, m_role(MenuRole(0))
, m_enabled(true)
, m_separator(false)
, m_menu(0)
{
}
void QIOSMenuItem::setText(const QString &text)
{
m_text = QPlatformTheme::removeMnemonics(text);
}
void QIOSMenuItem::setMenu(QPlatformMenu *menu)
{
m_menu = static_cast<QIOSMenu *>(menu);
}
void QIOSMenuItem::setVisible(bool isVisible)
{
m_visible = isVisible;
}
void QIOSMenuItem::setIsSeparator(bool isSeparator)
{
m_separator = isSeparator;
}
void QIOSMenuItem::setRole(QPlatformMenuItem::MenuRole role)
{
m_role = role;
}
#ifndef QT_NO_SHORTCUT
void QIOSMenuItem::setShortcut(const QKeySequence &sequence)
{
m_shortcut = sequence;
}
#endif
void QIOSMenuItem::setEnabled(bool enabled)
{
m_enabled = enabled;
}
QIOSMenu::QIOSMenu()
: QPlatformMenu()
, m_enabled(true)
, m_visible(false)
, m_text(QString())
, m_menuType(DefaultMenu)
, m_effectiveMenuType(DefaultMenu)
, m_parentWindow(0)
, m_targetItem(0)
, m_menuController(0)
, m_pickerView(0)
{
}
QIOSMenu::~QIOSMenu()
{
dismiss();
}
void QIOSMenu::insertMenuItem(QPlatformMenuItem *menuItem, QPlatformMenuItem *before)
{
if (!before) {
m_menuItems.append(static_cast<QIOSMenuItem *>(menuItem));
} else {
int index = m_menuItems.indexOf(static_cast<QIOSMenuItem *>(before)) + 1;
m_menuItems.insert(index, static_cast<QIOSMenuItem *>(menuItem));
}
if (m_currentMenu == this)
syncMenuItem(menuItem);
}
void QIOSMenu::removeMenuItem(QPlatformMenuItem *menuItem)
{
m_menuItems.removeOne(static_cast<QIOSMenuItem *>(menuItem));
if (m_currentMenu == this)
syncMenuItem(menuItem);
}
void QIOSMenu::syncMenuItem(QPlatformMenuItem *)
{
if (m_currentMenu != this)
return;
switch (m_effectiveMenuType) {
case EditMenu:
[m_menuController setVisibleMenuItems:filterFirstResponderActions(visibleMenuItems())];
break;
default:
[m_pickerView setVisibleMenuItems:visibleMenuItems() selectItem:m_targetItem];
break;
}
}
void QIOSMenu::setText(const QString &text)
{
m_text = text;
}
void QIOSMenu::setEnabled(bool enabled)
{
m_enabled = enabled;
}
void QIOSMenu::setVisible(bool visible)
{
m_visible = visible;
}
void QIOSMenu::setMenuType(QPlatformMenu::MenuType type)
{
m_menuType = type;
}
void QIOSMenu::handleItemSelected(QIOSMenuItem *menuItem)
{
emit menuItem->activated();
dismiss();
if (QIOSMenu *menu = menuItem->m_menu) {
menu->setMenuType(m_effectiveMenuType);
menu->showPopup(m_parentWindow, m_targetRect, 0);
}
}
void QIOSMenu::showPopup(const QWindow *parentWindow, const QRect &targetRect, const QPlatformMenuItem *item)
{
if (m_currentMenu == this || !parentWindow)
return;
emit aboutToShow();
m_parentWindow = const_cast<QWindow *>(parentWindow);
m_targetRect = targetRect;
m_targetItem = static_cast<const QIOSMenuItem *>(item);
if (!m_parentWindow->isActive())
m_parentWindow->requestActivate();
if (m_currentMenu && m_currentMenu != this)
m_currentMenu->dismiss();
m_currentMenu = this;
m_effectiveMenuType = m_menuType;
connect(qGuiApp, &QGuiApplication::focusObjectChanged, this, &QIOSMenu::dismiss);
switch (m_effectiveMenuType) {
case EditMenu:
toggleShowUsingUIMenuController(true);
break;
default:
toggleShowUsingUIPickerView(true);
break;
}
m_visible = true;
}
void QIOSMenu::dismiss()
{
if (m_currentMenu != this)
return;
emit aboutToHide();
disconnect(qGuiApp, &QGuiApplication::focusObjectChanged, this, &QIOSMenu::dismiss);
switch (m_effectiveMenuType) {
case EditMenu:
toggleShowUsingUIMenuController(false);
break;
default:
toggleShowUsingUIPickerView(false);
break;
}
m_currentMenu = 0;
m_visible = false;
}
void QIOSMenu::toggleShowUsingUIMenuController(bool show)
{
if (show) {
Q_ASSERT(!m_menuController);
m_menuController = [[QUIMenuController alloc] initWithVisibleMenuItems:filterFirstResponderActions(visibleMenuItems())];
repositionMenu();
connect(qGuiApp->inputMethod(), &QInputMethod::keyboardRectangleChanged, this, &QIOSMenu::repositionMenu);
} else {
disconnect(qGuiApp->inputMethod(), &QInputMethod::keyboardRectangleChanged, this, &QIOSMenu::repositionMenu);
Q_ASSERT(m_menuController);
[[UIMenuController sharedMenuController] setMenuVisible:NO animated:YES];
[m_menuController release];
m_menuController = 0;
}
}
void QIOSMenu::toggleShowUsingUIPickerView(bool show)
{
static QObject *focusObjectWithPickerView = 0;
if (show) {
Q_ASSERT(!m_pickerView);
m_pickerView = [[QUIPickerView alloc] initWithVisibleMenuItems:visibleMenuItems() selectItem:m_targetItem];
Q_ASSERT(!focusObjectWithPickerView);
focusObjectWithPickerView = qApp->focusWindow()->focusObject();
focusObjectWithPickerView->installEventFilter(this);
qApp->inputMethod()->update(Qt::ImEnabled | Qt::ImPlatformData);
} else {
Q_ASSERT(focusObjectWithPickerView);
focusObjectWithPickerView->removeEventFilter(this);
focusObjectWithPickerView = 0;
Q_ASSERT(m_pickerView);
[m_pickerView listenForKeyboardWillHideNotification:NO];
[m_pickerView release];
m_pickerView = 0;
qApp->inputMethod()->update(Qt::ImEnabled | Qt::ImPlatformData);
}
}
bool QIOSMenu::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::InputMethodQuery) {
QInputMethodQueryEvent *queryEvent = static_cast<QInputMethodQueryEvent *>(event);
if (queryEvent->queries() & Qt::ImPlatformData) {
// Let object fill inn default query results
obj->event(queryEvent);
QVariantMap imPlatformData = queryEvent->value(Qt::ImPlatformData).toMap();
imPlatformData.insert(kImePlatformDataInputView, QVariant::fromValue(static_cast<void *>(m_pickerView)));
imPlatformData.insert(kImePlatformDataInputAccessoryView, QVariant::fromValue(static_cast<void *>(m_pickerView.toolbar)));
imPlatformData.insert(kImePlatformDataHideShortcutsBar, true);
queryEvent->setValue(Qt::ImPlatformData, imPlatformData);
queryEvent->setValue(Qt::ImEnabled, true);
return true;
}
}
return QObject::eventFilter(obj, event);
}
QIOSMenuItemList QIOSMenu::visibleMenuItems() const
{
QIOSMenuItemList visibleMenuItems;
visibleMenuItems.reserve(m_menuItems.size());
std::copy_if(m_menuItems.begin(), m_menuItems.end(), std::back_inserter(visibleMenuItems),
[](QIOSMenuItem *item) { return item->m_enabled && item->m_visible && !item->m_separator; });
return visibleMenuItems;
}
QIOSMenuItemList QIOSMenu::filterFirstResponderActions(const QIOSMenuItemList &menuItems)
{
// UIResponderStandardEditActions found in first responder will be prepended to the edit
// menu automatically (or e.g made available as buttons on the virtual keyboard). So we
// filter them out to avoid duplicates, and let first responder handle the actions instead.
// In case of QIOSTextResponder, edit actions will be converted to key events that ends up
// triggering the shortcuts of the filtered menu items.
QIOSMenuItemList filteredMenuItems;
UIResponder *responder = [UIResponder currentFirstResponder];
for (int i = 0; i < menuItems.count(); ++i) {
QIOSMenuItem *menuItem = menuItems.at(i);
#ifndef QT_NO_SHORTCUT
QKeySequence shortcut = menuItem->m_shortcut;
if ((shortcut == QKeySequence::Cut && [responder canPerformAction:@selector(cut:) withSender:nil])
|| (shortcut == QKeySequence::Copy && [responder canPerformAction:@selector(copy:) withSender:nil])
|| (shortcut == QKeySequence::Paste && [responder canPerformAction:@selector(paste:) withSender:nil])
|| (shortcut == QKeySequence::Delete && [responder canPerformAction:@selector(delete:) withSender:nil])
|| (shortcut == QKeySequence::SelectAll && [responder canPerformAction:@selector(selectAll:) withSender:nil])
|| (shortcut == QKeySequence::Undo && [responder canPerformAction:@selector(undo:) withSender:nil])
|| (shortcut == QKeySequence::Redo && [responder canPerformAction:@selector(redo:) withSender:nil])
|| (shortcut == QKeySequence::Bold && [responder canPerformAction:@selector(toggleBoldface:) withSender:nil])
|| (shortcut == QKeySequence::Italic && [responder canPerformAction:@selector(toggleItalics:) withSender:nil])
|| (shortcut == QKeySequence::Underline && [responder canPerformAction:@selector(toggleUnderline:) withSender:nil])) {
continue;
}
#endif
filteredMenuItems.append(menuItem);
}
return filteredMenuItems;
}
void QIOSMenu::repositionMenu()
{
switch (m_effectiveMenuType) {
case EditMenu: {
UIView *view = reinterpret_cast<UIView *>(m_parentWindow->winId());
[[UIMenuController sharedMenuController] setTargetRect:m_targetRect.toCGRect() inView:view];
[[UIMenuController sharedMenuController] setMenuVisible:YES animated:YES];
break; }
default:
break;
}
}
QPlatformMenuItem *QIOSMenu::menuItemAt(int position) const
{
if (position < 0 || position >= m_menuItems.size())
return 0;
return m_menuItems.at(position);
}
QPlatformMenuItem *QIOSMenu::menuItemForTag(quintptr tag) const
{
for (int i = 0; i < m_menuItems.size(); ++i) {
QPlatformMenuItem *item = m_menuItems.at(i);
if (item->tag() == tag)
return item;
}
return 0;
}