From: Soeren Apel <redacted>
Date: Sun, 5 Jan 2020 19:44:37 +0000 (+0100)
Subject: DecodeTrace: Add FlowLayout and integrate it
X-Git-Url: https://sigrok.org/gitaction?a=commitdiff_plain;h=adf9e02242336548a384c23dadc062af7a2ef83a;p=pulseview.git

DecodeTrace: Add FlowLayout and integrate it
---

diff --git a/CMakeLists.txt b/CMakeLists.txt
index fbfb5eb1..3770f1e4 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -299,6 +299,7 @@ set(pulseview_SOURCES
 	pv/widgets/colorpopup.cpp
 	pv/widgets/devicetoolbutton.cpp
 	pv/widgets/exportmenu.cpp
+	pv/widgets/flowlayout.cpp
 	pv/widgets/importmenu.cpp
 	pv/widgets/popup.cpp
 	pv/widgets/popuptoolbutton.cpp
@@ -358,6 +359,7 @@ set(pulseview_HEADERS
 	pv/widgets/colorpopup.hpp
 	pv/widgets/devicetoolbutton.hpp
 	pv/widgets/exportmenu.hpp
+	pv/widgets/flowlayout.hpp
 	pv/widgets/importmenu.hpp
 	pv/widgets/popup.hpp
 	pv/widgets/popuptoolbutton.hpp
diff --git a/pv/views/trace/decodetrace.cpp b/pv/views/trace/decodetrace.cpp
index b1084f84..0715981c 100644
--- a/pv/views/trace/decodetrace.cpp
+++ b/pv/views/trace/decodetrace.cpp
@@ -58,6 +58,7 @@ extern "C" {
 #include <pv/data/logicsegment.hpp>
 #include <pv/widgets/decodergroupbox.hpp>
 #include <pv/widgets/decodermenu.hpp>
+#include <pv/widgets/flowlayout.hpp>
 
 using std::abs;
 using std::find_if;
@@ -99,6 +100,52 @@ const int DecodeTrace::DrawPadding = 100;
 const int DecodeTrace::MaxTraceUpdateRate = 1; // No more than 1 Hz
 const unsigned int DecodeTrace::AnimationDurationInTicks = 7;
 
+
+/**
+ * Helper function for forceUpdate()
+ */
+void invalidateLayout(QLayout* layout)
+{
+	// Recompute the given layout and all its child layouts recursively
+	for (int i = 0; i < layout->count(); i++) {
+		QLayoutItem *item = layout->itemAt(i);
+
+		if (item->layout())
+			invalidateLayout(item->layout());
+		else
+			item->invalidate();
+	}
+
+	layout->invalidate();
+	layout->activate();
+}
+
+void forceUpdate(QWidget* widget)
+{
+	// Update all child widgets recursively
+	for (QObject* child : widget->children())
+		if (child->isWidgetType())
+			forceUpdate((QWidget*)child);
+
+	// Invalidate the layout of the widget itself
+	if (widget->layout())
+		invalidateLayout(widget->layout());
+}
+
+
+ContainerWidget::ContainerWidget(QWidget *parent) :
+	QWidget(parent)
+{
+}
+
+void ContainerWidget::resizeEvent(QResizeEvent* event)
+{
+	QWidget::resizeEvent(event);
+
+	widgetResized(this);
+}
+
+
 DecodeTrace::DecodeTrace(pv::Session &session,
 	shared_ptr<data::SignalBase> signalbase, int index) :
 	Trace(signalbase),
@@ -582,6 +629,9 @@ void DecodeTrace::hover_point_changed(const QPoint &hp)
 
 void DecodeTrace::mouse_left_press_event(const QMouseEvent* event)
 {
+	// Update container widths which depend on the scrollarea's current width
+	update_expanded_rows();
+
 	// Handle row expansion marker
 	for (DecodeTraceRow& r : rows_) {
 		if (!r.expand_marker_highlighted)
@@ -599,15 +649,18 @@ void DecodeTrace::mouse_left_press_event(const QMouseEvent* event)
 			} else {
 				r.expanding = true;
 				r.anim_shape = 0;
+
+				// Force geometry update of the widget container to get
+				// an up-to-date height (which also depends on the width)
+				forceUpdate(r.container);
+
 				r.container->setVisible(true);
-				QApplication::processEvents();
-				r.expanded_height = 5 * default_row_height_ + r.container->size().height();
+				r.expanded_height = 2 * default_row_height_ + r.container->sizeHint().height();
 			}
 
 			r.animation_step = 0;
 			r.anim_height = r.height;
 
-			update_expanded_rows();
 			animation_timer_.start();
 		}
 	}
@@ -1181,6 +1234,7 @@ void DecodeTrace::export_annotations(vector<const Annotation*> *annotations) con
 
 void DecodeTrace::initialize_row_widgets(DecodeTraceRow* r, unsigned int row_id)
 {
+	// Set colors and fixed widths
 	QFontMetrics m(QApplication::font());
 
 	QPalette header_palette = owner_->view()->palette();
@@ -1201,9 +1255,9 @@ void DecodeTrace::initialize_row_widgets(DecodeTraceRow* r, unsigned int row_id)
 	const int w = m.boundingRect(r->decode_row->title()).width() + RowTitleMargin;
 	r->title_width = w;
 
-	r->container->resize(owner_->view()->viewport()->width() - r->container->pos().x(),
-		r->expanded_height - 2 * default_row_height_);
-	r->container->setVisible(false);
+	// Set up top-level container
+	connect(r->container, SIGNAL(widgetResized(QWidget*)),
+		this, SLOT(on_row_container_resized(QWidget*)));
 
 	QVBoxLayout* vlayout = new QVBoxLayout();
 	r->container->setLayout(vlayout);
@@ -1231,9 +1285,8 @@ void DecodeTrace::initialize_row_widgets(DecodeTraceRow* r, unsigned int row_id)
 
 	// Add selector container
 	vlayout->addWidget(r->selector_container);
-	r->selector_container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
-	r->selector_container->setMinimumSize(0, 3 * default_row_height_);                            // FIXME
-	r->selector_container->setLayout(new QHBoxLayout());
+	r->selector_container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+	r->selector_container->setLayout(new FlowLayout(r->selector_container));
 
 	r->selector_container->setAutoFillBackground(true);
 	r->selector_container->setPalette(selector_palette);
@@ -1289,7 +1342,7 @@ void DecodeTrace::update_rows()
 			nr.expanded = false;
 			nr.collapsing = false;
 			nr.expand_marker_shape = default_marker_shape_;
-			nr.container = new QWidget(owner_->view()->scrollarea());
+			nr.container = new ContainerWidget(owner_->view()->scrollarea());
 			nr.header_container = new QWidget(nr.container);
 			nr.selector_container = new QWidget(nr.container);
 
@@ -1365,11 +1418,23 @@ void DecodeTrace::set_row_collapsed(DecodeTraceRow* r)
 void DecodeTrace::update_expanded_rows()
 {
 	for (DecodeTraceRow& r : rows_) {
-		r.container->move(2 * ArrowSize,
-			get_row_y(&r) + default_row_height_);
-
-		r.container->resize(owner_->view()->viewport()->width() - r.container->pos().x(),
-			r.height - 2 * default_row_height_);
+		if (r.expanding || r.expanded)
+			r.expanded_height = 2 * default_row_height_ + r.container->sizeHint().height();
+
+		if (r.expanded)
+			r.height = r.expanded_height;
+
+		int x = 2 * ArrowSize;
+		int y = get_row_y(&r) + default_row_height_;
+		// Only update the position if it actually changes
+		if ((x != r.container->pos().x()) || (y != r.container->pos().y()))
+			r.container->move(x, y);
+
+		int w = owner_->view()->viewport()->width() - x;
+		int h = r.height - 2 * default_row_height_;
+		// Only update the dimension if they actually change
+		if ((w != r.container->sizeHint().width()) || (h != r.container->sizeHint().height()))
+			r.container->resize(w, h);
 	}
 }
 
@@ -1523,6 +1588,14 @@ void DecodeTrace::on_show_hide_class(QWidget* sender)
 	owner_->row_item_appearance_changed(false, true);
 }
 
+void DecodeTrace::on_row_container_resized(QWidget* sender)
+{
+	sender->update();
+
+	owner_->extents_changed(false, true);
+	owner_->row_item_appearance_changed(false, true);
+}
+
 void DecodeTrace::on_copy_annotation_to_clipboard()
 {
 	if (!selected_row_)
diff --git a/pv/views/trace/decodetrace.hpp b/pv/views/trace/decodetrace.hpp
index 5c756b26..9877dc11 100644
--- a/pv/views/trace/decodetrace.hpp
+++ b/pv/views/trace/decodetrace.hpp
@@ -81,6 +81,8 @@ class DecoderGroupBox;
 namespace views {
 namespace trace {
 
+class ContainerWidget;
+
 struct DecodeTraceRow {
 	// When adding a field, make sure it's initialized properly in
 	// DecodeTrace::update_rows()
@@ -92,7 +94,7 @@ struct DecodeTraceRow {
 	QPolygon expand_marker_shape;
 	float anim_height, anim_shape;
 
-	QWidget* container;
+	ContainerWidget* container;
 	QWidget* header_container;
 	QWidget* selector_container;
 	vector<QCheckBox*> selectors;
@@ -101,6 +103,19 @@ struct DecodeTraceRow {
 	map<uint32_t, QColor> ann_class_color;
 };
 
+class ContainerWidget : public QWidget
+{
+	Q_OBJECT
+
+public:
+	ContainerWidget(QWidget *parent = nullptr);
+
+	virtual void resizeEvent(QResizeEvent* event);
+
+Q_SIGNALS:
+	void widgetResized(QWidget* sender);
+};
+
 class DecodeTrace : public Trace
 {
 	Q_OBJECT
@@ -259,6 +274,7 @@ private Q_SLOTS:
 	void on_show_hide_decoder(int index);
 	void on_show_hide_row(int row_id);
 	void on_show_hide_class(QWidget* sender);
+	void on_row_container_resized(QWidget* sender);
 
 	void on_copy_annotation_to_clipboard();
 
diff --git a/pv/widgets/flowlayout.cpp b/pv/widgets/flowlayout.cpp
new file mode 100644
index 00000000..31ba7fed
--- /dev/null
+++ b/pv/widgets/flowlayout.cpp
@@ -0,0 +1,212 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2015 The Qt Company Ltd.
+ ** Contact: http://www.qt.io/licensing/
+ **
+ ** This file is part of the examples of the Qt Toolkit.
+ **
+ ** $QT_BEGIN_LICENSE:BSD$
+ ** You may use this file under the terms of the BSD license as follows:
+ **
+ ** "Redistribution and use in source and binary forms, with or without
+ ** modification, are permitted provided that the following conditions are
+ ** met:
+ **   * Redistributions of source code must retain the above copyright
+ **     notice, this list of conditions and the following disclaimer.
+ **   * Redistributions in binary form must reproduce the above copyright
+ **     notice, this list of conditions and the following disclaimer in
+ **     the documentation and/or other materials provided with the
+ **     distribution.
+ **   * Neither the name of The Qt Company Ltd nor the names of its
+ **     contributors may be used to endorse or promote products derived
+ **     from this software without specific prior written permission.
+ **
+ **
+ ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+ **
+ ** $QT_END_LICENSE$
+ **
+ ****************************************************************************/
+
+#include <QWidget>
+
+#include "flowlayout.hpp"
+
+FlowLayout::FlowLayout(QWidget *parent, int margin, int hSpacing, int vSpacing) :
+	QLayout(parent),
+	m_parent(parent),
+	m_hSpace(hSpacing),
+	m_vSpace(vSpacing)
+{
+	setContentsMargins(margin, margin, margin, margin);
+}
+
+FlowLayout::FlowLayout(int margin, int hSpacing, int vSpacing) :
+	m_parent(nullptr),
+	m_hSpace(hSpacing),
+	m_vSpace(vSpacing)
+{
+	setContentsMargins(margin, margin, margin, margin);
+}
+
+FlowLayout::~FlowLayout()
+{
+	QLayoutItem *item;
+	while ((item = takeAt(0)))
+		delete item;
+}
+
+void FlowLayout::addItem(QLayoutItem *item)
+{
+	itemList.append(item);
+}
+
+int FlowLayout::horizontalSpacing() const
+{
+	if (m_hSpace >= 0)
+		return m_hSpace;
+	else
+		return smartSpacing(QStyle::PM_LayoutHorizontalSpacing);
+}
+
+int FlowLayout::verticalSpacing() const
+{
+	if (m_vSpace >= 0)
+		return m_vSpace;
+	else
+		return smartSpacing(QStyle::PM_LayoutVerticalSpacing);
+}
+
+int FlowLayout::count() const
+{
+	return itemList.size();
+}
+
+QLayoutItem *FlowLayout::itemAt(int index) const
+{
+	return itemList.value(index);
+}
+
+QLayoutItem *FlowLayout::takeAt(int index)
+{
+	if ((index >= 0) && (index < itemList.size()))
+		return itemList.takeAt(index);
+	else
+		return 0;
+}
+
+Qt::Orientations FlowLayout::expandingDirections() const
+{
+	return Qt::Horizontal | Qt::Vertical;
+}
+
+bool FlowLayout::hasHeightForWidth() const
+{
+	return true;
+}
+
+int FlowLayout::heightForWidth(int width) const
+{
+	int height = doLayout(QRect(0, 0, width, 0), true);
+	return height;
+}
+
+void FlowLayout::setGeometry(const QRect &rect)
+{
+	QLayout::setGeometry(rect);
+	doLayout(rect, false);
+}
+
+QSize FlowLayout::sizeHint() const
+{
+	return minimumSize();
+}
+
+QSize FlowLayout::minimumSize() const
+{
+	QSize size(0, 0);
+
+	for (QLayoutItem* item : itemList) {
+		int w = item->geometry().x() + item->geometry().width();
+		if (w > size.width())
+			size.setWidth(w);
+
+		int h = item->geometry().y() + item->geometry().width();
+		if (h > size.height())
+			size.setHeight(h);
+	}
+
+	size += QSize(2 * margin(), 2 * margin());
+
+	return size;
+}
+
+int FlowLayout::doLayout(const QRect &rect, bool testOnly) const
+{
+	int left, top, right, bottom;
+	getContentsMargins(&left, &top, &right, &bottom);
+
+	QRect effectiveRect = rect.adjusted(left, top, -right, -bottom);
+	int x = effectiveRect.x();
+	int y = effectiveRect.y();
+
+	int lineHeight = 0;
+	for (QLayoutItem* item : itemList) {
+		QWidget* w = item->widget();
+
+		int spaceX = horizontalSpacing();
+		if (spaceX == -1)
+			spaceX = w->style()->layoutSpacing(
+				QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Horizontal);
+
+		int spaceY = verticalSpacing();
+		if (spaceY == -1)
+			spaceY = w->style()->layoutSpacing(
+				QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Vertical);
+
+		int nextX = x + item->sizeHint().width() + spaceX;
+		if (((nextX - spaceX) > effectiveRect.right()) && (lineHeight > 0)) {
+			x = effectiveRect.x();
+			y = y + lineHeight + spaceY;
+			nextX = x + item->sizeHint().width() + spaceX;
+			lineHeight = 0;
+		}
+
+		if (!testOnly)
+			item->setGeometry(QRect(QPoint(x, y), item->sizeHint()));
+
+		x = nextX;
+		lineHeight = qMax(lineHeight, item->sizeHint().height());
+	}
+
+	int height = y + lineHeight - rect.y() + bottom;
+
+	if (m_parent)
+		m_parent->setMinimumHeight(height);
+
+	return height;
+}
+
+int FlowLayout::smartSpacing(QStyle::PixelMetric pm) const
+{
+	QObject *parent = this->parent();
+
+	if (!parent)
+		return -1;
+
+	if (parent->isWidgetType()) {
+		QWidget *pw = qobject_cast<QWidget*>(parent);
+		return pw->style()->pixelMetric(pm, 0, pw);
+	} else
+		return static_cast<QLayout*>(parent)->spacing();
+}
diff --git a/pv/widgets/flowlayout.hpp b/pv/widgets/flowlayout.hpp
new file mode 100644
index 00000000..81407d1b
--- /dev/null
+++ b/pv/widgets/flowlayout.hpp
@@ -0,0 +1,78 @@
+/****************************************************************************
+ **
+ ** Copyright (C) 2015 The Qt Company Ltd.
+ ** Contact: http://www.qt.io/licensing/
+ **
+ ** This file is part of the examples of the Qt Toolkit.
+ **
+ ** $QT_BEGIN_LICENSE:BSD$
+ ** You may use this file under the terms of the BSD license as follows:
+ **
+ ** "Redistribution and use in source and binary forms, with or without
+ ** modification, are permitted provided that the following conditions are
+ ** met:
+ **   * Redistributions of source code must retain the above copyright
+ **     notice, this list of conditions and the following disclaimer.
+ **   * Redistributions in binary form must reproduce the above copyright
+ **     notice, this list of conditions and the following disclaimer in
+ **     the documentation and/or other materials provided with the
+ **     distribution.
+ **   * Neither the name of The Qt Company Ltd nor the names of its
+ **     contributors may be used to endorse or promote products derived
+ **     from this software without specific prior written permission.
+ **
+ **
+ ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
+ **
+ ** $QT_END_LICENSE$
+ **
+ ****************************************************************************/
+
+#ifndef FLOWLAYOUT_H
+#define FLOWLAYOUT_H
+
+#include <QLayout>
+#include <QRect>
+#include <QStyle>
+#include <QWidgetItem>
+
+class FlowLayout : public QLayout
+{
+public:
+	FlowLayout(QWidget *parent, int margin = -1, int hSpacing = -1, int vSpacing = -1);
+	FlowLayout(int margin = -1, int hSpacing = -1, int vSpacing = -1);
+	~FlowLayout();
+
+	void addItem(QLayoutItem *item);
+	int horizontalSpacing() const;
+	int verticalSpacing() const;
+	Qt::Orientations expandingDirections() const;
+	bool hasHeightForWidth() const;
+	int heightForWidth(int) const;
+	int count() const;
+	QLayoutItem *itemAt(int index) const;
+	QSize minimumSize() const;
+	void setGeometry(const QRect &rect);
+	QSize sizeHint() const;
+	QLayoutItem *takeAt(int index);
+
+private:
+	int doLayout(const QRect &rect, bool testOnly) const;
+	int smartSpacing(QStyle::PixelMetric pm) const;
+
+	QWidget* m_parent;
+	QList<QLayoutItem*> itemList;
+	int m_hSpace, m_vSpace;
+};
+
+#endif
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 99c490f8..dc874d59 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -85,6 +85,7 @@ set(pulseview_TEST_SOURCES
 	${PROJECT_SOURCE_DIR}/pv/widgets/colorpopup.cpp
 	${PROJECT_SOURCE_DIR}/pv/widgets/devicetoolbutton.cpp
 	${PROJECT_SOURCE_DIR}/pv/widgets/exportmenu.cpp
+	${PROJECT_SOURCE_DIR}/pv/widgets/flowlayout.cpp
 	${PROJECT_SOURCE_DIR}/pv/widgets/importmenu.cpp
 	${PROJECT_SOURCE_DIR}/pv/widgets/popup.cpp
 	${PROJECT_SOURCE_DIR}/pv/widgets/popuptoolbutton.cpp
@@ -153,6 +154,7 @@ set(pulseview_TEST_HEADERS
 	${PROJECT_SOURCE_DIR}/pv/widgets/colorpopup.hpp
 	${PROJECT_SOURCE_DIR}/pv/widgets/devicetoolbutton.hpp
 	${PROJECT_SOURCE_DIR}/pv/widgets/exportmenu.hpp
+	${PROJECT_SOURCE_DIR}/pv/widgets/flowlayout.hpp
 	${PROJECT_SOURCE_DIR}/pv/widgets/importmenu.hpp
 	${PROJECT_SOURCE_DIR}/pv/widgets/popup.hpp
 	${PROJECT_SOURCE_DIR}/pv/widgets/popuptoolbutton.hpp