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 "[MarkerRuler]"
19 
20 #include "MarkerRuler.h"
21 
22 
23 #include "misc/Debug.h"
24 #include "misc/Strings.h"
25 #include "base/Composition.h"
26 #include "base/RulerScale.h"
27 #include "document/RosegardenDocument.h"
28 #include "gui/general/GUIPalette.h"
29 #include "gui/dialogs/MarkerModifyDialog.h"
30 #include "commands/edit/ModifyMarkerCommand.h"
31 #include "document/CommandHistory.h"
32 
33 #include <QMouseEvent>
34 #include <QBrush>
35 #include <QCursor>
36 #include <QFont>
37 #include <QFontMetrics>
38 #include <QPainter>
39 #include <QPen>
40 #include <QPoint>
41 #include <QMenu>
42 #include <QRect>
43 #include <QSize>
44 #include <QString>
45 #include <QWidget>
46 #include <QAction>
47 #include <QToolTip>
48 #include <QMainWindow>
49 #include <QRegion>
50 
51 namespace Rosegarden
52 {
53 
MarkerRuler(RosegardenDocument * doc,RulerScale * rulerScale,QWidget * parent,const char * name)54 MarkerRuler::MarkerRuler(RosegardenDocument *doc,
55                          RulerScale *rulerScale,
56                          QWidget* parent,
57                          const char* name)
58         : QWidget(parent),
59         m_currentXOffset(0),
60         m_width(-1),
61         m_clickX(0),
62         m_menu(nullptr),
63         m_doc(doc),
64         m_rulerScale(rulerScale),
65         m_parentMainWindow( dynamic_cast<QMainWindow*>(doc->parent()) )
66 {
67     // If the parent window has a main window above it, we need to use
68     // that as the parent main window, not the document's parent.
69     // Otherwise we'll end up adding all actions to the same
70     // (document-level) action collection regardless of which window
71     // we're in.
72 
73     this->setObjectName(name);
74     QObject *probe = parent;
75     while (probe && !dynamic_cast<QMainWindow *>(probe)) probe = probe->parent();
76     if (probe) m_parentMainWindow = dynamic_cast<QMainWindow *>(probe);
77 
78     QFont font;
79     font.setPointSize((font.pointSize() * 9) / 10);
80     setFont(font);
81 
82     createAction("insert_marker_here", SLOT(slotInsertMarkerHere()));
83     createAction("insert_marker_at_pointer", SLOT(slotInsertMarkerAtPointer()));
84     createAction("delete_marker", SLOT(slotDeleteMarker()));
85     createAction("edit_marker", SLOT(slotEditMarker()));
86 
87     this->setToolTip(tr("Click on a marker to move the playback pointer.\nShift-click to set a range between markers.\nDouble-click to open the marker editor."));
88 }
89 
~MarkerRuler()90 MarkerRuler::~MarkerRuler()
91 {
92 }
93 
94 void
createMenu()95 MarkerRuler::createMenu()
96 {
97     createMenusAndToolbars("markerruler.rc");
98 
99     m_menu = findChild<QMenu *>("marker_ruler_menu");
100 
101 //    if (!tmp) {
102 //        RG_DEBUG << "MarkerRuler::createMenu() menu not found\n"
103 //                 << domDocument().toString(4) << endl;
104 //    }
105 
106     if (!m_menu) {
107         RG_DEBUG << "MarkerRuler::createMenu() failed\n";
108     }
109 }
110 
111 
112 void
scrollHoriz(int x)113 MarkerRuler::scrollHoriz(int x)
114 {
115     m_currentXOffset = -x;
116     update();
117 }
118 
119 QSize
sizeHint() const120 MarkerRuler::sizeHint() const
121 {
122     int lastBar =
123         m_rulerScale->getLastVisibleBar();
124     double width =
125         m_rulerScale->getBarPosition(lastBar) +
126         m_rulerScale->getBarWidth(lastBar);
127 
128     return QSize(std::max(int(width), m_width), fontMetrics().height());
129 }
130 
131 QSize
minimumSizeHint() const132 MarkerRuler::minimumSizeHint() const
133 {
134     double firstBarWidth = m_rulerScale->getBarWidth(0);
135 
136     return QSize(static_cast<int>(firstBarWidth), fontMetrics().height());
137 }
138 
139 void
slotInsertMarkerHere()140 MarkerRuler::slotInsertMarkerHere()
141 {
142     emit addMarker(getClickPosition());
143 }
144 
145 void
slotInsertMarkerAtPointer()146 MarkerRuler::slotInsertMarkerAtPointer()
147 {
148     emit addMarker(m_doc->getComposition().getPosition());
149 }
150 
151 void
slotDeleteMarker()152 MarkerRuler::slotDeleteMarker()
153 {
154     RG_DEBUG << "MarkerRuler::slotDeleteMarker()\n";
155 
156     Rosegarden::Marker* marker = getMarkerAtClickPosition();
157 
158     if (marker)
159         emit deleteMarker(marker->getID(),
160                           marker->getTime(),
161                           strtoqstr(marker->getName()),
162                           strtoqstr(marker->getDescription()));
163 }
164 
165 void
slotEditMarker()166 MarkerRuler::slotEditMarker()
167 {
168     Rosegarden::Marker* marker = getMarkerAtClickPosition();
169 
170     if (!marker) return;
171 
172     // I think the ruler should be doing all this stuff itself, or
173     // emitting signals connected to a dedicated marker model object,
174     // not just relying on the app object.  Same goes for practically
175     // everything else we do.  Hey ho.  Having this here is
176     // inconsistent with the other methods, so if anyone wants to move
177     // it, be my guest.
178 
179     MarkerModifyDialog dialog(this, &m_doc->getComposition(), marker);
180     if (dialog.exec() == QDialog::Accepted) {
181         ModifyMarkerCommand *command =
182             new ModifyMarkerCommand(&m_doc->getComposition(),
183                                     marker->getID(),
184                                     dialog.getOriginalTime(),
185                                     dialog.getTime(),
186                                     qstrtostr(dialog.getName()),
187                                     qstrtostr(dialog.getDescription()));
188         CommandHistory::getInstance()->addCommand(command);
189     }
190 }
191 
192 timeT
getClickPosition()193 MarkerRuler::getClickPosition()
194 {
195     timeT t = m_rulerScale->getTimeForX
196               (m_clickX - m_currentXOffset);
197 
198     return t;
199 }
200 
201 Rosegarden::Marker*
getMarkerAtClickPosition()202 MarkerRuler::getMarkerAtClickPosition()
203 {
204     // NO_QT3 NOTE:
205     //
206     // Let's try this.  We used to use QRect visibleRect() to get a rect for
207     // further calculations.  Now the equivalent method returns a region instead
208     // of a rect.  A region could be a complex shape, but our old code was
209     // written with a rectangle in mind.  Let's try getting the boundingRect for
210     // the entire region, and using that for our subsequent calculations,
211     // instead of refactoring everything to take a region into account (which
212     // requires deeper understanding of what the old code did than I have at a
213     // glance).  This is a shot in the dark, and it's hard to predict how this
214     // is going to behave until the code is running and testable.
215     QRect clipRect = visibleRegion().boundingRect();
216 
217     int firstBar = m_rulerScale->getBarForX(clipRect.x() -
218                                             m_currentXOffset);
219     int lastBar = m_rulerScale->getLastVisibleBar();
220     if (firstBar < m_rulerScale->getFirstVisibleBar()) {
221         firstBar = m_rulerScale->getFirstVisibleBar();
222     }
223 
224     Composition &comp = m_doc->getComposition();
225     Composition::markercontainer markers = comp.getMarkers();
226 
227     timeT start = comp.getBarStart(firstBar);
228     timeT end = comp.getBarEnd(lastBar);
229 
230     // need these to calculate the visible extents of a marker tag
231     QFontMetrics metrics = fontMetrics();
232 
233     for (Composition::markerconstiterator i = markers.begin();
234             i != markers.end(); ++i) {
235 
236         if ((*i)->getTime() >= start && (*i)->getTime() < end) {
237 
238             QString name(strtoqstr((*i)->getName()));
239 
240             int x = m_rulerScale->getXForTime((*i)->getTime())
241                     + m_currentXOffset;
242 
243             int width = metrics.boundingRect(name).width() + 5;
244 
245             int nextX = -1;
246             Composition::markerconstiterator j = i;
247             ++j;
248             if (j != markers.end()) {
249                 nextX = m_rulerScale->getXForTime((*j)->getTime())
250                         + m_currentXOffset;
251             }
252 
253             if (m_clickX >= x && m_clickX <= x + width) {
254 
255                 if (nextX < x || m_clickX <= nextX) {
256 
257                     return *i;
258                 }
259             }
260         }
261     }
262 
263     return nullptr;
264 }
265 
266 void
paintEvent(QPaintEvent *)267 MarkerRuler::paintEvent(QPaintEvent*)
268 {
269     QPainter painter(this);
270 
271     // See note elsewhere...
272     QRect clipRect = visibleRegion().boundingRect();
273 
274     // In a stylesheet world, we have to paint our our own background to rescue
275     // it from the muddle of QWidget background style hacks
276     QBrush bg = QBrush(GUIPalette::getColour(GUIPalette::RulerBackground));
277     painter.fillRect(clipRect, bg);
278 
279     // Now we set the pen dungle flungy to the newly defined foreground color in
280     // GUIPalette to make the text all pretty like again.  (Whew.)
281     painter.setPen(GUIPalette::getColour(GUIPalette::RulerForeground));
282 
283     int firstBar = m_rulerScale->getBarForX(clipRect.x() -
284                                             m_currentXOffset);
285     int lastBar = m_rulerScale->getLastVisibleBar();
286     if (firstBar < m_rulerScale->getFirstVisibleBar()) {
287         firstBar = m_rulerScale->getFirstVisibleBar();
288     }
289 
290     painter.drawLine(m_currentXOffset, 0, clipRect.width(), 0);
291 
292     float minimumWidth = 25.0;
293     float testSize = ((float)(m_rulerScale->getBarPosition(firstBar + 1) -
294                               m_rulerScale->getBarPosition(firstBar)))
295                      / minimumWidth;
296 
297     const int barHeight = height();
298 
299     int every = 0;
300     int count = 0;
301 
302     if (testSize < 1.0) {
303         every = (int(1.0 / testSize));
304 
305         if (every % 2 == 0)
306             every++;
307     }
308 
309     for (int i = firstBar; i <= lastBar; ++i) {
310 
311         double x = m_rulerScale->getBarPosition(i) + m_currentXOffset;
312 
313         // avoid writing bar numbers that will be overwritten
314         if (i < lastBar) {
315             double nextx = m_rulerScale->getBarPosition(i+1) + m_currentXOffset;
316             if ((nextx - x) < 0.0001) continue;
317         }
318 
319         if (x > clipRect.x() + clipRect.width())
320             break;
321 
322         // always the first bar number
323         if (every && i != firstBar) {
324             if (count < every) {
325                 count++;
326                 continue;
327             }
328 
329             // reset count if we passed
330             count = 0;
331         }
332 
333         // adjust count for first bar line
334         if (every == firstBar)
335             count++;
336 
337         if (i != lastBar) {
338             painter.drawLine(static_cast<int>(x), 0, static_cast<int>(x), barHeight);
339 
340             if (i >= 0) {
341                 const int yText = painter.fontMetrics().ascent();
342                 const QPoint textDrawPoint(static_cast<int>(x + 4), yText);
343                 painter.drawText(textDrawPoint, QString("%1").arg(i + 1));
344             }
345         } else {
346             const QPen normalPen = painter.pen();
347             QPen endPen(Qt::black, 2);
348             painter.setPen(endPen);
349             painter.drawLine(static_cast<int>(x), 0, static_cast<int>(x), barHeight);
350             painter.setPen(normalPen);
351         }
352     }
353 
354     if (m_doc) {
355         Composition &comp = m_doc->getComposition();
356         Composition::markercontainer markers = comp.getMarkers();
357         Composition::markerconstiterator it;
358 
359         timeT start = comp.getBarStart(firstBar);
360         timeT end = comp.getBarEnd(lastBar);
361 
362         QFontMetrics metrics = painter.fontMetrics();
363 
364         for (it = markers.begin(); it != markers.end(); ++it) {
365             if ((*it)->getTime() >= start && (*it)->getTime() < end) {
366                 QString name(strtoqstr((*it)->getName()));
367 
368                 double x = m_rulerScale->getXForTime((*it)->getTime())
369                            + m_currentXOffset;
370 
371                 painter.fillRect(static_cast<int>(x), 1,
372                                  static_cast<int>(metrics.boundingRect(name).width() + 5),
373                                  barHeight - 2,
374                                  QBrush(GUIPalette::getColour(GUIPalette::MarkerBackground)));
375 
376                 painter.drawLine(int(x), 1, int(x), barHeight - 2);
377                 painter.drawLine(int(x) + 1, 1, int(x) + 1, barHeight - 2);
378 
379                 const QPoint textDrawPoint(static_cast<int>(x + 3), barHeight - 4);
380                 painter.drawText(textDrawPoint, name);
381             }
382         }
383     }
384 }
385 
386 void
mousePressEvent(QMouseEvent * e)387 MarkerRuler::mousePressEvent(QMouseEvent *e)
388 {
389     RG_DEBUG << "MarkerRuler::mousePressEvent: x = " << e->pos().x();
390 
391     if (!m_doc || !e)
392         return;
393 
394     m_clickX = e->pos().x();
395     Rosegarden::Marker* clickedMarker = getMarkerAtClickPosition();
396 
397     // if right-click, show popup menu
398     //
399     if (e->button() == Qt::RightButton) {
400         if (!m_menu)
401             createMenu();
402         if (m_menu) {
403 //             actionCollection()->action("delete_marker")->setEnabled(clickedMarker != 0);
404 //             actionCollection()->action("edit_marker")->setEnabled(clickedMarker != 0);
405             findAction("delete_marker")->setEnabled(clickedMarker != nullptr);
406             findAction("edit_marker")->setEnabled(clickedMarker != nullptr);
407 
408             m_menu->exec(QCursor::pos());
409         }
410         return;
411     }
412 
413     bool shiftPressed = ((e->modifiers() & Qt::ShiftModifier) != 0);
414 
415     Composition &comp = m_doc->getComposition();
416     Composition::markercontainer markers = comp.getMarkers();
417 
418     if (shiftPressed) { // set loop
419 
420         timeT t = m_rulerScale->getTimeForX
421             (e->pos().x() - m_currentXOffset);
422 
423         timeT prev = 0;
424 
425         for (Composition::markerconstiterator i = markers.begin();
426                 i != markers.end(); ++i) {
427 
428             timeT cur = (*i)->getTime();
429 
430             if (cur >= t) {
431                 emit setLoop(prev, cur);
432                 return ;
433             }
434 
435             prev = cur;
436         }
437 
438         if (prev > 0)
439             emit setLoop(prev, comp.getEndMarker());
440 
441     } else { // set pointer to clicked marker
442 
443         if (clickedMarker)
444             emit setPointerPosition(clickedMarker->getTime());
445     }
446 }
447 
448 void
mouseDoubleClickEvent(QMouseEvent *)449 MarkerRuler::mouseDoubleClickEvent(QMouseEvent *)
450 {
451     RG_DEBUG << "MarkerRuler::mouseDoubleClickEvent";
452 
453     emit editMarkers();
454 }
455 
456 }
457