Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3462,3 +3462,38 @@ jobs:
CC: gcc-15
CXX: g++-15
run: bash scripts/check_install_export.sh

install-export-forms-qml:
name: MorphForms QML module consumability
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4

# GCC 15 for the same reason install-export uses it (<print>); the GL and
# xcb libraries are what the offscreen QPA plugin still links against.
- name: Install GCC 15, ninja and the Qt runtime libraries
run: |
sudo apt-get update -q
sudo apt-get install -y software-properties-common
sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test
sudo apt-get update -q
sudo apt-get install -y gcc-15 g++-15 ninja-build \
libgl1-mesa-dev libxkbcommon-x11-0 libxcb-cursor0 libxcb-icccm4 \
libxcb-keysyms1 libxcb-shape0 libxcb-xinerama0

# Not the distro's Qt: MORPH_BUILD_FORMS_QML needs 6.5+ and Ubuntu 24.04
# ships 6.4.2 (see linux-all-features' identical step).
- name: Install Qt ${{ env.QT_VERSION }}
uses: jurplel/install-qt-action@v4.3.1
with:
version: ${{ env.QT_VERSION }}
cache: true

# Install MorphForms to a scratch prefix, then build and *run* an
# application that renders a DynamicForm from it -- plus the vacuity
# guard that the same application without the plugin cannot.
- name: Install MorphForms and render a form from the prefix
env:
CC: gcc-15
CXX: g++-15
run: bash scripts/check_forms_qml_install.sh
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,17 @@ API surface).
`resolve()` takes it as an optional sixth argument, consulted after unit and
before type. See `docs/spec/forms/forms.md`, "Theming / component-override
registry" (fixes #812).
- **The MorphForms QML module installs, as the `forms_qml` component.**
`cmake --install` of a `MORPH_BUILD_FORMS_QML=ON` build installed the
controller-core header and nothing of the QML module, so a packaged morph
(e.g. a vcpkg port) could not be used to render a form. It now installs and
exports `morph::forms_qml` / `morph::forms_qmlplugin` together with the
object libraries a static QML module needs (compiled resources, the plugin's
static initialiser), and `qmldir`/`.qmltypes`/sources under
`MORPH_INSTALL_QMLDIR` (exported as `morph_QML_IMPORT_PATH`). A consumer
links `morph::forms_qmlplugin` and imports `MorphForms`;
`scripts/check_forms_qml_install.sh` (CI `install-export-forms-qml`) builds
and runs one against an install.

- **A locale numeric entry accepts an explicit `+`.**
`morph::render::normalizeLocaleNumber` had no notion of a positive sign: a
Expand Down Expand Up @@ -272,6 +283,15 @@ API surface).

### Fixed

- **An installed `qt_forms` component compiles.** `forms_controller_core.hpp`
includes `morph/qt/qt_executor.hpp`, which was installed only by the `qt`
component (`MORPH_BUILD_QT`, which needs Qt WebSockets), so an install built
with `MORPH_BUILD_FORMS_QML` alone shipped a header that could not be
included: `fatal error C1083: Cannot open include file:
'morph/qt/qt_executor.hpp'`. In-tree builds and the header-set verification
both read the source tree and could not see it. `qt_forms` now ships the
header too; it needs only QtCore.

- **The QML renderer's numeric-entry mirror matched the decimal and group
separators as one UTF-16 code unit while the C++ edge matched whole
strings.** `src/qt/forms/qml/DynamicForm.qml`'s `normalizeLocaleNumber` tested
Expand Down
55 changes: 55 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -514,12 +514,17 @@ if(MORPH_BUILD_FORMS_QML)
# stops being self-contained must fail here, not silently pass (morph#230).
set_target_properties(morph_qt_forms PROPERTIES VERIFY_INTERFACE_HEADER_SETS ON)
target_link_libraries(morph_qt_forms INTERFACE morph Qt6::Core)
# qt_executor.hpp is listed here too, not only under morph::qt: the
# controller core includes it, and morph::qt (MORPH_BUILD_QT, which needs
# Qt WebSockets) is not part of every install that ships this header. It
# needs nothing beyond QtCore.
target_sources(morph_qt_forms
INTERFACE
FILE_SET HEADERS
BASE_DIRS include
FILES
include/morph/qt/forms/forms_controller_core.hpp
include/morph/qt/qt_executor.hpp
)
endif()

Expand Down Expand Up @@ -834,6 +839,55 @@ if(MORPH_INSTALL)
)
endif()

# The MorphForms QML module (MORPH_BUILD_FORMS_QML): its backing library,
# its plugin, and the object libraries a static QML module carries --
# without the latter an installed module links but registers no QML types
# and embeds no .qml files. The qmldir/qmltypes/sources go to a QML import
# directory for tooling (qmllint, qmlls, a consumer's qmlcachegen); at run
# time the module resolves from its embedded resources.
set(MORPH_INSTALL_QMLDIR "${CMAKE_INSTALL_LIBDIR}/qml" CACHE STRING
"Import directory (relative to the prefix) the MorphForms QML module is installed under")
if(TARGET morph_forms_module)
set_target_properties(morph_forms_module PROPERTIES EXPORT_NAME forms_qml)
set_target_properties(morph_forms_moduleplugin PROPERTIES EXPORT_NAME forms_qmlplugin)
# A generated target's name starts with morph_forms_module(plugin)_;
# exporting it under that name would read morph::morph_... downstream.
foreach(_morph_forms_target IN LISTS MORPH_FORMS_QML_OUTPUT_TARGETS)
string(REGEX REPLACE "^morph_" "" _morph_forms_export_name "${_morph_forms_target}")
set_target_properties(${_morph_forms_target} PROPERTIES EXPORT_NAME ${_morph_forms_export_name})
endforeach()
install(TARGETS morph_forms_module morph_forms_moduleplugin ${MORPH_FORMS_QML_OUTPUT_TARGETS}
EXPORT morphTargets
ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}"
OBJECTS DESTINATION "${CMAKE_INSTALL_LIBDIR}"
)
list(APPEND MORPH_INSTALLED_COMPONENTS forms_qml)

get_target_property(_morph_forms_dir morph_forms_module QT_QML_MODULE_OUTPUT_DIRECTORY)
set(_morph_forms_qml_dest "${MORPH_INSTALL_QMLDIR}/MorphForms")
install(FILES "${_morph_forms_dir}/morph_forms_module.qmltypes"
DESTINATION "${_morph_forms_qml_dest}")
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/src/qt/forms/qml/"
DESTINATION "${_morph_forms_qml_dest}/qml"
FILES_MATCHING PATTERN "*.qml" PATTERN "*.js")
# qmldir names the plugin by its build-tree target; a consumer sees the
# exported name, which is what a static-Qt build's QML plugin import
# looks the plugin up by.
install(CODE "
file(READ \"${_morph_forms_dir}/qmldir\" _morph_qmldir)
string(REPLACE \"linktarget morph_forms_moduleplugin\" \"linktarget morph::forms_qmlplugin\"
_morph_qmldir \"\${_morph_qmldir}\")
file(WRITE \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${_morph_forms_qml_dest}/qmldir\" \"\${_morph_qmldir}\")
message(STATUS \"Installing: \$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/${_morph_forms_qml_dest}/qmldir\")
")
unset(_morph_forms_dir)
unset(_morph_forms_qml_dest)
unset(_morph_forms_target)
unset(_morph_forms_export_name)
endif()

install(EXPORT morphTargets
FILE morphTargets.cmake
NAMESPACE morph::
Expand All @@ -844,6 +898,7 @@ if(MORPH_INSTALL)
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/morphConfig.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/morphConfig.cmake"
INSTALL_DESTINATION "${MORPH_CMAKE_INSTALL_DIR}"
PATH_VARS MORPH_INSTALL_QMLDIR
)

# SameMajorVersion follows docs/spec/VERSIONING.md, which is what decides
Expand Down
24 changes: 21 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -461,9 +461,27 @@ target_link_libraries(your_app PRIVATE morph::net morph::offline_sqlite)
```

The components are `net` (`MORPH_BUILD_NET`, POSIX only), `offline_sqlite`
(`MORPH_BUILD_OFFLINE_SQLITE`), `qt` (`MORPH_BUILD_QT`) and `qt_forms`
(`MORPH_BUILD_FORMS_QML`). Asking for one that was not installed fails at
`find_package` and says which.
(`MORPH_BUILD_OFFLINE_SQLITE`), `qt` (`MORPH_BUILD_QT`), and `qt_forms` and
`forms_qml` (both `MORPH_BUILD_FORMS_QML`). Asking for one that was not
installed fails at `find_package` and says which.

`forms_qml` is the `MorphForms` QML module (`DynamicForm`, `SlotRegistry`,
`CollectionView`, …), built as a static QML module. Link its plugin and import
it; `qt_forms` is the header-only `FormsControllerCore` an app's controller
wraps:

```cmake
find_package(morph CONFIG REQUIRED COMPONENTS forms_qml qt_forms)
target_link_libraries(your_app PRIVATE morph::forms_qmlplugin morph::qt_forms)
# For qmllint / qmlls / your own qmlcachegen to see MorphForms' types:
list(APPEND QML_IMPORT_PATH "${morph_QML_IMPORT_PATH}")
```

The module resolves from resources embedded in the plugin, so nothing has to
be deployed next to the application; the installed
`<libdir>/qml/MorphForms` (`MORPH_INSTALL_QMLDIR`) directory is for tooling.
`scripts/check_forms_qml_install.sh` builds and runs such an application
against an install.

**Or vendor the repo.** `add_subdirectory(morph)` or `FetchContent` works and
gives you the same `morph::morph` target. morph's own install rules turn
Expand Down
8 changes: 8 additions & 0 deletions cmake/morphConfig.cmake.in
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ endif()
if("qt_forms" IN_LIST morph_KNOWN_COMPONENTS)
find_dependency(Qt6 6.5 COMPONENTS Core)
endif()
# The MorphForms QML module: morph::forms_qml (backing library) and
# morph::forms_qmlplugin, which is what an application links. The import
# directory holding its qmldir/qmltypes is exported for tooling, e.g.
# list(APPEND QML_IMPORT_PATH "${morph_QML_IMPORT_PATH}").
if("forms_qml" IN_LIST morph_KNOWN_COMPONENTS)
find_dependency(Qt6 6.5 COMPONENTS Core Gui Qml Quick)
set_and_check(morph_QML_IMPORT_PATH "@PACKAGE_MORPH_INSTALL_QMLDIR@")
endif()

if(_morph_added_prefix)
list(REMOVE_ITEM CMAKE_PREFIX_PATH "${PACKAGE_PREFIX_DIR}")
Expand Down
18 changes: 16 additions & 2 deletions docs/spec/forms/forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -1146,8 +1146,22 @@ renderer for it, Qt/QML, as a reusable component rather than example code.
structurally, not by name). It builds whenever `-DMORPH_BUILD_FORMS_QML=ON`,
independent of `MORPH_BUILD_EXAMPLES` — an app depends on it directly
(`target_link_libraries(... morph_forms_moduleplugin)` plus `import
MorphForms` in its own QML) instead of copying or forking it.
- **`include/morph/qt/forms/forms_controller_core.hpp`** ships
MorphForms` in its own QML) instead of copying or forking it. **Installed**,
it is the `forms_qml` component: `find_package(morph CONFIG REQUIRED
COMPONENTS forms_qml qt_forms)` and `morph::forms_qmlplugin`. The install
carries the backing library, the plugin and the object libraries a static
QML module needs (its compiled resources and the plugin's static
initialiser — without them an application links and then finds no
`MorphForms` at run time), plus `qmldir`/`.qmltypes`/sources under
`MORPH_INSTALL_QMLDIR` for tooling, exported as `morph_QML_IMPORT_PATH`. The
installed `qmldir`'s `linktarget` names `morph::forms_qmlplugin`, the name a
static-Qt build's QML plugin import resolves.
`scripts/check_forms_qml_install.sh` (CI job `install-export-forms-qml`)
builds and runs an application against an install, and proves the plugin is
what it measured by running the same application without it.
- **`include/morph/qt/forms/forms_controller_core.hpp`** (component
`qt_forms`, which also ships the `qt_executor.hpp` it includes, so an install
without `MORPH_BUILD_QT` still compiles it) ships
`morph::qt::forms::FormsControllerCore<Model>`, a header-only, model-agnostic
template (no `Q_OBJECT` — Qt cannot register a class *template* for QML) that
owns or composes over the `Bridge`/`BridgeHandler<Model>`/`QtExecutor`
Expand Down
Loading
Loading