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