/* * PatternClipboard.cpp * -------------------- * Purpose: Implementation of the pattern clipboard mechanism * Notes : (currently none) * Authors: OpenMPT Devs * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. */ #include "stdafx.h" #include "PatternClipboard.h" #include "PatternCursor.h" #include "Mainfrm.h" #include "Moddoc.h" #include "Clipboard.h" #include "View_pat.h" #include "../soundlib/mod_specifications.h" #include "../soundlib/Tables.h" OPENMPT_NAMESPACE_BEGIN /* Clipboard format: * Hdr: "ModPlug Tracker S3M\r\n" * Full: '|C#401v64A06' * Reset: '|...........' * Empty: '| ' * End of row: '\r\n' * * When pasting multiple patterns, the header line is followed by the order list: * Orders: 0,1,2,+,-,1\r\n * After that, the individual pattern headers and pattern data follows: * 'Rows: 64\r\n' (must be first) * 'Name: Pattern Name\r\n' (optional) * 'Signature: 4/16\r\n' (optional) * 'Swing: 16777216,16777216,16777216,16777216\r\n' (optional) * Pattern data... */ PatternClipboard PatternClipboard::instance; std::string PatternClipboard::GetFileExtension(const char *ext, bool addPadding) { std::string format(ext); if(format.size() > 3) { format.resize(3); } format = mpt::ToUpperCaseAscii(format); if(addPadding) { format.insert(0, 3 - format.size(), ' '); } return format; } std::string PatternClipboard::FormatClipboardHeader(const CSoundFile &sndFile) { return "ModPlug Tracker " + GetFileExtension(sndFile.GetModSpecifications().fileExtension, true) + "\r\n"; } // Copy a range of patterns to both the system clipboard and the internal clipboard. bool PatternClipboard::Copy(const CSoundFile &sndFile, ORDERINDEX first, ORDERINDEX last, bool onlyOrders) { const ModSequence &order = sndFile.Order(); LimitMax(first, order.GetLength()); LimitMax(last, order.GetLength()); // Set up clipboard header. std::string data = FormatClipboardHeader(sndFile) + "Orders: "; std::string patternData; // Pattern => Order list assignment std::vector patList(sndFile.Patterns.Size(), PATTERNINDEX_INVALID); PATTERNINDEX insertedPats = 0; // Add order list and pattern information to header. for(ORDERINDEX ord = first; ord <= last; ord++) { PATTERNINDEX pattern = order[ord]; if(ord != first) data += ','; if(pattern == order.GetInvalidPatIndex()) { data += '-'; } else if(pattern == order.GetIgnoreIndex()) { data += '+'; } else if(sndFile.Patterns.IsValidPat(pattern)) { if(onlyOrders) { patList[pattern] = pattern; } else if(patList[pattern] == PATTERNINDEX_INVALID) { // New pattern patList[pattern] = insertedPats++; const CPattern &pat = sndFile.Patterns[pattern]; patternData += MPT_AFORMAT("Rows: {}\r\n")(pat.GetNumRows()); std::string name = pat.GetName(); if(!name.empty()) { patternData += "Name: " + name + "\r\n"; } if(pat.GetOverrideSignature()) { patternData += MPT_AFORMAT("Signature: {}/{}\r\n")(pat.GetRowsPerBeat(), pat.GetRowsPerMeasure()); } if(pat.HasTempoSwing()) { patternData += "Swing: "; const TempoSwing &swing = pat.GetTempoSwing(); for(size_t i = 0; i < swing.size(); i++) { if(i == 0) { patternData += MPT_AFORMAT("{}")(swing[i]); } else { patternData += MPT_AFORMAT(",{}")(swing[i]); } } patternData += "\r\n"; } patternData += CreateClipboardString(sndFile, pattern, PatternRect(PatternCursor(), PatternCursor(sndFile.Patterns[pattern].GetNumRows() - 1, sndFile.GetNumChannels() - 1, PatternCursor::lastColumn))); } data += mpt::afmt::val(patList[pattern]); } } if(!onlyOrders) { data += "\r\n" + patternData; } if(instance.m_activeClipboard < instance.m_clipboards.size()) { // Copy to internal clipboard CString desc = MPT_CFORMAT("{} {} ({} to {})")(last - first + 1, onlyOrders ? CString(_T("Orders")) : CString(_T("Patterns")), first, last); instance.m_clipboards[instance.m_activeClipboard] = {data, desc}; } return ToSystemClipboard(data); } // Copy a pattern selection to both the system clipboard and the internal clipboard. bool PatternClipboard::Copy(const CSoundFile &sndFile, PATTERNINDEX pattern, PatternRect selection) { std::string data = CreateClipboardString(sndFile, pattern, selection); if(data.empty()) return false; // Set up clipboard header data.insert(0, FormatClipboardHeader(sndFile)); if(instance.m_activeClipboard < instance.m_clipboards.size()) { // Copy to internal clipboard CString desc; desc.Format(_T("%u rows, %u channels (pattern %u)"), selection.GetNumRows(), selection.GetNumChannels(), pattern); instance.m_clipboards[instance.m_activeClipboard] = {data, desc}; } return ToSystemClipboard(data); } // Copy a pattern or pattern channel to the internal pattern or channel clipboard. bool PatternClipboard::Copy(const CSoundFile &sndFile, PATTERNINDEX pattern, CHANNELINDEX channel) { if(!sndFile.Patterns.IsValidPat(pattern)) return false; const bool patternCopy = (channel == CHANNELINDEX_INVALID); const CPattern &pat = sndFile.Patterns[pattern]; PatternRect selection; if(patternCopy) selection = {PatternCursor(0, 0, PatternCursor::firstColumn), PatternCursor(pat.GetNumRows() - 1, pat.GetNumChannels() - 1, PatternCursor::lastColumn)}; else selection = {PatternCursor(0, channel, PatternCursor::firstColumn), PatternCursor(pat.GetNumRows() - 1, channel, PatternCursor::lastColumn)}; std::string data = CreateClipboardString(sndFile, pattern, selection); if(data.empty()) return false; // Set up clipboard header data.insert(0, FormatClipboardHeader(sndFile)); // Copy to internal clipboard (patternCopy ? instance.m_patternClipboard : instance.m_channelClipboard) = {data, {}}; return true; } // Create the clipboard text for a pattern selection std::string PatternClipboard::CreateClipboardString(const CSoundFile &sndFile, PATTERNINDEX pattern, PatternRect selection) { if(!sndFile.Patterns.IsValidPat(pattern)) return ""; if(selection.GetStartColumn() == PatternCursor::paramColumn) { // Special case: If selection starts with a parameter column, extend it to include the effect letter as well. PatternCursor upper(selection.GetUpperLeft()); upper.Move(0, 0, -1); selection = PatternRect(upper, selection.GetLowerRight()); } const ROWINDEX startRow = selection.GetStartRow(), numRows = selection.GetNumRows(); const CHANNELINDEX startChan = selection.GetStartChannel(), numChans = selection.GetNumChannels(); std::string data; data.reserve(numRows * (numChans * 12 + 2)); for(ROWINDEX row = 0; row < numRows; row++) { if(row + startRow >= sndFile.Patterns[pattern].GetNumRows()) break; const ModCommand *m = sndFile.Patterns[pattern].GetpModCommand(row + startRow, startChan); for(CHANNELINDEX chn = 0; chn < numChans; chn++, m++) { PatternCursor cursor(0, startChan + chn); data += '|'; // Note if(selection.ContainsHorizontal(cursor)) { if(m->IsNote()) { // Need to guarantee that sharps are used for the clipboard. data += mpt::ToCharset(mpt::Charset::Locale, mpt::ustring(NoteNamesSharp[(m->note - NOTE_MIN) % 12])); data += ('0' + (m->note - NOTE_MIN) / 12); } else { data += mpt::ToCharset(mpt::Charset::Locale, sndFile.GetNoteName(m->note)); } } else { // No note data += " "; } // Instrument cursor.Move(0, 0, 1); if(selection.ContainsHorizontal(cursor)) { if(m->instr) { data += ('0' + (m->instr / 10)); data += ('0' + (m->instr % 10)); } else { data += ".."; } } else { data += " "; } // Volume cursor.Move(0, 0, 1); if(selection.ContainsHorizontal(cursor)) { if(m->IsPcNote()) { data += mpt::afmt::dec0<3>(m->GetValueVolCol()); } else { if(m->volcmd != VOLCMD_NONE && m->vol <= 99) { data += sndFile.GetModSpecifications().GetVolEffectLetter(m->volcmd); data += mpt::afmt::dec0<2>(m->vol); } else { data += "..."; } } } else { data += " "; } // Effect cursor.Move(0, 0, 1); if(selection.ContainsHorizontal(cursor)) { if(m->IsPcNote()) { data += mpt::afmt::dec0<3>(m->GetValueEffectCol()); } else { if(m->command != CMD_NONE) { data += sndFile.GetModSpecifications().GetEffectLetter(m->command); } else { data += '.'; } if(m->param != 0 && m->command != CMD_NONE) { data += mpt::afmt::HEX0<2>(m->param); } else { data += ".."; } } } else { data += " "; } } // Next Row data += "\r\n"; } return data; } // Try pasting a pattern selection from the system clipboard. bool PatternClipboard::Paste(CSoundFile &sndFile, PatternEditPos &pastePos, PasteModes mode, PatternRect &pasteRect, bool &orderChanged) { std::string data; if(!FromSystemClipboard(data) || !HandlePaste(sndFile, pastePos, mode, data, pasteRect, orderChanged)) { // Fall back to internal clipboard if there's no valid pattern data in the system clipboard. return Paste(sndFile, pastePos, mode, pasteRect, instance.m_activeClipboard, orderChanged); } return true; } // Try pasting a pattern selection from an internal clipboard. bool PatternClipboard::Paste(CSoundFile &sndFile, PatternEditPos &pastePos, PasteModes mode, PatternRect &pasteRect, clipindex_t internalClipboard, bool &orderChanged) { if(internalClipboard >= instance.m_clipboards.size()) return false; return HandlePaste(sndFile, pastePos, mode, instance.m_clipboards[internalClipboard].content, pasteRect, orderChanged); } // Paste from pattern or channel clipboard. bool PatternClipboard::Paste(CSoundFile &sndFile, PATTERNINDEX pattern, CHANNELINDEX channel) { PatternEditPos pastePos{0, ORDERINDEX_INVALID, pattern, channel != CHANNELINDEX_INVALID ? channel : CHANNELINDEX(0)}; PatternRect pasteRect; bool orderChanged = false; return HandlePaste(sndFile, pastePos, pmOverwrite, (channel == CHANNELINDEX_INVALID ? instance.m_patternClipboard : instance.m_channelClipboard).content, pasteRect, orderChanged); } // Parse clipboard string and perform the pasting operation. bool PatternClipboard::HandlePaste(CSoundFile &sndFile, PatternEditPos &pastePos, PasteModes mode, const std::string &data, PatternRect &pasteRect, bool &orderChanged) { const std::string whitespace(" \n\r\t"); PATTERNINDEX pattern = pastePos.pattern; ORDERINDEX &curOrder = pastePos.order; orderChanged = false; if(sndFile.GetpModDoc() == nullptr) return false; CModDoc &modDoc = *(sndFile.GetpModDoc()); ModSequence &order = sndFile.Order(); bool success = false; bool prepareUndo = true; // Prepare pattern for undo next time bool firstUndo = true; // For chaining undos (see overflow / multi-pattern paste) // Search for signature std::string::size_type pos, startPos = 0; MODTYPE pasteFormat = MOD_TYPE_NONE; while(pasteFormat == MOD_TYPE_NONE && (startPos = data.find("ModPlug Tracker ", startPos)) != std::string::npos) { startPos += 16; // Check paste format const std::string format = mpt::ToUpperCaseAscii(mpt::trim(data.substr(startPos, 3))); for(const auto &spec : ModSpecs::Collection) { if(format == GetFileExtension(spec->fileExtension, false)) { pasteFormat = spec->internalType; startPos += 3; break; } } } // What is this I don't even if(startPos == std::string::npos) return false; // Skip whitespaces startPos = data.find_first_not_of(whitespace, startPos); if(startPos == std::string::npos) return false; // Multi-order stuff std::vector patList; // Multi-order mix-paste stuff std::vector ordList; std::vector patOffset; enum { kSinglePaste, kMultiInsert, kMultiOverwrite } patternMode = kSinglePaste; if(data.substr(startPos, 8) == "Orders: ") { // Pasting several patterns at once. patternMode = (mode == pmOverwrite) ? kMultiInsert : kMultiOverwrite; // Put new patterns after current pattern, if it exists if(order.IsValidPat(curOrder) && patternMode == kMultiInsert) curOrder++; pos = startPos + 8; startPos = data.find('\n', pos); ORDERINDEX writeOrder = curOrder; const bool onlyOrders = (startPos == std::string::npos); if(onlyOrders) { // Only create order list, no patterns startPos = data.size(); } else { startPos++; } while(pos < startPos && pos != std::string::npos) { PATTERNINDEX insertPat; auto curPos = pos; // Next order item, please pos = data.find(',', pos + 1); if(pos != std::string::npos) pos++; if(data[curPos] == '+') { insertPat = order.GetIgnoreIndex(); } else if(data[curPos] == '-') { insertPat = order.GetInvalidPatIndex(); } else { insertPat = ConvertStrTo(data.substr(curPos, 10)); if(patternMode == kMultiOverwrite) { // We only want the order of pasted patterns now, do not create any new patterns ordList.push_back(insertPat); continue; } if(insertPat < patList.size() && patList[insertPat] != PATTERNINDEX_INVALID) { // Duplicate pattern insertPat = patList[insertPat]; } else if(!onlyOrders) { // New pattern if(insertPat >= patList.size()) { patList.resize(insertPat + 1, PATTERNINDEX_INVALID); } patList[insertPat] = modDoc.InsertPattern(64); insertPat = patList[insertPat]; } } if((insertPat == order.GetIgnoreIndex() && !sndFile.GetModSpecifications().hasIgnoreIndex) || (insertPat == order.GetInvalidPatIndex() && !sndFile.GetModSpecifications().hasStopIndex) || insertPat == PATTERNINDEX_INVALID || patternMode == kMultiOverwrite) { continue; } if(order.insert(writeOrder, 1) == 0) { break; } order[writeOrder++] = insertPat; orderChanged = true; } if(patternMode == kMultiInsert) { if(!patList.empty()) { // First pattern we're going to paste in. pattern = patList[0]; } // We already modified the order list... success = true; pastePos.pattern = pattern; pastePos.row = 0; pastePos.channel = 0; } else { if(ordList.empty()) return success; // Find pattern offsets pos = startPos; patOffset.reserve(ordList.size()); bool patStart = false; while((pos = data.find_first_not_of(whitespace, pos)) != std::string::npos) { auto eol = data.find('\n', pos + 1); if(eol == std::string::npos) eol = data.size(); if(data.substr(pos, 6) == "Rows: ") { patStart = true; } else if(data.substr(pos, 1) == "|" && patStart) { patOffset.push_back(pos); patStart = false; } pos = eol; } if(patOffset.empty()) return success; startPos = patOffset[0]; } } size_t curPattern = 0; // Currently pasted pattern for multi-paste ROWINDEX startRow = pastePos.row; ROWINDEX curRow = startRow; CHANNELINDEX startChan = pastePos.channel, col; // Can we actually paste at this position? if(!sndFile.Patterns.IsValidPat(pattern) || startRow >= sndFile.Patterns[pattern].GetNumRows() || startChan >= sndFile.GetNumChannels()) { return success; } const CModSpecifications &sourceSpecs = CSoundFile::GetModSpecifications(pasteFormat); const bool overflowPaste = (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OVERFLOWPASTE) && mode != pmPasteFlood && mode != pmPushForward && patternMode != kMultiInsert && curOrder != ORDERINDEX_INVALID; const bool doITStyleMix = (mode == pmMixPasteIT); const bool doMixPaste = (mode == pmMixPaste) || doITStyleMix; const bool clipboardHasS3MCommands = (pasteFormat & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_S3M)); const bool insertNewPatterns = overflowPaste && (patternMode == kMultiOverwrite); PatternCursor startPoint(startRow, startChan, PatternCursor::lastColumn), endPoint(startRow, startChan, PatternCursor::firstColumn); ModCommand *patData = sndFile.Patterns[pattern].GetpModCommand(startRow, 0); auto multiPastePos = ordList.cbegin(); pos = startPos; while(curRow < sndFile.Patterns[pattern].GetNumRows() || overflowPaste || patternMode == kMultiInsert) { // Parse next line pos = data.find_first_not_of(whitespace, pos); if(pos == std::string::npos) { // End of paste if(mode == pmPasteFlood && curRow != startRow && curRow < sndFile.Patterns[pattern].GetNumRows()) { // Restarting pasting from beginning. pos = startPos; multiPastePos = ordList.cbegin(); continue; } else { // Prevent infinite loop with malformed clipboard data. break; } } auto eol = data.find('\n', pos + 1); if(eol == std::string::npos) eol = data.size(); // Handle multi-paste: Read pattern information if(patternMode != kSinglePaste) { // Parse pattern header lines bool parsedLine = true; if(data.substr(pos, 6) == "Rows: ") { pos += 6; // Advance to next pattern if(patternMode == kMultiOverwrite) { // In case of multi-pattern mix-paste, we know that we reached the end of the previous pattern and need to parse the next order now. multiPastePos++; if(multiPastePos == ordList.cend() || *multiPastePos >= patOffset.size()) pos = data.size(); else pos = patOffset[*multiPastePos]; continue; } // Otherwise, parse this pattern header normally. do { if(curPattern >= patList.size()) { return success; } pattern = patList[curPattern++]; } while (pattern == PATTERNINDEX_INVALID); ROWINDEX numRows = ConvertStrTo(data.substr(pos, 10)); sndFile.Patterns[pattern].Resize(numRows); patData = sndFile.Patterns[pattern].GetpModCommand(0, 0); curRow = 0; prepareUndo = true; } else if(data.substr(pos, 6) == "Name: ") { pos += 6; auto name = mpt::trim_right(data.substr(pos, eol - pos - 1)); sndFile.Patterns[pattern].SetName(name); } else if(data.substr(pos, 11) == "Signature: ") { pos += 11; auto pos2 = data.find("/", pos + 1); if(pos2 != std::string::npos) { pos2++; ROWINDEX rpb = ConvertStrTo(data.substr(pos, pos2 - pos)); ROWINDEX rpm = ConvertStrTo(data.substr(pos2, eol - pos2)); sndFile.Patterns[pattern].SetSignature(rpb, rpm); } } else if(data.substr(pos, 7) == "Swing: ") { pos += 7; TempoSwing swing; swing.resize(sndFile.Patterns[pattern].GetRowsPerBeat(), TempoSwing::Unity); size_t i = 0; while(pos != std::string::npos && pos < eol && i < swing.size()) { swing[i++] = ConvertStrTo(data.substr(pos, eol - pos)); pos = data.find(',', pos + 1); if(pos != std::string::npos) pos++; } sndFile.Patterns[pattern].SetTempoSwing(swing); } else { parsedLine = false; } if(parsedLine) { pos = eol; continue; } } if(data[pos] != '|') { // Not a valid line? pos = eol; continue; } if(overflowPaste) { // Handle overflow paste. Continue pasting in next pattern if enabled. // If Paste Flood is enabled, this won't be called due to obvious reasons. while(curRow >= sndFile.Patterns[pattern].GetNumRows()) { curRow = 0; ORDERINDEX nextOrder = order.GetNextOrderIgnoringSkips(curOrder); if(nextOrder <= curOrder || !order.IsValidPat(nextOrder)) { PATTERNINDEX newPat; if(!insertNewPatterns || curOrder >= sndFile.GetModSpecifications().ordersMax || (newPat = modDoc.InsertPattern(sndFile.Patterns[pattern].GetNumRows())) == PATTERNINDEX_INVALID || order.insert(curOrder + 1, 1, newPat) == 0) { return success; } orderChanged = true; nextOrder = curOrder + 1; } pattern = order[nextOrder]; if(!sndFile.Patterns.IsValidPat(pattern)) return success; patData = sndFile.Patterns[pattern].GetpModCommand(0, 0); curOrder = nextOrder; prepareUndo = true; startRow = 0; } } success = true; col = startChan; // Paste columns while((pos + 11 < data.size()) && (data[pos] == '|')) { pos++; // Handle pasting large pattern into smaller pattern (e.g. 128-row pattern into MOD, which only allows 64 rows) ModCommand dummy; ModCommand &m = curRow < sndFile.Patterns[pattern].GetNumRows() ? patData[col] : dummy; // Check valid paste condition. Paste will be skipped if // - col is not a valid channelindex or // - doing mix paste and paste destination modcommand is a PCnote or // - doing mix paste and trying to paste PCnote on non-empty modcommand. const bool skipPaste = col >= sndFile.GetNumChannels() || (doMixPaste && m.IsPcNote()) || (doMixPaste && data[pos] == 'P' && !m.IsEmpty()); if(skipPaste == false) { // Before changing anything in this pattern, we have to create an undo point. if(prepareUndo) { modDoc.GetPatternUndo().PrepareUndo(pattern, startChan, startRow, sndFile.GetNumChannels(), sndFile.Patterns[pattern].GetNumRows(), "Paste", !firstUndo); prepareUndo = false; firstUndo = false; } // ITSyle mixpaste requires that we keep a copy of the thing we are about to paste on // so that we can refer back to check if there was anything in e.g. the note column before we pasted. const ModCommand origModCmd = m; // push channel data below paste point first. if(mode == pmPushForward) { for(ROWINDEX pushRow = sndFile.Patterns[pattern].GetNumRows() - 1 - curRow; pushRow > 0; pushRow--) { patData[col + pushRow * sndFile.GetNumChannels()] = patData[col + (pushRow - 1) * sndFile.GetNumChannels()]; } m.Clear(); } PatternCursor::Columns firstCol = PatternCursor::lastColumn, lastCol = PatternCursor::firstColumn; // Note if(data[pos] != ' ' && (!doMixPaste || ((!doITStyleMix && origModCmd.note == NOTE_NONE) || (doITStyleMix && origModCmd.note == NOTE_NONE && origModCmd.instr == 0 && origModCmd.volcmd == VOLCMD_NONE)))) { firstCol = PatternCursor::noteColumn; m.note = NOTE_NONE; if(data[pos] == '=') m.note = NOTE_KEYOFF; else if(data[pos] == '^') m.note = NOTE_NOTECUT; else if(data[pos] == '~') m.note = NOTE_FADE; else if(data[pos] == 'P') { if(data[pos + 2] == 'S' || data[pos + 2] == 's') m.note = NOTE_PCS; else m.note = NOTE_PC; } else if (data[pos] != '.') { // Check note names for(uint8 i = 0; i < 12; i++) { if(data[pos] == NoteNamesSharp[i][0] && data[pos + 1] == NoteNamesSharp[i][1]) { m.note = ModCommand::NOTE(i + NOTE_MIN); break; } } if(m.note != NOTE_NONE) { // Check octave m.note += (data[pos + 2] - '0') * 12; if(!m.IsNote()) { // Invalid octave m.note = NOTE_NONE; } } } } // Instrument if(data[pos + 3] > ' ' && (!doMixPaste || ( (!doITStyleMix && origModCmd.instr == 0) || (doITStyleMix && origModCmd.note == NOTE_NONE && origModCmd.instr == 0 && origModCmd.volcmd == VOLCMD_NONE) ) )) { firstCol = std::min(firstCol, PatternCursor::instrColumn); lastCol = std::max(lastCol, PatternCursor::instrColumn); if(data[pos + 3] >= '0' && data[pos + 3] <= ('0' + (MAX_INSTRUMENTS / 10))) { m.instr = (data[pos + 3] - '0') * 10 + (data[pos + 4] - '0'); } else m.instr = 0; } // Volume if(data[pos + 5] > ' ' && (!doMixPaste || ((!doITStyleMix && origModCmd.volcmd == VOLCMD_NONE) || (doITStyleMix && origModCmd.note == NOTE_NONE && origModCmd.instr == 0 && origModCmd.volcmd == VOLCMD_NONE)))) { firstCol = std::min(firstCol, PatternCursor::volumeColumn); lastCol = std::max(lastCol, PatternCursor::volumeColumn); if(data[pos + 5] != '.') { if(m.IsPcNote()) { m.SetValueVolCol(ConvertStrTo(data.substr(pos + 5, 3))); } else { m.volcmd = VOLCMD_NONE; for(int i = VOLCMD_NONE + 1; i < MAX_VOLCMDS; i++) { const char cmd = sourceSpecs.GetVolEffectLetter(static_cast(i)); if(data[pos + 5] == cmd && cmd != '?') { m.volcmd = static_cast(i); break; } } m.vol = (data[pos + 6] - '0') * 10 + (data[pos + 7] - '0'); } } else { m.volcmd = VOLCMD_NONE; m.vol = 0; } } // Effect if(m.IsPcNote()) { if(data[pos + 8] != '.' && data[pos + 8] > ' ') { firstCol = std::min(firstCol, PatternCursor::paramColumn); lastCol = std::max(lastCol, PatternCursor::paramColumn); m.SetValueEffectCol(ConvertStrTo(data.substr(pos + 8, 3))); } else if(!origModCmd.IsPcNote()) { // No value provided in clipboard if((m.command == CMD_MIDI || m.command == CMD_SMOOTHMIDI) && m.param < 128) m.SetValueEffectCol(static_cast(Util::muldivr(m.param, ModCommand::maxColumnValue, 127))); else m.SetValueEffectCol(0); } } else { if(data[pos + 8] > ' ' && (!doMixPaste || ((!doITStyleMix && origModCmd.command == CMD_NONE) || (doITStyleMix && origModCmd.command == CMD_NONE && origModCmd.param == 0)))) { firstCol = std::min(firstCol, PatternCursor::effectColumn); lastCol = std::max(lastCol, PatternCursor::effectColumn); m.command = CMD_NONE; if(data[pos + 8] != '.') { for(int i = CMD_NONE + 1; i < MAX_EFFECTS; i++) { const char cmd = sourceSpecs.GetEffectLetter(static_cast(i)); if(data[pos + 8] == cmd && cmd != '?') { m.command = static_cast(i); break; } } } } // Effect value if(data[pos + 9] > ' ' && (!doMixPaste || ((!doITStyleMix && (origModCmd.command == CMD_NONE || origModCmd.param == 0)) || (doITStyleMix && origModCmd.command == CMD_NONE && origModCmd.param == 0)))) { firstCol = std::min(firstCol, PatternCursor::paramColumn); lastCol = std::max(lastCol, PatternCursor::paramColumn); m.param = 0; if(data[pos + 9] != '.') { for(uint8 i = 0; i < 16; i++) { if(data[pos + 9] == szHexChar[i]) m.param |= (i << 4); if(data[pos + 10] == szHexChar[i]) m.param |= i; } } } // Speed / tempo command conversion if (sndFile.GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM)) { switch(m.command) { case CMD_SPEED: case CMD_TEMPO: if(!clipboardHasS3MCommands) { if(m.param < 32) m.command = CMD_SPEED; else m.command = CMD_TEMPO; } else { if(m.command == CMD_SPEED && m.param >= 32) m.param = CMD_TEMPO; else if(m.command == CMD_TEMPO && m.param < 32) m.param = CMD_SPEED; } break; } } else { switch(m.command) { case CMD_SPEED: case CMD_TEMPO: if(!clipboardHasS3MCommands) { if(m.param < 32) m.command = CMD_SPEED; else m.command = CMD_TEMPO; } break; } } } // Convert some commands, if necessary. With mix paste convert only // if the original modcommand was empty as otherwise the unchanged parts // of the old modcommand would falsely be interpreted being of type // origFormat and ConvertCommand could change them. if(pasteFormat != sndFile.GetType() && (!doMixPaste || origModCmd.IsEmpty())) m.Convert(pasteFormat, sndFile.GetType(), sndFile); // Sanitize PC events if(m.IsPcNote()) { m.SetValueEffectCol(std::min(m.GetValueEffectCol(), static_cast(ModCommand::maxColumnValue))); m.SetValueVolCol(std::min(m.GetValueVolCol(), static_cast(ModCommand::maxColumnValue))); } // Adjust pattern selection if(col == startChan) startPoint.SetColumn(startChan, firstCol); if(endPoint.CompareColumn(PatternCursor(0, col, lastCol)) < 0) endPoint.SetColumn(col, lastCol); if(curRow > endPoint.GetRow()) endPoint.SetRow(curRow); pasteRect = PatternRect(startPoint, endPoint); } pos += 11; col++; } // Next row patData += sndFile.GetNumChannels(); curRow++; pos = eol; } return success; } // Copy one of the internal clipboards to the system clipboard. bool PatternClipboard::SelectClipboard(clipindex_t which) { instance.m_activeClipboard = which; return ToSystemClipboard(instance.m_clipboards[instance.m_activeClipboard]); } // Switch to the next internal clipboard. bool PatternClipboard::CycleForward() { instance.m_activeClipboard++; if(instance.m_activeClipboard >= instance.m_clipboards.size()) instance.m_activeClipboard = 0; return SelectClipboard(instance.m_activeClipboard); } // Switch to the previous internal clipboard. bool PatternClipboard::CycleBackward() { if(instance.m_activeClipboard == 0) instance.m_activeClipboard = instance.m_clipboards.size() - 1; else instance.m_activeClipboard--; return SelectClipboard(instance.m_activeClipboard); } // Set the maximum number of internal clipboards. void PatternClipboard::SetClipboardSize(clipindex_t maxEntries) { instance.m_clipboards.resize(maxEntries, {"", _T("unused")}); LimitMax(instance.m_activeClipboard, maxEntries - 1); } // Check whether patterns can be pasted from clipboard bool PatternClipboard::CanPaste() { return !!IsClipboardFormatAvailable(CF_TEXT); } // System-specific clipboard functions bool PatternClipboard::ToSystemClipboard(const std::string_view &data) { Clipboard clipboard(CF_TEXT, data.size() + 1); if(auto dst = clipboard.As()) { std::copy(data.begin(), data.end(), dst); dst[data.size()] = '\0'; return true; } return false; } // System-specific clipboard functions bool PatternClipboard::FromSystemClipboard(std::string &data) { Clipboard clipboard(CF_TEXT); if(auto cbdata = clipboard.Get(); cbdata.data()) { if(cbdata.size() > 0) data.assign(mpt::byte_cast(cbdata.data()), cbdata.size() - 1); return !data.empty(); } return false; } BEGIN_MESSAGE_MAP(PatternClipboardDialog, ResizableDialog) ON_EN_UPDATE(IDC_EDIT1, &PatternClipboardDialog::OnNumClipboardsChanged) ON_LBN_SELCHANGE(IDC_LIST1, &PatternClipboardDialog::OnSelectClipboard) ON_LBN_DBLCLK(IDC_LIST1, &PatternClipboardDialog::OnEditName) END_MESSAGE_MAP() PatternClipboardDialog PatternClipboardDialog::instance; void PatternClipboardDialog::DoDataExchange(CDataExchange *pDX) { DDX_Control(pDX, IDC_SPIN1, m_numClipboardsSpin); DDX_Control(pDX, IDC_LIST1, m_clipList); } PatternClipboardDialog::PatternClipboardDialog() : m_editNameBox(*this) { } void PatternClipboardDialog::Show() { instance.m_isLocked = true; if(!instance.m_isCreated) { instance.Create(IDD_CLIPBOARD, CMainFrame::GetMainFrame()); instance.m_numClipboardsSpin.SetRange(0, int16_max); } instance.SetDlgItemInt(IDC_EDIT1, mpt::saturate_cast(PatternClipboard::GetClipboardSize()), FALSE); instance.m_isLocked = false; instance.m_isCreated = true; instance.UpdateList(); instance.SetWindowPos(nullptr, instance.m_posX, instance.m_posY, 0, 0, SWP_SHOWWINDOW | SWP_NOOWNERZORDER | SWP_NOSIZE | SWP_NOZORDER | (instance.m_posX == -1 ? SWP_NOMOVE : 0)); } void PatternClipboardDialog::OnNumClipboardsChanged() { if(m_isLocked) { return; } OnEndEdit(); PatternClipboard::SetClipboardSize(GetDlgItemInt(IDC_EDIT1, nullptr, FALSE)); UpdateList(); } void PatternClipboardDialog::UpdateList() { if(instance.m_isLocked) { return; } instance.m_clipList.ResetContent(); PatternClipboard::clipindex_t i = 0; for(const auto &clip : PatternClipboard::instance.m_clipboards) { const int item = instance.m_clipList.AddString(clip.description); instance.m_clipList.SetItemDataPtr(item, reinterpret_cast(i)); if(PatternClipboard::instance.m_activeClipboard == i) { instance.m_clipList.SetCurSel(item); } i++; } } void PatternClipboardDialog::OnSelectClipboard() { if(m_isLocked) { return; } PatternClipboard::clipindex_t item = reinterpret_cast(m_clipList.GetItemDataPtr(m_clipList.GetCurSel())); PatternClipboard::SelectClipboard(item); OnEndEdit(); } void PatternClipboardDialog::OnOK() { const CWnd *focus = GetFocus(); if(focus == &m_editNameBox) { // User pressed enter in clipboard name edit box => cancel editing OnEndEdit(); } else if(focus == &m_clipList) { // User pressed enter in the clipboard name list => start editing OnEditName(); } else { ResizableDialog::OnOK(); } } void PatternClipboardDialog::OnCancel() { if(GetFocus() == &m_editNameBox) { // User pressed enter in clipboard name edit box => just cancel editing m_editNameBox.DestroyWindow(); return; } OnEndEdit(false); m_isCreated = false; m_isLocked = true; RECT rect; GetWindowRect(&rect); m_posX = rect.left; m_posY = rect.top; DestroyWindow(); } void PatternClipboardDialog::OnEditName() { OnEndEdit(); const int sel = m_clipList.GetCurSel(); if(sel == LB_ERR) { return; } CRect rect; m_clipList.GetItemRect(sel, rect); rect.InflateRect(0, 2, 0, 2); // Create the edit control m_editNameBox.Create(WS_VISIBLE | WS_CHILD | WS_BORDER | ES_LEFT | ES_AUTOHSCROLL, rect, &m_clipList, 1); m_editNameBox.SetFont(m_clipList.GetFont()); m_editNameBox.SetWindowText(PatternClipboard::instance.m_clipboards[sel].description); m_editNameBox.SetSel(0, -1, TRUE); m_editNameBox.SetFocus(); SetWindowLongPtr(m_editNameBox.m_hWnd, GWLP_USERDATA, (LONG_PTR)m_clipList.GetItemDataPtr(sel)); } void PatternClipboardDialog::OnEndEdit(bool apply) { if(m_editNameBox.GetSafeHwnd() == NULL) { return; } if(apply) { size_t sel = GetWindowLongPtr(m_editNameBox.m_hWnd, GWLP_USERDATA); if(sel >= PatternClipboard::instance.m_clipboards.size()) { // What happened? return; } CString newName; m_editNameBox.GetWindowText(newName); PatternClipboard::instance.m_clipboards[sel].description = newName; } SetWindowLongPtr(m_editNameBox.m_hWnd, GWLP_USERDATA, LONG_PTR(-1)); m_editNameBox.DestroyWindow(); UpdateList(); } BEGIN_MESSAGE_MAP(PatternClipboardDialog::CInlineEdit, CEdit) ON_WM_KILLFOCUS() END_MESSAGE_MAP() PatternClipboardDialog::CInlineEdit::CInlineEdit(PatternClipboardDialog &dlg) : parent(dlg) { } void PatternClipboardDialog::CInlineEdit::OnKillFocus(CWnd *newWnd) { parent.OnEndEdit(true); CEdit::OnKillFocus(newWnd); } OPENMPT_NAMESPACE_END