diff options
Diffstat (limited to 'src/gui/entry/EntryView.cpp')
-rw-r--r-- | src/gui/entry/EntryView.cpp | 364 |
1 files changed, 329 insertions, 35 deletions
diff --git a/src/gui/entry/EntryView.cpp b/src/gui/entry/EntryView.cpp index 1bdd4fbcf..19978a808 100644 --- a/src/gui/entry/EntryView.cpp +++ b/src/gui/entry/EntryView.cpp @@ -1,37 +1,42 @@ /* - * Copyright (C) 2010 Felix Geyer <debfx@fobos.de> + * Copyright (C) 2018 KeePassXC Team <team@keepassxc.org> + * Copyright (C) 2010 Felix Geyer <debfx@fobos.de> * - * 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 or (at your option) - * version 3 of the License. + * 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 or (at your option) + * 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. + * 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/>. + * 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 "EntryView.h" #include <QHeaderView> #include <QKeyEvent> +#include <QMenu> +#include "core/FilePath.h" #include "gui/SortFilterHideProxyModel.h" EntryView::EntryView(QWidget* parent) : QTreeView(parent) , m_model(new EntryModel(this)) , m_sortModel(new SortFilterHideProxyModel(this)) - , m_inEntryListMode(false) + , m_inSearchMode(false) { m_sortModel->setSourceModel(m_model); m_sortModel->setDynamicSortFilter(true); m_sortModel->setSortLocaleAware(true); m_sortModel->setSortCaseSensitivity(Qt::CaseInsensitive); + // Use Qt::UserRole as sort role, see EntryModel::data() + m_sortModel->setSortRole(Qt::UserRole); QTreeView::setModel(m_sortModel); setUniformRowHeights(true); @@ -40,15 +45,77 @@ EntryView::EntryView(QWidget* parent) setDragEnabled(true); setSortingEnabled(true); setSelectionMode(QAbstractItemView::ExtendedSelection); - header()->setDefaultSectionSize(150); // QAbstractItemView::startDrag() uses this property as the default drag action setDefaultDropAction(Qt::MoveAction); connect(this, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex))); connect(selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), SIGNAL(entrySelectionChanged())); - connect(m_model, SIGNAL(switchedToEntryListMode()), SLOT(switchToEntryListMode())); - connect(m_model, SIGNAL(switchedToGroupMode()), SLOT(switchToGroupMode())); + connect(m_model, SIGNAL(switchedToListMode()), SLOT(switchToListMode())); + connect(m_model, SIGNAL(switchedToSearchMode()), SLOT(switchToSearchMode())); + connect(m_model, SIGNAL(usernamesHiddenChanged()), SIGNAL(viewStateChanged())); + connect(m_model, SIGNAL(passwordsHiddenChanged()), SIGNAL(viewStateChanged())); + connect(this, SIGNAL(clicked(QModelIndex)), SLOT(emitEntryPressed(QModelIndex))); + + m_headerMenu = new QMenu(this); + m_headerMenu->setTitle(tr("Customize View")); + m_headerMenu->addSection(tr("Customize View")); + + m_hideUsernamesAction = m_headerMenu->addAction(tr("Hide Usernames"), m_model, SLOT(toggleUsernamesHidden(bool))); + m_hideUsernamesAction->setCheckable(true); + m_hidePasswordsAction = m_headerMenu->addAction(tr("Hide Passwords"), m_model, SLOT(togglePasswordsHidden(bool))); + m_hidePasswordsAction->setCheckable(true); + m_headerMenu->addSeparator(); + + // Actions to toggle column visibility, each carrying the corresponding + // colummn index as data + m_columnActions = new QActionGroup(this); + m_columnActions->setExclusive(false); + for (int columnIndex = 1; columnIndex < header()->count(); ++columnIndex) { + QString caption = m_model->headerData(columnIndex, Qt::Horizontal, Qt::DisplayRole).toString(); + if (columnIndex == EntryModel::Paperclip) { + caption = tr("Attachments (icon)"); + } + + QAction* action = m_headerMenu->addAction(caption); + action->setCheckable(true); + action->setData(columnIndex); + m_columnActions->addAction(action); + } + connect(m_columnActions, SIGNAL(triggered(QAction*)), this, SLOT(toggleColumnVisibility(QAction*))); + + m_headerMenu->addSeparator(); + m_headerMenu->addAction(tr("Fit to window"), this, SLOT(fitColumnsToWindow())); + m_headerMenu->addAction(tr("Fit to contents"), this, SLOT(fitColumnsToContents())); + m_headerMenu->addSeparator(); + m_headerMenu->addAction(tr("Reset to defaults"), this, SLOT(resetViewToDefaults())); + + header()->setMinimumSectionSize(24); + header()->setDefaultSectionSize(100); + header()->setStretchLastSection(false); + header()->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(header(), SIGNAL(customContextMenuRequested(QPoint)), SLOT(showHeaderMenu(QPoint))); + connect(header(), SIGNAL(sectionCountChanged(int, int)), SIGNAL(viewStateChanged())); + connect(header(), SIGNAL(sectionMoved(int, int, int)), SIGNAL(viewStateChanged())); + connect(header(), SIGNAL(sectionResized(int, int, int)), SIGNAL(viewStateChanged())); + connect(header(), SIGNAL(sortIndicatorChanged(int, Qt::SortOrder)), SIGNAL(viewStateChanged())); + + resetFixedColumns(); + + // Configure default search view state and save for later use + header()->showSection(EntryModel::ParentGroup); + m_sortModel->sort(EntryModel::ParentGroup, Qt::AscendingOrder); + sortByColumn(EntryModel::ParentGroup, Qt::AscendingOrder); + m_defaultSearchViewState = header()->saveState(); + + // Configure default list view state and save for later use + header()->hideSection(EntryModel::ParentGroup); + m_sortModel->sort(EntryModel::Title, Qt::AscendingOrder); + sortByColumn(EntryModel::Title, Qt::AscendingOrder); + m_defaultListViewState = header()->saveState(); + + m_model->setPaperClipPixmap(filePath()->icon("actions", "paperclip").pixmap(16)); } void EntryView::keyPressEvent(QKeyEvent* event) @@ -61,6 +128,20 @@ void EntryView::keyPressEvent(QKeyEvent* event) #endif } + int last = m_model->rowCount() - 1; + + if (event->key() == Qt::Key_Up && currentIndex().row() == 0) { + QModelIndex index = m_sortModel->mapToSource(m_sortModel->index(last, 0)); + setCurrentEntry(m_model->entryFromIndex(index)); + return; + } + + if (event->key() == Qt::Key_Down && currentIndex().row() == last) { + QModelIndex index = m_sortModel->mapToSource(m_sortModel->index(0, 0)); + setCurrentEntry(m_model->entryFromIndex(index)); + return; + } + QTreeView::keyPressEvent(event); } @@ -81,15 +162,14 @@ void EntryView::setFirstEntryActive() if (m_model->rowCount() > 0) { QModelIndex index = m_sortModel->mapToSource(m_sortModel->index(0, 0)); setCurrentEntry(m_model->entryFromIndex(index)); - } - else { + } else { emit entrySelectionChanged(); } } -bool EntryView::inEntryListMode() +bool EntryView::inSearchMode() { - return m_inEntryListMode; + return m_inSearchMode; } void EntryView::emitEntryActivated(const QModelIndex& index) @@ -99,6 +179,11 @@ void EntryView::emitEntryActivated(const QModelIndex& index) emit entryActivated(entry, static_cast<EntryModel::ModelColumn>(m_sortModel->mapToSource(index).column())); } +void EntryView::emitEntryPressed(const QModelIndex& index) +{ + emit entryPressed(entryFromIndex(index)); +} + void EntryView::setModel(QAbstractItemModel* model) { Q_UNUSED(model); @@ -110,8 +195,7 @@ Entry* EntryView::currentEntry() QModelIndexList list = selectionModel()->selectedRows(); if (list.size() == 1) { return m_model->entryFromIndex(m_sortModel->mapToSource(list.first())); - } - else { + } else { return nullptr; } } @@ -131,30 +215,240 @@ Entry* EntryView::entryFromIndex(const QModelIndex& index) { if (index.isValid()) { return m_model->entryFromIndex(m_sortModel->mapToSource(index)); - } - else { + } else { return nullptr; } } -void EntryView::switchToEntryListMode() +/** + * Switch to list mode, i.e. list entries of group + */ +void EntryView::switchToListMode() +{ + if (!m_inSearchMode) { + return; + } + + header()->hideSection(EntryModel::ParentGroup); + m_inSearchMode = false; +} + +/** + * Switch to search mode, i.e. list search results + */ +void EntryView::switchToSearchMode() +{ + if (m_inSearchMode) { + return; + } + + header()->showSection(EntryModel::ParentGroup); + + // Always set sorting to column 'Group', as it does not feel right to + // have the last known sort configuration of search view restored by + // 'DatabaseWidgetStateSync', which is what happens without this + m_sortModel->sort(EntryModel::ParentGroup, Qt::AscendingOrder); + sortByColumn(EntryModel::ParentGroup, Qt::AscendingOrder); + + m_inSearchMode = true; +} + +/** + * Get current state of 'Hide Usernames' setting (NOTE: just pass-through for + * m_model) + */ +bool EntryView::isUsernamesHidden() const +{ + return m_model->isUsernamesHidden(); +} + +/** + * Set state of 'Hide Usernames' setting (NOTE: just pass-through for m_model) + */ +void EntryView::setUsernamesHidden(const bool hide) +{ + m_model->setUsernamesHidden(hide); +} + +/** + * Get current state of 'Hide Passwords' setting (NOTE: just pass-through for + * m_model) + */ +bool EntryView::isPasswordsHidden() const +{ + return m_model->isPasswordsHidden(); +} + +/** + * Set state of 'Hide Passwords' setting (NOTE: just pass-through for m_model) + */ +void EntryView::setPasswordsHidden(const bool hide) +{ + m_model->setPasswordsHidden(hide); +} + +/** + * Get current view state + */ +QByteArray EntryView::viewState() const +{ + return header()->saveState(); +} + +/** + * Set view state + */ +bool EntryView::setViewState(const QByteArray& state) +{ + bool status = header()->restoreState(state); + resetFixedColumns(); + return status; +} + +/** + * Sync checkable menu actions to current state and display header context + * menu at specified position + */ +void EntryView::showHeaderMenu(const QPoint& position) +{ + m_hideUsernamesAction->setChecked(m_model->isUsernamesHidden()); + m_hidePasswordsAction->setChecked(m_model->isPasswordsHidden()); + const QList<QAction*> actions = m_columnActions->actions(); + for (auto& action : actions) { + Q_ASSERT(static_cast<QMetaType::Type>(action->data().type()) == QMetaType::Int); + if (static_cast<QMetaType::Type>(action->data().type()) != QMetaType::Int) { + continue; + } + int columnIndex = action->data().toInt(); + bool hidden = header()->isSectionHidden(columnIndex) || (header()->sectionSize(columnIndex) == 0); + action->setChecked(!hidden); + } + + m_headerMenu->popup(mapToGlobal(position)); +} + +/** + * Toggle visibility of column referenced by triggering action + */ +void EntryView::toggleColumnVisibility(QAction *action) { - m_sortModel->hideColumn(0, false); + // Verify action carries a column index as data. Since QVariant.toInt() + // below will accept anything that's interpretable as int, perform a type + // check here to make sure data actually IS int + Q_ASSERT(static_cast<QMetaType::Type>(action->data().type()) == QMetaType::Int); + if (static_cast<QMetaType::Type>(action->data().type()) != QMetaType::Int) { + return; + } - m_sortModel->sort(1, Qt::AscendingOrder); - m_sortModel->sort(0, Qt::AscendingOrder); - sortByColumn(0, Qt::AscendingOrder); + // Toggle column visibility. Visible columns will only be hidden if at + // least one visible column remains, as the table header will disappear + // entirely when all columns are hidden + int columnIndex = action->data().toInt(); + if (action->isChecked()) { + header()->showSection(columnIndex); + if (header()->sectionSize(columnIndex) == 0) { + header()->resizeSection(columnIndex, header()->defaultSectionSize()); + } + return; + } + if ((header()->count() - header()->hiddenSectionCount()) > 1) { + header()->hideSection(columnIndex); + return; + } + action->setChecked(true); +} + +/** + * Resize columns to fit all visible columns within the available space + * + * NOTE: + * If EntryView::resizeEvent() is overridden at some point in the future, + * its implementation MUST call the corresponding parent method using + * 'QTreeView::resizeEvent(event)'. Without this, fitting to window will + * be broken and/or work unreliably (stumbled upon during testing) + * + * NOTE: + * Testing showed that it is absolutely necessary to emit signal 'viewState + * Changed' here. Without this, an incomplete view state might get saved by + * 'DatabaseWidgetStateSync' (e.g. only some columns resized) + */ +void EntryView::fitColumnsToWindow() +{ + header()->resizeSections(QHeaderView::Stretch); + resetFixedColumns(); + fillRemainingWidth(true); + emit viewStateChanged(); +} + +/** + * Resize columns to fit current table contents, i.e. make all contents + * entirely visible + */ +void EntryView::fitColumnsToContents() +{ + // Resize columns to fit contents + header()->resizeSections(QHeaderView::ResizeToContents); + resetFixedColumns(); + fillRemainingWidth(false); + emit viewStateChanged(); +} + +/** + * Reset view to defaults + */ +void EntryView::resetViewToDefaults() +{ + m_model->setUsernamesHidden(false); + m_model->setPasswordsHidden(true); + + if (m_inSearchMode) { + header()->restoreState(m_defaultSearchViewState); + } else { + header()->restoreState(m_defaultListViewState); + } - m_inEntryListMode = true; + fitColumnsToWindow(); } -void EntryView::switchToGroupMode() +void EntryView::fillRemainingWidth(bool lastColumnOnly) { - m_sortModel->hideColumn(0, true); + // Determine total width of currently visible columns + int width = 0; + int lastColumnIndex = 0; + for (int columnIndex = 0; columnIndex < header()->count(); ++columnIndex) { + if (!header()->isSectionHidden(columnIndex)) { + width += header()->sectionSize(columnIndex); + } + if (header()->visualIndex(columnIndex) > lastColumnIndex) { + lastColumnIndex = header()->visualIndex(columnIndex); + } + } + + int numColumns = header()->count() - header()->hiddenSectionCount(); + int availWidth = header()->width() - width; + if ((numColumns <= 0) || (availWidth <= 0)) { + return; + } + + if (!lastColumnOnly) { + // Equally distribute remaining width to visible columns + int add = availWidth / numColumns; + width = 0; + for (int columnIndex = 0; columnIndex < header()->count(); ++columnIndex) { + if (!header()->isSectionHidden(columnIndex)) { + header()->resizeSection(columnIndex, header()->sectionSize(columnIndex) + add); + width += header()->sectionSize(columnIndex); + } + } + } - m_sortModel->sort(-1, Qt::AscendingOrder); - m_sortModel->sort(0, Qt::AscendingOrder); - sortByColumn(0, Qt::AscendingOrder); + // Add remaining width to last column + header()->resizeSection(header()->logicalIndex(lastColumnIndex), header()->sectionSize(lastColumnIndex) + (header()->width() - width)); +} - m_inEntryListMode = false; +void EntryView::resetFixedColumns() +{ + header()->setSectionResizeMode(EntryModel::Paperclip, QHeaderView::Fixed); + header()->resizeSection(EntryModel::Paperclip, header()->minimumSectionSize()); } + |