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