hydrogen 1.2.6
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-2025 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"
40#include "../HydrogenApp.h"
41#include "../Skin.h"
42
43
45 : QWidget( parent )
46 , m_nHoveredColumn( -1 )
47 , m_nTick( -1 )
48 {
49 setAttribute(Qt::WA_OpaquePaintEvent);
50 setMouseTracking( true );
51
52 //infoLog( "INIT" );
53
55
56 QColor backgroundColor( pPref->getColorTheme()->m_patternEditor_backgroundColor );
57
58 m_pPattern = nullptr;
60
62 m_nRulerHeight = 25;
63
65
66 qreal pixelRatio = devicePixelRatio();
67 m_pBackgroundPixmap = new QPixmap( m_nRulerWidth * pixelRatio,
68 m_nRulerHeight * pixelRatio );
69 m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio );
70
71 m_pTimer = new QTimer(this);
72 connect(m_pTimer, &QTimer::timeout, [=]() {
73 if ( H2Core::Hydrogen::get_instance()->getAudioEngine()->getState() ==
76 }
77 });
78
79 // Will set the active width and calls createBackground.
82 update();
83
85}
86
87
88
90 //infoLog( "DESTROY");
91 if ( m_pBackgroundPixmap ) {
93 }
94}
95
97
98 auto pHydrogen = Hydrogen::get_instance();
99 auto pAudioEngine = pHydrogen->getAudioEngine();
100
101 bool bIsSelectedPatternPlaying = false; // is the pattern playing now?
102
103 if ( pHydrogen->getMode() == Song::Mode::Song &&
104 pHydrogen->isPatternEditorLocked() ) {
105 // In case the pattern editor is locked we will always display
106 // the position tick. Even if no pattern is set at all.
107 bIsSelectedPatternPlaying = true;
108 } else {
109 /*
110 * Lock audio engine to make sure pattern list does not get
111 * modified / cleared during iteration
112 */
113 pAudioEngine->lock( RIGHT_HERE );
114
115 auto pList = pAudioEngine->getPlayingPatterns();
116 for (uint i = 0; i < pList->size(); i++) {
117 if ( m_pPattern == pList->get(i) ) {
118 bIsSelectedPatternPlaying = true;
119 break;
120 }
121 }
122
123 pAudioEngine->unlock();
124 }
125
126 int nTick = pAudioEngine->getTransportPosition()->getPatternTickPosition();
127
128 if ( nTick != m_nTick || bForce ) {
129 int nDiff = m_fGridWidth * (nTick - m_nTick);
130 m_nTick = nTick;
131 int nX = static_cast<int>( static_cast<float>(PatternEditor::nMargin) + 1 +
132 m_nTick * static_cast<float>(m_fGridWidth) -
133 static_cast<float>(Skin::nPlayheadWidth) / 2 );
134 QRect updateRect( nX -2, 0, 4 + Skin::nPlayheadWidth, height() );
135 update( updateRect );
136 if ( nDiff > 1 || nDiff < -1 ) {
137 // New cursor is far enough away from the old one that the single update rect won't cover both. So
138 // update at the old location as well.
139 updateRect.translate( -nDiff, 0 );
140 update( updateRect );
141 }
142
143 if ( ! bIsSelectedPatternPlaying ) {
144 nTick = -1;
145 }
146
147 auto pPatternEditorPanel = HydrogenApp::get_instance()->getPatternEditorPanel();
148 if ( pPatternEditorPanel != nullptr ) {
149 pPatternEditorPanel->getDrumPatternEditor()->updatePosition( nTick );
150 pPatternEditorPanel->getPianoRollEditor()->updatePosition( nTick );
151 pPatternEditorPanel->getVelocityEditor()->updatePosition( nTick );
152 pPatternEditorPanel->getPanEditor()->updatePosition( nTick );
153 pPatternEditorPanel->getLeadLagEditor()->updatePosition( nTick );
154 pPatternEditorPanel->getNoteKeyEditor()->updatePosition( nTick );
155 pPatternEditorPanel->getProbabilityEditor()->updatePosition( nTick );
156 }
157 }
158}
159
163
165 if (start) {
166 m_pTimer->start(50); // update ruler at 20 fps
167 }
168 else {
169 m_pTimer->stop();
170 }
171}
172
173
174
175void PatternEditorRuler::showEvent( QShowEvent *ev )
176{
177 UNUSED( ev );
179 updateStart(true);
180}
181
182
184 m_nHoveredColumn = -1;
185 update();
186
187 QWidget::leaveEvent( ev );
188}
189
190void PatternEditorRuler::hideEvent ( QHideEvent *ev )
191{
192 UNUSED( ev );
193 updateStart(false);
194}
195
196void PatternEditorRuler::mousePressEvent( QMouseEvent* ev ) {
197
198 auto pEv = static_cast<MouseEvent*>( ev );
199
200 if ( ev->button() == Qt::LeftButton &&
201 pEv->position().x() < m_nWidthActive ) {
202 auto pHydrogen = Hydrogen::get_instance();
203 auto pCoreActionController = pHydrogen->getCoreActionController();
204 auto pHydrogenApp = HydrogenApp::get_instance();
205 DrumPatternEditor* pDrumPatternEditor;
206 if ( pHydrogenApp->getPatternEditorPanel() != nullptr ) {
207 pDrumPatternEditor = pHydrogenApp->getPatternEditorPanel()->getDrumPatternEditor();
208 } else {
209 pDrumPatternEditor = nullptr;
210 }
211
212 // Fall back to default values in case the GUI is starting and the
213 // pattern editor is not ready yet.
214 float fResolution;
215 bool bIsUsingTriplets;
216 if ( pDrumPatternEditor != nullptr ) {
217 fResolution = static_cast<float>(pDrumPatternEditor->getResolution());
218 bIsUsingTriplets = pDrumPatternEditor->isUsingTriplets();
219 } else {
220 fResolution = 8;
221 bIsUsingTriplets = false;
222 }
223
224 float fTripletFactor;
225 if ( bIsUsingTriplets ) {
226 fTripletFactor = 3;
227 } else {
228 fTripletFactor = 4;
229 }
230
231 long nNewTick = std::floor( static_cast<float>(m_nHoveredColumn) *
232 4 * static_cast<float>(MAX_NOTES) /
233 ( fTripletFactor * fResolution ) );
234
235 if ( pHydrogen->getMode() != Song::Mode::Pattern ) {
236 pCoreActionController->activateSongMode( false );
237 pHydrogen->setIsModified( true );
238 }
239
240 pCoreActionController->locateToTick( nNewTick );
241 }
242}
243
244void PatternEditorRuler::mouseMoveEvent( QMouseEvent* ev ) {
245
246 auto pEv = static_cast<MouseEvent*>( ev );
247 if ( pEv->position().x() < m_nWidthActive ) {
248
249 auto pHydrogenApp = HydrogenApp::get_instance();
250 DrumPatternEditor* pDrumPatternEditor;
251 if ( pHydrogenApp->getPatternEditorPanel() != nullptr ) {
252 pDrumPatternEditor = pHydrogenApp->getPatternEditorPanel()->getDrumPatternEditor();
253 } else {
254 pDrumPatternEditor = nullptr;
255 }
256
257 // Fall back to default values in case the GUI is starting and the
258 // pattern editor is not ready yet.
259 float fResolution;
260 bool bIsUsingTriplets;
261 if ( pDrumPatternEditor != nullptr ) {
262 fResolution = static_cast<float>(pDrumPatternEditor->getResolution());
263 bIsUsingTriplets = pDrumPatternEditor->isUsingTriplets();
264 } else {
265 fResolution = 8;
266 bIsUsingTriplets = false;
267 }
268
269 float fTripletFactor;
270 if ( bIsUsingTriplets ) {
271 fTripletFactor = 3;
272 } else {
273 fTripletFactor = 4;
274 }
275
276 float fColumnWidth = fTripletFactor * fResolution /
277 ( 4 * static_cast<float>(MAX_NOTES) * m_fGridWidth );
278
279 int nHoveredColumn =
280 static_cast<int>(std::floor( static_cast<float>(
281 std::max( static_cast<int>(pEv->position().x()) -
283 static_cast<int>(std::round(1 / fColumnWidth / 2) ), 0 )) *
284 fColumnWidth ));
285
286 if ( nHoveredColumn != m_nHoveredColumn ) {
287 m_nHoveredColumn = nHoveredColumn;
288 update();
289 }
290 }
291}
292
293void PatternEditorRuler::updateEditor( bool bRedrawAll )
294{
295 Hydrogen *pHydrogen = Hydrogen::get_instance();
296 auto pAudioEngine = pHydrogen->getAudioEngine();
297
298 //Do not redraw anything if Export is active.
299 //https://github.com/hydrogen-music/hydrogen/issues/857
300 if( pHydrogen->getIsExportSessionActive() ) {
301 return;
302 }
303
304 PatternList *pPatternList = pHydrogen->getSong()->getPatternList();
305 int nSelectedPatternNumber = pHydrogen->getSelectedPatternNumber();
306 if ( (nSelectedPatternNumber != -1) && ( (uint)nSelectedPatternNumber < pPatternList->size() ) ) {
307 m_pPattern = pPatternList->get( nSelectedPatternNumber );
308 }
309 else {
310 m_pPattern = nullptr;
311 }
312
313 const bool bActiveRangeUpdated = updateActiveRange();
314
316
317 if ( bRedrawAll || bActiveRangeUpdated ) {
319 update( 0, 0, width(), height() );
320 }
321}
322
323
328
330{
331 auto pHydrogenApp = HydrogenApp::get_instance();
332 DrumPatternEditor* pDrumPatternEditor;
333 if ( pHydrogenApp->getPatternEditorPanel() != nullptr ) {
334 pDrumPatternEditor = pHydrogenApp->getPatternEditorPanel()->getDrumPatternEditor();
335 } else {
336 pDrumPatternEditor = nullptr;
337 }
339
340 // Resize pixmap if pixel ratio has changed
341 qreal pixelRatio = devicePixelRatio();
342 if ( m_pBackgroundPixmap->width() != m_nRulerWidth ||
343 m_pBackgroundPixmap->height() != m_nRulerHeight ||
344 m_pBackgroundPixmap->devicePixelRatio() != pixelRatio ) {
345 delete m_pBackgroundPixmap;
346 m_pBackgroundPixmap = new QPixmap( width() * pixelRatio , height() * pixelRatio );
347 m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio );
348 }
349
350 QColor backgroundColor( pPref->getColorTheme()->m_patternEditor_alternateRowColor.darker( 120 ) );
351 QColor textColor = pPref->getColorTheme()->m_patternEditor_textColor;
352 textColor.setAlpha( 220 );
353
354 QColor lineColor = pPref->getColorTheme()->m_patternEditor_lineColor;
355
356 QPainter painter( m_pBackgroundPixmap );
357
358 painter.fillRect( QRect( 0, 0, width(), height() ), backgroundColor );
359
360 // gray background for unusable section of pattern
361 if ( m_nRulerWidth - m_nWidthActive != 0 ) {
362 painter.fillRect( m_nWidthActive, 0, m_nRulerWidth - m_nWidthActive,
364 pPref->getColorTheme()->m_midLightColor );
365 }
366
367 // numbers
368
369 QFont font( pPref->getApplicationFontFamily(), getPointSize( pPref->getFontSize() ) );
370 painter.setFont(font);
371
372 uint nQuarter = 48;
373
374 // Fall back to default values in case the GUI is starting and the
375 // pattern editor is not ready yet.
376 int nResolution;
377 bool bIsUsingTriplets;
378 if ( pDrumPatternEditor != nullptr ) {
379 nResolution = pDrumPatternEditor->getResolution();
380 bIsUsingTriplets = pDrumPatternEditor->isUsingTriplets();
381 } else {
382 nResolution = 8;
383 bIsUsingTriplets = false;
384 }
385
386 // Draw numbers and quarter ticks
387 painter.setPen( textColor );
388 for ( int ii = 0; ii < 64 ; ii += 4 ) {
389 int nText_x = PatternEditor::nMargin + nQuarter / 4 * ii * m_fGridWidth;
390 painter.drawLine( nText_x, height() - 13, nText_x, height() - 1 );
391 painter.drawText( nText_x + 3, 0, 60, m_nRulerHeight,
392 Qt::AlignVCenter | Qt::AlignLeft,
393 QString("%1").arg(ii / 4 + 1) );
394 }
395
396 // Draw remaining ticks
397 float fStep;
398 if ( bIsUsingTriplets ) {
399 fStep = 4 * MAX_NOTES / ( 3 * nResolution ) * m_fGridWidth;
400 } else {
401 fStep = 4 * MAX_NOTES / ( 4 * nResolution ) * m_fGridWidth;
402 }
403 for ( float xx = PatternEditor::nMargin; xx < m_nWidthActive; xx += fStep ) {
404 painter.drawLine( xx, height() - 6, xx, height() - 1 );
405 }
406
407 painter.setPen( QPen( lineColor, 2, Qt::SolidLine ) );
408 painter.drawLine( 0, m_nRulerHeight, m_nRulerWidth, m_nRulerHeight);
409 painter.drawLine( m_nRulerWidth, 0, m_nRulerWidth, m_nRulerHeight );
410
411 m_bBackgroundInvalid = false;
412}
413
414
415void PatternEditorRuler::paintEvent( QPaintEvent *ev)
416{
418 auto pHydrogenApp = HydrogenApp::get_instance();
419 DrumPatternEditor* pDrumPatternEditor;
420 if ( pHydrogenApp->getPatternEditorPanel() != nullptr ) {
421 pDrumPatternEditor = pHydrogenApp->getPatternEditorPanel()->getDrumPatternEditor();
422 } else {
423 pDrumPatternEditor = nullptr;
424 }
425
426 if (!isVisible()) {
427 return;
428 }
429
430 qreal pixelRatio = devicePixelRatio();
431 if ( pixelRatio != m_pBackgroundPixmap->devicePixelRatio() || m_bBackgroundInvalid ) {
433 }
434
435 QPainter painter(this);
436
437 painter.drawPixmap( ev->rect(), *m_pBackgroundPixmap, QRectF( pixelRatio * ev->rect().x(),
438 pixelRatio * ev->rect().y(),
439 pixelRatio * ev->rect().width(),
440 pixelRatio * ev->rect().height() ) );
441
442 // draw cursor
443 if ( pHydrogenApp->getPatternEditorPanel() != nullptr &&
444 ( pDrumPatternEditor->hasFocus() ||
445 pHydrogenApp->getPatternEditorPanel()->getVelocityEditor()->hasFocus() ||
446 pHydrogenApp->getPatternEditorPanel()->getPanEditor()->hasFocus() ||
447 pHydrogenApp->getPatternEditorPanel()->getLeadLagEditor()->hasFocus() ||
448 pHydrogenApp->getPatternEditorPanel()->getNoteKeyEditor()->hasFocus() ||
449 pHydrogenApp->getPatternEditorPanel()->getProbabilityEditor()->hasFocus() ||
450 pHydrogenApp->getPatternEditorPanel()->getPianoRollEditor()->hasFocus() ) &&
451 ! pHydrogenApp->hideKeyboardCursor() ) {
452
453 int nCursorX = m_fGridWidth *
454 pHydrogenApp->getPatternEditorPanel()->getCursorPosition() +
456 m_fGridWidth * 5;
457
458 // Middle line to indicate the selected tick
459 painter.setPen( QPen( pPref->getColorTheme()->m_cursorColor, 2 ) );
460 painter.setRenderHint( QPainter::Antialiasing );
461 painter.drawLine( nCursorX + m_fGridWidth * 5 + 4, height() - 6,
462 nCursorX + m_fGridWidth * 5 + 4, height() - 13 );
463 painter.drawLine( nCursorX, 3, nCursorX + m_fGridWidth * 10 + 8, 3 );
464 painter.drawLine( nCursorX, 4, nCursorX, 5 );
465 painter.drawLine( nCursorX + m_fGridWidth * 10 + 8, 4,
466 nCursorX + m_fGridWidth * 10 + 8, 5 );
467 painter.drawLine( nCursorX, height() - 5,
468 nCursorX + m_fGridWidth * 10 + 8, height() - 5 );
469 painter.drawLine( nCursorX, height() - 7,
470 nCursorX, height() - 6 );
471 painter.drawLine( nCursorX + m_fGridWidth * 10 + 8, height() - 6,
472 nCursorX + m_fGridWidth * 10 + 8, height() - 7 );
473 }
474
475 // Fall back to default values in case the GUI is starting and the
476 // pattern editor is not ready yet.
477 float fResolution;
478 bool bIsUsingTriplets;
479 if ( pDrumPatternEditor != nullptr ) {
480 fResolution = static_cast<float>(pDrumPatternEditor->getResolution());
481 bIsUsingTriplets = pDrumPatternEditor->isUsingTriplets();
482 } else {
483 fResolution = 8;
484 bIsUsingTriplets = false;
485 }
486
487 float fTripletFactor;
488 if ( bIsUsingTriplets ) {
489 fTripletFactor = 3;
490 } else {
491 fTripletFactor = 4;
492 }
493
494 // draw playhead
495 if ( m_nTick != -1 ) {
496 int nOffset = Skin::getPlayheadShaftOffset();
497 int x = static_cast<int>(static_cast<float>(PatternEditor::nMargin) +
498 static_cast<float>(m_nTick) *
500
501 Skin::drawPlayhead( &painter, x - nOffset, 3, false );
502 painter.drawLine( x, 8, x, height() - 1 );
503 }
504
505 // Display playhead on hovering
506 if ( m_nHoveredColumn > -1 ) {
507 int x = PatternEditor::nMargin +
508 static_cast<int>(m_nHoveredColumn * 4 * static_cast<float>(MAX_NOTES) /
509 ( fTripletFactor * fResolution ) * m_fGridWidth);
510
511 if ( x < m_nWidthActive ) {
512 int nOffset = Skin::getPlayheadShaftOffset();
513 Skin::drawPlayhead( &painter, x - nOffset, 3, true );
514 painter.drawLine( x, 8, x, height() - 1 );
515 }
516 }
517}
518
520
521 auto pAudioEngine = H2Core::Hydrogen::get_instance()->getAudioEngine();
522 int nTicksInPattern = MAX_NOTES;
523
524 auto pPlayingPatterns = pAudioEngine->getPlayingPatterns();
525 if ( pPlayingPatterns->size() != 0 ) {
526 // Virtual patterns are already expanded in the playing
527 // patterns and must not be considered when determining the
528 // longest one.
529 nTicksInPattern = pPlayingPatterns->longest_pattern_length( false );
530 }
531
532 int nWidthActive = PatternEditor::nMargin + nTicksInPattern * m_fGridWidth;
533 if ( m_nWidthActive != nWidthActive ) {
534 m_nWidthActive = nWidthActive;
535 return true;
536 }
537
538 return false;
539}
540
542{
543 if ( m_fGridWidth >= 3 ){
544 m_fGridWidth *= 2;
545 } else {
546 m_fGridWidth *= 1.5;
547 }
549 resize( QSize( m_nRulerWidth, m_nRulerHeight ) );
550
552
554 update();
555}
556
557
559{
560 if ( m_fGridWidth > 1.5 ) {
561 if ( m_fGridWidth > 3 ){
562 m_fGridWidth /= 2;
563 } else {
564 m_fGridWidth /= 1.5;
565 }
567 resize( QSize(m_nRulerWidth, m_nRulerHeight) );
568
570
572 update();
573 }
574}
575
576
581
586
591
600
602{
603 if ( changes & ( H2Core::Preferences::Changes::Colors |
605 update( 0, 0, width(), height() );
606 }
607}
#define RIGHT_HERE
Macro intended to be used for the logging of the locking of the H2Core::AudioEngine.
Definition AudioEngine.h:61
Drum pattern editor.
@ Playing
Transport is rolling.
Hydrogen Audio Engine.
Definition Hydrogen.h:54
bool getIsExportSessionActive() const
Definition Hydrogen.h:658
std::shared_ptr< Song > getSong() const
Get the current song.
Definition Hydrogen.h:123
int getSelectedPatternNumber() const
Definition Hydrogen.h:674
static Hydrogen * get_instance()
Returns the current Hydrogen instance __instance.
Definition Hydrogen.h:84
AudioEngine * getAudioEngine() const
Definition Hydrogen.h:663
PatternList is a collection of patterns.
Definition PatternList.h:43
Pattern * get(int idx)
get a pattern from the list
Manager for User Preferences File (singleton)
Definition Preferences.h:79
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()
Compatibility class to support QMouseEvent more esily in Qt5 and Qt6.
Definition MouseEvent.h:35
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()) ...
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