]> git.lyx.org Git - features.git/blob - src/frontends/qt/qt_helpers.cpp
108d613042a2cf67f24f4acb37fcde73dfa807bc
[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 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 "FileDialog.h"
18 #include "LengthCombo.h"
19
20 #include "frontends/alert.h"
21
22 #include "BufferParams.h"
23 #include "FloatList.h"
24 #include "FuncRequest.h"
25 #include "Language.h"
26 #include "LyX.h"
27 #include "LyXRC.h"
28 #include "LyXAction.h"
29 #include "TextClass.h"
30
31 #include "support/convert.h"
32 #include "support/debug.h"
33 #include "support/gettext.h"
34 #include "support/Length.h"
35 #include "support/lstrings.h"
36 #include "support/os.h"
37 #include "support/Package.h"
38 #include "support/PathChanger.h"
39 #include "support/Systemcall.h"
40
41 #include <QApplication>
42 #include <QCheckBox>
43 #include <QComboBox>
44 #include <QDesktopServices>
45 #include <QDir>
46 #include <QLineEdit>
47 #include <QLocale>
48 #include <QPalette>
49 #include <QSet>
50 #include <QTextLayout>
51 #include <QTextDocument>
52 #include <QToolTip>
53 #include <QUrl>
54
55 #include <algorithm>
56 #include <fstream>
57 #include <locale>
58
59 // for FileFilter.
60 // FIXME: Remove
61 #include "support/regex.h"
62
63
64 using namespace std;
65 using namespace lyx::support;
66
67 namespace lyx {
68
69 FileName libFileSearch(QString const & dir, QString const & name,
70                                 QString const & ext, search_mode mode)
71 {
72         return support::libFileSearch(fromqstr(dir), fromqstr(name), fromqstr(ext), mode);
73 }
74
75
76 FileName imageLibFileSearch(QString & dir, QString const & name,
77                                 QString const & ext, search_mode mode)
78 {
79         string tmp = fromqstr(dir);
80         FileName fn = support::imageLibFileSearch(tmp, fromqstr(name), fromqstr(ext), mode);
81         dir = toqstr(tmp);
82         return fn;
83 }
84
85 namespace {
86
87 double locstringToDouble(QString const & str)
88 {
89         QLocale loc;
90         bool ok;
91         double res = loc.toDouble(str, &ok);
92         if (!ok) {
93                 // Fall back to C
94                 QLocale c(QLocale::C);
95                 res = c.toDouble(str);
96         }
97         return res;
98 }
99
100 } // namespace
101
102
103 namespace frontend {
104
105 string widgetsToLength(QLineEdit const * input, LengthCombo const * combo)
106 {
107         QString const length = input->text();
108         if (length.isEmpty())
109                 return string();
110
111         // Don't return unit-from-choice if the input(field) contains a unit
112         if (isValidGlueLength(fromqstr(length)))
113                 return fromqstr(length);
114
115         Length::UNIT const unit = combo->currentLengthItem();
116
117         return Length(locstringToDouble(length.trimmed()), unit).asString();
118 }
119
120
121 Length widgetsToLength(QLineEdit const * input, QComboBox const * combo)
122 {
123         QString const length = input->text();
124         if (length.isEmpty())
125                 return Length();
126
127         // don't return unit-from-choice if the input(field) contains a unit
128         if (isValidGlueLength(fromqstr(length)))
129                 return Length(fromqstr(length));
130
131         Length::UNIT unit = Length::UNIT_NONE;
132         QString const item = combo->currentText();
133         for (int i = 0; i < num_units; i++) {
134                 if (qt_(lyx::unit_name_gui[i]) == item) {
135                         unit = unitFromString(unit_name[i]);
136                         break;
137                 }
138         }
139
140         return Length(locstringToDouble(length.trimmed()), unit);
141 }
142
143
144 void lengthToWidgets(QLineEdit * input, LengthCombo * combo,
145         Length const & len, Length::UNIT /*defaultUnit*/)
146 {
147         if (len.empty()) {
148                 // no length (UNIT_NONE)
149                 combo->setCurrentItem(Length::defaultUnit());
150                 input->setText("");
151         } else {
152                 combo->setCurrentItem(len.unit());
153                 QLocale loc;
154                 loc.setNumberOptions(QLocale::OmitGroupSeparator);
155                 input->setText(formatLocFPNumber(Length(len).value()));
156         }
157 }
158
159
160 void lengthToWidgets(QLineEdit * input, LengthCombo * combo,
161         string const & len, Length::UNIT defaultUnit)
162 {
163         if (len.empty()) {
164                 // no length (UNIT_NONE)
165                 combo->setCurrentItem(defaultUnit);
166                 input->setText("");
167         } else if (!isValidLength(len) && !isStrDbl(len)) {
168                 // use input field only for gluelengths
169                 combo->setCurrentItem(defaultUnit);
170                 input->setText(toqstr(len));
171         } else {
172                 lengthToWidgets(input, combo, Length(len), defaultUnit);
173         }
174 }
175
176
177 void lengthToWidgets(QLineEdit * input, LengthCombo * combo,
178         docstring const & len, Length::UNIT defaultUnit)
179 {
180         lengthToWidgets(input, combo, to_utf8(len), defaultUnit);
181 }
182
183
184 double widgetToDouble(QLineEdit const * input)
185 {
186         QString const text = input->text();
187         if (text.isEmpty())
188                 return 0.0;
189
190         return locstringToDouble(text.trimmed());
191 }
192
193
194 string widgetToDoubleStr(QLineEdit const * input)
195 {
196         return convert<string>(widgetToDouble(input));
197 }
198
199
200 void doubleToWidget(QLineEdit * input, double value, char f, int prec)
201 {
202         QLocale loc;
203         loc.setNumberOptions(QLocale::OmitGroupSeparator);
204         input->setText(loc.toString(value, f, prec));
205 }
206
207
208 void doubleToWidget(QLineEdit * input, string const & value, char f, int prec)
209 {
210         doubleToWidget(input, convert<double>(value), f, prec);
211 }
212
213
214 QString formatLocFPNumber(double d)
215 {
216         QString result = toqstr(formatFPNumber(d));
217         QLocale loc;
218         result.replace('.', loc.decimalPoint());
219         return result;
220 }
221
222
223 bool SortLocaleAware(QString const & lhs, QString const & rhs)
224 {
225         return QString::localeAwareCompare(lhs, rhs) < 0;
226 }
227
228
229 bool ColorSorter(ColorCode lhs, ColorCode rhs)
230 {
231         return compare_no_case(lcolor.getGUIName(lhs), lcolor.getGUIName(rhs)) < 0;
232 }
233
234
235 void setValid(QWidget * widget, bool valid)
236 {
237         if (valid) {
238                 widget->setPalette(QPalette());
239         } else {
240                 QPalette pal = widget->palette();
241                 pal.setColor(QPalette::Active, QPalette::WindowText, QColor(255, 0, 0));
242                 widget->setPalette(pal);
243         }
244 }
245
246
247 void focusAndHighlight(QAbstractItemView * w)
248 {
249         w->setFocus();
250         w->setCurrentIndex(w->currentIndex());
251         w->scrollTo(w->currentIndex());
252 }
253
254
255 void setMessageColour(list<QWidget *> highlighted, list<QWidget *> plain)
256 {
257         QPalette pal = QApplication::palette();
258         QPalette newpal(pal.color(QPalette::Active, QPalette::HighlightedText),
259                         pal.color(QPalette::Active, QPalette::Highlight));
260         for (QWidget * w : highlighted)
261                 w->setPalette(newpal);
262         for (QWidget * w : plain)
263                 w->setPalette(pal);
264 }
265
266
267 /// wrapper to hide the change of method name to setSectionResizeMode
268 void setSectionResizeMode(QHeaderView * view,
269     int logicalIndex, QHeaderView::ResizeMode mode) {
270 #if (QT_VERSION >= 0x050000)
271         view->setSectionResizeMode(logicalIndex, mode);
272 #else
273         view->setResizeMode(logicalIndex, mode);
274 #endif
275 }
276
277 void setSectionResizeMode(QHeaderView * view, QHeaderView::ResizeMode mode) {
278 #if (QT_VERSION >= 0x050000)
279         view->setSectionResizeMode(mode);
280 #else
281         view->setResizeMode(mode);
282 #endif
283 }
284
285 void showDirectory(FileName const & directory)
286 {
287         if (!directory.exists())
288                 return;
289         QUrl qurl(QUrl::fromLocalFile(QDir::toNativeSeparators(toqstr(directory.absFileName()))));
290         // Give hints in case of bugs
291         if (!qurl.isValid()) {
292                 frontend::Alert::error(_("Invalid URL"),
293                         bformat(_("The URL `%1$s' could not be resolved."),
294                                 qstring_to_ucs4(qurl.toString())));
295                 return;
296
297         }
298         if (!QDesktopServices::openUrl(qurl))
299                 frontend::Alert::error(_("URL could not be accessed"),
300                         bformat(_("The URL `%1$s' could not be opened although it exists!"),
301                                 qstring_to_ucs4(qurl.toString())));
302 }
303
304 void showTarget(string const & target, string const & pdfv, string const & psv)
305 {
306         LYXERR(Debug::INSETS, "Showtarget:" << target << "\n");
307         if (prefixIs(target, "EXTERNAL ")) {
308                 if (!lyxrc.citation_search)
309                         return;
310                 string tmp, tar, opts;
311                 tar = split(target, tmp, ' ');
312                 if (!pdfv.empty())
313                         opts = " -v " + pdfv;
314                 if (!psv.empty())
315                         opts += " -w " + psv;
316                 if (!opts.empty())
317                         opts += " ";
318                 Systemcall one;
319                 string const command = lyxrc.citation_search_view + " " + opts + tar;
320                 int const result = one.startscript(Systemcall::Wait, command);
321                 if (result == 1)
322                         // Script failed
323                         frontend::Alert::error(_("Could not open file"),
324                                 _("The lyxpaperview script failed."));
325                 else if (result == 2)
326                         frontend::Alert::error(_("Could not open file"),
327                                 bformat(_("No file was found using the pattern `%1$s'."),
328                                         from_utf8(tar)));
329                 return;
330         }
331         if (!QDesktopServices::openUrl(QUrl(toqstr(target), QUrl::TolerantMode)))
332                 frontend::Alert::error(_("Could not open file"),
333                         bformat(_("The target `%1$s' could not be resolved."),
334                                 from_utf8(target)));
335 }
336 } // namespace frontend
337
338 QString const qt_(char const * str, const char *)
339 {
340         return toqstr(_(str));
341 }
342
343
344 QString const qt_(string const & str)
345 {
346         return toqstr(_(str));
347 }
348
349
350 QString const qt_(QString const & qstr)
351 {
352         return toqstr(_(fromqstr(qstr)));
353 }
354
355
356 void rescanTexStyles(string const & arg)
357 {
358         // Run rescan in user lyx directory
359         PathChanger p(package().user_support());
360         FileName const prog = support::libFileSearch("scripts", "TeXFiles.py");
361         Systemcall one;
362         string const command = os::python() + ' ' +
363             quoteName(prog.toFilesystemEncoding()) + ' ' +
364             arg;
365         int const status = one.startscript(Systemcall::Wait, command);
366         if (status == 0)
367                 return;
368         // FIXME UNICODE
369         frontend::Alert::error(_("Could not update TeX information"),
370                 bformat(_("The script `%1$s' failed."), from_utf8(prog.absFileName())));
371 }
372
373
374 QStringList texFileList(QString const & filename)
375 {
376         QStringList list;
377         FileName const file = libFileSearch(QString(), filename);
378         if (file.empty())
379                 return list;
380
381         // FIXME Unicode.
382         vector<docstring> doclist =
383                 getVectorFromString(file.fileContents("UTF-8"), from_ascii("\n"));
384
385         // Normalise paths like /foo//bar ==> /foo/bar
386         QSet<QString> set;
387         for (size_t i = 0; i != doclist.size(); ++i) {
388                 QString qfile = toqstr(doclist[i]);
389                 qfile.replace("\r", "");
390                 while (qfile.contains("//"))
391                         qfile.replace("//", "/");
392                 if (!qfile.isEmpty())
393                         set.insert(qfile);
394         }
395
396         // remove duplicates
397 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
398         return QList<QString>(set.begin(), set.end());
399 #else
400         return QList<QString>::fromSet(set);
401 #endif
402 }
403
404 QString const externalLineEnding(docstring const & str)
405 {
406 #ifdef Q_OS_MAC
407         // The MAC clipboard uses \r for lineendings, and we use \n
408         return toqstr(subst(str, '\n', '\r'));
409 #elif defined(Q_OS_WIN) || defined(Q_CYGWIN_WIN)
410         // Windows clipboard uses \r\n for lineendings, and we use \n
411         return toqstr(subst(str, from_ascii("\n"), from_ascii("\r\n")));
412 #else
413         return toqstr(str);
414 #endif
415 }
416
417
418 docstring const internalLineEnding(QString const & str)
419 {
420         docstring const s = subst(qstring_to_ucs4(str),
421                                   from_ascii("\r\n"), from_ascii("\n"));
422         return subst(s, '\r', '\n');
423 }
424
425
426 QString internalPath(const QString & str)
427 {
428         return toqstr(os::internal_path(fromqstr(str)));
429 }
430
431
432 QString onlyFileName(const QString & str)
433 {
434         return toqstr(support::onlyFileName(fromqstr(str)));
435 }
436
437
438 QString onlyPath(const QString & str)
439 {
440         return toqstr(support::onlyPath(fromqstr(str)));
441 }
442
443
444 QString changeExtension(QString const & oldname, QString const & ext)
445 {
446         return toqstr(support::changeExtension(fromqstr(oldname), fromqstr(ext)));
447 }
448
449 /// Remove the extension from \p name
450 QString removeExtension(QString const & name)
451 {
452         return toqstr(support::removeExtension(fromqstr(name)));
453 }
454
455 /** Add the extension \p ext to \p name.
456  Use this instead of changeExtension if you know that \p name is without
457  extension, because changeExtension would wrongly interpret \p name if it
458  contains a dot.
459  */
460 QString addExtension(QString const & name, QString const & ext)
461 {
462         return toqstr(support::addExtension(fromqstr(name), fromqstr(ext)));
463 }
464
465 /// Return the extension of the file (not including the .)
466 QString getExtension(QString const & name)
467 {
468         return toqstr(support::getExtension(fromqstr(name)));
469 }
470
471
472 /** Convert relative path into absolute path based on a basepath.
473   If relpath is absolute, just use that.
474   If basepath doesn't exist use CWD.
475   */
476 QString makeAbsPath(QString const & relpath, QString const & base)
477 {
478         return toqstr(support::makeAbsPath(fromqstr(relpath),
479                 fromqstr(base)).absFileName());
480 }
481
482
483 /////////////////////////////////////////////////////////////////////////
484 //
485 // FileFilterList
486 //
487 /////////////////////////////////////////////////////////////////////////
488
489 /** Given a string such as
490  *      "<glob> <glob> ... *.{abc,def} <glob>",
491  *  convert the csh-style brace expressions:
492  *      "<glob> <glob> ... *.abc *.def <glob>".
493  *  Requires no system support, so should work equally on Unix, Mac, Win32.
494  */
495 static string const convert_brace_glob(string const & glob)
496 {
497         // Matches " *.{abc,def,ghi}", storing "*." as group 1 and
498         // "abc,def,ghi" as group 2, while allowing spaces in group 2.
499         static lyx::regex const glob_re(" *([^ {]*)\\{([^}]+)\\}");
500         // Matches "abc" and "abc,", storing "abc" as group 1,
501         // while ignoring surrounding spaces.
502         static lyx::regex const block_re(" *([^ ,}]+) *,? *");
503
504         string pattern;
505
506         string::const_iterator it = glob.begin();
507         string::const_iterator const end = glob.end();
508         while (true) {
509                 match_results<string::const_iterator> what;
510                 if (!regex_search(it, end, what, glob_re)) {
511                         // Ensure that no information is lost.
512                         pattern += string(it, end);
513                         break;
514                 }
515
516                 // Everything from the start of the input to
517                 // the start of the match.
518                 pattern += string(what[-1].first, what[-1].second);
519
520                 // Given " *.{abc,def}", head == "*." and tail == "abc,def".
521                 string const head = string(what[1].first, what[1].second);
522                 string const tail = string(what[2].first, what[2].second);
523
524                 // Split the ','-separated chunks of tail so that
525                 // $head{$chunk1,$chunk2} becomes "$head$chunk1 $head$chunk2".
526                 string const fmt = " " + head + "$1";
527                 pattern += regex_replace(tail, block_re, fmt);
528
529                 // Increment the iterator to the end of the match.
530                 it += distance(it, what[0].second);
531         }
532
533         return pattern;
534 }
535
536
537 struct Filter
538 {
539         /* \param description text describing the filters.
540          * \param one or more wildcard patterns, separated by
541          * whitespace.
542          */
543         Filter(docstring const & description, std::string const & globs);
544
545         docstring const & description() const { return desc_; }
546
547         QString toString() const;
548
549         docstring desc_;
550         std::vector<std::string> globs_;
551 };
552
553
554 Filter::Filter(docstring const & description, string const & globs)
555         : desc_(description)
556 {
557         // Given "<glob> <glob> ... *.{abc,def} <glob>", expand to
558         //       "<glob> <glob> ... *.abc *.def <glob>"
559         string const expanded_globs = convert_brace_glob(globs);
560
561         // Split into individual globs.
562         globs_ = getVectorFromString(expanded_globs, " ");
563 }
564
565
566 QString Filter::toString() const
567 {
568         QString s;
569
570         bool const has_description = !desc_.empty();
571
572         if (has_description) {
573                 s += toqstr(desc_);
574                 s += " (";
575         }
576
577         s += toqstr(getStringFromVector(globs_, " "));
578
579         if (has_description)
580                 s += ')';
581         return s;
582 }
583
584
585 /** \c FileFilterList parses a Qt-style list of available file filters
586  *  to generate the corresponding vector.
587  *  For example "TeX documents (*.tex);;LyX Documents (*.lyx)"
588  *  will be parsed to fill a vector of size 2, whilst "*.{p[bgp]m} *.pdf"
589  *  will result in a vector of size 1 in which the description field is empty.
590  */
591 struct FileFilterList
592 {
593         // FIXME UNICODE: globs_ should be unicode...
594         /** \param qt_style_filter a list of available file filters.
595          *  Eg. "TeX documents (*.tex);;LyX Documents (*.lyx)".
596          *  The "All files (*)" filter is always added to the list.
597          */
598         explicit FileFilterList(docstring const & qt_style_filter =
599                                 docstring());
600
601         typedef std::vector<Filter>::size_type size_type;
602
603         bool empty() const { return filters_.empty(); }
604         size_type size() const { return filters_.size(); }
605         Filter & operator[](size_type i) { return filters_[i]; }
606         Filter const & operator[](size_type i) const { return filters_[i]; }
607
608         void parse_filter(std::string const & filter);
609         std::vector<Filter> filters_;
610 };
611
612
613 FileFilterList::FileFilterList(docstring const & qt_style_filter)
614 {
615         // FIXME UNICODE
616         string const filter = to_utf8(qt_style_filter)
617                 + (qt_style_filter.empty() ? string() : ";;")
618                 + to_utf8(_("All Files "))
619 #if defined(_WIN32)
620                 + ("(*.*)");
621 #else
622                 + ("(*)");
623 #endif
624
625         // Split data such as "TeX documents (*.tex);;LyX Documents (*.lyx)"
626         // into individual filters.
627         static lyx::regex const separator_re(";;");
628
629         string::const_iterator it = filter.begin();
630         string::const_iterator const end = filter.end();
631         while (true) {
632                 match_results<string::const_iterator> what;
633
634                 if (!lyx::regex_search(it, end, what, separator_re)) {
635                         parse_filter(string(it, end));
636                         break;
637                 }
638
639                 // Everything from the start of the input to
640                 // the start of the match.
641                 parse_filter(string(it, what[0].first));
642
643                 // Increment the iterator to the end of the match.
644                 it += distance(it, what[0].second);
645         }
646 }
647
648
649 void FileFilterList::parse_filter(string const & filter)
650 {
651         // Matches "TeX documents (plain) (*.tex)",
652         // storing "TeX documents (plain) " as group 1 and "*.tex" as group 2.
653         static lyx::regex const filter_re("(.*)\\(([^()]+)\\) *$");
654
655         match_results<string::const_iterator> what;
656         if (!lyx::regex_search(filter, what, filter_re)) {
657                 // Just a glob, no description.
658                 filters_.push_back(Filter(docstring(), trim(filter)));
659         } else {
660                 // FIXME UNICODE
661                 docstring const desc = from_utf8(string(what[1].first, what[1].second));
662                 string const globs = string(what[2].first, what[2].second);
663                 filters_.push_back(Filter(trim(desc), trim(globs)));
664         }
665 }
666
667
668 /** \returns the equivalent of the string passed in
669  *  although any brace expressions are expanded.
670  *  (E.g. "*.{png,jpg}" -> "*.png *.jpg")
671  */
672 QStringList fileFilters(QString const & desc)
673 {
674         // we have: "*.{gif,png,jpg,bmp,pbm,ppm,tga,tif,xpm,xbm}"
675         // but need:  "*.cpp;*.cc;*.C;*.cxx;*.c++"
676         FileFilterList filters(qstring_to_ucs4(desc));
677         //LYXERR0("DESC: " << desc);
678         QStringList list;
679         for (size_t i = 0; i != filters.filters_.size(); ++i) {
680                 QString f = filters.filters_[i].toString();
681                 //LYXERR0("FILTER: " << f);
682                 list.append(f);
683         }
684         return list;
685 }
686
687
688 QString formatToolTip(QString text, int width)
689 {
690         // 1. QTooltip activates word wrapping only if mightBeRichText()
691         //    is true. So we convert the text to rich text.
692         //
693         // 2. The default width is way too small. Setting the width is tricky; first
694         //    one has to compute the ideal width, and then force it with special
695         //    html markup.
696
697         // do nothing if empty or already formatted
698         if (text.isEmpty() || text.startsWith(QString("<html>")))
699                 return text;
700         // Convert to rich text if it is not already
701         if (!Qt::mightBeRichText(text))
702                 text = Qt::convertFromPlainText(text, Qt::WhiteSpaceNormal);
703         // Compute desired width in pixels
704         QFont const font = QToolTip::font();
705 #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
706         int const px_width = width * QFontMetrics(font).horizontalAdvance("M");
707 #else
708         int const px_width = width * QFontMetrics(font).width("M");
709 #endif
710         // Determine the ideal width of the tooltip
711         QTextDocument td("");
712         td.setHtml(text);
713         td.setDefaultFont(QToolTip::font());
714         td.setDocumentMargin(0);
715         td.setTextWidth(px_width);
716         double best_width = td.idealWidth();
717         // Set the line wrapping with appropriate width
718         return QString("<html><body><table><tr>"
719                        "<td align=justify width=%1>%2</td>"
720                        "</tr></table></body></html>")
721                 .arg(QString::number(int(best_width) + 1), text);
722 }
723
724
725 QString qtHtmlToPlainText(QString const & text)
726 {
727         if (!Qt::mightBeRichText(text))
728                 return text;
729         QTextDocument td;
730         td.setHtml(text);
731         return td.toPlainText();
732 }
733
734
735 } // namespace lyx