Qt QPushButton with states

qt

#1

Here is what i want to achieve:

image

I want to have same button but with 2 different states (Start and Reset)

Changing the state not only means that another function would be executed, but also changes in the UI itself. (For example in Start state all other buttons are disabled)

Sorry if it is a stupid question, but i was trying to find any examples to follow but was not able to.
Would be grateful for any hints or examples!


#2

I didn’t actually test this code, but this is one idea. There are many ways to do this.
I’ve sub-classed QPushButton and connected its clicked signal to my own function, this emits new custom signals you can connect to call whatever you want. I really recommend using signals, don’t embed functionality into widgets! It stores its state internally as an int but ideally you’d use an enum to make it clearer. Another option would be to only emit one signal but emit it with the current state and then react to that state in the caller.

from PySide2 import QtWidgets, QtCore


class StateButton(QtWidgets.QPushButton):
    clickedStart = QtCore.Signal()
    clickedReset = QtCore.Signal()

    def __init__(self, parent=None):
        super(StateButton, self).__init__(parent=parent)

        self._state = 0

        #init
        self._update_text()
        self.clicked.connect(self._emit_state)

    def _emit_state(self):
        if self._state == 0:
            self.clickedStart.emit()
            self._state = 1

        elif self._state == 1:
            self.clickedReset.emit()
            self._state = 0

        self._update_text()

    def _update_text(self):
        if self._state == 0:
            self.setText("Start")
        elif self._state == 1:
            self.setText("Reset")

#3

Adding to @leocov’s answer, also have a look at QPushButton.setCheckable(True) for giving the button checkbox functionality and QWidget.setEnabled() for disabling that group box when enabled is False.


#4

Thank you so much guys, it took me a while, but i was able to put it together!

I also needed an extra state of the button to handle user error - in my script i need to check if anything selected before executing actual functions - so i added an extra signal to the provided example (Thanks @leocov ! )
I guess i also need to check if user selects geometry rather then lights or locators or whatever…

As for the disabling group boxes I just have an extra variable to set-up once i know i want to change the visibility and a function to run when i want to do that (Thanks @marcuso ! )

Here is what i have right now,
It might be ugly and not very efficient as i am still such an amateur in python, but it gets the job done!
Thanks again, i would love to hear additional critique or advise if you have any!

from maya import cmds
from Qt import QtWidgets, QtCore, QtGui

# Custom QPushButton with multiple states/signals
class StateButton(QtWidgets.QPushButton):
    clickedStart = QtCore.Signal()
    clickedStartError = QtCore.Signal()
    clickedReset = QtCore.Signal()

    def __init__(self, parent=None):
        super(StateButton, self).__init__(parent=parent)
        self._state = 0

        #init
        self._update_text()
        self.clicked.connect(self._emit_state)


    def _emit_state(self):

        # Here i check if anything selected or not,
        # if yes we can change the state and proceed with the script,
        # if not - rise an error
        if self._state == 0:
            current_selection = cmds.ls(selection=True)
            if not current_selection:
                self.clickedStartError.emit()
                self._state = 0

            else:
                self.clickedStart.emit()
                self._state = 1

        elif self._state == 1:
            self.clickedReset.emit()
            self._state = 0

        self._update_text()

    def _update_text(self):
        if self._state == 0:
            self.setText("Start")
        elif self._state == 1:
            self.setText("Reset")



class Gradient_Baker_UI(QtWidgets.QTabWidget):

    def __init__(self):
        super(Gradient_Baker_UI, self).__init__()

        self.States_Button = StateButton()

        # Defining text messages for the Log window
        self.start_message = 'Select object(s) you want to work with and press Start'
        self.start_successful_message = 'Further instructions'
        self.nothing_selected_error_message = 'Nothing is selected, select an object or group'

        # Initial text in Log window
        self.log_text = self.start_message

        # Tabs
        self.tab1 = QtWidgets.QWidget()
        self.tab2 = QtWidgets.QWidget()
        self.tab3 = QtWidgets.QWidget()

        self.addTab(self.tab1, 'Tab 1')
        self.addTab(self.tab2, 'Tab 2')
        self.addTab(self.tab3, 'Tab 3')
        self.tab_01_UI()
        self.tab_02_UI()
        self.tab_03_UI()
        self.setWindowTitle('Gradient Baker')


    def tab_01_UI(self):
        layout = QtWidgets.QVBoxLayout()
        self.setTabText(0, "Baker")
        self.tab1.setLayout(layout)

        # Log window with overwrite mode turned on
        self.log_window = QtWidgets.QPlainTextEdit('%s' % self.log_text)
        self.log_window.setReadOnly(True)
        self.log_window.setOverwriteMode(True)
        self.log_window.setMaximumHeight(60)

        # Start/Reset button and its signals assigned to functions
        self.start_reset_button = self.States_Button

        self.start_reset_button.clickedStart.connect(self.start_function)
        self.start_reset_button.clickedStartError.connect(self.nothing_selected_error_function)
        self.start_reset_button.clickedReset.connect(self.reset_function)

        # Axis group box
        x_button = QtWidgets.QPushButton('X')
        x_button.setAutoExclusive(True)
        x_button.setCheckable(True)

        y_button = QtWidgets.QPushButton('Y')
        y_button.setAutoExclusive(True)
        y_button.setCheckable(True)
        y_button.setChecked(True)

        z_button = QtWidgets.QPushButton('Z')
        z_button.setAutoExclusive(True)
        z_button.setCheckable(True)

        # Another button
        self.reproject_faces_button = QtWidgets.QPushButton('Re-project for selected faces')

        self.axis_group_box = QtWidgets.QGroupBox("Axis")

        axis_group_box_layout = QtWidgets.QGridLayout()

        axis_group_box_layout.addWidget(x_button, 0, 0)
        axis_group_box_layout.addWidget(y_button, 0, 1)
        axis_group_box_layout.addWidget(z_button, 0, 2)
        axis_group_box_layout.addWidget(self.reproject_faces_button, 1, 0, 1, 0)
        self.axis_group_box.setLayout(axis_group_box_layout)


        layout.addWidget(self.log_window)
        layout.addWidget(self.start_reset_button)
        layout.addWidget(self.axis_group_box)

        self.locked = 1
        self.update_ui_visibility()


    def tab_02_UI(self):
        layout = QtWidgets.QVBoxLayout()
        self.setTabText(1, "Presets")
        self.tab2.setLayout(layout)


    def tab_03_UI(self):
        layout = QtWidgets.QVBoxLayout()
        self.setTabText(2, "About")
        self.tab3.setLayout(layout)


    # Changes text in the Log
    def text_change(self):
        self.log_window.setPlainText('%s' % self.log_text)


    # Error message style for the Log
    def log_window_error_stylesheet(self):
        self.log_window.setStyleSheet("""QPlainTextEdit {background-color: #ff6666; color: #333333;}""")

    # Default message style for the Log
    def log_window_default_stylesheet(self):
        self.log_window.setStyleSheet("")

    # Start function
    def start_function(self):
        self.log_text = self.start_successful_message
        self.text_change()
        self.log_window_default_stylesheet()
        self.locked = 0
        self.update_ui_visibility()

    # Reset function
    def reset_function(self):
        self.log_text = self.start_message
        self.text_change()
        self.log_window_default_stylesheet()
        self.locked = 1
        self.update_ui_visibility()

    # Error message function
    def nothing_selected_error_function(self):
        self.log_text = self.nothing_selected_error_message
        self.text_change()
        self.log_window_error_stylesheet()
        raise RuntimeError(self.nothing_selected_error_message)
        self.locked = 1
        self.update_ui_visibility()

    # Function to enable/disable group box
    def update_ui_visibility(self):
        if self.locked == 1:
            self.axis_group_box.setDisabled(True)

        elif self.locked == 0:
            self.axis_group_box.setDisabled(False)



# Function to show UI in Maya
def show_UI():
    ui = Gradient_Baker_UI()
    ui.show()
    return ui

from GradientBaker import Temp_UI
reload (Temp_UI)

ui = Temp_UI.show_UI()


#5

Ideally you’d not handle the selection check in the StateButton._emit_state()

youd check that in your start_function() and if nothing is selected you could then reset the state of you button with a new method in StateButton

def reset(self):
    self._state=0
    self._update_text()

You really dont want to embed funtionality into generic widgets because then you can use them in other places, and you wont forget were some random piece of code is located. Ideally you’d structure your code to separate the GUI from the functionality so that you could “in theory” run everything your gui does by just typing in commands.