diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index 1e938c8fd..4c0d486b1 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -816,6 +816,10 @@ Ctrl+4 - Use Virtual Keyboard (Windows Only)</p> Disable for this site + + Undo + + BrowserEntrySaveDialog diff --git a/src/browser/BrowserAccessControlDialog.cpp b/src/browser/BrowserAccessControlDialog.cpp index 3fce10c5c..f3a29c993 100644 --- a/src/browser/BrowserAccessControlDialog.cpp +++ b/src/browser/BrowserAccessControlDialog.cpp @@ -1,6 +1,6 @@ /* + * Copyright (C) 2023 KeePassXC Team * Copyright (C) 2013 Francois Ferrand - * Copyright (C) 2022 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -21,12 +21,11 @@ #include #include "core/Entry.h" -#include +#include "gui/Icons.h" BrowserAccessControlDialog::BrowserAccessControlDialog(QWidget* parent) : QDialog(parent) , m_ui(new Ui::BrowserAccessControlDialog()) - , m_entriesAccepted(false) { setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); @@ -34,13 +33,22 @@ BrowserAccessControlDialog::BrowserAccessControlDialog(QWidget* parent) connect(m_ui->allowButton, SIGNAL(clicked()), SLOT(accept())); connect(m_ui->denyButton, SIGNAL(clicked()), SLOT(reject())); + connect(m_ui->itemsTable, SIGNAL(cellDoubleClicked(int, int)), this, SLOT(accept())); + connect(m_ui->itemsTable->selectionModel(), + SIGNAL(selectionChanged(QItemSelection, QItemSelection)), + this, + SLOT(selectionChanged())); + connect(m_ui->itemsTable, SIGNAL(acceptSelections()), SLOT(accept())); + connect(m_ui->itemsTable, SIGNAL(focusInWithoutSelections()), this, SLOT(selectionChanged())); } BrowserAccessControlDialog::~BrowserAccessControlDialog() { } -void BrowserAccessControlDialog::setItems(const QList& items, const QString& urlString, bool httpAuth) +void BrowserAccessControlDialog::setEntries(const QList& entriesToConfirm, + const QString& urlString, + bool httpAuth) { QUrl url(urlString); m_ui->siteLabel->setText(m_ui->siteLabel->text().arg( @@ -49,60 +57,114 @@ void BrowserAccessControlDialog::setItems(const QList& items, const QStr m_ui->rememberDecisionCheckBox->setVisible(!httpAuth); m_ui->rememberDecisionCheckBox->setChecked(false); - m_ui->itemsTable->setRowCount(items.count()); + m_ui->itemsTable->setRowCount(entriesToConfirm.count()); m_ui->itemsTable->setColumnCount(2); int row = 0; - for (const auto& entry : items) { - auto item = new QTableWidgetItem(); - item->setText(entry->title() + " - " + entry->username()); - item->setData(Qt::UserRole, row); - item->setCheckState(Qt::Checked); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - m_ui->itemsTable->setItem(row, 0, item); - - auto disableButton = new QPushButton(tr("Disable for this site")); - disableButton->setAutoDefault(false); - connect(disableButton, &QAbstractButton::pressed, [&, item] { - emit disableAccess(item); - m_ui->itemsTable->removeRow(item->row()); - if (m_ui->itemsTable->rowCount() == 0) { - reject(); - } - }); - m_ui->itemsTable->setCellWidget(row, 1, disableButton); + for (const auto& entry : entriesToConfirm) { + addEntryToList(entry, row); ++row; } m_ui->itemsTable->resizeColumnsToContents(); m_ui->itemsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + m_ui->itemsTable->selectAll(); m_ui->allowButton->setFocus(); } +void BrowserAccessControlDialog::addEntryToList(Entry* entry, int row) +{ + auto item = new QTableWidgetItem(); + item->setText(entry->title() + " - " + entry->username()); + item->setData(Qt::UserRole, row); + item->setFlags(item->flags() | Qt::ItemIsSelectable); + m_ui->itemsTable->setItem(row, 0, item); + + auto disableButton = new QPushButton(); + disableButton->setIcon(icons()->icon("entry-delete")); + disableButton->setToolTip(tr("Disable for this site")); + + connect(disableButton, &QAbstractButton::pressed, [&, item, disableButton] { + auto font = item->font(); + if (item->flags() == Qt::NoItemFlags) { + item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); + item->setSelected(true); + + font.setStrikeOut(false); + item->setFont(font); + + disableButton->setIcon(icons()->icon("entry-delete")); + disableButton->setToolTip(tr("Disable for this site")); + m_ui->rememberDecisionCheckBox->setEnabled(true); + } else { + item->setFlags(Qt::NoItemFlags); + item->setSelected(false); + + font.setStrikeOut(true); + item->setFont(font); + + disableButton->setIcon(icons()->icon("entry-restore")); + disableButton->setToolTip(tr("Undo")); + + // Disable Remember checkbox if all items are disabled + auto areAllDisabled = BrowserAccessControlDialog::areAllDisabled(); + m_ui->rememberDecisionCheckBox->setEnabled(!areAllDisabled); + } + }); + + m_ui->itemsTable->setCellWidget(row, 1, disableButton); +} + bool BrowserAccessControlDialog::remember() const { return m_ui->rememberDecisionCheckBox->isChecked(); } -QList BrowserAccessControlDialog::getSelectedEntries() const +QList BrowserAccessControlDialog::getEntries(SelectionType selectionType) const { QList selected; - for (int i = 0; i < m_ui->itemsTable->rowCount(); ++i) { - auto item = m_ui->itemsTable->item(i, 0); - if (item->checkState() == Qt::Checked) { + for (auto& item : getAllItems()) { + // Add to list depending on selection type and item status + if ((selectionType == SelectionType::Selected && item->isSelected()) + || (selectionType == SelectionType::NonSelected && !item->isSelected()) + || (selectionType == SelectionType::Disabled && item->flags() == Qt::NoItemFlags)) { selected.append(item); } } return selected; } -QList BrowserAccessControlDialog::getNonSelectedEntries() const +void BrowserAccessControlDialog::selectionChanged() { - QList notSelected; - for (int i = 0; i < m_ui->itemsTable->rowCount(); ++i) { - auto item = m_ui->itemsTable->item(i, 0); - if (item->checkState() != Qt::Checked) { - notSelected.append(item); + auto selectedRows = m_ui->itemsTable->selectionModel()->selectedRows(); + + m_ui->allowButton->setEnabled(!selectedRows.isEmpty()); + m_ui->allowButton->setDefault(!selectedRows.isEmpty()); + m_ui->allowButton->setAutoDefault(!selectedRows.isEmpty()); + + if (selectedRows.isEmpty()) { + m_ui->allowButton->clearFocus(); + m_ui->denyButton->setFocus(); + } +} + +bool BrowserAccessControlDialog::areAllDisabled() const +{ + auto areAllDisabled = true; + for (const auto& item : getAllItems()) { + if (item->flags() != Qt::NoItemFlags) { + areAllDisabled = false; } } - return notSelected; + + return areAllDisabled; +} + +QList BrowserAccessControlDialog::getAllItems() const +{ + QList items; + for (int i = 0; i < m_ui->itemsTable->rowCount(); ++i) { + auto item = m_ui->itemsTable->item(i, 0); + items.append(item); + } + return items; } diff --git a/src/browser/BrowserAccessControlDialog.h b/src/browser/BrowserAccessControlDialog.h index 946db16d9..3ecf5b506 100644 --- a/src/browser/BrowserAccessControlDialog.h +++ b/src/browser/BrowserAccessControlDialog.h @@ -1,6 +1,6 @@ /* + * Copyright (C) 2023 KeePassXC Team * Copyright (C) 2013 Francois Ferrand - * Copyright (C) 2022 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -29,6 +29,13 @@ namespace Ui class BrowserAccessControlDialog; } +enum SelectionType +{ + Selected, + NonSelected, + Disabled +}; + class BrowserAccessControlDialog : public QDialog { Q_OBJECT @@ -37,20 +44,24 @@ public: explicit BrowserAccessControlDialog(QWidget* parent = nullptr); ~BrowserAccessControlDialog() override; - void setItems(const QList& items, const QString& urlString, bool httpAuth); + void setEntries(const QList& entriesToConfirm, const QString& urlString, bool httpAuth); bool remember() const; - - QList getSelectedEntries() const; - QList getNonSelectedEntries() const; + QList getEntries(SelectionType selectionType) const; signals: void disableAccess(QTableWidgetItem* item); +private slots: + void selectionChanged(); + +private: + void addEntryToList(Entry* entry, int row); + bool areAllDisabled() const; + QList getAllItems() const; + private: QScopedPointer m_ui; QList m_entriesToConfirm; - QList m_allowedEntries; - bool m_entriesAccepted; }; #endif // KEEPASSXC_BROWSERACCESSCONTROLDIALOG_H diff --git a/src/browser/BrowserAccessControlDialog.ui b/src/browser/BrowserAccessControlDialog.ui index 4224c1633..63f264311 100755 --- a/src/browser/BrowserAccessControlDialog.ui +++ b/src/browser/BrowserAccessControlDialog.ui @@ -31,7 +31,7 @@ - + QAbstractItemView::NoEditTriggers @@ -39,7 +39,10 @@ false - QAbstractItemView::NoSelection + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectRows false @@ -110,6 +113,19 @@ + + rememberDecisionCheckBox + allowButton + denyButton + + + + CustomTableWidget + QTableWidget +
browser/CustomTableWidget.h
+ 1 +
+
diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index 60412b5a3..133827dcf 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -431,25 +431,48 @@ QList BrowserService::confirmEntries(QList& entriesToConfirm, denyEntry(entry, siteHost, formUrl, entryParameters.realm); }); - accessControlDialog.setItems(entriesToConfirm, entryParameters.siteUrl, httpAuth); + accessControlDialog.setEntries(entriesToConfirm, entryParameters.siteUrl, httpAuth); QList allowedEntries; auto ret = accessControlDialog.exec(); - for (auto item : accessControlDialog.getSelectedEntries()) { - auto entry = entriesToConfirm[item->row()]; - if (accessControlDialog.remember()) { - if (ret == QDialog::Accepted) { + auto remember = accessControlDialog.remember(); + + // All are denied + if (ret == QDialog::Rejected && remember) { + for (auto& entry : entriesToConfirm) { + denyEntry(entry, siteHost, formUrl, entryParameters.realm); + } + } + + // Some/all are accepted + if (ret == QDialog::Accepted) { + auto selectedEntries = accessControlDialog.getEntries(SelectionType::Selected); + for (auto& item : selectedEntries) { + auto entry = entriesToConfirm[item->row()]; + allowedEntries.append(entry); + + if (remember) { allowEntry(entry, siteHost, formUrl, entryParameters.realm); - } else { - denyEntry(entry, siteHost, formUrl, entryParameters.realm); } } - if (ret == QDialog::Accepted) { - allowedEntries.append(entry); + // Remembered non-selected entries must be denied + if (remember) { + auto nonSelectedEntries = accessControlDialog.getEntries(SelectionType::NonSelected); + for (auto& item : nonSelectedEntries) { + auto entry = entriesToConfirm[item->row()]; + denyEntry(entry, siteHost, formUrl, entryParameters.realm); + } } } + // Handle disabled entries (returned Accept/Reject status does not matter) + auto disabledEntries = accessControlDialog.getEntries(SelectionType::Disabled); + for (auto& item : disabledEntries) { + auto entry = entriesToConfirm[item->row()]; + denyEntry(entry, siteHost, formUrl, entryParameters.realm); + } + // Re-hide the application if it wasn't visible before hideWindow(); m_dialogActive = false; diff --git a/src/browser/CMakeLists.txt b/src/browser/CMakeLists.txt index 2c344d31b..3ec5655ff 100755 --- a/src/browser/CMakeLists.txt +++ b/src/browser/CMakeLists.txt @@ -28,7 +28,9 @@ if(WITH_XC_BROWSER) BrowserService.cpp BrowserSettings.cpp BrowserShared.cpp - NativeMessageInstaller.cpp) + CustomTableWidget.cpp + NativeMessageInstaller.cpp + ) if(WITH_XC_BROWSER_PASSKEYS) list(APPEND keepassxcbrowser_SOURCES diff --git a/src/browser/CustomTableWidget.cpp b/src/browser/CustomTableWidget.cpp new file mode 100644 index 000000000..29009f4d5 --- /dev/null +++ b/src/browser/CustomTableWidget.cpp @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "CustomTableWidget.h" + +CustomTableWidget::CustomTableWidget(QWidget* parent) + : QTableWidget(parent) +{ +} + +void CustomTableWidget::keyPressEvent(QKeyEvent* event) +{ + if ((event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) && !selectedItems().isEmpty()) { + emit acceptSelections(); + } else { + QTableView::keyPressEvent(event); + } +} + +void CustomTableWidget::focusInEvent(QFocusEvent* event) +{ + // For some reason accept button gets selected if table is clicked without any + // selections, even if the button is actually disabled. Connecting to this + // signal and adjusting the button focuses fixes the issue. + if (event->reason() == Qt::MouseFocusReason && selectedItems().isEmpty()) { + emit focusInWithoutSelections(); + } +} diff --git a/src/browser/CustomTableWidget.h b/src/browser/CustomTableWidget.h new file mode 100644 index 000000000..655475e9d --- /dev/null +++ b/src/browser/CustomTableWidget.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CUSTOMTABLEWIDGET_H +#define CUSTOMTABLEWIDGET_H + +#include +#include +#include + +class CustomTableWidget : public QTableWidget +{ + Q_OBJECT + +public: + CustomTableWidget(QWidget* parent); + +signals: + void acceptSelections(); + void focusInWithoutSelections(); + +protected: + void keyPressEvent(QKeyEvent* event) override; + void focusInEvent(QFocusEvent* event) override; +}; + +#endif // CUSTOMTABLEWIDGET_H