1 //C- -*- C++ -*-
2 //C- -------------------------------------------------------------------
3 //C- DjView4
4 //C- Copyright (c) 2006- Leon Bottou
5 //C-
6 //C- This software is subject to, and may be distributed under, the
7 //C- GNU General Public License, either version 2 of the license,
8 //C- or (at your option) any later version. The license should have
9 //C- accompanied the software or you may obtain a copy of the license
10 //C- from the Free Software Foundation at http://www.fsf.org .
11 //C-
12 //C- This program is distributed in the hope that it will be useful,
13 //C- but WITHOUT ANY WARRANTY; without even the implied warranty of
14 //C- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 //C- GNU General Public License for more details.
16 //C- ------------------------------------------------------------------
17
18 #if AUTOCONF
19 # include "config.h"
20 #endif
21
22 #include <stddef.h>
23 #include <stdlib.h>
24 #include <math.h>
25
26 #include <QAbstractItemDelegate>
27 #include <QAbstractListModel>
28 #include <QAction>
29 #include <QActionGroup>
30 #include <QApplication>
31 #include <QComboBox>
32 #include <QContextMenuEvent>
33 #include <QCheckBox>
34 #include <QDebug>
35 #include <QEvent>
36 #include <QFont>
37 #include <QFontMetrics>
38 #include <QHBoxLayout>
39 #include <QHeaderView>
40 #include <QItemDelegate>
41 #include <QLabel>
42 #include <QLineEdit>
43 #include <QList>
44 #include <QListView>
45 #include <QMap>
46 #include <QMenu>
47 #include <QPainter>
48 #include <QPainterPath>
49 #include <QPushButton>
50 #include <QPixmap>
51 #include <QRegExp>
52 #include <QResizeEvent>
53 #include <QStackedLayout>
54 #include <QStringList>
55 #include <QTimer>
56 #include <QToolBar>
57 #include <QToolButton>
58 #include <QTreeWidget>
59 #include <QVariant>
60 #include <QVBoxLayout>
61 #if QT_VERSION >= 0x50000
62 # include <QUrlQuery>
63 #endif
64 #if QT_VERSION >= 0x50E00
65 # define zero(T) T()
66 #else
67 # define zero(T) 0
68 #endif
69
70 #include <libdjvu/ddjvuapi.h>
71 #include <libdjvu/miniexp.h>
72
73 #include "qdjvu.h"
74 #include "qdjvuwidget.h"
75 #include "qdjviewsidebar.h"
76 #include "qdjview.h"
77
78
79
80 // =======================================
81 // QDJVIEWOUTLINE
82 // =======================================
83
84
85
86
QDjViewOutline(QDjView * djview)87 QDjViewOutline::QDjViewOutline(QDjView *djview)
88 : QWidget(djview),
89 djview(djview),
90 loaded(false)
91 {
92 tree = new QTreeWidget(this);
93 tree->setColumnCount(1);
94 tree->setItemsExpandable(true);
95 tree->setUniformRowHeights(true);
96 tree->header()->hide();
97 tree->header()->setStretchLastSection(true);
98 tree->setEditTriggers(QAbstractItemView::NoEditTriggers);
99 tree->setSelectionBehavior(QAbstractItemView::SelectRows);
100 tree->setSelectionMode(QAbstractItemView::SingleSelection);
101 tree->setTextElideMode(Qt::ElideRight);
102 QVBoxLayout *layout = new QVBoxLayout(this);
103 layout->setMargin(0);
104 layout->setSpacing(0);
105 layout->addWidget(tree);
106
107 connect(tree, SIGNAL(itemActivated(QTreeWidgetItem*, int)),
108 this, SLOT(itemActivated(QTreeWidgetItem*)) );
109 connect(djview, SIGNAL(documentClosed(QDjVuDocument*)),
110 this, SLOT(clear()) );
111 connect(djview, SIGNAL(documentOpened(QDjVuDocument*)),
112 this, SLOT(clear()) );
113 connect(djview, SIGNAL(documentReady(QDjVuDocument*)),
114 this, SLOT(refresh()) );
115 connect(djview->getDjVuWidget(), SIGNAL(pageChanged(int)),
116 this, SLOT(pageChanged(int)) );
117 connect(djview->getDjVuWidget(), SIGNAL(layoutChanged()),
118 this, SLOT(refresh()) );
119
120 setWhatsThis(tr("<html><b>Document outline.</b><br/> "
121 "This panel display the document outline, "
122 "or the page names when the outline is not available, "
123 "Double-click any entry to jump to the selected page."
124 "</html>"));
125
126 if (djview->pageNum() > 0)
127 refresh();
128 }
129
130
131 void
clear()132 QDjViewOutline::clear()
133 {
134 tree->clear();
135 loaded = false;
136 }
137
138 void
refresh()139 QDjViewOutline::refresh()
140 {
141 QDjVuDocument *doc = djview->getDocument();
142 if (doc && !loaded && djview->pageNum()>0)
143 {
144 miniexp_t outline = doc->getDocumentOutline();
145 if (outline == miniexp_dummy)
146 return;
147 loaded = true;
148 if (outline)
149 {
150 if (!miniexp_consp(outline) ||
151 miniexp_car(outline) != miniexp_symbol("bookmarks"))
152 {
153 QString msg = tr("Outline data is corrupted");
154 qWarning("%s", (const char*)msg.toLocal8Bit());
155 }
156 tree->clear();
157 QTreeWidgetItem *root = new QTreeWidgetItem();
158 fillItems(root, miniexp_cdr(outline));
159 while (root->childCount() > 0)
160 tree->insertTopLevelItem(tree->topLevelItemCount(),
161 root->takeChild(0) );
162 if (tree->topLevelItemCount() == 1)
163 tree->topLevelItem(0)->setExpanded(true);
164 delete root;
165 }
166 else
167 {
168 tree->clear();
169 QTreeWidgetItem *root = new QTreeWidgetItem(tree);
170 root->setText(0, tr("Pages"));
171 root->setFlags(Qt::ItemIsEnabled);
172 root->setData(0, Qt::UserRole, -1);
173 for (int pageno=0; pageno<djview->pageNum(); pageno++)
174 {
175 QTreeWidgetItem *item = new QTreeWidgetItem(root);
176 QString name = djview->pageName(pageno);
177 item->setText(0, tr("Page %1").arg(name));
178 item->setData(0, Qt::UserRole, pageno);
179 item->setData(0, Qt::UserRole+1, pageno);
180 item->setFlags(Qt::ItemIsSelectable|Qt::ItemIsEnabled);
181 item->setToolTip(0, tr("Go: page %1.").arg(name));
182 item->setWhatsThis(0, whatsThis());
183 }
184 root->setExpanded(true);
185 }
186 pageChanged(djview->getDjVuWidget()->page());
187 }
188 }
189
190
191 int
pageNumber(const char * link)192 QDjViewOutline::pageNumber(const char *link)
193 {
194 if (link && link[0] == '#')
195 return djview->pageNumber(QString::fromUtf8(link+1));
196 if (link == 0 || link[0] != '?')
197 return -1;
198 QByteArray burl = QByteArray("http://f/f") + link;
199 #if QT_VERSION >= 0x50000
200 QUrlQuery qurl(QUrl::fromEncoded(burl));
201 #else
202 QUrl qurl = QUrl::fromEncoded(burl);
203 #endif
204 if (qurl.hasQueryItem("page"))
205 return djview->pageNumber(qurl.queryItemValue("page"));
206 else if (qurl.hasQueryItem("pageno"))
207 return djview->pageNumber("$" + qurl.queryItemValue("pageno"));
208 return -1;
209 }
210
211
212 static const QRegExp spaces("\\s+");
213
214 void
fillItems(QTreeWidgetItem * root,miniexp_t expr)215 QDjViewOutline::fillItems(QTreeWidgetItem *root, miniexp_t expr)
216 {
217 while(miniexp_consp(expr))
218 {
219 miniexp_t s = miniexp_car(expr);
220 expr = miniexp_cdr(expr);
221 if (miniexp_consp(s) &&
222 miniexp_consp(miniexp_cdr(s)) &&
223 miniexp_stringp(miniexp_car(s)) &&
224 miniexp_stringp(miniexp_cadr(s)) )
225 {
226 // fill item
227 const char *name = miniexp_to_str(miniexp_car(s));
228 const char *link = miniexp_to_str(miniexp_cadr(s));
229 int pageno = pageNumber(link);
230 QString pagename = (pageno>=0)?djview->pageName(pageno):QString();
231 QTreeWidgetItem *item = new QTreeWidgetItem(root);
232 QString text = QString::fromUtf8(name);
233 if (name && name[0])
234 item->setText(0, text.replace(spaces," "));
235 else if (! pagename.isEmpty())
236 item->setText(0, tr("Page %1").arg(pagename));
237 item->setFlags(zero(Qt::ItemFlags));
238 item->setWhatsThis(0, whatsThis());
239 if (link && link[0])
240 {
241 QString slink = QString::fromUtf8(link);
242 item->setData(0, Qt::UserRole+1, slink);
243 item->setFlags(Qt::ItemIsSelectable|Qt::ItemIsEnabled);
244 item->setToolTip(0, tr("Go: %1").arg(slink));
245 if (pageno >= 0)
246 item->setData(0, Qt::UserRole, pageno);
247 if (! pagename.isEmpty())
248 item->setToolTip(0, tr("Go: page %1.").arg(pagename));
249 }
250 // recurse
251 fillItems(item, miniexp_cddr(s));
252 }
253 }
254 }
255
256
257 void
pageChanged(int pageno)258 QDjViewOutline::pageChanged(int pageno)
259 {
260 int fp = -1;
261 QTreeWidgetItem *fi = 0;
262 // find current selection
263 QList<QTreeWidgetItem*> sel = tree->selectedItems();
264 QTreeWidgetItem *si = 0;
265 if (sel.size() == 1)
266 si = sel[0];
267 // current selection has priority
268 if (si)
269 searchItem(si, pageno, fi, fp);
270 // search
271 for (int i=0; i<tree->topLevelItemCount(); i++)
272 searchItem(tree->topLevelItem(i), pageno, fi, fp);
273 // select
274 if (si && fi && si != fi)
275 si->setSelected(false);
276 if (fi && si != fi)
277 {
278 tree->setCurrentItem(fi);
279 fi->setSelected(true);
280 tree->scrollToItem(fi);
281 }
282 }
283
284
285 void
searchItem(QTreeWidgetItem * item,int pageno,QTreeWidgetItem * & fi,int & fp)286 QDjViewOutline::searchItem(QTreeWidgetItem *item, int pageno,
287 QTreeWidgetItem *&fi, int &fp)
288 {
289 QVariant data = item->data(0, Qt::UserRole);
290 if (data.type() == QVariant::Int)
291 {
292 int page = data.toInt();
293 if (page>=0 && page<=pageno && page>fp)
294 {
295 fi = item;
296 fp = page;
297 }
298 }
299 for (int i=0; i<item->childCount(); i++)
300 searchItem(item->child(i), pageno, fi, fp);
301 }
302
303
304 void
itemActivated(QTreeWidgetItem * item)305 QDjViewOutline::itemActivated(QTreeWidgetItem *item)
306 {
307 QVariant data = item->data(0, Qt::UserRole+1);
308 if (data.type() == QVariant::String)
309 {
310 QString link = data.toString();
311 if (link.size() > 0)
312 djview->goToLink(link);
313 }
314 else if (data.type() == QVariant::Int)
315 {
316 int pageno = data.toInt();
317 if (pageno >= 0)
318 djview->goToPage(pageno);
319 }
320 }
321
322
323
324
325
326
327
328 // =======================================
329 // QDJVIEWTHUMBNAILS
330 // =======================================
331
332
333
334
335 class QDjViewThumbnails::View : public QListView
336 {
337 Q_OBJECT
338 public:
339 View(QDjViewThumbnails *widget);
340 protected:
341 QStyleOptionViewItem viewOptions() const;
342 private:
343 QDjViewThumbnails *widget;
344 };
345
346
347 class QDjViewThumbnails::Model : public QAbstractListModel
348 {
349 Q_OBJECT
350 public:
351 ~Model();
352 Model(QDjViewThumbnails*);
353 virtual int rowCount(const QModelIndex &parent) const;
354 virtual QVariant data(const QModelIndex &index, int role) const;
getSize()355 int getSize() { return size; }
getSmart()356 int getSmart() { return smart; }
357 public slots:
358 void setSize(int);
359 void setSmart(bool);
360 void scheduleRefresh();
361 protected slots:
362 void documentClosed(QDjVuDocument *doc);
363 void documentReady(QDjVuDocument *doc);
364 void thumbnail(int);
365 void refresh();
366 private:
367 QDjView *djview;
368 QDjViewThumbnails *widget;
369 QStringList names;
370 ddjvu_format_t *format;
371 QIcon icon;
372 int size;
373 bool smart;
374 bool refreshScheduled;
375 int pageInProgress;
376 QIcon makeIcon(int pageno) const;
377 QSize makeHint(int pageno) const;
378 };
379
380
381
382 // ----------------------------------------
383 // QDJVIEWTHUMBNAILS::VIEW
384
385
View(QDjViewThumbnails * widget)386 QDjViewThumbnails::View::View(QDjViewThumbnails *widget)
387 : QListView(widget),
388 widget(widget)
389 {
390 setDragEnabled(false);
391 setEditTriggers(QAbstractItemView::NoEditTriggers);
392 setSelectionBehavior(QAbstractItemView::SelectRows);
393 setSelectionMode(QAbstractItemView::SingleSelection);
394 setTextElideMode(Qt::ElideRight);
395 setViewMode(QListView::IconMode);
396 setFlow(QListView::LeftToRight);
397 setWrapping(true);
398 setMovement(QListView::Static);
399 setResizeMode(QListView::Adjust);
400 setSpacing(8);
401 setUniformItemSizes(true);
402 }
403
404
405 QStyleOptionViewItem
viewOptions() const406 QDjViewThumbnails::View::viewOptions() const
407 {
408 int size = widget->model->getSize();
409 QStyleOptionViewItem opt = QListView::viewOptions();
410 opt.decorationAlignment = Qt::AlignCenter;
411 opt.decorationPosition = QStyleOptionViewItem::Top;
412 opt.decorationSize = QSize(size, size);
413 opt.displayAlignment = Qt::AlignCenter;
414 return opt;
415 }
416
417
418
419 // ----------------------------------------
420 // QDJVIEWTHUMBNAILS::MODEL
421
422
~Model()423 QDjViewThumbnails::Model::~Model()
424 {
425 if (format)
426 ddjvu_format_release(format);
427 }
428
429
Model(QDjViewThumbnails * widget)430 QDjViewThumbnails::Model::Model(QDjViewThumbnails *widget)
431 : QAbstractListModel(widget),
432 djview(widget->djview),
433 widget(widget),
434 format(0),
435 size(0),
436 smart(true),
437 refreshScheduled(false),
438 pageInProgress(-1)
439 {
440 // create format
441 #if DDJVUAPI_VERSION < 18
442 unsigned int masks[3] = { 0xff0000, 0xff00, 0xff };
443 format = ddjvu_format_create(DDJVU_FORMAT_RGBMASK32, 3, masks);
444 #else
445 unsigned int masks[4] = { 0xff0000, 0xff00, 0xff, 0xff000000 };
446 format = ddjvu_format_create(DDJVU_FORMAT_RGBMASK32, 4, masks);
447 #endif
448 ddjvu_format_set_row_order(format, true);
449 ddjvu_format_set_y_direction(format, true);
450 ddjvu_format_set_ditherbits(format, QPixmap::defaultDepth());
451 // set size
452 setSize(64);
453 // connect
454 connect(djview, SIGNAL(documentClosed(QDjVuDocument*)),
455 this, SLOT(documentClosed(QDjVuDocument*)) );
456 connect(djview, SIGNAL(documentReady(QDjVuDocument*)),
457 this, SLOT(documentReady(QDjVuDocument*)) );
458 // update
459 if (djview->pageNum() > 0)
460 documentReady(djview->getDocument());
461 }
462
463
464 void
documentClosed(QDjVuDocument * doc)465 QDjViewThumbnails::Model::documentClosed(QDjVuDocument *doc)
466 {
467 if (names.size() > 0)
468 {
469 beginRemoveRows(QModelIndex(),0,names.size()-1);
470 names.clear();
471 pageInProgress = -1;
472 endRemoveRows();
473 }
474 disconnect(doc, 0, this, 0);
475 }
476
477
478 void
documentReady(QDjVuDocument * doc)479 QDjViewThumbnails::Model::documentReady(QDjVuDocument *doc)
480 {
481 if (names.size() > 0)
482 {
483 beginRemoveRows(QModelIndex(),0,names.size()-1);
484 names.clear();
485 pageInProgress = -1;
486 endRemoveRows();
487 }
488 int pagenum = djview->pageNum();
489 if (pagenum > 0)
490 {
491 beginInsertRows(QModelIndex(),0,pagenum-1);
492 for (int pageno=0; pageno<pagenum; pageno++)
493 names << djview->pageName(pageno);
494 endInsertRows();
495 }
496 connect(doc, SIGNAL(thumbnail(int)),
497 this, SLOT(thumbnail(int)) );
498 connect(doc, SIGNAL(pageinfo()),
499 this, SLOT(scheduleRefresh()) );
500 connect(doc, SIGNAL(idle()),
501 this, SLOT(scheduleRefresh()) );
502 widget->pageChanged(djview->getDjVuWidget()->page());
503 scheduleRefresh();
504 }
505
506
507 void
thumbnail(int pageno)508 QDjViewThumbnails::Model::thumbnail(int pageno)
509 {
510 QModelIndex mi = index(pageno);
511 emit dataChanged(mi, mi);
512 scheduleRefresh();
513 }
514
515
516 void
scheduleRefresh()517 QDjViewThumbnails::Model::scheduleRefresh()
518 {
519 if (! refreshScheduled)
520 QTimer::singleShot(0, this, SLOT(refresh()));
521 refreshScheduled = true;
522 }
523
524
525 void
refresh()526 QDjViewThumbnails::Model::refresh()
527 {
528 QDjVuDocument *doc = djview->getDocument();
529 ddjvu_status_t status;
530 refreshScheduled = false;
531 if (doc && pageInProgress >= 0)
532 {
533 status = ddjvu_thumbnail_status(*doc, pageInProgress, 0);
534 if (status >= DDJVU_JOB_OK)
535 pageInProgress = -1;
536 }
537 if (doc && pageInProgress < 0 && widget->isVisible())
538 {
539 QRect dr = widget->view->rect();
540 for (int i=0; i<names.size(); i++)
541 {
542 QModelIndex mi = index(i);
543 if (dr.intersects(widget->view->visualRect(mi)))
544 {
545 status = ddjvu_thumbnail_status(*doc, i, 0);
546 if (status == DDJVU_JOB_NOTSTARTED)
547 {
548 if (smart && !ddjvu_document_check_pagedata(*doc, i))
549 continue;
550 status = ddjvu_thumbnail_status(*doc, i, 1);
551 if (status == DDJVU_JOB_STARTED)
552 {
553 pageInProgress = i;
554 break;
555 }
556 }
557 }
558 }
559 }
560 }
561
562
563 void
setSmart(bool b)564 QDjViewThumbnails::Model::setSmart(bool b)
565 {
566 if (b != smart)
567 {
568 smart = b;
569 scheduleRefresh();
570 }
571 }
572
573
574 void
setSize(int newSize)575 QDjViewThumbnails::Model::setSize(int newSize)
576 {
577 newSize = qBound(16, newSize, 256);
578 if (newSize != size)
579 {
580 size = newSize;
581 QPixmap pixmap(size,size);
582 pixmap.fill();
583 QPainter painter;
584 int s8 = size/8;
585 if (s8 >= 1)
586 {
587 QPolygon poly;
588 poly << QPoint(s8,0)
589 << QPoint(size-2*s8,0)
590 << QPoint(size-s8-1,s8)
591 << QPoint(size-s8-1,size-1)
592 << QPoint(s8,size-1);
593 QPainter painter(&pixmap);
594 painter.setBrush(Qt::NoBrush);
595 painter.setPen(Qt::darkGray);
596 painter.drawPolygon(poly);
597 }
598 icon = QIcon(pixmap);
599 }
600 emit layoutChanged();
601 }
602
603
604 QIcon
makeIcon(int pageno) const605 QDjViewThumbnails::Model::makeIcon(int pageno) const
606 {
607 QDjVuDocument *doc = djview->getDocument();
608 if (doc)
609 {
610 // render thumbnail
611 #if QT_VERSION >= 0x50200
612 int dpr = djview->devicePixelRatio();
613 #else
614 int dpr = 1;
615 #endif
616 int w = size * dpr;
617 int h = size * dpr;
618 QImage img(size*dpr, size*dpr, QImage::Format_RGB32);
619 int status = ddjvu_thumbnail_status(*doc, pageno, 0);
620 if (status == DDJVU_JOB_NOTSTARTED)
621 {
622 const_cast<Model*>(this)->scheduleRefresh();
623 }
624 else if (ddjvu_thumbnail_render(*doc, pageno, &w, &h, format,
625 img.bytesPerLine(), (char*)img.bits() ))
626 {
627 QPixmap pixmap(size*dpr,size*dpr);
628 pixmap.fill();
629 QPoint dst((size*dpr-w)/2, (size*dpr-h)/2);
630 QRect src(0,0,w,h);
631 QPainter painter;
632 painter.begin(&pixmap);
633 painter.drawImage(dst, img, src);
634 painter.setBrush(Qt::NoBrush);
635 painter.setPen(Qt::darkGray);
636 painter.drawRect(dst.x(), dst.y(), w-1, h-1);
637 painter.end();
638 #if QT_VERSION >= 0x50200
639 pixmap.setDevicePixelRatio(dpr);
640 #endif
641 return QIcon(pixmap);
642 }
643 }
644 return icon;
645 }
646
647
648 QSize
makeHint(int) const649 QDjViewThumbnails::Model::makeHint(int) const
650 {
651 QFontMetrics metrics(widget->view->font());
652 return QSize(size, size+metrics.height());
653 }
654
655
656 int
rowCount(const QModelIndex &) const657 QDjViewThumbnails::Model::rowCount(const QModelIndex &) const
658 {
659 return names.size();
660 }
661
662
663 QVariant
data(const QModelIndex & index,int role) const664 QDjViewThumbnails::Model::data(const QModelIndex &index, int role) const
665 {
666 if (index.isValid())
667 {
668 int pageno = index.row();
669 if (pageno>=0 && pageno<names.size())
670 {
671 switch(role)
672 {
673 case Qt::DisplayRole:
674 case Qt::ToolTipRole:
675 return names[pageno];
676 case Qt::DecorationRole:
677 return makeIcon(pageno);
678 case Qt::WhatsThisRole:
679 return widget->whatsThis();
680 case Qt::UserRole:
681 return pageno;
682 case Qt::SizeHintRole:
683 return makeHint(pageno);
684 default:
685 break;
686 }
687 }
688 }
689 return QVariant();
690 }
691
692
693
694
695
696
697 // ----------------------------------------
698 // QDJVIEWTHUMBNAILS
699
700
QDjViewThumbnails(QDjView * djview)701 QDjViewThumbnails::QDjViewThumbnails(QDjView *djview)
702 : QWidget(djview),
703 djview(djview)
704 {
705 model = new Model(this);
706 selection = new QItemSelectionModel(model);
707 view = new View(this);
708 view->setModel(model);
709 view->setSelectionModel(selection);
710
711 QVBoxLayout *layout = new QVBoxLayout(this);
712 layout->setMargin(0);
713 layout->setSpacing(0);
714 layout->addWidget(view);
715
716 connect(djview->getDjVuWidget(), SIGNAL(pageChanged(int)),
717 this, SLOT(pageChanged(int)) );
718 connect(view, SIGNAL(activated(const QModelIndex&)),
719 this, SLOT(activated(const QModelIndex&)) );
720
721 menu = new QMenu(this);
722 QActionGroup *group = new QActionGroup(this);
723 QAction *action;
724 action = menu->addAction(tr("Tiny","thumbnail menu"));
725 connect(action,SIGNAL(triggered()),this,SLOT(setSize()) );
726 action->setCheckable(true);
727 action->setActionGroup(group);
728 action->setData(32);
729 action = menu->addAction(tr("Small","thumbnail menu"));
730 connect(action,SIGNAL(triggered()),this,SLOT(setSize()) );
731 action->setCheckable(true);
732 action->setActionGroup(group);
733 action->setData(64);
734 action = menu->addAction(tr("Medium","thumbnail menu"));
735 connect(action,SIGNAL(triggered()),this,SLOT(setSize()) );
736 action->setCheckable(true);
737 action->setActionGroup(group);
738 action->setData(96);
739 action = menu->addAction(tr("Large","thumbnail menu"));
740 connect(action,SIGNAL(triggered()),this,SLOT(setSize()) );
741 action->setCheckable(true);
742 action->setActionGroup(group);
743 action->setData(160);
744 menu->addSeparator();
745 action = menu->addAction(tr("Smart","thumbnail menu"));
746 connect(action,SIGNAL(toggled(bool)),this,SLOT(setSmart(bool)) );
747 action->setCheckable(true);
748 action->setData(true);
749 updateActions();
750
751 #ifdef Q_OS_DARWIN
752 QString mc = tr("Control Left Mouse Button");
753 #else
754 QString mc = tr("Right Mouse Button");
755 #endif
756 setWhatsThis(tr("<html><b>Document thumbnails.</b><br/> "
757 "This panel display thumbnails for the document pages. "
758 "Double click a thumbnail to jump to the selected page. "
759 "%1 to change the thumbnail size or the refresh mode. "
760 "The smart refresh mode only computes thumbnails "
761 "when the page data is present (displayed or cached.)"
762 "</html>").arg(mc) );
763 }
764
765
766 void
updateActions(void)767 QDjViewThumbnails::updateActions(void)
768 {
769 QAction *action;
770 int size = model->getSize();
771 bool smart = model->getSmart();
772 foreach(action, menu->actions())
773 {
774 QVariant data = action->data();
775 if (data.type() == QVariant::Bool)
776 action->setChecked(smart);
777 else
778 action->setChecked(data.toInt() == size);
779 }
780 }
781
782
783 void
pageChanged(int pageno)784 QDjViewThumbnails::pageChanged(int pageno)
785 {
786 if (pageno >= 0 && pageno < djview->pageNum())
787 {
788 QModelIndex mi = model->index(pageno);
789 if (! selection->isSelected(mi))
790 selection->select(mi, QItemSelectionModel::ClearAndSelect);
791 view->scrollTo(mi);
792 }
793 }
794
795
796 void
activated(const QModelIndex & index)797 QDjViewThumbnails::activated(const QModelIndex &index)
798 {
799 if (index.isValid())
800 {
801 int pageno = index.row();
802 if (pageno>=0 && pageno<djview->pageNum())
803 djview->goToPage(pageno);
804 }
805 }
806
807
808 int
size()809 QDjViewThumbnails::size()
810 {
811 return model->getSize();
812 }
813
814
815 void
setSize(int size)816 QDjViewThumbnails::setSize(int size)
817 {
818 model->setSize(size);
819 updateActions();
820 }
821
822
823 void
setSize()824 QDjViewThumbnails::setSize()
825 {
826 QAction *action = qobject_cast<QAction*>(sender());
827 if (action)
828 setSize(action->data().toInt());
829 }
830
831
832 bool
smart()833 QDjViewThumbnails::smart()
834 {
835 return model->getSmart();
836 }
837
838
839 void
setSmart(bool smart)840 QDjViewThumbnails::setSmart(bool smart)
841 {
842 model->setSmart(smart);
843 updateActions();
844 }
845
846 void
contextMenuEvent(QContextMenuEvent * event)847 QDjViewThumbnails::contextMenuEvent(QContextMenuEvent *event)
848 {
849 menu->exec(event->globalPos());
850 event->accept();
851 }
852
853
854
855
856
857
858
859 // =======================================
860 // QDJVIEWFIND
861 // =======================================
862
863
864 class QDjViewFind::Model : public QAbstractListModel
865 {
866 // This class implements the listview model.
867 // But the bulk contains private data for qdjviewfind!
868 Q_OBJECT
869 public:
870 Model(QDjViewFind*);
871 // model stuff
872 public:
873 virtual int rowCount(const QModelIndex &parent) const;
874 virtual QVariant data(const QModelIndex &index, int role) const;
875 void modelClear();
876 int modelFind(int pageno);
877 bool modelSelect(int pageno);
878 void modelAdd(int pageno, int hits);
879 private:
880 struct RowInfo { int pageno; int hits; QString name; };
881 QList<RowInfo> pages;
882 // private data stuff
883 public:
884 void nextHit(bool backwards);
885 void startFind(bool backwards, int delay=0);
886 void stopFind();
887 typedef QList<miniexp_t> Hit;
888 typedef QList<Hit> Hits;
889 QMap<int, Hits> hits;
890 protected:
891 virtual bool eventFilter(QObject*, QEvent*);
892 public slots:
893 void documentClosed(QDjVuDocument*);
894 void documentReady(QDjVuDocument*);
895 void clear();
896 void doHighlights(int pageno);
897 void doPending();
898 void workTimeout();
899 void animTimeout();
900 void makeSelectionVisible();
901 void pageChanged(int);
902 void textChanged();
903 void pageinfo();
904 void itemActivated(const QModelIndex&);
905 private:
906 friend class QDjViewFind;
907 QDjViewFind *widget;
908 QDjView *djview;
909 QTimer *animTimer;
910 QTimer *workTimer;
911 QItemSelectionModel *selection;
912 QAbstractButton *animButton;
913 QIcon animIcon;
914 QIcon findIcon;
915 QRegExp find;
916 int curWork;
917 int curPage;
918 int curHit;
919 bool searchBackwards;
920 bool caseSensitive;
921 bool wordOnly;
922 bool regExpMode;
923 bool working;
924 bool pending;
925 };
926
927
928 // ----------------------------------------
929 // QDJVIEWFIND::MODEL
930
931
Model(QDjViewFind * widget)932 QDjViewFind::Model::Model(QDjViewFind *widget)
933 : QAbstractListModel(widget),
934 widget(widget),
935 djview(widget->djview),
936 selection(0),
937 animButton(0),
938 curPage(0),
939 curHit(0),
940 searchBackwards(false),
941 caseSensitive(false),
942 wordOnly(true),
943 regExpMode(false),
944 working(false),
945 pending(false)
946 {
947 selection = new QItemSelectionModel(this);
948 animTimer = new QTimer(this);
949 workTimer = new QTimer(this);
950 workTimer->setSingleShot(true);
951 connect(animTimer, SIGNAL(timeout()), this, SLOT(animTimeout()));
952 connect(workTimer, SIGNAL(timeout()), this, SLOT(workTimeout()));
953 findIcon = QIcon(":/images/icon_empty.png");
954 }
955
956
957 int
rowCount(const QModelIndex &) const958 QDjViewFind::Model::rowCount(const QModelIndex&) const
959 {
960 return pages.size();
961 }
962
963
964 QVariant
data(const QModelIndex & index,int role) const965 QDjViewFind::Model::data(const QModelIndex &index, int role) const
966 {
967 if (index.isValid())
968 {
969 int row = index.row();
970 if (row>=0 && row<pages.size())
971 {
972 const RowInfo &info = pages[row];
973 switch(role)
974 {
975 case Qt::DisplayRole:
976 return info.name;
977 case Qt::ToolTipRole:
978 if (info.hits == 1)
979 return tr("1 hit");
980 return tr("%n hits", 0, info.hits);
981 case Qt::WhatsThisRole:
982 return widget->whatsThis();
983 default:
984 break;
985 }
986 }
987 }
988 return QVariant();
989 }
990
991
992 void
modelClear()993 QDjViewFind::Model::modelClear()
994 {
995 int nrows = pages.size();
996 if (nrows > 0)
997 {
998 beginRemoveRows(QModelIndex(), 0, nrows-1);
999 pages.clear();
1000 endRemoveRows();
1001 }
1002 }
1003
1004
1005 int
modelFind(int pageno)1006 QDjViewFind::Model::modelFind(int pageno)
1007 {
1008 int lo = 0;
1009 int hi = pages.size();
1010 while (lo < hi)
1011 {
1012 int k = (lo + hi - 1) / 2;
1013 if (pageno > pages[k].pageno)
1014 lo = k + 1;
1015 else if (pageno < pages[k].pageno)
1016 hi = k;
1017 else
1018 lo = hi = k;
1019 }
1020 return lo;
1021 }
1022
1023
1024 bool
modelSelect(int pageno)1025 QDjViewFind::Model::modelSelect(int pageno)
1026 {
1027 int lo = modelFind(pageno);
1028 QModelIndex mi = index(lo);
1029 if (lo < pages.size() && pages[lo].pageno == pageno)
1030 {
1031 if (!selection->isSelected(mi))
1032 selection->select(mi, QItemSelectionModel::ClearAndSelect);
1033 return true;
1034 }
1035 selection->select(mi, QItemSelectionModel::Clear);
1036 return false;
1037 }
1038
1039
1040 void
modelAdd(int pageno,int hits)1041 QDjViewFind::Model::modelAdd(int pageno, int hits)
1042 {
1043 QString name = djview->pageName(pageno);
1044 RowInfo info;
1045 info.pageno = pageno;
1046 info.hits = hits;
1047 if (hits == 1)
1048 info.name = tr("Page %1 (1 hit)").arg(name);
1049 else
1050 info.name = tr("Page %1 (%n hits)", 0, hits).arg(name);
1051 int lo = modelFind(pageno);
1052 if (lo < pages.size() && pages[lo].pageno == pageno)
1053 {
1054 pages[lo] = info;
1055 QModelIndex mi = index(lo);
1056 emit dataChanged(mi, mi);
1057 }
1058 else
1059 {
1060 beginInsertRows(QModelIndex(), lo, lo);
1061 pages.insert(lo, info);
1062 endInsertRows();
1063 if (pageno == djview->getDjVuWidget()->page())
1064 modelSelect(pageno);
1065 }
1066 }
1067
1068
1069 bool
eventFilter(QObject *,QEvent * event)1070 QDjViewFind::Model::eventFilter(QObject*, QEvent *event)
1071 {
1072 switch (event->type())
1073 {
1074 case QEvent::Show:
1075 if (working)
1076 {
1077 animTimer->start();
1078 workTimer->start();
1079 }
1080 default:
1081 break;
1082 }
1083 return false;
1084 }
1085
1086
1087 void
documentClosed(QDjVuDocument * doc)1088 QDjViewFind::Model::documentClosed(QDjVuDocument *doc)
1089 {
1090 disconnect(doc, 0, this, 0);
1091 stopFind();
1092 clear();
1093 curWork = 0;
1094 curPage = 0;
1095 curHit = -1;
1096 searchBackwards = false;
1097 pending = false;
1098 widget->eraseText();
1099 widget->combo->setEnabled(false);
1100 widget->label->setText(QString());
1101 widget->stack->setCurrentWidget(widget->label);
1102 }
1103
1104
1105 void
documentReady(QDjVuDocument * doc)1106 QDjViewFind::Model::documentReady(QDjVuDocument *doc)
1107 {
1108 curWork = djview->getDjVuWidget()->page();
1109 curPage = curWork;
1110 curHit = -1;
1111 if (doc)
1112 {
1113 widget->combo->setEnabled(true);
1114 connect(doc, SIGNAL(pageinfo()), this, SLOT(pageinfo()));
1115 connect(doc, SIGNAL(idle()), this, SLOT(pageinfo()));
1116 if (! find.isEmpty())
1117 startFind(false);
1118 }
1119 }
1120
1121
1122 static bool
miniexp_get_int(miniexp_t & r,int & x)1123 miniexp_get_int(miniexp_t &r, int &x)
1124 {
1125 if (! miniexp_numberp(miniexp_car(r)))
1126 return false;
1127 x = miniexp_to_int(miniexp_car(r));
1128 r = miniexp_cdr(r);
1129 return true;
1130 }
1131
1132
1133 static bool
miniexp_get_rect(miniexp_t & r,QRect & rect)1134 miniexp_get_rect(miniexp_t &r, QRect &rect)
1135 {
1136 int x1,y1,x2,y2;
1137 if (! (miniexp_get_int(r, x1) && miniexp_get_int(r, y1) &&
1138 miniexp_get_int(r, x2) && miniexp_get_int(r, y2) ))
1139 return false;
1140 if (x2<x1 || y2<y1)
1141 return false;
1142 rect.setCoords(x1, y1, x2, y2);
1143 return true;
1144 }
1145
1146
1147 static int
parse_text_type(miniexp_t exp)1148 parse_text_type(miniexp_t exp)
1149 {
1150 static const char *names[] = {
1151 "char", "word", "line", "para", "region", "column", "page"
1152 };
1153 static const int nsymbs = sizeof(names)/sizeof(const char*);
1154 static miniexp_t symbs[nsymbs];
1155 if (! symbs[0])
1156 for (int i=0; i<nsymbs; i++)
1157 symbs[i] = miniexp_symbol(names[i]);
1158 for (int i=0; i<nsymbs; i++)
1159 if (exp == symbs[i])
1160 return i;
1161 return -1;
1162 }
1163
count_final_spaces(const QString & s)1164 static int count_final_spaces(const QString &s)
1165 {
1166 int c = 0;
1167 int l = s.size();
1168 while (l>0 && s[--l].isSpace())
1169 c += 1;
1170 return c;
1171 }
1172
chopped(QString s,int c)1173 static QString chopped(QString s, int c)
1174 {
1175 s.chop(c);
1176 return s;
1177 }
1178
1179 static bool
miniexp_get_text(miniexp_t exp,QString & result,QMap<int,miniexp_t> & positions,int & state)1180 miniexp_get_text(miniexp_t exp, QString &result,
1181 QMap<int,miniexp_t> &positions, int &state)
1182 {
1183 miniexp_t type = miniexp_car(exp);
1184 int typenum = parse_text_type(type);
1185 miniexp_t r = exp = miniexp_cdr(exp);
1186 if (! miniexp_symbolp(type))
1187 return false;
1188 QRect rect;
1189 if (! miniexp_get_rect(r, rect))
1190 return false;
1191 miniexp_t s = miniexp_car(r);
1192 state = qMax(state, typenum);
1193 if (miniexp_stringp(s) && !miniexp_cdr(r))
1194 {
1195 int c = count_final_spaces(result);
1196 if (state >= 2 && !result.endsWith('\n'))
1197 result = chopped(result,c) + "\n";
1198 else if (state >= 1 && c == 0)
1199 result += " ";
1200 state = -1;
1201 positions[result.size()] = exp;
1202 result += QString::fromUtf8(miniexp_to_str(s)).trimmed();
1203 r = miniexp_cdr(r);
1204 }
1205 while(miniexp_consp(s))
1206 {
1207 miniexp_get_text(s, result, positions, state);
1208 r = miniexp_cdr(r);
1209 s = miniexp_car(r);
1210 }
1211 if (r)
1212 return false;
1213 state = qMax(state, typenum);
1214 return true;
1215 }
1216
1217
1218 static QList<QList<miniexp_t> >
miniexp_search_text(miniexp_t exp,QRegExp regex)1219 miniexp_search_text(miniexp_t exp, QRegExp regex)
1220 {
1221 QList<QList<miniexp_t> > hits;
1222 QString text;
1223 QMap<int, miniexp_t> positions;
1224 // build string
1225 int state = -1;
1226 if (! miniexp_get_text(exp, text, positions, state))
1227 return hits;
1228 // search hits
1229 int offset = 0;
1230 int match;
1231 while ((match = regex.indexIn(text, offset)) >= 0)
1232 {
1233 QList<miniexp_t> hit;
1234 int endmatch = match + regex.matchedLength();
1235 offset += 1;
1236 if (endmatch <= match)
1237 continue;
1238 QMap<int,miniexp_t>::const_iterator pos = positions.lowerBound(match);
1239 while (pos != positions.begin() && pos.key() > match)
1240 --pos;
1241 for (; pos != positions.end() && pos.key() < endmatch; ++pos)
1242 hit += pos.value();
1243 hits += hit;
1244 if (pos != positions.end())
1245 offset = pos.key();
1246 else
1247 break;
1248 }
1249 return hits;
1250 }
1251
1252
1253 void
clear()1254 QDjViewFind::Model::clear()
1255 {
1256 QDjVuWidget *djvuWidget = djview->getDjVuWidget();
1257 QMap<int,Hits>::const_iterator pos;
1258 for (pos=hits.begin(); pos!=hits.end(); ++pos)
1259 if (pos.value().size() > 0)
1260 djvuWidget->clearHighlights(pos.key());
1261 pending = false;
1262 hits.clear();
1263 modelClear();
1264 }
1265
1266
1267 void
doHighlights(int pageno)1268 QDjViewFind::Model::doHighlights(int pageno)
1269 {
1270 if (hits.contains(pageno))
1271 {
1272 QColor color = Qt::blue;
1273 QDjVuWidget *djvu = djview->getDjVuWidget();
1274 Hit pageHit;
1275 color.setAlpha(96);
1276 foreach(pageHit, hits[pageno])
1277 {
1278 QRect rect;
1279 miniexp_t exp;
1280 foreach(exp, pageHit)
1281 {
1282 if (miniexp_get_rect(exp, rect))
1283 djvu->addHighlight(pageno, rect.x(), rect.y(),
1284 rect.width(), rect.height(),
1285 color, true );
1286 }
1287 }
1288 }
1289 }
1290
1291
1292 void
doPending()1293 QDjViewFind::Model::doPending()
1294 {
1295 QDjVuWidget *djvu = djview->getDjVuWidget();
1296 while (pending && hits.contains(curPage) && pages.size()>0)
1297 {
1298 Hits pageHits = hits[curPage];
1299 int nhits = pageHits.size();
1300 if (searchBackwards)
1301 {
1302 if (curHit < 0 || curHit >= nhits)
1303 curHit = nhits;
1304 curHit--;
1305 }
1306 else
1307 {
1308 if (curHit < 0 || curHit >= nhits)
1309 curHit = -1;
1310 curHit++;
1311 }
1312 if (curHit >= 0 && curHit < nhits)
1313 {
1314 // jump to position
1315 pending = false;
1316 Hit hit = pageHits[curHit];
1317 QRect rect;
1318 if (hit.size() > 0 && miniexp_get_rect(hit[0], rect))
1319 {
1320 QDjVuWidget::Position pos;
1321 pos.pageNo = curPage;
1322 pos.posPage = rect.center();
1323 pos.inPage = true;
1324 djvu->setPosition(pos, djvu->viewport()->rect().center());
1325 }
1326 }
1327 else
1328 {
1329 // next page
1330 curHit = -1;
1331 if (searchBackwards)
1332 {
1333 curPage -= 1;
1334 if (curPage < 0)
1335 curPage = djview->pageNum() - 1;
1336 }
1337 else
1338 {
1339 curPage += 1;
1340 if (curPage >= djview->pageNum())
1341 curPage = 0;
1342 }
1343 }
1344 }
1345 if (! pending)
1346 djview->statusMessage(QString());
1347 }
1348
1349
1350 void
workTimeout()1351 QDjViewFind::Model::workTimeout()
1352 {
1353 // do some work
1354 int startingPoint = curWork;
1355 bool somePagesWithText = false;
1356 doPending();
1357 while (working)
1358 {
1359 if (hits.contains(curWork))
1360 {
1361 somePagesWithText = true;
1362 }
1363 else
1364 {
1365 QString name = djview->pageName(curWork);
1366 QDjVuDocument *doc = djview->getDocument();
1367 miniexp_t exp = doc->getPageText(curWork, false);
1368 if (exp == miniexp_dummy)
1369 {
1370 // data not present
1371 if (pending)
1372 djview->statusMessage(tr("Searching page %1 (waiting for data.)")
1373 .arg(name) );
1374 if (pending || widget->isVisible())
1375 doc->getPageText(curWork, true);
1376 // timer will be reactivated by pageinfo()
1377 return;
1378 }
1379 Hits pageHits;
1380 hits[curWork] = pageHits;
1381 if (exp != miniexp_nil)
1382 {
1383 somePagesWithText = true;
1384 if (pending)
1385 djview->statusMessage(tr("Searching page %1.").arg(name));
1386 pageHits = miniexp_search_text(exp, find);
1387 hits[curWork] = pageHits;
1388 if (pageHits.size() > 0)
1389 {
1390 modelAdd(curWork, pageHits.size());
1391 doHighlights(curWork);
1392 doPending();
1393 makeSelectionVisible();
1394 }
1395 // enough
1396 break;
1397 }
1398 }
1399 // next page
1400 int pageNum = djview->pageNum();
1401 if (searchBackwards)
1402 {
1403 if (curWork <= 0)
1404 curWork = pageNum;
1405 curWork -= 1;
1406 }
1407 else
1408 {
1409 curWork += 1;
1410 if (curWork >= pageNum)
1411 curWork = 0;
1412 }
1413 // finished?
1414 if (curWork == startingPoint)
1415 {
1416 stopFind();
1417 djview->statusMessage(QString());
1418 if (! pages.size())
1419 {
1420 QString msg = tr("No hits!");
1421 if (! somePagesWithText)
1422 {
1423 widget->eraseText();
1424 widget->combo->setEnabled(false);
1425 msg = tr("<html>Document is not searchable. "
1426 "No page contains information "
1427 "about its textual content.</html>");
1428 }
1429 else if (! find.isValid())
1430 {
1431 msg = tr("<html>Invalid regular expression.</html>");
1432 }
1433 widget->stack->setCurrentWidget(widget->label);
1434 widget->label->setText(msg);
1435 }
1436 }
1437 }
1438 // restart timer
1439 if (working)
1440 workTimer->start(0);
1441 }
1442
1443
1444 void
animTimeout()1445 QDjViewFind::Model::animTimeout()
1446 {
1447 if (animButton && !animIcon.isNull())
1448 {
1449 if (animButton->icon().cacheKey() == findIcon.cacheKey())
1450 animButton->setIcon(animIcon);
1451 else
1452 animButton->setIcon(findIcon);
1453 }
1454 }
1455
1456
1457 void
nextHit(bool backwards)1458 QDjViewFind::Model::nextHit(bool backwards)
1459 {
1460 djview->getDjVuWidget()->terminateAnimation();
1461 if (working && backwards != searchBackwards)
1462 {
1463 pending = false;
1464 startFind(backwards);
1465 }
1466 searchBackwards = backwards;
1467 if (! find.isEmpty())
1468 {
1469 pending = true;
1470 doPending();
1471 if (working && pending && !workTimer->isActive())
1472 workTimer->start(0);
1473 }
1474 }
1475
1476
1477 void
startFind(bool backwards,int delay)1478 QDjViewFind::Model::startFind(bool backwards, int delay)
1479 {
1480 stopFind();
1481 searchBackwards = backwards;
1482 if (! find.isEmpty() && djview->pageNum() > 0)
1483 {
1484 widget->label->setText(QString());
1485 widget->stack->setCurrentWidget(widget->view);
1486 animButton = (backwards) ? widget->upButton : widget->downButton;
1487 animIcon = animButton->icon();
1488 animTimer->start(250);
1489 workTimer->start(delay);
1490 curWork = djview->getDjVuWidget()->page();
1491 working = true;
1492 }
1493 }
1494
1495
1496 void
stopFind()1497 QDjViewFind::Model::stopFind()
1498 {
1499 animTimer->stop();
1500 workTimer->stop();
1501 if (animButton)
1502 {
1503 animButton->setIcon(animIcon);
1504 animButton = 0;
1505 }
1506 working = false;
1507 }
1508
1509
1510 void
pageinfo()1511 QDjViewFind::Model::pageinfo()
1512 {
1513 if (working && !workTimer->isActive())
1514 workTimer->start(0);
1515 }
1516
1517
1518 void
makeSelectionVisible()1519 QDjViewFind::Model::makeSelectionVisible()
1520 {
1521 QModelIndexList s = selection->selectedIndexes();
1522 if (s.size() > 0)
1523 widget->view->scrollTo(s[0]);
1524 }
1525
1526
1527 void
pageChanged(int pageno)1528 QDjViewFind::Model::pageChanged(int pageno)
1529 {
1530 QDjVuWidget *djvu = djview->getDjVuWidget();
1531 if (djvu && djvu->animationInProgress())
1532 return;
1533 if (pageno != curPage)
1534 {
1535 curHit = -1;
1536 curPage = pageno;
1537 pending = false;
1538 }
1539 curWork = pageno;
1540 }
1541
1542
1543 void
textChanged()1544 QDjViewFind::Model::textChanged()
1545 {
1546 stopFind();
1547 clear();
1548 QString s = widget->text();
1549 widget->label->setText(QString());
1550 widget->stack->setCurrentWidget(widget->label);
1551 if (s.isEmpty())
1552 {
1553 find = QRegExp();
1554 }
1555 else
1556 {
1557 if (!regExpMode)
1558 {
1559 s = QRegExp::escape(widget->text());
1560 s.replace(QRegExp("\\s+"), " ");
1561 }
1562 if (wordOnly)
1563 s = "\\b" + s;
1564 find = QRegExp(s);
1565 if (caseSensitive)
1566 find.setCaseSensitivity(Qt::CaseSensitive);
1567 else
1568 find.setCaseSensitivity(Qt::CaseInsensitive);
1569 startFind(searchBackwards, 250);
1570 }
1571 }
1572
1573
1574 void
itemActivated(const QModelIndex & mi)1575 QDjViewFind::Model::itemActivated(const QModelIndex &mi)
1576 {
1577 if (mi.isValid())
1578 {
1579 int row = mi.row();
1580 if (row >= 0 && row < pages.size() && pages[row].hits > 0)
1581 {
1582 curPage = pages[row].pageno;
1583 curHit = -1;
1584 pending = true;
1585 doPending();
1586 }
1587 }
1588 }
1589
1590
1591
1592 // ----------------------------------------
1593 // QDJVIEWFIND
1594
1595
QDjViewFind(QDjView * djview)1596 QDjViewFind::QDjViewFind(QDjView *djview)
1597 : QWidget(djview),
1598 djview(djview),
1599 model(0),
1600 view(0)
1601 {
1602 model = new Model(this);
1603 installEventFilter(model);
1604
1605 view = new QListView(this);
1606 view->setModel(model);
1607 view->setSelectionModel(model->selection);
1608 view->setDragEnabled(false);
1609 view->setEditTriggers(QAbstractItemView::NoEditTriggers);
1610 view->setSelectionBehavior(QAbstractItemView::SelectRows);
1611 view->setSelectionMode(QAbstractItemView::SingleSelection);
1612 view->setTextElideMode(Qt::ElideMiddle);
1613 view->setViewMode(QListView::ListMode);
1614 view->setWrapping(false);
1615 view->setResizeMode(QListView::Adjust);
1616 caseSensitiveAction = new QAction(tr("Case sensitive"), this);
1617 caseSensitiveAction->setCheckable(true);
1618 caseSensitiveAction->setChecked(model->caseSensitive);
1619 wordOnlyAction = new QAction(tr("Words only"), this);
1620 wordOnlyAction->setCheckable(true);
1621 wordOnlyAction->setChecked(model->wordOnly);
1622 regExpModeAction = new QAction(tr("Regular expression"), this);
1623 regExpModeAction->setCheckable(true);
1624 regExpModeAction->setChecked(model->regExpMode);
1625 menu = new QMenu(this);
1626 menu->addAction(caseSensitiveAction);
1627 menu->addAction(wordOnlyAction);
1628 menu->addAction(regExpModeAction);
1629 QBoxLayout *vlayout = new QVBoxLayout(this);
1630 combo = new QComboBox(this);
1631 combo->setEditable(true);
1632 combo->setMaxCount(8);
1633 combo->setInsertPolicy(QComboBox::InsertAtTop);
1634 combo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
1635 vlayout->addWidget(combo);
1636 QBoxLayout *hlayout = new QHBoxLayout;
1637 hlayout->setSpacing(0);
1638 vlayout->addLayout(hlayout);
1639 upButton = new QToolButton(this);
1640 upButton->setIcon(QIcon(":/images/icon_up.png"));
1641 upButton->setToolTip(tr("Find Previous (Shift+F3) "));
1642 upButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
1643 hlayout->addWidget(upButton);
1644 downButton = new QToolButton(this);
1645 downButton->setIcon(QIcon(":/images/icon_down.png"));
1646 downButton->setToolTip(tr("Find Next (F3) "));
1647 downButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
1648 hlayout->addWidget(downButton);
1649 hlayout->addStretch(2);
1650 resetButton = new QToolButton(this);
1651 resetButton->setIcon(QIcon(":/images/icon_erase.png"));
1652 resetButton->setToolTip(tr("Reset search options to default values."));
1653 resetButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
1654 hlayout->addWidget(resetButton);
1655 QToolButton *optionButton = new QToolButton(this);
1656 optionButton->setText(tr("Options"));
1657 optionButton->setPopupMode(QToolButton::InstantPopup);
1658 optionButton->setToolButtonStyle(Qt::ToolButtonTextOnly);
1659 optionButton->setMenu(menu);
1660 optionButton->setAttribute(Qt::WA_CustomWhatsThis);
1661 optionButton->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
1662 hlayout->addWidget(optionButton);
1663 stack = new QStackedLayout();
1664 view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
1665 view->setFrameShadow(QFrame::Sunken);
1666 view->setFrameShape(QFrame::StyledPanel);
1667 stack->addWidget(view);
1668 label = new QLabel(this);
1669 label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
1670 label->setAlignment(Qt::AlignCenter);
1671 label->setWordWrap(true);
1672 label->setFrameShadow(QFrame::Sunken);
1673 label->setFrameShape(QFrame::StyledPanel);
1674 stack->addWidget(label);
1675 vlayout->addLayout(stack);
1676
1677 connect(djview, SIGNAL(documentClosed(QDjVuDocument*)),
1678 model, SLOT(documentClosed(QDjVuDocument*)) );
1679 connect(djview, SIGNAL(documentReady(QDjVuDocument*)),
1680 model, SLOT(documentReady(QDjVuDocument*)) );
1681 connect(view, SIGNAL(activated(const QModelIndex&)),
1682 model, SLOT(itemActivated(const QModelIndex&)));
1683 connect(djview->getDjVuWidget(), SIGNAL(pageChanged(int)),
1684 this, SLOT(pageChanged(int)));
1685 connect(combo->lineEdit(), SIGNAL(textChanged(QString)),
1686 model, SLOT(textChanged()));
1687 connect(combo->lineEdit(), SIGNAL(returnPressed()),
1688 this, SLOT(findAgain()));
1689 connect(caseSensitiveAction, SIGNAL(triggered(bool)),
1690 this, SLOT(setCaseSensitive(bool)));
1691 connect(wordOnlyAction, SIGNAL(triggered(bool)),
1692 this, SLOT(setWordOnly(bool)));
1693 connect(regExpModeAction, SIGNAL(triggered(bool)),
1694 this, SLOT(setRegExpMode(bool)));
1695 connect(upButton, SIGNAL(clicked()),
1696 this, SLOT(findPrev()));
1697 connect(downButton, SIGNAL(clicked()),
1698 this, SLOT(findNext()));
1699 connect(resetButton, SIGNAL(clicked()),
1700 this, SLOT(eraseText()));
1701
1702 setWhatsThis
1703 (tr("<html><b>Finding text.</b><br/> "
1704 "Search hits appear progressively as soon as you type "
1705 "a search string. Typing enter jumps to the next hit. "
1706 "To move to the previous or next hit, you can also use "
1707 "the arrow buttons or the shortcuts <tt>F3</tt> or "
1708 "<tt>Shift-F3</tt>. You can also double click a page name. "
1709 "Use the <tt>Options</tt> menu to search words only or "
1710 "to specify the case sensitivity."
1711 "</html>"));
1712 wordOnlyAction->setStatusTip
1713 (tr("Specify whether search hits must begin on a word boundary."));
1714 caseSensitiveAction->setStatusTip
1715 (tr("Specify whether searches are case sensitive."));
1716 regExpModeAction->setStatusTip
1717 (tr("Regular expressions describe complex string matching patterns."));
1718 regExpModeAction->setWhatsThis
1719 (tr("<html><b>Regular Expression Quick Guide</b><ul>"
1720 "<li>The dot <tt>.</tt> matches any character.</li>"
1721 "<li>Most characters match themselves.</li>"
1722 "<li>Prepend a backslash <tt>\\</tt> to match special"
1723 " characters <tt>()[]{}|*+.?!^$\\</tt>.</li>"
1724 "<li><tt>\\b</tt> matches a word boundary.</li>"
1725 "<li><tt>\\w</tt> matches a word character.</li>"
1726 "<li><tt>\\d</tt> matches a digit character.</li>"
1727 "<li><tt>\\s</tt> matches a blank character.</li>"
1728 "<li><tt>\\n</tt> matches a newline character.</li>"
1729 "<li><tt>[<i>a</i>-<i>b</i>]</tt> matches characters in range"
1730 " <tt><i>a</i></tt>-<tt><i>b</i></tt>.</li>"
1731 "<li><tt>[^<i>a</i>-<i>b</i>]</tt> matches characters outside range"
1732 " <tt><i>a</i></tt>-<tt><i>b</i></tt>.</li>"
1733 "<li><tt><i>a</i>|<i>b</i></tt> matches either regular expression"
1734 " <tt><i>a</i></tt> or regular expression <tt><i>b</i></tt>.</li>"
1735 "<li><tt><i>a</i>{<i>n</i>,<i>m</i>}</tt> matches regular expression"
1736 " <tt><i>a</i></tt> repeated <tt><i>n</i></tt> to <tt><i>m</i></tt>"
1737 " times.</li>"
1738 "<li><tt><i>a</i>?</tt>, <tt><i>a</i>*</tt>, and <tt><i>a</i>+</tt>"
1739 " are shorthands for <tt><i>a</i>{0,1}</tt>, <tt><i>a</i>{0,}</tt>, "
1740 " and <tt><i>a</i>{1,}</tt>.</li>"
1741 "<li>Use parentheses <tt>()</tt> to group regular expressions "
1742 " before <tt>?+*{</tt>.</li>"
1743 "</ul></html>"));
1744
1745 eraseText();
1746 combo->setEnabled(false);
1747 label->setText(QString());
1748 stack->setCurrentWidget(label);
1749 if (djview->getDocument())
1750 model->documentReady(djview->getDocument());
1751 }
1752
1753
1754 void
contextMenuEvent(QContextMenuEvent * event)1755 QDjViewFind::contextMenuEvent(QContextMenuEvent *event)
1756 {
1757 menu->exec(event->globalPos());
1758 event->accept();
1759 }
1760
1761
1762 void
takeFocus(Qt::FocusReason reason)1763 QDjViewFind::takeFocus(Qt::FocusReason reason)
1764 {
1765 if (combo->isVisible())
1766 combo->setFocus(reason);
1767 }
1768
1769
1770 QString
text()1771 QDjViewFind::text()
1772 {
1773 return combo->lineEdit()->text();
1774 }
1775
1776
1777 bool
caseSensitive()1778 QDjViewFind::caseSensitive()
1779 {
1780 return model->caseSensitive;
1781 }
1782
1783
1784 bool
wordOnly()1785 QDjViewFind::wordOnly()
1786 {
1787 return model->wordOnly;
1788 }
1789
1790
1791 bool
regExpMode()1792 QDjViewFind::regExpMode()
1793 {
1794 return model->regExpMode;
1795 }
1796
1797
1798 void
setText(QString s)1799 QDjViewFind::setText(QString s)
1800 {
1801 if (s != text())
1802 combo->lineEdit()->setText(s);
1803 }
1804
1805
1806 void
selectAll()1807 QDjViewFind::selectAll()
1808 {
1809 combo->lineEdit()->selectAll();
1810 combo->lineEdit()->setFocus();
1811 }
1812
1813
1814 void
eraseText()1815 QDjViewFind::eraseText()
1816 {
1817 setText(QString());
1818 setRegExpMode(false);
1819 setWordOnly(true);
1820 setCaseSensitive(false);
1821 }
1822
1823
1824 void
setCaseSensitive(bool b)1825 QDjViewFind::setCaseSensitive(bool b)
1826 {
1827 if (b != model->caseSensitive)
1828 {
1829 caseSensitiveAction->setChecked(b);
1830 model->caseSensitive = b;
1831 model->textChanged();
1832 }
1833 }
1834
1835
1836 void
setWordOnly(bool b)1837 QDjViewFind::setWordOnly(bool b)
1838 {
1839 if (b != model->wordOnly)
1840 {
1841 wordOnlyAction->setChecked(b);
1842 model->wordOnly = b;
1843 model->textChanged();
1844 }
1845 }
1846
1847
1848 void
setRegExpMode(bool b)1849 QDjViewFind::setRegExpMode(bool b)
1850 {
1851 if (b != model->regExpMode)
1852 {
1853 regExpModeAction->setChecked(b);
1854 model->regExpMode = b;
1855 model->textChanged();
1856 }
1857 }
1858
1859
1860 void
findNext()1861 QDjViewFind::findNext()
1862 {
1863 if (text().isEmpty())
1864 djview->showFind();
1865 model->nextHit(false);
1866 }
1867
1868
1869 void
findPrev()1870 QDjViewFind::findPrev()
1871 {
1872 if (text().isEmpty())
1873 djview->showFind();
1874 model->nextHit(true);
1875 }
1876
1877
1878 void
findAgain()1879 QDjViewFind::findAgain()
1880 {
1881 if (model->searchBackwards)
1882 findPrev();
1883 else
1884 findNext();
1885 }
1886
1887
1888 void
pageChanged(int pageno)1889 QDjViewFind::pageChanged(int pageno)
1890 {
1891 if (pageno >= 0 && pageno < djview->pageNum())
1892 {
1893 model->pageChanged(pageno);
1894 if (model->modelSelect(pageno))
1895 model->makeSelectionVisible();
1896 }
1897 }
1898
1899
1900
1901
1902
1903
1904
1905
1906 // ----------------------------------------
1907 // MOC
1908
1909 #include "qdjviewsidebar.moc"
1910
1911
1912
1913
1914 /* -------------------------------------------------------------
1915 Local Variables:
1916 c++-font-lock-extra-types: ( "\\sw+_t" "[A-Z]\\sw*[a-z]\\sw*" )
1917 End:
1918 ------------------------------------------------------------- */
1919