1 /*
2  * %kadu copyright begin%
3  * Copyright 2012 Wojciech Treter (juzefwt@gmail.com)
4  * Copyright 2013, 2014 Bartosz Brachaczek (b.brachaczek@gmail.com)
5  * Copyright 2013, 2014, 2015 Rafał Przemysław Malinowski (rafal.przemyslaw.malinowski@gmail.com)
6  * %kadu copyright end%
7  *
8  * This program is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU General Public License as
10  * published by the Free Software Foundation; either version 2 of
11  * the License, or (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16  * GNU General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program. If not, see <http://www.gnu.org/licenses/>.
20  */
21 
22 #include <QtCore/QPoint>
23 #include <QtCore/QVariant>
24 #include <QtGui/QDrag>
25 #include <QtWidgets/QHBoxLayout>
26 #include <QtWidgets/QMenu>
27 
28 #include "chat/chat.h"
29 #include "chat/model/chat-data-extractor.h"
30 #include "configuration/configuration.h"
31 #include "configuration/deprecated-configuration-api.h"
32 #include "core/application.h"
33 #include "gui/hot-key.h"
34 #include "gui/widgets/chat-widget/chat-widget-manager.h"
35 #include "gui/widgets/chat-widget/chat-widget.h"
36 #include "gui/widgets/filtered-tree-view.h"
37 #include "gui/widgets/recent-chats-menu.h"
38 #include "gui/windows/message-dialog.h"
39 #include "gui/windows/open-chat-with/open-chat-with-service.h"
40 #include "gui/windows/open-chat-with/open-chat-with.h"
41 #include "icons/icons-manager.h"
42 #include "icons/kadu-icon.h"
43 #include "message/unread-message-repository.h"
44 #include "misc/misc.h"
45 #include "plugin/plugin-injected-factory.h"
46 #include "activate.h"
47 
48 #include "tab-bar.h"
49 #include "tabs.h"
50 
51 #include "tab-widget.h"
52 
TabWidget(TabsManager * manager)53 TabWidget::TabWidget(TabsManager *manager) : Manager(manager)
54 {
55 }
56 
~TabWidget()57 TabWidget::~TabWidget()
58 {
59 }
60 
setApplication(Application * application)61 void TabWidget::setApplication(Application *application)
62 {
63 	m_application = application;
64 }
65 
setChatWidgetManager(ChatWidgetManager * chatWidgetManager)66 void TabWidget::setChatWidgetManager(ChatWidgetManager *chatWidgetManager)
67 {
68 	m_chatWidgetManager = chatWidgetManager;
69 }
70 
setConfiguration(Configuration * configuration)71 void TabWidget::setConfiguration(Configuration *configuration)
72 {
73 	m_configuration = configuration;
74 }
75 
setIconsManager(IconsManager * iconsManager)76 void TabWidget::setIconsManager(IconsManager *iconsManager)
77 {
78 	m_iconsManager = iconsManager;
79 }
80 
setPluginInjectedFactory(PluginInjectedFactory * pluginInjectedFactory)81 void TabWidget::setPluginInjectedFactory(PluginInjectedFactory *pluginInjectedFactory)
82 {
83 	m_pluginInjectedFactory = pluginInjectedFactory;
84 }
85 
setOpenChatWithService(OpenChatWithService * openChatWithService)86 void TabWidget::setOpenChatWithService(OpenChatWithService *openChatWithService)
87 {
88 	m_openChatWithService = openChatWithService;
89 }
90 
init()91 void TabWidget::init()
92 {
93 	setWindowRole("kadu-tabs");
94 
95 	TabBar *tabbar = new TabBar(this);
96 	setTabBar(tabbar);
97 
98 	setAcceptDrops(true);
99 	setMovable(true);
100 
101 	setDocumentMode(true);
102 
103 	connect(tabbar, SIGNAL(contextMenu(int, const QPoint &)),
104 			SLOT(onContextMenu(int, const QPoint &)));
105 	connect(tabbar, SIGNAL(tabCloseRequested(int)),
106 			SLOT(onDeleteTab(int)));
107 	connect(tabbar,SIGNAL(mouseDoubleClickEventSignal(QMouseEvent *)),
108 			SLOT(mouseDoubleClickEvent(QMouseEvent *)));
109 	connect(tabbar, SIGNAL(currentChanged(int)),
110 			SLOT(currentTabChanged(int)));
111 
112 	//widget (container) for buttons with opening conversations
113 	//both buttons are displayed when checking Show "New Tab" button in configurations
114 	OpenChatButtonsWidget = new QWidget(this);
115 	QHBoxLayout *horizontalLayout = new QHBoxLayout;
116 
117 	horizontalLayout->setSpacing(2);
118 	horizontalLayout->setContentsMargins(3, 0, 2, 3);
119 
120 	//button for new chat from last conversations
121 	OpenRecentChatButton = new QToolButton(OpenChatButtonsWidget);
122 	OpenRecentChatButton->setIcon(m_iconsManager->iconByPath(KaduIcon("internet-group-chat")));
123 	OpenRecentChatButton->setToolTip(tr("Recent Chats"));
124 	OpenRecentChatButton->setAutoRaise(true);
125 	connect(OpenRecentChatButton, SIGNAL(clicked()), SLOT(openRecentChatsMenu()));
126 
127 	//menu for recent chats
128 	RecentChatsMenuWidget = m_pluginInjectedFactory->makeInjected<RecentChatsMenu>(OpenRecentChatButton);
129 	connect(RecentChatsMenuWidget, SIGNAL(triggered(QAction *)), this, SLOT(openRecentChat(QAction *)));
130 	connect(RecentChatsMenuWidget, SIGNAL(chatsListAvailable(bool)), OpenRecentChatButton, SLOT(setEnabled(bool)));
131 
132 	//button for opening chat
133 	QToolButton *openChatButton = new QToolButton(OpenChatButtonsWidget);
134 	openChatButton->setIcon(m_iconsManager->iconByPath(KaduIcon("mail-message-new")));
135 	openChatButton->setToolTip(tr("Open Chat with..."));
136 	openChatButton->setAutoRaise(true);
137 	connect(openChatButton, SIGNAL(clicked()), SLOT(newChat()));
138 
139 	horizontalLayout->addWidget(OpenRecentChatButton);
140 	horizontalLayout->addWidget(openChatButton);
141 
142 	OpenChatButtonsWidget->setLayout(horizontalLayout);
143 	OpenChatButtonsWidget->setVisible(false);
144 
145 	RightCornerWidget = new QWidget(this);
146 	QHBoxLayout *rightCornerWidgetLayout = new QHBoxLayout;
147 
148 	rightCornerWidgetLayout->setSpacing(2);
149 	rightCornerWidgetLayout->setContentsMargins(3, 0, 2, 3);
150 
151 	TabsMenu = new QMenu(this);
152 	connect(TabsMenu, SIGNAL(triggered(QAction *)), this, SLOT(tabsMenuSelected(QAction *)));
153 	TabsListButton = new QToolButton(RightCornerWidget);
154 	TabsListButton->setIcon(m_iconsManager->iconByPath(KaduIcon("internet-group-chat")));
155 	TabsListButton->setToolTip(tr("Tabs"));
156 	TabsListButton->setAutoRaise(true);
157 	TabsListButton->setVisible(false);
158 	TabsListButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
159 	TabsListButton->setMenu(TabsMenu);
160 	connect(TabsListButton, SIGNAL(clicked()), SLOT(openTabsList()));
161 	rightCornerWidgetLayout->addWidget(TabsListButton);
162 
163 	//przycisk zamkniecia aktywnej karty znajdujacy sie w prawym gornym rogu
164 	CloseChatButton = new QToolButton(this);
165 	CloseChatButton->setIcon(m_iconsManager->iconByPath(KaduIcon("kadu_icons/tab-remove")));
166 	CloseChatButton->setToolTip(tr("Close Tab"));
167 	CloseChatButton->setAutoRaise(true);
168 	CloseChatButton->setVisible(false);
169 	connect(CloseChatButton, SIGNAL(clicked()), SLOT(deleteTab()));
170 	rightCornerWidgetLayout->addWidget(CloseChatButton);
171 
172 	RightCornerWidget->setLayout(rightCornerWidgetLayout);
173 	setCornerWidget(RightCornerWidget, Qt::TopRightCorner);
174 
175 	configurationUpdated();
176 }
177 
isTabVisible(int index)178 bool TabWidget::isTabVisible(int index)
179 {
180 	QRect visibleTabRect = tabBar()->rect().intersected(tabBar()->tabRect(index));
181 
182 	return visibleTabRect.width() > 20;
183 }
184 
updateTabsMenu()185 void TabWidget::updateTabsMenu()
186 {
187 	TabsMenu->clear();
188 
189 	for (int i = 0; i < count(); i++)
190 	{
191 		QAction *action = new QAction(QIcon(), tabText(i), this);
192 		action->setData(QVariant(i));
193 
194 		if (i == tabBar()->currentIndex())
195 		{
196 			QFont font = action->font();
197 			font.setBold(true);
198 			action->setFont(font);
199 		}
200 
201 		TabsMenu->addAction(action);
202 	}
203 }
204 
currentTabChanged(int index)205 void TabWidget::currentTabChanged(int index)
206 {
207 	Q_UNUSED(index);
208 
209 	updateTabsMenu();
210 }
211 
tabsMenuSelected(QAction * action)212 void TabWidget::tabsMenuSelected(QAction *action)
213 {
214 	setCurrentIndex(action->data().toInt());
215 	tabBar()->setCurrentIndex(action->data().toInt());
216 }
217 
tryActivateChatWidget(ChatWidget * chatWidget)218 void TabWidget::tryActivateChatWidget(ChatWidget *chatWidget)
219 {
220 	int index = indexOf(chatWidget);
221 	if (index < 0)
222 		return;
223 
224 	_activateWindow(m_configuration, this);
225 
226 	setCurrentIndex(index);
227 	chatWidget->edit()->setFocus();
228 }
229 
tryMinimizeChatWidget(ChatWidget * chatWidget)230 void TabWidget::tryMinimizeChatWidget(ChatWidget *chatWidget)
231 {
232 	int index = indexOf(chatWidget);
233 	if (index < 0)
234 		return;
235 
236 	if (count() == 1)
237 		window()->showMinimized();
238 }
239 
closeTab(ChatWidget * chatWidget)240 void TabWidget::closeTab(ChatWidget *chatWidget)
241 {
242 	if (!chatWidget)
243 		return;
244 
245 	if (m_configuration->deprecatedApi()->readBoolEntry("Chat", "ChatCloseTimer"))
246 	{
247 		unsigned int period = m_configuration->deprecatedApi()->readUnsignedNumEntry("Chat",
248 			"ChatCloseTimerPeriod", 2);
249 
250 		if (QDateTime::currentDateTime() < chatWidget->lastReceivedMessageTime().addSecs(period))
251 		{
252 			MessageDialog *dialog = MessageDialog::create(m_iconsManager->iconByPath(KaduIcon("dialog-question")), tr("Kadu"), tr("New message received, close window anyway?"));
253 			dialog->addButton(QMessageBox::Yes, tr("Close window"));
254 			dialog->addButton(QMessageBox::No, tr("Cancel"));
255 
256 			if (!dialog->ask())
257 				return;
258 		}
259 	}
260 
261 	delete chatWidget;
262 }
263 
isChatWidgetActive(const ChatWidget * chatWidget)264 bool TabWidget::isChatWidgetActive(const ChatWidget *chatWidget)
265 {
266 	return currentWidget() == chatWidget && _isWindowActiveOrFullyVisible(this);
267 }
268 
closeEvent(QCloseEvent * e)269 void TabWidget::closeEvent(QCloseEvent *e)
270 {
271 	// do not block window closing when session is about to close
272 	if (m_application->isSavingSession())
273 	{
274 		QTabWidget::closeEvent(e);
275 		return;
276 	}
277 
278 	//w zaleznosci od opcji w konfiguracji zamykamy wszystkie karty, lub tylko aktywna
279 	if (config_oldStyleClosing)
280 		closeTab(static_cast<ChatWidget *>(currentWidget()));
281 	else
282 		for (int i = count() - 1; i >= 0; i--)
283 			closeTab(static_cast<ChatWidget *>(widget(i)));
284 
285 	if (count() > 0)
286 		e->ignore();
287 	else
288 		e->accept();
289 }
290 
chatKeyPressed(QKeyEvent * e,CustomInput * k,bool & handled)291 void TabWidget::chatKeyPressed(QKeyEvent *e, CustomInput *k, bool &handled)
292 {
293 	Q_UNUSED(k)
294 
295 	if (handled)
296 		return;
297 
298 	handled = true;
299 	// obsluga skrotow klawiszowych
300 	if (HotKey::shortCut(m_configuration, e, "ShortCuts", "MoveTabLeft"))
301 		moveTabLeft();
302 	else if (HotKey::shortCut(m_configuration, e, "ShortCuts", "MoveTabRight"))
303 		moveTabRight();
304 	else if (HotKey::shortCut(m_configuration, e, "ShortCuts", "SwitchTabLeft"))
305 		switchTabLeft();
306 	else if (HotKey::shortCut(m_configuration, e, "ShortCuts", "SwitchTabRight"))
307 		switchTabRight();
308 	else if (HotKey::shortCut(m_configuration, e, "ShortCuts", "ReopenClosedTab"))
309 		Manager->reopenClosedChat();
310 	#if defined(Q_OS_WIN)
311 		#define TAB_SWITCH_MODIFIER "Ctrl"
312 	#else
313 		#define TAB_SWITCH_MODIFIER "Alt"
314 	#endif
315 	else if (HotKey::keyEventToString(e, QKeySequence::PortableText) == TAB_SWITCH_MODIFIER "+0")
316 		setCurrentIndex(count() - 1);
317 	else if (HotKey::keyEventToString(e, QKeySequence::PortableText) == TAB_SWITCH_MODIFIER "+1")
318 		setCurrentIndex(0);
319 	else if (HotKey::keyEventToString(e, QKeySequence::PortableText) == TAB_SWITCH_MODIFIER "+2")
320 		setCurrentIndex(1);
321 	else if (HotKey::keyEventToString(e, QKeySequence::PortableText) == TAB_SWITCH_MODIFIER "+3")
322 		setCurrentIndex(2);
323 	else if (HotKey::keyEventToString(e, QKeySequence::PortableText) == TAB_SWITCH_MODIFIER "+4")
324 		setCurrentIndex(3);
325 	else if (HotKey::keyEventToString(e, QKeySequence::PortableText) == TAB_SWITCH_MODIFIER "+5")
326 		setCurrentIndex(4);
327 	else if (HotKey::keyEventToString(e, QKeySequence::PortableText) == TAB_SWITCH_MODIFIER "+6")
328 		setCurrentIndex(5);
329 	else if (HotKey::keyEventToString(e, QKeySequence::PortableText) == TAB_SWITCH_MODIFIER "+7")
330 		setCurrentIndex(6);
331 	else if (HotKey::keyEventToString(e, QKeySequence::PortableText) == TAB_SWITCH_MODIFIER "+8")
332 		setCurrentIndex(7);
333 	else if (HotKey::keyEventToString(e, QKeySequence::PortableText) == TAB_SWITCH_MODIFIER "+9")
334 		setCurrentIndex(8);
335 	else
336 		// skrot nie zostal znaleziony i wykonany. Przekazujemy zdarzenie dalej
337 		handled = false;
338 }
339 
onContextMenu(int id,const QPoint & pos)340 void TabWidget::onContextMenu(int id, const QPoint &pos)
341 {
342 	emit contextMenu(widget(id), pos);
343 }
344 
moveTab(int from,int to)345 void TabWidget::moveTab(int from, int to)
346 {
347 	kdebugf();
348 	QString tablabel = tabText(from);
349 	QWidget *w = widget(from);
350 	QIcon tabiconset = tabIcon(from);
351 	QString tabtooltip = tabToolTip(from);
352 	bool current = (w == currentWidget());
353 	blockSignals(true);
354 	removeTab(from);
355 
356 	insertTab(to, w, tabiconset, tablabel);
357 	setTabToolTip(to, tabtooltip);
358 
359 	if (current)
360 		setCurrentIndex(to);
361 
362 	blockSignals(false);
363 }
364 
onDeleteTab(int id)365 void TabWidget::onDeleteTab(int id)
366 {
367 	closeTab(static_cast<ChatWidget *>(widget(id)));
368 }
369 
switchTabLeft()370 void TabWidget::switchTabLeft()
371 {
372 	if (currentIndex() == 0)
373 		setCurrentIndex(count() - 1);
374 	else
375 		setCurrentIndex(currentIndex() - 1);
376 }
377 
switchTabRight()378 void TabWidget::switchTabRight()
379 {
380 	if (currentIndex() == (count() - 1))
381 		setCurrentIndex(0);
382 	else
383 		setCurrentIndex(currentIndex() + 1);
384 }
385 
moveTabLeft()386 void TabWidget::moveTabLeft()
387 {
388 	if (count() == 1)
389 		return;
390 
391 	if (currentIndex() == 0)
392 		moveTab(0, count() - 1);
393 	else
394 		moveTab(currentIndex(), currentIndex() - 1);
395 }
396 
moveTabRight()397 void TabWidget::moveTabRight()
398 {
399 	if (count() == 1)
400 		return;
401 
402 	if (currentIndex() == (count() - 1))
403 		moveTab(count() - 1, 0);
404 	else
405 		moveTab(currentIndex(), currentIndex() + 1);
406 }
407 
dragEnterEvent(QDragEnterEvent * e)408 void TabWidget::dragEnterEvent(QDragEnterEvent* e)
409 {
410 	kdebugf();
411 	// Akceptujemu dnd jezeli pochodzi on z UserBox'a lub paska kart
412 // 	if ((UlesDrag::canDecode(e) && (qobject_cast<ContactsListWidget *>(e->source()))))
413 // 		e->acceptProposedAction();
414 // 	else
415 		e->ignore();
416 //
417 	kdebugf2();
418 }
419 
dropEvent(QDropEvent * e)420 void TabWidget::dropEvent(QDropEvent* e)
421 {
422 	kdebugf();
423 	QStringList ules;
424 
425 	// Jezeli dnd pochodzil z userboxa probujemy dodac nowa karte
426 	if (qobject_cast<FilteredTreeView *>(e->source()) && false)/*UlesDrag::decode(e, ules))*/
427 	{
428 		if (tabBar()->tabAt(e->pos()) != -1)
429 		// Jezeli w miejscu upuszczenia jest karta, dodajemy na jej pozycji
430 			emit openTab(ules, tabBar()->tabAt(e->pos()));
431 		else
432 		// Jezeli nie na koncu tabbara
433 			emit openTab(ules, -1);
434 	}
435 
436 	kdebugf2();
437 }
438 
changeEvent(QEvent * event)439 void TabWidget::changeEvent(QEvent *event)
440 {
441 	QTabWidget::changeEvent(event);
442 	if (event->type() == QEvent::ActivationChange)
443 	{
444 		kdebugf();
445 		ChatWidget *chatWidget = static_cast<ChatWidget *>(currentWidget());
446 		if (chatWidget && _isActiveWindow(this))
447 			emit chatWidgetActivated(chatWidget);
448 		kdebugf2();
449 	}
450 }
451 
mouseDoubleClickEvent(QMouseEvent * e)452 void TabWidget::mouseDoubleClickEvent(QMouseEvent *e)
453 {
454 	kdebugf();
455 	// jezeli dwuklik nastapil lewym przyciskiem myszy pokazujemy okno openchatwith
456 	if (e->button() == Qt::LeftButton)
457 		newChat();
458 	kdebugf2();
459 }
460 
newChat()461 void TabWidget::newChat()
462 {
463 	m_openChatWithService->show();
464 }
465 
openRecentChatsMenu()466 void TabWidget::openRecentChatsMenu()
467 {
468 	//show last conversations menu under widget with buttons for opening chats
469 	RecentChatsMenuWidget->popup(OpenChatButtonsWidget->mapToGlobal(QPoint(0, OpenChatButtonsWidget->height())));
470 }
471 
openTabsList()472 void TabWidget::openTabsList()
473 {
474 	//show last conversations menu under widget with buttons for opening chats
475 	TabsMenu->popup(RightCornerWidget->mapToGlobal(QPoint(0, RightCornerWidget->height())));
476 }
477 
openRecentChat(QAction * action)478 void TabWidget::openRecentChat(QAction *action)
479 {
480 	m_chatWidgetManager->openChat(action->data().value<Chat>(), OpenChatActivation::Activate);
481 }
482 
deleteTab()483 void TabWidget::deleteTab()
484 {
485 	closeTab(static_cast<ChatWidget *>(currentWidget()));
486 }
487 
tabInserted(int index)488 void TabWidget::tabInserted(int index)
489 {
490 	Q_UNUSED(index)
491 
492 	auto chatWidget = static_cast<ChatWidget *>(widget(index));
493 	connect(chatWidget, SIGNAL(closeRequested(ChatWidget*)), this, SLOT(closeTab(ChatWidget*)));
494 
495 	updateTabsListButton();
496 	updateTabsMenu();
497 }
498 
tabRemoved(int index)499 void TabWidget::tabRemoved(int index)
500 {
501 	Q_UNUSED(index)
502 
503 	updateTabsListButton();
504 	updateTabsMenu();
505 
506 	if (count() == 0)
507 		hide();
508 }
509 
compositingEnabled()510 void TabWidget::compositingEnabled()
511 {
512 	if (m_configuration->deprecatedApi()->readBoolEntry("Chat", "UseTransparency", false))
513 	{
514 		setAutoFillBackground(false);
515 		setAttribute(Qt::WA_TranslucentBackground, true);
516 	}
517 	else
518 		compositingDisabled();
519 }
520 
compositingDisabled()521 void TabWidget::compositingDisabled()
522 {
523 	setAttribute(Qt::WA_TranslucentBackground, false);
524 	setAttribute(Qt::WA_NoSystemBackground, false);
525 	setAutoFillBackground(true);
526 }
527 
configurationUpdated()528 void TabWidget::configurationUpdated()
529 {
530 	triggerCompositingStateChanged();
531 
532 	CloseChatButton->setIcon(m_iconsManager->iconByPath(KaduIcon("kadu_icons/tab-remove")));
533 
534 	setTabsClosable(m_configuration->deprecatedApi()->readBoolEntry("Tabs", "CloseButtonOnTab"));
535 	config_oldStyleClosing = m_configuration->deprecatedApi()->readBoolEntry("Tabs", "OldStyleClosing");
536 
537 	bool isOpenChatButtonEnabled = (cornerWidget(Qt::TopLeftCorner) == OpenChatButtonsWidget);
538 	bool shouldEnableOpenChatButton = m_configuration->deprecatedApi()->readBoolEntry("Tabs", "OpenChatButton");
539 	bool isCloseButtonEnabled = CloseChatButton->isVisible();
540 	bool shouldEnableCloseButton = m_configuration->deprecatedApi()->readBoolEntry("Tabs", "CloseButton");
541 
542 	if (isOpenChatButtonEnabled != shouldEnableOpenChatButton)
543 	{
544 		OpenChatButtonsWidget->setVisible(true);
545 		setCornerWidget(shouldEnableOpenChatButton ? OpenChatButtonsWidget : 0, Qt::TopLeftCorner);
546 	}
547 
548 	if (isCloseButtonEnabled != shouldEnableCloseButton)
549 	{
550 		CloseChatButton->setVisible(shouldEnableCloseButton);
551 	}
552 }
553 
showEvent(QShowEvent * e)554 void TabWidget::showEvent(QShowEvent* e)
555 {
556 	QTabWidget::showEvent(e);
557 
558 	updateTabsListButton();
559 	updateTabsMenu();
560 }
561 
resizeEvent(QResizeEvent * e)562 void TabWidget::resizeEvent(QResizeEvent *e)
563 {
564 	QTabWidget::resizeEvent(e);
565 
566 	updateTabsListButton();
567 	updateTabsMenu();
568 }
569 
updateTabsListButton()570 void TabWidget::updateTabsListButton()
571 {
572 	bool allTabsVisible = true;
573 
574 	for (int i = 0; i < tabBar()->count(); i++)
575 	{
576 		if (!isTabVisible(i))
577 		{
578 			allTabsVisible = false;
579 			break;
580 		}
581 	}
582 
583 	TabsListButton->setVisible(!allTabsVisible);
584 	TabsListButton->setText(QString::number(count()));
585 }
586 
587 #include "moc_tab-widget.cpp"
588