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 "[MatrixWidget]"
19 
20 #include "MatrixWidget.h"
21 
22 #include "MatrixScene.h"
23 #include "MatrixToolBox.h"
24 #include "MatrixTool.h"
25 #include "MatrixSelector.h"
26 #include "MatrixPainter.h"
27 #include "MatrixEraser.h"
28 #include "MatrixMover.h"
29 #include "MatrixResizer.h"
30 #include "MatrixVelocity.h"
31 #include "MatrixMouseEvent.h"
32 #include "MatrixViewSegment.h"
33 #include "PianoKeyboard.h"
34 
35 #include "document/RosegardenDocument.h"
36 
37 #include "gui/application/RosegardenMainWindow.h"
38 
39 #include "gui/widgets/Panner.h"
40 #include "gui/widgets/Panned.h"
41 #include "gui/widgets/Thumbwheel.h"
42 
43 #include "gui/rulers/PitchRuler.h"
44 #include "gui/rulers/PercussionPitchRuler.h"
45 
46 #include "gui/rulers/ControlRulerWidget.h"
47 #include "gui/rulers/StandardRuler.h"
48 #include "gui/rulers/TempoRuler.h"
49 #include "gui/rulers/ChordNameRuler.h"
50 #include "gui/rulers/LoopRuler.h"
51 
52 #include "gui/general/ThornStyle.h"
53 
54 #include "gui/studio/StudioControl.h"
55 
56 #include "misc/Debug.h"
57 
58 #include "base/Composition.h"
59 #include "base/Instrument.h"
60 //#include "base/MidiProgram.h"
61 #include "base/RulerScale.h"
62 #include "base/PropertyName.h"
63 #include "base/BaseProperties.h"
64 #include "base/Controllable.h"
65 #include "base/Studio.h"
66 //#include "base/InstrumentStaticSignals.h"
67 #include "base/Device.h"
68 #include "base/MidiDevice.h"
69 #include "base/SoftSynthDevice.h"
70 #include "base/ColourMap.h"
71 
72 #include <QApplication>
73 #include <QColor>
74 #include <QGraphicsView>
75 #include <QGridLayout>
76 #include <QLabel>
77 #include <QScrollBar>
78 #include <QTimer>
79 #include <QGraphicsScene>
80 #include <QGraphicsProxyWidget>
81 #include <QPushButton>
82 #include <QRegularExpression>
83 
84 namespace Rosegarden
85 {
86 
87 
88 // Widgets vertical positions inside the main QGridLayout (m_layout).
89 enum {
90     CHORDNAMERULER_ROW,
91     TEMPORULER_ROW,
92     TOPRULER_ROW,
93     PANNED_ROW,  // Matrix
94     BOTTOMRULER_ROW,
95     CONTROLS_ROW,
96     HSCROLLBAR_ROW,
97     SEGMENTLABEL_ROW,
98     PANNER_ROW  // Navigation area
99 };
100 
101 // Widgets horizontal positions inside the main QGridLayout (m_layout).
102 enum {
103     HEADER_COL,  // Piano Ruler
104     MAIN_COL,
105 };
106 
MatrixWidget(bool drumMode)107 MatrixWidget::MatrixWidget(bool drumMode) :
108     m_document(nullptr),
109     m_scene(nullptr),
110     m_view(nullptr),
111     m_playTracking(true),
112     m_hZoomFactor(1.0),
113     m_vZoomFactor(1.0),
114     m_instrument(nullptr),
115     m_localMapping(nullptr),
116     m_pitchRuler(nullptr),
117     m_pianoScene(nullptr),
118     m_pianoView(nullptr),
119     m_onlyKeyMapping(false),
120     m_drumMode(drumMode),
121     m_firstNote(0),
122     m_lastNote(0),
123     m_highlightVisible(true),
124     m_toolBox(nullptr),
125     m_currentTool(nullptr),
126     m_currentVelocity(100),
127     m_lastZoomWasHV(true),
128     m_lastH(0),
129     m_lastV(0),
130     m_chordNameRuler(nullptr),
131     m_tempoRuler(nullptr),
132     m_topStandardRuler(nullptr),
133     m_bottomStandardRuler(nullptr),
134     m_referenceScale(nullptr)
135 {
136     //RG_DEBUG << "ctor";
137 
138     m_layout = new QGridLayout;
139     setLayout(m_layout);
140 
141     // Remove thick black lines between rulers and matrix
142     m_layout->setSpacing(0);
143 
144     // Remove black margins around the matrix
145     m_layout->setContentsMargins(0, 0, 0, 0);
146 
147     m_view = new Panned;
148     m_view->setBackgroundBrush(Qt::white);
149     m_view->setWheelZoomPan(true);
150     m_layout->addWidget(m_view, PANNED_ROW, MAIN_COL, 1, 1);
151 
152     m_pianoView = new Panned;
153     m_pianoView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
154     m_pianoView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
155     m_layout->addWidget(m_pianoView, PANNED_ROW, HEADER_COL, 1, 1);
156 
157     m_controlsWidget = new ControlRulerWidget;
158     m_layout->addWidget(m_controlsWidget, CONTROLS_ROW, MAIN_COL, 1, 1);
159 
160     m_segmentLabel = new QLabel("Segment Label");
161     m_segmentLabel->setAlignment(Qt::AlignHCenter);
162     m_segmentLabel->setAutoFillBackground(true);
163     m_layout->addWidget(m_segmentLabel, SEGMENTLABEL_ROW, MAIN_COL, 1, 1);
164 
165     // the panner along with zoom controls in one strip at one grid location
166     QWidget *panner = new QWidget;
167     QHBoxLayout *pannerLayout = new QHBoxLayout;
168     pannerLayout->setContentsMargins(0, 0, 0, 0);
169     pannerLayout->setSpacing(0);
170     panner->setLayout(pannerLayout);
171 
172     // the segment changer roller
173     m_changerWidget = new QWidget;
174     m_changerWidget->setAutoFillBackground(true);
175     QVBoxLayout *changerWidgetLayout = new QVBoxLayout;
176     m_changerWidget->setLayout(changerWidgetLayout);
177 
178     bool useRed = true;
179     m_segmentChanger = new Thumbwheel(Qt::Vertical, useRed);
180     m_segmentChanger->setToolTip(tr("<qt>Rotate wheel to change the active segment</qt>"));
181     m_segmentChanger->setFixedWidth(18);
182     m_segmentChanger->setMinimumValue(-120);
183     m_segmentChanger->setMaximumValue(120);
184     m_segmentChanger->setDefaultValue(0);
185     m_segmentChanger->setShowScale(true);
186     m_segmentChanger->setValue(60);
187     m_segmentChanger->setSpeed(0.05);
188     m_lastSegmentChangerValue = m_segmentChanger->getValue();
189     connect(m_segmentChanger, &Thumbwheel::valueChanged, this,
190             &MatrixWidget::slotSegmentChangerMoved);
191     changerWidgetLayout->addWidget(m_segmentChanger);
192 
193     pannerLayout->addWidget(m_changerWidget);
194 
195     // the panner
196     m_panner = new Panner;
197     m_panner->setMaximumHeight(60);
198     m_panner->setBackgroundBrush(Qt::white);
199     m_panner->setOptimizationFlag(QGraphicsView::DontAdjustForAntialiasing, true);
200 
201     pannerLayout->addWidget(m_panner);
202 
203     // row, col, row span, col span
204     QFrame *controls = new QFrame;
205 
206     QGridLayout *controlsLayout = new QGridLayout;
207     controlsLayout->setSpacing(0);
208     controlsLayout->setContentsMargins(0, 0, 0, 0);
209     controls->setLayout(controlsLayout);
210 
211     m_HVzoom = new Thumbwheel(Qt::Vertical);
212     m_HVzoom->setFixedSize(QSize(40, 40));
213     m_HVzoom->setToolTip(tr("Zoom"));
214 
215     // +/- 20 clicks seems to be the reasonable limit
216     m_HVzoom->setMinimumValue(-20);
217     m_HVzoom->setMaximumValue(20);
218     m_HVzoom->setDefaultValue(0);
219     m_HVzoom->setBright(true);
220     m_HVzoom->setShowScale(true);
221     m_lastHVzoomValue = m_HVzoom->getValue();
222     controlsLayout->addWidget(m_HVzoom, 0, 0, Qt::AlignCenter);
223 
224     connect(m_HVzoom, &Thumbwheel::valueChanged, this,
225             &MatrixWidget::slotPrimaryThumbwheelMoved);
226 
227     m_Hzoom = new Thumbwheel(Qt::Horizontal);
228     m_Hzoom->setFixedSize(QSize(50, 16));
229     m_Hzoom->setToolTip(tr("Horizontal Zoom"));
230 
231     m_Hzoom->setMinimumValue(-25);
232     m_Hzoom->setMaximumValue(60);
233     m_Hzoom->setDefaultValue(0);
234     m_Hzoom->setBright(false);
235     controlsLayout->addWidget(m_Hzoom, 1, 0);
236     connect(m_Hzoom, &Thumbwheel::valueChanged, this,
237             &MatrixWidget::slotHorizontalThumbwheelMoved);
238 
239     m_Vzoom = new Thumbwheel(Qt::Vertical);
240     m_Vzoom->setFixedSize(QSize(16, 50));
241     m_Vzoom->setToolTip(tr("Vertical Zoom"));
242     m_Vzoom->setMinimumValue(-25);
243     m_Vzoom->setMaximumValue(60);
244     m_Vzoom->setDefaultValue(0);
245     m_Vzoom->setBright(false);
246     controlsLayout->addWidget(m_Vzoom, 0, 1, Qt::AlignRight);
247 
248     connect(m_Vzoom, &Thumbwheel::valueChanged, this,
249             &MatrixWidget::slotVerticalThumbwheelMoved);
250 
251     // a blank QPushButton forced square looks better than the tool button did
252     m_reset = new QPushButton;
253     m_reset->setFixedSize(QSize(10, 10));
254     m_reset->setToolTip(tr("Reset Zoom"));
255     controlsLayout->addWidget(m_reset, 1, 1, Qt::AlignCenter);
256 
257     connect(m_reset, &QAbstractButton::clicked, this,
258             &MatrixWidget::slotResetZoomClicked);
259 
260     pannerLayout->addWidget(controls);
261 
262     m_layout->addWidget(panner, PANNER_ROW, HEADER_COL, 1, 2);
263 
264     // Rulers being not defined still, they can't be added to m_layout.
265     // This will be done in setSegments().
266 
267     // Move the scroll bar from m_view to MatrixWidget
268     m_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
269     m_layout->addWidget(m_view->horizontalScrollBar(),
270                         HSCROLLBAR_ROW, MAIN_COL, 1, 1);
271 
272     // Hide or show the horizontal scroll bar when needed
273     connect(m_view->horizontalScrollBar(), &QAbstractSlider::rangeChanged,
274             this, &MatrixWidget::slotHScrollBarRangeChanged);
275 
276     connect(m_view, &Panned::viewportChanged,
277             m_panner, &Panner::slotSetPannedRect);
278 
279     connect(m_view, &Panned::viewportChanged,
280             m_pianoView, &Panned::slotSetViewport);
281 
282     connect(m_view, &Panned::viewportChanged,
283             m_controlsWidget, &ControlRulerWidget::slotSetPannedRect);
284 
285     connect(m_view, &Panned::zoomIn,
286             this, &MatrixWidget::slotZoomIn);
287     connect(m_view, &Panned::zoomOut,
288             this, &MatrixWidget::slotZoomOut);
289 
290     connect(m_panner, &Panner::pannedRectChanged,
291             m_view, &Panned::slotSetViewport);
292 
293     connect(m_panner, &Panner::pannedRectChanged,
294             m_pianoView, &Panned::slotSetViewport);
295 
296     connect(m_panner, &Panner::pannedRectChanged,
297             m_controlsWidget, &ControlRulerWidget::slotSetPannedRect);
298 
299     connect(m_view, &Panned::pannedContentsScrolled,
300             this, &MatrixWidget::slotScrollRulers);
301 
302     connect(m_panner, &Panner::zoomIn,
303             this, &MatrixWidget::slotSyncPannerZoomIn);
304 
305     connect(m_panner, &Panner::zoomOut,
306             this, &MatrixWidget::slotSyncPannerZoomOut);
307 
308     connect(m_pianoView, &Panned::wheelEventReceived,
309             m_view, &Panned::slotEmulateWheelEvent);
310 
311     // Connect ControlRulerWidget for Auto-Scroll.
312     connect(m_controlsWidget, &ControlRulerWidget::mousePress,
313             this, &MatrixWidget::slotCRWMousePress);
314     connect(m_controlsWidget, &ControlRulerWidget::mouseMove,
315             this, &MatrixWidget::slotCRWMouseMove);
316     connect(m_controlsWidget, &ControlRulerWidget::mouseRelease,
317             this, &MatrixWidget::slotCRWMouseRelease);
318 
319     m_toolBox = new MatrixToolBox(this);
320 
321     // Relay context help from matrix tools
322     connect(m_toolBox, &BaseToolBox::showContextHelp,
323             this, &MatrixWidget::showContextHelp);
324 
325     // Relay context help from matrix rulers
326     connect(m_controlsWidget, &ControlRulerWidget::showContextHelp,
327             this, &MatrixWidget::showContextHelp);
328 
329     MatrixMover *matrixMoverTool = dynamic_cast <MatrixMover *> (m_toolBox->getTool(MatrixMover::ToolName()));
330     if (matrixMoverTool) {
331         connect(matrixMoverTool, &MatrixMover::hoveredOverNoteChanged,
332                 m_controlsWidget, &ControlRulerWidget::slotHoveredOverNoteChanged);
333     }
334 
335 //    MatrixVelocity *matrixVelocityTool = dynamic_cast <MatrixVelocity *> (m_toolBox->getTool(MatrixVelocity::ToolName()));
336 //    if (matrixVelocityTool) {
337 //        connect(matrixVelocityTool, SIGNAL(hoveredOverNoteChanged()),
338 //                m_controlsWidget, SLOT(slotHoveredOverNoteChanged()));
339 //    }
340 
341     connect(this, &MatrixWidget::toolChanged,
342             m_controlsWidget, &ControlRulerWidget::slotSetTool);
343 
344     // Make sure MatrixScene always gets mouse move events even when the
345     // button isn't pressed.  This way the keys on the piano keyboard
346     // to the left are always highlighted to show which note we are on.
347     m_view->setMouseTracking(true);
348 
349     connect(RosegardenMainWindow::self()->getDocument(),
350                 &RosegardenDocument::documentModified,
351             this, &MatrixWidget::slotDocumentModified);
352 
353     // Set up AutoScroller.
354     m_autoScroller.connectScrollArea(m_view);
355 }
356 
~MatrixWidget()357 MatrixWidget::~MatrixWidget()
358 {
359     // ??? QSharedPointer would be nice, but it opens up a can of worms
360     //     since this is passed to reuse code that is used across the system.
361     //     Panned and Panner in this case.  Probably worth doing.  Just a
362     //     bit more work than usual.
363     delete m_scene;
364     // ??? See above.
365     delete m_pianoScene;
366 }
367 
368 void
setSegments(RosegardenDocument * document,std::vector<Segment * > segments)369 MatrixWidget::setSegments(RosegardenDocument *document,
370                           std::vector<Segment *> segments)
371 {
372     if (m_document) {
373         disconnect(m_document, &RosegardenDocument::pointerPositionChanged,
374                    this, &MatrixWidget::slotPointerPositionChanged);
375     }
376 
377     m_document = document;
378 
379     Composition &comp = document->getComposition();
380 
381     Track *track;
382     Instrument *instr;
383 
384     // Look at segments to see if we need piano keyboard or key mapping ruler
385     // (cf comment in MatrixScene::setSegments())
386     m_onlyKeyMapping = true;
387     std::vector<Segment *>::iterator si;
388     for (si=segments.begin(); si!=segments.end(); ++si) {
389         track = comp.getTrackById((*si)->getTrack());
390         instr = document->getStudio().getInstrumentById(track->getInstrument());
391         if (instr) {
392             if (!instr->getKeyMapping()) {
393                 m_onlyKeyMapping = false;
394             }
395         }
396     }
397     // Note : m_onlyKeyMapping, whose value is defined above,
398     // must be set before calling m_scene->setSegments()
399 
400     delete m_scene;
401     m_scene = new MatrixScene();
402     m_scene->setMatrixWidget(this);
403     m_scene->setSegments(document, segments);
404 
405     m_referenceScale = m_scene->getReferenceScale();
406 
407     connect(m_scene, &MatrixScene::mousePressed,
408             this, &MatrixWidget::slotDispatchMousePress);
409 
410     connect(m_scene, &MatrixScene::mouseMoved,
411             this, &MatrixWidget::slotDispatchMouseMove);
412 
413     connect(m_scene, &MatrixScene::mouseReleased,
414             this, &MatrixWidget::slotDispatchMouseRelease);
415 
416     connect(m_scene, &MatrixScene::mouseDoubleClicked,
417             this, &MatrixWidget::slotDispatchMouseDoubleClick);
418 
419     connect(m_scene, &MatrixScene::segmentDeleted,
420             this, &MatrixWidget::segmentDeleted);
421 
422     connect(m_scene, &MatrixScene::sceneDeleted,
423             this, &MatrixWidget::sceneDeleted);
424 
425     m_view->setScene(m_scene);
426 
427     m_toolBox->setScene(m_scene);
428 
429     m_panner->setScene(m_scene);
430 
431     connect(m_view, &Panned::mouseLeaves,
432             this, &MatrixWidget::slotMouseLeavesView);
433 
434     generatePitchRuler();
435 
436     m_controlsWidget->setViewSegment(
437             dynamic_cast<ViewSegment *>(m_scene->getCurrentViewSegment()));
438     m_controlsWidget->setRulerScale(m_referenceScale);
439 
440     // For some reason this doesn't work in the constructor - not looked in detail
441     // ( ^^^ it's because m_scene is only set after construction --cc)
442     connect(m_scene, &MatrixScene::currentViewSegmentChanged,
443             m_controlsWidget, &ControlRulerWidget::slotSetCurrentViewSegment);
444 
445     connect(m_scene, &MatrixScene::selectionChanged,
446             m_controlsWidget, &ControlRulerWidget::slotSelectionChanged);
447 
448     connect(m_controlsWidget, &ControlRulerWidget::childRulerSelectionChanged,
449             m_scene, &MatrixScene::slotRulerSelectionChanged);
450 
451     m_controlsWidget->launchMatrixRulers(segments);
452 
453     connect(m_scene, SIGNAL(selectionChanged()),
454             this, SIGNAL(selectionChanged()));
455 
456     m_topStandardRuler = new StandardRuler(document,
457                                            m_referenceScale,
458                                            false);
459 
460     m_bottomStandardRuler = new StandardRuler(document,
461                                                m_referenceScale,
462                                                true);
463 
464     m_tempoRuler = new TempoRuler(m_referenceScale,
465                                   document,
466                                   24,     // height
467                                   true,   // small
468                                   ThornStyle::isEnabled());
469 
470     m_chordNameRuler = new ChordNameRuler(m_referenceScale,
471                                           document,
472                                           segments,
473                                           24);     // height
474 
475     m_layout->addWidget(m_topStandardRuler, TOPRULER_ROW, MAIN_COL, 1, 1);
476     m_layout->addWidget(m_bottomStandardRuler, BOTTOMRULER_ROW, MAIN_COL, 1, 1);
477     m_layout->addWidget(m_tempoRuler, TEMPORULER_ROW, MAIN_COL, 1, 1);
478     m_layout->addWidget(m_chordNameRuler, CHORDNAMERULER_ROW, MAIN_COL, 1, 1);
479 
480     m_topStandardRuler->setSnapGrid(m_scene->getSnapGrid());
481     m_bottomStandardRuler->setSnapGrid(m_scene->getSnapGrid());
482 
483     m_topStandardRuler->connectRulerToDocPointer(document);
484     m_bottomStandardRuler->connectRulerToDocPointer(document);
485 
486     connect(m_topStandardRuler, &StandardRuler::dragPointerToPosition,
487             this, &MatrixWidget::slotStandardRulerDrag);
488     connect(m_bottomStandardRuler, &StandardRuler::dragPointerToPosition,
489             this, &MatrixWidget::slotStandardRulerDrag);
490 
491     connect(m_topStandardRuler->getLoopRuler(), &LoopRuler::startMouseMove,
492             this, &MatrixWidget::slotSRStartMouseMove);
493     connect(m_topStandardRuler->getLoopRuler(), &LoopRuler::stopMouseMove,
494             this, &MatrixWidget::slotSRStopMouseMove);
495     connect(m_bottomStandardRuler->getLoopRuler(), &LoopRuler::startMouseMove,
496             this, &MatrixWidget::slotSRStartMouseMove);
497     connect(m_bottomStandardRuler->getLoopRuler(), &LoopRuler::stopMouseMove,
498             this, &MatrixWidget::slotSRStopMouseMove);
499 
500     connect(m_tempoRuler, &TempoRuler::mousePress,
501             this, &MatrixWidget::slotTRMousePress);
502     connect(m_tempoRuler, &TempoRuler::mouseRelease,
503             this, &MatrixWidget::slotTRMouseRelease);
504 
505     connect(m_document, &RosegardenDocument::pointerPositionChanged,
506             this, &MatrixWidget::slotPointerPositionChanged);
507 
508     m_chordNameRuler->setReady();
509 
510     updateSegmentChangerBackground();
511 
512     // If there is only one Segment, hide the widgets that are only needed
513     // for multiple Segments.
514     if (segments.size() == 1) {
515         m_segmentLabel->hide();
516         m_changerWidget->hide();
517     }
518 
519     // Go with zoom factors from the first Segment.
520     setHorizontalZoomFactor(segments[0]->matrixHZoomFactor);
521     setVerticalZoomFactor(segments[0]->matrixVZoomFactor);
522 
523 }
524 
525 void
generatePitchRuler()526 MatrixWidget::generatePitchRuler()
527 {
528     delete m_pianoScene;   // Delete the old m_pitchRuler if any
529     m_localMapping.reset();
530     bool isPercussion = false;
531 
532     Composition &comp = m_document->getComposition();
533     const MidiKeyMapping *mapping = nullptr;
534     Track *track = comp.getTrackById(m_scene->getCurrentSegment()->getTrack());
535     m_instrument = m_document->getStudio().
536                             getInstrumentById(track->getInstrument());
537     if (m_instrument) {
538 
539         // Make instrument tell us if it gets destroyed.
540         connect(m_instrument, &QObject::destroyed,
541                 this, &MatrixWidget::slotInstrumentGone);
542 
543         mapping = m_instrument->getKeyMapping();
544         if (mapping) {
545             //RG_DEBUG << "generatePitchRuler(): Instrument has key mapping: " << mapping->getName();
546             m_localMapping.reset(new MidiKeyMapping(*mapping));
547             m_localMapping->extend();
548             isPercussion = true;
549         } else {
550             //RG_DEBUG << "generatePitchRuler(): Instrument has no key mapping";
551             isPercussion = false;
552         }
553     }
554     if (mapping && !m_localMapping->getMap().empty()) {
555         m_pitchRuler = new PercussionPitchRuler(nullptr, m_localMapping,
556                                                 m_scene->getYResolution());
557     } else {
558         if (m_onlyKeyMapping) {
559             // In such a case, a matrix resolution of 11 is used.
560             // (See comments in MatrixScene::setSegments().)
561             // As the piano keyboard works only with a resolution of 7, an
562             // empty key mapping will be used in place of the keyboard.
563             m_localMapping.reset(new MidiKeyMapping());
564             m_localMapping->getMap()[0] = "";  // extent() doesn't work?
565             m_localMapping->getMap()[127] = "";
566             m_pitchRuler = new PercussionPitchRuler(nullptr, m_localMapping,
567                                                     m_scene->getYResolution());
568         } else {
569             m_pitchRuler = new PianoKeyboard(nullptr);
570         }
571     }
572 
573     m_pitchRuler->setFixedSize(m_pitchRuler->sizeHint());
574     m_pianoView->setFixedWidth(m_pitchRuler->sizeHint().width());
575 
576     m_pianoScene = new QGraphicsScene();
577     QGraphicsProxyWidget *pianoKbd = m_pianoScene->addWidget(m_pitchRuler);
578     m_pianoView->setScene(m_pianoScene);
579     m_pianoView->centerOn(pianoKbd);
580 
581     QObject::connect(m_pitchRuler, &PitchRuler::hoveredOverKeyChanged,
582                      this, &MatrixWidget::slotHoveredOverKeyChanged);
583 
584     QObject::connect(m_pitchRuler, &PitchRuler::keyPressed,
585                      this, &MatrixWidget::slotKeyPressed);
586 
587     QObject::connect(m_pitchRuler, &PitchRuler::keySelected,
588                      this, &MatrixWidget::slotKeySelected);
589 
590     // Don't send the "note off" midi message to a percussion instrument
591     // when clicking on the pitch ruler
592     if (!isPercussion || !m_drumMode) {
593         connect(m_pitchRuler, &PitchRuler::keyReleased,
594                 this, &MatrixWidget::slotKeyReleased);
595     }
596 
597     // If piano scene and matrix scene don't have the same height
598     // one may shift from the other when scrolling vertically
599     QRectF viewRect = m_scene->sceneRect();
600     QRectF pianoRect = m_pianoScene->sceneRect();
601     pianoRect.setHeight(viewRect.height() + 18);
602     m_pianoScene->setSceneRect(pianoRect);
603     //@@@ The 18 pixels have been added empirically in line above to
604     //    avoid any offset between matrix and pitchruler at the end of
605     //    vertical scroll. I have no idea from where they come from.
606 
607 
608     // Apply current zoom to the new pitch ruler
609     if (m_lastZoomWasHV) {
610         // Set the view's matrix.
611         // ??? Why?  Why only in this case?  Why not in the other case?
612         //     Why isn't this handled elsewhere?  Is it?
613         QTransform m;
614         m.scale(m_hZoomFactor, m_vZoomFactor);
615         m_view->setTransform(m);
616         // Only vertical zoom factor is applied to pitch ruler
617         QTransform m2;
618         m2.scale(1, m_vZoomFactor);
619         m_pianoView->setTransform(m2);
620         m_pianoView->setFixedWidth(m_pitchRuler->sizeHint().width());
621     } else {
622         // Only vertical zoom factor is applied to pitch ruler
623         QTransform m;
624         m.scale(1.0, m_vZoomFactor);
625         m_pianoView->setTransform(m);
626         m_pianoView->setFixedWidth(m_pitchRuler->sizeHint().width());
627     }
628 
629     // Move vertically the pianoView scene to fit the matrix scene.
630     QRect mr = m_view->rect();
631     QRect pr = m_pianoView->rect();
632     QRectF smr = m_view->mapToScene(mr).boundingRect();
633     QRectF spr = m_pianoView->mapToScene(pr).boundingRect();
634     m_pianoView->centerOn(spr.center().x(), smr.center().y());
635 
636     m_pianoView->update();
637 }
638 
639 bool
segmentsContainNotes() const640 MatrixWidget::segmentsContainNotes() const
641 {
642     if (!m_scene)
643         return false;
644 
645     return m_scene->segmentsContainNotes();
646 }
647 
648 void
setHorizontalZoomFactor(double factor)649 MatrixWidget::setHorizontalZoomFactor(double factor)
650 {
651     // NOTE: scaling the keyboard up and down works well for the primary zoom
652     // because it maintains the same aspect ratio for each step.  I tried a few
653     // different ways to deal with this before deciding that since
654     // independent-axis zoom is a separate and mutually exclusive subsystem,
655     // about the only sensible thing we can do is keep the keyboard scaled at
656     // 1.0 horizontally, and only scale it vertically.  Git'r done.
657 
658     m_hZoomFactor = factor;
659     if (m_referenceScale)
660         m_referenceScale->setXZoomFactor(m_hZoomFactor);
661     m_view->resetTransform();
662     m_view->scale(m_hZoomFactor, m_vZoomFactor);
663     // Only vertical zoom factor is applied to pitch ruler
664     QTransform m;
665     m.scale(1.0, m_vZoomFactor);
666     m_pianoView->setTransform(m);
667     m_pianoView->setFixedWidth(m_pitchRuler->sizeHint().width());
668     slotScrollRulers();
669 
670     // Store in Segment(s) for next time.
671     if (m_scene)
672         m_scene->setHorizontalZoomFactor(factor);
673 }
674 
675 void
setVerticalZoomFactor(double factor)676 MatrixWidget::setVerticalZoomFactor(double factor)
677 {
678     m_vZoomFactor = factor;
679     if (m_referenceScale)
680         m_referenceScale->setYZoomFactor(m_vZoomFactor);
681     m_view->resetTransform();
682     m_view->scale(m_hZoomFactor, m_vZoomFactor);
683     // Only vertical zoom factor is applied to pitch ruler
684     QTransform m;
685     m.scale(1.0, m_vZoomFactor);
686     m_pianoView->setTransform(m);
687     m_pianoView->setFixedWidth(m_pitchRuler->sizeHint().width());
688 
689     // Store in Segment(s) for next time.
690     if (m_scene)
691         m_scene->setVerticalZoomFactor(factor);
692 }
693 
694 void
zoomInFromPanner()695 MatrixWidget::zoomInFromPanner()
696 {
697     m_hZoomFactor /= 1.1;
698     m_vZoomFactor /= 1.1;
699     if (m_referenceScale)
700         m_referenceScale->setXZoomFactor(m_hZoomFactor);
701     QTransform m;
702     m.scale(m_hZoomFactor, m_vZoomFactor);
703     m_view->setTransform(m);
704     // Only vertical zoom factor is applied to pitch ruler
705     QTransform m2;
706     m2.scale(1, m_vZoomFactor);
707     m_pianoView->setTransform(m2);
708     m_pianoView->setFixedWidth(m_pitchRuler->sizeHint().width());
709     slotScrollRulers();
710 
711     // Store in Segment(s) for next time.
712     if (m_scene) {
713         m_scene->setHorizontalZoomFactor(m_hZoomFactor);
714         m_scene->setVerticalZoomFactor(m_vZoomFactor);
715     }
716 }
717 
718 void
zoomOutFromPanner()719 MatrixWidget::zoomOutFromPanner()
720 {
721     m_hZoomFactor *= 1.1;
722     m_vZoomFactor *= 1.1;
723     if (m_referenceScale)
724         m_referenceScale->setXZoomFactor(m_hZoomFactor);
725     QTransform m;
726     m.scale(m_hZoomFactor, m_vZoomFactor);
727     m_view->setTransform(m);
728     // Only vertical zoom factor is applied to pitch ruler
729     QTransform m2;
730     m2.scale(1, m_vZoomFactor);
731     m_pianoView->setTransform(m2);
732     m_pianoView->setFixedWidth(m_pitchRuler->sizeHint().width());
733     slotScrollRulers();
734 
735     // Store in Segment(s) for next time.
736     if (m_scene) {
737         m_scene->setHorizontalZoomFactor(m_hZoomFactor);
738         m_scene->setVerticalZoomFactor(m_vZoomFactor);
739     }
740 }
741 
742 void
slotScrollRulers()743 MatrixWidget::slotScrollRulers()
744 {
745     // Get time of the window left
746     QPointF topLeft = m_view->mapToScene(0, 0);
747 
748     // Apply zoom correction
749     int x = topLeft.x() * m_hZoomFactor;
750 
751     // Scroll rulers accordingly
752     // ( -2 : to fix a small offset between view and rulers)
753     m_topStandardRuler->slotScrollHoriz(x - 2);
754     m_bottomStandardRuler->slotScrollHoriz(x - 2);
755     m_tempoRuler->slotScrollHoriz(x - 2);
756     m_chordNameRuler->slotScrollHoriz(x - 2);
757 }
758 
759 EventSelection *
getSelection() const760 MatrixWidget::getSelection() const
761 {
762     if (!m_scene)
763         return nullptr;
764 
765     return m_scene->getSelection();
766 }
767 
768 void
setSelection(EventSelection * s,bool preview)769 MatrixWidget::setSelection(EventSelection *s, bool preview)
770 {
771     if (!m_scene)
772         return;
773 
774     m_scene->setSelection(s, preview);
775 }
776 
777 const SnapGrid *
getSnapGrid() const778 MatrixWidget::getSnapGrid() const
779 {
780     if (!m_scene)
781         return nullptr;
782 
783     return m_scene->getSnapGrid();
784 }
785 
786 void
setSnap(timeT t)787 MatrixWidget::setSnap(timeT t)
788 {
789     if (!m_scene)
790         return;
791 
792     m_scene->setSnap(t);
793 }
794 
795 void
selectAll()796 MatrixWidget::selectAll()
797 {
798     if (!m_scene)
799         return;
800 
801     m_scene->selectAll();
802 }
803 
804 void
clearSelection()805 MatrixWidget::clearSelection()
806 {
807     // Actually we don't clear the selection immediately: if we're
808     // using some tool other than the select tool, then the first
809     // press switches us back to the select tool.
810 
811     // ??? Why?  Plus there's a bug.  The toolbar button does not
812     //     become pressed for the select tool.  Instead it still
813     //     shows the old tool.  Recommend changing this to do what
814     //     the user has asked.  Just call setSelection().  I've
815     //     tested it and it works fine.
816 
817     MatrixSelector *selector = dynamic_cast<MatrixSelector *>(m_currentTool);
818 
819     if (!selector)
820         setSelectAndEditTool();
821     else
822         setSelection(nullptr, false);
823 }
824 
825 Segment *
getCurrentSegment()826 MatrixWidget::getCurrentSegment()
827 {
828     if (!m_scene)
829         return nullptr;
830 
831     return m_scene->getCurrentSegment();
832 }
833 
834 void
previousSegment()835 MatrixWidget::previousSegment()
836 {
837     if (!m_scene)
838         return;
839 
840     Segment *s = m_scene->getPriorSegment();
841     if (s)
842         m_scene->setCurrentSegment(s);
843     updatePointer(m_document->getComposition().getPosition());
844     updateSegmentChangerBackground();
845 }
846 
847 void
nextSegment()848 MatrixWidget::nextSegment()
849 {
850     if (!m_scene)
851         return;
852 
853     Segment *s = m_scene->getNextSegment();
854     if (s)
855         m_scene->setCurrentSegment(s);
856     updatePointer(m_document->getComposition().getPosition());
857     updateSegmentChangerBackground();
858 }
859 
860 Device *
getCurrentDevice()861 MatrixWidget::getCurrentDevice()
862 {
863     Segment *segment = getCurrentSegment();
864     if (!segment)
865         return nullptr;
866 
867     Studio &studio = m_document->getStudio();
868     Instrument *instrument =
869         studio.getInstrumentById
870         (segment->getComposition()->getTrackById(segment->getTrack())->
871          getInstrument());
872     if (!instrument)
873         return nullptr;
874 
875     return instrument->getDevice();
876 }
877 
878 void
slotDispatchMousePress(const MatrixMouseEvent * e)879 MatrixWidget::slotDispatchMousePress(const MatrixMouseEvent *e)
880 {
881     if (!m_currentTool)
882         return;
883 
884     // Check for left and right *first*
885     if ((e->buttons & Qt::LeftButton)  &&  (e->buttons & Qt::RightButton)) {
886         m_currentTool->handleMidButtonPress(e);
887     } else if (e->buttons & Qt::LeftButton) {
888         m_currentTool->handleLeftButtonPress(e);
889     } else if (e->buttons & Qt::MiddleButton) {
890         m_currentTool->handleMidButtonPress(e);
891     } else if (e->buttons & Qt::RightButton) {
892         m_currentTool->handleRightButtonPress(e);
893     }
894 
895     m_autoScroller.start();
896 }
897 
898 void
slotDispatchMouseMove(const MatrixMouseEvent * e)899 MatrixWidget::slotDispatchMouseMove(const MatrixMouseEvent *e)
900 {
901     if (m_highlightVisible)
902         m_pitchRuler->showHighlight(e->pitch);
903 
904     // Needed to remove black trailers left by highlight at high zoom levels.
905     m_pianoView->update();
906 
907     if (!m_currentTool)
908         return;
909 
910     FollowMode followMode = m_currentTool->handleMouseMove(e);
911 
912     m_autoScroller.setFollowMode(followMode);
913 }
914 
915 void
slotDispatchMouseRelease(const MatrixMouseEvent * e)916 MatrixWidget::slotDispatchMouseRelease(const MatrixMouseEvent *e)
917 {
918     m_autoScroller.stop();
919 
920     if (!m_currentTool)
921         return;
922 
923     m_currentTool->handleMouseRelease(e);
924 }
925 
926 void
slotDispatchMouseDoubleClick(const MatrixMouseEvent * e)927 MatrixWidget::slotDispatchMouseDoubleClick(const MatrixMouseEvent *e)
928 {
929     if (!m_currentTool)
930         return;
931 
932     m_currentTool->handleMouseDoubleClick(e);
933 }
934 
935 void
showHighlight(bool visible)936 MatrixWidget::showHighlight(bool visible)
937 {
938     m_highlightVisible = visible;
939 
940     if (!visible)
941         m_pitchRuler->hideHighlight();
942 }
943 
944 void
setCanvasCursor(QCursor c)945 MatrixWidget::setCanvasCursor(QCursor c)
946 {
947     if (m_view)
948         m_view->viewport()->setCursor(c);
949 }
950 
951 void
setTool(QString name)952 MatrixWidget::setTool(QString name)
953 {
954     MatrixTool *tool = dynamic_cast<MatrixTool *>(m_toolBox->getTool(name));
955     if (!tool)
956         return;
957 
958     if (m_currentTool)
959         m_currentTool->stow();
960 
961     m_currentTool = tool;
962     m_currentTool->ready();
963     emit toolChanged(name);
964 }
965 
966 void
setDrawTool()967 MatrixWidget::setDrawTool()
968 {
969     setTool(MatrixPainter::ToolName());
970 }
971 
972 void
setEraseTool()973 MatrixWidget::setEraseTool()
974 {
975     setTool(MatrixEraser::ToolName());
976 }
977 
978 void
setSelectAndEditTool()979 MatrixWidget::setSelectAndEditTool()
980 {
981     setTool(MatrixSelector::ToolName());
982 
983     MatrixSelector *selector = dynamic_cast<MatrixSelector *>(m_currentTool);
984     if (selector) {
985         //RG_DEBUG << "setSelectAndEditTool(): selector successfully set";
986 
987         // ??? This will pile up connections each time we select the arrow
988         //     tool.  Need to use Qt::AutoConnection | Qt::UniqueConnection.
989         //     This will cause connect() to fail.  Not sure how.  Might
990         //     need to check for errors.
991         connect(selector, &MatrixSelector::editTriggerSegment,
992                 this, &MatrixWidget::editTriggerSegment);
993     }
994 }
995 
996 void
setMoveTool()997 MatrixWidget::setMoveTool()
998 {
999     setTool(MatrixMover::ToolName());
1000 }
1001 
1002 void
setResizeTool()1003 MatrixWidget::setResizeTool()
1004 {
1005     setTool(MatrixResizer::ToolName());
1006 }
1007 
1008 void
setVelocityTool()1009 MatrixWidget::setVelocityTool()
1010 {
1011     setTool(MatrixVelocity::ToolName());
1012 }
1013 
1014 void
setScrollToFollowPlayback(bool tracking)1015 MatrixWidget::setScrollToFollowPlayback(bool tracking)
1016 {
1017     m_playTracking = tracking;
1018 
1019     if (m_playTracking)
1020         m_view->ensurePositionPointerInView(true);
1021 }
1022 
1023 void
showVelocityRuler()1024 MatrixWidget::showVelocityRuler()
1025 {
1026     m_controlsWidget->togglePropertyRuler(BaseProperties::VELOCITY);
1027 }
1028 
1029 void
showPitchBendRuler()1030 MatrixWidget::showPitchBendRuler()
1031 {
1032     m_controlsWidget->togglePitchBendRuler();
1033 }
1034 
1035 void
addControlRuler(QAction * action)1036 MatrixWidget::addControlRuler(QAction *action)
1037 {
1038     QString name = action->text();
1039 
1040     // FIX #1543: If name happens to come to us with an & included somewhere,
1041     // strip the & so the string will match the one we are comparing later on.
1042     //
1043     name.replace(QRegularExpression("&"), "");
1044 
1045     //RG_DEBUG << "addControlRuler()";
1046     //RG_DEBUG << "  my name is " << name;
1047 
1048     // we just cheaply paste the code from MatrixView that created the menu to
1049     // figure out what its indices must point to (and thinking about this whole
1050     // thing, I bet it's all buggy as hell in a multi-track view where the
1051     // active segment can change, and the segment's track's device could be
1052     // completely different from whatever was first used to create the menu...
1053     // there will probably be refresh problems and crashes and general bugginess
1054     // 20% of the time, but a solution that works 80% of the time is worth
1055     // shipping, I just read on some blog, and damn the torpedoes)
1056     Controllable *c =
1057         dynamic_cast<MidiDevice *>(getCurrentDevice());
1058     if (!c) {
1059         c = dynamic_cast<SoftSynthDevice *>(getCurrentDevice());
1060         if (!c)
1061             return ;
1062     }
1063 
1064     const ControlList &list = c->getControlParameters();
1065 
1066     QString itemStr;
1067 //  int i = 0;
1068 
1069     for (ControlList::const_iterator it = list.begin();
1070          it != list.end();
1071          ++it) {
1072 
1073         // Pitch Bend is treated separately now, and there's no point in adding
1074         // "unsupported" controllers to the menu, so skip everything else
1075         if (it->getType() != Controller::EventType)
1076             continue;
1077 
1078         const QString hexValue =
1079             QString::asprintf("(0x%x)", it->getControllerNumber());
1080 
1081         // strings extracted from data files must be QObject::tr()
1082         QString itemStr = QObject::tr("%1 Controller %2 %3")
1083                                      .arg(QObject::tr(it->getName().c_str()))
1084                                      .arg(it->getControllerNumber())
1085                                      .arg(hexValue);
1086 
1087         if (name != itemStr)
1088             continue;
1089 
1090         //RG_DEBUG << "addControlRuler(): name: " << name << " should match  itemStr: " << itemStr;
1091 
1092         m_controlsWidget->addControlRuler(*it);
1093 
1094 //      if (i == menuIndex) m_controlsWidget->slotAddControlRuler(*p);
1095 //      else i++;
1096     }
1097 }
1098 
1099 void
slotHScrollBarRangeChanged(int min,int max)1100 MatrixWidget::slotHScrollBarRangeChanged(int min, int max)
1101 {
1102     if (max > min)
1103         m_view->horizontalScrollBar()->show();
1104     else
1105         m_view->horizontalScrollBar()->hide();
1106 }
1107 
1108 void
updatePointer(timeT t)1109 MatrixWidget::updatePointer(timeT t)
1110 {
1111     if (!m_scene)
1112         return;
1113 
1114     // Convert to scene X coords.
1115     double pointerX = m_scene->getRulerScale()->getXForTime(t);
1116 
1117     // Compute the limits of the scene.
1118     double sceneXMin = m_scene->sceneRect().left();
1119     double sceneXMax = m_scene->sceneRect().right();
1120 
1121     // If the pointer has gone outside the limits
1122     if (pointerX < sceneXMin  ||  sceneXMax < pointerX) {
1123         // Never move the pointer outside the scene (else the scene will grow)
1124         m_view->hidePositionPointer();
1125         m_panner->slotHidePositionPointer();
1126     } else {
1127         m_view->showPositionPointer(pointerX);
1128         m_panner->slotShowPositionPointer(pointerX);
1129     }
1130 }
1131 
1132 void
slotPointerPositionChanged(timeT t)1133 MatrixWidget::slotPointerPositionChanged(timeT t)
1134 {
1135     // ??? We don't really need "t".  We could just use
1136     //     m_document->getComposition().getPosition().
1137 
1138     updatePointer(t);
1139 
1140     // Auto-scroll
1141 
1142     if (m_playTracking)
1143         m_view->ensurePositionPointerInView(true);  // page
1144 }
1145 
1146 void
slotStandardRulerDrag(timeT t)1147 MatrixWidget::slotStandardRulerDrag(timeT t)
1148 {
1149     updatePointer(t);
1150 }
1151 
1152 void
slotSRStartMouseMove()1153 MatrixWidget::slotSRStartMouseMove()
1154 {
1155     m_autoScroller.setFollowMode(FOLLOW_HORIZONTAL);
1156     m_autoScroller.start();
1157 }
1158 
1159 void
slotSRStopMouseMove()1160 MatrixWidget::slotSRStopMouseMove()
1161 {
1162     m_autoScroller.stop();
1163 }
1164 
1165 void
slotCRWMousePress()1166 MatrixWidget::slotCRWMousePress()
1167 {
1168     m_autoScroller.start();
1169 }
1170 
1171 void
slotCRWMouseMove(FollowMode followMode)1172 MatrixWidget::slotCRWMouseMove(FollowMode followMode)
1173 {
1174     m_autoScroller.setFollowMode(followMode);
1175 }
1176 
1177 void
slotCRWMouseRelease()1178 MatrixWidget::slotCRWMouseRelease()
1179 {
1180     m_autoScroller.stop();
1181 }
1182 
1183 void
slotTRMousePress()1184 MatrixWidget::slotTRMousePress()
1185 {
1186     m_autoScroller.setFollowMode(FOLLOW_HORIZONTAL);
1187     m_autoScroller.start();
1188 }
1189 
1190 void
slotTRMouseRelease()1191 MatrixWidget::slotTRMouseRelease()
1192 {
1193     m_autoScroller.stop();
1194 }
1195 
1196 void
setTempoRulerVisible(bool visible)1197 MatrixWidget::setTempoRulerVisible(bool visible)
1198 {
1199     if (visible)
1200         m_tempoRuler->show();
1201     else
1202         m_tempoRuler->hide();
1203 }
1204 
1205 void
setChordNameRulerVisible(bool visible)1206 MatrixWidget::setChordNameRulerVisible(bool visible)
1207 {
1208     if (visible)
1209         m_chordNameRuler->show();
1210     else
1211         m_chordNameRuler->hide();
1212 }
1213 
1214 void
showEvent(QShowEvent * event)1215 MatrixWidget::showEvent(QShowEvent * event)
1216 {
1217     QWidget::showEvent(event);
1218     slotScrollRulers();
1219 }
1220 
1221 void
slotHorizontalThumbwheelMoved(int v)1222 MatrixWidget::slotHorizontalThumbwheelMoved(int v)
1223 {
1224     // limits sanity check
1225     if (v < -25)
1226         v = -25;
1227     if (v > 60)
1228         v = 60;
1229     if (m_lastH < -25)
1230         m_lastH = -25;
1231     if (m_lastH > 60)
1232         m_lastH = 60;
1233 
1234     int steps = v - m_lastH;
1235     if (steps < 0)
1236         steps *= -1;
1237 
1238     bool zoomingIn = (v > m_lastH);
1239     double newZoom = m_hZoomFactor;
1240 
1241     for (int i = 0; i < steps; ++i) {
1242         if (zoomingIn)
1243             newZoom *= 1.1;
1244         else
1245             newZoom /= 1.1;
1246     }
1247 
1248     //RG_DEBUG << "slotHorizontalThumbwheelMoved(): v is: " << v << " h zoom factor was: " << m_lastH << " now: " << newZoom << " zooming " << (zoomingIn ? "IN" : "OUT");
1249 
1250     setHorizontalZoomFactor(newZoom);
1251     m_lastH = v;
1252     m_lastZoomWasHV = false;
1253 }
1254 
1255 void
slotVerticalThumbwheelMoved(int v)1256 MatrixWidget::slotVerticalThumbwheelMoved(int v)
1257 {
1258     // limits sanity check
1259     if (v < -25)
1260         v = -25;
1261     if (v > 60)
1262         v = 60;
1263     if (m_lastV < -25)
1264         m_lastV = -25;
1265     if (m_lastV > 60)
1266         m_lastV = 60;
1267 
1268     int steps = v - m_lastV;
1269     if (steps < 0)
1270         steps *= -1;
1271 
1272     bool zoomingIn = (v > m_lastV);
1273     double newZoom = m_vZoomFactor;
1274 
1275     for (int i = 0; i < steps; ++i) {
1276         if (zoomingIn)
1277             newZoom *= 1.1;
1278         else
1279             newZoom /= 1.1;
1280     }
1281 
1282     //RG_DEBUG << "slotVerticalThumbwheelMoved(): v is: " << v << " z zoom factor was: " << m_lastV << " now: " << newZoom << " zooming " << (zoomingIn ? "IN" : "OUT");
1283 
1284     setVerticalZoomFactor(newZoom);
1285     m_lastV = v;
1286     m_lastZoomWasHV = false;
1287 }
1288 
1289 void
slotPrimaryThumbwheelMoved(int v)1290 MatrixWidget::slotPrimaryThumbwheelMoved(int v)
1291 {
1292     // little bit of kludge work to deal with value manipulations that are
1293     // outside of the constraints imposed by the primary zoom wheel itself
1294     if (v < -20)
1295         v = -20;
1296     if (v > 20)
1297         v = 20;
1298     if (m_lastHVzoomValue < -20)
1299         m_lastHVzoomValue = -20;
1300     if (m_lastHVzoomValue > 20)
1301         m_lastHVzoomValue = 20;
1302 
1303     // When dragging the wheel up and down instead of mouse wheeling it, it
1304     // steps according to its speed.  I don't see a sure way (and after all
1305     // there are no docs!) to make sure dragging results in a smooth 1:1
1306     // relationship when compared with mouse wheeling, and we are just hijacking
1307     // zoomInFromPanner() here, so we will look at the number of steps
1308     // between the old value and the last one, and call the slot that many times
1309     // in order to enforce the 1:1 relationship.
1310     int steps = v - m_lastHVzoomValue;
1311     if (steps < 0)
1312         steps *= -1;
1313 
1314     for (int i = 0; i < steps; ++i) {
1315         if (v < m_lastHVzoomValue)
1316             zoomInFromPanner();
1317         else if (v > m_lastHVzoomValue)
1318             zoomOutFromPanner();
1319     }
1320 
1321     m_lastHVzoomValue = v;
1322     m_lastZoomWasHV = true;
1323 }
1324 
1325 void
slotResetZoomClicked()1326 MatrixWidget::slotResetZoomClicked()
1327 {
1328     //RG_DEBUG << "slotResetZoomClicked()";
1329 
1330     m_hZoomFactor = 1.0;
1331     m_vZoomFactor = 1.0;
1332     if (m_referenceScale) {
1333         m_referenceScale->setXZoomFactor(m_hZoomFactor);
1334         m_referenceScale->setYZoomFactor(m_vZoomFactor);
1335     }
1336     m_view->resetTransform();
1337     QTransform m;
1338     m.scale(m_hZoomFactor, m_vZoomFactor);
1339     m_view->setTransform(m);
1340     m_view->scale(m_hZoomFactor, m_vZoomFactor);
1341     // Only vertical zoom factor is applied to pitch ruler
1342     QTransform m2;
1343     m2.scale(1, m_vZoomFactor);
1344     m_pianoView->setTransform(m2);
1345     m_pianoView->setFixedWidth(m_pitchRuler->sizeHint().width());
1346     slotScrollRulers();
1347 
1348     // scale factor 1.0 = 100% zoom
1349     m_Hzoom->setValue(1);
1350     m_Vzoom->setValue(1);
1351     m_HVzoom->setValue(0);
1352     m_lastHVzoomValue = 0;
1353     m_lastH = 0;
1354     m_lastV = 0;
1355 
1356     // Store in Segment(s) for next time.
1357     if (m_scene) {
1358         m_scene->setHorizontalZoomFactor(m_hZoomFactor);
1359         m_scene->setVerticalZoomFactor(m_vZoomFactor);
1360     }
1361 }
1362 
1363 void
slotSyncPannerZoomIn()1364 MatrixWidget::slotSyncPannerZoomIn()
1365 {
1366     int v = m_lastHVzoomValue - 1;
1367 
1368     m_HVzoom->setValue(v);
1369     slotPrimaryThumbwheelMoved(v);
1370 }
1371 
1372 void
slotSyncPannerZoomOut()1373 MatrixWidget::slotSyncPannerZoomOut()
1374 {
1375     int v = m_lastHVzoomValue + 1;
1376 
1377     m_HVzoom->setValue(v);
1378     slotPrimaryThumbwheelMoved(v);
1379 }
1380 
1381 void
slotSegmentChangerMoved(int v)1382 MatrixWidget::slotSegmentChangerMoved(int v)
1383 {
1384     // see comments in slotPrimaryThumbWheelMoved() for an explanation of that
1385     // mechanism, which is repurposed and simplified here
1386 
1387     if (v < -120)
1388         v = -120;
1389     if (v > 120)
1390         v = 120;
1391     if (m_lastSegmentChangerValue < -120)
1392         m_lastSegmentChangerValue = -120;
1393     if (m_lastSegmentChangerValue > 120)
1394         m_lastSegmentChangerValue = 120;
1395 
1396     int steps = v - m_lastSegmentChangerValue;
1397     if (steps < 0) steps *= -1;
1398 
1399     for (int i = 0; i < steps; ++i) {
1400         if (v < m_lastSegmentChangerValue)
1401             nextSegment();
1402         else if (v > m_lastSegmentChangerValue)
1403             previousSegment();
1404     }
1405 
1406     m_lastSegmentChangerValue = v;
1407     updateSegmentChangerBackground();
1408 
1409     // If we are switching between a pitched instrument segment and a pecussion
1410     // segment or betwween two percussion segments with different percussion
1411     // sets, the pitch ruler may need to be regenerated.
1412     // TODO : test if regeneration is really needed before doing it
1413     generatePitchRuler();
1414 }
1415 
1416 void
updateSegmentChangerBackground()1417 MatrixWidget::updateSegmentChangerBackground()
1418 {
1419     const Composition &composition = m_document->getComposition();
1420     const Segment *segment = m_scene->getCurrentSegment();
1421 
1422     // set the changer widget background to the now current segment's
1423     // background, and reset the tooltip style to compensate
1424     QColor segmentColor = composition.getSegmentColourMap().
1425             getColour(segment->getColourIndex());
1426 
1427     QPalette palette = m_changerWidget->palette();
1428     palette.setColor(QPalette::Window, segmentColor);
1429     m_changerWidget->setPalette(palette);
1430 
1431     const Track *track = composition.getTrackById(segment->getTrack());
1432     if (!track)
1433         return;
1434 
1435     QString trackLabel = QString::fromStdString(track->getLabel());
1436     if (trackLabel == "")
1437         trackLabel = tr("<untitled>");
1438 
1439     const QString segmentText = tr("Track %1 (%2) | %3").
1440             arg(track->getPosition() + 1).
1441             arg(trackLabel).
1442             arg(QString::fromStdString(segment->getLabel()));
1443 
1444     m_segmentLabel->setText(segmentText);
1445 
1446     // Segment label colors
1447     palette = m_segmentLabel->palette();
1448     // Background
1449     palette.setColor(QPalette::Window, segmentColor);
1450     // Foreground/Text
1451     palette.setColor(QPalette::WindowText, segment->getPreviewColour());
1452     m_segmentLabel->setPalette(palette);
1453 }
1454 
1455 void
slotHoveredOverKeyChanged(unsigned int y)1456 MatrixWidget::slotHoveredOverKeyChanged(unsigned int y)
1457 {
1458     //RG_DEBUG << "slotHoveredOverKeyChanged(" << y << ")";
1459 
1460     int evPitch = m_scene->calculatePitchFromY(y);
1461     m_pitchRuler->showHighlight(evPitch);
1462     m_pianoView->update();   // Needed to remove black trailers left by
1463                              // highlight at high zoom levels
1464 }
1465 
1466 void
slotMouseLeavesView()1467 MatrixWidget::slotMouseLeavesView()
1468 {
1469     // The mouse has left the view, so the highlight in the pitch ruler
1470     // has to be un-highlighted.
1471     m_pitchRuler->hideHighlight();
1472 
1473     // At high zoom levels, black trailers are left behind.  This makes
1474     // sure they are removed.  (Retest this.)
1475     m_pianoView->update();
1476 }
1477 
1478 
slotKeyPressed(unsigned int y,bool repeating)1479 void MatrixWidget::slotKeyPressed(unsigned int y, bool repeating)
1480 {
1481     //RG_DEBUG << "slotKeyPressed(" << y << ")";
1482 
1483     slotHoveredOverKeyChanged(y);
1484     int evPitch = m_scene->calculatePitchFromY(y);
1485 
1486     // If we are part of a run up the keyboard, don't send redundant
1487     // note ons.  Otherwise we will send a note on for every tiny movement
1488     // of the mouse.
1489     if (m_lastNote == evPitch  &&  repeating)
1490         return;
1491 
1492     // Save value
1493     m_lastNote = evPitch;
1494     if (!repeating)
1495         m_firstNote = evPitch;
1496 
1497     Composition &comp = m_document->getComposition();
1498     Studio &studio = m_document->getStudio();
1499 
1500     MatrixViewSegment *current = m_scene->getCurrentViewSegment();
1501     Track *track = comp.getTrackById(current->getSegment().getTrack());
1502     if (!track)
1503         return;
1504 
1505     Instrument *ins = studio.getInstrumentById(track->getInstrument());
1506 
1507     StudioControl::playPreviewNote(
1508             ins,  // instrument
1509             evPitch + current->getSegment().getTranspose(),  // pitch
1510             MidiMaxValue,  // velocity
1511             RealTime(-1, 0),  // duration: Note-on, no note-off.
1512             false);  // oneshot
1513 }
1514 
slotKeySelected(unsigned int y,bool repeating)1515 void MatrixWidget::slotKeySelected(unsigned int y, bool repeating)
1516 {
1517     //RG_DEBUG << "slotKeySelected(" << y << ")";
1518 
1519     // This is called when shift is held down and a key is pressed on the
1520     // pitch ruler.  This will select all notes with this pitch in the
1521     // segment.
1522 
1523     slotHoveredOverKeyChanged(y);
1524 
1525 //    getCanvasView()->scrollVertSmallSteps(y);
1526 
1527     int evPitch = m_scene->calculatePitchFromY(y);
1528 
1529     // If we are part of a run up the keyboard, don't send redundant
1530     // note ons.  Otherwise we will send a note on for every tiny movement
1531     // of the mouse.
1532     if (m_lastNote == evPitch  &&  repeating)
1533         return;
1534 
1535     // Save value
1536     m_lastNote = evPitch;
1537     if (!repeating)
1538         m_firstNote = evPitch;
1539 
1540     MatrixViewSegment *current = m_scene->getCurrentViewSegment();
1541 
1542     EventSelection *eventSelection = new EventSelection(current->getSegment());
1543 
1544     // For each Event in the current Segment...
1545     for (Segment::iterator i = current->getSegment().begin();
1546          current->getSegment().isBeforeEndMarker(i);
1547          ++i) {
1548 
1549         // If this is a Note event with a pitch...
1550         if ((*i)->isa(Note::EventType)  &&
1551             (*i)->has(BaseProperties::PITCH)) {
1552 
1553             MidiByte pitch = (*i)->get<Int>(BaseProperties::PITCH);
1554 
1555             // If this note is between the first note selected and
1556             // where we are now, select it.
1557             if (pitch >= std::min((int)m_firstNote, evPitch)  &&
1558                 pitch <= std::max((int)m_firstNote, evPitch)) {
1559                 eventSelection->addEvent(*i);
1560             }
1561         }
1562     }
1563 
1564     if (getSelection()) {
1565         // allow addFromSelection to deal with eliminating duplicates
1566         eventSelection->addFromSelection(getSelection());
1567     }
1568 
1569     setSelection(eventSelection, false);
1570 
1571     // now play the note as well
1572     Composition &comp = m_document->getComposition();
1573     Studio &studio = m_document->getStudio();
1574 
1575     Track *track = comp.getTrackById(current->getSegment().getTrack());
1576     if (!track)
1577         return;
1578 
1579     Instrument *ins = studio.getInstrumentById(track->getInstrument());
1580 
1581     StudioControl::playPreviewNote(
1582             ins,  // instrument
1583             evPitch + current->getSegment().getTranspose(),  // pitch
1584             MidiMaxValue,  // velocity
1585             RealTime(-1, 0),  // duration: Note-on, no note-off.
1586             false);  // oneshot
1587 }
1588 
slotKeyReleased(unsigned int y,bool repeating)1589 void MatrixWidget::slotKeyReleased(unsigned int y, bool repeating)
1590 {
1591     //RG_DEBUG << "slotKeyReleased(" << y << ")";
1592 
1593     int evPitch = m_scene->calculatePitchFromY(y);
1594 
1595     // If we are part of a run up the keyboard, don't send a
1596     // note off for every tiny movement of the mouse.
1597     if (m_lastNote == evPitch  &&  repeating)
1598         return;
1599 
1600     // send note off (note on at zero velocity)
1601 
1602     Composition &comp = m_document->getComposition();
1603     Studio &studio = m_document->getStudio();
1604 
1605     MatrixViewSegment *current = m_scene->getCurrentViewSegment();
1606     Track *track = comp.getTrackById(current->getSegment().getTrack());
1607     if (!track)
1608         return;
1609 
1610     Instrument *ins = studio.getInstrumentById(track->getInstrument());
1611 
1612     StudioControl::playPreviewNote(
1613             ins,  // instrument
1614             evPitch + current->getSegment().getTranspose(),  // pitch
1615             0,  // velocity: 0 => note-off
1616             RealTime(0, 1),  // duration: Need non-zero to get through.
1617             false);  // oneshot
1618 }
1619 
1620 void
showInitialPointer()1621 MatrixWidget::showInitialPointer()
1622 {
1623     if (!m_scene)
1624         return;
1625 
1626     updatePointer(m_document->getComposition().getPosition());
1627 
1628     // QGraphicsView usually defaults to the center.  We want to
1629     // start at the left center.
1630     // Note: This worked fine at the end of setSegments() as well.
1631     //       This feels like a better place.  Especially since we
1632     //       might want to scroll to where the pointer is.
1633     // ??? Probably better to scroll left/right so that the beginning
1634     //     of the selected Segment is at the left edge.  That's a bit
1635     //     tricky, but if we don't do that, the user might end up someplace
1636     //     unexpected when editing multiple segments.
1637     m_view->centerOn(QPointF(0, m_view->sceneRect().center().y()));
1638 }
1639 
1640 void
slotInstrumentGone()1641 MatrixWidget::slotInstrumentGone()
1642 {
1643     m_instrument = nullptr;
1644 }
1645 
1646 void
slotPlayPreviewNote(Segment * segment,int pitch)1647 MatrixWidget::slotPlayPreviewNote(Segment *segment, int pitch)
1648 {
1649     m_scene->playNote(*segment, pitch, 100);
1650 }
1651 
1652 void
slotDocumentModified(bool)1653 MatrixWidget::slotDocumentModified(bool)
1654 {
1655     // This covers the test case when the user toggles the "Percussion"
1656     // checkbox on the MIPP.  Also covers the test case where the key
1657     // map changes due to a change in percussion program.
1658 
1659     // ??? Eventually, this should do a full refresh of the
1660     //     entire MatrixWidget and should cover all test cases where
1661     //     the UI must update.  Unfortunately, the design of MatrixWidget
1662     //     does not include an updateWidgets().  We'll probably have to
1663     //     redesign and rewrite MatrixWidget before this becomes possible.
1664 
1665     generatePitchRuler();
1666 }
1667 
1668 void
slotZoomIn()1669 MatrixWidget::slotZoomIn()
1670 {
1671     int v = m_lastH - 1;
1672 
1673     m_Hzoom->setValue(v);
1674     slotHorizontalThumbwheelMoved(v);
1675 }
1676 
1677 void
slotZoomOut()1678 MatrixWidget::slotZoomOut()
1679 {
1680     int v = m_lastH + 1;
1681 
1682     m_Hzoom->setValue(v);
1683     slotHorizontalThumbwheelMoved(v);
1684 }
1685 
1686 
1687 }
1688 
1689