hydrogen 1.2.6
SongEditor.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 <assert.h>
24#include <algorithm>
25#include <memory>
26
27#include <core/Basics/Song.h>
28#include <core/Hydrogen.h>
29#include <core/Basics/Pattern.h>
33#include <core/EventQueue.h>
34#include <core/Helpers/Files.h>
36#include <core/Helpers/Xml.h>
38using namespace H2Core;
39
40#include "UndoActions.h"
41#include "MainForm.h"
42#include "SongEditor.h"
43#include "SongEditorPanel.h"
46#include "PatternFillDialog.h"
53#include "../HydrogenApp.h"
54#include "../CommonStrings.h"
55#include "../InstrumentRack.h"
58#include "../Skin.h"
60
61
62
63
64#ifdef WIN32
65#include <time.h>
66#include <windows.h>
67#endif
68
70 bool bActive;
71 bool bNext;
72 QString sPatternName;
73};
74
75
76SongEditor::SongEditor( QWidget *parent, QScrollArea *pScrollView, SongEditorPanel *pSongEditorPanel )
77 : QWidget( parent )
78 , m_bSequenceChanged( true )
79 , m_pScrollView( pScrollView )
80 , m_pSongEditorPanel( pSongEditorPanel )
81 , m_selection( this )
82 , m_pHydrogen( nullptr )
83 , m_pAudioEngine( nullptr )
84 , m_bEntered( false )
85 , m_pBackgroundPixmap( nullptr )
86 , m_pSequencePixmap( nullptr )
87{
89 m_pAudioEngine = m_pHydrogen->getAudioEngine();
90
92 m_nMaxPatternColors = pPref->getMaxPatternColors(); // no need to
93 // update this one.
94
96 connect( m_pScrollView->verticalScrollBar(), SIGNAL( valueChanged( int ) ), this, SLOT( scrolled( int ) ) );
97 connect( m_pScrollView->horizontalScrollBar(), SIGNAL( valueChanged( int ) ), this, SLOT( scrolled( int ) ) );
98
99 setAttribute(Qt::WA_OpaquePaintEvent);
100 setFocusPolicy (Qt::StrongFocus);
101
104
105 m_nCursorRow = 0;
106 m_nCursorColumn = 0;
107
108 int nInitialWidth = SongEditor::nMargin + pPref->getMaxBars() * m_nGridWidth;
109
110 this->resize( QSize( nInitialWidth, m_nMinimumHeight ) );
111
112 createBackground(); // create m_backgroundPixmap pixmap
113
114 // Popup context menu
115 m_pPopupMenu = new QMenu( this );
116 m_pPopupMenu->addAction( tr( "&Cut" ), this, SLOT( cut() ) );
117 m_pPopupMenu->addAction( tr( "&Copy" ), this, SLOT( copy() ) );
118 m_pPopupMenu->addAction( tr( "&Paste" ), this, SLOT( paste() ) );
119 m_pPopupMenu->addAction( tr( "&Delete" ), this, SLOT( deleteSelection() ) );
120 m_pPopupMenu->addAction( tr( "Select &all" ), this, SLOT( selectAll() ) );
121 m_pPopupMenu->addAction( tr( "Clear selection" ), this, SLOT( selectNone() ) );
122 m_pPopupMenu->setObjectName( "SongEditorPopup" );
123
125}
126
127
128
130{
131 if ( m_pBackgroundPixmap ) {
132 delete m_pBackgroundPixmap;
133 }
134 if ( m_pSequencePixmap ) {
135 delete m_pSequencePixmap;
136 }
137}
138
139
159int SongEditor::yScrollTarget( QScrollArea *pScrollArea, int *pnPatternInView )
160{
161 Hydrogen *pHydrogen = Hydrogen::get_instance();
162 const int nScroll = pScrollArea->verticalScrollBar()->value();
163 int nHeight = pScrollArea->height();
164
165 auto pPlayingPatterns = m_pAudioEngine->getPlayingPatterns();
166
167 // If no patterns are playing, no scrolling needed either.
168 if ( pPlayingPatterns->size() == 0 ) {
169 return nScroll;
170 }
171
172 m_pAudioEngine->lock( RIGHT_HERE );
173
174 PatternList *pSongPatterns = pHydrogen->getSong()->getPatternList();
175
176 // Duplicate the playing patterns vector before finding the pattern numbers of the playing patterns. This
177 // avoids doing a linear search in the critical section.
178 std::vector<Pattern *> currentPatterns;
179 for ( int ii = 0; ii < pPlayingPatterns->size(); ++ii ) {
180 currentPatterns.push_back( pPlayingPatterns->get( ii ) );
181 }
182 m_pAudioEngine->unlock();
183
184 std::vector<int> playingRows;
185 for ( Pattern *pPattern : currentPatterns ) {
186 playingRows.push_back( pSongPatterns->index( pPattern ) );
187 }
188
189 // Occasionally the detection of playing patterns glitches at the
190 // transition to empty columns.
191 if ( playingRows.size() == 0 ) {
192 return nScroll;
193 }
194
195 // Check if there are any currently playing patterns which are entirely visible.
196 for ( int r : playingRows ) {
197 if ( r * m_nGridHeight >= nScroll
198 && (r+1) * m_nGridHeight <= nScroll + nHeight) {
199 // Entirely visible. Our current scroll value is good.
200 if ( pnPatternInView ) {
201 *pnPatternInView = r;
202 }
203 return nScroll;
204 }
205 }
206
207 // Find the maximum number of patterns that will fit in the viewport. We do this by sorting the playing
208 // patterns on their row value, and traversing in order, considering each pattern in turn as visible just
209 // at the bottom of the viewport. The pattern visible nearest the top of the viewport is tracked, and the
210 // number of patterns visible in the viewport is given by the difference of the indices in the pattern
211 // array.
212 //
213 // We track the maximum number of patterns visible, and record the patterns to scroll to differently
214 // depending on whether the pattern is above or below the current viewport: for patterns above, we record
215 // the topmost pattern in the maximal group, and for those below, record the bottommost pattern, as these
216 // define the minimum amount of scrolling needed to fit the patterns in and don't want to scroll further
217 // just to expose empty cells.
218
219 std::sort( playingRows.begin(), playingRows.end() );
220
221 int nTopIdx = 0;
222 int nAboveMax = 0, nAbovePattern = -1, nAboveClosestPattern = -1,
223 nBelowMax = 0, nBelowPattern = -1, nBelowClosestPattern = -1;
224
225 for ( int nBottomIdx = 0; nBottomIdx < playingRows.size(); nBottomIdx++) {
226 int nBottom = playingRows[ nBottomIdx ] * m_nGridHeight;
227 int nTop;
228 // Each bottom pattern is further down the list, so update the top pattern to track the top of the
229 // viewport.
230 for (;;) {
231 nTop = ( playingRows[ nTopIdx ] +1 ) * m_nGridHeight -1;
232 if ( nTop < nBottom - nHeight ) {
233 nTopIdx++;
234 assert( nTopIdx <= nBottomIdx && nTopIdx < playingRows.size() );
235 } else {
236 break;
237 }
238 }
239 int nPatternsInViewport = nBottomIdx - nTopIdx +1;
240 if ( nBottom < nScroll ) {
241 // Above the viewport, accept any new maximal group, to find the maximal group closest to the
242 // current viewport.
243 if ( nPatternsInViewport >= nAboveMax ) {
244 nAboveMax = nPatternsInViewport;
245 // Above the viewport, we want to move only so far as to get the top pattern into the
246 // viewport. Record the top pattern.
247 nAbovePattern = playingRows[ nTopIdx ];
248 nAboveClosestPattern = playingRows[ nBottomIdx ];
249 }
250 } else {
251 // Below the viewport, only accept a new maximal group if it's greater than the current maximal
252 // group.
253 if ( nPatternsInViewport > nBelowMax ) {
254 nBelowMax = nPatternsInViewport;
255 // Below the viewport, we want to scroll down to get the bottom pattern into view, so record
256 // the bottom pattern.
257 nBelowPattern = playingRows[ nBottomIdx ];
258 nBelowClosestPattern = playingRows[ nTopIdx ];
259 }
260 }
261 }
262
263 // Pick between moving up, or moving down.
264 int nAboveY = nAbovePattern * m_nGridHeight;
265 int nBelowY = (nBelowPattern +1) * m_nGridHeight - nHeight;
266 enum { Up, Down } direction = Down;
267 if ( nAboveMax != 0) {
268 if ( nAboveMax > nBelowMax ) {
269 // Move up to capture more active patterns
270 direction = Up;
271 } else if ( nBelowMax > nAboveMax ) {
272 // Move down to capture more active patterns
273 direction = Down;
274 } else {
275 // Tie-breaker. Which is closer?
276 assert( nAboveY <= nScroll && nScroll <= nBelowY );
277 if ( nScroll - nAboveY < nBelowY - nScroll ) {
278 direction = Up;
279 } else {
280 direction = Down;
281 }
282 }
283 } else {
284 assert( nBelowMax != 0 );
285 // Move down
286 direction = Down;
287 }
288
289 if ( direction == Up ) {
290 if ( pnPatternInView ) {
291 *pnPatternInView = nAboveClosestPattern;
292 }
293 return nAboveY;
294 } else {
295 if ( pnPatternInView ) {
296 *pnPatternInView = nBelowClosestPattern;
297 }
298 return nBelowY;
299 }
300}
301
302
304{
305 return m_nGridWidth;
306}
307
308
309
310void SongEditor::setGridWidth( uint width )
311{
312 if ( ( SONG_EDITOR_MIN_GRID_WIDTH <= width ) && ( SONG_EDITOR_MAX_GRID_WIDTH >= width ) ) {
313 m_nGridWidth = width;
314 resize( SongEditor::nMargin +
315 Preferences::get_instance()->getMaxBars() * m_nGridWidth, height() );
317 update();
318 }
319}
320
322{
323 return QPoint( (p.x() - SongEditor::nMargin) / (int)m_nGridWidth, p.y() / (int)m_nGridHeight );
324}
325
327{
328 return QPoint( SongEditor::nMargin + p.x() * m_nGridWidth, p.y() * m_nGridHeight );
329}
330
331
332void SongEditor::togglePatternActive( int nColumn, int nRow ) {
333 SE_togglePatternAction *action = new SE_togglePatternAction( nColumn, nRow );
334 HydrogenApp::get_instance()->m_pUndoStack->push( action );
335}
336
337void SongEditor::setPatternActive( int nColumn, int nRow, bool bActivate )
338{
340 std::shared_ptr<Song> pSong = m_pHydrogen->getSong();
341 bool bPatternIsActive = pSong->isPatternActive( nColumn, nRow );
342
343 if ( bPatternIsActive && ! bActivate || ! bPatternIsActive && bActivate ) {
344 h2app->m_pUndoStack->push( new SE_togglePatternAction( nColumn, nRow ) );
345 }
346}
347
348
350 PatternList *pPatternList = m_pHydrogen->getSong()->getPatternList();
351 std::vector<PatternList*>* pColumns = m_pHydrogen->getSong()->getPatternGroupVector();
352 m_selection.clearSelection();
353 for ( int nRow = 0; nRow < pPatternList->size(); nRow++ ) {
354 H2Core::Pattern *pPattern = pPatternList->get( nRow );
355 for ( int nCol = 0; nCol < pColumns->size(); nCol++ ) {
356 PatternList *pColumn = ( *pColumns )[ nCol ];
357 for ( uint i = 0; i < pColumn->size(); i++) {
358 if ( pColumn->get(i) == pPattern ) { // esiste un pattern in questa posizione
359 m_selection.addToSelection( QPoint( nCol, nRow ) );
360 }
361 }
362 }
363 }
364 m_bSequenceChanged = true;
365 update();
366}
367
369 m_selection.clearSelection();
370
371 m_bSequenceChanged = true;
372 update();
373}
374
376 QUndoStack *pUndo = HydrogenApp::get_instance()->m_pUndoStack;
377 std::vector< QPoint > addCells, deleteCells, mergeCells;
378 for ( QPoint cell : m_selection ) {
379 deleteCells.push_back( cell );
380 }
381 pUndo->push( new SE_modifyPatternCellsAction( addCells, deleteCells, mergeCells,
382 tr( "Delete selected cells" ) ) );
383 m_selection.clearSelection();
384}
385
386
395 XMLDoc doc;
396 XMLNode selection = doc.set_root( "patternSelection" );
397 XMLNode cellList = selection.createNode( "cellList" );
398 XMLNode positionNode = selection.createNode( "sourcePosition" );
399 // Top left of selection
400 int nMinX, nMinY;
401 bool bWrotePattern = false;
402
403 for ( QPoint cell : m_selection ) {
404 XMLNode cellNode = cellList.createNode( "cell" );
405 cellNode.write_int( "x", cell.x() );
406 cellNode.write_int( "y", cell.y() );
407 if ( bWrotePattern ) {
408 nMinX = std::min( nMinX, cell.x() );
409 nMinY = std::min( nMinY, cell.y() );
410 } else {
411 nMinX = cell.x();
412 nMinY = cell.y();
413 bWrotePattern = true;
414 }
415 }
416 if ( !bWrotePattern) {
417 nMinX = m_nCursorColumn;
418 nMinY = m_nCursorRow;
419 }
420 positionNode.write_int( "column", nMinX );
421 positionNode.write_int( "row", nMinY );
422
423 QApplication::clipboard()->setText( doc.toString() );
424
425 // Show the keyboard cursor so the user knows where the insertion point will be
427}
428
430 QUndoStack *pUndo = HydrogenApp::get_instance()->m_pUndoStack;
431 int nDeltaColumn = 0, nDeltaRow = 0;
432 std::shared_ptr<Song> pSong = Hydrogen::get_instance()->getSong();
433 int nPatterns = pSong->getPatternList()->size();
434
435 XMLDoc doc;
436 if ( ! doc.setContent( QApplication::clipboard()->text() ) ) {
437 // Pasted something that's not valid XML.
438 return;
439 }
440
441 m_selection.clearSelection();
443
444 XMLNode selection = doc.firstChildElement( "patternSelection" );
445 if ( ! selection.isNull() ) {
446 // Got pattern selection.
447 std::vector< QPoint > addCells, deleteCells, mergeCells;
448
449 XMLNode cellList = selection.firstChildElement( "cellList" );
450 if ( cellList.isNull() ) {
451 return;
452 }
453
454 XMLNode positionNode = selection.firstChildElement( "sourcePosition" );
455
456 // If position information is supplied in the selection, use
457 // it to adjust the location relative to the current keyboard
458 // input cursor.
459 if ( !positionNode.isNull() ) {
460
461 nDeltaColumn = m_nCursorColumn - positionNode.read_int( "column", m_nCursorColumn );
462 nDeltaRow = m_nCursorRow - positionNode.read_int( "row", m_nCursorRow );
463 }
464
465 if ( cellList.hasChildNodes() ) {
466 for ( XMLNode cellNode = cellList.firstChildElement( "cell" );
467 ! cellNode.isNull();
468 cellNode = cellNode.nextSiblingElement() ) {
469 int nCol = cellNode.read_int( "x", m_nCursorColumn ) + nDeltaColumn;
470 int nRow = cellNode.read_int( "y", m_nCursorRow ) + nDeltaRow;
471 if ( nCol >= 0 && nRow >= 0 && nRow < nPatterns ) {
472 // Paste cells
473 QPoint p = QPoint( nCol, nRow );
474 if ( m_gridCells.find( p ) == m_gridCells.end() ) {
475 // Cell is not active. Activate it.
476 addCells.push_back( p );
477 } else {
478 // Merge cell with existing
479 mergeCells.push_back( p );
480 }
481 }
482 }
483
484 pUndo->push( new SE_modifyPatternCellsAction( addCells, deleteCells, mergeCells,
485 tr( "Paste cells" ) ) );
486 }
487 }
488}
489
491 copy();
493}
494
495void SongEditor::keyPressEvent( QKeyEvent * ev )
496{
497 auto pHydrogenApp = HydrogenApp::get_instance();
498 auto pHydrogen = Hydrogen::get_instance();
499 auto pSong = pHydrogen->getSong();
500 const int nBlockSize = 5, nWordSize = 5;
501
502 bool bIsSelectionKey = false;
503 bool bUnhideCursor = true;
504
505 bool bOldCursorHidden = pHydrogenApp->hideKeyboardCursor();
506
507 H2Core::Song::ActionMode actionMode = pHydrogen->getActionMode();
508
509 if ( actionMode == H2Core::Song::ActionMode::selectMode ) {
510 bIsSelectionKey = m_selection.keyPressEvent( ev );
511 }
512
513 PatternList *pPatternList = pSong->getPatternList();
514 const QPoint centre = QPoint( m_nGridWidth / 2, m_nGridHeight / 2 );
515 bool bSelectionKey = false;
516
517 int nMaxPatternSequence = Preferences::get_instance()->getMaxBars();
518
519 updateModifiers( ev );
520
521 if ( bIsSelectionKey ) {
522 // Key was claimed by selection
523 } else if ( ev->key() == Qt::Key_Delete ) {
524 // Key: Delete: delete selected pattern cells, or cell at current position
525 if ( m_selection.begin() != m_selection.end() ) {
527 } else {
528 // No selection, delete at the current cursor position
530 }
531
532 } else if ( ev->matches( QKeySequence::MoveToNextChar ) || ( bSelectionKey = ev->matches( QKeySequence::SelectNextChar ) ) ) {
533 // ->
534 if ( m_nCursorColumn < nMaxPatternSequence -1 ) {
535 m_nCursorColumn += 1;
536 }
537
538 } else if ( ev->matches( QKeySequence::MoveToNextWord ) || ( bSelectionKey = ev->matches( QKeySequence::SelectNextWord ) ) ) {
539 // -->
540 m_nCursorColumn = std::min( (int)nMaxPatternSequence, m_nCursorColumn + nWordSize );
541
542 } else if ( ev->matches( QKeySequence::MoveToEndOfLine ) || ( bSelectionKey = ev->matches( QKeySequence::SelectEndOfLine ) ) ) {
543 // ->|
544 m_nCursorColumn = nMaxPatternSequence -1;
545
546 } else if ( ev->matches( QKeySequence::MoveToPreviousChar ) || ( bSelectionKey = ev->matches( QKeySequence::SelectPreviousChar ) ) ) {
547 // <-
548 if ( m_nCursorColumn > 0 ) {
549 m_nCursorColumn -= 1;
550 }
551
552 } else if ( ev->matches( QKeySequence::MoveToPreviousWord ) || ( bSelectionKey = ev->matches( QKeySequence::SelectPreviousWord ) ) ) {
553 // <--
554 m_nCursorColumn = std::max( 0, m_nCursorColumn - nWordSize );
555
556 } else if ( ev->matches( QKeySequence::MoveToStartOfLine ) || ( bSelectionKey = ev->matches( QKeySequence::SelectStartOfLine ) ) ) {
557 // |<-
558 m_nCursorColumn = 0;
559
560 } else if ( ev->matches( QKeySequence::MoveToNextLine ) || ( bSelectionKey = ev->matches( QKeySequence::SelectNextLine ) ) ) {
561 if ( m_nCursorRow < pPatternList->size()-1 ) {
562 m_nCursorRow += 1;
563 }
564
565 } else if ( ev->matches( QKeySequence::MoveToEndOfBlock ) || ( bSelectionKey = ev->matches( QKeySequence::SelectEndOfBlock ) ) ) {
566 m_nCursorRow = std::min( pPatternList->size()-1, m_nCursorRow + nBlockSize );
567
568 } else if ( ev->matches( QKeySequence::MoveToNextPage ) || ( bSelectionKey = ev->matches( QKeySequence::SelectNextPage ) ) ) {
569 // Page down, scroll by the number of patterns that fit into the viewport
570 QWidget *pParent = dynamic_cast< QWidget *>( parent() );
571 assert( pParent );
572 m_nCursorRow += pParent->height() / m_nGridHeight;
573
574 if ( m_nCursorRow >= pPatternList->size() ) {
575 m_nCursorRow = pPatternList->size()-1;
576 }
577
578 } else if ( ev->matches( QKeySequence::MoveToEndOfDocument ) || ( bSelectionKey = ev->matches( QKeySequence::SelectEndOfDocument ) ) ) {
579 m_nCursorRow = pPatternList->size() -1;
580
581 } else if ( ev->matches( QKeySequence::MoveToPreviousLine ) || ( bSelectionKey = ev->matches( QKeySequence::SelectPreviousLine ) ) ) {
582 if ( m_nCursorRow > 0 ) {
583 m_nCursorRow -= 1;
584 }
585
586 } else if ( ev->matches( QKeySequence::MoveToStartOfBlock ) || ( bSelectionKey = ev->matches( QKeySequence::SelectStartOfBlock ) ) ) {
587 m_nCursorRow = std::max( 0, m_nCursorRow - nBlockSize );
588
589
590 } else if ( ev->matches( QKeySequence::MoveToPreviousPage ) || ( bSelectionKey = ev->matches( QKeySequence::SelectPreviousPage ) ) ) {
591 QWidget *pParent = dynamic_cast< QWidget *>( parent() );
592 assert( pParent );
593 m_nCursorRow -= pParent->height() / m_nGridHeight;
594
595 if ( m_nCursorRow < 0 ) {
596 m_nCursorRow = 0;
597 }
598
599 } else if ( ev->matches( QKeySequence::MoveToStartOfDocument ) || ( bSelectionKey = ev->matches( QKeySequence::SelectStartOfDocument ) ) ) {
600 m_nCursorRow = 0;
601
602 } else if ( ev->matches( QKeySequence::SelectAll ) ) {
603 // Key: Ctrl + A: Select all pattern
604 bSelectionKey = true;
605 bUnhideCursor = false;
606 if ( actionMode == H2Core::Song::ActionMode::selectMode ) {
607 selectAll();
608 }
609
610 } else if ( ev->matches( QKeySequence::Deselect ) ) {
611 // Key: Shift + Ctrl + A: deselect any selected cells
612 bSelectionKey = true;
613 bUnhideCursor = false;
614 if ( actionMode == H2Core::Song::ActionMode::selectMode ) {
615 selectNone();
616 m_bSequenceChanged = false;
617 }
618
619 } else if ( ev->matches( QKeySequence::Copy ) ) {
620 bUnhideCursor = false;
621 copy();
622
623 } else if ( ev->matches( QKeySequence::Paste ) ) {
624 bUnhideCursor = false;
625 paste();
626 } else if ( ev->matches( QKeySequence::Cut ) ) {
627 bUnhideCursor = false;
628 cut();
629
630 } else if ( ev->key() == Qt::Key_Enter || ev->key() == Qt::Key_Return ) {
631 // Key: Return: Set or clear cell (draw mode), or start/end selection or move (select mode)
632
633 // In DRAW mode, Enter's obvious action is the same as a
634 // click - insert or delete pattern.
636
637 } else {
638 ev->ignore();
639 pHydrogenApp->setHideKeyboardCursor( true );
640
641 if ( bOldCursorHidden != pHydrogenApp->hideKeyboardCursor() ) {
642 pHydrogenApp->getSongEditorPanel()->getSongEditorPatternList()->update();
643 pHydrogenApp->getSongEditorPanel()->getSongEditorPositionRuler()->update();
644 update();
645 }
646 return;
647 }
648 if ( bUnhideCursor ) {
649 pHydrogenApp->setHideKeyboardCursor( false );
650 }
651
652 if ( bSelectionKey ) {
653 // If a "select" key movement is used in "draw" mode, it's probably a good idea to go straight into
654 // "select" mode.
655 if ( actionMode == H2Core::Song::ActionMode::drawMode ) {
656 pHydrogen->setActionMode( H2Core::Song::ActionMode::selectMode );
657 }
658 // Any selection key may need a repaint of the selection
659 m_bSequenceChanged = true;
660 }
661 if ( m_selection.isMoving() ) {
662 // If a selection is being moved, it will need to be repainted
663 m_bSequenceChanged = true;
664 }
665
666 QPoint cursorCentre = columnRowToXy( QPoint( m_nCursorColumn, m_nCursorRow ) ) + centre;
667 m_pScrollView->ensureVisible( cursorCentre.x(), cursorCentre.y() );
668 m_selection.updateKeyboardCursorPosition( getKeyboardCursorRect() );
669
670 if ( ! pHydrogenApp->hideKeyboardCursor() ) {
671 pHydrogenApp->getSongEditorPanel()->getSongEditorPatternList()->update();
672 pHydrogenApp->getSongEditorPanel()->getSongEditorPositionRuler()->update();
673 }
674 update();
675 ev->accept();
676}
677
678void SongEditor::keyReleaseEvent( QKeyEvent * ev ) {
679 updateModifiers( ev );
680}
681
682// Make cursor visible on focus
683void SongEditor::focusInEvent( QFocusEvent *ev )
684{
685 if ( ev->reason() == Qt::TabFocusReason || ev->reason() == Qt::BacktabFocusReason ) {
686 QPoint pos = columnRowToXy( QPoint( m_nCursorColumn, m_nCursorRow ))
687 + QPoint( m_nGridWidth / 2, m_nGridHeight / 2 );
688 m_pScrollView->ensureVisible( pos.x(), pos.y() );
690 }
691
692 // If there are some patterns selected, we have to switch their
693 // border color inactive <-> active.
695 update();
696
697 if ( ! HydrogenApp::get_instance()->hideKeyboardCursor() ) {
700 }
701}
702
703// Make cursor hidden
704void SongEditor::focusOutEvent( QFocusEvent *ev )
705{
706 UNUSED( ev );
707
708 // If there are some patterns selected, we have to switch their
709 // border color inactive <-> active.
711 update();
712
713 if ( ! HydrogenApp::get_instance()->hideKeyboardCursor() ) {
716 }
717}
718
719
720// Implement comparison between QPoints needed for std::set
721int operator<( QPoint a, QPoint b ) {
722 int nAx = a.x(), nBx = b.x();
723 if ( nAx != nBx ) {
724 return nAx < nBx;
725 } else {
726 int nAy = a.y(), nBy = b.y();
727 return nAy < nBy;
728 }
729}
730
731void SongEditor::mousePressEvent( QMouseEvent *ev )
732{
733 auto pEv = static_cast<MouseEvent*>( ev );
734
735 auto pHydrogenApp = HydrogenApp::get_instance();
736 updateModifiers( ev );
737 m_currentMousePosition = pEv->position().toPoint();
738 m_bSequenceChanged = true;
739
740 // Update keyboard cursor position
741 QPoint p = xyToColumnRow( pEv->position().toPoint() );
742 m_nCursorColumn = p.x();
743 m_nCursorRow = p.y();
744
745 bool bOldCursorHidden = pHydrogenApp->hideKeyboardCursor();
746
747 pHydrogenApp->setHideKeyboardCursor( true );
748
750 m_selection.mousePressEvent( ev );
751 if ( ! pHydrogenApp->hideKeyboardCursor() ) {
752 pHydrogenApp->getSongEditorPanel()->getSongEditorPatternList()->update();
753 pHydrogenApp->getSongEditorPanel()->getSongEditorPositionRuler()->update();
754 update();
755 }
756
757 } else {
758 if ( ev->button() == Qt::LeftButton ) {
759 // Start of a drawing gesture. Pick up whether we are painting Active or Inactive cells.
760 QPoint p = xyToColumnRow( pEv->position().toPoint() );
761 m_bDrawingActiveCell = Hydrogen::get_instance()->getSong()->isPatternActive( p.x(), p.y() );
762 setPatternActive( p.x(), p.y(), ! m_bDrawingActiveCell );
763 m_pSongEditorPanel->updatePlaybackTrackIfNecessary();
764
765 } else if ( ev->button() == Qt::RightButton ) {
766 m_pPopupMenu->popup( pEv->globalPosition().toPoint() );
767 }
768 }
769
770 // Cursor just got hidden.
771 if ( bOldCursorHidden != pHydrogenApp->hideKeyboardCursor() ) {
772 pHydrogenApp->getSongEditorPanel()->getSongEditorPatternList()->update();
773 pHydrogenApp->getSongEditorPanel()->getSongEditorPositionRuler()->update();
774 update();
775 }
776}
777
778
779void SongEditor::updateModifiers( QInputEvent *ev )
780{
781 if ( ev->modifiers() == Qt::ControlModifier ) {
782 m_bCopyNotMove = true;
783 } else {
784 m_bCopyNotMove = false;
785 }
786
787 if ( QKeyEvent *pEv = dynamic_cast<QKeyEvent*>( ev ) ) {
788 // Keyboard events for press and release of modifier keys don't have those keys in the modifiers set,
789 // so explicitly update these.
790 if ( pEv->key() == Qt::Key_Control ) {
791 m_bCopyNotMove = ( ev->type() == QEvent::KeyPress );
792 }
793 }
794
795 if ( m_selection.isMouseGesture() && m_selection.isMoving() ) {
796 // If a selection is currently being moved, change the cursor
797 // appropriately. Selection will change it back after the move
798 // is complete (or abandoned)
799 if ( m_bCopyNotMove && cursor().shape() != Qt::DragCopyCursor ) {
800 setCursor( QCursor( Qt::DragCopyCursor ) );
801 } else if ( !m_bCopyNotMove && cursor().shape() != Qt::DragMoveCursor ) {
802 setCursor( QCursor( Qt::DragMoveCursor ) );
803 }
804 }
805
806}
807
808void SongEditor::mouseMoveEvent(QMouseEvent *ev)
809{
810 auto pEv = static_cast<MouseEvent*>( ev );
811
812 auto pHydrogenApp = HydrogenApp::get_instance();
813 auto pSong = Hydrogen::get_instance()->getSong();
814 updateModifiers( ev );
815 m_currentMousePosition = pEv->position().toPoint();
816 bool bOldCursorHidden = pHydrogenApp->hideKeyboardCursor();
817
819 m_selection.mouseMoveEvent( ev );
820 } else {
821 if ( pEv->position().x() < SongEditor::nMargin ) {
822 return;
823 }
824
825 QPoint p = xyToColumnRow( pEv->position().toPoint() );
826 if ( m_nCursorColumn == p.x() && m_nCursorRow == p.y() ) {
827 // Cursor has not entered a different cell yet.
828 return;
829 }
830 m_nCursorColumn = p.x();
831 m_nCursorRow = p.y();
833
834 if ( m_nCursorRow >= pSong->getPatternList()->size() ) {
835 // We are below the bottom of the pattern list.
836 return;
837 }
838
839 // Drawing mode: continue drawing over other cells
840 setPatternActive( p.x(), p.y(), ! m_bDrawingActiveCell );
841 }
842
843 // Cursor just got hidden.
844 if ( bOldCursorHidden != pHydrogenApp->hideKeyboardCursor() ) {
845 pHydrogenApp->getSongEditorPanel()->getSongEditorPatternList()->update();
846 pHydrogenApp->getSongEditorPanel()->getSongEditorPositionRuler()->update();
847 update();
848 }
849}
850
851void SongEditor::mouseDragStartEvent( QMouseEvent *ev )
852{
853}
854
855void SongEditor::mouseDragUpdateEvent( QMouseEvent *ev )
856{
857}
858
859void SongEditor::mouseDragEndEvent( QMouseEvent *ev )
860{
861 unsetCursor();
862}
863
865{
867 Hydrogen *pHydrogen = Hydrogen::get_instance();
868 std::shared_ptr<Song> pSong = pHydrogen->getSong();
869 PatternList *pPatternList = pSong->getPatternList();
870 int nMaxPattern = pPatternList->size();
871
872 updateModifiers( ev );
873 QPoint offset = movingGridOffset();
874 if ( offset == QPoint( 0, 0 ) ) {
875 return;
876 }
877 std::vector< QPoint > addCells, deleteCells, mergeCells;
878
880
881 for ( QPoint cell : m_selection ) {
882 // Remove original active cell
883 if ( ! m_bCopyNotMove ) {
884 deleteCells.push_back( cell );
885 }
886 QPoint newCell = cell + offset;
887 // Place new cell if not already active
888 if ( newCell.x() >= 0 && newCell.y() >= 0 && newCell.y() < nMaxPattern ) {
889 if ( m_gridCells.find( newCell ) == m_gridCells.end() || m_selection.isSelected( newCell ) ) {
890 addCells.push_back( newCell );
891 } else {
892 // Cell is moved, but merges with existing cell
893 mergeCells.push_back( newCell );
894 }
895 }
896 }
897
898 pApp->m_pUndoStack->push( new SE_modifyPatternCellsAction( addCells, deleteCells, mergeCells,
900 ? tr( "Copy selected cells" )
901 : tr( "Move selected cells" ) ) ) );
902}
903
904
905void SongEditor::mouseClickEvent( QMouseEvent *ev )
906{
907 assert( m_pHydrogen->getActionMode() == H2Core::Song::ActionMode::selectMode );
908
909 auto pEv = static_cast<MouseEvent*>( ev );
910
911 if ( ev->button() == Qt::LeftButton ) {
912 QPoint p = xyToColumnRow( pEv->position().toPoint() );
913
914 m_selection.clearSelection();
915 togglePatternActive( p.x(), p.y() );
916 m_bSequenceChanged = true;
917 update();
918 if ( ! HydrogenApp::get_instance()->hideKeyboardCursor() ) {
921 }
922
923 } else if ( ev->button() == Qt::RightButton ) {
924 m_pPopupMenu->popup( pEv->globalPosition().toPoint() );
925 }
926}
927
928void SongEditor::mouseReleaseEvent( QMouseEvent *ev )
929{
930 if ( m_pHydrogen->getActionMode() == H2Core::Song::ActionMode::selectMode ) {
931 m_selection.mouseReleaseEvent( ev );
932 return;
933 }
934}
935
936
939void SongEditor::modifyPatternCellsAction( std::vector<QPoint> & addCells, std::vector<QPoint> & deleteCells, std::vector<QPoint> & selectCells ) {
940
941 for ( QPoint cell : deleteCells ) {
942 setPatternActive( cell.x(), cell.y(), false );
943 }
944
945 m_selection.clearSelection();
946 for ( QPoint cell : addCells ) {
947 setPatternActive( cell.x(), cell.y(), true );
948 m_selection.addToSelection( cell );
949 }
950 // Select additional cells (probably merged cells on redo)
951 for ( QPoint cell : selectCells ) {
952 m_selection.addToSelection( cell );
953 }
954}
955
957 // Only update the drawn sequence if necessary. This is only possible when the c
958 if ( m_selection.isMoving() ) {
959 QPoint currentGridOffset = movingGridOffset();
960 // Moving a selection never has to update the sequence (it's drawn on top of the sequence). Update
961 // is only ever needed when the move delta (in grid spaces) changes
962 if ( m_previousGridOffset != currentGridOffset ) {
963 update();
964 m_previousGridOffset = currentGridOffset;
965 }
966 } else if ( m_selection.isLasso() ) {
967 bool bCellBoundaryCrossed = xyToColumnRow( m_previousMousePosition ) !=
969 // Selection must redraw the pattern when a cell boundary is crossed, as the selected cells are
970 // drawn when drawing the pattern.
971 if ( bCellBoundaryCrossed ) {
972 m_bSequenceChanged = true;
973 }
974 update();
975 } else {
976 // Other reasons: force update
977 m_bSequenceChanged = true;
978 update();
979 }
981}
982
983
984void SongEditor::updatePosition( float fTick ) {
985 if ( fTick != m_fTick ) {
986 float fDiff = static_cast<float>(m_nGridWidth) * (fTick - m_fTick);
987 m_fTick = fTick;
989 int nOffset = Skin::getPlayheadShaftOffset();
990 QRect updateRect( nX + nOffset -2, 0, 4, height() );
991 update( updateRect );
992 if ( fDiff > 1.0 || fDiff < -1.0 ) {
993 // New cursor is far enough away from the old one that the single update rect won't cover both. So
994 // update at the old location as well.
995 updateRect.translate( -fDiff, 0 );
996 update( updateRect );
997 }
998 }
999}
1000
1002 // This can change the length of one pattern in a column
1003 // containing multiple ones.
1005 update();
1006}
1007
1009 if ( Hydrogen::get_instance()->isPatternEditorLocked() ) {
1011 update();
1012 }
1013}
1014
1016 if ( Hydrogen::get_instance()->isPatternEditorLocked() ) {
1018 update();
1019 }
1020}
1021
1022void SongEditor::paintEvent( QPaintEvent *ev )
1023{
1024 if ( m_bBackgroundInvalid ) {
1026 }
1027
1028 // ridisegno tutto solo se sono cambiate le note
1029 if (m_bSequenceChanged) {
1030 m_bSequenceChanged = false;
1031 drawSequence();
1032 }
1033
1034 auto pPref = Preferences::get_instance();
1035
1036 QPainter painter(this);
1037 painter.drawPixmap( ev->rect(), *m_pSequencePixmap, ev->rect() );
1038
1039 // Draw moving selected cells
1040 QColor patternColor( 0, 0, 0 );
1041 if ( m_selection.isMoving() ) {
1042 QPoint offset = movingGridOffset();
1043 for ( QPoint point : m_selection ) {
1044 int nWidth = m_gridCells[ point ].m_fWidth * m_nGridWidth;
1045 QRect r = QRect( columnRowToXy( point + offset ),
1046 QSize( nWidth, m_nGridHeight ) )
1047 .marginsRemoved( QMargins( 2, 4, 1 , 3 ) );
1048 painter.fillRect( r, patternColor );
1049 }
1050 }
1051 // Draw playhead
1052 if ( m_fTick != -1 ) {
1054 int nOffset = Skin::getPlayheadShaftOffset();
1055 Skin::setPlayheadPen( &painter, false );
1056 painter.drawLine( nX + nOffset, 0, nX + nOffset, height() );
1057 }
1058
1059 drawFocus( painter );
1060
1061 m_selection.paintSelection( &painter );
1062
1063 // Draw cursor
1064 if ( ! HydrogenApp::get_instance()->hideKeyboardCursor() && hasFocus() ) {
1065 QPen p( pPref->getColorTheme()->m_cursorColor );
1066 p.setWidth( 2 );
1067 painter.setPen( p );
1068 painter.setRenderHint( QPainter::Antialiasing );
1069 // Aim to leave a visible gap between the border of the
1070 // pattern cell, and the cursor line, for consistency and
1071 // visibility.
1072 painter.drawRoundedRect( QRect( QPoint(0, 1 ) + columnRowToXy( QPoint(m_nCursorColumn, m_nCursorRow ) ),
1073 QSize( m_nGridWidth+1, m_nGridHeight-1 ) ),
1074 4, 4 );
1075 }
1076}
1077
1078void SongEditor::drawFocus( QPainter& painter ) {
1079
1080 if ( ! m_bEntered && ! hasFocus() ) {
1081 return;
1082 }
1083
1084 QColor color = H2Core::Preferences::get_instance()->getColorTheme()->m_highlightColor;
1085
1086 // If the mouse is placed on the widget but the user hasn't
1087 // clicked it yet, the highlight will be done more transparent to
1088 // indicate that keyboard inputs are not accepted yet.
1089 if ( ! hasFocus() ) {
1090 color.setAlpha( 125 );
1091 }
1092
1093 int nStartX = m_pScrollView->horizontalScrollBar()->value();
1094 int nEndX = std::min( nStartX + m_pScrollView->viewport()->size().width(), width() );
1095 int nStartY = m_pScrollView->verticalScrollBar()->value();
1096 int nEndY = std::min( static_cast<int>( m_nGridHeight ) * m_pHydrogen->getSong()->getPatternList()->size(),
1097 nStartY + m_pScrollView->viewport()->size().height() );
1098
1099 QPen pen( color );
1100 pen.setWidth( 4 );
1101 painter.setPen( pen );
1102 painter.drawLine( QPoint( nStartX, nStartY ), QPoint( nEndX, nStartY ) );
1103 painter.drawLine( QPoint( nStartX, nStartY ), QPoint( nStartX, nEndY ) );
1104 painter.drawLine( QPoint( nEndX, nStartY ), QPoint( nEndX, nEndY ) );
1105 painter.drawLine( QPoint( nEndX, nEndY ), QPoint( nStartX, nEndY ) );
1106}
1107
1108void SongEditor::scrolled( int nValue ) {
1109 UNUSED( nValue );
1110 update();
1111}
1112
1113#ifdef H2CORE_HAVE_QT6
1114void SongEditor::enterEvent( QEnterEvent *ev ) {
1115#else
1116void SongEditor::enterEvent( QEvent *ev ) {
1117#endif
1118 UNUSED( ev );
1119 m_bEntered = true;
1120 update();
1121}
1122
1123void SongEditor::leaveEvent( QEvent *ev ) {
1124 UNUSED( ev );
1125 m_bEntered = false;
1126 update();
1127}
1128
1130{
1131 m_bBackgroundInvalid = false;
1132 auto pPref = H2Core::Preferences::get_instance();
1133 std::shared_ptr<Song> pSong = m_pHydrogen->getSong();
1134
1135 uint nPatterns = pSong->getPatternList()->size();
1136 int nSelectedPatternNumber = m_pHydrogen->getSelectedPatternNumber();
1137 int nMaxPatternSequence = pPref->getMaxBars();
1138
1139 static int nOldHeight = -1;
1140 int nNewHeight = m_nGridHeight * nPatterns;
1141
1142 if (nOldHeight != nNewHeight) {
1143 // cambiamento di dimensioni...
1144 if ( nNewHeight < m_nMinimumHeight ) {
1145 WARNINGLOG( QString( "nNewHeight [%1] below minimum one [%2]" )
1146 .arg( nNewHeight ).arg( m_nMinimumHeight ) );
1147 nNewHeight = m_nMinimumHeight; // the pixmap should not be empty
1148 }
1149 if ( m_pBackgroundPixmap ) {
1150 delete m_pBackgroundPixmap;
1151 }
1152 if ( m_pSequencePixmap ) {
1153 delete m_pSequencePixmap;
1154 }
1155 m_pBackgroundPixmap = new QPixmap( width(), nNewHeight ); // initialize the pixmap
1156 m_pSequencePixmap = new QPixmap( width(), nNewHeight ); // initialize the pixmap
1157 this->resize( QSize( width(), nNewHeight ) );
1158 }
1159
1160
1161 m_pBackgroundPixmap->fill( pPref->getColorTheme()->m_songEditor_backgroundColor );
1162
1163 QPainter p( m_pBackgroundPixmap );
1164
1165 for ( int ii = 0; ii < nPatterns + 1; ii++) {
1166 if ( ( ii % 2 ) == 0 &&
1167 ii != nSelectedPatternNumber ) {
1168 continue;
1169 }
1170
1171 int y = m_nGridHeight * ii;
1172
1173 if ( ii == nSelectedPatternNumber ) {
1174 p.fillRect( 0, y, nMaxPatternSequence * m_nGridWidth, m_nGridHeight,
1175 pPref->getColorTheme()->m_songEditor_selectedRowColor );
1176 } else {
1177 p.fillRect( 0, y, nMaxPatternSequence * m_nGridWidth, m_nGridHeight,
1178 pPref->getColorTheme()->m_songEditor_alternateRowColor );
1179 }
1180 }
1181
1182 p.setPen( QPen( pPref->getColorTheme()->m_songEditor_lineColor, 1,
1183 Qt::DotLine ) );
1184
1185 // vertical lines
1186 for ( float ii = 0; ii <= nMaxPatternSequence + 1; ii++) {
1187 float x = SongEditor::nMargin + ii * m_nGridWidth;
1188 p.drawLine( x, 0, x, m_nGridHeight * nPatterns );
1189 }
1190
1191 // horizontal lines
1192 for (uint i = 0; i < nPatterns; i++) {
1193 uint y = m_nGridHeight * i;
1194
1195 p.drawLine( 0, y, (nMaxPatternSequence * m_nGridWidth), y );
1196 }
1197
1198 // ~ celle
1199 m_bSequenceChanged = true;
1200
1201}
1202
1206
1207// Update the GridCell representation.
1209
1210 m_gridCells.clear();
1211 std::shared_ptr<Song> pSong = Hydrogen::get_instance()->getSong();
1212 PatternList *pPatternList = pSong->getPatternList();
1213 std::vector< PatternList* > *pColumns = pSong->getPatternGroupVector();
1214
1215 for ( int nColumn = 0; nColumn < pColumns->size(); nColumn++ ) {
1216 PatternList *pColumn = (*pColumns)[nColumn];
1217 int nMaxLength = pColumn->longest_pattern_length();
1218
1219 for ( uint nPat = 0; nPat < pColumn->size(); nPat++ ) {
1220 Pattern *pPattern = (*pColumn)[ nPat ];
1221 int y = pPatternList->index( pPattern );
1222 assert( y != -1 );
1223 GridCell *pCell = &( m_gridCells[ QPoint( nColumn, y ) ] );
1224 pCell->m_bActive = true;
1225 pCell->m_fWidth = (float) pPattern->get_length() / nMaxLength;
1226
1227 for ( Pattern *pVPattern : *( pPattern->get_flattened_virtual_patterns() ) ) {
1228 GridCell *pVCell = &( m_gridCells[ QPoint( nColumn, pPatternList->index( pVPattern ) ) ] );
1229 pVCell->m_bDrawnVirtual = true;
1230 pVCell->m_fWidth = (float) pVPattern->get_length() / nMaxLength;
1231 }
1232 }
1233 }
1234}
1235
1236// Return grid offset (in cell coordinate space) of moving selection
1238 QPoint rawOffset = m_selection.movingOffset();
1239 // Quantize offset to multiples of m_nGrid{Width,Height}
1240 int x_bias = m_nGridWidth / 2, y_bias = m_nGridHeight / 2;
1241 if ( rawOffset.y() < 0 ) {
1242 y_bias = -y_bias;
1243 }
1244 if ( rawOffset.x() < 0 ) {
1245 x_bias = -x_bias;
1246 }
1247 int x_off = (rawOffset.x() + x_bias) / (int)m_nGridWidth;
1248 int y_off = (rawOffset.y() + y_bias) / (int)m_nGridHeight;
1249 return QPoint( x_off, y_off );
1250}
1251
1252
1254{
1255 QPainter p;
1256
1257 p.begin( m_pSequencePixmap );
1258 p.drawPixmap( rect(), *m_pBackgroundPixmap, rect() );
1259 p.end();
1260
1262
1263 // Draw using GridCells representation
1264 for ( auto it : m_gridCells ) {
1265 if ( ! m_selection.isSelected( QPoint( it.first.x(), it.first.y() ) ) ) {
1266 drawPattern( it.first.x(), it.first.y(),
1267 it.second.m_bDrawnVirtual, it.second.m_fWidth );
1268 }
1269 }
1270 // We draw all selected patterns in a second run to ensure their
1271 // border does have the proper color (else the bottom and left one
1272 // could be overwritten by an adjecent, unselected pattern).
1273 for ( auto it : m_gridCells ) {
1274 if ( m_selection.isSelected( QPoint( it.first.x(), it.first.y() ) ) ) {
1275 drawPattern( it.first.x(), it.first.y(),
1276 it.second.m_bDrawnVirtual, it.second.m_fWidth );
1277 }
1278 }
1279}
1280
1281
1282
1283void SongEditor::drawPattern( int nPos, int nNumber, bool bInvertColour, double fWidth )
1284{
1285 QPainter p( m_pSequencePixmap );
1286 /*
1287 * The default color of the cubes in rgb is 97,167,251.
1288 */
1289 auto pPref = H2Core::Preferences::get_instance();
1290 std::shared_ptr<Song> pSong = Hydrogen::get_instance()->getSong();
1291 PatternList *pPatternList = pSong->getPatternList();
1292
1293 QColor patternColor;
1294 /*
1295 * The following color modes are available:
1296 *
1297 * Automatic: Steps = Number of pattern in song and colors will be
1298 * chosen internally.
1299 * Custom: Number of steps as well as the colors used are defined
1300 * by the user.
1301 */
1302 if ( pPref->getColoringMethod() == H2Core::InterfaceTheme::ColoringMethod::Automatic ) {
1303 int nSteps = pPatternList->size();
1304
1305 if( nSteps == 0 ) {
1306 //beware of the division by zero..
1307 nSteps = 1;
1308 }
1309
1310 int nHue = ( (nNumber % nSteps) * (300 / nSteps) + 213) % 300;
1311 patternColor.setHsv( nHue , 156 , 249);
1312 } else {
1313 int nIndex = nNumber % pPref->getVisiblePatternColors();
1314 if ( nIndex > m_nMaxPatternColors ) {
1315 nIndex = m_nMaxPatternColors;
1316 }
1317 patternColor = pPref->getPatternColors()[ nIndex ].toHsv();
1318 }
1319
1320 if ( true == bInvertColour ) {
1321 patternColor = patternColor.darker(200);
1322 }
1323
1324 bool bIsSelected = m_selection.isSelected( QPoint( nPos, nNumber ) );
1325
1326 if ( bIsSelected ) {
1327 patternColor = patternColor.darker( 130 );
1328 }
1329
1330 patternColor.setAlpha( 230 );
1331
1332 int x = SongEditor::nMargin + m_nGridWidth * nPos;
1333 int y = m_nGridHeight * nNumber;
1334
1335 p.fillRect( x + 1, y + 1, fWidth * (m_nGridWidth - 1), m_nGridHeight - 1, patternColor );
1336
1337 // To better distinguish between the individual patterns, they
1338 // will have a pronounced border.
1339 QColor borderColor;
1340 if ( bIsSelected ){
1341 if ( hasFocus() ) {
1342 borderColor = pPref->getColorTheme()->m_selectionHighlightColor;
1343 } else {
1344 borderColor = pPref->getColorTheme()->m_selectionInactiveColor;
1345 }
1346 } else {
1347 borderColor = QColor( 0, 0, 0 );
1348 }
1349 p.setPen( borderColor );
1350 p.drawRect( x, y, fWidth * m_nGridWidth, m_nGridHeight );
1351}
1352
1353std::vector<SongEditor::SelectionIndex> SongEditor::elementsIntersecting( QRect r )
1354{
1355 std::vector<SelectionIndex> elems;
1356 for ( auto it : m_gridCells ) {
1357 if ( r.intersects( QRect( columnRowToXy( it.first ),
1358 QSize( m_nGridWidth, m_nGridHeight) ) ) ) {
1359 if ( ! it.second.m_bDrawnVirtual ) {
1360 elems.push_back( it.first );
1361 }
1362 }
1363 }
1364 return elems;
1365}
1366
1368 return QRect( QPoint( 0, 1 ) + columnRowToXy( QPoint( m_nCursorColumn, m_nCursorRow ) ),
1369 QSize( m_nGridWidth, m_nGridHeight -1 ) );
1370}
1371
1373{
1374 Hydrogen *pHydrogen = Hydrogen::get_instance();
1375
1376 m_pAudioEngine->lock( RIGHT_HERE );
1377
1378 std::shared_ptr<Song> pSong = pHydrogen->getSong();
1379
1380 //before deleting the sequence, write a temp sequence file to disk
1381 pSong->writeTempPatternList( filename );
1382
1383 std::vector<PatternList*> *pPatternGroupsVect = pSong->getPatternGroupVector();
1384 for (uint i = 0; i < pPatternGroupsVect->size(); i++) {
1385 PatternList *pPatternList = (*pPatternGroupsVect)[i];
1386 pPatternList->clear();
1387 delete pPatternList;
1388 }
1389 pPatternGroupsVect->clear();
1390 pHydrogen->updateSongSize();
1391
1392 m_pAudioEngine->unlock();
1393
1394 pHydrogen->setIsModified( true );
1395 m_bSequenceChanged = true;
1396 update();
1397}
1398
1400{
1401 m_bSequenceChanged = true;
1402 update();
1403}
1404
1406{
1410 resize( SongEditor::nMargin +
1411 Preferences::get_instance()->getMaxBars() * m_nGridWidth, height() );
1412
1413 m_bSequenceChanged = true;
1414
1415 // Required to be called at least once in order to make the
1416 // scroll bars match the (potential) new width.
1418 }
1419}
1420
1421// :::::::::::::::::::
1422
1423
1425 : QWidget( parent )
1426 , EventListener()
1428 , m_pBackgroundPixmap( nullptr )
1429 , m_nRowHovered( -1 )
1430{
1432 m_pAudioEngine = m_pHydrogen->getAudioEngine();
1433
1434 auto pPref = Preferences::get_instance();
1435
1436 m_nWidth = 200;
1437 m_nGridHeight = pPref->getSongEditorGridHeight();
1438 setAttribute(Qt::WA_OpaquePaintEvent);
1439
1440 setAcceptDrops(true);
1441 setMouseTracking( true );
1442
1443 m_pPatternBeingEdited = nullptr;
1444
1445 m_pLineEdit = new QLineEdit( "Inline Pattern Name", this );
1446 m_pLineEdit->setFrame( false );
1447 m_pLineEdit->hide();
1448 m_pLineEdit->setAcceptDrops( false );
1449 connect( m_pLineEdit, SIGNAL(editingFinished()), this, SLOT(inlineEditingFinished()) );
1450 connect( m_pLineEdit, SIGNAL(returnPressed()), this, SLOT(inlineEditingEntered()) );
1451
1452 this->resize( m_nWidth, m_nInitialHeight );
1453
1454 m_playingPattern_on_Pixmap.load( Skin::getImagePath() + "/songEditor/playingPattern_on.png" );
1455 m_playingPattern_off_Pixmap.load( Skin::getImagePath() + "/songEditor/playingPattern_off.png" );
1456 m_playingPattern_empty_Pixmap.load( Skin::getImagePath() + "/songEditor/playingPattern_empty.png" );
1457
1458 m_pPatternPopup = new QMenu( this );
1459 m_pPatternPopup->addAction( tr("Duplicate"), this, SLOT( patternPopup_duplicate() ) );
1460 m_pPatternPopup->addAction( tr("Delete"), this, SLOT( patternPopup_delete() ) );
1461 m_pPatternPopup->addAction( tr("Fill/Clear..."), this, SLOT( patternPopup_fill() ) );
1462 m_pPatternPopup->addAction( tr("Properties"), this, SLOT( patternPopup_properties() ) );
1463 m_pPatternPopup->addAction( tr("Load Pattern"), this, SLOT( patternPopup_load() ) );
1464 m_pPatternPopup->addAction( tr("Save Pattern"), this, SLOT( patternPopup_save() ) );
1465 m_pPatternPopup->addAction( tr("Export Pattern"), this, SLOT( patternPopup_export() ) );
1466 m_pPatternPopup->addAction( tr("Virtual Pattern"), this, SLOT( patternPopup_virtualPattern() ) );
1467 m_pPatternPopup->setObjectName( "PatternListPopup" );
1468
1469 // Reset the clicked row once the popup is closed by clicking at
1470 // any position other than at an action of the popup.
1471 connect( m_pPatternPopup, &QMenu::aboutToHide, [=](){
1474 }
1475 });
1476
1478
1479 QScrollArea *pScrollArea = dynamic_cast< QScrollArea * >( parentWidget()->parentWidget() );
1480 assert( pScrollArea );
1481 m_pDragScroller = new DragScroller( pScrollArea );
1482
1483 m_pHighlightLockedTimer = new QTimer( this );
1484 m_pHighlightLockedTimer->setSingleShot( true );
1485 connect(m_pHighlightLockedTimer, &QTimer::timeout,
1487
1488 qreal pixelRatio = devicePixelRatio();
1489 m_pBackgroundPixmap = new QPixmap( m_nWidth * pixelRatio,
1490 height() * pixelRatio );
1491 m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio );
1492
1494 update();
1495}
1496
1497
1498
1506
1507
1512
1514 m_rowSelection = rowSelection;
1516 update();
1517}
1518
1523
1528
1533
1535 if ( Hydrogen::get_instance()->isPatternEditorLocked() ) {
1537 update();
1538 }
1539}
1540
1542 if ( Hydrogen::get_instance()->isPatternEditorLocked() ) {
1544 update();
1545 }
1546}
1547
1550{
1551 auto pEv = static_cast<MouseEvent*>( ev );
1552
1553 __drag_start_position = pEv->position().toPoint();
1554
1555 // -1 to compensate for the 1 pixel offset to align shadows and
1556 // -grid lines.
1557 int nRow = (( pEv->position().y() - 1 ) / m_nGridHeight);
1558
1559 auto pSong = m_pHydrogen->getSong();
1560 if ( pSong == nullptr ) {
1561 return;
1562 }
1563
1564 auto pPatternList = pSong->getPatternList();
1565
1566 if ( nRow < 0 || nRow >= (int)pPatternList->size() ) {
1567 ERRORLOG( QString( "Row [%1] out of bound" ).arg( nRow ) );
1568 return;
1569 }
1570
1571 if ( ( ev->button() == Qt::MiddleButton ||
1572 ( ev->modifiers() == Qt::ControlModifier && ev->button() == Qt::RightButton ) ||
1573 ( ev->modifiers() == Qt::ControlModifier && ev->button() == Qt::LeftButton ) ||
1574 pEv->position().x() < 15 ) &&
1575 m_pHydrogen->getPatternMode() == Song::PatternMode::Stacked ) {
1576
1577 m_pHydrogen->toggleNextPattern( nRow );
1578 }
1579 else {
1580 if ( ! ( m_pHydrogen->isPatternEditorLocked() &&
1581 m_pHydrogen->getAudioEngine()->getState() ==
1583 m_pHydrogen->setSelectedPatternNumber( nRow );
1584 } else {
1585 // Notify the users why nothing just happened by
1586 // highlighting the pattern locked button in the
1587 // SongEditorPanel.
1589 m_pHighlightLockedTimer->start( 250 );
1590 }
1591
1592 if (ev->button() == Qt::RightButton) {
1593
1595 // There is still a dialog window opened from the last
1596 // time. It needs to be closed before the popup will
1597 // be shown again.
1598 ERRORLOG( "A dialog is still opened. It needs to be closed first." );
1599 return;
1600 }
1601
1602 m_nRowClicked = nRow;
1604 m_pPatternPopup->popup( pEv->globalPosition().toPoint() );
1605 }
1606 }
1607
1609 update();
1610}
1611
1612
1617
1618 m_pHydrogen->toggleNextPattern( row );
1620 update();
1621}
1622
1623
1625{
1626 auto pEv = static_cast<MouseEvent*>( ev );
1627
1628 int row = (pEv->position().y() / m_nGridHeight);
1629 inlineEditPatternName( row );
1630}
1631
1633{
1634 std::shared_ptr<Song> pSong = m_pHydrogen->getSong();
1635 PatternList *pPatternList = pSong->getPatternList();
1636
1637 if ( row >= (int)pPatternList->size() ) {
1638 return;
1639 }
1640 m_pPatternBeingEdited = pPatternList->get( row );
1641 m_pLineEdit->setGeometry( 23, row * m_nGridHeight + 1 , m_nWidth - 23, m_nGridHeight );
1642 m_pLineEdit->setText( m_pPatternBeingEdited->get_name() );
1643 m_pLineEdit->selectAll();
1644 m_pLineEdit->show();
1645 m_pLineEdit->setFocus();
1646}
1647
1649{
1650 assert( m_pPatternBeingEdited != nullptr );
1651
1652 std::shared_ptr<Song> pSong = m_pHydrogen->getSong();
1653 PatternList *pPatternList = pSong->getPatternList();
1654
1655 /*
1656 * Make sure that the entered pattern name is unique.
1657 * If it is not, use an unused pattern name.
1658 */
1659
1660 QString patternName = pPatternList->find_unused_pattern_name( m_pLineEdit->text(), m_pPatternBeingEdited );
1661
1664 m_pPatternBeingEdited->get_info(),
1665 m_pPatternBeingEdited->get_category(),
1666 patternName,
1667 m_pPatternBeingEdited->get_info(),
1668 m_pPatternBeingEdited->get_category(),
1669 pPatternList->index( m_pPatternBeingEdited ) );
1670 HydrogenApp::get_instance()->m_pUndoStack->push( action );
1671}
1672
1673
1679
1680
1682{
1683 auto pPref = Preferences::get_instance();
1684 auto pHydrogenApp = HydrogenApp::get_instance();
1685 auto pSongEditor = pHydrogenApp->getSongEditorPanel()->getSongEditor();
1686
1687 QPainter painter(this);
1688 qreal pixelRatio = devicePixelRatio();
1689 if ( width() != m_pBackgroundPixmap->width() ||
1690 height() != m_pBackgroundPixmap->height() ||
1691 pixelRatio != m_pBackgroundPixmap->devicePixelRatio() ||
1694 }
1695 QRectF srcRect(
1696 pixelRatio * ev->rect().x(),
1697 pixelRatio * ev->rect().y(),
1698 pixelRatio * ev->rect().width(),
1699 pixelRatio * ev->rect().height()
1700 );
1701 painter.drawPixmap( ev->rect(), *m_pBackgroundPixmap, srcRect );
1702
1703 // In case a row was right-clicked or the cursor is positioned on
1704 // a grid cell within this row, highlight it using a border.
1705 if ( ( ! pHydrogenApp->hideKeyboardCursor() &&
1706 pSongEditor->hasFocus() ) ||
1708 QColor colorHighlight = pPref->getColorTheme()->m_highlightColor;
1709 QPen pen;
1710
1711 int nStartY;
1713 // In case a row was right-clicked, highlight it using a border.
1714 pen.setColor( pPref->getColorTheme()->m_highlightColor);
1715 nStartY = m_nRowClicked * m_nGridHeight;
1716 } else {
1717 pen.setColor( pPref->getColorTheme()->m_cursorColor );
1718 nStartY = pSongEditor->getCursorRow() * m_nGridHeight;
1719 }
1720 pen.setWidth( 2 );
1721 painter.setRenderHint( QPainter::Antialiasing );
1722
1723 painter.setPen( pen );
1724 painter.drawRoundedRect( QRect( 1, nStartY + 1, m_nWidth - 2,
1725 m_nGridHeight - 1 ), 4, 4 );
1726 }
1727}
1728
1729
1731{
1732 if(!isVisible()) {
1733 return;
1734 }
1735
1736 update();
1737}
1738
1740
1741 // Refresh pattern list display if in stacked mode
1742 if ( Hydrogen::get_instance()->getPatternMode() ==
1745 update();
1746 }
1747}
1748
1753
1755{
1756 auto pPref = H2Core::Preferences::get_instance();
1757 m_bBackgroundInvalid = false;
1758
1759 QFont boldTextFont( pPref->getLevel2FontFamily(), getPointSize( pPref->getFontSize() ) );
1760 boldTextFont.setBold( true );
1761
1762 //Do not redraw anything if Export is active.
1763 //https://github.com/hydrogen-music/hydrogen/issues/857
1764 if( m_pHydrogen->getIsExportSessionActive() ) {
1765 return;
1766 }
1767
1768 const std::shared_ptr<Song> pSong = m_pHydrogen->getSong();
1769 if ( pSong == nullptr ) {
1770 ERRORLOG( "no song set" );
1771 return;
1772 }
1773
1774 const auto pPatternList = pSong->getPatternList();
1775 int nPatterns = pPatternList->size();
1776 int nSelectedPattern = m_pHydrogen->getSelectedPatternNumber();
1777
1778 int newHeight = m_nGridHeight * nPatterns + 1;
1779
1780 if ( m_nWidth != m_pBackgroundPixmap->width() ||
1781 newHeight != m_pBackgroundPixmap->height() ||
1782 m_pBackgroundPixmap->devicePixelRatio() != devicePixelRatio() ) {
1783 if (newHeight == 0) {
1784 newHeight = 1; // the pixmap should not be empty
1785 }
1786 delete m_pBackgroundPixmap;
1787 qreal pixelRatio = devicePixelRatio();
1788 m_pBackgroundPixmap = new QPixmap( m_nWidth * pixelRatio , newHeight * pixelRatio ); // initialize the pixmap
1789 m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio );
1790 this->resize( m_nWidth, newHeight );
1791 }
1792
1793 QColor backgroundColor = pPref->getColorTheme()->m_songEditor_backgroundColor.darker( 120 );
1794 QColor backgroundColorSelected = pPref->getColorTheme()->m_songEditor_selectedRowColor.darker( 114 );
1795 QColor backgroundColorAlternate =
1796 pPref->getColorTheme()->m_songEditor_alternateRowColor.darker( 132 );
1797 QColor backgroundColorVirtual =
1798 pPref->getColorTheme()->m_songEditor_virtualRowColor;
1799
1800 QPainter p( m_pBackgroundPixmap );
1801
1802
1803 // Offset the pattern list by one pixel to align the dark shadows
1804 // at the bottom of each row with the grid lines in the song editor.
1805 p.fillRect( QRect( 0, 0, width(), 1 ), pPref->getColorTheme()->m_windowColor );
1806
1807 p.setFont( boldTextFont );
1808 for ( int ii = 0; ii < nPatterns; ii++ ) {
1809 uint y = m_nGridHeight * ii + 1;
1810
1811 if ( ii == nSelectedPattern ) {
1812 Skin::drawListBackground( &p, QRect( 0, y, width(), m_nGridHeight ),
1813 backgroundColorSelected, false );
1814 } else {
1815 const auto pPattern = pPatternList->get( ii );
1816 if ( pPattern != nullptr && pPattern->isVirtual() ) {
1817 Skin::drawListBackground( &p, QRect( 0, y, width(), m_nGridHeight ),
1818 backgroundColorVirtual,
1819 ii == m_nRowHovered );
1820 }
1821 else if ( ( ii % 2 ) == 0 ) {
1822 Skin::drawListBackground( &p, QRect( 0, y, width(), m_nGridHeight ),
1823 backgroundColor,
1824 ii == m_nRowHovered );
1825 }
1826 else {
1827 Skin::drawListBackground( &p, QRect( 0, y, width(), m_nGridHeight ),
1828 backgroundColorAlternate,
1829 ii == m_nRowHovered );
1830 }
1831 }
1832 }
1833
1834 std::unique_ptr<PatternDisplayInfo[]> PatternArray{new PatternDisplayInfo[nPatterns]};
1835
1836 m_pAudioEngine->lock( RIGHT_HERE );
1837 auto pPlayingPatterns = m_pAudioEngine->getPlayingPatterns();
1838
1839 //assemble the data..
1840 for ( int i = 0; i < nPatterns; i++ ) {
1841 H2Core::Pattern *pPattern = pSong->getPatternList()->get(i);
1842 if ( pPattern == nullptr ) {
1843 continue;
1844 }
1845
1846 if ( pPlayingPatterns->index( pPattern ) != -1 ) {
1847 PatternArray[i].bActive = true;
1848 } else {
1849 PatternArray[i].bActive = false;
1850 }
1851
1852 if ( m_pAudioEngine->getNextPatterns()->index( pPattern ) != -1 ) {
1853 PatternArray[i].bNext = true;
1854 } else {
1855 PatternArray[i].bNext = false;
1856 }
1857
1858 PatternArray[i].sPatternName = pPattern->get_name();
1859 }
1860 m_pAudioEngine->unlock();
1861
1863 for ( int i = 0; i < nPatterns; i++ ) {
1864 if ( i == nSelectedPattern ) {
1865 p.setPen( pPref->getColorTheme()->m_songEditor_selectedRowTextColor );
1866 }
1867 else {
1868 p.setPen( pPref->getColorTheme()->m_songEditor_textColor );
1869 }
1870
1871 uint text_y = i * m_nGridHeight;
1872
1873 p.drawText( 25, text_y - 1, m_nWidth - 25, m_nGridHeight + 2,
1874 Qt::AlignVCenter, PatternArray[i].sPatternName);
1875
1877 if ( PatternArray[i].bNext && PatternArray[i].bActive) {
1879 }
1880 else if ( PatternArray[i].bNext ) {
1881 mode = Skin::Stacked::OnNext;
1882 }
1883 else if (PatternArray[i].bActive) {
1884 mode = Skin::Stacked::On;
1885 }
1886 else if ( m_pHydrogen->getPatternMode() == Song::PatternMode::Stacked ) {
1887 mode = Skin::Stacked::Off;
1888 }
1889
1890 if ( mode != Skin::Stacked::None ) {
1891 Skin::drawStackedIndicator( &p, 5, text_y + 4, mode );
1892 }
1893
1894 }
1895}
1896
1901
1903{
1904 std::shared_ptr<Song> pSong = m_pHydrogen->getSong();
1905 if ( pSong == nullptr ) {
1906 ERRORLOG( "no song" );
1907 return;
1908 }
1909 PatternList *pPatternList = pSong->getPatternList();
1910 if ( pPatternList == nullptr ) {
1911 ERRORLOG( "no pattern list");
1912 return;
1913 }
1914
1916
1917 VirtualPatternDialog* pDialog = new VirtualPatternDialog( this );
1918 QListWidget* pPatternListWidget = pDialog->patternList;
1919 pPatternListWidget->setSortingEnabled(1);
1920
1921 auto pPatternClicked = pPatternList->get( m_nRowClicked );
1922
1923 std::map<QString, Pattern*> patternNameMap;
1924
1925 for ( const auto& pPattern : *pPatternList ) {
1926 QString sPatternName = pPattern->get_name();
1927
1928 if ( sPatternName == pPatternClicked->get_name() ) {
1929 // Current pattern. A virtual pattern must not contain itself.
1930 continue;
1931 }
1932
1933 patternNameMap[ sPatternName ] = pPattern;
1934
1935 QListWidgetItem* pNewItem =
1936 new QListWidgetItem( sPatternName, pPatternListWidget);
1937 pPatternListWidget->insertItem( 0, pNewItem );
1938
1939 if ( pPatternClicked->get_virtual_patterns()->find( pPattern ) !=
1940 pPatternClicked->get_virtual_patterns()->end() ) {
1941 // pattern is already contained in virtual pattern.
1942 pNewItem->setSelected( true );
1943 }
1944 }
1945
1946 if ( pDialog->exec() == QDialog::Accepted ) {
1947 pPatternClicked->virtual_patterns_clear();
1948 for ( int ii = 0; ii < pPatternListWidget->count(); ++ii ) {
1949 QListWidgetItem* pListItem = pPatternListWidget->item( ii );
1950 if ( pListItem != nullptr && pListItem->isSelected() ) {
1951 if ( patternNameMap.find( pListItem->text() ) !=
1952 patternNameMap.end() ) {
1953 pPatternClicked->virtual_patterns_add(
1954 patternNameMap[ pListItem->text() ] );
1955 }
1956 else {
1957 ERRORLOG( QString( "Selected pattern [%1] could not be retrieved" )
1958 .arg( pListItem->text() ) );
1959 }
1960 }
1961 }
1962
1963 m_pHydrogen->updateVirtualPatterns();
1964 }
1965
1966 delete pDialog;
1967
1969}//patternPopup_virtualPattern
1970
1971
1972
1974{
1976
1977 auto pHydrogen = Hydrogen::get_instance();
1978 auto pSong = pHydrogen->getSong();
1979
1980 if ( pSong == nullptr ) {
1982 return;
1983 }
1984
1985 Pattern* pPattern = pSong->getPatternList()->get( m_nRowClicked );
1986
1988 if ( ! Filesystem::dir_readable( sPath, false ) ){
1989 sPath = Filesystem::patterns_dir();
1990 }
1991
1992 FileDialog fd(this);
1993 fd.setAcceptMode( QFileDialog::AcceptOpen );
1994 fd.setFileMode( QFileDialog::ExistingFile );
1995 fd.setNameFilter( Filesystem::patterns_filter_name );
1996 fd.setDirectory( sPath );
1997 fd.setWindowTitle( QString( tr( "Open Pattern to Replace " )
1998 .append( pPattern->get_name() ) ) );
1999
2000 if (fd.exec() != QDialog::Accepted) {
2002 return;
2003 }
2004 QString patternPath = fd.selectedFiles().first();
2005
2006 QString prevPatternPath =
2007 Files::savePatternTmp( pPattern->get_name(), pPattern, pSong,
2008 pHydrogen->getLastLoadedDrumkitName() );
2009 if ( prevPatternPath.isEmpty() ) {
2010 QMessageBox::warning( this, "Hydrogen", tr("Could not save pattern to temporary directory.") );
2012 return;
2013 }
2014 QString sequencePath = Filesystem::tmp_file_path( "SEQ.xml" );
2015 if ( !pSong->writeTempPatternList( sequencePath ) ) {
2016 QMessageBox::warning( this, "Hydrogen", tr("Could not export sequence.") );
2018 return;
2019 }
2020 Preferences::get_instance()->setLastOpenPatternDirectory( fd.directory().absolutePath() );
2021
2022 SE_loadPatternAction *action =
2023 new SE_loadPatternAction( patternPath, prevPatternPath, sequencePath,
2024 m_nRowClicked, false );
2025 HydrogenApp *hydrogenApp = HydrogenApp::get_instance();
2026 hydrogenApp->m_pUndoStack->push( action );
2027
2029}
2030
2039
2041{
2043
2044 auto pHydrogenApp = HydrogenApp::get_instance();
2045 auto pCommonStrings = pHydrogenApp->getCommonStrings();
2046 auto pHydrogen = Hydrogen::get_instance();
2047 auto pSong = pHydrogen->getSong();
2048 auto pPattern = pSong->getPatternList()->get( m_nRowClicked );
2049
2050 QString sPath = Files::savePatternNew( pPattern->get_name(), pPattern,
2051 pSong, pHydrogen->getLastLoadedDrumkitName() );
2052 if ( sPath.isEmpty() ) {
2053 if ( QMessageBox::information(
2054 this, "Hydrogen",
2055 tr( "The pattern-file exists. \nOverwrite the existing pattern?"),
2056 QMessageBox::Ok | QMessageBox::Cancel,
2057 QMessageBox::Cancel ) != QMessageBox::Ok ) {
2059 return;
2060 }
2061 sPath = Files::savePatternOver( pPattern->get_name(), pPattern,
2062 pSong, pHydrogen->getLastLoadedDrumkitName() );
2063 }
2064
2065 if ( sPath.isEmpty() ) {
2066 QMessageBox::warning( this, "Hydrogen", tr("Could not export pattern.") );
2068 return;
2069 }
2070
2071 pHydrogenApp->showStatusBarMessage( tr( "Pattern saved." ) );
2072
2073 pHydrogen->getSoundLibraryDatabase()->updatePatterns();
2074
2076}
2077
2078
2079
2085
2086
2087
2089{
2090
2092 auto pHydrogen = Hydrogen::get_instance();
2093 auto pPattern = pHydrogen->getSong()->getPatternList()->get( m_nRowClicked );
2094
2095 PatternPropertiesDialog *dialog =
2096 new PatternPropertiesDialog( this, pPattern, m_nRowClicked, false);
2097 dialog->exec();
2098 delete dialog;
2099 dialog = nullptr;
2100
2102}
2103
2104
2105void SongEditorPatternList::acceptPatternPropertiesDialogSettings(QString newPatternName, QString newPatternInfo, QString newPatternCategory, int patternNr)
2106{
2107 Hydrogen *pHydrogen = Hydrogen::get_instance();
2108 std::shared_ptr<Song> pSong = pHydrogen->getSong();
2109 PatternList *patternList = pSong->getPatternList();
2110 H2Core::Pattern *pattern = patternList->get( patternNr );
2111 pattern->set_name( newPatternName );
2112 pattern->set_info( newPatternInfo );
2113 pattern->set_category( newPatternCategory );
2114 pHydrogen->setIsModified( true );
2117 update();
2118}
2119
2120
2121void SongEditorPatternList::revertPatternPropertiesDialogSettings(QString oldPatternName, QString oldPatternInfo, QString oldPatternCategory, int patternNr)
2122{
2123 Hydrogen *pHydrogen = Hydrogen::get_instance();
2124 std::shared_ptr<Song> pSong = pHydrogen->getSong();
2125 PatternList *patternList = pSong->getPatternList();
2126 H2Core::Pattern *pattern = patternList->get( patternNr );
2127 pattern->set_name( oldPatternName );
2128 pattern->set_category( oldPatternCategory );
2129 pHydrogen->setIsModified( true );
2132 update();
2133}
2134
2135
2137{
2139
2140 auto pSong = m_pHydrogen->getSong();
2141 auto pPattern = pSong->getPatternList()->get( m_nRowClicked );
2142
2143 QString patternPath =
2144 Files::savePatternTmp( pPattern->get_name(), pPattern, pSong,
2145 m_pHydrogen->getLastLoadedDrumkitName() );
2146 if ( patternPath.isEmpty() ) {
2147 QMessageBox::warning( this, "Hydrogen", tr("Could not save pattern to temporary directory.") );
2149 return;
2150 }
2151 QString sequencePath = Filesystem::tmp_file_path( "SEQ.xml" );
2152 if ( !pSong->writeTempPatternList( sequencePath ) ) {
2153 QMessageBox::warning( this, "Hydrogen", tr("Could not export sequence.") );
2155 return;
2156 }
2157
2159 new SE_deletePatternFromListAction( patternPath, sequencePath,
2160 m_nRowClicked );
2161 HydrogenApp *hydrogenApp = HydrogenApp::get_instance();
2162 hydrogenApp->m_pUndoStack->push( action );
2163
2165}
2166
2168{
2170
2171 auto pSong = m_pHydrogen->getSong();
2172 PatternList *pPatternList = pSong->getPatternList();
2173 auto pPattern = pPatternList->get( m_nRowClicked );
2174
2175 H2Core::Pattern *pNewPattern = new Pattern( pPattern );
2176 PatternPropertiesDialog *dialog = new PatternPropertiesDialog( this, pNewPattern, m_nRowClicked, true );
2177
2178 if ( dialog->exec() == QDialog::Accepted ) {
2179 QString filePath = Files::savePatternTmp( pNewPattern->get_name(),
2180 pNewPattern, pSong,
2181 m_pHydrogen->getLastLoadedDrumkitName() );
2182 if ( filePath.isEmpty() ) {
2183 QMessageBox::warning( this, "Hydrogen", tr("Could not save pattern to temporary directory.") );
2185 return;
2186 }
2188 new SE_duplicatePatternAction( filePath, m_nRowClicked + 1 );
2189 HydrogenApp::get_instance()->m_pUndoStack->push( action );
2190 }
2191
2192 delete dialog;
2193 delete pNewPattern;
2194
2196}
2197
2199{
2201
2202 FillRange range;
2203 PatternFillDialog *dialog = new PatternFillDialog( this, &range );
2204
2205 // use a PatternFillDialog to get the range and mode data
2206 if ( dialog->exec() == QDialog::Accepted ) {
2207
2210 HydrogenApp::get_instance()->m_pUndoStack->push( action );
2211 }
2212
2213 delete dialog;
2214
2216}
2217
2218
2220{
2221 m_pAudioEngine->lock( RIGHT_HERE );
2222
2223 std::shared_ptr<Song> pSong = m_pHydrogen->getSong();
2224 PatternList *pPatternList = pSong->getPatternList();
2225 H2Core::Pattern *pPattern = pPatternList->get( nPattern );
2226 std::vector<PatternList*> *pColumns = pSong->getPatternGroupVector(); // E' la lista di "colonne" di pattern
2227 PatternList *pColumn = nullptr;
2228
2229 int nColumn, nColumnIndex;
2230 bool bHasPattern = false;
2231 int fromVal = pRange->fromVal - 1;
2232 int toVal = pRange->toVal;
2233
2234 // Add patternlists to PatternGroupVector as necessary
2235 int nDelta = toVal - pColumns->size() + 1;
2236
2237 for ( int i = 0; i < nDelta; i++ ) {
2238 pColumn = new PatternList();
2239 pColumns->push_back( pColumn );
2240 }
2241
2242 // Fill or Clear each cell in range
2243 for ( nColumn = fromVal; nColumn < toVal; nColumn++ ) {
2244
2245 // expand Pattern
2246 pColumn = ( *pColumns )[ nColumn ];
2247
2248 assert( pColumn );
2249
2250 bHasPattern = false;
2251
2252 // check whether the pattern (and column) already exists
2253 for ( nColumnIndex = 0; pColumn && nColumnIndex < (int)pColumn->size(); nColumnIndex++) {
2254
2255 if ( pColumn->get( nColumnIndex ) == pPattern ) {
2256 bHasPattern = true;
2257 break;
2258 }
2259 }
2260
2261 if ( pRange->bInsert && !bHasPattern ) { //fill
2262 pColumn->add( pPattern);
2263 }
2264 else if ( !pRange->bInsert && bHasPattern ) { // clear
2265 pColumn->del( pPattern);
2266 }
2267 }
2268
2269 // remove all the empty patternlists at the end of the song
2270 for ( int i = pColumns->size() - 1; i != 0 ; i-- ) {
2271 PatternList *pList = (*pColumns)[ i ];
2272 int nSize = pList->size();
2273 if ( nSize == 0 ) {
2274 pColumns->erase( pColumns->begin() + i );
2275 delete pList;
2276 }
2277 else {
2278 break;
2279 }
2280 }
2281 m_pAudioEngine->unlock();
2282
2283
2284 // Update
2285 m_pHydrogen->setIsModified( true );
2287}
2288
2289
2291void SongEditorPatternList::dragEnterEvent(QDragEnterEvent *event)
2292{
2293 if ( event->mimeData()->hasFormat("text/plain") ) {
2294 event->acceptProposedAction();
2295 }
2296}
2297
2298
2300{
2301 std::shared_ptr<Song> pSong = m_pHydrogen->getSong();
2302
2303 QString sText = event->mimeData()->text();
2304 const QMimeData* mimeData = event->mimeData();
2305
2306 auto pEv = static_cast<DropEvent*>( event );
2307
2308 int nTargetPattern = 0;
2309 if(m_nGridHeight > 0)
2310 {
2311 nTargetPattern = pEv->position().y() / m_nGridHeight;
2312 }
2313
2314 if( sText.startsWith("Songs:") || sText.startsWith("move instrument:") || sText.startsWith("importInstrument:")){
2315 event->acceptProposedAction();
2316 return;
2317 }
2318
2319 if ( sText.startsWith("move pattern:") ) {
2320 QStringList tokens = sText.split( ":" );
2321 bool bOK = true;
2322
2323 int nSourcePattern = tokens[1].toInt(&bOK);
2324 if ( ! bOK ) {
2325 return;
2326 }
2327
2328 if ( nSourcePattern == nTargetPattern ) {
2329 event->acceptProposedAction();
2330 return;
2331 }
2332
2333 SE_movePatternListItemAction *action = new SE_movePatternListItemAction( nSourcePattern , nTargetPattern ) ;
2334 HydrogenApp::get_instance()->m_pUndoStack->push( action );
2335
2336 event->acceptProposedAction();
2337 }
2338 else if( sText.startsWith("file://") && mimeData->hasUrls() )
2339 {
2340 //Dragging a file from an external file manager
2341 PatternList *pPatternList = pSong->getPatternList();
2342 QList<QUrl> urlList = mimeData->urls();
2343
2344 int successfullyAddedPattern = 0;
2345
2346 for (int i = 0; i < urlList.size(); i++)
2347 {
2348 QString patternFilePath = urlList.at(i).toLocalFile();
2349 if( patternFilePath.endsWith(".h2pattern") )
2350 {
2351 Pattern* pPattern = Pattern::load_file( patternFilePath, pSong->getInstrumentList() );
2352 if ( pPattern)
2353 {
2354 H2Core::Pattern *pNewPattern = pPattern;
2355
2356 if(!pPatternList->check_name( pNewPattern->get_name() ) ){
2357 pNewPattern->set_name( pPatternList->find_unused_pattern_name( pNewPattern->get_name() ) );
2358 }
2359
2360 SE_insertPatternAction* pInsertPatternAction = new SE_insertPatternAction( nTargetPattern + successfullyAddedPattern, pNewPattern );
2361 HydrogenApp::get_instance()->m_pUndoStack->push( pInsertPatternAction );
2362
2363 successfullyAddedPattern++;
2364 }
2365 else
2366 {
2367 ERRORLOG( QString("Error loading pattern %1").arg(patternFilePath) );
2368 }
2369 }
2370 }
2371 }
2372 else
2373 {
2374 QStringList tokens = sText.split( "::" );
2375 QString sPatternName = tokens.at( 1 );
2376
2377 //create a unique sequencefilename
2378 Pattern *pPattern = pSong->getPatternList()->get( nTargetPattern );
2379 HydrogenApp *pHydrogenApp = HydrogenApp::get_instance();
2380
2381 QString oldPatternName = pPattern->get_name();
2382
2383 QString sequenceFilename = Filesystem::tmp_file_path( "SEQ.xml" );
2384 bool drag = false;
2385 if( QString( tokens.at(0) ).contains( "drag pattern" )) drag = true;
2386 SE_loadPatternAction *pAction = new SE_loadPatternAction( sPatternName, oldPatternName, sequenceFilename, nTargetPattern, drag );
2387
2388 pHydrogenApp->m_pUndoStack->push( pAction );
2389 }
2390}
2391
2392
2393
2394void SongEditorPatternList::movePatternLine( int nSourcePattern , int nTargetPattern )
2395{
2396 Hydrogen *pHydrogen = Hydrogen::get_instance();
2397
2398 std::shared_ptr<Song> pSong = pHydrogen->getSong();
2399 PatternList *pPatternList = pSong->getPatternList();
2400
2401
2402
2403 // move patterns...
2404 H2Core::Pattern *pSourcePattern = pPatternList->get( nSourcePattern );
2405 if ( nSourcePattern < nTargetPattern) {
2406 for (int nPatr = nSourcePattern; nPatr < nTargetPattern; nPatr++) {
2407 H2Core::Pattern *pPattern = pPatternList->get(nPatr + 1);
2408 pPatternList->replace( nPatr, pPattern );
2409 }
2410 pPatternList->replace( nTargetPattern, pSourcePattern );
2411 }
2412 else {
2413 for (int nPatr = nSourcePattern; nPatr > nTargetPattern; nPatr--) {
2414 H2Core::Pattern *pPattern = pPatternList->get(nPatr - 1);
2415 pPatternList->replace( nPatr, pPattern );
2416 }
2417 pPatternList->replace( nTargetPattern, pSourcePattern );
2418 }
2419
2420 if ( pHydrogen->isPatternEditorLocked() ) {
2421 pHydrogen->updateSelectedPattern();
2422 } else {
2423 pHydrogen->setSelectedPatternNumber( nTargetPattern );
2424 }
2426 pHydrogen->setIsModified( true );
2427}
2428
2430 UNUSED( ev );
2431 m_nRowHovered = -1;
2433 update();
2434}
2435
2437{
2438 auto pEv = static_cast<MouseEvent*>( event );
2439
2440 // Update the highlighting of the hovered row.
2441 if ( pEv->position().y() / m_nGridHeight != m_nRowHovered ) {
2442 m_nRowHovered = pEv->position().y() / m_nGridHeight;
2444 update();
2445 }
2446
2447 if (!(event->buttons() & Qt::LeftButton)) {
2448 return;
2449 }
2450 if ( (pEv->position().y() / m_nGridHeight) == (__drag_start_position.y() / m_nGridHeight) ) {
2451 return;
2452 }
2453 std::shared_ptr<Song> pSong = m_pHydrogen->getSong();
2454 PatternList *pPatternList = pSong->getPatternList();
2455 int row = (__drag_start_position.y() / m_nGridHeight);
2456 if ( row >= (int)pPatternList->size() ) {
2457 return;
2458 }
2459 Pattern *pPattern = pPatternList->get( row );
2460 QString sName = "<unknown>";
2461 if ( pPattern ) {
2462 sName = pPattern->get_name();
2463 }
2464 QString sText = QString("move pattern:%1:%2").arg( row ).arg( sName );
2465
2466 QDrag *pDrag = new QDrag(this);
2467 QMimeData *pMimeData = new QMimeData;
2468
2469 pMimeData->setText( sText );
2470 pDrag->setMimeData( pMimeData);
2471 //drag->setPixmap(iconPixmap);
2472
2473 m_pDragScroller->startDrag();
2474 pDrag->exec( Qt::CopyAction | Qt::MoveAction );
2475 m_pDragScroller->endDrag();
2476
2477 QWidget::mouseMoveEvent(event);
2478}
2479
2480
2485
2495
2496// ::::::::::::::::::::::::::
2497
2499 : QWidget( parent )
2500 , m_bRightBtnPressed( false )
2502 , m_nHoveredColumn( -1 )
2504 , m_nTagHeight( 6 )
2505 , m_fTick( 0 )
2506 , m_nColumn( 0 )
2507{
2508
2509 auto pPref = H2Core::Preferences::get_instance();
2510
2512
2514 m_pAudioEngine = m_pHydrogen->getAudioEngine();
2515
2516 setAttribute(Qt::WA_OpaquePaintEvent);
2517 setMouseTracking( true );
2518
2519 m_nGridWidth = pPref->getSongEditorGridWidth();
2520
2521 int nInitialWidth = SongEditor::nMargin +
2523
2524 m_nActiveColumns = m_pHydrogen->getSong()->getPatternGroupVector()->size();
2525
2526 resize( nInitialWidth, m_nMinimumHeight );
2527 setFixedHeight( m_nMinimumHeight );
2528
2529 qreal pixelRatio = devicePixelRatio();
2530 m_pBackgroundPixmap = new QPixmap( nInitialWidth * pixelRatio, m_nMinimumHeight * pixelRatio ); // initialize the pixmap
2531 m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio );
2532
2533 createBackground(); // create m_backgroundPixmap pixmap
2534 update();
2535
2536 m_pTimer = new QTimer(this);
2537 connect(m_pTimer, &QTimer::timeout, [=]() {
2538 if ( H2Core::Hydrogen::get_instance()->getAudioEngine()->getState() ==
2541 }
2542 });
2543 m_pTimer->start(200);
2544}
2545
2546
2547
2554
2558
2560 m_nActiveColumns = m_pHydrogen->getSong()->getPatternGroupVector()->size();
2562 update();
2563}
2564
2569
2571{
2573 {
2574 m_nGridWidth = width;
2575 resize( columnToX( Preferences::get_instance()->getMaxBars() ), height() );
2577 update();
2578 }
2579}
2580
2584
2586{
2588 auto pHydrogen = Hydrogen::get_instance();
2589 auto pSong = pHydrogen->getSong();
2590 auto pTimeline = pHydrogen->getTimeline();
2591 auto tagVector = pTimeline->getAllTags();
2592
2593 QColor textColor( pPref->getColorTheme()->m_songEditor_textColor );
2594 QColor textColorAlpha( textColor );
2595 textColorAlpha.setAlpha( 45 );
2596
2597 QColor backgroundColor = pPref->getColorTheme()->m_songEditor_alternateRowColor.darker( 115 );
2598 QColor backgroundInactiveColor = pPref->getColorTheme()->m_midLightColor;
2599 QColor backgroundColorTempoMarkers = backgroundColor.darker( 120 );
2600
2601 QColor colorHighlight = pPref->getColorTheme()->m_highlightColor;
2602
2603 QColor lineColor = pPref->getColorTheme()->m_songEditor_lineColor;
2604 QColor lineColorAlpha( lineColor );
2605 lineColorAlpha.setAlpha( 45 );
2606
2607 // Resize pixmap if pixel ratio has changed
2608 qreal pixelRatio = devicePixelRatio();
2609 if ( m_pBackgroundPixmap->devicePixelRatio() != pixelRatio ||
2610 m_pBackgroundPixmap->width() != width() ||
2611 m_pBackgroundPixmap->height() != height() ) {
2612 delete m_pBackgroundPixmap;
2613 m_pBackgroundPixmap = new QPixmap( width() * pixelRatio , height() * pixelRatio );
2614 m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio );
2615 }
2616
2617 QFont font( pPref->getApplicationFontFamily(), getPointSize( pPref->getFontSize() ) );
2618
2619 QPainter p( m_pBackgroundPixmap );
2620 p.setFont( font );
2621
2622 int nActiveWidth = columnToX( m_nActiveColumns ) + 1;
2623 p.fillRect( 0, 0, width(), height(), backgroundColorTempoMarkers );
2624 p.fillRect( 0, 25, nActiveWidth, height() - 25, backgroundColor );
2625 p.fillRect( nActiveWidth, 25, width() - nActiveWidth, height() - 25,
2626 backgroundInactiveColor );
2627 char tmp[10];
2628
2629 int nMaxPatternSequence = pPref->getMaxBars();
2630
2631 QColor textColorGrid( textColor );
2632 textColorGrid.setAlpha( 200 );
2633 p.setPen( QPen( textColorGrid, 1, Qt::SolidLine ) );
2634 for ( int ii = 0; ii < nMaxPatternSequence + 1; ii++) {
2635 int x = columnToX( ii );
2636
2637 if ( ( ii % 4 ) == 0 ) {
2638 p.drawLine( x, height() - 14, x, height() - 1);
2639 }
2640 else {
2641 p.drawLine( x, height() - 6, x, height() - 1);
2642 }
2643 }
2644
2645 // Add every 4th number to the grid
2646 p.setPen( textColor );
2647 for (uint i = 0; i < nMaxPatternSequence + 1; i += 4) {
2648 uint x = columnToX( i );
2649
2650 sprintf( tmp, "%d", i + 1 );
2651 if ( i < 10 ) {
2652 p.drawText( x, height() / 2 + 3, m_nGridWidth, height() / 2 - 7,
2653 Qt::AlignHCenter, tmp );
2654 } else {
2655 p.drawText( x + 2, height() / 2 + 3, m_nGridWidth * 3.5, height() / 2 - 7,
2656 Qt::AlignLeft, tmp );
2657 }
2658 }
2659
2660 // draw tags
2661 p.setPen( pPref->getColorTheme()->m_accentTextColor );
2662
2663 QFont font2( pPref->getApplicationFontFamily(), 5 );
2664 p.setFont( font2 );
2665
2666 for ( const auto& ttag : tagVector ){
2667 int x = columnToX( ttag->nColumn ) + 4;
2668 QRect rect( x, height() / 2 - 1 - m_nTagHeight,
2670
2671 p.fillRect( rect, pPref->getColorTheme()->m_highlightColor.darker( 135 ) );
2672 p.drawText( rect, Qt::AlignCenter, "T");
2673 }
2674 p.setFont( font );
2675
2676 // draw tempo content
2677
2678 // Draw tempo marker grid.
2679 if ( ! pHydrogen->isTimelineEnabled() ) {
2680 p.setPen( textColorAlpha );
2681 } else {
2682 QColor tempoMarkerGridColor( textColor );
2683 tempoMarkerGridColor.setAlpha( 170 );
2684 p.setPen( tempoMarkerGridColor );
2685 }
2686 for (uint ii = 0; ii < nMaxPatternSequence + 1; ii++) {
2687 uint x = columnToX( ii );
2688
2689 p.drawLine( x, 1, x, 4 );
2690 p.drawLine( x, height() / 2 - 5, x, height() / 2 );
2691 }
2692
2693 // Draw tempo markers
2694 auto tempoMarkerVector = pTimeline->getAllTempoMarkers();
2695 for ( const auto& ttempoMarker : tempoMarkerVector ){
2696 drawTempoMarker( ttempoMarker, false, p );
2697 }
2698
2699 p.setPen( QColor(35, 39, 51) );
2700 p.drawLine( 0, 0, width(), 0 );
2701 p.drawLine( 0, height() - 25, width(), height() - 25 );
2702 p.drawLine( 0, height(), width(), height() );
2703
2704 m_bBackgroundInvalid = false;
2705}
2706
2708 auto pTimeline = Hydrogen::get_instance()->getTimeline();
2709 if ( ! pTimeline->isFirstTempoMarkerSpecial() ) {
2710 return;
2711 }
2712
2713 // There is just the special tempo marker -> no tempo markers set
2714 // by the user. In this case the special marker isn't drawn and
2715 // doesn't need to be update.
2716 if ( pTimeline->getAllTempoMarkers().size() == 1 ) {
2717 return;
2718 }
2719
2721 update();
2722}
2723
2725 // This can change the size of the song and affect the position of
2726 // the playhead.
2727 update();
2728}
2729
2731 // Triggered every time the column of the SongEditor grid
2732 // changed. Either by rolling transport or by relocation.
2733 update();
2734}
2735
2737 m_nHoveredColumn = -1;
2739 update();
2740
2741 QWidget::leaveEvent( ev );
2742}
2743
2745{
2746 auto pHydrogen = Hydrogen::get_instance();
2747
2748 auto pEv = static_cast<MouseEvent*>( ev );
2749
2750 int nColumn = std::max( xToColumn( pEv->position().x() ), 0 );
2751
2752 HoveredRow row;
2753 if ( pEv->position().y() > 22 ) {
2754 row = HoveredRow::Ruler;
2755 } else if ( pEv->position().y() > 22 - 1 - m_nTagHeight ) {
2756 row = HoveredRow::Tag;
2757 } else {
2759 }
2760
2761 if ( nColumn != m_nHoveredColumn ||
2762 row != m_hoveredRow ) {
2763 // Cursor has moved into a region where the above caching
2764 // became invalid.
2765 m_hoveredRow = row;
2766 m_nHoveredColumn = nColumn;
2767
2768 update();
2769 }
2770
2771 if ( !m_bRightBtnPressed && ev->buttons() & Qt::LeftButton ) {
2772 // Click+drag triggers same action as clicking at new position
2773 mousePressEvent( ev );
2774 } else if ( ev->buttons() & Qt::RightButton ) {
2775 // Right-click+drag
2777
2778 if ( nColumn > (int)Hydrogen::get_instance()->getSong()->getPatternGroupVector()->size() ) {
2779 pPref->setPunchOutPos(-1);
2780 return;
2781 }
2782 if ( Hydrogen::get_instance()->getMode() == Song::Mode::Pattern ) {
2783 return;
2784 }
2785 pPref->setPunchOutPos( nColumn - 1 );
2786 update();
2787 }
2788}
2789
2791 if ( ev->type() == QEvent::ToolTip ) {
2792 const auto helpEv = dynamic_cast<QHelpEvent*>(ev);
2793 showToolTip( helpEv->pos(), helpEv->globalPos() );
2794 return 0;
2795 }
2796
2797 return QWidget::event( ev );
2798}
2799
2805
2810
2815
2817
2818 if ( nValue == 0 ) { // different song opened
2821 }
2822}
2823
2824void SongEditorPositionRuler::showToolTip( const QPoint& pos, const QPoint& globalPos ) {
2825 auto pHydrogen = Hydrogen::get_instance();
2826 auto pTimeline = pHydrogen->getTimeline();
2827
2828 const int nColumn = std::max( xToColumn( pos.x() ), 0 );
2829
2830 if ( pHydrogen->isTimelineEnabled() &&
2831 pTimeline->isFirstTempoMarkerSpecial() &&
2833 pos.x() < columnToX( 1 ) ) { // first tempo marker
2834 const QString sBpm =
2835 pTimeline->getTempoMarkerAtColumn( nColumn )->getPrettyString( -1 );
2836 QToolTip::showText( globalPos, QString( "%1: %2" )
2837 .arg( tr( "The tempo set in the BPM widget will be used as a default for the beginning of the song. Left-click to overwrite it." ) )
2838 .arg( sBpm ), this );
2839
2840 }
2841 else if ( m_hoveredRow == HoveredRow::TempoMarker ) {
2842 if ( pTimeline->hasColumnTempoMarker( nColumn ) ) {
2843 const QString sBpm =
2844 pTimeline->getTempoMarkerAtColumn( nColumn )->getPrettyString( -1 );
2845 QToolTip::showText( globalPos, sBpm, this );
2846 }
2847 }
2848 else if ( m_hoveredRow == HoveredRow::Tag ) {
2849 // Row containing the tags
2850 if ( pTimeline->hasColumnTag( nColumn ) ) {
2851 QToolTip::showText( globalPos,
2852 pTimeline->getTagAtColumn( nColumn ), this );
2853 }
2854 }
2855}
2856
2858{
2859 SongEditorPanelTagWidget dialog( this , nColumn );
2860 dialog.exec();
2861}
2862
2864{
2865 bool bTempoMarkerPresent =
2866 Hydrogen::get_instance()->getTimeline()->hasColumnTempoMarker( nColumn );
2867 m_nActiveBpmWidgetColumn = nColumn;
2868 update();
2869
2870 SongEditorPanelBpmWidget dialog( this , nColumn, bTempoMarkerPresent );
2871 dialog.exec();
2872
2874 update();
2875}
2876
2877
2879{
2880 auto pEv = static_cast<MouseEvent*>( ev );
2881
2882 auto pHydrogen = Hydrogen::get_instance();
2883 auto pCoreActionController = pHydrogen->getCoreActionController();
2884
2885 int nColumn = std::max( xToColumn( pEv->position().x() ), 0 );
2886
2887 if (ev->button() == Qt::LeftButton ) {
2888 if ( pEv->position().y() > 22 ) {
2889 // Relocate transport using position ruler
2890 m_bRightBtnPressed = false;
2891
2892 if ( nColumn > (int) m_pHydrogen->getSong()->getPatternGroupVector()->size() ) {
2893 return;
2894 }
2895
2896 if ( m_pHydrogen->getMode() == Song::Mode::Pattern ) {
2897 pCoreActionController->activateSongMode( true );
2898 m_pHydrogen->setIsModified( true );
2899 }
2900
2901 m_pHydrogen->getCoreActionController()->locateToColumn( nColumn );
2902 update();
2903 }
2904 else if ( pEv->position().y() > 22 - 1 - m_nTagHeight ) {
2905 showTagWidget( nColumn );
2906 }
2907 else if ( m_pHydrogen->isTimelineEnabled() ){
2908 showBpmWidget( nColumn );
2909 }
2910 }
2911 else if ( ev->button() == Qt::MiddleButton ) {
2912 showTagWidget( nColumn );
2913 }
2914 else if (ev->button() == Qt::RightButton && pEv->position().y() >= 26) {
2916 if ( nColumn >= (int) m_pHydrogen->getSong()->getPatternGroupVector()->size() ) {
2917 pPref->unsetPunchArea();
2918 return;
2919 }
2920 if ( m_pHydrogen->getMode() == Song::Mode::Pattern ) {
2921 return;
2922 }
2923 m_bRightBtnPressed = true;
2924 // Disable until mouse is moved
2925 pPref->setPunchInPos( nColumn );
2926 pPref->setPunchOutPos(-1);
2927 update();
2928 }
2929
2930}
2931
2932
2933
2934
2936{
2937 UNUSED( ev );
2938 m_bRightBtnPressed = false;
2939}
2940
2941
2943{
2944 auto pHydrogenApp = HydrogenApp::get_instance();
2945 auto pSongEditor = pHydrogenApp->getSongEditorPanel()->getSongEditor();
2946 auto pTimeline = m_pHydrogen->getTimeline();
2947 auto pPref = Preferences::get_instance();
2948 auto tempoMarkerVector = pTimeline->getAllTempoMarkers();
2949
2950 if ( m_bBackgroundInvalid ) {
2952 }
2953
2954 if (!isVisible()) {
2955 return;
2956 }
2957
2958 QColor textColor( pPref->getColorTheme()->m_songEditor_textColor );
2959 QColor textColorAlpha( textColor );
2960 textColorAlpha.setAlpha( 45 );
2961 QColor highlightColor = pPref->getColorTheme()->m_highlightColor;
2962 QColor colorHovered( highlightColor );
2963 colorHovered.setAlpha( 200 );
2964 QColor backgroundColor = pPref->getColorTheme()->m_songEditor_alternateRowColor.darker( 115 );
2965 QColor backgroundColorTempoMarkers = backgroundColor.darker( 120 );
2966
2967 int nPunchInPos = Preferences::get_instance()->getPunchInPos();
2968 int nPunchOutPos = Preferences::get_instance()->getPunchOutPos();
2969
2970 QPainter painter(this);
2971 QFont font( pPref->getApplicationFontFamily(), getPointSize( pPref->getFontSize() ) );
2972 qreal pixelRatio = devicePixelRatio();
2973 if ( pixelRatio != m_pBackgroundPixmap->devicePixelRatio() ) {
2975 }
2976 QRectF srcRect(
2977 pixelRatio * ev->rect().x(),
2978 pixelRatio * ev->rect().y(),
2979 pixelRatio * ev->rect().width(),
2980 pixelRatio * ev->rect().height()
2981 );
2982 painter.drawPixmap( ev->rect(), *m_pBackgroundPixmap, srcRect );
2983
2984 // Which tempo marker is the currently used one?
2985 int nCurrentTempoMarkerColumn = -1;
2986 for ( const auto& tempoMarker : tempoMarkerVector ) {
2987 if ( tempoMarker->nColumn > m_nColumn ) {
2988 break;
2989 }
2990 nCurrentTempoMarkerColumn = tempoMarker->nColumn;
2991 }
2992 if ( nCurrentTempoMarkerColumn == -1 &&
2993 tempoMarkerVector.size() != 0 ) {
2994 auto pTempoMarker = tempoMarkerVector[ tempoMarkerVector.size() - 1 ];
2995 if ( pTempoMarker != nullptr ) {
2996 nCurrentTempoMarkerColumn = pTempoMarker->nColumn;
2997 }
2998 }
2999 if ( nCurrentTempoMarkerColumn != -1 ) {
3000 auto pTempoMarker = pTimeline->getTempoMarkerAtColumn( nCurrentTempoMarkerColumn );
3001 if ( pTempoMarker != nullptr ) {
3002 // Reset the region and overwrite the marker's versio
3003 // using normal weight.
3004 const QRect rect = calcTempoMarkerRect( pTempoMarker, true );
3005 painter.fillRect( rect, backgroundColorTempoMarkers );
3006 drawTempoMarker( pTempoMarker, true, painter );
3007 }
3008 }
3009
3010 // Draw playhead
3011 if ( m_fTick != -1 ) {
3012 int nX = tickToColumn( m_fTick, m_nGridWidth );
3013 int nShaftOffset = Skin::getPlayheadShaftOffset();
3014 Skin::drawPlayhead( &painter, nX, height() / 2 + 2, false );
3015 painter.drawLine( nX + nShaftOffset, 0, nX + nShaftOffset, height() );
3016 }
3017
3018 // Highlight hovered tick of the Timeline
3019 if ( m_hoveredRow == HoveredRow::Tag ||
3021 m_pHydrogen->isTimelineEnabled() ) ||
3022 m_nActiveBpmWidgetColumn != -1 ) {
3023
3024 int nColumn;
3025 if ( m_nActiveBpmWidgetColumn != -1 ) {
3026 nColumn = m_nActiveBpmWidgetColumn;
3027 } else {
3028 nColumn = m_nHoveredColumn;
3029 }
3030 const int x = columnToX( nColumn );
3031
3032 // Erase background tick lines to not get any interference.
3033 painter.fillRect( x, 1, 1, 4,
3034 backgroundColorTempoMarkers );
3035 painter.fillRect( x, height() / 2 - 5, 1, 4,
3036 backgroundColorTempoMarkers );
3037
3039 // Erase the area of horizontal line above a tempo
3040 // marker too. The associated highlight will be drawn later on.
3041 painter.fillRect( x - 1, 1, m_nGridWidth - 1, 4,
3042 backgroundColorTempoMarkers );
3043 }
3044
3045 painter.setPen( QPen( highlightColor, 1 ) );
3046
3047 painter.drawLine( x, 1, x, 4 );
3048 painter.drawLine( x, height() / 2 - 5, x, height() / 2 - 1 );
3049 }
3050
3051 // Highlight tag
3052 bool bTagPresent = false;
3053 if ( m_hoveredRow == HoveredRow::Tag &&
3054 pTimeline->hasColumnTag( m_nHoveredColumn ) ) {
3055
3056 int x = columnToX( m_nHoveredColumn ) + 4;
3057 QRect rect( x, height() / 2 - 1 - m_nTagHeight,
3059
3060 QFont font2( pPref->getApplicationFontFamily(), 5 );
3061 painter.setFont( font2 );
3062
3063 painter.fillRect( rect, pPref->getColorTheme()->m_highlightColor );
3064 painter.setPen( pPref->getColorTheme()->m_highlightedTextColor );
3065 painter.drawText( rect, Qt::AlignCenter, "T");
3066
3067 painter.setFont( font );
3068 bTagPresent = true;
3069 }
3070
3071 // Draw a slight highlight around the tempo marker hovered using
3072 // mouse or touch events. This will also redraw the
3073 // tempo marker to ensure it's visible (they can overlap with
3074 // neighboring ones and be hardly readable).
3075 bool bTempoMarkerPresent = false;
3076 if ( ! bTagPresent &&
3077 ( ( m_pHydrogen->isTimelineEnabled() &&
3079 m_nActiveBpmWidgetColumn != -1 ) ) {
3080
3081 int nColumn;
3082 if ( m_nActiveBpmWidgetColumn != -1 ) {
3083 nColumn = m_nActiveBpmWidgetColumn;
3084 } else {
3085 nColumn = m_nHoveredColumn;
3086 }
3087
3088 if ( pTimeline->hasColumnTempoMarker( nColumn ) ||
3089 ( pTimeline->isFirstTempoMarkerSpecial() &&
3090 nColumn == 0 ) ) {
3091
3092 auto pTempoMarker = pTimeline->getTempoMarkerAtColumn( nColumn );
3093 if ( pTempoMarker != nullptr ) {
3094
3095 const bool bEmphasize = pTempoMarker->nColumn == nCurrentTempoMarkerColumn;
3096 const QRect rect = calcTempoMarkerRect( pTempoMarker, bEmphasize );
3097
3098 painter.fillRect( rect, backgroundColorTempoMarkers );
3099 drawTempoMarker( pTempoMarker, bEmphasize, painter );
3100
3101 if ( m_nActiveBpmWidgetColumn == -1 ) {
3102 painter.setPen( QPen( colorHovered, 1 ) );
3103 } else {
3104 painter.setPen( QPen( highlightColor, 1 ) );
3105 }
3106 painter.drawRect( rect );
3107
3108 painter.drawLine( rect.x(), 2, rect.x() + m_nGridWidth / 2, 2 );
3109
3110 bTempoMarkerPresent = true;
3111 }
3112 }
3113 }
3114
3115 // Draw hovering highlights in tempo marker row
3116 if ( ( m_nHoveredColumn > -1 &&
3117 ( ( m_hoveredRow == HoveredRow::Tag && !bTagPresent ) ||
3119 m_pHydrogen->isTimelineEnabled() &&
3120 ! bTempoMarkerPresent ) ) ) ||
3121 ( m_nActiveBpmWidgetColumn != -1 &&
3122 ! bTempoMarkerPresent ) ) {
3123
3124 QColor color;
3125 if ( m_nActiveBpmWidgetColumn == -1 ) {
3126 color = colorHovered;
3127 } else {
3128 color = highlightColor;
3129 }
3130 QPen p( color );
3131 p.setWidth( 1 );
3132 painter.setPen( p );
3133
3134 int nCursorX;
3135 if ( m_nActiveBpmWidgetColumn != -1 ) {
3136 nCursorX = columnToX( m_nActiveBpmWidgetColumn ) + 3;
3137 } else {
3138 nCursorX = columnToX( m_nHoveredColumn ) + 3;
3139 }
3140
3142 m_nActiveBpmWidgetColumn != -1 ) {
3143 // Reset the background during highlight in order to
3144 // indicate that no tempo marker is present in this
3145 // column.
3146 QRect hoveringRect( nCursorX, 6, m_nGridWidth - 5, 12 );
3147 painter.fillRect( hoveringRect, backgroundColorTempoMarkers );
3148 painter.drawRect( hoveringRect );
3149 } else {
3150 painter.drawRect( nCursorX, height() / 2 - 1 - m_nTagHeight,
3151 m_nGridWidth - 5, m_nTagHeight - 1 );
3152 }
3153 }
3154
3155 // Draw cursor
3156 if ( ! pHydrogenApp->hideKeyboardCursor() && pSongEditor->hasFocus() ) {
3157 int nCursorX = columnToX( pSongEditor->getCursorColumn() ) + 2;
3158
3159 QColor cursorColor = pPref->getColorTheme()->m_cursorColor;
3160
3161 QPen p( cursorColor );
3162 p.setWidth( 2 );
3163 painter.setPen( p );
3164 painter.setRenderHint( QPainter::Antialiasing );
3165 // Aim to leave a visible gap between the border of the
3166 // pattern cell, and the cursor line, for consistency and
3167 // visibility.
3168 painter.drawLine( nCursorX, height() / 2 + 3,
3169 nCursorX + m_nGridWidth - 3, height() / 2 + 3 );
3170 painter.drawLine( nCursorX, height() / 2 + 4,
3171 nCursorX, height() / 2 + 5 );
3172 painter.drawLine( nCursorX + m_nGridWidth - 3, height() / 2 + 4,
3173 nCursorX + m_nGridWidth - 3, height() / 2 + 5 );
3174 painter.drawLine( nCursorX, height() - 4,
3175 nCursorX + m_nGridWidth - 3, height() - 4 );
3176 painter.drawLine( nCursorX, height() - 6,
3177 nCursorX, height() - 5 );
3178 painter.drawLine( nCursorX + m_nGridWidth - 3, height() - 6,
3179 nCursorX + m_nGridWidth - 3, height() - 5 );
3180 }
3181
3182 // Faint playhead over hovered position marker.
3183 if ( m_nHoveredColumn > -1 &&
3186
3188 int nShaftOffset = Skin::getPlayheadShaftOffset();
3189 Skin::drawPlayhead( &painter, x, height() / 2 + 2, true );
3190 painter.drawLine( x + nShaftOffset, 0, x + nShaftOffset, height() / 2 + 1 );
3191 painter.drawLine( x + nShaftOffset, height() / 2 + 2 + Skin::nPlayheadHeight,
3192 x + nShaftOffset, height() );
3193 }
3194
3195 if ( nPunchInPos <= nPunchOutPos ) {
3196 const int xIn = columnToX( nPunchInPos );
3197 const int xOut = columnToX( nPunchOutPos + 1 );
3198 painter.fillRect( xIn, 30, xOut-xIn+1, 12, QColor(200, 100, 100, 100) );
3199 QPen pen(QColor(200, 100, 100));
3200 painter.setPen(pen);
3201 painter.drawRect( xIn, 30, xOut-xIn+1, 12 );
3202 }
3203}
3204
3205QRect SongEditorPositionRuler::calcTempoMarkerRect( std::shared_ptr<const Timeline::TempoMarker> pTempoMarker, bool bEmphasize ) const {
3206 assert( pTempoMarker );
3207
3208 auto pTimeline = Hydrogen::get_instance()->getTimeline();
3209 auto pPref = Preferences::get_instance();
3210 auto weight = QFont::Normal;
3211 if ( bEmphasize ) {
3212 weight = QFont::Bold;
3213 }
3214
3215 const QFont font( pPref->getApplicationFontFamily(),
3216 getPointSize( pPref->getFontSize() ), weight );
3217
3218 const int x = columnToX( pTempoMarker->nColumn );
3219 int nWidth = QFontMetrics( font ).size(
3220 Qt::TextSingleLine, pTempoMarker->getPrettyString( 2 ) ).width();
3221
3222 // Check whether the full width would overlap with an adjacent
3223 // tempo marker and trim it if necessary
3224 const int nCoveredNeighborColumns =
3225 static_cast<int>(std::floor( static_cast<float>(nWidth) /
3226 static_cast<float>(m_nGridWidth) ) );
3227 for ( int ii = 1; ii <= nCoveredNeighborColumns; ++ii ) {
3228 if ( pTimeline->hasColumnTempoMarker( pTempoMarker->nColumn + ii ) ) {
3229 nWidth = m_nGridWidth * ii;
3230 break;
3231 }
3232 }
3233
3234 QRect rect( x, 6, nWidth, 12 );
3235
3236 return std::move( rect );
3237}
3238
3239void SongEditorPositionRuler::drawTempoMarker( std::shared_ptr<const Timeline::TempoMarker> pTempoMarker, bool bEmphasize, QPainter& painter ) {
3240 assert( pTempoMarker );
3241
3242 auto pPref = Preferences::get_instance();
3243 auto pHydrogen = Hydrogen::get_instance();
3244 auto pSong = pHydrogen->getSong();
3245 auto pTimeline = pHydrogen->getTimeline();
3246
3247 // Only paint the special tempo marker in case Timeline is
3248 // activated.
3249 if ( pTempoMarker->nColumn == 0 && pTimeline->isFirstTempoMarkerSpecial() &&
3250 ! pHydrogen->isTimelineEnabled() ) {
3251 return;
3252 }
3253
3254 QFont font( pPref->getApplicationFontFamily(), getPointSize( pPref->getFontSize() ) );
3255
3256 QRect rect = calcTempoMarkerRect( pTempoMarker, bEmphasize );
3257
3258 // Draw an additional small horizontal line at the top of the
3259 // current column to better indicate the position of the tempo
3260 // marker (for larger float values e.g. 130.67).
3261 QColor textColor( pPref->getColorTheme()->m_songEditor_textColor );
3262
3263 if ( pTempoMarker->nColumn == 0 && pTimeline->isFirstTempoMarkerSpecial() ) {
3264 textColor = textColor.darker( 150 );
3265 }
3266
3267 if ( ! pHydrogen->isTimelineEnabled() ) {
3268 QColor textColorAlpha( textColor );
3269 textColorAlpha.setAlpha( 45 );
3270 painter.setPen( textColorAlpha );
3271 } else {
3272 QColor tempoMarkerGridColor( textColor );
3273 tempoMarkerGridColor.setAlpha( 170 );
3274 painter.setPen( tempoMarkerGridColor );
3275 }
3276
3277 painter.drawLine( rect.x(), 2, rect.x() + m_nGridWidth / 2, 2 );
3278
3279 QColor tempoMarkerColor( textColor );
3280 if ( ! pHydrogen->isTimelineEnabled() ) {
3281 tempoMarkerColor.setAlpha( 45 );
3282 }
3283 painter.setPen( tempoMarkerColor );
3284
3285 if ( bEmphasize ) {
3286 font.setBold( true );
3287 }
3288 painter.setFont( font );
3289 painter.drawText( rect, Qt::AlignLeft | Qt::AlignVCenter,
3290 pTempoMarker->getPrettyString( 2 ) );
3291
3292 if ( bEmphasize ) {
3293 font.setBold( false );
3294 }
3295 painter.setFont( font );
3296}
3297
3299{
3300 const auto pTimeline = m_pHydrogen->getTimeline();
3301 const auto pPref = Preferences::get_instance();
3302 const auto tempoMarkerVector = pTimeline->getAllTempoMarkers();
3303
3304 m_pAudioEngine->lock( RIGHT_HERE );
3305
3306 const auto pTransportPos = m_pAudioEngine->getTransportPosition();
3307 const auto pPatternGroupVector =
3308 m_pHydrogen->getSong()->getPatternGroupVector();
3309 m_nColumn = std::max( pTransportPos->getColumn(), 0 );
3310
3311 float fTick = static_cast<float>(m_nColumn);
3312
3313 if ( m_pHydrogen->getMode() == Song::Mode::Pattern ) {
3314 fTick = -1;
3315 }
3316 else {
3317 // Song mode
3318 if ( pTransportPos->getColumn() == -1 ) {
3319 // Transport reached end of song. This can mean we switched from
3320 // Pattern Mode and transport wasn't started yet -> playhead at
3321 // beginning or we reached the end during playback -> playhead stays
3322 // at the end.
3323 if ( m_pAudioEngine->isEndOfSongReached( pTransportPos ) ) {
3324 fTick = pPatternGroupVector->size();
3325 }
3326 else {
3327 fTick = 0;
3328 }
3329 }
3330 else if ( pPatternGroupVector->size() > m_nColumn &&
3331 pPatternGroupVector->at( m_nColumn )->size() > 0 ) {
3332 int nLength = pPatternGroupVector->at( m_nColumn )->longest_pattern_length();
3333 fTick += (float)pTransportPos->getPatternTickPosition() /
3334 (float)nLength;
3335 }
3336 else {
3337 // Empty column. Use the default length.
3338 fTick += (float)pTransportPos->getPatternTickPosition() /
3339 (float)MAX_NOTES;
3340 }
3341
3342 if ( fTick < 0 ) {
3343 // As some variables of the audio engine are initialized as or
3344 // reset to -1 we ensure this does not affect the position of
3345 // the playhead in the SongEditor.
3346 fTick = 0;
3347 }
3348 }
3349
3350 m_pAudioEngine->unlock();
3351
3352 if ( fTick != m_fTick ) {
3353 float fDiff = static_cast<float>(m_nGridWidth) * (fTick - m_fTick);
3354
3355 m_fTick = fTick;
3356 int nX = tickToColumn( m_fTick, m_nGridWidth );
3357
3358 QRect updateRect( nX -2, 0, 4 + Skin::nPlayheadWidth, height() );
3359 update( updateRect );
3360 if ( fDiff > 1.0 || fDiff < -1.0 ) {
3361 // New cursor is far enough away from the old one that the single update rect won't cover both. So
3362 // update at the old location as well.
3363 updateRect.translate( -fDiff, 0 );
3364 update( updateRect );
3365 }
3366
3367 auto pSongEditorPanel = HydrogenApp::get_instance()->getSongEditorPanel();
3368 if ( pSongEditorPanel != nullptr ) {
3369 pSongEditorPanel->getSongEditor()->updatePosition( fTick );
3370 pSongEditorPanel->getPlaybackTrackWaveDisplay()->updatePosition( fTick );
3371 pSongEditorPanel->getAutomationPathView()->updatePosition( fTick );
3372 }
3373 }
3374}
3375
3376int SongEditorPositionRuler::columnToX( int nColumn ) const {
3377 return SongEditor::nMargin + nColumn * static_cast<int>(m_nGridWidth);
3378}
3380 return static_cast<int>(
3381 ( static_cast<float>(nX) - static_cast<float>(SongEditor::nMargin)) /
3382 static_cast<float>(m_nGridWidth));
3383}
3384int SongEditorPositionRuler::tickToColumn( float fTick, uint nGridWidth ) {
3385 return static_cast<int>( static_cast<float>(SongEditor::nMargin) + 1 +
3386 fTick * static_cast<float>(nGridWidth) -
3387 static_cast<float>(Skin::nPlayheadWidth) / 2 );
3388}
3389
3390
3392{
3394 update();
3395}
3396
3398{
3399 if ( changes & ( H2Core::Preferences::Changes::Colors |
3401
3402 resize( SongEditor::nMargin +
3403 Preferences::get_instance()->getMaxBars() * m_nGridWidth, height() );
3405 update();
3406 }
3407}
3408
#define RIGHT_HERE
Macro intended to be used for the logging of the locking of the H2Core::AudioEngine.
Definition AudioEngine.h:61
#define WARNINGLOG(x)
Definition Object.h:241
#define ERRORLOG(x)
Definition Object.h:242
int operator<(QPoint a, QPoint b)
static const uint SONG_EDITOR_MAX_GRID_WIDTH
Definition SongEditor.h:55
static const uint SONG_EDITOR_MIN_GRID_WIDTH
Definition SongEditor.h:54
Drag scroller object.
Definition Selection.h:129
Compatibility class to support QDropEvent more esily in Qt5 and Qt6.
Definition DropEvent.h:35
Custom file dialog checking whether the user has write access to the selected folder before allowing ...
Definition FileDialog.h:34
@ Playing
Transport is rolling.
static EventQueue * get_instance()
Returns a pointer to the current EventQueue singleton stored in __instance.
Definition EventQueue.h:224
void push_event(const EventType type, const int nValue)
Queues the next event into the EventQueue.
static QString savePatternNew(const QString &fileName, Pattern *pattern, std::shared_ptr< Song > song, const QString &drumkitName)
save the given pattern to <user_data_path>/pattern/<drumkitName>/<fileName>.h2pattern will NOT overwr...
Definition Files.h:60
static QString savePatternOver(const QString &fileName, Pattern *pattern, std::shared_ptr< Song > song, const QString &drumkitName)
save the given pattern to <user_data_path>/pattern/<drumkitName>/<fileName>.h2pattern will overwrite ...
Definition Files.h:74
static QString savePatternTmp(const QString &fileName, Pattern *pattern, std::shared_ptr< Song > song, const QString &drumkitName)
save the given pattern under <Tmp_directory> with a unique filename built from <fileName> will overwr...
Definition Files.h:102
static const QString patterns_filter_name
Definition Filesystem.h:126
static bool dir_readable(const QString &path, bool silent=false)
returns true if the given path is a readable regular directory
static QString tmp_file_path(const QString &base)
touch a temporary file under tmp_dir() and return it's path.
static QString patterns_dir()
returns user patterns path
Hydrogen Audio Engine.
Definition Hydrogen.h:54
std::shared_ptr< Song > getSong() const
Get the current song.
Definition Hydrogen.h:123
std::shared_ptr< Timeline > getTimeline() const
Definition Hydrogen.h:644
static Hydrogen * get_instance()
Returns the current Hydrogen instance __instance.
Definition Hydrogen.h:84
void updateSelectedPattern(bool bNeedsLock=true)
Updates the selected pattern to the one recorded note will be inserted to.
Definition Hydrogen.cpp:903
void setIsModified(bool bIsModified)
Wrapper around Song::setIsModified() that checks whether a song is set.
bool isPatternEditorLocked() const
Convenience function checking whether using the Pattern Editor is locked in the song settings and the...
void setSelectedPatternNumber(int nPat, bool bNeedsLock=true, bool bForce=false)
Sets m_nSelectedPatternNumber.
Definition Hydrogen.cpp:915
PatternList is a collection of patterns.
Definition PatternList.h:43
bool check_name(QString patternName, Pattern *ignore=NULL)
check if a pattern with name patternName already exists in this list
void add(Pattern *pattern, bool bAddVirtuals=false)
add a pattern to the list
int index(const Pattern *pattern) const
get the index of the pattern within the patterns
Pattern * replace(int idx, Pattern *pattern)
replace the pattern at a given index with a new one
QString find_unused_pattern_name(QString sourceName, Pattern *ignore=NULL)
find unused patternName
Pattern * del(int idx)
remove the pattern at a given index, does not delete it
int longest_pattern_length(bool bIncludeVirtuals=true) const
Get the length of the longest pattern in the list.
void clear()
empty the pattern list
int size() const
returns the numbers of patterns
Pattern * get(int idx)
get a pattern from the list
Pattern class is a Note container.
Definition Pattern.h:46
void set_name(const QString &name)
get the name of the pattern
Definition Pattern.h:305
const QString & get_name() const
set the category of the pattern
Definition Pattern.h:310
const virtual_patterns_t * get_flattened_virtual_patterns() const
Definition Pattern.h:365
void set_info(const QString &info)
get the info of the pattern
Definition Pattern.h:315
static Pattern * load_file(const QString &sPpatternPath, std::shared_ptr< InstrumentList > pInstruments, bool bSilent=false)
load a pattern from a file
Definition Pattern.cpp:66
int get_length() const
set the denominator of the pattern
Definition Pattern.h:340
void set_category(const QString &category)
set the info of the pattern
Definition Pattern.h:325
Manager for User Preferences File (singleton)
Definition Preferences.h:79
unsigned getSongEditorGridHeight()
static Preferences * get_instance()
Returns a pointer to the current Preferences singleton stored in __instance.
const QString & getApplicationFontFamily() const
const std::shared_ptr< ColorTheme > getColorTheme() const
QString getLastOpenPatternDirectory() const
void setPunchInPos(unsigned pos)
int getMaxPatternColors() const
void setLastOpenPatternDirectory(QString sPath)
FontTheme::FontSize getFontSize() const
int getMaxBars() const
Changes
Bitwise or-able options showing which part of the Preferences were altered using the PreferencesDialo...
@ GeneralTab
Any option in the General tab appeared.
@ AppearanceTab
Any option in the Appearance tab excluding colors, font size, or font family.
@ Font
Either the font size or font family have changed.
@ Colors
At least one of the colors has changed.
void setPunchOutPos(unsigned pos)
unsigned getSongEditorGridWidth()
ActionMode
Defines the type of user interaction experienced in the SongEditor.
Definition Song.h:74
@ selectMode
Holding a pressed left mouse key will draw a rectangle to select a group of Notes.
Definition Song.h:77
@ drawMode
Holding a pressed left mouse key will draw/delete patterns in all grid cells encountered.
Definition Song.h:80
@ Stacked
An arbitrary number of pattern can be played.
Definition Song.h:103
XMLDoc is a subclass of QDomDocument with read and write methods.
Definition Xml.h:182
XMLNode set_root(const QString &node_name, const QString &xmlns=nullptr)
create the xml header and root node
Definition Xml.cpp:336
XMLNode is a subclass of QDomNode with read and write values methods.
Definition Xml.h:39
int read_int(const QString &node, int default_value, bool inexistent_ok=true, bool empty_ok=true, bool bSilent=false)
reads an integer stored into a child node
Definition Xml.cpp:151
XMLNode createNode(const QString &name)
create a new XMLNode that has to be appended into de XMLDoc
Definition Xml.cpp:44
void write_int(const QString &node, const int value)
write an integer into a child node
Definition Xml.cpp:265
void setHideKeyboardCursor(bool bHidden)
MainForm * getMainForm()
void addEventListener(EventListener *pListener)
static HydrogenApp * get_instance()
Returns the instance of HydrogenApp class.
QUndoStack * m_pUndoStack
PatternEditorPanel * getPatternEditorPanel()
void preferencesChanged(H2Core::Preferences::Changes changes)
Propagates a change in the Preferences through the GUI.
SongEditorPanel * getSongEditorPanel()
void action_file_export_pattern_as(int nPatternRow=-1)
Definition MainForm.cpp:975
Compatibility class to support QMouseEvent more esily in Qt5 and Qt6.
Definition MouseEvent.h:35
Pattern Fill Dialog.
Pattern Properties Dialog.
static constexpr int nPlayheadHeight
Definition Skin.h:78
Stacked
Definition Skin.h:84
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 void drawListBackground(QPainter *p, QRect rect, QColor background, bool bHovered)
Draws the background of a row in both the pattern list of the SongEditor and the instrument list in t...
Definition Skin.cpp:144
static void drawStackedIndicator(QPainter *p, int x, int y, Skin::Stacked stacked)
Definition Skin.cpp:223
static void setPlayheadPen(QPainter *p, bool bHovered=false)
Definition Skin.cpp:190
static QString getImagePath()
Definition Skin.h:36
static int getPlayheadShaftOffset()
Definition Skin.h:79
void updateAll()
Update and redraw all...
SongEditorPositionRuler * getSongEditorPositionRuler() const
void highlightPatternEditorLocked(bool bUseRedBackground)
Turns the background color of m_pPatternEditorLockedBtn red to signal the user her last action was no...
SongEditorPatternList * getSongEditorPatternList() const
virtual void timelineUpdateEvent(int nValue) override
void setRowSelection(RowSelection rowSelection)
QPixmap m_playingPattern_empty_Pixmap
Definition SongEditor.h:332
virtual void mousePressEvent(QMouseEvent *ev) override
Single click, select the next pattern.
QPixmap m_playingPattern_on_Pixmap
Definition SongEditor.h:330
QPixmap m_playingPattern_off_Pixmap
Definition SongEditor.h:331
QLineEdit * m_pLineEdit
Definition SongEditor.h:335
virtual void mouseDoubleClickEvent(QMouseEvent *ev) override
virtual void stackedModeActivationEvent(int nValue) override
virtual void leaveEvent(QEvent *ev)
void fillRangeWithPattern(FillRange *r, int nPattern)
H2Core::Pattern * m_pPatternBeingEdited
Definition SongEditor.h:336
virtual void mouseMoveEvent(QMouseEvent *event) override
void movePatternLine(int, int)
virtual void patternModifiedEvent() override
void onPreferencesChanged(H2Core::Preferences::Changes changes)
int m_nRowHovered
Specifies the row the mouse cursor is currently hovered over.
Definition SongEditor.h:358
void acceptPatternPropertiesDialogSettings(QString newPatternName, QString newPatternInfo, QString newPatternCategory, int patternNr)
virtual void songModeActivationEvent() override
void togglePattern(int)
Start/stop playing a pattern in "pattern mode".
H2Core::AudioEngine * m_pAudioEngine
Definition SongEditor.h:319
DragScroller * m_pDragScroller
Definition SongEditor.h:338
virtual void dragEnterEvent(QDragEnterEvent *event) override
drag & drop
void inlineEditPatternName(int row)
virtual void playingPatternsChangedEvent() override
QTimer * m_pHighlightLockedTimer
Definition SongEditor.h:360
SongEditorPatternList(QWidget *parent)
QPixmap * m_pBackgroundPixmap
Definition SongEditor.h:324
virtual void relocationEvent() override
virtual void paintEvent(QPaintEvent *ev) override
static constexpr uint m_nInitialHeight
Definition SongEditor.h:322
virtual void dropEvent(QDropEvent *event) override
virtual void selectedPatternChangedEvent() override
void revertPatternPropertiesDialogSettings(QString oldPatternName, QString oldPatternInfo, QString oldPatternCategory, int patternNr)
virtual void patternEditorLockedEvent() override
virtual void nextPatternsChangedEvent() override
H2Core::Hydrogen * m_pHydrogen
Definition SongEditor.h:318
virtual void timelineUpdateEvent(int nValue) override
virtual void mouseMoveEvent(QMouseEvent *ev) override
float m_fTick
Cached position of the playhead.
Definition SongEditor.h:429
virtual void timelineActivationEvent() override
virtual void mousePressEvent(QMouseEvent *ev) override
HoveredRow
Indicated the part of the widget the cursor is hovering over.
Definition SongEditor.h:414
@ TempoMarker
Upper half until the lower end of the tempo marker text.
Definition SongEditor.h:419
@ None
Cursor is not hovering the widget.
Definition SongEditor.h:416
@ Tag
Still part of the upper half, but only the last m_nTagHeight pixels.
Definition SongEditor.h:422
int columnToX(int nColumn) const
Calculates the position in pixel required to the painter for a particular nColumn of the grid.
int m_nColumn
Cached and coarsed-grained position of the playhead.
Definition SongEditor.h:431
void showTagWidget(int nColumn)
void drawTempoMarker(std::shared_ptr< const H2Core::Timeline::TempoMarker > pTempoMarker, bool bEmphasize, QPainter &painter)
static int tickToColumn(float fTick, uint nGridWidth)
void showBpmWidget(int nColumn)
virtual void patternModifiedEvent() override
void onPreferencesChanged(H2Core::Preferences::Changes changes)
virtual void songModeActivationEvent() override
H2Core::AudioEngine * m_pAudioEngine
Definition SongEditor.h:406
virtual void leaveEvent(QEvent *ev) override
void showToolTip(const QPoint &pos, const QPoint &globalPos)
virtual void tempoChangedEvent(int) override
virtual void mouseReleaseEvent(QMouseEvent *ev) override
virtual bool event(QEvent *ev) override
void setGridWidth(uint width)
int xToColumn(int nX) const
SongEditorPositionRuler(QWidget *parent)
virtual void songSizeChangedEvent() override
virtual void playingPatternsChangedEvent() override
virtual void updateSongEvent(int) override
QPixmap * m_pBackgroundPixmap
Definition SongEditor.h:438
virtual void relocationEvent() override
int m_nActiveColumns
Area covering the length of the song columns.
Definition SongEditor.h:436
static constexpr int m_nMinimumHeight
Definition SongEditor.h:393
virtual void paintEvent(QPaintEvent *ev) override
virtual void jackTimebaseStateChangedEvent(int nState) override
QRect calcTempoMarkerRect(std::shared_ptr< const H2Core::Timeline::TempoMarker > pTempoMarker, bool bEmphasize=false) const
H2Core::Hydrogen * m_pHydrogen
Definition SongEditor.h:405
virtual void mouseDragUpdateEvent(QMouseEvent *ev) override
virtual void mouseMoveEvent(QMouseEvent *ev) override
float m_fTick
Cached position of the playhead.
Definition SongEditor.h:229
void drawSequence()
virtual void keyReleaseEvent(QKeyEvent *ev) override
virtual void mousePressEvent(QMouseEvent *ev) override
virtual void updateModifiers(QInputEvent *ev)
int m_nCursorColumn
Definition SongEditor.h:167
virtual void selectionMoveEndEvent(QInputEvent *ev) override
void selectNone()
virtual QRect getKeyboardCursorRect() override
Calculate screen space occupied by keyboard cursor.
void drawPattern(int pos, int number, bool invertColour, double width)
QPoint columnRowToXy(QPoint p)
void scrolled(int)
void modifyPatternCellsAction(std::vector< QPoint > &addCells, std::vector< QPoint > &deleteCells, std::vector< QPoint > &selectCells)
Modify many pattern cells at once, for use in a single efficient undo/redo action.
QPoint m_currentMousePosition
Definition SongEditor.h:180
void updatePosition(float fTick)
QPixmap * m_pSequencePixmap
Definition SongEditor.h:161
bool m_bCopyNotMove
Definition SongEditor.h:135
void setPatternActive(int nColumn, int nRow, bool bActivate)
SongEditorPanel * m_pSongEditorPanel
Definition SongEditor.h:127
bool m_bSequenceChanged
Pattern sequence or selection has changed, so must be redrawn.
Definition SongEditor.h:145
int m_nMaxPatternColors
Definition SongEditor.h:137
QPoint xyToColumnRow(QPoint p)
virtual void patternModifiedEvent() override
void onPreferencesChanged(H2Core::Preferences::Changes changes)
void createBackground()
static constexpr int nMargin
Definition SongEditor.h:106
virtual std::vector< SelectionIndex > elementsIntersecting(QRect r) override
Find list of elements which intersect a rectangular area.
virtual void updateWidget() override
Selection or selection-related visual elements have changed, widget needs to be updated.
virtual void focusInEvent(QFocusEvent *ev) override
H2Core::AudioEngine * m_pAudioEngine
Definition SongEditor.h:130
bool m_bDrawingActiveCell
In "draw" mode, whether we're activating pattern cells ("drawing") or deactivating ("erasing") is set...
Definition SongEditor.h:142
unsigned m_nGridWidth
Definition SongEditor.h:133
virtual void leaveEvent(QEvent *ev) override
virtual void keyPressEvent(QKeyEvent *ev) override
virtual void mouseReleaseEvent(QMouseEvent *ev) override
void updateGridCells()
QScrollArea * m_pScrollView
Definition SongEditor.h:126
QMenu * m_pPopupMenu
Definition SongEditor.h:147
void setGridWidth(uint width)
virtual void mouseDragStartEvent(QMouseEvent *ev) override
unsigned m_nGridHeight
Definition SongEditor.h:132
void copy()
Copy a selection of cells to an XML representation in the clipboard.
virtual void focusOutEvent(QFocusEvent *ev) override
Selection< QPoint > m_selection
Definition SongEditor.h:124
void selectAll()
int m_nCursorRow
Definition SongEditor.h:166
SongEditor(QWidget *parent, QScrollArea *pScrollView, SongEditorPanel *pSongEditorPanel)
int yScrollTarget(QScrollArea *pScrollArea, int *pnPatternInView)
Calculate a target Y scroll value for tracking a playing song.
QPixmap * m_pBackgroundPixmap
Definition SongEditor.h:160
QPoint m_previousMousePosition
Mouse position during selection gestures (used to detect crossing cell boundaries)
Definition SongEditor.h:180
std::map< QPoint, GridCell > m_gridCells
Definition SongEditor.h:220
void deleteSelection()
virtual void mouseDragEndEvent(QMouseEvent *ev) override
virtual void relocationEvent() override
void clearThePatternSequenceVector(QString filename)
static constexpr int m_nMinimumHeight
Default value of Preferences::m_nSongEditorGridHeight * 5 (patterns)
Definition SongEditor.h:109
virtual void mouseClickEvent(QMouseEvent *ev) override
virtual void paintEvent(QPaintEvent *ev) override
int getGridWidth()
void togglePatternActive(int nColumn, int nRow)
void drawFocus(QPainter &painter)
void invalidateBackground()
virtual void patternEditorLockedEvent() override
QPoint m_previousGridOffset
Definition SongEditor.h:180
virtual void enterEvent(QEvent *ev) override
H2Core::Hydrogen * m_pHydrogen
Definition SongEditor.h:129
QPoint movingGridOffset() const
Quantise the selection move offset to the sequence grid.
bool m_bEntered
Definition SongEditor.h:222
bool m_bBackgroundInvalid
Definition SongEditor.h:149
void updateEditorandSetTrue()
Virtual Pattern Dialog.
int m_nRowClicked
Helper variable remembering for row was clicked last.
RowSelection m_rowSelection
Determines the highlighting of the row associated with m_nRowClicked.
RowSelection
Specifies whether the row corresponding to m_nRowClicked should be highlighted and determines the lif...
@ Popup
The m_nRowClicked row was right-clicked and a popup dialog did open and is still shown.
@ None
No highlighting will be drawn for the row last clicked.
@ Dialog
The popup dialog is already closed but the user clicked an associated action and its dialog is still ...
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
@ EVENT_PATTERN_MODIFIED
A pattern was added, deleted, or modified.
Definition EventQueue.h:68