[ktuberling] /: Add initial Android version

Albert Astals Cid null at kde.org
Mon Oct 9 11:25:27 UTC 2017


Git commit 7d4fde1f5a039d1a700d6806a4c000046c845465 by Albert Astals Cid.
Committed on 09/10/2017 at 11:25.
Pushed by aacid into branch 'master'.

Add initial Android version

Includes a few other tweaks for the desktop version too but very minor

M  +97   -68   CMakeLists.txt
A  +37   -0    android_data/AndroidManifest.xml
A  +-    --    android_data/audio-volume-high.png
A  +13   -0    android_data/audio-volume-high.svg
A  +-    --    android_data/audio-volume-muted.png
A  +21   -0    android_data/audio-volume-muted.svg
A  +-    --    android_data/games-config-theme.png
A  +13   -0    android_data/games-config-theme.svg
A  +-    --    android_data/res/drawable/ktuberling.png
A  +7    -0    android_data/resources.qrc
M  +0    -6    doc/index.docbook
A  +42   -0    filefactory.cpp     [License: GPL (v2+)]
A  +23   -0    filefactory.h     [License: GPL (v2+)]
A  +137  -0    main_mobile.cpp     [License: GPL (v2+)]
M  +-    --    pics/robot_workshop.svgz
M  +40   -29   playground.cpp
M  +18   -6    playground.h
M  +16   -22   soundfactory.cpp
M  +11   -8    soundfactory.h
M  +5    -5    toplevel.cpp
M  +8    -6    toplevel.h

https://commits.kde.org/ktuberling/7d4fde1f5a039d1a700d6806a4c000046c845465

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 42f415f..ca6240e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -7,26 +7,27 @@ set (KF5_MIN_VERSION "5.15.0")
 find_package(ECM 1.7.0 REQUIRED CONFIG)
 set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR})
 
-find_package(Qt5 ${QT_MIN_VERSION} REQUIRED NO_MODULE COMPONENTS PrintSupport Svg Widgets Xml)
-find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS
-    Completion
-    Config
-    ConfigWidgets
-    CoreAddons
-    Crash
-    DBusAddons
-    KIO
-    DocTools
-    I18n
-    KDELibs4Support #TODO eventually remove kdelibs4support
-    WidgetsAddons
-    XmlGui
-)
-
-find_package(KF5KDEGames 4.9.0 REQUIRED)
-find_package(Phonon4Qt5 CONFIG REQUIRED)
+find_package(Qt5 ${QT_MIN_VERSION} REQUIRED NO_MODULE COMPONENTS PrintSupport Svg Widgets Xml Multimedia)
+find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Config)
+
+if(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Android")
+    find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS
+        Completion
+        ConfigWidgets
+        CoreAddons
+        Crash
+        DBusAddons
+        KIO
+        DocTools
+        I18n
+        KDELibs4Support #TODO eventually remove kdelibs4support
+        WidgetsAddons
+        XmlGui
+    )
+
+    find_package(KF5KDEGames 4.9.0 REQUIRED)
+endif()
 
-include_directories(BEFORE ${PHONON_INCLUDES})
 
 include(FeatureSummary)
 include(ECMAddAppIcon)
@@ -40,59 +41,87 @@ add_definitions(-DQT_USE_FAST_CONCATENATION -DQT_USE_FAST_OPERATOR_PLUS)
 
 add_subdirectory(sounds)
 add_subdirectory(pics)
-add_subdirectory(doc)
+if(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Android")
+    add_subdirectory(doc)
+endif()
 
 ########### next target ###############
 
-set(ktuberling_SRCS 
-   action.cpp 
-   main.cpp 
-   toplevel.cpp 
-   playground.cpp 
-   todraw.cpp 
-   soundfactory.cpp 
-   playgrounddelegate.cpp
+set(ktuberling_common_SRCS
+   action.cpp
+   playground.cpp
+   todraw.cpp
+   soundfactory.cpp
+   filefactory.cpp
 )
 
-file(GLOB ICONS_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/*-apps-ktuberling.png")
-ecm_add_app_icon(ktuberling_SRCS ICONS ${ICONS_SRCS})
-add_executable(ktuberling ${ktuberling_SRCS})
-
-target_link_libraries(ktuberling
-    Qt5::PrintSupport
-    Qt5::Svg
-    KF5::Completion
-    KF5::Crash
-    KF5::DBusAddons
-    KF5::KIOCore
-    KF5::KDELibs4Support
-    KF5::XmlGui
-    Phonon::phonon4qt5
-    KF5KDEGames
-)
-
-install(TARGETS ktuberling  ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
-
-
-########### install files ###############
-
-install(PROGRAMS org.kde.ktuberling.desktop  DESTINATION  ${KDE_INSTALL_APPDIR})
-install(FILES ktuberlingui.rc  DESTINATION  ${KDE_INSTALL_KXMLGUI5DIR}/ktuberling)
-
-ecm_install_icons(ICONS
-    128-apps-ktuberling.png
-    16-apps-ktuberling.png
-    22-apps-ktuberling.png
-    32-apps-ktuberling.png
-    48-apps-ktuberling.png
-    64-apps-ktuberling.png
-    128-mimetypes-application-x-tuberling.png
-    16-mimetypes-application-x-tuberling.png
-    22-mimetypes-application-x-tuberling.png
-    32-mimetypes-application-x-tuberling.png
-    48-mimetypes-application-x-tuberling.png
-    64-mimetypes-application-x-tuberling.png
-    DESTINATION ${KDE_INSTALL_ICONDIR} THEME hicolor
-)
+if(${CMAKE_SYSTEM_NAME} MATCHES "Android")
+    set(ktuberling_mobile_SRCS
+        ${ktuberling_common_SRCS}
+        main_mobile.cpp
+    )
+
+    qt5_add_resources(ktuberling_mobile_SRCS android_data/resources.qrc)
+
+    add_executable(ktuberling_mobile ${ktuberling_mobile_SRCS})
+
+    target_link_libraries(ktuberling_mobile
+        Qt5::Gui
+        Qt5::Svg
+        Qt5::Multimedia
+        Qt5::Xml
+        Qt5::Widgets
+        KF5::ConfigCore )
+
+    install(TARGETS ktuberling_mobile RUNTIME DESTINATION bin)
+
+else()
+
+    set(ktuberling_SRCS
+        ${ktuberling_common_SRCS}
+        main.cpp
+        toplevel.cpp
+        playgrounddelegate.cpp
+    )
+
+    file(GLOB ICONS_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/*-apps-ktuberling.png")
+    ecm_add_app_icon(ktuberling_SRCS ICONS ${ICONS_SRCS})
+
+    add_executable(ktuberling ${ktuberling_SRCS})
+
+    target_link_libraries(ktuberling
+        Qt5::PrintSupport
+        Qt5::Svg
+        Qt5::Multimedia
+        KF5::Completion
+        KF5::Crash
+        KF5::DBusAddons
+        KF5::KIOCore
+        KF5::KDELibs4Support
+        KF5::XmlGui
+        KF5KDEGames
+    )
+
+    install(TARGETS ktuberling  ${KDE_INSTALL_TARGETS_DEFAULT_ARGS})
+
+    install(PROGRAMS org.kde.ktuberling.desktop  DESTINATION  ${KDE_INSTALL_APPDIR})
+    install(FILES ktuberlingui.rc  DESTINATION  ${KDE_INSTALL_KXMLGUI5DIR}/ktuberling)
+
+    ecm_install_icons(ICONS
+        128-apps-ktuberling.png
+        16-apps-ktuberling.png
+        22-apps-ktuberling.png
+        32-apps-ktuberling.png
+        48-apps-ktuberling.png
+        64-apps-ktuberling.png
+        128-mimetypes-application-x-tuberling.png
+        16-mimetypes-application-x-tuberling.png
+        22-mimetypes-application-x-tuberling.png
+        32-mimetypes-application-x-tuberling.png
+        48-mimetypes-application-x-tuberling.png
+        64-mimetypes-application-x-tuberling.png
+        DESTINATION ${KDE_INSTALL_ICONDIR} THEME hicolor
+    )
+endif()
 
 feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES)
diff --git a/android_data/AndroidManifest.xml b/android_data/AndroidManifest.xml
new file mode 100644
index 0000000..f57c1f0
--- /dev/null
+++ b/android_data/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionName="0.0.3" package="org.kde.ktuberling" android:installLocation="auto" android:versionCode="7">
+        <application android:name="org.qtproject.qt5.android.bindings.QtApplication"
+                     android:label="KTuberling"
+                     android:icon="@drawable/ktuberling">
+                <activity android:name="org.qtproject.qt5.android.bindings.QtActivity"
+                          android:label="KTuberling"
+                          android:screenOrientation="landscape"
+                          android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|locale|fontScale|keyboard|keyboardHidden|navigation">
+                        <intent-filter>
+                                <action android:name="android.intent.action.MAIN"/>
+                                <category android:name="android.intent.category.LAUNCHER"/>
+                        </intent-filter>
+                        <meta-data android:name="android.app.lib_name" android:value="-- %%INSERT_APP_LIB_NAME%% --"/>
+                        <meta-data android:name="android.app.qt_sources_resource_id" android:resource="@array/qt_sources"/>
+                        <meta-data android:name="android.app.repository" android:value="default"/>
+                        <meta-data android:name="android.app.qt_libs_resource_id" android:resource="@array/qt_libs"/>
+                        <meta-data android:name="android.app.bundled_libs_resource_id" android:resource="@array/bundled_libs"/>
+                        <!-- Deploy Qt libs as part of package -->
+                        <meta-data android:name="android.app.bundle_local_qt_libs" android:value="-- %%BUNDLE_LOCAL_QT_LIBS%% --"/>
+                        <meta-data android:name="android.app.bundled_in_lib_resource_id" android:resource="@array/bundled_in_lib"/>
+                        <meta-data android:name="android.app.bundled_in_assets_resource_id" android:resource="@array/bundled_in_assets"/>
+                        <!-- Run with local libs -->
+                        <meta-data android:name="android.app.use_local_qt_libs" android:value="-- %%USE_LOCAL_QT_LIBS%% --"/>
+                        <meta-data android:name="android.app.libs_prefix" android:value="/data/local/tmp/qt/"/>
+                        <meta-data android:name="android.app.load_local_libs" android:value="-- %%INSERT_LOCAL_LIBS%% --"/>
+                        <meta-data android:name="android.app.load_local_jars" android:value="-- %%INSERT_LOCAL_JARS%% --"/>
+                        <meta-data android:name="android.app.static_init_classes" android:value="-- %%INSERT_INIT_CLASSES%% --"/>
+                        <!--  Messages maps -->
+                        <meta-data android:name="android.app.ministro_not_found_msg" android:value="@string/ministro_not_found_msg"/>
+                        <meta-data android:name="android.app.ministro_needed_msg" android:value="@string/ministro_needed_msg"/>
+                        <meta-data android:name="android.app.fatal_error_msg" android:value="@string/fatal_error_msg"/>
+                </activity>
+        </application>
+        <supports-screens android:anyDensity="true" android:normalScreens="true" android:smallScreens="true" android:largeScreens="true"/>
+        <uses-sdk android:minSdkVersion="16" android:targetSdkVersion="19"/>
+</manifest>
diff --git a/android_data/audio-volume-high.png b/android_data/audio-volume-high.png
new file mode 100644
index 0000000..1df64b9
Binary files /dev/null and b/android_data/audio-volume-high.png differ
diff --git a/android_data/audio-volume-high.svg b/android_data/audio-volume-high.svg
new file mode 100644
index 0000000..a5dca4c
--- /dev/null
+++ b/android_data/audio-volume-high.svg
@@ -0,0 +1,13 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
+  <defs id="defs3051">
+    <style type="text/css" id="current-color-scheme">
+      .ColorScheme-Text {
+        color:#4d4d4d;
+      }
+      </style>
+  </defs>
+    <path
+       style="fill:currentColor;fill-opacity:1;stroke:none" 
+       d="M 10.988281 3 L 6 7.9902344 L 6 8 L 6 9 L 6 13 L 6 14 L 6 14.009766 L 10.988281 19 L 12 19 L 12 18.597656 L 12 3.4023438 L 12 3 L 10.988281 3 z M 13.865234 3.5371094 L 13.621094 4.5136719 A 7 7 0 0 1 18 11 A 7 7 0 0 1 13.619141 17.478516 L 13.863281 18.453125 A 8 8 0 0 0 19 11 A 8 8 0 0 0 13.865234 3.5371094 z M 14.324219 7.28125 L 13.785156 8.1425781 A 4 4 0 0 1 15 11 A 4 4 0 0 1 13.789062 13.861328 L 14.328125 14.724609 A 5 5 0 0 0 16 11 A 5 5 0 0 0 14.324219 7.28125 z M 3 8 L 3 9 L 3 13 L 3 14 L 5 14 L 5 13 L 5 9 L 5 8 L 3 8 z "
+          class="ColorScheme-Text"/>
+</svg>
diff --git a/android_data/audio-volume-muted.png b/android_data/audio-volume-muted.png
new file mode 100644
index 0000000..29508f7
Binary files /dev/null and b/android_data/audio-volume-muted.png differ
diff --git a/android_data/audio-volume-muted.svg b/android_data/audio-volume-muted.svg
new file mode 100644
index 0000000..1a9f68b
--- /dev/null
+++ b/android_data/audio-volume-muted.svg
@@ -0,0 +1,21 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
+  <defs id="defs3051">
+    <style type="text/css" id="current-color-scheme">
+      .ColorScheme-Text {
+        color:#4d4d4d;
+      }
+      .ColorScheme-NegativeText {
+        color:#da4453;
+      }
+      </style>
+  </defs>
+  <path 
+     style="fill:currentColor;fill-opacity:1;stroke:none" 
+     d="M 10.988281 3 L 6 7.9902344 L 6 8 L 6 9 L 6 13 L 6 14 L 6 14.009766 L 10.988281 19 L 12 19 L 12 18.597656 L 12 3.4023438 L 12 3 L 10.988281 3 z M 3 8 L 3 9 L 3 13 L 3 14 L 5 14 L 5 13 L 5 9 L 5 8 L 3 8 z "
+     class="ColorScheme-Text"/>
+  <path
+     style="fill:currentColor;fill-opacity:1;stroke:none" 
+     d="M 13 10 L 13 12 L 19 12 L 19 10 L 13 10 z "
+     class="ColorScheme-NegativeText"
+     />
+</svg>
diff --git a/android_data/games-config-theme.png b/android_data/games-config-theme.png
new file mode 100644
index 0000000..29a358a
Binary files /dev/null and b/android_data/games-config-theme.png differ
diff --git a/android_data/games-config-theme.svg b/android_data/games-config-theme.svg
new file mode 100644
index 0000000..c74afd0
--- /dev/null
+++ b/android_data/games-config-theme.svg
@@ -0,0 +1,13 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+  <defs id="defs3051">
+    <style type="text/css" id="current-color-scheme">
+      .ColorScheme-Text {
+        color:#4d4d4d;
+      }
+      </style>
+  </defs>
+ <path style="fill:currentColor;fill-opacity:1;stroke:none" 
+     d="M 19.513672 4.0078125 A 2 9 45 0 0 12.068359 9.1054688 A 2 9 45 0 0 8.5253906 13.21875 C 9.6014506 13.56139 10.440563 14.400494 10.783203 15.476562 A 2 9 45 0 0 14.896484 11.933594 A 2 9 45 0 0 19.845703 4.15625 A 2 9 45 0 0 19.513672 4.0078125 z M 8 14.664062 C 3.99999 15.735864 7 18.26795 4 20 C 8.00339 20 10 17.99918 10 16.664062 C 10 15.999182 10.0676 14.774622 8 14.664062 z "
+     class="ColorScheme-Text"
+     />
+</svg>
diff --git a/android_data/res/drawable/ktuberling.png b/android_data/res/drawable/ktuberling.png
new file mode 100644
index 0000000..6d5bbbf
Binary files /dev/null and b/android_data/res/drawable/ktuberling.png differ
diff --git a/android_data/resources.qrc b/android_data/resources.qrc
new file mode 100644
index 0000000..8a596b4
--- /dev/null
+++ b/android_data/resources.qrc
@@ -0,0 +1,7 @@
+<RCC>
+    <qresource prefix="/">
+        <file>games-config-theme.png</file>
+        <file>audio-volume-high.png</file>
+        <file>audio-volume-muted.png</file>
+    </qresource>
+</RCC>
diff --git a/doc/index.docbook b/doc/index.docbook
index 91cbdef..83bcd64 100644
--- a/doc/index.docbook
+++ b/doc/index.docbook
@@ -508,12 +508,6 @@ It will contain the playgrounds installed in your system.
 <sect2>
 <title>The Speech Menu</title>
 
-<!--FIXME
-<para>
-Please note that you need to have <command>&phonon;</command> installed
-and properly configured to be able to hear sounds.
-</para>-->
-
 <variablelist>
 
 <varlistentry>
diff --git a/filefactory.cpp b/filefactory.cpp
new file mode 100644
index 0000000..ef3d39c
--- /dev/null
+++ b/filefactory.cpp
@@ -0,0 +1,42 @@
+/***************************************************************************
+ *   Copyright (C) 2017 by Albert Astals Cid <aacid at kde.org>               *
+ *                                                                         *
+ *   This program is free software; you can redistribute it and/or modify  *
+ *   it under the terms of the GNU General Public License as published by  *
+ *   the Free Software Foundation; either version 2 of the License, or     *
+ *   (at your option) any later version.                                   *
+ ***************************************************************************/
+
+#include "filefactory.h"
+
+#include <QFileInfo>
+#include <QStandardPaths>
+
+bool FileFactory::folderExists(const QString &relativePath)
+{
+#if defined(Q_OS_ANDROID)
+    QFileInfo fi("/data/data/org.kde.ktuberling/qt-reserved-files/share/ktuberling/" + relativePath);
+    return fi.isDir();
+#else
+    return !(QStandardPaths::locate(QStandardPaths::AppDataLocation, relativePath, QStandardPaths::LocateDirectory).isEmpty());
+#endif
+}
+
+QString FileFactory::locate(const QString &relativePath)
+{
+#if defined(Q_OS_ANDROID)
+    return "/data/data/org.kde.ktuberling/qt-reserved-files/share/ktuberling/" + relativePath;
+#else
+    return QStandardPaths::locate(QStandardPaths::AppDataLocation, relativePath);
+#endif
+}
+
+QStringList FileFactory::locateAll(const QString &relativePath)
+{
+#if defined(Q_OS_ANDROID)
+    return { "/data/data/org.kde.ktuberling/qt-reserved-files/share/ktuberling/" + relativePath };
+#else
+    return QStandardPaths::locateAll(QStandardPaths::AppDataLocation, relativePath, QStandardPaths::LocateDirectory);
+#endif
+}
+
diff --git a/filefactory.h b/filefactory.h
new file mode 100644
index 0000000..f7954b8
--- /dev/null
+++ b/filefactory.h
@@ -0,0 +1,23 @@
+/***************************************************************************
+ *   Copyright (C) 2017 by Albert Astals Cid <aacid at kde.org>               *
+ *                                                                         *
+ *   This program is free software; you can redistribute it and/or modify  *
+ *   it under the terms of the GNU General Public License as published by  *
+ *   the Free Software Foundation; either version 2 of the License, or     *
+ *   (at your option) any later version.                                   *
+ ***************************************************************************/
+
+#ifndef FILEFACTORY_H
+#define FILEFACTORY_H
+
+class QString;
+class QStringList;
+
+namespace FileFactory
+{
+    bool folderExists(const QString &relativePath);
+    QString locate(const QString &relativePath);
+    QStringList locateAll(const QString &relativePath);
+};
+
+#endif
diff --git a/main_mobile.cpp b/main_mobile.cpp
new file mode 100644
index 0000000..429323f
--- /dev/null
+++ b/main_mobile.cpp
@@ -0,0 +1,137 @@
+/***************************************************************************
+ *   Copyright (C) 1999-2006 by Éric Bischoff <ebischoff at nerim.net>        *
+ *   Copyright (C) 2007 by Albert Astals Cid <aacid at kde.org>               *
+ *                                                                         *
+ *   This program is free software; you can redistribute it and/or modify  *
+ *   it under the terms of the GNU General Public License as published by  *
+ *   the Free Software Foundation; either version 2 of the License, or     *
+ *   (at your option) any later version.                                   *
+ ***************************************************************************/
+
+#include <QApplication>
+#include <QDebug>
+#include <QDesktopWidget>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QPushButton>
+
+#include "filefactory.h"
+#include "soundfactory.h"
+#include "playground.h"
+
+static const char version[] = "1.0.0";
+
+class KTuberlingMobile : public PlayGroundCallbacks, public SoundFactoryCallbacks
+{
+public:
+  KTuberlingMobile()
+   : m_soundEnabled(true)
+  {
+    m_soundFactory = new SoundFactory(this);
+    m_soundFactory->registerLanguages();
+    m_soundFactory->loadLanguage(FileFactory::locate("sounds/en.soundtheme"));
+
+    QWidget *mainWidget = new QWidget();
+    QHBoxLayout *lay = new QHBoxLayout(mainWidget);
+
+    m_themesWidget = new QWidget();
+    m_gameboardLayout = new QGridLayout(m_themesWidget);
+
+    m_playground = new PlayGround(this, mainWidget);
+    m_playground->registerPlayGrounds();
+    m_playground->lockAspectRatio(true);
+    m_playground->setAllowOnlyDrag(true);
+    m_playground->loadPlayGround(FileFactory::locate("pics/default_theme.theme"));
+
+    QVBoxLayout *sideLayout = new QVBoxLayout();
+
+    // Not sure this is the best way but it works for now
+    const int screenWidth = QDesktopWidget().screenGeometry().width();
+    const int iconWidth = screenWidth / 15;
+
+    QPushButton *themesButton = new QPushButton(mainWidget);
+    themesButton->setIcon(QPixmap(":/games-config-theme.png"));
+    themesButton->setIconSize(QSize(iconWidth, iconWidth));
+    themesButton->setFocusPolicy(Qt::NoFocus);
+    QObject::connect(themesButton, &QPushButton::clicked, [this, mainWidget] {
+      m_themesWidget->showFullScreen();
+    });
+
+    QPushButton *soundsButton = new QPushButton(mainWidget);
+    soundsButton->setIcon(QPixmap(":/audio-volume-high.png"));
+    soundsButton->setIconSize(QSize(iconWidth, iconWidth));
+    soundsButton->setFocusPolicy(Qt::NoFocus);
+    QObject::connect(soundsButton, &QPushButton::clicked, [this, soundsButton] {
+      m_soundEnabled = !m_soundEnabled;
+      soundsButton->setIcon(QPixmap(m_soundEnabled ? ":/audio-volume-high.png" : ":/audio-volume-muted.png"));
+    });
+
+    sideLayout->addWidget(themesButton);
+    sideLayout->addWidget(soundsButton);
+    sideLayout->addStretch(1);
+
+    lay->setContentsMargins(0, 0, 0, 0);
+    lay->setSpacing(0);
+    lay->addWidget(m_playground);
+    lay->addLayout(sideLayout);
+
+    mainWidget->showFullScreen();
+  }
+
+  ~KTuberlingMobile()
+  {
+    delete m_soundFactory;
+  }
+
+  void playSound(const QString &ref) override
+  {
+    m_soundFactory->playSound(ref);
+  }
+
+  void changeGameboard(const QString &/*gameboard*/) override
+  {
+    // Only needed when loading a file so not needed for now
+  }
+
+  void registerGameboard(const QString& menuText, const QString& boardFile, const QPixmap &/*pixmap*/) override
+  {
+    // TODO this should be scrollable
+    // TODO use the pixmap
+    QPushButton *pb = new QPushButton(menuText);
+    QObject::connect(pb, &QPushButton::clicked, [this, boardFile] {
+      m_playground->loadPlayGround(boardFile);
+      m_themesWidget->hide();
+    });
+
+    m_gameboardLayout->addWidget(pb, m_gameboardLayout->count() / 2, m_gameboardLayout->count() % 2);
+  }
+
+  bool isSoundEnabled() const override
+  {
+    return m_soundEnabled;
+  }
+
+  void registerLanguage(const QString &/*code*/, const QString &/*soundFile*/, bool /*enabled*/)
+  {
+    // TODO
+  }
+
+private:
+  SoundFactory *m_soundFactory;
+  PlayGround *m_playground;
+  QWidget *m_themesWidget;
+  QGridLayout *m_gameboardLayout;
+  bool m_soundEnabled;
+};
+
+// Main function
+Q_DECL_EXPORT int main(int argc, char *argv[])
+{
+  QApplication app(argc, argv);
+  QLocale::system().name(); // needed to workaround QTBUG-41385
+  app.setApplicationName("ktuberling");
+
+  KTuberlingMobile tuberling;
+
+  return app.exec();
+}
diff --git a/pics/robot_workshop.svgz b/pics/robot_workshop.svgz
index 57a61cf..2e0717f 100644
Binary files a/pics/robot_workshop.svgz and b/pics/robot_workshop.svgz differ
diff --git a/playground.cpp b/playground.cpp
index afe70c2..58d8d1f 100644
--- a/playground.cpp
+++ b/playground.cpp
@@ -12,12 +12,12 @@
 
 #include "playground.h"
 
-#include <KLocalizedString>
 #include <kconfig.h>
 #include <kconfiggroup.h>
 #include <qdebug.h>
 
 #include <QAction>
+#include <QApplication>
 #include <QCursor>
 #include <QDataStream>
 #include <QDir>
@@ -27,15 +27,10 @@
 #include <QGraphicsSvgItem>
 #include <QMouseEvent>
 #include <QPainter>
-#include <QPrinter>
-#include <QStandardPaths>
-
-#include <kstandardaction.h>
-#include <kactioncollection.h>
-#include <kstandardshortcut.h>
+#include <QPagedPaintDevice>
 
 #include "action.h"
-#include "toplevel.h"
+#include "filefactory.h"
 #include "todraw.h"
 
 static const char *saveGameTextScaleTextMode = "KTuberlingSaveGameV2";
@@ -43,10 +38,9 @@ static const char *saveGameTextTextMode = "KTuberlingSaveGameV3";
 static const char *saveGameText = "KTuberlingSaveGameV4";
 
 // Constructor
-PlayGround::PlayGround(TopLevel *parent)
-    : QGraphicsView(parent), m_newItem(0), m_dragItem(0), m_nextZValue(1), m_lockAspect(false)
+PlayGround::PlayGround(PlayGroundCallbacks *callbacks, QWidget *parent)
+    : QGraphicsView(parent), m_callbacks(callbacks), m_newItem(0), m_dragItem(0), m_nextZValue(1), m_lockAspect(false), m_allowOnlyDrag(false)
 {
-  m_topLevel = parent;
   setFrameStyle(QFrame::NoFrame);
   setOptimizationFlag(QGraphicsView::DontSavePainterState, true); // all items here save the painter state
   setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
@@ -99,7 +93,7 @@ bool PlayGround::saveAs(const QString & name)
 }
 
 // Print gameboard's picture
-bool PlayGround::printPicture(QPrinter &printer)
+bool PlayGround::printPicture(QPagedPaintDevice &printer)
 {
   QPainter artist;
   QPixmap picture(getPicture());
@@ -139,6 +133,8 @@ void PlayGround::mousePressEvent(QMouseEvent *event)
 
   if (event->button() != Qt::LeftButton) return;
 
+  m_mousePressPos = event->pos();
+
   if (m_dragItem) placeDraggedItem(event->pos());
   else if (m_newItem) placeNewItem(event->pos());
   else
@@ -163,7 +159,7 @@ void PlayGround::mousePressEvent(QMouseEvent *event)
       QPointF itemPos = mapToScene(event->pos());
       itemPos -= QPointF(elementSize.width()/2, elementSize.height()/2);
 
-      m_topLevel->playSound(m_objectsNameSound.value(foundElem));
+      m_callbacks->playSound(m_objectsNameSound.value(foundElem));
 
       m_newItem = new ToDraw;
       m_newItem->setBeingDragged(true);
@@ -186,7 +182,7 @@ void PlayGround::mousePressEvent(QMouseEvent *event)
       {
         QString elem = m_dragItem->elementId();
 
-        m_topLevel->playSound(m_objectsNameSound.value(elem));
+        m_callbacks->playSound(m_objectsNameSound.value(elem));
         setCursor(Qt::BlankCursor);
         m_dragItem->setBeingDragged(true);
         m_itemDraggedPos = m_dragItem->pos();
@@ -202,18 +198,22 @@ void PlayGround::mousePressEvent(QMouseEvent *event)
 
 void PlayGround::mouseMoveEvent(QMouseEvent *event)
 {
-  if (m_newItem) {
+  ToDraw *movingItem = m_newItem ? m_newItem : m_dragItem;
+  if (movingItem) {
     QPointF itemPos = mapToScene(event->pos());
-    const QSizeF elementSize = m_newItem->transform().mapRect(m_newItem->unclippedRect()).size();
+    const QSizeF elementSize = movingItem->transform().mapRect(movingItem->unclippedRect()).size();
     itemPos -= QPointF(elementSize.width()/2, elementSize.height()/2);
 
-    m_newItem->setPos(clipPos(itemPos, m_newItem));
-  } else if (m_dragItem) {
-    QPointF itemPos = mapToScene(event->pos());
-    const QSizeF elementSize = m_dragItem->transform().mapRect(m_dragItem->unclippedRect()).size();
-    itemPos -= QPointF(elementSize.width()/2, elementSize.height()/2);
+    movingItem->setPos(clipPos(itemPos, movingItem));
+  }
+}
 
-    m_dragItem->setPos(clipPos(itemPos, m_dragItem));
+void PlayGround::mouseReleaseEvent(QMouseEvent *event)
+{
+  QPoint point = event->pos() - m_mousePressPos;
+  if (m_allowOnlyDrag || point.manhattanLength() > qApp->startDragDistance()) {
+      if (m_dragItem) placeDraggedItem(event->pos());
+      else if (m_newItem) placeNewItem(event->pos());
   }
 }
 
@@ -317,7 +317,7 @@ bool PlayGround::isAspectRatioLocked() const
 void PlayGround::registerPlayGrounds()
 {
   QSet<QString> list;
-  const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, QStringLiteral("pics"), QStandardPaths::LocateDirectory);
+  const QStringList dirs = FileFactory::locateAll(QStringLiteral("pics"));
   Q_FOREACH (const QString &dir, dirs)
   {
     const QStringList fileNames = QDir(dir).entryList(QStringList() << QStringLiteral("*.theme"));
@@ -327,6 +327,8 @@ void PlayGround::registerPlayGrounds()
     }
   }
 
+  QMap<QString, QPair<QString, QPixmap>> sortedByName;
+
   foreach(const QString &theme, list)
   {
     QFile layoutFile(theme);
@@ -336,21 +338,26 @@ void PlayGround::registerPlayGrounds()
       if (layoutDocument.setContent(&layoutFile))
       {
         QString desktop = layoutDocument.documentElement().attribute(QStringLiteral( "desktop" ));
-        KConfig c( QStandardPaths::locate(QStandardPaths::AppDataLocation, QLatin1String( "pics/" ) + desktop ) );
+        KConfig c( FileFactory::locate( QLatin1String( "pics/" ) + desktop ) );
         KConfigGroup cg = c.group("KTuberlingTheme");
         QString gameboard = layoutDocument.documentElement().attribute(QStringLiteral( "gameboard" ));
         QPixmap pixmap(200,100);
         pixmap.fill(Qt::transparent);
         playGroundPixmap(gameboard,pixmap);
-        m_topLevel->registerGameboard(cg.readEntry("Name"), theme, pixmap);
+        sortedByName.insertMulti(cg.readEntry("Name"), QPair<QString, QPixmap>(theme, pixmap));
       }
     }
   }
+
+  for(auto it = sortedByName.begin(); it != sortedByName.end(); ++it) {
+    m_callbacks->registerGameboard(it.key(), it.value().first, it.value().second);
+  }
+
 }
 
 void PlayGround::playGroundPixmap(const QString &playgroundName, QPixmap &pixmap)
 {
-  m_SvgRenderer.load(QStandardPaths::locate(QStandardPaths::AppDataLocation, QLatin1String( "pics/" ) + playgroundName ));
+  m_SvgRenderer.load(FileFactory::locate(QLatin1String( "pics/" ) + playgroundName ));
   QPainter painter(&pixmap);
   m_SvgRenderer.render(&painter,QStringLiteral( "background" ));
 }
@@ -370,7 +377,6 @@ bool PlayGround::loadPlayGround(const QString &gameboardFile)
 
   QFile layoutFile(gameboardFile);
   if (!layoutFile.open(QIODevice::ReadOnly)) return false;
-
   QDomDocument layoutDocument;
   if (!layoutDocument.setContent(&layoutFile)) return false;
 
@@ -382,7 +388,7 @@ bool PlayGround::loadPlayGround(const QString &gameboardFile)
   if (!bgColor.isValid())
     bgColor = Qt::white;
 
-  if (!m_SvgRenderer.load(QStandardPaths::locate(QStandardPaths::AppDataLocation, QLatin1String( "pics/" ) + gameboardName )))
+  if (!m_SvgRenderer.load(FileFactory::locate( QLatin1String( "pics/" ) + gameboardName )))
     return false;
 
   objectsList = playGroundElement.elementsByTagName(QStringLiteral( "object" ));
@@ -434,6 +440,11 @@ bool PlayGround::loadPlayGround(const QString &gameboardFile)
   return true;
 }
 
+void PlayGround::setAllowOnlyDrag(bool allowOnlyDrag)
+{
+  m_allowOnlyDrag = allowOnlyDrag;
+}
+
 QString PlayGround::currentGameboard() const
 {
   return m_gameboardFile;
@@ -481,7 +492,7 @@ PlayGround::LoadError PlayGround::loadFrom(const QString &name)
 
   qreal xFactor = 1.0;
   qreal yFactor = 1.0;
-  m_topLevel->changeGameboard(board);
+  m_callbacks->changeGameboard(board);
 
   reset();
 
diff --git a/playground.h b/playground.h
index 39072d3..a671e8b 100644
--- a/playground.h
+++ b/playground.h
@@ -23,16 +23,24 @@ class KActionCollection;
 
 class Action;
 class ToDraw;
-class TopLevel;
-class QPrinter;
+class QPagedPaintDevice;
 class QGraphicsSvgItem;
 
+class PlayGroundCallbacks
+{
+public:
+  virtual ~PlayGroundCallbacks() {}
+  virtual void playSound(const QString &ref) = 0;
+  virtual void changeGameboard(const QString &gameboard) = 0;
+  virtual void registerGameboard(const QString& menuText, const QString& boardFile, const QPixmap& pixmap) = 0;
+};
+
 class PlayGround : public QGraphicsView
 {
   Q_OBJECT
 
 public:
-  explicit PlayGround(TopLevel *parent);
+  explicit PlayGround(PlayGroundCallbacks *callbacks, QWidget *parent = nullptr);
   ~PlayGround();
 
   enum LoadError { NoError, OldFileVersionError, OtherError };
@@ -40,7 +48,7 @@ public:
   void reset();
   LoadError loadFrom(const QString &name);
   bool saveAs(const QString &name);
-  bool printPicture(QPrinter &printer);
+  bool printPicture(QPagedPaintDevice &printer);
   QPixmap getPicture();
 
   void connectRedoAction(QAction *action);
@@ -49,6 +57,8 @@ public:
   void registerPlayGrounds();
   bool loadPlayGround(const QString &gameboardFile);
 
+  void setAllowOnlyDrag(bool allowOnlyDrag);
+
   QString currentGameboard() const;
 
   bool isAspectRatioLocked() const;
@@ -60,6 +70,7 @@ protected:
 
   void mousePressEvent(QMouseEvent *event) override;
   void mouseMoveEvent(QMouseEvent *event) override;
+  void mouseReleaseEvent(QMouseEvent *event) override;
   void resizeEvent(QResizeEvent *event) override;
 
 private:
@@ -75,12 +86,12 @@ private:
   QGraphicsScene *scene() const;
   QUndoStack *undoStack() const;
 
+  PlayGroundCallbacks *m_callbacks;
   QString m_gameboardFile;				// the file the board
   QMap<QString, QString> m_objectsNameSound;		// map between element name and sound
   QMap<QString, double> m_objectsNameRatio;		// map between element name and scaling ratio
 
-  TopLevel *m_topLevel;					// Top-level window
-
+  QPoint m_mousePressPos;
   QPointF m_itemDraggedPos;
   ToDraw *m_newItem;				    // the new item we are moving
   ToDraw *m_dragItem;					// the existing item we are dragging
@@ -88,6 +99,7 @@ private:
   int m_nextZValue;					// the next Z value to use
 
   bool m_lockAspect;					// whether we are locking aspect ratio
+  bool m_allowOnlyDrag;
   QUndoGroup m_undoGroup;
   
   class SceneData
diff --git a/soundfactory.cpp b/soundfactory.cpp
index db207df..1a5e197 100644
--- a/soundfactory.cpp
+++ b/soundfactory.cpp
@@ -14,48 +14,42 @@
 
 #include <stdlib.h>
 
-#include <kmessagebox.h>
-#include <KLocalizedString>
-
-#include <phonon/MediaObject>
-
 #include <QDir>
 #include <QDomDocument>
 #include <QFile>
-#include <QStandardPaths>
+#include <QMediaPlayer>
+#include <QSet>
+#include <QUrl>
 
-#include "toplevel.h"
+#include "filefactory.h"
 
 // Constructor
-SoundFactory::SoundFactory(TopLevel *parent)
+SoundFactory::SoundFactory(SoundFactoryCallbacks *callbacks)
+ : m_callbacks(callbacks)
 {
-  topLevel = parent;
-  player = Phonon::createPlayer(Phonon::GameCategory);
-  player->setParent(parent);
+  player = new QMediaPlayer();
 }
 
 // Destructor
 SoundFactory::~SoundFactory()
 {
+  delete player;
 }
 
 // Play some sound
 void SoundFactory::playSound(const QString &soundRef) const
 {
-  int sound;
-  QString soundFile;
-
-  if (!topLevel->isSoundEnabled()) return;
+  if (!m_callbacks->isSoundEnabled()) return;
 
+  int sound;
   for (sound = 0; sound < sounds; sound++)
 	  if (!namesList[sound].compare(soundRef)) break;
   if (sound == sounds) return;
 
-  soundFile = QStandardPaths::locate(QStandardPaths::AppDataLocation, QLatin1String( "sounds/" ) + filesList[sound]);
+  const QString soundFile = FileFactory::locate(QLatin1String( "sounds/" ) + filesList[sound]);
   if (soundFile.isEmpty()) return;
 
-//printf("%s\n", (const char *) soundFile);
-  player->setCurrentSource(QUrl::fromLocalFile(soundFile));
+  player->setMedia(QUrl::fromLocalFile(soundFile));
   player->play();
 }
 
@@ -63,7 +57,7 @@ void SoundFactory::playSound(const QString &soundRef) const
 void SoundFactory::registerLanguages()
 {
   QSet<QString> list;
-  const QStringList dirs = QStandardPaths::locateAll(QStandardPaths::AppDataLocation, QStringLiteral("sounds"), QStandardPaths::LocateDirectory);
+  const QStringList dirs = FileFactory::locateAll(QStringLiteral("sounds"));
   Q_FOREACH (const QString &dir, dirs)
   {
     const QStringList fileNames = QDir(dir).entryList(QStringList() << QStringLiteral("*.soundtheme"));
@@ -81,9 +75,9 @@ void SoundFactory::registerLanguages()
       QDomDocument document;
       if (document.setContent(&file))
       {
-        QString code = document.documentElement().attribute(QStringLiteral( "code" ));
-        bool enabled = !(QStandardPaths::locate(QStandardPaths::AppDataLocation, QLatin1String( "sounds/" ) + code + QLatin1Char( '/' ), QStandardPaths::LocateDirectory).isEmpty());
-        topLevel->registerLanguage(code, soundTheme, enabled);
+        const QString code = document.documentElement().attribute(QStringLiteral( "code" ));
+        const bool enabled = FileFactory::folderExists(QLatin1String( "sounds/" ) + code + QLatin1Char( '/' ));
+        m_callbacks->registerLanguage(code, soundTheme, enabled);
       }
     }
   }
diff --git a/soundfactory.h b/soundfactory.h
index a8d81e1..34b38b6 100644
--- a/soundfactory.h
+++ b/soundfactory.h
@@ -15,18 +15,20 @@
 
 #include <QStringList>
 
-class TopLevel;
+class QMediaPlayer;
 
-namespace Phonon
+class SoundFactoryCallbacks
 {
-      class MediaObject;
-}
+public:
+  virtual ~SoundFactoryCallbacks() {};
+  virtual bool isSoundEnabled() const = 0;
+  virtual void registerLanguage(const QString &code, const QString &soundFile, bool enabled) = 0;
+};
 
 class SoundFactory
 {
 public:
-
-  explicit SoundFactory(TopLevel *parent);
+  explicit SoundFactory(SoundFactoryCallbacks *callbacks);
   ~SoundFactory();
 
   bool loadLanguage(const QString &selectedLanguageFile);
@@ -37,14 +39,15 @@ public:
   void registerLanguages();
 
 private:
+  SoundFactoryCallbacks *m_callbacks;
+
   QString currentSndFile;		// The current language
 
   int sounds;				// Number of sounds
   QStringList namesList,		// List of sound names
               filesList;           // List of sound files associated with each sound name
 
-  TopLevel *topLevel;		// Top-level window
-  Phonon::MediaObject *player;  // Sound player
+  QMediaPlayer *player;
 };
 
 #endif
diff --git a/toplevel.cpp b/toplevel.cpp
index 414a78a..6df33b9 100644
--- a/toplevel.cpp
+++ b/toplevel.cpp
@@ -33,10 +33,10 @@
 #include <QMimeDatabase>
 #include <QPrintDialog>
 #include <QPrinter>
-#include <QStandardPaths>
 #include <QTemporaryFile>
 #include <QWidgetAction>
 
+#include "filefactory.h"
 #include "playground.h"
 #include "soundfactory.h"
 #include "playgrounddelegate.h"
@@ -52,7 +52,7 @@ TopLevel::TopLevel()
 {
   QString board, language;
 
-  playGround = new PlayGround(this);
+  playGround = new PlayGround(this, this);
   playGround->setObjectName( QStringLiteral( "playGround" ) );
 
   soundFactory = new SoundFactory(this);
@@ -146,7 +146,7 @@ void TopLevel::changeGameboard(const QString &newGameBoard)
   QFileInfo fi(newGameBoard);
   if (fi.isRelative())
   {
-    fileToLoad = QStandardPaths::locate(QStandardPaths::AppDataLocation, QLatin1String( "pics/" ) + newGameBoard);
+    fileToLoad = FileFactory::locate(QLatin1String( "pics/" ) + newGameBoard);
   }
   else
   {
@@ -193,7 +193,7 @@ void TopLevel::changeLanguage(const QString &soundFile)
   QFileInfo fi(soundFile);
   if (fi.isRelative())
   {
-    fileToLoad = QStandardPaths::locate(QStandardPaths::AppDataLocation, QLatin1String( "sounds/" ) + soundFile);
+    fileToLoad = FileFactory::locate(QLatin1String( "sounds/" ) + soundFile);
   }
   else
   {
@@ -217,7 +217,7 @@ void TopLevel::changeLanguage(const QString &soundFile)
 }
 
 // Play a sound
-void TopLevel::playSound(const QString &ref) const
+void TopLevel::playSound(const QString &ref)
 {
   soundFactory->playSound(ref);
 }
diff --git a/toplevel.h b/toplevel.h
index d8b3d96..662baea 100644
--- a/toplevel.h
+++ b/toplevel.h
@@ -14,11 +14,13 @@
 #include <kxmlguiwindow.h>
 #include <kcombobox.h>
 
+#include "soundfactory.h"
+#include "playground.h"
+
 class QActionGroup;
 class PlayGround;
-class SoundFactory;
 
-class TopLevel : public KXmlGuiWindow
+class TopLevel : public KXmlGuiWindow, public SoundFactoryCallbacks, public PlayGroundCallbacks
 {
   Q_OBJECT
 
@@ -28,12 +30,12 @@ public:
   ~TopLevel();
 
   void open(const QUrl &url);
-  void registerGameboard(const QString& menuText, const QString& boardFile, const QPixmap& pixmap);
-  void registerLanguage(const QString &code, const QString &soundFile, bool enabled);
+  void registerGameboard(const QString& menuText, const QString& boardFile, const QPixmap& pixmap) override;
+  void registerLanguage(const QString &code, const QString &soundFile, bool enabled) override;
   void changeLanguage(const QString &langCode);
-  void playSound(const QString &ref) const;
+  void playSound(const QString &ref) override;
 
-  bool isSoundEnabled() const;
+  bool isSoundEnabled() const override;
 
   void changeGameboard(const QString &gameboard);
 


More information about the kde-doc-english mailing list