|
|
|
@ -28,6 +28,7 @@ extern "C" { |
|
|
|
#include <QSplitter> |
|
|
|
#include <QSplitter> |
|
|
|
#include <QCloseEvent> |
|
|
|
#include <QCloseEvent> |
|
|
|
#include <QApplication> |
|
|
|
#include <QApplication> |
|
|
|
|
|
|
|
#include <QScreen> |
|
|
|
#include <QClipboard> |
|
|
|
#include <QClipboard> |
|
|
|
#include <QStyle> |
|
|
|
#include <QStyle> |
|
|
|
#include <QMessageBox> |
|
|
|
#include <QMessageBox> |
|
|
|
@ -38,6 +39,7 @@ extern "C" { |
|
|
|
#include <cmath> |
|
|
|
#include <cmath> |
|
|
|
#include <QJsonDocument> |
|
|
|
#include <QJsonDocument> |
|
|
|
#include <QJsonObject> |
|
|
|
#include <QJsonObject> |
|
|
|
|
|
|
|
#include <csignal> |
|
|
|
|
|
|
|
|
|
|
|
extern "C" { |
|
|
|
extern "C" { |
|
|
|
#include "chat/chat_core.h" |
|
|
|
#include "chat/chat_core.h" |
|
|
|
@ -49,12 +51,16 @@ void chat_core_save_ui_state_trampoline(void* arg); |
|
|
|
|
|
|
|
|
|
|
|
static MainWindow* s_mainWindow = nullptr; |
|
|
|
static MainWindow* s_mainWindow = nullptr; |
|
|
|
|
|
|
|
|
|
|
|
/* Сохранение last_channel_id в БД (ui_state), как все остальные UI-настройки.
|
|
|
|
#ifndef _WIN32 |
|
|
|
Пишется из utun-потока через trampoline, чтобы не трогать sqlite из GUI-потока. */ |
|
|
|
static volatile std::sig_atomic_t g_signalFlag = 0; |
|
|
|
static void saveLastChannelId(const QString& channelId) { |
|
|
|
static void unixSignalHandler(int) { g_signalFlag = 1; } |
|
|
|
const char key[] = "last_channel_id"; |
|
|
|
#endif |
|
|
|
QByteArray val = channelId.toUtf8(); |
|
|
|
|
|
|
|
size_t klen = sizeof(key) - 1, vlen = (size_t)val.size(), total = klen + 1 + vlen + 1; |
|
|
|
/* Сохранение key/value в БД (ui_state). Пишется из utun-потока через trampoline,
|
|
|
|
|
|
|
|
чтобы не трогать sqlite из GUI-потока. Буфер формата key\0value\0, ≤256 байт. */ |
|
|
|
|
|
|
|
static void postUiStateKv(const char* key, const QString& value) { |
|
|
|
|
|
|
|
QByteArray val = value.toUtf8(); |
|
|
|
|
|
|
|
size_t klen = strlen(key), vlen = (size_t)val.size(), total = klen + 1 + vlen + 1; |
|
|
|
if (total > 256) return; |
|
|
|
if (total > 256) return; |
|
|
|
void* arg = u_malloc(total); |
|
|
|
void* arg = u_malloc(total); |
|
|
|
memcpy(arg, key, klen + 1); |
|
|
|
memcpy(arg, key, klen + 1); |
|
|
|
@ -62,6 +68,10 @@ static void saveLastChannelId(const QString& channelId) { |
|
|
|
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg); |
|
|
|
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static void saveLastChannelId(const QString& channelId) { |
|
|
|
|
|
|
|
postUiStateKv("last_channel_id", channelId); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
static void onMsgReceivedCallback(const char* ch_id, int ch_id_len, uint64_t author) { |
|
|
|
static void onMsgReceivedCallback(const char* ch_id, int ch_id_len, uint64_t author) { |
|
|
|
if (s_mainWindow) |
|
|
|
if (s_mainWindow) |
|
|
|
s_mainWindow->onMessageReceived(ch_id, ch_id_len, author); |
|
|
|
s_mainWindow->onMessageReceived(ch_id, ch_id_len, author); |
|
|
|
@ -136,6 +146,14 @@ MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath, |
|
|
|
setupConnects(); |
|
|
|
setupConnects(); |
|
|
|
} |
|
|
|
} |
|
|
|
connect(m_autoPlayTimer, &QTimer::timeout, this, &MainWindow::autoPlayTimerTick); |
|
|
|
connect(m_autoPlayTimer, &QTimer::timeout, this, &MainWindow::autoPlayTimerTick); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#ifndef _WIN32 |
|
|
|
|
|
|
|
std::signal(SIGINT, unixSignalHandler); |
|
|
|
|
|
|
|
std::signal(SIGTERM, unixSignalHandler); |
|
|
|
|
|
|
|
m_shutdownTimer = new QTimer(this); |
|
|
|
|
|
|
|
connect(m_shutdownTimer, &QTimer::timeout, this, &MainWindow::onShutdownCheck); |
|
|
|
|
|
|
|
m_shutdownTimer->start(200); |
|
|
|
|
|
|
|
#endif |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
MainWindow::~MainWindow() { |
|
|
|
MainWindow::~MainWindow() { |
|
|
|
@ -167,6 +185,7 @@ MainWindow::~MainWindow() { |
|
|
|
|
|
|
|
|
|
|
|
void MainWindow::setupUi() { |
|
|
|
void MainWindow::setupUi() { |
|
|
|
auto *splitter = new QSplitter(Qt::Horizontal, this); |
|
|
|
auto *splitter = new QSplitter(Qt::Horizontal, this); |
|
|
|
|
|
|
|
m_splitter = splitter; |
|
|
|
|
|
|
|
|
|
|
|
m_channelList = new ChannelList(m_db, splitter); |
|
|
|
m_channelList = new ChannelList(m_db, splitter); |
|
|
|
m_recorder = new AudioRecorder(this); |
|
|
|
m_recorder = new AudioRecorder(this); |
|
|
|
@ -186,6 +205,49 @@ void MainWindow::setupUi() { |
|
|
|
setCentralWidget(splitter); |
|
|
|
setCentralWidget(splitter); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void MainWindow::saveWindowGeometry() { |
|
|
|
|
|
|
|
QRect g = normalGeometry(); |
|
|
|
|
|
|
|
if (g.isEmpty()) g = geometry(); |
|
|
|
|
|
|
|
postUiStateKv("window_geometry", |
|
|
|
|
|
|
|
QStringLiteral("%1,%2,%3,%4").arg(g.width()).arg(g.height()).arg(g.x()).arg(g.y())); |
|
|
|
|
|
|
|
postUiStateKv("window_maximized", isMaximized() ? QStringLiteral("1") : QStringLiteral("0")); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (m_splitter) { |
|
|
|
|
|
|
|
const QList<int> sizes = m_splitter->sizes(); |
|
|
|
|
|
|
|
if (sizes.size() == 3) |
|
|
|
|
|
|
|
postUiStateKv("window_splitter", |
|
|
|
|
|
|
|
QStringLiteral("%1,%2,%3").arg(sizes[0]).arg(sizes[1]).arg(sizes[2])); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void MainWindow::restoreWindowGeometry() { |
|
|
|
|
|
|
|
if (!m_db) return; |
|
|
|
|
|
|
|
const QStringList p = m_db->getUiState("window_geometry").split(','); |
|
|
|
|
|
|
|
if (p.size() != 4) return; |
|
|
|
|
|
|
|
int w = p[0].toInt(), h = p[1].toInt(), x = p[2].toInt(), y = p[3].toInt(); |
|
|
|
|
|
|
|
if (w <= 0 || h <= 0) return; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
QRect target(x, y, w, h); |
|
|
|
|
|
|
|
bool visible = false; |
|
|
|
|
|
|
|
for (const QScreen* s : QGuiApplication::screens()) { |
|
|
|
|
|
|
|
if (s->availableGeometry().intersects(target)) { visible = true; break; } |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
if (!visible) target.moveTo(0, 0); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setGeometry(target); |
|
|
|
|
|
|
|
if (m_db->getUiStateInt("window_maximized", 0)) |
|
|
|
|
|
|
|
showMaximized(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (m_splitter) { |
|
|
|
|
|
|
|
const QStringList sp = m_db->getUiState("window_splitter").split(','); |
|
|
|
|
|
|
|
if (sp.size() == 3) { |
|
|
|
|
|
|
|
int s0 = sp[0].toInt(), s1 = sp[1].toInt(), s2 = sp[2].toInt(); |
|
|
|
|
|
|
|
if (s0 > 0 && s1 > 0 && s2 > 0) |
|
|
|
|
|
|
|
m_splitter->setSizes({s0, s1, s2}); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void MainWindow::setupNode() { |
|
|
|
void MainWindow::setupNode() { |
|
|
|
m_node = new UtunNode(this); |
|
|
|
m_node = new UtunNode(this); |
|
|
|
connect(m_node, &UtunNode::error, this, [](const QString& msg) { |
|
|
|
connect(m_node, &UtunNode::error, this, [](const QString& msg) { |
|
|
|
@ -247,12 +309,14 @@ void MainWindow::setupSoundFromConfig() { |
|
|
|
QString baseDir = QDir::isAbsolutePath(soundDir) ? soundDir : binDir.absoluteFilePath(soundDir); |
|
|
|
QString baseDir = QDir::isAbsolutePath(soundDir) ? soundDir : binDir.absoluteFilePath(soundDir); |
|
|
|
auto resolvePath = [&baseDir](const QString& f) { return f.isEmpty() ? QString() : QDir::cleanPath(baseDir + "/" + f); }; |
|
|
|
auto resolvePath = [&baseDir](const QString& f) { return f.isEmpty() ? QString() : QDir::cleanPath(baseDir + "/" + f); }; |
|
|
|
|
|
|
|
|
|
|
|
struct { const char* name; QString file; } events[] = { |
|
|
|
if (msgInFile.isEmpty()) |
|
|
|
{"msg_incoming", msgInFile}, {"msg_outgoing", msgOutFile}, |
|
|
|
sm->loadSound("msg_incoming", ":/sounds/msg_incoming.mp3"); |
|
|
|
{"connect", connectFile}, {"disconnect", disconnectFile}, |
|
|
|
else |
|
|
|
}; |
|
|
|
sm->loadSound("msg_incoming", resolvePath(msgInFile)); |
|
|
|
for (auto& e : events) { if (!e.file.isEmpty()) sm->loadSound(e.name, resolvePath(e.file)); } |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!msgOutFile.isEmpty()) sm->loadSound("msg_outgoing", resolvePath(msgOutFile)); |
|
|
|
|
|
|
|
if (!connectFile.isEmpty()) sm->loadSound("connect", resolvePath(connectFile)); |
|
|
|
|
|
|
|
if (!disconnectFile.isEmpty()) sm->loadSound("disconnect", resolvePath(disconnectFile)); |
|
|
|
if (m_db && m_db->isOpen()) { |
|
|
|
if (m_db && m_db->isOpen()) { |
|
|
|
sm->setEnabled(m_db->getUiStateInt("sound_enabled", 1)); |
|
|
|
sm->setEnabled(m_db->getUiStateInt("sound_enabled", 1)); |
|
|
|
sm->setVolume(m_db->getUiStateInt("sound_volume", 80) / 100.0f); |
|
|
|
sm->setVolume(m_db->getUiStateInt("sound_volume", 80) / 100.0f); |
|
|
|
@ -307,6 +371,7 @@ void MainWindow::setupBridgeCallbacks() { |
|
|
|
if (!s_mainWindow || !s_mainWindow->m_db) return; |
|
|
|
if (!s_mainWindow || !s_mainWindow->m_db) return; |
|
|
|
s_mainWindow->m_db->setDb(chat_core_get_db()); |
|
|
|
s_mainWindow->m_db->setDb(chat_core_get_db()); |
|
|
|
s_mainWindow->m_channelList->loadChannels(); |
|
|
|
s_mainWindow->m_channelList->loadChannels(); |
|
|
|
|
|
|
|
s_mainWindow->restoreWindowGeometry(); |
|
|
|
QString lastCid = s_mainWindow->m_db->getUiState("last_channel_id"); |
|
|
|
QString lastCid = s_mainWindow->m_db->getUiState("last_channel_id"); |
|
|
|
if (!lastCid.isEmpty()) { |
|
|
|
if (!lastCid.isEmpty()) { |
|
|
|
/* восстанавливаем канал только если он реально существует в БД —
|
|
|
|
/* восстанавливаем канал только если он реально существует в БД —
|
|
|
|
@ -402,9 +467,7 @@ void MainWindow::setupTray() { |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
QAction *quitAction = m_trayMenu->addAction("Quit"); |
|
|
|
QAction *quitAction = m_trayMenu->addAction("Quit"); |
|
|
|
connect(quitAction, &QAction::triggered, this, [this]() { |
|
|
|
connect(quitAction, &QAction::triggered, this, &MainWindow::requestShutdown); |
|
|
|
if (m_node) m_node->requestStop(); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
m_trayIcon->setContextMenu(m_trayMenu); |
|
|
|
m_trayIcon->setContextMenu(m_trayMenu); |
|
|
|
|
|
|
|
|
|
|
|
@ -416,12 +479,24 @@ void MainWindow::setupTray() { |
|
|
|
|
|
|
|
|
|
|
|
void MainWindow::closeEvent(QCloseEvent *event) { |
|
|
|
void MainWindow::closeEvent(QCloseEvent *event) { |
|
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: closeEvent — hiding to tray"); |
|
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: closeEvent — hiding to tray"); |
|
|
|
|
|
|
|
saveWindowGeometry(); |
|
|
|
hide(); |
|
|
|
hide(); |
|
|
|
m_trayIcon->showMessage("vibechat", "Application minimized to tray.", |
|
|
|
m_trayIcon->showMessage("vibechat", "Application minimized to tray.", |
|
|
|
QSystemTrayIcon::Information, 2000); |
|
|
|
QSystemTrayIcon::Information, 2000); |
|
|
|
event->ignore(); |
|
|
|
event->ignore(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void MainWindow::requestShutdown() { |
|
|
|
|
|
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: requestShutdown — saving geometry and stopping node"); |
|
|
|
|
|
|
|
saveWindowGeometry(); |
|
|
|
|
|
|
|
if (m_node) m_node->requestStop(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void MainWindow::onShutdownCheck() { |
|
|
|
|
|
|
|
if (g_signalFlag) |
|
|
|
|
|
|
|
requestShutdown(); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void MainWindow::onTrayActivated(QSystemTrayIcon::ActivationReason reason) { |
|
|
|
void MainWindow::onTrayActivated(QSystemTrayIcon::ActivationReason reason) { |
|
|
|
if (reason == QSystemTrayIcon::Trigger || |
|
|
|
if (reason == QSystemTrayIcon::Trigger || |
|
|
|
reason == QSystemTrayIcon::DoubleClick) { |
|
|
|
reason == QSystemTrayIcon::DoubleClick) { |
|
|
|
|