mirror of
				https://github.com/subsurface/subsurface.git
				synced 2025-02-19 22:16:15 +00:00 
			
		
		
		
	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();
 | |
| }
 |