0

I am trying to set the forbidden cursor to a dynamically enabled/disabled line edit. But it does not seem to work at all.

from PySide6.QtCore import Qt
from PySide6.QtGui import QCursor

def toggle_line_edit(self, switch_type: SwitchType):
    match switch_type:
        case SwitchType.One:
            self.ui.line_edit.setCursor(QCursor(Qt.ForbiddenCursor))
            self.ui.line_edit.setDisabled(True)
        case SwitchType.Two:
            self.ui.line_edit.setCursor(QCursor(Qt.IBeamCursor))
            self.ui.line_edit.setDisabled(False)

The reason I need this behaviour is because there is a kind of expert mode that unlocks certain fields when activated. This is activated via a radio button group in the UI.

My goal would be that when the expert mode is deactivated, all inputs that require expert mode are "deactivated" in a way and that you are told with the cursor as well as with a tool tip that you can only change these fields when you activate the expert mode.

Is there something I miss?

sebwr
  • 113
  • 1
  • 10

1 Answers1

1

This is not just about QLineEdit: any disabled widget will not show its custom cursor whenever it is disabled.

That's for a valid reason, at least from a generic point of view: a disabled widget does not provide any user interaction (keyboard and mouse) so there is no point in showing a different cursor that could make the user believe that the widget is, in fact, enabled.

Note that, while you could make the QLineEdit read only, that wouldn't prevent some level of interaction: for instance, it would still provide text selection with the mouse and even with the keyboard (after it has been clicked or reached using Tab).

A possible alternative would be to completely prevent any interaction; the focus capability can be cleared by setting the policy to NoFocus, while mouse interaction requires ignoring all mouse events. For simplicity, it's better to use a subclass:

class FakeDisabledLineEdit(QLineEdit):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setCursor(Qt.ForbiddenCursor)
        self.setFocusPolicy(Qt.NoFocus)

    def event(self, event):
        if isinstance(event, QMouseEvent):
            return True
        return super().event(event)

Note: you seem to be using a UI created from Designer, so, in order to use such a subclass in your UI you will need widget promotion. Consider doing some research on the subject, starting from this post of mine.

The only problem might be that the widget would still appear as enabled, so a possible (dirty) workaround could be to temporarily set the WA_Disabled flag in the paint event. This will make Qt believe that the widget is disabled, and let the style draw its contents accordingly:

class FakeDisabledLineEdit(QLineEdit):
    # ... as above

    def paintEvent(self, event):
        enabled = self.isEnabled()
        if enabled:
            self.setAttribute(Qt.WA_Disabled, True)
        super().paintEvent(event)
        if enabled:
            self.setAttribute(Qt.WA_Disabled, False)

Edit based on comments

Since you want to show a visible "hint" that the field can, in fact, become enabled depending on other elements of the UI, there is another possibility.

You can make the line edit as read only, and use the placeholderText property to show that hint; disabling the focus as explained above will prevent any editing until the line edit becomes "enabled".

A basic implementation could just override the setReadOnly() and do everything required in there, after calling the base implementation:

class FakeDisabledLineEdit(QLineEdit):
    def setReadOnly(self, readOnly):
        super().setReadOnly(readOnly)
        if readOnly:
            self.setCursor(Qt.ForbiddenCursor)
            self.setFocusPolicy(Qt.NoFocus)
            self.clear()
            self.setPlaceholderText('Click the checkbox to enable')
        else:
            self.setCursor(Qt.IBeamCursor)
            self.setFocusPolicy(Qt.StrongFocus)
            self.setPlaceholderText('')

That could become problematic whenever you do have a placeholder text for that field, and even if you want to restore the text.

That would require a more comprehensive approach, but it's still feasible. Here is an example that shows the required behavior while allowing both the "normal" placeholder text and restoring the previously set text:

class FakeDisabledLineEdit(QLineEdit):
    _text = ''
    _readOnlyText = ''
    _placeholderText = ''
    def __init__(self, *args, **kwargs):
        if 'readOnlyText' in kwargs:
            self._readOnlyText = kwargs.pop('readOnlyText')
        super().__init__(*args, **kwargs)
        self._text = self.text()
        self._placeholderText = self.placeholderText()
        if self.isReadOnly() and self._readOnlyText:
            self.setReadOnly(True)

    @Property(str)
    def readOnlyText(self):
        return self._readOnlyText

    @readOnlyText.setter
    def readOnlyText(self, text):
        if self._readOnlyText != text:
            self._readOnlyText = text
            if self.isReadOnly():
                super().setPlaceholderText(text or self._placeholderText)

    def setReadOnlyText(self, text):
        self.readOnlyText = text

    def setPlaceholderText(self, text):
        self._placeholderText = text
        if self.isReadOnly():
            text = self._readOnlyText
        super().setPlaceholderText(text)

    def setReadOnly(self, readOnly):
        super().setReadOnly(readOnly)
        if readOnly:
            with QSignalBlocker(self):
                self.setCursor(Qt.ForbiddenCursor)
                self.setFocusPolicy(Qt.NoFocus)
                self._text = self.text()
                self.clear()
            placeholderText = self._readOnlyText
        else:
            self.setCursor(Qt.IBeamCursor)
            self.setFocusPolicy(Qt.StrongFocus)
            self.setText(self._text)
            placeholderText = self._placeholderText
        super().setPlaceholderText(placeholderText)

    def setText(self, text):
        if self._text == text:
            return
        self._text = text
        if self.isReadOnly():
            with QSignalBlocker(self): # avoid textChanged signal emit
                super().setText(self._readOnlyText)
        else:
            super().setText(text)

And here is a test that shows how the above works:

app = QApplication([])
win = QWidget()
placeholderEdit = QLineEdit('Placeholder text if editable', 
    placeholderText='Set placeholder')
readOnlyEdit = QLineEdit('Click the checkbox to enable', 
    placeholderText='Read only text')
check = QCheckBox('Enable field')
field = FakeDisabledLineEdit(
    readOnly=True, placeholderText=placeholderEdit.text(), 
    readOnlyText=readOnlyEdit.text())

layout = QVBoxLayout(win)
layout.addWidget(placeholderEdit)
layout.addWidget(readOnlyEdit)
layout.addWidget(check)
layout.addWidget(QFrame(frameShape=QFrame.HLine))
layout.addWidget(QLabel('Custom field:'))
layout.addWidget(field)

placeholderEdit.textChanged.connect(field.setPlaceholderText)
readOnlyEdit.textChanged.connect(field.setReadOnlyText)
check.toggled.connect(lambda checked: field.setReadOnly(not checked))

win.show()

app.exec()
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Thanks for the clarification. I will definitely give it a try. Since this all feels so hacky, is there a best practice to show the user that there is a way to specify something (Currently the "deactivated" QLineEdit) but this is currently not possible because a checkbox has to be checked first? – sebwr Apr 16 '23 at 08:54
  • There is no such "best practice": it depends on how the UI is designed, and considering UX aspects. Normally, if a field requires user interaction on another control (such as a checkbox) in order to get it enabled, that control should be near that field, but there are cases for which that might not be possible (for example, the control affects other and more important elements in different ways). I'll update the answer with a possible alternative, but, for future reference, I recommend you to clarify these aspects and requirements in the question. – musicamante Apr 17 '23 at 00:19
  • Thanks, I updated the question, to clarify my requirements – sebwr Apr 17 '23 at 07:07