PatternClipboard.cpp 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227
  1. /*
  2. * PatternClipboard.cpp
  3. * --------------------
  4. * Purpose: Implementation of the pattern clipboard mechanism
  5. * Notes : (currently none)
  6. * Authors: OpenMPT Devs
  7. * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
  8. */
  9. #include "stdafx.h"
  10. #include "PatternClipboard.h"
  11. #include "PatternCursor.h"
  12. #include "Mainfrm.h"
  13. #include "Moddoc.h"
  14. #include "Clipboard.h"
  15. #include "View_pat.h"
  16. #include "../soundlib/mod_specifications.h"
  17. #include "../soundlib/Tables.h"
  18. OPENMPT_NAMESPACE_BEGIN
  19. /* Clipboard format:
  20. * Hdr: "ModPlug Tracker S3M\r\n"
  21. * Full: '|C#401v64A06'
  22. * Reset: '|...........'
  23. * Empty: '| '
  24. * End of row: '\r\n'
  25. *
  26. * When pasting multiple patterns, the header line is followed by the order list:
  27. * Orders: 0,1,2,+,-,1\r\n
  28. * After that, the individual pattern headers and pattern data follows:
  29. * 'Rows: 64\r\n' (must be first)
  30. * 'Name: Pattern Name\r\n' (optional)
  31. * 'Signature: 4/16\r\n' (optional)
  32. * 'Swing: 16777216,16777216,16777216,16777216\r\n' (optional)
  33. * Pattern data...
  34. */
  35. PatternClipboard PatternClipboard::instance;
  36. std::string PatternClipboard::GetFileExtension(const char *ext, bool addPadding)
  37. {
  38. std::string format(ext);
  39. if(format.size() > 3)
  40. {
  41. format.resize(3);
  42. }
  43. format = mpt::ToUpperCaseAscii(format);
  44. if(addPadding)
  45. {
  46. format.insert(0, 3 - format.size(), ' ');
  47. }
  48. return format;
  49. }
  50. std::string PatternClipboard::FormatClipboardHeader(const CSoundFile &sndFile)
  51. {
  52. return "ModPlug Tracker " + GetFileExtension(sndFile.GetModSpecifications().fileExtension, true) + "\r\n";
  53. }
  54. // Copy a range of patterns to both the system clipboard and the internal clipboard.
  55. bool PatternClipboard::Copy(const CSoundFile &sndFile, ORDERINDEX first, ORDERINDEX last, bool onlyOrders)
  56. {
  57. const ModSequence &order = sndFile.Order();
  58. LimitMax(first, order.GetLength());
  59. LimitMax(last, order.GetLength());
  60. // Set up clipboard header.
  61. std::string data = FormatClipboardHeader(sndFile) + "Orders: ";
  62. std::string patternData;
  63. // Pattern => Order list assignment
  64. std::vector<PATTERNINDEX> patList(sndFile.Patterns.Size(), PATTERNINDEX_INVALID);
  65. PATTERNINDEX insertedPats = 0;
  66. // Add order list and pattern information to header.
  67. for(ORDERINDEX ord = first; ord <= last; ord++)
  68. {
  69. PATTERNINDEX pattern = order[ord];
  70. if(ord != first)
  71. data += ',';
  72. if(pattern == order.GetInvalidPatIndex())
  73. {
  74. data += '-';
  75. } else if(pattern == order.GetIgnoreIndex())
  76. {
  77. data += '+';
  78. } else if(sndFile.Patterns.IsValidPat(pattern))
  79. {
  80. if(onlyOrders)
  81. {
  82. patList[pattern] = pattern;
  83. } else if(patList[pattern] == PATTERNINDEX_INVALID)
  84. {
  85. // New pattern
  86. patList[pattern] = insertedPats++;
  87. const CPattern &pat = sndFile.Patterns[pattern];
  88. patternData += MPT_AFORMAT("Rows: {}\r\n")(pat.GetNumRows());
  89. std::string name = pat.GetName();
  90. if(!name.empty())
  91. {
  92. patternData += "Name: " + name + "\r\n";
  93. }
  94. if(pat.GetOverrideSignature())
  95. {
  96. patternData += MPT_AFORMAT("Signature: {}/{}\r\n")(pat.GetRowsPerBeat(), pat.GetRowsPerMeasure());
  97. }
  98. if(pat.HasTempoSwing())
  99. {
  100. patternData += "Swing: ";
  101. const TempoSwing &swing = pat.GetTempoSwing();
  102. for(size_t i = 0; i < swing.size(); i++)
  103. {
  104. if(i == 0)
  105. {
  106. patternData += MPT_AFORMAT("{}")(swing[i]);
  107. } else
  108. {
  109. patternData += MPT_AFORMAT(",{}")(swing[i]);
  110. }
  111. }
  112. patternData += "\r\n";
  113. }
  114. patternData += CreateClipboardString(sndFile, pattern, PatternRect(PatternCursor(), PatternCursor(sndFile.Patterns[pattern].GetNumRows() - 1, sndFile.GetNumChannels() - 1, PatternCursor::lastColumn)));
  115. }
  116. data += mpt::afmt::val(patList[pattern]);
  117. }
  118. }
  119. if(!onlyOrders)
  120. {
  121. data += "\r\n" + patternData;
  122. }
  123. if(instance.m_activeClipboard < instance.m_clipboards.size())
  124. {
  125. // Copy to internal clipboard
  126. CString desc = MPT_CFORMAT("{} {} ({} to {})")(last - first + 1, onlyOrders ? CString(_T("Orders")) : CString(_T("Patterns")), first, last);
  127. instance.m_clipboards[instance.m_activeClipboard] = {data, desc};
  128. }
  129. return ToSystemClipboard(data);
  130. }
  131. // Copy a pattern selection to both the system clipboard and the internal clipboard.
  132. bool PatternClipboard::Copy(const CSoundFile &sndFile, PATTERNINDEX pattern, PatternRect selection)
  133. {
  134. std::string data = CreateClipboardString(sndFile, pattern, selection);
  135. if(data.empty())
  136. return false;
  137. // Set up clipboard header
  138. data.insert(0, FormatClipboardHeader(sndFile));
  139. if(instance.m_activeClipboard < instance.m_clipboards.size())
  140. {
  141. // Copy to internal clipboard
  142. CString desc;
  143. desc.Format(_T("%u rows, %u channels (pattern %u)"), selection.GetNumRows(), selection.GetNumChannels(), pattern);
  144. instance.m_clipboards[instance.m_activeClipboard] = {data, desc};
  145. }
  146. return ToSystemClipboard(data);
  147. }
  148. // Copy a pattern or pattern channel to the internal pattern or channel clipboard.
  149. bool PatternClipboard::Copy(const CSoundFile &sndFile, PATTERNINDEX pattern, CHANNELINDEX channel)
  150. {
  151. if(!sndFile.Patterns.IsValidPat(pattern))
  152. return false;
  153. const bool patternCopy = (channel == CHANNELINDEX_INVALID);
  154. const CPattern &pat = sndFile.Patterns[pattern];
  155. PatternRect selection;
  156. if(patternCopy)
  157. selection = {PatternCursor(0, 0, PatternCursor::firstColumn), PatternCursor(pat.GetNumRows() - 1, pat.GetNumChannels() - 1, PatternCursor::lastColumn)};
  158. else
  159. selection = {PatternCursor(0, channel, PatternCursor::firstColumn), PatternCursor(pat.GetNumRows() - 1, channel, PatternCursor::lastColumn)};
  160. std::string data = CreateClipboardString(sndFile, pattern, selection);
  161. if(data.empty())
  162. return false;
  163. // Set up clipboard header
  164. data.insert(0, FormatClipboardHeader(sndFile));
  165. // Copy to internal clipboard
  166. (patternCopy ? instance.m_patternClipboard : instance.m_channelClipboard) = {data, {}};
  167. return true;
  168. }
  169. // Create the clipboard text for a pattern selection
  170. std::string PatternClipboard::CreateClipboardString(const CSoundFile &sndFile, PATTERNINDEX pattern, PatternRect selection)
  171. {
  172. if(!sndFile.Patterns.IsValidPat(pattern))
  173. return "";
  174. if(selection.GetStartColumn() == PatternCursor::paramColumn)
  175. {
  176. // Special case: If selection starts with a parameter column, extend it to include the effect letter as well.
  177. PatternCursor upper(selection.GetUpperLeft());
  178. upper.Move(0, 0, -1);
  179. selection = PatternRect(upper, selection.GetLowerRight());
  180. }
  181. const ROWINDEX startRow = selection.GetStartRow(), numRows = selection.GetNumRows();
  182. const CHANNELINDEX startChan = selection.GetStartChannel(), numChans = selection.GetNumChannels();
  183. std::string data;
  184. data.reserve(numRows * (numChans * 12 + 2));
  185. for(ROWINDEX row = 0; row < numRows; row++)
  186. {
  187. if(row + startRow >= sndFile.Patterns[pattern].GetNumRows())
  188. break;
  189. const ModCommand *m = sndFile.Patterns[pattern].GetpModCommand(row + startRow, startChan);
  190. for(CHANNELINDEX chn = 0; chn < numChans; chn++, m++)
  191. {
  192. PatternCursor cursor(0, startChan + chn);
  193. data += '|';
  194. // Note
  195. if(selection.ContainsHorizontal(cursor))
  196. {
  197. if(m->IsNote())
  198. {
  199. // Need to guarantee that sharps are used for the clipboard.
  200. data += mpt::ToCharset(mpt::Charset::Locale, mpt::ustring(NoteNamesSharp[(m->note - NOTE_MIN) % 12]));
  201. data += ('0' + (m->note - NOTE_MIN) / 12);
  202. } else
  203. {
  204. data += mpt::ToCharset(mpt::Charset::Locale, sndFile.GetNoteName(m->note));
  205. }
  206. } else
  207. {
  208. // No note
  209. data += " ";
  210. }
  211. // Instrument
  212. cursor.Move(0, 0, 1);
  213. if(selection.ContainsHorizontal(cursor))
  214. {
  215. if(m->instr)
  216. {
  217. data += ('0' + (m->instr / 10));
  218. data += ('0' + (m->instr % 10));
  219. } else
  220. {
  221. data += "..";
  222. }
  223. } else
  224. {
  225. data += " ";
  226. }
  227. // Volume
  228. cursor.Move(0, 0, 1);
  229. if(selection.ContainsHorizontal(cursor))
  230. {
  231. if(m->IsPcNote())
  232. {
  233. data += mpt::afmt::dec0<3>(m->GetValueVolCol());
  234. }
  235. else
  236. {
  237. if(m->volcmd != VOLCMD_NONE && m->vol <= 99)
  238. {
  239. data += sndFile.GetModSpecifications().GetVolEffectLetter(m->volcmd);
  240. data += mpt::afmt::dec0<2>(m->vol);
  241. } else
  242. {
  243. data += "...";
  244. }
  245. }
  246. } else
  247. {
  248. data += " ";
  249. }
  250. // Effect
  251. cursor.Move(0, 0, 1);
  252. if(selection.ContainsHorizontal(cursor))
  253. {
  254. if(m->IsPcNote())
  255. {
  256. data += mpt::afmt::dec0<3>(m->GetValueEffectCol());
  257. }
  258. else
  259. {
  260. if(m->command != CMD_NONE)
  261. {
  262. data += sndFile.GetModSpecifications().GetEffectLetter(m->command);
  263. } else
  264. {
  265. data += '.';
  266. }
  267. if(m->param != 0 && m->command != CMD_NONE)
  268. {
  269. data += mpt::afmt::HEX0<2>(m->param);
  270. } else
  271. {
  272. data += "..";
  273. }
  274. }
  275. } else
  276. {
  277. data += " ";
  278. }
  279. }
  280. // Next Row
  281. data += "\r\n";
  282. }
  283. return data;
  284. }
  285. // Try pasting a pattern selection from the system clipboard.
  286. bool PatternClipboard::Paste(CSoundFile &sndFile, PatternEditPos &pastePos, PasteModes mode, PatternRect &pasteRect, bool &orderChanged)
  287. {
  288. std::string data;
  289. if(!FromSystemClipboard(data) || !HandlePaste(sndFile, pastePos, mode, data, pasteRect, orderChanged))
  290. {
  291. // Fall back to internal clipboard if there's no valid pattern data in the system clipboard.
  292. return Paste(sndFile, pastePos, mode, pasteRect, instance.m_activeClipboard, orderChanged);
  293. }
  294. return true;
  295. }
  296. // Try pasting a pattern selection from an internal clipboard.
  297. bool PatternClipboard::Paste(CSoundFile &sndFile, PatternEditPos &pastePos, PasteModes mode, PatternRect &pasteRect, clipindex_t internalClipboard, bool &orderChanged)
  298. {
  299. if(internalClipboard >= instance.m_clipboards.size())
  300. return false;
  301. return HandlePaste(sndFile, pastePos, mode, instance.m_clipboards[internalClipboard].content, pasteRect, orderChanged);
  302. }
  303. // Paste from pattern or channel clipboard.
  304. bool PatternClipboard::Paste(CSoundFile &sndFile, PATTERNINDEX pattern, CHANNELINDEX channel)
  305. {
  306. PatternEditPos pastePos{0, ORDERINDEX_INVALID, pattern, channel != CHANNELINDEX_INVALID ? channel : CHANNELINDEX(0)};
  307. PatternRect pasteRect;
  308. bool orderChanged = false;
  309. return HandlePaste(sndFile, pastePos, pmOverwrite, (channel == CHANNELINDEX_INVALID ? instance.m_patternClipboard : instance.m_channelClipboard).content, pasteRect, orderChanged);
  310. }
  311. // Parse clipboard string and perform the pasting operation.
  312. bool PatternClipboard::HandlePaste(CSoundFile &sndFile, PatternEditPos &pastePos, PasteModes mode, const std::string &data, PatternRect &pasteRect, bool &orderChanged)
  313. {
  314. const std::string whitespace(" \n\r\t");
  315. PATTERNINDEX pattern = pastePos.pattern;
  316. ORDERINDEX &curOrder = pastePos.order;
  317. orderChanged = false;
  318. if(sndFile.GetpModDoc() == nullptr)
  319. return false;
  320. CModDoc &modDoc = *(sndFile.GetpModDoc());
  321. ModSequence &order = sndFile.Order();
  322. bool success = false;
  323. bool prepareUndo = true; // Prepare pattern for undo next time
  324. bool firstUndo = true; // For chaining undos (see overflow / multi-pattern paste)
  325. // Search for signature
  326. std::string::size_type pos, startPos = 0;
  327. MODTYPE pasteFormat = MOD_TYPE_NONE;
  328. while(pasteFormat == MOD_TYPE_NONE && (startPos = data.find("ModPlug Tracker ", startPos)) != std::string::npos)
  329. {
  330. startPos += 16;
  331. // Check paste format
  332. const std::string format = mpt::ToUpperCaseAscii(mpt::trim(data.substr(startPos, 3)));
  333. for(const auto &spec : ModSpecs::Collection)
  334. {
  335. if(format == GetFileExtension(spec->fileExtension, false))
  336. {
  337. pasteFormat = spec->internalType;
  338. startPos += 3;
  339. break;
  340. }
  341. }
  342. }
  343. // What is this I don't even
  344. if(startPos == std::string::npos)
  345. return false;
  346. // Skip whitespaces
  347. startPos = data.find_first_not_of(whitespace, startPos);
  348. if(startPos == std::string::npos)
  349. return false;
  350. // Multi-order stuff
  351. std::vector<PATTERNINDEX> patList;
  352. // Multi-order mix-paste stuff
  353. std::vector<ORDERINDEX> ordList;
  354. std::vector<std::string::size_type> patOffset;
  355. enum { kSinglePaste, kMultiInsert, kMultiOverwrite } patternMode = kSinglePaste;
  356. if(data.substr(startPos, 8) == "Orders: ")
  357. {
  358. // Pasting several patterns at once.
  359. patternMode = (mode == pmOverwrite) ? kMultiInsert : kMultiOverwrite;
  360. // Put new patterns after current pattern, if it exists
  361. if(order.IsValidPat(curOrder) && patternMode == kMultiInsert)
  362. curOrder++;
  363. pos = startPos + 8;
  364. startPos = data.find('\n', pos);
  365. ORDERINDEX writeOrder = curOrder;
  366. const bool onlyOrders = (startPos == std::string::npos);
  367. if(onlyOrders)
  368. {
  369. // Only create order list, no patterns
  370. startPos = data.size();
  371. } else
  372. {
  373. startPos++;
  374. }
  375. while(pos < startPos && pos != std::string::npos)
  376. {
  377. PATTERNINDEX insertPat;
  378. auto curPos = pos;
  379. // Next order item, please
  380. pos = data.find(',', pos + 1);
  381. if(pos != std::string::npos)
  382. pos++;
  383. if(data[curPos] == '+')
  384. {
  385. insertPat = order.GetIgnoreIndex();
  386. } else if(data[curPos] == '-')
  387. {
  388. insertPat = order.GetInvalidPatIndex();
  389. } else
  390. {
  391. insertPat = ConvertStrTo<PATTERNINDEX>(data.substr(curPos, 10));
  392. if(patternMode == kMultiOverwrite)
  393. {
  394. // We only want the order of pasted patterns now, do not create any new patterns
  395. ordList.push_back(insertPat);
  396. continue;
  397. }
  398. if(insertPat < patList.size() && patList[insertPat] != PATTERNINDEX_INVALID)
  399. {
  400. // Duplicate pattern
  401. insertPat = patList[insertPat];
  402. } else if(!onlyOrders)
  403. {
  404. // New pattern
  405. if(insertPat >= patList.size())
  406. {
  407. patList.resize(insertPat + 1, PATTERNINDEX_INVALID);
  408. }
  409. patList[insertPat] = modDoc.InsertPattern(64);
  410. insertPat = patList[insertPat];
  411. }
  412. }
  413. if((insertPat == order.GetIgnoreIndex() && !sndFile.GetModSpecifications().hasIgnoreIndex)
  414. || (insertPat == order.GetInvalidPatIndex() && !sndFile.GetModSpecifications().hasStopIndex)
  415. || insertPat == PATTERNINDEX_INVALID
  416. || patternMode == kMultiOverwrite)
  417. {
  418. continue;
  419. }
  420. if(order.insert(writeOrder, 1) == 0)
  421. {
  422. break;
  423. }
  424. order[writeOrder++] = insertPat;
  425. orderChanged = true;
  426. }
  427. if(patternMode == kMultiInsert)
  428. {
  429. if(!patList.empty())
  430. {
  431. // First pattern we're going to paste in.
  432. pattern = patList[0];
  433. }
  434. // We already modified the order list...
  435. success = true;
  436. pastePos.pattern = pattern;
  437. pastePos.row = 0;
  438. pastePos.channel = 0;
  439. } else
  440. {
  441. if(ordList.empty())
  442. return success;
  443. // Find pattern offsets
  444. pos = startPos;
  445. patOffset.reserve(ordList.size());
  446. bool patStart = false;
  447. while((pos = data.find_first_not_of(whitespace, pos)) != std::string::npos)
  448. {
  449. auto eol = data.find('\n', pos + 1);
  450. if(eol == std::string::npos)
  451. eol = data.size();
  452. if(data.substr(pos, 6) == "Rows: ")
  453. {
  454. patStart = true;
  455. } else if(data.substr(pos, 1) == "|" && patStart)
  456. {
  457. patOffset.push_back(pos);
  458. patStart = false;
  459. }
  460. pos = eol;
  461. }
  462. if(patOffset.empty())
  463. return success;
  464. startPos = patOffset[0];
  465. }
  466. }
  467. size_t curPattern = 0; // Currently pasted pattern for multi-paste
  468. ROWINDEX startRow = pastePos.row;
  469. ROWINDEX curRow = startRow;
  470. CHANNELINDEX startChan = pastePos.channel, col;
  471. // Can we actually paste at this position?
  472. if(!sndFile.Patterns.IsValidPat(pattern) || startRow >= sndFile.Patterns[pattern].GetNumRows() || startChan >= sndFile.GetNumChannels())
  473. {
  474. return success;
  475. }
  476. const CModSpecifications &sourceSpecs = CSoundFile::GetModSpecifications(pasteFormat);
  477. const bool overflowPaste = (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OVERFLOWPASTE) && mode != pmPasteFlood && mode != pmPushForward && patternMode != kMultiInsert && curOrder != ORDERINDEX_INVALID;
  478. const bool doITStyleMix = (mode == pmMixPasteIT);
  479. const bool doMixPaste = (mode == pmMixPaste) || doITStyleMix;
  480. const bool clipboardHasS3MCommands = (pasteFormat & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_S3M));
  481. const bool insertNewPatterns = overflowPaste && (patternMode == kMultiOverwrite);
  482. PatternCursor startPoint(startRow, startChan, PatternCursor::lastColumn), endPoint(startRow, startChan, PatternCursor::firstColumn);
  483. ModCommand *patData = sndFile.Patterns[pattern].GetpModCommand(startRow, 0);
  484. auto multiPastePos = ordList.cbegin();
  485. pos = startPos;
  486. while(curRow < sndFile.Patterns[pattern].GetNumRows() || overflowPaste || patternMode == kMultiInsert)
  487. {
  488. // Parse next line
  489. pos = data.find_first_not_of(whitespace, pos);
  490. if(pos == std::string::npos)
  491. {
  492. // End of paste
  493. if(mode == pmPasteFlood && curRow != startRow && curRow < sndFile.Patterns[pattern].GetNumRows())
  494. {
  495. // Restarting pasting from beginning.
  496. pos = startPos;
  497. multiPastePos = ordList.cbegin();
  498. continue;
  499. } else
  500. {
  501. // Prevent infinite loop with malformed clipboard data.
  502. break;
  503. }
  504. }
  505. auto eol = data.find('\n', pos + 1);
  506. if(eol == std::string::npos)
  507. eol = data.size();
  508. // Handle multi-paste: Read pattern information
  509. if(patternMode != kSinglePaste)
  510. {
  511. // Parse pattern header lines
  512. bool parsedLine = true;
  513. if(data.substr(pos, 6) == "Rows: ")
  514. {
  515. pos += 6;
  516. // Advance to next pattern
  517. if(patternMode == kMultiOverwrite)
  518. {
  519. // 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.
  520. multiPastePos++;
  521. if(multiPastePos == ordList.cend() || *multiPastePos >= patOffset.size())
  522. pos = data.size();
  523. else
  524. pos = patOffset[*multiPastePos];
  525. continue;
  526. }
  527. // Otherwise, parse this pattern header normally.
  528. do
  529. {
  530. if(curPattern >= patList.size())
  531. {
  532. return success;
  533. }
  534. pattern = patList[curPattern++];
  535. } while (pattern == PATTERNINDEX_INVALID);
  536. ROWINDEX numRows = ConvertStrTo<ROWINDEX>(data.substr(pos, 10));
  537. sndFile.Patterns[pattern].Resize(numRows);
  538. patData = sndFile.Patterns[pattern].GetpModCommand(0, 0);
  539. curRow = 0;
  540. prepareUndo = true;
  541. } else if(data.substr(pos, 6) == "Name: ")
  542. {
  543. pos += 6;
  544. auto name = mpt::trim_right(data.substr(pos, eol - pos - 1));
  545. sndFile.Patterns[pattern].SetName(name);
  546. } else if(data.substr(pos, 11) == "Signature: ")
  547. {
  548. pos += 11;
  549. auto pos2 = data.find("/", pos + 1);
  550. if(pos2 != std::string::npos)
  551. {
  552. pos2++;
  553. ROWINDEX rpb = ConvertStrTo<ROWINDEX>(data.substr(pos, pos2 - pos));
  554. ROWINDEX rpm = ConvertStrTo<ROWINDEX>(data.substr(pos2, eol - pos2));
  555. sndFile.Patterns[pattern].SetSignature(rpb, rpm);
  556. }
  557. } else if(data.substr(pos, 7) == "Swing: ")
  558. {
  559. pos += 7;
  560. TempoSwing swing;
  561. swing.resize(sndFile.Patterns[pattern].GetRowsPerBeat(), TempoSwing::Unity);
  562. size_t i = 0;
  563. while(pos != std::string::npos && pos < eol && i < swing.size())
  564. {
  565. swing[i++] = ConvertStrTo<TempoSwing::value_type>(data.substr(pos, eol - pos));
  566. pos = data.find(',', pos + 1);
  567. if(pos != std::string::npos)
  568. pos++;
  569. }
  570. sndFile.Patterns[pattern].SetTempoSwing(swing);
  571. } else
  572. {
  573. parsedLine = false;
  574. }
  575. if(parsedLine)
  576. {
  577. pos = eol;
  578. continue;
  579. }
  580. }
  581. if(data[pos] != '|')
  582. {
  583. // Not a valid line?
  584. pos = eol;
  585. continue;
  586. }
  587. if(overflowPaste)
  588. {
  589. // Handle overflow paste. Continue pasting in next pattern if enabled.
  590. // If Paste Flood is enabled, this won't be called due to obvious reasons.
  591. while(curRow >= sndFile.Patterns[pattern].GetNumRows())
  592. {
  593. curRow = 0;
  594. ORDERINDEX nextOrder = order.GetNextOrderIgnoringSkips(curOrder);
  595. if(nextOrder <= curOrder || !order.IsValidPat(nextOrder))
  596. {
  597. PATTERNINDEX newPat;
  598. if(!insertNewPatterns
  599. || curOrder >= sndFile.GetModSpecifications().ordersMax
  600. || (newPat = modDoc.InsertPattern(sndFile.Patterns[pattern].GetNumRows())) == PATTERNINDEX_INVALID
  601. || order.insert(curOrder + 1, 1, newPat) == 0)
  602. {
  603. return success;
  604. }
  605. orderChanged = true;
  606. nextOrder = curOrder + 1;
  607. }
  608. pattern = order[nextOrder];
  609. if(!sndFile.Patterns.IsValidPat(pattern)) return success;
  610. patData = sndFile.Patterns[pattern].GetpModCommand(0, 0);
  611. curOrder = nextOrder;
  612. prepareUndo = true;
  613. startRow = 0;
  614. }
  615. }
  616. success = true;
  617. col = startChan;
  618. // Paste columns
  619. while((pos + 11 < data.size()) && (data[pos] == '|'))
  620. {
  621. pos++;
  622. // Handle pasting large pattern into smaller pattern (e.g. 128-row pattern into MOD, which only allows 64 rows)
  623. ModCommand dummy;
  624. ModCommand &m = curRow < sndFile.Patterns[pattern].GetNumRows() ? patData[col] : dummy;
  625. // Check valid paste condition. Paste will be skipped if
  626. // - col is not a valid channelindex or
  627. // - doing mix paste and paste destination modcommand is a PCnote or
  628. // - doing mix paste and trying to paste PCnote on non-empty modcommand.
  629. const bool skipPaste =
  630. col >= sndFile.GetNumChannels() ||
  631. (doMixPaste && m.IsPcNote()) ||
  632. (doMixPaste && data[pos] == 'P' && !m.IsEmpty());
  633. if(skipPaste == false)
  634. {
  635. // Before changing anything in this pattern, we have to create an undo point.
  636. if(prepareUndo)
  637. {
  638. modDoc.GetPatternUndo().PrepareUndo(pattern, startChan, startRow, sndFile.GetNumChannels(), sndFile.Patterns[pattern].GetNumRows(), "Paste", !firstUndo);
  639. prepareUndo = false;
  640. firstUndo = false;
  641. }
  642. // ITSyle mixpaste requires that we keep a copy of the thing we are about to paste on
  643. // so that we can refer back to check if there was anything in e.g. the note column before we pasted.
  644. const ModCommand origModCmd = m;
  645. // push channel data below paste point first.
  646. if(mode == pmPushForward)
  647. {
  648. for(ROWINDEX pushRow = sndFile.Patterns[pattern].GetNumRows() - 1 - curRow; pushRow > 0; pushRow--)
  649. {
  650. patData[col + pushRow * sndFile.GetNumChannels()] = patData[col + (pushRow - 1) * sndFile.GetNumChannels()];
  651. }
  652. m.Clear();
  653. }
  654. PatternCursor::Columns firstCol = PatternCursor::lastColumn, lastCol = PatternCursor::firstColumn;
  655. // Note
  656. if(data[pos] != ' ' && (!doMixPaste || ((!doITStyleMix && origModCmd.note == NOTE_NONE) ||
  657. (doITStyleMix && origModCmd.note == NOTE_NONE && origModCmd.instr == 0 && origModCmd.volcmd == VOLCMD_NONE))))
  658. {
  659. firstCol = PatternCursor::noteColumn;
  660. m.note = NOTE_NONE;
  661. if(data[pos] == '=')
  662. m.note = NOTE_KEYOFF;
  663. else if(data[pos] == '^')
  664. m.note = NOTE_NOTECUT;
  665. else if(data[pos] == '~')
  666. m.note = NOTE_FADE;
  667. else if(data[pos] == 'P')
  668. {
  669. if(data[pos + 2] == 'S' || data[pos + 2] == 's')
  670. m.note = NOTE_PCS;
  671. else
  672. m.note = NOTE_PC;
  673. } else if (data[pos] != '.')
  674. {
  675. // Check note names
  676. for(uint8 i = 0; i < 12; i++)
  677. {
  678. if(data[pos] == NoteNamesSharp[i][0] && data[pos + 1] == NoteNamesSharp[i][1])
  679. {
  680. m.note = ModCommand::NOTE(i + NOTE_MIN);
  681. break;
  682. }
  683. }
  684. if(m.note != NOTE_NONE)
  685. {
  686. // Check octave
  687. m.note += (data[pos + 2] - '0') * 12;
  688. if(!m.IsNote())
  689. {
  690. // Invalid octave
  691. m.note = NOTE_NONE;
  692. }
  693. }
  694. }
  695. }
  696. // Instrument
  697. if(data[pos + 3] > ' ' && (!doMixPaste || ( (!doITStyleMix && origModCmd.instr == 0) ||
  698. (doITStyleMix && origModCmd.note == NOTE_NONE && origModCmd.instr == 0 && origModCmd.volcmd == VOLCMD_NONE) ) ))
  699. {
  700. firstCol = std::min(firstCol, PatternCursor::instrColumn);
  701. lastCol = std::max(lastCol, PatternCursor::instrColumn);
  702. if(data[pos + 3] >= '0' && data[pos + 3] <= ('0' + (MAX_INSTRUMENTS / 10)))
  703. {
  704. m.instr = (data[pos + 3] - '0') * 10 + (data[pos + 4] - '0');
  705. } else m.instr = 0;
  706. }
  707. // Volume
  708. if(data[pos + 5] > ' ' && (!doMixPaste || ((!doITStyleMix && origModCmd.volcmd == VOLCMD_NONE) ||
  709. (doITStyleMix && origModCmd.note == NOTE_NONE && origModCmd.instr == 0 && origModCmd.volcmd == VOLCMD_NONE))))
  710. {
  711. firstCol = std::min(firstCol, PatternCursor::volumeColumn);
  712. lastCol = std::max(lastCol, PatternCursor::volumeColumn);
  713. if(data[pos + 5] != '.')
  714. {
  715. if(m.IsPcNote())
  716. {
  717. m.SetValueVolCol(ConvertStrTo<uint16>(data.substr(pos + 5, 3)));
  718. } else
  719. {
  720. m.volcmd = VOLCMD_NONE;
  721. for(int i = VOLCMD_NONE + 1; i < MAX_VOLCMDS; i++)
  722. {
  723. const char cmd = sourceSpecs.GetVolEffectLetter(static_cast<VolumeCommand>(i));
  724. if(data[pos + 5] == cmd && cmd != '?')
  725. {
  726. m.volcmd = static_cast<VolumeCommand>(i);
  727. break;
  728. }
  729. }
  730. m.vol = (data[pos + 6] - '0') * 10 + (data[pos + 7] - '0');
  731. }
  732. } else
  733. {
  734. m.volcmd = VOLCMD_NONE;
  735. m.vol = 0;
  736. }
  737. }
  738. // Effect
  739. if(m.IsPcNote())
  740. {
  741. if(data[pos + 8] != '.' && data[pos + 8] > ' ')
  742. {
  743. firstCol = std::min(firstCol, PatternCursor::paramColumn);
  744. lastCol = std::max(lastCol, PatternCursor::paramColumn);
  745. m.SetValueEffectCol(ConvertStrTo<uint16>(data.substr(pos + 8, 3)));
  746. } else if(!origModCmd.IsPcNote())
  747. {
  748. // No value provided in clipboard
  749. if((m.command == CMD_MIDI || m.command == CMD_SMOOTHMIDI) && m.param < 128)
  750. m.SetValueEffectCol(static_cast<uint16>(Util::muldivr(m.param, ModCommand::maxColumnValue, 127)));
  751. else
  752. m.SetValueEffectCol(0);
  753. }
  754. } else
  755. {
  756. if(data[pos + 8] > ' ' && (!doMixPaste || ((!doITStyleMix && origModCmd.command == CMD_NONE) ||
  757. (doITStyleMix && origModCmd.command == CMD_NONE && origModCmd.param == 0))))
  758. {
  759. firstCol = std::min(firstCol, PatternCursor::effectColumn);
  760. lastCol = std::max(lastCol, PatternCursor::effectColumn);
  761. m.command = CMD_NONE;
  762. if(data[pos + 8] != '.')
  763. {
  764. for(int i = CMD_NONE + 1; i < MAX_EFFECTS; i++)
  765. {
  766. const char cmd = sourceSpecs.GetEffectLetter(static_cast<EffectCommand>(i));
  767. if(data[pos + 8] == cmd && cmd != '?')
  768. {
  769. m.command = static_cast<EffectCommand>(i);
  770. break;
  771. }
  772. }
  773. }
  774. }
  775. // Effect value
  776. if(data[pos + 9] > ' ' && (!doMixPaste || ((!doITStyleMix && (origModCmd.command == CMD_NONE || origModCmd.param == 0)) ||
  777. (doITStyleMix && origModCmd.command == CMD_NONE && origModCmd.param == 0))))
  778. {
  779. firstCol = std::min(firstCol, PatternCursor::paramColumn);
  780. lastCol = std::max(lastCol, PatternCursor::paramColumn);
  781. m.param = 0;
  782. if(data[pos + 9] != '.')
  783. {
  784. for(uint8 i = 0; i < 16; i++)
  785. {
  786. if(data[pos + 9] == szHexChar[i]) m.param |= (i << 4);
  787. if(data[pos + 10] == szHexChar[i]) m.param |= i;
  788. }
  789. }
  790. }
  791. // Speed / tempo command conversion
  792. if (sndFile.GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM))
  793. {
  794. switch(m.command)
  795. {
  796. case CMD_SPEED:
  797. case CMD_TEMPO:
  798. if(!clipboardHasS3MCommands)
  799. {
  800. if(m.param < 32)
  801. m.command = CMD_SPEED;
  802. else
  803. m.command = CMD_TEMPO;
  804. } else
  805. {
  806. if(m.command == CMD_SPEED && m.param >= 32)
  807. m.param = CMD_TEMPO;
  808. else if(m.command == CMD_TEMPO && m.param < 32)
  809. m.param = CMD_SPEED;
  810. }
  811. break;
  812. }
  813. } else
  814. {
  815. switch(m.command)
  816. {
  817. case CMD_SPEED:
  818. case CMD_TEMPO:
  819. if(!clipboardHasS3MCommands)
  820. {
  821. if(m.param < 32)
  822. m.command = CMD_SPEED;
  823. else
  824. m.command = CMD_TEMPO;
  825. }
  826. break;
  827. }
  828. }
  829. }
  830. // Convert some commands, if necessary. With mix paste convert only
  831. // if the original modcommand was empty as otherwise the unchanged parts
  832. // of the old modcommand would falsely be interpreted being of type
  833. // origFormat and ConvertCommand could change them.
  834. if(pasteFormat != sndFile.GetType() && (!doMixPaste || origModCmd.IsEmpty()))
  835. m.Convert(pasteFormat, sndFile.GetType(), sndFile);
  836. // Sanitize PC events
  837. if(m.IsPcNote())
  838. {
  839. m.SetValueEffectCol(std::min(m.GetValueEffectCol(), static_cast<decltype(m.GetValueEffectCol())>(ModCommand::maxColumnValue)));
  840. m.SetValueVolCol(std::min(m.GetValueVolCol(), static_cast<decltype(m.GetValueEffectCol())>(ModCommand::maxColumnValue)));
  841. }
  842. // Adjust pattern selection
  843. if(col == startChan) startPoint.SetColumn(startChan, firstCol);
  844. if(endPoint.CompareColumn(PatternCursor(0, col, lastCol)) < 0) endPoint.SetColumn(col, lastCol);
  845. if(curRow > endPoint.GetRow()) endPoint.SetRow(curRow);
  846. pasteRect = PatternRect(startPoint, endPoint);
  847. }
  848. pos += 11;
  849. col++;
  850. }
  851. // Next row
  852. patData += sndFile.GetNumChannels();
  853. curRow++;
  854. pos = eol;
  855. }
  856. return success;
  857. }
  858. // Copy one of the internal clipboards to the system clipboard.
  859. bool PatternClipboard::SelectClipboard(clipindex_t which)
  860. {
  861. instance.m_activeClipboard = which;
  862. return ToSystemClipboard(instance.m_clipboards[instance.m_activeClipboard]);
  863. }
  864. // Switch to the next internal clipboard.
  865. bool PatternClipboard::CycleForward()
  866. {
  867. instance.m_activeClipboard++;
  868. if(instance.m_activeClipboard >= instance.m_clipboards.size())
  869. instance.m_activeClipboard = 0;
  870. return SelectClipboard(instance.m_activeClipboard);
  871. }
  872. // Switch to the previous internal clipboard.
  873. bool PatternClipboard::CycleBackward()
  874. {
  875. if(instance.m_activeClipboard == 0)
  876. instance.m_activeClipboard = instance.m_clipboards.size() - 1;
  877. else
  878. instance.m_activeClipboard--;
  879. return SelectClipboard(instance.m_activeClipboard);
  880. }
  881. // Set the maximum number of internal clipboards.
  882. void PatternClipboard::SetClipboardSize(clipindex_t maxEntries)
  883. {
  884. instance.m_clipboards.resize(maxEntries, {"", _T("unused")});
  885. LimitMax(instance.m_activeClipboard, maxEntries - 1);
  886. }
  887. // Check whether patterns can be pasted from clipboard
  888. bool PatternClipboard::CanPaste()
  889. {
  890. return !!IsClipboardFormatAvailable(CF_TEXT);
  891. }
  892. // System-specific clipboard functions
  893. bool PatternClipboard::ToSystemClipboard(const std::string_view &data)
  894. {
  895. Clipboard clipboard(CF_TEXT, data.size() + 1);
  896. if(auto dst = clipboard.As<char>())
  897. {
  898. std::copy(data.begin(), data.end(), dst);
  899. dst[data.size()] = '\0';
  900. return true;
  901. }
  902. return false;
  903. }
  904. // System-specific clipboard functions
  905. bool PatternClipboard::FromSystemClipboard(std::string &data)
  906. {
  907. Clipboard clipboard(CF_TEXT);
  908. if(auto cbdata = clipboard.Get(); cbdata.data())
  909. {
  910. if(cbdata.size() > 0)
  911. data.assign(mpt::byte_cast<char *>(cbdata.data()), cbdata.size() - 1);
  912. return !data.empty();
  913. }
  914. return false;
  915. }
  916. BEGIN_MESSAGE_MAP(PatternClipboardDialog, ResizableDialog)
  917. ON_EN_UPDATE(IDC_EDIT1, &PatternClipboardDialog::OnNumClipboardsChanged)
  918. ON_LBN_SELCHANGE(IDC_LIST1, &PatternClipboardDialog::OnSelectClipboard)
  919. ON_LBN_DBLCLK(IDC_LIST1, &PatternClipboardDialog::OnEditName)
  920. END_MESSAGE_MAP()
  921. PatternClipboardDialog PatternClipboardDialog::instance;
  922. void PatternClipboardDialog::DoDataExchange(CDataExchange *pDX)
  923. {
  924. DDX_Control(pDX, IDC_SPIN1, m_numClipboardsSpin);
  925. DDX_Control(pDX, IDC_LIST1, m_clipList);
  926. }
  927. PatternClipboardDialog::PatternClipboardDialog() : m_editNameBox(*this)
  928. {
  929. }
  930. void PatternClipboardDialog::Show()
  931. {
  932. instance.m_isLocked = true;
  933. if(!instance.m_isCreated)
  934. {
  935. instance.Create(IDD_CLIPBOARD, CMainFrame::GetMainFrame());
  936. instance.m_numClipboardsSpin.SetRange(0, int16_max);
  937. }
  938. instance.SetDlgItemInt(IDC_EDIT1, mpt::saturate_cast<UINT>(PatternClipboard::GetClipboardSize()), FALSE);
  939. instance.m_isLocked = false;
  940. instance.m_isCreated = true;
  941. instance.UpdateList();
  942. 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));
  943. }
  944. void PatternClipboardDialog::OnNumClipboardsChanged()
  945. {
  946. if(m_isLocked)
  947. {
  948. return;
  949. }
  950. OnEndEdit();
  951. PatternClipboard::SetClipboardSize(GetDlgItemInt(IDC_EDIT1, nullptr, FALSE));
  952. UpdateList();
  953. }
  954. void PatternClipboardDialog::UpdateList()
  955. {
  956. if(instance.m_isLocked)
  957. {
  958. return;
  959. }
  960. instance.m_clipList.ResetContent();
  961. PatternClipboard::clipindex_t i = 0;
  962. for(const auto &clip : PatternClipboard::instance.m_clipboards)
  963. {
  964. const int item = instance.m_clipList.AddString(clip.description);
  965. instance.m_clipList.SetItemDataPtr(item, reinterpret_cast<void *>(i));
  966. if(PatternClipboard::instance.m_activeClipboard == i)
  967. {
  968. instance.m_clipList.SetCurSel(item);
  969. }
  970. i++;
  971. }
  972. }
  973. void PatternClipboardDialog::OnSelectClipboard()
  974. {
  975. if(m_isLocked)
  976. {
  977. return;
  978. }
  979. PatternClipboard::clipindex_t item = reinterpret_cast<PatternClipboard::clipindex_t>(m_clipList.GetItemDataPtr(m_clipList.GetCurSel()));
  980. PatternClipboard::SelectClipboard(item);
  981. OnEndEdit();
  982. }
  983. void PatternClipboardDialog::OnOK()
  984. {
  985. const CWnd *focus = GetFocus();
  986. if(focus == &m_editNameBox)
  987. {
  988. // User pressed enter in clipboard name edit box => cancel editing
  989. OnEndEdit();
  990. } else if(focus == &m_clipList)
  991. {
  992. // User pressed enter in the clipboard name list => start editing
  993. OnEditName();
  994. } else
  995. {
  996. ResizableDialog::OnOK();
  997. }
  998. }
  999. void PatternClipboardDialog::OnCancel()
  1000. {
  1001. if(GetFocus() == &m_editNameBox)
  1002. {
  1003. // User pressed enter in clipboard name edit box => just cancel editing
  1004. m_editNameBox.DestroyWindow();
  1005. return;
  1006. }
  1007. OnEndEdit(false);
  1008. m_isCreated = false;
  1009. m_isLocked = true;
  1010. RECT rect;
  1011. GetWindowRect(&rect);
  1012. m_posX = rect.left;
  1013. m_posY = rect.top;
  1014. DestroyWindow();
  1015. }
  1016. void PatternClipboardDialog::OnEditName()
  1017. {
  1018. OnEndEdit();
  1019. const int sel = m_clipList.GetCurSel();
  1020. if(sel == LB_ERR)
  1021. {
  1022. return;
  1023. }
  1024. CRect rect;
  1025. m_clipList.GetItemRect(sel, rect);
  1026. rect.InflateRect(0, 2, 0, 2);
  1027. // Create the edit control
  1028. m_editNameBox.Create(WS_VISIBLE | WS_CHILD | WS_BORDER | ES_LEFT | ES_AUTOHSCROLL, rect, &m_clipList, 1);
  1029. m_editNameBox.SetFont(m_clipList.GetFont());
  1030. m_editNameBox.SetWindowText(PatternClipboard::instance.m_clipboards[sel].description);
  1031. m_editNameBox.SetSel(0, -1, TRUE);
  1032. m_editNameBox.SetFocus();
  1033. SetWindowLongPtr(m_editNameBox.m_hWnd, GWLP_USERDATA, (LONG_PTR)m_clipList.GetItemDataPtr(sel));
  1034. }
  1035. void PatternClipboardDialog::OnEndEdit(bool apply)
  1036. {
  1037. if(m_editNameBox.GetSafeHwnd() == NULL)
  1038. {
  1039. return;
  1040. }
  1041. if(apply)
  1042. {
  1043. size_t sel = GetWindowLongPtr(m_editNameBox.m_hWnd, GWLP_USERDATA);
  1044. if(sel >= PatternClipboard::instance.m_clipboards.size())
  1045. {
  1046. // What happened?
  1047. return;
  1048. }
  1049. CString newName;
  1050. m_editNameBox.GetWindowText(newName);
  1051. PatternClipboard::instance.m_clipboards[sel].description = newName;
  1052. }
  1053. SetWindowLongPtr(m_editNameBox.m_hWnd, GWLP_USERDATA, LONG_PTR(-1));
  1054. m_editNameBox.DestroyWindow();
  1055. UpdateList();
  1056. }
  1057. BEGIN_MESSAGE_MAP(PatternClipboardDialog::CInlineEdit, CEdit)
  1058. ON_WM_KILLFOCUS()
  1059. END_MESSAGE_MAP()
  1060. PatternClipboardDialog::CInlineEdit::CInlineEdit(PatternClipboardDialog &dlg) : parent(dlg)
  1061. {
  1062. }
  1063. void PatternClipboardDialog::CInlineEdit::OnKillFocus(CWnd *newWnd)
  1064. {
  1065. parent.OnEndEdit(true);
  1066. CEdit::OnKillFocus(newWnd);
  1067. }
  1068. OPENMPT_NAMESPACE_END