Commit bcd15121 authored by Stefano Crocco's avatar Stefano Crocco Committed by David Faure

Add a button to display the credentials which will be saved

Currently, when the user is asked whether to save credentials using the
password bar, he has no way to know which values will be saved. To fix
this, a button is added to the password bar to toggle a widget which
displays those values.
parent 8c8cb4fb
......@@ -28,9 +28,11 @@ set(kwebenginepartlib_LIB_SRCS
ui/featurepermissionbar.cpp
about/konq_aboutpage.cpp
webenginecustomizecacheablefieldsdlg.cpp
webfieldsdataview.cpp
ui/credentialsdetailswidget.cpp
)
ki18n_wrap_ui(kwebenginepartlib_LIB_SRCS webenginecustomizecacheablefieldsdlg.ui)
ki18n_wrap_ui(kwebenginepartlib_LIB_SRCS webenginecustomizecacheablefieldsdlg.ui ui/credentialsdetailswidget.ui)
qt5_add_resources(kwebenginepartlib_LIB_SRCS webenginepart.qrc)
......
/*
* This file is part of the KDE project.
*
* Copyright 2020 Stefano Crocco <posta@stefanocrocco.it>
*
* 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 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "credentialsdetailswidget.h"
CredentialsDetailsWidget::CredentialsDetailsWidget(QWidget* parent)
: QWidget(parent), m_ui(new Ui::CredentialsDetailsWidget), m_model(new WebFieldsDataModel(false, this))
{
m_ui->setupUi(this);
m_ui->fields->setModel(m_model);
m_ui->fields->toggleDetails(false);
m_ui->fields->togglePasswords(false);
m_ui->fields->toggleToolTips(false);
m_ui->fields->horizontalHeader()->hide();
connect(m_ui->showPasswords, &QCheckBox::toggled, m_ui->fields, &WebFieldsDataView::togglePasswords);
}
CredentialsDetailsWidget::~CredentialsDetailsWidget()
{
}
void CredentialsDetailsWidget::setForms(const WebEngineWallet::WebFormList& forms)
{
m_model->setForms(forms);
}
/*
* This file is part of the KDE project.
*
* Copyright 2020 Stefano Crocco <posta@stefanocrocco.it>
*
* 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 2 of
* the License or (at your option) version 3 or any later version
* accepted by the membership of KDE e.V. (or its successor approved
* by the membership of KDE e.V.), which shall act as a proxy
* defined in Section 14 of version 3 of the license.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef CREDENTIALSDETAILSWIDGET_H
#define CREDENTIALSDETAILSWIDGET_H
#include <QWidget>
#include <QScopedPointer>
#include "ui_credentialsdetailswidget.h"
#include "webenginewallet.h"
class WebFieldsDataModel;
class CredentialsDetailsWidget : public QWidget
{
Q_OBJECT
public:
CredentialsDetailsWidget(QWidget* parent);
~CredentialsDetailsWidget() override;
void setForms(const WebEngineWallet::WebFormList &forms);
private:
QScopedPointer<Ui::CredentialsDetailsWidget> m_ui;
WebFieldsDataModel *m_model;
};
#endif // CREDENTIALSDETAILSWIDGET_H
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CredentialsDetailsWidget</class>
<widget class="QWidget" name="CredentialsDetailsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>304</width>
<height>236</height>
</rect>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="WebFieldsDataView" name="fields">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QCheckBox" name="showPasswords">
<property name="text">
<string>&amp;Show passwords</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>WebFieldsDataView</class>
<extends>QTableView</extends>
<header>webfieldsdataview.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
......@@ -29,10 +29,12 @@
#include <QCoreApplication>
#include <QAction>
#include <QPalette>
#include <QToolButton>
#include <QTimer>
#include <algorithm>
PasswordBar::PasswordBar(QWidget *parent)
:KMessageWidget(parent)
PasswordBar::PasswordBar(QWidget *parent) :KMessageWidget(parent), m_detailsVisible(false), m_detailsWidget(new CredentialsDetailsWidget(parent))
{
setCloseButtonVisible(false);
setMessageType(KMessageWidget::Information);
......@@ -48,6 +50,11 @@ PasswordBar::PasswordBar(QWidget *parent)
action = new QAction(i18nc("@action:not now", "N&ot now"), this);
connect(action, &QAction::triggered, this, &PasswordBar::onNotNowButtonClicked);
addAction(action);
m_detailsAction = new QAction(i18nc("@action:display details about credentials to store", "&Show details"), this);
m_detailsAction->setObjectName("detailsAction");
connect(m_detailsAction, &QAction::triggered, this, &PasswordBar::onDetailsButtonClicked);
addAction(m_detailsAction);
}
PasswordBar::~PasswordBar()
......@@ -101,3 +108,43 @@ void PasswordBar::clear()
m_requestKey.clear();
m_url.clear();
}
void PasswordBar::resizeEvent(QResizeEvent* event)
{
KMessageWidget::resizeEvent(event);
if (m_detailsVisible && m_detailsWidget) {
m_detailsWidget->move(computeDetailsWidgetPosition());
}
}
void PasswordBar::onDetailsButtonClicked()
{
m_detailsVisible = !m_detailsVisible;
if (m_detailsVisible) {
m_detailsAction->setText(i18nc("@action:hide details about credentials to store", "&Hide details"));
} else {
m_detailsAction->setText(i18nc("@action:display details about credentials to store", "&Show details"));
}
if (m_detailsWidget) {
m_detailsWidget->setVisible(m_detailsVisible);
if (m_detailsVisible) {
m_detailsWidget->resize(m_detailsWidget->sizeHint());
m_detailsWidget->move(computeDetailsWidgetPosition());
}
}
}
void PasswordBar::setForms(const WebEngineWallet::WebFormList& forms)
{
if (m_detailsWidget) {
m_detailsWidget->setForms(forms);
}
}
QPoint PasswordBar::computeDetailsWidgetPosition() const
{
if (!m_detailsWidget) {
return QPoint();
}
return mapTo(parentWidget(), {width() - m_detailsWidget->width(), height()});
}
......@@ -25,7 +25,10 @@
#include <KMessageWidget>
#include <QUrl>
#include <QPointer>
#include "webenginewallet.h"
#include "credentialsdetailswidget.h"
class PasswordBar : public KMessageWidget
{
......@@ -40,21 +43,33 @@ public:
void setUrl(const QUrl&);
void setRequestKey(const QString&);
void setForms(const WebEngineWallet::WebFormList &forms);
Q_SIGNALS:
void saveFormDataRejected(const QString &key);
void saveFormDataAccepted(const QString &key);
void done();
void toggleDetailsRequested(const QUrl &url, bool visible);
void moved();
private Q_SLOTS:
void onNotNowButtonClicked();
void onNeverButtonClicked();
void onRememberButtonClicked();
void onDetailsButtonClicked();
protected:
void resizeEvent(QResizeEvent * event) override;
private:
void clear();
QPoint computeDetailsWidgetPosition() const;
QUrl m_url;
QString m_requestKey;
bool m_detailsVisible;
QAction *m_detailsAction;
QPointer<CredentialsDetailsWidget> m_detailsWidget;
};
#endif // PASSWORDBAR_H
......@@ -23,118 +23,52 @@
#include "webenginecustomizecacheablefieldsdlg.h"
#include <QDialogButtonBox>
#include <QLayout>
#include <QTableView>
#include <QStandardItemModel>
#include <QStandardItem>
#include <QCheckBox>
#include "ui_webenginecustomizecacheablefieldsdlg.h"
#include "webfieldsdataview.h"
using WebForm = WebEngineWallet::WebForm;
using WebFormList = WebEngineWallet::WebFormList;
using WebField = WebEngineWallet::WebForm::WebField;
using WebFieldType = WebEngineWallet::WebForm::WebFieldType;
WebEnginePartPasswordDelegate::WebEnginePartPasswordDelegate(QObject* parent): QStyledItemDelegate(parent)
{
}
bool WebEnginePartPasswordDelegate::isPassword(const QModelIndex& idx)
{
return idx.data(WebEngineCustomizeCacheableFieldsDlg::PasswordRole).toBool();
}
QString WebEnginePartPasswordDelegate::passwordReplacement(const QStyleOptionViewItem& option, const QModelIndex& index)
{
const QWidget *w = option.widget;
QStyle *s = w->style();
QChar passwdChar = s->styleHint(QStyle::StyleHint::SH_LineEdit_PasswordCharacter, &option, w);
return QString(index.data().toString().length(), passwdChar);
}
void WebEnginePartPasswordDelegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const
{
if (!isPassword(index)) {
QStyledItemDelegate::paint(painter, option, index);
} else {
QString str = passwordReplacement(option, index);
option.widget->style()->drawItemText(painter, option.rect, index.data(Qt::TextAlignmentRole).toInt(), option.palette, true, str);
}
}
QSize WebEnginePartPasswordDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const
WebEngineCustomizeCacheableFieldsDlg::WebEngineCustomizeCacheableFieldsDlg(const WebEngineWallet::WebFormList &forms, const OldCustomizationData &oldCustomization, QWidget* parent):
QDialog(parent), m_model(new WebFieldsDataModel(true, this)),
m_ui(new Ui::WebEngineCustomizeCacheableFieldsDlg)
{
if (!isPassword(index)) {
return QStyledItemDelegate::sizeHint(option, index);
} else {
QString str = passwordReplacement(option, index);
return option.widget->style()->itemTextRect(option.fontMetrics, option.rect, option.displayAlignment, true, str).size();
}
m_ui->setupUi(this);
connect(m_ui->showPasswords, &QCheckBox::toggled, m_ui->fields, &WebFieldsDataView::togglePasswords);
connect(m_ui->showDetails, &QCheckBox::toggled, m_ui->fields, &WebFieldsDataView::toggleDetails);
m_model->setForms(forms);
addChecksToPreviouslyChosenItems(forms, oldCustomization);
m_ui->fields->setModel(m_model);
m_ui->fields->horizontalHeader()->setStretchLastSection(true);
m_ui->fields->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
m_ui->fields->toggleDetails(m_ui->showDetails->isChecked());
}
WebEngineCustomizeCacheableFieldsDlg::WebEngineCustomizeCacheableFieldsDlg(const WebEngineWallet::WebFormList &forms, const QMap<QString, QStringList> &oldCustomization, QWidget* parent):
QDialog(parent), m_forms(forms), m_model(new QStandardItemModel(this)), m_passwordDelegate(new WebEnginePartPasswordDelegate(this)),
m_ui(new Ui::WebEngineCustomizeCacheableFieldsDlg)
WebFormList WebEngineCustomizeCacheableFieldsDlg::selectedFields() const
{
m_ui->setupUi(this);
connect(m_ui->showPasswords, &QCheckBox::toggled, this, &WebEngineCustomizeCacheableFieldsDlg::toggleShowPasswords);
connect(m_ui->showDetails, &QCheckBox::toggled, this, &WebEngineCustomizeCacheableFieldsDlg::toggleDetails);
fillFieldTable(oldCustomization);
return m_model->checkedFields();
}
QList<QStandardItem *> WebEngineCustomizeCacheableFieldsDlg::createRowForField(const WebEngineWallet::WebForm::WebField& field)
bool WebEngineCustomizeCacheableFieldsDlg::immediatelyCacheData() const
{
QString type = WebForm::fieldNameFromType(field.type, true);
QStringList notes;
if (field.readOnly) {
notes << i18nc("web field has the readonly attribute", "read only");
}
if (!field.autocompleteAllowed) {
notes << i18nc("web field has the autocomplete attribute set to off", "auto-completion off");
}
if (field.disabled) {
notes << i18nc("web field is disabled", "disabled");
}
QString label = !field.label.isEmpty() ? field.label : field.name;
QStringList contents{QString(), label, field.value, field.name, type, field.id, notes.join(", ")};
QList<QStandardItem*> row;
row.reserve(contents.size());
auto itemFromString = [](const QString &s){
QStandardItem *it = new QStandardItem(s);
it->setTextAlignment(Qt::AlignCenter);
return it;
};
std::transform(contents.constBegin(), contents.constEnd(), std::back_inserter(row), itemFromString);
row[ValueCol]->setData(field.type == WebFieldType::Password, PasswordRole);
row[ChosenCol]->setCheckable(true);
QString toolTip = toolTipForField(field);
row[LabelCol]->setToolTip(toolTip);
row[ValueCol]->setToolTip(toolTip);
return row;
return m_ui->immediatelyCacheData->isChecked();
}
void WebEngineCustomizeCacheableFieldsDlg::fillFieldTable(const QMap<QString, QStringList> &oldCustomization)
void WebEngineCustomizeCacheableFieldsDlg::addChecksToPreviouslyChosenItems(const WebEngineWallet::WebFormList &forms, const OldCustomizationData &data)
{
bool autoCheck = oldCustomization.isEmpty();
m_ui->fields->setModel(m_model);
m_ui->fields->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_model->setHorizontalHeaderLabels(QStringList{"",
i18nc("Label of a web field", "Field name"),
i18nc("Value of a web field", "Field value"),
i18nc("Name attribute of a web field", "Internal field name"),
i18nc("Type of a web field", "Field type"),
i18nc("The id of a web field", "Field id"),
i18nc("Other details about a web field", "Details")});
m_ui->fields->setItemDelegateForColumn(2, m_passwordDelegate);
for (int i = 0; i < m_forms.length(); ++i) {
const WebForm &form = m_forms.at(i);
QStringList oldCustomInThisForm = oldCustomization.value(form.name);
bool autoCheck = data.isEmpty();
int row = 0;
for (int i = 0; i < forms.length(); ++i) {
const WebForm &form = forms.at(i);
QStringList oldCustomInThisForm = data.value(form.name);
for (int j = 0; j < form.fields.length(); ++j) {
WebField field = form.fields.at(j);
QList<QStandardItem*> row = createRowForField(field);
QStandardItem *chosen = row.at(ChosenCol);
chosen->setData(i, FormRole);
chosen->setData(j, FieldRole);
QStandardItem *chosen = m_model->item(row, WebFieldsDataModel::ChosenCol);
bool checked = false;
if (autoCheck) {
checked = !field.value.isEmpty() && !field.readOnly && !field.disabled && field.autocompleteAllowed;
......@@ -142,76 +76,7 @@ void WebEngineCustomizeCacheableFieldsDlg::fillFieldTable(const QMap<QString, QS
checked = oldCustomInThisForm.contains(field.name);
}
chosen->setCheckState(checked ? Qt::Checked : Qt::Unchecked);
m_model->appendRow(row);
++row;
}
}
m_ui->fields->verticalHeader()->hide();
m_ui->fields->horizontalHeader()->setStretchLastSection(true);
m_ui->fields->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
toggleDetails(m_ui->showDetails->isChecked());
}
WebFormList WebEngineCustomizeCacheableFieldsDlg::selectedFields() const
{
QMap<int, QVector<int>> fields;
for (int i = 0; i < m_model->rowCount(); ++i) {
QStandardItem *it = m_model->item(i, ChosenCol);
if (it->checkState() == Qt::Checked) {
fields[it->data(FormRole).toInt()].append(it->data(FieldRole).toInt());
}
}
WebFormList lst;
for (QMap<int, QVector<int>>::const_iterator it = fields.constBegin(); it != fields.constEnd(); ++it) {
if (it.value().isEmpty()) {
continue;
}
const WebForm &oldForm = m_forms.at(it.key());
WebForm form(oldForm);
form.fields.clear();
for (int i : it.value()) {
form.fields.append(oldForm.fields.at(i));
}
lst.append(form);
}
return lst;
}
void WebEngineCustomizeCacheableFieldsDlg::toggleDetails (bool show)
{
for (int i = InternalNameCol; i <= DetailsCol; ++i) {
m_ui->fields->setColumnHidden(i, !show);
}
}
void WebEngineCustomizeCacheableFieldsDlg::toggleShowPasswords (bool show)
{
//Do nothing if the item delegate setting is already correct. This should never happen
if (show == (m_ui->fields->itemDelegateForColumn(ValueCol) == m_ui->fields->itemDelegate())) {
return;
}
QAbstractItemDelegate *delegate = show ? m_ui->fields->itemDelegate() : m_passwordDelegate;
m_ui->fields->setItemDelegateForColumn(ValueCol, delegate);
}
bool WebEngineCustomizeCacheableFieldsDlg::immediatelyCacheData() const
{
return m_ui->immediatelyCacheData->isChecked();
}
QString WebEngineCustomizeCacheableFieldsDlg::toolTipForField(const WebEngineWallet::WebForm::WebField &field)
{
QString type = WebForm::fieldNameFromType(field.type, true);
const QString yes = i18nc("A statement about a field is true", "yes");
const QString no = i18nc("A statement about a field is false", "no");
auto boolToYesNo = [yes, no](bool val){return val ? yes : no;};
QString toolTip = i18n(
"<ul><li><b>Field internal name: </b>%1</li>"
"<li><b>Field type: </b>%2</li>"
"<li><b>Field id: </b>%3</li>"
"<li><b>Field is read only: </b>%4</li>"
"<li><b>Field is enabled: </b>%5</li>"
"<li><b>Autocompletion is enabled: </b>%6</li>"
"</ul>",
field.name, type, field.id, boolToYesNo(field.readOnly), boolToYesNo(!field.disabled), boolToYesNo(field.autocompleteAllowed));
return toolTip;
}
......@@ -24,82 +24,16 @@
#define WEBENGINECUSTOMIZECACHEABLEFIELDSDLG_H
#include <QDialog>
#include <QStyledItemDelegate>
#include "webenginewallet.h"
class QDialogButtonBox;
class QCheckBox;
class QTableView;
class QStandardItemModel;
class QStandardItem;
class WebFieldsDataViewPasswordDelegate;
class WebFieldsDataModel;
namespace Ui {
class WebEngineCustomizeCacheableFieldsDlg;
};
/**
* @brief Helper class to display passwords in WebEngineCustomizeCacheableFieldsDlg
*/
class WebEnginePartPasswordDelegate : public QStyledItemDelegate {
Q_OBJECT
public:
/**
* @brief Default constructor
*
* @param parent the parent object
*/
explicit WebEnginePartPasswordDelegate(QObject *parent=nullptr);
~WebEnginePartPasswordDelegate(){}
/**
* @brief Override of <a href="https://doc.qt.io/qt-5/qstyleditemdelegate.html#paint">QStyledItemDelegate::paint()</a>
*
* It displays the text replacing each character with a special character according to QStyle::StyleHint::SH_LineEdit_PasswordCharacter,
* but only if the data contained in the index represents a password, according to WebEngineCustomizeCacheableFieldsDlg::PasswordRole
*
* @param painter the painter
* @param option the option
* @param index the index to paint
*/
void paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const override;
/**
* @brief Override of <a href="https://doc.qt.io/qt-5/qstyleditemdelegate.html#sizeHint">QStyledItemDelegate::sizeHint()</a>
*
* @param option the option
* @param index the index whose size hint should be returned
* @return the size hint for @p index
*/
QSize sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const override;
private:
/**
* @brief Whether the given index represents a password
*
* This function uses the value of the WebEngineCustomizeCacheableFieldsDlg::PasswordRole to determine if @p idx represents or not a password
* @param idx the index
* @return @b true if @p idx represents and index and @b false otherwise
*/
static bool isPassword(const QModelIndex &idx);
/**
* @brief The string to display in place of a password
*
* It returns a string with the same length as the Qt::DisplayRole of @p index all made by the character returned by <tt>QStyle::styleHint</tt>
* called with argument QStyle::StyleHint::SH_LineEdit_PasswordCharacter.
*
* @param option the option to pass to QStyle::styleHint
* @param index the index containing the password
* @return a string suitable to be displayed instead of the display role of @p index
*/
static QString passwordReplacement(const QStyleOptionViewItem &option, const QModelIndex &index);
};
/**
* @brief Dialog where the user can choose which fields in a web page should be saved in KWallet
*
......@@ -116,6 +50,8 @@ class WebEngineCustomizeCacheableFieldsDlg : public QDialog
Q_OBJECT
public: