hydrogen 1.2.3
PatternEditorRuler.cpp
Go to the documentation of this file.
1/*
2 * Hydrogen
3 * Copyright(c) 2002-2008 by Alex >Comix< Cominu [comix@users.sourceforge.net]
4 * Copyright(c) 2008-2024 The hydrogen development team [hydrogen-devel@lists.sourceforge.net]
5 *
6 * http://www.hydrogen-music.org
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY, without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see https://www.gnu.org/licenses
20 *
21 */
22
23#include <core/Hydrogen.h>
26#include <core/Basics/Pattern.h>
28
29
30using namespace H2Core;
31
32#include <QTimer>
33#include <QPainter>
34
35#include "DrumPatternEditor.h"
36#include "PatternEditorRuler.h"
37#include "PatternEditorPanel.h"
38#include "NotePropertiesRuler.h"
39#include "../HydrogenApp.h"
40#include "../Skin.h"
41
42
44 : QWidget( parent )
45 , m_nHoveredColumn( -1 )
46 , m_nTick( -1 )
47 {
48 setAttribute(Qt::WA_OpaquePaintEvent);
49 setMouseTracking( true );
50
51 //infoLog( "INIT" );
52
54
55 QColor backgroundColor( pPref->getColorTheme()->m_patternEditor_backgroundColor );
56
57 m_pPattern = nullptr;
59
61 m_nRulerHeight = 25;
62
64
65 qreal pixelRatio = devicePixelRatio();
66 m_pBackgroundPixmap = new QPixmap( m_nRulerWidth * pixelRatio,
67 m_nRulerHeight * pixelRatio );
68 m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio );
69
70 m_pTimer = new QTimer(this);
71 connect(m_pTimer, &QTimer::timeout, [=]() {
72 if ( H2Core::Hydrogen::get_instance()->getAudioEngine()->getState() ==
75 }
76 });
77
78 // Will set the active width and calls createBackground.
81 update();
82
84}
85
86
87
89 //infoLog( "DESTROY");
90 if ( m_pBackgroundPixmap ) {
92 }
93}
94
96
97 auto pHydrogen = Hydrogen::get_instance();
98 auto pAudioEngine = pHydrogen->getAudioEngine();
99
100 bool bIsSelectedPatternPlaying = false; // is the pattern playing now?
101
102 if ( pHydrogen->getMode() == Song::Mode::Song &&
103 pHydrogen->isPatternEditorLocked() ) {
104 // In case the pattern editor is locked we will always display
105 // the position tick. Even if no pattern is set at all.
106 bIsSelectedPatternPlaying = true;
107 } else {
108 /*
109 * Lock audio engine to make sure pattern list does not get
110 * modified / cleared during iteration
111 */
112 pAudioEngine->lock( RIGHT_HERE );
113
114 auto pList = pAudioEngine->getPlayingPatterns();
115 for (uint i = 0; i < pList->size(); i++) {
116 if ( m_pPattern == pList->get(i) ) {
117 bIsSelectedPatternPlaying = true;
118 break;
119 }
120 }
121
122 pAudioEngine->unlock();
123 }
124
125 int nTick = pAudioEngine->getTransportPosition()->getPatternTickPosition();
126
127 if ( nTick != m_nTick || bForce ) {
128 int nDiff = m_fGridWidth * (nTick - m_nTick);
129 m_nTick = nTick;
130 int nX = static_cast<int>( static_cast<float>(PatternEditor::nMargin) + 1 +
131 m_nTick * static_cast<float>(m_fGridWidth) -
132 static_cast<float>(Skin::nPlayheadWidth) / 2 );
133 QRect updateRect( nX -2, 0, 4 + Skin::nPlayheadWidth, height() );
134 update( updateRect );
135 if ( nDiff > 1 || nDiff < -1 ) {
136 // New cursor is far enough away from the old one that the single update rect won't cover both. So
137 // update at the old location as well.
138 updateRect.translate( -nDiff, 0 );
139 update( updateRect );
140 }
141
142 if ( ! bIsSelectedPatternPlaying ) {
143 nTick = -1;
144 }
145
146 auto pPatternEditorPanel = HydrogenApp::get_instance()->getPatternEditorPanel();
147 if ( pPatternEditorPanel != nullptr ) {
148 pPatternEditorPanel->getDrumPatternEditor()->updatePosition( nTick );
149 pPatternEditorPanel->getPianoRollEditor()->updatePosition( nTick );
150 pPatternEditorPanel->getVelocityEditor()->updatePosition( nTick );
151 pPatternEditorPanel->getPanEditor()->updatePosition( nTick );
152 pPatternEditorPanel->getLeadLagEditor()->updatePosition( nTick );
153 pPatternEditorPanel->getNoteKeyEditor()->updatePosition( nTick );
154 pPatternEditorPanel->getProbabilityEditor()->updatePosition( nTick );
155 }
156 }
157}
158
162
164 if (start) {
165 m_pTimer->start(50); // update ruler at 20 fps
166 }
167 else {
168 m_pTimer->stop();
169 }
170}
171
172
173
174void PatternEditorRuler::showEvent( QShowEvent *ev )
175{
176 UNUSED( ev );
178 updateStart(true);
179}
180
181
183 m_nHoveredColumn = -1;
184 update();
185
186 QWidget::leaveEvent( ev );
187}
188
189void PatternEditorRuler::hideEvent ( QHideEvent *ev )
190{
191 UNUSED( ev );
192 updateStart(false);
193}
194
195void PatternEditorRuler::mousePressEvent( QMouseEvent* ev ) {
196
197 if ( ev->button() == Qt::LeftButton &&
198 ev->x() < m_nWidthActive ) {
199 auto pHydrogen = Hydrogen::get_instance();
200 auto pCoreActionController = pHydrogen->getCoreActionController();
201 auto pHydrogenApp = HydrogenApp::get_instance();
202 DrumPatternEditor* pDrumPatternEditor;
203 if ( pHydrogenApp->getPatternEditorPanel() != nullptr ) {
204 pDrumPatternEditor = pHydrogenApp->getPatternEditorPanel()->getDrumPatternEditor();
205 } else {
206 pDrumPatternEditor = nullptr;
207 }
208
209 // Fall back to default values in case the GUI is starting and the
210 // pattern editor is not ready yet.
211 float fResolution;
212 bool bIsUsingTriplets;
213 if ( pDrumPatternEditor != nullptr ) {
214 fResolution = static_cast<float>(pDrumPatternEditor->getResolution());
215 bIsUsingTriplets = pDrumPatternEditor->isUsingTriplets();
216 } else {
217 fResolution = 8;
218 bIsUsingTriplets = false;
219 }
220
221 float fTripletFactor;
222 if ( bIsUsingTriplets ) {
223 fTripletFactor = 3;
224 } else {
225 fTripletFactor = 4;
226 }
227
228 long nNewTick = std::floor( static_cast<float>(m_nHoveredColumn) *
229 4 * static_cast<float>(MAX_NOTES) /
230 ( fTripletFactor * fResolution ) );
231
232 if ( pHydrogen->getMode() != Song::Mode::Pattern ) {
233 pCoreActionController->activateSongMode( false );
234 pHydrogen->setIsModified( true );
235 }
236
237 pCoreActionController->locateToTick( nNewTick );
238 }
239}
240
241void PatternEditorRuler::mouseMoveEvent( QMouseEvent* ev ) {
242
243 if ( ev->x() < m_nWidthActive ) {
244
245 auto pHydrogenApp = HydrogenApp::get_instance();
246 DrumPatternEditor* pDrumPatternEditor;
247 if ( pHydrogenApp->getPatternEditorPanel() != nullptr ) {
248 pDrumPatternEditor = pHydrogenApp->getPatternEditorPanel()->getDrumPatternEditor();
249 } else {
250 pDrumPatternEditor = nullptr;
251 }
252
253 // Fall back to default values in case the GUI is starting and the
254 // pattern editor is not ready yet.
255 float fResolution;
256 bool bIsUsingTriplets;
257 if ( pDrumPatternEditor != nullptr ) {
258 fResolution = static_cast<float>(pDrumPatternEditor->getResolution());
259 bIsUsingTriplets = pDrumPatternEditor->isUsingTriplets();
260 } else {
261 fResolution = 8;
262 bIsUsingTriplets = false;
263 }
264
265 float fTripletFactor;
266 if ( bIsUsingTriplets ) {
267 fTripletFactor = 3;
268 } else {
269 fTripletFactor = 4;
270 }
271
272 float fColumnWidth = fTripletFactor * fResolution /
273 ( 4 * static_cast<float>(MAX_NOTES) * m_fGridWidth );
274
275 int nHoveredColumn =
276 static_cast<int>(std::floor( static_cast<float>(
277 std::max( ev->x() - PatternEditor::nMargin +
278 static_cast<int>(std::round(1 / fColumnWidth / 2) ), 0 )) *
279 fColumnWidth ));
280
281 if ( nHoveredColumn != m_nHoveredColumn ) {
282 m_nHoveredColumn = nHoveredColumn;
283 update();
284 }
285 }
286}
287
288void PatternEditorRuler::updateEditor( bool bRedrawAll )
289{
290 Hydrogen *pHydrogen = Hydrogen::get_instance();
291 auto pAudioEngine = pHydrogen->getAudioEngine();
292
293 //Do not redraw anything if Export is active.
294 //https://github.com/hydrogen-music/hydrogen/issues/857
295 if( pHydrogen->getIsExportSessionActive() ) {
296 return;
297 }
298
299 PatternList *pPatternList = pHydrogen->getSong()->getPatternList();
300 int nSelectedPatternNumber = pHydrogen->getSelectedPatternNumber();
301 if ( (nSelectedPatternNumber != -1) && ( (uint)nSelectedPatternNumber < pPatternList->size() ) ) {
302 m_pPattern = pPatternList->get( nSelectedPatternNumber );
303 }
304 else {
305 m_pPattern = nullptr;
306 }
307
308 const bool bActiveRangeUpdated = updateActiveRange();
309
311
312 if ( bRedrawAll || bActiveRangeUpdated ) {
314 update( 0, 0, width(), height() );
315 }
316}
317
318
323
325{
326 auto pHydrogenApp = HydrogenApp::get_instance();
327 DrumPatternEditor* pDrumPatternEditor;
328 if ( pHydrogenApp->getPatternEditorPanel() != nullptr ) {
329 pDrumPatternEditor = pHydrogenApp->getPatternEditorPanel()->getDrumPatternEditor();
330 } else {
331 pDrumPatternEditor = nullptr;
332 }
334
335 // Resize pixmap if pixel ratio has changed
336 qreal pixelRatio = devicePixelRatio();
337 if ( m_pBackgroundPixmap->width() != m_nRulerWidth ||
338 m_pBackgroundPixmap->height() != m_nRulerHeight ||
339 m_pBackgroundPixmap->devicePixelRatio() != pixelRatio ) {
340 delete m_pBackgroundPixmap;
341 m_pBackgroundPixmap = new QPixmap( width() * pixelRatio , height() * pixelRatio );
342 m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio );
343 }
344
345 QColor backgroundColor( pPref->getColorTheme()->m_patternEditor_alternateRowColor.darker( 120 ) );
346 QColor textColor = pPref->getColorTheme()->m_patternEditor_textColor;
347 textColor.setAlpha( 220 );
348
349 QColor lineColor = pPref->getColorTheme()->m_patternEditor_lineColor;
350
351 QPainter painter( m_pBackgroundPixmap );
352
353 painter.fillRect( QRect( 0, 0, width(), height() ), backgroundColor );
354
355 // gray background for unusable section of pattern
356 if ( m_nRulerWidth - m_nWidthActive != 0 ) {
357 painter.fillRect( m_nWidthActive, 0, m_nRulerWidth - m_nWidthActive,
359 pPref->getColorTheme()->m_midLightColor );
360 }
361
362 // numbers
363
364 QFont font( pPref->getApplicationFontFamily(), getPointSize( pPref->getFontSize() ) );
365 painter.setFont(font);
366
367 uint nQuarter = 48;
368
369 // Fall back to default values in case the GUI is starting and the
370 // pattern editor is not ready yet.
371 int nResolution;
372 bool bIsUsingTriplets;
373 if ( pDrumPatternEditor != nullptr ) {
374 nResolution = pDrumPatternEditor->getResolution();
375 bIsUsingTriplets = pDrumPatternEditor->isUsingTriplets();
376 } else {
377 nResolution = 8;
378 bIsUsingTriplets = false;
379 }
380
381 // Draw numbers and quarter ticks
382 painter.setPen( textColor );
383 for ( int ii = 0; ii < 64 ; ii += 4 ) {
384 int nText_x = PatternEditor::nMargin + nQuarter / 4 * ii * m_fGridWidth;
385 painter.drawLine( nText_x, height() - 13, nText_x, height() - 1 );
386 painter.drawText( nText_x + 3, 0, 60, m_nRulerHeight,
387 Qt::AlignVCenter | Qt::AlignLeft,
388 QString("%1").arg(ii / 4 + 1) );
389 }
390
391 // Draw remaining ticks
392 float fStep;
393 if ( bIsUsingTriplets ) {
394 fStep = 4 * MAX_NOTES / ( 3 * nResolution ) * m_fGridWidth;
395 } else {
396 fStep = 4 * MAX_NOTES / ( 4 * nResolution ) * m_fGridWidth;
397 }
398 for ( float xx = PatternEditor::nMargin; xx < m_nWidthActive; xx += fStep ) {
399 painter.drawLine( xx, height() - 6, xx, height() - 1 );
400 }
401
402 painter.setPen( QPen( lineColor, 2, Qt::SolidLine ) );
403 painter.drawLine( 0, m_nRulerHeight, m_nRulerWidth, m_nRulerHeight);
404 painter.drawLine( m_nRulerWidth, 0, m_nRulerWidth, m_nRulerHeight );
405
406 m_bBackgroundInvalid = false;
407}
408
409
410void PatternEditorRuler::paintEvent( QPaintEvent *ev)
411{
413 auto pHydrogenApp = HydrogenApp::get_instance();
414 DrumPatternEditor* pDrumPatternEditor;
415 if ( pHydrogenApp->getPatternEditorPanel() != nullptr ) {
416 pDrumPatternEditor = pHydrogenApp->getPatternEditorPanel()->getDrumPatternEditor();
417 } else {
418 pDrumPatternEditor = nullptr;
419 }
420
421 if (!isVisible()) {
422 return;
423 }
424
425 qreal pixelRatio = devicePixelRatio();
426 if ( pixelRatio != m_pBackgroundPixmap->devicePixelRatio() || m_bBackgroundInvalid ) {
428 }
429
430 QPainter painter(this);
431
432 painter.drawPixmap( ev->rect(), *m_pBackgroundPixmap, QRectF( pixelRatio * ev->rect().x(),
433 pixelRatio * ev->rect().y(),
434 pixelRatio * ev->rect().width(),
435 pixelRatio * ev->rect().height() ) );
436
437 // draw cursor
438 if ( pHydrogenApp->getPatternEditorPanel() != nullptr &&
439 ( pDrumPatternEditor->hasFocus() ||
440 pHydrogenApp->getPatternEditorPanel()->getVelocityEditor()->hasFocus() ||
441 pHydrogenApp->getPatternEditorPanel()->getPanEditor()->hasFocus() ||
442 pHydrogenApp->getPatternEditorPanel()->getLeadLagEditor()->hasFocus() ||
443 pHydrogenApp->getPatternEditorPanel()->getNoteKeyEditor()->hasFocus() ||
444 pHydrogenApp->getPatternEditorPanel()->getProbabilityEditor()->hasFocus() ||
445 pHydrogenApp->getPatternEditorPanel()->getPianoRollEditor()->hasFocus() ) &&
446 ! pHydrogenApp->hideKeyboardCursor() ) {
447
448 int nCursorX = m_fGridWidth *
449 pHydrogenApp->getPatternEditorPanel()->getCursorPosition() +
451 m_fGridWidth * 5;
452
453 // Middle line to indicate the selected tick
454 painter.setPen( QPen( pPref->getColorTheme()->m_cursorColor, 2 ) );
455 painter.setRenderHint( QPainter::Antialiasing );
456 painter.drawLine( nCursorX + m_fGridWidth * 5 + 4, height() - 6,
457 nCursorX + m_fGridWidth * 5 + 4, height() - 13 );
458 painter.drawLine( nCursorX, 3, nCursorX + m_fGridWidth * 10 + 8, 3 );
459 painter.drawLine( nCursorX, 4, nCursorX, 5 );
460 painter.drawLine( nCursorX + m_fGridWidth * 10 + 8, 4,
461 nCursorX + m_fGridWidth * 10 + 8, 5 );
462 painter.drawLine( nCursorX, height() - 5,
463 nCursorX + m_fGridWidth * 10 + 8, height() - 5 );
464 painter.drawLine( nCursorX, height() - 7,
465 nCursorX, height() - 6 );
466 painter.drawLine( nCursorX + m_fGridWidth * 10 + 8, height() - 6,
467 nCursorX + m_fGridWidth * 10 + 8, height() - 7 );
468 }
469
470 // Fall back to default values in case the GUI is starting and the
471 // pattern editor is not ready yet.
472 float fResolution;
473 bool bIsUsingTriplets;
474 if ( pDrumPatternEditor != nullptr ) {
475 fResolution = static_cast<float>(pDrumPatternEditor->getResolution());
476 bIsUsingTriplets = pDrumPatternEditor->isUsingTriplets();
477 } else {
478 fResolution = 8;
479 bIsUsingTriplets = false;
480 }
481
482 float fTripletFactor;
483 if ( bIsUsingTriplets ) {
484 fTripletFactor = 3;
485 } else {
486 fTripletFactor = 4;
487 }
488
489 // draw playhead
490 if ( m_nTick != -1 ) {
491 int nOffset = Skin::getPlayheadShaftOffset();
492 int x = static_cast<int>(static_cast<float>(PatternEditor::nMargin) +
493 static_cast<float>(m_nTick) *
495
496 Skin::drawPlayhead( &painter, x - nOffset, 3, false );
497 painter.drawLine( x, 8, x, height() - 1 );
498 }
499
500 // Display playhead on hovering
501 if ( m_nHoveredColumn > -1 ) {
502 int x = PatternEditor::nMargin +
503 static_cast<int>(m_nHoveredColumn * 4 * static_cast<float>(MAX_NOTES) /
504 ( fTripletFactor * fResolution ) * m_fGridWidth);
505
506 if ( x < m_nWidthActive ) {
507 int nOffset = Skin::getPlayheadShaftOffset();
508 Skin::drawPlayhead( &painter, x - nOffset, 3, true );
509 painter.drawLine( x, 8, x, height() - 1 );
510 }
511 }
512}
513
515
516 auto pAudioEngine = H2Core::Hydrogen::get_instance()->getAudioEngine();
517 int nTicksInPattern = MAX_NOTES;
518
519 auto pPlayingPatterns = pAudioEngine->getPlayingPatterns();
520 if ( pPlayingPatterns->size() != 0 ) {
521 // Virtual patterns are already expanded in the playing
522 // patterns and must not be considered when determining the
523 // longest one.
524 nTicksInPattern = pPlayingPatterns->longest_pattern_length( false );
525 }
526
527 int nWidthActive = PatternEditor::nMargin + nTicksInPattern * m_fGridWidth;
528 if ( m_nWidthActive != nWidthActive ) {
529 m_nWidthActive = nWidthActive;
530 return true;
531 }
532
533 return false;
534}
535
537{
538 if ( m_fGridWidth >= 3 ){
539 m_fGridWidth *= 2;
540 } else {
541 m_fGridWidth *= 1.5;
542 }
544 resize( QSize( m_nRulerWidth, m_nRulerHeight ) );
545
547
549 update();
550}
551
552
554{
555 if ( m_fGridWidth > 1.5 ) {
556 if ( m_fGridWidth > 3 ){
557 m_fGridWidth /= 2;
558 } else {
559 m_fGridWidth /= 1.5;
560 }
562 resize( QSize(m_nRulerWidth, m_nRulerHeight) );
563
565
567 update();
568 }
569}
570
571
576
581
586
595
597{
598 if ( changes & ( H2Core::Preferences::Changes::Colors |
600 update( 0, 0, width(), height() );
601 }
602}
#define RIGHT_HERE
Macro intended to be used for the logging of the locking of the H2Core::AudioEngine.
Definition AudioEngine.h:59
Drum pattern editor.
@ Playing
Transport is rolling.
const PatternList * getPlayingPatterns() const
Hydrogen Audio Engine.
Definition Hydrogen.h:54
bool getIsExportSessionActive() const
Definition Hydrogen.h:644
std::shared_ptr< Song > getSong() const
Get the current song.
Definition Hydrogen.h:122
int getSelectedPatternNumber() const
Definition Hydrogen.h:660
static Hydrogen * get_instance()
Returns the current Hydrogen instance __instance.
Definition Hydrogen.h:83
AudioEngine * getAudioEngine() const
Definition Hydrogen.h:649
PatternList is a collection of patterns.
Definition PatternList.h:43
int longest_pattern_length(bool bIncludeVirtuals=true) const
Get the length of the longest pattern in the list.
Pattern * get(int idx)
get a pattern from the list
Manager for User Preferences File (singleton)
Definition Preferences.h:78
static Preferences * get_instance()
Returns a pointer to the current Preferences singleton stored in __instance.
const std::shared_ptr< ColorTheme > getColorTheme() const
Changes
Bitwise or-able options showing which part of the Preferences were altered using the PreferencesDialo...
@ Font
Either the font size or font family have changed.
@ Colors
At least one of the colors has changed.
unsigned getPatternEditorGridWidth()
void addEventListener(EventListener *pListener)
static HydrogenApp * get_instance()
Returns the instance of HydrogenApp class.
PatternEditorPanel * getPatternEditorPanel()
DrumPatternEditor * getDrumPatternEditor()
void hideEvent(QHideEvent *ev) override
void mouseMoveEvent(QMouseEvent *ev) override
bool updateActiveRange()
Updates m_nWidthActive.
void mousePressEvent(QMouseEvent *ev) override
int m_nWidthActive
Length of the song in pixels.
void updateStart(bool start)
virtual void stateChangedEvent(H2Core::AudioEngine::State) override
void onPreferencesChanged(H2Core::Preferences::Changes changes)
virtual void songModeActivationEvent() override
void leaveEvent(QEvent *ev) override
void showEvent(QShowEvent *ev) override
virtual void playingPatternsChangedEvent() override
virtual void relocationEvent() override
void updateEditor(bool bRedrawAll=false)
void paintEvent(QPaintEvent *ev) override
void updatePosition(bool bForce=false)
Queries the audio engine to update the current position of the playhead.
virtual void selectedPatternChangedEvent() override
H2Core::Pattern * m_pPattern
PatternEditorRuler(QWidget *parent)
static bool isUsingAdditionalPatterns(const H2Core::Pattern *pPattern)
Determines whether to pattern editor should show further patterns (determined by getPattersToShow()) ...
void updatePosition(float fTick)
Caches the AudioEngine::m_nPatternTickPosition in the member variable m_nTick and triggers an update(...
bool isUsingTriplets() const
static constexpr int nMargin
uint getResolution() const
static void drawPlayhead(QPainter *p, int x, int y, bool bHovered=false)
Definition Skin.cpp:203
static constexpr int nPlayheadWidth
Definition Skin.h:77
static int getPlayheadShaftOffset()
Definition Skin.h:79
constexpr int getPointSize(H2Core::FontTheme::FontSize fontSize) const
#define MAX_NOTES
Maximum number of notes.
Definition config.dox:79
#define UNUSED(v)
Definition Globals.h:42