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.

Joey
  • 344,408
  • 85
  • 689
  • 683
  • Event order may depends on different factors (including user input). So it is not possible to describe any "standard" ways. – Dmitry Sazonov Mar 20 '14 at 12:37
  • 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. – Kuba hasn't forgotten Monica Mar 20 '14 at 12:56
  • "I can't exactly catch mouse clicks in the event filter" Why? They are delivered as `QMouseEvent`. – Kuba hasn't forgotten Monica Mar 20 '14 at 12:59
  • 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](https://qt.gitorious.org/qt/qt-gberg/source/8af3500125a03a54dddb8c46ee709b7b8d257f27:src/gui/widgets/qlineedit.cpp#L2341) you can see the default behavior. – thuga Mar 20 '14 at 13:00
  • @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. – Joey Mar 20 '14 at 13:05
  • @Joey: I was hoping you'd discover Thuga's way :) – Kuba hasn't forgotten Monica Mar 20 '14 at 13:41
  • Side note: It is an error not to call the base class's eventFilter. Qt's classes are free to act as event filters. – Kuba hasn't forgotten Monica Mar 20 '14 at 13:43

3 Answers3

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 http://stackoverflow.com/q/22535469/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"
Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
  • 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. – Joey Mar 20 '14 at 15:39
  • @Joey: Documented now :) – Kuba hasn't forgotten Monica Mar 20 '14 at 16:04
  • 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. – Joey Mar 20 '14 at 16:27
  • @Joey 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. – Kuba hasn't forgotten Monica Mar 20 '14 at 16:32
  • @Joey 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). – Kuba hasn't forgotten Monica Mar 20 '14 at 16:37
  • 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 :-) – Joey Mar 20 '14 at 16:39
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/50153/discussion-between--and-kuba-ober) – Joey Mar 20 '14 at 16:39
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);
}
Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
thuga
  • 12,601
  • 42
  • 52
  • 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). – Joey Mar 20 '14 at 13:49
  • Interestingly this does not work with an event filter, only in a subclass. – Joey Mar 20 '14 at 14:21
  • @Joey 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); });` – Kuba hasn't forgotten Monica Mar 20 '14 at 14:31
  • @Joey 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*. – Kuba hasn't forgotten Monica Mar 20 '14 at 14:32
0

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

neau
  • 317
  • 1
  • 12
  • 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. – Joey Sep 10 '15 at 11:40
  • But I don't think its a wrong answer either that you down voted it. – neau Sep 15 '15 at 09:04