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"
17 #include "LengthCombo.h"
20 #include "frontends/alert.h"
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"
30 #include <QApplication>
33 #include <QDesktopServices>
36 #include <QMessageBox>
39 #include <QPushButton>
42 #include <QTextLayout>
43 #include <QTextDocument>
56 using namespace lyx::support;
60 FileName libFileSearch(QString const & dir, QString const & name,
61 QString const & ext, search_mode mode)
63 return support::libFileSearch(fromqstr(dir), fromqstr(name), fromqstr(ext), mode);
67 FileName imageLibFileSearch(QString & dir, QString const & name,
68 QString const & ext, search_mode mode)
70 string tmp = fromqstr(dir);
71 FileName fn = support::imageLibFileSearch(tmp, fromqstr(name), fromqstr(ext), mode);
78 double locstringToDouble(QString const & str)
82 double res = loc.toDouble(str, &ok);
85 QLocale c(QLocale::C);
86 res = c.toDouble(str);
96 string widgetsToLength(QLineEdit const * input, LengthCombo const * combo)
98 QString const length = input->text();
102 // Don't return unit-from-choice if the input(field) contains a unit
103 if (isValidGlueLength(fromqstr(length)))
104 return fromqstr(length);
105 // Also try with localized version
106 if (isValidGlueLength(fromqstr(unlocLengthString(length))))
107 return fromqstr(unlocLengthString(length));
109 Length::UNIT const unit = combo->currentLengthItem();
111 return Length(locstringToDouble(length.trimmed()), unit).asString();
115 Length widgetsToLength(QLineEdit const * input, QComboBox const * combo)
117 QString const length = input->text();
118 if (length.isEmpty())
121 // don't return unit-from-choice if the input(field) contains a unit
122 if (isValidGlueLength(fromqstr(length)))
123 return Length(fromqstr(length));
124 // Also try with localized version
125 if (isValidGlueLength(fromqstr(unlocLengthString(length))))
126 return Length(fromqstr(unlocLengthString(length)));
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]);
137 return Length(locstringToDouble(length.trimmed()), unit);
141 void lengthToWidgets(QLineEdit * input, LengthCombo * combo,
142 Length const & len, Length::UNIT /*defaultUnit*/)
145 // no length (UNIT_NONE)
146 combo->setCurrentItem(Length::defaultUnit());
149 combo->setCurrentItem(len.unit());
151 loc.setNumberOptions(QLocale::OmitGroupSeparator);
152 input->setText(formatLocFPNumber(Length(len).value()));
157 void lengthToWidgets(QLineEdit * input, LengthCombo * combo,
158 string const & len, Length::UNIT defaultUnit)
161 // no length (UNIT_NONE)
162 combo->setCurrentItem(defaultUnit);
164 } else if (!isValidLength(len) && !isStrDbl(len)) {
165 // use input field only for gluelengths
166 combo->setCurrentItem(defaultUnit);
167 input->setText(locLengthString(toqstr(len)));
169 lengthToWidgets(input, combo, Length(len), defaultUnit);
174 void lengthToWidgets(QLineEdit * input, LengthCombo * combo,
175 docstring const & len, Length::UNIT defaultUnit)
177 lengthToWidgets(input, combo, to_utf8(len), defaultUnit);
181 double widgetToDouble(QLineEdit const * input)
183 QString const text = input->text();
187 return locstringToDouble(text.trimmed());
191 string widgetToDoubleStr(QLineEdit const * input)
193 return convert<string>(widgetToDouble(input));
197 void doubleToWidget(QLineEdit * input, double value, char f, int prec)
200 loc.setNumberOptions(QLocale::OmitGroupSeparator);
201 input->setText(loc.toString(value, f, prec));
205 void doubleToWidget(QLineEdit * input, string const & value, char f, int prec)
207 doubleToWidget(input, convert<double>(value), f, prec);
211 QString formatLocFPNumber(double d)
213 QString result = toqstr(formatFPNumber(d));
215 result.replace('.', loc.decimalPoint());
220 bool SortLocaleAware(QString const & lhs, QString const & rhs)
222 return QString::localeAwareCompare(lhs, rhs) < 0;
226 bool ColorSorter(ColorCode lhs, ColorCode rhs)
228 return compare_no_case(lcolor.getGUIName(lhs), lcolor.getGUIName(rhs)) < 0;
232 void setValid(QWidget * widget, bool valid)
235 if (qobject_cast<QCheckBox*>(widget) != nullptr)
236 // Check boxes need to be treated differenty, see
237 // https://forum.qt.io/topic/93253/
238 widget->setStyleSheet("");
240 widget->setPalette(QPalette());
242 if (qobject_cast<QCheckBox*>(widget) != nullptr) {
243 // Check boxes need to be treated differenty, see
244 // https://forum.qt.io/topic/93253/
245 if (qobject_cast<QCheckBox*>(widget)->isChecked())
246 widget->setStyleSheet("QCheckBox:unchecked{ color: red; }QCheckBox:checked{ color: red; }");
248 QPalette pal = widget->palette();
249 pal.setColor(QPalette::Active, QPalette::WindowText, QColor(255, 0, 0));
250 pal.setColor(QPalette::Active, QPalette::Text, QColor(255, 0, 0));
251 widget->setPalette(pal);
257 void focusAndHighlight(QAbstractItemView * w)
260 w->setCurrentIndex(w->currentIndex());
261 w->scrollTo(w->currentIndex());
265 void setMessageColour(list<QWidget *> highlighted, list<QWidget *> plain)
267 QPalette pal = QApplication::palette();
268 QPalette newpal(pal.color(QPalette::Active, QPalette::HighlightedText),
269 pal.color(QPalette::Active, QPalette::Highlight));
270 newpal.setColor(QPalette::WindowText,
271 pal.color(QPalette::Active, QPalette::HighlightedText));
272 for (QWidget * w : highlighted)
273 w->setPalette(newpal);
274 for (QWidget * w : plain)
279 void showDirectory(FileName const & directory)
281 if (!directory.exists())
283 QUrl qurl(QUrl::fromLocalFile(QDir::toNativeSeparators(toqstr(directory.absFileName()))));
284 // Give hints in case of bugs
285 if (!qurl.isValid()) {
286 frontend::Alert::error(_("Invalid URL"),
287 bformat(_("The URL `%1$s' could not be resolved."),
288 qstring_to_ucs4(qurl.toString())));
292 if (!QDesktopServices::openUrl(qurl))
293 frontend::Alert::error(_("URL could not be accessed"),
294 bformat(_("The URL `%1$s' could not be opened although it exists!"),
295 qstring_to_ucs4(qurl.toString())));
298 void showTarget(string const & target, string const & docpath,
299 string const & pdfv, string const & psv)
301 LYXERR(Debug::INSETS, "Showtarget:" << target << "\n");
303 // security measure: ask user before opening if document is not marked trusted.
305 if (!settings.value("trusted documents/" + toqstr(docpath), false).toBool()) {
306 QCheckBox * dontShowAgainCB = new QCheckBox();
307 dontShowAgainCB->setText(qt_("&Trust this document and do not ask me again!"));
308 dontShowAgainCB->setToolTip(qt_("If you check this, LyX will open all targets without asking for the given document in the future."));
309 docstring const warn =
310 prefixIs(target, "EXTERNAL ") ?
311 bformat(_("LyX will search your directory for files with the following keywords in their name "
312 "and then open it in an external application, if a file is found:\n"
314 "Be aware that this might entail security infringements!\n"
315 "Only do this if you trust origin of the document and the keywords used!\n"
316 "How do you want to proceed?"), from_utf8(target).substr(9, docstring::npos))
317 : bformat(_("LyX wants to open the following link in an external application:\n"
319 "Be aware that this might entail security infringements!\n"
320 "Only do this if you trust origin of the document and the target of the link!\n"
321 "How do you want to proceed?"), from_utf8(target));
322 QMessageBox box(QMessageBox::Warning, qt_("Open external target?"), toqstr(warn),
323 QMessageBox::NoButton, qApp->focusWidget());
324 QPushButton * openButton = box.addButton(qt_("&Open Target"), QMessageBox::ActionRole);
325 box.addButton(QMessageBox::Abort);
326 box.setCheckBox(dontShowAgainCB);
327 box.setDefaultButton(QMessageBox::Abort);
329 if (box.clickedButton() != openButton)
331 if (dontShowAgainCB->isChecked())
332 settings.setValue("trusted documents/"
333 + toqstr(docpath), true);
336 if (prefixIs(target, "EXTERNAL ")) {
337 if (!lyxrc.citation_search)
339 string tmp, tar, opts;
340 tar = split(target, tmp, ' ');
342 opts = " -v \"" + pdfv + "\"";
344 opts += " -w \"" + psv + "\"";
348 string const viewer = subst(lyxrc.citation_search_view, "$${python}", os::python());
349 string const command = viewer + " " + opts + tar;
350 int const result = one.startscript(Systemcall::Wait, command);
353 frontend::Alert::error(_("Could not open file"),
354 _("The lyxpaperview script failed."));
355 else if (result == 2)
356 frontend::Alert::error(_("Could not open file"),
357 bformat(_("No file was found using the pattern `%1$s'."),
361 if (!QDesktopServices::openUrl(QUrl(toqstr(target), QUrl::TolerantMode)))
362 frontend::Alert::error(_("Could not open file"),
363 bformat(_("The target `%1$s' could not be resolved."),
366 } // namespace frontend
368 QString const qt_(char const * str, const char *)
370 return toqstr(_(str));
374 QString const qt_(string const & str)
376 return toqstr(_(str));
380 QString const qt_(QString const & qstr)
382 return toqstr(_(fromqstr(qstr)));
386 void rescanTexStyles(string const & arg)
388 // Run rescan in user lyx directory
389 PathChanger p(package().user_support());
390 FileName const prog = support::libFileSearch("scripts", "TeXFiles.py");
392 string const command = os::python() + ' ' +
393 quoteName(prog.toFilesystemEncoding()) + ' ' +
395 int const status = one.startscript(Systemcall::Wait, command);
399 frontend::Alert::error(_("Could not update TeX information"),
400 bformat(_("The script `%1$s' failed."), from_utf8(prog.absFileName())));
404 QStringList texFileList(QString const & filename)
407 FileName const file = libFileSearch(QString(), filename);
412 vector<docstring> doclist =
413 getVectorFromString(file.fileContents("UTF-8"), from_ascii("\n"));
415 // Normalise paths like /foo//bar ==> /foo/bar
417 for (size_t i = 0; i != doclist.size(); ++i) {
418 QString qfile = toqstr(doclist[i]);
419 qfile.replace("\r", "");
420 while (qfile.contains("//"))
421 qfile.replace("//", "/");
422 if (!qfile.isEmpty())
427 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
428 return QList<QString>(set.begin(), set.end());
430 return QList<QString>::fromSet(set);
434 QString const externalLineEnding(docstring const & str)
437 // The MAC clipboard uses \r for lineendings, and we use \n
438 return toqstr(subst(str, '\n', '\r'));
439 #elif defined(Q_OS_WIN) || defined(Q_CYGWIN_WIN)
440 // Windows clipboard uses \r\n for lineendings, and we use \n
441 return toqstr(subst(str, from_ascii("\n"), from_ascii("\r\n")));
448 docstring const internalLineEnding(QString const & str)
450 docstring const s = subst(qstring_to_ucs4(str),
451 from_ascii("\r\n"), from_ascii("\n"));
452 return subst(s, '\r', '\n');
456 QString internalPath(const QString & str)
458 return toqstr(os::internal_path(fromqstr(str)));
462 QString onlyFileName(const QString & str)
464 return toqstr(support::onlyFileName(fromqstr(str)));
468 QString onlyPath(const QString & str)
470 return toqstr(support::onlyPath(fromqstr(str)));
474 QString changeExtension(QString const & oldname, QString const & ext)
476 return toqstr(support::changeExtension(fromqstr(oldname), fromqstr(ext)));
479 /// Remove the extension from \p name
480 QString removeExtension(QString const & name)
482 return toqstr(support::removeExtension(fromqstr(name)));
485 /** Add the extension \p ext to \p name.
486 Use this instead of changeExtension if you know that \p name is without
487 extension, because changeExtension would wrongly interpret \p name if it
490 QString addExtension(QString const & name, QString const & ext)
492 return toqstr(support::addExtension(fromqstr(name), fromqstr(ext)));
495 /// Return the extension of the file (not including the .)
496 QString getExtension(QString const & name)
498 return toqstr(support::getExtension(fromqstr(name)));
502 /** Convert relative path into absolute path based on a basepath.
503 If relpath is absolute, just use that.
504 If basepath doesn't exist use CWD.
506 QString makeAbsPath(QString const & relpath, QString const & base)
508 return toqstr(support::makeAbsPath(fromqstr(relpath),
509 fromqstr(base)).absFileName());
513 /////////////////////////////////////////////////////////////////////////
517 /////////////////////////////////////////////////////////////////////////
519 /** Given a string such as
520 * "<glob> <glob> ... *.{abc,def} <glob>",
521 * convert the csh-style brace expressions:
522 * "<glob> <glob> ... *.abc *.def <glob>".
523 * Requires no system support, so should work equally on Unix, Mac, Win32.
525 static string const convert_brace_glob(string const & glob)
527 // Matches " *.{abc,def,ghi}", storing "*." as group 1 and
528 // "abc,def,ghi" as group 2, while allowing spaces in group 2.
529 static regex const glob_re(" *([^ {]*)\\{([^}]+)\\}");
530 // Matches "abc" and "abc,", storing "abc" as group 1,
531 // while ignoring surrounding spaces.
532 static regex const block_re(" *([^ ,}]+) *,? *");
536 string::const_iterator it = glob.begin();
537 string::const_iterator const end = glob.end();
539 match_results<string::const_iterator> what;
540 if (!regex_search(it, end, what, glob_re)) {
541 // Ensure that no information is lost.
542 pattern += string(it, end);
546 // Everything from the start of the input to
547 // the start of the match.
548 pattern += string(what[-1].first, what[-1].second);
550 // Given " *.{abc,def}", head == "*." and tail == "abc,def".
551 string const head = string(what[1].first, what[1].second);
552 string const tail = string(what[2].first, what[2].second);
554 // Split the ','-separated chunks of tail so that
555 // $head{$chunk1,$chunk2} becomes "$head$chunk1 $head$chunk2".
556 string const fmt = " " + head + "$1";
557 pattern += regex_replace(tail, block_re, fmt);
559 // Increment the iterator to the end of the match.
560 it += distance(it, what[0].second);
569 /* \param description text describing the filters.
570 * \param one or more wildcard patterns, separated by
573 Filter(docstring const & description, std::string const & globs);
575 docstring const & description() const { return desc_; }
577 QString toString() const;
580 std::vector<std::string> globs_;
584 Filter::Filter(docstring const & description, string const & globs)
587 // Given "<glob> <glob> ... *.{abc,def} <glob>", expand to
588 // "<glob> <glob> ... *.abc *.def <glob>"
589 string const expanded_globs = convert_brace_glob(globs);
591 // Split into individual globs.
592 globs_ = getVectorFromString(expanded_globs, " ");
596 QString Filter::toString() const
600 bool const has_description = !desc_.empty();
602 if (has_description) {
607 s += toqstr(getStringFromVector(globs_, " "));
615 /** \c FileFilterList parses a Qt-style list of available file filters
616 * to generate the corresponding vector.
617 * For example "TeX documents (*.tex);;LyX Documents (*.lyx)"
618 * will be parsed to fill a vector of size 2, whilst "*.{p[bgp]m} *.pdf"
619 * will result in a vector of size 1 in which the description field is empty.
621 struct FileFilterList
623 // FIXME UNICODE: globs_ should be unicode...
624 /** \param qt_style_filter a list of available file filters.
625 * Eg. "TeX documents (*.tex);;LyX Documents (*.lyx)".
626 * The "All files (*)" filter is always added to the list.
628 explicit FileFilterList(docstring const & qt_style_filter =
631 typedef std::vector<Filter>::size_type size_type;
633 bool empty() const { return filters_.empty(); }
634 size_type size() const { return filters_.size(); }
635 Filter & operator[](size_type i) { return filters_[i]; }
636 Filter const & operator[](size_type i) const { return filters_[i]; }
638 void parse_filter(std::string const & filter);
639 std::vector<Filter> filters_;
642 FileFilterList::FileFilterList(docstring const & qt_style_filter)
645 string const filter = to_utf8(qt_style_filter)
646 + (qt_style_filter.empty() ? string() : ";;")
647 + to_utf8(_("All Files")) + " " + fromqstr(wildcardAllFiles());
649 // Split data such as "TeX documents (*.tex);;LyX Documents (*.lyx)"
650 // into individual filters.
651 static regex const separator_re(";;");
653 string::const_iterator it = filter.begin();
654 string::const_iterator const end = filter.end();
656 match_results<string::const_iterator> what;
658 if (!regex_search(it, end, what, separator_re)) {
659 parse_filter(string(it, end));
663 // Everything from the start of the input to
664 // the start of the match.
665 parse_filter(string(it, what[0].first));
667 // Increment the iterator to the end of the match.
668 it += distance(it, what[0].second);
673 void FileFilterList::parse_filter(string const & filter)
675 // Matches "TeX documents (plain) (*.tex)",
676 // storing "TeX documents (plain) " as group 1 and "*.tex" as group 2.
677 static regex const filter_re("(.*)\\(([^()]+)\\) *$");
679 match_results<string::const_iterator> what;
680 if (!regex_search(filter, what, filter_re)) {
681 // Just a glob, no description.
682 filters_.push_back(Filter(docstring(), trim(filter)));
685 docstring const desc = from_utf8(string(what[1].first, what[1].second));
686 string const globs = string(what[2].first, what[2].second);
687 filters_.push_back(Filter(trim(desc), trim(globs)));
692 QString wildcardAllFiles()
702 /** \returns the equivalent of the string passed in
703 * although any brace expressions are expanded.
704 * (E.g. "*.{png,jpg}" -> "*.png *.jpg")
706 QStringList fileFilters(QString const & desc)
708 // we have: "*.{gif,png,jpg,bmp,pbm,ppm,tga,tif,xpm,xbm}"
709 // but need: "*.cpp;*.cc;*.C;*.cxx;*.c++"
710 FileFilterList filters(qstring_to_ucs4(desc));
711 //LYXERR0("DESC: " << desc);
713 for (size_t i = 0; i != filters.filters_.size(); ++i) {
714 QString f = filters.filters_[i].toString();
715 //LYXERR0("FILTER: " << f);
722 QString formatToolTip(QString text, int width)
724 // 1. QTooltip activates word wrapping only if mightBeRichText()
725 // is true. So we convert the text to rich text.
727 // 2. The default width is way too small. Setting the width is tricky; first
728 // one has to compute the ideal width, and then force it with special
731 // do nothing if empty or already formatted
732 if (text.isEmpty() || text.startsWith(QString("<html>")))
734 // Convert to rich text if it is not already
735 if (!Qt::mightBeRichText(text))
736 text = Qt::convertFromPlainText(text, Qt::WhiteSpaceNormal);
737 // Compute desired width in pixels
738 QFont const font = QToolTip::font();
739 #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
740 int const px_width = width * QFontMetrics(font).horizontalAdvance("M");
742 int const px_width = width * QFontMetrics(font).width("M");
744 // Determine the ideal width of the tooltip
745 QTextDocument td("");
747 td.setDefaultFont(QToolTip::font());
748 td.setDocumentMargin(0);
749 td.setTextWidth(px_width);
750 double best_width = td.idealWidth();
751 // Set the line wrapping with appropriate width
752 return QString("<html><body><table><tr>"
753 "<td align=justify width=%1>%2</td>"
754 "</tr></table></body></html>")
755 .arg(QString::number(int(best_width) + 1), text);
759 QString qtHtmlToPlainText(QString const & text)
761 if (!Qt::mightBeRichText(text))
765 return td.toPlainText();