]> git.lyx.org Git - lyx.git/blob - src/frontends/qt/qt_helpers.cpp
Get rid of Qt4 code in src/
[lyx.git] / src / frontends / qt / qt_helpers.cpp
1 /**
2  * \file qt_helpers.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Dekel Tsur
7  * \author Jürgen Spitzmüller
8  * \author Richard Kimberly Heck
9  *
10  * Full author contact details are available in file CREDITS.
11  */
12
13 #include <config.h>
14
15 #include "qt_helpers.h"
16
17 #include "LengthCombo.h"
18 #include "LyXRC.h"
19
20 #include "frontends/alert.h"
21
22 #include "support/convert.h"
23 #include "support/debug.h"
24 #include "support/gettext.h"
25 #include "support/lstrings.h"
26 #include "support/Package.h"
27 #include "support/PathChanger.h"
28 #include "support/Systemcall.h"
29
30 #include <QApplication>
31 #include <QCheckBox>
32 #include <QComboBox>
33 #include <QDesktopServices>
34 #include <QDir>
35 #include <QLineEdit>
36 #include <QLocale>
37 #include <QPalette>
38 #include <QSet>
39 #include <QTextLayout>
40 #include <QTextDocument>
41 #include <QToolTip>
42 #include <QUrl>
43
44 #include <algorithm>
45 #include <fstream>
46 #include <locale>
47
48 // for FileFilter.
49 // FIXME: Remove
50 #include <regex>
51
52 using namespace std;
53 using namespace lyx::support;
54
55 namespace lyx {
56
57 FileName libFileSearch(QString const & dir, QString const & name,
58                                 QString const & ext, search_mode mode)
59 {
60         return support::libFileSearch(fromqstr(dir), fromqstr(name), fromqstr(ext), mode);
61 }
62
63
64 FileName imageLibFileSearch(QString & dir, QString const & name,
65                                 QString const & ext, search_mode mode)
66 {
67         string tmp = fromqstr(dir);
68         FileName fn = support::imageLibFileSearch(tmp, fromqstr(name), fromqstr(ext), mode);
69         dir = toqstr(tmp);
70         return fn;
71 }
72
73 namespace {
74
75 double locstringToDouble(QString const & str)
76 {
77         QLocale loc;
78         bool ok;
79         double res = loc.toDouble(str, &ok);
80         if (!ok) {
81                 // Fall back to C
82                 QLocale c(QLocale::C);
83                 res = c.toDouble(str);
84         }
85         return res;
86 }
87
88 } // namespace
89
90
91 namespace frontend {
92
93 string widgetsToLength(QLineEdit const * input, LengthCombo const * combo)
94 {
95         QString const length = input->text();
96         if (length.isEmpty())
97                 return string();
98
99         // Don't return unit-from-choice if the input(field) contains a unit
100         if (isValidGlueLength(fromqstr(length)))
101                 return fromqstr(length);
102         // Also try with localized version
103         if (isValidGlueLength(fromqstr(unlocLengthString(length))))
104                 return fromqstr(unlocLengthString(length));
105
106         Length::UNIT const unit = combo->currentLengthItem();
107
108         return Length(locstringToDouble(length.trimmed()), unit).asString();
109 }
110
111
112 Length widgetsToLength(QLineEdit const * input, QComboBox const * combo)
113 {
114         QString const length = input->text();
115         if (length.isEmpty())
116                 return Length();
117
118         // don't return unit-from-choice if the input(field) contains a unit
119         if (isValidGlueLength(fromqstr(length)))
120                 return Length(fromqstr(length));
121         // Also try with localized version
122         if (isValidGlueLength(fromqstr(unlocLengthString(length))))
123                 return Length(fromqstr(unlocLengthString(length)));
124
125         Length::UNIT unit = Length::UNIT_NONE;
126         QString const item = combo->currentText();
127         for (int i = 0; i < num_units; i++) {
128                 if (qt_(lyx::unit_name_gui[i]) == item) {
129                         unit = unitFromString(unit_name[i]);
130                         break;
131                 }
132         }
133
134         return Length(locstringToDouble(length.trimmed()), unit);
135 }
136
137
138 void lengthToWidgets(QLineEdit * input, LengthCombo * combo,
139         Length const & len, Length::UNIT /*defaultUnit*/)
140 {
141         if (len.empty()) {
142                 // no length (UNIT_NONE)
143                 combo->setCurrentItem(Length::defaultUnit());
144                 input->setText("");
145         } else {
146                 combo->setCurrentItem(len.unit());
147                 QLocale loc;
148                 loc.setNumberOptions(QLocale::OmitGroupSeparator);
149                 input->setText(formatLocFPNumber(Length(len).value()));
150         }
151 }
152
153
154 void lengthToWidgets(QLineEdit * input, LengthCombo * combo,
155         string const & len, Length::UNIT defaultUnit)
156 {
157         if (len.empty()) {
158                 // no length (UNIT_NONE)
159                 combo->setCurrentItem(defaultUnit);
160                 input->setText("");
161         } else if (!isValidLength(len) && !isStrDbl(len)) {
162                 // use input field only for gluelengths
163                 combo->setCurrentItem(defaultUnit);
164                 input->setText(locLengthString(toqstr(len)));
165         } else {
166                 lengthToWidgets(input, combo, Length(len), defaultUnit);
167         }
168 }
169
170
171 void lengthToWidgets(QLineEdit * input, LengthCombo * combo,
172         docstring const & len, Length::UNIT defaultUnit)
173 {
174         lengthToWidgets(input, combo, to_utf8(len), defaultUnit);
175 }
176
177
178 double widgetToDouble(QLineEdit const * input)
179 {
180         QString const text = input->text();
181         if (text.isEmpty())
182                 return 0.0;
183
184         return locstringToDouble(text.trimmed());
185 }
186
187
188 string widgetToDoubleStr(QLineEdit const * input)
189 {
190         return convert<string>(widgetToDouble(input));
191 }
192
193
194 void doubleToWidget(QLineEdit * input, double value, char f, int prec)
195 {
196         QLocale loc;
197         loc.setNumberOptions(QLocale::OmitGroupSeparator);
198         input->setText(loc.toString(value, f, prec));
199 }
200
201
202 void doubleToWidget(QLineEdit * input, string const & value, char f, int prec)
203 {
204         doubleToWidget(input, convert<double>(value), f, prec);
205 }
206
207
208 QString formatLocFPNumber(double d)
209 {
210         QString result = toqstr(formatFPNumber(d));
211         QLocale loc;
212         result.replace('.', loc.decimalPoint());
213         return result;
214 }
215
216
217 bool SortLocaleAware(QString const & lhs, QString const & rhs)
218 {
219         return QString::localeAwareCompare(lhs, rhs) < 0;
220 }
221
222
223 bool ColorSorter(ColorCode lhs, ColorCode rhs)
224 {
225         return compare_no_case(lcolor.getGUIName(lhs), lcolor.getGUIName(rhs)) < 0;
226 }
227
228
229 void setValid(QWidget * widget, bool valid)
230 {
231         if (valid) {
232                 if (qobject_cast<QCheckBox*>(widget) != nullptr)
233                         // Check boxes need to be treated differenty, see
234                         // https://forum.qt.io/topic/93253/
235                         widget->setStyleSheet("");
236                 else
237                         widget->setPalette(QPalette());
238         } else {
239                 if (qobject_cast<QCheckBox*>(widget) != nullptr) {
240                         // Check boxes need to be treated differenty, see
241                         // https://forum.qt.io/topic/93253/
242                         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 /// FIXME KILLQT4: kill this function
273 /// wrapper to hide the change of method name to setSectionResizeMode
274 void setSectionResizeMode(QHeaderView * view,
275     int logicalIndex, QHeaderView::ResizeMode mode) {
276         view->setSectionResizeMode(logicalIndex, mode);
277 }
278
279 /// FIXME KILLQT4: kill this function
280 void setSectionResizeMode(QHeaderView * view, QHeaderView::ResizeMode mode) {
281         view->setSectionResizeMode(mode);
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