winamp/Src/external_dependencies/openmpt-trunk/mptrack/View_pat.cpp

7321 lines
226 KiB
C++
Raw Normal View History

2024-09-24 13:54:57 +01:00
/*
* View_pat.cpp
* ------------
* Purpose: Pattern tab, lower panel.
* Notes : Welcome to about 7000 lines of, err, very beautiful code.
* Authors: Olivier Lapicque
* OpenMPT Devs
* The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
*/
#include "stdafx.h"
#include "Mptrack.h"
#include "Mainfrm.h"
#include "InputHandler.h"
#include "Childfrm.h"
#include "Moddoc.h"
#include "SampleEditorDialogs.h" // For amplification dialog (which is re-used from sample editor)
#include "Globals.h"
#include "View_pat.h"
#include "Ctrl_pat.h"
#include "PatternFont.h"
#include "PatternFindReplace.h"
#include "PatternFindReplaceDlg.h"
#include "EffectVis.h"
#include "PatternGotoDialog.h"
#include "MIDIMacros.h"
#include "../common/misc_util.h"
#include "../soundlib/MIDIEvents.h"
#include "../soundlib/mod_specifications.h"
#include "../soundlib/plugins/PlugInterface.h"
#include <algorithm>
OPENMPT_NAMESPACE_BEGIN
// Static initializers
ModCommand CViewPattern::m_cmdOld = ModCommand::Empty();
int32 CViewPattern::m_nTransposeAmount = 1;
IMPLEMENT_SERIAL(CViewPattern, CModScrollView, 0)
BEGIN_MESSAGE_MAP(CViewPattern, CModScrollView)
//{{AFX_MSG_MAP(CViewPattern)
ON_WM_ERASEBKGND()
ON_WM_VSCROLL()
ON_WM_SIZE()
ON_WM_MOUSEWHEEL()
ON_WM_XBUTTONUP()
ON_WM_MOUSEMOVE()
ON_WM_LBUTTONDOWN()
ON_WM_LBUTTONDBLCLK()
ON_WM_LBUTTONUP()
ON_WM_RBUTTONDOWN()
ON_WM_SETFOCUS()
ON_WM_KILLFOCUS()
ON_WM_SYSKEYDOWN()
ON_WM_DESTROY()
ON_MESSAGE(WM_MOD_KEYCOMMAND, &CViewPattern::OnCustomKeyMsg)
ON_MESSAGE(WM_MOD_MIDIMSG, &CViewPattern::OnMidiMsg)
ON_MESSAGE(WM_MOD_RECORDPARAM, &CViewPattern::OnRecordPlugParamChange)
ON_COMMAND(ID_EDIT_CUT, &CViewPattern::OnEditCut)
ON_COMMAND(ID_EDIT_COPY, &CViewPattern::OnEditCopy)
ON_COMMAND(ID_EDIT_PASTE, &CViewPattern::OnEditPaste)
ON_COMMAND(ID_EDIT_MIXPASTE, &CViewPattern::OnEditMixPaste)
ON_COMMAND(ID_EDIT_MIXPASTE_ITSTYLE,&CViewPattern::OnEditMixPasteITStyle)
ON_COMMAND(ID_EDIT_PASTEFLOOD, &CViewPattern::OnEditPasteFlood)
ON_COMMAND(ID_EDIT_PUSHFORWARDPASTE,&CViewPattern::OnEditPushForwardPaste)
ON_COMMAND(ID_EDIT_SELECT_ALL, &CViewPattern::OnEditSelectAll)
ON_COMMAND(ID_EDIT_SELECTCOLUMN,&CViewPattern::OnEditSelectChannel)
ON_COMMAND(ID_EDIT_SELECTCOLUMN2,&CViewPattern::OnSelectCurrentChannel)
ON_COMMAND(ID_EDIT_FIND, &CViewPattern::OnEditFind)
ON_COMMAND(ID_EDIT_GOTO_MENU, &CViewPattern::OnEditGoto)
ON_COMMAND(ID_EDIT_FINDNEXT, &CViewPattern::OnEditFindNext)
ON_COMMAND(ID_EDIT_RECSELECT, &CViewPattern::OnRecordSelect)
ON_COMMAND(ID_EDIT_SPLITRECSELECT, &CViewPattern::OnSplitRecordSelect)
ON_COMMAND(ID_EDIT_SPLITKEYBOARDSETTINGS, &CViewPattern::SetSplitKeyboardSettings)
ON_COMMAND(ID_EDIT_UNDO, &CViewPattern::OnEditUndo)
ON_COMMAND(ID_EDIT_REDO, &CViewPattern::OnEditRedo)
ON_COMMAND(ID_PATTERN_CHNRESET, &CViewPattern::OnChannelReset)
ON_COMMAND(ID_PATTERN_MUTE, &CViewPattern::OnMuteFromClick)
ON_COMMAND(ID_PATTERN_SOLO, &CViewPattern::OnSoloFromClick)
ON_COMMAND(ID_PATTERN_TRANSITIONMUTE, &CViewPattern::OnTogglePendingMuteFromClick)
ON_COMMAND(ID_PATTERN_TRANSITIONSOLO, &CViewPattern::OnPendingSoloChnFromClick)
ON_COMMAND(ID_PATTERN_TRANSITION_UNMUTEALL, &CViewPattern::OnPendingUnmuteAllChnFromClick)
ON_COMMAND(ID_PATTERN_UNMUTEALL,&CViewPattern::OnUnmuteAll)
ON_COMMAND(ID_PATTERN_SPLIT, &CViewPattern::OnSplitPattern)
ON_COMMAND(ID_NEXTINSTRUMENT, &CViewPattern::OnNextInstrument)
ON_COMMAND(ID_PREVINSTRUMENT, &CViewPattern::OnPrevInstrument)
ON_COMMAND(ID_PATTERN_PLAYROW, &CViewPattern::OnPatternStep)
ON_COMMAND(IDC_PATTERN_RECORD, &CViewPattern::OnPatternRecord)
ON_COMMAND(ID_PATTERN_DELETEROW, &CViewPattern::OnDeleteRow)
ON_COMMAND(ID_PATTERN_DELETEALLROW, &CViewPattern::OnDeleteWholeRow)
ON_COMMAND(ID_PATTERN_DELETEROWGLOBAL, &CViewPattern::OnDeleteRowGlobal)
ON_COMMAND(ID_PATTERN_DELETEALLROWGLOBAL, &CViewPattern::OnDeleteWholeRowGlobal)
ON_COMMAND(ID_PATTERN_INSERTROW, &CViewPattern::OnInsertRow)
ON_COMMAND(ID_PATTERN_INSERTALLROW, &CViewPattern::OnInsertWholeRow)
ON_COMMAND(ID_PATTERN_INSERTROWGLOBAL, &CViewPattern::OnInsertRowGlobal)
ON_COMMAND(ID_PATTERN_INSERTALLROWGLOBAL, &CViewPattern::OnInsertWholeRowGlobal)
ON_COMMAND(ID_RUN_SCRIPT, &CViewPattern::OnRunScript)
ON_COMMAND(ID_TRANSPOSE_UP, &CViewPattern::OnTransposeUp)
ON_COMMAND(ID_TRANSPOSE_DOWN, &CViewPattern::OnTransposeDown)
ON_COMMAND(ID_TRANSPOSE_OCTUP, &CViewPattern::OnTransposeOctUp)
ON_COMMAND(ID_TRANSPOSE_OCTDOWN, &CViewPattern::OnTransposeOctDown)
ON_COMMAND(ID_TRANSPOSE_CUSTOM, &CViewPattern::OnTransposeCustom)
ON_COMMAND(ID_PATTERN_PROPERTIES, &CViewPattern::OnPatternProperties)
ON_COMMAND(ID_PATTERN_INTERPOLATE_VOLUME, &CViewPattern::OnInterpolateVolume)
ON_COMMAND(ID_PATTERN_INTERPOLATE_EFFECT, &CViewPattern::OnInterpolateEffect)
ON_COMMAND(ID_PATTERN_INTERPOLATE_NOTE, &CViewPattern::OnInterpolateNote)
ON_COMMAND(ID_PATTERN_INTERPOLATE_INSTR, &CViewPattern::OnInterpolateInstr)
ON_COMMAND(ID_PATTERN_VISUALIZE_EFFECT, &CViewPattern::OnVisualizeEffect)
ON_COMMAND(ID_GROW_SELECTION, &CViewPattern::OnGrowSelection)
ON_COMMAND(ID_SHRINK_SELECTION, &CViewPattern::OnShrinkSelection)
ON_COMMAND(ID_PATTERN_SETINSTRUMENT, &CViewPattern::OnSetSelInstrument)
ON_COMMAND(ID_PATTERN_ADDCHANNEL_FRONT, &CViewPattern::OnAddChannelFront)
ON_COMMAND(ID_PATTERN_ADDCHANNEL_AFTER, &CViewPattern::OnAddChannelAfter)
ON_COMMAND(ID_PATTERN_RESETCHANNELCOLORS, &CViewPattern::OnResetChannelColors)
ON_COMMAND(ID_PATTERN_TRANSPOSECHANNEL, &CViewPattern::OnTransposeChannel)
ON_COMMAND(ID_PATTERN_DUPLICATECHANNEL, &CViewPattern::OnDuplicateChannel)
ON_COMMAND(ID_PATTERN_REMOVECHANNEL, &CViewPattern::OnRemoveChannel)
ON_COMMAND(ID_PATTERN_REMOVECHANNELDIALOG, &CViewPattern::OnRemoveChannelDialog)
ON_COMMAND(ID_PATTERN_AMPLIFY, &CViewPattern::OnPatternAmplify)
ON_COMMAND(ID_CLEAR_SELECTION, &CViewPattern::OnClearSelectionFromMenu)
ON_COMMAND(ID_SHOWTIMEATROW, &CViewPattern::OnShowTimeAtRow)
ON_COMMAND(ID_PATTERN_EDIT_PCNOTE_PLUGIN, &CViewPattern::OnTogglePCNotePluginEditor)
ON_COMMAND(ID_SETQUANTIZE, &CViewPattern::OnSetQuantize)
ON_COMMAND(ID_LOCK_PATTERN_ROWS, &CViewPattern::OnLockPatternRows)
ON_COMMAND_RANGE(ID_CHANGE_INSTRUMENT, ID_CHANGE_INSTRUMENT+MAX_INSTRUMENTS, &CViewPattern::OnSelectInstrument)
ON_COMMAND_RANGE(ID_CHANGE_PCNOTE_PARAM, ID_CHANGE_PCNOTE_PARAM + ModCommand::maxColumnValue, &CViewPattern::OnSelectPCNoteParam)
ON_UPDATE_COMMAND_UI(ID_EDIT_UNDO, &CViewPattern::OnUpdateUndo)
ON_UPDATE_COMMAND_UI(ID_EDIT_REDO, &CViewPattern::OnUpdateRedo)
ON_COMMAND_RANGE(ID_PLUGSELECT, ID_PLUGSELECT+MAX_MIXPLUGINS, &CViewPattern::OnSelectPlugin)
//}}AFX_MSG_MAP
ON_WM_INITMENU()
ON_WM_RBUTTONDBLCLK()
ON_WM_RBUTTONUP()
END_MESSAGE_MAP()
static_assert(ModCommand::maxColumnValue <= 999, "Command range for ID_CHANGE_PCNOTE_PARAM is designed for 999");
const CSoundFile *CViewPattern::GetSoundFile() const { return (GetDocument() != nullptr) ? &GetDocument()->GetSoundFile() : nullptr; };
CSoundFile *CViewPattern::GetSoundFile() { return (GetDocument() != nullptr) ? &GetDocument()->GetSoundFile() : nullptr; };
const ModSequence &CViewPattern::Order() const { return GetSoundFile()->Order(); }
ModSequence &CViewPattern::Order() { return GetSoundFile()->Order(); }
CViewPattern::CViewPattern()
{
EnableActiveAccessibility();
m_Dib.Init(CMainFrame::bmpNotes);
UpdateColors();
m_PCNoteEditMemory = ModCommand::Empty();
m_octaveKeyMemory.fill(NOTE_NONE);
}
CViewPattern::~CViewPattern()
{
m_offScreenBitmap.DeleteObject();
m_offScreenDC.DeleteDC();
}
void CViewPattern::OnInitialUpdate()
{
CModScrollView::OnInitialUpdate();
EnableToolTips();
ChnVUMeters.fill(0);
OldVUMeters.fill(0);
m_previousNote.fill(NOTE_NONE);
m_splitActiveNoteChannel.fill(NOTE_CHANNEL_MAP_INVALID);
m_activeNoteChannel.fill(NOTE_CHANNEL_MAP_INVALID);
m_nPlayPat = PATTERNINDEX_INVALID;
m_nPlayRow = m_nNextPlayRow = 0;
m_nPlayTick = 0;
m_nTicksOnRow = 1;
m_nMidRow = 0;
m_nDragItem = {};
m_bInItemRect = false;
m_bContinueSearch = false;
m_Status = psShowPluginNames;
m_nXScroll = m_nYScroll = 0;
m_nPattern = 0;
m_nSpacing = 0;
m_nAccelChar = 0;
PatternFont::UpdateFont(m_hWnd);
UpdateSizes();
UpdateScrollSize();
SetCurrentPattern(0);
m_fallbackInstrument = 0;
m_nLastPlayedRow = 0;
m_nLastPlayedOrder = ORDERINDEX_INVALID;
m_prevChordNote = NOTE_NONE;
}
bool CViewPattern::SetCurrentPattern(PATTERNINDEX pat, ROWINDEX row)
{
const CSoundFile *pSndFile = GetSoundFile();
if(pSndFile == nullptr)
return false;
if(pat == pSndFile->Order.GetIgnoreIndex() || pat == pSndFile->Order.GetInvalidPatIndex())
return false;
if(m_pEditWnd && m_pEditWnd->IsWindowVisible())
m_pEditWnd->ShowWindow(SW_HIDE);
m_nPattern = pat;
bool updateScroll = false;
if(pSndFile->Patterns.IsValidPat(pat))
{
if(row != ROWINDEX_INVALID && row != GetCurrentRow() && row < pSndFile->Patterns[m_nPattern].GetNumRows())
{
m_Cursor.SetRow(row);
updateScroll = true;
}
if(GetCurrentRow() >= pSndFile->Patterns[m_nPattern].GetNumRows())
{
m_Cursor.SetRow(0);
updateScroll = true;
}
}
SetSelToCursor();
UpdateSizes();
UpdateScrollSize();
UpdateIndicator();
if(m_bWholePatternFitsOnScreen)
SetScrollPos(SB_VERT, 0);
else if(updateScroll)
SetScrollPos(SB_VERT, (int)GetCurrentRow() * GetRowHeight());
UpdateScrollPos();
InvalidatePattern(true, true);
SendCtrlMessage(CTRLMSG_PATTERNCHANGED, m_nPattern);
return true;
}
// This should be used instead of consecutive calls to SetCurrentRow() then SetCurrentColumn().
bool CViewPattern::SetCursorPosition(const PatternCursor &cursor, bool wrap)
{
// Set row, but do not update scroll position yet
// as there is another position update on the way:
SetCurrentRow(cursor.GetRow(), wrap, false);
// Now set column and update scroll position:
SetCurrentColumn(cursor);
return true;
}
ROWINDEX CViewPattern::SetCurrentRow(ROWINDEX row, bool wrap, bool updateHorizontalScrollbar)
{
const CSoundFile *pSndFile = GetSoundFile();
if(pSndFile == nullptr || !pSndFile->Patterns.IsValidIndex(m_nPattern))
return ROWINDEX_INVALID;
if(wrap && pSndFile->Patterns[m_nPattern].GetNumRows())
{
const auto &order = Order();
if((int)row < 0)
{
if(m_Status[psKeyboardDragSelect | psMouseDragSelect])
{
row = 0;
} else if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL)
{
ORDERINDEX curOrder = GetCurrentOrder();
const ORDERINDEX prevOrd = order.GetPreviousOrderIgnoringSkips(curOrder);
if(prevOrd < curOrder && m_nPattern == order[curOrder])
{
const PATTERNINDEX nPrevPat = order[prevOrd];
if((nPrevPat < pSndFile->Patterns.Size()) && (pSndFile->Patterns[nPrevPat].GetNumRows()))
{
SetCurrentOrder(prevOrd);
if(SetCurrentPattern(nPrevPat))
return SetCurrentRow(pSndFile->Patterns[nPrevPat].GetNumRows() + (int)row);
}
}
row = 0;
} else if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_WRAP)
{
if((int)row < (int)0)
row += pSndFile->Patterns[m_nPattern].GetNumRows();
row %= pSndFile->Patterns[m_nPattern].GetNumRows();
}
} else //row >= 0
if(row >= pSndFile->Patterns[m_nPattern].GetNumRows())
{
if(m_Status[psKeyboardDragSelect | psMouseDragSelect])
{
row = pSndFile->Patterns[m_nPattern].GetNumRows() - 1;
} else if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL)
{
ORDERINDEX curOrder = GetCurrentOrder();
ORDERINDEX nextOrder = order.GetNextOrderIgnoringSkips(curOrder);
if(nextOrder > curOrder && m_nPattern == order[curOrder])
{
PATTERNINDEX nextPat = order[nextOrder];
if((nextPat < pSndFile->Patterns.Size()) && (pSndFile->Patterns[nextPat].GetNumRows()))
{
const ROWINDEX newRow = row - pSndFile->Patterns[m_nPattern].GetNumRows();
SetCurrentOrder(nextOrder);
if(SetCurrentPattern(nextPat))
return SetCurrentRow(newRow);
}
}
row = pSndFile->Patterns[m_nPattern].GetNumRows() - 1;
} else if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_WRAP)
{
row %= pSndFile->Patterns[m_nPattern].GetNumRows();
}
}
}
if(!(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL))
{
if(static_cast<int>(row) < 0)
row = 0;
if(row >= pSndFile->Patterns[m_nPattern].GetNumRows())
row = pSndFile->Patterns[m_nPattern].GetNumRows() - 1;
}
if((row >= pSndFile->Patterns[m_nPattern].GetNumRows()) || (!m_szCell.cy))
return false;
// Fix: If cursor isn't on screen move both scrollbars to make it visible
InvalidateRow();
m_Cursor.SetRow(row);
// Fix: Horizontal scrollbar pos screwed when selecting with mouse
UpdateScrollbarPositions(updateHorizontalScrollbar);
InvalidateRow();
PatternCursor selStart(m_Cursor);
if(m_Status[psKeyboardDragSelect | psMouseDragSelect] && !m_Status[psDragnDropEdit])
{
selStart.Set(m_StartSel);
}
SetCurSel(selStart, m_Cursor);
return row;
}
bool CViewPattern::SetCurrentColumn(CHANNELINDEX channel, PatternCursor::Columns column)
{
const CSoundFile *pSndFile = GetSoundFile();
if(pSndFile == nullptr)
{
return false;
}
LimitMax(column, m_nDetailLevel);
m_Cursor.SetColumn(channel, column);
PatternCursor selStart(m_Cursor);
if(m_Status[psKeyboardDragSelect | psMouseDragSelect] && !m_Status[psDragnDropEdit])
{
selStart = m_StartSel;
}
SetCurSel(selStart, m_Cursor);
// Fix: If cursor isn't on screen move both scrollbars to make it visible
UpdateScrollbarPositions();
return true;
}
// Set document as modified and optionally update all pattern views.
void CViewPattern::SetModified(bool updateAllViews)
{
CModDoc *pModDoc = GetDocument();
if(pModDoc != nullptr)
{
pModDoc->SetModified();
pModDoc->UpdateAllViews(this, PatternHint(m_nPattern).Data(), updateAllViews ? nullptr : this);
}
CMainFrame::GetMainFrame()->NotifyAccessibilityUpdate(*this);
}
// Fix: If cursor isn't on screen move scrollbars to make it visible
// Fix: save pattern scrollbar position when switching to other tab
// Assume that m_nRow and m_dwCursor are valid
// When we switching to other tab the CViewPattern object is deleted
// and when switching back new one is created
bool CViewPattern::UpdateScrollbarPositions(bool updateHorizontalScrollbar)
{
// HACK - after new CViewPattern object created SetCurrentRow() and SetCurrentColumn() are called -
// just skip first two calls of UpdateScrollbarPositions() if pModDoc->GetOldPatternScrollbarsPos() is valid
CModDoc *pModDoc = GetDocument();
if(pModDoc)
{
CSize scroll = pModDoc->GetOldPatternScrollbarsPos();
if(scroll.cx >= 0)
{
OnScrollBy(scroll);
scroll.cx = -1;
pModDoc->SetOldPatternScrollbarsPos(scroll);
return true;
} else if(scroll.cx >= -1)
{
scroll.cx = -2;
pModDoc->SetOldPatternScrollbarsPos(scroll);
return true;
}
}
CSize scroll(0, 0);
UINT row = GetCurrentRow();
UINT yofs = GetYScrollPos();
CRect rect;
GetClientRect(&rect);
rect.top += m_szHeader.cy;
int numrows = (rect.bottom - rect.top - 1) / m_szCell.cy;
if(numrows < 1)
numrows = 1;
if(m_nMidRow)
{
if(row != yofs)
{
scroll.cy = (int)(row - yofs) * m_szCell.cy;
}
} else
{
if(row < yofs)
{
scroll.cy = (int)(row - yofs) * m_szCell.cy;
} else if(row > yofs + (UINT)numrows - 1)
{
scroll.cy = (int)(row - yofs - numrows + 1) * m_szCell.cy;
}
}
if(updateHorizontalScrollbar)
{
UINT xofs = GetXScrollPos();
const CHANNELINDEX nchn = GetCurrentChannel();
if(nchn < xofs)
{
scroll.cx = (int)(xofs - nchn) * m_szCell.cx;
scroll.cx *= -1;
} else if(nchn > xofs)
{
int maxcol = (rect.right - m_szHeader.cx) / m_szCell.cx;
if((nchn >= (xofs + maxcol)) && (maxcol >= 0))
{
scroll.cx = (int)(nchn - xofs - maxcol + 1) * m_szCell.cx;
}
}
}
if(scroll.cx != 0 || scroll.cy != 0)
{
OnScrollBy(scroll, TRUE);
}
return true;
}
DragItem CViewPattern::GetDragItem(CPoint point, RECT &outRect) const
{
const CSoundFile *pSndFile = GetSoundFile();
if(pSndFile == nullptr)
return {};
CRect rcClient, rect, plugRect;
GetClientRect(&rcClient);
rect.SetRect(m_szHeader.cx, 0, m_szHeader.cx + GetChannelWidth(), m_szHeader.cy);
plugRect.SetRect(m_szHeader.cx, m_szHeader.cy - m_szPluginHeader.cy, m_szHeader.cx + GetChannelWidth(), m_szHeader.cy);
const auto xOffset = static_cast<CHANNELINDEX>(GetXScrollPos());
const CHANNELINDEX numChannels = pSndFile->GetNumChannels();
// Checking channel headers
if(m_Status[psShowPluginNames])
{
for(CHANNELINDEX n = xOffset; n < numChannels; n++)
{
if(plugRect.PtInRect(point))
{
outRect = plugRect;
return {DragItem::PluginName, n};
}
plugRect.OffsetRect(GetChannelWidth(), 0);
}
}
for(CHANNELINDEX n = xOffset; n < numChannels; n++)
{
if(rect.PtInRect(point))
{
outRect = rect;
return {DragItem::ChannelHeader, n};
}
rect.OffsetRect(GetChannelWidth(), 0);
}
if(pSndFile->Patterns.IsValidPat(m_nPattern) && (pSndFile->GetType() & (MOD_TYPE_XM | MOD_TYPE_IT | MOD_TYPE_MPT)))
{
// Clicking on upper-left corner with pattern number (for pattern properties)
rect.SetRect(0, 0, m_szHeader.cx, m_szHeader.cy);
if(rect.PtInRect(point))
{
outRect = rect;
return {DragItem::PatternHeader, 0};
}
}
return {};
}
// Drag a selection to position "cursor".
// If scrollHorizontal is true, the point's channel is ensured to be visible.
// Likewise, if scrollVertical is true, the point's row is ensured to be visible.
// If noMode if specified, the original selection points are not altered.
bool CViewPattern::DragToSel(const PatternCursor &cursor, bool scrollHorizontal, bool scrollVertical, bool noMove)
{
const CSoundFile *pSndFile = GetSoundFile();
if(pSndFile == nullptr || !pSndFile->Patterns.IsValidPat(m_nPattern))
return false;
CRect rect;
int yofs = GetYScrollPos(), xofs = GetXScrollPos();
int row, col;
if(!m_szCell.cy)
return false;
GetClientRect(&rect);
if(!noMove)
SetCurSel(m_StartSel, cursor);
if(!scrollHorizontal && !scrollVertical)
return true;
// Scroll to row
row = cursor.GetRow();
if(scrollVertical && row < (int)pSndFile->Patterns[m_nPattern].GetNumRows())
{
row += m_nMidRow;
rect.top += m_szHeader.cy;
int numrows = (rect.bottom - rect.top - 1) / m_szCell.cy;
if(numrows < 1)
numrows = 1;
if(row < yofs)
{
CSize sz;
sz.cx = 0;
sz.cy = (int)(row - yofs) * m_szCell.cy;
OnScrollBy(sz, TRUE);
} else if(row > yofs + numrows - 1)
{
CSize sz;
sz.cx = 0;
sz.cy = (int)(row - yofs - numrows + 1) * m_szCell.cy;
OnScrollBy(sz, TRUE);
}
}
// Scroll to column
col = cursor.GetChannel();
if(scrollHorizontal && col < (int)pSndFile->GetNumChannels())
{
int maxcol = (rect.right - m_szHeader.cx) - 4;
maxcol -= GetColumnOffset(cursor.GetColumnType());
maxcol /= GetChannelWidth();
if(col < xofs)
{
CSize size(-m_szCell.cx, 0);
if(!noMove)
size.cx = (col - xofs) * (int)m_szCell.cx;
OnScrollBy(size, TRUE);
} else if((col > xofs + maxcol) && (maxcol > 0))
{
CSize size(m_szCell.cx, 0);
if(!noMove)
size.cx = (col - maxcol + 1) * (int)m_szCell.cx;
OnScrollBy(size, TRUE);
}
}
UpdateWindow();
return true;
}
bool CViewPattern::SetPlayCursor(PATTERNINDEX pat, ROWINDEX row, uint32 tick)
{
PATTERNINDEX oldPat = m_nPlayPat;
ROWINDEX oldRow = m_nPlayRow;
uint32 oldTick = m_nPlayTick;
m_nPlayPat = pat;
m_nPlayRow = row;
m_nPlayTick = tick;
if(m_nPlayTick != oldTick && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_SMOOTHSCROLL))
InvalidatePattern(true, true);
else if(oldPat == m_nPattern)
InvalidateRow(oldRow);
else if(m_nPlayPat == m_nPattern)
InvalidateRow(m_nPlayRow);
return true;
}
UINT CViewPattern::GetCurrentInstrument() const
{
return static_cast<UINT>(SendCtrlMessage(CTRLMSG_GETCURRENTINSTRUMENT));
}
bool CViewPattern::ShowEditWindow()
{
if(!m_pEditWnd)
{
m_pEditWnd = new CEditCommand(*GetSoundFile());
}
if(m_pEditWnd)
{
m_pEditWnd->ShowEditWindow(m_nPattern, m_Cursor, this);
return true;
}
return false;
}
bool CViewPattern::PrepareUndo(const PatternCursor &beginSel, const PatternCursor &endSel, const char *description)
{
CModDoc *pModDoc = GetDocument();
const CHANNELINDEX chnBegin = beginSel.GetChannel(), chnEnd = endSel.GetChannel();
const ROWINDEX rowBegin = beginSel.GetRow(), rowEnd = endSel.GetRow();
if((chnEnd < chnBegin) || (rowEnd < rowBegin) || pModDoc == nullptr)
return false;
return pModDoc->GetPatternUndo().PrepareUndo(m_nPattern, chnBegin, rowBegin, chnEnd - chnBegin + 1, rowEnd - rowBegin + 1, description);
}
BOOL CViewPattern::PreTranslateMessage(MSG *pMsg)
{
if(pMsg)
{
//We handle keypresses before Windows has a chance to handle them (for alt etc..)
if((pMsg->message == WM_SYSKEYUP) || (pMsg->message == WM_KEYUP) ||
(pMsg->message == WM_SYSKEYDOWN) || (pMsg->message == WM_KEYDOWN))
{
CInputHandler *ih = CMainFrame::GetInputHandler();
//Translate message manually
UINT nChar = static_cast<UINT>(pMsg->wParam);
UINT nRepCnt = LOWORD(pMsg->lParam);
UINT nFlags = HIWORD(pMsg->lParam);
KeyEventType kT = ih->GetKeyEventType(nFlags);
InputTargetContext ctx = (InputTargetContext)(kCtxViewPatterns + 1 + m_Cursor.GetColumnType());
// If editing is disabled, preview notes no matter which column we are in
if(!IsEditingEnabled() && TrackerSettings::Instance().patternNoEditPopup)
ctx = kCtxViewPatternsNote;
if(ih->KeyEvent(ctx, nChar, nRepCnt, nFlags, kT) != kcNull)
{
return true; // Mapped to a command, no need to pass message on.
}
//HACK: fold kCtxViewPatternsFX and kCtxViewPatternsFXparam so that all commands of 1 are active in the other
else
{
if(ctx == kCtxViewPatternsFX)
{
if(ih->KeyEvent(kCtxViewPatternsFXparam, nChar, nRepCnt, nFlags, kT) != kcNull)
return true; // Mapped to a command, no need to pass message on.
} else if(ctx == kCtxViewPatternsFXparam)
{
if(ih->KeyEvent(kCtxViewPatternsFX, nChar, nRepCnt, nFlags, kT) != kcNull)
return true; // Mapped to a command, no need to pass message on.
} else if(ctx == kCtxViewPatternsIns)
{
// Do the same with instrument->note column
if(ih->KeyEvent(kCtxViewPatternsNote, nChar, nRepCnt, nFlags, kT) != kcNull)
return true; // Mapped to a command, no need to pass message on.
}
}
//end HACK.
// Handle Application (menu) key
if(pMsg->message == WM_KEYDOWN && nChar == VK_APPS)
{
OnRButtonDown(0, GetPointFromPosition(m_Cursor));
}
} else if(pMsg->message == WM_MBUTTONDOWN)
{
// Open quick channel properties dialog if we're middle-clicking a channel header.
CPoint point(GET_X_LPARAM(pMsg->lParam), GET_Y_LPARAM(pMsg->lParam));
if(point.y < m_szHeader.cy - m_szPluginHeader.cy)
{
PatternCursor cursor = GetPositionFromPoint(point);
if(cursor.GetChannel() < GetDocument()->GetNumChannels())
{
ClientToScreen(&point);
m_quickChannelProperties.Show(GetDocument(), cursor.GetChannel(), point);
return true;
}
}
}
}
return CModScrollView::PreTranslateMessage(pMsg);
}
////////////////////////////////////////////////////////////////////////
// CViewPattern message handlers
void CViewPattern::OnDestroy()
{
// Fix: save pattern scrollbar position when switching to other tab
// When we switching to other tab the CViewPattern object is deleted
CModDoc *pModDoc = GetDocument();
if(pModDoc)
{
pModDoc->SetOldPatternScrollbarsPos(CSize(m_nXScroll * m_szCell.cx, m_nYScroll * m_szCell.cy));
}
if(m_pEffectVis)
{
m_pEffectVis->DoClose();
m_pEffectVis = nullptr;
}
if(m_pEditWnd)
{
m_pEditWnd->DestroyWindow();
delete m_pEditWnd;
m_pEditWnd = NULL;
}
CModScrollView::OnDestroy();
}
void CViewPattern::OnSetFocus(CWnd *pOldWnd)
{
CScrollView::OnSetFocus(pOldWnd);
m_Status.set(psFocussed);
InvalidateRow();
CModDoc *pModDoc = GetDocument();
if(pModDoc)
{
pModDoc->SetNotifications(Notification::Position | Notification::VUMeters);
pModDoc->SetFollowWnd(m_hWnd);
UpdateIndicator();
}
}
void CViewPattern::OnKillFocus(CWnd *pNewWnd)
{
CScrollView::OnKillFocus(pNewWnd);
m_Status.reset(psKeyboardDragSelect | psCtrlDragSelect | psFocussed);
InvalidateRow();
}
void CViewPattern::OnGrowSelection()
{
CSoundFile *pSndFile = GetSoundFile();
if(pSndFile == nullptr || !pSndFile->Patterns.IsValidPat(m_nPattern))
{
return;
}
BeginWaitCursor();
m_Selection.Sanitize(pSndFile->Patterns[m_nPattern].GetNumRows(), pSndFile->GetNumChannels());
const PatternCursor startSel = m_Selection.GetUpperLeft();
const PatternCursor endSel = m_Selection.GetLowerRight();
PrepareUndo(startSel, PatternCursor(pSndFile->Patterns[m_nPattern].GetNumRows(), endSel), "Grow Selection");
const ROWINDEX finalDest = m_Selection.GetStartRow() + (m_Selection.GetNumRows() - 1) * 2;
for(int row = finalDest; row > (int)startSel.GetRow(); row -= 2)
{
if(ROWINDEX(row) >= pSndFile->Patterns[m_nPattern].GetNumRows())
{
continue;
}
int offset = row - startSel.GetRow();
for(CHANNELINDEX chn = startSel.GetChannel(); chn <= endSel.GetChannel(); chn++)
{
for(int i = PatternCursor::firstColumn; i <= PatternCursor::lastColumn; i++)
{
PatternCursor cell(row, chn, static_cast<PatternCursor::Columns>(i));
if(!m_Selection.ContainsHorizontal(cell))
{
// We might have to skip the first / last few entries.
continue;
}
ModCommand *dest = pSndFile->Patterns[m_nPattern].GetpModCommand(row, chn);
ModCommand *src = pSndFile->Patterns[m_nPattern].GetpModCommand(row - offset / 2, chn);
ModCommand *blank = pSndFile->Patterns[m_nPattern].GetpModCommand(row - 1, chn); // Row "in between"
switch(i)
{
case PatternCursor::noteColumn:
dest->note = src->note;
blank->note = NOTE_NONE;
break;
case PatternCursor::instrColumn:
dest->instr = src->instr;
blank->instr = 0;
break;
case PatternCursor::volumeColumn:
dest->volcmd = src->volcmd;
blank->volcmd = VOLCMD_NONE;
dest->vol = src->vol;
blank->vol = 0;
break;
case PatternCursor::effectColumn:
dest->command = src->command;
blank->command = CMD_NONE;
break;
case PatternCursor::paramColumn:
dest->param = src->param;
blank->param = 0;
break;
}
}
}
}
// Adjust selection
m_Selection = PatternRect(startSel, PatternCursor(std::min(finalDest, static_cast<ROWINDEX>(pSndFile->Patterns[m_nPattern].GetNumRows() - 1)), endSel));
InvalidatePattern();
SetModified();
EndWaitCursor();
SetFocus();
}
void CViewPattern::OnShrinkSelection()
{
CSoundFile *pSndFile = GetSoundFile();
if(pSndFile == nullptr || !pSndFile->Patterns.IsValidPat(m_nPattern))
{
return;
}
BeginWaitCursor();
m_Selection.Sanitize(pSndFile->Patterns[m_nPattern].GetNumRows(), pSndFile->GetNumChannels());
const PatternCursor startSel = m_Selection.GetUpperLeft();
const PatternCursor endSel = m_Selection.GetLowerRight();
PrepareUndo(startSel, endSel, "Shrink Selection");
const ROWINDEX finalDest = m_Selection.GetStartRow() + (m_Selection.GetNumRows() - 1) / 2;
for(ROWINDEX row = startSel.GetRow(); row <= endSel.GetRow(); row++)
{
const ROWINDEX offset = row - startSel.GetRow();
const ROWINDEX srcRow = startSel.GetRow() + (offset * 2);
for(CHANNELINDEX chn = startSel.GetChannel(); chn <= endSel.GetChannel(); chn++)
{
ModCommand *dest = pSndFile->Patterns[m_nPattern].GetpModCommand(row, chn);
ModCommand src;
if(row <= finalDest)
{
// Normal shrink operation
src = *pSndFile->Patterns[m_nPattern].GetpModCommand(srcRow, chn);
// If source command is empty, try next source row (so we don't lose all the stuff that's on odd rows).
if(srcRow < pSndFile->Patterns[m_nPattern].GetNumRows() - 1)
{
const ModCommand &srcNext = *pSndFile->Patterns[m_nPattern].GetpModCommand(srcRow + 1, chn);
if(src.note == NOTE_NONE)
src.note = srcNext.note;
if(src.instr == 0)
src.instr = srcNext.instr;
if(src.volcmd == VOLCMD_NONE)
{
src.volcmd = srcNext.volcmd;
src.vol = srcNext.vol;
}
if(src.command == CMD_NONE)
{
src.command = srcNext.command;
src.param = srcNext.param;
}
}
} else
{
// Clean up rows that are now supposed to be empty.
src = ModCommand::Empty();
}
for(int i = PatternCursor::firstColumn; i <= PatternCursor::lastColumn; i++)
{
PatternCursor cell(row, chn, static_cast<PatternCursor::Columns>(i));
if(!m_Selection.ContainsHorizontal(cell))
{
// We might have to skip the first / last few entries.
continue;
}
switch(i)
{
case PatternCursor::noteColumn:
dest->note = src.note;
break;
case PatternCursor::instrColumn:
dest->instr = src.instr;
break;
case PatternCursor::volumeColumn:
dest->vol = src.vol;
dest->volcmd = src.volcmd;
break;
case PatternCursor::effectColumn:
dest->command = src.command;
break;
case PatternCursor::paramColumn:
dest->param = src.param;
break;
}
}
}
}
// Adjust selection
m_Selection = PatternRect(startSel, PatternCursor(std::min(finalDest, static_cast<ROWINDEX>(pSndFile->Patterns[m_nPattern].GetNumRows() - 1)), endSel));
InvalidatePattern();
SetModified();
EndWaitCursor();
SetFocus();
}
void CViewPattern::OnClearSelectionFromMenu()
{
OnClearSelection();
}
void CViewPattern::OnClearSelection(bool ITStyle, RowMask rm) //Default RowMask: all elements enabled
{
CSoundFile *pSndFile = GetSoundFile();
if(pSndFile == nullptr || !pSndFile->Patterns.IsValidPat(m_nPattern) || !IsEditingEnabled_bmsg())
{
return;
}
BeginWaitCursor();
// If selection ends to a note column, in ITStyle extending it to instrument column since the instrument data is
// removed with note data.
if(ITStyle && m_Selection.GetEndColumn() == PatternCursor::noteColumn)
{
PatternCursor lower(m_Selection.GetLowerRight());
lower.Move(0, 0, 1);
m_Selection = PatternRect(m_Selection.GetUpperLeft(), lower);
}
m_Selection.Sanitize(pSndFile->Patterns[m_nPattern].GetNumRows(), pSndFile->GetNumChannels());
PrepareUndo(m_Selection, "Clear Selection");
ApplyToSelection([&] (ModCommand &m, ROWINDEX row, CHANNELINDEX chn)
{
for(int i = PatternCursor::firstColumn; i <= PatternCursor::lastColumn; i++)
{
PatternCursor cell(row, chn, static_cast<PatternCursor::Columns>(i));
if(!m_Selection.ContainsHorizontal(cell))
{
// We might have to skip the first / last few entries.
continue;
}
switch(i)
{
case PatternCursor::noteColumn: // Clear note
if(rm.note)
{
if(m.IsPcNote())
{ // Clear whole cell if clearing PC note
m.Clear();
} else
{
m.note = NOTE_NONE;
if(ITStyle)
m.instr = 0;
}
}
break;
case PatternCursor::instrColumn: // Clear instrument
if(rm.instrument)
{
m.instr = 0;
}
break;
case PatternCursor::volumeColumn: // Clear volume
if(rm.volume)
{
m.volcmd = VOLCMD_NONE;
m.vol = 0;
}
break;
case PatternCursor::effectColumn: // Clear Command
if(rm.command)
{
m.command = CMD_NONE;
if(m.IsPcNote())
{
m.SetValueEffectCol(0);
}
}
break;
case PatternCursor::paramColumn: // Clear Command Param
if(rm.parameter)
{
m.param = 0;
if(m.IsPcNote())
{
m.SetValueEffectCol(0);
if(cell.CompareColumn(m_Selection.GetUpperLeft()) == 0)
{
// If this is the first selected column, update effect column char as well
PatternCursor upper(m_Selection.GetUpperLeft());
upper.Move(0, 0, -1);
m_Selection = PatternRect(upper, m_Selection.GetLowerRight());
}
}
}
break;
}
}
});
// Expand invalidation to the whole column. Needed for:
// - Last column is the effect character (parameter needs to be invalidated, too
// - PC Notes
// - Default volume display is enabled.
PatternCursor endCursor(m_Selection.GetEndRow(), m_Selection.GetEndChannel() + 1);
InvalidateArea(m_Selection.GetUpperLeft(), endCursor);
SetModified();
EndWaitCursor();
SetFocus();
}
void CViewPattern::OnEditCut()
{
OnEditCopy();
OnClearSelection(false);
}
void CViewPattern::OnEditCopy()
{
CModDoc *pModDoc = GetDocument();
if(pModDoc)
{
CopyPattern(m_nPattern, m_Selection);
SetFocus();
}
}
void CViewPattern::StartRecordGroupDragging(const DragItem source)
{
// Drag-select record channels
const auto *modDoc = GetDocument();
if(modDoc == nullptr)
return;
m_initialDragRecordStatus.resize(modDoc->GetNumChannels());
for(CHANNELINDEX chn = 0; chn < modDoc->GetNumChannels(); chn++)
{
m_initialDragRecordStatus[chn] = modDoc->GetChannelRecordGroup(chn);
}
m_Status.reset(psDragging);
m_nDropItem = m_nDragItem = source;
}
void CViewPattern::OnLButtonDown(UINT nFlags, CPoint point)
{
const auto *modDoc = GetDocument();
if(modDoc == nullptr)
return;
const auto &sndFile = modDoc->GetSoundFile();
SetFocus();
m_nDropItem = m_nDragItem = GetDragItem(point, m_rcDragItem);
m_Status.set(psDragging);
m_bInItemRect = true;
m_Status.reset(psShiftDragging);
PatternCursor pointCursor(GetPositionFromPoint(point));
SetCapture();
if(point.x >= m_szHeader.cx && point.y <= m_szHeader.cy - m_szPluginHeader.cy)
{
// Click on channel header
if(nFlags & MK_CONTROL)
TogglePendingMute(pointCursor.GetChannel());
if(nFlags & MK_SHIFT)
{
// Drag-select record channels
StartRecordGroupDragging(m_nDragItem);
}
} else if(point.x >= m_szHeader.cx && point.y > m_szHeader.cy)
{
// Click on pattern data
if(IsLiveRecord() && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_NOFOLLOWONCLICK))
{
SendCtrlMessage(CTRLMSG_PAT_FOLLOWSONG, 0);
}
if(CMainFrame::GetInputHandler()->SelectionPressed()
&& (m_Status[psShiftSelect]
|| m_Selection.GetUpperLeft() == m_Selection.GetLowerRight()
|| !m_Selection.Contains(pointCursor)))
{
// Shift pressed -> set 2nd selection point
// This behaviour is only used if:
// * Shift-click has previously been used since the shift key has been pressed down (psShiftSelect flag is set),
// * No selection has been made yet, or
// * Shift-clicking outside the current selection.
// This is necessary so that selections can still be moved properly while the shift button is pressed (for copy-move).
DragToSel(pointCursor, true, true);
m_Status.set(psShiftSelect);
} else
{
// Set first selection point
m_StartSel = pointCursor;
if(m_StartSel.GetChannel() < sndFile.GetNumChannels())
{
m_Status.set(psMouseDragSelect);
if(m_Status[psCtrlDragSelect])
{
SetCurSel(m_StartSel);
}
if((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_DRAGNDROPEDIT)
&& ((m_Selection.GetUpperLeft() != m_Selection.GetLowerRight()) || m_Status[psCtrlDragSelect])
&& m_Selection.Contains(m_StartSel))
{
m_Status.set(psDragnDropEdit);
} else if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CENTERROW)
{
SetCurSel(m_StartSel);
} else
{
// Fix: Horizontal scrollbar pos screwed when selecting with mouse
SetCursorPosition(m_StartSel);
}
}
}
} else if(point.x < m_szHeader.cx && point.y > m_szHeader.cy)
{
// Mark row number => mark whole row (start)
InvalidateSelection();
if(pointCursor.GetRow() < sndFile.Patterns[m_nPattern].GetNumRows())
{
m_StartSel.Set(pointCursor.GetRow(), 0);
SetCurSel(m_StartSel, PatternCursor(pointCursor.GetRow(), sndFile.GetNumChannels() - 1, PatternCursor::lastColumn));
m_Status.set(psRowSelection);
}
}
if(m_nDragItem.IsValid())
{
InvalidateRect(&m_rcDragItem, FALSE);
UpdateWindow();
}
}
void CViewPattern::OnLButtonDblClk(UINT uFlags, CPoint point)
{
PatternCursor cursor = GetPositionFromPoint(point);
if(cursor == m_Cursor && point.y >= m_szHeader.cy)
{
// Double-click pattern cell: Select whole column or show cell properties.
if((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_DBLCLICKSELECT))
{
OnSelectCurrentChannel();
m_Status.set(psChannelSelection | psDragging);
return;
} else
{
if(ShowEditWindow())
return;
}
}
OnLButtonDown(uFlags, point);
}
void CViewPattern::OnLButtonUp(UINT nFlags, CPoint point)
{
CModDoc *modDoc = GetDocument();
if(modDoc == nullptr)
return;
const auto dragType = m_nDragItem.Type();
const bool wasDraggingRecordGroup = IsDraggingRecordGroup();
const bool itemSelected = m_bInItemRect || (dragType == DragItem::ChannelHeader);
m_bInItemRect = false;
ResetRecordGroupDragging();
ReleaseCapture();
m_Status.reset(psMouseDragSelect | psRowSelection | psChannelSelection | psDragging);
// Drag & Drop Editing
if(m_Status[psDragnDropEdit])
{
if(m_Status[psDragnDropping])
{
OnDrawDragSel();
m_Status.reset(psDragnDropping);
OnDropSelection();
}
if(GetPositionFromPoint(point) == m_StartSel)
{
SetCursorPosition(m_StartSel);
}
SetCursor(CMainFrame::curArrow);
m_Status.reset(psDragnDropEdit);
}
if(dragType != DragItem::ChannelHeader
&& dragType != DragItem::PatternHeader
&& dragType != DragItem::PluginName)
{
if((m_nMidRow) && (m_Selection.GetUpperLeft() == m_Selection.GetLowerRight()))
{
// Fix: Horizontal scrollbar pos screwed when selecting with mouse
SetCursorPosition(m_Selection.GetUpperLeft());
//UpdateIndicator();
}
}
if(!itemSelected || !m_nDragItem.IsValid())
return;
InvalidateRect(&m_rcDragItem, FALSE);
const CHANNELINDEX sourceChn = static_cast<CHANNELINDEX>(m_nDragItem.Value());
const CHANNELINDEX targetChn = m_nDropItem.IsValid() ? static_cast<CHANNELINDEX>(m_nDropItem.Value()) : CHANNELINDEX_INVALID;
switch(m_nDragItem.Type())
{
case DragItem::ChannelHeader:
if(sourceChn == targetChn && targetChn < modDoc->GetNumChannels())
{
// Just clicked a channel header...
if(nFlags & MK_SHIFT)
{
// Toggle record state
modDoc->ToggleChannelRecordGroup(sourceChn, RecordGroup::Group1);
InvalidateChannelsHeaders(sourceChn);
} else if(CMainFrame::GetInputHandler()->AltPressed())
{
// Solo / Unsolo
OnSoloChannel(sourceChn);
} else if(!(nFlags & MK_CONTROL))
{
// Mute / Unmute
OnMuteChannel(sourceChn);
}
} else if(!wasDraggingRecordGroup && targetChn < modDoc->GetNumChannels() && m_nDropItem.Type() == DragItem::ChannelHeader)
{
// Dragged to other channel header => move or copy channel
InvalidateRect(&m_rcDropItem, FALSE);
const bool duplicate = (nFlags & MK_SHIFT) != 0;
DragChannel(sourceChn, targetChn, 1, duplicate);
}
break;
case DragItem::PatternHeader:
OnPatternProperties();
break;
case DragItem::PluginName:
if(sourceChn < MAX_BASECHANNELS)
TogglePluginEditor(sourceChn);
break;
}
m_nDropItem = {};
}
void CViewPattern::DragChannel(CHANNELINDEX source, CHANNELINDEX target, CHANNELINDEX numChannels, bool duplicate)
{
auto modDoc = GetDocument();
const CHANNELINDEX newChannels = modDoc->GetNumChannels() + (duplicate ? numChannels : 0);
std::vector<CHANNELINDEX> channels(newChannels, 0);
bool modified = duplicate;
for(CHANNELINDEX chn = 0, fromChn = 0; chn < newChannels; chn++)
{
if(chn >= target && chn < target + numChannels)
{
channels[chn] = source + chn - target;
} else
{
if(fromChn == source && !duplicate) // Don't want the source channels twice if we're just moving
{
fromChn += numChannels;
}
channels[chn] = fromChn++;
}
if(channels[chn] != chn)
{
modified = true;
}
}
if(modified && modDoc->ReArrangeChannels(channels) != CHANNELINDEX_INVALID)
{
modDoc->UpdateAllViews(this, GeneralHint().Channels().ModType(), this);
if(duplicate)
{
// Number of channels changed: Update channel headers and other information.
SetCurrentPattern(m_nPattern);
}
if(!duplicate)
{
const auto oldSel = m_Selection;
if(auto chn = m_Cursor.GetChannel(); (chn >= source && chn < source + numChannels))
SetCurrentColumn(target + chn - source, m_Cursor.GetColumnType());
if(oldSel.GetStartChannel() >= source && oldSel.GetEndChannel() < source + numChannels)
{
const auto diff = static_cast<int>(target) - source;
auto upperLeft = oldSel.GetUpperLeft(), lowerRight = oldSel.GetLowerRight();
upperLeft.Move(0, diff, 0);
lowerRight.Move(0, diff, 0);
SetCurSel(upperLeft, lowerRight);
}
}
InvalidatePattern(true, false);
SetModified(false);
}
}
void CViewPattern::ShowPatternProperties(PATTERNINDEX pat)
{
CModDoc *pModDoc = GetDocument();
if(pat == PATTERNINDEX_INVALID)
pat = m_nPattern;
if(pModDoc && pModDoc->GetSoundFile().Patterns.IsValidPat(pat))
{
CPatternPropertiesDlg dlg(*pModDoc, pat, this);
if(dlg.DoModal() == IDOK)
{
UpdateScrollSize();
InvalidatePattern(true, true);
SanitizeCursor();
pModDoc->UpdateAllViews(this, PatternHint(pat).Data(), this);
}
}
}
void CViewPattern::OnRButtonDown(UINT flags, CPoint pt)
{
CModDoc *modDoc = GetDocument();
HMENU hMenu;
// Too far left to get a ctx menu:
if(!modDoc || pt.x < m_szHeader.cx)
{
return;
}
// Handle drag n drop
if(m_Status[psDragnDropEdit])
{
if(m_Status[psDragnDropping])
{
OnDrawDragSel();
m_Status.reset(psDragnDropping);
}
m_Status.reset(psDragnDropEdit | psMouseDragSelect);
if(m_Status[psDragging])
{
m_Status.reset(psDragging);
m_bInItemRect = false;
ReleaseCapture();
}
SetCursor(CMainFrame::curArrow);
return;
}
if((hMenu = ::CreatePopupMenu()) == NULL)
{
return;
}
CSoundFile &sndFile = modDoc->GetSoundFile();
m_MenuCursor = GetPositionFromPoint(pt);
// Right-click outside single-point selection? Reposition cursor to the new location
if(!m_Selection.Contains(m_MenuCursor) && m_Selection.GetUpperLeft() == m_Selection.GetLowerRight())
{
if(pt.y > m_szHeader.cy)
{
//ensure we're not clicking header
// Fix: Horizontal scrollbar pos screwed when selecting with mouse
SetCursorPosition(m_MenuCursor);
}
}
const CHANNELINDEX nChn = m_MenuCursor.GetChannel();
const bool inChannelHeader = (pt.y < m_szHeader.cy);
if((flags & MK_CONTROL) && nChn < sndFile.GetNumChannels() && inChannelHeader)
{
// Ctrl+Right-Click: Open quick channel properties.
ClientToScreen(&pt);
m_quickChannelProperties.Show(GetDocument(), nChn, pt);
} else if((flags & MK_SHIFT) && inChannelHeader)
{
// Drag-select record channels
StartRecordGroupDragging(GetDragItem(pt, m_rcDragItem));
} else if(nChn < sndFile.GetNumChannels() && sndFile.Patterns.IsValidPat(m_nPattern) && !(flags & (MK_CONTROL | MK_SHIFT)))
{
CInputHandler *ih = CMainFrame::GetInputHandler();
//------ Plugin Header Menu --------- :
if(m_Status[psShowPluginNames] &&
inChannelHeader && (pt.y > m_szHeader.cy - m_szPluginHeader.cy))
{
BuildPluginCtxMenu(hMenu, nChn, sndFile);
}
//------ Channel Header Menu ---------- :
else if(inChannelHeader)
{
if(ih->ShiftPressed())
{
//Don't bring up menu if shift is pressed, else we won't get button up msg.
} else
{
if(BuildSoloMuteCtxMenu(hMenu, ih, nChn, sndFile))
AppendMenu(hMenu, MF_SEPARATOR, 0, _T(""));
BuildRecordCtxMenu(hMenu, ih, nChn);
BuildChannelControlCtxMenu(hMenu, ih);
}
}
//------ Standard Menu ---------- :
else if((pt.x >= m_szHeader.cx) && (pt.y >= m_szHeader.cy))
{
// When combining menus, use bitwise ORs to avoid shortcuts
if(BuildSelectionCtxMenu(hMenu, ih))
AppendMenu(hMenu, MF_SEPARATOR, 0, _T(""));
if(BuildEditCtxMenu(hMenu, ih, modDoc))
AppendMenu(hMenu, MF_SEPARATOR, 0, _T(""));
if(BuildInterpolationCtxMenu(hMenu, ih)
| BuildTransposeCtxMenu(hMenu, ih))
AppendMenu(hMenu, MF_SEPARATOR, 0, _T(""));
if(BuildVisFXCtxMenu(hMenu, ih)
| BuildAmplifyCtxMenu(hMenu, ih)
| BuildSetInstCtxMenu(hMenu, ih))
AppendMenu(hMenu, MF_SEPARATOR, 0, _T(""));
if(BuildPCNoteCtxMenu(hMenu, ih))
AppendMenu(hMenu, MF_SEPARATOR, 0, _T(""));
if(BuildGrowShrinkCtxMenu(hMenu, ih))
AppendMenu(hMenu, MF_SEPARATOR, 0, _T(""));
if(BuildMiscCtxMenu(hMenu, ih))
AppendMenu(hMenu, MF_SEPARATOR, 0, _T(""));
if(BuildRowInsDelCtxMenu(hMenu, ih))
AppendMenu(hMenu, MF_SEPARATOR, 0, _T(""));
CString s = _T("&Quantize ");
if(TrackerSettings::Instance().recordQuantizeRows != 0)
{
uint32 rows = TrackerSettings::Instance().recordQuantizeRows.Get();
s += MPT_CFORMAT("(Currently: {} Row{})")(rows, CString(rows == 1 ? _T("") : _T("s")));
} else
{
s += _T("Settings...");
}
AppendMenu(hMenu, MF_STRING | (TrackerSettings::Instance().recordQuantizeRows != 0 ? MF_CHECKED : 0), ID_SETQUANTIZE, ih->GetKeyTextFromCommand(kcQuantizeSettings, s));
}
ClientToScreen(&pt);
::TrackPopupMenu(hMenu, TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, 0, m_hWnd, NULL);
} else if(nChn >= sndFile.GetNumChannels() && sndFile.GetNumChannels() < sndFile.GetModSpecifications().channelsMax && !(flags & (MK_CONTROL | MK_SHIFT)))
{
// Click outside of pattern: Offer easy way to add more channels
m_MenuCursor.Set(0, sndFile.GetNumChannels() - 1);
AppendMenu(hMenu, MF_STRING, ID_PATTERN_ADDCHANNEL_AFTER, _T("&Add Channel"));
ClientToScreen(&pt);
::TrackPopupMenu(hMenu, TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, 0, m_hWnd, NULL);
}
::DestroyMenu(hMenu);
}
void CViewPattern::OnRButtonUp(UINT nFlags, CPoint point)
{
CModDoc *pModDoc = GetDocument();
if(!pModDoc)
return;
ResetRecordGroupDragging();
const CHANNELINDEX sourceChn = static_cast<CHANNELINDEX>(m_nDragItem.Value());
const CHANNELINDEX targetChn = m_nDropItem.IsValid() ? static_cast<CHANNELINDEX>(m_nDropItem.Value()) : CHANNELINDEX_INVALID;
switch(m_nDragItem.Type())
{
case DragItem::ChannelHeader:
if(nFlags & MK_SHIFT)
{
if(sourceChn < MAX_BASECHANNELS && sourceChn == targetChn)
{
pModDoc->ToggleChannelRecordGroup(sourceChn, RecordGroup::Group2);
InvalidateChannelsHeaders(sourceChn);
}
}
break;
}
CModScrollView::OnRButtonUp(nFlags, point);
}
BOOL CViewPattern::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
{
if(nFlags & MK_CONTROL)
{
// Ctrl + mouse wheel: Increment / decrement values
DataEntry(zDelta > 0, (nFlags & MK_SHIFT) == MK_SHIFT);
return TRUE;
}
if(IsLiveRecord() && !m_Status[psDragActive])
{
// During live playback with "follow song" enabled, the mouse wheel can be used to jump forwards and backwards.
CursorJump(-mpt::signum(zDelta), false);
return TRUE;
}
return CModScrollView::OnMouseWheel(nFlags, zDelta, pt);
}
void CViewPattern::OnXButtonUp(UINT nFlags, UINT nButton, CPoint point)
{
if(nButton == XBUTTON1)
OnPrevOrder();
else if(nButton == XBUTTON2)
OnNextOrder();
CModScrollView::OnXButtonUp(nFlags, nButton, point);
}
void CViewPattern::OnMouseMove(UINT nFlags, CPoint point)
{
CModScrollView::OnMouseMove(nFlags, point);
const bool isDraggingRecordGroup = IsDraggingRecordGroup();
if(!m_Status[psDragging] && !isDraggingRecordGroup)
return;
// Drag&Drop actions
if(m_nDragItem.IsValid())
{
const CRect oldDropRect = m_rcDropItem;
const auto oldDropItem = m_nDropItem;
if(isDraggingRecordGroup)
{
// When drag-selecting record channels, ignore y position
point.y = m_rcDragItem.top;
}
m_Status.set(psShiftDragging, (nFlags & MK_SHIFT) != 0);
m_nDropItem = GetDragItem(point, m_rcDropItem);
const bool b = (m_nDropItem == m_nDragItem);
const bool dragChannel = m_nDragItem.Type() == DragItem::ChannelHeader;
if(b != m_bInItemRect || (m_nDropItem != oldDropItem && dragChannel))
{
m_bInItemRect = b;
InvalidateRect(&m_rcDragItem, FALSE);
// Drag-select record channels
if(isDraggingRecordGroup && m_nDropItem.Type() == DragItem::ChannelHeader)
{
auto modDoc = GetDocument();
auto startChn = static_cast<CHANNELINDEX>(m_nDragItem.Value());
auto endChn = static_cast<CHANNELINDEX>(m_nDropItem.Value());
RecordGroup setRecord = RecordGroup::NoGroup;
if(m_initialDragRecordStatus[startChn] != RecordGroup::Group1 && (nFlags & MK_LBUTTON))
setRecord = RecordGroup::Group1;
else if (m_initialDragRecordStatus[startChn] != RecordGroup::Group2 && (nFlags & MK_RBUTTON))
setRecord = RecordGroup::Group2;
if(startChn > endChn)
std::swap(startChn, endChn);
CHANNELINDEX numChannels = std::min(modDoc->GetNumChannels(), static_cast<CHANNELINDEX>(m_initialDragRecordStatus.size()));
for(CHANNELINDEX chn = 0; chn < numChannels; chn++)
{
auto oldState = modDoc->GetChannelRecordGroup(chn);
if(chn >= startChn && chn <= endChn)
GetDocument()->SetChannelRecordGroup(chn, setRecord);
else
GetDocument()->SetChannelRecordGroup(chn, m_initialDragRecordStatus[chn]);
if(oldState != modDoc->GetChannelRecordGroup(chn))
InvalidateChannelsHeaders(chn);
}
} else
{
// Dragging around channel headers? Update move indicator...
if(m_nDropItem.Type() == DragItem::ChannelHeader)
InvalidateRect(&m_rcDropItem, FALSE);
if(oldDropItem.Type() == DragItem::ChannelHeader)
InvalidateRect(&oldDropRect, FALSE);
}
UpdateWindow();
}
}
if(m_Status[psChannelSelection])
{
// Double-clicked a pattern cell to select whole channel.
// Continue dragging to select more channels.
const CSoundFile *pSndFile = GetSoundFile();
if(pSndFile->Patterns.IsValidPat(m_nPattern))
{
const ROWINDEX lastRow = pSndFile->Patterns[m_nPattern].GetNumRows() - 1;
CHANNELINDEX startChannel = m_Cursor.GetChannel();
CHANNELINDEX endChannel = GetPositionFromPoint(point).GetChannel();
m_StartSel = PatternCursor(0, startChannel, (startChannel <= endChannel ? PatternCursor::firstColumn : PatternCursor::lastColumn));
PatternCursor endSel = PatternCursor(lastRow, endChannel, (startChannel <= endChannel ? PatternCursor::lastColumn : PatternCursor::firstColumn));
DragToSel(endSel, true, false, false);
}
} else if(m_Status[psRowSelection] && point.y > m_szHeader.cy)
{
// Mark row number => mark whole row (continue)
InvalidateSelection();
PatternCursor cursor(GetPositionFromPoint(point));
cursor.SetColumn(GetDocument()->GetNumChannels() - 1, PatternCursor::lastColumn);
DragToSel(cursor, false, true, false);
} else if(m_Status[psMouseDragSelect])
{
PatternCursor cursor(GetPositionFromPoint(point));
const CSoundFile *pSndFile = GetSoundFile();
if(pSndFile != nullptr && m_nPattern < pSndFile->Patterns.Size())
{
ROWINDEX row = cursor.GetRow();
LimitMax(row, pSndFile->Patterns[m_nPattern].GetNumRows() - 1);
cursor.SetRow(row);
}
// Drag & Drop editing
if(m_Status[psDragnDropEdit])
{
const bool moved = m_DragPos.GetChannel() != cursor.GetChannel() || m_DragPos.GetRow() != cursor.GetRow();
if(!m_Status[psDragnDropping])
{
SetCursor(CMainFrame::curDragging);
}
if(!m_Status[psDragnDropping] || moved)
{
if(m_Status[psDragnDropping])
OnDrawDragSel();
m_Status.reset(psDragnDropping);
DragToSel(cursor, true, true, true);
m_DragPos = cursor;
m_Status.set(psDragnDropping);
OnDrawDragSel();
}
} else if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CENTERROW)
{
// Default: selection
DragToSel(cursor, true, true);
} else
{
// Fix: Horizontal scrollbar pos screwed when selecting with mouse
SetCursorPosition(cursor);
}
}
}
void CViewPattern::OnEditSelectAll()
{
const CSoundFile *pSndFile = GetSoundFile();
if(pSndFile != nullptr && pSndFile->Patterns.IsValidPat(m_nPattern))
{
SetCurSel(PatternCursor(0), PatternCursor(pSndFile->Patterns[m_nPattern].GetNumRows() - 1, pSndFile->GetNumChannels() - 1, PatternCursor::lastColumn));
}
}
void CViewPattern::OnEditSelectChannel()
{
const CSoundFile *pSndFile = GetSoundFile();
if(pSndFile != nullptr && pSndFile->Patterns.IsValidPat(m_nPattern))
{
SetCurSel(PatternCursor(0, m_MenuCursor.GetChannel()), PatternCursor(pSndFile->Patterns[m_nPattern].GetNumRows() - 1, m_MenuCursor.GetChannel(), PatternCursor::lastColumn));
}
}
void CViewPattern::OnSelectCurrentChannel()
{
const CSoundFile *pSndFile = GetSoundFile();
if(pSndFile != nullptr && pSndFile->Patterns.IsValidPat(m_nPattern))
{
PatternCursor beginSel(0, GetCurrentChannel());
PatternCursor endSel(pSndFile->Patterns[m_nPattern].GetNumRows() - 1, GetCurrentChannel(), PatternCursor::lastColumn);
// If column is already selected, select the current pattern
if((beginSel == m_Selection.GetUpperLeft()) && (endSel == m_Selection.GetLowerRight()))
{
beginSel.Set(0, 0);
endSel.Set(pSndFile->Patterns[m_nPattern].GetNumRows() - 1, pSndFile->GetNumChannels() - 1, PatternCursor::lastColumn);
}
SetCurSel(beginSel, endSel);
}
}
void CViewPattern::OnSelectCurrentColumn()
{
const CSoundFile *pSndFile = GetSoundFile();
if(pSndFile != nullptr && pSndFile->Patterns.IsValidPat(m_nPattern))
{
SetCurSel(PatternCursor(0, m_Cursor), PatternCursor(pSndFile->Patterns[m_nPattern].GetNumRows() - 1, m_Cursor));
}
}
void CViewPattern::OnChannelReset()
{
ResetChannel(m_MenuCursor.GetChannel());
}
// Reset all channel variables
void CViewPattern::ResetChannel(CHANNELINDEX chn)
{
CModDoc *pModDoc = GetDocument();
if(pModDoc == nullptr)
return;
CSoundFile &sndFile = pModDoc->GetSoundFile();
CriticalSection cs;
if(!pModDoc->IsChannelMuted(chn))
{
// Cut playing notes
sndFile.ChnSettings[chn].dwFlags.set(CHN_MUTE);
pModDoc->UpdateChannelMuteStatus(chn);
sndFile.ChnSettings[chn].dwFlags.reset(CHN_MUTE);
}
sndFile.m_PlayState.Chn[chn].Reset(ModChannel::resetTotal, sndFile, chn, CSoundFile::GetChannelMuteFlag());
}
void CViewPattern::OnMuteFromClick()
{
OnMuteChannel(m_MenuCursor.GetChannel());
}
void CViewPattern::OnMuteChannel(CHANNELINDEX chn)
{
CModDoc *pModDoc = GetDocument();
if(pModDoc)
{
pModDoc->SoloChannel(chn, false);
pModDoc->MuteChannel(chn, !pModDoc->IsChannelMuted(chn));
//If we just unmuted a channel, make sure none are still considered "solo".
if(!pModDoc->IsChannelMuted(chn))
{
for(CHANNELINDEX i = 0; i < pModDoc->GetNumChannels(); i++)
{
pModDoc->SoloChannel(i, false);
}
}
InvalidateChannelsHeaders();
pModDoc->UpdateAllViews(this, GeneralHint(chn).Channels());
}
}
void CViewPattern::OnSoloFromClick()
{
OnSoloChannel(m_MenuCursor.GetChannel());
}
// When trying to solo a channel that is already the only unmuted channel,
// this will result in unmuting all channels, in order to satisfy user habits.
// In all other cases, soloing a channel unsoloes all and mutes all except this channel
void CViewPattern::OnSoloChannel(CHANNELINDEX chn)
{
CModDoc *pModDoc = GetDocument();
if(pModDoc == nullptr)
return;
if(chn >= pModDoc->GetNumChannels())
{
return;
}
if(pModDoc->IsChannelSolo(chn))
{
bool nChnIsOnlyUnMutedChan = true;
for(CHANNELINDEX i = 0; i < pModDoc->GetNumChannels(); i++) //check status of all other chans
{
if(i != chn && !pModDoc->IsChannelMuted(i))
{
nChnIsOnlyUnMutedChan = false; //found a channel that isn't muted!
break;
}
}
if(nChnIsOnlyUnMutedChan) // this is the only playable channel and it is already soloed -> Unmute all
{
OnUnmuteAll();
return;
}
}
for(CHANNELINDEX i = 0; i < pModDoc->GetNumChannels(); i++)
{
pModDoc->MuteChannel(i, !(i == chn)); //mute all chans except nChn, unmute nChn
pModDoc->SoloChannel(i, (i == chn)); //unsolo all chans except nChn, solo nChn
}
InvalidateChannelsHeaders();
pModDoc->UpdateAllViews(this, GeneralHint(chn).Channels());
}
void CViewPattern::OnRecordSelect()
{
CModDoc *pModDoc = GetDocument();
if(pModDoc)
{
CHANNELINDEX chn = m_MenuCursor.GetChannel();
if(chn < pModDoc->GetNumChannels())
{
pModDoc->ToggleChannelRecordGroup(chn, RecordGroup::Group1);
InvalidateChannelsHeaders(chn);
}
}
}
void CViewPattern::OnSplitRecordSelect()
{
CModDoc *pModDoc = GetDocument();
if(pModDoc)
{
CHANNELINDEX chn = m_MenuCursor.GetChannel();
if(chn < pModDoc->GetNumChannels())
{
pModDoc->ToggleChannelRecordGroup(chn, RecordGroup::Group2);
InvalidateChannelsHeaders(chn);
}
}
}
void CViewPattern::OnUnmuteAll()
{
CModDoc *pModDoc = GetDocument();
if(pModDoc)
{
const CHANNELINDEX numChannels = pModDoc->GetNumChannels();
for(CHANNELINDEX chn = 0; chn < numChannels; chn++)
{
pModDoc->MuteChannel(chn, false);
pModDoc->SoloChannel(chn, false);
}
InvalidateChannelsHeaders();
}
}
bool CViewPattern::InsertOrDeleteRows(CHANNELINDEX firstChn, CHANNELINDEX lastChn, bool globalEdit, bool deleteRows)
{
CModDoc &modDoc = *GetDocument();
CSoundFile &sndFile = *GetSoundFile();
if(!sndFile.Patterns.IsValidPat(m_nPattern) || !IsEditingEnabled_bmsg())
return false;
LimitMax(lastChn, CHANNELINDEX(sndFile.GetNumChannels() - 1));
if(firstChn > lastChn)
return false;
const auto selection = (firstChn != lastChn || m_Selection.GetNumRows() > 1) ? PatternRect{{m_Selection.GetStartRow(), firstChn, PatternCursor::firstColumn}, {m_Selection.GetEndRow(), lastChn, PatternCursor::lastColumn}} : m_Selection;
const ROWINDEX numRows = selection.GetNumRows();
const char *undoDescription = "";
if(deleteRows)
undoDescription = numRows != 1 ? "Delete Rows" : "Delete Row";
else
undoDescription = numRows != 1 ? "Insert Rows" : "Insert Row";
const ROWINDEX startRow = selection.GetStartRow();
const CHANNELINDEX numChannels = lastChn - firstChn + 1;
std::vector<PATTERNINDEX> patterns;
if(globalEdit)
{
auto &order = Order();
const auto start = order.begin() + GetCurrentOrder();
const auto end = std::find(start, order.end(), order.GetInvalidPatIndex());
// As this is a global operation, ensure that all modified patterns are unique
bool orderListChanged = false;
const ORDERINDEX ordEnd = GetCurrentOrder() + static_cast<ORDERINDEX>(std::distance(start, end));
for(ORDERINDEX ord = GetCurrentOrder(); ord < ordEnd; ord++)
{
const auto pat = order[ord];
if(pat != order.EnsureUnique(ord))
orderListChanged = true;
}
if(orderListChanged)
modDoc.UpdateAllViews(this, SequenceHint().Data(), nullptr);
patterns.assign(start, end);
} else
{
patterns = {m_nPattern};
}
// Backup source data and create undo points
std::vector<ModCommand> patternData;
if(!deleteRows)
patternData.insert(patternData.begin(), numRows * numChannels, ModCommand{});
bool first = true;
for(auto pat : patterns)
{
if(!sndFile.Patterns.IsValidPat(pat))
continue;
const auto &pattern = sndFile.Patterns[pat];
const ROWINDEX firstRow = first ? startRow : 0;
for(ROWINDEX row = firstRow; row < pattern.GetNumRows(); row++)
{
const auto *m = pattern.GetpModCommand(row, firstChn);
patternData.insert(patternData.end(), m, m + numChannels);
}
modDoc.GetPatternUndo().PrepareUndo(pat, firstChn, firstRow, numChannels, pattern.GetNumRows(), undoDescription, !first);
first = false;
}
if(deleteRows)
patternData.insert(patternData.end(), numRows * numChannels, ModCommand{});
// Now do the actual shifting
auto src = patternData.cbegin();
if(deleteRows)
src += numRows * numChannels;
PATTERNINDEX firstNewPattern = m_nPattern;
first = true;
for(auto pat : patterns)
{
if(!sndFile.Patterns.IsValidPat(pat))
continue;
auto &pattern = sndFile.Patterns[pat];
for(ROWINDEX row = first ? startRow : 0; row < pattern.GetNumRows(); row++, src += numChannels)
{
ModCommand *dest = pattern.GetpModCommand(row, firstChn);
std::copy(src, src + numChannels, dest);
}
if(first)
firstNewPattern = pat;
first = false;
modDoc.UpdateAllViews(this, PatternHint(pat).Data(), this);
}
SetModified();
SetCurrentPattern(firstNewPattern);
InvalidatePattern();
SetCursorPosition(selection.GetUpperLeft());
SetCurSel(selection);
return true;
}
void CViewPattern::DeleteRows(CHANNELINDEX firstChn, CHANNELINDEX lastChn, bool globalEdit)
{
InsertOrDeleteRows(firstChn, lastChn, globalEdit, true);
}
void CViewPattern::OnDeleteRow()
{
DeleteRows(m_Selection.GetStartChannel(), m_Selection.GetEndChannel());
}
void CViewPattern::OnDeleteWholeRow()
{
DeleteRows(0, GetSoundFile()->GetNumChannels() - 1);
}
void CViewPattern::OnDeleteRowGlobal()
{
DeleteRows(m_Selection.GetStartChannel(), m_Selection.GetEndChannel(), true);
}
void CViewPattern::OnDeleteWholeRowGlobal()
{
DeleteRows(0, GetSoundFile()->GetNumChannels() - 1, true);
}
void CViewPattern::InsertRows(CHANNELINDEX firstChn, CHANNELINDEX lastChn, bool globalEdit)
{
InsertOrDeleteRows(firstChn, lastChn, globalEdit, false);
}
void CViewPattern::OnInsertRow()
{
InsertRows(m_Selection.GetStartChannel(), m_Selection.GetEndChannel());
}
void CViewPattern::OnInsertWholeRow()
{
InsertRows(0, GetSoundFile()->GetNumChannels() - 1);
}
void CViewPattern::OnInsertRowGlobal()
{
InsertRows(m_Selection.GetStartChannel(), m_Selection.GetEndChannel(), true);
}
void CViewPattern::OnInsertWholeRowGlobal()
{
InsertRows(0, GetSoundFile()->GetNumChannels() - 1, true);
}
void CViewPattern::OnSplitPattern()
{
COrderList &orderList = static_cast<CCtrlPatterns *>(GetControlDlg())->GetOrderList();
CSoundFile &sndFile = *GetSoundFile();
const auto &specs = sndFile.GetModSpecifications();
const PATTERNINDEX sourcePat = m_nPattern;
const ROWINDEX splitRow = m_MenuCursor.GetRow();
if(splitRow < 1 || !sndFile.Patterns.IsValidPat(sourcePat) || !sndFile.Patterns[sourcePat].IsValidRow(splitRow))
{
MessageBeep(MB_ICONWARNING);
return;
}
// Create a new pattern (ignore if it's too big for this format - if it is, then the source pattern already was too big, too)
CriticalSection cs;
const ROWINDEX numSplitRows = sndFile.Patterns[sourcePat].GetNumRows() - splitRow;
const PATTERNINDEX newPat = sndFile.Patterns.InsertAny(std::max(specs.patternRowsMin, numSplitRows), false);
if(newPat == PATTERNINDEX_INVALID)
{
cs.Leave();
Reporting::Error(MPT_AFORMAT("Pattern limit of the {} format ({} patterns) has been reached.")(mpt::ToUpperCaseAscii(specs.fileExtension), specs.patternsMax), "Split Pattern");
return;
}
auto &sourcePattern = sndFile.Patterns[sourcePat];
auto &newPattern = sndFile.Patterns[newPat];
auto &undo = GetDocument()->GetPatternUndo();
undo.PrepareUndo(sourcePat, 0, splitRow, sourcePattern.GetNumChannels(), numSplitRows, "Split Pattern");
undo.PrepareUndo(newPat, 0, 0, newPattern.GetNumChannels(), newPattern.GetNumRows(), "Split Pattern", true);
auto copyStart = sourcePattern.begin() + sourcePattern.GetNumChannels() * splitRow;
std::copy(copyStart, sourcePattern.end(), newPattern.begin());
// Reduce the row number or insert pattern breaks, if the patterns are too small for the format
sourcePattern.Resize(std::max(specs.patternRowsMin, splitRow));
if(splitRow != sourcePattern.GetNumRows())
{
std::fill(copyStart, sourcePattern.end(), ModCommand::Empty());
sourcePattern.WriteEffect(EffectWriter(CMD_PATTERNBREAK, 0).Row(splitRow - 1).RetryNextRow());
}
if(numSplitRows != newPattern.GetNumRows())
{
newPattern.WriteEffect(EffectWriter(CMD_PATTERNBREAK, 0).Row(numSplitRows - 1).RetryNextRow());
}
// Update every occurrence of the split pattern in all order lists
auto editOrd = GetCurrentOrder();
for(SEQUENCEINDEX seq = 0; seq < sndFile.Order.GetNumSequences(); seq++)
{
const bool isCurrentSeq = (seq == sndFile.Order.GetCurrentSequenceIndex());
bool editedSeq = false;
auto &order = sndFile.Order(seq);
for(ORDERINDEX i = 0; i < order.GetLength(); i++)
{
if(order[i] == sourcePat)
{
if(!order.insert(i + 1, 1, newPat))
continue;
editedSeq = true;
if(isCurrentSeq)
orderList.InsertUpdatePlaystate(i, i + 1);
i++;
// Slide the current selection accordingly so it doesn't end up in the wrong id
if(i < editOrd && isCurrentSeq)
editOrd++;
}
}
if(editedSeq)
GetDocument()->UpdateAllViews(nullptr, SequenceHint(seq).Data(), this);
}
orderList.SetSelection(editOrd + 1);
SetCurrentRow(0);
SetModified(true);
GetDocument()->UpdateAllViews(nullptr, PatternHint(newPat).Names().Data(), this);
}
void CViewPattern::OnEditGoto()
{
CModDoc *pModDoc = GetDocument();
if(!pModDoc)
return;
ORDERINDEX curOrder = GetCurrentOrder();
CHANNELINDEX curChannel = GetCurrentChannel() + 1;
CPatternGotoDialog dlg(this, GetCurrentRow(), curChannel, m_nPattern, curOrder, pModDoc->GetSoundFile());
if(dlg.DoModal() == IDOK)
{
if(dlg.m_nPattern != m_nPattern)
SetCurrentPattern(dlg.m_nPattern);
if(dlg.m_nOrder != curOrder)
SetCurrentOrder(dlg.m_nOrder);
if(dlg.m_nChannel != curChannel)
SetCurrentColumn(dlg.m_nChannel - 1);
if(dlg.m_nRow != GetCurrentRow())
SetCurrentRow(dlg.m_nRow);
CriticalSection cs;
pModDoc->SetElapsedTime(dlg.m_nOrder, dlg.m_nRow, false);
}
return;
}
void CViewPattern::OnPatternStep()
{
PatternStep();
}
void CViewPattern::PatternStep(ROWINDEX row)
{
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
CModDoc *pModDoc = GetDocument();
if(pMainFrm != nullptr && pModDoc != nullptr)
{
CSoundFile &sndFile = pModDoc->GetSoundFile();
if(!sndFile.Patterns.IsValidPat(m_nPattern))
return;
CriticalSection cs;
// In case we were previously in smooth scrolling mode during live playback, the pattern might be misaligned.
if(GetSmoothScrollOffset() != 0)
InvalidatePattern(true, true);
// Cut instruments/samples in virtual channels
for(CHANNELINDEX i = sndFile.GetNumChannels(); i < MAX_CHANNELS; i++)
{
sndFile.m_PlayState.Chn[i].dwFlags.set(CHN_NOTEFADE | CHN_KEYOFF);
}
sndFile.LoopPattern(m_nPattern);
sndFile.m_PlayState.m_nNextRow = row == ROWINDEX_INVALID ? GetCurrentRow() : row;
sndFile.m_SongFlags.reset(SONG_PAUSED);
sndFile.m_SongFlags.set(SONG_STEP);
SetPlayCursor(m_nPattern, sndFile.m_PlayState.m_nNextRow, 0);
cs.Leave();
if(pMainFrm->GetModPlaying() != pModDoc)
{
pModDoc->SetFollowWnd(m_hWnd);
pMainFrm->PlayMod(pModDoc);
}
pModDoc->SetNotifications(Notification::Position | Notification::VUMeters);
if(row == ROWINDEX_INVALID)
{
SetCurrentRow(GetCurrentRow() + 1,
(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL) || // Wrap around to next pattern if continous scroll is enabled...
(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_WRAP)); // ...or otherwise if cursor wrap is enabled.
}
SetFocus();
}
}
// Copy cursor to internal clipboard
void CViewPattern::OnCursorCopy()
{
const CSoundFile *pSndFile = GetSoundFile();
if(pSndFile == nullptr || !pSndFile->Patterns.IsValidPat(m_nPattern))
{
return;
}
const ModCommand &m = GetCursorCommand();
switch(m_Cursor.GetColumnType())
{
case PatternCursor::noteColumn:
case PatternCursor::instrColumn:
m_cmdOld.note = m.note;
m_cmdOld.instr = m.instr;
SendCtrlMessage(CTRLMSG_SETCURRENTINSTRUMENT, m_cmdOld.instr);
break;
case PatternCursor::volumeColumn:
m_cmdOld.volcmd = m.volcmd;
m_cmdOld.vol = m.vol;
break;
case PatternCursor::effectColumn:
case PatternCursor::paramColumn:
m_cmdOld.command = m.command;
m_cmdOld.param = m.param;
break;
}
}
// Paste cursor from internal clipboard
void CViewPattern::OnCursorPaste()
{
if(!IsEditingEnabled_bmsg())
{
return;
}
PrepareUndo(m_Cursor, m_Cursor, "Cursor Paste");
PatternCursor::Columns column = m_Cursor.GetColumnType();
ModCommand &m = GetCursorCommand();
switch(column)
{
case PatternCursor::noteColumn:
m.note = m_cmdOld.note;
[[fallthrough]];
case PatternCursor::instrColumn:
m.instr = m_cmdOld.instr;
break;
case PatternCursor::volumeColumn:
m.vol = m_cmdOld.vol;
m.volcmd = m_cmdOld.volcmd;
break;
case PatternCursor::effectColumn:
case PatternCursor::paramColumn:
m.command = m_cmdOld.command;
m.param = m_cmdOld.param;
break;
}
SetModified(false);
// Preview Row
if((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_PLAYEDITROW) && !IsLiveRecord())
{
PatternStep(GetCurrentRow());
}
if(GetSoundFile()->IsPaused() || !m_Status[psFollowSong] || (CMainFrame::GetMainFrame() && CMainFrame::GetMainFrame()->GetFollowSong(GetDocument()) != m_hWnd))
{
InvalidateCell(m_Cursor);
SetCurrentRow(GetCurrentRow() + m_nSpacing);
SetSelToCursor();
}
}
void CViewPattern::OnVisualizeEffect()
{
CModDoc *pModDoc = GetDocument();
if(pModDoc != nullptr && pModDoc->GetSoundFile().Patterns.IsValidPat(m_nPattern))
{
const ROWINDEX row0 = m_Selection.GetStartRow(), row1 = m_Selection.GetEndRow();
const CHANNELINDEX nchn = m_Selection.GetStartChannel();
if(m_pEffectVis)
{
// Window already there, update data
m_pEffectVis->UpdateSelection(row0, row1, nchn, m_nPattern);
} else
{
// Open window & send data
CriticalSection cs;
try
{
m_pEffectVis = std::make_unique<CEffectVis>(this, row0, row1, nchn, *pModDoc, m_nPattern);
m_pEffectVis->OpenEditor(CMainFrame::GetMainFrame());
// HACK: to get status window set up; must create clear destinction between
// construction, 1st draw code and all draw code.
m_pEffectVis->OnSize(0, 0, 0);
} catch(mpt::out_of_memory e)
{
mpt::delete_out_of_memory(e);
}
}
}
}
// Helper function for sweeping the pattern up and down to find suitable start and end points for interpolation.
// startCond must return true for the start row, endCond must return true for the end row.
PatternRect CViewPattern::SweepPattern(bool(*startCond)(const ModCommand &), bool(*endCond)(const ModCommand &, const ModCommand &)) const
{
const auto &pattern = GetSoundFile()->Patterns[m_nPattern];
const ROWINDEX numRows = pattern.GetNumRows();
const ROWINDEX cursorRow = m_Selection.GetStartRow();
if(cursorRow >= numRows)
return {};
const ModCommand *start = pattern.GetpModCommand(cursorRow, m_Selection.GetStartChannel()), *end = start;
// Sweep up
ROWINDEX startRow = ROWINDEX_INVALID;
for(ROWINDEX row = 0; row <= cursorRow; row++, start -= pattern.GetNumChannels())
{
if(startCond(*start))
{
startRow = cursorRow - row;
break;
}
}
if(startRow == ROWINDEX_INVALID)
return {};
// Sweep down
ROWINDEX endRow = ROWINDEX_INVALID;
for(ROWINDEX row = cursorRow; row < numRows; row++, end += pattern.GetNumChannels())
{
if(endCond(*start, *end))
{
endRow = row;
break;
}
}
if(endRow == ROWINDEX_INVALID)
return {};
return {PatternCursor(startRow, m_Selection.GetUpperLeft()), PatternCursor(endRow, m_Selection.GetUpperLeft())};
}
void CViewPattern::Interpolate(PatternCursor::Columns type)
{
CSoundFile *sndFile = GetSoundFile();
if(sndFile == nullptr || !sndFile->Patterns.IsValidPat(m_nPattern) || !IsEditingEnabled())
return;
bool changed = false;
std::vector<CHANNELINDEX> validChans;
if(type == PatternCursor::effectColumn || type == PatternCursor::paramColumn)
{
std::vector<CHANNELINDEX> effectChans;
std::vector<CHANNELINDEX> paramChans;
ListChansWhereColSelected(PatternCursor::effectColumn, effectChans);
ListChansWhereColSelected(PatternCursor::paramColumn, paramChans);
validChans.resize(effectChans.size() + paramChans.size());
validChans.resize(std::set_union(effectChans.begin(), effectChans.end(), paramChans.begin(), paramChans.end(), validChans.begin()) - validChans.begin());
} else
{
ListChansWhereColSelected(type, validChans);
}
if(m_Selection.GetUpperLeft() == m_Selection.GetLowerRight() && !validChans.empty())
{
// No selection has been made: Interpolate between closest non-zero values in this column.
PatternRect sweepSelection;
switch(type)
{
case PatternCursor::noteColumn:
// Allow note-to-note interpolation only.
sweepSelection = SweepPattern(
[](const ModCommand &start) { return start.note != NOTE_NONE; },
[](const ModCommand &start, const ModCommand &end) { return start.IsNote() && end.IsNote(); });
break;
case PatternCursor::instrColumn:
// Allow interpolation between same instrument, as long as it's not a PC note.
sweepSelection = SweepPattern(
[](const ModCommand &start) { return start.instr != 0 && !start.IsPcNote(); },
[](const ModCommand &start, const ModCommand &end) { return end.instr == start.instr; });
break;
case PatternCursor::volumeColumn:
// Allow interpolation between same volume effect, as long as it's not a PC note.
sweepSelection = SweepPattern(
[](const ModCommand &start) { return start.volcmd != VOLCMD_NONE && !start.IsPcNote(); },
[](const ModCommand &start, const ModCommand &end) { return end.volcmd == start.volcmd && !end.IsPcNote(); });
break;
case PatternCursor::effectColumn:
case PatternCursor::paramColumn:
// Allow interpolation between same effect, or anything if it's a PC note.
sweepSelection = SweepPattern(
[](const ModCommand &start) { return start.command != CMD_NONE || start.IsPcNote(); },
[](const ModCommand &start, const ModCommand &end) { return (end.command == start.command || start.IsPcNote()) && (!start.IsPcNote() || end.IsPcNote()); });
break;
}
if(sweepSelection.GetNumRows() > 1)
{
// Found usable end and start commands: Extend selection.
SetCurSel(sweepSelection);
}
}
const ROWINDEX row0 = m_Selection.GetStartRow(), row1 = m_Selection.GetEndRow();
//for all channels where type is selected
for(auto nchn : validChans)
{
if(!IsInterpolationPossible(row0, row1, nchn, type))
continue; //skip chans where interpolation isn't possible
if(!changed) //ensure we save undo buffer only before any channels are interpolated
{
const char *description = "";
switch(type)
{
case PatternCursor::noteColumn:
description = "Interpolate Note Column";
break;
case PatternCursor::instrColumn:
description = "Interpolate Instrument Column";
break;
case PatternCursor::volumeColumn:
description = "Interpolate Volume Column";
break;
case PatternCursor::effectColumn:
case PatternCursor::paramColumn:
description = "Interpolate Effect Column";
break;
}
PrepareUndo(m_Selection, description);
}
bool doPCinterpolation = false;
int vsrc, vdest, vcmd = 0, verr = 0, distance = row1 - row0;
const ModCommand srcCmd = *sndFile->Patterns[m_nPattern].GetpModCommand(row0, nchn);
const ModCommand destCmd = *sndFile->Patterns[m_nPattern].GetpModCommand(row1, nchn);
ModCommand::NOTE PCnote = NOTE_NONE;
uint16 PCinst = 0, PCparam = 0;
switch(type)
{
case PatternCursor::noteColumn:
vsrc = srcCmd.note;
vdest = destCmd.note;
vcmd = srcCmd.instr;
verr = (distance * (NOTE_MAX - 1)) / NOTE_MAX;
if(srcCmd.note == NOTE_NONE)
{
vsrc = vdest;
vcmd = destCmd.note;
} else if(destCmd.note == NOTE_NONE)
{
vdest = vsrc;
}
break;
case PatternCursor::instrColumn:
vsrc = srcCmd.instr;
vdest = destCmd.instr;
verr = (distance * 63) / 128;
if(srcCmd.instr == 0)
{
vsrc = vdest;
vcmd = destCmd.instr;
} else if(destCmd.instr == 0)
{
vdest = vsrc;
}
break;
case PatternCursor::volumeColumn:
vsrc = srcCmd.vol;
vdest = destCmd.vol;
vcmd = srcCmd.volcmd;
verr = (distance * 63) / 128;
if(srcCmd.volcmd == VOLCMD_NONE)
{
vcmd = destCmd.volcmd;
if(vcmd == VOLCMD_VOLUME && srcCmd.IsNote() && srcCmd.instr)
vsrc = GetDefaultVolume(srcCmd);
else
vsrc = vdest;
} else if(destCmd.volcmd == VOLCMD_NONE)
{
if(vcmd == VOLCMD_VOLUME && destCmd.IsNote() && destCmd.instr)
vdest = GetDefaultVolume(srcCmd);
else
vdest = vsrc;
}
break;
case PatternCursor::paramColumn:
case PatternCursor::effectColumn:
if(srcCmd.IsPcNote() || destCmd.IsPcNote())
{
doPCinterpolation = true;
PCnote = (srcCmd.IsPcNote()) ? srcCmd.note : destCmd.note;
vsrc = srcCmd.GetValueEffectCol();
vdest = destCmd.GetValueEffectCol();
PCparam = srcCmd.GetValueVolCol();
if((PCparam == 0 && destCmd.IsPcNote()) || !srcCmd.IsPcNote())
PCparam = destCmd.GetValueVolCol();
PCinst = srcCmd.instr;
if(PCinst == 0)
PCinst = destCmd.instr;
} else
{
vsrc = srcCmd.param;
vdest = destCmd.param;
vcmd = srcCmd.command;
if(srcCmd.command == CMD_NONE)
{
vsrc = vdest;
vcmd = destCmd.command;
} else if(destCmd.command == CMD_NONE)
{
vdest = vsrc;
}
}
verr = (distance * 63) / 128;
break;
default:
MPT_ASSERT(false);
return;
}
if(vdest < vsrc)
verr = -verr;
ModCommand *pcmd = sndFile->Patterns[m_nPattern].GetpModCommand(row0, nchn);
for(int i = 0; i <= distance; i++, pcmd += sndFile->GetNumChannels())
{
switch(type)
{
case PatternCursor::noteColumn:
if((pcmd->note == NOTE_NONE || pcmd->instr == vcmd) && !pcmd->IsPcNote())
{
int note = vsrc + ((vdest - vsrc) * i + verr) / distance;
pcmd->note = static_cast<ModCommand::NOTE>(note);
if(pcmd->instr == 0)
pcmd->instr = static_cast<ModCommand::VOLCMD>(vcmd);
}
break;
case PatternCursor::instrColumn:
if(pcmd->instr == 0)
{
int instr = vsrc + ((vdest - vsrc) * i + verr) / distance;
pcmd->instr = static_cast<ModCommand::INSTR>(instr);
}
break;
case PatternCursor::volumeColumn:
if((pcmd->volcmd == VOLCMD_NONE || pcmd->volcmd == vcmd) && !pcmd->IsPcNote())
{
int vol = vsrc + ((vdest - vsrc) * i + verr) / distance;
pcmd->vol = static_cast<ModCommand::VOL>(vol);
pcmd->volcmd = static_cast<ModCommand::VOLCMD>(vcmd);
}
break;
case PatternCursor::effectColumn:
if(doPCinterpolation)
{ // With PC/PCs notes, copy PCs note and plug index to all rows where
// effect interpolation is done if no PC note with non-zero instrument is there.
const uint16 val = static_cast<uint16>(vsrc + ((vdest - vsrc) * i + verr) / distance);
if(!pcmd->IsPcNote() || pcmd->instr == 0)
{
pcmd->note = PCnote;
pcmd->instr = static_cast<ModCommand::INSTR>(PCinst);
}
pcmd->SetValueVolCol(PCparam);
pcmd->SetValueEffectCol(val);
} else if(!pcmd->IsPcNote())
{
if((pcmd->command == CMD_NONE) || (pcmd->command == vcmd))
{
int val = vsrc + ((vdest - vsrc) * i + verr) / distance;
pcmd->param = static_cast<ModCommand::PARAM>(val);
pcmd->command = static_cast<ModCommand::COMMAND>(vcmd);
}
}
break;
default:
MPT_ASSERT(false);
}
}
changed = true;
} //end for all channels where type is selected
if(changed)
{
SetModified(false);
InvalidatePattern(false);
}
}
void CViewPattern::OnResetChannelColors()
{
CModDoc &modDoc = *GetDocument();
const CSoundFile &sndFile = *GetSoundFile();
modDoc.GetPatternUndo().PrepareChannelUndo(0, sndFile.GetNumChannels(), "Reset Channel Colours");
if(modDoc.SetDefaultChannelColors())
{
if(modDoc.SupportsChannelColors())
modDoc.SetModified();
modDoc.UpdateAllViews(nullptr, GeneralHint().Channels(), nullptr);
} else
{
modDoc.GetPatternUndo().RemoveLastUndoStep();
}
}
void CViewPattern::OnTransposeChannel()
{
CInputDlg dlg(this, _T("Enter transpose amount (affects all patterns):"), -(NOTE_MAX - NOTE_MIN), (NOTE_MAX - NOTE_MIN), m_nTransposeAmount);
if(dlg.DoModal() == IDOK)
{
m_nTransposeAmount = dlg.resultAsInt;
CSoundFile &sndFile = *GetSoundFile();
bool changed = false;
// Don't allow notes outside our supported note range.
const ModCommand::NOTE noteMin = sndFile.GetModSpecifications().noteMin;
const ModCommand::NOTE noteMax = sndFile.GetModSpecifications().noteMax;
for(PATTERNINDEX pat = 0; pat < sndFile.Patterns.Size(); pat++)
{
bool changedThisPat = false;
if(sndFile.Patterns.IsValidPat(pat))
{
ModCommand *m = sndFile.Patterns[pat].GetpModCommand(0, m_MenuCursor.GetChannel());
const ROWINDEX numRows = sndFile.Patterns[pat].GetNumRows();
for(ROWINDEX row = 0; row < numRows; row++)
{
if(m->IsNote())
{
if(!changedThisPat)
{
GetDocument()->GetPatternUndo().PrepareUndo(pat, m_MenuCursor.GetChannel(), 0, 1, numRows, "Transpose Channel", changed);
changed = changedThisPat = true;
}
int note = m->note + m_nTransposeAmount;
Limit(note, noteMin, noteMax);
m->note = static_cast<ModCommand::NOTE>(note);
}
m += sndFile.Patterns[pat].GetNumChannels();
}
}
}
if(changed)
{
SetModified(true);
InvalidatePattern(false);
}
}
}
void CViewPattern::OnTransposeCustom()
{
CInputDlg dlg(this, _T("Enter transpose amount:"), -(NOTE_MAX - NOTE_MIN), (NOTE_MAX - NOTE_MIN), m_nTransposeAmount);
if(dlg.DoModal() == IDOK)
{
m_nTransposeAmount = dlg.resultAsInt;
TransposeSelection(dlg.resultAsInt);
}
}
void CViewPattern::OnTransposeCustomQuick()
{
if(m_nTransposeAmount != 0)
TransposeSelection(m_nTransposeAmount);
else
OnTransposeCustom();
}
bool CViewPattern::TransposeSelection(int transp)
{
CSoundFile *pSndFile = GetSoundFile();
if(pSndFile == nullptr || !pSndFile->Patterns.IsValidPat(m_nPattern))
{
return false;
}
m_Selection.Sanitize(pSndFile->Patterns[m_nPattern].GetNumRows(), pSndFile->GetNumChannels());
// Don't allow notes outside our supported note range.
const ModCommand::NOTE noteMin = pSndFile->GetModSpecifications().noteMin;
const ModCommand::NOTE noteMax = pSndFile->GetModSpecifications().noteMax;
PrepareUndo(m_Selection, "Transpose");
std::vector<int> lastGroupSize(pSndFile->GetNumChannels(), 12);
ApplyToSelection([&] (ModCommand &m, ROWINDEX, CHANNELINDEX chn)
{
if(chn == m_Selection.GetStartChannel() && m_Selection.GetStartColumn() > PatternCursor::noteColumn)
return;
if(m.IsNote())
{
if(m.instr > 0)
{
lastGroupSize[chn] = GetDocument()->GetInstrumentGroupSize(m.instr);
}
int transpose = transp;
if(transpose == 12000 || transpose == -12000)
{
// Transpose one octave
transpose = lastGroupSize[chn] * mpt::signum(transpose);
}
int note = m.note + transpose;
Limit(note, noteMin, noteMax);
m.note = static_cast<ModCommand::NOTE>(note);
}
});
SetModified(false);
InvalidateSelection();
if(m_Selection.GetNumChannels() == 1 && m_Selection.GetNumRows() == 1 && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_PLAYTRANSPOSE))
{
// Preview a single transposed note
PreviewNote(m_Selection.GetStartRow(), m_Selection.GetStartChannel());
}
return true;
}
bool CViewPattern::DataEntry(bool up, bool coarse)
{
CSoundFile *pSndFile = GetSoundFile();
if(pSndFile == nullptr || !pSndFile->Patterns.IsValidPat(m_nPattern))
{
return false;
}
m_Selection.Sanitize(pSndFile->Patterns[m_nPattern].GetNumRows(), pSndFile->GetNumChannels());
const PatternCursor::Columns column = m_Selection.GetStartColumn();
// Don't allow notes outside our supported note range.
const ModCommand::NOTE noteMin = pSndFile->GetModSpecifications().noteMin;
const ModCommand::NOTE noteMax = pSndFile->GetModSpecifications().noteMax;
const int instrMax = std::min(static_cast<int>(Util::MaxValueOfType(ModCommand::INSTR())), static_cast<int>(pSndFile->GetNumInstruments() ? pSndFile->GetNumInstruments() : pSndFile->GetNumSamples()));
const EffectInfo effectInfo(*pSndFile);
const int offset = up ? 1 : -1;
PrepareUndo(m_Selection, "Data Entry");
// Notes per octave for non-TET12 tunings and coarse note steps
std::vector<int> lastGroupSize(pSndFile->GetNumChannels(), 12);
bool applyToSpecialNotes = true;
if(column == PatternCursor::noteColumn)
{
const CPattern &pattern = pSndFile->Patterns[m_nPattern];
const CHANNELINDEX startChn = m_Selection.GetStartChannel(), endChn = m_Selection.GetEndChannel();
const ROWINDEX endRow = m_Selection.GetEndRow();
for(ROWINDEX row = m_Selection.GetStartRow(); row <= endRow && applyToSpecialNotes; row++)
{
const ModCommand *m = pattern.GetpModCommand(row, startChn);
for(CHANNELINDEX chn = startChn; chn <= endChn; chn++, m++)
{
if(!m_Selection.ContainsHorizontal(PatternCursor(0, chn, PatternCursor::noteColumn)))
continue;
if(m->IsNote())
{
applyToSpecialNotes = false;
break;
}
}
}
}
ApplyToSelection([&] (ModCommand &m, ROWINDEX, CHANNELINDEX chn)
{
if(column == PatternCursor::noteColumn && m_Selection.ContainsHorizontal(PatternCursor(0, chn, PatternCursor::noteColumn)))
{
// Increase / decrease note
if(m.IsNote() && !applyToSpecialNotes)
{
if(m.instr > 0)
{
lastGroupSize[chn] = GetDocument()->GetInstrumentGroupSize(m.instr);
}
int note = m.note + offset * (coarse ? lastGroupSize[chn] : 1);
Limit(note, noteMin, noteMax);
m.note = (ModCommand::NOTE)note;
} else if(m.IsSpecialNote() && applyToSpecialNotes)
{
ModCommand::NOTE note = m.note;
do
{
note = static_cast<ModCommand::NOTE>(note + offset);
if(!ModCommand::IsSpecialNote(note))
{
break;
}
} while(!pSndFile->GetModSpecifications().HasNote(note));
if(ModCommand::IsSpecialNote(note))
{
if(m.IsPcNote() != ModCommand::IsPcNote(note))
{
m.Clear();
}
m.note = (ModCommand::NOTE)note;
}
}
}
if(column == PatternCursor::instrColumn && m_Selection.ContainsHorizontal(PatternCursor(0, chn, PatternCursor::instrColumn)) && m.instr != 0)
{
// Increase / decrease instrument
int instr = m.instr + offset * (coarse ? 10 : 1);
Limit(instr, 1, m.IsInstrPlug() ? MAX_MIXPLUGINS : instrMax);
m.instr = (ModCommand::INSTR)instr;
}
if(column == PatternCursor::volumeColumn && m_Selection.ContainsHorizontal(PatternCursor(0, chn, PatternCursor::volumeColumn)))
{
// Increase / decrease volume parameter
if(m.IsPcNote())
{
int val = m.GetValueVolCol() + offset * (coarse ? 10 : 1);
Limit(val, 0, ModCommand::maxColumnValue);
m.SetValueVolCol(static_cast<uint16>(val));
} else
{
int vol = m.vol + offset * (coarse ? 10 : 1);
if(m.volcmd == VOLCMD_NONE && m.IsNote() && m.instr)
{
m.volcmd = VOLCMD_VOLUME;
vol = GetDefaultVolume(m);
}
ModCommand::VOL minValue = 0, maxValue = 64;
effectInfo.GetVolCmdInfo(effectInfo.GetIndexFromVolCmd(m.volcmd), nullptr, &minValue, &maxValue);
Limit(vol, (int)minValue, (int)maxValue);
m.vol = (ModCommand::VOL)vol;
}
}
if((column == PatternCursor::effectColumn || column == PatternCursor::paramColumn) && (m_Selection.ContainsHorizontal(PatternCursor(0, chn, PatternCursor::effectColumn)) || m_Selection.ContainsHorizontal(PatternCursor(0, chn, PatternCursor::paramColumn))))
{
// Increase / decrease effect parameter
if(m.IsPcNote())
{
int val = m.GetValueEffectCol() + offset * (coarse ? 10 : 1);
Limit(val, 0, ModCommand::maxColumnValue);
m.SetValueEffectCol(static_cast<uint16>(val));
} else
{
int param = m.param + offset * (coarse ? 16 : 1);
ModCommand::PARAM minValue = 0x00, maxValue = 0xFF;
if(!m.IsSlideUpDownCommand())
{
const auto effectIndex = effectInfo.GetIndexFromEffect(m.command, m.param);
effectInfo.GetEffectInfo(effectIndex, nullptr, false, &minValue, &maxValue);
minValue = static_cast<ModCommand::PARAM>(effectInfo.MapPosToValue(effectIndex, minValue));
maxValue = static_cast<ModCommand::PARAM>(effectInfo.MapPosToValue(effectIndex, maxValue));
}
m.param = static_cast<ModCommand::PARAM>(Clamp(param, minValue, maxValue));
}
}
});
SetModified(false);
InvalidatePattern();
if(column == PatternCursor::noteColumn && m_Selection.GetNumChannels() == 1 && m_Selection.GetNumRows() == 1 && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_PLAYTRANSPOSE))
{
// Preview a single transposed note
PreviewNote(m_Selection.GetStartRow(), m_Selection.GetStartChannel());
}
return true;
}
// Get the velocity at which a given note would be played
int CViewPattern::GetDefaultVolume(const ModCommand &m, ModCommand::INSTR lastInstr) const
{
const CSoundFile &sndFile = *GetSoundFile();
SAMPLEINDEX sample = GetDocument()->GetSampleIndex(m, lastInstr);
if(sample)
return std::min(sndFile.GetSample(sample).nVolume, uint16(256)) / 4u;
else if(m.instr > 0 && m.instr <= sndFile.GetNumInstruments() && sndFile.Instruments[m.instr] != nullptr && sndFile.Instruments[m.instr]->HasValidMIDIChannel())
return std::min(sndFile.Instruments[m.instr]->nGlobalVol, uint32(64)); // For instrument plugins
else
return 64;
}
int CViewPattern::GetBaseNote() const
{
const CModDoc *modDoc = GetDocument();
INSTRUMENTINDEX instr = static_cast<INSTRUMENTINDEX>(GetCurrentInstrument());
if(!instr && !IsLiveRecord())
instr = GetCursorCommand().instr;
return modDoc->GetBaseNote(instr);
}
ModCommand::NOTE CViewPattern::GetNoteWithBaseOctave(int note) const
{
const CModDoc *modDoc = GetDocument();
INSTRUMENTINDEX instr = static_cast<INSTRUMENTINDEX>(GetCurrentInstrument());
if(!instr && !IsLiveRecord())
instr = GetCursorCommand().instr;
return modDoc->GetNoteWithBaseOctave(note, instr);
}
void CViewPattern::OnDropSelection()
{
CModDoc *pModDoc;
if((pModDoc = GetDocument()) == nullptr || !IsEditingEnabled_bmsg())
{
return;
}
CSoundFile &sndFile = pModDoc->GetSoundFile();
if(!sndFile.Patterns.IsValidPat(m_nPattern))
{
return;
}
// Compute relative movement
int dx = (int)m_DragPos.GetChannel() - (int)m_StartSel.GetChannel();
int dy = (int)m_DragPos.GetRow() - (int)m_StartSel.GetRow();
if((!dx) && (!dy))
{
return;
}
// Allocate replacement pattern
CPattern &pattern = sndFile.Patterns[m_nPattern];
auto origPattern = pattern.GetData();
// Compute destination rect
PatternCursor begin(m_Selection.GetUpperLeft()), end(m_Selection.GetLowerRight());
begin.Move(dy, dx, 0);
if(begin.GetChannel() >= sndFile.GetNumChannels())
{
// Moved outside pattern range.
return;
}
end.Move(dy, dx, 0);
if(end.GetColumnType() == PatternCursor::effectColumn)
{
// Extend to parameter column
end.Move(0, 0, 1);
}
begin.Sanitize(pattern.GetNumRows(), pattern.GetNumChannels());
end.Sanitize(pattern.GetNumRows(), pattern.GetNumChannels());
PatternRect destination(begin, end);
const bool moveSelection = !m_Status[psKeyboardDragSelect | psCtrlDragSelect];
BeginWaitCursor();
pModDoc->GetPatternUndo().PrepareUndo(m_nPattern, 0, 0, sndFile.GetNumChannels(), pattern.GetNumRows(), moveSelection ? "Move Selection" : "Copy Selection");
const ModCommand empty = ModCommand::Empty();
auto p = pattern.begin();
for(ROWINDEX row = 0; row < pattern.GetNumRows(); row++)
{
for(CHANNELINDEX chn = 0; chn < sndFile.GetNumChannels(); chn++, p++)
{
for(int c = PatternCursor::firstColumn; c <= PatternCursor::lastColumn; c++)
{
PatternCursor cell(row, chn, static_cast<PatternCursor::Columns>(c));
int xsrc = chn, ysrc = row;
if(destination.Contains(cell))
{
// Current cell is from destination selection
xsrc -= dx;
ysrc -= dy;
} else if(m_Selection.Contains(cell))
{
// Current cell is from source rectangle (clear)
if(moveSelection)
{
xsrc = -1;
}
} else
{
continue;
}
// Copy the data
const ModCommand &src = (xsrc >= 0 && xsrc < (int)sndFile.GetNumChannels() && ysrc >= 0 && ysrc < (int)sndFile.Patterns[m_nPattern].GetNumRows()) ? origPattern[ysrc * sndFile.GetNumChannels() + xsrc] : empty;
switch(c)
{
case PatternCursor::noteColumn:
p->note = src.note;
break;
case PatternCursor::instrColumn:
p->instr = src.instr;
break;
case PatternCursor::volumeColumn:
p->vol = src.vol;
p->volcmd = src.volcmd;
break;
case PatternCursor::effectColumn:
p->command = src.command;
p->param = src.param;
break;
}
}
}
}
// Fix: Horizontal scrollbar pos screwed when selecting with mouse
SetCursorPosition(begin);
SetCurSel(destination);
InvalidatePattern();
SetModified(false);
EndWaitCursor();
}
void CViewPattern::OnSetSelInstrument()
{
SetSelectionInstrument(static_cast<INSTRUMENTINDEX>(GetCurrentInstrument()), false);
}
void CViewPattern::OnRemoveChannelDialog()
{
CModDoc *pModDoc = GetDocument();
if(pModDoc == nullptr)
return;
pModDoc->ChangeNumChannels(0);
SetCurrentPattern(m_nPattern); //Updating the screen.
}
void CViewPattern::OnRemoveChannel()
{
CModDoc *pModDoc = GetDocument();
if(pModDoc == nullptr)
return;
const CSoundFile &sndFile = pModDoc->GetSoundFile();
if(sndFile.GetNumChannels() <= sndFile.GetModSpecifications().channelsMin)
{
Reporting::Error("No channel removed - channel number already at minimum.", "Remove channel");
return;
}
CHANNELINDEX nChn = m_MenuCursor.GetChannel();
const bool isEmpty = pModDoc->IsChannelUnused(nChn);
CString str;
str.Format(_T("Remove channel %d? This channel still contains note data!"), nChn + 1);
if(isEmpty || Reporting::Confirm(str, "Remove channel") == cnfYes)
{
std::vector<bool> keepMask(pModDoc->GetNumChannels(), true);
keepMask[nChn] = false;
pModDoc->RemoveChannels(keepMask, true);
SetCurrentPattern(m_nPattern); //Updating the screen.
pModDoc->UpdateAllViews(nullptr, GeneralHint().General().Channels(), this);
}
}
void CViewPattern::AddChannel(CHANNELINDEX parent, bool afterCurrent)
{
CModDoc *pModDoc = GetDocument();
if(pModDoc == nullptr)
return;
BeginWaitCursor();
// Create new channel order, with channel nBefore being an invalid (and thus empty) channel.
std::vector<CHANNELINDEX> channels(pModDoc->GetNumChannels() + 1, CHANNELINDEX_INVALID);
CHANNELINDEX i = 0;
for(CHANNELINDEX nChn = 0; nChn < pModDoc->GetNumChannels() + 1; nChn++)
{
if(nChn != (parent + (afterCurrent ? 1 : 0)))
{
channels[nChn] = i++;
}
}
if(pModDoc->ReArrangeChannels(channels) != CHANNELINDEX_INVALID)
{
auto &chnSettings = pModDoc->GetSoundFile().ChnSettings;
chnSettings[parent + (afterCurrent ? 1 : 0)].color = chnSettings[parent + (afterCurrent ? 0 : 1)].color;
pModDoc->SetModified();
pModDoc->UpdateAllViews(nullptr, GeneralHint().General().Channels(), this); //refresh channel headers
SetCurrentPattern(m_nPattern);
}
EndWaitCursor();
}
void CViewPattern::OnDuplicateChannel()
{
CModDoc *pModDoc = GetDocument();
if(pModDoc == nullptr)
return;
const CHANNELINDEX dupChn = m_MenuCursor.GetChannel();
if(dupChn >= pModDoc->GetNumChannels())
return;
if(!pModDoc->IsChannelUnused(dupChn) && Reporting::Confirm(_T("This affects all patterns, proceed?"), _T("Duplicate Channel")) != cnfYes)
return;
BeginWaitCursor();
// Create new channel order, with channel nDupChn duplicated.
std::vector<CHANNELINDEX> channels(pModDoc->GetNumChannels() + 1, 0);
CHANNELINDEX i = 0;
for(CHANNELINDEX nChn = 0; nChn < pModDoc->GetNumChannels() + 1; nChn++)
{
channels[nChn] = i;
if(nChn != dupChn)
i++;
}
// Check that duplication happened and in that case update.
if(pModDoc->ReArrangeChannels(channels) != CHANNELINDEX_INVALID)
{
pModDoc->SetModified();
pModDoc->UpdateAllViews(nullptr, GeneralHint().General().Channels(), this); //refresh channel headers
SetCurrentPattern(m_nPattern);
}
EndWaitCursor();
}
void CViewPattern::OnRunScript()
{
;
}
void CViewPattern::OnSwitchToOrderList()
{
PostCtrlMessage(CTRLMSG_SETFOCUS);
}
void CViewPattern::OnPrevOrder()
{
PostCtrlMessage(CTRLMSG_PREVORDER);
}
void CViewPattern::OnNextOrder()
{
PostCtrlMessage(CTRLMSG_NEXTORDER);
}
void CViewPattern::OnUpdateUndo(CCmdUI *pCmdUI)
{
CModDoc *pModDoc = GetDocument();
if((pCmdUI) && (pModDoc))
{
pCmdUI->Enable(pModDoc->GetPatternUndo().CanUndo());
pCmdUI->SetText(CMainFrame::GetInputHandler()->GetKeyTextFromCommand(kcEditUndo, _T("Undo ") + pModDoc->GetPatternUndo().GetUndoName()));
}
}
void CViewPattern::OnUpdateRedo(CCmdUI *pCmdUI)
{
CModDoc *pModDoc = GetDocument();
if((pCmdUI) && (pModDoc))
{
pCmdUI->Enable(pModDoc->GetPatternUndo().CanRedo());
pCmdUI->SetText(CMainFrame::GetInputHandler()->GetKeyTextFromCommand(kcEditRedo, _T("Redo ") + pModDoc->GetPatternUndo().GetRedoName()));
}
}
void CViewPattern::OnEditUndo()
{
UndoRedo(true);
}
void CViewPattern::OnEditRedo()
{
UndoRedo(false);
}
void CViewPattern::UndoRedo(bool undo)
{
CModDoc *pModDoc = GetDocument();
if(pModDoc && IsEditingEnabled_bmsg())
{
CHANNELINDEX oldNumChannels = pModDoc->GetNumChannels();
PATTERNINDEX pat = undo ? pModDoc->GetPatternUndo().Undo() : pModDoc->GetPatternUndo().Redo();
const CSoundFile &sndFile = pModDoc->GetSoundFile();
if(pat < sndFile.Patterns.Size())
{
if(pat != m_nPattern)
{
// Find pattern in sequence.
ORDERINDEX matchingOrder = Order().FindOrder(pat, GetCurrentOrder());
if(matchingOrder != ORDERINDEX_INVALID)
{
SetCurrentOrder(matchingOrder);
}
SetCurrentPattern(pat);
} else
{
InvalidatePattern(true, true);
}
SetModified(false);
SanitizeCursor();
UpdateScrollSize();
}
if(oldNumChannels != pModDoc->GetNumChannels())
{
pModDoc->UpdateAllViews(this, GeneralHint().Channels().ModType(), this);
}
}
}
// Apply amplification and fade function to volume
static void AmplifyFade(int &vol, int amp, ROWINDEX row, ROWINDEX numRows, int fadeIn, int fadeOut, Fade::Func &fadeFunc)
{
const bool doFadeIn = fadeIn != amp, doFadeOut = fadeOut != amp;
const double fadeStart = fadeIn / 100.0, fadeStartDiff = (amp - fadeIn) / 100.0;
const double fadeEnd = fadeOut / 100.0, fadeEndDiff = (amp - fadeOut) / 100.0;
double l;
if(doFadeIn && doFadeOut)
{
ROWINDEX numRows2 = numRows / 2;
if(row < numRows2)
l = fadeStart + fadeFunc(static_cast<double>(row) / numRows2) * fadeStartDiff;
else
l = fadeEnd + fadeFunc(static_cast<double>(numRows - row) / (numRows - numRows2)) * fadeEndDiff;
} else if(doFadeIn)
{
l = fadeStart + fadeFunc(static_cast<double>(row + 1) / numRows) * fadeStartDiff;
} else if(doFadeOut)
{
l = fadeEnd + fadeFunc(static_cast<double>(numRows - row) / numRows) * fadeEndDiff;
} else
{
l = amp / 100.0;
}
vol = mpt::saturate_round<int>(vol * l);
Limit(vol, 0, 64);
}
void CViewPattern::OnPatternAmplify()
{
static CAmpDlg::AmpSettings settings{Fade::kLinear, 0, 0, 100, false, false};
CAmpDlg dlg(this, settings, 0);
if(dlg.DoModal() != IDOK)
{
return;
}
CSoundFile &sndFile = *GetSoundFile();
if(!sndFile.Patterns.IsValidPat(m_nPattern))
return;
const bool useVolCol = sndFile.GetModSpecifications().HasVolCommand(VOLCMD_VOLUME);
BeginWaitCursor();
PrepareUndo(m_Selection, "Amplify");
m_Selection.Sanitize(sndFile.Patterns[m_nPattern].GetNumRows(), sndFile.GetNumChannels());
const CHANNELINDEX firstChannel = m_Selection.GetStartChannel(), lastChannel = m_Selection.GetEndChannel();
const ROWINDEX firstRow = m_Selection.GetStartRow(), lastRow = m_Selection.GetEndRow();
// For partically selected start and end channels, we check if the start and end columns contain the relevant columns.
bool firstChannelValid, lastChannelValid;
if(useVolCol)
{
// Volume column
firstChannelValid = m_Selection.ContainsHorizontal(PatternCursor(0, firstChannel, PatternCursor::volumeColumn));
lastChannelValid = m_Selection.ContainsHorizontal(PatternCursor(0, lastChannel, PatternCursor::volumeColumn));
} else
{
// Effect column
firstChannelValid = true; // We cannot start "too far right" in the channel, since this is the last column.
lastChannelValid = m_Selection.GetLowerRight().CompareColumn(PatternCursor(0, lastChannel, PatternCursor::effectColumn)) >= 0;
}
// Adjust min/max channel if they're only partly selected (i.e. volume column or effect column (when using .MOD) is not covered)
// XXX if only the effect column is marked in the XM format, we cannot amplify volume commands there. Does anyone use that?
if((!firstChannelValid && firstChannel >= lastChannel) || (!lastChannelValid && lastChannel <= firstChannel))
{
EndWaitCursor();
return;
}
// Volume memory for each channel.
std::vector<ModCommand::VOL> chvol(lastChannel + 1, 64);
// First, fill the volume memory in case we start the selection before some note
ApplyToSelection([&] (ModCommand &m, ROWINDEX, CHANNELINDEX chn)
{
if((chn == firstChannel && !firstChannelValid) || (chn == lastChannel && !lastChannelValid))
return;
if(m.command == CMD_VOLUME)
chvol[chn] = std::min(m.param, ModCommand::PARAM(64));
else if(m.volcmd == VOLCMD_VOLUME)
chvol[chn] = m.vol;
else if(m.instr != 0)
chvol[chn] = static_cast<ModCommand::VOL>(GetDefaultVolume(m));
});
Fade::Func fadeFunc = GetFadeFunc(settings.fadeLaw);
// Now do the actual amplification
const int cy = lastRow - firstRow + 1; // total rows (for fading)
ApplyToSelection([&] (ModCommand &m, ROWINDEX nRow, CHANNELINDEX chn)
{
if((chn == firstChannel && !firstChannelValid) || (chn == lastChannel && !lastChannelValid))
return;
if(m.command == CMD_VOLUME)
chvol[chn] = std::min(m.param, ModCommand::PARAM(64));
else if(m.volcmd == VOLCMD_VOLUME)
chvol[chn] = m.vol;
else if(m.instr != 0)
chvol[chn] = static_cast<ModCommand::VOL>(GetDefaultVolume(m));
if(settings.fadeIn || settings.fadeOut || (m.IsNote() && m.instr != 0))
{
// Insert new volume commands where necessary
if(useVolCol && m.volcmd == VOLCMD_NONE)
{
m.volcmd = VOLCMD_VOLUME;
m.vol = chvol[chn];
} else if(!useVolCol && m.command == CMD_NONE)
{
m.command = CMD_VOLUME;
m.param = chvol[chn];
}
}
if(m.volcmd == VOLCMD_VOLUME)
{
int vol = m.vol;
AmplifyFade(vol, settings.factor, nRow - firstRow, cy, settings.fadeIn ? settings.fadeInStart : settings.factor, settings.fadeOut ? settings.fadeOutEnd : settings.factor, fadeFunc);
m.vol = static_cast<ModCommand::VOL>(vol);
}
if(m_Selection.ContainsHorizontal(PatternCursor(0, chn, PatternCursor::effectColumn)) || m_Selection.ContainsHorizontal(PatternCursor(0, chn, PatternCursor::paramColumn)))
{
if(m.command == CMD_VOLUME && m.param <= 64)
{
int vol = m.param;
AmplifyFade(vol, settings.factor, nRow - firstRow, cy, settings.fadeIn ? settings.fadeInStart : settings.factor, settings.fadeOut ? settings.fadeOutEnd : settings.factor, fadeFunc);
m.param = static_cast<ModCommand::PARAM>(vol);
}
}
});
SetModified(false);
InvalidateSelection();
EndWaitCursor();
}
LRESULT CViewPattern::OnPlayerNotify(Notification *pnotify)
{
CSoundFile *pSndFile = GetSoundFile();
if(pSndFile == nullptr || pnotify == nullptr)
{
return 0;
}
if(pnotify->type[Notification::Position])
{
ORDERINDEX ord = pnotify->order;
ROWINDEX row = pnotify->row;
PATTERNINDEX pat = pnotify->pattern;
bool updateOrderList = false;
if(m_nLastPlayedOrder != ord)
{
updateOrderList = true;
m_nLastPlayedOrder = ord;
}
if(row < m_nLastPlayedRow)
{
InvalidateChannelsHeaders();
}
m_nLastPlayedRow = row;
if(!pSndFile->m_SongFlags[SONG_PAUSED | SONG_STEP])
{
const auto &order = Order();
if(ord >= order.GetLength() || order[ord] != pat)
{
//order doesn't correlate with pattern, so mark it as invalid
ord = ORDERINDEX_INVALID;
}
if(m_pEffectVis && m_pEffectVis->m_hWnd)
{
m_pEffectVis->SetPlayCursor(pat, row);
}
// Simple detection of backwards-going patterns to avoid jerky animation
m_nNextPlayRow = ROWINDEX_INVALID;
if((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_SMOOTHSCROLL) && pSndFile->Patterns.IsValidPat(pat) && pSndFile->Patterns[pat].IsValidRow(row))
{
for(const ModCommand *m = pSndFile->Patterns[pat].GetRow(row), *mEnd = m + pSndFile->GetNumChannels(); m != mEnd; m++)
{
if(m->command == CMD_PATTERNBREAK)
m_nNextPlayRow = m->param;
else if(m->command == CMD_POSITIONJUMP && (m_nNextPlayRow == ROWINDEX_INVALID || pSndFile->GetType() == MOD_TYPE_XM))
m_nNextPlayRow = 0;
}
}
if(m_nNextPlayRow == ROWINDEX_INVALID)
m_nNextPlayRow = row + 1;
m_nTicksOnRow = pnotify->ticksOnRow;
SetPlayCursor(pat, row, pnotify->tick);
// Don't follow song if user drags selections or scrollbars.
if((m_Status & (psFollowSong | psDragActive)) == psFollowSong)
{
if(pat < pSndFile->Patterns.Size())
{
if(pat != m_nPattern || ord != m_nOrder || updateOrderList)
{
if(pat != m_nPattern)
SetCurrentPattern(pat, row);
else if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_SHOWPREVIOUS)
InvalidatePattern(true, true); // Redraw previous / next pattern
if(ord < order.GetLength())
{
m_nOrder = ord;
SendCtrlMessage(CTRLMSG_NOTIFYCURRENTORDER, ord);
}
updateOrderList = false;
}
if(row != GetCurrentRow())
{
SetCurrentRow((row < pSndFile->Patterns[pat].GetNumRows()) ? row : 0, false, false);
}
}
} else
{
if(updateOrderList)
{
SendCtrlMessage(CTRLMSG_FORCEREFRESH); //force orderlist refresh
updateOrderList = false;
}
}
}
}
if(pnotify->type[Notification::VUMeters | Notification::Stop] && m_Status[psShowVUMeters])
{
UpdateAllVUMeters(pnotify);
}
if(pnotify->type[Notification::Stop])
{
m_baPlayingNote.reset();
ChnVUMeters.fill(0); // Also zero all non-visible VU meters
SetPlayCursor(PATTERNINDEX_INVALID, ROWINDEX_INVALID, 0);
}
UpdateIndicator(false);
return 0;
}
// record plugin parameter changes into current pattern
LRESULT CViewPattern::OnRecordPlugParamChange(WPARAM plugSlot, LPARAM paramIndex)
{
CModDoc *pModDoc = GetDocument();
if(pModDoc == nullptr || !IsEditingEnabled())
return 0;
CSoundFile &sndFile = pModDoc->GetSoundFile();
//Work out where to put the new data
const PatternEditPos editPos = GetEditPos(sndFile, IsLiveRecord());
const CHANNELINDEX chn = editPos.channel;
const ROWINDEX row = editPos.row;
const PATTERNINDEX pattern = editPos.pattern;
ModCommand &mSrc = *sndFile.Patterns[pattern].GetpModCommand(row, chn);
ModCommand m = mSrc;
// TODO: Is the right plugin active? Move to a chan with the right plug
// Probably won't do this - finish fluctuator implementation instead.
IMixPlugin *pPlug = sndFile.m_MixPlugins[plugSlot].pMixPlugin;
if(pPlug == nullptr)
return 0;
if(sndFile.GetModSpecifications().HasNote(NOTE_PCS))
{
// MPTM: Use PC Notes
// only overwrite existing PC Notes
if(m.IsEmpty() || m.IsPcNote())
{
m.Set(NOTE_PCS, static_cast<ModCommand::INSTR>(plugSlot + 1), static_cast<uint16>(paramIndex), static_cast<uint16>(pPlug->GetParameter(static_cast<PlugParamIndex>(paramIndex)) * ModCommand::maxColumnValue));
}
} else if(sndFile.GetModSpecifications().HasCommand(CMD_SMOOTHMIDI))
{
// Other formats: Use MIDI macros
// Figure out which plug param (if any) is controllable using the active macro on this channel.
int activePlugParam = -1;
auto activeMacro = sndFile.m_PlayState.Chn[chn].nActiveMacro;
if(sndFile.m_MidiCfg.GetParameteredMacroType(activeMacro) == kSFxPlugParam)
activePlugParam = sndFile.m_MidiCfg.MacroToPlugParam(activeMacro);
// If the wrong macro is active, see if we can find the right one.
// If we can, activate it for this chan by writing appropriate SFx command it.
if(activePlugParam != paramIndex)
{
int foundMacro = sndFile.m_MidiCfg.FindMacroForParam(static_cast<PlugParamIndex>(paramIndex));
if(foundMacro >= 0)
{
sndFile.m_PlayState.Chn[chn].nActiveMacro = static_cast<uint8>(foundMacro);
if(m.command == CMD_NONE || m.command == CMD_SMOOTHMIDI || m.command == CMD_MIDI) //we overwrite existing Zxx and \xx only.
{
m.command = CMD_S3MCMDEX;
if(!sndFile.GetModSpecifications().HasCommand(CMD_S3MCMDEX))
m.command = CMD_MODCMDEX;
m.param = 0xF0 | (foundMacro & 0x0F);
}
}
}
// Write the data, but we only overwrite if the command is a macro anyway.
if(m.command == CMD_NONE || m.command == CMD_SMOOTHMIDI || m.command == CMD_MIDI)
{
m.command = CMD_SMOOTHMIDI;
PlugParamValue param = pPlug->GetParameter(static_cast<PlugParamIndex>(paramIndex));
Limit(param, 0.0f, 1.0f);
m.param = static_cast<ModCommand::PARAM>(param * 127.0f);
}
}
if(m != mSrc)
{
pModDoc->GetPatternUndo().PrepareUndo(pattern, chn, row, 1, 1, "Automation Entry");
mSrc = m;
InvalidateCell(PatternCursor(row, chn));
SetModified(false);
}
return 0;
}
PatternEditPos CViewPattern::GetEditPos(const CSoundFile &sndFile, const bool liveRecord) const
{
PatternEditPos editPos;
if(liveRecord)
{
if(m_nPlayPat != PATTERNINDEX_INVALID)
{
editPos.row = m_nPlayRow;
editPos.order = GetCurrentOrder();
editPos.pattern = m_nPlayPat;
} else
{
editPos.row = sndFile.m_PlayState.m_nRow;
editPos.order = sndFile.m_PlayState.m_nCurrentOrder;
editPos.pattern = sndFile.m_PlayState.m_nPattern;
}
if(!sndFile.Patterns.IsValidPat(editPos.pattern) || !sndFile.Patterns[editPos.pattern].IsValidRow(editPos.row))
{
editPos.row = GetCurrentRow();
editPos.order = GetCurrentOrder();
editPos.pattern = m_nPattern;
}
const auto &order = Order();
if(!order.IsValidPat(editPos.order) || order[editPos.order] != editPos.pattern)
{
ORDERINDEX realOrder = order.FindOrder(editPos.pattern, editPos.order);
if(realOrder != ORDERINDEX_INVALID)
editPos.order = realOrder;
}
} else
{
editPos.row = GetCurrentRow();
editPos.order = GetCurrentOrder();
editPos.pattern = m_nPattern;
}
editPos.channel = GetCurrentChannel();
return editPos;
}
// Return ModCommand at the given cursor position of the current pattern.
// If the position is not valid, a pointer to a dummy command is returned.
ModCommand &CViewPattern::GetModCommand(PatternCursor cursor)
{
CSoundFile *pSndFile = GetSoundFile();
if(pSndFile != nullptr && pSndFile->Patterns.IsValidPat(GetCurrentPattern()) && pSndFile->Patterns[GetCurrentPattern()].IsValidRow(cursor.GetRow()))
{
return *pSndFile->Patterns[GetCurrentPattern()].GetpModCommand(cursor.GetRow(), cursor.GetChannel());
}
// Failed.
static ModCommand dummy;
return dummy;
}
// Sanitize cursor so that it can't point to an invalid position in the current pattern.
void CViewPattern::SanitizeCursor()
{
CSoundFile *pSndFile = GetSoundFile();
if(pSndFile != nullptr && pSndFile->Patterns.IsValidPat(GetCurrentPattern()))
{
m_Cursor.Sanitize(GetSoundFile()->Patterns[m_nPattern].GetNumRows(), GetSoundFile()->Patterns[m_nPattern].GetNumChannels());
}
};
// Returns pointer to modcommand at given position.
// If the position is not valid, a pointer to a dummy command is returned.
ModCommand &CViewPattern::GetModCommand(CSoundFile &sndFile, const PatternEditPos &pos)
{
static ModCommand dummy;
if(sndFile.Patterns.IsValidPat(pos.pattern) && pos.row < sndFile.Patterns[pos.pattern].GetNumRows() && pos.channel < sndFile.GetNumChannels())
return *sndFile.Patterns[pos.pattern].GetpModCommand(pos.row, pos.channel);
else
return dummy;
}
LRESULT CViewPattern::OnMidiMsg(WPARAM dwMidiDataParam, LPARAM)
{
const uint32 midiData = static_cast<uint32>(dwMidiDataParam);
static uint8 midiVolume = 127;
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
CModDoc *pModDoc = GetDocument();
if(pModDoc == nullptr || pMainFrm == nullptr)
return 0;
CSoundFile &sndFile = pModDoc->GetSoundFile();
//Midi message from our perspective:
// +---------------------------+---------------------------+-------------+-------------+
//bit: | 24.23.22.21 | 20.19.18.17 | 16.15.14.13 | 12.11.10.09 | 08.07.06.05 | 04.03.02.01 |
// +---------------------------+---------------------------+-------------+-------------+
// | Velocity (0-127) | Note (middle C is 60) | Event | Channel |
// +---------------------------+---------------------------+-------------+-------------+
//(http://home.roadrunner.com/~jgglatt/tech/midispec.htm)
//Notes:
//. Initial midi data handling is done in MidiInCallBack().
//. If no event is received, previous event is assumed.
//. A note-on (event=9) with velocity 0 is equivalent to a note off.
//. Basing the event solely on the velocity as follows is incorrect,
// since a note-off can have a velocity too:
// BYTE event = (dwMidiData>>16) & 0x64;
//. Sample- and instrumentview handle midi mesages in their own methods.
const uint8 midiByte1 = MIDIEvents::GetDataByte1FromEvent(midiData);
const uint8 midiByte2 = MIDIEvents::GetDataByte2FromEvent(midiData);
const uint8 channel = MIDIEvents::GetChannelFromEvent(midiData);
const uint8 nNote = midiByte1 + NOTE_MIN;
int vol = midiByte2; // At this stage nVol is a non linear value in [0;127]
// Need to convert to linear in [0;64] - see below
MIDIEvents::EventType event = MIDIEvents::GetTypeFromEvent(midiData);
if((event == MIDIEvents::evNoteOn) && !vol)
event = MIDIEvents::evNoteOff; //Convert event to note-off if req'd
// Handle MIDI mapping.
PLUGINDEX mappedIndex = uint8_max;
PlugParamIndex paramIndex = 0;
uint16 paramValue = uint16_max;
bool captured = sndFile.GetMIDIMapper().OnMIDImsg(midiData, mappedIndex, paramIndex, paramValue);
// Handle MIDI messages assigned to shortcuts
CInputHandler *ih = CMainFrame::GetInputHandler();
if(ih->HandleMIDIMessage(static_cast<InputTargetContext>(kCtxViewPatterns + 1 + m_Cursor.GetColumnType()), midiData) != kcNull
|| ih->HandleMIDIMessage(kCtxAllContexts, midiData) != kcNull)
{
// Mapped to a command, no need to pass message on.
captured = true;
}
// Write parameter control commands if needed.
if(paramValue != uint16_max && IsEditingEnabled() && sndFile.GetType() == MOD_TYPE_MPT)
{
const bool liveRecord = IsLiveRecord();
PatternEditPos editPos = GetEditPos(sndFile, liveRecord);
ModCommand &m = GetModCommand(sndFile, editPos);
pModDoc->GetPatternUndo().PrepareUndo(editPos.pattern, editPos.channel, editPos.row, 1, 1, "MIDI Mapping Record");
m.Set(NOTE_PCS, mappedIndex, static_cast<uint16>(paramIndex), static_cast<uint16>((paramValue * ModCommand::maxColumnValue) / 16383));
if(!liveRecord)
InvalidateRow(editPos.row);
pModDoc->SetModified();
pModDoc->UpdateAllViews(this, PatternHint(editPos.pattern).Data(), this);
}
if(captured)
{
// Event captured by MIDI mapping or shortcut, no need to pass message on.
return 1;
}
const auto &modSpecs = sndFile.GetModSpecifications();
bool recordParamAsZxx = false;
switch(event)
{
case MIDIEvents::evNoteOff: // Note Off
if(m_midiSustainActive[channel])
{
m_midiSustainBuffer[channel].push_back(midiData);
return 1;
}
// The following method takes care of:
// . Silencing specific active notes (just setting nNote to 255 as was done before is not acceptible)
// . Entering a note off in pattern if required
TempStopNote(nNote, ((TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_RECORDNOTEOFF) != 0));
break;
case MIDIEvents::evNoteOn: // Note On
// Continue playing as soon as MIDI notes are being received
if((pMainFrm->GetSoundFilePlaying() != &sndFile || sndFile.IsPaused()) && (TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_PLAYPATTERNONMIDIIN))
pModDoc->OnPatternPlayNoLoop();
vol = CMainFrame::ApplyVolumeRelatedSettings(midiData, midiVolume);
if(vol < 0)
vol = -1;
else
vol = (vol + 3) / 4; //Value from [0,256] to [0,64]
TempEnterNote(nNote, vol, true);
break;
case MIDIEvents::evPolyAftertouch: // Polyphonic aftertouch
EnterAftertouch(nNote, vol);
break;
case MIDIEvents::evChannelAftertouch: // Channel aftertouch
EnterAftertouch(NOTE_NONE, midiByte1);
break;
case MIDIEvents::evPitchBend: // Pitch wheel
recordParamAsZxx = (TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_MIDIMACROPITCHBEND) != 0 || modSpecs.HasCommand(CMD_FINETUNE);
break;
case MIDIEvents::evControllerChange: //Controller change
// Checking whether to record MIDI controller change as MIDI macro change.
// Don't write this if command was already written by MIDI mapping.
if((paramValue == uint16_max || sndFile.GetType() != MOD_TYPE_MPT)
&& (TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_MIDIMACROCONTROL)
&& !TrackerSettings::Instance().midiIgnoreCCs.Get()[midiByte1 & 0x7F])
{
recordParamAsZxx = true;
}
switch(midiByte1)
{
case MIDIEvents::MIDICC_Volume_Coarse:
midiVolume = midiByte2;
break;
case MIDIEvents::MIDICC_HoldPedal_OnOff:
m_midiSustainActive[channel] = (midiByte2 >= 0x40);
if(!m_midiSustainActive[channel])
{
// Release all notes
for(const auto offEvent : m_midiSustainBuffer[channel])
{
OnMidiMsg(offEvent, 0);
}
m_midiSustainBuffer[channel].clear();
}
recordParamAsZxx = false;
break;
}
break;
case MIDIEvents::evSystem:
if(TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_RESPONDTOPLAYCONTROLMSGS)
{
// Respond to MIDI song messages
switch(channel)
{
case MIDIEvents::sysStart: //Start song
pModDoc->OnPlayerPlayFromStart();
break;
case MIDIEvents::sysContinue: //Continue song
pModDoc->OnPlayerPlay();
break;
case MIDIEvents::sysStop: //Stop song
pModDoc->OnPlayerStop();
break;
}
}
break;
}
// Write CC or pitch bend message as MIDI macro change.
if(recordParamAsZxx && IsEditingEnabled())
{
const bool liveRecord = IsLiveRecord();
const auto editpos = GetEditPos(sndFile, liveRecord);
ModCommand &m = GetModCommand(sndFile, editpos);
bool update = false;
if(event == MIDIEvents::evPitchBend && (m.command == CMD_NONE || m.command == CMD_FINETUNE || m.command == CMD_FINETUNE_SMOOTH) && modSpecs.HasCommand(CMD_FINETUNE))
{
pModDoc->GetPatternUndo().PrepareUndo(editpos.pattern, editpos.channel, editpos.row, 1, 1, "MIDI Record Entry");
m.command = (m.command == CMD_NONE) ? CMD_FINETUNE : CMD_FINETUNE_SMOOTH;
m.param = (midiByte2 << 1) | (midiByte1 >> 7);
update = true;
} else if(m.IsPcNote())
{
pModDoc->GetPatternUndo().PrepareUndo(editpos.pattern, editpos.channel, editpos.row, 1, 1, "MIDI Record Entry");
m.SetValueEffectCol(static_cast<decltype(m.GetValueEffectCol())>(Util::muldivr(midiByte2, ModCommand::maxColumnValue, 127)));
update = true;
} else if((m.command == CMD_NONE || m.command == CMD_SMOOTHMIDI || m.command == CMD_MIDI)
&& (modSpecs.HasCommand(CMD_SMOOTHMIDI) || modSpecs.HasCommand(CMD_MIDI)))
{
// Write command only if there's no existing command or already a midi macro command.
pModDoc->GetPatternUndo().PrepareUndo(editpos.pattern, editpos.channel, editpos.row, 1, 1, "MIDI Record Entry");
m.command = modSpecs.HasCommand(CMD_SMOOTHMIDI) ? CMD_SMOOTHMIDI : CMD_MIDI;
m.param = midiByte2;
update = true;
}
if(update)
{
pModDoc->SetModified();
pModDoc->UpdateAllViews(this, PatternHint(editpos.pattern).Data(), this);
// Update GUI only if not recording live.
if(!liveRecord)
InvalidateRow(editpos.row);
}
}
// Pass MIDI to plugin
if(TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_MIDITOPLUG
&& pMainFrm->GetModPlaying() == pModDoc
&& event != MIDIEvents::evNoteOn
&& event != MIDIEvents::evNoteOff)
{
const INSTRUMENTINDEX instr = static_cast<INSTRUMENTINDEX>(GetCurrentInstrument());
IMixPlugin *plug = sndFile.GetInstrumentPlugin(instr);
if(plug)
{
plug->MidiSend(midiData);
// Sending MIDI may modify the plugin. For now, if MIDI data
// is not active sensing, set modified.
if(midiData != MIDIEvents::System(MIDIEvents::sysActiveSense))
pModDoc->SetModified();
}
}
return 1;
}
LRESULT CViewPattern::OnModViewMsg(WPARAM wParam, LPARAM lParam)
{
switch(wParam)
{
case VIEWMSG_SETCTRLWND:
m_hWndCtrl = (HWND)lParam;
m_nOrder = static_cast<ORDERINDEX>(SendCtrlMessage(CTRLMSG_GETCURRENTORDER));
SetCurrentPattern(static_cast<PATTERNINDEX>(SendCtrlMessage(CTRLMSG_GETCURRENTPATTERN)));
break;
case VIEWMSG_GETCURRENTPATTERN:
return m_nPattern;
case VIEWMSG_SETCURRENTPATTERN:
m_nOrder = static_cast<ORDERINDEX>(SendCtrlMessage(CTRLMSG_GETCURRENTORDER));
SetCurrentPattern(static_cast<PATTERNINDEX>(lParam));
break;
case VIEWMSG_GETCURRENTPOS:
return (m_nPattern << 16) | GetCurrentRow();
case VIEWMSG_FOLLOWSONG:
m_Status.reset(psFollowSong);
if(lParam)
{
CModDoc *pModDoc = GetDocument();
m_Status.set(psFollowSong);
if(pModDoc)
pModDoc->SetNotifications(Notification::Position | Notification::VUMeters);
if(pModDoc)
pModDoc->SetFollowWnd(m_hWnd);
SetFocus();
} else
{
InvalidateRow();
}
break;
case VIEWMSG_PATTERNLOOP:
SendCtrlMessage(CTRLMSG_PAT_LOOP, lParam);
break;
case VIEWMSG_SETRECORD:
m_Status.set(psRecordingEnabled, !!lParam);
break;
case VIEWMSG_SETSPACING:
m_nSpacing = static_cast<UINT>(lParam);
break;
case VIEWMSG_PATTERNPROPERTIES:
ShowPatternProperties(static_cast<PATTERNINDEX>(lParam));
GetParentFrame()->SetActiveView(this);
break;
case VIEWMSG_SETVUMETERS:
m_Status.set(psShowVUMeters, !!lParam);
UpdateSizes();
UpdateScrollSize();
InvalidatePattern(true, true);
break;
case VIEWMSG_SETPLUGINNAMES:
m_Status.set(psShowPluginNames, !!lParam);
UpdateSizes();
UpdateScrollSize();
InvalidatePattern(true, true);
break;
case VIEWMSG_DOMIDISPACING:
if(m_nSpacing)
{
int temp = timeGetTime();
if(temp - lParam >= 60)
{
CModDoc *pModDoc = GetDocument();
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
if(!m_Status[psFollowSong]
|| (pMainFrm->GetFollowSong(pModDoc) != m_hWnd)
|| (pModDoc->GetSoundFile().IsPaused()))
{
SetCurrentRow(GetCurrentRow() + m_nSpacing);
}
} else
{
Sleep(0);
PostMessage(WM_MOD_VIEWMSG, VIEWMSG_DOMIDISPACING, lParam);
}
}
break;
case VIEWMSG_LOADSTATE:
if(lParam)
{
PATTERNVIEWSTATE *pState = (PATTERNVIEWSTATE *)lParam;
if(pState->nDetailLevel != PatternCursor::firstColumn)
m_nDetailLevel = pState->nDetailLevel;
if(pState->initialized)
{
SetCurrentPattern(pState->nPattern);
// Fix: Horizontal scrollbar pos screwed when selecting with mouse
SetCursorPosition(pState->cursor);
SetCurSel(pState->selection);
}
}
break;
case VIEWMSG_SAVESTATE:
if(lParam)
{
PATTERNVIEWSTATE *pState = (PATTERNVIEWSTATE *)lParam;
pState->initialized = true;
pState->nPattern = m_nPattern;
pState->cursor = m_Cursor;
pState->selection = m_Selection;
pState->nDetailLevel = m_nDetailLevel;
pState->nOrder = GetCurrentOrder();
}
break;
case VIEWMSG_EXPANDPATTERN:
{
CModDoc *pModDoc = GetDocument();
if(pModDoc->ExpandPattern(m_nPattern))
{
m_Cursor.SetRow(m_Cursor.GetRow() * 2);
SetCurrentPattern(m_nPattern);
}
break;
}
case VIEWMSG_SHRINKPATTERN:
{
CModDoc *pModDoc = GetDocument();
if(pModDoc->ShrinkPattern(m_nPattern))
{
m_Cursor.SetRow(m_Cursor.GetRow() / 2);
SetCurrentPattern(m_nPattern);
}
break;
}
case VIEWMSG_COPYPATTERN:
{
const CSoundFile *pSndFile = GetSoundFile();
if(pSndFile != nullptr && pSndFile->Patterns.IsValidPat(m_nPattern))
{
CopyPattern(m_nPattern, PatternRect(PatternCursor(0, 0), PatternCursor(pSndFile->Patterns[m_nPattern].GetNumRows() - 1, pSndFile->GetNumChannels() - 1, PatternCursor::lastColumn)));
}
break;
}
case VIEWMSG_PASTEPATTERN:
PastePattern(m_nPattern, PatternCursor(0), PatternClipboard::pmOverwrite);
InvalidatePattern();
break;
case VIEWMSG_AMPLIFYPATTERN:
OnPatternAmplify();
break;
case VIEWMSG_SETDETAIL:
if(lParam != m_nDetailLevel)
{
m_nDetailLevel = static_cast<PatternCursor::Columns>(lParam);
UpdateSizes();
UpdateScrollSize();
SetCurrentColumn(m_Cursor);
InvalidatePattern(true, true);
}
break;
case VIEWMSG_DOSCROLL:
OnMouseWheel(0, static_cast<short>(lParam), CPoint(0, 0));
break;
default:
return CModScrollView::OnModViewMsg(wParam, lParam);
}
return 0;
}
void CViewPattern::CursorJump(int distance, bool snap)
{
ROWINDEX row = GetCurrentRow();
const bool upwards = distance < 0;
const int distanceAbs = std::abs(distance);
if(snap && distanceAbs)
// cppcheck false-positive
// cppcheck-suppress signConversion
row = (((row + (upwards ? -1 : 0)) / distanceAbs) + (upwards ? 0 : 1)) * distanceAbs;
else
row += distance;
row = SetCurrentRow(row, true);
if(IsLiveRecord() && !m_Status[psDragActive])
{
CriticalSection cs;
CSoundFile &sndFile = GetDocument()->GetSoundFile();
if(m_nOrder != sndFile.m_PlayState.m_nCurrentOrder)
{
// We jumped to a different order
sndFile.ResetChannels();
sndFile.StopAllVsti();
}
sndFile.m_PlayState.m_nCurrentOrder = sndFile.m_PlayState.m_nNextOrder = GetCurrentOrder();
sndFile.m_PlayState.m_nPattern = m_nPattern;
sndFile.m_PlayState.m_nRow = m_nPlayRow = row;
sndFile.m_PlayState.m_nNextRow = m_nNextPlayRow = row + 1;
// Queue the correct follow-up pattern if we just jumped to the last row.
if(sndFile.Patterns.IsValidPat(m_nPattern) && m_nNextPlayRow >= sndFile.Patterns[m_nPattern].GetNumRows())
{
sndFile.m_PlayState.m_nNextOrder++;
}
CMainFrame::GetMainFrame()->ResetNotificationBuffer();
} else
{
if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_PLAYNAVIGATEROW)
{
PatternStep(row);
}
}
}
LRESULT CViewPattern::OnCustomKeyMsg(WPARAM wParam, LPARAM lParam)
{
CModDoc *pModDoc = GetDocument();
if(!pModDoc)
return kcNull;
CSoundFile &sndFile = pModDoc->GetSoundFile();
switch(wParam)
{
case kcPrevInstrument: OnPrevInstrument(); return wParam;
case kcNextInstrument: OnNextInstrument(); return wParam;
case kcPrevOrder: OnPrevOrder(); return wParam;
case kcNextOrder: OnNextOrder(); return wParam;
case kcPatternPlayRow: OnPatternStep(); return wParam;
case kcPatternRecord: OnPatternRecord(); return wParam;
case kcCursorCopy: OnCursorCopy(); return wParam;
case kcCursorPaste: OnCursorPaste(); return wParam;
case kcChannelMute: for(CHANNELINDEX c = m_Selection.GetStartChannel(); c <= m_Selection.GetEndChannel(); c++)
OnMuteChannel(c);
return wParam;
case kcChannelSolo: OnSoloChannel(GetCurrentChannel()); return wParam;
case kcChannelUnmuteAll: OnUnmuteAll(); return wParam;
case kcToggleChanMuteOnPatTransition: for(CHANNELINDEX c = m_Selection.GetStartChannel(); c <= m_Selection.GetEndChannel(); c++)
TogglePendingMute(c);
return wParam;
case kcUnmuteAllChnOnPatTransition: OnPendingUnmuteAllChnFromClick(); return wParam;
case kcChannelRecordSelect: for(CHANNELINDEX c = m_Selection.GetStartChannel(); c <= m_Selection.GetEndChannel(); c++)
pModDoc->ToggleChannelRecordGroup(c, RecordGroup::Group1);
InvalidateChannelsHeaders(); return wParam;
case kcChannelSplitRecordSelect: for(CHANNELINDEX c = m_Selection.GetStartChannel(); c <= m_Selection.GetEndChannel(); c++)
pModDoc->ToggleChannelRecordGroup(c, RecordGroup::Group2);
InvalidateChannelsHeaders(); return wParam;
case kcChannelReset: for(CHANNELINDEX c = m_Selection.GetStartChannel(); c <= m_Selection.GetEndChannel(); c++)
ResetChannel(m_Cursor.GetChannel());
return wParam;
case kcTimeAtRow: OnShowTimeAtRow(); return wParam;
case kcSoloChnOnPatTransition: PendingSoloChn(GetCurrentChannel()); return wParam;
case kcTransposeUp: OnTransposeUp(); return wParam;
case kcTransposeDown: OnTransposeDown(); return wParam;
case kcTransposeOctUp: OnTransposeOctUp(); return wParam;
case kcTransposeOctDown: OnTransposeOctDown(); return wParam;
case kcTransposeCustom: OnTransposeCustom(); return wParam;
case kcTransposeCustomQuick: OnTransposeCustomQuick(); return wParam;
case kcDataEntryUp: DataEntry(true, false); return wParam;
case kcDataEntryDown: DataEntry(false, false); return wParam;
case kcDataEntryUpCoarse: DataEntry(true, true); return wParam;
case kcDataEntryDownCoarse: DataEntry(false, true); return wParam;
case kcSelectChannel: OnSelectCurrentChannel(); return wParam;
case kcSelectColumn: OnSelectCurrentColumn(); return wParam;
case kcPatternAmplify: OnPatternAmplify(); return wParam;
case kcPatternSetInstrumentNotEmpty:
case kcPatternSetInstrument: SetSelectionInstrument(static_cast<INSTRUMENTINDEX>(GetCurrentInstrument()), wParam == kcPatternSetInstrument); return wParam;
case kcPatternInterpolateNote: OnInterpolateNote(); return wParam;
case kcPatternInterpolateInstr: OnInterpolateInstr(); return wParam;
case kcPatternInterpolateVol: OnInterpolateVolume(); return wParam;
case kcPatternInterpolateEffect: OnInterpolateEffect(); return wParam;
case kcPatternVisualizeEffect: OnVisualizeEffect(); return wParam;
//case kcPatternOpenRandomizer: OnOpenRandomizer(); return wParam;
case kcPatternGrowSelection: OnGrowSelection(); return wParam;
case kcPatternShrinkSelection: OnShrinkSelection(); return wParam;
// Pattern navigation:
case kcPatternJumpUph1Select:
case kcPatternJumpUph1: CursorJump(-(int)GetRowsPerMeasure(), false); return wParam;
case kcPatternJumpDownh1Select:
case kcPatternJumpDownh1: CursorJump(GetRowsPerMeasure(), false); return wParam;
case kcPatternJumpUph2Select:
case kcPatternJumpUph2: CursorJump(-(int)GetRowsPerBeat(), false); return wParam;
case kcPatternJumpDownh2Select:
case kcPatternJumpDownh2: CursorJump(GetRowsPerBeat(), false); return wParam;
case kcPatternSnapUph1Select:
case kcPatternSnapUph1: CursorJump(-(int)GetRowsPerMeasure(), true); return wParam;
case kcPatternSnapDownh1Select:
case kcPatternSnapDownh1: CursorJump(GetRowsPerMeasure(), true); return wParam;
case kcPatternSnapUph2Select:
case kcPatternSnapUph2: CursorJump(-(int)GetRowsPerBeat(), true); return wParam;
case kcPatternSnapDownh2Select:
case kcPatternSnapDownh2: CursorJump(GetRowsPerBeat(), true); return wParam;
case kcNavigateDownSelect:
case kcNavigateDown: CursorJump(1, false); return wParam;
case kcNavigateUpSelect:
case kcNavigateUp: CursorJump(-1, false); return wParam;
case kcNavigateDownBySpacingSelect:
case kcNavigateDownBySpacing: CursorJump(m_nSpacing, false); return wParam;
case kcNavigateUpBySpacingSelect:
case kcNavigateUpBySpacing: CursorJump(-(int)m_nSpacing, false); return wParam;
case kcNavigateLeftSelect:
case kcNavigateLeft:
MoveCursor(false);
return wParam;
case kcNavigateRightSelect:
case kcNavigateRight:
MoveCursor(true);
return wParam;
case kcNavigateNextChanSelect:
case kcNavigateNextChan: SetCurrentColumn((GetCurrentChannel() + 1) % sndFile.GetNumChannels(), m_Cursor.GetColumnType()); return wParam;
case kcNavigatePrevChanSelect:
case kcNavigatePrevChan:{if(GetCurrentChannel() > 0)
SetCurrentColumn((GetCurrentChannel() - 1) % sndFile.GetNumChannels(), m_Cursor.GetColumnType());
else
SetCurrentColumn(sndFile.GetNumChannels() - 1, m_Cursor.GetColumnType());
SetSelToCursor();
return wParam;}
case kcHomeHorizontalSelect:
case kcHomeHorizontal: if (!m_Cursor.IsInFirstColumn()) SetCurrentColumn(0);
else if (GetCurrentRow() > 0) SetCurrentRow(0);
return wParam;
case kcHomeVerticalSelect:
case kcHomeVertical: if (GetCurrentRow() > 0) SetCurrentRow(0);
else if (!m_Cursor.IsInFirstColumn()) SetCurrentColumn(0);
return wParam;
case kcHomeAbsoluteSelect:
case kcHomeAbsolute: if (!m_Cursor.IsInFirstColumn()) SetCurrentColumn(0);
if (GetCurrentRow() > 0) SetCurrentRow(0);
return wParam;
case kcEndHorizontalSelect:
case kcEndHorizontal: if (m_Cursor.CompareColumn(PatternCursor(0, sndFile.GetNumChannels() - 1, m_nDetailLevel)) < 0) SetCurrentColumn(sndFile.GetNumChannels() - 1, m_nDetailLevel);
else if (GetCurrentRow() < pModDoc->GetPatternSize(m_nPattern) - 1) SetCurrentRow(pModDoc->GetPatternSize(m_nPattern) - 1);
return wParam;
case kcEndVerticalSelect:
case kcEndVertical: if (GetCurrentRow() < pModDoc->GetPatternSize(m_nPattern) - 1) SetCurrentRow(pModDoc->GetPatternSize(m_nPattern) - 1);
else if (m_Cursor.CompareColumn(PatternCursor(0, sndFile.GetNumChannels() - 1, m_nDetailLevel)) < 0) SetCurrentColumn(sndFile.GetNumChannels() - 1, m_nDetailLevel);
return wParam;
case kcEndAbsoluteSelect:
case kcEndAbsolute: SetCurrentColumn(sndFile.GetNumChannels() - 1, m_nDetailLevel);
if (GetCurrentRow() < pModDoc->GetPatternSize(m_nPattern) - 1) SetCurrentRow(pModDoc->GetPatternSize(m_nPattern) - 1);
return wParam;
case kcPrevEntryInColumn:
case kcNextEntryInColumn:
JumpToPrevOrNextEntry(wParam == kcNextEntryInColumn, false);
return wParam;
case kcPrevEntryInColumnSelect:
case kcNextEntryInColumnSelect:
JumpToPrevOrNextEntry(wParam == kcNextEntryInColumnSelect, true);
return wParam;
case kcNextPattern: { PATTERNINDEX n = m_nPattern + 1;
while ((n < sndFile.Patterns.Size()) && !sndFile.Patterns.IsValidPat(n)) n++;
SetCurrentPattern((n < sndFile.Patterns.Size()) ? n : 0);
ORDERINDEX currentOrder = GetCurrentOrder();
ORDERINDEX newOrder = Order().FindOrder(m_nPattern, currentOrder, true);
if(newOrder != ORDERINDEX_INVALID)
SetCurrentOrder(newOrder);
return wParam;
}
case kcPrevPattern: { PATTERNINDEX n = (m_nPattern) ? m_nPattern - 1 : sndFile.Patterns.Size() - 1;
while (n > 0 && !sndFile.Patterns.IsValidPat(n)) n--;
SetCurrentPattern(n);
ORDERINDEX currentOrder = GetCurrentOrder();
ORDERINDEX newOrder = Order().FindOrder(m_nPattern, currentOrder, false);
if(newOrder != ORDERINDEX_INVALID)
SetCurrentOrder(newOrder);
return wParam;
}
case kcPrevSequence:
case kcNextSequence:
SendCtrlMessage(CTRLMSG_PAT_SETSEQUENCE, mpt::wrapping_modulo(sndFile.Order.GetCurrentSequenceIndex() + (wParam == kcPrevSequence ? -1 : 1), sndFile.Order.GetNumSequences()));
return wParam;
case kcSelectWithCopySelect:
case kcSelectWithNav:
case kcSelect: if(!m_Status[psDragnDropEdit | psRowSelection | psChannelSelection | psMouseDragSelect]) m_StartSel = m_Cursor;
m_Status.set(psKeyboardDragSelect);
return wParam;
case kcSelectOffWithCopySelect:
case kcSelectOffWithNav:
case kcSelectOff: m_Status.reset(psKeyboardDragSelect | psShiftSelect);
return wParam;
case kcCopySelectWithSelect:
case kcCopySelectWithNav:
case kcCopySelect: if(!m_Status[psDragnDropEdit | psRowSelection | psChannelSelection | psMouseDragSelect]) m_StartSel = m_Cursor;
m_Status.set(psCtrlDragSelect); return wParam;
case kcCopySelectOffWithSelect:
case kcCopySelectOffWithNav:
case kcCopySelectOff: m_Status.reset(psCtrlDragSelect); return wParam;
case kcSelectBeat:
case kcSelectMeasure:
SelectBeatOrMeasure(wParam == kcSelectBeat); return wParam;
case kcSelectEvent: SetCurSel(PatternCursor(m_Selection.GetStartRow(), m_Selection.GetStartChannel(), PatternCursor::firstColumn),
PatternCursor(m_Selection.GetEndRow(), m_Selection.GetEndChannel(), PatternCursor::lastColumn));
return wParam;
case kcSelectRow: SetCurSel(PatternCursor(m_Selection.GetStartRow(), 0, PatternCursor::firstColumn),
PatternCursor(m_Selection.GetEndRow(), sndFile.GetNumChannels(), PatternCursor::lastColumn));
return wParam;
case kcClearRow: OnClearField(RowMask(), false); return wParam;
case kcClearField: OnClearField(RowMask(m_Cursor), false); return wParam;
case kcClearFieldITStyle: OnClearField(RowMask(m_Cursor), false, true); return wParam;
case kcClearRowStep: OnClearField(RowMask(), true); return wParam;
case kcClearFieldStep: OnClearField(RowMask(m_Cursor), true); return wParam;
case kcClearFieldStepITStyle: OnClearField(RowMask(m_Cursor), true, true); return wParam;
case kcDeleteRow: OnDeleteRow(); return wParam;
case kcDeleteWholeRow: OnDeleteWholeRow(); return wParam;
case kcDeleteRowGlobal: OnDeleteRowGlobal(); return wParam;
case kcDeleteWholeRowGlobal: OnDeleteWholeRowGlobal(); return wParam;
case kcInsertRow: OnInsertRow(); return wParam;
case kcInsertWholeRow: OnInsertWholeRow(); return wParam;
case kcInsertRowGlobal: OnInsertRowGlobal(); return wParam;
case kcInsertWholeRowGlobal: OnInsertWholeRowGlobal(); return wParam;
case kcShowNoteProperties: ShowEditWindow(); return wParam;
case kcShowPatternProperties: OnPatternProperties(); return wParam;
case kcShowSplitKeyboardSettings: SetSplitKeyboardSettings(); return wParam;
case kcShowEditMenu:
{
CPoint pt = GetPointFromPosition(m_Cursor);
pt.x += GetChannelWidth() / 2;
pt.y += GetRowHeight() / 2;
OnRButtonDown(0, pt);
}
return wParam;
case kcShowChannelCtxMenu:
{
CPoint pt = GetPointFromPosition(m_Cursor);
pt.x += GetChannelWidth() / 2;
pt.y = (m_szHeader.cy - m_szPluginHeader.cy) / 2;
OnRButtonDown(0, pt);
}
return wParam;
case kcShowChannelPluginCtxMenu:
{
CPoint pt = GetPointFromPosition(m_Cursor);
pt.x += GetChannelWidth() / 2;
pt.y = m_szHeader.cy - m_szPluginHeader.cy / 2;
OnRButtonDown(0, pt);
}
return wParam;
case kcPatternGoto: OnEditGoto(); return wParam;
case kcNoteCut: TempEnterNote(NOTE_NOTECUT); return wParam;
case kcNoteOff: TempEnterNote(NOTE_KEYOFF); return wParam;
case kcNoteFade: TempEnterNote(NOTE_FADE); return wParam;
case kcNotePC: TempEnterNote(NOTE_PC); return wParam;
case kcNotePCS: TempEnterNote(NOTE_PCS); return wParam;
case kcEditUndo: OnEditUndo(); return wParam;
case kcEditRedo: OnEditRedo(); return wParam;
case kcEditFind: OnEditFind(); return wParam;
case kcEditFindNext: OnEditFindNext(); return wParam;
case kcEditCut: OnEditCut(); return wParam;
case kcEditCopy: OnEditCopy(); return wParam;
case kcCopyAndLoseSelection:
OnEditCopy();
[[fallthrough]];
case kcLoseSelection:
SetSelToCursor();
return wParam;
case kcEditPaste: OnEditPaste(); return wParam;
case kcEditMixPaste: OnEditMixPaste(); return wParam;
case kcEditMixPasteITStyle: OnEditMixPasteITStyle(); return wParam;
case kcEditPasteFlood: OnEditPasteFlood(); return wParam;
case kcEditPushForwardPaste: OnEditPushForwardPaste(); return wParam;
case kcEditSelectAll: OnEditSelectAll(); return wParam;
case kcTogglePluginEditor: TogglePluginEditor(GetCurrentChannel()); return wParam;
case kcToggleFollowSong: SendCtrlMessage(CTRLMSG_PAT_FOLLOWSONG, 1); return wParam;
case kcChangeLoopStatus: SendCtrlMessage(CTRLMSG_PAT_LOOP, -1); return wParam;
case kcNewPattern: SendCtrlMessage(CTRLMSG_PAT_NEWPATTERN); return wParam;
case kcDuplicatePattern: SendCtrlMessage(CTRLMSG_PAT_DUPPATTERN); return wParam;
case kcSwitchToOrderList: OnSwitchToOrderList(); return wParam;
case kcToggleOverflowPaste: TrackerSettings::Instance().m_dwPatternSetup ^= PATTERN_OVERFLOWPASTE; return wParam;
case kcToggleNoteOffRecordPC: TrackerSettings::Instance().m_dwPatternSetup ^= PATTERN_KBDNOTEOFF; return wParam;
case kcToggleNoteOffRecordMIDI: TrackerSettings::Instance().m_dwMidiSetup ^= MIDISETUP_RECORDNOTEOFF; return wParam;
case kcPatternEditPCNotePlugin: OnTogglePCNotePluginEditor(); return wParam;
case kcQuantizeSettings: OnSetQuantize(); return wParam;
case kcLockPlaybackToRows: OnLockPatternRows(); return wParam;
case kcFindInstrument: FindInstrument(); return wParam;
case kcChannelSettings:
{
// Open centered Quick Channel Settings dialog.
CRect windowPos;
GetWindowRect(windowPos);
m_quickChannelProperties.Show(GetDocument(), m_Cursor.GetChannel(), CPoint(windowPos.left + windowPos.Width() / 2, windowPos.top + windowPos.Height() / 2));
return wParam;
}
case kcChannelTranspose: m_MenuCursor = m_Cursor; OnTransposeChannel(); return wParam;
case kcChannelDuplicate: m_MenuCursor = m_Cursor; OnDuplicateChannel(); return wParam;
case kcChannelAddBefore: m_MenuCursor = m_Cursor; OnAddChannelFront(); return wParam;
case kcChannelAddAfter: m_MenuCursor = m_Cursor; OnAddChannelAfter(); return wParam;
case kcChannelRemove: m_MenuCursor = m_Cursor; OnRemoveChannel(); return wParam;
case kcChannelMoveLeft:
if(CHANNELINDEX chn = m_Selection.GetStartChannel(); chn > 0)
DragChannel(chn, chn - 1u, m_Selection.GetNumChannels(), false);
return wParam;
case kcChannelMoveRight:
if (CHANNELINDEX chn = m_Selection.GetStartChannel(); chn < sndFile.GetNumChannels() - m_Selection.GetNumChannels())
DragChannel(chn, chn + 1u, m_Selection.GetNumChannels(), false);
return wParam;
case kcSplitPattern: m_MenuCursor = m_Cursor; OnSplitPattern(); return wParam;
case kcDecreaseSpacing:
if(m_nSpacing > 0) SetSpacing(m_nSpacing - 1);
return wParam;
case kcIncreaseSpacing:
if(m_nSpacing < MAX_SPACING) SetSpacing(m_nSpacing + 1);
return wParam;
case kcChordEditor:
{
CChordEditor dlg(this);
dlg.DoModal();
return wParam;
}
// Clipboard Manager
case kcToggleClipboardManager:
PatternClipboardDialog::Toggle();
return wParam;
case kcClipboardPrev:
PatternClipboard::CycleBackward();
PatternClipboardDialog::UpdateList();
return wParam;
case kcClipboardNext:
PatternClipboard::CycleForward();
PatternClipboardDialog::UpdateList();
return wParam;
case kcCutPatternChannel:
PatternClipboard::Copy(sndFile, GetCurrentPattern(), GetCurrentChannel());
OnEditSelectChannel();
OnClearSelection(false);
return wParam;
case kcCutPattern:
PatternClipboard::Copy(sndFile, GetCurrentPattern());
OnEditSelectAll();
OnClearSelection(false);
return wParam;
case kcCopyPatternChannel:
PatternClipboard::Copy(sndFile, GetCurrentPattern(), GetCurrentChannel());
return wParam;
case kcCopyPattern:
PatternClipboard::Copy(sndFile, GetCurrentPattern());
return wParam;
case kcPastePatternChannel:
case kcPastePattern:
if(PatternClipboard::Paste(sndFile, GetCurrentPattern(), wParam == kcPastePatternChannel ? GetCurrentChannel() : CHANNELINDEX_INVALID))
{
SetModified();
InvalidatePattern();
GetDocument()->UpdateAllViews(this, PatternHint(GetCurrentPattern()).Data(), this);
}
return wParam;
case kcTogglePatternPlayRow:
TrackerSettings::Instance().m_dwPatternSetup ^= PATTERN_PLAYNAVIGATEROW;
CMainFrame::GetMainFrame()->SetHelpText((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_PLAYNAVIGATEROW)
? _T("Play whole row when navigatin was turned is now enabled.") : _T("Play whole row when navigatin was turned is now disabled."));
return wParam;
}
// Ignore note entry if it is on key hold and user is in key-jazz mode or edit step is 0 (so repeated entry would be useless)
const auto keyCombination = KeyCombination::FromLPARAM(lParam);
const bool enterNote = keyCombination.EventType() != kKeyEventRepeat || (IsEditingEnabled() && m_nSpacing != 0);
// Ranges:
if(wParam >= kcVPStartNotes && wParam <= kcVPEndNotes)
{
if(enterNote)
TempEnterNote(GetNoteWithBaseOctave(static_cast<int>(wParam - kcVPStartNotes)));
return wParam;
} else if(wParam >= kcVPStartChords && wParam <= kcVPEndChords)
{
if(enterNote)
TempEnterChord(GetNoteWithBaseOctave(static_cast<int>(wParam - kcVPStartChords)));
return wParam;
}
if(wParam >= kcVPStartNoteStops && wParam <= kcVPEndNoteStops)
{
TempStopNote(GetNoteWithBaseOctave(static_cast<int>(wParam - kcVPStartNoteStops)));
return wParam;
} else if(wParam >= kcVPStartChordStops && wParam <= kcVPEndChordStops)
{
TempStopChord(GetNoteWithBaseOctave(static_cast<int>(wParam - kcVPStartChordStops)));
return wParam;
}
if(wParam >= kcSetSpacing0 && wParam <= kcSetSpacing9)
{
SetSpacing(static_cast<int>(wParam) - kcSetSpacing0);
return wParam;
}
if(wParam >= kcSetIns0 && wParam <= kcSetIns9)
{
if(IsEditingEnabled_bmsg())
TempEnterIns(static_cast<int>(wParam) - kcSetIns0);
return wParam;
}
if(wParam >= kcSetOctave0 && wParam <= kcSetOctave9)
{
if(IsEditingEnabled_bmsg())
TempEnterOctave(static_cast<int>(wParam) - kcSetOctave0);
return wParam;
}
if(wParam >= kcSetOctaveStop0 && wParam <= kcSetOctaveStop9)
{
TempStopOctave(static_cast<int>(wParam) - kcSetOctaveStop0);
return wParam;
}
if(wParam >= kcSetVolumeStart && wParam <= kcSetVolumeEnd)
{
if(IsEditingEnabled_bmsg())
TempEnterVol(static_cast<int>(wParam) - kcSetVolumeStart);
return wParam;
}
if(wParam >= kcSetFXStart && wParam <= kcSetFXEnd)
{
if(IsEditingEnabled_bmsg())
TempEnterFX(static_cast<ModCommand::COMMAND>(wParam - kcSetFXStart + 1));
return wParam;
}
if(wParam >= kcSetFXParam0 && wParam <= kcSetFXParamF)
{
if(IsEditingEnabled_bmsg())
TempEnterFXparam(static_cast<int>(wParam) - kcSetFXParam0);
return wParam;
}
return kcNull;
}
// Move pattern cursor to left or right, respecting invisible columns.
void CViewPattern::MoveCursor(bool moveRight)
{
if(!moveRight)
{
// Move cursor one column to the left
if((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_WRAP) && m_Cursor.IsInFirstColumn())
{
// Wrap around to last channel
SetCurrentColumn(GetDocument()->GetNumChannels() - 1, m_nDetailLevel);
} else if(!m_Cursor.IsInFirstColumn())
{
m_Cursor.Move(0, 0, -1);
SetCurrentColumn(m_Cursor);
}
} else
{
// Move cursor one column to the right
const PatternCursor rightmost(0, GetDocument()->GetNumChannels() - 1, m_nDetailLevel);
if(m_Cursor.CompareColumn(rightmost) >= 0)
{
if((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_WRAP))
{
// Wrap around to first channel.
SetCurrentColumn(0);
} else
{
SetCurrentColumn(rightmost);
}
} else
{
do
{
m_Cursor.Move(0, 0, 1);
} while(m_Cursor.GetColumnType() > m_nDetailLevel);
SetCurrentColumn(m_Cursor);
}
}
}
static bool EnterPCNoteValue(int v, ModCommand &m, uint16 (ModCommand::*getMethod)() const, void (ModCommand::*setMethod)(uint16))
{
if(v < 0 || v > 9)
return false;
uint16 val = (m.*getMethod)();
// Move existing digits to left, drop out leftmost digit and push new digit to the least significant digit.
val = static_cast<uint16>((val % 100) * 10 + v);
LimitMax(val, static_cast<uint16>(ModCommand::maxColumnValue));
(m.*setMethod)(val);
return true;
}
// Enter volume effect / number in the pattern.
void CViewPattern::TempEnterVol(int v)
{
CSoundFile *pSndFile = GetSoundFile();
if(pSndFile == nullptr || !IsEditingEnabled_bmsg())
return;
PrepareUndo(m_Cursor, m_Cursor, "Volume Entry");
ModCommand &target = GetCursorCommand();
ModCommand oldcmd = target; // This is the command we are about to overwrite
const bool isDigit = (v >= 0) && (v <= 9);
if(target.IsPcNote())
{
if(EnterPCNoteValue(v, target, &ModCommand::GetValueVolCol, &ModCommand::SetValueVolCol))
m_PCNoteEditMemory = target;
} else
{
ModCommand::VOLCMD volcmd = target.volcmd;
uint16 vol = target.vol;
if(isDigit)
{
vol = ((vol * 10) + v) % 100;
if(!volcmd)
volcmd = VOLCMD_VOLUME;
} else
{
switch(v + kcSetVolumeStart)
{
case kcSetVolumeVol: volcmd = VOLCMD_VOLUME; break;
case kcSetVolumePan: volcmd = VOLCMD_PANNING; break;
case kcSetVolumeVolSlideUp: volcmd = VOLCMD_VOLSLIDEUP; break;
case kcSetVolumeVolSlideDown: volcmd = VOLCMD_VOLSLIDEDOWN; break;
case kcSetVolumeFineVolUp: volcmd = VOLCMD_FINEVOLUP; break;
case kcSetVolumeFineVolDown: volcmd = VOLCMD_FINEVOLDOWN; break;
case kcSetVolumeVibratoSpd: volcmd = VOLCMD_VIBRATOSPEED; break;
case kcSetVolumeVibrato: volcmd = VOLCMD_VIBRATODEPTH; break;
case kcSetVolumeXMPanLeft: volcmd = VOLCMD_PANSLIDELEFT; break;
case kcSetVolumeXMPanRight: volcmd = VOLCMD_PANSLIDERIGHT; break;
case kcSetVolumePortamento: volcmd = VOLCMD_TONEPORTAMENTO; break;
case kcSetVolumeITPortaUp: volcmd = VOLCMD_PORTAUP; break;
case kcSetVolumeITPortaDown: volcmd = VOLCMD_PORTADOWN; break;
case kcSetVolumeITOffset: volcmd = VOLCMD_OFFSET; break;
}
if(target.volcmd == VOLCMD_NONE && volcmd == m_cmdOld.volcmd)
{
vol = m_cmdOld.vol;
}
}
uint16 max;
switch(volcmd)
{
case VOLCMD_VOLUME:
case VOLCMD_PANNING:
max = 64;
break;
default:
max = (pSndFile->GetType() == MOD_TYPE_XM) ? 0x0F : 9;
break;
}
if(vol > max)
vol %= 10;
if(pSndFile->GetModSpecifications().HasVolCommand(volcmd))
{
m_cmdOld.volcmd = target.volcmd = volcmd;
m_cmdOld.vol = target.vol = static_cast<ModCommand::VOL>(vol);
}
}
SetSelToCursor();
if(oldcmd != target)
{
SetModified(false);
InvalidateCell(m_Cursor);
UpdateIndicator();
}
// Cursor step for command letter
if(!target.IsPcNote() && !isDigit && m_nSpacing > 0 && !IsLiveRecord() && TrackerSettings::Instance().patternStepCommands)
{
if(m_Cursor.GetRow() + m_nSpacing < pSndFile->Patterns[m_nPattern].GetNumRows() || (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL))
{
SetCurrentRow(m_Cursor.GetRow() + m_nSpacing, (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL) != 0);
}
}
}
void CViewPattern::SetSpacing(int n)
{
if(static_cast<UINT>(n) != m_nSpacing)
{
m_nSpacing = static_cast<UINT>(n);
PostCtrlMessage(CTRLMSG_SETSPACING, m_nSpacing);
}
}
// Enter an effect letter in the pattern
void CViewPattern::TempEnterFX(ModCommand::COMMAND c, int v)
{
CSoundFile *pSndFile = GetSoundFile();
if(pSndFile == nullptr || !IsEditingEnabled_bmsg())
{
return;
}
ModCommand &target = GetCursorCommand();
ModCommand oldcmd = target; // This is the command we are about to overwrite
PrepareUndo(m_Cursor, m_Cursor, "Effect Entry");
if(target.IsPcNote())
{
if(EnterPCNoteValue(c, target, &ModCommand::GetValueEffectCol, &ModCommand::SetValueEffectCol))
m_PCNoteEditMemory = target;
} else if(pSndFile->GetModSpecifications().HasCommand(c))
{
if(c != CMD_NONE)
{
if((c == m_cmdOld.command) && (!target.param) && (target.command == CMD_NONE))
{
target.param = m_cmdOld.param;
} else
{
m_cmdOld.param = 0;
}
m_cmdOld.command = c;
}
target.command = c;
if(v >= 0)
{
target.param = static_cast<ModCommand::PARAM>(v);
}
// Check for MOD/XM Speed/Tempo command
if((pSndFile->GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM))
&& (target.command == CMD_SPEED || target.command == CMD_TEMPO))
{
target.command = static_cast<ModCommand::COMMAND>((target.param <= pSndFile->GetModSpecifications().speedMax) ? CMD_SPEED : CMD_TEMPO);
}
}
SetSelToCursor();
if(oldcmd != target)
{
SetModified(false);
InvalidateCell(m_Cursor);
UpdateIndicator();
}
// Cursor step for command letter
if(!target.IsPcNote() && m_nSpacing > 0 && !IsLiveRecord() && TrackerSettings::Instance().patternStepCommands)
{
if(m_Cursor.GetRow() + m_nSpacing < pSndFile->Patterns[m_nPattern].GetNumRows() || (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL))
{
SetCurrentRow(m_Cursor.GetRow() + m_nSpacing, (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL) != 0);
}
}
}
// Enter an effect param in the pattenr
void CViewPattern::TempEnterFXparam(int v)
{
CSoundFile *pSndFile = GetSoundFile();
if(pSndFile == nullptr || !IsEditingEnabled_bmsg())
{
return;
}
ModCommand &target = GetCursorCommand();
ModCommand oldcmd = target; // This is the command we are about to overwrite
PrepareUndo(m_Cursor, m_Cursor, "Parameter Entry");
if(target.IsPcNote())
{
if(EnterPCNoteValue(v, target, &ModCommand::GetValueEffectCol, &ModCommand::SetValueEffectCol))
m_PCNoteEditMemory = target;
} else
{
target.param = static_cast<ModCommand::PARAM>((target.param << 4) | v);
if(target.command == m_cmdOld.command)
{
m_cmdOld.param = target.param;
}
// Check for MOD/XM Speed/Tempo command
if((pSndFile->GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM))
&& (target.command == CMD_SPEED || target.command == CMD_TEMPO))
{
target.command = static_cast<ModCommand::COMMAND>((target.param <= pSndFile->GetModSpecifications().speedMax) ? CMD_SPEED : CMD_TEMPO);
}
}
SetSelToCursor();
if(target != oldcmd)
{
SetModified(false);
InvalidateCell(m_Cursor);
UpdateIndicator();
}
}
// Stop a note that has been entered
void CViewPattern::TempStopNote(ModCommand::NOTE note, const bool fromMidi, bool chordMode)
{
CModDoc *pModDoc = GetDocument();
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
if(pModDoc == nullptr || pMainFrm == nullptr || !ModCommand::IsNote(note))
{
return;
}
CSoundFile &sndFile = pModDoc->GetSoundFile();
if(!sndFile.Patterns.IsValidPat(m_nPattern))
{
return;
}
const CModSpecifications &specs = sndFile.GetModSpecifications();
Limit(note, specs.noteMin, specs.noteMax);
const bool liveRecord = IsLiveRecord();
const bool isSplit = IsNoteSplit(note);
UINT ins = 0;
chordMode = chordMode && (m_prevChordNote != NOTE_NONE);
auto &activeNoteMap = isSplit ? m_splitActiveNoteChannel : m_activeNoteChannel;
const CHANNELINDEX nChnCursor = GetCurrentChannel();
const CHANNELINDEX nChn = chordMode ? m_chordPatternChannels[0] : (activeNoteMap[note] < sndFile.GetNumChannels() ? activeNoteMap[note] : nChnCursor);
CHANNELINDEX noteChannels[MPTChord::notesPerChord] = {nChn};
ModCommand::NOTE notes[MPTChord::notesPerChord] = {note};
int numNotes = 1;
if(pModDoc)
{
if(isSplit)
{
ins = pModDoc->GetSplitKeyboardSettings().splitInstrument;
if(pModDoc->GetSplitKeyboardSettings().octaveLink)
{
int trNote = note + 12 * pModDoc->GetSplitKeyboardSettings().octaveModifier;
Limit(trNote, specs.noteMin, specs.noteMax);
note = static_cast<ModCommand::NOTE>(trNote);
}
}
if(!ins)
ins = GetCurrentInstrument();
if(!ins)
ins = m_fallbackInstrument;
if(chordMode)
{
m_Status.reset(psChordPlaying);
numNotes = ConstructChord(note, notes, m_prevChordBaseNote);
if(!numNotes)
{
return;
}
for(int i = 0; i < numNotes; i++)
{
pModDoc->NoteOff(notes[i], true, static_cast<INSTRUMENTINDEX>(ins), m_noteChannel[notes[i] - NOTE_MIN]);
m_noteChannel[notes[i] - NOTE_MIN] = CHANNELINDEX_INVALID;
m_baPlayingNote.reset(notes[i]);
noteChannels[i] = m_chordPatternChannels[i];
}
m_prevChordNote = NOTE_NONE;
} else
{
m_baPlayingNote.reset(note);
pModDoc->NoteOff(note, ((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_NOTEFADE) || sndFile.GetNumInstruments() == 0), static_cast<INSTRUMENTINDEX>(ins), m_noteChannel[note - NOTE_MIN]);
m_noteChannel[note - NOTE_MIN] = CHANNELINDEX_INVALID;
}
}
// Enter note off in pattern?
if(!ModCommand::IsNote(note))
return;
if(m_Cursor.GetColumnType() > PatternCursor::instrColumn && (chordMode || !fromMidi))
return;
if(!pModDoc || !pMainFrm || !(IsEditingEnabled()))
return;
activeNoteMap[note] = NOTE_CHANNEL_MAP_INVALID; //unlock channel
if(!((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_KBDNOTEOFF) || fromMidi))
{
// We don't want to write the note-off into the pattern if this feature is disabled and we're not recording from MIDI.
return;
}
// -- write sdx if playing live
const bool usePlaybackPosition = (!chordMode) && (liveRecord && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_AUTODELAY));
//Work out where to put the note off
PatternEditPos editPos = GetEditPos(sndFile, usePlaybackPosition);
const bool doQuantize = (liveRecord || (fromMidi && (TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_PLAYPATTERNONMIDIIN))) && TrackerSettings::Instance().recordQuantizeRows != 0;
if(doQuantize)
{
QuantizeRow(editPos.pattern, editPos.row);
}
ModCommand *pTarget = sndFile.Patterns[editPos.pattern].GetpModCommand(editPos.row, nChn);
// Don't overwrite:
if(pTarget->note != NOTE_NONE || pTarget->instr || pTarget->volcmd != VOLCMD_NONE)
{
// If there's a note in the current location and the song is playing and following,
// the user probably just tapped the key - let's try the next row down.
editPos.row++;
if(pTarget->note == note && liveRecord && sndFile.Patterns[editPos.pattern].IsValidRow(editPos.row))
{
pTarget = sndFile.Patterns[editPos.pattern].GetpModCommand(editPos.row, nChn);
if(pTarget->note != NOTE_NONE || (!chordMode && (pTarget->instr || pTarget->volcmd)))
return;
} else
{
return;
}
}
bool modified = false;
for(int i = 0; i < numNotes; i++)
{
if(m_previousNote[noteChannels[i]] != notes[i])
{
// This might be a note-off from a past note, but since we already hit a new note on this channel, we ignore it.
continue;
}
if(!modified)
{
pModDoc->GetPatternUndo().PrepareUndo(editPos.pattern, nChn, editPos.row, noteChannels[numNotes - 1] - nChn + 1, 1, "Note Stop Entry");
modified = true;
}
pTarget = sndFile.Patterns[editPos.pattern].GetpModCommand(editPos.row, noteChannels[i]);
// -- write sdx if playing live
if(usePlaybackPosition && m_nPlayTick && pTarget->command == CMD_NONE && !doQuantize)
{
pTarget->command = CMD_S3MCMDEX;
if(!specs.HasCommand(CMD_S3MCMDEX))
pTarget->command = CMD_MODCMDEX;
pTarget->param = static_cast<ModCommand::PARAM>(0xD0 | std::min(uint8(0xF), mpt::saturate_cast<uint8>(m_nPlayTick)));
}
//Enter note off
if(sndFile.GetModSpecifications().hasNoteOff && (sndFile.GetNumInstruments() > 0 || !sndFile.GetModSpecifications().hasNoteCut))
{
// ===
// Not used in sample (if module format supports ^^^ instead)
pTarget->note = NOTE_KEYOFF;
} else if(sndFile.GetModSpecifications().hasNoteCut)
{
// ^^^
pTarget->note = NOTE_NOTECUT;
} else
{
// we don't have anything to cut (MOD format) - use volume or ECx
if(usePlaybackPosition && m_nPlayTick && !doQuantize) // ECx
{
pTarget->command = CMD_S3MCMDEX;
if(!specs.HasCommand(CMD_S3MCMDEX))
pTarget->command = CMD_MODCMDEX;
pTarget->param = static_cast<ModCommand::PARAM>(0xC0 | std::min(uint8(0xF), mpt::saturate_cast<uint8>(m_nPlayTick)));
} else // C00
{
pTarget->note = NOTE_NONE;
pTarget->command = CMD_VOLUME;
pTarget->param = 0;
}
}
pTarget->instr = 0; // Instrument numbers next to note-offs can do all kinds of weird things in XM files, and they are pointless anyway.
pTarget->volcmd = VOLCMD_NONE;
pTarget->vol = 0;
}
if(!modified)
return;
SetModified(false);
if(editPos.pattern == m_nPattern)
{
InvalidateRow(editPos.row);
} else
{
InvalidatePattern();
}
// Update only if not recording live.
if(!liveRecord)
{
UpdateIndicator();
}
return;
}
// Enter an octave number in the pattern
void CViewPattern::TempEnterOctave(int val)
{
const CSoundFile *pSndFile = GetSoundFile();
if(pSndFile == nullptr)
{
return;
}
const ModCommand &target = GetCursorCommand();
if(target.IsNote())
{
int groupSize = GetDocument()->GetInstrumentGroupSize(target.instr);
// The following might look a bit convoluted... This is mostly because the "middle-C" in
// custom tunings always has octave 5, no matter how many octaves the tuning actually has.
int note = mpt::wrapping_modulo(target.note - NOTE_MIDDLEC, groupSize) + (val - 5) * groupSize + NOTE_MIDDLEC;
Limit(note, NOTE_MIN, NOTE_MAX);
TempEnterNote(static_cast<ModCommand::NOTE>(note));
// Memorize note for key-up
ASSERT(size_t(val) < m_octaveKeyMemory.size());
m_octaveKeyMemory[val] = target.note;
}
}
// Stop note that has been triggered by entering an octave in the pattern.
void CViewPattern::TempStopOctave(int val)
{
ASSERT(size_t(val) < m_octaveKeyMemory.size());
if(m_octaveKeyMemory[val] != NOTE_NONE)
{
TempStopNote(m_octaveKeyMemory[val]);
m_octaveKeyMemory[val] = NOTE_NONE;
}
}
// Enter an instrument number in the pattern
void CViewPattern::TempEnterIns(int val)
{
CSoundFile *pSndFile = GetSoundFile();
if(pSndFile == nullptr || !IsEditingEnabled_bmsg())
{
return;
}
PrepareUndo(m_Cursor, m_Cursor, "Instrument Entry");
ModCommand &target = GetCursorCommand();
ModCommand oldcmd = target; // This is the command we are about to overwrite
UINT instr = target.instr, nTotalMax, nTempMax;
if(target.IsPcNote()) // this is a plugin index
{
nTotalMax = MAX_MIXPLUGINS + 1;
nTempMax = MAX_MIXPLUGINS + 1;
} else if(pSndFile->GetNumInstruments() > 0) // this is an instrument index
{
nTotalMax = MAX_INSTRUMENTS;
nTempMax = pSndFile->GetNumInstruments();
} else
{
nTotalMax = MAX_SAMPLES;
nTempMax = pSndFile->GetNumSamples();
}
instr = ((instr * 10) + val) % 1000;
if(instr >= nTotalMax)
instr = instr % 100;
if(nTempMax < 100) // if we're using samples & have less than 100 samples
instr = instr % 100; // or if we're using instruments and have less than 100 instruments
// --> ensure the entered instrument value is less than 100.
target.instr = static_cast<ModCommand::INSTR>(instr);
SetSelToCursor();
if(target != oldcmd)
{
SetModified(false);
InvalidateCell(m_Cursor);
UpdateIndicator();
}
if(target.IsPcNote())
{
m_PCNoteEditMemory = target;
}
}
// Enter a note in the pattern
void CViewPattern::TempEnterNote(ModCommand::NOTE note, int vol, bool fromMidi)
{
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
CModDoc *pModDoc = GetDocument();
if(pMainFrm == nullptr || pModDoc == nullptr)
{
return;
}
CSoundFile &sndFile = pModDoc->GetSoundFile();
if(!sndFile.Patterns.IsValidPat(m_nPattern))
{
return;
}
if(note < NOTE_MIN_SPECIAL)
{
Limit(note, sndFile.GetModSpecifications().noteMin, sndFile.GetModSpecifications().noteMax);
}
// Special case: Convert note off commands to C00 for MOD files
if((sndFile.GetType() == MOD_TYPE_MOD) && (note == NOTE_NOTECUT || note == NOTE_FADE || note == NOTE_KEYOFF))
{
TempEnterFX(CMD_VOLUME, 0);
return;
}
// Check whether the module format supports the note.
if(sndFile.GetModSpecifications().HasNote(note) == false)
{
return;
}
const bool liveRecord = IsLiveRecord();
const bool usePlaybackPosition = (liveRecord && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_AUTODELAY) && !sndFile.m_SongFlags[SONG_STEP]);
const bool isSpecial = note >= NOTE_MIN_SPECIAL;
const bool isSplit = IsNoteSplit(note);
PatternEditPos editPos = GetEditPos(sndFile, usePlaybackPosition);
const bool recordEnabled = IsEditingEnabled();
CHANNELINDEX nChn = GetCurrentChannel();
auto recordGroup = pModDoc->GetChannelRecordGroup(nChn);
if(!isSpecial && pModDoc->GetSplitKeyboardSettings().IsSplitActive()
&& ((recordGroup == RecordGroup::Group1 && isSplit) || (recordGroup == RecordGroup::Group2 && !isSplit)))
{
// Record group 1 should be used for normal notes, record group 2 for split notes.
// If there are any channels assigned to the "other" record group, we switch to another channel.
auto otherGroup = (recordGroup == RecordGroup::Group1) ? RecordGroup::Group2 : RecordGroup::Group1;
const CHANNELINDEX newChannel = FindGroupRecordChannel(otherGroup, true);
if(newChannel != CHANNELINDEX_INVALID)
{
// Found a free channel, switch to other record group.
nChn = newChannel;
recordGroup = otherGroup;
}
}
// -- Chord autodetection: step back if we just entered a note
if(recordEnabled && recordGroup != RecordGroup::NoGroup && !liveRecord && !ModCommand::IsPcNote(note) && m_nSpacing > 0)
{
const auto &order = Order();
if((timeGetTime() - m_autoChordStartTime) < TrackerSettings::Instance().gnAutoChordWaitTime
&& order.IsValidPat(m_autoChordStartOrder)
&& sndFile.Patterns[order[m_autoChordStartOrder]].IsValidRow(m_autoChordStartRow))
{
const auto pattern = order[m_autoChordStartOrder];
if(pattern != editPos.pattern)
{
SetCurrentOrder(m_autoChordStartOrder);
SetCurrentPattern(pattern, m_autoChordStartRow);
}
editPos.pattern = pattern;
editPos.row = m_autoChordStartRow;
} else
{
m_autoChordStartRow = ROWINDEX_INVALID;
m_autoChordStartOrder = ORDERINDEX_INVALID;
}
m_autoChordStartTime = timeGetTime();
if(m_autoChordStartOrder == ORDERINDEX_INVALID || m_autoChordStartRow == ROWINDEX_INVALID)
{
m_autoChordStartOrder = editPos.order;
m_autoChordStartRow = editPos.row;
}
}
// Quantize
const bool doQuantize = (liveRecord || (fromMidi && (TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_PLAYPATTERNONMIDIIN))) && TrackerSettings::Instance().recordQuantizeRows != 0;
if(doQuantize)
{
QuantizeRow(editPos.pattern, editPos.row);
// "Grace notes" are stuffed into the next row, if possible
if(sndFile.Patterns[editPos.pattern].GetpModCommand(editPos.row, nChn)->IsNote() && editPos.row < sndFile.Patterns[editPos.pattern].GetNumRows() - 1)
{
editPos.row++;
}
}
// -- Work out where to put the new note
ModCommand *pTarget = sndFile.Patterns[editPos.pattern].GetpModCommand(editPos.row, nChn);
ModCommand newcmd = *pTarget;
// Param control 'note'
if(ModCommand::IsPcNote(note))
{
if(!pTarget->IsPcNote())
{
// We're overwriting a normal cell with a PC note.
newcmd = m_PCNoteEditMemory;
if((pTarget->command == CMD_MIDI || pTarget->command == CMD_SMOOTHMIDI) && pTarget->param < 128)
{
newcmd.SetValueEffectCol(static_cast<decltype(newcmd.GetValueEffectCol())>(Util::muldivr(pTarget->param, ModCommand::maxColumnValue, 127)));
if(!newcmd.instr)
newcmd.instr = sndFile.ChnSettings[nChn].nMixPlugin;
auto activeMacro = sndFile.m_PlayState.Chn[nChn].nActiveMacro;
if(!newcmd.GetValueVolCol() && sndFile.m_MidiCfg.GetParameteredMacroType(activeMacro) == kSFxPlugParam)
{
PlugParamIndex plugParam = sndFile.m_MidiCfg.MacroToPlugParam(sndFile.m_PlayState.Chn[nChn].nActiveMacro);
if(plugParam < ModCommand::maxColumnValue)
newcmd.SetValueVolCol(static_cast<decltype(newcmd.GetValueVolCol())>(plugParam));
}
}
} else if(recordEnabled)
{
// Pick up current entry to update PC note edit memory.
m_PCNoteEditMemory = newcmd;
}
newcmd.note = note;
} else
{
// Are we overwriting a PC note here?
if(pTarget->IsPcNote())
{
newcmd.Clear();
}
// -- write note and instrument data.
HandleSplit(newcmd, note);
// Nice idea actually: Use lower section of the keyboard to play chords (but it won't work 100% correctly this way...)
/*if(isSplit)
{
TempEnterChord(note);
return;
}*/
// -- write vol data
int volWrite = -1;
if(vol >= 0 && vol <= 64 && !(isSplit && pModDoc->GetSplitKeyboardSettings().splitVolume)) //write valid volume, as long as there's no split volume override.
{
volWrite = vol;
} else if(isSplit && pModDoc->GetSplitKeyboardSettings().splitVolume) //cater for split volume override.
{
if(pModDoc->GetSplitKeyboardSettings().splitVolume > 0 && pModDoc->GetSplitKeyboardSettings().splitVolume <= 64)
{
volWrite = pModDoc->GetSplitKeyboardSettings().splitVolume;
}
}
if(volWrite != -1 && !isSpecial)
{
if(sndFile.GetModSpecifications().HasVolCommand(VOLCMD_VOLUME))
{
newcmd.volcmd = VOLCMD_VOLUME;
newcmd.vol = (ModCommand::VOL)volWrite;
} else
{
newcmd.command = CMD_VOLUME;
newcmd.param = (ModCommand::PARAM)volWrite;
}
}
// -- write sdx if playing live
if(usePlaybackPosition && m_nPlayTick && !doQuantize) // avoid SD0 which will be mis-interpreted
{
if(newcmd.command == CMD_NONE) //make sure we don't overwrite any existing commands.
{
newcmd.command = CMD_S3MCMDEX;
if(!sndFile.GetModSpecifications().HasCommand(CMD_S3MCMDEX))
newcmd.command = CMD_MODCMDEX;
uint8 maxSpeed = 0x0F;
if(m_nTicksOnRow > 0)
maxSpeed = std::min(uint8(0x0F), mpt::saturate_cast<uint8>(m_nTicksOnRow - 1));
newcmd.param = static_cast<ModCommand::PARAM>(0xD0 | std::min(maxSpeed, mpt::saturate_cast<uint8>(m_nPlayTick)));
}
}
// Note cut/off/fade: erase instrument number
if(newcmd.note >= NOTE_MIN_SPECIAL)
newcmd.instr = 0;
}
// -- if recording, create undo point and write out modified command.
const bool modified = (recordEnabled && *pTarget != newcmd);
if(modified)
{
pModDoc->GetPatternUndo().PrepareUndo(editPos.pattern, nChn, editPos.row, 1, 1, "Note Entry");
*pTarget = newcmd;
}
// -- play note
if(((TrackerSettings::Instance().m_dwPatternSetup & (PATTERN_PLAYNEWNOTE | PATTERN_PLAYEDITROW)) || !recordEnabled) && !newcmd.IsPcNote())
{
const bool playWholeRow = ((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_PLAYEDITROW) && !liveRecord);
if(playWholeRow)
{
// play the whole row in "step mode"
PatternStep(editPos.row);
if(recordEnabled && newcmd.IsNote())
m_noteChannel[newcmd.note - NOTE_MIN] = nChn;
}
if(!playWholeRow || !recordEnabled)
{
// NOTE: This code is *also* used for the PATTERN_PLAYEDITROW edit mode because of some unforseeable race conditions when modifying pattern data.
// We have to use this code when editing is disabled or else we will get some stupid hazards, because we would first have to write the new note
// data to the pattern and then remove it again - but often, it is actually removed before the row is parsed by the soundlib.
// just play the newly inserted note using the already specified instrument...
ModCommand::INSTR playIns = newcmd.instr;
if(!playIns && ModCommand::IsNoteOrEmpty(note))
{
// ...or one that can be found on a previous row of this pattern.
ModCommand *search = pTarget;
ROWINDEX srow = editPos.row;
while(srow-- > 0)
{
search -= sndFile.GetNumChannels();
if(search->instr && !search->IsPcNote())
{
playIns = search->instr;
m_fallbackInstrument = playIns; //used to figure out which instrument to stop on key release.
break;
}
}
}
PlayNote(newcmd.note, playIns, 4 * vol, nChn);
}
}
if(newcmd.IsNote())
{
m_previousNote[nChn] = note;
}
// -- if recording, handle post note entry behaviour (move cursor etc..)
if(recordEnabled)
{
PatternCursor sel(editPos.row, nChn, m_Cursor.GetColumnType());
if(!liveRecord)
{
// Update only when not recording live.
SetCurSel(sel);
}
if(modified) // Has it really changed?
{
SetModified(false);
if(editPos.pattern == m_nPattern)
InvalidateCell(sel);
else
InvalidatePattern();
if(!liveRecord)
{
// Update only when not recording live.
UpdateIndicator();
}
}
// Set new cursor position (edit step aka row spacing)
if(!liveRecord)
{
if(m_nSpacing > 0)
{
if(editPos.row + m_nSpacing < sndFile.Patterns[editPos.pattern].GetNumRows() || (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL))
{
SetCurrentRow(editPos.row + m_nSpacing, (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL) != 0);
}
}
SetSelToCursor();
}
if(newcmd.IsPcNote())
{
// Nothing to do here anymore.
return;
}
auto &activeNoteMap = isSplit ? m_splitActiveNoteChannel : m_activeNoteChannel;
if(newcmd.note <= NOTE_MAX)
activeNoteMap[newcmd.note] = static_cast<decltype(m_activeNoteChannel)::value_type>(nChn);
if(recordGroup != RecordGroup::NoGroup)
{
// Move to next channel in record group
nChn = FindGroupRecordChannel(recordGroup, false, nChn + 1);
if(nChn != CHANNELINDEX_INVALID)
{
SetCurrentColumn(nChn);
}
}
}
}
void CViewPattern::PlayNote(ModCommand::NOTE note, ModCommand::INSTR instr, int volume, CHANNELINDEX channel)
{
CModDoc *modDoc = GetDocument();
modDoc->PlayNote(PlayNoteParam(note).Instrument(instr).Volume(volume).Channel(channel).CheckNNA(m_baPlayingNote), &m_noteChannel);
}
void CViewPattern::PreviewNote(ROWINDEX row, CHANNELINDEX channel)
{
const ModCommand &m = *GetSoundFile()->Patterns[m_nPattern].GetpModCommand(row, channel);
if(m.IsNote() && m.instr)
{
int vol = -1;
if(m.command == CMD_VOLUME)
vol = m.param * 4u;
else if(m.volcmd == VOLCMD_VOLUME)
vol = m.vol * 4u;
// Note-off any previews from this channel first
ModCommand::NOTE note = NOTE_MIN;
const auto &channels = GetSoundFile()->m_PlayState.Chn;
for(auto &chn : m_noteChannel)
{
if(chn != CHANNELINDEX_INVALID && channels[chn].isPreviewNote && channels[chn].nMasterChn == channel + 1)
{
GetDocument()->NoteOff(note, false, m.instr, chn);
}
note++;
}
PlayNote(m.note, m.instr, vol, channel);
}
}
// Construct a chord from the chord presets. Returns number of notes in chord.
int CViewPattern::ConstructChord(int note, ModCommand::NOTE (&outNotes)[MPTChord::notesPerChord], ModCommand::NOTE baseNote)
{
const MPTChords &chords = TrackerSettings::GetChords();
UINT chordNum = note - GetBaseNote();
if(chordNum >= chords.size())
{
return 0;
}
const MPTChord &chord = chords[chordNum];
const bool relativeMode = (chord.key == MPTChord::relativeMode); // Notes are relative to a previously entered note in the pattern
ModCommand::NOTE key;
if(relativeMode)
{
// Relative mode: Use pattern note as base note.
// If there is no valid note in the pattern: Use shortcut note as relative base note
key = ModCommand::IsNote(baseNote) ? baseNote : static_cast<ModCommand::NOTE>(note);
} else
{
// Default mode: Use base key
key = GetNoteWithBaseOctave(chord.key);
}
if(!ModCommand::IsNote(key))
{
return 0;
}
int numNotes = 0;
const CModSpecifications &specs = GetSoundFile()->GetModSpecifications();
if(specs.HasNote(key))
{
outNotes[numNotes++] = key;
}
int32 baseKey = key - NOTE_MIN;
if(!relativeMode)
{
// Only use octave information from the base key
baseKey = (baseKey / 12) * 12;
}
for(auto cnote : chord.notes)
{
if(cnote != MPTChord::noNote)
{
int32 chordNote = baseKey + cnote + NOTE_MIN;
if(chordNote >= NOTE_MIN && chordNote <= NOTE_MAX && specs.HasNote(static_cast<ModCommand::NOTE>(chordNote)))
{
outNotes[numNotes++] = static_cast<ModCommand::NOTE>(chordNote);
}
}
}
return numNotes;
}
// Enter a chord in the pattern
void CViewPattern::TempEnterChord(ModCommand::NOTE note)
{
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
CModDoc *pModDoc = GetDocument();
if(pMainFrm == nullptr || pModDoc == nullptr)
{
return;
}
CSoundFile &sndFile = pModDoc->GetSoundFile();
if(!sndFile.Patterns.IsValidPat(m_nPattern))
{
return;
}
const CHANNELINDEX chn = GetCurrentChannel();
const PatternRow rowBase = sndFile.Patterns[m_nPattern].GetRow(GetCurrentRow());
ModCommand::NOTE chordNotes[MPTChord::notesPerChord], baseNote = rowBase[chn].note;
if(!ModCommand::IsNote(baseNote))
{
baseNote = m_prevChordBaseNote;
}
int numNotes = ConstructChord(note, chordNotes, baseNote);
if(!numNotes)
{
return;
}
// Save old row contents
std::vector<ModCommand> newRow(rowBase, rowBase + sndFile.GetNumChannels());
const bool liveRecord = IsLiveRecord();
const bool recordEnabled = IsEditingEnabled();
bool modified = false;
// -- establish note data
HandleSplit(newRow[chn], note);
const auto recordGroup = pModDoc->GetChannelRecordGroup(chn);
CHANNELINDEX curChn = chn;
for(int i = 0; i < numNotes; i++)
{
// Find appropriate channel
while(curChn < sndFile.GetNumChannels() && pModDoc->GetChannelRecordGroup(curChn) != recordGroup)
{
curChn++;
}
if(curChn >= sndFile.GetNumChannels())
{
numNotes = i;
break;
}
m_chordPatternChannels[i] = curChn;
ModCommand &m = newRow[curChn];
m_previousNote[curChn] = m.note = chordNotes[i];
if(newRow[chn].instr)
{
m.instr = newRow[chn].instr;
}
if(rowBase[chn] != m)
{
modified = true;
}
curChn++;
}
m_Status.set(psChordPlaying);
// -- write notedata
if(recordEnabled)
{
SetSelToCursor();
if(modified)
{
// Simply backup the whole row.
pModDoc->GetPatternUndo().PrepareUndo(m_nPattern, chn, GetCurrentRow(), sndFile.GetNumChannels(), 1, "Chord Entry");
for(CHANNELINDEX n = 0; n < sndFile.GetNumChannels(); n++)
{
rowBase[n] = newRow[n];
}
SetModified(false);
InvalidateRow();
UpdateIndicator();
}
}
// -- play note
if((TrackerSettings::Instance().m_dwPatternSetup & (PATTERN_PLAYNEWNOTE | PATTERN_PLAYEDITROW)) || !recordEnabled)
{
if(m_prevChordNote != NOTE_NONE)
{
TempStopChord(m_prevChordNote);
}
const bool playWholeRow = ((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_PLAYEDITROW) && !liveRecord);
if(playWholeRow)
{
// play the whole row in "step mode"
PatternStep(GetCurrentRow());
if(recordEnabled)
{
for(int i = 0; i < numNotes; i++)
{
m_noteChannel[chordNotes[i] - NOTE_MIN] = m_chordPatternChannels[i];
}
}
}
if(!playWholeRow || !recordEnabled)
{
// NOTE: This code is *also* used for the PATTERN_PLAYEDITROW edit mode because of some unforseeable race conditions when modifying pattern data.
// We have to use this code when editing is disabled or else we will get some stupid hazards, because we would first have to write the new note
// data to the pattern and then remove it again - but often, it is actually removed before the row is parsed by the soundlib.
// just play the newly inserted notes...
const ModCommand &firstNote = rowBase[chn];
ModCommand::INSTR playIns = 0;
if(firstNote.instr)
{
// ...using the already specified instrument
playIns = firstNote.instr;
} else if(!firstNote.instr)
{
// ...or one that can be found on a previous row of this pattern.
const ModCommand *search = &firstNote;
ROWINDEX srow = GetCurrentRow();
while(srow-- > 0)
{
search -= sndFile.GetNumChannels();
if(search->instr)
{
playIns = search->instr;
m_fallbackInstrument = playIns; //used to figure out which instrument to stop on key release.
break;
}
}
}
for(int i = 0; i < numNotes; i++)
{
pModDoc->PlayNote(PlayNoteParam(chordNotes[i]).Instrument(playIns).Channel(chn).CheckNNA(m_baPlayingNote), &m_noteChannel);
}
}
} // end play note
m_prevChordNote = note;
m_prevChordBaseNote = baseNote;
// Set new cursor position (edit step aka row spacing) - only when not recording live
if(recordEnabled && !liveRecord)
{
if(m_nSpacing > 0)
{
// Shift from entering chord may have triggered this flag, which will prevent us from wrapping to the next pattern.
m_Status.reset(psKeyboardDragSelect);
SetCurrentRow(GetCurrentRow() + m_nSpacing, (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL) != 0);
}
SetSelToCursor();
}
}
// Translate incoming MIDI aftertouch messages to pattern commands
void CViewPattern::EnterAftertouch(ModCommand::NOTE note, int atValue)
{
if(TrackerSettings::Instance().aftertouchBehaviour == atDoNotRecord || !IsEditingEnabled())
return;
const CHANNELINDEX numChannels = GetSoundFile()->GetNumChannels();
std::set<CHANNELINDEX> channels;
if(ModCommand::IsNote(note))
{
// For polyphonic aftertouch, map the aftertouch note to the correct pattern channel.
const auto &activeNoteMap = IsNoteSplit(note) ? m_splitActiveNoteChannel : m_activeNoteChannel;
if(activeNoteMap[note] < numChannels)
{
channels.insert(activeNoteMap[note]);
} else
{
// Couldn't find the channel that belongs to this note... Don't bother writing aftertouch messages.
// This is actually necessary, because it is possible that the last aftertouch message for a note
// is received after the key-off event, in which case OpenMPT won't know anymore on which channel
// that particular note was, so it will just put the message on some other channel. We don't want that!
return;
}
} else
{
for(const auto &noteMap : { m_activeNoteChannel, m_splitActiveNoteChannel })
{
for(const auto chn : noteMap)
{
if(chn < numChannels)
channels.insert(chn);
}
}
if(channels.empty())
channels.insert(m_Cursor.GetChannel());
}
Limit(atValue, 0, 127);
const PatternCursor endOfRow{ m_Cursor.GetRow(), static_cast<CHANNELINDEX>(numChannels - 1u), PatternCursor::lastColumn };
const auto &specs = GetSoundFile()->GetModSpecifications();
bool first = true, modified = false;
for(const auto chn : channels)
{
const PatternCursor cursor{ m_Cursor.GetRow(), chn };
ModCommand &target = GetModCommand(cursor);
ModCommand newCommand = target;
if(target.IsPcNote())
continue;
switch(TrackerSettings::Instance().aftertouchBehaviour)
{
case atRecordAsVolume:
// Record aftertouch messages as volume commands
if(specs.HasVolCommand(VOLCMD_VOLUME))
{
if(newCommand.volcmd == VOLCMD_NONE || newCommand.volcmd == VOLCMD_VOLUME)
{
newCommand.volcmd = VOLCMD_VOLUME;
newCommand.vol = static_cast<ModCommand::VOL>((atValue * 64 + 64) / 127);
}
} else if(specs.HasCommand(CMD_VOLUME))
{
if(newCommand.command == CMD_NONE || newCommand.command == CMD_VOLUME)
{
newCommand.command = CMD_VOLUME;
newCommand.param = static_cast<ModCommand::PARAM>((atValue * 64 + 64) / 127);
}
}
break;
case atRecordAsMacro:
// Record aftertouch messages as MIDI Macros
if(newCommand.command == CMD_NONE || newCommand.command == CMD_SMOOTHMIDI || newCommand.command == CMD_MIDI)
{
auto cmd =
specs.HasCommand(CMD_SMOOTHMIDI) ? CMD_SMOOTHMIDI :
specs.HasCommand(CMD_MIDI) ? CMD_MIDI :
CMD_NONE;
if(cmd != CMD_NONE)
{
newCommand.command = static_cast<ModCommand::COMMAND>(cmd);
newCommand.param = static_cast<ModCommand::PARAM>(atValue);
}
}
break;
}
if(target != newCommand)
{
if(first)
PrepareUndo(cursor, endOfRow, "Aftertouch Entry");
first = false;
modified = true;
target = newCommand;
InvalidateCell(cursor);
}
}
if(modified)
{
SetModified(false);
UpdateIndicator();
}
}
// Apply quantization factor to given row.
void CViewPattern::QuantizeRow(PATTERNINDEX &pat, ROWINDEX &row) const
{
const CSoundFile *sndFile = GetSoundFile();
if(sndFile == nullptr || TrackerSettings::Instance().recordQuantizeRows == 0)
{
return;
}
const ROWINDEX currentTick = m_nTicksOnRow * row + m_nPlayTick;
const ROWINDEX ticksPerNote = TrackerSettings::Instance().recordQuantizeRows * m_nTicksOnRow;
// Previous quantization step
const ROWINDEX quantLow = (currentTick / ticksPerNote) * ticksPerNote;
// Next quantization step
const ROWINDEX quantHigh = (1 + (currentTick / ticksPerNote)) * ticksPerNote;
if(currentTick - quantLow < quantHigh - currentTick)
{
row = quantLow / m_nTicksOnRow;
} else
{
row = quantHigh / m_nTicksOnRow;
}
if(!sndFile->Patterns[pat].IsValidRow(row))
{
// Quantization exceeds current pattern, try stuffing note into next pattern instead.
PATTERNINDEX nextPat = sndFile->m_SongFlags[SONG_PATTERNLOOP] ? m_nPattern : GetNextPattern();
if(nextPat != PATTERNINDEX_INVALID)
{
pat = nextPat;
row = 0;
} else
{
row = sndFile->Patterns[pat].GetNumRows() - 1;
}
}
}
// Get previous pattern in order list
PATTERNINDEX CViewPattern::GetPrevPattern() const
{
const CSoundFile *sndFile = GetSoundFile();
if(sndFile != nullptr)
{
const auto &order = Order();
const ORDERINDEX curOrder = GetCurrentOrder();
if(curOrder > 0 && m_nPattern == order[curOrder])
{
const ORDERINDEX nextOrder = order.GetPreviousOrderIgnoringSkips(curOrder);
const PATTERNINDEX nextPat = order[nextOrder];
if(sndFile->Patterns.IsValidPat(nextPat) && sndFile->Patterns[nextPat].GetNumRows())
{
return nextPat;
}
}
}
return PATTERNINDEX_INVALID;
}
// Get follow-up pattern in order list
PATTERNINDEX CViewPattern::GetNextPattern() const
{
const CSoundFile *sndFile = GetSoundFile();
if(sndFile != nullptr)
{
const auto &order = Order();
const ORDERINDEX curOrder = GetCurrentOrder();
if(curOrder + 1 < order.GetLength() && m_nPattern == order[curOrder])
{
const ORDERINDEX nextOrder = order.GetNextOrderIgnoringSkips(curOrder);
const PATTERNINDEX nextPat = order[nextOrder];
if(sndFile->Patterns.IsValidPat(nextPat) && sndFile->Patterns[nextPat].GetNumRows())
{
return nextPat;
}
}
}
return PATTERNINDEX_INVALID;
}
void CViewPattern::OnSetQuantize()
{
CInputDlg dlg(this, _T("Quantize amount in rows for live recording (0 to disable):"), 0, MAX_PATTERN_ROWS, TrackerSettings::Instance().recordQuantizeRows);
if(dlg.DoModal())
{
TrackerSettings::Instance().recordQuantizeRows = static_cast<ROWINDEX>(dlg.resultAsInt);
}
}
void CViewPattern::OnLockPatternRows()
{
CSoundFile &sndFile = *GetSoundFile();
if(m_Selection.GetUpperLeft() != m_Selection.GetLowerRight())
{
sndFile.m_lockRowStart = m_Selection.GetStartRow();
sndFile.m_lockRowEnd = m_Selection.GetEndRow();
} else
{
sndFile.m_lockRowStart = sndFile.m_lockRowEnd = ROWINDEX_INVALID;
}
InvalidatePattern(true, true);
}
// Find a free channel for a record group, starting search from a given channel.
// If forceFreeChannel is true and all channels in the specified record group are active, some channel is picked from the specified record group.
CHANNELINDEX CViewPattern::FindGroupRecordChannel(RecordGroup recordGroup, bool forceFreeChannel, CHANNELINDEX startChannel) const
{
const CModDoc *pModDoc = GetDocument();
if(pModDoc == nullptr)
return CHANNELINDEX_INVALID;
CHANNELINDEX chn = startChannel;
CHANNELINDEX foundChannel = CHANNELINDEX_INVALID;
for(CHANNELINDEX i = 1; i < pModDoc->GetNumChannels(); i++, chn++)
{
if(chn >= pModDoc->GetNumChannels())
chn = 0; // loop around
if(pModDoc->GetChannelRecordGroup(chn) == recordGroup)
{
// Check if any notes are playing on this channel
bool channelLocked = false;
for(size_t k = 0; k < m_activeNoteChannel.size(); k++)
{
if(m_activeNoteChannel[k] == chn || m_splitActiveNoteChannel[k] == chn)
{
channelLocked = true;
break;
}
}
if(!channelLocked)
{
// Channel belongs to correct record group and no note is currently playing.
return chn;
}
if(forceFreeChannel)
{
// If all channels are active, we might still pick a random channel from the specified group.
foundChannel = chn;
}
}
}
return foundChannel;
}
void CViewPattern::OnClearField(const RowMask &mask, bool step, bool ITStyle)
{
CSoundFile *sndFile = GetSoundFile();
if(sndFile == nullptr || !IsEditingEnabled_bmsg())
return;
// If we have a selection, we want to do something different
if(m_Selection.GetUpperLeft() != m_Selection.GetLowerRight())
{
OnClearSelection(ITStyle);
return;
}
PrepareUndo(m_Cursor, m_Cursor, "Clear Field");
ModCommand &target = GetCursorCommand();
ModCommand oldcmd = target;
if(mask.note)
{
// Clear note
if(target.IsPcNote())
{
// Need to clear entire field if this is a PC Event.
target.Clear();
} else
{
target.note = NOTE_NONE;
if(ITStyle)
{
target.instr = 0;
}
}
}
if(mask.instrument)
{
// Clear instrument
target.instr = 0;
}
if(mask.volume)
{
// Clear volume effect
target.volcmd = VOLCMD_NONE;
target.vol = 0;
}
if(mask.command)
{
// Clear effect command
target.command = CMD_NONE;
}
if(mask.parameter)
{
// Clear effect parameter
target.param = 0;
}
if((mask.command || mask.parameter) && (target.IsPcNote()))
{
target.SetValueEffectCol(0);
}
SetSelToCursor();
if(target != oldcmd)
{
SetModified(false);
InvalidateRow();
UpdateIndicator();
}
if(step && (sndFile->IsPaused() || !m_Status[psFollowSong] ||
(CMainFrame::GetMainFrame() != nullptr && CMainFrame::GetMainFrame()->GetFollowSong(GetDocument()) != m_hWnd)))
{
// Preview Row
if((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_PLAYEDITROW) && !IsLiveRecord())
{
PatternStep(GetCurrentRow());
}
if(m_nSpacing > 0)
SetCurrentRow(GetCurrentRow() + m_nSpacing);
SetSelToCursor();
}
}
void CViewPattern::OnInitMenu(CMenu *pMenu)
{
CModScrollView::OnInitMenu(pMenu);
}
void CViewPattern::TogglePluginEditor(int chan)
{
CModDoc *modDoc = GetDocument();
if(!modDoc)
return;
int plug = modDoc->GetSoundFile().ChnSettings[chan].nMixPlugin;
if(plug > 0)
modDoc->TogglePluginEditor(plug - 1);
return;
}
void CViewPattern::OnSelectInstrument(UINT nID)
{
SetSelectionInstrument(static_cast<INSTRUMENTINDEX>(nID - ID_CHANGE_INSTRUMENT), true);
}
void CViewPattern::OnSelectPCNoteParam(UINT nID)
{
CSoundFile *sndFile = GetSoundFile();
if(sndFile == nullptr || !sndFile->Patterns.IsValidPat(m_nPattern))
return;
uint16 paramNdx = static_cast<uint16>(nID - ID_CHANGE_PCNOTE_PARAM);
bool modified = false;
ApplyToSelection([paramNdx, &modified] (ModCommand &m, ROWINDEX, CHANNELINDEX)
{
if(m.IsPcNote() && (m.GetValueVolCol() != paramNdx))
{
m.SetValueVolCol(paramNdx);
modified = true;
}
});
if(modified)
{
SetModified();
InvalidatePattern();
}
}
void CViewPattern::OnSelectPlugin(UINT nID)
{
CSoundFile *sndFile = GetSoundFile();
if(sndFile == nullptr)
return;
const CHANNELINDEX plugChannel = m_MenuCursor.GetChannel();
if(plugChannel < sndFile->GetNumChannels())
{
PLUGINDEX newPlug = static_cast<PLUGINDEX>(nID - ID_PLUGSELECT);
if(newPlug <= MAX_MIXPLUGINS && newPlug != sndFile->ChnSettings[plugChannel].nMixPlugin)
{
sndFile->ChnSettings[plugChannel].nMixPlugin = newPlug;
if(sndFile->GetModSpecifications().supportsPlugins)
{
SetModified(false);
}
InvalidateChannelsHeaders();
}
}
}
bool CViewPattern::HandleSplit(ModCommand &m, int note)
{
ModCommand::INSTR ins = static_cast<ModCommand::INSTR>(GetCurrentInstrument());
const bool isSplit = IsNoteSplit(note);
if(isSplit)
{
CModDoc *modDoc = GetDocument();
if(modDoc == nullptr)
return false;
const CSoundFile &sndFile = modDoc->GetSoundFile();
if(modDoc->GetSplitKeyboardSettings().octaveLink && note <= NOTE_MAX)
{
note += 12 * modDoc->GetSplitKeyboardSettings().octaveModifier;
Limit(note, sndFile.GetModSpecifications().noteMin, sndFile.GetModSpecifications().noteMax);
}
if(modDoc->GetSplitKeyboardSettings().splitInstrument)
{
ins = modDoc->GetSplitKeyboardSettings().splitInstrument;
}
}
m.note = static_cast<ModCommand::NOTE>(note);
if(ins)
{
m.instr = ins;
}
return isSplit;
}
bool CViewPattern::IsNoteSplit(int note) const
{
CModDoc *pModDoc = GetDocument();
return (pModDoc != nullptr
&& pModDoc->GetSplitKeyboardSettings().IsSplitActive()
&& note <= pModDoc->GetSplitKeyboardSettings().splitNote);
}
bool CViewPattern::BuildPluginCtxMenu(HMENU hMenu, UINT nChn, const CSoundFile &sndFile) const
{
for(PLUGINDEX plug = 0; plug <= MAX_MIXPLUGINS; plug++)
{
bool itemFound = false;
CString s;
if(!plug)
{
s = _T("No Plugin");
itemFound = true;
} else
{
const SNDMIXPLUGIN &plugin = sndFile.m_MixPlugins[plug - 1];
if(plugin.IsValidPlugin())
{
s.Format(_T("FX%u: "), plug);
s += mpt::ToCString(plugin.GetName());
itemFound = true;
}
}
if(itemFound)
{
UINT flags = MF_STRING | ((plug == sndFile.ChnSettings[nChn].nMixPlugin) ? MF_CHECKED : 0);
AppendMenu(hMenu, flags, ID_PLUGSELECT + plug, s);
}
}
return true;
}
bool CViewPattern::BuildSoloMuteCtxMenu(HMENU hMenu, CInputHandler *ih, UINT nChn, const CSoundFile &sndFile) const
{
AppendMenu(hMenu, sndFile.ChnSettings[nChn].dwFlags[CHN_MUTE] ? (MF_STRING | MF_CHECKED) : MF_STRING, ID_PATTERN_MUTE, ih->GetKeyTextFromCommand(kcChannelMute, _T("&Mute Channel")));
bool solo = false, unmuteAll = false;
bool soloPending = false, unmuteAllPending = false; // doesn't work perfectly yet
for(CHANNELINDEX i = 0; i < sndFile.GetNumChannels(); i++)
{
if(i != nChn)
{
if(!sndFile.ChnSettings[i].dwFlags[CHN_MUTE])
solo = soloPending = true;
if(sndFile.ChnSettings[i].dwFlags[CHN_MUTE] && sndFile.m_bChannelMuteTogglePending[i])
soloPending = true;
} else
{
if(sndFile.ChnSettings[i].dwFlags[CHN_MUTE])
solo = soloPending = true;
if(!sndFile.ChnSettings[i].dwFlags[CHN_MUTE] && sndFile.m_bChannelMuteTogglePending[i])
soloPending = true;
}
if(sndFile.ChnSettings[i].dwFlags[CHN_MUTE])
unmuteAll = unmuteAllPending = true;
if(!sndFile.ChnSettings[i].dwFlags[CHN_MUTE] && sndFile.m_bChannelMuteTogglePending[i])
unmuteAllPending = true;
}
if(solo)
AppendMenu(hMenu, MF_STRING, ID_PATTERN_SOLO, ih->GetKeyTextFromCommand(kcChannelSolo, _T("&Solo Channel")));
if(unmuteAll)
AppendMenu(hMenu, MF_STRING, ID_PATTERN_UNMUTEALL, ih->GetKeyTextFromCommand(kcChannelUnmuteAll, _T("&Unmute All")));
AppendMenu(hMenu, sndFile.m_bChannelMuteTogglePending[nChn] ? (MF_STRING | MF_CHECKED) : MF_STRING, ID_PATTERN_TRANSITIONMUTE, ih->GetKeyTextFromCommand(kcToggleChanMuteOnPatTransition, sndFile.ChnSettings[nChn].dwFlags[CHN_MUTE] ? _T("On Transition: Unmute\t") : _T("On Transition: Mute\t")));
if(unmuteAllPending)
AppendMenu(hMenu, MF_STRING, ID_PATTERN_TRANSITION_UNMUTEALL, ih->GetKeyTextFromCommand(kcUnmuteAllChnOnPatTransition, _T("On Transition: Unmute All")));
if(soloPending)
AppendMenu(hMenu, MF_STRING, ID_PATTERN_TRANSITIONSOLO, ih->GetKeyTextFromCommand(kcSoloChnOnPatTransition, _T("On Transition: Solo")));
AppendMenu(hMenu, MF_STRING, ID_PATTERN_CHNRESET, ih->GetKeyTextFromCommand(kcChannelReset, _T("&Reset Channel")));
return true;
}
bool CViewPattern::BuildRecordCtxMenu(HMENU hMenu, CInputHandler *ih, CHANNELINDEX nChn) const
{
const auto recordGroup = GetDocument()->GetChannelRecordGroup(nChn);
AppendMenu(hMenu, (recordGroup == RecordGroup::Group1) ? (MF_STRING | MF_CHECKED) : MF_STRING, ID_EDIT_RECSELECT, ih->GetKeyTextFromCommand(kcChannelRecordSelect, _T("R&ecord Select")));
AppendMenu(hMenu, (recordGroup == RecordGroup::Group2) ? (MF_STRING | MF_CHECKED) : MF_STRING, ID_EDIT_SPLITRECSELECT, ih->GetKeyTextFromCommand(kcChannelSplitRecordSelect, _T("S&plit Record Select")));
return true;
}
bool CViewPattern::BuildRowInsDelCtxMenu(HMENU hMenu, CInputHandler *ih) const
{
HMENU subMenuInsert = CreatePopupMenu();
HMENU subMenuDelete = CreatePopupMenu();
const auto numRows = m_Selection.GetNumRows();
const CString label = (numRows != 1) ? MPT_CFORMAT("{} Rows")(numRows) : CString(_T("Row"));
AppendMenu(subMenuInsert, MF_STRING, ID_PATTERN_INSERTROW, ih->GetKeyTextFromCommand(kcInsertRow, _T("Insert ") + label + _T(" (&Selection)")));
AppendMenu(subMenuInsert, MF_STRING, ID_PATTERN_INSERTALLROW, ih->GetKeyTextFromCommand(kcInsertWholeRow, _T("Insert ") + label + _T(" (&All Channels)")));
AppendMenu(subMenuInsert, MF_STRING, ID_PATTERN_INSERTROWGLOBAL, ih->GetKeyTextFromCommand(kcInsertRowGlobal, _T("Insert ") + label + _T(" (Selection, &Global)")));
AppendMenu(subMenuInsert, MF_STRING, ID_PATTERN_INSERTALLROWGLOBAL, ih->GetKeyTextFromCommand(kcInsertWholeRowGlobal, _T("Insert ") + label + _T(" (All &Channels, Global)")));
AppendMenu(hMenu, MF_POPUP, reinterpret_cast<UINT_PTR>(subMenuInsert), _T("&Insert ") + label);
AppendMenu(subMenuDelete, MF_STRING, ID_PATTERN_DELETEROW, ih->GetKeyTextFromCommand(kcDeleteRow, _T("Delete ") + label + _T(" (&Selection)")));
AppendMenu(subMenuDelete, MF_STRING, ID_PATTERN_DELETEALLROW, ih->GetKeyTextFromCommand(kcDeleteWholeRow, _T("Delete ") + label + _T(" (&All Channels)")));
AppendMenu(subMenuDelete, MF_STRING, ID_PATTERN_DELETEROWGLOBAL, ih->GetKeyTextFromCommand(kcDeleteRowGlobal, _T("Delete ") + label + _T(" (Selection, &Global)")));
AppendMenu(subMenuDelete, MF_STRING, ID_PATTERN_DELETEALLROWGLOBAL, ih->GetKeyTextFromCommand(kcDeleteWholeRowGlobal, _T("Delete ") + label + _T(" (All &Channels, Global)")));
AppendMenu(hMenu, MF_POPUP, reinterpret_cast<UINT_PTR>(subMenuDelete), _T("&Delete ") + label);
return true;
}
bool CViewPattern::BuildMiscCtxMenu(HMENU hMenu, CInputHandler *ih) const
{
AppendMenu(hMenu, MF_STRING, ID_SHOWTIMEATROW, ih->GetKeyTextFromCommand(kcTimeAtRow, _T("Show Row Play Time")));
if(m_Selection.GetStartRow() == m_Selection.GetEndRow())
{
CString s;
s.Format(_T("Split Pattern at Ro&w %u"), m_Selection.GetStartRow());
AppendMenu(hMenu, MF_STRING | (m_Selection.GetStartRow() < 1 ? MF_GRAYED : 0), ID_PATTERN_SPLIT, ih->GetKeyTextFromCommand(kcSplitPattern, s));
}
const CSoundFile &sndFile = *GetSoundFile();
CString lockStr;
bool lockActive = (sndFile.m_lockRowStart != ROWINDEX_INVALID);
if(m_Selection.GetUpperLeft() != m_Selection.GetLowerRight())
{
lockStr = _T("&Lock Playback to Selection");
if(lockActive)
{
lockStr.AppendFormat(_T(" (Current: %u-%u)"), sndFile.m_lockRowStart, sndFile.m_lockRowEnd);
}
} else if(lockActive)
{
lockStr = _T("Reset Playback &Lock");
} else
{
return true;
}
AppendMenu(hMenu, MF_STRING | (lockActive ? MF_CHECKED : 0), ID_LOCK_PATTERN_ROWS, ih->GetKeyTextFromCommand(kcLockPlaybackToRows, lockStr));
return true;
}
bool CViewPattern::BuildSelectionCtxMenu(HMENU hMenu, CInputHandler *ih) const
{
AppendMenu(hMenu, MF_STRING, ID_EDIT_SELECTCOLUMN, ih->GetKeyTextFromCommand(kcSelectChannel, _T("Select &Channel")));
AppendMenu(hMenu, MF_STRING, ID_EDIT_SELECT_ALL, ih->GetKeyTextFromCommand(kcEditSelectAll, _T("Select &Pattern")));
return true;
}
bool CViewPattern::BuildGrowShrinkCtxMenu(HMENU hMenu, CInputHandler *ih) const
{
AppendMenu(hMenu, MF_STRING, ID_GROW_SELECTION, ih->GetKeyTextFromCommand(kcPatternGrowSelection, _T("&Grow selection")));
AppendMenu(hMenu, MF_STRING, ID_SHRINK_SELECTION, ih->GetKeyTextFromCommand(kcPatternShrinkSelection, _T("&Shrink selection")));
return true;
}
bool CViewPattern::BuildInterpolationCtxMenu(HMENU hMenu, CInputHandler *ih) const
{
const CSoundFile *sndFile = GetSoundFile();
const bool isPCNote = sndFile->Patterns.IsValidPat(m_nPattern) && sndFile->Patterns[m_nPattern].GetpModCommand(m_Selection.GetStartRow(), m_Selection.GetStartChannel())->IsPcNote();
HMENU subMenu = CreatePopupMenu();
bool possible = BuildInterpolationCtxMenu(subMenu, PatternCursor::noteColumn, ih->GetKeyTextFromCommand(kcPatternInterpolateNote, _T("&Note Column")), ID_PATTERN_INTERPOLATE_NOTE)
| BuildInterpolationCtxMenu(subMenu, PatternCursor::instrColumn, ih->GetKeyTextFromCommand(kcPatternInterpolateInstr, isPCNote ? _T("&Plugin Column") : _T("&Instrument Column")), ID_PATTERN_INTERPOLATE_INSTR)
| BuildInterpolationCtxMenu(subMenu, PatternCursor::volumeColumn, ih->GetKeyTextFromCommand(kcPatternInterpolateVol, isPCNote ? _T("&Parameter Column") : _T("&Volume Column")), ID_PATTERN_INTERPOLATE_VOLUME)
| BuildInterpolationCtxMenu(subMenu, PatternCursor::effectColumn, ih->GetKeyTextFromCommand(kcPatternInterpolateEffect, isPCNote ? _T("&Value Column") : _T("&Effect Column")), ID_PATTERN_INTERPOLATE_EFFECT);
if(possible || !(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OLDCTXMENUSTYLE))
{
AppendMenu(hMenu, MF_POPUP | (possible ? 0 : MF_GRAYED), reinterpret_cast<UINT_PTR>(subMenu), _T("I&nterpolate..."));
return true;
}
return false;
}
bool CViewPattern::BuildInterpolationCtxMenu(HMENU hMenu, PatternCursor::Columns colType, CString label, UINT command) const
{
bool possible = IsInterpolationPossible(colType);
if(!possible && colType == PatternCursor::effectColumn)
{
// Extend search to param column
possible = IsInterpolationPossible(PatternCursor::paramColumn);
}
if(possible || !(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OLDCTXMENUSTYLE))
{
AppendMenu(hMenu, MF_STRING | (possible ? 0 : MF_GRAYED), command, label);
}
return possible;
}
bool CViewPattern::BuildEditCtxMenu(HMENU hMenu, CInputHandler *ih, CModDoc *pModDoc) const
{
HMENU pasteSpecialMenu = ::CreatePopupMenu();
AppendMenu(hMenu, MF_STRING, ID_EDIT_CUT, ih->GetKeyTextFromCommand(kcEditCut, _T("Cu&t")));
AppendMenu(hMenu, MF_STRING, ID_EDIT_COPY, ih->GetKeyTextFromCommand(kcEditCopy, _T("&Copy")));
AppendMenu(hMenu, MF_STRING | (PatternClipboard::CanPaste() ? 0 : MF_GRAYED), ID_EDIT_PASTE, ih->GetKeyTextFromCommand(kcEditPaste, _T("&Paste")));
AppendMenu(hMenu, MF_POPUP, reinterpret_cast<UINT_PTR>(pasteSpecialMenu), _T("Paste Special"));
AppendMenu(pasteSpecialMenu, MF_STRING, ID_EDIT_MIXPASTE, ih->GetKeyTextFromCommand(kcEditMixPaste, _T("&Mix Paste")));
AppendMenu(pasteSpecialMenu, MF_STRING, ID_EDIT_MIXPASTE_ITSTYLE, ih->GetKeyTextFromCommand(kcEditMixPasteITStyle, _T("M&ix Paste (IT Style)")));
AppendMenu(pasteSpecialMenu, MF_STRING, ID_EDIT_PASTEFLOOD, ih->GetKeyTextFromCommand(kcEditPasteFlood, _T("Paste Fl&ood")));
AppendMenu(pasteSpecialMenu, MF_STRING, ID_EDIT_PUSHFORWARDPASTE, ih->GetKeyTextFromCommand(kcEditPushForwardPaste, _T("&Push Forward Paste (Insert)")));
DWORD greyed = pModDoc->GetPatternUndo().CanUndo() ? MF_ENABLED : MF_GRAYED;
if(!greyed || !(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OLDCTXMENUSTYLE))
{
AppendMenu(hMenu, MF_STRING | greyed, ID_EDIT_UNDO, ih->GetKeyTextFromCommand(kcEditUndo, _T("&Undo")));
}
greyed = pModDoc->GetPatternUndo().CanRedo() ? MF_ENABLED : MF_GRAYED;
if(!greyed || !(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OLDCTXMENUSTYLE))
{
AppendMenu(hMenu, MF_STRING | greyed, ID_EDIT_REDO, ih->GetKeyTextFromCommand(kcEditRedo, _T("&Redo")));
}
AppendMenu(hMenu, MF_STRING, ID_CLEAR_SELECTION, ih->GetKeyTextFromCommand(kcSampleDelete, _T("Clear Selection")));
return true;
}
bool CViewPattern::BuildVisFXCtxMenu(HMENU hMenu, CInputHandler *ih) const
{
DWORD greyed = (IsColumnSelected(PatternCursor::effectColumn) || IsColumnSelected(PatternCursor::paramColumn)) ? FALSE : MF_GRAYED;
if(!greyed || !(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OLDCTXMENUSTYLE))
{
AppendMenu(hMenu, MF_STRING | greyed, ID_PATTERN_VISUALIZE_EFFECT, ih->GetKeyTextFromCommand(kcPatternVisualizeEffect, _T("&Visualize Effect")));
return true;
}
return false;
}
bool CViewPattern::BuildTransposeCtxMenu(HMENU hMenu, CInputHandler *ih) const
{
HMENU transMenu = CreatePopupMenu();
std::vector<CHANNELINDEX> validChans;
DWORD greyed = IsColumnSelected(PatternCursor::noteColumn) ? FALSE : MF_GRAYED;
if(!greyed || !(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OLDCTXMENUSTYLE))
{
AppendMenu(transMenu, MF_STRING | greyed, ID_TRANSPOSE_UP, ih->GetKeyTextFromCommand(kcTransposeUp, _T("Transpose +&1")));
AppendMenu(transMenu, MF_STRING | greyed, ID_TRANSPOSE_DOWN, ih->GetKeyTextFromCommand(kcTransposeDown, _T("Transpose -&1")));
AppendMenu(transMenu, MF_STRING | greyed, ID_TRANSPOSE_OCTUP, ih->GetKeyTextFromCommand(kcTransposeOctUp, _T("Transpose +1&2")));
AppendMenu(transMenu, MF_STRING | greyed, ID_TRANSPOSE_OCTDOWN, ih->GetKeyTextFromCommand(kcTransposeOctDown, _T("Transpose -1&2")));
AppendMenu(transMenu, MF_STRING | greyed, ID_TRANSPOSE_CUSTOM, ih->GetKeyTextFromCommand(kcTransposeCustom, _T("&Custom...")));
AppendMenu(hMenu, MF_POPUP | greyed, reinterpret_cast<UINT_PTR>(transMenu), _T("&Transpose..."));
return true;
}
return false;
}
bool CViewPattern::BuildAmplifyCtxMenu(HMENU hMenu, CInputHandler *ih) const
{
std::vector<CHANNELINDEX> validChans;
DWORD greyed = IsColumnSelected(PatternCursor::volumeColumn) ? 0 : MF_GRAYED;
if(!greyed || !(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OLDCTXMENUSTYLE))
{
AppendMenu(hMenu, MF_STRING | greyed, ID_PATTERN_AMPLIFY, ih->GetKeyTextFromCommand(kcPatternAmplify, _T("&Amplify")));
return true;
}
return false;
}
bool CViewPattern::BuildChannelControlCtxMenu(HMENU hMenu, CInputHandler *ih) const
{
const CModSpecifications &specs = GetDocument()->GetSoundFile().GetModSpecifications();
CHANNELINDEX numChannels = GetDocument()->GetNumChannels();
DWORD canAddChannels = (numChannels < specs.channelsMax) ? 0 : MF_GRAYED;
DWORD canRemoveChannels = (numChannels > specs.channelsMin) ? 0 : MF_GRAYED;
AppendMenu(hMenu, MF_SEPARATOR, 0, _T(""));
AppendMenu(hMenu, MF_STRING, ID_PATTERN_TRANSPOSECHANNEL, ih->GetKeyTextFromCommand(kcChannelTranspose, _T("&Transpose Channel")));
AppendMenu(hMenu, MF_STRING | canAddChannels, ID_PATTERN_DUPLICATECHANNEL, ih->GetKeyTextFromCommand(kcChannelDuplicate, _T("&Duplicate Channel")));
HMENU addChannelMenu = ::CreatePopupMenu();
AppendMenu(hMenu, MF_POPUP | canAddChannels, reinterpret_cast<UINT_PTR>(addChannelMenu), _T("&Add Channel\t"));
AppendMenu(addChannelMenu, MF_STRING, ID_PATTERN_ADDCHANNEL_FRONT, ih->GetKeyTextFromCommand(kcChannelAddBefore, _T("&Before this channel")));
AppendMenu(addChannelMenu, MF_STRING, ID_PATTERN_ADDCHANNEL_AFTER, ih->GetKeyTextFromCommand(kcChannelAddAfter, _T("&After this channel")));
HMENU removeChannelMenu = ::CreatePopupMenu();
AppendMenu(hMenu, MF_POPUP | canRemoveChannels, reinterpret_cast<UINT_PTR>(removeChannelMenu), _T("Remo&ve Channel\t"));
AppendMenu(removeChannelMenu, MF_STRING, ID_PATTERN_REMOVECHANNEL, ih->GetKeyTextFromCommand(kcChannelRemove, _T("&Remove this channel\t")));
AppendMenu(removeChannelMenu, MF_STRING, ID_PATTERN_REMOVECHANNELDIALOG, _T("&Choose channels to remove...\t"));
AppendMenu(hMenu, MF_STRING, ID_PATTERN_RESETCHANNELCOLORS, _T("Reset Channel &Colours"));
return false;
}
bool CViewPattern::BuildSetInstCtxMenu(HMENU hMenu, CInputHandler *ih) const
{
const CSoundFile *sndFile = GetSoundFile();
const CModDoc *modDoc;
if(sndFile == nullptr || (modDoc = sndFile->GetpModDoc()) == nullptr)
{
return false;
}
std::vector<CHANNELINDEX> validChans;
DWORD greyed = IsColumnSelected(PatternCursor::instrColumn) ? 0 : MF_GRAYED;
if(!greyed || !(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OLDCTXMENUSTYLE))
{
if((sndFile->Patterns.IsValidPat(m_nPattern)))
{
if(sndFile->Patterns[m_nPattern].GetpModCommand(m_Selection.GetStartRow(), m_Selection.GetStartChannel())->IsPcNote())
{
// Don't build instrument menu for PC notes.
return false;
}
}
// Create the new menu and add it to the existing menu.
HMENU instrumentChangeMenu = ::CreatePopupMenu();
AppendMenu(hMenu, MF_POPUP | greyed, reinterpret_cast<UINT_PTR>(instrumentChangeMenu), ih->GetKeyTextFromCommand(kcPatternSetInstrument, _T("Change Instrument")));
if(!greyed)
{
bool addSeparator = false;
if(sndFile->GetNumInstruments())
{
for(INSTRUMENTINDEX i = 1; i <= sndFile->GetNumInstruments(); i++)
{
if(sndFile->Instruments[i] == nullptr)
continue;
CString instString = modDoc->GetPatternViewInstrumentName(i, true);
if(!instString.IsEmpty())
{
AppendMenu(instrumentChangeMenu, MF_STRING, ID_CHANGE_INSTRUMENT + i, modDoc->GetPatternViewInstrumentName(i));
addSeparator = true;
}
}
} else
{
CString s;
for(SAMPLEINDEX i = 1; i <= sndFile->GetNumSamples(); i++) if (sndFile->GetSample(i).HasSampleData())
{
s.Format(_T("%02d: "), i);
s += mpt::ToCString(sndFile->GetCharsetInternal(), sndFile->GetSampleName(i));
AppendMenu(instrumentChangeMenu, MF_STRING, ID_CHANGE_INSTRUMENT + i, s);
addSeparator = true;
}
}
// Add options to remove instrument from selection.
if(addSeparator)
{
AppendMenu(instrumentChangeMenu, MF_SEPARATOR, 0, 0);
}
AppendMenu(instrumentChangeMenu, MF_STRING, ID_CHANGE_INSTRUMENT, _T("&Remove Instrument"));
AppendMenu(instrumentChangeMenu, MF_STRING, ID_CHANGE_INSTRUMENT + GetCurrentInstrument(), ih->GetKeyTextFromCommand(kcPatternSetInstrument, _T("&Current Instrument")));
AppendMenu(instrumentChangeMenu, MF_STRING, ID_PATTERN_SETINSTRUMENT, ih->GetKeyTextFromCommand(kcPatternSetInstrumentNotEmpty, _T("Current Instrument (&only change existing)")));
}
return BuildTogglePlugEditorCtxMenu(hMenu, ih);
}
return false;
}
// Context menu for Param Control notes
bool CViewPattern::BuildPCNoteCtxMenu(HMENU hMenu, CInputHandler *ih) const
{
const CSoundFile *sndFile = GetSoundFile();
if(sndFile == nullptr || !sndFile->Patterns.IsValidPat(m_nPattern))
{
return false;
}
const ModCommand &selStart = *sndFile->Patterns[m_nPattern].GetpModCommand(m_Selection.GetStartRow(), m_Selection.GetStartChannel());
if(!selStart.IsPcNote())
{
return false;
}
CString s;
// Create sub menu for "change plugin"
HMENU pluginChangeMenu = ::CreatePopupMenu();
AppendMenu(hMenu, MF_POPUP, reinterpret_cast<UINT_PTR>(pluginChangeMenu), ih->GetKeyTextFromCommand(kcPatternSetInstrument, _T("Change Plugin")));
for(PLUGINDEX nPlg = 0; nPlg < MAX_MIXPLUGINS; nPlg++)
{
if(sndFile->m_MixPlugins[nPlg].pMixPlugin != nullptr)
{
s = MPT_CFORMAT("{}: {}")(mpt::cfmt::dec0<2>(nPlg + 1), mpt::ToCString(sndFile->m_MixPlugins[nPlg].GetName()));
AppendMenu(pluginChangeMenu, MF_STRING | (((nPlg + 1) == selStart.instr) ? MF_CHECKED : 0), ID_CHANGE_INSTRUMENT + nPlg + 1, s);
}
}
if(selStart.instr >= 1 && selStart.instr <= MAX_MIXPLUGINS)
{
const SNDMIXPLUGIN &plug = sndFile->m_MixPlugins[selStart.instr - 1];
if(plug.pMixPlugin != nullptr)
{
// Create sub menu for "change plugin param"
HMENU paramChangeMenu = ::CreatePopupMenu();
AppendMenu(hMenu, MF_POPUP, reinterpret_cast<UINT_PTR>(paramChangeMenu), _T("Change Plugin Parameter\t"));
const PlugParamIndex curParam = selStart.GetValueVolCol(), nParams = plug.pMixPlugin->GetNumParameters();
for(PlugParamIndex i = 0; i < nParams; i++)
{
AppendMenu(paramChangeMenu, MF_STRING | ((i == curParam) ? MF_CHECKED : 0), ID_CHANGE_PCNOTE_PARAM + i, plug.pMixPlugin->GetFormattedParamName(i));
}
}
}
return BuildTogglePlugEditorCtxMenu(hMenu, ih);
}
bool CViewPattern::BuildTogglePlugEditorCtxMenu(HMENU hMenu, CInputHandler *ih) const
{
const CSoundFile *sndFile = GetSoundFile();
if(sndFile == nullptr || !sndFile->Patterns.IsValidPat(m_nPattern))
{
return false;
}
PLUGINDEX plug = 0;
const ModCommand &selStart = *sndFile->Patterns[m_nPattern].GetpModCommand(m_Selection.GetStartRow(), m_Selection.GetStartChannel());
if(selStart.IsPcNote())
{
// PC Event
plug = selStart.instr;
} else if(selStart.instr > 0 && selStart.instr <= sndFile->GetNumInstruments()
&& sndFile->Instruments[selStart.instr] != nullptr
&& sndFile->Instruments[selStart.instr]->nMixPlug)
{
// Regular instrument
plug = sndFile->Instruments[selStart.instr]->nMixPlug;
}
if(plug && plug <= MAX_MIXPLUGINS && sndFile->m_MixPlugins[plug - 1].pMixPlugin != nullptr)
{
AppendMenu(hMenu, MF_STRING, ID_PATTERN_EDIT_PCNOTE_PLUGIN, ih->GetKeyTextFromCommand(kcPatternEditPCNotePlugin, _T("Toggle Plugin &Editor")));
return true;
}
return false;
}
// Returns an ordered list of all channels in which a given column type is selected.
CHANNELINDEX CViewPattern::ListChansWhereColSelected(PatternCursor::Columns colType, std::vector<CHANNELINDEX> &chans) const
{
CHANNELINDEX startChan = m_Selection.GetStartChannel();
CHANNELINDEX endChan = m_Selection.GetEndChannel();
chans.clear();
chans.reserve(endChan - startChan + 1);
// Check in which channels this column is selected.
// Actually this check is only important for the first and last channel, but to keep things clean and simple, all channels are checked in the same manner.
for(CHANNELINDEX i = startChan; i <= endChan; i++)
{
if(m_Selection.ContainsHorizontal(PatternCursor(0, i, colType)))
{
chans.push_back(i);
}
}
return static_cast<CHANNELINDEX>(chans.size());
}
// Check if a column type is selected on any channel in the current selection.
bool CViewPattern::IsColumnSelected(PatternCursor::Columns colType) const
{
return m_Selection.ContainsHorizontal(PatternCursor(0, m_Selection.GetStartChannel(), colType))
|| m_Selection.ContainsHorizontal(PatternCursor(0, m_Selection.GetEndChannel(), colType));
}
// Check if the given interpolation type is actually possible in the current selection.
bool CViewPattern::IsInterpolationPossible(PatternCursor::Columns colType) const
{
std::vector<CHANNELINDEX> validChans;
ListChansWhereColSelected(colType, validChans);
ROWINDEX startRow = m_Selection.GetStartRow();
ROWINDEX endRow = m_Selection.GetEndRow();
for(auto chn : validChans)
{
if(IsInterpolationPossible(startRow, endRow, chn, colType))
{
return true;
}
}
return false;
}
// Check if the given interpolation type is actually possible in a given channel.
bool CViewPattern::IsInterpolationPossible(ROWINDEX startRow, ROWINDEX endRow, CHANNELINDEX chan, PatternCursor::Columns colType) const
{
const CSoundFile *sndFile = GetSoundFile();
if(startRow == endRow || sndFile == nullptr || !sndFile->Patterns.IsValidPat(m_nPattern))
return false;
bool result = false;
const ModCommand &startRowMC = *sndFile->Patterns[m_nPattern].GetpModCommand(startRow, chan);
const ModCommand &endRowMC = *sndFile->Patterns[m_nPattern].GetpModCommand(endRow, chan);
UINT startRowCmd, endRowCmd;
if(colType == PatternCursor::effectColumn && (startRowMC.IsPcNote() || endRowMC.IsPcNote()))
return true;
switch(colType)
{
case PatternCursor::noteColumn:
startRowCmd = startRowMC.note;
endRowCmd = endRowMC.note;
result = (startRowCmd == endRowCmd && startRowCmd != NOTE_NONE) // Interpolate between two identical notes or Cut / Fade / etc...
|| (startRowCmd != NOTE_NONE && endRowCmd == NOTE_NONE) // Fill in values from the first row
|| (startRowCmd == NOTE_NONE && endRowCmd != NOTE_NONE) // Fill in values from the last row
|| (ModCommand::IsNoteOrEmpty(startRowMC.note) && ModCommand::IsNoteOrEmpty(endRowMC.note) && !(startRowCmd == NOTE_NONE && endRowCmd == NOTE_NONE)); // Interpolate between two notes of which one may be empty
break;
case PatternCursor::instrColumn:
startRowCmd = startRowMC.instr;
endRowCmd = endRowMC.instr;
result = startRowCmd != 0 || endRowCmd != 0;
break;
case PatternCursor::volumeColumn:
startRowCmd = startRowMC.volcmd;
endRowCmd = endRowMC.volcmd;
result = (startRowCmd == endRowCmd && startRowCmd != VOLCMD_NONE) // Interpolate between two identical commands
|| (startRowCmd != VOLCMD_NONE && endRowCmd == VOLCMD_NONE) // Fill in values from the first row
|| (startRowCmd == VOLCMD_NONE && endRowCmd != VOLCMD_NONE); // Fill in values from the last row
break;
case PatternCursor::effectColumn:
case PatternCursor::paramColumn:
startRowCmd = startRowMC.command;
endRowCmd = endRowMC.command;
result = (startRowCmd == endRowCmd && startRowCmd != CMD_NONE) // Interpolate between two identical commands
|| (startRowCmd != CMD_NONE && endRowCmd == CMD_NONE) // Fill in values from the first row
|| (startRowCmd == CMD_NONE && endRowCmd != CMD_NONE); // Fill in values from the last row
break;
default:
result = false;
}
return result;
}
void CViewPattern::OnRButtonDblClk(UINT nFlags, CPoint point)
{
OnRButtonDown(nFlags, point);
CModScrollView::OnRButtonDblClk(nFlags, point);
}
// Toggle pending mute status for channel from context menu.
void CViewPattern::OnTogglePendingMuteFromClick()
{
TogglePendingMute(m_MenuCursor.GetChannel());
}
// Toggle pending solo status for channel from context menu.
void CViewPattern::OnPendingSoloChnFromClick()
{
PendingSoloChn(m_MenuCursor.GetChannel());
}
// Set pending unmute status for all channels.
void CViewPattern::OnPendingUnmuteAllChnFromClick()
{
CSoundFile *pSndFile = GetSoundFile();
if(pSndFile != nullptr)
{
GetSoundFile()->PatternTransitionChnUnmuteAll();
InvalidateChannelsHeaders();
}
}
// Toggle pending solo status for a channel.
void CViewPattern::PendingSoloChn(CHANNELINDEX nChn)
{
CSoundFile *pSndFile = GetSoundFile();
if(pSndFile != nullptr)
{
GetSoundFile()->PatternTranstionChnSolo(nChn);
InvalidateChannelsHeaders();
}
}
// Toggle pending mute status for a channel.
void CViewPattern::TogglePendingMute(CHANNELINDEX nChn)
{
CSoundFile *pSndFile = GetSoundFile();
if(pSndFile != nullptr)
{
pSndFile->m_bChannelMuteTogglePending[nChn] = !pSndFile->m_bChannelMuteTogglePending[nChn];
InvalidateChannelsHeaders();
}
}
// Check if editing is enabled, and if it's not, prompt the user to enable editing.
bool CViewPattern::IsEditingEnabled_bmsg()
{
if(IsEditingEnabled())
return true;
if(TrackerSettings::Instance().patternNoEditPopup)
return false;
HMENU hMenu;
if((hMenu = ::CreatePopupMenu()) == nullptr)
return false;
CPoint pt = GetPointFromPosition(m_Cursor);
// We add an mnemonic for an unbreakable space to avoid activating edit mode accidentally.
AppendMenuW(hMenu, MF_STRING, IDC_PATTERN_RECORD, L"Editing (recording) is disabled;&\u00A0 click here to enable it.");
ClientToScreen(&pt);
::TrackPopupMenu(hMenu, TPM_LEFTALIGN, pt.x, pt.y, 0, m_hWnd, NULL);
::DestroyMenu(hMenu);
return false;
}
// Show playback time at a given pattern position.
void CViewPattern::OnShowTimeAtRow()
{
CSoundFile *pSndFile = GetSoundFile();
if(pSndFile == nullptr)
{
return;
}
CString msg;
const auto &order = Order();
ORDERINDEX currentOrder = GetCurrentOrder();
if(currentOrder < order.size() && order[currentOrder] == m_nPattern)
{
const double t = pSndFile->GetPlaybackTimeAt(currentOrder, GetCurrentRow(), false, false);
if(t < 0)
msg.Format(_T("Unable to determine the time. Possible cause: No order %d, row %u found in play sequence."), currentOrder, GetCurrentRow());
else
{
const uint32 minutes = static_cast<uint32>(t / 60.0);
const double seconds = t - (minutes * 60);
msg.Format(_T("Estimate for playback time at order %d (pattern %d), row %u: %u minute%s %.2f seconds."), currentOrder, m_nPattern, GetCurrentRow(), minutes, (minutes == 1) ? _T("") : _T("s"), seconds);
}
} else
{
msg.Format(_T("Unable to determine the time: pattern at current order (%d) does not correspond to pattern in pattern view (pattern %d)."), currentOrder, m_nPattern);
}
Reporting::Notification(msg);
}
// Set up split keyboard
void CViewPattern::SetSplitKeyboardSettings()
{
CModDoc *pModDoc = GetDocument();
if(pModDoc == nullptr)
return;
CSplitKeyboardSettings dlg(CMainFrame::GetMainFrame(), pModDoc->GetSoundFile(), pModDoc->GetSplitKeyboardSettings());
if(dlg.DoModal() == IDOK)
{
// Update split keyboard settings in other pattern views
pModDoc->UpdateAllViews(NULL, SampleHint().Names());
}
}
// Paste pattern data using the given paste mode.
void CViewPattern::ExecutePaste(PatternClipboard::PasteModes mode)
{
if(IsEditingEnabled_bmsg() && PastePattern(m_nPattern, m_Selection.GetUpperLeft(), mode))
{
InvalidatePattern(false);
SetFocus();
}
}
// Show plugin editor for plugin assigned to PC Event at the cursor position.
void CViewPattern::OnTogglePCNotePluginEditor()
{
CModDoc *pModDoc = GetDocument();
if(pModDoc == nullptr)
return;
CSoundFile &sndFile = pModDoc->GetSoundFile();
if(!sndFile.Patterns.IsValidPat(m_nPattern))
return;
const ModCommand &m = *sndFile.Patterns[m_nPattern].GetpModCommand(m_Selection.GetStartRow(), m_Selection.GetStartChannel());
PLUGINDEX plug = 0;
if(!m.IsPcNote())
{
// No PC note: Toggle instrument's plugin editor
if(m.instr && m.instr <= sndFile.GetNumInstruments() && sndFile.Instruments[m.instr])
{
plug = sndFile.Instruments[m.instr]->nMixPlug;
}
} else
{
plug = m.instr;
}
if(plug > 0 && plug <= MAX_MIXPLUGINS)
pModDoc->TogglePluginEditor(plug - 1);
}
// Get the active pattern's rows per beat, or, if they are not overriden, the song's default rows per beat.
ROWINDEX CViewPattern::GetRowsPerBeat() const
{
const CSoundFile *pSndFile = GetSoundFile();
if(pSndFile == nullptr || !pSndFile->Patterns.IsValidPat(m_nPattern))
return 0;
if(!pSndFile->Patterns[m_nPattern].GetOverrideSignature())
return pSndFile->m_nDefaultRowsPerBeat;
else
return pSndFile->Patterns[m_nPattern].GetRowsPerBeat();
}
// Get the active pattern's rows per measure, or, if they are not overriden, the song's default rows per measure.
ROWINDEX CViewPattern::GetRowsPerMeasure() const
{
const CSoundFile *pSndFile = GetSoundFile();
if(pSndFile == nullptr || !pSndFile->Patterns.IsValidPat(m_nPattern))
return 0;
if(!pSndFile->Patterns[m_nPattern].GetOverrideSignature())
return pSndFile->m_nDefaultRowsPerMeasure;
else
return pSndFile->Patterns[m_nPattern].GetRowsPerMeasure();
}
// Set instrument
void CViewPattern::SetSelectionInstrument(const INSTRUMENTINDEX instr, bool setEmptyInstrument)
{
CSoundFile *pSndFile = GetSoundFile();
if(pSndFile == nullptr || !pSndFile->Patterns.IsValidPat(m_nPattern))
{
return;
}
BeginWaitCursor();
PrepareUndo(m_Selection, "Set Instrument");
bool modified = false;
ApplyToSelection([instr, setEmptyInstrument, &modified] (ModCommand &m, ROWINDEX, CHANNELINDEX)
{
// If a note or an instr is present on the row, do the change, if required.
// Do not set instr if note and instr are both blank,
// but set instr if note is a PC note and instr is blank.
if(((setEmptyInstrument && (m.IsNote() || m.IsPcNote())) || m.instr != 0)
&& (m.instr != instr))
{
m.instr = static_cast<ModCommand::INSTR>(instr);
modified = true;
}
});
if(modified)
{
SetModified();
InvalidatePattern();
}
EndWaitCursor();
}
// Select a whole beat (selectBeat = true) or measure.
void CViewPattern::SelectBeatOrMeasure(bool selectBeat)
{
const ROWINDEX adjust = selectBeat ? GetRowsPerBeat() : GetRowsPerMeasure();
// Snap to start of beat / measure of upper-left corner of current selection
const ROWINDEX startRow = m_Selection.GetStartRow() - (m_Selection.GetStartRow() % adjust);
// Snap to end of beat / measure of lower-right corner of current selection
const ROWINDEX endRow = m_Selection.GetEndRow() + adjust - (m_Selection.GetEndRow() % adjust) - 1;
CHANNELINDEX startChannel = m_Selection.GetStartChannel(), endChannel = m_Selection.GetEndChannel();
PatternCursor::Columns startColumn = PatternCursor::firstColumn, endColumn = PatternCursor::firstColumn;
if(m_Selection.GetUpperLeft() == m_Selection.GetLowerRight())
{
// No selection has been made yet => expand selection to whole channel.
endColumn = PatternCursor::lastColumn; // Extend to param column
} else if(startRow == m_Selection.GetStartRow() && endRow == m_Selection.GetEndRow())
{
// Whole beat or measure is already selected
if(m_Selection.GetStartColumn() == PatternCursor::firstColumn && m_Selection.GetEndColumn() == PatternCursor::lastColumn)
{
// Whole channel is already selected => expand selection to whole row.
startChannel = 0;
startColumn = PatternCursor::firstColumn;
endChannel = MAX_BASECHANNELS;
endColumn = PatternCursor::lastColumn;
} else
{
// Channel is only partly selected => expand to whole channel first.
endColumn = PatternCursor::lastColumn; // Extend to param column
}
} else
{
// Some arbitrary selection: Remember start / end column
startColumn = m_Selection.GetStartColumn();
endColumn = m_Selection.GetEndColumn();
}
SetCurSel(PatternCursor(startRow, startChannel, startColumn), PatternCursor(endRow, endChannel, endColumn));
}
// Sweep pattern channel to find instrument number to use
void CViewPattern::FindInstrument()
{
const CSoundFile *sndFile = GetSoundFile();
if(sndFile == nullptr)
{
return;
}
const auto &order = Order();
ORDERINDEX ord = GetCurrentOrder();
PATTERNINDEX pat = m_nPattern;
ROWINDEX row = m_Cursor.GetRow();
while(sndFile->Patterns.IsValidPat(pat))
{
// Seek upwards
do
{
auto &m = *sndFile->Patterns[pat].GetpModCommand(row, m_Cursor.GetChannel());
if(!m.IsPcNote() && m.instr != 0)
{
SendCtrlMessage(CTRLMSG_SETCURRENTINSTRUMENT, m.instr);
static_cast<CModControlView *>(CWnd::FromHandle(m_hWndCtrl))->InstrumentChanged(m.instr);
return;
}
} while(row-- != 0);
// Try previous pattern
if(ord == 0)
{
return;
}
ord = order.GetPreviousOrderIgnoringSkips(ord);
pat = order[ord];
if(!sndFile->Patterns.IsValidPat(pat))
{
return;
}
row = sndFile->Patterns[pat].GetNumRows() - 1;
}
}
// Find previous or next column entry (note, instrument, ...) on this channel
void CViewPattern::JumpToPrevOrNextEntry(bool nextEntry, bool select)
{
const CSoundFile *sndFile = GetSoundFile();
if(sndFile == nullptr || GetCurrentOrder() >= Order().size())
{
return;
}
const auto &order = Order();
ORDERINDEX ord = GetCurrentOrder();
PATTERNINDEX pat = m_nPattern;
CHANNELINDEX chn = m_Cursor.GetChannel();
PatternCursor::Columns column = m_Cursor.GetColumnType();
int32 row = m_Cursor.GetRow();
int direction = nextEntry ? 1 : -1;
row += direction; // Don't want to find the cell we're already in
while(sndFile->Patterns.IsValidPat(pat))
{
while(sndFile->Patterns[pat].IsValidRow(row))
{
auto &m = *sndFile->Patterns[pat].GetpModCommand(row, chn);
bool found;
switch(column)
{
case PatternCursor::noteColumn:
found = m.note != NOTE_NONE;
break;
case PatternCursor::instrColumn:
found = m.instr != 0;
break;
case PatternCursor::volumeColumn:
found = m.volcmd != VOLCMD_NONE;
break;
case PatternCursor::effectColumn:
case PatternCursor::paramColumn:
found = m.command != CMD_NONE;
break;
default:
found = false;
}
if(found)
{
if(select)
{
CursorJump(static_cast<int>(row) - m_Cursor.GetRow(), false);
} else
{
SetCurrentOrder(ord);
SetCurrentPattern(pat, row);
if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_PLAYNAVIGATEROW)
{
PatternStep(row);
}
}
return;
}
row += direction;
}
// Continue search in prev/next pattern (unless we also select - selections cannot span multiple patterns)
if(select)
return;
ORDERINDEX nextOrd = nextEntry ? order.GetNextOrderIgnoringSkips(ord) : order.GetPreviousOrderIgnoringSkips(ord);
pat = order[nextOrd];
if(nextOrd == ord || !sndFile->Patterns.IsValidPat(pat))
return;
ord = nextOrd;
row = nextEntry ? 0 : (sndFile->Patterns[pat].GetNumRows() - 1);
}
}
// Copy to clipboard
bool CViewPattern::CopyPattern(PATTERNINDEX nPattern, const PatternRect &selection)
{
BeginWaitCursor();
bool result = PatternClipboard::Copy(*GetSoundFile(), nPattern, selection);
EndWaitCursor();
PatternClipboardDialog::UpdateList();
return result;
}
// Paste from clipboard
bool CViewPattern::PastePattern(PATTERNINDEX nPattern, const PatternCursor &pastePos, PatternClipboard::PasteModes mode)
{
BeginWaitCursor();
PatternEditPos pos;
pos.pattern = nPattern;
pos.row = pastePos.GetRow();
pos.channel = pastePos.GetChannel();
pos.order = GetCurrentOrder();
PatternRect rect;
const bool patternExisted = GetSoundFile()->Patterns.IsValidPat(nPattern);
bool orderChanged = false;
bool result = PatternClipboard::Paste(*GetSoundFile(), pos, mode, rect, orderChanged);
EndWaitCursor();
PatternHint updateHint = PatternHint(PATTERNINDEX_INVALID).Data();
if(pos.pattern != nPattern)
{
// Multipaste: Switch to pasted pattern.
SetCurrentPattern(pos.pattern);
SetCurrentOrder(pos.order);
}
if(orderChanged || (patternExisted != GetSoundFile()->Patterns.IsValidPat(nPattern)))
{
updateHint.Names();
GetDocument()->UpdateAllViews(nullptr, SequenceHint(GetSoundFile()->Order.GetCurrentSequenceIndex()).Data(), nullptr);
}
if(result)
{
SetCurSel(rect);
GetDocument()->SetModified();
GetDocument()->UpdateAllViews(nullptr, updateHint, nullptr);
}
return result;
}
template<typename Func>
void CViewPattern::ApplyToSelection(Func func)
{
CSoundFile *sndFile = GetSoundFile();
if(sndFile == nullptr || !sndFile->Patterns.IsValidPat(m_nPattern))
return;
auto &pattern = sndFile->Patterns[m_nPattern];
m_Selection.Sanitize(pattern.GetNumRows(), pattern.GetNumChannels());
const CHANNELINDEX startChn = m_Selection.GetStartChannel(), endChn = m_Selection.GetEndChannel();
const ROWINDEX endRow = m_Selection.GetEndRow();
for(ROWINDEX row = m_Selection.GetStartRow(); row <= endRow; row++)
{
ModCommand *m = pattern.GetpModCommand(row, startChn);
for(CHANNELINDEX chn = startChn; chn <= endChn; chn++, m++)
{
func(*m, row, chn);
}
}
}
INT_PTR CViewPattern::OnToolHitTest(CPoint point, TOOLINFO *pTI) const
{
CRect rect;
const auto item = GetDragItem(point, rect);
const auto value = item.Value();
const CSoundFile &sndFile = *GetSoundFile();
mpt::winstring text;
switch(item.Type())
{
case DragItem::PatternHeader:
{
text = _T("Show Pattern Properties");
auto keyText = CMainFrame::GetInputHandler()->m_activeCommandSet->GetKeyTextFromCommand(kcShowPatternProperties, 0);
if(!keyText.IsEmpty())
text += MPT_CFORMAT(" ({})")(keyText);
break;
}
case DragItem::ChannelHeader:
if(value < sndFile.GetNumChannels())
{
if(!sndFile.ChnSettings[value].szName.empty())
text = MPT_TFORMAT("{}: {}")(value + 1, mpt::ToWin(sndFile.GetCharsetInternal(), sndFile.ChnSettings[value].szName));
else
text = MPT_TFORMAT("Channel {}")(value + 1);
}
break;
case DragItem::PluginName:
if(value < sndFile.GetNumChannels())
{
PLUGINDEX mixPlug = sndFile.ChnSettings[value].nMixPlugin;
if(mixPlug && mixPlug <= MAX_MIXPLUGINS)
text = MPT_TFORMAT("{}: {}")(mixPlug, mpt::ToWin(sndFile.m_MixPlugins[mixPlug - 1].GetName()));
else
text = _T("No Plugin");
}
break;
}
if(text.empty())
return CScrollView::OnToolHitTest(point, pTI);
pTI->hwnd = m_hWnd;
pTI->uId = item.ToIntPtr();
pTI->rect = rect;
// MFC will free() the text
TCHAR *textP = static_cast<TCHAR *>(calloc(text.size() + 1, sizeof(TCHAR)));
std::copy(text.begin(), text.end(), textP);
pTI->lpszText = textP;
return item.ToIntPtr();
}
// Accessible description for screen readers
HRESULT CViewPattern::get_accName(VARIANT varChild, BSTR *pszName)
{
const ModCommand &m = GetCursorCommand();
const size_t columnIndex = m_Cursor.GetColumnType();
const TCHAR *column = _T("");
static constexpr const TCHAR *regularColumns[] = {_T("Note"), _T("Instrument"), _T("Volume"), _T("Effect"), _T("Parameter")};
static constexpr const TCHAR *pcColumns[] = {_T("Note"), _T("Plugin"), _T("Plugin Parameter"), _T("Parameter Value"), _T("Parameter Value")};
static_assert(PatternCursor::lastColumn + 1 == std::size(regularColumns));
static_assert(PatternCursor::lastColumn + 1 == std::size(pcColumns));
if(m.IsPcNote() && columnIndex < std::size(pcColumns))
column = pcColumns[columnIndex];
else if(!m.IsPcNote() && columnIndex < std::size(regularColumns))
column = regularColumns[columnIndex];
const CSoundFile *sndFile = GetSoundFile();
const CHANNELINDEX chn = m_Cursor.GetChannel();
const auto channelNumber = mpt::cfmt::val(chn + 1);
CString channelName = channelNumber;
if(chn < sndFile->GetNumChannels() && !sndFile->ChnSettings[chn].szName.empty())
channelName += _T(": ") + mpt::ToCString(sndFile->GetCharsetInternal(), sndFile->ChnSettings[chn].szName);
CString str = TrackerSettings::Instance().patternAccessibilityFormat;
str.Replace(_T("%sequence%"), mpt::cfmt::val(sndFile->Order.GetCurrentSequenceIndex()));
str.Replace(_T("%order%"), mpt::cfmt::val(GetCurrentOrder()));
str.Replace(_T("%pattern%"), mpt::cfmt::val(GetCurrentPattern()));
str.Replace(_T("%row%"), mpt::cfmt::val(m_Cursor.GetRow()));
str.Replace(_T("%channel%"), channelNumber);
str.Replace(_T("%column_type%"), column);
str.Replace(_T("%column_description%"), GetCursorDescription());
str.Replace(_T("%channel_name%"), channelName);
if(str.IsEmpty())
return CModScrollView::get_accName(varChild, pszName);
*pszName = str.AllocSysString();
return S_OK;
}
OPENMPT_NAMESPACE_END