[Qgis-user] keyboard navigation in forms with multiline text fields

Ludwig Kniprath ludwig at kni-online.de
Wed Sep 23 14:38:08 PDT 2026


Hello Tobias,

interesting problem, because in "multiline-text-fields" (Qt: 
QPlainTextEdit rsp. QTextBrowser) the Tab-Key always has the function to 
insert a tab, what else... :)

The workaround-script below handles edits in "multiline-text-fields", if 
the user inserts a tab, it will instead switch the focus to the next 
form-element. By using this, the user won't be able to insert any tabs 
at all.

It has to be registered as init-code for the forms of your layer, see below.

Surprisingly complicated, perhaps there's a simpler solution...

Incidentally, Shift-Tab has no special function in multi-line text 
fields and simply moves the focus to the previous element.

from qgis.PyQt.QtWidgets import 
QDialog,QWidget,QPlainTextEdit,QTextBrowser,QCheckBox,QDateTimeEdit,QFrame
from qgis.PyQt.QtCore import Qt,QObject
from qgis.PyQt.QtGui import QTextCursor,QTextDocument
from qgis.gui import QgsFilterLineEdit,QgsSpinBox

def contents_change(position:int, chars_removed:int, chars_added: int):
     """triggered, if the content of the connected 
form_widget.document() has changed
     Will capture a single tab-insert and instead switch the focus to 
the next form-element.

     Args:
         position (int): position where edit was done
         chars_removed (int): number of removed chars
         chars_added (int): number of added chars
     """
     # check 1: only one char added
     if chars_added == 1:
         # QTextDocument, which was connected with this slot
         q_txt_dcmt:QTextDocument = QObject().sender()
         # check 2: and it was a tab
         last_char:str = q_txt_dcmt.toPlainText()[position]
         if last_char == "\t":
             # undo edit by deleting last_char
             cursor = QTextCursor(q_txt_dcmt)
             cursor.setPosition(position)
             cursor.deleteChar()

             # searching for QFrame with objectName 'mAttributeEditor' 
as topmost widget for the focusable form-elements
             # bottom-up from QObject().sender()
             att_ed_qfr:QFrame = None
             qpte:QTextBrowser|QPlainTextEdit = None
             parent = QObject().sender().parent()
             while parent:
                 if parent.objectName() and parent.__class__ in 
[QTextBrowser,QPlainTextEdit]:
                     # the triggering widget
                     qpte = parent
                 if parent.objectName() == "mAttributeEditor":
                     # topmost widget
                     att_ed_qfr = parent
                     break
                 parent = parent.parent()

             if att_ed_qfr and qpte:
                 qpte.setPlainText(q_txt_dcmt.toPlainText())

                 form_widgets = []
                 form_idx = 0
                 # findChildren fortunately delivers the widgets in 
tabIndex-order...
                 for widget in 
att_ed_qfr.findChildren(QWidget,options=Qt.FindChildOption.FindChildrenRecursively):
                     # hope to have all QGis-used widget-classes for 
attribute-forms
                     if widget.objectName() and widget.__class__ in 
[QTextBrowser,QPlainTextEdit,QgsFilterLineEdit,QCheckBox,QgsSpinBox,QDateTimeEdit]:
                         if widget == qpte:
                             # the index of the triggering widget
                             form_idx = len(form_widgets)
                         form_widgets.append(widget)

                 # focus next form-element
                 focus_idx = form_idx + 1 if form_idx < 
len(form_widgets) - 1 else 0
                 form_widgets[focus_idx].setFocus()
             else:
                 raise Exception("Something went terribly wrong...")

def my_form_open(dialog:QDialog, layer, feature):
     # identify the multiline form-widgets by type and objectName 
(matching to field-name)
     # and connect the contentsChange-signal to widget.document() (!)
     all_field_names = [field.name() for field in layer.fields()]

     for widget in 
dialog.findChildren(QWidget,options=Qt.FindChildOption.FindChildrenRecursively):
         if widget.objectName() in all_field_names and 
(isinstance(widget,QPlainTextEdit) or isinstance(widget,QTextBrowser)):
widget.document().contentsChange.connect(contents_change)



Register as Attribute-form-init-function: Copy&Paste code, adopt the 
other settings



Depending on your settings messages will be displayed each time you open 
a form:

That gets annoying after a while and can be prevented as follows:

Settings > Options > General > Project Files


hth

Ludwig

Am 22.09.26 um 16:16 schrieb Tobias Schula via QGIS-User:
> Hello list!
>
> I've designed an input form that contains multiline text fields for a 
> longer description of a feature. The users should be able to use the 
> form with the keyboard, so they can navigate with "Tab", "Shift+Tab", 
> accept changes with "Enter" and decline changes with "Esc".
>
> However, once I put a multiline text field in the form, the user is 
> trapped in this field and cannot escape without using the mouse. This 
> greatly interrupts the workflow, since the users are usually inputting 
> hand written field notes at their desk.
>
> I can't seem to find any documentation on the navigation in forms, nor 
> have I gotten to work any combinations of "Ctrl", "Shift", "Tab" or 
> "Enter". I can only close the form with "Esc", but then all input will 
> be lost.
>
> Does anyone have any idea or can someone point me to documentation? If 
> there is no way to exit multiline fields, I will file a bug/feature 
> request.
>
>
> Best regards
>
>
> _______________________________________________
> QGIS-User mailing list
> QGIS-User at lists.osgeo.org
> List info:https://lists.osgeo.org/mailman/listinfo/qgis-user
> Unsubscribe:https://lists.osgeo.org/mailman/listinfo/qgis-user
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.osgeo.org/pipermail/qgis-user/attachments/20260923/a9bb81e1/attachment-0001.htm>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: ow5eEI1TGLU0z83k.png
Type: image/png
Size: 63084 bytes
Desc: not available
URL: <http://lists.osgeo.org/pipermail/qgis-user/attachments/20260923/a9bb81e1/attachment-0004.png>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: vrBExeLr9bJkcRdA.png
Type: image/png
Size: 76661 bytes
Desc: not available
URL: <http://lists.osgeo.org/pipermail/qgis-user/attachments/20260923/a9bb81e1/attachment-0005.png>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: EpAXgPR4SqZCucPK.png
Type: image/png
Size: 15191 bytes
Desc: not available
URL: <http://lists.osgeo.org/pipermail/qgis-user/attachments/20260923/a9bb81e1/attachment-0006.png>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: RuWx0RtgAgtIIgm1.png
Type: image/png
Size: 90228 bytes
Desc: not available
URL: <http://lists.osgeo.org/pipermail/qgis-user/attachments/20260923/a9bb81e1/attachment-0007.png>


More information about the QGIS-User mailing list