In Java a FocusEvent
has a method getOppositeComponent()
which is where the focus came from or went to.
In PyQt5 is there any way to find what previously had focus when overriding focusInEvent
?
As explained in a note, I want to be able to start an edit session automatically when the table view gets focus, end an edit session by going Ctrl-E in the cell, leaving focus in the table view but without triggering another edit session.
MRE (using clunky added-attribute mechanism):
import sys from PyQt5 import QtWidgets, QtCore, QtGui class TableViewDelegate( QtWidgets.QStyledItemDelegate ): def createEditor(self, parent, option, index): # QPlainTextEdit for multi-line text cells... editor = QtWidgets.QPlainTextEdit( parent ) table_view = parent.parent() def end_edit(): print( f'end_edit...' ) # clunky mechanism table_view.do_not_start_edit = True self.closeEditor.emit( editor ) self.end_edit_shortcut = QtWidgets.QShortcut( 'Ctrl+E', editor, context = QtCore.Qt.ShortcutContext.WidgetShortcut ) self.end_edit_shortcut.activated.connect( end_edit ) return editor class TableViewModel( QtCore.QAbstractTableModel ): def __init__( self ): super().__init__() self._data = [ [ 1, 2, ], [ 3, 4, ], ] def rowCount( self, *args ): return len( self._data ) def columnCount(self, *args ): return 2 def data(self, index, role): if role == QtCore.Qt.DisplayRole: # print( f'data... {self._data[index.row()][index.column()]}') return self._data[index.row()][index.column()] def flags( self, index ): result = super().flags( index ) return QtCore.Qt.ItemFlag.ItemIsEditable | result class TableView( QtWidgets.QTableView ): def __init__(self ): super().__init__() self.setTabKeyNavigation( False ) self.setItemDelegate( TableViewDelegate() ) def focusInEvent( self, event ): print( f'table view focus-in event') super().focusInEvent( event ) if hasattr( self, 'do_not_start_edit' ): print( f'start of edit vetoed...') del self.do_not_start_edit return n_rows = self.model().rowCount() if n_rows == 0: return # go to last row, col 1 cell1_index = self.model().index( n_rows - 1, 1 ) self.edit( cell1_index ) class MainWindow( QtWidgets.QMainWindow ): def __init__( self ): super().__init__() self.setWindowTitle( 'Editor focus MRE' ) layout = QtWidgets.QVBoxLayout() self.table_view = TableView() table_label = QtWidgets.QLabel( '&Table' ) layout.addWidget( table_label ) table_label.setBuddy( self.table_view ) layout.addWidget( self.table_view ) self.table_view.setModel( TableViewModel() ) edit_box = QtWidgets.QLineEdit() layout.addWidget( edit_box ) centralwidget = QtWidgets.QWidget( self ) centralwidget.setLayout( layout ) self.setCentralWidget( centralwidget ) self.setGeometry( QtCore.QRect(400, 400, 400, 400) ) edit_box.setFocus() app = QtWidgets.QApplication([]) main_window = MainWindow() main_window.show() sys.exit(app.exec_())
Start by going Alt-T: this moves focus to the QTableView
and starts an edit of the bottom-right cell. Enter some text, and then press Ctrl-E. This stops the edit session and because of the clunky attribute do_not_start_edit
, a new edit session is (as desired) vetoed. Another way to end the edit session is to click in the QLineEdit
, for example.
I’m not sure that this rather clunky “added attribute” mechanism works in all circumstances. In fact it seems to work a bit better than I at first thought, hence this MRE… To me it doesn’t seem very elegant.
Advertisement
Answer
The answer is, I think, to use QApplication.focusChanged( old, new ), as alluded to by Musicamante in a comment, and combine this with overriding QStyledItemDelegate.destroyEditor( editor, index ) so that instead of destroying the editor component it is re-used each time createEditor
is called (lazy instantiation with first call). It is then trivial to detect when focus passes from the editor component to the QTableView
.
This “re-usable editor component” technique is fairly well-known in Java Swing (and probably JavaFX). It appears to work fine in PyQt5: modified MRE:
import sys from PyQt5 import QtWidgets, QtCore, QtGui class TableViewDelegate( QtWidgets.QStyledItemDelegate ): def __init__( self, *args, **kvargs ): super().__init__( *args, **kvargs ) self.editor = None def createEditor(self, parent, option, index): if not self.editor: self.editor = QtWidgets.QPlainTextEdit( parent ) def end_edit(): self.closeEditor.emit( self.editor ) self.end_edit_shortcut = QtWidgets.QShortcut( 'Ctrl+E', self.editor, context = QtCore.Qt.ShortcutContext.WidgetShortcut ) self.end_edit_shortcut.activated.connect( end_edit ) return self.editor def setEditorData(self, editor, index ): existing_text = index.model().data( index, QtCore.Qt.DisplayRole ) editor.document().setPlainText( str( existing_text ) ) def destroyEditor( self, editor, index ): index.model().setData( index, editor.document().toPlainText() ) editor.clear() class TableViewModel( QtCore.QAbstractTableModel ): def __init__( self ): super().__init__() self._data = [[ 1, 2, ], [ 3, 4, ],] def rowCount( self, *args ): return len( self._data ) def columnCount(self, *args ): return 2 def data(self, index, role): if role == QtCore.Qt.DisplayRole: return self._data[index.row()][index.column()] def setData(self, index, value, role = QtCore.Qt.EditRole ): if role == QtCore.Qt.EditRole: self._data[ index.row() ][ index.column() ] = value def flags( self, index ): result = super().flags( index ) return QtCore.Qt.ItemFlag.ItemIsEditable | result class TableView( QtWidgets.QTableView ): def __init__(self ): super().__init__() self.setTabKeyNavigation( False ) self.setItemDelegate( TableViewDelegate() ) def focus_changed( self, old, new ): print( f'table view focus change old {old} new {new}') if new == self: editor_component = self.itemDelegate().editor if old == None or old != editor_component: n_rows = self.model().rowCount() if n_rows == 0: return # go to last row, col 1 cell1_index = self.model().index( n_rows - 1, 1 ) self.edit( cell1_index ) else: print( 'edit command VETOED' ) class MainWindow( QtWidgets.QMainWindow ): def __init__( self ): super().__init__() self.setWindowTitle( 'Editor focus MRE' ) layout = QtWidgets.QVBoxLayout() self.table_view = TableView() table_label = QtWidgets.QLabel( '&Table' ) layout.addWidget( table_label ) table_label.setBuddy( self.table_view ) layout.addWidget( self.table_view ) self.table_view.setModel( TableViewModel() ) edit_box = QtWidgets.QLineEdit() layout.addWidget( edit_box ) centralwidget = QtWidgets.QWidget( self ) centralwidget.setLayout( layout ) self.setCentralWidget( centralwidget ) self.setGeometry( QtCore.QRect(400, 400, 400, 400) ) edit_box.setFocus() app = QtWidgets.QApplication([]) main_window = MainWindow() app.focusChanged.connect( main_window.table_view.focus_changed ) main_window.show() sys.exit(app.exec_())