1 /* Copyright (C) 2012 J.F.Dockes
2  *   This program is free software; you can redistribute it and/or modify
3  *   it under the terms of the GNU General Public License as published by
4  *   the Free Software Foundation; either version 2 of the License, or
5  *   (at your option) any later version.
6  *
7  *   This program is distributed in the hope that it will be useful,
8  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
9  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
10  *   GNU General Public License for more details.
11  *
12  *   You should have received a copy of the GNU General Public License
13  *   along with this program; if not, write to the
14  *   Free Software Foundation, Inc.,
15  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
16  */
17 #include "autoconfig.h"
18 
19 #include <stdio.h>
20 
21 #include <string>
22 #include <vector>
23 #include <sstream>
24 
25 #if defined(USING_WEBKIT)
26 #  include <QWebSettings>
27 #  include <QWebFrame>
28 #  include <QUrl>
29 #  define QWEBSETTINGS QWebSettings
30 #  define QWEBPAGE QWebPage
31 #elif defined(USING_WEBENGINE)
32 // Notes for WebEngine
33 // - All links must begin with http:// for acceptNavigationRequest to be
34 //   called.
35 // - The links passed to acceptNav.. have the host part
36 //   lowercased -> we change S0 to http://h/S0, not http://S0
37 #  include <QWebEnginePage>
38 #  include <QWebEngineSettings>
39 #  include <QtWebEngineWidgets>
40 #  define QWEBSETTINGS QWebEngineSettings
41 #  define QWEBPAGE QWebEnginePage
42 #else
43 #include <QTextBrowser>
44 #endif
45 
46 #include <QShortcut>
47 
48 #include "log.h"
49 #include "recoll.h"
50 #include "snippets_w.h"
51 #include "guiutils.h"
52 #include "rcldb.h"
53 #include "rclhelp.h"
54 #include "plaintorich.h"
55 #include "scbase.h"
56 
57 using namespace std;
58 
59 #if defined(USING_WEBKIT)
60 #define browser ((QWebView*)browserw)
61 #elif defined(USING_WEBENGINE)
62 #define browser ((QWebEngineView*)browserw)
63 #else
64 #define browser ((QTextBrowser*)browserw)
65 #endif
66 
67 class PlainToRichQtSnippets : public PlainToRich {
68 public:
startMatch(unsigned int)69     virtual string startMatch(unsigned int) {
70         return string("<span class='rclmatch' style='")
71             + qs2utf8s(prefs.qtermstyle) + string("'>");
72     }
endMatch()73     virtual string endMatch() {
74         return string("</span>");
75     }
76 };
77 static PlainToRichQtSnippets g_hiliter;
78 
init()79 void SnippetsW::init()
80 {
81     m_sortingByPage = prefs.snipwSortByPage;
82     QPushButton *searchButton = new QPushButton(tr("Search"));
83     searchButton->setAutoDefault(false);
84     buttonBox->addButton(searchButton, QDialogButtonBox::ActionRole);
85 //    setWindowFlags(Qt::WindowStaysOnTopHint);
86     searchFM->hide();
87 
88     onNewShortcuts();
89     connect(&SCBase::scBase(), SIGNAL(shortcutsChanged()),
90             this, SLOT(onNewShortcuts()));
91 
92     QPushButton *closeButton = buttonBox->button(QDialogButtonBox::Close);
93     if (closeButton)
94         connect(closeButton, SIGNAL(clicked()), this, SLOT(close()));
95     connect(searchButton, SIGNAL(clicked()), this, SLOT(slotEditFind()));
96     connect(searchLE, SIGNAL(textChanged(const QString&)),
97             this, SLOT(slotSearchTextChanged(const QString&)));
98     connect(nextPB, SIGNAL(clicked()), this, SLOT(slotEditFindNext()));
99     connect(prevPB, SIGNAL(clicked()), this, SLOT(slotEditFindPrevious()));
100 
101 
102     // Get rid of the placeholder widget created from the .ui
103     delete browserw;
104 #if defined(USING_WEBKIT)
105     browserw = new QWebView(this);
106     verticalLayout->insertWidget(0, browserw);
107     browser->setUrl(QUrl(QString::fromUtf8("about:blank")));
108     connect(browser, SIGNAL(linkClicked(const QUrl &)),
109             this, SLOT(onLinkClicked(const QUrl &)));
110     browser->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks);
111     browser->page()->currentFrame()->setScrollBarPolicy(Qt::Horizontal,
112                                                         Qt::ScrollBarAlwaysOff);
113     QWEBSETTINGS *ws = browser->page()->settings();
114     if (prefs.reslistfontfamily != "") {
115         ws->setFontFamily(QWEBSETTINGS::StandardFont, prefs.reslistfontfamily);
116         ws->setFontSize(QWEBSETTINGS::DefaultFontSize, prefs.reslistfontsize);
117     }
118     if (!prefs.snipCssFile.isEmpty())
119         ws->setUserStyleSheetUrl(QUrl::fromLocalFile(prefs.snipCssFile));
120     browserw->setContextMenuPolicy(Qt::CustomContextMenu);
121     connect(browserw, SIGNAL(customContextMenuRequested(const QPoint&)),
122             this, SLOT(createPopupMenu(const QPoint&)));
123 #elif defined(USING_WEBENGINE)
124     browserw = new QWebEngineView(this);
125     verticalLayout->insertWidget(0, browserw);
126     browser->setPage(new SnipWebPage(this));
127     QWEBSETTINGS *ws = browser->page()->settings();
128     if (prefs.reslistfontfamily != "") {
129         ws->setFontFamily(QWEBSETTINGS::StandardFont, prefs.reslistfontfamily);
130         ws->setFontSize(QWEBSETTINGS::DefaultFontSize, prefs.reslistfontsize);
131     }
132     // Stylesheet TBD
133     browserw->setContextMenuPolicy(Qt::CustomContextMenu);
134     connect(browserw, SIGNAL(customContextMenuRequested(const QPoint&)),
135             this, SLOT(createPopupMenu(const QPoint&)));
136 #else
137     browserw = new QTextBrowser(this);
138     verticalLayout->insertWidget(0, browserw);
139     connect(browser, SIGNAL(anchorClicked(const QUrl &)),
140             this, SLOT(onLinkClicked(const QUrl &)));
141     browser->setReadOnly(true);
142     browser->setUndoRedoEnabled(false);
143     browser->setOpenLinks(false);
144     browser->setTabChangesFocus(true);
145     if (prefs.reslistfontfamily != "") {
146         QFont nfont(prefs.reslistfontfamily, prefs.reslistfontsize);
147         browser->setFont(nfont);
148     } else {
149         browser->setFont(QFont());
150     }
151 #endif
152 }
153 
onNewShortcuts()154 void SnippetsW::onNewShortcuts()
155 {
156     SETSHORTCUT(this, "snippets:156", tr("Snippets Window"), tr("Find"),
157                 "Ctrl+F", m_find1sc, slotEditFind);
158     SETSHORTCUT(this, "snippets:158", tr("Snippets Window"), tr("Find (alt)"),
159                 "/", m_find2sc, slotEditFind);
160     SETSHORTCUT(this, "snippets:160", tr("Snippets Window"), tr("Find next"),
161                 "F3", m_findnextsc, slotEditFindNext);
162     SETSHORTCUT(this, "snippets:162", tr("Snippets Window"), tr("Find previous"),
163                 "Shift+F3", m_findprevsc, slotEditFindPrevious);
164     SETSHORTCUT(this, "snippets:164", tr("Snippets Window"), tr("Close window"),
165                 "Esc", m_hidesc, hide);
166 }
167 
listShortcuts()168 void SnippetsW::listShortcuts()
169 {
170     LISTSHORTCUT(this, "snippets:156", tr("Snippets Window"), tr("Find"),
171                  "Ctrl+F", m_find1sc, slotEditFind);
172     LISTSHORTCUT(this, "snippets:158", tr("Snippets Window"), tr("Find (alt)"),
173                  "/", m_find2sc, slotEditFind);
174     LISTSHORTCUT(this, "snippets:160",tr("Snippets Window"), tr("Find next"),
175                  "F3", m_find2sc, slotEditFindNext);
176     LISTSHORTCUT(this, "snippets:162",tr("Snippets Window"), tr("Find previous"),
177                  "Shift+F3", m_find2sc, slotEditFindPrevious);
178     LISTSHORTCUT(this, "snippets:164", tr("Snippets Window"), tr("Close window"),
179                  "Esc", m_hidesc, hide);
180 }
181 
createPopupMenu(const QPoint & pos)182 void SnippetsW::createPopupMenu(const QPoint& pos)
183 {
184     QMenu *popup = new QMenu(this);
185     if (m_sortingByPage) {
186         popup->addAction(tr("Sort By Relevance"), this,
187                          SLOT(reloadByRelevance()));
188     } else {
189         popup->addAction(tr("Sort By Page"), this, SLOT(reloadByPage()));
190     }
191     popup->popup(mapToGlobal(pos));
192 }
193 
reloadByRelevance()194 void SnippetsW::reloadByRelevance()
195 {
196     m_sortingByPage = false;
197     onSetDoc(m_doc, m_source);
198 }
reloadByPage()199 void SnippetsW::reloadByPage()
200 {
201     m_sortingByPage = true;
202     onSetDoc(m_doc, m_source);
203 }
204 
onSetDoc(Rcl::Doc doc,std::shared_ptr<DocSequence> source)205 void SnippetsW::onSetDoc(Rcl::Doc doc, std::shared_ptr<DocSequence> source)
206 {
207     m_doc = doc;
208     m_source = source;
209     if (!source)
210         return;
211 
212     // Make title out of file name if none yet
213     string titleOrFilename;
214     string utf8fn;
215     m_doc.getmeta(Rcl::Doc::keytt, &titleOrFilename);
216     m_doc.getmeta(Rcl::Doc::keyfn, &utf8fn);
217     if (titleOrFilename.empty()) {
218         titleOrFilename = utf8fn;
219     }
220     QString title("Recoll - Snippets");
221     if (!titleOrFilename.empty()) {
222         title += QString(" : ") + QString::fromUtf8(titleOrFilename.c_str());
223     }
224     setWindowTitle(title);
225 
226     vector<Rcl::Snippet> vpabs;
227     source->getAbstract(m_doc, vpabs, prefs.snipwMaxLength, m_sortingByPage);
228 
229     HighlightData hdata;
230     source->getTerms(hdata);
231 
232     ostringstream oss;
233     oss <<
234         "<html><head>"
235         "<meta http-equiv=\"content-type\" "
236         "content=\"text/html; charset=utf-8\">";
237 
238     oss << "<style type=\"text/css\">\nbody,table,select,input {\n";
239     oss << "color: " + qs2utf8s(prefs.fontcolor) + ";\n";
240     oss << "}\n</style>\n";
241     oss << qs2utf8s(prefs.darkreslistheadertext) << qs2utf8s(prefs.reslistheadertext);
242 
243     oss <<
244         "</head>"
245         "<body>"
246         "<table class=\"snippets\">"
247         ;
248 
249     g_hiliter.set_inputhtml(false);
250     bool nomatch = true;
251 
252     for (const auto& snippet : vpabs) {
253         if (snippet.page == -1) {
254             oss << "<tr><td colspan=\"2\">" <<
255                 snippet.snippet << "</td></tr>" << endl;
256             continue;
257         }
258         list<string> lr;
259         if (!g_hiliter.plaintorich(snippet.snippet, lr, hdata)) {
260             LOGDEB1("No match for [" << snippet.snippet << "]\n");
261             continue;
262         }
263         nomatch = false;
264         oss << "<tr><td>";
265         if (snippet.page > 0) {
266             oss << "<a href=\"http://h/P" << snippet.page << "T" <<
267                 snippet.term << "\">"
268                 << "P.&nbsp;" << snippet.page << "</a>";
269         }
270         oss << "</td><td>" << lr.front().c_str() << "</td></tr>" << endl;
271     }
272     oss << "</table>" << endl;
273     if (nomatch) {
274         oss.str("<html><head></head><body>\n");
275         oss << qs2utf8s(tr("<p>Sorry, no exact match was found within limits. "
276                            "Probably the document is very big and the snippets "
277                            "generator got lost in a maze...</p>"));
278     }
279     oss << "\n</body></html>";
280 #if defined(USING_WEBKIT) || defined(USING_WEBENGINE)
281     browser->setHtml(QString::fromUtf8(oss.str().c_str()));
282 #else
283     browser->clear();
284     browser->append(".");
285     browser->clear();
286     browser->insertHtml(QString::fromUtf8(oss.str().c_str()));
287     browser->moveCursor (QTextCursor::Start);
288     browser->ensureCursorVisible();
289 #endif
290     raise();
291 }
292 
slotEditFind()293 void SnippetsW::slotEditFind()
294 {
295     searchFM->show();
296     searchLE->selectAll();
297     searchLE->setFocus();
298 }
299 
slotEditFindNext()300 void SnippetsW::slotEditFindNext()
301 {
302     if (!searchFM->isVisible())
303         slotEditFind();
304 
305 #if defined(USING_WEBKIT)  || defined(USING_WEBENGINE)
306     browser->findText(searchLE->text());
307 #else
308     browser->find(searchLE->text());
309 #endif
310 
311 }
312 
slotEditFindPrevious()313 void SnippetsW::slotEditFindPrevious()
314 {
315     if (!searchFM->isVisible())
316         slotEditFind();
317 
318 #if defined(USING_WEBKIT) || defined(USING_WEBENGINE)
319     browser->findText(searchLE->text(), QWEBPAGE::FindBackward);
320 #else
321     browser->find(searchLE->text(), QTextDocument::FindBackward);
322 #endif
323 }
324 
slotSearchTextChanged(const QString & txt)325 void SnippetsW::slotSearchTextChanged(const QString& txt)
326 {
327 #if defined(USING_WEBKIT) || defined(USING_WEBENGINE)
328     browser->findText(txt);
329 #else
330     // Cursor thing is so that we don't go to the next occurrence with
331     // each character, but rather try to extend the current match
332     QTextCursor cursor = browser->textCursor();
333     cursor.setPosition(cursor.anchor(), QTextCursor::KeepAnchor);
334     browser->setTextCursor(cursor);
335     browser->find(txt, 0);
336 #endif
337 }
338 
onLinkClicked(const QUrl & url)339 void SnippetsW::onLinkClicked(const QUrl &url)
340 {
341     string ascurl = qs2u8s(url.toString()).substr(9);
342     LOGDEB("Snippets::onLinkClicked: [" << ascurl << "]\n");
343 
344     if (ascurl.size() > 3) {
345         int what = ascurl[0];
346         switch (what) {
347         case 'P':
348         {
349             string::size_type numpos = ascurl.find_first_of("0123456789");
350             if (numpos == string::npos)
351                 return;
352             int page = atoi(ascurl.c_str() + numpos);
353             string::size_type termpos = ascurl.find_first_of("T");
354             string term;
355             if (termpos != string::npos)
356                 term = ascurl.substr(termpos+1);
357             emit startNativeViewer(m_doc, page,
358                                    QString::fromUtf8(term.c_str()));
359             return;
360         }
361         }
362     }
363     LOGERR("Snippets::onLinkClicked: bad link [" << ascurl << "]\n");
364 }
365