openMSX
ImGuiDiskManipulator.cc
Go to the documentation of this file.
2
3#include "CustomFont.h"
4#include "ImGuiCpp.hh"
5#include "ImGuiManager.hh"
6#include "ImGuiMedia.hh"
7#include "ImGuiOpenFile.hh"
8#include "ImGuiUtils.hh"
9
10#include "DiskChanger.hh"
11#include "DiskImageUtils.hh"
12#include "DiskManipulator.hh"
13#include "DiskName.hh"
14#include "FileOperations.hh"
15#include "HD.hh"
16#include "MSXtar.hh"
17
18#include "Date.hh"
19#include "foreach_file.hh"
20#include "hash_set.hh"
21#include "one_of.hh"
22#include "stl.hh"
23#include "unreachable.hh"
24#include "view.hh"
25#include "xxhash.hh"
26
27#include <imgui_stdlib.h>
28
29#include <algorithm>
30#include <cassert>
31
32namespace openmsx {
33
34using namespace std::literals;
35
36DiskContainer* ImGuiDiskManipulator::getDrive()
37{
38 const auto& diskManipulator = manager.getReactor().getDiskManipulator();
39 return diskManipulator.getDrive(selectedDrive);
40}
41
42std::optional<DiskManipulator::DriveAndPartition> ImGuiDiskManipulator::getDriveAndDisk()
43{
44 const auto& diskManipulator = manager.getReactor().getDiskManipulator();
45 return diskManipulator.getDriveAndDisk(selectedDrive);
46}
47
48std::optional<ImGuiDiskManipulator::DrivePartitionTar> ImGuiDiskManipulator::getMsxStuff()
49{
50 auto dd = getDriveAndDisk();
51 if (!dd) return {};
52 auto& [drive, disk] = *dd;
53 try {
54 auto tar = std::make_unique<MSXtar>(*disk, manager.getReactor().getMsxChar2Unicode());
55 return DrivePartitionTar{drive, std::move(disk), std::move(tar)};
56 } catch (MSXException&) {
57 // e.g. triggers when trying to parse a partition table as a FAT-disk
58 return {};
59 }
60}
61
62bool ImGuiDiskManipulator::isValidMsxDirectory(DrivePartitionTar& stuff, const std::string& dir) const
63{
64 assert(dir.starts_with('/'));
65 try {
66 stuff.tar->chdir(dir);
67 return true;
68 } catch (MSXException&) {
69 return false;
70 }
71}
72
73std::vector<ImGuiDiskManipulator::FileInfo> ImGuiDiskManipulator::dirMSX(DrivePartitionTar& stuff)
74{
75 std::vector<FileInfo> result;
76 // most likely nothing changed, and then we have the same number of entries
77 result.reserve(msxFileCache.size());
78
79 try {
80 stuff.tar->chdir(msxDir);
81 } catch (MSXException&) {
82 msxDir = "/";
83 editMsxDir = "/";
84 stuff.tar->chdir(msxDir);
85 }
86
87 auto dir = stuff.tar->dirRaw();
88 auto num = dir.size();
89 for (unsigned i = 0; i < num; ++i) {
90 auto entry = dir.getListIndexUnchecked(i);
91 FileInfo info;
92 info.attrib = MSXDirEntry::AttribValue(uint8_t(entry.getListIndexUnchecked(1).getOptionalInt().value_or(0)));
93 if (info.attrib & MSXDirEntry::Attrib::VOLUME) continue; // skip
94 info.filename = std::string(entry.getListIndexUnchecked(0).getString());
95 if (info.filename == one_of(".", "..")) continue; // skip
96 info.modified = entry.getListIndexUnchecked(2).getOptionalInt().value_or(0);
97 info.size = entry.getListIndexUnchecked(3).getOptionalInt().value_or(0);
98 info.isDirectory = bool(info.attrib & MSXDirEntry::Attrib::DIRECTORY);
99 if (info.isDirectory) info.size = size_t(-1);
100 result.push_back(std::move(info));
101 }
102
103 return result;
104}
105
106void ImGuiDiskManipulator::refreshMsx(DrivePartitionTar& stuff)
107{
108 auto newFiles = dirMSX(stuff);
109 if (newFiles.size() != msxFileCache.size()) msxLastClick = -1;
110
111 // copy 'isSelected' from 'msxFileCache' to 'newFiles'
113 for (const auto& oldFile : msxFileCache) {
114 if (oldFile.isSelected) selected.insert(oldFile.filename);
115 }
116 if (!selected.empty()) {
117 for (auto& newFile : newFiles) {
118 if (selected.contains(newFile.filename)) newFile.isSelected = true;
119 }
120 }
121
122 std::swap(newFiles, msxFileCache);
123}
124
125void ImGuiDiskManipulator::refreshHost()
126{
127 if (hostDir.empty() || !FileOperations::isDirectory(hostDir)) {
129 editHostDir = hostDir;
130 }
131
132 hostFileCache.clear();
133 auto fileAction = [&](const std::string& /*fullPath*/, std::string_view filename, const FileOperations::Stat& st) {
134 FileInfo info;
135 info.filename = std::string{filename};
136 info.size = st.st_size;
137 info.modified = st.st_mtime;
138 info.isDirectory = false;
139 hostFileCache.push_back(std::move(info));
140 };
141 auto directoryAction = [&](const std::string& /*fullPath*/, std::string_view filename, const FileOperations::Stat& st) {
142 FileInfo info;
143 info.filename = std::string{filename};
144 info.size = size_t(-1); // not shown, but used for sorting
145 info.modified = st.st_mtime;
146 info.isDirectory = true;
147 hostFileCache.push_back(std::move(info));
148 };
149 foreach_file_and_directory(hostDir, fileAction, directoryAction);
150
151 hostLastClick = -1;
152 hostNeedRefresh = false;
153 hostForceSort = true;
154}
155
156void ImGuiDiskManipulator::checkSort(std::vector<FileInfo>& files, bool& forceSort) const
157{
158 auto* sortSpecs = ImGui::TableGetSortSpecs();
159 if (!forceSort && !sortSpecs->SpecsDirty) return;
160
161 forceSort = false;
162 sortSpecs->SpecsDirty = false;
163 assert(sortSpecs->SpecsCount == 1);
164 assert(sortSpecs->Specs);
165 assert(sortSpecs->Specs->SortOrder == 0);
166
167 switch (sortSpecs->Specs->ColumnIndex) {
168 case 0: // filename
169 sortUpDown_String(files, sortSpecs, &FileInfo::filename);
170 break;
171 case 1: // size
172 sortUpDown_T(files, sortSpecs, &FileInfo::size);
173 break;
174 case 2: // modified
175 sortUpDown_T(files, sortSpecs, &FileInfo::modified);
176 break;
177 case 3: // attrib
178 sortUpDown_T(files, sortSpecs, &FileInfo::attrib);
179 break;
180 default:
182 }
183}
184
185ImGuiDiskManipulator::Action ImGuiDiskManipulator::drawTable(
186 std::vector<FileInfo>& files, int& lastClickIdx, bool& forceSort, bool msxSide) const
187{
188 auto clearSelection = [&]{
189 for (auto& file : files) file.isSelected = false;
190 };
191 auto selectRange = [&](size_t curr, size_t last) {
192 assert(!files.empty());
193 assert(curr < files.size());
194 if (last >= files.size()) last = files.size() - 1;
195
196 auto begin = std::min(curr, last);
197 auto end = std::max(curr, last);
198 for (auto i = begin; i <= end; ++i) {
199 files[i].isSelected = true;
200 }
201 };
202
203 checkSort(files, forceSort);
204
205 Action result = Nop{};
206 im::ListClipperID(files.size(), [&](int i) {
207 auto& file = files[i];
208 if (ImGui::TableNextColumn()) { // filename
209 auto pos = ImGui::GetCursorPos();
210 if (ImGui::Selectable("##row", file.isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap | ImGuiSelectableFlags_AllowDoubleClick)) {
211 if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
212 result = ChangeDir{file.filename};
213 } else {
214 bool ctrl = ImGui::IsKeyDown(ImGuiKey_LeftCtrl ) || ImGui::IsKeyDown(ImGuiKey_RightCtrl);
215 bool shiftKey = ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift);
216 bool shift = shiftKey && (lastClickIdx != -1);
217 if (ctrl && shift) {
218 selectRange(i, lastClickIdx);
219 } else if (ctrl) {
220 file.isSelected = !file.isSelected;
221 lastClickIdx = i;
222 } else if (shift) {
223 clearSelection();
224 selectRange(i, lastClickIdx);
225 } else {
226 clearSelection();
227 file.isSelected = true;
228 lastClickIdx = i;
229 }
230 }
231 }
232 if (msxSide && ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Right)) {
233 ImGui::OpenPopup("table-context");
234 }
235 im::Popup("table-context", [&]{
236 ImGui::TextUnformatted(file.isDirectory ? "Directory:" : "File:");
237 ImGui::SameLine();
238 ImGui::TextUnformatted(file.filename);
239 ImGui::Separator();
240
241 if (ImGui::Selectable("Delete")) {
242 result = Delete{file.filename};
243 }
244 if (ImGui::Selectable("Rename ...")) {
245 result = Rename{file.filename};
246 }
247 });
248
249 ImGui::SetCursorPos(pos);
250
251 ImGui::Text("%s %s",
252 file.isDirectory ? ICON_IGFD_FOLDER_OPEN : ICON_IGFD_FILE,
253 file.filename.c_str());
254 }
255 if (ImGui::TableNextColumn()) { // size
256 if (!file.isDirectory) {
257 ImGui::Text("%zu", file.size);
258 }
259 }
260 if (ImGui::TableNextColumn()) { // modified
262 }
263 if (msxSide && ImGui::TableNextColumn()) { // attrib
265 }
266 });
267 return result;
268}
269
270[[nodiscard]] static std::string driveDisplayName(std::string_view drive)
271{
272 if (drive == "virtual_drive") return "Virtual drive";
273 std::string result;
274 if (drive.size() >= 5 && drive.starts_with("disk")) {
275 result = strCat("Disk drive ", char('A' + drive[4] - 'a'));
276 drive.remove_prefix(5);
277 } else if (drive.size() >= 3 && drive.starts_with("hd")) {
278 result = strCat("Hard disk ", char('A' + drive[2] - 'a'));
279 drive.remove_prefix(3);
280 } else {
281 return ""; // not supported
282 }
283 if (drive.empty()) return result; // Ok, no partition specified
284 auto num = StringOp::stringToBase<10, unsigned>(drive);
285 if (!num) return ""; // invalid format
286 strAppend(result, ", partition ", *num);
287 return result;
288}
289
290std::string ImGuiDiskManipulator::getDiskImageName()
291{
292 auto dd = getDriveAndDisk();
293 if (!dd) return "";
294 const auto& [drive, disk] = *dd;
295 if (const auto* dr = dynamic_cast<const DiskChanger*>(drive)) {
296 return dr->getDiskName().getResolved();
297 } else if (const auto* hd = dynamic_cast<const HD*>(drive)) {
298 return hd->getImageName().getResolved();
299 } else {
300 return "";
301 }
302}
303
304void ImGuiDiskManipulator::paint(MSXMotherBoard* /*motherBoard*/)
305{
306 if (!show) return;
307
308 auto stuff = getMsxStuff();
309 if (stuff) {
310 refreshMsx(*stuff);
311 } else {
312 msxFileCache.clear();
313 }
314 if (hostNeedRefresh) {
315 refreshHost();
316 }
317
318 bool renameMsxEntry = false;
319 bool createMsxDir = false;
320 bool createHostDir = false;
321 bool createDiskImage = false;
322 bool openCheckTransfer = false;
323
324 const auto& style = ImGui::GetStyle();
325 ImGui::SetNextWindowSize(gl::vec2{70, 45} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
326 im::Window("Disk Manipulator", &show, [&]{
327 auto availableSize = ImGui::GetContentRegionAvail();
328 auto tSize = ImGui::CalcTextSize(">>"sv);
329 auto bWidth = 2.0f * (style.ItemSpacing.x + style.FramePadding.x) + tSize.x;
330 auto cWidth = (availableSize.x - bWidth) * 0.5f;
331
332 bool writable = stuff && !stuff->disk->isWriteProtected();
333
334 int tableFlags = ImGuiTableFlags_RowBg |
335 ImGuiTableFlags_BordersV |
336 ImGuiTableFlags_BordersOuter |
337 ImGuiTableFlags_ScrollY |
338 ImGuiTableFlags_Resizable |
339 ImGuiTableFlags_Reorderable |
340 ImGuiTableFlags_Hideable |
341 ImGuiTableFlags_Sortable |
342 ImGuiTableFlags_ContextMenuInBody |
343 ImGuiTableFlags_ScrollX;
344 im::Child("##msx", {cWidth, 0.0f}, [&]{
345 auto b2Width = 2.0f * style.ItemSpacing.x + 4.0f * style.FramePadding.x +
346 ImGui::CalcTextSize(ICON_IGFD_ADD ICON_IGFD_FOLDER_OPEN).x;
347 ImGui::SetNextItemWidth(-b2Width);
348 bool driveOk = false;
349 im::Combo("##drive", driveDisplayName(selectedDrive).c_str(), [&]{
350 const auto& diskManipulator = manager.getReactor().getDiskManipulator();
351 for (auto drives = diskManipulator.getDriveNamesForCurrentMachine();
352 const auto& drive : drives) {
353 if (selectedDrive == drive) driveOk = true;
354 auto display = driveDisplayName(drive);
355 if (display.empty()) continue;
356 if (ImGui::Selectable(display.c_str())) {
357 selectedDrive = drive;
358 driveOk = true;
359 }
360 }
361 if (!driveOk) {
362 selectedDrive = "virtual_drive";
363 }
364 });
365 simpleToolTip([&]{ return getDiskImageName(); });
366 ImGui::SameLine();
367 createDiskImage = ImGui::Button(ICON_IGFD_ADD"##NewDiskImage");
368 simpleToolTip("Create new disk image");
369 ImGui::SameLine();
370 im::Disabled(selectedDrive.starts_with("hd"), [&]{
371 if (ImGui::Button(ICON_IGFD_FOLDER_OPEN"##BrowseDiskImage")) insertMsxDisk();
372 simpleToolTip("Insert disk image");
373 });
374
375 if (ImGui::Button(ICON_IGFD_CHEVRON_UP"##msxDirUp")) msxParentDirectory();
376 simpleToolTip("Go to parent directory");
377 ImGui::SameLine();
378 im::Disabled(!writable, [&]{
379 createMsxDir = ImGui::Button(ICON_IGFD_ADD"##msxNewDir");
380 simpleToolTip("New MSX directory");
381 });
382 ImGui::SameLine();
383 ImGui::SetNextItemWidth(-FLT_MIN);
384 if (stuff) {
385 im::StyleColor(!isValidMsxDirectory(*stuff, editMsxDir), ImGuiCol_Text, getColor(imColor::ERROR), [&]{
386 if (ImGui::InputText("##msxPath", &editMsxDir)) {
387 if (!editMsxDir.starts_with('/')) {
388 editMsxDir = '/' + editMsxDir;
389 }
390 if (isValidMsxDirectory(*stuff, editMsxDir)) {
391 msxDir = editMsxDir;
392 msxRefresh();
393 }
394 }
395 });
396 } else {
397 im::Disabled(true, []{
398 std::string noDisk = "No (valid) disk inserted";
399 ImGui::InputText("##msxPath", &noDisk);
400 });
401 }
402
403 im::Table("##msxFiles", 4, tableFlags, {-FLT_MIN, -ImGui::GetFrameHeightWithSpacing()}, [&]{
404 ImGui::TableSetupScrollFreeze(0, 1); // Make top row always visible
405 ImGui::TableSetupColumn("Filename", ImGuiTableColumnFlags_NoHide | ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_NoReorder);
406 ImGui::TableSetupColumn("Size");
407 ImGui::TableSetupColumn("Modified");
408 ImGui::TableSetupColumn("Attrib", ImGuiTableColumnFlags_DefaultHide);
409 ImGui::TableHeadersRow();
410 bool msxForceSort = true; // always sort
411 auto action = drawTable(msxFileCache, msxLastClick, msxForceSort, true);
412 std::visit(overloaded {
413 [](Nop) { /* nothing */},
414 [&](ChangeDir cd) {
415 msxDir = FileOperations::join(msxDir, cd.name);
416 msxRefresh();
417 },
418 [&](Delete d) {
419 stuff->tar->deleteItem(d.name);
420 msxRefresh();
421 },
422 [&](Rename r) {
423 renameFrom = r.name;
424 renameMsxEntry = true;
425 }
426 }, action);
427 });
428
429 im::Disabled(!stuff, [&]{
430 if (ImGui::Button("Export to new disk image")) exportDiskImage();
431 simpleToolTip("Save the content of the drive to a new disk image");
432 });
433 ImGui::SameLine();
434 if (stuff) {
435 auto [num, size] = stuff->tar->getFreeSpace();
436 auto free = (size_t(num) * size) / 1024; // in kB
437 auto status = strCat(free, "kB free");
438 if (!writable) strAppend(status, ", read-only");
440 } else {
441 ImGui::TextUnformatted("No (valid) disk inserted"sv);
442 }
443 });
444
445 ImGui::SameLine();
446 auto pos1 = ImGui::GetCursorPos();
447 auto b2Height = ImGui::GetFrameHeightWithSpacing() + ImGui::GetFrameHeight();
448 auto byPos = (availableSize.y - b2Height) * 0.5f;
449 im::Group([&]{
450 ImGui::Dummy({0.0f, byPos});
451 im::Disabled(!writable || ranges::none_of(hostFileCache, &FileInfo::isSelected), [&]{
452 if (ImGui::Button("<<")) {
453 if (setupTransferHostToMsx(*stuff)) {
454 openCheckTransfer = true;
455 }
456 }
457 simpleToolTip("Transfer files or directories from host to MSX");
458 });
459 im::Disabled(!stuff || ranges::none_of(msxFileCache, &FileInfo::isSelected), [&]{
460 if (ImGui::Button(">>")) transferMsxToHost(*stuff);
461 simpleToolTip("Transfer files or directories from MSX to host");
462 });
463 });
464 ImGui::SameLine();
465 ImGui::SetCursorPosY(pos1.y);
466
467 im::Child("##host", {cWidth, 0.0f}, [&]{
468 if (ImGui::Button(ICON_IGFD_CHEVRON_UP"##hostDirUp")) hostParentDirectory();
469 simpleToolTip("Go to parent directory");
470 ImGui::SameLine();
471 if (ImGui::Button(ICON_IGFD_REFRESH"##hostRefresh")) hostRefresh();
472 simpleToolTip("Refresh directory listing");
473 ImGui::SameLine();
474 createHostDir = ImGui::Button(ICON_IGFD_ADD"##hostNewDir");
475 simpleToolTip("New host directory");
476 ImGui::SameLine();
477 ImGui::SetNextItemWidth(-FLT_MIN);
478 im::StyleColor(!FileOperations::isDirectory(editHostDir), ImGuiCol_Text, getColor(imColor::ERROR), [&]{
479 if (ImGui::InputText("##hostPath", &editHostDir)) {
480 if (FileOperations::isDirectory(editHostDir)) {
481 hostDir = editHostDir;
482 hostRefresh();
483 }
484 }
485 });
486
487 im::Table("##hostFiles", 3, tableFlags, {-FLT_MIN, -FLT_MIN}, [&]{
488 ImGui::TableSetupScrollFreeze(0, 1); // Make top row always visible
489 ImGui::TableSetupColumn("Filename", ImGuiTableColumnFlags_NoHide | ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_NoReorder);
490 ImGui::TableSetupColumn("Size");
491 ImGui::TableSetupColumn("Modified");
492 ImGui::TableHeadersRow();
493 auto action = drawTable(hostFileCache, hostLastClick, hostForceSort, false);
494 std::visit(overloaded {
495 [](Nop) { /* nothing */},
496 [&](ChangeDir cd) {
497 hostDir = FileOperations::join(hostDir, cd.name);
498 hostRefresh();
499 },
500 [](Delete) { assert(false); },
501 [](Rename) { assert(false); }
502 }, action);
503 });
504 });
505
506 const char* const renameTitle = "Rename";
507 if (renameMsxEntry) {
508 editModal.clear();
509 ImGui::OpenPopup(renameTitle);
510 }
511 im::PopupModal(renameTitle, nullptr, ImGuiWindowFlags_AlwaysAutoResize, [&]{
512 bool close = false;
513 ImGui::TextUnformatted("from:"sv);
514 ImGui::SameLine();
515 ImGui::TextUnformatted(renameFrom);
516 ImGui::Text("to:");
517 ImGui::SameLine();
518 bool ok = ImGui::InputText("##newName", &editModal, ImGuiInputTextFlags_EnterReturnsTrue);
519 ok |= ImGui::Button("Ok");
520 if (ok) {
521 if (stuff) {
522 std::string error;
523 try {
524 stuff->tar->chdir(msxDir);
525 error = stuff->tar->renameItem(renameFrom, editModal);
526 } catch (MSXException& e) {
527 error = e.getMessage();
528 }
529 if (!error.empty()) {
530 manager.printError("Couldn't rename ", renameFrom,
531 " to ", editModal, ": ", error);
532 }
533 }
534 msxRefresh();
535 close = true;
536 }
537 ImGui::SameLine();
538 close |= ImGui::Button("Cancel");
539 if (close) ImGui::CloseCurrentPopup();
540 });
541
542 const char* const overwriteTitle = "Confirm overwrite?";
543 if (openCheckTransfer) {
544 ImGui::OpenPopup(overwriteTitle);
545 }
546 bool p_open = true;
547 im::PopupModal(overwriteTitle, &p_open, ImGuiWindowFlags_AlwaysAutoResize, [&]{
548 auto printList = [](const char* label, const std::vector<FileInfo>& list) {
549 float count = std::min(4.5f, narrow_cast<float>(list.size()));
550 float height = count * ImGui::GetTextLineHeightWithSpacing();
551 im::ListBox(label, {0.0f, height}, [&]{
552 for (const auto& f : list) {
553 ImGui::TextUnformatted(f.filename);
554 }
555 });
556 };
557 if (!existingFiles.empty()) {
558 ImGui::TextUnformatted("These files already exist and will be overwritten"sv);
559 printList("##files", existingFiles);
560 }
561 if (!existingDirs.empty()) {
562 ImGui::TextUnformatted("These directories already exist and will be overwritten"sv);
563 printList("##dirs", existingDirs);
564 }
565 if (!duplicateEntries.empty()) {
566 ImGui::TextUnformatted("These host entries will map to the same MSX entry"sv);
567 im::ListBox("##duplicates", {0.0f, 4.5f * ImGui::GetTextLineHeightWithSpacing()}, [&]{
568 for (const auto& [msxName, hostEntries] : duplicateEntries) {
569 ImGui::Text("%s:", msxName.c_str());
570 for (const auto& host : hostEntries) {
571 ImGui::Text(" %s", host.filename.c_str());
572 }
573 }
574 });
575 }
576 ImGui::Separator();
577
578 if (!p_open) {
579 transferHostToMsxPhase = IDLE;
580 }
581 if (ImGui::Button("Preserve")) {
582 transferHostToMsxPhase = EXECUTE_PRESERVE;
583 p_open = false;
584 }
585 simpleToolTip("Do not overwrite MSX files");
586 ImGui::SameLine();
587 if (ImGui::Button("Overwrite")) {
588 transferHostToMsxPhase = EXECUTE_OVERWRITE;
589 p_open = false;
590 }
591 simpleToolTip("Overwrite MSX files");
592 ImGui::SameLine();
593 if (ImGui::Button("Cancel")) {
594 transferHostToMsxPhase = IDLE;
595 p_open = false;
596 }
597 simpleToolTip("Abort transfer");
598
599 if (!p_open) {
600 ImGui::CloseCurrentPopup();
601 if (stuff && transferHostToMsxPhase != IDLE) {
602 executeTransferHostToMsx(*stuff);
603 }
604 }
605 });
606
607 const char* const newMsxDirTitle = "Create new MSX directory";
608 if (createMsxDir) {
609 editModal.clear();
610 ImGui::OpenPopup(newMsxDirTitle);
611 }
612 p_open = true;
613 im::PopupModal(newMsxDirTitle, &p_open, ImGuiWindowFlags_AlwaysAutoResize, [&]{
614 ImGui::TextUnformatted("Create new directory in:"sv);
615 ImGui::TextUnformatted(driveDisplayName(selectedDrive));
617 bool close = false;
618 bool ok = ImGui::InputText("##msxPath", &editModal, ImGuiInputTextFlags_EnterReturnsTrue);
619 ok |= ImGui::Button("Ok");
620 if (ok) {
621 if (stuff) {
622 try {
623 stuff->tar->chdir(msxDir);
624 stuff->tar->mkdir(editModal);
625 } catch (MSXException& e) {
626 manager.printError(
627 "Couldn't create new MSX directory: ", e.getMessage());
628 }
629 }
630
631 msxRefresh();
632 close = true;
633 }
634 ImGui::SameLine();
635 close |= ImGui::Button("Cancel");
636 if (close) ImGui::CloseCurrentPopup();
637 });
638
639 const char* const newHostDirTitle = "Create new host directory";
640 if (createHostDir) {
641 editModal.clear();
642 ImGui::OpenPopup(newHostDirTitle);
643 }
644 p_open = true;
645 im::PopupModal(newHostDirTitle, &p_open, ImGuiWindowFlags_AlwaysAutoResize, [&]{
646 ImGui::TextUnformatted("Create new directory in:"sv);
647 ImGui::TextUnformatted(hostDir);
648 bool close = false;
649 bool ok = ImGui::InputText("##hostPath", &editModal, ImGuiInputTextFlags_EnterReturnsTrue);
650 ok |= ImGui::Button("Ok");
651 if (ok) {
652 FileOperations::mkdirp(FileOperations::join(hostDir, editModal));
653 hostRefresh();
654 close = true;
655 }
656 ImGui::SameLine();
657 close |= ImGui::Button("Cancel");
658 if (close) ImGui::CloseCurrentPopup();
659 });
660
661 const char* const newDiskImageTitle = "Create new disk image";
662 if (createDiskImage) {
663 auto current = getDiskImageName();
664 auto cwd = current.empty() ? FileOperations::getCurrentWorkingDirectory()
665 : std::string(FileOperations::getDirName(current));
666 auto newName = FileOperations::getNextNumberedFileName(cwd, "new-image", ".dsk");
667 editModal = FileOperations::join(cwd, newName);
668
669 newDiskType = UNPARTITIONED;
670 bootType = static_cast<int>(MSXBootSectorType::DOS2);
671 unpartitionedSize = {720, PartitionSize::KB};
672 partitionSizes.assign(3, {32, PartitionSize::MB});
673 ImGui::OpenPopup(newDiskImageTitle);
674 }
675 ImGui::SetNextWindowSize(gl::vec2{30, 22} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
676 p_open = true;
677 im::PopupModal(newDiskImageTitle, &p_open, [&]{
678 bool close = false;
679
680 auto buttonWidth = style.ItemSpacing.x + 2.0f * style.FramePadding.x +
681 ImGui::CalcTextSize(ICON_IGFD_FOLDER_OPEN).x;
682 ImGui::SetNextItemWidth(-buttonWidth);
683 ImGui::InputText("##newDiskPath", &editModal);
684 simpleToolTip("Filename for new disk image");
685 ImGui::SameLine();
686 if (ImGui::Button(ICON_IGFD_FOLDER_OPEN"##BrowseNewImage")) {
687 manager.openFile->selectNewFile(
688 "Filename for new disk image",
689 "Disk image (*.dsk){.dsk}", // only .dsk (not all other disk extensions)
690 [&](const auto& fn) { editModal = fn; },
691 getDiskImageName(),
692 ImGuiOpenFile::Painter::DISKMANIPULATOR);
693 }
694 simpleToolTip("Select new file");
695 if (manager.openFile->mustPaint(ImGuiOpenFile::Painter::DISKMANIPULATOR)) {
696 manager.openFile->doPaint();
697 }
698
699 ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6.0f);
700 ImGui::Combo("type", &bootType, "DOS 1\0DOS 2\0Nextor\0", 3); // in sync with MSXBootSectorType enum
701 ImGui::Separator();
702
703 ImGui::RadioButton("No partitions (floppy disk)", &newDiskType, UNPARTITIONED);
704 im::DisabledIndent(newDiskType != UNPARTITIONED, [&]{
705 ImGui::SetNextItemWidth(ImGui::GetFontSize() * 5.0f);
706 ImGui::InputScalar("##count", ImGuiDataType_U32, &unpartitionedSize.count);
707 ImGui::SameLine();
708 ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.5f);
709 ImGui::Combo("##unit", &unpartitionedSize.unit, "kB\0MB\0", 2);
710 });
711
712 ImGui::RadioButton("Partitioned HD image", &newDiskType, PARTITIONED);
713 im::DisabledIndent(newDiskType != PARTITIONED, [&]{
714 float bottomHeight = 2.0f * style.ItemSpacing.y + 1.0f + 2.0f * style.FramePadding.y + ImGui::GetTextLineHeight();
715 im::ListBox("##partitions", {14 * ImGui::GetFontSize(), -bottomHeight}, [&]{
716 im::ID_for_range(partitionSizes.size(), [&](int i) {
717 ImGui::AlignTextToFramePadding();
718 ImGui::Text("%2d:", i + 1);
719 ImGui::SameLine();
720 ImGui::SetNextItemWidth(ImGui::GetFontSize() * 5.0f);
721 ImGui::InputScalar("##count", ImGuiDataType_U32, &partitionSizes[i].count);
722 ImGui::SameLine();
723 ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.5f);
724 ImGui::Combo("##unit", &partitionSizes[i].unit, "kB\0MB\0", 2);
725 });
726 });
727 ImGui::SameLine();
728 im::Group([&]{
729 im::Disabled(partitionSizes.size() >= 31, [&]{
730 if (ImGui::Button("Add")) {
731 partitionSizes.push_back({32, PartitionSize::MB});
732 }
733 });
734 im::Disabled(partitionSizes.size() <= 1, [&]{
735 if (ImGui::Button("Remove")) {
736 partitionSizes.pop_back();
737 }
738 });
739 });
740 });
741
742 ImGui::Separator();
743 im::Disabled(editModal.empty(), [&]{
744 if (ImGui::Button("Ok")) {
745 const auto& diskManipulator = manager.getReactor().getDiskManipulator();
746 auto sizes = [&]{
747 if (newDiskType == UNPARTITIONED) {
748 return std::vector<unsigned>(1, unpartitionedSize.asSectorCount());
749 } else {
750 return to_vector(view::transform(partitionSizes, &PartitionSize::asSectorCount));
751 }
752 }();
753 try {
754 diskManipulator.create(editModal, static_cast<MSXBootSectorType>(bootType), sizes);
755 if (auto* drive = getDrive()) {
756 drive->insertDisk(editModal); // might fail (return code), but ignore
757 msxRefresh();
758 }
759 } catch (MSXException& e) {
760 manager.printError(
761 "Couldn't create disk image: ", e.getMessage());
762 }
763 close = true;
764 }
765 });
766 ImGui::SameLine();
767 close |= ImGui::Button("Cancel");
768 if (close) ImGui::CloseCurrentPopup();
769 });
770 });
771}
772
773void ImGuiDiskManipulator::insertMsxDisk()
774{
775 if (selectedDrive.starts_with("hd")) {
776 // disallow changing HD image
777 return;
778 }
779 manager.openFile->selectFile(
780 strCat("Select disk image for ", driveDisplayName(selectedDrive)),
781 ImGuiMedia::diskFilter(),
782 [&](const auto& fn) {
783 auto* drive = getDrive();
784 if (!drive) return;
785 drive->insertDisk(fn); // might fail (return code), but ignore
786 msxRefresh();
787 },
788 getDiskImageName());
789}
790
791void ImGuiDiskManipulator::exportDiskImage()
792{
793 manager.openFile->selectNewFile(
794 strCat("Export ", driveDisplayName(selectedDrive), " to new disk image"),
795 "Disk image (*.dsk){.dsk}", // only .dsk (not all other disk extensions)
796 [&](const auto& fn) {
797 auto dd = getDriveAndDisk();
798 if (!dd) return;
799 const auto& [drive, disk] = *dd;
800 assert(disk);
801
802 try {
803 SectorBuffer buf;
804 File file(fn, File::OpenMode::CREATE);
805 for (auto i : xrange(disk->getNbSectors())) {
806 disk->readSector(i, buf);
807 file.write(buf.raw);
808 }
809 } catch (MSXException& e) {
810 manager.printError(
811 "Couldn't export disk image: ", e.getMessage());
812 }
813 },
814 getDiskImageName());
815}
816
817void ImGuiDiskManipulator::msxParentDirectory()
818{
819 if (msxDir.ends_with('/')) msxDir.pop_back();
820 msxDir = FileOperations::getDirName(msxDir);
821 if (msxDir.empty()) msxDir.push_back('/');
822 msxRefresh();
823}
824
825void ImGuiDiskManipulator::hostParentDirectory()
826{
827 if (hostDir.ends_with('/')) hostDir.pop_back();
828 hostDir = FileOperations::getDirName(hostDir);
829 if (hostDir.empty()) hostDir.push_back('/');
830 hostRefresh();
831}
832
833void ImGuiDiskManipulator::msxRefresh()
834{
835 editMsxDir = msxDir;
836 msxFileCache.clear();
837}
838
839void ImGuiDiskManipulator::hostRefresh()
840{
841 editHostDir = hostDir;
842 hostNeedRefresh = true;
843}
844
845bool ImGuiDiskManipulator::setupTransferHostToMsx(DrivePartitionTar& stuff)
846{
847 try {
848 stuff.tar->chdir(msxDir);
849 } catch (MSXException&) {
850 msxRefresh();
851 return false;
852 }
853
854 existingDirs.clear();
855 existingFiles.clear();
856 duplicateEntries.clear();
857 for (const auto& item : hostFileCache) {
858 if (!item.isSelected) continue;
859 auto msxName = stuff.tar->convertToMsxName(item.filename);
860 duplicateEntries[msxName].push_back(item);
861 auto it = ranges::find(msxFileCache, msxName, &FileInfo::filename);
862 if (it == msxFileCache.end()) continue;
863 (it->isDirectory ? existingDirs : existingFiles).push_back(*it);
864 }
865 std::erase_if(duplicateEntries, [](const auto& entry) { return entry.second.size() < 2; });
866 if (existingDirs.empty() && existingFiles.empty() && duplicateEntries.empty()) {
867 transferHostToMsxPhase = EXECUTE_PRESERVE;
868 executeTransferHostToMsx(stuff);
869 return false;
870 } else {
871 transferHostToMsxPhase = CHECK;
872 return true; // open modal window
873 }
874}
875
876void ImGuiDiskManipulator::executeTransferHostToMsx(DrivePartitionTar& stuff)
877{
878 transferHostToMsxPhase = IDLE;
879 try {
880 stuff.tar->chdir(msxDir);
881 } catch (MSXException&) {
882 msxRefresh();
883 return;
884 }
885
886 for (const auto& item : hostFileCache) {
887 if (!item.isSelected) continue;
888 try {
889 auto add = (transferHostToMsxPhase == EXECUTE_PRESERVE)
890 ? MSXtar::Add::PRESERVE : MSXtar::Add::OVERWRITE;
891 stuff.tar->addItem(FileOperations::join(hostDir, item.filename), add);
892 } catch (MSXException& e) {
893 manager.printError("Couldn't import ", item.filename, ": ", e.getMessage());
894 }
895 }
896 msxRefresh();
897}
898
899void ImGuiDiskManipulator::transferMsxToHost(DrivePartitionTar& stuff)
900{
901 try {
902 stuff.tar->chdir(msxDir);
903 } catch (MSXException&) {
904 msxRefresh();
905 return;
906 }
907 for (const auto& item : msxFileCache) {
908 if (!item.isSelected) continue;
909 try {
910 stuff.tar->getItemFromDir(hostDir, item.filename);
911 } catch (MSXException& e) {
912 manager.printError("Couldn't extract ", item.filename, ": ", e.getMessage());
913 }
914 }
915 hostRefresh();
916}
917
918} // namespace openmsx
bool empty() const
Definition hash_set.hh:521
bool contains(const K &key) const
Definition hash_set.hh:435
std::pair< iterator, bool > insert(V &&value)
Definition hash_set.hh:441
DiskContainer * getDrive(std::string_view driveName) const
std::optional< DriveAndPartition > getDriveAndDisk(std::string_view driveName) const
ImGuiManager & manager
Definition ImGuiPart.hh:30
DiskManipulator & getDiskManipulator()
Definition Reactor.hh:96
const MsxChar2Unicode & getMsxChar2Unicode() const
Definition Reactor.cc:374
CHECK(m3==m3)
auto CalcTextSize(std::string_view str)
Definition ImGuiUtils.hh:39
void TextUnformatted(const std::string &str)
Definition ImGuiUtils.hh:26
constexpr double e
Definition Math.hh:21
void DisabledIndent(bool b, std::invocable<> auto next)
Definition ImGuiCpp.hh:514
void Table(const char *str_id, int column, ImGuiTableFlags flags, const ImVec2 &outer_size, float inner_width, std::invocable<> auto next)
Definition ImGuiCpp.hh:455
void Window(const char *name, bool *p_open, ImGuiWindowFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:63
void Combo(const char *label, const char *preview_value, ImGuiComboFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:289
void ListBox(const char *label, const ImVec2 &size, std::invocable<> auto next)
Definition ImGuiCpp.hh:328
void StyleColor(bool active, Args &&...args)
Definition ImGuiCpp.hh:175
void Child(const char *str_id, const ImVec2 &size, ImGuiChildFlags child_flags, ImGuiWindowFlags window_flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:110
void PopupModal(const char *name, bool *p_open, ImGuiWindowFlags flags, std::invocable<> auto next)
Definition ImGuiCpp.hh:404
void Disabled(bool b, std::invocable<> auto next)
Definition ImGuiCpp.hh:506
void Group(std::invocable<> auto next)
Definition ImGuiCpp.hh:236
void ListClipperID(size_t count, std::invocable< int > auto next)
Definition ImGuiCpp.hh:559
void ID_for_range(std::integral auto count, std::invocable< int > auto next)
Definition ImGuiCpp.hh:281
std::string toString(time_t time)
Definition Date.cc:153
std::string formatAttrib(MSXDirEntry::AttribValue attrib)
string getCurrentWorkingDirectory()
Returns the current working directory.
bool isDirectory(const Stat &st)
This file implemented 3 utility functions:
Definition Autofire.cc:11
void sortUpDown_String(Range &range, const ImGuiTableSortSpecs *sortSpecs, Projection proj)
void simpleToolTip(std::string_view desc)
Definition ImGuiUtils.hh:79
void sortUpDown_T(Range &range, const ImGuiTableSortSpecs *sortSpecs, Projection proj)
ImU32 getColor(imColor col)
bool foreach_file_and_directory(std::string path, FileAction fileAction, DirAction dirAction)
constexpr bool none_of(InputRange &&range, UnaryPredicate pred)
Definition ranges.hh:212
auto find(InputRange &&range, const T &value)
Definition ranges.hh:162
std::string strCat()
Definition strCat.hh:703
void strAppend(std::string &result, Ts &&...ts)
Definition strCat.hh:752
#define UNREACHABLE
constexpr auto xrange(T e)
Definition xrange.hh:132
constexpr auto begin(const zstring_view &x)
constexpr auto end(const zstring_view &x)