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