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