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