From 78ef6f0d04578b4bfaea35c25a92b09c607d228a Mon Sep 17 00:00:00 2001 From: Daniel Wilches Date: Sat, 10 Mar 2018 19:31:43 -0800 Subject: [PATCH 1/2] Grey out Apply button when there are no changes Resolves #1313 What this commit does: * Whenever the Apply button is pressed, and if the save was successful, then the Apply button is disabled. * Each subwidget used by EditEntryWidget has now a signal called `widgetUpdated` that is emitted when the widgets' internal content changes. The EditEntryWidget subscribes to that signal to know when to enable the Apply button (by calling `entryUpdated()`). * There are some views that are not isolated in their own widgets (`m_advancedUi`, for example) so in those cases I invoked `entryUpdated()` directly whenever I detected an update: * some updates occur directly in a Qt widget like when editing the text of a QLineItem, so in that case I connected the widget's signals directly to the `entryUpdated()` slot. * some updates occur in EditEntryWidget, so in those cases the invocation to `entryUpdated()` is made as soon as the change is detected (for example when the user has confirmed an action in a dialog). A known problem: there are some situations when the Apply button will get enabled even if there are no changes, this is because the app changes the value of a field by itself so it's considered an update (for example, clicking on the "Reveal" button changes the text shown in a text field). The solution to this can be a bit complicated: disabling temporarily the `entryUpdated()` whenever the app is going to do an action with such side-effects. So I preferred to let the Apply button get enabled in those cases. --- src/gui/EditWidget.cpp | 8 +++ src/gui/EditWidget.h | 1 + src/gui/EditWidgetIcons.cpp | 11 ++++ src/gui/EditWidgetIcons.h | 1 + src/gui/entry/EditEntryWidget.cpp | 78 +++++++++++++++++++++++- src/gui/entry/EditEntryWidget.h | 2 + src/gui/entry/EntryAttachmentsWidget.cpp | 2 + src/gui/entry/EntryAttachmentsWidget.h | 1 + 8 files changed, 102 insertions(+), 2 deletions(-) diff --git a/src/gui/EditWidget.cpp b/src/gui/EditWidget.cpp index 65c6306e1..5f05a428a 100644 --- a/src/gui/EditWidget.cpp +++ b/src/gui/EditWidget.cpp @@ -117,6 +117,14 @@ bool EditWidget::readOnly() const return m_readOnly; } +void EditWidget::enableApplyButton(bool enabled) +{ + QPushButton* applyButton = m_ui->buttonBox->button(QDialogButtonBox::Apply); + if (applyButton) { + applyButton->setEnabled(enabled); + } +} + void EditWidget::showMessage(const QString& text, MessageWidget::MessageType type) { m_ui->messageWidget->setCloseButtonVisible(false); diff --git a/src/gui/EditWidget.h b/src/gui/EditWidget.h index 442365b96..38179a773 100644 --- a/src/gui/EditWidget.h +++ b/src/gui/EditWidget.h @@ -47,6 +47,7 @@ public: QLabel* headlineLabel(); void setReadOnly(bool readOnly); bool readOnly() const; + void enableApplyButton(bool enabled); signals: void apply(); diff --git a/src/gui/EditWidgetIcons.cpp b/src/gui/EditWidgetIcons.cpp index a70e0dca2..e02ad952f 100644 --- a/src/gui/EditWidgetIcons.cpp +++ b/src/gui/EditWidgetIcons.cpp @@ -66,6 +66,13 @@ EditWidgetIcons::EditWidgetIcons(QWidget* parent) connect(m_ui->deleteButton, SIGNAL(clicked()), SLOT(removeCustomIcon())); connect(m_ui->faviconButton, SIGNAL(clicked()), SLOT(downloadFavicon())); + connect(m_ui->defaultIconsRadio, SIGNAL(toggled(bool)), this, SIGNAL(widgetUpdated())); + connect(m_ui->defaultIconsRadio, SIGNAL(toggled(bool)), this, SIGNAL(widgetUpdated())); + connect(m_ui->defaultIconsView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), + this, SIGNAL(widgetUpdated())); + connect(m_ui->customIconsView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), + this, SIGNAL(widgetUpdated())); + m_ui->faviconButton->setVisible(false); } @@ -268,6 +275,8 @@ void EditWidgetIcons::addCustomIcon(const QImage& icon) updateRadioButtonCustomIcons(); QModelIndex index = m_customIconModel->indexFromUuid(uuid); m_ui->customIconsView->setCurrentIndex(index); + + emit widgetUpdated(); } } @@ -347,6 +356,8 @@ void EditWidgetIcons::removeCustomIcon() } else { m_ui->defaultIconsView->setCurrentIndex(m_defaultIconModel->index(Group::DefaultIconNumber)); } + + emit widgetUpdated(); } } } diff --git a/src/gui/EditWidgetIcons.h b/src/gui/EditWidgetIcons.h index 7b5edf80c..0f875a8a3 100644 --- a/src/gui/EditWidgetIcons.h +++ b/src/gui/EditWidgetIcons.h @@ -62,6 +62,7 @@ public slots: signals: void messageEditEntry(QString, MessageWidget::MessageType); void messageEditEntryDismiss(); + void widgetUpdated(); private slots: void downloadFavicon(); diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 95ce7ee6a..b29012e89 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -95,6 +95,7 @@ EditEntryWidget::EditEntryWidget(QWidget* parent) #endif setupProperties(); setupHistory(); + setupEntryUpdate(); connect(this, SIGNAL(accepted()), SLOT(acceptEntry())); connect(this, SIGNAL(rejected()), SLOT(cancel())); @@ -227,6 +228,59 @@ void EditEntryWidget::setupHistory() connect(m_historyUi->deleteAllButton, SIGNAL(clicked()), SLOT(deleteAllHistoryEntries())); } +void EditEntryWidget::setupEntryUpdate() +{ + // Entry tab + connect(m_mainUi->titleEdit, SIGNAL(textChanged(const QString&)), this, SLOT(setUnsavedChanges())); + connect(m_mainUi->usernameEdit, SIGNAL(textChanged(const QString&)), this, SLOT(setUnsavedChanges())); + connect(m_mainUi->passwordEdit, SIGNAL(textChanged(const QString&)), this, SLOT(setUnsavedChanges())); + connect(m_mainUi->passwordRepeatEdit, SIGNAL(textChanged(const QString&)), this, SLOT(setUnsavedChanges())); + connect(m_mainUi->urlEdit, SIGNAL(textChanged(const QString&)), this, SLOT(setUnsavedChanges())); + connect(m_mainUi->expireCheck, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges())); + connect(m_mainUi->notesEnabled, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges())); + connect(m_mainUi->expireDatePicker, SIGNAL(dateTimeChanged(const QDateTime&)), this, SLOT(setUnsavedChanges())); + connect(m_mainUi->notesEdit, SIGNAL(textChanged()), this, SLOT(setUnsavedChanges())); + + // Advanced tab + connect(m_advancedUi->attributesEdit, SIGNAL(textChanged()), this, SLOT(setUnsavedChanges())); + connect(m_advancedUi->protectAttributeButton, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges())); + connect(m_advancedUi->fgColorCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges())); + connect(m_advancedUi->bgColorCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges())); + connect(m_advancedUi->attachmentsWidget, SIGNAL(widgetUpdated()), this, SLOT(setUnsavedChanges())); + + // Icon tab + connect(m_iconsWidget, SIGNAL(widgetUpdated()), this, SLOT(setUnsavedChanges())); + + // Auto-Type tab + connect(m_autoTypeUi->enableButton, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges())); + connect(m_autoTypeUi->customWindowSequenceButton, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges())); + connect(m_autoTypeUi->inheritSequenceButton, SIGNAL(toggled(bool)), this, SLOT(setUnsavedChanges())); + connect(m_autoTypeUi->customSequenceButton, SIGNAL(toggled(bool)), this, SLOT(setUnsavedChanges())); + connect(m_autoTypeUi->windowSequenceEdit, SIGNAL(textChanged(const QString&)), this, SLOT(setUnsavedChanges())); + connect(m_autoTypeUi->sequenceEdit, SIGNAL(textChanged(const QString&)), this, SLOT(setUnsavedChanges())); + connect(m_autoTypeUi->windowTitleCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(setUnsavedChanges())); + connect(m_autoTypeUi->windowTitleCombo, SIGNAL(editTextChanged(const QString&)), this, SLOT(setUnsavedChanges())); + + // Properties and History tabs don't need extra connections + +#ifdef WITH_XC_SSHAGENT + // SSH Agent tab + if (config()->get("SSHAgent", false).toBool()) { + connect(m_sshAgentUi->attachmentRadioButton, SIGNAL(toggled(bool)), this, SLOT(setUnsavedChanges())); + connect(m_sshAgentUi->externalFileRadioButton, SIGNAL(toggled(bool)), this, SLOT(setUnsavedChanges())); + connect(m_sshAgentUi->attachmentComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(setUnsavedChanges())); + connect(m_sshAgentUi->attachmentComboBox, SIGNAL(editTextChanged(const QString&)), this, SLOT(setUnsavedChanges())); + connect(m_sshAgentUi->externalFileEdit, SIGNAL(textChanged(const QString&)), this, SLOT(setUnsavedChanges())); + connect(m_sshAgentUi->publicKeyEdit, SIGNAL(textChanged()), this, SLOT(setUnsavedChanges())); + connect(m_sshAgentUi->addKeyToAgentCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges())); + connect(m_sshAgentUi->removeKeyFromAgentCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges())); + connect(m_sshAgentUi->requireUserConfirmationCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges())); + connect(m_sshAgentUi->lifetimeCheckBox, SIGNAL(stateChanged(int)), this, SLOT(setUnsavedChanges())); + connect(m_sshAgentUi->lifetimeSpinBox, SIGNAL(valueChanged(int)), this, SLOT(setUnsavedChanges())); + } +#endif +} + void EditEntryWidget::emitHistoryEntryActivated(const QModelIndex& index) { Q_ASSERT(!m_history); @@ -581,7 +635,6 @@ void EditEntryWidget::loadEntry(Entry* entry, bool create, bool history, const Q m_database = database; m_create = create; m_history = history; - m_saved = false; if (history) { setHeadline(QString("%1 > %2").arg(parentName, tr("Entry history"))); @@ -601,6 +654,9 @@ void EditEntryWidget::loadEntry(Entry* entry, bool create, bool history, const Q setCurrentPage(0); setPageHidden(m_historyWidget, m_history || m_entry->historyItems().count() < 1); + + // Force the user to Save/Apply/Discard new entries + setUnsavedChanges(m_create); } void EditEntryWidget::setForms(const Entry* entry, bool restore) @@ -780,7 +836,7 @@ bool EditEntryWidget::commitEntry() } updateEntryData(m_entry); - m_saved = true; + setUnsavedChanges(false); if (!m_create) { m_entry->endUpdate(); @@ -940,6 +996,8 @@ void EditEntryWidget::insertAttribute() m_advancedUi->attributesView->setCurrentIndex(index); m_advancedUi->attributesView->edit(index); + + setUnsavedChanges(true); } void EditEntryWidget::editCurrentAttribute() @@ -950,6 +1008,7 @@ void EditEntryWidget::editCurrentAttribute() if (index.isValid()) { m_advancedUi->attributesView->edit(index); + setUnsavedChanges(true); } } @@ -963,6 +1022,7 @@ void EditEntryWidget::removeCurrentAttribute() if (MessageBox::question(this, tr("Confirm Remove"), tr("Are you sure you want to remove this attribute?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { m_entryAttributes->remove(m_attributesModel->keyByIndex(index)); + setUnsavedChanges(true); } } } @@ -1047,9 +1107,11 @@ void EditEntryWidget::revealCurrentAttribute() if (! m_advancedUi->attributesEdit->isEnabled()) { QModelIndex index = m_advancedUi->attributesView->currentIndex(); if (index.isValid()) { + bool oldBlockSignals = m_advancedUi->attributesEdit->blockSignals(true); QString key = m_attributesModel->keyByIndex(index); m_advancedUi->attributesEdit->setPlainText(m_entryAttributes->value(key)); m_advancedUi->attributesEdit->setEnabled(true); + m_advancedUi->attributesEdit->blockSignals(oldBlockSignals); } } } @@ -1083,6 +1145,7 @@ void EditEntryWidget::insertAutoTypeAssoc() m_autoTypeUi->assocView->setCurrentIndex(newIndex); loadCurrentAssoc(newIndex); m_autoTypeUi->windowTitleCombo->setFocus(); + setUnsavedChanges(true); } void EditEntryWidget::removeAutoTypeAssoc() @@ -1091,6 +1154,7 @@ void EditEntryWidget::removeAutoTypeAssoc() if (currentIndex.isValid()) { m_autoTypeAssoc->remove(currentIndex.row()); + setUnsavedChanges(true); } } @@ -1153,6 +1217,7 @@ void EditEntryWidget::restoreHistoryEntry() QModelIndex index = m_sortModel->mapToSource(m_historyUi->historyView->currentIndex()); if (index.isValid()) { setForms(m_historyModel->entryFromIndex(index), true); + setUnsavedChanges(true); } } @@ -1166,6 +1231,7 @@ void EditEntryWidget::deleteHistoryEntry() } else { m_historyUi->deleteAllButton->setEnabled(false); } + setUnsavedChanges(true); } } @@ -1178,6 +1244,7 @@ void EditEntryWidget::deleteAllHistoryEntries() else { m_historyUi->deleteAllButton->setEnabled(false); } + setUnsavedChanges(true); } QMenu* EditEntryWidget::createPresetsMenu() @@ -1229,5 +1296,12 @@ void EditEntryWidget::pickColor() QColor newColor = colorDialog.getColor(oldColor); if (newColor.isValid()) { setupColorButton(isForeground, newColor); + setUnsavedChanges(true); } } + +void EditEntryWidget::setUnsavedChanges(bool hasUnsaved) +{ + m_saved = !hasUnsaved; + enableApplyButton(hasUnsaved); +} diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h index 9b2a919c6..863d6505b 100644 --- a/src/gui/entry/EditEntryWidget.h +++ b/src/gui/entry/EditEntryWidget.h @@ -100,6 +100,7 @@ private slots: void useExpiryPreset(QAction* action); void toggleHideNotes(bool visible); void pickColor(); + void setUnsavedChanges(bool hasUnsaved = true); #ifdef WITH_XC_SSHAGENT void updateSSHAgent(); void updateSSHAgentAttachment(); @@ -122,6 +123,7 @@ private: #endif void setupProperties(); void setupHistory(); + void setupEntryUpdate(); void setupColorButton(bool foreground, const QColor& color); bool passwordsEqual(); diff --git a/src/gui/entry/EntryAttachmentsWidget.cpp b/src/gui/entry/EntryAttachmentsWidget.cpp index 78b6d741b..d420d9d65 100644 --- a/src/gui/entry/EntryAttachmentsWidget.cpp +++ b/src/gui/entry/EntryAttachmentsWidget.cpp @@ -147,6 +147,7 @@ void EntryAttachmentsWidget::insertAttachments() if (!insertAttachments(filenames, errorMessage)) { errorOccurred(errorMessage); } + emit widgetUpdated(); } void EntryAttachmentsWidget::removeSelectedAttachments() @@ -170,6 +171,7 @@ void EntryAttachmentsWidget::removeSelectedAttachments() keys.append(m_attachmentsModel->keyByIndex(index)); } m_entryAttachments->remove(keys); + emit widgetUpdated(); } } diff --git a/src/gui/entry/EntryAttachmentsWidget.h b/src/gui/entry/EntryAttachmentsWidget.h index 5fd19415e..590060ae1 100644 --- a/src/gui/entry/EntryAttachmentsWidget.h +++ b/src/gui/entry/EntryAttachmentsWidget.h @@ -39,6 +39,7 @@ signals: void errorOccurred(const QString& error); void readOnlyChanged(bool readOnly); void buttonsVisibleChanged(bool isButtonsVisible); + void widgetUpdated(); private slots: void insertAttachments(); From 240939ce3b83267cbb0e203b32ead6d45d0919a4 Mon Sep 17 00:00:00 2001 From: Daniel Wilches Date: Sat, 17 Mar 2018 14:31:15 -0700 Subject: [PATCH 2/2] Request confirmation to discard unsaved changes Solves #1181 --- src/gui/entry/EditEntryWidget.cpp | 13 +++++++++++++ tests/gui/TestGui.cpp | 1 + 2 files changed, 14 insertions(+) diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index b29012e89..8163a648c 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -927,6 +927,19 @@ void EditEntryWidget::cancel() m_entry->setIcon(Entry::DefaultIconNumber); } + if (!m_saved) { + auto result = MessageBox::question(this, QString(), tr("Entry has unsaved changes"), + QMessageBox::Cancel | QMessageBox::Save | QMessageBox::Discard, + QMessageBox::Cancel); + if (result == QMessageBox::Cancel) { + return; + } + if (result == QMessageBox::Save) { + commitEntry(); + m_saved = true; + } + } + clear(); emit editFinished(m_saved); diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 54203c284..c736ea485 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -492,6 +492,7 @@ void TestGui::testAddEntry() // Add entry "something 5" but click cancel button (does NOT add entry) QTest::mouseClick(entryNewWidget, Qt::LeftButton); QTest::keyClicks(titleEdit, "something 5"); + MessageBox::setNextAnswer(QMessageBox::Discard); QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Cancel), Qt::LeftButton); QApplication::processEvents();