]> git.lyx.org Git - lyx.git/blob - src/frontends/xforms/FormFiledialog.C
some more changes
[lyx.git] / src / frontends / xforms / FormFiledialog.C
1 /**
2  * \file FormFiledialog.C
3  * Copyright 2001 the LyX Team
4  * Read the file COPYING
5  *
6  * \author unknown
7  * \author John Levon, moz@compsoc.man.ac.uk
8  */
9
10 #include <config.h>
11
12 #include <unistd.h>
13 #include <cstdlib>
14 #include <pwd.h>
15 #include <grp.h>
16 #include <map>
17
18 using std::map;
19 using std::max;
20 using std::sort;
21
22 #include "frontends/Alert.h"
23 #include "support/FileInfo.h"
24 #include "support/lyxlib.h"
25 #include "support/lstrings.h"
26 #include "gettext.h"
27 #include "frontends/Dialogs.h"
28 #include "forms_gettext.h"
29
30 #include <boost/bind.hpp>
31
32 //#ifdef HAVE_ERRNO_H
33 //#include <cerrno>
34 //#endif
35
36 #if HAVE_DIRENT_H
37 # include <dirent.h>
38 #else
39 # define dirent direct
40 # if HAVE_SYS_NDIR_H
41 #  include <sys/ndir.h>
42 # endif
43 # if HAVE_SYS_DIR_H
44 #  include <sys/dir.h>
45 # endif
46 # if HAVE_NDIR_H
47 #  include <ndir.h>
48 # endif
49 #endif
50
51 #ifdef __GNUG__
52 #pragma implementation
53 #endif
54
55 #include "support/filetools.h"
56 #include "FormFiledialog.h"
57 #include "forms/form_filedialog.h"
58 #include FORMS_H_LOCATION
59
60 namespace {
61
62 // six months, in seconds
63 long const SIX_MONTH_SEC = 6L * 30L * 24L * 60L * 60L;
64 //static
65 long const ONE_HOUR_SEC = 60L * 60L;
66
67 extern "C" {
68
69         static
70         int C_LyXFileDlg_CancelCB(FL_FORM *fl, void *xev)
71         {
72                 return FileDialog::Private::CancelCB(fl, xev);
73         }
74
75         static
76         void C_LyXFileDlg_DoubleClickCB(FL_OBJECT * ob, long data)
77         {
78                 FileDialog::Private::DoubleClickCB(ob, data);
79         }
80
81         static
82         void C_LyXFileDlg_FileDlgCB(FL_OBJECT * ob, long data)
83         {
84                 FileDialog::Private::FileDlgCB(ob, data);
85         }
86
87 }
88
89 // *** User cache class implementation
90 /// User cache class definition
91 class UserCache {
92 public:
93         /// seeks user name from group ID
94         string const & find(uid_t ID) const {
95                 Users::const_iterator cit = users.find(ID);
96                 if (cit == users.end()) {
97                         add(ID);
98                         return users[ID];
99                 }
100                 return cit->second;
101         }
102 private:
103         ///
104         void add(uid_t ID) const;
105         ///
106         typedef map<uid_t, string> Users;
107         ///
108         mutable Users users;
109 };
110
111
112 void UserCache::add(uid_t ID) const
113 {
114         struct passwd const * entry = getpwuid(ID);
115         users[ID] = entry ? entry->pw_name : tostr(ID);
116 }
117
118
119 /// Group cache class definition
120 class GroupCache {
121 public:
122         /// seeks group name from group ID
123         string const & find(gid_t ID) const ;
124 private:
125         ///
126         void add(gid_t ID) const;
127         ///
128         typedef map<gid_t, string> Groups;
129         ///
130         mutable Groups groups;
131 };
132
133
134 string const & GroupCache::find(gid_t ID) const
135 {
136         Groups::const_iterator cit = groups.find(ID);
137         if (cit == groups.end()) {
138                 add(ID);
139                 return groups[ID];
140         }
141         return cit->second;
142 }
143
144
145 void GroupCache::add(gid_t ID) const
146 {
147         struct group const * entry = getgrgid(ID);
148         groups[ID] = entry ? entry->gr_name : tostr(ID);
149 }
150
151 // local instances
152 UserCache lyxUserCache;
153 GroupCache lyxGroupCache;
154
155 // compares two LyXDirEntry objects content (used for sort)
156 class comp_direntry {
157 public:
158         bool operator()(DirEntry const & r1, DirEntry const & r2) const
159         {
160                 bool const r1d = suffixIs(r1.name_, '/');
161                 bool const r2d = suffixIs(r2.name_, '/');
162                 if (r1d && !r2d)
163                         return true;
164                 if (!r1d && r2d)
165                         return false;
166                 return r1.name_ < r2.name_;
167         }
168 };
169
170
171 } // namespace anon
172
173
174
175 // *** FileDialog::Private class implementation
176
177 // static members
178 FD_filedialog * FileDialog::Private::file_dlg_form_ = 0;
179 FileDialog::Private * FileDialog::Private::current_dlg_ = 0;
180
181
182 // Reread: updates dialog list to match class directory
183 void FileDialog::Private::Reread()
184 {
185         // Opens directory
186         DIR * dir = ::opendir(directory_.c_str());
187         if (!dir) {
188                 Alert::err_alert(_("Warning! Couldn't open directory."),
189                         directory_);
190                 directory_ = lyx::getcwd();
191                 dir = ::opendir(directory_.c_str());
192         }
193
194         // Clear the present namelist
195         dir_entries_.clear();
196
197         // Updates display
198         fl_hide_object(file_dlg_form_->List);
199         fl_clear_browser(file_dlg_form_->List);
200         fl_set_input(file_dlg_form_->DirBox, directory_.c_str());
201
202         // Splits complete directory name into directories and compute depth
203         depth_ = 0;
204         string line, Temp;
205         string mode;
206         string File = directory_;
207         if (File != "/") {
208                 File = split(File, Temp, '/');
209         }
210         while (!File.empty() || !Temp.empty()) {
211                 string dline = "@b" + line + Temp + '/';
212                 fl_add_browser_line(file_dlg_form_->List, dline.c_str());
213                 File = split(File, Temp, '/');
214                 line += ' ';
215                 ++depth_;
216         }
217
218         // Parses all entries of the given subdirectory
219         time_t curTime = time(0);
220         rewinddir(dir);
221         while (dirent * entry = readdir(dir)) {
222                 bool isLink = false, isDir = false;
223
224                 // If the pattern doesn't start with a dot, skip hidden files
225                 if (!mask_.empty() && mask_[0] != '.' &&
226                     entry->d_name[0] == '.')
227                         continue;
228
229                 // Gets filename
230                 string fname = entry->d_name;
231
232                 // Under all circumstances, "." and ".." are not wanted
233                 if (fname == "." || fname == "..")
234                         continue;
235
236                 // gets file status
237                 File = AddName(directory_, fname);
238
239                 FileInfo fileInfo(File, true);
240
241                 // can this really happen?
242                 if (!fileInfo.isOK())
243                         continue;
244
245                 mode = fileInfo.modeString();
246                 unsigned int const nlink = fileInfo.getNumberOfLinks();
247                 string const user  = lyxUserCache.find(fileInfo.getUid());
248                 string const group = lyxGroupCache.find(fileInfo.getGid());
249
250                 time_t modtime = fileInfo.getModificationTime();
251                 string Time = ctime(&modtime);
252
253                 if (curTime > modtime + SIX_MONTH_SEC
254                     || curTime < modtime + ONE_HOUR_SEC) {
255                         // The file is fairly old or in the future. POSIX says
256                         // the cutoff is 6 months old. Allow a 1 hour slop
257                         // factor for what is considered "the future", to
258                         // allow for NFS server/client clock disagreement.
259                         // Show the year instead of the time of day.
260                         Time.erase(10, 9);
261                         Time.erase(15, string::npos);
262                 } else {
263                         Time.erase(16, string::npos);
264                 }
265
266                 string buffer = mode + ' ' +
267                         tostr(nlink) + ' ' +
268                         user + ' ' +
269                         group + ' ' +
270                         Time.substr(4, string::npos) + ' ';
271
272                 buffer += entry->d_name;
273                 buffer += fileInfo.typeIndicator();
274
275                 isLink = fileInfo.isLink();
276                 if (isLink) {
277                         string Link;
278
279                         if (LyXReadLink(File, Link)) {
280                                 buffer += " -> ";
281                                 buffer += Link;
282
283                                 // This gives the FileType of the file that
284                                 // is really pointed too after resolving all
285                                 // symlinks. This is not necessarily the same
286                                 // as the type of Link (which could again be a
287                                 // link). Is that intended?
288                                 //                              JV 199902
289                                 fileInfo.newFile(File);
290                                 if (fileInfo.isOK())
291                                         buffer += fileInfo.typeIndicator();
292                                 else
293                                         continue;
294                         }
295                 }
296
297                 // filters files according to pattern and type
298                 if (fileInfo.isRegular()
299                     || fileInfo.isChar()
300                     || fileInfo.isBlock()
301                     || fileInfo.isFifo()) {
302                         if (!regexMatch(fname, mask_))
303                                 continue;
304                 } else if (!(isDir = fileInfo.isDir()))
305                         continue;
306
307                 DirEntry tmp;
308
309                 // Note ls_entry_ is an string!
310                 tmp.ls_entry_ = buffer;
311                 // creates used name
312                 string temp = fname;
313                 if (isDir)
314                         temp += '/';
315
316                 tmp.name_ = temp;
317                 // creates displayed name
318                 temp = entry->d_name;
319                 if (isLink)
320                         temp += '@';
321                 else
322                         temp += fileInfo.typeIndicator();
323                 tmp.displayed_ = temp;
324
325                 dir_entries_.push_back(tmp);
326         }
327
328         closedir(dir);
329
330         // Sort the names
331         sort(dir_entries_.begin(), dir_entries_.end(), comp_direntry());
332
333         // Add them to directory box
334         for (DirEntries::const_iterator cit = dir_entries_.begin();
335              cit != dir_entries_.end(); ++cit) {
336                 string const temp = line + cit->displayed_;
337                 fl_add_browser_line(file_dlg_form_->List, temp.c_str());
338         }
339         fl_set_browser_topline(file_dlg_form_->List, depth_);
340         fl_show_object(file_dlg_form_->List);
341         last_sel_ = -1;
342 }
343
344
345 // SetDirectory: sets dialog current directory
346 void FileDialog::Private::SetDirectory(string const & path)
347 {
348         string tmp;
349         if (path.empty())
350                 tmp = lyx::getcwd();
351         else
352                 tmp = MakeAbsPath(ExpandPath(path), directory_);
353
354         // must check the directory exists
355         DIR * dir = ::opendir(tmp.c_str());
356         if (!dir) {
357                 Alert::err_alert(_("Warning! Couldn't open directory."), tmp);
358         } else {
359                 ::closedir(dir);
360                 directory_ = tmp;
361         }
362 }
363
364
365 // SetMask: sets dialog file mask
366 void FileDialog::Private::SetMask(string const & newmask)
367 {
368         mask_ = newmask;
369         fl_set_input(file_dlg_form_->PatBox, mask_.c_str());
370 }
371
372
373 // SetInfoLine: sets dialog information line
374 void FileDialog::Private::SetInfoLine(string const & line)
375 {
376         info_line_ = line;
377         fl_set_object_label(file_dlg_form_->FileInfo, info_line_.c_str());
378 }
379
380
381 FileDialog::Private::Private(Dialogs & dia)
382 {
383         directory_ = MakeAbsPath(string("."));
384         mask_ = '*';
385
386         // Creates form if necessary.
387         if (!file_dlg_form_) {
388                 file_dlg_form_ = build_filedialog(this);
389                 // Set callbacks. This means that we don't need a patch file
390                 fl_set_object_callback(file_dlg_form_->DirBox,
391                                        C_LyXFileDlg_FileDlgCB, 0);
392                 fl_set_object_callback(file_dlg_form_->PatBox,
393                                        C_LyXFileDlg_FileDlgCB, 1);
394                 fl_set_object_callback(file_dlg_form_->List,
395                                        C_LyXFileDlg_FileDlgCB, 2);
396                 fl_set_object_callback(file_dlg_form_->Filename,
397                                        C_LyXFileDlg_FileDlgCB, 3);
398                 fl_set_object_callback(file_dlg_form_->Rescan,
399                                        C_LyXFileDlg_FileDlgCB, 10);
400                 fl_set_object_callback(file_dlg_form_->Home,
401                                        C_LyXFileDlg_FileDlgCB, 11);
402                 fl_set_object_callback(file_dlg_form_->User1,
403                                        C_LyXFileDlg_FileDlgCB, 12);
404                 fl_set_object_callback(file_dlg_form_->User2,
405                                        C_LyXFileDlg_FileDlgCB, 13);
406
407                 // Make sure pressing the close box doesn't crash LyX. (RvdK)
408                 fl_set_form_atclose(file_dlg_form_->form,
409                                     C_LyXFileDlg_CancelCB, 0);
410                 // Register doubleclick callback
411                 fl_set_browser_dblclick_callback(file_dlg_form_->List,
412                                                  C_LyXFileDlg_DoubleClickCB,
413                                                  0);
414         }
415         fl_hide_object(file_dlg_form_->User1);
416         fl_hide_object(file_dlg_form_->User2);
417
418         r_ = dia.redrawGUI.connect(boost::bind(&FileDialog::Private::redraw, this));
419 }
420
421
422 FileDialog::Private::~Private()
423 {
424         r_.disconnect();
425 }
426
427
428 void FileDialog::Private::redraw()
429 {
430         if (file_dlg_form_->form && file_dlg_form_->form->visible)
431                 fl_redraw_form(file_dlg_form_->form);
432 }
433
434
435 // SetButton: sets file selector user button action
436 void FileDialog::Private::SetButton(int index, string const & name,
437                            string const & path)
438 {
439         FL_OBJECT * ob;
440         string * tmp;
441
442         if (index == 0) {
443                 ob = file_dlg_form_->User1;
444                 tmp = &user_path1_;
445         } else if (index == 1) {
446                 ob = file_dlg_form_->User2;
447                 tmp = &user_path2_;
448         } else {
449                 return;
450         }
451
452         if (!name.empty()) {
453                 fl_set_object_label(ob, idex(name.c_str()));
454                 fl_set_button_shortcut(ob, scex(name.c_str()), 1);
455                 fl_show_object(ob);
456                 *tmp = path;
457         } else {
458                 fl_hide_object(ob);
459                 tmp->erase();
460         }
461 }
462
463
464 // GetDirectory: gets last dialog directory
465 string const FileDialog::Private::GetDirectory() const
466 {
467         if (!directory_.empty())
468                 return directory_;
469         else
470                 return string(".");
471 }
472
473 namespace {
474         bool x_sync_kludge(bool ret)
475         {
476                 XSync(fl_get_display(), false);
477                 return ret;
478         }
479 } // namespace anon
480
481 // RunDialog: handle dialog during file selection
482 bool FileDialog::Private::RunDialog()
483 {
484         force_cancel_ = false;
485         force_ok_ = false;
486
487         // event loop
488         while (true) {
489                 FL_OBJECT * ob = fl_do_forms();
490
491                 if (ob == file_dlg_form_->Ready) {
492                         if (HandleOK())
493                                 return x_sync_kludge(true);
494
495                 } else if (ob == file_dlg_form_->Cancel || force_cancel_)
496                         return x_sync_kludge(false);
497
498                 else if (force_ok_)
499                         return x_sync_kludge(true);
500         }
501 }
502
503
504 // XForms objects callback (static)
505 void FileDialog::Private::FileDlgCB(FL_OBJECT *, long arg)
506 {
507         if (!current_dlg_)
508                 return;
509
510         switch (arg) {
511
512         case 0: // get directory
513                 current_dlg_->SetDirectory(fl_get_input(file_dlg_form_->DirBox));
514                 current_dlg_->Reread();
515                 break;
516
517         case 1: // get mask
518                 current_dlg_->SetMask(fl_get_input(file_dlg_form_->PatBox));
519                 current_dlg_->Reread();
520                 break;
521
522         case 2: // list
523                 current_dlg_->HandleListHit();
524                 break;
525
526         case 10: // rescan
527                 current_dlg_->SetDirectory(fl_get_input(file_dlg_form_->DirBox));
528                 current_dlg_->SetMask(fl_get_input(file_dlg_form_->PatBox));
529                 current_dlg_->Reread();
530                 break;
531
532         case 11: // home
533                 current_dlg_->SetDirectory(GetEnvPath("HOME"));
534                 current_dlg_->SetMask(fl_get_input(file_dlg_form_->PatBox));
535                 current_dlg_->Reread();
536                 break;
537
538         case 12: // user button 1
539                 current_dlg_->SetDirectory(current_dlg_->user_path1_);
540                 current_dlg_->SetMask(fl_get_input(file_dlg_form_->PatBox));
541                 current_dlg_->Reread();
542                 break;
543
544         case 13: // user button 2
545                 current_dlg_->SetDirectory(current_dlg_->user_path2_);
546                 current_dlg_->SetMask(fl_get_input(file_dlg_form_->PatBox));
547                 current_dlg_->Reread();
548                 break;
549
550         }
551 }
552
553
554 // Handle callback from list
555 void FileDialog::Private::HandleListHit()
556 {
557         // set info line
558         int const select_ = fl_get_browser(file_dlg_form_->List);
559         if (select_ > depth_)
560                 SetInfoLine(dir_entries_[select_ - depth_ - 1].ls_entry_);
561         else
562                 SetInfoLine(string());
563 }
564
565
566 // Callback for double click in list
567 void FileDialog::Private::DoubleClickCB(FL_OBJECT *, long)
568 {
569         // Simulate click on OK button
570         if (current_dlg_->HandleDoubleClick())
571                 current_dlg_->Force(false);
572 }
573
574
575 // Handle double click from list
576 bool FileDialog::Private::HandleDoubleClick()
577 {
578         string tmp;
579
580         // set info line
581         bool isDir = true;
582         int const select_ = fl_get_browser(file_dlg_form_->List);
583         if (select_ > depth_) {
584                 tmp = dir_entries_[select_ - depth_ - 1].name_;
585                 SetInfoLine(dir_entries_[select_ - depth_ - 1].ls_entry_);
586                 if (!suffixIs(tmp, '/')) {
587                         isDir = false;
588                         fl_set_input(file_dlg_form_->Filename, tmp.c_str());
589                 }
590         } else if (select_ != 0) {
591                 SetInfoLine(string());
592         } else
593                 return true;
594
595         // executes action
596         if (isDir) {
597                 string Temp;
598
599                 // builds new directory name
600                 if (select_ > depth_) {
601                         // Directory deeper down
602                         // First, get directory with trailing /
603                         Temp = fl_get_input(file_dlg_form_->DirBox);
604                         if (!suffixIs(Temp, '/'))
605                                 Temp += '/';
606                         Temp += tmp;
607                 } else {
608                         // Directory higher up
609                         Temp.erase();
610                         for (int i = 0; i < select_; ++i) {
611                                 string piece = fl_get_browser_line(file_dlg_form_->List, i+1);
612                                 // The '+2' is here to count the '@b' (JMarc)
613                                 Temp += piece.substr(i + 2);
614                         }
615                 }
616
617                 // assigns it
618                 SetDirectory(Temp);
619                 Reread();
620                 return false;
621         }
622         return true;
623 }
624
625
626 // Handle OK button call
627 bool FileDialog::Private::HandleOK()
628 {
629         // mask was changed
630         string tmp = fl_get_input(file_dlg_form_->PatBox);
631         if (tmp != mask_) {
632                 SetMask(tmp);
633                 Reread();
634                 return false;
635         }
636
637         // directory was changed
638         tmp = fl_get_input(file_dlg_form_->DirBox);
639         if (tmp != directory_) {
640                 SetDirectory(tmp);
641                 Reread();
642                 return false;
643         }
644
645         // Handle return from list
646         int const select = fl_get_browser(file_dlg_form_->List);
647         if (select > depth_) {
648                 string const temp = dir_entries_[select - depth_ - 1].name_;
649                 if (!suffixIs(temp, '/')) {
650                         // If user didn't type anything, use browser
651                         string const name = fl_get_input(file_dlg_form_->Filename);
652                         if (name.empty())
653                                 fl_set_input(file_dlg_form_->Filename, temp.c_str());
654                         return true;
655                 }
656         }
657
658         // Emulate a doubleclick
659         return HandleDoubleClick();
660 }
661
662
663 // Handle Cancel CB from WM close
664 int FileDialog::Private::CancelCB(FL_FORM *, void *)
665 {
666         // Simulate a click on the cancel button
667         current_dlg_->Force(true);
668         return FL_IGNORE;
669 }
670
671
672 // Simulates a click on OK/Cancel
673 void FileDialog::Private::Force(bool cancel)
674 {
675         if (cancel) {
676                 force_cancel_ = true;
677                 fl_set_button(file_dlg_form_->Cancel, 1);
678         } else {
679                 force_ok_ = true;
680                 fl_set_button(file_dlg_form_->Ready, 1);
681         }
682         // Start timer to break fl_do_forms loop soon
683         fl_set_timer(file_dlg_form_->timer, 0.1);
684 }
685
686
687 // Select: launches dialog and returns selected file
688 string const FileDialog::Private::Select(string const & title,
689                                          string const & path,
690                                          string const & mask,
691                                          string const & suggested)
692 {
693         // handles new mask and path
694         bool isOk = true;
695         if (!mask.empty()) {
696                 SetMask(mask);
697                 isOk = false;
698         }
699         if (!path.empty()) {
700                 SetDirectory(path);
701                 isOk = false;
702         }
703         if (!isOk)
704                 Reread();
705
706         // highlight the suggested file in the browser, if it exists.
707         int sel = 0;
708         string const filename = OnlyFilename(suggested);
709         if (!filename.empty()) {
710                 for (int i = 0; i < fl_get_browser_maxline(file_dlg_form_->List); ++i) {
711                         string s = fl_get_browser_line(file_dlg_form_->List, i + 1);
712                         s = strip(frontStrip(s));
713                         if (s == filename) {
714                                 sel = i + 1;
715                                 break;
716                         }
717                 }
718         }
719
720         if (sel != 0)
721                 fl_select_browser_line(file_dlg_form_->List, sel);
722         int const top = max(sel - 5, 1);
723         fl_set_browser_topline(file_dlg_form_->List, top);
724
725         // checks whether dialog can be started
726         if (current_dlg_)
727                 return string();
728         current_dlg_ = this;
729
730         // runs dialog
731         SetInfoLine(string());
732         fl_set_input(file_dlg_form_->Filename, suggested.c_str());
733         fl_set_button(file_dlg_form_->Cancel, 0);
734         fl_set_button(file_dlg_form_->Ready, 0);
735         fl_set_focus_object(file_dlg_form_->form, file_dlg_form_->Filename);
736         fl_deactivate_all_forms();
737         fl_show_form(file_dlg_form_->form,
738                      FL_PLACE_MOUSE | FL_FREE_SIZE, 0,
739                      title.c_str());
740
741         isOk = RunDialog();
742
743         fl_hide_form(file_dlg_form_->form);
744         fl_activate_all_forms();
745         current_dlg_ = 0;
746
747         // Returns filename or string() if no valid selection was made
748         if (!isOk || !fl_get_input(file_dlg_form_->Filename)[0])
749                 return string();
750
751         file_name_ = fl_get_input(file_dlg_form_->Filename);
752
753         if (!AbsolutePath(file_name_))
754                 file_name_ = AddName(fl_get_input(file_dlg_form_->DirBox), file_name_);
755         return file_name_;
756 }