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 ( 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