ACloudViewer  3.9.4
A Modern Library for 3D Data Processing
FileDialog.cpp
Go to the documentation of this file.
1 // ----------------------------------------------------------------------------
2 // - CloudViewer: www.cloudViewer.org -
3 // ----------------------------------------------------------------------------
4 // Copyright (c) 2018-2024 www.cloudViewer.org
5 // SPDX-License-Identifier: MIT
6 // ----------------------------------------------------------------------------
7 
8 // Include FileDialog here to get value of GUI_USE_NATIVE_FILE_DIALOG
10 
11 #if defined(__APPLE__) && GUI_USE_NATIVE_FILE_DIALOG
12 // see FileDialogNative.cpp
13 #else
14 
15 #include <FileSystem.h>
16 #include <Helper.h>
17 #include <Logging.h>
18 
19 #include <string>
20 #include <unordered_map>
21 #include <unordered_set>
22 
30 #include "visualization/gui/Util.h"
31 
32 // macOS sorts directories in with the files
33 // Windows and Linux (GTK) sort directories first.
34 #ifdef __APPLE__
35 #define INLINE_DIRS 1
36 #else
37 #define INLINE_DIRS 0
38 #endif // __APPLE__
39 
40 namespace cloudViewer {
41 namespace visualization {
42 namespace gui {
43 
44 namespace {
45 // The current path of the dialog should persist across runs of the dialog.
46 // This is defined here rather than in the class definition because we don't
47 // need to be exporting internal details of the class.
48 static std::string g_file_dialog_dir;
49 } // namespace
50 
51 class DirEntry {
52 public:
53  enum Type { DIR, FILE };
54 
55  DirEntry(const std::string &name, Type type) {
56  type_ = type;
57  name_ = name;
58  if (type == DIR) {
59  display_ = std::string("[ ] ") + name_ + "/";
60  } else {
61  display_ = std::string(" ") + name_;
62  }
63  }
64 
65  Type GetType() const { return type_; }
66  const std::string &GetName() const { return name_; }
67  const std::string &GetDisplayText() const { return display_; }
68 
69  bool operator==(const DirEntry &rhs) const {
70  return (type_ == rhs.type_ && name_ == rhs.name_);
71  }
72  bool operator!=(const DirEntry &rhs) const {
73  return !this->operator==(rhs);
74  }
75 
76  bool operator<(const DirEntry &rhs) const {
77 #if INLINE_DIRS
78  // Sort directories by name; if the OS allows directories and files
79  // to have the same name, put directories first.
80  if (name_ == rhs.name_) {
81  if (type_ == rhs.type_) {
82  return false;
83  } else {
84  return (type_ == DIR);
85  }
86  } else {
87  return (name_ < rhs.name_);
88  }
89 #else
90  // Sort directories first, then files.
91  // Within each category sort by name.
92  if (type_ == rhs.type_) {
93  return (name_ < rhs.name_);
94  } else {
95  return (type_ == DIR);
96  }
97 #endif // INLINE_DIRS
98  }
99 
100 private:
101  Type type_;
102  std::string name_;
103  std::string display_;
104 };
105 
108  std::vector<DirEntry> entries_;
109  std::shared_ptr<TextEdit> filename_;
110  std::shared_ptr<Combobox> dirtree_;
111  std::shared_ptr<ListView> filelist_;
112  std::shared_ptr<Combobox> filter_;
113  std::unordered_map<int, std::unordered_set<std::string>>
115  std::shared_ptr<Horiz> filter_row_;
116  std::shared_ptr<Button> ok_;
117  std::shared_ptr<Button> cancel_;
118  std::function<void()> on_cancel_;
119  std::function<void(const char *)> on_done_;
120 
122  static DirEntry g_bogus("", DirEntry::Type::FILE);
123 
124  int idx = filelist_->GetSelectedIndex();
125  if (idx >= 0) {
126  return entries_[idx];
127  } else {
128  return g_bogus;
129  }
130  }
131 
133  auto path = CalcCurrentDirectory();
134 
135  std::vector<std::string> raw_subdirs, raw_files;
136  utility::filesystem::ListDirectory(path, raw_subdirs, raw_files);
137 
138  entries_.clear();
139  entries_.reserve(raw_subdirs.size() + raw_files.size());
140  for (auto &dir : raw_subdirs) {
142  entries_.emplace_back(d, DirEntry::Type::DIR);
143  }
144  std::unordered_set<std::string> filter;
145  auto it = filter_idx_2_filter.find(filter_->GetSelectedIndex());
146  if (it != filter_idx_2_filter.end()) {
147  filter = it->second;
148  }
149  for (auto &file : raw_files) {
152  if (!ext.empty()) {
153  ext = std::string(".") + ext;
154  }
155  if (filter.empty() || filter.find(ext) != filter.end()) {
156  entries_.emplace_back(f, DirEntry::Type::FILE);
157  }
158  }
159  std::sort(entries_.begin(), entries_.end());
160 
161  // Include an entry for ".." for convenience on Linux.
162  // Don't do this on macOS because the native dialog has neither
163  // a back button nor a "..". Non-technical users aren't going to
164  // have any idea what ".." means, so its unclear if this should
165  // go in Windows, too, or just Linux (which is pretty much all
166  // technical people). Windows' file dialog does have some sense
167  // of "previous directory", though, so maybe it's okay if we
168  // include an up icon.
169 #ifndef __APPLE__
170  if (path != "/") {
171  entries_.insert(entries_.begin(),
172  DirEntry("..", DirEntry::Type::DIR));
173  }
174 #endif // __APPLE__
175 
176  std::vector<std::string> display;
177  display.reserve(entries_.size());
178  for (auto &e : entries_) {
179  display.push_back(e.GetDisplayText());
180  }
181 
182  filelist_->SetSelectedIndex(-1);
183  if (mode_ == Mode::OPEN) {
184  filename_->SetText("");
185  UpdateOk();
186  }
187  filelist_->SetItems(display);
188  }
189 
190  std::string CalcCurrentDirectory() const {
191 #ifdef _WIN32
192  const int nSkipSlash = 1;
193 #else
194  const int nSkipSlash = 2; // 0 is "/", so don't need "/" until 2.
195 #endif // _WIN32
196  auto idx = dirtree_->GetSelectedIndex();
197  std::string path;
198  for (int i = 0; i <= idx; ++i) {
199  if (i >= nSkipSlash) {
200  path += "/";
201  }
202  path += dirtree_->GetItem(i);
203  }
204  return path;
205  }
206 
207  void UpdateOk() {
208  ok_->SetEnabled(std::string(filename_->GetText()) != "");
209  }
210 };
211 
212 FileDialog::FileDialog(Mode mode, const char *title, const Theme &theme)
213  : Dialog("File"), impl_(new FileDialog::Impl()) {
214  auto em = theme.font_size;
215  auto layout = std::make_shared<Vert>(int(std::ceil(0.5 * em)), Margins(em));
216  impl_->mode_ = mode;
217 
218  // 'filename' needs to always exist, as we use it to store the name of
219  // the picked file, however, it is only displayed for SAVE.
220  impl_->filename_ = std::make_shared<TextEdit>();
221  if (mode == Mode::SAVE) {
222  auto filenameLabel = std::make_shared<Label>("Save as:");
223  auto horiz = std::make_shared<Horiz>();
224  horiz->AddStretch();
225  horiz->AddChild(filenameLabel);
226  horiz->AddChild(impl_->filename_);
227  horiz->AddStretch();
228  layout->AddChild(horiz);
229  }
230 
231  impl_->dirtree_ = std::make_shared<Combobox>();
232  layout->AddChild(Horiz::MakeCentered(impl_->dirtree_));
233 
234  impl_->filelist_ = std::make_shared<ListView>();
235  layout->AddChild(impl_->filelist_);
236 
237  impl_->cancel_ = std::make_shared<Button>("Cancel");
238  if (mode == Mode::OPEN) {
239  impl_->ok_ = std::make_shared<Button>("Open");
240  } else if (mode == Mode::SAVE) {
241  impl_->ok_ = std::make_shared<Button>("Save");
242  }
243 
244  impl_->filter_ = std::make_shared<Combobox>();
245  auto filter_label = std::make_shared<Label>("File type:");
246  impl_->filter_row_ = std::make_shared<Horiz>();
247  impl_->filter_row_->AddStretch();
248  impl_->filter_row_->AddChild(filter_label);
249  impl_->filter_row_->AddChild(impl_->filter_);
250  impl_->filter_row_->AddStretch();
251  impl_->filter_row_->SetVisible(false);
252  layout->AddChild(impl_->filter_row_);
253 
254  auto horiz = std::make_shared<Horiz>(em);
255  horiz->AddStretch();
256  horiz->AddChild(impl_->cancel_);
257  horiz->AddChild(impl_->ok_);
258  layout->AddChild(horiz);
259  this->AddChild(layout);
260 
261  impl_->filename_->SetOnTextChanged(
262  [this](const char *) { this->impl_->UpdateOk(); });
263  impl_->dirtree_->SetOnValueChanged([this](const char *, int) {
264  this->impl_->UpdateDirectoryListing();
265  });
266  impl_->filelist_->SetOnValueChanged([this](const char *value,
267  bool is_double_click) {
268  auto &entry = this->impl_->GetSelectedEntry();
269  if (is_double_click) {
270  if (entry.GetType() == DirEntry::Type::FILE) {
271  this->OnDone();
272  return;
273  } else {
274  auto new_dir = this->impl_->CalcCurrentDirectory();
275  new_dir = new_dir + "/" + entry.GetName();
276  this->SetPath(new_dir.c_str());
277  }
278  } else {
279  if (entry.GetType() == DirEntry::Type::FILE) {
280  this->impl_->filename_->SetText(entry.GetName().c_str());
281  } else {
282  if (this->impl_->mode_ == Mode::OPEN) {
283  this->impl_->filename_->SetText("");
284  }
285  }
286  }
287  this->impl_->UpdateOk();
288  });
289  impl_->filter_->SetOnValueChanged([this](const char *, int) {
290  this->impl_->UpdateDirectoryListing(); // re-filter directory
291  });
292  impl_->cancel_->SetOnClicked([this]() {
293  if (this->impl_->on_cancel_) {
294  this->impl_->on_cancel_();
295  } else {
296  utility::LogError("FileDialog: need to call SetOnClicked()");
297  }
298  });
299  impl_->ok_->SetOnClicked([this]() { this->OnDone(); });
300 
301  if (g_file_dialog_dir == "") {
302  g_file_dialog_dir = utility::filesystem::GetWorkingDirectory();
303  }
304  SetPath(g_file_dialog_dir.c_str());
305 
306  impl_->UpdateOk();
307 }
308 
309 FileDialog::~FileDialog() {}
310 
311 void FileDialog::SetPath(const char *path) {
312  auto components = utility::filesystem::GetPathComponents(path);
313 
314  std::string dirpath = "";
315  for (auto &dir : components) {
316  if (dirpath != "" && dirpath != "/") {
317  dirpath += "/";
318  }
319  dirpath += dir;
320  }
321  bool is_dir = utility::filesystem::DirectoryExists(dirpath);
322 
323  impl_->dirtree_->ClearItems();
324  int n = int(is_dir ? components.size() : components.size() - 1);
325  for (int i = 0; i < n; ++i) {
326  impl_->dirtree_->AddItem(components[i].c_str());
327  }
328  impl_->dirtree_->SetSelectedIndex(n - 1);
329  impl_->UpdateDirectoryListing();
330  if (is_dir) {
331  g_file_dialog_dir = dirpath;
332  }
333 
334  if (!is_dir) {
335  impl_->filename_->SetText(components.back().c_str());
336  }
337 }
338 
339 void FileDialog::AddFilter(const char *filter, const char *description) {
340  std::vector<std::string> exts = utility::SplitString(filter, ", ");
341 
342  std::unordered_set<std::string> ext_filter;
343  for (auto &ext : exts) {
344  ext_filter.insert(ext);
345  }
346 
347  bool first_filter = impl_->filter_idx_2_filter.empty();
348  impl_->filter_idx_2_filter[int(impl_->filter_idx_2_filter.size())] =
349  ext_filter;
350  impl_->filter_->AddItem(description);
351  if (first_filter) {
352  impl_->filter_->SetSelectedIndex(0);
353  impl_->UpdateDirectoryListing(); // apply filter
354  }
355  impl_->filter_row_->SetVisible(true);
356 }
357 
358 void FileDialog::SetOnCancel(std::function<void()> on_cancel) {
359  impl_->on_cancel_ = on_cancel;
360 }
361 
362 void FileDialog::SetOnDone(std::function<void(const char *)> on_done) {
363  impl_->on_done_ = on_done;
364 }
365 
366 void FileDialog::OnWillShow() {}
367 
368 void FileDialog::OnDone() {
369  if (this->impl_->on_done_) {
370  auto dir = this->impl_->CalcCurrentDirectory();
372  std::string name = this->impl_->filename_->GetText();
373  // If the user didn't specify an extension, automatically add one
374  // (unless we don't have the any-files (*.*) filter selected).
375  if (name.find(".") == std::string::npos && !name.empty()) {
376  int idx = this->impl_->filter_->GetSelectedIndex();
377  if (idx >= 0) {
378  auto &exts = impl_->filter_idx_2_filter[idx];
379  // Prefer PNG if available (otherwise in a list of common
380  // image files, e.g., ".jpg .png", we might pick the lossy one.
381  if (exts.find(".png") != exts.end()) {
382  name += ".png";
383  } else {
384  if (!exts.empty()) {
385  name += *exts.begin();
386  }
387  }
388  }
389  }
390  utility::LogInfo("[o3d] name: {}.", name);
391  this->impl_->on_done_((dir + "/" + name).c_str());
392  } else {
393  utility::LogError("FileDialog: need to call SetOnDone()");
394  }
395 }
396 
397 Size FileDialog::CalcPreferredSize(const LayoutContext &context,
398  const Constraints &constraints) const {
399  auto em = context.theme.font_size;
400  auto width = std::max(25 * em,
401  Super::CalcPreferredSize(context, constraints).width);
402  return Size(width, 30 * em);
403 }
404 
405 } // namespace gui
406 } // namespace visualization
407 } // namespace cloudViewer
408 
409 #endif // __APPLE__
int width
std::string name
char type
Base class for dialogs.
Definition: Dialog.h:19
const std::string & GetDisplayText() const
Definition: FileDialog.cpp:67
bool operator==(const DirEntry &rhs) const
Definition: FileDialog.cpp:69
bool operator<(const DirEntry &rhs) const
Definition: FileDialog.cpp:76
const std::string & GetName() const
Definition: FileDialog.cpp:66
DirEntry(const std::string &name, Type type)
Definition: FileDialog.cpp:55
bool operator!=(const DirEntry &rhs) const
Definition: FileDialog.cpp:72
FileDialog(Mode type, const char *title, const Theme &theme)
Definition: FileDialog.cpp:212
static std::shared_ptr< Horiz > MakeCentered(std::shared_ptr< Widget > w)
Definition: Layout.cpp:531
virtual void AddChild(std::shared_ptr< Widget > child)
Definition: Widget.cpp:43
#define LogInfo(...)
Definition: Logging.h:81
#define LogError(...)
Definition: Logging.h:60
int max(int a, int b)
Definition: cutil_math.h:48
Helper functions for the ml ops.
ImGuiContext * context
Definition: Window.cpp:76
const Theme * theme
Definition: Window.cpp:74
static const std::string path
Definition: PointCloud.cpp:59
bool ChangeWorkingDirectory(const std::string &directory)
Definition: FileSystem.cpp:427
bool DirectoryExists(const std::string &directory)
Definition: FileSystem.cpp:473
std::string GetFileExtensionInLowerCase(const std::string &filename)
Definition: FileSystem.cpp:281
std::vector< std::string > GetPathComponents(const std::string &path)
Definition: FileSystem.cpp:348
std::string GetFileNameWithoutDirectory(const std::string &filename)
Definition: FileSystem.cpp:301
bool ListDirectory(const std::string &directory, std::vector< std::string > &subdirs, std::vector< std::string > &filenames)
Definition: FileSystem.cpp:532
void SplitString(std::vector< std::string > &tokens, const std::string &str, const std::string &delimiters=" ", bool trim_empty_str=true)
Definition: Helper.cpp:197
MiniVec< float, N > ceil(const MiniVec< float, N > &a)
Definition: MiniVec.h:89
Generic file read and write utility for python interface.
std::function< void(const char *)> on_done_
Definition: FileDialog.cpp:119
std::unordered_map< int, std::unordered_set< std::string > > filter_idx_2_filter
Definition: FileDialog.cpp:114