1 // Licensed GNU LGPL v3 or later: http://www.gnu.org/licenses/lgpl.html
2
3 #include "smwindow.hh"
4 #include "smlineedit.hh"
5 #include "smfixedgrid.hh"
6 #include "smlabel.hh"
7 #include "smbutton.hh"
8 #include "smcombobox.hh"
9 #include "smlistbox.hh"
10 #include "smcheckbox.hh"
11 #include "smmessagebox.hh"
12 #include <glib/gstdio.h>
13 #include <map>
14
15 using namespace SpectMorph;
16
17 using std::vector;
18 using std::string;
19 using std::map;
20
21 namespace
22 {
23
24 static bool
ends_with(const std::string & str,const std::string & suffix)25 ends_with (const std::string& str, const std::string& suffix)
26 {
27 /* if suffix is .wav, match foo.wav, foo.WAV, foo.Wav, ... */
28 return str.size() >= suffix.size() &&
29 std::equal (str.end() - suffix.size(), str.end(), suffix.begin(),
30 [] (char c1, char c2) -> bool { return tolower (c1) == tolower (c2);});
31 }
32
33 class LinuxFileDialog : public NativeFileDialog
34 {
35 std::unique_ptr<Window> w;
36 public:
37 LinuxFileDialog (Window *window, bool open, const std::string& title, const FileDialogFormats& formats);
38
39 void process_events();
40 };
41
42 class FileDialogWindow : public Window
43 {
44 LineEdit *dir_edit;
45 LineEdit *file_edit;
46 ListBox *list_box;
47 Button *ok_button;
48 Button *cancel_button;
49 ComboBox *filter_combobox;
50 CheckBox *hidden_checkbox;
51
52 struct Item
53 {
54 string filename;
55 bool is_dir;
56 };
57 vector<Item> items;
58 string current_directory;
59 bool is_open_dialog = false;
60 LinuxFileDialog *lfd = nullptr;
61 FileDialogFormats::Format active_filter;
62 string default_ext;
63 map<string, FileDialogFormats::Format> filter_map;
64
65 static string last_start_directory;
66 public:
FileDialogWindow(Window * parent_window,bool open,const string & title,const FileDialogFormats & formats,LinuxFileDialog * lfd)67 FileDialogWindow (Window *parent_window, bool open, const string& title, const FileDialogFormats& formats, LinuxFileDialog *lfd) :
68 Window (*parent_window->event_loop(), title, 480, 320, 0, false, parent_window->native_window()),
69 is_open_dialog (open),
70 lfd (lfd)
71 {
72 set_close_callback ([lfd]() { lfd->signal_file_selected (""); });
73
74 FixedGrid grid;
75
76 double yoffset = 1;
77 dir_edit = new LineEdit (this, "");
78 dir_edit->set_click_to_focus (true);
79 grid.add_widget (dir_edit, 8, yoffset, 51, 3);
80
81 connect (dir_edit->signal_return_pressed, [this]() {
82 read_directory (dir_edit->text());
83 });
84
85 auto dir_label = new Label (this, "Directory");
86 grid.add_widget (dir_label, 1, yoffset, 8, 3);
87 yoffset += 3;
88
89 list_box = new ListBox (this);
90 grid.add_widget (list_box, 8, yoffset, 51, 26);
91 yoffset += 26;
92
93 connect (list_box->signal_item_clicked, [this]() {
94 int i = list_box->selected_item();
95 if (i >= 0 && i < int (items.size()))
96 if (!items[i].is_dir)
97 file_edit->set_text (items[i].filename);
98 });
99 connect (list_box->signal_item_double_clicked, [this]() {
100 int i = list_box->selected_item();
101 if (i >= 0 && i < int (items.size()))
102 {
103 if (items[i].is_dir)
104 read_directory (current_directory + "/" + items[i].filename);
105 else
106 handle_ok (items[i].filename);
107 }
108 });
109
110 file_edit = new LineEdit (this, "");
111 file_edit->set_click_to_focus (true);
112 grid.add_widget (file_edit, 8, yoffset, 51, 3);
113
114 connect (file_edit->signal_return_pressed, this, &FileDialogWindow::on_ok_clicked);
115 auto file_name = new Label (this, "Filename");
116 grid.add_widget (file_name, 1, yoffset, 8, 3);
117 yoffset += 3;
118
119 grid.add_widget (new Label (this, "Filter"), 1, yoffset, 8, 3);
120 filter_combobox = new ComboBox (this);
121 grid.add_widget (filter_combobox, 8, yoffset, 51, 3);
122 yoffset += 3;
123
124 for (size_t i = 0; i < formats.formats.size(); i++)
125 {
126 const auto& format = formats.formats[i];
127
128 filter_combobox->add_item (format.title);
129 filter_map[format.title] = format;
130 if (i == 0)
131 {
132 filter_combobox->set_text (format.title);
133 active_filter = format;
134
135 // NOTE: if the current filter has an extension, this extension will
136 // be used, rather than the default extension, so in most cases the
137 // actual value given here is ignored
138 if (format.exts.size())
139 default_ext = format.exts[0];
140 }
141 }
142
143 connect (filter_combobox->signal_item_changed, this, &FileDialogWindow::on_filter_changed);
144
145 ok_button = new Button (this, open ? "Open" : "Save");
146 connect (ok_button->signal_clicked, this, &FileDialogWindow::on_ok_clicked);
147
148 cancel_button = new Button (this, "Cancel");
149 connect (cancel_button->signal_clicked, [lfd]() { lfd->signal_file_selected (""); });
150
151 hidden_checkbox = new CheckBox (this, "Show Hidden");
152 connect (hidden_checkbox->signal_toggled, [this](bool) { read_directory (current_directory); });
153
154 grid.add_widget (ok_button, 37, yoffset, 10, 3);
155 grid.add_widget (cancel_button, 48, yoffset, 10, 3);
156 grid.add_widget (hidden_checkbox, 3, yoffset + 0.5, 16, 2);
157
158 /* put buttons left */
159 auto up_button = new Button (this, "Up");
160 auto home_button = new Button (this, "Home");
161 auto root_button = new Button (this, "Root");
162
163 connect (up_button->signal_pressed, [this]() {
164 char *dir_name = g_path_get_dirname (current_directory.c_str());
165 read_directory (dir_name);
166 g_free (dir_name);
167 });
168 connect (home_button->signal_pressed, [this]() { read_directory (g_get_home_dir()); });
169 connect (root_button->signal_pressed, [this]() { read_directory ("/"); });
170
171 yoffset = 4;
172 grid.add_widget (up_button, 1, yoffset, 6, 3);
173 yoffset += 3;
174 grid.add_widget (home_button, 1, yoffset, 6, 3);
175 yoffset += 3;
176 grid.add_widget (root_button, 1, yoffset, 6, 3);
177 yoffset += 3;
178
179 if (last_start_directory != "" && can_read_dir (last_start_directory))
180 read_directory (last_start_directory);
181 else
182 read_directory (g_get_home_dir());
183 }
184 bool
can_read_dir(const string & dirname)185 can_read_dir (const string& dirname)
186 {
187 /* simple check if directory was deleted */
188 vector<string> files;
189 Error error = read_dir (dirname, files);
190 return !error;
191 }
192 void
on_ok_clicked()193 on_ok_clicked()
194 {
195 if (file_edit->text() != "")
196 {
197 /* open file */
198 handle_ok (file_edit->text());
199 }
200 else
201 {
202 /* open selected dir (if any) */
203 int i = list_box->selected_item();
204 if (i >= 0 && i < int (items.size()))
205 {
206 if (items[i].is_dir)
207 read_directory (current_directory + "/" + items[i].filename);
208 }
209 }
210 }
211 void
handle_ok(const string & filename)212 handle_ok (const string& filename)
213 {
214 string path = current_directory + "/" + filename;
215
216 /* open dialog is easy */
217 if (is_open_dialog)
218 {
219 last_start_directory = current_directory;
220 lfd->signal_file_selected (path);
221 return;
222 }
223
224 /* save dialog */
225 if (!g_file_test (path.c_str(), G_FILE_TEST_EXISTS))
226 {
227 /* append extension if necessary */
228 string need_ext = default_ext;
229 if (active_filter.exts.size() == 1 && active_filter.exts[0] != "*")
230 need_ext = active_filter.exts[0];
231
232 if (need_ext != "" && !ends_with (path, "." + need_ext))
233 path += "." + need_ext;
234 }
235
236 if (g_file_test (path.c_str(), G_FILE_TEST_EXISTS))
237 {
238 /* confirm overwrite */
239 char *fn = g_path_get_basename (path.c_str());
240 string message = string ("File '") + fn + "' already exists.\n\nDo you wish to overwrite it?";
241 g_free (fn);
242
243 auto confirm_box = new MessageBox (window(), "Overwrite File?", message, MessageBox::SAVE | MessageBox::CANCEL);
244 confirm_box->run ([this, path](bool save_changes)
245 {
246 if (save_changes)
247 {
248 last_start_directory = current_directory;
249 lfd->signal_file_selected (path);
250 }
251 });
252 }
253 else
254 {
255 last_start_directory = current_directory;
256 lfd->signal_file_selected (path);
257 }
258 }
259 void
on_filter_changed()260 on_filter_changed()
261 {
262 active_filter = filter_map[filter_combobox->text()];
263 read_directory (current_directory);
264 }
265 string
canonicalize(const string & path)266 canonicalize (const string& path)
267 {
268 string result = path;
269
270 char *real_path = realpath (path.c_str(), nullptr);
271 if (real_path)
272 result = real_path;
273 free (real_path);
274
275 return result;
276 }
277 void
read_directory(const string & new_dir)278 read_directory (const string& new_dir)
279 {
280 string dir = canonicalize (new_dir);
281 vector<string> files;
282 Error error = read_dir (dir, files);
283 if (error)
284 {
285 MessageBox::critical (this, "Error", error.message());
286 /* preserve state on error */
287 dir_edit->set_text (current_directory);
288 return;
289 }
290 current_directory = dir;
291 dir_edit->set_text (current_directory);
292 list_box->clear();
293 items.clear();
294
295 for (auto file : files)
296 {
297 if (hidden_checkbox->checked() || (file.size() && file[0] != '.'))
298 {
299 string abs_path = dir + "/" + file;
300 GStatBuf stbuf;
301 if (g_stat (abs_path.c_str(), &stbuf) == 0)
302 {
303 Item item;
304 item.filename = file;
305 item.is_dir = S_ISDIR (stbuf.st_mode);
306
307 bool filter_ok = item.is_dir;
308 for (auto ext : active_filter.exts)
309 {
310 if (ext == "*" || ends_with (item.filename, "." + ext))
311 filter_ok = true;
312 }
313
314 if (filter_ok)
315 items.push_back (item);
316 }
317 }
318 }
319 if (dir != "/")
320 {
321 Item parent_item;
322 parent_item.filename = "..";
323 parent_item.is_dir = true;
324 items.push_back (parent_item);
325 }
326
327 std::sort (items.begin(), items.end(), [](Item& i1, Item& i2) {
328 int d1 = i1.is_dir;
329 int d2 = i2.is_dir;
330 if (d1 != d2)
331 return d1 > d2; // directories first
332
333 char *filename1_nocase = g_utf8_casefold (i1.filename.c_str(), -1);
334 char *filename2_nocase = g_utf8_casefold (i2.filename.c_str(), -1);
335 char *key1 = g_utf8_collate_key_for_filename (filename1_nocase, -1);
336 char *key2 = g_utf8_collate_key_for_filename (filename2_nocase, -1);
337 string ks1 = key1, ks2 = key2;
338 g_free (key1);
339 g_free (key2);
340 g_free (filename1_nocase);
341 g_free (filename2_nocase);
342 return ks1 < ks2;
343 });
344 for (auto item : items)
345 {
346 if (item.is_dir)
347 list_box->add_item ("[" + item.filename + "]");
348 else
349 list_box->add_item (item.filename);
350 }
351 }
352 };
353
354 string FileDialogWindow::last_start_directory;
355
356 }
357
LinuxFileDialog(Window * window,bool open,const string & title,const FileDialogFormats & formats)358 LinuxFileDialog::LinuxFileDialog (Window *window, bool open, const string& title, const FileDialogFormats& formats)
359 {
360 w.reset (new FileDialogWindow (window, open, title, formats, this));
361 w->show();
362 }
363
364 void
process_events()365 LinuxFileDialog::process_events()
366 {
367 }
368
369 NativeFileDialog *
create(Window * window,bool open,const string & title,const FileDialogFormats & formats)370 NativeFileDialog::create (Window *window, bool open, const string& title, const FileDialogFormats& formats)
371 {
372 return new LinuxFileDialog (window, open, title, formats);
373 }
374