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