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