2 // TTEDIT.CPP - The TrueType Editor
4 // (C) 2004 Underground Software
6 // JLH = James L. Hammons <jlhamm@acm.org>
9 // --- ---------- -------------------------------------------------------------
10 // JLH 08/28/2008 Created this file
11 // JLH 09/02/2008 Separated scrolling from dedicated tool to MMB drag
12 // JLH 03/13/2009 Converted from wxWidgets to Qt
21 // - Fix bug in Glyphpoints when dragging on an empty canvas or loading a font
22 // - Fix zooming, settings (ini)
23 // - Fix point adding bug 1: should be able to add points to empty canvas
24 // - Fix point adding bug 2: should be able to add point successfully to single
26 // - Add poly multi-select
27 // - Add point multi-select
31 // Uncomment this for debugging...
33 #define DEBUGFOO // Various tool debugging...
34 #define DEBUGTP // Toolpalette debugging...
36 #include "editwindow.h"
37 #include "charwindow.h"
39 #include "graphicprimitives.h"
40 #include "mainwindow.h"
45 EditWindow::EditWindow(QWidget * parent/*= NULL*/): QWidget(parent),
46 scale(1.0), offsetX(-10), offsetY(-10), tool(TOOLSelect),
47 ptHighlight(-1), oldPtHighlight(-1), ptNextHighlight(-1), oldPtNextHighlight(-1),
48 polyFirstPoint(true), showRotationCenter(false), haveZeroPoint(false)
50 setBackgroundRole(QPalette::Base);
51 setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
53 toolPalette = new ToolWindow();
55 setCursor(cur[TOOLSelect]);
56 setMouseTracking(true);
60 QSize EditWindow::minimumSizeHint() const
66 QSize EditWindow::sizeHint() const
68 return QSize(400, 400);
72 void EditWindow::CreateCursors(void)
74 int hotx[11] = { 1, 1, 11, 15, 1, 1, 1, 1, 1, 1, 1 };
75 int hoty[11] = { 1, 1, 11, 13, 1, 1, 1, 1, 1, 1, 1 };
76 char cursorName[11][48] = { "select", "select-poly", "scroll", "zoom", "add-point",
77 "add-poly", "del-point", "del-poly", "rotate", "rotate", "select" };
79 for(int i=0; i<11; i++)
82 s.sprintf(":/res/cursor-%s.png", cursorName[i]);
84 cur[i] = QCursor(pmTmp, hotx[i], hoty[i]);
89 QPoint EditWindow::GetAdjustedMousePosition(QMouseEvent * event)
91 QSize winSize = size();
92 // This is undoing the transform, e.g. going from client coords to local coords.
93 // In essence, the height - y is height + (y * -1), the (y * -1) term doing the
94 // conversion of the y-axis from increasing bottom to top.
95 return QPoint(offsetX + event->x(), offsetY + (winSize.height() - event->y()));
99 QPoint EditWindow::GetAdjustedClientPosition(int x, int y)
101 QSize winSize = size();
103 // VOODOO ALERT (ON Y COMPONENT!!!!)
104 return QPoint(-offsetX + x, (winSize.height() - (-offsetY + y)) * +1.0);
110 o Different colors for polys on selected points
111 o Different colors for handles on non-selected polys
112 o Line of sight (dashed, dotted) for off-curve points
113 o Repaints for press/release of CTRL/SHIFT during point creation
115 void EditWindow::paintEvent(QPaintEvent * /*event*/)
118 //hm, causes lockup (or does it???)
119 p.setRenderHint(QPainter::Antialiasing);
121 QSize winSize = size();
123 p.translate(QPoint(-offsetX, winSize.height() - (-offsetY)));
126 // Scrolling can be done by using OffsetViewportOrgEx
127 // Scaling can be done by adjusting SetWindowExtEx (it's denominator of txform)
128 // you'd use: % = ViewportExt / WindowExt
129 // But it makes the window look like crap: fuggetuboutit.
130 // Instead, we have to scale EVERYTHING by hand. Crap!
131 // It's not *that* bad, but not as convenient either...
133 p.setPen(QPen(Qt::blue, 1.0, Qt::DotLine));
135 // Draw coordinate axes
137 p.drawLine(0, -16384, 0, 16384);
138 p.drawLine(-16384, 0, 16384, 0);
140 // Draw rotation center (if active)
142 if (showRotationCenter)
144 p.setPen(QPen(Qt::red, 2.0, Qt::SolidLine));
145 p.drawLine(rotationCenter.x() + 7, rotationCenter.y(), rotationCenter.x() - 7, rotationCenter.y());
146 p.drawLine(rotationCenter.x(), rotationCenter.y() + 7, rotationCenter.x(), rotationCenter.y() - 7);
151 for(int i=0; i<pts.GetNumPoints(); i++)
153 if (i == ptHighlight)
155 p.setPen(QPen(Qt::red, 1.0, Qt::SolidLine));
157 if (pts.GetOnCurve(i))
159 DrawSquareDotN(p, pts.GetX(i), pts.GetY(i), 7);
160 DrawSquareDotN(p, pts.GetX(i), pts.GetY(i), 9);
164 DrawRoundDotN(p, pts.GetX(i), pts.GetY(i), 7);
165 DrawRoundDotN(p, pts.GetX(i), pts.GetY(i), 9);
168 else if ((i == ptHighlight || i == ptNextHighlight) && tool == TOOLAddPt)
170 p.setPen(QPen(Qt::green, 1.0, Qt::SolidLine));
172 if (pts.GetOnCurve(i))
174 DrawSquareDotN(p, pts.GetX(i), pts.GetY(i), 7);
175 DrawSquareDotN(p, pts.GetX(i), pts.GetY(i), 9);
179 DrawRoundDotN(p, pts.GetX(i), pts.GetY(i), 7);
180 DrawRoundDotN(p, pts.GetX(i), pts.GetY(i), 9);
185 p.setPen(QPen(Qt::black, 1.0, Qt::SolidLine));
187 if (pts.GetOnCurve(i))
188 DrawSquareDot(p, pts.GetX(i), pts.GetY(i));
190 DrawRoundDot(p, pts.GetX(i), pts.GetY(i));
193 if (tool == TOOLDelPt && i == ptHighlight)
195 p.setPen(QPen(Qt::red, 1.0, Qt::SolidLine));
196 p.drawLine(pts.GetX(i) - 5, pts.GetY(i) - 5, pts.GetX(i) + 5, pts.GetY(i) + 5);
197 p.drawLine(pts.GetX(i) + 5, pts.GetY(i) - 5, pts.GetX(i) - 5, pts.GetY(i) + 5);
201 // Draw curve formed by points
203 p.setPen(QPen(Qt::black, 1.0, Qt::SolidLine));
209 GlyphPoints rotated = pts;
211 if (tool == TOOLRotate)
212 rotated.RotatePoints(rotationAngle, IPoint(rotationCenter.x(), rotationCenter.y()));
213 else if (tool == TOOLRotatePoly)
215 uint16_t poly = rotated.GetPolyForPointNumber(ptHighlight);
216 rotated.RotatePolyAroundCentroid(poly, rotationAngle);
219 p.setPen(QPen(QColor(255, 0, 255), 1.0, Qt::SolidLine));
220 DrawGlyph(p, rotated);
225 void EditWindow::DrawGlyph(QPainter & p, GlyphPoints & glyph)
227 for(int poly=0; poly<glyph.GetNumPolys(); poly++)
230 if (glyph.GetNumPoints(poly) < 3)
233 // Initial move: If our start point is on curve, then go to it. Otherwise,
234 // check previous point. If it's on curve, go to it otherwise go the
235 // midpoint between start point and previous (since it's between two curve
237 IPoint pt = (glyph.GetOnCurve(poly, 0)
238 ? glyph.GetPoint(poly, 0) : (glyph.GetPrevOnCurve(poly, 0)
239 ? glyph.GetPrevPoint(poly, 0) : glyph.GetMidpointToPrev(poly, 0)));
241 // Need to add separate color handling here for polys that are being manipulated...
243 for(int i=0; i<glyph.GetNumPoints(poly); i++)
245 // If this point and then next are both on curve, we have a line...
246 if (glyph.GetOnCurve(poly, i) && glyph.GetNextOnCurve(poly, i))
248 IPoint pt2 = glyph.GetNextPoint(poly, i);
249 p.drawLine(pt.x, pt.y, pt2.x, pt2.y);
254 // Skip point if it's on curve (start of curve--it's already
255 // been plotted so we don't need to handle it...)
256 if (glyph.GetOnCurve(poly, i))
259 // We are now guaranteed that we are sitting on a curve control point
260 // (off curve). Figure the extent of the curve: If the following is a
261 // curve control point, then use the midpoint to it otherwise go to
262 // the next point since it's on curve.
263 IPoint pt2 = (glyph.GetNextOnCurve(poly, i)
264 ? glyph.GetNextPoint(poly, i) : glyph.GetMidpointToNext(poly, i));
266 Bezier(p, pt, glyph.GetPoint(poly, i), pt2);
271 DrawGlyphPoly(p, glyph, poly);
277 void EditWindow::DrawGlyphPoly(QPainter & p, GlyphPoints & glyph, uint16_t poly)
280 if (glyph.GetNumPoints(poly) < 3)
283 // Initial move: If our start point is on curve, then go to it. Otherwise,
284 // check previous point. If it's on curve, go to it otherwise go the
285 // midpoint between start point and previous (since it's between two curve
287 IPoint pt = (glyph.GetOnCurve(poly, 0)
288 ? glyph.GetPoint(poly, 0) : (glyph.GetPrevOnCurve(poly, 0)
289 ? glyph.GetPrevPoint(poly, 0) : glyph.GetMidpointToPrev(poly, 0)));
291 for(int i=0; i<glyph.GetNumPoints(poly); i++)
293 // If this point and then next are both on curve, we have a line...
294 if (glyph.GetOnCurve(poly, i) && glyph.GetNextOnCurve(poly, i))
296 IPoint pt2 = glyph.GetNextPoint(poly, i);
297 p.drawLine(pt.x, pt.y, pt2.x, pt2.y);
302 // Skip point if it's on curve (start of curve--it's already
303 // been plotted so we don't need to handle it...)
304 if (glyph.GetOnCurve(poly, i))
307 // We are now guaranteed that we are sitting on a curve control point
308 // (off curve). Figure the extent of the curve: If the following is a
309 // curve control point, then use the midpoint to it otherwise go to
310 // the next point since it's on curve.
311 IPoint pt2 = (glyph.GetNextOnCurve(poly, i)
312 ? glyph.GetNextPoint(poly, i) : glyph.GetMidpointToNext(poly, i));
314 Bezier(p, pt, glyph.GetPoint(poly, i), pt2);
321 void EditWindow::mousePressEvent(QMouseEvent * event)
323 if (event->button() == Qt::RightButton)
325 toolPalette->move(event->globalPos());
326 toolPalette->setVisible(true);
327 setCursor(cur[TOOLSelect]);
328 toolPalette->prevTool = TOOLSelect;
330 else if (event->button() == Qt::MidButton)
332 setCursor(cur[2]); // Scrolling cursor
334 else if (event->button() == Qt::LeftButton)
336 if (tool == TOOLScroll || tool == TOOLZoom)
337 ;//meh CaptureMouse(); // Make sure we capture the mouse when in scroll/zoom mode
338 else if (tool == TOOLAddPt) // "Add Point" tool
340 QPoint pt = GetAdjustedMousePosition(event);
341 IPoint pointToAdd(pt.x(), pt.y(), ((event->modifiers() == Qt::ShiftModifier || event->modifiers() == Qt::ControlModifier) ? false : true));
343 if (pts.GetNumPoints() < 2)
345 // pts += IPoint(pt.x(), pt.y(), ((event->modifiers() == Qt::ShiftModifier || event->modifiers() == Qt::ControlModifier) ? false : true));
347 ptHighlight = pts.GetNumPoints() - 1;
351 // QPoint pt = GetAdjustedMousePosition(event);
352 // pts.InsertPoint(pts.GetNext(ptHighlight), pt.x(), pt.y(), ((event->modifiers() == Qt::ShiftModifier || event->modifiers() == Qt::ControlModifier) ? false : true));
353 pts.InsertPoint(pts.GetNext(ptHighlight), pointToAdd);
354 ptHighlight = ptNextHighlight;
360 else if (tool == TOOLAddPoly) // "Add Poly" tool
363 WriteLogMsg("Adding point... # polys: %u, # points: %u", pts.GetNumPolys(), pts.GetNumPoints());
367 polyFirstPoint = false;
368 pts.AddNewPolyAtEnd();
371 QPoint pt = GetAdjustedMousePosition(event);
372 //printf("GetAdjustedMousePosition = %i, %i\n", pt.x(), pt.y());
373 // Append a point to the end of the structure
374 pts += IPoint(pt.x(), pt.y(), ((event->modifiers() == Qt::ShiftModifier || event->modifiers() == Qt::ControlModifier) ? false : true));
375 ptHighlight = pts.GetNumPoints() - 1;
378 WriteLogMsg(" --> [# polys: %u, # points: %u]\n", pts.GetNumPolys(), pts.GetNumPoints());
381 else if (tool == TOOLSelect || tool == TOOLPolySelect)
383 if (pts.GetNumPoints() > 0)
385 pt = GetAdjustedClientPosition(pts.GetX(ptHighlight), pts.GetY(ptHighlight));
386 //printf("GetAdjustedClientPosition = %i, %i\n", pt.x(), pt.y());
387 // WarpPointer(pt.x, pt.y);
388 QCursor::setPos(mapToGlobal(pt));
390 if (event->modifiers() == Qt::ShiftModifier || event->modifiers() == Qt::ControlModifier)
392 pts.SetOnCurve(ptHighlight, !pts.GetOnCurve(ptHighlight));
397 else if (tool == TOOLDelPt)
399 if (pts.GetNumPoints() > 0)
401 // if (ptHighlight != -1)
403 //This assumes that WM_MOUSEMOVE happens before this!
404 //The above commented out line should take care of this contingency... !!! FIX !!!
405 pts.DeletePoint(ptHighlight);
409 else if (tool == TOOLRotate)
411 // I think what's needed here is to keep the initial mouse click,
412 // paint the rotation center, then use the 1st mouse move event to establish
413 // the rotation "zero line", which becomes the line of reference to all
414 // subsequent mouse moves.
415 rotationCenter = GetAdjustedMousePosition(event);
416 showRotationCenter = true;
417 haveZeroPoint = false;
421 else if (tool == TOOLRotatePoly)
423 IPoint centroid = pts.GetPolyCentroid(pts.GetPolyForPointNumber(ptHighlight));
424 rotationCenter = QPoint(centroid.x, centroid.y);
425 showRotationCenter = true;
426 pt = GetAdjustedClientPosition(pts.GetX(ptHighlight), pts.GetY(ptHighlight));
427 QCursor::setPos(mapToGlobal(pt));
428 rotationZeroPoint = QPoint(pts.GetX(ptHighlight), pts.GetY(ptHighlight));
429 haveZeroPoint = true;
433 else if (tool == TOOLFlipWinding)
435 pts.InvertPolyDrawSequence(pts.GetPolyForPointNumber(ptHighlight));
436 pt = GetAdjustedClientPosition(pts.GetX(ptHighlight), pts.GetY(ptHighlight));
437 QCursor::setPos(mapToGlobal(pt));
446 void EditWindow::mouseMoveEvent(QMouseEvent * event)
448 if (event->buttons() == Qt::RightButton)
450 ToolType newTool = toolPalette->FindSelectedTool();
452 if (newTool != toolPalette->prevTool)
454 toolPalette->prevTool = newTool;
455 toolPalette->repaint();
458 else if (event->buttons() == Qt::MidButton)
460 // Calc offset from previous point
462 ptOffset = QPoint(pt.x() - ptPrevious.x(), pt.y() - ptPrevious.y());
464 // Then multiply it by the scaling factor. Whee!
465 // This looks wacky because we're using screen coords for the offset...
466 // Otherwise, we would subtract both offsets!
467 offsetX -= ptOffset.x(), offsetY += ptOffset.y();
471 else if (event->buttons() == Qt::LeftButton)
473 if (tool == TOOLAddPt || tool == TOOLAddPoly || tool == TOOLSelect)
475 // Bail out if we have the select tool and no points yet...
476 if (tool == TOOLSelect && pts.GetNumPoints() == 0)
479 QPoint pt2 = GetAdjustedMousePosition(event);
480 pts.SetXY(ptHighlight, pt2.x(), pt2.y());
483 else if (tool == TOOLPolySelect)
485 if (pts.GetNumPoints() > 0)
487 QPoint pt2 = GetAdjustedMousePosition(event);
488 // Should also set onCurve here as well, depending on keystate
490 //Would be nice, but we'd need to trap the keyPressEvent() as well, otherwise pressing/releasing
491 //the hotkey would show no change until the user moved their mouse.
492 pts.OffsetPoly(pts.GetPoly(ptHighlight), pt2.x() - pts.GetX(ptHighlight), pt2.y() - pts.GetY(ptHighlight));
496 else if (tool == TOOLRotate || tool == TOOLRotatePoly)
498 if (pts.GetNumPoints() > 0)
502 rotationZeroPoint = GetAdjustedMousePosition(event);
503 haveZeroPoint = true;
507 // Figure out the angle between the "zero" vector and the current one,
508 // then rotate all points relative to the "zero" vector (done by paint())
509 QPoint currentPoint = GetAdjustedMousePosition(event);
510 Vector v1(rotationZeroPoint.x(), rotationZeroPoint.y(), 0,
511 rotationCenter.x(), rotationCenter.y(), 0);
512 Vector v2(currentPoint.x(), currentPoint.y(), 0,
513 rotationCenter.x(), rotationCenter.y(), 0);
514 // rotationAngle = v1.Angle(v2);
515 rotationAngle = v2.Angle(v1);
518 s.sprintf("%.3f degrees", rotationAngle * 180.0 / 3.14159265358979323);
519 ((TTEdit *)qApp)->mainWindow->statusBar()->showMessage(s);
526 else if (event->buttons() == Qt::NoButton)
528 // Moving, not dragging...
529 if (tool == TOOLSelect || tool == TOOLDelPt || tool == TOOLAddPt
530 || tool == TOOLPolySelect || tool == TOOLRotatePoly || tool == TOOLFlipWinding)
532 QPoint pt2 = GetAdjustedMousePosition(event);
533 double closest = 1.0e+99;
535 for(int i=0; i<pts.GetNumPoints(); i++)
537 double dist = ((pt2.x() - pts.GetX(i)) * (pt2.x() - pts.GetX(i)))
538 + ((pt2.y() - pts.GetY(i)) * (pt2.y() - pts.GetY(i)));
541 closest = dist, ptHighlight = i;
544 if (ptHighlight != oldPtHighlight)
546 oldPtHighlight = ptHighlight;
550 // What follows here looks like voodoo, but is really simple. What we do is
551 // check to see if the mouse point has a perpendicular intersection with any of
552 // the line segments. If it does, calculate the length of the perpendicular
553 // and choose the smallest length. If there is no perpendicular, then choose the
554 // length of line connecting the closer of either the first endpoint or the
555 // second and choose the smallest of those.
557 // There is one bit of math that looks like voodoo to me ATM--will explain once
558 // I understand it better (the calculation of the length of the perpendicular).
560 if (pts.GetNumPoints() > 1 && tool == TOOLAddPt)
562 double smallest = 1.0e+99;
564 for(int i=0; i<pts.GetNumPoints(); i++)
566 int32_t p1x = pts.GetX(i), p1y = pts.GetY(i),
567 p2x = pts.GetX(pts.GetNext(i)), p2y = pts.GetY(pts.GetNext(i));
569 Vector ls(p2x, p2y, 0, p1x, p1y, 0), v1(pt2.x(), pt2.y(), 0, p1x, p1y, 0),
570 v2(pt2.x(), pt2.y(), 0, p2x, p2y, 0);
571 double pp = ls.Dot(v1) / ls.Magnitude(), dist;
572 // Geometric interpretation:
573 // pp is the paremeterized point on the vector ls where the perpendicular intersects ls.
574 // If pp < 0, then the perpendicular lies beyond the 1st endpoint. If pp > length of ls,
575 // then the perpendicular lies beyond the 2nd endpoint.
578 dist = v1.Magnitude();
579 else if (pp > ls.Magnitude())
580 dist = v2.Magnitude();
581 else // distance = ?Det?(ls, v1) / |ls|
582 dist = fabs((ls.x * v1.y - v1.x * ls.y) / ls.Magnitude());
584 //The answer to the above looks like it might be found here:
586 //If the segment endpoints are s and e, and the point is p, then the test for the perpendicular
587 //intercepting the segment is equivalent to insisting that the two dot products {s-e}.{s-p} and
588 //{e-s}.{e-p} are both non-negative. Perpendicular distance from the point to the segment is
589 //computed by first computing the area of the triangle the three points form, then dividing by the
590 //length of the segment. Distances are done just by the Pythagorean theorem. Twice the area of the
591 //triangle formed by three points is the determinant of the following matrix:
597 //By translating the start point to the origin, this can be rewritten as:
598 //By subtracting row 1 from all rows, you get the following:
599 //[because sx = sy = 0. you could leave out the -sx/y terms below. because we subtracted
600 // row 1 from all rows (including row 1) row 1 turns out to be zero. duh!]
603 //(ex - sx) (ey - sy) 0
604 //(px - sx) (py - sy) 0
606 //which greatly simplifies the calculation of the determinant.
609 smallest = dist, ptNextHighlight = pts.GetNext(i), ptHighlight = i;
612 if (ptNextHighlight != oldPtNextHighlight)
614 oldPtNextHighlight = ptNextHighlight;
620 ptPrevious = event->pos();
627 void EditWindow::mouseReleaseEvent(QMouseEvent * event)
629 if (event->button() == Qt::RightButton)
631 ToolType newTool = toolPalette->FindSelectedTool();
633 // We only change the tool if a new one was actually selected. Otherwise, we do nothing.
634 if (newTool != TOOLNone)
638 if (tool == TOOLScroll || tool == TOOLZoom || tool == TOOLAddPoly
639 || tool == TOOLDelPoly)
642 if (tool == TOOLAddPoly)
643 polyFirstPoint = true;
646 toolPalette->setVisible(false);
647 setCursor(cur[tool]);
648 // Just in case we changed highlighting style with the new tool...
651 else if (event->button() == Qt::MidButton)
653 setCursor(cur[tool]); // Restore previous cursor
655 else if (event->button() == Qt::LeftButton)
657 if (showRotationCenter)
659 showRotationCenter = false;
660 haveZeroPoint = false;
662 if (tool == TOOLRotate)
663 pts.RotatePoints(rotationAngle, IPoint(rotationCenter.x(), rotationCenter.y()));
666 uint16_t poly = pts.GetPolyForPointNumber(ptHighlight);
667 pts.RotatePolyAroundCentroid(poly, rotationAngle);
671 ((TTEdit *)qApp)->mainWindow->statusBar()->showMessage("");
674 // if (tool == TOOLScroll || tool == TOOLZoom)
676 //this is prolly too much
677 ((TTEdit *)qApp)->charWnd->MakePathFromPoints(&pts);
678 ((TTEdit *)qApp)->charWnd->update();
686 void EditWindow::keyPressEvent(QKeyEvent * event)
688 // Sanity checking...
689 if (ptHighlight == -1)
692 if (event->key() == Qt::Key_Up)
694 pts.SetXY(ptHighlight, pts.GetX(ptHighlight), pts.GetY(ptHighlight) + 1);
696 else if (event->key() == Qt::Key_Down)
697 pts.SetXY(ptHighlight, pts.GetX(ptHighlight), pts.GetY(ptHighlight) - 1);
698 else if (event->key() == Qt::Key_Right)
699 pts.SetXY(ptHighlight, pts.GetX(ptHighlight) + 1, pts.GetY(ptHighlight));
700 else if (event->key() == Qt::Key_Left)
701 pts.SetXY(ptHighlight, pts.GetX(ptHighlight) - 1, pts.GetY(ptHighlight));
707 ((TTEdit *)qApp)->charWnd->MakePathFromPoints(&pts);
708 ((TTEdit *)qApp)->charWnd->update();
712 void EditWindow::keyReleaseEvent(QKeyEvent * /*event*/)