]> git.lyx.org Git - lyx.git/blob - src/frontends/qt/qt_helpers.cpp
Fogotten case for replacing $$python.
[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 viewer = subst(lyxrc.citation_search_view, "$${python}", os::python());
309                 string const command = viewer + " " + opts + tar;
310                 int const result = one.startscript(Systemcall::Wait, command);
311                 if (result == 1)
312                         // Script failed
313                         frontend::Alert::error(_("Could not open file"),
314                                 _("The lyxpaperview script failed."));
315                 else if (result == 2)
316                         frontend::Alert::error(_("Could not open file"),
317                                 bformat(_("No file was found using the pattern `%1$s'."),
318                                         from_utf8(tar)));
319                 return;
320         }
321         if (!QDesktopServices::openUrl(QUrl(toqstr(target), QUrl::TolerantMode)))
322                 frontend::Alert::error(_("Could not open file"),
323                         bformat(_("The target `%1$s' could not be resolved."),
324                                 from_utf8(target)));
325 }
326 } // namespace frontend
327
328 QString const qt_(char const * str, const char *)
329 {
330         return toqstr(_(str));
331 }
332
333
334 QString const qt_(string const & str)
335 {
336         return toqstr(_(str));
337 }
338
339
340 QString const qt_(QString const & qstr)
341 {
342         return toqstr(_(fromqstr(qstr)));
343 }
344
345
346 void rescanTexStyles(string const & arg)
347 {
348         // Run rescan in user lyx directory
349         PathChanger p(package().user_support());
350         FileName const prog = support::libFileSearch("scripts", "TeXFiles.py");
351         Systemcall one;
352         string const command = os::python() + ' ' +
353             quoteName(prog.toFilesystemEncoding()) + ' ' +
354             arg;
355         int const status = one.startscript(Systemcall::Wait, command);
356         if (status == 0)
357                 return;
358         // FIXME UNICODE
359         frontend::Alert::error(_("Could not update TeX information"),
360                 bformat(_("The script `%1$s' failed."), from_utf8(prog.absFileName())));
361 }
362
363
364 QStringList texFileList(QString const & filename)
365 {
366         QStringList list;
367         FileName const file = libFileSearch(QString(), filename);
368         if (file.empty())
369                 return list;
370
371         // FIXME Unicode.
372         vector<docstring> doclist =
373                 getVectorFromString(file.fileContents("UTF-8"), from_ascii("\n"));
374
375         // Normalise paths like /foo//bar ==> /foo/bar
376         QSet<QString> set;
377         for (size_t i = 0; i != doclist.size(); ++i) {
378                 QString qfile = toqstr(doclist[i]);
379                 qfile.replace("\r", "");
380                 while (qfile.contains("//"))
381                         qfile.replace("//", "/");
382                 if (!qfile.isEmpty())
383                         set.insert(qfile);
384         }
385
386         // remove duplicates
387 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
388         return QList<QString>(set.begin(), set.end());
389 #else
390         return QList<QString>::fromSet(set);
391 #endif
392 }
393
394 QString const externalLineEnding(docstring const & str)
395 {
396 #ifdef Q_OS_MAC
397         // The MAC clipboard uses \r for lineendings, and we use \n
398         return toqstr(subst(str, '\n', '\r'));
399 #elif defined(Q_OS_WIN) || defined(Q_CYGWIN_WIN)
400         // Windows clipboard uses \r\n for lineendings, and we use \n
401         return toqstr(subst(str, from_ascii("\n"), from_ascii("\r\n")));
402 #else
403         return toqstr(str);
404 #endif
405 }
406
407
408 docstring const internalLineEnding(QString const & str)
409 {
410         docstring const s = subst(qstring_to_ucs4(str),
411                                   from_ascii("\r\n"), from_ascii("\n"));
412         return subst(s, '\r', '\n');
413 }
414
415
416 QString internalPath(const QString & str)
417 {
418         return toqstr(os::internal_path(fromqstr(str)));
419 }
420
421
422 QString onlyFileName(const QString & str)
423 {
424         return toqstr(support::onlyFileName(fromqstr(str)));
425 }
426
427
428 QString onlyPath(const QString & str)
429 {
430         return toqstr(support::onlyPath(fromqstr(str)));
431 }
432
433
434 QString changeExtension(QString const & oldname, QString const & ext)
435 {
436         return toqstr(support::changeExtension(fromqstr(oldname), fromqstr(ext)));
437 }
438
439 /// Remove the extension from \p name
440 QString removeExtension(QString const & name)
441 {
442         return toqstr(support::removeExtension(fromqstr(name)));
443 }
444
445 /** Add the extension \p ext to \p name.
446  Use this instead of changeExtension if you know that \p name is without
447  extension, because changeExtension would wrongly interpret \p name if it
448  contains a dot.
449  */
450 QString addExtension(QString const & name, QString const & ext)
451 {
452         return toqstr(support::addExtension(fromqstr(name), fromqstr(ext)));
453 }
454
455 /// Return the extension of the file (not including the .)
456 QString getExtension(QString const & name)
457 {
458         return toqstr(support::getExtension(fromqstr(name)));
459 }
460
461
462 /** Convert relative path into absolute path based on a basepath.
463   If relpath is absolute, just use that.
464   If basepath doesn't exist use CWD.
465   */
466 QString makeAbsPath(QString const & relpath, QString const & base)
467 {
468         return toqstr(support::makeAbsPath(fromqstr(relpath),
469                 fromqstr(base)).absFileName());
470 }
471
472
473 /////////////////////////////////////////////////////////////////////////
474 //
475 // FileFilterList
476 //
477 /////////////////////////////////////////////////////////////////////////
478
479 /** Given a string such as
480  *      "<glob> <glob> ... *.{abc,def} <glob>",
481  *  convert the csh-style brace expressions:
482  *      "<glob> <glob> ... *.abc *.def <glob>".
483  *  Requires no system support, so should work equally on Unix, Mac, Win32.
484  */
485 static string const convert_brace_glob(string const & glob)
486 {
487         // Matches " *.{abc,def,ghi}", storing "*." as group 1 and
488         // "abc,def,ghi" as group 2, while allowing spaces in group 2.
489         static regex const glob_re(" *([^ {]*)\\{([^}]+)\\}");
490         // Matches "abc" and "abc,", storing "abc" as group 1,
491         // while ignoring surrounding spaces.
492         static regex const block_re(" *([^ ,}]+) *,? *");
493
494         string pattern;
495
496         string::const_iterator it = glob.begin();
497         string::const_iterator const end = glob.end();
498         while (true) {
499                 match_results<string::const_iterator> what;
500                 if (!regex_search(it, end, what, glob_re)) {
501                         // Ensure that no information is lost.
502                         pattern += string(it, end);
503                         break;
504                 }
505
506                 // Everything from the start of the input to
507                 // the start of the match.
508                 pattern += string(what[-1].first, what[-1].second);
509
510                 // Given " *.{abc,def}", head == "*." and tail == "abc,def".
511                 string const head = string(what[1].first, what[1].second);
512                 string const tail = string(what[2].first, what[2].second);
513
514                 // Split the ','-separated chunks of tail so that
515                 // $head{$chunk1,$chunk2} becomes "$head$chunk1 $head$chunk2".
516                 string const fmt = " " + head + "$1";
517                 pattern += regex_replace(tail, block_re, fmt);
518
519                 // Increment the iterator to the end of the match.
520                 it += distance(it, what[0].second);
521         }
522
523         return pattern;
524 }
525
526
527 struct Filter
528 {
529         /* \param description text describing the filters.
530          * \param one or more wildcard patterns, separated by
531          * whitespace.
532          */
533         Filter(docstring const & description, std::string const & globs);
534
535         docstring const & description() const { return desc_; }
536
537         QString toString() const;
538
539         docstring desc_;
540         std::vector<std::string> globs_;
541 };
542
543
544 Filter::Filter(docstring const & description, string const & globs)
545         : desc_(description)
546 {
547         // Given "<glob> <glob> ... *.{abc,def} <glob>", expand to
548         //       "<glob> <glob> ... *.abc *.def <glob>"
549         string const expanded_globs = convert_brace_glob(globs);
550
551         // Split into individual globs.
552         globs_ = getVectorFromString(expanded_globs, " ");
553 }
554
555
556 QString Filter::toString() const
557 {
558         QString s;
559
560         bool const has_description = !desc_.empty();
561
562         if (has_description) {
563                 s += toqstr(desc_);
564                 s += " (";
565         }
566
567         s += toqstr(getStringFromVector(globs_, " "));
568
569         if (has_description)
570                 s += ')';
571         return s;
572 }
573
574
575 /** \c FileFilterList parses a Qt-style list of available file filters
576  *  to generate the corresponding vector.
577  *  For example "TeX documents (*.tex);;LyX Documents (*.lyx)"
578  *  will be parsed to fill a vector of size 2, whilst "*.{p[bgp]m} *.pdf"
579  *  will result in a vector of size 1 in which the description field is empty.
580  */
581 struct FileFilterList
582 {
583         // FIXME UNICODE: globs_ should be unicode...
584         /** \param qt_style_filter a list of available file filters.
585          *  Eg. "TeX documents (*.tex);;LyX Documents (*.lyx)".
586          *  The "All files (*)" filter is always added to the list.
587          */
588         explicit FileFilterList(docstring const & qt_style_filter =
589                                 docstring());
590
591         typedef std::vector<Filter>::size_type size_type;
592
593         bool empty() const { return filters_.empty(); }
594         size_type size() const { return filters_.size(); }
595         Filter & operator[](size_type i) { return filters_[i]; }
596         Filter const & operator[](size_type i) const { return filters_[i]; }
597
598         void parse_filter(std::string const & filter);
599         std::vector<Filter> filters_;
600 };
601
602
603 FileFilterList::FileFilterList(docstring const & qt_style_filter)
604 {
605         // FIXME UNICODE
606         string const filter = to_utf8(qt_style_filter)
607                 + (qt_style_filter.empty() ? string() : ";;")
608                 + to_utf8(_("All Files "))
609 #if defined(_WIN32)
610                 + ("(*.*)");
611 #else
612                 + ("(*)");
613 #endif
614
615         // Split data such as "TeX documents (*.tex);;LyX Documents (*.lyx)"
616         // into individual filters.
617         static regex const separator_re(";;");
618
619         string::const_iterator it = filter.begin();
620         string::const_iterator const end = filter.end();
621         while (true) {
622                 match_results<string::const_iterator> what;
623
624                 if (!regex_search(it, end, what, separator_re)) {
625                         parse_filter(string(it, end));
626                         break;
627                 }
628
629                 // Everything from the start of the input to
630                 // the start of the match.
631                 parse_filter(string(it, what[0].first));
632
633                 // Increment the iterator to the end of the match.
634                 it += distance(it, what[0].second);
635         }
636 }
637
638
639 void FileFilterList::parse_filter(string const & filter)
640 {
641         // Matches "TeX documents (plain) (*.tex)",
642         // storing "TeX documents (plain) " as group 1 and "*.tex" as group 2.
643         static regex const filter_re("(.*)\\(([^()]+)\\) *$");
644
645         match_results<string::const_iterator> what;
646         if (!regex_search(filter, what, filter_re)) {
647                 // Just a glob, no description.
648                 filters_.push_back(Filter(docstring(), trim(filter)));
649         } else {
650                 // FIXME UNICODE
651                 docstring const desc = from_utf8(string(what[1].first, what[1].second));
652                 string const globs = string(what[2].first, what[2].second);
653                 filters_.push_back(Filter(trim(desc), trim(globs)));
654         }
655 }
656
657
658 /** \returns the equivalent of the string passed in
659  *  although any brace expressions are expanded.
660  *  (E.g. "*.{png,jpg}" -> "*.png *.jpg")
661  */
662 QStringList fileFilters(QString const & desc)
663 {
664         // we have: "*.{gif,png,jpg,bmp,pbm,ppm,tga,tif,xpm,xbm}"
665         // but need:  "*.cpp;*.cc;*.C;*.cxx;*.c++"
666         FileFilterList filters(qstring_to_ucs4(desc));
667         //LYXERR0("DESC: " << desc);
668         QStringList list;
669         for (size_t i = 0; i != filters.filters_.size(); ++i) {
670                 QString f = filters.filters_[i].toString();
671                 //LYXERR0("FILTER: " << f);
672                 list.append(f);
673         }
674         return list;
675 }
676
677
678 QString formatToolTip(QString text, int width)
679 {
680         // 1. QTooltip activates word wrapping only if mightBeRichText()
681         //    is true. So we convert the text to rich text.
682         //
683         // 2. The default width is way too small. Setting the width is tricky; first
684         //    one has to compute the ideal width, and then force it with special
685         //    html markup.
686
687         // do nothing if empty or already formatted
688         if (text.isEmpty() || text.startsWith(QString("<html>")))
689                 return text;
690         // Convert to rich text if it is not already
691         if (!Qt::mightBeRichText(text))
692                 text = Qt::convertFromPlainText(text, Qt::WhiteSpaceNormal);
693         // Compute desired width in pixels
694         QFont const font = QToolTip::font();
695 #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
696         int const px_width = width * QFontMetrics(font).horizontalAdvance("M");
697 #else
698         int const px_width = width * QFontMetrics(font).width("M");
699 #endif
700         // Determine the ideal width of the tooltip
701         QTextDocument td("");
702         td.setHtml(text);
703         td.setDefaultFont(QToolTip::font());
704         td.setDocumentMargin(0);
705         td.setTextWidth(px_width);
706         double best_width = td.idealWidth();
707         // Set the line wrapping with appropriate width
708         return QString("<html><body><table><tr>"
709                        "<td align=justify width=%1>%2</td>"
710                        "</tr></table></body></html>")
711                 .arg(QString::number(int(best_width) + 1), text);
712 }
713
714
715 QString qtHtmlToPlainText(QString const & text)
716 {
717         if (!Qt::mightBeRichText(text))
718                 return text;
719         QTextDocument td;
720         td.setHtml(text);
721         return td.toPlainText();
722 }
723
724
725 } // namespace lyx