]> git.lyx.org Git - lyx.git/blob - src/frontends/qt/GuiLyXFiles.cpp
FancyLineEdit: use QLineEdit's own clear button with Qt >= 5.2
[lyx.git] / src / frontends / qt / GuiLyXFiles.cpp
1 /**
2  * \file GuiLyXFiles.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Jürgen Spitzmüller
7  *
8  * Full author contact details are available in file CREDITS.
9  */
10
11 #include <config.h>
12
13 #include "GuiLyXFiles.h"
14 #include "GuiApplication.h"
15 #include "qt_helpers.h"
16
17 #include "FileDialog.h"
18 #include "FuncRequest.h"
19 #include "Language.h"
20 #include "LyXRC.h"
21
22 #include "support/environment.h"
23 #include "support/gettext.h"
24 #include "support/lstrings.h"
25 #include "support/Messages.h"
26 #include "support/qstring_helpers.h"
27 #include "support/Package.h"
28
29 #include <QDirIterator>
30 #include <QTreeWidget>
31
32 using namespace std;
33 using namespace lyx::support;
34
35 namespace lyx {
36 namespace frontend {
37
38 namespace {
39
40 QString const guiString(QString const & in)
41 {
42         // recode specially encoded chars in file names (URL encoding and underbar)
43         return QString::fromUtf8(QByteArray::fromPercentEncoding(in.toUtf8())).replace('_', ' ');
44 }
45
46 } // namespace anon
47
48
49 QMap<QString, QString> GuiLyXFiles::getFiles()
50 {
51         QMap<QString, QString> result;
52         // We look for lyx files in the subdirectory dir of
53         //   1) user_lyxdir
54         //   2) build_lyxdir (if not empty)
55         //   3) system_lyxdir
56         // in this order. Files with a given sub-hierarchy will
57         // only be listed once.
58         // We also consider i18n subdirectories and store them separately.
59         QStringList dirs;
60         QStringList relpaths;
61
62         // The three locations to look at.
63         string const user = addPath(package().user_support().absFileName(), fromqstr(type_));
64         string const build = addPath(package().build_support().absFileName(), fromqstr(type_));
65         string const system = addPath(package().system_support().absFileName(), fromqstr(type_));
66
67         available_languages_.insert(toqstr("en"), qt_("English"));
68
69         QString const type = fileTypeCO->itemData(fileTypeCO->currentIndex()).toString();
70
71         // Search in the base paths
72         if (type == "all" || type == "user")
73                 dirs << toqstr(user);
74         if (type == "all" || type == "system")
75                 dirs << toqstr(build)
76                      << toqstr(system);
77
78         for (int i = 0; i < dirs.size(); ++i) {
79                 QString const & dir = dirs.at(i);
80                 QDirIterator it(dir, QDir::Files, QDirIterator::Subdirectories);
81                 while (it.hasNext()) {
82                         QString fn(QFile(it.next()).fileName());
83                         if (!fn.endsWith(getSuffix()))
84                                 continue;
85                         QString relpath = toqstr(makeRelPath(qstring_to_ucs4(fn),
86                                                              qstring_to_ucs4(dir)));
87                         // <cat>/
88                         int s = relpath.indexOf('/', 0);
89                         QString cat = qt_("General");
90                         QString localization = "en";
91                         if (s != -1) {
92                                 // <cat>/<subcat>/
93                                 cat = relpath.left(s);
94                                 if (all_languages_.contains(cat)
95                                     && !all_languages_.contains(dir.right(dir.lastIndexOf('/')))) {
96                                         QMap<QString, QString>::const_iterator li = all_languages_.find(cat);
97                                         // Skip i18n dir, but add language to the combo
98                                         if (!available_languages_.contains(li.key()))
99                                                 available_languages_.insert(li.key(), li.value());
100                                         localization = cat;
101                                         int sc = relpath.indexOf('/', s + 1);
102                                         cat = (sc == -1) ? qt_("General") : relpath.mid(s + 1, sc - s - 1);
103                                         s = sc;
104                                 }
105                                 if (s != -1) {
106                                         int sc = relpath.indexOf('/', s + 1);
107                                         QString const subcat = (sc == -1) ?
108                                                                 QString() : relpath.mid(s + 1, sc - s - 1);
109                                         if (!subcat.isEmpty())
110                                                 cat += '/' + subcat;
111                                 }
112                         }
113                         if (!relpaths.contains(relpath)) {
114                                 relpaths.append(relpath);
115                                 if (localization != "en")
116                                         // strip off lang/
117                                         relpath = relpath.mid(relpath.indexOf('/') + 1);
118                                 result.insert(relpath, cat);
119                                                                         
120                                 QMap<QString, QString> lm;
121                                 if (localizations_.contains(relpath))
122                                         lm = localizations_.find(relpath).value();
123                                 lm.insert(localization, fn);
124                                 localizations_.insert(relpath, lm);
125                         }
126                 }
127         }
128         // Find and store GUI language
129         for (auto const & l : guilangs_) {
130                 // First try with the full name
131                 // `en' files are not in a subdirectory
132                 if (available_languages_.contains(toqstr(l))) {
133                         guilang_ = toqstr(l);
134                         break;
135                 }
136                 // Then the name without country code
137                 string const shortl = token(l, '_', 0);
138                 if (available_languages_.contains(toqstr(shortl))) {
139                         guilang_ = toqstr(shortl);
140                         break;
141                 }
142         }
143         // pre-fill the language combo (it will be updated once an item 
144         // has been clicked)
145         languageCO->clear();
146         QMap<QString, QString>::const_iterator i =available_languages_.constBegin();
147         while (i != available_languages_.constEnd()) {
148                 languageCO->addItem(i.value(), i.key());
149                 ++i;
150         }
151         setLanguage();
152         languageLA->setText(qt_("Preferred &Language:"));
153         return result;
154 }
155
156
157 GuiLyXFiles::GuiLyXFiles(GuiView & lv)
158         : GuiDialog(lv, "lyxfiles", qt_("New File From Template"))
159 {
160         setupUi(this);
161
162         // Get all supported languages (by code) in order to exclude those
163         // dirs later.
164         QAbstractItemModel * language_model = guiApp->languageModel();
165         language_model->sort(0);
166         for (int i = 0; i != language_model->rowCount(); ++i) {
167                 QModelIndex index = language_model->index(i, 0);
168                 Language const * lang =
169                         languages.getLanguage(fromqstr(index.data(Qt::UserRole).toString()));
170                 if (!lang)
171                         continue;
172                 QString const code = toqstr(lang->code());
173                 if (!all_languages_.contains(code))
174                         all_languages_.insert(code, qt_(lang->display()));
175                 // Also store code without country code
176                 QString const shortcode = code.left(code.indexOf('_'));
177                 if (shortcode != code && !all_languages_.contains(shortcode))
178                         all_languages_.insert(shortcode, qt_(lang->display()));
179         }
180         // Get GUI language
181         string lang = getGuiMessages().language();
182         string const language = getEnv("LANGUAGE");
183         if (!language.empty())
184                 lang += ":" + language;
185         guilangs_ =  getVectorFromString(lang, ":");
186
187         // The filter bar
188         filter_ = new FancyLineEdit(this);
189         filter_->setClearButton(true);
190         filter_->setPlaceholderText(qt_("All available files"));
191         filter_->setToolTip(qt_("Enter string to filter the list of available files"));
192 #if (QT_VERSION < 0x050000)
193         connect(filter_, SIGNAL(downPressed()),
194                 filesLW, SLOT(setFocus()));
195 #else
196         connect(filter_, &FancyLineEdit::downPressed,
197                 filesLW, [this](){ focusAndHighlight(filesLW); });
198 #endif
199
200         filterBarL->addWidget(filter_, 0);
201         findKeysLA->setBuddy(filter_);
202
203         connect(buttonBox, SIGNAL(clicked(QAbstractButton *)),
204                 this, SLOT(slotButtonBox(QAbstractButton *)));
205
206         connect(filesLW, SIGNAL(itemClicked(QTreeWidgetItem *, int)),
207                 this, SLOT(fileSelectionChanged()));
208         connect(filesLW, SIGNAL(itemSelectionChanged()),
209                 this, SLOT(fileSelectionChanged()));
210         connect(filter_, SIGNAL(textEdited(QString)),
211                 this, SLOT(filterLabels()));
212         connect(filter_, SIGNAL(rightButtonClicked()),
213                 this, SLOT(resetFilter()));
214
215         bc().setPolicy(ButtonPolicy::OkApplyCancelPolicy);
216         bc().setOK(buttonBox->button(QDialogButtonBox::Open));
217         bc().setCancel(buttonBox->button(QDialogButtonBox::Cancel));
218
219         //filesLW->setViewMode(QListView::ListMode);
220         filesLW->setIconSize(QSize(22, 22));
221
222         QIcon user_icon(getPixmap("images/", "lyxfiles-user", "svgz,png"));
223         QIcon system_icon(getPixmap("images/", "lyxfiles-system", "svgz,png"));
224         fileTypeCO->addItem(qt_("User and System Files"), toqstr("all"));
225         fileTypeCO->addItem(user_icon, qt_("User Files Only"), toqstr("user"));
226         fileTypeCO->addItem(system_icon, qt_("System Files Only"), toqstr("system"));
227
228         setFocusProxy(filter_);
229 }
230
231
232 QString const GuiLyXFiles::getSuffix()
233 {
234         if (type_ == "bind" || type_ == "ui")
235                 return toqstr(".") + type_;
236         else if (type_ == "kbd")
237                 return ".kmap";
238         
239         return ".lyx";
240 }
241
242
243 bool GuiLyXFiles::translateName() const
244 {
245         return (type_ == "templates" || type_ == "examples");
246 }
247
248
249 void GuiLyXFiles::fileSelectionChanged()
250 {
251         if (!filesLW->currentItem()
252             || !filesLW->currentItem()->data(0, Qt::UserRole).toString().endsWith(getSuffix())) {
253                 // not a file (probably a header)
254                 bc().setValid(false);
255                 return;
256         }
257         changed();
258 }
259
260
261 void GuiLyXFiles::on_fileTypeCO_activated(int)
262 {
263         updateContents();
264 }
265
266
267 void GuiLyXFiles::on_languageCO_activated(int i)
268 {
269         savelang_ = languageCO->itemData(i).toString();
270         if (!filesLW->currentItem())
271                 return;
272
273         filesLW->currentItem()->setData(0, Qt::ToolTipRole, getRealPath());
274         changed();
275 }
276
277
278 void GuiLyXFiles::on_filesLW_itemDoubleClicked(QTreeWidgetItem * item, int)
279 {
280         if (!item || !item->data(0, Qt::UserRole).toString().endsWith(getSuffix())) {
281                 // not a file (probably a header)
282                 bc().setValid(false);
283                 return;
284         }
285
286         applyView();
287         dispatchParams();
288         close();
289 }
290
291 void GuiLyXFiles::on_filesLW_itemClicked(QTreeWidgetItem * item, int)
292 {
293         if (!item) {
294                 bc().setValid(false);
295                 return;
296         }
297
298         QString const data = item->data(0, Qt::UserRole).toString();
299         if (!data.endsWith(getSuffix())) {
300                 // not a file (probably a header)
301                 bc().setValid(false);
302                 return;
303         }
304
305         languageCO->clear();
306         QMap<QString, QString>::const_iterator i =available_languages_.constBegin();
307         while (i != available_languages_.constEnd()) {
308                 if (localizations_.contains(data)
309                     && localizations_.find(data).value().contains(i.key()))
310                         languageCO->addItem(i.value(), i.key());
311                 ++i;
312         }
313         languageLA->setText(qt_("File &Language:"));
314         languageCO->setToolTip(qt_("All available languages of the selected file are displayed here.\n"
315                                    "The selected language version will be opened."));
316         setLanguage();
317         QString const realpath = getRealPath();
318         filesLW->currentItem()->setData(0, Qt::ToolTipRole, realpath);
319         QIcon user_icon(getPixmap("images/", "lyxfiles-user", "svgz,png"));
320         QIcon system_icon(getPixmap("images/", "lyxfiles-system", "svgz,png"));
321         QIcon file_icon = (realpath.startsWith(toqstr(package().user_support().absFileName()))) ?
322                         user_icon : system_icon;
323         item->setIcon(0, file_icon);
324 }
325
326
327 void GuiLyXFiles::setLanguage()
328 {
329         // Enable language selection only if there is a selection.
330         languageCO->setEnabled(languageCO->count() > 1);
331         languageLA->setEnabled(languageCO->count() > 1);
332         // first try last setting
333         if (!savelang_.isEmpty()) {
334                 int index = languageCO->findData(savelang_);
335                 if (index != -1) {
336                         languageCO->setCurrentIndex(index);
337                         return;
338                 }
339         }
340         // next, try GUI lang
341         if (!guilang_.isEmpty()) {
342                 int index = languageCO->findData(guilang_);
343                 if (index != -1) {
344                         languageCO->setCurrentIndex(index);
345                         return;
346                 }
347         }
348         // Finally, fall back to English (which should be always there)
349         int index = languageCO->findData(toqstr("en"));
350         if (index != -1) {
351                 languageCO->setCurrentIndex(index);
352         }
353 }
354
355
356 void GuiLyXFiles::on_browsePB_pressed()
357 {
358         QString path1 = toqstr(lyxrc.document_path);
359         QString path2 = toqstr(lyxrc.example_path);
360         QString title = qt_("Select example file");
361         QString filter = qt_("LyX Documents (*.lyx)");
362         QString b1 = qt_("D&ocuments");
363         QString b2 = qt_("&Examples");
364
365         if (type_ == "templates") {
366                 path2 = toqstr(lyxrc.template_path);
367                 title = qt_("Select template file");
368                 b1 = qt_("D&ocuments");
369                 b2 = qt_("&Templates");
370         }
371         else if (type_ != "examples") {
372                 path1 = toqstr(addName(package().user_support().absFileName(), fromqstr(type_)));
373                 path2 = toqstr(addName(package().system_support().absFileName(), fromqstr(type_)));
374                 b1 = qt_("&User files");
375                 b2 = qt_("&System files");
376         }
377         if (type_ == "ui") {
378                 title = qt_("Chose UI file");
379                 filter = qt_("LyX UI Files (*.ui)");
380         }
381         if (type_ == "bind") {
382                 title = qt_("Chose bind file");
383                 filter = qt_("LyX Bind Files (*.bind)");
384         }
385         if (type_ == "kbd") {
386                 title = qt_("Chose keyboard map");
387                 filter = qt_("LyX Keymap Files (*.kmap)");
388         }
389
390         FileDialog dlg(title);
391         dlg.setButton1(b1, path1);
392         dlg.setButton2(b2, path2);
393
394         FileDialog::Result result = dlg.open(path2, QStringList(filter));
395
396         if (result.first != FileDialog::Later && !result.second.isEmpty()) {
397                 file_ = toqstr(FileName(fromqstr(result.second)).absFileName());
398                 dispatchParams();
399                 close();
400         }
401 }
402
403
404 void GuiLyXFiles::updateContents()
405 {
406         languageCO->clear();
407         QMap<QString, QString> files = getFiles();
408         languageCO->model()->sort(0);
409
410         filesLW->clear();
411         QIcon user_icon(getPixmap("images/", "lyxfiles-user", "svgz,png"));
412         QIcon system_icon(getPixmap("images/", "lyxfiles-system", "svgz,png"));
413         QStringList cats;
414         QMap<QString, QString>::const_iterator it = files.constBegin();
415         QFont capfont;
416         capfont.setBold(true);
417         while (it != files.constEnd()) {
418                 QFileInfo const info = QFileInfo(it.key());
419                 QString const realpath = getRealPath(it.key());
420                 QString cat = it.value();
421                 QString subcat;
422                 QString catsave;
423                 if (cat.contains('/')) {
424                         catsave = cat;
425                         cat = catsave.left(catsave.indexOf('/'));
426                         subcat = toqstr(translateIfPossible(
427                                         qstring_to_ucs4(guiString(catsave.mid(catsave.indexOf('/') + 1)))));
428                 }
429                 cat =  toqstr(translateIfPossible(qstring_to_ucs4(guiString(cat))));
430                 QTreeWidgetItem * catItem;
431                 if (!cats.contains(cat)) {
432                         catItem = new QTreeWidgetItem();
433                         catItem->setText(0, cat);
434                         catItem->setFont(0, capfont);
435                         filesLW->insertTopLevelItem(0, catItem);
436                         catItem->setExpanded(true);
437                         cats << cat;
438                 } else
439                         catItem = filesLW->findItems(cat, Qt::MatchExactly).first();
440                 QTreeWidgetItem * item = new QTreeWidgetItem();
441                 QString const filename = info.fileName();
442                 QString guiname = filename.left(filename.lastIndexOf(getSuffix())).replace('_', ' ');
443                 // Special case: defaults.lyx
444                 if (type_ == "templates" && guiname == "defaults")
445                         guiname = qt_("Default Template");
446                 else if (translateName())
447                         guiname = toqstr(translateIfPossible(qstring_to_ucs4(guiString(guiname))));
448                 QIcon file_icon = (realpath.startsWith(toqstr(package().user_support().absFileName()))) ?
449                                 user_icon : system_icon;
450                 item->setIcon(0, file_icon);
451                 item->setData(0, Qt::UserRole, it.key());
452                 item->setData(0, Qt::DisplayRole, guiname);
453                 item->setData(0, Qt::ToolTipRole, realpath);
454                 if (subcat.isEmpty())
455                         catItem->addChild(item);
456                 else {
457                         QTreeWidgetItem * subcatItem = nullptr;
458                         if (cats.contains(catsave)) {
459                                 QList<QTreeWidgetItem *> pcats = filesLW->findItems(cat, Qt::MatchExactly);
460                                 for (auto const & pcat : pcats) {
461                                         for (int cit = 0; cit < pcat->childCount(); ++cit) {
462                                                 if (pcat->child(cit)->text(0) == subcat) {
463                                                         subcatItem = pcat->child(cit);
464                                                         break;
465                                                 }
466                                         }
467                                 }
468                         }
469                         if (!subcatItem) {
470                                 subcatItem = new QTreeWidgetItem();
471                                 subcatItem->setText(0, subcat);
472                                 subcatItem->setIcon(0, file_icon);
473                                 cats << catsave;
474                         }
475                         subcatItem->addChild(item);
476                         catItem->addChild(subcatItem);
477                 }
478                 ++it;
479         }
480         filesLW->sortItems(0, Qt::AscendingOrder);
481         // redo filter
482         filterLabels();
483 }
484
485
486 void GuiLyXFiles::slotButtonBox(QAbstractButton * button)
487 {
488         switch (buttonBox->standardButton(button)) {
489         case QDialogButtonBox::Open:
490                 slotOK();
491                 break;
492         case QDialogButtonBox::Cancel:
493                 slotClose();
494                 break;
495         default:
496                 break;
497         }
498 }
499
500
501 void GuiLyXFiles::filterLabels()
502 {
503         Qt::CaseSensitivity cs = csFindCB->isChecked() ?
504                 Qt::CaseSensitive : Qt::CaseInsensitive;
505         QTreeWidgetItemIterator it(filesLW);
506         while (*it) {
507                 (*it)->setHidden(
508                         (*it)->childCount() == 0
509                         && !(*it)->text(0).contains(filter_->text(), cs)
510                 );
511                 ++it;
512         }
513 }
514
515
516 void GuiLyXFiles::resetFilter()
517 {
518         filter_->setText(QString());
519         filterLabels();
520 }
521
522 QString const GuiLyXFiles::getRealPath(QString relpath)
523 {
524         if (relpath.isEmpty() && filesLW->currentItem() != nullptr)
525                 relpath = filesLW->currentItem()->data(0, Qt::UserRole).toString();
526         QString const language = languageCO->itemData(languageCO->currentIndex()).toString();
527         if (localizations_.contains(relpath)) {
528                 if (localizations_.find(relpath).value().contains(language))
529                         return localizations_.find(relpath).value().find(language).value();
530                 else if (localizations_.find(relpath).value().contains(guilang_))
531                         return localizations_.find(relpath).value().find(guilang_).value();
532                 else if (localizations_.find(relpath).value().contains(toqstr("en")))
533                         return localizations_.find(relpath).value().find(toqstr("en")).value();
534         }
535         return QString();
536 }
537
538
539 void GuiLyXFiles::applyView()
540 {
541         file_ = getRealPath();
542 }
543
544
545 bool GuiLyXFiles::isValid()
546 {
547         return filesLW->currentItem() && filesLW->currentItem()->isSelected();
548 }
549
550
551 bool GuiLyXFiles::initialiseParams(string const & type)
552 {
553         type_ = type.empty() ? toqstr("templates") : toqstr(type);
554         paramsToDialog();
555         return true;
556 }
557
558
559 void GuiLyXFiles::passParams(string const & data)
560 {
561         initialiseParams(data);
562         updateContents();
563 }
564
565
566 void GuiLyXFiles::selectItem(QString const & item)
567 {
568         /* Using an intermediary variable flags is needed up to at least
569          * Qt 5.5 because of a subtle namespace issue. See:
570          *   https://stackoverflow.com/questions/10755058/qflags-enum-type-conversion-fails-all-of-a-sudden
571          * for details.*/
572         Qt::MatchFlags const flags(Qt::MatchExactly|Qt::MatchRecursive);
573         QList<QTreeWidgetItem *> twi = filesLW->findItems(item, flags);
574         if (!twi.isEmpty())
575                 twi.first()->setSelected(true);
576 }
577
578
579 void GuiLyXFiles::paramsToDialog()
580 {
581         if (type_ == "examples")
582                 setTitle(qt_("Open Example File"));
583         else if (type_ == "templates")
584                 setTitle(qt_("New File From Template"));
585         else
586                 setTitle(qt_("Open File"));
587
588         bc().setValid(isValid());
589 }
590
591
592 void GuiLyXFiles::dispatchParams()
593 {
594         if (file_.isEmpty())
595                 return;
596
597         string arg;
598         if (type_ == "templates")
599                 arg = "newfile ";
600         arg += quoteName(fromqstr(file_));
601         FuncCode const lfun = getLfun();
602
603         if (lfun == LFUN_NOACTION)
604                 // emit signal
605                 fileSelected(file_);
606         else
607                 dispatch(FuncRequest(lfun, arg));
608 }
609
610
611 FuncCode GuiLyXFiles::getLfun() const
612 {
613         if (type_ == "examples")
614                 return LFUN_FILE_OPEN;
615         else if (type_ == "templates")
616                 return LFUN_BUFFER_NEW_TEMPLATE;
617         return LFUN_NOACTION;
618 }
619
620
621 } // namespace frontend
622 } // namespace lyx
623
624 #include "moc_GuiLyXFiles.cpp"