OXYGENHUB
◆ main openmsx / src / imgui / ImGuiDiskManipulator.cc
28.6 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "ImGuiDiskManipulator.hh"
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 "xxhash.hh"
25
26
#include <imgui_stdlib.h>
27
28
#include <algorithm>
29
#include <cassert>
30
#include <ranges>
31
32
namespace openmsx {
33
34
using namespace std::literals;
35
36
DiskContainer* ImGuiDiskManipulator::getDrive()
37
{
38
	const auto& diskManipulator = manager.getReactor().getDiskManipulator();
39
	return diskManipulator.getDrive(selectedDrive);
40
}
41
42
std::optional<DiskManipulator::DriveAndPartition> ImGuiDiskManipulator::getDriveAndDisk()
43
{
44
	const auto& diskManipulator = manager.getReactor().getDiskManipulator();
45
	return diskManipulator.getDriveAndDisk(selectedDrive);
46
}
47
48
std::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 = drive, .disk = std::move(disk), .tar = 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
62
bool 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
73
std::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
	for (auto i : xrange(dir.size())) {
89
		auto entry = dir.getListIndexUnchecked(i);
90
		FileInfo info;
91
		info.attrib = MSXDirEntry::AttribValue(uint8_t(entry.getListIndexUnchecked(1).getOptionalInt().value_or(0)));
92
		if (info.attrib & MSXDirEntry::Attrib::VOLUME) continue; // skip
93
		info.filename = std::string(entry.getListIndexUnchecked(0).getString());
94
		if (info.filename == one_of(".", "..")) continue; // skip
95
		info.modified = entry.getListIndexUnchecked(2).getOptionalInt().value_or(0);
96
		info.size = entry.getListIndexUnchecked(3).getOptionalInt().value_or(0);
97
		info.isDirectory = bool(info.attrib & MSXDirEntry::Attrib::DIRECTORY);
98
		if (info.isDirectory) info.size = size_t(-1);
99
		result.push_back(std::move(info));
100
	}
101
102
	return result;
103
}
104
105
void ImGuiDiskManipulator::refreshMsx(DrivePartitionTar& stuff)
106
{
107
	auto newFiles = dirMSX(stuff);
108
	if (newFiles.size() != msxFileCache.size()) msxLastClick = -1;
109
110
	// copy 'isSelected' from 'msxFileCache' to 'newFiles'
111
	hash_set<std::string_view, std::identity, XXHasher> selected;
112
	for (const auto& oldFile : msxFileCache) {
113
		if (oldFile.isSelected) selected.insert(oldFile.filename);
114
	}
115
	if (!selected.empty()) {
116
		for (auto& newFile : newFiles) {
117
			if (selected.contains(newFile.filename)) newFile.isSelected = true;
118
		}
119
	}
120
121
	std::swap(newFiles, msxFileCache);
122
}
123
124
void ImGuiDiskManipulator::refreshHost()
125
{
126
	if (hostDir.empty() || !FileOperations::isDirectory(hostDir)) {
127
		hostDir = FileOperations::getCurrentWorkingDirectory();
128
		editHostDir = hostDir;
129
	}
130
131
	hostFileCache.clear();
132
	auto fileAction = [&](const std::string& /*fullPath*/, std::string_view filename, const FileOperations::Stat& st) {
133
		FileInfo info;
134
		info.filename = std::string{filename};
135
		info.size = st.st_size;
136
		info.modified = st.st_mtime;
137
		info.isDirectory = false;
138
		hostFileCache.push_back(std::move(info));
139
	};
140
	auto directoryAction = [&](const std::string& /*fullPath*/, std::string_view filename, const FileOperations::Stat& st) {
141
		FileInfo info;
142
		info.filename = std::string{filename};
143
		info.size = size_t(-1); // not shown, but used for sorting
144
		info.modified = st.st_mtime;
145
		info.isDirectory = true;
146
		hostFileCache.push_back(std::move(info));
147
	};
148
	foreach_file_and_directory(hostDir, fileAction, directoryAction);
149
150
	hostLastClick = -1;
151
	hostNeedRefresh = false;
152
	hostForceSort = true;
153
}
154
155
void ImGuiDiskManipulator::checkSort(std::vector<FileInfo>& files, bool& forceSort) const
156
{
157
	auto* sortSpecs = ImGui::TableGetSortSpecs();
158
	if (!forceSort && !sortSpecs->SpecsDirty) return;
159
160
	forceSort = false;
161
	sortSpecs->SpecsDirty = false;
162
	assert(sortSpecs->SpecsCount == 1);
163
	assert(sortSpecs->Specs);
164
	assert(sortSpecs->Specs->SortOrder == 0);
165
166
	switch (sortSpecs->Specs->ColumnIndex) {
167
	case 0: // filename
168
		sortUpDown_String(files, sortSpecs, &FileInfo::filename);
169
		break;
170
	case 1: // size
171
		sortUpDown_T(files, sortSpecs, &FileInfo::size);
172
		break;
173
	case 2: // modified
174
		sortUpDown_T(files, sortSpecs, &FileInfo::modified);
175
		break;
176
	case 3: // attrib
177
		sortUpDown_T(files, sortSpecs, &FileInfo::attrib);
178
		break;
179
	default:
180
		UNREACHABLE;
181
	}
182
}
183
184
ImGuiDiskManipulator::Action ImGuiDiskManipulator::drawTable(
185
	std::vector<FileInfo>& files, int& lastClickIdx, bool& forceSort, bool msxSide) const
186
{
187
	auto clearSelection = [&]{
188
		for (auto& file : files) file.isSelected = false;
189
	};
190
	auto selectRange = [&](size_t curr, size_t last) {
191
		assert(!files.empty());
192
		assert(curr < files.size());
193
		if (last >= files.size()) last = files.size() - 1;
194
195
		auto begin = std::min(curr, last);
196
		auto end   = std::max(curr, last);
197
		for (auto i = begin; i <= end; ++i) {
198
			files[i].isSelected = true;
199
		}
200
	};
201
202
	checkSort(files, forceSort);
203
204
	Action result = Nop{};
205
	im::ListClipperID(files.size(), [&](int i) {
206
		auto& file = files[i];
207
		if (ImGui::TableNextColumn()) { // filename
208
			auto pos = ImGui::GetCursorPos();
209
			if (ImGui::Selectable("##row", file.isSelected, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap | ImGuiSelectableFlags_AllowDoubleClick)) {
210
				if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
211
					result = ChangeDir{file.filename};
212
				} else {
213
					bool ctrl     = ImGui::IsKeyDown(ImGuiKey_LeftCtrl ) || ImGui::IsKeyDown(ImGuiKey_RightCtrl);
214
					bool shiftKey = ImGui::IsKeyDown(ImGuiKey_LeftShift) || ImGui::IsKeyDown(ImGuiKey_RightShift);
215
					bool shift = shiftKey && (lastClickIdx != -1);
216
					if (ctrl && shift) {
217
						selectRange(i, lastClickIdx);
218
					} else if (ctrl) {
219
						file.isSelected = !file.isSelected;
220
						lastClickIdx = i;
221
					} else if (shift) {
222
						clearSelection();
223
						selectRange(i, lastClickIdx);
224
					} else {
225
						clearSelection();
226
						file.isSelected = true;
227
						lastClickIdx = i;
228
					}
229
				}
230
			}
231
			if (msxSide && ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Right)) {
232
				ImGui::OpenPopup("table-context");
233
			}
234
			im::Popup("table-context", [&]{
235
				ImGui::TextUnformatted(file.isDirectory ? "Directory:"sv : "File:"sv);
236
				ImGui::SameLine();
237
				ImGui::TextUnformatted(file.filename);
238
				ImGui::Separator();
239
240
				if (ImGui::Selectable("Delete")) {
241
					result = Delete{file.filename};
242
				}
243
				if (ImGui::Selectable("Rename...")) {
244
					result = Rename{file.filename};
245
				}
246
			});
247
248
			ImGui::SetCursorPos(pos);
249
250
			ImGui::Text("%s %s",
251
				file.isDirectory ? ICON_IGFD_FOLDER_OPEN : ICON_IGFD_FILE,
252
				file.filename.c_str());
253
		}
254
		if (ImGui::TableNextColumn()) { // size
255
			if (!file.isDirectory) {
256
				ImGui::Text("%zu", file.size);
257
			}
258
		}
259
		if (ImGui::TableNextColumn()) { // modified
260
			std::array<char, 24> buf;
261
			ImGui::TextUnformatted(Date::toString(file.modified, buf));
262
		}
263
		if (msxSide && ImGui::TableNextColumn()) { // attrib
264
			ImGui::TextUnformatted(DiskImageUtils::formatAttrib(file.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
290
std::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
304
void 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 = tmpStrCat(free, "kB free", strCat_if(!writable, ", read-only"));
438
				ImGui::TextUnformatted(status);
439
			} else {
440
				ImGui::TextUnformatted("No (valid) disk inserted"sv);
441
			}
442
		});
443
444
		ImGui::SameLine();
445
		auto pos1 = ImGui::GetCursorPos();
446
		auto b2Height = ImGui::GetFrameHeightWithSpacing() + ImGui::GetFrameHeight();
447
		auto byPos = (availableSize.y - b2Height) * 0.5f;
448
		im::Group([&]{
449
			ImGui::Dummy({0.0f, byPos});
450
			im::Disabled(!writable || std::ranges::none_of(hostFileCache, &FileInfo::isSelected), [&]{
451
				if (ImGui::Button("<<")) {
452
					if (setupTransferHostToMsx(*stuff)) {
453
						openCheckTransfer = true;
454
					}
455
				}
456
				simpleToolTip("Transfer files or directories from host to MSX");
457
			});
458
			im::Disabled(!stuff || std::ranges::none_of(msxFileCache, &FileInfo::isSelected), [&]{
459
				if (ImGui::Button(">>")) transferMsxToHost(*stuff);
460
				simpleToolTip("Transfer files or directories from MSX to host");
461
			});
462
		});
463
		ImGui::SameLine();
464
		ImGui::SetCursorPosY(pos1.y);
465
466
		im::Child("##host", {cWidth, 0.0f}, [&]{
467
			if (ImGui::Button(ICON_IGFD_CHEVRON_UP"##hostDirUp")) hostParentDirectory();
468
			simpleToolTip("Go to parent directory");
469
			ImGui::SameLine();
470
			if (ImGui::Button(ICON_IGFD_REFRESH"##hostRefresh")) hostRefresh();
471
			simpleToolTip("Refresh directory listing");
472
			ImGui::SameLine();
473
			createHostDir = ImGui::Button(ICON_IGFD_ADD"##hostNewDir");
474
			simpleToolTip("New host directory");
475
			ImGui::SameLine();
476
			ImGui::SetNextItemWidth(-FLT_MIN);
477
			im::StyleColor(!FileOperations::isDirectory(editHostDir), ImGuiCol_Text, getColor(imColor::ERROR), [&]{
478
				if (ImGui::InputText("##hostPath", &editHostDir)) {
479
					if (FileOperations::isDirectory(editHostDir)) {
480
						hostDir = editHostDir;
481
						hostRefresh();
482
					}
483
				}
484
			});
485
486
			im::Table("##hostFiles", 3, tableFlags, {-FLT_MIN, -FLT_MIN}, [&]{
487
				ImGui::TableSetupScrollFreeze(0, 1); // Make top row always visible
488
				ImGui::TableSetupColumn("Filename", ImGuiTableColumnFlags_NoHide | ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_NoReorder);
489
				ImGui::TableSetupColumn("Size");
490
				ImGui::TableSetupColumn("Modified");
491
				ImGui::TableHeadersRow();
492
				auto action = drawTable(hostFileCache, hostLastClick, hostForceSort, false);
493
				std::visit(overloaded {
494
					[](Nop) { /* nothing */},
495
					[&](ChangeDir cd) {
496
						hostDir = FileOperations::join(hostDir, cd.name);
497
						hostRefresh();
498
					},
499
					[](Delete) { assert(false); },
500
					[](Rename) { assert(false); }
501
				}, action);
502
			});
503
		});
504
505
		const char* const renameTitle = "Rename";
506
		if (renameMsxEntry) {
507
			editModal.clear();
508
			ImGui::OpenPopup(renameTitle);
509
		}
510
		im::PopupModal(renameTitle, nullptr, ImGuiWindowFlags_AlwaysAutoResize, [&]{
511
			bool close = false;
512
			ImGui::TextUnformatted("from:"sv);
513
			ImGui::SameLine();
514
			ImGui::TextUnformatted(renameFrom);
515
			ImGui::Text("to:");
516
			ImGui::SameLine();
517
			bool ok = ImGui::InputText("##newName", &editModal, ImGuiInputTextFlags_EnterReturnsTrue);
518
			ok |= ImGui::Button("Ok");
519
			if (ok) {
520
				if (stuff) {
521
					std::string error;
522
					try {
523
						stuff->tar->chdir(msxDir);
524
						error = stuff->tar->renameItem(renameFrom, editModal);
525
					} catch (MSXException& e) {
526
						error = e.getMessage();
527
					}
528
					if (!error.empty()) {
529
						manager.printError("Couldn't rename ", renameFrom,
530
								" to ", editModal, ": ", error);
531
					}
532
				}
533
				msxRefresh();
534
				close = true;
535
			}
536
			ImGui::SameLine();
537
			close |= ImGui::Button("Cancel");
538
			if (close) ImGui::CloseCurrentPopup();
539
		});
540
541
		const char* const overwriteTitle = "Confirm overwrite?";
542
		if (openCheckTransfer) {
543
			ImGui::OpenPopup(overwriteTitle);
544
		}
545
		bool p_open = true;
546
		im::PopupModal(overwriteTitle, &p_open, ImGuiWindowFlags_AlwaysAutoResize, [&]{
547
			auto printList = [](const char* label, const std::vector<FileInfo>& list) {
548
				float count = std::min(4.5f, narrow_cast<float>(list.size()));
549
				float height = count * ImGui::GetTextLineHeightWithSpacing();
550
				im::ListBox(label, {0.0f, height}, [&]{
551
					for (const auto& f : list) {
552
						ImGui::TextUnformatted(f.filename);
553
					}
554
				});
555
			};
556
			if (!existingFiles.empty()) {
557
				ImGui::TextUnformatted("These files already exist and will be overwritten"sv);
558
				printList("##files", existingFiles);
559
			}
560
			if (!existingDirs.empty()) {
561
				ImGui::TextUnformatted("These directories already exist and will be overwritten"sv);
562
				printList("##dirs", existingDirs);
563
			}
564
			if (!duplicateEntries.empty()) {
565
				ImGui::TextUnformatted("These host entries will map to the same MSX entry"sv);
566
				im::ListBox("##duplicates", {0.0f, 4.5f * ImGui::GetTextLineHeightWithSpacing()}, [&]{
567
					for (const auto& [msxName, hostEntries] : duplicateEntries) {
568
						ImGui::Text("%s:", msxName.c_str());
569
						for (const auto& host : hostEntries) {
570
							ImGui::Text("    %s", host.filename.c_str());
571
						}
572
					}
573
				});
574
			}
575
			ImGui::Separator();
576
577
			if (!p_open) {
578
				transferHostToMsxPhase = IDLE;
579
			}
580
			if (ImGui::Button("Preserve")) {
581
				transferHostToMsxPhase = EXECUTE_PRESERVE;
582
				p_open = false;
583
			}
584
			simpleToolTip("Do not overwrite MSX files");
585
			ImGui::SameLine();
586
			if (ImGui::Button("Overwrite")) {
587
				transferHostToMsxPhase = EXECUTE_OVERWRITE;
588
				p_open = false;
589
			}
590
			simpleToolTip("Overwrite MSX files");
591
			ImGui::SameLine();
592
			if (ImGui::Button("Cancel")) {
593
				transferHostToMsxPhase = IDLE;
594
				p_open = false;
595
			}
596
			simpleToolTip("Abort transfer");
597
598
			if (!p_open) {
599
				ImGui::CloseCurrentPopup();
600
				if (stuff && transferHostToMsxPhase != IDLE) {
601
					executeTransferHostToMsx(*stuff);
602
				}
603
			}
604
		});
605
606
		const char* const newMsxDirTitle = "Create new MSX directory";
607
		if (createMsxDir) {
608
			editModal.clear();
609
			ImGui::OpenPopup(newMsxDirTitle);
610
		}
611
		p_open = true;
612
		im::PopupModal(newMsxDirTitle, &p_open, ImGuiWindowFlags_AlwaysAutoResize, [&]{
613
			ImGui::TextUnformatted("Create new directory in:"sv);
614
			ImGui::TextUnformatted(driveDisplayName(selectedDrive));
615
			ImGui::TextUnformatted(msxDir);
616
			bool close = false;
617
			bool ok = ImGui::InputText("##msxPath", &editModal, ImGuiInputTextFlags_EnterReturnsTrue);
618
			ok |= ImGui::Button("Ok");
619
			if (ok) {
620
				if (stuff) {
621
					try {
622
						stuff->tar->chdir(msxDir);
623
						stuff->tar->mkdir(editModal);
624
					} catch (MSXException& e) {
625
						manager.printError(
626
							"Couldn't create new MSX directory: ", e.getMessage());
627
					}
628
				}
629
630
				msxRefresh();
631
				close = true;
632
			}
633
			ImGui::SameLine();
634
			close |= ImGui::Button("Cancel");
635
			if (close) ImGui::CloseCurrentPopup();
636
		});
637
638
		const char* const newHostDirTitle = "Create new host directory";
639
		if (createHostDir) {
640
			editModal.clear();
641
			ImGui::OpenPopup(newHostDirTitle);
642
		}
643
		p_open = true;
644
		im::PopupModal(newHostDirTitle, &p_open, ImGuiWindowFlags_AlwaysAutoResize, [&]{
645
			ImGui::TextUnformatted("Create new directory in:"sv);
646
			ImGui::TextUnformatted(hostDir);
647
			bool close = false;
648
			bool ok = ImGui::InputText("##hostPath", &editModal, ImGuiInputTextFlags_EnterReturnsTrue);
649
			ok |= ImGui::Button("Ok");
650
			if (ok) {
651
				FileOperations::mkdirp(FileOperations::join(hostDir, editModal));
652
				hostRefresh();
653
				close = true;
654
			}
655
			ImGui::SameLine();
656
			close |= ImGui::Button("Cancel");
657
			if (close) ImGui::CloseCurrentPopup();
658
		});
659
660
		const char* const newDiskImageTitle = "Create new disk image";
661
		if (createDiskImage) {
662
			auto current = getDiskImageName();
663
			auto cwd = current.empty() ? FileOperations::getCurrentWorkingDirectory()
664
						: std::string(FileOperations::getDirName(current));
665
			auto newName = FileOperations::getNextNumberedFileName(cwd, "new-image", ".dsk");
666
			editModal = FileOperations::join(cwd, newName);
667
668
			newDiskType = UNPARTITIONED;
669
			bootType = static_cast<int>(MSXBootSectorType::DOS2);
670
			unpartitionedSize = {.count = 720, .unit = PartitionSize::KB};
671
			partitionSizes.assign(3, {.count = 32, .unit = PartitionSize::MB});
672
			ImGui::OpenPopup(newDiskImageTitle);
673
		}
674
		ImGui::SetNextWindowSize(gl::vec2{30, 22} * ImGui::GetFontSize(), ImGuiCond_FirstUseEver);
675
		p_open = true;
676
		im::PopupModal(newDiskImageTitle, &p_open, [&]{
677
			bool close = false;
678
679
			auto buttonWidth = style.ItemSpacing.x + 2.0f * style.FramePadding.x +
680
			                   ImGui::CalcTextSize(ICON_IGFD_FOLDER_OPEN).x;
681
			ImGui::SetNextItemWidth(-buttonWidth);
682
			ImGui::InputText("##newDiskPath", &editModal);
683
			simpleToolTip("Filename for new disk image");
684
			ImGui::SameLine();
685
			if (ImGui::Button(ICON_IGFD_FOLDER_OPEN"##BrowseNewImage")) {
686
				manager.openFile->selectNewFile(
687
					"Filename for new disk image",
688
					"Disk image (*.dsk){.dsk}", // only .dsk (not all other disk extensions)
689
					[&](const auto& fn) { editModal = fn; },
690
					getDiskImageName(),
691
					ImGuiOpenFile::Painter::DISKMANIPULATOR);
692
			}
693
			simpleToolTip("Select new file");
694
			if (manager.openFile->mustPaint(ImGuiOpenFile::Painter::DISKMANIPULATOR)) {
695
				manager.openFile->doPaint();
696
			}
697
698
			ImGui::SetNextItemWidth(ImGui::GetFontSize() * 6.0f);
699
			ImGui::Combo("type", &bootType, "DOS 1\0DOS 2\0Nextor\0", 3); // in sync with MSXBootSectorType enum
700
			ImGui::Separator();
701
702
			ImGui::RadioButton("No partitions (floppy disk)", &newDiskType, UNPARTITIONED);
703
			im::DisabledIndent(newDiskType != UNPARTITIONED, [&]{
704
				ImGui::SetNextItemWidth(ImGui::GetFontSize() * 5.0f);
705
				ImGui::InputScalar("##count", ImGuiDataType_U32, &unpartitionedSize.count);
706
				ImGui::SameLine();
707
				ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.5f);
708
				ImGui::Combo("##unit", &unpartitionedSize.unit, "kB\0MB\0", 2);
709
			});
710
711
			ImGui::RadioButton("Partitioned HD image", &newDiskType, PARTITIONED);
712
			im::DisabledIndent(newDiskType != PARTITIONED, [&]{
713
				float bottomHeight = 2.0f * style.ItemSpacing.y + 1.0f + 2.0f * style.FramePadding.y + ImGui::GetTextLineHeight();
714
				im::ListBox("##partitions", {14 * ImGui::GetFontSize(), -bottomHeight}, [&]{
715
					im::ID_for_range(partitionSizes.size(), [&](int i) {
716
						ImGui::AlignTextToFramePadding();
717
						ImGui::Text("%2d:", i + 1);
718
						ImGui::SameLine();
719
						ImGui::SetNextItemWidth(ImGui::GetFontSize() * 5.0f);
720
						ImGui::InputScalar("##count", ImGuiDataType_U32, &partitionSizes[i].count);
721
						ImGui::SameLine();
722
						ImGui::SetNextItemWidth(ImGui::GetFontSize() * 3.5f);
723
						ImGui::Combo("##unit", &partitionSizes[i].unit, "kB\0MB\0", 2);
724
					});
725
				});
726
				ImGui::SameLine();
727
				im::Group([&]{
728
					im::Disabled(partitionSizes.size() >= 31, [&]{
729
						if (ImGui::Button("Add")) {
730
							partitionSizes.push_back({32, PartitionSize::MB});
731
						}
732
					});
733
					im::Disabled(partitionSizes.size() <= 1, [&]{
734
						if (ImGui::Button("Remove")) {
735
							partitionSizes.pop_back();
736
						}
737
					});
738
				});
739
			});
740
741
			ImGui::Separator();
742
			im::Disabled(editModal.empty(), [&]{
743
				if (ImGui::Button("Ok")) {
744
					const auto& diskManipulator = manager.getReactor().getDiskManipulator();
745
					auto sizes = [&]{
746
						if (newDiskType == UNPARTITIONED) {
747
							return std::vector<unsigned>(1, unpartitionedSize.asSectorCount());
748
						} else {
749
							return to_vector(std::views::transform(partitionSizes, &PartitionSize::asSectorCount));
750
						}
751
					}();
752
					try {
753
						diskManipulator.create(editModal, static_cast<MSXBootSectorType>(bootType), sizes);
754
						if (auto* drive = getDrive()) {
755
							drive->insertDisk(editModal); // might fail (return code), but ignore
756
							msxRefresh();
757
						}
758
					} catch (MSXException& e) {
759
						manager.printError(
760
							"Couldn't create disk image: ", e.getMessage());
761
					}
762
					close = true;
763
				}
764
			});
765
			ImGui::SameLine();
766
			close |= ImGui::Button("Cancel");
767
			if (close) ImGui::CloseCurrentPopup();
768
		});
769
	});
770
}
771
772
void ImGuiDiskManipulator::insertMsxDisk()
773
{
774
	if (selectedDrive.starts_with("hd")) {
775
		// disallow changing HD image
776
		return;
777
	}
778
	manager.openFile->selectFile(
779
		strCat("Select disk image for ", driveDisplayName(selectedDrive)),
780
		ImGuiMedia::diskFilter(),
781
		[&](const auto& fn) {
782
			auto* drive = getDrive();
783
			if (!drive) return;
784
			drive->insertDisk(fn); // might fail (return code), but ignore
785
			msxRefresh();
786
		},
787
		getDiskImageName());
788
}
789
790
void ImGuiDiskManipulator::exportDiskImage()
791
{
792
	manager.openFile->selectNewFile(
793
		strCat("Export ", driveDisplayName(selectedDrive), " to new disk image"),
794
		"Disk image (*.dsk){.dsk}", // only .dsk (not all other disk extensions)
795
		[&](const auto& fn) {
796
			auto dd = getDriveAndDisk();
797
			if (!dd) return;
798
			const auto& [drive, disk] = *dd;
799
			assert(disk);
800
801
			try {
802
				SectorBuffer buf;
803
				File file(fn, File::OpenMode::CREATE);
804
				for (auto i : xrange(disk->getNbSectors())) {
805
					disk->readSector(i, buf);
806
					file.write(buf.raw);
807
				}
808
			} catch (MSXException& e) {
809
				manager.printError(
810
					"Couldn't export disk image: ", e.getMessage());
811
			}
812
		},
813
		getDiskImageName());
814
}
815
816
void ImGuiDiskManipulator::msxParentDirectory()
817
{
818
	if (msxDir.ends_with('/')) msxDir.pop_back();
819
	msxDir = FileOperations::getDirName(msxDir);
820
	if (msxDir.empty()) msxDir.push_back('/');
821
	msxRefresh();
822
}
823
824
void ImGuiDiskManipulator::hostParentDirectory()
825
{
826
	if (hostDir.ends_with('/')) hostDir.pop_back();
827
	hostDir = FileOperations::getDirName(hostDir);
828
	if (hostDir.empty()) hostDir.push_back('/');
829
	hostRefresh();
830
}
831
832
void ImGuiDiskManipulator::msxRefresh()
833
{
834
	editMsxDir = msxDir;
835
	msxFileCache.clear();
836
}
837
838
void ImGuiDiskManipulator::hostRefresh()
839
{
840
	editHostDir = hostDir;
841
	hostNeedRefresh = true;
842
}
843
844
bool ImGuiDiskManipulator::setupTransferHostToMsx(DrivePartitionTar& stuff)
845
{
846
	try {
847
		stuff.tar->chdir(msxDir);
848
	} catch (MSXException&) {
849
		msxRefresh();
850
		return false;
851
	}
852
853
	existingDirs.clear();
854
	existingFiles.clear();
855
	duplicateEntries.clear();
856
	for (const auto& item : hostFileCache) {
857
		if (!item.isSelected) continue;
858
		auto msxName = stuff.tar->convertToMsxName(item.filename);
859
		duplicateEntries[msxName].push_back(item);
860
		auto it = std::ranges::find(msxFileCache, msxName, &FileInfo::filename);
861
		if (it == msxFileCache.end()) continue;
862
		(it->isDirectory ? existingDirs : existingFiles).push_back(*it);
863
	}
864
	std::erase_if(duplicateEntries, [](const auto& entry) { return entry.second.size() < 2; });
865
	if (existingDirs.empty() && existingFiles.empty() && duplicateEntries.empty()) {
866
		transferHostToMsxPhase = EXECUTE_PRESERVE;
867
		executeTransferHostToMsx(stuff);
868
		return false;
869
	} else {
870
		transferHostToMsxPhase = CHECK;
871
		return true; // open modal window
872
	}
873
}
874
875
void ImGuiDiskManipulator::executeTransferHostToMsx(DrivePartitionTar& stuff)
876
{
877
	transferHostToMsxPhase = IDLE;
878
	try {
879
		stuff.tar->chdir(msxDir);
880
	} catch (MSXException&) {
881
		msxRefresh();
882
		return;
883
	}
884
885
	for (const auto& item : hostFileCache) {
886
		if (!item.isSelected) continue;
887
		try {
888
			auto add = (transferHostToMsxPhase == EXECUTE_PRESERVE)
889
			         ? MSXtar::Add::PRESERVE : MSXtar::Add::OVERWRITE;
890
			stuff.tar->addItem(FileOperations::join(hostDir, item.filename), add);
891
		} catch (MSXException& e) {
892
			manager.printError("Couldn't import ", item.filename, ": ", e.getMessage());
893
		}
894
	}
895
	msxRefresh();
896
}
897
898
void ImGuiDiskManipulator::transferMsxToHost(DrivePartitionTar& stuff)
899
{
900
	try {
901
		stuff.tar->chdir(msxDir);
902
	} catch (MSXException&) {
903
		msxRefresh();
904
		return;
905
	}
906
	for (const auto& item : msxFileCache) {
907
		if (!item.isSelected) continue;
908
		try {
909
			stuff.tar->getItemFromDir(hostDir, item.filename);
910
		} catch (MSXException& e) {
911
			manager.printError("Couldn't extract ", item.filename, ": ", e.getMessage());
912
		}
913
	}
914
	hostRefresh();
915
}
916
917
} // namespace openmsx