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