diff --git a/cmake/box2d.cmake b/cmake/box2d.cmake index ef50c6bb9..1179f9044 100644 --- a/cmake/box2d.cmake +++ b/cmake/box2d.cmake @@ -1,87 +1,83 @@ set(QML_BOX2D_MODULE "auto" CACHE STRING "Policy for qml-box2d module [auto|submodule|system|disabled]") -if (${QML_BOX2D_MODULE} STREQUAL "disabled") - # disable all activities depending on qml-box2d - set(_disabled_activities "balancebox,land_safe,submarine") - message(STATUS "Disabling qml-box2d module and depending activities: ${_disabled_activities}") -else() +if (NOT ${QML_BOX2D_MODULE} STREQUAL "disabled") include(qt_helper) getQtQmlPath(_qt_qml_system_path) set (_box2d_system_dir "${_qt_qml_system_path}/Box2D.2.0") if (${QML_BOX2D_MODULE} STREQUAL "submodule") message(STATUS "Building qml-box2d module from submodule") set(_need_box2d_submodule "TRUE") else() # try to find module in system scope find_library(QML_BOX2D_LIBRARY NAMES Box2D libBox2D PATHS ${_box2d_system_dir} NO_DEFAULT_PATH) if (QML_BOX2D_LIBRARY) message(STATUS "Using system qml-box2d plugin at ${QML_BOX2D_LIBRARY}") # for packaging builds, copy the module manually to the correct location if(SAILFISHOS) file(COPY ${_box2d_system_dir}/qmldir ${QML_BOX2D_LIBRARY} DESTINATION share/harbour-gcompris-qt/lib/qml/Box2D.2.0) elseif(ANDROID) file(COPY ${_box2d_system_dir}/qmldir ${QML_BOX2D_LIBRARY} DESTINATION lib/qml/Box2D.2.0) endif() # FIXME: add others as needed else() if (${QML_BOX2D_MODULE} STREQUAL "auto") message(STATUS "Did not find the qml-box2d module in system scope, falling back to submodule build ...") set (_need_box2d_submodule "TRUE") else() message(FATAL_ERROR "Did not find the qml-box2d module in system scope and submodule build was not requested. Can't continue!") endif() endif() endif() if (_need_box2d_submodule) # build qml-box2d ourselves from submodule include(ExternalProject) get_property(_qmake_program TARGET ${Qt5Core_QMAKE_EXECUTABLE} PROPERTY IMPORT_LOCATION) set (_box2d_source_dir ${CMAKE_CURRENT_SOURCE_DIR}/external/qml-box2d) if(WIN32) set (_box2d_library_dir "release/") set (_box2d_library_file "Box2D.dll") elseif(CMAKE_HOST_APPLE) set (_box2d_library_dir "") set (_box2d_library_file "libBox2D.dylib") else() set (_box2d_library_dir "") set (_box2d_library_file "libBox2D.so") endif() set (_box2d_install_dir ${CMAKE_CURRENT_BINARY_DIR}/lib/qml/Box2D.2.0) # make sure submodule is up2date find_package(Git) if(GIT_FOUND) execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) endif() # for visual studio, we need to create a vcxproj if(WIN32 AND NOT MINGW) set(_qmake_options -spec win32-msvc -tp vc) else() set(_qmake_options "") endif() ExternalProject_Add(qml_box2d DOWNLOAD_COMMAND "" SOURCE_DIR ${_box2d_source_dir} CONFIGURE_COMMAND ${_qmake_program} ${_qmake_options} ${_box2d_source_dir}/box2d.pro BUILD_COMMAND ${CMAKE_MAKE_PROGRAM} INSTALL_DIR ${_box2d_install_dir} INSTALL_COMMAND cp ${_box2d_library_dir}${_box2d_library_file} ${_box2d_source_dir}/qmldir ${_box2d_install_dir} ) add_library(qml-box2d SHARED IMPORTED) set_target_properties(qml-box2d PROPERTIES IMPORTED_LOCATION ${_box2d_install_dir}/${_box2d_library_file}) if(SAILFISHOS) install(DIRECTORY ${_box2d_install_dir} DESTINATION share/harbour-gcompris-qt/lib/qml) elseif(APPLE) install(DIRECTORY ${_box2d_install_dir} DESTINATION gcompris-qt.app/Contents/lib/qml) else() install(DIRECTORY ${_box2d_install_dir} DESTINATION lib/qml) endif() endif() endif() diff --git a/src/activities/CMakeLists.txt b/src/activities/CMakeLists.txt index d1ca10bc2..721f99710 100644 --- a/src/activities/CMakeLists.txt +++ b/src/activities/CMakeLists.txt @@ -1,34 +1,34 @@ add_subdirectory(menu) # Read the activities.txt file file(READ activities.txt ACTIVITIES) # Split the output on a list containing each line string(REGEX REPLACE ";" "\\\\;" ACTIVITIES "${ACTIVITIES}") string(REGEX REPLACE "\n" ";" ACTIVITIES "${ACTIVITIES}") file(REMOVE "activities_out.txt") add_custom_target(all_activities) foreach(ACTIVITY ${ACTIVITIES}) # For each line found, we remove comments string(FIND "${ACTIVITY}" "#" match) if(${match}) file(STRINGS "${ACTIVITY}/ActivityInfo.qml" demoline REGEX "demo:[ ]+") string(REGEX REPLACE ".*demo:.*(true|false).*" "\\1" demo "${demoline}" ) set(DEFAULT_MODE ON) if((${WITH_DEMO_ONLY}) AND (${demo} STREQUAL "false")) set(DEFAULT_MODE OFF) endif() # Set activities as options (enabled by default) option("USE_${ACTIVITY}" "Enable ${ACTIVITY} activity" ${DEFAULT_MODE}) - if(USE_${ACTIVITY} AND NOT (${_disabled_activities} MATCHES ${ACTIVITY})) + if(USE_${ACTIVITY}) # Add the directory for compilation add_subdirectory(${ACTIVITY}) file(APPEND "activities_out.txt" "${ACTIVITY}\n") add_dependencies(all_activities "rcc_${ACTIVITY}") endif() endif(${match}) endforeach(ACTIVITY ${ACTIVITIES}) GCOMPRIS_ADD_RCC(activities activities_out.txt) diff --git a/src/activities/balancebox/ActivityInfo.qml b/src/activities/balancebox/ActivityInfo.qml index a39a8505e..10a9bafd7 100644 --- a/src/activities/balancebox/ActivityInfo.qml +++ b/src/activities/balancebox/ActivityInfo.qml @@ -1,60 +1,60 @@ /* GCompris - ActivityInfo.qml * * Copyright (C) 2015 Holger Kaelberer * * Authors: * Holger Kaelberer * * 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 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ import GCompris 1.0 ActivityInfo { name: "balancebox/Balancebox.qml" difficulty: 2 icon: "balancebox/balancebox.svg" author: "Holger Kaelberer <holger.k@elberer.de>" demo: true //: Activity title title: qsTr("Balance Box") //: Help title description: qsTr("Navigate the ball to the door by tilting the box.") // intro: "Tilt the box to navigate the ball to the door." //: Help goal goal: qsTr("Practice fine motor skills and basic counting.") //: Help prerequisite prerequisite: "" //: Help manual manual: qsTr("Navigate the ball to the door. Be careful not to make it fall into the holes. Numbered-contact buttons in the box need to be touched in the correct order to unlock the door. You can move the ball by tilting your mobile device. On desktop platforms use the arrow keys to simulate tilting. In the configuration dialog you can choose between the default 'Built-in' level set and one that you can define yourself ('User'). A user-defined level set can be created by choosing the 'user' level set and start the level editor by clicking on the corresponding button. In the level editor you can create your own levels. Choose one of the editing tools on the left side to modify the map cells of the currently active level in the editor: Cross: Clear a map cell completely Horizontal Wall: Set/remove a horizontal wall on the lower edge of a cell Vertical Wall: Set/remove a vertical wall on the right edge of a cell Hole: Set/remove a hole on a cell Ball: Set the starting position of the ball Door: Set the door position Contact: Set/remove a contact button. With the spin-box you can adjust the value of the contact button. It is not possible to set a value more than once on a map. All tools (except the clear-tool) toggle their respective target on the clicked cell: An item can be placed by clicking on an empty cell, and by clicking again on the same cell with the same tool, you can remove it again. You can test a modified level by clicking on the 'Test' button on the right side of the editor view. You can return from testing mode by clicking on the home-button on the bar or by pressing escape on your keyboard or the back-button on your mobile device. In the editor you can change the level currently edited by using the arrow buttons on the bar. Back in the editor you can continue editing the current level and test it again if needed. When your level is finished you can save it to the user level file by clicking on the 'Save' button on the right side. To return to the configuration dialog click on the home-button on the bar or press Escape on your keyboard or the back button on your mobile device.") credit: "" section: "mobile fun" - enabled: !ApplicationInfo.isMobile || ApplicationInfo.sensorIsSupported("QTiltSensor") + enabled: ApplicationInfo.isBox2DInstalled && (!ApplicationInfo.isMobile || ApplicationInfo.sensorIsSupported("QTiltSensor")) createdInVersion: 5000 } diff --git a/src/activities/balancebox/CMakeLists.txt b/src/activities/balancebox/CMakeLists.txt index e11e390e7..a9c22279a 100644 --- a/src/activities/balancebox/CMakeLists.txt +++ b/src/activities/balancebox/CMakeLists.txt @@ -1,3 +1 @@ -if(NOT QML_BOX2D_MODULE STREQUAL disabled) - GCOMPRIS_ADD_RCC(activities/balancebox *.qml *.svg *.js resource/* editor/*) -endif() +GCOMPRIS_ADD_RCC(activities/balancebox *.qml *.svg *.js resource/* editor/*) diff --git a/src/activities/land_safe/ActivityInfo.qml b/src/activities/land_safe/ActivityInfo.qml index 6f06b7a07..79566b2c7 100644 --- a/src/activities/land_safe/ActivityInfo.qml +++ b/src/activities/land_safe/ActivityInfo.qml @@ -1,49 +1,49 @@ /* GCompris - ActivityInfo.qml * * Copyright (C) 2016 Holger Kaelberer * * 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 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ import GCompris 1.0 ActivityInfo { name: "land_safe/LandSafe.qml" difficulty: 4 icon: "land_safe/land_safe.svg" author: "Matilda Bernard <serah4291@gmail.com> (Gtk+), Holger Kaelberer <holger.k@elberer.de> (Qt Quick)" demo: true //: Activity title title: qsTr("Land Safe") //: Help title description: qsTr("Understanding acceleration due to gravity.") // intro: "Use the arrow keys to pilot your spaceship safely onto the landing pad." //: Help goal goal: qsTr("Pilot the spaceship towards the green landing area.") //: Help prerequisite prerequisite: "" //: Help manual manual: qsTr("Acceleration due to gravity experienced by the spaceship is directly proportional to the mass of the planet and inversely proportional to the square of the distance from the center of the planet. Thus, with every planet the acceleration will differ and as the spaceship comes closer and closer to the planet the acceleration increases. Use the up/down keys to control the thrust and the right/left keys to control direction. On touch screens you can control the rocket through the corresponding on-screen buttons. The accelerometer on the right border shows your rocket's overall vertical acceleration including gravitational force. In the upper green area of the accelerometer your acceleration is higher than the gravitational force, in the lower red area it's lower, and on the blue baseline in the yellow middle area the two forces cancel each other out. In higher levels, you can use the right/left keys to rotate the spaceship. By rotating the spaceship you can trigger an acceleration in non-vertical direction using the up/down keys. The landing platform is green if your speed is fine for a safe landing.") credit: "" section: "experiment" - enabled: true //ApplicationInfo.hasShader + enabled: ApplicationInfo.isBox2DInstalled //ApplicationInfo.hasShader createdInVersion: 6000 } diff --git a/src/activities/menu/Menu.qml b/src/activities/menu/Menu.qml index fced46224..83d6ef919 100644 --- a/src/activities/menu/Menu.qml +++ b/src/activities/menu/Menu.qml @@ -1,721 +1,733 @@ /* GCompris - Menu.qml * * Copyright (C) 2014 Bruno Coudoin * * Authors: * Bruno Coudoin (Qt Quick port) * * 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 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ import QtQuick 2.6 import "../../core" import GCompris 1.0 import QtGraphicalEffects 1.0 import "qrc:/gcompris/src/core/core.js" as Core import QtQuick.Controls 1.5 import QtQuick.Controls.Styles 1.4 /** * GCompris' top level menu screen. * * Displays a grid of available activities divided subdivided in activity * categories/sections. * * The visibility of the section row is toggled by the setting * ApplicationSettings.sectionVisible. * * The list of available activities depends on the following settings: * * * ApplicationSettings.showLockedActivities * * ApplicationSettings.filterLevelMin * * ApplicationSettings.filterLevelMax * * @inherit QtQuick.Item */ ActivityBase { id: activity focus: true activityInfo: ActivityInfoTree.rootMenu onBack: { pageView.pop(to); // Restore focus that has been taken by the loaded activity if(pageView.currentItem == activity) focus = true; } onHome: { if(pageView.depth === 1 && !ApplicationSettings.isKioskMode) { Core.quit(main); } else { pageView.pop(); // Restore focus that has been taken by the loaded activity if(pageView.currentItem == activity) focus = true; } } onDisplayDialog: pageView.push(dialog) onDisplayDialogs: { var toPush = new Array(); for (var i = 0; i < dialogs.length; i++) { toPush.push({item: dialogs[i]}); } pageView.push(toPush); } + Connections { + // At the launch of the application, box2d check is performed after we + // first initialize the menu. This connection is to refresh + // automatically the menu at start. + target: ApplicationInfo + onIsBox2DInstalledChanged: { + ActivityInfoTree.filterByTag(activity.currentTag) + ActivityInfoTree.filterLockedActivities() + ActivityInfoTree.filterEnabledActivities() + } + } + // @cond INTERNAL_DOCS property string url: "qrc:/gcompris/src/activities/menu/resource/" property var sections: [ { icon: activity.url + "all.svg", tag: "favorite" }, { icon: activity.url + "computer.svg", tag: "computer" }, { icon: activity.url + "discovery.svg", tag: "discovery" }, { icon: activity.url + "experience.svg", tag: "experiment" }, { icon: activity.url + "fun.svg", tag: "fun" }, { icon: activity.url + "math.svg", tag: "math" }, { icon: activity.url + "puzzle.svg", tag: "puzzle" }, { icon: activity.url + "reading.svg", tag: "reading" }, { icon: activity.url + "strategy.svg", tag: "strategy" }, { icon: activity.url + "search-icon.svg", tag: "search" } ] property string currentTag: sections[0].tag /// @endcond pageComponent: Image { id: background source: activity.url + "background.svg" sourceSize.width: Math.max(parent.width, parent.height) height: main.height fillMode: Image.PreserveAspectCrop Timer { // triggered once at startup to populate the keyboard id: keyboardFiller interval: 1000; running: true; onTriggered: { keyboard.populate(); } } function loadActivity() { // @TODO init of item would be better in setsource but it crashes on Qt5.6 // https://bugreports.qt.io/browse/QTBUG-49793 activityLoader.item.audioVoices = audioVoices activityLoader.item.audioEffects = audioEffects activityLoader.item.loading = loading //take the focus away from textField before starting an activity searchTextField.focus = false pageView.push(activityLoader.item) } Loader { id: activityLoader asynchronous: true onStatusChanged: { if (status == Loader.Loading) { loading.start(); } else if (status == Loader.Ready) { loading.stop(); loadActivity(); } else if (status == Loader.Error) loading.stop(); } } // Filters property bool horizontal: main.width >= main.height property int sectionIconWidth: { if(horizontal) return Math.min(100 * ApplicationInfo.ratio, main.width / (sections.length + 1)) else if(activity.currentTag === "search" && ApplicationSettings.isVirtualKeyboard) return Math.min(100 * ApplicationInfo.ratio, (background.height - (bar.height+keyboard.height)) / (sections.length + 1)) else return Math.min(100 * ApplicationInfo.ratio, (background.height - bar.height) / (sections.length + 1)) } property int sectionIconHeight: sectionIconWidth property int sectionCellWidth: sectionIconWidth * 1.1 property int sectionCellHeight: sectionIconHeight * 1.1 property var currentActiveGrid: activitiesGrid property bool keyboardMode: false Keys.onPressed: { // Ctrl-modifiers should never be handled by the search-field if (event.modifiers === Qt.ControlModifier) { if (event.key === Qt.Key_S) { // Ctrl+S toggle show / hide section ApplicationSettings.sectionVisible = !ApplicationSettings.sectionVisible } } else if(currentTag === "search") { // forward to the virtual keyboard the pressed keys if(event.key == Qt.Key_Backspace) keyboard.keypress(keyboard.backspace); else keyboard.keypress(event.text); } else if(event.key === Qt.Key_Space && currentActiveGrid.currentItem) { currentActiveGrid.currentItem.selectCurrentItem() } } Keys.onReleased: { keyboardMode = true event.accepted = false } Keys.onTabPressed: currentActiveGrid = ((currentActiveGrid == activitiesGrid) ? section : activitiesGrid); Keys.onEnterPressed: if(currentActiveGrid.currentItem) currentActiveGrid.currentItem.selectCurrentItem(); Keys.onReturnPressed: if(currentActiveGrid.currentItem) currentActiveGrid.currentItem.selectCurrentItem(); Keys.onRightPressed: if(currentActiveGrid.currentItem) currentActiveGrid.moveCurrentIndexRight(); Keys.onLeftPressed: if(currentActiveGrid.currentItem) currentActiveGrid.moveCurrentIndexLeft(); Keys.onDownPressed: if(currentActiveGrid.currentItem && !currentActiveGrid.atYEnd) currentActiveGrid.moveCurrentIndexDown(); Keys.onUpPressed: if(currentActiveGrid.currentItem && !currentActiveGrid.atYBeginning) currentActiveGrid.moveCurrentIndexUp(); GridView { id: section model: sections width: horizontal ? main.width : sectionCellWidth height: { if(horizontal) return sectionCellHeight else if(activity.currentTag === "search" && ApplicationSettings.isVirtualKeyboard) return sectionCellHeight * (sections.length+1) else return main.height - bar.height } x: ApplicationSettings.sectionVisible ? section.initialX : -sectionCellWidth y: ApplicationSettings.sectionVisible ? section.initialY : -sectionCellHeight visible: ApplicationSettings.sectionVisible cellWidth: sectionCellWidth cellHeight: sectionCellHeight interactive: false keyNavigationWraps: true property int initialX: 4 property int initialY: 4 Component { id: sectionDelegate Item { id: backgroundSection width: sectionCellWidth height: sectionCellHeight Image { source: modelData.icon sourceSize.height: sectionIconHeight anchors.margins: 5 anchors.horizontalCenter: parent.horizontalCenter } ParticleSystemStarLoader { id: particles anchors.fill: backgroundSection clip: false } MouseArea { anchors.fill: backgroundSection onClicked: { selectCurrentItem() } } function selectCurrentItem() { section.currentIndex = index activity.currentTag = modelData.tag particles.burst(10) if(modelData.tag === "search") { ActivityInfoTree.filterBySearch(searchTextField.text); } else { ActivityInfoTree.filterByTag(modelData.tag) ActivityInfoTree.filterLockedActivities() ActivityInfoTree.filterEnabledActivities() } } } } delegate: sectionDelegate highlight: Item { width: sectionCellWidth height: sectionCellHeight Rectangle { anchors.fill: parent color: "#5AFFFFFF" } Image { source: "qrc:/gcompris/src/core/resource/button.svg" anchors.fill: parent } Behavior on x { SpringAnimation { spring: 2; damping: 0.2 } } Behavior on y { SpringAnimation { spring: 2; damping: 0.2 } } } } // Activities property int iconWidth: 120 * ApplicationInfo.ratio property int activityCellWidth: horizontal ? background.width / Math.floor(background.width / iconWidth) : (background.width - section.width) / Math.floor((background.width - section.width) / iconWidth) property int activityCellHeight: iconWidth * 1.7 Loader { id: warningOverlay anchors { top: horizontal ? section.bottom : parent.top bottom: parent.bottom left: horizontal ? parent.left : section.right right: parent.right margins: 4 } active: (ActivityInfoTree.menuTree.length === 0) && (currentTag === "favorite") sourceComponent: Item { anchors.fill: parent GCText { id: instructionTxt fontSize: smallSize y: height * 0.2 x: (parent.width - width) / 2 z: 2 width: parent.width * 0.6 horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap font.weight: Font.DemiBold color: 'white' text: qsTr("Put your favorite activities here by selecting the " + "sun at the top right of that activity.") } Rectangle { anchors.fill: instructionTxt anchors.margins: -6 z: 1 opacity: 0.5 radius: 10 border.width: 2 border.color: "black" gradient: Gradient { GradientStop { position: 0.0; color: "#000" } GradientStop { position: 0.9; color: "#666" } GradientStop { position: 1.0; color: "#AAA" } } } } } GridView { id: activitiesGrid anchors { top: { if(activity.currentTag === "search") return searchBar.bottom else return horizontal ? section.bottom : parent.top } bottom: bar.top left: horizontal ? parent.left : section.right margins: 4 } width: background.width cellWidth: activityCellWidth cellHeight: activityCellHeight clip: true model: ActivityInfoTree.menuTree keyNavigationWraps: true property int spacing: 10 delegate: Item { id: delegateItem width: activityCellWidth - activitiesGrid.spacing height: activityCellHeight - activitiesGrid.spacing Rectangle { id: activityBackground width: parent.width height: parent.height anchors.horizontalCenter: parent.horizontalCenter color: "white" opacity: 0.5 } Image { source: "qrc:/gcompris/src/activities/" + icon; anchors.top: activityBackground.top anchors.horizontalCenter: parent.horizontalCenter width: iconWidth - activitiesGrid.spacing height: width sourceSize.width: width fillMode: Image.PreserveAspectFit anchors.margins: 5 Image { source: "qrc:/gcompris/src/core/resource/difficulty" + ActivityInfoTree.menuTree[index].difficulty + ".svg"; anchors.top: parent.top sourceSize.width: iconWidth * 0.15 x: 5 } Image { anchors { horizontalCenter: parent.horizontalCenter top: parent.top rightMargin: 4 } source: demo || !ApplicationSettings.isDemoMode ? "" : activity.url + "lock.svg" sourceSize.width: 30 * ApplicationInfo.ratio } Image { anchors { left: parent.left bottom: parent.bottom } source: ActivityInfoTree.menuTree[index].createdInVersion == ApplicationInfo.GCVersionCode ? activity.url + "new.svg" : "" sourceSize.width: 30 * ApplicationInfo.ratio } GCText { id: title anchors.top: parent.bottom anchors.horizontalCenter: parent.horizontalCenter horizontalAlignment: Text.AlignHCenter width: activityBackground.width fontSizeMode: Text.Fit minimumPointSize: 7 fontSize: regularSize elide: Text.ElideRight maximumLineCount: 2 wrapMode: Text.WordWrap text: ActivityInfoTree.menuTree[index].title } // If we have enough room at the bottom display the description GCText { id: description visible: delegateItem.height - (title.y + title.height) > description.height ? 1 : 0 anchors.top: title.bottom anchors.horizontalCenter: parent.horizontalCenter horizontalAlignment: Text.AlignHCenter width: activityBackground.width fontSizeMode: Text.Fit minimumPointSize: 7 fontSize: regularSize elide: Text.ElideRight maximumLineCount: 3 wrapMode: Text.WordWrap text: ActivityInfoTree.menuTree[index].description } } ParticleSystemStarLoader { id: particles anchors.fill: activityBackground } MouseArea { anchors.fill: activityBackground onClicked: selectCurrentItem() } Image { source: activity.url + (favorite ? "all.svg" : "all_disabled.svg"); anchors { top: parent.top right: parent.right rightMargin: 4 * ApplicationInfo.ratio } sourceSize.width: iconWidth * 0.25 visible: ApplicationSettings.sectionVisible MouseArea { anchors.fill: parent onClicked: favorite = !favorite } } function selectCurrentItem() { if(pageView.busy) return particles.burst(50) ActivityInfoTree.currentActivity = ActivityInfoTree.menuTree[index] activityLoader.setSource("qrc:/gcompris/src/activities/" + ActivityInfoTree.menuTree[index].name, { 'menu': activity, 'activityInfo': ActivityInfoTree.currentActivity }) if (activityLoader.status == Loader.Ready) loadActivity() } } highlight: Rectangle { width: activityCellWidth - activitiesGrid.spacing height: activityCellHeight - activitiesGrid.spacing color: "#AAFFFFFF" border.width: 3 border.color: "black" visible: background.keyboardMode Behavior on x { SpringAnimation { spring: 2; damping: 0.2 } } Behavior on y { SpringAnimation { spring: 2; damping: 0.2 } } } Rectangle { id: activitiesMask visible: false anchors.fill: activitiesGrid gradient: Gradient { GradientStop { position: 0.0; color: "#FFFFFFFF" } GradientStop { position: 0.92; color: "#FFFFFFFF" } GradientStop { position: 0.96; color: "#00FFFFFF"} } } layer.enabled: ApplicationInfo.useOpenGL layer.effect: OpacityMask { id: activitiesOpacity source: activitiesGrid maskSource: activitiesMask anchors.fill: activitiesGrid } } // The scroll buttons GCButtonScroll { visible: !ApplicationInfo.useOpenGL anchors.right: parent.right anchors.rightMargin: 5 * ApplicationInfo.ratio anchors.bottom: activitiesGrid.bottom anchors.bottomMargin: 30 * ApplicationInfo.ratio onUp: activitiesGrid.flick(0, 1127) onDown: activitiesGrid.flick(0, -1127) upVisible: activitiesGrid.visibleArea.yPosition <= 0 ? false : true downVisible: activitiesGrid.visibleArea.yPosition >= 1 ? false : true } Rectangle { id: searchBar width: horizontal ? parent.width/2 : parent.width - (section.width+10) height: searchTextField.height visible: activity.currentTag === "search" anchors { top: horizontal ? section.bottom : parent.top left: horizontal ? undefined : section.right } anchors.topMargin: horizontal ? 0 : 4 anchors.bottomMargin: horizontal ? 0 : 4 anchors.horizontalCenter: horizontal ? parent.horizontalCenter : undefined opacity: 0.5 radius: 10 border.width: 2 border.color: "black" gradient: Gradient { GradientStop { position: 0.3; color: "#000" } GradientStop { position: 0.9; color: "#666" } GradientStop { position: 1.0; color: "#AAA" } } Connections { // On mobile with GCompris' virtual keyboard activated: // Force invisibility of Androids virtual keyboard: target: (ApplicationInfo.isMobile && activity.currentTag === "search" && ApplicationSettings.isVirtualKeyboard) ? Qt.inputMethod : null onVisibleChanged: { if (ApplicationSettings.isVirtualKeyboard && visible) Qt.inputMethod.hide(); } onAnimatingChanged: { // note: seems to be never fired! if (ApplicationSettings.isVirtualKeyboard && Qt.inputMethod.visible) Qt.inputMethod.hide(); } } Connections { target: activity onCurrentTagChanged: { if (activity.currentTag === 'search') { searchTextField.focus = true; } else activity.focus = true; } } TextField { id: searchTextField width: parent.width textColor: "black" font.pointSize: 16 font.bold: true horizontalAlignment: TextInput.AlignHCenter verticalAlignment: TextInput.AlignVCenter font.family: GCSingletonFontLoader.fontLoader.name inputMethodHints: Qt.ImhNoPredictiveText // Note: we give focus to the textfield also in case // isMobile && !ApplicationSettings.isVirtualKeyboard // in conjunction with auto-hiding the inputMethod to always get // an input-cursor: activeFocusOnPress: true //ApplicationInfo.isMobile ? !ApplicationSettings.isVirtualKeyboard : true Keys.onReturnPressed: { if (ApplicationInfo.isMobile && !ApplicationSettings.isVirtualKeyboard) Qt.inputMethod.hide(); activity.focus = true; } onEditingFinished: { if (ApplicationInfo.isMobile && !ApplicationSettings.isVirtualKeyboard) Qt.inputMethod.hide(); activity.focus = true; } style: TextFieldStyle { placeholderTextColor: "black" } placeholderText: qsTr("Search specific activities") onTextChanged: ActivityInfoTree.filterBySearch(searchTextField.text); } } VirtualKeyboard { id: keyboard readonly property var letter: ActivityInfoTree.characters width: parent.width visible: activity.currentTag === "search" && ApplicationSettings.isVirtualKeyboard anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter onKeypress: { if(text == keyboard.backspace) { searchTextField.text = searchTextField.text.slice(0, -1); } else if(text == keyboard.space) { searchTextField.text = searchTextField.text.concat(" "); } else { searchTextField.text = searchTextField.text.concat(text); } } function populate() { var tmplayout = []; var row = 0; var offset = 0; var cols; while(offset < letter.length-1) { if(letter.length <= 100) { cols = Math.ceil((letter.length-offset) / (3 - row)); } else { cols = background.horizontal ? (Math.ceil((letter.length-offset) / (15 - row))) :(Math.ceil((letter.length-offset) / (22 - row))) if(row == 0) { tmplayout[row] = new Array(); tmplayout[row].push({ label: keyboard.backspace }); tmplayout[row].push({ label: keyboard.space }); row ++; } } tmplayout[row] = new Array(); for (var j = 0; j < cols; j++) tmplayout[row][j] = { label: letter[j+offset] }; offset += j; row ++; } if(letter.length <= 100) { tmplayout[0].push({ label: keyboard.space }); tmplayout[row-1].push({ label: keyboard.backspace }); } keyboard.layout = tmplayout } } Bar { id: bar // No exit button on mobile, UI Guidelines prohibits it content: BarEnumContent { value: help | config | about | (ApplicationInfo.isMobile ? 0 : exit) } anchors.bottom: keyboard.visible ? keyboard.top : parent.bottom onAboutClicked: { searchTextField.focus = false displayDialog(dialogAbout) } onHelpClicked: { searchTextField.focus = false displayDialog(dialogHelp) } onConfigClicked: { searchTextField.focus = false dialogActivityConfig.active = true dialogActivityConfig.loader.item.loadFromConfig() displayDialog(dialogActivityConfig) } } DialogAbout { id: dialogAbout onClose: home() } DialogHelp { id: dialogHelp onClose: home() activityInfo: ActivityInfoTree.rootMenu } DialogActivityConfig { id: dialogActivityConfig currentActivity: activity content: Component { ConfigurationItem { id: configItem width: dialogActivityConfig.width - 50 * ApplicationInfo.ratio } } onSaveData: { dialogActivityConfig.configItem.save(); } onClose: { if(activity.currentTag != "search") { ActivityInfoTree.filterByTag(activity.currentTag) ActivityInfoTree.filterLockedActivities() ActivityInfoTree.filterEnabledActivities() } else ActivityInfoTree.filterBySearch(searchTextField.text); home() } } } } diff --git a/src/activities/submarine/ActivityInfo.qml b/src/activities/submarine/ActivityInfo.qml index 93bbefbb9..fca9f575b 100644 --- a/src/activities/submarine/ActivityInfo.qml +++ b/src/activities/submarine/ActivityInfo.qml @@ -1,59 +1,60 @@ /* GCompris - ActivityInfo.qml * * Copyright (C) 2017 Rudra Nil Basu * * 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 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ import GCompris 1.0 ActivityInfo { name: "submarine/Submarine.qml" difficulty: 5 icon: "submarine/submarine.svg" author: "Rudra Nil Basu <rudra.nil.basu.1996@gmail.com>" demo: true //: Activity title title: qsTr("Pilot a Submarine") //: Help title description: qsTr("Drive the submarine to the end point.") //intro: "Drive the submarine to the right end of the screen without colliding with any objects" //: Help goal goal: qsTr("Learn how to control a submarine") //: Help prerequisite prerequisite: qsTr("Move and click using the mouse, physics basics") //: Help manual manual: qsTr("Control the various parts of the submarine (the engine, rudders and ballast tanks) to reach the end point. Controls: Engine: D / Right arrow: Increase velocity A / Left arrow: Decrease velocity Ballast tanks: Switch filling of Ballast tanks: W / Up arrow: Central ballast tank R: Left ballast tank T: Right ballast tanks Switch flush ballast tanks: S / Down arrow: Central ballast tank F: Left ballast tank G: Right ballast tanks Diving planes: + : Increase diving plane angle - : Decrease diving plane angle") credit: "" section: "experimental" + enabled: ApplicationInfo.isBox2DInstalled createdInVersion: 9000 } diff --git a/src/core/ApplicationInfo.cpp b/src/core/ApplicationInfo.cpp index 47d211b2f..d1ca1052d 100644 --- a/src/core/ApplicationInfo.cpp +++ b/src/core/ApplicationInfo.cpp @@ -1,259 +1,287 @@ /* GCompris - ApplicationInfo.cpp * * Copyright (C) 2014-2015 Bruno Coudoin * * Authors: * Bruno Coudoin * * This file was originally created from Digia example code under BSD license * and heavily modified since then. * * 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 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ #include "ApplicationInfo.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include QQuickWindow *ApplicationInfo::m_window = nullptr; ApplicationInfo *ApplicationInfo::m_instance = nullptr; ApplicationInfo::ApplicationInfo(QObject *parent): QObject(parent) { #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) || defined(Q_OS_BLACKBERRY) || defined(SAILFISHOS) m_isMobile = true; #else m_isMobile = false; #endif #if defined(Q_OS_ANDROID) // Put android before checking linux/unix as it is also a linux m_platform = Android; #elif defined(Q_OS_MAC) m_platform = MacOSX; #elif (defined(Q_OS_LINUX) || defined(Q_OS_UNIX)) m_platform = Linux; #elif defined(Q_OS_WIN) m_platform = Windows; #elif defined(Q_OS_IOS) m_platform = Ios; #elif defined(Q_OS_BLACKBERRY) m_platform = Blackberry; #elif defined(SAILFISHOS) m_platform = SailfishOS; #else // default is Linux m_platform = Linux; #endif + m_isBox2DInstalled = false; + QRect rect = qApp->primaryScreen()->geometry(); m_ratio = qMin(qMax(rect.width(), rect.height())/800. , qMin(rect.width(), rect.height())/520.); // calculate a factor for font-scaling, cf. // http://doc.qt.io/qt-5/scalability.html#calculating-scaling-ratio qreal refDpi = 216.; qreal refHeight = 1776.; qreal refWidth = 1080.; qreal height = qMax(rect.width(), rect.height()); qreal width = qMin(rect.width(), rect.height()); qreal dpi = qApp->primaryScreen()->logicalDotsPerInch(); m_fontRatio = qMax(qreal(1.0), qMin(height*refDpi/(dpi*refHeight), width*refDpi/(dpi*refWidth))); m_isPortraitMode = m_isMobile ? rect.height() > rect.width() : false; m_applicationWidth = m_isMobile ? rect.width() : 1120; m_useOpenGL = true; if (m_isMobile) connect(qApp->primaryScreen(), &QScreen::physicalSizeChanged, this, &ApplicationInfo::notifyPortraitMode); // @FIXME this does not work on iOS: https://bugreports.qt.io/browse/QTBUG-50624 #if not defined(Q_OS_IOS) // Get all symbol fonts to remove them QFontDatabase database; m_excludedFonts = database.families(QFontDatabase::Symbol); #endif // Get fonts from rcc const QStringList fontFilters = {"*.otf", "*.ttf"}; m_fontsFromRcc = QDir(":/gcompris/src/core/resource/fonts").entryList(fontFilters); } ApplicationInfo::~ApplicationInfo() { m_instance = nullptr; } bool ApplicationInfo::sensorIsSupported(const QString& sensorType) { return QSensor::sensorTypes().contains(sensorType.toUtf8()); } Qt::ScreenOrientation ApplicationInfo::getNativeOrientation() { return QGuiApplication::primaryScreen()->nativeOrientation(); } void ApplicationInfo::setApplicationWidth(const int newWidth) { if (newWidth != m_applicationWidth) { m_applicationWidth = newWidth; emit applicationWidthChanged(); } } QString ApplicationInfo::getResourceDataPath() { return QString("qrc:/gcompris/data"); } QString ApplicationInfo::getFilePath(const QString &file) { #if defined(Q_OS_ANDROID) return QString("assets:/share/GCompris/rcc/%1").arg(file); #elif defined(Q_OS_MACX) return QString("%1/../Resources/rcc/%2").arg(QCoreApplication::applicationDirPath(), file); #elif defined(Q_OS_IOS) return QString("%1/rcc/%2").arg(QCoreApplication::applicationDirPath(), file); #else return QString("%1/%2/rcc/%3").arg(QCoreApplication::applicationDirPath(), GCOMPRIS_DATA_FOLDER, file); #endif } QString ApplicationInfo::getAudioFilePath(const QString &file) { QString localeName = getVoicesLocale(ApplicationSettings::getInstance()->locale()); return getAudioFilePathForLocale(file, localeName); } QString ApplicationInfo::getAudioFilePathForLocale(const QString &file, const QString &localeName) { QString filename = file; filename.replace("$LOCALE", localeName); filename.replace("$CA", CompressedAudio()); if(file.startsWith('/') || file.startsWith(QLatin1String("qrc:")) || file.startsWith(':')) return filename; return getResourceDataPath() + '/' + filename; } QString ApplicationInfo::getLocaleFilePath(const QString &file) { QString localeShortName = localeShort(); QString filename = file; filename.replace("$LOCALE", localeShortName); return filename; } QStringList ApplicationInfo::getSystemExcludedFonts() { return m_excludedFonts; } QStringList ApplicationInfo::getFontsFromRcc() { return m_fontsFromRcc; } void ApplicationInfo::notifyPortraitMode() { int width = qApp->primaryScreen()->geometry().width(); int height = qApp->primaryScreen()->geometry().height(); setIsPortraitMode(height > width); } void ApplicationInfo::setIsPortraitMode(const bool newMode) { if (m_isPortraitMode != newMode) { m_isPortraitMode = newMode; emit portraitModeChanged(); } } void ApplicationInfo::setWindow(QQuickWindow *window) { m_window = window; } void ApplicationInfo::screenshot(const QString &file) { QImage img = m_window->grabWindow(); img.save(file); } void ApplicationInfo::notifyFullscreenChanged() { if(ApplicationSettings::getInstance()->isFullscreen()) m_window->showFullScreen(); else m_window->showNormal(); } +// Would be better to create a component importing Box2D 2.0 using QQmlContext and test if it exists but it does not work. +void ApplicationInfo::setBox2DInstalled(const QQmlEngine &engine) { + /* + QQmlContext *context = new QQmlContext(engine.rootContext()); + context->setContextObject(&myDataSet); + + QQmlComponent component(&engine); + component.setData("import QtQuick 2.0\nimport Box2D 2.0\nItem { }", QUrl()); + component.create(context); + box2dInstalled = (component != nullptr); + */ + bool box2dInstalled = false; + for(const QString &folder: engine.importPathList()) { + if(QDir(folder).entryList().contains(QStringLiteral("Box2D.2.0"))) { + if(QDir(folder+"/Box2D.2.0").entryList().contains("qmldir")) { + qDebug() << "Found box2d in " << folder; + box2dInstalled = true; + break; + } + } + } + m_isBox2DInstalled = box2dInstalled; + emit isBox2DInstalledChanged(); +} + // return the shortest possible locale name for the given locale, describing // a unique voices dataset QString ApplicationInfo::getVoicesLocale(const QString &locale) { QString _locale = locale; if(_locale == GC_DEFAULT_LOCALE) { _locale = QLocale::system().name(); if(_locale == "C") _locale = "en_US"; } // locales we have country-specific voices for: if (_locale.startsWith(QLatin1String("pt_BR")) || _locale.startsWith(QLatin1String("zh_CN")) || _locale.startsWith(QLatin1String("zh_TW"))) return QLocale(_locale).name(); // short locale for all the rest: return localeShort(_locale); } QVariantList ApplicationInfo::localeSort(QVariantList list, const QString& locale) const { std::sort(list.begin(), list.end(), [&locale,this](const QVariant& a, const QVariant& b) { return (localeCompare(a.toString(), b.toString(), locale) < 0); }); return list; } QObject *ApplicationInfo::applicationInfoProvider(QQmlEngine *engine, QJSEngine *scriptEngine) { Q_UNUSED(engine) Q_UNUSED(scriptEngine) /* * Connect the fullscreen change signal to applicationInfo in order to change * the QQuickWindow value */ ApplicationInfo* appInfo = getInstance(); connect(ApplicationSettings::getInstance(), &ApplicationSettings::fullscreenChanged, appInfo, &ApplicationInfo::notifyFullscreenChanged); + return appInfo; } diff --git a/src/core/ApplicationInfo.h b/src/core/ApplicationInfo.h index 630dd7808..8c9adbac0 100644 --- a/src/core/ApplicationInfo.h +++ b/src/core/ApplicationInfo.h @@ -1,429 +1,441 @@ /* GCompris - ApplicationInfo.h * * Copyright (C) 2014-2015 Bruno Coudoin * * Authors: * Bruno Coudoin * * This file was originally created from Digia example code under BSD license * and heavily modified since then. * * 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 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ #ifndef APPLICATIONINFO_H #define APPLICATIONINFO_H #include #include "ApplicationSettings.h" #include #include #include class QQuickWindow; /** * @class ApplicationInfo * @short A general purpose singleton that exposes miscellaneous native * functions to the QML layer. * @ingroup infrastructure */ class ApplicationInfo : public QObject { Q_OBJECT /** * Width of the application viewport. */ - Q_PROPERTY(int applicationWidth READ applicationWidth WRITE setApplicationWidth NOTIFY applicationWidthChanged) + Q_PROPERTY(int applicationWidth READ applicationWidth WRITE setApplicationWidth NOTIFY applicationWidthChanged) /** * Platform the application is currently running on. */ - Q_PROPERTY(Platform platform READ platform CONSTANT) + Q_PROPERTY(Platform platform READ platform CONSTANT) /** * Whether the application is currently running on a mobile platform. * * Mobile platforms are Android, Ios (not supported yet), * Blackberry (not supported) */ - Q_PROPERTY(bool isMobile READ isMobile CONSTANT) + Q_PROPERTY(bool isMobile READ isMobile CONSTANT) /** * Whether the current platform supports fragment shaders. * * This flag is used in some core modules to selectively deactivate * particle effects which cause crashes on some Android devices. * * cf. https://bugreports.qt.io/browse/QTBUG-44194 * * For now always set to false, to prevent crashes. */ - Q_PROPERTY(bool hasShader READ hasShader CONSTANT) + Q_PROPERTY(bool hasShader READ hasShader CONSTANT) /** * Whether currently in portrait mode, on mobile platforms. * * Based on viewport geometry. */ - Q_PROPERTY(bool isPortraitMode READ isPortraitMode WRITE setIsPortraitMode NOTIFY portraitModeChanged) + Q_PROPERTY(bool isPortraitMode READ isPortraitMode WRITE setIsPortraitMode NOTIFY portraitModeChanged) /** * Ratio factor used for scaling of sizes on high-dpi devices. * * Must be used by activities as a scaling factor to all pixel values. */ - Q_PROPERTY(qreal ratio READ ratio NOTIFY ratioChanged) + Q_PROPERTY(qreal ratio READ ratio NOTIFY ratioChanged) /** * Ratio factor used for font scaling. * * On some low-dpi Android devices with high res (e.g. Galaxy Tab 4) the * fonts in Text-like elements appear too small with respect to the other * graphics -- also if we are using font.pointSize. * * For these cases we calculate a fontRatio in ApplicationInfo that takes * dpi information into account, as proposed on * http://doc.qt.io/qt-5/scalability.html#calculating-scaling-ratio * * GCText applies this factor automatically on its new fontSize property. */ - Q_PROPERTY(qreal fontRatio READ fontRatio NOTIFY fontRatioChanged) + Q_PROPERTY(qreal fontRatio READ fontRatio NOTIFY fontRatioChanged) - /** - * Short (2-letter) locale string of the currently active language. - */ - Q_PROPERTY(QString localeShort READ localeShort CONSTANT) + /** + * Short (2-letter) locale string of the currently active language. + */ + Q_PROPERTY(QString localeShort READ localeShort CONSTANT) /** * GCompris version string (compile time). */ - Q_PROPERTY(QString GCVersion READ GCVersion CONSTANT) + Q_PROPERTY(QString GCVersion READ GCVersion CONSTANT) /** * GCompris version code (compile time). */ - Q_PROPERTY(int GCVersionCode READ GCVersionCode CONSTANT) + Q_PROPERTY(int GCVersionCode READ GCVersionCode CONSTANT) /** * Qt version string (runtime). */ - Q_PROPERTY(QString QTVersion READ QTVersion CONSTANT) + Q_PROPERTY(QString QTVersion READ QTVersion CONSTANT) /** * Audio codec used for voices resources. * * This is determined at compile time (ogg for free platforms, aac on * MacOSX and IOS). */ Q_PROPERTY(QString CompressedAudio READ CompressedAudio CONSTANT) /** * Download allowed * * This is determined at compile time. If set to NO GCompris will * never download anything. */ Q_PROPERTY(bool isDownloadAllowed READ isDownloadAllowed CONSTANT) /** * Whether the application is currently using OpenGL or not. * * Use to deactivate some effects if OpenGL not used. */ Q_PROPERTY(bool useOpenGL READ useOpenGL WRITE setUseOpenGL NOTIFY useOpenGLChanged) + + /** + * Whether Box2D is installed or not. + * + * Use to disable activities that use Box2D when it's not installed. + */ + Q_PROPERTY(bool isBox2DInstalled READ isBox2DInstalled NOTIFY isBox2DInstalledChanged) public: /** * Known host platforms. */ enum Platform { Linux, /**< Linux (except Android) */ Windows, /**< Windows */ MacOSX, /**< MacOSX */ Android, /**< Android */ Ios, /**< IOS (not supported) */ Blackberry, /**< Blackberry (not supported) */ SailfishOS /**< SailfishOS */ }; Q_ENUM(Platform) /** * Returns an absolute and platform independent path to the passed @p file * * @param file A relative filename. * @returns Absolute path to the file. */ static QString getFilePath(const QString &file); /** * Returns the short locale name for the passed @p locale. * * Handles also 'system' (GC_DEFAULT_LOCALE) correctly which resolves to * QLocale::system().name(). * * @param locale A locale string of the form \_\ * @returns A short locale string of the form \ */ static QString localeShort(const QString &locale) { QString _locale = locale; if(_locale == GC_DEFAULT_LOCALE) { _locale = QLocale::system().name(); } // Can't use left(2) because of Asturian where there are 3 chars return _locale.left(_locale.indexOf('_')); } /** * Returns a locale string that can be used in voices filenames. * * @param locale A locale string of the form \_\ * @returns A locale string as used in voices filenames. */ Q_INVOKABLE QString getVoicesLocale(const QString &locale); /** * Request GCompris to take the Audio Focus at the system level. * * On systems that support it, it will mute a running audio player. */ Q_INVOKABLE bool requestAudioFocus() const; /** * Abandon the Audio Focus. * * On systems that support it, it will let an audio player start again. */ Q_INVOKABLE void abandonAudioFocus() const; /** * Compare two strings respecting locale specific sort order. * * @param a First string to compare * @param b Second string to compare * @param locale Locale to respect for comparison in any of the forms * used in GCompris xx[_XX][.codeset]. Defaults to currently * set language from global configuration. * @returns -1, 0 or 1 if a is less than, equal to or greater than b */ Q_INVOKABLE int localeCompare(const QString& a, const QString& b, const QString& locale = "") const; /** * Sort a list of strings respecting locale specific sort order. * * This function is supposed to be called from QML/JS. As there are still * problems marshalling QStringList from C++ to QML/JS we use QVariantList * both for argument and return value. * * @param list List of strings to be sorted. * @param locale Locale to respect for sorting in any of the forms * used in GCompris xx[_XX][.codeset]. * @returns List sorted by the sort order of the passed locale. */ Q_INVOKABLE QVariantList localeSort(QVariantList list, const QString& locale = "") const; /// @cond INTERNAL_DOCS static ApplicationInfo *getInstance() { if(!m_instance) { m_instance = new ApplicationInfo(); } return m_instance; } static QObject *applicationInfoProvider(QQmlEngine *engine, QJSEngine *scriptEngine); static void setWindow(QQuickWindow *window); explicit ApplicationInfo(QObject *parent = 0); ~ApplicationInfo(); int applicationWidth() const { return m_applicationWidth; } void setApplicationWidth(const int newWidth); Platform platform() const { return m_platform; } bool isPortraitMode() const { return m_isPortraitMode; } void setIsPortraitMode(const bool newMode); bool isMobile() const { return m_isMobile; } bool hasShader() const { #if defined(Q_OS_ANDROID) return false; #else return true; #endif } qreal ratio() const { return m_ratio; } qreal fontRatio() const { return m_fontRatio; } QString localeShort() const { return localeShort( ApplicationSettings::getInstance()->locale() ); } static QString GCVersion() { return VERSION; } static int GCVersionCode() { return VERSION_CODE; } static QString QTVersion() { return qVersion(); } static QString CompressedAudio() { return COMPRESSED_AUDIO; } static bool isDownloadAllowed() { return QString(DOWNLOAD_ALLOWED) == "ON"; } bool useOpenGL() const { return m_useOpenGL; } void setUseOpenGL(bool useOpenGL) { m_useOpenGL = useOpenGL; } + bool isBox2DInstalled() const { return m_isBox2DInstalled; } + void setBox2DInstalled(const QQmlEngine &engine); + /** * Returns the native screen orientation. * * Wraps QScreen::nativeOrientation: The native orientation of the screen * is the orientation where the logo sticker of the device appears the * right way up, or Qt::PrimaryOrientation if the platform does not support * this functionality. * * The native orientation is a property of the hardware, and does not change */ Q_INVOKABLE Qt::ScreenOrientation getNativeOrientation(); /** * Change the desired orientation of the application. * * Android specific function, cf. http://developer.android.com/reference/android/app/Activity.html#setRequestedOrientation(int) * * @param orientation Desired orientation of the application. For possible * values cf. http://developer.android.com/reference/android/content/pm/ActivityInfo.html#screenOrientation . * Some useful values: * - -1: SCREEN_ORIENTATION_UNSPECIFIED * - 0: SCREEN_ORIENTATION_LANDSCAPE: forces landscape * - 1: SCREEN_ORIENTATION_PORTRAIT: forces portrait * - 5: SCREEN_ORIENTATION_NOSENSOR: forces native * orientation mode on each device (portrait on * smartphones, landscape on tablet) * - 14: SCREEN_ORIENTATION_LOCKED: lock current orientation */ Q_INVOKABLE void setRequestedOrientation(int orientation); /** * Query the desired orientation of the application. * * @sa setRequestedOrientation */ Q_INVOKABLE int getRequestedOrientation(); /** * Checks whether a sensor type from the QtSensor module is supported on * the current platform. * * @param sensorType Classname of a sensor from the QtSensor module * to be checked (e.g. "QTiltSensor"). */ Q_INVOKABLE bool sensorIsSupported(const QString& sensorType); /** * Toggles activation of screensaver on android * * @param value Whether screensaver should be disabled (true) or * enabled (false). */ Q_INVOKABLE void setKeepScreenOn(bool value); /// @endcond public slots: - /** - * Returns the resource root-path used for GCompris resources. - */ - QString getResourceDataPath(); + /** + * Returns the resource root-path used for GCompris resources. + */ + QString getResourceDataPath(); /** * Returns an absolute path to a language specific sound/voices file. If * the file is already absolute only the token replacement is applied. * * @param file A templated relative path to a language specific file. Any * occurrence of the '$LOCALE' placeholder will be replaced by * the currently active locale string. * Any occurrence of '$CA' placeholder will be replaced by * the current compressed audio format ('ogg' or 'aac). * Example: 'voices-$CA/$LOCALE/misc/click_on_letter.$CA' * @returns An absolute path to the corresponding resource file. */ Q_INVOKABLE QString getAudioFilePath(const QString &file); /** * Returns an absolute path to a language specific sound/voices file. If * the file is already absolute only the token replacement is applied. * * @param file A templated relative path to a language specific file. Any * occurrence of the '$LOCALE' placeholder will be replaced by * the currently active locale string. * Any occurrence of '$CA' placeholder will be replaced by * the current compressed audio format ('ogg' or 'aac). * Example: 'voices-$CA/$LOCALE/misc/click_on_letter.$CA' * @param localeName the locale for which to get this audio file * @returns An absolute path to the corresponding resource file. */ Q_INVOKABLE QString getAudioFilePathForLocale(const QString &file, const QString &localeName); /** * Returns an absolute path to a language specific resource file. * * Generalization of getAudioFilePath(). * @sa getAudioFilePath */ Q_INVOKABLE QString getLocaleFilePath(const QString &file); /** * @returns A list of systems-fonts that should be excluded from font * selection. */ Q_INVOKABLE QStringList getSystemExcludedFonts(); /** * @returns A list of fonts contained in the fonts resources. */ Q_INVOKABLE QStringList getFontsFromRcc(); /** * Stores a screenshot in the passed @p file. * * @param file Absolute destination filename. */ Q_INVOKABLE void screenshot(const QString &file); void notifyPortraitMode(); Q_INVOKABLE void notifyFullscreenChanged(); protected: qreal getSizeWithRatio(const qreal height) { return ratio() * height; } signals: void applicationWidthChanged(); void portraitModeChanged(); void ratioChanged(); void fontRatioChanged(); void applicationSettingsChanged(); void fullscreenChanged(); void useOpenGLChanged(); + void isBox2DInstalledChanged(); private: static ApplicationInfo *m_instance; int m_applicationWidth; Platform m_platform; bool m_isPortraitMode; bool m_isMobile; bool m_useOpenGL; + bool m_isBox2DInstalled; qreal m_ratio; qreal m_fontRatio; // Symbols fonts that user can't see QStringList m_excludedFonts; QStringList m_fontsFromRcc; static QQuickWindow *m_window; }; #endif // APPLICATIONINFO_H diff --git a/src/core/main.cpp b/src/core/main.cpp index 2c8071722..7f2824d6e 100644 --- a/src/core/main.cpp +++ b/src/core/main.cpp @@ -1,293 +1,294 @@ /* GCompris - main.cpp * * Copyright (C) 2014 Bruno Coudoin * * Authors: * Bruno Coudoin * * 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 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include "GComprisPlugin.h" #include "ApplicationInfo.h" #include "ActivityInfoTree.h" #include "DownloadManager.h" bool loadAndroidTranslation(QTranslator &translator, const QString &locale) { QFile file("assets:/share/GCompris/gcompris_" + locale + ".qm"); file.open(QIODevice::ReadOnly); QDataStream in(&file); uchar *data = (uchar*)malloc(file.size()); if(!file.exists()) qDebug() << "file assets:/share/GCompris/gcompris_" << locale << ".qm does not exist"; in.readRawData((char*)data, file.size()); if(!translator.load(data, file.size())) { qDebug() << "Unable to load translation for locale " << locale << ", use en_US by default"; free(data); return false; } // Do not free data, it is still needed by translator return true; } // Return the locale QString loadTranslation(QSettings &config, QTranslator &translator) { QString locale; // Get locale if(config.contains("General/locale")) { locale = config.value("General/locale").toString(); } else { locale = GC_DEFAULT_LOCALE; } if(locale == GC_DEFAULT_LOCALE) locale = QString(QLocale::system().name() + ".UTF-8"); if(locale == "C.UTF-8" || locale == "en_US.UTF-8") return "en_US"; // Load translation // Remove .UTF8 locale.remove(".UTF-8"); #if defined(Q_OS_ANDROID) if(!loadAndroidTranslation(translator, locale)) loadAndroidTranslation(translator, ApplicationInfo::localeShort(locale)); #else #if (defined(Q_OS_LINUX) || defined(Q_OS_UNIX)) // only useful for translators: load from $application_dir/../share/... if exists as it is where kde scripts install translations if(translator.load("gcompris_qt.qm", QString("%1/../share/locale/%2/LC_MESSAGES").arg(QCoreApplication::applicationDirPath(), locale))) { qDebug() << "load translation for locale " << locale << " in " << QString("%1/../share/locale/%2/LC_MESSAGES").arg(QCoreApplication::applicationDirPath(), locale); } else if(translator.load("gcompris_qt.qm", QString("%1/../share/locale/%2/LC_MESSAGES").arg(QCoreApplication::applicationDirPath(), locale.split('_')[0]))) { qDebug() << "load translation for locale " << locale << " in " << QString("%1/../share/locale/%2/LC_MESSAGES").arg(QCoreApplication::applicationDirPath(), locale.split('_')[0]); } else #endif if(!translator.load("gcompris_" + locale, QString("%1/%2/translations").arg(QCoreApplication::applicationDirPath(), GCOMPRIS_DATA_FOLDER))) { qDebug() << "Unable to load translation for locale " << locale << ", use en_US by default"; } #endif return locale; } int main(int argc, char *argv[]) { // Disable it because we already support HDPI display natively qunsetenv("QT_DEVICE_PIXEL_RATIO"); QApplication app(argc, argv); app.setOrganizationName("KDE"); app.setApplicationName(GCOMPRIS_APPLICATION_NAME); app.setOrganizationDomain("kde.org"); app.setApplicationVersion(ApplicationInfo::GCVersion()); #if defined(Q_OS_MAC) // Sandboxing on MacOSX as documented in: // http://doc.qt.io/qt-5/osx-deployment.html QDir dir(QGuiApplication::applicationDirPath()); dir.cdUp(); dir.cd("Plugins"); QGuiApplication::setLibraryPaths(QStringList(dir.absolutePath())); #endif // Local scope for config QSettings config(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/gcompris/" + GCOMPRIS_APPLICATION_NAME + ".conf", QSettings::IniFormat); // Load translations QTranslator translator; loadTranslation(config, translator); // Apply translation app.installTranslator(&translator); QCommandLineParser parser; parser.setApplicationDescription("GCompris is an educational software for children 2 to 10"); parser.addHelpOption(); parser.addVersionOption(); QCommandLineOption exportActivitiesAsSQL("export-activities-as-sql", "Export activities as SQL"); parser.addOption(exportActivitiesAsSQL); QCommandLineOption clDefaultCursor(QStringList() << "c" << "cursor", QObject::tr("Run GCompris with the default system cursor.")); parser.addOption(clDefaultCursor); QCommandLineOption clNoCursor(QStringList() << "C" << "nocursor", QObject::tr("Run GCompris without cursor (touch screen mode).")); parser.addOption(clNoCursor); QCommandLineOption clFullscreen(QStringList() << "f" << "fullscreen", QObject::tr("Run GCompris in fullscreen mode.")); parser.addOption(clFullscreen); QCommandLineOption clWindow(QStringList() << "w" << "window", QObject::tr("Run GCompris in window mode.")); parser.addOption(clWindow); QCommandLineOption clSound(QStringList() << "s" << "sound", QObject::tr("Run GCompris with sound enabled.")); parser.addOption(clSound); QCommandLineOption clMute(QStringList() << "m" << "mute", QObject::tr("Run GCompris without sound.")); parser.addOption(clMute); QCommandLineOption clWithoutKioskMode(QStringList() << "disable-kioskmode", QObject::tr("Disable the kiosk mode (default).")); parser.addOption(clWithoutKioskMode); QCommandLineOption clWithKioskMode(QStringList() << "enable-kioskmode", QObject::tr("Enable the kiosk mode.")); parser.addOption(clWithKioskMode); QCommandLineOption clSoftwareRenderer(QStringList() << "software-renderer", QObject::tr("Use software renderer instead of openGL (slower but should run with any graphical card, needs Qt 5.8 minimum).")); parser.addOption(clSoftwareRenderer); QCommandLineOption clOpenGLRenderer(QStringList() << "opengl-renderer", QObject::tr("Use openGL renderer instead of software (faster but crash potentially depending on your graphical card).")); parser.addOption(clOpenGLRenderer); parser.process(app); GComprisPlugin plugin; plugin.registerTypes("GCompris"); ActivityInfoTree::registerResources(); // Tell media players to stop playing, it's GCompris time ApplicationInfo::getInstance()->requestAudioFocus(); // Must be done after ApplicationSettings is constructed because we get an // async callback from the payment system ApplicationSettings::getInstance()->checkPayment(); // Getting fullscreen mode from config if exist, else true is default value bool isFullscreen = true; { if(config.contains("General/fullscreen")) { isFullscreen = config.value("General/fullscreen").toBool(); } // Set the cursor image bool defaultCursor = false; if(config.contains("General/defaultCursor")) { defaultCursor = config.value("General/defaultCursor").toBool(); } if(!defaultCursor && !parser.isSet(clDefaultCursor)) QGuiApplication::setOverrideCursor( QCursor(QPixmap(":/gcompris/src/core/resource/cursor.svg"), 0, 0)); // Hide the cursor bool noCursor = false; if(config.contains("General/noCursor")) { noCursor = config.value("General/noCursor").toBool(); } if(noCursor || parser.isSet(clNoCursor)) QGuiApplication::setOverrideCursor(QCursor(Qt::BlankCursor)); } // Update execution counter ApplicationSettings::getInstance()->setExeCount(ApplicationSettings::getInstance()->exeCount() + 1); if(parser.isSet(clFullscreen)) { isFullscreen = true; } if(parser.isSet(clWindow)) { isFullscreen = false; } if(parser.isSet(clMute)) { ApplicationSettings::getInstance()->setIsAudioEffectsEnabled(false); ApplicationSettings::getInstance()->setIsAudioVoicesEnabled(false); } if(parser.isSet(clSound)) { ApplicationSettings::getInstance()->setIsAudioEffectsEnabled(true); ApplicationSettings::getInstance()->setIsAudioVoicesEnabled(true); } if(parser.isSet(clWithoutKioskMode)) { ApplicationSettings::getInstance()->setKioskMode(false); } if(parser.isSet(clWithKioskMode)) { ApplicationSettings::getInstance()->setKioskMode(true); } if(parser.isSet(clSoftwareRenderer)) { ApplicationSettings::getInstance()->setRenderer(QStringLiteral("software")); } if(parser.isSet(clOpenGLRenderer)) { ApplicationSettings::getInstance()->setRenderer(QStringLiteral("opengl")); } // Set the renderer used const QString &renderer = ApplicationSettings::getInstance()->renderer(); ApplicationInfo::getInstance()->setUseOpenGL(renderer != QLatin1String("software")); #if QT_VERSION >= QT_VERSION_CHECK(5, 8, 0) if(renderer == QLatin1String("software")) QQuickWindow::setSceneGraphBackend(QSGRendererInterface::Software); else if(renderer == QLatin1String("opengl")) QQuickWindow::setSceneGraphBackend(QSGRendererInterface::OpenGL); #endif QQmlApplicationEngine engine(QUrl("qrc:/gcompris/src/core/main.qml")); QObject::connect(&engine, &QQmlApplicationEngine::quit, DownloadManager::getInstance(), &DownloadManager::shutdown); // add import path for shipped qml modules: #ifdef SAILFISHOS engine.addImportPath(QStringLiteral("%1/../share/%2/lib/qml") .arg(QCoreApplication::applicationDirPath()).arg(GCOMPRIS_APPLICATION_NAME)); #else engine.addImportPath(QStringLiteral("%1/../lib/qml") .arg(QCoreApplication::applicationDirPath())); #endif + ApplicationInfo::getInstance()->setBox2DInstalled(engine); + if(parser.isSet(exportActivitiesAsSQL)) { ActivityInfoTree *menuTree(qobject_cast(ActivityInfoTree::menuTreeProvider(&engine, nullptr))); menuTree->exportAsSQL(); exit(0); } QObject *topLevel = engine.rootObjects().value(0); QQuickWindow *window = qobject_cast(topLevel); if (window == nullptr) { qWarning("Error: Your root item has to be a Window."); return -1; } - ApplicationInfo::setWindow(window); window->setIcon(QIcon(QPixmap(QString::fromUtf8(":/gcompris/src/core/resource/gcompris-icon.png")))); if(isFullscreen) { window->showFullScreen(); } else { window->show(); } return app.exec(); }