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();
307 bool const is_external = prefixIs(target, "EXTERNAL ");
309 if (!lyxrc.citation_search)
312 tar = split(target, tmp, ' ');
313 string const scriptcmd = subst(lyxrc.citation_search_view, "$${python}", os::python());
314 string const command = scriptcmd + " " + tar;
315 cmd_ret const ret = runCommand(commandPrep(command));
318 frontend::Alert::error(_("Could not open file"),
319 _("The lyxpaperview script failed."));
322 // lyxpaperview returns a \n-separated list of paths
323 vector<string> targets = getVectorFromString(rtrim(ret.result, "\n"), "\n");
324 if (targets.empty()) {
325 frontend::Alert::error(_("Could not open file"),
326 bformat(_("No file was found using the pattern `%1$s'."),
330 if (targets.size() > 1) {
332 for (auto const & t : targets)
335 QString file = QInputDialog::getItem(nullptr, qt_("Multiple files found!"),
336 qt_("Select the file that should be opened:"),
337 files, 0, false, &ok);
338 if (!ok || file.isEmpty())
340 target = fromqstr(file);
342 target = targets.front();
344 // security measure: ask user before opening if document is not marked trusted.
346 if (!settings.value("trusted documents/" + toqstr(docpath), false).toBool()) {
347 QCheckBox * dontShowAgainCB = new QCheckBox();
348 dontShowAgainCB->setText(qt_("&Trust this document and do not ask me again!"));
349 dontShowAgainCB->setToolTip(qt_("If you check this, LyX will open all targets without asking for the given document in the future."));
350 docstring const warn = bformat(_("LyX wants to open the following target in an external application:\n"
352 "Be aware that this might entail security infringements!\n"
353 "Only do this if you trust origin of the document and the target of the link!\n"
354 "How do you want to proceed?"), from_utf8(target));
355 QMessageBox box(QMessageBox::Warning, qt_("Open external target?"), toqstr(warn),
356 QMessageBox::NoButton, qApp->focusWidget());
357 QPushButton * openButton = box.addButton(qt_("&Open Target"), QMessageBox::ActionRole);
358 box.addButton(QMessageBox::Abort);
359 box.setCheckBox(dontShowAgainCB);
360 box.setDefaultButton(QMessageBox::Abort);
362 if (box.clickedButton() != openButton)
364 if (dontShowAgainCB->isChecked())
365 settings.setValue("trusted documents/"
366 + toqstr(docpath), true);
369 bool success = false;
370 QUrl url = is_external
371 ? QUrl::fromLocalFile(toqstr(target))
372 : QUrl(toqstr(target), QUrl::TolerantMode);
373 if (url.isLocalFile()) {
374 // For local files, we use our own viewers
375 // (QDesktopServices employs xdg-open which
376 // does not yet work everywhere)
377 FileName fn(fromqstr(url.path()));
378 string const format = theFormats().getFormatFromFile(fn);
379 success = theFormats().view(buf, fn, format);
381 // For external files, we rely on QDesktopServices
382 success = QDesktopServices::openUrl(url);
385 frontend::Alert::error(_("Could not open file"),
386 bformat(_("The target `%1$s' could not be resolved."),
389 } // namespace frontend
391 QString const qt_(char const * str, const char *)
393 return toqstr(_(str));
397 QString const qt_(string const & str)
399 return toqstr(_(str));
403 QString const qt_(QString const & qstr)
405 return toqstr(_(fromqstr(qstr)));
409 void rescanTexStyles(string const & arg)
411 // Run rescan in user lyx directory
412 PathChanger p(package().user_support());
413 FileName const prog = support::libFileSearch("scripts", "TeXFiles.py");
415 string const command = os::python() + ' ' +
416 quoteName(prog.toFilesystemEncoding()) + ' ' +
418 int const status = one.startscript(Systemcall::Wait, command);
422 frontend::Alert::error(_("Could not update TeX information"),
423 bformat(_("The script `%1$s' failed."), from_utf8(prog.absFileName())));
427 QStringList texFileList(QString const & filename)
430 FileName const file = libFileSearch(QString(), filename);
435 vector<docstring> doclist =
436 getVectorFromString(file.fileContents("UTF-8"), from_ascii("\n"));
438 // Normalise paths like /foo//bar ==> /foo/bar
440 for (size_t i = 0; i != doclist.size(); ++i) {
441 QString qfile = toqstr(doclist[i]);
442 qfile.replace("\r", "");
443 while (qfile.contains("//"))
444 qfile.replace("//", "/");
445 if (!qfile.isEmpty())
450 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
451 return QList<QString>(set.begin(), set.end());
453 return QList<QString>::fromSet(set);
457 QString const externalLineEnding(docstring const & str)
460 // The MAC clipboard uses \r for lineendings, and we use \n
461 return toqstr(subst(str, '\n', '\r'));
462 #elif defined(Q_OS_WIN) || defined(Q_CYGWIN_WIN)
463 // Windows clipboard uses \r\n for lineendings, and we use \n
464 return toqstr(subst(str, from_ascii("\n"), from_ascii("\r\n")));
471 docstring const internalLineEnding(QString const & str)
473 docstring const s = subst(qstring_to_ucs4(str),
474 from_ascii("\r\n"), from_ascii("\n"));
475 return subst(s, '\r', '\n');
479 QString internalPath(const QString & str)
481 return toqstr(os::internal_path(fromqstr(str)));
485 QString onlyFileName(const QString & str)
487 return toqstr(support::onlyFileName(fromqstr(str)));
491 QString onlyPath(const QString & str)
493 return toqstr(support::onlyPath(fromqstr(str)));
497 QString changeExtension(QString const & oldname, QString const & ext)
499 return toqstr(support::changeExtension(fromqstr(oldname), fromqstr(ext)));
502 /// Remove the extension from \p name
503 QString removeExtension(QString const & name)
505 return toqstr(support::removeExtension(fromqstr(name)));
508 /** Add the extension \p ext to \p name.
509 Use this instead of changeExtension if you know that \p name is without
510 extension, because changeExtension would wrongly interpret \p name if it
513 QString addExtension(QString const & name, QString const & ext)
515 return toqstr(support::addExtension(fromqstr(name), fromqstr(ext)));
518 /// Return the extension of the file (not including the .)
519 QString getExtension(QString const & name)
521 return toqstr(support::getExtension(fromqstr(name)));
525 /** Convert relative path into absolute path based on a basepath.
526 If relpath is absolute, just use that.
527 If basepath doesn't exist use CWD.
529 QString makeAbsPath(QString const & relpath, QString const & base)
531 return toqstr(support::makeAbsPath(fromqstr(relpath),
532 fromqstr(base)).absFileName());
536 /////////////////////////////////////////////////////////////////////////
540 /////////////////////////////////////////////////////////////////////////
542 /** Given a string such as
543 * "<glob> <glob> ... *.{abc,def} <glob>",
544 * convert the csh-style brace expressions:
545 * "<glob> <glob> ... *.abc *.def <glob>".
546 * Requires no system support, so should work equally on Unix, Mac, Win32.
548 static string const convert_brace_glob(string const & glob)
550 // Matches " *.{abc,def,ghi}", storing "*." as group 1 and
551 // "abc,def,ghi" as group 2, while allowing spaces in group 2.
552 static regex const glob_re(" *([^ {]*)\\{([^}]+)\\}");
553 // Matches "abc" and "abc,", storing "abc" as group 1,
554 // while ignoring surrounding spaces.
555 static regex const block_re(" *([^ ,}]+) *,? *");
559 string::const_iterator it = glob.begin();
560 string::const_iterator const end = glob.end();
562 match_results<string::const_iterator> what;
563 if (!regex_search(it, end, what, glob_re)) {
564 // Ensure that no information is lost.
565 pattern += string(it, end);
569 // Everything from the start of the input to
570 // the start of the match.
571 pattern += string(what[-1].first, what[-1].second);
573 // Given " *.{abc,def}", head == "*." and tail == "abc,def".
574 string const head = string(what[1].first, what[1].second);
575 string const tail = string(what[2].first, what[2].second);
577 // Split the ','-separated chunks of tail so that
578 // $head{$chunk1,$chunk2} becomes "$head$chunk1 $head$chunk2".
579 string const fmt = " " + head + "$1";
580 pattern += regex_replace(tail, block_re, fmt);
582 // Increment the iterator to the end of the match.
583 it += distance(it, what[0].second);
592 /* \param description text describing the filters.
593 * \param one or more wildcard patterns, separated by
596 Filter(docstring const & description, std::string const & globs);
598 docstring const & description() const { return desc_; }
600 QString toString() const;
603 std::vector<std::string> globs_;
607 Filter::Filter(docstring const & description, string const & globs)
610 // Given "<glob> <glob> ... *.{abc,def} <glob>", expand to
611 // "<glob> <glob> ... *.abc *.def <glob>"
612 string const expanded_globs = convert_brace_glob(globs);
614 // Split into individual globs.
615 globs_ = getVectorFromString(expanded_globs, " ");
619 QString Filter::toString() const
623 bool const has_description = !desc_.empty();
625 if (has_description) {
630 s += toqstr(getStringFromVector(globs_, " "));
638 /** \c FileFilterList parses a Qt-style list of available file filters
639 * to generate the corresponding vector.
640 * For example "TeX documents (*.tex);;LyX Documents (*.lyx)"
641 * will be parsed to fill a vector of size 2, whilst "*.{p[bgp]m} *.pdf"
642 * will result in a vector of size 1 in which the description field is empty.
644 struct FileFilterList
646 // FIXME UNICODE: globs_ should be unicode...
647 /** \param qt_style_filter a list of available file filters.
648 * Eg. "TeX documents (*.tex);;LyX Documents (*.lyx)".
649 * The "All files (*)" filter is always added to the list.
651 explicit FileFilterList(docstring const & qt_style_filter =
654 typedef std::vector<Filter>::size_type size_type;
656 bool empty() const { return filters_.empty(); }
657 size_type size() const { return filters_.size(); }
658 Filter & operator[](size_type i) { return filters_[i]; }
659 Filter const & operator[](size_type i) const { return filters_[i]; }
661 void parse_filter(std::string const & filter);
662 std::vector<Filter> filters_;
665 FileFilterList::FileFilterList(docstring const & qt_style_filter)
668 string const filter = to_utf8(qt_style_filter)
669 + (qt_style_filter.empty() ? string() : ";;")
670 + to_utf8(_("All Files")) + " " + fromqstr(wildcardAllFiles());
672 // Split data such as "TeX documents (*.tex);;LyX Documents (*.lyx)"
673 // into individual filters.
674 static regex const separator_re(";;");
676 string::const_iterator it = filter.begin();
677 string::const_iterator const end = filter.end();
679 match_results<string::const_iterator> what;
681 if (!regex_search(it, end, what, separator_re)) {
682 parse_filter(string(it, end));
686 // Everything from the start of the input to
687 // the start of the match.
688 parse_filter(string(it, what[0].first));
690 // Increment the iterator to the end of the match.
691 it += distance(it, what[0].second);
696 void FileFilterList::parse_filter(string const & filter)
698 // Matches "TeX documents (plain) (*.tex)",
699 // storing "TeX documents (plain) " as group 1 and "*.tex" as group 2.
700 static regex const filter_re("(.*)\\(([^()]+)\\) *$");
702 match_results<string::const_iterator> what;
703 if (!regex_search(filter, what, filter_re)) {
704 // Just a glob, no description.
705 filters_.push_back(Filter(docstring(), trim(filter)));
708 docstring const desc = from_utf8(string(what[1].first, what[1].second));
709 string const globs = string(what[2].first, what[2].second);
710 filters_.push_back(Filter(trim(desc), trim(globs)));
715 QString wildcardAllFiles()
725 /** \returns the equivalent of the string passed in
726 * although any brace expressions are expanded.
727 * (E.g. "*.{png,jpg}" -> "*.png *.jpg")
729 QStringList fileFilters(QString const & desc)
731 // we have: "*.{gif,png,jpg,bmp,pbm,ppm,tga,tif,xpm,xbm}"
732 // but need: "*.cpp;*.cc;*.C;*.cxx;*.c++"
733 FileFilterList filters(qstring_to_ucs4(desc));
734 //LYXERR0("DESC: " << desc);
736 for (size_t i = 0; i != filters.filters_.size(); ++i) {
737 QString f = filters.filters_[i].toString();
738 //LYXERR0("FILTER: " << f);
745 QString formatToolTip(QString text, int width)
747 // 1. QTooltip activates word wrapping only if mightBeRichText()
748 // is true. So we convert the text to rich text.
750 // 2. The default width is way too small. Setting the width is tricky; first
751 // one has to compute the ideal width, and then force it with special
754 // do nothing if empty or already formatted
755 if (text.isEmpty() || text.startsWith(QString("<html>")))
757 // Convert to rich text if it is not already
758 if (!Qt::mightBeRichText(text))
759 text = Qt::convertFromPlainText(text, Qt::WhiteSpaceNormal);
760 // Compute desired width in pixels
761 QFont const font = QToolTip::font();
762 #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
763 int const px_width = width * QFontMetrics(font).horizontalAdvance("M");
765 int const px_width = width * QFontMetrics(font).width("M");
767 // Determine the ideal width of the tooltip
768 QTextDocument td("");
770 td.setDefaultFont(QToolTip::font());
771 td.setDocumentMargin(0);
772 td.setTextWidth(px_width);
773 double best_width = td.idealWidth();
774 // Set the line wrapping with appropriate width
775 return QString("<html><body><table><tr>"
776 "<td align=justify width=%1>%2</td>"
777 "</tr></table></body></html>")
778 .arg(QString::number(int(best_width) + 1), text);
782 QString qtHtmlToPlainText(QString const & text)
784 if (!Qt::mightBeRichText(text))
788 return td.toPlainText();