Add a file dialog implementation
This commit is contained in:
parent
9481997034
commit
f31e97edde
4 changed files with 335 additions and 0 deletions
33
libs/ui/include/psemek/ui/file_dialog.hpp
Normal file
33
libs/ui/include/psemek/ui/file_dialog.hpp
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#pragma once
|
||||
|
||||
#include <psemek/ui/element.hpp>
|
||||
#include <psemek/ui/window.hpp>
|
||||
#include <psemek/ui/element_factory.hpp>
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
|
||||
namespace psemek::ui
|
||||
{
|
||||
|
||||
enum class file_dialog_type
|
||||
{
|
||||
save,
|
||||
load
|
||||
};
|
||||
|
||||
struct file_dialog_options
|
||||
{
|
||||
struct element_factory & element_factory;
|
||||
file_dialog_type type;
|
||||
std::string caption;
|
||||
std::filesystem::path path;
|
||||
std::function<void(std::filesystem::path const &)> on_visited = nullptr;
|
||||
std::function<void(std::filesystem::path const &)> on_selected = nullptr;
|
||||
std::function<void()> on_canceled = nullptr;
|
||||
std::shared_ptr<ui::element> extra_widget = nullptr;
|
||||
};
|
||||
|
||||
std::shared_ptr<window> make_file_dialog(file_dialog_options const & options);
|
||||
|
||||
}
|
||||
BIN
libs/ui/resources/back.png
Normal file
BIN
libs/ui/resources/back.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 645 B |
BIN
libs/ui/resources/folder.png
Normal file
BIN
libs/ui/resources/folder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.5 KiB |
302
libs/ui/source/file_dialog.cpp
Normal file
302
libs/ui/source/file_dialog.cpp
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
#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/ui/resources/folder_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(geom::box<float, 2> const & bbox) override
|
||||
{
|
||||
child()->reshape(bbox);
|
||||
}
|
||||
|
||||
geom::box<float, 2> size_constraints() const override
|
||||
{
|
||||
return child()->size_constraints();
|
||||
}
|
||||
|
||||
geom::interval<float> width_constraints(float height) const override
|
||||
{
|
||||
return child()->width_constraints(height);
|
||||
}
|
||||
|
||||
geom::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_png(io::memory_istream(resources::back_png.data)));
|
||||
back_.linear_filter();
|
||||
|
||||
folder_.load(gfx::read_png(io::memory_istream(resources::folder_png.data)));
|
||||
folder_.linear_filter();
|
||||
|
||||
{
|
||||
gfx::pixmap_rgba empty({folder_.width(), folder_.height()}, gfx::color_rgba{0, 0, 0, 0});
|
||||
empty_.load(empty);
|
||||
empty_.nearest_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 == "empty")
|
||||
return gfx::texture_view_2d{&empty_};
|
||||
return {};
|
||||
}
|
||||
|
||||
private:
|
||||
gfx::texture_2d back_;
|
||||
gfx::texture_2d folder_;
|
||||
gfx::texture_2d empty_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
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 = geom::vector{2, 2};
|
||||
back_button->set_style(back_button_style);
|
||||
|
||||
auto common_paths_label = options.element_factory.make_label("Home\nRoot");
|
||||
|
||||
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(geom::vector{600.f, 400.f});
|
||||
auto directory_view_scroller = options.element_factory.make_scroller();
|
||||
auto directory_view = options.element_factory.make_label("Folder\nFolder\nFile\nFile");
|
||||
|
||||
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");
|
||||
|
||||
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::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 (component.string() != "/")
|
||||
path_str += "[link:" + util::to_string(++index) + "]" + component.string() + "[/link]/";
|
||||
else
|
||||
path_str += "[link:" + util::to_string(++index) + "]" + component.string() + "[/link]";
|
||||
}
|
||||
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;
|
||||
int index = 0;
|
||||
for (auto const & entry : std::filesystem::directory_iterator(new_path))
|
||||
{
|
||||
if (std::filesystem::is_directory(entry.path()))
|
||||
contents_str += "[image:folder]";
|
||||
else
|
||||
contents_str += "[image:empty]";
|
||||
|
||||
contents_str += "[link:" + util::to_string(index++) + "]";
|
||||
contents_str += entry.path().filename().string() + "[/link]\n";
|
||||
}
|
||||
directory_view->set_tagged_text(std::move(contents_str));
|
||||
|
||||
directory_view->on_link_click([=](std::string_view const & value){
|
||||
int index = util::from_string<int>(std::string{value});
|
||||
auto path = std::next(std::filesystem::directory_iterator(new_path), index)->path();
|
||||
if (std::filesystem::is_directory(path))
|
||||
(*set_path_callback)(path);
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
if (type == file_dialog_type::load)
|
||||
{
|
||||
[[maybe_unused]] io::file_istream test(path);
|
||||
}
|
||||
else
|
||||
{
|
||||
[[maybe_unused]] io::file_ostream test(path);
|
||||
}
|
||||
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());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
path_edit->on_text_entered([=](std::u32string_view const & text_u32){
|
||||
std::string text = util::to_utf8(std::u32string(text_u32));
|
||||
*selected_path = *current_path / text;
|
||||
}, false);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue