365 lines
11 KiB
C++
365 lines
11 KiB
C++
#include <psemek/ui/file_dialog.hpp>
|
|
|
|
#include <psemek/ui/window.hpp>
|
|
#include <psemek/ui/grid_layout.hpp>
|
|
#include <psemek/ui/button.hpp>
|
|
#include <psemek/ui/label.hpp>
|
|
#include <psemek/ui/scroller.hpp>
|
|
#include <psemek/ui/edit.hpp>
|
|
#include <psemek/ui/frame.hpp>
|
|
#include <psemek/util/to_string.hpp>
|
|
#include <psemek/util/recursive.hpp>
|
|
#include <psemek/io/memory_stream.hpp>
|
|
#include <psemek/io/file_stream.hpp>
|
|
#include <psemek/util/unicode.hpp>
|
|
#include <psemek/util/common_directories.hpp>
|
|
#include <psemek/util/to_shared.hpp>
|
|
|
|
#include <psemek/ui/resources/folder_png.hpp>
|
|
#include <psemek/ui/resources/file_png.hpp>
|
|
#include <psemek/ui/resources/back_png.hpp>
|
|
|
|
namespace psemek::ui
|
|
{
|
|
|
|
namespace
|
|
{
|
|
|
|
struct frame_style_fix
|
|
: ui::single_container
|
|
{
|
|
struct shape const & shape() const override
|
|
{
|
|
return child()->shape();
|
|
}
|
|
|
|
void reshape(math::box<float, 2> const & bbox) override
|
|
{
|
|
child()->reshape(bbox);
|
|
}
|
|
|
|
math::box<float, 2> size_constraints() const override
|
|
{
|
|
return child()->size_constraints();
|
|
}
|
|
|
|
math::interval<float> width_constraints(float height) const override
|
|
{
|
|
return child()->width_constraints(height);
|
|
}
|
|
|
|
math::interval<float> height_constraints(float width) const override
|
|
{
|
|
return child()->height_constraints(width);
|
|
}
|
|
|
|
void style_updated() const override
|
|
{
|
|
ui::single_container::style_updated();
|
|
|
|
auto style = merged_style();
|
|
auto new_style = std::make_shared<struct style>();
|
|
new_style->bg_color = style->action_color;
|
|
|
|
child()->set_own_style(new_style);
|
|
}
|
|
|
|
void draw(painter &) const override
|
|
{}
|
|
};
|
|
|
|
struct file_dialog_image_provider
|
|
: image_provider
|
|
{
|
|
file_dialog_image_provider()
|
|
{
|
|
back_.load(gfx::read_image<gfx::color_rgba>(io::memory_istream(resources::back_png.data)));
|
|
back_.linear_filter();
|
|
|
|
folder_.load(gfx::read_image<gfx::color_rgba>(io::memory_istream(resources::folder_png.data)));
|
|
folder_.linear_filter();
|
|
|
|
file_.load(gfx::read_image<gfx::color_rgba>(io::memory_istream(resources::file_png.data)));
|
|
file_.linear_filter();
|
|
}
|
|
|
|
gfx::texture_view_2d get(std::string_view const & id) const override
|
|
{
|
|
if (id == "back")
|
|
return gfx::texture_view_2d{&back_};
|
|
if (id == "folder")
|
|
return gfx::texture_view_2d{&folder_};
|
|
if (id == "file")
|
|
return gfx::texture_view_2d{&file_};
|
|
return {};
|
|
}
|
|
|
|
private:
|
|
gfx::texture_2d back_;
|
|
gfx::texture_2d folder_;
|
|
gfx::texture_2d file_;
|
|
};
|
|
|
|
std::string replace(std::string str, std::string const & match, std::string const & replacement)
|
|
{
|
|
for (std::size_t i = 0; (i = str.find(match, i)) != std::string::npos;)
|
|
{
|
|
str.replace(i, match.size(), replacement);
|
|
i += replacement.size();
|
|
}
|
|
return str;
|
|
}
|
|
|
|
std::string escape(std::string str)
|
|
{
|
|
str = replace(std::move(str), "\\", "\\\\");
|
|
str = replace(std::move(str), "[", "\\[");
|
|
str = replace(std::move(str), "]", "\\]");
|
|
return str;
|
|
}
|
|
|
|
}
|
|
|
|
std::shared_ptr<window> make_file_dialog(file_dialog_options const & options)
|
|
{
|
|
auto window = options.element_factory.make_window(options.caption);
|
|
|
|
auto main_layout = options.element_factory.make_grid_layout();
|
|
main_layout->set_size(4, options.extra_widget ? 3 : 2);
|
|
main_layout->set_column_weight(0, 0.f);
|
|
if (options.extra_widget)
|
|
main_layout->set_column_weight(2, 0.f);
|
|
main_layout->set_row_weight(0, 0.f);
|
|
main_layout->set_row_weight(2, 0.f);
|
|
main_layout->set_row_weight(3, 0.f);
|
|
|
|
auto image_provider = std::make_shared<file_dialog_image_provider>();
|
|
|
|
auto current_path = std::make_shared<std::filesystem::path>();
|
|
auto selected_path = std::make_shared<std::filesystem::path>();
|
|
|
|
auto back_button = options.element_factory.make_button(image_provider->get("back"));
|
|
back_button->icon()->set_downscale(false);
|
|
|
|
auto back_button_style = std::make_shared<style>();
|
|
back_button_style->inner_margin = math::vector{2, 2};
|
|
back_button->set_style(back_button_style);
|
|
|
|
auto common_paths_label = options.element_factory.make_label("");
|
|
|
|
auto common_paths = util::to_shared(util::common_directories());
|
|
{
|
|
std::string str;
|
|
int index = 0;
|
|
for (auto const & dir : *common_paths)
|
|
{
|
|
str += util::to_string("[link:", index++, "]", escape(dir.first), "[/link]\n");
|
|
}
|
|
common_paths_label->set_tagged_text(str);
|
|
}
|
|
|
|
auto current_path_label = options.element_factory.make_label("");
|
|
current_path_label->set_valign(label::valignment::center);
|
|
|
|
auto directory_view_frame_style_fix = std::make_shared<frame_style_fix>();
|
|
auto directory_view_frame = options.element_factory.make_frame();
|
|
directory_view_frame->set_fixed_size(math::vector{600.f, 400.f});
|
|
auto directory_view_scroller = options.element_factory.make_scroller();
|
|
auto directory_view = options.element_factory.make_label("");
|
|
|
|
auto directory_view_style = std::make_shared<style>();
|
|
directory_view_style->link_color = gfx::white.as_color_rgba();
|
|
directory_view->set_style(directory_view_style);
|
|
|
|
auto path_edit = options.element_factory.make_edit();
|
|
path_edit->set_enabled(options.type == file_dialog_type::save);
|
|
|
|
auto bottom_layout = options.element_factory.make_grid_layout();
|
|
bottom_layout->set_outer_margin(false);
|
|
bottom_layout->set_size(1, 4);
|
|
bottom_layout->set_column_weight(0, 0.f);
|
|
bottom_layout->set_column_weight(2, 0.f);
|
|
bottom_layout->set_column_weight(3, 0.f);
|
|
|
|
auto status_label = options.element_factory.make_label("");
|
|
|
|
auto status_label_style = std::make_shared<style>();
|
|
status_label_style->text_color = gfx::red.as_color_rgba();
|
|
status_label->set_style(status_label_style);
|
|
|
|
auto confirm_button = options.element_factory.make_button(options.type == file_dialog_type::save ? "Save" : "Open");
|
|
auto cancel_button = options.element_factory.make_button("Cancel");
|
|
|
|
confirm_button->set_hidden(true);
|
|
|
|
bottom_layout->set(0, 1, status_label);
|
|
bottom_layout->set(0, 2, confirm_button);
|
|
bottom_layout->set(0, 3, cancel_button);
|
|
|
|
directory_view_scroller->set_child(directory_view);
|
|
directory_view_frame->set_child(directory_view_scroller);
|
|
directory_view_frame_style_fix->set_child(directory_view_frame);
|
|
|
|
main_layout->set(0, 0, back_button);
|
|
main_layout->set(1, 0, common_paths_label);
|
|
main_layout->set(0, 1, current_path_label);
|
|
main_layout->set(1, 1, directory_view_frame_style_fix);
|
|
main_layout->set(2, 1, path_edit);
|
|
main_layout->set(3, 1, bottom_layout);
|
|
if (options.extra_widget)
|
|
main_layout->set(1, 2, options.extra_widget);
|
|
|
|
window->set_child(main_layout);
|
|
|
|
directory_view->set_image_provider(image_provider.get());
|
|
directory_view->data() = image_provider;
|
|
|
|
auto set_path_callback = std::make_shared<std::function<void(std::filesystem::path)>>();
|
|
|
|
*set_path_callback = [=, type = options.type, on_visited = options.on_visited](std::filesystem::path new_path)
|
|
{
|
|
new_path = std::filesystem::canonical(std::filesystem::absolute(new_path));
|
|
|
|
try
|
|
{
|
|
[[maybe_unused]] auto test_iterator = std::filesystem::directory_iterator(new_path);
|
|
}
|
|
catch (std::filesystem::filesystem_error const & error)
|
|
{
|
|
status_label->set_text(error.code().message());
|
|
return;
|
|
}
|
|
|
|
if (on_visited)
|
|
on_visited(new_path);
|
|
|
|
*current_path = new_path;
|
|
|
|
status_label->set_text("");
|
|
|
|
back_button->on_click([set_path_callback, new_path]{
|
|
(*set_path_callback)(new_path.parent_path());
|
|
});
|
|
|
|
{
|
|
std::string path_str;
|
|
|
|
int index = 0;
|
|
for (auto const & component : new_path)
|
|
{
|
|
if (!new_path.root_name().empty() && component == new_path.root_directory())
|
|
continue;
|
|
|
|
path_str += "[link:" + util::to_string(++index) + "]" + escape(component.string()) + "[/link]";
|
|
if (!new_path.root_name().empty() || component != new_path.root_directory())
|
|
path_str += escape(std::string(1, char(std::filesystem::path::preferred_separator)));
|
|
}
|
|
current_path_label->set_tagged_text(std::move(path_str));
|
|
|
|
current_path_label->on_link_click([set_path_callback, new_path, count = index](std::string_view const & value){
|
|
int index = util::from_string<int>(std::string{value});
|
|
auto path = new_path;
|
|
for (int i = 0; i < count - index; ++i)
|
|
path = path.parent_path();
|
|
(*set_path_callback)(path);
|
|
});
|
|
}
|
|
|
|
{
|
|
std::string contents_str;
|
|
|
|
struct directory_entry
|
|
{
|
|
bool folder;
|
|
std::filesystem::path path;
|
|
};
|
|
|
|
std::vector<directory_entry> entries;
|
|
|
|
for (auto const & entry : std::filesystem::directory_iterator(new_path))
|
|
entries.push_back({std::filesystem::is_directory(entry.path()), entry.path()});
|
|
|
|
std::sort(entries.begin(), entries.end(), [](directory_entry const & e1, directory_entry const & e2){
|
|
if (e1.folder && !e2.folder)
|
|
return true;
|
|
|
|
if (!e1.folder && e2.folder)
|
|
return false;
|
|
|
|
return e1.path.filename().string() < e2.path.filename().string();
|
|
});
|
|
|
|
int index = 0;
|
|
for (auto const & entry : entries)
|
|
{
|
|
if (entry.folder)
|
|
contents_str += "[image:folder]";
|
|
else
|
|
contents_str += "[image:file]";
|
|
|
|
contents_str += "[link:" + util::to_string(index++) + "]";
|
|
contents_str += escape(entry.path.filename().string()) + "[/link]\n";
|
|
}
|
|
directory_view->set_tagged_text(std::move(contents_str));
|
|
directory_view_scroller->set_position(scroller::direction::vertical, 0.f, false);
|
|
|
|
directory_view->on_link_click([=, entries = std::move(entries)](std::string_view const & value){
|
|
int index = util::from_string<int>(std::string{value});
|
|
if (entries[index].folder)
|
|
(*set_path_callback)(entries[index].path);
|
|
else
|
|
{
|
|
auto path = entries[index].path;
|
|
|
|
try
|
|
{
|
|
if (type == file_dialog_type::load)
|
|
{
|
|
[[maybe_unused]] io::file_istream test(path);
|
|
}
|
|
else
|
|
{
|
|
[[maybe_unused]] io::file_ostream test(path, io::file_ostream::append);
|
|
}
|
|
path_edit->set_text(path.filename().string());
|
|
status_label->set_text("");
|
|
*selected_path = path;
|
|
}
|
|
catch (std::exception const & error)
|
|
{
|
|
status_label->set_text(error.what());
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
common_paths_label->on_link_click([common_paths, set_path_callback](std::string const & link){
|
|
int index = util::from_string<int>(link);
|
|
(*set_path_callback)((*common_paths)[index].second);
|
|
});
|
|
|
|
path_edit->on_text_changed([=](std::u32string_view const & text_u32){
|
|
std::string text = util::to_utf8(std::u32string(text_u32));
|
|
*selected_path = *current_path / text;
|
|
confirm_button->set_hidden(text.empty());
|
|
});
|
|
|
|
cancel_button->on_click([=]{
|
|
window->close();
|
|
});
|
|
|
|
window->on_close(options.on_canceled);
|
|
|
|
confirm_button->on_click([=, on_selected = options.on_selected]{
|
|
window->close();
|
|
if (on_selected)
|
|
on_selected(*selected_path);
|
|
});
|
|
|
|
(*set_path_callback)(options.path);
|
|
|
|
return window;
|
|
}
|
|
|
|
}
|