1 /*
2 * This file is part of Licq, an instant messaging client for UNIX.
3 * Copyright (C) 1999-2014 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 "userview.h"
21
22 #include <QApplication>
23 #include <QDrag>
24 #include <QHeaderView>
25 #include <QMimeData>
26 #include <QMouseEvent>
27 #include <QTimer>
28
29 #include <licq/userid.h>
30
31 #include "config/contactlist.h"
32 #include "config/iconmanager.h"
33 #include "config/skin.h"
34
35 #include "contactlist/maincontactlistproxy.h"
36 #include "contactlist/mode2contactlistproxy.h"
37
38 using Licq::UserId;
39 using namespace LicqQtGui;
40
UserView(ContactListModel * contactList,QWidget * parent)41 UserView::UserView(ContactListModel* contactList, QWidget* parent)
42 : UserViewBase(contactList, true, parent)
43 {
44 myRemovedUserTimer = new QTimer(this);
45 myRemovedUserTimer->setSingleShot(true);
46 connect(myRemovedUserTimer, SIGNAL(timeout()), SLOT(forgetRemovedUser()));
47
48 // Use a proxy model for sorting and filtering
49 myListProxy = new MainContactListProxy(myContactList, this);
50 setModel(myListProxy);
51
52 // This is the main view
53 myIsMainView = true;
54
55 // Sorting
56 #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
57 header()->setSectionsClickable(true);
58 header()->setSectionsMovable(false);
59 #else
60 header()->setClickable(true);
61 header()->setMovable(false);
62 #endif
63 resort();
64 connect(header(), SIGNAL(sectionClicked(int)), SLOT(slotHeaderClicked(int)));
65
66 // Appearance
67 // Base class constructor doesn't know we overridden applySkin() so it we must call it here again
68 applySkin();
69
70 updateRootIndex();
71
72 connect(this, SIGNAL(expanded(const QModelIndex&)), SLOT(slotExpanded(const QModelIndex&)));
73 connect(this, SIGNAL(collapsed(const QModelIndex&)), SLOT(slotCollapsed(const QModelIndex&)));
74 connect(IconManager::instance(), SIGNAL(iconsChanged()), SLOT(configUpdated()));
75 connect(Config::ContactList::instance(), SIGNAL(listLookChanged()), SLOT(configUpdated()));
76 connect(Config::ContactList::instance(), SIGNAL(currentListChanged()), SLOT(updateRootIndex()));
77 connect(Config::ContactList::instance(), SIGNAL(listSortingChanged()), SLOT(resort()));
78 connect(myListProxy, SIGNAL(modelReset()), SLOT(updateRootIndex()));
79 }
80
~UserView()81 UserView::~UserView()
82 {
83 // Empty
84 }
85
currentUserId() const86 UserId UserView::currentUserId() const
87 {
88 if (!currentIndex().isValid())
89 return UserId();
90
91 if (static_cast<ContactListModel::ItemType>
92 (currentIndex().data(ContactListModel::ItemTypeRole).toInt()) != ContactListModel::UserItem)
93 return UserId();
94
95 return currentIndex().data(ContactListModel::UserIdRole).value<UserId>();
96 }
97
updateRootIndex()98 void UserView::updateRootIndex()
99 {
100 bool mode2View = Config::ContactList::instance()->mode2View();
101 int groupId = Config::ContactList::instance()->groupId();
102
103 QModelIndex newRoot = QModelIndex();
104
105 if (groupId == ContactListModel::AllGroupsGroupId)
106 {
107 // Hide the system groups that exist in the model but should not be displayed in threaded view
108 dynamic_cast<MainContactListProxy*>(myListProxy)->setThreadedView(true, mode2View);
109 }
110 else
111 {
112 newRoot = myContactList->groupIndex(groupId);
113 if (newRoot.isValid())
114 {
115 // Turn off group filtering first, otherwise we cannot switch from threaded view to a system group
116 dynamic_cast<MainContactListProxy*>(myListProxy)->setThreadedView(false, false);
117
118 // Hidden groups may not be sorted, force a resort just in case
119 resort();
120 }
121 }
122
123 UserViewBase::setRootIndex(myListProxy->mapFromSource(newRoot));
124 expandGroups();
125 configUpdated();
126 }
127
configUpdated()128 void UserView::configUpdated()
129 {
130 // Set column widths
131 for (int i = 0; i < Config::ContactList::instance()->columnCount(); i++)
132 setColumnWidth(i, Config::ContactList::instance()->columnWidth(i));
133
134 setVerticalScrollBarPolicy(Config::ContactList::instance()->allowScrollBar() ?
135 Qt::ScrollBarAsNeeded : Qt::ScrollBarAlwaysOff);
136
137 if (Config::ContactList::instance()->showHeader())
138 header()->show();
139 else
140 header()->hide();
141
142 spanRowRange(rootIndex(), 0, model()->rowCount(rootIndex()) - 1);
143 }
144
expandGroups()145 void UserView::expandGroups()
146 {
147 // No point in expanding groups unless we can actually see them
148 if (rootIndex().isValid())
149 return;
150
151 for (int i = 0; i < myListProxy->rowCount(QModelIndex()); ++i)
152 {
153 QModelIndex index = myListProxy->index(i, 0, QModelIndex());
154 if (static_cast<ContactListModel::ItemType>(index.data(ContactListModel::ItemTypeRole).toInt()) != ContactListModel::GroupItem)
155 continue;
156
157 int gid = index.data(ContactListModel::GroupIdRole).toInt();
158 bool online = (index.data(ContactListModel::SortPrefixRole).toInt() < 2);
159 setExpanded(index, Config::ContactList::instance()->groupState(gid, online));
160 }
161 }
162
spanRowRange(const QModelIndex & parent,int start,int end)163 void UserView::spanRowRange(const QModelIndex& parent, int start, int end)
164 {
165 for (int i = start; i <= end; i++)
166 {
167 // get the real model index
168 QModelIndex index = model()->index(i, 0, parent);
169 unsigned itemType = model()->data(index, ContactListModel::ItemTypeRole).toUInt();
170
171 if (itemType == ContactListModel::GroupItem ||
172 itemType == ContactListModel::BarItem)
173 setFirstColumnSpanned(i, parent, true);
174 }
175 }
176
setColors(QColor back)177 void UserView::setColors(QColor back)
178 {
179 UserViewBase::setColors(back);
180
181 if (!Config::ContactList::instance()->useSystemBackground() &&
182 Config::Skin::active()->frame.transparent)
183 {
184 QPalette pal = palette();
185 pal.setBrush(QPalette::Base, Qt::NoBrush);
186 setPalette(pal);
187 }
188 }
189
applySkin()190 void UserView::applySkin()
191 {
192 setFrameStyle(Config::Skin::active()->frame.frameStyle);
193 UserViewBase::applySkin();
194 }
195
rowsAboutToBeRemoved(const QModelIndex & parent,int start,int end)196 void UserView::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end)
197 {
198 if (currentIndex().isValid() && !myRemovedUser.isValid())
199 {
200 // Check all the removed rows and see if anyone of them is the currently select user
201 for (int i = start; i <= end; ++i)
202 {
203 if (model()->index(i, 0, parent) != currentIndex())
204 continue;
205
206 ContactListModel::ItemType itemType = static_cast<ContactListModel::ItemType>
207 (currentIndex().data(ContactListModel::ItemTypeRole).toInt());
208 if (itemType == ContactListModel::UserItem)
209 {
210 // Currently select user is being removed, remember it so we can select it again if it reappears
211 myRemovedUser = currentIndex().data(ContactListModel::UserIdRole).value<UserId>();
212
213 // ...but if event loop resumes first, it wasn't just a move between groups so forget it happened
214 myRemovedUserTimer->start();
215 }
216 }
217 }
218
219 UserViewBase::rowsAboutToBeRemoved(parent, start, end);
220 }
221
rowsInserted(const QModelIndex & parent,int start,int end)222 void UserView::rowsInserted(const QModelIndex& parent, int start, int end)
223 {
224 UserViewBase::rowsInserted(parent, start, end);
225 spanRowRange(parent, start, end);
226
227 // If we just got a new group we may want to expand it
228 if (!parent.isValid())
229 expandGroups();
230
231 if (myRemovedUser.isValid() && (!parent.isValid() || isExpanded(parent)))
232 {
233 // We have a user remembered that was just removed, check if he returned
234 for (int i = start; i <= end; ++i)
235 {
236 QModelIndex index = model()->index(i, 0, parent);
237 ContactListModel::ItemType itemType = static_cast<ContactListModel::ItemType>
238 (index.data(ContactListModel::ItemTypeRole).toInt());
239
240 if (itemType == ContactListModel::UserItem &&
241 index.data(ContactListModel::UserIdRole).value<UserId>() == myRemovedUser)
242 // User has returned, restore selection
243 setCurrentIndex(index);
244
245 if (itemType == ContactListModel::GroupItem && isExpanded(index))
246 {
247 // Inserted row was a group, then it might have users as sub items, check them too
248 int rows = model()->rowCount(index);
249 for (int j = 0; j < rows; ++j)
250 {
251 QModelIndex subindex = model()->index(j, 0, index);
252 ContactListModel::ItemType subitemType = static_cast<ContactListModel::ItemType>
253 (subindex.data(ContactListModel::ItemTypeRole).toInt());
254
255 if (subitemType == ContactListModel::UserItem &&
256 subindex.data(ContactListModel::UserIdRole).value<UserId>() == myRemovedUser)
257 // The appeared group has the user as a sub item, restore selection
258 setCurrentIndex(subindex);
259 }
260 }
261 }
262 }
263 }
264
forgetRemovedUser()265 void UserView::forgetRemovedUser()
266 {
267 myRemovedUser = UserId();
268 }
269
reset()270 void UserView::reset()
271 {
272 UserViewBase::reset();
273 // QTreeView::reset will collapse all groups so we have to reexpand them here
274 expandGroups();
275 }
276
mousePressEvent(QMouseEvent * event)277 void UserView::mousePressEvent(QMouseEvent* event)
278 {
279 UserViewBase::mousePressEvent(event);
280
281 if (event->button() == Qt::LeftButton)
282 {
283 QModelIndex clickedItem = indexAt(event->pos());
284 if (clickedItem.isValid())
285 {
286 ContactListModel::ItemType itemType = static_cast<ContactListModel::ItemType>
287 (currentIndex().data(ContactListModel::ItemTypeRole).toInt());
288 if (itemType == ContactListModel::GroupItem)
289 {
290 if (event->pos().x() <= 18) // we clicked an icon area
291 {
292 bool wasExpanded = isExpanded(clickedItem);
293 setExpanded(clickedItem, !wasExpanded);
294
295 // setExpand may fail, for example after changing sorting an
296 // expanded group can be collapsed but sometimes cannot be expanded
297 // again. This was seen with Qt 4.4.0.
298 if (isExpanded(clickedItem) == wasExpanded)
299 {
300 // Setting expanded state to same state as view currently (falsely)
301 // reports seems to fix it, then set it again to the state we
302 // actually wanted, this times it works.
303 setExpanded(clickedItem, wasExpanded);
304 setExpanded(clickedItem, !wasExpanded);
305 }
306 }
307 }
308 }
309 else
310 {
311 // Clicking outiside list will clear selection
312 selectionModel()->clearSelection();
313 setCurrentIndex(QModelIndex());
314 }
315 }
316 }
317
keyPressEvent(QKeyEvent * event)318 void UserView::keyPressEvent(QKeyEvent* event)
319 {
320 if (event->modifiers() & (Qt::ControlModifier | Qt::AltModifier))
321 {
322 event->ignore();
323 UserViewBase::keyPressEvent(event);
324 return;
325 }
326
327 ContactListModel::ItemType itemType = static_cast<ContactListModel::ItemType>
328 (currentIndex().data(ContactListModel::ItemTypeRole).toInt());
329
330 switch (event->key())
331 {
332 case Qt::Key_Return:
333 case Qt::Key_Enter:
334 if (itemType == ContactListModel::UserItem)
335 {
336 emit doubleClicked(currentIndex());
337 break;
338 }
339 // Fall through so return key expands and collapses groups
340
341 case Qt::Key_Space:
342 if (itemType == ContactListModel::GroupItem)
343 {
344 setExpanded(currentIndex(), !isExpanded(currentIndex()));
345 }
346 else
347 {
348 popupMenu(viewport()->mapToGlobal(QPoint(40, visualRect(currentIndex()).y())), currentIndex());
349 }
350 return;
351
352 default:
353 UserViewBase::keyPressEvent(event);
354 }
355 }
356
mouseMoveEvent(QMouseEvent * event)357 void UserView::mouseMoveEvent(QMouseEvent* event)
358 {
359 UserViewBase::mouseMoveEvent(event);
360
361 QModelIndex index = currentIndex();
362 if (index.isValid() == false)
363 return;
364
365 if (static_cast<ContactListModel::ItemType>
366 (index.data(ContactListModel::ItemTypeRole).toInt()) != ContactListModel::UserItem)
367 return;
368
369 QString id = index.data(ContactListModel::AccountIdRole).toString();
370 unsigned long ppid = index.data(ContactListModel::PpidRole).toUInt();
371
372 if ((event->buttons() & Qt::LeftButton) && !myMousePressPos.isNull() &&
373 (QPoint(event->pos() - myMousePressPos).manhattanLength() >= QApplication::startDragDistance()))
374 {
375 QString data(Licq::protocolId_toString(ppid).c_str());
376 data += id;
377
378 QDrag* drag = new QDrag(this);
379 QMimeData* mimeData = new QMimeData;
380 mimeData->setText(data);
381 drag->setMimeData(mimeData);
382 drag->start(Qt::CopyAction);
383 }
384 }
385
resort()386 void UserView::resort()
387 {
388 int column = Config::ContactList::instance()->sortColumn();
389 Qt::SortOrder order = (Config::ContactList::instance()->sortColumnAscending() ? Qt::AscendingOrder : Qt::DescendingOrder);
390
391 // Column 0 means sort on status or unsorted
392 if (column == 0)
393 {
394 dynamic_cast<SortedContactListProxy*>(myListProxy)->sort(0, ContactListModel::SortRole, Qt::AscendingOrder);
395
396 header()->setSortIndicatorShown(false);
397 }
398 else
399 {
400 // Column numbers in configuration is off by one
401 column--;
402
403 dynamic_cast<SortedContactListProxy*>(myListProxy)->sort(column, Qt::DisplayRole, order);
404
405 header()->setSortIndicatorShown(true);
406 header()->setSortIndicator(column, order);
407 }
408
409 // Group expansion gets confused when sorting is changed so refresh it
410 expandGroups();
411 }
412
slotExpanded(const QModelIndex & index)413 void UserView::slotExpanded(const QModelIndex& index)
414 {
415 int gid = index.data(ContactListModel::GroupIdRole).toInt();
416 bool online = (index.data(ContactListModel::SortPrefixRole).toInt() < 2);
417 Config::ContactList::instance()->setGroupState(gid, online, true);
418 }
419
slotCollapsed(const QModelIndex & index)420 void UserView::slotCollapsed(const QModelIndex& index)
421 {
422 int gid = index.data(ContactListModel::GroupIdRole).toInt();
423 bool online = (index.data(ContactListModel::SortPrefixRole).toInt() < 2);
424 Config::ContactList::instance()->setGroupState(gid, online, false);
425 }
426
slotHeaderClicked(int column)427 void UserView::slotHeaderClicked(int column)
428 {
429 // Clicking on a header will switch between three sorting modes
430 // - Ascending sort on the clicked column
431 // - Descending sort on the clicked colmun
432 // - Default sort (unsorted or sort by status)
433
434 // Columns in configuration is off by one as status was previously a separate column
435 column++;
436
437 if (Config::ContactList::instance()->sortColumn() == 0)
438 {
439 // Sort mode was default, change to ascending of this column
440 Config::ContactList::instance()->setSortColumn(column, true);
441 }
442 else if (Config::ContactList::instance()->sortColumn() != column)
443 {
444 // Sorting was of other column, change to ascending of this column
445 Config::ContactList::instance()->setSortColumn(column, true);
446 }
447 else if (Config::ContactList::instance()->sortColumnAscending() == true)
448 {
449 // Sorting was ascending of current column, change to descending
450 Config::ContactList::instance()->setSortColumn(column, false);
451 }
452 else
453 {
454 // Sorting was descending of current column, change to default
455 Config::ContactList::instance()->setSortColumn(0, true);
456 }
457 }
458