Yearly copyright bump :)
[quassel.git] / src / qtui / chatviewsearchcontroller.cpp
index 591c20e..997c195 100644 (file)
@@ -1,5 +1,5 @@
 /***************************************************************************
- *   Copyright (C) 2005-08 by the Quassel Project                          *
+ *   Copyright (C) 2005-09 by the Quassel Project                          *
  *   devel@quassel-irc.org                                                 *
  *                                                                         *
  *   This program is free software; you can redistribute it and/or modify  *
@@ -31,6 +31,7 @@
 ChatViewSearchController::ChatViewSearchController(QObject *parent)
   : QObject(parent),
     _scene(0),
+    _currentHighlight(0),
     _caseSensitive(false),
     _searchSenders(false),
     _searchMsgs(true),
@@ -68,60 +69,175 @@ void ChatViewSearchController::setSearchString(const QString &searchString) {
     return;
 
   connect(_scene, SIGNAL(destroyed()), this, SLOT(sceneDestroyed()));
+  connect(_scene, SIGNAL(layoutChanged()), this, SLOT(repositionHighlights()));
   updateHighlights();
  }
 
+void ChatViewSearchController::highlightNext() {
+  if(_highlightItems.isEmpty())
+    return;
+
+  if(_currentHighlight < _highlightItems.count()) {
+    _highlightItems.at(_currentHighlight)->setHighlighted(false);
+  }
 
+  _currentHighlight++;
+  if(_currentHighlight >= _highlightItems.count())
+    _currentHighlight = 0;
+  _highlightItems.at(_currentHighlight)->setHighlighted(true);
+  emit newCurrentHighlight(_highlightItems.at(_currentHighlight));
+}
+
+void ChatViewSearchController::highlightPrev() {
+  if(_highlightItems.isEmpty())
+    return;
+
+  if(_currentHighlight < _highlightItems.count()) {
+    _highlightItems.at(_currentHighlight)->setHighlighted(false);
+  }
+
+  _currentHighlight--;
+  if(_currentHighlight < 0)
+    _currentHighlight = _highlightItems.count() - 1;
+  _highlightItems.at(_currentHighlight)->setHighlighted(true);
+  emit newCurrentHighlight(_highlightItems.at(_currentHighlight));
+}
 
 void ChatViewSearchController::updateHighlights(bool reuse) {
   if(!_scene)
     return;
 
+  if(reuse) {
+    QSet<ChatLine *> chatLines;
+    foreach(SearchHighlightItem *highlightItem, _highlightItems) {
+      ChatLine *line = qgraphicsitem_cast<ChatLine *>(highlightItem->parentItem());
+      if(line)
+      chatLines << line;
+    }
+    foreach(ChatLine *line, QList<ChatLine *>(chatLines.toList())) {
+      updateHighlights(line);
+    }
+  } else {
+    QPointF oldHighlightPos;
+    if(!_highlightItems.isEmpty() && _currentHighlight < _highlightItems.count()) {
+      oldHighlightPos = _highlightItems[_currentHighlight]->scenePos();
+    }
+    qDeleteAll(_highlightItems);
+    _highlightItems.clear();
+    Q_ASSERT(_highlightItems.isEmpty());
+
+    if(searchString().isEmpty() || !(_searchSenders || _searchMsgs))
+      return;
+
+    checkMessagesForHighlight();
+
+    if(!_highlightItems.isEmpty()) {
+      if(!oldHighlightPos.isNull()) {
+       int start = 0; int end = _highlightItems.count() - 1;
+       QPointF startPos;
+       QPointF endPos;
+       while(1) {
+         startPos = _highlightItems[start]->scenePos();
+         endPos = _highlightItems[end]->scenePos();
+         if(startPos == oldHighlightPos) {
+           _currentHighlight = start;
+           break;
+         }
+         if(endPos == oldHighlightPos) {
+           _currentHighlight = end;
+           break;
+         }
+         if(end - start == 1) {
+           _currentHighlight = start;
+           break;
+         }
+         int pivot = (end + start) / 2;
+         QPointF pivotPos = _highlightItems[pivot]->scenePos();
+         if(startPos.y() == endPos.y()) {
+           if(oldHighlightPos.x() <= pivotPos.x())
+             end = pivot;
+           else
+             start = pivot;
+         } else {
+           if(oldHighlightPos.y() <= pivotPos.y())
+             end = pivot;
+           else
+             start = pivot;
+         }
+       }
+      } else {
+       _currentHighlight = _highlightItems.count() - 1;
+      }
+      _highlightItems[_currentHighlight]->setHighlighted(true);
+      emit newCurrentHighlight(_highlightItems[_currentHighlight]);
+    }
+  }
+}
+
+void ChatViewSearchController::checkMessagesForHighlight(int start, int end) {
   QAbstractItemModel *model = _scene->model();
   Q_ASSERT(model);
 
+  if(end == -1) {
+    end = model->rowCount() - 1;
+    if(end == -1)
+      return;
+  }
 
-  QList<ChatLine *> chatLines;
-  if(reuse) {
-    foreach(SearchHighlightItem *highlightItem, _highlightItems) {
-      ChatLine *line = dynamic_cast<ChatLine *>(highlightItem->parentItem());
-      if(!line || chatLines.contains(line))
+  QModelIndex index;
+  for(int row = start; row <= end; row++) {
+    if(_searchOnlyRegularMsgs) {
+      index = model->index(row, 0);
+      if(!checkType((Message::Type)index.data(MessageModel::TypeRole).toInt()))
        continue;
-      chatLines << line;
+      highlightLine(_scene->chatLine(row));
     }
   }
+}
 
-  qDeleteAll(_highlightItems);
-  _highlightItems.clear();
-  Q_ASSERT(_highlightItems.isEmpty());
+void ChatViewSearchController::updateHighlights(ChatLine *line) {
+  QList<ChatItem *> checkItems;
+  if(_searchSenders)
+    checkItems << &(line->item(MessageModel::SenderColumn));
 
-  if(searchString().isEmpty() || !(_searchSenders || _searchMsgs))
-    return;
+  if(_searchMsgs)
+    checkItems << &(line->item(MessageModel::ContentsColumn));
 
-  if(reuse) {
-    QModelIndex index;
-    foreach(ChatLine *line, chatLines) {
-      if(_searchOnlyRegularMsgs) {
-       index = model->index(line->row(), 0);
-       if(!checkType((Message::Type)index.data(MessageModel::TypeRole).toInt()))
-         continue;
-      }
-      highlightLine(line);
+  QHash<quint64, QHash<quint64, QRectF> > wordRects;
+  foreach(ChatItem *item, checkItems) {
+    foreach(QRectF wordRect, item->findWords(searchString(), caseSensitive())) {
+      wordRects[wordRect.x() + item->x()][wordRect.y()] = wordRect;
     }
-  } else {
-    // we have to crawl through the data
-    QModelIndex index;
-    QString plainText;
-    int rowCount = model->rowCount();
-    for(int row = 0; row < rowCount; row++) {
-      ChatLine *line = _scene->chatLine(row);
-
-      if(_searchOnlyRegularMsgs) {
-       index = model->index(row, 0);
-       if(!checkType((Message::Type)index.data(MessageModel::TypeRole).toInt()))
-         continue;
+  }
+
+  bool deleteAll = false;
+  QAbstractItemModel *model = _scene->model();
+  Q_ASSERT(model);
+  if(_searchOnlyRegularMsgs) {
+    QModelIndex index = model->index(line->row(), 0);
+    if(!checkType((Message::Type)index.data(MessageModel::TypeRole).toInt()))
+      deleteAll = true;
+  }
+
+  
+  foreach(QGraphicsItem *child, line->childItems()) {
+    SearchHighlightItem *highlightItem = qgraphicsitem_cast<SearchHighlightItem *>(child);
+    if(!highlightItem)
+      continue;
+
+    if(!deleteAll && wordRects.contains(highlightItem->pos().x()) && wordRects[highlightItem->pos().x()].contains(highlightItem->pos().y())) {
+      QRectF &wordRect = wordRects[highlightItem->pos().x()][highlightItem->pos().y()];
+      highlightItem->updateGeometry(wordRect.width(), wordRect.height());
+    } else {
+      int pos = _highlightItems.indexOf(highlightItem);
+      if(pos == _currentHighlight) {
+       highlightPrev();
+      } else if (pos < _currentHighlight) {
+       _currentHighlight--;
       }
-      highlightLine(line);
+
+      _highlightItems.removeAt(pos);
+      delete highlightItem;
     }
   }
 }
@@ -141,6 +257,50 @@ void ChatViewSearchController::highlightLine(ChatLine *line) {
   }
 }
 
+void ChatViewSearchController::repositionHighlights() {
+  QSet<ChatLine *> chatLines;
+  foreach(SearchHighlightItem *item, _highlightItems) {
+    ChatLine *line = qgraphicsitem_cast<ChatLine *>(item->parentItem());
+    if(line)
+      chatLines << line;
+  }
+  QList<ChatLine *> chatLineList(chatLines.toList());
+  foreach(ChatLine *line, chatLineList) {
+    repositionHighlights(line);
+  }
+}
+
+void ChatViewSearchController::repositionHighlights(ChatLine *line) {
+  QList<SearchHighlightItem *> searchHighlights;
+  foreach(QGraphicsItem *child, line->childItems()) {
+    SearchHighlightItem *highlightItem = qgraphicsitem_cast<SearchHighlightItem *>(child);
+    if(highlightItem)
+      searchHighlights << highlightItem;
+  }
+
+  if(searchHighlights.isEmpty())
+    return;
+
+  QList<QPointF> wordPos;
+  if(_searchSenders) {
+    foreach(QRectF wordRect, line->senderItem().findWords(searchString(), caseSensitive())) {
+      wordPos << QPointF(wordRect.x() + line->senderItem().x(), wordRect.y());
+    }
+  }
+  if(_searchMsgs) {
+    foreach(QRectF wordRect, line->contentsItem().findWords(searchString(), caseSensitive())) {
+      wordPos << QPointF(wordRect.x() + line->contentsItem().x(), wordRect.y());
+    }
+  }
+
+  qSort(searchHighlights.begin(), searchHighlights.end(), SearchHighlightItem::firstInLine);
+
+  Q_ASSERT(wordPos.count() == searchHighlights.count());
+  for(int i = 0; i < searchHighlights.count(); i++) {
+    searchHighlights.at(i)->setPos(wordPos.at(i));
+  }
+}
+
 void ChatViewSearchController::sceneDestroyed() {
   // WARNING: don't call any methods on scene!
   _scene = 0;
@@ -192,21 +352,63 @@ void ChatViewSearchController::setSearchOnlyRegularMsgs(bool searchOnlyRegularMs
   updateHighlights(searchOnlyRegularMsgs);
 }
 
+
+// ==================================================
+//  SearchHighlightItem
+// ==================================================
 SearchHighlightItem::SearchHighlightItem(QRectF wordRect, QGraphicsItem *parent)
-  : QGraphicsItem(parent),
-    _boundingRect(QRectF(-wordRect.width() / 2, -wordRect.height() / 2, wordRect.width(), wordRect.height()))
+  : QObject(),
+    QGraphicsItem(parent),
+    _highlighted(false),
+    _alpha(100),
+    _timeLine(150)
 {
-  setPos(wordRect.x() + wordRect.width() / 2 , wordRect.y() + wordRect.height() / 2);
-  scale(1.2, 1.2);
+  setPos(wordRect.x(), wordRect.y());
+  updateGeometry(wordRect.width(), wordRect.height());
+
+  connect(&_timeLine, SIGNAL(valueChanged(qreal)), this, SLOT(updateHighlight(qreal)));
+}
+
+void SearchHighlightItem::setHighlighted(bool highlighted) {
+  _highlighted = highlighted;
+
+  if(highlighted)
+    _timeLine.setDirection(QTimeLine::Forward);
+  else
+    _timeLine.setDirection(QTimeLine::Backward);
+
+  if(_timeLine.state() != QTimeLine::Running)
+    _timeLine.start();
+
+  update();
+}
+
+void SearchHighlightItem::updateHighlight(qreal value) {
+  _alpha = 100 + 155 * value;
+  update();
 }
 
 void SearchHighlightItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) {
   Q_UNUSED(option);
   Q_UNUSED(widget);
 
-  painter->setPen(QPen(Qt::black, 1.5));
-  //painter->setBrush(QColor(127, 133, 250));
-  painter->setBrush(QColor(254, 237, 45));
+  painter->setPen(QPen(QColor(0, 0, 0, _alpha), 1.5));
+  painter->setBrush(QColor(254, 237, 45, _alpha));
   painter->setRenderHints(QPainter::Antialiasing);
-  painter->drawRoundedRect(boundingRect(), 30, 30, Qt::RelativeSize);
+  qreal radius = boundingRect().height() * 0.30;
+  painter->drawRoundedRect(boundingRect(), radius, radius);
+}
+
+void SearchHighlightItem::updateGeometry(qreal width, qreal height) {
+  prepareGeometryChange();
+  qreal sizedelta = height * 0.1;
+  _boundingRect = QRectF(-sizedelta, -sizedelta, width + 2 * sizedelta, height + 2 * sizedelta);
+  update();
+}
+
+bool SearchHighlightItem::firstInLine(QGraphicsItem *item1, QGraphicsItem *item2) {
+  if(item1->pos().y() != item2->pos().y())
+    return item1->pos().y() < item2->pos().y();
+  else
+    return item1->pos().x() < item2->pos().x();
 }