1 /**************************************************************************
2 * Otter Browser: Web browser controlled by the user, not vice-versa.
3 * Copyright (C) 2015 - 2018 Michal Dutkiewicz aka Emdek <michal@emdek.pl>
4 *
5 * This program 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 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 **************************************************************************/
19 
20 #include "TabSwitcherWidget.h"
21 #include "Animation.h"
22 #include "MainWindow.h"
23 #include "Window.h"
24 #include "../core/Application.h"
25 #include "../core/SessionModel.h"
26 #include "../core/ThemesManager.h"
27 
28 #include <QtGui/QKeyEvent>
29 #include <QtWidgets/QFrame>
30 #include <QtWidgets/QHBoxLayout>
31 #include <QtWidgets/QHeaderView>
32 
33 namespace Otter
34 {
35 
TabSwitcherWidget(MainWindow * parent)36 TabSwitcherWidget::TabSwitcherWidget(MainWindow *parent) : QWidget(parent),
37 	m_mainWindow(parent),
38 	m_model(new QStandardItemModel(this)),
39 	m_tabsView(new ItemViewWidget(this)),
40 	m_previewLabel(new QLabel(this)),
41 	m_spinnerAnimation(nullptr),
42 	m_reason(KeyboardReason),
43 	m_isIgnoringMinimizedTabs(SettingsManager::getOption(SettingsManager::TabSwitcher_IgnoreMinimizedTabsOption).toBool())
44 {
45 	QFrame *frame(new QFrame(this));
46 	QHBoxLayout *mainLayout(new QHBoxLayout(this));
47 	mainLayout->addWidget(frame, 0, Qt::AlignCenter);
48 
49 	setLayout(mainLayout);
50 	setAutoFillBackground(false);
51 
52 	QHBoxLayout *frameLayout(new QHBoxLayout(frame));
53 	frameLayout->addWidget(m_tabsView, 1);
54 	frameLayout->addWidget(m_previewLabel, 0, Qt::AlignCenter);
55 
56 	m_model->setSortRole(OrderRole);
57 
58 	frame->setLayout(frameLayout);
59 	frame->setAutoFillBackground(true);
60 	frame->setMinimumWidth(600);
61 	frame->setObjectName(QLatin1String("tabSwitcher"));
62 	frame->setStyleSheet(QStringLiteral("#tabSwitcher {background:%1;border:1px solid #B3B3B3;border-radius:4px;}").arg(palette().color(QPalette::Base).name()));
63 
64 	m_tabsView->setModel(m_model);
65 	m_tabsView->setStyleSheet(QLatin1String("border:0;"));
66 	m_tabsView->header()->hide();
67 	m_tabsView->header()->setStretchLastSection(true);
68 	m_tabsView->viewport()->installEventFilter(this);
69 
70 	m_previewLabel->setFixedSize(260, 170);
71 	m_previewLabel->setAlignment(Qt::AlignCenter);
72 	m_previewLabel->setStyleSheet(QLatin1String("border:1px solid gray;"));
73 
74 	connect(m_tabsView, &ItemViewWidget::clicked, this, &TabSwitcherWidget::handleIndexClicked);
75 	connect(m_tabsView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TabSwitcherWidget::handleCurrentTabChanged);
76 }
77 
showEvent(QShowEvent * event)78 void TabSwitcherWidget::showEvent(QShowEvent *event)
79 {
80 	setFocus();
81 
82 	const MainWindowSessionItem *mainWindowItem(SessionsManager::getModel()->getMainWindowItem(m_mainWindow));
83 
84 	if (mainWindowItem)
85 	{
86 		const bool useSorting(SettingsManager::getOption(SettingsManager::TabSwitcher_OrderByLastActivityOption).toBool());
87 
88 		for (int i = 0; i < mainWindowItem->rowCount(); ++i)
89 		{
90 			const WindowSessionItem *windowItem(static_cast<WindowSessionItem*>(mainWindowItem->child(i, 0)));
91 
92 			if (windowItem && (!m_isIgnoringMinimizedTabs || (windowItem->getActiveWindow() && windowItem->getActiveWindow()->getWindowState().state != Qt::WindowMinimized)))
93 			{
94 				m_model->appendRow(createRow(windowItem->getActiveWindow(), (useSorting ? QVariant(windowItem->getActiveWindow()->getLastActivity()) : QVariant(i))));
95 			}
96 		}
97 	}
98 
99 	m_model->sort(0, (SettingsManager::getOption(SettingsManager::TabSwitcher_OrderByLastActivityOption).toBool() ? Qt::DescendingOrder : Qt::AscendingOrder));
100 
101 	const Window *activeWindow(m_mainWindow->getActiveWindow());
102 	const int contentsHeight(m_model->rowCount() * 22);
103 
104 	m_tabsView->setCurrentIndex(m_model->index((activeWindow ? findRow(activeWindow->getIdentifier()) : 0), 0));
105 	m_tabsView->setMinimumHeight(qMin(contentsHeight, int(height() * 0.9)));
106 
107 	QWidget::showEvent(event);
108 
109 	connect(m_mainWindow, &MainWindow::windowAdded, this, &TabSwitcherWidget::handleWindowAdded);
110 	connect(m_mainWindow, &MainWindow::windowRemoved, this, &TabSwitcherWidget::handleWindowRemoved);
111 }
112 
hideEvent(QHideEvent * event)113 void TabSwitcherWidget::hideEvent(QHideEvent *event)
114 {
115 	QWidget::hideEvent(event);
116 
117 	disconnect(m_mainWindow, &MainWindow::windowAdded, this, &TabSwitcherWidget::handleWindowAdded);
118 	disconnect(m_mainWindow, &MainWindow::windowRemoved, this, &TabSwitcherWidget::handleWindowRemoved);
119 
120 	m_model->clear();
121 }
122 
keyPressEvent(QKeyEvent * event)123 void TabSwitcherWidget::keyPressEvent(QKeyEvent *event)
124 {
125 	switch (event->key())
126 	{
127 		case Qt::Key_Tab:
128 		case Qt::Key_Down:
129 			selectTab(true);
130 
131 			break;
132 		case Qt::Key_Backtab:
133 		case Qt::Key_Up:
134 			selectTab(false);
135 
136 			break;
137 		case Qt::Key_Enter:
138 		case Qt::Key_Return:
139 			accept();
140 
141 			break;
142 		case Qt::Key_Escape:
143 			hide();
144 
145 			break;
146 		default:
147 			break;
148 	}
149 }
150 
keyReleaseEvent(QKeyEvent * event)151 void TabSwitcherWidget::keyReleaseEvent(QKeyEvent *event)
152 {
153 	if (event->key() == Qt::Key_Control && m_reason == KeyboardReason)
154 	{
155 		accept();
156 
157 		event->accept();
158 	}
159 }
160 
show(SwitcherReason reason)161 void TabSwitcherWidget::show(SwitcherReason reason)
162 {
163 	m_reason = reason;
164 
165 	QWidget::show();
166 }
167 
accept()168 void TabSwitcherWidget::accept()
169 {
170 	m_mainWindow->setActiveWindowByIdentifier(m_tabsView->currentIndex().data(IdentifierRole).toULongLong());
171 
172 	hide();
173 }
174 
selectTab(bool next)175 void TabSwitcherWidget::selectTab(bool next)
176 {
177 	const int currentRow(m_tabsView->currentIndex().row());
178 
179 	m_tabsView->setCurrentIndex(m_model->index((next ? ((currentRow == (m_model->rowCount() - 1)) ? 0 : (currentRow + 1)) : ((currentRow == 0) ? (m_model->rowCount() - 1) : (currentRow - 1))), 0));
180 }
181 
handleIndexClicked(const QModelIndex & index)182 void TabSwitcherWidget::handleIndexClicked(const QModelIndex &index)
183 {
184 	if (index.isValid())
185 	{
186 		m_mainWindow->setActiveWindowByIdentifier(index.data(IdentifierRole).toULongLong());
187 
188 		hide();
189 	}
190 }
191 
handleCurrentTabChanged(const QModelIndex & index)192 void TabSwitcherWidget::handleCurrentTabChanged(const QModelIndex &index)
193 {
194 	const Window *window(m_mainWindow->getWindowByIdentifier(index.data(IdentifierRole).toULongLong()));
195 
196 	m_previewLabel->setMovie(nullptr);
197 	m_previewLabel->setPixmap({});
198 
199 	if (!window)
200 	{
201 		return;
202 	}
203 
204 	if (window->getLoadingState() == WebWidget::DeferredLoadingState || window->getLoadingState() == WebWidget::OngoingLoadingState)
205 	{
206 		if (!m_spinnerAnimation)
207 		{
208 			const QString path(ThemesManager::getAnimationPath(QLatin1String("spinner")));
209 
210 			if (path.isEmpty())
211 			{
212 				m_spinnerAnimation = new SpinnerAnimation(this);
213 			}
214 			else
215 			{
216 				m_spinnerAnimation = new GenericAnimation(path, this);
217 			}
218 
219 			connect(m_spinnerAnimation, &Animation::frameChanged, m_previewLabel, [&]()
220 			{
221 				m_previewLabel->setPixmap(m_spinnerAnimation->getCurrentPixmap());
222 			});
223 		}
224 
225 		m_spinnerAnimation->start();
226 
227 		m_previewLabel->setPixmap(m_spinnerAnimation->getCurrentPixmap());
228 	}
229 	else
230 	{
231 		if (m_spinnerAnimation && m_spinnerAnimation->isRunning())
232 		{
233 			m_spinnerAnimation->stop();
234 		}
235 
236 		m_previewLabel->setPixmap((window->getLoadingState() == WebWidget::CrashedLoadingState) ? ThemesManager::createIcon(QLatin1String("tab-crashed")).pixmap(32, 32) : window->createThumbnail());
237 	}
238 }
239 
handleWindowAdded(quint64 identifier)240 void TabSwitcherWidget::handleWindowAdded(quint64 identifier)
241 {
242 	Window *window(m_mainWindow->getWindowByIdentifier(identifier));
243 
244 	if (window && (!m_isIgnoringMinimizedTabs || window->getWindowState().state != Qt::WindowMinimized))
245 	{
246 		m_model->insertRow(0, createRow(window, (SettingsManager::getOption(SettingsManager::TabSwitcher_OrderByLastActivityOption).toBool() ? QVariant(window->getLastActivity()) : QVariant(-1))));
247 	}
248 }
249 
handleWindowRemoved(quint64 identifier)250 void TabSwitcherWidget::handleWindowRemoved(quint64 identifier)
251 {
252 	const int row(findRow(identifier));
253 
254 	if (row >= 0)
255 	{
256 		m_model->removeRow(row);
257 	}
258 }
259 
setTitle(const QString & title)260 void TabSwitcherWidget::setTitle(const QString &title)
261 {
262 	const Window *window(qobject_cast<Window*>(sender()));
263 
264 	if (window)
265 	{
266 		const int row(findRow(window->getIdentifier()));
267 
268 		if (row >= 0)
269 		{
270 			m_model->setData(m_model->index(row, 0), title, Qt::DisplayRole);
271 		}
272 	}
273 }
274 
setIcon(const QIcon & icon)275 void TabSwitcherWidget::setIcon(const QIcon &icon)
276 {
277 	const Window *window(qobject_cast<Window*>(sender()));
278 
279 	if (window)
280 	{
281 		const int row(findRow(window->getIdentifier()));
282 
283 		if (row >= 0)
284 		{
285 			m_model->setData(m_model->index(row, 0), icon, Qt::DecorationRole);
286 		}
287 	}
288 }
289 
setLoadingState(WebWidget::LoadingState state)290 void TabSwitcherWidget::setLoadingState(WebWidget::LoadingState state)
291 {
292 	const Window *window(qobject_cast<Window*>(sender()));
293 
294 	if (window)
295 	{
296 		const int row(findRow(window->getIdentifier()));
297 
298 		if (row >= 0)
299 		{
300 			QColor color(palette().color(QPalette::Text));
301 
302 			if (state == WebWidget::DeferredLoadingState)
303 			{
304 				color.setAlpha(150);
305 			}
306 
307 			m_model->setData(m_model->index(row, 0), color, Qt::TextColorRole);
308 		}
309 	}
310 }
311 
createRow(Window * window,const QVariant & index) const312 QStandardItem* TabSwitcherWidget::createRow(Window *window, const QVariant &index) const
313 {
314 	QColor color(palette().color(QPalette::Text));
315 
316 	if (window->getLoadingState() == WebWidget::DeferredLoadingState)
317 	{
318 		color.setAlpha(150);
319 	}
320 
321 	QStandardItem *item(new QStandardItem(window->getIcon(), window->getTitle()));
322 	item->setData(color, Qt::TextColorRole);
323 	item->setData(window->getIdentifier(), IdentifierRole);
324 	item->setData(index, OrderRole);
325 	item->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
326 
327 	connect(window, &Window::titleChanged, this, &TabSwitcherWidget::setTitle);
328 	connect(window, &Window::iconChanged, this, &TabSwitcherWidget::setIcon);
329 	connect(window, &Window::loadingStateChanged, this, &TabSwitcherWidget::setLoadingState);
330 
331 	return item;
332 }
333 
getReason() const334 TabSwitcherWidget::SwitcherReason TabSwitcherWidget::getReason() const
335 {
336 	return m_reason;
337 }
338 
findRow(quint64 identifier) const339 int TabSwitcherWidget::findRow(quint64 identifier) const
340 {
341 	for (int i = 0; i < m_model->rowCount(); ++i)
342 	{
343 		if (m_model->index(i, 0).data(IdentifierRole).toULongLong() == identifier)
344 		{
345 			return i;
346 		}
347 	}
348 
349 	return -1;
350 }
351 
eventFilter(QObject * object,QEvent * event)352 bool TabSwitcherWidget::eventFilter(QObject *object, QEvent *event)
353 {
354 	if (object == m_tabsView->viewport() && event->type() == QEvent::MouseButtonPress)
355 	{
356 		const QMouseEvent *mouseEvent(static_cast<QMouseEvent*>(event));
357 
358 		if (mouseEvent->button() == Qt::MiddleButton)
359 		{
360 			const QModelIndex index(m_tabsView->indexAt(mouseEvent->pos()));
361 
362 			if (index.isValid())
363 			{
364 				Application::triggerAction(ActionsManager::CloseTabAction, {{QLatin1String("tab"), index.data(IdentifierRole).toULongLong()}}, parentWidget());
365 			}
366 
367 			return true;
368 		}
369 	}
370 
371 	return QObject::eventFilter(object, event);
372 }
373 
374 }
375