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