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