/* * 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(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(rcClient.right / m_cxFont); else { const int fontWidth = GetFontWidth(); return (fontWidth > 0) ? mpt::saturate_cast(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 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(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(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().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(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(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(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(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(m_nXScroll + (pt.x / m_cxFont)); if(posDest >= Order().size()) return FALSE; switch(pDropInfo->dropType) { case DRAGONDROP_PATTERN: Order()[posDest] = static_cast(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 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(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