Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • alcroito/qt-ui-viewer
  • design-studio/design-viewer/qt-ui-viewer
2 results
Show changes
Commits on Source (13)
Showing
with 472 additions and 194 deletions
Subproject commit c97fc94e0e129feb0c3596371f86d725540322d7
Subproject commit c98555164e3823a5d3a56cad5a04dc386d9f7e7b
# Qt UI Viewer
![Qt UI Viewer](assets/PlayStoreFeatureImage.svg)
## About
Launch [Qt Design Studio](https://www.qt.io/ui-design-tools) projects in your Android device. The viewer that helps you do with [Qt for Android](https://doc.qt.io/qt-6/android.html).
Launch [Qt Design Studio](https://www.qt.io/ui-design-tools) projects in your Android device in seconds. Qt UI Viewer is a simple app that allows you to view your Qt Design Studio projects on your Android device. It's a great tool for designers and developers to preview their designs on a real device.
Qt UI Viewer works with minimum Android 11 (API level 30).
Qt UI Viewer is built with [Qt for Android](https://doc.qt.io/qt-6/android.html).
## Getting the App
......@@ -13,18 +14,18 @@ You can get the pre-built app from either [Google Play Store][google-play-link]
## Prerequisites
* Qt Design Studio 4.7 or newer
* CMake 3.16 or newer
* Qt 6.8.0 or newer
* OpenSSL (<https://github.com/KDAB/android_openssl>)
* Android SDK and NDK (<https://developer.android.com/studio>)
* Android (min API 34) SDK and NDK (<https://developer.android.com/studio>)
## Code Map
* cicd: GitLab pipeline files
* resources: UI related files
* android: Files needed for Android build system
* src: Backend source files
* ui: UI source files
* src: All source files
* 3rdparty: Required 3rd party libraries
* qtquickdesigner-components: QML components
* zxing-cpp: QR code decoding/encoding
......@@ -82,7 +83,24 @@ cmake --build build
## Usage
Upload the final APK file to your Android device and run. Then follow the instructions within the app.
Upload the final APK file to your Android device or emulator and run. Then follow the instructions within the app.
### Running on Android Emulator
For creating and running an Android emulator, follow the instructions on the [avdmanager][avd-manager] and [emulator][android-emulator] pages. Please also make sure that [telnet][telnet-wiki] is installed on your system for [emulator console][emulator console] commands.
Android emulators run behind a [virtual router](https://developer.android.com/studio/run/emulator-networking) and network address translation (NAT) so they can't be accessed directly from the host machine. There is one TCP port required to be forwarded in between that virtual router and the host machine so that Qt Design Studio can communicate with the application.
After creating and spinning up the emulator, you can forward the required ports to the emulator with the following command on terminal:
> Note: This unfortunately needs to be repeated every time the emulator is started.
```bash
telnet localhost 5554 --> ( that will show you the file to get the auth code )
auth <auth-code>
redir add tcp:40000:40000
```
Then in the Qt Design Studio add a new device with the IP of 127.0.0.1
## Testing
......@@ -92,14 +110,14 @@ Test results can be viewed with any JUnit compatible viewer. For example, you ca
```bash
npm i -g xunit-viewer
xunit-viewer -r output.junit.xml
xunit-viewer -r output.junitxml -s -w
```
It'll create a `report.html` file that can be opened in a web browser. For more information about `xunit-viewer` please visit the [GitHub repo](https://github.com/lukejpreston/xunit-viewer).
It'll create a `report.html` file that can be opened in a web browser. For more information about `xunit-viewer` please visit the [GitHub repo][xunit-viewer].
## Versioning
[android:versionCode](https://developer.android.com/guide/topics/manifest/manifest-element#vcode) is an integer value that should be incremented for each new release and it's required by Google Play Store. It's used to determine if the app should be updated or not.
[android:versionCode][manifest-vcode] is an integer value that should be incremented for each new release and it's required by Google Play Store. It's used to determine if the app should be updated or not.
During the development this value can be set to any arbitrary integer. But for the release it should be incremented. The value can be controlled by setting the CMake variable called `GOOGLE_PLAY_APP_VERSION` during the CMake configuration step.
......@@ -109,7 +127,7 @@ It's easy for an individual developer to forget to increment it before the relea
android:versionCode = <total_number_of_tags> + 11
```
The value `11` is coming from the times where `GOOGLE_PLAY_APP_VERSION` was increased without creating a tag, so we have an offset in between the tag count and the actual value. From now on the value should be incremented by creating a new tag.
The magic value `11` is coming from the times where `GOOGLE_PLAY_APP_VERSION` was increased without creating a tag, so we have an offset in between the tag count and the actual value. From now on the value should be incremented by creating a new tag.
There's no `AndroidManifest.xml` file in the repository. `AndroidManifest.xml.in` file is used to generate the `AndroidManifest.xml` file during the configuration step. The `versionCode` value is set to the `GOOGLE_PLAY_APP_VERSION` and the `versionName` value is set to the output of the `git describe --always --tags` command.
......@@ -127,3 +145,9 @@ Both values can be seen in the `AndroidManifest.xml` file after the CMake config
[google-play-link]: <https://play.google.com/store/apps/details?id=io.qt.qtdesignviewer>
[package-registry]: <https://git.qt.io/design-studio/design-viewer/qt-ui-viewer/-/packages>
[xunit-viewer]: <https://github.com/lukejpreston/xunit-viewer>
[manifest-vcode]: <https://developer.android.com/studio/publish/versioning>
[avd-manager]:<https://developer.android.com/tools/avdmanager>
[android-emulator]:<https://developer.android.com/studio/run/emulator-commandline>
[telnet-wiki]:<https://en.wikipedia.org/wiki/Telnet>
[emulator console]:<https://developer.android.com/studio/run/emulator-console>
File deleted
......@@ -3,23 +3,27 @@
.build-components: &build-components
- pushd 3rdparty/qtquickdesigner-components
- |
cmake \
-S . \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE=${QDS_CI_JOB_QT_ANDROID_PATH}/lib/cmake/Qt6/qt.toolchain.cmake \
-DANDROID_SDK_ROOT=${DOCKER_ENV_ANDROID_SDK_ROOT} \
-DANDROID_NDK_ROOT=${DOCKER_ENV_ANDROID_NDK_ROOT} \
-DQT_HOST_PATH=${DOCKER_ENV_QT_PATH_LINUX_GCC_64} \
-DFLOWVIEW_AUTO_QMLDIR=ON \
-DCMAKE_INSTALL_PREFIX=${QDS_CI_JOB_QT_ANDROID_PATH}
- cmake --build .
- cmake --install .
for arch in ${QDS_CI_JOB_ARCHS}; do
QT_DIR_WITH_ARCH=${DOCKER_ENV_QT_PATH_WITH_VERSION}/${arch}
BUILD_PATH=${QDS_CI_JOB_BUILD_PATH}/components/${arch}
cmake \
-S . \
-G Ninja \
-B ${BUILD_PATH} \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE=${QT_DIR_WITH_ARCH}/lib/cmake/Qt6/qt.toolchain.cmake \
-DANDROID_SDK_ROOT=${DOCKER_ENV_ANDROID_SDK_ROOT} \
-DANDROID_NDK_ROOT=${DOCKER_ENV_ANDROID_NDK_ROOT} \
-DQT_HOST_PATH=${DOCKER_ENV_QT_PATH_LINUX_GCC_64} \
-DFLOWVIEW_AUTO_QMLDIR=ON \
-DCMAKE_INSTALL_PREFIX=${QT_DIR_WITH_ARCH}
cmake --build ${BUILD_PATH} --target install
rm -rf ${BUILD_PATH}
done
- popd
.build-android-apps: &build-android-apps
.build-app: &build-app
- export GOOGLE_PLAY_APP_VERSION=$(( $(git tag --list | wc -l) +11 ))
# where is this magic "$(git tag --list | wc -l) +11" coming from?
# "git tag --list | wc -l" counts the number of tags in the repository.
# The "+11" is a magic number which is the last GOOGLE_PLAY_APP_VERSION
# that was released before starting to add tags to the repository. After
......@@ -32,62 +36,81 @@
-B ${QDS_CI_JOB_BUILD_PATH} \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE=${QDS_CI_JOB_QT_ANDROID_PATH}/lib/cmake/Qt6/qt.toolchain.cmake \
-DCMAKE_TOOLCHAIN_FILE=${QDS_CI_JOB_QT_PATH}/lib/cmake/Qt6/qt.toolchain.cmake \
-DANDROID_SDK_ROOT=${DOCKER_ENV_ANDROID_SDK_ROOT} \
-DANDROID_NDK_ROOT=${DOCKER_ENV_ANDROID_NDK_ROOT} \
-DQT_HOST_PATH=${DOCKER_ENV_QT_PATH_LINUX_GCC_64} \
-DCMAKE_INSTALL_PREFIX=${QDS_CI_JOB_QT_ANDROID_PATH} \
-DCMAKE_INSTALL_PREFIX=${QDS_CI_JOB_QT_PATH} \
-DANDROID_OPENSSL_PATH=${QDS_CI_JOB_OPENSSL_PATH} \
-DGOOGLE_PLAY_APP_VERSION=${GOOGLE_PLAY_APP_VERSION} \
-DQT_ANDROID_BUILD_ALL_ABIS=ON \
-DQT_ANDROID_MULTI_ABI_FORWARD_VARS="GOOGLE_PLAY_APP_VERSION;ANDROID_OPENSSL_PATH" \
-DBUILD_EXAMPLES=OFF
- cmake --build ${QDS_CI_JOB_BUILD_PATH} --target aab
- cat ${QDS_CI_JOB_BUILD_PATH}/src/dynamic_imports.qml
- cmake --build ${QDS_CI_JOB_BUILD_PATH} --target ${QDS_CI_JOB_CMAKE_BUILD_TARGET}
.copy-and-sign-apks: &copy-and-sign-apks
- QDS_CI_KEYSTORE_PATH=$(pwd)/android_release.keystore
- QDS_CI_APK_SIGNER_PATH=${DOCKER_ENV_ANDROID_SDK_ROOT}/build-tools/${QDS_CI_ANDROID_SDK_VERSION}/apksigner
- echo ${QDS_VAR_KEYSTORE} | base64 -d > ${QDS_CI_KEYSTORE_PATH}
- cp -r ${QDS_CI_JOB_BUILD_PATH}/src/android-build/build/outputs/apk/release/* ${QDS_CI_JOB_ARTIFACTS_PATH_APP}
- cp -r ${QDS_CI_JOB_BUILD_PATH}/tests/android-build/build/outputs/apk/release/* ${QDS_CI_JOB_ARTIFACTS_PATH_TEST}
- echo ${QDS_VAR_PASS} | ${DOCKER_ENV_ANDROID_SDK_ROOT}/build-tools/${QDS_CI_ANDROID_SDK_VERSION}/apksigner sign -verbose -ks ${CI_PROJECT_DIR}/cicd/android/android_release.keystore -out ${QDS_CI_JOB_ARTIFACTS_PATH_APP}/android-build-release.apk ${QDS_CI_JOB_ARTIFACTS_PATH_APP}/android-build-release-unsigned.apk
- echo ${QDS_VAR_PASS} | ${DOCKER_ENV_ANDROID_SDK_ROOT}/build-tools/${QDS_CI_ANDROID_SDK_VERSION}/apksigner sign -verbose -ks ${CI_PROJECT_DIR}/cicd/android/android_release.keystore -out ${QDS_CI_JOB_ARTIFACTS_PATH_TEST}/android-build-release.apk ${QDS_CI_JOB_ARTIFACTS_PATH_TEST}/android-build-release-unsigned.apk
- echo ${QDS_VAR_PASS} | ${QDS_CI_APK_SIGNER_PATH} sign -verbose -ks ${QDS_CI_KEYSTORE_PATH} -out ${QDS_CI_JOB_ARTIFACTS_PATH_APP}/android-build-release.apk ${QDS_CI_JOB_ARTIFACTS_PATH_APP}/android-build-release-unsigned.apk
- echo ${QDS_VAR_PASS} | ${QDS_CI_APK_SIGNER_PATH} sign -verbose -ks ${QDS_CI_KEYSTORE_PATH} -out ${QDS_CI_JOB_ARTIFACTS_PATH_TEST}/android-build-release.apk ${QDS_CI_JOB_ARTIFACTS_PATH_TEST}/android-build-release-unsigned.apk
- rm -f ${QDS_CI_JOB_ARTIFACTS_PATH_APP}/android-build-release-unsigned.apk ${QDS_CI_JOB_ARTIFACTS_PATH_TEST}/android-build-release-unsigned.apk
.copy-and-sign-aab: &copy-and-sign-aab
- |
if [[ -n ${CI_COMMIT_TAG} ]];
then
QDS_CI_KEYSTORE_PATH=$(pwd)/android_release.keystore
echo ${QDS_VAR_KEYSTORE} | base64 -d > ${QDS_CI_KEYSTORE_PATH}
cp -r ${QDS_CI_JOB_BUILD_PATH}/src/android-build/build/outputs/bundle/release/* ${QDS_CI_JOB_ARTIFACTS_PATH_APP}
/usr/bin/jarsigner -keystore ${CI_PROJECT_DIR}/cicd/android/android_release.keystore ${QDS_CI_JOB_ARTIFACTS_PATH_APP}/android-build-release.aab designviewer -storepass ${QDS_VAR_PASS}
/usr/bin/jarsigner -keystore ${QDS_CI_KEYSTORE_PATH} ${QDS_CI_JOB_ARTIFACTS_PATH_APP}/android-build-release.aab ${QDS_VAR_KEYSTORE_ALIAS} -storepass ${QDS_VAR_PASS}
fi
build-android:
.build-common:
extends: .pipeline_common
stage: build
# rules:
# - if: $CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "web"
parallel:
matrix:
- QDS_CI_JOB_TARGET_ARCH: "arm64_v8a"
QDS_CI_JOB_OPENSSL_PATH: "/opt/openssl/ssl_3/arm64-v8a"
- QDS_CI_JOB_TARGET_ARCH: "armv7"
QDS_CI_JOB_OPENSSL_PATH: "/opt/openssl/ssl_3/armeabi-v7a"
- QDS_CI_JOB_TARGET_ARCH: "x86"
QDS_CI_JOB_OPENSSL_PATH: "/opt/openssl/ssl_3/x86"
- QDS_CI_JOB_TARGET_ARCH: "x86_64"
QDS_CI_JOB_OPENSSL_PATH: "/opt/openssl/ssl_3/x86_64"
variables:
QDS_CI_JOB_BUILD_PATH: "${QDS_CI_CACHE_PATH}/${QDS_CI_JOB_TARGET_ARCH}/build"
QDS_CI_JOB_TARGET_PLATFORM: "android"
QDS_CI_JOB_ARTIFACTS_PATH: ${QDS_CI_ARTIFACTS_PATH}/${QDS_CI_JOB_TARGET_ARCH}
QDS_CI_JOB_ARTIFACTS_PATH_APP: ${QDS_CI_JOB_ARTIFACTS_PATH}/app
QDS_CI_JOB_ARTIFACTS_PATH_TEST: ${QDS_CI_JOB_ARTIFACTS_PATH}/test
artifacts:
name: qt_ui_viewer-${CI_JOB_ID}-qt${QDS_CI_QT_VERSION}-${QDS_CI_JOB_TARGET_ARCH}
name: qt_ui_viewer-${CI_JOB_ID}-qt${QDS_CI_QT_VERSION}-${QDS_CI_JOB_TARGET_PLATFORM}
expose_as: "build-artifacts"
paths:
- ${QDS_CI_ARTIFACTS_PATH}
expire_in: 1 week
build-android:
extends: .build-common
variables:
QDS_CI_JOB_TARGET_PLATFORM: "android"
QDS_CI_JOB_ARCHS: android_arm64_v8a android_armv7 android_x86 android_x86_64
QDS_CI_JOB_OPENSSL_PATH: "/opt/openssl/ssl_3/arm64-v8a"
QDS_CI_JOB_BUILD_PATH: "${CI_PROJECT_DIR}/${QDS_CI_JOB_TARGET_PLATFORM}/build"
QDS_CI_JOB_ARTIFACTS_PATH: ${QDS_CI_ARTIFACTS_PATH}/${QDS_CI_JOB_TARGET_PLATFORM}
QDS_CI_JOB_ARTIFACTS_PATH_APP: ${QDS_CI_JOB_ARTIFACTS_PATH}/app
QDS_CI_JOB_ARTIFACTS_PATH_TEST: ${QDS_CI_JOB_ARTIFACTS_PATH}/test
QDS_CI_JOB_CMAKE_BUILD_TARGET: "aab"
script:
- mkdir -p ${QDS_CI_JOB_ARTIFACTS_PATH_APP} ${QDS_CI_JOB_ARTIFACTS_PATH_TEST}
- export QDS_CI_JOB_QT_ANDROID_PATH="${DOCKER_ENV_QT_PATH_WITH_VERSION}/android_${QDS_CI_JOB_TARGET_ARCH}"
- export QDS_CI_JOB_QT_PATH="${DOCKER_ENV_QT_PATH_WITH_VERSION}/android_arm64_v8a"
- *build-components
- *build-android-apps
- *build-app
- *copy-and-sign-apks
- *copy-and-sign-aab
build-desktop:
extends: .build-common
variables:
QDS_CI_JOB_TARGET_PLATFORM: "desktop"
QDS_CI_JOB_ARCHS: gcc_64
QDS_CI_JOB_BUILD_PATH: "${CI_PROJECT_DIR}/${QDS_CI_JOB_TARGET_PLATFORM}/build"
QDS_CI_JOB_ARTIFACTS_PATH: ${QDS_CI_ARTIFACTS_PATH}/${QDS_CI_JOB_TARGET_PLATFORM}
QDS_CI_JOB_CMAKE_BUILD_TARGET: "all"
script:
- mkdir -p ${QDS_CI_JOB_ARTIFACTS_PATH}
- export QDS_CI_JOB_QT_PATH="${DOCKER_ENV_QT_PATH_LINUX_GCC_64}"
- apt-get update
- apt-get install -y libpulse-dev
- *build-components
- *build-app
- cp -r ${QDS_CI_JOB_BUILD_PATH}/src//qtuiviewer ${QDS_CI_JOB_ARTIFACTS_PATH}
......@@ -5,7 +5,7 @@ create-packages:
rules:
- if: $CI_COMMIT_TAG
needs:
- job: test-x86_64
- job: test-android
optional: false
artifacts: true
- job: build-android
......@@ -29,7 +29,7 @@ create-packages:
cd "${arch}/app"
tar -czf "${arch}.tar.gz" *
echo "Uploading ${arch}.tar.gz to GitLab Package Registry"
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file "${arch}.tar.gz" ${QDS_PACKAGE_URL}/qt-ui-viewer-${CI_COMMIT_TAG}-qt${QDS_CI_QT_VERSION}-${arch}.tar.gz
curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file "${arch}.tar.gz" ${QDS_PACKAGE_URL}/qt-ui-viewer-${CI_COMMIT_TAG}-qt${QDS_CI_QT_VERSION}-multiarch.tar.gz
cd ${current_dir}
done
artifacts:
......@@ -59,7 +59,10 @@ create-release:
tag_name: ${CI_COMMIT_TAG}
assets:
links:
- name: "Binary Packages"
- name: "Google Play Store"
url: "https://play.google.com/store/apps/details?id=io.qt.qtdesignviewer"
link_type: "other"
- name: "Prebuilt APK Files"
url: "https://git.qt.io/design-studio/design-viewer/qt-ui-viewer/-/packages"
link_type: "package"
- name: "Docker Images Used in CI/CD Pipeline"
......
# todo: run the tests for all architectures
test-x86_64:
.test-common:
stage: test
extends: .pipeline_common
variables:
GIT_SUBMODULE_STRATEGY: none
QDS_CI_JOB_TEST_RESULTS_PATH: ${CI_PROJECT_DIR}/test
artifacts:
paths:
- ${QDS_CI_JOB_TEST_RESULTS_PATH}/
reports:
junit: ${QDS_CI_JOB_TEST_RESULTS_PATH}/test.junit.xml
expire_in: 1 week
test-android:
extends: .test-common
tags:
- qds-blade-server-shell
needs:
- job: build-android
optional: false
artifacts: true
variables:
GIT_SUBMODULE_STRATEGY: none
QDS_CI_JOB_TEST_RESULTS_PATH: ${CI_PROJECT_DIR}/test
- job: build-desktop
optional: false
artifacts: true
script:
- |
if [[ ${QDS_CI_SKIP_TESTS} == "false" ]]; then
......@@ -24,9 +35,36 @@ test-x86_64:
export PATH=$PATH:${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/
export PATH=$PATH:${ANDROID_SDK_ROOT}/emulator
export PATH=$PATH:${ANDROID_SDK_ROOT}/platform-tools
- cd ${QDS_CI_ARTIFACTS_PATH}/x86_64/test || exit 1
- |
if [[ $(adb devices | grep emulator) ]]; then
echo "Emulator is running";
else
echo "Starting emulator";
/opt/android/emulator/emulator -avd test -no-boot-anim -no-window -no-audio -no-snapshot -accel on & 2>&1 > /dev/null
counter=0
while [[ $(/opt/android/platform-tools/adb devices -l | egrep -Ev '^$|List' | wc -l) -eq 0 ]]; do
sleep 1
counter=$((counter+1))
if [[ $counter -gt 30 ]]; then
echo "Emulator didn't start in time. Exiting."
exit 1
fi
done
# let the emulator start
sleep 60;
fi
- cd ${QDS_CI_ARTIFACTS_PATH}/android/test || exit 1
- mkdir -p ${QDS_CI_JOB_TEST_RESULTS_PATH}
- adb install -r -g android-build-release.apk
- |
while ! adb install -r -g android-build-release.apk; do
sleep 5
counter=$((counter+1))
if [[ $counter -gt 10 ]]; then
echo "Can't install the APK to the device in time. Exiting."
exit 1
fi
done
- adb shell pm clear io.qt.qtdesignviewer.test
- adb shell "run-as io.qt.qtdesignviewer.test rm -rf /data/data/io.qt.qtdesignviewer.test/files/output.junitxml"
- adb shell am start -e applicationArguments "'-o output.junitxml,junitxml'" -n io.qt.qtdesignviewer.test/org.qtproject.qt.android.bindings.QtActivity
......@@ -60,9 +98,27 @@ test-x86_64:
- mv output.junit.xml ${QDS_CI_JOB_TEST_RESULTS_PATH}/test.junit.xml
- chmod +x ${CI_PROJECT_DIR}/tests/scripts/get_test_result.sh
- ${CI_PROJECT_DIR}/tests/scripts/get_test_result.sh ${QDS_CI_JOB_TEST_RESULTS_PATH}/test.junit.xml
artifacts:
paths:
- ${QDS_CI_JOB_TEST_RESULTS_PATH}/
reports:
junit: ${QDS_CI_JOB_TEST_RESULTS_PATH}/test.junit.xml
expire_in: 1 week
# it's not working as of because of the headless mode
# possible bugs:
# - https://bugreports.qt.io/browse/QTBUG-124467
# - https://bugreports.qt.io/browse/QTBUG-126508
.test-desktop:
extends: .test-common
needs:
- job: build-desktop
optional: false
artifacts: true
script:
- |
if [[ ${QDS_CI_SKIP_TESTS} == "false" ]]; then
echo "Running tests";
else
echo "Skipping tests";
exit 0;
fi
- cd ${QDS_CI_ARTIFACTS_PATH}/gcc_64 || exit 1
- apt update; apt install -y libxcb-cursor0
- ./qtuiviewer_test
- cp output.junitxml ${QDS_CI_JOB_TEST_RESULTS_PATH}/test.junit.xml
find_package(
Qt6
COMPONENTS Core Widgets Quick Gui Qml Multimedia MultimediaWidgets Concurrent Network WebSockets
COMPONENTS
Core Widgets Quick Gui Qml Multimedia MultimediaWidgets Concurrent Network WebSockets
TextToSpeech Location Sensors WebView Positioning
REQUIRED
)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Network WebSockets)
find_package(
QT NAMES Qt6 Qt5 REQUIRED
COMPONENTS
Core Network WebSockets TextToSpeech Location Sensors WebView Positioning)
# In shared Qt builds, the qml plugins don't have dependencies on their backing libraries, and
# thus the backing library packages are not automatically looked up.
# The StandaloneTests directory contains Config files that find_package all installed Qt packages.
# Abuse that, and include all of them to make all the backing libraries available as imported
# targets.
find_package(Qt6 COMPONENTS BuildInternals)
if(Qt6BuildInternals_DIR)
set(standalone_tests_dir "${Qt6BuildInternals_DIR}/StandaloneTests")
if(EXISTS "${standalone_tests_dir}")
# Glob the config files in there.
file(GLOB_RECURSE standalone_tests_files "${standalone_tests_dir}/*TestsConfig.cmake")
# Include each one of them.
foreach(standalone_tests_file IN LISTS standalone_tests_files)
include("${standalone_tests_file}")
endforeach()
endif()
endif()
set(imports "")
# Get all imported targets.
get_property(imported_targets DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY IMPORTED_TARGETS)
# Horrible hack for shared qt builds, to ensure we bundle all known shared library qml plugins.
# Do that by finding all qml modules, their plugins, the location of the plugins, then finding the
# path to the qmldir files of the plugins, extract the module name, and adding it as an import
# statement into a dynamically generated qml file.
foreach(imported_target IN LISTS imported_targets)
# Get all qml module targets, they would have the _qt_qml_module_installed_plugin_target
# property set.
get_target_property(qml_plugin_target "${imported_target}"
_qt_qml_module_installed_plugin_target)
if(qml_plugin_target)
message(STATUS
"Imported target: ${imported_target} -> ${qml_plugin_target}")
# Get location of the plugin file.
get_target_property(imported_location_default "${qml_plugin_target}" IMPORTED_LOCATION)
get_target_property(imported_location_release "${qml_plugin_target}" IMPORTED_LOCATION_RELEASE)
get_target_property(imported_location_min_size "${qml_plugin_target}" IMPORTED_LOCATION_MINSIZEREL)
get_target_property(imported_location_min_size_rel "${qml_plugin_target}" IMPORTED_LOCATION_RELWITHDEBINFO)
if(imported_location_default)
set(imported_location "${imported_location_default}")
elseif(imported_location_release)
set(imported_location "${imported_location_release}")
elseif(imported_location_min_size)
set(imported_location "${imported_location_min_size}")
elseif(imported_location_min_size_rel)
set(imported_location "${imported_location_min_size_rel}")
else()
message(WARNING "No imported location found for ${qml_plugin_target}.")
continue()
endif()
# Get parent dir of the plugin file.
get_filename_component(imported_location_dir "${imported_location}" DIRECTORY)
# Build path to qmldir in the same directory. We assume the qmldir is always next
# to the plugin file. That's at least usually the case for Qt qml plugins.
set(qmldir_path "${imported_location_dir}/qmldir")
if(NOT EXISTS "${qmldir_path}")
message(WARNING "No qmldir file found for ${qml_plugin_target}.")
continue()
endif()
# Read qmldir file contents.
file(READ "${qmldir_path}" qmldir_content)
if(qmldir_content STREQUAL "")
message(WARNING "Empty qmldir for ${qml_plugin_target}.")
continue()
endif()
# Find the module name in the qmldir file
string(REGEX MATCH "module ([^\n]+)" module_match "${qmldir_content}")
if(CMAKE_MATCH_1)
# Add the module name as an import to the imports list
list(APPEND imports "import ${CMAKE_MATCH_1}")
endif()
endif()
endforeach()
set(imports_file_path "${CMAKE_CURRENT_BINARY_DIR}/dynamic_imports.qml")
# Needed to stop __qt_get_relative_resource_path_for_file from erroring out.
set_source_files_properties("${imports_file_path}"
PROPERTIES QT_RESOURCE_ALIAS "dynamic_imports.qml")
if(imports)
list(REMOVE_DUPLICATES imports)
string(REPLACE ";" "\n" imports "${imports}")
endif()
# Use file(GENERATE) to prevent touching the file if the contents hasn't changed.
set(dummy_valid_content "ApplicationWindow {
visible: true
width: 640
height: 480
}")
file(GENERATE OUTPUT "${imports_file_path}" CONTENT "${imports}
${dummy_valid_content}
")
qt_add_executable(${PROJECT_NAME}
backend/importdummy.qml
backend/main.cpp
backend/logger.h
backend/backend.cpp backend/backend.h
......@@ -27,6 +138,7 @@ qt_add_qml_module(${PROJECT_NAME}
URI AndroidUI
VERSION 1.0
QML_FILES
"${imports_file_path}"
Constants.qml
HomePage.qml
Main.qml
......@@ -52,6 +164,8 @@ target_link_libraries(${PROJECT_NAME} PRIVATE
Qt6::Qml Qt6::GuiPrivate
Qt6::Multimedia Qt6::MultimediaWidgets
Qt6::Concurrent Qt6::Network Qt6::WebSockets
Qt6::TextToSpeech Qt6::Location Qt6::Sensors
Qt6::WebView Qt6::Positioning
ZXing::ZXing
)
......
......@@ -61,7 +61,6 @@ Rectangle {
Connections {
target: backend
function onConnectedChanged(isConnected) {
console.log("Connected changed to", isConnected)
root.connected = isConnected
}
}
......
......@@ -67,13 +67,27 @@ Backend::Backend(QObject *parent)
if (m_projectManager && !m_lastSessionId.isEmpty()
&& m_lastSessionId == m_projectManager->sessionId()) {
QMetaObject::invokeMethod(m_dsManager.get(),
"sendProjectLogs",
&DesignStudioManager::sendProjectLogs,
Qt::QueuedConnection,
Q_ARG(QString, m_lastProjectSenderId),
Q_ARG(QString, msg));
m_lastProjectSenderId,
msg);
}
});
connect(qApp,
&QGuiApplication::applicationStateChanged,
this,
[this](Qt::ApplicationState state) {
qDebug() << "Application state changed to" << state;
if (state == Qt::ApplicationState::ApplicationSuspended) {
if (m_projectManager && !m_projectManager->sessionId().isEmpty())
QMetaObject::invokeMethod(m_projectManager.get(),
&ProjectManager::stopProject);
m_dsManager->disconnectAllDesignStudios();
popupClose();
}
});
const QRect screenGeometry = QGuiApplication::primaryScreen()->geometry();
qDebug() << "Qt Design Viewer";
......@@ -151,16 +165,16 @@ void Backend::initProjectManager()
return;
QMetaObject::invokeMethod(m_dsManager.get(),
"sendProjectStopped",
&DesignStudioManager::sendProjectStopped,
Qt::QueuedConnection,
Q_ARG(QString, m_lastProjectSenderId));
m_lastProjectSenderId);
});
}
void Backend::initDsManager()
{
qDebug() << "Design Studio Manager thread started. Initializing Design Studio Manager";
m_dsManager.reset(new DesignStudioManager(m_settings.deviceUuid(), this));
m_dsManager.reset(new DesignStudioManager(m_settings.deviceUuid()));
connect(m_dsManager.get(), &DesignStudioManager::projectReceived, this, &Backend::runProject);
......@@ -173,7 +187,7 @@ void Backend::initDsManager()
this,
[this](const QString &id, const QString &ipAddr) {
if (id == m_lastProjectSenderId) {
QMetaObject::invokeMethod(m_projectManager.get(), "stopProject");
QMetaObject::invokeMethod(m_projectManager.get(), &ProjectManager::stopProject);
}
emit popupClose();
});
......@@ -193,7 +207,7 @@ void Backend::initDsManager()
m_lastProjectSenderId = id;
m_lastSessionId = QUuid::createUuid().toString(QUuid::WithoutBraces);
QMetaObject::invokeMethod(m_projectManager.get(),
"stopProject",
&ProjectManager::stopProject,
Qt::QueuedConnection);
});
......@@ -210,27 +224,18 @@ void Backend::initDsManager()
qDebug() << "Project stop requested";
emit popupClose();
QMetaObject::invokeMethod(m_projectManager.get(),
"stopProject",
&ProjectManager::stopProject,
Qt::QueuedConnection);
QMetaObject::invokeMethod(m_dsManager.get(),
"sendProjectStopped",
&DesignStudioManager::sendProjectStopped,
Qt::QueuedConnection,
Q_ARG(QString, m_lastProjectSenderId));
m_lastProjectSenderId);
});
m_dsManager->init();
qDebug() << "Design Studio Manager initialized";
}
void Backend::connectDesignStudio(const QString &ipAddr)
{
QMetaObject::invokeMethod(m_dsManager.get(),
"initDesignStudio",
Q_ARG(QString, ipAddr),
Q_ARG(QString, ""));
emit updatePopupText("Connecting in the background...", 1500);
}
void Backend::runProject(const QString &id, const QByteArray &projectData)
{
emit updatePopupText("Running project...");
......@@ -238,16 +243,22 @@ void Backend::runProject(const QString &id, const QByteArray &projectData)
QTimer::singleShot(1000, [this, id, projectData] {
bool retVal;
QMetaObject::invokeMethod(m_projectManager.get(),
"runProject",
&ProjectManager::runProject,
Q_RETURN_ARG(bool, retVal),
Q_ARG(QByteArray, projectData),
Q_ARG(bool, autoScaleProject()),
Q_ARG(QString, m_lastSessionId));
projectData,
autoScaleProject(),
m_lastSessionId);
if (!retVal)
QMetaObject::invokeMethod(m_dsManager.get(), "sendProjectStopped", Q_ARG(QString, id));
else
QMetaObject::invokeMethod(m_dsManager.get(), "sendProjectRunning", Q_ARG(QString, id));
QMetaObject::invokeMethod(m_dsManager.get(),
&DesignStudioManager::sendProjectStopped,
id);
else {
QMetaObject::invokeMethod(m_projectManager.get(), &ProjectManager::showAppWindow);
QMetaObject::invokeMethod(m_dsManager.get(),
&DesignStudioManager::sendProjectStarted,
id);
}
emit popupClose();
});
......@@ -280,7 +291,7 @@ void Backend::parseDesignViewerUrl(const QUrl &url)
return;
}
connectDesignStudio(url.host());
// connectDesignStudio(url.host());
}
void Backend::popupInterrupted()
......
......@@ -83,7 +83,6 @@ signals:
public slots:
QString buildInfo() const;
void scanQrCode();
void connectDesignStudio(const QString &ipAddr);
void parseDesignViewerUrl(const QUrl &url);
void popupInterrupted();
QJsonArray getIpAddresses() const;
......
......@@ -19,7 +19,7 @@ using namespace Qt::Literals;
constexpr auto deviceInfo = "deviceInfo"_L1;
constexpr auto projectReceivingProgress = "projectReceivingProgress"_L1;
constexpr auto projectStarting = "projectStarting"_L1;
constexpr auto projectRunning = "projectRunning"_L1;
constexpr auto projectStarted = "projectRunning"_L1;
constexpr auto projectStopped = "projectStopped"_L1;
constexpr auto projectLogs = "projectLogs"_L1;
}; // namespace PackageToDesignStudio
......@@ -38,7 +38,6 @@ DesignStudio::DesignStudio(QWebSocket *socket, const QString &deviceID, QObject
qDebug() << "Project is stalled. Closing the connection.";
m_socket->close();
m_socket->abort();
emit disconnected(m_designStudioID);
});
m_speedCalculator.setInterval(1000);
......@@ -67,6 +66,12 @@ void DesignStudio::initPingPong()
&QWebSocket::pong,
this,
[this](quint64 elapsedTime, const QByteArray &) {
if (elapsedTime > 1000)
qWarning() << "Design Studio pong is too slow:" << elapsedTime
<< "ms. Newtork issue?";
else if (elapsedTime > 500)
qWarning() << "Design Studio pong is slow:" << elapsedTime << "ms";
m_pongTimer.stop();
m_pingTimer.start();
});
......@@ -76,7 +81,6 @@ void DesignStudio::initPingPong()
<< "is not responding. Closing the connection.";
m_socket->close();
m_socket->abort();
emit disconnected(m_designStudioID);
});
}
......@@ -97,6 +101,9 @@ void DesignStudio::initSocket()
qDebug() << "Design Studio" << m_designStudioID << "disconnected";
m_pingTimer.stop();
m_pongTimer.stop();
m_projectStallTimer.stop();
m_speedCalculator.stop();
m_projectData.clear();
emit disconnected(m_designStudioID);
});
......@@ -111,6 +118,12 @@ void DesignStudio::initSocket()
&DesignStudio::processBinaryMessage);
}
void DesignStudio::disconnect()
{
m_socket->close();
m_socket->abort();
}
QString DesignStudio::ipv4Addr() const
{
return m_socket->peerAddress().toString();
......@@ -234,9 +247,9 @@ void DesignStudio::sendProjectStarting()
sendData(PackageToDesignStudio::projectStarting);
}
void DesignStudio::sendProjectRunning()
void DesignStudio::sendProjectStarted()
{
sendData(PackageToDesignStudio::projectRunning);
sendData(PackageToDesignStudio::projectStarted);
}
void DesignStudio::sendProjectStopped()
......
......@@ -13,6 +13,9 @@ class DesignStudio : public QObject
public:
DesignStudio(QWebSocket *socket, const QString &deviceID, QObject *parent = nullptr);
// Connection
void disconnect();
// Getters
QString ipv4Addr() const;
QString id() const;
......@@ -20,7 +23,7 @@ public:
// Send data
void sendDeviceInfo();
void sendProjectStarting();
void sendProjectRunning();
void sendProjectStarted();
void sendProjectStopped();
void sendProjectLogs(const QString &logs);
......
......@@ -9,26 +9,32 @@
#include <QJsonObject>
#include <QUdpSocket>
DesignStudioManager::DesignStudioManager(const QString &deviceUuid, QObject *parent)
DesignStudioManager::DesignStudioManager(const QString &deviceUuid,
const quint16 port,
QObject *parent)
: QObject(parent)
, m_port(port)
, m_deviceUuid(deviceUuid)
, m_webSocketServer("DesignStudio", QWebSocketServer::NonSecureMode)
{}
void DesignStudioManager::init()
bool DesignStudioManager::init()
{
static bool initialized = false;
if (initialized) {
return;
if (m_webSocketServer.isListening()) {
qWarning() << "TCP server is already running";
return false;
}
if (!m_webSocketServer.listen(QHostAddress::Any, m_port)) {
qWarning() << "Failed to start TCP server on port" << m_port;
return false;
}
initialized = true;
m_webSocketServer.listen(QHostAddress::Any, 40000);
connect(&m_webSocketServer,
&QWebSocketServer::newConnection,
this,
&DesignStudioManager::incomingConnection);
qDebug() << "TCP server listening on port 40000";
qDebug() << "TCP server listening on port" << m_webSocketServer.serverPort();
m_discoveryTimer.setInterval(10000);
connect(&m_discoveryTimer, &QTimer::timeout, [this]() {
......@@ -39,7 +45,10 @@ void DesignStudioManager::init()
QUdpSocket udpSocket;
const int port = 53452;
#ifdef Q_OS_ANDROID
udpSocket.writeDatagram(datagram, QHostAddress::LocalHost, port);
#endif
udpSocket.writeDatagram(datagram, QHostAddress::Broadcast, port);
udpSocket.writeDatagram(datagram, QHostAddress::AnyIPv4, port);
udpSocket.writeDatagram(datagram, QHostAddress::AnyIPv6, port);
......@@ -48,6 +57,18 @@ void DesignStudioManager::init()
udpSocket.writeDatagram(datagram, QHostAddress("10.0.2.2"), port);
});
m_discoveryTimer.start();
return true;
}
QString DesignStudioManager::webSocketServerName() const
{
return m_webSocketServer.serverName();
}
int DesignStudioManager::webSocketServerPort() const
{
return m_webSocketServer.serverPort();
}
void DesignStudioManager::incomingConnection()
......@@ -61,6 +82,7 @@ void DesignStudioManager::incomingConnection()
&QObject::deleteLater);
connect(designStudio.data(), &DesignStudio::disconnected, this, [this, designStudio]() {
m_designStudios.removeOne(designStudio);
emit designStudioDisconnected(designStudio->id(), designStudio->ipv4Addr());
qDebug() << "Remaining Design Studios:" << m_designStudios.size();
if (m_designStudios.isEmpty())
emit allDesignStudiosDisconnected();
......@@ -103,11 +125,11 @@ void DesignStudioManager::sendProjectStarting(const QString &id)
}
}
void DesignStudioManager::sendProjectRunning(const QString &id)
void DesignStudioManager::sendProjectStarted(const QString &id)
{
for (const auto &designStudio : m_designStudios) {
if (designStudio->id() == id)
designStudio->sendProjectRunning();
designStudio->sendProjectStarted();
}
}
......@@ -136,3 +158,24 @@ QString DesignStudioManager::getDesignStudioIp(const QString &id) const
return {};
}
bool DesignStudioManager::disconnectDesignStudio(const QString &id)
{
for (const auto &designStudio : m_designStudios) {
if (designStudio->id() == id) {
qDebug() << "Disconnecting Design Studio" << id;
designStudio->disconnect();
return true;
}
}
qWarning() << "Design Studio" << id << "not found for disconnection";
return false;
}
void DesignStudioManager::disconnectAllDesignStudios()
{
qDebug() << "Disconnecting all Design Studios";
for (const auto &designStudio : m_designStudios)
designStudio->disconnect();
}
......@@ -11,21 +11,28 @@ class DesignStudioManager : public QObject
{
Q_OBJECT
public:
explicit DesignStudioManager(const QString &deviceUuid, QObject *parent = nullptr);
explicit DesignStudioManager(const QString &deviceUuid,
const quint16 port = 40000,
QObject *parent = nullptr);
void init();
bool init();
QString webSocketServerName() const;
int webSocketServerPort() const;
public slots:
void sendProjectStarting(const QString &id);
void sendProjectRunning(const QString &id);
void sendProjectStarted(const QString &id);
void sendProjectStopped(const QString &id);
void sendProjectLogs(const QString &id, const QString &logs);
QString getDesignStudioIp(const QString &id) const;
bool disconnectDesignStudio(const QString &id);
void disconnectAllDesignStudios();
private:
// Network
QWebSocketServer m_webSocketServer;
QTimer m_discoveryTimer;
const quint16 m_port;
// Connected Design Studios
QList<QSharedPointer<DesignStudio>> m_designStudios;
......
// Hack to force the qml plugins to be linked statically
import QtQuick
import QtQuick.Controls
import QtQml
import QtQml.Models
import QtQml.StateMachine
import QtQuick3D
import QtQuick3D.AssetUtils
import QtQuick3D.Effects
import QtQuick3D.Helpers
import QtQuick3D.ParticleEffects
import QtQuick3D.Particles3D
import QtQuick.VirtualKeyboard
import QtQuick.Studio.Application
import QtQuick.Studio.Components
import QtQuick.Studio.Effects
import QtQuick.Studio.EventSimulator
import QtQuick.Studio.EventSystem
import QtQuick.Studio.LogicHelper
import QtQuick.Studio.MultiText
import QtQuick.Studio.Utils
import QtQuick.Studio.DesignEffects
import QtQuick.Timeline
import QtQuick.Timeline.BlendTrees
import QtQuickUltralite.Extras
import QtQuickUltralite.Layers
import QtCharts
import FlowView
import Qt.labs.folderlistmodel
import QtWebSockets
ApplicationWindow {
visible: true
width: 640
height: 480
}
......@@ -7,6 +7,18 @@
#ifdef Q_OS_ANDROID
#include <android/log.h>
#else
enum android_LogPriority {
ANDROID_LOG_UNKNOWN = 0,
ANDROID_LOG_DEFAULT,
ANDROID_LOG_VERBOSE,
ANDROID_LOG_DEBUG,
ANDROID_LOG_INFO,
ANDROID_LOG_WARN,
ANDROID_LOG_ERROR,
ANDROID_LOG_FATAL,
ANDROID_LOG_SILENT,
};
#endif
class Logger : public QObject
......@@ -30,28 +42,35 @@ public:
const char *file = context.file ? context.file : "";
const char *function = context.function ? context.function : "";
enum android_LogPriority logPriority;
switch (type) {
case QtDebugMsg:
logPrefix = QStringLiteral("Debug: ");
logPriority = ANDROID_LOG_DEBUG;
break;
case QtInfoMsg:
logPrefix = QStringLiteral("Info: ");
logPriority = ANDROID_LOG_INFO;
break;
case QtWarningMsg:
logPrefix = QStringLiteral("Warning: ");
logPriority = ANDROID_LOG_WARN;
break;
case QtCriticalMsg:
logPrefix = QStringLiteral("Critical: ");
logPriority = ANDROID_LOG_ERROR;
break;
case QtFatalMsg:
logPrefix = QStringLiteral("Fatal: ");
logSuffix = QStringLiteral(" (%1:%2, %3)").arg(file).arg(context.line).arg(function);
logPriority = ANDROID_LOG_FATAL;
break;
}
newLog += logPrefix + localMsg + logSuffix + "\n";
#ifdef Q_OS_ANDROID
__android_log_print(ANDROID_LOG_DEBUG, "Qt_UI_Viewer", "%s", qPrintable(newLog));
__android_log_print(logPriority, "Qt_UI_Viewer", "%s", qPrintable(newLog));
#else
fprintf(stderr, "%s", qPrintable(newLog));
#endif
......
......@@ -33,8 +33,6 @@
int main(int argc, char *argv[])
{
Logger::instance();
qDebug() << "Starting Qt Design Viewer";
qputenv("QML_COMPAT_RESOLVE_URLS_ON_ASSIGNMENT", "1");
QApplication app(argc, argv);
......@@ -48,7 +46,13 @@ int main(int argc, char *argv[])
view.engine()->rootContext()->setContextProperty("backend", &backend);
view.setSource(QUrl(QStringLiteral("qrc:/qt/qml/AndroidUI/Main.qml")));
view.setResizeMode(QQuickView::SizeRootObjectToView);
#ifdef Q_OS_ANDROID
view.showMaximized();
#else
QApplication::setWindowIcon(QIcon(QStringLiteral(":/images/appicon.svg")));
view.show();
#endif
return app.exec();
}
......@@ -135,9 +135,12 @@ QString ProjectManager::unpackProject(const QByteArray &project)
return resourcePath;
}
QString ProjectManager::findFile(const QString &dir, const QString &filter)
QString ProjectManager::findFile(const QString &dir, const QString &filter, const bool recursive)
{
QDirIterator it(dir, {filter}, QDir::Files);
QDirIterator it(dir,
{filter},
QDir::Files,
recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags);
return it.next();
}
......@@ -203,7 +206,7 @@ bool ProjectManager::runProjectInternal(const QByteArray &project)
const QString projectPath = unpackProject(project);
qDebug() << "Project location: " << projectPath;
const QString qmlProjectFile = findFile(projectPath, "*.qmlproject");
const QString qmlProjectFile = findFile(projectPath, "*.qmlproject", false);
if (qmlProjectFile.isEmpty()) {
qCritical() << "No \"*.qmlproject\" found in \"" << projectPath << "\".";
return false;
......@@ -217,7 +220,7 @@ bool ProjectManager::runProjectInternal(const QByteArray &project)
if (mainQmlFilePath.isEmpty())
return false;
const QString qtquickcontrols2File = findFile(projectPath, "qtquickcontrols2.conf");
const QString qtquickcontrols2File = findFile(projectPath, "qtquickcontrols2.conf", false);
if (!qtquickcontrols2File.isEmpty()) {
qputenv("QT_QUICK_CONTROLS_CONF", qtquickcontrols2File.toLatin1());
}
......@@ -252,15 +255,6 @@ bool ProjectManager::runProjectInternal(const QByteArray &project)
m_qmlEngine->addImportPath(importPath);
}
QObject::connect(m_qmlEngine.data(),
&QQmlEngine::warnings,
this,
[&](const QList<QQmlError> &warnings) {
for (const auto &warning : warnings) {
qWarning() << warning.toString();
}
});
qDebug() << "Loading mainQmlUrl: " << mainQmlUrl.toString();
m_qmlComponent.reset(new QQmlComponent(m_qmlEngine.data()));
......@@ -284,6 +278,8 @@ bool ProjectManager::runProjectInternal(const QByteArray &project)
qDebug() << "Setting up the quickWindow";
m_quickWindow.reset(qobject_cast<QQuickWindow *>(topLevel));
m_quickWindow->setVisible(false);
if (m_quickWindow) {
qDebug() << "Running with incubator controller";
m_qmlEngine->setIncubationController(m_quickWindow->incubationController());
......@@ -303,7 +299,7 @@ bool ProjectManager::runProjectInternal(const QByteArray &project)
view->setResizeMode(QQuickView::SizeViewToRootObject);
m_quickWindow->setBaseSize(QSize(contentItem->width(), contentItem->height()));
}
showAppWindow();
return true;
}
......@@ -341,7 +337,7 @@ void ProjectManager::orientateWindow(Qt::ScreenOrientation orientation)
<< m_quickWindow->width() << Qt::endl
<< "-- Child size: " << childItem->height() << " x " << childItem->width() << Qt::endl
<< "-- Child pos: " << childItem->x() << ", " << childItem->y() << Qt::endl
<< "-- Child scale: " << childItem->scale();
<< "-- Child scale: " << childItem->scale() << Qt::endl;
const QSizeF newContentSize = childItem->size().scaled(screenGeometry.size().toSizeF(),
Qt::AspectRatioMode::KeepAspectRatio);
......@@ -366,12 +362,15 @@ void ProjectManager::showAppWindow()
qDebug("Initializing and showing the QML app window");
QScreen *screen = QGuiApplication::primaryScreen();
connect(screen, &QScreen::orientationChanged, this, &ProjectManager::orientateWindow);
connect(screen, &QScreen::orientationChanged, this, [this](Qt::ScreenOrientation orientation) {
orientateWindow(orientation);
});
orientateWindow(screen->orientation());
connect(m_quickWindow.data(), &QQuickWindow::closing, this, [this, screen]() {
qDebug() << "QML app window is closing";
disconnect(screen, &QScreen::orientationChanged, this, &ProjectManager::orientateWindow);
disconnect(screen, &QScreen::orientationChanged, this, nullptr);
emit closingProject(m_sessionId);
m_sessionId.clear();
});
......@@ -382,11 +381,16 @@ void ProjectManager::showAppWindow()
void ProjectManager::stopProject()
{
if (!m_quickWindow || !m_quickWindow->isVisible())
if (!m_quickWindow)
return;
qDebug("Stopping the QML app window");
m_quickWindow->close();
m_qmlComponent.reset();
m_qmlEngine.reset();
m_quickWindow.reset();
m_sessionId.clear();
}
QString ProjectManager::sessionId() const
......
......@@ -41,8 +41,11 @@ public slots:
const bool autoScaleProject,
const QString &sessionId);
void stopProject();
void showAppWindow();
private:
friend class TestProjectManager;
// Member variables
QByteArray m_projectData;
QString m_projectPath;
......@@ -62,16 +65,14 @@ private:
// project management
bool runProjectInternal(const QByteArray &project);
QString findFile(const QString &dir, const QString &filter);
QString findFile(const QString &dir, const QString &filter, const bool recursive = true);
QString readQmlProjectFile(const QString &qmlProjectFilePath);
QString getMainQmlFile(const QString &projectPath, const QString &qmlProjectFileContent);
QStringList getImportPaths(const QString &projectPath, const QString &qmlProjectFileContent);
bool isQt6Project(const QString &qmlProjectFileContent);
void parseQmlProjectFile(const QString &fileName, QString *mainFile, QStringList *importPaths);
// window management
void orientateWindow(Qt::ScreenOrientation orientation);
void showAppWindow();
signals:
void closingProject(const QString &sessionId);
......
......@@ -83,22 +83,22 @@ void QrScanner::openCamera()
QtConcurrent::run([=] {
if (!m_captureSession)
return QList<Result>();
return QList<Barcode>();
QElapsedTimer timer;
timer.start();
QList<Result> results = ReadBarcodes(frame.toImage(), readerOptions);
auto results = ReadBarcodes(frame.toImage(), readerOptions);
qDebug() << "Barcode detection took" << timer.elapsed() << "ms";
if (!results.size())
m_processing = 0;
return results;
}).then([=](const QList<Result> &results) {
}).then([=](const QList<Barcode> &results) {
if (results.isEmpty() || !m_captureSession)
return;
Result result = results.first();
auto result = results.first();
qDebug() << "Text: " << result.text();
qDebug() << "Format: " << result.format();
......