]> git.lyx.org Git - lyx.git/blob - src/frontends/xforms/FormFiledialog.C
(Lars) Remove symbolic links on "make distclean".
[lyx.git] / src / frontends / xforms / FormFiledialog.C
1 /**
2  * \file FormFiledialog.C
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author unknown
7  * \author John Levon
8  *
9  * Full author contact details are available in file CREDITS.
10  */
11
12 #include <config.h>
13
14 #include "FormFiledialog.h"
15 #include "forms/form_filedialog.h"
16
17 #include "forms_gettext.h"
18 #include "globbing.h"
19 #include "xforms_helpers.h"
20
21 #include "frontends/Dialogs.h"
22
23 #include "support/filefilterlist.h"
24 #include "support/filetools.h"
25 #include "support/lstrings.h"
26 #include "support/lyxlib.h"
27 #include "support/package.h"
28
29 #include "lyx_forms.h"
30
31 #include <boost/bind.hpp>
32 #include <boost/filesystem/operations.hpp>
33 #include <boost/tokenizer.hpp>
34
35 #include <algorithm>
36 #include <sstream>
37
38 using lyx::support::AbsolutePath;
39 using lyx::support::AddName;
40 using lyx::support::ExpandPath;
41 using lyx::support::FileFilterList;
42 using lyx::support::getcwd;
43 using lyx::support::MakeAbsPath;
44 using lyx::support::OnlyFilename;
45 using lyx::support::package;
46 using lyx::support::split;
47 using lyx::support::suffixIs;
48 using lyx::support::trim;
49
50 using std::max;
51 using std::sort;
52 using std::ostringstream;
53 using std::string;
54 using std::vector;
55
56 using namespace lyx::frontend;
57 namespace fs = boost::filesystem;
58
59 namespace {
60
61 /** Given a string "<glob> <glob> <glob>", expand each glob in turn.
62  *  Any glob that cannot be expanded is ignored silently.
63  *  Invokes \c convert_brace_glob and \c glob internally, so use only
64  *  on systems supporting the Posix function 'glob'.
65  *  \param mask the string "<glob> <glob> <glob>".
66  *  \param directory the current working directory from
67  *  which \c glob is invoked.
68  *  \returns a vector of all matching file names.
69  */
70 vector<string> const expand_globs(string const & mask,
71                                   string const & directory)
72 {
73         // Split into individual globs and then call 'glob' on each one.
74         typedef boost::tokenizer<boost::char_separator<char> > Tokenizer;
75         boost::char_separator<char> const separator(" ");
76
77         vector<string> matches;
78         Tokenizer const tokens(mask, separator);
79         Tokenizer::const_iterator it = tokens.begin();
80         Tokenizer::const_iterator const end = tokens.end();
81         for (; it != end; ++it)
82                 lyx::support::glob(matches, *it, directory);
83
84         return matches;
85 }
86
87
88 extern "C" {
89
90         static
91         int C_LyXFileDlg_CancelCB(FL_FORM *fl, void *xev)
92         {
93                 return FileDialog::Private::CancelCB(fl, xev);
94         }
95
96         static
97         void C_LyXFileDlg_DoubleClickCB(FL_OBJECT * ob, long data)
98         {
99                 FileDialog::Private::DoubleClickCB(ob, data);
100         }
101
102         static
103         void C_LyXFileDlg_FileDlgCB(FL_OBJECT * ob, long data)
104         {
105                 FileDialog::Private::FileDlgCB(ob, data);
106         }
107
108 }
109
110
111 // compares two LyXDirEntry objects content (used for sort)
112 class comp_direntry : public std::binary_function<DirEntry, DirEntry, bool> {
113 public:
114         bool operator()(DirEntry const & r1, DirEntry const & r2) const
115         {
116                 bool const r1d = suffixIs(r1.name_, '/');
117                 bool const r2d = suffixIs(r2.name_, '/');
118                 if (r1d && !r2d)
119                         return true;
120                 if (!r1d && r2d)
121                         return false;
122                 return r1.name_ < r2.name_;
123         }
124 };
125
126 } // namespace anon
127
128
129
130 // *** FileDialog::Private class implementation
131
132 // static members
133 FD_filedialog * FileDialog::Private::file_dlg_form_ = 0;
134 FileDialog::Private * FileDialog::Private::current_dlg_ = 0;
135 int FileDialog::Private::minw_ = 0;
136 int FileDialog::Private::minh_ = 0;
137
138
139 // Reread: updates dialog list to match class directory
140 void FileDialog::Private::Reread()
141 {
142         // Opens directory
143         if (!fs::exists(directory_) || !fs::is_directory(directory_)) {
144 // FIXME: re-add ...
145 #if 0
146                 Alert::err_alert(_("Warning! Couldn't open directory."),
147                         directory_);
148 #endif
149                 directory_ = getcwd();
150         }
151
152         // Clear the present namelist
153         dir_entries_.clear();
154
155         // Updates display
156         fl_hide_object(file_dlg_form_->List);
157         fl_clear_browser(file_dlg_form_->List);
158         fl_set_input(file_dlg_form_->DirBox, directory_.c_str());
159
160         // Splits complete directory name into directories and compute depth
161         depth_ = 0;
162         string line;
163         string Temp;
164         string File = directory_;
165         if (File != "/")
166                 File = split(File, Temp, '/');
167
168         while (!File.empty() || !Temp.empty()) {
169                 string const dline = "@b" + line + Temp + '/';
170                 fl_add_browser_line(file_dlg_form_->List, dline.c_str());
171                 File = split(File, Temp, '/');
172                 line += ' ';
173                 ++depth_;
174         }
175
176         vector<string> const glob_matches = expand_globs(mask_, directory_);
177
178         fs::directory_iterator beg(directory_);
179         fs::directory_iterator end;
180         for (; beg != end; ++beg) {
181                 string const fname = beg->leaf();
182
183                 // If the pattern doesn't start with a dot, skip hidden files
184                 if (!mask_.empty() && mask_[0] != '.' && fname[0] == '.')
185                         continue;
186
187                 bool const isDir = fs::exists(*beg) && fs::is_directory(*beg);
188
189                 // filters files according to pattern and type
190                         typedef vector<string>::const_iterator viterator;
191                         viterator gbegin = glob_matches.begin();
192                         viterator const gend = glob_matches.end();
193
194                 if (!isDir && std::find(gbegin, gend, fname) == gend)
195                         continue;
196
197                 DirEntry tmp;
198                 // creates used name
199                 tmp.name_ = fname;
200
201                 if (isDir)
202                         tmp.name_ += '/';
203
204                 // creates displayed name
205                 tmp.displayed_ = fname;
206                 dir_entries_.push_back(tmp);
207         }
208
209         // Sort the names
210         sort(dir_entries_.begin(), dir_entries_.end(), comp_direntry());
211
212         // Add them to directory box
213         for (DirEntries::const_iterator cit = dir_entries_.begin();
214              cit != dir_entries_.end(); ++cit) {
215                 string const temp = line + cit->displayed_;
216                 fl_add_browser_line(file_dlg_form_->List, temp.c_str());
217         }
218         fl_set_browser_topline(file_dlg_form_->List, depth_);
219         fl_show_object(file_dlg_form_->List);
220         last_sel_ = -1;
221 }
222
223
224 // SetDirectory: sets dialog current directory
225 void FileDialog::Private::SetDirectory(string const & path)
226 {
227         string tmp;
228         if (path.empty())
229                 tmp = getcwd();
230         else
231                 tmp = MakeAbsPath(ExpandPath(path), directory_);
232
233         // must check the directory exists
234         if (!fs::exists(tmp) || !fs::is_directory(tmp)) {
235 // FIXME: re-add ...
236 #if 0
237                 Alert::err_alert(_("Warning! Couldn't open directory."), tmp);
238 #endif
239         } else {
240                 directory_ = tmp;
241         }
242 }
243
244
245 void FileDialog::Private::SetFilters(string const & mask)
246 {
247         SetFilters(FileFilterList(mask));
248 }
249
250
251 void FileDialog::Private::SetFilters(FileFilterList const & filters)
252 {
253         if (filters.empty())
254                 return;
255
256         // Just take the first one for now.
257         typedef FileFilterList::Filter::glob_iterator glob_iterator;
258         glob_iterator const begin = filters[0].begin();
259         glob_iterator const end = filters[0].end();
260         if (begin == end)
261                 return;
262
263         ostringstream ss;
264         for (glob_iterator it = begin; it != end; ++it) {
265                 if (it != begin)
266                         ss << ' ';
267                 ss << *it;
268         }
269
270         mask_ = ss.str();
271         fl_set_input(file_dlg_form_->PatBox, mask_.c_str());
272 }
273
274
275 FileDialog::Private::Private()
276 {
277         directory_ = MakeAbsPath(string("."));
278
279         // Creates form if necessary.
280         if (!file_dlg_form_) {
281                 file_dlg_form_ = build_filedialog(this);
282                 minw_ = file_dlg_form_->form->w;
283                 minh_ = file_dlg_form_->form->h;
284                 // Set callbacks. This means that we don't need a patch file
285                 fl_set_object_callback(file_dlg_form_->DirBox,
286                                        C_LyXFileDlg_FileDlgCB, 0);
287                 fl_set_object_callback(file_dlg_form_->PatBox,
288                                        C_LyXFileDlg_FileDlgCB, 1);
289                 fl_set_object_callback(file_dlg_form_->List,
290                                        C_LyXFileDlg_FileDlgCB, 2);
291                 fl_set_object_callback(file_dlg_form_->Filename,
292                                        C_LyXFileDlg_FileDlgCB, 3);
293                 fl_set_object_callback(file_dlg_form_->Rescan,
294                                        C_LyXFileDlg_FileDlgCB, 10);
295                 fl_set_object_callback(file_dlg_form_->Home,
296                                        C_LyXFileDlg_FileDlgCB, 11);
297                 fl_set_object_callback(file_dlg_form_->User1,
298                                        C_LyXFileDlg_FileDlgCB, 12);
299                 fl_set_object_callback(file_dlg_form_->User2,
300                                        C_LyXFileDlg_FileDlgCB, 13);
301
302                 // Make sure pressing the close box doesn't crash LyX. (RvdK)
303                 fl_set_form_atclose(file_dlg_form_->form,
304                                     C_LyXFileDlg_CancelCB, 0);
305                 // Register doubleclick callback
306                 fl_set_browser_dblclick_callback(file_dlg_form_->List,
307                                                  C_LyXFileDlg_DoubleClickCB,
308                                                  0);
309         }
310         fl_hide_object(file_dlg_form_->User1);
311         fl_hide_object(file_dlg_form_->User2);
312
313         r_ = Dialogs::redrawGUI().connect(boost::bind(&FileDialog::Private::redraw, this));
314 }
315
316
317 FileDialog::Private::~Private()
318 {
319         r_.disconnect();
320 }
321
322
323 void FileDialog::Private::redraw()
324 {
325         if (file_dlg_form_->form && file_dlg_form_->form->visible)
326                 fl_redraw_form(file_dlg_form_->form);
327 }
328
329
330 // SetButton: sets file selector user button action
331 void FileDialog::Private::SetButton(int index, string const & name,
332                            string const & path)
333 {
334         FL_OBJECT * ob;
335         string * tmp;
336
337         if (index == 0) {
338                 ob = file_dlg_form_->User1;
339                 tmp = &user_path1_;
340         } else if (index == 1) {
341                 ob = file_dlg_form_->User2;
342                 tmp = &user_path2_;
343         } else {
344                 return;
345         }
346
347         if (!name.empty()) {
348                 fl_set_object_label(ob, idex(name).c_str());
349                 fl_set_button_shortcut(ob, scex(name).c_str(), 1);
350                 fl_show_object(ob);
351                 *tmp = path;
352         } else {
353                 fl_hide_object(ob);
354                 tmp->erase();
355         }
356 }
357
358
359 // GetDirectory: gets last dialog directory
360 string const FileDialog::Private::GetDirectory() const
361 {
362         if (!directory_.empty())
363                 return directory_;
364         else
365                 return string(".");
366 }
367
368 namespace {
369         bool x_sync_kludge(bool ret)
370         {
371                 XSync(fl_get_display(), false);
372                 return ret;
373         }
374 } // namespace anon
375
376 // RunDialog: handle dialog during file selection
377 bool FileDialog::Private::RunDialog()
378 {
379         force_cancel_ = false;
380         force_ok_ = false;
381
382         // event loop
383         while (true) {
384                 FL_OBJECT * ob = fl_do_forms();
385
386                 if (ob == file_dlg_form_->Ready) {
387                         if (HandleOK())
388                                 return x_sync_kludge(true);
389
390                 } else if (ob == file_dlg_form_->Cancel || force_cancel_)
391                         return x_sync_kludge(false);
392
393                 else if (force_ok_)
394                         return x_sync_kludge(true);
395         }
396 }
397
398
399 // XForms objects callback (static)
400 void FileDialog::Private::FileDlgCB(FL_OBJECT *, long arg)
401 {
402         if (!current_dlg_)
403                 return;
404
405         switch (arg) {
406
407         case 0: // get directory
408                 current_dlg_->SetDirectory(fl_get_input(file_dlg_form_->DirBox));
409                 current_dlg_->Reread();
410                 break;
411
412         case 1: // get mask
413                 current_dlg_->SetFilters(fl_get_input(file_dlg_form_->PatBox));
414                 current_dlg_->Reread();
415                 break;
416
417         case 2: // list
418                 current_dlg_->HandleListHit();
419                 break;
420
421         case 10: // rescan
422                 current_dlg_->SetDirectory(fl_get_input(file_dlg_form_->DirBox));
423                 current_dlg_->SetFilters(fl_get_input(file_dlg_form_->PatBox));
424                 current_dlg_->Reread();
425                 break;
426
427         case 11: // home
428                 current_dlg_->SetDirectory(package().home_dir());
429                 current_dlg_->SetFilters(fl_get_input(file_dlg_form_->PatBox));
430                 current_dlg_->Reread();
431                 break;
432
433         case 12: // user button 1
434                 current_dlg_->SetDirectory(current_dlg_->user_path1_);
435                 current_dlg_->SetFilters(fl_get_input(file_dlg_form_->PatBox));
436                 current_dlg_->Reread();
437                 break;
438
439         case 13: // user button 2
440                 current_dlg_->SetDirectory(current_dlg_->user_path2_);
441                 current_dlg_->SetFilters(fl_get_input(file_dlg_form_->PatBox));
442                 current_dlg_->Reread();
443                 break;
444
445         }
446 }
447
448
449 // Handle callback from list
450 void FileDialog::Private::HandleListHit()
451 {
452         // set info line
453         int const select_ = fl_get_browser(file_dlg_form_->List);
454        string line = (select_ > depth_ ?
455                             dir_entries_[select_ - depth_ - 1].name_ :
456                             string());
457        if (suffixIs(line, '/'))
458                line.clear();
459        fl_set_input(file_dlg_form_->Filename, line.c_str());
460 }
461
462
463 // Callback for double click in list
464 void FileDialog::Private::DoubleClickCB(FL_OBJECT *, long)
465 {
466         // Simulate click on OK button
467         if (current_dlg_->HandleDoubleClick())
468                 current_dlg_->Force(false);
469 }
470
471
472 // Handle double click from list
473 bool FileDialog::Private::HandleDoubleClick()
474 {
475         string tmp;
476
477         // set info line
478         bool isDir = true;
479         int const select_ = fl_get_browser(file_dlg_form_->List);
480         if (select_ > depth_) {
481                 tmp = dir_entries_[select_ - depth_ - 1].name_;
482                 if (!suffixIs(tmp, '/')) {
483                         isDir = false;
484                         fl_set_input(file_dlg_form_->Filename, tmp.c_str());
485                 }
486         } else if (select_ == 0)
487                 return true;
488
489         // executes action
490         if (isDir) {
491                 string Temp;
492
493                 // builds new directory name
494                 if (select_ > depth_) {
495                         // Directory deeper down
496                         // First, get directory with trailing /
497                         Temp = fl_get_input(file_dlg_form_->DirBox);
498                         if (!suffixIs(Temp, '/'))
499                                 Temp += '/';
500                         Temp += tmp;
501                 } else {
502                         // Directory higher up
503                         Temp.erase();
504                         for (int i = 0; i < select_; ++i) {
505                                 string const piece = fl_get_browser_line(file_dlg_form_->List, i + 1);
506                                 // The '+2' is here to count the '@b' (JMarc)
507                                 Temp += piece.substr(i + 2);
508                         }
509                 }
510
511                 // assigns it
512                 SetDirectory(Temp);
513                 Reread();
514                 return false;
515         }
516         return true;
517 }
518
519
520 // Handle OK button call
521 bool FileDialog::Private::HandleOK()
522 {
523         // mask was changed
524         string tmp = fl_get_input(file_dlg_form_->PatBox);
525         if (tmp != mask_) {
526                 SetFilters(tmp);
527                 Reread();
528                 return false;
529         }
530
531         // directory was changed
532         tmp = fl_get_input(file_dlg_form_->DirBox);
533         if (tmp != directory_) {
534                 SetDirectory(tmp);
535                 Reread();
536                 return false;
537         }
538
539         // Handle return from list
540         int const select = fl_get_browser(file_dlg_form_->List);
541         if (select > depth_) {
542                 string const temp = dir_entries_[select - depth_ - 1].name_;
543                 if (!suffixIs(temp, '/')) {
544                         // If user didn't type anything, use browser
545                         string const name = fl_get_input(file_dlg_form_->Filename);
546                         if (name.empty())
547                                 fl_set_input(file_dlg_form_->Filename, temp.c_str());
548                         return true;
549                 }
550         }
551
552         // Emulate a doubleclick
553         return HandleDoubleClick();
554 }
555
556
557 // Handle Cancel CB from WM close
558 int FileDialog::Private::CancelCB(FL_FORM *, void *)
559 {
560         // Simulate a click on the cancel button
561         current_dlg_->Force(true);
562         return FL_IGNORE;
563 }
564
565
566 // Simulates a click on OK/Cancel
567 void FileDialog::Private::Force(bool cancel)
568 {
569         if (cancel) {
570                 force_cancel_ = true;
571                 fl_set_button(file_dlg_form_->Cancel, 1);
572         } else {
573                 force_ok_ = true;
574                 fl_set_button(file_dlg_form_->Ready, 1);
575         }
576         // Start timer to break fl_do_forms loop soon
577         fl_set_timer(file_dlg_form_->timer, 0.1);
578 }
579
580
581 // Select: launches dialog and returns selected file
582 string const FileDialog::Private::Select(string const & title,
583                                          string const & path,
584                                          FileFilterList const & filters,
585                                          string const & suggested)
586 {
587         // handles new mask and path
588         SetFilters(filters);
589         SetDirectory(path);
590         Reread();
591
592         // highlight the suggested file in the browser, if it exists.
593         int sel = 0;
594         string const filename = OnlyFilename(suggested);
595         if (!filename.empty()) {
596                 for (int i = 0; i < fl_get_browser_maxline(file_dlg_form_->List); ++i) {
597                         string s = fl_get_browser_line(file_dlg_form_->List, i + 1);
598                         s = trim(s);
599                         if (s == filename) {
600                                 sel = i + 1;
601                                 break;
602                         }
603                 }
604         }
605
606         if (sel != 0)
607                 fl_select_browser_line(file_dlg_form_->List, sel);
608         int const top = max(sel - 5, 1);
609         fl_set_browser_topline(file_dlg_form_->List, top);
610
611         // checks whether dialog can be started
612         if (current_dlg_)
613                 return string();
614         current_dlg_ = this;
615
616         // runs dialog
617         setEnabled(file_dlg_form_->Filename, true);
618         fl_set_input(file_dlg_form_->Filename, suggested.c_str());
619         fl_set_button(file_dlg_form_->Cancel, 0);
620         fl_set_button(file_dlg_form_->Ready, 0);
621         fl_set_focus_object(file_dlg_form_->form, file_dlg_form_->Filename);
622         fl_deactivate_all_forms();
623         // Prevent xforms crashing if the dialog gets too small by preventing
624         // it from being shrunk beyond a minimum size.
625         // calls to fl_set_form_minsize/maxsize apply only to the next
626         // fl_show_form(), so this comes first.
627         fl_set_form_minsize(file_dlg_form_->form, minw_, minh_);
628
629         fl_show_form(file_dlg_form_->form,
630                      FL_PLACE_MOUSE | FL_FREE_SIZE, 0,
631                      title.c_str());
632
633         bool const isOk = RunDialog();
634
635         fl_hide_form(file_dlg_form_->form);
636         fl_activate_all_forms();
637         current_dlg_ = 0;
638
639         // Returns filename or string() if no valid selection was made
640         if (!isOk || !fl_get_input(file_dlg_form_->Filename)[0])
641                 return string();
642
643         file_name_ = fl_get_input(file_dlg_form_->Filename);
644
645         if (!AbsolutePath(file_name_))
646                 file_name_ = AddName(fl_get_input(file_dlg_form_->DirBox), file_name_);
647         return file_name_;
648 }
649
650
651 // SelectDir: launches dialog and returns selected directory
652 string const FileDialog::Private::SelectDir(string const & title,
653                                          string const & path,
654                                          string const & suggested)
655 {
656         SetFilters("*/");
657         // handles new path
658         bool isOk = true;
659         if (!path.empty()) {
660                 // handle case where path does not end with "/"
661                 // remerge path+suggested and check if it is a valid path
662                 if (!suggested.empty()) {
663                         string tmp = suggested;
664                         if (!suffixIs(tmp, '/'))
665                                 tmp += '/';
666                         string const full_path = path + tmp;
667                         // check if this is really a directory
668                         if (fs::exists(full_path)
669                             && fs::is_directory(full_path))
670                                 SetDirectory(full_path);
671                         else
672                                 SetDirectory(path);
673                 } else
674                         SetDirectory(path);
675                 isOk = false;
676         }
677         if (!isOk)
678                 Reread();
679
680         // checks whether dialog can be started
681         if (current_dlg_)
682                 return string();
683         current_dlg_ = this;
684
685         // runs dialog
686         fl_set_input(file_dlg_form_->Filename, "");
687         setEnabled(file_dlg_form_->Filename, false);
688         fl_set_button(file_dlg_form_->Cancel, 0);
689         fl_set_button(file_dlg_form_->Ready, 0);
690         fl_set_focus_object(file_dlg_form_->form, file_dlg_form_->DirBox);
691         fl_deactivate_all_forms();
692         fl_show_form(file_dlg_form_->form,
693                      FL_PLACE_MOUSE | FL_FREE_SIZE, 0,
694                      title.c_str());
695
696         isOk = RunDialog();
697
698         fl_hide_form(file_dlg_form_->form);
699         fl_activate_all_forms();
700         current_dlg_ = 0;
701
702         // Returns directory or string() if no valid selection was made
703         if (!isOk)
704                 return string();
705
706         file_name_ = fl_get_input(file_dlg_form_->DirBox);
707         return file_name_;
708 }