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