From ba0b7c62a48076c30f12e5daa11ceb92bbe67ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20T=C5=AFma?= Date: Sat, 28 May 2022 14:05:14 +0200 Subject: [PATCH] Android port --- .appveyor.yml | 2 +- gpxsee.pro | 47 ++++- pkg/android/AndroidManifest.xml | 18 ++ pkg/android/build.gradle | 78 ++++++++ pkg/android/gradle.properties | 14 ++ pkg/android/res/drawable-hdpi/icon.png | Bin 0 -> 2864 bytes pkg/android/res/drawable-ldpi/icon.png | Bin 0 -> 1134 bytes pkg/android/res/drawable-mdpi/icon.png | Bin 0 -> 1977 bytes pkg/android/res/drawable-xhdpi/icon.png | Bin 0 -> 4538 bytes pkg/android/res/drawable-xxhdpi/icon.png | Bin 0 -> 8648 bytes pkg/android/res/drawable-xxxhdpi/icon.png | Bin 0 -> 12359 bytes pkg/android/res/values/libs.xml | 20 ++ pkg/gpxsee64.nsi | 2 +- src/GUI/app.cpp | 12 ++ src/GUI/app.h | 7 +- src/GUI/cadencegraph.cpp | 9 +- src/GUI/colorbox.cpp | 22 +- src/GUI/dirselectwidget.cpp | 4 +- src/GUI/elevationgraph.cpp | 11 + src/GUI/filebrowser.cpp | 19 +- src/GUI/filebrowser.h | 9 +- src/GUI/fileselectwidget.cpp | 4 +- src/GUI/gearratiograph.cpp | 9 + src/GUI/gui.cpp | 234 ++++++++++++++++++++-- src/GUI/gui.h | 32 ++- src/GUI/heartrategraph.cpp | 7 + src/GUI/mapview.cpp | 55 ++--- src/GUI/mapview.h | 6 + src/GUI/navigationwidget.cpp | 101 ++++++++++ src/GUI/navigationwidget.h | 35 ++++ src/GUI/optionsdialog.cpp | 27 ++- src/GUI/pdfexportdialog.cpp | 10 + src/GUI/pngexportdialog.cpp | 10 + src/GUI/powergraph.cpp | 9 +- src/GUI/settings.h | 2 + src/GUI/speedgraph.cpp | 7 + src/GUI/temperaturegraph.cpp | 9 + src/GUI/thumbnail.cpp | 10 + src/common/programpaths.cpp | 97 +++++---- src/common/util.cpp | 86 +++++++- src/common/util.h | 1 + 41 files changed, 913 insertions(+), 112 deletions(-) create mode 100644 pkg/android/AndroidManifest.xml create mode 100644 pkg/android/build.gradle create mode 100644 pkg/android/gradle.properties create mode 100644 pkg/android/res/drawable-hdpi/icon.png create mode 100644 pkg/android/res/drawable-ldpi/icon.png create mode 100644 pkg/android/res/drawable-mdpi/icon.png create mode 100644 pkg/android/res/drawable-xhdpi/icon.png create mode 100644 pkg/android/res/drawable-xxhdpi/icon.png create mode 100644 pkg/android/res/drawable-xxxhdpi/icon.png create mode 100644 pkg/android/res/values/libs.xml create mode 100644 src/GUI/navigationwidget.cpp create mode 100644 src/GUI/navigationwidget.h diff --git a/.appveyor.yml b/.appveyor.yml index ece7fffc..12112de1 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,4 +1,4 @@ -version: 10.8.{build} +version: 11.0.{build} configuration: - Release diff --git a/gpxsee.pro b/gpxsee.pro index 56e2f083..28ea5f1a 100644 --- a/gpxsee.pro +++ b/gpxsee.pro @@ -1,9 +1,9 @@ -unix:!macx { +unix:!macx:!android { TARGET = gpxsee } else { TARGET = GPXSee } -VERSION = 10.8 +VERSION = 11.0 QT += core \ gui \ @@ -13,7 +13,8 @@ QT += core \ concurrent \ widgets \ printsupport \ - positioning + positioning \ + svg greaterThan(QT_MAJOR_VERSION, 5) { QT += openglwidgets \ core5compat @@ -24,6 +25,7 @@ INCLUDEPATH += ./src HEADERS += src/common/config.h \ src/GUI/crosshairitem.h \ src/GUI/motioninfoitem.h \ + src/GUI/navigationwidget.h \ src/GUI/pluginparameters.h \ src/common/garmin.h \ src/common/coordinates.h \ @@ -246,6 +248,7 @@ HEADERS += src/common/config.h \ SOURCES += src/main.cpp \ src/GUI/crosshairitem.cpp \ src/GUI/motioninfoitem.cpp \ + src/GUI/navigationwidget.cpp \ src/GUI/pluginparameters.cpp \ src/common/coordinates.cpp \ src/common/rectc.cpp \ @@ -504,7 +507,7 @@ win32 { NOGDI } -unix:!macx { +unix:!macx:!android { isEmpty(PREFIX):PREFIX = /usr/local maps.files = $$files(pkg/maps/*) @@ -524,3 +527,39 @@ unix:!macx { target.path = $$PREFIX/bin INSTALLS += target maps csv symbols locale icon desktop mime } + +android { + defineReplace(versionCode) { + segments = $$split(1, ".") + for (segment, segments): \ + vCode = "$$first(vCode)$$format_number($$segment, width=3 zeropad)" + contains(ANDROID_TARGET_ARCH, armeabi-v7a): \ + suffix = 0 + contains(ANDROID_TARGET_ARCH, arm64-v8a): \ + suffix = 1 + contains(ANDROID_TARGET_ARCH, x86): \ + suffix = 2 + contains(ANDROID_TARGET_ARCH, x86_64): \ + suffix = 3 + + return($$first(vCode)$$first(suffix)) + } + + include($$OPENSSL_PATH/openssl.pri) + + ANDROID_VERSION_NAME = $$VERSION + ANDROID_VERSION_CODE = $$versionCode($$ANDROID_VERSION_NAME) + ANDROID_PACKAGE_SOURCE_DIR = $$PWD/pkg/android + DISTFILES += \ + pkg/android/AndroidManifest.xml \ + pkg/android/build.gradle \ + pkg/android/res/values/libs.xml + + maps.files = $$files(pkg/maps/*) + maps.path = /assets/maps + csv.files = $$files(pkg/csv/*) + csv.path = /assets/csv + symbols.files = $$files(icons/symbols/*.png) + symbols.path = /assets/symbols + INSTALLS += maps csv symbols +} diff --git a/pkg/android/AndroidManifest.xml b/pkg/android/AndroidManifest.xml new file mode 100644 index 00000000..39fae897 --- /dev/null +++ b/pkg/android/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/pkg/android/build.gradle b/pkg/android/build.gradle new file mode 100644 index 00000000..0a8601c4 --- /dev/null +++ b/pkg/android/build.gradle @@ -0,0 +1,78 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.0.2' + } +} + +repositories { + google() + mavenCentral() +} + +apply plugin: 'com.android.application' + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) +} + +android { + /******************************************************* + * The following variables: + * - androidBuildToolsVersion, + * - androidCompileSdkVersion + * - qtAndroidDir - holds the path to qt android files + * needed to build any Qt application + * on Android. + * + * are defined in gradle.properties file. This file is + * updated by QtCreator and androiddeployqt tools. + * Changing them manually might break the compilation! + *******************************************************/ + + compileSdkVersion androidCompileSdkVersion.toInteger() + buildToolsVersion androidBuildToolsVersion + ndkVersion androidNdkVersion + + sourceSets { + main { + manifest.srcFile 'AndroidManifest.xml' + java.srcDirs = [qtAndroidDir + '/src', 'src', 'java'] + aidl.srcDirs = [qtAndroidDir + '/src', 'src', 'aidl'] + res.srcDirs = [qtAndroidDir + '/res', 'res'] + resources.srcDirs = ['resources'] + renderscript.srcDirs = ['src'] + assets.srcDirs = ['assets'] + jniLibs.srcDirs = ['libs'] + } + } + + tasks.withType(JavaCompile) { + options.incremental = true + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + lintOptions { + abortOnError false + } + + // Do not compress Qt binary resources file + aaptOptions { + noCompress 'rcc' + } + + defaultConfig { + resConfig "en" + minSdkVersion qtMinSdkVersion + targetSdkVersion qtTargetSdkVersion + ndk.abiFilters = qtTargetAbiList.split(",") + } +} diff --git a/pkg/android/gradle.properties b/pkg/android/gradle.properties new file mode 100644 index 00000000..263d7023 --- /dev/null +++ b/pkg/android/gradle.properties @@ -0,0 +1,14 @@ +# Project-wide Gradle settings. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2500m -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# Enable building projects in parallel +org.gradle.parallel=true + +# Gradle caching allows reusing the build artifacts from a previous +# build with the same inputs. However, over time, the cache size will +# grow. Uncomment the following line to enable it. +#org.gradle.caching=true diff --git a/pkg/android/res/drawable-hdpi/icon.png b/pkg/android/res/drawable-hdpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..118bb5faf732537346e1cb03fb11a73d71160530 GIT binary patch literal 2864 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 literal 0 HcmV?d00001 diff --git a/pkg/android/res/drawable-ldpi/icon.png b/pkg/android/res/drawable-ldpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..fe1d7325617f68386cc48cd71a15c5255591100a GIT binary patch literal 1134 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANL}oCO|{#S9D_p&-oYBW9_` zz`!6`;u=vBoS#-wo>-L1P+nfHmzkGcoSayYs+V7sKKq@G6axeEZ%-G;kcig3)8jRE zSIZo~KYM0xYKrHl?I%4vxWuQ;F>up+@uW#xTyKhZX@GjTx~A*IDcqLNWb|CR4s%Tw z3R>RgG*NWthvj0oH{JXlGw0it&$kcXeDfrB_m7(S??2bZo&Wv!Z_W0T9$E~4xE`c4 zaQ=|n%k+c$fWE>{>HQk}@1Nhd&rVHMb>o&TQ`FVfAL@Nn_qbOhdLV17P{Rj9sosZr z52rGF{8`oi{rmU%*AzRKd|u6GV{ae4{yI0GkB?8wTL%{g6-yhNGmjo6Wtm7-+05I( z_#}R-{erV+eZPG9qM)kET3ub8cwb)d!0XrA-@biIFq(Pc!i9#dTerSv-EcGK!t1Xd z-rmhObB_J`_3JxRPEO8(ef#7X0zyMuXU~?7iH~>Zdr-8~z{;xY;6XgXZSNCuEH})xW=l1sW@Z8AR zHbHp89cFKD?+aO5SFBs-mX@ZrZ25B6>wDw$w{6?j#K6#UpV!{aVUpkS$y}{X{{H?p z_V&yDmM=b==kG>Zi^otJ?iSgFiB8GPHxefH9jkP-IUeT)h}MU)UQ>zkhZ=16f|aeA&pUlxZRrSiNq|8V@E58NO8^TKo3w;Yi-TeS1$|pV&KJ zZ|}wT-}AHW*u8sk&`J?m1IEzMP)=@*sa|pM@!m`mQW6st!^6W{e?55Mz@k6>^vkl{ z&EF4PyeQb!)wLjKWz(Zs^XI##r>oDJHEYAJU0rL|tl?j5I{WOR+65t6uGg)ttp3#= z>*wR);n}cz_wU2z+@aRi)@M$gT6F#O(z|)-MMXyKZEY6j=Et8tP5o747nH?+<=VBP zv^2G(hMPAdnHEH?O-o8ra(8oEvaFNw-u?SCCrmi7dGqEDIXMe%rpnVPMpiO>o(vb| zcI?^Ha{qn%!Gs_0|NV8bo6n!OJ$i-bqo+?@i`Fqb_*k)F$BvFmmx4|_o%lDTr@PxZ zKYx9|_N`kL_4WO$j69z|eym(vT+G>U@#4jpxVS!zrUpNM|H_&gou6|BI~Y{v3JVJ_ zl3TlC#gBLfrwQSF2PRKY_~*asYL;@B0?$c@Pv5t1H{Z5>d$8X0%LxV-^0wDcXZ~$8 w$&=w9!-vV|pHJ2l>3UyL%=AOr;kNzt34f;VlT&@lz`(%Z>FVdQ&MBb@0CX`A8~^|S literal 0 HcmV?d00001 diff --git a/pkg/android/res/drawable-mdpi/icon.png b/pkg/android/res/drawable-mdpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6183b854aefc92f9878ef8d8b9b8ffa75e04d415 GIT binary patch literal 1977 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F-L1P+nfHmzkGcoSayYs+V7sKKq@G6axdho2QFoNJit?$Y>wo z*JA&c=`KBZ&1sEogx3!U3Fx)#ak?bBxXle_f%Wnn8Tt7)qLSv5pHIN#g7;QDLT zrw0!nEZV8y;dk<*&Qb#-qeE9$hwFRIZ)jkM+g;|XCth5q>M&`0-rb~x1cj+ys*D>J zEo%Dw`E$|C`JbbI?O1U6<%0r?fS@2I1_4(=t*KsSzw6X5+`7fJHL91Ho$o+~$)c4j zIr-Y1&3@OZItZ;@ySAgJ$EBpiq^PLq!>g;>Jv}`qo*iD#qNb+y;mu9socsG^8yg!> zoI7{y`ug~bH*adr=HD-rk(t@3(RJeNSyl!WRn^3|x3=!6{cYxP>e1nR7LP+YX38om zES;U53=GW&8;%@txwE%gytue{(~pnRjI+#Y zUiagJA~Q3y>T=I~0oRF{nVCGiyh{&1EQq$R`C)K1%aom+J>2I&HkYZnxwlw!aiN#w*SFPgxpQ#v<*xYUahxI|A|8E5e*XM9sr(x2=9@fg!@8e5NjX{C z>(<=U!}I0KmjhW_^^Bj)f3TL#B{0R?oBL~(E#rj*gMjtdjg5>ZIXgScl`dMeXvTyI z4$Cip_WXKjsdtEmNKQ^pOd#(MndPp5DG?D8!NI|nhK2`co973uzIrLe=;Pz#{dvFR z_tkhZ=;-Tzf3sPPpo{-^V_n%|o$e^REyYz0Jx~J7#zh_UM z8rs;*30r-2`S-`i`-K_U+1UQLKe(|mnTLbp!G{kYly=_Ddp^&$dPUI6h{#CKKoLtj zyK|+luN8iOcQ@|y{nl^a%9s*LOSjf@Sj_c%cW3A1*=D(qzI@qo``2>+`7@?XbDQdQ zbkQQEW5~65}CQVxow}lyl_Dvd~MX$ z6~Wq5z2bHj9bH}?C=wYGa>O+I+K0ctzsr5!vURJWz5V>c!a^NQ&4({uytu?+Q~z%c zS8LO9zqy|zj`T{KtJufo-rG~T&}`4cPft&?9B6A}J6XEJ2w@dOA81+}w^a6?PwWntak_ zYt&ie^m8AcoSdwqtNZk5xA@P$zsskuN@u!y^=ind>wEaNriuLQ`XH?Yh$;!&w!|Gq9 zyrJ;1o0_WX$*;%mhV$(T(?e7L!`^T)gd52 zpijo~(YGa^KYiMedRpv0+Ye{nlCm-*YwOuobNi%v+sfbF`B=?6;mP*=`xWo*SROle z%*e`WR%&YMoLNzq>ZjXx?byHn{@d~iaqH8|%eTij|Cc$L#i^<;J$Q zy|->fNeZRDjCkjr+}Ln5Y2uv+p8vudnGU!!$f=)O8@>JC%#S+aTod*<1kIT_^WnDK z+hSK8yu7&7)z$w_e)H+UM28~fhWiXIwLd?(u2{d`Jw9GODJdx+B0}T2f9B4Qk_Yy) yFev;iXL!z#!~Wp@wcSMm2N)XaxtlJ0sc&vuXY_<;&Kd>=1_n=8KbLh*2~7adWU|5l literal 0 HcmV?d00001 diff --git a/pkg/android/res/drawable-xhdpi/icon.png b/pkg/android/res/drawable-xhdpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7fe1213cac729da6b785fea9ff8a0c49875d1c82 GIT binary patch literal 4538 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 literal 0 HcmV?d00001 diff --git a/pkg/android/res/drawable-xxhdpi/icon.png b/pkg/android/res/drawable-xxhdpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0371eb93e1d5128a02094bb86c00dd6670f98a16 GIT binary patch literal 8648 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 WWX$VQ{fB{pfx*+&&t;ucLK6Vgr|H!I literal 0 HcmV?d00001 diff --git a/pkg/android/res/drawable-xxxhdpi/icon.png b/pkg/android/res/drawable-xxxhdpi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..46f8b0ac199b3d9eb5d55ea602b345d614dfc790 GIT binary patch literal 12359 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 + + + + + + + + + + + + + + + + + + + diff --git a/pkg/gpxsee64.nsi b/pkg/gpxsee64.nsi index 7d1b083a..711bc32a 100644 --- a/pkg/gpxsee64.nsi +++ b/pkg/gpxsee64.nsi @@ -37,7 +37,7 @@ Unicode true ; The name of the installer Name "GPXSee" ; Program version -!define VERSION "10.8" +!define VERSION "11.0" ; The file to write OutFile "GPXSee-${VERSION}_x64.exe" diff --git a/src/GUI/app.cpp b/src/GUI/app.cpp index 2e232fc0..d2cb659c 100644 --- a/src/GUI/app.cpp +++ b/src/GUI/app.cpp @@ -61,6 +61,10 @@ App::App(int &argc, char **argv) : QApplication(argc, argv) Waypoint::loadSymbolIcons(ProgramPaths::symbolsDir()); _gui = new GUI(); + +#ifdef Q_OS_ANDROID + connect(this, &App::applicationStateChanged, this, &App::appStateChanged); +#endif // Q_OS_ANDROID } App::~App() @@ -93,6 +97,14 @@ int App::run() return exec(); } +#ifdef Q_OS_ANDROID +void App::appStateChanged(Qt::ApplicationState state) +{ + if (state == Qt::ApplicationSuspended) + _gui->writeSettings(); +} +#endif // Q_OS_ANDROID + bool App::event(QEvent *event) { if (event->type() == QEvent::FileOpen) { diff --git a/src/GUI/app.h b/src/GUI/app.h index 5c8d97c1..16a6f1dc 100644 --- a/src/GUI/app.h +++ b/src/GUI/app.h @@ -5,7 +5,7 @@ class GUI; -class App : QApplication +class App : public QApplication { Q_OBJECT @@ -17,6 +17,11 @@ public: protected: bool event(QEvent *event); +#ifdef Q_OS_ANDROID +private slots: + void appStateChanged(Qt::ApplicationState state); +#endif // Q_OS_ANDROID + private: void loadDatums(); void loadPCSs(); diff --git a/src/GUI/cadencegraph.cpp b/src/GUI/cadencegraph.cpp index 7b1d69ed..a8d49c58 100644 --- a/src/GUI/cadencegraph.cpp +++ b/src/GUI/cadencegraph.cpp @@ -24,10 +24,17 @@ void CadenceGraph::setInfo() if (_showTracks) { QLocale l(QLocale::system()); +#ifdef Q_OS_ANDROID + GraphView::addInfo(tr("Avg"), l.toString(avg() * yScale() + yOffset(), + 'f', 1) + UNIT_SPACE + yUnits()); + GraphView::addInfo(tr("Max"), l.toString(max() * yScale() + yOffset(), + 'f', 1) + UNIT_SPACE + yUnits()); +#else // Q_OS_ANDROID GraphView::addInfo(tr("Average"), l.toString(avg() * yScale() + yOffset(), 'f', 1) + UNIT_SPACE + yUnits()); GraphView::addInfo(tr("Maximum"), l.toString(max() * yScale() - + yOffset(), 'f', 1) + UNIT_SPACE + yUnits()); + + yOffset(), 'f', 1) + UNIT_SPACE + yUnits()); +#endif // Q_OS_ANDROID } else clearInfo(); } diff --git a/src/GUI/colorbox.cpp b/src/GUI/colorbox.cpp index b7fe891e..8e5f01e3 100644 --- a/src/GUI/colorbox.cpp +++ b/src/GUI/colorbox.cpp @@ -52,17 +52,19 @@ void ColorBox::paintEvent(QPaintEvent *event) void ColorBox::mousePressEvent(QMouseEvent *event) { - if (event->button() != Qt::LeftButton) - return; - QColorDialog::ColorDialogOptions options = _alpha - ? QColorDialog::ColorDialogOptions(QColorDialog::ShowAlphaChannel) - : QColorDialog::ColorDialogOptions(); - QColor color = QColorDialog::getColor(_color, this, QString(), options); - if (color.isValid()) { - _color = color; - update(); - emit colorChanged(_color); + if (event->button() == Qt::LeftButton) { + QColorDialog::ColorDialogOptions options = _alpha + ? QColorDialog::ColorDialogOptions(QColorDialog::ShowAlphaChannel) + : QColorDialog::ColorDialogOptions(); + QColor color = QColorDialog::getColor(_color, this, QString(), options); + if (color.isValid()) { + _color = color; + update(); + emit colorChanged(_color); + } } + + QWidget::mousePressEvent(event); } void ColorBox::setColor(const QColor &color) diff --git a/src/GUI/dirselectwidget.cpp b/src/GUI/dirselectwidget.cpp index f082fd6c..dc85cc04 100644 --- a/src/GUI/dirselectwidget.cpp +++ b/src/GUI/dirselectwidget.cpp @@ -9,10 +9,12 @@ DirSelectWidget::DirSelectWidget(QWidget *parent) : QWidget(parent) { - QFontMetrics fm(QApplication::font()); _edit = new QLineEdit(); +#ifndef Q_OS_ANDROID + QFontMetrics fm(QApplication::font()); _edit->setMinimumWidth(fm.averageCharWidth() * (QDir::homePath().length() + 12)); +#endif // Q_OS_ANDROID _edit->setPlaceholderText(tr("System default")); #ifdef Q_OS_WIN32 _button = new QPushButton("..."); diff --git a/src/GUI/elevationgraph.cpp b/src/GUI/elevationgraph.cpp index 9f644dc5..616c66ed 100644 --- a/src/GUI/elevationgraph.cpp +++ b/src/GUI/elevationgraph.cpp @@ -54,6 +54,16 @@ void ElevationGraph::setInfo() else { QLocale l(QLocale::system()); +#ifdef Q_OS_ANDROID + GraphView::addInfo(tr("Up"), l.toString(ascent() * yScale(), 'f', 0) + + UNIT_SPACE + yUnits()); + GraphView::addInfo(tr("Down"), l.toString(descent() * yScale(), 'f', 0) + + UNIT_SPACE + yUnits()); + GraphView::addInfo(tr("Max"), l.toString(max() * yScale(), 'f', 0) + + UNIT_SPACE + yUnits()); + GraphView::addInfo(tr("Min"), l.toString(min() * yScale(), 'f', 0) + + UNIT_SPACE + yUnits()); +#else // Q_OS_ANDROID GraphView::addInfo(tr("Ascent"), l.toString(ascent() * yScale(), 'f', 0) + UNIT_SPACE + yUnits()); GraphView::addInfo(tr("Descent"), l.toString(descent() * yScale(), @@ -62,6 +72,7 @@ void ElevationGraph::setInfo() 0) + UNIT_SPACE + yUnits()); GraphView::addInfo(tr("Minimum"), l.toString(min() * yScale(), 'f', 0) + UNIT_SPACE + yUnits()); +#endif // Q_OS_ANDROID } } diff --git a/src/GUI/filebrowser.cpp b/src/GUI/filebrowser.cpp index a864bfbb..5946a9e5 100644 --- a/src/GUI/filebrowser.cpp +++ b/src/GUI/filebrowser.cpp @@ -5,14 +5,25 @@ FileBrowser::FileBrowser(QObject *parent) : QObject(parent) { +#ifndef Q_OS_ANDROID _watcher = new QFileSystemWatcher(this); - connect(_watcher, &QFileSystemWatcher::directoryChanged, this, &FileBrowser::reloadDirectory); +#endif // Q_OS_ANDROID _index = -1; } +#ifdef Q_OS_ANDROID +void FileBrowser::setCurrentDir(const QString &path) +{ + QDir dir(path); + _files = dir.entryInfoList(_filter, QDir::Files); + _index = _files.empty() ? -1 : 0; + + emit listChanged(); +} +#else // Q_OS_ANDROID void FileBrowser::setCurrent(const QString &path) { QFileInfo file(path); @@ -28,6 +39,7 @@ void FileBrowser::setCurrent(const QString &path) _index = _files.empty() ? -1 : _files.indexOf(file); } +#endif // Q_OS_ANDROID void FileBrowser::setFilter(const QStringList &filter) { @@ -46,6 +58,11 @@ bool FileBrowser::isFirst() const return (_files.size() > 0 && _index == 0); } +QString FileBrowser::current() +{ + return (_index >= 0) ? _files.at(_index).absoluteFilePath() : QString(); +} + QString FileBrowser::next() { if (_index < 0 || _index == _files.size() - 1) diff --git a/src/GUI/filebrowser.h b/src/GUI/filebrowser.h index 00a06ce7..ec447f1c 100644 --- a/src/GUI/filebrowser.h +++ b/src/GUI/filebrowser.h @@ -14,9 +14,14 @@ class FileBrowser : public QObject public: FileBrowser(QObject *parent = 0); - void setFilter(const QStringList &filter); +#ifdef Q_OS_ANDROID + void setCurrentDir(const QString &path); +#else // Q_OS_ANDROID void setCurrent(const QString &path); +#endif // Q_OS_ANDROID + void setFilter(const QStringList &filter); + QString current(); QString next(); QString prev(); QString last(); @@ -32,7 +37,9 @@ private slots: void reloadDirectory(const QString &path); private: +#ifndef Q_OS_ANDROID QFileSystemWatcher *_watcher; +#endif // Q_OS_ANDROID QStringList _filter; QFileInfoList _files; int _index; diff --git a/src/GUI/fileselectwidget.cpp b/src/GUI/fileselectwidget.cpp index 3a2458c6..6f2de6ab 100644 --- a/src/GUI/fileselectwidget.cpp +++ b/src/GUI/fileselectwidget.cpp @@ -10,10 +10,12 @@ FileSelectWidget::FileSelectWidget(QWidget *parent) : QWidget(parent) { - QFontMetrics fm(QApplication::font()); _edit = new QLineEdit(); +#ifndef Q_OS_ANDROID + QFontMetrics fm(QApplication::font()); _edit->setMinimumWidth(fm.averageCharWidth() * (QDir::homePath().length() + 12)); +#endif // Q_OS_ANDROID #ifdef Q_OS_WIN32 _button = new QPushButton("..."); _button->setMaximumWidth(_button->sizeHint().width() / 2); diff --git a/src/GUI/gearratiograph.cpp b/src/GUI/gearratiograph.cpp index 040c5788..1758cbcc 100644 --- a/src/GUI/gearratiograph.cpp +++ b/src/GUI/gearratiograph.cpp @@ -24,12 +24,21 @@ void GearRatioGraph::setInfo() if (_showTracks) { QLocale l(QLocale::system()); +#ifdef Q_OS_ANDROID + GraphView::addInfo(tr("Top"), l.toString(top() * yScale(), 'f', 2) + + UNIT_SPACE + yUnits()); + GraphView::addInfo(tr("Min"), l.toString(min() * yScale(), 'f', 2) + + UNIT_SPACE + yUnits()); + GraphView::addInfo(tr("Max"), l.toString(max() * yScale(), 'f', 2) + + UNIT_SPACE + yUnits()); +#else // Q_OS_ANDROID GraphView::addInfo(tr("Most used"), l.toString(top() * yScale(), 'f', 2) + UNIT_SPACE + yUnits()); GraphView::addInfo(tr("Minimum"), l.toString(min() * yScale(), 'f', 2) + UNIT_SPACE + yUnits()); GraphView::addInfo(tr("Maximum"), l.toString(max() * yScale(), 'f', 2) + UNIT_SPACE + yUnits()); +#endif // Q_OS_ANDROID } else clearInfo(); } diff --git a/src/GUI/gui.cpp b/src/GUI/gui.cpp index b15681bb..63d540b0 100644 --- a/src/GUI/gui.cpp +++ b/src/GUI/gui.cpp @@ -53,6 +53,7 @@ #include "mapitem.h" #include "mapaction.h" #include "poiaction.h" +#include "navigationwidget.h" #include "gui.h" @@ -72,7 +73,11 @@ GUI::GUI() createStatusBar(); createActions(); createMenus(); +#ifdef Q_OS_ANDROID + createNavigation(); +#else // Q_OS_ANDROID createToolBars(); +#endif // Q_OS_ANDROID createBrowser(); _splitter = new QSplitter(); @@ -83,6 +88,7 @@ GUI::GUI() _splitter->setContentsMargins(0, 0, 0, 0); _splitter->setStretchFactor(0, 255); _splitter->setStretchFactor(1, 1); + setCentralWidget(_splitter); setWindowIcon(QIcon(APP_ICON)); @@ -181,19 +187,23 @@ void GUI::createActions() _navigationActionGroup->setEnabled(false); // General actions +#if !defined(Q_OS_MAC) && !defined(Q_OS_ANDROID) _exitAction = new QAction(QIcon(QUIT_ICON), tr("Quit"), this); _exitAction->setShortcut(QUIT_SHORTCUT); _exitAction->setMenuRole(QAction::QuitRole); connect(_exitAction, &QAction::triggered, this, &GUI::close); addAction(_exitAction); +#endif // Q_OS_MAC + Q_OS_ANDROID // Help & About _pathsAction = new QAction(tr("Paths"), this); _pathsAction->setMenuRole(QAction::NoRole); connect(_pathsAction, &QAction::triggered, this, &GUI::paths); +#ifndef Q_OS_ANDROID _keysAction = new QAction(tr("Keyboard controls"), this); _keysAction->setMenuRole(QAction::NoRole); connect(_keysAction, &QAction::triggered, this, &GUI::keys); +#endif // Q_OS_ANDROID _aboutAction = new QAction(QIcon(APP_ICON), tr("About GPXSee"), this); _aboutAction->setMenuRole(QAction::AboutRole); connect(_aboutAction, &QAction::triggered, this, &GUI::about); @@ -205,6 +215,12 @@ void GUI::createActions() connect(_openFileAction, &QAction::triggered, this, QOverload<>::of(&GUI::openFile)); addAction(_openFileAction); +#ifdef Q_OS_ANDROID + _openDirAction = new QAction(QIcon(OPEN_FILE_ICON), tr("Open directory..."), + this); + _openDirAction->setMenuRole(QAction::NoRole); + connect(_openDirAction, &QAction::triggered, this, &GUI::openDir); +#endif // Q_OS_ANDROID _printFileAction = new QAction(QIcon(PRINT_FILE_ICON), tr("Print..."), this); _printFileAction->setMenuRole(QAction::NoRole); @@ -459,10 +475,12 @@ void GUI::createActions() &GUI::showGraphSliderInfo); // Settings actions +#ifndef Q_OS_ANDROID _showToolbarsAction = new QAction(tr("Show toolbars"), this); _showToolbarsAction->setMenuRole(QAction::NoRole); _showToolbarsAction->setCheckable(true); connect(_showToolbarsAction, &QAction::triggered, this, &GUI::showToolbars); +#endif // Q_OS_ANDROID ag = new QActionGroup(this); ag->setExclusive(true); _totalTimeAction = new QAction(tr("Total time"), this); @@ -514,6 +532,7 @@ void GUI::createActions() _dmsAction->setCheckable(true); _dmsAction->setActionGroup(ag); connect(_dmsAction, &QAction::triggered, this, &GUI::setDMS); +#ifndef Q_OS_ANDROID _fullscreenAction = new QAction(QIcon(FULLSCREEN_ICON), tr("Fullscreen mode"), this); _fullscreenAction->setMenuRole(QAction::NoRole); @@ -521,11 +540,13 @@ void GUI::createActions() _fullscreenAction->setShortcut(FULLSCREEN_SHORTCUT); connect(_fullscreenAction, &QAction::triggered, this, &GUI::showFullscreen); addAction(_fullscreenAction); +#endif // Q_OS_ANDROID _openOptionsAction = new QAction(tr("Options..."), this); _openOptionsAction->setMenuRole(QAction::PreferencesRole); connect(_openOptionsAction, &QAction::triggered, this, &GUI::openOptions); // Navigation actions +#ifndef Q_OS_ANDROID _nextAction = new QAction(QIcon(NEXT_FILE_ICON), tr("Next"), this); _nextAction->setActionGroup(_navigationActionGroup); _nextAction->setMenuRole(QAction::NoRole); @@ -542,6 +563,7 @@ void GUI::createActions() _firstAction->setMenuRole(QAction::NoRole); _firstAction->setActionGroup(_navigationActionGroup); connect(_firstAction, &QAction::triggered, this, &GUI::first); +#endif // Q_OS_ANDROID } void GUI::createMapNodeMenu(const TreeNode &node, QMenu *menu, @@ -574,8 +596,13 @@ void GUI::createMenus() { QMenu *fileMenu = menuBar()->addMenu(tr("&File")); fileMenu->addAction(_openFileAction); +#ifdef Q_OS_ANDROID + fileMenu->addAction(_openDirAction); +#endif // Q_OS_ANDROID fileMenu->addSeparator(); +#ifndef Q_OS_ANDROID fileMenu->addAction(_printFileAction); +#endif // Q_OS_ANDROID fileMenu->addAction(_exportPDFFileAction); fileMenu->addAction(_exportPNGFileAction); fileMenu->addSeparator(); @@ -583,10 +610,10 @@ void GUI::createMenus() fileMenu->addSeparator(); fileMenu->addAction(_reloadFileAction); fileMenu->addAction(_closeFileAction); -#ifndef Q_OS_MAC +#if !defined(Q_OS_MAC) && !defined(Q_OS_ANDROID) fileMenu->addSeparator(); fileMenu->addAction(_exitAction); -#endif // Q_OS_MAC +#endif // Q_OS_MAC + Q_OS_ANDROID _mapMenu = menuBar()->addMenu(tr("&Map")); _mapsEnd = _mapMenu->addSeparator(); @@ -659,18 +686,31 @@ void GUI::createMenus() coordinatesMenu->addAction(_degreesMinutesAction); coordinatesMenu->addAction(_dmsAction); settingsMenu->addSeparator(); +#ifndef Q_OS_ANDROID settingsMenu->addAction(_showToolbarsAction); settingsMenu->addAction(_fullscreenAction); settingsMenu->addSeparator(); +#endif // Q_OS_ANDROID settingsMenu->addAction(_openOptionsAction); QMenu *helpMenu = menuBar()->addMenu(tr("&Help")); helpMenu->addAction(_pathsAction); +#ifndef Q_OS_ANDROID helpMenu->addAction(_keysAction); +#endif // Q_OS_ANDROID helpMenu->addSeparator(); helpMenu->addAction(_aboutAction); } +#ifdef Q_OS_ANDROID +void GUI::createNavigation() +{ + _navigation = new NavigationWidget(_mapView); + + connect(_navigation, &NavigationWidget::next, this, &GUI::next); + connect(_navigation, &NavigationWidget::prev, this, &GUI::prev); +} +#else // Q_OS_ANDROID void GUI::createToolBars() { int is = style()->pixelMetric(QStyle::PM_ToolBarIconSize); @@ -706,6 +746,7 @@ void GUI::createToolBars() _navigationToolBar->addAction(_nextAction); _navigationToolBar->addAction(_lastAction); } +#endif // Q_OS_ANDROID void GUI::createMapView() { @@ -713,7 +754,11 @@ void GUI::createMapView() _mapView = new MapView(_map, _poi, 0, this); _mapView->setSizePolicy(QSizePolicy(QSizePolicy::Ignored, QSizePolicy::Expanding)); +#ifdef Q_OS_ANDROID + _mapView->setMinimumHeight(100); +#else // Q_OS_ANDROID _mapView->setMinimumHeight(200); +#endif // Q_OS_ANDROID #ifdef Q_OS_WIN32 _mapView->setFrameShape(QFrame::NoFrame); #endif // Q_OS_WIN32 @@ -765,7 +810,16 @@ void GUI::about() QUrl homepage(APP_HOMEPAGE); msgBox.setWindowTitle(tr("About GPXSee")); - msgBox.setText("

" + QString(APP_NAME) + "

" + tr("Version %1") +#ifdef Q_OS_ANDROID + msgBox.setText("

" + QString(APP_NAME) + "

" + tr("Version %1") + .arg(QString(APP_VERSION) + " (" + QSysInfo::buildCpuArchitecture() + + ", Qt " + QT_VERSION_STR + ")") + "

" + + tr("GPXSee is distributed under the terms of the GNU General Public " + "License version 3. For more info about GPXSee visit the project " + "homepage at %1.").arg("" + + homepage.toString(QUrl::RemoveScheme).mid(2) + "") + "

"); +#else // Q_OS_ANDROID + msgBox.setText("

" + QString(APP_NAME) + "

" + tr("Version %1") .arg(QString(APP_VERSION) + " (" + QSysInfo::buildCpuArchitecture() + ", Qt " + QT_VERSION_STR + ")") + "

"); msgBox.setInformativeText("
" @@ -778,10 +832,12 @@ void GUI::about() QIcon icon = msgBox.windowIcon(); QSize size = icon.actualSize(QSize(64, 64)); msgBox.setIconPixmap(icon.pixmap(size)); +#endif // Q_OS_ANDROID msgBox.exec(); } +#ifndef Q_OS_ANDROID void GUI::keys() { QMessageBox msgBox(this); @@ -820,12 +876,30 @@ void GUI::keys() msgBox.exec(); } +#endif // Q_OS_ANDROID void GUI::paths() { QMessageBox msgBox(this); msgBox.setWindowTitle(tr("Paths")); +#ifdef Q_OS_ANDROID + msgBox.setText( + + "" + tr("Map directory:") + "
" + + QDir::cleanPath(ProgramPaths::mapDir(true)) + "

" + + tr("POI directory:") + "
" + + QDir::cleanPath(ProgramPaths::poiDir(true)) + "

" + + tr("GCS/PCS directory:") + "
" + + QDir::cleanPath(ProgramPaths::csvDir(true)) + "

" + + tr("DEM directory:") + "
" + + QDir::cleanPath(ProgramPaths::demDir(true)) + "

" + + tr("Styles directory:") + "
" + + QDir::cleanPath(ProgramPaths::styleDir(true)) + "

" + + tr("Symbols directory:") + "
" + + QDir::cleanPath(ProgramPaths::symbolsDir(true)) + "

" + + tr("Tile cache directory:") + "
" + + QDir::cleanPath(ProgramPaths::tilesDir()) + "
"); +#else // Q_OS_ANDROID msgBox.setText("

" + tr("Paths") + "

"); msgBox.setInformativeText( "
" @@ -844,14 +918,20 @@ void GUI::paths() + tr("Tile cache directory:") + "" + QDir::cleanPath(ProgramPaths::tilesDir()) + "
" ); +#endif // Q_OS_ANDROID msgBox.exec(); } void GUI::openFile() { +#ifdef Q_OS_ANDROID + QStringList files(QFileDialog::getOpenFileNames(this, tr("Open file"), + _dataDir)); +#else // Q_OS_ANDROID QStringList files(QFileDialog::getOpenFileNames(this, tr("Open file"), _dataDir, Data::formats())); +#endif // Q_OS_ANDROID for (int i = 0; i < files.size(); i++) openFile(files.at(i)); @@ -859,6 +939,19 @@ void GUI::openFile() _dataDir = QFileInfo(files.last()).path(); } +#ifdef Q_OS_ANDROID +void GUI::openDir() +{ + QString dir(QFileDialog::getExistingDirectory(this, tr("Open directory"), + _dataDir)); + + if (!dir.isEmpty()) { + _browser->setCurrentDir(dir); + openFile(_browser->current()); + } +} +#endif // Q_OS_ANDROID + bool GUI::openFile(const QString &fileName, bool silent) { if (_files.contains(fileName)) @@ -868,7 +961,9 @@ bool GUI::openFile(const QString &fileName, bool silent) return false; _files.append(fileName); +#ifndef Q_OS_ANDROID _browser->setCurrent(fileName); +#endif // Q_OS_ANDROID _fileActionGroup->setEnabled(true); // Explicitly enable the reload action as it may be disabled by loadMapDir() _reloadFileAction->setEnabled(true); @@ -898,7 +993,7 @@ bool GUI::loadFile(const QString &fileName, bool silent) _fileActionGroup->setEnabled(false); QString error = tr("Error loading data file:") + "\n\n" - + fileName + "\n\n" + data.errorString(); + + Util::displayName(fileName) + "\n\n" + data.errorString(); if (data.errorLine()) error.append("\n" + tr("Line: %1").arg(data.errorLine())); QMessageBox::critical(this, APP_NAME, error); @@ -967,8 +1062,13 @@ void GUI::loadData(const Data &data) void GUI::openPOIFile() { +#ifdef Q_OS_ANDROID + QStringList files(QFileDialog::getOpenFileNames(this, tr("Open POI file"), + _poiDir)); +#else // Q_OS_ANDROID QStringList files(QFileDialog::getOpenFileNames(this, tr("Open POI file"), _poiDir, Data::formats())); +#endif // Q_OS_ANDROID for (int i = 0; i < files.size(); i++) openPOIFile(files.at(i)); @@ -994,7 +1094,7 @@ bool GUI::openPOIFile(const QString &fileName) return true; } else { QString error = tr("Error loading POI file:") + "\n\n" - + fileName + "\n\n" + _poi->errorString(); + + Util::displayName(fileName) + "\n\n" + _poi->errorString(); if (_poi->errorLine()) error.append("\n" + tr("Line: %1").arg(_poi->errorLine())); QMessageBox::critical(this, APP_NAME, error); @@ -1207,12 +1307,72 @@ void GUI::exportPNGFile() void GUI::statistics() { QLocale l(QLocale::system()); + QMessageBox msgBox(this); + QString text; +#ifdef Q_OS_ANDROID + if (_showTracksAction->isChecked() && _trackCount > 1) + text.append("" + tr("Tracks") + ": " + + l.toString(_trackCount) + "
"); + if (_showRoutesAction->isChecked() && _routeCount > 1) + text.append("" + tr("Routes") + ": " + + l.toString(_routeCount) + "
"); + if (_showWaypointsAction->isChecked() && _waypointCount > 1) + text.append("" + tr("Waypoints") + ": " + + l.toString(_waypointCount) + "
"); + if (_showAreasAction->isChecked() && _areaCount > 1) + text.append("" + tr("Areas") + ": " + + l.toString(_areaCount) + "
"); + + if (_dateRange.first.isValid()) { + if (_dateRange.first == _dateRange.second) { + QString format = l.dateFormat(QLocale::LongFormat); + text.append("" + tr("Date") + ": " + + _dateRange.first.toString(format) + "
"); + } else { + QString format = l.dateFormat(QLocale::ShortFormat); + text.append("" + tr("Date") + ": " + + QString("%1 - %2").arg(_dateRange.first.toString(format), + _dateRange.second.toString(format)) + "
"); + } + } + + if (distance() > 0) + text.append("" + tr("Distance") + ": " + + Format::distance(distance(), units()) + "
"); + if (time() > 0) { + text.append("" + tr("Time") + ": " + + Format::timeSpan(time()) + "
"); + text.append("" + tr("Moving time") + ": " + + Format::timeSpan(movingTime()) + "
"); + } + text.append("
"); + + for (int i = 0; i < _tabs.count(); i++) { + const GraphTab *tab = _tabs.at(i); + if (tab->isEmpty()) + continue; + + text.append("" + tab->label() + "
"); + for (int j = 0; j < tab->info().size(); j++) { + const KV &kv = tab->info().at(j); + text.append("" + kv.key() + ": " + kv.value()); + if (j != tab->info().size() - 1) + text.append(" | "); + } + if (i != _tabs.count() - 1) + text.append("

"); + } + + msgBox.setWindowTitle(tr("Statistics")); + msgBox.setText(text); + +#else // Q_OS_ANDROID #ifdef Q_OS_WIN32 - QString text = ""; #else // Q_OS_WIN32 - QString text = "
"; #endif // Q_OS_WIN32 @@ -1267,11 +1427,11 @@ void GUI::statistics() text.append("
"); - - QMessageBox msgBox(this); msgBox.setWindowTitle(tr("Statistics")); msgBox.setText("

" + tr("Statistics") + "

"); msgBox.setInformativeText(text); +#endif // Q_OS_ANDROID + msgBox.exec(); } @@ -1430,8 +1590,10 @@ void GUI::reloadFiles() updateWindowTitle(); if (_files.isEmpty()) _fileActionGroup->setEnabled(false); +#ifndef Q_OS_ANDROID else _browser->setCurrent(_files.last()); +#endif // Q_OS_ANDROID updateDEMDownloadAction(); } @@ -1466,6 +1628,10 @@ void GUI::closeAll() updateWindowTitle(); updateGraphTabs(); updateDEMDownloadAction(); + +#ifdef Q_OS_ANDROID + _browser->setCurrentDir(QString()); +#endif // Q_OS_ANDROID } void GUI::showGraphs(bool show) @@ -1473,6 +1639,7 @@ void GUI::showGraphs(bool show) _graphTabWidget->setHidden(!show); } +#ifndef Q_OS_ANDROID void GUI::showToolbars(bool show) { if (show) { @@ -1515,6 +1682,7 @@ void GUI::showFullscreen(bool show) showNormal(); } } +#endif // Q_OS_ANDROID void GUI::showTracks(bool show) { @@ -1583,8 +1751,13 @@ void GUI::showPathMarkerInfo(QAction *action) void GUI::loadMap() { +#ifdef Q_OS_ANDROID + QStringList files(QFileDialog::getOpenFileNames(this, tr("Open map file"), + _mapDir)); +#else // Q_OS_ANDROID QStringList files(QFileDialog::getOpenFileNames(this, tr("Open map file"), _mapDir, MapList::formats())); +#endif // Q_OS_ANDROID MapAction *a, *lastReady = 0; for (int i = 0; i < files.size(); i++) { @@ -1627,7 +1800,8 @@ bool GUI::loadMapNode(const TreeNode &node, MapAction *&action, if (!map->isValid()) { if (!silent) QMessageBox::critical(this, APP_NAME, - tr("Error loading map:") + "\n\n" + map->path() + "\n\n" + tr("Error loading map:") + "\n\n" + + Util::displayName(map->path()) + "\n\n" + map->errorString()); delete map; } else { @@ -1672,8 +1846,8 @@ void GUI::mapLoaded() _showMapAction->setEnabled(true); _clearMapCacheAction->setEnabled(true); } else { - QString error = tr("Error loading map:") + "\n\n" + map->path() + "\n\n" - + map->errorString(); + QString error = tr("Error loading map:") + "\n\n" + + Util::displayName(map->path()) + "\n\n" + map->errorString(); QMessageBox::critical(this, APP_NAME, error); action->deleteLater(); } @@ -1691,8 +1865,8 @@ void GUI::mapLoadedDir() actions.append(action); _mapView->loadMaps(actions); } else { - QString error = tr("Error loading map:") + "\n\n" + map->path() + "\n\n" - + map->errorString(); + QString error = tr("Error loading map:") + "\n\n" + + Util::displayName(map->path()) + "\n\n" + map->errorString(); QMessageBox::critical(this, APP_NAME, error); action->deleteLater(); } @@ -1714,7 +1888,8 @@ void GUI::loadMapDirNode(const TreeNode &node, QList &actions if (!(a = findMapAction(existingActions, map))) { if (!map->isValid()) { QMessageBox::critical(this, APP_NAME, tr("Error loading map:") - + "\n\n" + map->path() + "\n\n" + map->errorString()); + + "\n\n" + Util::displayName(map->path()) + "\n\n" + + map->errorString()); delete map; } else { a = new MapAction(map, _mapsActionGroup); @@ -1817,7 +1992,7 @@ void GUI::updateStatusBarInfo() if (_files.count() == 0) _fileNameLabel->setText(tr("No files loaded")); else if (_files.count() == 1) - _fileNameLabel->setText(_files.at(0)); + _fileNameLabel->setText(Util::displayName(_files.at(0))); else _fileNameLabel->setText(tr("%n files", "", _files.count())); @@ -1840,6 +2015,10 @@ void GUI::updateStatusBarInfo() _timeLabel->clear(); _timeLabel->setToolTip(QString()); } + +#ifdef Q_OS_ANDROID + statusBar()->setVisible(!_files.isEmpty()); +#endif // Q_OS_ANDROID } void GUI::updateWindowTitle() @@ -1929,10 +2108,17 @@ void GUI::graphChanged(int index) void GUI::updateNavigationActions() { +#ifdef Q_OS_ANDROID + _navigation->enableNext(!_browser->isLast() + && !_browser->current().isNull()); + _navigation->enablePrev(!_browser->isFirst() + && !_browser->current().isNull()); +#else // Q_OS_ANDROID _lastAction->setEnabled(!_browser->isLast()); _nextAction->setEnabled(!_browser->isLast()); _firstAction->setEnabled(!_browser->isFirst()); _prevAction->setEnabled(!_browser->isFirst()); +#endif // Q_OS_ANDROID } bool GUI::updateGraphTabs() @@ -2042,6 +2228,7 @@ void GUI::first() openFile(file); } +#ifndef Q_OS_ANDROID void GUI::keyPressEvent(QKeyEvent *event) { QString file; @@ -2096,11 +2283,12 @@ void GUI::keyPressEvent(QKeyEvent *event) QMainWindow::keyPressEvent(event); } +#endif // Q_OS_ANDROID void GUI::closeEvent(QCloseEvent *event) { writeSettings(); - event->accept(); + QMainWindow::closeEvent(event); } void GUI::dragEnterEvent(QDragEnterEvent *event) @@ -2167,6 +2355,7 @@ void GUI::writeSettings() QSettings settings(qApp->applicationName(), qApp->applicationName()); settings.clear(); +#ifndef Q_OS_ANDROID settings.beginGroup(WINDOW_SETTINGS_GROUP); if (!_windowStates.isEmpty() && !_windowGeometries.isEmpty()) { settings.setValue(WINDOW_STATE_SETTING, _windowStates.first()); @@ -2176,6 +2365,7 @@ void GUI::writeSettings() settings.setValue(WINDOW_GEOMETRY_SETTING, saveGeometry()); } settings.endGroup(); +#endif // Q_OS_ANDROID settings.beginGroup(SETTINGS_SETTINGS_GROUP); if ((_movingTimeAction->isChecked() ? Moving : Total) != @@ -2190,9 +2380,11 @@ void GUI::writeSettings() : _degreesMinutesAction->isChecked() ? DegreesMinutes : DecimalDegrees; if (format != COORDINATES_DEFAULT) settings.setValue(COORDINATES_SETTING, format); +#ifndef Q_OS_ANDROID if (_showToolbarsAction->isChecked() != SHOW_TOOLBARS_DEFAULT) settings.setValue(SHOW_TOOLBARS_SETTING, _showToolbarsAction->isChecked()); +#endif // Q_OS_ANDROID settings.endGroup(); settings.beginGroup(MAP_SETTINGS_GROUP); @@ -2485,10 +2677,12 @@ void GUI::readSettings(QString &activeMap, QStringList &disabledPOIs) int value; QSettings settings(qApp->applicationName(), qApp->applicationName()); +#ifndef Q_OS_ANDROID settings.beginGroup(WINDOW_SETTINGS_GROUP); restoreGeometry(settings.value(WINDOW_GEOMETRY_SETTING).toByteArray()); restoreState(settings.value(WINDOW_STATE_SETTING).toByteArray()); settings.endGroup(); +#endif // Q_OS_ANDROID settings.beginGroup(SETTINGS_SETTINGS_GROUP); if (settings.value(TIME_TYPE_SETTING, TIME_TYPE_DEFAULT).toInt() == Moving) @@ -2512,10 +2706,12 @@ void GUI::readSettings(QString &activeMap, QStringList &disabledPOIs) else _decimalDegreesAction->trigger(); +#ifndef Q_OS_ANDROID if (!settings.value(SHOW_TOOLBARS_SETTING, SHOW_TOOLBARS_DEFAULT).toBool()) showToolbars(false); else _showToolbarsAction->setChecked(true); +#endif // Q_OS_ANDROID settings.endGroup(); settings.beginGroup(MAP_SETTINGS_GROUP); @@ -2523,8 +2719,8 @@ void GUI::readSettings(QString &activeMap, QStringList &disabledPOIs) _showMapAction->setChecked(true); else _mapView->showMap(false); - if (settings.value(SHOW_CURSOR_COORDINATES_SETTING, SHOW_CURSOR_COORDINATES_DEFAULT) - .toBool()) { + if (settings.value(SHOW_CURSOR_COORDINATES_SETTING, + SHOW_CURSOR_COORDINATES_DEFAULT).toBool()) { _showCoordinatesAction->setChecked(true); _mapView->showCursorCoordinates(true); } diff --git a/src/GUI/gui.h b/src/GUI/gui.h index a4419844..26fc01fa 100644 --- a/src/GUI/gui.h +++ b/src/GUI/gui.h @@ -35,6 +35,7 @@ class MapAction; class POIAction; class Data; class DEMLoader; +class NavigationWidget; class GUI : public QMainWindow { @@ -47,15 +48,21 @@ public: bool loadMap(const QString &fileName, MapAction *&action, bool silent = false); void show(); + void writeSettings(); private slots: void about(); +#ifndef Q_OS_ANDROID void keys(); +#endif // Q_OS_ANDROID void paths(); void printFile(); void exportPDFFile(); void exportPNGFile(); void openFile(); +#ifdef Q_OS_ANDROID + void openDir(); +#endif // Q_OS_ANDROID void closeAll(); void reloadFiles(); void statistics(); @@ -64,8 +71,10 @@ private slots: void showGraphGrids(bool show); void showGraphSliderInfo(bool show); void showPathMarkerInfo(QAction *action); +#ifndef Q_OS_ANDROID void showToolbars(bool show); void showFullscreen(bool show); +#endif // Q_OS_ANDROID void showTracks(bool show); void showRoutes(bool show); void showAreas(bool show); @@ -129,7 +138,11 @@ private: QAction *action = 0); void createActions(); void createMenus(); +#ifdef Q_OS_ANDROID + void createNavigation(); +#else // Q_OS_ANDROID void createToolBars(); +#endif // Q_OS_ANDROID void createStatusBar(); void createMapView(); void createGraphTabs(); @@ -160,19 +173,24 @@ private: QAction *mapAction(const QString &name); QGeoPositionInfoSource *positionSource(const Options &options); void readSettings(QString &activeMap, QStringList &disabledPOIs); - void writeSettings(); void loadInitialMaps(const QString &selected); void loadInitialPOIs(const QStringList &disabled); +#ifndef Q_OS_ANDROID void keyPressEvent(QKeyEvent *event); +#endif // Q_OS_ANDROID void closeEvent(QCloseEvent *event); void dragEnterEvent(QDragEnterEvent *event); void dropEvent(QDropEvent *event); +#ifdef Q_OS_ANDROID + NavigationWidget *_navigation; +#else // Q_OS_ANDROID QToolBar *_fileToolBar; QToolBar *_showToolBar; QToolBar *_navigationToolBar; +#endif // Q_OS_ANDROID QMenu *_poiMenu; QMenu *_mapMenu; @@ -180,15 +198,16 @@ private: QActionGroup *_navigationActionGroup; QActionGroup *_mapsActionGroup; QActionGroup *_poisActionGroup; +#if !defined(Q_OS_MAC) && !defined(Q_OS_ANDROID) QAction *_exitAction; - QAction *_keysAction; +#endif // Q_OS_MAC + Q_OS_ANDROID QAction *_pathsAction; QAction *_aboutAction; - QAction *_aboutQtAction; QAction *_printFileAction; QAction *_exportPDFFileAction; QAction *_exportPNGFileAction; QAction *_openFileAction; + QAction *_openDirAction; QAction *_closeFileAction; QAction *_reloadFileAction; QAction *_statisticsAction; @@ -204,7 +223,6 @@ private: QAction *_followPositionAction; QAction *_showPositionCoordinatesAction; QAction *_showMotionInfo; - QAction *_fullscreenAction; QAction *_loadMapAction; QAction *_loadMapDirAction; QAction *_clearMapCacheAction; @@ -213,11 +231,15 @@ private: QAction *_showGraphSliderInfoAction; QAction *_distanceGraphAction; QAction *_timeGraphAction; +#ifndef Q_OS_ANDROID + QAction *_keysAction; + QAction *_fullscreenAction; QAction *_showToolbarsAction; QAction *_nextAction; QAction *_prevAction; QAction *_lastAction; QAction *_firstAction; +#endif // Q_OS_ANDROID QAction *_metricUnitsAction; QAction *_imperialUnitsAction; QAction *_nauticalUnitsAction; @@ -271,9 +293,11 @@ private: DateTimeRange _dateRange; QString _pathName; +#ifndef Q_OS_ANDROID QList _windowStates; QList _windowGeometries; int _frameStyle; +#endif // Q_OS_ANDROID PDFExport _pdfExport; PNGExport _pngExport; diff --git a/src/GUI/heartrategraph.cpp b/src/GUI/heartrategraph.cpp index b9983c2d..b5282e9c 100644 --- a/src/GUI/heartrategraph.cpp +++ b/src/GUI/heartrategraph.cpp @@ -24,10 +24,17 @@ void HeartRateGraph::setInfo() if (_showTracks) { QLocale l(QLocale::system()); +#ifdef Q_OS_ANDROID + GraphView::addInfo(tr("Avg"), l.toString(avg() * yScale(), 'f', 0) + + UNIT_SPACE + yUnits()); + GraphView::addInfo(tr("Max"), l.toString(max() * yScale(), 'f', 0) + + UNIT_SPACE + yUnits()); +#else // Q_OS_ANDROID GraphView::addInfo(tr("Average"), l.toString(avg() * yScale(), 'f', 0) + UNIT_SPACE + yUnits()); GraphView::addInfo(tr("Maximum"), l.toString(max() * yScale(), 'f', 0) + UNIT_SPACE + yUnits()); +#endif // Q_OS_ANDROID } else clearInfo(); } diff --git a/src/GUI/mapview.cpp b/src/GUI/mapview.cpp index 152c5f1c..313c2624 100644 --- a/src/GUI/mapview.cpp +++ b/src/GUI/mapview.cpp @@ -662,20 +662,8 @@ void MapView::wheelEvent(QWheelEvent *event) #else // QT 5.15 zoom((delta > 0) ? 1 : -1, event->position().toPoint(), shift); #endif // QT 5.15 -} -void MapView::mouseDoubleClickEvent(QMouseEvent *event) -{ - bool shift = (event->modifiers() & MODIFIER) ? true : false; - - QGraphicsView::mouseDoubleClickEvent(event); - if (event->isAccepted()) - return; - - if (event->button() != Qt::LeftButton && event->button() != Qt::RightButton) - return; - - zoom((event->button() == Qt::LeftButton) ? 1 : -1, event->pos(), shift); + QGraphicsView::wheelEvent(event); } void MapView::keyPressEvent(QKeyEvent *event) @@ -713,15 +701,6 @@ void MapView::keyReleaseEvent(QKeyEvent *event) QGraphicsView::keyReleaseEvent(event); } -void MapView::mousePressEvent(QMouseEvent *event) -{ - if (event->button() == Qt::LeftButton && event->modifiers() & MODIFIER) - QApplication::clipboard()->setText(Format::coordinates( - _map->xy2ll(mapToScene(event->pos())), _cursorCoordinates->format())); - else - QGraphicsView::mousePressEvent(event); -} - void MapView::plot(QPainter *painter, const QRectF &target, qreal scale, PlotFlags flags) { @@ -1200,6 +1179,12 @@ void MapView::scrollContentsBy(int dx, int dy) } } +void MapView::leaveEvent(QEvent *event) +{ + _cursorCoordinates->setCoordinates(Coordinates()); + QGraphicsView::leaveEvent(event); +} + void MapView::mouseMoveEvent(QMouseEvent *event) { if (_cursorCoordinates->isVisible()) { @@ -1210,10 +1195,30 @@ void MapView::mouseMoveEvent(QMouseEvent *event) QGraphicsView::mouseMoveEvent(event); } -void MapView::leaveEvent(QEvent *event) + +void MapView::mousePressEvent(QMouseEvent *event) { - _cursorCoordinates->setCoordinates(Coordinates()); - QGraphicsView::leaveEvent(event); + if (event->button() == Qt::LeftButton) { + if (event->modifiers() & MODIFIER) + QApplication::clipboard()->setText(Format::coordinates(_map->xy2ll( + mapToScene(event->pos())), _cursorCoordinates->format())); +#ifdef Q_OS_ANDROID + else + emit clicked(event->pos()); +#endif // Q_OS_ANDROID + } + + QGraphicsView::mousePressEvent(event); +} + +void MapView::mouseDoubleClickEvent(QMouseEvent *event) +{ + bool shift = (event->modifiers() & MODIFIER) ? true : false; + + if (event->button() == Qt::LeftButton || event->button() == Qt::RightButton) + zoom((event->button() == Qt::LeftButton) ? 1 : -1, event->pos(), shift); + + QGraphicsView::mouseDoubleClickEvent(event); } bool MapView::event(QEvent *event) diff --git a/src/GUI/mapview.h b/src/GUI/mapview.h index e1e1ec92..a051e896 100644 --- a/src/GUI/mapview.h +++ b/src/GUI/mapview.h @@ -102,6 +102,11 @@ public: RectC boundingRect() const; +#ifdef Q_OS_ANDROID +signals: + void clicked(const QPoint &pos); +#endif // Q_OS_ANDROID + public slots: void showMap(bool show); void showPOI(bool show); @@ -162,6 +167,7 @@ private: void drawBackground(QPainter *painter, const QRectF &rect); void paintEvent(QPaintEvent *event); void leaveEvent(QEvent *event); + bool event(QEvent *event); void scrollContentsBy(int dx, int dy); diff --git a/src/GUI/navigationwidget.cpp b/src/GUI/navigationwidget.cpp new file mode 100644 index 00000000..00b8aefb --- /dev/null +++ b/src/GUI/navigationwidget.cpp @@ -0,0 +1,101 @@ +#include "navigationwidget.h" +#include +#include +#include + +#define MARGIN 5 +#define SIZE 40 + +#ifdef Q_OS_ANDROID + +NavigationWidget::NavigationWidget(MapView *view) + : QWidget(view), _showPrev(false), _showNext(false) +{ + setAttribute(Qt::WA_NoSystemBackground); + setAttribute(Qt::WA_TranslucentBackground); + setAttribute(Qt::WA_TransparentForMouseEvents); + + newParent(); + + connect(view, &MapView::clicked, this, &NavigationWidget::viewClicked); +} + +bool NavigationWidget::eventFilter(QObject *obj, QEvent *ev) +{ + if (obj == parent()) { + if (ev->type() == QEvent::Resize) + resize(static_cast(ev)->size()); + else if (ev->type() == QEvent::ChildAdded) + raise(); + } + + return QWidget::eventFilter(obj, ev); +} + +bool NavigationWidget::event(QEvent* ev) +{ + if (ev->type() == QEvent::ParentAboutToChange) { + if (parent()) + parent()->removeEventFilter(this); + } else if (ev->type() == QEvent::ParentChange) + newParent(); + + return QWidget::event(ev); +} + +void NavigationWidget::paintEvent(QPaintEvent *ev) +{ + Q_UNUSED(ev); + QPainter p(this); + + QColor c(Qt::black); + c.setAlpha(64); + p.setBrush(c); + p.setPen(Qt::NoPen); + + if (_showPrev) { + QPainterPath path; + path.addEllipse(QRect(MARGIN, rect().center().y() - SIZE/2, SIZE, SIZE)); + path.moveTo(QPointF(MARGIN + 0.66*SIZE, rect().center().y() - SIZE/4)); + path.lineTo(QPointF(MARGIN + SIZE/4, rect().center().y())); + path.lineTo(QPointF(MARGIN + 0.66*SIZE, rect().center().y() + SIZE/4)); + path.closeSubpath(); + p.drawPath(path); + } + if (_showNext) { + QPainterPath path; + path.addEllipse(QRect(rect().right() - (MARGIN + SIZE), + rect().center().y() - SIZE/2, SIZE, SIZE)); + path.moveTo(QPointF(rect().right() - (MARGIN + 0.66*SIZE), + rect().center().y() - SIZE/4)); + path.lineTo(QPointF(rect().right() - (MARGIN + SIZE/4), + rect().center().y())); + path.lineTo(QPointF(rect().right() - (MARGIN + 0.66*SIZE), + rect().center().y() + SIZE/4)); + path.closeSubpath(); + p.drawPath(path); + } +} + +void NavigationWidget::newParent() +{ + if (!parent()) + return; + + parent()->installEventFilter(this); + raise(); +} + +void NavigationWidget::viewClicked(const QPoint &pos) +{ + QRect prevRect(MARGIN, rect().center().y() - SIZE/2, SIZE, SIZE); + QRect nextRect(rect().right() - (MARGIN + SIZE), rect().center().y() + - SIZE/2, SIZE, SIZE); + + if (prevRect.contains(pos)) + emit prev(); + else if (nextRect.contains(pos)) + emit next(); +} + +#endif // Q_OS_ANDROID diff --git a/src/GUI/navigationwidget.h b/src/GUI/navigationwidget.h new file mode 100644 index 00000000..3f679946 --- /dev/null +++ b/src/GUI/navigationwidget.h @@ -0,0 +1,35 @@ +#ifndef NAVIGATIONWIDGET_H +#define NAVIGATIONWIDGET_H + +#include +#include "mapview.h" + +#ifdef Q_OS_ANDROID +class NavigationWidget : public QWidget +{ + Q_OBJECT + +public: + NavigationWidget(MapView *view); + + void enableNext(bool enable) {_showNext = enable; update();} + void enablePrev(bool enable) {_showPrev = enable; update();} + +signals: + void next(); + void prev(); + +private slots: + void viewClicked(const QPoint &pos); + +private: + bool eventFilter(QObject *obj, QEvent *ev); + bool event(QEvent *ev); + void paintEvent(QPaintEvent *ev); + void newParent(); + + bool _showPrev, _showNext; +}; +#endif // Q_OS_ANDROID + +#endif // NAVIGATIONWIDGET_H diff --git a/src/GUI/optionsdialog.cpp b/src/GUI/optionsdialog.cpp index 09e9394c..923f0027 100644 --- a/src/GUI/optionsdialog.cpp +++ b/src/GUI/optionsdialog.cpp @@ -26,8 +26,11 @@ #include "pluginparameters.h" #include "optionsdialog.h" - +#ifdef Q_OS_ANDROID +#define MENU_MARGIN 0 +#else // Q_OS_ANDROID #define MENU_MARGIN 20 +#endif // Q_OS_ANDROID #define MENU_ICON_SIZE 32 #ifdef Q_OS_MAC @@ -779,6 +782,11 @@ QWidget *OptionsDialog::createSystemPage() OptionsDialog::OptionsDialog(Options &options, Units units, QWidget *parent) : QDialog(parent), _options(options), _units(units) { +#ifdef Q_OS_ANDROID + setWindowFlags(Qt::Window); + setWindowState(Qt::WindowFullScreen); +#endif /* Q_OS_ANDROID */ + QStackedWidget *pages = new QStackedWidget(); pages->addWidget(createAppearancePage()); pages->addWidget(createMapPage()); @@ -791,16 +799,25 @@ OptionsDialog::OptionsDialog(Options &options, Units units, QWidget *parent) QListWidget *menu = new QListWidget(); menu->setIconSize(QSize(MENU_ICON_SIZE, MENU_ICON_SIZE)); - new QListWidgetItem(QIcon(APPEARANCE_ICON), tr("Appearance"), - menu); +#ifdef Q_OS_ANDROID + new QListWidgetItem(QIcon(APPEARANCE_ICON), QString(), menu); + new QListWidgetItem(QIcon(MAPS_ICON), QString(), menu); + new QListWidgetItem(QIcon(DATA_ICON), QString(), menu); + new QListWidgetItem(QIcon(POI_ICON), QString(), menu); + new QListWidgetItem(QIcon(DEM_ICON), QString(), menu); + new QListWidgetItem(QIcon(POSITION_ICON), QString(), menu); + new QListWidgetItem(QIcon(PRINT_EXPORT_ICON), QString(), menu); + new QListWidgetItem(QIcon(SYSTEM_ICON), QString(), menu); +#else // Q_OS_ANDROID + new QListWidgetItem(QIcon(APPEARANCE_ICON), tr("Appearance"), menu); new QListWidgetItem(QIcon(MAPS_ICON), tr("Maps"), menu); new QListWidgetItem(QIcon(DATA_ICON), tr("Data"), menu); new QListWidgetItem(QIcon(POI_ICON), tr("POI"), menu); new QListWidgetItem(QIcon(DEM_ICON), tr("DEM"), menu); new QListWidgetItem(QIcon(POSITION_ICON), tr("Position"), menu); - new QListWidgetItem(QIcon(PRINT_EXPORT_ICON), tr("Print & Export"), - menu); + new QListWidgetItem(QIcon(PRINT_EXPORT_ICON), tr("Print & Export"), menu); new QListWidgetItem(QIcon(SYSTEM_ICON), tr("System"), menu); +#endif // Q_OS_ANDROID QHBoxLayout *contentLayout = new QHBoxLayout(); contentLayout->addWidget(menu); diff --git a/src/GUI/pdfexportdialog.cpp b/src/GUI/pdfexportdialog.cpp index 788fd1e9..b4f5a55e 100644 --- a/src/GUI/pdfexportdialog.cpp +++ b/src/GUI/pdfexportdialog.cpp @@ -18,9 +18,16 @@ PDFExportDialog::PDFExportDialog(PDFExport &exp, Units units, QWidget *parent) { int index; +#ifdef Q_OS_ANDROID + setWindowFlags(Qt::Window); + setWindowState(Qt::WindowFullScreen); +#endif /* Q_OS_ANDROID */ + _fileSelect = new FileSelectWidget(); +#ifndef Q_OS_ANDROID _fileSelect->setFilter(tr("PDF files") + " (*.pdf);;" + tr("All files") + " (*)"); +#endif // Q_OS_ANDROID _fileSelect->setFile(_export.fileName); _paperSize = new QComboBox(); @@ -102,6 +109,9 @@ PDFExportDialog::PDFExportDialog(PDFExport &exp, Units units, QWidget *parent) #else // Q_OS_MAC layout->addWidget(pageSetupBox); layout->addWidget(outputFileBox); +#ifdef Q_OS_ANDROID + layout->addStretch(); +#endif // Q_OS_ANDROID #endif // Q_OS_MAC layout->addWidget(buttonBox); setLayout(layout); diff --git a/src/GUI/pngexportdialog.cpp b/src/GUI/pngexportdialog.cpp index 5a4ebd5b..4aeff14c 100644 --- a/src/GUI/pngexportdialog.cpp +++ b/src/GUI/pngexportdialog.cpp @@ -15,9 +15,16 @@ PNGExportDialog::PNGExportDialog(PNGExport &exp, QWidget *parent) : QDialog(parent), _export(exp) { +#ifdef Q_OS_ANDROID + setWindowFlags(Qt::Window); + setWindowState(Qt::WindowFullScreen); +#endif /* Q_OS_ANDROID */ + _fileSelect = new FileSelectWidget(); +#ifndef Q_OS_ANDROID _fileSelect->setFilter(tr("PNG files") + " (*.png);;" + tr("All files") + " (*)"); +#endif // Q_OS_ANDROID _fileSelect->setFile(_export.fileName); _width = new QSpinBox(); @@ -78,6 +85,9 @@ PNGExportDialog::PNGExportDialog(PNGExport &exp, QWidget *parent) #else // Q_OS_MAC layout->addWidget(pageSetupBox); layout->addWidget(outputFileBox); +#ifdef Q_OS_ANDROID + layout->addStretch(); +#endif // Q_OS_ANDROID #endif // Q_OS_MAC layout->addWidget(buttonBox); setLayout(layout); diff --git a/src/GUI/powergraph.cpp b/src/GUI/powergraph.cpp index 21456eb2..b1576992 100644 --- a/src/GUI/powergraph.cpp +++ b/src/GUI/powergraph.cpp @@ -24,10 +24,17 @@ void PowerGraph::setInfo() if (_showTracks) { QLocale l(QLocale::system()); +#ifdef Q_OS_ANDROID + GraphView::addInfo(tr("Avg"), l.toString(avg() * yScale() + yOffset(), + 'f', 1) + UNIT_SPACE + yUnits()); + GraphView::addInfo(tr("Max"), l.toString(max() * yScale() + yOffset(), + 'f', 1) + UNIT_SPACE + yUnits()); +#else // Q_OS_ANDROID GraphView::addInfo(tr("Average"), l.toString(avg() * yScale() + yOffset(), 'f', 1) + UNIT_SPACE + yUnits()); GraphView::addInfo(tr("Maximum"), l.toString(max() * yScale() - + yOffset(), 'f', 1) + UNIT_SPACE + yUnits()); + + yOffset(), 'f', 1) + UNIT_SPACE + yUnits()); +#endif // Q_OS_ANDROID } else clearInfo(); } diff --git a/src/GUI/settings.h b/src/GUI/settings.h index fe8251fe..c595b376 100644 --- a/src/GUI/settings.h +++ b/src/GUI/settings.h @@ -1,6 +1,8 @@ #ifndef SETTINGS_H #define SETTINGS_H +#include + #define IMPERIAL_UNITS() \ (QLocale::system().measurementSystem() == QLocale::ImperialSystem) #define POSITION_PLUGIN() \ diff --git a/src/GUI/speedgraph.cpp b/src/GUI/speedgraph.cpp index 305981bd..a1049a3e 100644 --- a/src/GUI/speedgraph.cpp +++ b/src/GUI/speedgraph.cpp @@ -31,10 +31,17 @@ void SpeedGraph::setInfo() QString pu = (_units == Metric) ? tr("min/km") : (_units == Imperial) ? tr("min/mi") : tr("min/nmi"); +#ifdef Q_OS_ANDROID + GraphView::addInfo(tr("Avg"), l.toString(avg() * yScale(), 'f', 1) + + UNIT_SPACE + yUnits()); + GraphView::addInfo(tr("Max"), l.toString(max() * yScale(), 'f', 1) + + UNIT_SPACE + yUnits()); +#else // Q_OS_ANDROID GraphView::addInfo(tr("Average"), l.toString(avg() * yScale(), 'f', 1) + UNIT_SPACE + yUnits()); GraphView::addInfo(tr("Maximum"), l.toString(max() * yScale(), 'f', 1) + UNIT_SPACE + yUnits()); +#endif // Q_OS_ANDROID GraphView::addInfo(tr("Pace"), pace + UNIT_SPACE + pu); } else clearInfo(); diff --git a/src/GUI/temperaturegraph.cpp b/src/GUI/temperaturegraph.cpp index e6b2e076..12ec98e9 100644 --- a/src/GUI/temperaturegraph.cpp +++ b/src/GUI/temperaturegraph.cpp @@ -24,12 +24,21 @@ void TemperatureGraph::setInfo() if (_showTracks) { QLocale l(QLocale::system()); +#ifdef Q_OS_ANDROID + GraphView::addInfo(tr("Avg"), l.toString(avg() * yScale() + yOffset(), + 'f', 1) + UNIT_SPACE + yUnits()); + GraphView::addInfo(tr("Min"), l.toString(min() * yScale() + yOffset(), + 'f', 1) + UNIT_SPACE + yUnits()); + GraphView::addInfo(tr("Max"), l.toString(max() * yScale() + yOffset(), + 'f', 1) + UNIT_SPACE + yUnits()); +#else // Q_OS_ANDROID GraphView::addInfo(tr("Average"), l.toString(avg() * yScale() + yOffset(), 'f', 1) + UNIT_SPACE + yUnits()); GraphView::addInfo(tr("Minimum"), l.toString(min() * yScale() + yOffset(), 'f', 1) + UNIT_SPACE + yUnits()); GraphView::addInfo(tr("Maximum"), l.toString(max() * yScale() + yOffset(), 'f', 1) + UNIT_SPACE + yUnits()); +#endif // Q_OS_ANDROID } else clearInfo(); } diff --git a/src/GUI/thumbnail.cpp b/src/GUI/thumbnail.cpp index 6201c8a8..98b7da48 100644 --- a/src/GUI/thumbnail.cpp +++ b/src/GUI/thumbnail.cpp @@ -32,11 +32,21 @@ Thumbnail::Thumbnail(const QString &path, int limit, QWidget *parent) setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); +#ifdef Q_OS_ANDROID + _path = path; +#else //Q_OS_ANDROID _path = QFileInfo(path).absoluteFilePath(); +#endif // Q_OS_ANDROID } void Thumbnail::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) +#ifdef Q_OS_ANDROID + QDesktopServices::openUrl(_path); +#else // Q_OS_ANDROID QDesktopServices::openUrl(QUrl::fromLocalFile(_path)); +#endif // Q_OS_ANDROID + + QLabel::mousePressEvent(event); } diff --git a/src/common/programpaths.cpp b/src/common/programpaths.cpp index 7c03595c..b437e261 100644 --- a/src/common/programpaths.cpp +++ b/src/common/programpaths.cpp @@ -19,65 +19,97 @@ #define TYP_FILE "style.typ" #define RENDERTHEME_FILE "style.xml" +#ifdef Q_OS_ANDROID +#define DATA_LOCATION QStandardPaths::GenericDataLocation +#else // Q_OS_ANDROID +#define DATA_LOCATION QStandardPaths::AppDataLocation +#endif // Q_OS_ANDROID + +#ifdef Q_OS_ANDROID +static QString assetsPath(const QString &path, const QString &dir) +{ + QDir pd(path); + + if (pd.isAbsolute() && pd.exists()) + return pd.absolutePath(); + else + return QString("assets://") + dir; +} +#endif // Q_OS_ANDROID QString ProgramPaths::mapDir(bool writable) { if (writable) - return QDir(QStandardPaths::writableLocation( - QStandardPaths::AppDataLocation)).filePath(MAP_DIR); + return QDir(QStandardPaths::writableLocation(DATA_LOCATION)) + .filePath(MAP_DIR); else - return QStandardPaths::locate(QStandardPaths::AppDataLocation, +#ifdef Q_OS_ANDROID + return assetsPath(QStandardPaths::locate(DATA_LOCATION, MAP_DIR, + QStandardPaths::LocateDirectory), MAP_DIR); +#else // Q_OS_ANDROID + return QStandardPaths::locate(DATA_LOCATION, MAP_DIR, QStandardPaths::LocateDirectory); +#endif // Q_OS_ANDROID } QString ProgramPaths::poiDir(bool writable) { if (writable) - return QDir(QStandardPaths::writableLocation( - QStandardPaths::AppDataLocation)).filePath(POI_DIR); + return QDir(QStandardPaths::writableLocation(DATA_LOCATION)) + .filePath(POI_DIR); else - return QStandardPaths::locate(QStandardPaths::AppDataLocation, - POI_DIR, QStandardPaths::LocateDirectory); + return QStandardPaths::locate(DATA_LOCATION, POI_DIR, + QStandardPaths::LocateDirectory); } QString ProgramPaths::csvDir(bool writable) { if (writable) - return QDir(QStandardPaths::writableLocation( - QStandardPaths::AppDataLocation)).filePath(CSV_DIR); + return QDir(QStandardPaths::writableLocation(DATA_LOCATION)) + .filePath(CSV_DIR); else - return QStandardPaths::locate(QStandardPaths::AppDataLocation, - CSV_DIR, QStandardPaths::LocateDirectory); +#ifdef Q_OS_ANDROID + return assetsPath(QStandardPaths::locate(DATA_LOCATION, CSV_DIR, + QStandardPaths::LocateDirectory), CSV_DIR); +#else // Q_OS_ANDROID + return QStandardPaths::locate(DATA_LOCATION, CSV_DIR, + QStandardPaths::LocateDirectory); +#endif // Q_OS_ANDROID } QString ProgramPaths::demDir(bool writable) { if (writable) - return QDir(QStandardPaths::writableLocation( - QStandardPaths::AppDataLocation)).filePath(DEM_DIR); + return QDir(QStandardPaths::writableLocation(DATA_LOCATION)) + .filePath(DEM_DIR); else - return QStandardPaths::locate(QStandardPaths::AppDataLocation, - DEM_DIR, QStandardPaths::LocateDirectory); + return QStandardPaths::locate(DATA_LOCATION, DEM_DIR, + QStandardPaths::LocateDirectory); } QString ProgramPaths::styleDir(bool writable) { if (writable) - return QDir(QStandardPaths::writableLocation( - QStandardPaths::AppDataLocation)).filePath(STYLE_DIR); + return QDir(QStandardPaths::writableLocation(DATA_LOCATION)) + .filePath(STYLE_DIR); else - return QStandardPaths::locate(QStandardPaths::AppDataLocation, - STYLE_DIR, QStandardPaths::LocateDirectory); + return QStandardPaths::locate(DATA_LOCATION, STYLE_DIR, + QStandardPaths::LocateDirectory); } QString ProgramPaths::symbolsDir(bool writable) { if (writable) - return QDir(QStandardPaths::writableLocation( - QStandardPaths::AppDataLocation)).filePath(SYMBOLS_DIR); + return QDir(QStandardPaths::writableLocation(DATA_LOCATION)) + .filePath(SYMBOLS_DIR); else - return QStandardPaths::locate(QStandardPaths::AppDataLocation, - SYMBOLS_DIR, QStandardPaths::LocateDirectory); +#ifdef Q_OS_ANDROID + return assetsPath(QStandardPaths::locate(DATA_LOCATION, SYMBOLS_DIR, + QStandardPaths::LocateDirectory), SYMBOLS_DIR); +#else // Q_OS_ANDROID + return QStandardPaths::locate(DATA_LOCATION, SYMBOLS_DIR, + QStandardPaths::LocateDirectory); +#endif // Q_OS_ANDROID } QString ProgramPaths::tilesDir() @@ -88,36 +120,31 @@ QString ProgramPaths::tilesDir() QString ProgramPaths::translationsDir() { - return QStandardPaths::locate(QStandardPaths::AppDataLocation, - TRANSLATIONS_DIR, QStandardPaths::LocateDirectory); + return QStandardPaths::locate(DATA_LOCATION, TRANSLATIONS_DIR, + QStandardPaths::LocateDirectory); } QString ProgramPaths::ellipsoidsFile() { - return QStandardPaths::locate(QStandardPaths::AppDataLocation, - CSV_DIR "/" ELLIPSOID_FILE, QStandardPaths::LocateFile); + return QDir(csvDir()).filePath(ELLIPSOID_FILE); } QString ProgramPaths::gcsFile() { - return QStandardPaths::locate(QStandardPaths::AppDataLocation, - CSV_DIR "/" GCS_FILE, QStandardPaths::LocateFile); + return QDir(csvDir()).filePath(GCS_FILE); } QString ProgramPaths::pcsFile() { - return QStandardPaths::locate(QStandardPaths::AppDataLocation, - CSV_DIR "/" PCS_FILE, QStandardPaths::LocateFile); + return QDir(csvDir()).filePath(PCS_FILE); } QString ProgramPaths::typFile() { - return QStandardPaths::locate(QStandardPaths::AppDataLocation, - STYLE_DIR "/" TYP_FILE, QStandardPaths::LocateFile); + return QDir(styleDir()).filePath(TYP_FILE); } QString ProgramPaths::renderthemeFile() { - return QStandardPaths::locate(QStandardPaths::AppDataLocation, - STYLE_DIR "/" RENDERTHEME_FILE, QStandardPaths::LocateFile); + return QDir(styleDir()).filePath(RENDERTHEME_FILE); } diff --git a/src/common/util.cpp b/src/common/util.cpp index 7a198f5a..e44a1439 100644 --- a/src/common/util.cpp +++ b/src/common/util.cpp @@ -1,9 +1,71 @@ #include #include #include +#ifdef Q_OS_ANDROID +#include +#include +#include +#include +#endif // Q_OS_ANDROID #include "util.h" +#ifdef Q_OS_ANDROID +static QString documentName(const QString &path) +{ + QJniEnvironment env; + + QJniObject urlString = QJniObject::fromString(path); + QJniObject uri = QJniObject::callStaticObjectMethod("android/net/Uri", + "parse", "(Ljava/lang/String;)Landroid/net/Uri;", + urlString.object()); + if (!uri.isValid()) { + env->ExceptionClear(); + return QString(); + } + QJniObject context = QNativeInterface::QAndroidApplication::context(); + if (!context.isValid()) { + env->ExceptionClear(); + return QString(); + } + QJniObject contentResolver = context.callObjectMethod( + "getContentResolver", "()Landroid/content/ContentResolver;"); + if (!contentResolver.isValid()) { + env->ExceptionClear(); + return QString(); + } + QJniObject columnName = QJniObject::getStaticObjectField( + "android/provider/MediaStore$MediaColumns", "DISPLAY_NAME"); + if (!columnName.isValid()) { + env->ExceptionClear(); + return QString(); + } + jobjectArray stringArray = env->NewObjectArray( + 1, env->FindClass("java/lang/String"), 0); + env->SetObjectArrayElement(stringArray, 0, columnName.object()); + QJniObject cursor = contentResolver.callObjectMethod("query", + "(Landroid/net/Uri;[Ljava/lang/String;Landroid/os/Bundle;" + "Landroid/os/CancellationSignal;)Landroid/database/Cursor;", + uri.object(), stringArray, 0, 0); + if (!cursor.isValid()) { + env->ExceptionClear(); + return QString(); + } + if (!cursor.callMethod("moveToFirst")) { + env->ExceptionClear(); + return QString(); + } + QJniObject str = cursor.callObjectMethod("getString", + "(I)Ljava/lang/String;", 0); + if (!str.isValid()) { + env->ExceptionClear(); + return QString(); + } + + return str.toString(); +} +#endif // Q_OS_ANDROID + int Util::str2int(const char *str, int len) { int res = 0; @@ -52,6 +114,28 @@ double Util::niceNum(double x, bool round) QString Util::file2name(const QString &path) { - QFileInfo fi(path); + QFileInfo fi(displayName(path)); return fi.baseName().replace('_', ' '); } + +QString Util::displayName(const QString &path) +{ +#ifdef Q_OS_ANDROID + QUrl url(path); + + // Not an Android URL, return standard filename. + if (url.scheme() != "content") { + QFileInfo fi(path); + return fi.fileName(); + // Directory browsing URLs. Those can not be translated using the Android + // content resolver but we can get the filename from the URL path. + } else if (url.path().startsWith("/tree/")) { + QFileInfo fi(url.fileName()); + return fi.fileName(); + // Translate all "regular" android URLs using the Android content resolver. + } else + return documentName(path); +#else + return path; +#endif // Q_OS_ANDROID +} diff --git a/src/common/util.h b/src/common/util.h index ccd6ca1c..2b1e2b44 100644 --- a/src/common/util.h +++ b/src/common/util.h @@ -8,6 +8,7 @@ namespace Util int str2int(const char *str, int len); double niceNum(double x, bool round); QString file2name(const QString &path); + QString displayName(const QString &path); } #endif // UTIL_H