Skip to content

Commit 156d6a7

Browse files
[migrations-gui] Qt 6 GUI for migration management
Ship a Qt 6 / QML desktop GUI for dbtool's migration workflow alongside the CLI. Users get a connection-aware view of pending vs applied migrations, a release-grouped timeline, one-click apply / rollback / backup, and an SQL preview before any destructive action. Architecture: - AppController is the top-level QML-exposed model: owns the SqlConnection, drives MigrationRunner / BackupRunner workers off the GUI thread, and exposes Q_PROPERTY surfaces consumed by the QML side. Persists profile selection, view-mode (Simple vs Expert) and window geometry via QSettings. - MigrationRunner / BackupRunner are QObject workers running on dedicated QThread instances. Both forward structured progress and log lines via Qt signals so the UI stays responsive during long operations. MigrationRunner consumes the structured MigrationException surface (timestamp, step index, driver message, failed SQL) for actionable failure reporting. - QmlProgressManager bridges Lightweight's IProgressManager interface to QML, mirroring the StandardProgressManager used by the CLI. - Models (MigrationListModel, ReleaseListModel, ProfileListModel, OdbcDataSourceListModel) expose typed list interfaces for the QML views; each is backed by domain data from Lightweight + ProfileStore + DataSourceEnumerator. - ThemeController owns palette + accent state and persists theme preference via QSettings. The Theme.qml singleton consumes it. Views: - Simple view: single centred column (connection -> status -> run -> progress -> success/failure) targeting downstream operators who just want "bring my DB up to date". Status card surfaces current/target release labels in large type, with a green up-to-date pill when no work is pending. Run card offers one primary button plus a "Back up first" checkbox that chains backup -> apply. - Expert view: full three-pane timeline with per-migration controls, bulk operations, release groupings, log panel, SQL preview, and backup/restore dialog. - ToolBar exposes a single Simple/Expert toggle button labelled with the destination view ("Switch to Expert view" / "Switch to Simple view"). FontMetrics reserves width for the longer label so the toolbar layout never reflows on toggle. - Per-view window sizing: each view stores its own preferred window geometry so toggling does not yank the user into a layout that doesn't fit their content. Build: - cmake/FindQt.cmake locates a Qt 6 install, top-level CMakeLists optionally descends into the GUI when Qt is found, and src/tools/CMakeLists.txt only registers the migrations-gui subdir when the optional dependency is satisfied. The CLI build is unaffected when Qt is absent. Docs: - docs/migrations-gui-plan.md captures the design rationale. - docs/migrations-gui-mockup.html is the static mockup the QML layout was derived from. Signed-off-by: Christian Parpart <christian@parpart.family>
1 parent 7f7c254 commit 156d6a7

62 files changed

Lines changed: 11371 additions & 4 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CMakeLists.txt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ endif()
4848
option(LIGHTWEIGHT_BUILD_LARGEDB_TOOL "Build large database generator tool" ${LIGHTWEIGHT_BUILD_LARGEDB_TOOL_DEFAULT})
4949
unset(LIGHTWEIGHT_BUILD_LARGEDB_TOOL_DEFAULT)
5050

51+
# GUI tool (Qt 6) — opt-in. If ON we auto-probe common Qt install locations
52+
# (C:/Qt, Homebrew qt@6, ~/Qt) and fall back gracefully if nothing is found.
53+
option(LIGHTWEIGHT_BUILD_GUI "Build the Qt 6 dbtool GUI (src/tools/dbtool-gui)" OFF)
54+
5155
option(LIGHTWEIGHT_BUILD_DOCUMENTATION "Create and install the HTML based API documentation (requires Doxygen)" OFF)
5256
option(LIGHTWEIGHT_DOCS_WARN_AS_ERROR "Treat Doxygen warnings as errors" OFF)
5357

@@ -177,6 +181,29 @@ set(CPACK_RESOURCE_FILE_README "${CMAKE_SOURCE_DIR}/README.md")
177181

178182
enable_testing()
179183

184+
# Resolve Qt 6 for the optional migrations GUI before descending into tools.
185+
# When LIGHTWEIGHT_BUILD_GUI=ON we auto-probe common install locations; if
186+
# nothing is found we downgrade the option to OFF instead of failing the
187+
# configure, so the rest of the project (library, dbtool, tests) still builds.
188+
if(LIGHTWEIGHT_BUILD_GUI)
189+
include(FindQt)
190+
lightweight_probe_qt6(
191+
MIN_VERSION 6.8.0
192+
REQUIRED_COMPONENTS Core Gui Qml Quick QuickControls2
193+
RESULT_VAR _LIGHTWEIGHT_QT6_PROBED
194+
)
195+
if(NOT _LIGHTWEIGHT_QT6_PROBED)
196+
message(WARNING
197+
"LIGHTWEIGHT_BUILD_GUI=ON but Qt 6 could not be auto-detected.\n"
198+
"Pass -DQt6_DIR=<path-to-qt>/lib/cmake/Qt6 or add the Qt prefix to "
199+
"CMAKE_PREFIX_PATH to enable the GUI. Disabling GUI build for now. "
200+
"See cmake/FindQt.cmake for the list of probed install locations."
201+
)
202+
set(LIGHTWEIGHT_BUILD_GUI OFF CACHE BOOL "" FORCE)
203+
endif()
204+
unset(_LIGHTWEIGHT_QT6_PROBED)
205+
endif()
206+
180207
add_subdirectory(src/Lightweight)
181208

182209
if(LIGHTWEIGHT_BUILD_TOOLS)

cmake/FindQt.cmake

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# FindQt.cmake
2+
#
3+
# Helper that locates a Qt 6 installation when the user has not explicitly
4+
# pointed CMake at one. The goal is to make `cmake -DLIGHTWEIGHT_BUILD_GUI=ON`
5+
# work out-of-the-box when Qt is installed in a standard location, while still
6+
# honouring explicit configuration (Qt6_DIR, CMAKE_PREFIX_PATH, QT_ROOT_DIR).
7+
#
8+
# NOTE: despite the Find*.cmake naming, this is a helper module that exposes
9+
# the `lightweight_probe_qt6()` function — it is not intended to be driven by
10+
# `find_package(Qt)` and does not set the usual `Qt_FOUND` contract. Include
11+
# it explicitly via `include(FindQt)` and then call `lightweight_probe_qt6()`.
12+
#
13+
# Resolution order:
14+
# 1. If Qt6_DIR is already set in the cache -> keep it, do nothing.
15+
# 2. If CMAKE_PREFIX_PATH already resolves Qt6 -> keep it, do nothing.
16+
# 3. Probe well-known install roots for Qt 6.5+ (LTS and newer).
17+
# 4. On failure, report what was searched and let the caller decide whether
18+
# to fall back to LIGHTWEIGHT_BUILD_GUI=OFF.
19+
#
20+
# After a successful probe, CMAKE_PREFIX_PATH is extended and the caller can
21+
# simply do `find_package(Qt6 ... COMPONENTS ...)`.
22+
#
23+
# Public entry point:
24+
# lightweight_probe_qt6([REQUIRED_COMPONENTS <comp> ...]
25+
# [MIN_VERSION <ver>]
26+
# [RESULT_VAR <var>])
27+
#
28+
# RESULT_VAR, if given, is set to TRUE on success and FALSE on failure.
29+
30+
include_guard(GLOBAL)
31+
32+
function(_lw_qt_candidate_roots out_var)
33+
set(roots "")
34+
35+
# Windows: Qt Online Installer default is C:/Qt, occasionally D:/Qt or on
36+
# the system drive. Also respect $USERPROFILE/Qt for per-user installs.
37+
if(WIN32)
38+
list(APPEND roots "C:/Qt" "D:/Qt")
39+
if(DEFINED ENV{SystemDrive})
40+
list(APPEND roots "$ENV{SystemDrive}/Qt")
41+
endif()
42+
if(DEFINED ENV{USERPROFILE})
43+
file(TO_CMAKE_PATH "$ENV{USERPROFILE}/Qt" _up_qt)
44+
list(APPEND roots "${_up_qt}")
45+
endif()
46+
endif()
47+
48+
# macOS: Homebrew keg-only qt@6 (both Apple Silicon and Intel prefixes).
49+
if(APPLE)
50+
list(APPEND roots
51+
"/opt/homebrew/opt/qt@6"
52+
"/opt/homebrew/opt/qt"
53+
"/usr/local/opt/qt@6"
54+
"/usr/local/opt/qt"
55+
)
56+
if(DEFINED ENV{HOME})
57+
list(APPEND roots "$ENV{HOME}/Qt")
58+
endif()
59+
endif()
60+
61+
# Linux: mostly handled by the distro-provided Qt6Config.cmake, but honour
62+
# a user-local Qt Online Installer layout under $HOME/Qt as a fallback.
63+
if(UNIX AND NOT APPLE)
64+
if(DEFINED ENV{HOME})
65+
list(APPEND roots "$ENV{HOME}/Qt")
66+
endif()
67+
list(APPEND roots "/opt/Qt")
68+
endif()
69+
70+
# Also honour QT_ROOT_DIR env var if set (Qt Online Installer sometimes
71+
# exports this in its "Qt Creator"-provided shell).
72+
if(DEFINED ENV{QT_ROOT_DIR})
73+
list(APPEND roots "$ENV{QT_ROOT_DIR}")
74+
endif()
75+
76+
list(REMOVE_DUPLICATES roots)
77+
set(${out_var} "${roots}" PARENT_SCOPE)
78+
endfunction()
79+
80+
# Given a Qt install root (e.g. C:/Qt), return candidate cmake directories
81+
# (.../lib/cmake/Qt6) sorted newest-version-first.
82+
function(_lw_qt_cmake_dirs_under_root root min_version out_var)
83+
set(candidates "")
84+
85+
if(NOT IS_DIRECTORY "${root}")
86+
set(${out_var} "" PARENT_SCOPE)
87+
return()
88+
endif()
89+
90+
# A Qt Online Installer layout looks like:
91+
# C:/Qt/6.9.0/msvc2022_64/lib/cmake/Qt6
92+
# C:/Qt/6.5.3/mingw_64/lib/cmake/Qt6
93+
# A Homebrew layout looks like:
94+
# /opt/homebrew/opt/qt@6/lib/cmake/Qt6
95+
# A distro layout looks like:
96+
# /usr/lib/x86_64-linux-gnu/cmake/Qt6 (already handled by system pkg)
97+
#
98+
# First, check if root itself is a Qt prefix.
99+
if(EXISTS "${root}/lib/cmake/Qt6/Qt6Config.cmake")
100+
list(APPEND candidates "${root}/lib/cmake/Qt6")
101+
endif()
102+
103+
# Then scan for version subdirectories (Qt Online Installer layout).
104+
file(GLOB _version_dirs RELATIVE "${root}" "${root}/6.*")
105+
# Sort descending so newer versions are tried first.
106+
list(SORT _version_dirs COMPARE NATURAL ORDER DESCENDING)
107+
108+
foreach(vdir IN LISTS _version_dirs)
109+
set(vroot "${root}/${vdir}")
110+
if(NOT IS_DIRECTORY "${vroot}")
111+
continue()
112+
endif()
113+
114+
# Skip versions below the required minimum.
115+
if(min_version AND vdir VERSION_LESS min_version)
116+
continue()
117+
endif()
118+
119+
# Inside a version dir, compiler-specific subdirs hold the actual Qt
120+
# prefix (msvc2022_64, mingw_1200_64, gcc_64, macos, etc.). Prefer
121+
# 64-bit MSVC on Windows, then MinGW; prefer gcc_64 on Linux; prefer
122+
# macos on macOS.
123+
set(_compiler_candidates "")
124+
if(WIN32)
125+
list(APPEND _compiler_candidates
126+
"msvc2022_64" "msvc2019_64" "msvc2022_arm64"
127+
"mingw_1310_64" "mingw_1200_64" "mingw_1120_64" "mingw_64"
128+
"llvm-mingw_64"
129+
)
130+
elseif(APPLE)
131+
list(APPEND _compiler_candidates "macos" "clang_64")
132+
else()
133+
list(APPEND _compiler_candidates "gcc_64" "linux_gcc_64")
134+
endif()
135+
136+
foreach(comp IN LISTS _compiler_candidates)
137+
set(cmake_dir "${vroot}/${comp}/lib/cmake/Qt6")
138+
if(EXISTS "${cmake_dir}/Qt6Config.cmake")
139+
list(APPEND candidates "${cmake_dir}")
140+
endif()
141+
endforeach()
142+
143+
# Fallback: pick whichever compiler subdir exists with a Qt6Config.
144+
file(GLOB _comp_subdirs RELATIVE "${vroot}" "${vroot}/*")
145+
foreach(comp IN LISTS _comp_subdirs)
146+
set(cmake_dir "${vroot}/${comp}/lib/cmake/Qt6")
147+
if(EXISTS "${cmake_dir}/Qt6Config.cmake")
148+
list(APPEND candidates "${cmake_dir}")
149+
endif()
150+
endforeach()
151+
endforeach()
152+
153+
list(REMOVE_DUPLICATES candidates)
154+
set(${out_var} "${candidates}" PARENT_SCOPE)
155+
endfunction()
156+
157+
function(lightweight_probe_qt6)
158+
set(options "")
159+
set(oneValueArgs MIN_VERSION RESULT_VAR)
160+
set(multiValueArgs REQUIRED_COMPONENTS)
161+
cmake_parse_arguments(LW_QT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
162+
163+
if(NOT LW_QT_MIN_VERSION)
164+
set(LW_QT_MIN_VERSION "6.5.0")
165+
endif()
166+
167+
# Fast path: caller already pointed at Qt explicitly.
168+
if(DEFINED CACHE{Qt6_DIR} AND EXISTS "${Qt6_DIR}/Qt6Config.cmake")
169+
message(STATUS "Qt6 already configured via Qt6_DIR=${Qt6_DIR}, skipping probe.")
170+
if(LW_QT_RESULT_VAR)
171+
set(${LW_QT_RESULT_VAR} TRUE PARENT_SCOPE)
172+
endif()
173+
return()
174+
endif()
175+
176+
# Also fast-path if CMAKE_PREFIX_PATH / system package already resolves it.
177+
find_package(Qt6 ${LW_QT_MIN_VERSION} QUIET COMPONENTS Core)
178+
if(Qt6_FOUND)
179+
message(STATUS "Qt6 ${Qt6_VERSION} found via existing CMAKE_PREFIX_PATH / system config.")
180+
if(LW_QT_RESULT_VAR)
181+
set(${LW_QT_RESULT_VAR} TRUE PARENT_SCOPE)
182+
endif()
183+
return()
184+
endif()
185+
186+
_lw_qt_candidate_roots(_roots)
187+
188+
set(_probed "")
189+
set(_picked "")
190+
foreach(root IN LISTS _roots)
191+
_lw_qt_cmake_dirs_under_root("${root}" "${LW_QT_MIN_VERSION}" _dirs)
192+
foreach(d IN LISTS _dirs)
193+
list(APPEND _probed "${d}")
194+
if(NOT _picked)
195+
set(_picked "${d}")
196+
endif()
197+
endforeach()
198+
endforeach()
199+
200+
if(_picked)
201+
get_filename_component(_qt_prefix "${_picked}/../../.." ABSOLUTE)
202+
message(STATUS "Qt6 auto-detected at: ${_qt_prefix}")
203+
list(PREPEND CMAKE_PREFIX_PATH "${_qt_prefix}")
204+
set(CMAKE_PREFIX_PATH "${CMAKE_PREFIX_PATH}" PARENT_SCOPE)
205+
set(Qt6_DIR "${_picked}" CACHE PATH "Qt6 cmake config directory" FORCE)
206+
if(LW_QT_RESULT_VAR)
207+
set(${LW_QT_RESULT_VAR} TRUE PARENT_SCOPE)
208+
endif()
209+
return()
210+
endif()
211+
212+
# Nothing found. Report clearly so the user can fix their install or pass
213+
# -DQt6_DIR=... / -DCMAKE_PREFIX_PATH=... explicitly.
214+
message(STATUS "Qt6 auto-detection failed. Searched roots:")
215+
foreach(r IN LISTS _roots)
216+
message(STATUS " ${r}")
217+
endforeach()
218+
if(LW_QT_RESULT_VAR)
219+
set(${LW_QT_RESULT_VAR} FALSE PARENT_SCOPE)
220+
endif()
221+
endfunction()

0 commit comments

Comments
 (0)