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