// Copyright (C) 2012 BogDan Vatra // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "androidjnimain.h" #include "androidjnimenu.h" #include "qandroidplatformmenu.h" #include "qandroidplatformmenubar.h" #include "qandroidplatformmenuitem.h" #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE using namespace QtAndroid; namespace QtAndroidMenu { static QList pendingContextMenus; static QAndroidPlatformMenu *visibleMenu = nullptr; Q_CONSTINIT static QRecursiveMutex visibleMenuMutex; static QSet menuBars; static QAndroidPlatformMenuBar *visibleMenuBar = nullptr; static QWindow *activeTopLevelWindow = nullptr; Q_CONSTINIT static QRecursiveMutex menuBarMutex; static jmethodID clearMenuMethodID = 0; static jmethodID addMenuItemMethodID = 0; static int menuNoneValue = 0; static jmethodID setHeaderTitleContextMenuMethodID = 0; static jmethodID setCheckableMenuItemMethodID = 0; static jmethodID setCheckedMenuItemMethodID = 0; static jmethodID setEnabledMenuItemMethodID = 0; static jmethodID setIconMenuItemMethodID = 0; static jmethodID setVisibleMenuItemMethodID = 0; void resetMenuBar() { qtActivityDelegate().callMethod("resetOptionsMenu"); } void openOptionsMenu() { qtActivityDelegate().callMethod("openOptionsMenu"); } void showContextMenu(QAndroidPlatformMenu *menu, const QRect &anchorRect) { QMutexLocker lock(&visibleMenuMutex); if (visibleMenu) pendingContextMenus.append(visibleMenu); visibleMenu = menu; menu->aboutToShow(); qtActivityDelegate().callMethod("openContextMenu", anchorRect.x(), anchorRect.y(), anchorRect.width(), anchorRect.height()); } void hideContextMenu(QAndroidPlatformMenu *menu) { QMutexLocker lock(&visibleMenuMutex); if (visibleMenu == menu) { qtActivityDelegate().callMethod("closeContextMenu"); pendingContextMenus.clear(); } else { pendingContextMenus.removeOne(menu); } } // FIXME void syncMenu(QAndroidPlatformMenu */*menu*/) { // QMutexLocker lock(&visibleMenuMutex); // if (visibleMenu == menu) // { // hideContextMenu(menu); // showContextMenu(menu); // } } void androidPlatformMenuDestroyed(QAndroidPlatformMenu *menu) { QMutexLocker lock(&visibleMenuMutex); if (visibleMenu == menu) visibleMenu = 0; } void setMenuBar(QAndroidPlatformMenuBar *menuBar, QWindow *window) { if (activeTopLevelWindow == window && visibleMenuBar != menuBar) { visibleMenuBar = menuBar; resetMenuBar(); } } void setActiveTopLevelWindow(QWindow *window) { Qt::WindowFlags flags = window ? window->flags() : Qt::WindowFlags(); if (!window) return; bool isNonRegularWindow = flags & (Qt::Desktop | Qt::Popup | Qt::Dialog | Qt::Sheet) & ~Qt::Window; if (isNonRegularWindow) return; QMutexLocker lock(&menuBarMutex); if (activeTopLevelWindow == window) return; visibleMenuBar = 0; activeTopLevelWindow = window; for (QAndroidPlatformMenuBar *menuBar : std::as_const(menuBars)) { if (menuBar->parentWindow() == window) { visibleMenuBar = menuBar; resetMenuBar(); break; } } } void addMenuBar(QAndroidPlatformMenuBar *menuBar) { QMutexLocker lock(&menuBarMutex); menuBars.insert(menuBar); } void removeMenuBar(QAndroidPlatformMenuBar *menuBar) { QMutexLocker lock(&menuBarMutex); menuBars.remove(menuBar); if (visibleMenuBar == menuBar) { visibleMenuBar = 0; resetMenuBar(); } } static QString removeAmpersandEscapes(QString s) { qsizetype i = 0; while (i < s.size()) { ++i; if (s.at(i - 1) != u'&') continue; if (i < s.size() && s.at(i) == u'&') ++i; s.remove(i-1,1); } return s.trimmed(); } static void fillMenuItem(JNIEnv *env, jobject menuItem, bool checkable, bool checked, bool enabled, bool visible, const QIcon &icon=QIcon()) { env->DeleteLocalRef(env->CallObjectMethod(menuItem, setCheckableMenuItemMethodID, checkable)); env->DeleteLocalRef(env->CallObjectMethod(menuItem, setCheckedMenuItemMethodID, checked)); env->DeleteLocalRef(env->CallObjectMethod(menuItem, setEnabledMenuItemMethodID, enabled)); if (!icon.isNull()) { // isNull() only checks the d pointer, not the actual image data. int sz = qMax(36, qEnvironmentVariableIntValue("QT_ANDROID_APP_ICON_SIZE")); QImage img = icon.pixmap(QSize(sz,sz), enabled ? QIcon::Normal : QIcon::Disabled, QIcon::On).toImage(); if (!img.isNull()) { // Make sure we have a valid image. env->DeleteLocalRef(env->CallObjectMethod(menuItem, setIconMenuItemMethodID, createBitmapDrawable(createBitmap(img, env), env))); } } env->DeleteLocalRef(env->CallObjectMethod(menuItem, setVisibleMenuItemMethodID, visible)); } static int addAllMenuItemsToMenu(JNIEnv *env, jobject menu, QAndroidPlatformMenu *platformMenu) { int order = 0; QMutexLocker lock(platformMenu->menuItemsMutex()); const auto items = platformMenu->menuItems(); for (QAndroidPlatformMenuItem *item : items) { if (item->isSeparator()) continue; QString itemText = removeAmpersandEscapes(item->text()); jstring jtext = env->NewString(reinterpret_cast(itemText.data()), itemText.length()); jint menuId = platformMenu->menuId(item); jobject menuItem = env->CallObjectMethod(menu, addMenuItemMethodID, menuNoneValue, menuId, order++, jtext); env->DeleteLocalRef(jtext); fillMenuItem(env, menuItem, item->isCheckable(), item->isChecked(), item->isEnabled(), item->isVisible(), item->icon()); env->DeleteLocalRef(menuItem); } return order; } static jboolean onPrepareOptionsMenu(JNIEnv *env, jobject thiz, jobject menu) { Q_UNUSED(thiz) env->CallVoidMethod(menu, clearMenuMethodID); QMutexLocker lock(&menuBarMutex); if (!visibleMenuBar) return JNI_FALSE; const QAndroidPlatformMenuBar::PlatformMenusType &menus = visibleMenuBar->menus(); int order = 0; QMutexLocker lockMenuBarMutex(visibleMenuBar->menusListMutex()); if (menus.size() == 1) { // Expand the menu order = addAllMenuItemsToMenu(env, menu, static_cast(menus.front())); } else { for (QAndroidPlatformMenu *item : menus) { QString itemText = removeAmpersandEscapes(item->text()); jstring jtext = env->NewString(reinterpret_cast(itemText.data()), itemText.length()); jint menuId = visibleMenuBar->menuId(item); jobject menuItem = env->CallObjectMethod(menu, addMenuItemMethodID, menuNoneValue, menuId, order++, jtext); env->DeleteLocalRef(jtext); fillMenuItem(env, menuItem, false, false, item->isEnabled(), item->isVisible(), item->icon()); } } return order ? JNI_TRUE : JNI_FALSE; } static jboolean onOptionsItemSelected(JNIEnv *env, jobject thiz, jint menuId, jboolean checked) { Q_UNUSED(env) Q_UNUSED(thiz) QMutexLocker lock(&menuBarMutex); if (!visibleMenuBar) return JNI_FALSE; const QAndroidPlatformMenuBar::PlatformMenusType &menus = visibleMenuBar->menus(); if (menus.size() == 1) { // Expanded menu QAndroidPlatformMenuItem *item = static_cast(menus.front()->menuItemForId(menuId)); if (item) { if (item->menu()) { showContextMenu(item->menu(), QRect()); } else { if (item->isCheckable()) item->setChecked(checked); item->activated(); } } } else { QAndroidPlatformMenu *menu = static_cast(visibleMenuBar->menuForId(menuId)); if (menu) showContextMenu(menu, QRect()); } return JNI_TRUE; } static void onOptionsMenuClosed(JNIEnv *env, jobject thiz, jobject menu) { Q_UNUSED(env) Q_UNUSED(thiz) Q_UNUSED(menu) } static void onCreateContextMenu(JNIEnv *env, jobject thiz, jobject menu) { Q_UNUSED(thiz) env->CallVoidMethod(menu, clearMenuMethodID); QMutexLocker lock(&visibleMenuMutex); if (!visibleMenu) return; QString menuText = removeAmpersandEscapes(visibleMenu->text()); jstring jtext = env->NewString(reinterpret_cast(menuText.data()), menuText.length()); env->CallObjectMethod(menu, setHeaderTitleContextMenuMethodID, jtext); env->DeleteLocalRef(jtext); addAllMenuItemsToMenu(env, menu, visibleMenu); } static void fillContextMenu(JNIEnv *env, jobject thiz, jobject menu) { Q_UNUSED(thiz) env->CallVoidMethod(menu, clearMenuMethodID); QMutexLocker lock(&visibleMenuMutex); if (!visibleMenu) return; addAllMenuItemsToMenu(env, menu, visibleMenu); } static jboolean onContextItemSelected(JNIEnv *env, jobject thiz, jint menuId, jboolean checked) { Q_UNUSED(env) Q_UNUSED(thiz) QMutexLocker lock(&visibleMenuMutex); QAndroidPlatformMenuItem * item = static_cast(visibleMenu->menuItemForId(menuId)); if (item) { if (item->menu()) { showContextMenu(item->menu(), QRect()); } else { if (item->isCheckable()) item->setChecked(checked); item->activated(); visibleMenu->aboutToHide(); visibleMenu = 0; for (QAndroidPlatformMenu *menu : std::as_const(pendingContextMenus)) { if (menu->isVisible()) menu->aboutToHide(); } pendingContextMenus.clear(); } } return JNI_TRUE; } static void onContextMenuClosed(JNIEnv *env, jobject thiz, jobject menu) { Q_UNUSED(env) Q_UNUSED(thiz) Q_UNUSED(menu) QMutexLocker lock(&visibleMenuMutex); if (!visibleMenu) return; visibleMenu->aboutToHide(); visibleMenu = 0; if (!pendingContextMenus.empty()) showContextMenu(pendingContextMenus.takeLast(), QRect()); } static JNINativeMethod methods[] = { {"onPrepareOptionsMenu", "(Landroid/view/Menu;)Z", (void *)onPrepareOptionsMenu}, {"onOptionsItemSelected", "(IZ)Z", (void *)onOptionsItemSelected}, {"onOptionsMenuClosed", "(Landroid/view/Menu;)V", (void*)onOptionsMenuClosed}, {"onCreateContextMenu", "(Landroid/view/ContextMenu;)V", (void *)onCreateContextMenu}, {"fillContextMenu", "(Landroid/view/Menu;)V", (void *)fillContextMenu}, {"onContextItemSelected", "(IZ)Z", (void *)onContextItemSelected}, {"onContextMenuClosed", "(Landroid/view/Menu;)V", (void*)onContextMenuClosed}, }; #define FIND_AND_CHECK_CLASS(CLASS_NAME) \ clazz = env->FindClass(CLASS_NAME); \ if (!clazz) { \ __android_log_print(ANDROID_LOG_FATAL, qtTagText(), classErrorMsgFmt(), CLASS_NAME); \ return false; \ } #define GET_AND_CHECK_METHOD(VAR, CLASS, METHOD_NAME, METHOD_SIGNATURE) \ VAR = env->GetMethodID(CLASS, METHOD_NAME, METHOD_SIGNATURE); \ if (!VAR) { \ __android_log_print(ANDROID_LOG_FATAL, qtTagText(), methodErrorMsgFmt(), METHOD_NAME, METHOD_SIGNATURE); \ return false; \ } #define GET_AND_CHECK_STATIC_METHOD(VAR, CLASS, METHOD_NAME, METHOD_SIGNATURE) \ VAR = env->GetStaticMethodID(CLASS, METHOD_NAME, METHOD_SIGNATURE); \ if (!VAR) { \ __android_log_print(ANDROID_LOG_FATAL, qtTagText(), methodErrorMsgFmt(), METHOD_NAME, METHOD_SIGNATURE); \ return false; \ } #define GET_AND_CHECK_STATIC_FIELD(VAR, CLASS, FIELD_NAME, FIELD_SIGNATURE) \ VAR = env->GetStaticFieldID(CLASS, FIELD_NAME, FIELD_SIGNATURE); \ if (!VAR) { \ __android_log_print(ANDROID_LOG_FATAL, qtTagText(), methodErrorMsgFmt(), FIELD_NAME, FIELD_SIGNATURE); \ return false; \ } bool registerNatives(QJniEnvironment &env) { jclass appClass = applicationClass(); if (!env.registerNativeMethods(appClass, methods, sizeof(methods) / sizeof(methods[0]))) { __android_log_print(ANDROID_LOG_FATAL,"Qt", "RegisterNatives failed"); return false; } jclass clazz; FIND_AND_CHECK_CLASS("android/view/Menu"); GET_AND_CHECK_METHOD(clearMenuMethodID, clazz, "clear", "()V"); GET_AND_CHECK_METHOD(addMenuItemMethodID, clazz, "add", "(IIILjava/lang/CharSequence;)Landroid/view/MenuItem;"); jfieldID menuNoneFiledId; GET_AND_CHECK_STATIC_FIELD(menuNoneFiledId, clazz, "NONE", "I"); menuNoneValue = env->GetStaticIntField(clazz, menuNoneFiledId); FIND_AND_CHECK_CLASS("android/view/ContextMenu"); GET_AND_CHECK_METHOD(setHeaderTitleContextMenuMethodID, clazz, "setHeaderTitle","(Ljava/lang/CharSequence;)Landroid/view/ContextMenu;"); FIND_AND_CHECK_CLASS("android/view/MenuItem"); GET_AND_CHECK_METHOD(setCheckableMenuItemMethodID, clazz, "setCheckable", "(Z)Landroid/view/MenuItem;"); GET_AND_CHECK_METHOD(setCheckedMenuItemMethodID, clazz, "setChecked", "(Z)Landroid/view/MenuItem;"); GET_AND_CHECK_METHOD(setEnabledMenuItemMethodID, clazz, "setEnabled", "(Z)Landroid/view/MenuItem;"); GET_AND_CHECK_METHOD(setIconMenuItemMethodID, clazz, "setIcon", "(Landroid/graphics/drawable/Drawable;)Landroid/view/MenuItem;"); GET_AND_CHECK_METHOD(setVisibleMenuItemMethodID, clazz, "setVisible", "(Z)Landroid/view/MenuItem;"); return true; } } QT_END_NAMESPACE