diff --git a/.github/README.md b/.github/README.md deleted file mode 100644 index 667ed74b9..000000000 --- a/.github/README.md +++ /dev/null @@ -1,187 +0,0 @@ - - - - Deskflow - - -**Deskflow** is a free and open source keyboard and mouse sharing app. -Use the keyboard, mouse, or trackpad of one computer to control nearby computers, -and work seamlessly between them. -It's like a software KVM (but without the video). -TLS encryption is enabled by default. Wayland is supported. Clipboard sharing is supported. - -> [!TIP] -> -> **Chat with us** -> -> - Main discussion on Matrix: [`#deskflow:matrix.org`](https://matrix.to/#/#deskflow:matrix.org) ([Matrix clients](https://matrix.org/ecosystem/clients/)) -> - Discussion also happens on IRC: `#deskflow` or `#deskflow-dev` on [Libera Chat](https://libera.chat/) -> - Start a [new discussion](https://github.com/deskflow/deskflow/discussions) on our GitHub project. - -## Download - -[![Downloads: Stable Release](https://img.shields.io/github/downloads/deskflow/deskflow/latest/total?style=for-the-badge&logo=github&label=Download%20Stable)](https://github.com/deskflow/deskflow/releases/latest)      [![Downloads: Continuous Build](https://img.shields.io/github/downloads/deskflow/deskflow/continuous/total?style=for-the-badge&logo=github&label=Download%20Continuous)](https://github.com/deskflow/deskflow/releases/continuous)      [![Download From Flathub](https://img.shields.io/flathub/downloads/org.deskflow.deskflow?style=for-the-badge&logo=flathub&label=Download%20from%20flathub)](https://flathub.org/apps/org.deskflow.deskflow) - -> [!NOTE] -> On Windows, you will need to install the -> [Microsoft Visual C++ Redistributable](https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170#latest-microsoft-visual-c-redistributable-version). -> Download latest: [`vc_redist.x64.exe`](https://aka.ms/vc14/vc_redist.x64.exe) [`vc_redist.arm64.exe`](https://aka.ms/vc14/vc_redist.arm64.exe) - -> [!TIP] -> For macOS users, the easiest way to install and stay up to date is to use [Homebrew](https://brew.sh) with our [homebrew-tap](https://github.com/deskflow/homebrew-tap). -> macOS reports unsigned apps as damaged. This occurs because we do not use an Apple certificate for notarization. Clear the quarantine attribute to run the app: `xattr -c Deskflow.app` - -To use Deskflow, download one of our [packages](https://github.com/deskflow/deskflow/releases), install `deskflow` (from your package repository), or [build it](https://github.com/deskflow/deskflow/wiki/Building) from source. - -## Stats - -[![GitHub commit activity](https://img.shields.io/github/commit-activity/m/deskflow/deskflow?logo=github)](https://github.com/deskflow/deskflow/commits/master/) -[![GitHub top language](https://img.shields.io/github/languages/top/deskflow/deskflow?logo=github)](https://github.com/deskflow/deskflow/commits/master/) -[![GitHub License](https://img.shields.io/github/license/deskflow/deskflow?logo=github)](LICENSE) -[![REUSE status](https://api.reuse.software/badge/github.com/deskflow/deskflow)](https://api.reuse.software/info/github.com/deskflow/deskflow) - -[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=deskflow_deskflow&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=deskflow_deskflow) -[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=deskflow_deskflow&metric=coverage)](https://sonarcloud.io/summary/new_code?id=deskflow_deskflow) -[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=deskflow_deskflow&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=deskflow_deskflow) -[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=deskflow_deskflow&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=deskflow_deskflow) - -[![CI](https://github.com/deskflow/deskflow/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/deskflow/deskflow/actions/workflows/continuous-integration.yml) -[![CodeQL Analysis](https://github.com/deskflow/deskflow/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/deskflow/deskflow/actions/workflows/codeql-analysis.yml) -[![SonarCloud Analysis](https://github.com/deskflow/deskflow/actions/workflows/sonarcloud-analysis.yml/badge.svg)](https://github.com/deskflow/deskflow/actions/workflows/sonarcloud-analysis.yml) - -## Contribute - -[![Good first issues](https://img.shields.io/github/issues/deskflow/deskflow/good%20first%20issue?label=good%20first%20issues&color=%2344cc11)](https://github.com/deskflow/deskflow/labels/good%20first%20issue) - -There are many ways to contribute to the Deskflow project. - -We're a friendly, active, and welcoming community focused on building a great app. - -Read our [Contributing](https://github.com/deskflow/deskflow/wiki/Contributing) page to get started. - -For instructions on building Deskflow, use the wiki page: [Building](https://github.com/deskflow/deskflow/wiki/Building) - -## Operating Systems - -We support all major operating systems, including Windows, macOS, Linux, and Unix-like BSD-derived. - -Windows 10 v1809 or higher is required. - -macOS 13 or higher is required to use our CI builds for Apple Silicon machines. macOS 12 or higher is required for Intel macs or local builds. - -Linux requires libei 1.3+ and libportal 0.8+ for the server/client. Additionally, Qt 6.7+ is required for the GUI. -Linux users with systems not meeting these requirements should use flatpak in place of a native package. - -We officially support FreeBSD, and would also like to support: OpenBSD, NetBSD, DragonFly, Solaris. - -## Repology - -Repology monitors a huge number of package repositories and other sources comparing package -versions across them and gathering other information. - -[![Repology](https://repology.org/badge/vertical-allrepos/deskflow.svg?columns=2&exclude_unsupported)](https://repology.org/project/deskflow/versions) - -## Installing on macOS - -When you install Deskflow on macOS, you need to allow accessibility access (Privacy & Security) to both the `Deskflow` app and the `deskflow` process. - -If using Sequoia, you may also need to allow `Deskflow` under Local Network‍ settings (Privacy & Security). -When prompted by the OS, go to the settings and enable the access. - -If you are upgrading and you already have `Deskflow` or `deskflow` -on the allowed list you will need to manually remove them before accessibility access can be granted to the new version. - -macOS users who download directly from releases may need to run `xattr -c /Applications/Deskflow.app` after copying the app to the `Applications` dir. - -It is recommended to install Deskflow using [Homebrew](https://brew.sh) from our [homebrew-tap](https://github.com/deskflow/homebrew-tap) - -To add our tap, run: - -``` -brew tap deskflow/tap -``` - -Then install either: - -- Stable: `brew install deskflow` -- Continuous: `brew install deskflow-dev` - -## Similar Projects - -In the open source developer community, similar projects collaborate for the improvement of all -mouse and keyboard sharing tools. We aim for idea sharing and interoperability. - -- [**Lan Mouse**](https://github.com/feschber/lan-mouse) - - Rust implementation with the goal of having native front-ends and interoperability with - Deskflow/Synergy. -- [**Synergy**](https://symless.com/synergy) - - Downstream commercial fork. Synergy sponsors Deskflow with financial support and contributes code ([learn more](https://github.com/deskflow/deskflow/wiki/Relationship-with-Synergy)). -- [**Input Leap**](https://github.com/input-leap/input-leap) - - Inactive Deskflow/Synergy-derivative with the goal continuing Barrier development (now a dead fork). - -## FAQ - -### Is Deskflow compatible with Synergy, Input Leap, or Barrier? - -Yes, Deskflow has network compatibility with all forks: - -- Requires Deskflow >= v1.17.0.96 -- Deskflow will _just work_ with Input Leap and Barrier (server or client). -- Connecting a Deskflow client to a Synergy 1 server will also _just work_. -- To connect a Synergy 1 client, you need to select the Synergy protocol in the Deskflow server settings. - -_Note:_ Only Synergy 1 is compatible with Deskflow (Synergy 3 is not yet compatible). - -### Is Deskflow compatible with Lan Mouse? - -We would love to see compatibility with Lan Mouse. This may be quite an effort as currently the way they handle the generated input is very different. - -### If I want to solve issues in Deskflow do I need to contribute to a fork? - -We welcome PRs (pull requests) from the community. If you'd like to make a change, please feel -free to [start a discussion](https://github.com/deskflow/deskflow/discussions) or -[open a PR](https://github.com/deskflow/deskflow/wiki/Contributing). - -### Is clipboard sharing supported? - -Absolutely. The clipboard-sharing feature is a cornerstone feature of the product and we are -committed to maintaining and improving that feature. - -### Is Wayland for Linux supported? - -Yes! Wayland (the Linux display server protocol aimed to become the successor of the X Window -System) is an important platform for us. -The [`libei`](https://gitlab.freedesktop.org/libinput/libei) and -[`libportal`](https://github.com/flatpak/libportal) libraries enable -Wayland support for Deskflow. We would like to give special thanks to Peter Hutterer, -who is the author of `libei`, a major contributor to `libportal`, and the author of the Wayland -implementation in Deskflow. Others such as Olivier Fourdan and Povilas Kanapickas helped with the -Wayland implementation. - -Some features _may_ be unavailable or broken on Wayland. Please see the [known Wayland issues](https://github.com/deskflow/deskflow/discussions/7499). - -### Where did it all start? - -Deskflow was first created as Synergy in 2001 by Chris Schoeneman. -Read about the [history of the project](https://github.com/deskflow/deskflow/wiki/History) on our -wiki. - -## Meow'Dib (our mascot) - -![Meow'Dib](https://github.com/user-attachments/assets/726f695c-3dfb-4abd-875d-ed658f6c610f) - -## Deskflow Contributors - -[![Sponsored by Synergy](https://raw.githubusercontent.com/deskflow/deskflow-artwork/b2c72a3e60a42dee793bd47efc275b5ee0bdaa5f/misc/synergy-sponsor.svg)](https://symless.com/synergy) - -[Synergy](https://symless.com/synergy) sponsors the Deskflow project by contributing code and providing financial support ([learn more](https://github.com/deskflow/deskflow/wiki/Relationship-with-Synergy)). - -Deskflow is made by possible by these contributors. - - - - - -## License - -This project is licensed under [GPL-2.0](LICENSE) with an [OpenSSL exception](../LICENSES/LicenseRef-OpenSSL-Exception.txt). diff --git a/.gitignore b/.gitignore index e10ad34f5..685035b71 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,7 @@ deskflow-config.toml .DS_Store *.code-workspace .env* -/scripts/*.egg-info +/scripts /*.user *.ui.autosave @@ -40,12 +40,6 @@ CMakeCache.txt CMakeUserPresets.json CMakeFiles/* -# vscode folder -/.vscode - -# scripts folder -/scripts +# AI +/.claude -# Ai helperfilers -**/[cC]laude.[mM][dD] -**/CLAUDE.[mM][dD] diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..0c264114d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "ms-vscode.cmake-tools", + "llvm-vs-code-extensions.vscode-clangd", + "ms-vscode.cpptools", + "llvm-vs-code-extensions.lldb-dap", + "jacqueslucke.gcov-viewer" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..8e0d516ce --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,139 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "unix - gui", + "type": "lldb-dap", + "cwd": "${workspaceRoot}", + "request": "launch", + "program": "${workspaceFolder}/build/bin/synergy", + "preLaunchTask": "build-silent", + "osx": { + "program": "${workspaceFolder}/build/bin/synergy.app/Contents/MacOS/synergy" + } + }, + { + "name": "unix - unittests", + "type": "lldb-dap", + "cwd": "${workspaceRoot}", + "request": "launch", + "program": "${workspaceFolder}/build/bin/unittests", + "args": ["${input:gtest-args}"], + "preLaunchTask": "build-silent" + }, + { + "name": "unix - integtests", + "type": "lldb-dap", + "cwd": "${workspaceRoot}", + "request": "launch", + "program": "${workspaceFolder}/build/bin/integtests", + "args": ["${input:gtest-args}"], + "preLaunchTask": "build-silent" + }, + { + "name": "unix - daemon", + "type": "lldb-dap", + "cwd": "${workspaceRoot}", + "request": "launch", + "program": "${workspaceFolder}/build/bin/synergy-daemon", + "args": ["-f"], + "preLaunchTask": "build-silent" + }, + { + "name": "unix - attach", + "type": "lldb-dap", + "request": "attach", + "pid": "${command:pickProcess}" + }, + { + "name": "windows - gui", + "type": "cppvsdbg", + "cwd": "${workspaceRoot}", + "request": "launch", + "program": "${workspaceFolder}/build/bin-copy/synergy", + "internalConsoleOptions": "openOnSessionStart", + "preLaunchTask": "build-kill-gui" + }, + { + "name": "windows - version", + "type": "cppvsdbg", + "cwd": "${workspaceRoot}", + "request": "launch", + "program": "${workspaceFolder}/build/bin-copy/synergy-core", + "args": ["--version"], + "internalConsoleOptions": "openOnSessionStart" + }, + { + "name": "windows - unittests", + "type": "cppvsdbg", + "cwd": "${workspaceRoot}", + "request": "launch", + "program": "${workspaceFolder}/build/bin-copy/unittests", + "args": ["${input:gtest-args}"], + "internalConsoleOptions": "openOnSessionStart" + }, + { + "name": "windows - integtests", + "type": "cppvsdbg", + "cwd": "${workspaceRoot}", + "request": "launch", + "program": "${workspaceFolder}/build/bin-copy/integtests", + "args": ["${input:gtest-args}"], + "internalConsoleOptions": "openOnSessionStart" + }, + { + "name": "windows - active-desktop", + "type": "cppvsdbg", + "cwd": "${workspaceRoot}", + "request": "launch", + "program": "${workspaceFolder}/build/bin-copy/synergy-core", + "args": ["--active-desktop"], + "internalConsoleOptions": "openOnSessionStart", + "preLaunchTask": "build-kill-core" + }, + { + "name": "windows - daemon foreground", + "type": "cppvsdbg", + "cwd": "${workspaceRoot}", + "request": "launch", + "program": "${workspaceFolder}/build/bin-copy/synergy-daemon", + "args": ["-f"], + "internalConsoleOptions": "openOnSessionStart", + "preLaunchTask": "build-kill-core" + }, + { + "name": "windows - daemon install", + "type": "cppvsdbg", + "cwd": "${workspaceRoot}", + "request": "launch", + "program": "${workspaceFolder}/build/bin-copy/synergy-daemon", + "args": ["--install"], + "internalConsoleOptions": "openOnSessionStart", + "preLaunchTask": "build" + }, + { + "name": "windows - daemon uninstall", + "type": "cppvsdbg", + "cwd": "${workspaceRoot}", + "request": "launch", + "program": "${workspaceFolder}/build/bin-copy/synergy-daemon", + "args": ["--uninstall"], + "internalConsoleOptions": "openOnSessionStart", + "preLaunchTask": "build" + }, + { + "name": "windows - attach", + "type": "cppvsdbg", + "request": "attach", + "processId": "${command:pickProcess}" + } + ], + "inputs": [ + { + "id": "gtest-args", + "type": "promptString", + "description": "Test arguments", + "default": "--gtest_filter=*" + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..f21a2ff01 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,181 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "type": "shell", + "dependsOn": ["build-cmake"], + "windows": { + "command": "robocopy ${workspaceFolder}/build/bin ${workspaceFolder}/build/bin-copy /R:0 /E /S /NJS /NJH /NFL /NDL; exit 0" + }, + "group": { + "kind": "build", + "isDefault": true + }, + }, + { + "label": "build-cmake", + "type": "cmake", + "command": "build", + "targets": ["all"], + "preset": "${command:cmake.activeBuildPresetName}", + "group": "build", + "problemMatcher": { + "base": "$gcc", + "fileLocation": ["absolute"] + }, + }, + { + "label": "build-kill-core", + "dependsOn": ["kill-daemon", "kill-core", "build"], + "dependsOrder": "sequence" + }, + { + "label": "build-kill-gui", + "dependsOn": ["kill-gui", "kill-core", "build"], + "dependsOrder": "sequence" + }, + { + "label": "build-silent", + "type": "shell", + "dependsOn": ["build"], + "group": "build", + "presentation": { + "reveal": "silent", + } + }, + { + "label": "clean", + "type": "cmake", + "command": "build", + "targets": ["clean"], + "preset": "${command:cmake.activeBuildPresetName}", + "group": "build" + }, + { + "label": "clean-gcda", + "type": "shell", + "command": "find . -name '*.gcda' -delete", + "windows": { + "command": "$null" + }, + "presentation": { + "reveal": "silent" + } + }, + { + "label": "clean-qt", + "type": "shell", + "command": "rm -r build/src/gui build/src/lib/gui", + "windows": { + "command": "remove-item -recurse build/src/gui,build/src/lib/gui" + } + }, + { + "label": "clean-config", + "type": "shell", + "linux": { + "command": "rm -r ~/.config/Synergy/Synergy.conf" + }, + "windows": { + "command": "remove-item -recurse $env:APPDATA\\Synergy\\Synergy" + }, + "osx": { + "command": "rm -r ~/Library/Application\\ Support/Synergy/Synergy" + } + }, + { + "label": "kill-all", + "type": "shell", + "dependsOn": ["kill-gui", "kill-daemon", "kill-core"], + }, + { + "label": "kill-gui", + "type": "shell", + "command": "killall -9 synergy || true", + "windows": { + "command": "taskkill /F /IM synergy.exe; $true" + }, + }, + { + "label": "kill-daemon", + "type": "shell", + "command": "killall -9 synergy-daemon || true", + "windows": { + "command": "taskkill /F /IM synergy-daemon.exe; $true" + }, + "dependsOn": ["daemon-stop"], + }, + { + "label": "kill-core", + "type": "shell", + "command": "killall -9 synergy-core || true", + "windows": { + "command": "taskkill /F /IM synergy-core.exe; $true" + }, + }, + { + "label": "gui", + "type": "process", + "command": "${workspaceFolder}/build/bin/synergy", + "problemMatcher": [], + "dependsOn": ["build"], + "windows": { + "dependsOn": ["build-kill-gui"], + "command": "${workspaceFolder}/build/bin-copy/synergy.exe" + } + }, + { + "label": "daemon-stop", + "type": "shell", + "command": "echo noop > /dev/null", + "windows": { + "command": "Stop-Service -Name 'Synergy' -Force; while ((Get-Service -Name 'Synergy').Status -ne 'Stopped') { Start-Sleep -Seconds 1 }", + }, + }, + { + "label": "daemon-start", + "type": "shell", + "command": "echo noop > /dev/null", + "windows": { + "command": "Start-Service -Name 'Synergy'; while ((Get-Service -Name 'Synergy').Status -ne 'Running') { Start-Sleep -Seconds 1 }", + }, + }, + { + "label": "daemon-restart", + "type": "shell", + "command": "echo noop > /dev/null", + "windows": { + "command": "Restart-Service -Name 'Synergy'", + }, + }, + { + "label": "daemon-deploy", + "type": "shell", + "dependsOn": ["build-kill-core", "daemon-start"], + "dependsOrder": "sequence" + }, + { + "label": "daemon-install", + "type": "shell", + "command": "echo noop > /dev/null", + "windows": { + "command": "${workspaceFolder}/build/bin-copy/synergy-daemon.exe --install" + } + }, + { + "label": "daemon-uninstall", + "type": "shell", + "command": "echo noop > /dev/null", + "windows": { + "command": "${workspaceFolder}/build/bin-copy/synergy-daemon.exe --uninstall" + } + }, + { + "label": "daemon-reinstall", + "type": "shell", + "dependsOn": ["build-kill-core", "daemon-stop", "daemon-uninstall", "daemon-install"], + "dependsOrder": "sequence", + } + ] +} diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e4f9b3ec..f8b3c18dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,13 +21,13 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_STANDARD_REQUIRED ON) -# Fallback for when git can not be found +# Project version. Synergy.cmake adds the dev/snapshot/release suffix later. set(DESKFLOW_VERSION_MAJOR 1) -set(DESKFLOW_VERSION_MINOR 26) +set(DESKFLOW_VERSION_MINOR 21) set(DESKFLOW_VERSION_PATCH 0) set(DESKFLOW_VERSION_TWEAK 0) -# Get the version from git if it's a git repository +# Git short SHA, consumed by VersionInfo.h.in (kVersionGitSha). if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/.git) find_package(Git) if(GIT_FOUND) @@ -37,29 +37,6 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/.git) OUTPUT_VARIABLE GIT_SHA_SHORT ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE ) - execute_process( - COMMAND ${GIT_EXECUTABLE} describe --long --match v* --always - WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" - OUTPUT_VARIABLE GITREV - ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE - ) - string(FIND "${GITREV}" "v" isRev) - if(NOT ${isRev} EQUAL -1) - string(REGEX MATCH [0-9]+ MAJOR ${GITREV}) - string(REGEX MATCH \\.[0-9]+ MINOR ${GITREV}) - string(REPLACE "." "" MINOR "${MINOR}") - string(REGEX MATCH [0-9]+\- PATCH ${GITREV}) - string(REPLACE "-" "" PATCH "${PATCH}") - string(REGEX MATCH \-[0-9]+\- TWEAK ${GITREV}) - string(REPLACE "-" "" TWEAK "${TWEAK}") - set(DESKFLOW_VERSION_MAJOR ${MAJOR}) - set(DESKFLOW_VERSION_MINOR ${MINOR}) - set(DESKFLOW_VERSION_PATCH ${PATCH}) - set(DESKFLOW_VERSION_TWEAK ${TWEAK}) - else() - set(DESKFLOW_VERSION_TWEAK "9999") - endif() - unset(GITREV) endif() endif() @@ -87,6 +64,10 @@ set(CMAKE_PROJECT_VENDOR "${CMAKE_PROJECT_PROPER_NAME} Devs") set(CMAKE_PROJECT_COPYRIGHT "(C) 2024-2026 ${CMAKE_PROJECT_VENDOR}") set(CMAKE_PROJECT_CONTACT "${CMAKE_PROJECT_PROPER_NAME} ") set(CMAKE_PROJECT_REV_FQDN "org.deskflow.deskflow") +set(CMAKE_PROJECT_DOMAIN "deskflow.org") + +# Pull in Synergy branding overrides. +include(${CMAKE_SOURCE_DIR}/extra/cmake/Synergy.cmake) #Unset the vars used in the project call unset(DESKFLOW_VERSION_MAJOR) @@ -183,10 +164,15 @@ endif() add_subdirectory(docs) +option(BUILD_GUI "Build GUI" ON) + # build translations before source, I18N unit tests fail if they are missing -add_subdirectory(translations) +if(BUILD_GUI) + add_subdirectory(translations) +endif() add_subdirectory(src) +add_subdirectory(extra) option(BUILD_INSTALLER "Build installer" ON) if(BUILD_INSTALLER) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 000000000..3f91b05c5 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,139 @@ +{ + "version": 2, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" + } + }, + { + "name": "minimal", + "hidden": true, + "environment": { + "SYNERGY_BUILD_MINIMAL": "ON" + } + }, + { + "name": "windows", + "inherits": "base", + "hidden": true, + "generator": "Ninja", + "cacheVariables": { + "CMAKE_C_COMPILER": "cl.exe", + "CMAKE_CXX_COMPILER": "cl.exe", + "CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake", + "QT_PATH": "$env{QT_PATH}" + }, + "architecture": { + "value": "x64", + "strategy": "external" + }, + "toolset": { + "value": "host=x64", + "strategy": "external" + } + }, + { + "name": "linux", + "hidden": true, + "inherits": "base", + "generator": "Unix Makefiles" + }, + { + "name": "macos", + "hidden": true, + "inherits": "base", + "generator": "Unix Makefiles", + "cacheVariables": { + "CMAKE_OSX_SYSROOT": "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk" + } + }, + { + "name": "windows-debug", + "inherits": "windows", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "windows-release", + "inherits": "windows", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "linux-debug", + "inherits": "linux", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "linux-release", + "inherits": "linux", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "macos-debug", + "inherits": "macos", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "macos-release", + "inherits": "macos", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "windows-debug-min", + "inherits": ["windows", "minimal"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "windows-release-min", + "inherits": ["windows", "minimal"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "linux-debug-min", + "inherits": ["linux", "minimal"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "linux-release-min", + "inherits": ["linux", "minimal"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "macos-debug-min", + "inherits": ["macos", "minimal"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "macos-release-min", + "inherits": ["macos", "minimal"], + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 000000000..157537874 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# Synergy + +[![CodeQL Analysis](https://github.com/symless/synergy/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/symless/synergy/actions/workflows/codeql-analysis.yml) +[![SonarCloud Analysis](https://github.com/symless/synergy/actions/workflows/sonarcloud-analysis.yml/badge.svg)](https://github.com/symless/synergy/actions/workflows/sonarcloud-analysis.yml) + +Use the keyboard, mouse, or trackpad of one computer to control nearby computers, and work seamlessly between them. + +- [Get Synergy](https://synergyapp.io) +- [Technical support](https://synergyapp.io/contact) + +This repository contains the source code used to build Synergy 1 and the Core for Synergy 3. +It's based on the upstream Deskflow community project, which is sponsored by Synergy. + +- [Contibute to Deskflow](https://deskflow.org) + +## FAQ + +### How do I build the source code? +If you're a customer, you generally don’t need to build Synergy yourself as we provide pre-built, tested releases. +However, if you're a customer looking to build Synergy from source, [contact us](https://synergyapp.io/contact) so we can help with that. +If you're a developer looking to contribute to an open source community, join us in our [Deskflow](https://deskflow.org) project. + +### What’s the difference between Synergy and Deskflow? +Synergy is a stable, supported commercial product. It is quality assurance tested, has a warranty, and is maintained by a team of full-time engineers. +Deskflow is the upstream project where the open source community, including Synergy engineers, prototype and iterate on new features. +Synergy is your business-ready solution; Deskflow is for open source contributors and early adopters. + +### Where should I file bugs or feature requests? +For supported customers, reach out to our [support team](https://synergyapp.io/contact) and we’ll triage and track issues internally. +If you're contributing to the community project, use [Deskflow issues](https://github.com/deskflow/deskflow/issues) to report bugs or request features. + +### Can I contribute code to Synergy? +We welcome contributions, but our community development happens upstream in Deskflow. +That’s the best place to propose changes and collaborate with the wider community. +Changes flow downstream to Synergy once they have matured enough and are ready for customer usage. + +### How often does Deskflow merge into Synergy? +We regularly port stable features and fixes from Deskflow into Synergy. +This involves QA, integration testing, and compliance review. Critical bug fixes are fast-tracked. +For specific timelines on particular bug fixes and features, please [get in touch](https://synergyapp.io/contact). + +### Is Deskflow stable? +Deskflow is intended for developers and contributors who can self-support and fix issues. +It’s not suitable for production or business-critical environments requiring stability guarantees. +For those cases, we recommend using Synergy. + +### Why have two projects? +This model lets us move fast without breaking things. Deskflow empowers rapid community-driven innovation. +Synergy delivers a stable, supported experience to customers. diff --git a/deploy/CMakeLists.txt b/deploy/CMakeLists.txt index c7b0ca781..d155d1309 100644 --- a/deploy/CMakeLists.txt +++ b/deploy/CMakeLists.txt @@ -33,7 +33,6 @@ else() message(STATUS "UNKNOWN System: ${CMAKE_SYSTEM_NAME}") endif() -# Always use "deskflow" for start of name set(CPACK_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME}-${PACKAGE_VERSION_LABEL}-${OS_STRING}") message(STATUS "Package Basename: ${CPACK_PACKAGE_FILE_NAME}") diff --git a/extra/CMakeLists.txt b/extra/CMakeLists.txt new file mode 100644 index 000000000..24207f25d --- /dev/null +++ b/extra/CMakeLists.txt @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: (C) 2012 - 2026 Synergy App Ltd +# SPDX-License-Identifier: MIT + +# Make synergy/... headers under this tree resolvable from anywhere that descends +# from this directory (and from upstream code once hooks are wired). +include_directories("${CMAKE_CURRENT_SOURCE_DIR}/src/lib") + +add_subdirectory(src) + +# Wire the synergy::hooks::* callsites in upstream src/. The synergy-gui sublib +# provides the inline functions in synergy/hooks/gui_hook.h that upstream calls +# behind the SYNERGY_EXTRA_HEADER guard. Setting the define + include path on +# the upstream gui target lets MainWindow.cpp / SettingsDialog.cpp / etc. find +# the header without editing upstream's CMakeLists. +if(TARGET gui) + target_compile_definitions(gui PRIVATE SYNERGY_EXTRA_HEADER) + target_include_directories(gui PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/lib) + # Cascade synergy-gui to anything that links gui (the synergy executable, + # but also unit tests that link libgui.a, which pull in MainWindow.cpp.o + # which now references synergy::hooks::* symbols). Plain signature matches + # upstream's gui CMakeLists. + target_link_libraries(gui synergy-gui) +endif() + +# Pick the gui app target name (Apple uses the proper-cased brand name). +if(APPLE) + set(_synergy_gui_app ${CMAKE_PROJECT_PROPER_NAME}) +else() + set(_synergy_gui_app ${CMAKE_PROJECT_NAME}) +endif() +if(TARGET ${_synergy_gui_app}) + target_compile_definitions(${_synergy_gui_app} PRIVATE SYNERGY_EXTRA_HEADER) + target_include_directories(${_synergy_gui_app} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/lib) +endif() diff --git a/extra/Synergy.test.example.conf b/extra/Synergy.test.example.conf new file mode 100644 index 000000000..88853b51a --- /dev/null +++ b/extra/Synergy.test.example.conf @@ -0,0 +1,64 @@ +# Synergy test settings: example/template. +# +# Copy to: +# ~/.config/Synergy/Synergy.test.conf (Linux) +# ~/Library/Synergy/Synergy.test.conf (macOS) +# %APPDATA%\Synergy\Synergy.test.conf (Windows) +# +# Sibling to Synergy.conf in the same dir. Edit this file instead of +# setting SYNERGY_TEST_* env vars (which are painful to thread into +# GUI launches on macOS/Windows). SYNERGY_TEST_* env vars still work and +# take precedence over this file when both are set, so CI/automation +# flows are unchanged. + +[test] + +# Master switch. When true: +# - The Test menu appears in the menu bar (fatal/critical-error triggers). +# - The other values in this file are honored. +# Set false (or leave the file absent) to disable test mode entirely. +# enabled=true + +# Force the license activation flow on. Useful for verifying activation +# behavior in development; left off by default so test mode (Test menu, +# URL overrides, etc.) can be on without forcing license prompts every +# launch. +# licensing=true + +# Override the URL the GUI POSTs to when activating a serial key. +# Default points at production. Use this to point at a local stack or +# staging environment during development. +# apiUrlActivate=http://localhost:4200/synergy/api/product/activate + +# Override the URL used for periodic license re-validation (the call +# the GUI makes after activation to confirm the license is still valid). +# Same default/use-case as apiUrlActivate. +# apiUrlCheck=http://localhost:4200/synergy/api/product/check + +# Pre-fill the activation dialog's serial key field. Hex-encoded payload +# matching the format the activation API expects. Test serial keys are +# distributed internally. Do not hardcode customer-visible keys here. +# serialKey= + +# Time-travel testing. UNIX epoch seconds; if set, the GUI behaves as if +# the system clock were at this point in time when checking license +# expiry, grace periods, etc. Use to verify "expired", "expiring soon", +# and "in grace period" UI without waiting for real wall-clock time to +# elapse. +# startTime=1751324400 + +[features] + +# Extra debug logging from the GUI. Logs every license-check decision, +# scope-switch step, settings reload. Useful when tracing a bug report. +# verbose=true + +# Skip the periodic remote license re-check. Use when developing offline +# or when the API endpoint is intentionally unavailable, so the GUI +# doesn't enter the grace-period flow. +# skipRemoteCheck=true + +# Treat expired licenses as still valid. Lets you start the core process +# with a known-expired test key without bypassing license enforcement +# entirely (useful for testing what features remain enabled vs blocked). +# allowExpiredLicenses=true diff --git a/extra/cmake/Synergy.cmake b/extra/cmake/Synergy.cmake new file mode 100644 index 000000000..1f94aa4a5 --- /dev/null +++ b/extra/cmake/Synergy.cmake @@ -0,0 +1,88 @@ +# SPDX-FileCopyrightText: (C) 2012 - 2026 Synergy App Ltd +# SPDX-License-Identifier: MIT + +# Must be included after deskflow's project() call and the CMAKE_PROJECT_* +# defaults are set, so these overrides take effect. +set(CMAKE_PROJECT_PROPER_NAME "Synergy") +set(CMAKE_PROJECT_VENDOR "Synergy App Ltd") +set(CMAKE_PROJECT_COPYRIGHT "(C) 2012-2026 ${CMAKE_PROJECT_VENDOR}") +set(CMAKE_PROJECT_CONTACT "${CMAKE_PROJECT_PROPER_NAME} ") +set(CMAKE_PROJECT_REV_FQDN "com.symless.synergy") +set(CMAKE_PROJECT_DOMAIN "synergyapp.io") +set(CMAKE_PROJECT_HOMEPAGE_URL "https://synergyapp.io") + +# Display brand. "Synergy 1" is the default user-facing name (window title, +# About dialog). When building as the Core, flip to "Synergy Core" so the +# same codebase ships under a different product label. +# Distinct from CMAKE_PROJECT_PROPER_NAME, which stays "Synergy" to keep file paths +# (~/.config/Synergy/, Synergy.conf) and Windows globals space-free. +option(SYNERGY_CORE_FLAVOR "Build as Synergy Core" OFF) +if(SYNERGY_CORE_FLAVOR) + set(SYNERGY_DISPLAY_NAME "Synergy Core") +else() + set(SYNERGY_DISPLAY_NAME "Synergy 1") +endif() +add_compile_definitions(SYNERGY_DISPLAY_NAME="${SYNERGY_DISPLAY_NAME}") + +# Core flavor seeds headless-build defaults (no GUI, no tests, no installer). +# No `FORCE` on the cache writes: the seed only fills empty slots, so a user +# passing -DBUILD_GUI=ON alongside the flavor flag still wins. +if(SYNERGY_CORE_FLAVOR) + set(BUILD_GUI OFF CACHE BOOL "Build GUI") + set(BUILD_TESTS OFF CACHE BOOL "Build tests") + set(BUILD_INSTALLER OFF CACHE BOOL "Build installer") +endif() + +# Don't run unit tests as part of the build. Devs can opt back in with +# -DSKIP_BUILD_TESTS=OFF if they want post-build ctest invocation. +set(SKIP_BUILD_TESTS ON CACHE BOOL "Skip build time test") + +# Resource paths consumed by extra/src/lib/synergy/gui/CMakeLists.txt. +set(GUI_RES_DIR "${CMAKE_SOURCE_DIR}/extra/src/apps/res") +set(GUI_QRC_FILE "${GUI_RES_DIR}/synergy.qrc") + +# Override deskflow's project name. This cascades into binary names +# (${CMAKE_PROJECT_NAME}-core, etc.), install paths, package names, +# translation file naming, and CPack metadata. Source files in src/apps/*/ +# are patched to use literal filenames since they previously assumed +# target name == source basename. +set(CMAKE_PROJECT_NAME synergy) + +# Synergy version mode suffix. Base version (MAJOR.MINOR.PATCH) is set in +# the root CMakeLists.txt. Default mode is dev; flip with -DSYNERGY_VERSION_RELEASE=ON +# or -DSYNERGY_VERSION_SNAPSHOT=ON for CI/release builds. +option(SYNERGY_VERSION_RELEASE "Release version" OFF) +option(SYNERGY_VERSION_SNAPSHOT "Snapshot version" OFF) + +# Compute revision count once. Used as both the +rN suffix in snapshot version +# strings and as CMAKE_PROJECT_VERSION_TWEAK (consumed by VersionInfo.h.in's +# kDisplayVersion ternary and src/apps/res/windows.rc.in's VER_VERSION 4th +# digit, which Windows update mechanisms key off for installer recognition). +set(_rev_count 0) +if(GIT_FOUND) + execute_process( + COMMAND ${GIT_EXECUTABLE} describe HEAD --tags --long --match "v[0-9]*" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + OUTPUT_VARIABLE _git_describe + ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(_git_describe MATCHES "-([0-9]+)-g") + set(_rev_count "${CMAKE_MATCH_1}") + endif() +endif() + +set(_base "${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH}") +if(SYNERGY_VERSION_RELEASE) + set(CMAKE_PROJECT_VERSION "${_base}") + set(CMAKE_PROJECT_VERSION_TWEAK 0) +elseif(SYNERGY_VERSION_SNAPSHOT) + set(CMAKE_PROJECT_VERSION "${_base}-snapshot+r${_rev_count}") + set(CMAKE_PROJECT_VERSION_TWEAK ${_rev_count}) +else() + set(CMAKE_PROJECT_VERSION "${_base}-dev") + set(CMAKE_PROJECT_VERSION_TWEAK ${_rev_count}) + add_compile_definitions(SYNERGY_VERSION_DEV) +endif() +unset(_base) +unset(_git_describe) +unset(_rev_count) diff --git a/extra/deploy/mac/bundle/Contents/Info.plist.in b/extra/deploy/mac/bundle/Contents/Info.plist.in new file mode 100644 index 000000000..90f4d5ad5 --- /dev/null +++ b/extra/deploy/mac/bundle/Contents/Info.plist.in @@ -0,0 +1,31 @@ + + + + CFBundleDevelopmentRegion + English + CFBundleDisplayName + @DESKFLOW_APP_NAME@ + CFBundleExecutable + @DESKFLOW_APP_ID@ + CFBundleIconFile + @DESKFLOW_APP_NAME@.icns + CFBundleIdentifier + @DESKFLOW_APP_ID@ + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + @DESKFLOW_APP_NAME@ + CFBundlePackageType + APPL + CFBundleSignature + @DESKFLOW_MAC_BUNDLE_CODE@ + CFBundleShortVersionString + @DESKFLOW_VERSION@ + CFBundleVersion + @DESKFLOW_VERSION@ + NSHumanReadableCopyright + © 2012-@DESKFLOW_BUILD_YEAR@ Synergy App Ltd + LSMinimumSystemVersion + 10.9.0 + + diff --git a/extra/deploy/mac/bundle/Contents/PkgInfo.in b/extra/deploy/mac/bundle/Contents/PkgInfo.in new file mode 100644 index 000000000..c50e7b961 --- /dev/null +++ b/extra/deploy/mac/bundle/Contents/PkgInfo.in @@ -0,0 +1 @@ +APPL@DESKFLOW_MAC_BUNDLE_CODE@ \ No newline at end of file diff --git a/extra/deploy/mac/bundle/Contents/Resources/Background.tiff b/extra/deploy/mac/bundle/Contents/Resources/Background.tiff new file mode 100644 index 000000000..e13ab35b3 Binary files /dev/null and b/extra/deploy/mac/bundle/Contents/Resources/Background.tiff differ diff --git a/extra/deploy/mac/bundle/Contents/Resources/Synergy.icns b/extra/deploy/mac/bundle/Contents/Resources/Synergy.icns new file mode 100644 index 000000000..8fc714750 Binary files /dev/null and b/extra/deploy/mac/bundle/Contents/Resources/Synergy.icns differ diff --git a/extra/deploy/mac/bundle/Contents/Resources/Volume.icns b/extra/deploy/mac/bundle/Contents/Resources/Volume.icns new file mode 100644 index 000000000..0e165618f Binary files /dev/null and b/extra/deploy/mac/bundle/Contents/Resources/Volume.icns differ diff --git a/extra/deploy/mac/dmgbuild/settings.py b/extra/deploy/mac/dmgbuild/settings.py new file mode 100644 index 000000000..16ff4b8ad --- /dev/null +++ b/extra/deploy/mac/dmgbuild/settings.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- + +# Example: https://dmgbuild.readthedocs.io/en/latest/example.html + +from __future__ import unicode_literals + +import os.path + +app = defines.get("app") +app_basename = os.path.basename(app) +format = defines.get("format", "UDBZ") +size = defines.get("size", None) +files = [app] +symlinks = {"Applications": "/Applications"} +icon = os.path.join(app, "Contents/Resources/Volume.icns") +icon_locations = { + app_basename: (144, 190), + "Applications": (455, 190), +} +background = os.path.join(app, "Contents/Resources/Background.tiff") +show_status_bar = False +show_tab_view = False +show_toolbar = False +show_pathbar = False +show_sidebar = False +sidebar_width = 180 +window_rect = ((200, 120), (620, 420)) +default_view = "icon-view" +show_icon_preview = False +include_icon_view_settings = "auto" +include_list_view_settings = "auto" +arrange_by = None +grid_offset = (0, 0) +grid_spacing = 100 +scroll_position = (0, 0) +label_pos = "bottom" +text_size = 16 +icon_size = 100 +list_icon_size = 16 +list_text_size = 12 +list_scroll_position = (0, 0) +list_sort_by = "name" +list_use_relative_dates = True +list_calculate_all_sizes = (False,) +list_columns = ("name", "date-modified", "size", "kind", "date-added") +list_column_widths = { + "name": 300, + "date-modified": 181, + "date-created": 181, + "date-added": 181, + "date-last-opened": 181, + "size": 97, + "kind": 115, + "label": 100, + "version": 75, + "comments": 300, +} +list_column_sort_directions = { + "name": "ascending", + "date-modified": "descending", + "date-created": "descending", + "date-added": "descending", + "date-last-opened": "descending", + "size": "descending", + "kind": "ascending", + "label": "ascending", + "version": "ascending", + "comments": "ascending", +} diff --git a/extra/deploy/wix/images/banner.png b/extra/deploy/wix/images/banner.png new file mode 100644 index 000000000..cbf9b4675 Binary files /dev/null and b/extra/deploy/wix/images/banner.png differ diff --git a/extra/deploy/wix/images/common_background.png b/extra/deploy/wix/images/common_background.png new file mode 100644 index 000000000..647e8b40c Binary files /dev/null and b/extra/deploy/wix/images/common_background.png differ diff --git a/extra/deploy/wix/images/dialog.png b/extra/deploy/wix/images/dialog.png new file mode 100644 index 000000000..c43c94470 Binary files /dev/null and b/extra/deploy/wix/images/dialog.png differ diff --git a/extra/deploy/wix/images/welcome_background.png b/extra/deploy/wix/images/welcome_background.png new file mode 100644 index 000000000..cbc306b21 Binary files /dev/null and b/extra/deploy/wix/images/welcome_background.png differ diff --git a/extra/src/CMakeLists.txt b/extra/src/CMakeLists.txt new file mode 100644 index 000000000..9c2b296e9 --- /dev/null +++ b/extra/src/CMakeLists.txt @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: (C) 2012 - 2026 Synergy App Ltd +# SPDX-License-Identifier: MIT + +add_subdirectory(lib) diff --git a/extra/src/apps/res/image/logo-dark.png b/extra/src/apps/res/image/logo-dark.png new file mode 100644 index 000000000..456e75eae Binary files /dev/null and b/extra/src/apps/res/image/logo-dark.png differ diff --git a/extra/src/apps/res/image/logo-light.png b/extra/src/apps/res/image/logo-light.png new file mode 100644 index 000000000..d0d640982 Binary files /dev/null and b/extra/src/apps/res/image/logo-light.png differ diff --git a/extra/src/apps/res/synergy.ico b/extra/src/apps/res/synergy.ico new file mode 100644 index 000000000..b35873027 Binary files /dev/null and b/extra/src/apps/res/synergy.ico differ diff --git a/extra/src/apps/res/synergy.png b/extra/src/apps/res/synergy.png new file mode 100644 index 000000000..15da1fa2e Binary files /dev/null and b/extra/src/apps/res/synergy.png differ diff --git a/extra/src/apps/res/synergy.qrc b/extra/src/apps/res/synergy.qrc new file mode 100644 index 000000000..11090b359 --- /dev/null +++ b/extra/src/apps/res/synergy.qrc @@ -0,0 +1,10 @@ + + + image/logo-dark.png + image/logo-light.png + synergy.svg + synergy.svg + synergy.svg + synergy.svg + + diff --git a/extra/src/apps/res/synergy.svg b/extra/src/apps/res/synergy.svg new file mode 100644 index 000000000..6e8edd76e --- /dev/null +++ b/extra/src/apps/res/synergy.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/extra/src/integtests/CMakeLists.txt b/extra/src/integtests/CMakeLists.txt new file mode 100644 index 000000000..dcb66dfbf --- /dev/null +++ b/extra/src/integtests/CMakeLists.txt @@ -0,0 +1,19 @@ +# Deskflow -- mouse and keyboard sharing utility +# Copyright (C) 2024 Synergy App Ltd +# +# This package is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# found in the file LICENSE that should have accompanied this file. +# +# This package is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +config_test() +set(target ${INTEG_TESTS_BIN}) +add_executable(${target} ${sources}) +target_link_libraries(${target} ${test_libs}) diff --git a/extra/src/lib/CMakeLists.txt b/extra/src/lib/CMakeLists.txt new file mode 100644 index 000000000..5b0c49c31 --- /dev/null +++ b/extra/src/lib/CMakeLists.txt @@ -0,0 +1,4 @@ +# SPDX-FileCopyrightText: (C) 2012 - 2026 Synergy App Ltd +# SPDX-License-Identifier: MIT + +add_subdirectory(synergy) diff --git a/extra/src/lib/synergy/CMakeLists.txt b/extra/src/lib/synergy/CMakeLists.txt new file mode 100644 index 000000000..b6db05c17 --- /dev/null +++ b/extra/src/lib/synergy/CMakeLists.txt @@ -0,0 +1,19 @@ +# Synergy -- mouse and keyboard sharing utility +# Copyright (C) 2024 Synergy App Ltd +# +# This package is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# found in the file LICENSE that should have accompanied this file. +# +# This package is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +add_subdirectory(license) +if(BUILD_GUI) + add_subdirectory(gui) +endif() diff --git a/extra/src/lib/synergy/build_config.h b/extra/src/lib/synergy/build_config.h new file mode 100644 index 000000000..84492ddc2 --- /dev/null +++ b/extra/src/lib/synergy/build_config.h @@ -0,0 +1,35 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2026 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +// Single point of translation from cmake-defined preprocessor macros to +// typed C++ constants. Code elsewhere should consume these constants and +// never reference SYNERGY_* macros directly. Keeps the macro surface +// contained and makes the build-flavor knobs greppable. + +namespace synergy { + +constexpr auto kDisplayName = SYNERGY_DISPLAY_NAME; + +#ifdef SYNERGY_VERSION_DEV +constexpr bool kIsDevBuild = true; +#else +constexpr bool kIsDevBuild = false; +#endif + +} // namespace synergy diff --git a/extra/src/lib/synergy/gui/ActivationDialog.cpp b/extra/src/lib/synergy/gui/ActivationDialog.cpp new file mode 100644 index 000000000..f1e8f2dc0 --- /dev/null +++ b/extra/src/lib/synergy/gui/ActivationDialog.cpp @@ -0,0 +1,231 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2016 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ActivationDialog.h" + +#include "CancelActivationDialog.h" +#include "common/Settings.h" +#include "synergy/gui/TestSettings.h" +#include "synergy/gui/constants.h" +#include "synergy/gui/license/LicenseHandler.h" +#include "synergy/gui/license/license_notices.h" +#include "synergy/gui/styles.h" +#include "synergy/license/parse_serial_key.h" +#include "ui_ActivationDialog.h" + +#include +#include +#include +#include +#include + +using namespace deskflow::gui; +using namespace synergy::gui; +using namespace synergy::license; + +const QString successTitle = "Serial key"; +const QString problemTitle = "Serial key problem"; + +ActivationDialog::ActivationDialog(QWidget *parent, LicenseHandler &licenseHandler) + : QDialog(parent), + m_ui(new Ui::ActivationDialog), + m_licenseHandler(licenseHandler) +{ + m_ui->setupUi(this); + + m_ui->m_pLabelNotice->setStyleSheet(kStyleNoticeLabel); + + refreshSerialKey(); +} + +ActivationDialog::~ActivationDialog() +{ + delete m_ui; +} + +void ActivationDialog::refreshSerialKey() +{ + const QString testSerialKey = TestSettings::instance().serialKey(); + if (!testSerialKey.isEmpty()) { + qDebug("using serial key from test settings"); + m_ui->m_pTextEditSerialKey->setText(testSerialKey); + } else { + qDebug("using serial key from config"); + const auto hexString = m_licenseHandler.license().serialKey().hexString; + m_ui->m_pTextEditSerialKey->setText(QString::fromStdString(hexString)); + } + + m_ui->m_pTextEditSerialKey->setFocus(); + m_ui->m_pTextEditSerialKey->moveCursor(QTextCursor::End); + + const auto &license = m_licenseHandler.license(); + if (license.isTimeLimited() && (license.isExpired() || license.isExpiringSoon())) { + m_ui->m_pLabelNotice->setText(licenseNotice(license, kColorWhite)); + m_ui->m_widgetNotice->show(); + } else { + m_ui->m_widgetNotice->hide(); + } +} + +void ActivationDialog::showEvent(QShowEvent *event) +{ + QDialog::showEvent(event); + + QTimer::singleShot(0, this, [this]() { + const auto &license = m_licenseHandler.license(); + if (license.isTimeLimited()) { + const auto notice = licenseNotice(license, kColorSecondary); + if (license.isExpired()) { + QMessageBox::warning( + this, "License expired", + tr("%1" + "

The application will now stop working. Please renew your license today to continue using the " + "application.

" + "

Once you have received your new serial key, you can enter it on the next screen.

") + .arg(notice) + ); + } else if (license.isExpiringSoon()) { + QMessageBox::warning( + this, "License expiring soon", + tr("%1" + "

If your license expires, the application will stop working. Please renew your license today to " + "avoid any interruptions.

" + "

Once you have received your new serial key, you can enter it on the next screen.

") + .arg(notice) + ); + } + } + }); +} + +void ActivationDialog::reject() +{ + // don't show the cancel confirmation dialog if they've already registered, + // since it's not relevant to customers who are changing their serial key. + const auto &license = m_licenseHandler.license(); + if (license.isValid() && !license.isExpired()) { + QDialog::reject(); + return; + } + + // the accept button should be labeled "Exit" on the cancel dialog. + CancelActivationDialog cancelActivationDialog(this); + if (cancelActivationDialog.exec() == QDialog::Accepted) { + QApplication::exit(); + } +} + +void ActivationDialog::accept() +{ + using Result = LicenseHandler::SetSerialKeyResult; + auto serialKey = m_ui->m_pTextEditSerialKey->toPlainText(); + + if (serialKey.isEmpty()) { + QMessageBox::information(this, "Activation", "Please enter a serial key."); + return; + } + + const auto result = m_licenseHandler.setLicense(serialKey); + if (result == Result::kUnchanged) { + qInfo() << "serial key did not change, nothing to do"; + QDialog::accept(); + return; + } + + if (result != Result::kSuccess) { + showResultDialog(result); + return; + } + + m_serialKeyChanged = true; + showSuccessDialog(); + QDialog::accept(); +} + +void ActivationDialog::showResultDialog(LicenseHandler::SetSerialKeyResult result) +{ + switch (result) { + using enum LicenseHandler::SetSerialKeyResult; + + case kInvalid: + QMessageBox::warning( + this, problemTitle, + QString( + "Invalid serial key. " + R"(Please contact us for help.)" + ) + .arg(kUrlContact) + .arg(kColorSecondary) + ); + break; + + case kExpired: + QMessageBox::warning( + this, problemTitle, + QString( + "Sorry, that serial key has expired. " + R"(Please renew your license.)" + ) + .arg(kUrlContact) + .arg(kColorSecondary) + ); + break; + + default: + qFatal("unexpected change serial key result: %d", static_cast(result)); + } +} + +void ActivationDialog::showSuccessDialog() +{ + const auto &license = m_licenseHandler.license(); + + QString title = successTitle; + QString message = tr("

Thanks for entering your serial key for %1.

").arg(m_licenseHandler.productName()); + + const auto tlsAvailable = m_licenseHandler.license().isTlsAvailable(); + if (tlsAvailable && Settings::value(Settings::Security::TlsEnabled).toBool()) { + message += "

To ensure that TLS encryption works correctly, " + "please use the same serial key on all of your computers.

"; + } + + if (license.isTimeLimited()) { + auto daysLeft = license.daysLeft().count(); + if (license.isTrial()) { + title = "Trial started"; + message += QString("Your trial will expire in %1 %2.").arg(daysLeft).arg((daysLeft == 1) ? "day" : "days"); + } else if (license.isSubscription()) { + message += QString("Your license will expire in %1 %2.").arg(daysLeft).arg((daysLeft == 1) ? "day" : "days"); + } + } + + QMessageBox::information(this, title, message); +} + +void ActivationDialog::showErrorDialog(const QString &message) +{ + QString fullMessage = QString( + "

There was a problem with your serial key.

" + R"(

Please contact us )" + "and provide the following information:

" + "%3" + ) + .arg(kUrlContact) + .arg(kColorSecondary) + .arg(message); + QMessageBox::warning(this, problemTitle, fullMessage); +} diff --git a/extra/src/lib/synergy/gui/ActivationDialog.h b/extra/src/lib/synergy/gui/ActivationDialog.h new file mode 100644 index 000000000..2e1389dae --- /dev/null +++ b/extra/src/lib/synergy/gui/ActivationDialog.h @@ -0,0 +1,65 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2016 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "synergy/gui/license/LicenseHandler.h" + +#include + +namespace Ui { +class ActivationDialog; +} + +class ActivationDialog : public QDialog +{ + Q_OBJECT + +public: + ActivationDialog(QWidget *parent, LicenseHandler &licenseHandler); + ~ActivationDialog() override; + + class ActivationMessageError : public std::runtime_error + { + public: + ActivationMessageError() : std::runtime_error("could not show activation message") + { + } + }; + + bool serialKeyChanged() const + { + return m_serialKeyChanged; + } + +public Q_SLOTS: + void reject() override; + void accept() override; + +protected: + void refreshSerialKey(); + +private: + void showResultDialog(LicenseHandler::SetSerialKeyResult result); + void showSuccessDialog(); + void showErrorDialog(const QString &message); + void showEvent(QShowEvent *) override; + + Ui::ActivationDialog *m_ui; + LicenseHandler &m_licenseHandler; + bool m_serialKeyChanged = false; +}; diff --git a/extra/src/lib/synergy/gui/ActivationDialog.ui b/extra/src/lib/synergy/gui/ActivationDialog.ui new file mode 100644 index 000000000..909b9ab7b --- /dev/null +++ b/extra/src/lib/synergy/gui/ActivationDialog.ui @@ -0,0 +1,153 @@ + + + ActivationDialog + + + + 0 + 0 + 541 + 241 + + + + Serial key + + + + + + + true + + + + Enter your serial key + + + + + + + <p>Your serial key is on your <a href="https://synergyapp.io/account?source=gui" style="color:#4285f4;">account</span></a> page. Don't have a license? <a href="https://synergyapp.io/contact?source=gui" style="color:#4285f4;">Contact us</span></a></p> + + + true + + + + + + + true + + + true + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> +p, li { white-space: pre-wrap; } +hr { height: 1px; border-width: 0; } +li.unchecked::marker { content: "\2610"; } +li.checked::marker { content: "\2612"; } +</style></head><body style=" font-family:'Cantarell'; font-size:11pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Sans'; font-size:10pt;"><br /></p></body></html> + + + false + + + + + + + + 2 + + + 0 + + + 0 + + + 8 + + + + + m_pLabelNotice + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + m_pTextEditSerialKey + + + + + buttonBox + accepted() + ActivationDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + ActivationDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/extra/src/lib/synergy/gui/AppTime.cpp b/extra/src/lib/synergy/gui/AppTime.cpp new file mode 100644 index 000000000..7f27594ff --- /dev/null +++ b/extra/src/lib/synergy/gui/AppTime.cpp @@ -0,0 +1,49 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "AppTime.h" + +#include "synergy/gui/TestSettings.h" + +#include + +namespace synergy::gui { + +AppTime::AppTime() +{ + m_realStartTime = std::chrono::system_clock::now(); + if (const auto testTime = TestSettings::instance().startTimeEpochSecs(); testTime != 0) { + qDebug("setting test time to: %lld", static_cast(testTime)); + m_testStartTime = std::chrono::seconds{testTime}; + } +} + +bool AppTime::hasTestTime() const +{ + return m_testStartTime.has_value(); +} + +AppTime::TimePoint AppTime::now() +{ + if (m_testStartTime.has_value()) { + const auto runtime = std::chrono::system_clock::now() - m_realStartTime; + return TimePoint{m_testStartTime.value()} + runtime; + } + return std::chrono::system_clock::now(); +} + +} // namespace synergy::gui diff --git a/extra/src/lib/synergy/gui/AppTime.h b/extra/src/lib/synergy/gui/AppTime.h new file mode 100644 index 000000000..97aeac4a7 --- /dev/null +++ b/extra/src/lib/synergy/gui/AppTime.h @@ -0,0 +1,39 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +namespace synergy::gui { + +class AppTime +{ + using TimePoint = std::chrono::system_clock::time_point; + +public: + AppTime(); + TimePoint now(); + bool hasTestTime() const; + +private: + std::optional m_testStartTime = std::nullopt; + TimePoint m_realStartTime = std::chrono::system_clock::now(); +}; + +} // namespace synergy::gui diff --git a/extra/src/lib/synergy/gui/CMakeLists.txt b/extra/src/lib/synergy/gui/CMakeLists.txt new file mode 100644 index 000000000..9b6518254 --- /dev/null +++ b/extra/src/lib/synergy/gui/CMakeLists.txt @@ -0,0 +1,42 @@ +# Synergy -- mouse and keyboard sharing utility +# Copyright (C) 2024 Synergy App Ltd +# +# This package is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# found in the file LICENSE that should have accompanied this file. +# +# This package is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(target synergy-gui) + +set(res_dir ${GUI_RES_DIR}) +set(qrc_file ${GUI_QRC_FILE}) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +file(GLOB_RECURSE sources *.cpp) +file(GLOB_RECURSE headers *.h) +file(GLOB_RECURSE ui_files *.ui) + +if(ADD_HEADERS_TO_SOURCES) + list(APPEND sources ${headers}) +endif() + +add_library(${target} STATIC ${sources} ${ui_files} ${qrc_file}) + +target_link_libraries( + ${target} + gui + license + Qt6::Core + Qt6::Widgets + Qt6::Network) diff --git a/extra/src/lib/synergy/gui/CancelActivationDialog.cpp b/extra/src/lib/synergy/gui/CancelActivationDialog.cpp new file mode 100644 index 000000000..21e217c9f --- /dev/null +++ b/extra/src/lib/synergy/gui/CancelActivationDialog.cpp @@ -0,0 +1,35 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * Copyright (C) 2016 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "CancelActivationDialog.h" + +#include "ui_CancelActivationDialog.h" + +#include "QPushButton" + +CancelActivationDialog::CancelActivationDialog(QWidget *parent) : QDialog(parent), ui(new Ui::CancelActivationDialog) +{ + ui->setupUi(this); + + ui->m_pButtonBox->button(QDialogButtonBox::Cancel)->setText("&Back"); + ui->m_pButtonBox->button(QDialogButtonBox::Ok)->setText("&Exit"); +} + +CancelActivationDialog::~CancelActivationDialog() +{ + delete ui; +} diff --git a/extra/src/lib/synergy/gui/CancelActivationDialog.h b/extra/src/lib/synergy/gui/CancelActivationDialog.h new file mode 100644 index 000000000..05362214a --- /dev/null +++ b/extra/src/lib/synergy/gui/CancelActivationDialog.h @@ -0,0 +1,36 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * Copyright (C) 2016 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +namespace Ui { +class CancelActivationDialog; +} + +class CancelActivationDialog : public QDialog +{ + Q_OBJECT + +public: + explicit CancelActivationDialog(QWidget *parent = 0); + ~CancelActivationDialog(); + +private: + Ui::CancelActivationDialog *ui; +}; diff --git a/extra/src/lib/synergy/gui/CancelActivationDialog.ui b/extra/src/lib/synergy/gui/CancelActivationDialog.ui new file mode 100644 index 000000000..7640407ea --- /dev/null +++ b/extra/src/lib/synergy/gui/CancelActivationDialog.ui @@ -0,0 +1,77 @@ + + + CancelActivationDialog + + + + 0 + 0 + 429 + 273 + + + + Cancel Activation + + + + + + <html><head/><body><p>You'll need to purchase a license to use Synergy.</p><p><a href="https://synergyapp.io/contact?source=gui"><span style=" text-decoration: underline; color:#007af4;">Contact us</span></a></p><p>The application will now exit.</p></body></html> + + + true + + + true + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + + + m_pButtonBox + accepted() + CancelActivationDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + m_pButtonBox + rejected() + CancelActivationDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/extra/src/lib/synergy/gui/ExtraSettings.cpp b/extra/src/lib/synergy/gui/ExtraSettings.cpp new file mode 100644 index 000000000..3de9b3dc2 --- /dev/null +++ b/extra/src/lib/synergy/gui/ExtraSettings.cpp @@ -0,0 +1,82 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "ExtraSettings.h" + +#include "common/Constants.h" +#include "common/Settings.h" + +#include +#include +#include + +namespace synergy::gui { + +namespace { + +// Persisted synergy-side state lives in its own sibling file rather than +// upstream's Synergy.conf, because upstream's Settings::cleanSettings() +// strips any key not in its allow-list at startup. Keeping our state out +// of that file means no upstream patch + no key-allow-list maintenance. +QString settingsFile() +{ + return QStringLiteral("%1/%2.extra.conf").arg(Settings::UserDir, kAppName); +} + +const auto kSerialKey = QStringLiteral("serialKey"); +const auto kActivated = QStringLiteral("activated"); +const auto kGraceStart = QStringLiteral("graceStartEpochSecs"); + +} // namespace + +void ExtraSettings::load() +{ + QSettings ini(settingsFile(), QSettings::IniFormat); + m_serialKey = ini.value(kSerialKey).toString(); + m_activated = ini.value(kActivated).toBool(); + m_graceStartEpochSecs = ini.value(kGraceStart).toLongLong(); +} + +void ExtraSettings::sync() +{ + QSettings ini(settingsFile(), QSettings::IniFormat); + if (!ini.isWritable()) { + qCritical() << "unable to save synergy settings, file not writable:" << ini.fileName(); + return; + } + ini.setValue(kSerialKey, m_serialKey); + ini.setValue(kActivated, m_activated); + ini.setValue(kGraceStart, m_graceStartEpochSecs); + ini.sync(); +} + +QString ExtraSettings::fileName() const +{ + return settingsFile(); +} + +bool ExtraSettings::isWritable() const +{ + QFileInfo info(settingsFile()); + if (info.exists()) { + return info.isWritable(); + } + // If the file doesn't exist yet, writability depends on the parent dir. + return QFileInfo(info.absolutePath()).isWritable(); +} + +} // namespace synergy::gui diff --git a/extra/src/lib/synergy/gui/ExtraSettings.h b/extra/src/lib/synergy/gui/ExtraSettings.h new file mode 100644 index 000000000..ce737cc76 --- /dev/null +++ b/extra/src/lib/synergy/gui/ExtraSettings.h @@ -0,0 +1,70 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +namespace synergy::gui { + +// Synergy license state (serial key, activation, grace period). Stored as +// synergy/* keys in the upstream Settings file via the new static Settings API. +class ExtraSettings +{ +public: + ExtraSettings() = default; + + void load(); + void sync(); + + QString serialKey() const + { + return m_serialKey; + } + void setSerialKey(const QString &serialKey) + { + m_serialKey = serialKey; + } + + bool activated() const + { + return m_activated; + } + void setActivated(bool activated) + { + m_activated = activated; + } + + qint64 graceStartEpochSecs() const + { + return m_graceStartEpochSecs; + } + void setGraceStartEpochSecs(qint64 epochSecs) + { + m_graceStartEpochSecs = epochSecs; + } + + QString fileName() const; + bool isWritable() const; + +private: + QString m_serialKey; + bool m_activated = false; + qint64 m_graceStartEpochSecs = 0; +}; + +} // namespace synergy::gui diff --git a/extra/src/lib/synergy/gui/FeatureHandler.cpp b/extra/src/lib/synergy/gui/FeatureHandler.cpp new file mode 100644 index 000000000..a5b6f9d96 --- /dev/null +++ b/extra/src/lib/synergy/gui/FeatureHandler.cpp @@ -0,0 +1,183 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2025 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "FeatureHandler.h" + +#include "common/Settings.h" +#include "license/LicenseHandler.h" +#include "synergy/gui/SettingsMigration.h" +#include "synergy/gui/SettingsScope.h" +#include "synergy/gui/TestSettings.h" +#include "synergy/gui/constants.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace synergy::gui; + +void FeatureHandler::handleMainWindow(QMainWindow *mainWindow) +{ + m_pMainWindow = mainWindow; +} + +void FeatureHandler::handleAppStart() +{ + // wl-clipboard's focus-stealing behavior on common compositors makes it + // unfit for shipping; force the setting off on every launch regardless + // of how it got set. + Settings::setValue(Settings::Core::UseWlClipboard, false); + + if (TestSettings::instance().isEnabled()) { + addTestMenu(); + } +} + +void FeatureHandler::addTestMenu() +{ + if (m_pMainWindow == nullptr) { + qWarning("cannot add test menu, main window not set"); + return; + } + + auto testMenu = new QMenu(QObject::tr("Test"), m_pMainWindow); + m_pMainWindow->menuBar()->addMenu(testMenu); + + auto fatalAction = new QAction(QObject::tr("Trigger fatal error"), m_pMainWindow); + QObject::connect(fatalAction, &QAction::triggered, [] { qFatal("test fatal error"); }); + testMenu->addAction(fatalAction); + + auto criticalAction = new QAction(QObject::tr("Trigger critical error"), m_pMainWindow); + QObject::connect(criticalAction, &QAction::triggered, [] { qCritical("test critical error"); }); + testMenu->addAction(criticalAction); +} + +void FeatureHandler::handleSettings(QDialog *parent) const +{ + if (parent == nullptr) { + return; + } + if (auto *wlClipboard = parent->findChild(QStringLiteral("widgetWlClipboard"))) { + wlClipboard->hide(); + } + + const auto &licenseHandler = LicenseHandler::instance(); + if (licenseHandler.isEnabled() && !licenseHandler.license().isSettingsScopeAvailable()) { + return; + } + addScopeTab(parent); +} + +namespace { + +QString pathLabel(const QString &path) +{ + if (QFileInfo::exists(path)) { + return QStringLiteral("%1").arg(path); + } + return QStringLiteral("%1 %2") + .arg(path, QObject::tr("(not yet created)")); +} + +} // namespace + +void FeatureHandler::addScopeTab(QDialog *parent) const +{ + auto *tabWidget = parent->findChild(); + if (tabWidget == nullptr) { + qWarning("scope tab: no QTabWidget in settings dialog, skipping"); + return; + } + + auto *scopeTab = new QWidget(tabWidget); + auto *layout = new QVBoxLayout(scopeTab); + + auto *intro = new QLabel( + QObject::tr("Choose where %1 stores its settings. Changes take effect when %1 restarts.") + .arg(QApplication::applicationName()), + scopeTab + ); + intro->setWordWrap(true); + layout->addWidget(intro); + layout->addSpacing(8); + + const auto mutedColor = scopeTab->palette().color(QPalette::Disabled, QPalette::WindowText).name(); + const auto helpStyle = QStringLiteral("color: %1;").arg(mutedColor); + const auto pathStyle = QStringLiteral("color: %1; font-size: 10pt;").arg(mutedColor); + + const auto buildOption = [&](const QString &title, const QString &help, const QString &path) { + auto *radio = new QRadioButton(title, scopeTab); + auto *helpLabel = new QLabel(help, scopeTab); + helpLabel->setWordWrap(true); + helpLabel->setIndent(22); + helpLabel->setStyleSheet(helpStyle); + auto *pathWidget = new QLabel(pathLabel(path), scopeTab); + pathWidget->setIndent(22); + pathWidget->setOpenExternalLinks(true); + pathWidget->setTextInteractionFlags(Qt::TextBrowserInteraction); + pathWidget->setWordWrap(true); + pathWidget->setStyleSheet(pathStyle); + + layout->addWidget(radio); + layout->addWidget(helpLabel); + layout->addWidget(pathWidget); + layout->addSpacing(14); + return radio; + }; + + auto *userRadio = buildOption( + QObject::tr("Current user"), + QObject::tr("Settings apply only to your user account on this computer."), + Settings::UserSettingFile + ); + auto *systemRadio = buildOption( + QObject::tr("All users"), + QObject::tr("Settings apply to every user on this computer. Requires administrator privileges."), + Settings::SystemSettingFile + ); + + const bool isSystem = (Settings::settingsFile() == Settings::SystemSettingFile); + userRadio->setChecked(!isSystem); + systemRadio->setChecked(isSystem); + + layout->addStretch(); + + QObject::connect(systemRadio, &QRadioButton::toggled, parent, [parent, userRadio, systemRadio](bool toSystem) { + if (SettingsScope::switchTo(parent, toSystem)) { + return; + } + QSignalBlocker blockUser(userRadio); + QSignalBlocker blockSystem(systemRadio); + userRadio->setChecked(!toSystem); + systemRadio->setChecked(toSystem ? false : true); + }); + + tabWidget->addTab(scopeTab, QObject::tr("Scope")); +} diff --git a/extra/src/lib/synergy/gui/FeatureHandler.h b/extra/src/lib/synergy/gui/FeatureHandler.h new file mode 100644 index 000000000..fa59accf1 --- /dev/null +++ b/extra/src/lib/synergy/gui/FeatureHandler.h @@ -0,0 +1,41 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2025 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +class QDialog; +class QMainWindow; + +class FeatureHandler +{ +public: + static FeatureHandler &instance() + { + static FeatureHandler instance; + return instance; + } + + void handleMainWindow(QMainWindow *mainWindow); + void handleAppStart(); + void handleSettings(QDialog *parent) const; + +private: + void addTestMenu(); + void addScopeTab(QDialog *parent) const; + + QMainWindow *m_pMainWindow = nullptr; +}; diff --git a/extra/src/lib/synergy/gui/SettingsMigration.cpp b/extra/src/lib/synergy/gui/SettingsMigration.cpp new file mode 100644 index 000000000..c73156f7e --- /dev/null +++ b/extra/src/lib/synergy/gui/SettingsMigration.cpp @@ -0,0 +1,308 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2026 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "SettingsMigration.h" + +#include "common/Constants.h" +#include "common/Settings.h" +#include "synergy/gui/SettingsScope.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace synergy::gui::migration { + +namespace { + +const auto kSchemaKey = QStringLiteral("migration/schemaVersion"); +const auto kNotifiedKey = QStringLiteral("migration/notifiedFor"); +const auto kLegacySystemScopeKey = QStringLiteral("systemScope"); + +QString extraFile() +{ + return QStringLiteral("%1/%2.extra.conf").arg(Settings::UserDir, kAppName); +} + +int storedSchemaVersion() +{ + QSettings ini(extraFile(), QSettings::IniFormat); + return ini.value(kSchemaKey, 0).toInt(); +} + +void writeSchemaVersion(int version) +{ + QSettings ini(extraFile(), QSettings::IniFormat); + ini.setValue(kSchemaKey, version); + ini.sync(); +} + +int notifiedSchemaVersion() +{ + QSettings ini(extraFile(), QSettings::IniFormat); + return ini.value(kNotifiedKey, 0).toInt(); +} + +void writeNotifiedVersion(int version) +{ + QSettings ini(extraFile(), QSettings::IniFormat); + ini.setValue(kNotifiedKey, version); + ini.sync(); +} + +std::optional> mapKey(const QString &oldKey, const QVariant &value) +{ + if (oldKey == "screenName") { + return std::make_pair(Settings::Core::ComputerName, value); + } + if (oldKey == "port") { + return std::make_pair(Settings::Core::Port, value); + } + if (oldKey == "interface") { + return std::make_pair(Settings::Core::Interface, value); + } + if (oldKey == "logLevel2") { + return std::make_pair(Settings::Log::Level, value); + } + if (oldKey == "logToFile") { + return std::make_pair(Settings::Log::ToFile, value); + } + if (oldKey == "logFilename") { + return std::make_pair(Settings::Log::File, value); + } + if (oldKey == "elevateModeEnum") { + return std::make_pair(Settings::Daemon::Elevate, value); + } + if (oldKey == "cryptoEnabled") { + return std::make_pair(Settings::Security::TlsEnabled, value); + } + if (oldKey == "autoHide") { + return std::make_pair(Settings::Gui::Autohide, value); + } + if (oldKey == "lastVersion") { + return std::make_pair(Settings::Core::LastVersion, value); + } + if (oldKey == "groupServerChecked") { + if (value.toBool()) { + return std::make_pair(Settings::Core::CoreMode, QVariant(Settings::Server)); + } + return std::nullopt; + } + if (oldKey == "groupClientChecked") { + if (value.toBool()) { + return std::make_pair(Settings::Core::CoreMode, QVariant(Settings::Client)); + } + return std::nullopt; + } + if (oldKey == "useExternalConfig") { + return std::make_pair(Settings::Server::ExternalConfig, value); + } + if (oldKey == "configFile") { + return std::make_pair(Settings::Server::ExternalConfigFile, value); + } + if (oldKey == "serverHostname") { + return std::make_pair(Settings::Client::RemoteHost, value); + } + if (oldKey == "tlsCertPath") { + return std::make_pair(Settings::Security::Certificate, value); + } + if (oldKey == "tlsKeyLength") { + return std::make_pair(Settings::Security::KeySize, value); + } + if (oldKey == "preventSleep") { + return std::make_pair(Settings::Core::PreventSleep, value); + } + if (oldKey == "languageSync") { + return std::make_pair(Settings::Client::LanguageSync, value); + } + if (oldKey == "invertScrollDirection") { + return std::make_pair(Settings::Client::InvertYScroll, value); + } + if (oldKey == "enableService") { + const auto mode = value.toBool() ? Settings::Service : Settings::Desktop; + return std::make_pair(Settings::Core::ProcessMode, QVariant(mode)); + } + if (oldKey == "closeToTray") { + return std::make_pair(Settings::Gui::CloseToTray, value); + } + if (oldKey == "showCloseReminder") { + return std::make_pair(Settings::Gui::CloseReminder, value); + } + if (oldKey == "enableUpdateCheck") { + return std::make_pair(Settings::Gui::AutoUpdateCheck, value); + } + return std::nullopt; +} + +// File-existence isn't a usable signal: on Linux the legacy NativeFormat +// path coincides with the new IniFormat path, so the file is always +// "present". Sentinel keys discriminate. +bool looksLikeLegacy(const QSettings &legacy) +{ + return legacy.contains(QStringLiteral("startedBefore")) || legacy.contains(QStringLiteral("groupServerChecked")) || + legacy.contains(QStringLiteral("groupClientChecked")) || legacy.contains(kLegacySystemScopeKey); +} + +// Dumps in-memory contents instead of file-copying so the backup format is +// uniform regardless of the legacy backend (file, plist, or registry). +QString backupLegacy(const QSettings &legacy, const QString &destPath) +{ + if (QFile::exists(destPath)) { + QFile::remove(destPath); + } + QSettings backup(destPath, QSettings::IniFormat); + for (const auto &key : legacy.allKeys()) { + backup.setValue(key, legacy.value(key)); + } + backup.sync(); + return destPath; +} + +// Master had hardcoded behavior where beta exposes settings; align beta's +// defaults with master's runtime behavior so migrated users see no change. +void applyMasterCompatDefaults(const QString &newPath) +{ + QSettings newSettings(newPath, QSettings::IniFormat); + newSettings.setValue(Settings::Gui::AutoStartCore, true); + newSettings.sync(); +} + +int migrateOneScope(QSettings &legacy, const QString &newPath) +{ + QSettings newSettings(newPath, QSettings::IniFormat); + int migrated = 0; + int dropped = 0; + for (const auto &oldKey : legacy.allKeys()) { + if (auto mapped = mapKey(oldKey, legacy.value(oldKey)); mapped.has_value()) { + newSettings.setValue(mapped->first, mapped->second); + migrated++; + } else { + dropped++; + } + } + newSettings.sync(); + qInfo("settings migration: scope %s, migrated %d keys, dropped %d obsolete", qPrintable(newPath), migrated, dropped); + return migrated; +} + +bool s_migrationRanThisLaunch = false; +QString s_lastBackupPath; + +// On Linux, NativeFormat resolves to the same file beta's Settings uses, +// so clear() on the legacy QSettings would wipe the keys we just wrote. +// On macOS/Windows the storage backends are distinct (plist / registry). +bool sharesPathWith(const QSettings &legacy, const QString &newPath) +{ + const auto legacyCanonical = QFileInfo(legacy.fileName()).canonicalFilePath(); + const auto newCanonical = QFileInfo(newPath).canonicalFilePath(); + if (legacyCanonical.isEmpty() || newCanonical.isEmpty()) { + return QFileInfo(legacy.fileName()).absoluteFilePath() == QFileInfo(newPath).absoluteFilePath(); + } + return legacyCanonical == newCanonical; +} + +void maybeClearLegacy(QSettings &legacy, const QString &newPath, const char *scopeLabel) +{ + if (sharesPathWith(legacy, newPath)) { + qDebug("settings migration: %s legacy shares storage with new file, skipping clear", scopeLabel); + return; + } + if (!legacy.isWritable()) { + qWarning("settings migration: %s legacy not writable, leaving legacy keys in place", scopeLabel); + return; + } + legacy.clear(); + legacy.sync(); + qInfo("settings migration: cleared %s legacy storage at %s", scopeLabel, qPrintable(legacy.fileName())); +} + +bool runLegacyMigration() +{ + bool any = false; + + QSettings legacyUser(QSettings::NativeFormat, QSettings::UserScope, kAppName, kAppName); + const bool legacyHadSystemScope = legacyUser.value(kLegacySystemScopeKey, false).toBool(); + if (looksLikeLegacy(legacyUser)) { + s_lastBackupPath = backupLegacy(legacyUser, Settings::UserSettingFile + QStringLiteral(".legacy.bak")); + qInfo().noquote() << "settings migration: user-scope legacy backed up to" << s_lastBackupPath; + if (migrateOneScope(legacyUser, Settings::UserSettingFile) > 0) { + any = true; + applyMasterCompatDefaults(Settings::UserSettingFile); + } + maybeClearLegacy(legacyUser, Settings::UserSettingFile, "user-scope"); + } + + QSettings legacySystem(QSettings::NativeFormat, QSettings::SystemScope, kAppName, kAppName); + if (looksLikeLegacy(legacySystem)) { + const auto backupPath = Settings::SystemSettingFile + QStringLiteral(".legacy.bak"); + s_lastBackupPath = backupLegacy(legacySystem, backupPath); + qInfo().noquote() << "settings migration: system-scope legacy backed up to" << s_lastBackupPath; + if (migrateOneScope(legacySystem, Settings::SystemSettingFile) > 0) { + any = true; + applyMasterCompatDefaults(Settings::SystemSettingFile); + } + maybeClearLegacy(legacySystem, Settings::SystemSettingFile, "system-scope"); + } + + if (legacyHadSystemScope) { + SettingsScope::setPreferSystem(true); + } + + return any; +} + +} // namespace + +bool migrateIfNeeded() +{ + if (storedSchemaVersion() >= kCurrentSchemaVersion) { + return false; + } + + s_migrationRanThisLaunch = runLegacyMigration(); + writeSchemaVersion(kCurrentSchemaVersion); + return s_migrationRanThisLaunch; +} + +void showNoticeIfPending(QWidget *parent) +{ + if (notifiedSchemaVersion() >= kCurrentSchemaVersion) { + return; + } + if (!s_migrationRanThisLaunch) { + writeNotifiedVersion(kCurrentSchemaVersion); + return; + } + + QMessageBox::information( + parent, QObject::tr("Settings updated"), + QObject::tr("

We've migrated your settings to a new format used by this version of Synergy.

" + "

Your previous settings have been backed up to:

" + "

%1

" + "

If anything looks different, please contact us.

") + .arg(s_lastBackupPath) + ); + writeNotifiedVersion(kCurrentSchemaVersion); +} + +} // namespace synergy::gui::migration diff --git a/extra/src/lib/synergy/gui/SettingsMigration.h b/extra/src/lib/synergy/gui/SettingsMigration.h new file mode 100644 index 000000000..ace999331 --- /dev/null +++ b/extra/src/lib/synergy/gui/SettingsMigration.h @@ -0,0 +1,49 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2026 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +class QWidget; + +namespace synergy::gui::migration { + +/// Bumped each time a new migration is added. +constexpr int kCurrentSchemaVersion = 1; + +/** + * @brief Ports legacy-format settings (Synergy 1.x, both user and system + * scope, native QSettings backend) into the new Settings layout. + * + * Must run before Settings::instance() is constructed: upstream's + * cleanSettings() strips any key not in its allow-list, which would erase + * the legacy keys before this can read them. Idempotent, gated on + * migration/schemaVersion in Synergy.extra.conf. + * + * @return true if a migration was performed this launch. + */ +bool migrateIfNeeded(); + +/** + * @brief Modal one-time notice shown after MainWindow is open. + * + * No-op unless a migration ran since the last call. Marks + * migration/notifiedFor=schemaVersion when dismissed so a new migration + * (bumped schema version) shows the popup again. + */ +void showNoticeIfPending(QWidget *parent); + +} // namespace synergy::gui::migration diff --git a/extra/src/lib/synergy/gui/SettingsScope.cpp b/extra/src/lib/synergy/gui/SettingsScope.cpp new file mode 100644 index 000000000..edbaa18ec --- /dev/null +++ b/extra/src/lib/synergy/gui/SettingsScope.cpp @@ -0,0 +1,120 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2026 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "SettingsScope.h" + +#include "common/Constants.h" +#include "common/Settings.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace synergy::gui { + +namespace { + +const auto kPreferSystemKey = QStringLiteral("scope/preferSystem"); + +QString extraFile() +{ + return QStringLiteral("%1/%2.extra.conf").arg(Settings::UserDir, kAppName); +} + +} // namespace + +bool SettingsScope::preferSystem() +{ + QSettings extra(extraFile(), QSettings::IniFormat); + return extra.value(kPreferSystemKey, false).toBool(); +} + +void SettingsScope::setPreferSystem(bool prefer) +{ + QSettings extra(extraFile(), QSettings::IniFormat); + extra.setValue(kPreferSystemKey, prefer); + extra.sync(); +} + +bool SettingsScope::isSystemWritable() +{ + const QFileInfo info(Settings::SystemSettingFile); + if (info.exists()) { + return info.isWritable(); + } + QFileInfo dir(QFileInfo(Settings::SystemSettingFile).absolutePath()); + if (dir.exists()) { + return dir.isWritable(); + } + QDir cursor = dir.dir(); + while (!cursor.exists() && cursor.cdUp()) { + } + return QFileInfo(cursor.absolutePath()).isWritable(); +} + +bool SettingsScope::switchTo(QDialog *parent, bool toSystem) +{ + if (toSystem && !isSystemWritable()) { + QMessageBox::warning( + parent, QObject::tr("Cannot switch to 'All users' scope"), + QObject::tr( + "

%1 can't write to %2.

" + "

The 'All users' scope requires administrator privileges. " + "Run %1 as administrator/root to change settings here.

" + ) + .arg(QApplication::applicationName(), Settings::SystemSettingFile) + ); + return false; + } + + Settings::save(); + + const auto destFile = toSystem ? Settings::SystemSettingFile : Settings::UserSettingFile; + const auto sourceFile = toSystem ? Settings::UserSettingFile : Settings::SystemSettingFile; + + QSettings dest(destFile, QSettings::IniFormat); + if (dest.allKeys().isEmpty()) { + qDebug() << "destination settings empty, copying from:" << sourceFile; + QSettings source(sourceFile, QSettings::IniFormat); + for (const auto &key : source.allKeys()) { + dest.setValue(key, source.value(key)); + } + dest.sync(); + } + + setPreferSystem(toSystem); + + const auto choice = QMessageBox::information( + parent, QObject::tr("Restart required"), + QObject::tr( + "

Settings scope changes take effect when %1 restarts.

" + "

Quit %1 now? You'll need to launch it again from your applications menu.

" + ) + .arg(QApplication::applicationName()), + QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes + ); + if (choice == QMessageBox::Yes) { + QApplication::quit(); + } + return true; +} + +} // namespace synergy::gui diff --git a/extra/src/lib/synergy/gui/SettingsScope.h b/extra/src/lib/synergy/gui/SettingsScope.h new file mode 100644 index 000000000..0882d369d --- /dev/null +++ b/extra/src/lib/synergy/gui/SettingsScope.h @@ -0,0 +1,54 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2026 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +class QDialog; + +namespace synergy::gui { + +class SettingsScope +{ +public: + /** + * @brief Whether the user has chosen the system-scope settings file. + * Persisted in Synergy.extra.conf so it survives restarts. + */ + static bool preferSystem(); + + /** + * @brief Records the user's scope preference. + */ + static void setPreferSystem(bool prefer); + + /** + * @brief Whether the current process can write to the system-scope settings + * directory. False on Linux without root, often false on Windows without admin. + */ + static bool isSystemWritable(); + + /** + * @brief Apply a scope switch: copy settings to the destination scope, + * persist the preference, prompt the user to restart. + * + * @return false if the switch was refused (e.g., system scope not writable + * on this user). Caller should revert any UI state on false. + */ + static bool switchTo(QDialog *parent, bool toSystem); +}; + +} // namespace synergy::gui diff --git a/extra/src/lib/synergy/gui/TestSettings.cpp b/extra/src/lib/synergy/gui/TestSettings.cpp new file mode 100644 index 000000000..813e2b450 --- /dev/null +++ b/extra/src/lib/synergy/gui/TestSettings.cpp @@ -0,0 +1,119 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2026 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "TestSettings.h" + +#include "common/Constants.h" +#include "common/Settings.h" + +#include +#include +#include + +namespace synergy::gui { + +namespace { + +QString envOrFile(const char *envName, const QString &fileValue, bool fileEnabled) +{ + const auto env = qEnvironmentVariable(envName); + if (!env.isEmpty()) { + return env; + } + return fileEnabled ? fileValue : QString(); +} + +} // namespace + +TestSettings &TestSettings::instance() +{ + static TestSettings inst; + return inst; +} + +TestSettings::TestSettings() + : m_fileName(QStringLiteral("%1/%2.test.conf").arg(Settings::UserDir, kAppName)) +{ + load(); +} + +void TestSettings::load() +{ + m_enabled = false; + m_licensing = false; + m_serialKey.clear(); + m_apiUrlActivate.clear(); + m_apiUrlCheck.clear(); + m_startTimeEpochSecs = 0; + m_verbose = false; + m_skipRemoteCheck = false; + m_allowExpiredLicenses = false; + + if (!QFile::exists(m_fileName)) { + qDebug().noquote() << "test settings file not found, test mode off:" << m_fileName; + return; + } + + QSettings ini(m_fileName, QSettings::IniFormat); + m_enabled = ini.value(QStringLiteral("test/enabled"), false).toBool(); + if (!m_enabled) { + qDebug().noquote() << "test settings file present but test/enabled=false:" << m_fileName; + return; + } + m_licensing = ini.value(QStringLiteral("test/licensing"), false).toBool(); + m_serialKey = ini.value(QStringLiteral("test/serialKey")).toString(); + m_apiUrlActivate = ini.value(QStringLiteral("test/apiUrlActivate")).toString(); + m_apiUrlCheck = ini.value(QStringLiteral("test/apiUrlCheck")).toString(); + m_startTimeEpochSecs = ini.value(QStringLiteral("test/startTime"), 0).toLongLong(); + + m_verbose = ini.value(QStringLiteral("features/verbose"), false).toBool(); + m_skipRemoteCheck = ini.value(QStringLiteral("features/skipRemoteCheck"), false).toBool(); + m_allowExpiredLicenses = ini.value(QStringLiteral("features/allowExpiredLicenses"), false).toBool(); + + qInfo().noquote() << "test mode enabled, loaded:" << m_fileName; +} + +void TestSettings::reload() +{ + load(); +} + +QString TestSettings::serialKey() const +{ + return envOrFile("SYNERGY_TEST_SERIAL_KEY", m_serialKey, m_enabled); +} + +QString TestSettings::apiUrlActivate() const +{ + return envOrFile("SYNERGY_TEST_API_URL_ACTIVATE", m_apiUrlActivate, m_enabled); +} + +QString TestSettings::apiUrlCheck() const +{ + return envOrFile("SYNERGY_TEST_API_URL_CHECK", m_apiUrlCheck, m_enabled); +} + +qint64 TestSettings::startTimeEpochSecs() const +{ + const auto env = qEnvironmentVariable("SYNERGY_TEST_START_TIME"); + if (!env.isEmpty()) { + return env.toLongLong(); + } + return m_enabled ? m_startTimeEpochSecs : 0; +} + +} // namespace synergy::gui diff --git a/extra/src/lib/synergy/gui/TestSettings.h b/extra/src/lib/synergy/gui/TestSettings.h new file mode 100644 index 000000000..7d85745e6 --- /dev/null +++ b/extra/src/lib/synergy/gui/TestSettings.h @@ -0,0 +1,101 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2026 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +namespace synergy::gui { + +// QA/test overrides for Synergy. Lives at `${UserDir}/${kAppName}.test.conf`, +// sibling to the main Synergy.conf settings file. Sourcing values from a file +// (rather than env vars) sidesteps the pain of getting env vars into GUI +// launches on macOS / Windows / VS Code F5. +// +// All accessors fall back to the corresponding SYNERGY_TEST_* env var when +// the env var is set, so existing CI/automation flows are unchanged. +class TestSettings +{ +public: + static TestSettings &instance(); + + // True when the test config file exists and `[test]/enabled=true`. The + // value is cached at construction; call reload() to re-read. + bool isEnabled() const + { + return m_enabled; + } + + // True when test mode is on AND `[test]/licensing=true`. Drives the + // license activation flow when set; off by default so test mode can be + // on (Test menu, URL overrides, etc.) without forcing license prompts. + bool isLicensingEnabled() const + { + return m_enabled && m_licensing; + } + + // Test overrides. Each accessor returns the env var when set, otherwise + // the file value when isEnabled(), otherwise empty / 0. + QString serialKey() const; + QString apiUrlActivate() const; + QString apiUrlCheck() const; + qint64 startTimeEpochSecs() const; + + // Feature toggles read from the [features] section. File-only, no env + // var fallback for these (no precedent). + bool verbose() const + { + return m_verbose; + } + bool skipRemoteCheck() const + { + return m_skipRemoteCheck; + } + bool allowExpiredLicenses() const + { + return m_allowExpiredLicenses; + } + + // Path to the test config file (whether or not it exists). + QString fileName() const + { + return m_fileName; + } + + // Re-read the file from disk (e.g., from a Test menu "Reload" action). + void reload(); + +private: + TestSettings(); + TestSettings(const TestSettings &) = delete; + TestSettings &operator=(const TestSettings &) = delete; + + void load(); + + QString m_fileName; + bool m_enabled = false; + bool m_licensing = false; + QString m_serialKey; + QString m_apiUrlActivate; + QString m_apiUrlCheck; + qint64 m_startTimeEpochSecs = 0; + bool m_verbose = false; + bool m_skipRemoteCheck = false; + bool m_allowExpiredLicenses = false; +}; + +} // namespace synergy::gui diff --git a/extra/src/lib/synergy/gui/constants.h b/extra/src/lib/synergy/gui/constants.h new file mode 100644 index 000000000..8f6f7109c --- /dev/null +++ b/extra/src/lib/synergy/gui/constants.h @@ -0,0 +1,42 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +#include + +namespace synergy::gui { + +const auto kUrlApi = "https://symless.com/synergy/api"; +const auto kUrlWebsite = QStringLiteral("https://synergyapp.io"); +const auto kUrlSourceQuery = "source=gui"; + +const auto kLinkBuy = R"(Buy now)"; +const auto kLinkRenew = R"(Renew now)"; +const auto kLinkDownload = R"(Download now)"; + +const auto kUrlPersonalUpgrade = QString("%1/purchase/upgrade?%2").arg(kUrlWebsite, kUrlSourceQuery); +const auto kUrlContact = QString("%1/contact?%2").arg(kUrlWebsite, kUrlSourceQuery); + +const auto kUrlApiLicenseActivate = QString("%1/product/activate").arg(kUrlApi); +const auto kUrlApiLicenseCheck = QString("%1/product/check").arg(kUrlApi); + +constexpr auto kLicenseGracePeriod = std::chrono::days{14}; + +} // namespace synergy::gui diff --git a/extra/src/lib/synergy/gui/dev_mode.h b/extra/src/lib/synergy/gui/dev_mode.h new file mode 100644 index 000000000..9b5953299 --- /dev/null +++ b/extra/src/lib/synergy/gui/dev_mode.h @@ -0,0 +1,37 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2026 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "common/VersionInfo.h" +#include "synergy/build_config.h" + +#include +#include + +namespace synergy::gui { + +inline QString titleWithDevSuffix(const QString &productName) +{ + if constexpr (synergy::kIsDevBuild) { + return QStringLiteral("%1 - Developer build - v%2").arg(productName, kDisplayVersion); + } else { + return productName; + } +} + +} // namespace synergy::gui diff --git a/extra/src/lib/synergy/gui/dialogs/UpgradeDialog.cpp b/extra/src/lib/synergy/gui/dialogs/UpgradeDialog.cpp new file mode 100644 index 000000000..dad7fbd14 --- /dev/null +++ b/extra/src/lib/synergy/gui/dialogs/UpgradeDialog.cpp @@ -0,0 +1,47 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * Copyright (C) 2022 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "UpgradeDialog.h" + +#include "synergy/gui/constants.h" + +#include +#include + +UpgradeDialog::UpgradeDialog(QWidget *parent) : QMessageBox(parent) +{ + m_cancel = addButton("Cancel", QMessageBox::RejectRole); + m_upgrade = addButton("Upgrade", QMessageBox::AcceptRole); +} + +void UpgradeDialog::showDialog(const QString &title, const QString &body, const QString &link) +{ + setWindowTitle(title); + setText(body); + exec(); + + if (clickedButton() == m_upgrade) { + const auto url = QUrl(link); + if (QDesktopServices::openUrl(url)) { + qDebug("opened url: %s", qUtf8Printable(url.toString())); + } else { + qCritical("failed to open url: %s", qUtf8Printable(url.toString())); + } + } else { + qDebug("upgrade was declined"); + } +} diff --git a/extra/src/lib/synergy/gui/dialogs/UpgradeDialog.h b/extra/src/lib/synergy/gui/dialogs/UpgradeDialog.h new file mode 100644 index 000000000..77d6ba7e6 --- /dev/null +++ b/extra/src/lib/synergy/gui/dialogs/UpgradeDialog.h @@ -0,0 +1,32 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * Copyright (C) 2022 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +class UpgradeDialog : public QMessageBox +{ +public: + explicit UpgradeDialog(QWidget *parent = nullptr); + void showDialog(const QString &title, const QString &body, const QString &link); + +private: + QPushButton *m_upgrade = nullptr; + QPushButton *m_cancel = nullptr; +}; diff --git a/extra/src/lib/synergy/gui/license/LicenseApiClient.cpp b/extra/src/lib/synergy/gui/license/LicenseApiClient.cpp new file mode 100644 index 000000000..2f723846a --- /dev/null +++ b/extra/src/lib/synergy/gui/license/LicenseApiClient.cpp @@ -0,0 +1,185 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2022-2026 Synergy Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "LicenseApiClient.h" + +#include "synergy/gui/TestSettings.h" +#include "synergy/gui/constants.h" + +#include +#include +#include +#include +#include + +namespace synergy::gui::license { + +QString activateUrl() +{ + const auto testUrl = TestSettings::instance().apiUrlActivate(); + return testUrl.isEmpty() ? kUrlApiLicenseActivate : testUrl; +} + +QString checkUrl() +{ + const auto testUrl = TestSettings::instance().apiUrlCheck(); + return testUrl.isEmpty() ? kUrlApiLicenseCheck : testUrl; +} + +LicenseApiClient::LicenseApiClient() +{ + connect(&m_manager, &QNetworkAccessManager::finished, this, &LicenseApiClient::handleResponse); +} + +void LicenseApiClient::activate(Data data) +{ + post(RequestKind::kActivate, QUrl(activateUrl()), data); +} + +void LicenseApiClient::check(Data data) +{ + post(RequestKind::kCheck, QUrl(checkUrl()), data); +} + +void LicenseApiClient::post(RequestKind kind, const QUrl &url, const Data &data) +{ + m_isBusy = true; + m_pendingKind = kind; + + qDebug().noquote() << "license api request:" << url.toString(); + + auto request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + m_manager.post(request, getRequestData(data)); +} + +void LicenseApiClient::handleResponse(QNetworkReply *reply) +{ + m_isBusy = false; + const auto kind = m_pendingKind; + + const auto emitFailed = [this, kind](const QString &message) { + if (kind == RequestKind::kActivate) { + Q_EMIT activationFailed(message); + } else { + Q_EMIT checkFailed(message); + } + }; + + const auto emitSucceeded = [this, kind] { + if (kind == RequestKind::kActivate) { + Q_EMIT activationSucceeded(); + } else { + Q_EMIT checkSucceeded(); + } + }; + + if (!reply) { + qWarning("no license api reply"); + emitFailed("License request failed, empty network reply."); + return; + } + + const auto response = reply->readAll(); + + if (reply->error() != QNetworkReply::NoError) { + const auto kLimit = 200; + const auto responseSliced = response.length() > kLimit ? response.sliced(0, kLimit) + "..." : response; + qWarning().noquote() << "license api error:" << reply->error() << reply->errorString() << responseSliced; + emitFailed("License request failed, there was a network error."); + reply->deleteLater(); + return; + } + + qDebug().noquote() << "license api response:" << response; + const auto jsonDoc = QJsonDocument::fromJson(response); + if (response.isNull()) { + qWarning("empty license api response"); + emitFailed("License request failed, the server sent an empty response."); + reply->deleteLater(); + return; + } + + const auto json = jsonDoc.object(); + if (json["status"].toString() != "success") { + const auto status = json["status"].toString(); + const auto message = json["message"].toString(); + + if (!status.isEmpty()) { + qWarning().noquote() << "license api status:" << status; + } else { + qWarning("license api status was empty"); + } + + if (!message.isEmpty()) { + qWarning().noquote() << "license api message:" << message; + } else { + qWarning("license api message was empty"); + } + + if (status == "disabled") { + Q_EMIT licenseDisabled(message.isEmpty() ? QStringLiteral("License has been disabled.") : message); + } else if (!message.isEmpty()) { + emitFailed(message); + } else { + emitFailed("License request failed, unknown error."); + } + + reply->deleteLater(); + return; + } + + qInfo().noquote() << "license api request successful"; + emitSucceeded(); + reply->deleteLater(); +} + +QByteArray LicenseApiClient::getRequestData(const Data &data) const +{ + if (data.machineSignature.isEmpty()) { + qFatal("cannot create license request, no machine id"); + } + + if (data.hostnameSignature.isEmpty()) { + qFatal("cannot create license request, no hostname"); + } + + if (data.serialKey.isEmpty()) { + qFatal("cannot create license request, no serial key"); + } + + if (data.appVersion.isEmpty()) { + qFatal("cannot create license request, no app version"); + } + + if (data.osName.isEmpty()) { + qFatal("cannot create license request, no os name"); + } + + QJsonObject requestData; + requestData["machineSignature"] = data.machineSignature; + requestData["hostnameSignature"] = data.hostnameSignature; + requestData["serialKey"] = data.serialKey; + requestData["appVersion"] = data.appVersion; + requestData["osName"] = data.osName; + requestData["isServer"] = data.isServer; + + return QJsonDocument(requestData).toJson(); +} + +}; // namespace synergy::gui::license diff --git a/extra/src/lib/synergy/gui/license/LicenseApiClient.h b/extra/src/lib/synergy/gui/license/LicenseApiClient.h new file mode 100644 index 000000000..126e63514 --- /dev/null +++ b/extra/src/lib/synergy/gui/license/LicenseApiClient.h @@ -0,0 +1,78 @@ +/* + * synergy -- mouse and keyboard sharing utility + * Copyright (C) 2022-2026 Synergy Ltd. + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include +#include + +class QNetworkReply; + +namespace synergy::gui::license { + +class LicenseApiClient : public QObject +{ + Q_OBJECT + +public: + struct Data + { + QString machineSignature; + QString hostnameSignature; + QString serialKey; + QString appVersion; + QString osName; + bool isServer; + }; + + explicit LicenseApiClient(); + + void activate(Data data); + void check(Data data); + + bool isBusy() + { + return m_isBusy; + } + +Q_SIGNALS: + void activationFailed(const QString &message); + void activationSucceeded(); + void checkFailed(const QString &message); + void checkSucceeded(); + void licenseDisabled(const QString &message); + +private Q_SLOTS: + void handleResponse(QNetworkReply *reply); + +private: + enum class RequestKind + { + kActivate, + kCheck + }; + + void post(RequestKind kind, const QUrl &url, const Data &data); + QByteArray getRequestData(const Data &data) const; + + QNetworkAccessManager m_manager; + bool m_isBusy = false; + RequestKind m_pendingKind = RequestKind::kActivate; +}; + +} // namespace synergy::gui::license diff --git a/extra/src/lib/synergy/gui/license/LicenseHandler.cpp b/extra/src/lib/synergy/gui/license/LicenseHandler.cpp new file mode 100644 index 000000000..4023e1216 --- /dev/null +++ b/extra/src/lib/synergy/gui/license/LicenseHandler.cpp @@ -0,0 +1,534 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2015 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "LicenseHandler.h" + +#include "ActivationDialog.h" +#include "common/Settings.h" +#include "common/VersionInfo.h" +#include "dialogs/UpgradeDialog.h" +#include "gui/core/CoreProcess.h" +#include "synergy/gui/constants.h" +#include "synergy/gui/dev_mode.h" +#include "synergy/gui/license/license_utils.h" +#include "synergy/gui/styles.h" +#include "synergy/license/Product.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::chrono; +using namespace synergy::gui::license; +using namespace synergy::gui; +using namespace deskflow::gui; +using License = synergy::license::License; + +LicenseHandler::LicenseHandler() +{ + m_enabled = synergy::gui::license::isActivationEnabled(); + + connect(&m_apiClient, &LicenseApiClient::activationFailed, this, [this](const QString &message) { + QString fullMessage = QString( + "

There was a problem activating your license.

" + "%3" + R"(

Please contact us )" + "if there is anything we can do to help.

" + ) + .arg(kUrlContact) + .arg(kColorSecondary) + .arg(message); + QMessageBox::warning(m_pMainWindow, "Activation failed", fullMessage); + + qWarning("activation failed, showing serial key dialog"); + return showSerialKeyDialog(); + }); + + connect(&m_apiClient, &LicenseApiClient::activationSucceeded, this, [this] { + qDebug("license activation succeeded, saving settings"); + m_settings.setActivated(true); + m_settings.setGraceStartEpochSecs(0); + m_settings.sync(); + m_warnedAboutGrace = false; + + if (m_pCoreProcess == nullptr) { + qFatal("core process not set"); + } + + qDebug("resuming core process after activation"); + m_pCoreProcess->start(); + }); + + connect(&m_apiClient, &LicenseApiClient::checkSucceeded, this, &LicenseHandler::handleRemoteCheckSucceeded); + connect(&m_apiClient, &LicenseApiClient::checkFailed, this, &LicenseHandler::handleRemoteCheckFailed); + connect(&m_apiClient, &LicenseApiClient::licenseDisabled, this, &LicenseHandler::disableLicenseRemotely); +} + +void LicenseHandler::handleMainWindow(QMainWindow *mainWindow, deskflow::gui::CoreProcess *coreProcess) +{ + // Must still be set as these are used when not enabled. + m_pMainWindow = mainWindow; + m_pCoreProcess = coreProcess; + + if (!m_enabled) { + qDebug("license handler disabled, skipping main window handler"); + return; + } + + qDebug("main window create handled"); + + if (!loadSettings()) { + qFatal("failed to load license settings"); + } +} + +bool LicenseHandler::handleAppStart() +{ + if (m_pMainWindow == nullptr) { + qFatal("main window not set"); + } + + if (!m_enabled) { + qDebug("license handler disabled, skipping start handler"); + return true; + } + + updateWindowTitle(); + + const auto serialKeyAction = new QAction("Change serial key", m_pMainWindow); + QObject::connect(serialKeyAction, &QAction::triggered, [this] { showSerialKeyDialog(); }); + + const auto licenseMenu = new QMenu("License"); + licenseMenu->addAction(serialKeyAction); + m_pMainWindow->menuBar()->addAction(licenseMenu->menuAction()); + + const auto checkResult = check(); + if (!checkResult) { + return false; + } + + qDebug("license is valid, continuing with start"); + updateWindowTitle(); + clampFeatures(); + runRemoteCheck(); + return true; +} + +void LicenseHandler::handleSettings(QDialog *parent) const +{ + Q_UNUSED(parent); + + // Settings UI injection (TLS toggle, scope radios, etc.) lives in extra/. + // To be wired when the synergy widget-injection layer lands; until then, + // license-tier clamping happens in clampFeatures() at app start and on + // license change. +} + +void LicenseHandler::handleVersionCheck(QString &versionUrl) +{ + if (!m_enabled) { + qDebug("license handler disabled, skipping version check handler"); + return; + } + + const auto edition = license().productEdition(); + if (edition == Product::Edition::kBusiness) { + versionUrl.append("/business"); + } else { + versionUrl.append("/personal"); + } +} + +bool LicenseHandler::handleCoreStart() +{ + // HACK: For some reason, the core start trigger gets called twice when clicking the 'start' button. + // If the activator is called twice in quick succession, the core is started twice. + if (m_apiClient.isBusy()) { + qDebug("activator is busy, skipping core start handler"); + return false; + } + + if (!m_enabled) { + qDebug("license handler disabled, skipping core start handler"); + return true; + } + + if (m_pMainWindow == nullptr) { + qFatal("main window not set"); + } + + if (m_pCoreProcess == nullptr) { + qFatal("core process not set"); + } + + if (m_settings.activated()) { + qDebug("license is activated, starting core"); + return true; + } + + if (m_license.serialKey().isOffline) { + qDebug("offline serial key, starting core"); + return true; + } + + if (!m_license.isValid()) { + qWarning("no valid license, skipping core start"); + return false; + } + + qInfo("activating license"); + m_apiClient.activate(buildApiData()); + + return false; +} + +bool LicenseHandler::loadSettings() +{ + using enum SetSerialKeyResult; + + m_settings.load(); + + const auto serialKey = m_settings.serialKey(); + if (!serialKey.isEmpty()) { + const auto result = setLicense(m_settings.serialKey(), true); + if (result != kSuccess && result != kUnchanged) { + qWarning("set serial key failed, showing activation dialog"); + return showSerialKeyDialog(); + } + } + + return true; +} + +void LicenseHandler::saveSettings() +{ + const auto hexString = m_license.serialKey().hexString; + m_settings.setSerialKey(QString::fromStdString(hexString)); + m_settings.sync(); +} + +bool LicenseHandler::showSerialKeyDialog() +{ + if (!m_settings.isWritable()) { + QMessageBox::warning( + m_pMainWindow, "Write access required", + tr("

The settings file is not writable:

" + "

%1

" + "

Please check the file permissions and try again.

") + .arg(m_settings.fileName()) + ); + return false; + } + + ActivationDialog dialog(m_pMainWindow, *this); + const auto result = dialog.exec(); + if (result != QDialog::Accepted) { + qWarning("license serial key dialog declined"); + return false; + } + + if (dialog.serialKeyChanged()) { + // Reset activation so new serial key can be activated. + qDebug("serial key changed, updating settings"); + m_settings.setActivated(false); + m_settings.setGraceStartEpochSecs(0); + m_warnedAboutGrace = false; + m_settings.sync(); + } + + saveSettings(); + updateWindowTitle(); + clampFeatures(); + + if (dialog.serialKeyChanged() && m_pCoreProcess->isStarted()) { + qDebug("restarting core on serial key change"); + m_pCoreProcess->restart(); + } + + // If the user accepted the dialog while not activated (e.g. recovering from a + // remote disable), retry activation so something visible happens regardless of + // whether the serial key changed. + if (!m_settings.activated() && m_license.isValid() && !m_license.serialKey().isOffline && !m_apiClient.isBusy()) { + qInfo("retrying activation after dialog accept"); + m_apiClient.activate(buildApiData()); + } + + qDebug("license serial key dialog accepted"); + return true; +} + +void LicenseHandler::updateWindowTitle() const +{ + const auto productName = QString::fromStdString(m_license.productName()); + qDebug("updating main window title: %s", qPrintable(productName)); + m_pMainWindow->setWindowTitle(synergy::gui::titleWithDevSuffix(productName)); +} + +const synergy::license::License &LicenseHandler::license() const +{ + return m_license; +} + +Product::Edition LicenseHandler::productEdition() const +{ + return m_license.productEdition(); +} + +QString LicenseHandler::productName() const +{ + return QString::fromStdString(m_license.productName()); +} + +/// @param allowExpired If true, allow expired licenses to be set. +/// Useful for passing an expired license to the activation dialog. +LicenseHandler::SetSerialKeyResult LicenseHandler::setLicense(const QString &hexString, bool allowExpired) +{ + using enum LicenseHandler::SetSerialKeyResult; + + if (hexString.isEmpty()) { + qFatal("serial key is empty"); + } + + qDebug() << "changing serial key to:" << hexString; + auto serialKey = parseSerialKey(hexString); + + if (!serialKey.isValid) { + qWarning() << "invalid serial key, ignoring:" << hexString; + return kInvalid; + } + + auto license = License(serialKey); + if (m_time.hasTestTime()) { + license.setNowFunc([this]() { return m_time.now(); }); + } + + if (!allowExpired && license.isExpired()) { + qDebug("license is expired, ignoring"); + return kExpired; + } + + const auto oldSerialKey = m_license.serialKey(); + m_license = license; + + // This delayed check logic seems really complex. Is it really worth the maintenance and testing cost? + // Condition must run *after* the license member is set, since it's async callback uses this member. + if (!m_license.isExpired() && m_license.isTimeLimited()) { + auto secondsLeft = m_license.secondsLeft(); + if (secondsLeft.count() < INT_MAX) { + const auto validateAt = secondsLeft + seconds{1}; + const auto interval = duration_cast(validateAt); + QTimer::singleShot(interval, this, &LicenseHandler::check); + } else { + qDebug("license expiry too distant to schedule timer"); + } + } + + if (serialKey == oldSerialKey) { + qDebug("serial key did not change, ignoring"); + return kUnchanged; + } + + return kSuccess; +} + +bool LicenseHandler::check() +{ + if (!m_license.isValid()) { + qDebug("license validation failed, license invalid"); + return showSerialKeyDialog(); + } else if (m_license.isExpired()) { + qDebug("license validation failed, license expired"); + return showSerialKeyDialog(); + } else if (m_license.isExpiringSoon()) { + if (isInGracePeriod()) { + qDebug("license expiring soon but in remote grace period, suppressing renew nag"); + return true; + } + qDebug("license is expiring soon, showing serial key dialog"); + showSerialKeyDialog(); + // Return true even if dialog cancelled, since expiring soon licenses are still valid. + return true; + } else { + qDebug("license validation succeeded"); + return true; + } +} + +void LicenseHandler::clampFeatures() +{ + if (Settings::value(Settings::Security::TlsEnabled).toBool() && !m_license.isTlsAvailable()) { + qWarning("tls not available, disabling tls"); + Settings::setValue(Settings::Security::TlsEnabled, false); + } + + const auto isSystemScope = (Settings::settingsFile() == Settings::SystemSettingFile); + if (isSystemScope && !m_license.isSettingsScopeAvailable()) { + qFatal("settings scope not available"); + } + + qDebug("committing default feature settings"); + Settings::save(); +} + +void LicenseHandler::disable() +{ + qDebug("disabling license handler"); + m_enabled = false; +} + +bool LicenseHandler::isInGracePeriod() const +{ + return m_settings.graceStartEpochSecs() > 0; +} + +bool LicenseHandler::isGracePeriodExpired() const +{ + if (!isInGracePeriod()) { + return false; + } + const auto now = QDateTime::currentSecsSinceEpoch(); + const auto elapsed = seconds{now - m_settings.graceStartEpochSecs()}; + return elapsed >= duration_cast(kLicenseGracePeriod); +} + +LicenseApiClient::Data LicenseHandler::buildApiData() const +{ + const auto machineId = QSysInfo::machineUniqueId(); + const auto hostname = QHostInfo::localHostName(); + + // Anonymise so a leak doesn't reveal which customer machine the data belongs to. + const auto machineSignature = QCryptographicHash::hash(machineId, QCryptographicHash::Sha256).toHex(); + const auto hostnameSignature = QCryptographicHash::hash(hostname.toUtf8(), QCryptographicHash::Sha256).toHex(); + + const auto isServer = (Settings::value(Settings::Core::CoreMode).toInt() == Settings::Server); + return { + machineSignature, + hostnameSignature, + QString::fromStdString(m_license.serialKey().hexString), + kVersion, + QSysInfo::prettyProductName(), + isServer + }; +} + +void LicenseHandler::runRemoteCheck() +{ + if (!m_settings.activated() || !m_license.isValid() || m_license.serialKey().isOffline) { + qDebug("license not activated or offline, skipping remote check"); + return; + } + + if (m_apiClient.isBusy()) { + qDebug("license activator busy, skipping remote check"); + return; + } + + qInfo("running remote license check"); + m_apiClient.check(buildApiData()); +} + +void LicenseHandler::handleRemoteCheckSucceeded() +{ + qInfo("remote license check succeeded"); + + const bool wasInGrace = isInGracePeriod(); + m_settings.setGraceStartEpochSecs(0); + m_settings.sync(); + m_warnedAboutGrace = false; + + if (wasInGrace && m_pMainWindow != nullptr) { + QMessageBox::information( + m_pMainWindow, "License restored", tr("Your license is valid again. Thanks for your patience.") + ); + } +} + +void LicenseHandler::handleRemoteCheckFailed(const QString &message) +{ + qWarning().noquote() << "remote license check failed:" << message; + + if (!isInGracePeriod()) { + m_settings.setGraceStartEpochSecs(QDateTime::currentSecsSinceEpoch()); + m_settings.sync(); + } + + if (isGracePeriodExpired()) { + disableLicenseRemotely(message); + return; + } + + if (!m_warnedAboutGrace && m_pMainWindow != nullptr) { + m_warnedAboutGrace = true; + const auto graceDays = static_cast(kLicenseGracePeriod.count()); + QMessageBox::warning( + m_pMainWindow, "License check failed", + tr("

We could not verify your license:

" + "

%1

" + "

%2 will keep working for %3 days. " + R"(If the problem persists, please contact us.)" + "

") + .arg(message.toHtmlEscaped()) + .arg(productName()) + .arg(graceDays) + .arg(kUrlContact) + .arg(kColorSecondary) + ); + } +} + +void LicenseHandler::disableLicenseRemotely(const QString &reason) +{ + qWarning().noquote() << "license grace period expired, disabling:" << reason; + + if (m_pCoreProcess != nullptr && m_pCoreProcess->isStarted()) { + qDebug("stopping core process due to disabled license"); + m_pCoreProcess->stop(); + } + + // Keep the serial key + in-memory license so the next activation attempt can succeed + // automatically if the server re-enables the license (e.g. after the customer pays). + m_settings.setActivated(false); + m_settings.setGraceStartEpochSecs(0); + m_settings.sync(); + m_warnedAboutGrace = false; + + if (m_pMainWindow != nullptr) { + QMessageBox::warning( + m_pMainWindow, "License disabled", + tr("

Your license has been disabled and could not be verified within the grace period:

" + "

%1

" + R"(

Please contact us to restore access. )" + "Once your license is reinstated, the app will resume automatically.

") + .arg(reason.toHtmlEscaped()) + .arg(kUrlContact) + .arg(kColorSecondary) + ); + } +} diff --git a/extra/src/lib/synergy/gui/license/LicenseHandler.h b/extra/src/lib/synergy/gui/license/LicenseHandler.h new file mode 100644 index 000000000..fc0a45f4b --- /dev/null +++ b/extra/src/lib/synergy/gui/license/LicenseHandler.h @@ -0,0 +1,100 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2015 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "synergy/gui/AppTime.h" +#include "synergy/gui/ExtraSettings.h" +#include "synergy/gui/license/LicenseApiClient.h" +#include "synergy/license/License.h" +#include "synergy/license/Product.h" + +class QMainWindow; +class QDialog; + +namespace deskflow::gui { +class CoreProcess; +} + +/** + * @brief A convenience wrapper for `License` that provides Qt signals, etc. + */ +class LicenseHandler : public QObject +{ + Q_OBJECT + + using License = synergy::license::License; + using SerialKey = synergy::license::SerialKey; + +public: + enum class SetSerialKeyResult + { + kSuccess, + kFatal, + kUnchanged, + kInvalid, + kExpired + }; + + explicit LicenseHandler(); + + static LicenseHandler &instance() + { + static LicenseHandler instance; + return instance; + } + + void handleMainWindow(QMainWindow *mainWindow, deskflow::gui::CoreProcess *coreProcess); + bool handleAppStart(); + void handleSettings(QDialog *parent) const; + void handleVersionCheck(QString &versionUrl); + bool handleCoreStart(); + bool loadSettings(); + void saveSettings(); + const License &license() const; + Product::Edition productEdition() const; + QString productName() const; + SetSerialKeyResult setLicense(const QString &hexString, bool allowExpired = false); + void clampFeatures(); + void disable(); + + bool isEnabled() const + { + return m_enabled; + } + +private: + void updateWindowTitle() const; + bool showSerialKeyDialog(); + bool check(); + void runRemoteCheck(); + void handleRemoteCheckSucceeded(); + void handleRemoteCheckFailed(const QString &message); + bool isInGracePeriod() const; + bool isGracePeriodExpired() const; + void disableLicenseRemotely(const QString &reason); + synergy::gui::license::LicenseApiClient::Data buildApiData() const; + + bool m_enabled = true; + synergy::gui::AppTime m_time; + License m_license = License::invalid(); + synergy::gui::ExtraSettings m_settings; + synergy::gui::license::LicenseApiClient m_apiClient; + bool m_warnedAboutGrace = false; + QMainWindow *m_pMainWindow = nullptr; + deskflow::gui::CoreProcess *m_pCoreProcess = nullptr; +}; diff --git a/extra/src/lib/synergy/gui/license/license_notices.cpp b/extra/src/lib/synergy/gui/license/license_notices.cpp new file mode 100644 index 000000000..635ee8701 --- /dev/null +++ b/extra/src/lib/synergy/gui/license/license_notices.cpp @@ -0,0 +1,78 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "license_notices.h" + +#include "constants.h" +#include "synergy/license/License.h" + +using License = synergy::license::License; + +namespace synergy::gui { + +QString trialLicenseNotice(const License &license, const QString &linkColor); +QString subscriptionLicenseNotice(const License &license, const QString &linkColor); + +QString licenseNotice(const License &license, const QString &linkColor) +{ + if (license.isTrial()) { + return trialLicenseNotice(license, linkColor); + } else if (license.isSubscription()) { + return subscriptionLicenseNotice(license, linkColor); + } else { + qFatal("license notice only for time limited licenses"); + return ""; // Workaround for no return warning on Windows. + } +} + +QString trialLicenseNotice(const License &license, const QString &linkColor) +{ + const QString buyLink = QString(kLinkBuy).arg(kUrlContact).arg(linkColor); + if (license.isExpired()) { + return QString("

Your trial has ended. %1

").arg(buyLink); + } else { + auto daysLeft = license.daysLeft().count(); + if (daysLeft <= 0) { + return QString("

Your trial ends today. %1

").arg(buyLink); + } else { + return QString("

Your trial ends in %1 %2. %3

") + .arg(daysLeft) + .arg((daysLeft == 1) ? "day" : "days") + .arg(buyLink); + } + } +} + +QString subscriptionLicenseNotice(const License &license, const QString &linkColor) +{ + const QString renewLink = QString(kLinkRenew).arg(kUrlContact).arg(linkColor); + if (license.isExpired()) { + return QString("

Your license has expired. %1

").arg(renewLink); + } else { + auto daysLeft = license.daysLeft().count(); + if (daysLeft <= 0) { + return QString("

Your license expires today. %1

").arg(renewLink); + } else { + return QString("

Your license expires in %1 %2. %3

") + .arg(daysLeft) + .arg((daysLeft == 1) ? "day" : "days") + .arg(renewLink); + } + } +} + +} // namespace synergy::gui diff --git a/extra/src/lib/synergy/gui/license/license_notices.h b/extra/src/lib/synergy/gui/license/license_notices.h new file mode 100644 index 000000000..72c0f98a7 --- /dev/null +++ b/extra/src/lib/synergy/gui/license/license_notices.h @@ -0,0 +1,28 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "synergy/license/License.h" + +#include + +namespace synergy::gui { + +QString licenseNotice(const synergy::license::License &license, const QString &linkColor); + +} // namespace synergy::gui diff --git a/extra/src/lib/synergy/gui/license/license_utils.cpp b/extra/src/lib/synergy/gui/license/license_utils.cpp new file mode 100644 index 000000000..e14767648 --- /dev/null +++ b/extra/src/lib/synergy/gui/license/license_utils.cpp @@ -0,0 +1,67 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "license_utils.h" + +#include "synergy/gui/TestSettings.h" +#include "synergy/license/parse_serial_key.h" + +#include +#include + +namespace synergy::gui::license { + +#ifdef SYNERGY_ENABLE_ACTIVATION +const bool kEnableActivation = true; +#else +const bool kEnableActivation = false; +#endif // SYNERGY_ENABLE_ACTIVATION + +namespace { +// Inlined from upstream's removed gui/string_utils.h. Trivial helper kept +// local to avoid taking a new dependency just for this one call site. +bool strToTrue(const QString &str) +{ + return str.toLower() == "true" || str == "1"; +} +} // namespace + +bool isActivationEnabled() +{ + if (strToTrue(qEnvironmentVariable("SYNERGY_ENABLE_ACTIVATION"))) { + return true; + } + if (synergy::gui::TestSettings::instance().isLicensingEnabled()) { + return true; + } + return kEnableActivation; +} + +synergy::license::SerialKey parseSerialKey(const QString &hexString) +{ + try { + return synergy::license::parseSerialKey(hexString.toStdString()); + } catch (const std::exception &e) { + qWarning("failed to parse serial key: %s", e.what()); + return synergy::license::SerialKey::invalid(); + } catch (...) { + qWarning("failed to parse serial key, unknown error"); + return synergy::license::SerialKey::invalid(); + } +} + +} // namespace synergy::gui::license diff --git a/extra/src/lib/synergy/gui/license/license_utils.h b/extra/src/lib/synergy/gui/license/license_utils.h new file mode 100644 index 000000000..448724a1c --- /dev/null +++ b/extra/src/lib/synergy/gui/license/license_utils.h @@ -0,0 +1,29 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +#include "synergy/license/SerialKey.h" + +namespace synergy::gui::license { + +bool isActivationEnabled(); +synergy::license::SerialKey parseSerialKey(const QString &hexString); + +} // namespace synergy::gui::license diff --git a/extra/src/lib/synergy/gui/styles.h b/extra/src/lib/synergy/gui/styles.h new file mode 100644 index 000000000..44963e5be --- /dev/null +++ b/extra/src/lib/synergy/gui/styles.h @@ -0,0 +1,34 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +// Style constants used by Synergy UI. Previously lived in upstream's +// `gui/styles.h`; that header was removed during the deskflow gui refactor, +// so we keep our own copy here in the overlay. + +const auto kColorWhite = "#ffffff"; +const auto kColorPrimary = "#ff7c00"; +const auto kColorSecondary = "#4285f4"; +const auto kColorNotice = "#3b67d3"; + +const auto kStyleNoticeLabel = // + QString("padding: 3px 5px; border-radius: 3px;" + "background-color: %1; color: %2") + .arg(kColorNotice, kColorWhite); diff --git a/extra/src/lib/synergy/hooks/gui_hook.h b/extra/src/lib/synergy/hooks/gui_hook.h new file mode 100644 index 000000000..876f74b68 --- /dev/null +++ b/extra/src/lib/synergy/hooks/gui_hook.h @@ -0,0 +1,93 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 - 2025 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "common/Settings.h" +#include "synergy/build_config.h" +#include "synergy/gui/FeatureHandler.h" +#include "synergy/gui/SettingsMigration.h" +#include "synergy/gui/SettingsScope.h" +#include "synergy/gui/dev_mode.h" +#include "synergy/gui/license/LicenseHandler.h" + +#include +#include + +namespace deskflow::gui { +class CoreProcess; +} + +namespace synergy::hooks { + +// Runs before any Settings::value() call, so that legacy-format keys can be +// migrated to the new format before upstream's cleanSettings() wipes them. +inline void onPreInit() +{ + synergy::gui::migration::migrateIfNeeded(); + + // setSettingsFile() instantiates Settings; that's expected here. + if (synergy::gui::SettingsScope::preferSystem()) { + if (synergy::gui::SettingsScope::isSystemWritable()) { + Settings::setSettingsFile(Settings::SystemSettingFile); + } else { + qWarning("scope: system-scope no longer writable, falling back to user scope"); + synergy::gui::SettingsScope::setPreferSystem(false); + } + } +} + +inline void onMainWindow(QMainWindow *mainWindow, deskflow::gui::CoreProcess *coreProcess) +{ + LicenseHandler::instance().handleMainWindow(mainWindow, coreProcess); + FeatureHandler::instance().handleMainWindow(mainWindow); + synergy::gui::migration::showNoticeIfPending(mainWindow); +} + +inline void onTitleApplied(QMainWindow *mainWindow) +{ + mainWindow->setWindowTitle(synergy::gui::titleWithDevSuffix(synergy::kDisplayName)); +} + +inline bool onAppStart() +{ + FeatureHandler::instance().handleAppStart(); + return LicenseHandler::instance().handleAppStart(); +} + +inline void onSettings(QDialog *parent) +{ + LicenseHandler::instance().handleSettings(parent); + FeatureHandler::instance().handleSettings(parent); +} + +inline void onVersionCheck(QString &versionUrl) +{ + return LicenseHandler::instance().handleVersionCheck(versionUrl); +} + +inline bool onCoreStart() +{ + return LicenseHandler::instance().handleCoreStart(); +} + +inline void onTestStart() +{ + LicenseHandler::instance().disable(); +} + +} // namespace synergy::hooks diff --git a/extra/src/lib/synergy/license/CMakeLists.txt b/extra/src/lib/synergy/license/CMakeLists.txt new file mode 100644 index 000000000..cc8f71243 --- /dev/null +++ b/extra/src/lib/synergy/license/CMakeLists.txt @@ -0,0 +1,25 @@ +# Synergy -- mouse and keyboard sharing utility +# Copyright (C) 2024 Synergy App Ltd +# +# This package is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# found in the file LICENSE that should have accompanied this file. +# +# This package is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +file(GLOB headers "*.h") +file(GLOB sources "*.cpp") + +if(ADD_HEADERS_TO_SOURCES) + list(APPEND sources ${headers}) +endif() + +add_library(license STATIC ${sources}) + +target_link_libraries(license arch base) diff --git a/extra/src/lib/synergy/license/License.cpp b/extra/src/lib/synergy/license/License.cpp new file mode 100644 index 000000000..31fdac70f --- /dev/null +++ b/extra/src/lib/synergy/license/License.cpp @@ -0,0 +1,123 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2016 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "License.h" + +#include "Product.h" +#include "synergy/license/SerialKey.h" +#include "synergy/license/parse_serial_key.h" + +#include + +using namespace std::chrono; + +namespace synergy::license { + +License::License(const std::string &hexString) : m_serialKey(parseSerialKey(hexString)) +{ +} + +License::License(const SerialKey &serialKey) : m_serialKey(serialKey) +{ + if (!m_serialKey.isValid) { + throw InvalidSerialKey(); + } +} + +bool License::isTrial() const +{ + return m_serialKey.type.isTrial(); +} + +bool License::isSubscription() const +{ + return m_serialKey.type.isSubscription(); +} + +bool License::isTimeLimited() const +{ + return m_serialKey.type.isSubscription() || m_serialKey.type.isTrial(); +} + +bool License::isTlsAvailable() const +{ + return m_serialKey.product.isFeatureAvailable(Product::Feature::kTls); +} + +bool License::isSettingsScopeAvailable() const +{ + return m_serialKey.product.isFeatureAvailable(Product::Feature::kSettingsScope); +} + +Product::Edition License::productEdition() const +{ + return m_serialKey.product.edition(); +} + +bool License::isExpiringSoon() const +{ + if (!isTimeLimited()) { + return false; + } + + if (!m_serialKey.warnTime.has_value()) { + throw NoTimeLimitError(); + } + + return m_nowFunc() >= m_serialKey.warnTime.value(); +} + +bool License::isExpired() const +{ + if (!isTimeLimited()) { + return false; + } + + if (!m_serialKey.expireTime.has_value()) { + throw NoTimeLimitError(); + } + + return m_nowFunc() >= m_serialKey.expireTime.value(); +} + +seconds License::secondsLeft() const +{ + if (!m_serialKey.expireTime.has_value()) { + throw NoTimeLimitError(); + } + + auto expireTime = m_serialKey.expireTime.value(); + + auto timeLeft = expireTime - m_nowFunc(); + return duration_cast(timeLeft); +} + +days License::daysLeft() const +{ + return duration_cast(secondsLeft()); +} + +std::string License::productName() const +{ + auto name = m_serialKey.product.name(); + if (m_serialKey.type.isTrial()) { + name += " (Trial)"; + } + return name; +} + +} // namespace synergy::license diff --git a/extra/src/lib/synergy/license/License.h b/extra/src/lib/synergy/license/License.h new file mode 100644 index 000000000..e1a34352d --- /dev/null +++ b/extra/src/lib/synergy/license/License.h @@ -0,0 +1,118 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2016 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "SerialKey.h" + +#include +#include +#include +#include + +class Server; +class LicenseHandler; +class LicenseTests; + +namespace synergy::license { + +class License +{ + friend class ::Server; + friend class ::LicenseHandler; + friend class ::LicenseTests; + + using days = std::chrono::days; + using system_clock = std::chrono::system_clock; + using time_point = system_clock::time_point; + using NowFunc = std::function; + using LicenseError = std::runtime_error; + +public: + explicit License(const SerialKey &serialKey); + explicit License(const std::string &hexString); + ~License() = default; + + friend bool operator==(License const &lhs, License const &rhs) + { + return lhs.m_serialKey == rhs.m_serialKey; + } + + bool isTlsAvailable() const; + bool isSettingsScopeAvailable() const; + bool isValid() const + { + return m_serialKey.isValid; + } + bool isExpiringSoon() const; + bool isExpired() const; + bool isTrial() const; + bool isSubscription() const; + bool isTimeLimited() const; + std::chrono::days daysLeft() const; + std::chrono::seconds secondsLeft() const; + Product::Edition productEdition() const; + std::string productName() const; + const SerialKey &serialKey() const + { + return m_serialKey; + } + void invalidate() + { + m_serialKey = SerialKey::invalid(); + } + + class InvalidSerialKey : public LicenseError + { + public: + explicit InvalidSerialKey() : LicenseError("invalid serial key") + { + } + }; + + class NoTimeLimitError : public LicenseError + { + public: + explicit NoTimeLimitError() : LicenseError("serial key has no time limit") + { + } + }; + +protected: + void setNowFunc(const NowFunc &nowFunc) + { + m_nowFunc = nowFunc; + } + +private: + // for intentionality, force use of `invalid()` static function. + License() = default; + + // prevent copy, so that changes can be reflected in one instance. + License(const License &) = default; + License &operator=(const License &) = default; + + static License invalid() + { + return License(); + } + + SerialKey m_serialKey = SerialKey::invalid(); + NowFunc m_nowFunc = []() { return system_clock::now(); }; +}; + +} // namespace synergy::license diff --git a/extra/src/lib/synergy/license/Product.cpp b/extra/src/lib/synergy/license/Product.cpp new file mode 100644 index 000000000..efbb9b8f3 --- /dev/null +++ b/extra/src/lib/synergy/license/Product.cpp @@ -0,0 +1,170 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2016 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include + +#include "Product.h" +#include "synergy/build_config.h" + +using SKE = Product::SerialKeyEditionID; + +const char *const kLicensedProductName = synergy::kDisplayName; + +const std::string SKE::Pro = "pro"; +const std::string SKE::Basic = "basic"; +const std::string SKE::Business = "business"; + +using Edition = Product::Edition; + +const std::map> kSerialKeyEditions{ + {SKE::Basic, Edition::kBasic}, + {SKE::Pro, Edition::kPro}, + {SKE::Business, Edition::kBusiness}, +}; + +Product::Product(Edition edition) : m_edition(edition) +{ +} + +Product::Product(const std::string &serialKeyEditionID) +{ + setEdition(serialKeyEditionID); +} + +Edition Product::edition() const +{ + return m_edition; +} + +std::string Product::serialKeyId() const +{ + switch (edition()) { + using enum Edition; + + case kPro: + return SKE::Pro; + + case kBasic: + return SKE::Basic; + + case kBusiness: + return SKE::Business; + + default: + throw InvalidProductEdition(); + } +} + +std::string Product::name() const +{ + + const std::string nameBase = kLicensedProductName; + switch (edition()) { + using enum Edition; + + case kUnregistered: + return nameBase + " (unregistered)"; + + case kBasic: + return nameBase + " Basic"; + + case kPro: + return nameBase + " Pro"; + + case kBusiness: + return nameBase + " Business"; + + default: + throw InvalidProductEdition(); + } +} + +void Product::setEdition(Edition edition) +{ + m_edition = edition; +} + +void Product::setEdition(const std::string &name) +{ + const auto &pType = kSerialKeyEditions.find(name); + + if (pType != kSerialKeyEditions.end()) { + m_edition = pType->second; + } else { + throw InvalidProductEdition(); + } +} + +bool Product::isValid() const +{ + if (m_edition == Edition::kUnregistered) { + return false; + } + return kSerialKeyEditions.contains(serialKeyId()); +} + +bool Product::isTlsAvailable() const +{ + switch (edition()) { + using enum Edition; + + case kPro: + case kBusiness: + return true; + + case kBasic: + case kUnregistered: + return false; + + default: + throw InvalidProductEdition(); + } +} + +bool Product::isSettingsScopeAvailable() const +{ + switch (edition()) { + using enum Edition; + + case kBusiness: + return true; + + case kBasic: + case kPro: + case kUnregistered: + return false; + + default: + throw InvalidProductEdition(); + } +} + +bool Product::isFeatureAvailable(Product::Feature feature) const +{ + switch (feature) { + using enum Product::Feature; + + case kTls: + return isTlsAvailable(); + + case kSettingsScope: + return isSettingsScopeAvailable(); + + default: + throw InvalidFeature(); + } +} diff --git a/extra/src/lib/synergy/license/Product.h b/extra/src/lib/synergy/license/Product.h new file mode 100644 index 000000000..cabf0c045 --- /dev/null +++ b/extra/src/lib/synergy/license/Product.h @@ -0,0 +1,87 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2016 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include +#include + +class Product +{ + friend bool operator==(Product const &, Product const &) = default; + +public: + class InvalidProductEdition : public std::runtime_error + { + public: + explicit InvalidProductEdition() : std::runtime_error("invalid product edition") + { + } + }; + + class InvalidFeature : public std::runtime_error + { + public: + explicit InvalidFeature() : std::runtime_error("invalid feature") + { + } + }; + + enum class Edition + { + kUnregistered = -1, + kBasic = 0, + kPro = 1, + kBusiness = 4, + }; + + enum class Feature + { + kTls = 0, + kSettingsScope = 2, + }; + + /** + * @brief Product edition IDs found in a decoded serial key. + */ + class SerialKeyEditionID + { + public: + static const std::string Basic; + static const std::string Pro; + static const std::string Business; + }; + + Product() = default; + explicit Product(Edition edition); + explicit Product(const std::string &serialKeyEditionID); + + bool isValid() const; + Edition edition() const; + std::string serialKeyId() const; + std::string name() const; + bool isFeatureAvailable(Feature feature) const; + + void setEdition(Edition type); + void setEdition(const std::string &serialKeyId); + +private: + bool isTlsAvailable() const; + bool isSettingsScopeAvailable() const; + + Edition m_edition = Edition::kUnregistered; +}; diff --git a/extra/src/lib/synergy/license/SerialKey.h b/extra/src/lib/synergy/license/SerialKey.h new file mode 100644 index 000000000..b930fd24c --- /dev/null +++ b/extra/src/lib/synergy/license/SerialKey.h @@ -0,0 +1,68 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2022 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "Product.h" +#include "SerialKeyType.h" + +#include +#include +#include +#include + +namespace synergy::license { + +struct SerialKey +{ + using time_point = std::chrono::system_clock::time_point; + + friend bool operator==(const SerialKey &lhs, const SerialKey &rhs) + { + return (lhs.hexString == rhs.hexString) && (lhs.warnTime == rhs.warnTime) && (lhs.expireTime == rhs.expireTime) && + (lhs.product == rhs.product) && (lhs.type == rhs.type); + } + + explicit SerialKey(const std::string &key) : hexString(key) + { + } + + static SerialKey invalid() + { + return SerialKey(Product::Edition::kUnregistered); + } + + const std::string &toString() const + { + return hexString; + } + + bool isValid = false; + std::string hexString = ""; + Product product; + SerialKeyType type; + std::optional warnTime = std::nullopt; + std::optional expireTime = std::nullopt; + bool isOffline = false; + +private: + explicit SerialKey(Product::Edition edition) : product(edition) + { + } +}; + +} // namespace synergy::license diff --git a/extra/src/lib/synergy/license/SerialKeyType.cpp b/extra/src/lib/synergy/license/SerialKeyType.cpp new file mode 100644 index 000000000..e819b5adb --- /dev/null +++ b/extra/src/lib/synergy/license/SerialKeyType.cpp @@ -0,0 +1,37 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2015 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "SerialKeyType.h" + +const std::string SerialKeyType::Trial = "trial"; +const std::string SerialKeyType::Subscription = "subscription"; + +void SerialKeyType::setType(const std::string_view &type) +{ + m_isTrial = (type == SerialKeyType::Trial); + m_isSubscription = (type == SerialKeyType::Subscription); +} + +bool SerialKeyType::isTrial() const +{ + return m_isTrial; +} + +bool SerialKeyType::isSubscription() const +{ + return m_isSubscription; +} diff --git a/extra/src/lib/synergy/license/SerialKeyType.h b/extra/src/lib/synergy/license/SerialKeyType.h new file mode 100644 index 000000000..78b538b24 --- /dev/null +++ b/extra/src/lib/synergy/license/SerialKeyType.h @@ -0,0 +1,44 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2016 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +class SerialKeyType +{ +private: + friend bool operator==(SerialKeyType const &lhs, SerialKeyType const &rhs) = default; + +public: + static const std::string Trial; + static const std::string Subscription; + + explicit SerialKeyType() = default; + explicit SerialKeyType(const std::string_view &type) + { + setType(type); + } + + void setType(const std::string_view &type); + bool isTrial() const; + bool isSubscription() const; + +private: + bool m_isTrial = false; + bool m_isSubscription = false; +}; diff --git a/extra/src/lib/synergy/license/parse_serial_key.cpp b/extra/src/lib/synergy/license/parse_serial_key.cpp new file mode 100644 index 000000000..759f5ffe3 --- /dev/null +++ b/extra/src/lib/synergy/license/parse_serial_key.cpp @@ -0,0 +1,185 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "parse_serial_key.h" + +#include "SerialKey.h" +#include "SerialKeyType.h" + +#include +#include +#include +#include +#include + +using Parts = std::vector; +using system_clock = std::chrono::system_clock; +using time_point = system_clock::time_point; + +namespace { +std::string trim(const std::string &s) +{ + const auto first = s.find_first_not_of(" \t\n\r\f\v"); + if (first == std::string::npos) { + return ""; + } + const auto last = s.find_last_not_of(" \t\n\r\f\v"); + return s.substr(first, last - first + 1); +} +} // namespace + +namespace synergy::license { + +std::string decode(const std::string &hexString); +Parts tokenize(const std::string &plainText); +SerialKey parseV1(const std::string &hexString, const Parts &parts); +SerialKey parseV2(const std::string &hexString, const Parts &parts); +SerialKey parseV3(const std::string &hexString, const Parts &parts); +std::optional parseDate(const std::string &unixTimeString); + +SerialKey parseSerialKey(const std::string &hexString) +{ + const auto &trimmed = trim(hexString); + const auto &plainText = decode(trimmed); + const auto &parts = tokenize(plainText); + const auto &version = parts.at(0); + + if (version == "v1") { + return parseV1(trimmed, parts); + } else if (version == "v2") { + return parseV2(trimmed, parts); + } else if (version == "v3") { + return parseV3(trimmed, parts); + } else { + throw InvalidSerialKeyVersion(version); + } +} + +std::string decode(const std::string &hexString) +{ + if (hexString.length() % 2 != 0) { + throw InvalidHexString(); + } + + std::string plainText; + plainText.reserve(hexString.length() / 2); + + for (size_t i = 0; i < hexString.length(); i += 2) { + std::string byteString = hexString.substr(i, 2); + auto byte = static_cast(std::stoi(byteString, nullptr, 16)); + plainText.push_back(byte); + } + + return plainText; +} + +SerialKey parseV1(const std::string &hexString, const Parts &parts) +{ + if (parts.size() < 8) { + throw InvalidSerialKeyFormat(); + } + + // e.g.: {v1;basic;name;seats;email;company;1398297600;1398384000} + SerialKey serialKey(hexString); + serialKey.product = Product(parts.at(1)); + serialKey.warnTime = parseDate(parts.at(6)); + serialKey.expireTime = parseDate(parts.at(7)); + serialKey.isValid = true; + return serialKey; +} + +SerialKey parseV2(const std::string &hexString, const Parts &parts) +{ + if (parts.size() < 9) { + throw InvalidSerialKeyFormat(); + } + // e.g.: {v2;trial;basic;name;seats;email;company;1398297600;1398384000} + SerialKey serialKey(hexString); + serialKey.type = SerialKeyType(parts.at(1)); + serialKey.product = Product(parts.at(2)); + serialKey.warnTime = parseDate(parts.at(7)); + serialKey.expireTime = parseDate(parts.at(8)); + serialKey.isValid = true; + return serialKey; +} + +SerialKey parseV3(const std::string &hexString, const Parts &parts) +{ + if (parts.size() < 10) { + throw InvalidSerialKeyFormat(); + } + // e.g.: {v3;offline;trial;basic;name;seats;email;company;1398297600;1398384000} + SerialKey serialKey(hexString); + serialKey.isOffline = (parts.at(1) == "offline"); + serialKey.type = SerialKeyType(parts.at(2)); + serialKey.product = Product(parts.at(3)); + serialKey.warnTime = parseDate(parts.at(8)); + serialKey.expireTime = parseDate(parts.at(9)); + serialKey.isValid = true; + return serialKey; +} + +Parts tokenize(const std::string &plainText) +{ + if (plainText.front() != '{' || plainText.back() != '}') { + throw InvalidSerialKeyFormat(); + } + + const auto serialData = plainText.substr(1, plainText.length() - 2); + + Parts parts; + std::stringstream ss(serialData); + std::string item; + + while (std::getline(ss, item, ';')) { + parts.push_back(item); + } + + // it's possible that the last character is a delimiter, so add an empty part + if (!serialData.empty() && serialData.back() == ';') { + parts.emplace_back(""); + } + + return parts; +} + +std::optional parseDate(const std::string &unixTimeString) +{ + auto clean = trim(unixTimeString); + if (clean.empty()) { + return std::nullopt; + } + + try { + auto seconds = std::stoll(clean); + if (seconds <= 0) { + return std::nullopt; + } else { + return time_point{std::chrono::seconds{seconds}}; + } + } catch (std::invalid_argument &) { + throw InvalidSerialKeyDate(unixTimeString, "invalid argument"); + } catch (std::out_of_range &) { + throw InvalidSerialKeyDate(unixTimeString, "out of range"); + } catch (std::exception &ex) { + throw InvalidSerialKeyDate(unixTimeString, ex.what()); + } catch (...) { // NOSONAR + throw InvalidSerialKeyDate(unixTimeString, "unknown error"); + } +} + +} // namespace synergy::license diff --git a/extra/src/lib/synergy/license/parse_serial_key.h b/extra/src/lib/synergy/license/parse_serial_key.h new file mode 100644 index 000000000..971df756f --- /dev/null +++ b/extra/src/lib/synergy/license/parse_serial_key.h @@ -0,0 +1,69 @@ +/* + * Synergy -- mouse and keyboard sharing utility + * Copyright (C) 2024 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "SerialKey.h" + +#include +#include + +namespace synergy::license { + +class SerialKeyParseError : public std::runtime_error +{ +public: + explicit SerialKeyParseError(const std::string &message) : std::runtime_error(message) + { + } +}; + +class InvalidHexString : public SerialKeyParseError +{ +public: + explicit InvalidHexString() : SerialKeyParseError("invalid hex string") + { + } +}; + +class InvalidSerialKeyFormat : public SerialKeyParseError +{ +public: + explicit InvalidSerialKeyFormat() : SerialKeyParseError("invalid serial key format") + { + } +}; + +class InvalidSerialKeyDate : public SerialKeyParseError +{ +public: + explicit InvalidSerialKeyDate(const std::string &date, const std::string &cause) + : SerialKeyParseError("invalid serial key date: " + date + "\n" + cause) + { + } +}; + +class InvalidSerialKeyVersion : public SerialKeyParseError +{ +public: + explicit InvalidSerialKeyVersion(const std::string &version) + : SerialKeyParseError("invalid serial key version: " + version) + { + } +}; + +SerialKey parseSerialKey(const std::string &hexString); + +} // namespace synergy::license diff --git a/extra/src/unittests/CMakeLists.txt b/extra/src/unittests/CMakeLists.txt new file mode 100644 index 000000000..dba1a08e4 --- /dev/null +++ b/extra/src/unittests/CMakeLists.txt @@ -0,0 +1,19 @@ +# Deskflow -- mouse and keyboard sharing utility +# Copyright (C) 2024 Synergy App Ltd +# +# This package is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# found in the file LICENSE that should have accompanied this file. +# +# This package is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +config_test() +set(target ${UNIT_TESTS_BIN}) +add_executable(${target} ${sources}) +target_link_libraries(${target} ${test_libs}) diff --git a/extra/src/unittests/gui/license/LicenseHandlerTests.cpp b/extra/src/unittests/gui/license/LicenseHandlerTests.cpp new file mode 100644 index 000000000..9f4853880 --- /dev/null +++ b/extra/src/unittests/gui/license/LicenseHandlerTests.cpp @@ -0,0 +1,40 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * Copyright (C) 2024 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "gui/license/LicenseHandler.h" + +#include +#include + +using namespace synergy::license; +using namespace std::chrono; + +const auto kPast = system_clock::now() - hours(1); +const auto kFuture = system_clock::now() + hours(1); + +TEST(LicenseHandlerTests, changeSerialKey_validExpiredLicense_returnsTrue) +{ + LicenseHandler licenseHandler; + licenseHandler.setEnabled(true); + auto hexString = // + "7B76313B70726F3B6E69636B20626F6C746F6E3B313B6" + "E69636B4073796D6C6573732E636F6D3B203B303B307D"; + + auto result = licenseHandler.changeSerialKey(hexString); + + ASSERT_EQ(LicenseHandler::ChangeSerialKeyResult::kSuccess, result); +} diff --git a/extra/src/unittests/gui/license/license_notices_tests.cpp b/extra/src/unittests/gui/license/license_notices_tests.cpp new file mode 100644 index 000000000..92d8a480e --- /dev/null +++ b/extra/src/unittests/gui/license/license_notices_tests.cpp @@ -0,0 +1,144 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * Copyright (C) 2024 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "gui/license/license_notices.h" + +#include +#include +#include + +using namespace std::chrono; +using namespace synergy::license; +using namespace synergy::gui; +using ::testing::HasSubstr; + +const auto kPast = system_clock::now() - hours(1); +const auto kFutureOneHour = system_clock::now() + hours(1); +const auto kFutureOneDay = system_clock::now() + days(1) + hours(1); +const auto kFutureOneWeek = system_clock::now() + days(7) + hours(1); + +TEST(license_notices_tests, licenseNotice_trialExpired_correctText) +{ + SerialKey serialKey(""); + serialKey.isValid = true; + serialKey.warnTime = kPast; + serialKey.expireTime = kPast; + serialKey.type.setType("trial"); + License license(serialKey); + + QString notice = licenseNotice(license); + + EXPECT_THAT(notice.toStdString(), HasSubstr("Your trial has expired")); +} + +TEST(license_notices_tests, licenseNotice_trialExpiringInOneHour_correctText) +{ + SerialKey serialKey(""); + serialKey.isValid = true; + serialKey.warnTime = kFutureOneHour; + serialKey.expireTime = kFutureOneHour; + serialKey.type.setType("trial"); + License license(serialKey); + + QString notice = licenseNotice(license); + + EXPECT_THAT(notice.toStdString(), HasSubstr("Your trial expires today")); +} + +TEST(license_notices_tests, licenseNotice_trialExpiringInOneDay_correctText) +{ + SerialKey serialKey(""); + serialKey.isValid = true; + serialKey.warnTime = kFutureOneDay; + serialKey.expireTime = kFutureOneDay; + serialKey.type.setType("trial"); + License license(serialKey); + + QString notice = licenseNotice(license); + + EXPECT_THAT(notice.toStdString(), HasSubstr("Your trial expires in 1 day")); +} + +TEST(license_notices_tests, licenseNotice_trialExpiringInOneWeek_correctText) +{ + SerialKey serialKey(""); + serialKey.isValid = true; + serialKey.warnTime = kFutureOneWeek; + serialKey.expireTime = kFutureOneWeek; + serialKey.type.setType("trial"); + License license(serialKey); + + QString notice = licenseNotice(license); + + EXPECT_THAT(notice.toStdString(), HasSubstr("Your trial expires in 7 days")); +} + +TEST(license_notices_tests, licenseNotice_subscriptionExpired_correctText) +{ + SerialKey serialKey(""); + serialKey.isValid = true; + serialKey.warnTime = kPast; + serialKey.expireTime = kPast; + serialKey.type.setType("subscription"); + License license(serialKey); + + QString notice = licenseNotice(license); + + EXPECT_THAT(notice.toStdString(), HasSubstr("Your license has expired")); +} + +TEST(license_notices_tests, licenseNotice_subscriptionExpiringInOneHour_correctText) +{ + SerialKey serialKey(""); + serialKey.isValid = true; + serialKey.warnTime = kFutureOneHour; + serialKey.expireTime = kFutureOneHour; + serialKey.type.setType("subscription"); + License license(serialKey); + + QString notice = licenseNotice(license); + + EXPECT_THAT(notice.toStdString(), HasSubstr("Your license expires today")); +} + +TEST(license_notices_tests, licenseNotice_subscriptionExpiringInOneDay_correctText) +{ + SerialKey serialKey(""); + serialKey.isValid = true; + serialKey.warnTime = kFutureOneDay; + serialKey.expireTime = kFutureOneDay; + serialKey.type.setType("subscription"); + License license(serialKey); + + QString notice = licenseNotice(license); + + EXPECT_THAT(notice.toStdString(), HasSubstr("Your license expires in 1 day")); +} + +TEST(license_notices_tests, licenseNotice_subscriptionExpiringInOneWeek_correctText) +{ + SerialKey serialKey(""); + serialKey.isValid = true; + serialKey.warnTime = kFutureOneWeek; + serialKey.expireTime = kFutureOneWeek; + serialKey.type.setType("subscription"); + License license(serialKey); + + QString notice = licenseNotice(license); + + EXPECT_THAT(notice.toStdString(), HasSubstr("Your license expires in 7 days")); +} diff --git a/extra/src/unittests/license/LicenseTests.cpp b/extra/src/unittests/license/LicenseTests.cpp new file mode 100644 index 000000000..f5a523aea --- /dev/null +++ b/extra/src/unittests/license/LicenseTests.cpp @@ -0,0 +1,198 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * Copyright (C) 2016 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#define TEST_ENV + +#include "synergy/license/License.h" +#include "synergy/license/Product.h" + +#include +#include + +using enum Product::Edition; +using namespace synergy::license; +using time_point = std::chrono::system_clock::time_point; +using seconds = std::chrono::seconds; + +class LicenseTests : public ::testing::Test +{ +protected: + void setNow(License &license, int unixTime) const + { + license.setNowFunc([unixTime]() { return time_point{seconds{unixTime}}; }); + } +}; + +TEST_F(LicenseTests, isExpiring_validV2TrialBasicSerial_isTrial) +{ + // {v2;trial;basic;Bob;1;email;company name;1;86400} + License license("7B76323B747269616C3B62617369633B426F623B313B656D61696C3B636" + "F6D70616E79206E616D653B313B38363430307D"); + setNow(license, 0); + + EXPECT_TRUE(license.isTrial()); +} + +TEST_F(LicenseTests, isExpiring_validV2TrialBasicSerial_isTimeLimited) +{ + // {v2;trial;basic;Bob;1;email;company name;1;86400} + License license("7B76323B747269616C3B62617369633B426F623B313B656D61696C3B636" + "F6D70616E79206E616D653B313B38363430307D"); + setNow(license, 0); + + EXPECT_TRUE(license.isTimeLimited()); +} + +TEST_F(LicenseTests, isExpiring_validV2TrialBasicSerial_isNotSubscription) +{ + // {v2;trial;basic;Bob;1;email;company name;1;86400} + License license("7B76323B747269616C3B62617369633B426F623B313B656D61696C3B636" + "F6D70616E79206E616D653B313B38363430307D"); + setNow(license, 0); + + EXPECT_FALSE(license.isSubscription()); +} + +TEST_F(LicenseTests, isExpiring_validV2TrialBasicSerial_isExpiring) +{ + // {v2;trial;basic;Bob;1;email;company name;1;86400} + License license("7B76323B747269616C3B62617369633B426F623B313B656D61696C3B636" + "F6D70616E79206E616D653B313B38363430307D"); + setNow(license, 0); + + EXPECT_FALSE(license.isExpiringSoon()); +} + +TEST_F(LicenseTests, isExpiring_validV2TrialBasicSerial_isBasicEdition) +{ + // {v2;trial;basic;Bob;1;email;company name;1;86400} + License license("7B76323B747269616C3B62617369633B426F623B313B656D61696C3B636" + "F6D70616E79206E616D653B313B38363430307D"); + setNow(license, 0); + + EXPECT_EQ(kBasic, license.productEdition()); +} + +TEST_F(LicenseTests, isExpiring_expiringV2TrialBasicSerial_returnTrue) +{ + // {v2;trial;basic;Bob;1;email;company name;86400;0} + License license("7B76323B747269616C3B62617369633B426F623B313B656D61696C3B636" + "F6D70616E79206E616D653B38363430303B307D"); + setNow(license, 86401); + + EXPECT_TRUE(license.isTrial()); + EXPECT_TRUE(license.isExpiringSoon()); +} + +TEST_F(LicenseTests, isExpired_validV2TrialBasicSerial_returnFalse) +{ + // {v2;trial;basic;Bob;1;email;company name;0;86400} + License license("7B76323B747269616C3B62617369633B426F623B313B656D61696C3B636" + "F6D70616E79206E616D653B303B38363430307D"); + setNow(license, 0); + + EXPECT_TRUE(license.isTrial()); + EXPECT_FALSE(license.isExpired()); +} + +TEST_F(LicenseTests, isExpired_expiringV2TrialBasicSerial_returnFalse) +{ + // {v2;trial;basic;Bob;1;email;company name;0;86400} + License license("7B76323B747269616C3B62617369633B426F623B313B656D61696C3B636" + "F6D70616E79206E616D653B303B38363430307D"); + setNow(license, 1); + + EXPECT_TRUE(license.isTrial()); + EXPECT_FALSE(license.isExpired()); +} + +TEST_F(LicenseTests, isExpired_expiredV2TrialBasicSerial_returnTrue) +{ + // {v2;trial;basic;Bob;1;email;company name;0;86400} + License license("7B76323B747269616C3B62617369633B426F623B313B656D61696C3B636" + "F6D70616E79206E616D653B303B38363430307D"); + setNow(license, 86401); + + EXPECT_TRUE(license.isTrial()); + EXPECT_TRUE(license.isExpired()); +} + +TEST_F(LicenseTests, daysLeft_validExactlyOneDayV2TrialBasicSerial_returnOne) +{ + // {v2;trial;basic;Bob;1;email;company name;0;86400} + License license("7B76323B747269616C3B62617369633B426F623B313B656D61696C3B636" + "F6D70616E79206E616D653B303B38363430307D"); + setNow(license, 0); + + EXPECT_EQ(1, license.secondsLeft().count()); +} + +TEST_F(LicenseTests, daysLeft_validWithinOneDayV2TrialBasicSerial_returnOne) +{ + // {v2;trial;basic;Bob;1;email;company name;0;86400} + License license("7B76323B747269616C3B62617369633B426F623B313B656D61696C3B636" + "F6D70616E79206E616D653B303B38363430307D"); + setNow(license, 0); + + EXPECT_EQ(1, license.secondsLeft().count()); +} + +TEST_F(LicenseTests, daysLeft_expiredV2TrialBasicSerial_returnZero) +{ + // {v2;trial;basic;Bob;1;email;company name;0;86400} + License license("7B76323B747269616C3B62617369633B426F623B313B656D61696C3B636" + "F6D70616E79206E616D653B303B38363430307D"); + setNow(license, 86401); + + EXPECT_EQ(0, license.secondsLeft().count()); +} + +// Subscription license tests +TEST_F(LicenseTests, isExpiring_validV2SubscriptionBasicSerial_returnFalse) +{ + // {v2;subscription;basic;Bob;1;email;company name;1;86400} + License license("7B76323B737562736372697074696F6E3B62617369633B426F623B313B6" + "56D61696C3B636F6D70616E79206E616D653B313B38363430307D"); + setNow(license, 0); + + EXPECT_TRUE(license.isSubscription()); + EXPECT_FALSE(license.isExpiringSoon()); + EXPECT_EQ(kBasic, license.productEdition()); +} + +TEST_F(LicenseTests, isExpiring_expiringV2SubscriptionBasicSerial_returnTrue) +{ + // {v2;subscription;basic;Bob;1;email;company name;86400;0} + License license("7B76323B737562736372697074696F6E3B62617369633B426F623B313B6" + "56D61696C3B636F6D70616E79206E616D653B38363430303B307D"); + setNow(license, 86401); + + EXPECT_TRUE(license.isSubscription()); + EXPECT_TRUE(license.isExpiringSoon()); +} + +TEST_F(LicenseTests, isExpired_expiredV2SubscriptionBasicSerial_returnTrue) +{ + // {v2;subscription;basic;Bob;1;email;company name;0;86400} + License license("7B76323B737562736372697074696F6E3B62617369633B426F623B313B6" + "56D61696C3B636F6D70616E79206E616D653B303B38363430307D"); + setNow(license, 86401); + + EXPECT_TRUE(license.isSubscription()); + EXPECT_TRUE(license.isExpired()); +} diff --git a/extra/src/unittests/license/ProductTests.cpp b/extra/src/unittests/license/ProductTests.cpp new file mode 100644 index 000000000..63a8e89c2 --- /dev/null +++ b/extra/src/unittests/license/ProductTests.cpp @@ -0,0 +1,90 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * Copyright (C) 2016 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +// TODO: Move these tests and the code under test downstream to Synergy + +#define TEST_ENV + +#include "synergy/license/Product.h" + +#include + +using enum Product::Edition; + +TEST(ProductTests, equal_operator) +{ + Product product1(kPro); + Product product2(kPro); + + EXPECT_EQ(product1, product2); +} + +TEST(ProductTests, ctor_businessName_isValid) +{ + Product product(Product::SerialKeyEditionID::Business); + + EXPECT_EQ(kBusiness, product.edition()); + EXPECT_TRUE(product.isValid()); +} + +TEST(ProductTests, ctor_basicType_isValid) +{ + Product product(kBasic); + + EXPECT_TRUE(product.isValid()); +} + +TEST(ProductTests, setEdition_invalidType_throws) +{ + Product product; + + EXPECT_THROW(product.setEdition("test"), Product::InvalidType); +} + +TEST(ProductTests, setEdition_pro_isValid) +{ + Product product; + + product.setEdition(kPro); + + EXPECT_EQ(kPro, product.edition()); + EXPECT_EQ(Product::SerialKeyEditionID::Pro, product.serialKeyId()); + EXPECT_EQ("Deskflow Pro", product.name()); + EXPECT_TRUE(product.isValid()); +} + +TEST(ProductTests, setEdition_basic_isValid) +{ + Product product; + + product.setEdition(kBasic); + + EXPECT_EQ(kBasic, product.edition()); + EXPECT_EQ(Product::SerialKeyEditionID::Basic, product.serialKeyId()); + EXPECT_EQ("Deskflow Basic", product.name()); +} + +TEST(ProductTests, setEdition_business_isValid) +{ + Product product; + + product.setEdition(kBusiness); + + EXPECT_EQ(kBusiness, product.edition()); + EXPECT_EQ(Product::SerialKeyEditionID::Business, product.serialKeyId()); + EXPECT_EQ("Deskflow Business", product.name()); +} diff --git a/extra/src/unittests/license/SerialKeyTypeTests.cpp b/extra/src/unittests/license/SerialKeyTypeTests.cpp new file mode 100644 index 000000000..db90b8492 --- /dev/null +++ b/extra/src/unittests/license/SerialKeyTypeTests.cpp @@ -0,0 +1,45 @@ +/* + * Deskflow -- mouse and keyboard sharing utility + * Copyright (C) 2016 Synergy App Ltd + * + * This package is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * found in the file LICENSE that should have accompanied this file. + * + * This package is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#define TEST_ENV + +#include "synergy/license/SerialKeyType.h" + +#include + +TEST(SerialKeyTypeTests, TrialTemporaryKeyType_false) +{ + SerialKeyType KeyType; + EXPECT_FALSE(KeyType.isTrial()); + EXPECT_FALSE(KeyType.isSubscription()); +} + +TEST(SerialKeyTypeTests, TrialTemporaryKeyType_true) +{ + SerialKeyType KeyType; + KeyType.setType("trial"); + EXPECT_TRUE(KeyType.isTrial()); + EXPECT_FALSE(KeyType.isSubscription()); +} + +TEST(SerialKeyTypeTests, TimeLimitedKeyType_true) +{ + SerialKeyType KeyType; + KeyType.setType("subscription"); + EXPECT_FALSE(KeyType.isTrial()); + EXPECT_TRUE(KeyType.isSubscription()); +} diff --git a/src/apps/CMakeLists.txt b/src/apps/CMakeLists.txt index 0fd26ee7d..749f85fba 100644 --- a/src/apps/CMakeLists.txt +++ b/src/apps/CMakeLists.txt @@ -18,11 +18,11 @@ function(generate_app_man TARGET NAME) --name ${NAME} --include ${CMAKE_SOURCE_DIR}/src/apps/res/manpage.txt --no-info - ${target} - -o $/${target}.1 + $ + -o $/$.1 ) install( - FILES $/${target}.1 + FILES $/$.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1 ) endif() @@ -39,4 +39,6 @@ set(WIN32_POST_EXCLUDE_REGEXES ".*system32.*") add_subdirectory(deskflow-core) add_subdirectory(deskflow-daemon) #Only used on windows -add_subdirectory(deskflow-gui) +if(BUILD_GUI) + add_subdirectory(deskflow-gui) +endif() diff --git a/src/apps/deskflow-core/CMakeLists.txt b/src/apps/deskflow-core/CMakeLists.txt index 2b4d990be..cd615ffe2 100644 --- a/src/apps/deskflow-core/CMakeLists.txt +++ b/src/apps/deskflow-core/CMakeLists.txt @@ -4,9 +4,10 @@ # SPDX-License-Identifier: MIT set(target ${CMAKE_PROJECT_NAME}-core) +set(filename deskflow-core) add_executable(${target} - "${target}.cpp" + "${filename}.cpp" CoreArgs.h CoreArgParser.h CoreArgParser.cpp @@ -15,12 +16,12 @@ add_executable(${target} if(WIN32) # Generate rc file set(EXE_DESCRIPTION "${CMAKE_PROJECT_PROPER_NAME} combined server and client application") - set(EXE_ICON "IDI_DESKFLOW ICON DISCARDABLE \"${CMAKE_SOURCE_DIR}/src/apps/res/deskflow.ico\"") + set(EXE_ICON "IDI_DESKFLOW ICON DISCARDABLE \"${CMAKE_SOURCE_DIR}/extra/src/apps/res/synergy.ico\"") configure_file(${CMAKE_SOURCE_DIR}/src/apps/res/windows.rc.in ${target}.rc) target_sources(${target} PRIVATE - ${target}.exe.manifest - ${CMAKE_SOURCE_DIR}/src/apps/res/deskflow.ico + ${filename}.exe.manifest + ${CMAKE_SOURCE_DIR}/extra/src/apps/res/synergy.ico ${CMAKE_CURRENT_BINARY_DIR}/${target}.rc ) endif() diff --git a/src/apps/deskflow-daemon/CMakeLists.txt b/src/apps/deskflow-daemon/CMakeLists.txt index 070c9248b..c5dd7641f 100644 --- a/src/apps/deskflow-daemon/CMakeLists.txt +++ b/src/apps/deskflow-daemon/CMakeLists.txt @@ -6,15 +6,16 @@ # Daemon is only needed on Windows for elevating processes to deal with UAC. if(WIN32) set(target ${CMAKE_PROJECT_NAME}-daemon) + set(filename deskflow-daemon) # Generate rc file set(EXE_DESCRIPTION "${CMAKE_PROJECT_PROPER_NAME} Daemon for handling secure desktops (UAC prompts, login screen, etc)") - set(EXE_ICON "IDI_DESKFLOW ICON DISCARDABLE \"${CMAKE_SOURCE_DIR}/src/apps/res/deskflow.ico\"") + set(EXE_ICON "IDI_DESKFLOW ICON DISCARDABLE \"${CMAKE_SOURCE_DIR}/extra/src/apps/res/synergy.ico\"") configure_file(${CMAKE_SOURCE_DIR}/src/apps/res/windows.rc.in ${target}.rc) add_executable( ${target} WIN32 - ${target}.cpp + ${filename}.cpp DaemonApp.cpp DaemonApp.h ${CMAKE_CURRENT_BINARY_DIR}/${target}.rc) diff --git a/src/apps/deskflow-daemon/deskflow-daemon.cpp b/src/apps/deskflow-daemon/deskflow-daemon.cpp index c88912d42..82dc0d101 100644 --- a/src/apps/deskflow-daemon/deskflow-daemon.cpp +++ b/src/apps/deskflow-daemon/deskflow-daemon.cpp @@ -10,6 +10,7 @@ #include "arch/Arch.h" #include "base/EventQueue.h" #include "base/Log.h" +#include "common/Constants.h" #include "common/ExitCodes.h" #include "common/Settings.h" #include "common/VersionInfo.h" @@ -131,6 +132,7 @@ void handleError(const char *message) #if defined(Q_OS_WIN) // Show a message box for when run from MSI in Win32 subsystem. - MessageBoxA(nullptr, message, "Deskflow daemon error", MB_OK | MB_ICONERROR); + const std::string title = std::string(kAppName) + " daemon error"; + MessageBoxA(nullptr, message, title.c_str(), MB_OK | MB_ICONERROR); #endif } diff --git a/src/apps/deskflow-gui/CMakeLists.txt b/src/apps/deskflow-gui/CMakeLists.txt index c485f84a3..397911272 100644 --- a/src/apps/deskflow-gui/CMakeLists.txt +++ b/src/apps/deskflow-gui/CMakeLists.txt @@ -3,16 +3,17 @@ # SPDX-License-Identifier: MIT if(APPLE) - set(target Deskflow) + set(target ${CMAKE_PROJECT_PROPER_NAME}) else() - set(target deskflow) + set(target ${CMAKE_PROJECT_NAME}) endif() +set(filename deskflow-gui) set(CMAKE_INCLUDE_CURRENT_DIR ON) if(WIN32) set(EXE_DESCRIPTION "${CMAKE_PROJECT_PROPER_NAME} GUI configuration tool") - set(EXE_ICON "IDI_ICON1 ICON DISCARDABLE \"${CMAKE_SOURCE_DIR}/src/apps/res/deskflow.ico\" ") + set(EXE_ICON "IDI_ICON1 ICON DISCARDABLE \"${CMAKE_SOURCE_DIR}/extra/src/apps/res/synergy.ico\" ") configure_file(${CMAKE_SOURCE_DIR}/src/apps/res/windows.rc.in deskflow.rc) set(platform_extra deskflow.rc) elseif(BUILD_OSX_BUNDLE) @@ -31,14 +32,15 @@ elseif(BUILD_OSX_BUNDLE) @ONLY ) - set(platform_extra ../res/Deskflow.icns) + set(platform_extra ${CMAKE_SOURCE_DIR}/extra/deploy/mac/bundle/Contents/Resources/${target}.icns) set_source_files_properties(${platform_extra} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") endif() add_executable(${target} WIN32 MACOSX_BUNDLE ${platform_extra} ../res/deskflow.qrc - deskflow-gui.cpp + ${CMAKE_SOURCE_DIR}/extra/src/apps/res/synergy.qrc + ${filename}.cpp ) target_link_libraries( diff --git a/src/apps/deskflow-gui/deskflow-gui.cpp b/src/apps/deskflow-gui/deskflow-gui.cpp index 7fd5cc09e..80ad3fe42 100644 --- a/src/apps/deskflow-gui/deskflow-gui.cpp +++ b/src/apps/deskflow-gui/deskflow-gui.cpp @@ -17,6 +17,10 @@ #include "gui/Messages.h" #include "gui/StyleUtils.h" +#ifdef SYNERGY_EXTRA_HEADER +#include "synergy/hooks/gui_hook.h" +#endif + #include #include #include @@ -63,6 +67,10 @@ int main(int argc, char *argv[]) QApplication app(argc, argv); +#ifdef SYNERGY_EXTRA_HEADER + synergy::hooks::onPreInit(); +#endif + // Ensure the I18N object is made before strings QTextStream(stdout) << "initial language: " << I18N::currentLanguage() << '\n'; @@ -152,6 +160,12 @@ int main(int argc, char *argv[]) MainWindow mainWindow; mainWindow.open(); +#ifdef SYNERGY_EXTRA_HEADER + if (!synergy::hooks::onAppStart()) { + return s_exitFailed; + } +#endif + return QApplication::exec(); } diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt index d007a8900..f114faf59 100644 --- a/src/lib/CMakeLists.txt +++ b/src/lib/CMakeLists.txt @@ -13,4 +13,6 @@ add_subdirectory(mt) add_subdirectory(net) add_subdirectory(platform) add_subdirectory(server) -add_subdirectory(gui) +if(BUILD_GUI) + add_subdirectory(gui) +endif() diff --git a/src/lib/arch/win32/ArchDaemonWindows.cpp b/src/lib/arch/win32/ArchDaemonWindows.cpp index 9e840caf4..d8285919a 100644 --- a/src/lib/arch/win32/ArchDaemonWindows.cpp +++ b/src/lib/arch/win32/ArchDaemonWindows.cpp @@ -22,7 +22,7 @@ ArchDaemonWindows *ArchDaemonWindows::s_daemon = nullptr; ArchDaemonWindows::ArchDaemonWindows() : m_daemonThreadID(0) { - m_quitMessage = RegisterWindowMessage(L"DeskflowDaemonExit"); + m_quitMessage = RegisterWindowMessage(kDaemonExitMsgNameW); } int ArchDaemonWindows::runDaemon(RunFunc runFunc) diff --git a/src/lib/common/Constants.h.in b/src/lib/common/Constants.h.in index 1aa97f1c0..8e6d3bc83 100644 --- a/src/lib/common/Constants.h.in +++ b/src/lib/common/Constants.h.in @@ -1,7 +1,7 @@ /* * Deskflow -- mouse and keyboard sharing utility * SPDX-FileCopyrightText: (C) 2024 - 2025 Chris Rizzitello - * SPDX-FileCopyrightText: (C) 2025 Symless Ltd. + * SPDX-FileCopyrightText: (C) 2025 - 2026 Symless Ltd. * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception */ @@ -10,18 +10,19 @@ const auto kAppName = "@CMAKE_PROJECT_PROPER_NAME@"; const auto kAppId = "@CMAKE_PROJECT_NAME@"; const auto kAppDescription = "@CMAKE_PROJECT_DESCRIPTION@"; +const auto kAppDomain = "@CMAKE_PROJECT_DOMAIN@"; const auto kDaemonBinName = "@CMAKE_PROJECT_NAME@-daemon"; const auto kDaemonIpcName = "@CMAKE_PROJECT_NAME@-daemon"; const auto kRevFqdnName = "@CMAKE_PROJECT_REV_FQDN@"; const auto kCopyright = // "Copyright @CMAKE_PROJECT_COPYRIGHT@\n" - "Copyright (C) 2012-2025 Symless Ltd.\n" "Copyright (C) 2009-2012 Nick Bolton\n" "Copyright (C) 2002-2009 Chris Schoeneman"; const auto kCoreBinName = "@CORE_BINARY@"; const auto kCoreIpcName = "@CMAKE_PROJECT_NAME@-core"; +const auto kGuiSocketName = "@CMAKE_PROJECT_NAME@-gui"; #ifdef _WIN32 @@ -30,7 +31,11 @@ const auto kWindowsRuntimeMajor = @REQUIRED_MSVC_RUNTIME_MAJOR@; const auto kWindowsRuntimeMinor = @REQUIRED_MSVC_RUNTIME_MINOR@; // clang-format on +constexpr auto kCoreBinNameW = L"@CORE_BINARY@"; constexpr auto kCloseEventName = L"Global\\@CMAKE_PROJECT_PROPER_NAME@Close"; constexpr auto kSendSasEventName = L"Global\\@CMAKE_PROJECT_PROPER_NAME@SendSAS"; +constexpr auto kClipboardOwnershipFormatW = L"@CMAKE_PROJECT_PROPER_NAME@ Ownership"; +constexpr auto kDeskClassNameW = L"@CMAKE_PROJECT_PROPER_NAME@Desk"; +constexpr auto kDaemonExitMsgNameW = L"@CMAKE_PROJECT_PROPER_NAME@DaemonExit"; #endif diff --git a/src/lib/common/Settings.cpp b/src/lib/common/Settings.cpp index d35b4d38a..789ae76a5 100644 --- a/src/lib/common/Settings.cpp +++ b/src/lib/common/Settings.cpp @@ -8,7 +8,7 @@ #include "LogLevel.h" #include "NetworkProtocol.h" -#include "UrlConstants.h" +#include "common/UrlConstants.h" #include #include diff --git a/src/lib/common/UrlConstants.h b/src/lib/common/UrlConstants.h index 332db4201..536dea00d 100644 --- a/src/lib/common/UrlConstants.h +++ b/src/lib/common/UrlConstants.h @@ -1,24 +1,26 @@ /* * Deskflow -- mouse and keyboard sharing utility - * SPDX-FileCopyrightText: (C) 2024 Symless Ltd. + * SPDX-FileCopyrightText: (C) 2024 - 2026 Symless Ltd. * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception */ #pragma once +#include "common/Constants.h" + #include // important: this is used for settings paths on some platforms, // and must not be a url. qt automatically converts this to reverse domain // notation (rdn), e.g. org.deskflow -const auto kOrgDomain = QStringLiteral("deskflow.org"); +const auto kOrgDomain = QString::fromUtf8(kAppDomain); const auto kUrlSourceQuery = QStringLiteral("source=gui"); const auto kUrlApp = QStringLiteral("https://%1").arg(kOrgDomain); const auto kUrlHelp = QStringLiteral("%1/help?%2").arg(kUrlApp, kUrlSourceQuery); const auto kUrlDownload = QStringLiteral("%1/download?%2").arg(kUrlApp, kUrlSourceQuery); -const auto kUrlUpdateCheck = QStringLiteral("https://api.%1/version").arg(kOrgDomain); +const auto kUrlUpdateCheck = QStringLiteral("https://api.symless.com/version").arg(kOrgDomain); #if defined(Q_OS_LINUX) const auto kUrlGnomeTrayFix = QStringLiteral("https://extensions.gnome.org/extension/615/appindicator-support/"); diff --git a/src/lib/gui/MainWindow.cpp b/src/lib/gui/MainWindow.cpp index ab467db76..74f9e9549 100644 --- a/src/lib/gui/MainWindow.cpp +++ b/src/lib/gui/MainWindow.cpp @@ -31,6 +31,10 @@ #include "net/FingerprintDatabase.h" #include "widgets/StatusBar.h" +#ifdef SYNERGY_EXTRA_HEADER +#include "synergy/hooks/gui_hook.h" +#endif + #include #include #include @@ -157,6 +161,10 @@ MainWindow::MainWindow() applyConfig(); m_statusBar->setSecurityIcon(TlsUtility::isEnabled()); restoreWindow(); + +#ifdef SYNERGY_EXTRA_HEADER + synergy::hooks::onMainWindow(this, &m_coreProcess); +#endif } MainWindow::~MainWindow() { @@ -405,6 +413,12 @@ void MainWindow::startCore() m_actionStartCore->setVisible(false); m_actionRestartCore->setVisible(true); + +#ifdef SYNERGY_EXTRA_HEADER + if (!synergy::hooks::onCoreStart()) { + return; + } +#endif m_coreProcess.start(); } @@ -702,6 +716,9 @@ void MainWindow::applyConfig() } else { setWindowTitle(kAppName); } +#ifdef SYNERGY_EXTRA_HEADER + synergy::hooks::onTitleApplied(this); +#endif if (const auto host = Settings::value(Settings::Client::RemoteHost).toString(); !host.isEmpty()) ui->lineHostname->setText(host); @@ -1046,7 +1063,7 @@ void MainWindow::updateText() m_menuHelp->setTitle(tr("&Help")); m_actionClearSettings->setText(tr("Clear settings")); - m_actionReportBug->setText(tr("Report a Bug")); + m_actionReportBug->setText(tr("Get help")); m_actionMinimize->setText(tr("&Minimize to tray")); m_actionQuit->setText(tr("&Quit")); m_actionTrayQuit->setText(tr("&Quit")); diff --git a/src/lib/gui/MainWindow.h b/src/lib/gui/MainWindow.h index 1ec5d97f6..3c34f706e 100644 --- a/src/lib/gui/MainWindow.h +++ b/src/lib/gui/MainWindow.h @@ -15,6 +15,7 @@ #include #include "VersionChecker.h" +#include "common/Constants.h" #include "config/ServerConfig.h" #include "gui/core/CoreProcess.h" #include "gui/core/NetworkMonitor.h" @@ -160,7 +161,7 @@ class MainWindow : public QMainWindow void serverClientsChanged(const QStringList &clients); - inline static const auto m_guiSocketName = QStringLiteral("deskflow-gui"); + inline static const auto m_guiSocketName = QString::fromUtf8(kGuiSocketName); inline static const auto m_nameRegEx = QRegularExpression(QStringLiteral("^[\\w\\-_\\.]{0,255}$")); VersionChecker m_versionChecker; diff --git a/src/lib/gui/VersionChecker.cpp b/src/lib/gui/VersionChecker.cpp index 1dbcf6cdf..f384433a9 100644 --- a/src/lib/gui/VersionChecker.cpp +++ b/src/lib/gui/VersionChecker.cpp @@ -9,6 +9,10 @@ #include "common/Settings.h" #include "common/VersionInfo.h" +#ifdef SYNERGY_EXTRA_HEADER +#include "synergy/hooks/gui_hook.h" +#endif + #include #include #include @@ -24,7 +28,10 @@ VersionChecker::VersionChecker(QObject *parent) : QObject(parent), m_network{new void VersionChecker::checkLatest() const { - const QString url = Settings::value(Settings::Gui::UpdateCheckUrl).toString(); + QString url = Settings::value(Settings::Gui::UpdateCheckUrl).toString(); +#ifdef SYNERGY_EXTRA_HEADER + synergy::hooks::onVersionCheck(url); +#endif qDebug("checking for updates at: %s", qPrintable(url)); auto request = QNetworkRequest(url); auto userAgent = QString("%1 %2 on %3").arg(kAppName, kVersion, QSysInfo::prettyProductName()); diff --git a/src/lib/gui/dialogs/AboutDialog.cpp b/src/lib/gui/dialogs/AboutDialog.cpp index 4b1fda8c9..66c03b11f 100644 --- a/src/lib/gui/dialogs/AboutDialog.cpp +++ b/src/lib/gui/dialogs/AboutDialog.cpp @@ -12,6 +12,7 @@ #include "common/Constants.h" #include "common/Settings.h" #include "common/VersionInfo.h" +#include "gui/StyleUtils.h" #include @@ -19,6 +20,12 @@ AboutDialog::AboutDialog(QWidget *parent) : QDialog(parent), ui{std::make_unique { ui->setupUi(this); + setWindowTitle(windowTitle().arg(kAppName)); + ui->lblIcon->hide(); + ui->lblName->setPixmap(QPixmap(QStringLiteral(":/image/logo-%1.png").arg(deskflow::gui::iconMode()))); + ui->lblName->setContentsMargins(0, 0, 0, 10); + ui->linkContributors->hide(); + const int px = (fontMetrics().height() * 6); const QSize pixmapSize(px, px); ui->lblIcon->setFixedSize(pixmapSize); diff --git a/src/lib/gui/dialogs/AboutDialog.ui b/src/lib/gui/dialogs/AboutDialog.ui index 173823351..1b1becb3a 100644 --- a/src/lib/gui/dialogs/AboutDialog.ui +++ b/src/lib/gui/dialogs/AboutDialog.ui @@ -20,7 +20,7 @@ - About Deskflow + About %1 diff --git a/src/lib/gui/dialogs/ServerConfigDialog.cpp b/src/lib/gui/dialogs/ServerConfigDialog.cpp index 7ac8c3737..35b0adb64 100644 --- a/src/lib/gui/dialogs/ServerConfigDialog.cpp +++ b/src/lib/gui/dialogs/ServerConfigDialog.cpp @@ -33,6 +33,8 @@ ServerConfigDialog::ServerConfigDialog(QWidget *parent, ServerConfig &config) { ui->setupUi(this); + ui->labelProtocol->setWhatsThis(ui->labelProtocol->whatsThis().arg(kAppName)); + m_originalProtocol = Settings::value(Settings::Server::Protocol).value(); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &ServerConfigDialog::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ServerConfigDialog::reject); diff --git a/src/lib/gui/dialogs/ServerConfigDialog.ui b/src/lib/gui/dialogs/ServerConfigDialog.ui index 523b32db6..e6cb3a887 100644 --- a/src/lib/gui/dialogs/ServerConfigDialog.ui +++ b/src/lib/gui/dialogs/ServerConfigDialog.ui @@ -614,7 +614,7 @@ - <html><head/><body><p>Enables compatibility with programs that use either the Synergy or Barrier protocols:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 uses the Synergy protocol.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap and Synergy 1 uses the Barrier protocol.</li></ul><p>A Deskflow client will automatically use either the Synergy or Barrier protocol depending on the server protocol.</p></body></html> + <html><head/><body><p>Enables compatibility with programs that use either the Synergy or Barrier protocols:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 uses the Synergy protocol.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap and Synergy 1 uses the Barrier protocol.</li></ul><p>A %1 client will automatically use either the Synergy or Barrier protocol depending on the server protocol.</p></body></html> Network protocol diff --git a/src/lib/gui/dialogs/SettingsDialog.cpp b/src/lib/gui/dialogs/SettingsDialog.cpp index 66672cbd0..2b3b773ce 100644 --- a/src/lib/gui/dialogs/SettingsDialog.cpp +++ b/src/lib/gui/dialogs/SettingsDialog.cpp @@ -11,12 +11,17 @@ #include "common/PlatformInfo.h" #include "ui_SettingsDialog.h" +#include "common/Constants.h" #include "common/I18N.h" #include "common/Settings.h" #include "gui/Messages.h" #include "gui/TlsUtility.h" #include "gui/core/NetworkMonitor.h" +#ifdef SYNERGY_EXTRA_HEADER +#include "synergy/hooks/gui_hook.h" +#endif + #include #include #include @@ -32,6 +37,8 @@ SettingsDialog::SettingsDialog(QWidget *parent, const ServerConfig &serverConfig ui->setupUi(this); + ui->lblWlClipboard->setText(ui->lblWlClipboard->text().arg(kAppName)); + // these are enabled by the control next to them ui->lineCommandEnter->setEnabled(false); ui->lineCommandExit->setEnabled(false); @@ -78,6 +85,10 @@ SettingsDialog::SettingsDialog(QWidget *parent, const ServerConfig &serverConfig setButtonBoxEnabledButtons(); initConnections(); + +#ifdef SYNERGY_EXTRA_HEADER + synergy::hooks::onSettings(this); +#endif } void SettingsDialog::changeEvent(QEvent *e) diff --git a/src/lib/gui/dialogs/SettingsDialog.ui b/src/lib/gui/dialogs/SettingsDialog.ui index ae33488fc..ee403285b 100644 --- a/src/lib/gui/dialogs/SettingsDialog.ui +++ b/src/lib/gui/dialogs/SettingsDialog.ui @@ -820,7 +820,7 @@ - <html><head/><body><p>Requires the wl-clipboard package</p><p>When using wl-clipboard v2.2.1, there is a focus stealing bug that may make Deskflow harder to use. This has been fixed when using the wl-clipboard master branch, unless your Compositor lacks wlroots-data-control protocol support.</p></body></html> + <html><head/><body><p>Requires the wl-clipboard package</p><p>When using wl-clipboard v2.2.1, there is a focus stealing bug that may make %1 harder to use. This has been fixed when using the wl-clipboard master branch, unless your Compositor lacks wlroots-data-control protocol support.</p></body></html> true diff --git a/src/lib/net/SecureUtils.cpp b/src/lib/net/SecureUtils.cpp index d6e0d24b7..8f0cdfa07 100644 --- a/src/lib/net/SecureUtils.cpp +++ b/src/lib/net/SecureUtils.cpp @@ -8,6 +8,7 @@ #include "SecureUtils.h" #include "base/FinalAction.h" +#include "common/Constants.h" #include #include @@ -84,7 +85,7 @@ void generatePemSelfSignedCert(const QString &path, int keyLength) X509_set_pubkey(cert, privateKey); auto *name = X509_get_subject_name(cert); - X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, reinterpret_cast("Deskflow"), -1, -1, 0); + X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, reinterpret_cast(kAppName), -1, -1, 0); X509_set_issuer_name(cert, name); X509_sign(cert, privateKey, EVP_sha256()); diff --git a/src/lib/platform/MSWindowsClipboard.cpp b/src/lib/platform/MSWindowsClipboard.cpp index eab6155df..3cf2bf89c 100644 --- a/src/lib/platform/MSWindowsClipboard.cpp +++ b/src/lib/platform/MSWindowsClipboard.cpp @@ -9,6 +9,7 @@ #include "platform/MSWindowsClipboard.h" #include "base/Log.h" +#include "common/Constants.h" #include "platform/MSWindowsClipboardBitmapConverter.h" #include "platform/MSWindowsClipboardFacade.h" #include "platform/MSWindowsClipboardHTMLConverter.h" @@ -207,7 +208,7 @@ bool MSWindowsClipboard::isOwnedByDeskflow() { // create ownership format if we haven't yet if (s_ownershipFormat == 0) { - s_ownershipFormat = RegisterClipboardFormat(TEXT("Deskflow Ownership")); + s_ownershipFormat = RegisterClipboardFormatW(kClipboardOwnershipFormatW); } return (IsClipboardFormatAvailable(getOwnershipFormat()) != 0); } @@ -216,7 +217,7 @@ UINT MSWindowsClipboard::getOwnershipFormat() { // create ownership format if we haven't yet if (s_ownershipFormat == 0) { - s_ownershipFormat = RegisterClipboardFormat(TEXT("Deskflow Ownership")); + s_ownershipFormat = RegisterClipboardFormatW(kClipboardOwnershipFormatW); } // return the format diff --git a/src/lib/platform/MSWindowsDesks.cpp b/src/lib/platform/MSWindowsDesks.cpp index 143425a8b..786429f51 100644 --- a/src/lib/platform/MSWindowsDesks.cpp +++ b/src/lib/platform/MSWindowsDesks.cpp @@ -13,6 +13,7 @@ #include "base/IJob.h" #include "base/Log.h" #include "base/TMethodJob.h" +#include "common/Constants.h" #include "deskflow/IScreenSaver.h" #include "deskflow/ScreenException.h" #include "deskflow/win32/AppUtilWindows.h" @@ -353,7 +354,7 @@ ATOM MSWindowsDesks::createDeskWindowClass(bool isPrimary) const classInfo.hCursor = m_cursor; classInfo.hbrBackground = nullptr; classInfo.lpszMenuName = nullptr; - classInfo.lpszClassName = L"DeskflowDesk"; + classInfo.lpszClassName = kDeskClassNameW; classInfo.hIconSm = nullptr; return RegisterClassEx(&classInfo); } @@ -615,7 +616,7 @@ void MSWindowsDesks::deskThread(const void *vdesk) // create a window. we use this window to hide the cursor. try { - desk->m_window = createWindow(m_deskClass, L"DeskflowDesk"); + desk->m_window = createWindow(m_deskClass, kDeskClassNameW); LOG_DEBUG("desk %ls window is 0x%08x", desk->m_name.c_str(), desk->m_window); } catch (...) { // ignore diff --git a/src/lib/platform/MSWindowsWatchdog.cpp b/src/lib/platform/MSWindowsWatchdog.cpp index b7b9ad876..8efdc9372 100644 --- a/src/lib/platform/MSWindowsWatchdog.cpp +++ b/src/lib/platform/MSWindowsWatchdog.cpp @@ -37,9 +37,7 @@ HANDLE openProcessForKill(const PROCESSENTRY32 &entry) if (entry.th32ProcessID == 0) return nullptr; - if (_wcsicmp(entry.szExeFile, L"deskflow-client.exe") != 0 && // - _wcsicmp(entry.szExeFile, L"deskflow-server.exe") != 0 && // - _wcsicmp(entry.szExeFile, L"deskflow-core.exe") != 0) { + if (_wcsicmp(entry.szExeFile, kCoreBinNameW) != 0) { return nullptr; } diff --git a/src/lib/platform/OSXPowerManager.cpp b/src/lib/platform/OSXPowerManager.cpp index c17c99906..3ed168d46 100644 --- a/src/lib/platform/OSXPowerManager.cpp +++ b/src/lib/platform/OSXPowerManager.cpp @@ -7,6 +7,9 @@ #include "OSXPowerManager.h" #include "base/Log.h" +#include "common/Constants.h" + +#include OSXPowerManager::~OSXPowerManager() { @@ -16,10 +19,12 @@ OSXPowerManager::~OSXPowerManager() void OSXPowerManager::disableSleep() { if (!m_sleepPreventionAssertionID) { - CFStringRef reasonForActivity = CFSTR("Deskflow application"); + const std::string reason = std::string(kAppName) + " application"; + CFStringRef reasonForActivity = CFStringCreateWithCString(nullptr, reason.c_str(), kCFStringEncodingUTF8); IOReturn result = IOPMAssertionCreateWithName( kIOPMAssertPreventUserIdleDisplaySleep, kIOPMAssertionLevelOn, reasonForActivity, &m_sleepPreventionAssertionID ); + CFRelease(reasonForActivity); if (result != kIOReturnSuccess) { m_sleepPreventionAssertionID = 0; LOG_ERR("failed to disable system idle sleep"); diff --git a/translations/CMakeLists.txt b/translations/CMakeLists.txt index 6e8a7bc87..c142f66f0 100644 --- a/translations/CMakeLists.txt +++ b/translations/CMakeLists.txt @@ -4,14 +4,16 @@ set_directory_properties(PROPERTIES CLEAN_NO_CUSTOM 1) option(CLEAN_TRS "Clean obsolete translations from tr files" OFF) find_package(Qt6 ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE COMPONENTS LinguistTools) +set(filename "deskflow") + # To add a new language Add 639 shortname -set (${CMAKE_PROJECT_NAME}_TRS - ${CMAKE_PROJECT_NAME}_es.ts - ${CMAKE_PROJECT_NAME}_it.ts - ${CMAKE_PROJECT_NAME}_ja.ts - ${CMAKE_PROJECT_NAME}_zh_CN.ts - ${CMAKE_PROJECT_NAME}_ru.ts - ${CMAKE_PROJECT_NAME}_ko.ts +set (translations + ${filename}_es.ts + ${filename}_it.ts + ${filename}_ja.ts + ${filename}_zh_CN.ts + ${filename}_ru.ts + ${filename}_ko.ts ) set(TR_OPTIONS -no-ui-lines -locations none -silent) @@ -20,10 +22,10 @@ if(CLEAN_TRS) endif() # English will have only plurals -qt_create_translation(TRS ${CMAKE_SOURCE_DIR}/src ${CMAKE_PROJECT_NAME}_en.ts OPTIONS -pluralonly ${TR_OPTIONS}) +qt_create_translation(TRS ${CMAKE_SOURCE_DIR}/src ${filename}_en.ts OPTIONS -pluralonly ${TR_OPTIONS}) # Other languages contain the full set of strings. -qt_create_translation(TRS ${CMAKE_SOURCE_DIR}/src ${${CMAKE_PROJECT_NAME}_TRS} OPTIONS ${TR_OPTIONS}) +qt_create_translation(TRS ${CMAKE_SOURCE_DIR}/src ${translations} OPTIONS ${TR_OPTIONS}) #ensure that the targets are built always add_custom_target(app_translations ALL DEPENDS ${TRS}) @@ -50,8 +52,8 @@ else() ) # install the other Qt lang files renamed to qt_lang.qm - foreach(LANG ${${CMAKE_PROJECT_NAME}_TRS}) - string(REPLACE "${CMAKE_PROJECT_NAME}_" "" LANG ${LANG}) + foreach(LANG ${translations}) + string(REPLACE "${filename}_" "" LANG ${LANG}) string(REPLACE ".ts" "" LANG ${LANG}) add_custom_command( TARGET app_translations POST_BUILD diff --git a/translations/deskflow_es.ts b/translations/deskflow_es.ts index d29fe9e3f..2a3fc508e 100644 --- a/translations/deskflow_es.ts +++ b/translations/deskflow_es.ts @@ -4,8 +4,8 @@ AboutDialog - About Deskflow - Acerca de Deskflow + About %1 + Acerca de %1 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> @@ -373,10 +373,6 @@ Do you want to connect to the server? Clear settings Borrar configuración - - Report a Bug - Informar un error - &Minimize to tray &Minimizar a la bandeja @@ -465,6 +461,10 @@ Do you want to connect to the server? %1 Connection Error %1 Error de conexión + + Get help + Obtener ayuda + No IP Detected No se detectó ninguna IP @@ -1065,8 +1065,8 @@ Además, verifique que puede %1 el archivo de configuración del servidor: %2ms - <html><head/><body><p>Enables compatibility with programs that use either the Synergy or Barrier protocols:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 uses the Synergy protocol.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap and Synergy 1 uses the Barrier protocol.</li></ul><p>A Deskflow client will automatically use either the Synergy or Barrier protocol depending on the server protocol.</p></body></html> - <html><head/><body><p>Habilita la compatibilidad con programas que usan los protocolos Synergy o Barrier:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 usa el protocolo Synergy.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap y Synergy 1 usan el protocolo Barrier.</li></ul><p>Un cliente de Deskflow usará automáticamente el Protocolo Synergy o Barrier según el protocolo del servidor.</p></body></html> + <html><head/><body><p>Enables compatibility with programs that use either the Synergy or Barrier protocols:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 uses the Synergy protocol.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap and Synergy 1 uses the Barrier protocol.</li></ul><p>A %1 client will automatically use either the Synergy or Barrier protocol depending on the server protocol.</p></body></html> + <html><head/><body><p>Habilita la compatibilidad con programas que usan los protocolos Synergy o Barrier:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 usa el protocolo Synergy.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap y Synergy 1 usan el protocolo Barrier.</li></ul><p>Un cliente de %1 usará automáticamente el Protocolo Synergy o Barrier según el protocolo del servidor.</p></body></html> Network protocol @@ -1265,8 +1265,8 @@ Al habilitar esta opción, se deshabilitará la interfaz gráfica de usuario (GU Habilitar la compatibilidad con wl-clipboard - <html><head/><body><p>Requires the wl-clipboard package</p><p>When using wl-clipboard v2.2.1, there is a focus stealing bug that may make Deskflow harder to use. This has been fixed when using the wl-clipboard master branch, unless your Compositor lacks wlroots-data-control protocol support.</p></body></html> - <html><head/><body><p>Requiere el paquete wl-clipboard</p><p>Al usar wl-clipboard v2.2.1, existe un error que provoca la pérdida del foco y que puede dificultar el uso de Deskflow. Este error se ha corregido al usar la rama principal de wl-clipboard, a menos que su Compositor no sea compatible con el protocolo wlroots-data-control.</p></body></html> + <html><head/><body><p>Requires the wl-clipboard package</p><p>When using wl-clipboard v2.2.1, there is a focus stealing bug that may make %1 harder to use. This has been fixed when using the wl-clipboard master branch, unless your Compositor lacks wlroots-data-control protocol support.</p></body></html> + <html><head/><body><p>Requiere el paquete wl-clipboard</p><p>Al usar wl-clipboard v2.2.1, existe un error que provoca la pérdida del foco y que puede dificultar el uso de %1. Este error se ha corregido al usar la rama principal de wl-clipboard, a menos que su Compositor no sea compatible con el protocolo wlroots-data-control.</p></body></html> Automatic diff --git a/translations/deskflow_it.ts b/translations/deskflow_it.ts index 733484120..deaa67ee3 100644 --- a/translations/deskflow_it.ts +++ b/translations/deskflow_it.ts @@ -4,8 +4,8 @@ AboutDialog - About Deskflow - Informazioni su Deskflow + About %1 + Informazioni su %1 Version: @@ -361,10 +361,6 @@ Vuoi connetterti al server? Clear settings Cancella impostazioni - - Report a Bug - Segnala un Bug - &Minimize to tray &Minimizza a icona @@ -453,6 +449,10 @@ Vuoi connetterti al server? %1 Connection Error Errore di connessione %1 + + Get help + Ottieni aiuto + No IP Detected Nessun IP rilevato @@ -1065,8 +1065,8 @@ Inoltre, verifica di poter %1 il file di configurazione del server: %2ms - <html><head/><body><p>Enables compatibility with programs that use either the Synergy or Barrier protocols:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 uses the Synergy protocol.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap and Synergy 1 uses the Barrier protocol.</li></ul><p>A Deskflow client will automatically use either the Synergy or Barrier protocol depending on the server protocol.</p></body></html> - <html><head/><body><p>Abilita la compatibilità con programmi che utilizzano i protocolli Synergy o Barrier:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 usa il protocollo Synergy.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap e Synergy 1 usano il protocollo Barrier.</li></ul><p>Un client Deskflow utilizzerà automaticamente il protocollo Synergy o Barrier a seconda del protocollo del server.</p></body></html> + <html><head/><body><p>Enables compatibility with programs that use either the Synergy or Barrier protocols:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 uses the Synergy protocol.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap and Synergy 1 uses the Barrier protocol.</li></ul><p>A %1 client will automatically use either the Synergy or Barrier protocol depending on the server protocol.</p></body></html> + <html><head/><body><p>Abilita la compatibilità con programmi che utilizzano i protocolli Synergy o Barrier:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 usa il protocollo Synergy.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap e Synergy 1 usano il protocollo Barrier.</li></ul><p>Un client %1 utilizzerà automaticamente il protocollo Synergy o Barrier a seconda del protocollo del server.</p></body></html> Network protocol @@ -1265,8 +1265,8 @@ L'abilitazione di questa impostazione disabiliterà l'interfaccia graf Abilita il supporto wl-clipboard - <html><head/><body><p>Requires the wl-clipboard package</p><p>When using wl-clipboard v2.2.1, there is a focus stealing bug that may make Deskflow harder to use. This has been fixed when using the wl-clipboard master branch, unless your Compositor lacks wlroots-data-control protocol support.</p></body></html> - <html><head/><body><p>Richiede il pacchetto wl-clipboard</p><p>Quando si utilizza wl-clipboard v2.2.1, si verifica un bug di furto del focus che potrebbe rendere Deskflow più difficile da usare. Questo problema è stato risolto quando si utilizza il ramo master di wl-clipboard, a meno che il proprio compositore non supporti il ​​protocollo wlroots-data-control.</p></body></html> + <html><head/><body><p>Requires the wl-clipboard package</p><p>When using wl-clipboard v2.2.1, there is a focus stealing bug that may make %1 harder to use. This has been fixed when using the wl-clipboard master branch, unless your Compositor lacks wlroots-data-control protocol support.</p></body></html> + <html><head/><body><p>Richiede il pacchetto wl-clipboard</p><p>Quando si utilizza wl-clipboard v2.2.1, si verifica un bug di furto del focus che potrebbe rendere %1 più difficile da usare. Questo problema è stato risolto quando si utilizza il ramo master di wl-clipboard, a meno che il proprio compositore non supporti il ​​protocollo wlroots-data-control.</p></body></html> Automatic diff --git a/translations/deskflow_ja.ts b/translations/deskflow_ja.ts index e93b0d591..5fc3c2aab 100644 --- a/translations/deskflow_ja.ts +++ b/translations/deskflow_ja.ts @@ -4,8 +4,8 @@ AboutDialog - About Deskflow - Deskflow について + About %1 + %1 について <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> @@ -421,6 +421,10 @@ Do you want to connect to the server? <p>Failed to connect to the server '%1'.</p><p>A Client with your name is already connected to the server.</p>Please ensure that you're using a unique name and that only a single instance of the client process is running.</p> <p>サーバー '%1' への接続に失敗しました。</p><p>同じ名前のクライアントがサーバーに接続済です。</p><p>名前の重複がないことと、クライアントプロセスが多重起動していない事を確認してください。</p> + + Get help + ヘルプを表示 + No IP Detected IPアドレスが見つかりません @@ -471,10 +475,6 @@ A bound IP is now invalid, you may need to restart the server. Clear settings 設定を消去 - - Report a Bug - バグレポート - &Minimize to tray トレイに最小化(&M) @@ -1067,8 +1067,8 @@ Additionally, check you are able to %1 the server config file: %2 ms - <html><head/><body><p>Enables compatibility with programs that use either the Synergy or Barrier protocols:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 uses the Synergy protocol.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap and Synergy 1 uses the Barrier protocol.</li></ul><p>A Deskflow client will automatically use either the Synergy or Barrier protocol depending on the server protocol.</p></body></html> - <html><head/><body><p>Synergy もしくは Barrier プロトコルを利用するプログラムとの互換性を有効にします。</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 は Synergy プロトコルを利用します。</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier、Input-Leap、Synergy 1 は Barrier プロトコルを利用します。</li></ul><p>Deskflow クライアントはサーバー側のプロトコルに従って自動的に Synergy か Barrier プロトコルを選択します。</p></body></html> + <html><head/><body><p>Enables compatibility with programs that use either the Synergy or Barrier protocols:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 uses the Synergy protocol.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap and Synergy 1 uses the Barrier protocol.</li></ul><p>A %1 client will automatically use either the Synergy or Barrier protocol depending on the server protocol.</p></body></html> + <html><head/><body><p>Synergy もしくは Barrier プロトコルを利用するプログラムとの互換性を有効にします。</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 は Synergy プロトコルを利用します。</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier、Input-Leap、Synergy 1 は Barrier プロトコルを利用します。</li></ul><p>%1 クライアントはサーバー側のプロトコルに従って自動的に Synergy か Barrier プロトコルを選択します。</p></body></html> Network protocol @@ -1267,8 +1267,8 @@ Enabling this setting will disable the server config GUI. wl-clipboard によるクリップボード対応を有効にする - <html><head/><body><p>Requires the wl-clipboard package</p><p>When using wl-clipboard v2.2.1, there is a focus stealing bug that may make Deskflow harder to use. This has been fixed when using the wl-clipboard master branch, unless your Compositor lacks wlroots-data-control protocol support.</p></body></html> - <html><head/><body><p>wl-clipboard パッケージが必要です。</p><p>wl-clipboard v2.2.1 を使用すると、フォーカス盗用のバグにより Deskflow の使い勝手が悪くなる可能性があります。この問題は wl-clipboard のマスターブランチで修正されていますが、使用しているコンポジターが wlroots-data-control プロトコルに対応している必要があります。</p></body></html> + <html><head/><body><p>Requires the wl-clipboard package</p><p>When using wl-clipboard v2.2.1, there is a focus stealing bug that may make %1 harder to use. This has been fixed when using the wl-clipboard master branch, unless your Compositor lacks wlroots-data-control protocol support.</p></body></html> + <html><head/><body><p>wl-clipboard パッケージが必要です。</p><p>wl-clipboard v2.2.1 を使用すると、フォーカス盗用のバグにより %1 の使い勝手が悪くなる可能性があります。この問題は wl-clipboard のマスターブランチで修正されていますが、使用しているコンポジターが wlroots-data-control プロトコルに対応している必要があります。</p></body></html> Automatic diff --git a/translations/deskflow_ko.ts b/translations/deskflow_ko.ts index 4df41ed3c..8850a3999 100644 --- a/translations/deskflow_ko.ts +++ b/translations/deskflow_ko.ts @@ -4,8 +4,8 @@ AboutDialog - About Deskflow - Deskflow 정보 + About %1 + %1 정보 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> @@ -421,6 +421,10 @@ Do you want to connect to the server? <p>Failed to connect to the server '%1'.</p><p>A Client with your name is already connected to the server.</p>Please ensure that you're using a unique name and that only a single instance of the client process is running.</p> <p>서버 '%1'에 연결하지 못했습니다.</p><p>같은 이름의 클라이언트가 이미 서버에 연결되어 있습니다.</p><p>고유한 이름을 사용하고, 클라이언트 프로세스가 하나만 실행 중인지 확인하세요.</p> + + Get help + 도움 받기 + No IP Detected IP를 감지하지 못했습니다 @@ -471,10 +475,6 @@ A bound IP is now invalid, you may need to restart the server. Clear settings 설정 초기화 - - Report a Bug - 버그 신고 - &Minimize to tray 트레이로 최소화(&M) @@ -1065,8 +1065,8 @@ Additionally, check you are able to %1 the server config file: %2 ms - <html><head/><body><p>Enables compatibility with programs that use either the Synergy or Barrier protocols:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 uses the Synergy protocol.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap and Synergy 1 uses the Barrier protocol.</li></ul><p>A Deskflow client will automatically use either the Synergy or Barrier protocol depending on the server protocol.</p></body></html> - <html><head/><body><p>Synergy 또는 Barrier 프로토콜을 사용하는 프로그램과의 호환성을 활성화합니다:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3는 Synergy 프로토콜을 사용합니다.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap, Synergy 1은 Barrier 프로토콜을 사용합니다.</li></ul><p>Deskflow 클라이언트는 서버 프로토콜에 따라 Synergy 또는 Barrier 프로토콜을 자동으로 사용합니다.</p></body></html> + <html><head/><body><p>Enables compatibility with programs that use either the Synergy or Barrier protocols:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 uses the Synergy protocol.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap and Synergy 1 uses the Barrier protocol.</li></ul><p>A %1 client will automatically use either the Synergy or Barrier protocol depending on the server protocol.</p></body></html> + <html><head/><body><p>Synergy 또는 Barrier 프로토콜을 사용하는 프로그램과의 호환성을 활성화합니다:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3는 Synergy 프로토콜을 사용합니다.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap, Synergy 1은 Barrier 프로토콜을 사용합니다.</li></ul><p>%1 클라이언트는 서버 프로토콜에 따라 Synergy 또는 Barrier 프로토콜을 자동으로 사용합니다.</p></body></html> Network protocol @@ -1265,8 +1265,8 @@ Enabling this setting will disable the server config GUI. wl-clipboard 지원 사용 - <html><head/><body><p>Requires the wl-clipboard package</p><p>When using wl-clipboard v2.2.1, there is a focus stealing bug that may make Deskflow harder to use. This has been fixed when using the wl-clipboard master branch, unless your Compositor lacks wlroots-data-control protocol support.</p></body></html> - <html><head/><body><p>wl-clipboard 패키지가 필요합니다.</p><p>wl-clipboard v2.2.1 사용 시 포커스 탈취 버그로 인해 Deskflow 사용이 어려워질 수 있습니다. 이 문제는 wl-clipboard master 브랜치를 사용하면 해결되지만, 사용 중인 컴포지터가 wlroots-data-control 프로토콜을 지원하지 않으면 해결이 안 될 수 있습니다.</p></body></html> + <html><head/><body><p>Requires the wl-clipboard package</p><p>When using wl-clipboard v2.2.1, there is a focus stealing bug that may make %1 harder to use. This has been fixed when using the wl-clipboard master branch, unless your Compositor lacks wlroots-data-control protocol support.</p></body></html> + <html><head/><body><p>wl-clipboard 패키지가 필요합니다.</p><p>wl-clipboard v2.2.1 사용 시 포커스 탈취 버그로 인해 %1 사용이 어려워질 수 있습니다. 이 문제는 wl-clipboard master 브랜치를 사용하면 해결되지만, 사용 중인 컴포지터가 wlroots-data-control 프로토콜을 지원하지 않으면 해결이 안 될 수 있습니다.</p></body></html> Automatic diff --git a/translations/deskflow_ru.ts b/translations/deskflow_ru.ts index fc5d6ab22..c2fd20fb4 100644 --- a/translations/deskflow_ru.ts +++ b/translations/deskflow_ru.ts @@ -4,8 +4,8 @@ AboutDialog - About Deskflow - О Deskflow + About %1 + О %1 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> @@ -421,6 +421,10 @@ Do you want to connect to the server? <p>Failed to connect to the server '%1'.</p><p>A Client with your name is already connected to the server.</p>Please ensure that you're using a unique name and that only a single instance of the client process is running.</p> <p>Не удалось подключиться к серверу '%1'.</p><p>Клиент с таким именем уже подключен к серверу.</p>Убедитесь, что вы используете уникальное имя и запущен только один процесс клиента.</p> + + Get help + Получить помощь + No IP Detected IP-адрес не обнаружен @@ -471,10 +475,6 @@ A bound IP is now invalid, you may need to restart the server. Clear settings Сбросить настройки - - Report a Bug - Сообщить об ошибке - &Minimize to tray &Свернуть в трей @@ -1065,7 +1065,7 @@ Additionally, check you are able to %1 the server config file: %2 мс - <html><head/><body><p>Enables compatibility with programs that use either the Synergy or Barrier protocols:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 uses the Synergy protocol.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap and Synergy 1 uses the Barrier protocol.</li></ul><p>A Deskflow client will automatically use either the Synergy or Barrier protocol depending on the server protocol.</p></body></html> + <html><head/><body><p>Enables compatibility with programs that use either the Synergy or Barrier protocols:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 uses the Synergy protocol.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap and Synergy 1 uses the Barrier protocol.</li></ul><p>A %1 client will automatically use either the Synergy or Barrier protocol depending on the server protocol.</p></body></html> <html><head/><body><p>Включает совместимость с протоколами Synergy или Barrier.</p></body></html> @@ -1263,7 +1263,7 @@ Enabling this setting will disable the server config GUI. Сбросить до значений по умолчанию - <html><head/><body><p>Requires the wl-clipboard package</p><p>When using wl-clipboard v2.2.1, there is a focus stealing bug that may make Deskflow harder to use. This has been fixed when using the wl-clipboard master branch, unless your Compositor lacks wlroots-data-control protocol support.</p></body></html> + <html><head/><body><p>Requires the wl-clipboard package</p><p>When using wl-clipboard v2.2.1, there is a focus stealing bug that may make %1 harder to use. This has been fixed when using the wl-clipboard master branch, unless your Compositor lacks wlroots-data-control protocol support.</p></body></html> <html><head/><body><p>Требуется пакет wl-clipboard. В версии 2.2.1 есть ошибка перехвата фокуса.</p></body></html> diff --git a/translations/deskflow_zh_CN.ts b/translations/deskflow_zh_CN.ts index 667bd7c00..979a23525 100644 --- a/translations/deskflow_zh_CN.ts +++ b/translations/deskflow_zh_CN.ts @@ -4,8 +4,8 @@ AboutDialog - About Deskflow - 关于 Deskflow + About %1 + 关于 %1 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> @@ -421,6 +421,10 @@ Do you want to connect to the server? <p>Failed to connect to the server '%1'.</p><p>A Client with your name is already connected to the server.</p>Please ensure that you're using a unique name and that only a single instance of the client process is running.</p> <p>连接到服务器“%1”失败。</p><p>一个同名的客户端已连接到服务器。</p>请确保您使用的名称唯一,且只有一个客户端进程实例在运行。</p> + + Get help + 获取帮助 + No IP Detected 未检测到 IP @@ -471,10 +475,6 @@ A bound IP is now invalid, you may need to restart the server. Clear settings 清除设置 - - Report a Bug - 报告 Bug - &Minimize to tray 最小化到托盘(&M) @@ -1067,8 +1067,8 @@ Additionally, check you are able to %1 the server config file: %2 毫秒 - <html><head/><body><p>Enables compatibility with programs that use either the Synergy or Barrier protocols:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 uses the Synergy protocol.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap and Synergy 1 uses the Barrier protocol.</li></ul><p>A Deskflow client will automatically use either the Synergy or Barrier protocol depending on the server protocol.</p></body></html> - <html><head/><body><p>启用与使用 Synergy 或 Barrier 协议的程序的兼容性:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 使用 Synergy 协议。</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap 和 Synergy 1 使用 Barrier 协议。</li></ul><p>Deskflow 客户端将根据服务器协议自动使用 Synergy 或 Barrier 协议。</p></body></html> + <html><head/><body><p>Enables compatibility with programs that use either the Synergy or Barrier protocols:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 uses the Synergy protocol.</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap and Synergy 1 uses the Barrier protocol.</li></ul><p>A %1 client will automatically use either the Synergy or Barrier protocol depending on the server protocol.</p></body></html> + <html><head/><body><p>启用与使用 Synergy 或 Barrier 协议的程序的兼容性:</p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Synergy 3 使用 Synergy 协议。</li><li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Barrier, Input-Leap 和 Synergy 1 使用 Barrier 协议。</li></ul><p>%1 客户端将根据服务器协议自动使用 Synergy 或 Barrier 协议。</p></body></html> Network protocol @@ -1267,8 +1267,8 @@ Enabling this setting will disable the server config GUI. 启用 wl-clipboard 支持 - <html><head/><body><p>Requires the wl-clipboard package</p><p>When using wl-clipboard v2.2.1, there is a focus stealing bug that may make Deskflow harder to use. This has been fixed when using the wl-clipboard master branch, unless your Compositor lacks wlroots-data-control protocol support.</p></body></html> - <html><head/><body><p>需要 wl-clipboard 包</p><p>使用 wl-clipboard v2.2.1 时存在一个焦点抢夺 Bug,可能导致 Deskflow 使用不便。该问题已在 wl-clipboard 的 master 分支中修复,除非您的合成器不支持 wlroots-data-control 协议。</p></body></html> + <html><head/><body><p>Requires the wl-clipboard package</p><p>When using wl-clipboard v2.2.1, there is a focus stealing bug that may make %1 harder to use. This has been fixed when using the wl-clipboard master branch, unless your Compositor lacks wlroots-data-control protocol support.</p></body></html> + <html><head/><body><p>需要 wl-clipboard 包</p><p>使用 wl-clipboard v2.2.1 时存在一个焦点抢夺 Bug,可能导致 %1 使用不便。该问题已在 wl-clipboard 的 master 分支中修复,除非您的合成器不支持 wlroots-data-control 协议。</p></body></html> Automatic