diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index a2f5f9368..0b4bd7b51 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -51,6 +51,7 @@ extern "C" { #include #include #include +#include #include #include #include @@ -199,6 +200,8 @@ MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath, } MainWindow::~MainWindow() { + delete m_videoPlayer.data(); + delete m_imageViewer.data(); gui_bridge_set_radio_call_cb(nullptr); if (m_radioCallWindow) { m_radioCallId = 0; delete m_radioCallWindow.data(); } if (m_streamPublishWindow) delete m_streamPublishWindow.data(); @@ -324,6 +327,98 @@ void MainWindow::restoreWindowGeometry() { } } +/* Один снимок на профиль; кэш обновляется до асинхронной записи, поэтому быстрое переоткрытие видит новую геометрию. */ +void MainWindow::saveMediaWindowGeometry(QWidget* window) { + const QRect normal = window->normalGeometry(); + const QScreen* display = window->screen(); + if (!normal.isValid() || !display) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "MediaWindow: cannot save geometry window=%s valid=%d screen=%d", + qPrintable(window->objectName()), normal.isValid(), display != nullptr); + return; + } + const QString mode = window->isFullScreen() ? "fullscreen" : window->isMaximized() ? "maximized" : "normal"; + m_mediaWindowState = {{"x", normal.x()}, {"y", normal.y()}, {"width", normal.width()}, {"height", normal.height()}, + {"mode", mode}, {"screen", display->name()}, {"serial", display->serialNumber()}, + {"screen_x", display->availableGeometry().x()}, {"screen_y", display->availableGeometry().y()}}; + m_mediaWindowStateLoaded = true; + const QByteArray value = QJsonDocument(m_mediaWindowState).toJson(QJsonDocument::Compact); + const int queued = gui_bridge_post_setting(GUI_SETTING_UI_STATE, "media_window_geometry", value.constData()); + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "MediaWindow: cached %s rect=%d,%d %dx%d mode=%s monitor=%s serial=%s queued=%d", + qPrintable(window->objectName()), normal.x(), normal.y(), normal.width(), normal.height(), qPrintable(mode), + qPrintable(display->name()), qPrintable(display->serialNumber()), queued == 0); + if (queued < 0) DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "MediaWindow: geometry remains cached; database write was not queued"); +} + +/* Имя/serial определяют физический монитор; относительное смещение сохраняется и при перестановке мониторов. */ +void MainWindow::restoreMediaWindowGeometry(QWidget* window) { + if (!m_mediaWindowStateLoaded && m_db && m_db->isOpen()) { + m_mediaWindowStateLoaded = true; + const QByteArray value = m_db->getUiState("media_window_geometry").toUtf8(); + if (!value.isEmpty()) { + QJsonParseError error; + const auto document = QJsonDocument::fromJson(value, &error); + if (error.error != QJsonParseError::NoError || !document.isObject()) + DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "MediaWindow: invalid saved state bytes=%d error=%s", int(value.size()), + qPrintable(error.errorString())); + else m_mediaWindowState = document.object(); + } + } + const QString mode = m_mediaWindowState.value("mode").toString(); + const int savedWidth = m_mediaWindowState.value("width").toInt(); + const int savedHeight = m_mediaWindowState.value("height").toInt(); + const bool saved = savedWidth > 0 && savedHeight > 0 && (mode == "normal" || mode == "maximized" || mode == "fullscreen"); + if (!m_mediaWindowState.isEmpty() && !saved) + DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "MediaWindow: invalid saved geometry width=%d height=%d mode=%s", + savedWidth, savedHeight, qPrintable(mode)); + const QString name = m_mediaWindowState.value("screen").toString(); + const QString serial = m_mediaWindowState.value("serial").toString(); + QScreen* display = nullptr; + if (saved) { + for (auto* candidate : QGuiApplication::screens()) { + if (candidate->name() == name && (serial.isEmpty() || candidate->serialNumber() == serial)) { + display = candidate; break; + } + } + if (!display && !serial.isEmpty()) { + for (auto* candidate : QGuiApplication::screens()) { + if (candidate->serialNumber() == serial) { display = candidate; break; } + } + } + } + if (!display) { + display = screen() ? screen() : QGuiApplication::primaryScreen(); + if (saved) DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "MediaWindow: saved monitor unavailable name=%s serial=%s fallback=%s", + qPrintable(name), qPrintable(serial), display ? qPrintable(display->name()) : "none"); + } + if (!display) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "MediaWindow: no physical screen available for restoration"); return; } + const QRect available = display->availableGeometry(); + if (!available.isValid()) { + DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "MediaWindow: monitor has no available area name=%s", qPrintable(display->name())); return; + } + window->setMinimumSize(window->minimumSize().boundedTo(available.size())); + const QSize size = QSize(saved ? savedWidth : 800, saved ? savedHeight : 600) + .expandedTo(window->minimumSize()).boundedTo(available.size()); + qint64 x = available.x() + (available.width() - size.width()) / 2; + qint64 y = available.y() + (available.height() - size.height()) / 2; + if (saved) { + x = qint64(m_mediaWindowState.value("x").toInt()) - m_mediaWindowState.value("screen_x").toInt() + available.x(); + y = qint64(m_mediaWindowState.value("y").toInt()) - m_mediaWindowState.value("screen_y").toInt() + available.y(); + } + const QRect target(int(qBound(available.left(), x, available.right() - size.width() + 1)), + int(qBound(available.top(), y, available.bottom() - size.height() + 1)), size.width(), size.height()); + if (saved && (target.x() != x || target.y() != y || size != QSize(savedWidth, savedHeight))) + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "MediaWindow: fitted saved rect into monitor=%s area=%d,%d %dx%d", + qPrintable(display->name()), available.x(), available.y(), available.width(), available.height()); + window->winId(); + window->windowHandle()->setScreen(display); + window->setGeometry(target); + window->setWindowState(!saved || mode == "normal" ? Qt::WindowNoState + : mode == "fullscreen" ? Qt::WindowFullScreen : Qt::WindowMaximized); + DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "MediaWindow: restored %s saved=%d rect=%d,%d %dx%d mode=%s monitor=%s serial=%s", + qPrintable(window->objectName()), saved, target.x(), target.y(), target.width(), target.height(), + saved ? qPrintable(mode) : "normal", qPrintable(display->name()), qPrintable(display->serialNumber())); +} + void MainWindow::setupNode() { m_node = new UtunNode(this); connect(m_node, &UtunNode::error, this, [](const QString& msg) { @@ -1274,6 +1369,10 @@ void MainWindow::requestShutdown() { } DEBUG_DEBUG(DEBUG_CATEGORY_GENERAL, "MainWindow: requestShutdown — saving geometry and stopping node"); if (m_messageNotification) m_messageNotification->dismiss("shutdown"); + QPointer first = m_imageViewer.data(), last = m_videoPlayer.data(); + if (first && first->isActiveWindow()) std::swap(first, last); // Активный просмотр закрывается последним и задаёт общий снимок. + if (first) first->close(); + if (last) last->close(); saveWindowGeometry(); if (m_messageList) m_messageList->saveChannelState(); // FIFO: stop исполняется после уже принятых записей UI. Node живёт до join его worker. @@ -1353,9 +1452,13 @@ void MainWindow::showSettings() { void MainWindow::openVideo(const QString& filePath) { if (!m_videoPlayer) { m_videoPlayer = new VideoPlayerWindow(nullptr); - connect(m_videoPlayer, &VideoPlayerWindow::closed, this, [this] { - m_videoPlayer->deleteLater(); + m_videoPlayer->setObjectName(QStringLiteral("chatVideoPlayer")); + restoreMediaWindowGeometry(m_videoPlayer); + auto* player = m_videoPlayer.data(); + connect(player, &VideoPlayerWindow::closed, this, [this, player] { + saveMediaWindowGeometry(player); m_videoPlayer = nullptr; + player->deleteLater(); }); } else { m_videoPlayer->raise(); @@ -1367,16 +1470,20 @@ void MainWindow::openVideo(const QString& filePath) { void MainWindow::openImage(const QString& filePath) { if (!m_imageViewer) { m_imageViewer = new ImageViewerWindow(nullptr); - connect(m_imageViewer, &ImageViewerWindow::closed, this, [this] { - m_imageViewer->deleteLater(); + m_imageViewer->setObjectName(QStringLiteral("chatImageViewer")); + restoreMediaWindowGeometry(m_imageViewer); + auto* viewer = m_imageViewer.data(); + connect(viewer, &ImageViewerWindow::closed, this, [this, viewer] { + saveMediaWindowGeometry(viewer); m_imageViewer = nullptr; + viewer->deleteLater(); }); } else { m_imageViewer->raise(); m_imageViewer->activateWindow(); } m_imageViewer->loadFile(filePath); - m_imageViewer->showFullScreen(); + m_imageViewer->show(); } void MainWindow::onMessageReceived(const char* ch_id, int ch_id_len, uint64_t author, bool initialSync) { diff --git a/tools/chatgui/src/mainwindow.h b/tools/chatgui/src/mainwindow.h index daecd1d41..e54f50166 100644 --- a/tools/chatgui/src/mainwindow.h +++ b/tools/chatgui/src/mainwindow.h @@ -11,6 +11,7 @@ #include #include #include +#include #include class ChannelList; @@ -134,6 +135,10 @@ private: void saveWindowGeometry(); // Восстановить размеры в пределах доступных экранов. void restoreWindowGeometry(); + // Сохранить общую геометрию фото/видео и идентификатор физического монитора в кэш и owned ui_state-команду. + void saveMediaWindowGeometry(QWidget* window); + // Восстановить геометрию/режим на прежнем физическом мониторе; без данных — обычное окно по центру текущего. + void restoreMediaWindowGeometry(QWidget* window); void setupTray(); void showIncomingMessageNotification(const QString& target, quint64 author); // Запустить UtunNode; stopping отсоединяет SQLite до native teardown. @@ -176,12 +181,14 @@ private: QHash> m_dmDeleteRequests; /* request_id → conv_id и удаляемое поколение */ AudioRecorder *m_recorder = nullptr; // Qt child, shutdown до остановки ядра. // Отдельные media windows; raw pointers обнуляются destroyed/closed callbacks, QPointer — автоматически. - VideoPlayerWindow *m_videoPlayer = nullptr; + QPointer m_videoPlayer; QPointer m_streamPlayer; QPointer m_streamGrid; StreamPublishEngine* m_streamPublisher = nullptr; // Собственный publisher, удаляется до node. QPointer m_streamPublishWindow; - ImageViewerWindow *m_imageViewer = nullptr; + QPointer m_imageViewer; + QJsonObject m_mediaWindowState; // Общий снимок фото/видео: обычный rect, режим, имя/serial и начало области монитора. + bool m_mediaWindowStateLoaded = false; // Снимок прочитан из открытой БД либо уже обновлён при закрытии окна. QPointer m_fileBrowser; QQueue m_autoPlayQueue; // Последовательный playback чужих voice, ожидание загрузки головы.