zar
zar

Reputation: 12227

How to abort quiting dialog with QDialogButtonBox handler?

I have QDialogButtonBox buttons on my dialog which are Ok and Cancel button pair. I have implemented accepted() signal to process when 'Ok' button is pressed but I want to abort quitting dialog if the directory path is invalid.

void SettingsDialog::on_buttonBox_accepted()
{
    QDir path( ui->lineEditRootPath->text() );

    if ( path.exists( ui->lineEditRootPath->text() ) )
    {
        QSettings settings; // save settings to registry
        settings.setValue(ROOT_PATH, ui->lineEditRootPath->text() );
    }
    else
    {
        // abort cancelling the dialog here
    }
}

Can the dialog quitting be abort from this handler? Do I have to implement the above code in some other signal? Do I have to use simple button to accomplish this instead of QDialogButtonBox?

Upvotes: 3

Views: 2761

Answers (1)

This issue comes from the dialog template bundled with Qt Creator. When you create an empty dialog with buttons, the .ui file has connections between the button box and and the underlying dialog. They are created behind your back, so to speak:

Dialog-with-buttons template, viewed in Qt Creator in signal edit mode, with default signals shown.

So, there really is no problem, since the button box doesn't actually accept the dialog. You must accept the dialog, if you don't then the dialog stays open.

The simple fix is to remove the default connection(s).


Other Nitpicks

You should not use the QDir::exists(const QString &) overload - it won't work. You already provided the path to dir's constructor. Simply use exists().

Thus:

void SettingsDialog::on_buttonBox_accepted()
{
  QDir path(ui->lineEditRootPath->text());
  if (!path.exists()) return;
  QSettings settings; // save settings to registry
  settings.setValue(ROOT_PATH, ui->lineEditRootPath->text());
  accept(); // accepts the dialog, closing it
}

You could also use the static QFileInfo::exists:

void SettingsDialog::on_buttonBox_accepted()
{
  if (! QFileInfo.exists(ui->lineEditRootPath->text()) return;
  ...
}

Finally, it's probably a nice idea to provide some sort of feedback to the user when an input is invalid. In C++11, that's quite easy to do:

#include <QApplication>
#include <QFileInfo>
#include <QDialog>
#include <QDialogButtonBox>
#include <QLineEdit>
#include <QGridLayout>

int main(int argc, char *argv[])
{
   QApplication a(argc, argv);
   QDialog dialog;
   QLineEdit edit("/");
   QDialogButtonBox buttons(QDialogButtonBox::Ok | QDialogButtonBox::Close);
   QGridLayout layout(&dialog);
   layout.addWidget(&edit, 0, 0);
   layout.addWidget(&buttons, 1, 0);

   QObject::connect(&buttons, &QDialogButtonBox::accepted, [&]{
      if (!QFileInfo::exists(edit.text())) return;
      //...
      dialog.accept();
   });
   QObject::connect(&buttons, &QDialogButtonBox::rejected, [&]{ dialog.reject(); });

   QObject::connect(&edit, &QLineEdit::textChanged, [&](const QString&){
      if (QFileInfo::exists(edit.text()))
         edit.setStyleSheet("");
      else
         edit.setStyleSheet("* { background: red; }");
   });

   dialog.show();
   return a.exec();
}

After some testing, you've realized that the users have a bad tendency to enter paths that might be on disconnected network volumes. When you attempt to check if they exist, it blocks the GUI just so that the OS can politely tell you "umm, nope".

The solution is to perform the check in a worker thread, so that if it blocks, the UI will not be directly affected. If the worker thread blocks, the path editor background will turn yellow. If the path doesn't exist, the background will turn red and the OK button will be disabled.

One bit of code requires some explanation: QObject::connect(&checker, &Checker::exists, &app, [&](...){...}) connects the checker's signal to a lambda in the thread context of the application object. Since checker's signals are emitted in the checker's thread, without the context (&app), the code would be executed in the checker's thread. We definitely don't want that, the GUI changes must be executed in the main thread. The simplest way to do it is to pass one object we surely know lives in the main thread: the application instance. If you don't pass the proper context, e.g. QObject::connect(&checker, &Checker::exists, [&](...){...})), you'll get undefined behavior and a crash.

#include <QApplication>
#include <QFileInfo>
#include <QDialog>
#include <QDialogButtonBox>
#include <QLineEdit>
#include <QPushButton>
#include <QGridLayout>
#include <QThread>
#include <QTimer>

class Thread : public QThread {
   using QThread::run; // final
public:
   ~Thread() { quit(); wait(); }
};

class Checker : public QObject {
   Q_OBJECT
public:
   Q_SIGNAL void exists(bool, const QString & path);
   Q_SLOT void check(const QString & path) { emit exists(QFileInfo::exists(path), path); }
};

int main(int argc, char *argv[])
{
   bool pathExists = true;
   QApplication app(argc, argv);
   QDialog dialog;
   QLineEdit edit("/");
   QDialogButtonBox buttons(QDialogButtonBox::Ok | QDialogButtonBox::Close);
   QGridLayout layout(&dialog);
   layout.addWidget(&edit, 0, 0);
   layout.addWidget(&buttons, 1, 0);

   QTimer checkTimer;
   Checker checker;
   Thread checkerThread;
   checker.moveToThread(&checkerThread);
   checkerThread.start();
   checkTimer.setInterval(500);
   checkTimer.setSingleShot(true);

   QObject::connect(&buttons, &QDialogButtonBox::accepted, [&]{
      if (!pathExists) return;
      //...
      dialog.accept();
   });
   QObject::connect(&buttons, &QDialogButtonBox::rejected, [&]{ dialog.reject(); });

   QObject::connect(&edit, &QLineEdit::textChanged, &checker, &Checker::check);
   QObject::connect(&edit, &QLineEdit::textChanged, &checkTimer, static_cast<void (QTimer::*)()>(&QTimer::start));
   QObject::connect(&checkTimer, &QTimer::timeout, [&]{ edit.setStyleSheet("background: yellow"); });

   QObject::connect(&checker, &Checker::exists, &app, [&](bool ok, const QString & path){
      if (path != edit.text()) return; // stale result
      checkTimer.stop();
      edit.setStyleSheet(ok ? "" : "background: red");
      buttons.button(QDialogButtonBox::Ok)->setEnabled(ok);
      pathExists = ok;
   });

   dialog.show();
   return app.exec();
}

#include "main.moc"

Upvotes: 4

Related Questions