1 /* This file is part of Clementine.
2    Copyright 2018, Vikram Ambrose <ambroseworks@gmail.com>
3 
4    Clementine is free software: you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation, either version 3 of the License, or
7    (at your option) any later version.
8 
9    Clementine is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY; without even the implied warranty of
11    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12    GNU General Public License for more details.
13 
14    You should have received a copy of the GNU General Public License
15    along with Clementine.  If not, see <http://www.gnu.org/licenses/>.
16 */
17 
18 
19 #include "fancytabwidget.h"
20 #include "stylehelper.h"
21 #include "core/logging.h"
22 
23 #include <QDebug>
24 
25 #include <QMenu>
26 #include <QMouseEvent>
27 #include <QPainter>
28 #include <QTabBar>
29 #include <QStylePainter>
30 #include <QTimer>
31 #include <QVBoxLayout>
32 #include <QSettings>
33 
34 const QSize FancyTabWidget::IconSize_LargeSidebar = QSize(24,24);
35 const QSize FancyTabWidget::IconSize_SmallSidebar = QSize(22,22);
36 
37 const QSize FancyTabWidget::TabSize_LargeSidebar = QSize(70,47);
38 
39 class FancyTabBar: public QTabBar {
40 
41 private:
42     int mouseHoverTabIndex = -1;
43     QMap<QWidget*,QString> labelCache;
44 
45 public:
FancyTabBar(QWidget * parent=0)46     explicit FancyTabBar(QWidget* parent=0) : QTabBar(parent) {
47         setMouseTracking(true);
48     }
49 
sizeHint() const50     QSize sizeHint() const {
51         QSize size(QTabBar::sizeHint());
52 
53         FancyTabWidget *tabWidget = (FancyTabWidget*) parentWidget();
54         if(tabWidget->mode() == FancyTabWidget::Mode_Tabs ||
55            tabWidget->mode() == FancyTabWidget::Mode_IconOnlyTabs)
56             return size;
57 
58         QSize tabSize(tabSizeHint(0));
59         size.setWidth(tabSize.width());
60         int guessHeight = tabSize.height()*count();
61         if(guessHeight > size.height())
62             size.setHeight(guessHeight);
63         return size;
64     }
65 
width()66     int width() {
67         return tabSizeHint(0).width();
68     }
69 
70 protected:
tabSizeHint(int index) const71     QSize tabSizeHint(int index) const {
72         FancyTabWidget *tabWidget = (FancyTabWidget*) parentWidget();
73         QSize size = FancyTabWidget::TabSize_LargeSidebar;
74 
75         if(tabWidget->mode() != FancyTabWidget::Mode_LargeSidebar) {
76             size = QTabBar::tabSizeHint(index);
77         }
78 
79         return size;
80     }
81 
leaveEvent(QEvent * event)82     void leaveEvent(QEvent * event) {
83         mouseHoverTabIndex = -1;
84         update();
85     }
86 
mouseMoveEvent(QMouseEvent * event)87     void mouseMoveEvent(QMouseEvent * event) {
88         QPoint pos = event->pos();
89 
90         mouseHoverTabIndex = tabAt(pos);
91         if(mouseHoverTabIndex > -1)
92             update();
93         QTabBar::mouseMoveEvent(event);
94     }
95 
paintEvent(QPaintEvent * pe)96     void paintEvent(QPaintEvent *pe) {
97         FancyTabWidget *tabWidget = (FancyTabWidget*) parentWidget();
98 
99         bool verticalTextTabs = false;
100 
101         if(tabWidget->mode() == FancyTabWidget::Mode_SmallSidebar)
102             verticalTextTabs = true;
103 
104         // Restore any label text that was hidden/cached for the IconOnlyTabs mode
105         if(labelCache.count() > 0 && tabWidget->mode() != FancyTabWidget::Mode_IconOnlyTabs) {
106                 for(int i =0; i < count(); i++) {
107                     setTabText(i,labelCache[tabWidget->widget(i)]);
108                 }
109                 labelCache.clear();
110         }
111         if(tabWidget->mode() != FancyTabWidget::Mode_LargeSidebar &&
112            tabWidget->mode() != FancyTabWidget::Mode_SmallSidebar) {
113             // Cache and hide label text for IconOnlyTabs mode
114             if(tabWidget->mode() == FancyTabWidget::Mode_IconOnlyTabs && labelCache.count() == 0) {
115                 for(int i =0; i < count(); i++) {
116                     labelCache[tabWidget->widget(i)] = tabText(i);
117                     setTabText(i,"");
118                 }
119             }
120             QTabBar::paintEvent(pe);
121             return;
122         }
123 
124         QStylePainter p(this);
125 
126 
127         for (int index = 0; index < count(); index++) {
128             const bool selected = tabWidget->currentIndex() == index;;
129 
130             QRect tabrect = tabRect(index);
131 
132             QRect selectionRect = tabrect;
133 
134             if(selected) {
135                 // Selection highlight
136                 p.save();
137                 QLinearGradient grad(selectionRect.topLeft(), selectionRect.topRight());
138                 grad.setColorAt(0, QColor(255, 255, 255, 140));
139                 grad.setColorAt(1, QColor(255, 255, 255, 210));
140                 p.fillRect(selectionRect.adjusted(0,0,0,-1), grad);
141                 p.restore();
142 
143                 // shadow lines
144                 p.setPen(QColor(0, 0, 0, 110));
145                 p.drawLine(selectionRect.topLeft()    + QPoint(1, -1), selectionRect.topRight()    - QPoint(0, 1));
146                 p.drawLine(selectionRect.bottomLeft(), selectionRect.bottomRight());
147                 p.setPen(QColor(0, 0, 0, 40));
148                 p.drawLine(selectionRect.topLeft(),    selectionRect.bottomLeft());
149 
150                 // highlights
151                 p.setPen(QColor(255, 255, 255, 50));
152                 p.drawLine(selectionRect.topLeft()    + QPoint(0, -2), selectionRect.topRight()    - QPoint(0, 2));
153                 p.drawLine(selectionRect.bottomLeft() + QPoint(0, 1),  selectionRect.bottomRight() + QPoint(0, 1));
154                 p.setPen(QColor(255, 255, 255, 40));
155                 p.drawLine(selectionRect.topLeft()    + QPoint(0, 0),  selectionRect.topRight());
156                 p.drawLine(selectionRect.topRight()   + QPoint(0, 1),  selectionRect.bottomRight() - QPoint(0, 1));
157                 p.drawLine(selectionRect.bottomLeft() + QPoint(0, -1), selectionRect.bottomRight() - QPoint(0, 1));
158 
159             }
160 
161             // Mouse hover effect
162             if(!selected && index == mouseHoverTabIndex && isTabEnabled(index))
163             {
164                 p.save();
165                 QLinearGradient grad(selectionRect.topLeft(),  selectionRect.topRight());
166                 grad.setColorAt(0, Qt::transparent);
167                 grad.setColorAt(0.5, QColor(255, 255, 255, 40));
168                 grad.setColorAt(1, Qt::transparent);
169                 p.fillRect(selectionRect, grad);
170                 p.setPen(QPen(grad, 1.0));
171                 p.drawLine(selectionRect.topLeft(),     selectionRect.topRight());
172                 p.drawLine(selectionRect.bottomRight(), selectionRect.bottomLeft());
173                 p.restore();
174             }
175 
176             // Label (Icon and Text)
177             {
178                 p.save();
179                 QTransform m;
180                 int textFlags;
181                 Qt::Alignment iconFlags;
182 
183                 QRect tabrectText;
184                 QRect tabrectLabel;
185 
186                 if (verticalTextTabs) {
187                     m = QTransform::fromTranslate(tabrect.left(), tabrect.bottom());
188                     m.rotate(-90);
189                     textFlags = Qt::AlignLeft | Qt::AlignVCenter ;
190                     iconFlags = Qt::AlignLeft  | Qt::AlignVCenter;
191 
192                     tabrectLabel = QRect(QPoint(0, 0), m.mapRect(tabrect).size());
193 
194                     tabrectText = tabrectLabel;
195                     tabrectText.translate(30,0);
196                 } else {
197                     m = QTransform::fromTranslate(tabrect.left(), tabrect.top());
198                     textFlags = Qt::AlignHCenter | Qt::AlignBottom ;
199                     iconFlags = Qt::AlignHCenter | Qt::AlignTop;
200 
201                     tabrectLabel = QRect(QPoint(0, 0), m.mapRect(tabrect).size());
202 
203                     tabrectText = tabrectLabel;
204                     tabrectText.translate(0,-5);
205                 }
206 
207                 p.setTransform(m);
208 
209                 QFont boldFont(p.font());
210                 boldFont.setPointSizeF(Utils::StyleHelper::sidebarFontSize());
211                 boldFont.setBold(true);
212                 p.setFont(boldFont);
213 
214                 // Text drop shadow color
215                 p.setPen(selected ? QColor(255,255,255,160) : QColor(0,0,0,110) );
216                 p.translate(0, 3);
217                 p.drawText(tabrectText, textFlags, tabText(index));
218 
219                 // Text foreground color
220                 p.translate(0, -1);
221                 p.setPen(selected ? QColor(60, 60, 60) : Utils::StyleHelper::panelTextColor());
222                 p.drawText(tabrectText, textFlags, tabText(index));
223 
224 
225                 // Draw the icon
226                 QRect tabrectIcon;
227                 const int PADDING = 5;
228                 if(verticalTextTabs) {
229                     tabrectIcon = tabrectLabel;
230                     tabrectIcon.setSize(FancyTabWidget::IconSize_SmallSidebar);
231                     tabrectIcon.translate(PADDING,PADDING);
232                 } else {
233                     tabrectIcon = tabrectLabel;
234                     tabrectIcon.setSize(FancyTabWidget::IconSize_LargeSidebar);
235                     // Center the icon
236                     const int moveRight = (FancyTabWidget::TabSize_LargeSidebar.width() -
237                                           FancyTabWidget::IconSize_LargeSidebar.width() -1)/2;
238                     tabrectIcon.translate(moveRight,PADDING);
239                 }
240                 tabIcon(index).paint(&p, tabrectIcon, iconFlags);
241                 p.restore();
242            }
243         }
244     }
245 };
246 
247 // Spacers are just disabled pages
addSpacer()248 void FancyTabWidget::addSpacer() {
249     QWidget *spacer = new QWidget();
250     const int index = addTab(spacer,QIcon(),QString());
251     setTabEnabled(index,false);
252 }
253 
setBackgroundPixmap(const QPixmap & pixmap)254 void FancyTabWidget::setBackgroundPixmap(const QPixmap& pixmap) {
255     background_pixmap_ = pixmap;
256     update();
257 }
258 
setCurrentIndex(int index)259 void FancyTabWidget::setCurrentIndex(int index) {
260     QWidget* currentPage = widget(index);
261 
262     QLayout *layout = currentPage->layout();
263     if(bottom_widget_ != nullptr)
264         layout->addWidget(bottom_widget_);
265 
266     QTabWidget::setCurrentIndex(index);
267 }
268 
269 // Slot
currentTabChanged(int index)270 void FancyTabWidget::currentTabChanged(int index) {
271     QWidget* currentPage = currentWidget();
272 
273     QLayout *layout = currentPage->layout();
274     if(bottom_widget_ != nullptr)
275         layout->addWidget(bottom_widget_);
276     emit CurrentChanged(index);
277 }
278 
FancyTabWidget(QWidget * parent)279 FancyTabWidget::FancyTabWidget(QWidget* parent) : QTabWidget(parent),
280       menu_(nullptr),
281       mode_(Mode_None),
282       bottom_widget_(nullptr) {
283     FancyTabBar *tabBar = new FancyTabBar(this);
284 
285     setTabBar(tabBar);
286     setTabPosition(QTabWidget::West);
287     setMovable(true);
288 
289     connect(tabBar, SIGNAL(currentChanged(int)), this, SLOT(currentTabChanged(int)));
290 }
291 
loadSettings(const QSettings & settings)292 void FancyTabWidget::loadSettings(const QSettings& settings) {
293   for (int i = 0; i < count(); i++) {
294     int originalIndex = tabBar()->tabData(i).toInt();
295     QString k = "tab_index_" + QString::number(originalIndex);
296 
297     int newIndex = settings.value(k, i).toInt();
298 
299     if (newIndex >= 0)
300       tabBar()->moveTab(i, newIndex);
301     else
302       removeTab(i);  // Does not delete page
303   }
304 }
305 
saveSettings(QSettings * settings)306 void FancyTabWidget::saveSettings(QSettings* settings) {
307   for (int i = 0; i < count(); i++) {
308     int originalIndex = tabBar()->tabData(i).toInt();
309     QString k = "tab_index_" + QString::number(originalIndex);
310 
311     settings->setValue(k, i);
312   }
313 }
314 
315 
addBottomWidget(QWidget * widget)316 void FancyTabWidget::addBottomWidget(QWidget* widget) {
317     bottom_widget_ = widget;
318 }
319 
addTab(QWidget * page,const QIcon & icon,const QString & label)320 int FancyTabWidget::addTab(QWidget * page, const QIcon & icon, const QString & label) {
321     return insertTab(count(),page,icon,label);
322 }
323 
insertTab(int index,QWidget * page,const QIcon & icon,const QString & label)324 int FancyTabWidget::insertTab(int index, QWidget * page, const QIcon & icon, const QString & label) {
325     // In order to achieve the same effect as the "Bottom Widget" of the
326     // old Nokia based FancyTabWidget a VBoxLayout is used on each page
327     QVBoxLayout *layout = new QVBoxLayout();
328     layout->setSpacing(0);
329     layout->setContentsMargins(0,0,0,0);
330     layout->addWidget(page);
331 
332     QWidget *newPage = new QWidget();
333     newPage->setLayout(layout);
334 
335     const int actualIndex = QTabWidget::insertTab(index,newPage,icon,label);
336 
337     // Remember the original index. Needed to save order of tabs
338     tabBar()->setTabData(actualIndex,QVariant(actualIndex));
339     return actualIndex;
340 }
341 
paintEvent(QPaintEvent * pe)342 void FancyTabWidget::paintEvent(QPaintEvent *pe) {
343     if(mode() != FancyTabWidget::Mode_LargeSidebar &&
344        mode() != FancyTabWidget::Mode_SmallSidebar) {
345         QTabWidget::paintEvent(pe);
346         return;
347     }
348     QStylePainter p(this);
349 
350     // The brown color (Ubuntu) you see on the background gradient
351     QColor baseColor = StyleHelper::baseColor();
352 
353     QRect backgroundRect = rect();
354     backgroundRect.setWidth(((FancyTabBar*)tabBar())->width());
355     p.fillRect(backgroundRect,baseColor);
356 
357     // Horizontal gradient over the sidebar from transparent to dark
358     Utils::StyleHelper::verticalGradient(&p,backgroundRect,backgroundRect,false);
359 
360     // Draw the translucent png graphics over the gradient fill
361     {
362         if (!background_pixmap_.isNull()) {
363             QRect pixmap_rect(background_pixmap_.rect());
364             pixmap_rect.moveTo(backgroundRect.topLeft());
365 
366             while (pixmap_rect.top() < backgroundRect.bottom()) {
367                 QRect source_rect(pixmap_rect.intersected(backgroundRect));
368                 source_rect.moveTo(0, 0);
369                 p.drawPixmap(pixmap_rect.topLeft(), background_pixmap_,source_rect);
370                 pixmap_rect.moveTop(pixmap_rect.bottom() - 10);
371             }
372         }
373     }
374 
375     // Shadow effect of the background
376     {
377         QColor light(255, 255, 255, 80);
378         p.setPen(light);
379         p.drawLine(backgroundRect.topRight() - QPoint(1, 0),  backgroundRect.bottomRight() - QPoint(1, 0));
380         QColor dark(0, 0, 0, 90);
381         p.setPen(dark);
382         p.drawLine(backgroundRect.topLeft(), backgroundRect.bottomLeft());
383 
384         p.setPen(Utils::StyleHelper::borderColor());
385         p.drawLine(backgroundRect.topRight(), backgroundRect.bottomRight());
386     }
387 
388 }
389 
tabBarUpdateGeometry()390 void FancyTabWidget::tabBarUpdateGeometry() {
391     tabBar()->updateGeometry();
392 }
393 
SetMode(FancyTabWidget::Mode mode)394 void FancyTabWidget::SetMode(FancyTabWidget::Mode mode) {
395     mode_ = mode;
396 
397     if(mode == FancyTabWidget::Mode_Tabs ||
398        mode == FancyTabWidget::Mode_IconOnlyTabs) {
399         setTabPosition(QTabWidget::North);
400     } else {
401         setTabPosition(QTabWidget::West);
402     }
403 
404     tabBar()->updateGeometry();
405     updateGeometry();
406 
407     // There appears to be a bug in QTabBar which causes tabSizeHint
408     // to be ignored thus the need for this second shot repaint
409     QTimer::singleShot(1,this,SLOT(tabBarUpdateGeometry()));
410 
411     emit ModeChanged(mode);
412 }
413 
addMenuItem(QActionGroup * group,const QString & text,Mode mode)414 void FancyTabWidget::addMenuItem(QActionGroup* group, const QString& text,
415                                  Mode mode) {
416   QAction* action = group->addAction(text);
417   action->setCheckable(true);
418   connect(action, &QAction::triggered, [this, mode]() { SetMode(mode); });
419 
420   if (mode == mode_) action->setChecked(true);
421 }
422 
423 
contextMenuEvent(QContextMenuEvent * e)424 void FancyTabWidget::contextMenuEvent(QContextMenuEvent* e) {
425   if (!menu_) {
426     menu_ = new QMenu(this);
427 
428     QActionGroup* group = new QActionGroup(this);
429     addMenuItem(group, tr("Large sidebar"), Mode_LargeSidebar);
430     addMenuItem(group, tr("Small sidebar"), Mode_SmallSidebar);
431     addMenuItem(group, tr("Plain sidebar"), Mode_PlainSidebar);
432     addMenuItem(group, tr("Tabs on top"), Mode_Tabs);
433     addMenuItem(group, tr("Icons on top"), Mode_IconOnlyTabs);
434     menu_->addActions(group->actions());
435   }
436 
437   menu_->popup(e->globalPos());
438 }
439