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