diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 2816f0c..fd533c1 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -21,10 +22,13 @@ #include #include #include +#include +#include #include #include #include #include +#include #include #include #include @@ -365,6 +369,7 @@ MainWindow::MainWindow(QWidget *parent) , m_rinex(new RinexNavigation(this)) { buildUi(); + buildMenus(); m_locationTimeoutTimer = new QTimer(this); m_locationTimeoutTimer->setSingleShot(true); @@ -374,11 +379,18 @@ MainWindow::MainWindow(QWidget *parent) m_uiRefreshTimer->setSingleShot(true); m_uiRefreshTimer->setInterval(UiRefreshIntervalMs); + m_portRefreshTimer = new QTimer(this); + m_portRefreshTimer->setInterval(2000); + connect(m_serial, &SerialReader::lineReceived, this, &MainWindow::handleSerialLine); connect(m_serial, &SerialReader::statusChanged, this, &MainWindow::handleSerialStatus); connect(m_serial, &SerialReader::errorOccurred, this, &MainWindow::handleSerialError); connect(m_locationTimeoutTimer, &QTimer::timeout, this, &MainWindow::handleLocationTimeout); connect(m_uiRefreshTimer, &QTimer::timeout, this, &MainWindow::flushUiUpdates); + connect(m_portRefreshTimer, &QTimer::timeout, this, [this] { + if (!m_serial->isOpen()) refreshSerialPorts(); + }); + m_portRefreshTimer->start(); connect(m_rinex, &RinexNavigation::satelliteMotionUpdated, m_globe, &GlobeWidget::setSatelliteMotion); connect(m_rinex, &RinexNavigation::statusChanged, this, @@ -399,9 +411,16 @@ MainWindow::MainWindow(QWidget *parent) rinexRefreshTimer->start(); resize(1480, 900); + loadSettings(); setWindowTitle(QStringLiteral("Canli Konum Haritasi")); } +void MainWindow::closeEvent(QCloseEvent *event) +{ + saveSettings(); + QMainWindow::closeEvent(event); +} + void MainWindow::toggleSerial() { if (m_serial->isOpen()) { @@ -475,6 +494,14 @@ void MainWindow::refreshSerialPorts() const QString configuredPort = qEnvironmentVariable("LOCATION_PLOTTER_PORT").trimmed(); const QStringList detectedPorts = detectedSerialPorts(); + if (m_portsInitialized && detectedPorts == m_lastDetectedPorts) { + return; + } + const bool deviceAdded = m_portsInitialized + && detectedPorts.size() > m_lastDetectedPorts.size(); + m_lastDetectedPorts = detectedPorts; + m_portsInitialized = true; + m_portCombo->clear(); for (const QString &port : detectedPorts) { m_portCombo->addItem(port); @@ -495,6 +522,106 @@ void MainWindow::refreshSerialPorts() m_portCombo->addItem(selectedPort); } m_portCombo->setCurrentText(selectedPort); + if (deviceAdded) { + statusBar()->showMessage(QStringLiteral("Yeni USB UART aygiti algilandi: %1") + .arg(selectedPort), 4000); + } +} + +void MainWindow::buildMenus() +{ + auto *fileMenu = menuBar()->addMenu(QStringLiteral("Dosya")); + QAction *openAction = fileMenu->addAction(QStringLiteral("Senaryo Logu Ac...")); + openAction->setObjectName(QStringLiteral("openLogAction")); + openAction->setShortcut(QKeySequence::Open); + connect(openAction, &QAction::triggered, this, &MainWindow::openTelemetryLog); + fileMenu->addSeparator(); + QAction *quitAction = fileMenu->addAction(QStringLiteral("Cikis")); + quitAction->setShortcut(QKeySequence::Quit); + connect(quitAction, &QAction::triggered, this, &QWidget::close); + + auto *connectionMenu = menuBar()->addMenu(QStringLiteral("Baglanti")); + QAction *listenAction = connectionMenu->addAction(QStringLiteral("Seri Baglantiyi Ac / Kapat")); + listenAction->setObjectName(QStringLiteral("toggleSerialAction")); + listenAction->setShortcut(QKeySequence(QStringLiteral("Ctrl+K"))); + connect(listenAction, &QAction::triggered, this, &MainWindow::toggleSerial); + QAction *portsAction = connectionMenu->addAction(QStringLiteral("Portlari Yenile")); + portsAction->setShortcut(QKeySequence(QStringLiteral("Ctrl+Shift+R"))); + connect(portsAction, &QAction::triggered, this, [this] { + m_portsInitialized = false; + refreshSerialPorts(); + }); + QAction *rinexAction = connectionMenu->addAction(QStringLiteral("RINEX Guncelle")); + rinexAction->setObjectName(QStringLiteral("rinexUpdateAction")); + rinexAction->setShortcut(QKeySequence::Refresh); + connect(rinexAction, &QAction::triggered, m_rinex, &RinexNavigation::refresh); + + auto *viewMenu = menuBar()->addMenu(QStringLiteral("Gorunum")); + QAction *fitAction = viewMenu->addAction(QStringLiteral("Rotayi Haritaya Sigdir")); + fitAction->setShortcut(QKeySequence(QStringLiteral("Ctrl+0"))); + connect(fitAction, &QAction::triggered, m_map, &MapWidget::fitToPoints); + QAction *zoomInAction = viewMenu->addAction(QStringLiteral("Yaklastir")); + zoomInAction->setShortcut(QKeySequence::ZoomIn); + connect(zoomInAction, &QAction::triggered, m_map, &MapWidget::zoomIn); + QAction *zoomOutAction = viewMenu->addAction(QStringLiteral("Uzaklastir")); + zoomOutAction->setShortcut(QKeySequence::ZoomOut); + connect(zoomOutAction, &QAction::triggered, m_map, &MapWidget::zoomOut); + + auto *helpMenu = menuBar()->addMenu(QStringLiteral("Yardim")); + QAction *aboutAction = helpMenu->addAction(QStringLiteral("Location Plotter Hakkinda")); + connect(aboutAction, &QAction::triggered, this, [this] { + QMessageBox::about(this, QStringLiteral("Location Plotter"), + QStringLiteral("GNSS telemetri, konum analizi ve RINEX uydu gorunumu\n" + "macOS surumu 0.2.0")); + }); +} + +void MainWindow::loadSettings() +{ + QSettings settings; + restoreGeometry(settings.value(QStringLiteral("window/geometry")).toByteArray()); + if (auto *splitter = findChild(QStringLiteral("mainSplitter"))) + splitter->restoreState(settings.value(QStringLiteral("window/mainSplitter")).toByteArray()); + if (auto *splitter = findChild(QStringLiteral("sideSplitter"))) + splitter->restoreState(settings.value(QStringLiteral("window/sideSplitter")).toByteArray()); + + const QString port = settings.value(QStringLiteral("serial/port")).toString(); + if (!port.isEmpty()) { + if (m_portCombo->findText(port) < 0) m_portCombo->addItem(port); + m_portCombo->setCurrentText(port); + } + m_baudCombo->setCurrentText(settings.value(QStringLiteral("serial/baud"), 115200).toString()); + m_maxPointsSpin->setValue(settings.value(QStringLiteral("telemetry/maxPoints"), 10).toInt()); + m_autoFollowCheck->setChecked(settings.value(QStringLiteral("map/autoFollow"), true).toBool()); + m_showTelemetryCheck->setChecked(settings.value(QStringLiteral("map/showTelemetry"), true).toBool()); + m_showRecentPointsCheck->setChecked(settings.value(QStringLiteral("map/showRecent"), true).toBool()); + m_showAverageCheck->setChecked(settings.value(QStringLiteral("map/showAverage"), true).toBool()); + if (auto *elevation = findChild(QStringLiteral("elevationMaskSpin"))) + elevation->setValue(settings.value(QStringLiteral("globe/elevationMask"), 10.0).toDouble()); + if (auto *tabs = findChild(QStringLiteral("mainViewTabs"))) + tabs->setCurrentIndex(settings.value(QStringLiteral("window/mainTab"), 0).toInt()); +} + +void MainWindow::saveSettings() const +{ + QSettings settings; + settings.setValue(QStringLiteral("window/geometry"), saveGeometry()); + if (auto *splitter = findChild(QStringLiteral("mainSplitter"))) + settings.setValue(QStringLiteral("window/mainSplitter"), splitter->saveState()); + if (auto *splitter = findChild(QStringLiteral("sideSplitter"))) + settings.setValue(QStringLiteral("window/sideSplitter"), splitter->saveState()); + settings.setValue(QStringLiteral("serial/port"), m_portCombo->currentText().trimmed()); + settings.setValue(QStringLiteral("serial/baud"), selectedBaudRate()); + settings.setValue(QStringLiteral("telemetry/maxPoints"), m_maxPointsSpin->value()); + settings.setValue(QStringLiteral("map/autoFollow"), m_autoFollowCheck->isChecked()); + settings.setValue(QStringLiteral("map/showTelemetry"), m_showTelemetryCheck->isChecked()); + settings.setValue(QStringLiteral("map/showRecent"), m_showRecentPointsCheck->isChecked()); + settings.setValue(QStringLiteral("map/showAverage"), m_showAverageCheck->isChecked()); + if (auto *elevation = findChild(QStringLiteral("elevationMaskSpin"))) + settings.setValue(QStringLiteral("globe/elevationMask"), elevation->value()); + if (auto *tabs = findChild(QStringLiteral("mainViewTabs"))) + settings.setValue(QStringLiteral("window/mainTab"), tabs->currentIndex()); + settings.sync(); } void MainWindow::handleSerialLine(const QString &line) @@ -1084,7 +1211,10 @@ void MainWindow::buildUi() connect(m_clearButton, &QPushButton::clicked, this, &MainWindow::clearPoints); connect(m_openLogButton, &QPushButton::clicked, this, &MainWindow::openTelemetryLog); connect(m_rinexUpdateButton, &QPushButton::clicked, m_rinex, &RinexNavigation::refresh); - connect(refreshPortsButton, &QToolButton::clicked, this, &MainWindow::refreshSerialPorts); + connect(refreshPortsButton, &QToolButton::clicked, this, [this] { + m_portsInitialized = false; + refreshSerialPorts(); + }); connect(m_maxPointsSpin, qOverload(&QSpinBox::valueChanged), this, &MainWindow::trimToMaxPoints); connect(m_autoFollowCheck, &QCheckBox::toggled, m_map, &MapWidget::setAutoFollow); diff --git a/src/MainWindow.h b/src/MainWindow.h index f3021e8..925cd67 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -18,6 +18,7 @@ class QPushButton; class QSpinBox; class QTableView; class QTimer; +class QCloseEvent; class MapWidget; class GlobeWidget; @@ -32,6 +33,9 @@ public: explicit MainWindow(QWidget *parent = nullptr); bool loadTelemetryLog(const QString &filePath); +protected: + void closeEvent(QCloseEvent *event) override; + private slots: void toggleSerial(); void clearPoints(); @@ -47,6 +51,9 @@ private slots: private: void buildUi(); + void buildMenus(); + void loadSettings(); + void saveSettings() const; void updateConnectionControls(); void addTelemetryPoint(const ParsedTelemetry &telemetry); void refreshTable(); @@ -98,6 +105,7 @@ private: QPlainTextEdit *m_log = nullptr; QTimer *m_locationTimeoutTimer = nullptr; QTimer *m_uiRefreshTimer = nullptr; + QTimer *m_portRefreshTimer = nullptr; QVector m_points; QStringList m_pendingLogLines; @@ -108,4 +116,6 @@ private: DopValues m_lastDop; ParsedTelemetry m_pendingTelemetry; bool m_telemetryRefreshPending = false; + bool m_portsInitialized = false; + QStringList m_lastDetectedPorts; }; diff --git a/tests/main_window_tests.cpp b/tests/main_window_tests.cpp index 04326d9..9c19132 100644 --- a/tests/main_window_tests.cpp +++ b/tests/main_window_tests.cpp @@ -2,6 +2,7 @@ #include "../src/MapWidget.h" #include +#include #include #include #include @@ -56,6 +57,9 @@ int main(int argc, char **argv) auto *mainViewTabs = window.findChild(QStringLiteral("mainViewTabs")); auto *sideSplitter = window.findChild(QStringLiteral("sideSplitter")); auto *rinexUpdateButton = window.findChild(QStringLiteral("rinexUpdateButton")); + auto *openLogAction = window.findChild(QStringLiteral("openLogAction")); + auto *toggleSerialAction = window.findChild(QStringLiteral("toggleSerialAction")); + auto *rinexUpdateAction = window.findChild(QStringLiteral("rinexUpdateAction")); assert(table != nullptr); assert(referenceLatInput != nullptr); assert(referenceLonInput != nullptr); @@ -85,6 +89,9 @@ int main(int argc, char **argv) assert(sideSplitter->orientation() == Qt::Vertical); assert(rinexUpdateButton != nullptr); assert(!rinexUpdateButton->toolTip().isEmpty()); + assert(openLogAction != nullptr && !openLogAction->shortcut().isEmpty()); + assert(toggleSerialAction != nullptr && !toggleSerialAction->shortcut().isEmpty()); + assert(rinexUpdateAction != nullptr && !rinexUpdateAction->shortcut().isEmpty()); assert(QMetaObject::invokeMethod(map, "referenceLocationSelected", Qt::DirectConnection, Q_ARG(double, 40.25), Q_ARG(double, 29.5)));