How to deselect all items in QTreeWidget? - qt

I have tried every suggestion I can come across online, from setting flags to using selectionModel
def DatabaseLoadWrapper(self,database, init):
self.treeWidget.currentItemChanged.disconnect(self.updateStackedWidget)
self.DatabaseLoad(database, init)
self.treeWidget.clearSelection()
self.treeWidget.setCurrentItem(self.treeWidget.findItems(self.selectedDatabase,Qt.MatchExactly|Qt.MatchRecursive)[0])
self.treeWidget.currentItemChanged.connect(self.updateStackedWidget)
This is where my code needs to force a selection on the QTreeWidget, none of the code I use throws up any errors but also has no effect on the selection. And I end up with this where the user has selected Database 1 but I need to revert back to having only Database 2 selected:
Edit: The Tree Widget is built using this code:
def setupMenu(self):
self.DatabaseParent = QTreeWidgetItem(['Databases'])
for item in NamesInDatabase():
self.DatabaseParent.addChild(QTreeWidgetItem([item]))
self.AverageParent = QTreeWidgetItem(['Averaged Database'])
self.SortingParent = QTreeWidgetItem(['Waste Composition'])
self.ResultParent = QTreeWidgetItem(['Results'])
self.treeWidget.addTopLevelItem(self.DatabaseParent)
self.treeWidget.addTopLevelItem(self.AverageParent)
self.treeWidget.addTopLevelItem(self.SortingParent)
self.treeWidget.addTopLevelItem(self.ResultParent)
It basically is adding databases, averaged database, waste compisition & results, as fixed parts of the navigation menu and then populating children of databases with the names of the databases in the save file.

Your question fails to expose the part of the code that is causing the problem. By default, setting the current item, as you do, also sets the selection. So this code, for example, correctly sets the selection to item "b":
from PySide import QtCore,QtGui
if __name__ == '__main__':
import sys
qApp = QtGui.QApplication(sys.argv)
treeWidget = QtGui.QTreeWidget()
parent = QtGui.QTreeWidgetItem(['Databases'])
items = []
for item_text in ["a","b","c"]:
item = QtGui.QTreeWidgetItem([item_text])
items.append(item)
parent.addChild(item)
treeWidget.addTopLevelItem(parent)
treeWidget.setCurrentItem(items[1])
treeWidget.show()
sys.exit(qApp.exec_())
However, I suspect there is code elsewhere in your project that is affecting this. For example, if you had set the selection mode for the QTableWidget selection model to MultiSelection then selections become cumulative:
from PySide import QtCore,QtGui
if __name__ == '__main__':
import sys
qApp = QtGui.QApplication(sys.argv)
treeWidget = QtGui.QTreeWidget()
parent = QtGui.QTreeWidgetItem(['Databases'])
items = []
for item_text in ["a","b","c"]:
item = QtGui.QTreeWidgetItem([item_text])
items.append(item)
parent.addChild(item)
treeWidget.addTopLevelItem(parent)
treeWidget.setSelectionMode(QtGui.QAbstractItemView.MultiSelection)
treeWidget.setCurrentItem(items[0])
treeWidget.setCurrentItem(items[2])
treeWidget.show()
sys.exit(qApp.exec_())
However, that still doesn't explain your issue because the clearSelection call should have cleared the preceding selection in any case. Further debugging of your code is needed, for example to check that the wrapper function and the setCurrentItem are being called as you claim. Also check what else is being called subsequent to the DatabaseLoadWrapper.

In Pyside2, This works for me:
If you click on treewidget the selection will be clear.
self.treeWidget.setSelectionMode(QtWidgets.QAbstractItemView.ContiguousSelection)

Related

PyQt mandatory answer on QPlainTextEdit [duplicate]

I have a login screen dialog written using pyqt and python and it shows a dialog pup up when it runs and you can type in a certin username and password to unlock it basicly. It's just something simple I made in learning pyqt. I'm trying to take and use it somewhere else but need to know if there is a way to prevent someone from using the x button and closing it i would like to also have it stay on top of all windows so it cant be moved out of the way? Is this possible? I did some research and couldn't find anything that could help me.
Edit:
as requested here is the code:
from PyQt4 import QtGui
class Test(QtGui.QDialog):
def __init__(self):
QtGui.QDialog.__init__(self)
self.textUsername = QtGui.QLineEdit(self)
self.textPassword = QtGui.QLineEdit(self)
self.loginbuton = QtGui.QPushButton('Test Login', self)
self.loginbuton.clicked.connect(self.Login)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.textUsername)
layout.addWidget(self.textPassword)
layout.addWidget(self.loginbuton)
def Login(self):
if (self.textUsername.text() == 'Test' and
self.textPassword.text() == 'Password'):
self.accept()
else:
QtGui.QMessageBox.warning(
self, 'Wrong', 'Incorrect user or password')
class Window(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
if Test().exec_() == QtGui.QDialog.Accepted:
window = Window()
window.show()
sys.exit(app.exec_())
Bad news first, it is not possible to remove the close button from the window, based on the Riverbank mailing system
You can't remove/disable close button because its handled by the
window manager, Qt can't do anything there.
Good news, you can override and ignore, so that when the user sends the event, you can ignore or put a message or something.
Read this article for ignoring the QCloseEvent
Also, take a look at this question, How do I catch a pyqt closeEvent and minimize the dialog instead of exiting?
Which uses this:
class MyDialog(QtGui.QDialog):
# ...
def __init__(self, parent=None):
super(MyDialog, self).__init__(parent)
# when you want to destroy the dialog set this to True
self._want_to_close = False
def closeEvent(self, evnt):
if self._want_to_close:
super(MyDialog, self).closeEvent(evnt)
else:
evnt.ignore()
self.setWindowState(QtCore.Qt.WindowMinimized)
You can disable the window buttons in PyQt5.
The key is to combine it with "CustomizeWindowHint",
and exclude the ones you want to be disabled.
Example:
#exclude "QtCore.Qt.WindowCloseButtonHint" or any other window button
self.setWindowFlags(
QtCore.Qt.Window |
QtCore.Qt.CustomizeWindowHint |
QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowMinimizeButtonHint
)
Result with QDialog:
Reference: https://doc.qt.io/qt-5/qt.html#WindowType-enum
Tip: if you want to change flags of the current window, use window.show()
after window.setWindowFlags,
because it needs to refresh it, so it calls window.hide().
Tested with QtWidgets.QDialog on:
Windows 10 x32,
Python 3.7.9,
PyQt5 5.15.1
.
I don't know if you want to do this but you can also make your window frameless. To make window frameless you can set the window flag equal to QtCore.Qt.FramelessWindowHint

QTreeWidget: Loading large numbers of items?

I'm using a QTreeWidget to display a large number of items (about 50_000) using addTopLevelItem/addTopLevelItems, insertTopLevelItem/insertTopLevelItem and by setting a parent and preceding item to the QTreeWidgetItem initializer (usually I need a combination of these adding methods).
This works fine but freezes the GUI for a bit. Is there a way to either do this faster or in the background, so that the GUI doesn't freeze? Speed is not paramount, not freezing the GUI is the top priority.
Contrived example of the way I currently add items (Python/PyQt5):
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class Window(QWidget):
def __init__(self):
super().__init__()
layout = QVBoxLayout(self)
self.setLayout(layout)
# Tree widget initialization
tree_widget = QTreeWidget()
self.layout().addWidget(tree_widget)
# Main Node 0 initialization
main_node_0 = QTreeWidgetItem()
main_node_0.setText(0, "Main Node 0")
tree_widget.addTopLevelItem(main_node_0)
# Add children to Main Node 0
for i in range(50_000):
child_node = QTreeWidgetItem(main_node_0)
child_node.setText(0, f"Child Node {i}")
# Main Node 1 initialization
main_node_1 = QTreeWidgetItem()
main_node_1.setText(0, "Main Node 1")
tree_widget.addTopLevelItem(main_node_1)
# Add children to Main Node 0
for i in range(50_000):
child_node = QTreeWidgetItem(main_node_1)
child_node.setText(0, f"Child Node {i}")
# ... More code similar to the above
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec())
After a lot of code restructuring and testing I can confirm that both #musicamante's suggestion:
create the top level items without parents, then add all the children, finally call addTopLevelItems()
and #HiFile.app - best file manager's suggestion:
You can create a QList of top level items and add children to each of the top level items using QTreeWidgetItem::addChild() or QTreeWidgetItem::addChildren(). And once you have the whole structure ready, you just call QTreeWidget::addTopLevelItems(). I.e. you insert all the items to the model in just one call.
are the best solution! It might take some effort to restructure the code, but the result is great: no delays in rendering large numbers of items with.
Thank you both for the answer!

How to solve a tkinter memory leak?

I have a dynamic table with a fixed row number (like a FIFO Queue), which updates continuously through tkinter's after() function. Inside the table is a Button, which text is editable.
To make the Button's text editable I used the solution of BrenBarn and reference a loop variable into a function call at the command-attribute.
When the function update_content_items() is cycled, I found, that the memory usage is increasing MB by MB per second. I can confirm that after commenting out the lambda expression, the memory leak was gone. (as seen live running 'top' in the terminal)
It seems I have to use the lambda, otherwise the Button will have a wrong index and the user edits the wrong row, when I just used self.list_items[i], though the user clicked the right one.
Is there a way to solve the problem? How can the user click the right button and edit it while having the right index and getting rid of the leak?
The corresponding code:
def update_content_items(self):
"""
Continuously fills and updates the Table with rows and content.
The size of the table rows is initially fixed by an external value at config.ini
:return: nothing
"""
if len(self.list_items) > self.queueMaxlen:
self.queueMaxlen = len(self.list_items)
self.build_table()
try:
for i in range(len(self.list_items)):
item = self.list_items[i]
self.barcodeImgList[i].image = item.plateimage
orig_image = Image.open(io.BytesIO(item.plateimage))
ein_image = ImageTk.PhotoImage(orig_image)
self.barcodeImgList[i].configure(image=ein_image)
# keeps a reference, because somehow tkinter forgets it...??? Bug of my implementation???
self.barcodeImgList[i].image = ein_image
orig_image = None
ein_image = None
#FIXME Memory LEAK?
self.numberList[i].configure(text=item.number,
command=lambda K=i: self.edit_barcode(self.list_items[K]))
self.timestampList[i].configure(text=item.timestamp)
self.search_hitlist[i].config(bg='white', cursor="xterm")
self.search_hitlist[i].unbind("<Button-1>")
if item.queryresult is not None:
if item.queryresult.gesamtstatus != 'Gruen':
self.search_hitlist[i].insert(tk.END, item.queryresult.barcode +
'\n' + item.queryresult.permitlevel)
self.search_hitlist[i].configure(bg='red', cursor="hand2")
self.search_hitlist[i].bind("<Button-1>", item.url_callback)
else:
self.search_hitlist[i].configure(bg='green', cursor="xterm")
self.search_hitlist[i].configure(state=tk.DISABLED)
self.on_frame_configure(None)
self.canvas.after(10, self.update_content_items)
except IndexError as ie:
for number, thing in enumerate(self.list_items):
print(number, thing)
raise ie
def edit_barcode(self, item=None):
"""
Opens the number plate edit dialogue and updates the corresponding list item.
:param item: as Hit DAO
:return: nothing
"""
if item is not None:
new_item_number = EditBarcodeEntry(self.master.master, item)
if new_item_number.mynumber != 0:
item.number = new_item_number.mynumber
self.list_items.request_work(item, 'update')
self.list_items.edit_hititem_by_id(item)
self.parent.master.queryQueue.put(item)
else:
print("You shouldn't get here at all. Please see edit_barcode function.")
EDIT: It seems there is indeed a deeper memory leak (python itself). The images won't get garbage collected. Memory is slowly leaking in Python 3.x and I do use PIL. Also here: Image loading by file name memory leak is not properly fixed
What can I do, because I have to cycle through a list with records and update Labels with images? Is there a workaround? PhotoImage has no explicit close() function, and if I call del, the reference is gc'ed and no configuring of the Label possible.
an example of my proposed changes, with indentation fixed:
def update_content_items(self):
"""
Continuously fills and updates the Table with rows and content.
The size of the table rows is initially fixed by an external value at config.ini
:return: nothing
"""
if len(self.list_items) > self.queueMaxlen:
self.queueMaxlen = len(self.list_items)
self.build_table()
try:
for i in range(len(self.list_items)):
item = self.list_items[i]
self.barcodeImgList[i].image = item.plateimage
orig_image = Image.open(io.BytesIO(item.plateimage))
ein_image = ImageTk.PhotoImage(orig_image)
self.barcodeImgList[i].configure(image=ein_image)
# keeps a reference, because somehow tkinter forgets it...??? Bug of my implementation???
self.barcodeImgList[i].image = ein_image
orig_image = None
ein_image = None
self.numberList[i].configure(text=item.number) # removed lambda
self.numberList[i].bind("<Button-1>", self.edit_barcode_binding) # added binding
self.timestampList[i].configure(text=item.timestamp)
self.search_hitlist[i].config(bg='white', cursor="xterm")
self.search_hitlist[i].unbind("<Button-1>")
if item.queryresult is not None:
if item.queryresult.gesamtstatus != 'Gruen':
self.search_hitlist[i].insert(tk.END, item.queryresult.barcode +
'\n' + item.queryresult.permitlevel)
self.search_hitlist[i].configure(bg='red', cursor="hand2")
self.search_hitlist[i].bind("<Button-1>", item.url_callback)
else:
self.search_hitlist[i].configure(bg='green', cursor="xterm")
self.search_hitlist[i].configure(state=tk.DISABLED)
self.on_frame_configure(None)
self.canvas.after(10, self.update_content_items)
except IndexError as ie:
for number, thing in enumerate(self.list_items):
print(number, thing)
raise ie
def edit_barcode_binding(self, event): # new wrapper for binding
K = self.numberList.index(event.widget) # get index from list
self.edit_barcode(self.list_items[K]) # call the original function
def edit_barcode(self, item=None):
"""
Opens the number plate edit dialogue and updates the corresponding list item.
:param item: as Hit DAO
:return: nothing
"""
if item is not None:
new_item_number = EditBarcodeEntry(self.master.master, item)
if new_item_number.mynumber != 0:
item.number = new_item_number.mynumber
self.list_items.request_work(item, 'update')
self.list_items.edit_hititem_by_id(item)
self.parent.master.queryQueue.put(item)
else:
print("You shouldn't get here at all. Please see edit_barcode function.")

When dragging multiple items from QListWidget, non-draggable items get removed

I have two QListWidgets. The user can select multiple items from one list and drag them to the other list. But within each list, some items are draggable and some are not. If the selection contains both draggable and non-draggable items, a problem happens. Only the draggable items appear in the second list, which is correct. But all the items disappear from the first list.
In the animated image above, items 00, 01, and 02 are selected. Only items 00 and 02 are drag enabled. After the drag-and-drop, all three items are gone from the first list. How can I fix this?
Here is some code to reproduce the problem:
import random
import sys
from PySide import QtCore, QtGui
class TestMultiDragDrop(QtGui.QMainWindow):
def __init__(self, parent=None):
super(TestMultiDragDrop, self).__init__(parent)
centralWidget = QtGui.QWidget()
self.setCentralWidget(centralWidget)
layout = QtGui.QHBoxLayout(centralWidget)
self.list1 = QtGui.QListWidget()
self.list1.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
self.list1.setDefaultDropAction(QtCore.Qt.MoveAction)
self.list1.setSelectionMode(QtGui.QListWidget.ExtendedSelection)
self.list2 = QtGui.QListWidget()
self.list2.setDragDropMode(QtGui.QAbstractItemView.DragDrop)
self.list2.setDefaultDropAction(QtCore.Qt.MoveAction)
self.list2.setSelectionMode(QtGui.QListWidget.ExtendedSelection)
layout.addWidget(self.list1)
layout.addWidget(self.list2)
self.fillListWidget(self.list1, 8, 'someItem')
self.fillListWidget(self.list2, 4, 'anotherItem')
def fillListWidget(self, listWidget, numItems, txt):
for i in range(numItems):
item = QtGui.QListWidgetItem()
newTxt = '{0}{1:02d}'.format(txt, i)
if random.randint(0, 1):
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled)
else:
# If the item is draggable, indicate it with a *
newTxt += ' *'
item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsDragEnabled)
item.setText(newTxt)
listWidget.addItem(item)
def openMultiDragDrop():
global multiDragDropUI
try:
multiDragDropUI.close()
except:
pass
multiDragDropUI = TestMultiDragDrop()
multiDragDropUI.setAttribute(QtCore.Qt.WA_DeleteOnClose)
multiDragDropUI.show()
return multiDragDropUI
if __name__ == '__main__':
app = QtGui.QApplication([])
openMultiDragDrop()
sys.exit(app.exec_())
Here I have some suspicion on setDefaultDropAction(QtCore.Qt.MoveAction)
Read below para from documentation: Specially the bold line
In the simplest case, the target of a drag and drop action receives a copy of the data being dragged, and the source decides whether to delete the original. This is described by the CopyAction action. The target may also choose to handle other actions, specifically the MoveAction and LinkAction actions. If the source calls QDrag::exec(), and it returns MoveAction, the source is responsible for deleting any original data if it chooses to do so. The QMimeData and QDrag objects created by the source widget should not be deleted - they will be destroyed by Qt.
(http://doc.qt.io/qt-4.8/dnd.html#overriding-proposed-actions)
First give a try with QtCore.Qt.CopyAction
Second, if MoveAction is mandatory, try creating QMimeData and QDrag objects in your source list widget's mouseMoveEvent.
Here in below link, you can find some help for creating QMimeData and QDrag objects in your source list widget's mouseMoveEvent. (code is in C++, My intention is to get conceptual idea).
http://doc.qt.io/qt-4.8/dnd.html#overriding-proposed-actions
I think Kuba Ober is right that this is a Qt bug. In the C++ source code, there is a function void QAbstractItemViewPrivate::clearOrRemove(). It deletes all selected rows, but it does not look at whether each item is drag-enabled or not.
That being the case, I came up with a few workarounds:
Method 1: Make all non-draggable items non-selectable as well
This is the easiest method. Just remove the QtCore.Qt.ItemIsEnabled flag from all non-draggable items. Of course if you want all of your items to be selectable, this won't work.
Method 2: Recreate the "startDrag" function
Since the clearOrRemove function belongs to a private class, I cannot override it. But that function is called by the startDrag function, which can be overridden. So I essentially duplicated the function in Python and replaced the call to clearOrRemove with my own function removeSelectedDraggableItems.
The problem with this method is that startDrag contains calls to a few other functions belonging to a private class. And those functions call other private class functions. Specifically, these functions are responsible for controlling how the items are drawn during the drag event. Since I didn't want to recreate all the functions, I just ignored those. The result is that this method results in the correct functionality, but it loses the graphical indication of which items are being dragged.
class DragListWidget(QtGui.QListWidget):
def __init__(self):
super(DragListWidget, self).__init__()
def startDrag(self, supportedDragActions):
indexes = self.getSelectedDraggableIndexes()
if not indexes:
return
mimeData = self.model().mimeData(indexes)
if not mimeData:
return
drag = QtGui.QDrag(self)
rect = QtCore.QRect()
# "renderToPixmap" is from a private class in the C++ code, so I can't use it.
#pixmap = renderToPixmap(indexes, rect)
#drag.setPixmap(pixmap)
drag.setMimeData(mimeData)
# "pressedPosition" is from a private class in the C++ code, so I can't use it.
#drag.setHotSpot(pressedPostion() - rect.topLeft())
defaultDropAction = self.defaultDropAction()
dropAction = QtCore.Qt.IgnoreAction
if ((defaultDropAction != QtCore.Qt.IgnoreAction) and
(supportedDragActions & defaultDropAction)):
dropAction = defaultDropAction
elif ((supportedDragActions & QtCore.Qt.CopyAction) and
(self.dragDropMode() != self.InternalMove)):
dropAction = QtCore.Qt.CopyAction
dragResult = drag.exec_(supportedDragActions, dropAction)
if dragResult == QtCore.Qt.MoveAction:
self.removeSelectedDraggableItems()
def getSelectedDraggableIndexes(self):
""" Get a list of indexes for selected items that are drag-enabled. """
indexes = []
for index in self.selectedIndexes():
item = self.itemFromIndex(index)
if item.flags() & QtCore.Qt.ItemIsDragEnabled:
indexes.append(index)
return indexes
def removeSelectedDraggableItems(self):
selectedDraggableIndexes = self.getSelectedDraggableIndexes()
# Use persistent indices so we don't lose track of the correct rows as
# we are deleting things.
root = self.rootIndex()
model = self.model()
persistentIndices = [QtCore.QPersistentModelIndex(i) for i in selectedDraggableIndexes]
for pIndex in persistentIndices:
model.removeRows(pIndex.row(), 1, root)
Method 3: Hack "startDrag"
This method changes the drop action from "MoveAction" to "CopyAction" before calling the built-in "startDrag" method. Then it calls a custom function for deleting the selected drag-enabled items. This solves the problem of losing the graphical dragging animation.
This is a pretty easy hack, but it comes with its own problem. Say the user installs an event filter that changes the drop action from "MoveAction" to "IgnoreAction" in certain cases. This hack code doesn't get the updated value. It will still delete the items as though the action is "MoveAction". (Method 2 does not have this problem.) There are workarounds for this problem, but I won't go into them here.
class DragListWidget2(QtGui.QListWidget):
def startDrag(self, supportedDragActions):
dropAction = self.defaultDropAction()
if dropAction == QtCore.Qt.MoveAction:
self.setDefaultDropAction(QtCore.Qt.CopyAction)
super(DragListWidget2, self).startDrag(supportedDragActions)
if dropAction == QtCore.Qt.MoveAction:
self.setDefaultDropAction(dropAction)
self.removeSelectedDraggableItems()
def removeSelectedDraggableItems(self):
# Same code from Method 2. Removed here for brevity.
pass

QTextEdit shift-tab wrong behaviour

shift+tab behaves as tab in QTextEdit/QPlainTextEdit.
Looks like a common problem with no good solution.
Is there any "classical" way to enable this functionality when tab increases indentation level and shift-tab decreases it?
This is a bit of an old question, but I got this figured out.
You just need to reimplement QPlainTextEdit (or QTextEdit) with your own class that inherits from it, and override the keyPressEvent.
By default a tab inserts a tabstop, but the below code catches a Qt.Key_Backtab event, which as near as I can tell is the event that occurs when you press Shift+Tab.
I tried and failed to catch Qt.Key_Tab and a Qt.Key_Shift or Qt.Key_Tab and a Shift modifier, so this must be the way to do it.
import sys
from PyQt4 import QtCore, QtGui
class TabPlainTextEdit(QtGui.QTextEdit):
def __init__(self,parent):
QtGui.QTextEdit.__init__(self, parent)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Backtab:
cur = self.textCursor()
# Copy the current selection
pos = cur.position() # Where a selection ends
anchor = cur.anchor() # Where a selection starts (can be the same as above)
# Can put QtGui.QTextCursor.MoveAnchor as the 2nd arg, but this is the default
cur.setPosition(pos)
# Move the position back one, selection the character prior to the original position
cur.setPosition(pos-1,QtGui.QTextCursor.KeepAnchor)
if str(cur.selectedText()) == "\t":
# The prior character is a tab, so delete the selection
cur.removeSelectedText()
# Reposition the cursor with the one character offset
cur.setPosition(anchor-1)
cur.setPosition(pos-1,QtGui.QTextCursor.KeepAnchor)
else:
# Try all of the above, looking before the anchor (This helps if the achor is before a tab)
cur.setPosition(anchor)
cur.setPosition(anchor-1,QtGui.QTextCursor.KeepAnchor)
if str(cur.selectedText()) == "\t":
cur.removeSelectedText()
cur.setPosition(anchor-1)
cur.setPosition(pos-1,QtGui.QTextCursor.KeepAnchor)
else:
# Its not a tab, so reset the selection to what it was
cur.setPosition(anchor)
cur.setPosition(pos,QtGui.QTextCursor.KeepAnchor)
else:
return QtGui.QTextEdit.keyPressEvent(self, event)
def main():
app = QtGui.QApplication(sys.argv)
w = TabPlainTextEdit(None)
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
I'm still refining this, but the rest of the code is on GitHub.

Resources