QLineEdit: Set cursor location to beginning on focus
Asked Answered
Z

3

7

I have a QLineEdit with an input mask, so that some kind of code can easily be entered (or pasted). Since you can place the cursor anywhere in the QLineEdit even if there is no text (because there is a placeholder from the input mask):

enter image description here

If people are careless and unattentive enough this leads to them typing in the middle of the text box whereas they should start typing at the beginning. I tried the trivial way of ensuring that the cursor is at the start upon focus by installing an event filter:

bool MyWindowPrivate::eventFilter(QObject * object, QEvent * event)
{
    if (object == ui.tbFoo && event->type() == QEvent::FocusIn) {
        ui.tbFoo->setCursorPosition(0);
    }
    return false;
}

This works fine with keybaord focus, i.e. when pressing or +, but when clicking with the mouse the cursor always ends up where I clicked. My guess would be that QLineEdit sets the cursor position upon click itself after it got focus, thereby undoing my position change.

Digging a little deeper, the following events are raised when clicking¹ and thus changing focus, in that order:

  1. FocusIn
  2. MouseButtonPress
  3. MouseButtonRelease

I can't exactly catch mouse clicks in the event filter, so is there a good method of setting the cursor position to start only when the control is being focused (whether by mouse or keyboard)?


¹ Side note: I hate that Qt has no documentation whatsoever about signal/event orders for common scenarios such as this.

Zestful answered 20/3, 2014 at 12:10 Comment(7)
Event order may depends on different factors (including user input). So it is not possible to describe any "standard" ways.Effortful
Re. side note: I agree. At least getting documentation patches approved shouldn't be hard, if you feel like getting that written up and included for posterity.Tippets
"I can't exactly catch mouse clicks in the event filter" Why? They are delivered as QMouseEvent.Tippets
Doing stuff like this in the event filter is not very wise. The reason is you are not filtering out the default behavior. So after you do your stuff, it will do the default focusInEvent behavior afterwards. What you should do is override the focusInEvent as suggested by Dmitry. Here you can see the default behavior.Panchromatic
@Kuba: Sure, but then I'd have to check some way of only handling a mouse click if it was preceded by a focus change and somehow not mess up if someone changes focus with the keyboard and then clicks the control. It's hairier than it needs to be.Zestful
@Joey: I was hoping you'd discover Thuga's way :)Tippets
Side note: It is an error not to call the base class's eventFilter. Qt's classes are free to act as event filters.Tippets
S
10

Below is an implementation that's factored into a separate class. It defers the setting of the cursor to after any pending events are posted for the object, thus sidestepping the issue of event order.

#include <QApplication>
#include <QLineEdit>
#include <QFormLayout>
#include <QMetaObject>

// Note: A helpful implementation of
// QDebug operator<<(QDebug str, const QEvent * ev)
// is given in https://mcmap.net/q/763978/-how-to-get-human-readable-event-type-from-qevent/1329652

/// Returns a cursor to zero position on a QLineEdit on focus-in.
class ReturnOnFocus : public QObject {
   Q_OBJECT
   /// Catches FocusIn events on the target line edit, and appends a call
   /// to resetCursor at the end of the event queue.
   bool eventFilter(QObject * obj, QEvent * ev) {
      QLineEdit * w = qobject_cast<QLineEdit*>(obj);
      // w is nullptr if the object isn't a QLineEdit
      if (w && ev->type() == QEvent::FocusIn) {
         QMetaObject::invokeMethod(this, "resetCursor",
                                   Qt::QueuedConnection, Q_ARG(QWidget*, w));
      }
      // A base QObject is free to be an event filter itself
      return QObject::eventFilter(obj, ev);
   }
   // Q_INVOKABLE is invokable, but is not a slot
   /// Resets the cursor position of a given widget.
   /// The widget must be a line edit.
   Q_INVOKABLE void resetCursor(QWidget * w) {
      static_cast<QLineEdit*>(w)->setCursorPosition(0);
   }
public:
   ReturnOnFocus(QObject * parent = 0) : QObject(parent) {}
   /// Installs the reset functionality on a given line edit
   void installOn(QLineEdit * ed) { ed->installEventFilter(this); }
};

class Ui : public QWidget {
   QFormLayout m_layout;
   QLineEdit m_maskedLine, m_line;
   ReturnOnFocus m_return;
public:
   Ui() : m_layout(this) {
      m_layout.addRow(&m_maskedLine);
      m_layout.addRow(&m_line);
      m_maskedLine.setInputMask("NNNN-NNNN-NNNN-NNNN");
      m_return.installOn(&m_maskedLine);
   }
};

int main(int argc, char *argv[])
{
   QApplication a(argc, argv);
   Ui ui;
   ui.show();
   return a.exec();
}

#include "main.moc"
Scimitar answered 20/3, 2014 at 14:27 Comment(7)
Crikey. I'd consider myself an absolute beginner regarding C++ and Qt (been using both for about 1½ years by now, give or take). This ... is way over my head for now. I think I'm sticking with the subclass for now. Mostly because I understand what it's doing (and thus I can document it accordingly). Thanks for your solution, though.Zestful
@Joey: Documented now :)Tippets
You really, really want me to use this method, right? :D Okay, I relented. After copying the implementation and basically reading every line it makes more sense now. I still couldn't write it myself, I fear.Zestful
@Zestful I try to be an educator :) I don't care much if anyone uses my code as long as they learn something along the way (even if it is that I was wrong - high-rep answers are wrong sometimes!). My biggest satisfaction in life comes from someone telling me "oh, I get it now". So you gave me a quantum of joy anyway, and I thank you for that.Tippets
@Zestful Lest I forget: in production code, you want to separate out the implementation from the interface. Thus the class and method declarations go into an .h file, while the method definitions go into a .cpp file. This goes "without saying", but I'll say it anyway :) I post it all as single file for brevity, ease of understanding, and ease of deployment (copy-paste into a fresh project, run).Tippets
Oh, you're welcome. To be fair, I'm constantly learning new things here, although very rarely by asking questions. Usually when I see "meta" things in Qt it's the moc-generated stuff while in the debugger and I've learned to just skip over that mentally. At the very least I now leared about invokeMethod and Q_INVOKABLE. The issue with focusing the window was there in the other variant as well and I think I can ignore that. My aim was just to provide a slightly nicer user experience here for a semi-common task :-)Zestful
let us continue this discussion in chatZestful
P
3

You could use a QTimer in focusInEvent to call a slot that sets the cursor position to 0.

This works well because a single shot timer posts a timer event at the end of the object's event queue. A mouse-originating focus-in event necessarily has the mouse clicks already posted to the event queue. Thus you're guaranteed that the timer's event (and the resulting slot call) will be invoked after any lingering mouse press events.

void LineEdit::focusInEvent(QFocusEvent *e)
{
    QLineEdit::focusInEvent(e);
    QTimer::singleShot(0, this, SLOT(resetCursorPos()));
}

void LineEdit::resetCursorPos()
{
    setCursorPosition(0);
}
Panchromatic answered 20/3, 2014 at 13:34 Comment(4)
Oh, I didn't think about singleShot with zero delay posting directly into the event queue and a timer with a set timeout felt quite hacky to me (not to mention the user confusion of the cursor jumping around).Zestful
Interestingly this does not work with an event filter, only in a subclass.Zestful
@Zestful It would work with an event filter if there was a way of passing a functor to singleShot. It would then look like this: QTimer::singleShot(0, this, [this]{ ui->lineEdit.setCursorPosition(0); });Tippets
@Zestful Given what QTimer::singleShot does, it's perfectly equivalent to use QMetaObject::invokeMethod or QMetaMethod::invoke with a queued connection. That's what I leverage in my answer. Remember: a zero-length timer event is simply a QMetaCallEvent dropped into the event queue.Tippets
S
0

set a validator instead of inputmask . http://doc.qt.io/qt-5/qregularexpressionvalidator.html

Sulk answered 10/9, 2015 at 7:42 Comment(2)
The input mask has the benefit of immediately making the expected pattern clear to the user. A validator does not do that and only validates the content. Those are two very separate concerns.Zestful
But I don't think its a wrong answer either that you down voted it.Sulk

© 2022 - 2024 — McMap. All rights reserved.