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