1. Обзор

Building custom widgets allows one to use the Qt Designer editor to place a custom widget rather than doing it manually in a handler file.

A useful custom widgets would be a great way to contribute back to LinuxCNC.

1.1. Widgets

Widget is the general name for the UI objects such as buttons and labels in PyQt.

There are also special widgets made for LinuxCNC that make integration easier.

All these widgets can be placed with Qt Designer editor - allowing one to see the result before actually loading the panel in LinuxCNC.

1.2. Qt Designer

Qt Designer is a WYSIWYG (What You See is What You Get) editor for placing PyQt widgets.

It’s original intend was for building the graphic widgets for programs.
We leverage it to build screens and panels for LinuxCNC.

In Qt Designer, on the left side of the editor, you find three categories of LinuxCNC widgets:

  • HAL only widgets.

  • LinuxCNC controller widgets.

  • dialog widgets.

For Qt Designer to add custom widgets to it’s editor it must have a plugin added to the right folder.

1.3. Initialization Process

QtVCP does extra setup for widgets subclassed from _HALWidgetBase, aka "HAL-ified" widgets.

This includes:

  • Injecting important variables,

  • Calling an extra setup function

  • Calling a closing cleanup function at shutdown.

These functions are not called when the Qt Designer editor displays the widgets.

When QtVCP builds a screen from the .ui file:

  1. It searches for all the HAL-ified widgets.

  2. It finds the ScreenOptions widget, to collect information it needs to inject into the other widgets

  3. It instantiates each widget and if it is a HAL-ified widget, calls the hal_init() function.
    hal_init() is defined in the base class and it:

    1. Adds variables such as the preference file to every HAL-ified widget.

    2. Call +_hal_init()+ on the widget.
      +_hal_init()+ allows the widget designer to do setup that requires access to the extra variables.

Here is a description of the extra variables injected into "HAL-ified" widgets:

self.HAL_GCOMP

The HAL component instance

self.HAL_NAME

This widget’s name as a string

self.QT_OBJECT_

This widget’s object instance

self.QTVCP_INSTANCE_

The very top level parent of the screen

self.PATHS_

The QtVCP’s path library instance

self.PREFS_

The optional preference file instance

self.SETTINGS_

The Qsettings object instance

1.4. cleanup process

When QtVCP closes, it calls the +_hal_cleanup()+ function on all HAL-ified widgets.

The base class creates an empty +_hal_cleanup()+ function, which can be redefined in the custom widget subclass.

This can be used to do such things as record preferences, etc.

This function is not called when the Qt Designer editor displays the widgets.

2. Custom HAL Widgets

HAL widgets are the simplest to show example of.
qtvcp/widgets/simple_widgets.py holds many HAL only widgets.

Lets look at a snippet of simple_widgets.py:

In the Imports section

This is where we import libraries that our widget class needs.

#!/usr/bin/env python3

###############################
# Imports
###############################
from PyQt5 import QtWidgets                     # <1>
from qtvcp.widgets.widget_baseclass \
    import _HalWidgetBase, _HalSensitiveBase    # <2>
import hal                                      # <3>

In this case we need access to:

  1. PyQt’s QtWidgets library,

  2. LinuxCNC’s HAL library, and

  3. QtVCP’s widget baseclass 's _HalSensitiveBase for automatic HAL pin setup and to disable/enable the widget (also known as input sensitivity).
    There is also _HalToggleBase, and _HalScaleBase functions available in the library._HalToggleBase, and _HalScaleBase.

In the WIDGET section

Here is a custom widget based on PyQt’s QGridLayout widget.

QGridLayout allows one to:

  • Place objects in a grid fashion.

  • Enable/disable all widgets inside it based on a HAL pin state.

######################
# WIDGET
######################

class Lcnc_GridLayout(QtWidgets.QWidget, _HalSensitiveBase):    # <1>
    def __init__(self, parent = None):                          # <2>
        super(GridLayout, self).__init__(parent)                # <3>

Line by Line:

  1. This defines the class name and the libraries it inherits from.
    This class, named Lcnc_GridLayout, inherits the functions of QWidget and +_HalSensitiveBase+.
    +_HalSensitiveBase+ is subclass of +_HalWidgetBase+, the base class of most QtVCP widgets, meaning it has all the functions of +_HalWidgetBase+ plus the functions of +_HalSensitiveBase+.
    It adds the function to make the widget be enabled or disabled based on a HAL input BIT pin.

  2. This is the function called when the widget is first made (said instantiated) - this is pretty standard.

  3. This function initializes our widget’s Super classes.
    Super just means the inherited baseclasses, that is QWidget and _HalSensitiveBase.
    Pretty standard other than the widget name will change.

3. Custom Controller Widgets Using STATUS

Widget that interact with LinuxCNC’s controller are only a little more complicated and they require some extra libraries.

In this cut down example we will add properties that can be changed in Qt Designer.

This LED indicator widget will respond to selectable LinuxCNC controller states.

#!/usr/bin/env python3

###############################
# Imports
###############################
from PyQt5.QtCore import pyqtProperty
from qtvcp.widgets.led_widget import LED
from qtvcp.core import Status

###########################################
# **** instantiate libraries section **** #
###########################################
STATUS = Status()

##########################################
# custom widget class definition
##########################################
class StateLED(LED):
    def __init__(self, parent=None):
        super(StateLED, self).__init__(parent)
        self.has_hal_pins = False
        self.setState(False)
        self.is_estopped = False
        self.is_on = False
        self.invert_state = False

    def _hal_init(self):
        if self.is_estopped:
            STATUS.connect('state-estop', lambda w:self._flip_state(True))
            STATUS.connect('state-estop-reset', lambda w:self._flip_state(False))
        elif self.is_on:
            STATUS.connect('state-on', lambda w:self._flip_state(True))
            STATUS.connect('state-off', lambda w:self._flip_state(False))

    def _flip_state(self, data):
            if self.invert_state:
                data = not data
            self.change_state(data)

    #########################################################################
    # Qt Designer properties setter/getters/resetters
    ########################################################################

    # invert status
    def set_invert_state(self, data):
        self.invert_state = data
    def get_invert_state(self):
        return self.invert_state
    def reset_invert_state(self):
        self.invert_state = False

    # machine is estopped status
    def set_is_estopped(self, data):
        self.is_estopped = data
    def get_is_estopped(self):
        return self.is_estopped
    def reset_is_estopped(self):
        self.is_estopped = False

    # machine is on status
    def set_is_on(self, data):
        self.is_on = data
    def get_is_on(self):
        return self.is_on
    def reset_is_on(self):
        self.is_on = False

    #######################################
    # Qt Designer properties
    #######################################
    invert_state_status = pyqtProperty(bool, get_invert_state, set_invert_state, reset_invert_state)
    is_estopped_status = pyqtProperty(bool, get_is_estopped, set_is_estopped, reset_is_estopped)
    is_on_status = pyqtProperty(bool, get_is_on, set_is_on, reset_is_on)

3.1. In The Imports Section

This is where we import libraries that our widget class needs.

#!/usr/bin/env python3

###############################
# Imports
###############################
from PyQt5.QtCore import pyqtProperty       # <1>
from qtvcp.widgets.led_widget import LED    # <2>
from qtvcp.core import Status               # <3>

We import

  1. pyqtProperty so we can interact with the Qt Designer editor,

  2. LED because our custom widget is based on it,

  3. Status because it gives us status messages from LinuxCNC.

3.2. In The Instantiate Libraries Section

Here we create the Status library instance:

###########################################
# **** instantiate libraries section **** #
###########################################
STATUS = Status()

Typically we instantiated the library outside of the widget class so that the reference to it is global - meaning you don’t need to use self. in front of it.

By convention we use all capital letters in the name for global references.

3.3. In The Custom Widget Class Definition Section

This is the meat and potatoes of our custom widget.

Class definition and instance initialization function
class StateLed(LED):                            # <1>
    def __init__(self, parent=None):            # <2>
        super(StateLed, self).__init__(parent)  # <3>
        self.has_hal_pins = False               # <4>
        self.setState(False)                    # <5>
        self.is_estopped = False
        self.is_on = False
        self.invert_state = False
  1. Defines the name of our custom widget and what other class it inherits from.
    In this case we inherit LED - a QtVCP widget that represents a status light.

  2. Typical of most widgets - called when the widget is first made.

  3. Typical of most widgets - calls the parent (super) widget initialization code.

    Then we set some attributes:

  4. Inherited from Lcnc_Led - we set it here so no HAL pin is made.

  5. Inherited from Lcnc_led - we set it to make sure the LED is off.

The other attributes are for the selectable options of our widget.

Widget’s HAL initialization function
    def _hal_init(self):
        if self.is_estopped:
            STATUS.connect('state-estop', lambda w:self._flip_state(True))
            STATUS.connect('state-estop-reset', lambda w:self._flip_state(False))
        elif self.is_on:
            STATUS.connect('state-on', lambda w:self._flip_state(True))
            STATUS.connect('state-off', lambda w:self._flip_state(False))

This function connects STATUS (LinuxCNC status message library) to our widget, so that the LED will on or off based on the selected state of the controller.

We have two states we can choose from is_estopped or is_on.
Depending on which is active our widget get connected to the appropriate STATUS messages.

+_hal_init()+ is called on each widget that inherits +_HalWidgetBase+, when QtVCP first builds the screen.
You might wonder why it’s called on this widget since we didn’t have +_HalWidgetBase+ in our class definition (class Lcnc_State_Led(Lcnc_Led):) - it’s called because Lcnc_Led inherits +_HalWidgetBase+.

In this function you have access to some extra information (though we don’t use them in this example):

self.HAL_GCOMP

the HAL component instance

self.HAL_NAME

This widget’s name as a string

self.QT_OBJECT_

This widget’s PyQt object instance

self.QTVCP_INSTANCE_

The very top level parent of the screen

self.PATHS_

The instance of QtVCP’s path library

self.PREFS_

the instance of an optional preference file

self.SETTINGS_

the Qsettings object

We could use this information to create HAL pins or look up image paths etc.

STATUS.connect('state-estop', lambda w:self._flip_state(True))

Lets look at this line more closely:

  • STATUS is very common theme is widget building.
    STATUS uses GObject message system to send messages to widgets that register to it.
    This line is the registering process.

  • state-estop is the message we wish to listen for and act on. There are many messages available.

  • lambda w:self._flip_state(True) is what happens when the message is caught.
    The lambda function accepts the widget instance (w) that GObject sends it and then calls the function self._flip_state(True).
    Lambda was used to strip the (w) object before calling the self._flip_state function.
    It also allowed use to send self._flip_state() the True state.

    def _flip_state(self, data):
            if self.invert_state:
                data = not data
            self.change_state(data)

This is the function that actually flips the state of the LED.
It is what gets called when the appropriate STATUS message is accepted.

STATUS.connect('current-feed-rate', self._set_feedrate_text)

The function called looks like this:

    def _set_feedrate_text(self, widget, data):

in which the widget and any data must be accepted by the function.

3.3.1. In the Designer Properties Setter/Getters/Resetters Section

    #########################################################################
    # Qt Designer properties setter/getters/resetters
    ########################################################################

    # invert status
    def set_invert_state(self, data):
        self.invert_state = data
    def get_invert_state(self):
        return self.invert_state
    def reset_invert_state(self):
        self.invert_state = False

    # machine is estopped status
    def set_is_estopped(self, data):
        self.is_estopped = data
    def get_is_estopped(self):
        return self.is_estopped
    def reset_is_estopped(self):
        self.is_estopped = False

    # machine is on status
    def set_is_on(self, data):
        self.is_on = data
    def get_is_on(self):
        return self.is_on
    def reset_is_on(self):
        self.is_on = False

This is how Qt Designer sets the attributes of the widget.
This can also be called directly in the widget.

3.3.2. In the Designer properties section

    #######################################
    # Qt Designer properties
    #######################################
    invert_state_status = pyqtProperty(bool, get_invert_state, set_invert_state, reset_invert_state)
    is_estopped_status = pyqtProperty(bool, get_is_estopped, set_is_estopped, reset_is_estopped)
    is_on_status = pyqtProperty(bool, get_is_on, set_is_on, reset_is_on)

This is the registering of properties in Qt Designer.

The property name:

  • is the text used in Qt Designer,

  • cannot be the same as the attributes they represent.

These properties show in Qt Designer in the order they appear here.

4. Custom Controller Widgets with Actions

Here is an example of a widget that sets the user reference system.

It changes:

  • the machine controller state using the ACTION library,

  • whether the button can be clicked or not using the STATUS library.

import os
import hal

from PyQt5.QtWidgets import QWidget, QToolButton, QMenu, QAction
from PyQt5.QtCore import Qt, QEvent, pyqtProperty, QBasicTimer, pyqtSignal
from PyQt5.QtGui import QIcon

from qtvcp.widgets.widget_baseclass import _HalWidgetBase
from qtvcp.widgets.dialog_widget import EntryDialog
from qtvcp.core import Status, Action, Info

# Instantiate the libraries with global reference
# STATUS gives us status messages from LinuxCNC
# INFO holds INI details
# ACTION gives commands to LinuxCNC
STATUS = Status()
INFO = Info()
ACTION = Action()

class SystemToolButton(QToolButton, _HalWidgetBase):
    def __init__(self, parent=None):
        super(SystemToolButton, self).__init__(parent)
        self._joint = 0
        self._last = 0
        self._block_signal = False
        self._auto_label_flag = True
        SettingMenu = QMenu()
        for system in('G54', 'G55', 'G56', 'G57', 'G58', 'G59', 'G59.1', 'G59.2', 'G59.3'):

            Button = QAction(QIcon('exit24.png'), system, self)
            Button.triggered.connect(self[system.replace('.','_')])
            SettingMenu.addAction(Button)

        self.setMenu(SettingMenu)
        self.dialog = EntryDialog()

    def _hal_init(self):
        if not self.text() == '':
            self._auto_label_flag = False
        def homed_on_test():
            return (STATUS.machine_is_on()
                    and (STATUS.is_all_homed() or INFO.NO_HOME_REQUIRED))

        STATUS.connect('state-off', lambda w: self.setEnabled(False))
        STATUS.connect('state-estop', lambda w: self.setEnabled(False))
        STATUS.connect('interp-idle', lambda w: self.setEnabled(homed_on_test()))
        STATUS.connect('interp-run', lambda w: self.setEnabled(False))
        STATUS.connect('all-homed', lambda w: self.setEnabled(True))
        STATUS.connect('not-all-homed', lambda w, data: self.setEnabled(False))
        STATUS.connect('interp-paused', lambda w: self.setEnabled(True))
        STATUS.connect('user-system-changed', self._set_user_system_text)

    def G54(self):
        ACTION.SET_USER_SYSTEM('54')

    def G55(self):
        ACTION.SET_USER_SYSTEM('55')

    def G56(self):
        ACTION.SET_USER_SYSTEM('56')

    def G57(self):
        ACTION.SET_USER_SYSTEM('57')

    def G58(self):
        ACTION.SET_USER_SYSTEM('58')

    def G59(self):
        ACTION.SET_USER_SYSTEM('59')

    def G59_1(self):
        ACTION.SET_USER_SYSTEM('59.1')

    def G59_2(self):
        ACTION.SET_USER_SYSTEM('59.2')

    def G59_3(self):
        ACTION.SET_USER_SYSTEM('59.3')

    def _set_user_system_text(self, w, data):
        convert = { 1:"G54", 2:"G55", 3:"G56", 4:"G57", 5:"G58", 6:"G59", 7:"G59.1", 8:"G59.2", 9:"G59.3"}
        if self._auto_label_flag:
            self.setText(convert[int(data)])

    def ChangeState(self, joint):
        if int(joint) != self._joint:
            self._block_signal = True
            self.setChecked(False)
            self._block_signal = False
            self.hal_pin.set(False)

    ##############################
    # required class boiler code #
    ##############################

    def __getitem__(self, item):
        return getattr(self, item)
    def __setitem__(self, item, value):
        return setattr(self, item, value)

5. Stylesheet Property Changes Based On Events

It’s possible to have widgets restyled when events change.

You must explicitly "polish" the widget to have PyQt redo the style.
This is a relatively expensive function so should be used sparingly.

This example sets an isHomed property based on LinuxCNC’s homed state and in turn uses it to change stylesheet properties:

This example will set the property isHomed based on LinuxCNC’s homed state.
class HomeLabel(QLabel, _HalWidgetBase):
    def __init__(self, parent=None):
        super(HomeLabel, self).__init__(parent)
        self.joint_number = 0
        # for stylesheet reading
        self._isHomed = False

    def _hal_init(self):
        super(HomeLabel, self)._hal_init()
        STATUS.connect('homed', lambda w,d: self._home_status_polish(int(d), True))
        STATUS.connect('unhomed', lambda w,d: self._home_status_polish(int(d), False))

    # update ishomed property
    # polish widget so stylesheet sees the property change
    # some stylesheets color the text on home/unhome
    def _home_status_polish(self, d, state):
        if self.joint_number = d:
            self.setProperty('isHomed', state)
            self.style().unpolish(self)
            self.style().polish(self)

    # Qproperty getter and setter
    def getisHomed(self):
        return self._isHomed
    def setisHomed(self, data):
        self._isHomed = data

    # Qproperty
    isHomed = QtCore.pyqtProperty(bool, getisHomed, setisHomed)

Here is a sample stylesheet to change text color based on home state.

In this case any widget based on the HomeLabel widget above will change text color.
You would usually pick specific widgets using HomeLabel #specific_widget_name[homed=true]:

HomeLabel[homed=true] {
    color: green;
}
HomeLabel[homed=false] {
    color: red;
}

6. Use Stylesheets To Change Custom Widget Properties

class Label(QLabel):
    def __init__(self, parent=None):
        super(Label, self).__init__(parent)
        alternateFont0 = self.font

    # Qproperty getter and setter
    def getFont0(self):
        return self.aleternateFont0
    def setFont0(self, value):
        self.alternateFont0(value)
    # Qproperty
    styleFont0 = pyqtProperty(QFont, getFont0, setFont0)

Sample stylesheet that sets a custom widget property.

Label{
    qproperty-styleFont0: "Times,12,-1,0,90,0,0,0,0,0";
}

7. Widget Plugins

We must register our custom widget for Qt Designer to use them.

Here are a typical samples.
They would need to be added to qtvcp/plugins/
Then qtvcp/plugins/qtvcp_plugin.py would need to be adjusted to import them.

7.1. Gridlayout Example

#!/usr/bin/env python3

from PyQt5 import QtCore, QtGui
from PyQt5.QtDesigner import QPyDesignerCustomWidgetPlugin
from qtvcp.widgets.simple_widgets import Lcnc_GridLayout
from qtvcp.widgets.qtvcp_icons import Icon
ICON = Icon()

####################################
# GridLayout
####################################
class LcncGridLayoutPlugin(QPyDesignerCustomWidgetPlugin):
    def __init__(self, parent = None):
        QPyDesignerCustomWidgetPlugin.__init__(self)
        self.initialized = False
    def initialize(self, formEditor):
        if self.initialized:
            return
        self.initialized = True
    def isInitialized(self):
        return self.initialized
    def createWidget(self, parent):
        return Lcnc_GridLayout(parent)
    def name(self):
        return "Lcnc_GridLayout"
    def group(self):
        return "LinuxCNC - HAL"
    def icon(self):
        return QtGui.QIcon(QtGui.QPixmap(ICON.get_path('lcnc_gridlayout')))
    def toolTip(self):
        return "HAL enable/disable GridLayout widget"
    def whatsThis(self):
        return ""
    def isContainer(self):
        return True
    def domXml(self):
        return '<widget class="Lcnc_GridLayout" name="lcnc_gridlayout" />\n'
    def includeFile(self):
        return "qtvcp.widgets.simple_widgets"

7.2. SystemToolbutton Example

#!/usr/bin/env python3

from PyQt5 import QtCore, QtGui
from PyQt5.QtDesigner import QPyDesignerCustomWidgetPlugin
from qtvcp.widgets.system_tool_button import SystemToolButton
from qtvcp.widgets.qtvcp_icons import Icon
ICON = Icon()

####################################
# SystemToolButton
####################################
class SystemToolButtonPlugin(QPyDesignerCustomWidgetPlugin):
    def __init__(self, parent = None):
        super(SystemToolButtonPlugin, self).__init__(parent)
        self.initialized = False
    def initialize(self, formEditor):
        if self.initialized:
            return
        self.initialized = True
    def isInitialized(self):
        return self.initialized
    def createWidget(self, parent):
        return SystemToolButton(parent)
    def name(self):
        return "SystemToolButton"
    def group(self):
        return "LinuxCNC - Controller"
    def icon(self):
        return QtGui.QIcon(QtGui.QPixmap(ICON.get_path('systemtoolbutton')))
    def toolTip(self):
        return "Button for selecting a User Coordinate System"
    def whatsThis(self):
        return ""
    def isContainer(self):
        return False
    def domXml(self):
        return '<widget class="SystemToolButton" name="systemtoolbutton" />\n'
    def includeFile(self):
        return "qtvcp.widgets.system_tool_button"

7.3. Making a plugin with a MenuEntry dialog box

It possible to add an entry to the dialog that pops up when you right click the widget in the layout.

This can do things such as selecting options in a more convenient way.

This is the plugin used for action buttons.

#!/usr/bin/env python3

import sip
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtDesigner import QPyDesignerCustomWidgetPlugin, \
                QPyDesignerTaskMenuExtension, QExtensionFactory, \
                QDesignerFormWindowInterface, QPyDesignerMemberSheetExtension
from qtvcp.widgets.action_button import ActionButton
from qtvcp.widgets.qtvcp_icons import Icon
ICON = Icon()

Q_TYPEID = {
    'QDesignerContainerExtension':     'org.qt-project.Qt.Designer.Container',
    'QDesignerPropertySheetExtension': 'org.qt-project.Qt.Designer.PropertySheet',
    'QDesignerTaskMenuExtension': 'org.qt-project.Qt.Designer.TaskMenu',
    'QDesignerMemberSheetExtension': 'org.qt-project.Qt.Designer.MemberSheet'
}

####################################
# ActionBUTTON
####################################
class ActionButtonPlugin(QPyDesignerCustomWidgetPlugin):

    # The __init__() method is only used to set up the plugin and define its
    # initialized variable.
    def __init__(self, parent=None):
        super(ActionButtonPlugin, self).__init__(parent)
        self.initialized = False

    # The initialize() and isInitialized() methods allow the plugin to set up
    # any required resources, ensuring that this can only happen once for each
    # plugin.
    def initialize(self, formEditor):

        if self.initialized:
            return
        manager = formEditor.extensionManager()
        if manager:
            self.factory = ActionButtonTaskMenuFactory(manager)
            manager.registerExtensions(self.factory, Q_TYPEID['QDesignerTaskMenuExtension'])
        self.initialized = True

    def isInitialized(self):
        return self.initialized

    # This factory method creates new instances of our custom widget
    def createWidget(self, parent):
        return ActionButton(parent)

    # This method returns the name of the custom widget class
    def name(self):
        return "ActionButton"

    # Returns the name of the group in Qt Designer's widget box
    def group(self):
        return "LinuxCNC - Controller"

    # Returns the icon
    def icon(self):
        return QtGui.QIcon(QtGui.QPixmap(ICON.get_path('actionbutton')))

    # Returns a tool tip short description
    def toolTip(self):
        return "Action button widget"

    # Returns a short description of the custom widget for use in a "What's
    # This?" help message for the widget.
    def whatsThis(self):
        return ""

    # Returns True if the custom widget acts as a container for other widgets;
    def isContainer(self):
        return False

    # Returns an XML description of a custom widget instance that describes
    # default values for its properties.
    def domXml(self):
        return '<widget class="ActionButton" name="actionbutton" />\n'

    # Returns the module containing the custom widget class. It may include
    # a module path.
    def includeFile(self):
        return "qtvcp.widgets.action_button"


class ActionButtonDialog(QtWidgets.QDialog):

   def __init__(self, widget, parent = None):

      QtWidgets.QDialog.__init__(self, parent)

      self.widget = widget

      self.previewWidget = ActionButton()

      buttonBox = QtWidgets.QDialogButtonBox()
      okButton = buttonBox.addButton(buttonBox.Ok)
      cancelButton = buttonBox.addButton(buttonBox.Cancel)

      okButton.clicked.connect(self.updateWidget)
      cancelButton.clicked.connect(self.reject)

      layout = QtWidgets.QGridLayout()
      self.c_estop = QtWidgets.QCheckBox("Estop Action")
      self.c_estop.setChecked(widget.estop )
      layout.addWidget(self.c_estop)

      layout.addWidget(buttonBox, 5, 0, 1, 2)
      self.setLayout(layout)

      self.setWindowTitle(self.tr("Set Options"))

   def updateWidget(self):

      formWindow = QDesignerFormWindowInterface.findFormWindow(self.widget)
      if formWindow:
          formWindow.cursor().setProperty("estop_action",
              QtCore.QVariant(self.c_estop.isChecked()))
      self.accept()

class ActionButtonMenuEntry(QPyDesignerTaskMenuExtension):

    def __init__(self, widget, parent):
        super(QPyDesignerTaskMenuExtension, self).__init__(parent)
        self.widget = widget
        self.editStateAction = QtWidgets.QAction(
          self.tr("Set Options..."), self)
        self.editStateAction.triggered.connect(self.updateOptions)

    def preferredEditAction(self):
        return self.editStateAction

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

    def updateOptions(self):
        dialog = ActionButtonDialog(self.widget)
        dialog.exec_()

class ActionButtonTaskMenuFactory(QExtensionFactory):
    def __init__(self, parent = None):
        QExtensionFactory.__init__(self, parent)

    def createExtension(self, obj, iid, parent):

        if not isinstance(obj, ActionButton):
            return None
        if iid == Q_TYPEID['QDesignerTaskMenuExtension']:
            return ActionButtonMenuEntry(obj, parent)
        elif iid == Q_TYPEID['QDesignerMemberSheetExtension']:
            return ActionButtonMemberSheet(obj, parent)
        return None