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