Skip to content
Advertisement

Determine the element which previously had focus?

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_())    
User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement