3 * This file is part of LyX, the document processor.
4 * Licence details can be found in the file COPYING.
7 * \author Jürgen Spitzmüller
8 * \author Richard Kimberly Heck
10 * Full author contact details are available in file CREDITS.
15 #include "qt_helpers.h"
18 #include "LengthCombo.h"
21 #include "frontends/alert.h"
23 #include "support/convert.h"
24 #include "support/debug.h"
25 #include "support/gettext.h"
26 #include "support/lstrings.h"
27 #include "support/Package.h"
28 #include "support/PathChanger.h"
29 #include "support/Systemcall.h"
31 #include <QApplication>
34 #include <QDesktopServices>
36 #include <QInputDialog>
38 #include <QMessageBox>
41 #include <QPushButton>
44 #include <QTextLayout>
45 #include <QTextDocument>
58 using namespace lyx::support;
62 FileName libFileSearch(QString const & dir, QString const & name,
63 QString const & ext, search_mode mode)
65 return support::libFileSearch(fromqstr(dir), fromqstr(name), fromqstr(ext), mode);
69 FileName imageLibFileSearch(QString & dir, QString const & name,
70 QString const & ext, search_mode mode)
72 string tmp = fromqstr(dir);
73 FileName fn = support::imageLibFileSearch(tmp, fromqstr(name), fromqstr(ext), mode);
80 double locstringToDouble(QString const & str)
84 double res = loc.toDouble(str, &ok);
87 QLocale c(QLocale::C);
88 res = c.toDouble(str);
98 string widgetsToLength(QLineEdit const * input, LengthCombo const * combo)
100 QString const length = input->text();
101 if (length.isEmpty())
104 // Don't return unit-from-choice if the input(field) contains a unit
105 if (isValidGlueLength(fromqstr(length)))
106 return fromqstr(length);
107 // Also try with localized version
108 if (isValidGlueLength(fromqstr(unlocLengthString(length))))
109 return fromqstr(unlocLengthString(length));
111 Length::UNIT const unit = combo->currentLengthItem();
113 return Length(locstringToDouble(length.trimmed()), unit).asString();
117 Length widgetsToLength(QLineEdit const * input, QComboBox const * combo)
119 QString const length = input->text();
120 if (length.isEmpty())
123 // don't return unit-from-choice if the input(field) contains a unit
124 if (isValidGlueLength(fromqstr(length)))
125 return Length(fromqstr(length));
126 // Also try with localized version
127 if (isValidGlueLength(fromqstr(unlocLengthString(length))))
128 return Length(fromqstr(unlocLengthString(length)));
130 Length::UNIT unit = Length::UNIT_NONE;
131 QString const item = combo->currentText();
132 for (int i = 0; i < num_units; i++) {
133 if (qt_(lyx::unit_name_gui[i]) == item) {
134 unit = unitFromString(unit_name[i]);
139 return Length(locstringToDouble(length.trimmed()), unit);
143 void lengthToWidgets(QLineEdit * input, LengthCombo * combo,
144 Length const & len, Length::UNIT /*defaultUnit*/)
147 // no length (UNIT_NONE)
148 combo->setCurrentItem(Length::defaultUnit());
151 combo->setCurrentItem(len.unit());
153 loc.setNumberOptions(QLocale::OmitGroupSeparator);
154 input->setText(formatLocFPNumber(Length(len).value()));
159 void lengthToWidgets(QLineEdit * input, LengthCombo * combo,
160 string const & len, Length::UNIT defaultUnit)
163 // no length (UNIT_NONE)
164 combo->setCurrentItem(defaultUnit);
166 } else if (!isValidLength(len) && !isStrDbl(len)) {
167 // use input field only for gluelengths
168 combo->setCurrentItem(defaultUnit);
169 input->setText(locLengthString(toqstr(len)));
171 lengthToWidgets(input, combo, Length(len), defaultUnit);
176 void lengthToWidgets(QLineEdit * input, LengthCombo * combo,
177 docstring const & len, Length::UNIT defaultUnit)
179 lengthToWidgets(input, combo, to_utf8(len), defaultUnit);
183 double widgetToDouble(QLineEdit const * input)
185 QString const text = input->text();
189 return locstringToDouble(text.trimmed());
193 string widgetToDoubleStr(QLineEdit const * input)
195 return convert<string>(widgetToDouble(input));
199 void doubleToWidget(QLineEdit * input, double value, char f, int prec)
202 loc.setNumberOptions(QLocale::OmitGroupSeparator);
203 input->setText(loc.toString(value, f, prec));
207 void doubleToWidget(QLineEdit * input, string const & value, char f, int prec)
209 doubleToWidget(input, convert<double>(value), f, prec);
213 QString formatLocFPNumber(double d)
215 QString result = toqstr(formatFPNumber(d));
217 result.replace('.', loc.decimalPoint());
222 bool SortLocaleAware(QString const & lhs, QString const & rhs)
224 return QString::localeAwareCompare(lhs, rhs) < 0;
228 bool ColorSorter(ColorCode lhs, ColorCode rhs)
230 return compare_no_case(lcolor.getGUIName(lhs), lcolor.getGUIName(rhs)) < 0;
234 void setValid(QWidget * widget, bool valid)
237 if (qobject_cast<QCheckBox*>(widget) != nullptr)
238 // Check boxes need to be treated differenty, see
239 // https://forum.qt.io/topic/93253/
240 widget->setStyleSheet("");
242 widget->setPalette(QPalette());
244 if (qobject_cast<QCheckBox*>(widget) != nullptr) {
245 // Check boxes need to be treated differenty, see
246 // https://forum.qt.io/topic/93253/
247 if (qobject_cast<QCheckBox*>(widget)->isChecked())
248 widget->setStyleSheet("QCheckBox:unchecked{ color: red; }QCheckBox:checked{ color: red; }");
250 QPalette pal = widget->palette();
251 pal.setColor(QPalette::Active, QPalette::WindowText, QColor(255, 0, 0));
252 pal.setColor(QPalette::Active, QPalette::Text, QColor(255, 0, 0));
253 widget->setPalette(pal);
259 void focusAndHighlight(QAbstractItemView * w)
262 w->setCurrentIndex(w->currentIndex());
263 w->scrollTo(w->currentIndex());
267 void setMessageColour(list<QWidget *> highlighted, list<QWidget *> plain)
269 QPalette pal = QApplication::palette();
270 QPalette newpal(pal.color(QPalette::Active, QPalette::HighlightedText),
271 pal.color(QPalette::Active, QPalette::Highlight));
272 newpal.setColor(QPalette::WindowText,
273 pal.color(QPalette::Active, QPalette::HighlightedText));
274 for (QWidget * w : highlighted)
275 w->setPalette(newpal);
276 for (QWidget * w : plain)
281 void showDirectory(FileName const & directory)
283 if (!directory.exists())
285 QUrl qurl(QUrl::fromLocalFile(QDir::toNativeSeparators(toqstr(directory.absFileName()))));
286 // Give hints in case of bugs
287 if (!qurl.isValid()) {
288 frontend::Alert::error(_("Invalid URL"),
289 bformat(_("The URL `%1$s' could not be resolved."),
290 qstring_to_ucs4(qurl.toString())));
294 if (!QDesktopServices::openUrl(qurl))
295 frontend::Alert::error(_("URL could not be accessed"),
296 bformat(_("The URL `%1$s' could not be opened although it exists!"),
297 qstring_to_ucs4(qurl.toString())));
300 void showTarget(string const & target_in, Buffer const & buf)
302 LYXERR(Debug::INSETS, "Showtarget: " << target_in << "\n");
304 string target = target_in;
305 string const & docpath = buf.absFileName();
306 vector<string> targets;
308 bool const is_external = prefixIs(target, "EXTERNAL ");
310 if (!lyxrc.citation_search)
313 tar = split(target, tmp, ' ');
314 string const scriptcmd = subst(lyxrc.citation_search_view, "$${python}", os::python());
315 string const command = scriptcmd + " " + tar;
316 cmd_ret const ret = runCommand(commandPrep(command));
319 frontend::Alert::error(_("Could not open file"),
320 _("The lyxpaperview script failed."));
323 // lyxpaperview returns a \n-separated list of paths
324 targets = getVectorFromString(rtrim(ret.result, "\n"), "\n");
325 if (targets.empty()) {
326 frontend::Alert::error(_("Could not open file"),
327 bformat(_("No file was found using the pattern `%1$s'."),
332 if (prefixIs(target, "file://")) {
333 // file might have a \n-separated list of paths
334 targets = getVectorFromString(target, "\n");
336 if (!targets.empty()) {
337 if (targets.size() > 1) {
339 for (auto const & t : targets)
342 QString file = QInputDialog::getItem(nullptr, qt_("Multiple files found!"),
343 qt_("Select the file that should be opened:"),
344 files, 0, false, &ok);
345 if (!ok || file.isEmpty())
347 target = fromqstr(file);
349 target = targets.front();
351 // security measure: ask user before opening if document is not marked trusted.
353 if (!settings.value("trusted documents/" + toqstr(docpath), false).toBool()) {
354 QCheckBox * dontShowAgainCB = new QCheckBox();
355 dontShowAgainCB->setText(qt_("&Trust this document and do not ask me again!"));
356 dontShowAgainCB->setToolTip(qt_("If you check this, LyX will open all targets without asking for the given document in the future."));
357 docstring const warn = bformat(_("LyX wants to open the following target in an external application:\n\n"
359 "Be aware that this might entail security infringements!\n\n"
360 "Only do this if you trust the origin of the document and the target of the link!\n\n"
361 "How do you want to proceed?"), from_utf8(target));
362 QMessageBox box(QMessageBox::Warning, qt_("Open external target?"), toqstr(warn),
363 QMessageBox::NoButton, qApp->focusWidget());
364 QPushButton * openButton = box.addButton(qt_("&Open Target"), QMessageBox::ActionRole);
365 box.addButton(QMessageBox::Abort);
366 box.setCheckBox(dontShowAgainCB);
367 box.setDefaultButton(QMessageBox::Abort);
369 if (box.clickedButton() != openButton)
371 if (dontShowAgainCB->isChecked())
372 settings.setValue("trusted documents/"
373 + toqstr(docpath), true);
376 bool success = false;
377 QUrl url = is_external
378 ? QUrl::fromLocalFile(toqstr(target))
379 : QUrl(toqstr(target), QUrl::TolerantMode);
380 if (url.isLocalFile()) {
381 // For local files, we use our own viewers
382 // (QDesktopServices employs xdg-open which
383 // does not yet work everywhere)
384 FileName fn(fromqstr(url.path()));
385 string const format = theFormats().getFormatFromFile(fn);
386 success = theFormats().view(buf, fn, format);
388 // For external files, we rely on QDesktopServices
389 success = QDesktopServices::openUrl(url);
392 frontend::Alert::error(_("Could not open file"),
393 bformat(_("The target `%1$s' could not be resolved."),
396 } // namespace frontend
398 QString const qt_(char const * str, const char *)
400 return toqstr(_(str));
404 QString const qt_(string const & str)
406 return toqstr(_(str));
410 QString const qt_(QString const & qstr)
412 return toqstr(_(fromqstr(qstr)));
416 void rescanTexStyles(string const & arg)
418 // Run rescan in user lyx directory
419 PathChanger p(package().user_support());
420 FileName const prog = support::libFileSearch("scripts", "TeXFiles.py");
422 string const command = os::python() + ' ' +
423 quoteName(prog.toFilesystemEncoding()) + ' ' +
425 int const status = one.startscript(Systemcall::Wait, command);
429 frontend::Alert::error(_("Could not update TeX information"),
430 bformat(_("The script `%1$s' failed."), from_utf8(prog.absFileName())));
434 QStringList texFileList(QString const & filename)
437 FileName const file = libFileSearch(QString(), filename);
442 vector<docstring> doclist =
443 getVectorFromString(file.fileContents("UTF-8"), from_ascii("\n"));
445 // Normalise paths like /foo//bar ==> /foo/bar
447 for (size_t i = 0; i != doclist.size(); ++i) {
448 QString qfile = toqstr(doclist[i]);
449 qfile.replace("\r", "");
450 while (qfile.contains("//"))
451 qfile.replace("//", "/");
452 if (!qfile.isEmpty())
457 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
458 return QList<QString>(set.begin(), set.end());
460 return QList<QString>::fromSet(set);
464 QString const externalLineEnding(docstring const & str)
467 // The MAC clipboard uses \r for lineendings, and we use \n
468 return toqstr(subst(str, '\n', '\r'));
469 #elif defined(Q_OS_WIN) || defined(Q_CYGWIN_WIN)
470 // Windows clipboard uses \r\n for lineendings, and we use \n
471 return toqstr(subst(str, from_ascii("\n"), from_ascii("\r\n")));
478 docstring const internalLineEnding(QString const & str)
480 docstring const s = subst(qstring_to_ucs4(str),
481 from_ascii("\r\n"), from_ascii("\n"));
482 return subst(s, '\r', '\n');
486 QString internalPath(const QString & str)
488 return toqstr(os::internal_path(fromqstr(str)));
492 QString onlyFileName(const QString & str)
494 return toqstr(support::onlyFileName(fromqstr(str)));
498 QString onlyPath(const QString & str)
500 return toqstr(support::onlyPath(fromqstr(str)));
504 QString changeExtension(QString const & oldname, QString const & ext)
506 return toqstr(support::changeExtension(fromqstr(oldname), fromqstr(ext)));
509 /// Remove the extension from \p name
510 QString removeExtension(QString const & name)
512 return toqstr(support::removeExtension(fromqstr(name)));
515 /** Add the extension \p ext to \p name.
516 Use this instead of changeExtension if you know that \p name is without
517 extension, because changeExtension would wrongly interpret \p name if it
520 QString addExtension(QString const & name, QString const & ext)
522 return toqstr(support::addExtension(fromqstr(name), fromqstr(ext)));
525 /// Return the extension of the file (not including the .)
526 QString getExtension(QString const & name)
528 return toqstr(support::getExtension(fromqstr(name)));
532 /** Convert relative path into absolute path based on a basepath.
533 If relpath is absolute, just use that.
534 If basepath doesn't exist use CWD.
536 QString makeAbsPath(QString const & relpath, QString const & base)
538 return toqstr(support::makeAbsPath(fromqstr(relpath),
539 fromqstr(base)).absFileName());
543 /////////////////////////////////////////////////////////////////////////
547 /////////////////////////////////////////////////////////////////////////
549 /** Given a string such as
550 * "<glob> <glob> ... *.{abc,def} <glob>",
551 * convert the csh-style brace expressions:
552 * "<glob> <glob> ... *.abc *.def <glob>".
553 * Requires no system support, so should work equally on Unix, Mac, Win32.
555 static string const convert_brace_glob(string const & glob)
557 // Matches " *.{abc,def,ghi}", storing "*." as group 1 and
558 // "abc,def,ghi" as group 2, while allowing spaces in group 2.
559 static regex const glob_re(" *([^ {]*)\\{([^}]+)\\}");
560 // Matches "abc" and "abc,", storing "abc" as group 1,
561 // while ignoring surrounding spaces.
562 static regex const block_re(" *([^ ,}]+) *,? *");
566 string::const_iterator it = glob.begin();
567 string::const_iterator const end = glob.end();
569 match_results<string::const_iterator> what;
570 if (!regex_search(it, end, what, glob_re)) {
571 // Ensure that no information is lost.
572 pattern += string(it, end);
576 // Everything from the start of the input to
577 // the start of the match.
578 pattern += string(what[-1].first, what[-1].second);
580 // Given " *.{abc,def}", head == "*." and tail == "abc,def".
581 string const head = string(what[1].first, what[1].second);
582 string const tail = string(what[2].first, what[2].second);
584 // Split the ','-separated chunks of tail so that
585 // $head{$chunk1,$chunk2} becomes "$head$chunk1 $head$chunk2".
586 string const fmt = " " + head + "$1";
587 pattern += regex_replace(tail, block_re, fmt);
589 // Increment the iterator to the end of the match.
590 it += distance(it, what[0].second);
599 /* \param description text describing the filters.
600 * \param one or more wildcard patterns, separated by
603 Filter(docstring const & description, std::string const & globs);
605 docstring const & description() const { return desc_; }
607 QString toString() const;
610 std::vector<std::string> globs_;
614 Filter::Filter(docstring const & description, string const & globs)
617 // Given "<glob> <glob> ... *.{abc,def} <glob>", expand to
618 // "<glob> <glob> ... *.abc *.def <glob>"
619 string const expanded_globs = convert_brace_glob(globs);
621 // Split into individual globs.
622 globs_ = getVectorFromString(expanded_globs, " ");
626 QString Filter::toString() const
630 bool const has_description = !desc_.empty();
632 if (has_description) {
637 s += toqstr(getStringFromVector(globs_, " "));
645 /** \c FileFilterList parses a Qt-style list of available file filters
646 * to generate the corresponding vector.
647 * For example "TeX documents (*.tex);;LyX Documents (*.lyx)"
648 * will be parsed to fill a vector of size 2, whilst "*.{p[bgp]m} *.pdf"
649 * will result in a vector of size 1 in which the description field is empty.
651 struct FileFilterList
653 // FIXME UNICODE: globs_ should be unicode...
654 /** \param qt_style_filter a list of available file filters.
655 * Eg. "TeX documents (*.tex);;LyX Documents (*.lyx)".
656 * The "All files (*)" filter is always added to the list.
658 explicit FileFilterList(docstring const & qt_style_filter =
661 typedef std::vector<Filter>::size_type size_type;
663 bool empty() const { return filters_.empty(); }
664 size_type size() const { return filters_.size(); }
665 Filter & operator[](size_type i) { return filters_[i]; }
666 Filter const & operator[](size_type i) const { return filters_[i]; }
668 void parse_filter(std::string const & filter);
669 std::vector<Filter> filters_;
672 FileFilterList::FileFilterList(docstring const & qt_style_filter)
675 string const filter = to_utf8(qt_style_filter)
676 + (qt_style_filter.empty() ? string() : ";;")
677 + to_utf8(_("All Files")) + " " + fromqstr(wildcardAllFiles());
679 // Split data such as "TeX documents (*.tex);;LyX Documents (*.lyx)"
680 // into individual filters.
681 static regex const separator_re(";;");
683 string::const_iterator it = filter.begin();
684 string::const_iterator const end = filter.end();
686 match_results<string::const_iterator> what;
688 if (!regex_search(it, end, what, separator_re)) {
689 parse_filter(string(it, end));
693 // Everything from the start of the input to
694 // the start of the match.
695 parse_filter(string(it, what[0].first));
697 // Increment the iterator to the end of the match.
698 it += distance(it, what[0].second);
703 void FileFilterList::parse_filter(string const & filter)
705 // Matches "TeX documents (plain) (*.tex)",
706 // storing "TeX documents (plain) " as group 1 and "*.tex" as group 2.
707 static regex const filter_re("(.*)\\(([^()]+)\\) *$");
709 match_results<string::const_iterator> what;
710 if (!regex_search(filter, what, filter_re)) {
711 // Just a glob, no description.
712 filters_.push_back(Filter(docstring(), trim(filter)));
715 docstring const desc = from_utf8(string(what[1].first, what[1].second));
716 string const globs = string(what[2].first, what[2].second);
717 filters_.push_back(Filter(trim(desc), trim(globs)));
722 QString wildcardAllFiles()
732 /** \returns the equivalent of the string passed in
733 * although any brace expressions are expanded.
734 * (E.g. "*.{png,jpg}" -> "*.png *.jpg")
736 QStringList fileFilters(QString const & desc)
738 // we have: "*.{gif,png,jpg,bmp,pbm,ppm,tga,tif,xpm,xbm}"
739 // but need: "*.cpp;*.cc;*.C;*.cxx;*.c++"
740 FileFilterList filters(qstring_to_ucs4(desc));
741 //LYXERR0("DESC: " << desc);
743 for (size_t i = 0; i != filters.filters_.size(); ++i) {
744 QString f = filters.filters_[i].toString();
745 //LYXERR0("FILTER: " << f);
752 QString formatToolTip(QString text, int width)
754 // 1. QTooltip activates word wrapping only if mightBeRichText()
755 // is true. So we convert the text to rich text.
757 // 2. The default width is way too small. Setting the width is tricky; first
758 // one has to compute the ideal width, and then force it with special
761 // do nothing if empty or already formatted
762 if (text.isEmpty() || text.startsWith(QString("<html>")))
764 // Convert to rich text if it is not already
765 if (!Qt::mightBeRichText(text))
766 text = Qt::convertFromPlainText(text, Qt::WhiteSpaceNormal);
767 // Compute desired width in pixels
768 QFont const font = QToolTip::font();
769 #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
770 int const px_width = width * QFontMetrics(font).horizontalAdvance("M");
772 int const px_width = width * QFontMetrics(font).width("M");
774 // Determine the ideal width of the tooltip
775 QTextDocument td("");
777 td.setDefaultFont(QToolTip::font());
778 td.setDocumentMargin(0);
779 td.setTextWidth(px_width);
780 double best_width = td.idealWidth();
781 // Set the line wrapping with appropriate width
782 return QString("<html><body><table><tr>"
783 "<td align=justify width=%1>%2</td>"
784 "</tr></table></body></html>")
785 .arg(QString::number(int(best_width) + 1), text);
789 QString qtHtmlToPlainText(QString const & text)
791 if (!Qt::mightBeRichText(text))
795 return td.toPlainText();