diff options
Diffstat (limited to 'examples/statemachine')
-rw-r--r-- | examples/statemachine/moveblocks/doc/moveblocks.png | bin | 0 -> 4532 bytes | |||
-rw-r--r-- | examples/statemachine/moveblocks/doc/moveblocks.rst | 10 | ||||
-rw-r--r-- | examples/statemachine/moveblocks/moveblocks.py | 222 | ||||
-rw-r--r-- | examples/statemachine/moveblocks/moveblocks.pyproject | 3 | ||||
-rw-r--r-- | examples/statemachine/ping_pong/ping_pong.py | 70 | ||||
-rw-r--r-- | examples/statemachine/ping_pong/ping_pong.pyproject | 3 | ||||
-rw-r--r-- | examples/statemachine/rogue/rogue.py | 179 | ||||
-rw-r--r-- | examples/statemachine/rogue/rogue.pyproject | 3 | ||||
-rw-r--r-- | examples/statemachine/trafficlight/doc/trafficlight.png | bin | 0 -> 4786 bytes | |||
-rw-r--r-- | examples/statemachine/trafficlight/doc/trafficlight.rst | 10 | ||||
-rw-r--r-- | examples/statemachine/trafficlight/trafficlight.py | 117 | ||||
-rw-r--r-- | examples/statemachine/trafficlight/trafficlight.pyproject | 3 |
12 files changed, 620 insertions, 0 deletions
diff --git a/examples/statemachine/moveblocks/doc/moveblocks.png b/examples/statemachine/moveblocks/doc/moveblocks.png Binary files differnew file mode 100644 index 000000000..56353d181 --- /dev/null +++ b/examples/statemachine/moveblocks/doc/moveblocks.png diff --git a/examples/statemachine/moveblocks/doc/moveblocks.rst b/examples/statemachine/moveblocks/doc/moveblocks.rst new file mode 100644 index 000000000..4cffd3b85 --- /dev/null +++ b/examples/statemachine/moveblocks/doc/moveblocks.rst @@ -0,0 +1,10 @@ +Move Blocks Example +=================== + +The Move Blocks example shows how to animate items in a QGraphicsScene +using a QStateMachine with a custom transition. + + +.. image:: moveblocks.png + :width: 400 + :alt: Move Blocks Screenshot diff --git a/examples/statemachine/moveblocks/moveblocks.py b/examples/statemachine/moveblocks/moveblocks.py new file mode 100644 index 000000000..0d52c5b00 --- /dev/null +++ b/examples/statemachine/moveblocks/moveblocks.py @@ -0,0 +1,222 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +"""PySide6 port of the examples/statemachine/moveblocks example from Qt v6.x""" + +import sys + +from PySide6.QtCore import (QAbstractAnimation, QEasingCurve, QEvent, QObject, + QParallelAnimationGroup, QPropertyAnimation, + QRandomGenerator, QRect, QSequentialAnimationGroup, + Qt, QTimer) +from PySide6.QtGui import QPainter, QResizeEvent +from PySide6.QtWidgets import (QApplication, QGraphicsView, QGraphicsScene, + QGraphicsWidget, QStyleOptionGraphicsItem, + QWidget) +from PySide6.QtStateMachine import (QAbstractTransition, QState, QStateMachine) + + +StateSwitchType = QEvent.Type(QEvent.Type.User + 256) + + +class StateSwitchEvent(QEvent): + def __init__(self, rand: int = 0) -> None: + super().__init__(StateSwitchType) + self._rand = rand + + def rand(self) -> int: + return self._rand + + +class QGraphicsRectWidget(QGraphicsWidget): + def __init__(self): + super().__init__() + + def paint(self, painter: QPainter, + option: QStyleOptionGraphicsItem, widget: QWidget | None = None): + painter.fillRect(self.rect(), Qt.blue) + + +class StateSwitchTransition(QAbstractTransition): + def __init__(self, rand: int = 0) -> None: + super().__init__() + self._rand = rand + + def eventTest(self, event: QEvent) -> bool: + return event.type() == StateSwitchType and event.rand() == self._rand + + def onTransition(self, event: QEvent): + pass + + +class StateSwitcher(QState): + def __init__(self, machine: QStateMachine) -> None: + super().__init__(machine) + self._state_count = 0 + self._last_index = 0 + self.rg = QRandomGenerator.global_() + + def onEntry(self, event: QEvent) -> None: + while True: + n = int(self.rg.bounded(self._state_count)) + 1 + if n != self._last_index: + break + self._last_index = n + self.event = StateSwitchEvent(n) + self.machine().postEvent(self.event) + + def onExit(self, event: QEvent) -> None: + pass + + def addState(self, state: QState, animation: QAbstractAnimation) -> None: + self._state_count += 1 + trans = StateSwitchTransition(self._state_count) + trans.setTargetState(state) + self.addTransition(trans) + trans.addAnimation(animation) + + +def createGeometryState(w1: QObject, rect1: QRect, + w2: QObject, rect2: QRect, + w3: QObject, rect3: QRect, + w4: QObject, rect4: QRect, parent: QState) -> QState: + result = QState(parent) + result.assignProperty(w1, "geometry", rect1) + result.assignProperty(w2, "geometry", rect2) + result.assignProperty(w3, "geometry", rect3) + result.assignProperty(w4, "geometry", rect4) + + return result + + +class GraphicsView(QGraphicsView): + def __init__(self, scene: QGraphicsScene, parent: QWidget | None = None): + super().__init__(scene, parent) + + def resizeEvent(self, event: QResizeEvent) -> None: + self.fitInView(self.sceneRect()) + super().resizeEvent(event) + + +if __name__ == '__main__': + app = QApplication(sys.argv) + + button1, button2 = QGraphicsRectWidget(), QGraphicsRectWidget() + button3, button4 = QGraphicsRectWidget(), QGraphicsRectWidget() + + button2.setZValue(1) + button3.setZValue(2) + button4.setZValue(3) + + scene = QGraphicsScene(0, 0, 300, 300) + scene.setBackgroundBrush(Qt.black) + scene.addItem(button1) + scene.addItem(button2) + scene.addItem(button3) + scene.addItem(button4) + + window = GraphicsView(scene) + window.setFrameStyle(0) + window.setAlignment(Qt.AlignLeft | Qt.AlignTop) + window.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + window.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + + machine = QStateMachine() + + group = QState() + group.setObjectName("group") + timer = QTimer() + timer.setInterval(1250) + timer.setSingleShot(True) + + group.entered.connect(timer.start) + + state1, state2, state3 = QState(), QState(), QState() + state4, state5, state6 = QState(), QState(), QState() + state7 = QState() + + state1 = createGeometryState(button1, QRect(100, 0, 50, 50), + button2, QRect(150, 0, 50, 50), + button3, QRect(200, 0, 50, 50), + button4, QRect(250, 0, 50, 50), + group) + state2 = createGeometryState(button1, QRect(250, 100, 50, 50), + button2, QRect(250, 150, 50, 50), + button3, QRect(250, 200, 50, 50), + button4, QRect(250, 250, 50, 50), + group) + state3 = createGeometryState(button1, QRect(150, 250, 50, 50), + button2, QRect(100, 250, 50, 50), + button3, QRect(50, 250, 50, 50), + button4, QRect(0, 250, 50, 50), + group) + state4 = createGeometryState(button1, QRect(0, 150, 50, 50), + button2, QRect(0, 100, 50, 50), + button3, QRect(0, 50, 50, 50), + button4, QRect(0, 0, 50, 50), + group) + state5 = createGeometryState(button1, QRect(100, 100, 50, 50), + button2, QRect(150, 100, 50, 50), + button3, QRect(100, 150, 50, 50), + button4, QRect(150, 150, 50, 50), + group) + state6 = createGeometryState(button1, QRect(50, 50, 50, 50), + button2, QRect(200, 50, 50, 50), + button3, QRect(50, 200, 50, 50), + button4, QRect(200, 200, 50, 50), + group) + state7 = createGeometryState(button1, QRect(0, 0, 50, 50), + button2, QRect(250, 0, 50, 50), + button3, QRect(0, 250, 50, 50), + button4, QRect(250, 250, 50, 50), + group) + group.setInitialState(state1) + + animation_group = QParallelAnimationGroup() + sub_group = QSequentialAnimationGroup() + + anim = QPropertyAnimation(button4, b"geometry") + anim.setDuration(1000) + anim.setEasingCurve(QEasingCurve.OutElastic) + animation_group.addAnimation(anim) + + sub_group = QSequentialAnimationGroup(animation_group) + sub_group.addPause(100) + anim = QPropertyAnimation(button3, b"geometry") + anim.setDuration(1000) + anim.setEasingCurve(QEasingCurve.OutElastic) + sub_group.addAnimation(anim) + + sub_group = QSequentialAnimationGroup(animation_group) + sub_group.addPause(150) + anim = QPropertyAnimation(button2, b"geometry") + anim.setDuration(1000) + anim.setEasingCurve(QEasingCurve.OutElastic) + sub_group.addAnimation(anim) + + sub_group = QSequentialAnimationGroup(animation_group) + sub_group.addPause(200) + anim = QPropertyAnimation(button1, b"geometry") + anim.setDuration(1000) + anim.setEasingCurve(QEasingCurve.OutElastic) + sub_group.addAnimation(anim) + + state_switcher = StateSwitcher(machine) + state_switcher.setObjectName("state_switcher") + group.addTransition(timer.timeout, state_switcher) + state_switcher.addState(state1, animation_group) + state_switcher.addState(state2, animation_group) + state_switcher.addState(state3, animation_group) + state_switcher.addState(state4, animation_group) + state_switcher.addState(state5, animation_group) + state_switcher.addState(state6, animation_group) + state_switcher.addState(state7, animation_group) + + machine.addState(group) + machine.setInitialState(group) + machine.start() + + window.resize(300, 300) + window.show() + + sys.exit(app.exec()) diff --git a/examples/statemachine/moveblocks/moveblocks.pyproject b/examples/statemachine/moveblocks/moveblocks.pyproject new file mode 100644 index 000000000..8c3eff508 --- /dev/null +++ b/examples/statemachine/moveblocks/moveblocks.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["moveblocks.py"] +} diff --git a/examples/statemachine/ping_pong/ping_pong.py b/examples/statemachine/ping_pong/ping_pong.py new file mode 100644 index 000000000..d5c18eb28 --- /dev/null +++ b/examples/statemachine/ping_pong/ping_pong.py @@ -0,0 +1,70 @@ +# Copyright (C) 2010 velociraptor Genjix <aphidia@hotmail.com> +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys + +from PySide6.QtCore import QCoreApplication, QEvent +from PySide6.QtStateMachine import QAbstractTransition, QState, QStateMachine + + +class PingEvent(QEvent): + def __init__(self): + super().__init__(QEvent.Type(QEvent.User + 2)) + + +class PongEvent(QEvent): + def __init__(self): + super().__init__(QEvent.Type(QEvent.User + 3)) + + +class Pinger(QState): + def __init__(self, parent): + super().__init__(parent) + + def onEntry(self, e): + self.p = PingEvent() + self.machine().postEvent(self.p) + print('ping?') + + +class PongTransition(QAbstractTransition): + def eventTest(self, e): + return e.type() == QEvent.User + 3 + + def onTransition(self, e): + self.p = PingEvent() + machine.postDelayedEvent(self.p, 500) + print('ping?') + + +class PingTransition(QAbstractTransition): + def eventTest(self, e): + return e.type() == QEvent.User + 2 + + def onTransition(self, e): + self.p = PongEvent() + machine.postDelayedEvent(self.p, 500) + print('pong!') + + +if __name__ == '__main__': + app = QCoreApplication(sys.argv) + + machine = QStateMachine() + group = QState(QState.ParallelStates) + group.setObjectName('group') + + pinger = Pinger(group) + pinger.setObjectName('pinger') + pinger.addTransition(PongTransition()) + + ponger = QState(group) + ponger.setObjectName('ponger') + ponger.addTransition(PingTransition()) + + machine.addState(group) + machine.setInitialState(group) + machine.start() + + sys.exit(app.exec()) diff --git a/examples/statemachine/ping_pong/ping_pong.pyproject b/examples/statemachine/ping_pong/ping_pong.pyproject new file mode 100644 index 000000000..7fb430352 --- /dev/null +++ b/examples/statemachine/ping_pong/ping_pong.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["ping_pong.py"] +} diff --git a/examples/statemachine/rogue/rogue.py b/examples/statemachine/rogue/rogue.py new file mode 100644 index 000000000..f0ce9c28c --- /dev/null +++ b/examples/statemachine/rogue/rogue.py @@ -0,0 +1,179 @@ +# Copyright (C) 2010 velociraptor Genjix <aphidia@hotmail.com> +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys + +from PySide6.QtCore import (QEvent, QPoint, QRandomGenerator, QSize, Qt, + Property) +from PySide6.QtGui import QFont, QFontMetrics, QFontDatabase, QPainter +from PySide6.QtWidgets import QApplication, QMainWindow +from PySide6.QtStateMachine import (QEventTransition, QFinalState, + QKeyEventTransition, QState, QStateMachine) + + +class MovementTransition(QEventTransition): + def __init__(self, window): + super().__init__(window, QEvent.KeyPress) + self.window = window + + def eventTest(self, event): + if (event.type() == QEvent.StateMachineWrapped + and event.event().type() == QEvent.KeyPress): + key = event.event().key() + return (key == Qt.Key_2 or key == Qt.Key_8 + or key == Qt.Key_6 or key == Qt.Key_4) + return False + + def onTransition(self, event): + key = event.event().key() + if key == Qt.Key_4: + self.window.move_player(self.window.left) + if key == Qt.Key_8: + self.window.move_player(self.window.Up) + if key == Qt.Key_6: + self.window.move_player(self.window.right) + if key == Qt.Key_2: + self.window.move_player(self.window.down) + + +class Custom(QState): + def __init__(self, parent, mw): + super().__init__(parent) + self.mw = mw + + def onEntry(self, e): + print(self.mw.status) + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.pX = 5 + self.pY = 5 + self.width = 35 + self.height = 20 + self._status_str = '' + + font = QFont() + if 'Monospace' in QFontDatabase.families(): + font = QFont('Monospace', 12) + else: + for family in QFontDatabase.families(): + if QFontDatabase.isFixedPitch(family): + font = QFont(family, 12) + self.setFont(font) + + self.setup_map() + self.build_machine() + self.show() + + def setup_map(self): + self.map = [] + generator = QRandomGenerator().global_() + for x in range(self.width): + column = [] + for y in range(self.height): + if (x == 0 or x == self.width - 1 or y == 0 + or y == self.height - 1 or generator.bounded(0, 40) == 0): + column.append('#') + else: + column.append('.') + self.map.append(column) + + def build_machine(self): + machine = QStateMachine(self) + + input_state = Custom(machine, self) + # this line sets the status + self.status = 'hello!' + # however this line does not + input_state.assignProperty(self, 'status', 'Move the rogue with 2, 4, 6, and 8') + + machine.setInitialState(input_state) + machine.start() + + transition = MovementTransition(self) + input_state.addTransition(transition) + + quit_state = QState(machine) + quit_state.assignProperty(self, 'status', 'Really quit(y/n)?') + + yes_transition = QKeyEventTransition(self, QEvent.KeyPress, Qt.Key_Y) + self._final_state = QFinalState(machine) + yes_transition.setTargetState(self._final_state) + quit_state.addTransition(yes_transition) + + no_transition = QKeyEventTransition(self, QEvent.KeyPress, Qt.Key_N) + no_transition.setTargetState(input_state) + quit_state.addTransition(no_transition) + + quit_transition = QKeyEventTransition(self, QEvent.KeyPress, Qt.Key_Q) + quit_transition.setTargetState(quit_state) + input_state.addTransition(quit_transition) + + machine.setInitialState(input_state) + machine.finished.connect(qApp.quit) # noqa: F821 + machine.start() + + def sizeHint(self): + metrics = QFontMetrics(self.font()) + return QSize(metrics.horizontalAdvance('X') * self.width, + metrics.height() * (self.height + 1)) + + def paintEvent(self, event): + metrics = QFontMetrics(self.font()) + with QPainter(self) as painter: + font_height = metrics.height() + font_width = metrics.horizontalAdvance('X') + + painter.fillRect(self.rect(), Qt.black) + painter.setPen(Qt.white) + + y_pos = font_height + painter.drawText(QPoint(0, y_pos), self.status) + for y in range(self.height): + y_pos += font_height + x_pos = 0 + for x in range(self.width): + if y == self.pY and x == self.pX: + x_pos += font_width + continue + painter.drawText(QPoint(x_pos, y_pos), self.map[x][y]) + x_pos += font_width + painter.drawText(QPoint(self.pX * font_width, (self.pY + 2) * font_height), '@') + + def move_player(self, direction): + if direction == self.left: + if self.map[self.pX - 1][self.pY] != '#': + self.pX -= 1 + elif direction == self.right: + if self.map[self.pX + 1][self.pY] != '#': + self.pX += 1 + elif direction == self.Up: + if self.map[self.pX][self.pY - 1] != '#': + self.pY -= 1 + elif direction == self.down: + if self.map[self.pX][self.pY + 1] != '#': + self.pY += 1 + self.repaint() + + def get_status(self): + return self._status_str + + def set_status(self, status): + self._status_str = status + self.repaint() + status = Property(str, get_status, set_status) + Up = 0 + down = 1 + left = 2 + right = 3 + width = 35 + height = 20 + + +if __name__ == '__main__': + app = QApplication(sys.argv) + main_win = MainWindow() + sys.exit(app.exec()) diff --git a/examples/statemachine/rogue/rogue.pyproject b/examples/statemachine/rogue/rogue.pyproject new file mode 100644 index 000000000..b8baf9802 --- /dev/null +++ b/examples/statemachine/rogue/rogue.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["rogue.py"] +} diff --git a/examples/statemachine/trafficlight/doc/trafficlight.png b/examples/statemachine/trafficlight/doc/trafficlight.png Binary files differnew file mode 100644 index 000000000..24d40beec --- /dev/null +++ b/examples/statemachine/trafficlight/doc/trafficlight.png diff --git a/examples/statemachine/trafficlight/doc/trafficlight.rst b/examples/statemachine/trafficlight/doc/trafficlight.rst new file mode 100644 index 000000000..57d369465 --- /dev/null +++ b/examples/statemachine/trafficlight/doc/trafficlight.rst @@ -0,0 +1,10 @@ +Traffic Light Example +===================== + +The Traffic Light example shows how to use The State Machine Framework to +implement the control flow of a traffic light. + + +.. image:: trafficlight.png + :width: 400 + :alt: Traffic Light Screenshot diff --git a/examples/statemachine/trafficlight/trafficlight.py b/examples/statemachine/trafficlight/trafficlight.py new file mode 100644 index 000000000..1e58384f9 --- /dev/null +++ b/examples/statemachine/trafficlight/trafficlight.py @@ -0,0 +1,117 @@ +# Copyright (C) 2010 velociraptor Genjix <aphidia@hotmail.com> +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +import sys + +from PySide6.QtCore import QTimer, Qt, Property, Slot +from PySide6.QtGui import QPainter, QPalette +from PySide6.QtWidgets import QApplication, QVBoxLayout, QWidget +from PySide6.QtStateMachine import QFinalState, QState, QStateMachine + + +class LightWidget(QWidget): + def __init__(self, color): + super().__init__() + self.color = color + self._on_val = False + + def is_on(self): + return self._on_val + + def set_on(self, on): + if self._on_val == on: + return + self._on_val = on + self.update() + + @Slot() + def turn_off(self): + self.set_on(False) + + @Slot() + def turn_on(self): + self.set_on(True) + + def paintEvent(self, e): + if not self._on_val: + return + with QPainter(self) as painter: + painter.setRenderHint(QPainter.Antialiasing) + painter.setBrush(self.color) + painter.drawEllipse(0, 0, self.width(), self.height()) + + on = Property(bool, is_on, set_on) + + +class TrafficLightWidget(QWidget): + def __init__(self): + super().__init__() + vbox = QVBoxLayout(self) + self._red_light = LightWidget(Qt.red) + vbox.addWidget(self._red_light) + self._yellow_light = LightWidget(Qt.yellow) + vbox.addWidget(self._yellow_light) + self._green_light = LightWidget(Qt.green) + vbox.addWidget(self._green_light) + pal = QPalette() + pal.setColor(QPalette.Window, Qt.black) + self.setPalette(pal) + self.setAutoFillBackground(True) + + +def create_light_state(light, duration, parent=None): + light_state = QState(parent) + timer = QTimer(light_state) + timer.setInterval(duration) + timer.setSingleShot(True) + timing = QState(light_state) + timing.entered.connect(light.turn_on) + timing.entered.connect(timer.start) + timing.exited.connect(light.turn_off) + done = QFinalState(light_state) + timing.addTransition(timer.timeout, done) + light_state.setInitialState(timing) + return light_state + + +class TrafficLight(QWidget): + def __init__(self): + super().__init__() + vbox = QVBoxLayout(self) + widget = TrafficLightWidget() + vbox.addWidget(widget) + vbox.setContentsMargins(0, 0, 0, 0) + + machine = QStateMachine(self) + red_going_yellow = create_light_state(widget._red_light, 1000) + red_going_yellow.setObjectName('redGoingYellow') + yellow_going_green = create_light_state(widget._red_light, 1000) + yellow_going_green.setObjectName('yellowGoingGreen') + red_going_yellow.addTransition(red_going_yellow.finished, + yellow_going_green) + green_going_yellow = create_light_state(widget._yellow_light, 3000) + green_going_yellow.setObjectName('greenGoingYellow') + yellow_going_green.addTransition(yellow_going_green.finished, + green_going_yellow) + yellow_going_red = create_light_state(widget._green_light, 1000) + yellow_going_red.setObjectName('yellowGoingRed') + green_going_yellow.addTransition(green_going_yellow.finished, + yellow_going_red) + yellow_going_red.addTransition(yellow_going_red.finished, + red_going_yellow) + + machine.addState(red_going_yellow) + machine.addState(yellow_going_green) + machine.addState(green_going_yellow) + machine.addState(yellow_going_red) + machine.setInitialState(red_going_yellow) + machine.start() + + +if __name__ == '__main__': + app = QApplication(sys.argv) + widget = TrafficLight() + widget.resize(110, 300) + widget.show() + sys.exit(app.exec()) diff --git a/examples/statemachine/trafficlight/trafficlight.pyproject b/examples/statemachine/trafficlight/trafficlight.pyproject new file mode 100644 index 000000000..912472693 --- /dev/null +++ b/examples/statemachine/trafficlight/trafficlight.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["trafficlight.py"] +} |