From 31d73626e5ebfdc0115b9fd745d6af760608f539 Mon Sep 17 00:00:00 2001 From: Fonic Date: Thu, 21 Dec 2017 10:01:47 +0100 Subject: [PATCH] Add header context menu to entry view table Add header context menu to entry view table (accessible using right click on header), providing: - Actions to toggle 'Hide Usernames' / 'Hide Passwords' - Actions to toggle column visibility - Actions to resize columns - Action to reset view to defaults --- src/gui/entry/EntryModel.cpp | 18 +++ src/gui/entry/EntryModel.h | 8 ++ src/gui/entry/EntryView.cpp | 268 ++++++++++++++++++++++++++++++++++- src/gui/entry/EntryView.h | 24 ++++ 4 files changed, 313 insertions(+), 5 deletions(-) diff --git a/src/gui/entry/EntryModel.cpp b/src/gui/entry/EntryModel.cpp index b042c255c..e03b0f791 100644 --- a/src/gui/entry/EntryModel.cpp +++ b/src/gui/entry/EntryModel.cpp @@ -578,3 +578,21 @@ void EntryModel::setHidePasswords(const bool hide) m_hidePasswords = hide; emit hidePasswordsChanged(); } + +/** + * @author Fonic + * Toggle state of 'Hide Usernames' setting + */ +void EntryModel::toggleHideUsernames(const bool hide) +{ + setHideUsernames(hide); +} + +/** + * @author Fonic + * Toggle state of 'Hide Passwords' setting + */ +void EntryModel::toggleHidePasswords(const bool hide) +{ + setHidePasswords(hide); +} diff --git a/src/gui/entry/EntryModel.h b/src/gui/entry/EntryModel.h index 5bb58c9ad..5b4f5e734 100644 --- a/src/gui/entry/EntryModel.h +++ b/src/gui/entry/EntryModel.h @@ -77,6 +77,7 @@ public: signals: void switchedToEntryListMode(); void switchedToGroupMode(); + /** * @author Fonic * Signals to notify about state changes of 'Hide Usernames' and 'Hide @@ -88,6 +89,13 @@ signals: public slots: void setGroup(Group* group); + /** + * @author Fonic + * Slots to toggle state of 'Hide Usernames' and 'Hide Passwords' settings + */ + void toggleHideUsernames(const bool hide); + void toggleHidePasswords(const bool hide); + private slots: void entryAboutToAdd(Entry* entry); void entryAdded(Entry* entry); diff --git a/src/gui/entry/EntryView.cpp b/src/gui/entry/EntryView.cpp index 6d2c88279..6657818d2 100644 --- a/src/gui/entry/EntryView.cpp +++ b/src/gui/entry/EntryView.cpp @@ -19,9 +19,26 @@ #include #include +/** + * @author Fonic + * Add include required for header context menu + */ +#include #include "gui/SortFilterHideProxyModel.h" +/** + * @author Fonic + * + * TODO NOTE: + * Currently, 'zombie' columns which are not hidden but have width == 0 + * (rendering them invisible) may appear. This is caused by DatabaseWidget + * StateSync. Corresponding checks/workarounds may be removed once sync + * code is updated accordingly + * -> relevant code pieces: if (header()->sectionSize(...) == 0) { ... } + * + */ + EntryView::EntryView(QWidget* parent) : QTreeView(parent) , m_model(new EntryModel(this)) @@ -47,7 +64,6 @@ 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); @@ -58,6 +74,69 @@ EntryView::EntryView(QWidget* parent) connect(m_model, SIGNAL(switchedToGroupMode()), SLOT(switchToGroupMode())); connect(this, SIGNAL(clicked(QModelIndex)), SLOT(emitEntryPressed(QModelIndex))); + + /** + * @author Fonic + * Create header context menu: + * - Actions to toggle state of 'Hide Usernames'/'Hide Passwords' settings + * - Actions to toggle column visibility, with each action carrying 'its' + * column index as data + * - Actions to resize columns + * - Action to reset view to defaults + */ + 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(toggleHideUsernames(bool))); + m_hideUsernamesAction->setCheckable(true); + m_hidePasswordsAction = m_headerMenu->addAction(tr("Hide Passwords"), m_model, SLOT(toggleHidePasswords(bool))); + m_hidePasswordsAction->setCheckable(true); + m_headerMenu->addSeparator(); + + m_columnActions = new QActionGroup(this); + m_columnActions->setExclusive(false); + for (int colidx = 1; colidx < header()->count(); colidx++) { + QString caption = m_model->headerData(colidx, Qt::Horizontal, Qt::DisplayRole).toString(); + QAction* action = m_headerMenu->addAction(caption); + action->setCheckable(true); + action->setData(colidx); + 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())); + + /** + * @author Fonic + * Configure header: + * - Set default section size + * - Disable stretching of last section (interferes with fitting columns + * to window) + * - Associate with context menu + */ + header()->setDefaultSectionSize(100); + header()->setStretchLastSection(false); + header()->setContextMenuPolicy(Qt::CustomContextMenu); + connect(header(), SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(showHeaderMenu(QPoint))); + + /** + * @author Fonic + * Finalize setup by resetting view to defaults. Although not really + * necessary at this point, it makes sense in order to avoid duplicating + * code (sorting order, visibility of first column etc.) + * + * TODO: + * Not working as expected, columns will end up being very small, most + * likely due to EntryView not being sized properly at this time. Either + * find a way to make this work by analizing when/where EntryView is + * created or remove + */ + //resetViewToDefaults(); } void EntryView::keyPressEvent(QKeyEvent* event) @@ -153,22 +232,201 @@ Entry* EntryView::entryFromIndex(const QModelIndex& index) void EntryView::switchToEntryListMode() { - m_sortModel->hideColumn(0, false); + /** + * @author Fonic + * Use header()->showSection() instead of m_sortModel->hideColumn() as + * the latter messes up column indices, interfering with code relying on + * proper indices + */ + header()->showSection(EntryModel::ParentGroup); + if (header()->sectionSize(EntryModel::ParentGroup) == 0) { + header()->resizeSection(EntryModel::ParentGroup, header()->defaultSectionSize()); + } + /** + * @author Fonic + * Set sorting column and order (TODO: check what first two lines do, if + * they are actually necessary, if indices are still correct and if indices + * may be replaced by EntryModel::) + */ m_sortModel->sort(1, Qt::AscendingOrder); m_sortModel->sort(0, Qt::AscendingOrder); - sortByColumn(0, Qt::AscendingOrder); + sortByColumn(EntryModel::ParentGroup, Qt::AscendingOrder); m_inEntryListMode = true; } void EntryView::switchToGroupMode() { - m_sortModel->hideColumn(0, true); + /** + * @author Fonic + * Use header()->hideSection() instead of m_sortModel->hideColumn() as + * the latter messes up column indices, interfering with code relying on + * proper indices + */ + header()->hideSection(EntryModel::ParentGroup); + /** + * @author Fonic + * Set sorting column and order (TODO: check what first two lines do, if + * they are actually necessary, if indices are still correct and if indices + * may be replaced by EntryModel::) + */ m_sortModel->sort(-1, Qt::AscendingOrder); m_sortModel->sort(0, Qt::AscendingOrder); - sortByColumn(0, Qt::AscendingOrder); + sortByColumn(EntryModel::Title, Qt::AscendingOrder); m_inEntryListMode = false; } + +/** + * @author Fonic + * Sync checkable menu actions to current state and display header context + * menu at specified position + */ +void EntryView::showHeaderMenu(const QPoint& position) +{ + /* Sync checked state of menu actions to current state of view */ + m_hideUsernamesAction->setChecked(m_model->hideUsernames()); + m_hidePasswordsAction->setChecked(m_model->hidePasswords()); + foreach (QAction *action, m_columnActions->actions()) { + if (static_cast(action->data().type()) != QMetaType::Int) { + Q_ASSERT(false); + continue; + } + int colidx = action->data().toInt(); + bool hidden = header()->isSectionHidden(colidx) || (header()->sectionSize(colidx) == 0); + action->setChecked(!hidden); + } + + /* Display menu */ + m_headerMenu->popup(mapToGlobal(position)); +} + +/** + * @author Fonic + * Toggle visibility of column referenced by triggering action + */ +void EntryView::toggleColumnVisibility(QAction *action) +{ + /* + * 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 + */ + if (static_cast(action->data().type()) != QMetaType::Int) { + Q_ASSERT(false); + return; + } + + /* + * 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, rendering the context menu in- + * accessible + */ + int colidx = action->data().toInt(); + if (action->isChecked()) { + header()->showSection(colidx); + if (header()->sectionSize(colidx) == 0) { + header()->resizeSection(colidx, header()->defaultSectionSize()); + } + } + else { + if ((header()->count() - header()->hiddenSectionCount()) > 1) { + header()->hideSection(colidx); + } + else { + action->setChecked(true); + } + } +} + +/** + * @author Fonic + * 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) + */ +void EntryView::fitColumnsToWindow() +{ + header()->resizeSections(QHeaderView::Stretch); +} + +/** + * @author Fonic + * 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); + + /* + * Determine total width of currently visible columns. If there is + * still some space available on the header, equally distribute it to + * visible columns and add remaining fraction to last visible column + */ + int width = 0; + for (int colidx = 0; colidx < header()->count(); colidx++) { + if (!header()->isSectionHidden(colidx)) { + width += header()->sectionSize(colidx); + } + } + int visible = header()->count() - header()->hiddenSectionCount(); + int avail = header()->width() - width; + if ((visible > 0) && (avail > 0)) { + int add = avail / visible; + width = 0; + int last = 0; + for (int colidx = 0; colidx < header()->count(); colidx++) { + if (!header()->isSectionHidden(colidx)) { + header()->resizeSection(colidx, header()->sectionSize(colidx) + add); + width += header()->sectionSize(colidx); + if (header()->visualIndex(colidx) > last) { + last = header()->visualIndex(colidx); + } + } + } + header()->resizeSection(header()->logicalIndex(last), header()->sectionSize(last) + (header()->width() - width)); + } +} + +/** + * @author Fonic + * Reset view to defaults + * + * NOTE: + * header()->saveState()/restoreState() could also be used for this, but + * testing showed that it complicates things more than it helps when trying + * to account for current list mode + */ +void EntryView::resetViewToDefaults() +{ + /* Reset state of 'Hide Usernames'/'Hide Passwords' settings */ + m_model->setHideUsernames(false); + m_model->setHidePasswords(true); + + /* Reset visibility, size and position of all columns */ + for (int colidx = 0; colidx < header()->count(); colidx++) { + header()->showSection(colidx); + header()->resizeSection(colidx, header()->defaultSectionSize()); + header()->moveSection(header()->visualIndex(colidx), colidx); + } + + /* Reenter current list mode (affects first column and sorting) */ + if (m_inEntryListMode) { + switchToEntryListMode(); + } + else { + switchToGroupMode(); + } + + /* Nicely fitting columns to window feels like a sane default */ + fitColumnsToWindow(); +} diff --git a/src/gui/entry/EntryView.h b/src/gui/entry/EntryView.h index 14c6b7ccc..2cadf0d82 100644 --- a/src/gui/entry/EntryView.h +++ b/src/gui/entry/EntryView.h @@ -26,6 +26,11 @@ class Entry; class EntryModel; class Group; class SortFilterHideProxyModel; +/** + * @author Fonic + * Add forward declaration for QActionGroup + */ +class QActionGroup; class EntryView : public QTreeView { @@ -59,10 +64,29 @@ private slots: void switchToEntryListMode(); void switchToGroupMode(); + /** + * @author Fonic + * Slots for header context menu and actions + */ + void showHeaderMenu(const QPoint& position); + void toggleColumnVisibility(QAction *action); + void fitColumnsToWindow(); + void fitColumnsToContents(); + void resetViewToDefaults(); + private: EntryModel* const m_model; SortFilterHideProxyModel* const m_sortModel; bool m_inEntryListMode; + + /** + * @author Fonic + * Properties to store header context menu and actions + */ + QMenu* m_headerMenu; + QAction* m_hideUsernamesAction; + QAction* m_hidePasswordsAction; + QActionGroup* m_columnActions; }; #endif // KEEPASSX_ENTRYVIEW_H