/* Copyright (C) 2005 J.F.Dockes * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the * Free Software Foundation, Inc., * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include "autoconfig.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef __APPLE__ #include #endif #include "debuglog.h" #include "smallut.h" #include "recoll.h" #include "guiutils.h" #include "pathut.h" #include "docseq.h" #include "pathut.h" #include "mimehandler.h" #include "plaintorich.h" #include "refcntr.h" #include "internfile.h" #include "reslist.h" #include "moc_reslist.cpp" #include "rclhelp.h" #ifdef RCL_USE_ASPELL #include "rclaspell.h" #endif #ifndef MIN #define MIN(A,B) ((A) < (B) ? (A) : (B)) #endif class QtGuiResListPager : public ResListPager { public: QtGuiResListPager(ResList *p, int ps) : ResListPager(ps), m_parent(p) {} virtual bool append(const string& data); virtual bool append(const string& data, int idx, const Rcl::Doc& doc); virtual string trans(const string& in); virtual string detailsLink(); virtual const string &parFormat(); virtual string nextUrl(); virtual string prevUrl(); virtual string pageTop(); virtual string iconPath(const string& mt); virtual void suggest(const vectoruterms, vector&sugg); virtual string absSep() {return (const char *)(prefs.abssep.toUtf8());} private: ResList *m_parent; }; #if 0 FILE *fp; void logdata(const char *data) { if (fp == 0) fp = fopen("/tmp/recolltoto.html", "a"); if (fp) fprintf(fp, "%s", data); } #else #define logdata(X) #endif ////////////////////////////// // /// QtGuiResListPager methods: bool QtGuiResListPager::append(const string& data) { LOGDEB2(("QtGuiReslistPager::appendString : %s\n", data.c_str())); logdata(data.c_str()); m_parent->append(QString::fromUtf8(data.c_str())); return true; } bool QtGuiResListPager::append(const string& data, int docnum, const Rcl::Doc&) { LOGDEB2(("QtGuiReslistPager::appendDoc: blockCount %d, %s\n", m_parent->document()->blockCount(), data.c_str())); logdata(data.c_str()); int blkcnt0 = m_parent->document()->blockCount(); m_parent->moveCursor(QTextCursor::End, QTextCursor::MoveAnchor); m_parent->textCursor().insertBlock(); m_parent->insertHtml(QString::fromUtf8(data.c_str())); m_parent->moveCursor(QTextCursor::Start, QTextCursor::MoveAnchor); m_parent->ensureCursorVisible(); int blkcnt1 = m_parent->document()->blockCount(); for (int block = blkcnt0; block < blkcnt1; block++) { m_parent->m_pageParaToReldocnums[block] = docnum; } return true; } string QtGuiResListPager::trans(const string& in) { return string((const char*)ResList::tr(in.c_str()).toUtf8()); } string QtGuiResListPager::detailsLink() { string chunk = ""; chunk += trans("(show query)"); chunk += ""; return chunk; } const string& QtGuiResListPager::parFormat() { return prefs.creslistformat; } string QtGuiResListPager::nextUrl() { return "n-1"; } string QtGuiResListPager::prevUrl() { return "p-1"; } string QtGuiResListPager::pageTop() { return string(); } string QtGuiResListPager::iconPath(const string& mtype) { string iconpath; theconfig->getMimeIconName(mtype, &iconpath); return iconpath; } void QtGuiResListPager::suggest(const vectoruterms, vector&sugg) { sugg.clear(); #ifdef RCL_USE_ASPELL bool noaspell = false; theconfig->getConfParam("noaspell", &noaspell); if (noaspell) return; if (!aspell) { LOGERR(("QtGuiResListPager:: aspell not initialized\n")); return; } for (vector::const_iterator uit = uterms.begin(); uit != uterms.end(); uit++) { list asuggs; string reason; if (aspell->check(*rcldb, *uit, reason)) continue; else if (!reason.empty()) return; if (!aspell->suggest(*rcldb, *uit, asuggs, reason)) { LOGERR(("QtGuiResListPager::suggest: aspell failed: %s\n", reason.c_str())); continue; } if (!asuggs.empty()) { sugg.push_back(*asuggs.begin()); } } #endif } /////// /////// End reslistpager methods class PlainToRichQtReslist : public PlainToRich { public: virtual ~PlainToRichQtReslist() {} virtual string startMatch() { return string(""); } virtual string endMatch() {return string("");} }; static PlainToRichQtReslist g_hiliter; ///////////////////////////////////// ResList::ResList(QWidget* parent, const char* name) : QTextBrowser(parent) { if (!name) setObjectName("resList"); else setObjectName(name); setReadOnly(TRUE); setUndoRedoEnabled(FALSE); setOpenLinks(FALSE); languageChange(); setTabChangesFocus(true); (void)new HelpClient(this); HelpClient::installMap((const char *)this->objectName().toAscii(), "RCL.SEARCH.RESLIST"); // signals and slots connections connect(this, SIGNAL(anchorClicked(const QUrl &)), this, SLOT(linkWasClicked(const QUrl &))); #if 0 // See comments in "highlighted connect(this, SIGNAL(highlighted(const QString &)), this, SLOT(highlighted(const QString &))); #endif setContextMenuPolicy(Qt::CustomContextMenu); connect(this, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(createPopupMenu(const QPoint&))); m_curPvDoc = -1; m_lstClckMod = 0; m_listId = 0; m_pager = new QtGuiResListPager(this, prefs.respagesize); m_pager->setHighLighter(&g_hiliter); if (prefs.reslistfontfamily.length()) { QFont nfont(prefs.reslistfontfamily, prefs.reslistfontsize); setFont(nfont); } } ResList::~ResList() { // These have to exist somewhere for translations to work #ifdef __GNUC__ __attribute__((unused)) #endif static const char* strings[] = { QT_TR_NOOP("

No results found
"), QT_TR_NOOP("Documents"), QT_TR_NOOP("out of at least"), QT_TR_NOOP("for"), QT_TR_NOOP("Previous"), QT_TR_NOOP("Next"), QT_TR_NOOP("Unavailable document"), QT_TR_NOOP("Preview"), QT_TR_NOOP("Open"), QT_TR_NOOP("(show query)"), QT_TR_NOOP("

Alternate spellings (accents suppressed): "), }; } int ResList::newListId() { static int id; return ++id; } extern "C" int XFlush(void *); void ResList::setDocSource(RefCntr nsource) { LOGDEB(("ResList::setDocSource()\n")); m_source = RefCntr(new DocSource(nsource)); } // Reapply parameters. Sort params probably changed void ResList::readDocSource() { LOGDEB(("ResList::readDocSource()\n")); resetView(); if (m_source.isNull()) return; m_listId = newListId(); // Reset the page size in case the preference was changed m_pager->setPageSize(prefs.respagesize); m_pager->setDocSource(m_source); resultPageNext(); emit hasResults(m_source->getResCnt()); } void ResList::setSortParams(DocSeqSortSpec) { LOGDEB2(("ResList::setSortParams\n")); } void ResList::setFilterParams(const DocSeqFiltSpec&) { LOGDEB2(("ResList::setFilterParams\n")); } void ResList::resetList() { LOGDEB(("ResList::resetList()\n")); setDocSource(RefCntr()); resetView(); } void ResList::resetView() { m_curPvDoc = -1; // There should be a progress bar for long searches but there isn't // We really want the old result list to go away, otherwise, for a // slow search, the user will wonder if anything happened. The // following helps making sure that the textedit is really // blank. Else, there are often icons or text left around clear(); QTextBrowser::append("."); clear(); #ifndef __APPLE__ XFlush(QX11Info::display()); #endif } bool ResList::displayingHistory() { // We want to reset the displayed history if it is currently // shown. Using the title value is an ugly hack string htstring = string((const char *)tr("Document history").toUtf8()); if (m_source.isNull() || m_source->title().empty()) return false; return m_source->title().find(htstring) == 0; } void ResList::languageChange() { setWindowTitle(tr("Result list")); } // Get document number from text block number int ResList::docnumfromparnum(int block) { if (m_pager->pageNumber() < 0) return -1; // Try to find the first number < input and actually in the map // (result blocks can be made of several text blocks) std::map::iterator it; do { it = m_pageParaToReldocnums.find(block); if (it != m_pageParaToReldocnums.end()) return pageFirstDocNum() + it->second; } while (--block >= 0); return -1; } // Get paragraph number from document number pair ResList::parnumfromdocnum(int docnum) { LOGDEB(("parnumfromdocnum: docnum %d\n", docnum)); if (m_pager->pageNumber() < 0) { LOGDEB(("parnumfromdocnum: no page return -1,-1\n")); return pair(-1,-1); } int winfirst = pageFirstDocNum(); if (docnum - winfirst < 0) { LOGDEB(("parnumfromdocnum: docnum %d < winfirst %d return -1,-1\n", docnum, winfirst)); return pair(-1,-1); } docnum -= winfirst; for (std::map::iterator it = m_pageParaToReldocnums.begin(); it != m_pageParaToReldocnums.end(); it++) { if (docnum == it->second) { int first = it->first; int last = first+1; std::map::iterator it1; while ((it1 = m_pageParaToReldocnums.find(last)) != m_pageParaToReldocnums.end() && it1->second == docnum) { last++; } LOGDEB(("parnumfromdocnum: return %d,%d\n", first, last)); return pair(first, last); } } LOGDEB(("parnumfromdocnum: not found return -1,-1\n")); return pair(-1,-1); } // Return doc from current or adjacent result pages. We can get called // for a document not in the current page if the user browses through // results inside a result window (with shift-arrow). This can only // result in a one-page change. bool ResList::getDoc(int docnum, Rcl::Doc &doc) { LOGDEB(("ResList::getDoc: docnum %d winfirst %d\n", docnum, pageFirstDocNum())); int winfirst = pageFirstDocNum(); int winlast = m_pager->pageLastDocNum(); if (docnum < 0 || winfirst < 0 || winlast < 0) return false; // Is docnum in current page ? Then all Ok if (docnum >= winfirst && docnum <= winlast) { return m_source->getDoc(docnum, doc); } // Else we accept to page down or up but not further if (docnum < winfirst && docnum >= winfirst - prefs.respagesize) { resultPageBack(); } else if (docnum < winlast + 1 + prefs.respagesize) { resultPageNext(); } winfirst = pageFirstDocNum(); winlast = m_pager->pageLastDocNum(); if (docnum >= winfirst && docnum <= winlast) { return m_source->getDoc(docnum, doc); } return false; } void ResList::keyPressEvent(QKeyEvent * e) { if (e->key() == Qt::Key_PageUp || e->key() == Qt::Key_Backspace) { resPageUpOrBack(); return; } else if (e->key() == Qt::Key_PageDown || e->key() == Qt::Key_Space) { resPageDownOrNext(); return; } QTextBrowser::keyPressEvent(e); } void ResList::mouseReleaseEvent(QMouseEvent *e) { m_lstClckMod = 0; if (e->modifiers() & Qt::ControlModifier) { m_lstClckMod |= Qt::ControlModifier; } if (e->modifiers() & Qt::ShiftModifier) { m_lstClckMod |= Qt::ShiftModifier; } QTextBrowser::mouseReleaseEvent(e); } void ResList::highlighted(const QString& ) { // This is supposedly called when a link is preactivated (hover or tab // traversal, but is not actually called for tabs. We would have liked to // give some kind of visual feedback for tab traversal } // Page Up/Down: we don't try to check if current paragraph is last or // first. We just page up/down and check if viewport moved. If it did, // fair enough, else we go to next/previous result page. void ResList::resPageUpOrBack() { int vpos = verticalScrollBar()->value(); verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepSub); if (vpos == verticalScrollBar()->value()) resultPageBack(); } void ResList::resPageDownOrNext() { int vpos = verticalScrollBar()->value(); verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepAdd); LOGDEB(("ResList::resPageDownOrNext: vpos before %d, after %d\n", vpos, verticalScrollBar()->value())); if (vpos == verticalScrollBar()->value()) resultPageNext(); } // Show previous page of results. We just set the current number back // 2 pages and show next page. void ResList::resultPageBack() { m_pager->resultPageBack(); displayPage(); } // Go to the first page void ResList::resultPageFirst() { // In case the preference was changed m_pager->setPageSize(prefs.respagesize); m_pager->resultPageFirst(); displayPage(); } void ResList::append(const QString &text) { LOGDEB2(("QtGuiReslistPager::appendQString : %s\n", (const char*)text.toUtf8())); QTextBrowser::append(text); } // Fill up result list window with next screen of hits void ResList::resultPageNext() { m_pager->resultPageNext(); displayPage(); } void ResList::resultPageFor(int docnum) { m_pager->resultPageFor(docnum); displayPage(); } void ResList::displayPage() { m_pageParaToReldocnums.clear(); clear(); m_pager->displayPage(theconfig); LOGDEB0(("ResList::resultPageNext: hasNext %d hasPrev %d\n", m_pager->hasPrev(), m_pager->hasNext())); emit prevPageAvailable(m_pager->hasPrev()); emit nextPageAvailable(m_pager->hasNext()); // Possibly color paragraph of current preview if any previewExposed(m_curPvDoc); ensureCursorVisible(); } // Color paragraph (if any) of currently visible preview void ResList::previewExposed(int docnum) { LOGDEB(("ResList::previewExposed: doc %d\n", docnum)); // Possibly erase old one to white pair blockrange; if (m_curPvDoc != -1) { blockrange = parnumfromdocnum(m_curPvDoc); if (blockrange.first != -1) { for (int blockn = blockrange.first; blockn < blockrange.second; blockn++) { QTextBlock block = document()->findBlockByNumber(blockn); QTextCursor cursor(block); QTextBlockFormat format = cursor.blockFormat(); format.clearBackground(); cursor.setBlockFormat(format); } } m_curPvDoc = -1; } // Set background for active preview's doc entry m_curPvDoc = docnum; blockrange = parnumfromdocnum(docnum); // Maybe docnum is -1 or not in this window, if (blockrange.first < 0) return; // Color the new active paragraph QColor color("LightBlue"); for (int blockn = blockrange.first+1; blockn < blockrange.second; blockn++) { QTextBlock block = document()->findBlockByNumber(blockn); QTextCursor cursor(block); QTextBlockFormat format; format.setBackground(QBrush(color)); cursor.mergeBlockFormat(format); setTextCursor(cursor); ensureCursorVisible(); } } // Double click in res list: add selection to simple search void ResList::mouseDoubleClickEvent(QMouseEvent *event) { QTextBrowser::mouseDoubleClickEvent(event); if (textCursor().hasSelection()) emit(wordSelect(textCursor().selectedText())); } void ResList::linkWasClicked(const QUrl &url) { QString s = url.toString(); const char *ascurl = s.toAscii(); LOGDEB(("ResList::linkWasClicked: [%s]\n", ascurl)); int i = atoi(ascurl+1) - 1; int what = ascurl[0]; switch (what) { case 'H': emit headerClicked(); break; case 'P': case 'E': { Rcl::Doc doc; if (!getDoc(i, doc)) { LOGERR(("ResList::linkWasClicked: can't get doc for %d\n", i)); return; } if (what == 'P') emit docPreviewClicked(i, doc, m_lstClckMod); else emit docEditClicked(doc); } break; case 'n': resultPageNext(); break; case 'p': resultPageBack(); break; default: LOGERR(("ResList::linkWasClicked: bad link [%s]\n", ascurl)); break;// ?? } } void ResList::createPopupMenu(const QPoint& pos) { LOGDEB(("ResList::createPopupMenu(%d, %d)\n", pos.x(), pos.y())); QTextCursor cursor = cursorForPosition(pos); int blocknum = cursor.blockNumber(); LOGDEB(("ResList::createPopupMenu(): block %d\n", blocknum)); m_popDoc = docnumfromparnum(blocknum); if (m_popDoc < 0) return; QMenu *popup = new QMenu(this); popup->addAction(tr("&Preview"), this, SLOT(menuPreview())); popup->addAction(tr("&Open"), this, SLOT(menuEdit())); popup->addAction(tr("Copy &File Name"), this, SLOT(menuCopyFN())); popup->addAction(tr("Copy &URL"), this, SLOT(menuCopyURL())); Rcl::Doc doc; if (getDoc(m_popDoc, doc) && !doc.ipath.empty()) { popup->addAction(tr("&Write to File"), this, SLOT(menuSaveToFile())); } popup->addAction(tr("Find &similar documents"), this, SLOT(menuExpand())); popup->addAction(tr("Preview P&arent document/folder"), this, SLOT(menuPreviewParent())); popup->addAction(tr("&Open Parent document/folder"), this, SLOT(menuOpenParent())); popup->popup(mapToGlobal(pos)); } void ResList::menuPreview() { Rcl::Doc doc; if (getDoc(m_popDoc, doc)) emit docPreviewClicked(m_popDoc, doc, 0); } void ResList::menuSaveToFile() { Rcl::Doc doc; if (getDoc(m_popDoc, doc)) emit docSaveToFileClicked(doc); } void ResList::menuPreviewParent() { Rcl::Doc doc; if (!getDoc(m_popDoc, doc) || m_source.isNull()) return; Rcl::Doc pdoc; if (m_source->getEnclosing(doc, pdoc)) { emit previewRequested(pdoc); } else { // No parent doc: show enclosing folder with app configured for // directories pdoc.url = path_getfather(doc.url); pdoc.mimetype = "application/x-fsdirectory"; emit editRequested(pdoc); } } void ResList::menuOpenParent() { Rcl::Doc doc; if (!getDoc(m_popDoc, doc) || m_source.isNull()) return; Rcl::Doc pdoc; if (m_source->getEnclosing(doc, pdoc)) { emit editRequested(pdoc); } else { // No parent doc: show enclosing folder with app configured for // directories pdoc.url = path_getfather(doc.url); pdoc.mimetype = "application/x-fsdirectory"; emit editRequested(pdoc); } } void ResList::menuEdit() { Rcl::Doc doc; if (getDoc(m_popDoc, doc)) emit docEditClicked(doc); } void ResList::menuCopyFN() { LOGDEB(("menuCopyFN\n")); Rcl::Doc doc; if (getDoc(m_popDoc, doc)) { LOGDEB(("menuCopyFN: Got doc, fn: [%s]\n", doc.url.c_str())); // Our urls currently always begin with "file://" // // Problem: setText expects a QString. Passing a (const char*) // as we used to do causes an implicit conversion from // latin1. File are binary and the right approach would be no // conversion, but it's probably better (less worse...) to // make a "best effort" tentative and try to convert from the // locale's charset than accept the default conversion. QString qfn = QString::fromLocal8Bit(doc.url.c_str()+7); QApplication::clipboard()->setText(qfn, QClipboard::Selection); QApplication::clipboard()->setText(qfn, QClipboard::Clipboard); } } void ResList::menuCopyURL() { Rcl::Doc doc; if (getDoc(m_popDoc, doc)) { string url = url_encode(doc.url, 7); QApplication::clipboard()->setText(url.c_str(), QClipboard::Selection); QApplication::clipboard()->setText(url.c_str(), QClipboard::Clipboard); } } void ResList::menuExpand() { Rcl::Doc doc; if (getDoc(m_popDoc, doc)) emit docExpand(doc); } int ResList::pageFirstDocNum() { return m_pager->pageFirstDocNum(); }