~khumba/qtah

Qt bindings for Haskell

136c597 Fix the copyright notice for qtah-cpp/cpp/clean.sh.

~khumba pushed to ~khumba/qtah git

3 months ago

b31ef6e Record the Debian packages to install for Qt 6 API documentation.

~khumba pushed to ~khumba/qtah git

4 months ago

#Qtah - Qt bindings for Haskell

Qtah is a set of Qt bindings for Haskell, providing a traditional imperative interface to a mature GUI toolkit. Qt 5 and Qt 6 are supported. Currently packages are published to Hackage for Qt 5 only.

Homepage: https://khumba.net/projects/qtah

Sourcehut project: https://sr.ht/~khumba/qtah/

Copyright 2015-2024 The Qtah Authors.

A range of successive copyright years may be written as XXXX-YYYY as an abbreviation for listing all of the years from XXXX to YYYY inclusive, individually.

This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

This program 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 Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along with this program. If not, see http://www.gnu.org/licenses/.

#Moved to Sourcehut

As of May 2024, Qtah development has moved from Gitlab to Sourcehut. Further development will continue here:

https://sr.ht/~khumba/qtah/

The Gitlab project will be archived and may be removed at some point in the future:

https://gitlab.com/khumba/qtah

#Table of Contents

  • Repository structure
  • Building locally
    • Dependencies
    • Qt version selection
    • Release versions
  • Using
    • Object lifetimes
  • Developing
    • Code layout
    • Extending the Qt API

#Repository structure

Qtah is split into three separate Cabal packages, qtah-generator, qtah-cpp, and qtah, that are built in order. The first contains a Hoppy generator; the second builds generated C++ code; and the third builds generated Haskell code. On Hackage, qtah-cpp and qtah have -qtX variants for specific major versions of Qt. Currently packages are only published for Qt 5.

Packages that use Qtah should only depend on a qtah-qtX package (or just qtah if building against Qtah git; see "Release versions" below). Executables that use Qtah need to be linked dynamically, so put ghc-options: -dynamic in your Cabal file. This includes unit tests.

#Building locally

The preferred way to build Qtah is with the Cabal "v2" build commands. To build Qtah locally and run the tests:

$ cabal v2-test all

Or to build and run the demo application:

$ cabal v2-run qtah-examples

Currently full builds are needed after changing API definitions in qtah-generator, as our Cabal custom setup machinery doesn't know to regenerate bindings after this kind of change. So when this happens, delete dist-newstyle and build from scratch.

There is also a historical install.sh script in the repository root for doing an old-style cabal build, cabal install build of Qtah, but this script is not actively used or maintained.

#Dependencies

  • Qt 5.x with development files
  • make and a C++ compiler
  • bash 4.1 or newer
  • GHC 8.2+
  • haskell-src
  • hoppy-generator
  • hoppy-runtime
  • hoppy-std
  • HUnit (for tests)
  • mtl

On Debian and derivatives:

apt install g++ make

# For Qt 5:
apt install qtbase5-dev  # Required.
apt install qtbase5-doc qt5-assistant  # For Assistant (Qt API documentation).

# For Qt 6:
apt install qt6-base-dev  # Required.
apt install qt6-base-doc assistant-qt6  # For Assistant (Qt API documentation).

On Fedora and derivatives:

dnf install gcc-c++ make

# For Qt 5:
dnf install qt5-qtbase-devel  # Required.
dnf install qt5-qtbase-doc qt5-qtdoc qt5-assistant  # For Assistant (Qt API documentation).

#Qt version selection

Qtah uses custom Cabal build scripts to tie its pieces together and to Qt. There are a few different ways to control the Qt version; we'll describe them from the lowest-level one up.

The qtah-generator package takes the Qt version to use at runtime. By default, it uses whatever version of Qt is provided by QMake (via qmake -version). If qtchooser is installed on your system, then you can select from multiple versions of Qt with e.g. qmake -qt=5 or QT_SELECT=5 qmake (valid values here are shown by running qtchooser -list-versions). So putting QT_SELECT in the environment when building is one way to select the version of Qt that Qtah will use.

qtah-generator also supports a QTAH_QT environment variable. This takes precedence over QT_SELECT, and can take version numbers of the form x.y or x (for example 5.4 or 5). If given x.y, then Qt x.y will be used, no questions asked. If given x, then QMake will be queried for the version of Qt to use. First qmake -version will be queried, and if this turns out to be a different major version of Qt, then qmake -qt=x -version will be queried (this lets QTAH_QT=x work on systems such as NixOS that activate a single Qt and don't have qtchooser). So putting QTAH_QT in the environment when building is another way to select the version of Qt to use, and unlike QT_SELECT, you can force a specific minor version.

Rather than environment variables, the preferred way of specifying a version is to set the qt5 or qt6 package flags on qtah-cpp and qtah. This is equivalent to setting QTAH_QT but is tracked by Cabal. At most one of these flags may be set, and if QTAH_QT is set as well, then they must agree. When using install.sh, the environment variable QTAH_QT_FLAGS will be passed via --flags to these two packages. This variable defaults to qt5 when unset (but not when empty).

Whether using an environment variable or a flag to specify a Qt version, it needs to be specified for both cabal configure and cabal install.

The path to the QMake executable can be specified in the environment variable QTAH_QMAKE, if Qtah can't find it or you wish to override it (it's used while building qtah-cpp).

#Release versions

Different major versions of Qt can be installed in parallel. To extend this to Haskell, Qtah supports building variant packages for each major Qt version. qtah-cpp and qtah both have -qtX variants, e.g. qtah-cpp-qt5 and qtah-qt5. These are what we upload to Hackage. qtah-generator works for all Qt versions and doesn't need variants. We choose separate package names because while Cabal supports installing multiple versions of a package at once, many other package managers don't.

To create these, run e.g. scripts/set-qt-version 5 before building. set-qt-version renames packages and does source patching as necessary for the variant packages to work with each other. There isn't an undo script and set-qt-version can't be run multiple times in a row; make sure you have local changes committed beforehand, and use Git to reset back to HEAD instead.

When qtah-cpp and qtah have -qtX on the end of their package names, they always use that major version of Qt, regardless of QTAH_QT and QT_SELECT. The qtX package flags are removed by set-qt-version.

#Using

Qtah modules live under Graphics.UI.Qtah. Each Qt class gets its own module, and these are split based on the Qt module they belong to, for example Graphics.UI.Qtah.Core.QObject.

There are some exceptions to this pattern:

  • Graphics.UI.Qtah.Core.Types contains things in the top-level Qt:: namespace. These are mostly enums. Many enums in Qt also support bitwise or on their values in certain contexts, so these types have both a Hoppy enum and a Flags instance defined.

  • Graphics.UI.Qtah.Event contains general event-handling functions. Events are C++ classes in their own right, but also have an Event typeclass instance.

  • Graphics.UI.Qtah.Signal contains functions for working with signals. Signals are represented by Signal objects in the module for the defining class.

  • Templates: Classes such as QList have instantiations to separate types manually. In this case, there is a separate module for each instantiation, e.g. Graphics.UI.Qtah.Core.QList.QObject represents QList<QObject*>.

In each class's module, there are the data types and typeclasses associated with the C++ class, as well as Haskell functions that wrap C++ methods:

  • Constructors start with new. Copy constructors are called newCopy.

  • Casting is provided by cast (upcast to nonconst), castConst (upcast to const), downCast (downcast to nonconst), and downCastConst (downcast to const).

  • encode and decode functions for classes with a native Haskell type.

  • All other methods, enums, etc. provided by the class.

There are many overlapping function names between these modules, so they are meant to be imported qualified, for example:

import qualified Graphics.UI.Qtah.Widgets.QTextEdit as QTextEdit

te <- QTextEdit.new
QTextEdit.setText te "Hello there."

Some types provide native Haskell types for easier manipulation. Naturally, QString maps to Haskell's String. Other types, such as QSize and QRect, come with types that start with H instead (HSize and HRect) in an adjacent module. These make it easier to construct values, since the Haskell version can be passed anywhere a const C++ version is expected.

For working with C++ objects, you will probably also want the functionality in Foreign.Hoppy.Runtime, in the hoppy-runtime package.

#Object lifetimes

Objects returned from constructors are not garbage-collected by default. Most of the time, this is correct, because Qt's object hierarchy ensures that objects get deleted properly. Objects that aren't owned by some other object that will be deleted need to be deleted manually though, e.g. with Hoppy's delete function.

Another option is to let the Haskell garbage collector manage objects, with Hoppy's toGc function. Don't use this for objects that are owned by another object, because they will be deleted twice. Some objects, such as QDir, are returned by-value from functions but don't have a native Haskell type. In these cases, they are assigned to the garbage collector automatically, so that in general, you do not have to manage objects you didn't create explicitly with a constructor call.

#Developing

The Qtah master branch tracks the latest Hoppy release. There may also be a next branch which tracks unreleased Hoppy master.

Patches are welcome! Please enable the pre-commit hook at scripts/git-pre-commit which checks lint and copyright/license issues:

$ ln -s ../../scripts/git-pre-commit .git/hooks/pre-commit

Also please try to fix warnings that your changes introduce, and follow local style, or the style guide.

#Code layout

There is a Hoppy generator in /qtah-generator. Within there, all API definitions are in src/Graphics/UI/Qtah/Generator/Interface. Qtah uses the following prefixes for naming C++ bindings in the generator:

  • c_MyClass for classes.
  • e_MyEnum for enums.
  • fl_MyBitspace for bitspaces.
  • f_MyFunction for functions.
  • cb_MyCallback for callbacks.

Generated bindings end up in /qtah-cpp and /qtah for the C++ and Haskell sides, respectively. For each supported Qt class, Hoppy creates the module Graphics.UI.Qtah.Generated.<module>.<class>. These bindings' names are prefixed with their class name, for example Graphics.UI.Qtah.Generated.Core.QPoint.qPoint_setX. Rather than expose this interface directly, Qtah includes a second generator that creates wrapper modules that are meant to be imported qualified, and leave out the class name from their bindings:

import qualified Graphics.UI.Qtah.Core.QPoint as QPoint

... QPoint.setX ...

These wrapper modules are also where Qtah adds support for signals and events, using the core support for these in Graphics.UI.Qtah.Signal and Graphics.UI.Qtah.Event.

Items in the Qt:: namespace go into Types.hs.

There are some circular dependencies among API definition modules, such as between QString and QChar. In these cases, GHC's circular import support makes this easy to solve: pick one of the modules in the cycle, create a .hs-boot file, and change the dependent module(s) to use import {-# SOURCE #-}.

#Extending the Qt API

#To add a method to an existing class

Declare the method in the class's interface file qtah-generator/src/Graphics/UI/Qtah/Generator/Interface/<module>/<class>.hs.

Check the Qt documentation for mention of when your function was introduced. If you find this out, then add a version check to the method. See for example QSpinBox for how this is done with test and collect.

#To add a class

Create a source file in the generator for your class (say QFoo):

qtah-generator/src/Graphics/UI/Qtah/Generator/Interface/Widgets/QFoo.hs.

Use a similar class as a prototype. Use AQtModule to create declare a module for your class (this encompasses both a Haskell module and a Hoppy module). List the items that the module will export; this includes your class and may include other things such as enums and signals.

You also need to add your new module to the following places:

  • qtah-generator/src/Graphics/UI/Qtah/Generator/Interface/Widgets.hs: This ties your definitions into the generator.

  • qtah-generator/qtah-generator.cabal: Your definitions need to be part of the generator package.

  • qtah/qtah.cabal twice: These are the API that the generator produces. Add Graphics.UI.Qtah.Widgets.QFoo to exposed-modules and Graphics.UI.Qtah.Generated.Widgets.QFoo to other-modules. (These are actually both generated modules.)

Check the Qt documentation for mention of when your class was introduced. If you find this out, then use makeQtModuleWithVersionBounds instead of makeQtModule to put a minimum version bound on the module. See QFormLayout as an example.

#To add an enum or bitspace

These are generally associated with a class. Use makeQtEnum or makeQtEnumAndFlags and include it in the associated class's module. See QMessageBox as an example.

#To add a signal

Declare your signals in a list using makeSignal or makeSignalPrivate, combine them with the class they belong to using makeQtClassAndSignals, and export them from the associated class's module with QtExportClassAndSignals. As an example, see QLineEdit.

Use test/collect as necessary for signals with minimum versions different from their classes'.

Each type of argument list that a signal can use must have associated listener classes and callbacks to provide machinery needed for the signal. For example, the signal QLineEdit::textChanged(QString) uses a listener class c_ListenerQString which uses a callback named cb_QStringVoid. The translation from c_Listener<args> to cb_<args>Void is automatic. Listeners can be added to qtah-listener-gen and callbacks to Callback.hs.