mirror of
https://github.com/subsurface/subsurface.git
synced 2024-11-28 05:00:20 +00:00
1e5191e33e
This was a user request: Sort bar charts by height of the bars. Obviously, this can only work for categorical charts, not for histograms. The UI is a break from the old concept: the sorting is chosen based on the chart, whereas for the rest of the features, the viable charts are presented based on the binning, etc. I found it confusing to have the possible charts be selected based on sorting. I.e. if a non-bin sort mode is selected, the histogram charts disappear. On the flip side, this would be more consistent. We can change it later. For value-based bar charts, there are three sort modes: by bin, by count (i.e. number of dives in that bar) and by value (i.e. length of the bar). This hopefully satisfies all needs. Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
248 lines
7.5 KiB
C++
248 lines
7.5 KiB
C++
// SPDX-License-Identifier: GPL-2.0
|
|
#include "statswidget.h"
|
|
#include "mainwindow.h"
|
|
#include "stats/statsview.h"
|
|
#include <QCheckBox>
|
|
#include <QPainter>
|
|
#include <QStyledItemDelegate>
|
|
|
|
class ChartItemDelegate : public QStyledItemDelegate {
|
|
private:
|
|
void paint(QPainter *painter, const QStyleOptionViewItem &option,
|
|
const QModelIndex &index) const override;
|
|
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
|
};
|
|
|
|
// Number of pixels that non-toplevel items are indented
|
|
static int indent(const QFontMetrics &fm)
|
|
{
|
|
#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0)
|
|
return 4 * fm.width('-');
|
|
#else
|
|
return 4 * fm.horizontalAdvance('-');
|
|
#endif
|
|
}
|
|
|
|
static const int iconSpace = 2; // Number of pixels between icon and text
|
|
static const int topSpace = 2; // Number of pixels above icon
|
|
|
|
void ChartItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
|
|
const QModelIndex &index) const
|
|
{
|
|
QFont font = index.data(Qt::FontRole).value<QFont>();
|
|
QFontMetrics fm(font);
|
|
QString name = index.data(ChartListModel::ChartNameRole).value<QString>();
|
|
painter->setFont(font);
|
|
QRect rect = option.rect;
|
|
if (option.state & QStyle::State_Selected) {
|
|
painter->save();
|
|
painter->setBrush(option.palette.highlight());
|
|
painter->setPen(Qt::NoPen);
|
|
painter->drawRect(rect);
|
|
painter->restore();
|
|
}
|
|
bool isHeader = index.data(ChartListModel::IsHeaderRole).value<bool>();
|
|
if (!isHeader)
|
|
rect.translate(indent(fm), 0);
|
|
QPixmap icon = index.data(ChartListModel::IconRole).value<QPixmap>();
|
|
if (!icon.isNull()) {
|
|
rect.translate(0, topSpace);
|
|
painter->drawPixmap(rect.topLeft(), icon);
|
|
rect.translate(icon.size().width() + iconSpace, (icon.size().height() - fm.height()) / 2);
|
|
}
|
|
|
|
painter->drawText(rect, name);
|
|
}
|
|
|
|
QSize ChartItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
|
|
{
|
|
QFont font = index.data(Qt::FontRole).value<QFont>();
|
|
QFontMetrics fm(font);
|
|
QString name = index.data(ChartListModel::ChartNameRole).value<QString>();
|
|
QSize iconSize = index.data(ChartListModel::IconSizeRole).value<QSize>();
|
|
QSize size = fm.size(Qt::TextSingleLine, name);
|
|
bool isHeader = index.data(ChartListModel::IsHeaderRole).value<bool>();
|
|
if (!isHeader)
|
|
size += QSize(indent(fm), 0);
|
|
if (iconSize.isValid())
|
|
size = QSize(size.width() + iconSize.width() + iconSpace,
|
|
std::max(size.height(), iconSize.height()) + 2 * topSpace);
|
|
return size;
|
|
}
|
|
|
|
static const QUrl urlStatsView = QUrl(QStringLiteral("qrc:/qml/statsview2.qml"));
|
|
StatsWidget::StatsWidget(QWidget *parent) : QWidget(parent)
|
|
{
|
|
ui.setupUi(this);
|
|
|
|
connect(ui.close, &QToolButton::clicked, this, &StatsWidget::closeStats);
|
|
|
|
ui.chartType->setModel(&charts);
|
|
connect(ui.chartType, QOverload<int>::of(&QComboBox::activated), this, &StatsWidget::chartTypeChanged);
|
|
connect(ui.var1, QOverload<int>::of(&QComboBox::activated), this, &StatsWidget::var1Changed);
|
|
connect(ui.var2, QOverload<int>::of(&QComboBox::activated), this, &StatsWidget::var2Changed);
|
|
connect(ui.var1Binner, QOverload<int>::of(&QComboBox::activated), this, &StatsWidget::var1BinnerChanged);
|
|
connect(ui.var2Binner, QOverload<int>::of(&QComboBox::activated), this, &StatsWidget::var2BinnerChanged);
|
|
connect(ui.var2Operation, QOverload<int>::of(&QComboBox::activated), this, &StatsWidget::var2OperationChanged);
|
|
connect(ui.var1Sort, QOverload<int>::of(&QComboBox::activated), this, &StatsWidget::var1SortChanged);
|
|
connect(ui.restrictButton, &QToolButton::clicked, this, &StatsWidget::restrict);
|
|
connect(ui.unrestrictButton, &QToolButton::clicked, this, &StatsWidget::unrestrict);
|
|
|
|
ui.stats->setSource(urlStatsView);
|
|
ui.stats->setResizeMode(QQuickWidget::SizeRootObjectToView);
|
|
QQuickItem *root = ui.stats->rootObject();
|
|
view = qobject_cast<StatsView *>(root);
|
|
if (!view)
|
|
qWarning("Oops. The root of the StatsView is not a StatsView.");
|
|
if (view)
|
|
view->setVisible(isVisible()); // Synchronize visibility of widget and QtQuick-view.
|
|
}
|
|
|
|
// Initialize QComboBox with list of variables
|
|
static void setVariableList(QComboBox *combo, const StatsState::VariableList &list)
|
|
{
|
|
combo->clear();
|
|
combo->setEnabled(!list.variables.empty());
|
|
for (const StatsState::Variable &v: list.variables)
|
|
combo->addItem(v.name, QVariant(v.id));
|
|
combo->setCurrentIndex(list.selected);
|
|
}
|
|
|
|
// Initialize QComboBox and QLabel of binners. Hide if there are no binners.
|
|
static void setBinList(QComboBox *combo, const StatsState::BinnerList &list)
|
|
{
|
|
combo->clear();
|
|
combo->setEnabled(!list.binners.empty());
|
|
|
|
for (const QString &s: list.binners)
|
|
combo->addItem(s);
|
|
combo->setCurrentIndex(list.selected);
|
|
}
|
|
|
|
void StatsWidget::updateUi()
|
|
{
|
|
StatsState::UIState uiState = state.getUIState();
|
|
setVariableList(ui.var1, uiState.var1);
|
|
setVariableList(ui.var2, uiState.var2);
|
|
int pos = charts.update(uiState.charts);
|
|
ui.chartType->setCurrentIndex(pos);
|
|
ui.chartType->setItemDelegate(new ChartItemDelegate);
|
|
setBinList(ui.var1Binner, uiState.binners1);
|
|
setBinList(ui.var2Binner, uiState.binners2);
|
|
setVariableList(ui.var2Operation, uiState.operations2);
|
|
setVariableList(ui.var1Sort, uiState.sortMode1);
|
|
ui.sortGroup->setVisible(!uiState.sortMode1.variables.empty());
|
|
|
|
// Add checkboxes for additional features
|
|
features.clear();
|
|
for (const StatsState::Feature &f: uiState.features) {
|
|
features.emplace_back(new QCheckBox(f.name));
|
|
QCheckBox *check = features.back().get();
|
|
check->setChecked(f.selected);
|
|
int id = f.id;
|
|
connect(check, &QCheckBox::stateChanged, [this,id] (int state) { featureChanged(id, state); });
|
|
ui.features->addWidget(check);
|
|
}
|
|
|
|
if (view)
|
|
view->plot(state);
|
|
}
|
|
|
|
void StatsWidget::updateRestrictionLabel()
|
|
{
|
|
if (!view)
|
|
return;
|
|
int num = view->restrictionCount();
|
|
if (num < 0)
|
|
ui.restrictionLabel->setText(tr("Analyzing all dives"));
|
|
else
|
|
ui.restrictionLabel->setText(tr("Analyzing subset (%L1) dives").arg(num));
|
|
ui.unrestrictButton->setEnabled(num > 0);
|
|
}
|
|
|
|
void StatsWidget::closeStats()
|
|
{
|
|
MainWindow::instance()->setApplicationState(MainWindow::ApplicationState::Default);
|
|
}
|
|
|
|
void StatsWidget::chartTypeChanged(int idx)
|
|
{
|
|
state.chartChanged(ui.chartType->itemData(idx).toInt());
|
|
updateUi();
|
|
}
|
|
|
|
void StatsWidget::var1Changed(int idx)
|
|
{
|
|
state.var1Changed(ui.var1->itemData(idx).toInt());
|
|
updateUi();
|
|
}
|
|
|
|
void StatsWidget::var2Changed(int idx)
|
|
{
|
|
state.var2Changed(ui.var2->itemData(idx).toInt());
|
|
updateUi();
|
|
}
|
|
|
|
void StatsWidget::var1BinnerChanged(int idx)
|
|
{
|
|
state.binner1Changed(idx);
|
|
updateUi();
|
|
}
|
|
|
|
void StatsWidget::var2BinnerChanged(int idx)
|
|
{
|
|
state.binner2Changed(idx);
|
|
updateUi();
|
|
}
|
|
|
|
void StatsWidget::var2OperationChanged(int idx)
|
|
{
|
|
state.var2OperationChanged(ui.var2Operation->itemData(idx).toInt());
|
|
updateUi();
|
|
}
|
|
|
|
void StatsWidget::var1SortChanged(int idx)
|
|
{
|
|
state.sortMode1Changed(ui.var1Sort->itemData(idx).toInt());
|
|
updateUi();
|
|
}
|
|
|
|
void StatsWidget::featureChanged(int idx, bool status)
|
|
{
|
|
state.featureChanged(idx, status);
|
|
// No need for a full chart replot - just show/hide the features
|
|
if (view)
|
|
view->updateFeatures(state);
|
|
}
|
|
|
|
void StatsWidget::showEvent(QShowEvent *e)
|
|
{
|
|
unrestrict();
|
|
updateUi();
|
|
QWidget::showEvent(e);
|
|
// Apparently, we have to manage the visibility of the view ourselves. That's mad.
|
|
if (view)
|
|
view->setVisible(true);
|
|
}
|
|
|
|
void StatsWidget::hideEvent(QHideEvent *e)
|
|
{
|
|
QWidget::hideEvent(e);
|
|
// Apparently, we have to manage the visibility of the view ourselves. That's mad.
|
|
if (view)
|
|
view->setVisible(false);
|
|
}
|
|
|
|
void StatsWidget::restrict()
|
|
{
|
|
if (view)
|
|
view->restrictToSelection();
|
|
updateRestrictionLabel();
|
|
}
|
|
|
|
void StatsWidget::unrestrict()
|
|
{
|
|
if (view)
|
|
view->unrestrict();
|
|
updateRestrictionLabel();
|
|
}
|