diff --git a/.ci/build_appimage.sh b/.ci/build_appimage.sh index f558edb..6bbe065 100755 --- a/.ci/build_appimage.sh +++ b/.ci/build_appimage.sh @@ -7,9 +7,12 @@ sudo () } # Build -if [[ "$1" != "0" ]]; then - .ci/common/build.sh appimage_build linux || exit 1 +if [[ "$1" == "" ]]; then + PLATFORM=linux +else + PLATFORM="linux_$1" fi +.ci/common/build.sh appimage_build $PLATFORM || exit 1 repo_dir=$(pwd) cd appimage_build @@ -45,45 +48,63 @@ cmake .. && make -j$(nproc --all) && mv src/linuxdeploy-plugin-appimage ../.. && cd ../.. && -rm -rf plugin-appimage && - -# Build AppImageKit -sudo apt install -y snapd squashfs-tools && -sudo snap install docker && -git clone https://github.com/AppImage/AppImageKit --recurse-submodules && -cd AppImageKit && -sudo env ARCH=$(arch) bash ci/build.sh -sudo cp out/appimagetool /usr/bin/ && -sudo cp out/digest /usr/bin/ && -sudo cp out/validate /usr/bin/ && -cd .. && -sudo mkdir -p /usr/lib/appimagekit && -sudo ln -s /usr/bin/mksquashfs /usr/lib/appimagekit/mksquashfs && +rm -rf plugin-appimage + +# Download appimagetool +wget https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage && +mv appimagetool-*.AppImage appimagetool +chmod +x appimagetool +export PATH=$(pwd):$PATH # Install patchelf from PyPI (see https://github.com/linuxdeploy/linuxdeploy-plugin-qt/issues/133#issuecomment-1608168363) sudo apt install -y python3-pip +python3 -m venv .venv +source .venv/bin/activate pip3 install patchelf -export PATH=$PATH:~/.local/bin +patchelf --version + +# Use custom ldd and strip +if [[ "$PLATFORM" == "linux_aarch64" ]]; then + ln -s /usr/bin/${BUILD_TOOLCHAIN_PREFIX}strip strip + sudo cp ../.ci/bin/xldd /usr/bin/${BUILD_TOOLCHAIN_PREFIX}ldd + ln -s /usr/bin/${BUILD_TOOLCHAIN_PREFIX}ldd ldd + export CT_XLDD_ROOT="$BUILD_SYSROOT_PATH" +fi + +# Set LD_LIBRARY_PATH (directories with *.so files) +# TODO: Installing with cmake is probably a better idea +LD_LIBRARY_PATH="" + +for file in $(find . -type f -name "*.so*"); do + dir=$(dirname "$file") + if [[ ":$LD_LIBRARY_PATH:" != *":$dir:"* ]]; then + LD_LIBRARY_PATH="$LD_LIBRARY_PATH:`readlink -f $dir`" + fi +done + +LD_LIBRARY_PATH=${LD_LIBRARY_PATH#:} +LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$QT_ROOT_DIR/lib" # Qt +export LD_LIBRARY_PATH +echo "LD_LIBRARY_PATH set to: $LD_LIBRARY_PATH" # Build AppImage +if [[ "$PLATFORM" == "linux_aarch64" ]]; then + wget https://github.com/AppImage/type2-runtime/releases/download/continuous/runtime-aarch64 + export ARCH=arm_aarch64 + APPIMAGE_ARCH=aarch64 + export LDAI_RUNTIME_FILE=runtime-aarch64 + export QEMU_LD_PREFIX="$BUILD_SYSROOT_PATH" + export PATH="$QT_ROOT_DIR/bin:$PATH" # use cross Qt + export PATH="$PATH:$QT_HOST_PATH/libexec" +else + APPIMAGE_ARCH=x86_64 +fi + export QML_SOURCES_PATHS=$(pwd)/src && -export EXTRA_QT_PLUGINS="svg;" && -export LDAI_UPDATE_INFORMATION="${appimage_zsync_prefix}${app_name}*-${APPIMAGE_ARCH-$(arch)}.AppImage.zsync" +export EXTRA_QT_MODULES="qml;svg;" && +export LDAI_UPDATE_INFORMATION="${appimage_zsync_prefix}${app_name}*-${APPIMAGE_ARCH}.AppImage.zsync" echo "AppImage update information: ${LDAI_UPDATE_INFORMATION}" -case "$(qmake -query QMAKE_XSPEC)" in - linux-arm-gnueabi-g++) - wget https://github.com/AppImage/AppImageKit/releases/download/continuous/runtime-armhf - export ARCH=arm - export LDAI_RUNTIME_FILE=runtime-armhf - ;; - linux-aarch64-gnu-g++) - wget https://github.com/AppImage/AppImageKit/releases/download/continuous/runtime-aarch64 - export ARCH=arm_aarch64 - export LDAI_RUNTIME_FILE=runtime-aarch64 - ;; -esac - ./linuxdeploy --appdir AppDir -e src/app/${executable_name} -i $repo_dir/res/${executable_name}.png -d $repo_dir/release/appimage.desktop --plugin qt --output appimage mv *.AppImage* $repo_dir diff --git a/.ci/build_qt6.sh b/.ci/build_qt6.sh index 8ccca89..a96a993 100755 --- a/.ci/build_qt6.sh +++ b/.ci/build_qt6.sh @@ -3,102 +3,62 @@ sudo apt install -y lsb-release qt_version="$1" -qt_modules="$(echo $2 | tr ' ' ',')" + +if [[ "$2" == "" ]]; then + qt_modules="" +else + qt_modules=",$(echo $2 | tr ' ' ',')" +fi + target_arch="$3" root_path="$(pwd)" -sysroot_path="${root_path}/sysroot" -sysroot_ubuntu_version="$(lsb_release -rs).1" -sysroot_ubuntu_codename="$(lsb_release -cs)" -host_prefix="${root_path}/qt-host" -cross_prefix="${root_path}/qt-cross" +#host_prefix="${root_path}/qt-host" +host_prefix="$QT_HOST_PATH" +cross_prefix="$QT_CROSS_PATH" target_prefix="/usr/local/qt" -toolchain_config="${root_path}/.ci/qt6-toolchain.cmake" - -case "$target_arch" in - aarch64) - target_arch_name="armv8-a" - toolchain_name="aarch64-linux-gnu" - target_platform="linux-aarch64-gnu-g++" - ;; - armv7) - target_arch_name="armv7-a" - toolchain_name="arm-linux-gnueabihf" - target_platform="linux-arm-gnueabi-g++" - ;; -esac - -toolchain_prefix="${toolchain_name}-" - -case "$target_arch" in - aarch64) - target_arch_debian_name="arm64" - ;; - armv7) - target_arch_debian_name="armhf" - ;; -esac echo "Qt version to build: ${qt_version}" echo "Qt modules: ${qt_modules}" -echo "Target architecture: ${target_arch} (${target_arch_name})" - -# Install dependencies -${root_path}/.ci/qt6_deps.sh || exit 1 -${root_path}/.ci/install_cross_compiler.sh "${target_arch}" || exit 1 -sudo apt install -y qemu-user-static || exit 1 -sudo apt install -y symlinks || exit 1 +echo "Target architecture: ${target_arch} (${BUILD_ARCH_NAME})" # Clone Qt git clone https://github.com/qt/qt5 qt || exit 1 cd qt -git checkout "v$qt_version" || exit 1 -./init-repository --module-subset=qtbase,qttools,qtdeclarative,qtsvg,${qt_modules} || exit 1 -# Build Qt (host) -mkdir host-build -cd host-build -echo "Building host Qt..." -../configure -release -nomake examples -nomake tests -opensource -confirm-license -prefix "$host_prefix" || exit 1 -cmake --build . --parallel $(nproc --all) || exit 1 -echo "Installing host Qt..." -cmake --install . || exit 1 -cd .. -rm -rf host-build +if [[ `grep -o '\.' <<<"$qt_version" | grep -c .` == "1" ]]; then + git checkout $(git tag | grep "^v${qt_version}.[0-9]*$" | sort -V | tail -n 1) || exit 1 +else + git checkout "v${qt_version}" +fi + +./init-repository --module-subset=qtbase,qttools,qtdeclarative,qtsvg${qt_modules} || exit 1 -# Prepare sysroot -echo "Preparing sysroot..." -curl "https://cdimage.ubuntu.com/ubuntu-base/releases/${sysroot_ubuntu_codename}/release/ubuntu-base-${sysroot_ubuntu_version}-base-${target_arch_debian_name}.tar.gz" > ./ubuntu-base.tar.gz || exit 1 -mkdir -p "$sysroot_path" -sudo tar -xvzf ubuntu-base.tar.gz -C "$sysroot_path" || exit 1 -sudo update-binfmts --enable qemu-arm || exit 1 -sudo mount -o bind /dev "${sysroot_path}/dev" || exit 1 -sudo cp /etc/resolv.conf "${sysroot_path}/etc" || exit 1 -sudo chmod 1777 "${sysroot_path}/tmp" || exit 1 -sudo cp "${root_path}/.ci/qt6_deps.sh" "${sysroot_path}/" -sudo chroot "$sysroot_path" /bin/bash -c "/qt6_deps.sh" || exit 1 -sudo chroot "$sysroot_path" /bin/bash -c "apt install -y symlinks && symlinks -rc /" || exit 1 +# Build Qt (host) +#mkdir host-build +#cd host-build +#echo "Building host Qt..." +#../configure -release -nomake examples -nomake tests -opensource -confirm-license -prefix "$host_prefix" || exit 1 +#cmake --build . --parallel $(nproc --all) || exit 1 +#echo "Installing host Qt..." +#cmake --install . || exit 1 +#cd .. +#rm -rf host-build # Build Qt (cross) mkdir cross-build cd cross-build echo "Cross-compiling Qt..." -export BUILD_SYSROOT_PATH=${sysroot_path} -export BUILD_TOOLCHAIN_NAME=${toolchain_name} -export BUILD_TOOLCHAIN_PREFIX=${toolchain_prefix} -export BUILD_ARCH_NAME=${target_arch_name} -../configure -release -opengl es2 -nomake examples -nomake tests -qt-host-path "$host_prefix" -xplatform "$target_platform" \ - -device-option CROSS_COMPILE="$toolchain_prefix" -sysroot "$sysroot_path" -opensource -confirm-license \ - -prefix "$target_prefix" -extprefix "$cross_prefix" -- -DCMAKE_TOOLCHAIN_FILE="$toolchain_config" \ +../configure -release -opengl desktop -nomake examples -nomake tests -qt-host-path "$host_prefix" -xplatform "$BUILD_PLATFORM" \ + -device-option CROSS_COMPILE="$BUILD_TOOLCHAIN_PREFIX" -sysroot "$BUILD_SYSROOT_PATH" -opensource -confirm-license \ + -prefix "$target_prefix" -extprefix "$cross_prefix" -- -DCMAKE_TOOLCHAIN_FILE="$BUILD_TOOLCHAIN_CONFIG" \ -DQT_FEATURE_xcb=ON -DFEATURE_xcb_xlib=ON -DQT_FEATURE_xlib=ON || exit 1 cmake --build . --parallel $(nproc --all) || exit 1 echo "Installing cross-compiled Qt..." cmake --install . || exit 1 cd .. -rm -rf cross-build # Cleanup -sudo umount "${sysroot_path}/dev" || exit 1 cd .. rm -rf qt # Required for cache -sudo chmod 777 -R ${sysroot_path} +sudo chmod 777 -R ${BUILD_SYSROOT_PATH} diff --git a/.ci/common/build.sh b/.ci/common/build.sh index 3c83bab..00cce04 100755 --- a/.ci/common/build.sh +++ b/.ci/common/build.sh @@ -12,6 +12,8 @@ mkdir -p "$BUILD_DIR" if [[ "$PLATFORM" == "win64" ]] || [[ "$PLATFORM" == "win32" ]]; then cmake -B "$BUILD_DIR" -G "Ninja" -DCMAKE_BUILD_TYPE=Release -DSCRATCHCPP_PLAYER_BUILD_UNIT_TESTS=OFF || exit 3 +elif [[ "$PLATFORM" == "linux_aarch64" ]]; then + cmake -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Release -DSCRATCHCPP_PLAYER_BUILD_UNIT_TESTS=OFF -DCMAKE_PREFIX_PATH="$QT_ROOT_DIR" -DCMAKE_TOOLCHAIN_FILE="$BUILD_TOOLCHAIN_CONFIG" -DCMAKE_FIND_ROOT_PATH="$QT_ROOT_DIR" -DQT_HOST_PATH="$QT_HOST_PATH" -DQT_HOST_CMAKE_DIR="$QT_HOST_PATH/lib/cmake" else cmake -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Release -DSCRATCHCPP_PLAYER_BUILD_UNIT_TESTS=OFF || exit 3 fi diff --git a/.ci/install_cross_compiler.sh b/.ci/install_cross_compiler.sh index cf70b6d..ada76c7 100755 --- a/.ci/install_cross_compiler.sh +++ b/.ci/install_cross_compiler.sh @@ -5,6 +5,4 @@ case "$1" in aarch64) sudo apt install -y g++-aarch64-linux-gnu ;; - armv7) - sudo apt install -y g++-arm-linux-gnueabihf esac diff --git a/.ci/macos_build.sh b/.ci/macos_build.sh new file mode 100755 index 0000000..96a430e --- /dev/null +++ b/.ci/macos_build.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +.ci/common/build.sh macos || exit $? +mv $(find . -name "${executable_name}.app") . || exit $? + +mkdir -p "${executable_name}.app/Contents/Frameworks" + +for file in $(find . -type f -name "*.dylib"); do + install_name_tool -change "$file" "@rpath/$file" "${executable_name}.app/Contents/MacOS/${executable_name}" || exit $? + cp "$file" "${executable_name}.app/Contents/Frameworks/" +done + +macdeployqt "${executable_name}.app" -qmldir=src || exit $? + +# Sign using self-signed certificate +codesign --verify --verbose --force --deep -s - "${executable_name}.app" +codesign --verify --verbose "${executable_name}.app" + +# Create .dmg with an install screen +npm install -g appdmg || exit $? +mv ${executable_name}.app res/macos-release/ +dmg_name=`echo "${app_name}.dmg" | tr ' ' '_'` +appdmg res/macos-release/scratchcpp-player.json "$dmg_name" || exit $? + +# Verify +#spctl -a -t open --context context:primary-signature -v "$dmg_name" diff --git a/.ci/prepare_cross_build.sh b/.ci/prepare_cross_build.sh index 5588c2b..6cc5cae 100755 --- a/.ci/prepare_cross_build.sh +++ b/.ci/prepare_cross_build.sh @@ -1,36 +1,52 @@ #!/bin/bash +sudo apt install -y lsb-release + target_arch="$1" +root_path="$(pwd)" +sysroot_path="${root_path}/sysroot" +sysroot_ubuntu_version="$(lsb_release -rs).1" +sysroot_ubuntu_codename="$(lsb_release -cs)" +cross_prefix="${root_path}/qt-cross" +toolchain_config="${root_path}/.ci/qt6-toolchain.cmake" case "$target_arch" in aarch64) - toolchain_prefix="aarch64-linux-gnu-" - echo "APPIMAGE_ARCH=aarch64" >> "${GITHUB_ENV}" - ;; - armv7) - toolchain_prefix="arm-linux-gnueabihf-" - echo "APPIMAGE_ARCH=armhf" >> "${GITHUB_ENV}" + target_arch_name="armv8-a" + target_arch_debian_name="arm64" + toolchain_name="aarch64-linux-gnu" + target_platform="linux-aarch64-gnu-g++" ;; esac -echo "$(pwd)/qt-cross/bin:$(pwd)/qt-host/libexec" >> $GITHUB_PATH -echo "LD_LIBRARY_PATH=$(pwd)/qt-cross/lib:$(pwd)/qt-host/lib" >> "${GITHUB_ENV}" -.ci/install-cross-compiler.sh "$target_arch" -.ci/qt6-dependencies.sh -if [[ "$target_arch" == "armv7" ]]; then - echo "QMAKE_CC=arm-linux-gnueabihf-gcc - QMAKE_CXX=arm-linux-gnueabihf-g++ - QMAKE_LINK=arm-linux-gnueabihf-g++ - QMAKE_LINK_SHLIB=arm-linux-gnueabihf-g++ - QMAKE_AR=arm-linux-gnueabihf-ar cqs - QMAKE_OBJCOPY=arm-linux-gnueabihf-objcopy - QMAKE_NM=arm-linux-gnueabihf-nm -P - QMAKE_STRIP=arm-linux-gnueabihf-strip" >> .qmake.conf -fi - -# Prepare cross-tools for linuxdeploy -sudo cp /usr/bin/${toolchain_prefix}strip strip -sudo mv /usr/bin/ldd /usr/bin/ldd-amd64 -sudo cp .ci/bin/xldd /usr/bin/${toolchain_prefix}ldd -sudo ln -s /usr/bin/${toolchain_prefix}ldd /usr/bin/ldd -echo "CT_XLDD_ROOT=$(pwd)/sysroot" >> "${GITHUB_ENV}" +toolchain_prefix="${toolchain_name}-" + +echo "Target architecture: ${target_arch} (${target_arch_name})" + +# Install dependencies +${root_path}/.ci/install_cross_compiler.sh "${target_arch}" || exit 1 +sudo apt install -y qemu-user-static || exit 1 + +# Prepare sysroot +echo "Preparing sysroot..." +curl "https://cdimage.ubuntu.com/ubuntu-base/releases/${sysroot_ubuntu_codename}/release/ubuntu-base-${sysroot_ubuntu_version}-base-${target_arch_debian_name}.tar.gz" > ./ubuntu-base.tar.gz || exit 1 +mkdir -p "$sysroot_path" +sudo tar -xvzf ubuntu-base.tar.gz -C "$sysroot_path" || exit 1 +rm ubuntu-base.tar.gz +sudo update-binfmts --enable qemu-arm || exit 1 +sudo mount -o bind /dev "${sysroot_path}/dev" || exit 1 +sudo cp /etc/resolv.conf "${sysroot_path}/etc" || exit 1 +sudo chmod 1777 "${sysroot_path}/tmp" || exit 1 +sudo cp "${root_path}/.ci/qt6_deps.sh" "${sysroot_path}/" +sudo chroot "$sysroot_path" /bin/bash -c "/qt6_deps.sh" || exit 1 +sudo chroot "$sysroot_path" /bin/bash -c "apt update && apt install -y symlinks libssl-dev && symlinks -rc /" || exit 1 +sudo umount "${sysroot_path}/dev" || exit 1 + +# Prepare environment +echo "BUILD_SYSROOT_PATH=$sysroot_path" >> "$GITHUB_ENV" +echo "BUILD_TOOLCHAIN_NAME=$toolchain_name" >> "$GITHUB_ENV" +echo "BUILD_TOOLCHAIN_PREFIX=$toolchain_prefix" >> "$GITHUB_ENV" +echo "BUILD_TOOLCHAIN_CONFIG=$toolchain_config" >> "$GITHUB_ENV" +echo "BUILD_ARCH_NAME=$target_arch_name" >> "$GITHUB_ENV" +echo "BUILD_PLATFORM=$target_platform" >> "$GITHUB_ENV" +echo "QT_ROOT_DIR=$cross_prefix" >> "$GITHUB_ENV" diff --git a/.ci/qt6-toolchain.cmake b/.ci/qt6-toolchain.cmake new file mode 100644 index 0000000..babfa69 --- /dev/null +++ b/.ci/qt6-toolchain.cmake @@ -0,0 +1,79 @@ +cmake_minimum_required(VERSION 3.18) +include_guard(GLOBAL) + +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_PROCESSOR arm) + +set(TARGET_SYSROOT $ENV{BUILD_SYSROOT_PATH}) +set(CMAKE_SYSROOT ${TARGET_SYSROOT}) + +set(ENV{PKG_CONFIG_PATH} $PKG_CONFIG_PATH:/usr/lib/$ENV{BUILD_TOOLCHAIN_NAME}/pkgconfig) +set(ENV{PKG_CONFIG_LIBDIR} /usr/lib/pkgconfig:/usr/share/pkgconfig/:${TARGET_SYSROOT}/usr/lib/$ENV{BUILD_TOOLCHAIN_NAME}/pkgconfig:${TARGET_SYSROOT}/usr/lib/pkgconfig) +set(ENV{PKG_CONFIG_SYSROOT_DIR} ${CMAKE_SYSROOT}) + +set(CMAKE_C_COMPILER /usr/bin/$ENV{BUILD_TOOLCHAIN_PREFIX}gcc) +set(CMAKE_CXX_COMPILER /usr/bin/$ENV{BUILD_TOOLCHAIN_PREFIX}g++) + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -I${TARGET_SYSROOT}/usr/include") +set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}") + +set(QT_COMPILER_FLAGS "-march=$ENV{BUILD_ARCH_NAME}") +set(QT_COMPILER_FLAGS_RELEASE "-O2 -pipe") +set(QT_LINKER_FLAGS "-Wl,-O1 -Wl,--hash-style=gnu -Wl,--as-needed") + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) +set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) +set(CMAKE_BUILD_RPATH ${TARGET_SYSROOT}) + + +include(CMakeInitializeConfigs) + +function(cmake_initialize_per_config_variable _PREFIX _DOCSTRING) + if (_PREFIX MATCHES "CMAKE_(C|CXX|ASM)_FLAGS") + set(CMAKE_${CMAKE_MATCH_1}_FLAGS_INIT "${QT_COMPILER_FLAGS}") + + foreach (config DEBUG RELEASE MINSIZEREL RELWITHDEBINFO) + if (DEFINED QT_COMPILER_FLAGS_${config}) + set(CMAKE_${CMAKE_MATCH_1}_FLAGS_${config}_INIT "${QT_COMPILER_FLAGS_${config}}") + endif() + endforeach() + endif() + + + if (_PREFIX MATCHES "CMAKE_(SHARED|MODULE|EXE)_LINKER_FLAGS") + foreach (config SHARED MODULE EXE) + set(CMAKE_${config}_LINKER_FLAGS_INIT "${QT_LINKER_FLAGS}") + endforeach() + endif() + + _cmake_initialize_per_config_variable(${ARGV}) +endfunction() + +set(XCB_PATH_VARIABLE ${TARGET_SYSROOT}) + +set(GL_INC_DIR ${TARGET_SYSROOT}/usr/include) +set(GL_LIB_DIR ${TARGET_SYSROOT}:${TARGET_SYSROOT}/usr/lib/$ENV{BUILD_TOOLCHAIN_NAME}/:${TARGET_SYSROOT}/usr:${TARGET_SYSROOT}/usr/lib) + +set(EGL_INCLUDE_DIR ${GL_INC_DIR}) +set(EGL_LIBRARY ${XCB_PATH_VARIABLE}/usr/lib/$ENV{BUILD_TOOLCHAIN_NAME}/libEGL.so) + +set(OPENGL_INCLUDE_DIR ${GL_INC_DIR}) +set(OPENGL_opengl_LIBRARY ${XCB_PATH_VARIABLE}/usr/lib/$ENV{BUILD_TOOLCHAIN_NAME}/libOpenGL.so) + +set(GLESv2_INCLUDE_DIR ${GL_INC_DIR}) +set(GLIB_LIBRARY ${XCB_PATH_VARIABLE}/usr/lib/$ENV{BUILD_TOOLCHAIN_NAME}/libGLESv2.so) + +set(GLESv2_INCLUDE_DIR ${GL_INC_DIR}) +set(GLESv2_LIBRARY ${XCB_PATH_VARIABLE}/usr/lib/$ENV{BUILD_TOOLCHAIN_NAME}/libGLESv2.so) + +set(gbm_INCLUDE_DIR ${GL_INC_DIR}) +set(gbm_LIBRARY ${XCB_PATH_VARIABLE}/usr/lib/$ENV{BUILD_TOOLCHAIN_NAME}/libgbm.so) + +set(Libdrm_INCLUDE_DIR ${GL_INC_DIR}) +set(Libdrm_LIBRARY ${XCB_PATH_VARIABLE}/usr/lib/$ENV{BUILD_TOOLCHAIN_NAME}/libdrm.so) + +set(XCB_XCB_INCLUDE_DIR ${GL_INC_DIR}) +set(XCB_XCB_LIBRARY ${XCB_PATH_VARIABLE}/usr/lib/$ENV{BUILD_TOOLCHAIN_NAME}/libxcb.so) diff --git a/.ci/qt6_deps.sh b/.ci/qt6_deps.sh index 15d564a..ab13bcf 100755 --- a/.ci/qt6_deps.sh +++ b/.ci/qt6_deps.sh @@ -33,6 +33,6 @@ sudo apt install -y libboost-all-dev libudev-dev libinput-dev libts-dev \ libxcb-glx0-dev libxi-dev libdrm-dev libxcb-xinerama0 libxcb-xinerama0-dev \ libatspi2.0-dev libxcursor-dev libxcomposite-dev libxdamage-dev libxss-dev \ libxtst-dev libpci-dev libcap-dev libxrandr-dev libdirectfb-dev libaudio-dev \ - libxkbcommon-x11-dev libclang-dev libclang-12-dev || exit 1 + libxkbcommon-x11-dev libclang-dev libclang-*-dev || exit 1 #sudo apt install -y gstreamer1.0-omx || exit 1 diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml index 06f8776..f1293c0 100644 --- a/.github/workflows/linux-build.yml +++ b/.github/workflows/linux-build.yml @@ -12,12 +12,26 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - qt-version: ['6.7'] - qt-target: ['desktop'] - qt-modules: [''] - arch: ['amd64'] - ubuntu-version: ['20.04'] + include: + - qt-version: '6.8' + qt-target: 'desktop' + qt-modules: '' + arch: 'amd64' + - qt-version: '6.8.1' + qt-target: 'desktop' + qt-modules: 'qtshadertools' + arch: 'aarch64' steps: + - name: Maximize build space + uses: easimon/maximize-build-space@master + with: + root-reserve-mb: 512 + swap-size-mb: 1024 + remove-dotnet: true + remove-android: true + remove-haskell: true + remove-codeql: true + remove-docker-images: true - uses: actions/checkout@v3 with: fetch-depth: 0 @@ -34,43 +48,64 @@ jobs: sudo apt-get update sudo apt-get install -y libxkbcommon-x11-0 shell: bash + - name: Install GCC11 + shell: bash + run: | + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + sudo apt update + sudo apt install gcc-11 g++-11 + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 110 --slave /usr/bin/g++ g++ /usr/bin/g++-11 --slave /usr/bin/gcov gcov /usr/bin/gcov-11 ## Install Qt - - if: contains(matrix.arch, 'amd64') - name: Install Qt (Ubuntu) + - name: Install Qt uses: jurplel/install-qt-action@v4 with: version: ${{ matrix.qt-version }} - host: 'linux' - arch: 'linux_gcc_64' + arch: linux_gcc_64 target: ${{ matrix.qt-target }} modules: ${{ matrix.qt-modules }} + dir: ${{ github.workspace }}/Qt_host + - name: Set host Qt path + run: echo "QT_HOST_PATH=${QT_ROOT_DIR}" >> "${GITHUB_ENV}" + shell: bash - if: "!contains(matrix.arch, 'amd64')" name: Restore cross-compiled Qt from cache - id: cache-qt-cross - uses: actions/cache@v3 + id: restore-qt-cross + uses: jlanga/cache/restore@remove-files with: - path: | - ./qt-host/ - ./qt-cross/ - ./sysroot/ + path: ./qt-cross/ key: qt-cross-${{ runner.os }}-${{ matrix.qt-version }}-${{ matrix.qt-target }}-${{ matrix.qt-modules }}-${{ matrix.arch }} - restore-keys: - qt-cross-${{ runner.os }}-${{ matrix.qt-version }}-${{ matrix.qt-target }}-${{ matrix.qt-modules }}-${{ matrix.arch }} - - if: "!contains(matrix.arch, 'amd64') && steps.cache-qt-cross.outputs.cache-hit != 'true'" - name: Cross-compile Qt + - if: "!contains(matrix.arch, 'amd64')" + name: Set cross Qt path + run: echo "QT_CROSS_PATH=$(pwd)/qt-cross" >> "$GITHUB_ENV" shell: bash - run: .ci/build_qt6.sh "${{ matrix.qt-version }}" "${{ matrix.qt-modules }}" "${{ matrix.arch }}" - ## Build - if: "!contains(matrix.arch, 'amd64')" name: Prepare cross-compilation environment run: .ci/prepare_cross_build.sh "${{ matrix.arch }}" shell: bash + - if: "!contains(matrix.arch, 'amd64') && steps.restore-qt-cross.outputs.cache-hit != 'true'" + name: Cross-compile Qt + shell: bash + run: .ci/build_qt6.sh "${{ matrix.qt-version }}" "${{ matrix.qt-modules }}" "${{ matrix.arch }}" + - if: "!contains(matrix.arch, 'amd64') && steps.restore-qt-cross.outputs.cache-hit != 'true'" + name: Cache and remove cross-compiled Qt + id: cache-qt-cross + uses: jlanga/cache/save@remove-files + with: + path: ./qt-cross/ + key: qt-cross-${{ runner.os }}-${{ matrix.qt-version }}-${{ matrix.qt-target }}-${{ matrix.qt-modules }}-${{ matrix.arch }} + - if: "!contains(matrix.arch, 'amd64') && steps.restore-qt-cross.outputs.cache-hit != 'true'" + name: Restore cross-compiled Qt from cache + uses: jlanga/cache/restore@remove-files + with: + path: ./qt-cross/ + key: qt-cross-${{ runner.os }}-${{ matrix.qt-version }}-${{ matrix.qt-target }}-${{ matrix.qt-modules }}-${{ matrix.arch }} + ## Build - name: Build AppImage - run: .ci/build_appimage.sh + run: .ci/build_appimage.sh ${{ matrix.arch }} shell: 'script -q -e -c "bash {0}"' ## Upload - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: build-Qt-${{ matrix.qt-version }}-${{ matrix.arch }} path: | diff --git a/.github/workflows/macos-build.yml b/.github/workflows/macos-build.yml new file mode 100644 index 0000000..964c090 --- /dev/null +++ b/.github/workflows/macos-build.yml @@ -0,0 +1,48 @@ +name: macOS Build + +on: + push: + branches: '*' + tags: '*' + pull_request: + branches: [ "master" ] + +jobs: + build: + runs-on: macos-14 + strategy: + matrix: + os: [macos-14] + qt-version: ['6.8'] + qt-target: ['desktop'] + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: 'recursive' + - name: Setup environment + run: | + sed -i -e '/^#/d' .github/config.env + sed -i -e '/^$/d' .github/config.env + cat .github/config.env >> "${GITHUB_ENV}" + shell: bash + - name: Set up node.js + uses: actions/setup-node@v3 + # Install Qt + - name: Install Qt + uses: jurplel/install-qt-action@v3 + with: + version: ${{ matrix.qt-version }} + host: 'mac' + target: ${{ matrix.qt-target }} + modules: '' + # Build + - name: Build + run: .ci/macos_build.sh + shell: bash + # Upload + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: build-Qt-${{ matrix.qt-version }} + path: '*.dmg' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0d071b3..4b7d3ef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,11 +9,26 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - qt-version: ['6.7'] - qt-target: ['desktop'] - qt-modules: [''] - arch: ['amd64'] + include: + - qt-version: '6.8' + qt-target: 'desktop' + qt-modules: '' + arch: 'amd64' + - qt-version: '6.8.1' + qt-target: 'desktop' + qt-modules: 'qtshadertools' + arch: 'aarch64' steps: + - name: Maximize build space + uses: easimon/maximize-build-space@master + with: + root-reserve-mb: 512 + swap-size-mb: 1024 + remove-dotnet: true + remove-android: true + remove-haskell: true + remove-codeql: true + remove-docker-images: true - uses: actions/checkout@v4 with: fetch-depth: 0 @@ -35,6 +50,13 @@ jobs: sudo apt-get update sudo apt-get install -y libxkbcommon-x11-0 shell: bash + - name: Install GCC11 + shell: bash + run: | + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + sudo apt update + sudo apt install gcc-11 g++-11 + sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 110 --slave /usr/bin/g++ g++ /usr/bin/g++-11 --slave /usr/bin/gcov gcov /usr/bin/gcov-11 - name: Get version run: | version=$(LC_ALL=en_US.utf8 grep -oP 'project\([^)]*\s+VERSION\s+\K[0-9]+\.[0-9]+\.[0-9]+' CMakeLists.txt) @@ -46,42 +68,56 @@ jobs: run: echo is_prerelease=1 >> "${GITHUB_ENV}" shell: bash # Install Qt - - if: contains(matrix.arch, 'amd64') - name: Install Qt + - name: Install Qt uses: jurplel/install-qt-action@v4 with: version: ${{ matrix.qt-version }} - host: 'linux' - arch: 'linux_gcc_64' + arch: linux_gcc_64 target: ${{ matrix.qt-target }} modules: ${{ matrix.qt-modules }} + dir: ${{ github.workspace }}/Qt_host + - name: Set host Qt path + run: echo "QT_HOST_PATH=${QT_ROOT_DIR}" >> "${GITHUB_ENV}" + shell: bash - if: "!contains(matrix.arch, 'amd64')" name: Restore cross-compiled Qt from cache - id: cache-qt-cross - uses: actions/cache@v3 + id: restore-qt-cross + uses: jlanga/cache/restore@remove-files with: - path: | - ./qt-host/ - ./qt-cross/ - ./sysroot/ + path: ./qt-cross/ key: qt-cross-${{ runner.os }}-${{ matrix.qt-version }}-${{ matrix.qt-target }}-${{ matrix.qt-modules }}-${{ matrix.arch }} - restore-keys: - qt-cross-${{ runner.os }}-${{ matrix.qt-version }}-${{ matrix.qt-target }}-${{ matrix.qt-modules }}-${{ matrix.arch }} - - if: "!contains(matrix.arch, 'amd64') && steps.cache-qt-cross.outputs.cache-hit != 'true'" - name: Cross-compile Qt + - if: "!contains(matrix.arch, 'amd64')" + name: Set cross Qt path + run: echo "QT_CROSS_PATH=$(pwd)/qt-cross" >> "$GITHUB_ENV" shell: bash - run: .ci/build_qt6.sh "${{ matrix.qt-version }}" "${{ matrix.qt-modules }}" "${{ matrix.arch }}" - # Build - if: "!contains(matrix.arch, 'amd64')" name: Prepare cross-compilation environment run: .ci/prepare_cross_build.sh "${{ matrix.arch }}" shell: bash + - if: "!contains(matrix.arch, 'amd64') && steps.restore-qt-cross.outputs.cache-hit != 'true'" + name: Cross-compile Qt + shell: bash + run: .ci/build_qt6.sh "${{ matrix.qt-version }}" "${{ matrix.qt-modules }}" "${{ matrix.arch }}" + - if: "!contains(matrix.arch, 'amd64') && steps.restore-qt-cross.outputs.cache-hit != 'true'" + name: Cache and remove cross-compiled Qt + id: cache-qt-cross + uses: jlanga/cache/save@remove-files + with: + path: ./qt-cross/ + key: qt-cross-${{ runner.os }}-${{ matrix.qt-version }}-${{ matrix.qt-target }}-${{ matrix.qt-modules }}-${{ matrix.arch }} + - if: "!contains(matrix.arch, 'amd64') && steps.restore-qt-cross.outputs.cache-hit != 'true'" + name: Restore cross-compiled Qt from cache + uses: jlanga/cache/restore@remove-files + with: + path: ./qt-cross/ + key: qt-cross-${{ runner.os }}-${{ matrix.qt-version }}-${{ matrix.qt-target }}-${{ matrix.qt-modules }}-${{ matrix.arch }} + # Build - name: Build AppImage - run: .ci/build_appimage.sh + run: .ci/build_appimage.sh ${{ matrix.arch }} shell: 'script -q -e -c "bash {0}"' # Upload artifacts - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: build-linux-${{ matrix.arch }} path: | @@ -92,7 +128,7 @@ jobs: runs-on: windows-latest strategy: matrix: - qt-version: ['6.7'] + qt-version: ['6.8'] qt-target: ['desktop'] qt-modules: [''] mingw-version: ['11.2.0'] @@ -183,11 +219,66 @@ jobs: shell: bash # Upload artifacts - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: build-windows path: '*installer.exe' + build-macos-release: + runs-on: macos-14 + strategy: + matrix: + os: [macos-14] + qt-version: ['6.8'] + qt-target: ['desktop'] + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + submodules: 'recursive' + - name: Setup environment + run: | + sed -i -e '/^#/d' .github/config.env + sed -i -e '/^$/d' .github/config.env + cat .github/config.env >> "${GITHUB_ENV}" + shell: bash + - name: Set up node.js + uses: actions/setup-node@v3 + - if: env.build_on_new_tags != 1 + name: Cancel if build on new tags is disabled + uses: andymckay/cancel-action@0.2 + - name: Get version + run: | + brew install grep + version=$(LC_ALL=en_US.utf8 ggrep -oP 'project\([^)]*\s+VERSION\s+\K[0-9]+\.[0-9]+\.[0-9]+' CMakeLists.txt) + echo "Project version: $version" + echo previous_tag=$version >> "${GITHUB_ENV}" + shell: bash + - if: contains(github.ref, '-') + name: Check if this is a pre-release + run: echo is_prerelease=1 >> "${GITHUB_ENV}" + shell: bash + - name: Set up node.js + uses: actions/setup-node@v3 + # Install Qt + - name: Install Qt + uses: jurplel/install-qt-action@v3 + with: + version: ${{ matrix.qt-version }} + host: 'mac' + target: ${{ matrix.qt-target }} + modules: '' + # Build + - name: Build + run: .ci/macos_build.sh + shell: bash + # Upload + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: build-macos + path: '*.dmg' + # Release release: runs-on: ubuntu-latest @@ -239,8 +330,7 @@ jobs: name: Create release uses: ncipollo/release-action@v1 with: - #artifacts: "build-windows/*.exe,build-linux-*/*.AppImage,build-linux-*/*.zsync,build-macos/*.dmg" - artifacts: "build-windows/*.exe,build-linux-*/*.AppImage,build-linux-*/*.zsync" + artifacts: "build-windows/*.exe,build-linux-*/*.AppImage,build-linux-*/*.zsync,build-macos/*.dmg" name: "${{ env.app_name }} ${{ env.version }}" owner: ${{ github.event.pusher.name }} draft: true diff --git a/.github/workflows/utests.yml b/.github/workflows/utests.yml index c0ca8bc..60d23e9 100644 --- a/.github/workflows/utests.yml +++ b/.github/workflows/utests.yml @@ -26,7 +26,7 @@ jobs: - name: Install Qt uses: jurplel/install-qt-action@v4 with: - version: '6.7.*' + version: '6.8.*' arch: 'linux_gcc_64' - name: Configure CMake run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DSCRATCHCPP_PLAYER_BUILD_UNIT_TESTS=ON diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index f087c6a..d6f3f80 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -12,7 +12,7 @@ jobs: runs-on: windows-latest strategy: matrix: - qt-version: ['6.7'] + qt-version: ['6.8'] qt-target: ['desktop'] qt-modules: [''] mingw-version: ['11.2.0'] @@ -75,7 +75,7 @@ jobs: shell: bash ## Upload - name: Upload artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: build-Qt-${{ matrix.qt-version }} path: win_release/ @@ -98,7 +98,7 @@ jobs: shell: bash # Upload installer - name: Upload installer - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: installer-Qt-${{ matrix.qt-version }} path: '*installer.exe' diff --git a/CMakeLists.txt b/CMakeLists.txt index 515ed40..fbf5734 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.16) -project(scratchcpp-player VERSION 0.3.0 LANGUAGES CXX) +project(scratchcpp-player VERSION 0.7.0 LANGUAGES CXX) set(CMAKE_AUTOMOC ON) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -10,6 +10,11 @@ option(SCRATCHCPP_PLAYER_BUILD_UNIT_TESTS "Build unit tests" ON) find_package(Qt6 6.6 COMPONENTS Quick QuickControls2 Widgets OpenGLWidgets REQUIRED) set(QT_LIBS Qt6::Quick Qt6::QuickControls2 Qt6::Widgets Qt6::OpenGLWidgets) +if (LINUX) + find_package(Qt6 6.6 COMPONENTS DBus REQUIRED) + set(QT_LIBS ${QT_LIBS} Qt6::DBus) +endif() + if (SCRATCHCPP_PLAYER_BUILD_UNIT_TESTS) set(GTEST_DIR thirdparty/googletest) add_subdirectory(${PROJECT_SOURCE_DIR}/${GTEST_DIR} ${CMAKE_CURRENT_BINARY_DIR}/${GTEST_DIR}) diff --git a/README.md b/README.md index ab9525a..5f8c6c7 100644 --- a/README.md +++ b/README.md @@ -109,9 +109,9 @@ When the project loads, click on the green flag button to run it. - [x] Turbo mode - [x] FPS options - [x] Stage size options -- [ ] HQ pen -- [ ] Mute sounds -- [ ] Theme options (light/dark mode, accent color, etc.) +- [x] HQ pen +- [x] Mute sounds +- [x] Theme options (light/dark mode, accent color, etc.) See the [open issues](https://github.com/scratchcpp/scratchcpp-player/issues) for a full list of proposed features (and known issues). diff --git a/res/macos-release/bg.png b/res/macos-release/bg.png new file mode 100644 index 0000000..3f2fbf1 Binary files /dev/null and b/res/macos-release/bg.png differ diff --git a/res/macos-release/scratchcpp-player.icns b/res/macos-release/scratchcpp-player.icns new file mode 100644 index 0000000..2e8f5bc Binary files /dev/null and b/res/macos-release/scratchcpp-player.icns differ diff --git a/res/macos-release/scratchcpp-player.json b/res/macos-release/scratchcpp-player.json new file mode 100644 index 0000000..1f9be64 --- /dev/null +++ b/res/macos-release/scratchcpp-player.json @@ -0,0 +1,9 @@ +{ + "title": "ScratchCPP Player", + "icon": "scratchcpp-player.icns", + "background": "bg.png", + "contents": [ + { "x": 448, "y": 344, "type": "link", "path": "/Applications" }, + { "x": 192, "y": 344, "type": "file", "path": "scratchcpp-player.app" } + ] +} diff --git a/scratchcpp-render b/scratchcpp-render index 3b639b2..e96c348 160000 --- a/scratchcpp-render +++ b/scratchcpp-render @@ -1 +1 @@ -Subproject commit 3b639b29f495132a7fed206c6de0c821091c64fc +Subproject commit e96c3487005c8b086c95dbe14f5d8fb90e9cf50a diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 3ec2ccd..94eaa90 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -8,17 +8,24 @@ qt_add_executable(${APP_TARGET} appmenubar.h libraryinfo.cpp libraryinfo.h + MANUAL_FINALIZATION ) +set_source_files_properties(qml/Colors.qml PROPERTIES QT_QML_SINGLETON_TYPE TRUE) + qt_add_qml_module(${APP_TARGET} URI ScratchCPP VERSION 1.0 QML_FILES qml/main.qml + qml/Colors.qml qml/dialogs/AboutDialog.qml qml/dialogs/ProjectSettingsDialog.qml + qml/dialogs/PreferencesDialog.qml ) +qt_finalize_executable(${APP_TARGET}) + set(QML_IMPORT_PATH "${QML_IMPORT_PATH};${CMAKE_CURRENT_LIST_DIR}" CACHE STRING "Qt Creator extra QML import paths" FORCE diff --git a/src/app/app.cpp b/src/app/app.cpp index cd7c871..8148f11 100644 --- a/src/app/app.cpp +++ b/src/app/app.cpp @@ -36,6 +36,10 @@ int App::run(int argc, char **argv) QCoreApplication::setApplicationName("ScratchCPP Player"); QCoreApplication::setApplicationVersion(BUILD_VERSION); +#ifdef Q_OS_MACOS + // Disable native menu bar on macOS + app.setAttribute(Qt::AA_DontUseNativeMenuBar); +#endif // Set style and icon theme name QQuickStyle::setStyle("Material"); QIcon::setThemeName("scratchcpp"); diff --git a/src/app/appmenubar.cpp b/src/app/appmenubar.cpp index 24229c1..6bd8b9c 100644 --- a/src/app/appmenubar.cpp +++ b/src/app/appmenubar.cpp @@ -69,6 +69,17 @@ AppMenuBar::AppMenuBar(QObject *parent) : m_editMenu->addItem(m_projectSettingsItem); connect(m_projectSettingsItem, &MenuItemModel::clicked, this, &AppMenuBar::projectSettingsTriggered); + // Edit -> (separator) + m_editSeparator = new MenuItemModel(m_editMenu); + m_editSeparator->setIsSeparator(true); + m_editMenu->addItem(m_editSeparator); + + // Edit -> Preferences + m_preferencesItem = new MenuItemModel(m_editMenu); + m_preferencesItem->setText(tr("Preferences...")); + m_editMenu->addItem(m_preferencesItem); + connect(m_preferencesItem, &MenuItemModel::clicked, this, &AppMenuBar::preferencesTriggered); + // Help menu m_helpMenu = new MenuModel(m_model); m_helpMenu->setTitle(tr("&Help")); diff --git a/src/app/appmenubar.h b/src/app/appmenubar.h index d957c84..5925021 100644 --- a/src/app/appmenubar.h +++ b/src/app/appmenubar.h @@ -52,6 +52,7 @@ class AppMenuBar : public QObject void fps60ModeChanged(); void muteChanged(); void projectSettingsTriggered(); + void preferencesTriggered(); void aboutAppTriggered(); private: @@ -72,6 +73,8 @@ class AppMenuBar : public QObject uicomponents::MenuItemModel *m_fps60ModeItem = nullptr; uicomponents::MenuItemModel *m_muteItem = nullptr; uicomponents::MenuItemModel *m_projectSettingsItem = nullptr; + uicomponents::MenuItemModel *m_editSeparator = nullptr; + uicomponents::MenuItemModel *m_preferencesItem = nullptr; uicomponents::MenuModel *m_helpMenu = nullptr; uicomponents::MenuItemModel *m_aboutAppItem = nullptr; diff --git a/src/app/qml/Colors.qml b/src/app/qml/Colors.qml new file mode 100644 index 0000000..4b6af59 --- /dev/null +++ b/src/app/qml/Colors.qml @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +pragma Singleton +import QtQuick +import ScratchCPP.Ui + +QtObject { + readonly property list darkAccentColors: [ + Qt.rgba(0.85, 0.31, 0.33, 1), + Qt.rgba(0.85, 0.62, 0.31, 1), + Qt.rgba(0.85, 0.84, 0.31, 1), + Qt.rgba(0.39, 0.85, 0.31, 1), + Qt.rgba(0.31, 0.75, 0.85, 1), + Qt.rgba(0.32, 0.32, 0.85, 1), + Qt.rgba(0.68, 0.31, 0.85, 1), + ] + + readonly property list lightAccentColors: [ + Qt.rgba(0.75, 0.08, 0.09, 1), + Qt.rgba(0.75, 0.47, 0.08, 1), + Qt.rgba(0.75, 0.74, 0.08, 1), + Qt.rgba(0.17, 0.75, 0.08, 1), + Qt.rgba(0.08, 0.63, 0.75, 1), + Qt.rgba(0.08, 0.08, 0.75, 1), + Qt.rgba(0.54, 0.08, 0.75, 1), + ] + + readonly property color defaultAccentColor: ThemeEngine.theme == ThemeEngine.DarkTheme ? darkAccentColors[1] : lightAccentColors[1] +} diff --git a/src/app/qml/dialogs/AboutDialog.qml b/src/app/qml/dialogs/AboutDialog.qml index 0a704a2..bee2253 100644 --- a/src/app/qml/dialogs/AboutDialog.qml +++ b/src/app/qml/dialogs/AboutDialog.qml @@ -4,6 +4,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import ScratchCPP +import ScratchCPP.Global import ScratchCPP.Ui import ScratchCPP.UiComponents diff --git a/src/app/qml/dialogs/PreferencesDialog.qml b/src/app/qml/dialogs/PreferencesDialog.qml new file mode 100644 index 0000000..b7bbc99 --- /dev/null +++ b/src/app/qml/dialogs/PreferencesDialog.qml @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import ScratchCPP.Global +import ScratchCPP.Ui +import ScratchCPP.UiComponents + +import ".." + +CustomDialog { + title: qsTr("Preferences") + standardButtons: Dialog.Cancel | Dialog.Ok + onOpened: Settings.freeze() + onAccepted: Settings.saveChanges() + onRejected: { + Settings.discardChanges(); + ThemeEngine.reloadTheme(); + } + + QtObject { + id: priv + property int accentColorIndex: -1 + } + + contentItem: ColumnLayout { + // Themes + Label { + text: qsTr("Themes") + font.pointSize: 14 + font.bold: true + } + + RowLayout { + RadioButton { + text: qsTr("Light") + checked: ThemeEngine.theme === ThemeEngine.LightTheme + onCheckedChanged: if(checked) ThemeEngine.theme = ThemeEngine.LightTheme + } + + RadioButton { + text: qsTr("Dark") + checked: ThemeEngine.theme === ThemeEngine.DarkTheme + onCheckedChanged: if(checked) ThemeEngine.theme = ThemeEngine.DarkTheme + } + } + + RowLayout { + Label { + text: qsTr("Accent color:") + } + + Repeater { + id: accentColors + model: ThemeEngine.theme == ThemeEngine.DarkTheme ? Colors.darkAccentColors : Colors.lightAccentColors + + ColorButton { + required property color modelData + required property int index + color: modelData + checked: { + if(ThemeEngine.accentColor === modelData) { + priv.accentColorIndex = index; + return true; + } else { + return false; + } + } + autoExclusive: true + checkable: true + onPressed: ThemeEngine.accentColor = modelData; + } + } + } + + Connections { + target: ThemeEngine + + function onThemeChanged() { + if(ThemeEngine.theme == ThemeEngine.DarkTheme) { + ThemeEngine.accentColor = Colors.darkAccentColors[priv.accentColorIndex]; + } else { + ThemeEngine.accentColor = Colors.lightAccentColors[priv.accentColorIndex]; + } + } + } + } +} diff --git a/src/app/qml/dialogs/ProjectSettingsDialog.qml b/src/app/qml/dialogs/ProjectSettingsDialog.qml index 53fd2cd..74352e9 100644 --- a/src/app/qml/dialogs/ProjectSettingsDialog.qml +++ b/src/app/qml/dialogs/ProjectSettingsDialog.qml @@ -9,6 +9,7 @@ import ScratchCPP.Render CustomDialog { property ProjectPlayer projectPlayer: null + property bool showCurrentFps: false title: qsTr("Project settings") standardButtons: Dialog.Close @@ -44,6 +45,13 @@ CustomDialog { onCheckedChanged: projectPlayer.hqPen = checked } + CheckBox { + id: showFpsCheckBox + text: qsTr("Show current FPS") + checked: showCurrentFps + onCheckedChanged: showCurrentFps = checked + } + // Remove limits Label { text: qsTr("Remove limits") diff --git a/src/app/qml/main.qml b/src/app/qml/main.qml index 15e6c0a..aceacd2 100644 --- a/src/app/qml/main.qml +++ b/src/app/qml/main.qml @@ -16,9 +16,9 @@ ApplicationWindow { minimumHeight: menuBar.height + layout.implicitHeight + layout.anchors.margins * 2 visible: true title: Qt.application.displayName - color: Material.background - Material.accent: "orange" - Material.theme: Material.Dark + color: ThemeEngine.bgColor + Material.accent: ThemeEngine.accentColor + Material.theme: ThemeEngine.theme == ThemeEngine.DarkTheme ? Material.Dark : Material.Light onActiveFocusItemChanged: UiEngine.activeFocusItem = activeFocusItem menuBar: CustomMenuBar { @@ -44,6 +44,10 @@ ApplicationWindow { projectSettingsDialog.open(); } + function onPreferencesTriggered() { + preferencesDialog.open(); + } + function onAboutAppTriggered() { aboutDialog.open(); } @@ -57,6 +61,15 @@ ApplicationWindow { projectPlayer: player } + PreferencesDialog { id: preferencesDialog } + + CustomMessageDialog { + id: unsupportedBlocksDialog + title: qsTr("Warning") + text: qsTr("This project contains unsupported blocks:") + informativeText: player.unsupportedBlocks.join('\r\n') + } + ColumnLayout { id: layout anchors.fill: parent @@ -69,6 +82,7 @@ ApplicationWindow { id: greenFlagButton icon.name: "green_flag" icon.color: "transparent" + highlighted: player.running onClicked: { switch (KeyboardInfo.keyboardModifiers()) { case Qt.ShiftModifier: @@ -153,6 +167,34 @@ ApplicationWindow { focus: true turboMode: AppMenuBar.turboMode mute: AppMenuBar.mute + onLoaded: { + if(unsupportedBlocks.length > 0) + unsupportedBlocksDialog.open() + + currentFps.enable = true; + } + + Loader { + id: currentFps + property bool enable: false + active: projectSettingsDialog.showCurrentFps && enable + anchors.right: player.stageRect.right + anchors.top: player.stageRect.top + + sourceComponent: Text { + text: player.renderFps + font.pointSize: 20 * player.stageScale + font.family: "mono" + color: Qt.rgba(1, 0, 0, 0.75) + style: Text.Outline + styleColor: "black" + } + } } } + + Component.onCompleted: { + if(ThemeEngine.accentColor === Qt.rgba(0, 0, 0, 0)) + ThemeEngine.accentColor = Colors.defaultAccentColor; + } } diff --git a/src/global/CMakeLists.txt b/src/global/CMakeLists.txt index 9b9c2a0..4acc8de 100644 --- a/src/global/CMakeLists.txt +++ b/src/global/CMakeLists.txt @@ -4,12 +4,18 @@ set(MODULE_SRC globalmodule.cpp globalmodule.h iappinfo.h + ifilepaths.h + isettings.h modularity/ioc.h modularity/modulesioc.h modularity/imoduleexportinterface.h modularity/imodulesetup.h internal/appinfo.cpp internal/appinfo.h + internal/filepaths.cpp + internal/filepaths.h + internal/settings.cpp + internal/settings.h ) include(${PROJECT_SOURCE_DIR}/build/module.cmake) diff --git a/src/global/globalmodule.cpp b/src/global/globalmodule.cpp index c0cba8d..b79055b 100644 --- a/src/global/globalmodule.cpp +++ b/src/global/globalmodule.cpp @@ -4,6 +4,8 @@ #include "globalmodule.h" #include "internal/appinfo.h" +#include "internal/filepaths.h" +#include "internal/settings.h" using namespace scratchcpp; @@ -17,6 +19,14 @@ void GlobalModule::registerExports() m_appInfo = std::make_shared(); QQmlEngine::setObjectOwnership(m_appInfo.get(), QQmlEngine::CppOwnership); - qmlRegisterSingletonInstance("ScratchCPP.Ui", 1, 0, "AppInfo", m_appInfo.get()); + qmlRegisterSingletonInstance("ScratchCPP.Global", 1, 0, "AppInfo", m_appInfo.get()); modularity::ioc()->registerExport(m_appInfo); + + modularity::ioc()->registerExport(FilePaths::instance()); + + m_settings = std::make_shared(); + + QQmlEngine::setObjectOwnership(m_settings.get(), QQmlEngine::CppOwnership); + qmlRegisterSingletonInstance("ScratchCPP.Global", 1, 0, "Settings", m_settings.get()); + modularity::ioc()->registerExport(m_settings); } diff --git a/src/global/globalmodule.h b/src/global/globalmodule.h index 723f3df..750725e 100644 --- a/src/global/globalmodule.h +++ b/src/global/globalmodule.h @@ -10,6 +10,7 @@ namespace scratchcpp { class AppInfo; +class Settings; class GlobalModule : public modularity::IModuleSetup { @@ -20,6 +21,7 @@ class GlobalModule : public modularity::IModuleSetup private: std::shared_ptr m_appInfo; + std::shared_ptr m_settings; }; } // namespace scratchcpp diff --git a/src/global/ifilepaths.h b/src/global/ifilepaths.h new file mode 100644 index 0000000..68bd881 --- /dev/null +++ b/src/global/ifilepaths.h @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "modularity/ioc.h" + +namespace scratchcpp +{ + +class IFilePaths : MODULE_EXPORT_INTERFACE +{ + public: + virtual ~IFilePaths() { } + + virtual QString configLocation() const = 0; +}; + +} // namespace scratchcpp diff --git a/src/global/internal/filepaths.cpp b/src/global/internal/filepaths.cpp new file mode 100644 index 0000000..06cf347 --- /dev/null +++ b/src/global/internal/filepaths.cpp @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include + +#include "filepaths.h" + +using namespace scratchcpp; + +std::shared_ptr FilePaths::m_instance = std::make_shared(); + +std::shared_ptr FilePaths::instance() +{ + return m_instance; +} + +QString scratchcpp::FilePaths::configLocation() const +{ + return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/" + qApp->applicationName() + "/config.ini"; +} diff --git a/src/global/internal/filepaths.h b/src/global/internal/filepaths.h new file mode 100644 index 0000000..e6e936a --- /dev/null +++ b/src/global/internal/filepaths.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "ifilepaths.h" + +namespace scratchcpp +{ + +class FilePaths : public IFilePaths +{ + public: + static std::shared_ptr instance(); + + QString configLocation() const override; + + private: + static std::shared_ptr m_instance; +}; + +} // namespace scratchcpp diff --git a/src/global/internal/settings.cpp b/src/global/internal/settings.cpp new file mode 100644 index 0000000..1d6f379 --- /dev/null +++ b/src/global/internal/settings.cpp @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include + +#include "settings.h" +#include "ifilepaths.h" + +using namespace scratchcpp; + +Settings::Settings(QObject *parent) : + QObject(parent), + m_mainSettingsInstance(paths()->configLocation(), QSettings::IniFormat), + m_tmpSettingsInstance(paths()->configLocation() + ".tmp", QSettings::IniFormat) +{ + m_settingsInstance = &m_mainSettingsInstance; +} + +void Settings::addKey(const QString &moduleName, const QString &keyName, const QVariant &defaultValue) +{ + m_defaults.insert({ moduleName, keyName }, defaultValue); +} + +void Settings::setValue(const QString &moduleName, const QString &keyName, const QVariant &value) +{ + set(moduleName + "/" + keyName, value); +} + +QVariant Settings::getValue(const QString &moduleName, const QString &keyName) const +{ + QPair key(moduleName, keyName); + QVariant defaultValue; + auto it = m_defaults.find(key); + + if (it != m_defaults.cend()) + defaultValue = it.value(); + + return get(moduleName + "/" + keyName, defaultValue); +} + +bool Settings::containsKey(const QString &moduleName, const QString &keyName) const +{ + QPair key(moduleName, keyName); + return contains(moduleName + "/" + keyName); +} + +/* + * Switches to temporary settings. You can decide to saveChanges() or discardChanges() later. + * This is useful for settings dialogs with a discard button. + */ +void Settings::freeze(void) +{ + Q_ASSERT(!m_frozen); + m_settingsInstance = &m_tmpSettingsInstance; + copySettings(&m_mainSettingsInstance, m_settingsInstance); + m_frozen = true; + emit stateChanged(); +} + +// Saves changes to real settings and switches back to them. +void Settings::saveChanges(void) +{ + Q_ASSERT(m_frozen); + copySettings(m_settingsInstance, &m_mainSettingsInstance); + m_mainSettingsInstance.sync(); + m_settingsInstance = &m_mainSettingsInstance; + m_frozen = false; + emit stateChanged(); + emit saved(); +} + +// Discards changes and switches back to real settings. +void Settings::discardChanges(void) +{ + Q_ASSERT(m_frozen); + m_settingsInstance = &m_mainSettingsInstance; + m_frozen = false; + emit stateChanged(); + emit discarded(); +} + +bool Settings::isFrozen(void) const +{ + return m_frozen; +} + +QVariant Settings::get(const QString &key, const QVariant &defaultValue) const +{ + Q_ASSERT(m_settingsInstance != nullptr); +#ifdef Q_OS_WASM + if (m_settingsInstance->isWritable()) { + if (!m_tempSettingsCopied) + const_cast(this)->copyTempSettings(); + return m_settingsInstance->value(key, defaultValue); + } else { + // Use temporary settings until sandbox is initialized + QSettings settings(paths()->configLocation(), QSettings::IniFormat); + return settings.value(key, defaultValue); + } +#else + return m_settingsInstance->value(key, defaultValue); +#endif // Q_OS_WASM +} + +bool Settings::contains(const QString &key) const +{ + Q_ASSERT(m_settingsInstance != nullptr); +#ifdef Q_OS_WASM + if (m_settingsInstance->isWritable()) { + if (!m_tempSettingsCopied) + const_cast(this)->copyTempSettings(); + return m_settingsInstance->contains(key); + } else { + // Use temporary settings until sandbox is initialized + QSettings settings(paths()->configLocation(), QSettings::IniFormat); + return settings.contains(key); + } +#else + return m_settingsInstance->contains(key); +#endif // Q_OS_WASM +} + +void Settings::set(const QString &key, const QVariant &value) +{ + Q_ASSERT(m_settingsInstance != nullptr); +#ifdef Q_OS_WASM + if (m_settingsInstance->isWritable()) { + if (!m_tempSettingsCopied) + copyTempSettings(); + m_settingsInstance->setValue(key, value); + m_settingsInstance->sync(); + } else { + // Use temporary settings until sandbox is initialized + QSettings settings(paths()->configLocation(), QSettings::IniFormat); + settings.setValue(key, value); + } +#else + m_settingsInstance->setValue(key, value); +#endif // Q_OS_WASM +} + +void Settings::copySettings(QSettings *source, QSettings *target) +{ +#ifndef Q_OS_WASM + target->clear(); +#endif + QStringList keys = source->allKeys(); + for (int i = 0; i < keys.count(); i++) + target->setValue(keys[i], source->value(keys[i])); +} + +#ifdef Q_OS_WASM +void Settings::copyTempSettings(void) +{ + QSettings settings(paths()->configLocation(), QSettings::IniFormat); + copySettings(&settings, m_settingsInstance); + m_settingsInstance->sync(); + m_tempSettingsCopied = true; +} +#endif // Q_OS_WASM diff --git a/src/global/internal/settings.h b/src/global/internal/settings.h new file mode 100644 index 0000000..7348ac0 --- /dev/null +++ b/src/global/internal/settings.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +#include "isettings.h" +#include "ifilepaths.h" + +class QSettings; + +namespace scratchcpp +{ + +// class IFilePaths; + +class Settings + : public QObject + , public ISettings +{ + Q_OBJECT + INJECT(IFilePaths, paths) + public: + Settings(QObject *parent = nullptr); + + void addKey(const QString &moduleName, const QString &keyName, const QVariant &defaultValue) override; + Q_INVOKABLE void setValue(const QString &moduleName, const QString &keyName, const QVariant &value) override; + Q_INVOKABLE QVariant getValue(const QString &moduleName, const QString &keyName) const override; + Q_INVOKABLE bool containsKey(const QString &moduleName, const QString &keyName) const override; + Q_INVOKABLE void freeze(void) override; + Q_INVOKABLE void saveChanges(void) override; + Q_INVOKABLE void discardChanges(void) override; + Q_INVOKABLE bool isFrozen(void) const override; + + signals: + void stateChanged(); + void saved(); + void discarded(); + + private: + QVariant get(const QString &key, const QVariant &defaultValue) const; + bool contains(const QString &key) const; + void set(const QString &key, const QVariant &value); + + static void copySettings(QSettings *source, QSettings *target); + + QSettings *m_settingsInstance = nullptr; + QSettings m_mainSettingsInstance; + QSettings m_tmpSettingsInstance; + bool m_frozen = false; + QMap, QVariant> m_defaults; + +#ifdef Q_OS_WASM + bool m_tempSettingsCopied = false; + void copyTempSettings(void); +#endif // Q_OS_WASM +}; + +} // namespace scratchcpp diff --git a/src/global/isettings.h b/src/global/isettings.h new file mode 100644 index 0000000..56ed9d2 --- /dev/null +++ b/src/global/isettings.h @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include "modularity/ioc.h" + +#define INIT_SETTINGS_KEY(keyName, defaultValue) modularity::ioc()->resolve()->addKey(QString::fromStdString(moduleName()), keyName, defaultValue) + +namespace scratchcpp +{ + +class ISettings : MODULE_EXPORT_INTERFACE +{ + public: + virtual ~ISettings() { } + + virtual void addKey(const QString &moduleName, const QString &keyName, const QVariant &defaultValue) = 0; + virtual void setValue(const QString &moduleName, const QString &keyName, const QVariant &value) = 0; + virtual QVariant getValue(const QString &moduleName, const QString &keyName) const = 0; + virtual bool containsKey(const QString &moduleName, const QString &keyName) const = 0; + + virtual void freeze() = 0; + virtual void saveChanges() = 0; + virtual void discardChanges() = 0; + virtual bool isFrozen() const = 0; +}; + +} // namespace scratchcpp diff --git a/src/global/test/CMakeLists.txt b/src/global/test/CMakeLists.txt index c7d28e9..b9390b0 100644 --- a/src/global/test/CMakeLists.txt +++ b/src/global/test/CMakeLists.txt @@ -2,6 +2,7 @@ set(MODULE_TEST_SRC modularity.cpp setup.cpp appinfo.cpp + settings.cpp fakeexport.h fakedependency.h mocks/moduleexportinterfacemock.h diff --git a/src/global/test/mocks/filepathsmock.h b/src/global/test/mocks/filepathsmock.h new file mode 100644 index 0000000..7603783 --- /dev/null +++ b/src/global/test/mocks/filepathsmock.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +namespace scratchcpp +{ + +class FilePathsMock : public IFilePaths +{ + public: + MOCK_METHOD(QString, configLocation, (), (const, override)); +}; + +} // namespace scratchcpp diff --git a/src/global/test/settings.cpp b/src/global/test/settings.cpp new file mode 100644 index 0000000..cd46d59 --- /dev/null +++ b/src/global/test/settings.cpp @@ -0,0 +1,125 @@ +#include +#include +#include +#include +#include + +#include "mocks/filepathsmock.h" + +#include "internal/settings.h" + +using namespace scratchcpp; + +using ::testing::Return; + +class SettingsTest : public testing::Test +{ + public: + void SetUp() override + { + m_paths = std::make_shared(); + modularity::ioc()->registerExport(m_paths); + EXPECT_CALL(*m_paths, configLocation()) + .WillRepeatedly(Return(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + "/" + qApp->applicationName() + "/config_test.ini")); + QFile::remove(m_paths->configLocation()); + m_settings = std::make_shared(); + } + + void TearDown() override + { + modularity::ioc()->reset(); + m_settings->setpaths(nullptr); + } + + std::shared_ptr m_settings; + std::shared_ptr m_paths; +}; + +TEST_F(SettingsTest, SetGetContains) +{ + ASSERT_FALSE(m_settings->containsKey("test", "something")); + ASSERT_TRUE(m_settings->getValue("test", "something").isNull()); + + m_settings->setValue("test", "something", "hello world"); + ASSERT_TRUE(m_settings->containsKey("test", "something")); + ASSERT_FALSE(m_settings->containsKey("test", "test")); + ASSERT_EQ(m_settings->getValue("test", "something").toString(), "hello world"); + ASSERT_TRUE(m_settings->getValue("test", "test").isNull()); + + m_settings->setValue("test", "test", 10); + ASSERT_TRUE(m_settings->containsKey("test", "something")); + ASSERT_TRUE(m_settings->containsKey("test", "test")); + ASSERT_EQ(m_settings->getValue("test", "something").toString(), "hello world"); + ASSERT_EQ(m_settings->getValue("test", "test").toInt(), 10); + + ASSERT_FALSE(m_settings->containsKey("test2", "something")); + ASSERT_FALSE(m_settings->containsKey("test2", "test")); + ASSERT_TRUE(m_settings->getValue("test2", "something").isNull()); + ASSERT_TRUE(m_settings->getValue("test2", "test").isNull()); + + m_settings->setValue("test2", "test", true); + ASSERT_TRUE(m_settings->containsKey("test", "something")); + ASSERT_TRUE(m_settings->containsKey("test", "test")); + ASSERT_FALSE(m_settings->containsKey("test2", "something")); + ASSERT_TRUE(m_settings->containsKey("test2", "test")); + ASSERT_EQ(m_settings->getValue("test", "something").toString(), "hello world"); + ASSERT_TRUE(m_settings->getValue("test2", "test").toBool()); + ASSERT_TRUE(m_settings->getValue("test2", "something").isNull()); + + // Test persistence + m_settings.reset(); + m_settings = std::make_shared(); + ASSERT_EQ(m_settings->getValue("test", "something").toString(), "hello world"); + ASSERT_TRUE(m_settings->containsKey("test", "something")); + ASSERT_TRUE(m_settings->containsKey("test", "test")); + ASSERT_EQ(m_settings->getValue("test", "something").toString(), "hello world"); + ASSERT_EQ(m_settings->getValue("test", "test").toInt(), 10); + ASSERT_TRUE(m_settings->getValue("test2", "test").toBool()); + ASSERT_TRUE(m_settings->getValue("test2", "something").isNull()); +} + +TEST_F(SettingsTest, DefaultValue) +{ + m_settings->addKey("test", "something", 5); + ASSERT_FALSE(m_settings->containsKey("test", "something")); + ASSERT_EQ(m_settings->getValue("test", "something").toInt(), 5); + + m_settings->setValue("test", "something", 10); + ASSERT_TRUE(m_settings->containsKey("test", "something")); + ASSERT_EQ(m_settings->getValue("test", "something").toInt(), 10); +} + +TEST_F(SettingsTest, Freeze) +{ + ASSERT_FALSE(m_settings->isFrozen()); + + // Discard + m_settings->setValue("test", "something", "hello"); + m_settings->freeze(); + ASSERT_TRUE(m_settings->isFrozen()); + ASSERT_TRUE(m_settings->containsKey("test", "something")); + ASSERT_EQ(m_settings->getValue("test", "something").toString(), "hello"); + + m_settings->setValue("test", "test", 15); + ASSERT_TRUE(m_settings->containsKey("test", "test")); + ASSERT_EQ(m_settings->getValue("test", "test").toInt(), 15); + + m_settings->discardChanges(); + ASSERT_FALSE(m_settings->isFrozen()); + ASSERT_TRUE(m_settings->containsKey("test", "something")); + ASSERT_EQ(m_settings->getValue("test", "something").toString(), "hello"); + ASSERT_FALSE(m_settings->containsKey("test", "test")); + ASSERT_TRUE(m_settings->getValue("test", "test").isNull()); + + // Save + m_settings->freeze(); + ASSERT_TRUE(m_settings->isFrozen()); + m_settings->setValue("test", "test", 15); + + m_settings->saveChanges(); + ASSERT_FALSE(m_settings->isFrozen()); + ASSERT_TRUE(m_settings->containsKey("test", "something")); + ASSERT_EQ(m_settings->getValue("test", "something").toString(), "hello"); + ASSERT_TRUE(m_settings->containsKey("test", "test")); + ASSERT_EQ(m_settings->getValue("test", "test").toInt(), 15); +} diff --git a/src/ui/CMakeLists.txt b/src/ui/CMakeLists.txt index 4c96021..eed3f01 100644 --- a/src/ui/CMakeLists.txt +++ b/src/ui/CMakeLists.txt @@ -4,8 +4,11 @@ set(MODULE_SRC uimodule.cpp uimodule.h iuiengine.h + ithemeengine.h internal/uiengine.cpp internal/uiengine.h + internal/themeengine.cpp + internal/themeengine.h ) include(${PROJECT_SOURCE_DIR}/build/module.cmake) diff --git a/src/ui/internal/themeengine.cpp b/src/ui/internal/themeengine.cpp new file mode 100644 index 0000000..4fa8b5a --- /dev/null +++ b/src/ui/internal/themeengine.cpp @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "themeengine.h" +#include "isettings.h" + +using namespace scratchcpp; + +static const QString MODULE = "ui"; +static const QString THEME_KEY = "theme"; +static const QString ACCENT_COLOR_KEY = "accentColor"; + +std::shared_ptr ThemeEngine::m_instance = std::make_shared(); + +std::shared_ptr ThemeEngine::instance() +{ + return m_instance; +} + +IThemeEngine::Theme ThemeEngine::theme() const +{ + return static_cast(settings()->getValue(MODULE, THEME_KEY).toInt()); +} + +void ThemeEngine::setTheme(Theme newTheme) +{ + settings()->setValue(MODULE, THEME_KEY, static_cast(newTheme)); + reloadTheme(); +} + +void ThemeEngine::reloadTheme() +{ + emit bgColorChanged(); + emit foregroundColorChanged(); + emit borderColorChanged(); + emit accentColorChanged(); + emit themeChanged(); +} + +void ThemeEngine::resetTheme() +{ + setTheme(Theme::DarkTheme); +} + +const QColor &ThemeEngine::bgColor() const +{ + static const QColor dark = QColor(31, 30, 28); + static const QColor light = QColor(255, 255, 255); + return theme() == Theme::DarkTheme ? dark : light; +} + +const QColor &ThemeEngine::foregroundColor() const +{ + static const QColor dark = QColor(255, 255, 255); + static const QColor light = QColor(0, 0, 0); + return theme() == Theme::DarkTheme ? dark : light; +} + +const QColor &ThemeEngine::borderColor() const +{ + static const QColor dark = QColor(255, 255, 255, 64); + static const QColor light = QColor(0, 0, 0, 64); + return theme() == Theme::DarkTheme ? dark : light; +} + +QColor ThemeEngine::accentColor() const +{ + return settings()->getValue(MODULE, ACCENT_COLOR_KEY).value(); +} + +void ThemeEngine::setAccentColor(const QColor &newAccentColor) +{ + settings()->setValue(MODULE, ACCENT_COLOR_KEY, newAccentColor); + emit accentColorChanged(); +} diff --git a/src/ui/internal/themeengine.h b/src/ui/internal/themeengine.h new file mode 100644 index 0000000..c28d1a9 --- /dev/null +++ b/src/ui/internal/themeengine.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "ithemeengine.h" + +Q_MOC_INCLUDE("isettings.h") + +namespace scratchcpp +{ + +class ISettings; + +class ThemeEngine : public IThemeEngine +{ + Q_OBJECT + INJECT(ISettings, settings) + Q_PROPERTY(Theme theme READ theme WRITE setTheme NOTIFY themeChanged FINAL) + Q_PROPERTY(QColor bgColor READ bgColor NOTIFY bgColorChanged FINAL) + Q_PROPERTY(QColor foregroundColor READ foregroundColor NOTIFY foregroundColorChanged FINAL) + Q_PROPERTY(QColor borderColor READ borderColor NOTIFY borderColorChanged FINAL) + Q_PROPERTY(QColor accentColor READ accentColor WRITE setAccentColor NOTIFY accentColorChanged FINAL) + public: + static std::shared_ptr instance(); + + Theme theme() const override; + void setTheme(Theme newTheme) override; + Q_INVOKABLE void reloadTheme() override; + Q_INVOKABLE void resetTheme() override; + + const QColor &bgColor() const override; + const QColor &foregroundColor() const override; + const QColor &borderColor() const override; + + QColor accentColor() const override; + void setAccentColor(const QColor &newAccentColor) override; + + signals: + void themeChanged(); + void bgColorChanged(); + void foregroundColorChanged(); + void borderColorChanged(); + void accentColorChanged(); + + private: + static std::shared_ptr m_instance; +}; + +} // namespace scratchcpp diff --git a/src/ui/internal/uiengine.cpp b/src/ui/internal/uiengine.cpp index 4ca1d0a..d4d3440 100644 --- a/src/ui/internal/uiengine.cpp +++ b/src/ui/internal/uiengine.cpp @@ -2,6 +2,10 @@ #include #include +#ifdef Q_OS_LINUX +#include +#include +#endif #include "uiengine.h" @@ -66,3 +70,24 @@ void UiEngine::setActiveFocusItem(QQuickItem *newActiveFocusItem) m_activeFocusItem = newActiveFocusItem; emit activeFocusItemChanged(); } + +bool UiEngine::useNativeMenuBar() const +{ +#if defined(Q_OS_MACOS) +#if QT_VERSION < QT_VERSION_CHECK(6, 8, 0) + return true; +#else + // Since Qt 6.8, Qt Quick Controls menu bar is native + return false; +#endif +#elif defined(Q_OS_LINUX) + const QDBusConnection connection = QDBusConnection::sessionBus(); + static const QString registrarService = QStringLiteral("com.canonical.AppMenu.Registrar"); + if (const auto iface = connection.interface()) + return iface->isServiceRegistered(registrarService); + else + return false; +#else + return false; +#endif +} diff --git a/src/ui/internal/uiengine.h b/src/ui/internal/uiengine.h index 3d01231..f0d2389 100644 --- a/src/ui/internal/uiengine.h +++ b/src/ui/internal/uiengine.h @@ -19,6 +19,7 @@ class UiEngine { Q_OBJECT Q_PROPERTY(QQuickItem *activeFocusItem READ activeFocusItem WRITE setActiveFocusItem NOTIFY activeFocusItemChanged) + Q_PROPERTY(bool useNativeMenuBar READ useNativeMenuBar NOTIFY useNativeMenuBarChanged FINAL) public: explicit UiEngine(QObject *parent = nullptr); @@ -34,8 +35,11 @@ class UiEngine QQuickItem *activeFocusItem() const; void setActiveFocusItem(QQuickItem *newActiveFocusItem); + bool useNativeMenuBar() const; + signals: void activeFocusItemChanged(); + void useNativeMenuBarChanged(); private: static std::shared_ptr m_instance; diff --git a/src/ui/ithemeengine.h b/src/ui/ithemeengine.h new file mode 100644 index 0000000..a6eecad --- /dev/null +++ b/src/ui/ithemeengine.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#include "modularity/ioc.h" + +namespace scratchcpp +{ + +class IThemeEngine + : public QObject + , MODULE_EXPORT_INTERFACE +{ + Q_OBJECT + public: + virtual ~IThemeEngine() { } + + enum class Theme + { + LightTheme = 1, + DarkTheme = 0 + }; + Q_ENUM(Theme) + + virtual Theme theme() const = 0; + virtual void setTheme(Theme newTheme) = 0; + virtual void reloadTheme() = 0; + virtual void resetTheme() = 0; + + virtual const QColor &bgColor() const = 0; + virtual const QColor &foregroundColor() const = 0; + virtual const QColor &borderColor() const = 0; + + virtual QColor accentColor() const = 0; + virtual void setAccentColor(const QColor &newAccentColor) = 0; +}; + +} // namespace scratchcpp diff --git a/src/ui/test/CMakeLists.txt b/src/ui/test/CMakeLists.txt index 654b6f5..1b4177d 100644 --- a/src/ui/test/CMakeLists.txt +++ b/src/ui/test/CMakeLists.txt @@ -1,5 +1,6 @@ set(MODULE_TEST_SRC uiengine.cpp + themeengine.cpp ) include(${PROJECT_SOURCE_DIR}/build/module_test.cmake) diff --git a/src/ui/test/mocks/settingsmock.h b/src/ui/test/mocks/settingsmock.h new file mode 100644 index 0000000..32b9b3a --- /dev/null +++ b/src/ui/test/mocks/settingsmock.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +namespace scratchcpp +{ + +class SettingsMock : public ISettings +{ + public: + MOCK_METHOD(void, addKey, (const QString &, const QString &, const QVariant &), (override)); + MOCK_METHOD(void, setValue, (const QString &, const QString &, const QVariant &), (override)); + MOCK_METHOD(QVariant, getValue, (const QString &, const QString &), (const, override)); + MOCK_METHOD(bool, containsKey, (const QString &, const QString &), (const, override)); + + MOCK_METHOD(void, freeze, (), (override)); + MOCK_METHOD(void, saveChanges, (), (override)); + MOCK_METHOD(void, discardChanges, (), (override)); + MOCK_METHOD(bool, isFrozen, (), (const, override)); +}; + +} // namespace scratchcpp diff --git a/src/ui/test/themeengine.cpp b/src/ui/test/themeengine.cpp new file mode 100644 index 0000000..b8e0d7d --- /dev/null +++ b/src/ui/test/themeengine.cpp @@ -0,0 +1,95 @@ +#include +#include +#include + +#include "mocks/settingsmock.h" + +#include "internal/themeengine.h" + +using namespace scratchcpp; + +using ::testing::Return; +using ::testing::_; + +static const QString MODULE = "ui"; +static const QString THEME_KEY = "theme"; +static const QString ACCENT_COLOR_KEY = "accentColor"; + +class ThemeEngineTest : public testing::Test +{ + public: + void SetUp() override + { + m_settings = std::make_shared(); + m_themeEngine.setsettings(m_settings); + + m_themeSpies.push_back(std::make_unique(&m_themeEngine, &ThemeEngine::themeChanged)); + m_themeSpies.push_back(std::make_unique(&m_themeEngine, &ThemeEngine::bgColorChanged)); + m_themeSpies.push_back(std::make_unique(&m_themeEngine, &ThemeEngine::foregroundColorChanged)); + m_themeSpies.push_back(std::make_unique(&m_themeEngine, &ThemeEngine::borderColorChanged)); + m_themeSpies.push_back(std::make_unique(&m_themeEngine, &ThemeEngine::accentColorChanged)); + } + + void TearDown() override { m_themeEngine.setsettings(nullptr); } + + void checkThemeSpies(int count) + { + for (const auto &spy : m_themeSpies) + ASSERT_EQ(spy->count(), count); + } + + ThemeEngine m_themeEngine; + std::shared_ptr m_settings; + std::vector> m_themeSpies; +}; + +TEST_F(ThemeEngineTest, Instance) +{ + ASSERT_TRUE(ThemeEngine::instance()); +} + +TEST_F(ThemeEngineTest, Theme) +{ + EXPECT_CALL(*m_settings, getValue(MODULE, THEME_KEY)).WillOnce(Return(0)); + ASSERT_EQ(m_themeEngine.theme(), ThemeEngine::Theme::DarkTheme); + + EXPECT_CALL(*m_settings, getValue(MODULE, THEME_KEY)).WillOnce(Return(1)); + ASSERT_EQ(m_themeEngine.theme(), ThemeEngine::Theme::LightTheme); + checkThemeSpies(0); + + EXPECT_CALL(*m_settings, setValue(MODULE, THEME_KEY, QVariant(0))); + m_themeEngine.setTheme(ThemeEngine::Theme::DarkTheme); + checkThemeSpies(1); + + EXPECT_CALL(*m_settings, setValue(MODULE, THEME_KEY, QVariant(1))); + m_themeEngine.setTheme(ThemeEngine::Theme::LightTheme); + checkThemeSpies(2); + + EXPECT_CALL(*m_settings, setValue(MODULE, THEME_KEY, _)); + m_themeEngine.resetTheme(); + checkThemeSpies(3); + + m_themeEngine.reloadTheme(); + checkThemeSpies(4); +} + +TEST_F(ThemeEngineTest, AccentColor) +{ + QSignalSpy spy(&m_themeEngine, &ThemeEngine::accentColorChanged); + + EXPECT_CALL(*m_settings, getValue(MODULE, ACCENT_COLOR_KEY)).WillOnce(Return(QColor(255, 0, 0))); + ASSERT_EQ(m_themeEngine.accentColor(), QColor(255, 0, 0)); + ASSERT_EQ(spy.count(), 0); + + EXPECT_CALL(*m_settings, getValue(MODULE, ACCENT_COLOR_KEY)).WillOnce(Return(QColor(0, 255, 128))); + ASSERT_EQ(m_themeEngine.accentColor(), QColor(0, 255, 128)); + ASSERT_EQ(spy.count(), 0); + + EXPECT_CALL(*m_settings, setValue(MODULE, ACCENT_COLOR_KEY, QVariant(QColor(255, 255, 255)))); + m_themeEngine.setAccentColor(QColor(255, 255, 255)); + ASSERT_EQ(spy.count(), 1); + + EXPECT_CALL(*m_settings, setValue(MODULE, ACCENT_COLOR_KEY, QVariant(QColor(0, 0, 0)))); + m_themeEngine.setAccentColor(QColor(0, 0, 0)); + ASSERT_EQ(spy.count(), 2); +} diff --git a/src/ui/uimodule.cpp b/src/ui/uimodule.cpp index a9a953b..b0bd617 100644 --- a/src/ui/uimodule.cpp +++ b/src/ui/uimodule.cpp @@ -3,7 +3,9 @@ #include #include "uimodule.h" +#include "isettings.h" #include "internal/uiengine.h" +#include "internal/themeengine.h" using namespace scratchcpp::ui; @@ -21,4 +23,14 @@ void scratchcpp::ui::UiModule::registerExports() QQmlEngine::setObjectOwnership(UiEngine::instance().get(), QQmlEngine::CppOwnership); qmlRegisterSingletonInstance("ScratchCPP.Ui", 1, 0, "UiEngine", UiEngine::instance().get()); modularity::ioc()->registerExport(UiEngine::instance()); + + QQmlEngine::setObjectOwnership(ThemeEngine::instance().get(), QQmlEngine::CppOwnership); + qmlRegisterSingletonInstance("ScratchCPP.Ui", 1, 0, "ThemeEngine", ThemeEngine::instance().get()); + modularity::ioc()->registerExport(ThemeEngine::instance()); +} + +void UiModule::initSettings() +{ + INIT_SETTINGS_KEY("theme", static_cast(IThemeEngine::Theme::DarkTheme)); + INIT_SETTINGS_KEY("accentColor", QColor(0, 0, 0, 0)); // default accent color should be set by the application } diff --git a/src/ui/uimodule.h b/src/ui/uimodule.h index 1f60570..04d80ec 100644 --- a/src/ui/uimodule.h +++ b/src/ui/uimodule.h @@ -15,6 +15,7 @@ class UiModule : public modularity::IModuleSetup std::string moduleName() const override; void registerExports() override; + void initSettings() override; }; } // namespace scratchcpp::ui diff --git a/src/uicomponents/CMakeLists.txt b/src/uicomponents/CMakeLists.txt index 283f65b..8475ef4 100644 --- a/src/uicomponents/CMakeLists.txt +++ b/src/uicomponents/CMakeLists.txt @@ -4,12 +4,15 @@ set(MODULE_QML_FILES CustomButton.qml CustomToolButton.qml AccentButton.qml + ColorButton.qml HoverToolTip.qml CustomMenuBar.qml CustomMenu.qml CustomMenuItem.qml CustomMenuSeparator.qml CustomDialog.qml + CustomMessageDialog.qml + IconLabel.qml internal/CustomDialogButtonBox.qml ) set(MODULE_SRC diff --git a/src/uicomponents/ColorButton.qml b/src/uicomponents/ColorButton.qml new file mode 100644 index 0000000..3b3fd9e --- /dev/null +++ b/src/uicomponents/ColorButton.qml @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Controls +import QtQuick.Controls.Material +import ScratchCPP.Ui + +RoundButton { + property color color: Material.background + id: control + onColorChanged: Material.background = color + + Rectangle { + width: control.background.width + height: control.background.height + anchors.centerIn: parent + radius: width / 2 + color: Qt.rgba(0, 0, 0, 0) + border.color: ThemeEngine.borderColor + } + + Rectangle { + visible: control.checked + width: control.background.width + 6 + height: control.background.height + 6 + anchors.centerIn: parent + radius: width / 2 + color: Qt.rgba(0, 0, 0, 0) + border.color: ThemeEngine.foregroundColor + } +} diff --git a/src/uicomponents/CustomButton.qml b/src/uicomponents/CustomButton.qml index 1830809..aa4e252 100644 --- a/src/uicomponents/CustomButton.qml +++ b/src/uicomponents/CustomButton.qml @@ -6,6 +6,6 @@ import QtQuick.Controls Button { id: control Material.roundedScale: Material.SmallScale - leftInset: icon.source + icon.name == "" ? 0 : -2 - rightInset: icon.source + icon.name == "" ? 0 : 5 + leftInset: icon.source + icon.name == "" ? 0 : 0 + rightInset: icon.source + icon.name == "" ? 0 : 3 } diff --git a/src/uicomponents/CustomDialog.qml b/src/uicomponents/CustomDialog.qml index 4861a04..a8dd33f 100644 --- a/src/uicomponents/CustomDialog.qml +++ b/src/uicomponents/CustomDialog.qml @@ -123,12 +123,10 @@ Item { property alias contentsLoader: contentsLoader property alias buttonBoxLoader: buttonBoxLoader anchors.fill: parent - // TODO: Read colors from ThemeEngine - color: /*ThemeEngine.bgColor*/ Material.background - //Material.background: ThemeEngine.bgColor - //Material.accent: ThemeEngine.currentAccentColor - //Material.theme: ThemeEngine.theme === ThemeEngine.DarkTheme ? Material.Dark : Material.Light - Material.theme: Material.Dark + color: ThemeEngine.bgColor + Material.background: ThemeEngine.bgColor + Material.accent: ThemeEngine.accentColor + Material.theme: ThemeEngine.theme === ThemeEngine.DarkTheme ? Material.Dark : Material.Light ColumnLayout { id: contentLayout diff --git a/src/uicomponents/CustomMenu.qml b/src/uicomponents/CustomMenu.qml index a7340c9..8449b63 100644 --- a/src/uicomponents/CustomMenu.qml +++ b/src/uicomponents/CustomMenu.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Controls +import ScratchCPP.Ui Menu { property bool isSubMenu: false @@ -27,8 +28,8 @@ Menu { font.pointSize: 10 background: Rectangle { // Load colors from theme - color: /*ThemeEngine.bgColor*/ Material.background - border.color: /*ThemeEngine.borderColor*/ Qt.rgba(1, 1, 1, 0.25) + color: ThemeEngine.bgColor + border.color: ThemeEngine.borderColor radius: 10 implicitHeight: 40 } diff --git a/src/uicomponents/CustomMenuBar.qml b/src/uicomponents/CustomMenuBar.qml index bfb7f96..3a789e1 100644 --- a/src/uicomponents/CustomMenuBar.qml +++ b/src/uicomponents/CustomMenuBar.qml @@ -2,6 +2,8 @@ import QtQuick import QtQuick.Controls +import Qt.labs.platform as Platform +import ScratchCPP.Ui import ScratchCPP.UiComponents MenuBar { @@ -21,8 +23,7 @@ MenuBar { } function getComponentString(typeName) { - //var imports = "import QtQuick; import QtQuick.Controls; import Qt.labs.platform as Platform;" - var imports = "import QtQuick; import QtQuick.Controls;" + var imports = "import QtQuick; import QtQuick.Controls; import Qt.labs.platform as Platform;" return imports + " " + typeName + " {}"; } @@ -72,7 +73,7 @@ MenuBar { } background: Rectangle { - color: Material.backgroundColor // Load the color from the theme + color: ThemeEngine.bgColor // Load the color from the theme } delegate: MenuBarItem { @@ -94,8 +95,6 @@ MenuBar { topPadding: 5 bottomPadding: 5 font.pointSize: 10 - Material.background: Qt.rgba(0, 0, 0, 0) - Material.foreground: Material.theme == Material.Dark ? "white" : "black" contentItem: Label { text: replaceText(menuBarItem.text) font: menuBarItem.font @@ -104,11 +103,11 @@ MenuBar { } function reload() { - /*if(nativeMenuBarEnabled) + if(UiEngine.useNativeMenuBar) { root.visible = false; return; - }*/ + } var oldObjects = []; @@ -133,14 +132,14 @@ MenuBar { Component.onCompleted: reload(); - /*onEnabledChanged: { + onEnabledChanged: { if(platformMenuBarLoader.active) platformMenuBarLoader.item.reload(); } Loader { id: platformMenuBarLoader - active: // whether the native menu bar is active + active: UiEngine.useNativeMenuBar sourceComponent: Platform.MenuBar { id: platformMenuBar @@ -150,14 +149,14 @@ MenuBar { createMenuBar(platformMenuBar, "Platform.Menu", "Platform.MenuItem", "Platform.MenuSeparator"); } - Connections { - target: QmlUtils + /*Connections { + target: // TODO: Add a class for the menu bar reload signal function onMenuBarReloadTriggered() { platformMenuBar.reload(); } - } + }*/ Component.onCompleted: reload(); } - }*/ + } } diff --git a/src/uicomponents/CustomMessageDialog.qml b/src/uicomponents/CustomMessageDialog.qml new file mode 100644 index 0000000..0a5aed0 --- /dev/null +++ b/src/uicomponents/CustomMessageDialog.qml @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls + +CustomDialog { + id: root + property string text + property string informativeText + standardButtons: Dialog.Ok + + contentItem: RowLayout { + spacing: 25 + + // TODO: Add icon + + ColumnLayout { + Label { + text: root.text + font.pointSize: 14 + font.bold: true + } + + Label { + text: root.informativeText + } + } + } +} diff --git a/src/uicomponents/CustomToolButton.qml b/src/uicomponents/CustomToolButton.qml index 1a16f76..f90ca1c 100644 --- a/src/uicomponents/CustomToolButton.qml +++ b/src/uicomponents/CustomToolButton.qml @@ -3,17 +3,18 @@ import QtQuick import QtQuick.Controls import QtQuick.Controls.Material +import ScratchCPP.Ui // Similar to ToolButton, but it's always a (rounded) rectangle // Double click events are not supported, use the clicked() signal like with a QPushButton CustomButton { - property color foregroundColor: Material.theme === Material.Dark ? "white" : "black" + property color foregroundColor: ThemeEngine.foregroundColor property string toolTipText property string accessibleDescription: "" signal clicked() id: control font.capitalization: Font.MixedCase - Material.background: Qt.rgba(foregroundColor.r, foregroundColor.g, foregroundColor.b, 0.15) + Material.background: ThemeEngine.theme == ThemeEngine.DarkTheme ? (highlighted ? Qt.darker(ThemeEngine.accentColor, 2) : Qt.rgba(0.25, 0.24, 0.25, 1)) : (highlighted ? Qt.lighter(ThemeEngine.accentColor, 2) : Qt.rgba(0.84, 0.84, 0.84, 1)) Material.foreground: foregroundColor icon.color: foregroundColor onReleased: clicked() diff --git a/src/uicomponents/IconLabel.qml b/src/uicomponents/IconLabel.qml new file mode 100644 index 0000000..e02f687 --- /dev/null +++ b/src/uicomponents/IconLabel.qml @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Controls.impl as Controls + +Controls.IconLabel {} diff --git a/src/uicomponents/internal/CustomDialogButtonBox.qml b/src/uicomponents/internal/CustomDialogButtonBox.qml index 4c2a91e..d0e06de 100644 --- a/src/uicomponents/internal/CustomDialogButtonBox.qml +++ b/src/uicomponents/internal/CustomDialogButtonBox.qml @@ -5,13 +5,12 @@ import QtQuick.Controls import ScratchCPP.Ui DialogButtonBox { - property color bgColor: /*ThemeEngine.bgColor*/ Material.background property int radius: 10 signal focusOut() id: dialogButtonBox font.capitalization: Font.MixedCase background: Rectangle { - color: bgColor + color: "transparent" radius: radius } diff --git a/test/main.cpp b/test/main.cpp index c5dfc22..eb6569c 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -4,6 +4,7 @@ int main(int argc, char **argv) { QApplication a(argc, argv); + a.setApplicationName("ScratchCPP Player"); ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }