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
This commit is contained in:
Fonic 2017-12-21 10:01:47 +01:00
parent 268035ff9e
commit 31d73626e5
4 changed files with 313 additions and 5 deletions

View File

@ -578,3 +578,21 @@ void EntryModel::setHidePasswords(const bool hide)
m_hidePasswords = hide;
emit hidePasswordsChanged();
}
/**
* @author Fonic <https://github.com/fonic>
* Toggle state of 'Hide Usernames' setting
*/
void EntryModel::toggleHideUsernames(const bool hide)
{
setHideUsernames(hide);
}
/**
* @author Fonic <https://github.com/fonic>
* Toggle state of 'Hide Passwords' setting
*/
void EntryModel::toggleHidePasswords(const bool hide)
{
setHidePasswords(hide);
}

View File

@ -77,6 +77,7 @@ public:
signals:
void switchedToEntryListMode();
void switchedToGroupMode();
/**
* @author Fonic <https://github.com/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 <https://github.com/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);

View File

@ -19,9 +19,26 @@
#include <QHeaderView>
#include <QKeyEvent>
/**
* @author Fonic <https://github.com/fonic>
* Add include required for header context menu
*/
#include <QMenu>
#include "gui/SortFilterHideProxyModel.h"
/**
* @author Fonic <https://github.com/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 <https://github.com/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 <https://github.com/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 <https://github.com/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 <https://github.com/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 <https://github.com/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::<column>)
*/
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 <https://github.com/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 <https://github.com/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::<column>)
*/
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 <https://github.com/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<QMetaType::Type>(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 <https://github.com/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<QMetaType::Type>(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 <https://github.com/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 <https://github.com/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 <https://github.com/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();
}

View File

@ -26,6 +26,11 @@ class Entry;
class EntryModel;
class Group;
class SortFilterHideProxyModel;
/**
* @author Fonic <https://github.com/fonic>
* Add forward declaration for QActionGroup
*/
class QActionGroup;
class EntryView : public QTreeView
{
@ -59,10 +64,29 @@ private slots:
void switchToEntryListMode();
void switchToGroupMode();
/**
* @author Fonic <https://github.com/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 <https://github.com/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