Task Menu Extension Example

This example shows how to add custom widgets to Qt Designer, which can be launched with pyside6-designer, and to extend its built-in context menu.

The main mechanism is based on the QPyDesignerCustomWidgetCollection class that takes care of handling the registration.

More information can be found in Custom Widgets in Qt Widgets Designer.

Task Menu Extension Screenshot

Download this example

# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from PySide6.QtCore import Qt, QPoint, QRect, QSize, Property, Slot
from PySide6.QtGui import QPainter, QPen
from PySide6.QtWidgets import QWidget


EMPTY = '-'
CROSS = 'X'
NOUGHT = 'O'
DEFAULT_STATE = "---------"


class TicTacToe(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._state = DEFAULT_STATE
        self._turn_number = 0

    def minimumSizeHint(self):
        return QSize(200, 200)

    def sizeHint(self):
        return QSize(200, 200)

    def setState(self, new_state):
        self._turn_number = 0
        self._state = DEFAULT_STATE
        for position in range(min(9, len(new_state))):
            mark = new_state[position]
            if mark == CROSS or mark == NOUGHT:
                self._turn_number += 1
                self._change_state_at(position, mark)
            position += 1
        self.update()

    def state(self):
        return self._state

    @Slot()
    def clear_board(self):
        self._state = DEFAULT_STATE
        self._turn_number = 0
        self.update()

    def _change_state_at(self, pos, new_state):
        self._state = (self._state[:pos] + new_state
                       + self._state[pos + 1:])

    def mousePressEvent(self, event):
        if self._turn_number == 9:
            self.clear_board()
            return
        for position in range(9):
            cell = self._cell_rect(position)
            if cell.contains(event.position().toPoint()):
                if self._state[position] == EMPTY:
                    new_state = CROSS if self._turn_number % 2 == 0 else NOUGHT
                    self._change_state_at(position, new_state)
                    self._turn_number += 1
                    self.update()

    def paintEvent(self, event):
        with QPainter(self) as painter:
            painter.setRenderHint(QPainter.RenderHint.Antialiasing)

            painter.setPen(QPen(Qt.darkGreen, 1))
            painter.drawLine(self._cell_width(), 0,
                             self._cell_width(), self.height())
            painter.drawLine(2 * self._cell_width(), 0,
                             2 * self._cell_width(), self.height())
            painter.drawLine(0, self._cell_height(),
                             self.width(), self._cell_height())
            painter.drawLine(0, 2 * self._cell_height(),
                             self.width(), 2 * self._cell_height())

            painter.setPen(QPen(Qt.darkBlue, 2))

            for position in range(9):
                cell = self._cell_rect(position)
                if self._state[position] == CROSS:
                    painter.drawLine(cell.topLeft(), cell.bottomRight())
                    painter.drawLine(cell.topRight(), cell.bottomLeft())
                elif self._state[position] == NOUGHT:
                    painter.drawEllipse(cell)

            painter.setPen(QPen(Qt.yellow, 3))

            for position in range(0, 8, 3):
                if (self._state[position] != EMPTY
                        and self._state[position + 1] == self._state[position]
                        and self._state[position + 2] == self._state[position]):
                    y = self._cell_rect(position).center().y()
                    painter.drawLine(0, y, self.width(), y)
                    self._turn_number = 9

            for position in range(3):
                if (self._state[position] != EMPTY
                        and self._state[position + 3] == self._state[position]
                        and self._state[position + 6] == self._state[position]):
                    x = self._cell_rect(position).center().x()
                    painter.drawLine(x, 0, x, self.height())
                    self._turn_number = 9

            if (self._state[0] != EMPTY and self._state[4] == self._state[0]
                    and self._state[8] == self._state[0]):
                painter.drawLine(0, 0, self.width(), self.height())
                self._turn_number = 9

            if (self._state[2] != EMPTY and self._state[4] == self._state[2]
                    and self._state[6] == self._state[2]):
                painter.drawLine(0, self.height(), self.width(), 0)
                self._turn_number = 9

    def _cell_rect(self, position):
        h_margin = self.width() / 30
        v_margin = self.height() / 30
        row = int(position / 3)
        column = position - 3 * row
        pos = QPoint(column * self._cell_width() + h_margin,
                     row * self._cell_height() + v_margin)
        size = QSize(self._cell_width() - 2 * h_margin,
                     self._cell_height() - 2 * v_margin)
        return QRect(pos, size)

    def _cell_width(self):
        return self.width() / 3

    def _cell_height(self):
        return self.height() / 3

    state = Property(str, state, setState)
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

"""PySide6 port of the Qt Designer taskmenuextension example from Qt v6.x"""

import sys
from PySide6.QtWidgets import QApplication

from tictactoe import TicTacToe

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = TicTacToe()
    window.state = "-X-XO----"
    window.show()
    sys.exit(app.exec())
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from tictactoe import TicTacToe  # noqa: F401
from tictactoeplugin import TicTacToePlugin

from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection

# Set PYSIDE_DESIGNER_PLUGINS to point to this directory and load the plugin


if __name__ == '__main__':
    QPyDesignerCustomWidgetCollection.addCustomWidget(TicTacToePlugin())
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from tictactoe import TicTacToe
from tictactoetaskmenu import TicTacToeTaskMenuFactory

from PySide6.QtGui import QIcon
from PySide6.QtDesigner import QDesignerCustomWidgetInterface


DOM_XML = """
<ui language='c++'>
    <widget class='TicTacToe' name='ticTacToe'>
        <property name='geometry'>
            <rect>
                <x>0</x>
                <y>0</y>
                <width>200</width>
                <height>200</height>
            </rect>
        </property>
        <property name='state'>
            <string>-X-XO----</string>
        </property>
    </widget>
</ui>
"""


class TicTacToePlugin(QDesignerCustomWidgetInterface):
    def __init__(self):
        super().__init__()
        self._form_editor = None

    def createWidget(self, parent):
        t = TicTacToe(parent)
        return t

    def domXml(self):
        return DOM_XML

    def group(self):
        return ''

    def icon(self):
        return QIcon()

    def includeFile(self):
        return 'tictactoe'

    def initialize(self, form_editor):
        self._form_editor = form_editor
        manager = form_editor.extensionManager()
        iid = TicTacToeTaskMenuFactory.task_menu_iid()
        manager.registerExtensions(TicTacToeTaskMenuFactory(manager), iid)

    def isContainer(self):
        return False

    def isInitialized(self):
        return self._form_editor is not None

    def name(self):
        return 'TicTacToe'

    def toolTip(self):
        return 'Tic Tac Toe Example, demonstrating class QDesignerTaskMenuExtension (Python)'

    def whatsThis(self):
        return self.toolTip()
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations

from tictactoe import TicTacToe

from PySide6.QtCore import Slot
from PySide6.QtGui import QAction
from PySide6.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout
from PySide6.QtDesigner import (QExtensionFactory, QPyDesignerTaskMenuExtension)


class TicTacToeDialog(QDialog):
    def __init__(self, parent):
        super().__init__(parent)
        layout = QVBoxLayout(self)
        self._ticTacToe = TicTacToe(self)
        layout.addWidget(self._ticTacToe)
        button_box = QDialogButtonBox(QDialogButtonBox.Ok
                                      | QDialogButtonBox.Cancel
                                      | QDialogButtonBox.Reset)
        button_box.accepted.connect(self.accept)
        button_box.rejected.connect(self.reject)
        reset_button = button_box.button(QDialogButtonBox.Reset)
        reset_button.clicked.connect(self._ticTacToe.clear_board)
        layout.addWidget(button_box)

    def set_state(self, new_state):
        self._ticTacToe.setState(new_state)

    def state(self):
        return self._ticTacToe.state


class TicTacToeTaskMenu(QPyDesignerTaskMenuExtension):
    def __init__(self, ticTacToe, parent):
        super().__init__(parent)
        self._ticTacToe = ticTacToe
        self._edit_state_action = QAction('Edit State...', None)
        self._edit_state_action.triggered.connect(self._edit_state)

    def taskActions(self):
        return [self._edit_state_action]

    def preferredEditAction(self):
        return self._edit_state_action

    @Slot()
    def _edit_state(self):
        dialog = TicTacToeDialog(self._ticTacToe)
        dialog.set_state(self._ticTacToe.state)
        if dialog.exec() == QDialog.Accepted:
            self._ticTacToe.state = dialog.state()


class TicTacToeTaskMenuFactory(QExtensionFactory):
    def __init__(self, extension_manager):
        super().__init__(extension_manager)

    @staticmethod
    def task_menu_iid():
        return 'org.qt-project.Qt.Designer.TaskMenu'

    def createExtension(self, object, iid, parent):
        if iid != TicTacToeTaskMenuFactory.task_menu_iid():
            return None
        if object.__class__.__name__ != 'TicTacToe':
            return None
        return TicTacToeTaskMenu(object, parent)