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