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