View Issue Details

IDProjectCategoryView StatusLast Update
0017022ScribusUser Interfacepublic2023-11-04 09:09
Reporternitramr Assigned Tonitramr  
PrioritynormalSeveritytweakReproducibilityN/A
Status closedResolutionfixed 
PlatformDesktop PCOSUbuntuOS Version23.04 64-bit
Product Version1.7.0.svn 
Target Version1.7.0Fixed in Version1.7.0.svn 
Summary0017022: Improve UI of CurveEditWidget
DescriptionThe current curve edit widget draws a pixelated curve. Antialiasing can't be enabled because of the implemented drawing method.

I reworked the widget and changed the behavior a bit. In the previous version you always had to click on the curve to add a new point and then you could move it. The new widget will add a new point directly at the location you clicked. So you save one step.
TagsNo tags attached.
PatchYes

Activities

nitramr

2023-10-08 11:10

developer  

curvewidget_2023-10-08_01.patch (16,532 bytes)   
Index: scribus/ui/curvewidget.cpp
===================================================================
--- scribus/ui/curvewidget.cpp	(Revision 25725)
+++ scribus/ui/curvewidget.cpp	(Arbeitskopie)
@@ -30,11 +30,23 @@
 #include "ui/customfdialog.h"
 #include "ui/scmessagebox.h"
 #include "util.h"
-#include "util_color.h"
+#include "util_math.h"
 
+/* ********************************************************************************* *
+ *
+ * Constructor + Setup
+ *
+ * ********************************************************************************* */
 
+const qreal POINT_RADIUS = 4.0;
+const qreal TOLLERANCE = 2.0; // pixel tollerance to hit a point
 
-KCurve::KCurve(QWidget *parent) : QWidget(parent)
+KCurve::KCurve(QWidget *parent) : QWidget(parent),
+	m_leftmost(0.0),
+	m_rightmost(0.0),
+	m_dragging(false),
+	m_linear(false),
+	m_selectedPoint(0)
 {
 	setMouseTracking(true);
 	setMinimumSize(150, 150);
@@ -41,6 +53,7 @@
 	m_points.resize(0);
 	m_points.addPoint(0.0, 0.0);
 	m_points.addPoint(1.0, 1.0);
+
 	setFocusPolicy(Qt::StrongFocus);
 }
 
@@ -50,52 +63,59 @@
 
 void KCurve::paintEvent(QPaintEvent *)
 {
-	int x = 0;
-	int wWidth = width() - 1;
-	int wHeight = height() - 1;
-	// Drawing selection or all histogram values.
-	QPainter p1;
-	p1.begin(this);
-	//  draw background
-	p1.fillRect(QRect(0, 0, wWidth, wHeight), QColor(255, 255, 255));
-	// Draw grid separators.
-	p1.setPen(QPen(Qt::gray, 1, Qt::SolidLine));
-	p1.drawLine(wWidth/4, 0, wWidth/4, wHeight);
-	p1.drawLine(wWidth/2, 0, wWidth/2, wHeight);
-	p1.drawLine(3*wWidth/4, 0, 3*wWidth/4, wHeight);
-	p1.drawLine(0, wHeight/4, wWidth, wHeight/4);
-	p1.drawLine(0, wHeight/2, wWidth, wHeight/2);
-	p1.drawLine(0, 3*wHeight/4, wWidth, 3*wHeight/4);
+	int wWidth = width();
+	int wHeight = height();
+	QPointF offset(-.5, -.5);
+	QColor colorHandle = palette().color(QPalette::Highlight);
+	QColor colorBackground = palette().color(QPalette::Base);
+	QColor colorGraph = palette().color(QPalette::WindowText);
+	QColor colorGrid = palette().color(QPalette::Midlight); // Qt::gray
+	QPainterPath path = curvePath();
 
-	// Draw curve.
-	double curvePrevVal = getCurveValue(0.0);
-	p1.setPen(QPen(Qt::black, 1, Qt::SolidLine));
-	for (x = 0 ; x < wWidth ; x++)
+	// Drawing selection or all histogram values
+	QPainter painter;
+	painter.begin(this);
+	painter.setRenderHint(QPainter::Antialiasing, true);
+
+	// Draw background
+	painter.fillRect(this->rect(), colorBackground);
+
+	// Draw grid separators
+	painter.setPen(QPen(colorGrid, 1, Qt::SolidLine));
+	painter.drawLine( QPoint(wWidth * .25, 0) - offset, QPoint(wWidth * .25, wHeight) - offset );
+	painter.drawLine( QPoint(wWidth * .5, 0) - offset, QPoint(wWidth * .5, wHeight) - offset );
+	painter.drawLine( QPoint(wWidth * .75, 0) - offset, QPoint(wWidth * .75, wHeight) - offset );
+	painter.drawLine( QPoint(0, wHeight * .25) - offset, QPoint(wWidth, wHeight * .25) - offset );
+	painter.drawLine( QPoint(0, wHeight * .5) - offset, QPoint(wWidth, wHeight * .5) - offset );
+	painter.drawLine( QPoint(0, wHeight * .75) - offset, QPoint(wWidth, wHeight * .75) - offset );
+
+	// Draw curve
+	painter.setPen(QPen(colorGraph, 2, Qt::SolidLine));
+	painter.drawPath(path);
+
+	painter.setBrush(Qt::NoBrush);
+
+	// Draw handles
+	for (int i = 0; i < m_points.size(); i++)
 	{
-		double curveX;
-		double curveVal;
-		//		curveX = (x + 0.5) / wWidth;
-		curveX = x / static_cast<double>(wWidth);
-		curveVal = getCurveValue(curveX);
-		p1.drawLine(x - 1, wHeight - int(curvePrevVal * wHeight), x,     wHeight - int(curveVal * wHeight));
-		curvePrevVal = curveVal;
-	}
-	p1.drawLine(x - 1, wHeight - int(curvePrevVal * wHeight), x,     wHeight - int(getCurveValue(1.0) * wHeight));
-	for (int dh = 0; dh < m_points.size(); dh++)
-	{
-		FPoint p = m_points.point(dh);
-		if (p == m_grab_point)
+		FPoint p = m_points.point(i);
+		QPointF dot = QPointF(p.x() * width(), (1 - p.y()) * height());
+
+		if (p == m_points.at(m_selectedPoint))
 		{
-			p1.setPen(QPen(Qt::red, 3, Qt::SolidLine));
-			p1.drawEllipse( int(p.x() * wWidth) - 2, wHeight - 2 - int(p.y() * wHeight), 4, 4 );
+			painter.setPen(QPen(colorHandle, 2, Qt::SolidLine));
+			painter.setBrush(colorHandle);
+			painter.drawEllipse( dot, POINT_RADIUS, POINT_RADIUS );
 		}
 		else
 		{
-			p1.setPen(QPen(Qt::red, 1, Qt::SolidLine));
-			p1.drawEllipse( int(p.x() * wWidth) - 3, wHeight - 3 - int(p.y() * wHeight), 6, 6 );
+			painter.setPen(QPen(colorGraph, 2, Qt::SolidLine));
+			painter.setBrush(colorBackground);
+			painter.drawEllipse( dot, POINT_RADIUS, POINT_RADIUS );
 		}
 	}
-	p1.end();
+
+	painter.end();
 }
 
 void KCurve::keyPressEvent(QKeyEvent *e)
@@ -104,29 +124,10 @@
 	{
 		if (m_points.size() > 2)
 		{
-			FPoint closest_point = m_points.point(0);
-			FPoint p = m_points.point(0);
-			int pos = 0;
-			int cc = 0;
-			double distance = 1000; // just a big number
-			while (cc < m_points.size())
-			{
-				p = m_points.point(cc);
-				if (fabs (m_grab_point.x() - p.x()) < distance)
-				{
-					distance = fabs(m_grab_point.x() - p.x());
-					closest_point = p;
-					m_pos = pos;
-				}
-				cc++;
-				pos++;
-			}
-			FPointArray cli;
-			cli.putPoints(0, m_pos, m_points);
-			cli.putPoints(cli.size(), m_points.size()-m_pos-1, m_points, m_pos+1);
-			m_points.resize(0);
-			m_points = cli.copy();
-			m_grab_point = closest_point;
+			m_points.removeAt(m_selectedPoint);
+
+			m_selectedPoint = qBound(0, m_selectedPoint - 1, m_points.size() - 1);
+
 			repaint();
 			emit modified();
 			QWidget::keyPressEvent(e);
@@ -136,84 +137,55 @@
 
 void KCurve::mousePressEvent ( QMouseEvent * e )
 {
-	FPoint closest_point = FPoint();
-	double distance;
+
 	if (e->button() != Qt::LeftButton)
 		return;
-	double x = e->pos().x() / (float)width();
-	double y = 1.0 - e->pos().y() / (float)height();
-	distance = 1000; // just a big number
-	FPoint p = m_points.point(0);
-	int insert_pos =0;
-	int pos = 0;
-	int cc = 0;
-	while (cc < m_points.size())
+
+	// invert mouseY
+	m_mousePos = QPointF( e->pos().x(), (height() - e->pos().y()));
+
+	m_leftmost = - 1;
+	m_selectedPoint = 0;
+	m_rightmost = m_points.size();
+
+	m_dragging = false;
+
+	for(int i = 0; i < m_points.size(); i++)
 	{
-		p = m_points.point(cc);
-		if (fabs (x - p.x()) < distance)
+		QPointF pt(m_points[i].x() * width(), m_points[i].y() * height());
+
+		// calculate left point of mouse position
+		if(pt.x() >= 0 && pt.x() <= m_mousePos.x())
+			m_leftmost = i;
+
+		if( distance(pt, m_mousePos) <= POINT_RADIUS + TOLLERANCE )
 		{
-			distance = fabs(x - p.x());
-			closest_point = p;
-			insert_pos = pos;
-		}
-		cc++;
-		pos++;
-	}
-	m_pos = insert_pos;
-	m_grab_point = closest_point;
-	m_grabOffsetX = m_grab_point.x() - x;
-	m_grabOffsetY = m_grab_point.y() - y;
-	m_grab_point = FPoint(x + m_grabOffsetX, y + m_grabOffsetY);
-	double curveVal = getCurveValue(x);
-	if (distance * width() > 5)
-	{
-		m_dragging = false;
-		if (fabs(y - curveVal) * width() > 5)
+			m_selectedPoint = i;
+			m_leftmost = i - 1;
+			m_rightmost = i + 1;
+
+			m_dragging = true;
+			repaint();
 			return;
-		if (m_points.size() < 14)
-		{
-			if (x > closest_point.x())
-				m_pos++;
-			FPointArray cli;
-			cli.putPoints(0, m_pos, m_points);
-			cli.resize(cli.size()+1);
-			cli.putPoints(cli.size()-1, 1, x, curveVal);
-			cli.putPoints(cli.size(), m_points.size()-m_pos, m_points, m_pos);
-			m_points.resize(0);
-			m_points = cli.copy();
-			m_dragging = true;
-			m_grab_point = m_points.point(m_pos);
-			m_grabOffsetX = m_grab_point.x() - x;
-			m_grabOffsetY = m_grab_point.y() - curveVal;
-			m_grab_point = FPoint(x + m_grabOffsetX, curveVal + m_grabOffsetY);
-			setCursor(QCursor(Qt::CrossCursor));
 		}
+
 	}
-	else
+
+	// add new point
+	if(!m_dragging)
 	{
-		if (fabs(y - closest_point.y()) * width() > 5)
-			return;
+		FPoint onCurve(m_mousePos.x() / width(), m_mousePos.y() / height() );
+		m_selectedPoint = m_leftmost + 1;
+		m_rightmost = m_leftmost + 2;
+		m_points.insert(m_selectedPoint, onCurve );
+
 		m_dragging = true;
-		setCursor(QCursor(Qt::CrossCursor));
+
+		repaint();
+		emit modified();
 	}
-	// Determine the leftmost and rightmost points.
-	m_leftmost = 0;
-	m_rightmost = 1;
-	cc = 0;
-	while (cc < m_points.size())
-	{
-		p = m_points.point(cc);
-		if (p != m_grab_point)
-		{
-			if (p.x() > m_leftmost && p.x() < x)
-				m_leftmost = p.x();
-			if (p.x() < m_rightmost && p.x() > x)
-				m_rightmost = p.x();
-		}
-		cc++;
-    }
-	repaint();
-	emit modified();
+
+
 }
 
 void KCurve::mouseReleaseEvent ( QMouseEvent * e )
@@ -228,57 +200,117 @@
 
 void KCurve::mouseMoveEvent ( QMouseEvent * e )
 {
-	double x = e->pos().x() / (float)width();
-	double y = 1.0 - e->pos().y() / (float)height();
+	//  boolHoverCurve = false;
 
-	if (!m_dragging)   // If no point is selected set the the cursor shape if on top
+	if(m_dragging)
 	{
-		double distance = 1000;
-		double ydistance = 1000;
-		int cc = 0;
-		while (cc < m_points.size())
-		{
-			FPoint p = m_points.point(cc);
-			if (fabs (x - p.x()) < distance)
-			{
-				distance = fabs(x - p.x());
-				ydistance = fabs(y - p.y());
-			}
-			cc++;
-		}
-		if (distance * width() > 5 || ydistance * height() > 5)
-			setCursor(QCursor(Qt::ArrowCursor));
-		else
-			setCursor(QCursor(Qt::CrossCursor));
-	}
-	else  // Else, drag the selected point
-	{
 		setCursor(QCursor(Qt::CrossCursor));
-		x += m_grabOffsetX;
-		y += m_grabOffsetY;
-		if (x <= m_leftmost)
-			x = m_leftmost + 1E-4; // the addition so we can grab the dot later.
-		if (x >= m_rightmost)
-			x = m_rightmost - 1E-4;
-		if (y > 1.0)
-			y = 1.0;
-		if (y < 0.0)
-			y = 0.0;
-		m_grab_point = FPoint(x, y);
-		m_points.setPoint( m_pos, m_grab_point);
+
+		// invert mouseY
+		QPointF mouse(e->pos().x(), height() - e->pos().y());
+
+		// left bound
+		qreal leftBound = (m_leftmost < 0) ? 0. : m_points.at(m_leftmost).x();
+		qreal rightBound = (m_rightmost >= m_points.size()) ? 1. : m_points.at(m_rightmost).x();
+
+		// limit to bounds
+		qreal yPos = qBound(0., mouse.y() / height(), 1.);
+		qreal xPos = qBound(leftBound + 1E-4, mouse.x() / width(), rightBound - 1E-4);
+
+		QPointF pointSelectedDot(xPos, yPos );
+
+		// set new position
+		m_points.setPoint( m_selectedPoint, pointSelectedDot);
+
 		repaint();
 		emit modified();
 	}
 }
 
-double KCurve::getCurveValue(double x)
+FPointArray KCurve::getCurve()
 {
-	return getCurveYValue(m_points, x, m_linear);
+	return m_points.copy();
 }
 
-FPointArray KCurve::getCurve()
+
+QPainterPath KCurve::curvePath()
 {
-	return m_points.copy();
+	QPointF pointStart( 0, (1 - m_points.at(0).y()) * height() );
+	QPointF pointEnd( width(), (1 - m_points.at(m_points.size() - 1).y()) * height() );
+
+
+	QList<QPointF> points;
+	points.append( pointStart );
+
+	// Calculate absolute position of each point
+	for (int i = 0; i < m_points.size(); i++)
+	{
+		FPoint p = m_points.at(i);
+		points.append(QPointF(p.x() * width(), (1 - p.y()) * height()));
+	}
+
+	points.append( pointEnd );
+
+
+	// Construct Path
+	QPainterPath path;
+
+	// Add start line
+	path.moveTo( pointStart );
+	path.lineTo( points.at(1) );
+
+	for (int id = 0; id < points.size(); id++)
+	{
+
+		if(m_linear)
+		{
+
+			path.lineTo(points.at(id).toPoint());
+
+		}
+		else
+		{
+
+			int i0 = id-1;
+			int i1 = id;
+			int i2 = id+1;
+			int i3 = id+2;
+
+			if(i0 < 0 || i2 > points.size()-1 || i3 > points.size()-1){
+
+				continue;
+			}
+
+			FPoint p0 = (i0 >= 0) ? points.at(i0) : points.at(i1);
+			FPoint p1 = points.at(i1);
+			FPoint p2 = (i2 < points.size()) ? points.at(i2) : points.at(points.size()-1);
+			FPoint p3 = (i3 < points.size()) ? points.at(i3) : points.at(points.size()-1);
+
+			QList<QPointF> inputPoints;
+			inputPoints.append( p0.toQPointF() ); // Previous point
+			inputPoints.append( p1.toQPointF() ); // Point
+			inputPoints.append( p2.toQPointF() ); // Next point
+			inputPoints.append( p3.toQPointF() ); // Next but one point
+
+			double t = qMax(width() / (p2.x() - p1.x()) / 6, 1.);
+
+			QList<QPointF> *bezPoints = catmullToBezier(inputPoints, t);
+
+			path.cubicTo(
+						QPointF( bezPoints->at(1) ), // Control 1
+						QPointF( bezPoints->at(2) ), // Control 2
+						QPointF( bezPoints->at(3) ) // Next Point
+
+						);
+		}
+
+	}
+
+	// Add end line
+	path.lineTo( pointEnd );
+
+	return path;
+
 }
 
 void KCurve::setCurve(const FPointArray& inlist)
@@ -310,6 +342,13 @@
 	return m_linear;
 }
 
+
+/* ********************************************************************************* *
+ *
+ * Constructor + Setup
+ *
+ * ********************************************************************************* */
+
 CurveWidget::CurveWidget( QWidget* parent ) : QWidget( parent )
 {
 	CurveWidgetLayout = new QHBoxLayout(this);
Index: scribus/ui/curvewidget.h
===================================================================
--- scribus/ui/curvewidget.h	(Revision 25725)
+++ scribus/ui/curvewidget.h	(Arbeitskopie)
@@ -44,6 +44,7 @@
 	FPointArray getCurve();
 	void setCurve(const FPointArray& inlist);
 	void resetCurve();
+	QPainterPath curvePath();
 	void setLinear(bool setter);
 	bool isLinear();
 
@@ -53,14 +54,16 @@
 private:
 	double m_leftmost { 0.0 };
 	double m_rightmost { 0.0 };
-	FPoint m_grab_point;
-	int m_pos { 0 };
+//	FPoint m_grab_point;
+//	int m_pos { 0 };
 	bool m_dragging { false };
 	bool m_linear { false };
-	double m_grabOffsetX { 0.0 };
-	double m_grabOffsetY { 0.0 };
+//	double m_grabOffsetX { 0.0 };
+//	double m_grabOffsetY { 0.0 };
 	FPointArray m_points;
 	FPointArray m_points_back;
+	QPointF m_mousePos;
+	int m_selectedPoint { 0 };
 };
 
 class SCRIBUS_API CurveWidget : public QWidget
Index: scribus/util_math.cpp
===================================================================
--- scribus/util_math.cpp	(Revision 25725)
+++ scribus/util_math.cpp	(Arbeitskopie)
@@ -547,3 +547,28 @@
 	dx = lineX.x1();
 	dy = lineX.y1();
 }
+
+QList<QPointF> *catmullToBezier(QList<QPointF> inputPoints, double t)
+{
+	// Inspired by the explanation on: https://pomax.github.io/bezierinfo/#catmullconv
+
+	QList<QPointF> *result = new QList<QPointF>();
+
+ //   qDebug() << "t" << t;
+
+	double x0 = inputPoints[1].x();
+	double y0 = inputPoints[1].y();
+	double x1 = inputPoints[1].x() + (inputPoints[2].x() - inputPoints[0].x()) / (6 * t);
+	double y1 = inputPoints[1].y() + (inputPoints[2].y() - inputPoints[0].y()) / (6 * t);
+	double x2 = inputPoints[2].x() - (inputPoints[3].x() - inputPoints[1].x()) / (6 * t);
+	double y2 = inputPoints[2].y() - (inputPoints[3].y() - inputPoints[1].y()) / (6 * t);
+	double x3 = inputPoints[2].x();
+	double y3 = inputPoints[2].y();
+
+	result->append( QPointF(x0, y0) );
+	result->append( QPointF(x1, y1) );
+	result->append( QPointF(x2, y2) );
+	result->append( QPointF(x3, y3) );
+
+	return result;
+}
Index: scribus/util_math.h
===================================================================
--- scribus/util_math.h	(Revision 25725)
+++ scribus/util_math.h	(Arbeitskopie)
@@ -36,6 +36,13 @@
 bool     SCRIBUS_API regionContainsRect(const QRegion& shape, QRect rect);
 QPolygon SCRIBUS_API flattenPath(const FPointArray& ina, QList<uint> &segments);
 QList<QPainterPath> SCRIBUS_API decomposePath(const QPainterPath &path);
+/**
+ * @brief CatmullToBezier
+ * @param inputPoints Point Array of at least 4 points (p0, p1, p2, p3). You will get cubic bezier points from p1 to p2.
+ * @param t
+ * @return
+ */
+QList<QPointF> *catmullToBezier(QList<QPointF> inputPoints, double t = 1);
 QPainterPath  SCRIBUS_API regularPolygonPath(double w, double h, uint c, bool star, double factor, double rota, double factor2 = 0.0, double innerRot = 0.0, double factor3 = 0.0);
 QPainterPath  SCRIBUS_API spiralPath(double spiralWidth, double spiralHeight, double spiralStartAngle, double spiralEndAngle, double spiralFactor);
 inline double SCRIBUS_API xy2Deg(double x, double y);
@@ -43,6 +50,7 @@
 inline double SCRIBUS_API cosd(double);
 inline double SCRIBUS_API square(double);
 inline double SCRIBUS_API distance(double, double);
+inline double SCRIBUS_API distance(QPointF p1, QPointF p2);
 /*! \brief Constrains an angle of rotation to 45 degree intervals
    Will make code simpler and reduce interval or provide as a parameter
    \param angle angle Angle in degrees
@@ -78,6 +86,12 @@
 	return sqrt(x*x + y*y);
 }
 
+inline double distance(QPointF p1, QPointF p2)
+{
+	QPointF point = p2 - p1;
+	return point.manhattanLength();
+}
+
 inline double xy2Deg(double x, double y)
 {
 	return atan2(y,x) * (180.0/M_PI);
curvewidget_2023-10-08_01.patch (16,532 bytes)   
old-widget.png (4,941 bytes)   
old-widget.png (4,941 bytes)   
new-widget.png (7,405 bytes)   
new-widget.png (7,405 bytes)   

cbradney

2023-10-08 14:10

administrator   ~0050370

There's a new crash introduced when undoing changes to the curve:


ASSERT failure in QList::at: "index out of range", file /Users/craig/Qt/6.5.3/macos/lib/QtCore.framework/Headers/qlist.h, line 423
Assertion failed: (false), function abort_on_error, file scribuscore.cpp, line 84.
Scribus Crash
-------------
Scribus crashes due to Signal 0000006
QWidget::repaint: Recursive repaint detected
ASSERT failure in QList::at: "index out of range", file /Users/craig/Qt/6.5.3/macos/lib/QtCore.framework/Headers/qlist.h, line 423

nitramr

2023-10-08 15:07

developer   ~0050371

@cbradney Do you reset the curve by click on rest button? I can't reproduce the crash.

cbradney

2023-10-08 15:53

administrator   ~0050372

Yes, the reset button causes it

nitramr

2023-10-08 16:31

developer   ~0050373

I still could not reproduce it, no idea why it is not crashing for me, but I found something.

"m_points_back" has no initial data in constructor and will set only if you load a scu file or invert the curve, but it is used when you click the reset button. Now, I set some dummy data in constructor.
m_points_back = m_points.copy();

The paint event draw a curve based on a QPainterPath that is created from "m_points". I guess when "m_point" is empty it occurs the crash. To protect this function, I added a check in KCurve::curvePath().

I hope it is fixed now, because it is just a guess.
curvewidget_2023-10-08_02.patch (16,322 bytes)   
Index: scribus/ui/curvewidget.cpp
===================================================================
--- scribus/ui/curvewidget.cpp	(Revision 25725)
+++ scribus/ui/curvewidget.cpp	(Arbeitskopie)
@@ -30,9 +30,16 @@
 #include "ui/customfdialog.h"
 #include "ui/scmessagebox.h"
 #include "util.h"
-#include "util_color.h"
+#include "util_math.h"
 
+/* ********************************************************************************* *
+ *
+ * Constructor + Setup
+ *
+ * ********************************************************************************* */
 
+const qreal POINT_RADIUS = 4.0;
+const qreal TOLLERANCE = 2.0; // pixel tollerance to hit a point
 
 KCurve::KCurve(QWidget *parent) : QWidget(parent)
 {
@@ -41,6 +48,8 @@
 	m_points.resize(0);
 	m_points.addPoint(0.0, 0.0);
 	m_points.addPoint(1.0, 1.0);
+	m_points_back = m_points.copy();
+
 	setFocusPolicy(Qt::StrongFocus);
 }
 
@@ -50,52 +59,59 @@
 
 void KCurve::paintEvent(QPaintEvent *)
 {
-	int x = 0;
-	int wWidth = width() - 1;
-	int wHeight = height() - 1;
-	// Drawing selection or all histogram values.
-	QPainter p1;
-	p1.begin(this);
-	//  draw background
-	p1.fillRect(QRect(0, 0, wWidth, wHeight), QColor(255, 255, 255));
-	// Draw grid separators.
-	p1.setPen(QPen(Qt::gray, 1, Qt::SolidLine));
-	p1.drawLine(wWidth/4, 0, wWidth/4, wHeight);
-	p1.drawLine(wWidth/2, 0, wWidth/2, wHeight);
-	p1.drawLine(3*wWidth/4, 0, 3*wWidth/4, wHeight);
-	p1.drawLine(0, wHeight/4, wWidth, wHeight/4);
-	p1.drawLine(0, wHeight/2, wWidth, wHeight/2);
-	p1.drawLine(0, 3*wHeight/4, wWidth, 3*wHeight/4);
+	int wWidth = width();
+	int wHeight = height();
+	QPointF offset(-.5, -.5);
+	QColor colorHandle = palette().color(QPalette::Highlight);
+	QColor colorBackground = palette().color(QPalette::Base);
+	QColor colorGraph = palette().color(QPalette::WindowText);
+	QColor colorGrid = palette().color(QPalette::Midlight); // Qt::gray
+	QPainterPath path = curvePath();
 
-	// Draw curve.
-	double curvePrevVal = getCurveValue(0.0);
-	p1.setPen(QPen(Qt::black, 1, Qt::SolidLine));
-	for (x = 0 ; x < wWidth ; x++)
+	// Drawing selection or all histogram values
+	QPainter painter;
+	painter.begin(this);
+	painter.setRenderHint(QPainter::Antialiasing, true);
+
+	// Draw background
+	painter.fillRect(this->rect(), colorBackground);
+
+	// Draw grid separators
+	painter.setPen(QPen(colorGrid, 1, Qt::SolidLine));
+	painter.drawLine( QPoint(wWidth * .25, 0) - offset, QPoint(wWidth * .25, wHeight) - offset );
+	painter.drawLine( QPoint(wWidth * .5, 0) - offset, QPoint(wWidth * .5, wHeight) - offset );
+	painter.drawLine( QPoint(wWidth * .75, 0) - offset, QPoint(wWidth * .75, wHeight) - offset );
+	painter.drawLine( QPoint(0, wHeight * .25) - offset, QPoint(wWidth, wHeight * .25) - offset );
+	painter.drawLine( QPoint(0, wHeight * .5) - offset, QPoint(wWidth, wHeight * .5) - offset );
+	painter.drawLine( QPoint(0, wHeight * .75) - offset, QPoint(wWidth, wHeight * .75) - offset );
+
+	// Draw curve
+	painter.setPen(QPen(colorGraph, 2, Qt::SolidLine));
+	painter.drawPath(path);
+
+	painter.setBrush(Qt::NoBrush);
+
+	// Draw handles
+	for (int i = 0; i < m_points.size(); i++)
 	{
-		double curveX;
-		double curveVal;
-		//		curveX = (x + 0.5) / wWidth;
-		curveX = x / static_cast<double>(wWidth);
-		curveVal = getCurveValue(curveX);
-		p1.drawLine(x - 1, wHeight - int(curvePrevVal * wHeight), x,     wHeight - int(curveVal * wHeight));
-		curvePrevVal = curveVal;
-	}
-	p1.drawLine(x - 1, wHeight - int(curvePrevVal * wHeight), x,     wHeight - int(getCurveValue(1.0) * wHeight));
-	for (int dh = 0; dh < m_points.size(); dh++)
-	{
-		FPoint p = m_points.point(dh);
-		if (p == m_grab_point)
+		FPoint p = m_points.point(i);
+		QPointF dot = QPointF(p.x() * width(), (1 - p.y()) * height());
+
+		if (p == m_points.at(m_selectedPoint))
 		{
-			p1.setPen(QPen(Qt::red, 3, Qt::SolidLine));
-			p1.drawEllipse( int(p.x() * wWidth) - 2, wHeight - 2 - int(p.y() * wHeight), 4, 4 );
+			painter.setPen(QPen(colorHandle, 2, Qt::SolidLine));
+			painter.setBrush(colorHandle);
+			painter.drawEllipse( dot, POINT_RADIUS, POINT_RADIUS );
 		}
 		else
 		{
-			p1.setPen(QPen(Qt::red, 1, Qt::SolidLine));
-			p1.drawEllipse( int(p.x() * wWidth) - 3, wHeight - 3 - int(p.y() * wHeight), 6, 6 );
+			painter.setPen(QPen(colorGraph, 2, Qt::SolidLine));
+			painter.setBrush(colorBackground);
+			painter.drawEllipse( dot, POINT_RADIUS, POINT_RADIUS );
 		}
 	}
-	p1.end();
+
+	painter.end();
 }
 
 void KCurve::keyPressEvent(QKeyEvent *e)
@@ -104,29 +120,10 @@
 	{
 		if (m_points.size() > 2)
 		{
-			FPoint closest_point = m_points.point(0);
-			FPoint p = m_points.point(0);
-			int pos = 0;
-			int cc = 0;
-			double distance = 1000; // just a big number
-			while (cc < m_points.size())
-			{
-				p = m_points.point(cc);
-				if (fabs (m_grab_point.x() - p.x()) < distance)
-				{
-					distance = fabs(m_grab_point.x() - p.x());
-					closest_point = p;
-					m_pos = pos;
-				}
-				cc++;
-				pos++;
-			}
-			FPointArray cli;
-			cli.putPoints(0, m_pos, m_points);
-			cli.putPoints(cli.size(), m_points.size()-m_pos-1, m_points, m_pos+1);
-			m_points.resize(0);
-			m_points = cli.copy();
-			m_grab_point = closest_point;
+			m_points.removeAt(m_selectedPoint);
+
+			m_selectedPoint = qBound(0, m_selectedPoint - 1, m_points.size() - 1);
+
 			repaint();
 			emit modified();
 			QWidget::keyPressEvent(e);
@@ -136,84 +133,55 @@
 
 void KCurve::mousePressEvent ( QMouseEvent * e )
 {
-	FPoint closest_point = FPoint();
-	double distance;
+
 	if (e->button() != Qt::LeftButton)
 		return;
-	double x = e->pos().x() / (float)width();
-	double y = 1.0 - e->pos().y() / (float)height();
-	distance = 1000; // just a big number
-	FPoint p = m_points.point(0);
-	int insert_pos =0;
-	int pos = 0;
-	int cc = 0;
-	while (cc < m_points.size())
+
+	// invert mouseY
+	m_mousePos = QPointF( e->pos().x(), (height() - e->pos().y()));
+
+	m_leftmost = - 1;
+	m_selectedPoint = 0;
+	m_rightmost = m_points.size();
+
+	m_dragging = false;
+
+	for(int i = 0; i < m_points.size(); i++)
 	{
-		p = m_points.point(cc);
-		if (fabs (x - p.x()) < distance)
+		QPointF pt(m_points[i].x() * width(), m_points[i].y() * height());
+
+		// calculate left point of mouse position
+		if(pt.x() >= 0 && pt.x() <= m_mousePos.x())
+			m_leftmost = i;
+
+		if( distance(pt, m_mousePos) <= POINT_RADIUS + TOLLERANCE )
 		{
-			distance = fabs(x - p.x());
-			closest_point = p;
-			insert_pos = pos;
-		}
-		cc++;
-		pos++;
-	}
-	m_pos = insert_pos;
-	m_grab_point = closest_point;
-	m_grabOffsetX = m_grab_point.x() - x;
-	m_grabOffsetY = m_grab_point.y() - y;
-	m_grab_point = FPoint(x + m_grabOffsetX, y + m_grabOffsetY);
-	double curveVal = getCurveValue(x);
-	if (distance * width() > 5)
-	{
-		m_dragging = false;
-		if (fabs(y - curveVal) * width() > 5)
+			m_selectedPoint = i;
+			m_leftmost = i - 1;
+			m_rightmost = i + 1;
+
+			m_dragging = true;
+			repaint();
 			return;
-		if (m_points.size() < 14)
-		{
-			if (x > closest_point.x())
-				m_pos++;
-			FPointArray cli;
-			cli.putPoints(0, m_pos, m_points);
-			cli.resize(cli.size()+1);
-			cli.putPoints(cli.size()-1, 1, x, curveVal);
-			cli.putPoints(cli.size(), m_points.size()-m_pos, m_points, m_pos);
-			m_points.resize(0);
-			m_points = cli.copy();
-			m_dragging = true;
-			m_grab_point = m_points.point(m_pos);
-			m_grabOffsetX = m_grab_point.x() - x;
-			m_grabOffsetY = m_grab_point.y() - curveVal;
-			m_grab_point = FPoint(x + m_grabOffsetX, curveVal + m_grabOffsetY);
-			setCursor(QCursor(Qt::CrossCursor));
 		}
+
 	}
-	else
+
+	// add new point
+	if(!m_dragging)
 	{
-		if (fabs(y - closest_point.y()) * width() > 5)
-			return;
+		FPoint onCurve(m_mousePos.x() / width(), m_mousePos.y() / height() );
+		m_selectedPoint = m_leftmost + 1;
+		m_rightmost = m_leftmost + 2;
+		m_points.insert(m_selectedPoint, onCurve );
+
 		m_dragging = true;
-		setCursor(QCursor(Qt::CrossCursor));
+
+		repaint();
+		emit modified();
 	}
-	// Determine the leftmost and rightmost points.
-	m_leftmost = 0;
-	m_rightmost = 1;
-	cc = 0;
-	while (cc < m_points.size())
-	{
-		p = m_points.point(cc);
-		if (p != m_grab_point)
-		{
-			if (p.x() > m_leftmost && p.x() < x)
-				m_leftmost = p.x();
-			if (p.x() < m_rightmost && p.x() > x)
-				m_rightmost = p.x();
-		}
-		cc++;
-    }
-	repaint();
-	emit modified();
+
+
 }
 
 void KCurve::mouseReleaseEvent ( QMouseEvent * e )
@@ -228,57 +196,113 @@
 
 void KCurve::mouseMoveEvent ( QMouseEvent * e )
 {
-	double x = e->pos().x() / (float)width();
-	double y = 1.0 - e->pos().y() / (float)height();
-
-	if (!m_dragging)   // If no point is selected set the the cursor shape if on top
+	if(m_dragging)
 	{
-		double distance = 1000;
-		double ydistance = 1000;
-		int cc = 0;
-		while (cc < m_points.size())
-		{
-			FPoint p = m_points.point(cc);
-			if (fabs (x - p.x()) < distance)
-			{
-				distance = fabs(x - p.x());
-				ydistance = fabs(y - p.y());
-			}
-			cc++;
-		}
-		if (distance * width() > 5 || ydistance * height() > 5)
-			setCursor(QCursor(Qt::ArrowCursor));
-		else
-			setCursor(QCursor(Qt::CrossCursor));
-	}
-	else  // Else, drag the selected point
-	{
 		setCursor(QCursor(Qt::CrossCursor));
-		x += m_grabOffsetX;
-		y += m_grabOffsetY;
-		if (x <= m_leftmost)
-			x = m_leftmost + 1E-4; // the addition so we can grab the dot later.
-		if (x >= m_rightmost)
-			x = m_rightmost - 1E-4;
-		if (y > 1.0)
-			y = 1.0;
-		if (y < 0.0)
-			y = 0.0;
-		m_grab_point = FPoint(x, y);
-		m_points.setPoint( m_pos, m_grab_point);
+
+		// invert mouseY
+		QPointF mouse(e->pos().x(), height() - e->pos().y());
+
+		// left bound
+		qreal leftBound = (m_leftmost < 0) ? 0. : m_points.at(m_leftmost).x();
+		qreal rightBound = (m_rightmost >= m_points.size()) ? 1. : m_points.at(m_rightmost).x();
+
+		// limit to bounds
+		qreal yPos = qBound(0., mouse.y() / height(), 1.);
+		qreal xPos = qBound(leftBound + 1E-4, mouse.x() / width(), rightBound - 1E-4);
+
+		QPointF pointSelectedDot(xPos, yPos );
+
+		// set new position
+		m_points.setPoint( m_selectedPoint, pointSelectedDot);
+
 		repaint();
 		emit modified();
 	}
 }
 
-double KCurve::getCurveValue(double x)
+FPointArray KCurve::getCurve()
 {
-	return getCurveYValue(m_points, x, m_linear);
+	return m_points.copy();
 }
 
-FPointArray KCurve::getCurve()
+
+QPainterPath KCurve::curvePath()
 {
-	return m_points.copy();
+	QPainterPath path;
+
+	if(m_points.size() <= 0)
+	{
+		qWarning() << Q_FUNC_INFO << "Can't create path because there are no path nodes!";
+		return path;
+	}
+
+	QPointF pointStart( 0, (1 - m_points.at(0).y()) * height() );
+	QPointF pointEnd( width(), (1 - m_points.at(m_points.size() - 1).y()) * height() );
+
+	QList<QPointF> points;
+	points.append( pointStart );
+
+	// Calculate absolute position of each point
+	for (int i = 0; i < m_points.size(); i++)
+	{
+		FPoint p = m_points.at(i);
+		points.append(QPointF(p.x() * width(), (1 - p.y()) * height()));
+	}
+
+	points.append( pointEnd );
+
+	// Add start line
+	path.moveTo( pointStart );
+	path.lineTo( points.at(1) );
+
+	for (int id = 0; id < points.size(); id++)
+	{
+
+		if(m_linear)
+		{
+			path.lineTo(points.at(id).toPoint());
+		}
+		else
+		{
+
+			int i0 = id-1;
+			int i1 = id;
+			int i2 = id+1;
+			int i3 = id+2;
+
+			if(i0 < 0 || i2 > points.size()-1 || i3 > points.size() - 1)
+				continue;
+
+			FPoint p0 = (i0 >= 0) ? points.at(i0) : points.at(i1);
+			FPoint p1 = points.at(i1);
+			FPoint p2 = (i2 < points.size()) ? points.at(i2) : points.at(points.size() - 1);
+			FPoint p3 = (i3 < points.size()) ? points.at(i3) : points.at(points.size() - 1);
+
+			QList<QPointF> inputPoints;
+			inputPoints.append( p0.toQPointF() ); // Previous point
+			inputPoints.append( p1.toQPointF() ); // Point
+			inputPoints.append( p2.toQPointF() ); // Next point
+			inputPoints.append( p3.toQPointF() ); // Next but one point
+
+			double t = qMax(width() / (p2.x() - p1.x()) / 6, 1.);
+
+			QList<QPointF> *bezPoints = catmullToBezier(inputPoints, t);
+
+			path.cubicTo(
+						QPointF( bezPoints->at(1) ), // Control 1
+						QPointF( bezPoints->at(2) ), // Control 2
+						QPointF( bezPoints->at(3) ) // Next Point
+						);
+		}
+
+	}
+
+	// Add end line
+	path.lineTo( pointEnd );
+
+	return path;
+
 }
 
 void KCurve::setCurve(const FPointArray& inlist)
@@ -310,6 +334,13 @@
 	return m_linear;
 }
 
+
+/* ********************************************************************************* *
+ *
+ * Constructor + Setup
+ *
+ * ********************************************************************************* */
+
 CurveWidget::CurveWidget( QWidget* parent ) : QWidget( parent )
 {
 	CurveWidgetLayout = new QHBoxLayout(this);
Index: scribus/ui/curvewidget.h
===================================================================
--- scribus/ui/curvewidget.h	(Revision 25725)
+++ scribus/ui/curvewidget.h	(Arbeitskopie)
@@ -44,6 +44,7 @@
 	FPointArray getCurve();
 	void setCurve(const FPointArray& inlist);
 	void resetCurve();
+	QPainterPath curvePath();
 	void setLinear(bool setter);
 	bool isLinear();
 
@@ -53,14 +54,12 @@
 private:
 	double m_leftmost { 0.0 };
 	double m_rightmost { 0.0 };
-	FPoint m_grab_point;
-	int m_pos { 0 };
 	bool m_dragging { false };
 	bool m_linear { false };
-	double m_grabOffsetX { 0.0 };
-	double m_grabOffsetY { 0.0 };
 	FPointArray m_points;
 	FPointArray m_points_back;
+	QPointF m_mousePos;
+	int m_selectedPoint { 0 };
 };
 
 class SCRIBUS_API CurveWidget : public QWidget
Index: scribus/util_math.cpp
===================================================================
--- scribus/util_math.cpp	(Revision 25725)
+++ scribus/util_math.cpp	(Arbeitskopie)
@@ -547,3 +547,28 @@
 	dx = lineX.x1();
 	dy = lineX.y1();
 }
+
+QList<QPointF> *catmullToBezier(QList<QPointF> inputPoints, double t)
+{
+	// Inspired by the explanation on: https://pomax.github.io/bezierinfo/#catmullconv
+
+	QList<QPointF> *result = new QList<QPointF>();
+
+ //   qDebug() << "t" << t;
+
+	double x0 = inputPoints[1].x();
+	double y0 = inputPoints[1].y();
+	double x1 = inputPoints[1].x() + (inputPoints[2].x() - inputPoints[0].x()) / (6 * t);
+	double y1 = inputPoints[1].y() + (inputPoints[2].y() - inputPoints[0].y()) / (6 * t);
+	double x2 = inputPoints[2].x() - (inputPoints[3].x() - inputPoints[1].x()) / (6 * t);
+	double y2 = inputPoints[2].y() - (inputPoints[3].y() - inputPoints[1].y()) / (6 * t);
+	double x3 = inputPoints[2].x();
+	double y3 = inputPoints[2].y();
+
+	result->append( QPointF(x0, y0) );
+	result->append( QPointF(x1, y1) );
+	result->append( QPointF(x2, y2) );
+	result->append( QPointF(x3, y3) );
+
+	return result;
+}
Index: scribus/util_math.h
===================================================================
--- scribus/util_math.h	(Revision 25725)
+++ scribus/util_math.h	(Arbeitskopie)
@@ -36,6 +36,13 @@
 bool     SCRIBUS_API regionContainsRect(const QRegion& shape, QRect rect);
 QPolygon SCRIBUS_API flattenPath(const FPointArray& ina, QList<uint> &segments);
 QList<QPainterPath> SCRIBUS_API decomposePath(const QPainterPath &path);
+/**
+ * @brief CatmullToBezier
+ * @param inputPoints Point Array of at least 4 points (p0, p1, p2, p3). You will get cubic bezier points from p1 to p2.
+ * @param t
+ * @return
+ */
+QList<QPointF> *catmullToBezier(QList<QPointF> inputPoints, double t = 1);
 QPainterPath  SCRIBUS_API regularPolygonPath(double w, double h, uint c, bool star, double factor, double rota, double factor2 = 0.0, double innerRot = 0.0, double factor3 = 0.0);
 QPainterPath  SCRIBUS_API spiralPath(double spiralWidth, double spiralHeight, double spiralStartAngle, double spiralEndAngle, double spiralFactor);
 inline double SCRIBUS_API xy2Deg(double x, double y);
@@ -43,6 +50,7 @@
 inline double SCRIBUS_API cosd(double);
 inline double SCRIBUS_API square(double);
 inline double SCRIBUS_API distance(double, double);
+inline double SCRIBUS_API distance(QPointF p1, QPointF p2);
 /*! \brief Constrains an angle of rotation to 45 degree intervals
    Will make code simpler and reduce interval or provide as a parameter
    \param angle angle Angle in degrees
@@ -78,6 +86,12 @@
 	return sqrt(x*x + y*y);
 }
 
+inline double distance(QPointF p1, QPointF p2)
+{
+	QPointF point = p2 - p1;
+	return point.manhattanLength();
+}
+
 inline double xy2Deg(double x, double y)
 {
 	return atan2(y,x) * (180.0/M_PI);
curvewidget_2023-10-08_02.patch (16,322 bytes)   

cbradney

2023-10-08 19:25

administrator   ~0050375

(lldb) bt
* thread 0000001, queue = 'com.apple.main-thread', stop reason = hit program assert
    frame #0: 0x00007ff8040a37a6 libsystem_kernel.dylib`__pthread_kill + 10
    frame 0000001: 0x00007ff8040dbf30 libsystem_pthread.dylib`pthread_kill + 262
    frame 0000002: 0x00007ff803ffaa4d libsystem_c.dylib`abort + 126
    frame 0000003: 0x00007ff803ff9d60 libsystem_c.dylib`__assert_rtn + 314
  * frame 0000004: 0x00000001007ec336 Scribus`abort_on_error(t=QtFatalMsg, (null)=0x00007ff7bfef8be0, m=0x00007ff7bfef8b98) at scribuscore.cpp:84:22
    frame 0000005: 0x00000001059e1148 QtCore`___lldb_unnamed_symbol7338 + 232
    frame 0000006: 0x00000001059dc2ea QtCore`___lldb_unnamed_symbol7324 + 42
    frame 0000007: 0x0000000105d480e5 QtCore`QMessageLogger::fatal(char const*, ...) const + 151
    frame 0000008: 0x0000000105d47431 QtCore`qt_assert_x(char const*, char const*, char const*, int) + 70
    frame 0000009: 0x000000010017f45a Scribus`QList<FPoint>::at(this=0x00006000069a6480, i=2) const at qlist.h:423:9
    frame 0000010: 0x0000000100b7e96d Scribus`KCurve::paintEvent(this=0x00006000069a6440, (null)=0x00007ff7bfef9470) at curvewidget.cpp:100:21
    frame 0000011: 0x0000000107ec292e QtWidgets`QWidget::event(QEvent*) + 1262
    frame 0000012: 0x0000000107e72fa7 QtWidgets`QApplicationPrivate::notify_helper(QObject*, QEvent*) + 247
    frame 0000013: 0x0000000107e73dcc QtWidgets`QApplication::notify(QObject*, QEvent*) + 508
    frame 0000014: 0x0000000105a3da1a QtCore`QCoreApplication::notifyInternal2(QObject*, QEvent*) + 170
    frame 0000015: 0x0000000107eb4a2d QtWidgets`QWidgetPrivate::drawWidget(QPaintDevice*, QRegion const&, QPoint const&, QFlags<QWidgetPrivate::DrawWidgetFlag>, QPainter*, QWidgetRepaintManager*) + 3981
    frame 0000016: 0x0000000107ebc66f QtWidgets`QWidgetPrivate::paintSiblingsRecursive(QPaintDevice*, QList<QObject*> const&, int, QRegion const&, QPoint const&, QFlags<QWidgetPrivate::DrawWidgetFlag>, QPainter*, QWidgetRepaintManager*) + 1007
    frame 0000017: 0x0000000107eb4b68 QtWidgets`QWidgetPrivate::drawWidget(QPaintDevice*, QRegion const&, QPoint const&, QFlags<QWidgetPrivate::DrawWidgetFlag>, QPainter*, QWidgetRepaintManager*) + 4296
    frame 0000018: 0x0000000107ebc66f QtWidgets`QWidgetPrivate::paintSiblingsRecursive(QPaintDevice*, QList<QObject*> const&, int, QRegion const&, QPoint const&, QFlags<QWidgetPrivate::DrawWidgetFlag>, QPainter*, QWidgetRepaintManager*) + 1007
    frame 0000019: 0x0000000107eb4b68 QtWidgets`QWidgetPrivate::drawWidget(QPaintDevice*, QRegion const&, QPoint const&, QFlags<QWidgetPrivate::DrawWidgetFlag>, QPainter*, QWidgetRepaintManager*) + 4296
    frame 0000020: 0x0000000107ebc66f QtWidgets`QWidgetPrivate::paintSiblingsRecursive(QPaintDevice*, QList<QObject*> const&, int, QRegion const&, QPoint const&, QFlags<QWidgetPrivate::DrawWidgetFlag>, QPainter*, QWidgetRepaintManager*) + 1007
    frame 0000021: 0x0000000107eb4b68 QtWidgets`QWidgetPrivate::drawWidget(QPaintDevice*, QRegion const&, QPoint const&, QFlags<QWidgetPrivate::DrawWidgetFlag>, QPainter*, QWidgetRepaintManager*) + 4296
    frame 0000022: 0x0000000107ebc66f QtWidgets`QWidgetPrivate::paintSiblingsRecursive(QPaintDevice*, QList<QObject*> const&, int, QRegion const&, QPoint const&, QFlags<QWidgetPrivate::DrawWidgetFlag>, QPainter*, QWidgetRepaintManager*) + 1007
    frame 0000023: 0x0000000107eb4b68 QtWidgets`QWidgetPrivate::drawWidget(QPaintDevice*, QRegion const&, QPoint const&, QFlags<QWidgetPrivate::DrawWidgetFlag>, QPainter*, QWidgetRepaintManager*) + 4296
    frame 0000024: 0x0000000107ebc66f QtWidgets`QWidgetPrivate::paintSiblingsRecursive(QPaintDevice*, QList<QObject*> const&, int, QRegion const&, QPoint const&, QFlags<QWidgetPrivate::DrawWidgetFlag>, QPainter*, QWidgetRepaintManager*) + 1007
    frame 0000025: 0x0000000107eb4b68 QtWidgets`QWidgetPrivate::drawWidget(QPaintDevice*, QRegion const&, QPoint const&, QFlags<QWidgetPrivate::DrawWidgetFlag>, QPainter*, QWidgetRepaintManager*) + 4296
    frame 0000026: 0x0000000107ed4b8d QtWidgets`QWidgetRepaintManager::paintAndFlush() + 5085
    frame 0000027: 0x0000000107ed4e6f QtWidgets`QWidgetRepaintManager::sync() + 255
    frame 0000028: 0x0000000107ec2b55 QtWidgets`QWidget::event(QEvent*) + 1813
    frame 0000029: 0x0000000107e72fa7 QtWidgets`QApplicationPrivate::notify_helper(QObject*, QEvent*) + 247
    frame 0000030: 0x0000000107e73dcc QtWidgets`QApplication::notify(QObject*, QEvent*) + 508
    frame 0000031: 0x0000000105a3da1a QtCore`QCoreApplication::notifyInternal2(QObject*, QEvent*) + 170
    frame 0000032: 0x0000000107ed11b5 QtWidgets`QWidgetRepaintManager::sendUpdateRequest(QWidget*, QWidgetRepaintManager::UpdateTime) + 629
    frame 0000033: 0x0000000107ed0ee3 QtWidgets`void QWidgetRepaintManager::markDirty<QRect>(QRect const&, QWidget*, QWidgetRepaintManager::UpdateTime, QWidgetRepaintManager::BufferState) + 2163
    frame 0000034: 0x0000000107ec5927 QtWidgets`QWidget::repaint(QRect const&) + 151
    frame 0000035: 0x0000000107ec5884 QtWidgets`QWidget::repaint() + 52
    frame 0000036: 0x0000000100b8033c Scribus`KCurve::resetCurve(this=0x00006000069a6440) at curvewidget.cpp:321:2
    frame 0000037: 0x0000000100b81689 Scribus`CurveWidget::doReset(this=0x000060000300ccb0) at curvewidget.cpp:416:12
    frame 0000038: 0x000000010002d344 Scribus`CurveWidget::qt_static_metacall(_o=0x000060000300ccb0, _c=InvokeMetaMethod, _id=1, _a=0x00007ff7bfefaa80) at moc_curvewidget.cpp:278:21

nitramr

2023-10-08 19:59

developer   ~0050376

Next try, I fixed a place in paint event function.
curvewidget_2023-10-08_03.patch (16,450 bytes)   
Index: scribus/ui/curvewidget.cpp
===================================================================
--- scribus/ui/curvewidget.cpp	(Revision 25725)
+++ scribus/ui/curvewidget.cpp	(Arbeitskopie)
@@ -30,9 +30,16 @@
 #include "ui/customfdialog.h"
 #include "ui/scmessagebox.h"
 #include "util.h"
-#include "util_color.h"
+#include "util_math.h"
 
+/* ********************************************************************************* *
+ *
+ * Constructor + Setup
+ *
+ * ********************************************************************************* */
 
+const qreal POINT_RADIUS = 4.0;
+const qreal TOLLERANCE = 2.0; // pixel tollerance to hit a point
 
 KCurve::KCurve(QWidget *parent) : QWidget(parent)
 {
@@ -41,6 +48,8 @@
 	m_points.resize(0);
 	m_points.addPoint(0.0, 0.0);
 	m_points.addPoint(1.0, 1.0);
+	m_points_back = m_points.copy();
+
 	setFocusPolicy(Qt::StrongFocus);
 }
 
@@ -50,52 +59,58 @@
 
 void KCurve::paintEvent(QPaintEvent *)
 {
-	int x = 0;
-	int wWidth = width() - 1;
-	int wHeight = height() - 1;
-	// Drawing selection or all histogram values.
-	QPainter p1;
-	p1.begin(this);
-	//  draw background
-	p1.fillRect(QRect(0, 0, wWidth, wHeight), QColor(255, 255, 255));
-	// Draw grid separators.
-	p1.setPen(QPen(Qt::gray, 1, Qt::SolidLine));
-	p1.drawLine(wWidth/4, 0, wWidth/4, wHeight);
-	p1.drawLine(wWidth/2, 0, wWidth/2, wHeight);
-	p1.drawLine(3*wWidth/4, 0, 3*wWidth/4, wHeight);
-	p1.drawLine(0, wHeight/4, wWidth, wHeight/4);
-	p1.drawLine(0, wHeight/2, wWidth, wHeight/2);
-	p1.drawLine(0, 3*wHeight/4, wWidth, 3*wHeight/4);
+	int wWidth = width();
+	int wHeight = height();
+	QPointF offset(-.5, -.5);
+	QColor colorHandle = palette().color(QPalette::Highlight);
+	QColor colorBackground = palette().color(QPalette::Base);
+	QColor colorGraph = palette().color(QPalette::WindowText);
+	QColor colorGrid = palette().color(QPalette::Midlight); // Qt::gray
+	QPainterPath path = curvePath();
 
-	// Draw curve.
-	double curvePrevVal = getCurveValue(0.0);
-	p1.setPen(QPen(Qt::black, 1, Qt::SolidLine));
-	for (x = 0 ; x < wWidth ; x++)
+	// Drawing selection or all histogram values
+	QPainter painter;
+	painter.begin(this);
+	painter.setRenderHint(QPainter::Antialiasing, true);
+
+	// Draw background
+	painter.fillRect(this->rect(), colorBackground);
+
+	// Draw grid separators
+	painter.setPen(QPen(colorGrid, 1, Qt::SolidLine));
+	painter.drawLine( QPoint(wWidth * .25, 0) - offset, QPoint(wWidth * .25, wHeight) - offset );
+	painter.drawLine( QPoint(wWidth * .5, 0) - offset, QPoint(wWidth * .5, wHeight) - offset );
+	painter.drawLine( QPoint(wWidth * .75, 0) - offset, QPoint(wWidth * .75, wHeight) - offset );
+	painter.drawLine( QPoint(0, wHeight * .25) - offset, QPoint(wWidth, wHeight * .25) - offset );
+	painter.drawLine( QPoint(0, wHeight * .5) - offset, QPoint(wWidth, wHeight * .5) - offset );
+	painter.drawLine( QPoint(0, wHeight * .75) - offset, QPoint(wWidth, wHeight * .75) - offset );
+
+	// Draw curve
+	painter.setPen(QPen(colorGraph, 2, Qt::SolidLine));
+	painter.drawPath(path);
+	painter.setBrush(Qt::NoBrush);
+
+	// Draw handles
+	for (int i = 0; i < m_points.size(); i++)
 	{
-		double curveX;
-		double curveVal;
-		//		curveX = (x + 0.5) / wWidth;
-		curveX = x / static_cast<double>(wWidth);
-		curveVal = getCurveValue(curveX);
-		p1.drawLine(x - 1, wHeight - int(curvePrevVal * wHeight), x,     wHeight - int(curveVal * wHeight));
-		curvePrevVal = curveVal;
-	}
-	p1.drawLine(x - 1, wHeight - int(curvePrevVal * wHeight), x,     wHeight - int(getCurveValue(1.0) * wHeight));
-	for (int dh = 0; dh < m_points.size(); dh++)
-	{
-		FPoint p = m_points.point(dh);
-		if (p == m_grab_point)
+		FPoint p = m_points.point(i);
+		QPointF dot = QPointF(p.x() * width(), (1 - p.y()) * height());
+
+		if(i == selectedPoint())
 		{
-			p1.setPen(QPen(Qt::red, 3, Qt::SolidLine));
-			p1.drawEllipse( int(p.x() * wWidth) - 2, wHeight - 2 - int(p.y() * wHeight), 4, 4 );
+			painter.setPen(QPen(colorHandle, 2, Qt::SolidLine));
+			painter.setBrush(colorHandle);
+			painter.drawEllipse( dot, POINT_RADIUS, POINT_RADIUS );
 		}
 		else
 		{
-			p1.setPen(QPen(Qt::red, 1, Qt::SolidLine));
-			p1.drawEllipse( int(p.x() * wWidth) - 3, wHeight - 3 - int(p.y() * wHeight), 6, 6 );
+			painter.setPen(QPen(colorGraph, 2, Qt::SolidLine));
+			painter.setBrush(colorBackground);
+			painter.drawEllipse( dot, POINT_RADIUS, POINT_RADIUS );
 		}
 	}
-	p1.end();
+
+	painter.end();
 }
 
 void KCurve::keyPressEvent(QKeyEvent *e)
@@ -104,29 +119,10 @@
 	{
 		if (m_points.size() > 2)
 		{
-			FPoint closest_point = m_points.point(0);
-			FPoint p = m_points.point(0);
-			int pos = 0;
-			int cc = 0;
-			double distance = 1000; // just a big number
-			while (cc < m_points.size())
-			{
-				p = m_points.point(cc);
-				if (fabs (m_grab_point.x() - p.x()) < distance)
-				{
-					distance = fabs(m_grab_point.x() - p.x());
-					closest_point = p;
-					m_pos = pos;
-				}
-				cc++;
-				pos++;
-			}
-			FPointArray cli;
-			cli.putPoints(0, m_pos, m_points);
-			cli.putPoints(cli.size(), m_points.size()-m_pos-1, m_points, m_pos+1);
-			m_points.resize(0);
-			m_points = cli.copy();
-			m_grab_point = closest_point;
+			m_points.removeAt(selectedPoint());
+
+			m_selectedPoint = qBound(0, m_selectedPoint - 1, m_points.size() - 1);
+
 			repaint();
 			emit modified();
 			QWidget::keyPressEvent(e);
@@ -136,84 +132,55 @@
 
 void KCurve::mousePressEvent ( QMouseEvent * e )
 {
-	FPoint closest_point = FPoint();
-	double distance;
+
 	if (e->button() != Qt::LeftButton)
 		return;
-	double x = e->pos().x() / (float)width();
-	double y = 1.0 - e->pos().y() / (float)height();
-	distance = 1000; // just a big number
-	FPoint p = m_points.point(0);
-	int insert_pos =0;
-	int pos = 0;
-	int cc = 0;
-	while (cc < m_points.size())
+
+	// invert mouseY
+	m_mousePos = QPointF( e->pos().x(), (height() - e->pos().y()));
+
+	m_leftmost = - 1;
+	m_selectedPoint = 0;
+	m_rightmost = m_points.size();
+
+	m_dragging = false;
+
+	for(int i = 0; i < m_points.size(); i++)
 	{
-		p = m_points.point(cc);
-		if (fabs (x - p.x()) < distance)
+		QPointF pt(m_points[i].x() * width(), m_points[i].y() * height());
+
+		// calculate left point of mouse position
+		if(pt.x() >= 0 && pt.x() <= m_mousePos.x())
+			m_leftmost = i;
+
+		if( distance(pt, m_mousePos) <= POINT_RADIUS + TOLLERANCE )
 		{
-			distance = fabs(x - p.x());
-			closest_point = p;
-			insert_pos = pos;
-		}
-		cc++;
-		pos++;
-	}
-	m_pos = insert_pos;
-	m_grab_point = closest_point;
-	m_grabOffsetX = m_grab_point.x() - x;
-	m_grabOffsetY = m_grab_point.y() - y;
-	m_grab_point = FPoint(x + m_grabOffsetX, y + m_grabOffsetY);
-	double curveVal = getCurveValue(x);
-	if (distance * width() > 5)
-	{
-		m_dragging = false;
-		if (fabs(y - curveVal) * width() > 5)
+			m_selectedPoint = i;
+			m_leftmost = i - 1;
+			m_rightmost = i + 1;
+
+			m_dragging = true;
+			repaint();
 			return;
-		if (m_points.size() < 14)
-		{
-			if (x > closest_point.x())
-				m_pos++;
-			FPointArray cli;
-			cli.putPoints(0, m_pos, m_points);
-			cli.resize(cli.size()+1);
-			cli.putPoints(cli.size()-1, 1, x, curveVal);
-			cli.putPoints(cli.size(), m_points.size()-m_pos, m_points, m_pos);
-			m_points.resize(0);
-			m_points = cli.copy();
-			m_dragging = true;
-			m_grab_point = m_points.point(m_pos);
-			m_grabOffsetX = m_grab_point.x() - x;
-			m_grabOffsetY = m_grab_point.y() - curveVal;
-			m_grab_point = FPoint(x + m_grabOffsetX, curveVal + m_grabOffsetY);
-			setCursor(QCursor(Qt::CrossCursor));
 		}
+
 	}
-	else
+
+	// add new point
+	if(!m_dragging)
 	{
-		if (fabs(y - closest_point.y()) * width() > 5)
-			return;
+		FPoint onCurve(m_mousePos.x() / width(), m_mousePos.y() / height() );
+		m_selectedPoint = m_leftmost + 1;
+		m_rightmost = m_leftmost + 2;
+		m_points.insert(m_selectedPoint, onCurve );
+
 		m_dragging = true;
-		setCursor(QCursor(Qt::CrossCursor));
+
+		repaint();
+		emit modified();
 	}
-	// Determine the leftmost and rightmost points.
-	m_leftmost = 0;
-	m_rightmost = 1;
-	cc = 0;
-	while (cc < m_points.size())
-	{
-		p = m_points.point(cc);
-		if (p != m_grab_point)
-		{
-			if (p.x() > m_leftmost && p.x() < x)
-				m_leftmost = p.x();
-			if (p.x() < m_rightmost && p.x() > x)
-				m_rightmost = p.x();
-		}
-		cc++;
-    }
-	repaint();
-	emit modified();
+
+
 }
 
 void KCurve::mouseReleaseEvent ( QMouseEvent * e )
@@ -228,57 +195,113 @@
 
 void KCurve::mouseMoveEvent ( QMouseEvent * e )
 {
-	double x = e->pos().x() / (float)width();
-	double y = 1.0 - e->pos().y() / (float)height();
-
-	if (!m_dragging)   // If no point is selected set the the cursor shape if on top
+	if(m_dragging)
 	{
-		double distance = 1000;
-		double ydistance = 1000;
-		int cc = 0;
-		while (cc < m_points.size())
-		{
-			FPoint p = m_points.point(cc);
-			if (fabs (x - p.x()) < distance)
-			{
-				distance = fabs(x - p.x());
-				ydistance = fabs(y - p.y());
-			}
-			cc++;
-		}
-		if (distance * width() > 5 || ydistance * height() > 5)
-			setCursor(QCursor(Qt::ArrowCursor));
-		else
-			setCursor(QCursor(Qt::CrossCursor));
-	}
-	else  // Else, drag the selected point
-	{
 		setCursor(QCursor(Qt::CrossCursor));
-		x += m_grabOffsetX;
-		y += m_grabOffsetY;
-		if (x <= m_leftmost)
-			x = m_leftmost + 1E-4; // the addition so we can grab the dot later.
-		if (x >= m_rightmost)
-			x = m_rightmost - 1E-4;
-		if (y > 1.0)
-			y = 1.0;
-		if (y < 0.0)
-			y = 0.0;
-		m_grab_point = FPoint(x, y);
-		m_points.setPoint( m_pos, m_grab_point);
+
+		// invert mouseY
+		QPointF mouse(e->pos().x(), height() - e->pos().y());
+
+		// left bound
+		qreal leftBound = (m_leftmost < 0) ? 0. : m_points.at(m_leftmost).x();
+		qreal rightBound = (m_rightmost >= m_points.size()) ? 1. : m_points.at(m_rightmost).x();
+
+		// limit to bounds
+		qreal yPos = qBound(0., mouse.y() / height(), 1.);
+		qreal xPos = qBound(leftBound + 1E-4, mouse.x() / width(), rightBound - 1E-4);
+
+		QPointF pointSelectedDot(xPos, yPos );
+
+		// set new position
+		m_points.setPoint( m_selectedPoint, pointSelectedDot);
+
 		repaint();
 		emit modified();
 	}
 }
 
-double KCurve::getCurveValue(double x)
+FPointArray KCurve::getCurve()
 {
-	return getCurveYValue(m_points, x, m_linear);
+	return m_points.copy();
 }
 
-FPointArray KCurve::getCurve()
+
+QPainterPath KCurve::curvePath()
 {
-	return m_points.copy();
+	QPainterPath path;
+
+	if(m_points.size() <= 0)
+	{
+		qWarning() << Q_FUNC_INFO << "Can't create path because there are no path nodes!";
+		return path;
+	}
+
+	QPointF pointStart( 0, (1 - m_points.at(0).y()) * height() );
+	QPointF pointEnd( width(), (1 - m_points.at(m_points.size() - 1).y()) * height() );
+
+	QList<QPointF> points;
+	points.append( pointStart );
+
+	// Calculate absolute position of each point
+	for (int i = 0; i < m_points.size(); i++)
+	{
+		FPoint p = m_points.at(i);
+		points.append(QPointF(p.x() * width(), (1 - p.y()) * height()));
+	}
+
+	points.append( pointEnd );
+
+	// Add start line
+	path.moveTo( pointStart );
+	path.lineTo( points.at(1) );
+
+	for (int id = 0; id < points.size(); id++)
+	{
+
+		if(m_linear)
+		{
+			path.lineTo(points.at(id).toPoint());
+		}
+		else
+		{
+
+			int i0 = id-1;
+			int i1 = id;
+			int i2 = id+1;
+			int i3 = id+2;
+
+			if(i0 < 0 || i2 > points.size()-1 || i3 > points.size() - 1)
+				continue;
+
+			FPoint p0 = (i0 >= 0) ? points.at(i0) : points.at(i1);
+			FPoint p1 = points.at(i1);
+			FPoint p2 = (i2 < points.size()) ? points.at(i2) : points.at(points.size() - 1);
+			FPoint p3 = (i3 < points.size()) ? points.at(i3) : points.at(points.size() - 1);
+
+			QList<QPointF> inputPoints;
+			inputPoints.append( p0.toQPointF() ); // Previous point
+			inputPoints.append( p1.toQPointF() ); // Point
+			inputPoints.append( p2.toQPointF() ); // Next point
+			inputPoints.append( p3.toQPointF() ); // Next but one point
+
+			double t = qMax(width() / (p2.x() - p1.x()) / 6, 1.);
+
+			QList<QPointF> *bezPoints = catmullToBezier(inputPoints, t);
+
+			path.cubicTo(
+						QPointF( bezPoints->at(1) ), // Control 1
+						QPointF( bezPoints->at(2) ), // Control 2
+						QPointF( bezPoints->at(3) ) // Next Point
+						);
+		}
+
+	}
+
+	// Add end line
+	path.lineTo( pointEnd );
+
+	return path;
+
 }
 
 void KCurve::setCurve(const FPointArray& inlist)
@@ -310,6 +333,18 @@
 	return m_linear;
 }
 
+int KCurve::selectedPoint()
+{
+	return m_points.size() > 0 ? qBound(0, m_selectedPoint, m_points.size() -1) : -1;
+}
+
+
+/* ********************************************************************************* *
+ *
+ * Constructor + Setup
+ *
+ * ********************************************************************************* */
+
 CurveWidget::CurveWidget( QWidget* parent ) : QWidget( parent )
 {
 	CurveWidgetLayout = new QHBoxLayout(this);
Index: scribus/ui/curvewidget.h
===================================================================
--- scribus/ui/curvewidget.h	(Revision 25725)
+++ scribus/ui/curvewidget.h	(Arbeitskopie)
@@ -44,6 +44,7 @@
 	FPointArray getCurve();
 	void setCurve(const FPointArray& inlist);
 	void resetCurve();
+	QPainterPath curvePath();
 	void setLinear(bool setter);
 	bool isLinear();
 
@@ -53,14 +54,13 @@
 private:
 	double m_leftmost { 0.0 };
 	double m_rightmost { 0.0 };
-	FPoint m_grab_point;
-	int m_pos { 0 };
 	bool m_dragging { false };
 	bool m_linear { false };
-	double m_grabOffsetX { 0.0 };
-	double m_grabOffsetY { 0.0 };
 	FPointArray m_points;
 	FPointArray m_points_back;
+	QPointF m_mousePos;
+	int m_selectedPoint { 0 };
+	int selectedPoint();
 };
 
 class SCRIBUS_API CurveWidget : public QWidget
Index: scribus/util_math.cpp
===================================================================
--- scribus/util_math.cpp	(Revision 25725)
+++ scribus/util_math.cpp	(Arbeitskopie)
@@ -547,3 +547,28 @@
 	dx = lineX.x1();
 	dy = lineX.y1();
 }
+
+QList<QPointF> *catmullToBezier(QList<QPointF> inputPoints, double t)
+{
+	// Inspired by the explanation on: https://pomax.github.io/bezierinfo/#catmullconv
+
+	QList<QPointF> *result = new QList<QPointF>();
+
+ //   qDebug() << "t" << t;
+
+	double x0 = inputPoints[1].x();
+	double y0 = inputPoints[1].y();
+	double x1 = inputPoints[1].x() + (inputPoints[2].x() - inputPoints[0].x()) / (6 * t);
+	double y1 = inputPoints[1].y() + (inputPoints[2].y() - inputPoints[0].y()) / (6 * t);
+	double x2 = inputPoints[2].x() - (inputPoints[3].x() - inputPoints[1].x()) / (6 * t);
+	double y2 = inputPoints[2].y() - (inputPoints[3].y() - inputPoints[1].y()) / (6 * t);
+	double x3 = inputPoints[2].x();
+	double y3 = inputPoints[2].y();
+
+	result->append( QPointF(x0, y0) );
+	result->append( QPointF(x1, y1) );
+	result->append( QPointF(x2, y2) );
+	result->append( QPointF(x3, y3) );
+
+	return result;
+}
Index: scribus/util_math.h
===================================================================
--- scribus/util_math.h	(Revision 25725)
+++ scribus/util_math.h	(Arbeitskopie)
@@ -36,6 +36,13 @@
 bool     SCRIBUS_API regionContainsRect(const QRegion& shape, QRect rect);
 QPolygon SCRIBUS_API flattenPath(const FPointArray& ina, QList<uint> &segments);
 QList<QPainterPath> SCRIBUS_API decomposePath(const QPainterPath &path);
+/**
+ * @brief CatmullToBezier
+ * @param inputPoints Point Array of at least 4 points (p0, p1, p2, p3). You will get cubic bezier points from p1 to p2.
+ * @param t
+ * @return
+ */
+QList<QPointF> *catmullToBezier(QList<QPointF> inputPoints, double t = 1);
 QPainterPath  SCRIBUS_API regularPolygonPath(double w, double h, uint c, bool star, double factor, double rota, double factor2 = 0.0, double innerRot = 0.0, double factor3 = 0.0);
 QPainterPath  SCRIBUS_API spiralPath(double spiralWidth, double spiralHeight, double spiralStartAngle, double spiralEndAngle, double spiralFactor);
 inline double SCRIBUS_API xy2Deg(double x, double y);
@@ -43,6 +50,7 @@
 inline double SCRIBUS_API cosd(double);
 inline double SCRIBUS_API square(double);
 inline double SCRIBUS_API distance(double, double);
+inline double SCRIBUS_API distance(QPointF p1, QPointF p2);
 /*! \brief Constrains an angle of rotation to 45 degree intervals
    Will make code simpler and reduce interval or provide as a parameter
    \param angle angle Angle in degrees
@@ -78,6 +86,12 @@
 	return sqrt(x*x + y*y);
 }
 
+inline double distance(QPointF p1, QPointF p2)
+{
+	QPointF point = p2 - p1;
+	return point.manhattanLength();
+}
+
 inline double xy2Deg(double x, double y)
 {
 	return atan2(y,x) * (180.0/M_PI);
curvewidget_2023-10-08_03.patch (16,450 bytes)   

cbradney

2023-10-08 20:19

administrator   ~0050377

That fixed it

jghali

2023-10-19 20:23

administrator   ~0050393

I have now applied your patches. i've brought two noticeable changes tho:
- I renamed the distance(QPointF, QPointF) function to manhattanDistance() to avoid any ambiguity with euclidian distance
- I modified the allocation method of the returned point list in catmullToBezier() in order to avoid a memory leak

Issue History

Date Modified Username Field Change
2023-10-08 11:10 nitramr New Issue
2023-10-08 11:10 nitramr Status new => assigned
2023-10-08 11:10 nitramr Assigned To => nitramr
2023-10-08 11:10 nitramr File Added: curvewidget_2023-10-08_01.patch
2023-10-08 11:10 nitramr File Added: old-widget.png
2023-10-08 11:10 nitramr File Added: new-widget.png
2023-10-08 14:10 cbradney Note Added: 0050370
2023-10-08 15:07 nitramr Note Added: 0050371
2023-10-08 15:53 cbradney Note Added: 0050372
2023-10-08 16:31 nitramr Note Added: 0050373
2023-10-08 16:31 nitramr File Added: curvewidget_2023-10-08_02.patch
2023-10-08 19:25 cbradney Note Added: 0050375
2023-10-08 19:59 nitramr Note Added: 0050376
2023-10-08 19:59 nitramr File Added: curvewidget_2023-10-08_03.patch
2023-10-08 20:19 cbradney Note Added: 0050377
2023-10-19 20:23 jghali Status assigned => resolved
2023-10-19 20:23 jghali Resolution open => fixed
2023-10-19 20:23 jghali Fixed in Version => 1.7.0.svn
2023-10-19 20:23 jghali Note Added: 0050393
2023-11-04 09:09 cbradney Status resolved => closed