1 /*
2  * Copyright 2014  Cristian Oneț <onet.cristian@gmail.com>
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU General Public License as
6  * published by the Free Software Foundation; either version 2 of
7  * the License or (at your option) version 3 or any later version
8  * accepted by the membership of KDE e.V. (or its successor approved
9  * by the membership of KDE e.V.), which shall act as a proxy
10  * defined in Section 14 of version 3 of the license.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
19  *
20  */
21 
22 #include "fixedcolumntreeview.h"
23 
24 // ----------------------------------------------------------------------------
25 // QT Includes
26 
27 #include <QEvent>
28 
29 #include <QScrollBar>
30 #include <QStyledItemDelegate>
31 #include <QHeaderView>
32 #include <QApplication>
33 #include <QMouseEvent>
34 
35 // ----------------------------------------------------------------------------
36 // KDE Includes
37 
38 // ----------------------------------------------------------------------------
39 // Project Includes
40 
41 class FixedColumnDelegate : public QStyledItemDelegate
42 {
43   QTreeView *m_sourceView;
44 
45 public:
FixedColumnDelegate(FixedColumnTreeView * fixedColumView,QTreeView * sourceView)46   explicit FixedColumnDelegate(FixedColumnTreeView *fixedColumView, QTreeView *sourceView) :
47       QStyledItemDelegate(fixedColumView),
48       m_sourceView(sourceView) {
49   }
50 
paint(QPainter * painter,const QStyleOptionViewItem & option,const QModelIndex & index) const51   virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const final override {
52     QStyleOptionViewItem optV4 = option;
53     initStyleOption(&optV4, index);
54     // the fixed column's position has always this value
55     optV4.viewItemPosition = QStyleOptionViewItem::Beginning;
56     if (m_sourceView->hasFocus()) {
57       // draw the current row as active if the source list has focus
58       QModelIndex currentIndex = m_sourceView->currentIndex();
59       if (currentIndex.isValid() && currentIndex.row() == index.row() && currentIndex.parent() == index.parent()) {
60         optV4.state |= QStyle::State_Active;
61       }
62     }
63     QStyledItemDelegate::paint(painter, optV4, index);
64   }
65 };
66 
67 struct FixedColumnTreeView::Private {
PrivateFixedColumnTreeView::Private68   Private(FixedColumnTreeView *pub, QTreeView *parent) :
69       m_pub(pub),
70       m_parent(parent) {
71   }
72 
syncExpandedFixedColumnTreeView::Private73   void syncExpanded(const QModelIndex& parentIndex = QModelIndex()) {
74     const int rows = m_parent->model()->rowCount(parentIndex);
75     for (auto i = 0; i < rows; ++i) {
76       const QModelIndex &index = m_parent->model()->index(i, 0, parentIndex);
77       if (m_parent->isExpanded(index)) {
78         m_pub->expand(index);
79         syncExpanded(index);
80       }
81     }
82   }
83 
syncModelsFixedColumnTreeView::Private84   void syncModels() {
85     if (m_pub->model() != m_parent->model()) {
86       // set the model
87       m_pub->setModel(m_parent->model());
88 
89       // hide all but the first column
90       for (int col = 1; col < m_pub->model()->columnCount(); ++col)
91         m_pub->setColumnHidden(col, true);
92 
93       // set the selection model
94       m_pub->setSelectionModel(m_parent->selectionModel());
95 
96       // when the model has changed we need to sync the expanded state of the views
97       syncExpanded();
98     }
99   }
100 
syncPropertiesFixedColumnTreeView::Private101   void syncProperties() {
102     //pub->setAllColumnsShowFocus(parent->allColumnsShowFocus());
103     m_pub->setAlternatingRowColors(m_parent->alternatingRowColors());
104     m_pub->setIconSize(m_parent->iconSize());
105     m_pub->setSortingEnabled(m_parent->isSortingEnabled());
106     m_pub->setUniformRowHeights(m_pub->uniformRowHeights());
107   }
108 
syncGeometryFixedColumnTreeView::Private109   void syncGeometry() {
110     // update the geometry of the fixed column view to match that of the source model's geometry
111     m_pub->setGeometry(m_parent->frameWidth(), m_parent->frameWidth(), m_parent->columnWidth(0),
112                      m_parent->viewport()->height() + (m_parent->header()->isVisible() ? m_parent->header()->height() : 0));
113   }
114 
115   FixedColumnTreeView *m_pub;
116   QTreeView *m_parent;
117 };
118 
FixedColumnTreeView(QTreeView * parent)119 FixedColumnTreeView::FixedColumnTreeView(QTreeView *parent)
120     : QTreeView(parent)
121     , d(new Private(this, parent))
122 {
123   // no borders and scrollbars for the fixed column view
124   setStyleSheet("QTreeView { border: none; }");
125   setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
126   setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
127   // the focus proxy is forwarded to the source list
128   setFocusProxy(parent);
129 
130   // perform the special selection and hover drawing even when the fixed column view has no focus
131   setItemDelegate(new FixedColumnDelegate(this, d->m_parent));
132 
133   // stack the source view under the fixed column view
134   d->m_parent->viewport()->stackUnder(this);
135 
136   // the resize mode of the fixed view needs to be fixed to allow a user resize only from the parent tree
137   header()->sectionResizeMode(QHeaderView::Fixed);
138 
139   // connect the scroll bars to keep the two views in sync
140   connect(verticalScrollBar(), &QAbstractSlider::valueChanged, d->m_parent->verticalScrollBar(), &QAbstractSlider::setValue);
141   connect(d->m_parent->verticalScrollBar(), &QAbstractSlider::valueChanged, verticalScrollBar(), &QAbstractSlider::setValue);
142 
143   // keep the expanded/collapsed states synchronized between the two views
144   connect(d->m_parent, &QTreeView::expanded, this, &FixedColumnTreeView::onExpanded);
145   connect(this, &QTreeView::expanded, this, &FixedColumnTreeView::onExpanded);
146   connect(d->m_parent, &QTreeView::collapsed, this, &FixedColumnTreeView::onCollapsed);
147   connect(this, &QTreeView::collapsed, this, &FixedColumnTreeView::onCollapsed);
148 
149   // keep the sort indicators synchronized between the two views
150   connect(d->m_parent->header(), &QHeaderView::sortIndicatorChanged, this, &FixedColumnTreeView::updateSortIndicator);
151   connect(header(), &QHeaderView::sortIndicatorChanged, this, &FixedColumnTreeView::updateSortIndicator);
152 
153   // forward all signals
154   connect(this, &QAbstractItemView::activated, d->m_parent, &QAbstractItemView::activated);
155   connect(this, &QAbstractItemView::clicked, d->m_parent, &QAbstractItemView::clicked);
156   connect(this, &QAbstractItemView::doubleClicked, d->m_parent, &QAbstractItemView::doubleClicked);
157   connect(this, &QAbstractItemView::entered, d->m_parent, &QAbstractItemView::entered);
158   connect(this, &QAbstractItemView::pressed, d->m_parent, &QAbstractItemView::pressed);
159   connect(this, &QAbstractItemView::viewportEntered, d->m_parent, &QAbstractItemView::viewportEntered);
160 
161   // handle the resize of the first column in the source view
162   connect(d->m_parent->header(), &QHeaderView::sectionResized, this, &FixedColumnTreeView::updateSectionWidth);
163 
164   // forward right click to the source list
165   setContextMenuPolicy(d->m_parent->contextMenuPolicy());
166   if (contextMenuPolicy() == Qt::CustomContextMenu) {
167     connect(this, &QWidget::customContextMenuRequested, d->m_parent, &QWidget::customContextMenuRequested);
168   }
169 
170   // enable hover indicator synchronization between the two views
171   d->m_parent->viewport()->installEventFilter(this);
172   d->m_parent->viewport()->setMouseTracking(true);
173   viewport()->setMouseTracking(true);
174 
175   d->syncProperties();
176 
177   if (d->m_parent->isVisible()) {
178     // the source view is already visible so show the frozen column also
179     d->syncModels();
180     show();
181     d->syncGeometry();
182   }
183 }
184 
~FixedColumnTreeView()185 FixedColumnTreeView::~FixedColumnTreeView()
186 {
187   delete d;
188 }
189 
sourceModelUpdated()190 void FixedColumnTreeView::sourceModelUpdated()
191 {
192   d->syncModels();
193   d->syncGeometry();
194 }
195 
onExpanded(const QModelIndex & index)196 void FixedColumnTreeView::onExpanded(const QModelIndex& index)
197 {
198   if (sender() == this && !d->m_parent->isExpanded(index)) {
199     d->m_parent->expand(index);
200   }
201 
202   if (sender() == d->m_parent && !isExpanded(index)) {
203     expand(index);
204   }
205 }
206 
onCollapsed(const QModelIndex & index)207 void FixedColumnTreeView::onCollapsed(const QModelIndex& index)
208 {
209   if (sender() == this && d->m_parent->isExpanded(index)) {
210     d->m_parent->collapse(index);
211   }
212 
213   if (sender() == d->m_parent && isExpanded(index)) {
214     collapse(index);
215   }
216 }
217 
viewportEvent(QEvent * event)218 bool FixedColumnTreeView::viewportEvent(QEvent *event)
219 {
220   if (underMouse()) {
221     // forward mouse move and hover leave events to the source list
222     if (event->type() == QEvent::MouseMove || event->type() == QEvent::HoverLeave) {
223       QApplication::sendEvent(d->m_parent->viewport(), event);
224     }
225   }
226   return QTreeView::viewportEvent(event);
227 }
228 
eventFilter(QObject * object,QEvent * event)229 bool FixedColumnTreeView::eventFilter(QObject *object, QEvent *event)
230 {
231   if (object == d->m_parent->viewport()) {
232     switch (event->type()) {
233       case QEvent::MouseMove:
234         if (!underMouse() && d->m_parent->underMouse()) {
235           QMouseEvent *me = static_cast<QMouseEvent*>(event);
236           // translate the position of the event but don't send buttons or modifiers because we only need the movement for the hover
237           QMouseEvent translatedMouseEvent(me->type(), QPoint(width() - 2, me->pos().y()), Qt::NoButton, Qt::MouseButtons(), Qt::KeyboardModifiers());
238           QApplication::sendEvent(viewport(), &translatedMouseEvent);
239         }
240         break;
241       case QEvent::HoverLeave:
242         if (!underMouse() && d->m_parent->underMouse()) {
243           QApplication::sendEvent(viewport(), event);
244         }
245         break;
246       case QEvent::Show:
247         d->syncModels();
248         show();
249         // intentional fall through
250       case QEvent::Resize:
251         d->syncGeometry();
252         break;
253       default:
254         break;
255     }
256   }
257   return QTreeView::eventFilter(object, event);
258 }
259 
updateSectionWidth(int logicalIndex,int,int newSize)260 void FixedColumnTreeView::updateSectionWidth(int logicalIndex, int, int newSize)
261 {
262   if (logicalIndex == 0) {
263     const int maxFirstColumnWidth = d->m_parent->width() * 0.8;
264     if (newSize > maxFirstColumnWidth) {
265       // limit the size of the first column so that it will not become larger than the view's width
266       d->m_parent->setColumnWidth(logicalIndex, maxFirstColumnWidth);
267     } else {
268       // update the size of the fixed column
269       setColumnWidth(0, newSize);
270       // update the geometry
271       d->syncGeometry();
272     }
273   }
274 }
275 
updateSortIndicator(int logicalIndex,Qt::SortOrder order)276 void FixedColumnTreeView::updateSortIndicator(int logicalIndex, Qt::SortOrder order)
277 {
278   if (sender() == header() && d->m_parent->header()->sortIndicatorSection() != logicalIndex) {
279     d->m_parent->header()->setSortIndicator(logicalIndex, order);
280   }
281 
282   if (sender() == d->m_parent->header() && header()->sortIndicatorSection() != logicalIndex) {
283     header()->setSortIndicator(logicalIndex, order);
284   }
285 }
286