1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
2
3 /*
4 Rosegarden
5 A MIDI and audio sequencer and musical notation editor.
6 Copyright 2000-2021 the Rosegarden development team.
7
8 Other copyrights also apply to some parts of this work. Please
9 see the AUTHORS file and individual file headers for details.
10
11 This program is free software; you can redistribute it and/or
12 modify it under the terms of the GNU General Public License as
13 published by the Free Software Foundation; either version 2 of the
14 License, or (at your option) any later version. See the file
15 COPYING included with this distribution for more information.
16 */
17
18 #define RG_MODULE_STRING "[SegmentSelector]"
19
20 #include "SegmentSelector.h"
21
22 #include "misc/Strings.h"
23 #include "misc/Debug.h"
24 #include "base/Composition.h"
25 #include "base/RealTime.h"
26 #include "base/SnapGrid.h"
27 #include "base/Track.h"
28 #include "commands/segment/SegmentQuickCopyCommand.h"
29 #include "commands/segment/SegmentQuickLinkCommand.h"
30 #include "commands/segment/SegmentReconfigureCommand.h"
31 #include "CompositionModelImpl.h"
32 #include "CompositionView.h"
33 #include "document/RosegardenDocument.h"
34 #include "document/CommandHistory.h"
35 #include "gui/general/RosegardenScrollView.h"
36 #include "SegmentPencil.h"
37 #include "SegmentResizer.h"
38 #include "SegmentToolBox.h"
39
40 #include <QMouseEvent>
41 #include <QKeyEvent>
42
43 #include <math.h>
44 #include <stdlib.h>
45
46 namespace Rosegarden
47 {
48
49
50 QString SegmentSelector::ToolName() { return "segmentselector"; }
51
52 SegmentSelector::SegmentSelector(CompositionView *c, RosegardenDocument *d) :
53 SegmentTool(c, d),
TableFuncNext(TableFuncScanState * node)54 m_clickPoint(),
55 m_lastMousePos(),
56 m_segmentAddMode(false),
57 m_segmentCopyMode(false),
58 m_segmentCopyingAsLink(false),
59 m_passedInertiaEdge(false),
60 m_segmentQuickCopyDone(false),
61 m_selectionMoveStarted(false),
62 m_changeMade(false),
63 m_dispatchTool(nullptr)
64 {
65 //RG_DEBUG << "SegmentSelector()";
66 }
67
68 SegmentSelector::~SegmentSelector()
69 {
70 }
71
72 void SegmentSelector::ready()
73 {
74 m_canvas->viewport()->setCursor(Qt::ArrowCursor);
75 setContextHelpFor(QPoint(0,0));
76 }
77
78 static bool isNearEdge(const QRect &segmentRect, const QPoint &cursor)
79 {
80 // Fifteen percent of the width of the segment, up to 10px
TableFuncRecheck(TableFuncScanState * node,TupleTableSlot * slot)81
82 int threshold = lround(segmentRect.width() * 0.15);
83
84 if (threshold == 0)
85 threshold = 1;
86 if (threshold > 10)
87 threshold = 10;
88
89 // Near right edge?
90 if (segmentRect.right() - cursor.x() < threshold)
91 return true;
92
93 // Near left edge?
94 if (cursor.x() - segmentRect.left() < threshold)
95 return true;
96
ExecTableFuncScan(PlanState * pstate)97 return false;
98 }
99
100 void
101 SegmentSelector::mousePressEvent(QMouseEvent *e)
102 {
103 // Let the baseclass have a go.
104 SegmentTool::mousePressEvent(e);
105
106 // We only care about the left and middle mouse buttons.
107 if (e->button() != Qt::LeftButton &&
108 e->button() != Qt::MiddleButton)
109 return;
110
ExecInitTableFuncScan(TableFuncScan * node,EState * estate,int eflags)111 // No need to propagate.
112 e->accept();
113
114 QPoint pos = m_canvas->viewportToContents(e->pos());
115
116 // Get the segment that was clicked.
117 ChangingSegmentPtr item = m_canvas->getModel()->getSegmentAt(pos);
118
119 // If middle button...
120 if (e->button() == Qt::MiddleButton) {
121 // If clicked on the background, create a new segment.
122 if (!item) {
123 m_dispatchTool = m_canvas->getToolBox()->getTool(SegmentPencil::ToolName());
124
125 if (m_dispatchTool) {
126 m_dispatchTool->ready(); // set mouse cursor
127 m_dispatchTool->mousePressEvent(e);
128 }
129 }
130
131 return;
132 }
133
134 // Left button was pressed.
135 // ??? Should we split this into a midPress(e) and a leftPress(e)?
136 // Might improve readability a little.
137
138 // *** Resize
139
140 // if the Segment was clicked near the edge, resize
141 if (item && isNearEdge(item->rect(), pos)) {
142 SegmentResizer *segmentResizer = dynamic_cast<SegmentResizer *>(
143 m_canvas->getToolBox()->getTool(SegmentResizer::ToolName()));
144
145 // Turn it over to SegmentResizer.
146 m_dispatchTool = segmentResizer;
147 m_dispatchTool->ready(); // set mouse cursor
148 m_dispatchTool->mousePressEvent(e);
149
150 return;
151 }
152
153 // *** Adjust Selection
154
155 // Shift key adds to selection.
156 m_segmentAddMode = ((e->modifiers() & Qt::ShiftModifier) != 0);
157
158 // if a segment was clicked
159 if (item) {
160 bool selected = m_canvas->getModel()->isSelected(item->getSegment());
161 if (m_segmentAddMode) {
162 // toggle item selection
163 m_canvas->getModel()->setSelected(item->getSegment(), !selected);
164 } else {
165 if (!selected) {
166 // make the item the selection
167 m_canvas->getModel()->clearSelected();
168 m_canvas->getModel()->setSelected(item->getSegment());
169 }
170 }
171 } else { // the background was clicked
172 if (!m_segmentAddMode) {
173 // clear the selection
174 m_canvas->getModel()->clearSelected();
175 }
176 }
177
178 // *** Perform Functions
179
180 bool ctrl = ((e->modifiers() & Qt::ControlModifier) != 0);
181
182 // if a segment was clicked
183 if (item) {
184
185 // * Move
186
187 // We don't dispatch to SegmentMover because SegmentMover
188 // doesn't support copying/linking.
189
190 bool alt = ((e->modifiers() & Qt::AltModifier) != 0);
191
192 // Ctrl and Alt+Ctrl are segment copy.
193 m_segmentCopyMode = ctrl;
194 // Alt+Ctrl is copy as link.
195 m_segmentCopyingAsLink = (alt && ctrl);
196
197 // If the segment is selected, put it in move mode.
198 if (m_canvas->getModel()->isSelected(item->getSegment())) {
199 m_canvas->getModel()->startChange(
200 item, CompositionModelImpl::ChangeMove);
201 }
202
203 setChangingSegment(item);
204
205 m_clickPoint = pos;
206
207 int guideX = item->rect().x();
208 int guideY = item->rect().y();
209
210 m_canvas->drawGuides(guideX, guideY);
211
212 setSnapTime(e, SnapGrid::SnapToBeat);
213
214 } else { // the background was clicked
ExecEndTableFuncScan(TableFuncScanState * node)215 if (ctrl) {
216
217 // * Create Segment
218
219 m_dispatchTool = m_canvas->getToolBox()->getTool(
220 SegmentPencil::ToolName());
221
222 if (m_dispatchTool) {
223 m_dispatchTool->ready(); // set mouse cursor
224 m_dispatchTool->mousePressEvent(e);
225 }
226
227 return;
228 }
229
230 // * Rubber Band
231
232 m_canvas->drawSelectionRectPos1(pos);
233
234 }
235
236 // Make sure the Segment Parameters box is updated. See
237 // RosegardenMainViewWidget::slotSelectedSegments().
238 m_canvas->getModel()->selectionHasChanged();
239
240 m_passedInertiaEdge = false;
241 }
242
243 void
ExecReScanTableFuncScan(TableFuncScanState * node)244 SegmentSelector::mouseReleaseEvent(QMouseEvent *e)
245 {
246 // We only care about the left and middle mouse buttons.
247 if (e->button() != Qt::LeftButton &&
248 e->button() != Qt::MiddleButton)
249 return;
250
251 // No need to propagate.
252 e->accept();
253
254 QPoint pos = m_canvas->viewportToContents(e->pos());
255
256 // If another tool (SegmentPencil or SegmentResizer) has taken
257 // over, delegate.
258 if (m_dispatchTool) {
259 m_dispatchTool->mouseReleaseEvent(e);
260 m_dispatchTool->stow();
261
262 // Forget about the tool.
263 // Note that this is not a memory leak. There is only one instance
264 // of each tool stored in BaseToolBox::m_tools.
265 m_dispatchTool = nullptr;
266
267 // Back to this tool.
268 ready();
269
270 return;
271 }
272
273 // We only handle the left button. The middle button is handled by
274 // the dispatch tool (segment pencil) or ignored.
275 if (e->button() != Qt::LeftButton)
276 return;
277
278 // The left button has been released.
279
280 m_canvas->hideGuides();
281 m_canvas->hideTextFloat();
282
283 // If rubber band mode
284 if (!getChangingSegment()) {
285 m_canvas->hideSelectionRect();
286 m_canvas->getModel()->finalizeSelectionRect();
287 m_canvas->getModel()->selectionHasChanged();
288 return;
289 }
290
291 m_canvas->viewport()->setCursor(Qt::ArrowCursor);
292
293 if (m_canvas->getModel()->isSelected(getChangingSegment()->getSegment())) {
294
295 if (m_changeMade) {
296
297 MacroCommand *macroCommand = nullptr;
298
299 CompositionModelImpl::ChangingSegmentSet &changingSegments =
300 m_canvas->getModel()->getChangingSegments();
301
302 if (m_segmentCopyMode) {
303 if (m_segmentCopyingAsLink) {
304 macroCommand = new MacroCommand(
305 tr("Copy %n Segment(s) as link(s)", "", changingSegments.size()));
306 } else {
307 macroCommand = new MacroCommand(
308 tr("Copy %n Segment(s)", "", changingSegments.size()));
309 }
310 } else {
311 macroCommand = new MacroCommand(
312 tr("Move %n Segment(s)", "", changingSegments.size()));
313 }
314
315 if (m_segmentCopyMode) {
316 // Make copies of the original Segment(s). These copies will
317 // take the place of the originals.
318
319 SegmentSelection selectedItems = m_canvas->getSelectedSegments();
320
321 // for each selected segment
322 for (SegmentSelection::iterator it = selectedItems.begin();
323 it != selectedItems.end();
324 ++it) {
325 Segment *segment = *it;
326
327 Command *command = nullptr;
328
329 if (m_segmentCopyingAsLink) {
330 command = new SegmentQuickLinkCommand(segment);
331 } else {
332 // if it's a link, copy as link
333 if (segment->isTrulyLinked())
334 command = new SegmentQuickLinkCommand(segment);
335 else // copy as a non-link segment
336 command = new SegmentQuickCopyCommand(segment);
337 }
338
339 macroCommand->addCommand(command);
340 }
341 }
342
343 const int startDragTrackPos =
344 m_canvas->grid().getYBin(m_clickPoint.y());
345 const int currentTrackPos = m_canvas->grid().getYBin(pos.y());
346 const int trackDiff = currentTrackPos - startDragTrackPos;
347
348 Composition &comp = m_doc->getComposition();
349
350 SegmentReconfigureCommand *segmentReconfigureCommand =
351 new SegmentReconfigureCommand("", &comp);
352
353 // For each changing segment, add the segment to the
354 // SegmentReconfigureCommand.
355 for (CompositionModelImpl::ChangingSegmentSet::iterator it =
356 changingSegments.begin();
357 it != changingSegments.end();
358 ++it) {
359 ChangingSegmentPtr changingSegment = *it;
360 Segment *segment = changingSegment->getSegment();
361
362 TrackId origTrackId = segment->getTrack();
363 int newTrackPos =
364 comp.getTrackPositionById(origTrackId) + trackDiff;
365
366 // Limit to [0, comp.getNbTracks()-1]
367 if (newTrackPos < 0)
368 newTrackPos = 0;
369 if (newTrackPos > static_cast<int>(comp.getNbTracks()) - 1)
370 newTrackPos = comp.getNbTracks() - 1;
371
372 Track *newTrack = comp.getTrackByPosition(newTrackPos);
373 int newTrackId = origTrackId;
374 if (newTrack)
375 newTrackId = newTrack->getId();
376
377 timeT startTime =
378 changingSegment->getStartTime(m_canvas->grid());
379
380 // endTime = startTime + segment duration
381 // We absolutely don't want to snap the end time to
382 // the grid. We want it to remain exactly the same as
383 // it was, but relative to the new start time.
384 timeT endTime = startTime +
385 segment->getEndMarkerTime(false) -
386 segment->getStartTime();
387
388 segmentReconfigureCommand->addSegment(
389 segment, startTime, endTime, newTrackId);
390 }
391
392 macroCommand->addCommand(segmentReconfigureCommand);
393
394 CommandHistory::getInstance()->addCommand(macroCommand);
395
396 m_canvas->update();
397 }
398
399 m_canvas->getModel()->endChange();
400 m_canvas->slotUpdateAll();
401 }
402
403 // Get ready for the next button press.
404 m_segmentQuickCopyDone = false;
405 m_changeMade = false;
406 m_selectionMoveStarted = false;
407 setChangingSegment(ChangingSegmentPtr());
408
409 setContextHelpFor(pos);
410 }
411
412 int
413 SegmentSelector::mouseMoveEvent(QMouseEvent *e)
414 {
415 // No need to propagate.
416 e->accept();
417
418 QPoint pos = m_canvas->viewportToContents(e->pos());
419 m_lastMousePos = pos;
420
421 // If no buttons are pressed, update the context help and bail.
422 // Note: Mouse tracking must be on for this to work. See
423 // QWidget::setMouseTracking().
424 if (e->buttons() == Qt::NoButton) {
425 setContextHelpFor(pos, e->modifiers());
426 return NO_FOLLOW;
427 }
428
429 // If another tool has taken over, delegate.
430 if (m_dispatchTool)
431 return m_dispatchTool->mouseMoveEvent(e);
432
433 // We only handle the left button. The middle button is handled by
434 // the dispatch tool (segment pencil) or ignored.
435 if (e->buttons() != Qt::LeftButton)
436 return NO_FOLLOW;
tfuncLoadRows(TableFuncScanState * tstate,ExprContext * econtext)437
438 // If we aren't moving anything, rubber band.
439 if (!getChangingSegment()) {
440 m_canvas->drawSelectionRectPos2(pos);
441 m_canvas->getModel()->selectionHasChanged();
442
443 return FOLLOW_HORIZONTAL | FOLLOW_VERTICAL;
444 }
445
446 // Moving
447
448 // If the segment that was clicked on isn't selected, bail.
449 if (!m_canvas->getModel()->isSelected(getChangingSegment()->getSegment()))
450 return NO_FOLLOW;
451
452 const int dx = pos.x() - m_clickPoint.x();
453 const int dy = pos.y() - m_clickPoint.y();
454 const int inertiaDistance = 8;
455
456 // If we've not already exceeded the inertia distance, and we
457 // still haven't, bail.
458 if (!m_passedInertiaEdge &&
459 abs(dx) < inertiaDistance &&
460 abs(dy) < inertiaDistance) {
461 return NO_FOLLOW;
462 }
463
464 m_passedInertiaEdge = true;
465
466 m_canvas->viewport()->setCursor(Qt::SizeAllCursor);
467
468 #if 0
469 // ??? Moving to mouseReleaseEvent().
470 if (m_segmentCopyMode && !m_segmentQuickCopyDone) {
471 // Make copies of the original Segment(s). These copies will
472 // take the place of the originals as the user drags the
473 // the originals to a new location.
474
475 MacroCommand *macroCommand = 0;
476
477 if (m_segmentCopyingAsLink) {
478 macroCommand = new MacroCommand(
479 SegmentQuickLinkCommand::getGlobalName());
480 } else {
481 macroCommand = new MacroCommand(
482 SegmentQuickCopyCommand::getGlobalName());
483 }
484
485 SegmentSelection selectedItems = m_canvas->getSelectedSegments();
486
487 // for each selected segment
488 for (SegmentSelection::iterator it = selectedItems.begin();
489 it != selectedItems.end();
490 ++it) {
491 Segment *segment = *it;
492
493 Command *command = 0;
494
495 if (m_segmentCopyingAsLink) {
496 command = new SegmentQuickLinkCommand(segment);
497 } else {
498 // if it's a link, copy as link
499 if (segment->isTrulyLinked())
500 command = new SegmentQuickLinkCommand(segment);
501 else // copy as a non-link segment
502 command = new SegmentQuickCopyCommand(segment);
503 }
504
505 macroCommand->addCommand(command);
506 }
507
508 CommandHistory::getInstance()->addCommand(macroCommand);
509
510 m_canvas->update();
511
512 // Make sure we don't do it again.
513 m_segmentQuickCopyDone = true;
514 }
515 #endif
516
517 setSnapTime(e, SnapGrid::SnapToBeat);
518
519 // start move on selected items only once
520 if (!m_selectionMoveStarted) {
521 m_selectionMoveStarted = true;
522
523 m_canvas->getModel()->startChangeSelection(
524 m_segmentCopyMode ? CompositionModelImpl::ChangeCopy :
525 CompositionModelImpl::ChangeMove);
526
527 // The call to startChangeSelection() generates a new changing segment.
528 // Get it.
529 ChangingSegmentPtr newChangingSegment =
530 m_canvas->getModel()->findChangingSegment(
531 getChangingSegment()->getSegment());
532
533 if (newChangingSegment) {
534 // Toss the local "changing" segment since it isn't going to
535 // be moving at all. Swap it for the same changing segment in
536 // CompositionModelImpl. That one *will* be moving and can be
537 // used to drive the guides.
538 setChangingSegment(newChangingSegment);
539 }
540 }
541
542 // Display help for the Shift key.
543 setContextHelpFor(pos, e->modifiers());
544
545 Composition &comp = m_doc->getComposition();
546
547 const int startDragTrackPos = m_canvas->grid().getYBin(m_clickPoint.y());
548 const int currentTrackPos = m_canvas->grid().getYBin(pos.y());
549 const int trackDiff = currentTrackPos - startDragTrackPos;
550
551 CompositionModelImpl::ChangingSegmentSet &changingSegments =
552 m_canvas->getModel()->getChangingSegments();
553
554 // For each changing segment
555 for (CompositionModelImpl::ChangingSegmentSet::iterator it =
556 changingSegments.begin();
557 it != changingSegments.end();
558 ++it) {
559 ChangingSegmentPtr changingSegment = *it;
560
561 const timeT newStartTime = m_canvas->grid().snapX(
562 changingSegment->savedRect().x() + dx);
563
564 const int newX = lround(
565 m_canvas->grid().getRulerScale()->getXForTime(newStartTime));
566
567 int newTrackPos = m_canvas->grid().getYBin(
568 changingSegment->savedRect().y()) + trackDiff;
569
570 // Limit to [0, comp.getNbTracks()-1].
571 if (newTrackPos < 0)
572 newTrackPos = 0;
573 if (newTrackPos > static_cast<int>(comp.getNbTracks()) - 1)
574 newTrackPos = comp.getNbTracks() - 1;
575
576 const int newY = m_canvas->grid().getYBinCoordinate(newTrackPos);
577
578 changingSegment->moveTo(newX, newY);
579 m_changeMade = true;
580 }
581
582 if (m_changeMade) {
583 // Make sure the segments are redrawn.
584 // Note: The update() call below doesn't cause the segments to be
585 // redrawn. It just updates from the cache.
586 m_canvas->slotUpdateAll();
587 }
588
589 // Guides
590
591 const int guideX = getChangingSegment()->rect().x();
592 const int guideY = getChangingSegment()->rect().y();
593
594 m_canvas->drawGuides(guideX, guideY);
595
596 // Text Float
597
598 const timeT guideTime = m_canvas->grid().snapX(guideX);
599
600 RealTime time = comp.getElapsedRealTime(guideTime);
601 const QString msecs = QString::asprintf("%03d", time.msec());
602
603 int bar;
604 int beat;
605 int fraction;
606 int remainder;
607 comp.getMusicalTimeForAbsoluteTime(guideTime, bar, beat, fraction, remainder);
608
609 QString posString = QString("%1.%2s (%3, %4, %5)").
610 arg(time.sec).arg(msecs).arg(bar + 1).arg(beat).arg(fraction);
611
612 m_canvas->drawTextFloat(guideX + 10, guideY - 30, posString);
613
614 m_canvas->update();
615
616 return FOLLOW_HORIZONTAL | FOLLOW_VERTICAL;
617 }
618
619 void SegmentSelector::keyPressEvent(QKeyEvent *e)
620 {
621 // If another tool has taken over, delegate.
622 if (m_dispatchTool) {
623 m_dispatchTool->keyPressEvent(e);
624 return;
625 }
626
627 // In case shift or ctrl were pressed, update the context help.
628 setContextHelpFor(m_lastMousePos, e->modifiers());
629 }
630
631 void SegmentSelector::keyReleaseEvent(QKeyEvent *e)
632 {
633 // If another tool has taken over, delegate.
634 if (m_dispatchTool) {
635 m_dispatchTool->keyReleaseEvent(e);
636 return;
637 }
638
639 // In case shift or ctrl were released, update the context help.
640 setContextHelpFor(m_lastMousePos, e->modifiers());
641 }
642
643 void SegmentSelector::setContextHelpFor(QPoint pos,
644 Qt::KeyboardModifiers modifiers)
645 {
646 // If we are moving something
647 if (m_selectionMoveStarted)
648 {
649 const bool shift = ((modifiers & Qt::ShiftModifier) != 0);
650
651 // If shift isn't being held down
652 if (!shift) {
653 setContextHelp(tr("Hold Shift to avoid snapping to beat grid"));
654 } else {
655 clearContextHelp();
656 }
657
658 return;
659 }
660
661 ChangingSegmentPtr segment = m_canvas->getModel()->getSegmentAt(pos);
662
663 // If the mouse is hovering over the background
664 if (!segment) {
665 setContextHelp(tr("Click and drag to select segments; middle-click and drag to draw an empty segment"));
666 return;
667 }
668
669 // The mouse is hovering over a segment.
670
671 const bool ctrl = ((modifiers & Qt::ControlModifier) != 0);
672
673 // If clicking would resize
674 if (m_canvas->getModel()->getSelectedSegments().size() <= 1 &&
675 isNearEdge(segment->rect(), pos)) {
676
677 if (!ctrl) {
678 setContextHelp(tr("Click and drag to resize a segment; hold Ctrl as well to rescale its contents"));
679 } else {
680 setContextHelp(tr("Click and drag to rescale segment"));
681 }
682 } else { // clicking would move
683 if (m_canvas->getModel()->haveMultipleSelection()) {
684 if (!ctrl) {
685 setContextHelp(tr("Click and drag to move segments; hold Ctrl as well to copy them; Ctrl + Alt for linked copies"));
686 } else {
687 setContextHelp(tr("Click and drag to copy segments"));
688 }
689 } else {
690 if (!ctrl) {
691 setContextHelp(tr("Click and drag to move segment; hold Ctrl as well to copy it; Ctrl + Alt for a linked copy; double-click to edit"));
692 } else {
693 setContextHelp(tr("Click and drag to copy segment"));
694 }
695 }
696 }
697 }
698
699
700 }
701