1 /* Copyright (C) 2005-2020 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 "restable.h"
20 
21 #include <stdlib.h>
22 #include <time.h>
23 #include <stdint.h>
24 
25 #include <algorithm>
26 #include <memory>
27 #include <fstream>
28 
29 #include <Qt>
30 #include <QShortcut>
31 #include <QAbstractTableModel>
32 #include <QSettings>
33 #include <QMenu>
34 #include <QScrollBar>
35 #include <QStyledItemDelegate>
36 #include <QTextDocument>
37 #include <QPainter>
38 #include <QSplitter>
39 #include <QFileDialog>
40 #include <QMessageBox>
41 #include <QTimer>
42 #include <QKeyEvent>
43 #include <QClipboard>
44 #include <QToolTip>
45 
46 #include "recoll.h"
47 #include "docseq.h"
48 #include "log.h"
49 #include "guiutils.h"
50 #include "reslistpager.h"
51 #include "reslist.h"
52 #include "rclconfig.h"
53 #include "plaintorich.h"
54 #include "indexer.h"
55 #include "respopup.h"
56 #include "rclmain_w.h"
57 #include "multisave.h"
58 #include "appformime.h"
59 #include "transcode.h"
60 #include "scbase.h"
61 
62 static const QKeySequence quitKeySeq("Ctrl+q");
63 static const QKeySequence closeKeySeq("Ctrl+w");
64 
65 // Compensate for the default and somewhat bizarre vertical placement
66 // of text in cells
67 static const int ROWHEIGHTPAD = 2;
68 static const int TEXTINCELLVTRANS = -4;
69 
70 // Adjust font size from prefs, display is slightly different here
71 static const int fsadjustdetail = 1;
72 static const int fsadjusttable = 1;
73 
74 static PlainToRichQtReslist g_hiliter;
75 
76 static const char *settingskey_fieldlist="/Recoll/prefs/query/restableFields";
77 static const char *settingskey_fieldwiths="/Recoll/prefs/query/restableWidths";
78 static const char *settingskey_splittersizes="resTableSplitterSizes";
79 
80 //////////////////////////////////////////////////////////////////////////
81 // Restable "pager". We use it to print details for a document in the
82 // detail area
83 ///
84 class ResTablePager : public ResListPager {
85 public:
ResTablePager(ResTable * p)86     ResTablePager(ResTable *p)
87         : ResListPager(1, prefs.alwaysSnippets), m_parent(p)
88         {}
89     virtual bool append(const string& data) override;
90     virtual bool flush() override;
91     virtual string trans(const string& in) override;
92     virtual const string &parFormat() override;
absSep()93     virtual string absSep() override {
94         return (const char *)(prefs.abssep.toUtf8());}
headerContent()95     virtual string headerContent() override {
96         return qs2utf8s(prefs.darkreslistheadertext) + qs2utf8s(prefs.reslistheadertext);
97     }
98 private:
99     ResTable *m_parent;
100     string m_data;
101 };
102 
append(const string & data)103 bool ResTablePager::append(const string& data)
104 {
105     m_data += data;
106     return true;
107 }
108 
flush()109 bool ResTablePager::flush()
110 {
111 #ifdef helps_discoverability_of_shiftclick_but_is_ennoying
112     QString msg = QApplication::translate(
113         "ResTable", "Use Shift+click to display the text instead.");
114     if (!prefs.resTableTextNoShift) {
115         m_data += std::string("<p>") + qs2utf8s(msg) + "</p>";
116     }
117 #endif
118     m_parent->m_detail->setHtml(u8s2qs(m_data));
119     m_data = "";
120     return true;
121 }
122 
trans(const string & in)123 string ResTablePager::trans(const string& in)
124 {
125     return string((const char*)ResList::tr(in.c_str()).toUtf8());
126 }
127 
parFormat()128 const string& ResTablePager::parFormat()
129 {
130     return prefs.creslistformat;
131 }
132 
133 
134 /////////////////////////////////////////////////////////////////////////////
135 /// Detail text area methods
136 
ResTableDetailArea(ResTable * parent)137 ResTableDetailArea::ResTableDetailArea(ResTable* parent)
138     : QTextBrowser(parent), m_table(parent)
139 {
140     setContextMenuPolicy(Qt::CustomContextMenu);
141     connect(this, SIGNAL(customContextMenuRequested(const QPoint&)),
142             this, SLOT(createPopupMenu(const QPoint&)));
143 }
144 
createPopupMenu(const QPoint & pos)145 void ResTableDetailArea::createPopupMenu(const QPoint& pos)
146 {
147     if (m_table && m_table->m_model && m_table->m_detaildocnum >= 0) {
148         int opts = m_table->m_ismainres ? ResultPopup::showExpand : 0;
149         opts |= ResultPopup::showSaveOne;
150         QMenu *popup = ResultPopup::create(
151             m_table, opts, m_table->m_model->getDocSource(), m_table->m_detaildoc);
152         popup->popup(mapToGlobal(pos));
153     }
154 }
155 
setFont()156 void ResTableDetailArea::setFont()
157 {
158     int fs = prefs.reslistfontsize;
159     // fs shows slightly bigger in qtextbrowser? adjust.
160     if (prefs.reslistfontsize > fsadjustdetail) {
161         fs -= fsadjustdetail;
162     }
163     if (prefs.reslistfontfamily != "") {
164         QFont nfont(prefs.reslistfontfamily, fs);
165         QTextBrowser::setFont(nfont);
166     } else {
167         QFont font;
168         font.setPointSize(fs);
169         QTextBrowser::setFont(font);
170     }
171 }
172 
init()173 void ResTableDetailArea::init()
174 {
175     setFont();
176     QTextBrowser::setHtml("");
177 }
178 
179 //////////////////////////////////////////////////////////////////////////////
180 //// Data model methods
181 ////
182 
183 // Routines used to extract named data from an Rcl::Doc. The basic one
184 // just uses the meta map. Others (ie: the date ones) need to do a
185 // little processing
gengetter(const string & fld,const Rcl::Doc & doc)186 static string gengetter(const string& fld, const Rcl::Doc& doc)
187 {
188     const auto it = doc.meta.find(fld);
189     if (it == doc.meta.end()) {
190         return string();
191     }
192     return it->second;
193 }
194 
sizegetter(const string & fld,const Rcl::Doc & doc)195 static string sizegetter(const string& fld, const Rcl::Doc& doc)
196 {
197     const auto it = doc.meta.find(fld);
198     if (it == doc.meta.end()) {
199         return string();
200     }
201     int64_t size = atoll(it->second.c_str());
202     return displayableBytes(size) + " (" + it->second + ")";
203 }
204 
dategetter(const string &,const Rcl::Doc & doc)205 static string dategetter(const string&, const Rcl::Doc& doc)
206 {
207     string sdate;
208     if (!doc.dmtime.empty() || !doc.fmtime.empty()) {
209         time_t mtime = doc.dmtime.empty() ?
210             atoll(doc.fmtime.c_str()) : atoll(doc.dmtime.c_str());
211         struct tm *tm = localtime(&mtime);
212         sdate = utf8datestring("%Y-%m-%d", tm);
213     }
214     return sdate;
215 }
216 
datetimegetter(const string &,const Rcl::Doc & doc)217 static string datetimegetter(const string&, const Rcl::Doc& doc)
218 {
219     string datebuf;
220     if (!doc.dmtime.empty() || !doc.fmtime.empty()) {
221         time_t mtime = doc.dmtime.empty() ?
222             atoll(doc.fmtime.c_str()) : atoll(doc.dmtime.c_str());
223         struct tm *tm = localtime(&mtime);
224         // Can't use reslistdateformat because it's html (&nbsp; etc.)
225         datebuf = utf8datestring("%Y-%m-%d %H:%M:%S", tm);
226     }
227     return datebuf;
228 }
229 
230 // Static map to translate from internal column names to displayable ones
231 map<string, QString> RecollModel::o_displayableFields;
232 
chooseGetter(const string & field)233 FieldGetter *RecollModel::chooseGetter(const string& field)
234 {
235     if (!stringlowercmp("date", field))
236         return dategetter;
237     else if (!stringlowercmp("datetime", field))
238         return datetimegetter;
239     else if (!stringlowercmp("bytes", field.substr(1)))
240         return sizegetter;
241     else
242         return gengetter;
243 }
244 
baseField(const string & field)245 string RecollModel::baseField(const string& field)
246 {
247     if (!stringlowercmp("date", field) || !stringlowercmp("datetime", field))
248         return "mtime";
249     else
250         return field;
251 }
252 
RecollModel(const QStringList fields,ResTable * tb,QObject * parent)253 RecollModel::RecollModel(const QStringList fields, ResTable *tb,
254                          QObject *parent)
255     : QAbstractTableModel(parent), m_table(tb), m_ignoreSort(false)
256 {
257     // Initialize the translated map for column headers
258     o_displayableFields["abstract"] = tr("Abstract");
259     o_displayableFields["author"] = tr("Author");
260     o_displayableFields["dbytes"] = tr("Document size");
261     o_displayableFields["dmtime"] = tr("Document date");
262     o_displayableFields["fbytes"] = tr("File size");
263     o_displayableFields["filename"] = tr("File name");
264     o_displayableFields["fmtime"] = tr("File date");
265     o_displayableFields["ipath"] = tr("Ipath");
266     o_displayableFields["keywords"] = tr("Keywords");
267     o_displayableFields["mtype"] = tr("MIME type");
268     o_displayableFields["origcharset"] = tr("Original character set");
269     o_displayableFields["relevancyrating"] = tr("Relevancy rating");
270     o_displayableFields["title"] = tr("Title");
271     o_displayableFields["url"] = tr("URL");
272     o_displayableFields["mtime"] = tr("Date");
273     o_displayableFields["date"] = tr("Date");
274     o_displayableFields["datetime"] = tr("Date and time");
275 
276     // Add dynamic "stored" fields to the full column list. This
277     // could be protected to be done only once, but it's no real
278     // problem
279     if (theconfig) {
280         const auto& stored = theconfig->getStoredFields();
281         for (const auto& field : stored) {
282             if (o_displayableFields.find(field) == o_displayableFields.end()) {
283                 o_displayableFields[field] = u8s2qs(field);
284             }
285         }
286     }
287 
288     // Construct the actual list of column names
289     for (QStringList::const_iterator it = fields.begin();
290          it != fields.end(); it++) {
291         m_fields.push_back((const char *)(it->toUtf8()));
292         m_getters.push_back(chooseGetter(m_fields.back()));
293     }
294 
295     g_hiliter.set_inputhtml(false);
296 }
297 
rowCount(const QModelIndex &) const298 int RecollModel::rowCount(const QModelIndex&) const
299 {
300     LOGDEB2("RecollModel::rowCount\n");
301     if (!m_source)
302         return 0;
303     return m_source->getResCnt();
304 }
305 
columnCount(const QModelIndex &) const306 int RecollModel::columnCount(const QModelIndex&) const
307 {
308     LOGDEB2("RecollModel::columnCount\n");
309     return m_fields.size();
310 }
311 
readDocSource()312 void RecollModel::readDocSource()
313 {
314     LOGDEB("RecollModel::readDocSource()\n");
315     beginResetModel();
316     endResetModel();
317 }
318 
setDocSource(std::shared_ptr<DocSequence> nsource)319 void RecollModel::setDocSource(std::shared_ptr<DocSequence> nsource)
320 {
321     LOGDEB("RecollModel::setDocSource\n");
322     m_rowforcachedoc = -1;
323     if (!nsource) {
324         m_source = std::shared_ptr<DocSequence>();
325     } else {
326         // We used to allocate a new DocSource here instead of sharing
327         // the input, but I can't see why.
328         m_source = nsource;
329         m_hdata.clear();
330     }
331 }
332 
deleteColumn(int col)333 void RecollModel::deleteColumn(int col)
334 {
335     if (col > 0 && col < int(m_fields.size())) {
336         vector<string>::iterator it = m_fields.begin();
337         it += col;
338         m_fields.erase(it);
339         vector<FieldGetter*>::iterator it1 = m_getters.begin();
340         it1 += col;
341         m_getters.erase(it1);
342         readDocSource();
343     }
344 }
345 
addColumn(int col,const string & field)346 void RecollModel::addColumn(int col, const string& field)
347 {
348     LOGDEB("AddColumn: col " << col << " fld ["  << field << "]\n");
349     if (col >= 0 && col < int(m_fields.size())) {
350         col++;
351         vector<string>::iterator it = m_fields.begin();
352         vector<FieldGetter*>::iterator it1 = m_getters.begin();
353         if (col) {
354             it += col;
355             it1 += col;
356         }
357         m_fields.insert(it, field);
358         m_getters.insert(it1, chooseGetter(field));
359         readDocSource();
360     }
361 }
362 
displayableField(const std::string & in)363 QString RecollModel::displayableField(const std::string& in)
364 {
365     const auto it = o_displayableFields.find(in);
366     return (it == o_displayableFields.end()) ? u8s2qs(in) : it->second;
367 }
368 
headerData(int idx,Qt::Orientation orientation,int role) const369 QVariant RecollModel::headerData(int idx, Qt::Orientation orientation,
370                                  int role) const
371 {
372     LOGDEB2("RecollModel::headerData: idx " << idx << " orientation " <<
373             (orientation == Qt::Vertical ? "vertical":"horizontal") <<
374             " role " << role << "\n");
375     if (orientation == Qt::Vertical && role == Qt::DisplayRole) {
376         if (idx < 26) {
377             return QString("%1/%2").arg(idx).arg(char('a'+idx));
378         } else {
379             return idx;
380         }
381     }
382     if (orientation == Qt::Horizontal && role == Qt::DisplayRole &&
383         idx < int(m_fields.size())) {
384         return displayableField(m_fields[idx]);
385     }
386     return QVariant();
387 }
388 
data(const QModelIndex & index,int role) const389 QVariant RecollModel::data(const QModelIndex& index, int role) const
390 {
391     LOGDEB2("RecollModel::data: row " << index.row() << " col " <<
392             index.column() << " role " << role << "\n");
393 
394     // The font is actually set in the custom delegate, but we need
395     // this to adjust the row height (there is probably a better way
396     // to do it in the delegate?)
397     if (role == Qt::FontRole && prefs.reslistfontsize > 0) {
398         if (m_reslfntszforcached != prefs.reslistfontsize) {
399             m_reslfntszforcached = prefs.reslistfontsize;
400             m_table->setDefRowHeight();
401             m_cachedfont = m_table->font();
402             int fs = prefs.reslistfontsize <= fsadjusttable ?
403                 prefs.reslistfontsize: prefs.reslistfontsize - fsadjusttable;
404             if (fs > 0)
405                 m_cachedfont.setPointSize(fs);
406         }
407         return m_cachedfont;
408     }
409 
410     // Note that, because we use a style sheet, there is no way to dynamically set the background
411     // color. See: https://forum.qt.io/topic/95940/model-backgroundrole-overridden-by-style-sheet/
412     // https://bugreports.qt.io/browse/QTBUG-70100
413 
414     if (!m_source || role != Qt::DisplayRole || !index.isValid() ||
415         index.column() >= int(m_fields.size())) {
416         return QVariant();
417     }
418 
419     if (m_rowforcachedoc != index.row()) {
420         m_rowforcachedoc = index.row();
421         m_cachedoc = Rcl::Doc();
422         if (!m_source->getDoc(index.row(), m_cachedoc)) {
423             return QVariant();
424         }
425     }
426 
427     string colname = m_fields[index.column()];
428 
429     string data = m_getters[index.column()](colname, m_cachedoc);
430 
431 #ifndef _WIN32
432     // Special case url, because it may not be utf-8. URL-encode in this case.
433     // Not on windows, where we always read the paths as Unicode.
434     if (!colname.compare("url")) {
435         int ecnt;
436         string data1;
437         if (!transcode(data, data1, "UTF-8", "UTF-8", &ecnt) || ecnt > 0) {
438             data = url_encode(data, 7);
439         }
440     }
441 #endif
442 
443     list<string> lr;
444     g_hiliter.plaintorich(data, lr, m_hdata);
445     return u8s2qs(lr.front());
446 }
447 
saveAsCSV(std::fstream & fp)448 void RecollModel::saveAsCSV(std::fstream& fp)
449 {
450     if (!m_source)
451         return;
452 
453     int cols = columnCount();
454     int rows = rowCount();
455     vector<string> tokens;
456 
457     for (int col = 0; col < cols; col++) {
458         QString qs = headerData(col, Qt::Horizontal,Qt::DisplayRole).toString();
459         tokens.push_back((const char *)qs.toUtf8());
460     }
461     string csv;
462     stringsToCSV(tokens, csv);
463     fp << csv << "\n";
464     tokens.clear();
465 
466     for (int row = 0; row < rows; row++) {
467         Rcl::Doc doc;
468         if (!m_source->getDoc(row, doc)) {
469             continue;
470         }
471         for (int col = 0; col < cols; col++) {
472             tokens.push_back(m_getters[col](m_fields[col], doc));
473         }
474         stringsToCSV(tokens, csv);
475         fp << csv << "\n";
476         tokens.clear();
477     }
478 }
479 
480 // This gets called when the column headers are clicked
sort(int column,Qt::SortOrder order)481 void RecollModel::sort(int column, Qt::SortOrder order)
482 {
483     if (m_ignoreSort) {
484         return;
485     }
486     LOGDEB("RecollModel::sort(" << column << ", " << order << ")\n");
487 
488     DocSeqSortSpec spec;
489     if (column >= 0 && column < int(m_fields.size())) {
490         spec.field = m_fields[column];
491         if (!stringlowercmp("relevancyrating", spec.field) &&
492             order != Qt::AscendingOrder) {
493             QMessageBox::warning(0, "Recoll",
494                                  tr("Can't sort by inverse relevance"));
495             QTimer::singleShot(0, m_table, SLOT(resetSort()));
496             return;
497         }
498         if (!stringlowercmp("date", spec.field) ||
499             !stringlowercmp("datetime", spec.field))
500             spec.field = "mtime";
501         spec.desc = order == Qt::AscendingOrder ? false : true;
502     }
503     emit sortDataChanged(spec);
504 }
505 
506 ///////////////////////////
507 // ResTable panel methods
508 
509 // We use a custom delegate to display the cells because the base
510 // tableview's can't handle rich text to highlight the match terms
511 class ResTableDelegate: public QStyledItemDelegate {
512 public:
ResTableDelegate(QObject * parent)513     ResTableDelegate(QObject *parent) : QStyledItemDelegate(parent) {}
514 
515     // We might want to optimize by passing the data to the base
516     // method if the text does not contain any term matches. Would
517     // need a modif to plaintorich to return the match count (easy),
518     // and a way to pass an indicator from data(), a bit more
519     // difficult. Anyway, the display seems fast enough as is.
paint(QPainter * painter,const QStyleOptionViewItem & option,const QModelIndex & index) const520     void paint(QPainter *painter, const QStyleOptionViewItem &option,
521                const QModelIndex &index) const {
522 
523         QVariant value = index.data(Qt::DisplayRole);
524         QString text;
525         if (value.isValid() && !value.isNull()) {
526             text = value.toString();
527         }
528         if (text.isEmpty()) {
529             QStyledItemDelegate::paint(painter, option, index);
530             return;
531         }
532 
533         QStyleOptionViewItem opt = option;
534         initStyleOption(&opt, index);
535 
536         painter->save();
537 
538         /* As we draw with a text document, not the normal tableview
539            painter, we need to retrieve the appropriate colors and set
540            them as HTML styles. */
541         QString color = opt.palette.color(QPalette::Base).name();
542         QString textcolor = opt.palette.color(QPalette::Text).name();
543         QString selcolor = opt.palette.color(QPalette::Highlight).name();
544         QString seltextcolor =
545             opt.palette.color(QPalette::HighlightedText).name();
546         QString fstyle;
547         if (prefs.reslistfontsize > 0) {
548             int fs = prefs.reslistfontsize <= fsadjusttable ?
549                 prefs.reslistfontsize : prefs.reslistfontsize - fsadjusttable;
550             fstyle = QString("font-size: %1pt").arg(fs);
551         }
552         QString ntxt("<div style='");
553         ntxt += " color:";
554         ntxt += (opt.state & QStyle::State_Selected)? seltextcolor:textcolor;
555         ntxt += ";";
556         ntxt += " background:";
557         ntxt += (opt.state & QStyle::State_Selected)? selcolor:color;
558         ntxt += ";";
559         ntxt += fstyle;
560         ntxt += QString("'>") + text + QString("</div>");
561         text.swap(ntxt);
562 
563         painter->setClipRect(opt.rect);
564         QPoint where = option.rect.topLeft();
565         where.ry() += TEXTINCELLVTRANS;
566         painter->translate(where);
567         QTextDocument document;
568         document.setHtml(text);
569         document.drawContents(painter);
570         painter->restore();
571     }
572 };
573 
setDefRowHeight()574 void ResTable::setDefRowHeight()
575 {
576     QHeaderView *header = tableView->verticalHeader();
577     if (header) {
578         // Don't do this: it forces a query on the whole model (all
579         // docs) to compute the height. No idea why this was needed,
580         // things seem to work ok without it. The row height does not
581         // shrink when the font is reduced, but I'm not sure that it
582         // worked before.
583 //        header->setSectionResizeMode(QHeaderView::ResizeToContents);
584         // Compute ourselves instead, for one row.
585         QFont font = tableView->font();
586         int fs = prefs.reslistfontsize <= fsadjusttable ?
587             prefs.reslistfontsize : prefs.reslistfontsize - fsadjusttable;
588         if (fs > 0)
589             font.setPointSize(fs);
590         QFontMetrics fm(font);
591         header->setDefaultSectionSize(fm.height() + ROWHEIGHTPAD);
592         header->setSectionResizeMode(QHeaderView::Fixed);
593     }
594 }
595 
init()596 void ResTable::init()
597 {
598     QSettings settings;
599     auto restableFields = settings.value(settingskey_fieldlist).toStringList();
600     if (restableFields.empty()) {
601         restableFields.push_back("date");
602         restableFields.push_back("title");
603         restableFields.push_back("filename");
604         restableFields.push_back("author");
605         restableFields.push_back("url");
606     }
607     if (!(m_model = new RecollModel(restableFields, this)))
608         return;
609     tableView->setModel(m_model);
610     tableView->setMouseTracking(true);
611     tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
612     tableView->setItemDelegate(new ResTableDelegate(this));
613     tableView->setContextMenuPolicy(Qt::CustomContextMenu);
614     tableView->setAlternatingRowColors(true);
615 
616     onNewShortcuts();
617     connect(&SCBase::scBase(), SIGNAL(shortcutsChanged()),
618             this, SLOT(onNewShortcuts()));
619 
620     auto sc = new QShortcut(QKeySequence(Qt::Key_Escape), this);
621     connect(sc, SIGNAL(activated()),
622             tableView->selectionModel(), SLOT(clear()));
623 
624     connect(tableView, SIGNAL(customContextMenuRequested(const QPoint&)),
625             this, SLOT(createPopupMenu(const QPoint&)));
626 
627     QHeaderView *header = tableView->horizontalHeader();
628     if (header) {
629         QString qw = settings.value(settingskey_fieldwiths).toString();
630         vector<string> vw;
631         stringToStrings(qs2utf8s(qw), vw);
632         vector<int> restableColWidths;
633         for (const auto& w : vw) {
634             restableColWidths.push_back(atoi(w.c_str()));
635         }
636         if (int(restableColWidths.size()) == header->count()) {
637             for (int i = 0; i < header->count(); i++) {
638                 header->resizeSection(i, restableColWidths[i]);
639             }
640         }
641         header->setSortIndicatorShown(true);
642         header->setSortIndicator(-1, Qt::AscendingOrder);
643         header->setContextMenuPolicy(Qt::CustomContextMenu);
644         header->setStretchLastSection(1);
645         connect(header, SIGNAL(sectionResized(int,int,int)),
646                 this, SLOT(saveColState()));
647         connect(header, SIGNAL(customContextMenuRequested(const QPoint&)),
648                 this, SLOT(createHeaderPopupMenu(const QPoint&)));
649     }
650 #if (QT_VERSION >= QT_VERSION_CHECK(5, 0, 0))
651     header->setSectionsMovable(true);
652 #else
653     header->setMovable(true);
654 #endif
655     setDefRowHeight();
656 
657     connect(tableView->selectionModel(),
658             SIGNAL(currentChanged(const QModelIndex&, const QModelIndex &)),
659             this, SLOT(onTableView_currentChanged(const QModelIndex&)));
660     connect(tableView, SIGNAL(doubleClicked(const QModelIndex&)),
661             this, SLOT(onDoubleClick(const QModelIndex&)));
662     connect(tableView, SIGNAL(clicked(const QModelIndex&)),
663             this, SLOT(onClicked(const QModelIndex&)));
664 
665     m_pager = new ResTablePager(this);
666     m_pager->setHighLighter(&g_hiliter);
667 
668     deleteZ(textBrowser);
669     m_detail = new ResTableDetailArea(this);
670     m_detail->setReadOnly(true);
671     m_detail->setUndoRedoEnabled(false);
672     m_detail->setOpenLinks(false);
673     m_detail->init();
674     // signals and slots connections
675     connect(m_detail, SIGNAL(anchorClicked(const QUrl&)), this, SLOT(linkWasClicked(const QUrl&)));
676     splitter->addWidget(m_detail);
677     splitter->setOrientation(Qt::Vertical);
678     QVariant saved = settings.value(settingskey_splittersizes);
679     if (saved != QVariant()) {
680         splitter->restoreState(saved.toByteArray());
681     } else {
682         QList<int> sizes;
683         sizes << 355 << 125;
684         splitter->setSizes(sizes);
685     }
686     installEventFilter(this);
687     onUiPrefsChanged();
688 }
689 
onNewShortcuts()690 void ResTable::onNewShortcuts()
691 {
692     if (prefs.noResTableRowJumpSC) {
693         for (auto& lnk : m_rowlinks) {
694             delete lnk;
695         }
696         m_rowlinks.clear();
697         for (auto& sc : m_rowsc) {
698             delete sc;
699         }
700         m_rowsc.clear();
701     } else if (m_rowlinks.empty()) {
702         // Set "go to row" accelerator shortcuts. letter or digit for 0-9,
703         // then letter up to 25
704         std::function<void(int)> setrow =
705             std::bind(&ResTable::setCurrentRowFromKbd, this, std::placeholders::_1);
706         for (int i = 0; i <= 25; i++) {
707             auto qs = QString("Ctrl+Shift+%1").arg(char('a'+i));
708             auto sc = new QShortcut(QKeySequence(qs2utf8s(qs).c_str()), this);
709             m_rowlinks.push_back(new SCData(this, setrow, i));
710             m_rowsc.push_back(sc);
711             connect(sc, SIGNAL(activated()), m_rowlinks.back(), SLOT(activate()));
712             if (i > 9)
713                 continue;
714             qs = QString("Ctrl+%1").arg(i);
715             sc = new QShortcut(QKeySequence(qs2utf8s(qs).c_str()), this);
716             m_rowsc.push_back(sc);
717             m_rowlinks.push_back(new SCData(this, setrow, i));
718             connect(sc, SIGNAL(activated()), m_rowlinks.back(), SLOT(activate()));
719         }
720     }
721     SETSHORTCUT(this, "restable:704", tr("Result Table"),
722                 tr("Open current result document"),"Ctrl+O", m_opensc, menuEdit);
723     SETSHORTCUT(this, "restable:706", tr("Result Table"),
724                 tr("Open current result and quit"),
725                 "Ctrl+Alt+Shift+O", m_openquitsc, menuEditAndQuit);
726     SETSHORTCUT(this, "restable:709", tr("Result Table"), tr("Preview"),
727                 "Ctrl+D", m_previewsc, menuPreview);
728     SETSHORTCUT(this, "restable:711", tr("Result Table"), tr("Show snippets"),
729                 "Ctrl+E", m_showsnipssc, menuShowSnippets);
730     SETSHORTCUT(this, "restable:713", tr("Result Table"), tr("Show header"),
731                 "Ctrl+H", m_showheadersc, toggleHeader);
732     SETSHORTCUT(this, "restable:715", tr("Result Table"),
733                 tr("Show vertical header"),
734                 "Ctrl+V", m_showvheadersc, toggleVHeader);
735     SETSHORTCUT(this, "restable:718", tr("Result Table"),
736                 tr("Copy current result text to clipboard"),
737                 "Ctrl+G", m_copycurtextsc, menuCopyText);
738     SETSHORTCUT(this, "restable:734", tr("Result Table"),
739                 tr("Copy result text and quit"),
740                 "Ctrl+Alt+Shift+G", m_copycurtextquitsc, menuCopyTextAndQuit);
741     std::vector<QShortcut*> scps={
742         m_opensc, m_openquitsc, m_previewsc, m_showsnipssc, m_showheadersc,
743         m_showvheadersc, m_copycurtextsc, m_copycurtextquitsc};
744     for (auto& scp : scps) {
745         scp->setContext(Qt::WidgetWithChildrenShortcut);
746     }
747 }
748 
eventFilter(QObject *,QEvent * event)749 bool ResTable::eventFilter(QObject*, QEvent *event)
750 {
751     if (event->type() == QEvent::KeyPress) {
752         QKeyEvent* key = static_cast<QKeyEvent*>(event);
753         if ((key->key() == Qt::Key_Enter) || (key->key() == Qt::Key_Return)) {
754             menuEdit();
755             return true;
756         }
757     }
758     return false;
759 }
760 
setRclMain(RclMain * m,bool ismain)761 void ResTable::setRclMain(RclMain *m, bool ismain)
762 {
763     m_rclmain = m;
764     m_ismainres = ismain;
765 
766     // We allow single selection only in the main table because this
767     // may have a mix of file-level docs and subdocs and multisave
768     // only works for subdocs
769     if (m_ismainres)
770         tableView->setSelectionMode(QAbstractItemView::SingleSelection);
771     else
772         tableView->setSelectionMode(QAbstractItemView::ExtendedSelection);
773 
774     if (!m_ismainres) {
775         // Don't set this shortcut when we are a child of main, would be duplicate/ambiguous
776         connect(new QShortcut(quitKeySeq, this), SIGNAL(activated()), m_rclmain, SLOT (fileExit()));
777     }
778 
779     new QShortcut(closeKeySeq, this, SLOT (close()));
780     connect(this, SIGNAL(previewRequested(Rcl::Doc)), m_rclmain, SLOT(startPreview(Rcl::Doc)));
781     connect(this, SIGNAL(editRequested(Rcl::Doc)), m_rclmain, SLOT(startNativeViewer(Rcl::Doc)));
782     connect(this, SIGNAL(docSaveToFileClicked(Rcl::Doc)), m_rclmain, SLOT(saveDocToFile(Rcl::Doc)));
783     connect(this, SIGNAL(showSnippets(Rcl::Doc)), m_rclmain, SLOT(showSnippets(Rcl::Doc)));
784 }
785 
toggleHeader()786 void ResTable::toggleHeader()
787 {
788     if (tableView->horizontalHeader()->isVisible()) {
789         prefs.noResTableHeader = true;
790         tableView->horizontalHeader()->hide();
791     } else {
792         prefs.noResTableHeader = false;
793         tableView->horizontalHeader()->show();
794     }
795 }
796 
toggleVHeader()797 void ResTable::toggleVHeader()
798 {
799     if (tableView->verticalHeader()->isVisible()) {
800         prefs.showResTableVHeader = false;
801         tableView->verticalHeader()->hide();
802     } else {
803         prefs.showResTableVHeader = true;
804         tableView->verticalHeader()->show();
805     }
806 }
807 
onUiPrefsChanged()808 void ResTable::onUiPrefsChanged()
809 {
810     if (m_detail) {
811         m_detail->setFont();
812     }
813     auto index = tableView->indexAt(QPoint(0, 0));
814     // There may be a better way to force repainting all visible rows
815     // with the possibly new font, but this works...
816     tableView->setAlternatingRowColors(false);
817     tableView->setAlternatingRowColors(true);
818     makeRowVisible(index.row());
819     if (prefs.noResTableHeader) {
820         tableView->horizontalHeader()->hide();
821     } else {
822         tableView->horizontalHeader()->show();
823     }
824     if (prefs.showResTableVHeader) {
825         tableView->verticalHeader()->show();
826     } else {
827         tableView->verticalHeader()->hide();
828     }
829 }
830 
setCurrentRowFromKbd(int row)831 void ResTable::setCurrentRowFromKbd(int row)
832 {
833     LOGDEB1("setCurrentRowFromKbd: " << row << "\n");
834     m_rowchangefromkbd = true;
835     tableView->setFocus(Qt::ShortcutFocusReason);
836 
837     // After calling setCurrentIndex(), currentChanged() gets called
838     // twice, once with row 0 and no selection, once with the actual
839     // target row and selection set. It uses this fact to discriminate
840     // this from hovering. For some reason, when row is zero, there is
841     // only one call. So, in this case, we first select row 1, and
842     // this so pretty hack gets things working
843     if (row == 0) {
844         tableView->selectionModel()->setCurrentIndex(
845             m_model->index(1, 0),
846             QItemSelectionModel::ClearAndSelect|QItemSelectionModel::Rows);
847     }
848     tableView->selectionModel()->setCurrentIndex(
849         m_model->index(row, 0),
850         QItemSelectionModel::ClearAndSelect|QItemSelectionModel::Rows);
851 }
852 
getDetailDocNumOrTopRow()853 int ResTable::getDetailDocNumOrTopRow()
854 {
855     if (m_detaildocnum >= 0)
856         return m_detaildocnum;
857     QModelIndex modelIndex = tableView->indexAt(QPoint(0, 0));
858     return modelIndex.row();
859 }
860 
makeRowVisible(int row)861 void ResTable::makeRowVisible(int row)
862 {
863     LOGDEB("ResTable::showRow(" << row << ")\n");
864     QModelIndex modelIndex = m_model->index(row, 0);
865     tableView->scrollTo(modelIndex, QAbstractItemView::PositionAtTop);
866     tableView->selectionModel()->clear();
867     m_detail->init();
868     m_detaildocnum = -1;
869 }
870 
871 // This is called by rclmain_w prior to exiting
saveColState()872 void ResTable::saveColState()
873 {
874     if (!m_ismainres)
875         return;
876     QSettings settings;
877     settings.setValue(settingskey_splittersizes, splitter->saveState());
878 
879     QHeaderView *header = tableView->horizontalHeader();
880     const vector<string>& vf = m_model->getFields();
881     if (!header) {
882         LOGERR("ResTable::saveColState: no table header ??\n");
883         return;
884     }
885 
886     // Remember the current column order. Walk in visual order and
887     // create new list
888     QStringList newfields;
889     QString newwidths;
890     for (int vi = 0; vi < header->count(); vi++) {
891         int li = header->logicalIndex(vi);
892         if (li < 0 || li >= int(vf.size())) {
893             LOGERR("saveColState: logical index beyond list size!\n");
894             continue;
895         }
896         newfields.push_back(u8s2qs(vf[li]));
897         newwidths += QString().setNum(header->sectionSize(li)) + QString(" ");
898     }
899     settings.setValue(settingskey_fieldlist, newfields);
900     settings.setValue(settingskey_fieldwiths, newwidths);
901 }
902 
onTableView_currentChanged(const QModelIndex & index)903 void ResTable::onTableView_currentChanged(const QModelIndex& index)
904 {
905     bool hasselection = tableView->selectionModel()->hasSelection();
906     LOGDEB2("ResTable::onTableView_currentChanged(" << index.row() << ", " <<
907         index.column() << ") from kbd " << m_rowchangefromkbd  << " hasselection " <<
908             hasselection << "\n");
909 
910     if (!m_model || !m_model->getDocSource())
911         return;
912     Rcl::Doc doc;
913     if (!m_model->getDocSource()->getDoc(index.row(), doc)) {
914         m_detaildocnum = -1;
915         return;
916     }
917 
918     m_detail->init();
919     m_detaildocnum = index.row();
920     m_detaildoc = doc;
921     bool isShift = (QApplication::keyboardModifiers() & Qt::ShiftModifier);
922     bool showcontent{false};
923     bool showmeta{false};
924 
925     if (m_rowchangefromkbd) {
926         // Ctrl+... jump to row. Show text/meta as for simple click
927         if (hasselection) {
928             // When getting here from ctrl+... we get called twice, once with row 0
929             // and no selection, once with the actual row and selection. Only
930             // reset fromkbd and set showcontent in the second case.
931             m_rowchangefromkbd = false;
932             showcontent = prefs.resTableTextNoShift;
933         }
934     } else {
935         // Mouse click. Show text or meta depending on shift key. Never show text when hovering
936         // (no selection).
937         showcontent = hasselection && (isShift ^ prefs.resTableTextNoShift);
938     }
939     if (!showcontent) {
940         showmeta = hasselection || !prefs.resTableNoHoverMeta;
941     }
942 
943     bool displaydone{false};
944 
945     if (showcontent) {
946         // If it's an image, and simply stored in a file, display it. We don't go to the trouble of
947         // extracting an embedded image here.
948         if (mimeIsImage(m_detaildoc.mimetype) && m_detaildoc.ipath.empty()) {
949             auto image = QImage(fileurltolocalpath(m_detaildoc.url).c_str());
950             if (!image.isNull()) {
951                 m_detail->setPlainText("");
952                 auto w =  m_detail->width();
953                 auto h = m_detail->height();
954                 if (image.width() > w || image.height() > h) {
955                     image = image.scaled(w, h, Qt::KeepAspectRatio);
956                 }
957                 m_detail->document()->addResource(QTextDocument::ImageResource,QUrl("image"),image);
958                 m_detail->textCursor().insertImage("image");
959                 displaydone = true;
960             }
961         }
962         if (!displaydone && rcldb->getDocRawText(m_detaildoc)) {
963             m_detail->setPlainText(u8s2qs(m_detaildoc.text));
964             displaydone = true;
965         }
966     }
967 
968     if (!displaydone && showmeta) {
969         m_pager->displaySingleDoc(theconfig, m_detaildocnum, m_detaildoc, m_model->m_hdata);
970     }
971     emit(detailDocChanged(doc, m_model->getDocSource()));
972 }
973 
on_tableView_entered(const QModelIndex & index)974 void ResTable::on_tableView_entered(const QModelIndex& index)
975 {
976     LOGDEB2("ResTable::on_tableView_entered(" << index.row() << ", "  <<
977             index.column() << ")\n");
978     if (!tableView->selectionModel()->hasSelection())
979         onTableView_currentChanged(index);
980 }
981 
takeFocus()982 void ResTable::takeFocus()
983 {
984 //    LOGDEB("resTable: take focus\n");
985     tableView->setFocus(Qt::ShortcutFocusReason);
986 }
987 
setDocSource(std::shared_ptr<DocSequence> nsource)988 void ResTable::setDocSource(std::shared_ptr<DocSequence> nsource)
989 {
990     LOGDEB("ResTable::setDocSource\n");
991     if (m_model)
992         m_model->setDocSource(nsource);
993     if (m_pager)
994         m_pager->setDocSource(nsource, 0);
995     if (m_detail)
996         m_detail->init();
997     m_detaildocnum = -1;
998 }
999 
resetSource()1000 void ResTable::resetSource()
1001 {
1002     LOGDEB("ResTable::resetSource\n");
1003     setDocSource(std::shared_ptr<DocSequence>());
1004     readDocSource();
1005 }
1006 
saveAsCSV()1007 void ResTable::saveAsCSV()
1008 {
1009     LOGDEB("ResTable::saveAsCSV\n");
1010     if (!m_model)
1011         return;
1012     QString s = QFileDialog::getSaveFileName(
1013         this, tr("Save table to CSV file"), path2qs(path_home()));
1014     if (s.isEmpty())
1015         return;
1016     std::string tofile = qs2path(s);
1017     std::fstream fp;
1018     if (!path_streamopen(tofile, std::ios::out|std::ios::trunc,fp)) {
1019         QMessageBox::warning(0, "Recoll",
1020                              tr("Can't open/create file: ") + s);
1021         return;
1022     }
1023     m_model->saveAsCSV(fp);
1024     fp.close();
1025 }
1026 
1027 // This is called when the sort order is changed from another widget
onSortDataChanged(DocSeqSortSpec spec)1028 void ResTable::onSortDataChanged(DocSeqSortSpec spec)
1029 {
1030     LOGDEB("ResTable::onSortDataChanged: [" << spec.field << "] desc " <<
1031            spec.desc << "\n");
1032     QHeaderView *header = tableView->horizontalHeader();
1033     if (!header || !m_model)
1034         return;
1035 
1036     // Check if the specified field actually matches one of columns
1037     // and set indicator
1038     m_model->setIgnoreSort(true);
1039     bool matched = false;
1040     const vector<string> fields = m_model->getFields();
1041     for (unsigned int i = 0; i < fields.size(); i++) {
1042         if (!spec.field.compare(m_model->baseField(fields[i]))) {
1043             header->setSortIndicator(i, spec.desc ?
1044                                      Qt::DescendingOrder : Qt::AscendingOrder);
1045             matched = true;
1046         }
1047     }
1048     if (!matched)
1049         header->setSortIndicator(-1, Qt::AscendingOrder);
1050     m_model->setIgnoreSort(false);
1051 }
1052 
resetSort()1053 void ResTable::resetSort()
1054 {
1055     LOGDEB("ResTable::resetSort()\n");
1056     QHeaderView *header = tableView->horizontalHeader();
1057     if (header)
1058         header->setSortIndicator(-1, Qt::AscendingOrder);
1059     // the model's sort slot is not called by qt in this case (qt 4.7)
1060     if (m_model)
1061         m_model->sort(-1, Qt::AscendingOrder);
1062 }
1063 
readDocSource(bool resetPos)1064 void ResTable::readDocSource(bool resetPos)
1065 {
1066     LOGDEB("ResTable::readDocSource("  << resetPos << ")\n");
1067     if (resetPos)
1068         tableView->verticalScrollBar()->setSliderPosition(0);
1069 
1070     if (m_model->m_source) {
1071         m_model->m_source->getTerms(m_model->m_hdata);
1072     } else {
1073         m_model->m_hdata.clear();
1074     }
1075     m_model->readDocSource();
1076     m_detail->init();
1077     m_detaildocnum = -1;
1078 }
1079 
linkWasClicked(const QUrl & url)1080 void ResTable::linkWasClicked(const QUrl &url)
1081 {
1082     if (m_detaildocnum < 0) {
1083         return;
1084     }
1085     QString s = url.toString();
1086     const char *ascurl = s.toUtf8();
1087     LOGDEB("ResTable::linkWasClicked: [" << ascurl << "]\n");
1088 
1089     int docseqnum = atoi(ascurl+1) -1;
1090     if (m_detaildocnum != docseqnum) {
1091         //? Really we should abort...
1092         LOGERR("ResTable::linkWasClicked: m_detaildocnum != docseqnum !\n");
1093         return;
1094     }
1095 
1096     int what = ascurl[0];
1097     switch (what) {
1098         // Open abstract/snippets window
1099     case 'A':
1100         emit(showSnippets(m_detaildoc));
1101         break;
1102     case 'D':
1103     {
1104         vector<Rcl::Doc> dups;
1105         if (m_rclmain && m_model->getDocSource()->docDups(m_detaildoc, dups)) {
1106             m_rclmain->newDupsW(m_detaildoc, dups);
1107         }
1108     }
1109     break;
1110 
1111     // Open parent folder
1112     case 'F':
1113     {
1114         emit editRequested(ResultPopup::getFolder(m_detaildoc));
1115     }
1116     break;
1117 
1118     case 'P':
1119     case 'E':
1120     {
1121         if (what == 'P') {
1122             if (m_ismainres) {
1123                 emit docPreviewClicked(docseqnum, m_detaildoc, 0);
1124             }  else {
1125                 emit previewRequested(m_detaildoc);
1126             }
1127         } else {
1128             emit editRequested(m_detaildoc);
1129         }
1130     }
1131     break;
1132 
1133     // Run script. Link format Rnn|Script Name
1134     case 'R':
1135     {
1136         int bar = s.indexOf("|");
1137         if (bar == -1 || bar >= s.size()-1)
1138             break;
1139         string cmdname = qs2utf8s(s.right(s.size() - (bar + 1)));
1140         DesktopDb ddb(path_cat(theconfig->getConfDir(), "scripts"));
1141         DesktopDb::AppDef app;
1142         if (ddb.appByName(cmdname, app)) {
1143             QAction act(QString::fromUtf8(app.name.c_str()), this);
1144             QVariant v(QString::fromUtf8(app.command.c_str()));
1145             act.setData(v);
1146             menuOpenWith(&act);
1147         }
1148     }
1149     break;
1150 
1151     default:
1152         LOGERR("ResTable::linkWasClicked: bad link [" << ascurl << "]\n");
1153         break;// ??
1154     }
1155 }
1156 
onClicked(const QModelIndex & index)1157 void ResTable::onClicked(const QModelIndex& index)
1158 {
1159     // If the current row is the one clicked, currentChanged is not
1160     // called so that we would not do the text display if we did not
1161     // call it from here
1162     m_rowchangefromkbd = false;
1163     if (index.row() == m_detaildocnum) {
1164         onTableView_currentChanged(index);
1165     }
1166 }
1167 
onDoubleClick(const QModelIndex & index)1168 void ResTable::onDoubleClick(const QModelIndex& index)
1169 {
1170     m_rowchangefromkbd = false;
1171     if (!m_model || !m_model->getDocSource())
1172         return;
1173     Rcl::Doc doc;
1174     if (m_model->getDocSource()->getDoc(index.row(), doc)) {
1175         if (m_detaildocnum != index.row()) {
1176             m_detail->init();
1177             m_detaildocnum = index.row();
1178             m_pager->displayDoc(theconfig, index.row(), m_detaildoc,
1179                                 m_model->m_hdata);
1180         }
1181         m_detaildoc = doc;
1182         if (m_detaildocnum >= 0)
1183             emit editRequested(m_detaildoc);
1184     } else {
1185         m_detaildocnum = -1;
1186     }
1187 }
1188 
createPopupMenu(const QPoint & pos)1189 void ResTable::createPopupMenu(const QPoint& pos)
1190 {
1191     LOGDEB("ResTable::createPopupMenu: m_detaildocnum " << m_detaildocnum << "\n");
1192     if (m_detaildocnum >= 0 && m_model) {
1193         int opts = m_ismainres? ResultPopup::isMain : 0;
1194 
1195         int selsz = tableView->selectionModel()->selectedRows().size();
1196 
1197         if (selsz == 1) {
1198             opts |= ResultPopup::showSaveOne;
1199         } else if (selsz > 1 && !m_ismainres) {
1200             // We don't show save multiple for the main list because not all
1201             // docs are necessary subdocs and multisave only works with those.
1202             opts |= ResultPopup::showSaveSel;
1203         }
1204         QMenu *popup = ResultPopup::create(this, opts, m_model->getDocSource(), m_detaildoc);
1205         popup->popup(mapToGlobal(pos));
1206     }
1207 }
1208 
menuPreview()1209 void ResTable::menuPreview()
1210 {
1211     if (m_detaildocnum >= 0) {
1212         if (m_ismainres) {
1213             emit docPreviewClicked(m_detaildocnum, m_detaildoc, 0);
1214         } else {
1215             emit previewRequested(m_detaildoc);
1216         }
1217     }
1218 }
1219 
menuSaveToFile()1220 void ResTable::menuSaveToFile()
1221 {
1222     if (m_detaildocnum >= 0)
1223         emit docSaveToFileClicked(m_detaildoc);
1224 }
1225 
menuSaveSelection()1226 void ResTable::menuSaveSelection()
1227 {
1228     if (m_model == 0 || !m_model->getDocSource())
1229         return;
1230 
1231     QModelIndexList indexl = tableView->selectionModel()->selectedRows();
1232     vector<Rcl::Doc> v;
1233     for (int i = 0; i < indexl.size(); i++) {
1234         Rcl::Doc doc;
1235         if (m_model->getDocSource()->getDoc(indexl[i].row(), doc))
1236             v.push_back(doc);
1237     }
1238     if (v.size() == 0) {
1239         return;
1240     } else if (v.size() == 1) {
1241         emit docSaveToFileClicked(v[0]);
1242     } else {
1243         multiSave(this, v);
1244     }
1245 }
1246 
menuPreviewParent()1247 void ResTable::menuPreviewParent()
1248 {
1249     if (m_detaildocnum >= 0 && m_model &&
1250         m_model->getDocSource()) {
1251         Rcl::Doc pdoc = ResultPopup::getParent(m_model->getDocSource(),
1252                                                m_detaildoc);
1253         if (pdoc.mimetype == "inode/directory") {
1254             emit editRequested(pdoc);
1255         } else {
1256             emit previewRequested(pdoc);
1257         }
1258     }
1259 }
1260 
menuOpenParent()1261 void ResTable::menuOpenParent()
1262 {
1263     if (m_detaildocnum >= 0 && m_model && m_model->getDocSource()) {
1264         Rcl::Doc pdoc =
1265             ResultPopup::getParent(m_model->getDocSource(), m_detaildoc);
1266         if (!pdoc.url.empty()) {
1267             emit editRequested(pdoc);
1268         }
1269     }
1270 }
1271 
menuOpenFolder()1272 void ResTable::menuOpenFolder()
1273 {
1274     if (m_detaildocnum >= 0) {
1275         Rcl::Doc pdoc = ResultPopup::getFolder(m_detaildoc);
1276         if (!pdoc.url.empty()) {
1277             emit editRequested(pdoc);
1278         }
1279     }
1280 }
1281 
menuEdit()1282 void ResTable::menuEdit()
1283 {
1284     if (m_detaildocnum >= 0)
1285         emit editRequested(m_detaildoc);
1286 }
menuEditAndQuit()1287 void ResTable::menuEditAndQuit()
1288 {
1289     if (m_detaildocnum >= 0) {
1290         emit editRequested(m_detaildoc);
1291         m_rclmain->fileExit();
1292     }
1293 }
menuOpenWith(QAction * act)1294 void ResTable::menuOpenWith(QAction *act)
1295 {
1296     if (act == 0)
1297         return;
1298     string cmd = qs2utf8s(act->data().toString());
1299     if (m_detaildocnum >= 0)
1300         emit openWithRequested(m_detaildoc, cmd);
1301 }
1302 
menuCopyFN()1303 void ResTable::menuCopyFN()
1304 {
1305     if (m_detaildocnum >= 0)
1306         ResultPopup::copyFN(m_detaildoc);
1307 }
1308 
menuCopyPath()1309 void ResTable::menuCopyPath()
1310 {
1311     if (m_detaildocnum >= 0)
1312         ResultPopup::copyPath(m_detaildoc);
1313 }
1314 
menuCopyURL()1315 void ResTable::menuCopyURL()
1316 {
1317     if (m_detaildocnum >= 0)
1318         ResultPopup::copyURL(m_detaildoc);
1319 }
1320 
menuCopyText()1321 void ResTable::menuCopyText()
1322 {
1323     if (m_detaildocnum >= 0 && rcldb) {
1324         ResultPopup::copyText(m_detaildoc, m_rclmain);
1325         if (m_rclmain) {
1326             auto msg = tr("%1 bytes copied to clipboard").arg(m_detaildoc.text.size());
1327             // Feedback was requested: tray messages are too ennoying, not
1328             // everybody displays the status bar, and the tool tip only
1329             // works when the copy is triggered through a shortcut (else,
1330             // it appears that the mouse event cancels it and it's not
1331             // shown). So let's do status bar if visible else tooltip.
1332             //  Menu trigger with no status bar -> no feedback...
1333 
1334             // rclmain->showTrayMessage(msg);
1335             if (m_rclmain->statusBar()->isVisible()) {
1336                 m_rclmain->statusBar()->showMessage(msg, 1000);
1337             } else {
1338                 int x = tableView->columnViewportPosition(0) + tableView->width() / 2 ;
1339                 int y = tableView->rowViewportPosition(m_detaildocnum);
1340                 QPoint pos = tableView->mapToGlobal(QPoint(x,y));
1341                 QToolTip::showText(pos, msg);
1342                 QTimer::singleShot(1500, m_rclmain, SLOT(hideToolTip()));
1343             }
1344         }
1345 
1346     }
1347 }
1348 
menuCopyTextAndQuit()1349 void ResTable::menuCopyTextAndQuit()
1350 {
1351     if (m_detaildocnum >= 0 && rcldb) {
1352         menuCopyText();
1353         m_rclmain->fileExit();
1354     }
1355 }
1356 
menuExpand()1357 void ResTable::menuExpand()
1358 {
1359     if (m_detaildocnum >= 0)
1360         emit docExpand(m_detaildoc);
1361 }
1362 
menuShowSnippets()1363 void ResTable::menuShowSnippets()
1364 {
1365     if (m_detaildocnum >= 0)
1366         emit showSnippets(m_detaildoc);
1367 }
1368 
menuShowSubDocs()1369 void ResTable::menuShowSubDocs()
1370 {
1371     if (m_detaildocnum >= 0)
1372         emit showSubDocs(m_detaildoc);
1373 }
1374 
createHeaderPopupMenu(const QPoint & pos)1375 void ResTable::createHeaderPopupMenu(const QPoint& pos)
1376 {
1377     LOGDEB("ResTable::createHeaderPopupMenu(" << pos.x() << ", " << pos.y() << ")\n");
1378     QHeaderView *header = tableView->horizontalHeader();
1379     if (!header || !m_model)
1380         return;
1381 
1382     m_popcolumn = header->logicalIndexAt(pos);
1383     if (m_popcolumn < 0)
1384         return;
1385 
1386     const map<string, QString>& allfields = m_model->getAllFields();
1387     const vector<string>& fields = m_model->getFields();
1388     QMenu *popup = new QMenu(this);
1389 
1390     popup->addAction(tr("&Reset sort"), this, SLOT(resetSort()));
1391     popup->addSeparator();
1392 
1393     popup->addAction(tr("&Save as CSV"), this, SLOT(saveAsCSV()));
1394     popup->addSeparator();
1395 
1396     popup->addAction(tr("&Delete column"), this, SLOT(deleteColumn()));
1397     popup->addSeparator();
1398 
1399     QAction *act;
1400     for (const auto& field : allfields) {
1401         if (std::find(fields.begin(), fields.end(), field.first) != fields.end())
1402             continue;
1403         act = new QAction(tr("Add \"%1\" column").arg(field.second), popup);
1404         act->setData(u8s2qs(field.first));
1405         connect(act, SIGNAL(triggered(bool)), this , SLOT(addColumn()));
1406         popup->addAction(act);
1407     }
1408     popup->popup(mapToGlobal(pos));
1409 }
1410 
deleteColumn()1411 void ResTable::deleteColumn()
1412 {
1413     if (m_model)
1414         m_model->deleteColumn(m_popcolumn);
1415 }
1416 
addColumn()1417 void ResTable::addColumn()
1418 {
1419     QAction *action = (QAction *)sender();
1420     if (nullptr == action || nullptr == m_model)
1421         return;
1422     std::string field = qs2utf8s(action->data().toString());
1423     LOGDEB("addColumn: text " << qs2utf8s(action->text()) << ", field " << field << "\n");
1424     m_model->addColumn(m_popcolumn, field);
1425 }
1426