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