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 "androidjnimain.h"
41 #include "androidjnimenu.h"
42 #include "qandroidplatformtheme.h"
43 #include "qandroidplatformmenubar.h"
44 #include "qandroidplatformmenu.h"
45 #include "qandroidplatformmenuitem.h"
46 #include "qandroidplatformdialoghelpers.h"
47 #include "qandroidplatformfiledialoghelper.h"
48 
49 #include <QCoreApplication>
50 #include <QDebug>
51 #include <QFileInfo>
52 #include <QJsonDocument>
53 #include <QVariant>
54 
55 #include <private/qguiapplication_p.h>
56 #include <private/qhighdpiscaling_p.h>
57 #include <qandroidplatformintegration.h>
58 
59 QT_BEGIN_NAMESPACE
60 
61 namespace {
62     const int textStyle_bold = 1;
63     const int textStyle_italic = 2;
64 
65     const int typeface_sans = 1;
66     const int typeface_serif = 2;
67     const int typeface_monospace = 3;
68 }
69 
fontType(const QString & androidControl)70 static int fontType(const QString &androidControl)
71 {
72     if (androidControl == QLatin1String("defaultStyle"))
73         return QPlatformTheme::SystemFont;
74     if (androidControl == QLatin1String("textViewStyle"))
75         return QPlatformTheme::LabelFont;
76     else if (androidControl == QLatin1String("buttonStyle"))
77         return QPlatformTheme::PushButtonFont;
78     else if (androidControl == QLatin1String("checkboxStyle"))
79         return QPlatformTheme::CheckBoxFont;
80     else if (androidControl == QLatin1String("radioButtonStyle"))
81         return QPlatformTheme::RadioButtonFont;
82     else if (androidControl == QLatin1String("simple_list_item_single_choice"))
83         return QPlatformTheme::ItemViewFont;
84     else if (androidControl == QLatin1String("simple_spinner_dropdown_item"))
85         return QPlatformTheme::ComboMenuItemFont;
86     else if (androidControl == QLatin1String("spinnerStyle"))
87         return QPlatformTheme::ComboLineEditFont;
88     else if (androidControl == QLatin1String("simple_list_item"))
89         return QPlatformTheme::ListViewFont;
90     return -1;
91 }
92 
paletteType(const QString & androidControl)93 static int paletteType(const QString &androidControl)
94 {
95     if (androidControl == QLatin1String("defaultStyle"))
96         return QPlatformTheme::SystemPalette;
97     if (androidControl == QLatin1String("textViewStyle"))
98         return QPlatformTheme::LabelPalette;
99     else if (androidControl == QLatin1String("buttonStyle"))
100         return QPlatformTheme::ButtonPalette;
101     else if (androidControl == QLatin1String("checkboxStyle"))
102         return QPlatformTheme::CheckBoxPalette;
103     else if (androidControl == QLatin1String("radioButtonStyle"))
104         return QPlatformTheme::RadioButtonPalette;
105     else if (androidControl == QLatin1String("simple_list_item_single_choice"))
106         return QPlatformTheme::ItemViewPalette;
107     else if (androidControl == QLatin1String("editTextStyle"))
108         return QPlatformTheme::TextLineEditPalette;
109     else if (androidControl == QLatin1String("spinnerStyle"))
110         return QPlatformTheme::ComboBoxPalette;
111     return -1;
112 }
113 
setPaletteColor(const QVariantMap & object,QPalette & palette,QPalette::ColorRole role)114 static void setPaletteColor(const QVariantMap &object,
115                                     QPalette &palette,
116                                     QPalette::ColorRole role)
117 {
118     // QPalette::Active -> ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET
119     palette.setColor(QPalette::Active,
120                      role,
121                      QRgb(object.value(QLatin1String("ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET")).toInt()));
122 
123     // QPalette::Inactive -> ENABLED_STATE_SET
124     palette.setColor(QPalette::Inactive,
125                      role,
126                      QRgb(object.value(QLatin1String("ENABLED_STATE_SET")).toInt()));
127 
128     // QPalette::Disabled -> EMPTY_STATE_SET
129     palette.setColor(QPalette::Disabled,
130                      role,
131                      QRgb(object.value(QLatin1String("EMPTY_STATE_SET")).toInt()));
132 
133     palette.setColor(QPalette::Current, role, palette.color(QPalette::Active, role));
134 
135     if (role == QPalette::WindowText) {
136         // QPalette::BrightText -> PRESSED
137         // QPalette::Active -> PRESSED_ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET
138         palette.setColor(QPalette::Active,
139                          QPalette::BrightText,
140                          QRgb(object.value(QLatin1String("PRESSED_ENABLED_FOCUSED_WINDOW_FOCUSED_STATE_SET")).toInt()));
141 
142         // QPalette::Inactive -> PRESSED_ENABLED_STATE_SET
143         palette.setColor(QPalette::Inactive,
144                          QPalette::BrightText,
145                          QRgb(object.value(QLatin1String("PRESSED_ENABLED_STATE_SET")).toInt()));
146 
147         // QPalette::Disabled -> PRESSED_STATE_SET
148         palette.setColor(QPalette::Disabled,
149                          QPalette::BrightText,
150                          QRgb(object.value(QLatin1String("PRESSED_STATE_SET")).toInt()));
151 
152         palette.setColor(QPalette::Current, QPalette::BrightText, palette.color(QPalette::Active, QPalette::BrightText));
153 
154         // QPalette::HighlightedText -> SELECTED
155         // QPalette::Active -> ENABLED_SELECTED_WINDOW_FOCUSED_STATE_SET
156         palette.setColor(QPalette::Active,
157                          QPalette::HighlightedText,
158                          QRgb(object.value(QLatin1String("ENABLED_SELECTED_WINDOW_FOCUSED_STATE_SET")).toInt()));
159 
160         // QPalette::Inactive -> ENABLED_SELECTED_STATE_SET
161         palette.setColor(QPalette::Inactive,
162                          QPalette::HighlightedText,
163                          QRgb(object.value(QLatin1String("ENABLED_SELECTED_STATE_SET")).toInt()));
164 
165         // QPalette::Disabled -> SELECTED_STATE_SET
166         palette.setColor(QPalette::Disabled,
167                          QPalette::HighlightedText,
168                          QRgb(object.value(QLatin1String("SELECTED_STATE_SET")).toInt()));
169 
170         palette.setColor(QPalette::Current,
171                          QPalette::HighlightedText,
172                          palette.color(QPalette::Active, QPalette::HighlightedText));
173 
174         // Same colors for Text
175         palette.setColor(QPalette::Active, QPalette::Text, palette.color(QPalette::Active, role));
176         palette.setColor(QPalette::Inactive, QPalette::Text, palette.color(QPalette::Inactive, role));
177         palette.setColor(QPalette::Disabled, QPalette::Text, palette.color(QPalette::Disabled, role));
178         palette.setColor(QPalette::Current, QPalette::Text, palette.color(QPalette::Current, role));
179 
180         // And for ButtonText
181         palette.setColor(QPalette::Active, QPalette::ButtonText, palette.color(QPalette::Active, role));
182         palette.setColor(QPalette::Inactive, QPalette::ButtonText, palette.color(QPalette::Inactive, role));
183         palette.setColor(QPalette::Disabled, QPalette::ButtonText, palette.color(QPalette::Disabled, role));
184         palette.setColor(QPalette::Current, QPalette::ButtonText, palette.color(QPalette::Current, role));
185     }
186 }
187 
loadStyleData()188 QJsonObject AndroidStyle::loadStyleData()
189 {
190     QString stylePath(QLatin1String(qgetenv("MINISTRO_ANDROID_STYLE_PATH")));
191     const QLatin1Char slashChar('/');
192     if (!stylePath.isEmpty() && !stylePath.endsWith(slashChar))
193         stylePath += slashChar;
194 
195     QString androidTheme = QLatin1String(qgetenv("QT_ANDROID_THEME"));
196     if (!androidTheme.isEmpty() && !androidTheme.endsWith(slashChar))
197         androidTheme += slashChar;
198 
199     if (stylePath.isEmpty()) {
200         stylePath = QLatin1String("/data/data/org.kde.necessitas.ministro/files/dl/style/")
201                   + QLatin1String(qgetenv("QT_ANDROID_THEME_DISPLAY_DPI")) + slashChar;
202     }
203     Q_ASSERT(!stylePath.isEmpty());
204 
205     if (!androidTheme.isEmpty() && QFileInfo::exists(stylePath + androidTheme + QLatin1String("style.json")))
206         stylePath += androidTheme;
207 
208     QFile f(stylePath + QLatin1String("style.json"));
209     if (!f.open(QIODevice::ReadOnly))
210         return QJsonObject();
211 
212     QJsonParseError error;
213     QJsonDocument document = QJsonDocument::fromJson(f.readAll(), &error);
214     if (Q_UNLIKELY(document.isNull())) {
215         qCritical() << error.errorString();
216         return QJsonObject();
217     }
218 
219     if (Q_UNLIKELY(!document.isObject())) {
220         qCritical("Style.json does not contain a valid style.");
221         return QJsonObject();
222     }
223     return document.object();
224 }
225 
loadAndroidStyle(QPalette * defaultPalette)226 static std::shared_ptr<AndroidStyle> loadAndroidStyle(QPalette *defaultPalette)
227 {
228     double pixelDensity = QHighDpiScaling::isActive() ? QtAndroid::pixelDensity() : 1.0;
229     std::shared_ptr<AndroidStyle> style = std::make_shared<AndroidStyle>();
230     style->m_styleData = AndroidStyle::loadStyleData();
231     if (style->m_styleData.isEmpty())
232         return std::shared_ptr<AndroidStyle>();
233 
234     for (QJsonObject::const_iterator objectIterator = style->m_styleData.constBegin();
235          objectIterator != style->m_styleData.constEnd();
236          ++objectIterator) {
237         QString key = objectIterator.key();
238         QJsonValue value = objectIterator.value();
239         if (!value.isObject()) {
240             qWarning("Style.json structure is unrecognized.");
241             continue;
242         }
243         QJsonObject item = value.toObject();
244         QJsonObject::const_iterator attributeIterator = item.find(QLatin1String("qtClass"));
245         QByteArray qtClassName;
246         if (attributeIterator != item.constEnd()) {
247             // The item has palette and font information for a specific Qt Class (e.g. QWidget, QPushButton, etc.)
248             qtClassName = attributeIterator.value().toString().toLatin1();
249         }
250         const int ft = fontType(key);
251         if (ft > -1 || !qtClassName.isEmpty()) {
252             // Extract font information
253             QFont font;
254 
255             // Font size (in pixels)
256             attributeIterator = item.find(QLatin1String("TextAppearance_textSize"));
257             if (attributeIterator != item.constEnd())
258                 font.setPixelSize(int(attributeIterator.value().toDouble() / pixelDensity));
259 
260             // Font style
261             attributeIterator = item.find(QLatin1String("TextAppearance_textStyle"));
262             if (attributeIterator != item.constEnd()) {
263                 const int style = int(attributeIterator.value().toDouble());
264                 font.setBold(style & textStyle_bold);
265                 font.setItalic(style & textStyle_italic);
266             }
267 
268             // Font typeface
269             attributeIterator = item.find(QLatin1String("TextAppearance_typeface"));
270             if (attributeIterator != item.constEnd()) {
271                 QFont::StyleHint styleHint = QFont::AnyStyle;
272                 switch (int(attributeIterator.value().toDouble())) {
273                 case typeface_sans:
274                     styleHint = QFont::SansSerif;
275                     break;
276                 case typeface_serif:
277                     styleHint = QFont::Serif;
278                     break;
279                 case typeface_monospace:
280                     styleHint = QFont::Monospace;
281                     break;
282                 }
283                 font.setStyleHint(styleHint, QFont::PreferMatch);
284             }
285             if (!qtClassName.isEmpty())
286                 style->m_QWidgetsFonts.insert(qtClassName, font);
287 
288             if (ft > -1) {
289                 style->m_fonts.insert(ft, font);
290                 if (ft == QPlatformTheme::SystemFont)
291                     QGuiApplication::setFont(font);
292             }
293             // Extract font information
294         }
295 
296         const int pt = paletteType(key);
297         if (pt > -1 || !qtClassName.isEmpty()) {
298             // Extract palette information
299             QPalette palette = *defaultPalette;
300 
301             attributeIterator = item.find(QLatin1String("defaultTextColorPrimary"));
302             if (attributeIterator != item.constEnd())
303                 palette.setColor(QPalette::WindowText, QRgb(int(attributeIterator.value().toDouble())));
304 
305             attributeIterator = item.find(QLatin1String("defaultBackgroundColor"));
306             if (attributeIterator != item.constEnd())
307                 palette.setColor(QPalette::Background, QRgb(int(attributeIterator.value().toDouble())));
308 
309             attributeIterator = item.find(QLatin1String("TextAppearance_textColor"));
310             if (attributeIterator != item.constEnd())
311                 setPaletteColor(attributeIterator.value().toObject().toVariantMap(), palette, QPalette::WindowText);
312 
313             attributeIterator = item.find(QLatin1String("TextAppearance_textColorLink"));
314             if (attributeIterator != item.constEnd())
315                 setPaletteColor(attributeIterator.value().toObject().toVariantMap(), palette, QPalette::Link);
316 
317             attributeIterator = item.find(QLatin1String("TextAppearance_textColorHighlight"));
318             if (attributeIterator != item.constEnd())
319                 palette.setColor(QPalette::Highlight, QRgb(int(attributeIterator.value().toDouble())));
320 
321             if (pt == QPlatformTheme::SystemPalette)
322                 *defaultPalette = style->m_standardPalette = palette;
323 
324             if (pt > -1)
325                 style->m_palettes.insert(pt, palette);
326             // Extract palette information
327         }
328     }
329     return style;
330 }
331 
QAndroidPlatformTheme(QAndroidPlatformNativeInterface * androidPlatformNativeInterface)332 QAndroidPlatformTheme::QAndroidPlatformTheme(QAndroidPlatformNativeInterface *androidPlatformNativeInterface)
333 {
334     QColor background(229, 229, 229);
335     QColor light = background.lighter(150);
336     QColor mid(background.darker(130));
337     QColor midLight = mid.lighter(110);
338     QColor base(249, 249, 249);
339     QColor disabledBase(background);
340     QColor dark = background.darker(150);
341     QColor darkDisabled = dark.darker(110);
342     QColor text = Qt::black;
343     QColor highlightedText = Qt::black;
344     QColor disabledText = QColor(190, 190, 190);
345     QColor button(241, 241, 241);
346     QColor shadow(201, 201, 201);
347     QColor highlight(148, 210, 231);
348     QColor disabledShadow = shadow.lighter(150);
349 
350     m_defaultPalette = QPalette(Qt::black,background,light,dark,mid,text,base);
351     m_defaultPalette.setBrush(QPalette::Midlight, midLight);
352     m_defaultPalette.setBrush(QPalette::Button, button);
353     m_defaultPalette.setBrush(QPalette::Shadow, shadow);
354     m_defaultPalette.setBrush(QPalette::HighlightedText, highlightedText);
355 
356     m_defaultPalette.setBrush(QPalette::Disabled, QPalette::Text, disabledText);
357     m_defaultPalette.setBrush(QPalette::Disabled, QPalette::WindowText, disabledText);
358     m_defaultPalette.setBrush(QPalette::Disabled, QPalette::ButtonText, disabledText);
359     m_defaultPalette.setBrush(QPalette::Disabled, QPalette::Base, disabledBase);
360     m_defaultPalette.setBrush(QPalette::Disabled, QPalette::Dark, darkDisabled);
361     m_defaultPalette.setBrush(QPalette::Disabled, QPalette::Shadow, disabledShadow);
362 
363     m_defaultPalette.setBrush(QPalette::Active, QPalette::Highlight, highlight);
364     m_defaultPalette.setBrush(QPalette::Inactive, QPalette::Highlight, highlight);
365     m_defaultPalette.setBrush(QPalette::Disabled, QPalette::Highlight, highlight.lighter(150));
366     m_androidStyleData = loadAndroidStyle(&m_defaultPalette);
367     QGuiApplication::setPalette(m_defaultPalette);
368     androidPlatformNativeInterface->m_androidStyle = m_androidStyleData;
369 
370     // default in case the style has not set a font
371     m_systemFont = QFont(QLatin1String("Roboto"), 14.0 * 100 / 72); // keep default size the same after changing from 100 dpi to 72 dpi
372 }
373 
createPlatformMenuBar() const374 QPlatformMenuBar *QAndroidPlatformTheme::createPlatformMenuBar() const
375 {
376     return new QAndroidPlatformMenuBar;
377 }
378 
createPlatformMenu() const379 QPlatformMenu *QAndroidPlatformTheme::createPlatformMenu() const
380 {
381     return new QAndroidPlatformMenu;
382 }
383 
createPlatformMenuItem() const384 QPlatformMenuItem *QAndroidPlatformTheme::createPlatformMenuItem() const
385 {
386     return new QAndroidPlatformMenuItem;
387 }
388 
showPlatformMenuBar()389 void QAndroidPlatformTheme::showPlatformMenuBar()
390 {
391     QtAndroidMenu::openOptionsMenu();
392 }
393 
paletteType(QPlatformTheme::Palette type)394 static inline int paletteType(QPlatformTheme::Palette type)
395 {
396     switch (type) {
397     case QPlatformTheme::ToolButtonPalette:
398     case QPlatformTheme::ButtonPalette:
399         return QPlatformTheme::ButtonPalette;
400 
401     case QPlatformTheme::CheckBoxPalette:
402         return QPlatformTheme::CheckBoxPalette;
403 
404     case QPlatformTheme::RadioButtonPalette:
405         return QPlatformTheme::RadioButtonPalette;
406 
407     case QPlatformTheme::ComboBoxPalette:
408         return QPlatformTheme::ComboBoxPalette;
409 
410     case QPlatformTheme::TextEditPalette:
411     case QPlatformTheme::TextLineEditPalette:
412         return QPlatformTheme::TextLineEditPalette;
413 
414     case QPlatformTheme::ItemViewPalette:
415         return QPlatformTheme::ItemViewPalette;
416 
417     default:
418         return QPlatformTheme::SystemPalette;
419     }
420 }
421 
palette(Palette type) const422 const QPalette *QAndroidPlatformTheme::palette(Palette type) const
423 {
424     if (m_androidStyleData) {
425         auto it = m_androidStyleData->m_palettes.find(paletteType(type));
426         if (it != m_androidStyleData->m_palettes.end())
427             return &(it.value());
428     }
429     return &m_defaultPalette;
430 }
431 
fontType(QPlatformTheme::Font type)432 static inline int fontType(QPlatformTheme::Font type)
433 {
434     switch (type) {
435     case QPlatformTheme::LabelFont:
436         return QPlatformTheme::SystemFont;
437 
438     case QPlatformTheme::ToolButtonFont:
439         return QPlatformTheme::PushButtonFont;
440 
441     default:
442         return type;
443     }
444 }
445 
font(Font type) const446 const QFont *QAndroidPlatformTheme::font(Font type) const
447 {
448     if (m_androidStyleData) {
449         auto it = m_androidStyleData->m_fonts.find(fontType(type));
450         if (it != m_androidStyleData->m_fonts.end())
451             return &(it.value());
452     }
453 
454     if (type == QPlatformTheme::SystemFont)
455         return &m_systemFont;
456     return 0;
457 }
458 
themeHint(ThemeHint hint) const459 QVariant QAndroidPlatformTheme::themeHint(ThemeHint hint) const
460 {
461     switch (hint) {
462     case StyleNames:
463         if (qEnvironmentVariableIntValue("QT_USE_ANDROID_NATIVE_STYLE")
464                 && m_androidStyleData) {
465             return QStringList(QLatin1String("android"));
466         }
467         return QStringList(QLatin1String("fusion"));
468     case DialogButtonBoxLayout:
469         return QVariant(QPlatformDialogHelper::AndroidLayout);
470     case MouseDoubleClickDistance:
471     {
472             int minimumDistance = qEnvironmentVariableIntValue("QT_ANDROID_MINIMUM_MOUSE_DOUBLE_CLICK_DISTANCE");
473             int ret = minimumDistance;
474 
475             QAndroidPlatformIntegration *platformIntegration
476                     = static_cast<QAndroidPlatformIntegration *>(QGuiApplicationPrivate::platformIntegration());
477             QAndroidPlatformScreen *platformScreen = platformIntegration->screen();
478             if (platformScreen != 0) {
479                 QScreen *screen = platformScreen->screen();
480                 qreal dotsPerInch = screen->physicalDotsPerInch();
481 
482                 // Allow 15% of an inch between clicks when double clicking
483                 int distance = qRound(dotsPerInch * 0.15);
484                 if (distance > minimumDistance)
485                     ret = distance;
486             }
487 
488             if (ret > 0)
489                 return ret;
490 
491             Q_FALLTHROUGH();
492     }
493     default:
494         return QPlatformTheme::themeHint(hint);
495     }
496 }
497 
standardButtonText(int button) const498 QString QAndroidPlatformTheme::standardButtonText(int button) const
499 {
500     switch (button) {
501     case QPlatformDialogHelper::Yes:
502         return QCoreApplication::translate("QAndroidPlatformTheme", "Yes");
503     case QPlatformDialogHelper::YesToAll:
504         return QCoreApplication::translate("QAndroidPlatformTheme", "Yes to All");
505     case QPlatformDialogHelper::No:
506         return QCoreApplication::translate("QAndroidPlatformTheme", "No");
507     case QPlatformDialogHelper::NoToAll:
508         return QCoreApplication::translate("QAndroidPlatformTheme", "No to All");
509     }
510     return QPlatformTheme::standardButtonText(button);
511 }
512 
usePlatformNativeDialog(QPlatformTheme::DialogType type) const513 bool QAndroidPlatformTheme::usePlatformNativeDialog(QPlatformTheme::DialogType type) const
514 {
515     if (type == MessageDialog)
516         return qEnvironmentVariableIntValue("QT_USE_ANDROID_NATIVE_DIALOGS") == 1;
517     if (type == FileDialog)
518         return true;
519     return false;
520 }
521 
createPlatformDialogHelper(QPlatformTheme::DialogType type) const522 QPlatformDialogHelper *QAndroidPlatformTheme::createPlatformDialogHelper(QPlatformTheme::DialogType type) const
523 {
524     switch (type) {
525     case MessageDialog:
526         return new QtAndroidDialogHelpers::QAndroidPlatformMessageDialogHelper;
527     case FileDialog:
528         return new QtAndroidFileDialogHelper::QAndroidPlatformFileDialogHelper;
529     default:
530         return 0;
531     }
532 }
533 
534 QT_END_NAMESPACE
535