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