]> git.lyx.org Git - lyx.git/blob - src/frontends/qt/qt_helpers.cpp
Allow LyX to find pdfs and urls of citation references and follow them from context...
[lyx.git] / src / frontends / qt / qt_helpers.cpp
1 /**
2  * \file qt_helpers.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Dekel Tsur
7  * \author Jürgen Spitzmüller
8  * \author Richard Heck
9  *
10  * Full author contact details are available in file CREDITS.
11  */
12
13 #include <config.h>
14
15 #include "qt_helpers.h"
16
17 #include "FileDialog.h"
18 #include "LengthCombo.h"
19
20 #include "frontends/alert.h"
21
22 #include "BufferParams.h"
23 #include "FloatList.h"
24 #include "FuncRequest.h"
25 #include "Language.h"
26 #include "LyX.h"
27 #include "LyXAction.h"
28 #include "TextClass.h"
29
30 #include "support/convert.h"
31 #include "support/debug.h"
32 #include "support/gettext.h"
33 #include "support/Length.h"
34 #include "support/lstrings.h"
35 #include "support/lyxalgo.h"
36 #include "support/os.h"
37 #include "support/Package.h"
38 #include "support/PathChanger.h"
39 #include "support/Systemcall.h"
40
41 #include <QApplication>
42 #include <QCheckBox>
43 #include <QComboBox>
44 #include <QDesktopServices>
45 #include <QDir>
46 #include <QLineEdit>
47 #include <QLocale>
48 #include <QPalette>
49 #include <QSet>
50 #include <QTextLayout>
51 #include <QTextDocument>
52 #include <QToolTip>
53 #include <QUrl>
54
55 #include <algorithm>
56 #include <fstream>
57 #include <locale>
58
59 // for FileFilter.
60 // FIXME: Remove
61 #include "support/regex.h"
62
63
64 using namespace std;
65 using namespace lyx::support;
66
67 namespace lyx {
68
69 FileName libFileSearch(QString const & dir, QString const & name,
70                                 QString const & ext, search_mode mode)
71 {
72         return support::libFileSearch(fromqstr(dir), fromqstr(name), fromqstr(ext), mode);
73 }
74
75
76 FileName imageLibFileSearch(QString & dir, QString const & name,
77                                 QString const & ext, search_mode mode)
78 {
79         string tmp = fromqstr(dir);
80         FileName fn = support::imageLibFileSearch(tmp, fromqstr(name), fromqstr(ext), mode);
81         dir = toqstr(tmp);
82         return fn;
83 }
84
85 namespace {
86
87 double locstringToDouble(QString const & str)
88 {
89         QLocale loc;
90         bool ok;
91         double res = loc.toDouble(str, &ok);
92         if (!ok) {
93                 // Fall back to C
94                 QLocale c(QLocale::C);
95                 res = c.toDouble(str);
96         }
97         return res;
98 }
99
100 } // namespace
101
102
103 namespace frontend {
104
105 string widgetsToLength(QLineEdit const * input, LengthCombo const * combo)
106 {
107         QString const length = input->text();
108         if (length.isEmpty())
109                 return string();
110
111         // Don't return unit-from-choice if the input(field) contains a unit
112         if (isValidGlueLength(fromqstr(length)))
113                 return fromqstr(length);
114
115         Length::UNIT const unit = combo->currentLengthItem();
116
117         return Length(locstringToDouble(length.trimmed()), unit).asString();
118 }
119
120
121 Length widgetsToLength(QLineEdit const * input, QComboBox const * combo)
122 {
123         QString const length = input->text();
124         if (length.isEmpty())
125                 return Length();
126
127         // don't return unit-from-choice if the input(field) contains a unit
128         if (isValidGlueLength(fromqstr(length)))
129                 return Length(fromqstr(length));
130
131         Length::UNIT unit = Length::UNIT_NONE;
132         QString const item = combo->currentText();
133         for (int i = 0; i < num_units; i++) {
134                 if (qt_(lyx::unit_name_gui[i]) == item) {
135                         unit = unitFromString(unit_name[i]);
136                         break;
137                 }
138         }
139
140         return Length(locstringToDouble(length.trimmed()), unit);
141 }
142
143
144 void lengthToWidgets(QLineEdit * input, LengthCombo * combo,
145         Length const & len, Length::UNIT /*defaultUnit*/)
146 {
147         if (len.empty()) {
148                 // no length (UNIT_NONE)
149                 combo->setCurrentItem(Length::defaultUnit());
150                 input->setText("");
151         } else {
152                 combo->setCurrentItem(len.unit());
153                 QLocale loc;
154                 loc.setNumberOptions(QLocale::OmitGroupSeparator);
155                 input->setText(formatLocFPNumber(Length(len).value()));
156         }
157 }
158
159
160 void lengthToWidgets(QLineEdit * input, LengthCombo * combo,
161         string const & len, Length::UNIT defaultUnit)
162 {
163         if (len.empty()) {
164                 // no length (UNIT_NONE)
165                 combo->setCurrentItem(defaultUnit);
166                 input->setText("");
167         } else if (!isValidLength(len) && !isStrDbl(len)) {
168                 // use input field only for gluelengths
169                 combo->setCurrentItem(defaultUnit);
170                 input->setText(toqstr(len));
171         } else {
172                 lengthToWidgets(input, combo, Length(len), defaultUnit);
173         }
174 }
175
176
177 void lengthToWidgets(QLineEdit * input, LengthCombo * combo,
178         docstring const & len, Length::UNIT defaultUnit)
179 {
180         lengthToWidgets(input, combo, to_utf8(len), defaultUnit);
181 }
182
183
184 double widgetToDouble(QLineEdit const * input)
185 {
186         QString const text = input->text();
187         if (text.isEmpty())
188                 return 0.0;
189
190         return locstringToDouble(text.trimmed());
191 }
192
193
194 string widgetToDoubleStr(QLineEdit const * input)
195 {
196         return convert<string>(widgetToDouble(input));
197 }
198
199
200 void doubleToWidget(QLineEdit * input, double const & value, char f, int prec)
201 {
202         QLocale loc;
203         loc.setNumberOptions(QLocale::OmitGroupSeparator);
204         input->setText(loc.toString(value, f, prec));
205 }
206
207
208 void doubleToWidget(QLineEdit * input, string const & value, char f, int prec)
209 {
210         doubleToWidget(input, convert<double>(value), f, prec);
211 }
212
213
214 QString formatLocFPNumber(double d)
215 {
216         QString result = toqstr(formatFPNumber(d));
217         QLocale loc;
218         result.replace('.', loc.decimalPoint());
219         return result;
220 }
221
222
223 bool SortLocaleAware(QString const & lhs, QString const & rhs)
224 {
225         return QString::localeAwareCompare(lhs, rhs) < 0;
226 }
227
228
229 bool ColorSorter(ColorCode lhs, ColorCode rhs)
230 {
231         return compare_no_case(lcolor.getGUIName(lhs), lcolor.getGUIName(rhs)) < 0;
232 }
233
234
235 void setValid(QWidget * widget, bool valid)
236 {
237         if (valid) {
238                 widget->setPalette(QPalette());
239         } else {
240                 QPalette pal = widget->palette();
241                 pal.setColor(QPalette::Active, QPalette::Foreground, QColor(255, 0, 0));
242                 widget->setPalette(pal);
243         }
244 }
245
246
247 void focusAndHighlight(QAbstractItemView * w)
248 {
249         w->setFocus();
250         w->setCurrentIndex(w->currentIndex());
251         w->scrollTo(w->currentIndex());
252 }
253
254
255 void setMessageColour(list<QWidget *> highlighted, list<QWidget *> plain)
256 {
257         QPalette pal = QApplication::palette();
258         QPalette newpal(pal.color(QPalette::Active, QPalette::HighlightedText),
259                         pal.color(QPalette::Active, QPalette::Highlight));
260         for (QWidget * w : highlighted)
261                 w->setPalette(newpal);
262         for (QWidget * w : plain)
263                 w->setPalette(pal);
264 }
265
266
267 /// wrapper to hide the change of method name to setSectionResizeMode
268 void setSectionResizeMode(QHeaderView * view,
269     int logicalIndex, QHeaderView::ResizeMode mode) {
270 #if (QT_VERSION >= 0x050000)
271         view->setSectionResizeMode(logicalIndex, mode);
272 #else
273         view->setResizeMode(logicalIndex, mode);
274 #endif
275 }
276
277 void setSectionResizeMode(QHeaderView * view, QHeaderView::ResizeMode mode) {
278 #if (QT_VERSION >= 0x050000)
279         view->setSectionResizeMode(mode);
280 #else
281         view->setResizeMode(mode);
282 #endif
283 }
284
285 void showDirectory(FileName const & directory)
286 {
287         if (!directory.exists())
288                 return;
289         QUrl qurl(QUrl::fromLocalFile(QDir::toNativeSeparators(toqstr(directory.absFileName()))));
290         // Give hints in case of bugs
291         if (!qurl.isValid()) {
292                 LYXERR0("QUrl is invalid!");
293                 return;
294
295         }
296         if (!QDesktopServices::openUrl(qurl))
297                 LYXERR0("Unable to open QUrl even though dir exists!");
298 }
299
300 void showTarget(string const & target){
301         LYXERR(Debug::INSETS, "Showtarget:" << target << "\n");
302         if (prefixIs(target,"EXTERNAL ")) {
303                 string tmp,tar;
304                 tar = split(target, tmp, ' ');
305                 FuncRequest cmd = FuncRequest(LFUN_VC_COMMAND,"U . \"lyxpaperview " + tar + "\"");
306                 lyx::dispatch(cmd);
307                 return;
308         } 
309         QDesktopServices::openUrl(QUrl(toqstr(target), QUrl::TolerantMode));
310 }
311 } // namespace frontend
312
313 QString const qt_(char const * str, const char *)
314 {
315         return toqstr(_(str));
316 }
317
318
319 QString const qt_(string const & str)
320 {
321         return toqstr(_(str));
322 }
323
324
325 QString const qt_(QString const & qstr)
326 {
327         return toqstr(_(fromqstr(qstr)));
328 }
329
330
331 void rescanTexStyles(string const & arg)
332 {
333         // Run rescan in user lyx directory
334         PathChanger p(package().user_support());
335         FileName const prog = support::libFileSearch("scripts", "TeXFiles.py");
336         Systemcall one;
337         string const command = os::python() + ' ' +
338             quoteName(prog.toFilesystemEncoding()) + ' ' +
339             arg;
340         int const status = one.startscript(Systemcall::Wait, command);
341         if (status == 0)
342                 return;
343         // FIXME UNICODE
344         frontend::Alert::error(_("Could not update TeX information"),
345                 bformat(_("The script `%1$s' failed."), from_utf8(prog.absFileName())));
346 }
347
348
349 QStringList texFileList(QString const & filename)
350 {
351         QStringList list;
352         FileName const file = libFileSearch(QString(), filename);
353         if (file.empty())
354                 return list;
355
356         // FIXME Unicode.
357         vector<docstring> doclist =
358                 getVectorFromString(file.fileContents("UTF-8"), from_ascii("\n"));
359
360         // Normalise paths like /foo//bar ==> /foo/bar
361         QSet<QString> set;
362         for (size_t i = 0; i != doclist.size(); ++i) {
363                 QString qfile = toqstr(doclist[i]);
364                 qfile.replace("\r", "");
365                 while (qfile.contains("//"))
366                         qfile.replace("//", "/");
367                 if (!qfile.isEmpty())
368                         set.insert(qfile);
369         }
370
371         // remove duplicates
372 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
373         return QList<QString>(set.begin(), set.end());
374 #else
375         return QList<QString>::fromSet(set);
376 #endif
377 }
378
379 QString const externalLineEnding(docstring const & str)
380 {
381 #ifdef Q_OS_MAC
382         // The MAC clipboard uses \r for lineendings, and we use \n
383         return toqstr(subst(str, '\n', '\r'));
384 #elif defined(Q_OS_WIN) || defined(Q_CYGWIN_WIN)
385         // Windows clipboard uses \r\n for lineendings, and we use \n
386         return toqstr(subst(str, from_ascii("\n"), from_ascii("\r\n")));
387 #else
388         return toqstr(str);
389 #endif
390 }
391
392
393 docstring const internalLineEnding(QString const & str)
394 {
395         docstring const s = subst(qstring_to_ucs4(str),
396                                   from_ascii("\r\n"), from_ascii("\n"));
397         return subst(s, '\r', '\n');
398 }
399
400
401 QString internalPath(const QString & str)
402 {
403         return toqstr(os::internal_path(fromqstr(str)));
404 }
405
406
407 QString onlyFileName(const QString & str)
408 {
409         return toqstr(support::onlyFileName(fromqstr(str)));
410 }
411
412
413 QString onlyPath(const QString & str)
414 {
415         return toqstr(support::onlyPath(fromqstr(str)));
416 }
417
418
419 QString changeExtension(QString const & oldname, QString const & ext)
420 {
421         return toqstr(support::changeExtension(fromqstr(oldname), fromqstr(ext)));
422 }
423
424 /// Remove the extension from \p name
425 QString removeExtension(QString const & name)
426 {
427         return toqstr(support::removeExtension(fromqstr(name)));
428 }
429
430 /** Add the extension \p ext to \p name.
431  Use this instead of changeExtension if you know that \p name is without
432  extension, because changeExtension would wrongly interpret \p name if it
433  contains a dot.
434  */
435 QString addExtension(QString const & name, QString const & ext)
436 {
437         return toqstr(support::addExtension(fromqstr(name), fromqstr(ext)));
438 }
439
440 /// Return the extension of the file (not including the .)
441 QString getExtension(QString const & name)
442 {
443         return toqstr(support::getExtension(fromqstr(name)));
444 }
445
446
447 /** Convert relative path into absolute path based on a basepath.
448   If relpath is absolute, just use that.
449   If basepath doesn't exist use CWD.
450   */
451 QString makeAbsPath(QString const & relpath, QString const & base)
452 {
453         return toqstr(support::makeAbsPath(fromqstr(relpath),
454                 fromqstr(base)).absFileName());
455 }
456
457
458 /////////////////////////////////////////////////////////////////////////
459 //
460 // FileFilterList
461 //
462 /////////////////////////////////////////////////////////////////////////
463
464 /** Given a string such as
465  *      "<glob> <glob> ... *.{abc,def} <glob>",
466  *  convert the csh-style brace expressions:
467  *      "<glob> <glob> ... *.abc *.def <glob>".
468  *  Requires no system support, so should work equally on Unix, Mac, Win32.
469  */
470 static string const convert_brace_glob(string const & glob)
471 {
472         // Matches " *.{abc,def,ghi}", storing "*." as group 1 and
473         // "abc,def,ghi" as group 2, while allowing spaces in group 2.
474         static lyx::regex const glob_re(" *([^ {]*)\\{([^}]+)\\}");
475         // Matches "abc" and "abc,", storing "abc" as group 1,
476         // while ignoring surrounding spaces.
477         static lyx::regex const block_re(" *([^ ,}]+) *,? *");
478
479         string pattern;
480
481         string::const_iterator it = glob.begin();
482         string::const_iterator const end = glob.end();
483         while (true) {
484                 match_results<string::const_iterator> what;
485                 if (!regex_search(it, end, what, glob_re)) {
486                         // Ensure that no information is lost.
487                         pattern += string(it, end);
488                         break;
489                 }
490
491                 // Everything from the start of the input to
492                 // the start of the match.
493                 pattern += string(what[-1].first, what[-1].second);
494
495                 // Given " *.{abc,def}", head == "*." and tail == "abc,def".
496                 string const head = string(what[1].first, what[1].second);
497                 string const tail = string(what[2].first, what[2].second);
498
499                 // Split the ','-separated chunks of tail so that
500                 // $head{$chunk1,$chunk2} becomes "$head$chunk1 $head$chunk2".
501                 string const fmt = " " + head + "$1";
502                 pattern += regex_replace(tail, block_re, fmt);
503
504                 // Increment the iterator to the end of the match.
505                 it += distance(it, what[0].second);
506         }
507
508         return pattern;
509 }
510
511
512 struct Filter
513 {
514         /* \param description text describing the filters.
515          * \param one or more wildcard patterns, separated by
516          * whitespace.
517          */
518         Filter(docstring const & description, std::string const & globs);
519
520         docstring const & description() const { return desc_; }
521
522         QString toString() const;
523
524         docstring desc_;
525         std::vector<std::string> globs_;
526 };
527
528
529 Filter::Filter(docstring const & description, string const & globs)
530         : desc_(description)
531 {
532         // Given "<glob> <glob> ... *.{abc,def} <glob>", expand to
533         //       "<glob> <glob> ... *.abc *.def <glob>"
534         string const expanded_globs = convert_brace_glob(globs);
535
536         // Split into individual globs.
537         globs_ = getVectorFromString(expanded_globs, " ");
538 }
539
540
541 QString Filter::toString() const
542 {
543         QString s;
544
545         bool const has_description = !desc_.empty();
546
547         if (has_description) {
548                 s += toqstr(desc_);
549                 s += " (";
550         }
551
552         s += toqstr(getStringFromVector(globs_, " "));
553
554         if (has_description)
555                 s += ')';
556         return s;
557 }
558
559
560 /** \c FileFilterList parses a Qt-style list of available file filters
561  *  to generate the corresponding vector.
562  *  For example "TeX documents (*.tex);;LyX Documents (*.lyx)"
563  *  will be parsed to fill a vector of size 2, whilst "*.{p[bgp]m} *.pdf"
564  *  will result in a vector of size 1 in which the description field is empty.
565  */
566 struct FileFilterList
567 {
568         // FIXME UNICODE: globs_ should be unicode...
569         /** \param qt_style_filter a list of available file filters.
570          *  Eg. "TeX documents (*.tex);;LyX Documents (*.lyx)".
571          *  The "All files (*)" filter is always added to the list.
572          */
573         explicit FileFilterList(docstring const & qt_style_filter =
574                                 docstring());
575
576         typedef std::vector<Filter>::size_type size_type;
577
578         bool empty() const { return filters_.empty(); }
579         size_type size() const { return filters_.size(); }
580         Filter & operator[](size_type i) { return filters_[i]; }
581         Filter const & operator[](size_type i) const { return filters_[i]; }
582
583         void parse_filter(std::string const & filter);
584         std::vector<Filter> filters_;
585 };
586
587
588 FileFilterList::FileFilterList(docstring const & qt_style_filter)
589 {
590         // FIXME UNICODE
591         string const filter = to_utf8(qt_style_filter)
592                 + (qt_style_filter.empty() ? string() : ";;")
593                 + to_utf8(_("All Files "))
594 #if defined(_WIN32)
595                 + ("(*.*)");
596 #else
597                 + ("(*)");
598 #endif
599
600         // Split data such as "TeX documents (*.tex);;LyX Documents (*.lyx)"
601         // into individual filters.
602         static lyx::regex const separator_re(";;");
603
604         string::const_iterator it = filter.begin();
605         string::const_iterator const end = filter.end();
606         while (true) {
607                 match_results<string::const_iterator> what;
608
609                 if (!lyx::regex_search(it, end, what, separator_re)) {
610                         parse_filter(string(it, end));
611                         break;
612                 }
613
614                 // Everything from the start of the input to
615                 // the start of the match.
616                 parse_filter(string(it, what[0].first));
617
618                 // Increment the iterator to the end of the match.
619                 it += distance(it, what[0].second);
620         }
621 }
622
623
624 void FileFilterList::parse_filter(string const & filter)
625 {
626         // Matches "TeX documents (plain) (*.tex)",
627         // storing "TeX documents (plain) " as group 1 and "*.tex" as group 2.
628         static lyx::regex const filter_re("(.*)\\(([^()]+)\\) *$");
629
630         match_results<string::const_iterator> what;
631         if (!lyx::regex_search(filter, what, filter_re)) {
632                 // Just a glob, no description.
633                 filters_.push_back(Filter(docstring(), trim(filter)));
634         } else {
635                 // FIXME UNICODE
636                 docstring const desc = from_utf8(string(what[1].first, what[1].second));
637                 string const globs = string(what[2].first, what[2].second);
638                 filters_.push_back(Filter(trim(desc), trim(globs)));
639         }
640 }
641
642
643 /** \returns the equivalent of the string passed in
644  *  although any brace expressions are expanded.
645  *  (E.g. "*.{png,jpg}" -> "*.png *.jpg")
646  */
647 QStringList fileFilters(QString const & desc)
648 {
649         // we have: "*.{gif,png,jpg,bmp,pbm,ppm,tga,tif,xpm,xbm}"
650         // but need:  "*.cpp;*.cc;*.C;*.cxx;*.c++"
651         FileFilterList filters(qstring_to_ucs4(desc));
652         //LYXERR0("DESC: " << desc);
653         QStringList list;
654         for (size_t i = 0; i != filters.filters_.size(); ++i) {
655                 QString f = filters.filters_[i].toString();
656                 //LYXERR0("FILTER: " << f);
657                 list.append(f);
658         }
659         return list;
660 }
661
662
663 QString formatToolTip(QString text, int em)
664 {
665         // 1. QTooltip activates word wrapping only if mightBeRichText()
666         //    is true. So we convert the text to rich text.
667         //
668         // 2. The default width is way too small. Setting the width is tricky; first
669         //    one has to compute the ideal width, and then force it with special
670         //    html markup.
671
672         // do nothing if empty or already formatted
673         if (text.isEmpty() || text.startsWith(QString("<html>")))
674                 return text;
675         // Convert to rich text if it is not already
676         if (!Qt::mightBeRichText(text))
677                 text = Qt::convertFromPlainText(text, Qt::WhiteSpaceNormal);
678         // Compute desired width in pixels
679         QFont const font = QToolTip::font();
680         int const px_width = em * QFontMetrics(font).width("M");
681         // Determine the ideal width of the tooltip
682         QTextDocument td("");
683         td.setHtml(text);
684         td.setDefaultFont(QToolTip::font());
685         td.setDocumentMargin(0);
686         td.setTextWidth(px_width);
687         double best_width = td.idealWidth();
688         // Set the line wrapping with appropriate width
689         return QString("<html><body><table><tr>"
690                        "<td align=justify width=%1>%2</td>"
691                        "</tr></table></body></html>")
692                 .arg(QString::number(int(best_width) + 1), text);
693 }
694
695
696 QString qtHtmlToPlainText(QString const & html)
697 {
698         if (!Qt::mightBeRichText(html))
699                 return html;
700         QTextDocument td;
701         td.setHtml(html);
702         return td.toPlainText();
703 }
704
705
706 } // namespace lyx