From a15f81bc35ff8155eb2df942d1602b637f159290 Mon Sep 17 00:00:00 2001 From: Martin Kleusberg Date: Sun, 21 Jun 2020 13:53:25 +0200 Subject: [PATCH] Add new option to freeze columns in the Browse Data tab This adds a new context menu action called "Freeze Columns" to the context menu which appears when you right click the column headers in a Browse Data dock. With this action all columns from the first up to the clicked column are fixed in place when you scroll horizontally. This can be used to make for example the id column always visible. See issue #1888. --- src/ExtendedTableWidget.cpp | 200 +++++++++++++++++++++++++++++++++++- src/ExtendedTableWidget.h | 19 ++++ src/FilterTableHeader.cpp | 14 +-- src/FilterTableHeader.h | 3 +- src/MainWindow.cpp | 3 + src/TableBrowser.cpp | 48 +++++++-- src/TableBrowser.h | 5 +- src/TableBrowser.ui | 11 ++ 8 files changed, 282 insertions(+), 21 deletions(-) diff --git a/src/ExtendedTableWidget.cpp b/src/ExtendedTableWidget.cpp index c141813b..2199f195 100644 --- a/src/ExtendedTableWidget.cpp +++ b/src/ExtendedTableWidget.cpp @@ -224,7 +224,9 @@ void ExtendedTableWidgetEditorDelegate::updateEditorGeometry(QWidget* editor, co ExtendedTableWidget::ExtendedTableWidget(QWidget* parent) : - QTableView(parent) + QTableView(parent), + m_frozen_table_view(qobject_cast(parent) ? nullptr : new ExtendedTableWidget(this)), + m_frozen_column_count(0) { setHorizontalScrollMode(ExtendedTableWidget::ScrollPerPixel); // Force ScrollPerItem, so scrolling shows all table rows @@ -405,21 +407,81 @@ ExtendedTableWidget::ExtendedTableWidget(QWidget* parent) : selectionModel()->select(QItemSelection(selectionModel()->selectedIndexes().first(), selectionModel()->selectedIndexes().last()), QItemSelectionModel::Select | QItemSelectionModel::Rows); }); + // Set up frozen columns child widget + if(m_frozen_table_view) + { + // Set up widget + m_frozen_table_view->setFocusPolicy(Qt::NoFocus); + m_frozen_table_view->verticalHeader()->hide(); + m_frozen_table_view->setStyleSheet("QTableView { border: none; }" + "QTableView::item:selected{ background-color: " + palette().color(QPalette::Active, QPalette::Highlight).name() + "}"); + m_frozen_table_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_frozen_table_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_frozen_table_view->setVerticalScrollMode(verticalScrollMode()); + viewport()->stackUnder(m_frozen_table_view); + m_tableHeader->stackUnder(m_frozen_table_view); + + // Keep both widgets in sync + connect(horizontalHeader(), &QHeaderView::sectionResized, this, &ExtendedTableWidget::updateSectionWidth); + connect(m_frozen_table_view->horizontalHeader(), &QHeaderView::sectionResized, this, &ExtendedTableWidget::updateSectionWidth); + connect(verticalHeader(), &QHeaderView::sectionResized, this, &ExtendedTableWidget::updateSectionHeight); + connect(m_frozen_table_view->verticalHeader(), &QHeaderView::sectionResized, this, &ExtendedTableWidget::updateSectionHeight); + connect(m_frozen_table_view->verticalScrollBar(), &QAbstractSlider::valueChanged, verticalScrollBar(), &QAbstractSlider::setValue); + connect(verticalScrollBar(), &QAbstractSlider::valueChanged, m_frozen_table_view->verticalScrollBar(), &QAbstractSlider::setValue); + + // Forward signals from frozen table view widget to the main table view widget + connect(m_frozen_table_view, &ExtendedTableWidget::doubleClicked, this, &ExtendedTableWidget::doubleClicked); + connect(m_frozen_table_view->filterHeader(), &FilterTableHeader::sectionClicked, filterHeader(), &FilterTableHeader::sectionClicked); + connect(m_frozen_table_view->filterHeader(), &QHeaderView::sectionDoubleClicked, filterHeader(), &QHeaderView::sectionDoubleClicked); + connect(m_frozen_table_view->verticalHeader(), &QHeaderView::sectionResized, verticalHeader(), &QHeaderView::sectionResized); + connect(m_frozen_table_view->horizontalHeader(), &QHeaderView::customContextMenuRequested, horizontalHeader(), &QHeaderView::customContextMenuRequested); + connect(m_frozen_table_view->verticalHeader(), &QHeaderView::customContextMenuRequested, verticalHeader(), &QHeaderView::customContextMenuRequested); + connect(m_frozen_table_view, &ExtendedTableWidget::openFileFromDropEvent, this, &ExtendedTableWidget::openFileFromDropEvent); + connect(m_frozen_table_view, &ExtendedTableWidget::selectedRowsToBeDeleted, this, &ExtendedTableWidget::selectedRowsToBeDeleted); + connect(m_frozen_table_view->filterHeader(), &FilterTableHeader::filterChanged, filterHeader(), &FilterTableHeader::filterChanged); + connect(m_frozen_table_view->filterHeader(), &FilterTableHeader::addCondFormat, filterHeader(), &FilterTableHeader::addCondFormat); + connect(m_frozen_table_view->filterHeader(), &FilterTableHeader::allCondFormatsCleared, filterHeader(), &FilterTableHeader::allCondFormatsCleared); + connect(m_frozen_table_view->filterHeader(), &FilterTableHeader::condFormatsEdited, filterHeader(), &FilterTableHeader::condFormatsEdited); + connect(m_frozen_table_view, &ExtendedTableWidget::editCondFormats, this, &ExtendedTableWidget::editCondFormats); + connect(m_frozen_table_view, &ExtendedTableWidget::dataAboutToBeEdited, this, &ExtendedTableWidget::dataAboutToBeEdited); + connect(m_frozen_table_view, &ExtendedTableWidget::foreignKeyClicked, this, &ExtendedTableWidget::foreignKeyClicked); + connect(m_frozen_table_view, &ExtendedTableWidget::currentIndexChanged, this, &ExtendedTableWidget::currentIndexChanged); + } + #if QT_VERSION >= QT_VERSION_CHECK(5, 12, 0) && QT_VERSION < QT_VERSION_CHECK(5, 12, 3) // This work arounds QTBUG-73721 and it is applied only for the affected version range. setWordWrap(false); #endif } +ExtendedTableWidget::~ExtendedTableWidget() +{ + delete m_frozen_table_view; +} + +void ExtendedTableWidget::setModel(QAbstractItemModel* item_model) +{ + // Set model + QTableView::setModel(item_model); + + // Set up frozen table view widget + if(item_model) + setFrozenColumns(m_frozen_column_count); + else + m_frozen_table_view->hide(); +} + void ExtendedTableWidget::reloadSettings() { - // Set the new font and font size + // We only get the font here to get its metrics. The actual font for the view is set in the model QFont dataBrowserFont(Settings::getValue("databrowser", "font").toString()); dataBrowserFont.setPointSize(Settings::getValue("databrowser", "fontsize").toInt()); - setFont(dataBrowserFont); // Set new default row height depending on the font size - verticalHeader()->setDefaultSectionSize(verticalHeader()->fontMetrics().height()+10); + QFontMetrics fontMetrics(dataBrowserFont); + verticalHeader()->setDefaultSectionSize(fontMetrics.height()+10); + if(m_frozen_table_view) + m_frozen_table_view->reloadSettings(); } void ExtendedTableWidget::copyMimeData(const QModelIndexList& fromIndices, QMimeData* mimeData, const bool withHeaders, const bool inSQL) @@ -872,13 +934,21 @@ void ExtendedTableWidget::updateGeometries() // Call the parent implementation first - it does most of the actual logic QTableView::updateGeometries(); + // Update frozen columns view too + if(m_frozen_table_view) + m_frozen_table_view->updateGeometries(); + // Check if a model has already been set yet if(model()) { // If so and if it is a SqliteTableModel and if the parent implementation of this method decided that a scrollbar is needed, update its maximum value SqliteTableModel* m = qobject_cast(model()); if(m && verticalScrollBar()->maximum()) + { verticalScrollBar()->setMaximum(m->rowCount() - numVisibleRows() + 1); + if(m_frozen_table_view) + m_frozen_table_view->verticalScrollBar()->setMaximum(verticalScrollBar()->maximum()); + } } } @@ -1120,3 +1190,125 @@ void ExtendedTableWidget::setToNull(const QModelIndexList& indices) return; } } + +void ExtendedTableWidget::setFrozenColumns(size_t count) +{ + if(!m_frozen_table_view) + return; + + m_frozen_column_count = count; + + // Set up frozen table view widget + m_frozen_table_view->setModel(model()); + m_frozen_table_view->setSelectionModel(selectionModel()); + + // Only show frozen columns in extra table view and copy column widths + m_frozen_table_view->horizontalHeader()->blockSignals(true); // Signals need to be blocked because hiding a column would emit resizedSection + for(size_t col=0;col(model()->columnCount());++col) + m_frozen_table_view->setColumnHidden(static_cast(col), col >= count); + m_frozen_table_view->horizontalHeader()->blockSignals(false); + for(int col=0;col(count);++col) + m_frozen_table_view->setColumnWidth(col, columnWidth(col)); + + updateFrozenTableGeometry(); + + // Only show extra table view when there are frozen columns to see + if(count) + m_frozen_table_view->show(); + else + m_frozen_table_view->hide(); +} + +void ExtendedTableWidget::generateFilters(size_t number, bool show_rowid) +{ + m_tableHeader->generateFilters(number, m_frozen_column_count); + + if(m_frozen_table_view) + { + size_t frozen_columns = std::min(m_frozen_column_count, number); + m_frozen_table_view->m_tableHeader->generateFilters(frozen_columns, show_rowid ? 0 : 1); + } +} + +void ExtendedTableWidget::updateSectionWidth(int logicalIndex, int /* oldSize */, int newSize) +{ + if(!m_frozen_table_view) + return; + + if(logicalIndex < static_cast(m_frozen_column_count)) + { + m_frozen_table_view->setColumnWidth(logicalIndex, newSize); + setColumnWidth(logicalIndex, newSize); + updateFrozenTableGeometry(); + } +} + +void ExtendedTableWidget::updateSectionHeight(int logicalIndex, int /* oldSize */, int newSize) +{ + if(!m_frozen_table_view) + return; + + m_frozen_table_view->setRowHeight(logicalIndex, newSize); +} + +void ExtendedTableWidget::resizeEvent(QResizeEvent* event) +{ + QTableView::resizeEvent(event); + updateFrozenTableGeometry(); +} + +QModelIndex ExtendedTableWidget::moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers) +{ + QModelIndex current = QTableView::moveCursor(cursorAction, modifiers); + if(!m_frozen_table_view) + return current; + + int width = 0; + for(int i=0;i(m_frozen_column_count);i++) + width += m_frozen_table_view->columnWidth(i); + + if(cursorAction == MoveLeft && current.column() > 0 && visualRect(current).topLeft().x() < width) + { + const int newValue = horizontalScrollBar()->value() + visualRect(current).topLeft().x() - width; + horizontalScrollBar()->setValue(newValue); + } + return current; +} + +void ExtendedTableWidget::scrollTo(const QModelIndex& index, ScrollHint hint) +{ + if(index.column() >= static_cast(m_frozen_column_count)) + QTableView::scrollTo(index, hint); +} + +void ExtendedTableWidget::updateFrozenTableGeometry() +{ + if(!m_frozen_table_view) + return; + + int width = 0; + for(int i=0;i(m_frozen_column_count);i++) + { + if(!isColumnHidden(i)) + width += columnWidth(i); + } + + m_frozen_table_view->setGeometry(verticalHeader()->width() + frameWidth(), + frameWidth(), + width, + viewport()->height() + horizontalHeader()->height()); +} + +void ExtendedTableWidget::setEditTriggers(QAbstractItemView::EditTriggers editTriggers) +{ + QTableView::setEditTriggers(editTriggers); + if(m_frozen_table_view) + m_frozen_table_view->setEditTriggers(editTriggers); +} + +void ExtendedTableWidget::setFilter(size_t column, const QString& value) +{ + filterHeader()->setFilter(column, value); + if(m_frozen_table_view) + m_frozen_table_view->filterHeader()->setFilter(column, value); +} diff --git a/src/ExtendedTableWidget.h b/src/ExtendedTableWidget.h index dea8cc3d..b3a3d99d 100644 --- a/src/ExtendedTableWidget.h +++ b/src/ExtendedTableWidget.h @@ -51,8 +51,10 @@ class ExtendedTableWidget : public QTableView public: explicit ExtendedTableWidget(QWidget* parent = nullptr); + ~ExtendedTableWidget() override; FilterTableHeader* filterHeader() { return m_tableHeader; } + void generateFilters(size_t number, bool show_rowid); public: // Get set of selected columns (all cells in column has to be selected) @@ -64,12 +66,19 @@ public: void sortByColumns(const std::vector& columns); + void setFrozenColumns(size_t count); + + void setModel(QAbstractItemModel* item_model) override; + + void setEditTriggers(QAbstractItemView::EditTriggers editTriggers); + public slots: void reloadSettings(); void selectTableLine(int lineToSelect); void selectTableLines(int firstLine, int count); void selectAll() override; void openPrintDialog(); + void setFilter(size_t column, const QString& value); signals: void foreignKeyClicked(const sqlb::ObjectIdentifier& table, const std::string& column, const QByteArray& value); @@ -95,9 +104,15 @@ private: static std::vector> m_buffer; static QString m_generatorStamp; + ExtendedTableWidget* m_frozen_table_view; + size_t m_frozen_column_count; + void updateFrozenTableGeometry(); + private slots: void vscrollbarChanged(int value); void cellClicked(const QModelIndex& index); + void updateSectionWidth(int logicalIndex, int oldSize, int newSize); + void updateSectionHeight(int logicalIndex, int oldSize, int newSize); protected: void keyPressEvent(QKeyEvent* event) override; @@ -107,6 +122,10 @@ protected: void dropEvent(QDropEvent* event) override; void currentChanged(const QModelIndex ¤t, const QModelIndex &previous) override; + void resizeEvent(QResizeEvent* event) override; + QModelIndex moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers) override; + void scrollTo(const QModelIndex& index, ScrollHint hint = EnsureVisible) override; + FilterTableHeader* m_tableHeader; QMenu* m_contextMenu; ExtendedTableWidgetEditorDelegate* m_editorDelegate; diff --git a/src/FilterTableHeader.cpp b/src/FilterTableHeader.cpp index 0149e39e..e4d4fc8f 100644 --- a/src/FilterTableHeader.cpp +++ b/src/FilterTableHeader.cpp @@ -29,7 +29,7 @@ FilterTableHeader::FilterTableHeader(QTableView* parent) : setContextMenuPolicy(Qt::CustomContextMenu); } -void FilterTableHeader::generateFilters(size_t number, bool showFirst) +void FilterTableHeader::generateFilters(size_t number, size_t number_of_hidden_filters) { // Delete all the current filter widgets qDeleteAll(filterWidgets); @@ -39,10 +39,7 @@ void FilterTableHeader::generateFilters(size_t number, bool showFirst) for(size_t i=0;i < number; ++i) { FilterLineEdit* l = new FilterLineEdit(this, &filterWidgets, i); - if(!showFirst && i == 0) // This hides the first input widget which belongs to the hidden rowid column - l->setVisible(false); - else - l->setVisible(true); + l->setVisible(i >= number_of_hidden_filters); connect(l, &FilterLineEdit::delayedTextChanged, this, &FilterTableHeader::inputChanged); connect(l, &FilterLineEdit::addFilterAsCondFormat, this, &FilterTableHeader::addFilterAsCondFormat); connect(l, &FilterLineEdit::clearAllCondFormats, this, &FilterTableHeader::clearAllCondFormats); @@ -51,7 +48,7 @@ void FilterTableHeader::generateFilters(size_t number, bool showFirst) } // Position them correctly - adjustPositions(); + updateGeometries(); } QSize FilterTableHeader::sizeHint() const @@ -128,3 +125,8 @@ void FilterTableHeader::setFilter(size_t column, const QString& value) if(column < filterWidgets.size()) filterWidgets.at(column)->setText(value); } + +QString FilterTableHeader::filterValue(size_t column) const +{ + return filterWidgets[column]->text(); +} diff --git a/src/FilterTableHeader.h b/src/FilterTableHeader.h index ef64eaf2..0f32f561 100644 --- a/src/FilterTableHeader.h +++ b/src/FilterTableHeader.h @@ -15,9 +15,10 @@ public: explicit FilterTableHeader(QTableView* parent = nullptr); QSize sizeHint() const override; bool hasFilters() const {return (filterWidgets.size() > 0);} + QString filterValue(size_t column) const; public slots: - void generateFilters(size_t number, bool showFirst = false); + void generateFilters(size_t number, size_t number_of_hidden_filters = 1); void adjustPositions(); void clearFilters(); void setFilter(size_t column, const QString& value); diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 8f6d1ed1..a6b41a68 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -2432,6 +2432,8 @@ static void loadBrowseDataTableSettings(BrowseDataTableSettings& settings, QXmlS settings.encoding = xml.attributes().value("encoding").toString(); settings.plotXAxis = xml.attributes().value("plot_x_axis").toString(); settings.unlockViewPk = xml.attributes().value("unlock_view_pk").toString(); + if(xml.attributes().hasAttribute("freeze_columns")) + settings.frozenColumns = xml.attributes().value("freeze_columns").toUInt(); while(xml.readNext() != QXmlStreamReader::EndElement && xml.name() != "table") { if(xml.name() == "sort") @@ -2820,6 +2822,7 @@ static void saveBrowseDataTableSettings(const BrowseDataTableSettings& object, Q xml.writeAttribute("encoding", object.encoding); xml.writeAttribute("plot_x_axis", object.plotXAxis); xml.writeAttribute("unlock_view_pk", object.unlockViewPk); + xml.writeAttribute("freeze_columns", QString::number(object.frozenColumns)); xml.writeStartElement("sort"); for(const auto& column : object.sortColumns) diff --git a/src/TableBrowser.cpp b/src/TableBrowser.cpp index cd1fe53d..0a27e429 100644 --- a/src/TableBrowser.cpp +++ b/src/TableBrowser.cpp @@ -56,6 +56,7 @@ TableBrowser::TableBrowser(DBBrowserDB* _db, QWidget* parent) : popupHeaderMenu = new QMenu(this); popupHeaderMenu->addAction(ui->actionShowRowidColumn); + popupHeaderMenu->addAction(ui->actionFreezeColumns); popupHeaderMenu->addAction(ui->actionHideColumns); popupHeaderMenu->addAction(ui->actionShowAllColumns); popupHeaderMenu->addAction(ui->actionSelectColumn); @@ -68,7 +69,13 @@ TableBrowser::TableBrowser(DBBrowserDB* _db, QWidget* parent) : connect(ui->actionSelectColumn, &QAction::triggered, [this]() { ui->dataTable->selectColumn(ui->actionBrowseTableEditDisplayFormat->property("clicked_column").toInt()); - }); + }); + connect(ui->actionFreezeColumns, &QAction::triggered, [this](bool checked) { + if(checked) + freezeColumns(ui->actionBrowseTableEditDisplayFormat->property("clicked_column").toUInt() + 1); + else + freezeColumns(0); + }); // Set up shortcuts QShortcut* dittoRecordShortcut = new QShortcut(QKeySequence("Ctrl+\""), this); @@ -728,7 +735,7 @@ void TableBrowser::updateRecordsetLabel() generateFilters(); ui->dataTable->adjustSize(); } else if(!needs_filters && header->hasFilters()) { - header->generateFilters(0); + ui->dataTable->generateFilters(0, false); } } } @@ -836,6 +843,9 @@ void TableBrowser::applyViewportSettings(const BrowseDataTableSettings& storedDa ui->actionUnlockViewEditing->setVisible(true); ui->actionShowRowidColumn->setVisible(false); } + + // Frozen columns + freezeColumns(storedData.frozenColumns); } void TableBrowser::enableEditing(bool enable_edit) @@ -850,8 +860,8 @@ void TableBrowser::enableEditing(bool enable_edit) void TableBrowser::showRowidColumn(bool show) { - // Block all signals from the horizontal header. Otherwise the QHeaderView::sectionResized signal causes us trouble - ui->dataTable->horizontalHeader()->blockSignals(true); + // Disconnect the resized signal from the horizontal header. Otherwise it's resetting the automatic column widths + disconnect(ui->dataTable->horizontalHeader(), &QHeaderView::sectionResized, this, &TableBrowser::updateColumnWidth); // WORKAROUND // Set the opposite hidden/visible status of what we actually want for the rowid column. This is to work around a Qt bug which @@ -864,6 +874,8 @@ void TableBrowser::showRowidColumn(bool show) // Show/hide rowid column ui->dataTable->setColumnHidden(0, !show); + if(show) + ui->dataTable->setColumnWidth(0, ui->dataTable->horizontalHeader()->defaultSectionSize()); // Update checked status of the popup menu action ui->actionShowRowidColumn->setChecked(show); @@ -878,25 +890,43 @@ void TableBrowser::showRowidColumn(bool show) // Update the filter row generateFilters(); - // Re-enable signals - ui->dataTable->horizontalHeader()->blockSignals(false); + // Re-enable signal + connect(ui->dataTable->horizontalHeader(), &QHeaderView::sectionResized, this, &TableBrowser::updateColumnWidth); ui->dataTable->update(); } +void TableBrowser::freezeColumns(size_t columns) +{ + // Update checked status of the popup menu action + ui->actionFreezeColumns->setChecked(columns != 0); + + // Save settings for this table + sqlb::ObjectIdentifier current_table = currentlyBrowsedTableName(); + if (m_settings[current_table].frozenColumns != columns) { + emit projectModified(); + m_settings[current_table].frozenColumns = columns; + } + + // Apply settings + ui->dataTable->horizontalHeader()->blockSignals(true); + ui->dataTable->setFrozenColumns(columns); + generateFilters(); + ui->dataTable->horizontalHeader()->blockSignals(false); +} + void TableBrowser::generateFilters() { // Generate a new row of filter line edits const auto& settings = m_settings[currentlyBrowsedTableName()]; - qobject_cast(ui->dataTable->horizontalHeader())->generateFilters(static_cast(m_model->columnCount()), - settings.showRowid); + ui->dataTable->generateFilters(static_cast(m_model->columnCount()), settings.showRowid); // Apply the stored filter strings to the new row of line edits // Set filters blocking signals for this since the filter is already applied to the browse table model FilterTableHeader* filterHeader = qobject_cast(ui->dataTable->horizontalHeader()); bool oldState = filterHeader->blockSignals(true); for(auto filterIt=settings.filterValues.cbegin();filterIt!=settings.filterValues.cend();++filterIt) - filterHeader->setFilter(static_cast(filterIt->first), filterIt->second); + ui->dataTable->setFilter(static_cast(filterIt->first), filterIt->second); filterHeader->blockSignals(oldState); } diff --git a/src/TableBrowser.h b/src/TableBrowser.h index a029c53f..daebe4f3 100644 --- a/src/TableBrowser.h +++ b/src/TableBrowser.h @@ -40,11 +40,13 @@ struct BrowseDataTableSettings QString unlockViewPk; std::map hiddenColumns; std::vector globalFilters; + size_t frozenColumns; BrowseDataTableSettings() : showRowid(false), plotYAxes({std::map(), std::map()}), - unlockViewPk("_rowid_") + unlockViewPk("_rowid_"), + frozenColumns(0) { } }; @@ -105,6 +107,7 @@ private slots: void editCondFormats(size_t column); void enableEditing(bool enable_edit); void showRowidColumn(bool show); + void freezeColumns(size_t columns); void unlockViewEditing(bool unlock, QString pk = QString()); void hideColumns(int column = -1, bool hide = true); void on_actionShowAllColumns_triggered(); diff --git a/src/TableBrowser.ui b/src/TableBrowser.ui index 7b929e70..dd351d83 100644 --- a/src/TableBrowser.ui +++ b/src/TableBrowser.ui @@ -1042,6 +1042,17 @@ Replace text in cells + + + true + + + Freeze columns + + + Make all columns from the first column up to this column not move when scrolling horizontally + +