1 /**************************************************************************** 2 ** 3 ** Copyright (C) 2012 BogDan Vatra <bogdan@kde.org> 4 ** Contact: https://www.qt.io/licensing/ 5 ** 6 ** This file is part of the plugins of the Qt Toolkit. 7 ** 8 ** $QT_BEGIN_LICENSE:LGPL$ 9 ** Commercial License Usage 10 ** Licensees holding valid commercial Qt licenses may use this file in 11 ** accordance with the commercial license agreement provided with the 12 ** Software or, alternatively, in accordance with the terms contained in 13 ** a written agreement between you and The Qt Company. For licensing terms 14 ** and conditions see https://www.qt.io/terms-conditions. For further 15 ** information use the contact form at https://www.qt.io/contact-us. 16 ** 17 ** GNU Lesser General Public License Usage 18 ** Alternatively, this file may be used under the terms of the GNU Lesser 19 ** General Public License version 3 as published by the Free Software 20 ** Foundation and appearing in the file LICENSE.LGPL3 included in the 21 ** packaging of this file. Please review the following information to 22 ** ensure the GNU Lesser General Public License version 3 requirements 23 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. 24 ** 25 ** GNU General Public License Usage 26 ** Alternatively, this file may be used under the terms of the GNU 27 ** General Public License version 2.0 or (at your option) the GNU General 28 ** Public license version 3 or any later version approved by the KDE Free 29 ** Qt Foundation. The licenses are as published by the Free Software 30 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 31 ** included in the packaging of this file. Please review the following 32 ** information to ensure the GNU General Public License requirements will 33 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and 34 ** https://www.gnu.org/licenses/gpl-3.0.html. 35 ** 36 ** $QT_END_LICENSE$ 37 ** 38 ****************************************************************************/ 39 40 #include "androidjnimenu.h" 41 #include "androidjnimain.h" 42 #include "qandroidplatformmenubar.h" 43 #include "qandroidplatformmenu.h" 44 #include "qandroidplatformmenuitem.h" 45 46 #include <QMutex> 47 #include <QPoint> 48 #include <QQueue> 49 #include <QRect> 50 #include <QSet> 51 #include <QWindow> 52 #include <QtCore/private/qjnihelpers_p.h> 53 #include <QtCore/private/qjni_p.h> 54 55 QT_BEGIN_NAMESPACE 56 57 using namespace QtAndroid; 58 59 namespace QtAndroidMenu 60 { 61 static QList<QAndroidPlatformMenu *> pendingContextMenus; 62 static QAndroidPlatformMenu *visibleMenu = 0; 63 static QRecursiveMutex visibleMenuMutex; 64 65 static QSet<QAndroidPlatformMenuBar *> menuBars; 66 static QAndroidPlatformMenuBar *visibleMenuBar = 0; 67 static QWindow *activeTopLevelWindow = 0; 68 static QRecursiveMutex menuBarMutex; 69 70 static jmethodID openContextMenuMethodID = 0; 71 72 static jmethodID clearMenuMethodID = 0; 73 static jmethodID addMenuItemMethodID = 0; 74 static int menuNoneValue = 0; 75 static jmethodID setHeaderTitleContextMenuMethodID = 0; 76 77 static jmethodID setCheckableMenuItemMethodID = 0; 78 static jmethodID setCheckedMenuItemMethodID = 0; 79 static jmethodID setEnabledMenuItemMethodID = 0; 80 static jmethodID setIconMenuItemMethodID = 0; 81 static jmethodID setVisibleMenuItemMethodID = 0; 82 resetMenuBar()83 void resetMenuBar() 84 { 85 QJNIObjectPrivate::callStaticMethod<void>(applicationClass(), "resetOptionsMenu"); 86 } 87 openOptionsMenu()88 void openOptionsMenu() 89 { 90 QJNIObjectPrivate::callStaticMethod<void>(applicationClass(), "openOptionsMenu"); 91 } 92 showContextMenu(QAndroidPlatformMenu * menu,const QRect & anchorRect,JNIEnv * env)93 void showContextMenu(QAndroidPlatformMenu *menu, const QRect &anchorRect, JNIEnv *env) 94 { 95 QMutexLocker lock(&visibleMenuMutex); 96 if (visibleMenu) 97 pendingContextMenus.append(visibleMenu); 98 visibleMenu = menu; 99 menu->aboutToShow(); 100 env->CallStaticVoidMethod(applicationClass(), openContextMenuMethodID, anchorRect.x(), anchorRect.y(), anchorRect.width(), anchorRect.height()); 101 } 102 hideContextMenu(QAndroidPlatformMenu * menu)103 void hideContextMenu(QAndroidPlatformMenu *menu) 104 { 105 QMutexLocker lock(&visibleMenuMutex); 106 if (visibleMenu == menu) { 107 QJNIObjectPrivate::callStaticMethod<void>(applicationClass(), "closeContextMenu"); 108 pendingContextMenus.clear(); 109 } else { 110 pendingContextMenus.removeOne(menu); 111 } 112 } 113 syncMenu(QAndroidPlatformMenu *)114 void syncMenu(QAndroidPlatformMenu */*menu*/) 115 { 116 // QMutexLocker lock(&visibleMenuMutex); 117 // if (visibleMenu == menu) 118 // { 119 // hideContextMenu(menu); 120 // showContextMenu(menu); 121 // } 122 } 123 androidPlatformMenuDestroyed(QAndroidPlatformMenu * menu)124 void androidPlatformMenuDestroyed(QAndroidPlatformMenu *menu) 125 { 126 QMutexLocker lock(&visibleMenuMutex); 127 if (visibleMenu == menu) 128 visibleMenu = 0; 129 } 130 setMenuBar(QAndroidPlatformMenuBar * menuBar,QWindow * window)131 void setMenuBar(QAndroidPlatformMenuBar *menuBar, QWindow *window) 132 { 133 if (activeTopLevelWindow == window && visibleMenuBar != menuBar) { 134 visibleMenuBar = menuBar; 135 resetMenuBar(); 136 } 137 } 138 setActiveTopLevelWindow(QWindow * window)139 void setActiveTopLevelWindow(QWindow *window) 140 { 141 Qt::WindowFlags flags = window ? window->flags() : Qt::WindowFlags(); 142 if (!window) 143 return; 144 145 bool isNonRegularWindow = flags & (Qt::Desktop | Qt::Popup | Qt::Dialog | Qt::Sheet) & ~Qt::Window; 146 if (isNonRegularWindow) 147 return; 148 149 QMutexLocker lock(&menuBarMutex); 150 if (activeTopLevelWindow == window) 151 return; 152 153 visibleMenuBar = 0; 154 activeTopLevelWindow = window; 155 for (QAndroidPlatformMenuBar *menuBar : qAsConst(menuBars)) { 156 if (menuBar->parentWindow() == window) { 157 visibleMenuBar = menuBar; 158 resetMenuBar(); 159 break; 160 } 161 } 162 163 } 164 addMenuBar(QAndroidPlatformMenuBar * menuBar)165 void addMenuBar(QAndroidPlatformMenuBar *menuBar) 166 { 167 QMutexLocker lock(&menuBarMutex); 168 menuBars.insert(menuBar); 169 } 170 removeMenuBar(QAndroidPlatformMenuBar * menuBar)171 void removeMenuBar(QAndroidPlatformMenuBar *menuBar) 172 { 173 QMutexLocker lock(&menuBarMutex); 174 menuBars.remove(menuBar); 175 if (visibleMenuBar == menuBar) { 176 visibleMenuBar = 0; 177 resetMenuBar(); 178 } 179 } 180 removeAmpersandEscapes(QString s)181 static QString removeAmpersandEscapes(QString s) 182 { 183 int i = 0; 184 while (i < s.size()) { 185 ++i; 186 if (s.at(i-1) != QLatin1Char('&')) 187 continue; 188 if (i < s.size() && s.at(i) == QLatin1Char('&')) 189 ++i; 190 s.remove(i-1,1); 191 } 192 return s.trimmed(); 193 } 194 fillMenuItem(JNIEnv * env,jobject menuItem,bool checkable,bool checked,bool enabled,bool visible,const QIcon & icon=QIcon ())195 static void fillMenuItem(JNIEnv *env, jobject menuItem, bool checkable, bool checked, bool enabled, bool visible, const QIcon &icon=QIcon()) 196 { 197 env->DeleteLocalRef(env->CallObjectMethod(menuItem, setCheckableMenuItemMethodID, checkable)); 198 env->DeleteLocalRef(env->CallObjectMethod(menuItem, setCheckedMenuItemMethodID, checked)); 199 env->DeleteLocalRef(env->CallObjectMethod(menuItem, setEnabledMenuItemMethodID, enabled)); 200 201 if (!icon.isNull()) { // isNull() only checks the d pointer, not the actual image data. 202 int sz = qMax(36, qEnvironmentVariableIntValue("QT_ANDROID_APP_ICON_SIZE")); 203 QImage img = icon.pixmap(QSize(sz,sz), 204 enabled 205 ? QIcon::Normal 206 : QIcon::Disabled, 207 QIcon::On).toImage(); 208 if (!img.isNull()) { // Make sure we have a valid image. 209 env->DeleteLocalRef(env->CallObjectMethod(menuItem, 210 setIconMenuItemMethodID, 211 createBitmapDrawable(createBitmap(img, env), env))); 212 } 213 } 214 215 env->DeleteLocalRef(env->CallObjectMethod(menuItem, setVisibleMenuItemMethodID, visible)); 216 } 217 addAllMenuItemsToMenu(JNIEnv * env,jobject menu,QAndroidPlatformMenu * platformMenu)218 static int addAllMenuItemsToMenu(JNIEnv *env, jobject menu, QAndroidPlatformMenu *platformMenu) { 219 int order = 0; 220 QMutexLocker lock(platformMenu->menuItemsMutex()); 221 const auto items = platformMenu->menuItems(); 222 for (QAndroidPlatformMenuItem *item : items) { 223 if (item->isSeparator()) 224 continue; 225 QString itemText = removeAmpersandEscapes(item->text()); 226 jstring jtext = env->NewString(reinterpret_cast<const jchar *>(itemText.data()), 227 itemText.length()); 228 jint menuId = platformMenu->menuId(item); 229 jobject menuItem = env->CallObjectMethod(menu, 230 addMenuItemMethodID, 231 menuNoneValue, 232 menuId, 233 order++, 234 jtext); 235 env->DeleteLocalRef(jtext); 236 fillMenuItem(env, 237 menuItem, 238 item->isCheckable(), 239 item->isChecked(), 240 item->isEnabled(), 241 item->isVisible(), 242 item->icon()); 243 env->DeleteLocalRef(menuItem); 244 } 245 246 return order; 247 } 248 onPrepareOptionsMenu(JNIEnv * env,jobject,jobject menu)249 static jboolean onPrepareOptionsMenu(JNIEnv *env, jobject /*thiz*/, jobject menu) 250 { 251 env->CallVoidMethod(menu, clearMenuMethodID); 252 QMutexLocker lock(&menuBarMutex); 253 if (!visibleMenuBar) 254 return JNI_FALSE; 255 256 const QAndroidPlatformMenuBar::PlatformMenusType &menus = visibleMenuBar->menus(); 257 int order = 0; 258 QMutexLocker lockMenuBarMutex(visibleMenuBar->menusListMutex()); 259 if (menus.size() == 1) { // Expand the menu 260 order = addAllMenuItemsToMenu(env, menu, static_cast<QAndroidPlatformMenu *>(menus.front())); 261 } else { 262 for (QAndroidPlatformMenu *item : menus) { 263 QString itemText = removeAmpersandEscapes(item->text()); 264 jstring jtext = env->NewString(reinterpret_cast<const jchar *>(itemText.data()), 265 itemText.length()); 266 jint menuId = visibleMenuBar->menuId(item); 267 jobject menuItem = env->CallObjectMethod(menu, 268 addMenuItemMethodID, 269 menuNoneValue, 270 menuId, 271 order++, 272 jtext); 273 env->DeleteLocalRef(jtext); 274 275 fillMenuItem(env, 276 menuItem, 277 false, 278 false, 279 item->isEnabled(), 280 item->isVisible(), 281 item->icon()); 282 } 283 } 284 return order ? JNI_TRUE : JNI_FALSE; 285 } 286 onOptionsItemSelected(JNIEnv * env,jobject,jint menuId,jboolean checked)287 static jboolean onOptionsItemSelected(JNIEnv *env, jobject /*thiz*/, jint menuId, jboolean checked) 288 { 289 QMutexLocker lock(&menuBarMutex); 290 if (!visibleMenuBar) 291 return JNI_FALSE; 292 293 const QAndroidPlatformMenuBar::PlatformMenusType &menus = visibleMenuBar->menus(); 294 if (menus.size() == 1) { // Expanded menu 295 QAndroidPlatformMenuItem *item = static_cast<QAndroidPlatformMenuItem *>(menus.front()->menuItemForId(menuId)); 296 if (item) { 297 if (item->menu()) { 298 showContextMenu(item->menu(), QRect(), env); 299 } else { 300 if (item->isCheckable()) 301 item->setChecked(checked); 302 item->activated(); 303 } 304 } 305 } else { 306 QAndroidPlatformMenu *menu = static_cast<QAndroidPlatformMenu *>(visibleMenuBar->menuForId(menuId)); 307 if (menu) 308 showContextMenu(menu, QRect(), env); 309 } 310 311 return JNI_TRUE; 312 } 313 onOptionsMenuClosed(JNIEnv *,jobject,jobject)314 static void onOptionsMenuClosed(JNIEnv */*env*/, jobject /*thiz*/, jobject /*menu*/) 315 { 316 } 317 onCreateContextMenu(JNIEnv * env,jobject,jobject menu)318 static void onCreateContextMenu(JNIEnv *env, jobject /*thiz*/, jobject menu) 319 { 320 env->CallVoidMethod(menu, clearMenuMethodID); 321 QMutexLocker lock(&visibleMenuMutex); 322 if (!visibleMenu) 323 return; 324 325 QString menuText = removeAmpersandEscapes(visibleMenu->text()); 326 jstring jtext = env->NewString(reinterpret_cast<const jchar*>(menuText.data()), 327 menuText.length()); 328 env->CallObjectMethod(menu, setHeaderTitleContextMenuMethodID, jtext); 329 env->DeleteLocalRef(jtext); 330 addAllMenuItemsToMenu(env, menu, visibleMenu); 331 } 332 fillContextMenu(JNIEnv * env,jobject,jobject menu)333 static void fillContextMenu(JNIEnv *env, jobject /*thiz*/, jobject menu) 334 { 335 env->CallVoidMethod(menu, clearMenuMethodID); 336 QMutexLocker lock(&visibleMenuMutex); 337 if (!visibleMenu) 338 return; 339 340 addAllMenuItemsToMenu(env, menu, visibleMenu); 341 } 342 onContextItemSelected(JNIEnv * env,jobject,jint menuId,jboolean checked)343 static jboolean onContextItemSelected(JNIEnv *env, jobject /*thiz*/, jint menuId, jboolean checked) 344 { 345 QMutexLocker lock(&visibleMenuMutex); 346 QAndroidPlatformMenuItem * item = static_cast<QAndroidPlatformMenuItem *>(visibleMenu->menuItemForId(menuId)); 347 if (item) { 348 if (item->menu()) { 349 showContextMenu(item->menu(), QRect(), env); 350 } else { 351 if (item->isCheckable()) 352 item->setChecked(checked); 353 item->activated(); 354 visibleMenu->aboutToHide(); 355 visibleMenu = 0; 356 for (QAndroidPlatformMenu *menu : qAsConst(pendingContextMenus)) { 357 if (menu->isVisible()) 358 menu->aboutToHide(); 359 } 360 pendingContextMenus.clear(); 361 } 362 } 363 return JNI_TRUE; 364 } 365 onContextMenuClosed(JNIEnv * env,jobject,jobject)366 static void onContextMenuClosed(JNIEnv *env, jobject /*thiz*/, jobject /*menu*/) 367 { 368 QMutexLocker lock(&visibleMenuMutex); 369 if (!visibleMenu) 370 return; 371 372 visibleMenu->aboutToHide(); 373 visibleMenu = 0; 374 if (!pendingContextMenus.empty()) 375 showContextMenu(pendingContextMenus.takeLast(), QRect(), env); 376 } 377 378 static JNINativeMethod methods[] = { 379 {"onPrepareOptionsMenu", "(Landroid/view/Menu;)Z", (void *)onPrepareOptionsMenu}, 380 {"onOptionsItemSelected", "(IZ)Z", (void *)onOptionsItemSelected}, 381 {"onOptionsMenuClosed", "(Landroid/view/Menu;)V", (void*)onOptionsMenuClosed}, 382 {"onCreateContextMenu", "(Landroid/view/ContextMenu;)V", (void *)onCreateContextMenu}, 383 {"fillContextMenu", "(Landroid/view/Menu;)V", (void *)fillContextMenu}, 384 {"onContextItemSelected", "(IZ)Z", (void *)onContextItemSelected}, 385 {"onContextMenuClosed", "(Landroid/view/Menu;)V", (void*)onContextMenuClosed}, 386 }; 387 388 #define FIND_AND_CHECK_CLASS(CLASS_NAME) \ 389 clazz = env->FindClass(CLASS_NAME); \ 390 if (!clazz) { \ 391 __android_log_print(ANDROID_LOG_FATAL, qtTagText(), classErrorMsgFmt(), CLASS_NAME); \ 392 return false; \ 393 } 394 395 #define GET_AND_CHECK_METHOD(VAR, CLASS, METHOD_NAME, METHOD_SIGNATURE) \ 396 VAR = env->GetMethodID(CLASS, METHOD_NAME, METHOD_SIGNATURE); \ 397 if (!VAR) { \ 398 __android_log_print(ANDROID_LOG_FATAL, qtTagText(), methodErrorMsgFmt(), METHOD_NAME, METHOD_SIGNATURE); \ 399 return false; \ 400 } 401 402 #define GET_AND_CHECK_STATIC_METHOD(VAR, CLASS, METHOD_NAME, METHOD_SIGNATURE) \ 403 VAR = env->GetStaticMethodID(CLASS, METHOD_NAME, METHOD_SIGNATURE); \ 404 if (!VAR) { \ 405 __android_log_print(ANDROID_LOG_FATAL, qtTagText(), methodErrorMsgFmt(), METHOD_NAME, METHOD_SIGNATURE); \ 406 return false; \ 407 } 408 409 #define GET_AND_CHECK_STATIC_FIELD(VAR, CLASS, FIELD_NAME, FIELD_SIGNATURE) \ 410 VAR = env->GetStaticFieldID(CLASS, FIELD_NAME, FIELD_SIGNATURE); \ 411 if (!VAR) { \ 412 __android_log_print(ANDROID_LOG_FATAL, qtTagText(), methodErrorMsgFmt(), FIELD_NAME, FIELD_SIGNATURE); \ 413 return false; \ 414 } 415 registerNatives(JNIEnv * env)416 bool registerNatives(JNIEnv *env) 417 { 418 jclass appClass = applicationClass(); 419 420 if (env->RegisterNatives(appClass, methods, sizeof(methods) / sizeof(methods[0])) < 0) { 421 __android_log_print(ANDROID_LOG_FATAL,"Qt", "RegisterNatives failed"); 422 return false; 423 } 424 425 GET_AND_CHECK_STATIC_METHOD(openContextMenuMethodID, appClass, "openContextMenu", "(IIII)V"); 426 427 jclass clazz; 428 FIND_AND_CHECK_CLASS("android/view/Menu"); 429 GET_AND_CHECK_METHOD(clearMenuMethodID, clazz, "clear", "()V"); 430 GET_AND_CHECK_METHOD(addMenuItemMethodID, clazz, "add", "(IIILjava/lang/CharSequence;)Landroid/view/MenuItem;"); 431 jfieldID menuNoneFiledId; 432 GET_AND_CHECK_STATIC_FIELD(menuNoneFiledId, clazz, "NONE", "I"); 433 menuNoneValue = env->GetStaticIntField(clazz, menuNoneFiledId); 434 435 FIND_AND_CHECK_CLASS("android/view/ContextMenu"); 436 GET_AND_CHECK_METHOD(setHeaderTitleContextMenuMethodID, clazz, "setHeaderTitle","(Ljava/lang/CharSequence;)Landroid/view/ContextMenu;"); 437 438 FIND_AND_CHECK_CLASS("android/view/MenuItem"); 439 GET_AND_CHECK_METHOD(setCheckableMenuItemMethodID, clazz, "setCheckable", "(Z)Landroid/view/MenuItem;"); 440 GET_AND_CHECK_METHOD(setCheckedMenuItemMethodID, clazz, "setChecked", "(Z)Landroid/view/MenuItem;"); 441 GET_AND_CHECK_METHOD(setEnabledMenuItemMethodID, clazz, "setEnabled", "(Z)Landroid/view/MenuItem;"); 442 GET_AND_CHECK_METHOD(setIconMenuItemMethodID, clazz, "setIcon", "(Landroid/graphics/drawable/Drawable;)Landroid/view/MenuItem;"); 443 GET_AND_CHECK_METHOD(setVisibleMenuItemMethodID, clazz, "setVisible", "(Z)Landroid/view/MenuItem;"); 444 return true; 445 } 446 } 447 448 QT_END_NAMESPACE 449