This blog entry has been written due to a discussion that took place on KDE’s source code review system lately.

The use case is as follows: A user types in a URL into a text widget and the application shall check if the URL exists and display a possibly available favicon on a button next to the edit widget. In case the URL is not available or is an invalid URL, the button shall be disabled.

Since these checks could take some time, I did not want to do this check on every keystroke and decided to postpone it to 200ms after the last keystroke by the user was detected.

The solution to the problem in our beloved Qt5/KF5 land is easy. Here’s how: we have two players in this game. A QTextEdit and a QTimer. We forget the QTooButton because it is not relevant to this. One could setup the following connections

  1. QTextEdit::textChanged() -> QTimer::start()
  2. QTimer::triggered() -> slot_that_starts_KIO::FavIconRequestJob()
  3. KIO::FavIconRequestJob::result() -> slot_that_examines_KIO::results()

And code in slot_that_examines_KIO::results() will take care of enabling and disabling the QToolButton.

In practice it is a bit more complex, so that the QTextEdit does not start the timer directly as shown above, but is connected to another slot in the application which does some checks to make sure that triggering the timer only happens, when the text entered does not contain a protocol (because that shall be fixed to https in this case). Also, slot_that_starts_KIO::FavIconRequestJob() shall not be started multiple times in parallel.

Here’s the (somewhat simplified) code so far for the object KNewBankDlg. It has a private part which is pointed to by d (you sure know about the d-pointer paradigm): This is where all the objects are maintained.

KNewBankDlg::KNewBankDlg()
{
  d->m_iconLoadTimer.setSingleShot(true);
  d->m_iconLoadTimer.setInterval(200);

  connect(d->ui->urlEdit, &QLineEdit::textChanged, this, &KNewBankDlg::slotUrlChanged);
  connect(&d->m_iconLoadTimer, &QTimer::timeout, this, &KNewBankDlg::slotLoadIcon);
}

void KNewBankDlg::slotUrlChanged(const QString& newUrl)
{
  Q_D(KNewBankDlg);
  // remove a possible leading protocol since we only provide https for now
  QRegularExpression protocol(QStringLiteral("^[a-zA-Z]+://(?<url>.*)"),
                              QRegularExpression::CaseInsensitiveOption);
  QRegularExpressionMatch matcher = protocol.match(newUrl);
  if (matcher.hasMatch()) {
    d->ui->urlEdit->setText(matcher.captured(QStringLiteral("url")));
  }
  d->m_iconLoadTimer.start();
}

void KNewBankDlg::slotLoadIcon()
{
  Q_D(KNewBankDlg);

  // if currently a check is running, retry later
  if (d->m_favIconJob) {
    d->m_iconLoadTimer.start();
    return;
  }
  // Here the actual job is created and started. url is created
  // here as well, but the details are not so important
  d->m_favIconJob = KIO::FavIconRequestJob(url);
  connect(d->m_favIconJob, &KIO::FavIconRequestJob::result,
          this, &KNewBankDlg::slotIconLoaded);
}

void KNewBankDlg::slotIconLoaded(KJob* job)
{
  // evaluate the result of the job here
}

This code is working perfectly but one of the reviewers asked, why I don’t use QTimer::singleShot() to solve the problem. The explanation is easy: when the above logic calls QTimer::start() it should simply restart the running timer. Using QTimer::singleShot() on the other hand will create a new timer each time and start it. Any previously started timer remains running. This in fact will lead to multiple calss of slotLoadIcon() which is what we actually want to avoid.

You can argue, well but in case a job is still running then it does not start a second job. Yes, that is true, but assume the following: The user enters the URL with a delay of 180ms between each text change of the URL. The job to retrieve the icon from the net is assumed to take 150ms. In this case, the job is done by the time the slot is called and thus started again. So for 20 changes of the URL the job is performed 20 times. Using a QTimer object in the form shown above will only call the job once, and that is 200ms after the last change of the URL.

Now you know the difference between

  QTimer  myTimer;
  myTimer.setSingleShot(true);
  myTimer.start();

and

  QTimer::singleShot();
Don’t shoot yourself in the foot when using QTimer::singleShot