Suppress departure from cell in QTableView

classic Classic list List threaded Threaded
11 messages Options
Reply | Threaded
Open this post in threaded view
|

Suppress departure from cell in QTableView

jfsturtz
Hi.  [Windows 10/Python 3.7.1/PyQt 5.11.3; My first time posting here, so please pardon in advance if anything is not quite right ...]

I have a QTableView with associated QAbstractTableModel.  The View uses a Delegate that creates a QLineEdit widget for cell editing.

I think (I hope) what I'm trying to do is conceptually very simple:  When editing in a cell is in progress, I want to prevent departure from the cell.  (This would typically be under circumstances where the cell contents are determined not to be valid).  For example, while editing the contents of a cell, the user hits Tab to move to the next cell.  I want to suppress that and leave the cursor right where it is, as though nothing happened.

After reading the QEvent documentation, I think I should be able to do this by filtering and suppressing either the FocusAboutToChange or FocusOut events.

So I define a QObject to serve as the event filter, and gave it an eventFilter() method.  Then I use installEventFilter() to install it as the event filter for the QLineEdit.

If I start editing in a cell, then hit (for example) the Tab key, the eventFilter() method properly detects the FocusAboutToChange and FocusOut events.  eventFilter() returns True for both of these, which should (I think) stop processing of the events.

But it doesn't.  Focus moves to the next cell anyhow, in spite of the filter. 

From everything I've read, I'd have thought I was doing this right.  What am I overlooking?

Thanks! 

Since I haven't posted here before, I'm not exactly sure how to include sample code.  I'll try adding them as attachments to the e-mail, but here are links as well:

test.py  [QTableView, Delegate, editor widget and event filter definitions, and __main__]
model.py  [QAbstractTableModel definition, imported by test.py]

_______________________________________________
PyQt mailing list    [hidden email]
https://www.riverbankcomputing.com/mailman/listinfo/pyqt

model.py (1K) Download Attachment
test.py (3K) Download Attachment
Reply | Threaded
Open this post in threaded view
|

Re: Suppress departure from cell in QTableView

jfsturtz

On 2/16/2019 8:34:21 AM, Colin McPhail <[hidden email]> wrote:

Hi John,

I added a simple reimplementation of focusNextPrevChild() to the EditWidget class in your example:

    def focusNextPrevChild(self, next):
        print(f'focusNextPrevChild({next})')
        text = self.text()
        if len(text) == 3:
            return False
        return True

This causes TAB and Shift-TAB to be ignored unless the line editor contains exactly three characters. It doesn't prevent focus moving from a partially edited cell by means of cursor movement and mouse click, however.

Regards,
Colin

Hi Colin.  Thanks for the response!

This is interesting, insofar as I was unaware of focusNextPrevChild(), so it's new information to me.

However, I already had been able to produce that effect by catching the Tab and shift-Tab keypress events (as well as the other keypress events that depart the editor cell) in the EditWidget event handler.  That worked fine.

Then I realized you can depart the cell by mouse movement and mouse click as well.  So I set about the task of trying to catch mouse click events and suppress them also.  That I couldn't get to work.  It was several days ago, and I can't remember the details.  But I think the problem was this:  If you click outside the edit cell, it isn't the editor widget that receives the mouse click event (which is what you'd expect, I guess).  I think it's the QTableView object.  By the time the View detects the mouse click, focus has been lost already from the edit widget, and it's too late to prevent it.  [I'd have to go back and look at it again, but my recollection is that no matter how I tried to intercept mouse clicks, the editor widget always lost focus anyhow].

That was what put me onto the path of trying to catch and suppress the FocusOut event.  I'm keenly curious as to why what I did doesn't work.  It seems from all I've read that it should.

I see there is also a focusOutEvent() method that can be reimplemented.  But alas, it doesn't appear to allow any way to specify that the event should be suppressed.  I'm starting to wonder if there just isn't any way to do this.  That would surprise me ...

Thanks again.

/John

_______________________________________________
PyQt mailing list    [hidden email]
https://www.riverbankcomputing.com/mailman/listinfo/pyqt
Reply | Threaded
Open this post in threaded view
|

Re: Suppress departure from cell in QTableView

Maurizio Berti
Working with focus events in item views might be a real headache, since you have to deal with the whole widget tree hierarchy a view has, including the editor.

I'd suggest another approach, which makes more sense to me: the closeEditor() is what actually closes (and eventually commits) data to the model, and it's called from key events and accepted mousePressEvent()s, which actually try to close the editor, change the current index and, eventually, create a new editor).
This is a very basic implementation.

class KeepFocusTable(QtWidgets.QTableView):
    currentEditor = None

    def closeEditor(self, editor, hint):
        self.currentEditor = editor

    def mousePressEvent(self, event):
        if self.state() == self.EditingState and self.currentEditor is not None:
            self.currentEditor.setFocus()
            self.currentEditor.selectAll()
        else:
            QtWidgets.QTableView.mousePressEvent(self, event)

If you return closeEditor(), the editor will stay active, and the view state() will be kept as EditingState.
This allows you to keep the focus on the editor when using the [shift-]tab, enter and escape keys; I also added a currentEditor property that allows to keep track of the current editor.
The mousePressEvent() can now check the state, since it's still in EditingState and a currentEditor exists, it will focus it; the selectAll() is obviously optional, I just added so that the user can clearly see that the focus has been kept; you might also want to ensure that the cell is visible (in case the user has scrolled "out" of it) by using scrollTo().

At this point you can add your implementation to check and validate the contents (for example by using a QValidator inside the closeEditor implementation), and eventually call the QTableView.closeEditor original method whenever you find it appropriate.

I hope this helps,
Maurizio

--
È difficile avere una convinzione precisa quando si parla delle ragioni del cuore. - "Sostiene Pereira", Antonio Tabucchi
http://www.jidesk.net

_______________________________________________
PyQt mailing list    [hidden email]
https://www.riverbankcomputing.com/mailman/listinfo/pyqt
Reply | Threaded
Open this post in threaded view
|

Re: Suppress departure from cell in QTableView

jfsturtz
Greetings Maurizio.

Yes, this certainly does help!  After a little playing around, it looks like this is going to lead me to a solution.  :-)

There are still a few things I don't fully understand.

I actually did experiment with reimplementing closeEditor() awhile back.  But I reimplemented it in the delegate, not in the view.  I thought I understood closeEditor() to be a member of QAbstractItemDelegate, not QTableView.  I don't quite understand how it is possible to override this method in the QTableView definition.  (Still, empirically, it seems to work, so I must be wrong).

One slight problem with your code as written:  I found that if a delegate editor is active and I mouse click on a different cell, the mousePressEvent actually registers before the closeEditor() method is called.  You assign self.currentEditor inside closeEditor(), so on the first mouse click it isn't set yet.  However, if I assign currentEditor when the editor is created (i.e., in the delegate's createEditor() method), then it seems to work.

And with that, I find I don't seem to need to reimplement closeEditor() at all.  Just catching a mousePressEvent, checking that self.state() is EditState and calling self.currentEditor.setFocus() to set the focus back to the editor widget seems to be enough.

[Modified code that demonstrates proof of concept is attached]

I will also need to similarly catch keystrokes that try to change to focus to a different cell.  But that is easier, because those events go to the delegate, so can be handled within the delegate's eventFilter() method.

I still don't really understand why it didn't work when I tried catching and suppressing the FocusOut event.  I feel like I followed the documentation properly.  But I guess it's somehow related to the fact that there are multiple levels of widgets that the events are passing through.

In any event, it looks like I have a workaround.  Thank you!

/John

_______________________________________________
PyQt mailing list    [hidden email]
https://www.riverbankcomputing.com/mailman/listinfo/pyqt

test.py (3K) Download Attachment
Reply | Threaded
Open this post in threaded view
|

Re: Suppress departure from cell in QTableView

Maurizio Berti
Il giorno dom 17 feb 2019 alle ore 23:19 John F Sturtz <[hidden email]> ha scritto:
[...] 
There are still a few things I don't fully understand.

I actually did experiment with reimplementing closeEditor() awhile back.  But I reimplemented it in the delegate, not in the view.  I thought I understood closeEditor() to be a member of QAbstractItemDelegate, not QTableView.  I don't quite understand how it is possible to override this method in the QTableView definition.  (Still, empirically, it seems to work, so I must be wrong).

Well, closeEditor is a signal in delegates, which is automatically connected to the closeEditor slot of the item view.
The signal is sent anyway from the delegate, whenever the focus is actually changed by the item view (or any other widget, including the widget's window losing focus), or when a "change cell" is internally requested for the delegate by the view (via tab navigation or any way of commit/ignore is supported by it): for example, if you use a QComboBox as an editor for the itemDelegate, the closeEditor won't be sent if the item of the combo has changed, but will be sent if the combo is editable and you "submit" any data or ignore it.

As explained in the QItemDelegate details ( https://doc.qt.io/archives/qt-4.8/qitemdelegate.html#details ):

The closeEditor() signal indicates that the user has completed editing the data, and that the editor widget can be destroyed.

The stress is on the "can be destroyed".
The fact that the signal is emitted does not actually mean that the editor will actually be closed: it only means that the editing is theoretically finished, and it's a way of telling the view (and the delegate) that it *should* close the editor.
In fact, you can actually disconnect the signal of the delegate from the relative slot of the view:

self.myTable.itemDelegate().closeEditor.disconnect(self.myTable.closeEditor)

This will result in an editor kept alive even when trying to commit (or ignore) the data by using keyboard events.
In spite of this, though, the view.closeEditor() slot will obviously be called anyway from the view the first time you click on any other cell, due to the currentChanged() slot called from the mousePressEvent(); note that if you return the closeEditor() slot as above - without actually destroying the editor -, the slot won't be called another time even if you change the current index again by clicking on another item or outside the table contents (I suppose that it's because the closeEditor signal has been already fired once and the delegate is still there), unless the focus is restored to the editor again first. Also, in the same scenario, changing the focus to another widget won't result in the closeEditor() slot called, but the signal will be fired anyway.

As a side note, be aware that if you implement the closeEditor slot, you'll need to properly set the slot signature to correctly allow the auto connection the view creates for the delegate to work and, eventually, disconnect from it.
In PyQt4, using PyQt objects was fine enough:

@QtCore.Slot(QtWidgets.QWidget, QtWidgets.QAbstractItemDelegate.EndEditHint)
def closeEditor(self, widget, hint):
    [...]

With PyQt5 (at least on my old version, 5.7.1) it seems that the actual C++ string signature is required as reported from the official Qt docs (not PyQt/PySide), so it will need to be like this in order to allow the original signal disconnection to work, since the connection was not "created by Python":

@QtCore.Slot('QWidget*', 'QAbstractItemDelegate::EndEditHint')
def closeEditor(self, widget, hint):
    [...]


That said, since the type of control you need can be required whenever any event type (tab navigation, editor commit/ignore, mouse events, etc) and source (the delegate, the view or the application) occurs, it's better to implement the closeEditor in the view itself, which is not only the "simplest" and most logic solution, but also the way Qt actually deals with item editing.


One slight problem with your code as written:  I found that if a delegate editor is active and I mouse click on a different cell, the mousePressEvent actually registers before the closeEditor() method is called.  You assign self.currentEditor inside closeEditor(), so on the first mouse click it isn't set yet.  However, if I assign currentEditor when the editor is created (i.e., in the delegate's createEditor() method), then it seems to work.

And with that, I find I don't seem to need to reimplement closeEditor() at all.  Just catching a mousePressEvent, checking that self.state() is EditState and calling self.currentEditor.setFocus() to set the focus back to the editor widget seems to be enough.

It works because you've set the currentEditor in the createEditor() slot.
I didn't know you were already using a delegate, so your solution can work too.

Just a small "conceptual" note about this. Remember that, even if your code works fine and makes perfect sense, from the modularity point of view, that's not "theoretically" good, as item delegates can be used interactively and more than once. A much more "elegant" way to do so would be to request the actual view from the parent argument of createEditor(); keep in mind, though, that the parent of the editor is the viewport of the view (since an item view is a QAbstractScrollArea and its contents are in an embedded "scrollable" QWidget), so the reference to the view is the parent() of the parent argument of createEditor() method, actually.
Anyway, as said, that's not an actual "error", and in most cases your implementation is fine enough (I've done something similar lots of times myself).


I will also need to similarly catch keystrokes that try to change to focus to a different cell.  But that is easier, because those events go to the delegate, so can be handled within the delegate's eventFilter() method.

Luckily enough, Qt's implementation correctly handles QKeyEvents of delegates and views in the right "levels". Tab navigation and return/escape keys are catched by the view, so you can take care of everything in the closeEditor() slot.


I still don't really understand why it didn't work when I tried catching and suppressing the FocusOut event.  I feel like I followed the documentation properly.  But I guess it's somehow related to the fact that there are multiple levels of widgets that the events are passing through.

As I mentioned, working with focus events in widgets as complex as views can be very hard to deal with.
You have an inherited QAbstractItemView which is inherited from QAbstractScrollArea, which might even have one or two QHeaderViews and QScrollBars (and other scrollbar additional widgets, optionally), an optional cornerWidget, and a QWidget that works as a viewport, containing the actual view contents; *then* you can have an editor, which can also be a complex widget like a combobox (with its internal widgets handling focus on their own, including the popup view). Moreover, a QTableView also has a cornerButton too.
So, you'll have to exactly understand the hierarchy of all those widget levels, remembering that any widget reacts differently according to it's level in the tree hierarchy and the event type, by accepting or ignoring the event (which doesn't mean that they won't deal with it) and eventually sending it "back" to the whole parent widget tree if required.
Finally, whenever an editor is active, it has a focusProxy set to the view's focusProxy, which means that that widget handles focus events of another one, thus reacting to those events in its place first.

 
In any event, it looks like I have a workaround.  Thank you!

Glad to be of any help! :-)

Cheers,
Maurizio

PS: in your code, you've been using model.setData() inside the setEditorData() method of the itemDelegate, which not only doesn't makes not much sense, but might also result in an infinite recursion.

--
È difficile avere una convinzione precisa quando si parla delle ragioni del cuore. - "Sostiene Pereira", Antonio Tabucchi
http://www.jidesk.net

_______________________________________________
PyQt mailing list    [hidden email]
https://www.riverbankcomputing.com/mailman/listinfo/pyqt
Reply | Threaded
Open this post in threaded view
|

Re: Suppress departure from cell in QTableView

jfsturtz
Wow!  Thank you so much for all this information.  In spite of the time I have spent with PyQt, it is clear I still have much to learn.  :-/
Well, closeEditor is a signal in delegates, which is automatically connected to the closeEditor slot of the item view.
Doh!  The documentation for QAbstractItemDelegate clearly says closeEditor is a signal.  This was just a stupid oversight on my part.  [:headslap:]

A much more "elegant" way to do so would be to request the actual view from the parent argument of createEditor(); keep in mind, though, that the parent of the editor is the viewport of the view (since an item view is a QAbstractScrollArea and its contents are in an embedded "scrollable" QWidget), so the reference to the view is the parent() of the parent argument of createEditor() method, actually.
This is great!  I was looking for a way to get a reference to the view from the delegate, but I hadn't been able to find one.  Didn't look quite hard enough ...

Luckily enough, Qt's implementation correctly handles QKeyEvents of delegates and views in the right "levels". Tab navigation and return/escape keys are catched by the view, so you can take care of everything in the closeEditor() slot.
With your help, and after a bit more messing around, I have a version which seems nicely concise and reliable.  I validate input inside setModelData(), and call model.setData() if valid, but inhibit departure from the cell if not.  Then from closeEditor(), I use the same validation function, and close the editor if valid, but leave it open if not.

It seems to work for any method of cell departure, without explicitly catching mouse clicks or keystrokes.

I've attached it in case you're interested.  (For test purposes, the validation function calls any string of digits valid, and anything else not).

PS: in your code, you've been using model.setData() inside the setEditorData() method of the itemDelegate, which not only doesn't makes not much sense, but might also result in an infinite recursion.
This is a fallout of the following peculiar fact:  My actual intention is to use a QLabel as the delegate editor widget, and not a QLineEdit.  I know that seems weird, but it gives me more precise control over the editing that occurs.  (Basically, I respond to keyboard input, modify the QLabel's text, then redisplay it.  In the attached example, I changed the QLineEdit back to a QLabel, so you can see what I mean).

So the reason I called model.setData() (with a null value) inside setEditorData() was to clear out the previous cell contents.  If I didn't, the previous cell contents and the QLabel text would both display, overlapped with one another.

It turns out all I needed to do was give the QLabel an opaque background, so it just covers up the previous cell contents.  :-)

------

There is one thing in the current version that still puzzles me:  In the most recent round of messing around, I decided to subclass QLabel, and then use an object of the subclass as the editor widget rather than an actual QLabel.  And I decided to move keypress handling from eventFilter() in the delegate to keyPressEvent() in the editor widget subclass.  In the process, it seemed to change the way Tab is handled.

If you run the code as I attached it, keyboard input is handled by the delegate's eventFilter() method.  It doesn't do anything with the Tab character.  But if you hit Tab while editing, it closes the editor, moves to the next cell, and that cell is not opened for editing.

If you comment out the eventFilter() method (lines 59-81), and enable keyPressEvent() in the editor widget subclass (lines 92-112), then tabbing out of an edit field closes the editor, and opens the next field up for editing.

The latter is the default behavior for the Tab character, I believe.  But the former is actually what I want.  So it works the way I'd wish when I handle keyboard input in the delegate.  But I'm not doing anything with Tab character in either case, so it seems strange to me that in one case I'm getting one behavior, and in the other case another.


In any event, I think I'm making progress.  Thanks again for the help!  I really appreciate it.

/John

_______________________________________________
PyQt mailing list    [hidden email]
https://www.riverbankcomputing.com/mailman/listinfo/pyqt

test.py (4K) Download Attachment
model.py (1K) Download Attachment
Reply | Threaded
Open this post in threaded view
|

Re: Suppress departure from cell in QTableView

Maurizio Berti
Il giorno lun 18 feb 2019 alle ore 05:31 John F Sturtz <[hidden email]> ha scritto:
Wow!  Thank you so much for all this information.  In spite of the time I have spent with PyQt, it is clear I still have much to learn.  :-/

You'll never end to learn ;-)
I still learn things from day to day (also by replying to this mailing list, as in this case).
While I'm still "stuck" with Python 2.7 and PyQt4 (which are very old) for my most important project, I realize that there are dozens of things I still do not know yet, even with products that old. And that's good :-)
 
A much more "elegant" way to do so would be to request the actual view from the parent argument of createEditor(); keep in mind, though, that the parent of the editor is the viewport of the view (since an item view is a QAbstractScrollArea and its contents are in an embedded "scrollable" QWidget), so the reference to the view is the parent() of the parent argument of createEditor() method, actually.
This is great!  I was looking for a way to get a reference to the view from the delegate, but I hadn't been able to find one.  Didn't look quite hard enough ...

That's neat, isn't it? It took me a while to understand that, since the parent didn't seem to be a "real" and "usable" widget until I actually realized the QAbstractScrollArea inheritance of item views, and I'm still not quite sure that it's the best (and "programmatically correct") way to handle this, but I think that it's a better and more coherent way to achieve that in most cases.
Obviously, if you really know what you're doing (and, most importantly, what you might be doing in the future), setting a "virtual" parent view while init-ing the delegate is fine enough.

PS: in your code, you've been using model.setData() inside the setEditorData() method of the itemDelegate, which not only doesn't makes not much sense, but might also result in an infinite recursion.
This is a fallout of the following peculiar fact:  My actual intention is to use a QLabel as the delegate editor widget, and not a QLineEdit.  I know that seems weird, but it gives me more precise control over the editing that occurs.  (Basically, I respond to keyboard input, modify the QLabel's text, then redisplay it.  In the attached example, I changed the QLineEdit back to a QLabel, so you can see what I mean).
[...] 
There is one thing in the current version that still puzzles me:  In the most recent round of messing around, I decided to subclass QLabel, and then use an object of the subclass as the editor widget rather than an actual QLabel.  And I decided to move keypress handling from eventFilter() in the delegate to keyPressEvent() in the editor widget subclass.  In the process, it seemed to change the way Tab is handled. 
[...] 
The latter is the default behavior for the Tab character, I believe.  But the former is actually what I want.  So it works the way I'd wish when I handle keyboard input in the delegate.  But I'm not doing anything with Tab character in either case, so it seems strange to me that in one case I'm getting one behavior, and in the other case another.

I'm not quite sure I'm following you on that.
I think I can understand your "precise control" needs, but if you can better explain what your needs are, we might help with that.
Using a QLabel for editing is not suggested, not only because it's not its purpose, but also because it doesn't provide sufficient editing cababilities and aidings, from both programming and UX perspectives (and I'm usually mostly focused on the latter).
Let me explain on that.
A label doesn't show the editing cursor (you might object that for simple editings it doesn't matter that much, but knowing where you're typing is a huge feedback) and it doesn't provide any selection nor clipboard implementation. From the user point of view that's not good, as its usage is not intuitive: while the user can "learn" how it works from its behavior, it's usage doesn't justify the change of behavior between similar and common [table/editing] user interfaces.
From a GUI programming perspective, it's also a problem whenever "common" keyboard shortcuts come in place. If I type a valid value in the editing cell and then use any keyboard navigation shortcut (arrows, page up/down, home/end, and further modifier combinations) I'd expect to "move" the editing cursor, while in you implementation it results in submitting the value and changing focus to another cell in the view (since the value is considered valid), according to the QKeySequence class implementation of QKeyEvents of an item view. So, you'll need to catch *all* those cases in the eventFilter() according to those listed in https://doc.qt.io/archives/qt-4.8/qabstractitemview.html#details , including possible multiple cell selection scenarios, since you're returning False to all "unrecognized" cases of keyboard events - thus letting the parent object (the item view, its container layout/widget, etc) handle those events.
That said, it might be better to keep the default QLineEdit editor. You can then set your custom QValidator to the editor in the createEditor() or even implement your own editor. Since it seems you're requiring numeric values (as it looks from the regex you use in the validate() method) you can even return a custom inherited QSpinBox, and implement its own validate(), fixup() and/or textFromValue() methods according to your needs. I know it might look a bit more complicated to program with, but you'll eventually realize that it's a better solution once you understand how they behave and how all this can actually help both you and your users.

Finally, if you are really aware about those issues, and you really want to "escape" tab keypresses only, just add an elif key == QtCore.Qt.Key_Tab statement to your eventFiler() and check for the QtCore.Qt.ShiftModifier.


In any event, I think I'm making progress.  Thanks again for the help!  I really appreciate it.

Glad to be of any help :-)

Cheers,
Maurizio

--
È difficile avere una convinzione precisa quando si parla delle ragioni del cuore. - "Sostiene Pereira", Antonio Tabucchi
http://www.jidesk.net

_______________________________________________
PyQt mailing list    [hidden email]
https://www.riverbankcomputing.com/mailman/listinfo/pyqt
Reply | Threaded
Open this post in threaded view
|

Re: Suppress departure from cell in QTableView

jfsturtz

On 2/20/2019 6:21:15 PM, Maurizio Berti <[hidden email]> wrote:


This is a fallout of the following peculiar fact:  My actual intention is to use a QLabel as the delegate editor widget, and not a QLineEdit.  I know that seems weird, but it gives me more precise control over the editing that occurs.  

I'm not quite sure I'm following you on that.
I think I can understand your "precise control" needs, but if you can better explain what your needs are, we might help with that.
Using a QLabel for editing is not suggested, not only because it's not its purpose, but also because it doesn't provide sufficient editing cababilities and aidings, from both programming and UX perspectives (and I'm usually mostly focused on the latter).
Let me explain on that.
A label doesn't show the editing cursor (you might object that for simple editings it doesn't matter that much, but knowing where you're typing is a huge feedback) and it doesn't provide any selection nor clipboard implementation. From the user point of view that's not good, as its usage is not intuitive: while the user can "learn" how it works from its behavior, it's usage doesn't justify the change of behavior between similar and common [table/editing] user interfaces.
This is a surprisingly hot-button issue!  I posted a question to stackoverflow that involved this issue, and someone grilled me quite extensively on my use of QLabel for this.

It may be that I could make it work fine using a QLineEdit.  I haven't played with it much, and I probably should.  There are two things I am doing with cell input that I think would be complicated using a QLineEdit:
  1. Validating cell input character by character.  Each column in the table view has regular expression validation.  I catch a character when it is typed, determine what the cell contents would be with that character added, and disallow that input right at that moment if it would be invalid.
    (I think this might be workable with a QLineEdit; I haven't messed with it ...)
  2. There are cases where I want to have different text style for different parts of the cell contents (for example, some part of the text in Italic and the rest not).
    This is quite easy to do with a QLabel because it can contain rich text (e.g.,'foo<span style="font-style: italic;">bar</span>').  This doesn't appear to work for the contents of a QLineEdit widget.

_______________________________________________
PyQt mailing list    [hidden email]
https://www.riverbankcomputing.com/mailman/listinfo/pyqt
Reply | Threaded
Open this post in threaded view
|

Re: Suppress departure from cell in QTableView

jfsturtz
In reply to this post by Maurizio Berti
Hello again Maurizio.

I attempted a response to your message a few minutes ago, and accidentally sent it before I was finished with it.  :-/

Please disregard that one; this is the full response:
I'm not quite sure I'm following you on that.
I think I can understand your "precise control" needs, but if you can better explain what your needs are, we might help with that.
Using a QLabel for editing is not suggested, not only because it's not its purpose, but also because it doesn't provide sufficient editing cababilities and aidings, from both programming and UX perspectives (and I'm usually mostly focused on the latter).
Let me explain on that.
A label doesn't show the editing cursor (you might object that for simple editings it doesn't matter that much, but knowing where you're typing is a huge feedback) and it doesn't provide any selection nor clipboard implementation. From the user point of view that's not good, as its usage is not intuitive: while the user can "learn" how it works from its behavior, it's usage doesn't justify the change of behavior between similar and common [table/editing] user interfaces.
This is a surprisingly hot-button issue! I posted a question to stackoverflow that involved this issue, and someone grilled me quite extensively on my use of QLabel in this way.

It may be that I could make it work fine using a QLineEdit. I haven't played with it much, and I probably should. There are two things I am doing with cell input that I think would be complicated using a QLineEdit:
  1. Validating cell input character by character. Each column in the table view has regular expression validation. I catch a character when it is typed, determine what the cell contents would be with that character added, and disallow that input right at that moment if it would be invalid.
    (I think this might be workable with a QLineEdit; I haven't messed with it ...)

  2. There are cases where I want to have different text style for different parts of the cell contents (for example, some part of the text in italic and the rest normal).  This is quite easy to do with a QLabel because it can contain rich text -- e.g.,
    setText('foo<span style="font-style: italic;">bar</span>')
    This doesn't appear to work for the contents of a QLineEdit widget.
    I did find this stackoverflow post regarding styling of text within a QLineEdit:
    https://stackoverflow.com/questions/14417333/how-can-i-change-color-of-part-of-the-text-in-qlineedit
    But it looks like a lot of messiness ...
/John

_______________________________________________
PyQt mailing list    [hidden email]
https://www.riverbankcomputing.com/mailman/listinfo/pyqt
Reply | Threaded
Open this post in threaded view
|

Re: Suppress departure from cell in QTableView

Maurizio Berti
Il giorno ven 22 feb 2019 alle ore 02:37 John F Sturtz <[hidden email]> ha scritto:
This is a surprisingly hot-button issue! I posted a question to stackoverflow that involved this issue, and someone grilled me quite extensively on my use of QLabel in this way.

Well, that's understandable: as I pointed out (which is probably what that user told you about), that's not the purpose of a label. While it "can" work for simple scenarios, it's not intuitive from the user perspective, and can be an issue whenever the size of the cell doesn't allow to show the complete text the user is typing.


It may be that I could make it work fine using a QLineEdit. I haven't played with it much, and I probably should. There are two things I am doing with cell input that I think would be complicated using a QLineEdit:

Validating cell input character by character. Each column in the table view has regular expression validation. I catch a character when it is typed, determine what the cell contents would be with that character added, and disallow that input right at that moment if it would be invalid.
(I think this might be workable with a QLineEdit; I haven't messed with it ...)

There are cases where I want to have different text style for different parts of the cell contents (for example, some part of the text in italic and the rest normal).  This is quite easy to do with a QLabel because it can contain rich text -- e.g.,
setText('foo<span style="font-style: italic;">bar</span>')
This doesn't appear to work for the contents of a QLineEdit widget.
I did find this stackoverflow post regarding styling of text within a QLineEdit:
https://stackoverflow.com/questions/14417333/how-can-i-change-color-of-part-of-the-text-in-qlineedit

Since you need inline text formatting, implementing QLineEdit can be an issue as you'd probably need to do all the painting by yourself.

I'd suggest another approach instead: use a QTextEdit. It's implementation is a bit harder to get, but allows a better way to do what you need, while giving the user a standard and much more comfortable text editing UX.

Here's a small example I made which also uses QSyntaxHighlighter to do the formatting (in this case, it makes all occurrences of the word "red" in italic red).
It doesn't count in the "unallowed" text input you wrote about, but that can be done inside the keyPressEvent() method (but the release could need some checking too); you could also validate the input by connecting the textChanged() signal, to inhibit invalid clipboard pasting (like new line characters).


class MyHighlighter(QtGui.QSyntaxHighlighter):
    def __init__(self, *args, **kwargs):
        QtGui.QSyntaxHighlighter.__init__(self, *args, **kwargs)
        self.myFormat = QtGui.QTextCharFormat()
        self.myFormat.setFontItalic(True)
        self.myFormat.setForeground(QtCore.Qt.red)
        self.re = re.compile(r'(red)+')

    def highlightBlock(self, text):
        for match in self.re.finditer(text):
            self.setFormat(match.start(), match.end() - match.start(), self.myFormat)


class MyEditor(QtWidgets.QTextEdit):
    submit = QtCore.pyqtSignal()

    def __init__(self, *args, **kwargs):
        QtWidgets.QTextEdit.__init__(self, *args, **kwargs)
        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setWordWrapMode(QtGui.QTextOption.NoWrap)
        self.highlighter = MyHighlighter(self.document())

    def keyPressEvent(self, event):
        if event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
            self.submit.emit()
        elif event.key() == QtCore.Qt.Key_Tab:
            # Ignore tab by letting the parent(s) handle it, as you are 
            # probably not interested in the tab character.
            # Since you also want to prevent leaving the cell editing 
            # if the content is not valid, here you can also choose to
            # accept the event, inhibiting cell departure.
            # Please note that the shift-tab is intercepted by the view.
            event.ignore()
        else:
            QtWidgets.QTextEdit.keyPressEvent(self, event)


class MyDelegate(QtWidgets.QStyledItemDelegate):
    def createEditor(self, parent, option, index):
        editor = MyEditor(parent)
        editor.setPlainText(index.data())
        editor.submit.connect(lambda: self.submit(editor, index))
        return editor

    def submit(self, editor, index):
        # You can also check for contents before proceeding with this
        self.setModelData(editor, index.model(), index)
        self.closeEditor.emit(editor, self.EditNextItem)

    def setModelData(self, editor, model, index):
        # This is required, otherwise you'll get the rich text contents as XHTML
        model.setData(index, editor.toPlainText(), QtCore.Qt.DisplayRole)


Now you only have to setItemDelegate() on your view, and eventually set the highlighting [validation] by checking the index.column() argument in the createEditor() method according to your needs (or just create different delegates for each column).

Cheers!

Maurizio

--
È difficile avere una convinzione precisa quando si parla delle ragioni del cuore. - "Sostiene Pereira", Antonio Tabucchi
http://www.jidesk.net

_______________________________________________
PyQt mailing list    [hidden email]
https://www.riverbankcomputing.com/mailman/listinfo/pyqt
Reply | Threaded
Open this post in threaded view
|

Re: Suppress departure from cell in QTableView

jfsturtz
Thanks again Maurizio!  This is very cool.

I played around with using QLineEdit, and that works quite well for most of the columns in my view.  The nice thing about that is that I can use a QRegExpValidator for validation, which is pretty slick.

Actually, the reason I was wanting to do special text formatting in some of the columns is to implement auto-completion (basically, what the user has typed in so far shows in one style, and the auto-completed portion in another).  It turns out using a QLineEdit, I can use a QCompleter for this.  I played around with that too, and it works pretty well.

On the other hand, I could implement my own auto-completion and use QTextEdit as you have done.  It so happens that the columns in my view that will have auto-completion don't have much validation, so I wouldn't really miss the QRegExpValidator for those cells.

Thanks again!  You've given me lots of great ideas.

/John


Since you need inline text formatting, implementing QLineEdit can be an issue as you'd probably need to do all the painting by yourself.

I'd suggest another approach instead: use a QTextEdit. It's implementation is a bit harder to get, but allows a better way to do what you need, while giving the user a standard and much more comfortable text editing UX.

Here's a small example I made which also uses QSyntaxHighlighter to do the formatting (in this case, it makes all occurrences of the word "red" in italic red).
It doesn't count in the "unallowed" text input you wrote about, but that can be done inside the keyPressEvent() method (but the release could need some checking too); you could also validate the input by connecting the textChanged() signal, to inhibit invalid clipboard pasting (like new line characters).


class MyHighlighter(QtGui.QSyntaxHighlighter):
    def __init__(self, *args, **kwargs):
        QtGui.QSyntaxHighlighter.__init__(self, *args, **kwargs)
        self.myFormat = QtGui.QTextCharFormat()
        self.myFormat.setFontItalic(True)
        self.myFormat.setForeground(QtCore.Qt.red)
        self.re = re.compile(r'(red)+')

    def highlightBlock(self, text):
        for match in self.re.finditer(text):
            self.setFormat(match.start(), match.end() - match.start(), self.myFormat)


class MyEditor(QtWidgets.QTextEdit):
    submit = QtCore.pyqtSignal()

    def __init__(self, *args, **kwargs):
        QtWidgets.QTextEdit.__init__(self, *args, **kwargs)
        self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setWordWrapMode(QtGui.QTextOption.NoWrap)
        self.highlighter = MyHighlighter(self.document())

    def keyPressEvent(self, event):
        if event.key() in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
            self.submit.emit()
        elif event.key() == QtCore.Qt.Key_Tab:
            # Ignore tab by letting the parent(s) handle it, as you are 
            # probably not interested in the tab character.
            # Since you also want to prevent leaving the cell editing 
            # if the content is not valid, here you can also choose to
            # accept the event, inhibiting cell departure.
            # Please note that the shift-tab is intercepted by the view.
            event.ignore()
        else:
            QtWidgets.QTextEdit.keyPressEvent(self, event)


class MyDelegate(QtWidgets.QStyledItemDelegate):
    def createEditor(self, parent, option, index):
        editor = MyEditor(parent)
        editor.setPlainText(index.data())
        editor.submit.connect(lambda: self.submit(editor, index))
        return editor

    def submit(self, editor, index):
        # You can also check for contents before proceeding with this
        self.setModelData(editor, index.model(), index)
        self.closeEditor.emit(editor, self.EditNextItem)

    def setModelData(self, editor, model, index):
        # This is required, otherwise you'll get the rich text contents as XHTML
        model.setData(index, editor.toPlainText(), QtCore.Qt.DisplayRole)


Now you only have to setItemDelegate() on your view, and eventually set the highlighting [validation] by checking the index.column() argument in the createEditor() method according to your needs (or just create different delegates for each column).

Cheers!

Maurizio

_______________________________________________
PyQt mailing list    [hidden email]
https://www.riverbankcomputing.com/mailman/listinfo/pyqt