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