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