diff --git a/CMakeLists.txt b/CMakeLists.txt index c573fde..442ba9b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,7 +31,7 @@ add_custom_command( set(SOURCES src/main.cpp src/mainwindow.cpp - src/nvme_monitor.cpp + src/temp_monitor.cpp src/temperature_chart.cpp src/config_manager.cpp ${CMAKE_BINARY_DIR}/resources.c @@ -39,7 +39,7 @@ set(SOURCES set(HEADERS include/mainwindow.h - include/nvme_monitor.h + include/temp_monitor.h include/temperature_chart.h include/config_manager.h ) diff --git a/include/mainwindow.h b/include/mainwindow.h index a0deb4b..63c9881 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -2,7 +2,7 @@ #define MAINWINDOW_H #include -#include "nvme_monitor.h" +#include "temp_monitor.h" #include "temperature_chart.h" #include "config_manager.h" @@ -24,12 +24,13 @@ private: static void onRefreshRateChanged(GtkSpinButton *spinButton, gpointer userData); static void onClearButtonClicked(GtkButton *button, gpointer userData); static void onQuitButtonClicked(GtkButton *button, gpointer userData); + static void onColorSet(GObject *object, GParamSpec *pspec, gpointer userData); GtkWidget *window; GtkWidget *statusLabel; GtkSpinButton *refreshRateSpinBox; TemperatureChart *chart; - NvmeMonitor *monitor; + TempMonitor *monitor; ConfigManager *config; guint timerID; int refreshRateSec; diff --git a/include/nvme_monitor.h b/include/temp_monitor.h similarity index 69% rename from include/nvme_monitor.h rename to include/temp_monitor.h index ea139bf..ab022a9 100644 --- a/include/nvme_monitor.h +++ b/include/temp_monitor.h @@ -1,5 +1,5 @@ -#ifndef NVME_MONITOR_H -#define NVME_MONITOR_H +#ifndef TEMP_MONITOR_H +#define TEMP_MONITOR_H #include #include @@ -12,11 +12,11 @@ struct TemperatureData { long timestamp; }; -class NvmeMonitor { +class TempMonitor { public: - explicit NvmeMonitor(); + explicit TempMonitor(); - // Get list of available NVMe devices + // Get list of available devices std::vector getAvailableDevices(); // Read temperatures from a specific device @@ -26,9 +26,7 @@ public: std::map> getAllTemperatures(); private: - std::vector scanDevices(); - std::map findHwmonDevices(); double readTemperatureFromFile(const std::string &filePath); }; -#endif // NVME_MONITOR_H +#endif // TEMP_MONITOR_H diff --git a/include/temperature_chart.h b/include/temperature_chart.h index ab10bca..e24638e 100644 --- a/include/temperature_chart.h +++ b/include/temperature_chart.h @@ -26,18 +26,19 @@ public: GtkWidget* getWidget() const { return drawingArea; } - void addTemperatureData(const std::string &device, const std::string &sensor, + bool addTemperatureData(const std::string &device, const std::string &sensor, double temperature, int64_t timestamp); void clear(); void draw(); - // Get list of devices with their colors - std::vector> getDeviceColors() const; + // Get list of series with their colors + std::vector> getSeriesColors() const; + void setSeriesColor(const std::string &seriesName, const GdkRGBA &color); void drawChart(GtkDrawingArea *area, cairo_t *cr, int width, int height); private: void setupColors(); - GdkRGBA getColorForDevice(const std::string &device); + GdkRGBA getColorForSeries(const std::string &seriesKey); void redraw(); void updateThemeColors(); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 4929067..556023d 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -11,7 +11,7 @@ MainWindow::MainWindow() : window(nullptr), statusLabel(nullptr), refreshRateSpinBox(nullptr), chart(nullptr), monitor(nullptr), config(nullptr), timerID(0), refreshRateSec(3) { - monitor = new NvmeMonitor(); + monitor = new TempMonitor(); config = new ConfigManager(); config->load(); @@ -192,6 +192,7 @@ void MainWindow::updateTemperatures() clock_gettime(CLOCK_REALTIME, &ts); int64_t currentTime = (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; int totalReadings = 0; + bool needsLegendUpdate = false; for (auto &deviceEntry : allTemps) { const std::string &device = deviceEntry.first; @@ -201,7 +202,9 @@ void MainWindow::updateTemperatures() const std::string &sensorName = tempEntry.first; double temperature = tempEntry.second; - chart->addTemperatureData(device, sensorName, temperature, currentTime); + if (chart->addTemperatureData(device, sensorName, temperature, currentTime)) { + needsLegendUpdate = true; + } totalReadings++; } } @@ -216,8 +219,10 @@ void MainWindow::updateTemperatures() gtk_label_set_text(GTK_LABEL(statusLabel), oss.str().c_str()); - // Update legend - updateLegend(); + // Update legend ONLY if new sensors were found or legend is empty + if (needsLegendUpdate || gtk_widget_get_first_child(legendBox) == nullptr) { + updateLegend(); + } } void MainWindow::updateLegend() @@ -230,82 +235,32 @@ void MainWindow::updateLegend() child = next; } - // Get device colors from chart - auto deviceColors = chart->getDeviceColors(); + // Get series colors from chart + auto seriesColors = chart->getSeriesColors(); // Add legend items - for (const auto &pair : deviceColors) { - const std::string &device = pair.first; + for (const auto &pair : seriesColors) { + const std::string &seriesName = pair.first; const GdkRGBA &color = pair.second; // Create container for legend item GtkWidget *itemBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); - // Create color box (drawing area with fixed color) - GtkWidget *colorBox = gtk_drawing_area_new(); - gtk_widget_set_size_request(colorBox, 20, 20); + // Create color dialog and button (modern GTK4 way) + GtkColorDialog *dialog = gtk_color_dialog_new(); + GtkWidget *colorButton = gtk_color_dialog_button_new(dialog); + gtk_color_dialog_button_set_rgba(GTK_COLOR_DIALOG_BUTTON(colorButton), &color); + gtk_widget_set_size_request(colorButton, 24, 24); - // Store color in user data - GdkRGBA *colorPtr = new GdkRGBA(color); - g_object_set_data_full(G_OBJECT(colorBox), "color", colorPtr, - [](gpointer data) { delete static_cast(data); }); + // Store series name in user data to know which series to update + g_object_set_data_full(G_OBJECT(colorButton), "series-name", g_strdup(seriesName.c_str()), g_free); - gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(colorBox), - [](GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer data) { - GdkRGBA *color = static_cast(data); - cairo_set_source_rgba(cr, color->red, color->green, color->blue, color->alpha); - cairo_paint(cr); - - // Get theme colors from GTK settings - gboolean isDark = FALSE; - GtkSettings *settings = gtk_settings_get_default(); - if (settings) { - gchar *themeName = nullptr; - g_object_get(settings, "gtk-theme-name", &themeName, nullptr); - if (themeName) { - std::string theme(themeName); - isDark = (theme.find("dark") != std::string::npos || - theme.find("Dark") != std::string::npos); - g_free(themeName); - } - } - - // Try GSetting if not found in GTK settings - if (!isDark) { - GSettingsSchemaSource *source = g_settings_schema_source_get_default(); - if (source) { - GSettingsSchema *schema = g_settings_schema_source_lookup(source, "org.gnome.desktop.interface", FALSE); - if (schema) { - GSettings *gsettings = g_settings_new("org.gnome.desktop.interface"); - if (gsettings) { - gchar *gtkTheme = g_settings_get_string(gsettings, "gtk-theme"); - if (gtkTheme) { - std::string theme(gtkTheme); - isDark = (theme.find("dark") != std::string::npos || - theme.find("Dark") != std::string::npos); - g_free(gtkTheme); - } - g_object_unref(gsettings); - } - g_settings_schema_unref(schema); - } - } - } - - if (isDark) { - cairo_set_source_rgb(cr, 0.6, 0.6, 0.6); // Lighter gray for dark theme - } else { - cairo_set_source_rgb(cr, 0.3, 0.3, 0.3); // Darker gray for light theme - } - cairo_set_line_width(cr, 1); - cairo_rectangle(cr, 0, 0, width, height); - cairo_stroke(cr); - }, colorPtr, nullptr); + g_signal_connect(colorButton, "notify::rgba", G_CALLBACK(onColorSet), this); - gtk_box_append(GTK_BOX(itemBox), colorBox); + gtk_box_append(GTK_BOX(itemBox), colorButton); - // Create label with device name - GtkWidget *label = gtk_label_new(device.c_str()); + // Create label with series name + GtkWidget *label = gtk_label_new(seriesName.c_str()); gtk_box_append(GTK_BOX(itemBox), label); gtk_box_append(GTK_BOX(legendBox), itemBox); @@ -318,3 +273,16 @@ void MainWindow::show() { gtk_widget_set_visible(window, TRUE); } + +void MainWindow::onColorSet(GObject *object, GParamSpec *pspec, gpointer userData) +{ + MainWindow *self = static_cast(userData); + const char *seriesName = static_cast(g_object_get_data(object, "series-name")); + + if (seriesName) { + const GdkRGBA *color = gtk_color_dialog_button_get_rgba(GTK_COLOR_DIALOG_BUTTON(object)); + if (color) { + self->chart->setSeriesColor(seriesName, *color); + } + } +} diff --git a/src/nvme_monitor.cpp b/src/nvme_monitor.cpp deleted file mode 100644 index 3d689b5..0000000 --- a/src/nvme_monitor.cpp +++ /dev/null @@ -1,164 +0,0 @@ -#include "nvme_monitor.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -NvmeMonitor::NvmeMonitor() -{ -} - -std::vector NvmeMonitor::getAvailableDevices() -{ - return scanDevices(); -} - -double NvmeMonitor::readTemperatureFromFile(const std::string &filePath) -{ - std::ifstream file(filePath); - if (!file.is_open()) { - return 0.0; - } - - std::string line; - if (std::getline(file, line)) { - try { - // Temperature is in millidegrees Celsius, convert to Celsius - long tempMilliC = std::stol(line); - return tempMilliC / 1000.0; - } catch (const std::exception &e) { - std::cerr << "Failed to parse temperature from " << filePath << ": " << e.what() << std::endl; - } - } - file.close(); - return 0.0; -} - -std::map NvmeMonitor::findHwmonDevices() -{ - std::map hwmonMap; // Maps device name to hwmon path - - DIR* hwmonDir = opendir("/sys/class/hwmon"); - if (!hwmonDir) return hwmonMap; - - struct dirent* entry; - while ((entry = readdir(hwmonDir)) != nullptr) { - std::string hwmonName = entry->d_name; - - // Look for hwmon* directories - if (hwmonName.find("hwmon") == 0) { - std::string namePath = "/sys/class/hwmon/" + hwmonName + "/name"; - std::ifstream nameFile(namePath); - if (nameFile.is_open()) { - std::string deviceName; - if (std::getline(nameFile, deviceName)) { - // Remove trailing whitespace - deviceName.erase(deviceName.find_last_not_of(" \n\r\t") + 1); - - if (deviceName == "nvme") { - hwmonMap[hwmonName] = "/sys/class/hwmon/" + hwmonName; - } - } - nameFile.close(); - } - } - } - closedir(hwmonDir); - - return hwmonMap; -} - -std::vector NvmeMonitor::scanDevices() -{ - std::vector devices; - - DIR* devDir = opendir("/dev"); - if (!devDir) return devices; - - struct dirent* entry; - while ((entry = readdir(devDir)) != nullptr) { - std::string name = entry->d_name; - - // Only include main device entries (nvme0, nvme1, etc.), not namespaces or fabrics - if (name.find("nvme") == 0 && name.length() > 4) { - // Check if the rest after "nvme" contains only digits - bool onlyDigits = true; - for (size_t i = 4; i < name.length(); i++) { - if (!std::isdigit(name[i])) { - onlyDigits = false; - break; - } - } - if (onlyDigits) { - devices.push_back("/dev/" + name); - } - } - } - closedir(devDir); - - return devices; -} - -std::map NvmeMonitor::readTemperatures(const std::string &device) -{ - std::map temperatures; - - // Map hwmon devices to NVMe devices - auto hwmonMap = findHwmonDevices(); - - if (hwmonMap.empty()) { - std::cerr << "No NVMe hwmon devices found in /sys/class/hwmon" << std::endl; - return temperatures; - } - - // For now, read from the first NVMe device found - // In a more sophisticated implementation, we could map specific nvme devices to hwmon entries - auto firstHwmon = hwmonMap.begin(); - std::string hwmonPath = firstHwmon->second; - - // Read temperature sensors from hwmon - // temp1_input is typically the main sensor - double temp1 = readTemperatureFromFile(hwmonPath + "/temp1_input"); - if (temp1 > 0) { - temperatures["Main"] = temp1; - } - - // Check for additional temperature sensors (temp2, temp3, etc.) - for (int i = 2; i <= 10; i++) { - std::string tempPath = hwmonPath + "/temp" + std::to_string(i) + "_input"; - struct stat buffer; - if (stat(tempPath.c_str(), &buffer) == 0) { - double temp = readTemperatureFromFile(tempPath); - if (temp > 0) { - std::string sensorName = "Sensor " + std::to_string(i); - temperatures[sensorName] = temp; - } - } - } - - if (temperatures.empty()) { - std::cerr << "Failed to read temperatures from device: " << device << std::endl; - } - - return temperatures; -} - -std::map> NvmeMonitor::getAllTemperatures() -{ - std::map> allTemperatures; - - std::vector devices = scanDevices(); - for (const auto &device : devices) { - auto temps = readTemperatures(device); - if (!temps.empty()) { - allTemperatures[device] = temps; - } - } - - return allTemperatures; -} diff --git a/src/temp_monitor.cpp b/src/temp_monitor.cpp new file mode 100644 index 0000000..7d2b724 --- /dev/null +++ b/src/temp_monitor.cpp @@ -0,0 +1,124 @@ +#include "temp_monitor.h" +#include +#include +#include +#include +#include +#include +#include + +TempMonitor::TempMonitor() +{ +} + +std::vector TempMonitor::getAvailableDevices() +{ + std::vector devices; + auto all = getAllTemperatures(); + for (auto const& [name, sensors] : all) { + devices.push_back(name); + } + return devices; +} + +double TempMonitor::readTemperatureFromFile(const std::string &filePath) +{ + std::ifstream file(filePath); + if (!file.is_open()) { + return -1000.0; + } + + std::string line; + if (std::getline(file, line)) { + try { + long tempMilliC = std::stol(line); + return tempMilliC / 1000.0; + } catch (...) { + } + } + return -1000.0; +} + +std::map TempMonitor::readTemperatures(const std::string &device) +{ + auto all = getAllTemperatures(); + if (all.count(device)) { + return all.at(device); + } + return {}; +} + +std::map> TempMonitor::getAllTemperatures() +{ + std::map> allTemperatures; + + DIR* hwmonDir = opendir("/sys/class/hwmon"); + if (!hwmonDir) return allTemperatures; + + struct dirent* entry; + while ((entry = readdir(hwmonDir)) != nullptr) { + std::string hwmonName = entry->d_name; + if (hwmonName.find("hwmon") != 0) continue; + + std::string path = "/sys/class/hwmon/" + hwmonName; + + // Read "name" from sysfs as requested + std::string nameFromSysfs = "Unknown"; + std::ifstream nameFile(path + "/name"); + if (nameFile.is_open()) { + std::getline(nameFile, nameFromSysfs); + nameFromSysfs.erase(nameFromSysfs.find_last_not_of(" \n\r\t") + 1); + nameFile.close(); + } + + // Filter: Only interested in nvme and coretemp (CPU) + if (nameFromSysfs != "nvme" && nameFromSysfs != "coretemp") continue; + + std::map sensors; + DIR* dDir = opendir(path.c_str()); + if (dDir) { + struct dirent* sEntry; + while ((sEntry = readdir(dDir)) != nullptr) { + std::string fname = sEntry->d_name; + // Only temperature sensors (temp*_input) + if (fname.find("temp") == 0 && fname.find("_input") != std::string::npos) { + std::string id = fname.substr(4, fname.find("_input") - 4); + + double temp = readTemperatureFromFile(path + "/" + fname); + if (temp > -200.0) { + // Use "label" from sysfs for sensor name if available + std::string labelFromSysfs = ""; + std::ifstream labelFile(path + "/temp" + id + "_label"); + if (labelFile.is_open()) { + std::getline(labelFile, labelFromSysfs); + labelFromSysfs.erase(labelFromSysfs.find_last_not_of(" \n\r\t") + 1); + labelFile.close(); + } + + // For CPU (coretemp), filter only Package id 0 + if (nameFromSysfs == "coretemp") { + if (labelFromSysfs != "Package id 0") continue; + } + + // For NVMe, filter out Composite sensor + if (nameFromSysfs == "nvme") { + if (labelFromSysfs == "Composite") continue; + } + + std::string finalSensorName = labelFromSysfs.empty() ? "temp" + id : labelFromSysfs; + sensors[finalSensorName] = temp; + } + } + } + closedir(dDir); + } + + if (!sensors.empty()) { + // Label device with its sysfs name and instance ID + allTemperatures[nameFromSysfs + " (" + hwmonName + ")"] = sensors; + } + } + closedir(hwmonDir); + + return allTemperatures; +} diff --git a/src/temperature_chart.cpp b/src/temperature_chart.cpp index 38de490..2c77233 100644 --- a/src/temperature_chart.cpp +++ b/src/temperature_chart.cpp @@ -60,8 +60,14 @@ TemperatureChart::~TemperatureChart() { if (tickHandler) { g_source_remove(tickHandler); + tickHandler = 0; + } + + if (tooltipWindow) { + gtk_widget_unparent(tooltipWindow); + tooltipWindow = nullptr; + tooltipLabel = nullptr; } - hideTooltip(); } void TemperatureChart::setupColors() @@ -143,9 +149,9 @@ void TemperatureChart::updateThemeColors() } } -GdkRGBA TemperatureChart::getColorForDevice(const std::string &device) +GdkRGBA TemperatureChart::getColorForSeries(const std::string &seriesKey) { - if (colorMap.find(device) == colorMap.end()) { + if (colorMap.find(seriesKey) == colorMap.end()) { const GdkRGBA colors[] = { {1.0, 0.0, 0.0, 1.0}, {0.0, 0.0, 1.0, 1.0}, @@ -158,23 +164,25 @@ GdkRGBA TemperatureChart::getColorForDevice(const std::string &device) }; int colorIndex = colorMap.size() % 8; - colorMap[device] = colors[colorIndex]; + colorMap[seriesKey] = colors[colorIndex]; } - return colorMap[device]; + return colorMap[seriesKey]; } -void TemperatureChart::addTemperatureData(const std::string &device, const std::string &sensor, +bool TemperatureChart::addTemperatureData(const std::string &device, const std::string &sensor, double temperature, int64_t timestamp) { std::string seriesKey = device + " - " + sensor; + bool isNew = false; // Create series if it doesn't exist if (seriesMap.find(seriesKey) == seriesMap.end()) { SeriesData series; - series.color = getColorForDevice(device); + series.color = getColorForSeries(seriesKey); series.name = seriesKey; seriesMap[seriesKey] = series; + isNew = true; } // Add data point @@ -204,6 +212,8 @@ void TemperatureChart::addTemperatureData(const std::string &device, const std:: // Trigger redraw gtk_widget_queue_draw(drawingArea); + + return isNew; } void TemperatureChart::clear() @@ -217,7 +227,7 @@ void TemperatureChart::clear() gtk_widget_queue_draw(drawingArea); } -std::vector> TemperatureChart::getDeviceColors() const +std::vector> TemperatureChart::getSeriesColors() const { std::vector> result; for (const auto &entry : colorMap) { @@ -226,6 +236,19 @@ std::vector> TemperatureChart::getDeviceColors() return result; } +void TemperatureChart::setSeriesColor(const std::string &seriesName, const GdkRGBA &color) +{ + // Update color map + colorMap[seriesName] = color; + + // Update series if it exists + if (seriesMap.find(seriesName) != seriesMap.end()) { + seriesMap[seriesName].color = color; + } + + gtk_widget_queue_draw(drawingArea); +} + gboolean TemperatureChart::onTick(gpointer userData) { TemperatureChart *self = static_cast(userData); @@ -253,7 +276,7 @@ void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, i } // Margins - double marginLeft = 70, marginRight = 20, marginTop = 40, marginBottom = 40; + double marginLeft = 20, marginRight = 70, marginTop = 40, marginBottom = 40; double plotWidth = width - marginLeft - marginRight; double plotHeight = height - marginTop - marginBottom; @@ -280,9 +303,9 @@ void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, i // Draw axes cairo_set_source_rgb(cr, axisColor.red, axisColor.green, axisColor.blue); cairo_set_line_width(cr, 2); - cairo_move_to(cr, marginLeft, marginTop); - cairo_line_to(cr, marginLeft, marginTop + plotHeight); + cairo_move_to(cr, marginLeft + plotWidth, marginTop); cairo_line_to(cr, marginLeft + plotWidth, marginTop + plotHeight); + cairo_line_to(cr, marginLeft, marginTop + plotHeight); cairo_stroke(cr); // Draw axis labels @@ -297,12 +320,12 @@ void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, i char tempStr[16]; snprintf(tempStr, sizeof(tempStr), "%.0f°C", temp); - // Measure text width for proper right alignment + // Measure text width for proper alignment cairo_text_extents_t extents; cairo_text_extents(cr, tempStr, &extents); - // Position text right-aligned, 8 pixels before the plot area - double textX = marginLeft - 8 - extents.width; + // Position text 8 pixels after the plot area on the right + double textX = marginLeft + plotWidth + 8; cairo_move_to(cr, textX, y + 4); cairo_show_text(cr, tempStr); } @@ -331,7 +354,7 @@ void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, i // Set series color cairo_set_source_rgba(cr, series.color.red, series.color.green, series.color.blue, series.color.alpha); - cairo_set_line_width(cr, 2); + cairo_set_line_width(cr, 2.0); bool firstPoint = true; for (const DataPoint &point : series.points) { @@ -347,17 +370,6 @@ void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, i } } cairo_stroke(cr); - - // Draw dots at data points - cairo_set_source_rgba(cr, series.color.red, series.color.green, - series.color.blue, series.color.alpha); - for (const DataPoint &point : series.points) { - double x = marginLeft + plotWidth * (1.0 - (double)(maxTime - point.timestamp) / MAX_TIME_MS); - double y = marginTop + plotHeight * (1.0 - (point.temperature - minTemp) / (maxTemp - minTemp)); - - cairo_arc(cr, x, y, 3, 0, 2 * M_PI); - cairo_fill(cr); - } } // Draw title @@ -379,7 +391,7 @@ TemperatureChart::NearestPoint TemperatureChart::findNearestDataPoint(double mou } // Same margins as in drawChart - double marginLeft = 70, marginRight = 20, marginTop = 40, marginBottom = 40; + double marginLeft = 20, marginRight = 70, marginTop = 40, marginBottom = 40; double plotWidth = width - marginLeft - marginRight; double plotHeight = height - marginTop - marginBottom; @@ -421,6 +433,8 @@ void TemperatureChart::showTooltip(double x, double y, const NearestPoint &point gtk_widget_set_margin_bottom(tooltipLabel, 5); gtk_popover_set_child(GTK_POPOVER(tooltipWindow), tooltipLabel); gtk_popover_set_position(GTK_POPOVER(tooltipWindow), GTK_POS_TOP); + gtk_popover_set_has_arrow(GTK_POPOVER(tooltipWindow), TRUE); + gtk_popover_set_autohide(GTK_POPOVER(tooltipWindow), FALSE); gtk_widget_set_parent(tooltipWindow, drawingArea); } @@ -431,8 +445,10 @@ void TemperatureChart::showTooltip(double x, double y, const NearestPoint &point time_t timeSeconds = point.timestamp / 1000; struct tm *timeinfo = localtime(&timeSeconds); - char timeStr[32]; - strftime(timeStr, sizeof(timeStr), "%H:%M:%S", timeinfo); + char timeStr[32] = "??:??:??"; + if (timeinfo) { + strftime(timeStr, sizeof(timeStr), "%H:%M:%S", timeinfo); + } snprintf(tooltipText, sizeof(tooltipText), "%s\n%.1f°C\n%s", @@ -450,15 +466,15 @@ void TemperatureChart::showTooltip(double x, double y, const NearestPoint &point rect.height = 1; gtk_popover_set_pointing_to(GTK_POPOVER(tooltipWindow), &rect); - gtk_widget_set_visible(tooltipWindow, TRUE); + if (!gtk_widget_get_visible(tooltipWindow)) { + gtk_widget_set_visible(tooltipWindow, TRUE); + } } void TemperatureChart::hideTooltip() { - if (tooltipWindow) { - gtk_widget_unparent(tooltipWindow); - tooltipWindow = nullptr; - tooltipLabel = nullptr; + if (tooltipWindow && gtk_widget_get_visible(tooltipWindow)) { + gtk_widget_set_visible(tooltipWindow, FALSE); } }