1 //
2 // This file is part of libyacurs.
3 // Copyright (C) 2013  Rafael Ostertag
4 //
5 // This program is free software: you can redistribute it and/or
6 // modify it under the terms of the GNU General Public License as
7 // published by the Free Software Foundation, either version 3 of the
8 // License, or (at your option) any later version.
9 //
10 // This program is distributed in the hope that it will be useful, but
11 // WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 // General Public License for more details.
14 //
15 // You should have received a copy of the GNU General Public License
16 // along with this program.  If not, see
17 // <http://www.gnu.org/licenses/>.
18 //
19 //
20 // $Id: filesavedialog.cc 5702 2013-11-26 18:18:33Z rafi $
21 
22 #ifdef HAVE_CONFIG_H
23 #include "config.h"
24 #endif
25 
26 #include "gettext.h"
27 
28 #include <cassert>
29 #include <cerrno>
30 #include <cstdlib>
31 
32 #include <sys/types.h>
33 #include <unistd.h>
34 
35 #include <sys/stat.h>
36 
37 #ifdef HAVE_FCNTL_H
38 #include <fcntl.h>
39 #endif
40 
41 #include <dirent.h>
42 #include <unistd.h>
43 #include <climits>
44 
45 #include "filedialog.h"
46 #include "yacursconst.h"
47 
48 using namespace YACURS;
49 
50 //
51 // Private
52 //
53 
cleanup_path(const std::string & p) const54 std::string FileDialog::cleanup_path(const std::string& p) const {
55     if (p.empty()) return p;
56 
57     std::string working_copy(p);
58 
59     if (working_copy.find("//") == std::string::npos) return working_copy;
60 
61     std::string::size_type pos = 0;
62 
63     while ((pos = working_copy.find("//")) != std::string::npos)
64         working_copy = working_copy.replace(pos, 2, "/");
65 
66     return working_copy;
67 }
68 
69 //
70 // Protected
71 //
dir_up(const std::string & dir)72 std::string FileDialog::dir_up(const std::string& dir) {
73     std::string::size_type pos = dir.rfind('/');
74 
75     std::string retval;
76 
77     if (pos != 0) {
78         retval = dir.substr(0, pos);
79     } else {
80         // pos==0 is the root directory, which we want
81         // to preserve
82         retval = "/";
83     }
84 
85     return retval;
86 }
87 
read_dir()88 void FileDialog::read_dir() {
89     std::list<std::string> dirs;
90     std::list<std::string> files;
91 
92     if (_path.label().empty()) {
93         try {
94             _path.label(getcwd());
95         } catch (EXCEPTIONS::SystemError& e) {
96             if (e.errorno() == EACCES)
97                 _path.label("/");
98             else
99                 throw;
100         }
101     }
102 
103     DIR* dir = opendir(_path.label().c_str());
104     if (dir == 0) throw EXCEPTIONS::SystemError(errno);
105 
106     if (_do_chdir && (chdir(_path.label().c_str()) != 0)) {
107         int sav_errno = errno;
108         (void)closedir(dir);
109         throw EXCEPTIONS::SystemError(sav_errno);
110     }
111 
112     std::string _base(_path.label());
113     assert(_base.length() > 0);
114 
115     // If _base.length()==1, then it is the root directory and we
116     // don't append a slash
117     if (_base.length() > 1) _base += "/";
118 
119     dirent* dent;
120     while (errno = 0, dent = readdir(dir)) {
121         std::string _tmp(_base + dent->d_name);
122 
123         struct stat _stat;
124         if (stat(_tmp.c_str(), &_stat) == -1) {
125             continue;
126         }
127 
128         if (_stat.st_mode & S_IFDIR) {
129             dirs.push_back(dent->d_name);
130             continue;
131         }
132         if (_stat.st_mode & S_IFREG) {
133             files.push_back(dent->d_name);
134             continue;
135         }
136     }
137 
138     // On Fedora 18, somewhere in _directories->set(dirs), errno
139     // will be updated, so we save it here, before proceeding
140     int errno_save = errno;
141 
142     _directories.set(dirs);
143     // If the file mode on the directory is only read for the user,
144     // e.g. 444, and we don't do a chdir(), we end up with an empty
145     // directory list. So, make sure at least `.' and `..' is in the
146     // list.
147     if (_directories.empty()) {
148         _directories.add(".");
149         _directories.add("..");
150     }
151 
152     _files.set(files);
153 
154     if (errno_save != 0) {
155         (void)closedir(dir);
156         throw EXCEPTIONS::SystemError(errno_save);
157     }
158 
159     if (closedir(dir) != 0) throw EXCEPTIONS::SystemError(errno);
160 }
161 
listbox_enter_handler(Event & e)162 void FileDialog::listbox_enter_handler(Event& e) {
163     assert(e == EVT_LISTBOX_ENTER);
164 
165     EventEx<ListBox<>*>& _evt = dynamic_cast<EventEx<ListBox<>*>&>(e);
166 
167     if (&_directories == _evt.data()) {
168         if (_directories.selected() == ".") return;
169 
170         if (_directories.selected() == "..") {
171             _path.label(dir_up(_path.label()));
172         } else {
173             // If _path.label().length()==1, then it is the root
174             // directory and we don't append a slash
175             if (_path.label().length() > 1)
176                 _path.label(_path.label() + "/" + _directories.selected());
177             else
178                 _path.label(_path.label() + _directories.selected());
179         }
180         try {
181             read_dir();
182         } catch (EXCEPTIONS::SystemError& ex) {
183             std::string _tmp(_("Cannot change to ") + _path.label() + ":");
184             _errmsgbox =
185                 new MessageBox2(_("System Error"), _tmp, ex.what(), OK_ONLY);
186             _errmsgbox->show();
187 
188             // Most likely the directory is not accessible, so go
189             // one up. If we happen to reach the root directory,
190             // and that's not accessible too, the system probably
191             // is messed up...
192             _path.label(dir_up(_path.label()));
193         }
194         return;
195     }
196 
197     if (&_files == _evt.data()) {
198         _filename.input(_files.selected());
199     }
200 }
201 
filename_readonly(bool ro)202 void FileDialog::filename_readonly(bool ro) { _filename.readonly(ro); }
203 
getcwd() const204 std::string FileDialog::getcwd() const {
205     long path_max;
206     if (_path.label().empty()) {
207         path_max = pathconf("/", _PC_PATH_MAX);
208     } else {
209         path_max = pathconf(_path.label().c_str(), _PC_PATH_MAX);
210     }
211 
212     if (path_max < 0) {
213         // probably something went wrong in call to pathconf(). Use
214         // the fallback size.
215         path_max = DEFCWDBUFSZ;
216     }
217 
218     char* cwd = new char[path_max];
219 
220     char* ptr = ::getcwd(cwd, path_max);
221     if (ptr == 0) {
222         delete[] cwd;
223         throw EXCEPTIONS::SystemError(errno);
224     }
225 
226     std::string retval(ptr);
227     delete[] cwd;
228 
229     return retval;
230 }
231 
window_close_handler(Event & e)232 void FileDialog::window_close_handler(Event& e) {
233     assert(e == EVT_WINDOW_CLOSE);
234 
235     EventEx<WindowBase*>& _evt = dynamic_cast<EventEx<WindowBase*>&>(e);
236 
237     if (_filetypemismatch == _evt.data()) {
238         delete _filetypemismatch;
239         _filetypemismatch = 0;
240         return;
241     }
242 
243     if (_errmsgbox == _evt.data()) {
244         delete _errmsgbox;
245         _errmsgbox = 0;
246         return;
247     }
248 }
249 
button_press_handler(Event & e)250 void FileDialog::button_press_handler(Event& e) {
251     // We hook in the button_press_handler of Dialog, so we can test
252     // if the file selected matches the required criteria
253 
254     if (realization() != REALIZED) return;
255 
256     // If any file type is ok, call Dialog's button press handler
257     // immediately and return, since there is no work for us.
258     if (_sel_type == ANY) {
259         Dialog::button_press_handler(e);
260         return;
261     }
262 
263     assert(e == EVT_BUTTON_PRESS);
264     EventEx<Button*>& evt = dynamic_cast<EventEx<Button*>&>(e);
265 
266     // make sure the event is intended for us
267     if (evt.data() != ok_button() && evt.data() != yes_button() &&
268         evt.data() != cancel_button() && evt.data() != no_button())
269         return;
270 
271     // only intercept ok or yes button, i.e. ignore cancel or no
272     // button.
273     if (evt.data() != ok_button() && evt.data() != yes_button()) {
274         Dialog::button_press_handler(e);
275         return;
276     }
277 
278     if (!selection_type_match()) {
279         // the selection did not match the requested selection
280         // type. The method has raised a dialog to indicate the
281         // missmatch to the user. So we have to return and do nothing.
282         return;
283     }
284 
285     Dialog::button_press_handler(e);
286 }
287 
selection_type_match()288 bool FileDialog::selection_type_match() {
289     struct stat statbuf;
290     if (stat(filepath().c_str(), &statbuf) != 0) {
291         // there is an error, it might be, that the selected file does
292         // not exist. In that case, we can't make any assertion about
293         // the file type and assume that it matches the selection
294         // criteria.
295         if (errno == ENOENT) {
296             // ok, no such file
297             return true;
298         }
299 
300         // some other error
301         //
302         // get error message by using the SystemError exception
303         // which does the job for us...
304         EXCEPTIONS::SystemError _err(errno);
305         // ... and display it
306         assert(_errmsgbox == 0);
307         _errmsgbox = new MessageBox2(_("System Error"), filepath(), _err.what(),
308                                      OK_ONLY);
309         _errmsgbox->show();
310         return false;
311     }
312 
313     // no error, let's see if the selection matches the expectation
314     switch (_sel_type) {
315         case FILE:
316             if (S_ISREG(statbuf.st_mode) == 0) {
317                 assert(_filetypemismatch == 0);
318                 _filetypemismatch = new MessageBox2(
319                     _("Error"), filepath(), _("is not a file"), OK_ONLY);
320                 _filetypemismatch->show();
321                 return false;
322             }
323             break;
324         case DIRECTORY:
325             if (S_ISDIR(statbuf.st_mode) == 0) {
326                 assert(_filetypemismatch == 0);
327                 _filetypemismatch = new MessageBox2(
328                     _("Error"), filepath(), _("is not a directory"), OK_ONLY);
329                 _filetypemismatch->show();
330                 return false;
331             }
332             break;
333         default:
334             throw std::invalid_argument(
335                 _("Selection type unknown for FileDialog"));
336             break;
337     }
338 
339     return true;
340 }
341 
342 //
343 // Public
344 //
345 
FileDialog(const std::string & title,std::string path,bool do_chdir,DIALOG_TYPE dt)346 FileDialog::FileDialog(const std::string& title, std::string path,
347                        bool do_chdir, DIALOG_TYPE dt)
348     : Dialog(title, dt, FULLSIZE),
349       _errmsgbox(0),
350       _filetypemismatch(0),
351       _path(path),
352       _directories(),
353       _files(),
354       _filename(),
355       _hpack(),
356       _vpack(),
357       _do_chdir(do_chdir),
358       _sel_type(ANY),
359       _suffix() {
360     _path.color(DIALOG);
361     // Make sure the path specified by the user does not have a
362     // trailing slash
363     if (!_path.label().empty()) {
364         std::string tmp = cleanup_path(_path.label());
365         // is it the root directory, if so, don't touch it, if not,
366         // see if there is a trailing slash
367         if (tmp.length() == 1 && tmp[0] == '/') {
368             _path.label(tmp);
369         } else {  // ok, see if we have to remove a trailing slash
370             if (tmp[tmp.length() - 1] == '/')
371                 _path.label(tmp.substr(0, tmp.length() - 2));
372             else
373                 _path.label(tmp);
374         }
375     }
376 
377     _directories.sort_order(ASCENDING);
378 
379     _files.sort_order(ASCENDING);
380 
381     _hpack.add_back(&_directories);
382     _hpack.add_back(&_files);
383 
384     _hpack.always_dynamic(true);
385     _hpack.hinting(false);
386 
387     _vpack.add_back(&_path);
388     _vpack.add_back(&_hpack);
389 
390     _vpack.add_back(&_filename);
391 
392     _vpack.always_dynamic(true);
393     _vpack.hinting(false);
394 
395     widget(&_vpack);
396 
397     EventQueue::connect_event(EventConnectorMethod1<FileDialog>(
398         EVT_LISTBOX_ENTER, this, &FileDialog::listbox_enter_handler));
399     EventQueue::connect_event(EventConnectorMethod1<FileDialog>(
400         EVT_WINDOW_CLOSE, this, &FileDialog::window_close_handler));
401 }
402 
~FileDialog()403 FileDialog::~FileDialog() {
404     EventQueue::disconnect_event(EventConnectorMethod1<FileDialog>(
405         EVT_LISTBOX_ENTER, this, &FileDialog::listbox_enter_handler));
406     EventQueue::disconnect_event(EventConnectorMethod1<FileDialog>(
407         EVT_WINDOW_CLOSE, this, &FileDialog::window_close_handler));
408 
409     if (_errmsgbox) delete (_errmsgbox);
410     if (_filetypemismatch) delete (_filetypemismatch);
411 }
412 
filepath() const413 std::string FileDialog::filepath() const {
414     std::string retval;
415 
416     if (_path.label() == "/")
417         retval = _path.label() + filename();
418     else
419         retval = _path.label() + "/" + filename();
420 
421     return retval;
422 }
423 
directory() const424 const std::string& FileDialog::directory() const { return _path.label(); }
425 
filename() const426 std::string FileDialog::filename() const {
427     if (_sel_type == FILE && !_suffix.empty()) {
428         if (_filename.input().length() < _suffix.length())
429             return _filename.input() + _suffix;
430 
431         if (_filename.input().substr(_filename.input().length() -
432                                      _suffix.length()) != _suffix)
433             return _filename.input() + _suffix;
434 
435         return _filename.input();
436     } else {
437         return _filename.input();
438     }
439 }
440 
do_chdir(bool v)441 void FileDialog::do_chdir(bool v) { _do_chdir = v; }
442 
do_chdir() const443 bool FileDialog::do_chdir() const { return _do_chdir; }
444 
selection_type(FILEDIALOG_SELECTION_TYPE t)445 void FileDialog::selection_type(FILEDIALOG_SELECTION_TYPE t) { _sel_type = t; }
446 
447 FILEDIALOG_SELECTION_TYPE
selection_type() const448 FileDialog::selection_type() const { return _sel_type; }
449 
suffix(const std::string & s)450 void FileDialog::suffix(const std::string& s) { _suffix = s; }
451 
refresh(bool immediate)452 void FileDialog::refresh(bool immediate) {
453     Dialog::refresh(immediate);
454 
455     try {
456         read_dir();
457     } catch (EXCEPTIONS::SystemError& ex) {
458         std::string _tmp(_("Cannot change to ") + _path.label() + ":");
459         _errmsgbox =
460             new MessageBox2(_("System Error"), _tmp, ex.what(), OK_ONLY);
461         _errmsgbox->show();
462 
463         // Most likely the directory is not accessible, so go one
464         // up. If we happen to reach the root directory, and
465         // that's not accessible too, the system probably is
466         // messed up...
467         _path.label(dir_up(_path.label()));
468     }
469 }
470