Repository: myokit/myokit (language: python)

Issue:
Syntax highlighting: # in string treated as comment
![highlight-bug](https://user-images.githubusercontent.com/517644/41285567-7fde4f48-6e34-11e8-8d39-7d6268028fb9.png)



Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b528b004e..be1d174ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,10 +4,12 @@ This page lists the main changes made to Myokit in each release.
 
 ## Unreleased
 - Added
+  - [#979](https://github.com/myokit/myokit/pull/979) Added support for PyQt6 and PySide6.
 - Changed
   - [#980](https://github.com/myokit/myokit/pull/980) Python 2 is no longer supported. The minimum Python version is 3.6.
 - Deprecated
 - Removed
+  - [#979](https://github.com/myokit/myokit/pull/979) Removed support for PyQt4 and PySide, which were deprecated since 2019.
 - Fixed
 
 ## [1.34.0] - 2023-06-07
diff --git a/docs/source/api_gui/ide.rst b/docs/source/api_gui/ide.rst
index 2b99d6d7e..65c24ab08 100644
--- a/docs/source/api_gui/ide.rst
+++ b/docs/source/api_gui/ide.rst
@@ -9,7 +9,7 @@ imported from CellML and then modified, exported or used in simulations.
 
 Implementation
 ==============
-The gui is built using Qt, implemented in Python via either PyQt4 or PySide.
+The gui is built using Qt, implemented in Python via either PyQt or PySide.
 
 .. module:: myokit.gui.ide
 
diff --git a/docs/source/cmd/system.rst b/docs/source/cmd/system.rst
index f6ffe0c9a..9b4f52d80 100644
--- a/docs/source/cmd/system.rst
+++ b/docs/source/cmd/system.rst
@@ -193,37 +193,38 @@ Example::
 Example output::
 
     == System information ==
-    Myokit: 1.33.1
-    Python: 3.9.7 (default, Aug 30 2021, 00:00:00)
-            [GCC 11.2.1 20210728 (Red Hat 11.2.1-1)]
+    Myokit: 1.35.0
+    Python: 3.11.3 (main, May 24 2023, 00:00:00) [GCC 12.3.1 20230508 (Red Hat 12.3.1-1)]
+
     OS: Linux (linux, posix)
+        Linux-6.3.5-100.fc37.x86_64-x86_64-with-glibc2.36
 
     == Python requirements ==
-    NumPy: 1.20.1
-    SciPy: 1.6.2
-    Matplotlib: 3.4.3
+    NumPy: 1.24.3
+    SciPy: 1.8.1
+    Matplotlib: 3.6.3
     ConfigParser: OK
-    Setuptools: 53.0.0
+    Setuptools: 62.6.0
 
     == Python extras ==
-    SymPy: 1.8
+    SymPy: 1.11.1
     MoviePy: 1.0.3
 
     == GUI ==
-    PyQt5: 5.15.2
-      Sip: OK
-    PyQt4: 4.8.7
+    PyQt6: 6.5.1
+      Sip: Not found
+    PyQt5: 5.15.9
       Sip: OK
-    PySide: Not found
-    PySide2: 5.15.2
+    PySide6: 6.5.1.1
+    PySide2: 5.15.7
 
     == Development tools ==
-    Sphinx: 3.4.3
-    Flake8: 3.8.4
+    Sphinx: 5.0.2
+    Flake8: 5.0.3
 
     == Simulation tools ==
-    Compiler: GCC 11.2.1
-    Sundials: 5.6.1
+    Compiler: GCC 12.3.1
+    Sundials: 5.8.0
     OpenCL: 2 device(s) found
       Intel(R) Core(TM) i9-10885H CPU @ 2.40GHz on Intel(R) CPU Runtime for OpenCL(TM) Applications
       Intel(R) UHD Graphics [0x9bc4] on Intel(R) OpenCL HD Graphics
@@ -233,41 +234,45 @@ Example output::
     [myokit]
     # This file can be used to set global configuration options for Myokit.
 
+    [compatibility]
+    # Optional settings to make Myokit work on tricky systems.
+    # Don't capture compiler output.
+    #no_capture = True
+    # Don't use the file-descriptor method.
+    #no_fd_capture = True
+
     [time]
     # Date format used throughout Myokit
-    # Format should be acceptable for time.strftime
+    # The format should be acceptable for time.strftime
     date_format = %Y-%m-%d %H:%M:%S
     # Time format used throughout Myokit
     time_format = %H:%M:%S
 
-    [debug]
-    # Add line numbers to debug output of simulations
-    line_numbers = True
-
     [gui]
     # Backend to use for graphical user interface.
-    # Valid options are "pyqt5", "pyqt4" or "pyside".
+    # Valid options are pyqt5, pyqt4, pyside2 and pyside.
     # Leave unset for automatic selection.
     #backend = pyqt5
     #backend = pyqt4
+    #backend = pyside2
     #backend = pyside
 
     [sundials]
-    # Location of sundials shared libary files (.so or .dll).
+    # Location of sundials shared libary files (.so, .dll, or .dylib).
     # Multiple paths can be set using ; as separator.
-    #lib = /usr/local/lib;/opt/local/lib
+    lib = /usr/local/lib;/usr/local/lib64
     # Location of sundials header files (.h).
-    #inc = /usr/local/include;/opt/local/include
-    version = 50300
+    inc = /usr/local/include
 
     [opencl]
-    # Location of opencl shared libary files (.so or .dll).
+    # Location of opencl shared libary files (.so, .dll, .dylib).
     # Multiple paths can be set using ; as separator.
     lib = /usr/lib64;/usr/lib64/nvidia;/usr/local/cuda/lib64
     # Location of opencl header files (.h).
     inc = /usr/include/CL;/usr/local/cuda/include
 
 
+
 .. _cmd/test:
 
 ============
diff --git a/myokit/__init__.py b/myokit/__init__.py
index 3139c7525..872fd64f0 100644
--- a/myokit/__init__.py
+++ b/myokit/__init__.py
@@ -271,9 +271,9 @@
 #
 # GUI: Favor PySide or PyQt
 #
+FORCE_PYQT6 = False
 FORCE_PYQT5 = False
-FORCE_PYQT4 = False
-FORCE_PYSIDE = False
+FORCE_PYSIDE6 = False
 FORCE_PYSIDE2 = False
 
 
diff --git a/myokit/__main__.py b/myokit/__main__.py
index 347231210..f9575048c 100644
--- a/myokit/__main__.py
+++ b/myokit/__main__.py
@@ -26,6 +26,55 @@ def colored(color, text):
     return colors[color] + str(text) + colors['normal']
 
 
+def qtforce(pyqt6=False, pyqt5=False, pyside6=False, pyside2=False):
+    """ Enforce a chosen Qt version. """
+
+    if pyqt6 or pyqt5 or pyside6 or pyside2:
+        import myokit
+
+        myokit.FORCE_PYQT6 = False
+        myokit.FORCE_PYQT5 = False
+        myokit.FORCE_PYSIDE6 = False
+        myokit.FORCE_PYSIDE2 = False
+
+        if pyqt6:
+            myokit.FORCE_PYQT6 = True
+        elif pyside6:
+            myokit.FORCE_PYSIDE6 = True
+        elif pyqt5:
+            myokit.FORCE_PYQT5 = True
+        elif pyside2:
+            myokit.FORCE_PYSIDE2 = True
+
+        import myokit.gui
+        print('Using backend: ' + myokit.gui.backend)
+
+
+def add_qtforce_arguments(parser):
+    """ Updates a parser with arguments to force a Qt version. """
+
+    parser.add_argument(
+        '--pyqt6',
+        action='store_true',
+        help='Run using the PyQt6 backend.',
+    )
+    parser.add_argument(
+        '--pyqt5',
+        action='store_true',
+        help='Run using the PyQt5 backend.',
+    )
+    parser.add_argument(
+        '--pyside6',
+        action='store_true',
+        help='Run using the PySide6 backend.',
+    )
+    parser.add_argument(
+        '--pyside2',
+        action='store_true',
+        help='Run using the PySide2 backend.',
+    )
+
+
 def main():
     """
     Parses command line arguments.
@@ -85,36 +134,13 @@ def main():
 #
 # Data block viewer
 #
-
-def block(filename, pyqt4=False, pyqt5=False, pyside=False, pyside2=False):
+def block(filename, pyqt6=False, pyqt5=False, pyside6=False, pyside2=False):
     """
     Runs the DataBlock viewer.
     """
-    import myokit
-    if pyqt5:
-        myokit.FORCE_PYQT5 = True
-        myokit.FORCE_PYQT4 = False
-        myokit.FORCE_PYSIDE = False
-        myokit.FORCE_PYSIDE2 = False
-    elif pyqt4:
-        myokit.FORCE_PYQT5 = False
-        myokit.FORCE_PYQT4 = True
-        myokit.FORCE_PYSIDE = False
-        myokit.FORCE_PYSIDE2 = False
-    elif pyside:
-        myokit.FORCE_PYQT5 = False
-        myokit.FORCE_PYQT4 = False
-        myokit.FORCE_PYSIDE = True
-        myokit.FORCE_PYSIDE2 = False
-    elif pyside2:
-        myokit.FORCE_PYQT5 = False
-        myokit.FORCE_PYQT4 = False
-        myokit.FORCE_PYSIDE = False
-        myokit.FORCE_PYSIDE2 = True
-    import myokit.gui
+    qtforce(pyqt6, pyqt5, pyside6, pyside2)
+
     import myokit.gui.datablock_viewer
-    if pyqt5 or pyqt4 or pyside or pyside2:
-        print('Using backend: ' + myokit.gui.backend)
     myokit.gui.run(myokit.gui.datablock_viewer.DataBlockViewer, filename)
 
 
@@ -134,26 +160,7 @@ def add_block_parser(subparsers):
         metavar='filename',
         help='The DataBlock zip file to open (optional).',
     )
-    parser.add_argument(
-        '--pyqt5',
-        action='store_true',
-        help='Run the DataBlock Viewer using the PyQt5 backend.',
-    )
-    parser.add_argument(
-        '--pyqt4',
-        action='store_true',
-        help='Run the DataBlock Viewer using the PyQt4 backend.',
-    )
-    parser.add_argument(
-        '--pyside',
-        action='store_true',
-        help='Run the DataBlock Viewer using the PySide backend.',
-    )
-    parser.add_argument(
-        '--pyside2',
-        action='store_true',
-        help='Run the DataBlock Viewer using the PySide2 backend.',
-    )
+    add_qtforce_arguments(parser)
     parser.set_defaults(func=block)
 
 
@@ -578,38 +585,17 @@ def add_icon_parser(subparsers):
 # IDE
 #
 
-def ide(filename, pyqt4=False, pyqt5=False, pyside=False, pyside2=False):
+def ide(filename, pyqt6=False, pyqt5=False, pyside6=False, pyside2=False):
     """
     Runs the Myokit IDE.
     """
+    qtforce(pyqt6, pyqt5, pyside6, pyside2)
+
     import os
-    import myokit
-    if pyqt5:
-        myokit.FORCE_PYQT5 = True
-        myokit.FORCE_PYQT4 = False
-        myokit.FORCE_PYSIDE = False
-        myokit.FORCE_PYSIDE2 = False
-    elif pyqt4:
-        myokit.FORCE_PYQT5 = False
-        myokit.FORCE_PYQT4 = True
-        myokit.FORCE_PYSIDE = False
-        myokit.FORCE_PYSIDE2 = False
-    elif pyside:
-        myokit.FORCE_PYQT5 = False
-        myokit.FORCE_PYQT4 = False
-        myokit.FORCE_PYSIDE = True
-        myokit.FORCE_PYSIDE2 = False
-    elif pyside2:
-        myokit.FORCE_PYQT5 = False
-        myokit.FORCE_PYQT4 = False
-        myokit.FORCE_PYSIDE = False
-        myokit.FORCE_PYSIDE2 = True
-    import myokit.gui
-    import myokit.gui.ide
-    if pyqt5 or pyqt4 or pyside or pyside2:
-        print('Using backend: ' + myokit.gui.backend)
     if filename is not None:
         filename = os.path.abspath(os.path.expanduser(filename))
+
+    import myokit.gui.ide
     myokit.gui.run(myokit.gui.ide.MyokitIDE, filename)
 
 
@@ -629,26 +615,7 @@ def add_ide_parser(subparsers):
         metavar='filename',
         help='The mmt file to open (optional).',
     )
-    parser.add_argument(
-        '--pyqt5',
-        action='store_true',
-        help='Run the IDE using the PyQt5 backend.',
-    )
-    parser.add_argument(
-        '--pyqt4',
-        action='store_true',
-        help='Run the IDE using the PyQt4 backend.',
-    )
-    parser.add_argument(
-        '--pyside',
-        action='store_true',
-        help='Run the IDE using the PySide backend.',
-    )
-    parser.add_argument(
-        '--pyside2',
-        action='store_true',
-        help='Run the DataBlock Viewer using the PySide2 backend.',
-    )
+    add_qtforce_arguments(parser)
     parser.set_defaults(func=ide)
 
 
@@ -732,11 +699,12 @@ def add_import_parser(subparsers):
 # Log viewer
 #
 
-def log(filenames):
+def log(filenames, pyqt6=False, pyqt5=False, pyside6=False, pyside2=False):
     """
     Runs the DataLog Viewer.
     """
-    import myokit.gui
+    qtforce(pyqt6, pyqt5, pyside6, pyside2)
+
     import myokit.gui.datalog_viewer
     myokit.gui.run(myokit.gui.datalog_viewer.DataLogViewer, *filenames)
 
@@ -752,6 +720,7 @@ def add_log_parser(subparsers):
         description='Runs the DataLog Viewer (PROTOTYPE).',
         help='Runs the DataLog Viewer (PROTOTYPE).',
     )
+    add_qtforce_arguments(parser)
     parser.add_argument(
         'filenames',
         default=None,
diff --git a/myokit/_config.py b/myokit/_config.py
index fd63c449e..ca629a74f 100644
--- a/myokit/_config.py
+++ b/myokit/_config.py
@@ -62,12 +62,12 @@ def _create(path):
     # GUI Backend
     config.add_section('gui')
     config.set('gui', '# Backend to use for graphical user interface.')
-    config.set('gui', '# Valid options are pyqt5, pyqt4, pyside2 and pyside.')
+    config.set('gui', '# Valid options are pyqt6, pyqt5, pyside6 and pyside2.')
     config.set('gui', '# Leave unset for automatic selection.')
+    config.set('gui', '#backend = pyqt6')
     config.set('gui', '#backend = pyqt5')
-    config.set('gui', '#backend = pyqt4')
+    config.set('gui', '#backend = pyside6')
     config.set('gui', '#backend = pyside2')
-    config.set('gui', '#backend = pyside')
 
     # Locations of sundials library
     config.add_section('sundials')
@@ -261,30 +261,30 @@ def _load():
     # GUI Backend
     if config.has_option('gui', 'backend'):
         x = config.get('gui', 'backend').strip().lower()
-        if x == 'pyqt' or x == 'pyqt4':
-            myokit.FORCE_PYQT4 = True
+        if x == 'pyqt6':
+            myokit.FORCE_PYQT6 = True
             myokit.FORCE_PYQT5 = False
-            myokit.FORCE_PYSIDE = False
+            myokit.FORCE_PYSIDE6 = False
             myokit.FORCE_PYSIDE2 = False
         elif x == 'pyqt5':
-            myokit.FORCE_PYQT4 = False
+            myokit.FORCE_PYQT6 = False
             myokit.FORCE_PYQT5 = True
-            myokit.FORCE_PYSIDE = False
+            myokit.FORCE_PYSIDE6 = False
             myokit.FORCE_PYSIDE2 = False
-        elif x == 'pyside':
-            myokit.FORCE_PYQT4 = False
+        elif x == 'pyside6':
+            myokit.FORCE_PYQT6 = False
             myokit.FORCE_PYQT5 = False
-            myokit.FORCE_PYSIDE = True
+            myokit.FORCE_PYSIDE6 = True
             myokit.FORCE_PYSIDE2 = False
         elif x == 'pyside2':
-            myokit.FORCE_PYQT4 = False
+            myokit.FORCE_PYQT6 = False
             myokit.FORCE_PYQT5 = False
-            myokit.FORCE_PYSIDE = False
+            myokit.FORCE_PYSIDE6 = False
             myokit.FORCE_PYSIDE2 = True
         elif x != '':
             warnings.warn(
                 'Invalid setting in myokit.ini. Expected values for backend'
-                ' are pyqt, pyqt4, pyqt5, pyside, or pyside2. Got: ' + x)
+                ' are pyqt6, pyqt5, pyside6, or pyside2. Got: ' + x)
 
     # Sundials libraries, header files, and version
     if config.has_option('sundials', 'lib'):
diff --git a/myokit/_system.py b/myokit/_system.py
index d524c995d..9158af849 100644
--- a/myokit/_system.py
+++ b/myokit/_system.py
@@ -65,25 +65,22 @@ def extend(self, xs):
     out.append('== GUI ==')
 
     try:    # pragma: no cover
-        from PyQt5.QtCore import QT_VERSION_STR
-        out.append('PyQt5: ' + QT_VERSION_STR)
+        from PyQt6.QtCore import QT_VERSION_STR
+        out.append('PyQt6: ' + QT_VERSION_STR)
         out.append('  Sip: ' + _module_version('sip'))
         del QT_VERSION_STR
     except ImportError:
-        out.append('PyQt5: Not found')
+        out.append('PyQt6: Not found')
 
     try:    # pragma: no cover
-        from PyQt4.QtCore import QT_VERSION_STR
-        out.append('PyQt4: ' + QT_VERSION_STR)
+        from PyQt5.QtCore import QT_VERSION_STR
+        out.append('PyQt5: ' + QT_VERSION_STR)
         out.append('  Sip: ' + _module_version('sip'))
         del QT_VERSION_STR
     except ImportError:
-        out.append('PyQt4: Not found')
-    except RuntimeError:    # pragma: no cover
-        # Happens if PyQt5 was also found
-        out.append('PyQt4: OK')
+        out.append('PyQt5: Not found')
 
-    out.append('PySide: ' + _module_version('PySide'))
+    out.append('PySide6: ' + _module_version('PySide6'))
     out.append('PySide2: ' + _module_version('PySide2'))
     out.append('')
 
diff --git a/myokit/gui/__init__.py b/myokit/gui/__init__.py
index 328386dea..899cfbd9c 100644
--- a/myokit/gui/__init__.py
+++ b/myokit/gui/__init__.py
@@ -4,7 +4,6 @@
 # This file is part of Myokit.
 # See http://myokit.org for copyright, sharing, and licensing details.
 #
-import logging
 import os
 import platform
 import signal
@@ -16,286 +15,119 @@
 platform = platform.system()
 
 # Select Qt library to use
-pyqt4 = False
 pyqt5 = False
-pyside = False
+pyqt6 = False
 pyside2 = False
+pyside6 = False
 
 # Allow overriding automatic selection
+if myokit.FORCE_PYQT6:
+    pyqt6 = True
 if myokit.FORCE_PYQT5:
     pyqt5 = True
-elif myokit.FORCE_PYQT4:
-    pyqt4 = True
-elif myokit.FORCE_PYSIDE:
-    pyside = True
+elif myokit.FORCE_PYSIDE6:
+    pyside6 = True
 elif myokit.FORCE_PYSIDE2:
     pyside2 = True
 else:
     # Automatic selection
     try:
-        import PyQt5  # noqa
-        pyqt5 = True
+        import PyQt6  # noqa
+        pyqt6 = True
     except ImportError:
         try:
-            import PySide2  # noqa
-            pyside2 = True
+            import PySide6  # noqa
+            pyside6 = True
         except ImportError:
             try:
-                import PyQt4  # noqa
-                pyqt4 = True
+                import PyQt5  # noqa
+                pyqt5 = True
             except ImportError:
                 try:
-                    import PySide  # noqa
-                    pyside = True
+                    import PySide2  # noqa
+                    pyside2 = True
                 except ImportError:
                     raise ImportError(
-                        'Unable to find PyQt5, PyQt4, PySide2 or PySide')
+                        'Unable to find PyQt6, PyQt5, PySide6, or PySide2.')
+
 
 # Import and configure Qt
-if pyqt5:
+if pyqt6:
+    from PyQt6 import QtGui, QtWidgets, QtCore
+    from PyQt6.QtCore import Qt
+
+    # Use PySide signal names
+    QtCore.Signal = QtCore.pyqtSignal
+    QtCore.Slot = QtCore.pyqtSlot
+    QtCore.Property = QtCore.pyqtProperty
 
-    # Load PyQt5
+    # Set backend variables
+    backend = 'PyQt6'
+    qtversion = 6
+
+elif pyqt5:
     from PyQt5 import QtGui, QtWidgets, QtCore
     from PyQt5.QtCore import Qt
 
-    # Fix PyQt naming issues
+    # Mimic PyQt6 API changes
+    # https://doc.qt.io/qt-6/widgets-changes-qt6.html
+    QtGui.QAction = QtWidgets.QAction
+    QtWidgets.QApplication.exec = QtWidgets.QApplication.exec_
+
+    # Use PySide signal names
     QtCore.Signal = QtCore.pyqtSignal
     QtCore.Slot = QtCore.pyqtSlot
     QtCore.Property = QtCore.pyqtProperty
 
-    # Configure Matplotlib for use with PyQt5
-    import matplotlib
-    try:
-        matplotlib.use('Qt5Agg')
-    except ImportError:
-        # In matplotlib 3.7.0 this raises ImportErrors if a previous backend
-        # was already set.
-        pass
-    else:   # pragma: no cover
-        # The method below was deprecated by matplotlib version 2.2, released
-        # in 2018. So probably fine to remove this (and similar) after 2023
-        v = [int(x) for x in matplotlib.__version__.split('.')]
-        if v[0] < 3 and v[1] < 3:
-            matplotlib.rcParams['backend.qt5'] = 'PyQt5'
-    import matplotlib.backends.backend_qt5agg as matplotlib_backend
-
     # Set backend variables
     backend = 'PyQt5'
     qtversion = 5
 
-elif pyside2:
+elif pyside6:
+    from PySide6 import QtGui, QtWidgets, QtCore
+    from PySide6.QtCore import Qt
+
+    # Mimic PyQt6 API changes
+    QtWidgets.QApplication.exec = QtWidgets.QApplication.exec_
+
+    # Set backend variables
+    backend = 'PySide6'
+    qtversion = 6
 
-    # Load PySide2
-    # Load main classes
+elif pyside2:
     from PySide2 import QtGui, QtWidgets, QtCore
     from PySide2.QtCore import Qt
 
-    # Configure Matplotlib for use with PySide2
-    import matplotlib
-    try:
-        matplotlib.use('Qt5Agg')
-    except ImportError:
-        # In matplotlib 3.3.0 this raises ImportErrors if a previous backend
-        # was already set.
-        pass
-    else:   # pragma: no cover
-        v = [int(x) for x in matplotlib.__version__.split('.')]
-        if v[0] < 3 and v[1] < 3:
-            matplotlib.rcParams['backend.qt5'] = 'PySide2'
-    import matplotlib.backends.backend_qt5agg as matplotlib_backend  # noqa
+    # Mimic PyQt6 API changes
+    QtGui.QAction = QtWidgets.QAction
+    QtWidgets.QApplication.exec = QtWidgets.QApplication.exec_
 
     # Set backend variables
     backend = 'PySide2'
     qtversion = 5
 
-elif pyqt4:
-
-    # Load PyQt4
-
-    # Deprecated since 2019-09-11
-    logger = logging.getLogger('myokit')
-    logger.warning(
-        'PyQt4 support has been deprecated. Please upgrade to PyQt5 or'
-        ' Pyside2.'
-    )
-
-    # Set PyQt to "API 2"
-    import sip
-    sip.setapi('QString', 2)
-    sip.setapi('QVariant', 2)
-    sip.setapi('QDate', 2)
-    sip.setapi('QDateTime', 2)
-    sip.setapi('QTextStream', 2)
-    sip.setapi('QTime', 2)
-    sip.setapi('QUrl', 2)
-
-    # Load main classes
-    from PyQt4 import QtGui, QtCore
-    from PyQt4.QtCore import Qt
-
-    # Qt5 compatibility
-    QtWidgets = QtGui
-    QtCore.QItemSelection = QtGui.QItemSelection
-    QtCore.QItemSelectionModel = QtGui.QItemSelectionModel
-    QtCore.QItemSelectionRange = QtGui.QItemSelectionRange
-    QtCore.QSortFilterProxyModel = QtGui.QSortFilterProxyModel
-    QtWidgets.QStyleOptionViewItem = QtWidgets.QStyleOptionViewItemV4
-
-    # Fix Qt4 location issue
-    import PyQt4.Qt
-    QtGui.QKeySequence = PyQt4.Qt.QKeySequence
-    QtGui.QTextCursor = PyQt4.Qt.QTextCursor
-    del PyQt4.Qt
-
-    # Fix PyQt4 naming issues
-    QtCore.Signal = QtCore.pyqtSignal
-    QtCore.Slot = QtCore.pyqtSlot
-    QtCore.Property = QtCore.pyqtProperty
-
-    # Fix QFileDialog.getOpenFileName return type issues:
-    # Return type in PyQt4 is not a tuple (call ...AndFilter methods instead)
-    # Fix getOpenFileName
-    def gofn(parent=None, caption='', directory='', filter='',
-             initialFilter='', options=0):
-        if options == 0:
-            options = QtWidgets.QFileDialog.Options()
-        return QtWidgets.QFileDialog.getOpenFileNameAndFilter(
-            parent, caption, directory, filter, initialFilter, options)
-    QtWidgets.QFileDialog.getOpenFileName = staticmethod(gofn)
-    del gofn
-
-    # Fix getOpenFileNames
-    def gofns(parent=None, caption='', directory='', filter='',
-              initialFilter='', options=0):
-        if options == 0:
-            options = QtWidgets.QFileDialog.Options()
-        return QtWidgets.QFileDialog.getOpenFileNamesAndFilter(
-            parent, caption, directory, filter, initialFilter, options)
-    QtWidgets.QFileDialog.getOpenFileNames = staticmethod(gofns)
-    del gofns
-
-    # Fix getSaveFileName
-    def gsfn(parent=None, caption='', directory='', filter='',
-             initialFilter='', options=0):
-        if options == 0:
-            options = QtWidgets.QFileDialog.Options()
-        return QtWidgets.QFileDialog.getSaveFileNameAndFilter(
-            parent, caption, directory, filter, initialFilter, options)
-    QtWidgets.QFileDialog.getSaveFileName = staticmethod(gsfn)
-    del gsfn
-
-    # Configure Matplotlib for use with PyQt4
-    import matplotlib
-    try:
-        matplotlib.use('Qt4Agg')
-    except ImportError:
-        # In matplotlib 3.3.0 this raises ImportErrors if a previous backend
-        # was already set.
-        pass
-    else:   # pragma: no cover
-        v = [int(x) for x in matplotlib.__version__.split('.')]
-        if v[0] < 3 and v[1] < 3:
-            matplotlib.rcParams['backend.qt4'] = 'PyQt4'
-    import matplotlib.backends.backend_qt4agg as matplotlib_backend
-
-    # Set backend variables
-    backend = 'PyQt4'
-    qtversion = 4
-
-elif pyside:
-
-    # Load PySide
-
-    # Deprecated since 2019-09-11
-    logger = logging.getLogger('myokit')
-    logger.warning(
-        'PySide support has been deprecated. Please upgrade to PyQt5 or'
-        ' Pyside2.'
-    )
-
-    # Load main classes
-    from PySide import QtGui, QtCore
-    from PySide.QtCore import Qt
-
-    # Qt5 compatibility
-    QtWidgets = QtGui
-    QtCore.QItemSelection = QtGui.QItemSelection
-    QtCore.QItemSelectionModel = QtGui.QItemSelectionModel
-    QtCore.QItemSelectionRange = QtGui.QItemSelectionRange
-    QtCore.QSortFilterProxyModel = QtGui.QSortFilterProxyModel
-    QtWidgets.QStyleOptionViewItem = QtWidgets.QStyleOptionViewItemV4
-
-    # Fix QFileDialog.getOpenFileName signature issues (is different in PySide,
-    #  which causes issues when using keyword arguments. This is fixed simply
-    #  by simply wrapping the methods.)
-    # Signature in PySide
-    #  parent
-    #  caption
-    #  dir --> directory PyQt4/5
-    #  filter
-    #  selectedFilter --> initialFilter in PyQt4/5
-    #  options
-
-    # Fix getOpenFileName
-    gofn_org = QtWidgets.QFileDialog.getOpenFileName
-
-    def gofn(parent=None, caption='', directory='', filter='',
-             initialFilter='', options=0):
-        if options == 0:
-            options = QtWidgets.QFileDialog.Options()
-        return gofn_org(
-            parent, caption, directory, filter, initialFilter, options)
-    QtWidgets.QFileDialog.getOpenFileName = staticmethod(gofn)
-    del gofn
-
-    # Fix getOpenFileNames
-    gofns_org = QtWidgets.QFileDialog.getOpenFileNames
-
-    def gofns(parent=None, caption='', directory='', filter='',
-              initialFilter='', options=0):
-        if options == 0:
-            options = QtWidgets.QFileDialog.Options()
-        return gofns_org(
-            parent, caption, directory, filter, initialFilter, options)
-    QtWidgets.QFileDialog.getOpenFileNames = staticmethod(gofns)
-    del gofns
-
-    # Fix getSaveFileName
-    gsfn_org = QtWidgets.QFileDialog.getSaveFileName
-
-    def gsfn(parent=None, caption='', directory='', filter='',
-             initialFilter='', options=0):
-        if options == 0:
-            options = QtWidgets.QFileDialog.Options()
-        return gsfn_org(
-            parent, caption, directory, filter, initialFilter, options)
-    QtWidgets.QFileDialog.getSaveFileName = staticmethod(gsfn)
-    del gsfn
-
-    # Configure Matplotlib for use with PySide
-    import matplotlib
-    try:
-        matplotlib.use('Qt4Agg')
-    except ImportError:
-        # In matplotlib 3.3.0 this raises ImportErrors if a previous backend
-        # was already set.
-        pass
-    else:   # pragma: no cover
-        v = [int(x) for x in matplotlib.__version__.split('.')]
-        if v[0] < 3 and v[1] < 3:
-            matplotlib.rcParams['backend.qt4'] = 'PySide'
-    import matplotlib.backends.backend_qt4agg as matplotlib_backend  # noqa
-
-    # Set backend variables
-    backend = 'PySide'
-    qtversion = 4
-
 else:
-
     raise Exception('Selection of qt version failed.')
 
+
+# Configure Matplotlib for use with Qt
+import matplotlib  # noqa
+try:
+    matplotlib.use('QtAgg')
+except ImportError:
+    # In matplotlib 3.7.0 this raises ImportErrors if a previous backend
+    # was already set.
+    pass
+try:
+    # New Matplotlib no longer has qt4agg/qt5agg, but now uses qtagg for all
+    import matplotlib.backends.back_qtagg as matplotlib_backend  # noqa
+except ImportError:
+    # https://matplotlib.org/stable/api/backend_qt_api.html
+    import matplotlib.backends.backend_qt5agg as matplotlib_backend  # noqa
+
 # Delete temporary variables
-del pyqt4, pyqt5, pyside
+del pyqt5, pyqt6, pyside2, pyside6
+
 
 # Load Gnome theme on Wayland (for icons)
 if platform == 'Linux':
@@ -318,11 +150,11 @@ def gsfn(parent=None, caption='', directory='', filter='',
     ICONS[k] = os.path.join(ICON_PATH, v)
 
 # Toolbar style suitable for platform
-TOOL_BUTTON_STYLE = Qt.ToolButtonTextUnderIcon
+TOOL_BUTTON_STYLE = Qt.ToolButtonStyle.ToolButtonTextUnderIcon
 if platform == 'Windows':   # pragma: no linux cover
-    TOOL_BUTTON_STYLE = Qt.ToolButtonIconOnly
+    TOOL_BUTTON_STYLE = Qt.ToolButtonStyle.ToolButtonIconOnly
 elif platform == 'Darwin':  # pragma: no linux cover
-    TOOL_BUTTON_STYLE = Qt.ToolButtonTextOnly
+    TOOL_BUTTON_STYLE = Qt.ToolButtonStyle.ToolButtonTextOnly
 
 
 # Stand alone applications
@@ -350,10 +182,11 @@ def qtMonospaceFont():
     """
     font = QtGui.QFont('monospace')
     if platform == 'Windows':   # pragma: no linux cover
-        font.setStyleHint(QtGui.QFont.TypeWriter)
+        font.setStyleHint(QtGui.QFont.StyleHint.TypeWriter)
     else:
-        font.setStyleHint(QtGui.QFont.Monospace)
-    font.setHintingPreference(QtGui.QFont.PreferVerticalHinting)  # Qt5
+        font.setStyleHint(QtGui.QFont.StyleHint.Monospace)
+    font.setHintingPreference(
+        QtGui.QFont.HintingPreference.PreferVerticalHinting)
     return font
 
 
@@ -403,5 +236,5 @@ def int_signal(signum, frame):
     timer.timeout.connect(lambda: None)
 
     # Wait for app to exit
-    sys.exit(a.exec_())
+    sys.exit(a.exec())
 
diff --git a/myokit/gui/datablock_viewer.py b/myokit/gui/datablock_viewer.py
index 4e705a0a7..84d24c1d9 100644
--- a/myokit/gui/datablock_viewer.py
+++ b/myokit/gui/datablock_viewer.py
@@ -89,19 +89,23 @@ def __init__(self, filename=None):
         super().__init__()
         # Set application icon
         self.setWindowIcon(icon())
+
         # Set size, center
         self.resize(800, 600)
         self.setMinimumSize(600, 400)
         qr = self.frameGeometry()
-        cp = QtWidgets.QDesktopWidget().availableGeometry().center()
+        cp = QtGui.QGuiApplication.primaryScreen().availableGeometry().center()
         qr.moveCenter(cp)
         self.move(qr.topLeft())
+
         # Status bar
         self._label_cursor = QtWidgets.QLabel()
         self.statusBar().addPermanentWidget(self._label_cursor)
         self.statusBar().showMessage('Ready')
+
         # Menu bar
         self.create_menu()
+
         # Timer
         self._timer_interval = 50
         self._timer = QtCore.QTimer(self)
@@ -109,10 +113,8 @@ def __init__(self, filename=None):
         self._timer.setSingleShot(False)
         self._timer.timeout.connect(self.action_next_frame)
         self._timer_paused = False
-        try:
-            self._timer.setTimerType(Qt.PreciseTimer)
-        except AttributeError:
-            pass    # Qt4 uses precise timer by default
+        self._timer.setTimerType(Qt.TimerType.PreciseTimer)
+
         # Video widget
         self._video_scene = VideoScene()
         self._video_scene.mouse_moved.connect(self.event_video_mouse_move)
@@ -120,12 +122,13 @@ def __init__(self, filename=None):
         self._video_scene.double_click.connect(self.event_video_double_click)
         self._video_view = VideoView(self._video_scene)
         self._video_view.setMouseTracking(True)
-        self._video_view.setCursor(Qt.CrossCursor)
+        self._video_view.setCursor(Qt.CursorShape.CrossCursor)
         #self._video_view.setViewport(QtOpenGL.QGLWidget())
         self._video_pixmap = None
         self._video_item = None
         self._video_frames = None
         self._video_iframe = None
+
         # Colorbar widget
         self._colorbar_width = 20
         self._colorbar_height = 256
@@ -134,52 +137,65 @@ def __init__(self, filename=None):
         self._colorbar_view.setMaximumWidth(self._colorbar_width)
         self._colorbar_pixmap = None
         self._colorbar_item = None
+
         # Video slider
-        self._slider = QtWidgets.QSlider(Qt.Horizontal)
-        self._slider.setTickPosition(QtWidgets.QSlider.NoTicks)
-        #self._slider.setTickPosition(QtWidgets.QSlider.TicksBothSides)
+        self._slider = QtWidgets.QSlider(Qt.Orientation.Horizontal)
+        self._slider.setTickPosition(QtWidgets.QSlider.TickPosition.NoTicks)
         self._slider.setSingleStep(1)
         self._slider.setMinimum(0)
         self._slider.setMaximum(0)
         self._slider.sliderPressed.connect(self.action_pause_timer)
         self._slider.sliderReleased.connect(self.action_depause_timer)
         self._slider.valueChanged.connect(self.action_set_frame)
+
         # Controls
         style = QtWidgets.QApplication.style()
+
         # Play button
-        self._play_icon_play = style.standardIcon(style.SP_MediaPlay)
-        self._play_icon_pause = style.standardIcon(style.SP_MediaPause)
+        self._play_icon_play = style.standardIcon(
+            style.StandardPixmap.SP_MediaPlay)
+        self._play_icon_pause = style.standardIcon(
+            style.StandardPixmap.SP_MediaPause)
         self._play_button = QtWidgets.QPushButton()
         self._play_button.setIcon(self._play_icon_play)
         self._play_button.pressed.connect(self.action_start_stop)
+
         # Frame indicator
         self._frame_label = QtWidgets.QLabel('Frame')
         self._frame_field = QtWidgets.QLineEdit('0')
         self._frame_field.setReadOnly(True)
         self._frame_field.setMaxLength(6)
         self._frame_field.setMaximumWidth(100)
-        self._frame_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
+        self._frame_label.setAlignment(
+            Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
+
         # Time indicator
         self._time_label = QtWidgets.QLabel('Time')
         self._time_field = QtWidgets.QLineEdit('0')
         self._time_field.setReadOnly(True)
         self._time_field.setMaxLength(6)
         self._time_field.setMaximumWidth(100)
-        self._time_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
+        self._time_label.setAlignment(
+            Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
+
         # Speed indicator
         self._rate_label = QtWidgets.QLabel('Delay')
         self._rate_field = QtWidgets.QLineEdit(str(self._timer_interval))
         self._rate_field.setValidator(QtGui.QIntValidator(1, 2**20, self))
         self._rate_field.editingFinished.connect(self.event_rate_changed)
         self._rate_field.setMaximumWidth(100)
-        self._rate_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
+        self._rate_label.setAlignment(
+            Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
+
         # Graph controls
         self._graph_clear_button = QtWidgets.QPushButton('Clear graphs')
         self._graph_clear_button.pressed.connect(self.action_clear_graphs)
+
         # Variable selection
         self._variable_select = QtWidgets.QComboBox()
         self._variable_select.activated.connect(self.event_variable_selected)
         self._variable_select.setMinimumWidth(120)
+
         # Colormap selection
         self._colormap = next(iter(myokit.ColorMap.names()))
         self._colormap_select = QtWidgets.QComboBox()
@@ -187,6 +203,7 @@ def __init__(self, filename=None):
             self._colormap_select.addItem(cmap)
         self._colormap_select.activated.connect(self.event_colormap_selected)
         self._colormap_select.setMinimumWidth(120)
+
         # Control layout
         self._control_layout = QtWidgets.QHBoxLayout()
         self._control_layout.addWidget(self._play_button)
@@ -199,15 +216,18 @@ def __init__(self, filename=None):
         self._control_layout.addWidget(self._graph_clear_button)
         self._control_layout.addWidget(self._variable_select)
         self._control_layout.addWidget(self._colormap_select)
+
         # Graph area
         self._graph_area = GraphArea()
         self._graph_area.mouse_moved.connect(self.event_graph_mouse_move)
         self._graph_area.setMouseTracking(True)
-        self._graph_area.setCursor(Qt.CrossCursor)
+        self._graph_area.setCursor(Qt.CursorShape.CrossCursor)
+
         # Video and colorbar layout
         self._video_plus_layout = QtWidgets.QHBoxLayout()
         self._video_plus_layout.addWidget(self._video_view)
         self._video_plus_layout.addWidget(self._colorbar_view)
+
         # Video Layout
         self._video_layout = QtWidgets.QVBoxLayout()
         self._video_layout.addLayout(self._video_plus_layout)
@@ -215,34 +235,42 @@ def __init__(self, filename=None):
         self._video_layout.addLayout(self._control_layout)
         self._video_widget = QtWidgets.QWidget()
         self._video_widget.setLayout(self._video_layout)
+
         # Central layout
-        self._central_widget = QtWidgets.QSplitter(Qt.Vertical)
+        self._central_widget = QtWidgets.QSplitter(Qt.Orientation.Vertical)
         self._central_widget.addWidget(self._video_widget)
         self._central_widget.addWidget(self._graph_area)
         self.setCentralWidget(self._central_widget)
+
         # Current path, current file, recent files
         self._path = QtCore.QDir.currentPath()
         self._file = None
         self._recent_files = []
+
         # Current data block, display variable
         self._data = None
         self._variable = None
+
         # Load settings from ini file
         self.load_config()
         self.update_window_title()
+
         # Set controls to correct values
         self._colormap_select.setCurrentIndex(self._colormap_select.findText(
             self._colormap))
         self._rate_field.setText(str(self._timer_interval))
         self._timer.setInterval(self._timer_interval)
+
         # Pause video playback during resize
         self._resize_timer = QtCore.QTimer()
         self._resize_timer.timeout.connect(self._resize_timeout)
         self._video_view.resize_event.connect(self._resize_started)
         self._colorbar_view.resize_event.connect(self._resize_started)
+
         # Attempt to load selected file
         if filename and os.path.isfile(filename):
             self.load_data_file(filename)
+
         # Focus on play button
         self._play_button.setFocus()
 
@@ -300,7 +328,8 @@ def action_extract_colormap_image(self):
             nx = 200
             ny = 800
             image = myokit.ColorMap.image(self._colormap, nx, ny)
-            image = QtGui.QImage(image, nx, ny, QtGui.QImage.Format_ARGB32)
+            image = QtGui.QImage(
+                image, nx, ny, QtGui.QImage.Format.Format_ARGB32)
             painter = QtGui.QPainter(image)
             painter.drawText(10, 15, str(upper))
             painter.drawText(10, ny - 5, str(lower))
@@ -354,7 +383,8 @@ def action_extract_frame_image(self):
                 return
             nt, ny, nx = self._data.shape()
             image = self._video_frames[self._video_iframe]
-            image = QtGui.QImage(image, nx, ny, QtGui.QImage.Format_ARGB32)
+            image = QtGui.QImage(
+                image, nx, ny, QtGui.QImage.Format.Format_ARGB32)
             image.save(fname, ext)
 
     def action_extract_graphs(self):
@@ -458,7 +488,8 @@ def action_set_colormap(self, name):
             nx = self._colorbar_width
             ny = self._colorbar_height
             image = myokit.ColorMap.image(self._colormap, nx, ny)
-            image = QtGui.QImage(image, nx, ny, QtGui.QImage.Format_ARGB32)
+            image = QtGui.QImage(
+                image, nx, ny, QtGui.QImage.Format.Format_ARGB32)
             self._colorbar_pixmap.convertFromImage(image)
             self._colorbar_item.setPixmap(self._colorbar_pixmap)  # qt5
             self.action_set_variable(self._variable)
@@ -498,7 +529,8 @@ def action_set_frame(self, frame):
             self._time_field.setText(str(self._data.time()[frame]))
             # Update scene
             image = self._video_frames[self._video_iframe]
-            image = QtGui.QImage(image, nx, ny, QtGui.QImage.Format_ARGB32)
+            image = QtGui.QImage(
+                image, nx, ny, QtGui.QImage.Format.Format_ARGB32)
             self._video_pixmap.convertFromImage(image)
             self._video_item.setPixmap(self._video_pixmap)   # qt5
         # Update graph area
@@ -534,7 +566,7 @@ def create_menu(self):
         # File menu
         self._menu_file = self._menu.addMenu('&File')
         # File > Open
-        self._tool_open = QtWidgets.QAction('&Open', self)
+        self._tool_open = QtGui.QAction('&Open', self)
         self._tool_open.setShortcut('Ctrl+O')
         self._tool_open.setStatusTip('Open a DataLog for inspection.')
         self._tool_open.setIcon(QtGui.QIcon.fromTheme('document-open'))
@@ -545,14 +577,14 @@ def create_menu(self):
         # File > Recent files
         self._recent_file_tools = []
         for i in range(N_RECENT_FILES):
-            tool = QtWidgets.QAction(self, visible=False)
+            tool = QtGui.QAction(self, visible=False)
             tool.triggered.connect(self.action_recent_file)
             self._recent_file_tools.append(tool)
             self._menu_file.addAction(tool)
         # File > ----
         self._menu_file.addSeparator()
         # File > Quit
-        self._tool_exit = QtWidgets.QAction('&Quit', self)
+        self._tool_exit = QtGui.QAction('&Quit', self)
         self._tool_exit.setShortcut('Ctrl+Q')
         self._tool_exit.setStatusTip('Exit application.')
         self._tool_exit.setIcon(QtGui.QIcon.fromTheme('application-exit'))
@@ -561,27 +593,26 @@ def create_menu(self):
         # Data menu
         self._menu_data = self._menu.addMenu('&Data')
         # Data > Extract frame
-        self._tool_exframe = QtWidgets.QAction('Extract &frame', self)
+        self._tool_exframe = QtGui.QAction('Extract &frame', self)
         self._tool_exframe.setStatusTip(
             'Extract the current frame as csv file.')
         self._tool_exframe.triggered.connect(self.action_extract_frame)
         self._menu_data.addAction(self._tool_exframe)
         # Data > Extract graphs
-        self._tool_exgraph = QtWidgets.QAction('Extract &graphs', self)
+        self._tool_exgraph = QtGui.QAction('Extract &graphs', self)
         self._tool_exgraph.setStatusTip('Extract the current graphs.')
         self._tool_exgraph.triggered.connect(self.action_extract_graphs)
         self._menu_data.addAction(self._tool_exgraph)
         # Data > ----
         self._menu_data.addSeparator()
         # Data > Extract frame as image
-        self._tool_imgframe = QtWidgets.QAction('Save frame as &image', self)
+        self._tool_imgframe = QtGui.QAction('Save frame as &image', self)
         self._tool_imgframe.setStatusTip(
             'Save the current frame as an image file.')
         self._tool_imgframe.triggered.connect(self.action_extract_frame_image)
         self._menu_data.addAction(self._tool_imgframe)
         # Data > Extract colormap as image
-        self._tool_imgcolor = QtWidgets.QAction(
-            'Save &colormap as image', self)
+        self._tool_imgcolor = QtGui.QAction('Save &colormap as image', self)
         self._tool_imgcolor.setStatusTip('Save the colormap as an image file.')
         self._tool_imgcolor.triggered.connect(
             self.action_extract_colormap_image)
@@ -589,11 +620,11 @@ def create_menu(self):
         # Help menu
         self._menu_help = self._menu.addMenu('&Help')
         # Help > About
-        self._tool_about = QtWidgets.QAction('&About', self)
+        self._tool_about = QtGui.QAction('&About', self)
         self._tool_about.setStatusTip('View information about this program.')
         self._tool_about.triggered.connect(self.action_about)
         self._menu_help.addAction(self._tool_about)
-        self._tool_license = QtWidgets.QAction('&License', self)
+        self._tool_license = QtGui.QAction('&License', self)
         self._tool_license.setStatusTip('View this program\'s license info.')
         self._tool_license.triggered.connect(self.action_license)
         self._menu_help.addAction(self._tool_license)
@@ -671,7 +702,7 @@ def keyPressEvent(self, e):
         """
         Catch key presses?
         """
-        if e.key() == Qt.Key_Space:
+        if e.key() == Qt.Key.Key_Space:
             self.action_start_stop()
 
     def load_config(self):
@@ -745,7 +776,7 @@ def load_data_file(self, fname):
         self.statusBar().showMessage('Loading ' + str(fname))
         n = 1000000
         pd = QtWidgets.QProgressDialog('Loading data file...', 'Cancel', 0, n)
-        pd.setWindowModality(Qt.WindowModal)
+        pd.setWindowModality(Qt.WindowModality.WindowModal)
         pd.setValue(0)
 
         class Reporter(myokit.ProgressReporter):
@@ -956,8 +987,8 @@ def mousePressEvent(self, event):
         """
         Single-click
         """
-        if event.button() == QtCore.Qt.LeftButton:
-            if event.modifiers() == Qt.NoModifier:
+        if event.button() == Qt.MouseButton.LeftButton:
+            if event.modifiers() == Qt.KeyBoardModifier.NoModifier:
                 p = event.scenePos()
                 x, y = int(p.x()), int(p.y())
                 if x >= 0 and x < self._w and y >= 0 and y < self._h:
@@ -968,8 +999,8 @@ def mouseDoubleClickEvent(self, event):
         """
         Double-click
         """
-        if event.button() == QtCore.Qt.LeftButton:
-            if event.modifiers() == Qt.NoModifier:
+        if event.button() == Qt.MouseButton.LeftButton:
+            if event.modifiers() == Qt.KeyBoardModifier.NoModifier:
                 p = event.scenePos()
                 x, y = int(p.x()), int(p.y())
                 if x >= 0 and x < self._w and y >= 0 and y < self._h:
@@ -1004,17 +1035,18 @@ class VideoView(QtWidgets.QGraphicsView):
     def __init__(self, scene):
         super().__init__(scene)
         # Disable scrollbars
-        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
-        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+        self.setHorizontalScrollBarPolicy(
+            Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
+        self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
 
         # Set rendering hints
-        self.setRenderHint(QtGui.QPainter.Antialiasing)
+        self.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
         self.setViewportUpdateMode(
-            QtWidgets.QGraphicsView.BoundingRectViewportUpdate)
+            QtWidgets.QGraphicsView.ViewportUpdateMode.BoundingRectViewportUpdate)  # noqa
 
         # Fit scene rect in view
         self.fitInView(self.sceneRect(), keepAspect=True)
-        self.setAlignment(Qt.AlignCenter)
+        self.setAlignment(Qt.AlignmentFlag.AlignCenter)
 
         # Delayed resizing
         self._resize_timer = QtCore.QTimer()
@@ -1032,22 +1064,13 @@ def fitInView(self, rect, keepAspect=False):
         https://bugreports.qt-project.org/browse/QTBUG-11945
         """
         # Reset the view scale
-        try:
-            # PyQt5 / PySide
-            unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1))
-        except AttributeError:
-            # PyQt4
-            unity = self.matrix().mapRect(QtCore.QRectF(0, 0, 1, 1))
+        unity = self.transform().mapRect(QtCore.QRectF(0, 0, 1, 1))
         w, h = max(1, unity.width()), max(1, unity.height())
         self.scale(1. / w, 1 / h)
 
         # Find the ideal scaling ratio
         viewRect = self.viewport().rect()
-        try:
-            sceneRect = self.transform().mapRect(rect)
-        except AttributeError:
-            # PyQt4
-            sceneRect = self.matrix().mapRect(rect)
+        sceneRect = self.transform().mapRect(rect)
         xr = viewRect.width() / sceneRect.width()
         yr = viewRect.height() / sceneRect.height()
         if keepAspect:
@@ -1114,20 +1137,20 @@ def __init__(self):
         self._position = self._tmin
 
         # Colors for drawing
-        self._color_temp = Qt.black
+        self._color_temp = Qt.GlobalColor.black
         self._color_cycle = [
-            Qt.red,
-            #Qt.green,
-            Qt.blue,
-            #Qt.cyan,
-            Qt.magenta,
-            #Qt.yellow,
-            Qt.darkRed,
-            Qt.darkGreen,
-            Qt.darkBlue,
-            Qt.darkCyan,
-            Qt.darkMagenta,
-            Qt.darkYellow,
+            Qt.GlobalColor.red,
+            #Qt.GlobalColor.green,
+            Qt.GlobalColor.blue,
+            #Qt.GlobalColor.cyan,
+            Qt.GlobalColor.magenta,
+            #Qt.GlobalColor.yellow,
+            Qt.GlobalColor.darkRed,
+            Qt.GlobalColor.darkGreen,
+            Qt.GlobalColor.darkBlue,
+            Qt.GlobalColor.darkCyan,
+            Qt.GlobalColor.darkMagenta,
+            Qt.GlobalColor.darkYellow,
         ]
 
         # Scaling factors from pixels to normalized (0, 1) coordinates. Updated
@@ -1259,11 +1282,11 @@ def paintEvent(self, event):
         painter.begin(self)
 
         # Fill background
-        painter.fillRect(self.rect(), QtGui.QBrush(Qt.white))
+        painter.fillRect(self.rect(), QtGui.QBrush(Qt.GlobalColor.white))
 
         # Create coordinate system for graphs
         painter.scale(self.width(), self.height())
-        painter.setRenderHint(QtGui.QPainter.Antialiasing)
+        painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing)
 
         # Create pen
         pen = QtGui.QPen()
@@ -1287,7 +1310,7 @@ def paintEvent(self, event):
             painter.drawPath(self._temp_path)
 
         # Show time indicator
-        pen.setColor(Qt.red)
+        pen.setColor(Qt.GlobalColor.red)
         painter.setPen(pen)
         t = (self._position - self._tmin) / self._trange
         painter.drawLine(QtCore.QLineF(t, 0, t, 1))
diff --git a/myokit/gui/datalog_viewer.py b/myokit/gui/datalog_viewer.py
index 9658767c1..86c4d4ba7 100755
--- a/myokit/gui/datalog_viewer.py
+++ b/myokit/gui/datalog_viewer.py
@@ -110,7 +110,7 @@ def __init__(self, *filenames):
         # Set size, center
         self.resize(800, 600)
         qr = self.frameGeometry()
-        cp = QtWidgets.QDesktopWidget().availableGeometry().center()
+        cp = QtGui.QGuiApplication.primaryScreen().availableGeometry().center()
         qr.moveCenter(cp)
         self.move(qr.topLeft())
 
@@ -263,7 +263,7 @@ def create_menu(self):
         # File menu
         self._menu_file = self._menu.addMenu('&File')
         # File > Open
-        self._tool_open = QtWidgets.QAction('&Open', self)
+        self._tool_open = QtGui.QAction('&Open', self)
         self._tool_open.setShortcut('Ctrl+O')
         self._tool_open.setStatusTip('Open a file')
         self._tool_open.setIcon(QtGui.QIcon.fromTheme('document-open'))
@@ -272,7 +272,7 @@ def create_menu(self):
         # File > ----
         self._menu_file.addSeparator()
         # File > Quit
-        self._tool_exit = QtWidgets.QAction('&Quit', self)
+        self._tool_exit = QtGui.QAction('&Quit', self)
         self._tool_exit.setShortcut('Ctrl+Q')
         self._tool_exit.setStatusTip('Exit application.')
         self._tool_exit.setIcon(QtGui.QIcon.fromTheme('application-exit'))
@@ -281,7 +281,7 @@ def create_menu(self):
         # View menu
         self._menu_view = self._menu.addMenu('&View')
         # View > Next file
-        self._tool_next_file = QtWidgets.QAction('Next file', self)
+        self._tool_next_file = QtGui.QAction('Next file', self)
         self._tool_next_file.setShortcut('Ctrl+PgDown')
         self._tool_next_file.setStatusTip('Select the next open file')
         self._tool_next_file.triggered.connect(self.action_next_file)
@@ -289,21 +289,21 @@ def create_menu(self):
         self._menu_view.addAction(self._tool_next_file)
         # View > Previous file
         self._menu_view.addAction(self._tool_next_file)
-        self._tool_prev_file = QtWidgets.QAction('Previous file', self)
+        self._tool_prev_file = QtGui.QAction('Previous file', self)
         self._tool_prev_file.setShortcut('Ctrl+PgUp')
         self._tool_prev_file.setStatusTip('Select the previous open file')
         self._tool_prev_file.triggered.connect(self.action_prev_file)
         self._tool_prev_file.setEnabled(False)
         self._menu_view.addAction(self._tool_prev_file)
         # View > Next variable
-        self._tool_next_var = QtWidgets.QAction('Next variable', self)
+        self._tool_next_var = QtGui.QAction('Next variable', self)
         self._tool_next_var.setShortcut('PgDown')
         self._tool_next_var.setStatusTip('Show the next variable')
         self._tool_next_var.triggered.connect(self.action_next_var)
         self._tool_next_var.setEnabled(False)
         self._menu_view.addAction(self._tool_next_var)
         # View > Previous var
-        self._tool_prev_var = QtWidgets.QAction('Previous variable', self)
+        self._tool_prev_var = QtGui.QAction('Previous variable', self)
         self._tool_prev_var.setShortcut('PgUp')
         self._tool_prev_var.setStatusTip('Show the previous variable')
         self._tool_prev_var.triggered.connect(self.action_prev_var)
@@ -312,12 +312,12 @@ def create_menu(self):
         # Help menu
         self._menu_help = self._menu.addMenu('&Help')
         # Help > About
-        self._tool_about = QtWidgets.QAction('&About', self)
+        self._tool_about = QtGui.QAction('&About', self)
         self._tool_about.setStatusTip('View information about this program.')
         self._tool_about.triggered.connect(self.action_about)
         self._menu_help.addAction(self._tool_about)
         # Help > License
-        self._tool_license = QtWidgets.QAction('&License', self)
+        self._tool_license = QtGui.QAction('&License', self)
         self._tool_license.setStatusTip('View this program\'s license info.')
         self._tool_license.triggered.connect(self.action_license)
         self._menu_help.addAction(self._tool_license)
@@ -329,7 +329,8 @@ def create_toolbar(self):
         self._toolbar = self.addToolBar('tools')
         self._toolbar.setFloatable(False)
         self._toolbar.setMovable(False)
-        self._toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
+        self._toolbar.setToolButtonStyle(
+            Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
         self._toolbar.addAction(self._tool_open)
         #self._toolbar.addSeparator()
 
@@ -544,7 +545,7 @@ class AbfTab(TabWidget):
     def __init__(self, parent, abf):
         super().__init__(parent)
         self.setTabsClosable(False)
-        self.setTabPosition(self.East)
+        self.setTabPosition(self.TabPosition.East)
         self._abf = abf
         self._figures = []
         self._axes = []
@@ -648,7 +649,7 @@ def __init__(self, parent, atf):
         self._atf = atf
 
         self.setTabsClosable(False)
-        self.setTabPosition(self.East)
+        self.setTabPosition(self.TabPosition.East)
 
         self._figures = []
         self._axes = []
@@ -719,7 +720,7 @@ class CsvTab(TabWidget):
     def __init__(self, parent, log, filename):
         super().__init__(parent)
         self.setTabsClosable(False)
-        self.setTabPosition(self.East)
+        self.setTabPosition(self.TabPosition.East)
         self._log = log.npview()
         self._filename = filename
         self._figures = []
@@ -816,7 +817,7 @@ class MatTab(TabWidget):
     def __init__(self, parent, mat, filename):
         super().__init__(parent)
         self.setTabsClosable(False)
-        self.setTabPosition(self.East)
+        self.setTabPosition(self.TabPosition.East)
         self._figures = []
         self._filename = filename
         self._axes = []
@@ -904,7 +905,7 @@ class TxtTab(TabWidget):
     def __init__(self, parent, data, filename):
         super().__init__(parent)
         self.setTabsClosable(False)
-        self.setTabPosition(self.East)
+        self.setTabPosition(self.TabPosition.East)
         self._figures = []
         self._filename = filename
         self._axes = []
@@ -986,7 +987,7 @@ class WcpTab(TabWidget):
     def __init__(self, parent, wcp):
         super().__init__(parent)
         self.setTabsClosable(False)
-        self.setTabPosition(self.East)
+        self.setTabPosition(self.TabPosition.East)
         self._wcp = wcp
         self._figures = []
         self._axes = []
diff --git a/myokit/gui/explorer.py b/myokit/gui/explorer.py
index 2c7a05b72..9401107e0 100644
--- a/myokit/gui/explorer.py
+++ b/myokit/gui/explorer.py
@@ -75,7 +75,8 @@ def __init__(self, parent, sim_method, output_stream, duration=1000):
         self._close_button = QtWidgets.QPushButton('Close')
         self._close_button.clicked.connect(self.action_close)
         # Create button layout
-        button_layout = QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.LeftToRight)
+        button_layout = QtWidgets.QBoxLayout(
+            QtWidgets.QBoxLayout.Direction.LeftToRight)
         button_layout.addWidget(label1)
         button_layout.addWidget(self._pre_field)
         button_layout.addWidget(label2)
@@ -84,14 +85,16 @@ def __init__(self, parent, sim_method, output_stream, duration=1000):
         button_layout.addWidget(self._run_button)
         # Create graph options layout
         graph_option_layout = QtWidgets.QBoxLayout(
-            QtWidgets.QBoxLayout.LeftToRight)
+            QtWidgets.QBoxLayout.Direction.LeftToRight)
         graph_option_layout.addWidget(self._select_x)
         graph_option_layout.addWidget(self._select_y)
         # Create bottom layout
-        bottom_layout = QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.LeftToRight)
+        bottom_layout = QtWidgets.QBoxLayout(
+            QtWidgets.QBoxLayout.Direction.LeftToRight)
         bottom_layout.addWidget(self._close_button)
         # Create central layout
-        layout = QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.TopToBottom)
+        layout = QtWidgets.QBoxLayout(
+            QtWidgets.QBoxLayout.Direction.TopToBottom)
         layout.addLayout(button_layout)
         layout.addLayout(graph_option_layout)
         layout.addWidget(self._canvas)
@@ -146,11 +149,11 @@ def action_run(self):
         """
         pbar = progress.ProgressBar(self, 'Creating simulation')
         QtWidgets.QApplication.processEvents(
-            QtCore.QEventLoop.ExcludeUserInputEvents)
+            QtCore.QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
         try:
             pbar.show()
             QtWidgets.QApplication.processEvents(
-                QtCore.QEventLoop.ExcludeUserInputEvents)
+                QtCore.QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
             # Create and run simulation
             out = self._sim_method()
             if type(out) == str:
@@ -159,7 +162,7 @@ def action_run(self):
             else:
                 m, p, s = out
             QtWidgets.QApplication.processEvents(
-                QtCore.QEventLoop.ExcludeUserInputEvents)
+                QtCore.QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
             pre = float(self._pre_field.text())
             run = float(self._run_field.text())
             if pre:
@@ -167,7 +170,7 @@ def action_run(self):
             d = s.run(run, progress=pbar.reporter()).npview()
             self._stream.write('Final state: \n' + m.format_state(s.state()))
             QtWidgets.QApplication.processEvents(
-                QtCore.QEventLoop.ExcludeUserInputEvents)
+                QtCore.QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
         except myokit.SimulationCancelledError:
             return
         except myokit.MyokitError as e:
@@ -180,7 +183,7 @@ def action_run(self):
             pbar.close()
             pbar.deleteLater()
             QtWidgets.QApplication.processEvents(
-                QtCore.QEventLoop.ExcludeUserInputEvents)
+                QtCore.QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
         # Reset combo-box keys?
         reset_keys = True
         if self._keys:
diff --git a/myokit/gui/ide.py b/myokit/gui/ide.py
index bb4e89d84..60052bd82 100755
--- a/myokit/gui/ide.py
+++ b/myokit/gui/ide.py
@@ -100,6 +100,10 @@ class MyokitIDE(myokit.gui.MyokitApplication):
     def __init__(self, filename=None):
         super().__init__()
 
+        # Regular expression for navigator
+        self._nav_query = QtCore.QRegularExpression(
+            r'^\[[a-zA-Z]{1}[a-zA-Z0-9_]*\]')
+
         # Set application icon
         self.setWindowIcon(icon())
 
@@ -107,7 +111,7 @@ def __init__(self, filename=None):
         self.resize(950, 720)
         self.setMinimumSize(600, 440)
         qr = self.frameGeometry()
-        cp = QtWidgets.QDesktopWidget().availableGeometry().center()
+        cp = QtGui.QGuiApplication.primaryScreen().availableGeometry().center()
         qr.moveCenter(cp)
         self.move(qr.topLeft())
 
@@ -172,7 +176,7 @@ def __init__(self, filename=None):
         self._script_tools.add(self._script_search, 'Find/Replace')
 
         # Create editor tabs
-        self._model_tab = QtWidgets.QSplitter(Qt.Horizontal)
+        self._model_tab = QtWidgets.QSplitter(Qt.Orientation.Horizontal)
         self._model_tab.editor = self._model_editor
         self._model_tab.search = self._model_search
         self._model_tab.addWidget(self._model_editor)
@@ -181,7 +185,7 @@ def __init__(self, filename=None):
         self._model_tab.setCollapsible(0, False)
         self._model_tab.setCollapsible(1, False)
 
-        self._protocol_tab = QtWidgets.QSplitter(Qt.Horizontal)
+        self._protocol_tab = QtWidgets.QSplitter(Qt.Orientation.Horizontal)
         self._protocol_tab.editor = self._protocol_editor
         self._protocol_tab.search = self._protocol_search
         self._protocol_tab.addWidget(self._protocol_editor)
@@ -190,7 +194,7 @@ def __init__(self, filename=None):
         self._protocol_tab.setCollapsible(0, False)
         self._protocol_tab.setCollapsible(1, False)
 
-        self._script_tab = QtWidgets.QSplitter(Qt.Horizontal)
+        self._script_tab = QtWidgets.QSplitter(Qt.Orientation.Horizontal)
         self._script_tab.editor = self._script_editor
         self._script_tab.search = self._script_search
         self._script_tab.addWidget(self._script_editor)
@@ -233,7 +237,7 @@ def __init__(self, filename=None):
         self._console.write('Loading Myokit IDE')
 
         # Create central layout: vertical splitter
-        self._central_splitter = QtWidgets.QSplitter(Qt.Vertical)
+        self._central_splitter = QtWidgets.QSplitter(Qt.Orientation.Vertical)
         self._central_splitter.addWidget(self._editor_tabs)
         self._central_splitter.addWidget(self._console)
         self._central_splitter.setSizes([580, 120])
@@ -369,10 +373,10 @@ def action_clear_units(self):
         try:
             # Ask are you sure?
             msg = 'Remove all units from expressions in model?'
-            box = QtWidgets.QMessageBox
-            options = box.Yes | box.No
-            reply = box.question(self, TITLE, msg, options)
-            if reply == box.No:
+            sb = QtWidgets.QMessageBox.StandardButton
+            reply = QtWidgets.QMessageBox.question(
+                self, TITLE, msg, sb.Yes | sb.No)
+            if reply == sb.No:
                 return
             # Strip units
             # Note: lines are used in error handling!
@@ -454,7 +458,7 @@ def action_explore(self):
         # Simulation creation method
         def sim():
             QtWidgets.QApplication.processEvents(
-                QtCore.QEventLoop.ExcludeUserInputEvents)
+                QtCore.QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
             try:
                 # Get model and protocol
                 m = self.model(errors_in_console=True)
@@ -463,12 +467,12 @@ def sim():
                 elif m is None:
                     return 'Empty model definition'
                 QtWidgets.QApplication.processEvents(
-                    QtCore.QEventLoop.ExcludeUserInputEvents)
+                    QtCore.QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
                 p = self.protocol(errors_in_console=True)
                 if p is False:
                     return 'Errors in protocol'
                 QtWidgets.QApplication.processEvents(
-                    QtCore.QEventLoop.ExcludeUserInputEvents)
+                    QtCore.QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
                 # Create and return simulation
                 self.statusBar().showMessage('Creating simulation...')
                 return m, p, myokit.Simulation(m, p)
@@ -994,12 +998,12 @@ def action_run(self):
             #self.setEnabled(False)
             self._console.write('Running embedded script.')
             QtWidgets.QApplication.setOverrideCursor(
-                QtGui.QCursor(Qt.WaitCursor))
+                QtGui.QCursor(Qt.CursorShape.WaitCursor))
             # Create progress bar
             pbar = progress.ProgressBar(self, 'Running embedded script')
             pbar.show()
             QtWidgets.QApplication.processEvents(
-                QtCore.QEventLoop.ExcludeUserInputEvents)
+                QtCore.QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
             # Get model and protocol
             m = self.model(errors_in_console=True)
             if m is False:
@@ -1008,16 +1012,16 @@ def action_run(self):
             if p is False:
                 return
             QtWidgets.QApplication.processEvents(
-                QtCore.QEventLoop.ExcludeUserInputEvents)
+                QtCore.QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
             # Clone model & protocol: the script may modify them!
             if m:
                 m = m.clone()
             QtWidgets.QApplication.processEvents(
-                QtCore.QEventLoop.ExcludeUserInputEvents)
+                QtCore.QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
             if p:
                 p = p.clone()
             QtWidgets.QApplication.processEvents(
-                QtCore.QEventLoop.ExcludeUserInputEvents)
+                QtCore.QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
             # Get embedded script
             x = self._script_editor.get_text()
             # Run
@@ -1039,7 +1043,7 @@ def action_run(self):
                 self._console.write(traceback.format_exc())
         finally:
             QtWidgets.QApplication.processEvents(
-                QtCore.QEventLoop.ExcludeUserInputEvents)
+                QtCore.QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
             # Hide progress bar
             if pbar is not None:
                 pbar.close()
@@ -1534,8 +1538,10 @@ def closeEvent(self, event=None):
             if event:
                 event.ignore()
             return
+
         # Close all windows, including matplotlib plots
-        QtWidgets.qApp.closeAllWindows()
+        QtWidgets.QApplication.instance().closeAllWindows()
+
         # Accept event, closing this window
         if event:
             event.accept()
@@ -1558,14 +1564,14 @@ def create_menu(self):
         # File menu
         self._menu_file = self._menu.addMenu('&File')
         # File > New
-        self._tool_new = QtWidgets.QAction('&New', self)
+        self._tool_new = QtGui.QAction('&New', self)
         self._tool_new.setShortcut('Ctrl+N')
         self._tool_new.setStatusTip('Create a new mmt file.')
         self._tool_new.setIcon(myokit.gui.icon('document-new'))
         self._tool_new.triggered.connect(self.action_new)
         self._menu_file.addAction(self._tool_new)
         # File > Open
-        self._tool_open = QtWidgets.QAction('&Open', self)
+        self._tool_open = QtGui.QAction('&Open', self)
         self._tool_open.setShortcut('Ctrl+O')
         self._tool_open.setStatusTip('Open an existing mmt file.')
         self._tool_open.setIcon(myokit.gui.icon('document-open'))
@@ -1574,7 +1580,7 @@ def create_menu(self):
         # File > ----
         self._menu_file.addSeparator()
         # File > Save
-        self._tool_save = QtWidgets.QAction('&Save', self)
+        self._tool_save = QtGui.QAction('&Save', self)
         self._tool_save.setShortcut('Ctrl+S')
         self._tool_save.setStatusTip('Save the current file')
         self._tool_save.setIcon(myokit.gui.icon('document-save'))
@@ -1582,7 +1588,7 @@ def create_menu(self):
         self._tool_save.setEnabled(False)
         self._menu_file.addAction(self._tool_save)
         # File > Save as
-        self._tool_save_as = QtWidgets.QAction('Save &as', self)
+        self._tool_save_as = QtGui.QAction('Save &as', self)
         self._tool_save_as.setShortcut('Ctrl+Shift+S')
         self._tool_save_as.setStatusTip(
             'Save the current file under a different name.')
@@ -1593,14 +1599,14 @@ def create_menu(self):
         # File > Recent files
         self._recent_file_tools = []
         for i in range(N_RECENT_FILES):
-            tool = QtWidgets.QAction(self, visible=False)
+            tool = QtGui.QAction(self, visible=False)
             tool.triggered.connect(self.action_open_recent)
             self._recent_file_tools.append(tool)
             self._menu_file.addAction(tool)
         # File > ----
         self._menu_file.addSeparator()
         # File > Quit
-        self._tool_exit = QtWidgets.QAction('&Quit', self)
+        self._tool_exit = QtGui.QAction('&Quit', self)
         self._tool_exit.setShortcut('Ctrl+Q')
         self._tool_exit.setStatusTip('Exit application.')
         self._tool_exit.triggered.connect(self.close)
@@ -1610,7 +1616,7 @@ def create_menu(self):
         #
         self._menu_edit = self._menu.addMenu('&Edit')
         # Edit > Undo
-        self._tool_undo = QtWidgets.QAction('&Undo', self)
+        self._tool_undo = QtGui.QAction('&Undo', self)
         self._tool_undo.setShortcut('Ctrl+Z')
         self._tool_undo.setStatusTip('Undo the last edit.')
         self._tool_undo.setIcon(myokit.gui.icon('edit-undo'))
@@ -1618,7 +1624,7 @@ def create_menu(self):
         self._tool_undo.setEnabled(False)
         self._menu_edit.addAction(self._tool_undo)
         # Edit > Redo
-        self._tool_redo = QtWidgets.QAction('&Redo', self)
+        self._tool_redo = QtGui.QAction('&Redo', self)
         self._tool_redo.setShortcut('Ctrl+Shift+Z')
         self._tool_redo.setStatusTip('Redo the last undone edit.')
         self._tool_redo.setIcon(myokit.gui.icon('edit-redo'))
@@ -1628,7 +1634,7 @@ def create_menu(self):
         # Edit > ----
         self._menu_edit.addSeparator()
         # Edit > Cut
-        self._tool_cut = QtWidgets.QAction('&Cut', self)
+        self._tool_cut = QtGui.QAction('&Cut', self)
         self._tool_cut.setShortcut('Ctrl+X')
         self._tool_cut.setStatusTip(
             'Cut the selected text and copy it to the clipboard.')
@@ -1637,7 +1643,7 @@ def create_menu(self):
         self._tool_cut.setEnabled(False)
         self._menu_edit.addAction(self._tool_cut)
         # Edit > Copy
-        self._tool_copy = QtWidgets.QAction('&Copy', self)
+        self._tool_copy = QtGui.QAction('&Copy', self)
         self._tool_copy.setShortcut('Ctrl+C')
         self._tool_copy.setStatusTip(
             'Copy the selected text to the clipboard.')
@@ -1646,7 +1652,7 @@ def create_menu(self):
         self._tool_copy.setEnabled(False)
         self._menu_edit.addAction(self._tool_copy)
         # Edit > Paste
-        self._tool_paste = QtWidgets.QAction('&Paste', self)
+        self._tool_paste = QtGui.QAction('&Paste', self)
         self._tool_paste.setShortcut('Ctrl+V')
         self._tool_paste.setStatusTip(
             'Paste text from the clipboard into the editor.')
@@ -1656,7 +1662,7 @@ def create_menu(self):
         # Edit > ----
         self._menu_edit.addSeparator()
         # Edit > Find and replace
-        self._tool_find = QtWidgets.QAction('&Find and replace', self)
+        self._tool_find = QtGui.QAction('&Find and replace', self)
         self._tool_find.setShortcut('Ctrl+F')
         self._tool_find.setStatusTip('Find and/or replace some text.')
         self._tool_find.setIcon(myokit.gui.icon('edit-find'))
@@ -1665,8 +1671,7 @@ def create_menu(self):
         # Edit > ----
         self._menu_edit.addSeparator()
         # Edit > Format protocol
-        self._tool_format_protocol = QtWidgets.QAction(
-            'Format protocol', self)
+        self._tool_format_protocol = QtGui.QAction('Format protocol', self)
         self._tool_format_protocol.setStatusTip(
             'Standardise the formatting of the protocol section.')
         self._tool_format_protocol.triggered.connect(
@@ -1676,7 +1681,7 @@ def create_menu(self):
         # Edit > ----
         self._menu_edit.addSeparator()
         # Edit > Comment or uncomment
-        self._tool_comment = QtWidgets.QAction(
+        self._tool_comment = QtGui.QAction(
             '&Comment/uncomment selected lines', self)
         self._tool_comment.setShortcut('Ctrl+;')
         self._tool_comment.setStatusTip(
@@ -1684,7 +1689,7 @@ def create_menu(self):
         self._tool_comment.triggered.connect(self.action_comment)
         self._menu_edit.addAction(self._tool_comment)
         # Edit > Remove units from expressions
-        self._tool_remove_units = QtWidgets.QAction(
+        self._tool_remove_units = QtGui.QAction(
             'Remove units from &expressions', self)
         self._tool_remove_units.setStatusTip(
             'Remove all units inside expressions.')
@@ -1692,7 +1697,7 @@ def create_menu(self):
         self._menu_edit.addAction(self._tool_remove_units)
         self._model_widgets.append(self._tool_remove_units)
         # Edit > Trim whitespace
-        self._tool_trim_whitespace = QtWidgets.QAction(
+        self._tool_trim_whitespace = QtGui.QAction(
             'Trim trailing &whitespace', self)
         self._tool_trim_whitespace.setStatusTip(
             'Remove trailing whitespace from each line.')
@@ -1704,14 +1709,13 @@ def create_menu(self):
         #
         self._menu_view = self._menu.addMenu('&View')
         # View > View model definition
-        self._tool_view_model = QtWidgets.QAction(
-            'View &model definition', self)
+        self._tool_view_model = QtGui.QAction('View &model definition', self)
         self._tool_view_model.setShortcut('Alt+1')
         self._tool_view_model.setStatusTip('View the model definition tab')
         self._tool_view_model.triggered.connect(self.action_view_model)
         self._menu_view.addAction(self._tool_view_model)
         # View > View protocol definition
-        self._tool_view_protocol = QtWidgets.QAction(
+        self._tool_view_protocol = QtGui.QAction(
             'View &protocol definition', self)
         self._tool_view_protocol.setShortcut('Alt+2')
         self._tool_view_protocol.setStatusTip(
@@ -1719,8 +1723,7 @@ def create_menu(self):
         self._tool_view_protocol.triggered.connect(self.action_view_protocol)
         self._menu_view.addAction(self._tool_view_protocol)
         # View > View embedded script
-        self._tool_view_script = QtWidgets.QAction(
-            'View embedded &script', self)
+        self._tool_view_script = QtGui.QAction('View embedded &script', self)
         self._tool_view_script.setShortcut('Alt+3')
         self._tool_view_script.setStatusTip('View the embedded script tab')
         self._tool_view_script.triggered.connect(self.action_view_script)
@@ -1728,7 +1731,7 @@ def create_menu(self):
         # View > ----
         self._menu_view.addSeparator()
         # View > Show model components (navigator)
-        self._tool_view_navigator = QtWidgets.QAction(
+        self._tool_view_navigator = QtGui.QAction(
             'Show model &components', self)
         self._tool_view_navigator.setCheckable(True)
         self._tool_view_navigator.setStatusTip(
@@ -1740,8 +1743,7 @@ def create_menu(self):
         # View > ----
         self._menu_view.addSeparator()
         # View > Preview protocol
-        self._tool_preview_protocol = QtWidgets.QAction(
-            '&Preview protocol', self)
+        self._tool_preview_protocol = QtGui.QAction('&Preview protocol', self)
         self._tool_preview_protocol.setShortcut('Ctrl+P')
         self._tool_preview_protocol.setStatusTip(
             'Show a preview of the current protocol.')
@@ -1755,7 +1757,7 @@ def create_menu(self):
         self._menu_convert = self._menu.addMenu('&Convert')
 
         # Convert > Import CellML
-        self._tool_import_cellml = QtWidgets.QAction(
+        self._tool_import_cellml = QtGui.QAction(
             'Import model from CellML', self)
         self._tool_import_cellml.setStatusTip(
             'Import a model definition from a CellML file.')
@@ -1763,7 +1765,7 @@ def create_menu(self):
             lambda: self.action_import_model('cellml', FILTER_CELLML))
         self._menu_convert.addAction(self._tool_import_cellml)
         # Convert > Export CellML 1
-        self._tool_export_cellml1 = QtWidgets.QAction(
+        self._tool_export_cellml1 = QtGui.QAction(
             'Export model to CellML 1.0', self)
         self._tool_export_cellml1.setStatusTip(
             'Export a model definition to a CellML 1.0 document.')
@@ -1772,7 +1774,7 @@ def create_menu(self):
                 'cellml1', '.cellml', FILTER_CELLML))
         self._menu_convert.addAction(self._tool_export_cellml1)
         # Convert > Export CellML 2
-        self._tool_export_cellml2 = QtWidgets.QAction(
+        self._tool_export_cellml2 = QtGui.QAction(
             'Export model to CellML 2.0', self)
         self._tool_export_cellml2.setStatusTip(
             'Export a model definition to a CellML 2.0 document.')
@@ -1784,15 +1786,14 @@ def create_menu(self):
         # Convert > ----
         self._menu_convert.addSeparator()
         # Convert > Import ABF
-        self._tool_import_abf = QtWidgets.QAction(
-            'Import protocol from ABF', self)
+        self._tool_import_abf = QtGui.QAction('Import protocol from ABF', self)
         self._tool_import_abf.setStatusTip(
             'Import a protocol definition from an ABF file.')
         self._tool_import_abf.triggered.connect(
             self.action_import_abf_protocol)
         self._menu_convert.addAction(self._tool_import_abf)
         # Convert > Import ChannelML
-        self._tool_import_channelml = QtWidgets.QAction(
+        self._tool_import_channelml = QtGui.QAction(
             'Import model from ChannelML', self)
         self._tool_import_channelml.setStatusTip(
             'Import a channel model from ChannelML.')
@@ -1800,8 +1801,7 @@ def create_menu(self):
             lambda: self.action_import_model('channelml', FILTER_CHANNELML))
         self._menu_convert.addAction(self._tool_import_channelml)
         # Convert > Import SBML
-        self._tool_import_sbml = QtWidgets.QAction(
-            'Import model from SBML', self)
+        self._tool_import_sbml = QtGui.QAction('Import model from SBML', self)
         self._tool_import_sbml.setStatusTip(
             'Import a model from SBML.')
         self._tool_import_sbml.triggered.connect(
@@ -1811,8 +1811,7 @@ def create_menu(self):
         # Convert > ----
         self._menu_convert.addSeparator()
         # Convert > Export HTML
-        self._tool_export_html = QtWidgets.QAction(
-            'Export model to HTML', self)
+        self._tool_export_html = QtGui.QAction('Export model to HTML', self)
         self._tool_export_html.setStatusTip(
             'Export a model definition to an HTML document using presentation'
             ' MathML.')
@@ -1820,8 +1819,7 @@ def create_menu(self):
             lambda: self.action_export_model('html', '.html', FILTER_HTML))
         self._menu_convert.addAction(self._tool_export_html)
         # Convert > Export Latex
-        self._tool_export_latex = QtWidgets.QAction(
-            'Export model to Latex', self)
+        self._tool_export_latex = QtGui.QAction('Export model to Latex', self)
         self._tool_export_latex.setStatusTip(
             'Export a model definition to a Latex document.')
         self._tool_export_latex.triggered.connect(
@@ -1833,7 +1831,7 @@ def create_menu(self):
         self._menu_convert.addSeparator()
 
         # Convert > Ansic
-        self._tool_export_ansic = QtWidgets.QAction('Export to Ansi C', self)
+        self._tool_export_ansic = QtGui.QAction('Export to Ansi C', self)
         self._tool_export_ansic.setStatusTip(
             'Export to a runnable Ansi C program.')
         self._tool_export_ansic.triggered.connect(
@@ -1841,8 +1839,7 @@ def create_menu(self):
         self._menu_convert.addAction(self._tool_export_ansic)
 
         # Convert > CUDA
-        self._tool_export_cuda = QtWidgets.QAction(
-            'Export to CUDA kernel', self)
+        self._tool_export_cuda = QtGui.QAction('Export to CUDA kernel', self)
         self._tool_export_cuda.setStatusTip(
             'Export a model definition to a CUDA kernel program.')
         self._tool_export_cuda.triggered.connect(
@@ -1850,7 +1847,7 @@ def create_menu(self):
         self._menu_convert.addAction(self._tool_export_cuda)
 
         # Convert > CUDA RL
-        self._tool_export_cuda_rl = QtWidgets.QAction(
+        self._tool_export_cuda_rl = QtGui.QAction(
             'Export to CUDA kernel with RL updates', self)
         self._tool_export_cuda_rl.setStatusTip(
             'Export a model definition to a CUDA kernel program using'
@@ -1860,7 +1857,7 @@ def create_menu(self):
         self._menu_convert.addAction(self._tool_export_cuda_rl)
 
         # Convert > EasyML
-        self._tool_export_easyml = QtWidgets.QAction(
+        self._tool_export_easyml = QtGui.QAction(
             'Export to EasyML (Carp)', self)
         self._tool_export_easyml.setStatusTip(
             'Export to an EasyML script for use with Carp/Carpentry.')
@@ -1869,7 +1866,7 @@ def create_menu(self):
         self._menu_convert.addAction(self._tool_export_easyml)
 
         # Convert > Matlab
-        self._tool_export_matlab = QtWidgets.QAction(
+        self._tool_export_matlab = QtGui.QAction(
             'Export to Matlab/Octave', self)
         self._tool_export_matlab.setStatusTip(
             'Export to a runnable Matlab/Octave script.')
@@ -1878,7 +1875,7 @@ def create_menu(self):
         self._menu_convert.addAction(self._tool_export_matlab)
 
         # Convert > OpenCL
-        self._tool_export_opencl = QtWidgets.QAction(
+        self._tool_export_opencl = QtGui.QAction(
             'Export to OpenCL kernel', self)
         self._tool_export_opencl.setStatusTip(
             'Export a model definition to an OpenCL kernel program using')
@@ -1887,7 +1884,7 @@ def create_menu(self):
         self._menu_convert.addAction(self._tool_export_opencl)
 
         # Convert > OpenCL RL
-        self._tool_export_opencl_rl = QtWidgets.QAction(
+        self._tool_export_opencl_rl = QtGui.QAction(
             'Export to OpenCL kernel with RL updates', self)
         self._tool_export_opencl_rl.setStatusTip(
             'Export a model definition to an OpenCL kernel program using'
@@ -1897,7 +1894,7 @@ def create_menu(self):
         self._menu_convert.addAction(self._tool_export_opencl_rl)
 
         # Convert > Python
-        self._tool_export_python = QtWidgets.QAction('Export to Python', self)
+        self._tool_export_python = QtGui.QAction('Export to Python', self)
         self._tool_export_python.setStatusTip(
             'Export a model definition to a runnable Python script.')
         self._tool_export_python.triggered.connect(
@@ -1909,7 +1906,7 @@ def create_menu(self):
         #
         self._menu_analysis = self._menu.addMenu('&Analysis')
         # Analysis > Model statistics
-        self._tool_stats = QtWidgets.QAction('Show model statistics', self)
+        self._tool_stats = QtGui.QAction('Show model statistics', self)
         self._tool_stats.setStatusTip(
             'Displays some basic statistics about the current model.')
         self._tool_stats.triggered.connect(self.action_model_stats)
@@ -1917,8 +1914,7 @@ def create_menu(self):
         # Analysis > ----
         self._menu_analysis.addSeparator()
         # Analysis > Check units strict
-        self._tool_units_strict = QtWidgets.QAction(
-            'Check units (&strict)', self)
+        self._tool_units_strict = QtGui.QAction('Check units (&strict)', self)
         self._tool_units_strict.setShortcut('F9')
         self._tool_units_strict.setStatusTip(
             'Check this model\'s units in strict mode.')
@@ -1926,7 +1922,7 @@ def create_menu(self):
             self.action_check_units_strict)
         self._menu_analysis.addAction(self._tool_units_strict)
         # Analysis > Check units tolerant
-        self._tool_units_tolerant = QtWidgets.QAction(
+        self._tool_units_tolerant = QtGui.QAction(
             'Check units (&tolerant)', self)
         self._tool_units_tolerant.setShortcut('F10')
         self._tool_units_tolerant.setStatusTip(
@@ -1937,7 +1933,7 @@ def create_menu(self):
         # Analysis > ----
         self._menu_analysis.addSeparator()
         # Analysis > Show variable info
-        self._tool_variable_info = QtWidgets.QAction(
+        self._tool_variable_info = QtGui.QAction(
             'Show quick variable info', self)
         self._tool_variable_info.setShortcut('Ctrl+R')
         self._tool_variable_info.setStatusTip(
@@ -1946,7 +1942,7 @@ def create_menu(self):
         self._menu_analysis.addAction(self._tool_variable_info)
         self._model_widgets.append(self._tool_variable_info)
         # Analysis > Show variable evaluation
-        self._tool_variable_evaluation = QtWidgets.QAction(
+        self._tool_variable_evaluation = QtGui.QAction(
             'Show variable evaluation', self)
         self._tool_variable_evaluation.setShortcut('Ctrl+E')
         self._tool_variable_evaluation.setStatusTip(
@@ -1956,7 +1952,7 @@ def create_menu(self):
         self._menu_analysis.addAction(self._tool_variable_evaluation)
         self._model_widgets.append(self._tool_variable_evaluation)
         # Analysis > Show variable dependencies
-        self._tool_variable_dependencies = QtWidgets.QAction(
+        self._tool_variable_dependencies = QtGui.QAction(
             'Show variable dependencies', self)
         self._tool_variable_dependencies.setShortcut('Ctrl+D')
         self._tool_variable_dependencies.setStatusTip(
@@ -1966,7 +1962,7 @@ def create_menu(self):
         self._menu_analysis.addAction(self._tool_variable_dependencies)
         self._model_widgets.append(self._tool_variable_dependencies)
         # Analysis > Show variable users
-        self._tool_variable_users = QtWidgets.QAction(
+        self._tool_variable_users = QtGui.QAction(
             'Show variable users', self)
         self._tool_variable_users.setShortcut('Ctrl+U')
         self._tool_variable_users.setStatusTip(
@@ -1976,7 +1972,7 @@ def create_menu(self):
         self._menu_analysis.addAction(self._tool_variable_users)
         self._model_widgets.append(self._tool_variable_users)
         # Analysis > Graph variable
-        self._tool_variable_graph = QtWidgets.QAction(
+        self._tool_variable_graph = QtGui.QAction(
             'Graph selected variable', self)
         self._tool_variable_graph.setShortcut('Ctrl+G')
         self._tool_variable_graph.setStatusTip(
@@ -1985,7 +1981,7 @@ def create_menu(self):
         self._menu_analysis.addAction(self._tool_variable_graph)
         self._model_widgets.append(self._tool_variable_graph)
         # Analysis > Jump to variable definition
-        self._tool_variable_jump = QtWidgets.QAction(
+        self._tool_variable_jump = QtGui.QAction(
             'Jump to variable definition', self)
         self._tool_variable_jump.setShortcut('Ctrl+J')
         self._tool_variable_jump.setStatusTip(
@@ -1998,7 +1994,7 @@ def create_menu(self):
         # Analysis > ----
         self._menu_analysis.addSeparator()
         # Analysis > Evaluate state derivatives
-        self._tool_state_derivatives = QtWidgets.QAction(
+        self._tool_state_derivatives = QtGui.QAction(
             'Evaluate state derivatives', self)
         self._tool_state_derivatives.setShortcut('F7')
         self._tool_state_derivatives.setStatusTip(
@@ -2007,7 +2003,7 @@ def create_menu(self):
             self.action_state_derivatives)
         self._menu_analysis.addAction(self._tool_state_derivatives)
         # Analysis > Evaluate state derivatives without error checking
-        self._tool_state_derivatives2 = QtWidgets.QAction(
+        self._tool_state_derivatives2 = QtGui.QAction(
             'Evaluate state derivatives (no error checking)', self)
         self._tool_state_derivatives2.setShortcut('F8')
         self._tool_state_derivatives2.setStatusTip(
@@ -2019,7 +2015,7 @@ def create_menu(self):
         # Analysis > ----
         self._menu_analysis.addSeparator()
         # Analysis > Show component dependency graph
-        self._tool_component_dependency_graph = QtWidgets.QAction(
+        self._tool_component_dependency_graph = QtGui.QAction(
             'Show component dependency graph', self)
         self._tool_component_dependency_graph.setStatusTip(
             'Display a graph of the dependencies between components.')
@@ -2027,7 +2023,7 @@ def create_menu(self):
             self.action_component_dependency_graph)
         self._menu_analysis.addAction(self._tool_component_dependency_graph)
         # Analysis > Show variable dependency graph
-        self._tool_variable_dependency_graph = QtWidgets.QAction(
+        self._tool_variable_dependency_graph = QtGui.QAction(
             'Show variable dependency graph', self)
         self._tool_variable_dependency_graph.setStatusTip(
             'Display a graph of the dependencies between variables.')
@@ -2035,14 +2031,14 @@ def create_menu(self):
             self.action_variable_dependency_graph)
         self._menu_analysis.addAction(self._tool_variable_dependency_graph)
         # Analysis > Show state dependency matrix
-        self._tool_state_matrix = QtWidgets.QAction(
+        self._tool_state_matrix = QtGui.QAction(
             'Show state dependency matrix', self)
         self._tool_state_matrix.setStatusTip(
             'Display a matrix graph of the dependencies between states.')
         self._tool_state_matrix.triggered.connect(self.action_state_matrix)
         self._menu_analysis.addAction(self._tool_state_matrix)
         # Analysis > Show component dependency cycles
-        self._tool_component_cycles = QtWidgets.QAction(
+        self._tool_component_cycles = QtGui.QAction(
             'Show cyclical component dependencies', self)
         self._tool_component_cycles.setStatusTip(
             'Display a list of cyclical dependencies between components.')
@@ -2054,29 +2050,27 @@ def create_menu(self):
         #
         self._menu_run = self._menu.addMenu('&Run')
         # Run > Validate
-        self._tool_validate = QtWidgets.QAction(
-            '&Validate model', self)
+        self._tool_validate = QtGui.QAction('&Validate model', self)
         self._tool_validate.setShortcut('Ctrl+B')
         self._tool_validate.setStatusTip('Validate the model.')
         self._tool_validate.triggered.connect(self.action_validate)
         self._menu_run.addAction(self._tool_validate)
         # Run > Jump to error
-        self._tool_jump_to_error = QtWidgets.QAction(
-            '&Jump to last error', self)
+        self._tool_jump_to_error = QtGui.QAction('&Jump to last error', self)
         self._tool_jump_to_error.setShortcut('Ctrl+Space')
         self._tool_jump_to_error.setStatusTip(
             'Jump to the last model error found.')
         self._tool_jump_to_error.triggered.connect(self.action_jump_to_error)
         self._menu_run.addAction(self._tool_jump_to_error)
         # Run > Run embedded script
-        self._tool_run = QtWidgets.QAction('&Run embedded script', self)
+        self._tool_run = QtGui.QAction('&Run embedded script', self)
         self._tool_run.setShortcut('F5')
         self._tool_run.setStatusTip('Run the embedded script.')
         self._tool_run.setIcon(myokit.gui.icon('media-playback-start'))
         self._tool_run.triggered.connect(self.action_run)
         self._menu_run.addAction(self._tool_run)
         # Run > Run explorer
-        self._tool_explore = QtWidgets.QAction('&Run explorer', self)
+        self._tool_explore = QtGui.QAction('&Run explorer', self)
         self._tool_explore.setShortcut('F6')
         self._tool_explore.setStatusTip(
             'Run a simulation and display the results in the explorer.')
@@ -2088,12 +2082,12 @@ def create_menu(self):
         #
         self._menu_help = self._menu.addMenu('&Help')
         # Help > About
-        self._tool_about = QtWidgets.QAction('&About', self)
+        self._tool_about = QtGui.QAction('&About', self)
         self._tool_about.setStatusTip('View information about this program.')
         self._tool_about.triggered.connect(self.action_about)
         self._menu_help.addAction(self._tool_about)
         # Help > License
-        self._tool_license = QtWidgets.QAction('&License', self)
+        self._tool_license = QtGui.QAction('&License', self)
         self._tool_license.setStatusTip('View this program\'s license info.')
         self._tool_license.triggered.connect(self.action_license)
         self._menu_help.addAction(self._tool_license)
@@ -2348,15 +2342,15 @@ def prompt_save_changes(self, cancel=False):
             msg = 'Save changes to ' + str(self._file) + '?'
         else:
             msg = 'Save changes to new file?'
-        box = QtWidgets.QMessageBox
-        options = box.Yes | box.No
+        sb = QtWidgets.QMessageBox.StandardButton
+        options = sb.Yes | sb.No
         if cancel:
-            options |= box.Cancel
-        reply = box.question(self, TITLE, msg, options)
-        if reply == box.Yes:
+            options |= sb.Cancel
+        reply = QtWidgets.QMessageBox.question(self, TITLE, msg, options)
+        if reply == sb.Yes:
             # Only allow quitting if save succesful
             return self.save_file(save_as=False)
-        elif reply == box.No:
+        elif reply == sb.No:
             return True
         else:
             return False
@@ -2578,16 +2572,13 @@ def show_exception(self):
     def update_navigator(self):
         """ Updates the model navigator contents. """
         # Find all components and store their positions in a list (name, line)
-        #Sloppy version: query = QtCore.QRegExp(r'^\[') could be faster?
-        query = QtCore.QRegExp(r'^\[[a-zA-Z]{1}[a-zA-Z0-9_]*\]')
         pos = 0
-        found = self._model_editor.document().find(query, pos)
+        found = self._model_editor.document().find(self._nav_query, pos)
         positions = []
         while not found.isNull():
             pos = found.position()
-            block = found.block()
-            positions.append((block.text(), pos))
-            found = self._model_editor.document().find(query, pos)
+            positions.append((found.selectedText(), pos))
+            found = self._model_editor.document().find(self._nav_query, pos)
         self._model_navigator.set_positions(positions)
 
     def update_recent_files_menu(self):
@@ -2650,7 +2641,7 @@ def _close_button_hit(self, index):
     def keyPressEvent(self, event):
         """ Qt event: A key-press reaches the widget. """
         key = event.key()
-        if key == Qt.Key_Escape:
+        if key == Qt.Key.Key_Escape:
             self.setFocus()
             self.focusPreviousChild()
         else:
@@ -2724,7 +2715,7 @@ def __init__(self, parent=None):
     def current_item_changed(self, item, previous_item):
         """ Called if the navigator item is changed. """
         if item is not None:
-            line = item.data(Qt.UserRole)
+            line = item.data(Qt.ItemDataRole.UserRole)
             self.item_changed.emit(line)
 
     def set_positions(self, positions):
@@ -2740,7 +2731,7 @@ def set_positions(self, positions):
         self._list_widget.setSortingEnabled(True)
         for text, pos in self._positions:
             item = QtWidgets.QListWidgetItem(text[1:-1])
-            item.setData(Qt.UserRole, pos)
+            item.setData(Qt.ItemDataRole.UserRole, pos)
             self._list_widget.addItem(item)
 
 
@@ -2757,7 +2748,8 @@ def __init__(self, parent=None):
         font = myokit.gui.qtMonospaceFont()
         font.setPointSize(10)
         self.setFont(font)
-        self.setFrameStyle(QtWidgets.QFrame.WinPanel | QtWidgets.QFrame.Sunken)
+        self.setFrameStyle(
+            QtWidgets.QFrame.Shape.WinPanel | QtWidgets.QFrame.Shadow.Sunken)
 
     def clear(self):
         """
@@ -2799,4 +2791,4 @@ def write(self, text=None):
         self.verticalScrollBar().setValue(self.verticalScrollBar().maximum())
         # Autoflush
         QtWidgets.QApplication.processEvents(
-            QtCore.QEventLoop.ExcludeUserInputEvents)
+            QtCore.QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
diff --git a/myokit/gui/progress.py b/myokit/gui/progress.py
index a58aaef56..f138227ea 100644
--- a/myokit/gui/progress.py
+++ b/myokit/gui/progress.py
@@ -20,8 +20,9 @@ class ProgressBar(QtWidgets.QProgressDialog):
     """
     def __init__(self, parent, message):
         super().__init__(
-            message, 'Cancel', 0, N, parent=parent)
-        self.setWindowModality(Qt.WindowModal)
+            message, 'Cancel', 0, N, parent)
+        self.setWindowTitle(' ')
+        self.setWindowModality(Qt.WindowModality.WindowModal)
         self.setAutoClose(False)
         self.setAutoReset(False)
         self._reporter = ProgressBarReporter(self)
@@ -57,8 +58,9 @@ def enter(self, msg=None):
         if msg is not None:
             self._pd.setLabelText(str(msg))
         self._pd.setValue(0)
+        self._pd.repaint()
         QtWidgets.QApplication.processEvents(
-            QtCore.QEventLoop.ExcludeUserInputEvents)
+            QtCore.QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
 
     def exit(self):
         self._pd.setEnabled(False)
@@ -66,5 +68,6 @@ def exit(self):
     def update(self, f):
         self._pd.setValue((int)(N * f))
         QtWidgets.QApplication.processEvents(
-            QtCore.QEventLoop.ExcludeUserInputEvents)
+            QtCore.QEventLoop.ProcessEventsFlag.ExcludeUserInputEvents)
+        self._pd.repaint()
         return not self._pd.wasCanceled()
diff --git a/myokit/gui/source.py b/myokit/gui/source.py
index 3a98e699f..f4ca416cc 100644
--- a/myokit/gui/source.py
+++ b/myokit/gui/source.py
@@ -67,7 +67,7 @@ def _check_for_dark_mode(palette):
         STYLE_ANNOT_KEY.setForeground(QtGui.QColor(0, 31, 231))
         STYLE_ANNOT_VAL.setForeground(QtGui.QColor(57, 115, 214))
         STYLE_KEYWORD_1.setForeground(QtGui.QColor(0, 128, 0))
-        STYLE_KEYWORD_1.setFontWeight(QtGui.QFont.Bold)
+        STYLE_KEYWORD_1.setFontWeight(QtGui.QFont.Weight.Bold)
         STYLE_KEYWORD_2.setForeground(QtGui.QColor(0, 128, 128))
         STYLE_LITERAL.setForeground(QtGui.QColor(255, 20, 215))
         STYLE_INLINE_UNIT.setForeground(QtGui.QColor(128, 0, 128))
@@ -77,7 +77,7 @@ def _check_for_dark_mode(palette):
         STYLE_ANNOT_KEY.setForeground(QtGui.QColor(179, 179, 179))
         STYLE_ANNOT_VAL.setForeground(QtGui.QColor(171, 177, 205))
         STYLE_KEYWORD_1.setForeground(QtGui.QColor(10, 195, 87))
-        STYLE_KEYWORD_1.setFontWeight(QtGui.QFont.Bold)
+        STYLE_KEYWORD_1.setFontWeight(QtGui.QFont.Weight.Bold)
         STYLE_KEYWORD_2.setForeground(QtGui.QColor(10, 195, 87))
         STYLE_LITERAL.setForeground(QtGui.QColor(255, 223, 12))
         STYLE_INLINE_UNIT.setForeground(QtGui.QColor(168, 152, 33))
@@ -114,7 +114,12 @@ def __init__(self, parent=None):
         self.cursor_changed()
 
         # Line position
-        self._line_offset = self.fontMetrics().width(' ' * 79)
+        try:
+            # https://doc.qt.io/qt-5/qfontmetrics.html#horizontalAdvance
+            # Qt 5.5.11 and onwards
+            self._line_offset = self.fontMetrics().horizontalAdvance(' ' * 79)
+        except AttributeError:
+            self._line_offset = self.fontMetrics().width(' ' * 79)
 
         # Number of blocks in page up/down
         self._blocks_per_page = 1
@@ -130,7 +135,7 @@ def cursor_changed(self):
         selection = QtWidgets.QTextEdit.ExtraSelection()
         selection.format.setBackground(COLOR_SELECTED_LINE)
         selection.format.setProperty(
-            QtGui.QTextFormat.FullWidthSelection, True)
+            QtGui.QTextFormat.Property.FullWidthSelection, True)
         selection.cursor = self.textCursor()
         selection.cursor.clearSelection()
         extra_selections.append(selection)
@@ -142,13 +147,14 @@ def cursor_changed(self):
             pos = cursor.position()
             bracket = None
             if not cursor.atEnd():
-                cursor.setPosition(pos + 1, QtGui.QTextCursor.KeepAnchor)
+                cursor.setPosition(
+                    pos + 1, QtGui.QTextCursor.MoveMode.KeepAnchor)
                 text = cursor.selectedText()
                 if text in BRACKETS:
                     bracket = cursor
             elif bracket is None and not cursor.atStart():
                 cursor.setPosition(pos - 1)
-                cursor.setPosition(pos, QtGui.QTextCursor.KeepAnchor)
+                cursor.setPosition(pos, QtGui.QTextCursor.MoveMode.KeepAnchor)
                 text = cursor.selectedText()
                 if text in BRACKETS:
                     bracket = cursor
@@ -162,10 +168,10 @@ def cursor_changed(self):
                     if text in BRACKETS_CLOSE:
                         other = doc.find(
                             text, start - 1,
-                            QtGui.QTextDocument.FindBackward)
+                            QtGui.QTextDocument.FindFlag.FindBackward)
                         match = doc.find(
                             BRACKETS[text], start - 1,
-                            QtGui.QTextDocument.FindBackward)
+                            QtGui.QTextDocument.FindFlag.FindBackward)
                     else:
                         other = doc.find(text, start)
                         match = doc.find(BRACKETS[text], start)
@@ -221,11 +227,20 @@ def _default_settings(self):
         # Set font
         self.setFont(FONT)
         # Set frame
-        self.setFrameStyle(QtWidgets.QFrame.WinPanel | QtWidgets.QFrame.Sunken)
+        self.setFrameStyle(
+            QtWidgets.QFrame.Shape.WinPanel | QtWidgets.QFrame.Shadow.Sunken)
         # Disable wrapping
-        self.setLineWrapMode(self.NoWrap)
+        self.setLineWrapMode(QtWidgets.QPlainTextEdit.LineWrapMode.NoWrap)
         # Set tab width (if ever seen) to 4 spaces
-        self.setTabStopWidth(self.fontMetrics().width(' ' * 4))
+        try:
+            # https://doc.qt.io/qt-5/qtextedit-obsolete.html
+            # https://doc.qt.io/qt-5/qfontmetrics.html#horizontalAdvance
+            # Qt 5.10/5.11 and onwards
+            ts = self.fontMetrics().horizontalAdvance(' ' * 4)
+            self.setTabStopDistance(ts)
+        except AttributeError:
+            ts = self.fontMetrics().width(' ' * 4)
+            self.setTabStopWidth(ts)
 
     def get_text(self):
         """ Returns the text in this editor. """
@@ -241,6 +256,11 @@ def jump_to(self, line, char):
 
     def keyPressEvent(self, event):
         """ Qt event: A key was pressed. """
+        K = Qt.Key
+        KM = Qt.KeyboardModifier
+        MM = QtGui.QTextCursor.MoveMode
+        MO = QtGui.QTextCursor.MoveOperation
+
         # Get key and modifiers
         key = event.key()
         mod = event.modifiers()
@@ -250,11 +270,13 @@ def keyPressEvent(self, event):
         #  MetaModifier (i.e. super key)
         #  KeyPadModifier (button is part of keypad)
         #  GroupSwitchModifier (x11 thing)
+
         # Ignore the keypad modifier, we don't care!
-        if mod & Qt.KeypadModifier:
-            mod = mod ^ Qt.KeypadModifier   # xor!
+        if mod & KM.KeypadModifier:
+            mod = mod ^ KM.KeypadModifier   # xor!
+
         # Actions per key/modifier combination
-        if key == Qt.Key_Tab and mod == Qt.NoModifier:
+        if key == K.Key_Tab and mod == KM.NoModifier:
             # Indent
             cursor = self.textCursor()
             start, end = cursor.selectionStart(), cursor.selectionEnd()
@@ -274,7 +296,7 @@ def keyPressEvent(self, event):
                 pos = cursor.positionInBlock()
                 cursor.insertText((TABS - pos % TABS) * SPACE)
 
-        elif key == Qt.Key_Backtab and mod == Qt.ShiftModifier:
+        elif key == K.Key_Backtab and mod == KM.ShiftModifier:
             # Dedent all lines in selection (or single line if no selection)
             '''
             cursor = self.textCursor()
@@ -294,11 +316,11 @@ def keyPressEvent(self, event):
                 p2 = p1 + min(4, len(t) - len(t.lstrip()))
                 c = self.textCursor()
                 c.setPosition(p1)
-                c.setPosition(p2, QtGui.QTextCursor.KeepAnchor)
+                c.setPosition(p2, MM.KeepAnchor)
                 c.removeSelectedText()
             cursor.endEditBlock()
             '''
-            # This silly method is required because of a bug in qt4/qt5
+            # This silly method is required because of a bug in qt5 (and 6?)
             cursor = self.textCursor()
             start, end = cursor.selectionStart(), cursor.selectionEnd()
             first = self.document().findBlock(start)
@@ -323,22 +345,21 @@ def keyPressEvent(self, event):
                 cursor.beginEditBlock()
                 cursor.setPosition(first.position())
                 cursor.setPosition(
-                    last.position() + last.length() - 1,
-                    QtGui.QTextCursor.KeepAnchor)
+                    last.position() + last.length() - 1, MM.KeepAnchor)
                 cursor.removeSelectedText()
                 cursor.insertText('\n'.join(new_text))
                 cursor.endEditBlock()
                 # Set new cursor
                 cursor.setPosition(new_start)
-                cursor.setPosition(new_end, QtGui.QTextCursor.KeepAnchor)
+                cursor.setPosition(new_end, MM.KeepAnchor)
                 self.setTextCursor(cursor)
 
-        elif key == Qt.Key_Enter or key == Qt.Key_Return:
+        elif key == K.Key_Enter or key == K.Key_Return:
             # Enter/Return with modifier is overruled here to mean nothing
             # This is very important as the default for shift-enter is to
             # start a new line within the same block (this can't happen with
             # copy-pasting, so it's safe to just catch it here).
-            if mod == Qt.NoModifier:
+            if mod == KM.NoModifier:
                 # "Smart" enter:
                 #   - If selection, selection is deleted
                 #   - Else, autoindenting is performed
@@ -360,8 +381,8 @@ def keyPressEvent(self, event):
                 # Scroll if necessary
                 self.ensureCursorVisible()
 
-        elif key == Qt.Key_Home and (
-                mod == Qt.NoModifier or mod == Qt.ShiftModifier):
+        elif key == K.Key_Home and (
+                mod == KM.NoModifier or mod == KM.ShiftModifier):
             # Plain home button: move to start of line
             # If Control is used: Jump to start of document
             # Ordinary home button: Jump to first column or first
@@ -384,25 +405,22 @@ def keyPressEvent(self, event):
                 # Smart up/down:
                 self._last_column = indent
             # If Shift is used: only move position (keep anchor, i.e. select)
-            anchor = (
-                QtGui.QTextCursor.KeepAnchor if mod == Qt.ShiftModifier
-                else QtGui.QTextCursor.MoveAnchor)
-            cursor.setPosition(newpos, anchor)
+            cursor.setPosition(
+                newpos,
+                MM.KeepAnchor if mod == KM.ShiftModifier else MM.MoveAnchor)
             self.setTextCursor(cursor)
 
-        elif key == Qt.Key_Home and (
-                mod == Qt.ControlModifier
-                or mod == Qt.ControlModifier & Qt.ShiftModifier):
+        elif key == K.Key_Home and (
+                mod == KM.ControlModifier
+                or mod == KM.ControlModifier & KM.ShiftModifier):
             # Move to start of document
             # If Shift is used: only move position (keep anchor, i.e. select)
-            anchor = (
-                QtGui.QTextCursor.KeepAnchor if mod == Qt.ShiftModifier
-                else QtGui.QTextCursor.MoveAnchor)
             cursor = self.textCursor()
-            cursor.setPosition(0, anchor)
+            cursor.setPosition(
+                0, MM.KeepAnchor if mod == KM.ShiftModifier else MM.MoveAnchor)
             self.setTextCursor(cursor)
 
-        elif key in (Qt.Key_Up, Qt.Key_Down) and mod == Qt.AltModifier:
+        elif key in (K.Key_Up, K.Key_Down) and mod == KM.AltModifier:
             # Move selected lines up or down
             # Get current selection
             doc = self.document()
@@ -419,7 +437,7 @@ def keyPressEvent(self, event):
                     block2 = block2.previous()  # always valid
             block2 = block1 if start == end else doc.findBlock(end)
             # Check if we can move
-            if key == Qt.Key_Up:
+            if key == K.Key_Up:
                 if not block1.previous().isValid():
                     return
             elif not block2.next().isValid():
@@ -428,30 +446,28 @@ def keyPressEvent(self, event):
             b1pos = block1.position()
             cursor.beginEditBlock()
             cursor.setPosition(b1pos)
-            cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
-            cursor.movePosition(
-                QtGui.QTextCursor.EndOfLine, QtGui.QTextCursor.KeepAnchor)
+            cursor.setPosition(end, MM.KeepAnchor)
+            cursor.movePosition(MO.EndOfLine, MM.KeepAnchor)
             line = cursor.selectedText()
             size = cursor.selectionEnd() - cursor.selectionStart()
             cursor.removeSelectedText()
-            if key == Qt.Key_Up:
+            if key == K.Key_Up:
                 cursor.deletePreviousChar()
-                cursor.movePosition(QtGui.QTextCursor.StartOfLine)
+                cursor.movePosition(MO.StartOfLine)
                 cursor.insertText(line + '\n')
-                cursor.movePosition(QtGui.QTextCursor.Left)
+                cursor.movePosition(MO.Left)
             else:
                 cursor.deleteChar()
-                cursor.movePosition(QtGui.QTextCursor.EndOfLine)
+                cursor.movePosition(MO.EndOfLine)
                 cursor.insertText('\n' + line)
             cursor.endEditBlock()
             # Cursor is at the end of the moved lines.
             # Set moved lines as selection
-            cursor.movePosition(
-                QtGui.QTextCursor.Left, QtGui.QTextCursor.KeepAnchor, size)
+            cursor.movePosition(MO.Left, MM.KeepAnchor, size)
             self.setTextCursor(cursor)
 
-        elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown) \
-                and (mod == Qt.NoModifier or mod == Qt.ShiftModifier):
+        elif key in (K.Key_Up, K.Key_Down, K.Key_PageUp, K.Key_PageDown) \
+                and (mod == KM.NoModifier or mod == KM.ShiftModifier):
             # Move cursor up/down
             # Maintain the column position, even when the current row doesn't
             # have as many characters. Reset this behavior as soon as a
@@ -459,13 +475,12 @@ def keyPressEvent(self, event):
             # changed.
             # Set up operation
             anchor = (
-                QtGui.QTextCursor.KeepAnchor if mod == Qt.ShiftModifier
-                else QtGui.QTextCursor.MoveAnchor)
-            operation = \
-                QtGui.QTextCursor.PreviousBlock if key in (
-                    Qt.Key_Up, Qt.Key_PageUp) else QtGui.QTextCursor.NextBlock
-            n = 1 if key in (Qt.Key_Up, Qt.Key_Down) else (
+                MM.KeepAnchor if mod == KM.ShiftModifier else MM.MoveAnchor)
+            operation = (MO.PreviousBlock if key in (K.Key_Up, K.Key_PageUp)
+                         else MO.NextBlock)
+            n = 1 if key in (K.Key_Up, K.Key_Down) else (
                 self._blocks_per_page - 3)
+
             # Move
             cursor = self.textCursor()
             if self._last_column is None:
@@ -477,22 +492,22 @@ def keyPressEvent(self, event):
             else:
                 # Up/Down beyond document start/end? Move cursor to document
                 # start/end and update last column
-                if operation == QtGui.QTextCursor.NextBlock:
-                    cursor.movePosition(QtGui.QTextCursor.EndOfBlock, anchor)
+                if operation == MO.NextBlock:
+                    cursor.movePosition(MO.EndOfBlock, anchor)
                 else:
-                    cursor.movePosition(QtGui.QTextCursor.StartOfBlock, anchor)
+                    cursor.movePosition(MO.StartOfBlock, anchor)
                 self._last_column = cursor.positionInBlock()
             self.setTextCursor(cursor)
 
-        elif key in (Qt.Key_Left, Qt.Key_Right, Qt.Key_End) and not (
-                mod & Qt.AltModifier):
+        elif key in (K.Key_Left, K.Key_Right, K.Key_End) and not (
+                mod & KM.AltModifier):
             # Allow all modifiers except alt
             # Reset smart up/down behavior
             self._last_column = None
             # Pass to parent class
             super().keyPressEvent(event)
 
-        elif key == Qt.Key_Insert and mod == Qt.NoModifier:
+        elif key == K.Key_Insert and mod == KM.NoModifier:
             # Insert/replace
             self.setOverwriteMode(not self.overwriteMode())
 
@@ -529,11 +544,11 @@ def keyPressEvent(self, event):
             # Shift-Enter           Starts new line within the same block!
             #                       Definitely removed
             # Ctrl-i                Undocumented, but inserts tab...
-            ctrl_ignore = (Qt.Key_K, Qt.Key_I)
-            if mod == Qt.ControlModifier and key in ctrl_ignore:
+            ctrl_ignore = (K.Key_K, K.Key_I)
+            if mod == KM.ControlModifier and key in ctrl_ignore:
                 # Control-K: ignore
                 pass
-            elif key == Qt.Key_Up or key == Qt.Key_Down:
+            elif key == K.Key_Up or key == K.Key_Down:
                 # Up/down with modifiers: ignore
                 pass
             else:
@@ -542,7 +557,13 @@ def keyPressEvent(self, event):
 
     def _line_number_area_width(self):
         """ Returns the required width for the number area. """
-        return 8 + self.fontMetrics().width(str(max(1, self.blockCount())))
+        text = str(max(1, self.blockCount()))
+        try:
+            # https://doc.qt.io/qt-5/qfontmetrics.html#horizontalAdvance
+            # Qt 5.5.11 and onwards
+            return 8 + self.fontMetrics().horizontalAdvance(text)
+        except AttributeError:
+            return 8 + self.fontMetrics().width(text)
 
     def _line_number_area_paint(self, area, event):
         """ Repaints the line number area. """
@@ -572,8 +593,8 @@ def _line_number_area_paint(self, area, event):
         while block.isValid() and btop <= ebot:
             count += 1
             if block.isVisible() and bbot >= etop:
-                painter.drawText(
-                    0, btop, width - 4, height, Qt.AlignRight, str(count))
+                painter.drawText(0, btop, width - 4, height,
+                                 Qt.AlignmentFlag.AlignRight, str(count))
             block = block.next()
             btop = bbot
             bbot += int(self.blockBoundingRect(block).height())
@@ -676,7 +697,8 @@ def toggle_comment(self):
             for block in blocks:
                 p = block.position() + indent
                 cursor.setPosition(p)
-                cursor.setPosition(p + 1, QtGui.QTextCursor.KeepAnchor)
+                cursor.setPosition(
+                    p + 1, QtGui.QTextCursor.MoveMode.KeepAnchor)
                 cursor.removeSelectedText()
         else:
 
@@ -685,7 +707,8 @@ def toggle_comment(self):
                 n = len(block.text())
                 if len(block.text()) < indent:
                     cursor.setPosition(p)
-                    cursor.setPosition(p + n, QtGui.QTextCursor.KeepAnchor)
+                    cursor.setPosition(
+                        p + n, QtGui.QTextCursor.MoveMode.KeepAnchor)
                     cursor.removeSelectedText()
                     cursor.insertText(' ' * indent + '#')
                 else:
@@ -704,8 +727,8 @@ def trim_trailing_whitespace(self):
             b = len(t.rstrip())
             if a > b:
                 cursor.setPosition(block.position() + b)
-                cursor.setPosition(
-                    block.position() + a, QtGui.QTextCursor.KeepAnchor)
+                cursor.setPosition(block.position() + a,
+                                   QtGui.QTextCursor.MoveMode.KeepAnchor)
                 cursor.removeSelectedText()
             block = block.next()
         cursor.endEditBlock()
@@ -788,7 +811,8 @@ def __init__(self, parent, editor):
         text_layout.addWidget(self._search_field, 0, 1)
         text_layout.addWidget(self._replace_label, 1, 0)
         text_layout.addWidget(self._replace_field, 1, 1)
-        check_layout = QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.TopToBottom)
+        check_layout = QtWidgets.QBoxLayout(
+            QtWidgets.QBoxLayout.Direction.TopToBottom)
         check_layout.addWidget(self._case_check)
         check_layout.addWidget(self._whole_check)
         button_layout = QtWidgets.QGridLayout()
@@ -796,7 +820,8 @@ def __init__(self, parent, editor):
         button_layout.addWidget(self._replace_button, 0, 2)
         button_layout.addWidget(self._find_button, 0, 3)
 
-        layout = QtWidgets.QBoxLayout(QtWidgets.QBoxLayout.TopToBottom)
+        layout = QtWidgets.QBoxLayout(
+            QtWidgets.QBoxLayout.Direction.TopToBottom)
         layout.addLayout(text_layout)
         layout.addLayout(check_layout)
         layout.addLayout(button_layout)
@@ -813,11 +838,11 @@ def action_find(self):
         if query == '':
             self.find_action.emit('No query set')
             return
-        flags = 0x0
+        flags = QtGui.QTextDocument.FindFlag(0)
         if self._case_check.isChecked():
-            flags |= QtGui.QTextDocument.FindCaseSensitively
+            flags |= QtGui.QTextDocument.FindFlag.FindCaseSensitively
         if self._whole_check.isChecked():
-            flags |= QtGui.QTextDocument.FindWholeWords
+            flags |= QtGui.QTextDocument.FindFlag.FindWholeWords
         if flags:
             found = self._editor.find(query, flags)
         else:
@@ -866,11 +891,11 @@ def action_replace_all(self):
         if query == '':
             self.find_action.emit('No query set')
             return
-        flags = 0x0
+        flags = QtGui.QTextDocument.FindFlag(0)
         if self._case_check.isChecked():
-            flags |= QtGui.QTextDocument.FindCaseSensitively
+            flags |= QtGui.QTextDocument.FindFlag.FindCaseSensitively
         if self._whole_check.isChecked():
-            flags |= QtGui.QTextDocument.FindWholeWords
+            flags |= QtGui.QTextDocument.FindFlag.FindWholeWords
         n = 0
         found = True
         scrollpos = self._editor.verticalScrollBar().value()
@@ -916,7 +941,7 @@ def activate(self):
     def keyPressEvent(self, event):
         """ Qt event: A key-press reaches the widget. """
         key = event.key()
-        if key == Qt.Key_Enter or key == Qt.Key_Return:
+        if key == Qt.Key.Key_Enter or key == Qt.Key.Key_Return:
             self.action_find()
         else:
             super().keyPressEvent(event)
@@ -958,141 +983,150 @@ def __init__(self, document):
         super().__init__(document)
 
         # Expressions used to find strings & comments
-        self._string_start = QtCore.QRegExp(r'"""')
-        self._string_stop = QtCore.QRegExp(r'"""')
-        self._comment_start = QtCore.QRegExp(r'#[^\n]*')
+        R = QtCore.QRegularExpression
+        self._string = R(r'"""')
 
         # Headers
         name = r'[a-zA-Z]+[a-zA-Z0-9_]*'
-        self._rule_head = QtCore.QRegExp(r'^(\s*)\[{1,2}' + name + '\]{1,2}')
+        self._rule_head = R(r'^\s*(\[{1,2}' + name + '\]{1,2})')
 
         # Simple rules
         self._rules = []
 
         # Numbers
-        pattern = QtCore.QRegExp(r'\b[+-]?[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?\b')
+        pattern = R(r'\b[+-]?[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?\b')
         self._rules.append((pattern, STYLE_LITERAL))
-        unit = r'\[[a-zA-Z0-9/^-*]+\]'
-        self._rules.append((QtCore.QRegExp(unit), STYLE_INLINE_UNIT))
+        unit = r'\[([a-zA-Z0-9/^-]|\*)+\]'
+        self._rules.append((R(unit), STYLE_INLINE_UNIT))
 
         # Keywords
         for keyword in self.KEYWORD_1:
-            pattern = QtCore.QRegExp(r'\b' + keyword + r'\b')
-            self._rules.append((pattern, STYLE_KEYWORD_1))
+            self._rules.append((R(r'\b' + keyword + r'\b'), STYLE_KEYWORD_1))
         for keyword in self.KEYWORD_2:
-            pattern = QtCore.QRegExp(r'\b' + keyword + r'\b')
-            self._rules.append((pattern, STYLE_KEYWORD_2))
+            self._rules.append((R(r'\b' + keyword + r'\b'), STYLE_KEYWORD_2))
 
         # Meta-data coloring
         self._rules_labels = [
-            QtCore.QRegExp(r'(\s*)(bind)\s+(' + name + ')'),
-            QtCore.QRegExp(r'(\s*)(label)\s+(' + name + ')'),
+            R(r'(\s*)(bind)\s+(' + name + ')'),
+            R(r'(\s*)(label)\s+(' + name + ')'),
         ]
-        self._rule_meta_key = QtCore.QRegExp(name + r':')
-        self._rule_meta_val = QtCore.QRegExp(r':(\s*)(.+)')
-        self._rule_var_unit = QtCore.QRegExp(r'^(\s*)(in)(\s*)(' + unit + ')')
+        self._rule_meta = R(r'^\s*(' + name + r':)(\s*)(.+)')
+        self._rule_var_unit = R(r'^(\s*)(in)(\s*)(' + unit + ')')
 
-        # Comments
-        # Note: For some reason this can _not_ use self._comment_start
-        self._comments = QtCore.QRegExp(r'#[^\n]*')
+        # Comment
+        self._comment = R(r'#')
+
+    def _highlight_ok(self, strings, start, length):
+        """ Checks if the string ``start`` to ``length`` needs formatted. """
+        for lo, hi in strings:
+            if lo <= start < hi or lo <= start + length < hi:
+                return False
+        return True
 
     def highlightBlock(self, text):
         """ Qt: Called whenever a block should be highlighted. """
-        # Simple rules
-        for (pattern, style) in self._rules:
-            i = pattern.indexIn(text)
-            while i >= 0:
-                # Note: Can't use matchedLength() here because it does quirky
-                # things with the subgroup for the numbers regex.
-                n = len(pattern.cap(0))
-                self.setFormat(i, n, style)
-                i = pattern.indexIn(text, i + n)
-
-        # Model and component headers
-        i = self._rule_head.indexIn(text)
-        while i >= 0:
-            m = len(self._rule_head.cap(1))
-            n = len(self._rule_head.cap(0))
-            self.setFormat(i + m, n - m, STYLE_HEADER)
-            i = self._rule_head.indexIn(text, i + n)
-
-        # Annotations
-        # Labels
-        for pattern in self._rules_labels:
-            i = pattern.indexIn(text)
-            while i >= 0:
-                n0 = len(pattern.cap(0))
-                n1 = len(pattern.cap(1))
-                n2 = len(pattern.cap(2))
-                n3 = len(pattern.cap(3))
-                self.setFormat(i + n1, n2, STYLE_ANNOT_KEY)
-                self.setFormat(i + n0 - n3, n3, STYLE_ANNOT_VAL)
-                i = pattern.indexIn(text, i + n0)
-
-        # Units
-        i = self._rule_var_unit.indexIn(text)
-        if i == 0:
-            n1 = len(self._rule_var_unit.cap(1))
-            n2 = len(self._rule_var_unit.cap(2))
-            n3 = len(self._rule_var_unit.cap(3))
-            n4 = len(self._rule_var_unit.cap(4))
-            self.setFormat(n1, n2, STYLE_ANNOT_KEY)
-            self.setFormat(n1 + n2 + n3, n4, STYLE_ANNOT_VAL)
-
-        # Meta properties
-        i = self._rule_meta_key.indexIn(text)
-        if i >= 0:
-            n0 = len(self._rule_meta_key.cap(0))
-            self.setFormat(i, n0, STYLE_ANNOT_KEY)
-        i = self._rule_meta_val.indexIn(text)
-        if i >= 0:
-            n1 = len(self._rule_meta_val.cap(1))
-            n2 = len(self._rule_meta_val.cap(2))
-            self.setFormat(i, 1, STYLE_ANNOT_KEY)
-            self.setFormat(1 + i + n1, n2, STYLE_ANNOT_VAL)
-
-        # Comments (overrule all other formatting except multi-line strings)
-        i = self._comments.indexIn(text)
-        while i >= 0:
-            n = len(self._comments.cap(0))
-            self.setFormat(i, n, STYLE_COMMENT)
-            i = self._comments.indexIn(text, i + n)
-
-        # Multi-line strings
-        # Block states:
-        #  0 Normal
-        #  1 Multi-line string (meta)
+
+        # To avoid formatting within strings each is stored as a (start, end).
+        strings = []
+        # If the start has been handled, set the offset.
+        offset = 0
+        # If the end has been handled, chop it off the string
+
+        # Multi-line strings are done first, because they overrule a lot of
+        # things and we can skip formatting if we're inside one.
+        # Block states: 0=No string, 1=A """ string
         self.setCurrentBlockState(0)
 
-        # Check state of previous block
-        if self.previousBlockState() != 1:
-            # Normal block
-            next = start = self._string_start.indexIn(text)
-            comment = self._comment_start.indexIn(text)
-            if next >= 0 and (comment < 0 or comment > next):
-                next += self._string_start.matchedLength()
+        # Continuing a multi-line string?
+        if self.previousBlockState() == 1:
+            # Search for string stop
+            ms = self._string.match(text)
+            if ms.hasMatch():
+                # Terminate the multi-line string
+                offset = ms.capturedEnd(0)
+                self.setFormat(0, offset, STYLE_ANNOT_VAL)
             else:
-                start = next = -1
+                # Whole line in the string
+                self.setCurrentBlockState(1)
+                self.setFormat(0, len(text), STYLE_ANNOT_VAL)
+                return
         else:
-            start = next = 0
+            # Search for string start
+            ms = self._string.match(text)
+            if ms.hasMatch():
+                # Potential start, but check that it's not commented out
+                start = ms.capturedStart()
+                mc = self._comment.match(text)
+                if not (mc.hasMatch() and mc.capturedStart() < start):
+                    # Definitely a string start. See if it ends on this line
+                    me = self._string.match(text, offset=ms.capturedEnd())
+                    if me.hasMatch():
+                        # Terminate the single-line string
+                        end = me.capturedEnd()
+                        self.setFormat(start, end - start, STYLE_ANNOT_VAL)
+                        strings.append((start, end))
+                    else:
+                        # Multi-line string
+                        self.setCurrentBlockState(1)
+                        self.setFormat(start, len(text), STYLE_ANNOT_VAL)
+
+        # Comment
+        i = self._comment.globalMatch(text, offset=offset)
+        while i.hasNext():
+            m = i.next()
+            x = m.capturedStart()
+            if self._highlight_ok(strings, x, 1):
+                self.setFormat(x, len(text) - x, STYLE_COMMENT)
+                text = text[:x]
+                break
 
-        # Find any occurrences of string start / stop
-        while next >= 0:
-            stop = self._string_stop.indexIn(text, next)
-            if stop < 0:
-                # Continuing multi-line string
-                self.setCurrentBlockState(1)
-                self.setFormat(start, len(text) - start, STYLE_ANNOT_VAL)
-                next = -1
-            else:
-                # Ending single-line or multi-line string
-                # Format until stop token
-                next = stop = stop + self._string_stop.matchedLength()
-                self.setFormat(start, stop - start, STYLE_ANNOT_VAL)
-                # Find next string in block, if any
-                next = start = self._string_start.indexIn(text, next)
-                if next >= 0:
-                    next += self._string_start.matchedLength()
+        # Rule-based formatting
+        for (pattern, style) in self._rules:
+            i = pattern.globalMatch(text, offset=offset)
+            while i.hasNext():
+                m = i.next()
+                x, w = m.capturedStart(), m.capturedLength()
+                if self._highlight_ok(strings, x, w):
+                    self.setFormat(x, w, style)
+
+        # Model and component headers (must be at start of string)
+        if offset == 0:
+            m = self._rule_head.match(text)
+            if m.hasMatch():
+                x, w = m.capturedStart(1), m.capturedLength(1)
+                self.setFormat(x, w, STYLE_HEADER)
+
+        # Variable units (must be at start of string)
+        if offset == 0:
+            m = self._rule_var_unit.match(text)
+            if m.hasMatch():
+                self.setFormat(
+                    m.capturedStart(2), m.capturedLength(2), STYLE_ANNOT_KEY)
+                self.setFormat(
+                    m.capturedStart(4), m.capturedLength(4), STYLE_ANNOT_VAL)
+
+        # Binds and labels
+        for pattern in self._rules_labels:
+            i = pattern.globalMatch(text, offset=offset)
+            while i.hasNext():
+                m = i.next()
+                x, w = m.capturedStart(), m.capturedLength()
+                if self._highlight_ok(strings, x, w):
+                    self.setFormat(m.capturedStart(2), m.capturedLength(2),
+                                   STYLE_ANNOT_KEY)
+                    self.setFormat(m.capturedStart(3), m.capturedLength(3),
+                                   STYLE_ANNOT_VAL)
+
+        # Meta properties (must be at start of string)
+        if offset == 0:
+            m = self._rule_meta.match(text)
+            if m.hasMatch():
+                self.setFormat(
+                    m.capturedStart(1), m.capturedLength(1), STYLE_ANNOT_KEY)
+                # Don't reformat strings (or bits after string end!)
+                if m.captured(3)[:3] != '"""':
+                    self.setFormat(m.capturedStart(3), m.capturedLength(3),
+                                   STYLE_ANNOT_VAL)
 
 
 class ProtocolHighlighter(QtGui.QSyntaxHighlighter):
@@ -1103,40 +1137,37 @@ def __init__(self, document):
         super().__init__(document)
 
         # Headers and units
-        self._rule_head = QtCore.QRegExp(r'^(\s*)\[\[[a-zA-Z0-9_]+\]\]')
+        R = QtCore.QRegularExpression
+        self._rule_head = R(r'^\s*(\[\[[a-zA-Z0-9_]+\]\])')
 
         # Highlighting rules
         self._rules = []
 
         # Numbers
-        pattern = QtCore.QRegExp(r'\b[+-]?[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?\b')
-        self._rules.append((pattern, STYLE_LITERAL))
+        self._rules.append(
+            (R(r'\b[+-]?[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?\b'), STYLE_LITERAL))
 
         # Keyword "next"
-        pattern = QtCore.QRegExp(r'\bnext\b')
-        self._rules.append((pattern, STYLE_KEYWORD_1))
+        self._rules.append((R(r'\bnext\b'), STYLE_KEYWORD_1))
 
         # Comments
-        pattern = QtCore.QRegExp(r'#[^\n]*')
-        self._rules.append((pattern, STYLE_COMMENT))
+        self._rules.append((R(r'#[^\n]*'), STYLE_COMMENT))
 
     def highlightBlock(self, text):
         """ Qt: Called whenever a block should be highlighted. """
+
         # Rule based formatting
         for (pattern, style) in self._rules:
-            i = pattern.indexIn(text)
-            while i >= 0:
-                n = len(pattern.cap(0))
-                self.setFormat(i, n, style)
-                i = pattern.indexIn(text, i + n)
+            i = pattern.globalMatch(text)
+            while i.hasNext():
+                m = i.next()
+                self.setFormat(m.capturedStart(), m.capturedLength(), style)
 
-        # Protocol header
-        i = self._rule_head.indexIn(text)
-        while i >= 0:
-            m = len(self._rule_head.cap(1))
-            n = len(self._rule_head.cap(0))
-            self.setFormat(i + m, n - m, STYLE_HEADER)
-            i = self._rule_head.indexIn(text, i + n)
+        # Protocol header (must be at strart of string)
+        m = self._rule_head.match(text)
+        if m.hasMatch():
+            self.setFormat(
+                m.capturedStart(1), m.capturedLength(1), STYLE_HEADER)
 
 
 class ScriptHighlighter(QtGui.QSyntaxHighlighter):
@@ -1146,8 +1177,9 @@ class ScriptHighlighter(QtGui.QSyntaxHighlighter):
     def __init__(self, document):
         super().__init__(document)
 
-        # Headers and units
-        self._rule_head = QtCore.QRegExp(r'^(\s*)\[\[[a-zA-Z0-9_]+\]\]')
+        # Script header
+        R = QtCore.QRegularExpression
+        self._rule_head = R(r'^\s*(\[\[[a-zA-Z0-9_]+\]\])')
 
         # Highlighting rules
         self._rules = []
@@ -1155,119 +1187,124 @@ def __init__(self, document):
         # Keywords
         import keyword
         for kw in keyword.kwlist:
-            pattern = QtCore.QRegExp(r'\b' + kw + r'\b')
-            self._rules.append((pattern, STYLE_KEYWORD_1))
+            self._rules.append((R(r'\b' + kw + r'\b'), STYLE_KEYWORD_1))
 
         # Built-in essential functions
         for func in _PYFUNC:
-            pattern = QtCore.QRegExp(r'\b' + str(func) + r'\b')
-            self._rules.append((pattern, STYLE_KEYWORD_2))
+            self._rules.append((R(r'\b' + str(func) + r'\b'), STYLE_KEYWORD_2))
 
         # Literals: numbers, True, False, None
         # Override some keywords
-        pattern = QtCore.QRegExp(r'\b[+-]?[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?\b')
-        self._rules.append((pattern, STYLE_LITERAL))
-        pattern = QtCore.QRegExp(r'\bTrue\b')
-        self._rules.append((pattern, STYLE_LITERAL))
-        pattern = QtCore.QRegExp(r'\bFalse\b')
-        self._rules.append((pattern, STYLE_LITERAL))
-        pattern = QtCore.QRegExp(r'\bNone\b')
-        self._rules.append((pattern, STYLE_LITERAL))
+        self._rules.append((R(r'\b[+-]?[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?\b'),
+                            STYLE_LITERAL))
+        self._rules.append((R(r'\bTrue\b'), STYLE_LITERAL))
+        self._rules.append((R(r'\bFalse\b'), STYLE_LITERAL))
+        self._rules.append((R(r'\bNone\b'), STYLE_LITERAL))
 
         # Strings
-        pattern = QtCore.QRegExp(r'"([^"\\]|\\")*"')
-        self._rules.append((pattern, STYLE_LITERAL))
-        pattern = QtCore.QRegExp(r"'([^'\\]|\\')*'")
-        self._rules.append((pattern, STYLE_LITERAL))
-
-        # Multi-line strings
-        self._string1 = QtCore.QRegExp(r"'''")
-        self._string2 = QtCore.QRegExp(r'"""')
+        self._s1 = R(r'"')
+        self._s2 = R(r"'")
+        self._ms1 = R(r'"""')
+        self._ms2 = R(r"'''")
+        self._s_start = R(r'"""|\'\'\'|"|\'')
+        self._s_end = {
+            '"': self._s1, "'": self._s2, '"""': self._ms1, "'''": self._ms2}
 
         # Comments
-        pattern = QtCore.QRegExp(r'#[^\n]*')
-        self._rules.append((pattern, STYLE_COMMENT))
+        self._comment = R(r'#')
+
+    def _highlight_ok(self, strings, start, length):
+        """ Checks if the string ``start`` to ``length`` needs formatted. """
+        for lo, hi in strings:
+            if lo <= start < hi or lo <= start + length < hi:
+                return False
+        return True
 
     def highlightBlock(self, text):
         """ Qt: Called whenever a block should be highlighted. """
-        # Rule based formatting
-        for (pattern, style) in self._rules:
-            i = pattern.indexIn(text)
-            while i >= 0:
-                # Note: Can't use matchedLength() here because it does quirky
-                # things with the subgroup for the numbers regex.
-                n = len(pattern.cap(0))
-                self.setFormat(i, n, style)
-                i = pattern.indexIn(text, i + n)
 
-        # Script header
-        i = self._rule_head.indexIn(text)
-        while i >= 0:
-            m = len(self._rule_head.cap(1))
-            n = len(self._rule_head.cap(0))
-            self.setFormat(i + m, n - m, STYLE_HEADER)
-            i = self._rule_head.indexIn(text, i + n)
-
-        # Block states:
-        #  0 Normal
-        #  1 Multi-line string 1
-        #  2 Multi-line string 2
+        # To avoid formatting within strings each is stored as a (start, end).
+        strings = []
+        # If the start has been handled, set the offset.
+        offset = 0
+        # If the end has been handled, chop it off the string
+
+        # Multi-line strings are done first, because they overrule a lot of
+        # things and we can skip formatting if we're inside one.
+        # Block states: 0=No string, 1=A " " " string, 2=A ' ' ' string
         self.setCurrentBlockState(0)
 
-        # Multi-line formats
-        def find_start(text, next):
-            s1 = self._string1.indexIn(text, next)
-            s2 = self._string2.indexIn(text, next)
-            if s1 < 0 and s2 < 0:
-                current = 0
-                start = -1
-                next = -1
-            else:
-                if s1 >= 0 and s2 >= 0:
-                    current = 1 if s1 < s2 else 2
-                    start = min(s1, s2)
-                elif s1 >= 0:
-                    current = 1
-                    start = s1
-                else:   # (s2 >= 0)
-                    current = 2
-                    start = s2
-                next = start + 3
-
-                # Check we're not in a comment
-                i = text.rfind('\n', start) + 1
-                if text[i:i + 1] == '#':
-                    current = 0
-                    start = next = -1
-
-            return current, start, next
-
-        # Check state of previous block
+        # Continuing a multi-line string?
         previous = self.previousBlockState()
         if previous == 1 or previous == 2:
-            current, start, next = previous, 0, 0
-        else:
-            current, start, next = find_start(text, 0)
-
-        # Find any occurrences of string start / stop
-        while next >= 0:
-            if current == 1:
-                stop = self._string1.indexIn(text, next)
+            # Search for string stop
+            r = self._ms1 if previous == 1 else self._ms2
+            ms = r.match(text)
+            if ms.hasMatch():
+                # Terminate the multi-line string, and increase global offset
+                offset = ms.capturedEnd(0)
+                self.setFormat(0, offset, STYLE_LITERAL)
             else:
-                stop = self._string2.indexIn(text, next)
-            if stop < 0:
-                # Continuing multi-line string
-                self.setCurrentBlockState(current)
-                self.setFormat(start, len(text) - start, STYLE_LITERAL)
-                next = -1
+                # Whole line in the string
+                self.setCurrentBlockState(previous)
+                self.setFormat(0, len(text), STYLE_LITERAL)
+                return
+
+        # Search for string starts (single or multi-line)
+        stroff = offset     # Offset for string start/end searching
+        m1 = self._s_start.match(text, offset=stroff)
+        while m1.hasMatch():
+            stroff = m1.capturedEnd()
+            start = m1.capturedStart()
+            # Are we in a comment?
+            mc = self._comment.match(text)
+            if (mc.hasMatch() and mc.capturedStart() < start):
+                # No point searching for further string starts
+                stroff = len(text)
             else:
-                # Ending single-line or multi-line string
-                # Format until stop token
-                stop += 3
-                self.setFormat(start, stop - start, STYLE_LITERAL)
-                # Find next string in block, if any
-                next = stop + 3
-                current, start, next = find_start(text, next)
+                # Find string end
+                m2 = self._s_end[m1.captured()].match(text, offset=stroff)
+                if m2.hasMatch():
+                    stroff = m2.capturedEnd()
+                    # Ignore if escaped
+                    if text[m2.capturedStart() - 1] != '\\':
+                        # Terminate the single line string and move on
+                        self.setFormat(start, stroff - start, STYLE_LITERAL)
+                        strings.append((start, stroff))
+                elif m1.capturedLength() > 1:
+                    # Multi-line string start. Block finished!
+                    self.setCurrentBlockState(
+                        1 if m1.captured() == '"""' else 2)
+                    self.setFormat(start, len(text) - start, STYLE_LITERAL)
+                    return
+                # No Match? Then not a string so ignore and continue
+            m1 = self._s_start.match(text, offset=stroff)
+
+        # Comment
+        i = self._comment.globalMatch(text, offset=offset)
+        while i.hasNext():
+            m = i.next()
+            x = m.capturedStart()
+            if self._highlight_ok(strings, x, 1):
+                self.setFormat(x, len(text) - x, STYLE_COMMENT)
+                text = text[:x]
+                break
+
+        # Script header (must be at start of string)
+        if offset == 0:
+            m = self._rule_head.match(text)
+            if m.hasMatch():
+                self.setFormat(
+                    m.capturedStart(1), m.capturedLength(1), STYLE_HEADER)
+
+        # Rule based formatting
+        for (pattern, style) in self._rules:
+            i = pattern.globalMatch(text, offset=offset)
+            while i.hasNext():
+                m = i.next()
+                x, w = m.capturedStart(), m.capturedLength()
+                if self._highlight_ok(strings, x, w):
+                    self.setFormat(x, w, style)
 
 
 # List of essential built-in python functions
diff --git a/setup.py b/setup.py
index 3b750f687..122461f0c 100644
--- a/setup.py
+++ b/setup.py
@@ -103,9 +103,9 @@
             'scipy',            # Used in data log, and data log viewer
             'sympy',            # Used in formats.sympy
         ],
-        'gui': ['pyqt5', 'sip'],
-        'pyqt': ['pyqt5', 'sip'],
-        'pyside': ['pyside2'],
+        'gui': ['pyqt6', 'sip'],
+        'pyqt': ['pyqt6', 'sip'],
+        'pyside': ['pyside6'],
     },
 
     # Unit tests


Test patch:
diff --git a/myokit/tests/test_config.py b/myokit/tests/test_config.py
index 201ab9693..72a858df5 100755
--- a/myokit/tests/test_config.py
+++ b/myokit/tests/test_config.py
@@ -68,10 +68,10 @@ def setUpClass(cls):
         cls._compat_no_fd_capture = myokit.COMPAT_NO_FD_CAPTURE
         cls._date_format = myokit.DATE_FORMAT
         cls._time_format = myokit.TIME_FORMAT
-        cls._force_pyside = myokit.FORCE_PYSIDE
-        cls._force_pyside2 = myokit.FORCE_PYSIDE2
-        cls._force_pyqt4 = myokit.FORCE_PYQT4
+        cls._force_pyqt6 = myokit.FORCE_PYQT6
         cls._force_pyqt5 = myokit.FORCE_PYQT5
+        cls._force_pyside6 = myokit.FORCE_PYSIDE6
+        cls._force_pyside2 = myokit.FORCE_PYSIDE2
         cls._sundials_lib = myokit.SUNDIALS_LIB
         cls._sundials_inc = myokit.SUNDIALS_INC
         cls._opencl_lib = myokit.OPENCL_LIB
@@ -100,10 +100,10 @@ def tearDownClass(cls):
         myokit.COMPAT_NO_FD_CAPTURE = cls._compat_no_fd_capture
         myokit.DATE_FORMAT = cls._date_format
         myokit.TIME_FORMAT = cls._time_format
-        myokit.FORCE_PYSIDE = cls._force_pyside
-        myokit.FORCE_PYSIDE2 = cls._force_pyside2
-        myokit.FORCE_PYQT4 = cls._force_pyqt4
+        myokit.FORCE_PYQT6 = cls._force_pyqt6
         myokit.FORCE_PYQT5 = cls._force_pyqt5
+        myokit.FORCE_PYSIDE6 = cls._force_pyside6
+        myokit.FORCE_PYSIDE2 = cls._force_pyside2
         myokit.SUNDIALS_LIB = cls._sundials_lib
         myokit.SUNDIALS_INC = cls._sundials_inc
         myokit.OPENCL_LIB = cls._opencl_lib
@@ -117,10 +117,10 @@ def tearDownClass(cls):
         assert myokit.COMPAT_NO_FD_CAPTURE == cls._compat_no_fd_capture
         assert myokit.DATE_FORMAT != 'TEST_DATE_FORMAT'
         assert myokit.TIME_FORMAT != 'TEST_TIME_FORMAT'
-        assert myokit.FORCE_PYSIDE == cls._force_pyside
-        assert myokit.FORCE_PYSIDE2 == cls._force_pyside2
-        assert myokit.FORCE_PYQT4 == cls._force_pyqt4
+        assert myokit.FORCE_PYQT6 == cls._force_pyqt6
         assert myokit.FORCE_PYQT5 == cls._force_pyqt5
+        assert myokit.FORCE_PYSIDE6 == cls._force_pyside6
+        assert myokit.FORCE_PYSIDE2 == cls._force_pyside2
         assert myokit.SUNDIALS_LIB != ['one', 'two']
         assert myokit.SUNDIALS_INC != ['three', 'four']
         assert myokit.OPENCL_LIB != ['five', 'six']
@@ -154,17 +154,17 @@ def test_load_basic(self):
         myokit.SUNDIALS_INC = []
         myokit.OPENCL_LIB = []
         myokit.OPENCL_INC = []
-        myokit.FORCE_PYSIDE = myokit.FORCE_PYSIDE2 = False
-        myokit.FORCE_PYQT4 = myokit.FORCE_PYQT5 = False
+        myokit.FORCE_PYSIDE6 = myokit.FORCE_PYSIDE2 = False
+        myokit.FORCE_PYQT6 = myokit.FORCE_PYQT5 = False
         with open(self._temp_dir.path('myokit.ini'), 'w') as f:
             f.write(config_basic)
         self._config_module._load()
         self.assertEqual(myokit.DATE_FORMAT, 'TEST_DATE_FORMAT')
         self.assertEqual(myokit.TIME_FORMAT, 'TEST_TIME_FORMAT')
-        self.assertFalse(myokit.FORCE_PYSIDE)
-        self.assertFalse(myokit.FORCE_PYSIDE2)
-        self.assertFalse(myokit.FORCE_PYQT4)
+        self.assertFalse(myokit.FORCE_PYQT6)
         self.assertFalse(myokit.FORCE_PYQT5)
+        self.assertFalse(myokit.FORCE_PYSIDE6)
+        self.assertFalse(myokit.FORCE_PYSIDE2)
         self.assertEqual(myokit.SUNDIALS_LIB, ['one', 'two'])
         self.assertEqual(myokit.SUNDIALS_INC, ['three', 'four'])
         self.assertEqual(myokit.OPENCL_LIB, ['five', 'six'])
@@ -205,36 +205,36 @@ def test_load_gui_backend(self):
 
         # Qt gui options
         with open(self._temp_dir.path('myokit.ini'), 'w') as f:
-            f.write('[gui]\nbackend=pyqt4\n')
+            f.write('[gui]\nbackend=pyqt6\n')
         self._config_module._load()
-        self.assertFalse(myokit.FORCE_PYSIDE)
-        self.assertFalse(myokit.FORCE_PYSIDE2)
-        self.assertTrue(myokit.FORCE_PYQT4)
+        self.assertTrue(myokit.FORCE_PYQT6)
         self.assertFalse(myokit.FORCE_PYQT5)
+        self.assertFalse(myokit.FORCE_PYSIDE6)
+        self.assertFalse(myokit.FORCE_PYSIDE2)
 
         with open(self._temp_dir.path('myokit.ini'), 'w') as f:
             f.write('[gui]\nbackend=pyqt5\n')
         self._config_module._load()
-        self.assertFalse(myokit.FORCE_PYSIDE)
-        self.assertFalse(myokit.FORCE_PYSIDE2)
-        self.assertFalse(myokit.FORCE_PYQT4)
+        self.assertFalse(myokit.FORCE_PYQT6)
         self.assertTrue(myokit.FORCE_PYQT5)
+        self.assertFalse(myokit.FORCE_PYSIDE6)
+        self.assertFalse(myokit.FORCE_PYSIDE2)
 
         with open(self._temp_dir.path('myokit.ini'), 'w') as f:
-            f.write('[gui]\nbackend=pyside\n')
+            f.write('[gui]\nbackend=pyside6\n')
         self._config_module._load()
-        self.assertTrue(myokit.FORCE_PYSIDE)
-        self.assertFalse(myokit.FORCE_PYSIDE2)
-        self.assertFalse(myokit.FORCE_PYQT4)
+        self.assertFalse(myokit.FORCE_PYQT6)
         self.assertFalse(myokit.FORCE_PYQT5)
+        self.assertTrue(myokit.FORCE_PYSIDE6)
+        self.assertFalse(myokit.FORCE_PYSIDE2)
 
         with open(self._temp_dir.path('myokit.ini'), 'w') as f:
             f.write('[gui]\nbackend=pyside2\n')
         self._config_module._load()
-        self.assertFalse(myokit.FORCE_PYSIDE)
-        self.assertTrue(myokit.FORCE_PYSIDE2)
-        self.assertFalse(myokit.FORCE_PYQT4)
+        self.assertFalse(myokit.FORCE_PYQT6)
         self.assertFalse(myokit.FORCE_PYQT5)
+        self.assertFalse(myokit.FORCE_PYSIDE6)
+        self.assertTrue(myokit.FORCE_PYSIDE2)
 
         # Empty has no effect
         with open(self._temp_dir.path('myokit.ini'), 'w') as f:
@@ -242,10 +242,10 @@ def test_load_gui_backend(self):
         with WarningCollector() as c:
             self._config_module._load()
         self.assertEqual(c.text(), '')
-        self.assertFalse(myokit.FORCE_PYSIDE)
-        self.assertTrue(myokit.FORCE_PYSIDE2)
-        self.assertFalse(myokit.FORCE_PYQT4)
+        self.assertFalse(myokit.FORCE_PYQT6)
         self.assertFalse(myokit.FORCE_PYQT5)
+        self.assertFalse(myokit.FORCE_PYSIDE6)
+        self.assertTrue(myokit.FORCE_PYSIDE2)
 
         # Invalid raises warning
         with open(self._temp_dir.path('myokit.ini'), 'w') as f: