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