1 /*
2 Copyright © 2014-2019 by The qTox Project Contributors
3
4 This file is part of qTox, a Qt-based graphical interface for Tox.
5
6 qTox is libre software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 qTox is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with qTox. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 #include "widget.h"
21
22 #include <cassert>
23
24 #include <QClipboard>
25 #include <QDebug>
26 #include <QDesktopServices>
27 #include <QDesktopWidget>
28 #include <QMessageBox>
29 #include <QMouseEvent>
30 #include <QPainter>
31 #include <QShortcut>
32 #include <QString>
33 #include <QSvgRenderer>
34 #include <QWindow>
35 #ifdef Q_OS_MAC
36 #include <QMenuBar>
37 #include <QSignalMapper>
38 #include <QWindow>
39 #endif
40
41 #include "circlewidget.h"
42 #include "contentdialog.h"
43 #include "contentlayout.h"
44 #include "friendlistwidget.h"
45 #include "friendwidget.h"
46 #include "groupwidget.h"
47 #include "maskablepixmapwidget.h"
48 #include "splitterrestorer.h"
49 #include "form/groupchatform.h"
50 #include "src/audio/audio.h"
51 #include "src/chatlog/content/filetransferwidget.h"
52 #include "src/core/core.h"
53 #include "src/core/coreav.h"
54 #include "src/core/corefile.h"
55 #include "src/friendlist.h"
56 #include "src/grouplist.h"
57 #include "src/model/chathistory.h"
58 #include "src/model/chatroom/friendchatroom.h"
59 #include "src/model/chatroom/groupchatroom.h"
60 #include "src/model/friend.h"
61 #include "src/model/group.h"
62 #include "src/model/groupinvite.h"
63 #include "src/model/profile/profileinfo.h"
64 #include "src/model/status.h"
65 #include "src/net/updatecheck.h"
66 #include "src/nexus.h"
67 #include "src/persistence/offlinemsgengine.h"
68 #include "src/persistence/profile.h"
69 #include "src/persistence/settings.h"
70 #include "src/platform/timer.h"
71 #include "src/widget/contentdialogmanager.h"
72 #include "src/widget/form/addfriendform.h"
73 #include "src/widget/form/chatform.h"
74 #include "src/widget/form/filesform.h"
75 #include "src/widget/form/groupinviteform.h"
76 #include "src/widget/form/profileform.h"
77 #include "src/widget/form/settingswidget.h"
78 #include "src/widget/gui.h"
79 #include "src/widget/style.h"
80 #include "src/widget/translator.h"
81 #include "tool/removefrienddialog.h"
82
toxActivateEventHandler(const QByteArray &)83 bool toxActivateEventHandler(const QByteArray&)
84 {
85 Widget* widget = Nexus::getDesktopGUI();
86 if (!widget) {
87 return true;
88 }
89
90 qDebug() << "Handling [activate] event from other instance";
91 widget->forceShow();
92
93 return true;
94 }
95
96 namespace {
97
98 /**
99 * @brief Dangerous way to find out if a path is writable.
100 * @param filepath Path to file which should be deleted.
101 * @return True, if file writeable, false otherwise.
102 */
tryRemoveFile(const QString & filepath)103 bool tryRemoveFile(const QString& filepath)
104 {
105 QFile tmp(filepath);
106 bool writable = tmp.open(QIODevice::WriteOnly);
107 tmp.remove();
108 return writable;
109 }
110
acceptFileTransfer(const ToxFile & file,const QString & path)111 void acceptFileTransfer(const ToxFile& file, const QString& path)
112 {
113 QString filepath;
114 int number = 0;
115
116 QString suffix = QFileInfo(file.fileName).completeSuffix();
117 QString base = QFileInfo(file.fileName).baseName();
118
119 do {
120 filepath = QString("%1/%2%3.%4")
121 .arg(path, base,
122 number > 0 ? QString(" (%1)").arg(QString::number(number)) : QString(),
123 suffix);
124 ++number;
125 } while (QFileInfo(filepath).exists());
126
127 // Do not automatically accept the file-transfer if the path is not writable.
128 // The user can still accept it manually.
129 if (tryRemoveFile(filepath)) {
130 CoreFile* coreFile = Core::getInstance()->getCoreFile();
131 coreFile->acceptFileRecvRequest(file.friendId, file.fileNum, filepath);
132 } else {
133 qWarning() << "Cannot write to " << filepath;
134 }
135 }
136 } // namespace
137
138 Widget* Widget::instance{nullptr};
139
Widget(IAudioControl & audio,QWidget * parent)140 Widget::Widget(IAudioControl& audio, QWidget* parent)
141 : QMainWindow(parent)
142 , icon{nullptr}
143 , trayMenu{nullptr}
144 , ui(new Ui::MainWindow)
145 , activeChatroomWidget{nullptr}
146 , eventFlag(false)
147 , eventIcon(false)
148 , audio(audio)
149 , settings(Settings::getInstance())
150 {
151 installEventFilter(this);
152 QString locale = settings.getTranslation();
153 Translator::translate(locale);
154 }
155
init()156 void Widget::init()
157 {
158 ui->setupUi(this);
159
160 QIcon themeIcon = QIcon::fromTheme("qtox");
161 if (!themeIcon.isNull()) {
162 setWindowIcon(themeIcon);
163 }
164
165 timer = new QTimer();
166 timer->start(1000);
167
168 icon_size = 15;
169
170 actionShow = new QAction(this);
171 connect(actionShow, &QAction::triggered, this, &Widget::forceShow);
172
173 // Preparing icons and set their size
174 statusOnline = new QAction(this);
175 statusOnline->setIcon(
176 prepareIcon(Status::getIconPath(Status::Status::Online), icon_size, icon_size));
177 connect(statusOnline, &QAction::triggered, this, &Widget::setStatusOnline);
178
179 statusAway = new QAction(this);
180 statusAway->setIcon(prepareIcon(Status::getIconPath(Status::Status::Away), icon_size, icon_size));
181 connect(statusAway, &QAction::triggered, this, &Widget::setStatusAway);
182
183 statusBusy = new QAction(this);
184 statusBusy->setIcon(prepareIcon(Status::getIconPath(Status::Status::Busy), icon_size, icon_size));
185 connect(statusBusy, &QAction::triggered, this, &Widget::setStatusBusy);
186
187 actionLogout = new QAction(this);
188 actionLogout->setIcon(prepareIcon(":/img/others/logout-icon.svg", icon_size, icon_size));
189
190 actionQuit = new QAction(this);
191 #ifndef Q_OS_OSX
192 actionQuit->setMenuRole(QAction::QuitRole);
193 #endif
194
195 actionQuit->setIcon(
196 prepareIcon(Style::getImagePath("rejectCall/rejectCall.svg"), icon_size, icon_size));
197 connect(actionQuit, &QAction::triggered, qApp, &QApplication::quit);
198
199 layout()->setContentsMargins(0, 0, 0, 0);
200
201 profilePicture = new MaskablePixmapWidget(this, QSize(40, 40), ":/img/avatar_mask.svg");
202 profilePicture->setPixmap(QPixmap(":/img/contact_dark.svg"));
203 profilePicture->setClickable(true);
204 profilePicture->setObjectName("selfAvatar");
205 ui->myProfile->insertWidget(0, profilePicture);
206 ui->myProfile->insertSpacing(1, 7);
207
208 filterMenu = new QMenu(this);
209 filterGroup = new QActionGroup(this);
210 filterDisplayGroup = new QActionGroup(this);
211
212 filterDisplayName = new QAction(this);
213 filterDisplayName->setCheckable(true);
214 filterDisplayName->setChecked(true);
215 filterDisplayGroup->addAction(filterDisplayName);
216 filterMenu->addAction(filterDisplayName);
217 filterDisplayActivity = new QAction(this);
218 filterDisplayActivity->setCheckable(true);
219 filterDisplayGroup->addAction(filterDisplayActivity);
220 filterMenu->addAction(filterDisplayActivity);
221 settings.getFriendSortingMode() == FriendListWidget::SortingMode::Name
222 ? filterDisplayName->setChecked(true)
223 : filterDisplayActivity->setChecked(true);
224 filterMenu->addSeparator();
225
226 filterAllAction = new QAction(this);
227 filterAllAction->setCheckable(true);
228 filterAllAction->setChecked(true);
229 filterGroup->addAction(filterAllAction);
230 filterMenu->addAction(filterAllAction);
231 filterOnlineAction = new QAction(this);
232 filterOnlineAction->setCheckable(true);
233 filterGroup->addAction(filterOnlineAction);
234 filterMenu->addAction(filterOnlineAction);
235 filterOfflineAction = new QAction(this);
236 filterOfflineAction->setCheckable(true);
237 filterGroup->addAction(filterOfflineAction);
238 filterMenu->addAction(filterOfflineAction);
239 filterFriendsAction = new QAction(this);
240 filterFriendsAction->setCheckable(true);
241 filterGroup->addAction(filterFriendsAction);
242 filterMenu->addAction(filterFriendsAction);
243 filterGroupsAction = new QAction(this);
244 filterGroupsAction->setCheckable(true);
245 filterGroup->addAction(filterGroupsAction);
246 filterMenu->addAction(filterGroupsAction);
247
248 ui->searchContactFilterBox->setMenu(filterMenu);
249
250 contactListWidget = new FriendListWidget(this, settings.getGroupchatPosition());
251 connect(contactListWidget, &FriendListWidget::searchCircle, this, &Widget::searchCircle);
252 connect(contactListWidget, &FriendListWidget::connectCircleWidget, this,
253 &Widget::connectCircleWidget);
254 ui->friendList->setWidget(contactListWidget);
255 ui->friendList->setLayoutDirection(Qt::RightToLeft);
256 ui->friendList->setContextMenuPolicy(Qt::CustomContextMenu);
257
258 ui->statusLabel->setEditable(true);
259
260 QMenu* statusButtonMenu = new QMenu(ui->statusButton);
261 statusButtonMenu->addAction(statusOnline);
262 statusButtonMenu->addAction(statusAway);
263 statusButtonMenu->addAction(statusBusy);
264 ui->statusButton->setMenu(statusButtonMenu);
265
266 // disable proportional scaling
267 ui->mainSplitter->setStretchFactor(0, 0);
268 ui->mainSplitter->setStretchFactor(1, 1);
269
270 onStatusSet(Status::Status::Offline);
271
272 // Disable some widgets until we're connected to the DHT
273 ui->statusButton->setEnabled(false);
274
275 Style::setThemeColor(settings.getThemeColor());
276
277 filesForm = new FilesForm();
278 addFriendForm = new AddFriendForm;
279 groupInviteForm = new GroupInviteForm;
280 #if UPDATE_CHECK_ENABLED
281 updateCheck = std::unique_ptr<UpdateCheck>(new UpdateCheck(settings));
282 connect(updateCheck.get(), &UpdateCheck::updateAvailable, this, &Widget::onUpdateAvailable);
283 #endif
284 settingsWidget = new SettingsWidget(updateCheck.get(), audio, this);
285 #if UPDATE_CHECK_ENABLED
286 updateCheck->checkForUpdate();
287 #endif
288
289 core = Nexus::getCore();
290 CoreFile* coreFile = core->getCoreFile();
291 Profile* profile = Nexus::getProfile();
292 profileInfo = new ProfileInfo(core, profile);
293 profileForm = new ProfileForm(profileInfo);
294
295 // connect logout tray menu action
296 connect(actionLogout, &QAction::triggered, profileForm, &ProfileForm::onLogoutClicked);
297
298 connect(profile, &Profile::selfAvatarChanged, profileForm, &ProfileForm::onSelfAvatarLoaded);
299
300 connect(coreFile, &CoreFile::fileReceiveRequested, this, &Widget::onFileReceiveRequested);
301 connect(coreFile, &CoreFile::fileDownloadFinished, filesForm, &FilesForm::onFileDownloadComplete);
302 connect(coreFile, &CoreFile::fileUploadFinished, filesForm, &FilesForm::onFileUploadComplete);
303 connect(ui->addButton, &QPushButton::clicked, this, &Widget::onAddClicked);
304 connect(ui->groupButton, &QPushButton::clicked, this, &Widget::onGroupClicked);
305 connect(ui->transferButton, &QPushButton::clicked, this, &Widget::onTransferClicked);
306 connect(ui->settingsButton, &QPushButton::clicked, this, &Widget::onShowSettings);
307 connect(profilePicture, &MaskablePixmapWidget::clicked, this, &Widget::showProfile);
308 connect(ui->nameLabel, &CroppingLabel::clicked, this, &Widget::showProfile);
309 connect(ui->statusLabel, &CroppingLabel::editFinished, this, &Widget::onStatusMessageChanged);
310 connect(ui->mainSplitter, &QSplitter::splitterMoved, this, &Widget::onSplitterMoved);
311 connect(addFriendForm, &AddFriendForm::friendRequested, this, &Widget::friendRequested);
312 connect(groupInviteForm, &GroupInviteForm::groupCreate, core, &Core::createGroup);
313 connect(timer, &QTimer::timeout, this, &Widget::onUserAwayCheck);
314 connect(timer, &QTimer::timeout, this, &Widget::onEventIconTick);
315 connect(timer, &QTimer::timeout, this, &Widget::onTryCreateTrayIcon);
316 connect(ui->searchContactText, &QLineEdit::textChanged, this, &Widget::searchContacts);
317 connect(filterGroup, &QActionGroup::triggered, this, &Widget::searchContacts);
318 connect(filterDisplayGroup, &QActionGroup::triggered, this, &Widget::changeDisplayMode);
319 connect(ui->friendList, &QWidget::customContextMenuRequested, this, &Widget::friendListContextMenu);
320
321 connect(coreFile, &CoreFile::fileSendStarted, this, &Widget::dispatchFile);
322 connect(coreFile, &CoreFile::fileReceiveRequested, this, &Widget::dispatchFile);
323 connect(coreFile, &CoreFile::fileTransferAccepted, this, &Widget::dispatchFile);
324 connect(coreFile, &CoreFile::fileTransferCancelled, this, &Widget::dispatchFile);
325 connect(coreFile, &CoreFile::fileTransferFinished, this, &Widget::dispatchFile);
326 connect(coreFile, &CoreFile::fileTransferPaused, this, &Widget::dispatchFile);
327 connect(coreFile, &CoreFile::fileTransferInfo, this, &Widget::dispatchFile);
328 connect(coreFile, &CoreFile::fileTransferRemotePausedUnpaused, this, &Widget::dispatchFileWithBool);
329 connect(coreFile, &CoreFile::fileTransferBrokenUnbroken, this, &Widget::dispatchFileWithBool);
330 connect(coreFile, &CoreFile::fileSendFailed, this, &Widget::dispatchFileSendFailed);
331 // NOTE: We intentionally do not connect the fileUploadFinished and fileDownloadFinished signals
332 // because they are duplicates of fileTransferFinished NOTE: We don't hook up the
333 // fileNameChanged signal since it is only emitted before a fileReceiveRequest. We get the
334 // initial request with the sanitized name so there is no work for us to do
335
336 // keyboard shortcuts
337 new QShortcut(Qt::CTRL + Qt::Key_Q, this, SLOT(close()));
338 new QShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_Tab, this, SLOT(previousContact()));
339 new QShortcut(Qt::CTRL + Qt::Key_Tab, this, SLOT(nextContact()));
340 new QShortcut(Qt::CTRL + Qt::Key_PageUp, this, SLOT(previousContact()));
341 new QShortcut(Qt::CTRL + Qt::Key_PageDown, this, SLOT(nextContact()));
342 new QShortcut(Qt::Key_F11, this, SLOT(toggleFullscreen()));
343
344 #ifdef Q_OS_MAC
345 QMenuBar* globalMenu = Nexus::getInstance().globalMenuBar;
346 QAction* windowMenu = Nexus::getInstance().windowMenu->menuAction();
347 QAction* viewMenu = Nexus::getInstance().viewMenu->menuAction();
348 QAction* frontAction = Nexus::getInstance().frontAction;
349
350 fileMenu = globalMenu->insertMenu(viewMenu, new QMenu(this));
351
352 editProfileAction = fileMenu->menu()->addAction(QString());
353 connect(editProfileAction, &QAction::triggered, this, &Widget::showProfile);
354
355 changeStatusMenu = fileMenu->menu()->addMenu(QString());
356 fileMenu->menu()->addAction(changeStatusMenu->menuAction());
357 changeStatusMenu->addAction(statusOnline);
358 changeStatusMenu->addSeparator();
359 changeStatusMenu->addAction(statusAway);
360 changeStatusMenu->addAction(statusBusy);
361
362 fileMenu->menu()->addSeparator();
363 logoutAction = fileMenu->menu()->addAction(QString());
364 connect(logoutAction, &QAction::triggered, [this]() { Nexus::getInstance().showLogin(); });
365
366 editMenu = globalMenu->insertMenu(viewMenu, new QMenu(this));
367 editMenu->menu()->addSeparator();
368
369 viewMenu->menu()->insertMenu(Nexus::getInstance().fullscreenAction, filterMenu);
370
371 viewMenu->menu()->insertSeparator(Nexus::getInstance().fullscreenAction);
372
373 contactMenu = globalMenu->insertMenu(windowMenu, new QMenu(this));
374
375 addContactAction = contactMenu->menu()->addAction(QString());
376 connect(addContactAction, &QAction::triggered, this, &Widget::onAddClicked);
377
378 nextConversationAction = new QAction(this);
379 Nexus::getInstance().windowMenu->insertAction(frontAction, nextConversationAction);
380 nextConversationAction->setShortcut(QKeySequence::SelectNextPage);
381 connect(nextConversationAction, &QAction::triggered, [this]() {
382 if (ContentDialogManager::getInstance()->current() == QApplication::activeWindow())
383 ContentDialogManager::getInstance()->current()->cycleContacts(true);
384 else if (QApplication::activeWindow() == this)
385 cycleContacts(true);
386 });
387
388 previousConversationAction = new QAction(this);
389 Nexus::getInstance().windowMenu->insertAction(frontAction, previousConversationAction);
390 previousConversationAction->setShortcut(QKeySequence::SelectPreviousPage);
391 connect(previousConversationAction, &QAction::triggered, [this] {
392 if (ContentDialogManager::getInstance()->current() == QApplication::activeWindow())
393 ContentDialogManager::getInstance()->current()->cycleContacts(false);
394 else if (QApplication::activeWindow() == this)
395 cycleContacts(false);
396 });
397
398 windowMenu->menu()->insertSeparator(frontAction);
399
400 QAction* preferencesAction = viewMenu->menu()->addAction(QString());
401 preferencesAction->setMenuRole(QAction::PreferencesRole);
402 connect(preferencesAction, &QAction::triggered, this, &Widget::onShowSettings);
403
404 QAction* aboutAction = viewMenu->menu()->addAction(QString());
405 aboutAction->setMenuRole(QAction::AboutRole);
406 connect(aboutAction, &QAction::triggered, [this]() {
407 onShowSettings();
408 settingsWidget->showAbout();
409 });
410
411 QMenu* dockChangeStatusMenu = new QMenu(tr("Status"), this);
412 dockChangeStatusMenu->addAction(statusOnline);
413 statusOnline->setIconVisibleInMenu(true);
414 dockChangeStatusMenu->addSeparator();
415 dockChangeStatusMenu->addAction(statusAway);
416 dockChangeStatusMenu->addAction(statusBusy);
417 Nexus::getInstance().dockMenu->addAction(dockChangeStatusMenu->menuAction());
418
419 connect(this, &Widget::windowStateChanged, &Nexus::getInstance(), &Nexus::onWindowStateChanged);
420 #endif
421
422 contentLayout = nullptr;
423 onSeparateWindowChanged(settings.getSeparateWindow(), false);
424
425 ui->addButton->setCheckable(true);
426 ui->groupButton->setCheckable(true);
427 ui->transferButton->setCheckable(true);
428 ui->settingsButton->setCheckable(true);
429
430 if (contentLayout) {
431 onAddClicked();
432 }
433
434 // restore window state
435 restoreGeometry(settings.getWindowGeometry());
436 restoreState(settings.getWindowState());
437 SplitterRestorer restorer(ui->mainSplitter);
438 restorer.restore(settings.getSplitterState(), size());
439
440 friendRequestsButton = nullptr;
441 groupInvitesButton = nullptr;
442 unreadGroupInvites = 0;
443
444 connect(addFriendForm, &AddFriendForm::friendRequested, this, &Widget::friendRequestsUpdate);
445 connect(addFriendForm, &AddFriendForm::friendRequestsSeen, this, &Widget::friendRequestsUpdate);
446 connect(addFriendForm, &AddFriendForm::friendRequestAccepted, this, &Widget::friendRequestAccepted);
447 connect(groupInviteForm, &GroupInviteForm::groupInvitesSeen, this, &Widget::groupInvitesClear);
448 connect(groupInviteForm, &GroupInviteForm::groupInviteAccepted, this,
449 &Widget::onGroupInviteAccepted);
450
451 // settings
452 connect(&settings, &Settings::showSystemTrayChanged, this, &Widget::onSetShowSystemTray);
453 connect(&settings, &Settings::separateWindowChanged, this, &Widget::onSeparateWindowClicked);
454 connect(&settings, &Settings::compactLayoutChanged, contactListWidget,
455 &FriendListWidget::onCompactChanged);
456 connect(&settings, &Settings::groupchatPositionChanged, contactListWidget,
457 &FriendListWidget::onGroupchatPositionChanged);
458
459 reloadTheme();
460 updateIcons();
461 retranslateUi();
462 Translator::registerHandler(std::bind(&Widget::retranslateUi, this), this);
463
464 if (!settings.getShowSystemTray()) {
465 show();
466 }
467
468 #ifdef Q_OS_MAC
469 Nexus::getInstance().updateWindows();
470 #endif
471 }
472
eventFilter(QObject * obj,QEvent * event)473 bool Widget::eventFilter(QObject* obj, QEvent* event)
474 {
475 QWindowStateChangeEvent* ce = nullptr;
476 Qt::WindowStates state = windowState();
477
478 switch (event->type()) {
479 case QEvent::Close:
480 // It's needed if user enable `Close to tray`
481 wasMaximized = state.testFlag(Qt::WindowMaximized);
482 break;
483
484 case QEvent::WindowStateChange:
485 ce = static_cast<QWindowStateChangeEvent*>(event);
486 if (state.testFlag(Qt::WindowMinimized) && obj) {
487 wasMaximized = ce->oldState().testFlag(Qt::WindowMaximized);
488 }
489
490 #ifdef Q_OS_MAC
491 emit windowStateChanged(windowState());
492 #endif
493 break;
494 default:
495 break;
496 }
497
498 return false;
499 }
500
updateIcons()501 void Widget::updateIcons()
502 {
503 if (!icon) {
504 return;
505 }
506
507 const QString assetSuffix = Status::getAssetSuffix(static_cast<Status::Status>(
508 ui->statusButton->property("status").toInt()))
509 + (eventIcon ? "_event" : "");
510
511 // Some builds of Qt appear to have a bug in icon loading:
512 // QIcon::hasThemeIcon is sometimes unaware that the icon returned
513 // from QIcon::fromTheme was a fallback icon, causing hasThemeIcon to
514 // incorrectly return true.
515 //
516 // In qTox this leads to the tray and window icons using the static qTox logo
517 // icon instead of an icon based on the current presence status.
518 //
519 // This workaround checks for an icon that definitely does not exist to
520 // determine if hasThemeIcon can be trusted.
521 //
522 // On systems with the Qt bug, this workaround will always use our included
523 // icons but user themes will be unable to override them.
524 static bool checkedHasThemeIcon = false;
525 static bool hasThemeIconBug = false;
526
527 if (!checkedHasThemeIcon) {
528 hasThemeIconBug = QIcon::hasThemeIcon("qtox-asjkdfhawjkeghdfjgh");
529 checkedHasThemeIcon = true;
530
531 if (hasThemeIconBug) {
532 qDebug()
533 << "Detected buggy QIcon::hasThemeIcon. Icon overrides from theme will be ignored.";
534 }
535 }
536
537 QIcon ico;
538 if (!hasThemeIconBug && QIcon::hasThemeIcon("qtox-" + assetSuffix)) {
539 ico = QIcon::fromTheme("qtox-" + assetSuffix);
540 } else {
541 QString color = settings.getLightTrayIcon() ? "light" : "dark";
542 QString path = ":/img/taskbar/" + color + "/taskbar_" + assetSuffix + ".svg";
543 QSvgRenderer renderer(path);
544
545 // Prepare a QImage with desired characteritisc
546 QImage image = QImage(250, 250, QImage::Format_ARGB32);
547 image.fill(Qt::transparent);
548 QPainter painter(&image);
549 renderer.render(&painter);
550 ico = QIcon(QPixmap::fromImage(image));
551 }
552
553 setWindowIcon(ico);
554 if (icon) {
555 icon->setIcon(ico);
556 }
557 }
558
~Widget()559 Widget::~Widget()
560 {
561 QWidgetList windowList = QApplication::topLevelWidgets();
562
563 for (QWidget* window : windowList) {
564 if (window != this) {
565 window->close();
566 }
567 }
568
569 Translator::unregister(this);
570 if (icon) {
571 icon->hide();
572 }
573
574 for (Group* g : GroupList::getAllGroups()) {
575 removeGroup(g, true);
576 }
577
578 for (Friend* f : FriendList::getAllFriends()) {
579 removeFriend(f, true);
580 }
581
582 for (auto form : chatForms) {
583 delete form;
584 }
585
586 delete profileForm;
587 delete profileInfo;
588 delete addFriendForm;
589 delete groupInviteForm;
590 delete filesForm;
591 delete timer;
592 delete contentLayout;
593 delete settingsWidget;
594
595 FriendList::clear();
596 GroupList::clear();
597 delete trayMenu;
598 delete ui;
599 instance = nullptr;
600 }
601
602 /**
603 * @brief Switches to the About settings page.
604 */
showUpdateDownloadProgress()605 void Widget::showUpdateDownloadProgress()
606 {
607 onShowSettings();
608 settingsWidget->showAbout();
609 }
610
moveEvent(QMoveEvent * event)611 void Widget::moveEvent(QMoveEvent* event)
612 {
613 if (event->type() == QEvent::Move) {
614 saveWindowGeometry();
615 saveSplitterGeometry();
616 }
617
618 QWidget::moveEvent(event);
619 }
620
closeEvent(QCloseEvent * event)621 void Widget::closeEvent(QCloseEvent* event)
622 {
623 if (settings.getShowSystemTray() && settings.getCloseToTray()) {
624 QWidget::closeEvent(event);
625 } else {
626 if (autoAwayActive) {
627 emit statusSet(Status::Status::Online);
628 autoAwayActive = false;
629 }
630 saveWindowGeometry();
631 saveSplitterGeometry();
632 QWidget::closeEvent(event);
633 qApp->quit();
634 }
635 }
636
changeEvent(QEvent * event)637 void Widget::changeEvent(QEvent* event)
638 {
639 if (event->type() == QEvent::WindowStateChange) {
640 if (isMinimized() && settings.getShowSystemTray() && settings.getMinimizeToTray()) {
641 this->hide();
642 }
643 }
644 }
645
resizeEvent(QResizeEvent * event)646 void Widget::resizeEvent(QResizeEvent* event)
647 {
648 saveWindowGeometry();
649 QMainWindow::resizeEvent(event);
650 }
651
getUsername()652 QString Widget::getUsername()
653 {
654 return core->getUsername();
655 }
656
onSelfAvatarLoaded(const QPixmap & pic)657 void Widget::onSelfAvatarLoaded(const QPixmap& pic)
658 {
659 profilePicture->setPixmap(pic);
660 }
661
onCoreChanged(Core & core)662 void Widget::onCoreChanged(Core& core)
663 {
664
665 connect(&core, &Core::connected, this, &Widget::onConnected);
666 connect(&core, &Core::disconnected, this, &Widget::onDisconnected);
667 connect(&core, &Core::statusSet, this, &Widget::onStatusSet);
668 connect(&core, &Core::usernameSet, this, &Widget::setUsername);
669 connect(&core, &Core::statusMessageSet, this, &Widget::setStatusMessage);
670 connect(&core, &Core::friendAdded, this, &Widget::addFriend);
671 connect(&core, &Core::failedToAddFriend, this, &Widget::addFriendFailed);
672 connect(&core, &Core::friendUsernameChanged, this, &Widget::onFriendUsernameChanged);
673 connect(&core, &Core::friendStatusChanged, this, &Widget::onFriendStatusChanged);
674 connect(&core, &Core::friendStatusMessageChanged, this, &Widget::onFriendStatusMessageChanged);
675 connect(&core, &Core::friendRequestReceived, this, &Widget::onFriendRequestReceived);
676 connect(&core, &Core::friendMessageReceived, this, &Widget::onFriendMessageReceived);
677 connect(&core, &Core::receiptRecieved, this, &Widget::onReceiptReceived);
678 connect(&core, &Core::groupInviteReceived, this, &Widget::onGroupInviteReceived);
679 connect(&core, &Core::groupMessageReceived, this, &Widget::onGroupMessageReceived);
680 connect(&core, &Core::groupPeerlistChanged, this, &Widget::onGroupPeerlistChanged);
681 connect(&core, &Core::groupPeerNameChanged, this, &Widget::onGroupPeerNameChanged);
682 connect(&core, &Core::groupTitleChanged, this, &Widget::onGroupTitleChanged);
683 connect(&core, &Core::groupPeerAudioPlaying, this, &Widget::onGroupPeerAudioPlaying);
684 connect(&core, &Core::emptyGroupCreated, this, &Widget::onEmptyGroupCreated);
685 connect(&core, &Core::groupJoined, this, &Widget::onGroupJoined);
686 connect(&core, &Core::friendTypingChanged, this, &Widget::onFriendTypingChanged);
687 connect(&core, &Core::groupSentFailed, this, &Widget::onGroupSendFailed);
688 connect(&core, &Core::usernameSet, this, &Widget::refreshPeerListsLocal);
689 connect(this, &Widget::statusSet, &core, &Core::setStatus);
690 connect(this, &Widget::friendRequested, &core, &Core::requestFriendship);
691 connect(this, &Widget::friendRequestAccepted, &core, &Core::acceptFriendRequest);
692 connect(this, &Widget::changeGroupTitle, &core, &Core::changeGroupTitle);
693
694 sharedMessageProcessorParams.setPublicKey(core.getSelfPublicKey().toString());
695 }
696
onConnected()697 void Widget::onConnected()
698 {
699 ui->statusButton->setEnabled(true);
700 emit core->statusSet(core->getStatus());
701 }
702
onDisconnected()703 void Widget::onDisconnected()
704 {
705 ui->statusButton->setEnabled(false);
706 emit core->statusSet(Status::Status::Offline);
707 }
708
onFailedToStartCore()709 void Widget::onFailedToStartCore()
710 {
711 QMessageBox critical(this);
712 critical.setText(tr(
713 "toxcore failed to start, the application will terminate after you close this message."));
714 critical.setIcon(QMessageBox::Critical);
715 critical.exec();
716 qApp->exit(EXIT_FAILURE);
717 }
718
onBadProxyCore()719 void Widget::onBadProxyCore()
720 {
721 settings.setProxyType(Settings::ProxyType::ptNone);
722 QMessageBox critical(this);
723 critical.setText(tr("toxcore failed to start with your proxy settings. "
724 "qTox cannot run; please modify your "
725 "settings and restart.",
726 "popup text"));
727 critical.setIcon(QMessageBox::Critical);
728 critical.exec();
729 onShowSettings();
730 }
731
onStatusSet(Status::Status status)732 void Widget::onStatusSet(Status::Status status)
733 {
734 ui->statusButton->setProperty("status", static_cast<int>(status));
735 ui->statusButton->setIcon(prepareIcon(getIconPath(status), icon_size, icon_size));
736 updateIcons();
737 }
738
onSeparateWindowClicked(bool separate)739 void Widget::onSeparateWindowClicked(bool separate)
740 {
741 onSeparateWindowChanged(separate, true);
742 }
743
onSeparateWindowChanged(bool separate,bool clicked)744 void Widget::onSeparateWindowChanged(bool separate, bool clicked)
745 {
746 if (!separate) {
747 QWindowList windowList = QGuiApplication::topLevelWindows();
748
749 for (QWindow* window : windowList) {
750 if (window->objectName() == "detachedWindow") {
751 window->close();
752 }
753 }
754
755 QWidget* contentWidget = new QWidget(this);
756 contentWidget->setObjectName("contentWidget");
757
758 contentLayout = new ContentLayout(contentWidget);
759 ui->mainSplitter->addWidget(contentWidget);
760
761 setMinimumWidth(775);
762
763 SplitterRestorer restorer(ui->mainSplitter);
764 restorer.restore(settings.getSplitterState(), size());
765
766 onShowSettings();
767 } else {
768 int width = ui->friendList->size().width();
769 QSize size;
770 QPoint pos;
771
772 if (contentLayout) {
773 pos = mapToGlobal(ui->mainSplitter->widget(1)->pos());
774 size = ui->mainSplitter->widget(1)->size();
775 }
776
777 if (contentLayout) {
778 contentLayout->clear();
779 contentLayout->parentWidget()->setParent(nullptr); // Remove from splitter.
780 contentLayout->parentWidget()->hide();
781 contentLayout->parentWidget()->deleteLater();
782 contentLayout->deleteLater();
783 contentLayout = nullptr;
784 }
785
786 setMinimumWidth(ui->tooliconsZone->sizeHint().width());
787
788 if (clicked) {
789 showNormal();
790 resize(width, height());
791
792 if (settingsWidget) {
793 ContentLayout* contentLayout = createContentDialog((DialogType::SettingDialog));
794 contentLayout->parentWidget()->resize(size);
795 contentLayout->parentWidget()->move(pos);
796 settingsWidget->show(contentLayout);
797 setActiveToolMenuButton(ActiveToolMenuButton::None);
798 }
799 }
800
801 setWindowTitle(QString());
802 setActiveToolMenuButton(ActiveToolMenuButton::None);
803 }
804 }
805
setWindowTitle(const QString & title)806 void Widget::setWindowTitle(const QString& title)
807 {
808 if (title.isEmpty()) {
809 QMainWindow::setWindowTitle(QApplication::applicationName());
810 } else {
811 QString tmp = title;
812 /// <[^>]*> Regexp to remove HTML tags, in case someone used them in title
813 QMainWindow::setWindowTitle(QApplication::applicationName() + QStringLiteral(" - ")
814 + tmp.remove(QRegExp("<[^>]*>")));
815 }
816 }
817
forceShow()818 void Widget::forceShow()
819 {
820 hide(); // Workaround to force minimized window to be restored
821 show();
822 activateWindow();
823 }
824
onAddClicked()825 void Widget::onAddClicked()
826 {
827 if (settings.getSeparateWindow()) {
828 if (!addFriendForm->isShown()) {
829 addFriendForm->show(createContentDialog(DialogType::AddDialog));
830 }
831
832 setActiveToolMenuButton(ActiveToolMenuButton::None);
833 } else {
834 hideMainForms(nullptr);
835 addFriendForm->show(contentLayout);
836 setWindowTitle(fromDialogType(DialogType::AddDialog));
837 setActiveToolMenuButton(ActiveToolMenuButton::AddButton);
838 }
839 }
840
onGroupClicked()841 void Widget::onGroupClicked()
842 {
843 if (settings.getSeparateWindow()) {
844 if (!groupInviteForm->isShown()) {
845 groupInviteForm->show(createContentDialog(DialogType::GroupDialog));
846 }
847
848 setActiveToolMenuButton(ActiveToolMenuButton::None);
849 } else {
850 hideMainForms(nullptr);
851 groupInviteForm->show(contentLayout);
852 setWindowTitle(fromDialogType(DialogType::GroupDialog));
853 setActiveToolMenuButton(ActiveToolMenuButton::GroupButton);
854 }
855 }
856
onTransferClicked()857 void Widget::onTransferClicked()
858 {
859 if (settings.getSeparateWindow()) {
860 if (!filesForm->isShown()) {
861 filesForm->show(createContentDialog(DialogType::TransferDialog));
862 }
863
864 setActiveToolMenuButton(ActiveToolMenuButton::None);
865 } else {
866 hideMainForms(nullptr);
867 filesForm->show(contentLayout);
868 setWindowTitle(fromDialogType(DialogType::TransferDialog));
869 setActiveToolMenuButton(ActiveToolMenuButton::TransferButton);
870 }
871 }
872
confirmExecutableOpen(const QFileInfo & file)873 void Widget::confirmExecutableOpen(const QFileInfo& file)
874 {
875 static const QStringList dangerousExtensions = {"app", "bat", "com", "cpl", "dmg",
876 "exe", "hta", "jar", "js", "jse",
877 "lnk", "msc", "msh", "msh1", "msh1xml",
878 "msh2", "msh2xml", "mshxml", "msi", "msp",
879 "pif", "ps1", "ps1xml", "ps2", "ps2xml",
880 "psc1", "psc2", "py", "reg", "scf",
881 "sh", "src", "vb", "vbe", "vbs",
882 "ws", "wsc", "wsf", "wsh"};
883
884 if (dangerousExtensions.contains(file.suffix())) {
885 bool answer = GUI::askQuestion(tr("Executable file", "popup title"),
886 tr("You have asked qTox to open an executable file. "
887 "Executable files can potentially damage your computer. "
888 "Are you sure want to open this file?",
889 "popup text"),
890 false, true);
891 if (!answer) {
892 return;
893 }
894
895 // The user wants to run this file, so make it executable and run it
896 QFile(file.filePath())
897 .setPermissions(file.permissions() | QFile::ExeOwner | QFile::ExeUser | QFile::ExeGroup
898 | QFile::ExeOther);
899 }
900
901 QDesktopServices::openUrl(QUrl::fromLocalFile(file.filePath()));
902 }
903
onIconClick(QSystemTrayIcon::ActivationReason reason)904 void Widget::onIconClick(QSystemTrayIcon::ActivationReason reason)
905 {
906 if (reason == QSystemTrayIcon::Trigger) {
907 if (isHidden() || isMinimized()) {
908 if (wasMaximized) {
909 showMaximized();
910 } else {
911 showNormal();
912 }
913
914 activateWindow();
915 } else if (!isActiveWindow()) {
916 activateWindow();
917 } else {
918 wasMaximized = isMaximized();
919 hide();
920 }
921 } else if (reason == QSystemTrayIcon::Unknown) {
922 if (isHidden()) {
923 forceShow();
924 }
925 }
926 }
927
onShowSettings()928 void Widget::onShowSettings()
929 {
930 if (settings.getSeparateWindow()) {
931 if (!settingsWidget->isShown()) {
932 settingsWidget->show(createContentDialog(DialogType::SettingDialog));
933 }
934
935 setActiveToolMenuButton(ActiveToolMenuButton::None);
936 } else {
937 hideMainForms(nullptr);
938 settingsWidget->show(contentLayout);
939 setWindowTitle(fromDialogType(DialogType::SettingDialog));
940 setActiveToolMenuButton(ActiveToolMenuButton::SettingButton);
941 }
942 }
943
showProfile()944 void Widget::showProfile() // onAvatarClicked, onUsernameClicked
945 {
946 if (settings.getSeparateWindow()) {
947 if (!profileForm->isShown()) {
948 profileForm->show(createContentDialog(DialogType::ProfileDialog));
949 }
950
951 setActiveToolMenuButton(ActiveToolMenuButton::None);
952 } else {
953 hideMainForms(nullptr);
954 profileForm->show(contentLayout);
955 setWindowTitle(fromDialogType(DialogType::ProfileDialog));
956 setActiveToolMenuButton(ActiveToolMenuButton::None);
957 }
958 }
959
hideMainForms(GenericChatroomWidget * chatroomWidget)960 void Widget::hideMainForms(GenericChatroomWidget* chatroomWidget)
961 {
962 setActiveToolMenuButton(ActiveToolMenuButton::None);
963
964 if (contentLayout != nullptr) {
965 contentLayout->clear();
966 }
967
968 if (activeChatroomWidget != nullptr) {
969 activeChatroomWidget->setAsInactiveChatroom();
970 }
971
972 activeChatroomWidget = chatroomWidget;
973 }
974
setUsername(const QString & username)975 void Widget::setUsername(const QString& username)
976 {
977 if (username.isEmpty()) {
978 ui->nameLabel->setText(tr("Your name"));
979 ui->nameLabel->setToolTip(tr("Your name"));
980 } else {
981 ui->nameLabel->setText(username);
982 ui->nameLabel->setToolTip(
983 Qt::convertFromPlainText(username, Qt::WhiteSpaceNormal)); // for overlength names
984 }
985
986 sharedMessageProcessorParams.onUserNameSet(username);
987 }
988
onStatusMessageChanged(const QString & newStatusMessage)989 void Widget::onStatusMessageChanged(const QString& newStatusMessage)
990 {
991 // Keep old status message until Core tells us to set it.
992 core->setStatusMessage(newStatusMessage);
993 }
994
setStatusMessage(const QString & statusMessage)995 void Widget::setStatusMessage(const QString& statusMessage)
996 {
997 ui->statusLabel->setText(statusMessage);
998 // escape HTML from tooltips and preserve newlines
999 // TODO: move newspace preservance to a generic function
1000 ui->statusLabel->setToolTip("<p style='white-space:pre'>" + statusMessage.toHtmlEscaped() + "</p>");
1001 }
1002
1003 /**
1004 * @brief Plays a sound via the audioNotification AudioSink
1005 * @param sound Sound to play
1006 * @param loop if true, loop the sound until onStopNotification() is called
1007 */
playNotificationSound(IAudioSink::Sound sound,bool loop)1008 void Widget::playNotificationSound(IAudioSink::Sound sound, bool loop)
1009 {
1010 if (!settings.getAudioOutDevEnabled()) {
1011 // don't try to play sounds if audio is disabled
1012 return;
1013 }
1014
1015 if (audioNotification == nullptr) {
1016 audioNotification = std::unique_ptr<IAudioSink>(audio.makeSink());
1017 if (audioNotification == nullptr) {
1018 qDebug() << "Failed to allocate AudioSink";
1019 return;
1020 }
1021 }
1022
1023 audioNotification->connectTo_finishedPlaying(this, [this](){ cleanupNotificationSound(); });
1024
1025 audioNotification->playMono16Sound(sound);
1026
1027 if (loop) {
1028 audioNotification->startLoop();
1029 }
1030 }
1031
cleanupNotificationSound()1032 void Widget::cleanupNotificationSound()
1033 {
1034 audioNotification.reset();
1035 }
1036
incomingNotification(uint32_t friendnumber)1037 void Widget::incomingNotification(uint32_t friendnumber)
1038 {
1039 const auto& friendId = FriendList::id2Key(friendnumber);
1040 newFriendMessageAlert(friendId, {}, false);
1041
1042 // loop until call answered or rejected
1043 playNotificationSound(IAudioSink::Sound::IncomingCall, true);
1044 }
1045
outgoingNotification()1046 void Widget::outgoingNotification()
1047 {
1048 // loop until call answered or rejected
1049 playNotificationSound(IAudioSink::Sound::OutgoingCall, true);
1050 }
1051
onCallEnd()1052 void Widget::onCallEnd()
1053 {
1054 playNotificationSound(IAudioSink::Sound::CallEnd);
1055 }
1056
1057 /**
1058 * @brief Widget::onStopNotification Stop the notification sound.
1059 */
onStopNotification()1060 void Widget::onStopNotification()
1061 {
1062 audioNotification.reset();
1063 }
1064
1065 /**
1066 * @brief Dispatches file to the appropriate chatlog and accepts the transfer if necessary
1067 */
dispatchFile(ToxFile file)1068 void Widget::dispatchFile(ToxFile file)
1069 {
1070 const auto& friendId = FriendList::id2Key(file.friendId);
1071 Friend* f = FriendList::findFriend(friendId);
1072 if (!f) {
1073 return;
1074 }
1075
1076 auto pk = f->getPublicKey();
1077
1078 if (file.status == ToxFile::INITIALIZING && file.direction == ToxFile::RECEIVING) {
1079 auto sender =
1080 (file.direction == ToxFile::SENDING) ? Core::getInstance()->getSelfPublicKey() : pk;
1081
1082 const Settings& settings = Settings::getInstance();
1083 QString autoAcceptDir = settings.getAutoAcceptDir(f->getPublicKey());
1084
1085 if (autoAcceptDir.isEmpty() && settings.getAutoSaveEnabled()) {
1086 autoAcceptDir = settings.getGlobalAutoAcceptDir();
1087 }
1088
1089 auto maxAutoAcceptSize = settings.getMaxAutoAcceptSize();
1090 bool autoAcceptSizeCheckPassed = maxAutoAcceptSize == 0 || maxAutoAcceptSize >= file.filesize;
1091
1092 if (!autoAcceptDir.isEmpty() && autoAcceptSizeCheckPassed) {
1093 acceptFileTransfer(file, autoAcceptDir);
1094 }
1095 }
1096
1097 const auto senderPk = (file.direction == ToxFile::SENDING) ? core->getSelfPublicKey() : pk;
1098 friendChatLogs[pk]->onFileUpdated(senderPk, file);
1099 }
1100
dispatchFileWithBool(ToxFile file,bool)1101 void Widget::dispatchFileWithBool(ToxFile file, bool)
1102 {
1103 dispatchFile(file);
1104 }
1105
dispatchFileSendFailed(uint32_t friendId,const QString & fileName)1106 void Widget::dispatchFileSendFailed(uint32_t friendId, const QString& fileName)
1107 {
1108 const auto& friendPk = FriendList::id2Key(friendId);
1109
1110 auto chatForm = chatForms.find(friendPk);
1111 if (chatForm == chatForms.end()) {
1112 return;
1113 }
1114
1115 chatForm.value()->addSystemInfoMessage(tr("Failed to send file \"%1\"").arg(fileName),
1116 ChatMessage::ERROR, QDateTime::currentDateTime());
1117 }
1118
onRejectCall(uint32_t friendId)1119 void Widget::onRejectCall(uint32_t friendId)
1120 {
1121 CoreAV* const av = core->getAv();
1122 av->cancelCall(friendId);
1123 }
1124
addFriend(uint32_t friendId,const ToxPk & friendPk)1125 void Widget::addFriend(uint32_t friendId, const ToxPk& friendPk)
1126 {
1127 settings.updateFriendAddress(friendPk.toString());
1128
1129 Friend* newfriend = FriendList::addFriend(friendId, friendPk);
1130 auto dialogManager = ContentDialogManager::getInstance();
1131 auto rawChatroom = new FriendChatroom(newfriend, dialogManager);
1132 std::shared_ptr<FriendChatroom> chatroom(rawChatroom);
1133 const auto compact = settings.getCompactLayout();
1134 auto widget = new FriendWidget(chatroom, compact);
1135 connectFriendWidget(*widget);
1136 auto history = Nexus::getProfile()->getHistory();
1137
1138 auto messageProcessor = MessageProcessor(sharedMessageProcessorParams);
1139 auto friendMessageDispatcher =
1140 std::make_shared<FriendMessageDispatcher>(*newfriend, std::move(messageProcessor), *core);
1141
1142 // Note: We do not have to connect the message dispatcher signals since
1143 // ChatHistory hooks them up in a very specific order
1144 auto chatHistory =
1145 std::make_shared<ChatHistory>(*newfriend, history, *core, Settings::getInstance(),
1146 *friendMessageDispatcher);
1147 auto friendForm = new ChatForm(newfriend, *chatHistory, *friendMessageDispatcher);
1148 connect(friendForm, &ChatForm::updateFriendActivity, this, &Widget::updateFriendActivity);
1149
1150 friendMessageDispatchers[friendPk] = friendMessageDispatcher;
1151 friendChatLogs[friendPk] = chatHistory;
1152 friendChatrooms[friendPk] = chatroom;
1153 friendWidgets[friendPk] = widget;
1154 chatForms[friendPk] = friendForm;
1155
1156 const auto activityTime = settings.getFriendActivity(friendPk);
1157 const auto chatTime = friendForm->getLatestTime();
1158 if (chatTime > activityTime && chatTime.isValid()) {
1159 settings.setFriendActivity(friendPk, chatTime);
1160 }
1161
1162 contactListWidget->addFriendWidget(widget, Status::Status::Offline,
1163 settings.getFriendCircleID(friendPk));
1164
1165
1166 auto notifyReceivedCallback = [this, friendPk](const ToxPk& author, const Message& message) {
1167 auto isTargeted = std::any_of(message.metadata.begin(), message.metadata.end(),
1168 [](MessageMetadata metadata) {
1169 return metadata.type == MessageMetadataType::selfMention;
1170 });
1171 newFriendMessageAlert(friendPk, message.content);
1172 };
1173
1174 auto notifyReceivedConnection =
1175 connect(friendMessageDispatcher.get(), &IMessageDispatcher::messageReceived,
1176 notifyReceivedCallback);
1177
1178 friendAlertConnections.insert(friendPk, notifyReceivedConnection);
1179 connect(newfriend, &Friend::aliasChanged, this, &Widget::onFriendAliasChanged);
1180 connect(newfriend, &Friend::displayedNameChanged, this, &Widget::onFriendDisplayedNameChanged);
1181
1182 connect(friendForm, &ChatForm::incomingNotification, this, &Widget::incomingNotification);
1183 connect(friendForm, &ChatForm::outgoingNotification, this, &Widget::outgoingNotification);
1184 connect(friendForm, &ChatForm::stopNotification, this, &Widget::onStopNotification);
1185 connect(friendForm, &ChatForm::endCallNotification, this, &Widget::onCallEnd);
1186 connect(friendForm, &ChatForm::rejectCall, this, &Widget::onRejectCall);
1187
1188 connect(widget, &FriendWidget::newWindowOpened, this, &Widget::openNewDialog);
1189 connect(widget, &FriendWidget::chatroomWidgetClicked, this, &Widget::onChatroomWidgetClicked);
1190 connect(widget, &FriendWidget::chatroomWidgetClicked, friendForm, &ChatForm::focusInput);
1191 connect(widget, &FriendWidget::friendHistoryRemoved, friendForm, &ChatForm::clearChatArea);
1192 connect(widget, &FriendWidget::copyFriendIdToClipboard, this, &Widget::copyFriendIdToClipboard);
1193 connect(widget, &FriendWidget::contextMenuCalled, widget, &FriendWidget::onContextMenuCalled);
1194 connect(widget, SIGNAL(removeFriend(const ToxPk&)), this, SLOT(removeFriend(const ToxPk&)));
1195
1196 Profile* profile = Nexus::getProfile();
1197 connect(profile, &Profile::friendAvatarSet, widget, &FriendWidget::onAvatarSet);
1198 connect(profile, &Profile::friendAvatarRemoved, widget, &FriendWidget::onAvatarRemoved);
1199
1200 // Try to get the avatar from the cache
1201 QPixmap avatar = Nexus::getProfile()->loadAvatar(friendPk);
1202 if (!avatar.isNull()) {
1203 friendForm->onAvatarChanged(friendPk, avatar);
1204 widget->onAvatarSet(friendPk, avatar);
1205 }
1206
1207 FilterCriteria filter = getFilterCriteria();
1208 widget->search(ui->searchContactText->text(), filterOffline(filter));
1209 }
1210
addFriendFailed(const ToxPk &,const QString & errorInfo)1211 void Widget::addFriendFailed(const ToxPk&, const QString& errorInfo)
1212 {
1213 QString info = QString(tr("Couldn't request friendship"));
1214 if (!errorInfo.isEmpty()) {
1215 info = info + QStringLiteral(": ") + errorInfo;
1216 }
1217
1218 QMessageBox::critical(nullptr, "Error", info);
1219 }
1220
onFriendStatusChanged(int friendId,Status::Status status)1221 void Widget::onFriendStatusChanged(int friendId, Status::Status status)
1222 {
1223 const auto& friendPk = FriendList::id2Key(friendId);
1224 Friend* f = FriendList::findFriend(friendPk);
1225 if (!f) {
1226 return;
1227 }
1228
1229 bool isActualChange = f->getStatus() != status;
1230
1231 FriendWidget* widget = friendWidgets[f->getPublicKey()];
1232 if (isActualChange) {
1233 if (!Status::isOnline(f->getStatus())) {
1234 contactListWidget->moveWidget(widget, Status::Status::Online);
1235 } else if (status == Status::Status::Offline) {
1236 contactListWidget->moveWidget(widget, Status::Status::Offline);
1237 }
1238 }
1239
1240 f->setStatus(status);
1241 widget->updateStatusLight();
1242 if (widget->isActive()) {
1243 setWindowTitle(widget->getTitle());
1244 }
1245
1246 ContentDialogManager::getInstance()->updateFriendStatus(friendPk);
1247 }
1248
onFriendStatusMessageChanged(int friendId,const QString & message)1249 void Widget::onFriendStatusMessageChanged(int friendId, const QString& message)
1250 {
1251 const auto& friendPk = FriendList::id2Key(friendId);
1252 Friend* f = FriendList::findFriend(friendPk);
1253 if (!f) {
1254 return;
1255 }
1256
1257 QString str = message;
1258 str.replace('\n', ' ').remove('\r').remove(QChar('\0'));
1259 f->setStatusMessage(str);
1260
1261 friendWidgets[friendPk]->setStatusMsg(str);
1262 chatForms[friendPk]->setStatusMessage(str);
1263 }
1264
onFriendDisplayedNameChanged(const QString & displayed)1265 void Widget::onFriendDisplayedNameChanged(const QString& displayed)
1266 {
1267 Friend* f = qobject_cast<Friend*>(sender());
1268 const auto& friendPk = f->getPublicKey();
1269 for (Group* g : GroupList::getAllGroups()) {
1270 if (g->getPeerList().contains(friendPk)) {
1271 g->updateUsername(friendPk, displayed);
1272 }
1273 }
1274
1275 FriendWidget* friendWidget = friendWidgets[f->getPublicKey()];
1276 if (friendWidget->isActive()) {
1277 GUI::setWindowTitle(displayed);
1278 }
1279 }
1280
onFriendUsernameChanged(int friendId,const QString & username)1281 void Widget::onFriendUsernameChanged(int friendId, const QString& username)
1282 {
1283 const auto& friendPk = FriendList::id2Key(friendId);
1284 Friend* f = FriendList::findFriend(friendPk);
1285 if (!f) {
1286 return;
1287 }
1288
1289 QString str = username;
1290 str.replace('\n', ' ').remove('\r').remove(QChar('\0'));
1291 f->setName(str);
1292 }
1293
onFriendAliasChanged(const ToxPk & friendId,const QString & alias)1294 void Widget::onFriendAliasChanged(const ToxPk& friendId, const QString& alias)
1295 {
1296 Friend* f = qobject_cast<Friend*>(sender());
1297
1298 // TODO(sudden6): don't update the contact list here, make it update itself
1299 FriendWidget* friendWidget = friendWidgets[friendId];
1300 Status::Status status = f->getStatus();
1301 contactListWidget->moveWidget(friendWidget, status);
1302 FilterCriteria criteria = getFilterCriteria();
1303 bool filter = status == Status::Status::Offline ? filterOffline(criteria) : filterOnline(criteria);
1304 friendWidget->searchName(ui->searchContactText->text(), filter);
1305
1306 settings.setFriendAlias(friendId, alias);
1307 settings.savePersonal();
1308 }
1309
onChatroomWidgetClicked(GenericChatroomWidget * widget)1310 void Widget::onChatroomWidgetClicked(GenericChatroomWidget* widget)
1311 {
1312 openDialog(widget, /* newWindow = */ false);
1313 }
1314
openNewDialog(GenericChatroomWidget * widget)1315 void Widget::openNewDialog(GenericChatroomWidget* widget)
1316 {
1317 openDialog(widget, /* newWindow = */ true);
1318 }
1319
openDialog(GenericChatroomWidget * widget,bool newWindow)1320 void Widget::openDialog(GenericChatroomWidget* widget, bool newWindow)
1321 {
1322 widget->resetEventFlags();
1323 widget->updateStatusLight();
1324
1325 GenericChatForm* form;
1326 GroupId id;
1327 const Friend* frnd = widget->getFriend();
1328 const Group* group = widget->getGroup();
1329 if (frnd) {
1330 form = chatForms[frnd->getPublicKey()];
1331 } else {
1332 id = group->getPersistentId();
1333 form = groupChatForms[id].data();
1334 }
1335 bool chatFormIsSet;
1336 ContentDialogManager::getInstance()->focusContact(id);
1337 chatFormIsSet = ContentDialogManager::getInstance()->contactWidgetExists(id);
1338
1339
1340 if ((chatFormIsSet || form->isVisible()) && !newWindow) {
1341 return;
1342 }
1343
1344 if (settings.getSeparateWindow() || newWindow) {
1345 ContentDialog* dialog = nullptr;
1346
1347 if (!settings.getDontGroupWindows() && !newWindow) {
1348 dialog = ContentDialogManager::getInstance()->current();
1349 }
1350
1351 if (dialog == nullptr) {
1352 dialog = createContentDialog();
1353 }
1354
1355 dialog->show();
1356
1357 if (frnd) {
1358 addFriendDialog(frnd, dialog);
1359 } else {
1360 Group* group = widget->getGroup();
1361 addGroupDialog(group, dialog);
1362 }
1363
1364 dialog->raise();
1365 dialog->activateWindow();
1366 } else {
1367 hideMainForms(widget);
1368 if (frnd) {
1369 chatForms[frnd->getPublicKey()]->show(contentLayout);
1370 } else {
1371 groupChatForms[group->getPersistentId()]->show(contentLayout);
1372 }
1373 widget->setAsActiveChatroom();
1374 setWindowTitle(widget->getTitle());
1375 }
1376 }
1377
onFriendMessageReceived(uint32_t friendnumber,const QString & message,bool isAction)1378 void Widget::onFriendMessageReceived(uint32_t friendnumber, const QString& message, bool isAction)
1379 {
1380 const auto& friendId = FriendList::id2Key(friendnumber);
1381 Friend* f = FriendList::findFriend(friendId);
1382 if (!f) {
1383 return;
1384 }
1385
1386 friendMessageDispatchers[f->getPublicKey()]->onMessageReceived(isAction, message);
1387 }
1388
onReceiptReceived(int friendId,ReceiptNum receipt)1389 void Widget::onReceiptReceived(int friendId, ReceiptNum receipt)
1390 {
1391 const auto& friendKey = FriendList::id2Key(friendId);
1392 Friend* f = FriendList::findFriend(friendKey);
1393 if (!f) {
1394 return;
1395 }
1396
1397 friendMessageDispatchers[f->getPublicKey()]->onReceiptReceived(receipt);
1398 }
1399
addFriendDialog(const Friend * frnd,ContentDialog * dialog)1400 void Widget::addFriendDialog(const Friend* frnd, ContentDialog* dialog)
1401 {
1402 uint32_t friendId = frnd->getId();
1403 const ToxPk& friendPk = frnd->getPublicKey();
1404 ContentDialog* contentDialog = ContentDialogManager::getInstance()->getFriendDialog(friendPk);
1405 bool isSeparate = settings.getSeparateWindow();
1406 FriendWidget* widget = friendWidgets[friendPk];
1407 bool isCurrent = activeChatroomWidget == widget;
1408 if (!contentDialog && !isSeparate && isCurrent) {
1409 onAddClicked();
1410 }
1411
1412 auto form = chatForms[friendPk];
1413 auto chatroom = friendChatrooms[friendPk];
1414 FriendWidget* friendWidget =
1415 ContentDialogManager::getInstance()->addFriendToDialog(dialog, chatroom, form);
1416
1417 friendWidget->setStatusMsg(widget->getStatusMsg());
1418
1419 #if (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0))
1420 auto widgetRemoveFriend = QOverload<const ToxPk&>::of(&Widget::removeFriend);
1421 #else
1422 auto widgetRemoveFriend = static_cast<void (Widget::*)(const ToxPk&)>(&Widget::removeFriend);
1423 #endif
1424 connect(friendWidget, &FriendWidget::removeFriend, this, widgetRemoveFriend);
1425 connect(friendWidget, &FriendWidget::middleMouseClicked, dialog,
1426 [=]() { dialog->removeFriend(friendPk); });
1427 connect(friendWidget, &FriendWidget::copyFriendIdToClipboard, this,
1428 &Widget::copyFriendIdToClipboard);
1429 connect(friendWidget, &FriendWidget::newWindowOpened, this, &Widget::openNewDialog);
1430
1431 // Signal transmission from the created `friendWidget` (which shown in
1432 // ContentDialog) to the `widget` (which shown in main widget)
1433 // FIXME: emit should be removed
1434 connect(friendWidget, &FriendWidget::contextMenuCalled, widget,
1435 [=](QContextMenuEvent* event) { emit widget->contextMenuCalled(event); });
1436
1437 connect(friendWidget, &FriendWidget::chatroomWidgetClicked, [=](GenericChatroomWidget* w) {
1438 Q_UNUSED(w);
1439 emit widget->chatroomWidgetClicked(widget);
1440 });
1441 connect(friendWidget, &FriendWidget::newWindowOpened, [=](GenericChatroomWidget* w) {
1442 Q_UNUSED(w);
1443 emit widget->newWindowOpened(widget);
1444 });
1445 // FIXME: emit should be removed
1446 emit widget->chatroomWidgetClicked(widget);
1447
1448 Profile* profile = Nexus::getProfile();
1449 connect(profile, &Profile::friendAvatarSet, friendWidget, &FriendWidget::onAvatarSet);
1450 connect(profile, &Profile::friendAvatarRemoved, friendWidget, &FriendWidget::onAvatarRemoved);
1451
1452 QPixmap avatar = Nexus::getProfile()->loadAvatar(frnd->getPublicKey());
1453 if (!avatar.isNull()) {
1454 friendWidget->onAvatarSet(frnd->getPublicKey(), avatar);
1455 }
1456 }
1457
addGroupDialog(Group * group,ContentDialog * dialog)1458 void Widget::addGroupDialog(Group* group, ContentDialog* dialog)
1459 {
1460 const GroupId& groupId = group->getPersistentId();
1461 ContentDialog* groupDialog = ContentDialogManager::getInstance()->getGroupDialog(groupId);
1462 bool separated = settings.getSeparateWindow();
1463 GroupWidget* widget = groupWidgets[groupId];
1464 bool isCurrentWindow = activeChatroomWidget == widget;
1465 if (!groupDialog && !separated && isCurrentWindow) {
1466 onAddClicked();
1467 }
1468
1469 auto chatForm = groupChatForms[groupId].data();
1470 auto chatroom = groupChatrooms[groupId];
1471 auto groupWidget =
1472 ContentDialogManager::getInstance()->addGroupToDialog(dialog, chatroom, chatForm);
1473
1474 #if (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0))
1475 auto removeGroup = QOverload<const GroupId&>::of(&Widget::removeGroup);
1476 #else
1477 auto removeGroup = static_cast<void (Widget::*)(const GroupId&)>(&Widget::removeGroup);
1478 #endif
1479 connect(groupWidget, &GroupWidget::removeGroup, this, removeGroup);
1480 connect(groupWidget, &GroupWidget::chatroomWidgetClicked, chatForm, &GroupChatForm::focusInput);
1481 connect(groupWidget, &GroupWidget::middleMouseClicked, dialog,
1482 [=]() { dialog->removeGroup(groupId); });
1483 connect(groupWidget, &GroupWidget::chatroomWidgetClicked, chatForm, &ChatForm::focusInput);
1484 connect(groupWidget, &GroupWidget::newWindowOpened, this, &Widget::openNewDialog);
1485
1486 // Signal transmission from the created `groupWidget` (which shown in
1487 // ContentDialog) to the `widget` (which shown in main widget)
1488 // FIXME: emit should be removed
1489 connect(groupWidget, &GroupWidget::chatroomWidgetClicked, [=](GenericChatroomWidget* w) {
1490 Q_UNUSED(w);
1491 emit widget->chatroomWidgetClicked(widget);
1492 });
1493
1494 connect(groupWidget, &GroupWidget::newWindowOpened, [=](GenericChatroomWidget* w) {
1495 Q_UNUSED(w);
1496 emit widget->newWindowOpened(widget);
1497 });
1498
1499 // FIXME: emit should be removed
1500 emit widget->chatroomWidgetClicked(widget);
1501 }
1502
newFriendMessageAlert(const ToxPk & friendId,const QString & text,bool sound,bool file)1503 bool Widget::newFriendMessageAlert(const ToxPk& friendId, const QString& text, bool sound, bool file)
1504 {
1505 bool hasActive;
1506 QWidget* currentWindow;
1507 ContentDialog* contentDialog = ContentDialogManager::getInstance()->getFriendDialog(friendId);
1508 Friend* f = FriendList::findFriend(friendId);
1509
1510 if (contentDialog != nullptr) {
1511 currentWindow = contentDialog->window();
1512 hasActive = ContentDialogManager::getInstance()->isContactActive(friendId);
1513 } else {
1514 if (settings.getSeparateWindow() && settings.getShowWindow()) {
1515 if (settings.getDontGroupWindows()) {
1516 contentDialog = createContentDialog();
1517 } else {
1518 contentDialog = ContentDialogManager::getInstance()->current();
1519 if (!contentDialog) {
1520 contentDialog = createContentDialog();
1521 }
1522 }
1523
1524 addFriendDialog(f, contentDialog);
1525 currentWindow = contentDialog->window();
1526 hasActive = ContentDialogManager::getInstance()->isContactActive(friendId);
1527 } else {
1528 currentWindow = window();
1529 FriendWidget* widget = friendWidgets[friendId];
1530 hasActive = widget == activeChatroomWidget;
1531 }
1532 }
1533
1534 if (newMessageAlert(currentWindow, hasActive, sound)) {
1535 FriendWidget* widget = friendWidgets[friendId];
1536 f->setEventFlag(true);
1537 widget->updateStatusLight();
1538 ui->friendList->trackWidget(widget);
1539 #if DESKTOP_NOTIFICATIONS
1540 if (settings.getNotifyHide()) {
1541 notifier.notifyMessageSimple(file ? DesktopNotify::MessageType::FRIEND_FILE
1542 : DesktopNotify::MessageType::FRIEND);
1543 } else {
1544 QString title = f->getDisplayedName();
1545 if (file) {
1546 title += " - " + tr("File sent");
1547 }
1548 notifier.notifyMessagePixmap(title, text,
1549 Nexus::getProfile()->loadAvatar(f->getPublicKey()));
1550 }
1551 #endif
1552
1553 if (contentDialog == nullptr) {
1554 if (hasActive) {
1555 setWindowTitle(widget->getTitle());
1556 }
1557 } else {
1558 ContentDialogManager::getInstance()->updateFriendStatus(friendId);
1559 }
1560
1561 return true;
1562 }
1563
1564 return false;
1565 }
1566
newGroupMessageAlert(const GroupId & groupId,const ToxPk & authorPk,const QString & message,bool notify)1567 bool Widget::newGroupMessageAlert(const GroupId& groupId, const ToxPk& authorPk,
1568 const QString& message, bool notify)
1569 {
1570 bool hasActive;
1571 QWidget* currentWindow;
1572 ContentDialog* contentDialog = ContentDialogManager::getInstance()->getGroupDialog(groupId);
1573 Group* g = GroupList::findGroup(groupId);
1574 GroupWidget* widget = groupWidgets[groupId];
1575
1576 if (contentDialog != nullptr) {
1577 currentWindow = contentDialog->window();
1578 hasActive = ContentDialogManager::getInstance()->isContactActive(groupId);
1579 } else {
1580 currentWindow = window();
1581 hasActive = widget == activeChatroomWidget;
1582 }
1583
1584 if (!newMessageAlert(currentWindow, hasActive, true, notify)) {
1585 return false;
1586 }
1587
1588 g->setEventFlag(true);
1589 widget->updateStatusLight();
1590 #if DESKTOP_NOTIFICATIONS
1591 if (settings.getNotifyHide()) {
1592 notifier.notifyMessageSimple(DesktopNotify::MessageType::GROUP);
1593 } else {
1594 Friend* f = FriendList::findFriend(authorPk);
1595 QString title = g->getPeerList().value(authorPk) + " (" + g->getDisplayedName() + ")";
1596 if (!f) {
1597 notifier.notifyMessage(title, message);
1598 } else {
1599 notifier.notifyMessagePixmap(title, message,
1600 Nexus::getProfile()->loadAvatar(f->getPublicKey()));
1601 }
1602 }
1603 #endif
1604
1605 if (contentDialog == nullptr) {
1606 if (hasActive) {
1607 setWindowTitle(widget->getTitle());
1608 }
1609 } else {
1610 ContentDialogManager::getInstance()->updateGroupStatus(groupId);
1611 }
1612
1613 return true;
1614 }
1615
fromDialogType(DialogType type)1616 QString Widget::fromDialogType(DialogType type)
1617 {
1618 switch (type) {
1619 case DialogType::AddDialog:
1620 return tr("Add friend", "title of the window");
1621 case DialogType::GroupDialog:
1622 return tr("Group invites", "title of the window");
1623 case DialogType::TransferDialog:
1624 return tr("File transfers", "title of the window");
1625 case DialogType::SettingDialog:
1626 return tr("Settings", "title of the window");
1627 case DialogType::ProfileDialog:
1628 return tr("My profile", "title of the window");
1629 }
1630
1631 assert(false);
1632 return QString();
1633 }
1634
newMessageAlert(QWidget * currentWindow,bool isActive,bool sound,bool notify)1635 bool Widget::newMessageAlert(QWidget* currentWindow, bool isActive, bool sound, bool notify)
1636 {
1637 bool inactiveWindow = isMinimized() || !currentWindow->isActiveWindow();
1638
1639 if (!inactiveWindow && isActive) {
1640 return false;
1641 }
1642
1643 if (notify) {
1644 if (settings.getShowWindow()) {
1645 currentWindow->show();
1646 }
1647
1648 if (settings.getNotify()) {
1649 if (inactiveWindow) {
1650 #if DESKTOP_NOTIFICATIONS
1651 if (!settings.getDesktopNotify()) {
1652 QApplication::alert(currentWindow);
1653 }
1654 #else
1655 QApplication::alert(currentWindow);
1656 #endif
1657 eventFlag = true;
1658 }
1659 bool isBusy = core->getStatus() == Status::Status::Busy;
1660 bool busySound = settings.getBusySound();
1661 bool notifySound = settings.getNotifySound();
1662
1663 if (notifySound && sound && (!isBusy || busySound)) {
1664 playNotificationSound(IAudioSink::Sound::NewMessage);
1665 }
1666 }
1667 }
1668
1669 return true;
1670 }
1671
onFriendRequestReceived(const ToxPk & friendPk,const QString & message)1672 void Widget::onFriendRequestReceived(const ToxPk& friendPk, const QString& message)
1673 {
1674 if (addFriendForm->addFriendRequest(friendPk.toString(), message)) {
1675 friendRequestsUpdate();
1676 newMessageAlert(window(), isActiveWindow(), true, true);
1677 #if DESKTOP_NOTIFICATIONS
1678 if (settings.getNotifyHide()) {
1679 notifier.notifyMessageSimple(DesktopNotify::MessageType::FRIEND_REQUEST);
1680 } else {
1681 notifier.notifyMessage(friendPk.toString() + tr(" sent you a friend request."), message);
1682 }
1683 #endif
1684 }
1685 }
1686
onFileReceiveRequested(const ToxFile & file)1687 void Widget::onFileReceiveRequested(const ToxFile& file)
1688 {
1689 const ToxPk& friendPk = FriendList::id2Key(file.friendId);
1690 newFriendMessageAlert(friendPk,
1691 file.fileName + " ("
1692 + FileTransferWidget::getHumanReadableSize(file.filesize) + ")",
1693 true, true);
1694 }
1695
updateFriendActivity(const Friend & frnd)1696 void Widget::updateFriendActivity(const Friend& frnd)
1697 {
1698 const ToxPk& pk = frnd.getPublicKey();
1699 const auto oldTime = settings.getFriendActivity(pk);
1700 const auto newTime = QDateTime::currentDateTime();
1701 settings.setFriendActivity(pk, newTime);
1702 FriendWidget* widget = friendWidgets[frnd.getPublicKey()];
1703 contactListWidget->moveWidget(widget, frnd.getStatus());
1704 contactListWidget->updateActivityTime(oldTime); // update old category widget
1705 }
1706
removeFriend(Friend * f,bool fake)1707 void Widget::removeFriend(Friend* f, bool fake)
1708 {
1709 if (!fake) {
1710 RemoveFriendDialog ask(this, f);
1711 ask.exec();
1712
1713 if (!ask.accepted()) {
1714 return;
1715 }
1716
1717 if (ask.removeHistory()) {
1718 Nexus::getProfile()->getHistory()->removeFriendHistory(f->getPublicKey().toString());
1719 }
1720 }
1721
1722 const ToxPk friendPk = f->getPublicKey();
1723 auto widget = friendWidgets[friendPk];
1724 widget->setAsInactiveChatroom();
1725 if (widget == activeChatroomWidget) {
1726 activeChatroomWidget = nullptr;
1727 onAddClicked();
1728 }
1729
1730 friendAlertConnections.remove(friendPk);
1731
1732 contactListWidget->removeFriendWidget(widget);
1733
1734 ContentDialog* lastDialog = ContentDialogManager::getInstance()->getFriendDialog(friendPk);
1735 if (lastDialog != nullptr) {
1736 lastDialog->removeFriend(friendPk);
1737 }
1738
1739 FriendList::removeFriend(friendPk, fake);
1740 if (!fake) {
1741 core->removeFriend(f->getId());
1742 // aliases aren't supported for non-friend peers in groups, revert to basic username
1743 for (Group* g : GroupList::getAllGroups()) {
1744 if (g->getPeerList().contains(friendPk)) {
1745 g->updateUsername(friendPk, f->getUserName());
1746 }
1747 }
1748 }
1749
1750 friendWidgets.remove(friendPk);
1751 delete widget;
1752
1753 auto chatForm = chatForms[friendPk];
1754 chatForms.remove(friendPk);
1755 delete chatForm;
1756
1757 delete f;
1758 if (contentLayout && contentLayout->mainHead->layout()->isEmpty()) {
1759 onAddClicked();
1760 }
1761
1762 contactListWidget->reDraw();
1763 }
1764
removeFriend(const ToxPk & friendId)1765 void Widget::removeFriend(const ToxPk& friendId)
1766 {
1767 removeFriend(FriendList::findFriend(friendId), false);
1768 }
1769
onDialogShown(GenericChatroomWidget * widget)1770 void Widget::onDialogShown(GenericChatroomWidget* widget)
1771 {
1772 widget->resetEventFlags();
1773 widget->updateStatusLight();
1774
1775 ui->friendList->updateTracking(widget);
1776 resetIcon();
1777 }
1778
onFriendDialogShown(const Friend * f)1779 void Widget::onFriendDialogShown(const Friend* f)
1780 {
1781 onDialogShown(friendWidgets[f->getPublicKey()]);
1782 }
1783
onGroupDialogShown(Group * g)1784 void Widget::onGroupDialogShown(Group* g)
1785 {
1786 const GroupId& groupId = g->getPersistentId();
1787 onDialogShown(groupWidgets[groupId]);
1788 }
1789
toggleFullscreen()1790 void Widget::toggleFullscreen()
1791 {
1792 if (windowState().testFlag(Qt::WindowFullScreen)) {
1793 setWindowState(windowState() & ~Qt::WindowFullScreen);
1794 } else {
1795 setWindowState(windowState() | Qt::WindowFullScreen);
1796 }
1797 }
1798
onUpdateAvailable()1799 void Widget::onUpdateAvailable()
1800 {
1801 ui->settingsButton->setProperty("update-available", true);
1802 ui->settingsButton->style()->unpolish(ui->settingsButton);
1803 ui->settingsButton->style()->polish(ui->settingsButton);
1804 }
1805
createContentDialog() const1806 ContentDialog* Widget::createContentDialog() const
1807 {
1808 ContentDialog* contentDialog = new ContentDialog();
1809
1810 registerContentDialog(*contentDialog);
1811 return contentDialog;
1812 }
1813
registerContentDialog(ContentDialog & contentDialog) const1814 void Widget::registerContentDialog(ContentDialog& contentDialog) const
1815 {
1816 ContentDialogManager::getInstance()->addContentDialog(contentDialog);
1817 connect(&contentDialog, &ContentDialog::friendDialogShown, this, &Widget::onFriendDialogShown);
1818 connect(&contentDialog, &ContentDialog::groupDialogShown, this, &Widget::onGroupDialogShown);
1819 connect(core, &Core::usernameSet, &contentDialog, &ContentDialog::setUsername);
1820 connect(&settings, &Settings::groupchatPositionChanged, &contentDialog,
1821 &ContentDialog::reorderLayouts);
1822 connect(&contentDialog, &ContentDialog::addFriendDialog, this, &Widget::addFriendDialog);
1823 connect(&contentDialog, &ContentDialog::addGroupDialog, this, &Widget::addGroupDialog);
1824 connect(&contentDialog, &ContentDialog::connectFriendWidget, this, &Widget::connectFriendWidget);
1825
1826 #ifdef Q_OS_MAC
1827 Nexus& n = Nexus::getInstance();
1828 connect(&contentDialog, &ContentDialog::destroyed, &n, &Nexus::updateWindowsClosed);
1829 connect(&contentDialog, &ContentDialog::windowStateChanged, &n, &Nexus::onWindowStateChanged);
1830 connect(contentDialog.windowHandle(), &QWindow::windowTitleChanged, &n, &Nexus::updateWindows);
1831 n.updateWindows();
1832 #endif
1833 }
1834
createContentDialog(DialogType type) const1835 ContentLayout* Widget::createContentDialog(DialogType type) const
1836 {
1837 class Dialog : public ActivateDialog
1838 {
1839 public:
1840 explicit Dialog(DialogType type, Settings& settings, Core* core)
1841 : ActivateDialog(nullptr, Qt::Window)
1842 , type(type)
1843 , settings(settings)
1844 , core{core}
1845 {
1846 restoreGeometry(settings.getDialogSettingsGeometry());
1847 Translator::registerHandler(std::bind(&Dialog::retranslateUi, this), this);
1848 retranslateUi();
1849 setWindowIcon(QIcon(":/img/icons/qtox.svg"));
1850 setStyleSheet(Style::getStylesheet("window/general.css"));
1851
1852 connect(core, &Core::usernameSet, this, &Dialog::retranslateUi);
1853 }
1854
1855 ~Dialog()
1856 {
1857 Translator::unregister(this);
1858 }
1859
1860 public slots:
1861
1862 void retranslateUi()
1863 {
1864 setWindowTitle(core->getUsername() + QStringLiteral(" - ") + Widget::fromDialogType(type));
1865 }
1866
1867 protected:
1868 void resizeEvent(QResizeEvent* event) override
1869 {
1870 settings.setDialogSettingsGeometry(saveGeometry());
1871 QDialog::resizeEvent(event);
1872 }
1873
1874 void moveEvent(QMoveEvent* event) override
1875 {
1876 settings.setDialogSettingsGeometry(saveGeometry());
1877 QDialog::moveEvent(event);
1878 }
1879
1880 private:
1881 DialogType type;
1882 Settings& settings;
1883 Core* core;
1884 };
1885
1886 Dialog* dialog = new Dialog(type, settings, core);
1887 dialog->setAttribute(Qt::WA_DeleteOnClose);
1888 ContentLayout* contentLayoutDialog = new ContentLayout(dialog);
1889
1890 dialog->setObjectName("detached");
1891 dialog->setLayout(contentLayoutDialog);
1892 dialog->layout()->setMargin(0);
1893 dialog->layout()->setSpacing(0);
1894 dialog->setMinimumSize(720, 400);
1895 dialog->setAttribute(Qt::WA_DeleteOnClose);
1896 dialog->show();
1897
1898 #ifdef Q_OS_MAC
1899 connect(dialog, &Dialog::destroyed, &Nexus::getInstance(), &Nexus::updateWindowsClosed);
1900 connect(dialog, &ActivateDialog::windowStateChanged, &Nexus::getInstance(),
1901 &Nexus::updateWindowsStates);
1902 connect(dialog->windowHandle(), &QWindow::windowTitleChanged, &Nexus::getInstance(),
1903 &Nexus::updateWindows);
1904 Nexus::getInstance().updateWindows();
1905 #endif
1906
1907 return contentLayoutDialog;
1908 }
1909
copyFriendIdToClipboard(const ToxPk & friendId)1910 void Widget::copyFriendIdToClipboard(const ToxPk& friendId)
1911 {
1912 Friend* f = FriendList::findFriend(friendId);
1913 if (f != nullptr) {
1914 QClipboard* clipboard = QApplication::clipboard();
1915 clipboard->setText(friendId.toString(), QClipboard::Clipboard);
1916 }
1917 }
1918
onGroupInviteReceived(const GroupInvite & inviteInfo)1919 void Widget::onGroupInviteReceived(const GroupInvite& inviteInfo)
1920 {
1921 const uint32_t friendId = inviteInfo.getFriendId();
1922 const ToxPk& friendPk = FriendList::id2Key(friendId);
1923 const Friend* f = FriendList::findFriend(friendPk);
1924 updateFriendActivity(*f);
1925
1926 const uint8_t confType = inviteInfo.getType();
1927 if (confType == TOX_CONFERENCE_TYPE_TEXT || confType == TOX_CONFERENCE_TYPE_AV) {
1928 if (settings.getAutoGroupInvite(f->getPublicKey())) {
1929 onGroupInviteAccepted(inviteInfo);
1930 } else {
1931 if (!groupInviteForm->addGroupInvite(inviteInfo)) {
1932 return;
1933 }
1934
1935 ++unreadGroupInvites;
1936 groupInvitesUpdate();
1937 newMessageAlert(window(), isActiveWindow(), true, true);
1938 #if DESKTOP_NOTIFICATIONS
1939 if (settings.getNotifyHide()) {
1940 notifier.notifyMessageSimple(DesktopNotify::MessageType::GROUP_INVITE);
1941 } else {
1942 notifier.notifyMessagePixmap(f->getDisplayedName() + tr(" invites you to join a group."),
1943 {}, Nexus::getProfile()->loadAvatar(f->getPublicKey()));
1944 }
1945 #endif
1946 }
1947 } else {
1948 qWarning() << "onGroupInviteReceived: Unknown groupchat type:" << confType;
1949 return;
1950 }
1951 }
1952
onGroupInviteAccepted(const GroupInvite & inviteInfo)1953 void Widget::onGroupInviteAccepted(const GroupInvite& inviteInfo)
1954 {
1955 const uint32_t groupId = core->joinGroupchat(inviteInfo);
1956 if (groupId == std::numeric_limits<uint32_t>::max()) {
1957 qWarning() << "onGroupInviteAccepted: Unable to accept group invite";
1958 return;
1959 }
1960 }
1961
onGroupMessageReceived(int groupnumber,int peernumber,const QString & message,bool isAction)1962 void Widget::onGroupMessageReceived(int groupnumber, int peernumber, const QString& message,
1963 bool isAction)
1964 {
1965 const GroupId& groupId = GroupList::id2Key(groupnumber);
1966 Group* g = GroupList::findGroup(groupId);
1967 assert(g);
1968
1969 ToxPk author = core->getGroupPeerPk(groupnumber, peernumber);
1970
1971 groupMessageDispatchers[groupId]->onMessageReceived(author, isAction, message);
1972 }
1973
onGroupPeerlistChanged(uint32_t groupnumber)1974 void Widget::onGroupPeerlistChanged(uint32_t groupnumber)
1975 {
1976 const GroupId& groupId = GroupList::id2Key(groupnumber);
1977 Group* g = GroupList::findGroup(groupId);
1978 assert(g);
1979 g->regeneratePeerList();
1980 }
1981
onGroupPeerNameChanged(uint32_t groupnumber,const ToxPk & peerPk,const QString & newName)1982 void Widget::onGroupPeerNameChanged(uint32_t groupnumber, const ToxPk& peerPk, const QString& newName)
1983 {
1984 const GroupId& groupId = GroupList::id2Key(groupnumber);
1985 Group* g = GroupList::findGroup(groupId);
1986 assert(g);
1987
1988 const QString setName = FriendList::decideNickname(peerPk, newName);
1989 g->updateUsername(peerPk, newName);
1990 }
1991
onGroupTitleChanged(uint32_t groupnumber,const QString & author,const QString & title)1992 void Widget::onGroupTitleChanged(uint32_t groupnumber, const QString& author, const QString& title)
1993 {
1994 const GroupId& groupId = GroupList::id2Key(groupnumber);
1995 Group* g = GroupList::findGroup(groupId);
1996 assert(g);
1997
1998 GroupWidget* widget = groupWidgets[groupId];
1999 if (widget->isActive()) {
2000 GUI::setWindowTitle(title);
2001 }
2002
2003 g->setTitle(author, title);
2004 FilterCriteria filter = getFilterCriteria();
2005 widget->searchName(ui->searchContactText->text(), filterGroups(filter));
2006 }
2007
titleChangedByUser(const QString & title)2008 void Widget::titleChangedByUser(const QString& title)
2009 {
2010 const auto* group = qobject_cast<Group*>(sender());
2011 assert(group != nullptr);
2012 emit changeGroupTitle(group->getId(), title);
2013 }
2014
onGroupPeerAudioPlaying(int groupnumber,ToxPk peerPk)2015 void Widget::onGroupPeerAudioPlaying(int groupnumber, ToxPk peerPk)
2016 {
2017 const GroupId& groupId = GroupList::id2Key(groupnumber);
2018 Group* g = GroupList::findGroup(groupId);
2019 assert(g);
2020
2021 auto form = groupChatForms[groupId].data();
2022 form->peerAudioPlaying(peerPk);
2023 }
2024
removeGroup(Group * g,bool fake)2025 void Widget::removeGroup(Group* g, bool fake)
2026 {
2027 const auto& groupId = g->getPersistentId();
2028 const auto groupnumber = g->getId();
2029 auto groupWidgetIt = groupWidgets.find(groupId);
2030 if (groupWidgetIt == groupWidgets.end()) {
2031 qWarning() << "Tried to remove group" << groupnumber << "but GroupWidget doesn't exist";
2032 return;
2033 }
2034 auto widget = groupWidgetIt.value();
2035 widget->setAsInactiveChatroom();
2036 if (static_cast<GenericChatroomWidget*>(widget) == activeChatroomWidget) {
2037 activeChatroomWidget = nullptr;
2038 onAddClicked();
2039 }
2040
2041 GroupList::removeGroup(groupId, fake);
2042 ContentDialog* contentDialog = ContentDialogManager::getInstance()->getGroupDialog(groupId);
2043 if (contentDialog != nullptr) {
2044 contentDialog->removeGroup(groupId);
2045 }
2046
2047 if (!fake) {
2048 core->removeGroup(groupnumber);
2049 }
2050 contactListWidget->removeGroupWidget(widget); // deletes widget
2051
2052 groupWidgets.remove(groupId);
2053 auto groupChatFormIt = groupChatForms.find(groupId);
2054 if (groupChatFormIt == groupChatForms.end()) {
2055 qWarning() << "Tried to remove group" << groupnumber << "but GroupChatForm doesn't exist";
2056 return;
2057 }
2058 groupChatForms.erase(groupChatFormIt);
2059 delete g;
2060 if (contentLayout && contentLayout->mainHead->layout()->isEmpty()) {
2061 onAddClicked();
2062 }
2063
2064 groupAlertConnections.remove(groupId);
2065
2066 contactListWidget->reDraw();
2067 }
2068
removeGroup(const GroupId & groupId)2069 void Widget::removeGroup(const GroupId& groupId)
2070 {
2071 removeGroup(GroupList::findGroup(groupId));
2072 }
2073
createGroup(uint32_t groupnumber,const GroupId & groupId)2074 Group* Widget::createGroup(uint32_t groupnumber, const GroupId& groupId)
2075 {
2076 Group* g = GroupList::findGroup(groupId);
2077 if (g) {
2078 qWarning() << "Group already exists";
2079 return g;
2080 }
2081
2082 const auto groupName = tr("Groupchat #%1").arg(groupnumber);
2083 const bool enabled = core->getGroupAvEnabled(groupnumber);
2084 Group* newgroup =
2085 GroupList::addGroup(groupnumber, groupId, groupName, enabled, core->getUsername());
2086 auto dialogManager = ContentDialogManager::getInstance();
2087 auto rawChatroom = new GroupChatroom(newgroup, dialogManager);
2088 std::shared_ptr<GroupChatroom> chatroom(rawChatroom);
2089
2090 const auto compact = settings.getCompactLayout();
2091 auto widget = new GroupWidget(chatroom, compact);
2092 auto messageProcessor = MessageProcessor(sharedMessageProcessorParams);
2093 auto messageDispatcher =
2094 std::make_shared<GroupMessageDispatcher>(*newgroup, std::move(messageProcessor), *core,
2095 *core, Settings::getInstance());
2096 auto groupChatLog = std::make_shared<SessionChatLog>(*core);
2097
2098 connect(messageDispatcher.get(), &IMessageDispatcher::messageReceived, groupChatLog.get(),
2099 &SessionChatLog::onMessageReceived);
2100 connect(messageDispatcher.get(), &IMessageDispatcher::messageSent, groupChatLog.get(),
2101 &SessionChatLog::onMessageSent);
2102 connect(messageDispatcher.get(), &IMessageDispatcher::messageComplete, groupChatLog.get(),
2103 &SessionChatLog::onMessageComplete);
2104
2105 auto notifyReceivedCallback = [this, groupId](const ToxPk& author, const Message& message) {
2106 auto isTargeted = std::any_of(message.metadata.begin(), message.metadata.end(),
2107 [](MessageMetadata metadata) {
2108 return metadata.type == MessageMetadataType::selfMention;
2109 });
2110 newGroupMessageAlert(groupId, author, message.content,
2111 isTargeted || settings.getGroupAlwaysNotify());
2112 };
2113
2114 auto notifyReceivedConnection =
2115 connect(messageDispatcher.get(), &IMessageDispatcher::messageReceived, notifyReceivedCallback);
2116 groupAlertConnections.insert(groupId, notifyReceivedConnection);
2117
2118 auto form = new GroupChatForm(newgroup, *groupChatLog, *messageDispatcher);
2119 connect(&settings, &Settings::nameColorsChanged, form, &GenericChatForm::setColorizedNames);
2120 form->setColorizedNames(settings.getEnableGroupChatsColor());
2121 groupMessageDispatchers[groupId] = messageDispatcher;
2122 groupChatLogs[groupId] = groupChatLog;
2123 groupWidgets[groupId] = widget;
2124 groupChatrooms[groupId] = chatroom;
2125 groupChatForms[groupId] = QSharedPointer<GroupChatForm>(form);
2126
2127 contactListWidget->addGroupWidget(widget);
2128 widget->updateStatusLight();
2129 contactListWidget->activateWindow();
2130
2131 connect(widget, &GroupWidget::chatroomWidgetClicked, this, &Widget::onChatroomWidgetClicked);
2132 connect(widget, &GroupWidget::newWindowOpened, this, &Widget::openNewDialog);
2133 #if (QT_VERSION >= QT_VERSION_CHECK(5, 7, 0))
2134 auto widgetRemoveGroup = QOverload<const GroupId&>::of(&Widget::removeGroup);
2135 #else
2136 auto widgetRemoveGroup = static_cast<void (Widget::*)(const GroupId&)>(&Widget::removeGroup);
2137 #endif
2138 connect(widget, &GroupWidget::removeGroup, this, widgetRemoveGroup);
2139 connect(widget, &GroupWidget::middleMouseClicked, this, [=]() { removeGroup(groupId); });
2140 connect(widget, &GroupWidget::chatroomWidgetClicked, form, &ChatForm::focusInput);
2141 connect(newgroup, &Group::titleChangedByUser, this, &Widget::titleChangedByUser);
2142 connect(core, &Core::usernameSet, newgroup, &Group::setSelfName);
2143
2144 FilterCriteria filter = getFilterCriteria();
2145 widget->searchName(ui->searchContactText->text(), filterGroups(filter));
2146
2147 return newgroup;
2148 }
2149
onEmptyGroupCreated(uint32_t groupnumber,const GroupId & groupId,const QString & title)2150 void Widget::onEmptyGroupCreated(uint32_t groupnumber, const GroupId& groupId, const QString& title)
2151 {
2152 Group* group = createGroup(groupnumber, groupId);
2153 if (!group) {
2154 return;
2155 }
2156 if (title.isEmpty()) {
2157 // Only rename group if groups are visible.
2158 if (groupsVisible()) {
2159 groupWidgets[groupId]->editName();
2160 }
2161 } else {
2162 group->setTitle(QString(), title);
2163 }
2164 }
2165
onGroupJoined(int groupId,const GroupId & groupPersistentId)2166 void Widget::onGroupJoined(int groupId, const GroupId& groupPersistentId)
2167 {
2168 createGroup(groupId, groupPersistentId);
2169 }
2170
2171 /**
2172 * @brief Used to reset the blinking icon.
2173 */
resetIcon()2174 void Widget::resetIcon()
2175 {
2176 eventIcon = false;
2177 eventFlag = false;
2178 updateIcons();
2179 }
2180
event(QEvent * e)2181 bool Widget::event(QEvent* e)
2182 {
2183 switch (e->type()) {
2184 case QEvent::MouseButtonPress:
2185 case QEvent::MouseButtonDblClick:
2186 focusChatInput();
2187 break;
2188 case QEvent::Paint:
2189 ui->friendList->updateVisualTracking();
2190 break;
2191 case QEvent::WindowActivate:
2192 if (activeChatroomWidget) {
2193 activeChatroomWidget->resetEventFlags();
2194 activeChatroomWidget->updateStatusLight();
2195 setWindowTitle(activeChatroomWidget->getTitle());
2196 }
2197
2198 if (eventFlag) {
2199 resetIcon();
2200 }
2201
2202 focusChatInput();
2203
2204 #ifdef Q_OS_MAC
2205 emit windowStateChanged(windowState());
2206
2207 case QEvent::WindowStateChange:
2208 Nexus::getInstance().updateWindowsStates();
2209 #endif
2210 break;
2211 default:
2212 break;
2213 }
2214
2215 return QMainWindow::event(e);
2216 }
2217
onUserAwayCheck()2218 void Widget::onUserAwayCheck()
2219 {
2220 #ifdef QTOX_PLATFORM_EXT
2221 uint32_t autoAwayTime = settings.getAutoAwayTime() * 60 * 1000;
2222 bool online = static_cast<Status::Status>(ui->statusButton->property("status").toInt())
2223 == Status::Status::Online;
2224 bool away = autoAwayTime && Platform::getIdleTime() >= autoAwayTime;
2225
2226 if (online && away) {
2227 qDebug() << "auto away activated at" << QTime::currentTime().toString();
2228 emit statusSet(Status::Status::Away);
2229 autoAwayActive = true;
2230 } else if (autoAwayActive && !away) {
2231 qDebug() << "auto away deactivated at" << QTime::currentTime().toString();
2232 emit statusSet(Status::Status::Online);
2233 autoAwayActive = false;
2234 }
2235 #endif
2236 }
2237
onEventIconTick()2238 void Widget::onEventIconTick()
2239 {
2240 if (eventFlag) {
2241 eventIcon ^= true;
2242 updateIcons();
2243 }
2244 }
2245
onTryCreateTrayIcon()2246 void Widget::onTryCreateTrayIcon()
2247 {
2248 static int32_t tries = 15;
2249 if (!icon && tries--) {
2250 if (QSystemTrayIcon::isSystemTrayAvailable()) {
2251 icon = std::unique_ptr<QSystemTrayIcon>(new QSystemTrayIcon);
2252 updateIcons();
2253 trayMenu = new QMenu(this);
2254
2255 // adding activate to the top, avoids accidentally clicking quit
2256 trayMenu->addAction(actionShow);
2257 trayMenu->addSeparator();
2258 trayMenu->addAction(statusOnline);
2259 trayMenu->addAction(statusAway);
2260 trayMenu->addAction(statusBusy);
2261 trayMenu->addSeparator();
2262 trayMenu->addAction(actionLogout);
2263 trayMenu->addAction(actionQuit);
2264 icon->setContextMenu(trayMenu);
2265
2266 connect(icon.get(), &QSystemTrayIcon::activated, this, &Widget::onIconClick);
2267
2268 if (settings.getShowSystemTray()) {
2269 icon->show();
2270 setHidden(settings.getAutostartInTray());
2271 } else {
2272 show();
2273 }
2274
2275 #ifdef Q_OS_MAC
2276 Nexus::getInstance().dockMenu->setAsDockMenu();
2277 #endif
2278 } else if (!isVisible()) {
2279 show();
2280 }
2281 } else {
2282 disconnect(timer, &QTimer::timeout, this, &Widget::onTryCreateTrayIcon);
2283 if (!icon) {
2284 qWarning() << "No system tray detected!";
2285 show();
2286 }
2287 }
2288 }
2289
setStatusOnline()2290 void Widget::setStatusOnline()
2291 {
2292 if (!ui->statusButton->isEnabled()) {
2293 return;
2294 }
2295
2296 core->setStatus(Status::Status::Online);
2297 }
2298
setStatusAway()2299 void Widget::setStatusAway()
2300 {
2301 if (!ui->statusButton->isEnabled()) {
2302 return;
2303 }
2304
2305 core->setStatus(Status::Status::Away);
2306 }
2307
setStatusBusy()2308 void Widget::setStatusBusy()
2309 {
2310 if (!ui->statusButton->isEnabled()) {
2311 return;
2312 }
2313
2314 core->setStatus(Status::Status::Busy);
2315 }
2316
onGroupSendFailed(uint32_t groupnumber)2317 void Widget::onGroupSendFailed(uint32_t groupnumber)
2318 {
2319 const auto& groupId = GroupList::id2Key(groupnumber);
2320 Group* g = GroupList::findGroup(groupId);
2321 assert(g);
2322
2323 const auto message = tr("Message failed to send");
2324 const auto curTime = QDateTime::currentDateTime();
2325 auto form = groupChatForms[groupId].data();
2326 form->addSystemInfoMessage(message, ChatMessage::INFO, curTime);
2327 }
2328
onFriendTypingChanged(uint32_t friendnumber,bool isTyping)2329 void Widget::onFriendTypingChanged(uint32_t friendnumber, bool isTyping)
2330 {
2331 const auto& friendId = FriendList::id2Key(friendnumber);
2332 Friend* f = FriendList::findFriend(friendId);
2333 if (!f) {
2334 return;
2335 }
2336
2337 chatForms[f->getPublicKey()]->setFriendTyping(isTyping);
2338 }
2339
onSetShowSystemTray(bool newValue)2340 void Widget::onSetShowSystemTray(bool newValue)
2341 {
2342 if (icon) {
2343 icon->setVisible(newValue);
2344 }
2345 }
2346
saveWindowGeometry()2347 void Widget::saveWindowGeometry()
2348 {
2349 settings.setWindowGeometry(saveGeometry());
2350 settings.setWindowState(saveState());
2351 }
2352
saveSplitterGeometry()2353 void Widget::saveSplitterGeometry()
2354 {
2355 if (!settings.getSeparateWindow()) {
2356 settings.setSplitterState(ui->mainSplitter->saveState());
2357 }
2358 }
2359
onSplitterMoved(int pos,int index)2360 void Widget::onSplitterMoved(int pos, int index)
2361 {
2362 Q_UNUSED(pos);
2363 Q_UNUSED(index);
2364 saveSplitterGeometry();
2365 }
2366
cycleContacts(bool forward)2367 void Widget::cycleContacts(bool forward)
2368 {
2369 contactListWidget->cycleContacts(activeChatroomWidget, forward);
2370 }
2371
filterGroups(FilterCriteria index)2372 bool Widget::filterGroups(FilterCriteria index)
2373 {
2374 switch (index) {
2375 case FilterCriteria::Offline:
2376 case FilterCriteria::Friends:
2377 return true;
2378 default:
2379 return false;
2380 }
2381 }
2382
filterOffline(FilterCriteria index)2383 bool Widget::filterOffline(FilterCriteria index)
2384 {
2385 switch (index) {
2386 case FilterCriteria::Online:
2387 case FilterCriteria::Groups:
2388 return true;
2389 default:
2390 return false;
2391 }
2392 }
2393
filterOnline(FilterCriteria index)2394 bool Widget::filterOnline(FilterCriteria index)
2395 {
2396 switch (index) {
2397 case FilterCriteria::Offline:
2398 case FilterCriteria::Groups:
2399 return true;
2400 default:
2401 return false;
2402 }
2403 }
2404
clearAllReceipts()2405 void Widget::clearAllReceipts()
2406 {
2407 QList<Friend*> frnds = FriendList::getAllFriends();
2408 for (Friend* f : frnds) {
2409 friendMessageDispatchers[f->getPublicKey()]->clearOutgoingMessages();
2410 }
2411 }
2412
reloadTheme()2413 void Widget::reloadTheme()
2414 {
2415 this->setStyleSheet(Style::getStylesheet("window/general.css"));
2416 QString statusPanelStyle = Style::getStylesheet("window/statusPanel.css");
2417 ui->tooliconsZone->setStyleSheet(Style::getStylesheet("tooliconsZone/tooliconsZone.css"));
2418 ui->statusPanel->setStyleSheet(statusPanelStyle);
2419 ui->statusHead->setStyleSheet(statusPanelStyle);
2420 ui->friendList->setStyleSheet(Style::getStylesheet("friendList/friendList.css"));
2421 ui->statusButton->setStyleSheet(Style::getStylesheet("statusButton/statusButton.css"));
2422 contactListWidget->reDraw();
2423
2424 profilePicture->setStyleSheet(Style::getStylesheet("window/profile.css"));
2425
2426 if (contentLayout != nullptr) {
2427 contentLayout->reloadTheme();
2428 }
2429
2430 for (Friend* f : FriendList::getAllFriends()) {
2431 friendWidgets[f->getPublicKey()]->reloadTheme();
2432 }
2433
2434 for (Group* g : GroupList::getAllGroups()) {
2435 groupWidgets[g->getPersistentId()]->reloadTheme();
2436 }
2437
2438
2439 for (auto f : FriendList::getAllFriends()) {
2440 chatForms[f->getPublicKey()]->reloadTheme();
2441 }
2442
2443 for (auto g : GroupList::getAllGroups()) {
2444 groupChatForms[g->getPersistentId()]->reloadTheme();
2445 }
2446 }
2447
nextContact()2448 void Widget::nextContact()
2449 {
2450 cycleContacts(true);
2451 }
2452
previousContact()2453 void Widget::previousContact()
2454 {
2455 cycleContacts(false);
2456 }
2457
2458 // Preparing needed to set correct size of icons for GTK tray backend
prepareIcon(QString path,int w,int h)2459 inline QIcon Widget::prepareIcon(QString path, int w, int h)
2460 {
2461 #ifdef Q_OS_LINUX
2462
2463 QString desktop = getenv("XDG_CURRENT_DESKTOP");
2464 if (desktop.isEmpty()) {
2465 desktop = getenv("DESKTOP_SESSION");
2466 }
2467
2468 desktop = desktop.toLower();
2469 if (desktop == "xfce" || desktop.contains("gnome") || desktop == "mate" || desktop == "x-cinnamon") {
2470 if (w > 0 && h > 0) {
2471 QSvgRenderer renderer(path);
2472
2473 QPixmap pm(w, h);
2474 pm.fill(Qt::transparent);
2475 QPainter painter(&pm);
2476 renderer.render(&painter, pm.rect());
2477
2478 return QIcon(pm);
2479 }
2480 }
2481 #endif
2482 return QIcon(path);
2483 }
2484
searchContacts()2485 void Widget::searchContacts()
2486 {
2487 QString searchString = ui->searchContactText->text();
2488 FilterCriteria filter = getFilterCriteria();
2489
2490 contactListWidget->searchChatrooms(searchString, filterOnline(filter), filterOffline(filter),
2491 filterGroups(filter));
2492
2493 updateFilterText();
2494
2495 contactListWidget->reDraw();
2496 }
2497
changeDisplayMode()2498 void Widget::changeDisplayMode()
2499 {
2500 filterDisplayGroup->setEnabled(false);
2501
2502 if (filterDisplayGroup->checkedAction() == filterDisplayActivity) {
2503 contactListWidget->setMode(FriendListWidget::SortingMode::Activity);
2504 } else if (filterDisplayGroup->checkedAction() == filterDisplayName) {
2505 contactListWidget->setMode(FriendListWidget::SortingMode::Name);
2506 }
2507
2508 searchContacts();
2509 filterDisplayGroup->setEnabled(true);
2510
2511 updateFilterText();
2512 }
2513
updateFilterText()2514 void Widget::updateFilterText()
2515 {
2516 QString action = filterDisplayGroup->checkedAction()->text();
2517 QString text = filterGroup->checkedAction()->text();
2518 text = action + QStringLiteral(" | ") + text;
2519 ui->searchContactFilterBox->setText(text);
2520 }
2521
getFilterCriteria() const2522 Widget::FilterCriteria Widget::getFilterCriteria() const
2523 {
2524 QAction* checked = filterGroup->checkedAction();
2525
2526 if (checked == filterOnlineAction)
2527 return FilterCriteria::Online;
2528 else if (checked == filterOfflineAction)
2529 return FilterCriteria::Offline;
2530 else if (checked == filterFriendsAction)
2531 return FilterCriteria::Friends;
2532 else if (checked == filterGroupsAction)
2533 return FilterCriteria::Groups;
2534
2535 return FilterCriteria::All;
2536 }
2537
searchCircle(CircleWidget & circleWidget)2538 void Widget::searchCircle(CircleWidget& circleWidget)
2539 {
2540 FilterCriteria filter = getFilterCriteria();
2541 QString text = ui->searchContactText->text();
2542 circleWidget.search(text, true, filterOnline(filter), filterOffline(filter));
2543 }
2544
groupsVisible() const2545 bool Widget::groupsVisible() const
2546 {
2547 FilterCriteria filter = getFilterCriteria();
2548 return !filterGroups(filter);
2549 }
2550
friendListContextMenu(const QPoint & pos)2551 void Widget::friendListContextMenu(const QPoint& pos)
2552 {
2553 QMenu menu(this);
2554 QAction* createGroupAction = menu.addAction(tr("Create new group..."));
2555 QAction* addCircleAction = menu.addAction(tr("Add new circle..."));
2556 QAction* chosenAction = menu.exec(ui->friendList->mapToGlobal(pos));
2557
2558 if (chosenAction == addCircleAction) {
2559 contactListWidget->addCircleWidget();
2560 } else if (chosenAction == createGroupAction) {
2561 core->createGroup();
2562 }
2563 }
2564
friendRequestsUpdate()2565 void Widget::friendRequestsUpdate()
2566 {
2567 unsigned int unreadFriendRequests = settings.getUnreadFriendRequests();
2568
2569 if (unreadFriendRequests == 0) {
2570 delete friendRequestsButton;
2571 friendRequestsButton = nullptr;
2572 } else if (!friendRequestsButton) {
2573 friendRequestsButton = new QPushButton(this);
2574 friendRequestsButton->setObjectName("green");
2575 ui->statusLayout->insertWidget(2, friendRequestsButton);
2576
2577 connect(friendRequestsButton, &QPushButton::released, [this]() {
2578 onAddClicked();
2579 addFriendForm->setMode(AddFriendForm::Mode::FriendRequest);
2580 });
2581 }
2582
2583 if (friendRequestsButton) {
2584 friendRequestsButton->setText(tr("%n New Friend Request(s)", "", unreadFriendRequests));
2585 }
2586 }
2587
groupInvitesUpdate()2588 void Widget::groupInvitesUpdate()
2589 {
2590 if (unreadGroupInvites == 0) {
2591 delete groupInvitesButton;
2592 groupInvitesButton = nullptr;
2593 } else if (!groupInvitesButton) {
2594 groupInvitesButton = new QPushButton(this);
2595 groupInvitesButton->setObjectName("green");
2596 ui->statusLayout->insertWidget(2, groupInvitesButton);
2597
2598 connect(groupInvitesButton, &QPushButton::released, this, &Widget::onGroupClicked);
2599 }
2600
2601 if (groupInvitesButton) {
2602 groupInvitesButton->setText(tr("%n New Group Invite(s)", "", unreadGroupInvites));
2603 }
2604 }
2605
groupInvitesClear()2606 void Widget::groupInvitesClear()
2607 {
2608 unreadGroupInvites = 0;
2609 groupInvitesUpdate();
2610 }
2611
setActiveToolMenuButton(ActiveToolMenuButton newActiveButton)2612 void Widget::setActiveToolMenuButton(ActiveToolMenuButton newActiveButton)
2613 {
2614 ui->addButton->setChecked(newActiveButton == ActiveToolMenuButton::AddButton);
2615 ui->addButton->setDisabled(newActiveButton == ActiveToolMenuButton::AddButton);
2616 ui->groupButton->setChecked(newActiveButton == ActiveToolMenuButton::GroupButton);
2617 ui->groupButton->setDisabled(newActiveButton == ActiveToolMenuButton::GroupButton);
2618 ui->transferButton->setChecked(newActiveButton == ActiveToolMenuButton::TransferButton);
2619 ui->transferButton->setDisabled(newActiveButton == ActiveToolMenuButton::TransferButton);
2620 ui->settingsButton->setChecked(newActiveButton == ActiveToolMenuButton::SettingButton);
2621 ui->settingsButton->setDisabled(newActiveButton == ActiveToolMenuButton::SettingButton);
2622 }
2623
retranslateUi()2624 void Widget::retranslateUi()
2625 {
2626 ui->retranslateUi(this);
2627 setUsername(core->getUsername());
2628 setStatusMessage(core->getStatusMessage());
2629
2630 filterDisplayName->setText(tr("By Name"));
2631 filterDisplayActivity->setText(tr("By Activity"));
2632 filterAllAction->setText(tr("All"));
2633 filterOnlineAction->setText(tr("Online"));
2634 filterOfflineAction->setText(tr("Offline"));
2635 filterFriendsAction->setText(tr("Friends"));
2636 filterGroupsAction->setText(tr("Groups"));
2637 ui->searchContactText->setPlaceholderText(tr("Search Contacts"));
2638 updateFilterText();
2639
2640 ui->searchContactText->setPlaceholderText(tr("Search Contacts"));
2641 statusOnline->setText(tr("Online", "Button to set your status to 'Online'"));
2642 statusAway->setText(tr("Away", "Button to set your status to 'Away'"));
2643 statusBusy->setText(tr("Busy", "Button to set your status to 'Busy'"));
2644 actionLogout->setText(tr("Logout", "Tray action menu to logout user"));
2645 actionQuit->setText(tr("Exit", "Tray action menu to exit tox"));
2646 actionShow->setText(tr("Show", "Tray action menu to show qTox window"));
2647
2648 if (!settings.getSeparateWindow() && (settingsWidget && settingsWidget->isShown())) {
2649 setWindowTitle(fromDialogType(DialogType::SettingDialog));
2650 }
2651
2652 friendRequestsUpdate();
2653 groupInvitesUpdate();
2654
2655
2656 #ifdef Q_OS_MAC
2657 Nexus::getInstance().retranslateUi();
2658
2659 filterMenu->menuAction()->setText(tr("Filter..."));
2660
2661 fileMenu->setText(tr("File"));
2662 editMenu->setText(tr("Edit"));
2663 contactMenu->setText(tr("Contacts"));
2664 changeStatusMenu->menuAction()->setText(tr("Change Status"));
2665 editProfileAction->setText(tr("Edit Profile"));
2666 logoutAction->setText(tr("Log out"));
2667 addContactAction->setText(tr("Add Contact..."));
2668 nextConversationAction->setText(tr("Next Conversation"));
2669 previousConversationAction->setText(tr("Previous Conversation"));
2670 #endif
2671 }
2672
focusChatInput()2673 void Widget::focusChatInput()
2674 {
2675 if (activeChatroomWidget) {
2676 if (const Friend* f = activeChatroomWidget->getFriend()) {
2677 chatForms[f->getPublicKey()]->focusInput();
2678 } else if (Group* g = activeChatroomWidget->getGroup()) {
2679 groupChatForms[g->getPersistentId()]->focusInput();
2680 }
2681 }
2682 }
2683
refreshPeerListsLocal(const QString & username)2684 void Widget::refreshPeerListsLocal(const QString& username)
2685 {
2686 for (Group* g : GroupList::getAllGroups()) {
2687 g->updateUsername(core->getSelfPublicKey(), username);
2688 }
2689 }
2690
connectCircleWidget(CircleWidget & circleWidget)2691 void Widget::connectCircleWidget(CircleWidget& circleWidget)
2692 {
2693 connect(&circleWidget, &CircleWidget::searchCircle, this, &Widget::searchCircle);
2694 connect(&circleWidget, &CircleWidget::newContentDialog, this, &Widget::registerContentDialog);
2695 }
2696
connectFriendWidget(FriendWidget & friendWidget)2697 void Widget::connectFriendWidget(FriendWidget& friendWidget)
2698 {
2699 connect(&friendWidget, &FriendWidget::searchCircle, this, &Widget::searchCircle);
2700 connect(&friendWidget, &FriendWidget::updateFriendActivity, this, &Widget::updateFriendActivity);
2701 }
2702