/** * \file FormFiledialog.C * This file is part of LyX, the document processor. * Licence details can be found in the file COPYING. * * \author unknown * \author John Levon * * Full author contact details are available in file CREDITS. */ #include #include "FormFiledialog.h" #include "forms/form_filedialog.h" #include "forms_gettext.h" #include "xforms_helpers.h" #include "frontends/Dialogs.h" #include "support/filefilterlist.h" #include "support/filetools.h" #include "support/globbing.h" #include "support/lstrings.h" #include "support/lyxlib.h" #include "support/package.h" #include "lyx_forms.h" #include #include #include #include #include using lyx::support::AbsolutePath; using lyx::support::AddName; using lyx::support::ExpandPath; using lyx::support::FileFilterList; using lyx::support::getcwd; using lyx::support::MakeAbsPath; using lyx::support::OnlyFilename; using lyx::support::package; using lyx::support::split; using lyx::support::suffixIs; using lyx::support::trim; using std::max; using std::sort; using std::ostringstream; using std::string; using std::vector; using namespace lyx::frontend; namespace fs = boost::filesystem; namespace { /** Given a string " ", expand each glob in turn. * Any glob that cannot be expanded is ignored silently. * Invokes \c convert_brace_glob and \c glob internally, so use only * on systems supporting the Posix function 'glob'. * \param mask the string " ". * \param directory the current working directory from * which \c glob is invoked. * \returns a vector of all matching file names. */ vector const expand_globs(string const & mask, string const & directory) { // Split into individual globs and then call 'glob' on each one. typedef boost::tokenizer > Tokenizer; boost::char_separator const separator(" "); vector matches; Tokenizer const tokens(mask, separator); Tokenizer::const_iterator it = tokens.begin(); Tokenizer::const_iterator const end = tokens.end(); for (; it != end; ++it) lyx::support::glob(matches, *it, directory); return matches; } extern "C" { static int C_LyXFileDlg_CancelCB(FL_FORM *fl, void *xev) { return FileDialog::Private::CancelCB(fl, xev); } static void C_LyXFileDlg_DoubleClickCB(FL_OBJECT * ob, long data) { FileDialog::Private::DoubleClickCB(ob, data); } static void C_LyXFileDlg_FileDlgCB(FL_OBJECT * ob, long data) { FileDialog::Private::FileDlgCB(ob, data); } } // compares two LyXDirEntry objects content (used for sort) class comp_direntry : public std::binary_function { public: bool operator()(DirEntry const & r1, DirEntry const & r2) const { bool const r1d = suffixIs(r1.name_, '/'); bool const r2d = suffixIs(r2.name_, '/'); if (r1d && !r2d) return true; if (!r1d && r2d) return false; return r1.name_ < r2.name_; } }; } // namespace anon // *** FileDialog::Private class implementation // static members FD_filedialog * FileDialog::Private::file_dlg_form_ = 0; FileDialog::Private * FileDialog::Private::current_dlg_ = 0; int FileDialog::Private::minw_ = 0; int FileDialog::Private::minh_ = 0; // Reread: updates dialog list to match class directory void FileDialog::Private::Reread() { // Opens directory if (!fs::exists(directory_) || !fs::is_directory(directory_)) { // FIXME: re-add ... #if 0 Alert::err_alert(_("Warning! Couldn't open directory."), directory_); #endif directory_ = getcwd(); } // Clear the present namelist dir_entries_.clear(); // Updates display fl_hide_object(file_dlg_form_->List); fl_clear_browser(file_dlg_form_->List); fl_set_input(file_dlg_form_->DirBox, directory_.c_str()); // Splits complete directory name into directories and compute depth depth_ = 0; string line; string Temp; string File = directory_; if (File != "/") File = split(File, Temp, '/'); while (!File.empty() || !Temp.empty()) { string const dline = "@b" + line + Temp + '/'; fl_add_browser_line(file_dlg_form_->List, dline.c_str()); File = split(File, Temp, '/'); line += ' '; ++depth_; } vector const glob_matches = expand_globs(mask_, directory_); fs::directory_iterator beg(directory_); fs::directory_iterator end; for (; beg != end; ++beg) { string const fname = beg->leaf(); // If the pattern doesn't start with a dot, skip hidden files if (!mask_.empty() && mask_[0] != '.' && fname[0] == '.') continue; bool const isDir = fs::is_directory(*beg); // filters files according to pattern and type typedef vector::const_iterator viterator; viterator gbegin = glob_matches.begin(); viterator const gend = glob_matches.end(); if (!isDir && std::find(gbegin, gend, fname) == gend) continue; DirEntry tmp; // creates used name tmp.name_ = fname; if (isDir) tmp.name_ += '/'; // creates displayed name tmp.displayed_ = fname; dir_entries_.push_back(tmp); } // Sort the names sort(dir_entries_.begin(), dir_entries_.end(), comp_direntry()); // Add them to directory box for (DirEntries::const_iterator cit = dir_entries_.begin(); cit != dir_entries_.end(); ++cit) { string const temp = line + cit->displayed_; fl_add_browser_line(file_dlg_form_->List, temp.c_str()); } fl_set_browser_topline(file_dlg_form_->List, depth_); fl_show_object(file_dlg_form_->List); last_sel_ = -1; } // SetDirectory: sets dialog current directory void FileDialog::Private::SetDirectory(string const & path) { string tmp; if (path.empty()) tmp = getcwd(); else tmp = MakeAbsPath(ExpandPath(path), directory_); // must check the directory exists if (!fs::exists(tmp) || !fs::is_directory(tmp)) { // FIXME: re-add ... #if 0 Alert::err_alert(_("Warning! Couldn't open directory."), tmp); #endif } else { directory_ = tmp; } } void FileDialog::Private::SetFilters(string const & mask) { SetFilters(FileFilterList(mask)); } void FileDialog::Private::SetFilters(FileFilterList const & filters) { if (filters.empty()) return; // Just take the first one for now. typedef FileFilterList::Filter::glob_iterator glob_iterator; glob_iterator const begin = filters[0].begin(); glob_iterator const end = filters[0].end(); if (begin == end) return; ostringstream ss; for (glob_iterator it = begin; it != end; ++it) { if (it != begin) ss << ' '; ss << *it; } mask_ = ss.str(); fl_set_input(file_dlg_form_->PatBox, mask_.c_str()); } FileDialog::Private::Private() { directory_ = MakeAbsPath(string(".")); // Creates form if necessary. if (!file_dlg_form_) { file_dlg_form_ = build_filedialog(this); minw_ = file_dlg_form_->form->w; minh_ = file_dlg_form_->form->h; // Set callbacks. This means that we don't need a patch file fl_set_object_callback(file_dlg_form_->DirBox, C_LyXFileDlg_FileDlgCB, 0); fl_set_object_callback(file_dlg_form_->PatBox, C_LyXFileDlg_FileDlgCB, 1); fl_set_object_callback(file_dlg_form_->List, C_LyXFileDlg_FileDlgCB, 2); fl_set_object_callback(file_dlg_form_->Filename, C_LyXFileDlg_FileDlgCB, 3); fl_set_object_callback(file_dlg_form_->Rescan, C_LyXFileDlg_FileDlgCB, 10); fl_set_object_callback(file_dlg_form_->Home, C_LyXFileDlg_FileDlgCB, 11); fl_set_object_callback(file_dlg_form_->User1, C_LyXFileDlg_FileDlgCB, 12); fl_set_object_callback(file_dlg_form_->User2, C_LyXFileDlg_FileDlgCB, 13); // Make sure pressing the close box doesn't crash LyX. (RvdK) fl_set_form_atclose(file_dlg_form_->form, C_LyXFileDlg_CancelCB, 0); // Register doubleclick callback fl_set_browser_dblclick_callback(file_dlg_form_->List, C_LyXFileDlg_DoubleClickCB, 0); } fl_hide_object(file_dlg_form_->User1); fl_hide_object(file_dlg_form_->User2); r_ = Dialogs::redrawGUI().connect(boost::bind(&FileDialog::Private::redraw, this)); } FileDialog::Private::~Private() { r_.disconnect(); } void FileDialog::Private::redraw() { if (file_dlg_form_->form && file_dlg_form_->form->visible) fl_redraw_form(file_dlg_form_->form); } // SetButton: sets file selector user button action void FileDialog::Private::SetButton(int index, string const & name, string const & path) { FL_OBJECT * ob; string * tmp; if (index == 0) { ob = file_dlg_form_->User1; tmp = &user_path1_; } else if (index == 1) { ob = file_dlg_form_->User2; tmp = &user_path2_; } else { return; } if (!name.empty()) { fl_set_object_label(ob, idex(name).c_str()); fl_set_button_shortcut(ob, scex(name).c_str(), 1); fl_show_object(ob); *tmp = path; } else { fl_hide_object(ob); tmp->erase(); } } // GetDirectory: gets last dialog directory string const FileDialog::Private::GetDirectory() const { if (!directory_.empty()) return directory_; else return string("."); } namespace { bool x_sync_kludge(bool ret) { XSync(fl_get_display(), false); return ret; } } // namespace anon // RunDialog: handle dialog during file selection bool FileDialog::Private::RunDialog() { force_cancel_ = false; force_ok_ = false; // event loop while (true) { FL_OBJECT * ob = fl_do_forms(); if (ob == file_dlg_form_->Ready) { if (HandleOK()) return x_sync_kludge(true); } else if (ob == file_dlg_form_->Cancel || force_cancel_) return x_sync_kludge(false); else if (force_ok_) return x_sync_kludge(true); } } // XForms objects callback (static) void FileDialog::Private::FileDlgCB(FL_OBJECT *, long arg) { if (!current_dlg_) return; switch (arg) { case 0: // get directory current_dlg_->SetDirectory(fl_get_input(file_dlg_form_->DirBox)); current_dlg_->Reread(); break; case 1: // get mask current_dlg_->SetFilters(fl_get_input(file_dlg_form_->PatBox)); current_dlg_->Reread(); break; case 2: // list current_dlg_->HandleListHit(); break; case 10: // rescan current_dlg_->SetDirectory(fl_get_input(file_dlg_form_->DirBox)); current_dlg_->SetFilters(fl_get_input(file_dlg_form_->PatBox)); current_dlg_->Reread(); break; case 11: // home current_dlg_->SetDirectory(package().home_dir()); current_dlg_->SetFilters(fl_get_input(file_dlg_form_->PatBox)); current_dlg_->Reread(); break; case 12: // user button 1 current_dlg_->SetDirectory(current_dlg_->user_path1_); current_dlg_->SetFilters(fl_get_input(file_dlg_form_->PatBox)); current_dlg_->Reread(); break; case 13: // user button 2 current_dlg_->SetDirectory(current_dlg_->user_path2_); current_dlg_->SetFilters(fl_get_input(file_dlg_form_->PatBox)); current_dlg_->Reread(); break; } } // Handle callback from list void FileDialog::Private::HandleListHit() { // set info line int const select_ = fl_get_browser(file_dlg_form_->List); string line = (select_ > depth_ ? dir_entries_[select_ - depth_ - 1].name_ : string()); if (suffixIs(line, '/')) line.clear(); fl_set_input(file_dlg_form_->Filename, line.c_str()); } // Callback for double click in list void FileDialog::Private::DoubleClickCB(FL_OBJECT *, long) { // Simulate click on OK button if (current_dlg_->HandleDoubleClick()) current_dlg_->Force(false); } // Handle double click from list bool FileDialog::Private::HandleDoubleClick() { string tmp; // set info line bool isDir = true; int const select_ = fl_get_browser(file_dlg_form_->List); if (select_ > depth_) { tmp = dir_entries_[select_ - depth_ - 1].name_; if (!suffixIs(tmp, '/')) { isDir = false; fl_set_input(file_dlg_form_->Filename, tmp.c_str()); } } else if (select_ == 0) return true; // executes action if (isDir) { string Temp; // builds new directory name if (select_ > depth_) { // Directory deeper down // First, get directory with trailing / Temp = fl_get_input(file_dlg_form_->DirBox); if (!suffixIs(Temp, '/')) Temp += '/'; Temp += tmp; } else { // Directory higher up Temp.erase(); for (int i = 0; i < select_; ++i) { string const piece = fl_get_browser_line(file_dlg_form_->List, i + 1); // The '+2' is here to count the '@b' (JMarc) Temp += piece.substr(i + 2); } } // assigns it SetDirectory(Temp); Reread(); return false; } return true; } // Handle OK button call bool FileDialog::Private::HandleOK() { // mask was changed string tmp = fl_get_input(file_dlg_form_->PatBox); if (tmp != mask_) { SetFilters(tmp); Reread(); return false; } // directory was changed tmp = fl_get_input(file_dlg_form_->DirBox); if (tmp != directory_) { SetDirectory(tmp); Reread(); return false; } // Handle return from list int const select = fl_get_browser(file_dlg_form_->List); if (select > depth_) { string const temp = dir_entries_[select - depth_ - 1].name_; if (!suffixIs(temp, '/')) { // If user didn't type anything, use browser string const name = fl_get_input(file_dlg_form_->Filename); if (name.empty()) fl_set_input(file_dlg_form_->Filename, temp.c_str()); return true; } } // Emulate a doubleclick return HandleDoubleClick(); } // Handle Cancel CB from WM close int FileDialog::Private::CancelCB(FL_FORM *, void *) { // Simulate a click on the cancel button current_dlg_->Force(true); return FL_IGNORE; } // Simulates a click on OK/Cancel void FileDialog::Private::Force(bool cancel) { if (cancel) { force_cancel_ = true; fl_set_button(file_dlg_form_->Cancel, 1); } else { force_ok_ = true; fl_set_button(file_dlg_form_->Ready, 1); } // Start timer to break fl_do_forms loop soon fl_set_timer(file_dlg_form_->timer, 0.1); } // Select: launches dialog and returns selected file string const FileDialog::Private::Select(string const & title, string const & path, FileFilterList const & filters, string const & suggested) { // handles new mask and path SetFilters(filters); SetDirectory(path); Reread(); // highlight the suggested file in the browser, if it exists. int sel = 0; string const filename = OnlyFilename(suggested); if (!filename.empty()) { for (int i = 0; i < fl_get_browser_maxline(file_dlg_form_->List); ++i) { string s = fl_get_browser_line(file_dlg_form_->List, i + 1); s = trim(s); if (s == filename) { sel = i + 1; break; } } } if (sel != 0) fl_select_browser_line(file_dlg_form_->List, sel); int const top = max(sel - 5, 1); fl_set_browser_topline(file_dlg_form_->List, top); // checks whether dialog can be started if (current_dlg_) return string(); current_dlg_ = this; // runs dialog setEnabled(file_dlg_form_->Filename, true); fl_set_input(file_dlg_form_->Filename, suggested.c_str()); fl_set_button(file_dlg_form_->Cancel, 0); fl_set_button(file_dlg_form_->Ready, 0); fl_set_focus_object(file_dlg_form_->form, file_dlg_form_->Filename); fl_deactivate_all_forms(); // Prevent xforms crashing if the dialog gets too small by preventing // it from being shrunk beyond a minimum size. // calls to fl_set_form_minsize/maxsize apply only to the next // fl_show_form(), so this comes first. fl_set_form_minsize(file_dlg_form_->form, minw_, minh_); fl_show_form(file_dlg_form_->form, FL_PLACE_MOUSE | FL_FREE_SIZE, 0, title.c_str()); bool const isOk = RunDialog(); fl_hide_form(file_dlg_form_->form); fl_activate_all_forms(); current_dlg_ = 0; // Returns filename or string() if no valid selection was made if (!isOk || !fl_get_input(file_dlg_form_->Filename)[0]) return string(); file_name_ = fl_get_input(file_dlg_form_->Filename); if (!AbsolutePath(file_name_)) file_name_ = AddName(fl_get_input(file_dlg_form_->DirBox), file_name_); return file_name_; } // SelectDir: launches dialog and returns selected directory string const FileDialog::Private::SelectDir(string const & title, string const & path, string const & suggested) { SetFilters("*/"); // handles new path bool isOk = true; if (!path.empty()) { // handle case where path does not end with "/" // remerge path+suggested and check if it is a valid path if (!suggested.empty()) { string tmp = suggested; if (!suffixIs(tmp, '/')) tmp += '/'; string const full_path = path + tmp; // check if this is really a directory if (fs::exists(full_path) && fs::is_directory(full_path)) SetDirectory(full_path); else SetDirectory(path); } else SetDirectory(path); isOk = false; } if (!isOk) Reread(); // checks whether dialog can be started if (current_dlg_) return string(); current_dlg_ = this; // runs dialog fl_set_input(file_dlg_form_->Filename, ""); setEnabled(file_dlg_form_->Filename, false); fl_set_button(file_dlg_form_->Cancel, 0); fl_set_button(file_dlg_form_->Ready, 0); fl_set_focus_object(file_dlg_form_->form, file_dlg_form_->DirBox); fl_deactivate_all_forms(); fl_show_form(file_dlg_form_->form, FL_PLACE_MOUSE | FL_FREE_SIZE, 0, title.c_str()); isOk = RunDialog(); fl_hide_form(file_dlg_form_->form); fl_activate_all_forms(); current_dlg_ = 0; // Returns directory or string() if no valid selection was made if (!isOk) return string(); file_name_ = fl_get_input(file_dlg_form_->DirBox); return file_name_; }