From 18cee41cfb2a38fb63634d9898b9bba3b40ff462 Mon Sep 17 00:00:00 2001 From: "Tux3 / Mlkj / !Lev.uXFMLA" Date: Tue, 24 Jun 2014 22:11:11 +0200 Subject: [PATCH] Initial commit --- addfriendform.cpp | 45 ++ addfriendform.h | 35 ++ chatform.cpp | 153 ++++++ chatform.h | 57 +++ chattextedit.cpp | 19 + chattextedit.h | 20 + copyableelidelabel.cpp | 47 ++ copyableelidelabel.h | 35 ++ core.cpp | 568 ++++++++++++++++++++++ core.h | 216 ++++++++ editablelabelwidget.cpp | 110 +++++ editablelabelwidget.h | 66 +++ elidelabel.cpp | 82 ++++ elidelabel.h | 50 ++ esclineedit.cpp | 34 ++ esclineedit.h | 36 ++ friend.cpp | 33 ++ friend.h | 25 + friendlist.cpp | 32 ++ friendlist.h | 21 + friendrequestdialog.cpp | 61 +++ friendrequestdialog.h | 29 ++ friendwidget.cpp | 62 +++ friendwidget.h | 28 ++ group.cpp | 98 ++++ group.h | 37 ++ groupchatform.cpp | 175 +++++++ groupchatform.h | 56 +++ grouplist.cpp | 31 ++ grouplist.h | 22 + groupwidget.cpp | 75 +++ groupwidget.h | 29 ++ img/button icons/arrow_medgrey.png | Bin 0 -> 172 bytes img/button icons/arrow_medgrey.svg | 7 + img/button icons/arrow_medgrey_2x.png | Bin 0 -> 249 bytes img/button icons/arrow_white.png | Bin 0 -> 152 bytes img/button icons/arrow_white.svg | 7 + img/button icons/arrow_white_2x.png | Bin 0 -> 214 bytes img/button icons/attach.png | Bin 0 -> 274 bytes img/button icons/attach.svg | 38 ++ img/button icons/attach_2x.png | Bin 0 -> 432 bytes img/button icons/call.png | Bin 0 -> 307 bytes img/button icons/call.svg | 10 + img/button icons/call_2x.png | Bin 0 -> 510 bytes img/button icons/check.svg | 7 + img/button icons/emoticon.png | Bin 0 -> 220 bytes img/button icons/emoticon.svg | 10 + img/button icons/emoticon_2x.png | Bin 0 -> 323 bytes img/button icons/no.png | Bin 0 -> 178 bytes img/button icons/no.svg | 8 + img/button icons/no_2x.png | Bin 0 -> 265 bytes img/button icons/pause.png | Bin 0 -> 142 bytes img/button icons/pause.svg | 10 + img/button icons/pause_2x.png | Bin 0 -> 129 bytes img/button icons/sendmessage.png | Bin 0 -> 327 bytes img/button icons/sendmessage.svg | 10 + img/button icons/sendmessage_2x.png | Bin 0 -> 533 bytes img/button icons/video.png | Bin 0 -> 248 bytes img/button icons/video.svg | 10 + img/button icons/video_2x.png | Bin 0 -> 382 bytes img/button icons/yes.png | Bin 0 -> 183 bytes img/button icons/yes_2x.png | Bin 0 -> 267 bytes img/contact list icons/add.png | Bin 0 -> 164 bytes img/contact list icons/add.svg | 8 + img/contact list icons/add_2x.png | Bin 0 -> 192 bytes img/contact list icons/contact.png | Bin 0 -> 790 bytes img/contact list icons/group.png | Bin 0 -> 314 bytes img/contact list icons/group.svg | 20 + img/contact list icons/group_2x.png | Bin 0 -> 512 bytes img/contact list icons/settings.png | Bin 0 -> 291 bytes img/contact list icons/settings.svg | 19 + img/contact list icons/settings_2x.png | Bin 0 -> 474 bytes img/contact list icons/transfer.png | Bin 0 -> 288 bytes img/contact list icons/transfer.svg | 18 + img/contact list icons/transfer_2x.png | Bin 0 -> 433 bytes img/icon.ico | Bin 0 -> 370070 bytes img/icon.png | Bin 0 -> 1912 bytes img/status/dot_away.png | Bin 0 -> 407 bytes img/status/dot_away.svg | 36 ++ img/status/dot_away_2x.png | Bin 0 -> 411 bytes img/status/dot_away_notification.png | Bin 0 -> 472 bytes img/status/dot_away_notification.svg | 40 ++ img/status/dot_away_notification_2x.png | Bin 0 -> 852 bytes img/status/dot_idle.png | Bin 0 -> 240 bytes img/status/dot_idle.svg | 8 + img/status/dot_idle_2x.png | Bin 0 -> 376 bytes img/status/dot_idle_notification.png | Bin 0 -> 458 bytes img/status/dot_idle_notification.svg | 14 + img/status/dot_idle_notification_2x.png | Bin 0 -> 825 bytes img/status/dot_online.png | Bin 0 -> 272 bytes img/status/dot_online.svg | 7 + img/status/dot_online_2x.png | Bin 0 -> 282 bytes img/status/dot_online_notification.png | Bin 0 -> 424 bytes img/status/dot_online_notification.svg | 14 + img/status/dot_online_notification_2x.png | Bin 0 -> 766 bytes main.cpp | 17 + res.qrc | 5 + res/settings.ini | 55 +++ settings.cpp | 343 +++++++++++++ settings.h | 163 +++++++ settingsform.cpp | 50 ++ settingsform.h | 33 ++ status.cpp | 35 ++ status.h | 45 ++ toxgui.pro | 66 +++ widget.cpp | 396 +++++++++++++++ widget.h | 84 ++++ 107 files changed, 3980 insertions(+) create mode 100644 addfriendform.cpp create mode 100644 addfriendform.h create mode 100644 chatform.cpp create mode 100644 chatform.h create mode 100644 chattextedit.cpp create mode 100644 chattextedit.h create mode 100644 copyableelidelabel.cpp create mode 100644 copyableelidelabel.h create mode 100644 core.cpp create mode 100644 core.h create mode 100644 editablelabelwidget.cpp create mode 100644 editablelabelwidget.h create mode 100644 elidelabel.cpp create mode 100644 elidelabel.h create mode 100644 esclineedit.cpp create mode 100644 esclineedit.h create mode 100644 friend.cpp create mode 100644 friend.h create mode 100644 friendlist.cpp create mode 100644 friendlist.h create mode 100644 friendrequestdialog.cpp create mode 100644 friendrequestdialog.h create mode 100644 friendwidget.cpp create mode 100644 friendwidget.h create mode 100644 group.cpp create mode 100644 group.h create mode 100644 groupchatform.cpp create mode 100644 groupchatform.h create mode 100644 grouplist.cpp create mode 100644 grouplist.h create mode 100644 groupwidget.cpp create mode 100644 groupwidget.h create mode 100644 img/button icons/arrow_medgrey.png create mode 100644 img/button icons/arrow_medgrey.svg create mode 100644 img/button icons/arrow_medgrey_2x.png create mode 100644 img/button icons/arrow_white.png create mode 100644 img/button icons/arrow_white.svg create mode 100644 img/button icons/arrow_white_2x.png create mode 100644 img/button icons/attach.png create mode 100644 img/button icons/attach.svg create mode 100644 img/button icons/attach_2x.png create mode 100644 img/button icons/call.png create mode 100644 img/button icons/call.svg create mode 100644 img/button icons/call_2x.png create mode 100644 img/button icons/check.svg create mode 100644 img/button icons/emoticon.png create mode 100644 img/button icons/emoticon.svg create mode 100644 img/button icons/emoticon_2x.png create mode 100644 img/button icons/no.png create mode 100644 img/button icons/no.svg create mode 100644 img/button icons/no_2x.png create mode 100644 img/button icons/pause.png create mode 100644 img/button icons/pause.svg create mode 100644 img/button icons/pause_2x.png create mode 100644 img/button icons/sendmessage.png create mode 100644 img/button icons/sendmessage.svg create mode 100644 img/button icons/sendmessage_2x.png create mode 100644 img/button icons/video.png create mode 100644 img/button icons/video.svg create mode 100644 img/button icons/video_2x.png create mode 100644 img/button icons/yes.png create mode 100644 img/button icons/yes_2x.png create mode 100644 img/contact list icons/add.png create mode 100644 img/contact list icons/add.svg create mode 100644 img/contact list icons/add_2x.png create mode 100644 img/contact list icons/contact.png create mode 100644 img/contact list icons/group.png create mode 100644 img/contact list icons/group.svg create mode 100644 img/contact list icons/group_2x.png create mode 100644 img/contact list icons/settings.png create mode 100644 img/contact list icons/settings.svg create mode 100644 img/contact list icons/settings_2x.png create mode 100644 img/contact list icons/transfer.png create mode 100644 img/contact list icons/transfer.svg create mode 100644 img/contact list icons/transfer_2x.png create mode 100644 img/icon.ico create mode 100644 img/icon.png create mode 100644 img/status/dot_away.png create mode 100644 img/status/dot_away.svg create mode 100644 img/status/dot_away_2x.png create mode 100644 img/status/dot_away_notification.png create mode 100644 img/status/dot_away_notification.svg create mode 100644 img/status/dot_away_notification_2x.png create mode 100644 img/status/dot_idle.png create mode 100644 img/status/dot_idle.svg create mode 100644 img/status/dot_idle_2x.png create mode 100644 img/status/dot_idle_notification.png create mode 100644 img/status/dot_idle_notification.svg create mode 100644 img/status/dot_idle_notification_2x.png create mode 100644 img/status/dot_online.png create mode 100644 img/status/dot_online.svg create mode 100644 img/status/dot_online_2x.png create mode 100644 img/status/dot_online_notification.png create mode 100644 img/status/dot_online_notification.svg create mode 100644 img/status/dot_online_notification_2x.png create mode 100644 main.cpp create mode 100644 res.qrc create mode 100644 res/settings.ini create mode 100644 settings.cpp create mode 100644 settings.h create mode 100644 settingsform.cpp create mode 100644 settingsform.h create mode 100644 status.cpp create mode 100644 status.h create mode 100644 toxgui.pro create mode 100644 widget.cpp create mode 100644 widget.h diff --git a/addfriendform.cpp b/addfriendform.cpp new file mode 100644 index 000000000..2c83c4a31 --- /dev/null +++ b/addfriendform.cpp @@ -0,0 +1,45 @@ +#include "addfriendform.h" +#include "ui_widget.h" +#include + +AddFriendForm::AddFriendForm() +{ + main = new QWidget(), head = new QWidget(); + QFont bold; + bold.setBold(true); + headLabel.setText("Add Friends"); + headLabel.setFont(bold); + + toxIdLabel.setText("Tox ID"); + messageLabel.setText("Message"); + sendButton.setText("Send friend request"); + + main->setLayout(&layout); + layout.addWidget(&toxIdLabel); + layout.addWidget(&toxId); + layout.addWidget(&messageLabel); + layout.addWidget(&message); + layout.addWidget(&sendButton); + + head->setLayout(&headLayout); + headLayout.addWidget(&headLabel); + + connect(&sendButton, SIGNAL(clicked()), this, SLOT(onSendTriggered())); +} + +void AddFriendForm::show(Ui::Widget &ui) +{ + ui.mainContent->layout()->addWidget(main); + ui.mainHead->layout()->addWidget(head); + main->show(); + head->show(); +} + +void AddFriendForm::onSendTriggered() +{ + QString id = toxId.text(), msg = message.toPlainText(); + if (id.isEmpty()) + return; + + emit friendRequested(id, msg); +} diff --git a/addfriendform.h b/addfriendform.h new file mode 100644 index 000000000..72fb0fd9f --- /dev/null +++ b/addfriendform.h @@ -0,0 +1,35 @@ +#ifndef ADDFRIENDFORM_H +#define ADDFRIENDFORM_H + +#include +#include +#include +#include +#include + +#include "ui_widget.h" + +class AddFriendForm : public QObject +{ + Q_OBJECT +public: + AddFriendForm(); + + void show(Ui::Widget& ui); + +signals: + void friendRequested(const QString& friendAddress, const QString& message); + +private slots: + void onSendTriggered(); + +private: + QLabel headLabel, toxIdLabel, messageLabel; + QPushButton sendButton; + QLineEdit toxId; + QTextEdit message; + QVBoxLayout layout, headLayout; + QWidget *head, *main; +}; + +#endif // ADDFRIENDFORM_H diff --git a/chatform.cpp b/chatform.cpp new file mode 100644 index 000000000..1a7ebab82 --- /dev/null +++ b/chatform.cpp @@ -0,0 +1,153 @@ +#include "chatform.h" +#include "friend.h" +#include "friendwidget.h" +#include "widget.h" +#include +#include +#include + +ChatForm::ChatForm(Friend* chatFriend) + : f(chatFriend), curRow{0}, lockSliderToBottom{true} +{ + main = new QWidget(), head = new QWidget(), chatAreaWidget = new QWidget(); + name = new QLabel(), avatar = new QLabel(), statusMessage = new QLabel(); + headLayout = new QHBoxLayout(), mainFootLayout = new QHBoxLayout(); + headTextLayout = new QVBoxLayout(), mainLayout = new QVBoxLayout(); + mainChatLayout = new QGridLayout(); + msgEdit = new ChatTextEdit(); + sendButton = new QPushButton(); + chatArea = new QScrollArea(); + + QFont bold; + bold.setBold(true); + name->setText(chatFriend->widget->name.text()); + name->setFont(bold); + statusMessage->setText(chatFriend->widget->statusMessage.text()); + avatar->setPixmap(*chatFriend->widget->avatar.pixmap()); + + chatAreaWidget->setLayout(mainChatLayout); + chatArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); + chatArea->setWidgetResizable(true); + mainChatLayout->setColumnStretch(1,1); + mainChatLayout->setSpacing(10); + + sendButton->setIcon(QIcon("img/button icons/sendmessage_2x.png")); + sendButton->setFlat(true); + QPalette pal; + pal.setColor(QPalette::Button, QColor(107,194,96)); // Tox Green + sendButton->setPalette(pal); + sendButton->setAutoFillBackground(true); + msgEdit->setFixedHeight(50); + sendButton->setFixedSize(50, 50); + + main->setLayout(mainLayout); + mainLayout->addWidget(chatArea); + mainLayout->addLayout(mainFootLayout); + mainLayout->setMargin(0); + + mainFootLayout->addWidget(msgEdit); + mainFootLayout->addWidget(sendButton); + + head->setLayout(headLayout); + headLayout->addWidget(avatar); + headLayout->addLayout(headTextLayout); + headLayout->addStretch(); + + headTextLayout->addStretch(); + headTextLayout->addWidget(name); + headTextLayout->addWidget(statusMessage); + headTextLayout->addStretch(); + + chatArea->setWidget(chatAreaWidget); + + connect(sendButton, SIGNAL(clicked()), this, SLOT(onSendTriggered())); + connect(msgEdit, SIGNAL(enterPressed()), this, SLOT(onSendTriggered())); + connect(chatArea->verticalScrollBar(), SIGNAL(rangeChanged(int,int)), this, SLOT(onSliderRangeChanged())); +} + +ChatForm::~ChatForm() +{ + delete main; + delete head; +} + +void ChatForm::show(Ui::Widget &ui) +{ + ui.mainContent->layout()->addWidget(main); + ui.mainHead->layout()->addWidget(head); + main->show(); + head->show(); +} + +void ChatForm::setName(QString newName) +{ + name->setText(newName); +} + +void ChatForm::setStatusMessage(QString newMessage) +{ + statusMessage->setText(newMessage); +} + +void ChatForm::onSendTriggered() +{ + QString msg = msgEdit->toPlainText(); + if (msg.isEmpty()) + return; + QString name = Widget::getInstance()->getUsername(); + msgEdit->clear(); + addMessage(name, msg); + emit sendMessage(f->friendId, msg); +} + +void ChatForm::addFriendMessage(QString message) +{ + QLabel *msgAuthor = new QLabel(name->text()); + QLabel *msgText = new QLabel(message); + QLabel *msgDate = new QLabel(QTime::currentTime().toString("hh:mm")); + + addMessage(msgAuthor, msgText, msgDate); +} + +void ChatForm::addMessage(QString author, QString message, QString date) +{ + addMessage(new QLabel(author), new QLabel(message), new QLabel(date)); +} + +void ChatForm::addMessage(QLabel* author, QLabel* message, QLabel* date) +{ + QScrollBar* scroll = chatArea->verticalScrollBar(); + lockSliderToBottom = scroll && scroll->value() == scroll->maximum(); + author->setAlignment(Qt::AlignTop | Qt::AlignRight); + date->setAlignment(Qt::AlignTop); + if (author->text() == Widget::getInstance()->getUsername()) + { + QPalette pal; + pal.setColor(QPalette::WindowText, Qt::gray); + author->setPalette(pal); + message->setPalette(pal); + } + if (previousName.isEmpty() || previousName != author->text()) + { + if (curRow) + { + mainChatLayout->setRowStretch(curRow, 0); + mainChatLayout->addItem(new QSpacerItem(0,AUTHOR_CHANGE_SPACING),curRow,0,1,3); + curRow++; + } + mainChatLayout->addWidget(author, curRow, 0); + } + previousName = author->text(); + mainChatLayout->addWidget(message, curRow, 1); + mainChatLayout->addWidget(date, curRow, 3); + mainChatLayout->setRowStretch(curRow+1, 1); + mainChatLayout->setRowStretch(curRow, 0); + curRow++; +} + +void ChatForm::onSliderRangeChanged() +{ + QScrollBar* scroll = chatArea->verticalScrollBar(); + if (lockSliderToBottom) + scroll->setValue(scroll->maximum()); +} diff --git a/chatform.h b/chatform.h new file mode 100644 index 000000000..defafe7d2 --- /dev/null +++ b/chatform.h @@ -0,0 +1,57 @@ +#ifndef CHATFORM_H +#define CHATFORM_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "chattextedit.h" +#include "ui_widget.h" + +// Spacing in px inserted when the author of the last message changes +#define AUTHOR_CHANGE_SPACING 5 + +class Friend; + +class ChatForm : public QObject +{ + Q_OBJECT +public: + ChatForm(Friend* chatFriend); + ~ChatForm(); + void show(Ui::Widget& ui); + void setName(QString newName); + void setStatusMessage(QString newMessage); + void addFriendMessage(QString message); + void addMessage(QString author, QString message, QString date=QTime::currentTime().toString("hh:mm")); + void addMessage(QLabel* author, QLabel* message, QLabel* date); + + +signals: + void sendMessage(int, QString); + +private slots: + void onSendTriggered(); + void onSliderRangeChanged(); + +private: + Friend* f; + QHBoxLayout *headLayout, *mainFootLayout; + QVBoxLayout *headTextLayout, *mainLayout; + QGridLayout *mainChatLayout; + QLabel *avatar, *name, *statusMessage; + ChatTextEdit *msgEdit; + QPushButton *sendButton; + QScrollArea *chatArea; + QWidget *main, *head, *chatAreaWidget; + QString previousName; + int curRow; + bool lockSliderToBottom; +}; + +#endif // CHATFORM_H diff --git a/chattextedit.cpp b/chattextedit.cpp new file mode 100644 index 000000000..f5bf7cf6a --- /dev/null +++ b/chattextedit.cpp @@ -0,0 +1,19 @@ +#include "chattextedit.h" +#include + +ChatTextEdit::ChatTextEdit(QWidget *parent) : + QTextEdit(parent) +{ +} + +void ChatTextEdit::keyPressEvent(QKeyEvent * event) +{ + int key = event->key(); + if ((key == Qt::Key_Enter || key == Qt::Key_Return) + && !(event->modifiers() && Qt::ShiftModifier)) + { + emit enterPressed(); + return; + } + QTextEdit::keyPressEvent(event); +} diff --git a/chattextedit.h b/chattextedit.h new file mode 100644 index 000000000..338ff09ed --- /dev/null +++ b/chattextedit.h @@ -0,0 +1,20 @@ +#ifndef CHATTEXTEDIT_H +#define CHATTEXTEDIT_H + +#include + +class ChatTextEdit : public QTextEdit +{ + Q_OBJECT +public: + explicit ChatTextEdit(QWidget *parent = 0); + virtual void keyPressEvent(QKeyEvent * event) override; + +signals: + void enterPressed(); + +public slots: + +}; + +#endif // CHATTEXTEDIT_H diff --git a/copyableelidelabel.cpp b/copyableelidelabel.cpp new file mode 100644 index 000000000..39927db69 --- /dev/null +++ b/copyableelidelabel.cpp @@ -0,0 +1,47 @@ +/* + Copyright (C) 2013 by Maxim Biro + + This file is part of Tox Qt GUI. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + See the COPYING file for more details. +*/ + +#include "copyableelidelabel.h" + +#include +#include +#include + +CopyableElideLabel::CopyableElideLabel(QWidget* parent) : + ElideLabel(parent) +{ + setContextMenuPolicy(Qt::CustomContextMenu); + connect(this, &CopyableElideLabel::customContextMenuRequested, this, &CopyableElideLabel::showContextMenu); + + actionCopy = new QAction(tr("Copy"), this); + connect(actionCopy, &QAction::triggered, [this]() { + QApplication::clipboard()->setText(text()); + }); +} + +void CopyableElideLabel::showContextMenu(const QPoint& pos) +{ + if (text().length() == 0) { + return; + } + + QPoint globalPos = mapToGlobal(pos); + + QMenu contextMenu; + contextMenu.addAction(actionCopy); + + contextMenu.exec(globalPos); +} diff --git a/copyableelidelabel.h b/copyableelidelabel.h new file mode 100644 index 000000000..de7c53c08 --- /dev/null +++ b/copyableelidelabel.h @@ -0,0 +1,35 @@ +/* + Copyright (C) 2013 by Maxim Biro + + This file is part of Tox Qt GUI. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + See the COPYING file for more details. +*/ + +#ifndef COPYABLEELIDELABEL_HPP +#define COPYABLEELIDELABEL_HPP + +#include "elidelabel.h" + +class CopyableElideLabel : public ElideLabel +{ +public: + explicit CopyableElideLabel(QWidget* parent = 0); + +private: + QAction* actionCopy; + +private slots: + void showContextMenu(const QPoint& pos); + +}; + +#endif // COPYABLEELIDELABEL_HPP diff --git a/core.cpp b/core.cpp new file mode 100644 index 000000000..69f90cba2 --- /dev/null +++ b/core.cpp @@ -0,0 +1,568 @@ +/* + Copyright (C) 2013 by Maxim Biro + + This file is part of Tox Qt GUI. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + See the COPYING file for more details. +*/ + +#include "core.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "settings.h" + +const QString Core::CONFIG_FILE_NAME = "tox_save"; + +Core::Core() : + tox(nullptr) +{ + toxTimer = new QTimer(this); + toxTimer->setSingleShot(true); + saveTimer = new QTimer(this); + saveTimer->start(TOX_SAVE_INTERVAL); + connect(toxTimer, &QTimer::timeout, this, &Core::process); + connect(saveTimer, &QTimer::timeout, this, &Core::saveConfiguration); + connect(&Settings::getInstance(), &Settings::dhtServerListChanged, this, &Core::bootstrapDht); +} + +Core::~Core() +{ + if (tox) { + saveConfiguration(); + tox_kill(tox); + } +} + +void Core::onFriendRequest(Tox*/* tox*/, const uint8_t* cUserId, const uint8_t* cMessage, uint16_t cMessageSize, void* core) +{ + emit static_cast(core)->friendRequestReceived(CUserId::toString(cUserId), CString::toString(cMessage, cMessageSize)); +} + +void Core::onFriendMessage(Tox*/* tox*/, int friendId, uint8_t* cMessage, uint16_t cMessageSize, void* core) +{ + emit static_cast(core)->friendMessageReceived(friendId, CString::toString(cMessage, cMessageSize)); +} + +void Core::onFriendNameChange(Tox*/* tox*/, int friendId, uint8_t* cName, uint16_t cNameSize, void* core) +{ + emit static_cast(core)->friendUsernameChanged(friendId, CString::toString(cName, cNameSize)); +} + +void Core::onFriendTypingChange(Tox*/* tox*/, int friendId, uint8_t isTyping, void *core) +{ + emit static_cast(core)->friendTypingChanged(friendId, isTyping ? true : false); +} + +void Core::onStatusMessageChanged(Tox*/* tox*/, int friendId, uint8_t* cMessage, uint16_t cMessageSize, void* core) +{ + emit static_cast(core)->friendStatusMessageChanged(friendId, CString::toString(cMessage, cMessageSize)); +} + +void Core::onUserStatusChanged(Tox*/* tox*/, int friendId, uint8_t userstatus, void* core) +{ + Status status; + switch (userstatus) { + case TOX_USERSTATUS_NONE: + status = Status::Online; + break; + case TOX_USERSTATUS_AWAY: + status = Status::Away; + break; + case TOX_USERSTATUS_BUSY: + status = Status::Busy; + break; + default: + status = Status::Online; + break; + } + emit static_cast(core)->friendStatusChanged(friendId, status); +} + +void Core::onConnectionStatusChanged(Tox*/* tox*/, int friendId, uint8_t status, void* core) +{ + Status friendStatus = status ? Status::Online : Status::Offline; + emit static_cast(core)->friendStatusChanged(friendId, friendStatus); + if (friendStatus == Status::Offline) { + static_cast(core)->checkLastOnline(friendId); + } +} + +void Core::onAction(Tox*/* tox*/, int friendId, uint8_t *cMessage, uint16_t cMessageSize, void *core) +{ + emit static_cast(core)->actionReceived(friendId, CString::toString(cMessage, cMessageSize)); +} + +void Core::onGroupInvite(Tox*, int friendnumber, uint8_t *group_public_key, void *core) +{ + qDebug() << QString("Core: Group invite by %1").arg(friendnumber); + emit static_cast(core)->groupInviteReceived(friendnumber, group_public_key); +} + +void Core::onGroupMessage(Tox*, int groupnumber, int friendgroupnumber, uint8_t * message, uint16_t length, void *core) +{ + emit static_cast(core)->groupMessageReceived(groupnumber, friendgroupnumber, CString::toString(message, length)); +} + +void Core::onGroupNamelistChange(Tox*, int groupnumber, int peernumber, uint8_t change, void *core) +{ + qDebug() << QString("Core: Group namelist change %1:%2 %3").arg(groupnumber).arg(peernumber).arg(change); + emit static_cast(core)->groupNamelistChanged(groupnumber, peernumber, change); +} + +void Core::acceptFriendRequest(const QString& userId) +{ + int friendId = tox_add_friend_norequest(tox, CUserId(userId).data()); + if (friendId == -1) { + emit failedToAddFriend(userId); + } else { + emit friendAdded(friendId, userId); + } +} + +void Core::requestFriendship(const QString& friendAddress, const QString& message) +{ + CString cMessage(message); + + int friendId = tox_add_friend(tox, CFriendAddress(friendAddress).data(), cMessage.data(), cMessage.size()); + const QString userId = friendAddress.mid(0, TOX_CLIENT_ID_SIZE * 2); + // TODO: better error handling + if (friendId < 0) { + emit failedToAddFriend(userId); + } else { + emit friendAdded(friendId, userId); + } +} + +void Core::sendMessage(int friendId, const QString& message) +{ + CString cMessage(message); + + int messageId = tox_send_message(tox, friendId, cMessage.data(), cMessage.size()); + emit messageSentResult(friendId, message, messageId); +} + +void Core::sendAction(int friendId, const QString &action) +{ + CString cMessage(action); + int ret = tox_send_action(tox, friendId, cMessage.data(), cMessage.size()); + emit actionSentResult(friendId, action, ret); +} + +void Core::sendTyping(int friendId, bool typing) +{ + int ret = tox_set_user_is_typing(tox, friendId, typing); + if (ret == -1) + emit failedToSetTyping(typing); +} + +void Core::sendGroupMessage(int groupId, const QString& message) +{ + CString cMessage(message); + + tox_group_message_send(tox, groupId, cMessage.data(), cMessage.size()); +} + +void Core::removeFriend(int friendId) +{ + if (tox_del_friend(tox, friendId) == -1) { + emit failedToRemoveFriend(friendId); + } else { + emit friendRemoved(friendId); + } +} + +void Core::removeGroup(int groupId) +{ + tox_del_groupchat(tox, groupId); +} + +void Core::setUsername(const QString& username) +{ + CString cUsername(username); + + if (tox_set_name(tox, cUsername.data(), cUsername.size()) == -1) { + emit failedToSetUsername(username); + } else { + emit usernameSet(username); + } +} + +void Core::setStatusMessage(const QString& message) +{ + CString cMessage(message); + + if (tox_set_status_message(tox, cMessage.data(), cMessage.size()) == -1) { + emit failedToSetStatusMessage(message); + } else { + emit statusMessageSet(message); + } +} + +void Core::setStatus(Status status) +{ + TOX_USERSTATUS userstatus; + switch (status) { + case Status::Online: + userstatus = TOX_USERSTATUS_NONE; + break; + case Status::Away: + userstatus = TOX_USERSTATUS_AWAY; + break; + case Status::Busy: + userstatus = TOX_USERSTATUS_BUSY; + break; + default: + userstatus = TOX_USERSTATUS_INVALID; + break; + } + + if (tox_set_user_status(tox, userstatus) == 0) { + emit statusSet(status); + } else { + emit failedToSetStatus(status); + } +} + +void Core::bootstrapDht() +{ + const Settings& s = Settings::getInstance(); + QList dhtServerList = s.getDhtServerList(); + + for (const Settings::DhtServer& dhtServer : dhtServerList) { + tox_bootstrap_from_address(tox, dhtServer.address.toLatin1().data(), 0, qToBigEndian(dhtServer.port), CUserId(dhtServer.userId).data()); + } +} + +void Core::process() +{ + tox_do(tox); +#ifdef DEBUG + //we want to see the debug messages immediately + fflush(stdout); +#endif + checkConnection(); + if (!tox_isconnected(tox)) + bootstrapDht(); + toxTimer->start(tox_do_interval(tox)); +} + +void Core::checkConnection() +{ + static bool isConnected = false; + + if (tox_isconnected(tox) && !isConnected) { + emit connected(); + isConnected = true; + } else if (!tox_isconnected(tox) && isConnected) { + emit disconnected(); + isConnected = false; + } +} + +void Core::loadConfiguration() +{ + QString path = Settings::getSettingsDirPath() + '/' + CONFIG_FILE_NAME; + + QFile configurationFile(path); + + if (!configurationFile.exists()) { + qWarning() << "The Tox configuration file was not found"; + return; + } + + if (!configurationFile.open(QIODevice::ReadOnly)) { + qCritical() << "File " << path << " cannot be opened"; + return; + } + + qint64 fileSize = configurationFile.size(); + if (fileSize > 0) { + QByteArray data = configurationFile.readAll(); + tox_load(tox, reinterpret_cast(data.data()), data.size()); + } + + configurationFile.close(); + + loadFriends(); +} + +void Core::saveConfiguration() +{ + QString path = Settings::getSettingsDirPath(); + + QDir directory(path); + + if (!directory.exists() && !directory.mkpath(directory.absolutePath())) { + qCritical() << "Error while creating directory " << path; + return; + } + + path += '/' + CONFIG_FILE_NAME; + QSaveFile configurationFile(path); + if (!configurationFile.open(QIODevice::WriteOnly)) { + qCritical() << "File " << path << " cannot be opened"; + return; + } + + qDebug() << "Core: writing tox_save"; + uint32_t fileSize = tox_size(tox); + if (fileSize > 0 && fileSize <= INT32_MAX) { + uint8_t *data = new uint8_t[fileSize]; + tox_save(tox, data); + configurationFile.write(reinterpret_cast(data), fileSize); + configurationFile.commit(); + delete[] data; + } +} + +void Core::loadFriends() +{ + const uint32_t friendCount = tox_count_friendlist(tox); + if (friendCount > 0) { + // assuming there are not that many friends to fill up the whole stack + int32_t *ids = new int32_t[friendCount]; + tox_get_friendlist(tox, ids, friendCount); + uint8_t clientId[TOX_CLIENT_ID_SIZE]; + for (int32_t i = 0; i < static_cast(friendCount); ++i) { + if (tox_get_client_id(tox, ids[i], clientId) == 0) { + emit friendAdded(ids[i], CUserId::toString(clientId)); + + const int nameSize = tox_get_name_size(tox, ids[i]); + if (nameSize > 0) { + uint8_t *name = new uint8_t[nameSize]; + if (tox_get_name(tox, ids[i], name) == nameSize) { + emit friendUsernameLoaded(ids[i], CString::toString(name, nameSize)); + } + delete[] name; + } + + const int statusMessageSize = tox_get_status_message_size(tox, ids[i]); + if (statusMessageSize > 0) { + uint8_t *statusMessage = new uint8_t[statusMessageSize]; + if (tox_get_status_message(tox, ids[i], statusMessage, statusMessageSize) == statusMessageSize) { + emit friendStatusMessageLoaded(ids[i], CString::toString(statusMessage, statusMessageSize)); + } + delete[] statusMessage; + } + + checkLastOnline(ids[i]); + } + + } + delete[] ids; + } +} + +void Core::checkLastOnline(int friendId) { + const uint64_t lastOnline = tox_get_last_online(tox, friendId); + if (lastOnline > 0) { + emit friendLastSeenChanged(friendId, QDateTime::fromTime_t(lastOnline)); + } +} + +void Core::start() +{ + tox = tox_new(0); + + if (tox == nullptr) { + qCritical() << "Core failed to start"; + emit failedToStart(); + return; + } + + loadConfiguration(); + + tox_callback_friend_request(tox, onFriendRequest, this); + tox_callback_friend_message(tox, onFriendMessage, this); + tox_callback_friend_action(tox, onAction, this); + tox_callback_name_change(tox, onFriendNameChange, this); + tox_callback_typing_change(tox, onFriendTypingChange, this); + tox_callback_status_message(tox, onStatusMessageChanged, this); + tox_callback_user_status(tox, onUserStatusChanged, this); + tox_callback_connection_status(tox, onConnectionStatusChanged, this); + tox_callback_group_invite(tox, onGroupInvite, this); + tox_callback_group_message(tox, onGroupMessage, this); + tox_callback_group_namelist_change(tox, onGroupNamelistChange, this); + + uint8_t friendAddress[TOX_FRIEND_ADDRESS_SIZE]; + tox_get_address(tox, friendAddress); + + emit friendAddressGenerated(CFriendAddress::toString(friendAddress)); + + CString cUsername(Settings::getInstance().getUsername()); + tox_set_name(tox, cUsername.data(), cUsername.size()); + + CString cStatusMessage(Settings::getInstance().getStatusMessage()); + tox_set_status_message(tox, cStatusMessage.data(), cStatusMessage.size()); + + bootstrapDht(); + + toxTimer->start(tox_do_interval(tox)); +} + +int Core::getGroupNumberPeers(int groupId) const +{ + return tox_group_number_peers(tox, groupId); +} + +QString Core::getGroupPeerName(int groupId, int peerId) const +{ + QString name; + uint8_t nameArray[TOX_MAX_NAME_LENGTH]; + int length = tox_group_peername(tox, groupId, peerId, nameArray); + if (length == -1) + { + qWarning() << "Core::getGroupPeerName: Unknown error"; + return name; + } + name = CString::toString(nameArray, length); + return name; +} + +QList Core::getGroupPeerNames(int groupId) const +{ + QList names; + int nPeers = getGroupNumberPeers(groupId); + if (nPeers == -1) + { + qWarning() << "Core::getGroupPeerNames: Unable to get number of peers"; + return names; + } + uint8_t namesArray[nPeers][TOX_MAX_NAME_LENGTH]; + uint16_t* lengths = new uint16_t[nPeers]; + int result = tox_group_get_names(tox, groupId, namesArray, lengths, nPeers); + if (result != nPeers) + { + qWarning() << "Core::getGroupPeerNames: Unexpected result"; + return names; + } + for (int i=0; i(cData), cDataSize).toHex()).toUpper(); +} + +uint16_t Core::CData::fromString(const QString& data, uint8_t* cData) +{ + QByteArray arr = QByteArray::fromHex(data.toLower().toLatin1()); + memcpy(cData, reinterpret_cast(arr.data()), arr.size()); + return arr.size(); +} + + +// CUserId + +Core::CUserId::CUserId(const QString &userId) : + CData(userId, SIZE) +{ + // intentionally left empty +} + +QString Core::CUserId::toString(const uint8_t* cUserId) +{ + return CData::toString(cUserId, SIZE); +} + + +// CFriendAddress + +Core::CFriendAddress::CFriendAddress(const QString &friendAddress) : + CData(friendAddress, SIZE) +{ + // intentionally left empty +} + +QString Core::CFriendAddress::toString(const uint8_t *cFriendAddress) +{ + return CData::toString(cFriendAddress, SIZE); +} + + +// CString + +Core::CString::CString(const QString& string) +{ + cString = new uint8_t[string.length() * MAX_SIZE_OF_UTF8_ENCODED_CHARACTER](); + cStringSize = fromString(string, cString); +} + +Core::CString::~CString() +{ + delete[] cString; +} + +uint8_t* Core::CString::data() +{ + return cString; +} + +uint16_t Core::CString::size() +{ + return cStringSize; +} + +QString Core::CString::toString(const uint8_t* cString, uint16_t cStringSize) +{ + return QString::fromUtf8(reinterpret_cast(cString), cStringSize); +} + +uint16_t Core::CString::fromString(const QString& string, uint8_t* cString) +{ + QByteArray byteArray = QByteArray(string.toUtf8()); + memcpy(cString, reinterpret_cast(byteArray.data()), byteArray.size()); + return byteArray.size(); +} + +void Core::quitGroupChat(int groupId) const +{ + tox_del_groupchat(tox, groupId); +} diff --git a/core.h b/core.h new file mode 100644 index 000000000..5f9c3da1b --- /dev/null +++ b/core.h @@ -0,0 +1,216 @@ +/* + Copyright (C) 2013 by Maxim Biro + + This file is part of Tox Qt GUI. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + See the COPYING file for more details. +*/ + +#ifndef CORE_HPP +#define CORE_HPP + +#include "status.h" + +#include + +#include +#include +#include +#include +#include + +#define GROUPCHAT_MAX_SIZE 32 +#define TOX_SAVE_INTERVAL 10*1000 + +struct DhtServer +{ + QString name; + QString userId; + QString address; + int port; +}; + +class Core : public QObject +{ + Q_OBJECT +public: + explicit Core(); + ~Core(); + +private: + static void onFriendRequest(Tox* tox, const uint8_t* cUserId, const uint8_t* cMessage, uint16_t cMessageSize, void* core); + static void onFriendMessage(Tox* tox, int friendId, uint8_t* cMessage, uint16_t cMessageSize, void* core); + static void onFriendNameChange(Tox* tox, int friendId, uint8_t* cName, uint16_t cNameSize, void* core); + static void onFriendTypingChange(Tox* tox, int friendId, uint8_t isTyping, void* core); + static void onStatusMessageChanged(Tox* tox, int friendId, uint8_t* cMessage, uint16_t cMessageSize, void* core); + static void onUserStatusChanged(Tox* tox, int friendId, uint8_t userstatus, void* core); + static void onConnectionStatusChanged(Tox* tox, int friendId, uint8_t status, void* core); + static void onAction(Tox* tox, int friendId, uint8_t* cMessage, uint16_t cMessageSize, void* core); + static void onGroupInvite(Tox *tox, int friendnumber, uint8_t *group_public_key, void *userdata); + static void onGroupMessage(Tox *tox, int groupnumber, int friendgroupnumber, uint8_t * message, uint16_t length, void *userdata); + static void onGroupNamelistChange(Tox *tox, int groupnumber, int peernumber, uint8_t change, void *userdata); + + void checkConnection(); + + void loadConfiguration(); + void saveConfiguration(); + void loadFriends(); + + void checkLastOnline(int friendId); + + Tox* tox; + QTimer *toxTimer, *saveTimer; + QList dhtServerList; + int dhtServerId; + + static const QString CONFIG_FILE_NAME; + + class CData + { + public: + uint8_t* data(); + uint16_t size(); + + protected: + explicit CData(const QString& data, uint16_t byteSize); + virtual ~CData(); + + static QString toString(const uint8_t* cData, const uint16_t cDataSize); + + private: + uint8_t* cData; + uint16_t cDataSize; + + static uint16_t fromString(const QString& userId, uint8_t* cData); + }; + + class CUserId : public CData + { + public: + explicit CUserId(const QString& userId); + + static QString toString(const uint8_t *cUserId); + + private: + static const uint16_t SIZE = TOX_CLIENT_ID_SIZE; + + }; + + class CFriendAddress : public CData + { + public: + explicit CFriendAddress(const QString& friendAddress); + + static QString toString(const uint8_t* cFriendAddress); + + private: + static const uint16_t SIZE = TOX_FRIEND_ADDRESS_SIZE; + + }; + + class CString + { + public: + explicit CString(const QString& string); + ~CString(); + + uint8_t* data(); + uint16_t size(); + + static QString toString(const uint8_t* cMessage, const uint16_t cMessageSize); + + private: + const static int MAX_SIZE_OF_UTF8_ENCODED_CHARACTER = 4; + + uint8_t* cString; + uint16_t cStringSize; + + static uint16_t fromString(const QString& message, uint8_t* cMessage); + }; + +public: + int getGroupNumberPeers(int groupId) const; + QString getGroupPeerName(int groupId, int peerId) const; + QList getGroupPeerNames(int groupId) const; + int joinGroupchat(int32_t friendnumber, uint8_t* friend_group_public_key) const; + void quitGroupChat(int groupId) const; + +public slots: + void start(); + + void acceptFriendRequest(const QString& userId); + void requestFriendship(const QString& friendAddress, const QString& message); + + void removeFriend(int friendId); + void removeGroup(int groupId); + + void sendMessage(int friendId, const QString& message); + void sendAction(int friendId, const QString& action); + void sendTyping(int friendId, bool typing); + void sendGroupMessage(int groupId, const QString& message); + + void setUsername(const QString& username); + void setStatusMessage(const QString& message); + void setStatus(Status status); + + void process(); + + void bootstrapDht(); + +signals: + void connected(); + void disconnected(); + + void friendRequestReceived(const QString& userId, const QString& message); + void friendMessageReceived(int friendId, const QString& message); + + void friendAdded(int friendId, const QString& userId); + + void friendStatusChanged(int friendId, Status status); + void friendStatusMessageChanged(int friendId, const QString& message); + void friendUsernameChanged(int friendId, const QString& username); + void friendTypingChanged(int friendId, bool isTyping); + + void friendStatusMessageLoaded(int friendId, const QString& message); + void friendUsernameLoaded(int friendId, const QString& username); + + void friendAddressGenerated(const QString& friendAddress); + + void friendRemoved(int friendId); + + void friendLastSeenChanged(int friendId, const QDateTime& dateTime); + + void groupInviteReceived(int friendnumber, uint8_t *group_public_key); + void groupMessageReceived(int groupnumber, int friendgroupnumber, const QString& message); + void groupNamelistChanged(int groupnumber, int peernumber, uint8_t change); + + void usernameSet(const QString& username); + void statusMessageSet(const QString& message); + void statusSet(Status status); + + void messageSentResult(int friendId, const QString& message, int messageId); + void actionSentResult(int friendId, const QString& action, int success); + + void failedToAddFriend(const QString& userId); + void failedToRemoveFriend(int friendId); + void failedToSetUsername(const QString& username); + void failedToSetStatusMessage(const QString& message); + void failedToSetStatus(Status status); + void failedToSetTyping(bool typing); + + void actionReceived(int friendId, const QString& acionMessage); + + void failedToStart(); + +}; + +#endif // CORE_HPP + diff --git a/editablelabelwidget.cpp b/editablelabelwidget.cpp new file mode 100644 index 000000000..880f76a2e --- /dev/null +++ b/editablelabelwidget.cpp @@ -0,0 +1,110 @@ +/* + Copyright (C) 2013 by Maxim Biro + + This file is part of Tox Qt GUI. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + See the COPYING file for more details. +*/ + +#include "editablelabelwidget.h" + +#include +#include +#include +#include +#include + +ClickableCopyableElideLabel::ClickableCopyableElideLabel(QWidget* parent) : + CopyableElideLabel(parent) +{ +} + +bool ClickableCopyableElideLabel::event(QEvent* event) +{ + if (event->type() == QEvent::MouseButtonRelease) { + QMouseEvent* mouseEvent = static_cast(event); + if (mouseEvent->button() == Qt::LeftButton) { + emit clicked(); + } + } else if (event->type() == QEvent::Enter) { + QApplication::setOverrideCursor(QCursor(Qt::PointingHandCursor)); + } else if (event->type() == QEvent::Leave) { + QApplication::restoreOverrideCursor(); + } + + return CopyableElideLabel::event(event); +} + +EditableLabelWidget::EditableLabelWidget(QWidget* parent) : + QStackedWidget(parent), isSubmitting(false) +{ + label = new ClickableCopyableElideLabel(this); + + connect(label, &ClickableCopyableElideLabel::clicked, this, &EditableLabelWidget::onLabelClicked); + + lineEdit = new EscLineEdit(this); + lineEdit->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + lineEdit->setMinimumHeight(label->fontMetrics().lineSpacing() + LINE_SPACING_OFFSET); + + connect(lineEdit, &EscLineEdit::editingFinished, this, &EditableLabelWidget::onLabelChangeSubmited); + connect(lineEdit, &EscLineEdit::escPressed, this, &EditableLabelWidget::onLabelChangeCancelled); + + addWidget(label); + addWidget(lineEdit); + + setCurrentWidget(label); +} + +void EditableLabelWidget::setText(const QString& text) +{ + label->setText(text); + lineEdit->setText(text); +} + +QString EditableLabelWidget::text() +{ + return label->text(); +} + +void EditableLabelWidget::onLabelChangeSubmited() +{ + if (isSubmitting) { + return; + } + isSubmitting = true; + + QString oldText = label->text(); + QString newText = lineEdit->text(); + // `lineEdit->clearFocus()` triggers `onLabelChangeSubmited()`, we use `isSubmitting` as a workaround + lineEdit->clearFocus(); + setCurrentWidget(label); + + if (oldText != newText) { + label->setText(newText); + emit textChanged(newText, oldText); + } + + isSubmitting = false; +} + +void EditableLabelWidget::onLabelChangeCancelled() +{ + // order of calls matters, since clearFocus() triggers EditableLabelWidget::onLabelChangeSubmited() + lineEdit->setText(label->text()); + lineEdit->clearFocus(); + setCurrentWidget(label); +} + +void EditableLabelWidget::onLabelClicked() +{ + setCurrentWidget(lineEdit); + lineEdit->setFocus(); +} diff --git a/editablelabelwidget.h b/editablelabelwidget.h new file mode 100644 index 000000000..17772f8c4 --- /dev/null +++ b/editablelabelwidget.h @@ -0,0 +1,66 @@ +/* + Copyright (C) 2013 by Maxim Biro + + This file is part of Tox Qt GUI. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + See the COPYING file for more details. +*/ + +#ifndef EDITABLELABELWIDGET_HPP +#define EDITABLELABELWIDGET_HPP + +#include "copyableelidelabel.h" +#include "esclineedit.h" + +#include +#include + +class ClickableCopyableElideLabel : public CopyableElideLabel +{ + Q_OBJECT +public: + explicit ClickableCopyableElideLabel(QWidget* parent = 0); + +protected: + bool event(QEvent* event) Q_DECL_OVERRIDE; + +signals: + void clicked(); + +}; + +class EditableLabelWidget : public QStackedWidget +{ + Q_OBJECT +public: + explicit EditableLabelWidget(QWidget* parent = 0); + + ClickableCopyableElideLabel* label; + EscLineEdit* lineEdit; + + void setText(const QString& text); + QString text(); + +private: + static const int LINE_SPACING_OFFSET = 2; + bool isSubmitting; + +private slots: + void onLabelChangeSubmited(); + void onLabelChangeCancelled(); + void onLabelClicked(); + +signals: + void textChanged(QString newText, QString oldText); + +}; + +#endif // EDITABLELABELWIDGET_HPP diff --git a/elidelabel.cpp b/elidelabel.cpp new file mode 100644 index 000000000..239400f49 --- /dev/null +++ b/elidelabel.cpp @@ -0,0 +1,82 @@ +/* + Copyright (C) 2013 by Maxim Biro + + This file is part of Tox Qt GUI. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + See the COPYING file for more details. +*/ + +#include "elidelabel.h" + +#include +#include + +ElideLabel::ElideLabel(QWidget *parent) : + QLabel(parent), _textElide(false), _textElideMode(Qt::ElideNone), _showToolTipOnElide(false) +{ +} + +void ElideLabel::paintEvent(QPaintEvent *event) +{ + QFrame::paintEvent(event); + QPainter p(this); + QFontMetrics metrics(font()); + if ((metrics.width(text()) > contentsRect().width()) && textElide()) { + QString elidedText = fontMetrics().elidedText(text(), textElideMode(), rect().width()); + p.drawText(rect(), alignment(), elidedText); + } else { + QLabel::paintEvent(event); + } +} + +bool ElideLabel::event(QEvent *event) +{ + if (event->type() == QEvent::ToolTip) { + QFontMetrics metrics(font()); + if ((metrics.width(text()) > contentsRect().width()) && textElide() && showToolTipOnElide()) { + setToolTip(text()); + } else { + setToolTip(""); + } + } + + return QLabel::event(event); +} + +void ElideLabel::setTextElide(bool set) +{ + _textElide = set; +} + +bool ElideLabel::textElide() const +{ + return _textElide; +} + +void ElideLabel::setTextElideMode(Qt::TextElideMode mode) +{ + _textElideMode = mode; +} + +Qt::TextElideMode ElideLabel::textElideMode() const +{ + return _textElideMode; +} + +void ElideLabel::setShowToolTipOnElide(bool show) +{ + _showToolTipOnElide = show; +} + +bool ElideLabel::showToolTipOnElide() +{ + return _showToolTipOnElide; +} diff --git a/elidelabel.h b/elidelabel.h new file mode 100644 index 000000000..5dd309001 --- /dev/null +++ b/elidelabel.h @@ -0,0 +1,50 @@ +/* + Copyright (C) 2013 by Maxim Biro + + This file is part of Tox Qt GUI. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + See the COPYING file for more details. +*/ + +#ifndef ELIDELABEL_HPP +#define ELIDELABEL_HPP + +#include + +class ElideLabel : public QLabel +{ + Q_OBJECT +public: + explicit ElideLabel(QWidget *parent = 0); + + void setTextElide(bool set); + bool textElide() const; + + void setTextElideMode(Qt::TextElideMode mode); + Qt::TextElideMode textElideMode() const; + + void setShowToolTipOnElide(bool show); + bool showToolTipOnElide(); + +protected: + void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE; + bool event(QEvent *e) Q_DECL_OVERRIDE; + +private: + bool _textElide; + Qt::TextElideMode _textElideMode; + + bool _showToolTipOnElide; + + +}; + +#endif // ELIDELABEL_HPP diff --git a/esclineedit.cpp b/esclineedit.cpp new file mode 100644 index 000000000..1a6adc373 --- /dev/null +++ b/esclineedit.cpp @@ -0,0 +1,34 @@ +/* + Copyright (C) 2013 by Maxim Biro + + This file is part of Tox Qt GUI. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + See the COPYING file for more details. +*/ + +#include "esclineedit.h" + +#include +#include + +EscLineEdit::EscLineEdit(QWidget* parent) : + QLineEdit(parent) +{ +} + +void EscLineEdit::keyPressEvent(QKeyEvent* event) +{ + if (event->key() == Qt::Key_Escape && event->modifiers() == Qt::NoModifier) { + emit escPressed(); + } else { + QLineEdit::keyPressEvent(event); + } +} diff --git a/esclineedit.h b/esclineedit.h new file mode 100644 index 000000000..75eac7315 --- /dev/null +++ b/esclineedit.h @@ -0,0 +1,36 @@ +/* + Copyright (C) 2013 by Maxim Biro + + This file is part of Tox Qt GUI. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + See the COPYING file for more details. +*/ + +#ifndef ESCLINEEDIT_HPP +#define ESCLINEEDIT_HPP + +#include + +class EscLineEdit : public QLineEdit +{ + Q_OBJECT +public: + explicit EscLineEdit(QWidget* parent); + +protected: + void keyPressEvent(QKeyEvent* event) Q_DECL_OVERRIDE; + +signals: + void escPressed(); + +}; + +#endif // ESCLINEEDIT_HPP diff --git a/friend.cpp b/friend.cpp new file mode 100644 index 000000000..40536ba81 --- /dev/null +++ b/friend.cpp @@ -0,0 +1,33 @@ +#include "friend.h" +#include "friendlist.h" +#include "friendwidget.h" + +Friend::Friend(int FriendId, QString UserId) + : friendId(FriendId), userId(UserId) +{ + widget = new FriendWidget(friendId, userId); + chatForm = new ChatForm(this); +} + +Friend::~Friend() +{ + delete chatForm; + delete widget; +} + +void Friend::setName(QString name) +{ + widget->name.setText(name); + chatForm->setName(name); +} + +void Friend::setStatusMessage(QString message) +{ + widget->statusMessage.setText(message); + chatForm->setStatusMessage(message); +} + +QString Friend::getName() +{ + return widget->name.text(); +} diff --git a/friend.h b/friend.h new file mode 100644 index 000000000..dcc636137 --- /dev/null +++ b/friend.h @@ -0,0 +1,25 @@ +#ifndef FRIEND_H +#define FRIEND_H + +#include +#include "chatform.h" + +class FriendWidget; + +struct Friend +{ +public: + Friend(int FriendId, QString UserId); + ~Friend(); + void setName(QString name); + void setStatusMessage(QString message); + QString getName(); + +public: + FriendWidget* widget; + int friendId; + QString userId; + ChatForm* chatForm; +}; + +#endif // FRIEND_H diff --git a/friendlist.cpp b/friendlist.cpp new file mode 100644 index 000000000..997372f8b --- /dev/null +++ b/friendlist.cpp @@ -0,0 +1,32 @@ +#include "friend.h" +#include "friendlist.h" +#include + +QList FriendList::friendList; + +Friend* FriendList::addFriend(int friendId, QString userId) +{ + Friend* newfriend = new Friend(friendId, userId); + friendList.append(newfriend); + return newfriend; +} + +Friend* FriendList::findFriend(int friendId) +{ + for (Friend* f : friendList) + if (f->friendId == friendId) + return f; + return nullptr; +} + +void FriendList::removeFriend(int friendId) +{ + for (int i=0; ifriendId == friendId) + { + friendList.removeAt(i); + return; + } + } +} diff --git a/friendlist.h b/friendlist.h new file mode 100644 index 000000000..f053943d4 --- /dev/null +++ b/friendlist.h @@ -0,0 +1,21 @@ +#ifndef FRIENDLIST_H +#define FRIENDLIST_H + +#include +#include + +class Friend; + +class FriendList +{ +public: + FriendList(); + static Friend* addFriend(int friendId, QString userId); + static Friend* findFriend(int friendId); + static void removeFriend(int friendId); + +public: + static QList friendList; +}; + +#endif // FRIENDLIST_H diff --git a/friendrequestdialog.cpp b/friendrequestdialog.cpp new file mode 100644 index 000000000..1c7e66139 --- /dev/null +++ b/friendrequestdialog.cpp @@ -0,0 +1,61 @@ +/* + Copyright (C) 2013 by Maxim Biro + + This file is part of Tox Qt GUI. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + See the COPYING file for more details. +*/ + +#include "friendrequestdialog.h" + +#include +#include +#include +#include +#include +#include + +FriendRequestDialog::FriendRequestDialog(QWidget *parent, const QString &userId, const QString &message) : + QDialog(parent) +{ + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + setWindowTitle("Friend request"); + + QLabel *friendsLabel = new QLabel("Someone wants to make friends with you.", this); + QLabel *userIdLabel = new QLabel("User ID:", this); + QLineEdit *userIdEdit = new QLineEdit(userId, this); + userIdEdit->setCursorPosition(0); + userIdEdit->setReadOnly(true); + QLabel *messageLabel = new QLabel("Friend request message:", this); + QPlainTextEdit *messageEdit = new QPlainTextEdit(message, this); + messageEdit->setReadOnly(true); + + + QDialogButtonBox *buttonBox = new QDialogButtonBox(Qt::Horizontal, this); + + buttonBox->addButton("Accept", QDialogButtonBox::AcceptRole); + buttonBox->addButton("Reject", QDialogButtonBox::RejectRole); + + connect(buttonBox, &QDialogButtonBox::accepted, this, &FriendRequestDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &FriendRequestDialog::reject); + + QVBoxLayout *layout = new QVBoxLayout(this); + + layout->addWidget(friendsLabel); + layout->addSpacing(12); + layout->addWidget(userIdLabel); + layout->addWidget(userIdEdit); + layout->addWidget(messageLabel); + layout->addWidget(messageEdit); + layout->addWidget(buttonBox); + + resize(300, 200); +} diff --git a/friendrequestdialog.h b/friendrequestdialog.h new file mode 100644 index 000000000..0b7f936f7 --- /dev/null +++ b/friendrequestdialog.h @@ -0,0 +1,29 @@ +/* + Copyright (C) 2013 by Maxim Biro + + This file is part of Tox Qt GUI. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + See the COPYING file for more details. +*/ + +#ifndef FRIENDREQUESTDIALOG_HPP +#define FRIENDREQUESTDIALOG_HPP + +#include + +class FriendRequestDialog : public QDialog +{ + Q_OBJECT +public: + explicit FriendRequestDialog(QWidget *parent, const QString &userId, const QString &message); +}; + +#endif // FRIENDREQUESTDIALOG_HPP diff --git a/friendwidget.cpp b/friendwidget.cpp new file mode 100644 index 000000000..36113677b --- /dev/null +++ b/friendwidget.cpp @@ -0,0 +1,62 @@ +#include "friendwidget.h" +#include +#include + +FriendWidget::FriendWidget(int FriendId, QString id) + : friendId(FriendId) +{ + this->setLayout(&layout); + this->setFixedWidth(225); + this->setFixedHeight(55); + layout.setSpacing(0); + layout.setMargin(0); + textLayout.setSpacing(0); + textLayout.setMargin(0); + + avatar.setPixmap(QPixmap("img/contact list icons/contact.png")); + name.setText(id); + statusPic.setPixmap(QPixmap("img/status/dot_away.png")); + QFont small; + small.setPixelSize(10); + statusMessage.setFont(small); + QPalette pal; + pal.setColor(QPalette::WindowText,Qt::gray); + statusMessage.setPalette(pal); + + textLayout.addStretch(); + textLayout.addWidget(&name); + textLayout.addWidget(&statusMessage); + textLayout.addStretch(); + + layout.addSpacing(20); + layout.addWidget(&avatar); + layout.addSpacing(5); + layout.addLayout(&textLayout); + layout.addStretch(); + layout.addSpacing(5); + layout.addWidget(&statusPic); + layout.addSpacing(5); +} + +void FriendWidget::mouseReleaseEvent (QMouseEvent*) +{ + emit friendWidgetClicked(this); +} + +void FriendWidget::contextMenuEvent(QContextMenuEvent * event) +{ + QPoint pos = event->globalPos(); + QMenu menu; + menu.addAction("Remove friend"); + + QAction* selectedItem = menu.exec(pos); + if (selectedItem) + { + if (selectedItem->text() == "Remove friend") + { + hide(); + emit removeFriend(friendId); + return; + } + } +} diff --git a/friendwidget.h b/friendwidget.h new file mode 100644 index 000000000..a37a8987d --- /dev/null +++ b/friendwidget.h @@ -0,0 +1,28 @@ +#ifndef FRIENDWIDGET_H +#define FRIENDWIDGET_H + +#include +#include +#include +#include + +struct FriendWidget : public QWidget +{ + Q_OBJECT +public: + FriendWidget(int FriendId, QString id); + void mouseReleaseEvent (QMouseEvent* event); + void contextMenuEvent(QContextMenuEvent * event); + +signals: + void friendWidgetClicked(FriendWidget* widget); + void removeFriend(int friendId); + +public: + int friendId; + QLabel avatar, name, statusMessage, statusPic; + QHBoxLayout layout; + QVBoxLayout textLayout; +}; + +#endif // FRIENDWIDGET_H diff --git a/group.cpp b/group.cpp new file mode 100644 index 000000000..5ac97aef8 --- /dev/null +++ b/group.cpp @@ -0,0 +1,98 @@ +#include "group.h" +#include "groupwidget.h" +#include "groupchatform.h" +#include "friendlist.h" +#include "friend.h" +#include "widget.h" +#include "core.h" +#include + +Group::Group(int GroupId, QString Name) + : groupId(GroupId), nPeers{0}, hasPeerInfo{false} +{ + widget = new GroupWidget(groupId, Name); + chatForm = new GroupChatForm(this); + connect(&peerInfoTimer, SIGNAL(timeout()), this, SLOT(queryPeerInfo())); + peerInfoTimer.setInterval(500); + peerInfoTimer.setSingleShot(false); + //peerInfoTimer.start(); +} + +Group::~Group() +{ + delete chatForm; + delete widget; +} + +void Group::queryPeerInfo() +{ + const Core* core = Widget::getInstance()->getCore(); + int nPeersResult = core->getGroupNumberPeers(groupId); + if (nPeersResult == -1) + { + qDebug() << "Group::queryPeerInfo: Can't get number of peers"; + return; + } + nPeers = nPeersResult; + widget->onUserListChanged(); + chatForm->onUserListChanged(); + + if (nPeersResult == 0) + return; + + bool namesOk = true; + QList names = core->getGroupPeerNames(groupId); + if (names.isEmpty()) + { + qDebug() << "Group::queryPeerInfo: Can't get names of peers"; + return; + } + for (int i=0; ionUserListChanged(); + chatForm->onUserListChanged(); + + if (namesOk) + { + qDebug() << "Group::queryPeerInfo: Successfully loaded names"; + hasPeerInfo = true; + peerInfoTimer.stop(); + } +} + +void Group::addPeer(int peerId, QString name) +{ + if (peers.contains(peerId)) + qWarning() << "Group::addPeer: peerId already used, overwriting anyway"; + if (name.isEmpty()) + peers[peerId] = ""; + else + peers[peerId] = name; + nPeers++; + widget->onUserListChanged(); + chatForm->onUserListChanged(); +} + +void Group::removePeer(int peerId) +{ + peers.remove(peerId); + widget->onUserListChanged(); + chatForm->onUserListChanged(); +} + +void Group::updatePeer(int peerId, QString name) +{ + peers[peerId] = name; + widget->onUserListChanged(); + chatForm->onUserListChanged(); +} diff --git a/group.h b/group.h new file mode 100644 index 000000000..b064a5b96 --- /dev/null +++ b/group.h @@ -0,0 +1,37 @@ +#ifndef GROUP_H +#define GROUP_H + +#include +#include +#include + +#define RETRY_PEER_INFO_INTERVAL 500 + +class Friend; +class GroupWidget; +class GroupChatForm; + +class Group : public QObject +{ + Q_OBJECT +public: + Group(int GroupId, QString Name); + ~Group(); + void addPeer(int peerId, QString name); + void removePeer(int peerId); + void updatePeer(int peerId, QString newName); + +private slots: + void queryPeerInfo(); + +public: + int groupId; + QMap peers; + int nPeers; + GroupWidget* widget; + GroupChatForm* chatForm; + bool hasPeerInfo; + QTimer peerInfoTimer; +}; + +#endif // GROUP_H diff --git a/groupchatform.cpp b/groupchatform.cpp new file mode 100644 index 000000000..4904ae077 --- /dev/null +++ b/groupchatform.cpp @@ -0,0 +1,175 @@ +#include "groupchatform.h" +#include "group.h" +#include "groupwidget.h" +#include "widget.h" +#include "friend.h" +#include "friendlist.h" +#include +#include +#include + +GroupChatForm::GroupChatForm(Group* chatGroup) + : group(chatGroup), curRow{0}, lockSliderToBottom{true} +{ + main = new QWidget(), head = new QWidget(), chatAreaWidget = new QWidget(); + headLayout = new QHBoxLayout(), mainFootLayout = new QHBoxLayout(); + headTextLayout = new QVBoxLayout(), mainLayout = new QVBoxLayout(); + mainChatLayout = new QGridLayout(); + avatar = new QLabel(), name = new QLabel(), nusers = new QLabel(), namesList = new QLabel(); + msgEdit = new ChatTextEdit(); + sendButton = new QPushButton(); + chatArea = new QScrollArea(); + QFont bold; + bold.setBold(true); + QFont small; + small.setPixelSize(10); + name->setText(group->widget->name.text()); + name->setFont(bold); + nusers->setFont(small); + nusers->setText(QString("%1 users in chat").arg(group->peers.size())); + avatar->setPixmap(QPixmap("img/contact list icons/group.png")); + QString names; + for (QString& s : group->peers) + names.append(s+", "); + names.chop(2); + namesList->setText(names); + namesList->setFont(small); + + chatAreaWidget->setLayout(mainChatLayout); + chatArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); + chatArea->setWidgetResizable(true); + mainChatLayout->setColumnStretch(1,1); + mainChatLayout->setHorizontalSpacing(10); + + sendButton->setIcon(QIcon("img/button icons/sendmessage_2x.png")); + sendButton->setFlat(true); + QPalette pal; + pal.setColor(QPalette::Button, QColor(107,194,96)); // Tox Green + sendButton->setPalette(pal); + sendButton->setAutoFillBackground(true); + msgEdit->setFixedHeight(50); + sendButton->setFixedSize(50, 50); + + main->setLayout(mainLayout); + mainLayout->addWidget(chatArea); + mainLayout->addLayout(mainFootLayout); + mainLayout->setMargin(0); + + mainFootLayout->addWidget(msgEdit); + mainFootLayout->addWidget(sendButton); + + head->setLayout(headLayout); + headLayout->addWidget(avatar); + headLayout->addLayout(headTextLayout); + headLayout->addStretch(); + headLayout->setMargin(0); + + headTextLayout->addStretch(); + headTextLayout->addWidget(name); + headTextLayout->addWidget(nusers); + headTextLayout->addWidget(namesList); + headTextLayout->setMargin(0); + headTextLayout->setSpacing(0); + headTextLayout->addStretch(); + + chatArea->setWidget(chatAreaWidget); + + connect(sendButton, SIGNAL(clicked()), this, SLOT(onSendTriggered())); + connect(msgEdit, SIGNAL(enterPressed()), this, SLOT(onSendTriggered())); + connect(chatArea->verticalScrollBar(), SIGNAL(rangeChanged(int,int)), this, SLOT(onSliderRangeChanged())); +} + +GroupChatForm::~GroupChatForm() +{ + delete head; + delete main; +} + +void GroupChatForm::show(Ui::Widget &ui) +{ + ui.mainContent->layout()->addWidget(main); + ui.mainHead->layout()->addWidget(head); + main->show(); + head->show(); +} + +void GroupChatForm::setName(QString newName) +{ + name->setText(newName); +} + +void GroupChatForm::onSendTriggered() +{ + QString msg = msgEdit->toPlainText(); + if (msg.isEmpty()) + return; + msgEdit->clear(); + emit sendMessage(group->groupId, msg); +} + +void GroupChatForm::addGroupMessage(QString message, int peerId) +{ + QLabel *msgAuthor; + if (group->peers.contains(peerId)) + msgAuthor = new QLabel(group->peers[peerId]); + else + msgAuthor = new QLabel(""); + + QLabel *msgText = new QLabel(message); + QLabel *msgDate = new QLabel(QTime::currentTime().toString("hh:mm")); + + addMessage(msgAuthor, msgText, msgDate); +} + +void GroupChatForm::addMessage(QString author, QString message, QString date) +{ + addMessage(new QLabel(author), new QLabel(message), new QLabel(date)); +} + +void GroupChatForm::addMessage(QLabel* author, QLabel* message, QLabel* date) +{ + QScrollBar* scroll = chatArea->verticalScrollBar(); + lockSliderToBottom = scroll && scroll->value() == scroll->maximum(); + author->setAlignment(Qt::AlignTop | Qt::AlignLeft); + date->setAlignment(Qt::AlignTop); + if (author->text() == Widget::getInstance()->getUsername()) + { + QPalette pal; + pal.setColor(QPalette::WindowText, Qt::gray); + author->setPalette(pal); + message->setPalette(pal); + } + if (previousName.isEmpty() || previousName != author->text()) + { + if (curRow) + { + mainChatLayout->setRowStretch(curRow, 0); + mainChatLayout->addItem(new QSpacerItem(0,AUTHOR_CHANGE_SPACING),curRow,0,1,3); + curRow++; + } + mainChatLayout->addWidget(author, curRow, 0); + } + previousName = author->text(); + mainChatLayout->addWidget(message, curRow, 1); + mainChatLayout->addWidget(date, curRow, 3); + mainChatLayout->setRowStretch(curRow+1, 1); + mainChatLayout->setRowStretch(curRow, 0); + curRow++; +} + +void GroupChatForm::onSliderRangeChanged() +{ + QScrollBar* scroll = chatArea->verticalScrollBar(); + if (lockSliderToBottom) + scroll->setValue(scroll->maximum()); +} + +void GroupChatForm::onUserListChanged() +{ + nusers->setText(QString("%1 users in chat").arg(group->nPeers)); + QString names; + for (QString& s : group->peers) + names.append(s+", "); + names.chop(2); + namesList->setText(names); +} diff --git a/groupchatform.h b/groupchatform.h new file mode 100644 index 000000000..68faac959 --- /dev/null +++ b/groupchatform.h @@ -0,0 +1,56 @@ +#ifndef GROUPCHATFORM_H +#define GROUPCHATFORM_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "chattextedit.h" +#include "ui_widget.h" + +// Spacing in px inserted when the author of the last message changes +#define AUTHOR_CHANGE_SPACING 5 + +class Group; + +class GroupChatForm : public QObject +{ + Q_OBJECT +public: + GroupChatForm(Group* chatGroup); + ~GroupChatForm(); + void show(Ui::Widget& ui); + void setName(QString newName); + void addGroupMessage(QString message, int peerId); + void addMessage(QString author, QString message, QString date=QTime::currentTime().toString("hh:mm")); + void addMessage(QLabel* author, QLabel* message, QLabel* date); + void onUserListChanged(); + +signals: + void sendMessage(int, QString); + +private slots: + void onSendTriggered(); + void onSliderRangeChanged(); + +private: + Group* group; + QHBoxLayout *headLayout, *mainFootLayout; + QVBoxLayout *headTextLayout, *mainLayout; + QGridLayout *mainChatLayout; + QLabel *avatar, *name, *nusers, *namesList; + ChatTextEdit *msgEdit; + QPushButton *sendButton; + QScrollArea *chatArea; + QWidget *main, *head, *chatAreaWidget; + QString previousName; + int curRow; + bool lockSliderToBottom; +}; + +#endif // GROUPCHATFORM_H diff --git a/grouplist.cpp b/grouplist.cpp new file mode 100644 index 000000000..c9079dd08 --- /dev/null +++ b/grouplist.cpp @@ -0,0 +1,31 @@ +#include "grouplist.h" +#include "group.h" + +QList GroupList::groupList; + +Group* GroupList::addGroup(int groupId, QString name) +{ + Group* newGroup = new Group(groupId, name); + groupList.append(newGroup); + return newGroup; +} + +Group* GroupList::findGroup(int groupId) +{ + for (Group* g : groupList) + if (g->groupId == groupId) + return g; + return nullptr; +} + +void GroupList::removeGroup(int groupId) +{ + for (int i=0; igroupId == groupId) + { + groupList.removeAt(i); + return; + } + } +} diff --git a/grouplist.h b/grouplist.h new file mode 100644 index 000000000..976c32926 --- /dev/null +++ b/grouplist.h @@ -0,0 +1,22 @@ +#ifndef GROUPLIST_H +#define GROUPLIST_H + +#include +#include +#include + +class Group; + +class GroupList +{ +public: + GroupList(); + static Group* addGroup(int groupId, QString name); + static Group* findGroup(int groupId); + static void removeGroup(int groupId); + +public: + static QList groupList; +}; + +#endif // GROUPLIST_H diff --git a/groupwidget.cpp b/groupwidget.cpp new file mode 100644 index 000000000..9cb54a6ad --- /dev/null +++ b/groupwidget.cpp @@ -0,0 +1,75 @@ +#include "groupwidget.h" +#include "grouplist.h" +#include "group.h" +#include +#include +#include + +GroupWidget::GroupWidget(int GroupId, QString Name) + : groupId{GroupId} +{ + this->setLayout(&layout); + this->setFixedWidth(225); + this->setFixedHeight(55); + layout.setSpacing(0); + layout.setMargin(0); + textLayout.setSpacing(0); + textLayout.setMargin(0); + + avatar.setPixmap(QPixmap("img/contact list icons/group_2x.png")); + name.setText(Name); + QFont small; + small.setPixelSize(10); + nusers.setFont(small); + QPalette pal; + pal.setColor(QPalette::WindowText,Qt::gray); + nusers.setPalette(pal); + Group* g = GroupList::findGroup(groupId); + if (g) + nusers.setText(QString("%1 users in chat").arg(g->peers.size())); + else + nusers.setText("0 users in chat"); + + textLayout.addStretch(); + textLayout.addWidget(&name); + textLayout.addWidget(&nusers); + textLayout.addStretch(); + + layout.addSpacing(20); + layout.addWidget(&avatar); + layout.addSpacing(5); + layout.addLayout(&textLayout); + layout.addStretch(); +} + +void GroupWidget::mouseReleaseEvent (QMouseEvent*) +{ + emit groupWidgetClicked(this); +} + +void GroupWidget::contextMenuEvent(QContextMenuEvent * event) +{ + QPoint pos = event->globalPos(); + QMenu menu; + menu.addAction("Quit group"); + + QAction* selectedItem = menu.exec(pos); + if (selectedItem) + { + if (selectedItem->text() == "Quit group") + { + hide(); + emit removeGroup(groupId); + return; + } + } +} + +void GroupWidget::onUserListChanged() +{ + Group* g = GroupList::findGroup(groupId); + if (g) + nusers.setText(QString("%1 users in chat").arg(g->nPeers)); + else + nusers.setText("0 users in chat"); +} diff --git a/groupwidget.h b/groupwidget.h new file mode 100644 index 000000000..3ab5ae18e --- /dev/null +++ b/groupwidget.h @@ -0,0 +1,29 @@ +#ifndef GROUPWIDGET_H +#define GROUPWIDGET_H + +#include +#include +#include +#include + +class GroupWidget : public QWidget +{ + Q_OBJECT +public: + GroupWidget(int GroupId, QString Name); + void onUserListChanged(); + void mouseReleaseEvent (QMouseEvent* event); + void contextMenuEvent(QContextMenuEvent * event); + +signals: + void groupWidgetClicked(GroupWidget* widget); + void removeGroup(int groupId); + +public: + int groupId; + QLabel avatar, name, nusers; + QHBoxLayout layout; + QVBoxLayout textLayout; +}; + +#endif // GROUPWIDGET_H diff --git a/img/button icons/arrow_medgrey.png b/img/button icons/arrow_medgrey.png new file mode 100644 index 0000000000000000000000000000000000000000..83077ebd0209b7eb70c6f0e86d46709952d1f0fe GIT binary patch literal 172 zcmeAS@N?(olHy`uVBq!ia0vp^>_E)M!3HGx`C7_=lw^r(L`iUdT1k0gQ7VIDN`6wR zf@f}GdTLN=VoGJ<$y6JlB0oF!Nj`plr3nWZBN}!vcD&tqqBey|h}FWx%;4Yu z|ML}9RdX392=}WL7M`0E$>tNl=keWL#dAYug6D!&`~kZi6O<>^GaO;yFkmp;C6a9} SU!?*xnZeW5&t;ucLK6VW`!nkR literal 0 HcmV?d00001 diff --git a/img/button icons/arrow_medgrey.svg b/img/button icons/arrow_medgrey.svg new file mode 100644 index 000000000..3000b8375 --- /dev/null +++ b/img/button icons/arrow_medgrey.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/img/button icons/arrow_medgrey_2x.png b/img/button icons/arrow_medgrey_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c3d17f3592281a5d6a09b91ac4539225065f0b21 GIT binary patch literal 249 zcmeAS@N?(olHy`uVBq!ia0vp^d_c^@!3HFQ8hUO4DajJoh?3y^w370~qErUQl>DSr z1<%~X^wgl##FWaylc_d9MYBC!978H@CHeUI)gCx-K;gWDQUcQ>CbmllosRwg|KE7Y zk|nYXMQ=Kf7;zk%@<4{iBe#M|U3X0y>ky)78&qol`;+0AdVT-~a#s literal 0 HcmV?d00001 diff --git a/img/button icons/arrow_white.png b/img/button icons/arrow_white.png new file mode 100644 index 0000000000000000000000000000000000000000..af438d915ae34ad1629e1ac74ff3959bbd281ec4 GIT binary patch literal 152 zcmeAS@N?(olHy`uVBq!ia0vp^>_E)M!3HGx`C7_=lw^r(L`iUdT1k0gQ7VIDN`6wR zf@f}GdTLN=VoGJ<$y6JlA}dc9$B>F!Nq_$T-_Ojf$1ug_gS_<*hDjy|6eAfXu{>nW y5E6HIWZ=m-!O&w8--$^IF$`00FbFeq7%-%yHa^p{dRzlEg~8L+&t;ucLK6VL#w>yW literal 0 HcmV?d00001 diff --git a/img/button icons/arrow_white.svg b/img/button icons/arrow_white.svg new file mode 100644 index 000000000..b6f24a218 --- /dev/null +++ b/img/button icons/arrow_white.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/img/button icons/arrow_white_2x.png b/img/button icons/arrow_white_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c2722d0aa0dcfab6cf903a86ce261f30b1c9d392 GIT binary patch literal 214 zcmeAS@N?(olHy`uVBq!ia0vp^d_c^@!3HFQ8hUO4DajJoh?3y^w370~qErUQl>DSr z1<%~X^wgl##FWaylc_d9MHQYdjv*DdlK%Yv|DTzSjjfU`h%q4fK>ekIOcUG$>>Qew z@jK`+T+VQ4I>t6ZZibscoI}epc7v}cq*xc6JHXDgA(Q1Hlg%tXh1IUtPcW47nJsYe zSj;TuD}3Q<;^huGTloe5)V&rnO*+PulU0zJ(qQ$TQLnOrftf+@r`(^V=dxD<-N4}K L>gTe~DWM4fS?x## literal 0 HcmV?d00001 diff --git a/img/button icons/attach.png b/img/button icons/attach.png new file mode 100644 index 0000000000000000000000000000000000000000..2d8cd8c661e58b0217adae9d1995dcdc2a0bf8e6 GIT binary patch literal 274 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1|;P@bT0xa$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWw1GTRdGHLn>}fnRJo&umX?EZNCE?Jre|tn3N5i+)rryd}Xd| zz@(hQVphOw<=xiS5@pqxv;TzJca=rQeIu@Q8EdrUFzS4tx|Q38 zSvPj=A>#>&xvTbFRGBj2^NY?e>|0(NTH1wMw=#ycPN z%$yp$f1Pz#q|R}>9-lLh`C8H3t2R1m0jGVp-u}qjeq{5 + + + + + + + + diff --git a/img/button icons/attach_2x.png b/img/button icons/attach_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..adb743a6c8c51d8b2d46a02b7cd73ac82923b28b GIT binary patch literal 432 zcmV;h0Z;ykP)?S~9Ug+egPniZ9{da* z`I;Ws6ukMeo>u^;7==`(2Z&&F`;KkaAbQ;bL@<4lU&r#Q?t&$cY>mDXmi!`X)(=(I z6l{Y($-A#$L!OuT9g=%{x*#%5ax{T|7#iVKu%r#-&`WaI0^<>itMm@op#=dYu%!Uw zc{}ujI*kN2ux}(VgL20000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUy)k#D_RCwC7lz|O|APhy-2^_%@+~6GH zMqmU-aD*Fy+rSaH4cOoqfem`al*C{$LeC~YF|>IUez66BMi?*DNZ$@MHvuZp6OLh8 zVW$T5NEap00rlk)ouJDlmyke*JD@t75xT!J?nC#78BODJN2iknI5WU?8_`OjVt^ji z=b(z7UkPwKcC3koVTT3fc10?I^*OiG+PJgN6l>H-_vfzwBh*1%cn%n$rUK~b1nj+s z;kShY|E8a^J$t6X0DGZ3vv>GRduKIchNJbVWR8CV3;+}0XpaGYD!l*z002ovPDHLk FV1g?nd8hyY literal 0 HcmV?d00001 diff --git a/img/button icons/call.svg b/img/button icons/call.svg new file mode 100644 index 000000000..b30e5f424 --- /dev/null +++ b/img/button icons/call.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/img/button icons/call_2x.png b/img/button icons/call_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b088ad7f8367ecfd371d0d2339e335fc03d03eb2 GIT binary patch literal 510 zcmV?0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUzph-kQRCwC#nC(r&Fc5|V3y=-S26zL^ z24Moi2Hk+N0UHoD2ovBDx&iG5?gZ`y&XPqF_s&gxsl+S2e$YDbOp9< zSRGjTwS_R~n~TeO*sC8}Ia%<{i8VWLB?;Pq--&CVB|$gfJL1|;DNqgm;=CXQ+Jc{! z4qAb~xi4J{T_Uz5c9fEm76IxmWk(J8{t`e-@Rl3?|2#_28vL^K%EUddk4ta!dTpiq z(OAt8fNO&eAy2p! z@rFA*zenGoo~eGuHsCt(Oxl!lNd>JDZZNM+e7k#XV&58i*kCn?sU{?I!-Om;pd}G7 z{DV + + + + + diff --git a/img/button icons/emoticon.png b/img/button icons/emoticon.png new file mode 100644 index 0000000000000000000000000000000000000000..1e8566c5eab4887223c208649f4717aeb6daf508 GIT binary patch literal 220 zcmeAS@N?(olHy`uVBq!ia0vp^+#t-s1|(OmDOUqhk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5X>O5T>Ln>}1{rLaio>@d%VWZ@R&ciH1|C|pnnY3y=GC0T` zaFE|&O~VoiR~a=8h9m47ELdIV2V7LoZ|F2*X+*a(7?#B>=?%-`#-G- QK$kFhy85}Sb4q9e0Mp1$NB{r; literal 0 HcmV?d00001 diff --git a/img/button icons/emoticon.svg b/img/button icons/emoticon.svg new file mode 100644 index 000000000..6d00f6260 --- /dev/null +++ b/img/button icons/emoticon.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/img/button icons/emoticon_2x.png b/img/button icons/emoticon_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..676406e46e9cca2c953dc4dd665ca47024ef0136 GIT binary patch literal 323 zcmeAS@N?(olHy`uVBq!ia0vp^Vj#@H1|*Mc$*~4fk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xo_e}ChE&{o6B5XKSV3TKVuD44Y6z2%*23Z-&dCOxlLI)X z9*{9$HP28~+}<1{_HFC*Jxli9**m5BDWg);^Nh@b=8x|uByO{}(Ri{@XwmM!OlBGn z4Aou%8J{i`@XX>m-+Am;mG)=9)y=#L3k3fi+Anyl;}!SXh;*UW@KS!+y^Vf9Bt_f2 z60NrHh%VCh*&JtL$(RthAi+^!XX#m=3;Z!0 zU+un~JJNAIi>+bflz;p?4A|my-^b*%)n?oD^-nH5KBcaFnZt$3%Z0D2xD6PdYVDuk T6T9Le(8CO#u6{1-oD!M<5}kp% literal 0 HcmV?d00001 diff --git a/img/button icons/no.png b/img/button icons/no.png new file mode 100644 index 0000000000000000000000000000000000000000..73eacd2e47a0abbdc7cefdd5bc6c84d802ede353 GIT binary patch literal 178 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4NtU=qlmzFem6RtIr7}3C2g7L3OtKgcubEzo4s z{PjWpRA58l1rce5?&KFPK|P1k4=k2A%2v{@o)E}UB*lDEP3WoHqFP;ssYj&y7#P_Y X8Yd*Dtf`(3auI{4tDnm{r-UW|%LO + + + + + diff --git a/img/button icons/no_2x.png b/img/button icons/no_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..86c05eb5a378e1a9c6f906843e7fd7434076db6d GIT binary patch literal 265 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE;=WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8)t)YnAr-ggT)oKKYQVu7@Q9`Fk%w&WM45wvB~7P{6QqKk zeDZkbKXtOnQm?(`mrPD--fY{dvpQw^hPAyjM7yNYjFVdQ&MBb@0D!(`cmMzZ literal 0 HcmV?d00001 diff --git a/img/button icons/pause.png b/img/button icons/pause.png new file mode 100644 index 0000000000000000000000000000000000000000..62decdfd41817161725eede8c7d85597577e6830 GIT binary patch literal 142 zcmeAS@N?(olHy`uVBq!ia0vp^96-#;!3HGxgLCzOlw^r(L`iUdT1k0gQ7VIDN`6wR zf@f}GdTLN=VoGJ<$y6JlA_Gqs$B>F!Nq_$T-_OL%%*@ezfK}liL#p$UMlPv;$_>l{ n#tw!Nb2=LtB;>dln0Ocr5)_M?)PF4m>SgeB^>bP0l+XkKv`Hq{ literal 0 HcmV?d00001 diff --git a/img/button icons/pause.svg b/img/button icons/pause.svg new file mode 100644 index 000000000..a71a8cc40 --- /dev/null +++ b/img/button icons/pause.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/img/button icons/pause_2x.png b/img/button icons/pause_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6ee39a91e3cd34fe44f599dd3f7791f6aa733426 GIT binary patch literal 129 zcmeAS@N?(olHy`uVBq!ia0vp^0zfRp!3HFQtmCqPlw^r(L`iUdT1k0gQ7VIDN`6wR zf@f}GdTLN=VoGJ<$y6JlB4tk($B>F!Nq_iRcxE(AWHfgOJa3V8UZS>$o<@N8a7V(ja~KSI_%4^ + + + + + diff --git a/img/button icons/sendmessage_2x.png b/img/button icons/sendmessage_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6d78ee2cbc188e6eb853c6a728b87791257c5de5 GIT binary patch literal 533 zcmV+w0_y#VP)!m`=Rq?F7-mEjV6 zvvAG@Jb-({_ELa5U4UD#kZu~m9M{?Xy?ai|5$s5HxUM%~jlVpEMqJ++kjpsYE`Xhk ztIh^6Hry&?sY%D=Z!EpeSU)G3fC79+0Pq8g|F1&bNLp&ccyur|DWe!As3(YT`QK-AhrGPVso9U80~&0ZK+!kpOEY{tOqP?ef{pl1dwZx*=Ep^nCqIe4)=YR#?IjI|m7jUx_ zL52sO6T)4vlNM{kqm=uTc5U04I+8zk?%wfR6L4^}q|#-&rDl13iqzW72X!<>Ah_Ol zfH|KRz8(lCwg#D*M>|+!Nl?xgjteZdk1Hjm+U%w*Bj^(G&GE*ODYiBDfr|k+?3n?k z0xWIk(Xj!!Yg%If=Ch|z|6H_V+Po~-c70vQ=aSW-rmGtNTe|zSM2O1kIJ}`FZZxC4dgF)c7@dP#@ z$p*c6h9g`G@(&KM{XfafKBK|x2=m8Ug*=8NMjbtY1O;IO#f`5Ue=rKD8w79~Ed2Ph zvBJ)zM|MT71y6%gzP-tc6?qnxC;L+lOJ-#LV2zTf6YP*}+|eL(_+s%3_Z5xYD`a(q u8zUN{*bc|+6MS`xy@E?|6N5toBg28UnU$|Sqt*gl$>8bg=d#Wzp$PzsfLDhA literal 0 HcmV?d00001 diff --git a/img/button icons/video.svg b/img/button icons/video.svg new file mode 100644 index 000000000..29dd36858 --- /dev/null +++ b/img/button icons/video.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/img/button icons/video_2x.png b/img/button icons/video_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..324bf9d02579e208d0fc345a5a48c36ca7668d5f GIT binary patch literal 382 zcmV-^0fGLBP)4gkJkaB#K#DfWyN z>WRSexDCe^*ef>X1`edphWwv%0hagRKXI?l<)-vOsu73yKR0DCvYT-M7IeMF7;$`H7+ cqj(B10BOE*koVJ{{Qv*}07*qoM6N<$f`OZ)8UO$Q literal 0 HcmV?d00001 diff --git a/img/button icons/yes.png b/img/button icons/yes.png new file mode 100644 index 0000000000000000000000000000000000000000..79688224b646888a98fc70d5a792d329729c24b3 GIT binary patch literal 183 zcmeAS@N?(olHy`uVBq!ia0vp@K+MI#1|;*}T{!}zBuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrH1%MS8k8hE&{2`t$$4Ju~M4h7S1+6IMKA@DOG`p{BHfQK0?@ zKdVYMLlf(XNnA%V7}^;_j2N|MICwDoOg>Z}GNHkr*=%+K!*aG1-y0@LR~y!G#y)0> dQREUxV7PxGS#YgrY$DJS22WQ%mvv4FO#mZ_Ig|hZ literal 0 HcmV?d00001 diff --git a/img/button icons/yes_2x.png b/img/button icons/yes_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a4de224446c623e7d85d4f2b6eba5a98c90edb97 GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^Qa~)i!3HEJJLv8MQj#UE5hcO-X(i=}MX3yqDfvmM z3ZA)%>8U}fi7AzZCsS>Jiq?9%IEGZ*+H&P0Z?gdp%Y{vh${QSS_vV?nz2NAX5P0K2 zsfrJC)*X>}oxA*u_JSSkf;%T>U(uPh}1B^+S<_y7O<5AvcG3BkvYI4DY(HZ(R0O6>aU zaEqaP4#TV;tP1}cba#FGznmf94XfCS%MvHP9m?>MVPRunkzkN>%4JFUXjK6;k-^i| K&t;ucLK6V!**6FP literal 0 HcmV?d00001 diff --git a/img/contact list icons/add.svg b/img/contact list icons/add.svg new file mode 100644 index 000000000..e9159a2a5 --- /dev/null +++ b/img/contact list icons/add.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/img/contact list icons/add_2x.png b/img/contact list icons/add_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9c513e2f7924a32cef7668b92f199e217ad6fb72 GIT binary patch literal 192 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWw1GNuDl_Ar-fh5)=gg{QtlIp?%&1hgFq6Jj^_9vIegj0@-`4 z60R`nEZ8*H#r9jplSiIw`6V1pPh?0*lpZ*7uFc_%$D(-^3VsI^!#R$KXS{lJ(d^+0 iW~f#kYjy?}2?p8M-jem^JJB0(4j;NPgQMG)2{Ho+ixU=b{YG_f>bp&)FPX0=-IYKs&>L?i?YL2D0U zrOo~U2|>0KHid$fV&Q=(v4}_rL=e=u%AARDzf63~*?H%g&*de-U=T)v>)74CF8WVu`# zMwiPaI-L$N4Ns?2)a!M_aJ^o~bUNMb2lDXnU>MeuqX{dDLS$LijmokNMNxMDR|7z~ zTt>Is)$G^pc2O>ubxlwgJQ|Hip-@1h(b(Fj(P*GhC}1=i>3aOcNFi`=aDYf8LQhXm zv0ex?n@!x^-I2Gqx9=+itX3;*HXCNM*@yPS!$S!G#^dqDdOO1~n9t`}EEeXsn&){c zNfH$Vfqwor9*-|RqW~ZX0+l3*@;pxsub5?7&*kN1FAxa4x8-s<)M_==^9zMS^y=zr z{Y4<3&;NRPdHJgeyId}MeSHnL+r2SxrBXq)S|y*ionaUVf+NCB;q`iZmR(BNT}oez-EQAIY`5F-5B#0` UTOjxRz5oCK07*qoM6N<$g6!UGM*si- literal 0 HcmV?d00001 diff --git a/img/contact list icons/group.png b/img/contact list icons/group.png new file mode 100644 index 0000000000000000000000000000000000000000..d2f78a50e43055066f7731833f5fb3af3acf0465 GIT binary patch literal 314 zcmV-A0mc4_P)38;k5K_wso z37wNVn1s#&5;_N)gVsUoAS2szM>ER0zsnsV3I7Q|1Tb?Q?7<&Q!CFcXd>3p%d?Xd@ zd5b~CM-}fX+DA=gG+Ne%Rip>A!5Y;T>$V8N5o@nx_BddPIYt#nDRQdMNIj#S)fh8Z ziR39PC-l-5`m~ZKDo|ZkZ-@s%& z9>*X)9Rcq3DuaY?;s0dpxQuSE(D5<2T(Lh`{Jz@i&(E-V2QL8z049oE)s~A7jsO4v M07*qoM6N<$f}8?;8~^|S literal 0 HcmV?d00001 diff --git a/img/contact list icons/group.svg b/img/contact list icons/group.svg new file mode 100644 index 000000000..f1b06522c --- /dev/null +++ b/img/contact list icons/group.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/img/contact list icons/group_2x.png b/img/contact list icons/group_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..eadc7487b5b87b9d1326a121b8bbbcfd905740b2 GIT binary patch literal 512 zcmV+b0{{JqP)PRHPBU0SY|Cy%Dgq7qtuS#+IGlD3w)prq(-|J>qXbKFlnxo?V(>&zg$O&> z0?aUPs@GBB&1wUr5K<5qf%aTBqH}BH(%{+ior5`|eqFW*yH8`G?=rk|E-ARZ#r*P+ zu#g$t2u@%_toNh*!Sif`rYIwPP!pa z*{^$1aV7IBaYKA^iFIOP`Cr8B+i6V9v-yYeC%^!KLv%PHHb*f40000g%5B*VC%!MX z7L0bddwjB9z$HcwogbP0l+XkKOVn(l literal 0 HcmV?d00001 diff --git a/img/contact list icons/settings.svg b/img/contact list icons/settings.svg new file mode 100644 index 000000000..fc0aba07d --- /dev/null +++ b/img/contact list icons/settings.svg @@ -0,0 +1,19 @@ + + + + + + diff --git a/img/contact list icons/settings_2x.png b/img/contact list icons/settings_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ebf1aa9a69e48ca0183264a525a3976638f5e4f2 GIT binary patch literal 474 zcmV<00VV#4P)RCwCFSAmVgAPj^~zzF;X+2Glr z9UAbg8>AZ~KLK?lN1_%DhC)t4QAmuj;d~%O0<5*2eZ?daaY(dN zf%kF~fM_*5w5R_hasdWw=z#EUptW`zBibsosg~t|HmrRd=dP0rv>FG9MTgE;geSp> z@Kbc5Jy46Xc!V-g7mU6SvOr3Cg~#un(MwMjMjy>G;R}L%yv@KuwFu{TXg6$J)Hzr% zU@>y-6w`Lva;})p)rSGRWI-Y~6{cRtu3_RHT9@rCB4{))E9p8XT0v#$R+8*WK^#jv{qjn5$(jRh6a zaClrwl1DBLRUyV{T84empKN(lTN(H+TzBTpx`T>1cofhko{C*{oM`l%NwiL1!(hmS zVx-BY?pe;sB+Q#{hMf0TH0;YqhMQAQz3+WF%?6p60eX6xp~T)(#(D}c0B|f#J>ht> QQ2+n{07*qoM6N<$g6et4(*OVf literal 0 HcmV?d00001 diff --git a/img/contact list icons/transfer.png b/img/contact list icons/transfer.png new file mode 100644 index 0000000000000000000000000000000000000000..13b4af62d0255caf7ae5cddca92de713a457747d GIT binary patch literal 288 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5X4tcsbhE&{2N@-~N|Np-g7QGf5a38O0?W zU~OR2VEFj^|BglmW<`b~Ry}8l4n~JkPK%s+|IRdl8~)qQ-r#9qvv54i>5+J;VM(1` zNS&Qeg00!vjE%_+%Ou%;IW{pl)X2peU-?$2W2W5v+rV>%;t4bJ19Q5ZKUYuLa6o7S zZ$%R8{xq9J=Q(LB7=IT^Bsk=(vgtqE@bidN1HZ_DD~6A_nH5-1H7?NkBiLc@a8sdx iamH518ny-oW`-$ynYFbqcxVBA!{F)a=d#Wzp$P!hFl8tJ literal 0 HcmV?d00001 diff --git a/img/contact list icons/transfer.svg b/img/contact list icons/transfer.svg new file mode 100644 index 000000000..12c60859e --- /dev/null +++ b/img/contact list icons/transfer.svg @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/img/contact list icons/transfer_2x.png b/img/contact list icons/transfer_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a0fe5d1cccc08b1672dcb63273440696e0752b98 GIT binary patch literal 433 zcmV;i0Z#sjP)AxiqtW5lo`~D%D@qI!z3dKYKgb)Rsm5}{_xd(eoKU5qk7$Y&ja*Cb; z0~gQz2F?@+&+yA#$+2GcBuDh*8bESb>X_Y@1(M%q=ESfN)dH;M3cIk(iPmAB2o`e7 z=8fYp6XuVC$c^xJ>JoFEq{24>VuLa6EJDl>G+;Ty9a61{5EcTdNJ;|vb8-knge~Wl zKK(`!WO5_C%|d=_a|A0No!qg&S{aTBc@}Z%{@)ks#YMWTdXAIFi~!HcwFta8PEHZ9 z=_8d}z+_la$*Yj3+LDMEMTj-fb9F&U5K%jQoUY)r;2*TM`|3Vu!H4w&D5MVb9;f#A bPk;dc6I|>m&U7(r00000NkvXXu0mjfGk3Lv literal 0 HcmV?d00001 diff --git a/img/icon.ico b/img/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..260f04c63889dc0ff99148c263800a91170196f2 GIT binary patch literal 370070 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 literal 0 HcmV?d00001 diff --git a/img/icon.png b/img/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b9cbe71bf9281da551578cc5298fddf1336b6a90 GIT binary patch literal 1912 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 literal 0 HcmV?d00001 diff --git a/img/status/dot_away.svg b/img/status/dot_away.svg new file mode 100644 index 000000000..cb30abea6 --- /dev/null +++ b/img/status/dot_away.svg @@ -0,0 +1,36 @@ + + + + + + diff --git a/img/status/dot_away_2x.png b/img/status/dot_away_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..66054bf36f50e9368624dde303fa7a16a6bbef3c GIT binary patch literal 411 zcmV;M0c8G(P)JdBwPgkX<6C&8XzB`?LvGt-S$#Ru}^SW7J>0mFg zr8>IBN@mZB)6#LpM39*!o75)18>VJ?-Laz8d<9Af!+7DyiMn@dc1= z7l?2WA4u|{AiBM7JI|FpijO;f0Xbyy=K);7%}koeO*_hF=bUvavcy9q<&Li?Qew3X zjjzkifMwEydhl}yu#Guj6*0-#avRk~M1kLZ(7smz1^~F)dIR{7@w)&3002ovPDHLk FV1hpAwOIfF literal 0 HcmV?d00001 diff --git a/img/status/dot_away_notification.png b/img/status/dot_away_notification.png new file mode 100644 index 0000000000000000000000000000000000000000..770899540a4578756f3306a80830f722c31ef649 GIT binary patch literal 472 zcmV;}0Vn>6P)jne>DYhOWKm%kC=`e@bHUEr4l+#~YnIlc#xc^Rh5k!gx!SunZ!-HxI=$o}ZL> zFu58OWHb$?D|rGE&;11CLPg4?3Fww(pLcRbzmBR`1yM!G#yT=WZ`l!`)gmy66y?9! zt~@wZ#8e?5yVzW@fKLOTgH%a6J5mGpRw~3Q6l6IvfsOPL0dfL}t+KNt1GS@J59jK^ zbRYB@WbWrvoU`3^TAfM!k{zq1mErR@zXB6&$7u1L9{H9l*ni5O00RIy=&D#0kzg_a O0000 + + + + + + diff --git a/img/status/dot_away_notification_2x.png b/img/status/dot_away_notification_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2c46e511a9cc86da4a1515a60430442abdf6bb8e GIT binary patch literal 852 zcmV-a1FQUrP)d1(tv0 zKU9FIz@h>}1%wK~6`&3*DnR_s537TygQ$euF;AZ5&EDlsOm^nRj8EEow{Jg{%p&C1 zvMkM?rC9|Q%V1V#*43=$$GPc3bteIbXXekwtT(f=S)5oP?*>r)5D){x0@zIez|xwv z^;szKSm9(=sciSw>@fd8dzP+eyr+R*yR4HqKwK0bu(#S9MQtFRC9Ij#_R;9rdVx4V z47f=X%LnxB;A2uOO-CP)52C|zx-c&d$|1RO+s^T9392PsD|xwu9P{7raKM@D~7Kra2!ZUN+Di36)0i{w$1s$W*W`Aver1-~%%%&;0C?I+` zajQg-rQ?E2-UJaNeXRn;UqIeubV%-mC8X4_m|Y93ZjCEAfbvrwSBgqH% zN z0L7TB*m034=h9j!d2dPSSSFsr?42U|kP}5+t>u)ltfX}7j1o_)6sZ$|)hxly71x(n z>S6k^4fqr`8 zDNWEiWK#P_1RV&(06*+~bIn0%{~L?|eIy92&s8n$n)H4;5?GB6y$cwwUMXogL3n3h z$1I{v{jzrC%HeWST)V8AOyDf4NtU=qlmzFem6RtIr7}3C5Io}QqhLsEoVhoSi9cm7oNaXKGOK8zz z@I2G5<;33WaFo}e^>^bGW`TGHPlk`|3T!ju_?N01Fo`z$MDlMvCU{m;VfMLZ5o1Qq z`V|#QyXHFd$}cc8*x=C1*2uDeGh)t(1N^}p%wKsb7*4$E<#Az{aESGS!C`hj14mgI k4ql-g29cCT21Ygpg;cfY?@k2x16{}9>FVdQ&MBb@0M9H*rvLx| literal 0 HcmV?d00001 diff --git a/img/status/dot_idle.svg b/img/status/dot_idle.svg new file mode 100644 index 000000000..f1923e4bb --- /dev/null +++ b/img/status/dot_idle.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/img/status/dot_idle_2x.png b/img/status/dot_idle_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..96a2516f25a2415fc1c960a6152a5eadd07d2819 GIT binary patch literal 376 zcmV-;0f+vHP)+h38CMg#lJ z<_F|zz926Vh3Sq`&4=V!sJp6(qgUL(d*r+62{MsEUY4Q=mJ#(z7PJusElFk%5yTv{ z9I4{0dWdo9EMHlcT1x*Ey#>E7CEBH1oP2N9#~)h?ocnvly~*tqxRH~-cYf?cfB^vX W6ko7GX0jLn0000 zIsr`Z6Uu9uq(&f)8@80c*Ho4wnvs)I&L1E*3N@`&+LPPQ)Np&CB_ClN_y(S4 zld+eaxv`D{=zn|#4k*=aD>t=KeLC6~VDcfr0L*Qh AGynhq literal 0 HcmV?d00001 diff --git a/img/status/dot_idle_notification.svg b/img/status/dot_idle_notification.svg new file mode 100644 index 000000000..ba103006d --- /dev/null +++ b/img/status/dot_idle_notification.svg @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/img/status/dot_idle_notification_2x.png b/img/status/dot_idle_notification_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fe560aea825af5ace649748be13cd6676834a0d5 GIT binary patch literal 825 zcmV-91IGM`P)5s$Ez2gCgkK|(}<4MLS3KIqLB7inm0TB=iV15Ar zmYs~}E)T^WURcY>J<~^ZyScxnIStpXt@D8|+Po9DfY^FCpjItqquoG0P0gDLO_j38 zb0?4%5VcuSg~r47n;XYaG)!d|kXH(a#x#>fb;m&`&|RLgY+F;fzM^q45p&fCLiAGk zcx>T8!ynC*Bdt@wZ{+E5vwsYh{eq3plm-ykCx3Clc(aCoZA7QgKb-wzM%EPs;?98P z0Th|Zx;HZ*vhZ!pV~tE@K0zQ)m>n29An=I7rj~pb-B^cuP)TUv!F1j-QR(~XY-dEm zLzblY%JXvYM6E-wDJLuwChBqoBWEm7kLIT*j)>B_`J&c{;+TlJ?Ml0jOu-C1xJRP_F(Ury`)@yAdv4= zt&zM(N9@h|5|V(lGXQrU%I4z`wi8Z4mII}qy=cGOGcbT^Cx!&KTWjRveJTb5@XT1P9F z@mu7IRuOOc<`T`?nrCk1I^QBgnrG8l5ePMMf?$2`nw6*hudEk220kZJGc4)zblXdB zfrQ0o*W7;SdCTM$zSMKOXTJ4u;9JD^>c_hy@f?mNzG(Am-y_@?-%zP7JIbqmmM}m~ zZOH?E8BmXXv$soc4T?j}gUWd0d0_7?g?FLE{v*HuTW(h<^f4u_bxCyDx`7I;J! zGca%qgD@k*tT_@uLG}_)Usv{fY~q}ZR%I*Zu>ytWdb&7uvH6t^ z3{AR(UT4EIx9UEBMlVndsi3NO~&bP|bwNmD<>i0C34ZC#N_Ug>$UM8l@ zF!NBs${?$^$Br+*_Ps{pnp=?GN`LL!sj-{uLo;trHht~HFzJ_IoYrYM!Hi3-w;~LB zpDoXi>}j&+p7}_8lIEgg;kTb2-&~ftqvh%5x6a~u&wz%#ynpciccWmHxs#IwfevNx MboFyt=akR{0Hgq7s{jB1 literal 0 HcmV?d00001 diff --git a/img/status/dot_online.svg b/img/status/dot_online.svg new file mode 100644 index 000000000..c28873540 --- /dev/null +++ b/img/status/dot_online.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/img/status/dot_online_2x.png b/img/status/dot_online_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..77ce1c84d09c1a80189a73c570666d34bc7781fe GIT binary patch literal 282 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE;=WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8J)SO(Ar-fh5)Uwa;#1LNSXrY`#*oBp@{-%3hM_~M9>jzxi!|ijS z$4|F_A?1OKmXyUr&L|e&1x<+yCOv1=36j#}InBKxi6=*4%Zoyl%NMu<_L}G=tPEvk bV3A;Wf8TmqI**tP&>sw*u6{1-oD!M=>(-60Fnuy6W|T%4Vn?s4SI*plCi=^6;C=n$a}tb$vYc!hse?gFYbAO zGy|rVp+I^EJTT@2shVrDk+L_`R;YO?CPtY^4oFta9{;!cE`33jHF_2rTxC-61Jo#1 zk=#o`wGO>J*X2^c_)wnTu+`AhaDybF5~J6!(KomhVKBF3@_+j4WKf=%thcBDs52b(Pk`nFE!&xavUz0QSLsTjcxy}3s;2; zrfpdfdTnqZlg)yIv*g1bMC8=ql6om47Qvi-x0sEK + + + + + + + + diff --git a/img/status/dot_online_notification_2x.png b/img/status/dot_online_notification_2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c31122e14397f6251dec7b2977d2c77b8013ffa7 GIT binary patch literal 766 zcmV@Tph>xs3%!b8pnC}e^-?Zko)>Bqsx-k z1+A1;CbrbH8d_hpc5ln~Huh{431CH+-1Ff++0yzTK=mXb1i)%vvi4KN0(uNtNCiN= z(B;ANXvd~Es44AF8SoXYYY%t};- zU~G(?`+$5yfEh;Iii64lDZJsBY6HsJ=hh2}1}#(J!&X?K8dyhcY*g&RKVd5suqSuV zwfjN1#0X2gh+vDAOjy8K$|<>UiJ|#JsA(ZyEJuPkfZ-%?Rjb+b zOQuVXF5+xNpsD5t=HyErkjx_stWB&erh2ADkeWc9m?{^ZPW=x^CZZYxZBCKr?eRt@ zrxV!(v1gr~5=aPGqeU4`MY*0+LgZ+##fB8(#mo#w(uXrtT8j-=9uhQ{dtU(suiMD~(ueH!R&wk^lez07*qoM6N<$g6;QGf&c&j literal 0 HcmV?d00001 diff --git a/main.cpp b/main.cpp new file mode 100644 index 000000000..7e82fbd9e --- /dev/null +++ b/main.cpp @@ -0,0 +1,17 @@ +#include "widget.h" +#include + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + a.setApplicationName("Toxgui"); + a.setOrganizationName("Tox"); + Widget* w = Widget::getInstance(); + w->show(); + + int errorcode = a.exec(); + + delete w; + + return errorcode; +} diff --git a/res.qrc b/res.qrc new file mode 100644 index 000000000..e56d4179e --- /dev/null +++ b/res.qrc @@ -0,0 +1,5 @@ + + + res/settings.ini + + diff --git a/res/settings.ini b/res/settings.ini new file mode 100644 index 000000000..97b0eeaf4 --- /dev/null +++ b/res/settings.ini @@ -0,0 +1,55 @@ +[DHT%20Server] +dhtServerList\size=13 +dhtServerList\1\name=benwaffle +dhtServerList\1\userId=8E6667FF967EA30B3DC3DB57A4B533152476E7AAE090158B9C2D9DF58ECC7B78 +dhtServerList\1\address=192.3.30.132 +dhtServerList\1\port=33445 +dhtServerList\2\name=zlacki RU #1 +dhtServerList\2\userId=D59F99384592DE4C8AB9D534D5197DB90F4755CC9E975ED0C565E18468A1445B +dhtServerList\2\address=31.192.105.19 +dhtServerList\2\port=33445 +dhtServerList\3\name=aitjcize +dhtServerList\3\userId=7F9C31FE850E97CEFD4C4591DF93FC757C7C12549DDD55F8EEAECC34FE76C029 +dhtServerList\3\address=54.199.139.199 +dhtServerList\3\port=33445 +dhtServerList\4\name=zlacki US +dhtServerList\4\userId=9430A83211A7AD1C294711D069D587028CA0B4782FA43CB9B30008247A43C944 +dhtServerList\4\address=69.42.220.58 +dhtServerList\4\port=33445 +dhtServerList\5\name=platos +dhtServerList\5\userId=B24E2FB924AE66D023FE1E42A2EE3B432010206F751A2FFD3E297383ACF1572E +dhtServerList\5\address=66.175.223.88 +dhtServerList\5\port=33445 +dhtServerList\6\name=stqism +dhtServerList\6\userId=951C88B7E75C867418ACDB5D273821372BB5BD652740BCDF623A4FA293E75D2F +dhtServerList\6\address=192.254.75.98 +dhtServerList\6\port=33445 +dhtServerList\7\name=nurupo +dhtServerList\7\userId=F404ABAA1C99A9D37D61AB54898F56793E1DEF8BD46B1038B9D822E8460FAB67 +dhtServerList\7\address=192.210.149.121 +dhtServerList\7\port=33445 +dhtServerList\8\name=JmanGuy +dhtServerList\8\userId=20C797E098701A848B07D0384222416B0EFB60D08CECB925B860CAEAAB572067 +dhtServerList\8\address=66.74.15.98 +dhtServerList\8\port=33445 +dhtServerList\9\name=zlacki NL +dhtServerList\9\userId=CC2B02636A2ADBC2871D6EC57C5E9589D4FD5E6F98A14743A4B949914CF26D39 +dhtServerList\9\address=5.39.218.35 +dhtServerList\9\port=33445 +dhtServerList\10\name=zlacki RU #2 +dhtServerList\10\userId=AE27E1E72ADA3DC423C60EEBACA241456174048BE76A283B41AD32D953182D49 +dhtServerList\10\address=193.107.16.73 +dhtServerList\10\port=33445 +dhtServerList\11\name=stal +dhtServerList\11\userId=A09162D68618E742FFBCA1C2C70385E6679604B2D80EA6E84AD0996A1AC8A074 +dhtServerList\11\address=23.226.230.47 +dhtServerList\11\port=33445 +dhtServerList\12\name=sonOfRa +dhtServerList\12\userId=DDCF277B8B45B0D357D78AA4E201766932DF6CDB7179FC7D5C9F3C2E8E705326 +dhtServerList\12\address=144.76.60.215 +dhtServerList\12\port=33445 +dhtServerList\13\name=anonymous +dhtServerList\13\userId=5CD7EB176C19A2FD840406CD56177BB8E75587BB366F7BB3004B19E3EDC04143 +dhtServerList\13\address=192.184.81.118 +dhtServerList\13\port=33445 + diff --git a/settings.cpp b/settings.cpp new file mode 100644 index 000000000..c9c3db2d3 --- /dev/null +++ b/settings.cpp @@ -0,0 +1,343 @@ +/* + Copyright (C) 2013 by Maxim Biro + + This file is part of Tox Qt GUI. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + See the COPYING file for more details. +*/ + +#include "settings.h" + +#include +#include +#include +#include +#include + +const QString Settings::FILENAME = "settings.ini"; + +Settings::Settings() : + loaded(false) +{ + load(); +} + +Settings::~Settings() +{ + save(); +} + +Settings& Settings::getInstance() +{ + static Settings settings; + return settings; +} + +void Settings::load() +{ + if (loaded) { + return; + } + + QString filePath = getSettingsDirPath() + '/' + FILENAME; + + //if no settings file exist -- use the default one + QFile file(filePath); + if (!file.exists()) { + filePath = ":/conf/" + FILENAME; + } + + QSettings s(filePath, QSettings::IniFormat); + s.beginGroup("DHT Server"); + int serverListSize = s.beginReadArray("dhtServerList"); + for (int i = 0; i < serverListSize; i ++) { + s.setArrayIndex(i); + DhtServer server; + server.name = s.value("name").toString(); + server.userId = s.value("userId").toString(); + server.address = s.value("address").toString(); + server.port = s.value("port").toInt(); + dhtServerList << server; + } + s.endArray(); + s.endGroup(); + + //NOTE: uncomment when logging will be implemented +/* + s.beginGroup("Logging"); + enableLogging = s.value("enableLogging", false).toBool(); + encryptLogs = s.value("encryptLogs", true).toBool(); + s.endGroup(); +*/ + + s.beginGroup("General"); + username = s.value("username", "My name").toString(); + statusMessage = s.value("statusMessage", "My status").toString(); + s.endGroup(); + + s.beginGroup("Widgets"); + QList objectNames = s.childKeys(); + for (const QString& name : objectNames) { + widgetSettings[name] = s.value(name).toByteArray(); + } + s.endGroup(); + + s.beginGroup("GUI"); + enableSmoothAnimation = s.value("smoothAnimation", true).toBool(); + smileyPack = s.value("smileyPack").toByteArray(); + customEmojiFont = s.value("customEmojiFont", true).toBool(); + emojiFontFamily = s.value("emojiFontFamily", "DejaVu Sans").toString(); + emojiFontPointSize = s.value("emojiFontPointSize", QApplication::font().pointSize()).toInt(); + firstColumnHandlePos = s.value("firstColumnHandlePos", 50).toInt(); + secondColumnHandlePosFromRight = s.value("secondColumnHandlePosFromRight", 50).toInt(); + timestampFormat = s.value("timestampFormat", "hh:mm").toString(); + minimizeOnClose = s.value("minimizeOnClose", false).toBool(); + s.endGroup(); + + s.beginGroup("Privacy"); + typingNotification = s.value("typingNotification", false).toBool(); + s.endGroup(); + + loaded = true; +} + +void Settings::save() +{ + QString filePath = getSettingsDirPath() + '/' + FILENAME; + + QSettings s(filePath, QSettings::IniFormat); + + s.clear(); + + s.beginGroup("DHT Server"); + s.beginWriteArray("dhtServerList", dhtServerList.size()); + for (int i = 0; i < dhtServerList.size(); i ++) { + s.setArrayIndex(i); + s.setValue("name", dhtServerList[i].name); + s.setValue("userId", dhtServerList[i].userId); + s.setValue("address", dhtServerList[i].address); + s.setValue("port", dhtServerList[i].port); + } + s.endArray(); + s.endGroup(); + + //NOTE: uncomment when logging will be implemented +/* + s.beginGroup("Logging"); + s.setValue("storeLogs", enableLogging); + s.setValue("encryptLogs", encryptLogs); + s.endGroup(); +*/ + + s.beginGroup("General"); + s.setValue("username", username); + s.setValue("statusMessage", statusMessage); + s.endGroup(); + + s.beginGroup("Widgets"); + const QList widgetNames = widgetSettings.keys(); + for (const QString& name : widgetNames) { + s.setValue(name, widgetSettings.value(name)); + } + s.endGroup(); + + s.beginGroup("GUI"); + s.setValue("smoothAnimation", enableSmoothAnimation); + s.setValue("smileyPack", smileyPack); + s.setValue("customEmojiFont", customEmojiFont); + s.setValue("emojiFontFamily", emojiFontFamily); + s.setValue("emojiFontPointSize", emojiFontPointSize); + s.setValue("firstColumnHandlePos", firstColumnHandlePos); + s.setValue("secondColumnHandlePosFromRight", secondColumnHandlePosFromRight); + s.setValue("timestampFormat", timestampFormat); + s.setValue("minimizeOnClose", minimizeOnClose); + s.endGroup(); + + s.beginGroup("Privacy"); + s.setValue("typingNotification", typingNotification); + s.endGroup(); +} + +QString Settings::getSettingsDirPath() +{ + // workaround for https://bugreports.qt-project.org/browse/QTBUG-38845 +#ifdef Q_OS_WIN + return QStandardPaths::writableLocation(QStandardPaths::ConfigLocation); +#else + return QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + '/' + qApp->organizationName() + '/' + qApp->applicationName(); +#endif +} + +const QList& Settings::getDhtServerList() const +{ + return dhtServerList; +} + +void Settings::setDhtServerList(const QList& newDhtServerList) +{ + dhtServerList = newDhtServerList; + emit dhtServerListChanged(); +} + +QString Settings::getUsername() const +{ + return username; +} + +void Settings::setUsername(const QString& newUsername) +{ + username = newUsername; +} + +QString Settings::getStatusMessage() const +{ + return statusMessage; +} + +void Settings::setStatusMessage(const QString& newMessage) +{ + statusMessage = newMessage; +} + +bool Settings::getEnableLogging() const +{ + return enableLogging; +} + +void Settings::setEnableLogging(bool newValue) +{ + enableLogging = newValue; +} + +bool Settings::getEncryptLogs() const +{ + return encryptLogs; +} + +void Settings::setEncryptLogs(bool newValue) +{ + encryptLogs = newValue; +} + +void Settings::setWidgetData(const QString& uniqueName, const QByteArray& data) +{ + widgetSettings[uniqueName] = data; +} + +QByteArray Settings::getWidgetData(const QString& uniqueName) const +{ + return widgetSettings.value(uniqueName); +} + +bool Settings::isAnimationEnabled() const +{ + return enableSmoothAnimation; +} + +void Settings::setAnimationEnabled(bool newValue) +{ + enableSmoothAnimation = newValue; +} + +QByteArray Settings::getSmileyPack() const +{ + return smileyPack; +} + +void Settings::setSmileyPack(const QByteArray &value) +{ + smileyPack = value; + emit smileyPackChanged(); +} + +bool Settings::isCurstomEmojiFont() const +{ + return customEmojiFont; +} + +void Settings::setCurstomEmojiFont(bool value) +{ + customEmojiFont = value; + emit emojiFontChanged(); +} + +int Settings::getEmojiFontPointSize() const +{ + return emojiFontPointSize; +} + +void Settings::setEmojiFontPointSize(int value) +{ + emojiFontPointSize = value; + emit emojiFontChanged(); +} + +int Settings::getFirstColumnHandlePos() const +{ + return firstColumnHandlePos; +} + +void Settings::setFirstColumnHandlePos(const int pos) +{ + firstColumnHandlePos = pos; +} + +int Settings::getSecondColumnHandlePosFromRight() const +{ + return secondColumnHandlePosFromRight; +} + +void Settings::setSecondColumnHandlePosFromRight(const int pos) +{ + secondColumnHandlePosFromRight = pos; +} + +const QString &Settings::getTimestampFormat() const +{ + return timestampFormat; +} + +void Settings::setTimestampFormat(const QString &format) +{ + timestampFormat = format; + emit timestampFormatChanged(); +} + +QString Settings::getEmojiFontFamily() const +{ + return emojiFontFamily; +} + +void Settings::setEmojiFontFamily(const QString &value) +{ + emojiFontFamily = value; + emit emojiFontChanged(); +} + +bool Settings::isMinimizeOnCloseEnabled() const +{ + return minimizeOnClose; +} + +void Settings::setMinimizeOnClose(bool newValue) +{ + minimizeOnClose = newValue; +} + +bool Settings::isTypingNotificationEnabled() const +{ + return typingNotification; +} + +void Settings::setTypingNotification(bool enabled) +{ + typingNotification = enabled; +} diff --git a/settings.h b/settings.h new file mode 100644 index 000000000..ad481e9fa --- /dev/null +++ b/settings.h @@ -0,0 +1,163 @@ +/* + Copyright (C) 2013 by Maxim Biro + + This file is part of Tox Qt GUI. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + See the COPYING file for more details. +*/ + +#ifndef SETTINGS_HPP +#define SETTINGS_HPP + +#include +#include +#include + +class Settings : public QObject +{ + Q_OBJECT +public: + static Settings& getInstance(); + ~Settings(); + + void executeSettingsDialog(QWidget* parent); + + static QString getSettingsDirPath(); + + struct DhtServer + { + QString name; + QString userId; + QString address; + int port; + }; + + const QList& getDhtServerList() const; + void setDhtServerList(const QList& newDhtServerList); + + QString getUsername() const; + void setUsername(const QString& newUsername); + + QString getStatusMessage() const; + void setStatusMessage(const QString& newMessage); + + bool getEnableLogging() const; + void setEnableLogging(bool newValue); + + bool getEncryptLogs() const; + void setEncryptLogs(bool newValue); + + // Assume all widgets have unique names + // Don't use it to save every single thing you want to save, use it + // for some general purpose widgets, such as MainWindows or Splitters, + // which have widget->saveX() and widget->loadX() methods. + QByteArray getWidgetData(const QString& uniqueName) const; + void setWidgetData(const QString& uniqueName, const QByteArray& data); + + // Wrappers around getWidgetData() and setWidgetData() + // Assume widget has a unique objectName set + template + void restoreGeometryState(T* widget) const + { + widget->restoreGeometry(getWidgetData(widget->objectName() + "Geometry")); + widget->restoreState(getWidgetData(widget->objectName() + "State")); + } + template + void saveGeometryState(const T* widget) + { + setWidgetData(widget->objectName() + "Geometry", widget->saveGeometry()); + setWidgetData(widget->objectName() + "State", widget->saveState()); + } + + bool isAnimationEnabled() const; + void setAnimationEnabled(bool newValue); + + QByteArray getSmileyPack() const; + void setSmileyPack(const QByteArray &value); + + bool isCurstomEmojiFont() const; + void setCurstomEmojiFont(bool value); + + QString getEmojiFontFamily() const; + void setEmojiFontFamily(const QString &value); + + int getEmojiFontPointSize() const; + void setEmojiFontPointSize(int value); + + // ChatView + int getFirstColumnHandlePos() const; + void setFirstColumnHandlePos(const int pos); + + int getSecondColumnHandlePosFromRight() const; + void setSecondColumnHandlePosFromRight(const int pos); + + const QString &getTimestampFormat() const; + void setTimestampFormat(const QString &format); + + bool isMinimizeOnCloseEnabled() const; + void setMinimizeOnClose(bool newValue); + + // Privacy + bool isTypingNotificationEnabled() const; + void setTypingNotification(bool enabled); + +private: + Settings(); + Settings(Settings &settings) = delete; + Settings& operator=(const Settings&) = delete; + + void save(); + void load(); + + + + static const QString FILENAME; + + bool loaded; + + QList dhtServerList; + int dhtServerId; + bool dontShowDhtDialog; + + QString username; + QString statusMessage; + + bool enableLogging; + bool encryptLogs; + + QHash widgetSettings; + + // GUI + bool enableSmoothAnimation; + QByteArray smileyPack; + bool customEmojiFont; + QString emojiFontFamily; + int emojiFontPointSize; + bool minimizeOnClose; + + // ChatView + int firstColumnHandlePos; + int secondColumnHandlePosFromRight; + QString timestampFormat; + + // Privacy + bool typingNotification; + +signals: + //void dataChanged(); + void dhtServerListChanged(); + void logStorageOptsChanged(); + void smileyPackChanged(); + void emojiFontChanged(); + void timestampFormatChanged(); +}; + +#endif // SETTINGS_HPP diff --git a/settingsform.cpp b/settingsform.cpp new file mode 100644 index 000000000..ae755fab5 --- /dev/null +++ b/settingsform.cpp @@ -0,0 +1,50 @@ +#include "settingsform.h" +#include + +SettingsForm::SettingsForm() + : QObject() +{ + main = new QWidget(), head = new QWidget(); + QFont bold, small; + bold.setBold(true); + small.setPixelSize(7); + headLabel.setText("User Settings"); + headLabel.setFont(bold); + + nameLabel.setText("Name"); + statusTextLabel.setText("Status"); + idLabel.setText("Tox ID"); + id.setFont(small); + id.setTextInteractionFlags(Qt::TextSelectableByMouse); + + main->setLayout(&layout); + layout.addWidget(&nameLabel); + layout.addWidget(&name); + layout.addWidget(&statusTextLabel); + layout.addWidget(&statusText); + layout.addWidget(&idLabel); + layout.addWidget(&id); + layout.addStretch(); + + head->setLayout(&headLayout); + headLayout.addWidget(&headLabel); +} + +SettingsForm::~SettingsForm() +{ +} + +void SettingsForm::setFriendAddress(const QString& friendAddress) +{ + id.setText(friendAddress); +} + +void SettingsForm::show(Ui::Widget &ui) +{ + name.setText(ui.nameLabel->text()); + statusText.setText(ui.statusLabel->text()); + ui.mainContent->layout()->addWidget(main); + ui.mainHead->layout()->addWidget(head); + main->show(); + head->show(); +} diff --git a/settingsform.h b/settingsform.h new file mode 100644 index 000000000..ea51c1ec7 --- /dev/null +++ b/settingsform.h @@ -0,0 +1,33 @@ +#ifndef SETTINGSFORM_H +#define SETTINGSFORM_H + +#include +#include +#include +#include +#include +#include +#include "ui_widget.h" + +class SettingsForm : public QObject +{ + Q_OBJECT +public: + SettingsForm(); + ~SettingsForm(); + + void show(Ui::Widget& ui); + +public slots: + void setFriendAddress(const QString& friendAddress); + +private: + QLabel headLabel, nameLabel, statusTextLabel, idLabel, id; + QVBoxLayout layout, headLayout; + QWidget *main, *head; + +public: + QLineEdit name, statusText; +}; + +#endif // SETTINGSFORM_H diff --git a/status.cpp b/status.cpp new file mode 100644 index 000000000..4d9445e25 --- /dev/null +++ b/status.cpp @@ -0,0 +1,35 @@ +/* + Copyright (C) 2013 by Maxim Biro + + This file is part of Tox Qt GUI. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + See the COPYING file for more details. +*/ + +#include "status.h" + +const QList StatusHelper::info = +{ + {"Online", ":/icons/status_online.png"}, + {"Away", ":/icons/status_away.png"}, + {"Busy", ":/icons/status_busy.png"}, + {"Offline", ":/icons/status_offline.png"} +}; + +StatusHelper::Info StatusHelper::getInfo(int status) +{ + return info.at(status); +} + +StatusHelper::Info StatusHelper::getInfo(Status status) +{ + return StatusHelper::getInfo(static_cast(status)); +} diff --git a/status.h b/status.h new file mode 100644 index 000000000..3d753dcca --- /dev/null +++ b/status.h @@ -0,0 +1,45 @@ +/* + Copyright (C) 2013 by Maxim Biro + + This file is part of Tox Qt GUI. + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + See the COPYING file for more details. +*/ + +#ifndef STATUS_HPP +#define STATUS_HPP + +#include + +enum class Status : int {Online = 0, Away, Busy, Offline}; + +class StatusHelper +{ +public: + + static const int MAX_STATUS = static_cast(Status::Offline); + + struct Info { + QString name; + QString iconPath; + }; + + static Info getInfo(int status); + static Info getInfo(Status status); + +private: + const static QList info; + +}; + +Q_DECLARE_METATYPE(Status) + +#endif // STATUS_HPP diff --git a/toxgui.pro b/toxgui.pro new file mode 100644 index 000000000..df0fa6da8 --- /dev/null +++ b/toxgui.pro @@ -0,0 +1,66 @@ +#------------------------------------------------- +# +# Project created by QtCreator 2014-06-22T14:07:35 +# +#------------------------------------------------- + +QT += core gui + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +TARGET = toxgui +TEMPLATE = app + + +SOURCES += main.cpp\ + widget.cpp \ + core.cpp \ + status.cpp \ + settings.cpp \ + addfriendform.cpp \ + settingsform.cpp \ + editablelabelwidget.cpp \ + copyableelidelabel.cpp \ + elidelabel.cpp \ + esclineedit.cpp \ + friendlist.cpp \ + friend.cpp \ + chatform.cpp \ + chattextedit.cpp \ + friendrequestdialog.cpp \ + friendwidget.cpp \ + groupwidget.cpp \ + group.cpp \ + grouplist.cpp \ + groupchatform.cpp + +HEADERS += widget.h \ + core.h \ + status.h \ + settings.h \ + addfriendform.h \ + settingsform.h \ + editablelabelwidget.h \ + elidelabel.hpp \ + copyableelidelabel.h \ + elidelabel.h \ + esclineedit.h \ + friendlist.h \ + friend.h \ + chatform.h \ + chattextedit.h \ + friendrequestdialog.h \ + friendwidget.h \ + groupwidget.h \ + group.h \ + grouplist.h \ + groupchatform.h + +FORMS += widget.ui + +CONFIG += c++11 + +RESOURCES += \ + res.qrc + +LIBS += -ltoxcore -lsodium diff --git a/widget.cpp b/widget.cpp new file mode 100644 index 000000000..c3c445fe0 --- /dev/null +++ b/widget.cpp @@ -0,0 +1,396 @@ +#include "widget.h" +#include "ui_widget.h" +#include "settings.h" +#include "friend.h" +#include "friendlist.h" +#include "friendrequestdialog.h" +#include "friendwidget.h" +#include "grouplist.h" +#include "group.h" +#include "groupwidget.h" +#include "groupchatform.h" +#include +#include + +Widget *Widget::instance{nullptr}; + +Widget::Widget(QWidget *parent) : + QWidget(parent), + ui(new Ui::Widget) +{ + ui->setupUi(this); + ui->mainContent->setLayout(new QVBoxLayout()); + ui->mainHead->setLayout(new QVBoxLayout()); + QWidget* friendListWidget = new QWidget(); + friendListWidget->setLayout(new QVBoxLayout()); + friendListWidget->layout()->setSpacing(0); + friendListWidget->layout()->setMargin(0); + friendListWidget->setLayoutDirection(Qt::LeftToRight); + ui->friendList->setWidget(friendListWidget); + + ui->nameLabel->setText(Settings::getInstance().getUsername()); + ui->statusLabel->setText(Settings::getInstance().getStatusMessage()); + ui->friendList->widget()->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + + qRegisterMetaType("Status"); + qRegisterMetaType("uint8_t"); + + core = new Core(); + coreThread = new QThread(this); + core->moveToThread(coreThread); + connect(coreThread, &QThread::started, core, &Core::start); + + connect(core, &Core::connected, this, &Widget::onConnected); + connect(core, &Core::disconnected, this, &Widget::onDisconnected); + connect(core, &Core::failedToStart, this, &Widget::onFailedToStartCore); + connect(core, &Core::statusSet, this, &Widget::onStatusSet); + connect(core, &Core::usernameSet, this, &Widget::setUsername); + connect(core, &Core::statusMessageSet, this, &Widget::setStatusMessage); + connect(core, &Core::friendAddressGenerated, &settingsForm, &SettingsForm::setFriendAddress); + connect(core, &Core::friendAdded, this, &Widget::addFriend); + connect(core, &Core::friendStatusChanged, this, &Widget::onFriendStatusChanged); + connect(core, &Core::friendUsernameChanged, this, &Widget::onFriendUsernameChanged); + connect(core, &Core::friendStatusChanged, this, &Widget::onFriendStatusChanged); + connect(core, &Core::friendStatusMessageChanged, this, &Widget::onFriendStatusMessageChanged); + connect(core, &Core::friendUsernameLoaded, this, &Widget::onFriendUsernameLoaded); + connect(core, &Core::friendStatusMessageLoaded, this, &Widget::onFriendStatusMessageLoaded); + connect(core, &Core::friendRequestReceived, this, &Widget::onFriendRequestReceived); + connect(core, &Core::friendMessageReceived, this, &Widget::onFriendMessageReceived); + connect(core, &Core::groupInviteReceived, this, &Widget::onGroupInviteReceived); + connect(core, &Core::groupMessageReceived, this, &Widget::onGroupMessageReceived); + connect(core, &Core::groupNamelistChanged, this, &Widget::onGroupNamelistChanged); + + connect(this, &Widget::statusSet, core, &Core::setStatus); + connect(this, &Widget::friendRequested, core, &Core::requestFriendship); + connect(this, &Widget::friendRequestAccepted, core, &Core::acceptFriendRequest); + + connect(ui->addButton, SIGNAL(clicked()), this, SLOT(onAddClicked())); + connect(ui->groupButton, SIGNAL(clicked()), this, SLOT(onGroupClicked())); + connect(ui->transferButton, SIGNAL(clicked()), this, SLOT(onTransferClicked())); + connect(ui->settingsButton, SIGNAL(clicked()), this, SLOT(onSettingsClicked())); + connect(ui->nameLabel, SIGNAL(textChanged(QString,QString)), this, SLOT(onUsernameChanged(QString,QString))); + connect(ui->statusLabel, SIGNAL(textChanged(QString,QString)), this, SLOT(onStatusMessageChanged(QString,QString))); + connect(&settingsForm.name, SIGNAL(textChanged(QString)), this, SLOT(onUsernameChanged(QString))); + connect(&settingsForm.statusText, SIGNAL(textChanged(QString)), this, SLOT(onStatusMessageChanged(QString))); + connect(&friendForm, SIGNAL(friendRequested(QString,QString)), this, SIGNAL(friendRequested(QString,QString))); + + coreThread->start(); + + friendForm.show(*ui); + + // TODO: For the friendlist just stack friend widgets in a scrollable widget's layout +} + +Widget::~Widget() +{ + instance = nullptr; + coreThread->exit(); + coreThread->wait(); + delete core; + + hideMainForms(); + + for (Friend* f : FriendList::friendList) + delete f; + FriendList::friendList.clear(); + for (Group* g : GroupList::groupList) + delete g; + GroupList::groupList.clear(); + + delete ui; +} + +Widget* Widget::getInstance() +{ + if (!instance) + instance = new Widget(); + return instance; +} + +QString Widget::getUsername() +{ + return ui->nameLabel->text(); +} + +void Widget::onConnected() +{ + emit statusSet(Status::Online); +} + +void Widget::onDisconnected() +{ + emit statusSet(Status::Offline); +} + +void Widget::onFailedToStartCore() +{ + QMessageBox critical(this); + critical.setText("Toxcor failed to start, the application will terminate after you close this message."); + critical.setIcon(QMessageBox::Critical); + critical.exec(); + qApp->quit(); +} + +void Widget::onStatusSet(Status status) +{ + if (status == Status::Online) + ui->statImg->setPixmap(QPixmap("img/status/dot_online_2x.png")); + else if (status == Status::Busy || status == Status::Away) + ui->statImg->setPixmap(QPixmap("img/status/dot_idle_2x.png")); + else if (status == Status::Offline) + ui->statImg->setPixmap(QPixmap("img/status/dot_away_2x.png")); +} + +void Widget::onAddClicked() +{ + hideMainForms(); + friendForm.show(*ui); +} + +void Widget::onGroupClicked() +{ + +} + +void Widget::onTransferClicked() +{ + +} + +void Widget::onSettingsClicked() +{ + hideMainForms(); + settingsForm.show(*ui); +} + +void Widget::hideMainForms() +{ + QLayoutItem* item; + while ((item = ui->mainHead->layout()->takeAt(0)) != 0) + item->widget()->hide(); + while ((item = ui->mainContent->layout()->takeAt(0)) != 0) + item->widget()->hide(); +} + +void Widget::onUsernameChanged(const QString& newUsername) +{ + ui->nameLabel->setText(newUsername); + settingsForm.name.setText(newUsername); + core->setUsername(newUsername); +} + +void Widget::onUsernameChanged(const QString& newUsername, const QString& oldUsername) +{ + ui->nameLabel->setText(oldUsername); // restore old username until Core tells us to set it + settingsForm.name.setText(oldUsername); + core->setUsername(newUsername); +} + +void Widget::setUsername(const QString& username) +{ + ui->nameLabel->setText(username); + settingsForm.name.setText(username); + Settings::getInstance().setUsername(username); +} + +void Widget::onStatusMessageChanged(const QString& newStatusMessage) +{ + ui->statusLabel->setText(newStatusMessage); + settingsForm.statusText.setText(newStatusMessage); + core->setStatusMessage(newStatusMessage); +} + +void Widget::onStatusMessageChanged(const QString& newStatusMessage, const QString& oldStatusMessage) +{ + ui->statusLabel->setText(oldStatusMessage); // restore old status message until Core tells us to set it + settingsForm.statusText.setText(oldStatusMessage); + core->setStatusMessage(newStatusMessage); +} + +void Widget::setStatusMessage(const QString &statusMessage) +{ + ui->statusLabel->setText(statusMessage); + settingsForm.statusText.setText(statusMessage); + Settings::getInstance().setStatusMessage(statusMessage); +} + +void Widget::addFriend(int friendId, const QString &userId) +{ + Friend* newfriend = FriendList::addFriend(friendId, userId); + QWidget* widget = ui->friendList->widget(); + QLayout* layout = widget->layout(); + layout->addWidget(newfriend->widget); + connect(newfriend->widget, SIGNAL(friendWidgetClicked(FriendWidget*)), this, SLOT(onFriendWidgetClicked(FriendWidget*))); + connect(newfriend->widget, SIGNAL(removeFriend(int)), this, SLOT(removeFriend(int))); + connect(newfriend->chatForm, SIGNAL(sendMessage(int,QString)), core, SLOT(sendMessage(int,QString))); +} + +void Widget::onFriendStatusChanged(int friendId, Status status) +{ + Friend* f = FriendList::findFriend(friendId); + if (!f) + return; + + if (status == Status::Online) + f->widget->statusPic.setPixmap(QPixmap("img/status/dot_online.png")); + else if (status == Status::Busy || status == Status::Away) + f->widget->statusPic.setPixmap(QPixmap("img/status/dot_idle.png")); + else if (status == Status::Offline) + f->widget->statusPic.setPixmap(QPixmap("img/status/dot_away.png")); +} + +void Widget::onFriendStatusMessageChanged(int friendId, const QString& message) +{ + Friend* f = FriendList::findFriend(friendId); + if (!f) + return; + + f->setStatusMessage(message); +} + +void Widget::onFriendUsernameChanged(int friendId, const QString& username) +{ + Friend* f = FriendList::findFriend(friendId); + if (!f) + return; + + f->setName(username); +} + +void Widget::onFriendStatusMessageLoaded(int friendId, const QString& message) +{ + Friend* f = FriendList::findFriend(friendId); + if (!f) + return; + + f->setStatusMessage(message); +} + +void Widget::onFriendUsernameLoaded(int friendId, const QString& username) +{ + Friend* f = FriendList::findFriend(friendId); + if (!f) + return; + + f->setName(username); +} + +void Widget::onFriendWidgetClicked(FriendWidget *widget) +{ + Friend* f = FriendList::findFriend(widget->friendId); + if (!f) + return; + + hideMainForms(); + f->chatForm->show(*ui); +} + +void Widget::onFriendMessageReceived(int friendId, const QString& message) +{ + Friend* f = FriendList::findFriend(friendId); + if (!f) + return; + + f->chatForm->addFriendMessage(message); +} + +void Widget::onFriendRequestReceived(const QString& userId, const QString& message) +{ + FriendRequestDialog dialog(this, userId, message); + + if (dialog.exec() == QDialog::Accepted) + emit friendRequestAccepted(userId); +} + +void Widget::removeFriend(int friendId) +{ + Friend* f = FriendList::findFriend(friendId); + FriendList::removeFriend(friendId); + core->removeFriend(friendId); + delete f; + if (ui->mainHead->layout()->isEmpty()) + onAddClicked(); +} + +void Widget::onGroupInviteReceived(int friendId, uint8_t* publicKey) +{ + int groupId = core->joinGroupchat(friendId, publicKey); + if (groupId == -1) + { + qWarning() << "Widget::onGroupInviteReceived: Unable to accept invitation"; + return; + } + //createGroup(groupId); +} + +void Widget::onGroupMessageReceived(int groupnumber, int friendgroupnumber, const QString& message) +{ + Group* g = GroupList::findGroup(groupnumber); + if (!g) + return; + + g->chatForm->addGroupMessage(message, friendgroupnumber); +} + +void Widget::onGroupNamelistChanged(int groupnumber, int peernumber, uint8_t Change) +{ + Group* g = GroupList::findGroup(groupnumber); + if (!g) + { + qDebug() << "Widget::onGroupNamelistChanged: Group not found, creating it"; + g = createGroup(groupnumber); + } + + TOX_CHAT_CHANGE change = static_cast(Change); + if (change == TOX_CHAT_CHANGE_PEER_ADD) + g->addPeer(peernumber,""); + else if (change == TOX_CHAT_CHANGE_PEER_DEL) + g->removePeer(peernumber); + else if (change == TOX_CHAT_CHANGE_PEER_NAME) + g->updatePeer(peernumber,core->getGroupPeerName(groupnumber, peernumber)); +} + +void Widget::onGroupWidgetClicked(GroupWidget* widget) +{ + Group* g = GroupList::findGroup(widget->groupId); + if (!g) + return; + + hideMainForms(); + g->chatForm->show(*ui); +} + +void Widget::removeGroup(int groupId) +{ + Group* g = GroupList::findGroup(groupId); + GroupList::removeGroup(groupId); + core->removeGroup(groupId); + delete g; + if (ui->mainHead->layout()->isEmpty()) + onAddClicked(); +} + +const Core* Widget::getCore() +{ + return core; +} + +Group *Widget::createGroup(int groupId) +{ + Group* g = GroupList::findGroup(groupId); + if (g) + { + qWarning() << "Widget::createGroup: Group already exists"; + return g; + } + + QString groupName = QString("Groupchat #%1").arg(groupId); + Group* newgroup = GroupList::addGroup(groupId, groupName); + QWidget* widget = ui->friendList->widget(); + QLayout* layout = widget->layout(); + layout->addWidget(newgroup->widget); + connect(newgroup->widget, SIGNAL(groupWidgetClicked(GroupWidget*)), this, SLOT(onGroupWidgetClicked(GroupWidget*))); + connect(newgroup->widget, SIGNAL(removeGroup(int)), this, SLOT(removeGroup(int))); + connect(newgroup->chatForm, SIGNAL(sendMessage(int,QString)), core, SLOT(sendGroupMessage(int,QString))); + return newgroup; +} diff --git a/widget.h b/widget.h new file mode 100644 index 000000000..2d6cd8b04 --- /dev/null +++ b/widget.h @@ -0,0 +1,84 @@ +#ifndef WIDGET_H +#define WIDGET_H + +#include +#include +#include +#include "core.h" +#include "addfriendform.h" +#include "settingsform.h" + +namespace Ui { +class Widget; +} + +class GroupWidget; +class AddFriendForm; +class SettingsForm; +class FriendWidget; +class Group; + +class Widget : public QWidget +{ + Q_OBJECT + +public: + explicit Widget(QWidget *parent = 0); + QString getUsername(); + const Core* getCore(); + static Widget* getInstance(); + ~Widget(); + +signals: + void friendRequestAccepted(const QString& userId); + void friendRequested(const QString& friendAddress, const QString& message); + void statusSet(Status status); + void statusSelected(Status status); + void usernameChanged(const QString& username); + void statusMessageChanged(const QString& statusMessage); + +private slots: + void onConnected(); + void onDisconnected(); + void onStatusSet(Status status); + void onAddClicked(); + void onGroupClicked(); + void onTransferClicked(); + void onSettingsClicked(); + void onFailedToStartCore(); + void onUsernameChanged(const QString& newUsername, const QString& oldUsername); + void onStatusMessageChanged(const QString& newStatusMessage, const QString& oldStatusMessage); + void onUsernameChanged(const QString& newUsername); + void onStatusMessageChanged(const QString& newStatusMessage); + void setUsername(const QString& username); + void setStatusMessage(const QString &statusMessage); + void addFriend(int friendId, const QString& userId); + void onFriendStatusChanged(int friendId, Status status); + void onFriendStatusMessageChanged(int friendId, const QString& message); + void onFriendUsernameChanged(int friendId, const QString& username); + void onFriendStatusMessageLoaded(int friendId, const QString& message); + void onFriendUsernameLoaded(int friendId, const QString& username); + void onFriendWidgetClicked(FriendWidget* widget); + void onFriendMessageReceived(int friendId, const QString& message); + void onFriendRequestReceived(const QString& userId, const QString& message); + void onGroupInviteReceived(int friendId, uint8_t *publicKey); + void onGroupMessageReceived(int groupnumber, int friendgroupnumber, const QString& message); + void onGroupNamelistChanged(int groupnumber, int peernumber, uint8_t change); + void onGroupWidgetClicked(GroupWidget* widget); + void removeFriend(int friendId); + void removeGroup(int groupId); + +private: + void hideMainForms(); + Group* createGroup(int groupId); + +private: + Ui::Widget *ui; + Core* core; + QThread* coreThread; + AddFriendForm friendForm; + SettingsForm settingsForm; + static Widget* instance; +}; + +#endif // WIDGET_H