I am using Qt4.5.2 on Linux. I have a simple QTableWidget, in which a column displays dates in a human-friendly format. Unfortunately "human-friendly dates" are not easy to sort correctly. So, in the QTableWidget I keep a hidden column with the UNIX timestamp corresponding to that date.
I am trying to make sure that, whenever a request to sort on the DATE column is issued, in reality the sort be done on the (invisible) TIMESTAMP column. I tried reimplementing sortByColumn (this is in Python) by subclassing from QTableWidget and defining:
def sortByColumn(self, col, order):
print 'got request to sort col %d in order %s' % (col, str(order) )
Yet, whenever I click on one of the headers of my table the normal sort method continues to be called.
How can I override it?
You could derive your own class of QTableWidgetItem and then write your own __lt__ operator. This would alleviate the need for an extra column as well. Something along the lines of:
from PyQt4 import QtCore, QtGui
import sys
import datetime
class MyTableWidgetItem(QtGui.QTableWidgetItem):
def __init__(self, text, sortKey):
#call custom constructor with UserType item type
QtGui.QTableWidgetItem.__init__(self, text, QtGui.QTableWidgetItem.UserType)
self.sortKey = sortKey
#Qt uses a simple < check for sorting items, override this to use the sortKey
def __lt__(self, other):
return self.sortKey < other.sortKey
app = QtGui.QApplication(sys.argv)
window = QtGui.QMainWindow()
window.setGeometry(0, 0, 400, 400)
table = QtGui.QTableWidget(window)
table.setGeometry(0, 0, 400, 400)
table.setRowCount(3)
table.setColumnCount(1)
date1 = datetime.date.today()
date2 = datetime.date.today() + datetime.timedelta(days=1)
date3 = datetime.date.today() + datetime.timedelta(days=2)
item1 = MyTableWidgetItem(str(date1.strftime("%A %d. %B %Y")), str(date1))
item2 = MyTableWidgetItem(str(date2.strftime("%A %d. %B %Y")), str(date2))
item3 = MyTableWidgetItem(str(date3.strftime("%A %d. %B %Y")), str(date3))
table.setItem(0, 0, item1)
table.setItem(2, 0, item2)
table.setItem(1, 0, item3)
table.setSortingEnabled(True)
window.show()
sys.exit(app.exec_())
This produced correct results for me, you can run it yourself to verify. The cell text displays text like "Saturday 20. February 2010" but when you sort on the column it will correctly sort by the sortKey field which is "2010-02-20" (iso formatted).
Oh, it should also be noted that this will NOT work with PySide because it appears that the __lt__ operator is not bound, where-as it is with PyQt4. I spent a while trying to debug why it wasn't working and then I switched from PySide to PyQt4 and it worked fine. You may notice that the __lt__ is not listed here:
http://www.pyside.org/docs/pyside/PySide/QtGui/QTableWidgetItem.html
but it is here:
http://doc.qt.digia.com/4.5/qtablewidgetitem.html#operator-lt
Related
I would like to display the output of datatime.now being refreshed every 1 second on the streamlit webui.
from datetime import datetime
datetime.now()
# print this output every one second
datetime.datetime(2020, 5, 19, 4, 22, 40, 921985)
What I have already tried
#!/usr/bin/env python3
import streamlit as st
from datetime import datetime
timenow = str(datetime.now())
st.write(timenow)
I suppose it depends on whether you need exactly one second resolution or not, but the solution is approximately:
import time
from datetime import datetime
import streamlit as st
t = st.empty()
while True:
t.markdown("%s" % str(datetime.now()))
time.sleep(1)
The while loop keeps the process going forever. By having the st.empty() call outside of the loop, we keep modifying the t variable. On each loop repetition, the value for the markdown string gets overwritten by the datetime.now() argument.
Display Time With Infinite While Loop AT END OF Streamlit Code
The placeholder code in the while loop is likely not needed. Using t.write or t.markdown as #Randy Zwitch does is fine. I just wanted to show that IF you use this while look approach, put it at the end of your streamlit script. Even then, watch how the time is interrupted when the button state is updated in the code above the while loop.
import streamlit as st
from datetime import datetime
import time
# Section 1
button = st.button('Button')
button_placeholder = st.empty()
button_placeholder.write(f'button = {button}')
time.sleep(2)
button = False
button_placeholder.write(f'button = {button}')
# Section 2
time_placeholder = st.empty()
while True:
timenow = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
time_placeholder.write(timenow)
time.sleep(1)
I have encountered a theoretical question. I'm using pyqt5, but this is probably are very generalistic and framework independent question.
I have a QMainwindow sitting around waiting for the user to do stuff. The user can show / hide dialogues (subclasses of QDockwidgets) as he chooses using the QMenu and the associated shortcuts (it's a checkable QAction for each individual dialogue).
I have been struggling with showing / hiding the dialogues efficiently. Currently, I'm just initiating them all at start up, hiding those that I don't want to show up in the beginning. This makes triggering the dialogues easy, since I can just dialogue.show() /dialogue.hide() depending on the dialogues current visibility.
But I cannot believe that this is best practice and very efficient.
I have tried (I currently do not have my pyqt environment set up on this computer, so I had to strip down my actual code without being able to test if this runs):
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class InfoPanel(QDockWidget):
def __init__(self, title='Tool Box'):
QDockWidget.__init__(self, title)
self.setFeatures(QDockWidget.DockWidgetFloatable | QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetClosable)
self.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
frame = QFrame()
layout = QGridLayout()
self.canvas = QGraphicsView()
self.canvas.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(40, 40, 40)))
layout.addWidget(self.canvas)
frame.setLayout(layout)
self.setWidget(frame)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.showpanelAct = QAction("&Show Panel", self, enabled=True,checkable=True, shortcut="F10")
self.showpanelAct.triggered.connect(lambda: self.showPanel(0))
self.viewMenu = QMenu("&View", self)
self.viewMenu.addAction(self.showpanelAct)
self.setDockOptions(QMainWindow.AnimatedDocks)
def showPanel(self,i:int = 0): # this is not so smart - should construct and deconstuct to save memory!?
if i == 0: #infopanel
dialogueExists = True
try: self.infoPanel
#except NameError: #does not catch the error
except:
dialogueExists = False
if dialogueExists:
print('destroy')
self.infoPanel.destroy()
else:
print('create')
self.infoPanel = InfoPanel() #init
self.infoPanel.show()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
Which works the first time, but after that, it only seems to trigger the destruction of the dialogue (which, surprisingly, does not crash anything it just keeps on going).
Why is that and is there a standard way to approach the showing hiding of dialogues?
I took the exposed MCVE of OP and tried to make it running in my cygwin64 on Windows 10.
At first I had to apply little fixes. (OP stated that he was not able to test it at the time of publishing.)
First, I inserted a “hut” at first line for convenient start in bash:
#!/usr/bin/python3
Second, the self.viewMenu didn't appear. Hence, I inserted a line after
self.viewMenu = QMenu("&View", self)
self.viewMenu.addAction(self.showpanelAct)
to add the viewMenu to main menu bar:
self.menuBar().addMenu(self.viewMenu)
which fixed it.
Third, when clicking the menu item I got:
Traceback (most recent call last):
File "./testQDockPanelShowHide.py", line 27, in <lambda>
self.showpanelAct.triggered.connect(lambda: self.showPanel(0))
File "./testQDockPanelShowHide.py", line 45, in showPanel
self.infoPanel = InfoPanel() #init
File "./testQDockPanelShowHide.py", line 17, in __init__
self.canvas.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(40, 40, 40)))
NameError: name 'QtGui' is not defined
Aborted (core dumped)
I must admit that my Python knowledge is very limited. (I'm the guy who writes the Python bindings in C++ for the colleagues. So, my colleagues are the actual experts. At most, I play a little bit in Python when I test whether new implemented bindings do what's expected.) However, I modified
self.canvas.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(40, 40, 40)))
to:
self.canvas.setBackgroundBrush(QBrush(QColor(40, 40, 40)))
which fixed this issue.
After this, I got the behavior described by OP and did a closer look where I (and OP) suspected the error:
def showPanel(self,i:int = 0): # this is not so smart - should construct and deconstuct to save memory!?
if i == 0: #infopanel
dialogueExists = True
try: self.infoPanel
#except NameError: #does not catch the error
except:
dialogueExists = False
if dialogueExists:
print('destroy')
self.infoPanel.destroy()
else:
print('create')
self.infoPanel = InfoPanel() #init
self.infoPanel.show()
I strongly believe that try: self.infoPanel doesn't do what OP thinks it would.
It tries to access self.infoPanel which isn't existing until the first call of this method. (Please, be aware, the member variable self.infoPanel isn't existing.) So, the except: branch is executed and sets dialogueExists = False which a few lines later causes self.infoPanel = InfoPanel() #init. Now, the member variable self.infoPanel is existing, and the try: self.infoPanel will never fail again until destruction of this MainWindow.
Out of curiosity, I had a look at QWidget.destroy() (to be sure not to tell something wrong):
QWidget.destroy (self, bool destroyWindow = True, bool destroySubWindows = True)
Frees up window system resources. Destroys the widget window if destroyWindow is true.
destroy() calls itself recursively for all the child widgets, passing destroySubWindows for the destroyWindow parameter. To have more control over destruction of subwidgets, destroy subwidgets selectively first.
This function is usually called from the QWidget destructor.
It definitely doesn't destroy the member variable self.infoPanel.
After having understood this, a fix was easy and obvious:
def showPanel(self,i:int = 0): # this is not so smart - should construct and deconstuct to save memory!?
if i == 0: #infopanel
try: self.infoPanel
#except NameError: #does not catch the error
except:
print('create')
self.infoPanel = InfoPanel() #init
if self.infoPanel.isVisible():
self.infoPanel.hide()
else:
self.infoPanel.show()
Btw. I replaced destroy() by hide() which makes a re-creation of the InfoPanel() obsolete.
I tested this by toggling the menu item multiple times – it works as expected now (at least, it looks like).
The complete sample finally:
#!/usr/bin/python3
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class InfoPanel(QDockWidget):
def __init__(self, title='Tool Box'):
QDockWidget.__init__(self, title)
self.setFeatures(QDockWidget.DockWidgetFloatable | QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetClosable)
self.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea)
frame = QFrame()
layout = QGridLayout()
self.canvas = QGraphicsView()
# self.canvas.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(40, 40, 40)))
self.canvas.setBackgroundBrush(QBrush(QColor(40, 40, 40)))
layout.addWidget(self.canvas)
frame.setLayout(layout)
self.setWidget(frame)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.showpanelAct = QAction("&Show Panel", self, enabled=True,checkable=True, shortcut="F10")
self.showpanelAct.triggered.connect(lambda: self.showPanel(0))
self.viewMenu = QMenu("&View", self)
self.viewMenu.addAction(self.showpanelAct)
self.menuBar().addMenu(self.viewMenu)
self.setDockOptions(QMainWindow.AnimatedDocks)
def showPanel(self,i:int = 0): # this is not so smart - should construct and deconstuct to save memory!?
if i == 0: #infopanel
try: self.infoPanel
#except NameError: #does not catch the error
except:
print('create')
self.infoPanel = InfoPanel() #init
if self.infoPanel.isVisible():
self.infoPanel.hide()
else:
self.infoPanel.show()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
After taking a break from coding the solution to my problem was very obvious. Going back to my original code (which did not produce the expected output of creating / destroying the dialogue self.infoPanel on demand):
dialogueExists = True
try: self.infoPanel
#except NameError: #does not catch the error
except:
dialogueExists = False
if dialogueExists:
print('destroy')
self.infoPanel.destroy()
else:
print('create')
self.infoPanel = InfoPanel() #init
self.infoPanel.show()
My main problem was that I confused two separate things. Qt destroyed the widget contained in the object self.infoPanel when I called self.infoPanel.destroy(). But that doesn't mean the object self.infoPanel does not exist (that's exactly what I use try: ... for, to see if the object exists). The simple and obvious way to create and destroy dialogues on demand obviously involves deleting the object from the environment (del self.infoPanel).
The working code is:
dialogueExists = True
try:
self.infoPanel.destroy() #not sure this is needed, but I guess it doesn't hurt
del self.infoPanel #this is the deletion of the actual object
except:
dialogueExists = False
if not dialogueExists :
self.infoPanel = InfoPanel()
Cheers and many thanks for the helpful advice on deciding whether to show / hide dialogues or to create / destroy them!
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
Literally the inverse of this question, is there an easy way to get a .Net DateTime from an IronPython datetime?
Clearly, one could
Output a string and parse it or
Dump all the date parts into a DateTime constructor
but those are both messy. This doesn't work either:
pydate = datetime.datetime.now()
csharp = DateTime(pydate) # Crashes, because .Net wants a 'long' for Ticks
Is there an easy cast or a short way to get the Ticks that .Net wants?
I was fairly certain a direct conversion was already allowed, but I was wrong. I added it in 31f5c88 but that won't be available until (currently unscheduled) 2.7.6.
In the meantime the best way would be to use the timetuple method to get the parts:
dt = datetime.now()
d = DateTime(*dt.timetuple()[:6])
# For UTC times, you need to pass 'kind' as a kwarg
# because of Python's rules around using * unpacking
udt = datetime.now()
ud = DateTime(*udt.timetuple()[:6], kind=DateTimeKind.Utc)
Now that 2.7.6 has been released, you can use clr.Convert to make an explicit cast, if needed. There could be better ways to do this, but I'm stealing this one from Jeff Hardy's commit.
>>> from System import DateTime
>>> from datetime import datetime
>>> from clr import Convert
>>> now_py = datetime.now()
>>> now_py
datetime.datetime(2020, 1, 1, 18, 28, 34, 598000)
>>> Convert(now_py, DateTime)
<System.DateTime object at 0x00000000006C [1/1/2020 6:28:34 PM]>
>>> now_py == Convert(now_py, DateTime)
True
DateTime(pydate) still crashes.
Jeff's answer gets a DateTime to the second. If you want to stretch the precision to the millisecond, you can use something like this:
def cs_date(date):
return DateTime(*date.timetuple()[:6] + (date.microsecond/1000,))
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.