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