closeEvent and focus
-
I have a QDialog with OK, APPLY, and CANCEL buttons, and a bunch of fields.. When the user presses OK, or APPLY, focus leaves the field being edited. This causes the QLineEdit::editingFinished signal to be generated and a method is called which saves the data that the user was typing.
I'm trying to make it so that QDialog::closeEvent asks the user if they want to save the data before exiting. I overrode the close Event, display the confirmation dialog and perform the necessary action. That all works fine.
The problem is that the editingFinished signal isn;t generated so the data in the QLineEdit isn't saved.
I added a call to clearFocus at the beginning of closeEvent, and now the editingFinished signal is generated, but not until closeEvent completes. That causes the closeEvent method not to work properly. closeEvent needs the data in the field to be saved before checking to see if the data has changed. I tried adding an event loop and a call to processEvents to process the clearFocus event, but that didn't make a difference.
What I would like is a way to make it so that clicking on the x in the upper right and triggering the closeEvent would automatically change the focus, just like clicking on any other button. Failing that, a way to get the clearFocus event to be handled before continuing on to the rest of the code in closeEvent.
Any suggestions would be helpful.
-
@james-b-s Not a direct answer, but aren't you complicating it too much? If the data isn't too big just copy it and don't fiddle with focuses and close event and just edit the copy and apply or discard based on the dialog result, i.e.
auto data = copyExistingData(); MyDialog dialog(data); if (dialog.exec() == QDialog::Accepted) { applyModifiedData(data); }
-
@Chris-Kawa There is a lot of data, of different types, and an indepth class hierarchy, and multiple dialogs, with preexisting code that I don't quite understand. I believe that the one dialog that I am focusing on has 18 fields and there are about a dozen dialogs. I've got my code in a base class so that it is (mostly) implemented in one spot.
-
@james-b-s said in closeEvent and focus:
@jeremy_k The user clicks the x in the upper right. Is there somewhere other than closeEvent that I can detect that he did that?
class Widget : public QWidget { bool m_allowExit = false; public: void closeEvent(QCloseEvent *event) override { if (!m_allowExit) { QMessageBox *dialog = new QMessageBox(QMessageBox::NoIcon, "Click ok to close", "Click ok to close", QMessageBox::Ok); QObject::connect(dialog, &QMessageBox::buttonClicked, [&]() { m_allowExit = true; close(); }); dialog->show(); } event->setAccepted(m_allowExit); } };
-
@jeremy_k I don't see anything in that code that detects whether something has actually changed in the dialog. The problem is not the displaying of the message. The problem is that I don't want the message to be displayed if something has changed.
There are two copies of the data; the original data and the data currently on the screen. I have code that correctly compares the two. The problem comes about from fields that are in the middle of being edited. When the user presses the OK or APPLY button, a focus out event occurs. Existing code then causes the data in the field to be edited to be committed as if the use had pressed the enter key. When the user presses the x, the focus out does not occur. When I compare the original copy of the data to the copy of data on the screen, the data in the field being edited has not been committed and is lost. I need that focus out event to occur before the code that I placed in closeEvent compares the old and new values.
I thought putting a call to clearFocus would do it. It does cause a focus out event to occur, but not until closeEvent is done. I tried added an event loop to process events, but the focusOut still occurs after closeEvent completes.
I'm thinking that if I have closeEvent call clearFocus and then emit another signal that triggers the rest of the code, I can get QT to give me the order of events that I need, but that seems hacky. I'm hoping that there is a better way.
-
@james-b-s said in closeEvent and focus:
I'm thinking that if I have closeEvent call clearFocus and then emit another signal that triggers the rest of the code, I can get QT to give me the order of events that I need, but that seems hacky. I'm hoping that there is a better way.
Given your explanation of what is happening (I have not verified). I was thinking of answering this before I saw this as your last paragraph. You are saying the
closeEvent
event comes too early, it needs to be delayed till after the focus clear or whatever causes your other signals to be processed. So:- Define a new signal of your own, for
hadCloseEvent()
. - In
closeEvent()
, either have connected to that as queued signal and emit it, or set a single shotQTimer
to emit it after a minimal (0?) delay.. - Do your
clearFocus()
or whatever is required. - From this
closeEvent()
reject the close event unconditionally. - Handle the "delayed" signal in a slot which asks the user whether to quit/save changes/whatever. If user says "yes" then
close()
the window. - If that causes
cluseEvent()
to be called again, you will have to retain some state so you can recognise this is from yourhadCloseEvent()
and should not do the same code a second time.
I agree this sounds "not clean". But Qt handles events differently from signals, and I can see why there is a problem here. I don't know why someone else has not encountered your situation.
- Define a new signal of your own, for
-
To me it sounds like what they should do is
- Populate the UI elements from the data object when the dialog is opened
- Retain the state in the UI and not flush any changes to the data object on any "edit/chaged" signal.
- Handle dialog closeEvent
3.1 In close event compare the current data object state to the UI state
3.2 When changes are detected consult the user about quitting without changes or whatever
3.3 Sync the changes from the UI to the data object
On a related note it's crazy how many ways there are to close a dialog and how such a simple thing has been been made super complicated.
- User clicks on the window decoration's close button
- User hits some key combo such as Alt+F4 or Ctrl+W
- User clicks on a button explicitly in the dialog
- User hits enter and some button has "autoDefault = true" (and QtDesigner often lies about this too)
When a system is so complicated that simple things become difficult then truly difficult things become impossible.
-
@james-b-s said in closeEvent and focus:
Existing code then causes the data in the field to be edited to be committed as if the use had pressed the enter key. When the user presses the x, the focus out does not occur.
So the whole problem in short is, that you dont get the
editingFinished
signal if the user aborts and closes the dialog while editing?!
Everything works if you get the signal?!It's stated (you prob. know that already):
This signal is emitted when the Return or Enter key is pressed, or if the line edit loses focus and its contents have changed since the last time this signal was emitted.
All you need is some sort of "dirty data flag" for your dialog.
Wouldn't it be enough to save/sync new data (considered as "changed" as soon as one widget receives keyboard input or any widget (sliders, comboBoxes, etc) notice any actions?
Why you want to rely oneditingFinished
which is only emitted when pressing Enter or the widget "correctly" losing focus?! I assumeAlt + F4
'ing your app would also "hack" your current logic to save/sync data.
You could look for that into other projects how "dirty data" is detected and handled there and do the same or similar in yourcloseEvent
or even before.
QTextDocument
for example, hassetModified
which triggers the dirty flag. You can display it, by adding (IIRC)[ * ]
to your saveFile name. (there are plenty of topics dealing with this "dirty data" stuff here, but unfortunately I cant find the one I was thinking of)... -
@james-b-s said in closeEvent and focus:
@jeremy_k I don't see anything in that code that detects whether something has actually changed in the dialog. The problem is not the displaying of the message. The problem is that I don't want the message to be displayed if something has changed.
The point of the code presented is to move complex handling out of closeEvent(). Schedule it for processing later, and return to the main event loop.
As far as comparing the data, do that when the input changes, not when the dialog is about to close. At close time, it can be as simple as checking the value of a bool.
-
@jeremy_k said in closeEvent and focus:
do that when the input changes
The point is that this, or changed signals, happen for him when a widget loses focus. Which is a legitimate event for this (e.g.
editingFinished
will presumably require/fire on it). But he says focus loss/change does not happen on clicking the "X" close window button (it probably ought to for this case, but apparently not per his report). The input has not changed at the timecloseEvent()
is called. -
@JonB said in closeEvent and focus:
@jeremy_k said in closeEvent and focus:
do that when the input changes
The point is that this, or changed signals, happen for him when a widget loses focus. Which is a legitimate event for this (e.g.
editingFinished
will presumably require/fire on it). But he says focus loss/change does not happen on clicking the "X" close window button (it probably ought to for this case, but apparently not per his report). The input has not changed at the timecloseEvent()
is called.I am severely confused. OP said:
I believe that the one dialog that I am focusing on has 18 fields and there are about a dozen dialogs
While it is possible that a widget would have 18 separate fields (item views for example), people usually implement each field as a widget. Use the appropriate changed signal (not editing finished) to track when a field changes, and store whether that individual field differs from its saved version.
-
@jeremy_k
I do not disagree with you. I just accept this is the OP's question/situationThe problem is that the editingFinished signal isn;t generated so the data in the QLineEdit isn't saved.
If this is the case the OP has a legitimate difficulty.
@james-b-s
I would be interested to know how a QDataWidgetMapper Class behaves in your situation. Set up a mapping, does it see data changed before yourcloseEvent
? -
@james-b-s Starting an event loop in closeEvent won't work. You're already in an event handler and you need to get back from it for the dialog to function properly.
You can do what @JonB said - delay closing until you process your focus loss. For example use a QDialog derived class like this:
class MyDialog : public QDialog { bool allowClose = false; public: using QDialog::QDialog; void closeEvent(QCloseEvent* evt) override { if (!allowClose) { evt->ignore(); // don't close and let event loop handle focus loss allowClose = true; // let it close next time setFocus(Qt::OtherFocusReason); // switch focus to something else, e.g. this or cancel button QTimer::singleShot(0, this, &MyDialog::close); // schedule another close event } else { askUserToSaveStuff(); QDialog::closeEvent(evt); } } };