1
2
3 #include "shifttracetool.h"
4 #include "toonz/onionskinmask.h"
5 #include "toonz/tonionskinmaskhandle.h"
6 #include "tools/cursors.h"
7 #include "timage.h"
8 #include "trasterimage.h"
9 #include "ttoonzimage.h"
10 #include "tvectorimage.h"
11 #include "toonz/txsheet.h"
12 #include "toonz/txshcell.h"
13 #include "toonz/txsheethandle.h"
14 #include "toonz/tframehandle.h"
15 #include "toonz/tcolumnhandle.h"
16 #include "toonz/txshlevelhandle.h"
17 #include "tools/toolhandle.h"
18 #include "toonz/txshsimplelevel.h"
19 #include "toonz/dpiscale.h"
20 #include "toonz/stage.h"
21 #include "tpixel.h"
22 #include "toonzqt/menubarcommand.h"
23
24 #include "toonz/preferences.h"
25 #include "toonzqt/gutil.h"
26
27 #include "tgl.h"
28 #include <math.h>
29 #include <QKeyEvent>
30
31 //=============================================================================
32
circumCenter(TPointD & out,const TPointD & a,const TPointD & b,const TPointD & c)33 static bool circumCenter(TPointD &out, const TPointD &a, const TPointD &b,
34 const TPointD &c) {
35 double d = 2 * (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y));
36 if (fabs(d) < 0.0001) {
37 out = TPointD();
38 return false;
39 }
40 out.x = ((a.y * a.y + a.x * a.x) * (b.y - c.y) +
41 (b.y * b.y + b.x * b.x) * (c.y - a.y) +
42 (c.y * c.y + c.x * c.x) * (a.y - b.y)) /
43 d;
44 out.y = ((a.y * a.y + a.x * a.x) * (c.x - b.x) +
45 (b.y * b.y + b.x * b.x) * (a.x - c.x) +
46 (c.y * c.y + c.x * c.x) * (b.x - a.x)) /
47 d;
48 return true;
49 }
50
51 //=============================================================================
52
ShiftTraceTool()53 ShiftTraceTool::ShiftTraceTool()
54 : TTool("T_ShiftTrace")
55 , m_ghostIndex(0)
56 , m_curveStatus(NoCurve)
57 , m_gadget(NoGadget)
58 , m_highlightedGadget(NoGadget) {
59 bind(TTool::AllTargets); // Deals with tool deactivation internally
60 }
61
clearData()62 void ShiftTraceTool::clearData() {
63 m_ghostIndex = 0;
64 m_curveStatus = NoCurve;
65 m_gadget = NoGadget;
66 m_highlightedGadget = NoGadget;
67
68 m_box = TRectD();
69 for (int i = 0; i < 2; i++) {
70 m_row[i] = -1;
71 m_aff[i] = TAffine();
72 m_center[i] = TPointD();
73 }
74 }
75
updateBox()76 void ShiftTraceTool::updateBox() {
77 if (m_ghostIndex < 0 || 2 <= m_ghostIndex || m_row[m_ghostIndex] < 0) return;
78
79 TImageP img;
80
81 TApplication *app = TTool::getApplication();
82 if (app->getCurrentFrame()->isEditingScene()) {
83 int col = app->getCurrentColumn()->getColumnIndex();
84 int row = m_row[m_ghostIndex];
85 TXsheet *xsh = app->getCurrentXsheet()->getXsheet();
86
87 TXshCell cell = xsh->getCell(row, col);
88 TXshSimpleLevel *sl = cell.getSimpleLevel();
89 if (sl) {
90 m_dpiAff = getDpiAffine(sl, cell.m_frameId);
91 img = cell.getImage(false);
92 }
93 }
94 // on editing level
95 else {
96 TXshLevel *level = app->getCurrentLevel()->getLevel();
97 if (!level) return;
98 TXshSimpleLevel *sl = level->getSimpleLevel();
99 if (!sl) return;
100
101 const TFrameId &ghostFid = sl->index2fid(m_row[m_ghostIndex]);
102 m_dpiAff = getDpiAffine(sl, ghostFid);
103 img = sl->getFrame(ghostFid, false);
104 }
105
106 if (img) {
107 if (TRasterImageP ri = img) {
108 TRasterP ras = ri->getRaster();
109 m_box = (convert(ras->getBounds()) - ras->getCenterD()) *
110 ri->getSubsampling();
111 } else if (TToonzImageP ti = img) {
112 TRasterP ras = ti->getRaster();
113 m_box = (convert(ras->getBounds()) - ras->getCenterD()) *
114 ti->getSubsampling();
115 } else if (TVectorImageP vi = img) {
116 m_box = vi->getBBox();
117 }
118 }
119 }
120
updateData()121 void ShiftTraceTool::updateData() {
122 m_box = TRectD();
123 for (int i = 0; i < 2; i++) m_row[i] = -1;
124 m_dpiAff = TAffine();
125 TApplication *app = TTool::getApplication();
126
127 OnionSkinMask osm = app->getCurrentOnionSkin()->getOnionSkinMask();
128 int previousOffset = osm.getShiftTraceGhostFrameOffset(0);
129 int forwardOffset = osm.getShiftTraceGhostFrameOffset(1);
130 // we must find the prev (m_row[0]) and next (m_row[1]) reference images
131 // (either might not exist)
132 // see also stage.cpp, StageBuilder::addCellWithOnionSkin
133 if (app->getCurrentFrame()->isEditingScene()) {
134 TXsheet *xsh = app->getCurrentXsheet()->getXsheet();
135 int row = app->getCurrentFrame()->getFrame();
136 int col = app->getCurrentColumn()->getColumnIndex();
137 TXshCell cell = xsh->getCell(row, col);
138 int r;
139 r = row + previousOffset;
140 if (r >= 0 && xsh->getCell(r, col) != cell &&
141 (cell.getSimpleLevel() == 0 ||
142 xsh->getCell(r, col).getSimpleLevel() == cell.getSimpleLevel())) {
143 m_row[0] = r;
144 }
145
146 r = row + forwardOffset;
147 if (r >= 0 && xsh->getCell(r, col) != cell &&
148 (cell.getSimpleLevel() == 0 ||
149 xsh->getCell(r, col).getSimpleLevel() == cell.getSimpleLevel())) {
150 m_row[1] = r;
151 }
152 }
153 // on editing level
154 else {
155 TXshLevel *level = app->getCurrentLevel()->getLevel();
156 if (level) {
157 TXshSimpleLevel *sl = level->getSimpleLevel();
158 if (sl) {
159 TFrameId fid = app->getCurrentFrame()->getFid();
160 int row = sl->guessIndex(fid);
161 m_row[0] = row + previousOffset;
162 m_row[1] = row + forwardOffset;
163 }
164 }
165 }
166 updateBox();
167 }
168
169 //
170 // Compute m_aff[0] and m_aff[1] according to the current curve
171 //
updateCurveAffs()172 void ShiftTraceTool::updateCurveAffs() {
173 if (m_curveStatus != ThreePointsCurve) {
174 m_aff[0] = m_aff[1] = TAffine();
175 } else {
176 double phi0 = 0, phi1 = 0;
177 TPointD center;
178 if (circumCenter(center, m_p0, m_p1, m_p2)) {
179 TPointD v0 = normalize(m_p0 - center);
180 TPointD v1 = normalize(m_p1 - center);
181 TPointD v2 = normalize(m_p2 - center);
182 TPointD u0(-v0.y, v0.x);
183 TPointD u1(-v1.y, v1.x);
184 phi0 = atan2((v2 * u0), (v2 * v0)) * 180.0 / 3.1415;
185 phi1 = atan2((v2 * u1), (v2 * v1)) * 180.0 / 3.1415;
186 }
187 m_aff[0] = TTranslation(m_p2 - m_p0) * TRotation(m_p0, phi0);
188 m_aff[1] = TTranslation(m_p2 - m_p1) * TRotation(m_p1, phi1);
189 }
190 }
191
updateGhost()192 void ShiftTraceTool::updateGhost() {
193 OnionSkinMask osm =
194 TTool::getApplication()->getCurrentOnionSkin()->getOnionSkinMask();
195 osm.setShiftTraceGhostAff(0, m_aff[0]);
196 osm.setShiftTraceGhostAff(1, m_aff[1]);
197 osm.setShiftTraceGhostCenter(0, m_center[0]);
198 osm.setShiftTraceGhostCenter(1, m_center[1]);
199 TTool::getApplication()->getCurrentOnionSkin()->setOnionSkinMask(osm);
200 }
201
reset()202 void ShiftTraceTool::reset() {
203 int ghostIndex = m_ghostIndex;
204 onActivate();
205 invalidate();
206 m_ghostIndex = ghostIndex;
207
208 TTool::getApplication()
209 ->getCurrentTool()
210 ->notifyToolChanged(); // Refreshes toolbar values
211 }
212
getGhostAff()213 TAffine ShiftTraceTool::getGhostAff() {
214 if (0 <= m_ghostIndex && m_ghostIndex < 2)
215 return m_aff[m_ghostIndex] * m_dpiAff;
216 else
217 return TAffine();
218 }
219
drawDot(const TPointD & center,double r,const TPixel32 & color)220 void ShiftTraceTool::drawDot(const TPointD ¢er, double r,
221 const TPixel32 &color) {
222 tglColor(color);
223 tglDrawDisk(center, r);
224 glColor3d(0.2, 0.2, 0.2);
225 tglDrawCircle(center, r);
226 }
227
drawControlRect()228 void ShiftTraceTool::drawControlRect() { // TODO
229 if (m_ghostIndex < 0 || m_ghostIndex > 1) return;
230 int row = m_row[m_ghostIndex];
231 if (row < 0) return;
232
233 TRectD box = m_box;
234 if (box.isEmpty()) return;
235 glPushMatrix();
236 tglMultMatrix(getGhostAff());
237
238 TPixel32 color;
239
240 // draw onion-colored rectangle to indicate which ghost is grabbed
241 {
242 TPixel32 frontOniColor, backOniColor;
243 bool inksOnly;
244 Preferences::instance()->getOnionData(frontOniColor, backOniColor,
245 inksOnly);
246 color = (m_ghostIndex == 0) ? backOniColor : frontOniColor;
247 double unit = sqrt(tglGetPixelSize2());
248 unit *= getDevPixRatio();
249 TRectD coloredBox = box.enlarge(3.0 * unit);
250 tglColor(color);
251 glBegin(GL_LINE_STRIP);
252 glVertex2d(coloredBox.x0, coloredBox.y0);
253 glVertex2d(coloredBox.x1, coloredBox.y0);
254 glVertex2d(coloredBox.x1, coloredBox.y1);
255 glVertex2d(coloredBox.x0, coloredBox.y1);
256 glVertex2d(coloredBox.x0, coloredBox.y0);
257 glEnd();
258 }
259
260 color = m_highlightedGadget == TranslateGadget
261 ? TPixel32(200, 100, 100)
262 : m_highlightedGadget == RotateGadget ? TPixel32(100, 200, 100)
263 : TPixel32(120, 120, 120);
264 tglColor(color);
265 glBegin(GL_LINE_STRIP);
266 glVertex2d(box.x0, box.y0);
267 glVertex2d(box.x1, box.y0);
268 glVertex2d(box.x1, box.y1);
269 glVertex2d(box.x0, box.y1);
270 glVertex2d(box.x0, box.y0);
271 glEnd();
272 color = m_highlightedGadget == ScaleGadget ? TPixel32(200, 100, 100)
273 : TPixel32::White;
274 double r = 4 * sqrt(tglGetPixelSize2());
275 drawDot(box.getP00(), r, color);
276 drawDot(box.getP01(), r, color);
277 drawDot(box.getP10(), r, color);
278 drawDot(box.getP11(), r, color);
279 if (m_curveStatus == NoCurve) {
280 color = m_highlightedGadget == MoveCenterGadget ? TPixel32(200, 100, 100)
281 : TPixel32::White;
282 TPointD c = m_center[m_ghostIndex];
283 drawDot(c, r, color);
284 }
285 glPopMatrix();
286 }
287
drawCurve()288 void ShiftTraceTool::drawCurve() {
289 if (m_curveStatus == NoCurve) return;
290 double r = 4 * sqrt(tglGetPixelSize2());
291 double u = getPixelSize();
292 if (m_curveStatus == TwoPointsCurve) {
293 TPixel32 color = m_highlightedGadget == CurveP0Gadget
294 ? TPixel32(200, 100, 100)
295 : TPixel32::White;
296 drawDot(m_p0, r, color);
297 glColor3d(0.2, 0.2, 0.2);
298 tglDrawSegment(m_p0, m_p1);
299 drawDot(m_p1, r, TPixel32::Red);
300 } else if (m_curveStatus == ThreePointsCurve) {
301 TPixel32 color = m_highlightedGadget == CurveP0Gadget
302 ? TPixel32(200, 100, 100)
303 : TPixel32::White;
304 drawDot(m_p0, r, color);
305 color = m_highlightedGadget == CurveP1Gadget ? TPixel32(200, 100, 100)
306 : TPixel32::White;
307 drawDot(m_p1, r, color);
308
309 glColor3d(0.2, 0.2, 0.2);
310
311 TPointD center;
312 if (circumCenter(center, m_p0, m_p1, m_p2)) {
313 double radius = norm(center - m_p1);
314 glBegin(GL_LINE_STRIP);
315 int n = 100;
316 for (int i = 0; i < n; i++) {
317 double t = (double)i / n;
318 TPointD p = (1 - t) * m_p0 + t * m_p2;
319 p = center + radius * normalize(p - center);
320 tglVertex(p);
321 }
322 for (int i = 0; i < n; i++) {
323 double t = (double)i / n;
324 TPointD p = (1 - t) * m_p2 + t * m_p1;
325 p = center + radius * normalize(p - center);
326 tglVertex(p);
327 }
328 glEnd();
329 } else {
330 tglDrawSegment(m_p0, m_p1);
331 }
332 color = m_highlightedGadget == CurvePmGadget ? TPixel32(200, 100, 100)
333 : TPixel32::White;
334 drawDot(m_p2, r, color);
335 }
336 }
337
onActivate()338 void ShiftTraceTool::onActivate() {
339 m_ghostIndex = 0;
340 m_curveStatus = NoCurve;
341 clearData();
342 OnionSkinMask osm =
343 TTool::getApplication()->getCurrentOnionSkin()->getOnionSkinMask();
344 m_aff[0] = osm.getShiftTraceGhostAff(0);
345 m_aff[1] = osm.getShiftTraceGhostAff(1);
346 m_center[0] = osm.getShiftTraceGhostCenter(0);
347 m_center[1] = osm.getShiftTraceGhostCenter(1);
348 }
349
onDeactivate()350 void ShiftTraceTool::onDeactivate() {
351 QAction *action = CommandManager::instance()->getAction("MI_EditShift");
352 action->setChecked(false);
353 }
354
getGadget(const TPointD & p)355 ShiftTraceTool::GadgetId ShiftTraceTool::getGadget(const TPointD &p) {
356 std::vector<std::pair<TPointD, GadgetId>> gadgets;
357 gadgets.push_back(std::make_pair(m_p0, CurveP0Gadget));
358 gadgets.push_back(std::make_pair(m_p1, CurveP1Gadget));
359 gadgets.push_back(std::make_pair(m_p2, CurvePmGadget));
360 TAffine aff = getGhostAff();
361 double pixelSize = getPixelSize();
362 double d = 15 * pixelSize; // offset for rotation handle
363 if (0 <= m_ghostIndex && m_ghostIndex < 2) {
364 gadgets.push_back(std::make_pair(aff * m_box.getP00(), ScaleGadget));
365 gadgets.push_back(std::make_pair(aff * m_box.getP01(), ScaleGadget));
366 gadgets.push_back(std::make_pair(aff * m_box.getP10(), ScaleGadget));
367 gadgets.push_back(std::make_pair(aff * m_box.getP11(), ScaleGadget));
368 gadgets.push_back(
369 std::make_pair(aff * m_center[m_ghostIndex], MoveCenterGadget));
370 }
371 int k = -1;
372 double minDist2 = pow(10 * pixelSize, 2);
373 for (int i = 0; i < (int)gadgets.size(); i++) {
374 double d2 = norm2(gadgets[i].first - p);
375 if (d2 < minDist2) {
376 minDist2 = d2;
377 k = i;
378 }
379 }
380 if (k >= 0) return gadgets[k].second;
381
382 // rect-point
383 if (0 <= m_ghostIndex && m_ghostIndex < 2) {
384 TPointD q = aff.inv() * p;
385 double big = 1.0e6;
386 double d = big, x = 0, y = 0;
387 if (m_box.x0 < q.x && q.x < m_box.x1) {
388 x = q.x;
389 double d0 = fabs(m_box.y0 - q.y);
390 double d1 = fabs(m_box.y1 - q.y);
391 if (d0 < d1) {
392 d = d0;
393 y = m_box.y0;
394 } else {
395 d = d1;
396 y = m_box.y1;
397 }
398 }
399 if (m_box.y0 < q.y && q.y < m_box.y1) {
400 double d0 = fabs(m_box.x0 - q.x);
401 double d1 = fabs(m_box.x1 - q.x);
402 if (d0 < d) {
403 d = d0;
404 y = q.y;
405 x = m_box.x0;
406 }
407 if (d1 < d) {
408 d = d1;
409 y = q.y;
410 x = m_box.x1;
411 }
412 }
413 if (d < big) {
414 TPointD pp = aff * TPointD(x, y);
415 double d = norm(p - pp);
416 if (d < 10 * getPixelSize()) {
417 if (m_box.contains(q))
418 return TranslateGadget;
419 else
420 return RotateGadget;
421 }
422 }
423 if (m_box.contains(q))
424 return NoGadget_InBox;
425 else
426 return NoGadget;
427 }
428 return NoGadget;
429 }
430
mouseMove(const TPointD & pos,const TMouseEvent & e)431 void ShiftTraceTool::mouseMove(const TPointD &pos, const TMouseEvent &e) {
432 GadgetId highlightedGadget = getGadget(pos);
433 if (highlightedGadget != m_highlightedGadget) {
434 m_highlightedGadget = highlightedGadget;
435 invalidate();
436 }
437 }
438
leftButtonDown(const TPointD & pos,const TMouseEvent & e)439 void ShiftTraceTool::leftButtonDown(const TPointD &pos, const TMouseEvent &e) {
440 m_gadget = m_highlightedGadget;
441 m_oldPos = m_startPos = pos;
442
443 bool notify = false;
444
445 if (!e.isCtrlPressed() &&
446 (m_gadget == NoGadget || m_gadget == NoGadget_InBox)) {
447 if (m_gadget == NoGadget_InBox) {
448 m_gadget = TranslateGadget;
449 } else {
450 m_gadget = RotateGadget;
451 }
452
453 int row = getViewer()->posToRow(e.m_pos, 5.0, false, true);
454 if (row >= 0) {
455 int index = -1;
456 TApplication *app = TTool::getApplication();
457 if (app->getCurrentFrame()->isEditingScene()) {
458 int currentRow = getFrame();
459 if (m_row[0] >= 0 && row < currentRow)
460 index = 0;
461 else if (m_row[1] >= 0 && row > currentRow)
462 index = 1;
463 } else {
464 if (m_row[0] == row)
465 index = 0;
466 else if (m_row[1] == row)
467 index = 1;
468 }
469
470 if (index >= 0) {
471 m_ghostIndex = index;
472 updateBox();
473 m_gadget = TranslateGadget;
474 m_highlightedGadget = TranslateGadget;
475 notify = true;
476 }
477 }
478 } else if (e.isCtrlPressed()) {
479 m_gadget = NoGadget_InBox;
480 }
481
482 m_oldAff = m_aff[m_ghostIndex];
483 invalidate();
484
485 if (notify) {
486 TTool::getApplication()
487 ->getCurrentTool()
488 ->notifyToolChanged(); // Refreshes toolbar values
489 }
490 }
491
leftButtonDrag(const TPointD & pos,const TMouseEvent & e)492 void ShiftTraceTool::leftButtonDrag(const TPointD &pos, const TMouseEvent &e) {
493 if (m_gadget == NoGadget || m_gadget == NoGadget_InBox) {
494 if (norm(pos - m_oldPos) > 10 * getPixelSize()) {
495 m_curveStatus = TwoPointsCurve;
496 m_p0 = m_oldPos;
497 m_gadget = CurveP1Gadget;
498 }
499 }
500
501 if (isCurveGadget(m_gadget)) {
502 if (m_gadget == CurveP0Gadget)
503 m_p0 = pos;
504 else if (m_gadget == CurveP1Gadget)
505 m_p1 = pos;
506 else
507 m_p2 = pos;
508 updateCurveAffs();
509 } else if (m_gadget == RotateGadget) {
510 TAffine aff = getGhostAff();
511 TPointD c = aff * m_center[m_ghostIndex];
512 TPointD a = m_oldPos - c;
513 TPointD b = pos - c;
514 m_oldPos = pos;
515 TPointD u = normalize(a);
516 double phi =
517 atan2(-u.y * b.x + u.x * b.y, u.x * b.x + u.y * b.y) * 180.0 / 3.14153;
518
519 TPointD imgC = aff * m_center[m_ghostIndex];
520
521 m_aff[m_ghostIndex] = TRotation(imgC, phi) * m_aff[m_ghostIndex];
522 } else if (m_gadget == MoveCenterGadget) {
523 TAffine aff = getGhostAff().inv();
524 TPointD delta = aff * pos - aff * m_oldPos;
525 m_oldPos = pos;
526 m_center[m_ghostIndex] += delta;
527 } else if (m_gadget == TranslateGadget) {
528 TPointD delta = pos - m_oldPos;
529 m_oldPos = pos;
530 m_aff[m_ghostIndex] = TTranslation(delta) * m_aff[m_ghostIndex];
531 } else if (m_gadget == ScaleGadget) {
532 TAffine aff = getGhostAff();
533 TPointD c = m_center[m_ghostIndex];
534 TPointD a = aff.inv() * m_oldPos - c;
535 TPointD b = aff.inv() * pos - c;
536 TPointD imgC = aff * m_center[m_ghostIndex];
537
538 if (e.isShiftPressed())
539 m_aff[m_ghostIndex] = TScale(imgC, b.x / a.x, b.y / a.y) * m_oldAff;
540 else {
541 double scale = std::max(b.x / a.x, b.y / a.y);
542 m_aff[m_ghostIndex] = TScale(imgC, scale) * m_oldAff;
543 }
544 }
545
546 updateGhost();
547 invalidate();
548 }
549
leftButtonUp(const TPointD & pos,const TMouseEvent &)550 void ShiftTraceTool::leftButtonUp(const TPointD &pos, const TMouseEvent &) {
551 if (CurveP0Gadget <= m_gadget && m_gadget <= CurvePmGadget) {
552 if (m_curveStatus == TwoPointsCurve) {
553 m_p2 = (m_p0 + m_p1) * 0.5;
554 m_curveStatus = ThreePointsCurve;
555 updateCurveAffs();
556 updateGhost();
557
558 m_center[0] = (m_aff[0] * m_dpiAff).inv() * m_p2;
559 m_center[1] = (m_aff[1] * m_dpiAff).inv() * m_p2;
560 }
561 }
562 m_gadget = NoGadget;
563 invalidate();
564
565 TTool::getApplication()
566 ->getCurrentTool()
567 ->notifyToolChanged(); // Refreshes toolbar values
568 }
569
draw()570 void ShiftTraceTool::draw() {
571 updateData();
572 drawControlRect();
573 drawCurve();
574 }
575
getCursorId() const576 int ShiftTraceTool::getCursorId() const {
577 if (m_highlightedGadget == RotateGadget || m_highlightedGadget == NoGadget)
578 return ToolCursor::RotateCursor;
579 else if (m_highlightedGadget == ScaleGadget)
580 return ToolCursor::ScaleCursor;
581 else if (isCurveGadget(m_highlightedGadget))
582 return ToolCursor::PinchCursor;
583 else // Curve Points, TranslateGadget, NoGadget_InBox
584 return ToolCursor::MoveCursor;
585 }
586
isEventAcceptable(QEvent * e)587 bool ShiftTraceTool::isEventAcceptable(QEvent *e) {
588 // F1, F2 and F3 keys are used for flipping
589 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
590 int key = keyEvent->key();
591 return (Qt::Key_F1 <= key && key <= Qt::Key_F3);
592 }
593
onLeave()594 void ShiftTraceTool::onLeave() {
595 OnionSkinMask osm =
596 TTool::getApplication()->getCurrentOnionSkin()->getOnionSkinMask();
597 osm.clearGhostFlipKey();
598 TTool::getApplication()->getCurrentOnionSkin()->setOnionSkinMask(osm);
599 }
600
setCurrentGhostIndex(int index)601 void ShiftTraceTool::setCurrentGhostIndex(int index) {
602 m_ghostIndex = index;
603 updateBox();
604 invalidate();
605 }
606
607 ShiftTraceTool shiftTraceTool;
608