PDA

View Full Version : Trouble when re-implementing undo framework for QLineEdits



tristam
14th March 2014, 14:24
I have created a custom line edit widget so that I can incorporate undo/redo commands on it into my application's general undo stack (rather than use the built-in undo/redo facilities that come with QLineEdit widgets). The undo/redo logic is fairly straightforward: when the line edit widget receives focus, its content is immediately assigned to an instance variable (self.init_text); and when the line edit widget loses focus, if the text content differs from that stored in self.init_text, then a new QUndoCommand object is created. The undo() method will re-set the content to whatever is in self.init_text, while the redo() method will re-set the content to whatever was captured when the line edit widget lost focus. (In either method, the line edit will again receive focus so that it will be obvious to the user what the undo or redo command actually affected.)

It seems to work just fine with one exception: if the user very quickly cycles through undo or redo commands through the QPushButtons, then the framework just breaks. I can't describe it much better than that because I'm not sure what's going on under the Qt hood, but, for example, the count() of the QUndoStack may be changed completely. The app continues to run with no errors reported on the terminal, but it is nonetheless a broken undo stack.

I have created a little QDialog app so you can try to re-create the issue. (Using Python 2.7.3/PySide 1.2.1 ... if you have a recent PyQt binding installed, I don't think you should need to replace anything except the first two import statements.) For example, in the first tab's QLineEdit, if you type 'hello', then tab out, then click back in and type 'world', then tab out again, try very swiftly clicking the undo button (down to and beyond the bottom of the undo stack) and redo button (up to and beyond the top of the undo stack). For me, that was enough to break it.



#!/usr/bin/python
#coding=utf-8
from PySide.QtCore import *
from PySide.QtGui import *
import sys

class CustomRightClick(QObject):

customRightClicked = Signal()

def __init__(self, parent=None):
QObject.__init__(self, parent)

def eventFilter(self, obj, event):
if event.type() == QEvent.ContextMenu:
# emit signal so that your widgets can connect a slot to that signal
self.customRightClicked.emit()
return True
else:
# standard event processing
return QObject.eventFilter(self, obj, event)

class CommandLineEdit(QUndoCommand):

def __init__(self, line_edit, init_text, tab_widget, tab_index, description):
QUndoCommand.__init__(self, description)
self._line_edit = line_edit
self._current_text = line_edit.text()
self._init_text = init_text
self._tab_widget = tab_widget
self._tab_index = tab_index

def undo(self):
self._line_edit.setText(self._init_text)
self._tab_widget.setCurrentIndex(self._tab_index)
self._line_edit.setFocus(Qt.OtherFocusReason)

def redo(self):
self._line_edit.setText(self._current_text)
self._tab_widget.setCurrentIndex(self._tab_index)
self._line_edit.setFocus(Qt.OtherFocusReason)

class CustomLineEdit(QLineEdit):

def __init__(self, parent, tab_widget, tab_index):
super(CustomLineEdit, self).__init__(parent)
self.parent = parent
self.tab_widget = tab_widget
self.tab_index = tab_index
self.init_text = self.text()
self.setContextMenuPolicy(Qt.CustomContextMenu)

undoAction=QAction("Undo", self)
undoAction.triggered.connect(self.parent.undo_stac k.undo)

self.customContextMenu = QMenu()
self.customContextMenu.addAction(undoAction)

custom_clicker = CustomRightClick(self)
self.installEventFilter(custom_clicker)
self.right_clicked = False
custom_clicker.customRightClicked.connect(self.men uShow)

def menuShow(self):
self.right_clicked = True # set self.right_clicked to True so that the focusOutEvent won't push anything to the undo stack as a consequence of right-clicking
self.customContextMenu.popup(QCursor.pos())
self.right_clicked = False

# re-implement focusInEvent() so that it captures as an instance variable the current value of the text *at the time of the focusInEvent(). This will be utilized for the undo stack command push below
def focusInEvent(self, event):
self.init_text = self.text()
QLineEdit.focusInEvent(self, event)

# re-implement focusOutEvent() so that it pushes the current text to the undo stack.... but only if there was a change!
def focusOutEvent(self, event):
if self.text() != self.init_text and not self.right_clicked:
print "Focus out event. (self.text is %s and init_text is %s). Pushing onto undo stack. (Event reason is %s)" % (self.text(), self.init_text, event.reason())
command = CommandLineEdit(self, self.init_text, self.tab_widget, self.tab_index, "editing a text box")
self.parent.undo_stack.push(command)
QLineEdit.focusOutEvent(self, event)

def keyPressEvent(self, event):
if event.key() == Qt.Key_Z:
if event.modifiers() & Qt.ControlModifier:
self.parent.undo_stack.undo()
else:
QLineEdit.keyPressEvent(self, event)
elif event.key() == Qt.Key_Y:
if event.modifiers() & Qt.ControlModifier:
self.parent.undo_stack.redo()
else:
QLineEdit.keyPressEvent(self, event)
else:
QLineEdit.keyPressEvent(self, event)

class Form(QDialog):

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

self.undo_stack = QUndoStack()

self.tab_widget = QTabWidget()

self.line_edit1 = CustomLineEdit(self, self.tab_widget, 0)
self.line_edit2 = CustomLineEdit(self, self.tab_widget, 1)
self.undo_counter = QLineEdit()

tab1widget = QWidget()
tab1layout = QHBoxLayout()
tab1layout.addWidget(self.line_edit1)
tab1widget.setLayout(tab1layout)

tab2widget = QWidget()
tab2layout = QHBoxLayout()
tab2layout.addWidget(self.line_edit2)
tab2widget.setLayout(tab2layout)

self.tab_widget.addTab(tab1widget, "Tab 1")
self.tab_widget.addTab(tab2widget, "Tab 2")

self.undo_button = QPushButton("Undo")
self.redo_button = QPushButton("Redo")
layout = QGridLayout()
layout.addWidget(self.tab_widget, 0, 0, 1, 2)
layout.addWidget(self.undo_button, 1, 0)
layout.addWidget(self.redo_button, 1, 1)
layout.addWidget(QLabel("Undo Stack Counter"), 2, 0)
layout.addWidget(self.undo_counter)
self.setLayout(layout)

self.undo_button.clicked.connect(self.undo_stack.u ndo)
self.redo_button.clicked.connect(self.undo_stack.r edo)
self.undo_stack.indexChanged.connect(self.changeUn doCount)

def changeUndoCount(self, index):
self.undo_counter.setText("%s / %s" % (index, self.undo_stack.count()))

app = QApplication(sys.argv)
form = Form()
form.show()
app.exec_()


Is this a Qt bug? A PySide bug? Or is there a problem in my re-implementation? Any help is appreciated!

wysota
20th March 2014, 17:21
I'd suggest modifying your implementation. Instead of reacting on focus events, react on textChanged() or textEdited() signals, it should be much easier than tracking focus events.