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.
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_())