From b82f59673c848cf6741a0030b39bc79c7372166b Mon Sep 17 00:00:00 2001 From: tz <185176969+tzhaoo@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:53:17 +0200 Subject: [PATCH 1/8] Migrate Qt and OpenCV to vcpkg across platforms --- .github/workflows/compile-check.yml | 27 +++++++---- .gitignore | 2 + CMakeLists.txt | 3 -- README.md | 73 ++++++++++++++++++----------- ScopeOneCore/CMakeLists.txt | 20 +++----- plugins/CMakeLists.txt | 2 +- scripts/build-unix.sh | 42 ++++++++++++++++- scripts/build.ps1 | 39 ++++++++++----- vcpkg.json | 46 ++++++++++++++++++ 9 files changed, 185 insertions(+), 69 deletions(-) create mode 100644 vcpkg.json diff --git a/.github/workflows/compile-check.yml b/.github/workflows/compile-check.yml index 7e2ee7a..494607f 100644 --- a/.github/workflows/compile-check.yml +++ b/.github/workflows/compile-check.yml @@ -18,7 +18,7 @@ jobs: compile: name: Compile runs-on: ubuntu-24.04 - timeout-minutes: 120 + timeout-minutes: 180 steps: - name: Check out ScopeOne @@ -39,15 +39,22 @@ jobs: autoconf-archive \ pkg-config \ libboost-all-dev \ - libopencv-dev \ - libtiff-dev \ - zlib1g-dev - - - name: Install Qt - uses: jurplel/install-qt-action@v4 - with: - version: '6.11.0' - cache: true + ninja-build curl zip unzip \ + libgl1-mesa-dev libegl1-mesa-dev \ + libx11-dev libx11-xcb-dev libxext-dev libxfixes-dev libxi-dev libxrender-dev \ + libxcb1-dev libxcb-cursor-dev libxcb-util-dev libxcb-glx0-dev libxcb-icccm4-dev \ + libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev \ + libxcb-render-util0-dev libxcb-shape0-dev libxcb-shm0-dev \ + libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev libxcb-xkb-dev \ + libxkbcommon-dev libxkbcommon-x11-dev + + - name: Set up vcpkg + shell: bash + run: | + git clone https://github.com/microsoft/vcpkg.git "$RUNNER_TEMP/scopeone-vcpkg" + git -C "$RUNNER_TEMP/scopeone-vcpkg" checkout "$(jq -r '."builtin-baseline"' vcpkg.json)" + "$RUNNER_TEMP/scopeone-vcpkg/bootstrap-vcpkg.sh" -disableMetrics + echo "VCPKG_ROOT=$RUNNER_TEMP/scopeone-vcpkg" >> "$GITHUB_ENV" - name: Compile mmCoreAndDevices and ScopeOne shell: bash diff --git a/.gitignore b/.gitignore index c66dec1..98996c4 100644 --- a/.gitignore +++ b/.gitignore @@ -33,9 +33,11 @@ !/CMakeLists.txt !/README.md !/VERSION +!/vcpkg.json !/.gitignore /build/ +/vcpkg_installed/ /ref/ **/.DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt index 611f16b..b76ed20 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,9 +28,6 @@ set(ScopeOneCore_ROOT "${CMAKE_SOURCE_DIR}/ScopeOneCore/install" CACHE PATH "ScopeOneCore install prefix" ) -if (WIN32) - list(PREPEND CMAKE_PREFIX_PATH "C:/Qt/6.11.0/msvc2022_64") -endif () list(PREPEND CMAKE_PREFIX_PATH "${ScopeOneCore_ROOT}") file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/include") diff --git a/README.md b/README.md index 0b6a665..81c0db3 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,7 @@ There is an example dual-camera .cfg file in the config folder, just change the - [CMake](https://cmake.org/download/) 4.1.0 - [Visual Studio 2022](https://visualstudio.microsoft.com/vs/) (MSVC v143 toolset) -- [Qt](https://www.qt.io/development/download-qt-installer-oss) 6.9.1 (msvc2022_64) -- OpenCV 4.12.0 +- [vcpkg](https://github.com/microsoft/vcpkg) for Qt 6.11.2 and OpenCV 4.14.0 - mmCoreAndDevices Clone ScopeOne and initialize all submodules with: @@ -79,7 +78,6 @@ ScopeOne/ include/scopeone/ Core API and external plugin contracts external/ mmCoreAndDevices/ - opencv-4.12.0/ ScopeWriter/ ``` @@ -102,22 +100,38 @@ The Image Processing panel can process all live cameras or one selected camera. **Windows Build Steps:** -1. Build and install `ScopeOneCore`: +Set up vcpkg once outside the ScopeOne repository: ```powershell -cmake -S ScopeOneCore -B ScopeOneCore/build -cmake --build ScopeOneCore/build --config Release --parallel -cmake --install ScopeOneCore/build --config Release +git clone https://github.com/microsoft/vcpkg.git C:/dev/vcpkg +C:/dev/vcpkg/bootstrap-vcpkg.bat +$env:VCPKG_ROOT = "C:/dev/vcpkg" ``` -2. Build the GUI application: +For an existing vcpkg checkout, run `git -C $env:VCPKG_ROOT pull --ff-only` and rerun its `bootstrap-vcpkg.bat` before migrating. + +Build Core, plugins, and GUI with the repository script: + +```powershell +.\scripts\build.ps1 --configure +``` + +When migrating an existing build, use `--clean --configure` once to replace the old CMake caches. Later builds only need `.\scripts\build.ps1`. + +The root `vcpkg.json` pins Qt and OpenCV. Core, GUI, and plugins share `vcpkg_installed/`; the build scripts select `x64-windows`, `x64-linux`, `arm64-osx`, or another supported host triplet. CMake installs dependencies on the first configure, and the Windows GUI uses vcpkg's `windeployqt` to deploy Qt plugins. Micro-Manager, CUDA, ONNX Runtime, and ScopeWriter retain their existing dependency setup. + +For a direct Core CMake configure: ```powershell -cmake -S . -B build -cmake --build build --config Release --parallel +cmake -S ScopeOneCore -B ScopeOneCore/build ` + "-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" ` + "-DVCPKG_MANIFEST_DIR=$PWD" ` + "-DVCPKG_INSTALLED_DIR=$PWD/vcpkg_installed" ` + -DVCPKG_TARGET_TRIPLET=x64-windows ` + -DCMAKE_DISABLE_FIND_PACKAGE_Tiff=ON ``` -3. Run: +Run the built application: ```powershell .\build\Release\ScopeOne.exe @@ -125,18 +139,24 @@ cmake --build build --config Release --parallel **Linux and macOS Build Steps (experimental):** -Linux and macOS use the same native build flow. Use the top-level `micro-manager` repository to configure the native build, then compile `MMDevice`, `MMCore`, and the required device adapters. Create a symlink so ScopeOne can find the `mmCoreAndDevices` tree at the path expected by the current CMake files. +Linux and macOS use vcpkg for Qt and OpenCV. Install and bootstrap vcpkg, set `VCPKG_ROOT`, then use the build script. The script selects the triplet from the host operating system and architecture. Micro-Manager is still built from its checkout because its native core and device adapters are not vcpkg dependencies. -Run `./scripts/build-unix.sh` from the ScopeOne repository root on Linux or macOS to automate the complete sequence below. +Run `./scripts/build-unix.sh` from the ScopeOne repository root on Linux or macOS. Add `--clean` once when migrating an existing system-package build. -1. Install common build dependencies. +1. Install common build dependencies and vcpkg. Linux: ```bash sudo apt install \ git subversion build-essential cmake autoconf automake libtool autoconf-archive \ - pkg-config libboost-all-dev qt6-base-dev libopencv-dev libtiff-dev zlib1g-dev + pkg-config ninja-build curl zip unzip libboost-all-dev \ + libgl1-mesa-dev libegl1-mesa-dev libx11-dev libx11-xcb-dev libxext-dev \ + libxfixes-dev libxi-dev libxrender-dev libxcb1-dev libxcb-cursor-dev libxcb-util-dev \ + libxcb-glx0-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev \ + libxcb-randr0-dev libxcb-render-util0-dev libxcb-shape0-dev libxcb-shm0-dev \ + libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev libxcb-xkb-dev \ + libxkbcommon-dev libxkbcommon-x11-dev ``` macOS: @@ -145,9 +165,17 @@ macOS: xcode-select --install brew install \ git subversion cmake autoconf automake libtool autoconf-archive \ - pkg-config boost qt opencv libtiff zlib + pkg-config ninja boost ``` +```bash +git clone https://github.com/microsoft/vcpkg.git "$HOME/vcpkg" +"$HOME/vcpkg/bootstrap-vcpkg.sh" +export VCPKG_ROOT="$HOME/vcpkg" +``` + +For an existing build made with system Qt or OpenCV, pass `--clean` once to remove its CMake caches. Linux still needs system X11 and OpenGL development headers for Qt's desktop platform integration; these are not a system Qt installation. + 2. Clone Micro-Manager and create the `mmCoreAndDevices` symlink: ```bash @@ -184,20 +212,11 @@ make -C mmCoreAndDevices/DeviceAdapters/DemoCamera -j4 make -C mmCoreAndDevices/DeviceAdapters/Utilities -j4 ``` -6. Build and install `ScopeOneCore`: +6. Build and install `ScopeOneCore` and the GUI: ```bash cd /path/to/ScopeOne -cmake -S ScopeOneCore -B ScopeOneCore/build -DCMAKE_BUILD_TYPE=Release -cmake --build ScopeOneCore/build --parallel -cmake --install ScopeOneCore/build -``` - -7. Build the GUI application: - -```bash -cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -cmake --build build --parallel +./scripts/build-unix.sh ``` The Linux or macOS executable is expected at: diff --git a/ScopeOneCore/CMakeLists.txt b/ScopeOneCore/CMakeLists.txt index 956b349..f76b4dd 100644 --- a/ScopeOneCore/CMakeLists.txt +++ b/ScopeOneCore/CMakeLists.txt @@ -11,15 +11,13 @@ include(CMakePackageConfigHelpers) if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${CMAKE_CURRENT_SOURCE_DIR}/install" CACHE PATH "Install path" FORCE) endif () -if (WIN32) - list(PREPEND CMAKE_PREFIX_PATH "C:/Qt/6.11.0/msvc2022_64") -endif () set(MMCORE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/external/mmCoreAndDevices") set(MMCORE_INCLUDE_DIR "${MMCORE_DIR}/MMCore") set(MMDEVICE_INCLUDE_DIR "${MMCORE_DIR}/MMDevice") find_package(Qt6 COMPONENTS Core Gui Network Concurrent REQUIRED) +find_package(OpenCV CONFIG REQUIRED COMPONENTS core imgproc imgcodecs) set(SCOPEWRITER_BUILD_TESTS OFF) add_subdirectory(external/ScopeWriter) @@ -29,15 +27,11 @@ set(CMAKE_AUTOMOC ON) if (WIN32) set(MMCORE_BIN_DIR "${MMCORE_DIR}/build/Release/x64") - set(OpenCV_DIR "${CMAKE_CURRENT_SOURCE_DIR}/external/opencv-4.12.0/build") - - find_package(OpenCV REQUIRED COMPONENTS core imgproc imgcodecs) - set(SCOPEONE_OPENCV_LIBRARIES opencv_world) - file(GLOB MMCORE_RUNTIME_DLLS "${MMCORE_BIN_DIR}/*.dll") + file(GLOB VCPKG_RUNTIME_DLLS "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/bin/*.dll") set(SCOPEONE_RUNTIME_DLLS - $ + ${VCPKG_RUNTIME_DLLS} ${MMCORE_RUNTIME_DLLS} ) @@ -48,11 +42,9 @@ if (WIN32) endif () endforeach () else () - # Linux/Unix: use system OpenCV and link the MMCore static libraries + # Linux/Unix: link the MMCore static libraries # built from the sibling micro-manager checkout (symlinked into # external/mmCoreAndDevices). No runtime DLLs to stage. - find_package(OpenCV REQUIRED COMPONENTS core imgproc imgcodecs) - set(SCOPEONE_OPENCV_LIBRARIES ${OpenCV_LIBS}) set(MMCORE_BIN_DIR "") add_library(MMCore STATIC IMPORTED GLOBAL) @@ -192,7 +184,9 @@ target_link_libraries(ScopeOneCore MMCore Qt::Network Qt::Concurrent - ${SCOPEONE_OPENCV_LIBRARIES} + opencv_core + opencv_imgproc + opencv_imgcodecs ScopeWriter::ScopeWriter ) target_compile_definitions(ScopeOneCore PRIVATE diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 3d8de74..4d4909c 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -6,7 +6,7 @@ set(CMAKE_AUTOMOC ON) find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets) find_package(ScopeOneCore CONFIG REQUIRED) -find_package(OpenCV REQUIRED COMPONENTS core imgcodecs imgproc) +find_package(OpenCV CONFIG REQUIRED COMPONENTS core imgcodecs imgproc) find_program(SCOPEONE_CUDA_NVCC_EXECUTABLE nvcc) function(scopeone_add_plugin target kind) diff --git a/scripts/build-unix.sh b/scripts/build-unix.sh index 1189430..f85b93c 100755 --- a/scripts/build-unix.sh +++ b/scripts/build-unix.sh @@ -7,6 +7,19 @@ EXTERNAL_DIR="$CORE_DIR/external" MM_REPO_DIR="$EXTERNAL_DIR/micro-manager" MMCORE_LINK="$EXTERNAL_DIR/mmCoreAndDevices" BUILD_TYPE="${BUILD_TYPE:-Release}" +CLEAN=false + +for argument in "$@"; do + case "$argument" in + --clean) CLEAN=true ;; + *) echo "Unknown option: $argument" >&2; exit 1 ;; + esac +done + +if [[ -z "${VCPKG_ROOT:-}" || ! -f "$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" ]]; then + echo "Set VCPKG_ROOT to a bootstrapped vcpkg checkout." >&2 + exit 1 +fi case "$(uname -s)" in Linux*) @@ -27,10 +40,35 @@ case "$(uname -s)" in ;; esac +case "$(uname -m)" in + x86_64|amd64) + VCPKG_TRIPLET="$([[ "$PLATFORM_NAME" == "Linux" ]] && echo x64-linux || echo x64-osx)" + ;; + aarch64|arm64) + VCPKG_TRIPLET="$([[ "$PLATFORM_NAME" == "Linux" ]] && echo arm64-linux || echo arm64-osx)" + ;; + *) + echo "Unsupported host architecture: $(uname -m)" >&2 + exit 1 + ;; +esac + +VCPKG_ARGS=( + "-DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" + "-DVCPKG_MANIFEST_DIR=$ROOT_DIR" + "-DVCPKG_INSTALLED_DIR=$ROOT_DIR/vcpkg_installed" + "-DVCPKG_TARGET_TRIPLET=$VCPKG_TRIPLET" + "-DCMAKE_DISABLE_FIND_PACKAGE_Tiff=ON" +) + JOBS="${JOBS:-$DEFAULT_JOBS}" DEMO_ADAPTER="$MMCORE_LINK/DeviceAdapters/DemoCamera/.libs/$DEMO_ADAPTER_NAME" UTILITIES_ADAPTER="$MMCORE_LINK/DeviceAdapters/Utilities/.libs/$UTILITIES_ADAPTER_NAME" +if [[ "$CLEAN" == true ]]; then + rm -rf "$CORE_DIR/build" "$ROOT_DIR/build" +fi + step() { printf '\n==> %s\n' "$1" } @@ -87,12 +125,12 @@ step "Building required Micro-Manager components" ) step "Building and installing ScopeOneCore" -run cmake -S "$CORE_DIR" -B "$CORE_DIR/build" -DCMAKE_BUILD_TYPE="$BUILD_TYPE" +run cmake -S "$CORE_DIR" -B "$CORE_DIR/build" -DCMAKE_BUILD_TYPE="$BUILD_TYPE" "${VCPKG_ARGS[@]}" run cmake --build "$CORE_DIR/build" --parallel "$JOBS" run cmake --install "$CORE_DIR/build" step "Building ScopeOne GUI" -run cmake -S "$ROOT_DIR" -B "$ROOT_DIR/build" -DCMAKE_BUILD_TYPE="$BUILD_TYPE" +run cmake -S "$ROOT_DIR" -B "$ROOT_DIR/build" -DCMAKE_BUILD_TYPE="$BUILD_TYPE" "${VCPKG_ARGS[@]}" run cmake --build "$ROOT_DIR/build" --parallel "$JOBS" step "Copying demo adapters" diff --git a/scripts/build.ps1 b/scripts/build.ps1 index 18dabe9..c51ebe4 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -217,6 +217,7 @@ function Import-MsvcEnvironment { $env:CXX = "cl.exe" } +$scopeOneVcpkgRoot = $env:VCPKG_ROOT Import-MsvcEnvironment $cmake = (Get-Command cmake -ErrorAction Stop).Source @@ -244,6 +245,28 @@ $coreCachePath = Join-Path $coreBuildDir "CMakeCache.txt" $guiCachePath = Join-Path $guiBuildDir "CMakeCache.txt" $pluginCachePath = Join-Path $pluginBuildDir "CMakeCache.txt" +$vcpkgConfigureArgs = @() +if ($env:OS -eq "Windows_NT" -and $target -ne "scopewriter") { + if (-not $scopeOneVcpkgRoot) { + throw "Set VCPKG_ROOT to a bootstrapped vcpkg checkout." + } + $vcpkgToolchain = Join-Path $scopeOneVcpkgRoot "scripts\buildsystems\vcpkg.cmake" + if (-not (Test-Path $vcpkgToolchain)) { + throw "Set VCPKG_ROOT to a bootstrapped vcpkg checkout." + } + $vcpkgConfigureArgs = @( + "-DCMAKE_TOOLCHAIN_FILE=$vcpkgToolchain", + "-DVCPKG_MANIFEST_DIR=$repoRoot", + "-DVCPKG_INSTALLED_DIR=$(Join-Path $repoRoot 'vcpkg_installed')", + "-DVCPKG_TARGET_TRIPLET=x64-windows", + "-DCMAKE_DISABLE_FIND_PACKAGE_Tiff=ON", + "-UTiff_DIR", + "-UOpenCV_DIR", + "-UQt6*_DIR", + "-UWINDEPLOYQT_EXECUTABLE" + ) +} + if ($clean) { if ($target -eq "scopewriter") { if (Test-Path $writerBuildRoot) { @@ -397,7 +420,7 @@ if ($target -in @("all", "core")) { $coreConfigureArgs += "-DCMAKE_INSTALL_PREFIX=$coreInstallDir" } - $coreConfigureArgs += $coreConfigureOption + $coreConfigureArgs += $vcpkgConfigureArgs + $coreConfigureOption Invoke-Step ` -Label "Configuring ScopeOneCore" ` @@ -444,17 +467,7 @@ if ($target -in @("all", "plugins")) { "-DCMAKE_PREFIX_PATH=$coreInstallDir", "-DCMAKE_INSTALL_PREFIX=$pluginInstallDir" ) - $coreQt6Dir = Get-CMakeCacheValue -CachePath $coreCachePath -Key "Qt6_DIR" - if ($coreQt6Dir) { - $pluginConfigureArgs += "-DQt6_DIR=$coreQt6Dir" - } - $coreOpenCvDir = Get-CMakeCacheValue -CachePath $coreCachePath -Key "OpenCV_DIR" - if (-not $coreOpenCvDir) { - $coreOpenCvDir = Join-Path $coreSourceDir "external\opencv-4.12.0\build" - } - if ($coreOpenCvDir) { - $pluginConfigureArgs += "-DOpenCV_DIR=$coreOpenCvDir" - } + $pluginConfigureArgs += $vcpkgConfigureArgs if ($env:CUDA_PATH) { $pluginConfigureArgs += @("-T", "cuda=$env:CUDA_PATH") } @@ -512,7 +525,7 @@ if ($target -in @("all", "gui")) { "-S", $guiSourceDir, "-B", $guiBuildDir, "-DScopeOneCore_ROOT=$coreInstallDir" - ) + $guiConfigureOption + ) + $vcpkgConfigureArgs + $guiConfigureOption Invoke-Step ` -Label "Configuring ScopeOne GUI" ` diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 0000000..8ba8a20 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,46 @@ +{ + "name": "scopeone", + "builtin-baseline": "434307da09bc05b2c86996dccc8b2351fc0d5d37", + "dependencies": [ + { + "name": "opencv4", + "default-features": false, + "features": ["fs", "intrinsics", "jpeg", "png", "thread", "tiff", "webp"] + }, + { + "name": "qtbase", + "default-features": false, + "features": ["concurrent", "dnslookup", "gui", "harfbuzz", "jpeg", "network", "opengl", "png", "thread", "widgets"] + }, + { + "name": "qtsvg", + "default-features": false + }, + { + "name": "qtbase", + "default-features": false, + "features": ["windeployqt"], + "platform": "windows" + }, + { + "name": "qtbase", + "default-features": false, + "features": ["egl", "fontconfig", "xcb", "xcb-xlib", "xrender"], + "platform": "linux" + } + ], + "overrides": [ + { + "name": "opencv4", + "version": "4.14.0" + }, + { + "name": "qtbase", + "version": "6.11.2" + }, + { + "name": "qtsvg", + "version": "6.11.2" + } + ] +} From 2aaa944ac5e31d46f89ce1778fc82d16afb8b4bd Mon Sep 17 00:00:00 2001 From: tz <185176969+tzhaoo@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:58:52 +0200 Subject: [PATCH 2/8] Use dynamic vcpkg triplets on Linux and macOS The default static x64-linux triplet linked a separate copy of Qt into both libScopeOneCore.so and the ScopeOne executable, which crashed at startup in QGuiApplication::screenAdded. Build with the *-dynamic triplets so the GUI, core, and plugins share one Qt, and set the host triplet to match so vcpkg does not build a second full-featured host Qt. Shared vcpkg libraries need to be found at runtime, so the installed ScopeOneCore, ScopeWriter, and DriverHost now keep the link directories in their RPATH, and DriverHost also searches its own directory. Update the Linux system package list to what qtbase actually requires (adds libxcb-xinput-dev and libopengl-dev, drops unused packages) and avoid upgrading Mesa drivers. Bump ScopeWriter for the ZLIB::ZLIB fix. --- README.md | 18 ++++++++++-------- ScopeOneCore/CMakeLists.txt | 4 +++- ScopeOneCore/external/ScopeWriter | 2 +- scripts/build-unix.sh | 5 +++-- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 81c0db3..0740d76 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ Build Core, plugins, and GUI with the repository script: When migrating an existing build, use `--clean --configure` once to replace the old CMake caches. Later builds only need `.\scripts\build.ps1`. -The root `vcpkg.json` pins Qt and OpenCV. Core, GUI, and plugins share `vcpkg_installed/`; the build scripts select `x64-windows`, `x64-linux`, `arm64-osx`, or another supported host triplet. CMake installs dependencies on the first configure, and the Windows GUI uses vcpkg's `windeployqt` to deploy Qt plugins. Micro-Manager, CUDA, ONNX Runtime, and ScopeWriter retain their existing dependency setup. +The root `vcpkg.json` pins Qt and OpenCV. Core, GUI, and plugins share `vcpkg_installed/`; the build scripts select `x64-windows`, `x64-linux-dynamic`, `arm64-osx-dynamic`, or another supported host triplet. Linux and macOS use the dynamic triplets so Qt is shared between the GUI, ScopeOneCore, and plugins. CMake installs dependencies on the first configure, and the Windows GUI uses vcpkg's `windeployqt` to deploy Qt plugins. Micro-Manager, CUDA, ONNX Runtime, and ScopeWriter retain their existing dependency setup. For a direct Core CMake configure: @@ -150,15 +150,17 @@ Linux: ```bash sudo apt install \ git subversion build-essential cmake autoconf automake libtool autoconf-archive \ - pkg-config ninja-build curl zip unzip libboost-all-dev \ - libgl1-mesa-dev libegl1-mesa-dev libx11-dev libx11-xcb-dev libxext-dev \ - libxfixes-dev libxi-dev libxrender-dev libxcb1-dev libxcb-cursor-dev libxcb-util-dev \ - libxcb-glx0-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev \ - libxcb-randr0-dev libxcb-render-util0-dev libxcb-shape0-dev libxcb-shm0-dev \ - libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev libxcb-xkb-dev \ - libxkbcommon-dev libxkbcommon-x11-dev + pkg-config ninja-build curl zip unzip libboost-all-dev +sudo apt install --no-upgrade \ + libgl-dev libegl-dev libopengl-dev libx11-dev libx11-xcb-dev libxrender-dev \ + libxcb1-dev libxcb-cursor-dev libxcb-icccm4-dev libxcb-util-dev libxcb-image0-dev \ + libxcb-keysyms1-dev libxcb-randr0-dev libxcb-render-util0-dev libxcb-shape0-dev \ + libxcb-shm0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xkb-dev libxcb-xinput-dev \ + libxcb-glx0-dev libxkbcommon-dev libxkbcommon-x11-dev ``` +The second command installs the X11 and OpenGL development files that vcpkg's `qtbase` port expects from the system. It uses the glvnd `libgl-dev`/`libegl-dev` packages instead of `libgl1-mesa-dev`/`libegl1-mesa-dev`, so `--no-upgrade` can install it without upgrading the installed Mesa drivers. + macOS: ```bash diff --git a/ScopeOneCore/CMakeLists.txt b/ScopeOneCore/CMakeLists.txt index f76b4dd..969083b 100644 --- a/ScopeOneCore/CMakeLists.txt +++ b/ScopeOneCore/CMakeLists.txt @@ -168,6 +168,7 @@ if (UNIX AND NOT APPLE) set_target_properties(ScopeWriter ScopeOneCore PROPERTIES BUILD_RPATH "$ORIGIN" INSTALL_RPATH "$ORIGIN" + INSTALL_RPATH_USE_LINK_PATH TRUE ) endif () set_target_properties(ScopeOnePluginSDK PROPERTIES EXPORT_NAME PluginSDK) @@ -247,7 +248,8 @@ add_executable(ScopeOne_DriverHost if (UNIX AND NOT APPLE) set_target_properties(ScopeOne_DriverHost PROPERTIES BUILD_RPATH "$ORIGIN" - INSTALL_RPATH "$ORIGIN/../lib" + INSTALL_RPATH "$ORIGIN/../lib;$ORIGIN" + INSTALL_RPATH_USE_LINK_PATH TRUE ) endif () target_link_libraries(ScopeOne_DriverHost diff --git a/ScopeOneCore/external/ScopeWriter b/ScopeOneCore/external/ScopeWriter index 69f6bcb..c21226e 160000 --- a/ScopeOneCore/external/ScopeWriter +++ b/ScopeOneCore/external/ScopeWriter @@ -1 +1 @@ -Subproject commit 69f6bcb952cecdcc01ffb7ebf76f3edcb17cbe87 +Subproject commit c21226ed94a91742449f49482e31747e3aab781f diff --git a/scripts/build-unix.sh b/scripts/build-unix.sh index f85b93c..a5ebc55 100755 --- a/scripts/build-unix.sh +++ b/scripts/build-unix.sh @@ -42,10 +42,10 @@ esac case "$(uname -m)" in x86_64|amd64) - VCPKG_TRIPLET="$([[ "$PLATFORM_NAME" == "Linux" ]] && echo x64-linux || echo x64-osx)" + VCPKG_TRIPLET="$([[ "$PLATFORM_NAME" == "Linux" ]] && echo x64-linux-dynamic || echo x64-osx-dynamic)" ;; aarch64|arm64) - VCPKG_TRIPLET="$([[ "$PLATFORM_NAME" == "Linux" ]] && echo arm64-linux || echo arm64-osx)" + VCPKG_TRIPLET="$([[ "$PLATFORM_NAME" == "Linux" ]] && echo arm64-linux-dynamic || echo arm64-osx-dynamic)" ;; *) echo "Unsupported host architecture: $(uname -m)" >&2 @@ -58,6 +58,7 @@ VCPKG_ARGS=( "-DVCPKG_MANIFEST_DIR=$ROOT_DIR" "-DVCPKG_INSTALLED_DIR=$ROOT_DIR/vcpkg_installed" "-DVCPKG_TARGET_TRIPLET=$VCPKG_TRIPLET" + "-DVCPKG_HOST_TRIPLET=$VCPKG_TRIPLET" "-DCMAKE_DISABLE_FIND_PACKAGE_Tiff=ON" ) From fe0eb9d3b9d01013f5d29ced62a80890e3d9df8d Mon Sep 17 00:00:00 2001 From: tz <185176969+tzhaoo@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:22:35 +0200 Subject: [PATCH 3/8] Sync CI Linux packages with qtbase requirements Install the same X11 and OpenGL development packages as the README. Adds libxcb-xinput-dev, which qtbase requires because the vcpkg port disables the bundled xcb-xinput, and libopengl-dev for FindOpenGL's OpenGL component. Drops packages qtbase does not use. --- .github/workflows/compile-check.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/compile-check.yml b/.github/workflows/compile-check.yml index 494607f..c6ddbce 100644 --- a/.github/workflows/compile-check.yml +++ b/.github/workflows/compile-check.yml @@ -40,13 +40,11 @@ jobs: pkg-config \ libboost-all-dev \ ninja-build curl zip unzip \ - libgl1-mesa-dev libegl1-mesa-dev \ - libx11-dev libx11-xcb-dev libxext-dev libxfixes-dev libxi-dev libxrender-dev \ - libxcb1-dev libxcb-cursor-dev libxcb-util-dev libxcb-glx0-dev libxcb-icccm4-dev \ - libxcb-image0-dev libxcb-keysyms1-dev libxcb-randr0-dev \ - libxcb-render-util0-dev libxcb-shape0-dev libxcb-shm0-dev \ - libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev libxcb-xkb-dev \ - libxkbcommon-dev libxkbcommon-x11-dev + libgl-dev libegl-dev libopengl-dev libx11-dev libx11-xcb-dev libxrender-dev \ + libxcb1-dev libxcb-cursor-dev libxcb-icccm4-dev libxcb-util-dev libxcb-image0-dev \ + libxcb-keysyms1-dev libxcb-randr0-dev libxcb-render-util0-dev libxcb-shape0-dev \ + libxcb-shm0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xkb-dev libxcb-xinput-dev \ + libxcb-glx0-dev libxkbcommon-dev libxkbcommon-x11-dev - name: Set up vcpkg shell: bash From 09ea7a2f15962c1f0a12eaf606ff72d62a29dfb7 Mon Sep 17 00:00:00 2001 From: tz <185176969+tzhaoo@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:47:40 +0200 Subject: [PATCH 4/8] Polish recording, preview, and layout controls --- src/DeviceControlWidget.cpp | 17 ++++-- src/InspectWidget.cpp | 107 ------------------------------------ src/InspectWidget.h | 6 -- src/MainWindow.cpp | 75 +++++++++++++++++++++++++ src/MainWindow.h | 4 ++ src/RecordingWidget.cpp | 17 +++++- 6 files changed, 106 insertions(+), 120 deletions(-) diff --git a/src/DeviceControlWidget.cpp b/src/DeviceControlWidget.cpp index 7eea85f..adae816 100644 --- a/src/DeviceControlWidget.cpp +++ b/src/DeviceControlWidget.cpp @@ -683,8 +683,11 @@ namespace scopeone::ui QStringLiteral("Update display levels continuously as images arrive")); auto* layerHistogramLogCheckBox = new QCheckBox(QStringLiteral("Log scale"), layerHistogramGroup); - histogramLayout->addWidget(m_layerAutoStretchCheckBox); - histogramLayout->addWidget(layerHistogramLogCheckBox); + auto* histogramOptionsLayout = new QHBoxLayout(); + histogramOptionsLayout->addWidget(m_layerAutoStretchCheckBox); + histogramOptionsLayout->addWidget(layerHistogramLogCheckBox); + histogramOptionsLayout->addStretch(1); + histogramLayout->addLayout(histogramOptionsLayout); connect(layerHistogramLogCheckBox, &QCheckBox::toggled, m_layerHistogramWidget, &LayerHistogramWidget::setLogScale); @@ -1451,8 +1454,10 @@ namespace scopeone::ui { emit snapRequested(m_currentTarget); }); - layout->addWidget(m_previewToggleButton, row, 0); - layout->addWidget(m_snapButton, row, 1); + QHBoxLayout* previewLayout = new QHBoxLayout(); + previewLayout->addWidget(m_previewToggleButton, 1); + previewLayout->addWidget(m_snapButton, 1); + layout->addLayout(previewLayout, row, 0, 1, 2); QHBoxLayout* roiLayout = new QHBoxLayout(); m_drawROIButton = new QPushButton("Draw ROI", group); @@ -1932,6 +1937,10 @@ namespace scopeone::ui m_previewToggleButton->setText(m_previewRunning ? QStringLiteral("Stop Preview") : QStringLiteral("Start Preview")); + m_previewToggleButton->setStyleSheet(m_previewRunning && canControlHardware + ? QStringLiteral( + "QPushButton { background-color: #c0392b; color: white; font-weight: bold; }") + : QString()); const bool hasRoiTarget = !roiCameraTarget().isEmpty(); m_drawROIButton->setEnabled(canControlHardware && hasRoiTarget); m_halfROIButton->setEnabled(canControlHardware && hasRoiTarget); diff --git a/src/InspectWidget.cpp b/src/InspectWidget.cpp index 76a5caa..f136295 100644 --- a/src/InspectWidget.cpp +++ b/src/InspectWidget.cpp @@ -12,8 +12,6 @@ #include #include #include -#include -#include #include #include #include @@ -615,67 +613,10 @@ namespace scopeone::ui infoGroup.layerKey = normalizedLayerKey; infoGroup.groupBox = group; - auto* slidersLayout = new QHBoxLayout(); - - auto* minLabel = new QLabel(QStringLiteral("Min:"), group); - auto* minSlider = new QSlider(Qt::Horizontal, group); - minSlider->setRange(0, 255); - minSlider->setValue(0); - minSlider->setMinimumWidth(100); - auto* minSliderValueLabel = new QLabel(QStringLiteral("0"), group); - minSliderValueLabel->setMinimumWidth(50); - minSliderValueLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); - - auto* maxLabel = new QLabel(QStringLiteral("Max:"), group); - auto* maxSlider = new QSlider(Qt::Horizontal, group); - maxSlider->setRange(0, 255); - maxSlider->setValue(255); - maxSlider->setMinimumWidth(100); - auto* maxSliderValueLabel = new QLabel(QStringLiteral("255"), group); - maxSliderValueLabel->setMinimumWidth(50); - maxSliderValueLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter); - - slidersLayout->addWidget(minLabel); - slidersLayout->addWidget(minSlider, 1); - slidersLayout->addWidget(minSliderValueLabel); - slidersLayout->addWidget(maxLabel); - slidersLayout->addWidget(maxSlider, 1); - slidersLayout->addWidget(maxSliderValueLabel); - layout->addLayout(slidersLayout); - - infoGroup.minSlider = minSlider; - infoGroup.maxSlider = maxSlider; - infoGroup.minSliderValueLabel = minSliderValueLabel; - infoGroup.maxSliderValueLabel = maxSliderValueLabel; layout->addWidget(createStatisticsGroup(infoGroup)); m_layerInfoGroups.insert(normalizedLayerKey, infoGroup); m_contentLayout->insertWidget(m_contentLayout->count() - 1, group); - connect(minSlider, &QSlider::valueChanged, this, - [this, normalizedLayerKey, minSlider, maxSlider, minSliderValueLabel](int value) - { - if (value >= maxSlider->value()) - { - QSignalBlocker blocker(minSlider); - minSlider->setValue(maxSlider->value() - 1); - value = maxSlider->value() - 1; - } - minSliderValueLabel->setText(QString::number(value)); - onLayerSliderChanged(normalizedLayerKey, value, maxSlider->value()); - }); - connect(maxSlider, &QSlider::valueChanged, this, - [this, normalizedLayerKey, minSlider, maxSlider, maxSliderValueLabel](int value) - { - if (value <= minSlider->value()) - { - QSignalBlocker blocker(maxSlider); - maxSlider->setValue(minSlider->value() + 1); - value = minSlider->value() + 1; - } - maxSliderValueLabel->setText(QString::number(value)); - onLayerSliderChanged(normalizedLayerKey, minSlider->value(), value); - }); - return group; } @@ -754,7 +695,6 @@ namespace scopeone::ui { return; } - LayerInfoGroup& infoGroup = it.value(); LayerInspectState& state = getOrCreateLayerState(normalizedLayerKey); state.stats = stats; @@ -764,25 +704,6 @@ namespace scopeone::ui return; } - scopeone::core::DocumentLayer layer; - if (!m_sceneModel || !m_sceneModel->findLayer(normalizedLayerKey, layer)) - { - return; - } - - const int maxValue = qMax(1, layer.display.levelDomainMax); - const int displayMin = qBound(0, layer.display.levelMin, maxValue - 1); - const int displayMax = qBound(displayMin + 1, layer.display.levelMax, maxValue); - infoGroup.minSlider->setRange(0, maxValue); - infoGroup.maxSlider->setRange(0, maxValue); - { - QSignalBlocker minBlocker(infoGroup.minSlider); - QSignalBlocker maxBlocker(infoGroup.maxSlider); - infoGroup.minSlider->setValue(displayMin); - infoGroup.maxSlider->setValue(displayMax); - } - infoGroup.minSliderValueLabel->setText(QString::number(displayMin)); - infoGroup.maxSliderValueLabel->setText(QString::number(displayMax)); updateStatisticsDisplay(normalizedLayerKey, stats); updateControlsState(); } @@ -840,16 +761,6 @@ namespace scopeone::ui && m_availableLayerKeys.contains(layerKey); m_drawMeasurementLineButton->setEnabled(annotationEnabled); m_clearMeasurementLinesButton->setEnabled(annotationEnabled); - - for (auto it = m_layerInfoGroups.begin(); it != m_layerInfoGroups.end(); ++it) - { - LayerInfoGroup& infoGroup = it.value(); - const auto stateIt = m_layerStates.constFind(infoGroup.layerKey); - const bool hasStats = stateIt != m_layerStates.constEnd() && stateIt.value().hasStats; - const bool isActiveLayer = infoGroup.layerKey == layerKey; - infoGroup.minSlider->setEnabled(hasStats && isActiveLayer); - infoGroup.maxSlider->setEnabled(hasStats && isActiveLayer); - } } // Shows inspect controls for the selected preview layer @@ -883,24 +794,6 @@ namespace scopeone::ui return it.value(); } - // Apply manual display range changes from layer sliders - void InspectWidget::onLayerSliderChanged(const QString& layerKey, int minValue, int maxValue) - { - auto stateIt = m_layerStates.find(layerKey); - if (stateIt == m_layerStates.end()) - { - return; - } - const LayerInspectState& state = stateIt.value(); - if (!state.hasStats) - { - return; - } - m_workspace->setLayerAutoStretchEnabled(layerKey, false); - m_sceneModel->setLayerDisplayLevels( - layerKey, minValue, maxValue, qMax(1, state.stats.maxValue)); - } - QString InspectWidget::currentLayerCameraId() const { return scopeone::core::ScopeOneCore::sourceIdFromLayerKey(currentLayerKey()); diff --git a/src/InspectWidget.h b/src/InspectWidget.h index 03a39c0..ac1cc0d 100644 --- a/src/InspectWidget.h +++ b/src/InspectWidget.h @@ -12,7 +12,6 @@ class QGroupBox; class QLabel; class QPushButton; -class QSlider; class QVBoxLayout; namespace scopeone::ui @@ -63,10 +62,6 @@ namespace scopeone::ui { QString layerKey; QGroupBox* groupBox{nullptr}; - QSlider* minSlider{nullptr}; - QSlider* maxSlider{nullptr}; - QLabel* minSliderValueLabel{nullptr}; - QLabel* maxSliderValueLabel{nullptr}; QLabel* meanLabel{nullptr}; QLabel* minLabel{nullptr}; QLabel* maxLabel{nullptr}; @@ -98,7 +93,6 @@ namespace scopeone::ui void restoreViewerState(); QString currentLayerKey() const; LayerInspectState& getOrCreateLayerState(const QString& layerKey); - void onLayerSliderChanged(const QString& layerKey, int minValue, int maxValue); QString currentLayerCameraId() const; scopeone::core::ScopeOneCore* m_scopeonecore{nullptr}; diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 7c1cb08..ae3b2f4 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -63,6 +63,8 @@ namespace scopeone::ui namespace { constexpr qsizetype kMaxRecentConfigurations = 8; + // Bump when dock widgets change so stale saved layouts are ignored + constexpr int kWindowLayoutVersion = 1; // Build raw layer keys for all camera ids QStringList rawLayerKeys(const QStringList& cameraIds) @@ -298,6 +300,7 @@ namespace scopeone::ui setWindowTitle("ScopeOne"); setMinimumSize(1366, 768); resize(1600, 900); + restoreWindowLayout(); } // Confirm what to do with unsaved gallery sessions @@ -309,6 +312,24 @@ namespace scopeone::ui return; } + saveWindowLayout(); + + // Core shutdown stops and finalizes the recording; confirm before cutting it short + if (m_scopeonecore->isRecording()) + { + const QMessageBox::StandardButton recordingReply = QMessageBox::warning( + this, + tr("Recording in Progress"), + tr("A recording is still running. Stop the recording and close ScopeOne?"), + QMessageBox::Yes | QMessageBox::Cancel, + QMessageBox::Cancel); + if (recordingReply != QMessageBox::Yes) + { + event->ignore(); + return; + } + } + const auto unsavedSessions = m_imageGalleryWidget->unsavedSessions(); if (unsavedSessions.isEmpty()) { @@ -963,6 +984,17 @@ namespace scopeone::ui { return m_imageWorkspace->pixelSizeUm(layerKey); }); + // Remember the scale bar choice across launches + { + QSettings settings(QStringLiteral("ScopeOne"), QStringLiteral("ScopeOne")); + m_previewWidget->setScaleBarVisible( + settings.value(QStringLiteral("View/scaleBarVisible"), true).toBool()); + } + connect(m_previewWidget, &PreviewWidget::scaleBarVisibilityChanged, this, [](bool visible) + { + QSettings settings(QStringLiteral("ScopeOne"), QStringLiteral("ScopeOne")); + settings.setValue(QStringLiteral("View/scaleBarVisible"), visible); + }); setCentralWidget(m_imageWorkspace->viewerHost()); setupTools(); @@ -1241,6 +1273,11 @@ namespace scopeone::ui viewMenu->addSeparator(); m_dockWidgetsMenu = viewMenu->addMenu(tr("&Dock Widgets")); + auto* resetLayoutAction = viewMenu->addAction(tr("&Reset Layout")); + connect(resetLayoutAction, &QAction::triggered, this, [this]() + { + restoreState(m_defaultWindowState, kWindowLayoutVersion); + }); auto* togglePreviewAction = new QAction(tr("Toggle Live Preview"), this); togglePreviewAction->setShortcut(QKeySequence(Qt::Key_Space)); @@ -1342,26 +1379,36 @@ namespace scopeone::ui m_imageProcessingWidget = new ImageProcessingWidget(m_scopeonecore, m_imageWorkspace, this); m_controlDockWidget = new QDockWidget(tr("Control"), this); + + m_controlDockWidget->setObjectName(QStringLiteral("ControlDock")); m_controlDockWidget->setWidget(m_deviceControlWidget->hardwareControlsWidget()); m_controlDockWidget->setAllowedAreas(Qt::RightDockWidgetArea); addDockWidget(Qt::RightDockWidgetArea, m_controlDockWidget); m_viewDockWidget = new QDockWidget(tr("View"), this); + + m_viewDockWidget->setObjectName(QStringLiteral("ViewDock")); m_viewDockWidget->setWidget(m_deviceControlWidget->imageControlsWidget()); m_viewDockWidget->setAllowedAreas(Qt::RightDockWidgetArea); tabifyDockWidget(m_controlDockWidget, m_viewDockWidget); m_analyzeDockWidget = new QDockWidget(tr("Analyze"), this); + + m_analyzeDockWidget->setObjectName(QStringLiteral("AnalyzeDock")); m_analyzeDockWidget->setWidget(m_inspectWidget); m_analyzeDockWidget->setAllowedAreas(Qt::RightDockWidgetArea); tabifyDockWidget(m_controlDockWidget, m_analyzeDockWidget); m_processDockWidget = new QDockWidget(tr("Process"), this); + + m_processDockWidget->setObjectName(QStringLiteral("ProcessDock")); m_processDockWidget->setWidget(m_imageProcessingWidget); m_processDockWidget->setAllowedAreas(Qt::RightDockWidgetArea); tabifyDockWidget(m_controlDockWidget, m_processDockWidget); m_consoleDockWidget = new QDockWidget(tr("Console"), this); + + m_consoleDockWidget->setObjectName(QStringLiteral("ConsoleDock")); m_consoleWidget = new ConsoleWidget(m_consoleDockWidget); m_consoleDockWidget->setWidget(m_consoleWidget); m_consoleDockWidget->setAllowedAreas(Qt::RightDockWidgetArea); @@ -1379,6 +1426,7 @@ namespace scopeone::ui void MainWindow::setupPropertyBrowser() { m_propertyDockWidget = new QDockWidget(tr("Properties"), this); + m_propertyDockWidget->setObjectName(QStringLiteral("PropertiesDock")); m_propertyDockWidget->setAllowedAreas(Qt::LeftDockWidgetArea); m_propertyBrowser = new DevicePropertyWidget(m_scopeonecore, this); @@ -1388,6 +1436,8 @@ namespace scopeone::ui addDockWidget(Qt::LeftDockWidgetArea, m_propertyDockWidget); m_configPresetDockWidget = new QDockWidget(tr("Configs"), this); + + m_configPresetDockWidget->setObjectName(QStringLiteral("ConfigsDock")); m_configPresetDockWidget->setWidget(m_configPresetWidget); m_configPresetDockWidget->setAllowedAreas(Qt::LeftDockWidgetArea); } @@ -1396,6 +1446,7 @@ namespace scopeone::ui void MainWindow::setupRecording() { m_recordingDockWidget = new QDockWidget(tr("Recording"), this); + m_recordingDockWidget->setObjectName(QStringLiteral("RecordingDock")); m_recordingDockWidget->setAllowedAreas(Qt::LeftDockWidgetArea); m_recordingWidget = new RecordingWidget(m_scopeonecore, this); @@ -1409,6 +1460,7 @@ namespace scopeone::ui void MainWindow::setupImageGallery() { m_imageGalleryDockWidget = new QDockWidget(tr("Image Gallery"), this); + m_imageGalleryDockWidget->setObjectName(QStringLiteral("ImageGalleryDock")); m_imageGalleryDockWidget->setAllowedAreas(Qt::LeftDockWidgetArea); m_imageGalleryDockWidget->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | @@ -1521,6 +1573,29 @@ namespace scopeone::ui addDock(m_consoleDockWidget, QStringLiteral("Console")); } + // Restore the saved window geometry and dock layout + void MainWindow::restoreWindowLayout() + { + QSettings settings(QStringLiteral("ScopeOne"), QStringLiteral("ScopeOne")); + restoreGeometry(settings.value(QStringLiteral("MainWindow/geometry")).toByteArray()); + const QByteArray savedState = settings.value(QStringLiteral("MainWindow/state")).toByteArray(); + + // Dock sizes are only valid once the window is shown, so capture the default layout then + QTimer::singleShot(0, this, [this, savedState]() + { + m_defaultWindowState = saveState(kWindowLayoutVersion); + restoreState(savedState, kWindowLayoutVersion); + }); + } + + // Save the window geometry and dock layout for the next launch + void MainWindow::saveWindowLayout() const + { + QSettings settings(QStringLiteral("ScopeOne"), QStringLiteral("ScopeOne")); + settings.setValue(QStringLiteral("MainWindow/geometry"), saveGeometry()); + settings.setValue(QStringLiteral("MainWindow/state"), saveState(kWindowLayoutVersion)); + } + // Push loaded camera ids into every dependent panel void MainWindow::applyLoadedCameraState(const QStringList& cameraIds) { diff --git a/src/MainWindow.h b/src/MainWindow.h index 36252ac..b44a6c6 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -3,6 +3,7 @@ #include "scopeone/ScopeOneCore.h" #include "ScopeOneToolPlugin.h" +#include #include #include #include @@ -76,6 +77,8 @@ namespace scopeone::ui void showLivePreview(); void updateControlTarget(const QString& target); void updateDockWidgetMenu(); + void restoreWindowLayout(); + void saveWindowLayout() const; void syncCameraState(); void applyLoadedCameraState(const QStringList& cameraIds); void applyNoCameraState(); @@ -150,6 +153,7 @@ namespace scopeone::ui QMenu* m_recentConfigurationsMenu{nullptr}; QMenu* m_dockWidgetsMenu{nullptr}; + QByteArray m_defaultWindowState; QAction* m_toggleDimensionAction{nullptr}; QAction* m_toggle3dColorbarAction{nullptr}; diff --git a/src/RecordingWidget.cpp b/src/RecordingWidget.cpp index 5aa776c..e5f3369 100644 --- a/src/RecordingWidget.cpp +++ b/src/RecordingWidget.cpp @@ -308,6 +308,10 @@ namespace scopeone::ui { m_isRecording = recording; m_startStopButton->setText(recording ? "Stop" : "Start"); + m_startStopButton->setStyleSheet(recording + ? QStringLiteral( + "QPushButton { background-color: #c0392b; color: white; font-weight: bold; }") + : QString()); if (recording) { m_storageStatusTimer->start(); @@ -642,16 +646,23 @@ namespace scopeone::ui statusLayout->addWidget(m_burstCountLabel, 1, 1); statusLayout->addWidget(m_writerStatusLabel, 2, 0, 1, 2); statusLayout->addWidget(m_storageStatusLabel, 3, 0, 1, 2); - contentLayout->addWidget(statusGroup); m_startStopButton = new QPushButton("Start", this); m_startStopButton->setMinimumHeight(28); - contentLayout->addWidget(m_startStopButton); contentLayout->addStretch(1); scrollArea->setWidget(content); - mainLayout->addWidget(scrollArea); + mainLayout->addWidget(scrollArea, 1); + + // Keep status and Start/Stop visible below the scrolling settings + auto* footer = new QWidget(this); + auto* footerLayout = new QVBoxLayout(footer); + footerLayout->setContentsMargins(6, 6, 6, 6); + footerLayout->setSpacing(6); + footerLayout->addWidget(statusGroup); + footerLayout->addWidget(m_startStopButton); + mainLayout->addWidget(footer); } // Updates available detector choices From d36f78d19e7484e7680123419b5170b01a5377b6 Mon Sep 17 00:00:00 2001 From: tz <185176969+tzhaoo@users.noreply.github.com> Date: Wed, 7 Oct 2026 17:23:51 +0200 Subject: [PATCH 5/8] Provide ScopeWriter dependencies through vcpkg --- README.md | 3 +-- ScopeOneCore/external/ScopeWriter | 2 +- scripts/build-unix.sh | 1 - scripts/build.ps1 | 16 +++++++++++++--- vcpkg.json | 8 ++++++++ 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0740d76..0f8ef11 100644 --- a/README.md +++ b/README.md @@ -127,8 +127,7 @@ cmake -S ScopeOneCore -B ScopeOneCore/build ` "-DCMAKE_TOOLCHAIN_FILE=$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" ` "-DVCPKG_MANIFEST_DIR=$PWD" ` "-DVCPKG_INSTALLED_DIR=$PWD/vcpkg_installed" ` - -DVCPKG_TARGET_TRIPLET=x64-windows ` - -DCMAKE_DISABLE_FIND_PACKAGE_Tiff=ON + -DVCPKG_TARGET_TRIPLET=x64-windows ``` Run the built application: diff --git a/ScopeOneCore/external/ScopeWriter b/ScopeOneCore/external/ScopeWriter index c21226e..e070daf 160000 --- a/ScopeOneCore/external/ScopeWriter +++ b/ScopeOneCore/external/ScopeWriter @@ -1 +1 @@ -Subproject commit c21226ed94a91742449f49482e31747e3aab781f +Subproject commit e070dafd0549f4afbe499a997f69f0721388c83b diff --git a/scripts/build-unix.sh b/scripts/build-unix.sh index a5ebc55..db803a5 100755 --- a/scripts/build-unix.sh +++ b/scripts/build-unix.sh @@ -59,7 +59,6 @@ VCPKG_ARGS=( "-DVCPKG_INSTALLED_DIR=$ROOT_DIR/vcpkg_installed" "-DVCPKG_TARGET_TRIPLET=$VCPKG_TRIPLET" "-DVCPKG_HOST_TRIPLET=$VCPKG_TRIPLET" - "-DCMAKE_DISABLE_FIND_PACKAGE_Tiff=ON" ) JOBS="${JOBS:-$DEFAULT_JOBS}" diff --git a/scripts/build.ps1 b/scripts/build.ps1 index c51ebe4..b525dad 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -246,7 +246,8 @@ $guiCachePath = Join-Path $guiBuildDir "CMakeCache.txt" $pluginCachePath = Join-Path $pluginBuildDir "CMakeCache.txt" $vcpkgConfigureArgs = @() -if ($env:OS -eq "Windows_NT" -and $target -ne "scopewriter") { +$writerVcpkgArgs = @() +if ($env:OS -eq "Windows_NT") { if (-not $scopeOneVcpkgRoot) { throw "Set VCPKG_ROOT to a bootstrapped vcpkg checkout." } @@ -254,12 +255,21 @@ if ($env:OS -eq "Windows_NT" -and $target -ne "scopewriter") { if (-not (Test-Path $vcpkgToolchain)) { throw "Set VCPKG_ROOT to a bootstrapped vcpkg checkout." } +} +if ($env:OS -eq "Windows_NT" -and $target -eq "scopewriter") { + # Standalone ScopeWriter installs its own vcpkg.json dependencies and ships their DLLs + $writerVcpkgArgs = @( + "-DCMAKE_TOOLCHAIN_FILE=$vcpkgToolchain", + "-DVCPKG_TARGET_TRIPLET=x64-windows", + "-DX_VCPKG_APPLOCAL_DEPS_INSTALL=ON" + ) +} +elseif ($env:OS -eq "Windows_NT") { $vcpkgConfigureArgs = @( "-DCMAKE_TOOLCHAIN_FILE=$vcpkgToolchain", "-DVCPKG_MANIFEST_DIR=$repoRoot", "-DVCPKG_INSTALLED_DIR=$(Join-Path $repoRoot 'vcpkg_installed')", "-DVCPKG_TARGET_TRIPLET=x64-windows", - "-DCMAKE_DISABLE_FIND_PACKAGE_Tiff=ON", "-UTiff_DIR", "-UOpenCV_DIR", "-UQt6*_DIR", @@ -336,7 +346,7 @@ if ($target -eq "scopewriter") { "-B", $writerBuildDir, "-DCMAKE_INSTALL_PREFIX=$writerInstallDir", "-DSCOPEWRITER_BUILD_TESTS=ON" - ) + $writerGeneratorArgs) ` + ) + $writerVcpkgArgs + $writerGeneratorArgs) ` -WorkingDirectory $repoRoot Invoke-Step ` diff --git a/vcpkg.json b/vcpkg.json index 8ba8a20..7f27d78 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -2,6 +2,7 @@ "name": "scopeone", "builtin-baseline": "434307da09bc05b2c86996dccc8b2351fc0d5d37", "dependencies": [ + "crc32c", { "name": "opencv4", "default-features": false, @@ -16,6 +17,13 @@ "name": "qtsvg", "default-features": false }, + { + "name": "tiff", + "default-features": false, + "features": ["zip"] + }, + "zlib", + "zstd", { "name": "qtbase", "default-features": false, From e51a1bf500bdde4a6e495b1cbe5a4462afcac9ab Mon Sep 17 00:00:00 2001 From: tz <185176969+tzhaoo@users.noreply.github.com> Date: Wed, 7 Oct 2026 17:25:06 +0200 Subject: [PATCH 6/8] Update README for vcpkg-provided ScopeWriter dependencies --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0f8ef11..3799133 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ ScopeOne/ ScopeWriter/ ``` -ScopeWriter contains its filesystem Zarr V3 writer and carries libtiff, zlib, zstd and crc32c under its own `third_party` directory. It builds these dependencies from source without downloading packages during CMake configuration. +ScopeWriter contains its filesystem Zarr V3 writer. It uses libtiff, zlib, zstd and crc32c from vcpkg; ScopeOne lists them in its root `vcpkg.json`, and ScopeWriter declares the same dependencies in its own `vcpkg.json` for standalone builds. ### Plugin layout @@ -118,7 +118,7 @@ Build Core, plugins, and GUI with the repository script: When migrating an existing build, use `--clean --configure` once to replace the old CMake caches. Later builds only need `.\scripts\build.ps1`. -The root `vcpkg.json` pins Qt and OpenCV. Core, GUI, and plugins share `vcpkg_installed/`; the build scripts select `x64-windows`, `x64-linux-dynamic`, `arm64-osx-dynamic`, or another supported host triplet. Linux and macOS use the dynamic triplets so Qt is shared between the GUI, ScopeOneCore, and plugins. CMake installs dependencies on the first configure, and the Windows GUI uses vcpkg's `windeployqt` to deploy Qt plugins. Micro-Manager, CUDA, ONNX Runtime, and ScopeWriter retain their existing dependency setup. +The root `vcpkg.json` pins Qt and OpenCV and provides ScopeWriter's libtiff, zlib, zstd and crc32c. Core, GUI, and plugins share `vcpkg_installed/`; the build scripts select `x64-windows`, `x64-linux-dynamic`, `arm64-osx-dynamic`, or another supported host triplet. Linux and macOS use the dynamic triplets so Qt is shared between the GUI, ScopeOneCore, and plugins. CMake installs dependencies on the first configure, and the Windows GUI uses vcpkg's `windeployqt` to deploy Qt plugins. Micro-Manager, CUDA, and ONNX Runtime retain their existing dependency setup. For a direct Core CMake configure: From 902fbbab13e71f134af23817392fd555f37f8007 Mon Sep 17 00:00:00 2001 From: tz <185176969+tzhaoo@users.noreply.github.com> Date: Wed, 7 Oct 2026 17:38:40 +0200 Subject: [PATCH 7/8] Show ScopeWriter version in About instead of libtiff and zlib --- ScopeOneCore/external/ScopeWriter | 2 +- ScopeOneCore/include/scopeone/ScopeOneCore.h | 1 + ScopeOneCore/src/ScopeOneCore.cpp | 5 +++++ src/AboutDialog.cpp | 11 ++++------- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/ScopeOneCore/external/ScopeWriter b/ScopeOneCore/external/ScopeWriter index e070daf..bfcba79 160000 --- a/ScopeOneCore/external/ScopeWriter +++ b/ScopeOneCore/external/ScopeWriter @@ -1 +1 @@ -Subproject commit e070dafd0549f4afbe499a997f69f0721388c83b +Subproject commit bfcba79c6c357f8798e89b3800197fe5edbee3cf diff --git a/ScopeOneCore/include/scopeone/ScopeOneCore.h b/ScopeOneCore/include/scopeone/ScopeOneCore.h index 163050f..d98ad48 100644 --- a/ScopeOneCore/include/scopeone/ScopeOneCore.h +++ b/ScopeOneCore/include/scopeone/ScopeOneCore.h @@ -519,6 +519,7 @@ namespace scopeone::core static QString getVersion(); static QString getMMCoreVersion(); static QString getOpenCVVersion(); + static QString getScopeWriterVersion(); static QString getLibTiffVersion(); static QString getZlibVersion(); static QString rawLayerKey(const QString& cameraId); diff --git a/ScopeOneCore/src/ScopeOneCore.cpp b/ScopeOneCore/src/ScopeOneCore.cpp index 6b5cd4f..08b4bf9 100644 --- a/ScopeOneCore/src/ScopeOneCore.cpp +++ b/ScopeOneCore/src/ScopeOneCore.cpp @@ -1145,6 +1145,11 @@ namespace scopeone::core } // Return the ScopeWriter libtiff version + QString ScopeOneCore::getScopeWriterVersion() + { + return QString::fromStdString(scopewriter::version()); + } + QString ScopeOneCore::getLibTiffVersion() { return QString::fromStdString(scopewriter::libTiffVersion()); diff --git a/src/AboutDialog.cpp b/src/AboutDialog.cpp index c9012c2..a2b33db 100644 --- a/src/AboutDialog.cpp +++ b/src/AboutDialog.cpp @@ -61,8 +61,7 @@ namespace scopeone::ui const QString coreVersion = scopeone::core::ScopeOneCore::getVersion(); const QString mmCoreVersion = scopeone::core::ScopeOneCore::getMMCoreVersion(); const QString openCvVersion = scopeone::core::ScopeOneCore::getOpenCVVersion(); - const QString libTiffVersion = scopeone::core::ScopeOneCore::getLibTiffVersion(); - const QString zlibVersion = scopeone::core::ScopeOneCore::getZlibVersion(); + const QString scopeWriterVersion = scopeone::core::ScopeOneCore::getScopeWriterVersion(); const QString commit = QStringLiteral(SCOPEONE_GIT_COMMIT); const QString platformInfo = QString("%1, %2") .arg(QSysInfo::prettyProductName(), QSysInfo::currentCpuArchitecture()); @@ -79,9 +78,8 @@ namespace scopeone::ui MMCore%4 Qt%5 OpenCV%6 -libtiff%7 -zlib%8 -Platform%9 +ScopeWriter%7 +Platform%8

Links

@@ -103,8 +101,7 @@ Licensed under the BSD 3-Clause License. mmCoreVersion.toHtmlEscaped(), QString::fromLatin1(qVersion()).toHtmlEscaped(), openCvVersion.toHtmlEscaped(), - libTiffVersion.toHtmlEscaped(), - zlibVersion.toHtmlEscaped(), + scopeWriterVersion.toHtmlEscaped(), platformInfo.toHtmlEscaped())); } From ed45bc2cbaa0047177369b3534273763df519c42 Mon Sep 17 00:00:00 2001 From: tz <185176969+tzhaoo@users.noreply.github.com> Date: Wed, 7 Oct 2026 17:43:19 +0200 Subject: [PATCH 8/8] Simplify Linux and macOS build instructions --- README.md | 79 +++++++------------------------------------------------ 1 file changed, 10 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index 3799133..e18ba0b 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ There is an example dual-camera .cfg file in the config folder, just change the - [CMake](https://cmake.org/download/) 4.1.0 - [Visual Studio 2022](https://visualstudio.microsoft.com/vs/) (MSVC v143 toolset) -- [vcpkg](https://github.com/microsoft/vcpkg) for Qt 6.11.2 and OpenCV 4.14.0 +- [vcpkg](https://github.com/microsoft/vcpkg) for Qt 6.11.2, OpenCV 4.14.0, and ScopeWriter's libtiff, zlib, zstd and crc32c - mmCoreAndDevices Clone ScopeOne and initialize all submodules with: @@ -138,11 +138,9 @@ Run the built application: **Linux and macOS Build Steps (experimental):** -Linux and macOS use vcpkg for Qt and OpenCV. Install and bootstrap vcpkg, set `VCPKG_ROOT`, then use the build script. The script selects the triplet from the host operating system and architecture. Micro-Manager is still built from its checkout because its native core and device adapters are not vcpkg dependencies. +Linux and macOS use vcpkg for Qt, OpenCV, and ScopeWriter's dependencies. Micro-Manager is built from its own checkout because its native core and device adapters are not vcpkg dependencies. -Run `./scripts/build-unix.sh` from the ScopeOne repository root on Linux or macOS. Add `--clean` once when migrating an existing system-package build. - -1. Install common build dependencies and vcpkg. +1. Install the system build tools. Linux: @@ -158,7 +156,7 @@ sudo apt install --no-upgrade \ libxcb-glx0-dev libxkbcommon-dev libxkbcommon-x11-dev ``` -The second command installs the X11 and OpenGL development files that vcpkg's `qtbase` port expects from the system. It uses the glvnd `libgl-dev`/`libegl-dev` packages instead of `libgl1-mesa-dev`/`libegl1-mesa-dev`, so `--no-upgrade` can install it without upgrading the installed Mesa drivers. +The second command installs the X11 and OpenGL development files that vcpkg's `qtbase` port expects from the system; it is not a system Qt installation. It uses the glvnd `libgl-dev`/`libegl-dev` packages instead of `libgl1-mesa-dev`/`libegl1-mesa-dev`, so `--no-upgrade` can install it without upgrading the installed Mesa drivers. macOS: @@ -169,82 +167,25 @@ brew install \ pkg-config ninja boost ``` +2. Install and bootstrap vcpkg: + ```bash git clone https://github.com/microsoft/vcpkg.git "$HOME/vcpkg" "$HOME/vcpkg/bootstrap-vcpkg.sh" export VCPKG_ROOT="$HOME/vcpkg" ``` -For an existing build made with system Qt or OpenCV, pass `--clean` once to remove its CMake caches. Linux still needs system X11 and OpenGL development headers for Qt's desktop platform integration; these are not a system Qt installation. - -2. Clone Micro-Manager and create the `mmCoreAndDevices` symlink: +3. Build from the ScopeOne repository root: ```bash -cd /path/to/ScopeOne/ScopeOneCore -mkdir -p external -cd external - -git clone --recurse-submodules https://github.com/micro-manager/micro-manager.git micro-manager -ln -s micro-manager/mmCoreAndDevices mmCoreAndDevices - -cd micro-manager -git submodule update --init --recursive -``` - -3. Configure Micro-Manager without the Java application layer: - -```bash -./autogen.sh -./configure --without-java --enable-static -``` - -4. Build the native core components: - -```bash -# Adjust `-j4` to match your CPU cores -make -C mmCoreAndDevices/MMDevice -j4 -make -C mmCoreAndDevices/MMCore -j4 -``` - -5. Build the adapters required by the demo configuration. You can build additional adapters as needed. - -```bash -make -C mmCoreAndDevices/DeviceAdapters/DemoCamera -j4 -make -C mmCoreAndDevices/DeviceAdapters/Utilities -j4 -``` - -6. Build and install `ScopeOneCore` and the GUI: - -```bash -cd /path/to/ScopeOne ./scripts/build-unix.sh ``` -The Linux or macOS executable is expected at: - -```text -build/ScopeOne -``` - -To run `config/MMConfig_demo.cfg`, copy its runtime adapters next to the ScopeOne executable. - -Linux: - -```bash -cp -L \ - ScopeOneCore/external/mmCoreAndDevices/DeviceAdapters/DemoCamera/.libs/libmmgr_dal_DemoCamera.so.0 \ - ScopeOneCore/external/mmCoreAndDevices/DeviceAdapters/Utilities/.libs/libmmgr_dal_Utilities.so.0 \ - build/ -``` +The script initializes the submodules, clones Micro-Manager into `ScopeOneCore/external/micro-manager` and links `mmCoreAndDevices`, builds MMDevice, MMCore and the DemoCamera and Utilities adapters, builds and installs ScopeOneCore, builds the GUI, and copies the demo adapters next to the executable. It selects the `x64-linux-dynamic`, `arm64-linux-dynamic`, `x64-osx-dynamic`, or `arm64-osx-dynamic` triplet from the host. Set `JOBS` to limit parallel jobs and `BUILD_TYPE` to change the CMake build type (default `Release`). -macOS uses an extensionless Mach-O bundle rather than the static `.a` file: +Pass `--clean` once when migrating from a build that used system Qt or OpenCV, or from an earlier static `x64-linux` triplet build. -```bash -cp \ - ScopeOneCore/external/mmCoreAndDevices/DeviceAdapters/DemoCamera/.libs/libmmgr_dal_DemoCamera \ - ScopeOneCore/external/mmCoreAndDevices/DeviceAdapters/Utilities/.libs/libmmgr_dal_Utilities \ - build/ -``` +The executable is `build/ScopeOne`, and `config/MMConfig_demo.cfg` runs with the copied demo adapters. It loads Qt and OpenCV from `vcpkg_installed//lib`, so run it from the build tree; there is no Linux or macOS deployment step yet. ## 🤖 Automation and AI Agents