[vlc-commits] [Git][videolan/vlc][master] 4 commits: qml: provide common FSM implementation

Jean-Baptiste Kempf (@jbk) gitlab at videolan.org
Thu Jul 6 13:46:58 UTC 2023



Jean-Baptiste Kempf pushed to branch master at VideoLAN / VLC


Commits:
4cb4e204 by Pierre Lamot at 2023-07-06T13:14:10+00:00
qml: provide common FSM implementation

this provides an implementation for Hierarchical State Machine in QML/JS.

why not qt implementation:

This avoid requiring an extra dependency

QtQml.StateMachine implementation is quite buggy: no being able to pass complex
types through guard and actions is really limiting.

Qt SCXML implementation requires another language for declaring FSM and would
be tedious to integrate with meson.

what's missing:

* no timeout transition: right now you can start/stop a Timer instance in your
  enter/exit callbacks, it's more verbose but it felt unnecessary to add a
  special implementation for it.

- - - - -
e8ade651 by Pierre Lamot at 2023-07-06T13:14:10+00:00
qt: integrate qml unit tests in meson & autotools

- - - - -
e0f52ae2 by Pierre Lamot at 2023-07-06T13:14:10+00:00
qml: add unit tests for FSM implementation

- - - - -
85c48fb8 by Pierre Lamot at 2023-07-06T13:14:10+00:00
qml: use Util.FSM implementation

- - - - -


11 changed files:

- configure.ac
- modules/gui/qt/Makefile.am
- modules/gui/qt/meson.build
- modules/gui/qt/player/qml/Player.qml
- modules/gui/qt/player/qml/PlayerPlaylistVisibilityFSM.qml
- modules/gui/qt/player/qml/SliderBar.qml
- + modules/gui/qt/tests/qml_test.cpp
- + modules/gui/qt/tests/tst_FSM.qml
- + modules/gui/qt/util/qml/FSM.qml
- + modules/gui/qt/util/qml/FSMState.qml
- modules/gui/qt/vlc.qrc


Changes:

=====================================
configure.ac
=====================================
@@ -3951,6 +3951,7 @@ AC_ARG_ENABLE([qt],
 ])
 have_qt5_x11="no"
 have_qt5_gtk="no"
+have_qt5_quick_test="no"
 AS_IF([test "${enable_qt}" != "no"], [
   PKG_CHECK_MODULES([QT], [Qt5Core >= 5.12.0 Qt5Widgets Qt5Gui Qt5Quick Qt5QuickWidgets Qt5QuickControls2 Qt5Svg], [
       PKG_CHECK_MODULES([QT5_X11], [Qt5X11Extras], [
@@ -3965,6 +3966,11 @@ AS_IF([test "${enable_qt}" != "no"], [
           AC_MSG_WARN([Not building Qt Interface with wayland support.])
       ])
 
+      PKG_CHECK_MODULES([QT5_QUICK_TEST], [Qt5QuickTest], [
+          have_qt5_quick_test="yes"
+      ],[
+      ])
+
       QT_PATH="$(eval $PKG_CONFIG --variable=exec_prefix Qt5Core)"
       QT_HOST_PATH="$(eval $PKG_CONFIG --variable=host_bins Qt5Core)"
       QT_INCLUDE_DIRECTORY="$(eval $PKG_CONFIG --variable=includedir Qt5Core)"
@@ -4069,6 +4075,7 @@ AM_CONDITIONAL([HAVE_QT5_X11], [test "${have_qt5_x11}" = "yes"])
 AM_CONDITIONAL([HAVE_QT5_WAYLAND], [test "${have_qt5_wayland}" = "yes"])
 AM_CONDITIONAL([HAVE_QT5_GTK], [test "${have_qt5_gtk}" = "yes"])
 AM_CONDITIONAL([HAVE_QT5_DECLARATIVE_PRIVATE], [test "${have_declarative_private}" = "yes"])
+AM_CONDITIONAL([HAVE_QT5_QUICK_TEST], [test "${have_qt5_quick_test}" = "yes"])
 
 dnl
 dnl detect kde4-config patch (used for kde solids).


=====================================
modules/gui/qt/Makefile.am
=====================================
@@ -982,6 +982,8 @@ libqt_plugin_la_QML = \
 	gui/qt/util/qml/ViewDragAutoScrollHandler.qml \
 	gui/qt/util/qml/BindingRev8.qml \
 	gui/qt/util/qml/BindingRev14.qml \
+	gui/qt/util/qml/FSM.qml \
+	gui/qt/util/qml/FSMState.qml \
 	gui/qt/util/qml/VanillaObject.qml \
 	gui/qt/util/qml/NativeMenu.qml \
 	gui/qt/util/qml/MLContextMenu.qml \
@@ -1138,4 +1140,21 @@ if !HAVE_OS2
 pkglibexec_PROGRAMS += vlc-qt-check
 endif
 endif
+
+if HAVE_QT5_QUICK_TEST
+
+qml_test_SOURCES = gui/qt/tests/qml_test.cpp
+nodist_qml_test_SOURCES = gui/qt/resources.cpp
+if HAVE_QMLCACHE
+nodist_qml_test_SOURCES += gui/qt/qmlcache_loader.cpp $(libqt_plugin_la_QML)
+endif
+qml_test_CXXFLAGS = $(AM_CXXFLAGS) $(QT_CFLAGS) -fPIC $(CXXFLAGS_qt) ${QT5_QUICK_TEST_CFLAGS} -DQUICK_TEST_SOURCE_DIR="\"${srcdir}/gui/qt/tests\""
+qml_test_LDADD = $(QT_LIBS) $(LIBS_qt) $(QT5_PLUGINS_LIBS) ${QT5_QUICK_TEST_LIBS}
+check_PROGRAMS += qml_test
+EXTRA_DIST += gui/qt/tests/tst_FSM.qml
+
+TESTS += qml_test
+
+endif
+
 endif


=====================================
modules/gui/qt/meson.build
=====================================
@@ -539,9 +539,14 @@ if host_system == 'windows'
 endif
 
 if qt5_dep.found()
+    qt5pre_qrc = qt5.preprocess(
+        qresources: qrc_files,
+        include_directories: qt_include_dir,
+        dependencies: qt5_dep)
+
+
     qt5pre_files = qt5.preprocess(ui_files: ui_sources,
         moc_headers: moc_headers,
-        qresources: qrc_files,
         include_directories: qt_include_dir,
         dependencies: qt5_dep)
 
@@ -681,11 +686,23 @@ if qt5_dep.found()
 
     vlc_modules += {
         'name' : 'qt',
-        'sources' : [qt5pre_files, qt_sources, some_sources],
+        'sources' : [qt5pre_files, qt5pre_qrc, qt_sources, some_sources],
         'dependencies' : [qt5_dep, qt_extra_deps],
         'include_directories' : qt_include_dir,
         'c_args' : qt_extra_flags,
         'cpp_args' : [qt_extra_flags, qt_cppargs]
     }
 
+    test_qt5_dep = dependency('qt5', modules: ['QuickTest'], required: false)
+    if test_qt5_dep.found()
+        qml_test = executable(
+          'qml_test',
+          files('tests/qml_test.cpp'),
+          qt5pre_qrc,
+          build_by_default: false,
+          dependencies: [test_qt5_dep],
+          cpp_args: ['-DQUICK_TEST_SOURCE_DIR="' + meson.current_source_dir() + '/tests"']
+        )
+        test('qml_test', qml_test, suite:'qt')
+    endif
 endif


=====================================
modules/gui/qt/player/qml/Player.qml
=====================================
@@ -152,12 +152,10 @@ FocusScope {
         id: playlistVisibility
 
         onShowPlaylist: {
-            rootPlayer.lockUnlockAutoHide(true)
             MainCtx.playlistVisible = true
         }
 
         onHidePlaylist: {
-            rootPlayer.lockUnlockAutoHide(false)
             MainCtx.playlistVisible = false
         }
     }


=====================================
modules/gui/qt/player/qml/PlayerPlaylistVisibilityFSM.qml
=====================================
@@ -18,6 +18,8 @@
 import QtQuick 2.12
 import org.videolan.vlc 0.1
 
+import "qrc:///util/" as Util
+
 /**
  * playlist visibility state machine
  *
@@ -37,7 +39,7 @@ import org.videolan.vlc 0.1
  * @enduml
  *
  */
-Item {
+Util.FSM {
     id: fsm
 
     //incoming signals
@@ -51,180 +53,101 @@ Item {
     signal hidePlaylist()
 
     //exposed internal states
-    property alias isPlaylistVisible: fsmVisible.enabled
-
-    property var _substate: undefined
-
-    function setState(state, substate) {
-        //callLater is used to avoid Connections on a signal to be immediatly
-        //re-trigered on the new state
-        Qt.callLater(function() {
-            if (state._substate === substate)
-                return;
-
-            if (state._substate !== undefined) {
-                if (state._substate.exited) {
-                    state._substate.exited()
-                }
-                state._substate.enabled = false
-            }
+    property alias isPlaylistVisible: fsmVisible.active
 
-            state._substate = substate
+    initialState: MainCtx.playlistDocked ? fsmDocked : fsmFloating
 
-            if (state._substate !== undefined) {
-                state._substate.enabled = true
-                if (state._substate.entered) {
-                    state._substate.entered()
-                }
-            }
-        })
-    }
-
-    //initial state
-    Component.onCompleted: {
-        if (MainCtx.playlistDocked)
-            fsm.setState(fsm, fsmDocked)
-        else
-            fsm.setState(fsm, fsmFloating)
-    }
+    signalMap: ({
+        togglePlaylistVisibility: fsm.togglePlaylistVisibility,
+        updatePlaylistVisible: fsm.updatePlaylistVisible,
+        updatePlaylistDocked: fsm.updatePlaylistDocked,
+        updateVideoEmbed: fsm.updateVideoEmbed
+    })
 
-    Item {
+    Util.FSMState {
         id: fsmFloating
-        enabled: false
 
-        Connections {
-            target: fsm
-            //explicitly bind on parent enabled, as Connections doens't behave as Item
-            //regarding enabled propagation on children, ditto bellow
-            enabled: fsmFloating.enabled
-
-            onTogglePlaylistVisibility: {
-                MainCtx.playlistVisible = !MainCtx.playlistVisible
-            }
-
-            onUpdatePlaylistDocked: {
-                if (MainCtx.playlistDocked) {
-                    fsm.setState(fsm, fsmDocked)
+        transitions: ({
+            togglePlaylistVisibility: {
+                action: () => {
+                    MainCtx.playlistVisible = !MainCtx.playlistVisible
                 }
+            },
+            updatePlaylistDocked: {
+                guard: () => MainCtx.playlistDocked,
+                target: fsmDocked
             }
-        }
+        })
     }
 
-    Item {
+    Util.FSMState {
         id: fsmDocked
-        enabled: false
-
-        property var _substate: undefined
 
-        function entered() {
-            if(MainCtx.hasEmbededVideo) {
-                fsm.setState(fsmDocked, fsmForceHidden)
-            } else {
-                fsm.setState(fsmDocked, fsmFollowVisible)
-            }
-        }
-
-        function exited() {
-            fsm.setState(fsmDocked, undefined)
-        }
+        initialState: MainCtx.hasEmbededVideo ? fsmForceHidden : fsmFollowVisible
 
-        Item {
+        Util.FSMState {
             id: fsmFollowVisible
-            enabled: false
-
-            property var _substate: undefined
 
-            function entered() {
-                if(MainCtx.playlistVisible)
-                    fsm.setState(this, fsmVisible)
-                else
-                    fsm.setState(this, fsmHidden)
-            }
+            initialState: MainCtx.playlistVisible ? fsmVisible : fsmHidden
 
-            function exited() {
-                fsm.setState(this, undefined)
-            }
-
-            Connections {
-                target: fsm
-                enabled: fsmFollowVisible.enabled
-
-                onUpdatePlaylistDocked: {
-                    if (!MainCtx.playlistDocked) {
-                        fsm.setState(fsm, fsmFloating)
-                    }
-                }
-
-                onUpdateVideoEmbed: {
-                    if (MainCtx.hasEmbededVideo) {
-                        fsm.setState(fsmDocked, fsmForceHidden)
-                    }
+            transitions: ({
+                updatePlaylistDocked: {
+                    guard: () => !MainCtx.playlistDocked,
+                    target: fsmFloating
+                },
+                updateVideoEmbed: {
+                    guard: () => MainCtx.hasEmbededVideo,
+                    target: fsmForceHidden
                 }
-            }
+            })
 
-            Item {
+            Util.FSMState {
                 id: fsmVisible
-                enabled: false
-
-                function entered() {
-                    fsm.showPlaylist()
-                }
-
-                function exited() {
-                    fsm.hidePlaylist()
-                }
 
-                Connections {
-                    target: fsm
-                    enabled: fsmVisible.enabled
-
-                    onUpdatePlaylistVisible: {
-                        if (!MainCtx.playlistVisible)
-                            fsm.setState(fsmFollowVisible, fsmHidden)
+                transitions: ({
+                    updatePlaylistVisible: {
+                        guard: ()=> !MainCtx.playlistVisible,
+                        target: fsmHidden
+                    },
+                    togglePlaylistVisibility: {
+                        action: () => { MainCtx.playlistVisible = false },
+                        target: fsmHidden
                     }
-
-                    onTogglePlaylistVisibility: fsm.setState(fsmFollowVisible, fsmHidden)
-                }
+                })
             }
 
-            Item {
+            Util.FSMState {
                 id: fsmHidden
-                enabled: false
 
-                Connections {
-                    target: fsm
-                    enabled: fsmHidden.enabled
-
-                    onUpdatePlaylistVisible: {
-                        if (MainCtx.playlistVisible)
-                            fsm.setState(fsmFollowVisible, fsmVisible)
+                transitions: ({
+                    updatePlaylistVisible: {
+                        guard: () => MainCtx.playlistVisible,
+                        target: fsmVisible
+                    },
+                    togglePlaylistVisibility: {
+                        action: () => { MainCtx.playlistVisible = true },
+                        target: fsmVisible
                     }
-
-                    onTogglePlaylistVisibility: fsm.setState(fsmFollowVisible, fsmVisible)
-                }
+                })
             }
 
         }
 
-        Item {
+        Util.FSMState {
             id: fsmForceHidden
-            enabled: false
 
-            Connections {
-                target: fsm
-                enabled: fsmForceHidden.enabled
-
-                onUpdateVideoEmbed: {
-                    if (!MainCtx.hasEmbededVideo) {
-                        fsm.setState(fsmDocked, fsmFollowVisible)
-                    }
+            transitions: ({
+                updateVideoEmbed: {
+                    guard: () => !MainCtx.hasEmbededVideo,
+                    target: fsmFollowVisible
+                },
+                togglePlaylistVisibility: {
+                    action: () => {
+                        MainCtx.playlistVisible = true
+                    },
+                    target: fsmFollowVisible
                 }
-
-                onTogglePlaylistVisibility: {
-                    MainCtx.playlistVisible = true
-                    fsm.setState(fsmDocked, fsmFollowVisible)
-                }
-            }
+            })
         }
     }
 }


=====================================
modules/gui/qt/player/qml/SliderBar.qml
=====================================
@@ -25,6 +25,7 @@ import org.videolan.vlc 0.1
 import "qrc:///widgets/" as Widgets
 import "qrc:///style/"
 import "qrc:///util/Helpers.js" as Helpers
+import "qrc:///util/" as Util
 
 Slider {
     id: control
@@ -86,7 +87,7 @@ Slider {
         pos: Qt.point(sliderRectMouseArea.mouseX, 0)
     }
 
-    QtObject {
+    Util.FSM {
         id: fsm
         signal playerUpdatePosition(real position)
         signal pressControl(real position, bool forcePrecise)
@@ -95,83 +96,14 @@ Slider {
 
         //each signal is associated to a key, when a signal is received,
         //transitions of active state for the given key are evaluated
-        property var signalMap: ({
-            playerUpdatePosition: fsm.playerUpdatePosition,
-            pressControl: fsm.pressControl,
-            releaseControl: fsm.releaseControl,
-            moveControl: fsm.moveControl
+        signalMap: ({
+            "playerUpdatePosition": fsm.playerUpdatePosition,
+            "pressControl": fsm.pressControl,
+            "releaseControl": fsm.releaseControl,
+            "moveControl": fsm.moveControl
         })
 
-        property var initialState: fsmReleased
-        property var _state: null
-
-        function _evaluateTransition(state, event, t, ...args) {
-            if ("guard" in t) {
-                if (!(t.guard instanceof Function)) {
-                    console.error(`guard property of ${state}::${event} is not a function`)
-                }
-                if (!t.guard(...args))
-                   return false
-            }
-
-            if ("action" in t) {
-                if (!(t.action instanceof Function))
-                    console.error(`action property of ${state}::${event} is not a function`)
-                t.action(...args)
-            }
-
-            if ("target" in t)
-                changeState(t.target)
-
-            return true
-        }
-
-        function handleSignal(event, state, ...args) {
-            if (!state)
-                return
-
-            if (!(event in _state.transitions))
-                return
-
-            const transitions = state.transitions[event]
-            if (Array.isArray(transitions)) {
-                for (const t of transitions) {
-                    //stop at the first accepted transition
-                    if (_evaluateTransition(state, event, t, ...args))
-                        return
-                }
-            } else {
-                _evaluateTransition(state, event, transitions, ...args)
-            }
-        }
-
-        function changeState(state) {
-            if (_state) {
-                if (_state.exit instanceof Function)
-                    _state.exit()
-            }
-
-            _state = state
-
-            if (_state) {
-                if (_state.enter instanceof Function)
-                    _state.enter()
-            }
-        }
-
-        Component.onCompleted: {
-            for (const signalName of Object.keys(signalMap)) {
-                signalMap[signalName].connect((...args) => {
-                    //use callLater to ensure transitions are ordered.
-                    //signal are not queued by default, this is an issue
-                    //if an action/enter/exit function raise another signal
-                    Qt.callLater(() => {
-                        handleSignal(signalName, fsm._state, ...args)
-                    })
-                })
-            }
-            changeState(initialState)
-        }
+        initialState: fsmReleased
 
         function _seekToPosition(position, threshold, forcePrecise) {
             position = Helpers.clamp(position, 0., 1.)
@@ -186,38 +118,39 @@ Slider {
             Player.position = position
         }
 
-        property list<QtObject> subStates: [
-            QtObject {
-                id: fsmReleased
-                property var transitions: ({
-                    playerUpdatePosition: {
-                        action: (position) => {
-                            control.value = position
-                        }
-                    },
-                    pressControl: {
-                        action: (position, forcePrecise) => {
-                            control.forceActiveFocus()
-                            fsm._seekToPosition(position, VLCStyle.dp(4) / control.width, forcePrecise)
-                        },
-                        target: fsmHeld
+        Util.FSMState {
+            id: fsmReleased
+
+            transitions: ({
+                "playerUpdatePosition": {
+                    action: (position) => {
+                        control.value = position
                     }
-                })
-            },
-            QtObject  {
-                id: fsmHeld
-                property var transitions: ({
-                    moveControl: {
-                        action: (position, forcePrecise) => {
-                            fsm._seekToPosition(position, VLCStyle.dp(2) / control.width, forcePrecise)
-                        }
+                },
+                "pressControl": {
+                    action: (position, forcePrecise) => {
+                        control.forceActiveFocus()
+                        fsm._seekToPosition(position, VLCStyle.dp(4) / control.width, forcePrecise)
                     },
-                    releaseControl: {
-                        target: fsmReleased
+                    target: fsmHeld
+                }
+            })
+        }
+
+        Util.FSMState  {
+            id: fsmHeld
+
+            transitions: ({
+                "moveControl": {
+                    action: (position, forcePrecise) => {
+                        fsm._seekToPosition(position, VLCStyle.dp(2) / control.width, forcePrecise)
                     }
-                })
-            }
-        ]
+                },
+                "releaseControl": {
+                    target: fsmReleased
+                }
+            })
+        }
     }
 
     Connections {


=====================================
modules/gui/qt/tests/qml_test.cpp
=====================================
@@ -0,0 +1,31 @@
+/*****************************************************************************
+ * Copyright (C) 2023 VLC authors and VideoLAN
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+*****************************************************************************/
+
+#include <QtQuickTest>
+
+// not much right now, type registration & initialisation may be required later on
+// https://doc.qt.io/qt-5/qtquicktest-index.html#executing-c-before-qml-tests
+
+int main(int argc, char **argv)
+{
+    QTEST_SET_MAIN_SOURCE_PATH
+    //run tests offscreen as the CI doesn't have a desktop environment
+    qputenv("QT_QPA_PLATFORM", "offscreen");
+    Q_INIT_RESOURCE(vlc);
+    return quick_test_main(argc, argv, "qml_test", QUICK_TEST_SOURCE_DIR);
+}


=====================================
modules/gui/qt/tests/tst_FSM.qml
=====================================
@@ -0,0 +1,645 @@
+/*****************************************************************************
+ * Copyright (C) 2023 VLC authors and VideoLAN
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * ( at your option ) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+import QtQuick 2.12
+import QtTest 1.12
+import "qrc:///util/" as Util
+
+TestCase {
+    id: root
+    name: "FSM"
+
+    property var events: []
+
+    function recEvent(e) {
+        events.push(e)
+    }
+
+    Util.FSM {
+        id: fsm
+        initialState: fsmA
+
+        signal signalSelfInternal()
+        signal signalSelfExternal()
+        signal signalToParent()
+        signal signalToSibiling()
+        signal signalToBA()
+        signal signalToC()
+        signal signalInParent()
+
+        signalMap: ({
+            signalSelfInternal: signalSelfInternal,
+            signalSelfExternal: signalSelfExternal,
+            signalToParent: signalToParent,
+            signalToSibiling: signalToSibiling,
+            signalToBA: signalToBA,
+            signalToC: signalToC,
+            signalInParent: signalInParent,
+        })
+
+        property bool selfTransitionDone: false
+
+        Util.FSMState {
+            id: fsmA
+            objectName: "fsmA"
+
+            initialState: fsmAA
+
+            function enter() {
+                recEvent("+A")
+            }
+
+            function exit() {
+                recEvent("-A")
+            }
+
+            Util.FSMState {
+                id: fsmAA
+                objectName: "fsmAA"
+
+                initialState: fsmAAA
+
+                function enter() {
+                    recEvent("+AA")
+                }
+
+                function exit() {
+                    recEvent("-AA")
+                }
+
+                transitions: ({
+                    signalInParent: fsmC,
+                })
+
+                Util.FSMState {
+                    id: fsmAAA
+                    objectName: "fsmAAA"
+
+                    function enter() {
+                        recEvent("+AAA")
+                    }
+
+                    function exit() {
+                        recEvent("-AAA")
+                    }
+
+                    transitions: ({
+                        signalSelfInternal: {
+                            action: () => { fsm.selfTransitionDone = true },
+                        },
+                        signalSelfExternal: {
+                            action: () => { fsm.selfTransitionDone = true },
+                            target: fsmAAA,
+                        },
+                        signalToParent: {
+                            action: () => { fsm.selfTransitionDone = true },
+                            target: fsmAA
+                        },
+                        signalToSibiling: fsmAAB,
+                        signalToBA: fsmBA,
+                        signalToC: fsmC,
+                    })
+                }
+
+                Util.FSMState {
+                    id: fsmAAB
+                    objectName: "fsmAAB"
+
+                    function enter() {
+                        recEvent("+AAB")
+                    }
+
+                    function exit() {
+                        recEvent("-AAB")
+                    }
+
+                    transitions: ({
+                        signalToParent: {
+                            action: () => { fsm.selfTransitionDone = true },
+                            target: fsmAA
+                        }
+                    })
+
+                }
+            }
+        }
+        Util.FSMState {
+            id: fsmB
+            objectName: "fsmB"
+
+            //no initial state
+            function enter() { recEvent("+B") }
+            function exit() {  recEvent("-B") }
+
+            Util.FSMState {
+                id: fsmBA
+                objectName: "fsmBA"
+
+                function enter() { recEvent("+BA") }
+                function exit() {  recEvent("-BA") }
+            }
+        }
+        Util.FSMState {
+            id: fsmC
+            objectName: "fsmC"
+
+            function enter() {
+                recEvent("+C")
+            }
+
+            function exit() {
+                recEvent("-C")
+            }
+        }
+    }
+
+
+    function check_active_inactive(active, inactive)
+    {
+        for (const s of inactive)
+            verify(!s.active, `${s} should be inactive`)
+
+        for (const s of active)
+            verify(s.active, `${s} should be active`)
+    }
+
+    function check_events(expected)
+    {
+        verify(
+            expected.every((e, idx) => root.events[idx] === e),
+            `expected transitions are ${expected}, got "${root.events}"`)
+    }
+
+    function init() {
+        fsm.reset()
+
+        fsmSeq.reset()
+        fsmGuard.reset()
+        fsmAction.reset()
+        events = []
+    }
+
+    function test_initial_state() {
+        check_active_inactive([fsmA, fsmAA, fsmAAA], [fsmAAB, fsmB, fsmBA, fsmC])
+    }
+
+    function test_reset() {
+        fsm.reset()
+        tryCompare(fsmAAA, "active", true)
+        check_events(["+A", "+AA", "+AAA"])
+        check_active_inactive([fsmA, fsmAA, fsmAAA], [fsmAAB, fsmB, fsmBA, fsmC])
+
+    }
+
+    function test_transitions_data() {
+        return [
+            {tag: "signalToSibiling", transition: fsm.signalToSibiling, events: ["-AAA", "+AAB"],
+             active: [fsmAAB, fsmA, fsmAA], inactive: [fsmB, fsmBA, fsmC, fsmAAA]},
+            {tag: "signalToBA", transition: fsm.signalToBA, events: ["-AAA", "-AA", "-A", "+B", "+BA"],
+             active: [fsmBA, fsmB],  inactive: [fsmC, fsmA, fsmAA, fsmAAB, fsmAAA]},
+            {tag:"signalInParent", transition: fsm.signalInParent, events: ["-AAA", "-AA", "-A", "+C"],
+             active: [fsmC],  inactive: [fsmB, fsmBA, fsmA, fsmAA, fsmAAB, fsmAAA]},
+        ]
+    }
+
+    function test_transitions(data) {
+        data.transition()
+        tryCompare(data.active[0], "active", true)
+        check_events(data.events)
+        check_active_inactive(data.active, data.inactive)
+    }
+
+
+    function test_self_transitions_data() {
+        return [
+            {tag: "SelfInternal", transition: fsm.signalSelfInternal, events: [],
+             active: [fsmA, fsmAA, fsmAAA], inactive: [fsmB, fsmBA, fsmC, fsmAAB]},
+            {tag: "SelfExternal", transition: fsm.signalSelfExternal, events: ["-AAA", "+AAA"],
+             active: [fsmA, fsmAA, fsmAAA], inactive: [fsmB, fsmBA, fsmC, fsmAAB]},
+            {tag: "signalToParent", transition: fsm.signalToParent, events: ["-AAA", "-AA", "+AA", "+AAA"],
+             active: [fsmA, fsmAA, fsmAAA], inactive: [fsmB, fsmBA, fsmC, fsmAAB]},
+        ]
+    }
+
+    function test_self_transitions(data) {
+        fsm.selfTransitionDone = false
+        data.transition()
+        //state doesn't change, can't wait for state activation
+        tryCompare(fsm, "selfTransitionDone", true)
+        check_events(data.events)
+        check_active_inactive(data.active, data.inactive)
+    }
+
+    function test_self_transitions_reset_initial_state() {
+        //as a setup, move to AAB
+        fsm.signalToSibiling()
+        tryCompare(fsmAAB, "active", true)
+        root.events = []
+
+        //move to the parent state
+        fsm.signalToParent()
+        tryCompare(fsmAAA, "active", true)
+        check_events(["-AAB", "-AA", "+AA", "+AAA"])
+        check_active_inactive([fsmA, fsmAA, fsmAAA], [fsmAAB])
+    }
+
+    Util.FSM {
+        id: fsmSeq
+
+        signal atob()
+        signal atoc()
+        signal ctoa()
+
+        signalMap: ({
+            atob: atob,
+            atoc: atoc,
+            ctod: ctoa,
+        })
+
+        initialState: seqA
+        Util.FSMState {
+            id: seqA
+            objectName: "seqA"
+            function enter() { recEvent("+A")  }
+            function exit() { recEvent("-A") }
+            transitions: ({
+                atob: seqB,
+                atoc: seqC,
+
+            })
+        }
+        Util.FSMState {
+            id: seqB
+            objectName: "seqB"
+            function enter() { recEvent("+B") }
+            function exit() { recEvent("-B")  }
+            transitions: ({
+                atob: seqA,
+            })
+        }
+        Util.FSMState {
+            id: seqC
+            objectName: "seqC"
+            function enter() {
+                recEvent("+C")
+                fsmSeq.ctoa()
+            }
+            function exit() { recEvent("-C") }
+            transitions: ({
+                ctod: seqD,
+            })
+        }
+        Util.FSMState {
+            id: seqD
+            objectName: "seqD"
+            function enter() { recEvent("+D") }
+            function exit() { recEvent("-D") }
+        }
+    }
+
+    function test_sequential_transitions_data() {
+        return [
+            {tag: "non-recursive", transition: fsmSeq.atob, events: ["-A", "+B"],
+             active: [seqB], inactive: [seqA, seqC]},
+            //C auto transition to A
+            {tag: "recursive", transition: fsmSeq.atoc, events: ["-A", "+C", "-C", "+D"],
+             active: [seqD], inactive: [seqB, seqC]},
+        ]
+    }
+
+    function test_sequential_transitions(data) {
+        data.transition()
+        tryCompare(data.active[0], "active", true)
+        check_events(data.events)
+        check_active_inactive(data.active, data.inactive)
+    }
+
+    Util.FSM {
+        id: fsmGuard
+
+        signal success()
+        signal fail()
+        signal multiple()
+        signal multipleDefault()
+        signal multipleFail()
+        signal paramBool(var a)
+        signal paramMultiple(var a, var b, var c, var d, var e)
+
+        signalMap: ({
+            success: fsmGuard.success,
+            fail: fsmGuard.fail,
+            multiple: fsmGuard.multiple,
+            multipleDefault: fsmGuard.multipleDefault,
+            multipleFail: fsmGuard.multipleFail,
+            paramBool: fsmGuard.paramBool,
+            paramMultiple: fsmGuard.paramMultiple,
+        })
+
+        initialState: guardInit
+        Util.FSMState {
+            id: guardInit
+            objectName: "guardInit"
+            transitions: ({
+                success: {
+                    guard: () => true,
+                    target: guardOK
+                },
+                fail: {
+                    guard: () => false,
+                    target: guardFail
+                },
+                multiple: [{
+                    guard: () => false,
+                    target: guardFail
+                }, {
+                    guard: () => true,
+                    target: guardOK
+                }, {
+                    //will not be evaluated
+                    target: guardFail
+                }],
+                multipleFail: [{
+                    guard: () => false,
+                    target: guardFail
+                }, {
+                    guard: () => false,
+                    target: guardFail
+                }],
+                multipleDefault: [{
+                    guard: () => false,
+                    target: guardFail
+                }, {
+                    target: guardOK
+                }, {
+                    //will not be evaluated
+                    target: guardFail
+                }],
+                paramBool: {
+                    guard: (v) => v,
+                    target: guardOK
+                },
+                paramMultiple: {
+                    guard: (a, b, c, d, e) => a === true && b === 42 && c === "test" && d(e),
+                    target: guardOK
+                },
+
+            })
+        }
+        Util.FSMState {
+            id: guardOK
+            objectName: "guardOK"
+        }
+        Util.FSMState {
+            id: guardFail
+            objectName: "guardFail"
+        }
+    }
+
+    function test_guard_transitions_data() {
+        return [
+            {tag: "success",  transition: fsmGuard.success,
+             active: [guardOK], inactive: [guardInit, guardFail]},
+            {tag: "fail",  transition: fsmGuard.fail,
+             active: [guardInit], inactive: [guardOK, guardFail]},
+            {tag: "multiple",  transition: fsmGuard.multiple,
+             active: [guardOK], inactive: [guardInit, guardFail]},
+            {tag: "multipleDefault",  transition: fsmGuard.multipleDefault,
+             active: [guardOK], inactive: [guardInit, guardFail]},
+            {tag: "multipleFail",  transition: fsmGuard.multipleFail,
+             active: [guardInit], inactive: [guardOK, guardFail]},
+            {tag: "param(true)", transition: function() { fsmGuard.paramBool(true) },
+             active: [guardOK], inactive: [guardInit, guardFail]},
+            {tag: "param(false)", transition: function() { fsmGuard.paramBool(false) },
+             active: [guardInit], inactive: [guardOK, guardFail]},
+            {tag: "param(mutiple)", transition: function() { fsmGuard.paramMultiple(true, 42, "test", (bar) =>  bar.a === 51, { a: 51}) },
+             active: [guardOK], inactive: [guardInit, guardFail]},
+            {tag: "param(mutiple fail)", transition: function() { fsmGuard.paramMultiple(false, -1, "nope", (bar) =>  bar.a === 51, { a: 2}) },
+             active: [guardInit], inactive: [guardOK, guardFail]},
+        ]
+    }
+
+    function test_guard_transitions(data) {
+        data.transition()
+        tryCompare(data.active[0], "active", true)
+        check_active_inactive(data.active, data.inactive)
+    }
+
+
+    Util.FSM {
+        id: fsmAction
+
+        signal simple()
+        signal redefinedInChild()
+        signal guardedFalse()
+        signal guardedTrue()
+        signal guardedMultiple()
+        signal signalInAction1()
+        signal signalInAction2()
+        signal withParam(var a, var b, var c)
+
+        signalMap: ({
+            simple: simple,
+            redefinedInChild: redefinedInChild,
+            guardedFalse: guardedFalse,
+            guardedTrue: guardedTrue,
+            guardedMultiple: guardedMultiple,
+            signalInAction1: signalInAction1,
+            signalInAction2: signalInAction2,
+            withParam: withParam,
+        })
+
+        initialState: actionA
+
+        Util.FSMState {
+            id: actionA
+            objectName: "actionA"
+            initialState: actionAA
+
+            function enter() { recEvent("+A") }
+            function exit() { recEvent("-A") }
+
+            transitions: ({
+                redefinedInChild: {
+                    //child will transition before us, this won't be triggered
+                    action: () => recEvent("KO"),
+                    target: actionKO
+                }
+            })
+
+            Util.FSMState {
+                id: actionAA
+                objectName: "actionAA"
+                function enter() { recEvent("+AA") }
+                function exit() { recEvent("-AA") }
+
+                transitions: ({
+                    simple: {
+                        action: () => recEvent("OK"),
+                        target: actionOK
+                    },
+                    guardedFalse: {
+                        guard: () => false,
+                        action: () => recEvent("KO"),
+                        target: actionOK
+                    },
+                    guardedTrue: {
+                        guard: () => true,
+                        action: () => recEvent("OK"),
+                        target: actionOK
+                    },
+                    guardedMultiple: [{
+                        guard: () => false,
+                        action: () => recEvent("KO"),
+                        target: actionKO
+                    }, {
+                        guard: () => true,
+                        action: () => recEvent("OK"),
+                        target: actionOK
+                    },{
+                        //this should not be triggered
+                        guard: () => true,
+                        action: () => recEvent("KO"),
+                        target: actionKO
+                    }],
+                    redefinedInChild: {
+                        action: () => recEvent("OK"),
+                        target: actionOK
+                    },
+                    withParam: {
+                        action: (a,b,c) => {
+                            if (a === 42 && b === "p1" && c.a === 51)
+                                recEvent("OK")
+                        },
+                        target: actionOK
+                    },
+                    signalInAction1: {
+                        action: () => { fsmAction.signalInAction2() },
+                        target: actionTransient
+                    },
+                    signalInAction2: {
+                        //this should not be triggered
+                        action: () => { recEvent("KO") },
+                        target: actionOK
+                    },
+                })
+            }
+        }
+        Util.FSMState {
+            id: actionOK
+            objectName: "actionOK"
+             function enter() { recEvent("+OK") }
+            function exit() { recEvent("-OK") }
+
+        }
+        Util.FSMState {
+            id: actionKO
+            objectName: "actionKO"
+            function enter() { recEvent("+KO") }
+            function exit() { recEvent("-KO") }
+        }
+        Util.FSMState {
+            id: actionTransient
+            objectName: "actionTransient"
+            function enter() { recEvent("+T") }
+            function exit() { recEvent("-T") }
+            transitions: ({
+                signalInAction2: actionOK
+            })
+        }
+    }
+
+    function test_action_transitions_data() {
+        return [
+            {tag: "simple",  transition: fsmAction.simple,
+             events: ["OK", "-AA", "-A", "+OK"], active: [actionOK], inactive: [actionA, actionAA]},
+            {tag: "guardedFalse",  transition: fsmAction.guardedFalse,
+             events: [], active: [actionA, actionAA], inactive: [actionOK]},
+            {tag: "guardedTrue",  transition: fsmAction.guardedTrue,
+             events: ["OK", "-AA", "-A", "+OK"], active: [actionOK], inactive: [actionA, actionAA]},
+            {tag: "guardedMultiple",  transition: fsmAction.guardedMultiple,
+             events: ["OK", "-AA", "-A", "+OK"], active: [actionOK], inactive: [actionA, actionAA]},
+            {tag: "redefinedInChild",  transition: fsmAction.redefinedInChild,
+             events: ["OK", "-AA", "-A", "+OK"], active: [actionOK], inactive: [actionA, actionAA]},
+            {tag: "withParam",  transition: function() { fsmAction.withParam(42, "p1", {a: 51}) },
+             events: ["OK", "-AA", "-A", "+OK"], active: [actionOK], inactive: [actionA, actionAA]},
+            {tag: "signalInAction",  transition: fsmAction.signalInAction1,
+             events: ["-AA", "-A", "+T", "-T", "+OK"], active: [actionOK], inactive: [actionA, actionAA]},
+        ]
+    }
+
+    function test_action_transitions(data) {
+        data.transition()
+        tryCompare(data.active[0], "active", true)
+        check_events(data.events)
+        check_active_inactive(data.active, data.inactive)
+    }
+
+
+    //check that the FSM hierarchy may contain other object than FSMState nodes
+    Util.FSM {
+        id: fsmMixed
+        initialState: mixedA
+
+        signal atob()
+        signal btoc()
+
+        signalMap: ({
+            atob: atob,
+            btoc: btoc,
+        })
+
+        Util.FSMState {
+            id: mixedA
+            objectName: "mixedA"
+
+            //whatever
+            Rectangle {}
+
+            transitions: ({
+                atob: mixedB
+            })
+        }
+
+        Util.FSMState {
+            id: mixedB
+            objectName: "mixedB"
+
+            Timer {
+                interval: 20
+                running: mixedB.active
+                onTriggered: fsmMixed.btoc()
+            }
+
+            transitions: ({
+                btoc: mixedC
+            })
+        }
+
+        Util.FSMState {
+            id: mixedC
+            objectName: "mixedC"
+        }
+    }
+
+    function test_mixed_children() {
+        fsmMixed.atob()
+        tryCompare(mixedB, "active", true)
+        wait(30)
+        verify(mixedC.active, true)
+    }
+
+}


=====================================
modules/gui/qt/util/qml/FSM.qml
=====================================
@@ -0,0 +1,322 @@
+/*****************************************************************************
+ * Copyright (C) 2023 VLC authors and VideoLAN
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * ( at your option ) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+import QtQuick 2.12
+
+/**
+ * @brief a pure QML hierarchical Finite State Machine implementation
+ *
+ * FSM {
+ *   signal aSignal()
+ *   signal anotherSignal(a, b, c)
+ *
+ *   //map signals to a key
+ *   signalMap: ({
+ *     "aSignal": aSignal,
+ *     "anotherSignal": anotherSignal
+ *   })
+ *
+ *   //define what is the initial sub state
+ *   initialState: firstState
+ *
+ *   FSMState {
+ *     id: firstState
+ *     //transitions defintions for this state
+ *     transitions: ({
+ *        "aSignal": finalState, //transition to finalState when receiving aSignal
+ *        "anotherSignal": [{
+ *           action: (a,b,c) => {
+ *              //action is executed if transition is taken (anotherSignal is received and
+ *              //guard returns true)
+ *           },
+ *           guard: (a,b,c) => a + b > c, //transition is taken if guard returns true
+ *           target: subStateA //target state
+ *        }, {
+ *           target: subStateB
+ *        }]
+ *     })
+ *   }
+ *   FSMState {
+ *     id: anotherState
+ *
+ *     initialState: subStateA
+ *     FSMState {
+ *       id: subStateA
+ *       //states may be nested
+ *     }
+ *     FSMState {
+ *       id: subStateB
+ *     }
+ *   }
+ *   FSMState {
+ *     id: finalState
+ *   }
+ * }
+ */
+FSMState {
+    id: fsm
+
+    //each signal is associated to a key, when a signal is received,
+    //transitions of active state for the given key are evaluated
+    property var signalMap: ({
+    })
+
+    property bool running: true
+
+    /**
+     * @param {FSMState} state state handling the event
+     * @param {string} event name of the event
+     * @param {...*} args event arguments
+     * @param {Object} t transition definition
+     * @return {boolean} true if the state has handled the event
+     */
+    function _evaluateTransition(state, event, t, ...args) {
+        if ("guard" in t) {
+            if (!(t.guard instanceof Function)) {
+                console.error(`guard property of ${state}::${event} is not a function`)
+            }
+            if (!t.guard(...args))
+                return false
+        }
+
+        if ("action" in t) {
+            if (!(t.action instanceof Function))
+                console.error(`action property of ${state}::${event} is not a function`)
+            t.action(...args)
+        }
+
+        if ("target" in t)
+            _changeState(t.target)
+
+        return true
+    }
+
+    /**
+     * @param {FSMState} state state handling the event
+     * @param {string} event name of the event
+     * @param {...*} args event arguments
+     * @return {boolean} true if the state has handled the event
+     */
+    function handleSignal(state, event, ...args) {
+        if (!running)
+            return false
+
+        if (!state)
+            return false
+
+        if (state._state) {
+            if (handleSignal(state._state, event, ...args))
+                return true
+        }
+
+        if (!(event in state.transitions)) {
+            return false
+        }
+
+        const transitions = state.transitions[event]
+        if (transitions === undefined) {
+            console.warn(`undefined transition for ${state}::${event}`)
+            //FIXME: comparing object to QML type with instanceof fails with 5.12
+        } else if (transitions === null || transitions.toString().startsWith("FSMState")) {
+            _changeState(transitions)
+            return true
+        } else if (Array.isArray(transitions)) {
+            for (const t of transitions) {
+                //stop at the first accepted transition
+                if (_evaluateTransition(state, event, t, ...args))
+                    return true
+            }
+            return false
+        } else {
+            return _evaluateTransition(state, event, transitions, ...args)
+        }
+    }
+
+    /**
+     * @param {FSMState} state
+     */
+    function _exitState(state) {
+        if (!state)
+            return
+
+        //exit sub states
+        if (state._state)
+            _exitState(state._state)
+
+        state._state = null
+        state.active = false
+        if (state.exit instanceof Function)
+            state.exit()
+    }
+
+    /**
+     * @brief mark the state as active, enter handler is evaluated
+     * @param {FSMState} state
+     */
+    function _activateState(state) {
+        if (!state)
+            return
+
+        if (!state.active) {
+            state.active = true
+            if (state.enter instanceof Function)
+                state.enter()
+        }
+    }
+
+    /**
+     * @brief enter the target state, enter handler are evaluated
+     * inital sub-states are entered recursively
+     * @param {FSMState} state
+     */
+    function _enterState(state) {
+        if (!state)
+            return
+
+        _activateState(state)
+        if (state.initialState) {
+            state._state = state.initialState
+            _enterState(state._state)
+        }
+    }
+
+    /**
+     * @param {FSMState} state
+     * @param {FSMState[]} parentStates
+     */
+    function _resolveStatesHierarchy(state, parentStates) {
+        if (!state)
+            return
+        state._parentStates = parentStates
+        for (let i in state._children) {
+            const child = state._children[i]
+            if (child instanceof FSMState) {
+                state._subStates.push(child)
+            }
+        }
+        for (const s of state._subStates) {
+            _resolveStatesHierarchy(s, [...parentStates, state])
+        }
+    }
+
+    /**
+     * @param {FSMState} state
+     */
+    function _validateFSM(state) {
+        if (!state)
+            return
+
+        if (!(state instanceof FSMState)) {
+            console.warn(`invalid state machine: ${state} is not an FSMState node`)
+        }
+
+        for (const key of Object.keys(state.transitions)) {
+            if (!Object.keys(fsm.signalMap).includes(key)) {
+                console.warn(`transition ${key} ${state} match no signal`, Object.keys(fsm.signalMap))
+            }
+        }
+
+        for (const s of state._subStates) {
+            _validateFSM(s)
+        }
+    }
+
+    /**
+     * @param {FSMState[]} a state list
+     * @param {FSMState} a root state
+     * @return {FSMState} the common ancestor
+     */
+    function _findCommonAncestorState(a, b) {
+        if (!a || !b)
+            return null
+
+        if (!b._parentStates.includes(a))
+            return null
+
+        const node = _findCommonAncestorState(a._state, b)
+        if (node !== null)
+            return node
+
+        return a
+    }
+
+
+    /**
+     * @param {FSMState} state target state
+     */
+    function _changeState(state) {
+        const ancestor = _findCommonAncestorState(fsm, state)
+
+        //exit uncommon states
+        if (ancestor) {
+            _exitState(ancestor._state)
+        }
+
+        if (!state) {
+            return
+        }
+
+        //activate parent state, but do not enter their initialState
+        let parentState = fsm
+        for (let i in state._parentStates) {
+            _activateState(state._parentStates[i])
+            parentState._state = state._parentStates[i]
+            parentState = state._parentStates[i]
+        }
+
+        //enter target state, then enter initial sub-states
+        parentState._state = state
+        _enterState(state)
+    }
+
+
+    /**
+     * reset the FSM to its initial state, exit handlers of the current state are not
+     * evaluated. enter hander of initial state will be evaluated
+     */
+    function reset() {
+        function reset_rec(state) {
+            if (!state)
+                return
+            if (state._state) {
+                reset_rec(state._state)
+                state._state = null
+            }
+            state.active = false
+        }
+        reset_rec(fsm)
+        _changeState(initialState)
+    }
+
+    Component.onCompleted: {
+        _resolveStatesHierarchy(fsm, [])
+        _validateFSM(fsm)
+
+        for (const signalName of Object.keys(signalMap)) {
+            signalMap[signalName].connect((...args) => {
+                //use callLater to ensure transitions are ordered.
+                //signal are not queued by default, this is an issue
+                //if an action/enter/exit function raise another signal
+                Qt.callLater(() => {
+                    handleSignal(fsm, signalName, ...args)
+                })
+            })
+        }
+
+        _changeState(fsm)
+    }
+}


=====================================
modules/gui/qt/util/qml/FSMState.qml
=====================================
@@ -0,0 +1,48 @@
+/*****************************************************************************
+ * Copyright (C) 2023 VLC authors and VideoLAN
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * ( at your option ) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+import QtQuick 2.12
+
+QtObject {
+    property bool active: false
+
+    ///initial sub state
+    property var initialState: null
+
+    /**
+     * @typedef {Object} TransitionDefinition
+     * @property {Function} [guard] transition is only evaluated if this function returns true
+     * @property {Function} [action] action executed on transition
+     * @property {FSMState} [target] target state, if not defined, FSM will stay in
+     *           current state
+     */
+
+    /**
+     * dictionnary containing definition of transitions
+     * key is the signal name as defined in
+     * @type {Object<string:null|FSMState|TransitionDefinition|TransitionDefinition[]>}
+     */
+    property var transitions: ({})
+
+
+    default property list<QtObject> _children
+
+    //subStates
+    property QtObject _state: null
+    property var _parentStates: []
+    property var _subStates: []
+}


=====================================
modules/gui/qt/vlc.qrc
=====================================
@@ -82,6 +82,8 @@
         <file alias="MLContextMenu.qml">util/qml/MLContextMenu.qml</file>
         <file alias="ListViewRev15.qml">util/qml/ListViewRev15.qml</file>
         <file alias="ListViewRev11.qml">util/qml/ListViewRev11.qml</file>
+        <file alias="FSM.qml">util/qml/FSM.qml</file>
+        <file alias="FSMState.qml">util/qml/FSMState.qml</file>
     </qresource>
     <qresource prefix="/sd">
         <file alias="capture-card.svg">pixmaps/sd/capture-card.svg</file>



View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/aedff4718c5c0a547cebfaadbf0d365ab24ef88f...85c48fb80f0b98893a723c3ee6af8d5cf89cea55

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/aedff4718c5c0a547cebfaadbf0d365ab24ef88f...85c48fb80f0b98893a723c3ee6af8d5cf89cea55
You're receiving this email because of your account on code.videolan.org.


VideoLAN code repository instance


More information about the vlc-commits mailing list