diff --git a/ChangeLog b/ChangeLog index 0d78982f3f..1ef41ca159 100644 --- a/ChangeLog +++ b/ChangeLog @@ -83,6 +83,35 @@ - The tempo used left of the first tempo marker is painted in a darker color. - The currently used Tempo Marker gets highlighted. + - Tags have been moved into Timeline (next to the Tempo Markers) + in order to make room to accommodate the cursor in the ruler to + highlight the current position + - Tags can be inserted by left-clicking the bottom area of the + Timeline (above the ruler). + - Clicking the ruler is now always enabled and automatically + switches transport into Song Mode. + - Full-size playhead + - The icons in the pattern list indicating whether a pattern is + playing in stacked pattern mode are now colored and can have four + different states: on, off, off next (pattern is played till the + end and then turned off), and on next (pattern is played as soon + as transport is looped again) + * PatternEditor UX tweaks: + - Relocating transport by clicking the ruler is now supported + (like in the SongEditor) and automatically switches transport + into Pattern Mode. + - Full-size playhead + - The ruler was decoupled from the currently selected pattern. It + always has the size of the largest playing pattern and always + shows the transport position using a playhead. Whether or not the + current pattern is played back is indicated by a full-height + cursor. + - All note properties except of the note key can now be altered in + both the drum pattern editor and the piano roll editor by + right-clicking and dragging a note. + - All notes of the currently playing patterns will be hinted in + stacked pattern mode. Even those exceeding the length of the + current pattern. * OSC API - Add command /Hydrogen/LOAD_DRUMKIT - Add command /Hydrogen/UPGRADE_DRUMKIT diff --git a/data/img/gray/patternEditor/instrument_line.png b/data/img/gray/patternEditor/instrument_line.png deleted file mode 100644 index 827da4d207..0000000000 Binary files a/data/img/gray/patternEditor/instrument_line.png and /dev/null differ diff --git a/data/img/gray/patternEditor/instrument_line_selected.png b/data/img/gray/patternEditor/instrument_line_selected.png deleted file mode 100644 index cc137184f9..0000000000 Binary files a/data/img/gray/patternEditor/instrument_line_selected.png and /dev/null differ diff --git a/data/img/gray/patternEditor/tickPosition.png b/data/img/gray/patternEditor/tickPosition.png deleted file mode 100644 index 955f8710cc..0000000000 Binary files a/data/img/gray/patternEditor/tickPosition.png and /dev/null differ diff --git a/data/img/gray/songEditor/playingPattern_empty.png b/data/img/gray/songEditor/playingPattern_empty.png deleted file mode 100644 index 7929d92a4d..0000000000 Binary files a/data/img/gray/songEditor/playingPattern_empty.png and /dev/null differ diff --git a/data/img/gray/songEditor/playingPattern_off.png b/data/img/gray/songEditor/playingPattern_off.png deleted file mode 100644 index c2851e48b6..0000000000 Binary files a/data/img/gray/songEditor/playingPattern_off.png and /dev/null differ diff --git a/data/img/gray/songEditor/playingPattern_on.png b/data/img/gray/songEditor/playingPattern_on.png deleted file mode 100644 index 7cb1b37ee1..0000000000 Binary files a/data/img/gray/songEditor/playingPattern_on.png and /dev/null differ diff --git a/data/img/gray/songEditor/songEditorLabelABG.png b/data/img/gray/songEditor/songEditorLabelABG.png deleted file mode 100644 index defd659abf..0000000000 Binary files a/data/img/gray/songEditor/songEditorLabelABG.png and /dev/null differ diff --git a/data/img/gray/songEditor/songEditorLabelBG.png b/data/img/gray/songEditor/songEditorLabelBG.png deleted file mode 100644 index b7bbd6cc77..0000000000 Binary files a/data/img/gray/songEditor/songEditorLabelBG.png and /dev/null differ diff --git a/data/img/gray/songEditor/songEditorLabelSBG.png b/data/img/gray/songEditor/songEditorLabelSBG.png deleted file mode 100644 index 5404e205c1..0000000000 Binary files a/data/img/gray/songEditor/songEditorLabelSBG.png and /dev/null differ diff --git a/data/img/scalable/icons/black/lock_closed.svg b/data/img/scalable/icons/black/lock_closed.svg new file mode 100644 index 0000000000..d960590537 --- /dev/null +++ b/data/img/scalable/icons/black/lock_closed.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/data/img/scalable/icons/black/lock_open.svg b/data/img/scalable/icons/black/lock_open.svg new file mode 100644 index 0000000000..45d2fefe69 --- /dev/null +++ b/data/img/scalable/icons/black/lock_open.svg @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/data/img/scalable/icons/white/lock_closed.svg b/data/img/scalable/icons/white/lock_closed.svg new file mode 100644 index 0000000000..47ce2fe8f4 --- /dev/null +++ b/data/img/scalable/icons/white/lock_closed.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/data/img/scalable/icons/white/lock_open.svg b/data/img/scalable/icons/white/lock_open.svg new file mode 100644 index 0000000000..2ffedd84c2 --- /dev/null +++ b/data/img/scalable/icons/white/lock_open.svg @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/data/themes/default.h2theme b/data/themes/default.h2theme index 4cb3447751..aa298915da 100644 --- a/data/themes/default.h2theme +++ b/data/themes/default.h2theme @@ -1,31 +1,43 @@ - 1.1.0-'4130cbf4' + 1.1.1-'4228e825' - 95,101,117 - 128,134,152 - 128,134,152 - 72,76,88 - 196,201,214 + 128,134,152 + 106,111,126 + 149,157,178 + 0,0,0 + 54,57,67 + 206,211,224 + 83,89,103 + 45,66,89 + 255,255,255 + 127,159,127 + 240,223,175 + 247,100,100 - 167,168,163 - 167,168,163 - 207,208,200 - 40,40,40 - 40,40,40 - 0,0,0 - 65,65,65 - 75,75,75 - 95,95,95 - 115,115,115 - 125,125,125 - 135,135,135 + 165,166,160 + 133,134,129 + 194,195,187 + 0,0,0 + 193,194,186 + 240,240,240 + 247,100,100 + 40,40,40 + 89,131,175 + 255,255,255 + 0,0,0 + 45,45,45 + 55,55,55 + 75,75,75 + 95,95,95 + 105,105,105 + 115,115,115 - 0,0,255 - 85,85,85 + 255,255,255 + 199,199,199 58,62,72 @@ -46,16 +58,16 @@ 64,64,66 - 95,143,182 - 0,0,0 + 67,96,131 + 255,255,255 164,170,190 - 0,0,0 + 10,10,10 247,100,100 10,10,10 51,74,100 240,240,240 - 52,76,100 - 255,255,255 + 0,0,0 + 38,39,44 @@ -66,23 +78,23 @@ 1.1 1 67,96,131 - 67,96,131 - 67,96,131 - 67,96,131 - 67,96,131 - 67,96,131 - 67,96,131 - 67,96,131 - 67,96,131 - 67,96,131 - 67,96,131 - 67,96,131 - 67,96,131 - 67,96,131 - 67,96,131 - 67,96,131 - 67,96,131 - 67,96,131 + 67,114,175 + 105,165,189 + 148,189,202 + 193,211,215 + 229,201,130 + 234,160,90 + 218,97,60 + 186,55,50 + 152,22,26 + 186,55,50 + 218,97,60 + 234,160,90 + 229,201,130 + 193,211,215 + 148,189,202 + 105,165,189 + 67,114,175 67,96,131 67,96,131 67,96,131 @@ -115,12 +127,12 @@ 67,96,131 67,96,131 67,96,131 - 1 + 18 - Lucida Grande - Lucida Grande - Lucida Grande + DejaVu Sans + DejaVu Sans + DejaVu Sans 0 diff --git a/src/core/AudioEngine/AudioEngine.cpp b/src/core/AudioEngine/AudioEngine.cpp index e997de446a..52526efbc3 100644 --- a/src/core/AudioEngine/AudioEngine.cpp +++ b/src/core/AudioEngine/AudioEngine.cpp @@ -111,6 +111,7 @@ AudioEngine::AudioEngine() , m_pMetronomeInstrument( nullptr ) , m_nPatternStartTick( 0 ) , m_nPatternTickPosition( 0 ) + , m_nPatternSize( MAX_NOTES ) , m_fSongSizeInTicks( 0 ) , m_nRealtimeFrames( 0 ) , m_nAddRealtimeNoteTickPosition( 0 ) @@ -126,7 +127,6 @@ AudioEngine::AudioEngine() , m_currentTickTime( {0,0}) , m_fTickMismatch( 0 ) , m_fLastTickIntervalEnd( -1 ) - , m_nLastPlayingPatternsColumn( -1 ) , m_nFrameOffset( 0 ) , m_fTickOffset( 0 ) { @@ -338,12 +338,10 @@ void AudioEngine::reset( bool bWithJackBroadcast ) { m_nFrameOffset = 0; m_fTickOffset = 0; m_fLastTickIntervalEnd = -1; - m_nLastPlayingPatternsColumn = -1; updateBpmAndTickSize(); clearNoteQueue(); - #ifdef H2CORE_HAVE_JACK if ( pHydrogen->haveJackTransport() && bWithJackBroadcast ) { @@ -429,11 +427,7 @@ void AudioEngine::locate( const double fTick, bool bWithJackBroadcast ) { #endif setFrames( nNewFrame ); - updateTransportPosition( fTick, pHydrogen->getSong()->isLoopEnabled() ); - - // Ensure the playing patterns are updated regardless of whether - // transport is rolling or not. - updatePlayingPatterns( getColumn(), getTick(), getPatternStartTick() ); + updateTransportPosition( fTick ); } void AudioEngine::locateToFrame( const long long nFrame ) { @@ -444,11 +438,7 @@ void AudioEngine::locateToFrame( const long long nFrame ) { double fNewTick = computeTickFromFrame( nFrame ); - updateTransportPosition( fNewTick, pHydrogen->getSong()->isLoopEnabled() ); - - // Ensure the playing patterns are updated regardless of whether - // transport is rolling or not. - updatePlayingPatterns( getColumn(), getTick(), getPatternStartTick() ); + updateTransportPosition( fNewTick ); } void AudioEngine::incrementTransportPosition( uint32_t nFrames ) { @@ -470,10 +460,10 @@ void AudioEngine::incrementTransportPosition( uint32_t nFrames ) { // .arg( fNewTick, 0, 'f' ) // .arg( getTickSize(), 0, 'f' ) ); - updateTransportPosition( fNewTick, pSong->isLoopEnabled() ); + updateTransportPosition( fNewTick ); } -void AudioEngine::updateTransportPosition( double fTick, bool bUseLoopMode ) { +void AudioEngine::updateTransportPosition( double fTick ) { const auto pHydrogen = Hydrogen::get_instance(); const auto pSong = pHydrogen->getSong(); @@ -487,67 +477,23 @@ void AudioEngine::updateTransportPosition( double fTick, bool bUseLoopMode ) { // .arg( m_nPatternStartTick ) // .arg( m_nColumn ) // .arg( getFrames() ) ); - - setTick( fTick ); - // Column, tick since the beginning of the current pattern, and - // number of ticks till be the beginning of the current pattern - // corresponding to nTick. - int nNewColumn; + // Update m_nPatternStartTick, m_nPatternTickPosition, and + // m_nPatternSize. if ( pHydrogen->getMode() == Song::Mode::Song ) { + updateSongTransportPosition( fTick ); + } + else if ( pHydrogen->getMode() == Song::Mode::Pattern ) { - nNewColumn = pHydrogen->getColumnForTick( std::floor( fTick ), - bUseLoopMode, - &m_nPatternStartTick ); - - if ( fTick > m_fSongSizeInTicks && - m_fSongSizeInTicks != 0 ) { - // When using the JACK audio driver the overall - // transport position will be managed by an external - // server. Since it is agnostic of all the looping in - // its clients, it will only increment time and - // Hydrogen has to take care of the looping itself. - m_nPatternTickPosition = - std::fmod( std::floor( fTick ) - m_nPatternStartTick, - m_fSongSizeInTicks ); - } else { - m_nPatternTickPosition = std::floor( fTick ) - m_nPatternStartTick; - } - - if ( m_nColumn != nNewColumn ) { - m_nLastPlayingPatternsColumn = m_nColumn; - setColumn( nNewColumn ); - } - - } else if ( pHydrogen->getMode() == Song::Mode::Pattern ) { - - int nPatternSize; - if ( m_pPlayingPatterns->size() != 0 ) { - nPatternSize = m_pPlayingPatterns->longest_pattern_length(); - } else { - nPatternSize = MAX_NOTES; - } - - // Transport went past the end of the pattern or Pattern mode - // was just activated. - if ( fTick >= m_nPatternStartTick + nPatternSize ) { - if ( nPatternSize > 0 ) { - m_nPatternStartTick += - std::floor( ( std::floor( fTick ) - m_nPatternStartTick ) / - nPatternSize ) * nPatternSize; - } else { - m_nPatternStartTick = std::floor( fTick ); - } - } - - m_nPatternTickPosition = static_cast(std::floor( fTick )) - - m_nPatternStartTick; - if ( nPatternSize > 0 && m_nPatternTickPosition > nPatternSize ) { - m_nPatternTickPosition = ( static_cast(std::floor( fTick )) - - m_nPatternStartTick ) % - nPatternSize; + // If the transport is rolling, pattern tick variables were + // already updated in the call to updateNoteQueue. + if ( getState() != State::Playing ) { + updatePatternTransportPosition( fTick ); } } + + setTick( fTick ); + updateBpmAndTickSize(); // WARNINGLOG( QString( "[After] frame: %5, tick: %1, pTickPos: %2, pStartPos: %3, column: %4" ) @@ -559,6 +505,88 @@ void AudioEngine::updateTransportPosition( double fTick, bool bUseLoopMode ) { } +void AudioEngine::updatePatternTransportPosition( double fTick ) { + + auto pHydrogen = Hydrogen::get_instance(); + + // In selected pattern mode we update the pattern size _before_ + // checking the whether transport reached the end of a + // pattern. This way when switching from a shorter to one double + // its size transport will just continue till the end of the new, + // longer one. If we would not update pattern size, transport + // would be looped at half the length of the newly selected + // pattern, which looks like a glitch. + // + // The update of the playing pattern is done asynchronous in + // Hydrogen::setSelectedPatternNumber() and does not have to + // queried in here in each run. + // if ( pHydrogen->getPatternMode() == Song::PatternMode::Selected ) { + // updatePlayingPatterns( 0, fTick ); + // } + + // Transport went past the end of the pattern or Pattern mode + // was just activated. + if ( fTick >= static_cast(m_nPatternStartTick + m_nPatternSize) || + fTick < static_cast(m_nPatternStartTick) ) { + m_nPatternStartTick += + static_cast(std::floor( ( fTick - + static_cast(m_nPatternStartTick) ) / + static_cast(m_nPatternSize) )) * + m_nPatternSize; + + // In stacked pattern mode we will only update the playing + // patterns if the transport of the original pattern is + // looped. This way all patterns start fresh at the beginning. + if ( pHydrogen->getPatternMode() == Song::PatternMode::Stacked ) { + // Updates m_nPatternSize. + updatePlayingPatterns( 0, fTick ); + } + } + + m_nPatternTickPosition = static_cast(std::floor( fTick )) - + m_nPatternStartTick; + if ( m_nPatternTickPosition > m_nPatternSize ) { + m_nPatternTickPosition = ( static_cast(std::floor( fTick )) + - m_nPatternStartTick ) % + m_nPatternSize; + } +} + +void AudioEngine::updateSongTransportPosition( double fTick ) { + + auto pHydrogen = Hydrogen::get_instance(); + const auto pSong = pHydrogen->getSong(); + + if ( fTick < 0 ) { + ERRORLOG( QString( "Provided tick [%1] is negative!" ) + .arg( fTick, 0, 'f' ) ); + return; + } + + int nNewColumn = pHydrogen->getColumnForTick( std::floor( fTick ), + pSong->isLoopEnabled(), + &m_nPatternStartTick ); + + // While the current tick position is constantly increasing, + // m_nPatternStartTick is only defined between 0 and + // m_fSongSizeInTicks. We will take care of the looping next. + if ( fTick > m_fSongSizeInTicks && + m_fSongSizeInTicks != 0 ) { + + m_nPatternTickPosition = + std::fmod( std::floor( fTick ) - m_nPatternStartTick, + m_fSongSizeInTicks ); + } + else { + m_nPatternTickPosition = std::floor( fTick ) - m_nPatternStartTick; + } + + if ( m_nColumn != nNewColumn ) { + setColumn( nNewColumn ); + updatePlayingPatterns( nNewColumn, 0 ); + } +} + void AudioEngine::updateBpmAndTickSize() { if ( ! ( m_state == State::Playing || m_state == State::Ready || @@ -1350,6 +1378,36 @@ void AudioEngine::raiseError( unsigned nErrorCode ) m_pEventQueue->push_event( EVENT_ERROR, nErrorCode ); } +void AudioEngine::handleSelectedPattern() { + // Expects the AudioEngine being locked. + + auto pHydrogen = Hydrogen::get_instance(); + auto pSong = pHydrogen->getSong(); + if ( pHydrogen->isPatternEditorLocked() ) { + + int nColumn = m_nColumn; + if ( m_nColumn == -1 ) { + nColumn = 0; + } + + auto pPatternList = pSong->getPatternList(); + auto pColumn = ( *pSong->getPatternGroupVector() )[ nColumn ]; + + int nPatternNumber = -1; + + int nIndex; + for ( const auto& pattern : *pColumn ) { + nIndex = pPatternList->index( pattern ); + + if ( nIndex > nPatternNumber ) { + nPatternNumber = nIndex; + } + } + + pHydrogen->setSelectedPatternNumber( nPatternNumber, true ); + } +} + void AudioEngine::processPlayNotes( unsigned long nframes ) { Hydrogen* pHydrogen = Hydrogen::get_instance(); @@ -1543,7 +1601,6 @@ int AudioEngine::audioEngine_process( uint32_t nframes, void* /*arg*/ ) // was started or stopped by the user. if ( pAudioEngine->getNextState() == State::Playing ) { if ( pAudioEngine->getState() == State::Ready ) { - DEBUGLOG("set playing"); pAudioEngine->startPlayback(); } @@ -1727,9 +1784,13 @@ void AudioEngine::setSong( std::shared_ptr pNewSong ) setupLadspaFX(); } - // find the first pattern and set as current + // find the first pattern and set as current since we start in + // pattern mode. if ( pNewSong->getPatternList()->size() > 0 ) { m_pPlayingPatterns->add( pNewSong->getPatternList()->get( 0 ) ); + m_nPatternSize = m_pPlayingPatterns->longest_pattern_length(); + } else { + m_nPatternSize = MAX_NOTES; } Hydrogen::get_instance()->renameJackPorts( pNewSong ); @@ -1743,6 +1804,7 @@ void AudioEngine::setSong( std::shared_ptr pNewSong ) locate( 0 ); Hydrogen::get_instance()->setTimeline( pNewSong->getTimeline() ); + Hydrogen::get_instance()->getTimeline()->activate(); this->unlock(); } @@ -1784,6 +1846,18 @@ void AudioEngine::updateSongSize() { return; } + if ( m_pPlayingPatterns->size() > 0 ) { + m_nPatternSize = m_pPlayingPatterns->longest_pattern_length(); + } else { + m_nPatternSize = MAX_NOTES; + } + + EventQueue::get_instance()->push_event( EVENT_SONG_SIZE_CHANGED, 0 ); + + if ( pHydrogen->getMode() == Song::Mode::Pattern ) { + return; + } + double fNewSongSizeInTicks = static_cast( pSong->lengthInTicks() ); // WARNINGLOG( QString( "[Before] frame: %1, bpm: %2, tickSize: %3, column: %4, tick: %5, mod(tick): %6, pTickPos: %7, pStartPos: %8, m_fLastTickIntervalEnd: %9, m_fSongSizeInTicks: %10" ) @@ -1880,8 +1954,7 @@ void AudioEngine::updateSongSize() { // After tick and frame information as well as notes are updated // we will make the remainder of the transport information // consistent. - updateTransportPosition( getDoubleTick(), - pSong->isLoopEnabled() ); + updateTransportPosition( getDoubleTick() ); if ( m_nColumn == -1 ) { stop(); @@ -1897,15 +1970,16 @@ void AudioEngine::updateSongSize() { } -void AudioEngine::flushPlayingPatterns() { - m_pPlayingPatterns->clear(); +void AudioEngine::removePlayingPattern( int nIndex ) { + m_pPlayingPatterns->del( nIndex ); } -void AudioEngine::updatePlayingPatterns( int nColumn, long nTick, long nPatternStartTick ) { +void AudioEngine::updatePlayingPatterns( int nColumn, long nTick ) { auto pHydrogen = Hydrogen::get_instance(); auto pSong = pHydrogen->getSong(); if ( pHydrogen->getMode() == Song::Mode::Song ) { + // Called when transport enteres a new column. m_pPlayingPatterns->clear(); if ( nColumn < 0 || nColumn >= pSong->getPatternGroupVector()->size() ) { @@ -1913,61 +1987,81 @@ void AudioEngine::updatePlayingPatterns( int nColumn, long nTick, long nPatternS } for ( const auto& ppattern : *( *( pSong->getPatternGroupVector() ) )[ nColumn ] ) { - m_pPlayingPatterns->add( ppattern ); - ppattern->addFlattenedVirtualPatterns( m_pPlayingPatterns ); + if ( ppattern != nullptr ) { + m_pPlayingPatterns->add( ppattern ); + ppattern->addFlattenedVirtualPatterns( m_pPlayingPatterns ); + } + } + + if ( m_pPlayingPatterns->size() > 0 ) { + m_nPatternSize = m_pPlayingPatterns->longest_pattern_length(); + } else { + m_nPatternSize = MAX_NOTES; } + EventQueue::get_instance()->push_event( EVENT_PATTERN_CHANGED, 0 ); - - } else if ( pHydrogen->getMode() == Song::Mode::Pattern ) { - if ( Preferences::get_instance()->patternModePlaysSelected() ) { - auto pSelectedPattern = - pSong->getPatternList()->get( pHydrogen->getSelectedPatternNumber() ); - if ( m_pPlayingPatterns->size() != 1 || - ( m_pPlayingPatterns->size() == 1 && - m_pPlayingPatterns->get( 0 ) != pSelectedPattern ) ) { - - m_pPlayingPatterns->clear(); + } + else if ( pHydrogen->getPatternMode() == Song::PatternMode::Selected ) { + // Called asynchronous when a different pattern number + // gets selected or the user switches from stacked into + // selected pattern mode. + + auto pSelectedPattern = + pSong->getPatternList()->get( pHydrogen->getSelectedPatternNumber() ); + if ( m_pPlayingPatterns->size() != 1 || + ( m_pPlayingPatterns->size() == 1 && + m_pPlayingPatterns->get( 0 ) != pSelectedPattern ) ) { + + m_pPlayingPatterns->clear(); + + if ( pSelectedPattern != nullptr ) { m_pPlayingPatterns->add( pSelectedPattern ); pSelectedPattern->addFlattenedVirtualPatterns( m_pPlayingPatterns ); - - EventQueue::get_instance()->push_event( EVENT_PATTERN_CHANGED, 0 ); } - } else { - // Stacked pattern mode - int nPatternSize; - if ( m_pPlayingPatterns->size() != 0 ) { - nPatternSize = m_pPlayingPatterns->longest_pattern_length(); + if ( m_pPlayingPatterns->size() > 0 ) { + m_nPatternSize = m_pPlayingPatterns->longest_pattern_length(); } else { - nPatternSize = 0; + m_nPatternSize = MAX_NOTES; } + + EventQueue::get_instance()->push_event( EVENT_PATTERN_CHANGED, 0 ); + } + } + else if ( pHydrogen->getPatternMode() == Song::PatternMode::Stacked ) { - if ( nPatternSize == 0 || - nTick >= nPatternStartTick + nPatternSize ) { - - if ( m_pNextPatterns->size() > 0 ) { - for ( const auto& ppattern : *m_pNextPatterns ) { - // If provided pattern is not part of the - // list, a nullptr will be returned. Else, a - // pointer to the deleted pattern will be - // returned. - if ( ( m_pPlayingPatterns->del( ppattern ) ) == nullptr ) { - // pPattern was not present yet. It will - // be added. - m_pPlayingPatterns->add( ppattern ); - ppattern->addFlattenedVirtualPatterns( m_pPlayingPatterns ); - } else { - // pPattern was already present. It will - // be deleted. - ppattern->removeFlattenedVirtualPatterns( m_pPlayingPatterns ); - } - EventQueue::get_instance()->push_event( EVENT_PATTERN_CHANGED, 0 ); - } - m_pNextPatterns->clear(); + if ( m_pNextPatterns->size() > 0 ) { + + for ( const auto& ppattern : *m_pNextPatterns ) { + // If provided pattern is not part of the + // list, a nullptr will be returned. Else, a + // pointer to the deleted pattern will be + // returned. + if ( ppattern == nullptr ) { + continue; + } + + if ( ( m_pPlayingPatterns->del( ppattern ) ) == nullptr ) { + // pPattern was not present yet. It will + // be added. + m_pPlayingPatterns->add( ppattern ); + ppattern->addFlattenedVirtualPatterns( m_pPlayingPatterns ); + } else { + // pPattern was already present. It will + // be deleted. + ppattern->removeFlattenedVirtualPatterns( m_pPlayingPatterns ); } + EventQueue::get_instance()->push_event( EVENT_PATTERN_CHANGED, 0 ); + } + m_pNextPatterns->clear(); + + if ( m_pPlayingPatterns->size() != 0 ) { + m_nPatternSize = m_pPlayingPatterns->longest_pattern_length(); + } else { + m_nPatternSize = MAX_NOTES; } - } // Stacked mode - } // Pattern mode + } + } } void AudioEngine::toggleNextPattern( int nPatternNumber ) { @@ -2210,17 +2304,6 @@ int AudioEngine::updateNoteQueue( unsigned nFrames ) return 0; } - // Use local representations of the current transport position in - // order for it to not get into a dirty state. - int nColumn = m_nColumn; - - // Since we deal with a lookahead the pattern start and tick - // position in the loop below are _not_ the same as the ones of - // the audio engine itself. We set the pattern start tick to -1 to - // signal the code below its state is dirty and needs to be - // updated. This is more efficient than updating it in every iteration. - long nPatternStartTick = -1; - long nPatternTickPosition = -1; long long nNoteStart; float fUsedTickSize; @@ -2246,31 +2329,18 @@ int AudioEngine::updateNoteQueue( unsigned nFrames ) stop(); return -1; } - - nColumn = pHydrogen->getColumnForTick( nnTick, - pSong->isLoopEnabled(), - &nPatternStartTick ); - if ( nnTick >= std::floor( m_fSongSizeInTicks ) && - std::floor( m_fSongSizeInTicks ) != 0 ) { - // When using the JACK audio driver the overall - // transport position will be managed by an external - // server. Since it is agnostic of all the looping in - // its clients, it will only increment time and - // Hydrogen has to take care of the looping itself. - nPatternTickPosition = ( nnTick - nPatternStartTick ) - % static_cast(std::floor( m_fSongSizeInTicks )); - } else { - nPatternTickPosition = nnTick - nPatternStartTick; - } + int nOldColumn = m_nColumn; + + updateSongTransportPosition( static_cast(nnTick) ); // If no pattern list could not be found, either choose // the first one if loop mode is activate or the // function returns indicating that the end of the song is // reached. - if ( nColumn == -1 || + if ( m_nColumn == -1 || ( pSong->getLoopMode() == Song::LoopMode::Finishing && - nColumn < m_nLastPlayingPatternsColumn ) ) { + m_nColumn < nOldColumn ) ) { INFOLOG( "End of Song" ); if( pHydrogen->getMidiOutput() != nullptr ){ @@ -2279,87 +2349,31 @@ int AudioEngine::updateNoteQueue( unsigned nFrames ) return -1; } - - if ( nColumn != m_nLastPlayingPatternsColumn ) { - updatePlayingPatterns( nColumn ); - m_nLastPlayingPatternsColumn = nColumn; - } } ////////////////////////////////////////////////////////////// // PATTERN MODE else if ( pHydrogen->getMode() == Song::Mode::Pattern ) { - int nPatternSize = -1; - if ( nPatternStartTick == -1 ) { - if ( m_pPlayingPatterns->size() != 0 ) { - nPatternSize = m_pPlayingPatterns->longest_pattern_length(); - } else { - nPatternSize = MAX_NOTES; - } + updatePatternTransportPosition( nnTick ); - if ( nPatternSize > 0 ) { - nPatternStartTick = - std::floor( static_cast(nnTick - - std::max( nPatternStartTick, - static_cast(0) )) / - static_cast(nPatternSize) ) * nPatternSize; - } else { - nPatternStartTick = nnTick; - } - } - - updatePlayingPatterns( 0, nnTick, nPatternStartTick ); - - if ( nPatternSize == -1 || - nPatternSize != m_pPlayingPatterns->longest_pattern_length() ) { - - if ( m_pPlayingPatterns->size() != 0 ) { - nPatternSize = m_pPlayingPatterns->longest_pattern_length(); - } else { - nPatternSize = MAX_NOTES; - } - - if ( nPatternSize == 0 ) { - ERRORLOG( "nPatternSize == 0" ); - } - - if ( nPatternStartTick == -1 || - nnTick >= nPatternStartTick + nPatternSize ) { - if ( nPatternSize > 0 ) { - nPatternStartTick += - std::floor( static_cast(nnTick - - std::max( nPatternStartTick, - static_cast(0) )) / - static_cast(nPatternSize) ) * nPatternSize; - } else { - nPatternStartTick = nnTick; - } - } - } - - nPatternTickPosition = nnTick - nPatternStartTick; - if ( nPatternSize > 0 && nPatternTickPosition > nPatternSize ) { - nPatternTickPosition = ( nnTick - nPatternStartTick ) % - nPatternSize; - } + // DEBUGLOG( QString( "[post] nnTick: %1, m_nPatternTickPosition: %2, m_nPatternStartTick: %3, m_nPatternSize: %4" ) + // .arg( nnTick ).arg( m_nPatternTickPosition ) + // .arg( m_nPatternStartTick ).arg( m_nPatternSize ) ); - // DEBUGLOG( QString( "[post] nnTick: %1, nPatternTickPosition: %2, nPatternStartTick: %3, nPatternSize: %4" ) - // .arg( nnTick ).arg( nPatternTickPosition ) - // .arg( nPatternStartTick ).arg( nPatternSize ) ); } ////////////////////////////////////////////////////////////// // Metronome // Only trigger the metronome at a predefined rate. - if ( nPatternTickPosition % 48 == 0 ) { + if ( m_nPatternTickPosition % 48 == 0 ) { float fPitch; float fVelocity; // Depending on whether the metronome beat will be issued // at the beginning or in the remainder of the pattern, // two different sounds and events will be used. - if ( nPatternTickPosition == 0 ) { + if ( m_nPatternTickPosition == 0 ) { fPitch = 3; fVelocity = 1.0; EventQueue::get_instance()->push_event( EVENT_METRONOME, 1 ); @@ -2408,7 +2422,7 @@ int AudioEngine::updateNoteQueue( unsigned nFrames ) // Loop over all notes at tick nPatternTickPosition // (associated tick is determined by Note::__position // at the time of insertion into the Pattern). - FOREACH_NOTE_CST_IT_BOUND(notes,it,nPatternTickPosition) { + FOREACH_NOTE_CST_IT_BOUND(notes, it, m_nPatternTickPosition) { Note *pNote = it->second; if ( pNote ) { pNote->set_just_recorded( false ); @@ -2421,8 +2435,8 @@ int AudioEngine::updateNoteQueue( unsigned nFrames ) /** Swing 16ths // * delay the upbeat 16th-notes by a constant (manual) offset */ - if ( ( ( nPatternTickPosition % ( MAX_NOTES / 16 ) ) == 0 ) - && ( ( nPatternTickPosition % ( MAX_NOTES / 8 ) ) != 0 ) + if ( ( ( m_nPatternTickPosition % ( MAX_NOTES / 16 ) ) == 0 ) + && ( ( m_nPatternTickPosition % ( MAX_NOTES / 8 ) ) != 0 ) && pSong->getSwingFactor() > 0 ) { /* TODO: incorporate the factor MAX_NOTES / 32. either in Song::m_fSwingFactor * or make it a member variable. @@ -2501,7 +2515,7 @@ int AudioEngine::updateNoteQueue( unsigned nFrames ) pCopiedNote->computeNoteStart(); if ( pHydrogen->getMode() == Song::Mode::Song ) { - float fPos = static_cast( nColumn ) + + float fPos = static_cast( m_nColumn ) + pCopiedNote->get_position() % 192 / 192.f; pCopiedNote->set_velocity( pNote->get_velocity() * pAutomationPath->get_value( fPos ) ); @@ -3538,7 +3552,7 @@ bool AudioEngine::testNoteEnqueuing() { ////////////////////////////////////////////////////////////////// pCoreActionController->activateSongMode( false ); - pPref->setPatternModePlaysSelected( true ); + pHydrogen->setPatternMode( Song::PatternMode::Selected ); pHydrogen->setSelectedPatternNumber( 4 ); lock( RIGHT_HERE ); diff --git a/src/core/AudioEngine/AudioEngine.h b/src/core/AudioEngine/AudioEngine.h index 2a926188f6..8aa5e75c1a 100644 --- a/src/core/AudioEngine/AudioEngine.h +++ b/src/core/AudioEngine/AudioEngine.h @@ -80,6 +80,20 @@ namespace H2Core * thread to lock the engine and unlock() to make it accessible for * other threads once again. * + * The audio engine does not have one but two consistent states with + * respect it its member variables. #m_fTick, #m_nFrames, + * #m_fTickOffset, #m_fTickMismatch, #m_fBpm, #m_fTickSize, + * #m_nFrameOffset, #m_state, and #m_nRealtimeFrames are associated + * with the current transport position. #m_fLastTickIntervalEnd, + * #m_nColumn, #m_nPatternSize, #m_nPatternStartTick, and + * #m_nPatternTickPosition determine the current position + * updateNoteQueue() is adding notes from #m_pPlayingPatterns into + * #m_songNoteQueue. Since the latter is ahead of the current + * transport position by a non-constant (tempo-dependent) lookahead, + * both states are out of sync while in playback but in sync again + * once the transport gets relocated (which resets the lookahead). But + * within themselves both states are consistent. + * * \ingroup docCore docAudioEngine */ class AudioEngine : public H2Core::TransportInfo, public H2Core::Object @@ -411,7 +425,7 @@ class AudioEngine : public H2Core::TransportInfo, public H2Core::Object pSong ); /** Is allowed to call removeSong().*/ friend void Hydrogen::removeSong(); + /** Is allowed to use locate() to directly set the position in + frames as well as to used setColumn and setPatternTickPos to + move the arrow in the SongEditorPositionRuler even when + playback is stopped.*/ + friend void Hydrogen::updateSelectedPattern(); friend bool CoreActionController::locateToTick( long nTick, bool ); /** Is allowed to set m_state to State::Ready via setState()*/ friend int FakeDriver::connect(); friend void JackAudioDriver::updateTransportInfo(); friend void JackAudioDriver::relocateUsingBBT(); private: + + /** + * Sets the Hydrogen::m_nSelectedPatternNumber to the pattern + * recorded notes will be inserted in. + */ + void handleSelectedPattern(); + + inline void processPlayNotes( unsigned long nframes ); /** * Converts a tick into frames under the assumption of a constant * @a fTickSize since the beginning of the song (sample rate, @@ -588,8 +611,6 @@ class AudioEngine : public H2Core::TransportInfo, public H2Core::Object public: /** possible keys */ enum Key { C=KEY_MIN, Cs, D, Ef, E, F, Fs, G, Af, A, Bf, B }; + static QString KeyToQString( Key key ); + /** possible octaves */ enum Octave { P8Z=-3, P8Y=-2, P8X=-1, P8=OCTAVE_DEFAULT, P8A=1, P8B=2, P8C=3 }; @@ -344,6 +346,20 @@ class Note : public H2Core::Object return pow( 1.0594630943593, fPitch ); } + static inline Octave pitchToOctave( int nPitch ) { + if ( nPitch >= 0 ) { + return (Octave)(nPitch / 12); + } else { + return (Octave)((nPitch-11) / 12); + } + } + static inline Key pitchToKey( int nPitch ) { + return (Key)(nPitch - 12 * pitchToOctave( nPitch )); + } + static inline int octaveKeyToPitch( Octave octave, Key key ) { + return 12 * (int)octave + (int)key; + } + /** * Returns the sample associated with the note for a specific * InstrumentComponent @a nComponentID. diff --git a/src/core/Basics/PatternList.cpp b/src/core/Basics/PatternList.cpp index fbdc0e76c0..ed70c6caaa 100644 --- a/src/core/Basics/PatternList.cpp +++ b/src/core/Basics/PatternList.cpp @@ -255,7 +255,7 @@ QString PatternList::find_unused_pattern_name( QString sourceName, Pattern* igno return unusedPatternNameCandidate; } -int PatternList::longest_pattern_length() { +int PatternList::longest_pattern_length() const { int nMax = -1; for ( int i = 0; i < __patterns.size(); i++ ) { nMax = std::max( nMax, __patterns[i]->get_length() ); diff --git a/src/core/Basics/PatternList.h b/src/core/Basics/PatternList.h index 7ca0120d16..96736b5136 100644 --- a/src/core/Basics/PatternList.h +++ b/src/core/Basics/PatternList.h @@ -158,7 +158,7 @@ class AudioEngineLocking; * Get the length of the longest pattern in the list * \return pattern length in ticks, -1 if list is empty */ - int longest_pattern_length(); + int longest_pattern_length() const; /** Formatted string version for debugging purposes. * \param sPrefix String prefix which will be added in front of * every new line diff --git a/src/core/Basics/Song.cpp b/src/core/Basics/Song.cpp index 63ec5bb84e..b1bc6e6693 100644 --- a/src/core/Basics/Song.cpp +++ b/src/core/Basics/Song.cpp @@ -78,6 +78,7 @@ Song::Song( const QString& sName, const QString& sAuthor, float fBpm, float fVol , m_pComponents( nullptr ) , m_sFilename( "" ) , m_loopMode( LoopMode::Disabled ) + , m_patternMode( PatternMode::Selected ) , m_fHumanizeTimeValue( 0.0 ) , m_fHumanizeVelocityValue( 0.0 ) , m_fSwingFactor( 0.0 ) @@ -89,6 +90,7 @@ Song::Song( const QString& sName, const QString& sAuthor, float fBpm, float fVol , m_pVelocityAutomationPath( nullptr ) , m_sLicense( "" ) , m_actionMode( ActionMode::selectMode ) + , m_bIsPatternEditorLocked( false ) , m_nPanLawType ( Sampler::RATIO_STRAIGHT_POLYGONAL ) , m_fPanLawKNorm ( Sampler::K_NORM_DEFAULT ) { @@ -148,13 +150,6 @@ void Song::setBpm( float fBpm ) { void Song::setActionMode( Song::ActionMode actionMode ) { m_actionMode = actionMode; - if ( actionMode == Song::ActionMode::selectMode ) { - EventQueue::get_instance()->push_event( EVENT_ACTION_MODE_CHANGE, 0 ); - } else if ( actionMode == Song::ActionMode::drawMode ) { - EventQueue::get_instance()->push_event( EVENT_ACTION_MODE_CHANGE, 1 ); - } else { - ERRORLOG( QString( "Unknown actionMode" ) ); - } setIsModified( true ); } @@ -1009,7 +1004,14 @@ std::shared_ptr SongReader::readSong( const QString& sFileName ) QString sNotes( LocalFileMng::readXmlString( songNode, "notes", "..." ) ); QString sLicense( LocalFileMng::readXmlString( songNode, "license", "Unknown license" ) ); bool bLoopEnabled = LocalFileMng::readXmlBool( songNode, "loopEnabled", false ); - pPreferences->setPatternModePlaysSelected( LocalFileMng::readXmlBool( songNode, "patternModeMode", true ) ); + bool bPatternMode = + LocalFileMng::readXmlBool( songNode, "patternModeMode", + static_cast(Song::PatternMode::Selected) ); + + Song::PatternMode patternMode = Song::PatternMode::Selected; + if ( ! bPatternMode ) { + patternMode = Song::PatternMode::Stacked; + } Song::Mode mode = Song::Mode::Pattern; QString sMode = LocalFileMng::readXmlString( songNode, "mode", "pattern" ); if ( sMode == "song" ) { @@ -1022,6 +1024,7 @@ std::shared_ptr SongReader::readSong( const QString& sFileName ) Song::ActionMode actionMode = static_cast( LocalFileMng::readXmlInt( songNode, "action_mode", static_cast( Song::ActionMode::selectMode ) ) ); + bool bIsPatternEditorLocked = LocalFileMng::readXmlBool( songNode, "isPatternEditorLocked", false ); float fHumanizeTimeValue = LocalFileMng::readXmlFloat( songNode, "humanize_time", 0.0 ); float fHumanizeVelocityValue = LocalFileMng::readXmlFloat( songNode, "humanize_velocity", 0.0 ); float fSwingFactor = LocalFileMng::readXmlFloat( songNode, "swing_factor", 0.0 ); @@ -1047,6 +1050,7 @@ std::shared_ptr SongReader::readSong( const QString& sFileName ) } else { pSong->setLoopMode( Song::LoopMode::Disabled ); } + pSong->setPatternMode( patternMode ); pSong->setMode( mode ); pSong->setHumanizeTimeValue( fHumanizeTimeValue ); pSong->setHumanizeVelocityValue( fHumanizeVelocityValue ); @@ -1055,6 +1059,7 @@ std::shared_ptr SongReader::readSong( const QString& sFileName ) pSong->setPlaybackTrackEnabled( bPlaybackTrackEnabled ); pSong->setPlaybackTrackVolume( fPlaybackTrackVolume ); pSong->setActionMode( actionMode ); + pSong->setIsPatternEditorLocked( bIsPatternEditorLocked ); pSong->setIsTimelineActivated( bIsTimelineActivated ); // pan law diff --git a/src/core/Basics/Song.h b/src/core/Basics/Song.h index 9cbb722a08..715b928865 100644 --- a/src/core/Basics/Song.h +++ b/src/core/Basics/Song.h @@ -82,7 +82,24 @@ class Song : public H2Core::Object, public std::enable_shared_from_this, public std::enable_shared_from_this, public std::enable_shared_from_this, public std::enable_shared_from_this, public std::enable_shared_from_this, public std::enable_shared_from_this, public std::enable_shared_from_this Song::getTimeline() const { return m_pTimeline; } @@ -525,6 +571,15 @@ inline void Song::setLoopMode( Song::LoopMode loopMode ) setIsModified( true ); } +inline Song::PatternMode Song::getPatternMode() const +{ + return m_patternMode; +} +inline void Song::setPatternMode( Song::PatternMode patternMode ) +{ + m_patternMode = patternMode; +} + inline float Song::getHumanizeTimeValue() const { return m_fHumanizeTimeValue; diff --git a/src/core/CoreActionController.cpp b/src/core/CoreActionController.cpp index 0dbdb99210..893a9d6dd7 100644 --- a/src/core/CoreActionController.cpp +++ b/src/core/CoreActionController.cpp @@ -799,6 +799,7 @@ bool CoreActionController::activateJackTimebaseMaster( bool bActivate ) { bool CoreActionController::activateSongMode( bool bActivate ) { auto pHydrogen = Hydrogen::get_instance(); + auto pAudioEngine = pHydrogen->getAudioEngine(); if ( pHydrogen->getSong() == nullptr ) { ERRORLOG( "no song set" ); @@ -807,11 +808,18 @@ bool CoreActionController::activateSongMode( bool bActivate ) { pHydrogen->sequencer_stop(); if ( bActivate && pHydrogen->getMode() != Song::Mode::Song ) { - locateToColumn( 0 ); pHydrogen->setMode( Song::Mode::Song ); } else if ( ! bActivate && pHydrogen->getMode() != Song::Mode::Pattern ) { pHydrogen->setMode( Song::Mode::Pattern ); + + // Add the selected pattern to playing ones. + if ( pHydrogen->getPatternMode() == Song::PatternMode::Selected ) { + pAudioEngine->lock( RIGHT_HERE ); + pAudioEngine->updatePlayingPatterns( 0, 0 ); + pAudioEngine->unlock(); + } } + locateToColumn( 0 ); return true; } @@ -1194,13 +1202,12 @@ bool CoreActionController::extractDrumkit( const QString& sDrumkitPath, const QS bool CoreActionController::locateToColumn( int nPatternGroup ) { if ( nPatternGroup < -1 ) { - ERRORLOG( QString( "Provided column [%1] too low. Assigning -1 (indicating the beginning of a song without showing a cursor in the SongEditorPositionRuler) instead." ) + ERRORLOG( QString( "Provided column [%1] too low. Assigning 0 instead." ) .arg( nPatternGroup ) ); - nPatternGroup = -1; + nPatternGroup = 0; } auto pHydrogen = Hydrogen::get_instance(); - if ( pHydrogen->getSong() == nullptr ) { ERRORLOG( "no song set" ); return false; @@ -1209,6 +1216,7 @@ bool CoreActionController::locateToColumn( int nPatternGroup ) { auto pAudioEngine = pHydrogen->getAudioEngine(); EventQueue::get_instance()->push_event( EVENT_METRONOME, 1 ); + long nTotalTick = pHydrogen->getTickForColumn( nPatternGroup ); if ( nTotalTick < 0 ) { // There is no pattern inserted in the SongEditor. @@ -1241,6 +1249,8 @@ bool CoreActionController::locateToTick( long nTick, bool bWithJackBroadcast ) { pAudioEngine->locate( nTick, bWithJackBroadcast ); pAudioEngine->unlock(); + + EventQueue::get_instance()->push_event( EVENT_RELOCATION, 0 ); return true; } @@ -1290,7 +1300,11 @@ bool CoreActionController::setPattern( Pattern* pPattern, int nPatternPosition ) } pPatternList->insert( nPatternPosition, pPattern ); - pHydrogen->setSelectedPatternNumber( nPatternPosition ); + if ( pHydrogen->isPatternEditorLocked() ) { + pHydrogen->updateSelectedPattern(); + } else { + pHydrogen->setSelectedPatternNumber( nPatternPosition ); + } pHydrogen->setIsModified( true ); // Update the SongEditor. @@ -1367,8 +1381,7 @@ bool CoreActionController::removePattern( int nPatternNumber ) { // mode. for ( int ii = 0; ii < pPlayingPatterns->size(); ++ii ) { if ( pPlayingPatterns->get( ii ) == pPattern ) { - pAudioEngine->flushPlayingPatterns(); - pAudioEngine->updatePlayingPatterns( pAudioEngine->getColumn() ); + pAudioEngine->removePlayingPattern( ii ); break; } } diff --git a/src/core/EventQueue.h b/src/core/EventQueue.h index 902bbe6547..b4bb430212 100644 --- a/src/core/EventQueue.h +++ b/src/core/EventQueue.h @@ -42,17 +42,12 @@ enum EventType { EVENT_NONE, EVENT_STATE, /** - * The current list of patterns the transport position resides in - * changed. + * The list of currently played patterns in changed. * * In #Song::Mode::Song this is triggered every time the column of * the SongEditor grid changed. Either by rolling transport or by * relocation. * - * In #Song::Mode::Pattern with - * Preferences::m_bPatternModePlaysSelected set true it is - * triggered if the currently selected pattern changes. - * * It is handled by EventListener::patternChangedEvent(). */ EVENT_PATTERN_CHANGED, @@ -61,11 +56,9 @@ enum EventType { */ EVENT_PATTERN_MODIFIED, /** Another pattern was selected via MIDI or the GUI without - * affecting the audio transport (e.g in Song::PATTERN_MODE when - * Preferences::m_bPatternModePlaysSelected is set to true). While - * the selection in the former case already happens in the GUI, - * this event will be used to tell it the selection was successful - * and had been done. + * affecting the audio transport. While the selection in the + * former case already happens in the GUI, this event will be used + * to tell it the selection was successful and had been done. * * Handled by EventListener::selectedPatternChangedEvent(). */ @@ -151,7 +144,8 @@ enum EventType { and informs the GUI about a state change.*/ EVENT_JACK_TIMEBASE_STATE_CHANGED, EVENT_SONG_MODE_ACTIVATION, - /** Activate stacked mode (1) or "focus"/"PatternPlaysSelected"/normal mode */ + /** Song::PatternMode::Stacked (0) or Song::PatternMode::Selected + (1) was activated */ EVENT_STACKED_MODE_ACTIVATION, /** Toggles the button indicating the usage loop mode.*/ EVENT_LOOP_MODE_ACTIVATION, @@ -163,7 +157,15 @@ enum EventType { (either during playback or when relocated by the user)*/ EVENT_COLUMN_CHANGED, /** A the current drumkit was replaced by a new one*/ - EVENT_DRUMKIT_LOADED + EVENT_DRUMKIT_LOADED, + /** Locks the PatternEditor on the pattern currently played back.*/ + EVENT_PATTERN_EDITOR_LOCKED, + /** Triggered in case there is a relocation of the transport + * position due to an user interaction or an incoming MIDI/OSC + * command. + */ + EVENT_RELOCATION, + EVENT_SONG_SIZE_CHANGED }; /** Basic building block for the communication between the core of diff --git a/src/core/Hydrogen.cpp b/src/core/Hydrogen.cpp index 3ee77ed7cd..6e906acc5d 100644 --- a/src/core/Hydrogen.cpp +++ b/src/core/Hydrogen.cpp @@ -257,7 +257,6 @@ void Hydrogen::setSong( std::shared_ptr pSong ) std::shared_ptr pCurrentSong = getSong(); if ( pSong == pCurrentSong ) { - DEBUGLOG( "pSong == pCurrentSong" ); return; } @@ -721,7 +720,7 @@ bool Hydrogen::instrumentHasNotes( std::shared_ptr pInst ) { if( pPatternList->get( nPattern )->references( pInst ) ) { - DEBUGLOG("Instrument " + pInst->get_name() + " has notes" ); + INFOLOG("Instrument " + pInst->get_name() + " has notes" ); return true; } } @@ -846,6 +845,13 @@ void Hydrogen::restartLadspaFX() } } +void Hydrogen::updateSelectedPattern() { + if ( isPatternEditorLocked() ) { + m_pAudioEngine->lock( RIGHT_HERE ); + m_pAudioEngine->handleSelectedPattern(); + m_pAudioEngine->unlock(); + } +} void Hydrogen::setSelectedPatternNumber( int nPat, bool bNeedsLock ) { @@ -853,15 +859,18 @@ void Hydrogen::setSelectedPatternNumber( int nPat, bool bNeedsLock ) return; } - if ( Preferences::get_instance()->patternModePlaysSelected() ) { + if ( getPatternMode() == Song::PatternMode::Selected ) { if ( bNeedsLock ) { - getAudioEngine()->lock( RIGHT_HERE ); + m_pAudioEngine->lock( RIGHT_HERE ); } m_nSelectedPatternNumber = nPat; + // The specific values provided are not important since we a + // in selected pattern mode. + m_pAudioEngine->updatePlayingPatterns( 0, 0 ); if ( bNeedsLock ) { - getAudioEngine()->unlock(); + m_pAudioEngine->unlock(); } } else { m_nSelectedPatternNumber = nPat; @@ -1075,28 +1084,6 @@ void Hydrogen::onJackMaster() #endif } -void Hydrogen::setPlaysSelected( bool bPlaysSelected ) -{ - auto pAudioEngine = m_pAudioEngine; - - if ( getMode() != Song::Mode::Pattern ) { - return; - } - - auto pSong = getSong(); - auto pPref = Preferences::get_instance(); - - if ( pPref->patternModePlaysSelected() != bPlaysSelected ) { - pAudioEngine->lock( RIGHT_HERE ); - - pPref->setPatternModePlaysSelected( bPlaysSelected ); - - pAudioEngine->updatePlayingPatterns( pAudioEngine->getColumn() ); - - pAudioEngine->unlock(); - } -} - void Hydrogen::addInstrumentToDeathRow( std::shared_ptr pInstr ) { __instrument_death_row.push_back( pInstr ); __kill_instruments(); @@ -1209,7 +1196,7 @@ bool Hydrogen::isUnderSessionManagement() const { } bool Hydrogen::isTimelineEnabled() const { - if ( getSong()->getIsTimelineActivated() && + if ( __song->getIsTimelineActivated() && getMode() == Song::Mode::Song && getJackTimebaseState() != JackAudioDriver::Timebase::Slave ) { return true; @@ -1218,6 +1205,89 @@ bool Hydrogen::isTimelineEnabled() const { return false; } +bool Hydrogen::isPatternEditorLocked() const { + if ( getMode() == Song::Mode::Song ) { + if ( __song->getIsPatternEditorLocked() ) { + return true; + } + } + + return false; +} + +void Hydrogen::setIsPatternEditorLocked( bool bValue ) { + if ( __song != nullptr ) { + __song->setIsPatternEditorLocked( bValue ); + + EventQueue::get_instance()->push_event( EVENT_PATTERN_EDITOR_LOCKED, + bValue ); + } +} + +Song::Mode Hydrogen::getMode() const { + if ( __song != nullptr ) { + return __song->getMode(); + } + + return Song::Mode::None; +} + +void Hydrogen::setMode( Song::Mode mode ) { + if ( __song != nullptr && mode != __song->getMode() ) { + __song->setMode( mode ); + EventQueue::get_instance()->push_event( EVENT_SONG_MODE_ACTIVATION, + ( mode == Song::Mode::Song) ? 1 : 0 ); + } +} + +Song::ActionMode Hydrogen::getActionMode() const { + if ( __song != nullptr ) { + return __song->getActionMode(); + } + return Song::ActionMode::None; +} + +void Hydrogen::setActionMode( Song::ActionMode mode ) { + if ( __song != nullptr ) { + __song->setActionMode( mode ); + EventQueue::get_instance()->push_event( EVENT_ACTION_MODE_CHANGE, + ( mode == Song::ActionMode::drawMode ) ? 1 : 0 ); + } +} + +Song::PatternMode Hydrogen::getPatternMode() const { + if ( getMode() == Song::Mode::Pattern ) { + return __song->getPatternMode(); + } + return Song::PatternMode::None; +} + +void Hydrogen::setPatternMode( Song::PatternMode mode ) +{ + if ( __song != nullptr && + getPatternMode() != mode ) { + m_pAudioEngine->lock( RIGHT_HERE ); + + __song->setPatternMode( mode ); + setIsModified( true ); + + if ( mode == Song::PatternMode::Selected || + m_pAudioEngine->getState() != AudioEngine::State::Playing ) { + // Only update the playing patterns in selected pattern + // mode or if transport is not rolling. In stacked pattern + // mode with transport rolling + // AudioEngine::updatePatternTransportPosition() will call + // the functions and activate the next patterns once the + // current ones are looped. + m_pAudioEngine->updatePlayingPatterns( m_pAudioEngine->getColumn() ); + } + + m_pAudioEngine->unlock(); + EventQueue::get_instance()->push_event( EVENT_STACKED_MODE_ACTIVATION, + ( mode == Song::PatternMode::Selected ) ? 1 : 0 ); + } +} + Hydrogen::Tempo Hydrogen::getTempoSource() const { if ( getMode() == Song::Mode::Song ) { if ( getJackTimebaseState() == JackAudioDriver::Timebase::Slave ) { @@ -1269,7 +1339,6 @@ void Hydrogen::startNsmClient() void Hydrogen::recalculateRubberband( float fBpm ) { - DEBUGLOG( fBpm ); if ( !Preferences::get_instance()->getRubberBandBatchMode() ) { return; @@ -1336,13 +1405,6 @@ void Hydrogen::setIsModified( bool bIsModified ) { } } -void Hydrogen::setMode( Song::Mode mode ) { - if ( getSong() != nullptr ) { - getSong()->setMode( mode ); - EventQueue::get_instance()->push_event( EVENT_SONG_MODE_ACTIVATION, ( mode == Song::Mode::Song) ? 1 : 0 ); - } -} - void Hydrogen::setIsTimelineActivated( bool bEnabled ) { if ( getSong() != nullptr ) { auto pPref = Preferences::get_instance(); diff --git a/src/core/Hydrogen.h b/src/core/Hydrogen.h index ac2ddef207..672314ae48 100644 --- a/src/core/Hydrogen.h +++ b/src/core/Hydrogen.h @@ -108,12 +108,6 @@ class Hydrogen : public H2Core::Object void toggleNextPattern( int nPatternNumber ); /** Wrapper around AudioEngine::flushAndAddNextPattern().*/ void flushAndAddNextPattern( int nPatternNumber ); - /** - * Switches playback to focused pattern. - * - * ("Focused pattern" or "PlaysSelected" is the opposite of "Stacked" mode) - */ - void setPlaysSelected( bool bPlaysSelected ); /** * Get the current song. @@ -177,6 +171,18 @@ class Hydrogen : public H2Core::Object EVENT_SONG_MODE_ACTIVATION and should be used by all parts of the code except for song reading/setting.*/ void setMode( Song::Mode mode ); + + Song::ActionMode getActionMode() const; + /** Wrapper around Song::setActionMode() which also triggers + EVENT_ACTION_MODE_CHANGE and should be used by all parts of the + code except for song reading/setting.*/ + void setActionMode( Song::ActionMode mode ); + + Song::PatternMode getPatternMode() const; + /** Wrapper around Song::setPatternMode() which also triggers + EVENT_STACKED_MODE_ACTIVATION and should be used by all parts of the + code except for song reading/setting.*/ + void setPatternMode( Song::PatternMode mode ); /** Wrapper around both Song::setIsTimelineActivated (recent) and Preferences::setUseTimelinebpm() (former place to store the @@ -316,20 +322,18 @@ void previewSample( Sample *pSample ); /** * Sets #m_nSelectedPatternNumber. * - * If Preferences::m_pPatternModePlaysSelected is set to true, the - * AudioEngine is locked before @a nPat will be assigned. But in - * any case the function will push the - * #EVENT_SELECTED_PATTERN_CHANGED Event to the EventQueue. - * - * If @a nPat is equal to #m_nSelectedPatternNumber, the function - * will return right away. - * *\param nPat Sets #m_nSelectedPatternNumber * \param bNeedsLock Whether the function was called with the * audio engine locked already or it should do so itself. */ void setSelectedPatternNumber( int nPat, bool bNeedsLock = true ); + /** + * Updates the selected pattern to the one recorded note will be + * inserted to. + */ + void updateSelectedPattern(); + int getSelectedInstrumentNumber() const; void setSelectedInstrumentNumber( int nInstrument ); @@ -450,6 +454,13 @@ void previewSample( Sample *pSample ); */ bool isTimelineEnabled() const; + /** + * Convenience function checking whether using the Pattern Editor + * is locked in the song settings and the song is in song mode. + */ + bool isPatternEditorLocked() const; + void setIsPatternEditorLocked( bool bValue ); + Tempo getTempoSource() const; /** @@ -676,9 +687,6 @@ inline int Hydrogen::getSelectedInstrumentNumber() const { return m_nSelectedInstrumentNumber; } -inline Song::Mode Hydrogen::getMode() const { - return getSong()->getMode(); -} }; #endif diff --git a/src/core/LocalFileMgr.cpp b/src/core/LocalFileMgr.cpp index ab43cf438a..5af1612ae0 100644 --- a/src/core/LocalFileMgr.cpp +++ b/src/core/LocalFileMgr.cpp @@ -424,13 +424,20 @@ int SongWriter::writeSong( std::shared_ptr pSong, const QString& filename LocalFileMng::writeXmlString( songNode, "notes", pSong->getNotes() ); LocalFileMng::writeXmlString( songNode, "license", pSong->getLicense() ); LocalFileMng::writeXmlBool( songNode, "loopEnabled", pSong->isLoopEnabled() ); - LocalFileMng::writeXmlBool( songNode, "patternModeMode", Preferences::get_instance()->patternModePlaysSelected()); + + bool bPatternMode = static_cast(Song::PatternMode::Selected); + if ( pSong->getPatternMode() == Song::PatternMode::Stacked ) { + bPatternMode = static_cast(Song::PatternMode::Stacked); + } + LocalFileMng::writeXmlBool( songNode, "patternModeMode", bPatternMode ); LocalFileMng::writeXmlString( songNode, "playbackTrackFilename", QString("%1").arg( pSong->getPlaybackTrackFilename() ) ); LocalFileMng::writeXmlBool( songNode, "playbackTrackEnabled", pSong->getPlaybackTrackEnabled() ); LocalFileMng::writeXmlString( songNode, "playbackTrackVolume", QString("%1").arg( pSong->getPlaybackTrackVolume() ) ); LocalFileMng::writeXmlString( songNode, "action_mode", QString::number( static_cast( pSong->getActionMode() ) ) ); + LocalFileMng::writeXmlBool( songNode, "isPatternEditorLocked", + pSong->getIsPatternEditorLocked() ); LocalFileMng::writeXmlBool( songNode, "isTimelineActivated", pSong->getIsTimelineActivated() ); if ( pSong->getMode() == Song::Mode::Song ) { diff --git a/src/core/Logger.cpp b/src/core/Logger.cpp index ac8f972595..09015e54cb 100644 --- a/src/core/Logger.cpp +++ b/src/core/Logger.cpp @@ -178,7 +178,7 @@ void Logger::log( unsigned level, const QString& class_name, const char* func_na void Logger::flush() const { int nTimeout = 100; - for ( int ii = 0; ii < nTimeout; +ii ) { + for ( int ii = 0; ii < nTimeout; ++ii ) { if ( __msg_queue.empty() ) { break; } diff --git a/src/core/MidiAction.cpp b/src/core/MidiAction.cpp index 0659c05c01..f68a45cfbc 100644 --- a/src/core/MidiAction.cpp +++ b/src/core/MidiAction.cpp @@ -397,10 +397,10 @@ bool MidiActionManager::select_next_pattern( std::shared_ptr pAction, Hy .arg( pHydrogen->getSong()->getPatternList()->size() - 1 ) ); return false; } - if(Preferences::get_instance()->patternModePlaysSelected()) { + if ( pHydrogen->getPatternMode() == Song::PatternMode::Selected ) { pHydrogen->setSelectedPatternNumber( row ); } - else { + else if ( pHydrogen->getPatternMode() == Song::PatternMode::Stacked ) { pHydrogen->toggleNextPattern( row ); } return true; @@ -421,9 +421,8 @@ bool MidiActionManager::select_only_next_pattern( std::shared_ptr pActio .arg( pHydrogen->getSong()->getPatternList()->size() - 1 ) ); return false; } - if(Preferences::get_instance()->patternModePlaysSelected()) - { - return true; + if ( pHydrogen->getPatternMode() == Song::PatternMode::Selected ) { + return select_next_pattern( pAction, pHydrogen ); } pHydrogen->flushAndAddNextPattern( row ); @@ -438,7 +437,7 @@ bool MidiActionManager::select_next_pattern_relative( std::shared_ptr pA } bool ok; - if(!Preferences::get_instance()->patternModePlaysSelected()) { + if( pHydrogen->getPatternMode() == Song::PatternMode::Stacked ) { return true; } int row = pHydrogen->getSelectedPatternNumber() + pAction->getParameter1().toInt(&ok,10); @@ -470,7 +469,7 @@ bool MidiActionManager::select_next_pattern_cc_absolute( std::shared_ptr return false; } - if(Preferences::get_instance()->patternModePlaysSelected()) { + if( pHydrogen->getPatternMode() == Song::PatternMode::Selected ) { pHydrogen->setSelectedPatternNumber( row ); } else { diff --git a/src/core/Object.h b/src/core/Object.h index 9d8abd6b3d..f7d6088c72 100644 --- a/src/core/Object.h +++ b/src/core/Object.h @@ -123,7 +123,7 @@ class Base { * displayed without line breaks. * * \return String presentation of current object.*/ - virtual QString toQString( const QString& sPrefix, bool bShort = true ) const; + virtual QString toQString( const QString& sPrefix = "", bool bShort = true ) const; /** Prints content of toQString() via DEBUGLOG * * \param bShort Whether to display the content of the member diff --git a/src/core/Preferences/Preferences.cpp b/src/core/Preferences/Preferences.cpp index f4fdf0a30d..670fd90c03 100644 --- a/src/core/Preferences/Preferences.cpp +++ b/src/core/Preferences/Preferences.cpp @@ -221,7 +221,6 @@ Preferences::Preferences() m_nOscTemporaryPort = -1; //___ General properties ___ - m_bPatternModePlaysSelected = true; m_brestoreLastSong = true; m_brestoreLastPlaylist = false; m_bUseLash = false; @@ -329,7 +328,6 @@ void Preferences::loadPreferences( bool bGlobal ) m_bShowNoteOverwriteWarning = LocalFileMng::readXmlBool( rootNode, "showNoteOverwriteWarning", m_bShowNoteOverwriteWarning ); m_brestoreLastSong = LocalFileMng::readXmlBool( rootNode, "restoreLastSong", m_brestoreLastSong ); m_brestoreLastPlaylist = LocalFileMng::readXmlBool( rootNode, "restoreLastPlaylist", m_brestoreLastPlaylist ); - m_bPatternModePlaysSelected = LocalFileMng::readXmlBool( rootNode, "patternModePlaysSelected", true ); m_bUseLash = LocalFileMng::readXmlBool( rootNode, "useLash", false ); __useTimelineBpm = LocalFileMng::readXmlBool( rootNode, "useTimeLine", __useTimelineBpm ); m_nMaxBars = LocalFileMng::readXmlInt( rootNode, "maxBars", 400 ); @@ -839,8 +837,6 @@ void Preferences::savePreferences() LocalFileMng::writeXmlString( rootNode, "restoreLastSong", m_brestoreLastSong ? "true": "false" ); LocalFileMng::writeXmlString( rootNode, "restoreLastPlaylist", m_brestoreLastPlaylist ? "true": "false" ); - LocalFileMng::writeXmlString( rootNode, "patternModePlaysSelected", m_bPatternModePlaysSelected ? "true": "false" ); - LocalFileMng::writeXmlString( rootNode, "useLash", m_bsetLash ? "true": "false" ); LocalFileMng::writeXmlString( rootNode, "useTimeLine", __useTimelineBpm ? "true": "false" ); diff --git a/src/core/Preferences/Preferences.h b/src/core/Preferences/Preferences.h index c49ba19161..638fef531c 100644 --- a/src/core/Preferences/Preferences.h +++ b/src/core/Preferences/Preferences.h @@ -532,10 +532,6 @@ class Preferences : public H2Core::Object const std::shared_ptr getColorTheme() const; void setColorTheme( const std::shared_ptr pNewColorTheme ); - /** \return #m_bPatternModePlaysSelected*/ - bool patternModePlaysSelected(); - /** \param b Sets #m_bPatternModePlaysSelected*/ - void setPatternModePlaysSelected( bool b ); bool useLash(); void setUseLash( bool b ); @@ -671,15 +667,7 @@ class Preferences : public H2Core::Object QString m_sH2ProcessName; //Name of hydrogen's main process ///rubberband bpm change queue bool m_useTheRubberbandBpmChangeEvent; - /** - * When transport is in Song::PATTERN_MODE and this variable is - * set to true, the currently focused Pattern will be used for - * playback. - * - * It is set by setPatternModePlaysSelected() and queried by - * patternModePlaysSelected(). - */ - bool m_bPatternModePlaysSelected; + ///< Restore last song? bool m_brestoreLastSong; bool m_brestoreLastPlaylist; @@ -1336,13 +1324,6 @@ inline void Preferences::setColorTheme( const std::shared_ptr pNewCo m_pTheme->setColorTheme( pNewColorTheme ); } -inline bool Preferences::patternModePlaysSelected() { - return m_bPatternModePlaysSelected; -} -inline void Preferences::setPatternModePlaysSelected( bool b ) { - m_bPatternModePlaysSelected = b; -} - inline bool Preferences::useLash(){ return m_bUseLash; } diff --git a/src/core/Preferences/Theme.cpp b/src/core/Preferences/Theme.cpp index 672036ffbf..60924da283 100644 --- a/src/core/Preferences/Theme.cpp +++ b/src/core/Preferences/Theme.cpp @@ -30,24 +30,37 @@ namespace H2Core { ColorTheme::ColorTheme() - : m_songEditor_backgroundColor( QColor(95, 101, 117) ) - , m_songEditor_alternateRowColor( QColor(128, 134, 152) ) - , m_songEditor_selectedRowColor( QColor(128, 134, 152) ) - , m_songEditor_lineColor( QColor(72, 76, 88) ) - , m_songEditor_textColor( QColor(196, 201, 214) ) - , m_patternEditor_backgroundColor( QColor(167, 168, 163) ) - , m_patternEditor_alternateRowColor( QColor(167, 168, 163) ) - , m_patternEditor_selectedRowColor( QColor(207, 208, 200) ) - , m_patternEditor_textColor( QColor(40, 40, 40) ) - , m_patternEditor_noteColor( QColor(40, 40, 40) ) - , m_patternEditor_lineColor( QColor(65, 65, 65) ) - , m_patternEditor_line1Color( QColor(75, 75, 75) ) - , m_patternEditor_line2Color( QColor(95, 95, 95) ) - , m_patternEditor_line3Color( QColor(115, 115, 115) ) - , m_patternEditor_line4Color( QColor(125, 125, 125) ) - , m_patternEditor_line5Color( QColor(135, 135, 135) ) - , m_selectionHighlightColor( QColor(0, 0, 255) ) - , m_selectionInactiveColor( QColor(85, 85, 85) ) + : m_songEditor_backgroundColor( QColor( 128, 134, 152 ) ) + , m_songEditor_alternateRowColor( QColor( 106, 111, 126 ) ) + , m_songEditor_selectedRowColor( QColor( 149, 157, 178 ) ) + , m_songEditor_selectedRowTextColor( QColor( 0, 0, 0 ) ) + , m_songEditor_lineColor( QColor( 54, 57, 67 ) ) + , m_songEditor_textColor( QColor( 206, 211, 224 ) ) + , m_songEditor_automationBackgroundColor( QColor( 83, 89, 103 ) ) + , m_songEditor_automationLineColor( QColor( 45, 66, 89 ) ) + , m_songEditor_automationNodeColor( QColor( 255, 255, 255 ) ) + , m_songEditor_stackedModeOnColor( QColor( 127, 159, 127 ) ) + , m_songEditor_stackedModeOnNextColor( QColor( 240, 223, 175 ) ) + , m_songEditor_stackedModeOffNextColor( QColor( 247, 100, 100 ) ) + , m_patternEditor_backgroundColor( QColor( 165, 166, 160 ) ) + , m_patternEditor_alternateRowColor( QColor( 133, 134, 129 ) ) + , m_patternEditor_selectedRowColor( QColor( 194, 195, 187 ) ) + , m_patternEditor_selectedRowTextColor( QColor( 0, 0, 0 ) ) + , m_patternEditor_octaveRowColor( QColor( 193, 194, 186 ) ) + , m_patternEditor_textColor( QColor( 240, 240, 240 ) ) + , m_patternEditor_noteVelocityFullColor( QColor( 247, 100, 100 ) ) + , m_patternEditor_noteVelocityDefaultColor( QColor( 40, 40, 40 ) ) + , m_patternEditor_noteVelocityHalfColor( QColor( 89, 131, 175 ) ) + , m_patternEditor_noteVelocityZeroColor( QColor( 255, 255, 255 ) ) + , m_patternEditor_noteOffColor( QColor( 0, 0, 0 ) ) + , m_patternEditor_lineColor( QColor(45, 45, 45) ) + , m_patternEditor_line1Color( QColor(55, 55, 55) ) + , m_patternEditor_line2Color( QColor(75, 75, 75) ) + , m_patternEditor_line3Color( QColor(95, 95, 95) ) + , m_patternEditor_line4Color( QColor(105, 105, 105) ) + , m_patternEditor_line5Color( QColor(115, 115, 115) ) + , m_selectionHighlightColor( QColor( 255, 255, 255 ) ) + , m_selectionInactiveColor( QColor( 199, 199, 199 ) ) , m_windowColor( QColor( 58, 62, 72 ) ) , m_windowTextColor( QColor( 255, 255, 255 ) ) , m_baseColor( QColor( 88, 94, 112 ) ) @@ -70,10 +83,10 @@ ColorTheme::ColorTheme() , m_buttonRedTextColor( QColor( 10, 10, 10 ) ) , m_spinBoxColor( QColor( 51, 74 , 100 ) ) , m_spinBoxTextColor( QColor( 240, 240, 240 ) ) - , m_automationColor( QColor( 67, 96, 131 ) ) - , m_automationCircleColor( QColor( 255, 255, 255 ) ) , m_accentColor( QColor( 67, 96, 131 ) ) , m_accentTextColor( QColor( 255, 255, 255 ) ) + , m_playheadColor( QColor( 0, 0, 0 ) ) + , m_cursorColor( QColor( 38, 39, 44 ) ) { } @@ -81,14 +94,26 @@ ColorTheme::ColorTheme( const std::shared_ptr pOther ) : m_songEditor_backgroundColor( pOther->m_songEditor_backgroundColor ) , m_songEditor_alternateRowColor( pOther->m_songEditor_alternateRowColor ) , m_songEditor_selectedRowColor( pOther->m_songEditor_selectedRowColor ) + , m_songEditor_selectedRowTextColor( pOther->m_songEditor_selectedRowTextColor ) , m_songEditor_lineColor( pOther->m_songEditor_lineColor ) , m_songEditor_textColor( pOther->m_songEditor_textColor ) + , m_songEditor_automationBackgroundColor( pOther->m_songEditor_automationBackgroundColor ) + , m_songEditor_automationLineColor( pOther->m_songEditor_automationLineColor ) + , m_songEditor_automationNodeColor( pOther->m_songEditor_automationNodeColor ) + , m_songEditor_stackedModeOnColor( pOther->m_songEditor_stackedModeOnColor ) + , m_songEditor_stackedModeOnNextColor( pOther->m_songEditor_stackedModeOnNextColor ) + , m_songEditor_stackedModeOffNextColor( pOther->m_songEditor_stackedModeOffNextColor ) , m_patternEditor_backgroundColor( pOther->m_patternEditor_backgroundColor ) , m_patternEditor_alternateRowColor( pOther->m_patternEditor_alternateRowColor ) , m_patternEditor_selectedRowColor( pOther->m_patternEditor_selectedRowColor ) + , m_patternEditor_selectedRowTextColor( pOther->m_patternEditor_selectedRowTextColor ) + , m_patternEditor_octaveRowColor( pOther->m_patternEditor_octaveRowColor ) , m_patternEditor_textColor( pOther->m_patternEditor_textColor ) - , m_patternEditor_noteColor( pOther->m_patternEditor_noteColor ) - , m_patternEditor_noteoffColor( pOther->m_patternEditor_noteoffColor ) + , m_patternEditor_noteVelocityFullColor( pOther->m_patternEditor_noteVelocityFullColor ) + , m_patternEditor_noteVelocityDefaultColor( pOther->m_patternEditor_noteVelocityDefaultColor ) + , m_patternEditor_noteVelocityHalfColor( pOther->m_patternEditor_noteVelocityHalfColor ) + , m_patternEditor_noteVelocityZeroColor( pOther->m_patternEditor_noteVelocityZeroColor ) + , m_patternEditor_noteOffColor( pOther->m_patternEditor_noteOffColor ) , m_patternEditor_lineColor( pOther->m_patternEditor_lineColor ) , m_patternEditor_line1Color( pOther->m_patternEditor_line1Color ) , m_patternEditor_line2Color( pOther->m_patternEditor_line2Color ) @@ -121,8 +146,8 @@ ColorTheme::ColorTheme( const std::shared_ptr pOther ) , m_buttonRedTextColor( pOther->m_buttonRedTextColor ) , m_spinBoxColor( pOther->m_spinBoxColor ) , m_spinBoxTextColor( pOther->m_spinBoxTextColor ) - , m_automationColor( pOther->m_automationColor ) - , m_automationCircleColor( pOther->m_automationCircleColor ) + , m_playheadColor( pOther->m_playheadColor ) + , m_cursorColor( pOther->m_cursorColor ) { } @@ -137,7 +162,7 @@ InterfaceTheme::InterfaceTheme() , m_scalingPolicy( InterfaceTheme::ScalingPolicy::Smaller ) , m_iconColor( InterfaceTheme::IconColor::Black ) , m_coloringMethod( InterfaceTheme::ColoringMethod::Custom ) - , m_nVisiblePatternColors( 1 ) + , m_nVisiblePatternColors( 18 ) , m_nMaxPatternColors( 50 ) { std::vector m_patternColors( m_nMaxPatternColors ); for ( int ii = 0; ii < m_nMaxPatternColors; ii++ ) { @@ -190,15 +215,43 @@ void Theme::setTheme( const std::shared_ptr pOther ) { DEBUGLOG(""); m_pColorTheme->m_songEditor_backgroundColor = pOther->getColorTheme()->m_songEditor_backgroundColor; m_pColorTheme->m_songEditor_alternateRowColor = pOther->getColorTheme()->m_songEditor_alternateRowColor; - m_pColorTheme->m_songEditor_selectedRowColor = pOther->getColorTheme()->m_songEditor_selectedRowColor; + m_pColorTheme->m_songEditor_selectedRowColor = + pOther->getColorTheme()->m_songEditor_selectedRowColor; + m_pColorTheme->m_songEditor_selectedRowTextColor = + pOther->getColorTheme()->m_songEditor_selectedRowTextColor; m_pColorTheme->m_songEditor_lineColor = pOther->getColorTheme()->m_songEditor_lineColor; m_pColorTheme->m_songEditor_textColor = pOther->getColorTheme()->m_songEditor_textColor; + m_pColorTheme->m_songEditor_automationBackgroundColor = + pOther->getColorTheme()->m_songEditor_automationBackgroundColor; + m_pColorTheme->m_songEditor_automationLineColor = + pOther->getColorTheme()->m_songEditor_automationLineColor; + m_pColorTheme->m_songEditor_automationNodeColor = + pOther->getColorTheme()->m_songEditor_automationNodeColor; + m_pColorTheme->m_songEditor_stackedModeOnColor = + pOther->getColorTheme()->m_songEditor_stackedModeOnColor; + m_pColorTheme->m_songEditor_stackedModeOnNextColor = + pOther->getColorTheme()->m_songEditor_stackedModeOnNextColor; + m_pColorTheme->m_songEditor_stackedModeOffNextColor = + pOther->getColorTheme()->m_songEditor_stackedModeOffNextColor; m_pColorTheme->m_patternEditor_backgroundColor = pOther->getColorTheme()->m_patternEditor_backgroundColor; m_pColorTheme->m_patternEditor_alternateRowColor = pOther->getColorTheme()->m_patternEditor_alternateRowColor; - m_pColorTheme->m_patternEditor_selectedRowColor = pOther->getColorTheme()->m_patternEditor_selectedRowColor; + m_pColorTheme->m_patternEditor_selectedRowColor = + pOther->getColorTheme()->m_patternEditor_selectedRowColor; + m_pColorTheme->m_patternEditor_selectedRowTextColor = + pOther->getColorTheme()->m_patternEditor_selectedRowTextColor; + m_pColorTheme->m_patternEditor_octaveRowColor = + pOther->getColorTheme()->m_patternEditor_octaveRowColor; m_pColorTheme->m_patternEditor_textColor = pOther->getColorTheme()->m_patternEditor_textColor; - m_pColorTheme->m_patternEditor_noteColor = pOther->getColorTheme()->m_patternEditor_noteColor; - m_pColorTheme->m_patternEditor_noteoffColor = pOther->getColorTheme()->m_patternEditor_noteoffColor; + m_pColorTheme->m_patternEditor_noteVelocityFullColor = + pOther->getColorTheme()->m_patternEditor_noteVelocityFullColor; + m_pColorTheme->m_patternEditor_noteVelocityDefaultColor = + pOther->getColorTheme()->m_patternEditor_noteVelocityDefaultColor; + m_pColorTheme->m_patternEditor_noteVelocityHalfColor = + pOther->getColorTheme()->m_patternEditor_noteVelocityHalfColor; + m_pColorTheme->m_patternEditor_noteVelocityZeroColor = + pOther->getColorTheme()->m_patternEditor_noteVelocityZeroColor; + m_pColorTheme->m_patternEditor_noteOffColor = + pOther->getColorTheme()->m_patternEditor_noteOffColor; m_pColorTheme->m_patternEditor_lineColor = pOther->getColorTheme()->m_patternEditor_lineColor; m_pColorTheme->m_patternEditor_line1Color = pOther->getColorTheme()->m_patternEditor_line1Color; m_pColorTheme->m_patternEditor_line2Color = pOther->getColorTheme()->m_patternEditor_line2Color; @@ -231,8 +284,8 @@ void Theme::setTheme( const std::shared_ptr pOther ) { m_pColorTheme->m_buttonRedTextColor = pOther->getColorTheme()->m_buttonRedTextColor; m_pColorTheme->m_spinBoxColor = pOther->getColorTheme()->m_spinBoxColor; m_pColorTheme->m_spinBoxTextColor = pOther->getColorTheme()->m_spinBoxTextColor; - m_pColorTheme->m_automationColor = pOther->getColorTheme()->m_automationColor; - m_pColorTheme->m_automationCircleColor = pOther->getColorTheme()->m_automationCircleColor; + m_pColorTheme->m_playheadColor = pOther->getColorTheme()->m_playheadColor; + m_pColorTheme->m_cursorColor = pOther->getColorTheme()->m_cursorColor; m_pInterfaceTheme->m_sQTStyle = pOther->getInterfaceTheme()->m_sQTStyle; m_pInterfaceTheme->m_fMixerFalloffSpeed = pOther->getInterfaceTheme()->m_fMixerFalloffSpeed; @@ -264,19 +317,46 @@ void Theme::writeColorTheme( QDomNode* parent, std::shared_ptr pTheme ) QDomNode songEditorNode = doc.createElement( "songEditor" ); LocalFileMng::writeXmlColor( songEditorNode, "backgroundColor", pTheme->getColorTheme()->m_songEditor_backgroundColor ); LocalFileMng::writeXmlColor( songEditorNode, "alternateRowColor", pTheme->getColorTheme()->m_songEditor_alternateRowColor ); - LocalFileMng::writeXmlColor( songEditorNode, "selectedRowColor", pTheme->getColorTheme()->m_songEditor_selectedRowColor ); + LocalFileMng::writeXmlColor( songEditorNode, "selectedRowColor", + pTheme->getColorTheme()->m_songEditor_selectedRowColor ); + LocalFileMng::writeXmlColor( songEditorNode, "selectedRowTextColor", + pTheme->getColorTheme()->m_songEditor_selectedRowTextColor ); LocalFileMng::writeXmlColor( songEditorNode, "lineColor", pTheme->getColorTheme()->m_songEditor_lineColor ); LocalFileMng::writeXmlColor( songEditorNode, "textColor", pTheme->getColorTheme()->m_songEditor_textColor ); + LocalFileMng::writeXmlColor( songEditorNode, "automationBackgroundColor", + pTheme->getColorTheme()->m_songEditor_automationBackgroundColor ); + LocalFileMng::writeXmlColor( songEditorNode, "automationLineColor", + pTheme->getColorTheme()->m_songEditor_automationLineColor ); + LocalFileMng::writeXmlColor( songEditorNode, "automationNodeColor", + pTheme->getColorTheme()->m_songEditor_automationNodeColor ); + LocalFileMng::writeXmlColor( songEditorNode, "stackedModeOnColor", + pTheme->getColorTheme()->m_songEditor_stackedModeOnColor ); + LocalFileMng::writeXmlColor( songEditorNode, "stackedModeOnNextColor", + pTheme->getColorTheme()->m_songEditor_stackedModeOnNextColor ); + LocalFileMng::writeXmlColor( songEditorNode, "stackedModeOffNextColor", + pTheme->getColorTheme()->m_songEditor_stackedModeOffNextColor ); node.appendChild( songEditorNode ); // PATTERN EDITOR QDomNode patternEditorNode = doc.createElement( "patternEditor" ); LocalFileMng::writeXmlColor( patternEditorNode, "backgroundColor", pTheme->getColorTheme()->m_patternEditor_backgroundColor ); LocalFileMng::writeXmlColor( patternEditorNode, "alternateRowColor", pTheme->getColorTheme()->m_patternEditor_alternateRowColor ); - LocalFileMng::writeXmlColor( patternEditorNode, "selectedRowColor", pTheme->getColorTheme()->m_patternEditor_selectedRowColor ); + LocalFileMng::writeXmlColor( patternEditorNode, "selectedRowColor", + pTheme->getColorTheme()->m_patternEditor_selectedRowColor ); + LocalFileMng::writeXmlColor( patternEditorNode, "selectedRowTextColor", + pTheme->getColorTheme()->m_patternEditor_selectedRowTextColor ); + LocalFileMng::writeXmlColor( patternEditorNode, "octaveRowColor", + pTheme->getColorTheme()->m_patternEditor_octaveRowColor ); LocalFileMng::writeXmlColor( patternEditorNode, "textColor", pTheme->getColorTheme()->m_patternEditor_textColor ); - LocalFileMng::writeXmlColor( patternEditorNode, "noteColor", pTheme->getColorTheme()->m_patternEditor_noteColor ); - LocalFileMng::writeXmlColor( patternEditorNode, "noteoffColor", pTheme->getColorTheme()->m_patternEditor_noteoffColor ); + LocalFileMng::writeXmlColor( patternEditorNode, "noteVelocityFullColor", + pTheme->getColorTheme()->m_patternEditor_noteVelocityFullColor ); + LocalFileMng::writeXmlColor( patternEditorNode, "noteVelocityDefaultColor", + pTheme->getColorTheme()->m_patternEditor_noteVelocityDefaultColor ); + LocalFileMng::writeXmlColor( patternEditorNode, "noteVelocityHalfColor", + pTheme->getColorTheme()->m_patternEditor_noteVelocityHalfColor ); + LocalFileMng::writeXmlColor( patternEditorNode, "noteVelocityZeroColor", + pTheme->getColorTheme()->m_patternEditor_noteVelocityZeroColor ); + LocalFileMng::writeXmlColor( patternEditorNode, "noteOffColor", pTheme->getColorTheme()->m_patternEditor_noteOffColor ); LocalFileMng::writeXmlColor( patternEditorNode, "lineColor", pTheme->getColorTheme()->m_patternEditor_lineColor ); LocalFileMng::writeXmlColor( patternEditorNode, "line1Color", pTheme->getColorTheme()->m_patternEditor_line1Color ); @@ -319,8 +399,8 @@ void Theme::writeColorTheme( QDomNode* parent, std::shared_ptr pTheme ) LocalFileMng::writeXmlColor( widgetNode, "buttonRedTextColor", pTheme->getColorTheme()->m_buttonRedTextColor ); LocalFileMng::writeXmlColor( widgetNode, "spinBoxColor", pTheme->getColorTheme()->m_spinBoxColor ); LocalFileMng::writeXmlColor( widgetNode, "spinBoxTextColor", pTheme->getColorTheme()->m_spinBoxTextColor ); - LocalFileMng::writeXmlColor( widgetNode, "automationColor", pTheme->getColorTheme()->m_automationColor ); - LocalFileMng::writeXmlColor( widgetNode, "automationCircleColor", pTheme->getColorTheme()->m_automationCircleColor ); + LocalFileMng::writeXmlColor( widgetNode, "playheadColor", pTheme->getColorTheme()->m_playheadColor ); + LocalFileMng::writeXmlColor( widgetNode, "cursorColor", pTheme->getColorTheme()->m_cursorColor ); node.appendChild( widgetNode ); parent->appendChild( node ); @@ -333,9 +413,32 @@ void Theme::readColorTheme( QDomNode parent, std::shared_ptr pTheme ) if ( !pSongEditorNode.isNull() ) { pTheme->getColorTheme()->m_songEditor_backgroundColor = LocalFileMng::readXmlColor( pSongEditorNode, "backgroundColor", pTheme->getColorTheme()->m_songEditor_backgroundColor ); pTheme->getColorTheme()->m_songEditor_alternateRowColor = LocalFileMng::readXmlColor( pSongEditorNode, "alternateRowColor", pTheme->getColorTheme()->m_songEditor_alternateRowColor ); - pTheme->getColorTheme()->m_songEditor_selectedRowColor = LocalFileMng::readXmlColor( pSongEditorNode, "selectedRowColor", pTheme->getColorTheme()->m_songEditor_selectedRowColor ); + pTheme->getColorTheme()->m_songEditor_selectedRowColor = + LocalFileMng::readXmlColor( pSongEditorNode, "selectedRowColor", + pTheme->getColorTheme()->m_songEditor_selectedRowColor ); + pTheme->getColorTheme()->m_songEditor_selectedRowTextColor = + LocalFileMng::readXmlColor( pSongEditorNode, "selectedRowTextColor", + pTheme->getColorTheme()->m_songEditor_selectedRowTextColor ); pTheme->getColorTheme()->m_songEditor_lineColor = LocalFileMng::readXmlColor( pSongEditorNode, "lineColor", pTheme->getColorTheme()->m_songEditor_lineColor ); pTheme->getColorTheme()->m_songEditor_textColor = LocalFileMng::readXmlColor( pSongEditorNode, "textColor", pTheme->getColorTheme()->m_songEditor_textColor ); + pTheme->getColorTheme()->m_songEditor_automationBackgroundColor = + LocalFileMng::readXmlColor( pSongEditorNode, "automationBackgroundColor", + pTheme->getColorTheme()->m_songEditor_automationBackgroundColor ); + pTheme->getColorTheme()->m_songEditor_automationLineColor = + LocalFileMng::readXmlColor( pSongEditorNode, "automationLineColor", + pTheme->getColorTheme()->m_songEditor_automationLineColor ); + pTheme->getColorTheme()->m_songEditor_automationNodeColor = + LocalFileMng::readXmlColor( pSongEditorNode, "automationNodeColor", + pTheme->getColorTheme()->m_songEditor_automationNodeColor ); + pTheme->getColorTheme()->m_songEditor_stackedModeOnColor = + LocalFileMng::readXmlColor( pSongEditorNode, "stackedModeOnColor", + pTheme->getColorTheme()->m_songEditor_stackedModeOnColor ); + pTheme->getColorTheme()->m_songEditor_stackedModeOnNextColor = + LocalFileMng::readXmlColor( pSongEditorNode, "stackedModeOnNextColor", + pTheme->getColorTheme()->m_songEditor_stackedModeOnNextColor ); + pTheme->getColorTheme()->m_songEditor_stackedModeOffNextColor = + LocalFileMng::readXmlColor( pSongEditorNode, "stackedModeOffNextColor", + pTheme->getColorTheme()->m_songEditor_stackedModeOffNextColor ); } else { WARNINGLOG( "songEditor node not found" ); } @@ -345,10 +448,31 @@ void Theme::readColorTheme( QDomNode parent, std::shared_ptr pTheme ) if ( !pPatternEditorNode.isNull() ) { pTheme->getColorTheme()->m_patternEditor_backgroundColor = LocalFileMng::readXmlColor( pPatternEditorNode, "backgroundColor", pTheme->getColorTheme()->m_patternEditor_backgroundColor ); pTheme->getColorTheme()->m_patternEditor_alternateRowColor = LocalFileMng::readXmlColor( pPatternEditorNode, "alternateRowColor", pTheme->getColorTheme()->m_patternEditor_alternateRowColor ); - pTheme->getColorTheme()->m_patternEditor_selectedRowColor = LocalFileMng::readXmlColor( pPatternEditorNode, "selectedRowColor", pTheme->getColorTheme()->m_patternEditor_selectedRowColor ); + pTheme->getColorTheme()->m_patternEditor_selectedRowColor = + LocalFileMng::readXmlColor( pPatternEditorNode, "selectedRowColor", + pTheme->getColorTheme()->m_patternEditor_selectedRowColor ); + pTheme->getColorTheme()->m_patternEditor_selectedRowTextColor = + LocalFileMng::readXmlColor( pPatternEditorNode, "selectedRowTextColor", + pTheme->getColorTheme()->m_patternEditor_selectedRowTextColor ); + pTheme->getColorTheme()->m_patternEditor_octaveRowColor = + LocalFileMng::readXmlColor( pPatternEditorNode, "octaveRowColor", + pTheme->getColorTheme()->m_patternEditor_octaveRowColor ); pTheme->getColorTheme()->m_patternEditor_textColor = LocalFileMng::readXmlColor( pPatternEditorNode, "textColor", pTheme->getColorTheme()->m_patternEditor_textColor ); - pTheme->getColorTheme()->m_patternEditor_noteColor = LocalFileMng::readXmlColor( pPatternEditorNode, "noteColor", pTheme->getColorTheme()->m_patternEditor_noteColor ); - pTheme->getColorTheme()->m_patternEditor_noteoffColor = LocalFileMng::readXmlColor( pPatternEditorNode, "noteoffColor", pTheme->getColorTheme()->m_patternEditor_noteoffColor ); + pTheme->getColorTheme()->m_patternEditor_noteVelocityFullColor = + LocalFileMng::readXmlColor( pPatternEditorNode, "noteVelocityFullColor", + pTheme->getColorTheme()->m_patternEditor_noteVelocityFullColor ); + pTheme->getColorTheme()->m_patternEditor_noteVelocityDefaultColor = + LocalFileMng::readXmlColor( pPatternEditorNode, "noteVelocityDefaultColor", + pTheme->getColorTheme()->m_patternEditor_noteVelocityDefaultColor ); + pTheme->getColorTheme()->m_patternEditor_noteVelocityHalfColor = + LocalFileMng::readXmlColor( pPatternEditorNode, "noteVelocityHalfColor", + pTheme->getColorTheme()->m_patternEditor_noteVelocityHalfColor ); + pTheme->getColorTheme()->m_patternEditor_noteVelocityZeroColor = + LocalFileMng::readXmlColor( pPatternEditorNode, "noteVelocityZeroColor", + pTheme->getColorTheme()->m_patternEditor_noteVelocityZeroColor ); + pTheme->getColorTheme()->m_patternEditor_noteOffColor = + LocalFileMng::readXmlColor( pPatternEditorNode, "noteOffColor", + pTheme->getColorTheme()->m_patternEditor_noteOffColor ); pTheme->getColorTheme()->m_patternEditor_lineColor = LocalFileMng::readXmlColor( pPatternEditorNode, "lineColor", pTheme->getColorTheme()->m_patternEditor_lineColor ); pTheme->getColorTheme()->m_patternEditor_line1Color = LocalFileMng::readXmlColor( pPatternEditorNode, "line1Color", pTheme->getColorTheme()->m_patternEditor_line1Color ); pTheme->getColorTheme()->m_patternEditor_line2Color = LocalFileMng::readXmlColor( pPatternEditorNode, "line2Color", pTheme->getColorTheme()->m_patternEditor_line2Color ); @@ -399,8 +523,12 @@ void Theme::readColorTheme( QDomNode parent, std::shared_ptr pTheme ) pTheme->getColorTheme()->m_buttonRedTextColor = LocalFileMng::readXmlColor( pWidgetNode, "buttonRedTextColor", pTheme->getColorTheme()->m_buttonRedTextColor ); pTheme->getColorTheme()->m_spinBoxColor = LocalFileMng::readXmlColor( pWidgetNode, "spinBoxColor", pTheme->getColorTheme()->m_spinBoxColor ); pTheme->getColorTheme()->m_spinBoxTextColor = LocalFileMng::readXmlColor( pWidgetNode, "spinBoxTextColor", pTheme->getColorTheme()->m_spinBoxTextColor ); - pTheme->getColorTheme()->m_automationColor = LocalFileMng::readXmlColor( pWidgetNode, "automationColor", pTheme->getColorTheme()->m_automationColor ); - pTheme->getColorTheme()->m_automationCircleColor = LocalFileMng::readXmlColor( pWidgetNode, "automationCircleColor", pTheme->getColorTheme()->m_automationCircleColor ); + pTheme->getColorTheme()->m_playheadColor = + LocalFileMng::readXmlColor( pWidgetNode, "playheadColor", + pTheme->getColorTheme()->m_playheadColor ); + pTheme->getColorTheme()->m_cursorColor = + LocalFileMng::readXmlColor( pWidgetNode, "cursorColor", + pTheme->getColorTheme()->m_cursorColor ); } else { WARNINGLOG( "widget node not found" ); } diff --git a/src/core/Preferences/Theme.h b/src/core/Preferences/Theme.h index 0267d10512..8350484aac 100644 --- a/src/core/Preferences/Theme.h +++ b/src/core/Preferences/Theme.h @@ -49,15 +49,27 @@ class ColorTheme : public H2Core::Object QColor m_songEditor_backgroundColor; QColor m_songEditor_alternateRowColor; QColor m_songEditor_selectedRowColor; + QColor m_songEditor_selectedRowTextColor; QColor m_songEditor_lineColor; QColor m_songEditor_textColor; + QColor m_songEditor_automationBackgroundColor; + QColor m_songEditor_automationLineColor; + QColor m_songEditor_automationNodeColor; + QColor m_songEditor_stackedModeOnColor; + QColor m_songEditor_stackedModeOnNextColor; + QColor m_songEditor_stackedModeOffNextColor; QColor m_patternEditor_backgroundColor; QColor m_patternEditor_alternateRowColor; QColor m_patternEditor_selectedRowColor; + QColor m_patternEditor_selectedRowTextColor; + QColor m_patternEditor_octaveRowColor; QColor m_patternEditor_textColor; - QColor m_patternEditor_noteColor; - QColor m_patternEditor_noteoffColor; + QColor m_patternEditor_noteVelocityFullColor; + QColor m_patternEditor_noteVelocityDefaultColor; + QColor m_patternEditor_noteVelocityHalfColor; + QColor m_patternEditor_noteVelocityZeroColor; + QColor m_patternEditor_noteOffColor; QColor m_patternEditor_lineColor; QColor m_patternEditor_line1Color; QColor m_patternEditor_line2Color; @@ -109,8 +121,8 @@ class ColorTheme : public H2Core::Object QColor m_buttonRedTextColor; QColor m_spinBoxColor; QColor m_spinBoxTextColor; - QColor m_automationColor; - QColor m_automationCircleColor; + QColor m_playheadColor; + QColor m_cursorColor; }; diff --git a/src/core/Timeline.cpp b/src/core/Timeline.cpp index ddd26552d2..703b70e866 100644 --- a/src/core/Timeline.cpp +++ b/src/core/Timeline.cpp @@ -127,6 +127,14 @@ bool Timeline::hasColumnTempoMarker( int nColumn ) const { } std::shared_ptr Timeline::getTempoMarkerAtColumn( int nColumn ) const { + if ( isFirstTempoMarkerSpecial() && nColumn == 0 ) { + + std::shared_ptr pTempoMarker = std::make_shared(); + pTempoMarker->nColumn = 0; + pTempoMarker->fBpm = Hydrogen::get_instance()->getSong()->getBpm(); + return pTempoMarker; + } + for ( const auto& tt : m_tempoMarkers ){ if ( tt->nColumn == nColumn ) { return tt; @@ -264,5 +272,39 @@ QString Timeline::toQString( const QString& sPrefix, bool bShort ) const { return sOutput; } +QString Timeline::TempoMarker::toQString( const QString& sPrefix, bool bShort ) const { + QString s = Base::sPrintIndention; + QString sOutput; + if ( ! bShort ) { + sOutput = QString( "%1[TempoMarker]\n" ).arg( sPrefix ) + .append( QString( "%1%2nColumn: %3\n" ).arg( sPrefix ).arg( s ).arg( nColumn ) ) + .append( QString( "%1%2fBpm: %3\n" ).arg( sPrefix ).arg( s ).arg( fBpm ) ); + } else { + + sOutput = QString( "%1[TempoMarker] " ).arg( sPrefix ) + .append( QString( "nColumn: %3, " ).arg( nColumn ) ) + .append( QString( "fBpm: %3" ).arg( fBpm ) ); + } + + return sOutput; +} + +QString Timeline::Tag::toQString( const QString& sPrefix, bool bShort ) const { + QString s = Base::sPrintIndention; + QString sOutput; + if ( ! bShort ) { + sOutput = QString( "%1[TempoMarker]\n" ).arg( sPrefix ) + .append( QString( "%1%2nColumn: %3\n" ).arg( sPrefix ).arg( s ).arg( nColumn ) ) + .append( QString( "%1%2sTag: %3\n" ).arg( sPrefix ).arg( s ).arg( sTag ) ); + } else { + + sOutput = QString( "%1[TempoMarker] " ).arg( sPrefix ) + .append( QString( "nColumn: %3, " ).arg( nColumn ) ) + .append( QString( "sTag: %3" ).arg( sTag ) ); + } + + return sOutput; +} + }; diff --git a/src/core/Timeline.h b/src/core/Timeline.h index 4768cd7c30..03be122691 100644 --- a/src/core/Timeline.h +++ b/src/core/Timeline.h @@ -80,6 +80,8 @@ class Timeline : public H2Core::Object { int nColumn; // beat position in timeline float fBpm; // tempo in beats per minute + + QString toQString( const QString& sPrefix = "", bool bShort = true ) const; }; /** @@ -90,6 +92,8 @@ class Timeline : public H2Core::Object { int nColumn; // beat position in timeline QString sTag; // tag + + QString toQString( const QString& sPrefix = "", bool bShort = true ) const; }; /** diff --git a/src/gui/src/CommonStrings.cpp b/src/gui/src/CommonStrings.cpp index 91ad81b38d..180d45ee50 100644 --- a/src/gui/src/CommonStrings.cpp +++ b/src/gui/src/CommonStrings.cpp @@ -337,6 +337,7 @@ CommonStrings::CommonStrings(){ m_sTimelineEnabled = tr( "Enable the Timeline for custom tempo changes" ); m_sTimelineDisabledPatternMode = tr( "The Timeline is only available in Song Mode" ); m_sTimelineDisabledTimebaseSlave = tr( "In the presence of an external JACK Timebase master the tempo can not be altered from within Hydrogen" ); + m_sPatternEditorLocked = tr( "Lock the Pattern Editor to only show and follow the pattern recorded notes will be inserted into while in Song Mode." ); /*: Displayed in the Preferences dialog in the info section for a particular driver in case it is not properly supported on the diff --git a/src/gui/src/CommonStrings.h b/src/gui/src/CommonStrings.h index ab666a2093..6c4ceec4ef 100644 --- a/src/gui/src/CommonStrings.h +++ b/src/gui/src/CommonStrings.h @@ -118,10 +118,10 @@ class CommonStrings : public H2Core::Object { const QString& getMidiTooltipHeading() const { return m_sMidiTooltipHeading; } const QString& getMidiTooltipBound() const { return m_sMidiTooltipBound; } const QString& getMidiTooltipUnbound() const { return m_sMidiTooltipUnbound; } - const QString& getPatternSizeDisabledTooltip() const { return m_sPatternSizeDisabledTooltip; } const QString& getShowDrumkitEditorTooltip() const { return m_sShowDrumkitEditorTooltip; } const QString& getShowPianoRollEditorTooltip() const { return m_sShowPianoRollEditorTooltip; } + const QString& getPatternSizeDisabledTooltip() const { return m_sPatternSizeDisabledTooltip; } const QString& getMidiSenseWindowTitle() const { return m_sMidiSenseWindowTitle; } const QString& getMidiSenseInput() const { return m_sMidiSenseInput; } @@ -137,6 +137,7 @@ class CommonStrings : public H2Core::Object { const QString& getTimelineEnabled() const { return m_sTimelineEnabled; } const QString& getTimelineDisabledPatternMode() const { return m_sTimelineDisabledPatternMode; } const QString& getTimelineDisabledTimebaseSlave() const { return m_sTimelineDisabledTimebaseSlave; } + const QString& getPatternEditorLocked() const { return m_sPatternEditorLocked; } const QString& getPreferencesNotCompiled() const { return m_sPreferencesNotCompiled; } const QString& getPreferencesNone() const { return m_sPreferencesNone; } @@ -247,6 +248,7 @@ class CommonStrings : public H2Core::Object { QString m_sTimelineEnabled; QString m_sTimelineDisabledPatternMode; QString m_sTimelineDisabledTimebaseSlave; + QString m_sPatternEditorLocked; QString m_sPreferencesNotCompiled; QString m_sPreferencesNone; diff --git a/src/gui/src/EventListener.h b/src/gui/src/EventListener.h index aaf61d6c34..6869844cca 100644 --- a/src/gui/src/EventListener.h +++ b/src/gui/src/EventListener.h @@ -58,6 +58,9 @@ class EventListener virtual void actionModeChangeEvent( int nValue ){ UNUSED( nValue ); } virtual void updateSongEditorEvent( int nValue ){ UNUSED( nValue ); } virtual void drumkitLoadedEvent(){} + virtual void patternEditorLockedEvent( int nValue ){ UNUSED( nValue ); } + virtual void relocationEvent(){} + virtual void songSizeChangedEvent(){} virtual ~EventListener() {} }; diff --git a/src/gui/src/HydrogenApp.cpp b/src/gui/src/HydrogenApp.cpp index 1dfe898a7f..3e155278b0 100644 --- a/src/gui/src/HydrogenApp.cpp +++ b/src/gui/src/HydrogenApp.cpp @@ -258,6 +258,9 @@ void HydrogenApp::setupSinglePanedInterface() } else { m_pSongEditorPanel = new SongEditorPanel( m_pTab ); } + // trigger a relocation to sync the transport position of the + // editors in the panel. + H2Core::Hydrogen::get_instance()->getCoreActionController()->locateToColumn( 0 ); WindowProperties songEditorProp = pPref->getSongEditorProperties(); setWindowProperties( m_pSongEditorPanel, songEditorProp, SetWidth + SetHeight ); @@ -287,6 +290,8 @@ void HydrogenApp::setupSinglePanedInterface() // PATTERN EDITOR m_pPatternEditorPanel = new PatternEditorPanel( nullptr ); + // Sync the playhead position in all editors all objects are available. + m_pPatternEditorPanel->getPatternEditorRuler()->updatePosition( true ); WindowProperties patternEditorProp = pPref->getPatternEditorProperties(); setWindowProperties( m_pPatternEditorPanel, patternEditorProp, SetWidth + SetHeight ); @@ -835,7 +840,19 @@ void HydrogenApp::onEventQueueTimer() case EVENT_DRUMKIT_LOADED: pListener->drumkitLoadedEvent(); break; + + case EVENT_PATTERN_EDITOR_LOCKED: + pListener->patternEditorLockedEvent( event.value ); + break; + + case EVENT_RELOCATION: + pListener->relocationEvent(); + break; + case EVENT_SONG_SIZE_CHANGED: + pListener->songSizeChangedEvent(); + break; + default: ERRORLOG( QString("[onEventQueueTimer] Unhandled event: %1").arg( event.type ) ); } diff --git a/src/gui/src/InstrumentEditor/InstrumentEditor.cpp b/src/gui/src/InstrumentEditor/InstrumentEditor.cpp index 72ee07c12e..2cd800fe2d 100644 --- a/src/gui/src/InstrumentEditor/InstrumentEditor.cpp +++ b/src/gui/src/InstrumentEditor/InstrumentEditor.cpp @@ -91,8 +91,9 @@ InstrumentEditor::InstrumentEditor( QWidget* pParent ) m_pInstrumentProp->move(0, 31); m_pInstrumentProp->setPixmap( "/instrumentEditor/instrumentTab.png" ); - m_pNameLbl = new ClickableLabel( m_pInstrumentProp, QSize( 275, 28 ), "" ); - m_pNameLbl->move( 8, 5 ); + m_pNameLbl = new ClickableLabel( m_pInstrumentProp, QSize( 279, 27 ), "", + ClickableLabel::Color::Bright, true, true ); + m_pNameLbl->move( 5, 4 ); m_pNameLbl->setScaledContents( true ); ///////////// diff --git a/src/gui/src/InstrumentEditor/WaveDisplay.cpp b/src/gui/src/InstrumentEditor/WaveDisplay.cpp index 06061a8e3e..d457d45a18 100644 --- a/src/gui/src/InstrumentEditor/WaveDisplay.cpp +++ b/src/gui/src/InstrumentEditor/WaveDisplay.cpp @@ -60,42 +60,45 @@ WaveDisplay::~WaveDisplay() delete[] m_pPeakData; } -void WaveDisplay::paintEvent( QPaintEvent *ev ) -{ +void WaveDisplay::paintEvent( QPaintEvent *ev ) { UNUSED(ev); - + QPainter painter( this ); + + createBackground( &painter ); +} + +void WaveDisplay::createBackground( QPainter* painter ) { auto pPref = H2Core::Preferences::get_instance(); - QPainter painter( this ); - painter.setRenderHint( QPainter::Antialiasing ); + painter->setRenderHint( QPainter::Antialiasing ); QBrush brush = QBrush(Qt::red, m_Background); brush.setStyle(Qt::TexturePattern); - painter.setBrush(brush); - painter.drawRect(0, 0, width(), height()); + painter->setBrush(brush); + painter->drawRect(0, 0, width(), height()); if( m_pLayer ){ - painter.setPen( QColor( 102, 150, 205 ) ); + painter->setPen( QColor( 102, 150, 205 ) ); int VCenter = height() / 2; for ( int x = 0; x < width(); x++ ) { - painter.drawLine( x, VCenter, x, m_pPeakData[x] + VCenter ); - painter.drawLine( x, VCenter, x, -m_pPeakData[x] + VCenter ); + painter->drawLine( x, VCenter, x, m_pPeakData[x] + VCenter ); + painter->drawLine( x, VCenter, x, -m_pPeakData[x] + VCenter ); } } QFont font( pPref->getApplicationFontFamily(), getPointSize( pPref->getFontSize() ) ); font.setWeight( 63 ); - painter.setFont( font ); - painter.setPen( QColor( 255 , 255, 255, 200 ) ); + painter->setFont( font ); + painter->setPen( QColor( 255 , 255, 255, 200 ) ); if( m_SampleNameAlignment == Qt::AlignCenter ){ - painter.drawText( 0, 0, width(), 20, m_SampleNameAlignment, m_sSampleName ); + painter->drawText( 0, 0, width(), 20, m_SampleNameAlignment, m_sSampleName ); } else if( m_SampleNameAlignment == Qt::AlignLeft ) { // Use a small offnset iso. starting directly at the left border - painter.drawText( 20, 0, width(), 20, m_SampleNameAlignment, m_sSampleName ); + painter->drawText( 20, 0, width(), 20, m_SampleNameAlignment, m_sSampleName ); } } diff --git a/src/gui/src/InstrumentEditor/WaveDisplay.h b/src/gui/src/InstrumentEditor/WaveDisplay.h index 9e69bf6b95..5b49933ff6 100644 --- a/src/gui/src/InstrumentEditor/WaveDisplay.h +++ b/src/gui/src/InstrumentEditor/WaveDisplay.h @@ -45,6 +45,7 @@ class WaveDisplay : public QWidget, protected WidgetWithScalableFont<8, 10, 12> explicit WaveDisplay(QWidget* pParent); ~WaveDisplay(); + virtual void updateDisplay( std::shared_ptr pLayer ); virtual void paintEvent( QPaintEvent *ev ) override; @@ -60,6 +61,9 @@ public slots: void doubleClicked(QWidget *pWidget); protected: + + void createBackground( QPainter* painter ); + Qt::AlignmentFlag m_SampleNameAlignment; QPixmap m_Background; QString m_sSampleName; diff --git a/src/gui/src/MainForm.cpp b/src/gui/src/MainForm.cpp index 858913439a..89719455f0 100644 --- a/src/gui/src/MainForm.cpp +++ b/src/gui/src/MainForm.cpp @@ -832,7 +832,7 @@ void MainForm::showUserManual() } -void MainForm::action_file_export_pattern_as() +void MainForm::action_file_export_pattern_as( int nPatternRow ) { Hydrogen *pHydrogen = Hydrogen::get_instance(); @@ -840,8 +840,18 @@ void MainForm::action_file_export_pattern_as() Hydrogen::get_instance()->sequencer_stop(); } + if ( nPatternRow == -1 ) { + nPatternRow = pHydrogen->getSelectedPatternNumber(); + } + + if ( nPatternRow == -1 ) { + QMessageBox::warning( this, "Hydrogen", tr("No pattern selected.") ); + return; + } + std::shared_ptr pSong = pHydrogen->getSong(); - Pattern *pPattern = pSong->getPatternList()->get( pHydrogen->getSelectedPatternNumber() ); + + Pattern *pPattern = pSong->getPatternList()->get( nPatternRow ); QString sPath = Preferences::get_instance()->getLastExportPatternAsDirectory(); if ( ! Filesystem::dir_writable( sPath, false ) ){ @@ -930,8 +940,15 @@ void MainForm::action_file_openPattern() if ( pNewPattern == nullptr ) { QMessageBox::critical( this, "Hydrogen", HydrogenApp::get_instance()->getCommonStrings()->getPatternLoadError() ); } else { + int nRow; + if ( pHydrogen->getSelectedPatternNumber() == -1 ) { + nRow = pSong->getPatternList()->size(); + } else { + nRow = pHydrogen->getSelectedPatternNumber() + 1; + } + SE_insertPatternAction* pAction = - new SE_insertPatternAction( pHydrogen->getSelectedPatternNumber() + 1, pNewPattern ); + new SE_insertPatternAction( nRow, pNewPattern ); HydrogenApp::get_instance()->m_pUndoStack->push( pAction ); } } @@ -2366,7 +2383,6 @@ void MainForm::startPlaybackAtCursor( QObject* pObject ) { if ( pHydrogen->getMode() != Song::Mode::Pattern ) { pCoreActionController->activateSongMode( false ); - pApp->getPlayerControl()->songModeActivationEvent( 0 ); } // To provide a similar behaviour as when pressing diff --git a/src/gui/src/MainForm.h b/src/gui/src/MainForm.h index f7d7fb3b5b..5e2beb6554 100644 --- a/src/gui/src/MainForm.h +++ b/src/gui/src/MainForm.h @@ -136,7 +136,7 @@ public slots: */ void action_file_save_as(); void action_file_openPattern(); - void action_file_export_pattern_as(); + void action_file_export_pattern_as( int nPatternRow = -1 ); bool action_file_exit(); void action_file_export(); diff --git a/src/gui/src/PatternEditor/DrumPatternEditor.cpp b/src/gui/src/PatternEditor/DrumPatternEditor.cpp index 1ce845cc3c..0d1a3273a5 100644 --- a/src/gui/src/PatternEditor/DrumPatternEditor.cpp +++ b/src/gui/src/PatternEditor/DrumPatternEditor.cpp @@ -22,7 +22,8 @@ #include "DrumPatternEditor.h" #include "PatternEditorPanel.h" -#include "NotePropertiesRuler.h" +#include "PatternEditorRuler.h" +#include "PatternEditorInstrumentList.h" #include #include @@ -42,6 +43,7 @@ #include "UndoActions.h" #include "../HydrogenApp.h" #include "../Mixer/Mixer.h" +#include "../Skin.h" #include #include @@ -51,30 +53,28 @@ using namespace H2Core; DrumPatternEditor::DrumPatternEditor(QWidget* parent, PatternEditorPanel *panel) - : PatternEditor( parent, panel ), - m_nRealColumn(0), - m_nColumn(0), - m_nRow(0), - m_nOldLength(0) + : PatternEditor( parent, panel ) { + m_editor = PatternEditor::Editor::DrumPattern; auto pPref = H2Core::Preferences::get_instance(); m_nGridHeight = pPref->getPatternEditorGridHeight(); m_nEditorHeight = m_nGridHeight * MAX_INSTRUMENTS; + m_nActiveWidth = m_nEditorWidth; resize( m_nEditorWidth, m_nEditorHeight ); Hydrogen::get_instance()->setSelectedInstrumentNumber( 0 ); + createBackground(); } DrumPatternEditor::~DrumPatternEditor() { } - - void DrumPatternEditor::updateEditor( bool bPatternOnly ) { - auto pAudioEngine = H2Core::Hydrogen::get_instance()->getAudioEngine(); + auto pHydrogen = H2Core::Hydrogen::get_instance(); + auto pAudioEngine = pHydrogen->getAudioEngine(); if ( pAudioEngine->getState() != H2Core::AudioEngine::State::Ready && pAudioEngine->getState() != H2Core::AudioEngine::State::Playing ) { ERRORLOG( "FIXME: skipping pattern editor update (state should be READY or PLAYING)" ); @@ -83,21 +83,40 @@ void DrumPatternEditor::updateEditor( bool bPatternOnly ) updatePatternInfo(); - if ( m_pPattern ) { - m_nEditorWidth = m_nMargin + m_fGridWidth * m_pPattern->get_length(); + if ( m_pPattern != nullptr ) { + + m_nActiveWidth = PatternEditor::nMargin + m_fGridWidth * + m_pPattern->get_length(); + + if ( pHydrogen->getPatternMode() == Song::PatternMode::Stacked ) { + m_nEditorWidth = + std::max( PatternEditor::nMargin + m_fGridWidth * + pAudioEngine->getPlayingPatterns()->longest_pattern_length() + 1, + static_cast(m_nActiveWidth) ); + } else { + m_nEditorWidth = m_nActiveWidth; + } } else { - m_nEditorWidth = m_nMargin + m_fGridWidth * MAX_NOTES; + m_nEditorWidth = PatternEditor::nMargin + m_fGridWidth * MAX_NOTES; + m_nActiveWidth = m_nEditorWidth; } resize( m_nEditorWidth, height() ); // redraw all + createBackground(); update( 0, 0, width(), height() ); } void DrumPatternEditor::addOrRemoveNote( int nColumn, int nRealColumn, int row, bool bDoAdd, bool bDoDelete ) { + + if ( m_pPattern == nullptr || m_nSelectedPatternNumber == -1 ) { + // No pattern selected. + return; + } + std::shared_ptr pSong = Hydrogen::get_instance()->getSong(); auto pSelectedInstrument = pSong->getInstrumentList()->get( row ); H2Core::Note *pOldNote = m_pPattern->find_note( nColumn, nRealColumn, pSelectedInstrument ); @@ -154,8 +173,9 @@ void DrumPatternEditor::addOrRemoveNote( int nColumn, int nRealColumn, int row, void DrumPatternEditor::mouseClickEvent( QMouseEvent *ev ) { + auto pHydrogenApp = HydrogenApp::get_instance(); Hydrogen *pHydrogen = Hydrogen::get_instance(); - if ( m_pPattern == nullptr ) { + if ( m_pPattern == nullptr || m_nSelectedPatternNumber == -1 ) { return; } std::shared_ptr pSong = pHydrogen->getSong(); @@ -167,12 +187,11 @@ void DrumPatternEditor::mouseClickEvent( QMouseEvent *ev ) int nColumn = getColumn( ev->x(), /* bUseFineGrained=*/ true ); int nRealColumn = 0; - if( ev->x() > m_nMargin ) { - nRealColumn = ( ev->x() - m_nMargin) / static_cast(m_fGridWidth); + if( ev->x() > PatternEditor::nMargin ) { + nRealColumn = ( ev->x() - PatternEditor::nMargin) / static_cast(m_fGridWidth); } if ( nColumn >= (int)m_pPattern->get_length() ) { - update( 0, 0, width(), height() ); return; } auto pSelectedInstrument = pSong->getInstrumentList()->get( row ); @@ -180,7 +199,6 @@ void DrumPatternEditor::mouseClickEvent( QMouseEvent *ev ) if( ev->button() == Qt::LeftButton && (ev->modifiers() & Qt::ShiftModifier) ) { //shift + leftClick: add noteOff note - HydrogenApp *pApp = HydrogenApp::get_instance(); Note *pNote = m_pPattern->find_note( nColumn, nRealColumn, pSelectedInstrument, false ); if ( pNote != nullptr ) { SE_addOrDeleteNoteAction *action = new SE_addOrDeleteNoteAction( nColumn, @@ -198,68 +216,144 @@ void DrumPatternEditor::mouseClickEvent( QMouseEvent *ev ) false, false, pNote->get_note_off() ); - pApp->m_pUndoStack->push( action ); + pHydrogenApp->m_pUndoStack->push( action ); } else { // Add stop-note SE_addNoteOffAction *action = new SE_addNoteOffAction( nColumn, row, m_nSelectedPatternNumber, pNote != nullptr ); - pApp->m_pUndoStack->push( action ); + pHydrogenApp->m_pUndoStack->push( action ); } } else if ( ev->button() == Qt::LeftButton ) { - pHydrogen->setSelectedInstrumentNumber( row ); addOrRemoveNote( nColumn, nRealColumn, row ); m_selection.clearSelection(); } else if ( ev->button() == Qt::RightButton ) { m_pPopupMenu->popup( ev->globalPos() ); - pHydrogen->setSelectedInstrumentNumber( row ); - - } else { - // Other clicks may also set instrument - pHydrogen->setSelectedInstrumentNumber( row ); } m_pPatternEditorPanel->setCursorPosition( nColumn ); - HydrogenApp::get_instance()->setHideKeyboardCursor( true ); + + // Cursor either just got hidden or was moved. + if ( ! pHydrogenApp->hideKeyboardCursor() ) { + // Immediate update to prevent visual delay. + m_pPatternEditorPanel->getInstrumentList()->repaintInstrumentLines(); + m_pPatternEditorPanel->getPatternEditorRuler()->update(); + } update(); } +void DrumPatternEditor::mousePressEvent( QMouseEvent* ev ) { + + if ( ev->x() > m_nActiveWidth ) { + return; + } + + PatternEditor::mousePressEvent( ev ); + + auto pHydrogenApp = HydrogenApp::get_instance(); + auto pHydrogen = Hydrogen::get_instance(); + auto pSong = pHydrogen->getSong(); + int nInstruments = pSong->getInstrumentList()->size(); + int nRow = static_cast( ev->y() / static_cast(m_nGridHeight) ); + if ( nRow >= nInstruments ) { + return; + } + + pHydrogen->setSelectedInstrumentNumber( nRow ); + + // Hide cursor in case this behavior was selected in the + // Preferences. + bool bOldCursorHidden = pHydrogenApp->hideKeyboardCursor(); + pHydrogenApp->setHideKeyboardCursor( true ); + + // Cursor just got hidden. + if ( bOldCursorHidden != pHydrogenApp->hideKeyboardCursor() ) { + // Immediate update to prevent visual delay. + m_pPatternEditorPanel->getInstrumentList()->repaintInstrumentLines(); + m_pPatternEditorPanel->getPatternEditorRuler()->update(); + update(); + } + + // Update cursor position + if ( ! HydrogenApp::get_instance()->hideKeyboardCursor() ) { + int nColumn = getColumn( ev->x(), /* bUseFineGrained=*/ true ); + if ( ( m_pPattern != nullptr && + nColumn >= (int)m_pPattern->get_length() ) || + nColumn >= MAX_INSTRUMENTS ) { + return; + } + + pHydrogen->setSelectedInstrumentNumber( nRow ); + m_pPatternEditorPanel->setCursorPosition( nColumn ); + + update(); + m_pPatternEditorPanel->getInstrumentList()->selectedInstrumentChangedEvent(); + m_pPatternEditorPanel->getPatternEditorRuler()->update(); + } +} + + void DrumPatternEditor::mouseDragStartEvent( QMouseEvent *ev ) { - int row = (int)( ev->y() / (float)m_nGridHeight); - Hydrogen *pHydrogen = Hydrogen::get_instance(); - std::shared_ptr pSong = pHydrogen->getSong(); + if ( m_pPattern == nullptr ) { + return; + } + + auto pHydrogen = Hydrogen::get_instance(); + auto pSong = pHydrogen->getSong(); + + // Set the selected instrument _before_ it will be stored in + // PatternEditor::mouseDragStartEvent. + int nRow = std::floor(static_cast(ev->y()) / + static_cast(m_nGridHeight)); + pHydrogen->setSelectedInstrumentNumber( nRow ); + auto pSelectedInstrument = pSong->getInstrumentList()->get( nRow ); + + // Handles cursor repositioning and hiding and stores general + // properties. + PatternEditor::mouseDragStartEvent( ev ); + int nColumn = getColumn( ev->x() ); + if ( ev->button() == Qt::RightButton ) { // Right button drag: adjust note length int nRealColumn = 0; - auto pSelectedInstrument = pSong->getInstrumentList()->get( row ); - if( ev->x() > m_nMargin ) { - nRealColumn = ( ev->x() - m_nMargin) / static_cast(m_fGridWidth); + if( ev->x() > PatternEditor::nMargin ) { + nRealColumn = + static_cast(std::floor( + static_cast((ev->x() - PatternEditor::nMargin)) / + m_fGridWidth)); } - m_pDraggedNote = m_pPattern->find_note( nColumn, nRealColumn, pSelectedInstrument, false ); - // needed for undo note length - m_nRealColumn = nRealColumn; - m_nColumn = nColumn; - m_nRow = row; - if( m_pDraggedNote ){ - m_nOldLength = m_pDraggedNote->get_length(); - } else { - m_nOldLength = -1; - } - } else { - // Other drag (selection or move) we'll set the cursor input position to the start of the gesture - pHydrogen->setSelectedInstrumentNumber( row ); - m_pPatternEditorPanel->setCursorPosition( nColumn ); - HydrogenApp::get_instance()->setHideKeyboardCursor( true ); + m_pDraggedNote = m_pPattern->find_note( nColumn, nRealColumn, + pSelectedInstrument, false ); + + // Store note-specific properties. + storeNoteProperties( m_pDraggedNote ); + + m_nRow = nRow; } } +/// +/// Update the state during a Selection drag. +/// +void DrumPatternEditor::mouseDragUpdateEvent( QMouseEvent *ev ) +{ + int nRow = MAX_INSTRUMENTS - 1 - + static_cast(std::floor(static_cast(ev->y()) / + static_cast(m_nGridHeight))); + if ( nRow >= MAX_INSTRUMENTS ) { + return; + } + + PatternEditor::mouseDragUpdateEvent( ev ); +} + void DrumPatternEditor::addOrDeleteNoteAction( int nColumn, int row, int selectedPatternNumber, @@ -371,6 +465,10 @@ void DrumPatternEditor::moveNoteAction( int nColumn, int nNewRow, Note *pNote) { + if ( m_pPattern == nullptr ) { + return; + } + Hydrogen *pHydrogen = Hydrogen::get_instance(); std::shared_ptr pSong = pHydrogen->getSong(); @@ -436,26 +534,6 @@ void DrumPatternEditor::moveNoteAction( int nColumn, m_pPatternEditorPanel->updateEditors(); } - -void DrumPatternEditor::mouseDragEndEvent( QMouseEvent *ev ) -{ - UNUSED( ev ); - unsetCursor(); - - if (m_pPattern == nullptr) { - return; - } - - if ( m_pDraggedNote ) { - if ( m_pDraggedNote->get_note_off() ) return; - - SE_editNoteLenghtAction *action = new SE_editNoteLenghtAction( m_pDraggedNote->get_position(), m_pDraggedNote->get_position(), m_nRow, m_pDraggedNote->get_length(),m_nOldLength, m_nSelectedPatternNumber ); - HydrogenApp::get_instance()->m_pUndoStack->push( action ); - m_pDraggedNote = nullptr; - } -} - - /// /// Move or copy notes. /// @@ -464,6 +542,11 @@ void DrumPatternEditor::mouseDragEndEvent( QMouseEvent *ev ) /// void DrumPatternEditor::selectionMoveEndEvent( QInputEvent *ev ) { + if ( m_pPattern == nullptr || m_nSelectedPatternNumber == -1 ) { + // No pattern selected. + return; + } + updateModifiers( ev ); QPoint offset = movingGridOffset(); if ( offset.x() == 0 && offset.y() == 0 ) { @@ -552,80 +635,6 @@ void DrumPatternEditor::selectionMoveEndEvent( QInputEvent *ev ) } -void DrumPatternEditor::editNoteLengthAction( int nColumn, int nRealColumn, int row, int length, int selectedPatternNumber ) -{ - Hydrogen *pHydrogen = Hydrogen::get_instance(); - PatternList *pPatternList = pHydrogen->getSong()->getPatternList(); - - H2Core::Pattern *pPattern = nullptr; - if ( (selectedPatternNumber != -1) && ( (uint)selectedPatternNumber < pPatternList->size() ) ) { - pPattern = pPatternList->get( selectedPatternNumber ); - } - - if( pPattern ) { - Note *pDraggedNote = nullptr; - std::shared_ptr pSong = pHydrogen->getSong(); - auto pSelectedInstrument = pSong->getInstrumentList()->get( row ); - - m_pAudioEngine->lock( RIGHT_HERE ); - - pDraggedNote = pPattern->find_note( nColumn, nRealColumn, pSelectedInstrument, false ); - if( pDraggedNote ){ - pDraggedNote->set_length( length ); - } - - pHydrogen->setIsModified( true ); - m_pAudioEngine->unlock(); - - m_pPatternEditorPanel->updateEditors(); - } -} - - -/// -/// Update the state during a Selection drag. -/// -void DrumPatternEditor::mouseDragUpdateEvent( QMouseEvent *ev ) -{ - if (m_pPattern == nullptr) { - return; - } - - int row = MAX_INSTRUMENTS - 1 - (ev->y() / (int)m_nGridHeight); - if (row >= MAX_INSTRUMENTS) { - return; - } - - if ( m_pDraggedNote ) { - if ( m_pDraggedNote->get_note_off() ) return; - int nTickColumn = getColumn( ev->x() ); - - m_pAudioEngine->lock( RIGHT_HERE ); // lock the audio engine - int nLen = nTickColumn - (int)m_pDraggedNote->get_position(); - - if (nLen <= 0) { - nLen = -1; - } - - float fNotePitch = m_pDraggedNote->get_octave() * 12 + m_pDraggedNote->get_key(); - float fStep = 0; - if(nLen > -1){ - fStep = Note::pitchToFrequency( ( double )fNotePitch ); - }else - { - fStep = 1.0; - } - m_pDraggedNote->set_length( nLen * fStep); - - Hydrogen::get_instance()->setIsModified( true ); - m_pAudioEngine->unlock(); // unlock the audio engine - - m_pPatternEditorPanel->updateEditors(); - } - -} - - /// /// Handle key press events. /// @@ -633,10 +642,17 @@ void DrumPatternEditor::mouseDragUpdateEvent( QMouseEvent *ev ) /// void DrumPatternEditor::keyPressEvent( QKeyEvent *ev ) { + if ( m_pPattern == nullptr ) { + return; + } + + auto pHydrogenApp = HydrogenApp::get_instance(); + bool bOldCursorHidden = pHydrogenApp->hideKeyboardCursor(); + const int nBlockSize = 5, nWordSize = 5; - Hydrogen *pH2 = Hydrogen::get_instance(); - int nSelectedInstrument = pH2->getSelectedInstrumentNumber(); - int nMaxInstrument = pH2->getSong()->getInstrumentList()->size(); + Hydrogen *pHydrogen = Hydrogen::get_instance(); + int nSelectedInstrument = pHydrogen->getSelectedInstrumentNumber(); + int nMaxInstrument = pHydrogen->getSong()->getInstrumentList()->size(); bool bUnhideCursor = true; bool bIsSelectionKey = m_selection.keyPressEvent( ev ); @@ -670,10 +686,10 @@ void DrumPatternEditor::keyPressEvent( QKeyEvent *ev ) } else if ( ev->matches( QKeySequence::MoveToNextLine ) || ev->matches( QKeySequence::SelectNextLine ) ) { if ( nSelectedInstrument + 1 < nMaxInstrument ) { - pH2->setSelectedInstrumentNumber( nSelectedInstrument + 1 ); + pHydrogen->setSelectedInstrumentNumber( nSelectedInstrument + 1 ); } } else if ( ev->matches( QKeySequence::MoveToEndOfBlock ) || ev->matches( QKeySequence::SelectEndOfBlock ) ) { - pH2->setSelectedInstrumentNumber( std::min( nSelectedInstrument + nBlockSize, + pHydrogen->setSelectedInstrumentNumber( std::min( nSelectedInstrument + nBlockSize, nMaxInstrument-1 ) ); } else if ( ev->matches( QKeySequence::MoveToNextPage ) || ev->matches( QKeySequence::SelectNextPage ) ) { @@ -685,17 +701,17 @@ void DrumPatternEditor::keyPressEvent( QKeyEvent *ev ) if ( nSelectedInstrument >= nMaxInstrument ) { nSelectedInstrument = nMaxInstrument - 1; } - pH2->setSelectedInstrumentNumber( nSelectedInstrument ); + pHydrogen->setSelectedInstrumentNumber( nSelectedInstrument ); } else if ( ev->matches( QKeySequence::MoveToEndOfDocument ) || ev->matches( QKeySequence::SelectEndOfDocument ) ) { - pH2->setSelectedInstrumentNumber( nMaxInstrument-1 ); + pHydrogen->setSelectedInstrumentNumber( nMaxInstrument-1 ); } else if ( ev->matches( QKeySequence::MoveToPreviousLine ) || ev->matches( QKeySequence::SelectPreviousLine ) ) { if ( nSelectedInstrument > 0 ) { - pH2->setSelectedInstrumentNumber( nSelectedInstrument - 1 ); + pHydrogen->setSelectedInstrumentNumber( nSelectedInstrument - 1 ); } } else if ( ev->matches( QKeySequence::MoveToStartOfBlock ) || ev->matches( QKeySequence::SelectStartOfBlock ) ) { - pH2->setSelectedInstrumentNumber( std::max( nSelectedInstrument - nBlockSize, 0 ) ); + pHydrogen->setSelectedInstrumentNumber( std::max( nSelectedInstrument - nBlockSize, 0 ) ); } else if ( ev->matches( QKeySequence::MoveToPreviousPage ) || ev->matches( QKeySequence::SelectPreviousPage ) ) { QWidget *pParent = dynamic_cast< QWidget *>( parent() ); @@ -704,10 +720,10 @@ void DrumPatternEditor::keyPressEvent( QKeyEvent *ev ) if ( nSelectedInstrument < 0 ) { nSelectedInstrument = 0; } - pH2->setSelectedInstrumentNumber( nSelectedInstrument ); + pHydrogen->setSelectedInstrumentNumber( nSelectedInstrument ); } else if ( ev->matches( QKeySequence::MoveToStartOfDocument ) || ev->matches( QKeySequence::SelectStartOfDocument ) ) { - pH2->setSelectedInstrumentNumber( 0 ); + pHydrogen->setSelectedInstrumentNumber( 0 ); } else if ( ev->key() == Qt::Key_Enter || ev->key() == Qt::Key_Return ) { // Key: Enter / Return: add or remove note at current position @@ -749,13 +765,26 @@ void DrumPatternEditor::keyPressEvent( QKeyEvent *ev ) } else { ev->ignore(); + pHydrogenApp->setHideKeyboardCursor( true ); + + if ( bOldCursorHidden != pHydrogenApp->hideKeyboardCursor() ) { + m_pPatternEditorPanel->getInstrumentList()->repaintInstrumentLines(); + m_pPatternEditorPanel->getPatternEditorRuler()->update(); + update(); + } return; } if ( bUnhideCursor ) { - HydrogenApp::get_instance()->setHideKeyboardCursor( false ); + pHydrogenApp->setHideKeyboardCursor( false ); } m_selection.updateKeyboardCursorPosition( getKeyboardCursorRect() ); m_pPatternEditorPanel->ensureCursorVisible(); + + if ( ! pHydrogenApp->hideKeyboardCursor() ) { + // Immediate update to prevent visual delay. + m_pPatternEditorPanel->getInstrumentList()->repaintInstrumentLines(); + m_pPatternEditorPanel->getPatternEditorRuler()->update(); + } update(); ev->accept(); @@ -772,6 +801,11 @@ void DrumPatternEditor::keyReleaseEvent( QKeyEvent *ev ) { /// std::vector DrumPatternEditor::elementsIntersecting( QRect r ) { + std::vector result; + if ( m_pPattern == nullptr ) { + return std::move( result ); + } + std::shared_ptr pSong = Hydrogen::get_instance()->getSong(); InstrumentList * pInstrList = pSong->getInstrumentList(); uint h = m_nGridHeight / 3; @@ -790,16 +824,15 @@ std::vector DrumPatternEditor::elementsInters // Calculate the first and last position values that this rect will intersect with - int x_min = (r.left() - m_nMargin - 1) / m_fGridWidth; - int x_max = (r.right() - m_nMargin) / m_fGridWidth; + int x_min = (r.left() - PatternEditor::nMargin - 1) / m_fGridWidth; + int x_max = (r.right() - PatternEditor::nMargin) / m_fGridWidth; const Pattern::notes_t* notes = m_pPattern->get_notes(); - std::vector result; for (auto it = notes->lower_bound( x_min ); it != notes->end() && it->first <= x_max; ++it ) { Note *note = it->second; int nInstrument = pInstrList->index( note->get_instrument() ); - uint x_pos = m_nMargin + (it->first * m_fGridWidth); + uint x_pos = PatternEditor::nMargin + (it->first * m_fGridWidth); uint y_pos = ( nInstrument * m_nGridHeight) + (m_nGridHeight / 2) - 3; if ( r.contains( QPoint( x_pos, y_pos + h/2) ) ) { @@ -816,7 +849,7 @@ std::vector DrumPatternEditor::elementsInters QRect DrumPatternEditor::getKeyboardCursorRect() { - uint x = m_nMargin + m_pPatternEditorPanel->getCursorPosition() * m_fGridWidth; + uint x = PatternEditor::nMargin + m_pPatternEditorPanel->getCursorPosition() * m_fGridWidth; int nSelectedInstrument = Hydrogen::get_instance()->getSelectedInstrumentNumber(); uint y = nSelectedInstrument * m_nGridHeight; return QRect( x-m_fGridWidth*3, y+2, m_fGridWidth*6, m_nGridHeight-3 ); @@ -825,6 +858,10 @@ QRect DrumPatternEditor::getKeyboardCursorRect() void DrumPatternEditor::selectAll() { + if ( m_pPattern == nullptr ) { + return; + } + m_selection.clearSelection(); FOREACH_NOTE_CST_IT_BEGIN_END(m_pPattern->get_notes(), it) { m_selection.addToSelection( it->second ); @@ -835,6 +872,11 @@ void DrumPatternEditor::selectAll() void DrumPatternEditor::deleteSelection() { + if ( m_nSelectedPatternNumber == -1 ) { + // No pattern selected. + return; + } + if ( m_selection.begin() != m_selection.end() ) { // Selection exists, delete it. Hydrogen *pHydrogen = Hydrogen::get_instance(); @@ -882,6 +924,11 @@ void DrumPatternEditor::deleteSelection() /// void DrumPatternEditor::paste() { + if ( m_pPattern == nullptr || m_nSelectedPatternNumber == -1 ) { + // No pattern selected. + return; + } + QClipboard *clipboard = QApplication::clipboard(); QUndoStack *pUndo = HydrogenApp::get_instance()->m_pUndoStack; InstrumentList *pInstrList = Hydrogen::get_instance()->getSong()->getInstrumentList(); @@ -989,56 +1036,13 @@ void DrumPatternEditor::paste() /// /// Draws a pattern /// -void DrumPatternEditor::__draw_pattern(QPainter& painter) +void DrumPatternEditor::drawPattern(QPainter& painter) { auto pPref = H2Core::Preferences::get_instance(); - - const QColor selectedRowColor( pPref->getColorTheme()->m_patternEditor_selectedRowColor ); - - __create_background( painter ); - - if (m_pPattern == nullptr) { - return; - } - int nNotes = m_pPattern->get_length(); - int nSelectedInstrument = Hydrogen::get_instance()->getSelectedInstrumentNumber(); std::shared_ptr pSong = Hydrogen::get_instance()->getSong(); - InstrumentList * pInstrList = pSong->getInstrumentList(); - - if ( m_nEditorHeight != (int)( m_nGridHeight * pInstrList->size() ) ) { - // the number of instruments is changed...recreate all - m_nEditorHeight = m_nGridHeight * pInstrList->size(); - resize( width(), m_nEditorHeight ); - } - - for ( uint nInstr = 0; nInstr < pInstrList->size(); ++nInstr ) { - uint y = m_nGridHeight * nInstr; - if ( nInstr == (uint)nSelectedInstrument ) { // selected instrument - painter.fillRect( 0, y + 1, ( m_nMargin + nNotes * m_fGridWidth ), m_nGridHeight - 1, selectedRowColor ); - } - } - - - // draw the grid - __draw_grid( painter ); - - - // Draw cursor - if ( hasFocus() && !HydrogenApp::get_instance()->hideKeyboardCursor() ) { - uint x = m_nMargin + m_pPatternEditorPanel->getCursorPosition() * m_fGridWidth; - int nSelectedInstrument = Hydrogen::get_instance()->getSelectedInstrumentNumber(); - uint y = nSelectedInstrument * m_nGridHeight; - QPen p( Qt::black ); - p.setWidth( 2 ); - painter.setPen( p ); - painter.setRenderHint( QPainter::Antialiasing ); - painter.drawRoundedRect( QRect( x-m_fGridWidth*3, y+2, m_fGridWidth*6, m_nGridHeight-3 ), 4, 4 ); - } - - /* BUGFIX @@ -1080,7 +1084,7 @@ void DrumPatternEditor::__draw_pattern(QPainter& painter) instruments.push( pNote->get_instrument() ); } - __draw_note( pNote, painter, bIsForeground ); + drawNote( pNote, painter, bIsForeground ); ++noteIt; } @@ -1092,7 +1096,7 @@ void DrumPatternEditor::__draw_pattern(QPainter& painter) if ( noteCount[ nInstrumentID ] > 1 ) { // Draw "2x" text to the left of the note int nInstrument = pInstrList->index( pInstrument ); - int x = m_nMargin + (nPosition * m_fGridWidth); + int x = PatternEditor::nMargin + (nPosition * m_fGridWidth); int y = ( nInstrument * m_nGridHeight); const int boxWidth = 128; @@ -1118,7 +1122,7 @@ void DrumPatternEditor::__draw_pattern(QPainter& painter) /// /// Draws a note /// -void DrumPatternEditor::__draw_note( Note *note, QPainter& p, bool bIsForeground ) +void DrumPatternEditor::drawNote( Note *note, QPainter& p, bool bIsForeground ) { InstrumentList *pInstrList = Hydrogen::get_instance()->getSong()->getInstrumentList(); int nInstrument = pInstrList->index( note->get_instrument() ); @@ -1127,63 +1131,27 @@ void DrumPatternEditor::__draw_note( Note *note, QPainter& p, bool bIsForeground return; } - QPoint pos ( m_nMargin + note->get_position() * m_fGridWidth, + QPoint pos ( PatternEditor::nMargin + note->get_position() * m_fGridWidth, ( nInstrument * m_nGridHeight) + (m_nGridHeight / 2) - 3 ); drawNoteSymbol( p, pos, note, bIsForeground ); } - - - -void DrumPatternEditor::__draw_grid( QPainter& p ) +void DrumPatternEditor::drawBackground( QPainter& p) { - - auto pPref = H2Core::Preferences::get_instance(); - - // Start with generic pattern editor grid lining. - drawGridLines( p ); - - int nNotes = MAX_NOTES; - if ( m_pPattern ) { - nNotes = m_pPattern->get_length(); - } - - // fill the first half of the rect with a solid color - const QColor backgroundColor( pPref->getColorTheme()->m_patternEditor_backgroundColor ); - const QColor selectedRowColor( pPref->getColorTheme()->m_patternEditor_selectedRowColor ); - int nSelectedInstrument = Hydrogen::get_instance()->getSelectedInstrumentNumber(); - std::shared_ptr pSong = Hydrogen::get_instance()->getSong(); - int nInstruments = pSong->getInstrumentList()->size(); - for ( uint i = 0; i < (uint)nInstruments; i++ ) { - uint y = m_nGridHeight * i + 1; - if ( i == (uint)nSelectedInstrument ) { - p.fillRect( 0, y, (m_nMargin + nNotes * m_fGridWidth), (int)( m_nGridHeight * 0.7 ), selectedRowColor ); - } - else { - p.fillRect( 0, y, (m_nMargin + nNotes * m_fGridWidth), (int)( m_nGridHeight * 0.7 ), backgroundColor ); - } - } - -} - - -void DrumPatternEditor::__create_background( QPainter& p) -{ - auto pPref = H2Core::Preferences::get_instance(); + auto pHydrogen = H2Core::Hydrogen::get_instance(); const QColor backgroundColor( pPref->getColorTheme()->m_patternEditor_backgroundColor ); + const QColor backgroundInactiveColor( pPref->getColorTheme()->m_windowColor ); const QColor alternateRowColor( pPref->getColorTheme()->m_patternEditor_alternateRowColor ); + const QColor selectedRowColor( pPref->getColorTheme()->m_patternEditor_selectedRowColor ); const QColor lineColor( pPref->getColorTheme()->m_patternEditor_lineColor ); + const QColor lineInactiveColor( pPref->getColorTheme()->m_windowTextColor.darker( 170 ) ); - int nNotes = MAX_NOTES; - if ( m_pPattern ) { - nNotes = m_pPattern->get_length(); - } - - std::shared_ptr pSong = Hydrogen::get_instance()->getSong(); + std::shared_ptr pSong = pHydrogen->getSong(); int nInstruments = pSong->getInstrumentList()->size(); + int nSelectedInstrument = pHydrogen->getSelectedInstrumentNumber(); if ( m_nEditorHeight != (int)( m_nGridHeight * nInstruments ) ) { // the number of instruments is changed...recreate all @@ -1191,34 +1159,143 @@ void DrumPatternEditor::__create_background( QPainter& p) resize( width(), m_nEditorHeight ); } - p.fillRect(0, 0, m_nMargin + nNotes * m_fGridWidth, height(), backgroundColor); - for ( uint i = 0; i < (uint)nInstruments; i++ ) { - uint y = m_nGridHeight * i; - if ( ( i % 2) != 0) { - p.fillRect( 0, y, (m_nMargin + nNotes * m_fGridWidth), m_nGridHeight, alternateRowColor ); + p.fillRect(0, 0, m_nActiveWidth, height(), backgroundColor); + p.fillRect(m_nActiveWidth, 0, m_nEditorWidth - m_nActiveWidth, height(), + backgroundInactiveColor); + + for ( int ii = 0; ii < nInstruments; ii++ ) { + int y = static_cast(m_nGridHeight) * ii; + if ( ii == nSelectedInstrument ) { + p.fillRect( 0, y, m_nActiveWidth, m_nGridHeight, + selectedRowColor ); + } + else if ( ( ii % 2 ) != 0 ) { + p.fillRect( 0, y, m_nActiveWidth, m_nGridHeight, alternateRowColor ); } } // horizontal lines - p.setPen( lineColor ); + p.setPen( QPen( lineColor, 2, Qt::SolidLine ) ); for ( uint i = 0; i < (uint)nInstruments; i++ ) { uint y = m_nGridHeight * i + m_nGridHeight; - p.drawLine( 0, y, (m_nMargin + nNotes * m_fGridWidth), y); + p.drawLine( 0, y, m_nActiveWidth, y); + } + + if ( m_nActiveWidth + 1 < m_nEditorWidth ) { + p.setPen( lineInactiveColor ); + for ( uint i = 0; i < (uint)nInstruments; i++ ) { + uint y = m_nGridHeight * i + m_nGridHeight; + p.drawLine( m_nActiveWidth, y, m_nEditorWidth, y); + } } - p.drawLine( 0, m_nEditorHeight, (m_nMargin + nNotes * m_fGridWidth), m_nEditorHeight ); + // We skip the grid and cursor in case there is no pattern. This + // way it may be more obvious that it is not armed and does not + // expect user interaction. + if ( m_pPattern == nullptr ) { + return; + } + drawGridLines( p ); + + // The grid lines above are drawn full height. We will erase the + // upper part. + for ( int ii = 0; ii < nInstruments; ii++ ) { + int y = static_cast(m_nGridHeight) * ii; + if ( ii == nSelectedInstrument ) { + p.fillRect( 0, y, m_nActiveWidth, (int)( m_nGridHeight * 0.7 ), selectedRowColor ); + } else { + if ( ( ii % 2 ) == 0 ) { + p.fillRect( 0, y, m_nActiveWidth, (int)( m_nGridHeight * 0.7 ), backgroundColor ); + } else { + p.fillRect( 0, y, m_nActiveWidth, + (int)( m_nGridHeight * 0.7 ), alternateRowColor ); + } + } + + p.fillRect( m_nActiveWidth, y, m_nEditorWidth - m_nActiveWidth, + (int)( m_nGridHeight * 0.7 ), backgroundInactiveColor ); + } + + // borders + p.setPen( lineColor ); + p.drawLine( 0, m_nEditorHeight -1 , m_nActiveWidth - 1, m_nEditorHeight - 1 ); + + if ( m_nEditorWidth > m_nActiveWidth + 1 ) { + p.setPen( lineInactiveColor ); + p.drawLine( m_nActiveWidth - 1, m_nEditorHeight - 1, m_nEditorWidth - 1, m_nEditorHeight - 1 ); + } + + p.setPen( QPen( lineColor, 2, Qt::SolidLine ) ); + p.drawLine( m_nEditorWidth, 0, m_nEditorWidth, m_nEditorHeight ); + } +void DrumPatternEditor::createBackground() { + // Resize pixmap if pixel ratio has changed + qreal pixelRatio = devicePixelRatio(); + if ( m_pBackgroundPixmap->width() != m_nEditorWidth || + m_pBackgroundPixmap->height() != m_nEditorHeight || + m_pBackgroundPixmap->devicePixelRatio() != pixelRatio ) { + delete m_pBackgroundPixmap; + m_pBackgroundPixmap = new QPixmap( width() * pixelRatio, height() * pixelRatio ); + m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio ); + } + + QPainter painter( m_pBackgroundPixmap ); + + drawBackground( painter ); + + drawPattern( painter ); +} -void DrumPatternEditor::paintEvent( QPaintEvent* /*ev*/ ) +void DrumPatternEditor::paintEvent( QPaintEvent* ev ) { + if (!isVisible()) { + return; + } + + auto pPref = Preferences::get_instance(); + + qreal pixelRatio = devicePixelRatio(); + if ( pixelRatio != m_pBackgroundPixmap->devicePixelRatio() ) { + createBackground(); + } + QPainter painter( this ); - __draw_pattern( painter ); + painter.drawPixmap( ev->rect(), *m_pBackgroundPixmap, QRectF( pixelRatio * ev->rect().x(), + pixelRatio * ev->rect().y(), + pixelRatio * ev->rect().width(), + pixelRatio * ev->rect().height() ) ); + + // Draw playhead + if ( m_nTick != -1 ) { + int nOffset = Skin::getPlayheadShaftOffset(); + int nX = static_cast(static_cast(PatternEditor::nMargin) + + static_cast(m_nTick) * + m_fGridWidth ); + Skin::setPlayheadPen( &painter, false ); + painter.drawLine( nX, 0, nX, height() ); + } + drawFocus( painter ); m_selection.paintSelection( &painter ); + + // Draw cursor + if ( hasFocus() && !HydrogenApp::get_instance()->hideKeyboardCursor() ) { + uint x = PatternEditor::nMargin + m_pPatternEditorPanel->getCursorPosition() * m_fGridWidth; + int nSelectedInstrument = Hydrogen::get_instance()->getSelectedInstrumentNumber(); + uint y = nSelectedInstrument * m_nGridHeight; + QPen p( pPref->getColorTheme()->m_cursorColor ); + p.setWidth( 2 ); + painter.setPen( p ); + painter.setBrush( Qt::NoBrush ); + painter.setRenderHint( QPainter::Antialiasing ); + painter.drawRoundedRect( QRect( x-m_fGridWidth*3, y+2, m_fGridWidth*6, m_nGridHeight-3 ), 4, 4 ); + } + } void DrumPatternEditor::drawFocus( QPainter& painter ) { @@ -1266,53 +1343,27 @@ void DrumPatternEditor::hideEvent ( QHideEvent *ev ) UNUSED( ev ); } - - -void DrumPatternEditor::focusInEvent ( QFocusEvent *ev ) -{ - UNUSED( ev ); - if ( ev->reason() == Qt::TabFocusReason || ev->reason() == Qt::BacktabFocusReason ) { - m_pPatternEditorPanel->ensureCursorVisible(); - HydrogenApp::get_instance()->setHideKeyboardCursor( false ); - } - updateEditor(); -} - void DrumPatternEditor::selectedInstrumentChangedEvent() -{ - update( 0, 0, width(), height() ); -} - -/// This method is called from another thread (audio engine) -void DrumPatternEditor::patternModifiedEvent() -{ - update( 0, 0, width(), height() ); -} - - -void DrumPatternEditor::patternChangedEvent() { updateEditor(); } - void DrumPatternEditor::selectedPatternChangedEvent() { updateEditor(); } - ///NotePropertiesRuler undo redo action void DrumPatternEditor::undoRedoAction( int column, - QString mode, - int nSelectedPatternNumber, - int nSelectedInstrument, - float velocity, - float fPan, - float leadLag, - float probability, - int noteKeyVal, - int octaveKeyVal) + PatternEditor::Mode mode, + int nSelectedPatternNumber, + int nSelectedInstrument, + float velocity, + float fPan, + float leadLag, + float probability, + int noteKeyVal, + int octaveKeyVal) { Hydrogen *pHydrogen = Hydrogen::get_instance(); std::shared_ptr pSong = pHydrogen->getSong(); @@ -1323,7 +1374,7 @@ void DrumPatternEditor::undoRedoAction( int column, pPattern = pPatternList->get( nSelectedPatternNumber ); } - if(pPattern) { + if( pPattern != nullptr ) { const Pattern::notes_t* notes = pPattern->get_notes(); FOREACH_NOTE_CST_IT_BOUND(notes,it,column) { Note *pNote = it->second; @@ -1333,23 +1384,25 @@ void DrumPatternEditor::undoRedoAction( int column, continue; } - if ( mode == "VELOCITY" && !pNote->get_note_off() ) { + if ( mode == PatternEditor::Mode::Velocity && + !pNote->get_note_off() ) { pNote->set_velocity( velocity ); } - else if ( mode == "PAN" ){ + else if ( mode == PatternEditor::Mode::Pan ){ pNote->setPan( fPan ); } - else if ( mode == "LEADLAG" ){ + else if ( mode == PatternEditor::Mode::LeadLag ){ pNote->set_lead_lag( leadLag ); } - else if ( mode == "NOTEKEY" ){ + else if ( mode == PatternEditor::Mode::NoteKey ){ pNote->set_key_octave( (Note::Key)noteKeyVal, (Note::Octave)octaveKeyVal ); } - else if ( mode == "PROBABILITY" ){ + else if ( mode == PatternEditor::Mode::Probability ){ pNote->set_probability( probability ); } pHydrogen->setIsModified( true ); + NotePropertiesRuler::triggerStatusMessage( pNote, mode ); break; } diff --git a/src/gui/src/PatternEditor/DrumPatternEditor.h b/src/gui/src/PatternEditor/DrumPatternEditor.h index 4b41e2c2c6..6f68e896f0 100644 --- a/src/gui/src/PatternEditor/DrumPatternEditor.h +++ b/src/gui/src/PatternEditor/DrumPatternEditor.h @@ -27,6 +27,7 @@ #include "../EventListener.h" #include "../Selection.h" #include "PatternEditor.h" +#include "NotePropertiesRuler.h" #include "../Widgets/WidgetWithScalableFont.h" #include @@ -52,8 +53,6 @@ class DrumPatternEditor : public PatternEditor, protected WidgetWithScalableFont ~DrumPatternEditor(); // Implements EventListener interface - virtual void patternModifiedEvent() override; - virtual void patternChangedEvent() override; virtual void selectedPatternChangedEvent() override; virtual void selectedInstrumentChangedEvent() override; //~ Implements EventListener interface @@ -80,9 +79,8 @@ class DrumPatternEditor : public PatternEditor, protected WidgetWithScalableFont H2Core::Note *note); void addOrRemoveNote( int nColumn, int nRealColumn, int row, bool bDoAdd = true, bool bDoDelete = true ); - void editNoteLengthAction( int nColumn, int nRealColumn, int row, int length, int selectedPatternNumber ); void undoRedoAction( int column, - QString mode, + NotePropertiesRuler::Mode mode, int nSelectedPatternNumber, int nSelectedInstrument, float velocity, @@ -117,7 +115,6 @@ class DrumPatternEditor : public PatternEditor, protected WidgetWithScalableFont virtual void mouseClickEvent( QMouseEvent *ev ) override; virtual void mouseDragStartEvent( QMouseEvent *ev ) override; virtual void mouseDragUpdateEvent( QMouseEvent *ev ) override; - virtual void mouseDragEndEvent( QMouseEvent *ev ) override; virtual void selectionMoveEndEvent( QInputEvent *ev ) override; // Selected notes are indexed by their address to ensure that a @@ -136,10 +133,10 @@ class DrumPatternEditor : public PatternEditor, protected WidgetWithScalableFont void onPreferencesChanged( H2Core::Preferences::Changes changes ); private: - void __draw_note( H2Core::Note* note, QPainter& painter, bool bIsForeground = true ); - void __draw_pattern( QPainter& painter ); - void __draw_grid( QPainter& painter ); - void __create_background( QPainter& pointer ); + void createBackground() override; + void drawNote( H2Core::Note* note, QPainter& painter, bool bIsForeground = true ); + void drawPattern( QPainter& painter ); + void drawBackground( QPainter& pointer ); void drawFocus( QPainter& painter ); virtual void keyPressEvent (QKeyEvent *ev) override; @@ -147,16 +144,11 @@ class DrumPatternEditor : public PatternEditor, protected WidgetWithScalableFont virtual void showEvent ( QShowEvent *ev ) override; virtual void hideEvent ( QHideEvent *ev ) override; virtual void paintEvent(QPaintEvent *ev) override; - virtual void focusInEvent( QFocusEvent *ev ) override; + virtual void mousePressEvent( QMouseEvent *ev ) override; int findFreeCompoID( int startingPoint = 0 ); int findExistingCompo( QString SourceName ); QString renameCompo( QString OriginalName ); - - int m_nRealColumn; - int m_nColumn; - int m_nRow; - int m_nOldLength; }; diff --git a/src/gui/src/PatternEditor/NotePropertiesRuler.cpp b/src/gui/src/PatternEditor/NotePropertiesRuler.cpp index 033b6ca358..9f8485f886 100644 --- a/src/gui/src/PatternEditor/NotePropertiesRuler.cpp +++ b/src/gui/src/PatternEditor/NotePropertiesRuler.cpp @@ -25,7 +25,6 @@ #include #include #include -#include using namespace H2Core; #include @@ -35,43 +34,35 @@ using namespace H2Core; #include "UndoActions.h" #include "NotePropertiesRuler.h" #include "PatternEditorPanel.h" +#include "PatternEditorRuler.h" #include "DrumPatternEditor.h" #include "PianoRollEditor.h" +#include "../Skin.h" -NotePropertiesRuler::NotePropertiesRuler( QWidget *parent, PatternEditorPanel *pPatternEditorPanel, NotePropertiesMode mode ) +NotePropertiesRuler::NotePropertiesRuler( QWidget *parent, PatternEditorPanel *pPatternEditorPanel, PatternEditor::Mode mode ) : PatternEditor( parent, pPatternEditorPanel ) , m_bEntered( false ) { - m_Mode = mode; + + m_editor = PatternEditor::Editor::NotePropertiesRuler; + m_mode = mode; m_fGridWidth = (Preferences::get_instance())->getPatternEditorGridWidth(); - m_nEditorWidth = m_nMargin + m_fGridWidth * ( MAX_NOTES * 4 ); + m_nEditorWidth = PatternEditor::nMargin + m_fGridWidth * ( MAX_NOTES * 4 ); m_fLastSetValue = 0.0; m_bValueHasBeenSet = false; - m_bNeedsUpdate = true; - if (m_Mode == VELOCITY ) { - m_nEditorHeight = 100; - } - else if ( m_Mode == PAN ) { - m_nEditorHeight = 100; - } - else if ( m_Mode == LEADLAG ) { - m_nEditorHeight = 100; - } - else if ( m_Mode == NOTEKEY ) { + if ( m_mode == PatternEditor::Mode::NoteKey ) { m_nEditorHeight = 210; } - if (m_Mode == PROBABILITY ) { + else { m_nEditorHeight = 100; } resize( m_nEditorWidth, m_nEditorHeight ); setMinimumSize( m_nEditorWidth, m_nEditorHeight ); - m_pBackground = new QPixmap( m_nEditorWidth, m_nEditorHeight ); - updateEditor(); show(); @@ -95,7 +86,6 @@ NotePropertiesRuler::NotePropertiesRuler( QWidget *parent, PatternEditorPanel *p NotePropertiesRuler::~NotePropertiesRuler() { - //infoLog("DESTROY"); } @@ -132,7 +122,10 @@ void NotePropertiesRuler::wheelEvent(QWheelEvent *ev ) #endif m_pPatternEditorPanel->setCursorPosition( nColumn ); - HydrogenApp::get_instance()->setHideKeyboardCursor( true ); + + auto pHydrogenApp = HydrogenApp::get_instance(); + bool bOldCursorHidden = pHydrogenApp->hideKeyboardCursor(); + pHydrogenApp->setHideKeyboardCursor( true ); std::shared_ptr pSong = pHydrogen->getSong(); auto pSelectedInstrument = pSong->getInstrumentList()->get( pHydrogen->getSelectedInstrumentNumber() ); @@ -148,18 +141,30 @@ void NotePropertiesRuler::wheelEvent(QWheelEvent *ev ) notes.push_back( it->second ); } } - + + bool bValueChanged = false; for ( Note *pNote : notes ) { assert( pNote ); if ( pNote->get_instrument() != pSelectedInstrument && !m_selection.isSelected( pNote ) ) { continue; } + bValueChanged = true; adjustNotePropertyDelta( pNote, fDelta, /* bMessage=*/ true ); } + + if ( bOldCursorHidden != pHydrogenApp->hideKeyboardCursor() ) { + // Immediate update to prevent visual delay. + m_pPatternEditorPanel->getPatternEditorRuler()->update(); + if ( ! bValueChanged ) { + update(); + } + } - pHydrogen->setIsModified( true ); - addUndoAction(); - updateEditor(); + if ( bValueChanged ) { + addUndoAction(); + createBackground(); + update(); + } } @@ -175,6 +180,43 @@ void NotePropertiesRuler::mouseClickEvent( QMouseEvent *ev ) { } } +void NotePropertiesRuler::mousePressEvent( QMouseEvent* ev ) { + if ( ev->x() > m_nActiveWidth ) { + return; + } + + PatternEditor::mousePressEvent( ev ); + + auto pHydrogenApp = HydrogenApp::get_instance(); + + // Hide cursor in case this behavior was selected in the + // Preferences. + bool bOldCursorHidden = pHydrogenApp->hideKeyboardCursor(); + pHydrogenApp->setHideKeyboardCursor( true ); + + // Cursor just got hidden. + if ( bOldCursorHidden != pHydrogenApp->hideKeyboardCursor() ) { + // Immediate update to prevent visual delay. + m_pPatternEditorPanel->getPatternEditorRuler()->update(); + update(); + } + + // Update cursor position + if ( ! pHydrogenApp->hideKeyboardCursor() ) { + int nColumn = getColumn( ev->x(), /* bUseFineGrained=*/ true ); + if ( ( m_pPattern != nullptr && + nColumn >= (int)m_pPattern->get_length() ) || + nColumn >= MAX_INSTRUMENTS ) { + return; + } + + m_pPatternEditorPanel->setCursorPosition( nColumn ); + + update(); + m_pPatternEditorPanel->getPatternEditorRuler()->update(); + } +} + void NotePropertiesRuler::mouseDragStartEvent( QMouseEvent *ev ) { if ( m_selection.isMoving() ) { prepareUndoAction( ev->x() ); @@ -205,12 +247,24 @@ void NotePropertiesRuler::selectionMoveUpdateEvent( QMouseEvent *ev ) { float fDelta; QPoint movingOffset = m_selection.movingOffset(); - if ( m_Mode == NOTEKEY ) { + if ( m_mode == PatternEditor::Mode::NoteKey ) { fDelta = (float)-movingOffset.y() / 10; } else { fDelta = (float)-movingOffset.y() / height(); } + // Only send a status message for the update in case a single note + // was selected. + bool bSendStatusMsg = false; + int nNotes = 0; + for ( Note *pNote : m_selection ) { + ++nNotes; + } + if ( nNotes == 1 ) { + bSendStatusMsg = true; + } + + bool bValueChanged = false; for ( Note *pNote : m_selection ) { if ( pNote->get_instrument() == pSelectedInstrument || m_selection.isSelected( pNote ) ) { @@ -219,16 +273,22 @@ void NotePropertiesRuler::selectionMoveUpdateEvent( QMouseEvent *ev ) { m_oldNotes[ pNote ] = new Note( pNote ); } - adjustNotePropertyDelta( pNote, fDelta ); + adjustNotePropertyDelta( pNote, fDelta, bSendStatusMsg ); + bValueChanged = true; } } - updateEditor(); + + if ( bValueChanged ) { + createBackground(); + update(); + } } void NotePropertiesRuler::selectionMoveEndEvent( QInputEvent *ev ) { //! The "move" has already been reflected in the notes. Now just complete Undo event. addUndoAction(); - updateEditor(); + createBackground(); + update(); } void NotePropertiesRuler::clearOldNotes() { @@ -242,32 +302,43 @@ void NotePropertiesRuler::clearOldNotes() { void NotePropertiesRuler::selectionMoveCancelEvent() { for ( auto it : m_oldNotes ) { Note *pNote = it.first, *pOldNote = it.second; - switch ( m_Mode ) { - case VELOCITY: + switch ( m_mode ) { + case PatternEditor::Mode::Velocity: pNote->set_velocity( pOldNote->get_velocity() ); break; - case PAN: + case PatternEditor::Mode::Pan: pNote->setPan( pOldNote->getPan() ); break; - case LEADLAG: + case PatternEditor::Mode::LeadLag: pNote->set_lead_lag( pOldNote->get_lead_lag() ); break; - case NOTEKEY: + case PatternEditor::Mode::NoteKey: pNote->set_key_octave( pOldNote->get_key(), pOldNote->get_octave() ); break; - case PROBABILITY: + case PatternEditor::Mode::Probability: pNote->set_probability( pOldNote->get_probability() ); break; default: break; } } + + if ( m_oldNotes.size() == 0 ) { + for ( const auto& it : m_oldNotes ){ + PatternEditor::triggerStatusMessage( it.second, m_mode ); + } + } + clearOldNotes(); } void NotePropertiesRuler::mouseMoveEvent( QMouseEvent *ev ) { + if ( m_pPattern == nullptr ) { + return; + } + if ( ev->buttons() == Qt::NoButton ) { int nColumn = getColumn( ev->x() ); bool bFound = false; @@ -291,7 +362,8 @@ void NotePropertiesRuler::propertyDragStart( QMouseEvent *ev ) { setCursor( Qt::CrossCursor ); prepareUndoAction( ev->x() ); - updateEditor(); + createBackground(); + update(); } @@ -341,7 +413,14 @@ void NotePropertiesRuler::propertyDragUpdate( QMouseEvent *ev ) int nColumn = getColumn( ev->x() ); m_pPatternEditorPanel->setCursorPosition( nColumn ); - HydrogenApp::get_instance()->setHideKeyboardCursor( true ); + + auto pHydrogenApp = HydrogenApp::get_instance(); + auto pHydrogen = Hydrogen::get_instance(); + auto pAudioEngine = pHydrogen->getAudioEngine(); + auto pSong = pHydrogen->getSong(); + + bool bOldCursorHidden = pHydrogenApp->hideKeyboardCursor(); + pHydrogenApp->setHideKeyboardCursor( true ); if ( m_nDragPreviousColumn != nColumn ) { // Complete current undo action, and start a new one. @@ -358,88 +437,90 @@ void NotePropertiesRuler::propertyDragUpdate( QMouseEvent *ev ) } int keyval = val; val = val / height(); // val is normalized, in [0;1] - Hydrogen *pHydrogen = Hydrogen::get_instance(); int nSelectedInstrument = pHydrogen->getSelectedInstrumentNumber(); - std::shared_ptr pSong = pHydrogen->getSong(); auto pSelectedInstrument = pSong->getInstrumentList()->get( nSelectedInstrument ); - FOREACH_NOTE_CST_IT_BOUND( m_pPattern->get_notes(), it, nColumn ) { + bool bValueSet = false; + + FOREACH_NOTE_CST_IT_BOUND( m_pPattern->get_notes(), it, nColumn ) { Note *pNote = it->second; - if ( pNote->get_instrument() != pSelectedInstrument && !m_selection.isSelected( pNote ) ) { + if ( pNote->get_instrument() != pSelectedInstrument && + !m_selection.isSelected( pNote ) ) { continue; } - if ( m_Mode == VELOCITY && !pNote->get_note_off() ) { + if ( m_mode == PatternEditor::Mode::Velocity && !pNote->get_note_off() ) { pNote->set_velocity( val ); m_fLastSetValue = val; - m_bValueHasBeenSet = true; - char valueChar[100]; - sprintf( valueChar, "%#.2f", val); - HydrogenApp::get_instance()->setStatusBarMessage( QString("Set note velocity [%1]").arg( valueChar ), 2000 ); + bValueSet = true; } - else if ( m_Mode == PAN && !pNote->get_note_off() ){ + else if ( m_mode == PatternEditor::Mode::Pan && !pNote->get_note_off() ){ if ( (ev->button() == Qt::MiddleButton) || (ev->modifiers() == Qt::ControlModifier && ev->button() == Qt::LeftButton) ) { val = 0.5; // central pan } pNote->setPanWithRangeFrom0To1( val ); // checks the boundaries m_fLastSetValue = pNote->getPanWithRangeFrom0To1(); - m_bValueHasBeenSet = true; + bValueSet = true; + } - else if ( m_Mode == LEADLAG ){ - if ( (ev->button() == Qt::MiddleButton) || (ev->modifiers() == Qt::ControlModifier && ev->button() == Qt::LeftButton) ) { + else if ( m_mode == PatternEditor::Mode::LeadLag ){ + if ( (ev->button() == Qt::MiddleButton) || + (ev->modifiers() == Qt::ControlModifier && + ev->button() == Qt::LeftButton) ) { pNote->set_lead_lag(0.0); - } else { - + m_fLastSetValue = 0.0; + bValueSet = true; + } + else { m_fLastSetValue = val * -2.0 + 1.0; - m_bValueHasBeenSet = true; - pNote->set_lead_lag((val * -2.0) + 1.0); - char valueChar[100]; - if (pNote->get_lead_lag() < 0.0) { - sprintf( valueChar, "%.2f", ( pNote->get_lead_lag() * -5)); // FIXME: '5' taken from fLeadLagFactor calculation in hydrogen.cpp - HydrogenApp::get_instance()->setStatusBarMessage( QString("Leading beat by: %1 ticks").arg( valueChar ), 2000 ); - } else if (pNote->get_lead_lag() > 0.0) { - sprintf( valueChar, "%.2f", ( pNote->get_lead_lag() * 5)); // FIXME: '5' taken from fLeadLagFactor calculation in hydrogen.cpp - HydrogenApp::get_instance()->setStatusBarMessage( QString("Lagging beat by: %1 ticks").arg( valueChar ), 2000 ); - } else { - HydrogenApp::get_instance()->setStatusBarMessage( QString("Note on beat"), 2000 ); - } - + bValueSet = true; + pNote->set_lead_lag( m_fLastSetValue ); } } - - else if ( m_Mode == NOTEKEY ){ - if ( (ev->button() == Qt::MiddleButton) || (ev->modifiers() == Qt::ControlModifier && ev->button() == Qt::LeftButton) ) { - ; - } else { + else if ( m_mode == PatternEditor::Mode::NoteKey ){ + if ( ev->button() != Qt::MiddleButton && + ! ( ev->modifiers() == Qt::ControlModifier && + ev->button() == Qt::LeftButton ) ) { //set the note height int k = 666; int o = 666; - if(keyval >=6 && keyval<=125) { - k = (keyval-6)/10; - } else if(keyval>=135 && keyval<=205) { - o = (keyval-166)/10; - if(o==-4) o=-3; // 135 + if( keyval >= 6 && keyval <= 125 ) { + k = ( keyval - 6 ) / 10; + } + else if( keyval >= 135 && keyval <= 205 ) { + o = ( keyval - 166 ) / 10; + if ( o == -4 ) { + o = -3; // 135 + } } m_fLastSetValue = o * 12 + k; - m_bValueHasBeenSet = true; + bValueSet = true; pNote->set_key_octave((Note::Key)k,(Note::Octave)o); // won't set wrong values see Note::set_key_octave } } - else if ( m_Mode == PROBABILITY && !pNote->get_note_off() ) { + else if ( m_mode == PatternEditor::Mode::Probability && !pNote->get_note_off() ) { m_fLastSetValue = val; - m_bValueHasBeenSet = true; + bValueSet = true; pNote->set_probability( val ); - char valueChar[100]; - sprintf( valueChar, "%#.2f", val); - HydrogenApp::get_instance()->setStatusBarMessage( QString("Set note probability [%1]").arg( valueChar ), 2000 ); + } + + if ( bValueSet ) { + PatternEditor::triggerStatusMessage( pNote, m_mode ); + m_bValueHasBeenSet = true; + Hydrogen::get_instance()->setIsModified( true ); } } - m_nDragPreviousColumn = nColumn; + // Cursor just got hidden. + if ( bOldCursorHidden != pHydrogenApp->hideKeyboardCursor() ) { + // Immediate update to prevent visual delay. + m_pPatternEditorPanel->getPatternEditorRuler()->update(); + } - Hydrogen::get_instance()->setIsModified( true ); - updateEditor(); + m_nDragPreviousColumn = nColumn; + createBackground(); + update(); m_pPatternEditorPanel->getPianoRollEditor()->updateEditor(); m_pPatternEditorPanel->getDrumPatternEditor()->updateEditor(); @@ -449,7 +530,8 @@ void NotePropertiesRuler::propertyDragEnd() { addUndoAction(); unsetCursor(); - updateEditor(); + createBackground(); + update(); } //! Adjust a note's property by applying a delta to the current value, and clipping to the appropriate @@ -458,58 +540,43 @@ void NotePropertiesRuler::adjustNotePropertyDelta( Note *pNote, float fDelta, bo { Note *pOldNote = m_oldNotes[ pNote ]; assert( pOldNote ); - switch (m_Mode) { - case VELOCITY: + + bool bValueSet = false; + + switch (m_mode) { + case PatternEditor::Mode::Velocity: if ( !pNote->get_note_off() ) { float fVelocity = qBound( VELOCITY_MIN, (pOldNote->get_velocity() + fDelta), VELOCITY_MAX ); pNote->set_velocity( fVelocity ); m_fLastSetValue = fVelocity; - m_bValueHasBeenSet = true; - if ( bMessage ) { - char valueChar[100]; - sprintf( valueChar, "%#.2f", fVelocity ); - ( HydrogenApp::get_instance() )->setStatusBarMessage( QString( tr( "Set note velocity [%1]" ) ) - .arg( valueChar ), 2000 ); - } + bValueSet = true; } break; - case PAN: + case PatternEditor::Mode::Pan: if ( !pNote->get_note_off() ) { float fVal = pOldNote->getPanWithRangeFrom0To1() + fDelta; // value in [0,1] or slight out of boundaries pNote->setPanWithRangeFrom0To1( fVal ); // checks the boundaries as well m_fLastSetValue = pNote->getPanWithRangeFrom0To1(); - m_bValueHasBeenSet = true; + bValueSet = true; } break; - case LEADLAG: + case PatternEditor::Mode::LeadLag: { float fLeadLag = qBound( LEAD_LAG_MIN, pOldNote->get_lead_lag() - fDelta, LEAD_LAG_MAX ); pNote->set_lead_lag( fLeadLag ); m_fLastSetValue = fLeadLag; - m_bValueHasBeenSet = true; - if ( bMessage ) { - char valueChar[100]; - if (pNote->get_lead_lag() < 0.0) { - sprintf( valueChar, "%.2f", ( pNote->get_lead_lag() * -5)); // FIXME: '5' taken from fLeadLagFactor calculation in hydrogen.cpp - HydrogenApp::get_instance()->setStatusBarMessage( QString("Leading beat by: %1 ticks").arg( valueChar ), 2000 ); - } else if (pNote->get_lead_lag() > 0.0) { - sprintf( valueChar, "%.2f", ( pNote->get_lead_lag() * 5)); // FIXME: '5' taken from fLeadLagFactor calculation in hydrogen.cpp - HydrogenApp::get_instance()->setStatusBarMessage( QString("Lagging beat by: %1 ticks").arg( valueChar ), 2000 ); - } else { - HydrogenApp::get_instance()->setStatusBarMessage( QString("Note on beat"), 2000 ); - } - } + bValueSet = true; } break; - case PROBABILITY: + case PatternEditor::Mode::Probability: if ( !pNote->get_note_off() ) { float fProbability = qBound( 0.0f, pOldNote->get_probability() + fDelta, 1.0f ); pNote->set_probability( fProbability ); m_fLastSetValue = fProbability; - m_bValueHasBeenSet = true; + bValueSet = true; } break; - case NOTEKEY: + case PatternEditor::Mode::NoteKey: int nPitch = qBound( 12 * OCTAVE_MIN, (int)( pOldNote->get_notekey_pitch() + fDelta ), 12 * OCTAVE_MAX + KEY_MAX ); Note::Octave octave; @@ -523,18 +590,34 @@ void NotePropertiesRuler::adjustNotePropertyDelta( Note *pNote, float fDelta, bo pNote->set_key_octave( key, octave ); m_fLastSetValue = 12 * octave + key; - m_bValueHasBeenSet = true; + bValueSet = true; break; } - Hydrogen::get_instance()->setIsModified( true ); + + if ( bValueSet ) { + Hydrogen::get_instance()->setIsModified( true ); + m_bValueHasBeenSet = true; + if ( bMessage ) { + PatternEditor::triggerStatusMessage( pNote, m_mode ); + } + } } void NotePropertiesRuler::keyPressEvent( QKeyEvent *ev ) { + if ( m_pPattern == nullptr ) { + return; + } + + auto pHydrogenApp = HydrogenApp::get_instance(); + bool bOldCursorHidden = pHydrogenApp->hideKeyboardCursor(); + const int nWordSize = 5; bool bIsSelectionKey = m_selection.keyPressEvent( ev ); bool bUnhideCursor = true; + bool bValueChanged = false; + if ( bIsSelectionKey ) { // Key was claimed by selection } else if ( ev->matches( QKeySequence::MoveToNextChar ) || ev->matches( QKeySequence::SelectNextChar ) ) { @@ -562,6 +645,7 @@ void NotePropertiesRuler::keyPressEvent( QKeyEvent *ev ) m_pPatternEditorPanel->setCursorPosition(0); } else { + // Value adjustments float fDelta = 0.0; bool bRepeatLastValue = false; @@ -626,13 +710,16 @@ void NotePropertiesRuler::keyPressEvent( QKeyEvent *ev ) Note *pNote = it->second; assert( pNote ); assert( pNote->get_position() == column ); - nNotes++; - notes.push_back( pNote ); + if ( pNote->get_instrument() == + pSong->getInstrumentList()->get( nSelectedInstrument ) ) { + nNotes++; + notes.push_back( pNote ); + } } } // For the NoteKeyEditor, adjust the pitch by a whole semitone - if ( m_Mode == NOTEKEY ) { + if ( m_mode == PatternEditor::Mode::NoteKey ) { if ( fDelta > 0.0 ) { fDelta = 1; } else if ( fDelta < 0.0 ) { @@ -640,118 +727,119 @@ void NotePropertiesRuler::keyPressEvent( QKeyEvent *ev ) } } - prepareUndoAction( m_nMargin + column * m_fGridWidth ); + prepareUndoAction( PatternEditor::nMargin + column * m_fGridWidth ); for ( Note *pNote : notes ) { - - if ( pNote->get_instrument() != pSong->getInstrumentList()->get( nSelectedInstrument ) - && !m_selection.isSelected( pNote ) ) { - continue; - } + bValueChanged = true; if ( !bRepeatLastValue ) { + // Apply delta to the property adjustNotePropertyDelta( pNote, fDelta, nNotes == 1 ); } else { + + bool bValueSet = false; + // Repeating last value - switch (m_Mode) { - case VELOCITY: + switch (m_mode) { + case PatternEditor::Mode::Velocity: if ( !pNote->get_note_off() ) { pNote->set_velocity( m_fLastSetValue ); + bValueSet = true; } break; - case PAN: + case PatternEditor::Mode::Pan: if ( !pNote->get_note_off() ) { if ( m_fLastSetValue > 1. ) { // TODO whats this for? is it ever reached? printf( "reached m_fLastSetValue > 1 in NotePropertiesRuler.cpp\n" ); pNote->setPanWithRangeFrom0To1( m_fLastSetValue ); } - break; + bValueSet = true; } - case LEADLAG: + break; + case PatternEditor::Mode::LeadLag: pNote->set_lead_lag( m_fLastSetValue ); + bValueSet = true; break; - case PROBABILITY: + case PatternEditor::Mode::Probability: if ( !pNote->get_note_off() ) { pNote->set_probability( m_fLastSetValue ); + bValueSet = true; } break; - case NOTEKEY: + case PatternEditor::Mode::NoteKey: pNote->set_key_octave( (Note::Key)( (int)m_fLastSetValue % 12 ), (Note::Octave)( (int)m_fLastSetValue / 12 ) ); + bValueSet = true; break; } + + if ( bValueSet ) { + if ( nNotes == 1 ) { + PatternEditor::triggerStatusMessage( pNote, m_mode ); + } + Hydrogen::get_instance()->setIsModified( true ); + } } } addUndoAction(); } else { - HydrogenApp::get_instance()->setHideKeyboardCursor( true ); + pHydrogenApp->setHideKeyboardCursor( true ); ev->ignore(); + + // Cursor either just got hidden. + if ( bOldCursorHidden != pHydrogenApp->hideKeyboardCursor() ) { + // Immediate update to prevent visual delay. + m_pPatternEditorPanel->getPatternEditorRuler()->update(); + update(); + } return; } } if ( bUnhideCursor ) { - HydrogenApp::get_instance()->setHideKeyboardCursor( false ); + pHydrogenApp->setHideKeyboardCursor( false ); } - m_selection.updateKeyboardCursorPosition( getKeyboardCursorRect() ); - updateEditor(); - ev->accept(); - -} - -void NotePropertiesRuler::focusInEvent( QFocusEvent * ev ) -{ - if ( ev->reason() == Qt::TabFocusReason || ev->reason() == Qt::BacktabFocusReason ) { - HydrogenApp::get_instance()->setHideKeyboardCursor( false ); + // Cursor either just got hidden or was moved. + if ( ! HydrogenApp::get_instance()->hideKeyboardCursor() || + bOldCursorHidden != pHydrogenApp->hideKeyboardCursor() ) { + // Immediate update to prevent visual delay. + m_pPatternEditorPanel->getPatternEditorRuler()->update(); } - updateEditor(); -} + m_selection.updateKeyboardCursorPosition( getKeyboardCursorRect() ); + + if ( bValueChanged ) { + createBackground(); + } + update(); + + ev->accept(); -void NotePropertiesRuler::focusOutEvent( QFocusEvent * ev ) -{ - updateEditor(); } - void NotePropertiesRuler::addUndoAction() { + if ( m_nSelectedPatternNumber == -1 ) { + // No pattern selected. + return; + } + InstrumentList *pInstrumentList = Hydrogen::get_instance()->getSong()->getInstrumentList(); int nSize = m_oldNotes.size(); if ( nSize != 0 ) { QUndoStack *pUndoStack = HydrogenApp::get_instance()->m_pUndoStack; - QString sMode; - switch ( m_Mode ) { - case VELOCITY: - sMode = "VELOCITY"; - break; - case PAN: - sMode = "PAN"; - break; - case LEADLAG: - sMode = "LEADLAG"; - break; - case NOTEKEY: - sMode = "NOTEKEY"; - break; - case PROBABILITY: - sMode = "PROBABILITY"; - break; - default: - break; - } if ( nSize != 1 ) { - pUndoStack->beginMacro( QString( tr( "Edit %1 property of %2 notes" ) ) - .arg( sMode.toLower() ) + pUndoStack->beginMacro( QString( tr( "Edit [%1] property of [%2] notes" ) ) + .arg( NotePropertiesRuler::modeToQString( m_mode ) ) .arg( nSize ) ); } for ( auto it : m_oldNotes ) { Note *pNewNote = it.first, *pOldNote = it.second; pUndoStack->push( new SE_editNotePropertiesVolumeAction( pNewNote->get_position(), - sMode, + m_mode, m_nSelectedPatternNumber, pInstrumentList->index( pNewNote->get_instrument() ), pNewNote->get_velocity(), @@ -776,15 +864,50 @@ void NotePropertiesRuler::addUndoAction() void NotePropertiesRuler::paintEvent( QPaintEvent *ev) { - QPainter painter(this); - if ( m_bNeedsUpdate ) { - finishUpdateEditor(); + if (!isVisible()) { + return; } - painter.drawPixmap( ev->rect(), *m_pBackground, ev->rect() ); + auto pPref = Preferences::get_instance(); + + qreal pixelRatio = devicePixelRatio(); + if ( pixelRatio != m_pBackgroundPixmap->devicePixelRatio() ) { + createBackground(); + } + + QPainter painter(this); + painter.drawPixmap( ev->rect(), *m_pBackgroundPixmap, + QRectF( pixelRatio * ev->rect().x(), + pixelRatio * ev->rect().y(), + pixelRatio * ev->rect().width(), + pixelRatio * ev->rect().height() ) ); + + // Draw playhead + if ( m_nTick != -1 ) { + + int nOffset = Skin::getPlayheadShaftOffset(); + int nX = static_cast(static_cast(PatternEditor::nMargin) + + static_cast(m_nTick) * + m_fGridWidth ); + Skin::setPlayheadPen( &painter, false ); + painter.drawLine( nX, 0, nX, height() ); + } + drawFocus( painter ); - // m_selection.paintSelection( &painter ); + m_selection.paintSelection( &painter ); + + // cursor + if ( hasFocus() && ! HydrogenApp::get_instance()->hideKeyboardCursor() ) { + uint x = PatternEditor::nMargin + m_pPatternEditorPanel->getCursorPosition() * m_fGridWidth; + + QPen pen( pPref->getColorTheme()->m_cursorColor ); + pen.setWidth( 2 ); + painter.setPen( pen ); + painter.setBrush( Qt::NoBrush ); + painter.setRenderHint( QPainter::Antialiasing ); + painter.drawRoundedRect( QRect( x-m_fGridWidth*3, 0 + 3, m_fGridWidth*6, height() - 6 ), 4, 4 ); + } } void NotePropertiesRuler::drawFocus( QPainter& painter ) { @@ -806,20 +929,20 @@ void NotePropertiesRuler::drawFocus( QPainter& painter ) { const QScrollArea* pScrollArea; - switch ( m_Mode ) { - case VELOCITY: + switch ( m_mode ) { + case PatternEditor::Mode::Velocity: pScrollArea = HydrogenApp::get_instance()->getPatternEditorPanel()->getNoteVelocityScrollArea(); break; - case PAN: + case PatternEditor::Mode::Pan: pScrollArea = HydrogenApp::get_instance()->getPatternEditorPanel()->getNotePanScrollArea(); break; - case LEADLAG: + case PatternEditor::Mode::LeadLag: pScrollArea = HydrogenApp::get_instance()->getPatternEditorPanel()->getNoteLeadLagScrollArea(); break; - case NOTEKEY: + case PatternEditor::Mode::NoteKey: pScrollArea = HydrogenApp::get_instance()->getPatternEditorPanel()->getNoteNoteKeyScrollArea(); break; - case PROBABILITY: + case PatternEditor::Mode::Probability: pScrollArea = HydrogenApp::get_instance()->getPatternEditorPanel()->getNoteProbabilityScrollArea(); break; default: @@ -876,35 +999,59 @@ void NotePropertiesRuler::leaveEvent( QEvent *ev ) { update(); } - - -void NotePropertiesRuler::createVelocityBackground(QPixmap *pixmap) -{ +void NotePropertiesRuler::drawDefaultBackground( QPainter& painter, int nHeight, int nIncrement ) { + auto pPref = H2Core::Preferences::get_instance(); - QColor res_1( pPref->getColorTheme()->m_patternEditor_line1Color ); - - QColor backgroundColor( pPref->getColorTheme()->m_patternEditor_backgroundColor ); - QColor horizLinesColor( backgroundColor.red() - 20, - backgroundColor.green() - 20, - backgroundColor.blue() - 20 ); + const QColor borderColor( pPref->getColorTheme()->m_patternEditor_lineColor ); + const QColor lineColor( pPref->getColorTheme()->m_patternEditor_line5Color ); + const QColor lineInactiveColor( pPref->getColorTheme()->m_windowTextColor.darker( 170 ) ); + const QColor backgroundColor( pPref->getColorTheme()->m_patternEditor_backgroundColor ); + const QColor backgroundInactiveColor( pPref->getColorTheme()->m_windowColor ); - unsigned nNotes = MAX_NOTES; - if ( m_pPattern ) { - nNotes = m_pPattern->get_length(); + if ( nHeight == 0 ) { + nHeight = height(); + } + if ( nIncrement == 0 ) { + nIncrement = nHeight / 10; } - QPainter p( pixmap ); + painter.fillRect( 0, 0, m_nActiveWidth, height(), backgroundColor ); + painter.fillRect( m_nActiveWidth, 0, m_nEditorWidth - m_nActiveWidth, + height(), backgroundInactiveColor ); + + drawGridLines( painter, Qt::DotLine ); + + painter.setPen( lineColor ); + for (unsigned y = 0; y < nHeight; y += nIncrement ) { + painter.drawLine( PatternEditor::nMargin, y, m_nActiveWidth, y ); + } + + painter.setPen( borderColor ); + painter.drawLine( 0, 0, m_nActiveWidth, 0 ); + painter.drawLine( 0, m_nEditorHeight - 1, m_nActiveWidth, m_nEditorHeight - 1 ); + + if ( m_nActiveWidth + 1 < m_nEditorWidth ) { + painter.setPen( lineInactiveColor ); + for (unsigned y = 0; y < nHeight; y += nIncrement ) { + painter.drawLine( m_nActiveWidth, y, m_nEditorWidth, y ); + } + + painter.drawLine( m_nActiveWidth, 0, m_nEditorWidth, 0 ); + painter.drawLine( m_nActiveWidth, m_nEditorHeight - 1, + m_nEditorWidth, m_nEditorHeight - 1 ); + } +} - p.fillRect( 0, 0, m_nMargin + nNotes * m_fGridWidth, height(), backgroundColor ); +void NotePropertiesRuler::createNormalizedBackground(QPixmap *pixmap) +{ + auto pPref = H2Core::Preferences::get_instance(); - drawGridLines( p, Qt::DotLine ); + QColor borderColor( pPref->getColorTheme()->m_patternEditor_lineColor ); + const QColor lineInactiveColor( pPref->getColorTheme()->m_windowTextColor.darker( 170 ) ); + QPainter p( pixmap ); - // Horizontal lines at 10% intervals - p.setPen( horizLinesColor ); - for (unsigned y = 0; y < m_nEditorHeight; y = y + (m_nEditorHeight / 10)) { - p.drawLine( m_nMargin, y, 20 + nNotes * m_fGridWidth, y ); - } + drawDefaultBackground( p ); // draw velocity lines if (m_pPattern != nullptr) { @@ -927,20 +1074,29 @@ void NotePropertiesRuler::createVelocityBackground(QPixmap *pixmap) && !m_selection.isSelected( pNote ) ) { continue; } - uint x_pos = m_nMargin + pos * m_fGridWidth; + uint x_pos = PatternEditor::nMargin + pos * m_fGridWidth; uint line_end = height(); uint value = 0; - if ( m_Mode == VELOCITY ) { + if ( m_mode == PatternEditor::Mode::Velocity ) { value = (uint)(pNote->get_velocity() * height()); } - else if ( m_Mode == PROBABILITY ) { + else if ( m_mode == PatternEditor::Mode::Probability ) { value = (uint)(pNote->get_probability() * height()); } uint line_start = line_end - value; - QColor centerColor = DrumPatternEditor::computeNoteColor( pNote->get_velocity() ); + QColor noteColor = DrumPatternEditor::computeNoteColor( pNote->get_velocity() ); int nLineWidth = 3; + + p.fillRect( x_pos - 1 + xoffset, line_start, + nLineWidth, line_end - line_start, + noteColor ); + p.setPen( QPen( Qt::black, 1 ) ); + p.setRenderHint( QPainter::Antialiasing ); + p.drawRoundedRect( x_pos - 1 - 1 + xoffset, line_start - 1, + nLineWidth + 2, line_end - line_start + 2, 2, 2 ); + if ( m_selection.isSelected( pNote ) ) { p.setPen( selectedPen ); p.setRenderHint( QPainter::Antialiasing ); @@ -948,47 +1104,48 @@ void NotePropertiesRuler::createVelocityBackground(QPixmap *pixmap) nLineWidth + 4, line_end - line_start + 4 , 4, 4 ); } - - p.fillRect( x_pos - 1 + xoffset, line_start, nLineWidth, line_end - line_start , centerColor ); xoffset++; } } } - p.setPen(res_1); - p.drawLine(0, 0, m_nEditorWidth, 0); - p.drawLine(0, m_nEditorHeight - 1, m_nEditorWidth, m_nEditorHeight - 1); + + p.setPen( borderColor ); + p.setRenderHint( QPainter::Antialiasing ); + p.drawLine( 0, 0, m_nEditorWidth, 0 ); + p.setPen( QPen( borderColor, 2 ) ); + p.drawLine( 0, m_nEditorHeight, m_nEditorWidth, m_nEditorHeight ); + + if ( m_nActiveWidth + 1 < m_nEditorWidth ) { + p.setPen( lineInactiveColor ); + p.drawLine( m_nActiveWidth, 0, m_nEditorWidth, 0 ); + p.setPen( QPen( lineInactiveColor, 2 ) ); + p.drawLine( m_nActiveWidth, m_nEditorHeight, + m_nEditorWidth, m_nEditorHeight ); + } } - - -void NotePropertiesRuler::createPanBackground(QPixmap *pixmap) +void NotePropertiesRuler::createCenteredBackground(QPixmap *pixmap) { auto pPref = H2Core::Preferences::get_instance(); - QColor backgroundColor( pPref->getColorTheme()->m_patternEditor_backgroundColor ); - - QColor horizLinesColor( backgroundColor.red() - 20, - backgroundColor.green() - 20, - backgroundColor.blue() - 20 ); - - QColor res_1( pPref->getColorTheme()->m_patternEditor_line1Color ); + QColor baseLineColor( pPref->getColorTheme()->m_patternEditor_lineColor ); + QColor borderColor( pPref->getColorTheme()->m_patternEditor_lineColor ); + const QColor lineInactiveColor( pPref->getColorTheme()->m_windowTextColor.darker( 170 ) ); QPainter p( pixmap ); - unsigned nNotes = MAX_NOTES; - if (m_pPattern) { - nNotes = m_pPattern->get_length(); - } - p.fillRect( 0, 0, m_nMargin + nNotes * m_fGridWidth, height(), backgroundColor ); + drawDefaultBackground( p ); // central line - p.setPen( horizLinesColor ); - p.drawLine(0, height() / 2.0, m_nEditorWidth, height() / 2.0); - - // vertical lines - drawGridLines( p, Qt::DotLine ); + p.setPen( baseLineColor ); + p.drawLine(0, height() / 2.0, m_nActiveWidth, height() / 2.0); + if ( m_nActiveWidth + 1 < m_nEditorWidth ) { + p.setPen( lineInactiveColor ); + p.drawLine( m_nActiveWidth, height() / 2.0, + m_nEditorWidth, height() / 2.0); + } - if ( m_pPattern ) { + if ( m_pPattern != nullptr ) { int nSelectedInstrument = Hydrogen::get_instance()->getSelectedInstrumentNumber(); std::shared_ptr pSong = Hydrogen::get_instance()->getSong(); QPen selectedPen( selectedNoteColor() ); @@ -1008,181 +1165,110 @@ void NotePropertiesRuler::createPanBackground(QPixmap *pixmap) && !m_selection.isSelected( pNote ) ) ) { continue; } - uint x_pos = m_nMargin + pNote->get_position() * m_fGridWidth; - QColor centerColor = DrumPatternEditor::computeNoteColor( pNote->get_velocity() ); + uint x_pos = PatternEditor::nMargin + pNote->get_position() * m_fGridWidth; + QColor noteColor = DrumPatternEditor::computeNoteColor( pNote->get_velocity() ); p.setPen( Qt::NoPen ); - if ( pNote->getPan() == 0.f ) { - // pan value is centered - draw circle - int y_pos = (int)( height() * 0.5 ); - p.setBrush(QColor( centerColor )); - p.drawEllipse( x_pos-4 + xoffset, y_pos-4, 8, 8); - } else { - int y_start = height() * 0.5; - int y_width = (int)( - 0.5 * height() * pNote->getPan() ); - int nLineWidth = 3; - p.fillRect( x_pos - 1 + xoffset, y_start, nLineWidth, y_width, QColor( centerColor) ); - p.fillRect( x_pos - 1 + xoffset, ( height() / 2.0 ) - 2 , nLineWidth, 5, QColor( centerColor ) ); + float fValue = 0; + if ( m_mode == PatternEditor::Mode::Pan ) { + fValue = pNote->getPan(); + } else if ( m_mode == PatternEditor::Mode::LeadLag ) { + fValue = -1 * pNote->get_lead_lag(); } + // Rounding in order to not miss the center due to + // rounding errors introduced in the Note class + // internals. + fValue *= 100; + fValue = std::round( fValue ); + fValue /= 100; + int nLineWidth = 3; - if ( m_selection.isSelected( pNote ) ) { - p.setPen( selectedPen ); + p.setPen( QPen( Qt::black, 1 ) ); + p.setRenderHint( QPainter::Antialiasing ); + if ( fValue == 0.f ) { + // value is centered - draw circle + int y_pos = (int)( height() * 0.5 ); + p.setBrush(QColor( noteColor )); + p.drawEllipse( x_pos-4 + xoffset, y_pos-4, 8, 8); p.setBrush( Qt::NoBrush ); - p.setRenderHint( QPainter::Antialiasing ); - p.drawRoundedRect( x_pos - 1 -2 + xoffset, 0, - nLineWidth + 4, height() , - 4, 4 ); - } - xoffset++; - } - } - } - p.setPen(res_1); - p.drawLine(0, 0, m_nEditorWidth, 0); - p.drawLine(0, m_nEditorHeight - 1, m_nEditorWidth, m_nEditorHeight - 1); -} - -void NotePropertiesRuler::createLeadLagBackground(QPixmap *pixmap) -{ - auto pPref = H2Core::Preferences::get_instance(); - - QColor backgroundColor( pPref->getColorTheme()->m_patternEditor_backgroundColor ); - - QColor horizLinesColor( backgroundColor.red() - 20, - backgroundColor.green() - 20, - backgroundColor.blue() - 20 ); - - QColor res_1( pPref->getColorTheme()->m_patternEditor_line1Color ); - - QPainter p( pixmap ); - - unsigned nNotes = MAX_NOTES; - if (m_pPattern) { - nNotes = m_pPattern->get_length(); - } - p.fillRect( 0, 0, m_nMargin + nNotes * m_fGridWidth, height(), backgroundColor ); - - // central line - p.setPen( horizLinesColor ); - p.drawLine(0, height() / 2.0, m_nEditorWidth, height() / 2.0); - - // vertical lines - drawGridLines( p, Qt::DotLine ); - - if ( m_pPattern ) { - int nSelectedInstrument = Hydrogen::get_instance()->getSelectedInstrumentNumber(); - std::shared_ptr pSong = Hydrogen::get_instance()->getSong(); - QPen selectedPen( selectedNoteColor() ); - selectedPen.setWidth( 2 ); - - const Pattern::notes_t* notes = m_pPattern->get_notes(); - FOREACH_NOTE_CST_IT_BEGIN_END(notes,it) { - Note *pposNote = it->second; - assert( pposNote ); - uint pos = pposNote->get_position(); - int xoffset = 0; - FOREACH_NOTE_CST_IT_BOUND(notes,coit,pos) { - Note *pNote = coit->second; - assert( pNote ); - if ( pNote->get_instrument() != pSong->getInstrumentList()->get( nSelectedInstrument ) - && !m_selection.isSelected( pNote ) ) { - continue; + if ( m_selection.isSelected( pNote ) ) { + p.setPen( selectedPen ); + p.setRenderHint( QPainter::Antialiasing ); + p.drawEllipse( x_pos - 6 + xoffset, y_pos - 6, + 12, 12); + } } + else { + // value was altered - draw a rectangle + int nHeight = 0.5 * height() * std::abs( fValue ) + 5; + int nStartY = height() * 0.5 - 2; + if ( fValue >= 0 ) { + nStartY = nStartY - nHeight + 5; + } - uint x_pos = m_nMargin + pNote->get_position() * m_fGridWidth; - - int red1 = (int) (pNote->get_velocity() * 255); - int green1; - int blue1; - blue1 = ( 255 - (int) red1 )* .33; - green1 = ( 255 - (int) red1 ); + p.fillRect( x_pos - 1 + xoffset, nStartY, + nLineWidth, nHeight, QColor( noteColor ) ); + p.drawRoundedRect( x_pos - 1 + xoffset - 1, nStartY - 1, + nLineWidth + 2, nHeight + 2, 2, 2 ); - p.setPen( Qt::NoPen ); - if (pNote->get_lead_lag() == 0) { - - // leadlag value is centered - draw circle - int y_pos = (int)( height() * 0.5 ); - p.setBrush(QColor( 0 , 0 , 0 )); - p.drawEllipse( x_pos-4 + xoffset, y_pos-4, 8, 8); - } else { - int y_start = (int)( height() * 0.5 ); - int y_end = y_start + ((pNote->get_lead_lag()/2) * height()); - - int nLineWidth = 3; - int red; - int green; - int blue = (int) (pNote->get_lead_lag() * 255); - if (blue < 0) { - red = blue *-1; - blue = (int) red * .33; - green = (int) red * .33; - } else { - red = (int) blue * .33; - green = (int) blue * .33; + if ( m_selection.isSelected( pNote ) ) { + p.setPen( selectedPen ); + p.drawRoundedRect( x_pos - 1 - 2 + xoffset, nStartY - 2, + nLineWidth + 4, nHeight + 4, + 4, 4 ); } - p.fillRect( x_pos - 1 + xoffset, y_start, nLineWidth, y_end - y_start, QColor( red, green ,blue ) ); - - p.fillRect( x_pos - 1 + xoffset, ( height() / 2.0 ) - 2 , nLineWidth, 5, QColor( red1, green1 ,blue1 ) ); } - - int nLineWidth = 3; - if ( m_selection.isSelected( pNote ) ) { - p.setPen( selectedPen ); - p.setBrush( Qt::NoBrush ); - p.setRenderHint( QPainter::Antialiasing ); - p.drawRoundedRect( x_pos - 1 -2 + xoffset, 0, - nLineWidth + 4, height() , - 4, 4 ); - } - xoffset++; - } + } } } - p.setPen(res_1); - p.drawLine(0, 0, m_nEditorWidth, 0); - p.drawLine(0, m_nEditorHeight - 1, m_nEditorWidth, m_nEditorHeight - 1); + + p.setPen( borderColor ); + p.setRenderHint( QPainter::Antialiasing ); + p.drawLine( 0, 0, m_nEditorWidth, 0 ); + p.setPen( QPen( borderColor, 2 ) ); + p.drawLine( 0, m_nEditorHeight, m_nEditorWidth, m_nEditorHeight ); + + if ( m_nActiveWidth + 1 < m_nEditorWidth ) { + p.setPen( lineInactiveColor ); + p.drawLine( m_nActiveWidth, 0, m_nEditorWidth, 0 ); + p.setPen( QPen( lineInactiveColor, 2 ) ); + p.drawLine( m_nActiveWidth, m_nEditorHeight, + m_nEditorWidth, m_nEditorHeight ); + } } - - void NotePropertiesRuler::createNoteKeyBackground(QPixmap *pixmap) { auto pPref = H2Core::Preferences::get_instance(); - - QColor res_1( pPref->getColorTheme()->m_patternEditor_line1Color ); - - QColor backgroundColor( pPref->getColorTheme()->m_patternEditor_backgroundColor ); + QColor backgroundColor = pPref->getColorTheme()->m_patternEditor_backgroundColor; + const QColor backgroundInactiveColor( pPref->getColorTheme()->m_windowColor ); + QColor alternateRowColor = pPref->getColorTheme()->m_patternEditor_alternateRowColor; + QColor octaveColor = pPref->getColorTheme()->m_patternEditor_octaveRowColor; + QColor lineColor( pPref->getColorTheme()->m_patternEditor_lineColor ); + const QColor lineInactiveColor( pPref->getColorTheme()->m_windowTextColor.darker( 170 ) ); + QColor textColor( pPref->getColorTheme()->m_patternEditor_textColor ); - QColor horizLinesColor( backgroundColor.red() - 100, - backgroundColor.green() - 100, - backgroundColor.blue() - 100 ); - - unsigned nNotes = MAX_NOTES; - if (m_pPattern) { - nNotes = m_pPattern->get_length(); - } QPainter p( pixmap ); + p.fillRect( 0, 0, m_nEditorWidth, m_nEditorHeight, backgroundInactiveColor ); + drawDefaultBackground( p, 80, 10 ); - p.fillRect( 0, 0, m_nMargin + nNotes * m_fGridWidth, height(), backgroundColor ); - - p.setPen( horizLinesColor ); - for (unsigned y = 10; y < 80; y = y + 10 ) { - p.setPen( QPen( res_1, 1, Qt::DashLine ) ); - if (y == 40) p.setPen( QPen( QColor(0,0,0), 1, Qt::SolidLine ) ); - p.drawLine( m_nMargin, y, m_nMargin + nNotes * m_fGridWidth, y ); - } - - for (unsigned y = 90; y < 210; y = y + 10 ) { - p.setPen( QPen( QColor( 255, 255, 255 ), 9, Qt::SolidLine, Qt::FlatCap) ); - if ( y == 100 ||y == 120 ||y == 140 ||y == 170 ||y == 190) { - p.setPen( QPen( QColor( 128, 128, 128 ), 9, Qt::SolidLine, Qt::FlatCap ) ); + // fill the background of the key region; + for ( unsigned y = 90; y < 210; y = y + 10 ) { + + if ( y == 100 || y == 120 || y == 140 || y == 170 || y == 190) { + p.setPen( QPen( alternateRowColor, + 9, Qt::SolidLine, Qt::FlatCap ) ); + } + else { + p.setPen( QPen( octaveColor, 9, Qt::SolidLine, Qt::FlatCap) ); } - p.drawLine( m_nMargin, y, m_nMargin + nNotes * m_fGridWidth, y ); + + p.drawLine( PatternEditor::nMargin, y, m_nActiveWidth, y ); } // Annotate with note class names @@ -1192,27 +1278,26 @@ void NotePropertiesRuler::createNoteKeyBackground(QPixmap *pixmap) QFont font( pPref->getApplicationFontFamily(), getPointSize( pPref->getFontSize() ) ); p.setFont( font ); - p.setPen( QColor( 0, 0, 0 ) ); + p.setPen( textColor ); for ( int n = 0; n < 12; n++ ) { - p.drawText( 5, 90 + 10 * n +3, noteNames[n] ); + p.drawText( 3, 90 + 10 * n +3, noteNames[n] ); } - // vertical lines - drawGridLines( p, Qt::DotLine ); - - p.setPen(res_1); - p.drawLine(0, 0, m_nEditorWidth, 0); - p.drawLine(0, m_nEditorHeight - 1, m_nEditorWidth, m_nEditorHeight - 1); - - - // Black outline each key + // Horizontal grid lines in the key region + p.setPen( QPen( lineColor, 1, Qt::SolidLine)); for (unsigned y = 90; y <= 210; y = y + 10 ) { - p.setPen( QPen( QColor( 0, 0, 0 ), 1, Qt::SolidLine)); - p.drawLine( m_nMargin, y-5, m_nMargin + nNotes * m_fGridWidth, y-5); + p.drawLine( PatternEditor::nMargin, y - 5, m_nActiveWidth, y-5); + } + + if ( m_nActiveWidth + 1 < m_nEditorWidth ) { + p.setPen( lineInactiveColor ); + for (unsigned y = 90; y <= 210; y = y + 10 ) { + p.drawLine( m_nActiveWidth, y - 5, m_nEditorWidth, y-5); + } } //paint the octave - if ( m_pPattern ) { + if ( m_pPattern != nullptr ) { int nSelectedInstrument = Hydrogen::get_instance()->getSelectedInstrumentNumber(); std::shared_ptr pSong = Hydrogen::get_instance()->getSong(); QPen selectedPen( selectedNoteColor() ); @@ -1229,14 +1314,14 @@ void NotePropertiesRuler::createNoteKeyBackground(QPixmap *pixmap) if ( !pNote->get_note_off() ) { uint x_pos = 17 + pNote->get_position() * m_fGridWidth; uint y_pos = (4-pNote->get_octave())*10-3; - p.setBrush(QColor( 99, 160, 233 )); + p.setBrush( DrumPatternEditor::computeNoteColor( pNote->get_velocity() ) ); p.drawEllipse( x_pos, y_pos, 6, 6); } } } //paint the note - if ( m_pPattern ) { + if ( m_pPattern != nullptr ) { int nSelectedInstrument = Hydrogen::get_instance()->getSelectedInstrumentNumber(); std::shared_ptr pSong = Hydrogen::get_instance()->getSong(); QPen selectedPen( selectedNoteColor() ); @@ -1260,8 +1345,8 @@ void NotePropertiesRuler::createNoteKeyBackground(QPixmap *pixmap) x_pos -= 1; y_pos -= 1; d += 2; - p.setPen( Qt::NoPen ); - p.setBrush(QColor( 0, 0, 0)); + p.setPen( QPen( Qt::black, 1 ) ); + p.setBrush( DrumPatternEditor::computeNoteColor( pNote->get_velocity() ) ); p.drawEllipse( x_pos, y_pos, d, d); // Paint selection outlines @@ -1270,17 +1355,31 @@ void NotePropertiesRuler::createNoteKeyBackground(QPixmap *pixmap) p.setPen( selectedPen ); p.setBrush( Qt::NoBrush ); p.setRenderHint( QPainter::Antialiasing ); - p.drawRoundedRect( x_pos - 1 -2 +3, 0, - nLineWidth + 4 + 4, height() , + p.drawRoundedRect( x_pos - 1 -2 +3, 2, + nLineWidth + 4 + 4, height() - 4, 4, 4 ); } } } } + + p.setPen( lineColor ); + p.setRenderHint( QPainter::Antialiasing ); + p.drawLine( 0, 0, m_nEditorWidth, 0 ); + p.setPen( QPen( lineColor, 2 ) ); + p.drawLine( 0, m_nEditorHeight, m_nEditorWidth, m_nEditorHeight ); + + if ( m_nActiveWidth + 1 < m_nEditorWidth ) { + p.setPen( lineInactiveColor ); + p.drawLine( m_nActiveWidth, 0, m_nEditorWidth, 0 ); + p.setPen( QPen( lineInactiveColor, 2 ) ); + p.drawLine( m_nActiveWidth, m_nEditorHeight, + m_nEditorWidth, m_nEditorHeight ); + } } -void NotePropertiesRuler::updateEditor( bool bPatternOnly ) +void NotePropertiesRuler::updateEditor( bool ) { Hydrogen *pHydrogen = Hydrogen::get_instance(); PatternList *pPatternList = pHydrogen->getSong()->getPatternList(); @@ -1294,54 +1393,54 @@ void NotePropertiesRuler::updateEditor( bool bPatternOnly ) m_nSelectedPatternNumber = nSelectedPatternNumber; // update editor width - if ( m_pPattern ) { - m_nEditorWidth = m_nMargin + m_pPattern->get_length() * m_fGridWidth; + if ( m_pPattern != nullptr ) { + m_nActiveWidth = PatternEditor::nMargin + m_fGridWidth * + m_pPattern->get_length(); + + if ( pHydrogen->getPatternMode() == + Song::PatternMode::Stacked ) { + m_nEditorWidth = + std::max( PatternEditor::nMargin + m_fGridWidth * + pHydrogen->getAudioEngine()->getPlayingPatterns()->longest_pattern_length() + 1, + static_cast(m_nActiveWidth) ); + } else { + m_nEditorWidth = m_nActiveWidth; + } } else { - m_nEditorWidth = m_nMargin + MAX_NOTES * m_fGridWidth; + m_nEditorWidth = PatternEditor::nMargin + MAX_NOTES * m_fGridWidth; + m_nActiveWidth = m_nEditorWidth; } - if ( !m_bNeedsUpdate ) { - m_bNeedsUpdate = true; - update(); - } + createBackground(); + update(); } -void NotePropertiesRuler::finishUpdateEditor() +void NotePropertiesRuler::createBackground() { - assert( m_bNeedsUpdate ); resize( m_nEditorWidth, height() ); - - delete m_pBackground; - m_pBackground = new QPixmap( m_nEditorWidth, m_nEditorHeight ); - - if ( m_Mode == VELOCITY || m_Mode == PROBABILITY ) { - createVelocityBackground( m_pBackground ); - } - else if ( m_Mode == PAN ) { - createPanBackground( m_pBackground ); + + qreal pixelRatio = devicePixelRatio(); + if ( m_pBackgroundPixmap->width() != m_nEditorWidth || + m_pBackgroundPixmap->height() != m_nEditorHeight || + m_pBackgroundPixmap->devicePixelRatio() != pixelRatio ) { + delete m_pBackgroundPixmap; + m_pBackgroundPixmap = new QPixmap( m_nEditorWidth * pixelRatio , + m_nEditorHeight * pixelRatio ); + m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio ); } - else if ( m_Mode == LEADLAG ) { - createLeadLagBackground( m_pBackground ); + + if ( m_mode == PatternEditor::Mode::Velocity || + m_mode == PatternEditor::Mode::Probability ) { + createNormalizedBackground( m_pBackgroundPixmap ); } - else if ( m_Mode == NOTEKEY ) { - createNoteKeyBackground( m_pBackground ); + else if ( m_mode == PatternEditor::Mode::Pan || + m_mode == PatternEditor::Mode::LeadLag ) { + createCenteredBackground( m_pBackgroundPixmap ); } - - if ( hasFocus() && ! HydrogenApp::get_instance()->hideKeyboardCursor() ) { - QPainter p( m_pBackground ); - - uint x = m_nMargin + m_pPatternEditorPanel->getCursorPosition() * m_fGridWidth; - - QPen pen( Qt::black ); - pen.setWidth( 2 ); - p.setPen( pen ); - p.setRenderHint( QPainter::Antialiasing ); - p.drawRoundedRect( QRect( x-m_fGridWidth*3, 0 + 3, m_fGridWidth*6, height() - 6 ), 4, 4 ); + else if ( m_mode == PatternEditor::Mode::NoteKey ) { + createNoteKeyBackground( m_pBackgroundPixmap ); } - - // redraw all - m_bNeedsUpdate = false; update(); } @@ -1351,16 +1450,17 @@ void NotePropertiesRuler::selectedPatternChangedEvent() updateEditor(); } - - void NotePropertiesRuler::selectedInstrumentChangedEvent() { updateEditor(); } - std::vector NotePropertiesRuler::elementsIntersecting( QRect r ) { std::vector result; + if ( m_pPattern == nullptr ) { + return std::move( result ); + } + const Pattern::notes_t* notes = m_pPattern->get_notes(); std::shared_ptr pSong = Hydrogen::get_instance()->getSong(); int nSelectedInstrument = Hydrogen::get_instance()->getSelectedInstrumentNumber(); @@ -1382,14 +1482,16 @@ std::vector NotePropertiesRuler::elementsIn } int pos = it->first; - uint x_pos = m_nMargin + pos * m_fGridWidth; + uint x_pos = PatternEditor::nMargin + pos * m_fGridWidth; if ( r.intersects( QRect( x_pos, 0, 1, height() ) ) ) { result.push_back( it->second ); } } - // Updating selection, we may need to repaint the whole widget. - updateEditor(); + // Updating selection, we may need to repaint the whole widget. + createBackground(); + update(); + return std::move(result); } @@ -1398,7 +1500,8 @@ std::vector NotePropertiesRuler::elementsIn /// QRect NotePropertiesRuler::getKeyboardCursorRect() { - uint x = m_nMargin + m_pPatternEditorPanel->getCursorPosition() * m_fGridWidth; + uint x = PatternEditor::nMargin + + m_pPatternEditorPanel->getCursorPosition() * m_fGridWidth; return QRect( x-m_fGridWidth*3, 3, m_fGridWidth*6, height()-6 ); } @@ -1412,7 +1515,7 @@ void NotePropertiesRuler::onPreferencesChanged( H2Core::Preferences::Changes cha if ( changes & ( H2Core::Preferences::Changes::Colors | H2Core::Preferences::Changes::Font ) ) { - m_bNeedsUpdate = true; + createBackground(); update(); } } diff --git a/src/gui/src/PatternEditor/NotePropertiesRuler.h b/src/gui/src/PatternEditor/NotePropertiesRuler.h index e53fd5cc3f..847425a5a2 100644 --- a/src/gui/src/PatternEditor/NotePropertiesRuler.h +++ b/src/gui/src/PatternEditor/NotePropertiesRuler.h @@ -29,6 +29,7 @@ #include #include +#include #include #include #include @@ -44,23 +45,16 @@ namespace H2Core class PatternEditorPanel; /** \ingroup docGUI*/ +//! NotePropertiesEditor is (currently) a single class instantiated in different "modes" to select +//! which property it edits. There are individual instances for each property which are hidden and +//! shown depending on what the user selects. class NotePropertiesRuler : public PatternEditor, protected WidgetWithScalableFont<7, 9, 11> { H2_OBJECT(NotePropertiesRuler) Q_OBJECT public: - //! NotePropertiesEditor is (currently) a single class instantiated in different "modes" to select - //! which property it edits. There are individual instances for each property which are hidden and - //! shown depending on what the user selects. - enum NotePropertiesMode { - VELOCITY, - PAN, - LEADLAG, - NOTEKEY, - PROBABILITY - }; - - NotePropertiesRuler( QWidget *parent, PatternEditorPanel *pPatternEditorPanel, NotePropertiesMode mode ); + + NotePropertiesRuler( QWidget *parent, PatternEditorPanel *pPatternEditorPanel, Mode mode ); ~NotePropertiesRuler(); NotePropertiesRuler(const NotePropertiesRuler&) = delete; @@ -80,6 +74,7 @@ class NotePropertiesRuler : public PatternEditor, protected WidgetWithScalableFo //! @{ virtual std::vector elementsIntersecting( QRect r ) override; virtual void mouseClickEvent( QMouseEvent *ev ) override; + virtual void mousePressEvent( QMouseEvent *ev ) override; virtual void mouseDragStartEvent( QMouseEvent *ev ) override; virtual void mouseDragUpdateEvent( QMouseEvent *ev ) override; virtual void mouseDragEndEvent( QMouseEvent *ev ) override; @@ -89,6 +84,7 @@ class NotePropertiesRuler : public PatternEditor, protected WidgetWithScalableFo virtual QRect getKeyboardCursorRect() override; //! @} + public slots: virtual void updateEditor( bool bPatternOnly = false ) override; virtual void selectAll() override; @@ -102,26 +98,20 @@ class NotePropertiesRuler : public PatternEditor, protected WidgetWithScalableFo private: bool m_bNeedsUpdate; - void finishUpdateEditor(); + void createBackground() override; + void drawDefaultBackground( QPainter& painter, int nHeight = 0, int nIncrement = 0 ); void drawFocus( QPainter& painter ); - NotePropertiesMode m_Mode; - - QPixmap *m_pBackground; - double m_fLastSetValue; bool m_bValueHasBeenSet; - void createVelocityBackground(QPixmap *pixmap); - void createPanBackground(QPixmap *pixmap); - void createLeadLagBackground(QPixmap *pixmap); + void createNormalizedBackground(QPixmap *pixmap); + void createCenteredBackground(QPixmap *pixmap); void createNoteKeyBackground(QPixmap *pixmap); void paintEvent(QPaintEvent *ev) override; void wheelEvent(QWheelEvent *ev) override; void keyPressEvent( QKeyEvent *ev ) override; - void focusInEvent( QFocusEvent *ev ) override; - void focusOutEvent( QFocusEvent *ev ) override; void addUndoAction(); void prepareUndoAction( int x ); void enterEvent( QEvent *ev ) override; diff --git a/src/gui/src/PatternEditor/PatternEditor.cpp b/src/gui/src/PatternEditor/PatternEditor.cpp index 2d357c778f..22fad195ab 100644 --- a/src/gui/src/PatternEditor/PatternEditor.cpp +++ b/src/gui/src/PatternEditor/PatternEditor.cpp @@ -20,6 +20,8 @@ */ #include "PatternEditor.h" +#include "PatternEditorRuler.h" +#include "PatternEditorInstrumentList.h" #include #include @@ -60,12 +62,17 @@ PatternEditor::PatternEditor( QWidget *pParent, , m_pPattern( nullptr ) , m_bSelectNewNotes( false ) , m_bFineGrained( false ) - , m_bCopyNotMove( false ) { + , m_bCopyNotMove( false ) + , m_nTick( -1 ) + , m_editor( Editor::None ) + , m_mode( Mode::None ) +{ auto pPref = H2Core::Preferences::get_instance(); m_fGridWidth = pPref->getPatternEditorGridWidth(); - m_nEditorWidth = m_nMargin + m_fGridWidth * ( MAX_NOTES * 4 ); + m_nEditorWidth = PatternEditor::nMargin + m_fGridWidth * ( MAX_NOTES * 4 ); + m_nActiveWidth = m_nEditorWidth; setFocusPolicy(Qt::StrongFocus); @@ -82,6 +89,11 @@ PatternEditor::PatternEditor( QWidget *pParent, m_pPopupMenu->addAction( tr( "&Delete" ), this, &PatternEditor::deleteSelection ); m_pPopupMenu->addAction( tr( "Select &all" ), this, &PatternEditor::selectAll ); m_pPopupMenu->addAction( tr( "Clear selection" ), this, &PatternEditor::selectNone ); + + qreal pixelRatio = devicePixelRatio(); + m_pBackgroundPixmap = new QPixmap( m_nEditorWidth * pixelRatio, + height() * pixelRatio ); + m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio ); } void PatternEditor::onPreferencesChanged( H2Core::Preferences::Changes changes ) @@ -125,32 +137,53 @@ void PatternEditor::zoomOut() } } -QColor PatternEditor::computeNoteColor( float velocity ) { - int red; - int green; - int blue; - - - /* - The note gets painted black if it has the default velocity (0.8). - The color changes if you alter the velocity.. - */ - - //qDebug() << "x: " << x; - //qDebug() << "x2: " << x*x; +QColor PatternEditor::computeNoteColor( float fVelocity ) { + float fRed, fGreen, fBlue; + auto pPref = H2Core::Preferences::get_instance(); - if( velocity < 0.8){ - red = fabs(-( velocity - 0.8))*255; - green = fabs(-( velocity - 0.8))*255; - blue = green * 1.25; + QColor fullColor = pPref->getColorTheme()->m_patternEditor_noteVelocityFullColor; + QColor defaultColor = pPref->getColorTheme()->m_patternEditor_noteVelocityDefaultColor; + QColor halfColor = pPref->getColorTheme()->m_patternEditor_noteVelocityHalfColor; + QColor zeroColor = pPref->getColorTheme()->m_patternEditor_noteVelocityZeroColor; + + // The colors defined in the Preferences correspond to fixed + // velocity values. In case the velocity lies between two of those + // the corresponding colors will be interpolated. + float fWeightFull = 0; + float fWeightDefault = 0; + float fWeightHalf = 0; + float fWeightZero = 0; + + if ( fVelocity >= 1.0 ) { + fWeightFull = 1.0; + } else if ( fVelocity >= 0.8 ) { + fWeightDefault = ( 1.0 - fVelocity )/ ( 1.0 - 0.8 ); + fWeightFull = 1.0 - fWeightDefault; + } else if ( fVelocity >= 0.5 ) { + fWeightHalf = ( 0.8 - fVelocity )/ ( 0.8 - 0.5 ); + fWeightDefault = 1.0 - fWeightHalf; } else { - green = blue = 0; - red = (velocity-0.8)*5*255; + fWeightZero = ( 0.5 - fVelocity )/ ( 0.5 ); + fWeightHalf = 1.0 - fWeightZero; } - //qDebug() << "R " << red << "G " << green << "blue " << blue; - return QColor( red, green, blue ); + fRed = fWeightFull * fullColor.redF() + + fWeightDefault * defaultColor.redF() + + fWeightHalf * halfColor.redF() + fWeightZero * zeroColor.redF(); + fGreen = fWeightFull * fullColor.greenF() + + fWeightDefault * defaultColor.greenF() + + fWeightHalf * halfColor.greenF() + fWeightZero * zeroColor.greenF(); + fBlue = fWeightFull * fullColor.blueF() + + fWeightDefault * defaultColor.blueF() + + fWeightHalf * halfColor.blueF() + fWeightZero * zeroColor.blueF(); + + QColor color; + color.setRedF( fRed ); + color.setGreenF( fGreen ); + color.setBlueF( fBlue ); + + return color; } @@ -158,8 +191,10 @@ void PatternEditor::drawNoteSymbol( QPainter &p, QPoint pos, H2Core::Note *pNote { auto pPref = H2Core::Preferences::get_instance(); - static const QColor noteColor( pPref->getColorTheme()->m_patternEditor_noteColor ); - static const QColor noteoffColor( pPref->getColorTheme()->m_patternEditor_noteoffColor ); + const QColor noteColor( pPref->getColorTheme()->m_patternEditor_noteVelocityDefaultColor ); + const QColor noteInactiveColor( pPref->getColorTheme()->m_windowTextColor.darker( 150 ) ); + const QColor noteoffColor( pPref->getColorTheme()->m_patternEditor_noteOffColor ); + const QColor noteoffInactiveColor( pPref->getColorTheme()->m_windowTextColor ); p.setRenderHint( QPainter::Antialiasing ); @@ -195,6 +230,12 @@ void PatternEditor::drawNoteSymbol( QPainter &p, QPoint pos, H2Core::Note *pNote QBrush noteBrush( color ); QPen notePen( noteColor ); if ( !bIsForeground ) { + + if ( x_pos >= m_nActiveWidth ) { + noteBrush.setColor( noteInactiveColor ); + notePen.setColor( noteInactiveColor ); + } + noteBrush.setStyle( Qt::Dense4Pattern ); notePen.setStyle( Qt::DotLine ); } @@ -245,6 +286,10 @@ void PatternEditor::drawNoteSymbol( QPainter &p, QPoint pos, H2Core::Note *pNote QBrush noteOffBrush( noteoffColor ); if ( !bIsForeground ) { noteOffBrush.setStyle( Qt::Dense4Pattern ); + + if ( x_pos >= m_nActiveWidth ) { + noteOffBrush.setColor( noteoffInactiveColor ); + } } p.setPen( Qt::NoPen ); @@ -267,7 +312,7 @@ int PatternEditor::getColumn( int x, bool bUseFineGrained ) const nGranularity = granularity(); } int nWidth = m_fGridWidth * nGranularity; - int nColumn = ( x - m_nMargin + (nWidth / 2) ) / nWidth; + int nColumn = ( x - PatternEditor::nMargin + (nWidth / 2) ) / nWidth; nColumn = nColumn * nGranularity; if ( nColumn < 0 ) { return 0; @@ -342,6 +387,10 @@ void PatternEditor::cut() void PatternEditor::selectInstrumentNotes( int nInstrument ) { + if ( m_pPattern == nullptr ) { + return; + } + InstrumentList *pInstrumentList = Hydrogen::get_instance()->getSong()->getInstrumentList(); auto pInstrument = pInstrumentList->get( nInstrument ); @@ -420,6 +469,10 @@ bool PatternEditor::notesMatchExactly( Note *pNoteA, Note *pNoteB ) const { bool PatternEditor::checkDeselectElements( std::vector &elements ) { + if ( m_pPattern == nullptr ) { + return false; + } + // Hydrogen *pH = Hydrogen::get_instance(); std::set< Note *> duplicates; for ( Note *pNote : elements ) { @@ -473,6 +526,10 @@ bool PatternEditor::checkDeselectElements( std::vector &elements void PatternEditor::deselectAndOverwriteNotes( std::vector< H2Core::Note *> &selected, std::vector< H2Core::Note *> &overwritten ) { + if ( m_pPattern == nullptr ) { + return; + } + // Iterate over all the notes in 'selected' and 'overwrite' by erasing any *other* notes occupying the // same position. m_pAudioEngine->lock( RIGHT_HERE ); @@ -504,6 +561,10 @@ void PatternEditor::deselectAndOverwriteNotes( std::vector< H2Core::Note *> &sel void PatternEditor::undoDeselectAndOverwriteNotes( std::vector< H2Core::Note *> &selected, std::vector< H2Core::Note *> &overwritten ) { + if ( m_pPattern == nullptr ) { + return; + } + // Restore previously-overwritten notes, and select notes that were selected before. m_selection.clearSelection( /* bCheck=*/false ); m_pAudioEngine->lock( RIGHT_HERE ); @@ -533,7 +594,7 @@ void PatternEditor::updatePatternInfo() { m_pPattern = nullptr; m_nSelectedPatternNumber = pHydrogen->getSelectedPatternNumber(); - if ( pSong ) { + if ( pSong != nullptr ) { PatternList *pPatternList = pSong->getPatternList(); if ( ( m_nSelectedPatternNumber != -1 ) && ( m_nSelectedPatternNumber < pPatternList->size() ) ) { m_pPattern = pPatternList->get( m_nSelectedPatternNumber ); @@ -567,21 +628,24 @@ QPoint PatternEditor::movingGridOffset( ) const { //! Draw lines for note grid. void PatternEditor::drawGridLines( QPainter &p, Qt::PenStyle style ) const { + auto pPref = H2Core::Preferences::get_instance(); - static const QColor res[5] = { + const QColor colorsActive[5] = { QColor( pPref->getColorTheme()->m_patternEditor_line1Color ), QColor( pPref->getColorTheme()->m_patternEditor_line2Color ), QColor( pPref->getColorTheme()->m_patternEditor_line3Color ), QColor( pPref->getColorTheme()->m_patternEditor_line4Color ), QColor( pPref->getColorTheme()->m_patternEditor_line5Color ), }; + const QColor colorsInactive[5] = { + QColor( pPref->getColorTheme()->m_windowTextColor.darker( 170 ) ), + QColor( pPref->getColorTheme()->m_windowTextColor.darker( 190 ) ), + QColor( pPref->getColorTheme()->m_windowTextColor.darker( 200 ) ), + QColor( pPref->getColorTheme()->m_windowTextColor.darker( 210 ) ), + QColor( pPref->getColorTheme()->m_windowTextColor.darker( 230 ) ), + }; int nGranularity = granularity() * m_nResolution; - int nNotes = MAX_NOTES; - if ( m_pPattern ) { - nNotes = m_pPattern->get_length(); - } - int nMaxX = m_fGridWidth * nNotes + m_nMargin; if ( !m_bUseTriplets ) { @@ -601,8 +665,13 @@ void PatternEditor::drawGridLines( QPainter &p, Qt::PenStyle style ) const // First, quarter note markers. All the quarter note markers must be drawn. if ( m_nResolution >= nRes ) { - p.setPen( QPen( res[ 0 ], 0, style ) ); - for ( float x = m_nMargin ; x < nMaxX; x += fStep ) { + p.setPen( QPen( colorsActive[ 0 ], 1, style ) ); + for ( float x = PatternEditor::nMargin ; x < m_nActiveWidth; x += fStep ) { + p.drawLine( x, 1, x, m_nEditorHeight - 1 ); + } + + p.setPen( QPen( colorsInactive[ 0 ], 1, style ) ); + for ( float x = m_nActiveWidth ; x < m_nEditorWidth; x += fStep ) { p.drawLine( x, 1, x, m_nEditorHeight - 1 ); } } @@ -614,10 +683,17 @@ void PatternEditor::drawGridLines( QPainter &p, Qt::PenStyle style ) const // pitch, so only the odd numbered lines need to be drawn. int nColour = 1; while ( m_nResolution >= nRes ) { - p.setPen( QPen( res[ nColour++ ], 0, style ) ); - for ( float x = m_nMargin + fStep; x < nMaxX; x += fStep * 2) { + nColour++; + p.setPen( QPen( colorsActive[ nColour ], 1, style ) ); + for ( float x = PatternEditor::nMargin + fStep; x < m_nActiveWidth + fStep; x += fStep * 2) { + p.drawLine( x, 1, x, m_nEditorHeight - 1 ); + } + + p.setPen( QPen( colorsInactive[ nColour ], 1, style ) ); + for ( float x = m_nActiveWidth + fStep; x < m_nEditorWidth; x += fStep * 2) { p.drawLine( x, 1, x, m_nEditorHeight - 1 ); } + nRes *= 2; fStep /= 2; } @@ -627,13 +703,25 @@ void PatternEditor::drawGridLines( QPainter &p, Qt::PenStyle style ) const // Triplet style markers, we only differentiate colours on the // first of every triplet. float fStep = granularity() * m_fGridWidth; - p.setPen( QPen( res[ 0 ], 0, style ) ); - for ( float x = m_nMargin; x < nMaxX; x += fStep * 3 ) { + p.setPen( QPen( colorsActive[ 0 ], 1, style ) ); + for ( float x = PatternEditor::nMargin; x < m_nActiveWidth; x += fStep * 3 ) { + p.drawLine(x, 1, x, m_nEditorHeight - 1); + } + + p.setPen( QPen( colorsInactive[ 0 ], 1, style ) ); + for ( float x = m_nActiveWidth; x < m_nEditorWidth; x += fStep * 3 ) { p.drawLine(x, 1, x, m_nEditorHeight - 1); } + // Second and third marks - p.setPen( QPen( res[ 2 ], 0, style ) ); - for ( float x = m_nMargin + fStep; x < nMaxX; x += fStep * 3 ) { + p.setPen( QPen( colorsActive[ 2 ], 1, style ) ); + for ( float x = PatternEditor::nMargin + fStep; x < m_nActiveWidth + fStep; x += fStep * 3 ) { + p.drawLine(x, 1, x, m_nEditorHeight - 1); + p.drawLine(x + fStep, 1, x + fStep, m_nEditorHeight - 1); + } + + p.setPen( QPen( colorsInactive[ 2 ], 1, style ) ); + for ( float x = m_nActiveWidth + fStep; x < m_nEditorWidth; x += fStep * 3 ) { p.drawLine(x, 1, x, m_nEditorHeight - 1); p.drawLine(x + fStep, 1, x + fStep, m_nEditorHeight - 1); } @@ -647,10 +735,10 @@ QColor PatternEditor::selectedNoteColor() const { auto pPref = H2Core::Preferences::get_instance(); if ( hasFocus() ) { - static const QColor selectHilightColor( pPref->getColorTheme()->m_selectionHighlightColor ); - return selectHilightColor; + const QColor selectHighlightColor( pPref->getColorTheme()->m_selectionHighlightColor ); + return selectHighlightColor; } else { - static const QColor selectInactiveColor( pPref->getColorTheme()->m_selectionInactiveColor ); + const QColor selectInactiveColor( pPref->getColorTheme()->m_selectionInactiveColor ); return selectInactiveColor; } } @@ -661,6 +749,10 @@ QColor PatternEditor::selectedNoteColor() const { /// void PatternEditor::validateSelection() { + if ( m_pPattern == nullptr ) { + return; + } + // Rebuild selection from valid notes. std::set valid; std::vector< Note *> invalidated; @@ -695,8 +787,42 @@ void PatternEditor::enterEvent( QEvent *ev ) { void PatternEditor::leaveEvent( QEvent *ev ) { UNUSED( ev ); m_bEntered = false; + update(); } +void PatternEditor::focusInEvent( QFocusEvent *ev ) { + UNUSED( ev ); + if ( ev->reason() == Qt::TabFocusReason || ev->reason() == Qt::BacktabFocusReason ) { + HydrogenApp::get_instance()->setHideKeyboardCursor( false ); + m_pPatternEditorPanel->ensureCursorVisible(); + } + if ( ! HydrogenApp::get_instance()->hideKeyboardCursor() ) { + // Immediate update to prevent visual delay. + m_pPatternEditorPanel->getPatternEditorRuler()->update(); + m_pPatternEditorPanel->getInstrumentList()->update(); + } + + // If there are some patterns selected, we have to switch their + // border color inactive <-> active. + createBackground(); + update(); +} + +void PatternEditor::focusOutEvent( QFocusEvent *ev ) { + UNUSED( ev ); + if ( ! HydrogenApp::get_instance()->hideKeyboardCursor() ) { + m_pPatternEditorPanel->getPatternEditorRuler()->update(); + m_pPatternEditorPanel->getInstrumentList()->update(); + } + + // If there are some patterns selected, we have to switch their + // border color inactive <-> active. + createBackground(); + update(); +} + +void PatternEditor::createBackground() { +} //! Get notes to show in pattern editor. //! This may include "background" notes that are in currently-playing patterns @@ -707,27 +833,25 @@ std::vector< Pattern *> PatternEditor::getPatternsToShow( void ) std::vector patterns; // Add stacked-mode patterns - if ( pHydrogen->getSong()->getMode() == Song::Mode::Pattern ) { - if ( !Preferences::get_instance()->patternModePlaysSelected() ) { - m_pAudioEngine->lock( RIGHT_HERE ); - std::set< Pattern *> patternSet; - for ( const PatternList *pPatternList : { m_pAudioEngine->getPlayingPatterns(), - m_pAudioEngine->getNextPatterns() } ) { - for ( int i = 0; i < pPatternList->size(); i++) { - Pattern *pPattern = pPatternList->get( i ); - if ( pPattern != m_pPattern ) { - patternSet.insert( pPattern ); - } + if ( pHydrogen->getPatternMode() == Song::PatternMode::Stacked ) { + m_pAudioEngine->lock( RIGHT_HERE ); + std::set< Pattern *> patternSet; + for ( const PatternList *pPatternList : { m_pAudioEngine->getPlayingPatterns(), + m_pAudioEngine->getNextPatterns() } ) { + for ( int i = 0; i < pPatternList->size(); i++) { + Pattern *pPattern = pPatternList->get( i ); + if ( pPattern != m_pPattern ) { + patternSet.insert( pPattern ); } } - for ( Pattern *pPattern : patternSet ) { - patterns.push_back( pPattern ); - } - m_pAudioEngine->unlock(); } + for ( Pattern *pPattern : patternSet ) { + patterns.push_back( pPattern ); + } + m_pAudioEngine->unlock(); } - if ( m_pPattern ) { + if ( m_pPattern != nullptr ) { patterns.push_back( m_pPattern ); } @@ -748,3 +872,471 @@ void PatternEditor::stackedModeActivationEvent( int nValue ) // May need to draw (or hide) other background patterns update(); } + +void PatternEditor::updatePosition( float fTick ) { + m_nTick = fTick; + update(); +} + +void PatternEditor::storeNoteProperties( const Note* pNote ) { + if( pNote != nullptr ){ + m_nOldLength = pNote->get_length(); + //needed to undo note properties + m_fOldVelocity = pNote->get_velocity(); + m_fOldPan = pNote->getPan(); + + m_fOldLeadLag = pNote->get_lead_lag(); + + m_fVelocity = m_fOldVelocity; + m_fPan = m_fOldPan; + m_fLeadLag = m_fOldLeadLag; + } + else { + m_nOldLength = -1; + } +} + +void PatternEditor::mouseDragStartEvent( QMouseEvent *ev ) { + + auto pHydrogenApp = HydrogenApp::get_instance(); + auto pHydrogen = Hydrogen::get_instance(); + + // Move cursor. + int nColumn = getColumn( ev->x() ); + m_pPatternEditorPanel->setCursorPosition( nColumn ); + + // Hide cursor. + bool bOldCursorHidden = pHydrogenApp->hideKeyboardCursor(); + + pHydrogenApp->setHideKeyboardCursor( true ); + + // Cursor either just got hidden or was moved. + if ( bOldCursorHidden != pHydrogenApp->hideKeyboardCursor() ) { + // Immediate update to prevent visual delay. + m_pPatternEditorPanel->getInstrumentList()->repaintInstrumentLines(); + m_pPatternEditorPanel->getPatternEditorRuler()->update(); + } + + int nRealColumn = 0; + + if ( ev->button() == Qt::RightButton ) { + + int nPressedLine = + std::floor(static_cast(ev->y()) / static_cast(m_nGridHeight)); + int nSelectedInstrumentNumber = pHydrogen->getSelectedInstrumentNumber(); + + if( ev->x() > PatternEditor::nMargin ) { + nRealColumn = + static_cast(std::floor( + static_cast((ev->x() - PatternEditor::nMargin)) / + m_fGridWidth)); + } + + // Needed for undo changes in the note length + m_nOldPoint = ev->y(); + m_nRealColumn = nRealColumn; + m_nColumn = nColumn; + m_nPressedLine = nPressedLine; + m_nSelectedInstrumentNumber = pHydrogen->getSelectedInstrumentNumber(); + } + +} + +void PatternEditor::mouseDragUpdateEvent( QMouseEvent *ev) { + + if ( m_pPattern == nullptr || m_pDraggedNote == nullptr ) { + return; + } + + if ( m_pDraggedNote->get_note_off() ) { + return; + } + + int nTickColumn = getColumn( ev->x() ); + + m_pAudioEngine->lock( RIGHT_HERE ); + int nLen = nTickColumn - m_pDraggedNote->get_position(); + + if ( nLen <= 0 ) { + nLen = -1; + } + + float fNotePitch = m_pDraggedNote->get_notekey_pitch(); + float fStep = 0; + if ( nLen > -1 ){ + fStep = Note::pitchToFrequency( ( double )fNotePitch ); + } else { + fStep = 1.0; + } + m_pDraggedNote->set_length( nLen * fStep); + + m_mode = m_pPatternEditorPanel->getNotePropertiesMode(); + + // edit note property. We do not support the note key property. + if ( m_mode != Mode::NoteKey ) { + + float fValue = 0.0; + if ( m_mode == Mode::Velocity ) { + fValue = m_pDraggedNote->get_velocity(); + } + else if ( m_mode == Mode::Pan ) { + fValue = m_pDraggedNote->getPanWithRangeFrom0To1(); + } + else if ( m_mode == Mode::LeadLag ) { + fValue = ( m_pDraggedNote->get_lead_lag() - 1.0 ) / -2.0 ; + } + else if ( m_mode == Mode::Probability ) { + fValue = m_pDraggedNote->get_probability(); + } + + float fMoveY = m_nOldPoint - ev->y(); + fValue = fValue + (fMoveY / 100); + if ( fValue > 1 ) { + fValue = 1; + } + else if ( fValue < 0.0 ) { + fValue = 0.0; + } + + if ( m_mode == Mode::Velocity ) { + m_pDraggedNote->set_velocity( fValue ); + m_fVelocity = fValue; + } + else if ( m_mode == Mode::Pan ) { + m_pDraggedNote->setPanWithRangeFrom0To1( fValue ); + m_fPan = m_pDraggedNote->getPan(); + } + else if ( m_mode == Mode::LeadLag ) { + m_pDraggedNote->set_lead_lag( ( fValue * -2.0 ) + 1.0 ); + m_fLeadLag = ( fValue * -2.0 ) + 1.0; + } + else if ( m_mode == Mode::Probability ) { + m_pDraggedNote->set_probability( fValue ); + m_fProbability = fValue; + } + + PatternEditor::triggerStatusMessage( m_pDraggedNote, m_mode ); + + m_nOldPoint = ev->y(); + } + + m_pAudioEngine->unlock(); // unlock the audio engine + Hydrogen::get_instance()->setIsModified( true ); + + if ( m_pPatternEditorPanel != nullptr ) { + m_pPatternEditorPanel->updateEditors( true ); + } +} + +void PatternEditor::mouseDragEndEvent( QMouseEvent* ev ) { + + UNUSED( ev ); + unsetCursor(); + + if ( m_pPattern == nullptr || m_nSelectedPatternNumber == -1 ) { + return; + } + + if ( m_pDraggedNote == nullptr || m_pDraggedNote->get_note_off() ) { + return; + } + + if ( m_pDraggedNote->get_length() != m_nOldLength ) { + SE_editNoteLengthAction *action = + new SE_editNoteLengthAction( m_pDraggedNote->get_position(), + m_pDraggedNote->get_position(), + m_nRow, + m_pDraggedNote->get_length(), + m_nOldLength, + m_nSelectedPatternNumber, + m_nSelectedInstrumentNumber, + m_editor ); + HydrogenApp::get_instance()->m_pUndoStack->push( action ); + } + + + if( m_fVelocity == m_fOldVelocity && + m_fOldPan == m_fPan && + m_fOldLeadLag == m_fLeadLag && + m_fOldProbability == m_fProbability ) { + return; + } + + SE_editNotePropertiesAction *action = + new SE_editNotePropertiesAction( m_pDraggedNote->get_position(), + m_pDraggedNote->get_position(), + m_nRow, + m_nSelectedPatternNumber, + m_nSelectedInstrumentNumber, + m_mode, + m_editor, + m_fVelocity, + m_fOldVelocity, + m_fPan, + m_fOldPan, + m_fLeadLag, + m_fOldLeadLag, + m_fProbability, + m_fOldProbability ); + HydrogenApp::get_instance()->m_pUndoStack->push( action ); +} + +void PatternEditor::editNoteLengthAction( int nColumn, + int nRealColumn, + int nRow, + int nLength, + int nSelectedPatternNumber, + int nSelectedInstrumentnumber, + Editor editor) +{ + + auto pHydrogen = Hydrogen::get_instance(); + auto pSong = pHydrogen->getSong(); + auto pPatternList = pSong->getPatternList(); + + H2Core::Pattern* pPattern = nullptr; + if ( nSelectedPatternNumber != -1 && + nSelectedPatternNumber < pPatternList->size() ) { + pPattern = pPatternList->get( nSelectedPatternNumber ); + } + + if ( pPattern == nullptr ) { + return; + } + + m_pAudioEngine->lock( RIGHT_HERE ); + + // Find the note to edit + Note* pDraggedNote = nullptr; + if ( editor == Editor::PianoRoll ) { + auto pSelectedInstrument = + pSong->getInstrumentList()->get( nSelectedInstrumentnumber ); + + Note::Octave pressedOctave = Note::pitchToOctave( lineToPitch( nRow ) ); + Note::Key pressedNoteKey = Note::pitchToKey( lineToPitch( nRow ) ); + + auto pDraggedNote = pPattern->find_note( nColumn, nRealColumn, + pSelectedInstrument, + pressedNoteKey, pressedOctave, + false ); + } + else if ( editor == Editor::DrumPattern ) { + auto pSelectedInstrument = pSong->getInstrumentList()->get( nRow ); + pDraggedNote = pPattern->find_note( nColumn, nRealColumn, pSelectedInstrument, false ); + } + else { + ERRORLOG( QString( "Unsupported editor [%1]" ) + .arg( static_cast(editor) ) ); + m_pAudioEngine->unlock(); + return; + } + + if ( pDraggedNote != nullptr ){ + pDraggedNote->set_length( nLength ); + } + + m_pAudioEngine->unlock(); + + pHydrogen->setIsModified( true ); + + if ( m_pPatternEditorPanel != nullptr ) { + m_pPatternEditorPanel->updateEditors( true ); + } +} + + +void PatternEditor::editNotePropertiesAction( int nColumn, + int nRealColumn, + int nRow, + int nSelectedPatternNumber, + int nSelectedInstrumentNumber, + Mode mode, + Editor editor, + float fVelocity, + float fPan, + float fLeadLag, + float fProbability ) +{ + + auto pHydrogen = Hydrogen::get_instance(); + auto pSong = pHydrogen->getSong(); + auto pPatternList = pSong->getPatternList(); + + H2Core::Pattern* pPattern = nullptr; + if ( nSelectedPatternNumber != -1 && + nSelectedPatternNumber < pPatternList->size() ) { + pPattern = pPatternList->get( nSelectedPatternNumber ); + } + + if ( pPattern == nullptr ) { + return; + } + + m_pAudioEngine->lock( RIGHT_HERE ); + + // Find the note to edit + Note* pDraggedNote = nullptr; + if ( editor == Editor::PianoRoll ) { + + auto pSelectedInstrument = + pSong->getInstrumentList()->get( nSelectedInstrumentNumber ); + + Note::Octave pressedOctave = Note::pitchToOctave( lineToPitch( nRow ) ); + Note::Key pressedNoteKey = Note::pitchToKey( lineToPitch( nRow ) ); + + pDraggedNote = pPattern->find_note( nColumn, nRealColumn, + pSelectedInstrument, + pressedNoteKey, pressedOctave, + false ); + } + else if ( editor == Editor::DrumPattern ) { + auto pSelectedInstrument = pSong->getInstrumentList()->get( nRow ); + pDraggedNote = pPattern->find_note( nColumn, nRealColumn, pSelectedInstrument, false ); + } + else { + ERRORLOG( QString( "Unsupported editor [%1]" ) + .arg( static_cast(editor) ) ); + m_pAudioEngine->unlock(); + return; + } + + bool bValueChanged = true; + + if ( pDraggedNote != nullptr ){ + switch ( mode ) { + case Mode::Velocity: + pDraggedNote->set_velocity( fVelocity ); + break; + case Mode::Pan: + pDraggedNote->setPan( fPan ); + break; + case Mode::LeadLag: + pDraggedNote->set_lead_lag( fLeadLag ); + break; + case Mode::Probability: + pDraggedNote->set_probability( fProbability ); + break; + } + bValueChanged = true; + PatternEditor::triggerStatusMessage( pDraggedNote, mode ); + } else { + ERRORLOG("note could not be found"); + } + + m_pAudioEngine->unlock(); + + if ( bValueChanged && + m_pPatternEditorPanel != nullptr ) { + pHydrogen->setIsModified( true ); + m_pPatternEditorPanel->updateEditors( true ); + } +} + + +QString PatternEditor::modeToQString( Mode mode ) { + QString s; + + switch ( mode ) { + case PatternEditor::Mode::Velocity: + s = "Velocity"; + break; + case PatternEditor::Mode::Pan: + s = "Pan"; + break; + case PatternEditor::Mode::LeadLag: + s = "LeadLag"; + break; + case PatternEditor::Mode::NoteKey: + s = "NoteKey"; + break; + case PatternEditor::Mode::Probability: + s = "Probability"; + break; + default: + s = QString( "Unknown mode [%1]" ).arg( static_cast(mode) ) ; + break; + } + + return s; +} + +void PatternEditor::triggerStatusMessage( Note* pNote, Mode mode ) { + QString s; + QString sUnit( tr( "ticks" ) ); + float fValue; + + switch ( mode ) { + case PatternEditor::Mode::Velocity: + if ( ! pNote->get_note_off() ) { + s = QString( tr( "Set note velocity" ) ) + .append( QString( ": [%1]") + .arg( pNote->get_velocity(), 2, 'f', 2 ) ); + } + break; + + case PatternEditor::Mode::Pan: + if ( ! pNote->get_note_off() ) { + + // Round the pan to not miss the center due to fluctuations + fValue = pNote->getPan() * 100; + fValue = std::round( fValue ); + fValue = fValue / 100; + + if ( fValue > 0.0 ) { + s = QString( tr( "Note panned to the right by" ) ). + append( QString( ": [%1]" ).arg( fValue / 2, 2, 'f', 2 ) ); + } else if ( fValue < 0.0 ) { + s = QString( tr( "Note panned to the left by" ) ). + append( QString( ": [%1]" ).arg( -1 * fValue / 2, 2, 'f', 2 ) ); + } else { + s = QString( tr( "Note centered" ) ); + } + } + break; + + case PatternEditor::Mode::LeadLag: + // Round the pan to not miss the center due to fluctuations + fValue = pNote->get_lead_lag() * 100; + fValue = std::round( fValue ); + fValue = fValue / 100; + if ( fValue < 0.0 ) { + s = QString( tr( "Leading beat by" ) ) + .append( QString( ": [%1] " ) + .arg( fValue * -1 * + AudioEngine::getLeadLagInTicks() , 2, 'f', 2 ) ) + .append( sUnit ); + } + else if ( fValue > 0.0 ) { + s = QString( tr( "Lagging beat by" ) ) + .append( QString( ": [%1] " ) + .arg( fValue * + AudioEngine::getLeadLagInTicks() , 2, 'f', 2 ) ) + .append( sUnit ); + } + else { + s = tr( "Note on beat" ); + } + break; + + case PatternEditor::Mode::NoteKey: + s = QString( tr( "Set pitch" ) ).append( ": " ).append( tr( "key" ) ) + .append( QString( " [%1], " ).arg( Note::KeyToQString( pNote->get_key() ) ) ) + .append( tr( "octave" ) ) + .append( QString( ": [%1]" ).arg( pNote->get_octave() ) ); + break; + + case PatternEditor::Mode::Probability: + if ( ! pNote->get_note_off() ) { + s = tr( "Set note probability to" ) + .append( QString( ": [%1]" ).arg( pNote->get_probability(), 2, 'f', 2 ) ); + } + break; + + default: + ERRORLOG( PatternEditor::modeToQString( mode ) ); + } + + if ( ! s.isEmpty() ) { + HydrogenApp::get_instance()->setStatusBarMessage( s, 2000 ); + } +} diff --git a/src/gui/src/PatternEditor/PatternEditor.h b/src/gui/src/PatternEditor/PatternEditor.h index d5ff9b8997..be9f4f339c 100644 --- a/src/gui/src/PatternEditor/PatternEditor.h +++ b/src/gui/src/PatternEditor/PatternEditor.h @@ -62,6 +62,23 @@ class PatternEditor : public QWidget, Q_OBJECT public: + enum class Editor { + DrumPattern = 0, + PianoRoll = 1, + NotePropertiesRuler = 2, + None = 3 + }; + + enum class Mode { + Velocity = 0, + Pan = 1, + LeadLag = 2, + NoteKey = 3, + Probability = 4, + None = 5 + }; + static QString modeToQString( Mode mode ); + PatternEditor( QWidget *pParent, PatternEditorPanel *panel ); @@ -131,11 +148,49 @@ class PatternEditor : public QWidget, virtual void mousePressEvent( QMouseEvent *ev ) override; virtual void mouseMoveEvent( QMouseEvent *ev ) override; virtual void mouseReleaseEvent( QMouseEvent *ev ) override; + + virtual void mouseDragStartEvent( QMouseEvent *ev ) override; + virtual void mouseDragUpdateEvent( QMouseEvent *ev ) override; + virtual void mouseDragEndEvent( QMouseEvent *ev ) override; virtual void songModeActivationEvent( int nValue ) override; virtual void stackedModeActivationEvent( int nValue ) override; + static constexpr int nMargin = 20; + + /** Caches the AudioEngine::m_nPatternTickPosition in the member + variable #m_nTick and triggeres an update(). */ + void updatePosition( float fTick ); + void editNoteLengthAction( int nColumn, + int nRealColumn, + int nRow, + int nLength, + int nSelectedPatternNumber, + int nSelectedInstrumentnumber, + Editor editor ); + + void editNotePropertiesAction( int nColumn, + int nRealColumn, + int nRow, + int nSelectedPatternNumber, + int nSelectedInstrumentNumber, + Mode mode, + Editor editor, + float fVelocity, + float fPan, + float fLeadLag, + float fProbability ); + static void triggerStatusMessage( H2Core::Note* pNote, Mode mode ); + + // Pitch / line conversions + int lineToPitch( int nLine ) { + return 12 * (OCTAVE_MIN+m_nOctaves) - 1 - nLine; + } + int pitchToLine( int nPitch ) { + return 12 * (OCTAVE_MIN+m_nOctaves) - 1 - nPitch; + } + protected: //! The Selection object. @@ -171,14 +226,15 @@ public slots: uint m_nEditorHeight; uint m_nEditorWidth; + // width of the editor covered by the current pattern. + int m_nActiveWidth; + float m_fGridWidth; unsigned m_nGridHeight; int m_nSelectedPatternNumber = 0; H2Core::Pattern *m_pPattern; - const int m_nMargin = 20; - uint m_nResolution; bool m_bUseTriplets; bool m_bFineGrained; @@ -212,11 +268,45 @@ public slots: //! Update current pattern information void updatePatternInfo(); + /** Updates #m_pBackgroundPixmap to show the latest content. */ + virtual void createBackground(); + QPixmap *m_pBackgroundPixmap; + /** Indicates whether the mouse pointer entered the widget.*/ bool m_bEntered; virtual void enterEvent( QEvent *ev ) override; virtual void leaveEvent( QEvent *ev ) override; + virtual void focusInEvent( QFocusEvent *ev ) override; + virtual void focusOutEvent( QFocusEvent *ev ) override; + int m_nTick; + + unsigned m_nOctaves = 7; + + /** Stores the properties of @a pNote in member variables.*/ + void storeNoteProperties( const H2Core::Note* pNote ); + + /** Cached properties used when adjusting a note property via + * right-press mouse movement. + */ + int m_nSelectedInstrumentNumber = 0; + int m_nRealColumn = 0; + int m_nColumn = 0; + int m_nRow = 0; + int m_nPressedLine = 0; + int m_nOldPoint; + + int m_nOldLength = 0; + float m_fVelocity = 0; + float m_fOldVelocity = 0; + float m_fPan = 0; + float m_fOldPan = 0; + float m_fLeadLag = 0; + float m_fOldLeadLag = 0; + float m_fProbability = 0; + float m_fOldProbability = 0; + Editor m_editor; + Mode m_mode; }; #endif // PATERN_EDITOR_H diff --git a/src/gui/src/PatternEditor/PatternEditorInstrumentList.cpp b/src/gui/src/PatternEditor/PatternEditorInstrumentList.cpp index a8c0f9ea5b..e084966d39 100644 --- a/src/gui/src/PatternEditor/PatternEditorInstrumentList.cpp +++ b/src/gui/src/PatternEditor/PatternEditorInstrumentList.cpp @@ -42,6 +42,7 @@ using namespace H2Core; #include "../HydrogenApp.h" #include "../Mixer/Mixer.h" #include "../Widgets/Button.h" +#include "../Skin.h" #include #include @@ -53,7 +54,9 @@ using namespace H2Core; InstrumentLine::InstrumentLine(QWidget* pParent) : PixmapWidget(pParent) - , m_bIsSelected(false) + , WidgetWithHighlightedList() + , m_bIsSelected( false ) + , m_bEntered( false ) { auto pPref = H2Core::Preferences::get_instance(); @@ -63,7 +66,6 @@ InstrumentLine::InstrumentLine(QWidget* pParent) setFixedSize(181, h); QFont nameFont( pPref->getLevel2FontFamily(), getPointSize( pPref->getFontSize() ) ); - nameFont.setBold( true ); m_pNameLbl = new QLabel(this); m_pNameLbl->resize( 145, h ); @@ -72,7 +74,7 @@ InstrumentLine::InstrumentLine(QWidget* pParent) /*: Text displayed on the button for muting an instrument. Its size is designed for a single character.*/ - m_pMuteBtn = new Button( this, QSize( 18, height() - 1 ), + m_pMuteBtn = new Button( this, QSize( InstrumentLine::m_nButtonWidth, height() - 1 ), Button::Type::Toggle, "", pCommonStrings->getSmallMuteButton(), true, QSize(), tr("Mute instrument"), @@ -84,7 +86,7 @@ InstrumentLine::InstrumentLine(QWidget* pParent) /*: Text displayed on the button for soloing an instrument. Its size is designed for a single character.*/ - m_pSoloBtn = new Button( this, QSize( 18, height() - 1 ), + m_pSoloBtn = new Button( this, QSize( InstrumentLine::m_nButtonWidth, height() - 1 ), Button::Type::Toggle, "", pCommonStrings->getSmallSoloButton(), false, QSize(), tr("Solo"), @@ -129,45 +131,142 @@ InstrumentLine::InstrumentLine(QWidget* pParent) m_pFunctionPopup->addAction( tr( "Delete instrument" ), this, SLOT( functionDeleteInstrument() ) ); m_pFunctionPopup->setObjectName( "PatternEditorFunctionPopup" ); - m_bIsSelected = true; - setSelected(false); + // Reset the clicked row once the popup is closed by clicking at + // any position other than at an action of the popup. + connect( m_pFunctionPopup, &QMenu::aboutToHide, [=](){ + if ( m_rowSelection == RowSelection::Popup ) { + setRowSelection( RowSelection::None ); + } + }); + + updateStyleSheet(); } +void InstrumentLine::setRowSelection( RowSelection rowSelection ) { + if ( m_rowSelection != rowSelection ) { + m_rowSelection = rowSelection; + update(); + } +} + void InstrumentLine::setName(const QString& sName) { - m_pNameLbl->setText(sName); + if ( m_pNameLbl->text() != sName ){ + m_pNameLbl->setText(sName); + } } -void InstrumentLine::setSelected(bool bSelected) +void InstrumentLine::setSelected( bool bSelected ) { - if (bSelected == m_bIsSelected) { + if ( bSelected == m_bIsSelected ) { return; } + m_bIsSelected = bSelected; - if (m_bIsSelected) { - setPixmap( "/patternEditor/instrument_line_selected.png"); - } - else { - setPixmap( "/patternEditor/instrument_line.png"); + + updateStyleSheet(); + update(); +} + +void InstrumentLine::updateStyleSheet() { + + auto pPref = H2Core::Preferences::get_instance(); + + QColor textColor; + if ( m_bIsSelected ) { + textColor = pPref->getColorTheme()->m_patternEditor_selectedRowTextColor; + } else { + textColor = pPref->getColorTheme()->m_patternEditor_textColor; } + + m_pNameLbl->setStyleSheet( QString( "\ +QLabel {\ + color: %1;\ + font-weight: bold;\ + }" ).arg( textColor.name() ) ); } +void InstrumentLine::enterEvent( QEvent* ev ) { + UNUSED( ev ); + m_bEntered = true; + update(); +} + +void InstrumentLine::leaveEvent( QEvent* ev ) { + UNUSED( ev ); + m_bEntered = false; + update(); +} + +void InstrumentLine::paintEvent( QPaintEvent* ev ) { + auto pPref = Preferences::get_instance(); + auto pHydrogenApp = HydrogenApp::get_instance(); + + QPainter painter(this); + + QColor backgroundColor; + if ( m_bIsSelected ) { + backgroundColor = pPref->getColorTheme()->m_patternEditor_selectedRowColor.darker( 114 ); + } else { + if ( m_nInstrumentNumber == 0 || + m_nInstrumentNumber % 2 == 0 ) { + backgroundColor = pPref->getColorTheme()->m_patternEditor_backgroundColor.darker( 120 ); + } else { + backgroundColor = pPref->getColorTheme()->m_patternEditor_alternateRowColor.darker( 132 ); + } + } + + // Make the background slightly lighter when hovered. + bool bHovered = false; + if ( m_bEntered && m_rowSelection == RowSelection::None ) { + bHovered = true; + } + + Skin::drawListBackground( &painter, QRect( 0, 0, width(), height() ), + backgroundColor, bHovered ); + + // Draw border indicating cursor position + if ( ( m_bIsSelected && pHydrogenApp->getPatternEditorPanel() != nullptr && + pHydrogenApp->getPatternEditorPanel()->getDrumPatternEditor()->hasFocus() && + ! pHydrogenApp->hideKeyboardCursor() ) || + m_rowSelection != RowSelection::None ) { + + QPen pen; + + if ( m_rowSelection != RowSelection::None ) { + // In case a row was right-clicked, highlight it using a border. + pen.setColor( pPref->getColorTheme()->m_highlightColor); + } else { + pen.setColor( pPref->getColorTheme()->m_cursorColor ); + } + + pen.setWidth( 2 ); + painter.setPen( pen ); + painter.setRenderHint( QPainter::Antialiasing ); + painter.drawRoundedRect( QRect( 1, 1, width() - 2 * InstrumentLine::m_nButtonWidth - 1, + height() - 2 ), 4, 4 ); + } +} void InstrumentLine::setNumber(int nIndex) { - m_nInstrumentNumber = nIndex; + if ( m_nInstrumentNumber != nIndex ) { + m_nInstrumentNumber = nIndex; + update(); + } } void InstrumentLine::setMuted(bool isMuted) { - if ( ! m_pMuteBtn->isDown() ) { + if ( ! m_pMuteBtn->isDown() && + m_pMuteBtn->isChecked() != isMuted ) { m_pMuteBtn->setChecked(isMuted); } } @@ -175,7 +274,8 @@ void InstrumentLine::setMuted(bool isMuted) void InstrumentLine::setSoloed( bool soloed ) { - if ( ! m_pSoloBtn->isDown() ) { + if ( ! m_pSoloBtn->isDown() && + m_pSoloBtn->isChecked() != soloed ) { m_pSoloBtn->setChecked( soloed ); } } @@ -241,8 +341,19 @@ void InstrumentLine::mousePressEvent(QMouseEvent *ev) Note *pNote = new Note( pInstr, 0, velocity, fPan, nLength, fPitch); Hydrogen::get_instance()->getAudioEngine()->getSampler()->noteOn(pNote); - } - else if (ev->button() == Qt::RightButton ) { + + } else if (ev->button() == Qt::RightButton ) { + + if ( m_rowSelection == RowSelection::Dialog ) { + // There is still a dialog window opened from the last + // time. It needs to be closed before the popup will + // be shown again. + ERRORLOG( "A dialog is still opened. It needs to be closed first." ); + return; + } + + setRowSelection( RowSelection::Popup ); + m_pFunctionPopup->popup( QPoint( ev->globalX(), ev->globalY() ) ); } @@ -259,7 +370,8 @@ H2Core::Pattern* InstrumentLine::getCurrentPattern() assert( pPatternList != nullptr ); int nSelectedPatternNumber = pHydrogen->getSelectedPatternNumber(); - if ( nSelectedPatternNumber != -1 ) { + if ( nSelectedPatternNumber != -1 && + nSelectedPatternNumber < pPatternList->size() ) { Pattern* pCurrentPattern = pPatternList->get( nSelectedPatternNumber ); return pCurrentPattern; } @@ -276,6 +388,11 @@ void InstrumentLine::functionClearNotes() Pattern *pPattern = getCurrentPattern(); auto pSelectedInstrument = pHydrogen->getSong()->getInstrumentList()->get( m_nInstrumentNumber ); + if ( selectedPatternNr == -1 ) { + // No pattern selected. Nothing to be clear. + return; + } + std::list< Note* > noteList; const Pattern::notes_t* notes = pPattern->get_notes(); FOREACH_NOTE_CST_IT_BEGIN_END(notes,it) { @@ -379,6 +496,10 @@ void InstrumentLine::functionFillEverySixteenNotes(){ functionFillNotes(16); } void InstrumentLine::functionFillNotes( int every ) { Hydrogen *pHydrogen = Hydrogen::get_instance(); + if ( pHydrogen->getSelectedPatternNumber() == -1 ) { + // No pattern selected. Nothing to be filled. + return; + } PatternEditorPanel *pPatternEditorPanel = HydrogenApp::get_instance()->getPatternEditorPanel(); DrumPatternEditor *pPatternEditor = pPatternEditorPanel->getDrumPatternEditor(); @@ -433,6 +554,11 @@ void InstrumentLine::functionRandomizeVelocity() { Hydrogen *pHydrogen = Hydrogen::get_instance(); + if ( pHydrogen->getSelectedPatternNumber() == -1 ) { + // No pattern selected. Nothing to be randomized. + return; + } + PatternEditorPanel *pPatternEditorPanel = HydrogenApp::get_instance()->getPatternEditorPanel(); DrumPatternEditor *pPatternEditor = pPatternEditorPanel->getDrumPatternEditor(); @@ -487,6 +613,7 @@ void InstrumentLine::functionRandomizeVelocity() void InstrumentLine::functionRenameInstrument() { + setRowSelection( RowSelection::Dialog ); // This code is pretty much a duplicate of void InstrumentEditor::labelClicked // in InstrumentEditor.cpp Hydrogen * pHydrogen = Hydrogen::get_instance(); @@ -512,7 +639,8 @@ void InstrumentLine::functionRenameInstrument() { // user entered nothing or pressed Cancel } - + + setRowSelection( RowSelection::None ); } void InstrumentLine::functionDeleteInstrument() @@ -549,6 +677,11 @@ void InstrumentLine::onPreferencesChanged( H2Core::Preferences::Changes changes m_pNameLbl->setFont( QFont( pPref->getLevel2FontFamily(), getPointSize( pPref->getFontSize() ) ) ); } + + if ( changes & H2Core::Preferences::Changes::Colors ) { + updateStyleSheet(); + update(); + } } @@ -557,6 +690,9 @@ void InstrumentLine::onPreferencesChanged( H2Core::Preferences::Changes changes PatternEditorInstrumentList::PatternEditorInstrumentList( QWidget *parent, PatternEditorPanel *pPatternEditorPanel ) : QWidget( parent ) { + + HydrogenApp::get_instance()->addEventListener( this ); + //INFOLOG("INIT"); m_pPattern = nullptr; m_pPatternEditorPanel = pPatternEditorPanel; @@ -564,11 +700,10 @@ PatternEditorInstrumentList::PatternEditorInstrumentList( QWidget *parent, Patte m_nGridHeight = Preferences::get_instance()->getPatternEditorGridHeight(); m_nEditorWidth = 181; - m_nEditorHeight = m_nGridHeight * MAX_INSTRUMENTS; + m_nEditorHeight = m_nGridHeight * MAX_INSTRUMENTS + 1; resize( m_nEditorWidth, m_nEditorHeight ); - setAcceptDrops(true); for ( int i = 0; i < MAX_INSTRUMENTS; ++i) { @@ -608,7 +743,48 @@ InstrumentLine* PatternEditorInstrumentList::createInstrumentLine() return pLine; } +void PatternEditorInstrumentList::updateSongEvent( int nEvent ) { + if ( nEvent == 0 || nEvent == 1 ) { + updateInstrumentLines(); + } +} +void PatternEditorInstrumentList::drumkitLoadedEvent() { + updateInstrumentLines(); +} + +void PatternEditorInstrumentList::repaintInstrumentLines() { + auto pHydrogen = Hydrogen::get_instance(); + auto pSong = pHydrogen->getSong(); + InstrumentList *pInstrList = pSong->getInstrumentList(); + + unsigned nInstruments = pInstrList->size(); + for ( unsigned nInstr = 0; nInstr < MAX_INSTRUMENTS; ++nInstr ) { + if ( nInstr < nInstruments && + m_pInstrumentLine[ nInstr ] != nullptr ) { + m_pInstrumentLine[ nInstr ]->update(); + } + } +} + +void PatternEditorInstrumentList::selectedInstrumentChangedEvent() { + + auto pHydrogen = Hydrogen::get_instance(); + auto pSong = pHydrogen->getSong(); + InstrumentList *pInstrList = pSong->getInstrumentList(); + + unsigned nSelectedInstr = pHydrogen->getSelectedInstrumentNumber(); + + unsigned nInstruments = pInstrList->size(); + for ( unsigned nInstr = 0; nInstr < MAX_INSTRUMENTS; ++nInstr ) { + if ( nInstr < nInstruments && + m_pInstrumentLine[ nInstr ] != nullptr ) { + + InstrumentLine *pLine = m_pInstrumentLine[ nInstr ]; + pLine->setSelected( nInstr == nSelectedInstr ); + } + } +} /// /// Update every InstrumentLine, create or destroy lines if necessary. @@ -630,7 +806,7 @@ void PatternEditorInstrumentList::updateInstrumentLines() delete m_pInstrumentLine[ nInstr ]; m_pInstrumentLine[ nInstr ] = nullptr; - int newHeight = m_nGridHeight * nInstruments; + int newHeight = m_nGridHeight * nInstruments + 1; resize( width(), newHeight ); } @@ -640,7 +816,7 @@ void PatternEditorInstrumentList::updateInstrumentLines() if ( m_pInstrumentLine[ nInstr ] == nullptr ) { // the instrument line doesn't exists..I'll create a new one! m_pInstrumentLine[ nInstr ] = createInstrumentLine(); - m_pInstrumentLine[nInstr]->move( 0, m_nGridHeight * nInstr ); + m_pInstrumentLine[nInstr]->move( 0, m_nGridHeight * nInstr + 1 ); m_pInstrumentLine[nInstr]->show(); int newHeight = m_nGridHeight * nInstruments; @@ -661,7 +837,7 @@ void PatternEditorInstrumentList::updateInstrumentLines() } } - + void PatternEditorInstrumentList::dragEnterEvent(QDragEnterEvent *event) { event->acceptProposedAction(); diff --git a/src/gui/src/PatternEditor/PatternEditorInstrumentList.h b/src/gui/src/PatternEditor/PatternEditorInstrumentList.h index dca099057e..d2d05a2a29 100644 --- a/src/gui/src/PatternEditor/PatternEditorInstrumentList.h +++ b/src/gui/src/PatternEditor/PatternEditorInstrumentList.h @@ -33,8 +33,10 @@ #include #include #include "../Widgets/PixmapWidget.h" +#include "../EventListener.h" #include "../Selection.h" #include "../Widgets/WidgetWithScalableFont.h" +#include "../Widgets/WidgetWithHighlightedList.h" namespace H2Core { @@ -45,7 +47,9 @@ class PatternEditorPanel; class Button; /** \ingroup docGUI*/ -class InstrumentLine : public PixmapWidget, protected WidgetWithScalableFont<8, 10, 12> +class InstrumentLine : public PixmapWidget + , protected WidgetWithScalableFont<8, 10, 12> + , protected WidgetWithHighlightedList { H2_OBJECT(InstrumentLine) Q_OBJECT @@ -60,6 +64,8 @@ class InstrumentLine : public PixmapWidget, protected WidgetWithScalableFont<8, void setSoloed( bool soloed ); void setSamplesMissing( bool bSamplesMissing ); + static constexpr int m_nButtonWidth = 18; + public slots: void onPreferencesChanged( H2Core::Preferences::Changes changes ); @@ -102,12 +108,23 @@ public slots: Button *m_pSampleWarning; virtual void mousePressEvent(QMouseEvent *ev) override; + virtual void enterEvent( QEvent *ev ); + virtual void leaveEvent( QEvent *ev ); + virtual void paintEvent( QPaintEvent* ev ) override; H2Core::Pattern* getCurrentPattern(); + + void updateStyleSheet(); + void setRowSelection( RowSelection rowSelection ); + + /** Whether the cursor entered the boundary of the widget.*/ + bool m_bEntered; }; /** \ingroup docGUI*/ -class PatternEditorInstrumentList : public QWidget, public H2Core::Object { +class PatternEditorInstrumentList : public QWidget, + public EventListener, + public H2Core::Object { H2_OBJECT(PatternEditorInstrumentList) Q_OBJECT @@ -122,7 +139,11 @@ class PatternEditorInstrumentList : public QWidget, public H2Core::ObjectsetHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); m_pPianoRollEditor = new PianoRollEditor( m_pPianoRollScrollView->viewport(), this, m_pPianoRollScrollView ); m_pPianoRollScrollView->setWidget( m_pPianoRollEditor ); - connect( m_pPianoRollScrollView->horizontalScrollBar(), SIGNAL( valueChanged(int) ), this, SLOT( on_patternEditorHScroll(int) ) ); + connect( m_pPianoRollScrollView->horizontalScrollBar(), SIGNAL( valueChanged(int) ), + this, SLOT( on_patternEditorHScroll(int) ) ); + connect( m_pPianoRollScrollView->horizontalScrollBar(), SIGNAL( valueChanged(int) ), + m_pPianoRollEditor, SLOT( scrolled( int ) ) ); connect( m_pPianoRollScrollView->verticalScrollBar(), SIGNAL( valueChanged( int ) ), m_pPianoRollEditor, SLOT( scrolled( int ) ) ); connect( HydrogenApp::get_instance(), &HydrogenApp::preferencesChanged, @@ -344,7 +347,7 @@ PatternEditorPanel::PatternEditorPanel( QWidget *pParent ) m_pNoteVelocityScrollView->setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); m_pNoteVelocityScrollView->setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); m_pNoteVelocityEditor = new NotePropertiesRuler( m_pNoteVelocityScrollView->viewport(), this, - NotePropertiesRuler::VELOCITY ); + NotePropertiesRuler::Mode::Velocity ); m_pNoteVelocityScrollView->setWidget( m_pNoteVelocityEditor ); m_pNoteVelocityScrollView->setFixedHeight( 100 ); connect( m_pNoteVelocityScrollView->horizontalScrollBar(), SIGNAL( valueChanged(int) ), this, SLOT( on_patternEditorHScroll(int) ) ); @@ -363,7 +366,8 @@ PatternEditorPanel::PatternEditorPanel( QWidget *pParent ) m_pNotePanScrollView->setFrameShape( QFrame::NoFrame ); m_pNotePanScrollView->setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); m_pNotePanScrollView->setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); - m_pNotePanEditor = new NotePropertiesRuler( m_pNotePanScrollView->viewport(), this, NotePropertiesRuler::PAN ); + m_pNotePanEditor = new NotePropertiesRuler( m_pNotePanScrollView->viewport(), this, + NotePropertiesRuler::Mode::Pan ); m_pNotePanScrollView->setWidget( m_pNotePanEditor ); m_pNotePanScrollView->setFixedHeight( 100 ); @@ -385,7 +389,7 @@ PatternEditorPanel::PatternEditorPanel( QWidget *pParent ) m_pNoteLeadLagScrollView->setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); m_pNoteLeadLagScrollView->setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); m_pNoteLeadLagEditor = new NotePropertiesRuler( m_pNoteLeadLagScrollView->viewport(), this, - NotePropertiesRuler::LEADLAG ); + NotePropertiesRuler::Mode::LeadLag ); m_pNoteLeadLagScrollView->setWidget( m_pNoteLeadLagEditor ); m_pNoteLeadLagScrollView->setFixedHeight( 100 ); @@ -409,7 +413,7 @@ PatternEditorPanel::PatternEditorPanel( QWidget *pParent ) m_pNoteNoteKeyScrollView->setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); m_pNoteNoteKeyScrollView->setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); m_pNoteNoteKeyEditor = new NotePropertiesRuler( m_pNoteNoteKeyScrollView->viewport(), this, - NotePropertiesRuler::NOTEKEY ); + NotePropertiesRuler::Mode::NoteKey ); m_pNoteNoteKeyScrollView->setWidget( m_pNoteNoteKeyEditor ); m_pNoteNoteKeyScrollView->setFixedHeight( 210 ); connect( m_pNoteNoteKeyScrollView->horizontalScrollBar(), SIGNAL( valueChanged( int ) ), @@ -431,7 +435,7 @@ PatternEditorPanel::PatternEditorPanel( QWidget *pParent ) m_pNoteProbabilityScrollView->setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); m_pNoteProbabilityScrollView->setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff ); m_pNoteProbabilityEditor = new NotePropertiesRuler( m_pNoteProbabilityScrollView->viewport(), this, - NotePropertiesRuler::PROBABILITY ); + NotePropertiesRuler::Mode::Probability ); m_pNoteProbabilityScrollView->setWidget( m_pNoteProbabilityEditor ); m_pNoteProbabilityScrollView->setFixedHeight( 100 ); connect( m_pNoteProbabilityScrollView->horizontalScrollBar(), SIGNAL( valueChanged(int) ), @@ -699,7 +703,8 @@ void PatternEditorPanel::selectedPatternChangedEvent() PatternList *pPatternList = Hydrogen::get_instance()->getSong()->getPatternList(); int nSelectedPatternNumber = Hydrogen::get_instance()->getSelectedPatternNumber(); - if ( ( nSelectedPatternNumber != -1 ) && ( (uint) nSelectedPatternNumber < pPatternList->size() ) ) { + if ( ( nSelectedPatternNumber != -1 ) && + ( (uint) nSelectedPatternNumber < pPatternList->size() ) ) { // update pattern name text m_pPattern = pPatternList->get( nSelectedPatternNumber ); QString sCurrentPatternName = m_pPattern->get_name(); @@ -708,12 +713,13 @@ void PatternEditorPanel::selectedPatternChangedEvent() // update pattern size LCD updatePatternSizeLCD(); + updateEditors(); } else { m_pPattern = nullptr; - this->setWindowTitle( ( tr( "Pattern editor - %1" ).arg(QString( "No pattern selected." ) ) ) ); + this->setWindowTitle( tr( "Pattern editor - No pattern selected" ) ); m_pPatternNameLbl->setText( tr( "No pattern selected" ) ); } @@ -914,19 +920,23 @@ void PatternEditorPanel::updateEditors( bool bPatternOnly ) { m_pDrumPatternEditor->updateEditor(); } +void PatternEditorPanel::patternModifiedEvent() { + selectedPatternChangedEvent(); +} -void PatternEditorPanel::patternLengthChanged() -{ - // INFOLOG( QString("idx %1 -> %2 eighth").arg( nSelected ).arg( ( MAX_NOTES / 8 ) * ( nSelected + 1 ) ) ); +void PatternEditorPanel::patternChangedEvent() { + updateEditors( true ); +} - if ( !m_pPattern ) { - return; +void PatternEditorPanel::songModeActivationEvent( int ) { + if ( Hydrogen::get_instance()->getPatternMode() == + Song::PatternMode::Stacked ) { + updateEditors( true ); } +} - updateEditors(); - resizeEvent( nullptr ); - - EventQueue::get_instance()->push_event( EVENT_SELECTED_PATTERN_CHANGED, -1 ); +void PatternEditorPanel::stackedModeActivationEvent( int ) { + updateEditors( true ); } void PatternEditorPanel::updatePatternSizeLCD() { @@ -936,13 +946,10 @@ void PatternEditorPanel::updatePatternSizeLCD() { m_bArmPatternSizeSpinBoxes = false; - bool bChanged = false; - double fNewDenominator = static_cast( m_pPattern->get_denominator() ); if ( fNewDenominator != m_pLCDSpinBoxDenominator->value() && ! m_pLCDSpinBoxDenominator->hasFocus() ) { m_pLCDSpinBoxDenominator->setValue( fNewDenominator ); - bChanged = true; // Update numerator to allow only for a maximum pattern length of // four measures. @@ -952,19 +959,17 @@ void PatternEditorPanel::updatePatternSizeLCD() { double fNewNumerator = static_cast( m_pPattern->get_length() * m_pPattern->get_denominator() ) / static_cast( MAX_NOTES ); if ( fNewNumerator != m_pLCDSpinBoxNumerator->value() && ! m_pLCDSpinBoxNumerator->hasFocus() ) { m_pLCDSpinBoxNumerator->setValue( fNewNumerator ); - bChanged = true; } m_bArmPatternSizeSpinBoxes = true; - - if ( bChanged ) { - patternLengthChanged(); - } - } void PatternEditorPanel::patternSizeChanged( double fValue ){ + if ( m_pPattern == nullptr ) { + return; + } + if ( ! m_bArmPatternSizeSpinBoxes ) { // Don't execute this function if the values of the spin boxes // have been set by Hydrogen instead of by the user. @@ -1000,7 +1005,7 @@ void PatternEditorPanel::patternSizeChanged( double fValue ){ pHydrogen->setIsModified( true ); - patternLengthChanged(); + EventQueue::get_instance()->push_event( EVENT_PATTERN_MODIFIED, -1 ); } void PatternEditorPanel::dragEnterEvent( QDragEnterEvent *event ) @@ -1120,6 +1125,10 @@ int PatternEditorPanel::moveCursorLeft( int n ) int PatternEditorPanel::moveCursorRight( int n ) { + if ( m_pPattern == nullptr ) { + return 0; + } + m_nCursorPosition = std::min( m_nCursorPosition + m_nCursorIncrement * n, m_pPattern->get_length() - m_nCursorIncrement ); @@ -1190,3 +1199,31 @@ void PatternEditorPanel::switchPatternSizeFocus() { m_pLCDSpinBoxNumerator->setFocus(); } } + +NotePropertiesRuler::Mode PatternEditorPanel::getNotePropertiesMode() const +{ + NotePropertiesRuler::Mode mode; + + switch ( m_pPropertiesCombo->currentIndex() ) { + case 0: + mode = NotePropertiesRuler::Mode::Velocity; + break; + case 1: + mode = NotePropertiesRuler::Mode::Pan; + break; + case 2: + mode = NotePropertiesRuler::Mode::LeadLag; + break; + case 3: + mode = NotePropertiesRuler::Mode::NoteKey; + break; + case 4: + mode = NotePropertiesRuler::Mode::Probability; + break; + default: + ERRORLOG( QString( "Unsupported m_pPropertiesCombo index [%1]" ) + .arg( m_pPropertiesCombo->currentIndex() ) ); + } + + return mode; +} diff --git a/src/gui/src/PatternEditor/PatternEditorPanel.h b/src/gui/src/PatternEditor/PatternEditorPanel.h index 809affcb41..745e3b20ab 100644 --- a/src/gui/src/PatternEditor/PatternEditorPanel.h +++ b/src/gui/src/PatternEditor/PatternEditorPanel.h @@ -83,7 +83,7 @@ class PatternEditorPanel : public QWidget, protected WidgetWithScalableFont<8, const QScrollArea* getNoteProbabilityScrollArea() const { return m_pNoteProbabilityScrollView; } const QScrollBar* getVerticalScrollBar() const { return m_pPatternEditorVScrollBar; } const QScrollBar* getHorizontalScrollBar() const { return m_pPatternEditorHScrollBar; } - int getPropertiesComboValue(){ return m_pPropertiesCombo->currentIndex(); } + NotePropertiesRuler::Mode getNotePropertiesMode() const; void updateSLnameLabel(); @@ -92,8 +92,12 @@ class PatternEditorPanel : public QWidget, protected WidgetWithScalableFont<8, // Implements EventListener interface virtual void selectedPatternChangedEvent() override; virtual void selectedInstrumentChangedEvent() override; + virtual void patternModifiedEvent() override; + virtual void patternChangedEvent() override; virtual void drumkitLoadedEvent() override; virtual void updateSongEvent( int nValue ) override; + virtual void songModeActivationEvent( int ) override; + virtual void stackedModeActivationEvent( int ) override; //~ Implements EventListener interface void ensureCursorVisible(); @@ -114,7 +118,6 @@ class PatternEditorPanel : public QWidget, protected WidgetWithScalableFont<8, private slots: void gridResolutionChanged( int nSelected ); void propertiesComboChanged( int nSelected ); - void patternLengthChanged(); /** Batch version for setting the values of the pattern size spin boxes.*/ void updatePatternSizeLCD(); diff --git a/src/gui/src/PatternEditor/PatternEditorRuler.cpp b/src/gui/src/PatternEditor/PatternEditorRuler.cpp index c323cbe24d..ae1f8efb02 100644 --- a/src/gui/src/PatternEditor/PatternEditorRuler.cpp +++ b/src/gui/src/PatternEditor/PatternEditorRuler.cpp @@ -31,17 +31,21 @@ using namespace H2Core; #include #include - +#include "DrumPatternEditor.h" #include "PatternEditorRuler.h" #include "PatternEditorPanel.h" +#include "NotePropertiesRuler.h" #include "../HydrogenApp.h" #include "../Skin.h" PatternEditorRuler::PatternEditorRuler( QWidget* parent ) - : QWidget( parent ) + : QWidget( parent ) + , m_nHoveredColumn( -1 ) + , m_nTick( -1 ) { setAttribute(Qt::WA_OpaquePaintEvent); + setMouseTracking( true ); //infoLog( "INIT" ); @@ -52,23 +56,26 @@ PatternEditorRuler::PatternEditorRuler( QWidget* parent ) m_pPattern = nullptr; m_fGridWidth = Preferences::get_instance()->getPatternEditorGridWidth(); - m_nRulerWidth = 20 + m_fGridWidth * ( MAX_NOTES * 4 ); + m_nRulerWidth = PatternEditor::nMargin + m_fGridWidth * ( MAX_NOTES * 4 ); m_nRulerHeight = 25; - - m_nTicks = 0; resize( m_nRulerWidth, m_nRulerHeight ); - bool ok = m_tickPosition.load( Skin::getImagePath() + "/patternEditor/tickPosition.png" ); - if( ok == false ){ - ERRORLOG( "Error loading pixmap " ); - } - - m_pBackground = nullptr; - createBackground(); + qreal pixelRatio = devicePixelRatio(); + m_pBackgroundPixmap = new QPixmap( m_nRulerWidth * pixelRatio, + m_nRulerHeight * pixelRatio ); + m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio ); m_pTimer = new QTimer(this); - connect(m_pTimer, SIGNAL(timeout()), this, SLOT(updateEditor())); + connect(m_pTimer, &QTimer::timeout, [=]() { + if ( H2Core::Hydrogen::get_instance()->getAudioEngine()->getState() == + H2Core::AudioEngine::State::Playing ) { + updatePosition(); + } + }); + + // Will set the active width and calls createBackground. + updateActiveRange(); HydrogenApp::get_instance()->addEventListener( this ); } @@ -79,7 +86,69 @@ PatternEditorRuler::~PatternEditorRuler() { //infoLog( "DESTROY"); } +void PatternEditorRuler::updatePosition( bool bForce ) { + + auto pHydrogen = Hydrogen::get_instance(); + auto pAudioEngine = pHydrogen->getAudioEngine(); + + bool bIsSelectedPatternPlaying = false; // is the pattern playing now? + + if ( pHydrogen->getMode() == Song::Mode::Song && + pHydrogen->isPatternEditorLocked() ) { + // In case the pattern editor is locked we will always display + // the position tick. Even if no pattern is set at all. + bIsSelectedPatternPlaying = true; + } else { + /* + * Lock audio engine to make sure pattern list does not get + * modified / cleared during iteration + */ + pAudioEngine->lock( RIGHT_HERE ); + + auto pList = pAudioEngine->getPlayingPatterns(); + for (uint i = 0; i < pList->size(); i++) { + if ( m_pPattern == pList->get(i) ) { + bIsSelectedPatternPlaying = true; + break; + } + } + + pAudioEngine->unlock(); + } + + int nTick = pAudioEngine->getPatternTickPosition(); + + if ( nTick != m_nTick || bForce ) { + m_nTick = nTick; + update(); + + if ( ! bIsSelectedPatternPlaying ) { + nTick = -1; + } + + auto pPatternEditorPanel = HydrogenApp::get_instance()->getPatternEditorPanel(); + if ( pPatternEditorPanel != nullptr ) { + pPatternEditorPanel->getDrumPatternEditor()->updatePosition( nTick ); + pPatternEditorPanel->getPianoRollEditor()->updatePosition( nTick ); + pPatternEditorPanel->getVelocityEditor()->updatePosition( nTick ); + pPatternEditorPanel->getPanEditor()->updatePosition( nTick ); + pPatternEditorPanel->getLeadLagEditor()->updatePosition( nTick ); + pPatternEditorPanel->getNoteKeyEditor()->updatePosition( nTick ); + pPatternEditorPanel->getProbabilityEditor()->updatePosition( nTick ); + } + } +} + +void PatternEditorRuler::relocationEvent() { + updatePosition(); +} +void PatternEditorRuler::updateSongEvent( int nValue ) { + + if ( nValue == 0 ) { // new song loaded + updatePosition(); + } +} void PatternEditorRuler::updateStart(bool start) { if (start) { @@ -92,14 +161,20 @@ void PatternEditorRuler::updateStart(bool start) { -void PatternEditorRuler::showEvent ( QShowEvent *ev ) +void PatternEditorRuler::showEvent( QShowEvent *ev ) { UNUSED( ev ); - updateEditor(); + updatePosition(); updateStart(true); } +void PatternEditorRuler::leaveEvent( QEvent* ev ){ + m_nHoveredColumn = -1; + update(); + + QWidget::leaveEvent( ev ); +} void PatternEditorRuler::hideEvent ( QHideEvent *ev ) { @@ -107,12 +182,100 @@ void PatternEditorRuler::hideEvent ( QHideEvent *ev ) updateStart(false); } +void PatternEditorRuler::mousePressEvent( QMouseEvent* ev ) { + if ( ev->button() == Qt::LeftButton && + ev->x() < m_nWidthActive ) { + auto pHydrogen = Hydrogen::get_instance(); + auto pCoreActionController = pHydrogen->getCoreActionController(); + auto pHydrogenApp = HydrogenApp::get_instance(); + DrumPatternEditor* pDrumPatternEditor; + if ( pHydrogenApp->getPatternEditorPanel() != nullptr ) { + pDrumPatternEditor = pHydrogenApp->getPatternEditorPanel()->getDrumPatternEditor(); + } else { + pDrumPatternEditor = nullptr; + } + + // Fall back to default values in case the GUI is starting and the + // pattern editor is not ready yet. + float fResolution; + bool bIsUsingTriplets; + if ( pDrumPatternEditor != nullptr ) { + fResolution = static_cast(pDrumPatternEditor->getResolution()); + bIsUsingTriplets = pDrumPatternEditor->isUsingTriplets(); + } else { + fResolution = 8; + bIsUsingTriplets = false; + } + + float fTripletFactor; + if ( bIsUsingTriplets ) { + fTripletFactor = 3; + } else { + fTripletFactor = 4; + } + + long nNewTick = std::floor( static_cast(m_nHoveredColumn) * + 4 * static_cast(MAX_NOTES) / + ( fTripletFactor * fResolution ) ); + + if ( pHydrogen->getMode() != Song::Mode::Pattern ) { + pCoreActionController->activateSongMode( false ); + } + + pCoreActionController->locateToTick( nNewTick ); + } +} + +void PatternEditorRuler::mouseMoveEvent( QMouseEvent* ev ) { + + if ( ev->x() < m_nWidthActive ) { + + auto pHydrogenApp = HydrogenApp::get_instance(); + DrumPatternEditor* pDrumPatternEditor; + if ( pHydrogenApp->getPatternEditorPanel() != nullptr ) { + pDrumPatternEditor = pHydrogenApp->getPatternEditorPanel()->getDrumPatternEditor(); + } else { + pDrumPatternEditor = nullptr; + } + + // Fall back to default values in case the GUI is starting and the + // pattern editor is not ready yet. + float fResolution; + bool bIsUsingTriplets; + if ( pDrumPatternEditor != nullptr ) { + fResolution = static_cast(pDrumPatternEditor->getResolution()); + bIsUsingTriplets = pDrumPatternEditor->isUsingTriplets(); + } else { + fResolution = 8; + bIsUsingTriplets = false; + } + + float fTripletFactor; + if ( bIsUsingTriplets ) { + fTripletFactor = 3; + } else { + fTripletFactor = 4; + } + + float fColumnWidth = fTripletFactor * fResolution / + ( 4 * static_cast(MAX_NOTES) * m_fGridWidth ); + + int nHoveredColumn = + static_cast(std::floor( static_cast( + std::max( ev->x() - PatternEditor::nMargin + + static_cast(std::round(1 / fColumnWidth / 2) ), 0 )) * + fColumnWidth )); + + if ( nHoveredColumn != m_nHoveredColumn ) { + m_nHoveredColumn = nHoveredColumn; + update(); + } + } +} void PatternEditorRuler::updateEditor( bool bRedrawAll ) { - static int oldNTicks = 0; - Hydrogen *pHydrogen = Hydrogen::get_instance(); auto pAudioEngine = pHydrogen->getAudioEngine(); @@ -130,42 +293,12 @@ void PatternEditorRuler::updateEditor( bool bRedrawAll ) else { m_pPattern = nullptr; } + updateActiveRange(); - - bool bActive = false; // is the pattern playing now? - - /* - * Lock audio engine to make sure pattern list does not get - * modified / cleared during iteration - */ - pAudioEngine->lock( RIGHT_HERE ); - - auto pPlayingPatterns = pAudioEngine->getPlayingPatterns(); - for ( int ii = 0; ii < pPlayingPatterns->size(); ++ii ) { - auto ppattern = pPlayingPatterns->get( ii ); - if ( m_pPattern == ppattern ) { - bActive = true; - break; - } - } - - pAudioEngine->unlock(); - - if ( ( pAudioEngine->getState() == H2Core::AudioEngine::State::Playing ) && bActive ) { - m_nTicks = pAudioEngine->getPatternTickPosition(); - } - else { - m_nTicks = -1; // hide the tickPosition - } - - - if (oldNTicks != m_nTicks) { - // redraw all - bRedrawAll = true; - } - oldNTicks = m_nTicks; - + updatePosition(); + if (bRedrawAll) { + createBackground(); update( 0, 0, width(), height() ); } } @@ -173,84 +306,217 @@ void PatternEditorRuler::updateEditor( bool bRedrawAll ) void PatternEditorRuler::createBackground() { - auto pPref = H2Core::Preferences::get_instance(); - - if ( m_pBackground ) { - delete m_pBackground; + auto pHydrogenApp = HydrogenApp::get_instance(); + DrumPatternEditor* pDrumPatternEditor; + if ( pHydrogenApp->getPatternEditorPanel() != nullptr ) { + pDrumPatternEditor = pHydrogenApp->getPatternEditorPanel()->getDrumPatternEditor(); + } else { + pDrumPatternEditor = nullptr; } + auto pPref = H2Core::Preferences::get_instance(); - // Create new background pixmap at native device pixelratio + // Resize pixmap if pixel ratio has changed qreal pixelRatio = devicePixelRatio(); - m_pBackground = new QPixmap( pixelRatio * QSize( m_nRulerWidth, m_nRulerHeight ) ); - m_pBackground->setDevicePixelRatio( pixelRatio ); + if ( m_pBackgroundPixmap->width() != m_nRulerWidth || + m_pBackgroundPixmap->height() != m_nRulerHeight || + m_pBackgroundPixmap->devicePixelRatio() != pixelRatio ) { + delete m_pBackgroundPixmap; + m_pBackgroundPixmap = new QPixmap( width() * pixelRatio , height() * pixelRatio ); + m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio ); + } - QColor backgroundColor( pPref->getColorTheme()->m_patternEditor_backgroundColor ); - m_pBackground->fill( backgroundColor ); + QColor backgroundColor( pPref->getColorTheme()->m_patternEditor_alternateRowColor.darker( 120 ) ); + QColor textColor = pPref->getColorTheme()->m_patternEditor_textColor; + textColor.setAlpha( 220 ); + + QColor lineColor = pPref->getColorTheme()->m_patternEditor_lineColor; - QPainter painter( m_pBackground ); + QPainter painter( m_pBackgroundPixmap ); + + painter.fillRect( QRect( 0, 0, width(), height() ), backgroundColor ); // gray background for unusable section of pattern - if (m_pPattern) { - int nXStart = 20 + m_pPattern->get_length() * m_fGridWidth; - if ( (m_nRulerWidth - nXStart) != 0 ) { - painter.fillRect( nXStart, 0, m_nRulerWidth - nXStart, m_nRulerHeight, QColor(170,170,170) ); - } + if ( m_nRulerWidth - m_nWidthActive != 0 ) { + painter.fillRect( m_nWidthActive, 0, m_nRulerWidth - m_nWidthActive, + m_nRulerHeight, + pPref->getColorTheme()->m_midLightColor ); } // numbers - QColor textColor( 100, 100, 100 ); - QColor lineColor( 170, 170, 170 ); QFont font( pPref->getApplicationFontFamily(), getPointSize( pPref->getFontSize() ) ); painter.setFont(font); - painter.drawLine( 0, 0, m_nRulerWidth, 0 ); - painter.drawLine( 0, m_nRulerHeight - 1, m_nRulerWidth - 1, m_nRulerHeight - 1); uint nQuarter = 48; - for ( int i = 0; i < 64 ; i++ ) { - int nText_x = 20 + nQuarter / 4 * i * m_fGridWidth; - if ( ( i % 4 ) == 0 ) { - painter.setPen( textColor ); - painter.drawText( nText_x - 30, 0, 60, m_nRulerHeight, Qt::AlignCenter, QString("%1").arg(i / 4 + 1) ); - //ERRORLOG(QString("nText_x: %1, true, : %2").arg(nText_x).arg(m_nRulerWidth)); - } - else { - painter.setPen( QPen( QColor( lineColor ), 1, Qt::SolidLine ) ); - painter.drawLine( nText_x, ( m_nRulerHeight - 5 ) / 2, nText_x, m_nRulerHeight - ( (m_nRulerHeight - 5 ) / 2 )); - //ERRORLOG("PAINT LINE"); - } + // Fall back to default values in case the GUI is starting and the + // pattern editor is not ready yet. + int nResolution; + bool bIsUsingTriplets; + if ( pDrumPatternEditor != nullptr ) { + nResolution = pDrumPatternEditor->getResolution(); + bIsUsingTriplets = pDrumPatternEditor->isUsingTriplets(); + } else { + nResolution = 8; + bIsUsingTriplets = false; + } + + // Draw numbers and quarter ticks + painter.setPen( textColor ); + for ( int ii = 0; ii < 64 ; ii += 4 ) { + int nText_x = PatternEditor::nMargin + nQuarter / 4 * ii * m_fGridWidth; + painter.drawLine( nText_x, height() - 13, nText_x, height() - 1 ); + painter.drawText( nText_x + 3, 0, 60, m_nRulerHeight, + Qt::AlignVCenter | Qt::AlignLeft, + QString("%1").arg(ii / 4 + 1) ); + } + + // Draw remaining ticks + float fStep; + if ( bIsUsingTriplets ) { + fStep = 4 * MAX_NOTES / ( 3 * nResolution ) * m_fGridWidth; + } else { + fStep = 4 * MAX_NOTES / ( 4 * nResolution ) * m_fGridWidth; } + for ( float xx = PatternEditor::nMargin; xx < m_nWidthActive; xx += fStep ) { + painter.drawLine( xx, height() - 5, xx, height() - 1 ); + } + + painter.setPen( QPen( lineColor, 2, Qt::SolidLine ) ); + painter.drawLine( 0, m_nRulerHeight, m_nRulerWidth, m_nRulerHeight); + painter.drawLine( m_nRulerWidth, 0, m_nRulerWidth, m_nRulerHeight ); } void PatternEditorRuler::paintEvent( QPaintEvent *ev) { + auto pPref = H2Core::Preferences::get_instance(); + auto pHydrogenApp = HydrogenApp::get_instance(); + DrumPatternEditor* pDrumPatternEditor; + if ( pHydrogenApp->getPatternEditorPanel() != nullptr ) { + pDrumPatternEditor = pHydrogenApp->getPatternEditorPanel()->getDrumPatternEditor(); + } else { + pDrumPatternEditor = nullptr; + } + if (!isVisible()) { return; } qreal pixelRatio = devicePixelRatio(); - if ( pixelRatio != m_pBackground->devicePixelRatio() ) { + if ( pixelRatio != m_pBackgroundPixmap->devicePixelRatio() ) { createBackground(); } QPainter painter(this); - painter.drawPixmap( ev->rect(), *m_pBackground, QRectF( pixelRatio * ev->rect().x(), + painter.drawPixmap( ev->rect(), *m_pBackgroundPixmap, QRectF( pixelRatio * ev->rect().x(), pixelRatio * ev->rect().y(), pixelRatio * ev->rect().width(), pixelRatio * ev->rect().height() ) ); - // draw tickPosition - if (m_nTicks != -1) { - uint x = (uint)( 20 + m_nTicks * m_fGridWidth - 5 - 11 / 2.0 ); - painter.drawPixmap( QRect( x, height() / 2, 11, 8 ), m_tickPosition, QRect( 0, 0, 11, 8 ) ); + // draw cursor + if ( pHydrogenApp->getPatternEditorPanel() != nullptr && + ( pDrumPatternEditor->hasFocus() || + pHydrogenApp->getPatternEditorPanel()->getVelocityEditor()->hasFocus() || + pHydrogenApp->getPatternEditorPanel()->getPanEditor()->hasFocus() || + pHydrogenApp->getPatternEditorPanel()->getLeadLagEditor()->hasFocus() || + pHydrogenApp->getPatternEditorPanel()->getNoteKeyEditor()->hasFocus() || + pHydrogenApp->getPatternEditorPanel()->getProbabilityEditor()->hasFocus() || + pHydrogenApp->getPatternEditorPanel()->getPianoRollEditor()->hasFocus() ) && + ! pHydrogenApp->hideKeyboardCursor() ) { + + int nCursorX = m_fGridWidth * + pHydrogenApp->getPatternEditorPanel()->getCursorPosition() + + PatternEditor::nMargin - 4 - + m_fGridWidth * 5; + + // Middle line to indicate the selected tick + QColor cursorColor( pPref->getColorTheme()->m_cursorColor ); + painter.setPen( cursorColor ); + painter.drawLine( nCursorX + m_fGridWidth * 5 + 4, height() - 6, + nCursorX + m_fGridWidth * 5 + 4, height() - 13 ); + + QPen pen( cursorColor ); + pen.setWidth( 2 ); + painter.setPen( pen ); + painter.setRenderHint( QPainter::Antialiasing ); + painter.drawLine( nCursorX, 3, nCursorX + m_fGridWidth * 10 + 8, 3 ); + painter.drawLine( nCursorX, 4, nCursorX, 5 ); + painter.drawLine( nCursorX + m_fGridWidth * 10 + 8, 4, + nCursorX + m_fGridWidth * 10 + 8, 5 ); + painter.drawLine( nCursorX, height() - 5, + nCursorX + m_fGridWidth * 10 + 8, height() - 5 ); + painter.drawLine( nCursorX, height() - 7, + nCursorX, height() - 6 ); + painter.drawLine( nCursorX + m_fGridWidth * 10 + 8, height() - 6, + nCursorX + m_fGridWidth * 10 + 8, height() - 7 ); + } + + // Fall back to default values in case the GUI is starting and the + // pattern editor is not ready yet. + float fResolution; + bool bIsUsingTriplets; + if ( pDrumPatternEditor != nullptr ) { + fResolution = static_cast(pDrumPatternEditor->getResolution()); + bIsUsingTriplets = pDrumPatternEditor->isUsingTriplets(); + } else { + fResolution = 8; + bIsUsingTriplets = false; + } + + float fTripletFactor; + if ( bIsUsingTriplets ) { + fTripletFactor = 3; + } else { + fTripletFactor = 4; + } + + // draw playhead + if ( m_nTick != -1 ) { + int nOffset = Skin::getPlayheadShaftOffset(); + int x = static_cast(static_cast(PatternEditor::nMargin) + + static_cast(m_nTick) * + m_fGridWidth); + + Skin::drawPlayhead( &painter, x - nOffset, 3, false ); + painter.drawLine( x, 8, x, height() - 1 ); + } + + // Display playhead on hovering + if ( m_nHoveredColumn > -1 ) { + int x = PatternEditor::nMargin + + static_cast(m_nHoveredColumn * 4 * static_cast(MAX_NOTES) / + ( fTripletFactor * fResolution ) * m_fGridWidth); + + if ( x < m_nWidthActive ) { + int nOffset = Skin::getPlayheadShaftOffset(); + Skin::drawPlayhead( &painter, x - nOffset, 3, true ); + painter.drawLine( x, 8, x, height() - 1 ); + } } } +void PatternEditorRuler::updateActiveRange() { + + auto pAudioEngine = H2Core::Hydrogen::get_instance()->getAudioEngine(); + int nTicksInPattern = MAX_NOTES; + auto pPlayingPatterns = pAudioEngine->getPlayingPatterns(); + if ( pPlayingPatterns->size() != 0 ) { + nTicksInPattern = pPlayingPatterns->longest_pattern_length(); + } + + int nWidthActive = PatternEditor::nMargin + nTicksInPattern * m_fGridWidth; + + if ( m_nWidthActive != nWidthActive ) { + m_nWidthActive = nWidthActive; + + createBackground(); + update(); + } +} void PatternEditorRuler::zoomIn() { @@ -259,8 +525,11 @@ void PatternEditorRuler::zoomIn() } else { m_fGridWidth *= 1.5; } - m_nRulerWidth = 20 + m_fGridWidth * ( MAX_NOTES * 4 ); + m_nRulerWidth = PatternEditor::nMargin + m_fGridWidth * ( MAX_NOTES * 4 ); resize( QSize(m_nRulerWidth, m_nRulerHeight )); + + updateActiveRange(); + createBackground(); update(); } @@ -274,14 +543,27 @@ void PatternEditorRuler::zoomOut() } else { m_fGridWidth /= 1.5; } - m_nRulerWidth = 20 + m_fGridWidth * ( MAX_NOTES * 4 ); + m_nRulerWidth = PatternEditor::nMargin + m_fGridWidth * ( MAX_NOTES * 4 ); resize( QSize(m_nRulerWidth, m_nRulerHeight) ); + + updateActiveRange(); + createBackground(); update(); } } +void PatternEditorRuler::songModeActivationEvent( int ) +{ + updatePosition(); +} + +void PatternEditorRuler::stateChangedEvent( H2Core::AudioEngine::State ) +{ + updatePosition(); +} + void PatternEditorRuler::selectedPatternChangedEvent() { createBackground(); diff --git a/src/gui/src/PatternEditor/PatternEditorRuler.h b/src/gui/src/PatternEditor/PatternEditorRuler.h index fb66d65572..585c61db4b 100644 --- a/src/gui/src/PatternEditor/PatternEditorRuler.h +++ b/src/gui/src/PatternEditor/PatternEditorRuler.h @@ -53,9 +53,25 @@ class PatternEditorRuler : public QWidget, protected WidgetWithScalableFont<8, void paintEvent(QPaintEvent *ev) override; void updateStart(bool start); + /** + * Queries the audio engine to update the current position of the + * playhead. + * + * \param bForce The transport position is cached and updates in + * the transport position are only propagated to the other member + * of the PatternEditor once it changes. However, this will leave the + * pattern editor in a dirty state during startup since the ruler + * has to wait for all other associated objects being + * constructed. Using the @a bForce option an update is performed + * regardlessly. + */ + void updatePosition( bool bForce = false ); void showEvent( QShowEvent *ev ) override; void hideEvent( QHideEvent *ev ) override; + void mouseMoveEvent( QMouseEvent *ev ) override; + void mousePressEvent( QMouseEvent *ev ) override; + void leaveEvent( QEvent *ev ) override; void zoomIn(); void zoomOut(); @@ -74,15 +90,28 @@ class PatternEditorRuler : public QWidget, protected WidgetWithScalableFont<8, uint m_nRulerHeight; float m_fGridWidth; - QPixmap *m_pBackground; - QPixmap m_tickPosition; + QPixmap *m_pBackgroundPixmap; QTimer *m_pTimer; - int m_nTicks; + int m_nTick; H2Core::Pattern *m_pPattern; + int m_nHoveredColumn; + /** + * Length of the song in pixels. As soon as the x coordinate of an + * event is smaller than this value, it lies within the active + * range of the song. + */ + int m_nWidthActive; + /** Updates #m_nWidthActive.*/ + void updateActiveRange(); + // Implements EventListener interface virtual void selectedPatternChangedEvent() override; + virtual void stateChangedEvent( H2Core::AudioEngine::State ) override; + virtual void songModeActivationEvent( int ) override; + virtual void relocationEvent() override; + virtual void updateSongEvent( int ) override; //~ Implements EventListener interface }; diff --git a/src/gui/src/PatternEditor/PianoRollEditor.cpp b/src/gui/src/PatternEditor/PianoRollEditor.cpp index 39b7f19347..1d4d92be44 100644 --- a/src/gui/src/PatternEditor/PianoRollEditor.cpp +++ b/src/gui/src/PatternEditor/PianoRollEditor.cpp @@ -22,7 +22,8 @@ #include "PianoRollEditor.h" #include "PatternEditorPanel.h" -#include "NotePropertiesRuler.h" +#include "PatternEditorRuler.h" +#include "PatternEditorInstrumentList.h" #include "UndoActions.h" #include @@ -37,14 +38,16 @@ using namespace H2Core; #include "../HydrogenApp.h" +#include "../Skin.h" PianoRollEditor::PianoRollEditor( QWidget *pParent, PatternEditorPanel *panel, QScrollArea *pScrollView) : PatternEditor( pParent, panel ) , m_pScrollView( pScrollView ) - , m_nOldPoint( 0 ) { + m_editor = PatternEditor::Editor::PianoRoll; + m_nGridHeight = 10; m_nOctaves = 7; @@ -52,7 +55,6 @@ PianoRollEditor::PianoRollEditor( QWidget *pParent, PatternEditorPanel *panel, m_nEditorHeight = m_nOctaves * 12 * m_nGridHeight; - m_pBackground = new QPixmap( m_nEditorWidth, m_nEditorHeight ); m_pTemp = new QPixmap( m_nEditorWidth, m_nEditorHeight ); m_nCursorPitch = 0; @@ -78,13 +80,29 @@ PianoRollEditor::~PianoRollEditor() void PianoRollEditor::updateEditor( bool bPatternOnly ) { + // Ensure that m_pPattern is up to date. + updatePatternInfo(); + + auto pHydrogen = H2Core::Hydrogen::get_instance(); // uint nEditorWidth; - if ( m_pPattern ) { - m_nEditorWidth = m_nMargin + m_fGridWidth * m_pPattern->get_length(); + if ( m_pPattern != nullptr ) { + + m_nActiveWidth = PatternEditor::nMargin + m_fGridWidth * + m_pPattern->get_length(); + if ( pHydrogen->getPatternMode() == Song::PatternMode::Stacked ) { + m_nEditorWidth = + std::max( PatternEditor::nMargin + m_fGridWidth * + pHydrogen->getAudioEngine()->getPlayingPatterns()->longest_pattern_length() + 1, + static_cast(m_nActiveWidth) ); + } else { + m_nEditorWidth = m_nActiveWidth; + } } else { - m_nEditorWidth = m_nMargin + m_fGridWidth * MAX_NOTES; + m_nEditorWidth = PatternEditor::nMargin + m_fGridWidth * MAX_NOTES; + m_nActiveWidth = m_nEditorWidth; } + if ( !bPatternOnly ) { m_bNeedsBackgroundUpdate = true; } @@ -104,23 +122,15 @@ void PianoRollEditor::finishUpdateEditor() if ( m_bNeedsBackgroundUpdate ) { createBackground(); + } else { + drawPattern(); } - drawPattern(); + // ERRORLOG(QString("update editor %1").arg(m_nEditorWidth)); m_bNeedsUpdate = false; m_bNeedsBackgroundUpdate = false; } - - -//eventlistener -void PianoRollEditor::patternModifiedEvent() -{ - updateEditor(); -} - - - void PianoRollEditor::selectedInstrumentChangedEvent() { // Update pattern only @@ -138,15 +148,54 @@ void PianoRollEditor::selectedPatternChangedEvent() void PianoRollEditor::paintEvent(QPaintEvent *ev) { + if (!isVisible()) { + return; + } + + auto pPref = Preferences::get_instance(); + + qreal pixelRatio = devicePixelRatio(); + if ( pixelRatio != m_pBackgroundPixmap->devicePixelRatio() ) { + createBackground(); + } + QPainter painter( this ); if ( m_bNeedsUpdate ) { finishUpdateEditor(); } - painter.drawPixmap( ev->rect(), *m_pTemp, ev->rect() ); + painter.drawPixmap( ev->rect(), *m_pTemp, + QRectF( pixelRatio * ev->rect().x(), + pixelRatio * ev->rect().y(), + pixelRatio * ev->rect().width(), + pixelRatio * ev->rect().height() ) ); + + // Draw playhead + if ( m_nTick != -1 ) { + + int nOffset = Skin::getPlayheadShaftOffset(); + int nX = static_cast(static_cast(PatternEditor::nMargin) + + static_cast(m_nTick) * + m_fGridWidth ); + Skin::setPlayheadPen( &painter, false ); + painter.drawLine( nX, 2, nX, height() - 2 ); + } drawFocus( painter ); m_selection.paintSelection( &painter ); + + // Draw cursor + if ( hasFocus() && !HydrogenApp::get_instance()->hideKeyboardCursor() ) { + QPoint pos = cursorPosition(); + + QPen pen( pPref->getColorTheme()->m_cursorColor ); + pen.setWidth( 2 ); + painter.setPen( pen ); + painter.setBrush( Qt::NoBrush ); + painter.setRenderHint( QPainter::Antialiasing ); + painter.drawRoundedRect( QRect( pos.x() - m_fGridWidth*3, pos.y()-2, + m_fGridWidth*6, m_nGridHeight+3 ), 4, 4 ); + } } void PianoRollEditor::drawFocus( QPainter& painter ) { @@ -182,97 +231,88 @@ void PianoRollEditor::drawFocus( QPainter& painter ) { void PianoRollEditor::createBackground() { - auto pPref = H2Core::Preferences::get_instance(); - //INFOLOG( "(re)creating the background" ); - - QColor backgroundColor( 250, 250, 250 ); - m_pBackground->fill( backgroundColor ); - - - QColor octaveColor( 230, 230, 230 ); - QColor octaveAlternateColor( 200, 200, 200 ); - QColor baseOctaveColor( 245, 245, 245 ); - QColor baseNoteColor( 255, 255, 255 ); - - QColor fbk( 160, 160, 160 ); + const QColor backgroundColor = pPref->getColorTheme()->m_patternEditor_backgroundColor; + const QColor backgroundInactiveColor = pPref->getColorTheme()->m_windowColor; + const QColor alternateRowColor = pPref->getColorTheme()->m_patternEditor_alternateRowColor; + const QColor octaveColor = pPref->getColorTheme()->m_patternEditor_octaveRowColor; + // The line corresponding to the default pitch set to new notes + // will be highlighted. + const QColor baseNoteColor = octaveColor.lighter( 119 ); + const QColor lineColor( pPref->getColorTheme()->m_patternEditor_lineColor ); + const QColor lineInactiveColor( pPref->getColorTheme()->m_windowTextColor.darker( 170 ) ); unsigned start_x = 0; - unsigned end_x = width(); - - QPainter p( m_pBackground ); - - for ( uint octave = 0; octave < m_nOctaves; ++octave ) { - unsigned start_y = octave * 12 * m_nGridHeight; - - if ( octave % 2 ) { + unsigned end_x = m_nActiveWidth; + + // Resize pixmap if pixel ratio has changed + qreal pixelRatio = devicePixelRatio(); + if ( m_pBackgroundPixmap->width() != m_nEditorWidth || + m_pBackgroundPixmap->height() != m_nEditorHeight || + m_pBackgroundPixmap->devicePixelRatio() != pixelRatio ) { + delete m_pBackgroundPixmap; + m_pBackgroundPixmap = new QPixmap( width() * pixelRatio , height() * pixelRatio ); + m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio ); + delete m_pTemp; + m_pTemp = new QPixmap( width() * pixelRatio , height() * pixelRatio ); + m_pTemp->setDevicePixelRatio( pixelRatio ); + } + m_pBackgroundPixmap->fill( backgroundInactiveColor ); - if ( octave == 3 ){ + QPainter p( m_pBackgroundPixmap ); - // p.fillRect( start_x, start_y, end_x - start_x, 12 * m_nGridHeight, baseOctaveColor ); - p.fillRect( start_x, start_y, end_x - start_x, start_y + 1 * m_nGridHeight, baseOctaveColor ); - p.fillRect( start_x, start_y + 1 * m_nGridHeight, end_x - start_x, start_y + 2 * m_nGridHeight, fbk ); - p.fillRect( start_x, start_y + 2 * m_nGridHeight, end_x - start_x, start_y + 3 * m_nGridHeight, baseOctaveColor ); - p.fillRect( start_x, start_y + 3 * m_nGridHeight, end_x - start_x, start_y + 4 * m_nGridHeight, fbk ); - p.fillRect( start_x, start_y + 4 * m_nGridHeight, end_x - start_x, start_y + 5 * m_nGridHeight, baseOctaveColor ); - p.fillRect( start_x, start_y + 5 * m_nGridHeight, end_x - start_x, start_y + 6 * m_nGridHeight, fbk ); - p.fillRect( start_x, start_y + 6 * m_nGridHeight, end_x - start_x, start_y + 7 * m_nGridHeight, baseOctaveColor ); - p.fillRect( start_x, start_y + 7 * m_nGridHeight, end_x - start_x, start_y + 8 * m_nGridHeight, baseOctaveColor ); - p.fillRect( start_x, start_y + 8 * m_nGridHeight, end_x - start_x, start_y + 9 * m_nGridHeight, fbk ); - p.fillRect( start_x, start_y + 9 * m_nGridHeight, end_x - start_x, start_y + 10 * m_nGridHeight, baseOctaveColor ); - p.fillRect( start_x, start_y + 10 * m_nGridHeight, end_x - start_x, start_y + 11 * m_nGridHeight, fbk ); - p.fillRect( start_x, start_y + 11 * m_nGridHeight, end_x - start_x, start_y + 12 * m_nGridHeight, baseNoteColor ); - } - else - { - // p.fillRect( start_x, start_y, end_x - start_x, 12 * m_nGridHeight, octaveColor ); - p.fillRect( start_x, start_y, end_x - start_x, start_y + 1 * m_nGridHeight, octaveColor ); - p.fillRect( start_x, start_y + 1 * m_nGridHeight, end_x - start_x, start_y + 2 * m_nGridHeight, fbk ); - p.fillRect( start_x, start_y + 2 * m_nGridHeight, end_x - start_x, start_y + 3 * m_nGridHeight, octaveColor ); - p.fillRect( start_x, start_y + 3 * m_nGridHeight, end_x - start_x, start_y + 4 * m_nGridHeight, fbk ); - p.fillRect( start_x, start_y + 4 * m_nGridHeight, end_x - start_x, start_y + 5 * m_nGridHeight, octaveColor ); - p.fillRect( start_x, start_y + 5 * m_nGridHeight, end_x - start_x, start_y + 6 * m_nGridHeight, fbk ); - p.fillRect( start_x, start_y + 6 * m_nGridHeight, end_x - start_x, start_y + 7 * m_nGridHeight, octaveColor ); - p.fillRect( start_x, start_y + 7 * m_nGridHeight, end_x - start_x, start_y + 8 * m_nGridHeight, octaveColor ); - p.fillRect( start_x, start_y + 8 * m_nGridHeight, end_x - start_x, start_y + 9 * m_nGridHeight, fbk ); - p.fillRect( start_x, start_y + 9 * m_nGridHeight, end_x - start_x, start_y + 10 * m_nGridHeight, octaveColor ); - p.fillRect( start_x, start_y + 10 * m_nGridHeight, end_x - start_x, start_y + 11 * m_nGridHeight, fbk ); - p.fillRect( start_x, start_y + 11 * m_nGridHeight, end_x - start_x, start_y + 12 * m_nGridHeight, octaveColor ); + for ( uint ooctave = 0; ooctave < m_nOctaves; ++ooctave ) { + unsigned start_y = ooctave * 12 * m_nGridHeight; + for ( int ii = 0; ii < 12; ++ii ) { + if ( ii == 0 || ii == 2 || ii == 4 || ii == 6 || ii == 7 || + ii == 9 || ii == 11 ) { + if ( ooctave % 2 != 0 ) { + p.fillRect( start_x, start_y + ii * m_nGridHeight, + end_x - start_x, start_y + ( ii + 1 ) * m_nGridHeight, + octaveColor ); + } else { + p.fillRect( start_x, start_y + ii * m_nGridHeight, + end_x - start_x, start_y + ( ii + 1 ) * m_nGridHeight, + backgroundColor ); + } + } else { + p.fillRect( start_x, start_y + ii * m_nGridHeight, + end_x - start_x, start_y + ( ii + 1 ) * m_nGridHeight, + alternateRowColor ); } } - else { - // p.fillRect( start_x, start_y, end_x - start_x, 12 * m_nGridHeight, octaveAlternateColor ); - p.fillRect( start_x, start_y, end_x - start_x, start_y + 1 * m_nGridHeight, octaveAlternateColor ); - p.fillRect( start_x, start_y + 1 * m_nGridHeight, end_x - start_x, start_y + 2 * m_nGridHeight, fbk ); - p.fillRect( start_x, start_y + 2 * m_nGridHeight, end_x - start_x, start_y + 3 * m_nGridHeight, octaveAlternateColor ); - p.fillRect( start_x, start_y + 3 * m_nGridHeight, end_x - start_x, start_y + 4 * m_nGridHeight, fbk ); - p.fillRect( start_x, start_y + 4 * m_nGridHeight, end_x - start_x, start_y + 5 * m_nGridHeight, octaveAlternateColor ); - p.fillRect( start_x, start_y + 5 * m_nGridHeight, end_x - start_x, start_y + 6 * m_nGridHeight, fbk ); - p.fillRect( start_x, start_y + 6 * m_nGridHeight, end_x - start_x, start_y + 7 * m_nGridHeight, octaveAlternateColor ); - p.fillRect( start_x, start_y + 7 * m_nGridHeight, end_x - start_x, start_y + 8 * m_nGridHeight, octaveAlternateColor ); - p.fillRect( start_x, start_y + 8 * m_nGridHeight, end_x - start_x, start_y + 9 * m_nGridHeight, fbk ); - p.fillRect( start_x, start_y + 9 * m_nGridHeight, end_x - start_x, start_y + 10 * m_nGridHeight, octaveAlternateColor ); - p.fillRect( start_x, start_y + 10 * m_nGridHeight, end_x - start_x, start_y + 11 * m_nGridHeight, fbk ); - p.fillRect( start_x, start_y + 11 * m_nGridHeight, end_x - start_x, start_y + 12 * m_nGridHeight, octaveAlternateColor ); - + + // Highlight base note pitch + if ( ooctave == 3 ) { + p.fillRect( start_x, start_y + 11 * m_nGridHeight, + end_x - start_x, start_y + 12 * m_nGridHeight, + baseNoteColor ); } } // horiz lines + p.setPen( lineColor ); for ( uint row = 0; row < ( 12 * m_nOctaves ); ++row ) { unsigned y = row * m_nGridHeight; - p.drawLine( start_x, y,end_x , y ); + p.drawLine( start_x, y, end_x, y ); + } + + if ( m_nActiveWidth + 1 < m_nEditorWidth ) { + p.setPen( lineInactiveColor ); + for ( uint row = 0; row < ( 12 * m_nOctaves ); ++row ) { + unsigned y = row * m_nGridHeight; + p.drawLine( m_nActiveWidth, y, m_nEditorWidth, y ); + } } //draw text QFont font( pPref->getApplicationFontFamily(), getPointSize( pPref->getFontSize() ) ); - // font.setWeight( 63 ); p.setFont( font ); - p.setPen( QColor(10, 10, 10 ) ); + p.setPen( pPref->getColorTheme()->m_patternEditor_textColor ); int offset = 0; int insertx = 3; @@ -310,6 +350,10 @@ void PianoRollEditor::createBackground() } drawGridLines( p, Qt::DashLine ); + drawPattern(); + + p.setPen( QPen( lineColor, 2, Qt::SolidLine ) ); + p.drawLine( m_nEditorWidth, 0, m_nEditorWidth, m_nEditorHeight ); } @@ -319,10 +363,15 @@ void PianoRollEditor::drawPattern() validateSelection(); + qreal pixelRatio = devicePixelRatio(); + QPainter p( m_pTemp ); // copy the background image - p.drawPixmap( rect(), *m_pBackground, rect() ); - + p.drawPixmap( rect(), *m_pBackgroundPixmap, + QRectF( pixelRatio * rect().x(), + pixelRatio * rect().y(), + pixelRatio * rect().width(), + pixelRatio * rect().height() ) ); // for each note... for ( Pattern *pPattern : getPatternsToShow() ) { @@ -335,19 +384,6 @@ void PianoRollEditor::drawPattern() } } - // Draw cursor - if ( hasFocus() && !HydrogenApp::get_instance()->hideKeyboardCursor() ) { - QPoint pos = cursorPosition(); - - QPen pen( Qt::black ); - pen.setWidth( 2 ); - p.setPen( pen ); - p.setBrush( Qt::NoBrush ); - p.setRenderHint( QPainter::Antialiasing ); - p.drawRoundedRect( QRect( pos.x() - m_fGridWidth*3, pos.y()-2, - m_fGridWidth*6, m_nGridHeight+3 ), 4, 4 ); - } - } @@ -356,7 +392,7 @@ void PianoRollEditor::drawNote( Note *pNote, QPainter *pPainter, bool bIsForegro Hydrogen *pHydrogen = Hydrogen::get_instance(); InstrumentList * pInstrList = pHydrogen->getSong()->getInstrumentList(); if ( pInstrList->index( pNote->get_instrument() ) == pHydrogen->getSelectedInstrumentNumber() ) { - QPoint pos ( m_nMargin + pNote->get_position() * m_fGridWidth, + QPoint pos ( PatternEditor::nMargin + pNote->get_position() * m_fGridWidth, m_nGridHeight * pitchToLine( pNote->get_notekey_pitch() ) + 1); drawNoteSymbol( *pPainter, pos, pNote, bIsForeground ); } @@ -367,6 +403,11 @@ void PianoRollEditor::addOrRemoveNote( int nColumn, int nRealColumn, int nLine, int nNotekey, int nOctave, bool bDoAdd, bool bDoDelete ) { + if ( m_pPattern == nullptr || m_nSelectedPatternNumber == -1 ) { + // No pattern selected. + return; + } + Note::Octave octave = (Note::Octave)nOctave; Note::Key notekey = (Note::Key)nNotekey; Hydrogen *pHydrogen = Hydrogen::get_instance(); @@ -431,10 +472,11 @@ void PianoRollEditor::addOrRemoveNote( int nColumn, int nRealColumn, int nLine, void PianoRollEditor::mouseClickEvent( QMouseEvent *ev ) { - if ( m_pPattern == nullptr ) { + if ( m_pPattern == nullptr || m_nSelectedPatternNumber == -1 ) { return; } + auto pHydrogenApp = HydrogenApp::get_instance(); std::shared_ptr pSong = Hydrogen::get_instance()->getSong(); int nPressedLine = ((int) ev->y()) / ((int) m_nGridHeight); @@ -449,8 +491,6 @@ void PianoRollEditor::mouseClickEvent( QMouseEvent *ev ) { return; } m_pPatternEditorPanel->setCursorPosition( nColumn ); - HydrogenApp::get_instance()->setHideKeyboardCursor( true ); - std::shared_ptr pSelectedInstrument = nullptr; int nSelectedInstrumentnumber = Hydrogen::get_instance()->getSelectedInstrumentNumber(); @@ -458,15 +498,15 @@ void PianoRollEditor::mouseClickEvent( QMouseEvent *ev ) { assert(pSelectedInstrument); int nPitch = lineToPitch( nPressedLine ); - Note::Octave pressedoctave = pitchToOctave( nPitch ); - Note::Key pressednotekey = pitchToKey( nPitch ); + Note::Octave pressedoctave = Note::pitchToOctave( nPitch ); + Note::Key pressednotekey = Note::pitchToKey( nPitch ); m_nCursorPitch = nPitch; if (ev->button() == Qt::LeftButton ) { unsigned nRealColumn = 0; - if( ev->x() > m_nMargin ) { - nRealColumn = (ev->x() - m_nMargin) / static_cast(m_fGridWidth); + if( ev->x() > PatternEditor::nMargin ) { + nRealColumn = (ev->x() - PatternEditor::nMargin) / static_cast(m_fGridWidth); } if ( ev->modifiers() & Qt::ShiftModifier ) { @@ -484,10 +524,10 @@ void PianoRollEditor::mouseClickEvent( QMouseEvent *ev ) { pNote->get_octave(), pNote->get_probability(), pNote != nullptr ); - HydrogenApp::get_instance()->m_pUndoStack->push( action ); + pHydrogenApp->m_pUndoStack->push( action ); } else { SE_addPianoRollNoteOffAction *action = new SE_addPianoRollNoteOffAction( nColumn, nPressedLine, m_nSelectedPatternNumber, nSelectedInstrumentnumber ); - HydrogenApp::get_instance()->m_pUndoStack->push( action ); + pHydrogenApp->m_pUndoStack->push( action ); } return; } @@ -502,60 +542,105 @@ void PianoRollEditor::mouseClickEvent( QMouseEvent *ev ) { } -void PianoRollEditor::mouseDragStartEvent( QMouseEvent *ev ) -{ - m_pDraggedNote = nullptr; - Hydrogen *pH2 = Hydrogen::get_instance(); - int nColumn = getColumn( ev->x() ); - std::shared_ptr pSong = pH2->getSong(); - int nSelectedInstrumentnumber = pH2->getSelectedInstrumentNumber(); - auto pSelectedInstrument = pSong->getInstrumentList()->get( nSelectedInstrumentnumber ); - m_pPatternEditorPanel->setCursorPosition( nColumn ); - HydrogenApp::get_instance()->setHideKeyboardCursor( true ); +void PianoRollEditor::mousePressEvent( QMouseEvent* ev ) { + if ( ev->x() > m_nActiveWidth ) { + return; + } - int nPressedLine = ((int) ev->y()) / ((int) m_nGridHeight); + PatternEditor::mousePressEvent( ev ); + + auto pHydrogenApp = HydrogenApp::get_instance(); - Note::Octave pressedoctave = pitchToOctave( lineToPitch( nPressedLine ) ); - Note::Key pressednotekey = pitchToKey( lineToPitch( nPressedLine ) ); - m_nCursorPitch = lineToPitch( nPressedLine ); + // Hide cursor in case this behavior was selected in the + // Preferences. + bool bOldCursorHidden = pHydrogenApp->hideKeyboardCursor(); + pHydrogenApp->setHideKeyboardCursor( true ); - if (ev->button() == Qt::RightButton ) { - m_nOldPoint = ev->y(); + // Cursor just got hidden. + if ( bOldCursorHidden != pHydrogenApp->hideKeyboardCursor() ) { + // Immediate update to prevent visual delay. + m_pPatternEditorPanel->getPatternEditorRuler()->update(); + update(); + } - unsigned nRealColumn = 0; - if( ev->x() > m_nMargin ) { - nRealColumn = (ev->x() - m_nMargin) / static_cast(m_fGridWidth); + // Update cursor position + if ( ! pHydrogenApp->hideKeyboardCursor() ) { + int nPressedLine = ((int) ev->y()) / ((int) m_nGridHeight); + if ( nPressedLine >= (int) m_nOctaves * 12 ) { + return; + } + m_nCursorPitch = lineToPitch( nPressedLine ); + + int nColumn = getColumn( ev->x(), /* bUseFineGrained=*/ true ); + if ( ( m_pPattern != nullptr && + nColumn >= (int)m_pPattern->get_length() ) || + nColumn >= MAX_INSTRUMENTS ) { + return; } + m_pPatternEditorPanel->setCursorPosition( nColumn ); + + update(); + m_pPatternEditorPanel->getPatternEditorRuler()->update(); + } +} + +void PianoRollEditor::mouseDragStartEvent( QMouseEvent *ev ) +{ + if ( m_pPattern == nullptr ) { + return; + } - // m_pAudioEngine->lock( RIGHT_HERE ); + // Handles cursor repositioning and hiding and stores general + // properties. + PatternEditor::mouseDragStartEvent( ev ); + + m_pDraggedNote = nullptr; + Hydrogen *pHydrogen = Hydrogen::get_instance(); + int nColumn = getColumn( ev->x() ); + std::shared_ptr pSong = pHydrogen->getSong(); + int nSelectedInstrumentNumber = pHydrogen->getSelectedInstrumentNumber(); + auto pSelectedInstrument = pSong->getInstrumentList()->get( nSelectedInstrumentNumber ); - m_pDraggedNote = m_pPattern->find_note( nColumn, nRealColumn, pSelectedInstrument, pressednotekey, pressedoctave, false ); + int nRow = std::floor(static_cast(ev->y()) / + static_cast(m_nGridHeight)); - //needed for undo note length - m_nRealColumn = nRealColumn; - m_nColumn = nColumn; - m_nPressedLine = nPressedLine; - m_nSelectedInstrumentNumber = nSelectedInstrumentnumber; - if( m_pDraggedNote ){ - m_nOldLength = m_pDraggedNote->get_length(); - //needed to undo note properties - m_fOldVelocity = m_pDraggedNote->get_velocity(); - m_fOldPan = m_pDraggedNote->getPan(); + Note::Octave pressedOctave = Note::pitchToOctave( lineToPitch( nRow ) ); + Note::Key pressedNoteKey = Note::pitchToKey( lineToPitch( nRow ) ); + m_nCursorPitch = lineToPitch( nRow ); - m_fOldLeadLag = m_pDraggedNote->get_lead_lag(); + if (ev->button() == Qt::RightButton ) { - m_fVelocity = m_fOldVelocity; - m_fPan = m_fOldPan; - m_fLeadLag = m_fOldLeadLag; - }else - { - m_nOldLength = -1; + int nRealColumn = 0; + if( ev->x() > PatternEditor::nMargin ) { + nRealColumn = + static_cast(std::floor( + static_cast((ev->x() - PatternEditor::nMargin)) / + m_fGridWidth)); } - // m_pAudioEngine->unlock(); + + m_pDraggedNote = m_pPattern->find_note( nColumn, nRealColumn, + pSelectedInstrument, + pressedNoteKey, + pressedOctave, false ); + + // Store note-specific properties. + storeNoteProperties( m_pDraggedNote ); + + m_nRow = nRow; } } +void PianoRollEditor::mouseDragUpdateEvent( QMouseEvent *ev ) +{ + int nRow = std::floor(static_cast(ev->y()) / + static_cast(m_nGridHeight)); + if ( nRow >= (int) m_nOctaves * 12 ) { + return; + } + + PatternEditor::mouseDragUpdateEvent( ev ); +} void PianoRollEditor::addOrDeleteNoteAction( int nColumn, int pressedLine, @@ -571,6 +656,10 @@ void PianoRollEditor::addOrDeleteNoteAction( int nColumn, bool noteOff, bool isDelete ) { + if ( m_pPattern == nullptr ) { + return; + } + Hydrogen *pHydrogen = Hydrogen::get_instance(); std::shared_ptr pSong = pHydrogen->getSong(); PatternList *pPatternList = pHydrogen->getSong()->getPatternList(); @@ -583,8 +672,8 @@ void PianoRollEditor::addOrDeleteNoteAction( int nColumn, pPattern = pPatternList->get( selectedPatternNumber ); } - Note::Octave pressedoctave = pitchToOctave( lineToPitch( pressedLine ) ); - Note::Key pressednotekey = pitchToKey( lineToPitch( pressedLine ) ); + Note::Octave pressedoctave = Note::pitchToOctave( lineToPitch( pressedLine ) ); + Note::Key pressednotekey = Note::pitchToKey( lineToPitch( pressedLine ) ); m_pAudioEngine->lock( RIGHT_HERE ); // lock the audio engine @@ -691,179 +780,9 @@ void PianoRollEditor::moveNoteAction( int nColumn, } - - -void PianoRollEditor::mouseDragUpdateEvent( QMouseEvent *ev ) -{ - if ( m_pPattern == nullptr ) { - return; - } - - int nRow = ((int) ev->y()) / ((int) m_nGridHeight); - if ( nRow >= (int) m_nOctaves * 12 ) { - return; - } - - if ( m_pDraggedNote ) { - if ( m_pDraggedNote->get_note_off() ) { - return; - } - int nTickColumn = getColumn( ev->x() ); - - m_pAudioEngine->lock( RIGHT_HERE ); // lock the audio engine - int nLen = nTickColumn - (int)m_pDraggedNote->get_position(); - - if (nLen <= 0) { - nLen = -1; - } - - float fNotePitch = m_pDraggedNote->get_notekey_pitch(); - float fStep = 0; - if(nLen > -1){ - fStep = Note::pitchToFrequency( ( double )fNotePitch ); - } else { - fStep = 1.0; - } - m_pDraggedNote->set_length( nLen * fStep); - - Hydrogen::get_instance()->setIsModified( true ); - m_pAudioEngine->unlock(); // unlock the audio engine - - m_pPatternEditorPanel->updateEditors( true ); - } - - int selectedProperty = m_pPatternEditorPanel->getPropertiesComboValue(); - - //edit velocity - if ( m_pDraggedNote && selectedProperty == 0 ) { // Velocity - if ( m_pDraggedNote->get_note_off() ) return; - - m_pAudioEngine->lock( RIGHT_HERE ); // lock the audio engine - - float val = m_pDraggedNote->get_velocity(); - - - float ymove = m_nOldPoint - ev->y(); - val = val + (ymove / 100); - if (val > 1) { - val = 1; - } - else if (val < 0.0) { - val = 0.0; - } - - m_pDraggedNote->set_velocity( val ); - - m_fVelocity = val; - - Hydrogen::get_instance()->setIsModified( true ); - m_pAudioEngine->unlock(); // unlock the audio engine - - m_pPatternEditorPanel->updateEditors( true ); - m_nOldPoint = ev->y(); - } - - //edit pan - if ( m_pDraggedNote && selectedProperty == 1 ) { // Pan - if ( m_pDraggedNote->get_note_off() ) return; - - m_pAudioEngine->lock( RIGHT_HERE ); // lock the audio engine - - float ymove = m_nOldPoint - ev->y(); - float fVal = m_pDraggedNote->getPanWithRangeFrom0To1() + (ymove / 100); - - m_pDraggedNote->setPanWithRangeFrom0To1( fVal ); // checks the boundaries as well - m_fPan = m_pDraggedNote->getPan(); - - Hydrogen::get_instance()->setIsModified( true ); - m_pAudioEngine->unlock(); // unlock the audio engine - - m_pPatternEditorPanel->updateEditors(); - m_nOldPoint = ev->y(); - } - - //edit lead lag - if ( m_pDraggedNote && selectedProperty == 2 ) { // Lead and Lag - if ( m_pDraggedNote->get_note_off() ) return; - - m_pAudioEngine->lock( RIGHT_HERE ); // lock the audio engine - - - float val = ( m_pDraggedNote->get_lead_lag() - 1.0 ) / -2.0 ; - - float ymove = m_nOldPoint - ev->y(); - val = val + (ymove / 100); - - if (val > 1.0) { - val = 1.0; - } - else if (val < 0.0) { - val = 0.0; - } - - m_pDraggedNote->set_lead_lag((val * -2.0) + 1.0); - - m_fLeadLag = (val * -2.0) + 1.0; - - char valueChar[100]; - if ( m_pDraggedNote->get_lead_lag() < 0.0 ) { - sprintf( valueChar, "%.2f", ( m_pDraggedNote->get_lead_lag() * -5 ) ); // FIXME: '5' taken from fLeadLagFactor calculation in hydrogen.cpp - HydrogenApp::get_instance()->setStatusBarMessage( QString("Leading beat by: %1 ticks").arg( valueChar ), 2000 ); - } else if ( m_pDraggedNote->get_lead_lag() > 0.0 ) { - sprintf( valueChar, "%.2f", ( m_pDraggedNote->get_lead_lag() * 5 ) ); // FIXME: '5' taken from fLeadLagFactor calculation in hydrogen.cpp - HydrogenApp::get_instance()->setStatusBarMessage( QString("Lagging beat by: %1 ticks").arg( valueChar ), 2000 ); - } else { - HydrogenApp::get_instance()->setStatusBarMessage( QString("Note on beat"), 2000 ); - } - - Hydrogen::get_instance()->setIsModified( true ); - m_pAudioEngine->unlock(); // unlock the audio engine - - m_pPatternEditorPanel->updateEditors( true ); - m_nOldPoint = ev->y(); - } - -} - - -void PianoRollEditor::mouseDragEndEvent( QMouseEvent *ev ) -{ - //INFOLOG("Mouse release event" ); - if (m_pPattern == nullptr) { - return; - } - - if ( m_pDraggedNote ) { - if ( m_pDraggedNote->get_note_off() ) return; - - - - if( m_pDraggedNote->get_length() != m_nOldLength ) - { - SE_editPianoRollNoteLengthAction *action = new SE_editPianoRollNoteLengthAction( m_pDraggedNote->get_position(), m_pDraggedNote->get_position(), m_pDraggedNote->get_length(),m_nOldLength, m_nSelectedPatternNumber, m_nSelectedInstrumentNumber, m_nPressedLine ); - HydrogenApp::get_instance()->m_pUndoStack->push( action ); - } - - - if( m_fVelocity == m_fOldVelocity && m_fOldLeadLag == m_fLeadLag && m_fOldPan == m_fPan ) return; - SE_editNotePropertiesPianoRollAction *action = new SE_editNotePropertiesPianoRollAction( m_pDraggedNote->get_position(), - m_pDraggedNote->get_position(), - m_nSelectedPatternNumber, - m_nSelectedInstrumentNumber, - m_fVelocity, - m_fOldVelocity, - m_fPan, - m_fOldPan, - m_fLeadLag, - m_fOldLeadLag, - m_nPressedLine ); - HydrogenApp::get_instance()->m_pUndoStack->push( action ); - } -} - QPoint PianoRollEditor::cursorPosition() { - uint x = m_nMargin + m_pPatternEditorPanel->getCursorPosition() * m_fGridWidth; + uint x = PatternEditor::nMargin + m_pPatternEditorPanel->getCursorPosition() * m_fGridWidth; uint y = m_nGridHeight * pitchToLine( m_nCursorPitch ) + 1; return QPoint(x, y); } @@ -876,6 +795,11 @@ void PianoRollEditor::selectAll() void PianoRollEditor::deleteSelection() { + if ( m_nSelectedPatternNumber == -1 ) { + // No pattern selected. + return; + } + if ( m_selection.begin() != m_selection.end() ) { // Delete a selection. Hydrogen *pHydrogen = Hydrogen::get_instance(); @@ -921,6 +845,11 @@ void PianoRollEditor::deleteSelection() /// void PianoRollEditor::paste() { + if ( m_pPattern == nullptr || m_nSelectedPatternNumber == -1 ) { + // No pattern selected. + return; + } + QClipboard *clipboard = QApplication::clipboard(); QUndoStack *pUndo = HydrogenApp::get_instance()->m_pUndoStack; InstrumentList *pInstrList = Hydrogen::get_instance()->getSong()->getInstrumentList(); @@ -1025,6 +954,13 @@ void PianoRollEditor::paste() void PianoRollEditor::keyPressEvent( QKeyEvent * ev ) { + if ( m_pPattern == nullptr ) { + return; + } + + auto pHydrogenApp = HydrogenApp::get_instance(); + bool bOldCursorHidden = pHydrogenApp->hideKeyboardCursor(); + const int nBlockSize = 5, nWordSize = 5; bool bIsSelectionKey = m_selection.keyPressEvent( ev ); bool bUnhideCursor = true; @@ -1057,50 +993,50 @@ void PianoRollEditor::keyPressEvent( QKeyEvent * ev ) m_pPatternEditorPanel->setCursorPosition( 0 ); } else if ( ev->matches( QKeySequence::MoveToNextLine ) || ev->matches( QKeySequence::SelectNextLine ) ) { - if ( m_nCursorPitch > octaveKeyToPitch( (Note::Octave)OCTAVE_MIN, (Note::Key)KEY_MIN ) ) { + if ( m_nCursorPitch > Note::octaveKeyToPitch( (Note::Octave)OCTAVE_MIN, (Note::Key)KEY_MIN ) ) { m_nCursorPitch --; } } else if ( ev->matches( QKeySequence::MoveToEndOfBlock ) || ev->matches( QKeySequence::SelectEndOfBlock ) ) { - m_nCursorPitch = std::max( octaveKeyToPitch( (Note::Octave)OCTAVE_MIN, (Note::Key)KEY_MIN ), + m_nCursorPitch = std::max( Note::octaveKeyToPitch( (Note::Octave)OCTAVE_MIN, (Note::Key)KEY_MIN ), m_nCursorPitch - nBlockSize ); } else if ( ev->matches( QKeySequence::MoveToNextPage ) || ev->matches( QKeySequence::SelectNextPage ) ) { // Page down -- move down by a whole octave - int nMinPitch = octaveKeyToPitch( (Note::Octave)OCTAVE_MIN, (Note::Key)KEY_MIN ); + int nMinPitch = Note::octaveKeyToPitch( (Note::Octave)OCTAVE_MIN, (Note::Key)KEY_MIN ); m_nCursorPitch -= 12; if ( m_nCursorPitch < nMinPitch ) { m_nCursorPitch = nMinPitch; } } else if ( ev->matches( QKeySequence::MoveToEndOfDocument ) || ev->matches( QKeySequence::SelectEndOfDocument ) ) { - m_nCursorPitch = octaveKeyToPitch( (Note::Octave)OCTAVE_MIN, (Note::Key)KEY_MIN ); + m_nCursorPitch = Note::octaveKeyToPitch( (Note::Octave)OCTAVE_MIN, (Note::Key)KEY_MIN ); } else if ( ev->matches( QKeySequence::MoveToPreviousLine ) || ev->matches( QKeySequence::SelectPreviousLine ) ) { - if ( m_nCursorPitch < octaveKeyToPitch( (Note::Octave)OCTAVE_MAX, (Note::Key)KEY_MAX ) ) { + if ( m_nCursorPitch < Note::octaveKeyToPitch( (Note::Octave)OCTAVE_MAX, (Note::Key)KEY_MAX ) ) { m_nCursorPitch ++; } } else if ( ev->matches( QKeySequence::MoveToStartOfBlock ) || ev->matches( QKeySequence::SelectStartOfBlock ) ) { - m_nCursorPitch = std::min( octaveKeyToPitch( (Note::Octave)OCTAVE_MAX, (Note::Key)KEY_MAX ), + m_nCursorPitch = std::min( Note::octaveKeyToPitch( (Note::Octave)OCTAVE_MAX, (Note::Key)KEY_MAX ), m_nCursorPitch + nBlockSize ); } else if ( ev->matches( QKeySequence::MoveToPreviousPage ) || ev->matches( QKeySequence::SelectPreviousPage ) ) { - int nMaxPitch = octaveKeyToPitch( (Note::Octave)OCTAVE_MAX, (Note::Key)KEY_MAX ); + int nMaxPitch = Note::octaveKeyToPitch( (Note::Octave)OCTAVE_MAX, (Note::Key)KEY_MAX ); m_nCursorPitch += 12; if ( m_nCursorPitch >= nMaxPitch ) { m_nCursorPitch = nMaxPitch; } } else if ( ev->matches( QKeySequence::MoveToStartOfDocument ) || ev->matches( QKeySequence::SelectStartOfDocument ) ) { - m_nCursorPitch = octaveKeyToPitch( (Note::Octave)OCTAVE_MAX, (Note::Key)KEY_MAX ); + m_nCursorPitch = Note::octaveKeyToPitch( (Note::Octave)OCTAVE_MAX, (Note::Key)KEY_MAX ); } else if ( ev->key() == Qt::Key_Enter || ev->key() == Qt::Key_Return ) { // Key: Enter/Return : Place or remove note at current position int pressedline = pitchToLine( m_nCursorPitch ); int nPitch = lineToPitch( pressedline ); addOrRemoveNote( m_pPatternEditorPanel->getCursorPosition(), -1, pressedline, - pitchToKey( nPitch ), pitchToOctave( nPitch ) ); + Note::pitchToKey( nPitch ), Note::pitchToOctave( nPitch ) ); } else if ( ev->matches( QKeySequence::SelectAll ) ) { // Key: Ctrl + A: Select all @@ -1122,7 +1058,7 @@ void PianoRollEditor::keyPressEvent( QKeyEvent * ev ) int pressedline = pitchToLine( m_nCursorPitch ); int nPitch = lineToPitch( pressedline ); addOrRemoveNote( m_pPatternEditorPanel->getCursorPosition(), -1, pressedline, - pitchToKey( nPitch ), pitchToOctave( nPitch ), + Note::pitchToKey( nPitch ), Note::pitchToOctave( nPitch ), /*bDoAdd=*/false, /*bDoDelete=*/true ); } @@ -1140,6 +1076,12 @@ void PianoRollEditor::keyPressEvent( QKeyEvent * ev ) } else { ev->ignore(); + pHydrogenApp->setHideKeyboardCursor( true ); + + if ( bOldCursorHidden != pHydrogenApp->hideKeyboardCursor() ) { + m_pPatternEditorPanel->getPatternEditorRuler()->update(); + update(); + } return; } @@ -1150,81 +1092,16 @@ void PianoRollEditor::keyPressEvent( QKeyEvent * ev ) } m_pScrollView->ensureVisible( pos.x(), pos.y() ); m_selection.updateKeyboardCursorPosition( getKeyboardCursorRect() ); - updateEditor( true ); - ev->accept(); -} - -void PianoRollEditor::focusInEvent( QFocusEvent * ev ) -{ - UNUSED( ev ); - if ( ev->reason() == Qt::TabFocusReason || ev->reason() == Qt::BacktabFocusReason ) { - HydrogenApp::get_instance()->setHideKeyboardCursor( false ); - m_pPatternEditorPanel->ensureCursorVisible(); + if ( ! HydrogenApp::get_instance()->hideKeyboardCursor() ) { + // Immediate update to prevent visual delay. + m_pPatternEditorPanel->getPatternEditorRuler()->update(); } + updateEditor( true ); + ev->accept(); } - -void PianoRollEditor::editNoteLengthAction( int nColumn, int nRealColumn, int length, int selectedPatternNumber, int nSelectedInstrumentnumber, int pressedline) -{ - - Hydrogen *pHydrogen = Hydrogen::get_instance(); - - std::shared_ptr pSong = pHydrogen->getSong(); - auto pSelectedInstrument = pSong->getInstrumentList()->get( nSelectedInstrumentnumber ); - - - Note::Octave pressedoctave = pitchToOctave( lineToPitch( pressedline ) ); - Note::Key pressednotekey = pitchToKey( lineToPitch( pressedline ) ); - - Note* pDraggedNote = nullptr; - m_pAudioEngine->lock( RIGHT_HERE ); - pDraggedNote = m_pPattern->find_note( nColumn, nRealColumn, pSelectedInstrument, pressednotekey, pressedoctave, false ); - if ( pDraggedNote ){ - pDraggedNote->set_length( length ); - } - - pHydrogen->setIsModified( true ); - m_pAudioEngine->unlock(); - m_pPatternEditorPanel->updateEditors( true ); -} - - - -void PianoRollEditor::editNotePropertiesAction( int nColumn, - int nRealColumn, - int selectedPatternNumber, - int selectedInstrumentnumber, - float velocity, - float fPan, - float leadLag, - int pressedline ) -{ - - Hydrogen *pHydrogen = Hydrogen::get_instance(); - - Note::Octave pressedoctave = pitchToOctave( lineToPitch( pressedline ) ); - Note::Key pressednotekey = pitchToKey( lineToPitch( pressedline ) ); - - std::shared_ptr pSong = pHydrogen->getSong(); - - auto pSelectedInstrument = pSong->getInstrumentList()->get( selectedInstrumentnumber ); - - Note* pDraggedNote = nullptr; - m_pAudioEngine->lock( RIGHT_HERE ); - pDraggedNote = m_pPattern->find_note( nColumn, nRealColumn, pSelectedInstrument, pressednotekey, pressedoctave, false ); - if ( pDraggedNote ){ - pDraggedNote->set_velocity( velocity ); - pDraggedNote->setPan( fPan ); - pDraggedNote->set_lead_lag( leadLag ); - } - pHydrogen->setIsModified( true ); - m_pAudioEngine->unlock(); - m_pPatternEditorPanel->updateEditors( true ); -} - - // Selection manager interface void PianoRollEditor::selectionMoveEndEvent( QInputEvent *ev ) { @@ -1242,6 +1119,11 @@ void PianoRollEditor::selectionMoveEndEvent( QInputEvent *ev ) int nSelectedPatternNumber = pHydrogen->getSelectedPatternNumber(); int nSelectedInstrumentNumber = pHydrogen->getSelectedInstrumentNumber(); + if ( m_pPattern == nullptr || nSelectedPatternNumber == -1 ) { + // No pattern selected. Nothing to be selected. + return; + } + QUndoStack *pUndo = HydrogenApp::get_instance()->m_pUndoStack; if (m_bCopyNotMove) { @@ -1270,8 +1152,8 @@ void PianoRollEditor::selectionMoveEndEvent( QInputEvent *ev ) // Transpose note int nNewPitch = pNote->get_notekey_pitch() - offset.y(); int nLine = pitchToLine( nNewPitch ); - Note::Octave newOctave = pitchToOctave( nNewPitch ); - Note::Key newKey = pitchToKey( nNewPitch ); + Note::Octave newOctave = Note::pitchToOctave( nNewPitch ); + Note::Key newKey = Note::pitchToKey( nNewPitch ); bool bNoteInRange = ( newOctave >= OCTAVE_MIN && newOctave <= OCTAVE_MAX && nNewPosition >= 0 && nNewPosition < m_pPattern->get_length() ); @@ -1314,8 +1196,13 @@ void PianoRollEditor::selectionMoveEndEvent( QInputEvent *ev ) pUndo->endMacro(); } -std::vector PianoRollEditor::elementsIntersecting( QRect r ) +std::vector PianoRollEditor::elementsIntersecting( QRect r ) { + std::vector result; + if ( m_pPattern == nullptr ) { + return std::move( result ); + } + int w = 8; int h = m_nGridHeight - 2; int nInstr = Hydrogen::get_instance()->getSelectedInstrumentNumber(); @@ -1327,16 +1214,15 @@ std::vector PianoRollEditor::elementsIntersecti } // Calculate the first and last position values that this rect will intersect with - int x_min = (r.left() - w - m_nMargin) / m_fGridWidth; - int x_max = (r.right() + w - m_nMargin) / m_fGridWidth; + int x_min = (r.left() - w - PatternEditor::nMargin) / m_fGridWidth; + int x_max = (r.right() + w - PatternEditor::nMargin) / m_fGridWidth; const Pattern::notes_t* pNotes = m_pPattern->get_notes(); - std::vector result; for ( auto it = pNotes->lower_bound( x_min ); it != pNotes->end() && it->first <= x_max; ++it ) { Note *pNote = it->second; if ( pNote->get_instrument() == pInstr ) { - uint start_x = m_nMargin + pNote->get_position() * m_fGridWidth; + uint start_x = PatternEditor::nMargin + pNote->get_position() * m_fGridWidth; uint start_y = m_nGridHeight * pitchToLine( pNote->get_notekey_pitch() ) + 1; if ( r.intersects( QRect( start_x -4 , start_y, w, h ) ) ) { diff --git a/src/gui/src/PatternEditor/PianoRollEditor.h b/src/gui/src/PatternEditor/PianoRollEditor.h index f7a731efc8..809865bec0 100644 --- a/src/gui/src/PatternEditor/PianoRollEditor.h +++ b/src/gui/src/PatternEditor/PianoRollEditor.h @@ -29,6 +29,7 @@ #include "../EventListener.h" #include "../Selection.h" #include "PatternEditor.h" +#include "NotePropertiesRuler.h" #include "../Widgets/WidgetWithScalableFont.h" #include @@ -48,31 +49,8 @@ class PianoRollEditor: public PatternEditor, protected WidgetWithScalableFont<7, // Implements EventListener interface virtual void selectedPatternChangedEvent() override; virtual void selectedInstrumentChangedEvent() override; - virtual void patternModifiedEvent() override; //~ Implements EventListener interface - // Pitch / line conversions - int lineToPitch( int nLine ) { - return 12 * (OCTAVE_MIN+m_nOctaves) - 1 - nLine; - } - int pitchToLine( int nPitch ) { - return 12 * (OCTAVE_MIN+m_nOctaves) - 1 - nPitch; - } - - H2Core::Note::Octave pitchToOctave( int nPitch ) { - if ( nPitch >= 0 ) { - return (H2Core::Note::Octave)(nPitch / 12); - } else { - return (H2Core::Note::Octave)((nPitch-11) / 12); - } - } - H2Core::Note::Key pitchToKey( int nPitch ) { - return (H2Core::Note::Key)(nPitch - 12 * pitchToOctave( nPitch )); - } - int octaveKeyToPitch( H2Core::Note::Octave octave, H2Core::Note::Key key ) { - return 12 * (int)octave + (int)key; - } - void addOrDeleteNoteAction( int nColumn, int pressedLine, int selectedPatternNumber, @@ -96,25 +74,15 @@ class PianoRollEditor: public PatternEditor, protected WidgetWithScalableFont<7, H2Core::Note::Key newKey, H2Core::Note *pNote); - void editNotePropertiesAction( int nColumn, - int nRealColumn, - int selectedPatternNumber, - int selectedInstrumentnumber, - float velocity, - float fPan, - float leadLag, - int pressedLine ); - void editNoteLengthAction( int nColumn, int nRealColumn, int length, int selectedPatternNumber, int nSelectedInstrumentnumber, int pressedLine ); - // Selection manager interface //! Selections are indexed by Note pointers. virtual std::vector elementsIntersecting( QRect r ) override; virtual void mouseClickEvent( QMouseEvent *ev ) override; + virtual void mousePressEvent( QMouseEvent *ev ) override; virtual void mouseDragStartEvent( QMouseEvent *ev ) override; virtual void mouseDragUpdateEvent( QMouseEvent *ev ) override; - virtual void mouseDragEndEvent( QMouseEvent *ev ) override; virtual void selectionMoveEndEvent( QInputEvent *ev ) override; virtual QRect getKeyboardCursorRect() override; @@ -127,8 +95,7 @@ class PianoRollEditor: public PatternEditor, protected WidgetWithScalableFont<7, void onPreferencesChanged( H2Core::Preferences::Changes changes ); private: - - void createBackground(); + void createBackground() override; void drawPattern(); void drawFocus( QPainter& painter ); void drawNote( H2Core::Note *pNote, QPainter *pPainter, bool bIsForeground ); @@ -139,37 +106,20 @@ class PianoRollEditor: public PatternEditor, protected WidgetWithScalableFont<7, virtual void paintEvent(QPaintEvent *ev) override; virtual void keyPressEvent ( QKeyEvent * ev ) override; - virtual void focusInEvent ( QFocusEvent * ev ) override; void finishUpdateEditor(); bool m_bNeedsUpdate; bool m_bNeedsBackgroundUpdate; - - unsigned m_nOctaves; QPixmap *m_pBackground; QPixmap *m_pTemp; - int m_nOldPoint; // Note pitch position of cursor int m_nCursorPitch; QPoint cursorPosition(); QScrollArea *m_pScrollView; - - int m_nSelectedInstrumentNumber = 0; - int m_nRealColumn = 0; - int m_nColumn = 0; - int m_nPressedLine = 0; - int m_nOldLength = 0; - - float m_fVelocity = 0; - float m_fOldVelocity = 0; - float m_fPan = 0; - float m_fOldPan = 0; - float m_fLeadLag = 0; - float m_fOldLeadLag = 0; }; #endif diff --git a/src/gui/src/PreferencesDialog/PreferencesDialog.cpp b/src/gui/src/PreferencesDialog/PreferencesDialog.cpp index 5cdbba7ca7..1c23f7ceb1 100644 --- a/src/gui/src/PreferencesDialog/PreferencesDialog.cpp +++ b/src/gui/src/PreferencesDialog/PreferencesDialog.cpp @@ -540,56 +540,70 @@ PreferencesDialog::PreferencesDialog(QWidget* parent) ColorTreeItem* pTopLevelItem; colorTree->clear(); - pTopLevelItem = new ColorTreeItem( 0x000, colorTree, "General" ); - new ColorTreeItem( 0x100, pTopLevelItem, "Window" ); - new ColorTreeItem( 0x101, pTopLevelItem, "Window Text" ); - new ColorTreeItem( 0x102, pTopLevelItem, "Base" ); - new ColorTreeItem( 0x103, pTopLevelItem, "Alternate Base" ); - new ColorTreeItem( 0x104, pTopLevelItem, "Text" ); - new ColorTreeItem( 0x105, pTopLevelItem, "Button" ); - new ColorTreeItem( 0x106, pTopLevelItem, "Button Text" ); - new ColorTreeItem( 0x107, pTopLevelItem, "Light" ); - new ColorTreeItem( 0x108, pTopLevelItem, "Mid Light" ); - new ColorTreeItem( 0x109, pTopLevelItem, "Mid" ); - new ColorTreeItem( 0x10a, pTopLevelItem, "Dark" ); - new ColorTreeItem( 0x10b, pTopLevelItem, "Shadow Text" ); - new ColorTreeItem( 0x10c, pTopLevelItem, "Highlight" ); - new ColorTreeItem( 0x10d, pTopLevelItem, "Highlight Text" ); - new ColorTreeItem( 0x10e, pTopLevelItem, "Selection Highlight" ); - new ColorTreeItem( 0x10f, pTopLevelItem, "Selection Inactive" ); - new ColorTreeItem( 0x110, pTopLevelItem, "Tool Tip Base" ); - new ColorTreeItem( 0x111, pTopLevelItem, "Tool Tip Text" ); + pTopLevelItem = new ColorTreeItem( 0x000, colorTree, tr( "General" ) ); + new ColorTreeItem( 0x100, pTopLevelItem, tr( "Window" ) ); + new ColorTreeItem( 0x101, pTopLevelItem, tr( "Window Text" ) ); + new ColorTreeItem( 0x102, pTopLevelItem, tr( "Base" ) ); + new ColorTreeItem( 0x103, pTopLevelItem, tr( "Alternate Base" ) ); + new ColorTreeItem( 0x104, pTopLevelItem, tr( "Text" ) ); + new ColorTreeItem( 0x105, pTopLevelItem, tr( "Button" ) ); + new ColorTreeItem( 0x106, pTopLevelItem, tr( "Button Text" ) ); + new ColorTreeItem( 0x107, pTopLevelItem, tr( "Light" ) ); + new ColorTreeItem( 0x108, pTopLevelItem, tr( "Mid Light" ) ); + new ColorTreeItem( 0x109, pTopLevelItem, tr( "Mid" ) ); + new ColorTreeItem( 0x10a, pTopLevelItem, tr( "Dark" ) ); + new ColorTreeItem( 0x10b, pTopLevelItem, tr( "Shadow Text" ) ); + new ColorTreeItem( 0x10c, pTopLevelItem, tr( "Highlight" ) ); + new ColorTreeItem( 0x10d, pTopLevelItem, tr( "Highlight Text" ) ); + new ColorTreeItem( 0x10e, pTopLevelItem, tr( "Selection Highlight" ) ); + new ColorTreeItem( 0x10f, pTopLevelItem, tr( "Selection Inactive" ) ); + new ColorTreeItem( 0x110, pTopLevelItem, tr( "Tool Tip Base" ) ); + new ColorTreeItem( 0x111, pTopLevelItem, tr( "Tool Tip Text" ) ); - pTopLevelItem = new ColorTreeItem( 0x000, colorTree, "Widgets" ); - new ColorTreeItem( 0x200, pTopLevelItem, "Widget" ); - new ColorTreeItem( 0x201, pTopLevelItem, "Widget Text" ); - new ColorTreeItem( 0x202, pTopLevelItem, "Accent" ); - new ColorTreeItem( 0x203, pTopLevelItem, "Accent Text" ); - new ColorTreeItem( 0x204, pTopLevelItem, "Button Red" ); - new ColorTreeItem( 0x205, pTopLevelItem, "Button Red Text" ); - new ColorTreeItem( 0x206, pTopLevelItem, "Spin Box" ); - new ColorTreeItem( 0x207, pTopLevelItem, "Spin Box Text" ); - new ColorTreeItem( 0x208, pTopLevelItem, "Automation" ); - new ColorTreeItem( 0x209, pTopLevelItem, "Automation Circle" ); - pTopLevelItem = new ColorTreeItem( 0x000, colorTree, "Song Editor" ); - new ColorTreeItem( 0x300, pTopLevelItem, "Background" ); - new ColorTreeItem( 0x301, pTopLevelItem, "Alternate Row" ); - new ColorTreeItem( 0x302, pTopLevelItem, "Selected Row" ); - new ColorTreeItem( 0x303, pTopLevelItem, "Line" ); - new ColorTreeItem( 0x304, pTopLevelItem, "Text" ); - pTopLevelItem = new ColorTreeItem( 0x000, colorTree, "Pattern Editor" ); - new ColorTreeItem( 0x400, pTopLevelItem, "Background" ); - new ColorTreeItem( 0x401, pTopLevelItem, "Alternate Row" ); - new ColorTreeItem( 0x402, pTopLevelItem, "Selected Row" ); - new ColorTreeItem( 0x403, pTopLevelItem, "Text" ); - new ColorTreeItem( 0x404, pTopLevelItem, "Note" ); - new ColorTreeItem( 0x405, pTopLevelItem, "Note Off" ); - new ColorTreeItem( 0x406, pTopLevelItem, "Line" ); - new ColorTreeItem( 0x407, pTopLevelItem, "Line 1" ); - new ColorTreeItem( 0x408, pTopLevelItem, "Line 2" ); - new ColorTreeItem( 0x409, pTopLevelItem, "Line 3" ); - new ColorTreeItem( 0x40a, pTopLevelItem, "Line 4" ); - new ColorTreeItem( 0x40b, pTopLevelItem, "Line 5" ); + pTopLevelItem = new ColorTreeItem( 0x000, colorTree, tr( "Widgets" ) ); + new ColorTreeItem( 0x200, pTopLevelItem, tr( "Widget" ) ); + new ColorTreeItem( 0x201, pTopLevelItem, tr( "Widget Text" ) ); + new ColorTreeItem( 0x202, pTopLevelItem, tr( "Accent" ) ); + new ColorTreeItem( 0x203, pTopLevelItem, tr( "Accent Text" ) ); + new ColorTreeItem( 0x204, pTopLevelItem, tr( "Button Red" ) ); + new ColorTreeItem( 0x205, pTopLevelItem, tr( "Button Red Text" ) ); + new ColorTreeItem( 0x206, pTopLevelItem, tr( "Spin Box" ) ); + new ColorTreeItem( 0x207, pTopLevelItem, tr( "Spin Box Text" ) ); + new ColorTreeItem( 0x208, pTopLevelItem, tr( "Playhead" ) ); + new ColorTreeItem( 0x209, pTopLevelItem, tr( "Cursor" ) ); + + pTopLevelItem = new ColorTreeItem( 0x000, colorTree, tr( "Song Editor" ) ); + new ColorTreeItem( 0x300, pTopLevelItem, tr( "Background" ) ); + new ColorTreeItem( 0x301, pTopLevelItem, tr( "Alternate Row" ) ); + new ColorTreeItem( 0x302, pTopLevelItem, tr( "Selected Row" ) ); + new ColorTreeItem( 0x303, pTopLevelItem, tr( "Selected Row Text" ) ); + new ColorTreeItem( 0x304, pTopLevelItem, tr( "Line" ) ); + new ColorTreeItem( 0x305, pTopLevelItem, tr( "Text" ) ); + new ColorTreeItem( 0x306, pTopLevelItem, tr( "Automation Background" ) ); + new ColorTreeItem( 0x307, pTopLevelItem, tr( "Automation Line" ) ); + new ColorTreeItem( 0x308, pTopLevelItem, tr( "Automation Node" ) ); + new ColorTreeItem( 0x309, pTopLevelItem, tr( "Stacked Mode On" ) ); + new ColorTreeItem( 0x30a, pTopLevelItem, tr( "Stacked Mode On Next" ) ); + new ColorTreeItem( 0x30b, pTopLevelItem, tr( "Stacked Mode Off Next" ) ); + + pTopLevelItem = new ColorTreeItem( 0x000, colorTree, tr( "Pattern Editor" ) ); + new ColorTreeItem( 0x400, pTopLevelItem, tr( "Background" ) ); + new ColorTreeItem( 0x401, pTopLevelItem, tr( "Alternate Row" ) ); + new ColorTreeItem( 0x402, pTopLevelItem, tr( "Selected Row" ) ); + new ColorTreeItem( 0x403, pTopLevelItem, tr( "Selected Row Text" ) ); + new ColorTreeItem( 0x404, pTopLevelItem, tr( "Octave Row" ) ); + new ColorTreeItem( 0x405, pTopLevelItem, tr( "Text" ) ); + new ColorTreeItem( 0x406, pTopLevelItem, tr( "Note (Full Velocity)" ) ); + new ColorTreeItem( 0x407, pTopLevelItem, tr( "Note (Default Velocity)" ) ); + new ColorTreeItem( 0x408, pTopLevelItem, tr( "Note (Half Velocity)" ) ); + new ColorTreeItem( 0x409, pTopLevelItem, tr( "Note (Zero Velocity)" ) ); + new ColorTreeItem( 0x40a, pTopLevelItem, tr( "Note Off" ) ); + new ColorTreeItem( 0x40b, pTopLevelItem, tr( "Grid Line 1" ) ); + new ColorTreeItem( 0x40c, pTopLevelItem, tr( "Grid Line 2" ) ); + new ColorTreeItem( 0x40d, pTopLevelItem, tr( "Grid Line 3" ) ); + new ColorTreeItem( 0x40e, pTopLevelItem, tr( "Grid Line 4" ) ); + new ColorTreeItem( 0x40f, pTopLevelItem, tr( "Grid Line 5" ) ); + new ColorTreeItem( 0x410, pTopLevelItem, tr( "Grid Line 6" ) ); colorButton->setEnabled( false ); @@ -1545,25 +1559,37 @@ QColor* PreferencesDialog::getColorById( int nId, std::shared_ptrm_buttonRedTextColor; case 0x206: return &pColorTheme->m_spinBoxColor; case 0x207: return &pColorTheme->m_spinBoxTextColor; - case 0x208: return &pColorTheme->m_automationColor; - case 0x209: return &pColorTheme->m_automationCircleColor; + case 0x208: return &pColorTheme->m_playheadColor; + case 0x209: return &pColorTheme->m_cursorColor; case 0x300: return &pColorTheme->m_songEditor_backgroundColor; case 0x301: return &pColorTheme->m_songEditor_alternateRowColor; case 0x302: return &pColorTheme->m_songEditor_selectedRowColor; - case 0x303: return &pColorTheme->m_songEditor_lineColor; - case 0x304: return &pColorTheme->m_songEditor_textColor; + case 0x303: return &pColorTheme->m_songEditor_selectedRowTextColor; + case 0x304: return &pColorTheme->m_songEditor_lineColor; + case 0x305: return &pColorTheme->m_songEditor_textColor; + case 0x306: return &pColorTheme->m_songEditor_automationBackgroundColor; + case 0x307: return &pColorTheme->m_songEditor_automationLineColor; + case 0x308: return &pColorTheme->m_songEditor_automationNodeColor; + case 0x309: return &pColorTheme->m_songEditor_stackedModeOnColor; + case 0x30a: return &pColorTheme->m_songEditor_stackedModeOnNextColor; + case 0x30b: return &pColorTheme->m_songEditor_stackedModeOffNextColor; case 0x400: return &pColorTheme->m_patternEditor_backgroundColor; case 0x401: return &pColorTheme->m_patternEditor_alternateRowColor; case 0x402: return &pColorTheme->m_patternEditor_selectedRowColor; - case 0x403: return &pColorTheme->m_patternEditor_textColor; - case 0x404: return &pColorTheme->m_patternEditor_noteColor; - case 0x405: return &pColorTheme->m_patternEditor_noteoffColor; - case 0x406: return &pColorTheme->m_patternEditor_lineColor; - case 0x407: return &pColorTheme->m_patternEditor_line1Color; - case 0x408: return &pColorTheme->m_patternEditor_line2Color; - case 0x409: return &pColorTheme->m_patternEditor_line3Color; - case 0x40a: return &pColorTheme->m_patternEditor_line4Color; - case 0x40b: return &pColorTheme->m_patternEditor_line5Color; + case 0x403: return &pColorTheme->m_patternEditor_selectedRowTextColor; + case 0x404: return &pColorTheme->m_patternEditor_octaveRowColor; + case 0x405: return &pColorTheme->m_patternEditor_textColor; + case 0x406: return &pColorTheme->m_patternEditor_noteVelocityFullColor; + case 0x407: return &pColorTheme->m_patternEditor_noteVelocityDefaultColor; + case 0x408: return &pColorTheme->m_patternEditor_noteVelocityHalfColor; + case 0x409: return &pColorTheme->m_patternEditor_noteVelocityZeroColor; + case 0x40a: return &pColorTheme->m_patternEditor_noteOffColor; + case 0x40b: return &pColorTheme->m_patternEditor_lineColor; + case 0x40c: return &pColorTheme->m_patternEditor_line1Color; + case 0x40d: return &pColorTheme->m_patternEditor_line2Color; + case 0x40e: return &pColorTheme->m_patternEditor_line3Color; + case 0x40f: return &pColorTheme->m_patternEditor_line4Color; + case 0x410: return &pColorTheme->m_patternEditor_line5Color; default: return nullptr; } @@ -1625,9 +1651,9 @@ void PreferencesDialog::setColorById( int nId, const QColor& color, break; case 0x207: pColorTheme->m_spinBoxTextColor = color; break; - case 0x208: pColorTheme->m_automationColor = color; + case 0x208: pColorTheme->m_playheadColor = color; break; - case 0x209: pColorTheme->m_automationCircleColor = color; + case 0x209: pColorTheme->m_cursorColor = color; break; case 0x300: pColorTheme->m_songEditor_backgroundColor = color; break; @@ -1635,9 +1661,23 @@ void PreferencesDialog::setColorById( int nId, const QColor& color, break; case 0x302: pColorTheme->m_songEditor_selectedRowColor = color; break; - case 0x303: pColorTheme->m_songEditor_lineColor = color; + case 0x303: pColorTheme->m_songEditor_selectedRowTextColor = color; + break; + case 0x304: pColorTheme->m_songEditor_lineColor = color; + break; + case 0x305: pColorTheme->m_songEditor_textColor = color; + break; + case 0x306: pColorTheme->m_songEditor_automationBackgroundColor = color; + break; + case 0x307: pColorTheme->m_songEditor_automationLineColor = color; + break; + case 0x308: pColorTheme->m_songEditor_automationNodeColor = color; break; - case 0x304: pColorTheme->m_songEditor_textColor = color; + case 0x309: pColorTheme->m_songEditor_stackedModeOnColor = color; + break; + case 0x30a: pColorTheme->m_songEditor_stackedModeOnNextColor = color; + break; + case 0x30b: pColorTheme->m_songEditor_stackedModeOffNextColor = color; break; case 0x400: pColorTheme->m_patternEditor_backgroundColor = color; break; @@ -1645,23 +1685,33 @@ void PreferencesDialog::setColorById( int nId, const QColor& color, break; case 0x402: pColorTheme->m_patternEditor_selectedRowColor = color; break; - case 0x403: pColorTheme->m_patternEditor_textColor = color; + case 0x403: pColorTheme->m_patternEditor_selectedRowTextColor = color; + break; + case 0x404: pColorTheme->m_patternEditor_octaveRowColor = color; + break; + case 0x405: pColorTheme->m_patternEditor_textColor = color; + break; + case 0x406: pColorTheme->m_patternEditor_noteVelocityFullColor = color; + break; + case 0x407: pColorTheme->m_patternEditor_noteVelocityDefaultColor = color; + break; + case 0x408: pColorTheme->m_patternEditor_noteVelocityHalfColor = color; break; - case 0x404: pColorTheme->m_patternEditor_noteColor = color; + case 0x409: pColorTheme->m_patternEditor_noteVelocityZeroColor = color; break; - case 0x405: pColorTheme->m_patternEditor_noteoffColor = color; + case 0x40a: pColorTheme->m_patternEditor_noteOffColor = color; break; - case 0x406: pColorTheme->m_patternEditor_lineColor = color; + case 0x40b: pColorTheme->m_patternEditor_lineColor = color; break; - case 0x407: pColorTheme->m_patternEditor_line1Color = color; + case 0x40c: pColorTheme->m_patternEditor_line1Color = color; break; - case 0x408: pColorTheme->m_patternEditor_line2Color = color; + case 0x40d: pColorTheme->m_patternEditor_line2Color = color; break; - case 0x409: pColorTheme->m_patternEditor_line3Color = color; + case 0x40e: pColorTheme->m_patternEditor_line3Color = color; break; - case 0x40a: pColorTheme->m_patternEditor_line4Color = color; + case 0x40f: pColorTheme->m_patternEditor_line4Color = color; break; - case 0x40b: pColorTheme->m_patternEditor_line5Color = color; + case 0x410: pColorTheme->m_patternEditor_line5Color = color; break; default: DEBUGLOG( "Unknown ID" ); } diff --git a/src/gui/src/Selection.h b/src/gui/src/Selection.h index 496d99efb8..ce8771fcbf 100644 --- a/src/gui/src/Selection.h +++ b/src/gui/src/Selection.h @@ -34,6 +34,8 @@ #include #include +#include + //! SelectionWidget defines the interface used by the Selection manager to communicate with a widget //! implementing selection, and provides for event translation, testing for intersection with selectable //! objects, keyboard input cursor geometry, and screen refresh. It must be subclassed and @@ -478,7 +480,8 @@ class Selection { //! Paint selection-related elements (ie lasso) void paintSelection( QPainter *painter ) { if ( m_selectionState == MouseLasso || m_selectionState == KeyboardLasso ) { - QPen pen( Qt::white ); + QPen pen( H2Core::Preferences::get_instance()->getColorTheme() + ->m_selectionHighlightColor ); pen.setStyle( Qt::DotLine ); pen.setWidth(2); painter->setPen( pen ); diff --git a/src/gui/src/Skin.cpp b/src/gui/src/Skin.cpp index 61096d547e..65a97c760f 100644 --- a/src/gui/src/Skin.cpp +++ b/src/gui/src/Skin.cpp @@ -155,6 +155,29 @@ border-color: %3;" ) .arg( pPref->getColorTheme()->m_windowColor.name() ); } +void Skin::drawListBackground( QPainter* p, QRect rect, QColor background, + bool bHovered ) { + + if ( bHovered ) { + background = background.lighter( 110 ); + } + + QColor backgroundLight = background.lighter( 150 ); + QColor backgroundDark = background.darker( 220 ); + + p->fillRect( QRect( rect.x() + 1, rect.y() + 1, + rect.width() - 2, rect.height() - 2 ), + background ); + p->fillRect( QRect( rect.x(), rect.y(), rect.width(), 1 ), + backgroundLight ); + p->fillRect( QRect( rect.x(), rect.y(), 1, rect.height() ), + backgroundLight ); + p->fillRect( QRect( rect.x(), rect.y() + rect.height() - 1, rect.width(), 1 ), + backgroundDark ); + p->fillRect( QRect( rect.x() + rect.width() - 1, rect.y(), 1, rect.height() ), + backgroundDark ); +} + QColor Skin::makeWidgetColorInactive( QColor color ){ int nHue, nSaturation, nValue; color.getHsv( &nHue, &nSaturation, &nValue ); @@ -177,3 +200,73 @@ QColor Skin::makeTextColorInactive( QColor color ) { return color; } + +void Skin::setPlayheadPen( QPainter* p, bool bHovered ) { + + QColor playheadColor( H2Core::Preferences::get_instance()->getColorTheme()->m_playheadColor ); + if ( bHovered ) { + playheadColor = Skin::makeTextColorInactive( playheadColor ); + } + QPen pen ( playheadColor ); + pen.setWidth( 2 ); + + p->setPen( pen ); + p->setRenderHint( QPainter::Antialiasing ); +} + +void Skin::drawPlayhead( QPainter* p, int x, int y, bool bHovered ) { + + const QPointF points[3] = { + QPointF( x, y ), + QPointF( x + Skin::nPlayheadWidth - 1, y ), + QPointF( x + Skin::getPlayheadShaftOffset(), + y + Skin::nPlayheadHeight ), + }; + + QColor playheadColor( H2Core::Preferences::get_instance()->getColorTheme()->m_playheadColor ); + if ( bHovered ) { + playheadColor = Skin::makeTextColorInactive( playheadColor ); + } + + Skin::setPlayheadPen( p, bHovered ); + p->setBrush( playheadColor ); + p->drawPolygon( points, 3 ); + p->setBrush( Qt::NoBrush ); +} + +void Skin::drawStackedIndicator( QPainter* p, int x, int y, Skin::Stacked stacked ) { + + auto pPref = H2Core::Preferences::get_instance(); + + const QPointF points[3] = { + QPointF( x, y ), + QPointF( x + 8, y + 6 ), + QPointF( x, y + 12 ) + }; + + QPen pen( Qt::black ); + pen.setWidth( 1 ); + + QColor fillColor; + switch ( stacked ) { + case Skin::Stacked::Off: + fillColor = QColor( 0, 0, 0 ); + fillColor.setAlpha( 0 ); + break; + case Skin::Stacked::OffNext: + fillColor = pPref->getColorTheme()->m_songEditor_stackedModeOffNextColor; + break; + case Skin::Stacked::On: + fillColor = pPref->getColorTheme()->m_songEditor_stackedModeOnColor; + break; + case Skin::Stacked::OnNext: + fillColor = pPref->getColorTheme()->m_songEditor_stackedModeOnNextColor; + break; + } + + p->setPen( pen ); + p->setBrush( fillColor ); + p->setRenderHint( QPainter::Antialiasing ); + p->drawPolygon( points, 3 ); + p->setBrush( Qt::NoBrush ); +} diff --git a/src/gui/src/Skin.h b/src/gui/src/Skin.h index 7117d9a66f..c62d8417f1 100644 --- a/src/gui/src/Skin.h +++ b/src/gui/src/Skin.h @@ -68,12 +68,41 @@ class Skin */ static QString getWarningButtonStyleSheet( int nSize ); + /** + * Draws the background of a row in both the pattern list of the + * SongEditor and the instrument list in the PatternEditor using + * @a p. + * + * \param p Painter used in the calling QPaintEvent routine. + * \param rect Boundary that encloses element (one row). + * \param background Color used. + * \param bHovered Whether the element is currently hovered by mouse. + */ + static void drawListBackground( QPainter* p, QRect rect, QColor background, + bool bHovered ); /** If a widget is marked inactive the value of its background color are reduced by this factor.*/ static QColor makeWidgetColorInactive( QColor color ); /** If a widget is marked inactive the value of its text color are reduced by this factor.*/ static QColor makeTextColorInactive( QColor color ); + + static constexpr int nPlayheadWidth = 11; + static constexpr int nPlayheadHeight = 8; + static int getPlayheadShaftOffset() { + return std::floor( Skin::nPlayheadWidth / 2 ); } + static void setPlayheadPen( QPainter* p, bool bHovered = false ); + static void drawPlayhead( QPainter* p, int x, int y, bool bHovered = false ); + + enum class Stacked { + None, + Off, + OffNext, + On, + OnNext + }; + + static void drawStackedIndicator( QPainter* p, int x, int y, Skin::Stacked stacked ); }; diff --git a/src/gui/src/SongEditor/PlaybackTrackWaveDisplay.cpp b/src/gui/src/SongEditor/PlaybackTrackWaveDisplay.cpp index 66f3b089f4..50fa7c5b6c 100644 --- a/src/gui/src/SongEditor/PlaybackTrackWaveDisplay.cpp +++ b/src/gui/src/SongEditor/PlaybackTrackWaveDisplay.cpp @@ -33,6 +33,7 @@ using namespace H2Core; #include "../HydrogenApp.h" #include "../InstrumentEditor/WaveDisplay.h" +#include "../Skin.h" #include "PlaybackTrackWaveDisplay.h" #include "SongEditor.h" @@ -40,8 +41,12 @@ using namespace H2Core; PlaybackTrackWaveDisplay::PlaybackTrackWaveDisplay(QWidget* pParent) : WaveDisplay( pParent ) + , m_fTick( 0 ) { - + qreal pixelRatio = devicePixelRatio(); + m_pBackgroundPixmap = new QPixmap( width() * pixelRatio, + height() * pixelRatio ); + m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio ); setAcceptDrops(true); } @@ -81,13 +86,28 @@ void PlaybackTrackWaveDisplay::updateDisplay( std::shared_ptrgetColorTheme()->m_songEditor_backgroundColor; + // Resize pixmap if pixel ratio has changed + qreal pixelRatio = devicePixelRatio(); + if ( m_pBackgroundPixmap->devicePixelRatio() != pixelRatio || + width() != m_pBackgroundPixmap->width() || + height() != m_pBackgroundPixmap->height() ) { + delete m_pBackgroundPixmap; + m_pBackgroundPixmap = new QPixmap( width() * pixelRatio , height() * pixelRatio ); + m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio ); + } + int currentWidth = width(); - - if(!pLayer || currentWidth <= 0){ + + if( pLayer == nullptr || currentWidth <= 0 ){ m_pLayer = nullptr; - m_sSampleName = "-"; - + m_sSampleName = tr( "No playback track selected" ); + + QPainter painter( m_pBackgroundPixmap ); + createBackground( &painter ); + update(); return; } @@ -145,7 +165,9 @@ void PlaybackTrackWaveDisplay::updateDisplay( std::shared_ptrgetBpm() * 2) / 60) float fLengthOfCurrentPatternInSecs = (maxPatternSize/24) / ((pSong->getBpm() * 2) / 60); @@ -189,6 +211,51 @@ void PlaybackTrackWaveDisplay::updateDisplay( std::shared_ptrdevicePixelRatio() || + width() != m_pBackgroundPixmap->width() || + height() != m_pBackgroundPixmap->height() ) { + updateDisplay( m_pLayer ); + } + + // Render the wave display. + painter.drawPixmap( ev->rect(), *m_pBackgroundPixmap, + QRectF( pixelRatio * ev->rect().x(), + pixelRatio * ev->rect().y(), + pixelRatio * ev->rect().width(), + pixelRatio * ev->rect().height() ) ); + + // Draw playhead + auto pSongEditorPanel = HydrogenApp::get_instance()->getSongEditorPanel(); + if ( m_fTick != -1 && pSongEditorPanel != nullptr ) { + if ( pSongEditorPanel->getSongEditor() != nullptr ) { + int nX = static_cast( static_cast(SongEditor::nMargin) + 1 + + m_fTick * + static_cast(pSongEditorPanel->getSongEditor()-> + getGridWidth()) - + static_cast(Skin::nPlayheadWidth) / 2 ); + int nOffset = Skin::getPlayheadShaftOffset(); + Skin::setPlayheadPen( &painter, false ); + painter.drawLine( nX + nOffset, 0, nX + nOffset, height() ); + } + } + +} diff --git a/src/gui/src/SongEditor/PlaybackTrackWaveDisplay.h b/src/gui/src/SongEditor/PlaybackTrackWaveDisplay.h index 93670bb44a..62aaeb09d5 100644 --- a/src/gui/src/SongEditor/PlaybackTrackWaveDisplay.h +++ b/src/gui/src/SongEditor/PlaybackTrackWaveDisplay.h @@ -46,11 +46,18 @@ class PlaybackTrackWaveDisplay : public WaveDisplay ~PlaybackTrackWaveDisplay() = default; void updateDisplay( std::shared_ptr pLayer ) override; + void updatePosition( float fTick ); public slots: virtual void dragMoveEvent(QDragMoveEvent *event) override; virtual void dropEvent(QDropEvent *event) override; virtual void dragEnterEvent(QDragEnterEvent * event) override; + virtual void paintEvent(QPaintEvent * event) override; + +private: + QPixmap *m_pBackgroundPixmap; + /** Cached position of the playhead.*/ + float m_fTick; }; #endif diff --git a/src/gui/src/SongEditor/SongEditor.cpp b/src/gui/src/SongEditor/SongEditor.cpp index f816c744b6..87c7b70838 100644 --- a/src/gui/src/SongEditor/SongEditor.cpp +++ b/src/gui/src/SongEditor/SongEditor.cpp @@ -33,7 +33,6 @@ #include #include #include -#include #include using namespace H2Core; @@ -44,6 +43,7 @@ using namespace H2Core; #include "SongEditorPanelBpmWidget.h" #include "SongEditorPanelTagWidget.h" #include "PatternFillDialog.h" +#include "PlaybackTrackWaveDisplay.h" #include "VirtualPatternDialog.h" #include "SoundLibrary/SoundLibraryPanel.h" #include "SoundLibrary/SoundLibraryDatastructures.h" @@ -101,7 +101,7 @@ SongEditor::SongEditor( QWidget *parent, QScrollArea *pScrollView, SongEditorPan m_nCursorColumn = 0; m_nMaxPatternSequence = pPref->getMaxBars(); - int m_nInitialWidth = m_nMargin + m_nMaxPatternSequence * m_nGridWidth; + int m_nInitialWidth = SongEditor::nMargin + m_nMaxPatternSequence * m_nGridWidth; int m_nInitialHeight = 10; this->resize( QSize(m_nInitialWidth, m_nInitialHeight) ); @@ -118,8 +118,7 @@ SongEditor::SongEditor( QWidget *parent, QScrollArea *pScrollView, SongEditorPan m_pPopupMenu->addAction( tr( "Clear selection" ), this, &SongEditor::selectNone ); m_pPopupMenu->setObjectName( "SongEditorPopup" ); - - update(); + HydrogenApp::get_instance()->addEventListener( this ); } @@ -296,18 +295,19 @@ void SongEditor::setGridWidth( uint width ) { if ( ( SONG_EDITOR_MIN_GRID_WIDTH <= width ) && ( SONG_EDITOR_MAX_GRID_WIDTH >= width ) ) { m_nGridWidth = width; - this->resize ( m_nMargin + m_nMaxPatternSequence * m_nGridWidth, height() ); + this->resize ( SongEditor::nMargin + + m_nMaxPatternSequence * m_nGridWidth, height() ); } } QPoint SongEditor::xyToColumnRow( QPoint p ) { - return QPoint( (p.x() - m_nMargin) / (int)m_nGridWidth, p.y() / (int)m_nGridHeight ); + return QPoint( (p.x() - SongEditor::nMargin) / (int)m_nGridWidth, p.y() / (int)m_nGridHeight ); } QPoint SongEditor::columnRowToXy( QPoint p ) { - return QPoint( m_nMargin + p.x() * m_nGridWidth, p.y() * m_nGridHeight ); + return QPoint( SongEditor::nMargin + p.x() * m_nGridWidth, p.y() * m_nGridHeight ); } @@ -476,19 +476,23 @@ void SongEditor::cut() { void SongEditor::keyPressEvent( QKeyEvent * ev ) { - Hydrogen* pHydrogen = Hydrogen::get_instance(); + auto pHydrogenApp = HydrogenApp::get_instance(); + auto pHydrogen = Hydrogen::get_instance(); + auto pSong = pHydrogen->getSong(); const int nBlockSize = 5, nWordSize = 5; bool bIsSelectionKey = false; bool bUnhideCursor = true; - H2Core::Song::ActionMode actionMode = m_pHydrogen->getSong()->getActionMode(); + bool bOldCursorHidden = pHydrogenApp->hideKeyboardCursor(); + + H2Core::Song::ActionMode actionMode = pHydrogen->getActionMode(); if ( actionMode == H2Core::Song::ActionMode::selectMode ) { bIsSelectionKey = m_selection.keyPressEvent( ev ); } - PatternList *pPatternList = pHydrogen->getSong()->getPatternList(); + PatternList *pPatternList = pSong->getPatternList(); const QPoint centre = QPoint( m_nGridWidth / 2, m_nGridHeight / 2 ); bool bSelectionKey = false; @@ -612,18 +616,24 @@ void SongEditor::keyPressEvent( QKeyEvent * ev ) } else { ev->ignore(); - HydrogenApp::get_instance()->setHideKeyboardCursor( true ); + pHydrogenApp->setHideKeyboardCursor( true ); + + if ( bOldCursorHidden != pHydrogenApp->hideKeyboardCursor() ) { + pHydrogenApp->getSongEditorPanel()->getSongEditorPatternList()->update(); + pHydrogenApp->getSongEditorPanel()->getSongEditorPositionRuler()->update(); + update(); + } return; } if ( bUnhideCursor ) { - HydrogenApp::get_instance()->setHideKeyboardCursor( false ); + pHydrogenApp->setHideKeyboardCursor( false ); } if ( bSelectionKey ) { // If a "select" key movement is used in "draw" mode, it's probably a good idea to go straight into // "select" mode. if ( actionMode == H2Core::Song::ActionMode::drawMode ) { - Hydrogen::get_instance()->getSong()->setActionMode( H2Core::Song::ActionMode::selectMode ); + pHydrogen->setActionMode( H2Core::Song::ActionMode::selectMode ); } // Any selection key may need a repaint of the selection m_bSequenceChanged = true; @@ -636,6 +646,11 @@ void SongEditor::keyPressEvent( QKeyEvent * ev ) QPoint cursorCentre = columnRowToXy( QPoint( m_nCursorColumn, m_nCursorRow ) ) + centre; m_pScrollView->ensureVisible( cursorCentre.x(), cursorCentre.y() ); m_selection.updateKeyboardCursorPosition( getKeyboardCursorRect() ); + + if ( ! pHydrogenApp->hideKeyboardCursor() ) { + pHydrogenApp->getSongEditorPanel()->getSongEditorPatternList()->update(); + pHydrogenApp->getSongEditorPanel()->getSongEditorPositionRuler()->update(); + } update(); ev->accept(); } @@ -653,7 +668,32 @@ void SongEditor::focusInEvent( QFocusEvent *ev ) m_pScrollView->ensureVisible( pos.x(), pos.y() ); HydrogenApp::get_instance()->setHideKeyboardCursor( false ); } + + // If there are some patterns selected, we have to switch their + // border color inactive <-> active. + createBackground(); + update(); + + if ( ! HydrogenApp::get_instance()->hideKeyboardCursor() ) { + HydrogenApp::get_instance()->getSongEditorPanel()->getSongEditorPatternList()->update(); + HydrogenApp::get_instance()->getSongEditorPanel()->getSongEditorPositionRuler()->update(); + } +} + +// Make cursor hidden +void SongEditor::focusOutEvent( QFocusEvent *ev ) +{ + UNUSED( ev ); + + // If there are some patterns selected, we have to switch their + // border color inactive <-> active. + createBackground(); update(); + + if ( ! HydrogenApp::get_instance()->hideKeyboardCursor() ) { + HydrogenApp::get_instance()->getSongEditorPanel()->getSongEditorPatternList()->update(); + HydrogenApp::get_instance()->getSongEditorPanel()->getSongEditorPositionRuler()->update(); + } } @@ -670,6 +710,7 @@ int operator<( QPoint a, QPoint b ) { void SongEditor::mousePressEvent( QMouseEvent *ev ) { + auto pHydrogenApp = HydrogenApp::get_instance(); updateModifiers( ev ); m_currentMousePosition = ev->pos(); m_bSequenceChanged = true; @@ -678,10 +719,18 @@ void SongEditor::mousePressEvent( QMouseEvent *ev ) QPoint p = xyToColumnRow( ev->pos() ); m_nCursorColumn = p.x(); m_nCursorRow = p.y(); - HydrogenApp::get_instance()->setHideKeyboardCursor( true ); - if ( Hydrogen::get_instance()->getSong()->getActionMode() == H2Core::Song::ActionMode::selectMode ) { + bool bOldCursorHidden = pHydrogenApp->hideKeyboardCursor(); + + pHydrogenApp->setHideKeyboardCursor( true ); + + if ( Hydrogen::get_instance()->getActionMode() == H2Core::Song::ActionMode::selectMode ) { m_selection.mousePressEvent( ev ); + if ( ! pHydrogenApp->hideKeyboardCursor() ) { + pHydrogenApp->getSongEditorPanel()->getSongEditorPatternList()->update(); + pHydrogenApp->getSongEditorPanel()->getSongEditorPositionRuler()->update(); + update(); + } } else { if ( ev->button() == Qt::LeftButton ) { @@ -695,6 +744,13 @@ void SongEditor::mousePressEvent( QMouseEvent *ev ) m_pPopupMenu->popup( ev->globalPos() ); } } + + // Cursor just got hidden. + if ( bOldCursorHidden != pHydrogenApp->hideKeyboardCursor() ) { + pHydrogenApp->getSongEditorPanel()->getSongEditorPatternList()->update(); + pHydrogenApp->getSongEditorPanel()->getSongEditorPositionRuler()->update(); + update(); + } } @@ -729,14 +785,16 @@ void SongEditor::updateModifiers( QInputEvent *ev ) void SongEditor::mouseMoveEvent(QMouseEvent *ev) { + auto pHydrogenApp = HydrogenApp::get_instance(); auto pSong = Hydrogen::get_instance()->getSong(); updateModifiers( ev ); m_currentMousePosition = ev->pos(); + bool bOldCursorHidden = pHydrogenApp->hideKeyboardCursor(); - if ( pSong->getActionMode() == H2Core::Song::ActionMode::selectMode ) { + if ( Hydrogen::get_instance()->getActionMode() == H2Core::Song::ActionMode::selectMode ) { m_selection.mouseMoveEvent( ev ); } else { - if ( ev->x() < m_nMargin ) { + if ( ev->x() < SongEditor::nMargin ) { return; } @@ -757,6 +815,13 @@ void SongEditor::mouseMoveEvent(QMouseEvent *ev) // Drawing mode: continue drawing over other cells setPatternActive( p.x(), p.y(), ! m_bDrawingActiveCell ); } + + // Cursor just got hidden. + if ( bOldCursorHidden != pHydrogenApp->hideKeyboardCursor() ) { + pHydrogenApp->getSongEditorPanel()->getSongEditorPatternList()->update(); + pHydrogenApp->getSongEditorPanel()->getSongEditorPositionRuler()->update(); + update(); + } } void SongEditor::mouseDragStartEvent( QMouseEvent *ev ) @@ -815,7 +880,7 @@ void SongEditor::selectionMoveEndEvent( QInputEvent *ev ) void SongEditor::mouseClickEvent( QMouseEvent *ev ) { - assert( m_pHydrogen->getSong()->getActionMode() == H2Core::Song::ActionMode::selectMode ); + assert( m_pHydrogen->getActionMode() == H2Core::Song::ActionMode::selectMode ); if ( ev->button() == Qt::LeftButton ) { QPoint p = xyToColumnRow( ev->pos() ); @@ -823,6 +888,10 @@ void SongEditor::mouseClickEvent( QMouseEvent *ev ) togglePatternActive( p.x(), p.y() ); m_bSequenceChanged = true; update(); + if ( ! HydrogenApp::get_instance()->hideKeyboardCursor() ) { + HydrogenApp::get_instance()->getSongEditorPanel()->getSongEditorPatternList()->update(); + HydrogenApp::get_instance()->getSongEditorPanel()->getSongEditorPositionRuler()->update(); + } } else if ( ev->button() == Qt::RightButton ) { m_pPopupMenu->popup( ev->globalPos() ); @@ -831,7 +900,7 @@ void SongEditor::mouseClickEvent( QMouseEvent *ev ) void SongEditor::mouseReleaseEvent( QMouseEvent *ev ) { - if ( m_pHydrogen->getSong()->getActionMode() == H2Core::Song::ActionMode::selectMode ) { + if ( m_pHydrogen->getActionMode() == H2Core::Song::ActionMode::selectMode ) { m_selection.mouseReleaseEvent( ev ); return; } @@ -888,6 +957,11 @@ void SongEditor::updateWidget() { m_previousMousePosition = m_currentMousePosition; } +void SongEditor::updatePosition( float fTick ) { + m_fTick = fTick; + update(); +} + void SongEditor::paintEvent( QPaintEvent *ev ) { @@ -896,6 +970,8 @@ void SongEditor::paintEvent( QPaintEvent *ev ) m_bSequenceChanged = false; drawSequence(); } + + auto pPref = Preferences::get_instance(); QPainter painter(this); painter.drawPixmap( ev->rect(), *m_pSequencePixmap, ev->rect() ); @@ -912,10 +988,23 @@ void SongEditor::paintEvent( QPaintEvent *ev ) painter.fillRect( r, patternColor ); } } + // Draw playhead + if ( m_fTick != -1 ) { + int nX = static_cast( static_cast(SongEditor::nMargin) + 1 + + m_fTick * static_cast(m_nGridWidth) - + static_cast(Skin::nPlayheadWidth) / 2 ); + int nOffset = Skin::getPlayheadShaftOffset(); + Skin::setPlayheadPen( &painter, false ); + painter.drawLine( nX + nOffset, 0, nX + nOffset, height() ); + } + + drawFocus( painter ); + + m_selection.paintSelection( &painter ); // Draw cursor if ( ! HydrogenApp::get_instance()->hideKeyboardCursor() && hasFocus() ) { - QPen p( Qt::black ); + QPen p( pPref->getColorTheme()->m_cursorColor ); p.setWidth( 2 ); painter.setPen( p ); painter.setRenderHint( QPainter::Antialiasing ); @@ -926,10 +1015,6 @@ void SongEditor::paintEvent( QPaintEvent *ev ) QSize( m_nGridWidth+1, m_nGridHeight-1 ) ), 4, 4 ); } - - drawFocus( painter ); - - m_selection.paintSelection( &painter ); } void SongEditor::drawFocus( QPainter& painter ) { @@ -985,6 +1070,7 @@ void SongEditor::createBackground() std::shared_ptr pSong = m_pHydrogen->getSong(); uint nPatterns = pSong->getPatternList()->size(); + int nSelectedPatternNumber = m_pHydrogen->getSelectedPatternNumber(); static int nOldHeight = -1; int nNewHeight = m_nGridHeight * nPatterns; @@ -1000,41 +1086,42 @@ void SongEditor::createBackground() this->resize( QSize( width(), nNewHeight ) ); } - m_pBackgroundPixmap->fill( pPref->getColorTheme()->m_songEditor_alternateRowColor ); + + m_pBackgroundPixmap->fill( pPref->getColorTheme()->m_songEditor_backgroundColor ); QPainter p( m_pBackgroundPixmap ); - p.setPen( pPref->getColorTheme()->m_songEditor_lineColor ); + + for ( int ii = 0; ii < nPatterns + 1; ii++) { + if ( ( ii % 2 ) == 0 && + ii != nSelectedPatternNumber ) { + continue; + } + + int y = m_nGridHeight * ii; + + if ( ii == nSelectedPatternNumber ) { + p.fillRect( 0, y, m_nMaxPatternSequence * m_nGridWidth, m_nGridHeight, + pPref->getColorTheme()->m_songEditor_selectedRowColor ); + } else { + p.fillRect( 0, y, m_nMaxPatternSequence * m_nGridWidth, m_nGridHeight, + pPref->getColorTheme()->m_songEditor_alternateRowColor ); + } + } - // vertical lines - for (uint i = 0; i < m_nMaxPatternSequence + 1; i++) { - uint x = m_nMargin + i * m_nGridWidth; - int x1 = x; - int x2 = x + m_nGridWidth; + p.setPen( QPen( pPref->getColorTheme()->m_songEditor_lineColor, 1, + Qt::SolidLine ) ); - p.drawLine( x1, 0, x1, m_nGridHeight * nPatterns ); - p.drawLine( x2, 0, x2, m_nGridHeight * nPatterns ); + // vertical lines + for ( float ii = 0; ii <= m_nMaxPatternSequence + 1; ii++) { + float x = SongEditor::nMargin + ii * m_nGridWidth; + p.drawLine( x, 0, x, m_nGridHeight * nPatterns ); } - - p.setPen( pPref->getColorTheme()->m_songEditor_lineColor ); + // horizontal lines for (uint i = 0; i < nPatterns; i++) { uint y = m_nGridHeight * i; - int y1 = y + 2; - int y2 = y + m_nGridHeight - 2; - - p.drawLine( 0, y1, (m_nMaxPatternSequence * m_nGridWidth), y1 ); - p.drawLine( 0, y2, (m_nMaxPatternSequence * m_nGridWidth), y2 ); - } - - - p.setPen( pPref->getColorTheme()->m_songEditor_backgroundColor ); - // horizontal lines (erase..) - for (uint i = 0; i < nPatterns + 1; i++) { - uint y = m_nGridHeight * i; - - p.fillRect( 0, y, m_nMaxPatternSequence * m_nGridWidth, 2, pPref->getColorTheme()->m_songEditor_backgroundColor ); - p.drawLine( 0, y + m_nGridHeight - 1, m_nMaxPatternSequence * m_nGridWidth, y + m_nGridHeight - 1 ); + p.drawLine( 0, y, (m_nMaxPatternSequence * m_nGridWidth), y ); } //~ celle @@ -1104,7 +1191,19 @@ void SongEditor::drawSequence() // Draw using GridCells representation for ( auto it : m_gridCells ) { - drawPattern( it.first.x(), it.first.y(), it.second.m_bDrawnVirtual, it.second.m_fWidth ); + if ( ! m_selection.isSelected( QPoint( it.first.x(), it.first.y() ) ) ) { + drawPattern( it.first.x(), it.first.y(), + it.second.m_bDrawnVirtual, it.second.m_fWidth ); + } + } + // We draw all selected patterns in a second run to ensure their + // border does have the proper color (else the bottom and left one + // could be overwritten by an adjecent, unselected pattern). + for ( auto it : m_gridCells ) { + if ( m_selection.isSelected( QPoint( it.first.x(), it.first.y() ) ) ) { + drawPattern( it.first.x(), it.first.y(), + it.second.m_bDrawnVirtual, it.second.m_fWidth ); + } } } @@ -1157,10 +1256,27 @@ void SongEditor::drawPattern( int nPos, int nNumber, bool bInvertColour, double patternColor = patternColor.darker( 130 ); } - int x = m_nMargin + m_nGridWidth * nPos; + patternColor.setAlpha( 230 ); + + int x = SongEditor::nMargin + m_nGridWidth * nPos; int y = m_nGridHeight * nNumber; - p.fillRect( x + 1, y + 3, fWidth * (m_nGridWidth - 1), m_nGridHeight - 5, patternColor ); + p.fillRect( x + 1, y + 1, fWidth * (m_nGridWidth - 1), m_nGridHeight - 1, patternColor ); + + // To better distinguish between the individual patterns, they + // will have a pronounced border. + QColor borderColor; + if ( bIsSelected ){ + if ( hasFocus() ) { + borderColor = pPref->getColorTheme()->m_selectionHighlightColor; + } else { + borderColor = pPref->getColorTheme()->m_selectionInactiveColor; + } + } else { + borderColor = QColor( 0, 0, 0 ); + } + p.setPen( borderColor ); + p.drawRect( x, y, fWidth * m_nGridWidth, m_nGridHeight ); } std::vector SongEditor::elementsIntersecting( QRect r ) @@ -1230,7 +1346,9 @@ void SongEditor::onPreferencesChanged( H2Core::Preferences::Changes changes ) SongEditorPatternList::SongEditorPatternList( QWidget *parent ) : QWidget( parent ) , EventListener() + , WidgetWithHighlightedList() , m_pBackgroundPixmap( nullptr ) + , m_nRowHovered( -1 ) { m_pHydrogen = Hydrogen::get_instance(); m_pAudioEngine = m_pHydrogen->getAudioEngine(); @@ -1242,6 +1360,7 @@ SongEditorPatternList::SongEditorPatternList( QWidget *parent ) setAttribute(Qt::WA_OpaquePaintEvent); setAcceptDrops(true); + setMouseTracking( true ); m_pPatternBeingEdited = nullptr; @@ -1254,9 +1373,6 @@ SongEditorPatternList::SongEditorPatternList( QWidget *parent ) this->resize( m_nWidth, m_nInitialHeight ); - m_labelBackgroundLight.load( Skin::getImagePath() + "/songEditor/songEditorLabelBG.png" ); - m_labelBackgroundDark.load( Skin::getImagePath() + "/songEditor/songEditorLabelABG.png" ); - m_labelBackgroundSelected.load( Skin::getImagePath() + "/songEditor/songEditorLabelSBG.png" ); m_playingPattern_on_Pixmap.load( Skin::getImagePath() + "/songEditor/playingPattern_on.png" ); m_playingPattern_off_Pixmap.load( Skin::getImagePath() + "/songEditor/playingPattern_off.png" ); m_playingPattern_empty_Pixmap.load( Skin::getImagePath() + "/songEditor/playingPattern_empty.png" ); @@ -1272,12 +1388,30 @@ SongEditorPatternList::SongEditorPatternList( QWidget *parent ) m_pPatternPopup->addAction( tr("Virtual Pattern"), this, SLOT( patternPopup_virtualPattern() ) ); m_pPatternPopup->setObjectName( "PatternListPopup" ); + // Reset the clicked row once the popup is closed by clicking at + // any position other than at an action of the popup. + connect( m_pPatternPopup, &QMenu::aboutToHide, [=](){ + if ( m_rowSelection == RowSelection::Popup ) { + setRowSelection( RowSelection::None ); + } + }); + HydrogenApp::get_instance()->addEventListener( this ); QScrollArea *pScrollArea = dynamic_cast< QScrollArea * >( parentWidget()->parentWidget() ); assert( pScrollArea ); m_pDragScroller = new DragScroller( pScrollArea ); + + m_pHighlightLockedTimer = new QTimer( this ); + m_pHighlightLockedTimer->setSingleShot( true ); + connect(m_pHighlightLockedTimer, &QTimer::timeout, + [=](){ HydrogenApp::get_instance()->getSongEditorPanel()->highlightPatternEditorLocked( false ); } ); + qreal pixelRatio = devicePixelRatio(); + m_pBackgroundPixmap = new QPixmap( m_nWidth * pixelRatio, + height() * pixelRatio ); + m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio ); + createBackground(); update(); } @@ -1294,6 +1428,12 @@ void SongEditorPatternList::patternChangedEvent() { update(); } +void SongEditorPatternList::setRowSelection( RowSelection rowSelection ) { + m_rowSelection = rowSelection; + createBackground(); + update(); +} + void SongEditorPatternList::patternModifiedEvent() { createBackground(); update(); @@ -1303,12 +1443,20 @@ void SongEditorPatternList::patternModifiedEvent() { void SongEditorPatternList::mousePressEvent( QMouseEvent *ev ) { __drag_start_position = ev->pos(); - int row = (ev->y() / m_nGridHeight); + + // -1 to compensate for the 1 pixel offset to align shadows and + // -grid lines. + int nRow = (( ev->y() - 1 ) / m_nGridHeight); - std::shared_ptr song = m_pHydrogen->getSong(); - PatternList *patternList = song->getPatternList(); + auto pSong = m_pHydrogen->getSong(); + if ( pSong == nullptr ) { + return; + } + + auto pPatternList = pSong->getPatternList(); - if ( row >= (int)patternList->size() ) { + if ( nRow < 0 || nRow >= (int)pPatternList->size() ) { + ERRORLOG( QString( "Row [%1] out of bound" ).arg( nRow ) ); return; } @@ -1316,11 +1464,33 @@ void SongEditorPatternList::mousePressEvent( QMouseEvent *ev ) || (ev->modifiers() == Qt::ControlModifier && ev->button() == Qt::RightButton) || (ev->modifiers() == Qt::ControlModifier && ev->button() == Qt::LeftButton) || ev->pos().x() < 15 ){ - togglePattern( row ); - EventQueue::get_instance()->push_event( EVENT_SELECTED_PATTERN_CHANGED, -1 ); - } else { - m_pHydrogen->setSelectedPatternNumber( row ); + if ( m_pHydrogen->getPatternMode() == Song::PatternMode::Stacked ) { + m_pHydrogen->toggleNextPattern( nRow ); + } + } + else { + if ( ! m_pHydrogen->isPatternEditorLocked() ) { + m_pHydrogen->setSelectedPatternNumber( nRow ); + } else { + // Notify the users why nothing just happened by + // highlighting the pattern locked button in the + // SongEditorPanel. + HydrogenApp::get_instance()->getSongEditorPanel()->highlightPatternEditorLocked( true ); + m_pHighlightLockedTimer->start( 250 ); + } + if (ev->button() == Qt::RightButton) { + + if ( m_rowSelection == RowSelection::Dialog ) { + // There is still a dialog window opened from the last + // time. It needs to be closed before the popup will + // be shown again. + ERRORLOG( "A dialog is still opened. It needs to be closed first." ); + return; + } + + m_nRowClicked = nRow; + setRowSelection( RowSelection::Popup ); m_pPatternPopup->popup( QPoint( ev->globalX(), ev->globalY() ) ); } } @@ -1356,7 +1526,7 @@ void SongEditorPatternList::inlineEditPatternName( int row ) return; } m_pPatternBeingEdited = pPatternList->get( row ); - m_pLineEdit->setGeometry( 23, row * m_nGridHeight , m_nWidth - 23, m_nGridHeight ); + m_pLineEdit->setGeometry( 23, row * m_nGridHeight + 1 , m_nWidth - 23, m_nGridHeight ); m_pLineEdit->setText( m_pPatternBeingEdited->get_name() ); m_pLineEdit->selectAll(); m_pLineEdit->show(); @@ -1377,10 +1547,14 @@ void SongEditorPatternList::inlineEditingEntered() QString patternName = pPatternList->find_unused_pattern_name( m_pLineEdit->text(), m_pPatternBeingEdited ); - int nSelectedPattern = m_pHydrogen->getSelectedPatternNumber(); - - SE_modifyPatternPropertiesAction *action = new SE_modifyPatternPropertiesAction( m_pPatternBeingEdited->get_name() , m_pPatternBeingEdited->get_info(), m_pPatternBeingEdited->get_category(), - patternName, m_pPatternBeingEdited->get_info(), m_pPatternBeingEdited->get_category(), nSelectedPattern ); + SE_modifyPatternPropertiesAction *action = + new SE_modifyPatternPropertiesAction( m_pPatternBeingEdited->get_name(), + m_pPatternBeingEdited->get_info(), + m_pPatternBeingEdited->get_category(), + patternName, + m_pPatternBeingEdited->get_info(), + m_pPatternBeingEdited->get_category(), + pPatternList->index( m_pPatternBeingEdited ) ); HydrogenApp::get_instance()->m_pUndoStack->push( action ); } @@ -1394,9 +1568,15 @@ void SongEditorPatternList::inlineEditingFinished() void SongEditorPatternList::paintEvent( QPaintEvent *ev ) { + auto pPref = Preferences::get_instance(); + auto pHydrogenApp = HydrogenApp::get_instance(); + auto pSongEditor = pHydrogenApp->getSongEditorPanel()->getSongEditor(); + QPainter painter(this); qreal pixelRatio = devicePixelRatio(); - if ( pixelRatio != m_pBackgroundPixmap->devicePixelRatio() ) { + if ( width() != m_pBackgroundPixmap->width() || + height() != m_pBackgroundPixmap->height() || + pixelRatio != m_pBackgroundPixmap->devicePixelRatio() ) { createBackground(); } QRectF srcRect( @@ -1406,6 +1586,31 @@ void SongEditorPatternList::paintEvent( QPaintEvent *ev ) pixelRatio * ev->rect().height() ); painter.drawPixmap( ev->rect(), *m_pBackgroundPixmap, srcRect ); + + // In case a row was right-clicked or the cursor is positioned on + // a grid cell within this row, highlight it using a border. + if ( ( ! pHydrogenApp->hideKeyboardCursor() && + pSongEditor->hasFocus() ) || + m_rowSelection != RowSelection::None ) { + QColor colorHighlight = pPref->getColorTheme()->m_highlightColor; + QPen pen; + + int nStartY; + if ( m_rowSelection != RowSelection::None ) { + // In case a row was right-clicked, highlight it using a border. + pen.setColor( pPref->getColorTheme()->m_highlightColor); + nStartY = m_nRowClicked * m_nGridHeight; + } else { + pen.setColor( pPref->getColorTheme()->m_cursorColor ); + nStartY = pSongEditor->getCursorRow() * m_nGridHeight; + } + pen.setWidth( 2 ); + painter.setRenderHint( QPainter::Antialiasing ); + + painter.setPen( pen ); + painter.drawRoundedRect( QRect( 1, nStartY + 1, m_nWidth - 2, + m_nGridHeight - 1 ), 4, 4 ); + } } @@ -1423,7 +1628,8 @@ void SongEditorPatternList::songModeActivationEvent( int nValue ) { UNUSED( nValue ); // Refresh pattern list display if in stacked mode - if ( ! Preferences::get_instance()->patternModePlaysSelected() ) { + if ( Hydrogen::get_instance()->getPatternMode() == + Song::PatternMode::Stacked ) { createBackground(); update(); } @@ -1446,10 +1652,11 @@ void SongEditorPatternList::createBackground() int nPatterns = pSong->getPatternList()->size(); int nSelectedPattern = m_pHydrogen->getSelectedPatternNumber(); - static int oldHeight = -1; - int newHeight = m_nGridHeight * nPatterns; + int newHeight = m_nGridHeight * nPatterns + 1; - if ( oldHeight != newHeight || m_pBackgroundPixmap->devicePixelRatio() != devicePixelRatio() ) { + if ( m_nWidth != m_pBackgroundPixmap->width() || + newHeight != m_pBackgroundPixmap->height() || + m_pBackgroundPixmap->devicePixelRatio() != devicePixelRatio() ) { if (newHeight == 0) { newHeight = 1; // the pixmap should not be empty } @@ -1459,21 +1666,34 @@ void SongEditorPatternList::createBackground() m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio ); this->resize( m_nWidth, newHeight ); } - m_pBackgroundPixmap->fill( Qt::black ); + + QColor backgroundColor = pPref->getColorTheme()->m_songEditor_backgroundColor.darker( 120 ); + QColor backgroundColorSelected = pPref->getColorTheme()->m_songEditor_selectedRowColor.darker( 114 ); + QColor backgroundColorAlternate = pPref->getColorTheme()->m_songEditor_alternateRowColor.darker( 132 ); QPainter p( m_pBackgroundPixmap ); + + + // Offset the pattern list by one pixel to align the dark shadows + // at the bottom of each row with the grid lines in the song editor. + p.fillRect( QRect( 0, 0, width(), 1 ), pPref->getColorTheme()->m_windowColor ); + p.setFont( boldTextFont ); - for ( int i = 0; i < nPatterns; i++ ) { - uint y = m_nGridHeight * i; - if ( i == nSelectedPattern ) { - p.drawPixmap( QPoint( 0, y ), m_labelBackgroundSelected ); - } - else { - if ( ( i % 2) == 0 ) { - p.drawPixmap( QPoint( 0, y ), m_labelBackgroundDark ); - } - else { - p.drawPixmap( QPoint( 0, y ), m_labelBackgroundLight ); + for ( int ii = 0; ii < nPatterns; ii++ ) { + uint y = m_nGridHeight * ii + 1; + + if ( ii == nSelectedPattern ) { + Skin::drawListBackground( &p, QRect( 0, y, width(), m_nGridHeight ), + backgroundColorSelected, false ); + } else { + if ( ( ii % 2 ) == 0 ) { + Skin::drawListBackground( &p, QRect( 0, y, width(), m_nGridHeight ), + backgroundColor, + ii == m_nRowHovered ); + } else { + Skin::drawListBackground( &p, QRect( 0, y, width(), m_nGridHeight ), + backgroundColorAlternate, + ii == m_nRowHovered ); } } } @@ -1509,39 +1729,55 @@ void SongEditorPatternList::createBackground() /// paint the foreground (pattern name etc.) for ( int i = 0; i < nPatterns; i++ ) { if ( i == nSelectedPattern ) { - p.setPen( QColor( 0,0,0 ) ); + p.setPen( pPref->getColorTheme()->m_songEditor_selectedRowTextColor ); } else { p.setPen( pPref->getColorTheme()->m_songEditor_textColor ); } uint text_y = i * m_nGridHeight; - if ( PatternArray[i].bNext ) { - p.drawPixmap( QPoint( 5, text_y + 3 ), m_playingPattern_off_Pixmap ); + + p.drawText( 25, text_y - 1, m_nWidth - 25, m_nGridHeight + 2, + Qt::AlignVCenter, PatternArray[i].sPatternName); + + Skin::Stacked mode = Skin::Stacked::None; + if ( PatternArray[i].bNext && PatternArray[i].bActive) { + mode = Skin::Stacked::OffNext; + } + else if ( PatternArray[i].bNext ) { + mode = Skin::Stacked::OnNext; } else if (PatternArray[i].bActive) { - //mark active pattern with triangular - p.drawPixmap( QPoint( 5, text_y + 3 ), m_playingPattern_on_Pixmap ); - } else if ( ! pPref->patternModePlaysSelected() && pSong->getMode() == Song::Mode::Pattern ) { - p.drawPixmap( QPoint( 5, text_y + 3 ), m_playingPattern_empty_Pixmap ); + mode = Skin::Stacked::On; + } + else if ( m_pHydrogen->getPatternMode() == Song::PatternMode::Stacked ) { + mode = Skin::Stacked::Off; + } + + if ( mode != Skin::Stacked::None ) { + Skin::drawStackedIndicator( &p, 5, text_y + 4, mode ); } - p.drawText( 25, text_y - 1, m_nWidth - 25, m_nGridHeight + 2, Qt::AlignVCenter, PatternArray[i].sPatternName); } } +void SongEditorPatternList::stackedModeActivationEvent( int ) { + createBackground(); + update(); +} void SongEditorPatternList::patternPopup_virtualPattern() { + setRowSelection( RowSelection::Dialog ); + VirtualPatternDialog *dialog = new VirtualPatternDialog( this ); SongEditorPanel *pSEPanel = HydrogenApp::get_instance()->getSongEditorPanel(); - int tmpselectedpatternpos = m_pHydrogen->getSelectedPatternNumber(); dialog->patternList->setSortingEnabled(1); std::shared_ptr song = m_pHydrogen->getSong(); PatternList *pPatternList = song->getPatternList(); - H2Core::Pattern *selectedPattern = pPatternList->get(tmpselectedpatternpos); + auto pPatternClicked = pPatternList->get( m_nRowClicked ); std::map patternNameMap; @@ -1550,7 +1786,7 @@ void SongEditorPatternList::patternPopup_virtualPattern() H2Core::Pattern *curPattern = pPatternList->get( index ); QString patternName = curPattern->get_name(); - if (patternName == selectedPattern->get_name()) { + if (patternName == pPatternClicked->get_name()) { continue; }//if @@ -1559,18 +1795,19 @@ void SongEditorPatternList::patternPopup_virtualPattern() QListWidgetItem *newItem = new QListWidgetItem(patternName, dialog->patternList); dialog->patternList->insertItem(0, newItem ); - if (selectedPattern->get_virtual_patterns()->find(curPattern) != selectedPattern->get_virtual_patterns()->end()) { + if (pPatternClicked->get_virtual_patterns()->find(curPattern) != + pPatternClicked->get_virtual_patterns()->end()) { newItem->setSelected( true ); }//if }//for if ( dialog->exec() == QDialog::Accepted ) { - selectedPattern->virtual_patterns_clear(); + pPatternClicked->virtual_patterns_clear(); for (unsigned int index = 0; index < listsize-1; ++index) { QListWidgetItem *listItem = dialog->patternList->item(index); if (listItem->isSelected() == true) { if (patternNameMap.find(listItem->text()) != patternNameMap.end()) { - selectedPattern->virtual_patterns_add(patternNameMap[listItem->text()]); + pPatternClicked->virtual_patterns_add(patternNameMap[listItem->text()]); }//if }//if }//for @@ -1581,16 +1818,25 @@ void SongEditorPatternList::patternPopup_virtualPattern() pPatternList->flattened_virtual_patterns_compute(); delete dialog; + + setRowSelection( RowSelection::None ); }//patternPopup_virtualPattern void SongEditorPatternList::patternPopup_load() { - Hydrogen *engine = Hydrogen::get_instance(); - int nSelectedPattern = engine->getSelectedPatternNumber(); - std::shared_ptr song = engine->getSong(); - Pattern *pattern = song->getPatternList()->get( nSelectedPattern ); + setRowSelection( RowSelection::Dialog ); + + auto pHydrogen = Hydrogen::get_instance(); + auto pSong = pHydrogen->getSong(); + + if ( pSong == nullptr ) { + setRowSelection( RowSelection::None ); + return; + } + + Pattern* pPattern = pSong->getPatternList()->get( m_nRowClicked ); QString sPath = Preferences::get_instance()->getLastOpenPatternDirectory(); if ( ! Filesystem::dir_readable( sPath, false ) ){ @@ -1601,66 +1847,87 @@ void SongEditorPatternList::patternPopup_load() fd.setFileMode( QFileDialog::ExistingFile ); fd.setNameFilter( Filesystem::patterns_filter_name ); fd.setDirectory( sPath ); - fd.setWindowTitle( tr( "Open Pattern" ) ); + fd.setWindowTitle( QString( tr( "Open Pattern to Replace " ) + .append( pPattern->get_name() ) ) ); if (fd.exec() != QDialog::Accepted) { + setRowSelection( RowSelection::None ); return; } QString patternPath = fd.selectedFiles().first(); - QString prevPatternPath = Files::savePatternTmp( pattern->get_name(), pattern, song, engine->getCurrentDrumkitName() ); + QString prevPatternPath = + Files::savePatternTmp( pPattern->get_name(), pPattern, pSong, + pHydrogen->getCurrentDrumkitName() ); if ( prevPatternPath.isEmpty() ) { QMessageBox::warning( this, "Hydrogen", tr("Could not save pattern to temporary directory.") ); + setRowSelection( RowSelection::None ); return; } LocalFileMng fileMng; QString sequencePath = Filesystem::tmp_file_path( "SEQ.xml" ); - if ( !song->writeTempPatternList( sequencePath ) ) { + if ( !pSong->writeTempPatternList( sequencePath ) ) { QMessageBox::warning( this, "Hydrogen", tr("Could not export sequence.") ); + setRowSelection( RowSelection::None ); return; } Preferences::get_instance()->setLastOpenPatternDirectory( fd.directory().absolutePath() ); - SE_loadPatternAction *action = new SE_loadPatternAction( patternPath, prevPatternPath, sequencePath, nSelectedPattern, false ); + SE_loadPatternAction *action = + new SE_loadPatternAction( patternPath, prevPatternPath, sequencePath, + m_nRowClicked, false ); HydrogenApp *hydrogenApp = HydrogenApp::get_instance(); hydrogenApp->m_pUndoStack->push( action ); + + setRowSelection( RowSelection::None ); } void SongEditorPatternList::patternPopup_export() { - HydrogenApp::get_instance()->getMainForm()->action_file_export_pattern_as(); + setRowSelection( RowSelection::Dialog ); + HydrogenApp::get_instance()->getMainForm()->action_file_export_pattern_as( m_nRowClicked ); + + setRowSelection( RowSelection::None ); return; } void SongEditorPatternList::patternPopup_save() { - auto pCommonStrings = HydrogenApp::get_instance()->getCommonStrings(); - Hydrogen *engine = Hydrogen::get_instance(); - std::shared_ptr song = engine->getSong(); - Pattern *pattern = song->getPatternList()->get( engine->getSelectedPatternNumber() ); + setRowSelection( RowSelection::Dialog ); + + auto pHydrogenApp = HydrogenApp::get_instance(); + auto pCommonStrings = pHydrogenApp->getCommonStrings(); + auto pHydrogen = Hydrogen::get_instance(); + auto pSong = pHydrogen->getSong(); + auto pPattern = pSong->getPatternList()->get( m_nRowClicked ); - QString path = Files::savePatternNew( pattern->get_name(), pattern, song, engine->getCurrentDrumkitName() ); - if ( path.isEmpty() ) { - if ( QMessageBox::information( this, "Hydrogen", - tr( "The pattern-file exists. \nOverwrite the existing pattern?"), + QString sPath = Files::savePatternNew( pPattern->get_name(), pPattern, + pSong, pHydrogen->getCurrentDrumkitName() ); + if ( sPath.isEmpty() ) { + if ( QMessageBox::information( this, "Hydrogen", tr( "The pattern-file exists. \nOverwrite the existing pattern?"), pCommonStrings->getButtonOk(), pCommonStrings->getButtonCancel(), nullptr, 1 ) != 0 ) { + setRowSelection( RowSelection::None ); return; } - path = Files::savePatternOver( pattern->get_name(), pattern, song, engine->getCurrentDrumkitName() ); + sPath = Files::savePatternOver( pPattern->get_name(), pPattern, + pSong, pHydrogen->getCurrentDrumkitName() ); } - if ( path.isEmpty() ) { + if ( sPath.isEmpty() ) { QMessageBox::warning( this, "Hydrogen", tr("Could not export pattern.") ); + setRowSelection( RowSelection::None ); return; } - HydrogenApp::get_instance()->setStatusBarMessage( tr( "Pattern saved." ), 10000 ); + pHydrogenApp->setStatusBarMessage( tr( "Pattern saved." ), 10000 ); SoundLibraryDatabase::get_instance()->updatePatterns(); - HydrogenApp::get_instance()->getInstrumentRack()->getSoundLibraryPanel()->test_expandedItems(); - HydrogenApp::get_instance()->getInstrumentRack()->getSoundLibraryPanel()->updateDrumkitList(); + pHydrogenApp->getInstrumentRack()->getSoundLibraryPanel()->test_expandedItems(); + pHydrogenApp->getInstrumentRack()->getSoundLibraryPanel()->updateDrumkitList(); + + setRowSelection( RowSelection::None ); } @@ -1675,17 +1942,18 @@ void SongEditorPatternList::patternPopup_edit() void SongEditorPatternList::patternPopup_properties() { - Hydrogen *engine = Hydrogen::get_instance(); - std::shared_ptr song = engine->getSong(); - PatternList *patternList = song->getPatternList(); - - int nSelectedPattern = engine->getSelectedPatternNumber(); - H2Core::Pattern *pattern = patternList->get( nSelectedPattern ); + + setRowSelection( RowSelection::Dialog ); + auto pHydrogen = Hydrogen::get_instance(); + auto pPattern = pHydrogen->getSong()->getPatternList()->get( m_nRowClicked ); - PatternPropertiesDialog *dialog = new PatternPropertiesDialog(this, pattern, nSelectedPattern, false); + PatternPropertiesDialog *dialog = + new PatternPropertiesDialog( this, pPattern, m_nRowClicked, false); dialog->exec(); delete dialog; dialog = nullptr; + + setRowSelection( RowSelection::None ); } @@ -1699,7 +1967,7 @@ void SongEditorPatternList::acceptPatternPropertiesDialogSettings(QString newPat pattern->set_info( newPatternInfo ); pattern->set_category( newPatternCategory ); pHydrogen->setIsModified( true ); - EventQueue::get_instance()->push_event( EVENT_SELECTED_PATTERN_CHANGED, -1 ); + EventQueue::get_instance()->push_event( EVENT_PATTERN_MODIFIED, -1 ); createBackground(); update(); } @@ -1714,7 +1982,7 @@ void SongEditorPatternList::revertPatternPropertiesDialogSettings(QString oldPat pattern->set_name( oldPatternName ); pattern->set_category( oldPatternCategory ); pHydrogen->setIsModified( true ); - EventQueue::get_instance()->push_event( EVENT_SELECTED_PATTERN_CHANGED, -1 ); + EventQueue::get_instance()->push_event( EVENT_PATTERN_MODIFIED, -1 ); createBackground(); update(); } @@ -1722,67 +1990,85 @@ void SongEditorPatternList::revertPatternPropertiesDialogSettings(QString oldPat void SongEditorPatternList::patternPopup_delete() { - std::shared_ptr pSong = m_pHydrogen->getSong(); - int patternPosition = m_pHydrogen->getSelectedPatternNumber(); - Pattern *pattern = pSong->getPatternList()->get( patternPosition ); + setRowSelection( RowSelection::Dialog ); + + auto pSong = m_pHydrogen->getSong(); + auto pPattern = pSong->getPatternList()->get( m_nRowClicked ); - QString patternPath = Files::savePatternTmp( pattern->get_name(), pattern, pSong, m_pHydrogen->getCurrentDrumkitName() ); + QString patternPath = + Files::savePatternTmp( pPattern->get_name(), pPattern, pSong, + m_pHydrogen->getCurrentDrumkitName() ); if ( patternPath.isEmpty() ) { QMessageBox::warning( this, "Hydrogen", tr("Could not save pattern to temporary directory.") ); + setRowSelection( RowSelection::None ); return; } LocalFileMng fileMng; QString sequencePath = Filesystem::tmp_file_path( "SEQ.xml" ); if ( !pSong->writeTempPatternList( sequencePath ) ) { QMessageBox::warning( this, "Hydrogen", tr("Could not export sequence.") ); + setRowSelection( RowSelection::None ); return; } - SE_deletePatternFromListAction *action = new SE_deletePatternFromListAction( patternPath , sequencePath, patternPosition ); + SE_deletePatternFromListAction *action = + new SE_deletePatternFromListAction( patternPath, sequencePath, + m_nRowClicked ); HydrogenApp *hydrogenApp = HydrogenApp::get_instance(); hydrogenApp->m_pUndoStack->push( action ); + setRowSelection( RowSelection::None ); } void SongEditorPatternList::patternPopup_duplicate() { - std::shared_ptr pSong = m_pHydrogen->getSong(); + setRowSelection( RowSelection::Dialog ); + + auto pSong = m_pHydrogen->getSong(); PatternList *pPatternList = pSong->getPatternList(); - int nSelectedPattern = m_pHydrogen->getSelectedPatternNumber(); - H2Core::Pattern *pPattern = pPatternList->get( nSelectedPattern ); + auto pPattern = pPatternList->get( m_nRowClicked ); H2Core::Pattern *pNewPattern = new Pattern( pPattern ); - PatternPropertiesDialog *dialog = new PatternPropertiesDialog( this, pNewPattern, nSelectedPattern, true ); + PatternPropertiesDialog *dialog = new PatternPropertiesDialog( this, pNewPattern, m_nRowClicked, true ); if ( dialog->exec() == QDialog::Accepted ) { - QString filePath = Files::savePatternTmp( pNewPattern->get_name(), pNewPattern, pSong, m_pHydrogen->getCurrentDrumkitName() ); + QString filePath = Files::savePatternTmp( pNewPattern->get_name(), + pNewPattern, pSong, + m_pHydrogen->getCurrentDrumkitName() ); if ( filePath.isEmpty() ) { QMessageBox::warning( this, "Hydrogen", tr("Could not save pattern to temporary directory.") ); + setRowSelection( RowSelection::None ); return; } - SE_duplicatePatternAction *action = new SE_duplicatePatternAction( filePath, nSelectedPattern + 1 ); + SE_duplicatePatternAction *action = + new SE_duplicatePatternAction( filePath, m_nRowClicked + 1 ); HydrogenApp::get_instance()->m_pUndoStack->push( action ); } delete dialog; delete pNewPattern; + + setRowSelection( RowSelection::None ); } void SongEditorPatternList::patternPopup_fill() { - int nSelectedPattern = m_pHydrogen->getSelectedPatternNumber(); + setRowSelection( RowSelection::Dialog ); + FillRange range; PatternFillDialog *dialog = new PatternFillDialog( this, &range ); // use a PatternFillDialog to get the range and mode data if ( dialog->exec() == QDialog::Accepted ) { - SE_fillRangePatternAction *action = new SE_fillRangePatternAction( &range, nSelectedPattern ); + SE_fillRangePatternAction *action = + new SE_fillRangePatternAction( &range, m_nRowClicked ); HydrogenApp::get_instance()->m_pUndoStack->push( action ); } delete dialog; + setRowSelection( RowSelection::None ); } @@ -1984,14 +2270,32 @@ void SongEditorPatternList::movePatternLine( int nSourcePattern , int nTargetPat } pPatternList->replace( nTargetPattern, pSourcePattern ); } - pHydrogen->setSelectedPatternNumber( nTargetPattern ); + + if ( pHydrogen->isPatternEditorLocked() ) { + pHydrogen->updateSelectedPattern(); + } else { + pHydrogen->setSelectedPatternNumber( nTargetPattern ); + } HydrogenApp::get_instance()->getSongEditorPanel()->updateAll(); pHydrogen->setIsModified( true ); } +void SongEditorPatternList::leaveEvent( QEvent* ev ) { + UNUSED( ev ); + m_nRowHovered = -1; + createBackground(); + update(); +} void SongEditorPatternList::mouseMoveEvent(QMouseEvent *event) { + // Update the highlighting of the hovered row. + if ( event->pos().y() / m_nGridHeight != m_nRowHovered ) { + m_nRowHovered = event->pos().y() / m_nGridHeight; + createBackground(); + update(); + } + if (!(event->buttons() & Qt::LeftButton)) { return; } @@ -2046,12 +2350,12 @@ void SongEditorPatternList::onPreferencesChanged( H2Core::Preferences::Changes c SongEditorPositionRuler::SongEditorPositionRuler( QWidget *parent ) : QWidget( parent ) , m_bRightBtnPressed( false ) - , m_nPlayheadWidth( 11 ) - , m_nPlayheadHeight( 8 ) , m_nActiveBpmWidgetColumn( -1 ) , m_nHoveredColumn( -1 ) - , m_nHoveredRow( -1 ) - , m_bHighlightHoveredColumn( false ) + , m_hoveredRow( HoveredRow::None ) + , m_nTagHeight( 6 ) + , m_fTick( 0 ) + , m_nColumn( 0 ) { auto pPref = H2Core::Preferences::get_instance(); @@ -2068,13 +2372,8 @@ SongEditorPositionRuler::SongEditorPositionRuler( QWidget *parent ) m_nMaxPatternSequence = pPref->getMaxBars(); m_nInitialWidth = m_nMaxPatternSequence * 16; - - // Offset position of the shaft of the arrow head indicating the - // playback position. - m_nXShaft = std::floor( static_cast( m_nPlayheadWidth ) / 2 ); - if ( m_nPlayheadWidth % 2 != 0 ) { - m_nXShaft++; - } + + m_nActiveColumns = m_pHydrogen->getSong()->getPatternGroupVector()->size(); resize( m_nInitialWidth, m_nHeight ); setFixedHeight( m_nHeight ); @@ -2084,17 +2383,15 @@ SongEditorPositionRuler::SongEditorPositionRuler( QWidget *parent ) m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio ); createBackground(); // create m_backgroundPixmap pixmap - - // create tick position pixmap - bool ok = m_tickPositionPixmap.load( Skin::getImagePath() + "/patternEditor/tickPosition.png" ); - if( ok == false ){ - ERRORLOG( "Error loading pixmap" ); - } - update(); m_pTimer = new QTimer(this); - connect(m_pTimer, SIGNAL(timeout()), this, SLOT(updatePosition())); + connect(m_pTimer, &QTimer::timeout, [=]() { + if ( H2Core::Hydrogen::get_instance()->getAudioEngine()->getState() == + H2Core::AudioEngine::State::Playing ) { + updatePosition(); + } + }); m_pTimer->start(200); } @@ -2104,6 +2401,15 @@ SongEditorPositionRuler::~SongEditorPositionRuler() { m_pTimer->stop(); } +void SongEditorPositionRuler::relocationEvent() { + updatePosition(); +} + +void SongEditorPositionRuler::songSizeChangedEvent() { + m_nActiveColumns = m_pHydrogen->getSong()->getPatternGroupVector()->size(); + createBackground(); + update(); +} uint SongEditorPositionRuler::getGridWidth() { @@ -2115,7 +2421,8 @@ void SongEditorPositionRuler::setGridWidth( uint width ) if ( SONG_EDITOR_MIN_GRID_WIDTH <= width && SONG_EDITOR_MAX_GRID_WIDTH >= width ) { m_nGridWidth = width; - createBackground (); + createBackground(); + update(); } } @@ -2132,11 +2439,16 @@ void SongEditorPositionRuler::createBackground() QColor textColorAlpha( textColor ); textColorAlpha.setAlpha( 45 ); - QColor backgroundColor = pPref->getColorTheme()->m_songEditor_backgroundColor; + QColor backgroundColor = pPref->getColorTheme()->m_songEditor_alternateRowColor.darker( 115 ); + QColor backgroundInactiveColor = pPref->getColorTheme()->m_midLightColor; QColor backgroundColorTempoMarkers = backgroundColor.darker( 120 ); QColor colorHighlight = pPref->getColorTheme()->m_highlightColor; + QColor lineColor = pPref->getColorTheme()->m_songEditor_lineColor; + QColor lineColorAlpha( lineColor ); + lineColorAlpha.setAlpha( 45 ); + // Resize pixmap if pixel ratio has changed qreal pixelRatio = devicePixelRatio(); if ( m_pBackgroundPixmap->devicePixelRatio() != pixelRatio ) { @@ -2145,208 +2457,92 @@ void SongEditorPositionRuler::createBackground() m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio ); } - m_pBackgroundPixmap->fill( backgroundColor ); - QFont font( pPref->getApplicationFontFamily(), getPointSize( pPref->getFontSize() ) ); QPainter p( m_pBackgroundPixmap ); p.setFont( font ); - p.fillRect( 0, 0, width(), 24, backgroundColorTempoMarkers ); + int nActiveWidth = static_cast( static_cast(SongEditor::nMargin) + 1 + + static_cast(m_nActiveColumns) * + static_cast(m_nGridWidth) ); + p.fillRect( 0, 0, width(), height(), backgroundColorTempoMarkers ); + p.fillRect( 0, 25, nActiveWidth, height() - 25, backgroundColor ); + p.fillRect( nActiveWidth, 25, width() - nActiveWidth, height() - 25, + backgroundInactiveColor ); char tmp[10]; - if ( pHydrogen->getMode() == Song::Mode::Pattern ) { - p.setPen( textColorAlpha ); - } else { - p.setPen( textColor ); - } - for (uint i = 0; i < m_nMaxPatternSequence + 1; i++) { - uint x = m_nMargin + i * m_nGridWidth; + QColor textColorGrid( textColor ); + textColorGrid.setAlpha( 200 ); + p.setPen( QPen( textColorGrid, 1, Qt::SolidLine ) ); + for ( int ii = 0; ii < m_nMaxPatternSequence + 1; ii++) { + int x = SongEditor::nMargin + ii * m_nGridWidth; - if ( (i % 4) == 0 ) { - sprintf( tmp, "%d", i + 1 ); - p.drawText( x - m_nGridWidth, 12, m_nGridWidth * 2, height(), Qt::AlignCenter, tmp ); + if ( ( ii % 4 ) == 0 ) { + p.drawLine( x, height() - 14, x, height() - 1); } else { - p.drawLine( x, 32, x, 40 ); + p.drawLine( x, height() - 6, x, height() - 1); } } - - int nPaintedTags = 0; - // draw tags - if ( pHydrogen->getMode() == Song::Mode::Pattern ) { - QColor colorHiglightAlpha( colorHighlight.lighter( 120 ) ); - colorHiglightAlpha.setAlpha( 45 ); - p.setPen( colorHiglightAlpha ); - } else { - p.setPen( colorHighlight.lighter( 120 ) ); - } - for ( uint ii = 0; ii < m_nMaxPatternSequence + 1; ii++) { - uint x = m_nMargin + ii * m_nGridWidth; - for ( int tt = 0; tt < static_cast(tagVector.size()); tt++){ - if ( tagVector[tt]->nColumn == ii ) { - p.drawText( x - m_nGridWidth / 2 , 12, m_nGridWidth * 2, height() , Qt::AlignCenter, "T"); - ++nPaintedTags; - } - } - - // Let's be more efficient and finish as soon as we are done. - if ( nPaintedTags == tagVector.size() ) { - break; + // Add every 4th number to the grid + p.setPen( textColor ); + for (uint i = 0; i < m_nMaxPatternSequence + 1; i += 4) { + uint x = SongEditor::nMargin + i * m_nGridWidth; + + sprintf( tmp, "%d", i + 1 ); + if ( i < 10 ) { + p.drawText( x, height() / 2 + 3, m_nGridWidth, height() / 2 - 7, + Qt::AlignHCenter, tmp ); + } else { + p.drawText( x + 2, height() / 2 + 3, m_nGridWidth * 2, height() / 2 - 7, + Qt::AlignLeft, tmp ); } } + + // draw tags + p.setPen( pPref->getColorTheme()->m_accentTextColor ); + + QFont font2( pPref->getApplicationFontFamily(), 5 ); + p.setFont( font2 ); + + for ( const auto& ttag : tagVector ){ + int x = SongEditor::nMargin + ttag->nColumn * m_nGridWidth + 4; + QRect rect( x, height() / 2 - 1 - m_nTagHeight, + m_nGridWidth - 6, m_nTagHeight ); - - // draw tempo content - auto tempoMarkerVector = pTimeline->getAllTempoMarkers(); - - QColor tempoMarkerColor; - if ( pHydrogen->isTimelineEnabled() ) { - tempoMarkerColor = pPref->getColorTheme()->m_songEditor_textColor; - } else { - tempoMarkerColor = textColorAlpha; + p.fillRect( rect, pPref->getColorTheme()->m_highlightColor.darker( 135 ) ); + p.drawText( rect, Qt::AlignCenter, "T"); } - p.setPen( tempoMarkerColor ); - - int nCurrentColumn = pHydrogen->getAudioEngine()->getColumn(); - int nPaintedTempoMarkers = 0; + p.setFont( font ); - // Which tempo marker is the currently used one? - int nCurrentTempoMarkerIndex = -1; - for ( int tt = 0; tt < static_cast(tempoMarkerVector.size()); tt++){ - if ( tempoMarkerVector[tt]->nColumn > nCurrentColumn ) { - nCurrentTempoMarkerIndex = std::max( tt - 1, 0 ); - break; - } - } - if ( nCurrentTempoMarkerIndex == -1 ) { - nCurrentTempoMarkerIndex = tempoMarkerVector.size() - 1; - } + // draw tempo content - // Draw tempo marker grid - for (uint ii = 0; ii < m_nMaxPatternSequence + 1; ii++) { - uint x = m_nMargin + ii * m_nGridWidth; - p.drawLine( x, 2, x, 5 ); - p.drawLine( x, 19, x, 20 ); + // Draw tempo marker grid. + if ( ! pHydrogen->isTimelineEnabled() ) { + p.setPen( textColorAlpha ); + } else { + QColor tempoMarkerGridColor( textColor ); + tempoMarkerGridColor.setAlpha( 170 ); + p.setPen( tempoMarkerGridColor ); } - - // Draw tempo markers - char tempo[10]; for (uint ii = 0; ii < m_nMaxPatternSequence + 1; ii++) { - for ( int tt = 0; tt < static_cast(tempoMarkerVector.size()); tt++){ - if ( tempoMarkerVector[tt]->nColumn == ii ) { - if ( ii == 0 && pTimeline->isFirstTempoMarkerSpecial() ) { - if ( ! pHydrogen->isTimelineEnabled() ) { - // Omit the special tempo marker. - continue; - } - p.setPen( tempoMarkerColor.darker( 150 ) ); - } - - // Highlight the currently used tempo marker by - // drawing it bold. - if ( tt == nCurrentTempoMarkerIndex ) { - font.setBold( true ); - p.setFont( font ); - } - QRect rect( m_nMargin - SONG_EDITOR_MAX_GRID_WIDTH + - ii * m_nGridWidth, - 7, 2 * SONG_EDITOR_MAX_GRID_WIDTH, 12 ); - - sprintf( tempo, "%d", ((int)tempoMarkerVector[tt]->fBpm) ); - p.drawText( rect, Qt::AlignCenter, tempo ); - - ++nPaintedTempoMarkers; - - // Reset painter - if ( ii == 0 && pTimeline->isFirstTempoMarkerSpecial() ) { - p.setPen( tempoMarkerColor ); - } - if ( tt == nCurrentTempoMarkerIndex ) { - font.setBold( false ); - p.setFont( font ); - } - } - } + uint x = SongEditor::nMargin + ii * m_nGridWidth; - // Let's be more efficient and finish as soon as we are done. - if ( nPaintedTempoMarkers == tempoMarkerVector.size() ){ - break; - } - + p.drawLine( x, 1, x, 4 ); + p.drawLine( x, height() / 2 - 5, x, height() / 2 ); } - // Draw a slight highlight around the tempo marker hovered using - // mouse or touch events. This will also redraw the - // tempo marker to ensure it's visible (they can overlap with - // neighboring ones and be hardly readable). - if ( pHydrogen->isTimelineEnabled() && - m_bHighlightHoveredColumn ) { - QRect rect( m_nMargin - SONG_EDITOR_MAX_GRID_WIDTH + - m_nHoveredColumn * m_nGridWidth, - 7, 2 * SONG_EDITOR_MAX_GRID_WIDTH, 12 ); - p.fillRect( rect, backgroundColorTempoMarkers ); - - for ( int tt = 0; tt < static_cast(tempoMarkerVector.size()); tt++){ - if ( tempoMarkerVector[tt]->nColumn == m_nHoveredColumn ) { - if ( tt == nCurrentTempoMarkerIndex ) { - font.setBold( true ); - p.setFont( font ); - } - - sprintf( tempo, "%d", ((int)tempoMarkerVector[tt]->fBpm) ); - p.drawText( rect, Qt::AlignCenter, tempo ); - if ( tt == nCurrentTempoMarkerIndex ) { - font.setBold( false ); - p.setFont( font ); - } - break; - } - } - QColor colorHovered( colorHighlight ); - colorHovered.setAlpha( 150 ); - p.setPen( colorHovered ); - p.drawRect( rect ); - } - - // Draw a highlight in case the BPM widget was opened by - // left-clicking one of the markers. This will also redraw the - // tempo marker to ensure it's visible (they can overlap with - // neighboring ones and be hardly readable). - if ( m_nActiveBpmWidgetColumn != -1 ) { - QRect rect( m_nMargin - SONG_EDITOR_MAX_GRID_WIDTH + - m_nActiveBpmWidgetColumn * m_nGridWidth, - 7, 2 * SONG_EDITOR_MAX_GRID_WIDTH, 12 ); - p.fillRect( rect, backgroundColorTempoMarkers ); - - for ( int tt = 0; tt < static_cast(tempoMarkerVector.size()); tt++){ - if ( tempoMarkerVector[tt]->nColumn == m_nActiveBpmWidgetColumn ) { - if ( tt == nCurrentTempoMarkerIndex ) { - font.setBold( true ); - p.setFont( font ); - } - - sprintf( tempo, "%d", ((int)tempoMarkerVector[tt]->fBpm) ); - p.drawText(rect, Qt::AlignCenter, tempo ); - - if ( tt == nCurrentTempoMarkerIndex ) { - font.setBold( false ); - p.setFont( font ); - } - break; - } - } - p.setPen( colorHighlight ); - p.drawRect( rect ); + // Draw tempo markers + auto tempoMarkerVector = pTimeline->getAllTempoMarkers(); + for ( const auto& ttempoMarker : tempoMarkerVector ){ + drawTempoMarker( ttempoMarker, false, p ); } p.setPen( QColor(35, 39, 51) ); p.drawLine( 0, 0, width(), 0 ); - - p.fillRect ( 0, height() - 27, width(), 1, QColor(35, 39, 51) ); - p.fillRect ( 0, height() - 3, width(), 2, pPref->getColorTheme()->m_songEditor_alternateRowColor ); - + p.drawLine( 0, height() - 25, width(), height() - 25 ); + p.drawLine( 0, height(), width(), height() ); } void SongEditorPositionRuler::tempoChangedEvent( int ) { @@ -2363,6 +2559,7 @@ void SongEditorPositionRuler::tempoChangedEvent( int ) { } createBackground(); + update(); } void SongEditorPositionRuler::patternModifiedEvent() { @@ -2379,11 +2576,8 @@ void SongEditorPositionRuler::patternChangedEvent() { void SongEditorPositionRuler::leaveEvent( QEvent* ev ){ m_nHoveredColumn = -1; - m_nHoveredRow = -1; - if ( m_bHighlightHoveredColumn ) { - m_bHighlightHoveredColumn = false; - createBackground(); - } + m_hoveredRow = HoveredRow::None; + update(); QWidget::leaveEvent( ev ); } @@ -2392,24 +2586,25 @@ void SongEditorPositionRuler::mouseMoveEvent(QMouseEvent *ev) { auto pHydrogen = Hydrogen::get_instance(); - int nColumn = ev->x() / m_nGridWidth; - int nRow = ev->y() <= 21 ? 0 : 1; + int nColumn = ( std::max( ev->x() - SongEditor::nMargin, 0 ) ) / m_nGridWidth; + + HoveredRow row; + if ( ev->y() > 22 ) { + row = HoveredRow::Ruler; + } else if ( ev->y() > 22 - 1 - m_nTagHeight ) { + row = HoveredRow::Tag; + } else { + row = HoveredRow::TempoMarker; + } + if ( nColumn != m_nHoveredColumn || - nRow != m_nHoveredRow ) { + row != m_hoveredRow ) { // Cursor has moved into a region where the above caching // became invalid. - if ( pHydrogen->getTimeline()->hasColumnTempoMarker( nColumn ) ) { - m_bHighlightHoveredColumn = true; - } else { - m_bHighlightHoveredColumn = false; - } - if ( nRow != 0 ) { - m_bHighlightHoveredColumn = false; - } - m_nHoveredRow = nRow; + m_hoveredRow = row; m_nHoveredColumn = nColumn; - createBackground(); + update(); } if ( !m_bRightBtnPressed && ev->buttons() & Qt::LeftButton ) { @@ -2441,15 +2636,26 @@ bool SongEditorPositionRuler::event( QEvent* ev ) { } void SongEditorPositionRuler::songModeActivationEvent( int ) { + updatePosition(); createBackground(); + update(); } void SongEditorPositionRuler::timelineActivationEvent( int ) { createBackground(); + update(); } void SongEditorPositionRuler::jackTimebaseStateChangedEvent( int ) { createBackground(); + update(); +} + +void SongEditorPositionRuler::updateSongEvent( int nValue ) { + + if ( nValue == 0 ) { // different song opened + updatePosition(); + } } void SongEditorPositionRuler::showToolTip( QHelpEvent* ev ) { @@ -2458,16 +2664,16 @@ void SongEditorPositionRuler::showToolTip( QHelpEvent* ev ) { if ( pHydrogen->isTimelineEnabled() && pTimeline->isFirstTempoMarkerSpecial() && - ev->y() <= 21 && // First row containing tempo markers - ev->x() < m_nMargin + m_nGridWidth ) { // first tempo marker + m_hoveredRow == HoveredRow::TempoMarker && + ev->x() < SongEditor::nMargin + m_nGridWidth ) { // first tempo marker QToolTip::showText( ev->globalPos(), 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." ), this ); - } else if ( ev->y() > 21 ) { + } else if ( m_hoveredRow == HoveredRow::Tag ) { // Row containing the tags - int nColumn = ev->x() / m_nGridWidth; - if ( pTimeline->hasColumnTag( nColumn - 1 ) ) { + int nColumn = std::max( ev->x() - SongEditor::nMargin, 0 ) / m_nGridWidth; + if ( pTimeline->hasColumnTag( nColumn ) ) { QToolTip::showText( ev->globalPos(), - pTimeline->getTagAtColumn( nColumn - 1 ), this ); + pTimeline->getTagAtColumn( nColumn ), this ); } } } @@ -2475,54 +2681,57 @@ void SongEditorPositionRuler::showToolTip( QHelpEvent* ev ) { void SongEditorPositionRuler::showTagWidget( int nColumn ) { SongEditorPanelTagWidget dialog( this , nColumn ); - if (dialog.exec() == QDialog::Accepted) { - //createBackground(); - } - + dialog.exec(); } void SongEditorPositionRuler::showBpmWidget( int nColumn ) { - bool bTempoMarkerPresent = Hydrogen::get_instance()->getTimeline()->hasColumnTempoMarker( nColumn ); - if ( bTempoMarkerPresent ) { - m_nActiveBpmWidgetColumn = nColumn; - createBackground(); - } + bool bTempoMarkerPresent = + Hydrogen::get_instance()->getTimeline()->hasColumnTempoMarker( nColumn ); + m_nActiveBpmWidgetColumn = nColumn; + update(); SongEditorPanelBpmWidget dialog( this , nColumn, bTempoMarkerPresent ); dialog.exec(); - if ( bTempoMarkerPresent ) { - m_nActiveBpmWidgetColumn = -1; - createBackground(); - } + m_nActiveBpmWidgetColumn = -1; + update(); } void SongEditorPositionRuler::mousePressEvent( QMouseEvent *ev ) { - if (ev->button() == Qt::LeftButton && ev->y() >= 26) { - int column = (ev->x() / m_nGridWidth); - m_bRightBtnPressed = false; + auto pHydrogen = Hydrogen::get_instance(); + auto pCoreActionController = pHydrogen->getCoreActionController(); + + int nColumn = ( std::max( ev->x() - SongEditor::nMargin, 0 ) / m_nGridWidth); + + if (ev->button() == Qt::LeftButton ) { + if ( ev->y() > 22 ) { + // Relocate transport using position ruler + m_bRightBtnPressed = false; - if ( column > (int) m_pHydrogen->getSong()->getPatternGroupVector()->size() ) { - return; - } + if ( nColumn > (int) m_pHydrogen->getSong()->getPatternGroupVector()->size() ) { + return; + } - // disabling son relocates while in pattern mode as it causes weird behaviour. (jakob lund) - if ( m_pHydrogen->getMode() == Song::Mode::Pattern ) { - return; - } + if ( m_pHydrogen->getMode() == Song::Mode::Pattern ) { + pCoreActionController->activateSongMode( true ); + } - m_pHydrogen->getCoreActionController()->locateToColumn( column ); - update(); - - } else if (ev->button() == Qt::MiddleButton && ev->y() >= 26) { - showTagWidget( ev->x() / m_nGridWidth ); + m_pHydrogen->getCoreActionController()->locateToColumn( nColumn ); + update(); + } else if ( ev->y() > 22 - 1 - m_nTagHeight ) { + showTagWidget( nColumn ); + + } else if ( m_pHydrogen->isTimelineEnabled() ){ + showBpmWidget( nColumn ); + } + } else if ( ev->button() == Qt::MiddleButton ) { + showTagWidget( nColumn ); } else if (ev->button() == Qt::RightButton && ev->y() >= 26) { - int column = (ev->x() / m_nGridWidth); Preferences* pPref = Preferences::get_instance(); - if ( column >= (int) m_pHydrogen->getSong()->getPatternGroupVector()->size() ) { + if ( nColumn >= (int) m_pHydrogen->getSong()->getPatternGroupVector()->size() ) { pPref->unsetPunchArea(); return; } @@ -2531,12 +2740,9 @@ void SongEditorPositionRuler::mousePressEvent( QMouseEvent *ev ) } m_bRightBtnPressed = true; // Disable until mouse is moved - pPref->setPunchInPos(column); + pPref->setPunchInPos( nColumn ); pPref->setPunchOutPos(-1); update(); - } else if( ( ev->button() == Qt::LeftButton || ev->button() == Qt::RightButton ) - && ev->y() <= 25 && m_pHydrogen->isTimelineEnabled() ){ - showBpmWidget( ev->x() / m_nGridWidth ); } } @@ -2553,37 +2759,30 @@ void SongEditorPositionRuler::mouseReleaseEvent(QMouseEvent *ev) void SongEditorPositionRuler::paintEvent( QPaintEvent *ev ) { + auto pHydrogenApp = HydrogenApp::get_instance(); + auto pSongEditor = pHydrogenApp->getSongEditorPanel()->getSongEditor(); + auto pTimeline = m_pHydrogen->getTimeline(); + auto pPref = Preferences::get_instance(); + auto tempoMarkerVector = pTimeline->getAllTempoMarkers(); + if (!isVisible()) { return; } + + QColor textColor( pPref->getColorTheme()->m_songEditor_textColor ); + QColor textColorAlpha( textColor ); + textColorAlpha.setAlpha( 45 ); + QColor highlightColor = pPref->getColorTheme()->m_highlightColor; + QColor colorHovered( highlightColor ); + colorHovered.setAlpha( 200 ); + QColor backgroundColor = pPref->getColorTheme()->m_songEditor_alternateRowColor.darker( 115 ); + QColor backgroundColorTempoMarkers = backgroundColor.darker( 120 ); - float fPos = m_pHydrogen->getAudioEngine()->getColumn(); int pIPos = Preferences::get_instance()->getPunchInPos(); int pOPos = Preferences::get_instance()->getPunchOutPos(); - m_pAudioEngine->lock( RIGHT_HERE ); - - auto pPatternGroupVector = Hydrogen::get_instance()->getSong()->getPatternGroupVector(); - int nColumn = m_pAudioEngine->getColumn(); - - if ( pPatternGroupVector->size() >= nColumn && - pPatternGroupVector->at( nColumn )->size() > 0 ) { - int nLength = pPatternGroupVector->at( nColumn )->longest_pattern_length(); - fPos += (float)m_pAudioEngine->getPatternTickPosition() / (float)nLength; - } else { - // nessun pattern, uso la grandezza di default - fPos += (float)m_pAudioEngine->getPatternTickPosition() / (float)MAX_NOTES; - } - - if ( m_pHydrogen->getMode() == Song::Mode::Pattern ) { - fPos = -1; - pIPos = 0; - pOPos = -1; - } - - m_pAudioEngine->unlock(); - QPainter painter(this); + QFont font( pPref->getApplicationFontFamily(), getPointSize( pPref->getFontSize() ) ); qreal pixelRatio = devicePixelRatio(); if ( pixelRatio != m_pBackgroundPixmap->devicePixelRatio() ) { createBackground(); @@ -2595,18 +2794,198 @@ void SongEditorPositionRuler::paintEvent( QPaintEvent *ev ) pixelRatio * ev->rect().height() ); painter.drawPixmap( ev->rect(), *m_pBackgroundPixmap, srcRect ); + + // Which tempo marker is the currently used one? + int nCurrentTempoMarkerColumn = -1; + for ( const auto& tempoMarker : tempoMarkerVector ) { + if ( tempoMarker->nColumn > m_nColumn ) { + break; + } + nCurrentTempoMarkerColumn = tempoMarker->nColumn; + } + if ( nCurrentTempoMarkerColumn == -1 && + tempoMarkerVector.size() != 0 ) { + auto pTempoMarker = tempoMarkerVector[ tempoMarkerVector.size() - 1 ]; + if ( pTempoMarker != nullptr ) { + nCurrentTempoMarkerColumn = pTempoMarker->nColumn; + } + } + if ( nCurrentTempoMarkerColumn != -1 ) { + auto pTempoMarker = pTimeline->getTempoMarkerAtColumn( nCurrentTempoMarkerColumn ); + if ( pTempoMarker != nullptr ) { + drawTempoMarker( pTempoMarker, true, painter ); + } + } + + // Draw playhead + if ( m_fTick != -1 ) { + int nX = static_cast( static_cast(SongEditor::nMargin) + 1 + + m_fTick * static_cast(m_nGridWidth) - + static_cast(Skin::nPlayheadWidth) / 2 ); + int nShaftOffset = Skin::getPlayheadShaftOffset(); + Skin::drawPlayhead( &painter, nX, height() / 2 + 2, false ); + painter.drawLine( nX + nShaftOffset, 0, nX + nShaftOffset, height() ); + } + + // Highlight hovered tick of the Timeline + if ( m_hoveredRow == HoveredRow::Tag || + ( m_hoveredRow == HoveredRow::TempoMarker && + m_pHydrogen->isTimelineEnabled() ) ) { + + painter.setPen( QPen( highlightColor, 1 ) ); + int x = SongEditor::nMargin + m_nHoveredColumn * m_nGridWidth; - if (fPos != -1) { - uint x = (int)( m_nMargin + fPos * m_nGridWidth - m_nPlayheadWidth / 2 ); - painter.drawPixmap( QRect( x, height() / 2, m_nPlayheadWidth, m_nPlayheadHeight ), - m_tickPositionPixmap, - QRect( 0, 0, m_nPlayheadWidth, m_nPlayheadHeight ) ); - painter.setPen( QColor(35, 39, 51) ); - painter.drawLine( x + m_nXShaft, m_nPlayheadHeight, x + m_nXShaft, 24 ); + painter.drawLine( x, 1, x, 4 ); + painter.drawLine( x, height() / 2 - 5, x, height() / 2 ); + } + + // Highlight tag + bool bTagPresent = false; + if ( m_hoveredRow == HoveredRow::Tag && + pTimeline->hasColumnTag( m_nHoveredColumn ) ) { + + int x = SongEditor::nMargin + m_nHoveredColumn * m_nGridWidth + 4; + QRect rect( x, height() / 2 - 1 - m_nTagHeight, + m_nGridWidth - 6, m_nTagHeight ); + + QFont font2( pPref->getApplicationFontFamily(), 5 ); + painter.setFont( font2 ); + + painter.fillRect( rect, pPref->getColorTheme()->m_highlightColor ); + painter.setPen( pPref->getColorTheme()->m_highlightedTextColor ); + painter.drawText( rect, Qt::AlignCenter, "T"); + + painter.setFont( font ); + bTagPresent = true; + } + + // Draw a slight highlight around the tempo marker hovered using + // mouse or touch events. This will also redraw the + // tempo marker to ensure it's visible (they can overlap with + // neighboring ones and be hardly readable). + bool bTempoMarkerPresent = false; + if ( ! bTagPresent && + ( ( m_pHydrogen->isTimelineEnabled() && + m_hoveredRow == HoveredRow::TempoMarker ) || + m_nActiveBpmWidgetColumn != -1 ) ) { + + int nColumn; + if ( m_nActiveBpmWidgetColumn != -1 ) { + nColumn = m_nActiveBpmWidgetColumn; + } else { + nColumn = m_nHoveredColumn; + } + + if ( pTimeline->hasColumnTempoMarker( nColumn ) || + ( pTimeline->isFirstTempoMarkerSpecial() && + nColumn == 0 ) ) { + + QRect rect( SongEditor::nMargin - SONG_EDITOR_MAX_GRID_WIDTH + + nColumn * m_nGridWidth + m_nGridWidth / 2, + 6, 2 * SONG_EDITOR_MAX_GRID_WIDTH, 12 ); + painter.fillRect( rect, backgroundColorTempoMarkers ); + + auto pTempoMarker = pTimeline->getTempoMarkerAtColumn( nColumn ); + if ( pTempoMarker != nullptr ) { + drawTempoMarker( pTempoMarker, + pTempoMarker->nColumn == nCurrentTempoMarkerColumn, // emphasize + painter ); + } + + if ( m_nActiveBpmWidgetColumn == -1 ) { + painter.setPen( QPen( colorHovered, 1 ) ); + } else { + painter.setPen( QPen( highlightColor, 1 ) ); + } + painter.drawRect( rect ); + + bTempoMarkerPresent = true; + } + } + + // Draw hovering highlights in tempo marker row + if ( ( m_nHoveredColumn > -1 && + ( ( m_hoveredRow == HoveredRow::Tag && !bTagPresent ) || + ( m_hoveredRow == HoveredRow::TempoMarker && + m_pHydrogen->isTimelineEnabled() && + ! bTempoMarkerPresent ) ) ) || + ( m_nActiveBpmWidgetColumn != -1 && + ! bTempoMarkerPresent ) ) { + + QColor color; + if ( m_nActiveBpmWidgetColumn == -1 ) { + color = colorHovered; + } else { + color = highlightColor; + } + QPen p( color ); + p.setWidth( 1 ); + painter.setPen( p ); + + int nCursorX; + if ( m_nActiveBpmWidgetColumn != -1 ) { + nCursorX = SongEditor::nMargin + + m_nActiveBpmWidgetColumn * m_nGridWidth + 3; + } else { + nCursorX = SongEditor::nMargin + + m_nHoveredColumn * m_nGridWidth + 3; + } + + if ( m_hoveredRow == HoveredRow::TempoMarker || + m_nActiveBpmWidgetColumn != -1 ) { + painter.drawRect( nCursorX, 6, m_nGridWidth - 5, 12 ); + } else { + painter.drawRect( nCursorX, height() / 2 - 1 - m_nTagHeight, + m_nGridWidth - 5, m_nTagHeight - 1 ); + } + } + + // Draw cursor + if ( ! pHydrogenApp->hideKeyboardCursor() && pSongEditor->hasFocus() ) { + int nCursorX = SongEditor::nMargin + + pSongEditor->getCursorColumn() * m_nGridWidth + 2; + + QColor cursorColor = pPref->getColorTheme()->m_cursorColor; + + QPen p( cursorColor ); + p.setWidth( 2 ); + painter.setPen( p ); + painter.setRenderHint( QPainter::Antialiasing ); + // Aim to leave a visible gap between the border of the + // pattern cell, and the cursor line, for consistency and + // visibility. + painter.drawLine( nCursorX, height() / 2 + 3, + nCursorX + m_nGridWidth - 3, height() / 2 + 3 ); + painter.drawLine( nCursorX, height() / 2 + 4, + nCursorX, height() / 2 + 5 ); + painter.drawLine( nCursorX + m_nGridWidth - 3, height() / 2 + 4, + nCursorX + m_nGridWidth - 3, height() / 2 + 5 ); + painter.drawLine( nCursorX, height() - 4, + nCursorX + m_nGridWidth - 3, height() - 4 ); + painter.drawLine( nCursorX, height() - 6, + nCursorX, height() - 5 ); + painter.drawLine( nCursorX + m_nGridWidth - 3, height() - 6, + nCursorX + m_nGridWidth - 3, height() - 5 ); + } + + // Faint playhead over hovered position marker. + if ( m_nHoveredColumn > -1 && + m_hoveredRow == HoveredRow::Ruler && + m_nHoveredColumn <= m_nActiveColumns ) { + + int x = static_cast( static_cast(SongEditor::nMargin) + 1 + + static_cast(m_nHoveredColumn) * + static_cast(m_nGridWidth) - + static_cast(Skin::nPlayheadWidth) / 2 ); + int nShaftOffset = Skin::getPlayheadShaftOffset(); + Skin::drawPlayhead( &painter, x, height() / 2 + 2, true ); + painter.drawLine( x + nShaftOffset, 0, x + nShaftOffset, height() / 2 + 1 ); + painter.drawLine( x + nShaftOffset, height() / 2 + 2 + Skin::nPlayheadHeight, + x + nShaftOffset, height() ); } if ( pIPos <= pOPos ) { - int xIn = (int)( m_nMargin + pIPos * m_nGridWidth ); + int xIn = (int)( SongEditor::nMargin + pIPos * m_nGridWidth ); int xOut = (int)( 9 + (pOPos+1) * m_nGridWidth ); painter.fillRect( xIn, 30, xOut-xIn+1, 12, QColor(200, 100, 100, 100) ); QPen pen(QColor(200, 100, 100)); @@ -2616,16 +2995,95 @@ void SongEditorPositionRuler::paintEvent( QPaintEvent *ev ) } +void SongEditorPositionRuler::drawTempoMarker( std::shared_ptr tempoMarker, bool bEmphasize, QPainter& painter ) { + + auto pPref = Preferences::get_instance(); + auto pHydrogen = Hydrogen::get_instance(); + auto pSong = pHydrogen->getSong(); + auto pTimeline = pHydrogen->getTimeline(); + QFont font( pPref->getApplicationFontFamily(), getPointSize( pPref->getFontSize() ) ); + + QColor tempoMarkerColor; + tempoMarkerColor = pPref->getColorTheme()->m_songEditor_textColor; + if ( ! pHydrogen->isTimelineEnabled() ) { + tempoMarkerColor.setAlpha( 45 ); + } + + if ( tempoMarker->nColumn == 0 && pTimeline->isFirstTempoMarkerSpecial() ) { + if ( ! pHydrogen->isTimelineEnabled() ) { + // Omit the special tempo marker. + return; + } + painter.setPen( tempoMarkerColor.darker( 150 ) ); + } else { + painter.setPen( tempoMarkerColor ); + } + + if ( bEmphasize ) { + font.setBold( true ); + } + painter.setFont( font ); + + QRect rect( SongEditor::nMargin - SONG_EDITOR_MAX_GRID_WIDTH + + tempoMarker->nColumn * m_nGridWidth + m_nGridWidth / 2, + 6, 2 * SONG_EDITOR_MAX_GRID_WIDTH, 12 ); + + char tempo[10]; + sprintf( tempo, "%d", static_cast( tempoMarker->fBpm ) ); + painter.drawText( rect, Qt::AlignCenter, tempo ); + if ( bEmphasize ) { + font.setBold( false ); + } + painter.setFont( font ); +} void SongEditorPositionRuler::updatePosition() { - update(); + auto pTimeline = m_pHydrogen->getTimeline(); + auto pPref = Preferences::get_instance(); + auto tempoMarkerVector = pTimeline->getAllTempoMarkers(); + + float fTick = m_pAudioEngine->getColumn(); + + m_pAudioEngine->lock( RIGHT_HERE ); + + auto pPatternGroupVector = m_pHydrogen->getSong()->getPatternGroupVector(); + m_nColumn = m_pAudioEngine->getColumn(); + + if ( pPatternGroupVector->size() >= m_nColumn && + pPatternGroupVector->at( m_nColumn )->size() > 0 ) { + int nLength = pPatternGroupVector->at( m_nColumn )->longest_pattern_length(); + fTick += (float)m_pAudioEngine->getPatternTickPosition() / (float)nLength; + } else { + // Empty column. Use the default length. + fTick += (float)m_pAudioEngine->getPatternTickPosition() / (float)MAX_NOTES; + } + + if ( m_pHydrogen->getMode() == Song::Mode::Pattern ) { + fTick = -1; + } + + m_pAudioEngine->unlock(); + + if ( fTick != m_fTick ) { + + m_fTick = fTick; + + update(); + auto pSongEditorPanel = HydrogenApp::get_instance()->getSongEditorPanel(); + if ( pSongEditorPanel != nullptr ) { + pSongEditorPanel->getSongEditor()->updatePosition( fTick ); + pSongEditorPanel->getPlaybackTrackWaveDisplay()->updatePosition( fTick ); + pSongEditorPanel->getAutomationPathView()->updatePosition( fTick ); + } + } } void SongEditorPositionRuler::timelineUpdateEvent( int nValue ) { createBackground(); + update(); } void SongEditorPositionRuler::onPreferencesChanged( H2Core::Preferences::Changes changes ) diff --git a/src/gui/src/SongEditor/SongEditor.h b/src/gui/src/SongEditor/SongEditor.h index 03e667f5b0..b3b8fd57e6 100644 --- a/src/gui/src/SongEditor/SongEditor.h +++ b/src/gui/src/SongEditor/SongEditor.h @@ -24,6 +24,7 @@ #define SONG_EDITOR_H #include +#include #include @@ -33,10 +34,12 @@ #include #include +#include #include "../EventListener.h" #include "PatternFillDialog.h" #include "../Selection.h" #include "../Widgets/WidgetWithScalableFont.h" +#include "../Widgets/WidgetWithHighlightedList.h" namespace H2Core { class Hydrogen; @@ -81,6 +84,7 @@ class SongEditor : public QWidget ~SongEditor(); void createBackground(); + void updatePosition( float fTick ); void cleanUp(); @@ -100,6 +104,8 @@ class SongEditor : public QWidget int yScrollTarget( QScrollArea *pScrollArea, int *pnPatternInView ); + static constexpr int nMargin = 10; + public slots: void selectAll(); @@ -152,8 +158,6 @@ class SongEditor : public QWidget QPixmap * m_pSequencePixmap; //! @} - const int m_nMargin = 10; - //! @name Position of the keyboard input cursor //! @{ int m_nCursorRow; @@ -193,6 +197,7 @@ class SongEditor : public QWidget virtual void keyReleaseEvent (QKeyEvent *ev) override; virtual void paintEvent(QPaintEvent *ev) override; virtual void focusInEvent( QFocusEvent *ev ) override; + virtual void focusOutEvent( QFocusEvent *ev ) override; virtual void enterEvent( QEvent *ev ) override; virtual void leaveEvent( QEvent *ev ) override; //! @} @@ -208,6 +213,9 @@ class SongEditor : public QWidget std::map< QPoint, GridCell > m_gridCells; void updateGridCells(); bool m_bEntered; + + /** Cached position of the playhead.*/ + float m_fTick; public: void patternModifiedEvent() override; @@ -244,12 +252,17 @@ inline int SongEditor::getCursorColumn() const { /// Song editor pattern list /// /** \ingroup docGUI*/ -class SongEditorPatternList : public QWidget, protected WidgetWithScalableFont<8, 10, 12>, public H2Core::Object, public EventListener +class SongEditorPatternList : public QWidget + , protected WidgetWithScalableFont<8, 10, 12> + , public WidgetWithHighlightedList + , public H2Core::Object + , public EventListener { H2_OBJECT(SongEditorPatternList) Q_OBJECT public: + explicit SongEditorPatternList( QWidget *parent ); ~SongEditorPatternList(); @@ -310,14 +323,24 @@ class SongEditorPatternList : public QWidget, protected WidgetWithScalableFont< virtual void mousePressEvent( QMouseEvent *ev ) override; virtual void mouseDoubleClickEvent( QMouseEvent *ev ) override; virtual void paintEvent( QPaintEvent *ev ) override; + virtual void mouseMoveEvent(QMouseEvent *event) override; + virtual void leaveEvent( QEvent *ev ); + QPoint __drag_start_position; void togglePattern( int ); virtual void patternChangedEvent() override; virtual void songModeActivationEvent( int nValue ) override; - void mouseMoveEvent(QMouseEvent *event) override; - QPoint __drag_start_position; + virtual void stackedModeActivationEvent( int nValue ) override; + void setRowSelection( RowSelection rowSelection ); + /** + * Specifies the row the mouse cursor is currently hovered + * over. -1 for no cursor. + */ + int m_nRowHovered; + + QTimer* m_pHighlightLockedTimer; }; @@ -338,11 +361,13 @@ class SongEditorPositionRuler : public QWidget, protected WidgetWithScalableFon uint getGridWidth(); void setGridWidth (uint width); - int getPlayheadWidth() const; void tempoChangedEvent( int ) override; void patternChangedEvent() override; void songModeActivationEvent( int nValue ) override; + void relocationEvent() override; + void songSizeChangedEvent() override; void patternModifiedEvent() override; + void updateSongEvent( int ) override; void timelineActivationEvent( int nValue ) override; void timelineUpdateEvent( int nValue ) override; @@ -363,27 +388,37 @@ class SongEditorPositionRuler : public QWidget, protected WidgetWithScalableFon uint m_nGridWidth; uint m_nMaxPatternSequence; uint m_nInitialWidth; - static const uint m_nHeight = 50; - const int m_nMargin = 10; - - /** Width of the playhead pixmap in pixel.*/ - int m_nPlayheadWidth; - /** Height of the playhead pixmap in pixel.*/ - int m_nPlayheadHeight; - /** Horizontal offset of the line used to represent the base of - the playhead.*/ - int m_nXShaft; + static constexpr uint m_nHeight = 50; int m_nActiveBpmWidgetColumn; int m_nHoveredColumn; - /** 0 if the mouse cursor is located in the top 20 pixels of the - widget. 1 for the tags and -1 if uninitilialized. Used for caching - (in order to not redraw the Ruler in every mouse move*/ - bool m_nHoveredRow; - bool m_bHighlightHoveredColumn; + + /** Indicated the part of the widget the cursor is hovering over.*/ + enum class HoveredRow { + /** Cursor is not hovering the widget.*/ + None, + /** Upper half until the lower end of the tempo marker + text. */ + TempoMarker, + /** Still part of the upper half, but only the last + #m_nTagHeight pixels.*/ + Tag, + /** Lower half*/ + Ruler + }; + HoveredRow m_hoveredRow; + + /** Cached position of the playhead.*/ + float m_fTick; + /** Cached and coarsed-grained position of the playhead.*/ + int m_nColumn; + + int m_nTagHeight; + + /** Area covering the length of the song columns.*/ + int m_nActiveColumns; QPixmap * m_pBackgroundPixmap; - QPixmap m_tickPositionPixmap; bool m_bRightBtnPressed; virtual void mouseMoveEvent(QMouseEvent *ev) override; @@ -397,10 +432,9 @@ class SongEditorPositionRuler : public QWidget, protected WidgetWithScalableFon void showToolTip( QHelpEvent* ev ); -}; + void drawTempoMarker( std::shared_ptr tempoMarker, + bool bEmphasize, QPainter& painter ); -inline int SongEditorPositionRuler::getPlayheadWidth() const { - return m_nPlayheadWidth; -} +}; #endif diff --git a/src/gui/src/SongEditor/SongEditorPanel.cpp b/src/gui/src/SongEditor/SongEditorPanel.cpp index 5877dd12af..a753f06e98 100644 --- a/src/gui/src/SongEditor/SongEditorPanel.cpp +++ b/src/gui/src/SongEditor/SongEditorPanel.cpp @@ -26,6 +26,7 @@ #include "../HydrogenApp.h" #include "../PatternPropertiesDialog.h" #include "../SongPropertiesDialog.h" +#include "../Skin.h" #include "../Widgets/AutomationPathView.h" #include "../Widgets/Button.h" #include "../Widgets/Fader.h" @@ -78,7 +79,6 @@ SongEditorPanel::SongEditorPanel(QWidget *pParent) m_pTimelineBtn->move( 94, 4 ); m_pTimelineBtn->setObjectName( "TimelineBtn" ); connect( m_pTimelineBtn, SIGNAL( pressed() ), this, SLOT( timelineBtnPressed() ) ); - m_bLastIsTimelineActivated = pSong->getIsTimelineActivated(); if ( pHydrogen->getJackTimebaseState() == JackAudioDriver::Timebase::Slave ) { m_pTimelineBtn->setToolTip( pCommonStrings->getTimelineDisabledTimebaseSlave() ); m_pTimelineBtn->setIsActive( false ); @@ -88,67 +88,111 @@ SongEditorPanel::SongEditorPanel(QWidget *pParent) } else { m_pTimelineBtn->setChecked( m_bLastIsTimelineActivated ); } + m_bLastIsTimelineActivated = pSong->getIsTimelineActivated(); // clear sequence button - m_pClearPatternSeqBtn = new Button( pBackPanel, QSize( 60, 19 ), Button::Type::Push, "", pCommonStrings->getClearButton(), false, QSize(), tr("Clear pattern sequence") ); - m_pClearPatternSeqBtn->move( 2, 26 ); + m_pClearPatternSeqBtn = new Button( pBackPanel, QSize( 61, 21 ), Button::Type::Push, "", pCommonStrings->getClearButton(), false, QSize(), tr("Clear pattern sequence") ); + m_pClearPatternSeqBtn->move( 2, 25 ); connect( m_pClearPatternSeqBtn, SIGNAL( pressed() ), this, SLOT( clearSequence() ) ); // new pattern button - Button *newPatBtn = new Button( pBackPanel, QSize( 20, 19 ), Button::Type::Push, "plus.svg", "", false, QSize( 11, 11 ), tr("Create new pattern") ); - newPatBtn->move( 64, 26 ); + Button *newPatBtn = new Button( pBackPanel, QSize( 25, 21 ), Button::Type::Push, "plus.svg", "", false, QSize( 15, 15 ), tr("Create new pattern") ); + newPatBtn->move( 64, 25 ); connect( newPatBtn, SIGNAL( pressed() ), this, SLOT( newPatBtnClicked() ) ); // down button - m_pDownBtn = new Button( pBackPanel, QSize( 20, 19 ), Button::Type::Push, "down.svg", "", false, QSize( 11, 11 ), tr("Move the selected pattern down") ); - m_pDownBtn->move( 87, 26 ); + m_pDownBtn = new Button( pBackPanel, QSize( 25, 10 ), Button::Type::Push, + "down.svg", "", false, QSize( 7, 7 ), + tr("Move the selected pattern down"), false, true, "2" ); + m_pDownBtn->move( 90, 36 ); connect( m_pDownBtn, SIGNAL( pressed() ), this, SLOT( downBtnClicked() ) ); // up button - m_pUpBtn = new Button( pBackPanel, QSize( 20, 19 ), Button::Type::Push, "up.svg", "", false, QSize( 11, 11 ), tr("Move the selected pattern up") ); - m_pUpBtn->move( 106, 26 ); + m_pUpBtn = new Button( pBackPanel, QSize( 25, 10 ), Button::Type::Push, + "up.svg", "", false, QSize( 7, 7 ), + tr("Move the selected pattern up"), false, true, "2" ); + m_pUpBtn->move( 90, 25 ); connect( m_pUpBtn, SIGNAL( pressed() ), this, SLOT( upBtnClicked() ) ); - // select toggle button - m_pSelectionModeBtn = new Button( pBackPanel, QSize( 20, 19 ), Button::Type::Toggle, "select.svg", "", false, QSize( 15, 12 ), tr( "Select mode" ) ); - m_pSelectionModeBtn->move( 128, 26 ); + // Two buttons sharing the same position and either of them is + // shown unpressed. + m_pSelectionModeBtn = new Button( pBackPanel, QSize( 25, 21 ), Button::Type::Toggle, "select.svg", "", false, QSize( 17, 16 ), tr( "Select mode" ) ); + m_pSelectionModeBtn->move( 116, 25 ); connect( m_pSelectionModeBtn, SIGNAL( pressed() ), this, SLOT( selectionModeBtnPressed() ) ); - // draw toggle button - m_pDrawModeBtn = new Button( pBackPanel, QSize( 20, 19 ), Button::Type::Toggle, "draw.svg", "", false, QSize( 15, 12 ), tr( "Draw mode") ); - m_pDrawModeBtn->move( 147, 26 ); + m_pDrawModeBtn = new Button( pBackPanel, QSize( 25, 21 ), Button::Type::Toggle, "draw.svg", "", false, QSize( 17, 16 ), tr( "Draw mode") ); + m_pDrawModeBtn->move( 116, 25 ); connect( m_pDrawModeBtn, SIGNAL( pressed() ), this, SLOT( drawModeBtnPressed() ) ); - if ( pSong->getActionMode() == H2Core::Song::ActionMode::selectMode ) { - m_pSelectionModeBtn->setChecked( true ); - m_pDrawModeBtn->setChecked( false ); + if ( pHydrogen->getActionMode() == H2Core::Song::ActionMode::selectMode ) { + m_pDrawModeBtn->hide(); + } else { + m_pSelectionModeBtn->hide(); + } + + // Two buttons sharing the same position and either of them is + // shown unpressed. + m_pPatternEditorLockedBtn = new Button( pBackPanel, QSize( 25, 21 ), + Button::Type::Toggle, "lock_closed.svg", + "", false, QSize( 21, 17 ), + pCommonStrings->getPatternEditorLocked(), + false, true ); + m_pPatternEditorLockedBtn->move( 142, 25 ); + if ( pHydrogen->getMode() == Song::Mode::Pattern ) { + m_pPatternEditorLockedBtn->setChecked( false ); } else { - m_pSelectionModeBtn->setChecked( false ); - m_pDrawModeBtn->setChecked( true ); + m_pPatternEditorLockedBtn->setChecked( true ); } + m_pPatternEditorLockedBtn->setVisible( pHydrogen->isPatternEditorLocked() ); + connect( m_pPatternEditorLockedBtn, &Button::pressed, + [=](){Hydrogen::get_instance()->setIsPatternEditorLocked( false ); } ); + + m_pPatternEditorUnlockedBtn = new Button( pBackPanel, QSize( 25, 21 ), + Button::Type::Push, + "lock_open.svg", "", false, + QSize( 21, 17 ), + pCommonStrings->getPatternEditorLocked(), + false, true ); + m_pPatternEditorUnlockedBtn->move( 142, 25 ); + m_pPatternEditorUnlockedBtn->setVisible( ! pHydrogen->isPatternEditorLocked() ); + connect( m_pPatternEditorUnlockedBtn, &Button::pressed, + [=](){Hydrogen::get_instance()->setIsPatternEditorLocked( true ); } ); // Two buttons sharing the same position and either of them is // shown unpressed. - m_pModeActionSingleBtn = new Button( pBackPanel, QSize( 23, 19 ), + m_pPlaySelectedSingleBtn = new Button( pBackPanel, QSize( 25, 21 ), Button::Type::Push, "single_layer.svg", - "", false, QSize( 15, 11 ), + "", false, QSize( 17, 13 ), tr( "single pattern mode"), false, true ); - m_pModeActionSingleBtn->move( 170, 26 ); - m_pModeActionSingleBtn->setVisible( pPref->patternModePlaysSelected() ); - connect( m_pModeActionSingleBtn, SIGNAL( pressed() ), this, SLOT( modeActionBtnPressed() ) ); + m_pPlaySelectedSingleBtn->move( 168, 25 ); + connect( m_pPlaySelectedSingleBtn, &Button::pressed, [=]() { + Hydrogen::get_instance()->setPatternMode( Song::PatternMode::Stacked ); + }); - m_pModeActionMultipleBtn = new Button( pBackPanel, QSize( 23, 19 ), + m_pPlaySelectedMultipleBtn = new Button( pBackPanel, QSize( 25, 21 ), Button::Type::Push, "multiple_layers.svg", "", false, - QSize( 19, 15 ), + QSize( 21, 17 ), tr( "stacked pattern mode"), false, true ); - m_pModeActionMultipleBtn->move( 170, 26 ); - m_pModeActionMultipleBtn->hide(); - m_pModeActionMultipleBtn->setVisible( pPref->patternModePlaysSelected() ); - connect( m_pModeActionMultipleBtn, SIGNAL( pressed() ), this, SLOT( modeActionBtnPressed() ) ); - setModeActionBtn( Preferences::get_instance()->patternModePlaysSelected() ); + m_pPlaySelectedMultipleBtn->move( 168, 25 ); + m_pPlaySelectedMultipleBtn->hide(); + connect( m_pPlaySelectedMultipleBtn, &Button::pressed, [=]() { + Hydrogen::get_instance()->setPatternMode( Song::PatternMode::Selected ); + }); + + // We access the raw variable in the song class since we do not + // care whether Hydrogen is in song or pattern mode in here. + if ( pHydrogen->getSong() == nullptr || + pHydrogen->getSong()->getPatternMode() == Song::PatternMode::Selected ) { + m_pPlaySelectedSingleBtn->setVisible( true ); + m_pPlaySelectedMultipleBtn->setVisible( false ); + } + else { + m_pPlaySelectedSingleBtn->setVisible( false ); + m_pPlaySelectedMultipleBtn->setVisible( true ); + } // ZOOM m_pHScrollBar = new QScrollBar( Qt::Horizontal, nullptr ); @@ -216,6 +260,8 @@ SongEditorPanel::SongEditorPanel(QWidget *pParent) QWidget *pHScrollbarPanel = new QWidget(); pHScrollbarPanel->setLayout( pHZoomLayout ); + songModeActivationEvent(0); + //~ ZOOM // PATTERN LIST @@ -321,13 +367,14 @@ SongEditorPanel::SongEditorPanel(QWidget *pParent) pGridLayout->addWidget( m_pEditorScrollView, 1, 1 ); pGridLayout->addWidget( m_pVScrollBar, 1, 2, 2, 1 ); pGridLayout->addWidget( m_pAutomationPathScrollView, 2, 1); - pGridLayout->addWidget( m_pAutomationCombo, 2, 0, Qt::AlignTop | Qt::AlignRight ); + pGridLayout->addWidget( m_pAutomationCombo, 2, 0, + Qt::AlignVCenter | Qt::AlignRight ); pGridLayout->addWidget( pHScrollbarPanel, 3, 1 ); if( !pPref->getShowAutomationArea() ){ m_pAutomationPathScrollView->hide(); m_pAutomationCombo->hide(); } - + this->setLayout( pGridLayout ); QPalette defaultPalette; defaultPalette.setColor( QPalette::Window, QColor( 58, 62, 72 ) ); @@ -343,8 +390,6 @@ SongEditorPanel::SongEditorPanel(QWidget *pParent) connect(m_pTimer, SIGNAL(timeout()), this, SLOT( updatePlayHeadPosition() ) ); connect(m_pTimer, SIGNAL(timeout()), this, SLOT( updatePlaybackFaderPeaks() ) ); - // connect(HydrogenApp::get_instance()->getPlayerControl(), SIGNAL(songModeChanged()), - // this, SLOT(onSongModeChanged())); m_pTimer->start(100); } @@ -389,8 +434,8 @@ void SongEditorPanel::updatePlayHeadPosition() int nIncrement = 100; if ( nIncrement > std::round( static_cast(nWidth) / 3 ) ) { nIncrement = std::round( static_cast(nWidth) / 3 ); - } else if ( nIncrement < 2 * m_pPositionRuler->getPlayheadWidth() ) { - nIncrement = 2 * m_pPositionRuler->getPlayheadWidth(); + } else if ( nIncrement < 2 * Skin::nPlayheadWidth ) { + nIncrement = 2 * Skin::nPlayheadWidth; } if ( nPlayHeadPosition > ( x + nWidth - std::floor( static_cast( nIncrement ) / 2 ) ) ) { @@ -402,6 +447,10 @@ void SongEditorPanel::updatePlayHeadPosition() } } +void SongEditorPanel::highlightPatternEditorLocked( bool bUseRedBackground ) { + m_pPatternEditorLockedBtn->setUseRedBackground( bUseRedBackground ); +} + void SongEditorPanel::updatePlaybackFaderPeaks() { Sampler* pSampler = Hydrogen::get_instance()->getAudioEngine()->getSampler(); @@ -472,10 +521,8 @@ void SongEditorPanel::hScrollTo( int value ) /// void SongEditorPanel::updateAll() { - Hydrogen * pHydrogen = Hydrogen::get_instance(); - std::shared_ptr pSong = pHydrogen->getSong(); - - updatePlaybackTrackIfNecessary(); + auto pHydrogen = Hydrogen::get_instance(); + auto pSong = pHydrogen->getSong(); m_pPatternList->createBackground(); m_pPatternList->update(); @@ -487,11 +534,7 @@ void SongEditorPanel::updateAll() updatePositionRuler(); - m_pAutomationPathView->setAutomationPath( pSong->getVelocityAutomationPath() ); - - resyncExternalScrollBar(); - - m_pPlaybackTrackFader->setIsActive( ! H2Core::Hydrogen::get_instance()->getSong()->getPlaybackTrackFilename().isEmpty() ); + patternModifiedEvent(); } void SongEditorPanel::patternModifiedEvent() { @@ -533,8 +576,16 @@ void SongEditorPanel::newPatBtnClicked() PatternPropertiesDialog *pDialog = new PatternPropertiesDialog( this, pNewPattern, 0, true ); if ( pDialog->exec() == QDialog::Accepted ) { + int nRow; + if ( pHydrogen->getSelectedPatternNumber() == -1 ) { + nRow = pPatternList->size(); + } else { + nRow = pHydrogen->getSelectedPatternNumber() + 1; + } SE_insertPatternAction* pAction = - new SE_insertPatternAction( pHydrogen->getSelectedPatternNumber() + 1, new Pattern( pNewPattern->get_name() , pNewPattern->get_info(), pNewPattern->get_category() ) ); + new SE_insertPatternAction( nRow, new Pattern( pNewPattern->get_name(), + pNewPattern->get_info(), + pNewPattern->get_category() ) ); HydrogenApp::get_instance()->m_pUndoStack->push( pAction ); } @@ -569,7 +620,8 @@ void SongEditorPanel::downBtnClicked() std::shared_ptr pSong = pHydrogen->getSong(); PatternList *pPatternList = pSong->getPatternList(); - if( pHydrogen->getSelectedPatternNumber() +1 >= pPatternList->size() ) { + if( pHydrogen->getSelectedPatternNumber() < 0 || + pHydrogen->getSelectedPatternNumber() + 1 >= pPatternList->size() ) { return; } @@ -647,39 +699,65 @@ void SongEditorPanel::resizeEvent( QResizeEvent *ev ) resyncExternalScrollBar(); } -void SongEditorPanel::actionModeChangeEvent( int nValue ) { +void SongEditorPanel::updateSongEvent( int nValue ) { - if ( nValue == 0 ) { - if ( ! m_pSelectionModeBtn->isDown() ) { - m_pSelectionModeBtn->setChecked( true ); - } - m_pDrawModeBtn->setChecked( false ); - } else if ( nValue == 1 ) { - m_pSelectionModeBtn->setChecked( false ); - if ( ! m_pDrawModeBtn->isDown() ) { - m_pDrawModeBtn->setChecked( true ); - } + if ( nValue == 0 ) { // different song opened + patternEditorLockedEvent( 0 ); + actionModeChangeEvent( 0 ); + stackedModeActivationEvent( 0 ); + jackTimebaseStateChangedEvent( 0 ); + songModeActivationEvent( 0 ); + timelineActivationEvent( 0 ); + + updateAll(); + } +} + +void SongEditorPanel::patternEditorLockedEvent( int ) { + + auto pHydrogen = Hydrogen::get_instance(); + if ( ! m_pPatternEditorLockedBtn->isDown() && + pHydrogen->getMode() == Song::Mode::Song ) { + m_pPatternEditorLockedBtn->setChecked( true ); + + } else if ( ! m_pPatternEditorLockedBtn->isDown() && + pHydrogen->getMode() == Song::Mode::Pattern ) { + m_pPatternEditorLockedBtn->setChecked( false ); + } + m_pPatternEditorUnlockedBtn->setChecked( false ); + + if ( pHydrogen->getSong()->getIsPatternEditorLocked() ) { + m_pPatternEditorLockedBtn->show(); + m_pPatternEditorUnlockedBtn->hide(); } else { - ERRORLOG( QString( "Unknown EVENT_ACTION_MODE_CHANGE value" ) ); + m_pPatternEditorLockedBtn->hide(); + m_pPatternEditorUnlockedBtn->show(); } } -void SongEditorPanel::selectionModeBtnPressed() -{ - if ( Hydrogen::get_instance()->getSong()->getActionMode() != H2Core::Song::ActionMode::selectMode ) { - Hydrogen::get_instance()->getSong()->setActionMode( H2Core::Song::ActionMode::selectMode ); +void SongEditorPanel::actionModeChangeEvent( int ) { + + m_pSelectionModeBtn->setChecked( false ); + m_pDrawModeBtn->setChecked( false ); + + if ( Hydrogen::get_instance()->getActionMode() == + H2Core::Song::ActionMode::drawMode ) { + m_pDrawModeBtn->show(); + m_pSelectionModeBtn->hide(); } else { - m_pSelectionModeBtn->setChecked( false ); + m_pDrawModeBtn->hide(); + m_pSelectionModeBtn->show(); } } +void SongEditorPanel::selectionModeBtnPressed() +{ + Hydrogen::get_instance()->setActionMode( H2Core::Song::ActionMode::drawMode ); +} + void SongEditorPanel::drawModeBtnPressed() { - if ( Hydrogen::get_instance()->getSong()->getActionMode() != H2Core::Song::ActionMode::drawMode ) { - Hydrogen::get_instance()->getSong()->setActionMode( H2Core::Song::ActionMode::drawMode ); - } else { - m_pDrawModeBtn->setChecked( false ); - } + Hydrogen::get_instance()->setActionMode( H2Core::Song::ActionMode::selectMode ); } @@ -806,38 +884,20 @@ void SongEditorPanel::editPlaybackTrackBtnPressed() updateAll(); } -void SongEditorPanel::modeActionBtnPressed( ) +void SongEditorPanel::stackedModeActivationEvent( int ) { - bool bWasStacked = m_pModeActionSingleBtn->isVisible(); - if( bWasStacked ){ - m_pModeActionSingleBtn->hide(); - m_pModeActionMultipleBtn->show(); - } else { - m_pModeActionSingleBtn->show(); - m_pModeActionMultipleBtn->hide(); - } - Hydrogen::get_instance()->setPlaysSelected( bWasStacked ); - Hydrogen::get_instance()->setIsModified( true ); - EventQueue::get_instance()->push_event( EVENT_STACKED_MODE_ACTIVATION, bWasStacked ? 0 : 1 ); - updateAll(); -} - -void SongEditorPanel::setModeActionBtn( bool mode ) -{ - if( mode ){ - m_pModeActionSingleBtn->hide(); - m_pModeActionMultipleBtn->show(); - } else { - m_pModeActionSingleBtn->show(); - m_pModeActionMultipleBtn->hide(); + auto pHydrogen = Hydrogen::get_instance(); + + // We access the raw variable in the song class since we do not + // care whether Hydrogen is in song or pattern mode in here. + if ( pHydrogen->getSong() == nullptr || + pHydrogen->getSong()->getPatternMode() == Song::PatternMode::Selected ) { + m_pPlaySelectedSingleBtn->setVisible( true ); + m_pPlaySelectedMultipleBtn->setVisible( false ); } - // Set disabled or enabled - if ( Hydrogen::get_instance()->getMode() == Song::Mode::Song ) { - m_pModeActionMultipleBtn->setDisabled( true ); - m_pModeActionSingleBtn->setDisabled( true ); - } else { - m_pModeActionMultipleBtn->setDisabled( false ); - m_pModeActionSingleBtn->setDisabled( false ); + else { + m_pPlaySelectedSingleBtn->setVisible( false ); + m_pPlaySelectedMultipleBtn->setVisible( true ); } } @@ -884,12 +944,16 @@ void SongEditorPanel::faderChanged( WidgetWithInput *pRef ) void SongEditorPanel::selectedPatternChangedEvent() { - setModeActionBtn( Preferences::get_instance()->patternModePlaysSelected() ); updateAll(); + auto pHydrogen = Hydrogen::get_instance(); + if ( pHydrogen->getSelectedPatternNumber() == -1 ) { + return; + } + // Make sure currently selected pattern is visible. int nGridHeight = m_pPatternList->getGridHeight(); - m_pPatternListScrollView->ensureVisible( 0, (Hydrogen::get_instance()->getSelectedPatternNumber() + m_pPatternListScrollView->ensureVisible( 0, (pHydrogen->getSelectedPatternNumber() * nGridHeight + nGridHeight/2 ), 0, nGridHeight ); } @@ -952,11 +1016,29 @@ void SongEditorPanel::songModeActivationEvent( int ) { if ( pHydrogen->getMode() == Song::Mode::Pattern ) { setTimelineEnabled( false ); m_pTimelineBtn->setToolTip( pCommonStrings->getTimelineDisabledPatternMode() ); + + // Since the recorded notes will always enter the selected + // pattern in pattern mode, the behavior doesn't change + // regardless of whether the PatternEditor is locked or + // not. This redundancy is highlighted by unchecking the button. + m_pPatternEditorLockedBtn->setChecked( false ); } else if ( pHydrogen->getJackTimebaseState() != JackAudioDriver::Timebase::Slave ) { setTimelineEnabled( true ); m_pTimelineBtn->setToolTip( pCommonStrings->getTimelineEnabled() ); + + // We check the locked button to indicate it does take effect + // while in song mode. + m_pPatternEditorLockedBtn->setChecked( true ); + } + + // Set disabled or enabled + if ( Hydrogen::get_instance()->getMode() == Song::Mode::Song ) { + m_pPlaySelectedMultipleBtn->setDisabled( true ); + m_pPlaySelectedSingleBtn->setDisabled( true ); + } else { + m_pPlaySelectedMultipleBtn->setDisabled( false ); + m_pPlaySelectedSingleBtn->setDisabled( false ); } - setModeActionBtn( Preferences::get_instance()->patternModePlaysSelected() ); } void SongEditorPanel::timelineActivationEvent( int ){ @@ -1032,7 +1114,8 @@ void SongEditorPanel::patternChangedEvent() { int scroll = m_pSongEditor->yScrollTarget( m_pEditorScrollView, &nPatternInView ); vScrollTo( scroll ); - if ( pPref->patternFollowsSong() ) { + if ( pPref->patternFollowsSong() && + ! pHydrogen->isPatternEditorLocked()) { // Selected pattern follows song. // // If the currently selected pattern is no longer one of those currently playing in the song, then @@ -1060,6 +1143,7 @@ void SongEditorPanel::patternChangedEvent() { } if ( !bFound && patternList.size() != 0 ) { assert( nPatternInView != -1 ); + pHydrogen->setSelectedPatternNumber( nPatternInView ); } } diff --git a/src/gui/src/SongEditor/SongEditorPanel.h b/src/gui/src/SongEditor/SongEditorPanel.h index d796e26165..60e3f99852 100644 --- a/src/gui/src/SongEditor/SongEditorPanel.h +++ b/src/gui/src/SongEditor/SongEditorPanel.h @@ -53,10 +53,11 @@ class SongEditorPanel : public QWidget, public EventListener, public H2Core::O explicit SongEditorPanel( QWidget *parent ); ~SongEditorPanel(); - SongEditor* getSongEditor(){ return m_pSongEditor; } - SongEditorPatternList* getSongEditorPatternList(){ return m_pPatternList; } - SongEditorPositionRuler* getSongEditorPositionRuler(){ return m_pPositionRuler; } + SongEditor* getSongEditor() const { return m_pSongEditor; } + SongEditorPatternList* getSongEditorPatternList() const { return m_pPatternList; } + SongEditorPositionRuler* getSongEditorPositionRuler() const { return m_pPositionRuler; } AutomationPathView* getAutomationPathView() const { return m_pAutomationPathView; } + PlaybackTrackWaveDisplay* getPlaybackTrackWaveDisplay() const { return m_pPlaybackTrackWaveDisplay; } void updateAll(); void updatePositionRuler(); @@ -70,9 +71,12 @@ class SongEditorPanel : public QWidget, public EventListener, public H2Core::O void setTimelineActive( bool bActive ); bool getTimelineEnabled() const; void setTimelineEnabled( bool bEnabled ); - - // Implements EventListener interface - virtual void selectedPatternChangedEvent() override; + + /** + * Turns the background color of #m_pPatternEditorLockedBtn red to + * signal the user her last action was not permitted. + */ + void highlightPatternEditorLocked( bool bUseRedBackground ); void restoreGroupVector( QString filename ); //~ Implements EventListener interface /** Disables and deactivates the Timeline when an external @@ -80,6 +84,9 @@ class SongEditorPanel : public QWidget, public EventListener, public H2Core::O * gone or Hydrogen itself becomes the timebase master. */ void updateTimelineUsage(); + + // Implements EventListener interface + virtual void selectedPatternChangedEvent() override; virtual void timelineActivationEvent( int nValue ) override; /** Updates the associated buttons if the action mode was * changed within the core. @@ -94,11 +101,12 @@ class SongEditorPanel : public QWidget, public EventListener, public H2Core::O virtual void patternChangedEvent() override; - + virtual void patternEditorLockedEvent( int ) override; + virtual void stackedModeActivationEvent( int ) override; + virtual void updateSongEvent( int ) override; virtual void songModeActivationEvent( int nValue ) override; public slots: - void setModeActionBtn( bool mode ); void showHideTimeline( bool bPressed ) { m_pTimelineBtn->setChecked( bPressed ); timelineBtnPressed(); @@ -123,7 +131,6 @@ class SongEditorPanel : public QWidget, public EventListener, public H2Core::O void viewPlaybackTrackBtnPressed(); void mutePlaybackTrackBtnPressed(); void editPlaybackTrackBtnPressed(); - void modeActionBtnPressed( ); void zoomInBtnPressed(); void zoomOutBtnPressed(); @@ -162,8 +169,6 @@ class SongEditorPanel : public QWidget, public EventListener, public H2Core::O Button * m_pDownBtn; Button * m_pClearPatternSeqBtn; Button * m_pSelectionModeBtn; - Button * m_pModeActionSingleBtn; - Button * m_pModeActionMultipleBtn; Button * m_pDrawModeBtn; Fader* m_pPlaybackTrackFader; @@ -174,6 +179,11 @@ class SongEditorPanel : public QWidget, public EventListener, public H2Core::O Button * m_pMutePlaybackBtn; Button * m_pEditPlaybackBtn; + Button * m_pPlaySelectedSingleBtn; + Button * m_pPlaySelectedMultipleBtn; + Button * m_pPatternEditorLockedBtn; + Button * m_pPatternEditorUnlockedBtn; + QTimer* m_pTimer; AutomationPathView * m_pAutomationPathView; diff --git a/src/gui/src/UndoActions.h b/src/gui/src/UndoActions.h index c79ae38003..a8bd0fe385 100644 --- a/src/gui/src/UndoActions.h +++ b/src/gui/src/UndoActions.h @@ -672,40 +672,148 @@ class SE_moveNoteAction : public QUndoCommand }; /** \ingroup docGUI*/ -class SE_editNoteLenghtAction : public QUndoCommand +class SE_editNoteLengthAction : public QUndoCommand { public: - SE_editNoteLenghtAction( int nColumn, int nRealColumn, int row, int length, int oldLength, int selectedPatternNumber ){ + SE_editNoteLengthAction( int nColumn, int nRealColumn, int nRow, int nLength, + int nOldLength, int nSelectedPatternNumber, + int nSelectedInstrumentNumber, + PatternEditor::Editor editor ){ setText( QObject::tr( "Change note length" ) ); - __nColumn = nColumn; - __nRealColumn = nRealColumn; - __row = row; - __length = length; - __oldLength = oldLength; - __selectedPatternNumber = selectedPatternNumber; + m_nColumn = nColumn; + m_nRealColumn = nRealColumn; + m_nRow = nRow; + m_nLength = nLength; + m_nOldLength = nOldLength; + m_nSelectedPatternNumber = nSelectedPatternNumber; + m_nSelectedInstrumentNumber = nSelectedInstrumentNumber; + m_editor = editor; } virtual void undo() { - //qDebug() << "Change note length Undo "; - HydrogenApp* h2app = HydrogenApp::get_instance(); - h2app->getPatternEditorPanel()->getDrumPatternEditor()->editNoteLengthAction( __nColumn, __nRealColumn, __row, __oldLength, __selectedPatternNumber ); + // For now it does not matter which derived class of the + // PatternEditor will execute the call to + // editNoteLengthAction(). + HydrogenApp::get_instance()->getPatternEditorPanel()->getDrumPatternEditor() + ->editNoteLengthAction( m_nColumn, m_nRealColumn, m_nRow, + m_nOldLength, m_nSelectedPatternNumber, + m_nSelectedInstrumentNumber, m_editor ); } virtual void redo() { - //qDebug() << "Change note length Redo " ; - HydrogenApp* h2app = HydrogenApp::get_instance(); - h2app->getPatternEditorPanel()->getDrumPatternEditor()->editNoteLengthAction( __nColumn, __nRealColumn, __row, __length, __selectedPatternNumber ); + // For now it does not matter which derived class of the + // PatternEditor will execute the call to + // editNoteLengthAction(). + HydrogenApp::get_instance()->getPatternEditorPanel()->getDrumPatternEditor() + ->editNoteLengthAction( m_nColumn, m_nRealColumn, m_nRow, + m_nLength, m_nSelectedPatternNumber, + m_nSelectedInstrumentNumber, m_editor ); } private: - int __nColumn; - int __nRealColumn; - int __row; - int __oldLength; - int __length; - int __selectedPatternNumber; + int m_nColumn; + int m_nRealColumn; + int m_nRow; + int m_nLength; + int m_nOldLength; + int m_nSelectedPatternNumber; + int m_nSelectedInstrumentNumber; + PatternEditor::Editor m_editor; }; +/** \ingroup docGUI*/ +class SE_editNotePropertiesAction : public QUndoCommand +{ +public: + SE_editNotePropertiesAction( int nColumn, + int nRealColumn, + int nRow, + int nSelectedPatternNumber, + int nSelectedInstrumentNumber, + PatternEditor::Mode mode, + PatternEditor::Editor editor, + float fVelocity, + float fOldVelocity, + float fPan, + float fOldPan, + float fLeadLag, + float fOldLeadLag, + float fProbability, + float fOldProbability ){ + setText( QObject::tr( "Change note properties piano roll" ) + .append( QString( ": [%1" ) + .arg( PatternEditor::modeToQString( mode ) ) ) ); + m_nColumn = nColumn; + m_nRealColumn = nRealColumn; + m_nRow = nRow; + m_nSelectedPatternNumber = nSelectedPatternNumber; + m_nSelectedInstrumentNumber = nSelectedInstrumentNumber; + m_mode = mode; + m_editor = editor; + m_fVelocity = fVelocity; + m_fOldVelocity = fOldVelocity; + m_fPan = fPan; + m_fOldPan = fOldPan; + m_fLeadLag = fLeadLag; + m_fOldLeadLag = fOldLeadLag; + m_fProbability = fProbability; + m_fOldProbability = fOldProbability; + } + virtual void undo() + { + // For now it does not matter which derived class of the + // PatternEditor will execute the call to + // editNoteLengthAction(). + HydrogenApp::get_instance()->getPatternEditorPanel()->getPianoRollEditor()-> + editNotePropertiesAction( m_nColumn, + m_nRealColumn, + m_nRow, + m_nSelectedPatternNumber, + m_nSelectedInstrumentNumber, + m_mode, + m_editor, + m_fOldVelocity, + m_fOldPan, + m_fOldLeadLag, + m_fOldProbability ); + } + virtual void redo() + { + // For now it does not matter which derived class of the + // PatternEditor will execute the call to + // editNoteLengthAction(). + HydrogenApp::get_instance()->getPatternEditorPanel()->getPianoRollEditor()-> + editNotePropertiesAction( m_nColumn, + m_nRealColumn, + m_nRow, + m_nSelectedPatternNumber, + m_nSelectedInstrumentNumber, + m_mode, + m_editor, + m_fVelocity, + m_fPan, + m_fLeadLag, + m_fProbability ); + } + +private: + int m_nColumn; + int m_nRealColumn; + int m_nRow; + int m_nSelectedPatternNumber; + int m_nSelectedInstrumentNumber; + PatternEditor::Mode m_mode; + PatternEditor::Editor m_editor; + float m_fVelocity; + float m_fOldVelocity; + float m_fPan; + float m_fOldPan; + float m_fLeadLag; + float m_fOldLeadLag; + float m_fProbability; + float m_fOldProbability; +}; + /** \ingroup docGUI*/ class SE_clearNotesPatternEditorAction : public QUndoCommand { @@ -1123,117 +1231,6 @@ class SE_addPianoRollNoteOffAction : public QUndoCommand int __nSelectedInstrumentnumber; }; - -/** \ingroup docGUI*/ -class SE_editPianoRollNoteLengthAction : public QUndoCommand -{ -public: - SE_editPianoRollNoteLengthAction( int nColumn, int nRealColumn, int length, int oldLength, int selectedPatternNumber, int nSelectedInstrumentnumber, int pressedLine){ - setText( QObject::tr( "Change piano roll note length " ) ); - __nColumn = nColumn; - __nRealColumn = nRealColumn; - __length = length; - __oldLength = oldLength; - __selectedPatternNumber = selectedPatternNumber; - __nSelectedInstrumentnumber = nSelectedInstrumentnumber; - __pressedLine = pressedLine; - } - virtual void undo() - { - //qDebug() << "Change note length Piano Roll Undo "; - HydrogenApp* h2app = HydrogenApp::get_instance(); - h2app->getPatternEditorPanel()->getPianoRollEditor()->editNoteLengthAction( __nColumn, __nRealColumn, __oldLength, __selectedPatternNumber, __nSelectedInstrumentnumber, __pressedLine); - } - virtual void redo() - { - //qDebug() << "Change note length Piano RollRedo " ; - HydrogenApp* h2app = HydrogenApp::get_instance(); - h2app->getPatternEditorPanel()->getPianoRollEditor()->editNoteLengthAction( __nColumn, - __nRealColumn, - __length, - __selectedPatternNumber, - __nSelectedInstrumentnumber, - __pressedLine ); - } -private: - int __nColumn; - int __nRealColumn; - int __oldLength; - int __length; - int __selectedPatternNumber; - int __nSelectedInstrumentnumber; - int __pressedLine; -}; - -/** \ingroup docGUI*/ -class SE_editNotePropertiesPianoRollAction : public QUndoCommand -{ -public: - SE_editNotePropertiesPianoRollAction( int nColumn, - int nRealColumn, - int selectedPatternNumber, - int selectedInstrumentnumber, - float velocity, - float oldVelocity, - float fPan, - float fOldPan, - float leadLag, - float oldLeadLag, - int pressedLine ){ - setText( QObject::tr( "Change note properties piano roll" ) ); - __nColumn = nColumn; - __nRealColumn = nRealColumn; - __selectedPatternNumber = selectedPatternNumber; - __nSelectedInstrumentnumber = selectedInstrumentnumber; - __velocity = velocity; - __oldVelocity = oldVelocity; - m_fPan = fPan; - m_fOldPan = fOldPan; - __leadLag = leadLag; - __oldLeadLag = oldLeadLag; - __pressedLine = pressedLine; - } - virtual void undo() - { - //qDebug() << "Change Note properties Piano Roll Undo "; - HydrogenApp* h2app = HydrogenApp::get_instance(); - h2app->getPatternEditorPanel()->getPianoRollEditor()->editNotePropertiesAction( __nColumn, - __nRealColumn, - __selectedPatternNumber, - __nSelectedInstrumentnumber, - __oldVelocity, - m_fOldPan, - __oldLeadLag, - __pressedLine ); - } - virtual void redo() - { - //qDebug() << "Change Note properties Piano RollRedo " ; - HydrogenApp* h2app = HydrogenApp::get_instance(); - h2app->getPatternEditorPanel()->getPianoRollEditor()->editNotePropertiesAction( __nColumn, - __nRealColumn, - __selectedPatternNumber, - __nSelectedInstrumentnumber, - __velocity, - m_fPan, - __leadLag, - __pressedLine ); - } - -private: - int __nColumn; - int __nRealColumn; - int __selectedPatternNumber; - int __nSelectedInstrumentnumber; - float __velocity; - float __oldVelocity; - float m_fPan; - float m_fOldPan; - float __leadLag; - float __oldLeadLag; - int __pressedLine; -}; - /** \ingroup docGUI*/ class SE_moveNotePianoRollAction : public QUndoCommand { @@ -1293,7 +1290,7 @@ class SE_editNotePropertiesVolumeAction : public QUndoCommand public: SE_editNotePropertiesVolumeAction( int undoColumn, - QString mode, + NotePropertiesRuler::Mode mode, int nSelectedPatternNumber, int nSelectedInstrument, float velocity, @@ -1309,7 +1306,8 @@ class SE_editNotePropertiesVolumeAction : public QUndoCommand int octaveKeyVal, int oldOctaveKeyVal) { - setText( QObject::tr( "Edit note property %1" ).arg( mode.toLower() ) ); + setText( QObject::tr( "Edit note property %1" ) + .arg( NotePropertiesRuler::modeToQString( mode ) ) ); __undoColumn = undoColumn; __mode = mode; __nSelectedPatternNumber = nSelectedPatternNumber; @@ -1363,7 +1361,7 @@ class SE_editNotePropertiesVolumeAction : public QUndoCommand int __undoColumn; - QString __mode; + NotePropertiesRuler::Mode __mode; int __nSelectedPatternNumber; int __nSelectedInstrument; float __velocity; diff --git a/src/gui/src/Widgets/AutomationPathView.cpp b/src/gui/src/Widgets/AutomationPathView.cpp index 861f9212f9..f753c10a00 100644 --- a/src/gui/src/Widgets/AutomationPathView.cpp +++ b/src/gui/src/Widgets/AutomationPathView.cpp @@ -22,7 +22,9 @@ #include "AutomationPathView.h" #include "../SongEditor/SongEditor.h" +#include "../SongEditor/SongEditorPanel.h" #include "../HydrogenApp.h" +#include "../Skin.h" using namespace H2Core; @@ -30,9 +32,9 @@ AutomationPathView::AutomationPathView(QWidget *parent) : QWidget(parent), H2Core::Object(), m_nGridWidth(16), - m_nMarginWidth(10), m_nMarginHeight(4), - m_bIsHolding(false) + m_bIsHolding(false), + m_fTick( 0 ) { setFocusPolicy( Qt::ClickFocus ); Preferences *pPref = Preferences::get_instance(); @@ -42,35 +44,47 @@ AutomationPathView::AutomationPathView(QWidget *parent) _path = nullptr; autoResize(); + + qreal pixelRatio = devicePixelRatio(); + m_pBackgroundPixmap = new QPixmap( width() * pixelRatio, + height() * pixelRatio ); + m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio ); + createBackground(); } void AutomationPathView::onPreferencesChanged( H2Core::Preferences::Changes changes ) { if ( changes & H2Core::Preferences::Changes::Colors ) { + createBackground(); update(); } } -void AutomationPathView::setAutomationPath(AutomationPath *path) +void AutomationPathView::setAutomationPath( AutomationPath *path, bool bUpdate ) { if ( path == _path ) { return; } _path = path; - if ( _path ) { + + if( _path ) { _selectedPoint = path->end(); } - update(); + + if ( bUpdate ) { + createBackground(); + update(); + } } // Make sure we have the current automation path void AutomationPathView::updateAutomationPath() { auto pSong = Hydrogen::get_instance()->getSong(); - if ( pSong ) { - setAutomationPath( pSong->getVelocityAutomationPath() ); + if ( pSong != nullptr ) { + setAutomationPath( pSong->getVelocityAutomationPath(), false ); } else { - setAutomationPath( nullptr ); + setAutomationPath( nullptr, false ); } } @@ -79,6 +93,8 @@ void AutomationPathView::setGridWidth( int width ) if ( ( SONG_EDITOR_MIN_GRID_WIDTH <= width ) && ( SONG_EDITOR_MAX_GRID_WIDTH >= width ) ) { m_nGridWidth = width; autoResize(); + createBackground(); + update(); } } @@ -100,7 +116,7 @@ QPoint AutomationPathView::translatePoint(const std::pair &p) const int contentHeight = height() - 2* m_nMarginHeight; return QPoint( - m_nMarginWidth + p.first * m_nGridWidth, + SongEditor::nMargin + p.first * m_nGridWidth, m_nMarginHeight + contentHeight * ((_path->get_max()-p.second)/(_path->get_max()-_path->get_min())) ); } @@ -111,7 +127,7 @@ QPoint AutomationPathView::translatePoint(const std::pair &p) const */ bool AutomationPathView::checkBounds(QMouseEvent *event) const { - return event->x() > m_nMarginWidth + return event->x() > SongEditor::nMargin && event->y() > m_nMarginHeight && event->y() < height()-m_nMarginHeight; } @@ -124,28 +140,93 @@ std::pair AutomationPathView::locate(QMouseEvent *event) con { int contentHeight = height() - 2* m_nMarginHeight; - float x = (event->x() - m_nMarginWidth) / (float)m_nGridWidth; + float x = (event->x() - SongEditor::nMargin) / (float)m_nGridWidth; float y = ((contentHeight-event->y()+m_nMarginHeight)/(float)contentHeight) * (_path->get_max() - _path->get_min()) + _path->get_min(); return std::pair(x,y); } +void AutomationPathView::updatePosition( float fTick ) { + m_fTick = fTick; + update(); +} /** * \brief Repaint widget **/ -void AutomationPathView::paintEvent(QPaintEvent *event) +void AutomationPathView::paintEvent(QPaintEvent *ev) { + + if (!isVisible()) { + return; + } + + qreal pixelRatio = devicePixelRatio(); + if ( pixelRatio != m_pBackgroundPixmap->devicePixelRatio() || + width() != m_pBackgroundPixmap->width() || + height() != m_pBackgroundPixmap->height() ) { + createBackground(); + } + + QPainter painter( this ); + painter.drawPixmap( ev->rect(), *m_pBackgroundPixmap, + QRectF( pixelRatio * ev->rect().x(), + pixelRatio * ev->rect().y(), + pixelRatio * ev->rect().width(), + pixelRatio * ev->rect().height() ) ); + + // Draw playhead + // + // Using the grid width of the song editor over class' own one is + // crucial in order to keep the full-height playhead in sync. + auto pSongEditorPanel = HydrogenApp::get_instance()->getSongEditorPanel(); + if ( m_fTick != -1 && pSongEditorPanel != nullptr ) { + int nOffset = Skin::getPlayheadShaftOffset(); + int nX = static_cast( static_cast(SongEditor::nMargin) + 1 + + m_fTick * + static_cast(pSongEditorPanel->getSongEditor()-> + getGridWidth()) - + static_cast(Skin::nPlayheadWidth) / 2 ); + Skin::setPlayheadPen( &painter, false ); + painter.drawLine( nX + nOffset, 0, nX + nOffset, height() ); + } + +} +void AutomationPathView::createBackground() { + auto pPref = H2Core::Preferences::get_instance(); updateAutomationPath(); - QPainter painter(this); + QColor backgroundColor = + pPref->getColorTheme()->m_songEditor_automationBackgroundColor; + QColor automationLineColor = + pPref->getColorTheme()->m_songEditor_automationLineColor; + QColor nodeColor = pPref->getColorTheme()->m_songEditor_automationNodeColor; + QColor textColor = pPref->getColorTheme()->m_songEditor_textColor; + + // Resize pixmap if pixel ratio has changed + qreal pixelRatio = devicePixelRatio(); + if ( m_pBackgroundPixmap->devicePixelRatio() != pixelRatio || + width() != m_pBackgroundPixmap->width() || + height() != m_pBackgroundPixmap->height() ) { + delete m_pBackgroundPixmap; + m_pBackgroundPixmap = new QPixmap( width() * pixelRatio , height() * pixelRatio ); + m_pBackgroundPixmap->setDevicePixelRatio( pixelRatio ); + } + + m_pBackgroundPixmap->fill( backgroundColor ); + + QPainter painter( m_pBackgroundPixmap ); painter.setRenderHint(QPainter::Antialiasing); + // Border + painter.setPen( Qt::black ); + painter.drawLine( 0, 0, width(), 0 ); + QPen rulerPen(Qt::DotLine); - rulerPen.setColor( pPref->getColorTheme()->m_lightColor ); + rulerPen.setColor( textColor ); painter.setPen(rulerPen); /* Paint min, max */ @@ -160,7 +241,7 @@ void AutomationPathView::paintEvent(QPaintEvent *event) QPoint def = translatePoint(0, _path->get_default()); painter.drawLine(0, def.y(), width(), def.y()); - QPen linePen( pPref->getColorTheme()->m_automationColor ); + QPen linePen( automationLineColor ); linePen.setWidth(2); painter.setPen(linePen); @@ -183,7 +264,7 @@ void AutomationPathView::paintEvent(QPaintEvent *event) } - QPen circlePen( pPref->getColorTheme()->m_automationCircleColor ); + QPen circlePen( nodeColor ); circlePen.setWidth(1); painter.setPen(circlePen); painter.setBrush(QBrush( pPref->getColorTheme()->m_windowColor )); @@ -230,6 +311,7 @@ void AutomationPathView::mousePressEvent(QMouseEvent *event) } H2Core::Hydrogen::get_instance()->setIsModified( true ); + createBackground(); update(); m_bIsHolding = true; @@ -286,6 +368,7 @@ void AutomationPathView::mouseMoveEvent(QMouseEvent *event) H2Core::Hydrogen::get_instance()->setIsModified( true ); } + createBackground(); update(); } @@ -308,6 +391,7 @@ void AutomationPathView::keyPressEvent(QKeyEvent *event) H2Core::Hydrogen::get_instance()->setIsModified( true ); emit pointRemoved( x, y ); + createBackground(); update(); emit valueChanged(); @@ -325,5 +409,5 @@ void AutomationPathView::keyPressEvent(QKeyEvent *event) **/ void AutomationPathView::autoResize() { - resize ( 10 + m_nMaxPatternSequence * m_nGridWidth, 64 ); + resize( SongEditor::nMargin + m_nMaxPatternSequence * m_nGridWidth, 64 ); } diff --git a/src/gui/src/Widgets/AutomationPathView.h b/src/gui/src/Widgets/AutomationPathView.h index 0dbf582581..c8a1da26f1 100644 --- a/src/gui/src/Widgets/AutomationPathView.h +++ b/src/gui/src/Widgets/AutomationPathView.h @@ -36,9 +36,10 @@ class AutomationPathView : public QWidget, public H2Core::Object #include -Button::Button( QWidget *pParent, QSize size, Type type, const QString& sIcon, const QString& sText, bool bUseRedBackground, QSize iconSize, QString sBaseTooltip, bool bColorful, bool bModifyOnChange ) +Button::Button( QWidget *pParent, QSize size, Type type, const QString& sIcon, const QString& sText, bool bUseRedBackground, QSize iconSize, QString sBaseTooltip, bool bColorful, bool bModifyOnChange, const QString& sBorderRadius ) : QPushButton( pParent ) , m_size( size ) , m_iconSize( iconSize ) @@ -48,6 +48,7 @@ Button::Button( QWidget *pParent, QSize size, Type type, const QString& sIcon, c , m_bUseRedBackground( bUseRedBackground ) , m_nFixedFontSize( -1 ) , m_bModifyOnChange( bModifyOnChange ) + , m_sBorderRadius( sBorderRadius ) { setAttribute( Qt::WA_OpaquePaintEvent ); setFocusPolicy( Qt::NoFocus ); @@ -65,12 +66,14 @@ Button::Button( QWidget *pParent, QSize size, Type type, const QString& sIcon, c setText( sText ); } - if ( size.width() <= 12 || size.height() <= 12 ) { - m_sBorderRadius = "0"; - } else if ( size.width() <= 20 || size.height() <= 20 ) { - m_sBorderRadius = "3"; - } else { - m_sBorderRadius = "5"; + if ( m_sBorderRadius.isEmpty() ) { + if ( size.width() <= 12 || size.height() <= 12 ) { + m_sBorderRadius = "0"; + } else if ( size.width() <= 20 || size.height() <= 20 ) { + m_sBorderRadius = "3"; + } else { + m_sBorderRadius = "5"; + } } if ( type == Type::Toggle ) { @@ -118,6 +121,13 @@ void Button::updateIcon() { setIconSize( m_iconSize ); } +void Button::setUseRedBackground( bool bUseRedBackground ) { + m_bUseRedBackground = bUseRedBackground; + + updateStyleSheet(); + update(); +} + void Button::updateStyleSheet() { auto pPref = H2Core::Preferences::get_instance(); diff --git a/src/gui/src/Widgets/Button.h b/src/gui/src/Widgets/Button.h index f725998e45..9e12b99526 100644 --- a/src/gui/src/Widgets/Button.h +++ b/src/gui/src/Widgets/Button.h @@ -91,6 +91,8 @@ class Button : public QPushButton, protected WidgetWithScalableFont<6, 8, 10>, * \param bModifyOnChange Whether Hydrogen::setIsModified() is * invoked with `true` as soon as the value of the widget does * change. + * \param sBorderRadius Radius of the button in pixel, which will + * be passed to the style sheet. */ Button( QWidget *pParent, @@ -102,7 +104,8 @@ class Button : public QPushButton, protected WidgetWithScalableFont<6, 8, 10>, QSize iconSize = QSize( 0, 0 ), QString sBaseTooltip = "", bool bColorful = false, - bool bModifyOnChange = false + bool bModifyOnChange = false, + const QString& sBorderRadius = "" ); virtual ~Button(); @@ -121,6 +124,9 @@ class Button : public QPushButton, protected WidgetWithScalableFont<6, 8, 10>, void setFixedFontSize( int nPixelSize ); int getFixedFontSize() const; + void setUseRedBackground( bool bUseRedBackground ); + bool getUseRedBackground() const; + public slots: void onPreferencesChanged( H2Core::Preferences::Changes changes ); @@ -173,5 +179,8 @@ inline void Button::setFixedFontSize( int nPixelSize ) { inline int Button::getFixedFontSize() const { return m_nFixedFontSize; } +inline bool Button::getUseRedBackground() const { + return m_bUseRedBackground; +} #endif diff --git a/src/gui/src/Widgets/ClickableLabel.cpp b/src/gui/src/Widgets/ClickableLabel.cpp index cbd943936d..d378820622 100644 --- a/src/gui/src/Widgets/ClickableLabel.cpp +++ b/src/gui/src/Widgets/ClickableLabel.cpp @@ -28,11 +28,13 @@ #include -ClickableLabel::ClickableLabel( QWidget *pParent, QSize size, QString sText, Color color, bool bModifyOnChange ) +ClickableLabel::ClickableLabel( QWidget *pParent, QSize size, QString sText, Color color, bool bModifyOnChange, bool bIsEditable ) : QLabel( pParent ) , m_size( size ) , m_color( color ) , m_bModifyOnChange( bModifyOnChange ) + , m_bIsEditable( bIsEditable ) + , m_bEntered( false ) { if ( ! size.isNull() ) { setFixedSize( size ); @@ -71,6 +73,52 @@ void ClickableLabel::mousePressEvent( QMouseEvent * e ) emit labelClicked( this ); } +void ClickableLabel::paintEvent( QPaintEvent *ev ) { + + QLabel::paintEvent( ev ); + + if ( ! m_bIsEditable ) { + return; + } + + auto pPref = H2Core::Preferences::get_instance(); + + if ( m_bEntered || hasFocus() ) { + QPainter painter(this); + + QColor colorHighlightActive = pPref->getColorTheme()->m_highlightColor; + + // If the mouse is placed on the widget but the user hasn't + // clicked it yet, the highlight will be done more transparent to + // indicate that keyboard inputs are not accepted yet. + if ( ! hasFocus() ) { + colorHighlightActive.setAlpha( 150 ); + } + + QPen pen; + pen.setColor( colorHighlightActive ); + pen.setWidth( 3 ); + painter.setPen( pen ); + painter.drawRoundedRect( QRect( 0, 0, m_size.width(), m_size.height() ), 3, 3 ); + } +} + +void ClickableLabel::enterEvent( QEvent* ev ) { + QLabel::enterEvent( ev ); + if ( m_bIsEditable ) { + m_bEntered = true; + update(); + } +} + +void ClickableLabel::leaveEvent( QEvent* ev ) { + QLabel::leaveEvent( ev ); + if ( m_bIsEditable ) { + m_bEntered = false; + update(); + } +} + void ClickableLabel::updateFont( QString sFontFamily, H2Core::FontTheme::FontSize fontSize ) { int nPixelSize = 0; diff --git a/src/gui/src/Widgets/ClickableLabel.h b/src/gui/src/Widgets/ClickableLabel.h index c8c5e90d54..101f8cfb9b 100644 --- a/src/gui/src/Widgets/ClickableLabel.h +++ b/src/gui/src/Widgets/ClickableLabel.h @@ -51,8 +51,9 @@ class ClickableLabel : public QLabel, public H2Core::Object Dark }; - explicit ClickableLabel( QWidget *pParent, QSize size = QSize( 0, 0 ), QString sText = "", Color color = Color::Bright, bool bModifyOnChange = true ); - virtual void mousePressEvent( QMouseEvent * e ) override; + explicit ClickableLabel( QWidget *pParent, QSize size = QSize( 0, 0 ), + QString sText = "", Color color = Color::Bright, + bool bModifyOnChange = true, bool bIsEditable = false ); public slots: void onPreferencesChanged( H2Core::Preferences::Changes changes ); @@ -65,12 +66,22 @@ public slots: void updateStyleSheet(); void updateFont( QString sFontFamily, H2Core::FontTheme::FontSize fontSize ); + virtual void mousePressEvent( QMouseEvent * e ) override; + virtual void enterEvent( QEvent * e ) override; + virtual void leaveEvent( QEvent * e ) override; + virtual void paintEvent( QPaintEvent * e ) override; QSize m_size; Color m_color; /** Whether Hydrogen::setIsModified() is invoked with `true` as soon as the value of the widget does change.*/ bool m_bModifyOnChange; + + /** If set to true a highlight will be painted when hovered. This + should be set if a callback is connected and the user is able to + change its content.*/ + bool m_bIsEditable; + bool m_bEntered; }; diff --git a/src/gui/src/Widgets/WidgetWithHighlightedList.h b/src/gui/src/Widgets/WidgetWithHighlightedList.h new file mode 100644 index 0000000000..d85b632ee4 --- /dev/null +++ b/src/gui/src/Widgets/WidgetWithHighlightedList.h @@ -0,0 +1,84 @@ +/* + * Hydrogen + * Copyright (C) 2021 The hydrogen development team + * + * http://www.hydrogen-music.org + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY, without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#ifndef WIDGET_WITH_HIGHLIGHTED_LIST_H +#define WIDGET_WITH_HIGHLIGHTED_LIST_H + +/** Widget has a list of items associated with a popup which in turn + can open dialog windows. + */ +/** \ingroup docGUI*/ +class WidgetWithHighlightedList { +public: + /** + * Specifies whether the row corresponding to #m_nRowClicked should + * be highlighted and determines the lifecycle of the + * highlighting. + * + * The highlighting starts when the user right-clicks a row and + * the associated popup window is shown. This will set + * #m_rowSelection to RowSelection::Popup. As soon as the popup + * get's hidden (by clicking at an arbitrary position), it will be + * reset to RowSelection::None by a callback. If the user clicks + * an action of the popup, #m_rowSelection will be upgraded to + * RowSelection::Dialog and set the RowSelection::None once the + * associated dialog is closed. + * + * This slightly intricate state handling was introduced to ensure + * the associated row is highlighted whenever a popup and the + * corresponding dialog is opened. Else, one might very quickly + * forget whether "Pattern 6" or "Pattern 7" is the target of the + * delete operation. + */ + enum class RowSelection { + /** + * No highlighting will be drawn for the row last clicked. + */ + None, + /** + * The #m_nRowClicked row was right-clicked and a popup dialog + * did open and is still shown. + */ + Popup, + /** + * The popup dialog is already closed but the user clicked an + * associated action and its dialog is still opened. + */ + Dialog + }; + +protected: + WidgetWithHighlightedList() : m_nRowClicked( 0 ) + , m_rowSelection( RowSelection::None ) {}; + + /** + * Helper variable remembering for row was clicked last. + */ + int m_nRowClicked; + /** + * Determines the highlighting of the row associated with #m_nRowClicked. + */ + RowSelection m_rowSelection; + +}; + + #endif