/* * Copyright (c) 2010-2016 Isode Limited. * All rights reserved. * See the COPYING file for more information. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Swift { QtChatTabs::QtChatTabs(bool singleWindow, SettingsProvider* settingsProvider, bool trellisMode) : QWidget(), singleWindow_(singleWindow), settingsProvider_(settingsProvider), trellisMode_(trellisMode), dynamicGrid_(nullptr), gridSelectionDialog_(nullptr) { #ifndef Q_OS_MAC setWindowIcon(QIcon(":/logo-chat-16.png")); #else setAttribute(Qt::WA_ShowWithoutActivating); #endif dynamicGrid_ = new QtDynamicGridLayout(this, trellisMode); connect(dynamicGrid_, SIGNAL(tabCloseRequested(int)), this, SLOT(handleTabCloseRequested(int))); connect(dynamicGrid_, SIGNAL(onCurrentIndexChanged(int)), this, SLOT(handleCurrentTabIndexChanged(int))); QVBoxLayout *layout = new QVBoxLayout; layout->setSpacing(0); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(dynamicGrid_); setLayout(layout); if (trellisMode) { // restore size std::string gridSizeString = settingsProvider->getSetting(SettingConstants::TRELLIS_GRID_SIZE); if (!gridSizeString.empty()) { QByteArray gridSizeData = QByteArray::fromBase64(P2QSTRING(gridSizeString).toUtf8()); QDataStream dataStreamGridSize(&gridSizeData, QIODevice::ReadWrite); QSize gridSize(1,1); dataStreamGridSize >> gridSize; dynamicGrid_->setDimensions(gridSize); } // restore positions std::string tabPositionsString = settingsProvider->getSetting(SettingConstants::TRELLIS_GRID_POSITIONS); if (!tabPositionsString.empty()) { QByteArray tabPositionsData = QByteArray::fromBase64(P2QSTRING(tabPositionsString).toUtf8()); QDataStream inTabPositions(&tabPositionsData, QIODevice::ReadWrite); QHash tabPositions; inTabPositions >> tabPositions; dynamicGrid_->setTabPositions(tabPositions); } } gridSelectionDialog_ = new QtGridSelectionDialog(); // setup shortcuts shortcuts_ << new QShortcut(QKeySequence(tr("CTRL+W", "Close chat tab.")), window(), SLOT(handleCloseTabShortcut())); shortcuts_ << new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_PageUp), window(), SLOT(handleRequestedPreviousTab())); shortcuts_ << new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_PageDown), window(), SLOT(handleRequestedNextTab())); shortcuts_ << new QShortcut(QKeySequence(Qt::ALT + Qt::Key_A), window(), SLOT(handleRequestedActiveTab())); } QtChatTabs::~QtChatTabs() { foreach (QShortcut* shortcut, shortcuts_) { delete shortcut; } if (trellisMode_) { storeTabPositions(); } delete gridSelectionDialog_; } void QtChatTabs::closeEvent(QCloseEvent* event) { //Hide first to prevent flickering as each tab is removed. hide(); if (trellisMode_) { storeTabPositions(); } for (int i = dynamicGrid_->count() - 1; i >= 0; i--) { dynamicGrid_->widget(i)->close(); } event->accept(); } QtTabbable* QtChatTabs::getCurrentTab() { return qobject_cast(dynamicGrid_->currentWidget()); } void QtChatTabs::setViewMenu(QMenu* viewMenu) { if (trellisMode_) { viewMenu->addSeparator(); QAction* action = new QAction(tr("Change &layout"), this); action->setShortcutContext(Qt::ApplicationShortcut); action->setShortcut(QKeySequence(tr("Ctrl+Alt+L"))); connect(action, SIGNAL(triggered()), this, SLOT(handleOpenLayoutChangeDialog())); viewMenu->addAction(action); action = new QAction(tr("Move Tab right"), this); action->setShortcutContext(Qt::ApplicationShortcut); action->setShortcut(QKeySequence(tr("Ctrl+Shift+PgDown"))); connect(action, SIGNAL(triggered()), dynamicGrid_, SLOT(moveCurrentTabRight())); viewMenu->addAction(action); action = new QAction(tr("Move Tab left"), this); action->setShortcutContext(Qt::ApplicationShortcut); action->setShortcut(QKeySequence(tr("Ctrl+Shift+PgUp"))); connect(action, SIGNAL(triggered()), dynamicGrid_, SLOT(moveCurrentTabLeft())); viewMenu->addAction(action); action = new QAction(tr("Move Tab to next group"), this); action->setShortcutContext(Qt::ApplicationShortcut); action->setShortcut(QKeySequence(tr("Ctrl+Alt+PgDown"))); connect(action, SIGNAL(triggered()), dynamicGrid_, SLOT(moveCurrentTabToNextGroup())); viewMenu->addAction(action); action = new QAction(tr("Move Tab to previous group"), this); action->setShortcutContext(Qt::ApplicationShortcut); action->setShortcut(QKeySequence(tr("Ctrl+Alt+PgUp"))); connect(action, SIGNAL(triggered()), dynamicGrid_, SLOT(moveCurrentTabToPreviousGroup())); viewMenu->addAction(action); } } void QtChatTabs::addTab(QtTabbable* tab) { QSizePolicy policy = sizePolicy(); /* Chat windows like to grow - don't let them */ setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); dynamicGrid_->addTab(tab, tab->windowTitle()); connect(tab, SIGNAL(titleUpdated()), this, SLOT(handleTabTitleUpdated()), Qt::UniqueConnection); connect(tab, SIGNAL(countUpdated()), this, SLOT(handleTabTitleUpdated()), Qt::UniqueConnection); connect(tab, SIGNAL(windowClosing()), this, SLOT(handleTabClosing()), Qt::UniqueConnection); connect(tab, SIGNAL(windowOpening()), this, SLOT(handleWidgetShown()), Qt::UniqueConnection); connect(tab, SIGNAL(wantsToActivate()), this, SLOT(handleWantsToActivate()), Qt::UniqueConnection); connect(tab, SIGNAL(requestNextTab()), this, SLOT(handleRequestedNextTab()), Qt::UniqueConnection); connect(tab, SIGNAL(requestActiveTab()), this, SLOT(handleRequestedActiveTab()), Qt::UniqueConnection); connect(tab, SIGNAL(requestPreviousTab()), this, SLOT(handleRequestedPreviousTab()), Qt::UniqueConnection); connect(tab, SIGNAL(requestFlash()), this, SLOT(flash()), Qt::UniqueConnection); setSizePolicy(policy); } void QtChatTabs::handleWidgetShown() { QtTabbable* widget = qobject_cast(sender()); if (!widget) { return; } checkForFirstShow(); if (dynamicGrid_->indexOf(widget) >= 0) { handleTabTitleUpdated(widget); return; } widget->blockSignals(true); addTab(widget); widget->blockSignals(false); show(); } void QtChatTabs::handleCurrentTabIndexChanged(int newIndex) { handleTabTitleUpdated(dynamicGrid_->widget(newIndex)); } void QtChatTabs::handleWantsToActivate() { QtTabbable* widget = qobject_cast(sender()); Q_ASSERT(widget); //Un-minimize and bring to front. setWindowState(windowState() & ~Qt::WindowMinimized); setWindowState(windowState() | Qt::WindowActive); show(); widget->show(); dynamicGrid_->setCurrentWidget(widget); handleTabTitleUpdated(widget); widget->setFocus(); raise(); activateWindow(); } void QtChatTabs::handleTabClosing() { QWidget* widget = qobject_cast(sender()); int index; if (widget && ((index = dynamicGrid_->indexOf(widget)) >= 0)) { dynamicGrid_->removeTab(index); if (dynamicGrid_->count() == 0) { if (!singleWindow_) { hide(); } else { setWindowTitle(""); onTitleChanged(""); } } else { handleTabTitleUpdated(dynamicGrid_->currentWidget()); } } } void QtChatTabs::handleRequestedPreviousTab() { int newIndex = dynamicGrid_->currentIndex() - 1; dynamicGrid_->setCurrentIndex(newIndex >= 0 ? newIndex : dynamicGrid_->count() - 1); } void QtChatTabs::handleRequestedNextTab() { int newIndex = dynamicGrid_->currentIndex() + 1; dynamicGrid_->setCurrentIndex(newIndex < dynamicGrid_->count() ? newIndex : 0); } void QtChatTabs::handleRequestedActiveTab() { QtTabbable::AlertType types[] = {QtTabbable::WaitingActivity, QtTabbable::ImpendingActivity}; bool finished = false; for (int j = 0; j < 2; j++) { bool looped = false; for (int i = dynamicGrid_->currentIndex() + 1; !finished && i != dynamicGrid_->currentIndex(); i++) { if (i >= dynamicGrid_->count()) { if (looped) { break; } looped = true; i = 0; } if (qobject_cast(dynamicGrid_->widget(i))->getWidgetAlertState() == types[j]) { dynamicGrid_->setCurrentIndex(i); finished = true; break; } } } } void QtChatTabs::handleCloseTabShortcut() { QWidget* currentWidget = dynamicGrid_->currentWidget(); if (currentWidget) { currentWidget->close(); } } void QtChatTabs::handleTabCloseRequested(int index) { if (trellisMode_) { storeTabPositions(); } assert(index < dynamicGrid_->count()); QWidget* widget = dynamicGrid_->widget(index); assert(widget); widget->close(); } void QtChatTabs::handleTabTitleUpdated() { QWidget* widget = qobject_cast(sender()); handleTabTitleUpdated(widget); } void QtChatTabs::handleTabTitleUpdated(QWidget* widget) { if (!widget) { return; } QtTabbable* tabbable = qobject_cast(widget); int index = dynamicGrid_->indexOf(widget); if (index < 0) { return; } QString tabText = tabbable->windowTitle().simplified(); // look for spectrum-generated and other long JID localparts, and // try to abbreviate the resulting long tab texts QRegExp hasTrailingGarbage("^(.[-\\w\\s,&]+)([^\\s\\,w].*)$"); if (hasTrailingGarbage.exactMatch(tabText) && hasTrailingGarbage.cap(1).simplified().length() >= 2 && hasTrailingGarbage.cap(2).length() >= 7) { // there may be some trailing garbage, and it's long // enough to be worth removing, and we'd leave at // least a couple of characters. tabText = hasTrailingGarbage.cap(1).simplified(); } // QTabBar interprets &, so escape that tabText.replace("&", "&&"); // see which alt[a-z] keys other tabs use bool accelsTaken[26]; int i = 0; while (i < 26) { accelsTaken[i] = (i == 0); //A is used for 'switch to active tab' i++; } int other = dynamicGrid_->count(); while (other >= 0) { other--; if (other != index) { int tabIndex = -1; QtTabWidget* tabWidget = dynamicGrid_->indexToTabWidget(other, tabIndex); QString t = tabWidget->tabBar()->tabText(tabIndex).toLower(); int r = t.indexOf('&'); if (r >= 0 && t[r+1] >= 'a' && t[r+1] <= 'z') { accelsTaken[t[r+1].unicode()-'a'] = true; } } } // then look to see which letters in tabText may be used i = 0; int accelPos = -1; while (i < tabText.length() && accelPos < 0) { if (tabText[i] >= 'A' && tabText[i] <= 'Z' && !accelsTaken[tabText[i].unicode()-'A']) { accelPos = i; } if (tabText[i] >= 'a' && tabText[i] <= 'z' && !accelsTaken[tabText[i].unicode()-'a']) { accelPos = i; } ++i; } if (accelPos >= 0) { tabText = tabText.mid(0, accelPos) + "&" + tabText.mid(accelPos); } // this could be improved on some european keyboards, such as // the German one (where alt-Sz-Ligature is available) and basically // doesn't work on Arabic/Indic keyboards (where Latin letters // aren't available), but I don't care to deal with those. int tabIndex = -1; QtTabWidget* tabWidget = dynamicGrid_->indexToTabWidget(index, tabIndex); tabWidget->setTabText(tabIndex, tabbable->getCount() > 0 ? QString("(%1) %2").arg(tabbable->getCount()).arg(tabText) : tabText); QColor tabTextColor; switch (tabbable->getWidgetAlertState()) { case QtTabbable::WaitingActivity : tabTextColor = QColor(217, 20, 43); break; case QtTabbable::ImpendingActivity : tabTextColor = QColor(27, 171, 32); break; case QtTabbable::NoActivity : tabTextColor = QColor(); break; } tabWidget->tabBar()->setTabTextColor(tabIndex, tabTextColor); std::vector > unreads; for (int i = 0; i < dynamicGrid_->count(); i++) { QtTabbable* tab = qobject_cast(dynamicGrid_->widget(i)); if (tab) { unreads.push_back(std::pair(Q2PSTRING(tab->windowTitle()), tab->getCount())); } } std::string current(Q2PSTRING(qobject_cast(dynamicGrid_->currentWidget())->windowTitle())); ChatMessageSummarizer summary; QString title = summary.getSummary(current, unreads).c_str(); setWindowTitle(title); emit onTitleChanged(title); } void QtChatTabs::flash() { #ifndef SWIFTEN_PLATFORM_MACOSX QApplication::alert(this, 0); #endif } void QtChatTabs::handleOpenLayoutChangeDialog() { disconnect(gridSelectionDialog_, SIGNAL(currentGridSizeChanged(QSize)), dynamicGrid_, SLOT(setDimensions(QSize))); gridSelectionDialog_->setCurrentGridSize(dynamicGrid_->getDimension()); gridSelectionDialog_->move(QCursor::pos()); connect(gridSelectionDialog_, SIGNAL(currentGridSizeChanged(QSize)), dynamicGrid_, SLOT(setDimensions(QSize))); gridSelectionDialog_->show(); } void QtChatTabs::storeTabPositions() { // save size QByteArray gridSizeData; QDataStream dataStreamGridSize(&gridSizeData, QIODevice::ReadWrite); dataStreamGridSize << dynamicGrid_->getDimension(); settingsProvider_->storeSetting(SettingConstants::TRELLIS_GRID_SIZE, Q2PSTRING(QString(gridSizeData.toBase64()))); // save positions QByteArray tabPositionsData; QDataStream dataStreamTabPositions(&tabPositionsData, QIODevice::ReadWrite); dynamicGrid_->updateTabPositions(); dataStreamTabPositions << dynamicGrid_->getTabPositions(); settingsProvider_->storeSetting(SettingConstants::TRELLIS_GRID_POSITIONS, Q2PSTRING(QString(tabPositionsData.toBase64()))); } void QtChatTabs::resizeEvent(QResizeEvent*) { emit geometryChanged(); } void QtChatTabs::moveEvent(QMoveEvent*) { emit geometryChanged(); } void QtChatTabs::checkForFirstShow() { if (!isVisible()) { #ifndef Q_OS_MAC showMinimized(); #else /* https://bugreports.qt-project.org/browse/QTBUG-19194 * ^ When the above is fixed we can swap the below for just show(); * WA_ShowWithoutActivating seems to helpfully not work, so... */ QWidget* currentWindow = QApplication::activeWindow(); /* Remember who had focus if we're the current application*/ show(); QCoreApplication::processEvents(); /* Run through the eventloop to clear the show() */ if (currentWindow) { currentWindow->activateWindow(); /* Set focus back */ } #endif } } }