winamp/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_seq.cpp
2024-09-24 14:54:57 +02:00

1676 lines
48 KiB
C++

/*
* Ctrl_seq.cpp
* ------------
* Purpose: Order list for the pattern editor upper panel.
* Notes : (currently none)
* Authors: Olivier Lapicque
* OpenMPT Devs
* The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
*/
#include "stdafx.h"
#include "Mainfrm.h"
#include "InputHandler.h"
#include "Moddoc.h"
#include "../soundlib/mod_specifications.h"
#include "Globals.h"
#include "Ctrl_pat.h"
#include "PatternClipboard.h"
#include "../common/mptStringBuffer.h"
OPENMPT_NAMESPACE_BEGIN
enum SequenceAction : SEQUENCEINDEX
{
kAddSequence = MAX_SEQUENCES,
kDuplicateSequence,
kDeleteSequence,
kSplitSequence,
kMaxSequenceActions
};
// Little helper function to avoid copypasta
static bool IsSelectionKeyPressed() { return CMainFrame::GetInputHandler()->SelectionPressed(); }
static bool IsCtrlKeyPressed() { return CMainFrame::GetInputHandler()->CtrlPressed(); }
//////////////////////////////////////////////////////////////
// CPatEdit
BOOL CPatEdit::PreTranslateMessage(MSG *pMsg)
{
if(((pMsg->message == WM_KEYDOWN) || (pMsg->message == WM_KEYUP)) && (pMsg->wParam == VK_TAB))
{
if((pMsg->message == WM_KEYUP) && (m_pParent))
{
m_pParent->SwitchToView();
}
return TRUE;
}
return CEdit::PreTranslateMessage(pMsg);
}
//////////////////////////////////////////////////////////////
// COrderList
BEGIN_MESSAGE_MAP(COrderList, CWnd)
//{{AFX_MSG_MAP(COrderList)
ON_WM_PAINT()
ON_WM_ERASEBKGND()
ON_WM_MOUSEMOVE()
ON_WM_LBUTTONDOWN()
ON_WM_LBUTTONDBLCLK()
ON_WM_LBUTTONUP()
ON_WM_RBUTTONDOWN()
ON_WM_MBUTTONDOWN()
ON_WM_SETFOCUS()
ON_WM_KILLFOCUS()
ON_WM_HSCROLL()
ON_WM_SIZE()
ON_COMMAND(ID_ORDERLIST_INSERT, &COrderList::OnInsertOrder)
ON_COMMAND(ID_ORDERLIST_INSERT_SEPARATOR, &COrderList::OnInsertSeparatorPattern)
ON_COMMAND(ID_ORDERLIST_DELETE, &COrderList::OnDeleteOrder)
ON_COMMAND(ID_ORDERLIST_RENDER, &COrderList::OnRenderOrder)
ON_COMMAND(ID_ORDERLIST_EDIT_COPY, &COrderList::OnEditCopy)
ON_COMMAND(ID_ORDERLIST_EDIT_CUT, &COrderList::OnEditCut)
ON_COMMAND(ID_ORDERLIST_EDIT_COPY_ORDERS, &COrderList::OnEditCopyOrders)
ON_COMMAND(ID_PATTERN_PROPERTIES, &COrderList::OnPatternProperties)
ON_COMMAND(ID_PLAYER_PLAY, &COrderList::OnPlayerPlay)
ON_COMMAND(ID_PLAYER_PAUSE, &COrderList::OnPlayerPause)
ON_COMMAND(ID_PLAYER_PLAYFROMSTART, &COrderList::OnPlayerPlayFromStart)
ON_COMMAND(IDC_PATTERN_PLAYFROMSTART, &COrderList::OnPatternPlayFromStart)
ON_COMMAND(ID_ORDERLIST_NEW, &COrderList::OnCreateNewPattern)
ON_COMMAND(ID_ORDERLIST_COPY, &COrderList::OnDuplicatePattern)
ON_COMMAND(ID_ORDERLIST_MERGE, &COrderList::OnMergePatterns)
ON_COMMAND(ID_PATTERNCOPY, &COrderList::OnPatternCopy)
ON_COMMAND(ID_PATTERNPASTE, &COrderList::OnPatternPaste)
ON_COMMAND(ID_SETRESTARTPOS, &COrderList::OnSetRestartPos)
ON_COMMAND(ID_ORDERLIST_LOCKPLAYBACK, &COrderList::OnLockPlayback)
ON_COMMAND(ID_ORDERLIST_UNLOCKPLAYBACK, &COrderList::OnUnlockPlayback)
ON_COMMAND_RANGE(ID_SEQUENCE_ITEM, ID_SEQUENCE_ITEM + kMaxSequenceActions - 1, &COrderList::OnSelectSequence)
ON_MESSAGE(WM_MOD_DRAGONDROPPING, &COrderList::OnDragonDropping)
ON_MESSAGE(WM_HELPHITTEST, &COrderList::OnHelpHitTest)
ON_MESSAGE(WM_MOD_KEYCOMMAND, &COrderList::OnCustomKeyMsg)
ON_NOTIFY_EX(TTN_NEEDTEXT, 0, &COrderList::OnToolTipText)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
COrderList::COrderList(CCtrlPatterns &parent, CModDoc &document)
: m_nOrderlistMargins(TrackerSettings::Instance().orderlistMargins)
, m_modDoc(document)
, m_pParent(parent)
{
EnableActiveAccessibility();
}
bool COrderList::EnsureEditable(ORDERINDEX ord)
{
auto &sndFile = m_modDoc.GetSoundFile();
if(ord >= Order().size())
{
if(ord < sndFile.GetModSpecifications().ordersMax)
{
try
{
Order().resize(ord + 1);
} catch(mpt::out_of_memory e)
{
mpt::delete_out_of_memory(e);
return false;
}
} else
{
return false;
}
}
return true;
}
ModSequence &COrderList::Order() { return m_modDoc.GetSoundFile().Order(); }
const ModSequence &COrderList::Order() const { return m_modDoc.GetSoundFile().Order(); }
void COrderList::SetScrollPos(int pos)
{
// Work around 16-bit limitations of WM_HSCROLL
SCROLLINFO si;
MemsetZero(si);
si.cbSize = sizeof(si);
si.fMask = SIF_TRACKPOS;
GetScrollInfo(SB_HORZ, &si);
si.nPos = pos;
SetScrollInfo(SB_HORZ, &si);
}
int COrderList::GetScrollPos(bool getTrackPos)
{
// Work around 16-bit limitations of WM_HSCROLL
SCROLLINFO si;
MemsetZero(si);
si.cbSize = sizeof(si);
si.fMask = SIF_TRACKPOS;
GetScrollInfo(SB_HORZ, &si);
return getTrackPos ? si.nTrackPos : si.nPos;
}
bool COrderList::IsOrderInMargins(int order, int startOrder)
{
const ORDERINDEX nMargins = GetMargins();
return ((startOrder != 0 && order - startOrder < nMargins) ||
order - startOrder >= GetLength() - nMargins);
}
void COrderList::EnsureVisible(ORDERINDEX order)
{
// nothing needs to be done
if(!IsOrderInMargins(order, m_nXScroll) || order == ORDERINDEX_INVALID)
return;
if(order < m_nXScroll)
{
if(order < GetMargins())
m_nXScroll = 0;
else
m_nXScroll = order - GetMargins();
} else
{
m_nXScroll = order + 2 * GetMargins() - 1;
if(m_nXScroll < GetLength())
m_nXScroll = 0;
else
m_nXScroll -= GetLength();
}
}
bool COrderList::IsPlaying() const
{
return (CMainFrame::GetMainFrame()->GetModPlaying() == &m_modDoc);
}
ORDERINDEX COrderList::GetOrderFromPoint(const CPoint &pt) const
{
if(m_cxFont)
return mpt::saturate_cast<ORDERINDEX>(m_nXScroll + pt.x / m_cxFont);
return 0;
}
CRect COrderList::GetRectFromOrder(ORDERINDEX ord) const
{
return CRect{CPoint{(ord - m_nXScroll) * m_cxFont, 0}, CSize{m_cxFont, m_cyFont}};
}
BOOL COrderList::Init(const CRect &rect, HFONT hFont)
{
CreateEx(WS_EX_STATICEDGE, NULL, _T(""), WS_CHILD | WS_VISIBLE, rect, &m_pParent, IDC_ORDERLIST);
m_hFont = hFont;
SendMessage(WM_SETFONT, (WPARAM)m_hFont);
SetScrollPos(0);
EnableScrollBarCtrl(SB_HORZ, TRUE);
SetCurSel(0);
EnableToolTips();
return TRUE;
}
void COrderList::UpdateScrollInfo()
{
CRect rcClient;
GetClientRect(&rcClient);
if((m_cxFont > 0) && (rcClient.right > 0))
{
CRect rect;
SCROLLINFO info;
UINT nPage;
int nMax = Order().GetLengthTailTrimmed();
GetScrollInfo(SB_HORZ, &info, SIF_PAGE | SIF_RANGE);
info.fMask = SIF_PAGE | SIF_RANGE;
info.nMin = 0;
nPage = rcClient.right / m_cxFont;
if(nMax <= (int)nPage)
nMax = nPage + 1;
if((nMax != info.nMax) || (nPage != info.nPage))
{
info.nPage = nPage;
info.nMax = nMax;
SetScrollInfo(SB_HORZ, &info, TRUE);
}
}
}
int COrderList::GetFontWidth()
{
if((m_cxFont <= 0) && (m_hWnd) && (m_hFont))
{
CClientDC dc(this);
HGDIOBJ oldfont = dc.SelectObject(m_hFont);
CSize sz = dc.GetTextExtent(_T("000+"), 4);
if(oldfont)
dc.SelectObject(oldfont);
return sz.cx;
}
return m_cxFont;
}
void COrderList::InvalidateSelection()
{
ORDERINDEX ordLo = m_nScrollPos, count = 1;
static ORDERINDEX m_nScrollPos2Old = m_nScrollPos2nd;
if(m_nScrollPos2Old != ORDERINDEX_INVALID)
{
// there were multiple orders selected - remove them all
ORDERINDEX ordHi = m_nScrollPos;
if(m_nScrollPos2Old < m_nScrollPos)
ordLo = m_nScrollPos2Old;
else
ordHi = m_nScrollPos2Old;
count = ordHi - ordLo + 1;
}
m_nScrollPos2Old = m_nScrollPos2nd;
CRect rcClient, rect;
GetClientRect(&rcClient);
rect.left = rcClient.left + (ordLo - m_nXScroll) * m_cxFont;
rect.top = rcClient.top;
rect.right = rect.left + m_cxFont * count;
rect.bottom = rcClient.bottom;
rect &= rcClient;
if(rect.right > rect.left)
InvalidateRect(rect, FALSE);
if(m_playPos != ORDERINDEX_INVALID)
{
rect.left = rcClient.left + (m_playPos - m_nXScroll) * m_cxFont;
rect.top = rcClient.top;
rect.right = rect.left + m_cxFont;
rect &= rcClient;
if(rect.right > rect.left)
InvalidateRect(rect, FALSE);
m_playPos = ORDERINDEX_INVALID;
}
}
ORDERINDEX COrderList::GetLength()
{
CRect rcClient;
GetClientRect(&rcClient);
if(m_cxFont > 0)
return mpt::saturate_cast<ORDERINDEX>(rcClient.right / m_cxFont);
else
{
const int fontWidth = GetFontWidth();
return (fontWidth > 0) ? mpt::saturate_cast<ORDERINDEX>(rcClient.right / fontWidth) : 0;
}
}
OrdSelection COrderList::GetCurSel(bool ignoreSelection) const
{
// returns the currently selected order(s)
OrdSelection result;
result.firstOrd = result.lastOrd = m_nScrollPos;
// ignoreSelection: true if only first selection marker is important.
if(!ignoreSelection && m_nScrollPos2nd != ORDERINDEX_INVALID)
{
if(m_nScrollPos2nd < m_nScrollPos) // ord2 < ord1
result.firstOrd = m_nScrollPos2nd;
else
result.lastOrd = m_nScrollPos2nd;
}
ORDERINDEX lastIndex = std::max(Order().GetLengthTailTrimmed(), m_modDoc.GetSoundFile().GetModSpecifications().ordersMax) - 1u;
LimitMax(result.firstOrd, lastIndex);
LimitMax(result.lastOrd, lastIndex);
return result;
}
void COrderList::SetSelection(ORDERINDEX firstOrd, ORDERINDEX lastOrd)
{
SetCurSel(firstOrd, true, false, true);
SetCurSel(lastOrd != ORDERINDEX_INVALID ? lastOrd : firstOrd, false, true, true);
}
bool COrderList::SetCurSel(ORDERINDEX sel, bool setPlayPos, bool shiftClick, bool ignoreCurSel)
{
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
CSoundFile &sndFile = m_modDoc.GetSoundFile();
ORDERINDEX &ord = shiftClick ? m_nScrollPos2nd : m_nScrollPos;
const ORDERINDEX lastIndex = std::max(Order().GetLength(), sndFile.GetModSpecifications().ordersMax) - 1u;
if((sel < 0) || (sel > lastIndex) || (!m_pParent) || (!pMainFrm))
return false;
if(!ignoreCurSel && sel == ord && (sel == sndFile.m_PlayState.m_nCurrentOrder))
return true;
const ORDERINDEX shownLength = GetLength();
InvalidateSelection();
ord = sel;
if(!EnsureEditable(ord))
return false;
if(!m_bScrolling)
{
const ORDERINDEX margins = GetMargins(GetMarginsMax(shownLength));
if(ord < m_nXScroll + margins)
{
// Must move first shown sequence item to left in order to show the new active order.
m_nXScroll = (ord > margins) ? (ord - margins) : 0;
SetScrollPos(m_nXScroll);
Invalidate(FALSE);
} else
{
ORDERINDEX maxsel = shownLength;
if(maxsel)
maxsel--;
if(ord - m_nXScroll >= maxsel - margins)
{
// Must move first shown sequence item to right in order to show the new active order.
m_nXScroll = ord - (maxsel - margins);
SetScrollPos(m_nXScroll);
Invalidate(FALSE);
}
}
}
InvalidateSelection();
PATTERNINDEX n = Order()[m_nScrollPos];
if(setPlayPos && !shiftClick && sndFile.Patterns.IsValidPat(n))
{
const bool isPlaying = IsPlaying();
bool changedPos = false;
if(isPlaying && sndFile.m_SongFlags[SONG_PATTERNLOOP])
{
pMainFrm->ResetNotificationBuffer();
// Update channel parameters and play time
CriticalSection cs;
m_modDoc.SetElapsedTime(m_nScrollPos, 0, !sndFile.m_SongFlags[SONG_PAUSED | SONG_STEP]);
changedPos = true;
} else if(m_pParent.GetFollowSong())
{
FlagSet<SongFlags> pausedFlags = sndFile.m_SongFlags & (SONG_PAUSED | SONG_STEP | SONG_PATTERNLOOP);
// Update channel parameters and play time
CriticalSection cs;
sndFile.SetCurrentOrder(m_nScrollPos);
m_modDoc.SetElapsedTime(m_nScrollPos, 0, !sndFile.m_SongFlags[SONG_PAUSED | SONG_STEP]);
sndFile.m_SongFlags.set(pausedFlags);
if(isPlaying)
pMainFrm->ResetNotificationBuffer();
changedPos = true;
}
if(changedPos && Order().IsPositionLocked(m_nScrollPos))
{
// Users wants to go somewhere else, so let them do that.
OnUnlockPlayback();
}
m_pParent.SetCurrentPattern(n);
} else if(setPlayPos && !shiftClick && n != Order().GetIgnoreIndex() && n != Order().GetInvalidPatIndex())
{
m_pParent.SetCurrentPattern(n);
}
UpdateInfoText();
if(m_nScrollPos == m_nScrollPos2nd)
m_nScrollPos2nd = ORDERINDEX_INVALID;
return true;
}
PATTERNINDEX COrderList::GetCurrentPattern() const
{
const ModSequence &order = Order();
if(m_nScrollPos < order.size())
{
return order[m_nScrollPos];
}
return 0;
}
BOOL COrderList::PreTranslateMessage(MSG *pMsg)
{
//handle Patterns View context keys that we want to take effect in the orderlist.
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 = (UINT)pMsg->wParam;
UINT nRepCnt = LOWORD(pMsg->lParam);
UINT nFlags = HIWORD(pMsg->lParam);
KeyEventType kT = ih->GetKeyEventType(nFlags);
if(ih->KeyEvent(kCtxCtrlOrderlist, nChar, nRepCnt, nFlags, kT) != kcNull)
return true; // Mapped to a command, no need to pass message on.
//HACK: masquerade as kCtxViewPatternsNote context until we implement appropriate
// command propagation to kCtxCtrlOrderlist context.
if(ih->KeyEvent(kCtxViewPatternsNote, nChar, nRepCnt, nFlags, kT) != kcNull)
return true; // Mapped to a command, no need to pass message on.
// Handle Application (menu) key
if(pMsg->message == WM_KEYDOWN && nChar == VK_APPS)
{
const auto selection = GetCurSel();
auto pt = (GetRectFromOrder(selection.firstOrd) | GetRectFromOrder(selection.lastOrd)).CenterPoint();
CRect clientRect;
GetClientRect(clientRect);
if(!clientRect.PtInRect(pt))
pt = clientRect.CenterPoint();
OnRButtonDown(0, pt);
}
}
return CWnd::PreTranslateMessage(pMsg);
}
LRESULT COrderList::OnCustomKeyMsg(WPARAM wParam, LPARAM lParam)
{
bool isPlaying = IsPlaying();
switch(wParam)
{
case kcEditCopy:
OnEditCopy(); return wParam;
case kcEditCut:
OnEditCut(); return wParam;
case kcEditPaste:
OnPatternPaste(); return wParam;
case kcOrderlistEditCopyOrders:
OnEditCopyOrders(); return wParam;
// Orderlist navigation
case kcOrderlistNavigateLeftSelect:
case kcOrderlistNavigateLeft:
SetCurSelTo2ndSel(wParam == kcOrderlistNavigateLeftSelect); SetCurSel(m_nScrollPos - 1, wParam == kcOrderlistNavigateLeft || !isPlaying); return wParam;
case kcOrderlistNavigateRightSelect:
case kcOrderlistNavigateRight:
SetCurSelTo2ndSel(wParam == kcOrderlistNavigateRightSelect); SetCurSel(m_nScrollPos + 1, wParam == kcOrderlistNavigateRight || !isPlaying); return wParam;
case kcOrderlistNavigateFirstSelect:
case kcOrderlistNavigateFirst:
SetCurSelTo2ndSel(wParam == kcOrderlistNavigateFirstSelect); SetCurSel(0, wParam == kcOrderlistNavigateFirst || !isPlaying); return wParam;
case kcEditSelectAll:
SetCurSel(0, !isPlaying);
[[fallthrough]];
case kcOrderlistNavigateLastSelect:
case kcOrderlistNavigateLast:
{
SetCurSelTo2ndSel(wParam == kcOrderlistNavigateLastSelect || wParam == kcEditSelectAll);
ORDERINDEX nLast = Order().GetLengthTailTrimmed();
if(nLast > 0) nLast--;
SetCurSel(nLast, wParam == kcOrderlistNavigateLast || !isPlaying);
}
return wParam;
// Orderlist edit
case kcOrderlistEditDelete:
OnDeleteOrder(); return wParam;
case kcOrderlistEditInsert:
OnInsertOrder(); return wParam;
case kcOrderlistEditInsertSeparator:
OnInsertSeparatorPattern(); return wParam;
case kcOrderlistSwitchToPatternView:
OnSwitchToView(); return wParam;
case kcOrderlistEditPattern:
OnLButtonDblClk(0, CPoint(0, 0)); OnSwitchToView(); return wParam;
// Enter pattern number
case kcOrderlistPat0:
case kcOrderlistPat1:
case kcOrderlistPat2:
case kcOrderlistPat3:
case kcOrderlistPat4:
case kcOrderlistPat5:
case kcOrderlistPat6:
case kcOrderlistPat7:
case kcOrderlistPat8:
case kcOrderlistPat9:
EnterPatternNum(static_cast<UINT>(wParam) - kcOrderlistPat0); return wParam;
case kcOrderlistPatMinus:
EnterPatternNum(10); return wParam;
case kcOrderlistPatPlus:
EnterPatternNum(11); return wParam;
case kcOrderlistPatIgnore:
EnterPatternNum(12); return wParam;
case kcOrderlistPatInvalid:
EnterPatternNum(13); return wParam;
// kCtxViewPatternsNote messages
case kcSwitchToOrderList:
OnSwitchToView();
return wParam;
case kcChangeLoopStatus:
m_pParent.OnModCtrlMsg(CTRLMSG_PAT_LOOP, -1); return wParam;
case kcToggleFollowSong:
m_pParent.OnModCtrlMsg(CTRLMSG_PAT_FOLLOWSONG, 1); return wParam;
case kcChannelUnmuteAll:
case kcUnmuteAllChnOnPatTransition:
return m_pParent.SendMessage(WM_MOD_KEYCOMMAND, wParam, lParam);
case kcOrderlistLockPlayback:
OnLockPlayback(); return wParam;
case kcOrderlistUnlockPlayback:
OnUnlockPlayback(); return wParam;
case kcDuplicatePattern:
OnDuplicatePattern(); return wParam;
case kcMergePatterns:
OnMergePatterns(); return wParam;
case kcNewPattern:
OnCreateNewPattern(); return wParam;
}
return kcNull;
}
// Helper function to enter pattern index into the orderlist.
// Call with param 0...9 (enter digit), 10 (decrease) or 11 (increase).
void COrderList::EnterPatternNum(int enterNum)
{
CSoundFile &sndFile = m_modDoc.GetSoundFile();
if(!EnsureEditable(m_nScrollPos))
return;
PATTERNINDEX curIndex = Order()[m_nScrollPos];
const PATTERNINDEX maxIndex = std::max(PATTERNINDEX(1), sndFile.Patterns.GetNumPatterns()) - 1;
const PATTERNINDEX firstInvalid = sndFile.GetModSpecifications().hasIgnoreIndex ? sndFile.Order.GetIgnoreIndex() : sndFile.Order.GetInvalidPatIndex();
if(enterNum >= 0 && enterNum <= 9) // enter 0...9
{
if(curIndex >= sndFile.Patterns.Size())
curIndex = 0;
curIndex = curIndex * 10 + static_cast<PATTERNINDEX>(enterNum);
static_assert(MAX_PATTERNS < 10000);
if((curIndex >= 1000) && (curIndex > maxIndex)) curIndex %= 1000;
if((curIndex >= 100) && (curIndex > maxIndex)) curIndex %= 100;
if((curIndex >= 10) && (curIndex > maxIndex)) curIndex %= 10;
} else if(enterNum == 10) // decrease pattern index
{
if(curIndex == 0)
{
curIndex = sndFile.Order.GetInvalidPatIndex();
} else if(curIndex > maxIndex && curIndex <= firstInvalid)
{
curIndex = maxIndex;
} else
{
do
{
curIndex--;
} while(curIndex > 0 && curIndex < firstInvalid && !sndFile.Patterns.IsValidPat(curIndex));
}
} else if(enterNum == 11) // increase pattern index
{
if(curIndex >= sndFile.Order.GetInvalidPatIndex())
{
curIndex = 0;
} else if(curIndex >= maxIndex && curIndex < firstInvalid)
{
curIndex = firstInvalid;
} else
{
do
{
curIndex++;
} while(curIndex <= maxIndex && !sndFile.Patterns.IsValidPat(curIndex));
}
} else if(enterNum == 12) // ignore index (+++)
{
if(sndFile.GetModSpecifications().hasIgnoreIndex)
{
curIndex = sndFile.Order.GetIgnoreIndex();
}
} else if(enterNum == 13) // invalid index (---)
{
curIndex = sndFile.Order.GetInvalidPatIndex();
}
// apply
if(curIndex != Order()[m_nScrollPos])
{
Order()[m_nScrollPos] = curIndex;
m_modDoc.SetModified();
m_modDoc.UpdateAllViews(nullptr, SequenceHint().Data(), this);
InvalidateSelection();
m_pParent.SetCurrentPattern(curIndex);
}
}
void COrderList::OnEditCut()
{
OnEditCopy();
OnDeleteOrder();
}
void COrderList::OnCopy(bool onlyOrders)
{
const OrdSelection ordsel = GetCurSel();
BeginWaitCursor();
PatternClipboard::Copy(m_modDoc.GetSoundFile(), ordsel.firstOrd, ordsel.lastOrd, onlyOrders);
PatternClipboardDialog::UpdateList();
EndWaitCursor();
}
void COrderList::UpdateView(UpdateHint hint, CObject *pObj)
{
if(pObj != this && hint.ToType<SequenceHint>().GetType()[HINT_MODTYPE | HINT_MODSEQUENCE])
{
Invalidate(FALSE);
UpdateInfoText();
}
if(hint.GetType()[HINT_MPTOPTIONS])
{
m_nOrderlistMargins = TrackerSettings::Instance().orderlistMargins;
}
}
void COrderList::OnSwitchToView()
{
m_pParent.PostViewMessage(VIEWMSG_SETFOCUS);
}
void COrderList::UpdateInfoText()
{
if(::GetFocus() != m_hWnd)
return;
CSoundFile &sndFile = m_modDoc.GetSoundFile();
const auto &order = Order();
const ORDERINDEX length = order.GetLengthTailTrimmed();
CString s;
if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_HEXDISPLAY)
s.Format(_T("Position %02Xh of %02Xh"), m_nScrollPos, length);
else
s.Format(_T("Position %u of %u (%02Xh of %02Xh)"), m_nScrollPos, length, m_nScrollPos, length);
if(order.IsValidPat(m_nScrollPos))
{
if(const auto patName = sndFile.Patterns[order[m_nScrollPos]].GetName(); !patName.empty())
s += _T(": ") + mpt::ToCString(sndFile.GetCharsetInternal(), patName);
}
CMainFrame::GetMainFrame()->SetInfoText(s);
CMainFrame::GetMainFrame()->NotifyAccessibilityUpdate(*this);
}
// Accessible description for screen readers
HRESULT COrderList::get_accName(VARIANT, BSTR *pszName)
{
CSoundFile &sndFile = m_modDoc.GetSoundFile();
const auto &order = Order();
CString s;
const bool singleSel = m_nScrollPos2nd == ORDERINDEX_INVALID || m_nScrollPos2nd == m_nScrollPos;
const auto firstOrd = singleSel ? m_nScrollPos : std::min(m_nScrollPos, m_nScrollPos2nd), lastOrd = singleSel ? m_nScrollPos : std::max(m_nScrollPos, m_nScrollPos2nd);
if(singleSel)
s = MPT_CFORMAT("Order {}, ")(m_nScrollPos);
else
s = MPT_CFORMAT("Order selection {} to {}: ")(firstOrd, lastOrd);
bool first = true;
for(ORDERINDEX o = firstOrd; o <= lastOrd; o++)
{
if(!first)
s += _T(", ");
first = false;
PATTERNINDEX pat = order[o];
if(pat == ModSequence::GetIgnoreIndex())
s += _T(" Skip");
else if(pat == ModSequence::GetInvalidPatIndex())
s += _T(" Stop");
else
s += MPT_CFORMAT("Pattern {}")(pat);
if(sndFile.Patterns.IsValidPat(pat))
{
if(const auto patName = sndFile.Patterns[pat].GetName(); !patName.empty())
s += _T(" (") + mpt::ToCString(sndFile.GetCharsetInternal(), patName) + _T(")");
}
}
*pszName = s.AllocSysString();
return S_OK;
}
/////////////////////////////////////////////////////////////////
// COrderList messages
void COrderList::OnPaint()
{
TCHAR s[64];
CPaintDC dc(this);
HGDIOBJ oldfont = dc.SelectObject(m_hFont);
HGDIOBJ oldpen = dc.SelectStockObject(DC_PEN);
const auto separatorColor = GetSysColor(COLOR_WINDOW) ^ 0x808080;
const auto colorText = GetSysColor(COLOR_WINDOWTEXT), colorInvalid = GetSysColor(COLOR_GRAYTEXT), colorTextSel = GetSysColor(COLOR_HIGHLIGHTTEXT);
const auto windowBrush = GetSysColorBrush(COLOR_WINDOW), highlightBrush = GetSysColorBrush(COLOR_HIGHLIGHT), faceBrush = GetSysColorBrush(COLOR_BTNFACE);
SetDCPenColor(dc, separatorColor);
// First time?
if(m_cxFont <= 0 || m_cyFont <= 0)
{
CSize sz = dc.GetTextExtent(_T("000+"), 4);
m_cxFont = sz.cx;
m_cyFont = sz.cy;
}
if(m_cxFont > 0 && m_cyFont > 0)
{
CRect rcClient;
GetClientRect(&rcClient);
CRect rect = rcClient;
UpdateScrollInfo();
dc.SetBkMode(TRANSPARENT);
const OrdSelection selection = GetCurSel();
const int lineWidth1 = Util::ScalePixels(1, m_hWnd);
const int lineWidth2 = Util::ScalePixels(2, m_hWnd);
const bool isFocussed = (::GetFocus() == m_hWnd);
const auto &order = Order();
CSoundFile &sndFile = m_modDoc.GetSoundFile();
ORDERINDEX maxEntries = sndFile.GetModSpecifications().ordersMax;
if(order.size() > maxEntries)
{
// Only computed if potentially needed.
maxEntries = std::max(maxEntries, order.GetLengthTailTrimmed());
}
// Scrolling the shown orders(the showns rectangles)?
for(size_t pos = m_nXScroll; rect.left < rcClient.right; pos++, rect.left += m_cxFont)
{
const ORDERINDEX ord = mpt::saturate_cast<ORDERINDEX>(pos);
dc.SetTextColor(colorText);
const bool inSelection = (ord >= selection.firstOrd && ord <= selection.lastOrd);
const bool highLight = (isFocussed && inSelection);
if((rect.right = rect.left + m_cxFont) > rcClient.right)
rect.right = rcClient.right;
rect.right--;
HBRUSH background;
if(highLight)
background = highlightBrush; // Currently selected order item
else if(order.IsPositionLocked(ord))
background = faceBrush; // "Playback lock" indicator - grey out all order items which aren't played.
else
background = windowBrush; // Normal, unselected item.
::FillRect(dc, &rect, background);
// Drawing the shown pattern-indicator or drag position.
if(ord == (m_bDragging ? m_nDropPos : m_nScrollPos))
{
rect.InflateRect(-1, -1);
dc.DrawFocusRect(&rect);
rect.InflateRect(1, 1);
}
MoveToEx(dc, rect.right, rect.top, NULL);
LineTo(dc, rect.right, rect.bottom);
// Drawing the 'ctrl-transition' indicator
if(ord == sndFile.m_PlayState.m_nSeqOverride && sndFile.m_PlayState.m_nSeqOverride != ORDERINDEX_INVALID)
{
dc.FillSolidRect(CRect{rect.left + 4, rect.bottom - 4 - lineWidth1, rect.right - 4, rect.bottom - 4}, separatorColor);
}
// Drawing 'playing'-indicator.
if(ord == sndFile.GetCurrentOrder() && CMainFrame::GetMainFrame()->IsPlaying())
{
dc.FillSolidRect(CRect{rect.left + 4, rect.top + 2, rect.right - 4, rect.top + 2 + lineWidth1}, separatorColor);
m_playPos = ord;
}
// Drawing drop indicator
if(m_bDragging && ord == m_nDropPos && !inSelection)
{
const bool dropLeft = (m_nDropPos < selection.firstOrd) || TrackerSettings::Instance().orderListOldDropBehaviour;
dc.FillSolidRect(CRect{dropLeft ? (rect.left + 2) : (rect.right - 2 - lineWidth2), rect.top + 2, dropLeft ? (rect.left + 2 + lineWidth2) : (rect.right - 2), rect.bottom - 2}, separatorColor);
}
s[0] = _T('\0');
const PATTERNINDEX pat = (ord < order.size()) ? order[ord] : PATTERNINDEX_INVALID;
if(ord < maxEntries && (rect.left + m_cxFont - 4) <= rcClient.right)
{
if(pat == order.GetInvalidPatIndex())
_tcscpy(s, _T("---"));
else if(pat == order.GetIgnoreIndex())
_tcscpy(s, _T("+++"));
else
wsprintf(s, _T("%u"), pat);
}
COLORREF textCol;
if(highLight)
textCol = colorTextSel; // Highlighted pattern
else if(sndFile.Patterns.IsValidPat(pat))
textCol = colorText; // Normal pattern
else
textCol = colorInvalid; // Non-existent pattern
dc.SetTextColor(textCol);
dc.DrawText(s, -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
}
}
if(oldpen)
dc.SelectObject(oldpen);
if(oldfont)
dc.SelectObject(oldfont);
}
void COrderList::OnSetFocus(CWnd *pWnd)
{
CWnd::OnSetFocus(pWnd);
InvalidateSelection();
UpdateInfoText();
CMainFrame::GetMainFrame()->m_pOrderlistHasFocus = this;
}
void COrderList::OnKillFocus(CWnd *pWnd)
{
CWnd::OnKillFocus(pWnd);
InvalidateSelection();
CMainFrame::GetMainFrame()->m_pOrderlistHasFocus = nullptr;
}
void COrderList::OnLButtonDown(UINT nFlags, CPoint pt)
{
CRect rect;
GetClientRect(&rect);
if(pt.y < rect.bottom)
{
SetFocus();
if(IsCtrlKeyPressed())
{
// Queue pattern
QueuePattern(pt);
} else
{
// mark pattern (+skip to)
const int oldXScroll = m_nXScroll;
ORDERINDEX ord = GetOrderFromPoint(pt);
OrdSelection selection = GetCurSel();
// check if cursor is in selection - if it is, only react on MouseUp as the user might want to drag those orders
if(m_nScrollPos2nd == ORDERINDEX_INVALID || ord < selection.firstOrd || ord > selection.lastOrd)
{
m_nScrollPos2nd = ORDERINDEX_INVALID;
SetCurSel(ord, true, IsSelectionKeyPressed());
}
m_bDragging = !IsOrderInMargins(m_nScrollPos, oldXScroll) || !IsOrderInMargins(m_nScrollPos2nd, oldXScroll);
m_nMouseDownPos = ord;
if(m_bDragging)
{
m_nDragOrder = m_nDropPos = GetCurSel(true).firstOrd;
SetCapture();
}
}
} else
{
CWnd::OnLButtonDown(nFlags, pt);
}
}
void COrderList::OnLButtonUp(UINT nFlags, CPoint pt)
{
CRect rect;
GetClientRect(&rect);
// Copy or move orders?
const bool copyOrders = IsSelectionKeyPressed();
if(m_bDragging)
{
m_bDragging = false;
ReleaseCapture();
if(rect.PtInRect(pt))
{
ORDERINDEX n = GetOrderFromPoint(pt);
const OrdSelection selection = GetCurSel();
if(n != ORDERINDEX_INVALID && n == m_nDropPos && (n < selection.firstOrd || n > selection.lastOrd))
{
const bool multiSelection = (selection.firstOrd != selection.lastOrd);
const bool moveBack = m_nDropPos < m_nDragOrder;
ORDERINDEX moveCount = (selection.lastOrd - selection.firstOrd), movePos = selection.firstOrd;
if(!moveBack && !TrackerSettings::Instance().orderListOldDropBehaviour)
m_nDropPos++;
bool modified = false;
for(int i = 0; i <= moveCount; i++)
{
if(!m_modDoc.MoveOrder(movePos, m_nDropPos, true, copyOrders))
break;
modified = true;
if(moveBack != copyOrders && multiSelection)
{
movePos++;
m_nDropPos++;
}
if(moveBack && copyOrders && multiSelection)
{
movePos += 2;
m_nDropPos++;
}
}
if(multiSelection)
{
// adjust selection
m_nScrollPos2nd = m_nDropPos - 1;
m_nDropPos -= moveCount + (moveBack ? 0 : 1);
SetCurSel((moveBack && !copyOrders) ? m_nDropPos - 1 : m_nDropPos);
} else
{
SetCurSel((m_nDragOrder < m_nDropPos && !copyOrders) ? m_nDropPos - 1 : m_nDropPos);
}
// Did we actually change anything?
if(modified)
m_modDoc.SetModified();
} else
{
if(pt.y < rect.bottom && n == m_nMouseDownPos && !copyOrders)
{
// Remove selection if we didn't drag anything but multiselect was active
m_nScrollPos2nd = ORDERINDEX_INVALID;
SetFocus();
SetCurSel(n);
}
}
}
Invalidate(FALSE);
} else
{
CWnd::OnLButtonUp(nFlags, pt);
}
}
void COrderList::OnMouseMove(UINT nFlags, CPoint pt)
{
if((m_bDragging) && (m_cxFont))
{
CRect rect;
GetClientRect(&rect);
ORDERINDEX n = ORDERINDEX_INVALID;
if(rect.PtInRect(pt))
{
CSoundFile &sndFile = m_modDoc.GetSoundFile();
n = GetOrderFromPoint(pt);
if(n >= Order().size() && n >= sndFile.GetModSpecifications().ordersMax)
n = ORDERINDEX_INVALID;
}
if(n != m_nDropPos)
{
if(n != ORDERINDEX_INVALID)
{
m_nMouseDownPos = ORDERINDEX_INVALID;
m_nDropPos = n;
Invalidate(FALSE);
SetCursor(CMainFrame::curDragging);
} else
{
m_nDropPos = ORDERINDEX_INVALID;
SetCursor(CMainFrame::curNoDrop);
}
}
} else
{
CWnd::OnMouseMove(nFlags, pt);
}
}
void COrderList::OnSelectSequence(UINT nid)
{
SelectSequence(static_cast<SEQUENCEINDEX>(nid - ID_SEQUENCE_ITEM));
}
void COrderList::OnRButtonDown(UINT nFlags, CPoint pt)
{
CRect rect;
GetClientRect(&rect);
if(m_bDragging)
{
m_nDropPos = ORDERINDEX_INVALID;
OnLButtonUp(nFlags, pt);
}
if(pt.y >= rect.bottom)
return;
bool multiSelection = (m_nScrollPos2nd != ORDERINDEX_INVALID);
if(!multiSelection)
SetCurSel(GetOrderFromPoint(pt), false, false, false);
SetFocus();
HMENU hMenu = ::CreatePopupMenu();
if(!hMenu)
return;
CSoundFile &sndFile = m_modDoc.GetSoundFile();
// Check if at least one pattern in the current selection exists
bool patExists = false;
OrdSelection selection = GetCurSel();
LimitMax(selection.lastOrd, Order().GetLastIndex());
for(ORDERINDEX ord = selection.firstOrd; ord <= selection.lastOrd && !patExists; ord++)
{
patExists = Order().IsValidPat(ord);
}
const DWORD greyed = patExists ? 0 : MF_GRAYED;
CInputHandler *ih = CMainFrame::GetInputHandler();
if(multiSelection)
{
// Several patterns are selected.
AppendMenu(hMenu, MF_STRING, ID_ORDERLIST_INSERT, ih->GetKeyTextFromCommand(kcOrderlistEditInsert, _T("&Insert Patterns")));
AppendMenu(hMenu, MF_STRING, ID_ORDERLIST_DELETE, ih->GetKeyTextFromCommand(kcOrderlistEditDelete, _T("&Remove Patterns")));
AppendMenu(hMenu, MF_SEPARATOR, NULL, _T(""));
AppendMenu(hMenu, MF_STRING, ID_ORDERLIST_EDIT_COPY, ih->GetKeyTextFromCommand(kcEditCopy, _T("&Copy Patterns")));
AppendMenu(hMenu, MF_STRING, ID_ORDERLIST_EDIT_COPY_ORDERS, ih->GetKeyTextFromCommand(kcOrderlistEditCopyOrders, _T("&Copy Orders")));
AppendMenu(hMenu, MF_STRING, ID_ORDERLIST_EDIT_CUT, ih->GetKeyTextFromCommand(kcEditCut, _T("&C&ut Patterns")));
AppendMenu(hMenu, MF_STRING | greyed, ID_PATTERNPASTE, ih->GetKeyTextFromCommand(kcEditPaste, _T("P&aste Patterns")));
AppendMenu(hMenu, MF_SEPARATOR, NULL, _T(""));
AppendMenu(hMenu, MF_STRING | greyed, ID_ORDERLIST_COPY, ih->GetKeyTextFromCommand(kcDuplicatePattern, _T("&Duplicate Patterns")));
AppendMenu(hMenu, MF_STRING | greyed, ID_ORDERLIST_MERGE, ih->GetKeyTextFromCommand(kcMergePatterns, _T("&Merge Patterns")));
} else
{
// Only one pattern is selected
AppendMenu(hMenu, MF_STRING, ID_ORDERLIST_INSERT, ih->GetKeyTextFromCommand(kcOrderlistEditInsert, _T("&Insert Pattern")));
if(sndFile.GetModSpecifications().hasIgnoreIndex)
{
AppendMenu(hMenu, MF_STRING, ID_ORDERLIST_INSERT_SEPARATOR, ih->GetKeyTextFromCommand(kcOrderlistEditInsertSeparator, _T("&Insert Separator")));
}
AppendMenu(hMenu, MF_STRING, ID_ORDERLIST_DELETE, ih->GetKeyTextFromCommand(kcOrderlistEditDelete, _T("&Remove Pattern")));
AppendMenu(hMenu, MF_SEPARATOR, NULL, _T(""));
AppendMenu(hMenu, MF_STRING, ID_ORDERLIST_NEW, ih->GetKeyTextFromCommand(kcNewPattern, _T("Create &New Pattern")));
AppendMenu(hMenu, MF_STRING | greyed, ID_ORDERLIST_COPY, ih->GetKeyTextFromCommand(kcDuplicatePattern, _T("&Duplicate Pattern")));
AppendMenu(hMenu, MF_STRING | greyed, ID_PATTERNCOPY, _T("&Copy Pattern"));
AppendMenu(hMenu, MF_STRING, ID_PATTERNPASTE, ih->GetKeyTextFromCommand(kcEditPaste, _T("P&aste Pattern")));
const bool hasPatternProperties = sndFile.GetModSpecifications().patternRowsMin != sndFile.GetModSpecifications().patternRowsMax;
if(hasPatternProperties || sndFile.GetModSpecifications().hasRestartPos)
{
AppendMenu(hMenu, MF_SEPARATOR, NULL, _T(""));
if(hasPatternProperties)
AppendMenu(hMenu, MF_STRING | greyed, ID_PATTERN_PROPERTIES, _T("&Pattern Properties..."));
if(sndFile.GetModSpecifications().hasRestartPos)
AppendMenu(hMenu, MF_STRING | greyed | ((Order().GetRestartPos() == m_nScrollPos) ? MF_CHECKED : 0), ID_SETRESTARTPOS, _T("R&estart Position"));
}
if(sndFile.GetModSpecifications().sequencesMax > 1)
{
AppendMenu(hMenu, MF_SEPARATOR, NULL, _T(""));
HMENU menuSequence = ::CreatePopupMenu();
AppendMenu(hMenu, MF_POPUP, (UINT_PTR)menuSequence, _T("&Sequences"));
const SEQUENCEINDEX numSequences = sndFile.Order.GetNumSequences();
for(SEQUENCEINDEX i = 0; i < numSequences; i++)
{
CString str;
if(sndFile.Order(i).GetName().empty())
str = MPT_CFORMAT("Sequence {}")(i + 1);
else
str = MPT_CFORMAT("{}: {}")(i + 1, mpt::ToCString(sndFile.Order(i).GetName()));
const UINT flags = (sndFile.Order.GetCurrentSequenceIndex() == i) ? MF_STRING | MF_CHECKED : MF_STRING;
AppendMenu(menuSequence, flags, ID_SEQUENCE_ITEM + i, str);
}
if(sndFile.Order.GetNumSequences() < sndFile.GetModSpecifications().sequencesMax)
{
AppendMenu(menuSequence, MF_STRING, ID_SEQUENCE_ITEM + kDuplicateSequence, _T("&Duplicate current sequence"));
AppendMenu(menuSequence, MF_STRING, ID_SEQUENCE_ITEM + kAddSequence, _T("&Create empty sequence"));
}
if(sndFile.Order.GetNumSequences() > 1)
AppendMenu(menuSequence, MF_STRING, ID_SEQUENCE_ITEM + kDeleteSequence, _T("D&elete current sequence"));
else
AppendMenu(menuSequence, MF_STRING, ID_SEQUENCE_ITEM + kSplitSequence, _T("&Split sub songs into sequences"));
}
}
AppendMenu(hMenu, MF_SEPARATOR, NULL, _T(""));
AppendMenu(hMenu, ((selection.firstOrd == sndFile.m_lockOrderStart && selection.lastOrd == sndFile.m_lockOrderEnd) ? (MF_STRING | MF_CHECKED) : MF_STRING), ID_ORDERLIST_LOCKPLAYBACK, ih->GetKeyTextFromCommand(kcOrderlistLockPlayback, _T("&Lock Playback to Selection")));
AppendMenu(hMenu, (sndFile.m_lockOrderStart == ORDERINDEX_INVALID ? (MF_STRING | MF_GRAYED) : MF_STRING), ID_ORDERLIST_UNLOCKPLAYBACK, ih->GetKeyTextFromCommand(kcOrderlistUnlockPlayback, _T("&Unlock Playback")));
AppendMenu(hMenu, MF_SEPARATOR, NULL, _T(""));
AppendMenu(hMenu, MF_STRING | greyed, ID_ORDERLIST_RENDER, _T("Render to &Wave"));
ClientToScreen(&pt);
::TrackPopupMenu(hMenu, TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, 0, m_hWnd, NULL);
::DestroyMenu(hMenu);
}
void COrderList::OnLButtonDblClk(UINT, CPoint)
{
auto &sndFile = m_modDoc.GetSoundFile();
m_nScrollPos2nd = ORDERINDEX_INVALID;
SetFocus();
if(!EnsureEditable(m_nScrollPos))
return;
PATTERNINDEX pat = Order()[m_nScrollPos];
if(sndFile.Patterns.IsValidPat(pat))
m_pParent.SetCurrentPattern(pat);
else if(pat != sndFile.Order.GetIgnoreIndex())
OnCreateNewPattern();
}
void COrderList::OnMButtonDown(UINT nFlags, CPoint pt)
{
MPT_UNREFERENCED_PARAMETER(nFlags);
QueuePattern(pt);
}
void COrderList::OnHScroll(UINT nSBCode, UINT /*nPos*/, CScrollBar *)
{
UINT nNewPos = m_nXScroll;
UINT smin, smax;
GetScrollRange(SB_HORZ, (LPINT)&smin, (LPINT)&smax);
m_bScrolling = true;
switch(nSBCode)
{
case SB_LINELEFT: if (nNewPos) nNewPos--; break;
case SB_LINERIGHT: if (nNewPos < smax) nNewPos++; break;
case SB_PAGELEFT: if (nNewPos > 4) nNewPos -= 4; else nNewPos = 0; break;
case SB_PAGERIGHT: if (nNewPos + 4 < smax) nNewPos += 4; else nNewPos = smax; break;
case SB_THUMBPOSITION:
case SB_THUMBTRACK: nNewPos = GetScrollPos(true); break;
case SB_LEFT: nNewPos = 0; break;
case SB_RIGHT: nNewPos = smax; break;
case SB_ENDSCROLL: m_bScrolling = false; break;
}
if (nNewPos > smax) nNewPos = smax;
if (nNewPos != m_nXScroll)
{
m_nXScroll = static_cast<ORDERINDEX>(nNewPos);
SetScrollPos(m_nXScroll);
Invalidate(FALSE);
}
}
void COrderList::OnSize(UINT nType, int cx, int cy)
{
int nPos;
int smin, smax;
CWnd::OnSize(nType, cx, cy);
UpdateScrollInfo();
GetScrollRange(SB_HORZ, &smin, &smax);
nPos = GetScrollPos();
if(nPos > smax)
nPos = smax;
if(m_nXScroll != nPos)
{
m_nXScroll = mpt::saturate_cast<ORDERINDEX>(nPos);
SetScrollPos(m_nXScroll);
Invalidate(FALSE);
}
}
void COrderList::OnInsertOrder()
{
// insert the same order(s) after the currently selected order(s)
ModSequence &order = Order();
const OrdSelection selection = GetCurSel();
const ORDERINDEX insertCount = order.insert(selection.lastOrd + 1, selection.lastOrd - selection.firstOrd + 1);
if(!insertCount)
return;
std::copy(order.begin() + selection.firstOrd, order.begin() + selection.firstOrd + insertCount, order.begin() + selection.lastOrd + 1);
InsertUpdatePlaystate(selection.firstOrd, selection.lastOrd);
m_nScrollPos = std::min(ORDERINDEX(selection.lastOrd + 1), order.GetLastIndex());
if(insertCount > 1)
m_nScrollPos2nd = std::min(ORDERINDEX(m_nScrollPos + insertCount - 1), order.GetLastIndex());
else
m_nScrollPos2nd = ORDERINDEX_INVALID;
InvalidateSelection();
EnsureVisible(m_nScrollPos2nd);
// first inserted order has higher priority than the last one
EnsureVisible(m_nScrollPos);
Invalidate(FALSE);
m_modDoc.SetModified();
m_modDoc.UpdateAllViews(nullptr, SequenceHint().Data(), this);
}
void COrderList::OnInsertSeparatorPattern()
{
// Insert a separator pattern after the current pattern, don't move order list cursor
ModSequence &order = Order();
const OrdSelection selection = GetCurSel(true);
ORDERINDEX insertPos = selection.firstOrd;
if(!EnsureEditable(insertPos))
return;
if(order[insertPos] != order.GetInvalidPatIndex())
{
// If we're not inserting at a stop (---) index, we move on by one position.
insertPos++;
order.insert(insertPos, 1, order.GetIgnoreIndex());
} else
{
order[insertPos] = order.GetIgnoreIndex();
}
InsertUpdatePlaystate(insertPos, insertPos);
Invalidate(FALSE);
m_modDoc.SetModified();
m_modDoc.UpdateAllViews(nullptr, SequenceHint().Data(), this);
}
void COrderList::OnRenderOrder()
{
OrdSelection selection = GetCurSel();
m_modDoc.OnFileWaveConvert(selection.firstOrd, selection.lastOrd);
}
void COrderList::OnDeleteOrder()
{
OrdSelection selection = GetCurSel();
// remove selection
m_nScrollPos2nd = ORDERINDEX_INVALID;
Order().Remove(selection.firstOrd, selection.lastOrd);
m_modDoc.SetModified();
Invalidate(FALSE);
m_modDoc.UpdateAllViews(nullptr, SequenceHint().Data(), this);
DeleteUpdatePlaystate(selection.firstOrd, selection.lastOrd);
SetCurSel(selection.firstOrd, true, false, true);
}
void COrderList::OnPatternProperties()
{
ModSequence &order = Order();
const auto ord = GetCurSel(true).firstOrd;
if(order.IsValidPat(ord))
m_pParent.PostViewMessage(VIEWMSG_PATTERNPROPERTIES, order[ord]);
}
void COrderList::OnPlayerPlay()
{
m_pParent.PostMessage(WM_COMMAND, ID_PLAYER_PLAY);
}
void COrderList::OnPlayerPause()
{
m_pParent.PostMessage(WM_COMMAND, ID_PLAYER_PAUSE);
}
void COrderList::OnPlayerPlayFromStart()
{
m_pParent.PostMessage(WM_COMMAND, ID_PLAYER_PLAYFROMSTART);
}
void COrderList::OnPatternPlayFromStart()
{
m_pParent.PostMessage(WM_COMMAND, IDC_PATTERN_PLAYFROMSTART);
}
void COrderList::OnCreateNewPattern()
{
m_pParent.PostMessage(WM_COMMAND, ID_ORDERLIST_NEW);
}
void COrderList::OnDuplicatePattern()
{
m_pParent.PostMessage(WM_COMMAND, ID_ORDERLIST_COPY);
}
void COrderList::OnMergePatterns()
{
m_pParent.PostMessage(WM_COMMAND, ID_ORDERLIST_MERGE);
}
void COrderList::OnPatternCopy()
{
m_pParent.PostMessage(WM_COMMAND, ID_PATTERNCOPY);
}
void COrderList::OnPatternPaste()
{
m_pParent.PostMessage(WM_COMMAND, ID_PATTERNPASTE);
}
void COrderList::OnSetRestartPos()
{
CSoundFile &sndFile = m_modDoc.GetSoundFile();
bool modified = false;
if(m_nScrollPos == Order().GetRestartPos())
{
// Unset position
modified = (m_nScrollPos != 0);
Order().SetRestartPos(0);
} else if(sndFile.GetModSpecifications().hasRestartPos)
{
// Set new position
modified = true;
Order().SetRestartPos(m_nScrollPos);
}
if(modified)
{
m_modDoc.SetModified();
m_modDoc.UpdateAllViews(nullptr, SequenceHint().RestartPos(), this);
}
}
LRESULT COrderList::OnHelpHitTest(WPARAM, LPARAM)
{
return HID_BASE_COMMAND + IDC_ORDERLIST;
}
LRESULT COrderList::OnDragonDropping(WPARAM doDrop, LPARAM lParam)
{
const DRAGONDROP *pDropInfo = (const DRAGONDROP *)lParam;
CPoint pt;
if((!pDropInfo) || (&m_modDoc.GetSoundFile() != pDropInfo->sndFile) || (!m_cxFont))
return FALSE;
BOOL canDrop = FALSE;
switch(pDropInfo->dropType)
{
case DRAGONDROP_ORDER:
if(pDropInfo->dropItem >= Order().size())
break;
case DRAGONDROP_PATTERN:
canDrop = TRUE;
break;
}
if(!canDrop || !doDrop)
return canDrop;
GetCursorPos(&pt);
ScreenToClient(&pt);
if(pt.x < 0)
pt.x = 0;
ORDERINDEX posDest = mpt::saturate_cast<ORDERINDEX>(m_nXScroll + (pt.x / m_cxFont));
if(posDest >= Order().size())
return FALSE;
switch(pDropInfo->dropType)
{
case DRAGONDROP_PATTERN:
Order()[posDest] = static_cast<PATTERNINDEX>(pDropInfo->dropItem);
break;
case DRAGONDROP_ORDER:
Order()[posDest] = Order()[pDropInfo->dropItem];
break;
}
if(canDrop)
{
Invalidate(FALSE);
m_modDoc.SetModified();
m_modDoc.UpdateAllViews(nullptr, SequenceHint().Data(), this);
SetCurSel(posDest, true);
}
return canDrop;
}
ORDERINDEX COrderList::SetMargins(int i)
{
m_nOrderlistMargins = i;
return GetMargins();
}
void COrderList::SelectSequence(const SEQUENCEINDEX seq)
{
CriticalSection cs;
CMainFrame::GetMainFrame()->ResetNotificationBuffer();
CSoundFile &sndFile = m_modDoc.GetSoundFile();
const bool editSequence = seq >= sndFile.Order.GetNumSequences();
if(seq == kSplitSequence)
{
if(!sndFile.Order.CanSplitSubsongs())
{
Reporting::Information(U_("No sub songs have been found in this sequence."));
return;
}
if(Reporting::Confirm(U_("The order list contains separator items.\nDo you want to split the sequence at the separators into multiple song sequences?")) != cnfYes)
return;
if(!sndFile.Order.SplitSubsongsToMultipleSequences())
return;
} else if(seq == kDeleteSequence)
{
SEQUENCEINDEX curSeq = sndFile.Order.GetCurrentSequenceIndex();
mpt::ustring str = MPT_UFORMAT("Remove sequence {}: {}?")(curSeq + 1, mpt::ToUnicode(Order().GetName()));
if(Reporting::Confirm(str) == cnfYes)
sndFile.Order.RemoveSequence(curSeq);
else
return;
} else if(seq == kAddSequence || seq == kDuplicateSequence)
{
const bool duplicate = (seq == kDuplicateSequence);
const SEQUENCEINDEX newIndex = sndFile.Order.GetCurrentSequenceIndex() + 1u;
std::vector<SEQUENCEINDEX> newOrder(sndFile.Order.GetNumSequences());
std::iota(newOrder.begin(), newOrder.end(), SEQUENCEINDEX(0));
newOrder.insert(newOrder.begin() + newIndex, duplicate ? sndFile.Order.GetCurrentSequenceIndex() : SEQUENCEINDEX_INVALID);
if(m_modDoc.ReArrangeSequences(newOrder))
{
sndFile.Order.SetSequence(newIndex);
if(const auto name = sndFile.Order().GetName(); duplicate && !name.empty())
sndFile.Order().SetName(name + U_(" (Copy)"));
m_modDoc.UpdateAllViews(nullptr, SequenceHint(SEQUENCEINDEX_INVALID).Names().Data());
}
} else if(seq == sndFile.Order.GetCurrentSequenceIndex())
return;
else if(seq < sndFile.Order.GetNumSequences())
sndFile.Order.SetSequence(seq);
ORDERINDEX posCandidate = Order().GetLengthTailTrimmed() - 1;
SetCurSel(std::min(m_nScrollPos, posCandidate), true, false, true);
m_pParent.SetCurrentPattern(Order()[m_nScrollPos]);
UpdateScrollInfo();
// This won't make sense anymore in the new sequence.
OnUnlockPlayback();
cs.Leave();
if(editSequence)
m_modDoc.SetModified();
m_modDoc.UpdateAllViews(nullptr, SequenceHint().Data(), nullptr);
}
void COrderList::QueuePattern(CPoint pt)
{
CRect rect;
GetClientRect(&rect);
if(!rect.PtInRect(pt))
return;
CSoundFile &sndFile = m_modDoc.GetSoundFile();
const PATTERNINDEX ignoreIndex = sndFile.Order.GetIgnoreIndex();
const PATTERNINDEX stopIndex = sndFile.Order.GetInvalidPatIndex();
const ORDERINDEX length = Order().GetLength();
ORDERINDEX order = GetOrderFromPoint(pt);
// If this is not a playable order item, find the next valid item.
while(order < length && (Order()[order] == ignoreIndex || Order()[order] == stopIndex))
{
order++;
}
if(order < length)
{
if(sndFile.m_PlayState.m_nSeqOverride == order)
{
// This item is already queued: Dequeue it.
sndFile.m_PlayState.m_nSeqOverride = ORDERINDEX_INVALID;
} else
{
if(Order().IsPositionLocked(order))
{
// Users wants to go somewhere else, so let them do that.
OnUnlockPlayback();
}
sndFile.m_PlayState.m_nSeqOverride = order;
}
Invalidate(FALSE);
}
}
void COrderList::OnLockPlayback()
{
CSoundFile &sndFile = m_modDoc.GetSoundFile();
OrdSelection selection = GetCurSel();
if(selection.firstOrd == sndFile.m_lockOrderStart && selection.lastOrd == sndFile.m_lockOrderEnd)
{
OnUnlockPlayback();
} else
{
sndFile.m_lockOrderStart = selection.firstOrd;
sndFile.m_lockOrderEnd = selection.lastOrd;
Invalidate(FALSE);
}
}
void COrderList::OnUnlockPlayback()
{
CSoundFile &sndFile = m_modDoc.GetSoundFile();
sndFile.m_lockOrderStart = sndFile.m_lockOrderEnd = ORDERINDEX_INVALID;
Invalidate(FALSE);
}
void COrderList::InsertUpdatePlaystate(ORDERINDEX first, ORDERINDEX last)
{
auto &sndFile = m_modDoc.GetSoundFile();
Util::InsertItem(first, last, sndFile.m_PlayState.m_nNextOrder);
if(sndFile.m_PlayState.m_nSeqOverride != ORDERINDEX_INVALID)
Util::InsertItem(first, last, sndFile.m_PlayState.m_nSeqOverride);
// Adjust order lock position
if(sndFile.m_lockOrderStart != ORDERINDEX_INVALID)
Util::InsertRange(first, last, sndFile.m_lockOrderStart, sndFile.m_lockOrderEnd);
}
void COrderList::DeleteUpdatePlaystate(ORDERINDEX first, ORDERINDEX last)
{
auto &sndFile = m_modDoc.GetSoundFile();
Util::DeleteItem(first, last, sndFile.m_PlayState.m_nNextOrder);
if(sndFile.m_PlayState.m_nSeqOverride != ORDERINDEX_INVALID)
Util::DeleteItem(first, last, sndFile.m_PlayState.m_nSeqOverride);
// Adjust order lock position
if(sndFile.m_lockOrderStart != ORDERINDEX_INVALID)
Util::DeleteRange(first, last, sndFile.m_lockOrderStart, sndFile.m_lockOrderEnd);
}
INT_PTR COrderList::OnToolHitTest(CPoint point, TOOLINFO *pTI) const
{
CRect rect;
GetClientRect(&rect);
pTI->hwnd = m_hWnd;
pTI->uId = GetOrderFromPoint(point);
pTI->rect = rect;
pTI->lpszText = LPSTR_TEXTCALLBACK;
return pTI->uId;
}
BOOL COrderList::OnToolTipText(UINT, NMHDR *pNMHDR, LRESULT *)
{
TOOLTIPTEXT *pTTT = (TOOLTIPTEXT *)pNMHDR;
if(!(pTTT->uFlags & TTF_IDISHWND))
{
CString text;
const CSoundFile &sndFile = m_modDoc.GetSoundFile();
const ModSequence &order = Order();
const ORDERINDEX ord = mpt::saturate_cast<ORDERINDEX>(pNMHDR->idFrom), ordLen = order.GetLengthTailTrimmed();
text.Format(_T("Position %u of %u [%02Xh of %02Xh]"), ord, ordLen, ord, ordLen);
if(order.IsValidPat(ord))
{
PATTERNINDEX pat = order[ord];
const std::string name = sndFile.Patterns[pat].GetName();
if(!name.empty())
{
::SendMessage(pNMHDR->hwndFrom, TTM_SETMAXTIPWIDTH, 0, int32_max); // Allow multiline tooltip
text += _T("\r\n") + mpt::ToCString(sndFile.GetCharsetInternal(), name);
}
}
mpt::String::WriteCStringBuf(pTTT->szText) = text;
return TRUE;
}
return FALSE;
}
OPENMPT_NAMESPACE_END