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