OXYGENHUB
◆ main openmsx / src / imgui / FileListWidget.cc
6.6 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "FileListWidget.hh"
2
3
#include "ImGuiCpp.hh"
4
#include "ImGuiUtils.hh"
5
6
#include "FileContext.hh"
7
#include "FileOperations.hh"
8
9
#include "foreach_file.hh"
10
#include "zstring_view.hh"
11
12
#include <array>
13
#include <cassert>
14
#include <filesystem>
15
#include <span>
16
17
namespace openmsx {
18
19
static std::string_view formatFileTimeFull(std::time_t fileTime, std::span<char, 32> buf)
20
{
21
	// Convert time_t to local time (broken-down time in the local time zone)
22
	const std::tm* local_time = std::localtime(&fileTime);
23
24
	// Get the local time in human-readable format
25
	auto len = std::strftime(buf.data(), buf.size(), "%F %T", local_time);
26
	assert(len > 0); // buf.size == 32 should always be sufficient for this format
27
	return {buf.data(), len};
28
}
29
30
static std::string_view formatFileAbbreviated(std::time_t fileTime, std::span<char, 32> buf)
31
{
32
	// Convert time_t to local time (broken-down time in the local time zone)
33
	std::tm local_time = *std::localtime(&fileTime);
34
35
	std::time_t t_now = std::time(nullptr); // get time now
36
	std::tm now = *std::localtime(&t_now);
37
38
	auto format = ((now.tm_mday == local_time.tm_mday) &&
39
	               (now.tm_mon  == local_time.tm_mon ) &&
40
	               (now.tm_year == local_time.tm_year)) ? zstring_view("%T") : zstring_view("%F");
41
42
	// Get the local time in human-readable format
43
	auto len = std::strftime(buf.data(), buf.size(), format.c_str(), &local_time);
44
	assert(len > 0); // buf.size == 32 should always be sufficient for these formats
45
	return {buf.data(), len};
46
};
47
48
FileListWidget::FileListWidget(
49
		std::string_view fileType_, std::string_view extension_, std::string_view directory_)
50
	: fileType(fileType_), extension(extension_), directory(directory_)
51
	, confirmDelete(strCat("Confirm delete##filelist-", fileType))
52
{
53
	// setup default actions
54
	drawAction = [&] { drawTable(); };
55
	hoverAction = [this](const Entry& entry) { defaultHoverAction(entry); };
56
	deleteAction = [](const Entry& entry) { defaultDeleteAction(entry); };
57
	displayName = [](const Entry& entry) { return std::string(entry.getDefaultDisplayName()); };
58
}
59
60
bool FileListWidget::menu(const char* text)
61
{
62
	menuOpen = im::Menu(text, [&]{ draw(); });
63
	return menuOpen;
64
}
65
66
bool FileListWidget::menu(const char* text, bool enabled)
67
{
68
	menuOpen = im::Menu(text, enabled, [&]{ draw(); });
69
	return menuOpen;
70
}
71
72
void FileListWidget::draw()
73
{
74
	if (!menuOpen) scanDirectory(); // only re-scan on transition from closed -> open
75
	drawAction(); // calls drawTable() (and possibly does more)
76
}
77
78
void FileListWidget::defaultHoverAction(const Entry& entry) const
79
{
80
	simpleToolTip(displayName(entry));
81
}
82
83
std::string_view FileListWidget::Entry::getDefaultDisplayName() const
84
{
85
	return FileOperations::stem(fullName);
86
}
87
88
void FileListWidget::defaultDeleteAction(const Entry& entry) {
89
	FileOperations::unlink(entry.fullName);
90
}
91
92
[[nodiscard]] static time_t fileTimeToTimeT(std::filesystem::file_time_type fileTime)
93
{
94
	// As we still want to support gcc-11 (and not require 13 yet), we have to use this method
95
	// as workaround instead of using more modern std::chrono and std::format stuff in all time
96
	// calculations and formatting.
97
98
	// Convert file_time_type to system_clock::time_point (the standard clock since epoch)
99
	auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(
100
		fileTime - std::filesystem::file_time_type::clock::now() +
101
		std::chrono::system_clock::now());
102
103
	// Convert system_clock::time_point to time_t (time since Unix epoch in seconds)
104
	return std::chrono::system_clock::to_time_t(sctp);
105
}
106
107
void FileListWidget::scanDirectory()
108
{
109
	entries.clear();
110
	for (auto context = userDataFileContext(directory);
111
	     const auto& path : context.getPaths()) {
112
		foreach_file(path, [&](const std::string& fullName, std::string_view fileName) {
113
			if (fileName.ends_with(extension)) {
114
				std::filesystem::file_time_type ftime = std::filesystem::last_write_time(fullName);
115
				entries.emplace_back(fullName, fileTimeToTimeT(ftime));
116
			}
117
		});
118
	}
119
	needSort = true;
120
}
121
122
void FileListWidget::drawTable()
123
{
124
	if (entries.empty()) {
125
		ImGui::TextUnformatted(tmpStrCat("No ", fileType, " files found"));
126
		return;
127
	}
128
129
	int tableFlags = ImGuiTableFlags_RowBg |
130
		ImGuiTableFlags_BordersV |
131
		ImGuiTableFlags_BordersOuter |
132
		ImGuiTableFlags_Resizable |
133
		ImGuiTableFlags_Sortable |
134
		ImGuiTableFlags_Hideable |
135
		ImGuiTableFlags_Reorderable |
136
		ImGuiTableFlags_ContextMenuInBody |
137
		ImGuiTableFlags_ScrollY |
138
		ImGuiTableFlags_SizingStretchProp;
139
	im::Table("##select-filelist", 2, tableFlags, ImVec2(ImGui::GetFontSize() * 25.0f, 240.0f), [&]{
140
		ImGui::TableSetupScrollFreeze(0, 1); // Make top row always visible
141
		ImGui::TableSetupColumn("Name");
142
		ImGui::TableSetupColumn("Date/time", ImGuiTableColumnFlags_DefaultSort | ImGuiTableColumnFlags_PreferSortDescending | ImGuiTableColumnFlags_WidthFixed);
143
		ImGui::TableHeadersRow();
144
145
		// check sort order
146
		if (auto* sortSpecs = ImGui::TableGetSortSpecs();
147
		    sortSpecs->SpecsDirty || needSort) {
148
			sortSpecs->SpecsDirty = false;
149
			needSort = false;
150
			assert(sortSpecs->SpecsCount == 1);
151
			assert(sortSpecs->Specs);
152
			assert(sortSpecs->Specs->SortOrder == 0);
153
154
			switch (sortSpecs->Specs->ColumnIndex) {
155
			case 0: // name
156
				sortUpDown_String(entries, sortSpecs, &FileListWidget::Entry::getDefaultDisplayName);
157
				break;
158
			case 1: // time
159
				sortUpDown_T(entries, sortSpecs, &FileListWidget::Entry::ftime);
160
				break;
161
			default:
162
				UNREACHABLE;
163
			}
164
		}
165
166
		for (const auto& entry : entries) {
167
			if (ImGui::TableNextColumn()) {
168
				imColor color = displayColor ? displayColor(entry) : imColor::TEXT;
169
				im::StyleColor(ImGuiCol_Text, getColor(color), [&]{
170
					if (ImGui::Selectable(displayName(entry).c_str(), false, ImGuiSelectableFlags_SpanAllColumns | ImGuiSelectableFlags_AllowOverlap | (doubleClickAction ? ImGuiSelectableFlags_AllowDoubleClick : 0))) {
171
						if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
172
							doubleClickAction(entry);
173
						} else {
174
							singleClickAction(entry);
175
						}
176
					}
177
				});
178
				if (hoverAction && ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) {
179
					hoverAction(entry);
180
				}
181
				if (deleteAction) {
182
					im::PopupContextItem([&]{
183
						if (ImGui::MenuItem("delete")) {
184
							confirmDelete.open(
185
								strCat("Delete ", fileType, " file '", entry.getDefaultDisplayName(), "'?"),
186
								[entry, this]{
187
									deleteAction(entry);
188
									scanDirectory();
189
								});
190
						}
191
					});
192
				}
193
			}
194
			if (ImGui::TableNextColumn()) {
195
				std::array<char, 32> buf;
196
				ImGui::TextUnformatted(formatFileAbbreviated(entry.ftime, buf));
197
				simpleToolTip([&] { return formatFileTimeFull(entry.ftime, buf); });
198
			}
199
	}});
200
	confirmDelete.execute();
201
}
202
203
} // namespace openmsx