1 /*
2  * This file is part of Licq, an instant messaging client for UNIX.
3  * Copyright (C) 2007-2013 Licq developers <licq-dev@googlegroups.com>
4  *
5  * Licq is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * Licq is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with Licq; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
18  */
19 
20 #include "historydlg.h"
21 
22 #include "config.h"
23 
24 #include <cstdio>
25 #include <QCheckBox>
26 #include <QDialogButtonBox>
27 #include <QGroupBox>
28 #include <QLabel>
29 #include <QLineEdit>
30 #include <QHBoxLayout>
31 #include <QPushButton>
32 #include <QRegExp>
33 #include <QShortcut>
34 #include <QVBoxLayout>
35 
36 #include <licq/contactlist/owner.h>
37 #include <licq/contactlist/user.h>
38 #include <licq/contactlist/usermanager.h>
39 #include <licq/event.h>
40 #include <licq/pluginsignal.h>
41 #include <licq/userevents.h>
42 
43 #include "config/chat.h"
44 #include "core/licqgui.h"
45 #include "core/signalmanager.h"
46 #include "core/usermenu.h"
47 #include "helpers/support.h"
48 #include "widgets/calendar.h"
49 #include "widgets/historyview.h"
50 
51 
52 using namespace LicqQtGui;
53 /* TRANSLATOR LicqQtGui::HistoryDlg */
54 
HistoryDlg(const Licq::UserId & userId,QWidget * parent)55 HistoryDlg::HistoryDlg(const Licq::UserId& userId, QWidget* parent)
56   : QDialog(parent),
57     myUserId(userId)
58 {
59   Support::setWidgetProps(this, "UserHistoryDialog");
60   setAttribute(Qt::WA_DeleteOnClose, true);
61 
62   QVBoxLayout* topLayout = new QVBoxLayout(this);
63 
64   // Main content (everything except dialog buttons)
65   QHBoxLayout* mainLayout = new QHBoxLayout();
66   topLayout->addLayout(mainLayout);
67 
68   // Left sidebar with calendar and search controls
69   QVBoxLayout* sidebarLayout = new QVBoxLayout();
70   mainLayout->addLayout(sidebarLayout);
71 
72   // Calendar for navigating
73   myCalendar = new Calendar();
74   connect(myCalendar, SIGNAL(clicked(const QDate&)), SLOT(calenderClicked()));
75   sidebarLayout->addWidget(myCalendar);
76 
77   // Buttons to go to previos/next day with activity
78   QHBoxLayout* navigateLayout = new QHBoxLayout();
79   sidebarLayout->addLayout(navigateLayout);
80   QPushButton* previousDateButton = new QPushButton(tr("&Previous day"));
81   connect(previousDateButton, SIGNAL(clicked()), SLOT(previousDate()));
82   navigateLayout->addWidget(previousDateButton);
83   navigateLayout->addStretch(1);
84   QPushButton* nextDateButton = new QPushButton(tr("&Next day"));
85   connect(nextDateButton, SIGNAL(clicked()), SLOT(nextDate()));
86   navigateLayout->addWidget(nextDateButton);
87 
88   // Status label for showing various messages
89   myStatusLabel = new QLabel();
90   sidebarLayout->addWidget(myStatusLabel);
91 
92   sidebarLayout->addStretch(1);
93 
94   // Controls for searching history
95   QGroupBox* searchGroup = new QGroupBox(tr("Search"));
96   sidebarLayout->addWidget(searchGroup);
97   QVBoxLayout* searchLayout = new QVBoxLayout(searchGroup);
98 
99   // Input field for searching
100   QHBoxLayout* patternLayout = new QHBoxLayout();
101   searchLayout->addLayout(patternLayout);
102   QLabel* patternLabel = new QLabel(tr("Find:"));
103   patternLayout->addWidget(patternLabel);
104   myPatternEdit = new QLineEdit();
105   patternLayout->addWidget(myPatternEdit);
106   patternLabel->setBuddy(myPatternEdit);
107 
108   // Set focus to search box if user presses slash
109   QShortcut* patternShortcut = new QShortcut(Qt::Key_Slash, this);
110   connect(patternShortcut, SIGNAL(activated()), myPatternEdit, SLOT(setFocus()));
111 
112   // Options to control searching
113   myMatchCaseCheck = new QCheckBox(tr("Match &case"));
114   searchLayout->addWidget(myMatchCaseCheck);
115   myRegExpSearchCheck = new QCheckBox(tr("&Regular expression"));
116   searchLayout->addWidget(myRegExpSearchCheck);
117 
118   // Find button
119   QHBoxLayout* findLayout = new QHBoxLayout();
120   myFindPrevButton = new QPushButton(tr("F&ind previous"));
121   myFindPrevButton->setEnabled(false);
122   connect(myFindPrevButton, SIGNAL(clicked()), SLOT(findPrevious()));
123   findLayout->addWidget(myFindPrevButton);
124   findLayout->addStretch(1);
125   myFindNextButton = new QPushButton(tr("&Find next"));
126   myFindNextButton->setDefault(true);
127   myFindNextButton->setEnabled(false);
128   connect(myFindNextButton, SIGNAL(clicked()), SLOT(findNext()));
129   findLayout->addWidget(myFindNextButton);
130   searchLayout->addLayout(findLayout);
131   connect(myPatternEdit, SIGNAL(textChanged(const QString&)), SLOT(searchTextChanged(const QString&)));
132   myPatternChanged = true;
133 
134   // Shortcuts for searching
135   QShortcut* findPrevShortcut = new QShortcut(Qt::SHIFT + Qt::Key_F3, this);
136   connect(findPrevShortcut, SIGNAL(activated()), SLOT(findPrevious()));
137   QShortcut* findNextShortcut = new QShortcut(Qt::Key_F3, this);
138   connect(findNextShortcut, SIGNAL(activated()), SLOT(findNext()));
139 
140   // Widget to show history entries
141   myHistoryView = new HistoryView(true, myUserId);
142   mainLayout->addWidget(myHistoryView, 1);
143 
144   // Dialog buttons
145   QHBoxLayout* buttonsLayout = new QHBoxLayout();
146   topLayout->addLayout(buttonsLayout);
147   if (!myUserId.isOwner())
148   {
149     QPushButton* menuButton = new QPushButton(tr("&Menu"));
150     connect(menuButton, SIGNAL(pressed()), SLOT(showUserMenu()));
151     menuButton->setMenu(gUserMenu);
152     buttonsLayout->addWidget(menuButton);
153   }
154   QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Close);
155   connect(buttons, SIGNAL(rejected()), SLOT(close()));
156   buttonsLayout->addWidget(buttons);
157 
158   show();
159 
160   {
161     Licq::UserReadGuard u(myUserId);
162 
163     setTitle(*u);
164 
165     bool validHistory = false;
166 
167     if (!u.isLocked())
168     {
169       myStatusLabel->setText(tr("Invalid user requested"));
170     }
171     // Fetch list of all history entries
172     else if (!u->GetHistory(myHistoryList))
173     {
174       myStatusLabel->setText(tr("Error loading history file"));
175     }
176     // No point in doing anything more if history is empty
177     else if (myHistoryList.empty())
178     {
179       myStatusLabel->setText(tr("History is empty"));
180     }
181     else
182     {
183       // No problems that should stop us from continuing
184       validHistory = true;
185     }
186 
187     if (!validHistory)
188     {
189       myCalendar->setEnabled(false);
190       previousDateButton->setEnabled(false);
191       nextDateButton->setEnabled(false);
192       myPatternEdit->setEnabled(false);
193       myFindPrevButton->setEnabled(false);
194       myFindNextButton->setEnabled(false);
195       return;
196     }
197 
198     myContactName = tr("server");
199     myUseHtml = false;
200 
201     if (!myUserId.isOwner())
202       myContactName = QString::fromUtf8(u->getAlias().c_str());
203     if (u->protocolId() == ICQ_PPID)
204     {
205       QString myId = u->accountId().c_str();
206       for (int x = 0; x < myId.length(); x++)
207       {
208         if (!myId[x].isDigit())
209         {
210           myUseHtml = true;
211           break;
212         }
213       }
214     }
215   }
216 
217   {
218     Licq::OwnerReadGuard o(myUserId.ownerId());
219     if (o.isLocked())
220       myOwnerName = QString::fromUtf8(o->getAlias().c_str());
221   }
222 
223   // Mark all dates with activity so they are easier to find
224   Licq::HistoryList::iterator item;
225   for (item = myHistoryList.begin(); item != myHistoryList.end(); ++item)
226   {
227     QDate date = QDateTime::fromTime_t((*item)->Time()).date();
228     myCalendar->markDate(date);
229   }
230 
231   // Limit calendar to dates where we have history entries
232   myCalendar->setMinimumDate(QDateTime::fromTime_t((*myHistoryList.begin())->Time()).date());
233   QDate lastDate = QDateTime::fromTime_t((*(--myHistoryList.end()))->Time()).date();
234   myCalendar->setMaximumDate(lastDate);
235   myCalendar->setSelectedDate(lastDate);
236   calenderClicked();
237 
238   // Catch sent messages and add them to history
239   connect(gLicqGui, SIGNAL(eventSent(const Licq::Event*)),
240       SLOT(eventSent(const Licq::Event*)));
241 
242   // Catch received messages so we can add them to history
243   connect(gGuiSignalManager,
244       SIGNAL(updatedUser(const Licq::UserId&, unsigned long, int, unsigned long)),
245       SLOT(updatedUser(const Licq::UserId&, unsigned long, int)));
246 }
247 
~HistoryDlg()248 HistoryDlg::~HistoryDlg()
249 {
250   Licq::User::ClearHistory(myHistoryList);
251 }
252 
updatedUser(const Licq::UserId & userId,unsigned long subSignal,int argument)253 void HistoryDlg::updatedUser(const Licq::UserId& userId, unsigned long subSignal, int argument)
254 {
255   if (userId != myUserId)
256     return;
257 
258   if (subSignal == Licq::PluginSignal::UserEvents)
259   {
260     const Licq::UserEvent* event;
261     {
262       Licq::UserReadGuard u(myUserId);
263       if (!u.isLocked())
264         return;
265 
266       event = u->EventPeekId(argument);
267     }
268 
269     if (event != NULL && argument > 0 && argument > (*(--myHistoryList.end()))->Id())
270       addMsg(event);
271   }
272   else if (subSignal == Licq::PluginSignal::UserBasic)
273   {
274     Licq::UserReadGuard u(myUserId);
275     setTitle(*u);
276   }
277 }
278 
setTitle(const Licq::User * user)279 void HistoryDlg::setTitle(const Licq::User* user)
280 {
281   QString name;
282   if (user == NULL)
283   {
284     name = tr("INVALID USER");
285   }
286   else
287   {
288     name = QString::fromUtf8(user->getFullName().c_str());
289     if (!name.isEmpty())
290       name = " (" + name + ")";
291     name.prepend(QString::fromUtf8(user->getAlias().c_str()));
292   }
293 
294   setWindowTitle(tr("Licq - History ") + name);
295 }
296 
eventSent(const Licq::Event * event)297 void HistoryDlg::eventSent(const Licq::Event* event)
298 {
299   if (event->userId() == myUserId && event->userEvent() != NULL)
300     addMsg(event->userEvent());
301 }
302 
addMsg(const Licq::UserEvent * event)303 void HistoryDlg::addMsg(const Licq::UserEvent* event)
304 {
305   Licq::UserEvent* eventCopy = event->Copy();
306   myHistoryList.push_back(eventCopy);
307   QDate date = QDateTime::fromTime_t(event->Time()).date();
308   myCalendar->markDate(date);
309   myCalendar->setMaximumDate(date);
310 }
311 
getRegExp() const312 QRegExp HistoryDlg::getRegExp() const
313 {
314   // Since QRegExp has a FixedString mode we can use it for normal search also
315   return QRegExp(
316       myPatternEdit->text(),
317       (myMatchCaseCheck->isChecked() ? Qt::CaseSensitive : Qt::CaseInsensitive),
318       (myRegExpSearchCheck->isChecked() ? QRegExp::RegExp2 : QRegExp::FixedString));
319 }
320 
showHistory()321 void HistoryDlg::showHistory()
322 {
323   if (myHistoryList.empty())
324     return;
325 
326   myHistoryView->clear();
327   myHistoryView->setReverse(Config::Chat::instance()->reverseHistory());
328 
329   QDateTime date;
330 
331   // Go through all entries in the list
332   Licq::HistoryList::iterator item;
333   for (item = myHistoryList.begin(); item != myHistoryList.end(); ++item)
334   {
335     date.setTime_t((*item)->Time());
336 
337     // Skip those that aren't from the selected date
338     if (date.date() != myCalendar->selectedDate())
339       continue;
340 
341     QString messageText = QString::fromUtf8((*item)->text().c_str());
342     QString name = (*item)->isReceiver() ? myContactName : myOwnerName;
343 
344     QRegExp highlight;
345 
346     // Check if this is the entry we've searched for
347     if (item == mySearchPos)
348     {
349       highlight = getRegExp();
350       highlight.setMinimal(true);
351     }
352     messageText = HistoryView::toRichText(messageText, true, myUseHtml, highlight);
353 
354     // Add entry to history view
355     myHistoryView->addMsg((*item)->isReceiver(), false,
356         ((*item)->eventType() == Licq::UserEvent::TypeMessage ? "" : ((*item)->description() + " ").c_str()),
357         date,
358         (*item)->IsDirect(),
359         (*item)->IsMultiRec(),
360         (*item)->IsUrgent(),
361         (*item)->IsEncrypted(),
362         name,
363         messageText,
364         (item == mySearchPos ? "SearchHit" : QString()));
365   }
366 
367   // Tell history view to update in case it is buffered
368   myHistoryView->updateContent();
369 }
370 
calenderClicked()371 void HistoryDlg::calenderClicked()
372 {
373   // Clear search position
374   mySearchPos = myHistoryList.end();
375 
376   myStatusLabel->setText(QString());
377   showHistory();
378 }
379 
findNext()380 void HistoryDlg::findNext()
381 {
382   find(false);
383 }
384 
findPrevious()385 void HistoryDlg::findPrevious()
386 {
387   find(true);
388 }
389 
find(bool backwards)390 void HistoryDlg::find(bool backwards)
391 {
392   if (myPatternEdit->text().isEmpty())
393     return;
394 
395   QRegExp regExp(getRegExp());
396 
397   // An expression that can match zero characters is no better than an empty search text
398   if (regExp.indexIn("") != -1)
399     return;
400 
401   // If search pattern has changed, find all matching dates and mark them in the calendar
402   if (myPatternChanged)
403   {
404     myCalendar->clearMatches();
405 
406     Licq::HistoryList::iterator i;
407     for (i = myHistoryList.begin(); i != myHistoryList.end(); ++i)
408     {
409       QString messageText = QString::fromUtf8((*i)->text().c_str());
410       if (messageText.contains(regExp))
411       {
412         QDate date = QDateTime::fromTime_t((*i)->Time()).date();
413         myCalendar->addMatch(date);
414       }
415     }
416 
417     // No need to do this again next time
418     myPatternChanged = false;
419   }
420 
421   myStatusLabel->setText(QString());
422 
423   // If this is first search we need to find an entry to start searching from
424   if (mySearchPos == myHistoryList.end())
425   {
426     for (mySearchPos = myHistoryList.begin(); mySearchPos != myHistoryList.end(); ++mySearchPos)
427     {
428       QDate date = QDateTime::fromTime_t((*mySearchPos)->Time()).date();
429 
430       // When searching backwards, set start to first entry after current day
431       if (date > myCalendar->selectedDate())
432         break;
433 
434       // When searching forwards, set start to last entry before current day
435       if (!backwards && date >= myCalendar->selectedDate())
436         break;
437     }
438 
439     // Back one step to actually get entry before current day
440     if (!backwards)
441       --mySearchPos;
442   }
443 
444   // Remember where we started so we can stop after checking all entries once
445   Licq::HistoryList::iterator startPos = mySearchPos;
446 
447   while (true)
448   {
449     if (backwards)
450       --mySearchPos;
451     else
452       ++mySearchPos;
453 
454     // end is outside list so don't try to match it
455     if (mySearchPos != myHistoryList.end())
456     {
457       QString messageText = QString::fromUtf8((*mySearchPos)->text().c_str());
458       if (messageText.contains(regExp))
459         // We have a match
460         break;
461     }
462 
463     if (mySearchPos == startPos)
464     {
465       myStatusLabel->setText(tr("Search returned no matches"));
466       myPatternEdit->setStyleSheet("background: red");
467       return;
468     }
469 
470     if (mySearchPos == myHistoryList.end())
471     {
472       myStatusLabel->setText(tr("Search wrapped around"));
473 
474       // Iterator wraps around between begin and end so no extra handling needed
475       continue;
476     }
477   }
478 
479   QDate date = QDateTime::fromTime_t((*mySearchPos)->Time()).date();
480   myCalendar->setSelectedDate(date);
481   showHistory();
482   myHistoryView->scrollToAnchor("SearchHit");
483 }
484 
searchTextChanged(const QString & text)485 void HistoryDlg::searchTextChanged(const QString& text)
486 {
487   // Disable search buttons if there is no text in the search box
488   myFindNextButton->setEnabled(!text.isEmpty());
489   myFindPrevButton->setEnabled(!text.isEmpty());
490 
491   // Clear failed status from previous search
492   myPatternEdit->setStyleSheet("");
493 
494   // Mark that pattern has changed since previous search
495   myPatternChanged = true;
496 
497   // Search field is cleared so clear status message and matching dates
498   if (text.isEmpty())
499   {
500     myStatusLabel->setText(QString());
501     myCalendar->clearMatches();
502   }
503 }
504 
showUserMenu()505 void HistoryDlg::showUserMenu()
506 {
507   gUserMenu->setUser(myUserId);
508 }
509 
nextDate()510 void HistoryDlg::nextDate()
511 {
512   QDateTime date;
513   Licq::HistoryList::iterator item;
514 
515   // Find first entry in next date
516   for (item = myHistoryList.begin(); item != myHistoryList.end(); ++item)
517   {
518     date.setTime_t((*item)->Time());
519 
520     // Stop when we find an entry with date later then current
521     if (date.date() > myCalendar->selectedDate())
522       break;
523   }
524 
525   // No later date found so go to oldest entry
526   if (item == myHistoryList.end())
527     date.setTime_t((*myHistoryList.begin())->Time());
528 
529   myCalendar->setSelectedDate(date.date());
530   calenderClicked();
531 }
532 
previousDate()533 void HistoryDlg::previousDate()
534 {
535   QDateTime date;
536   Licq::HistoryList::iterator item;
537 
538   // Find first entry in next date
539   for (item = myHistoryList.begin(); item != myHistoryList.end(); ++item)
540   {
541     date.setTime_t((*item)->Time());
542 
543     // Stop when we find an entry with date later then current
544     if (date.date() >= myCalendar->selectedDate())
545       break;
546   }
547 
548   // Go back to last entry of previous day
549   --item;
550 
551   // No earlier date, go to last
552   if (item == myHistoryList.end())
553     --item;
554 
555   date.setTime_t((*item)->Time());
556 
557   myCalendar->setSelectedDate(date.date());
558   calenderClicked();
559 }
560