added cpu, line graph, some fixes

This commit is contained in:
rdavidek 2026-01-11 00:16:32 +01:00
parent 44f968d9db
commit 1519a1bee4
8 changed files with 227 additions and 283 deletions

View File

@ -31,7 +31,7 @@ add_custom_command(
set(SOURCES set(SOURCES
src/main.cpp src/main.cpp
src/mainwindow.cpp src/mainwindow.cpp
src/nvme_monitor.cpp src/temp_monitor.cpp
src/temperature_chart.cpp src/temperature_chart.cpp
src/config_manager.cpp src/config_manager.cpp
${CMAKE_BINARY_DIR}/resources.c ${CMAKE_BINARY_DIR}/resources.c
@ -39,7 +39,7 @@ set(SOURCES
set(HEADERS set(HEADERS
include/mainwindow.h include/mainwindow.h
include/nvme_monitor.h include/temp_monitor.h
include/temperature_chart.h include/temperature_chart.h
include/config_manager.h include/config_manager.h
) )

View File

@ -2,7 +2,7 @@
#define MAINWINDOW_H #define MAINWINDOW_H
#include <gtk/gtk.h> #include <gtk/gtk.h>
#include "nvme_monitor.h" #include "temp_monitor.h"
#include "temperature_chart.h" #include "temperature_chart.h"
#include "config_manager.h" #include "config_manager.h"
@ -24,12 +24,13 @@ private:
static void onRefreshRateChanged(GtkSpinButton *spinButton, gpointer userData); static void onRefreshRateChanged(GtkSpinButton *spinButton, gpointer userData);
static void onClearButtonClicked(GtkButton *button, gpointer userData); static void onClearButtonClicked(GtkButton *button, gpointer userData);
static void onQuitButtonClicked(GtkButton *button, gpointer userData); static void onQuitButtonClicked(GtkButton *button, gpointer userData);
static void onColorSet(GObject *object, GParamSpec *pspec, gpointer userData);
GtkWidget *window; GtkWidget *window;
GtkWidget *statusLabel; GtkWidget *statusLabel;
GtkSpinButton *refreshRateSpinBox; GtkSpinButton *refreshRateSpinBox;
TemperatureChart *chart; TemperatureChart *chart;
NvmeMonitor *monitor; TempMonitor *monitor;
ConfigManager *config; ConfigManager *config;
guint timerID; guint timerID;
int refreshRateSec; int refreshRateSec;

View File

@ -1,5 +1,5 @@
#ifndef NVME_MONITOR_H #ifndef TEMP_MONITOR_H
#define NVME_MONITOR_H #define TEMP_MONITOR_H
#include <string> #include <string>
#include <map> #include <map>
@ -12,11 +12,11 @@ struct TemperatureData {
long timestamp; long timestamp;
}; };
class NvmeMonitor { class TempMonitor {
public: public:
explicit NvmeMonitor(); explicit TempMonitor();
// Get list of available NVMe devices // Get list of available devices
std::vector<std::string> getAvailableDevices(); std::vector<std::string> getAvailableDevices();
// Read temperatures from a specific device // Read temperatures from a specific device
@ -26,9 +26,7 @@ public:
std::map<std::string, std::map<std::string, double>> getAllTemperatures(); std::map<std::string, std::map<std::string, double>> getAllTemperatures();
private: private:
std::vector<std::string> scanDevices();
std::map<std::string, std::string> findHwmonDevices();
double readTemperatureFromFile(const std::string &filePath); double readTemperatureFromFile(const std::string &filePath);
}; };
#endif // NVME_MONITOR_H #endif // TEMP_MONITOR_H

View File

@ -26,18 +26,19 @@ public:
GtkWidget* getWidget() const { return drawingArea; } 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); double temperature, int64_t timestamp);
void clear(); void clear();
void draw(); void draw();
// Get list of devices with their colors // Get list of series with their colors
std::vector<std::pair<std::string, GdkRGBA>> getDeviceColors() const; std::vector<std::pair<std::string, GdkRGBA>> getSeriesColors() const;
void setSeriesColor(const std::string &seriesName, const GdkRGBA &color);
void drawChart(GtkDrawingArea *area, cairo_t *cr, int width, int height); void drawChart(GtkDrawingArea *area, cairo_t *cr, int width, int height);
private: private:
void setupColors(); void setupColors();
GdkRGBA getColorForDevice(const std::string &device); GdkRGBA getColorForSeries(const std::string &seriesKey);
void redraw(); void redraw();
void updateThemeColors(); void updateThemeColors();

View File

@ -11,7 +11,7 @@ MainWindow::MainWindow()
: window(nullptr), statusLabel(nullptr), refreshRateSpinBox(nullptr), : window(nullptr), statusLabel(nullptr), refreshRateSpinBox(nullptr),
chart(nullptr), monitor(nullptr), config(nullptr), timerID(0), refreshRateSec(3) chart(nullptr), monitor(nullptr), config(nullptr), timerID(0), refreshRateSec(3)
{ {
monitor = new NvmeMonitor(); monitor = new TempMonitor();
config = new ConfigManager(); config = new ConfigManager();
config->load(); config->load();
@ -192,6 +192,7 @@ void MainWindow::updateTemperatures()
clock_gettime(CLOCK_REALTIME, &ts); clock_gettime(CLOCK_REALTIME, &ts);
int64_t currentTime = (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; int64_t currentTime = (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
int totalReadings = 0; int totalReadings = 0;
bool needsLegendUpdate = false;
for (auto &deviceEntry : allTemps) { for (auto &deviceEntry : allTemps) {
const std::string &device = deviceEntry.first; const std::string &device = deviceEntry.first;
@ -201,7 +202,9 @@ void MainWindow::updateTemperatures()
const std::string &sensorName = tempEntry.first; const std::string &sensorName = tempEntry.first;
double temperature = tempEntry.second; double temperature = tempEntry.second;
chart->addTemperatureData(device, sensorName, temperature, currentTime); if (chart->addTemperatureData(device, sensorName, temperature, currentTime)) {
needsLegendUpdate = true;
}
totalReadings++; totalReadings++;
} }
} }
@ -216,9 +219,11 @@ void MainWindow::updateTemperatures()
gtk_label_set_text(GTK_LABEL(statusLabel), oss.str().c_str()); gtk_label_set_text(GTK_LABEL(statusLabel), oss.str().c_str());
// Update legend // Update legend ONLY if new sensors were found or legend is empty
if (needsLegendUpdate || gtk_widget_get_first_child(legendBox) == nullptr) {
updateLegend(); updateLegend();
} }
}
void MainWindow::updateLegend() void MainWindow::updateLegend()
{ {
@ -230,82 +235,32 @@ void MainWindow::updateLegend()
child = next; child = next;
} }
// Get device colors from chart // Get series colors from chart
auto deviceColors = chart->getDeviceColors(); auto seriesColors = chart->getSeriesColors();
// Add legend items // Add legend items
for (const auto &pair : deviceColors) { for (const auto &pair : seriesColors) {
const std::string &device = pair.first; const std::string &seriesName = pair.first;
const GdkRGBA &color = pair.second; const GdkRGBA &color = pair.second;
// Create container for legend item // Create container for legend item
GtkWidget *itemBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); GtkWidget *itemBox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
// Create color box (drawing area with fixed color) // Create color dialog and button (modern GTK4 way)
GtkWidget *colorBox = gtk_drawing_area_new(); GtkColorDialog *dialog = gtk_color_dialog_new();
gtk_widget_set_size_request(colorBox, 20, 20); 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 // Store series name in user data to know which series to update
GdkRGBA *colorPtr = new GdkRGBA(color); g_object_set_data_full(G_OBJECT(colorButton), "series-name", g_strdup(seriesName.c_str()), g_free);
g_object_set_data_full(G_OBJECT(colorBox), "color", colorPtr,
[](gpointer data) { delete static_cast<GdkRGBA*>(data); });
gtk_drawing_area_set_draw_func(GTK_DRAWING_AREA(colorBox), g_signal_connect(colorButton, "notify::rgba", G_CALLBACK(onColorSet), this);
[](GtkDrawingArea *area, cairo_t *cr, int width, int height, gpointer data) {
GdkRGBA *color = static_cast<GdkRGBA*>(data);
cairo_set_source_rgba(cr, color->red, color->green, color->blue, color->alpha);
cairo_paint(cr);
// Get theme colors from GTK settings gtk_box_append(GTK_BOX(itemBox), colorButton);
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 // Create label with series name
if (!isDark) { GtkWidget *label = gtk_label_new(seriesName.c_str());
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);
gtk_box_append(GTK_BOX(itemBox), colorBox);
// Create label with device name
GtkWidget *label = gtk_label_new(device.c_str());
gtk_box_append(GTK_BOX(itemBox), label); gtk_box_append(GTK_BOX(itemBox), label);
gtk_box_append(GTK_BOX(legendBox), itemBox); gtk_box_append(GTK_BOX(legendBox), itemBox);
@ -318,3 +273,16 @@ void MainWindow::show()
{ {
gtk_widget_set_visible(window, TRUE); gtk_widget_set_visible(window, TRUE);
} }
void MainWindow::onColorSet(GObject *object, GParamSpec *pspec, gpointer userData)
{
MainWindow *self = static_cast<MainWindow*>(userData);
const char *seriesName = static_cast<const char*>(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);
}
}
}

View File

@ -1,164 +0,0 @@
#include "nvme_monitor.h"
#include <cstdlib>
#include <fstream>
#include <sstream>
#include <regex>
#include <dirent.h>
#include <sys/stat.h>
#include <iostream>
#include <cctype>
#include <algorithm>
NvmeMonitor::NvmeMonitor()
{
}
std::vector<std::string> 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<std::string, std::string> NvmeMonitor::findHwmonDevices()
{
std::map<std::string, std::string> 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<std::string> NvmeMonitor::scanDevices()
{
std::vector<std::string> 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<std::string, double> NvmeMonitor::readTemperatures(const std::string &device)
{
std::map<std::string, double> 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<std::string, std::map<std::string, double>> NvmeMonitor::getAllTemperatures()
{
std::map<std::string, std::map<std::string, double>> allTemperatures;
std::vector<std::string> devices = scanDevices();
for (const auto &device : devices) {
auto temps = readTemperatures(device);
if (!temps.empty()) {
allTemperatures[device] = temps;
}
}
return allTemperatures;
}

124
src/temp_monitor.cpp Normal file
View File

@ -0,0 +1,124 @@
#include "temp_monitor.h"
#include <cstdlib>
#include <fstream>
#include <sstream>
#include <dirent.h>
#include <sys/stat.h>
#include <iostream>
#include <algorithm>
TempMonitor::TempMonitor()
{
}
std::vector<std::string> TempMonitor::getAvailableDevices()
{
std::vector<std::string> 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<std::string, double> TempMonitor::readTemperatures(const std::string &device)
{
auto all = getAllTemperatures();
if (all.count(device)) {
return all.at(device);
}
return {};
}
std::map<std::string, std::map<std::string, double>> TempMonitor::getAllTemperatures()
{
std::map<std::string, std::map<std::string, double>> 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<std::string, double> 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;
}

View File

@ -60,8 +60,14 @@ TemperatureChart::~TemperatureChart()
{ {
if (tickHandler) { if (tickHandler) {
g_source_remove(tickHandler); g_source_remove(tickHandler);
tickHandler = 0;
}
if (tooltipWindow) {
gtk_widget_unparent(tooltipWindow);
tooltipWindow = nullptr;
tooltipLabel = nullptr;
} }
hideTooltip();
} }
void TemperatureChart::setupColors() 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[] = { const GdkRGBA colors[] = {
{1.0, 0.0, 0.0, 1.0}, {1.0, 0.0, 0.0, 1.0},
{0.0, 0.0, 1.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; 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) double temperature, int64_t timestamp)
{ {
std::string seriesKey = device + " - " + sensor; std::string seriesKey = device + " - " + sensor;
bool isNew = false;
// Create series if it doesn't exist // Create series if it doesn't exist
if (seriesMap.find(seriesKey) == seriesMap.end()) { if (seriesMap.find(seriesKey) == seriesMap.end()) {
SeriesData series; SeriesData series;
series.color = getColorForDevice(device); series.color = getColorForSeries(seriesKey);
series.name = seriesKey; series.name = seriesKey;
seriesMap[seriesKey] = series; seriesMap[seriesKey] = series;
isNew = true;
} }
// Add data point // Add data point
@ -204,6 +212,8 @@ void TemperatureChart::addTemperatureData(const std::string &device, const std::
// Trigger redraw // Trigger redraw
gtk_widget_queue_draw(drawingArea); gtk_widget_queue_draw(drawingArea);
return isNew;
} }
void TemperatureChart::clear() void TemperatureChart::clear()
@ -217,7 +227,7 @@ void TemperatureChart::clear()
gtk_widget_queue_draw(drawingArea); gtk_widget_queue_draw(drawingArea);
} }
std::vector<std::pair<std::string, GdkRGBA>> TemperatureChart::getDeviceColors() const std::vector<std::pair<std::string, GdkRGBA>> TemperatureChart::getSeriesColors() const
{ {
std::vector<std::pair<std::string, GdkRGBA>> result; std::vector<std::pair<std::string, GdkRGBA>> result;
for (const auto &entry : colorMap) { for (const auto &entry : colorMap) {
@ -226,6 +236,19 @@ std::vector<std::pair<std::string, GdkRGBA>> TemperatureChart::getDeviceColors()
return result; 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) gboolean TemperatureChart::onTick(gpointer userData)
{ {
TemperatureChart *self = static_cast<TemperatureChart*>(userData); TemperatureChart *self = static_cast<TemperatureChart*>(userData);
@ -253,7 +276,7 @@ void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, i
} }
// Margins // 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 plotWidth = width - marginLeft - marginRight;
double plotHeight = height - marginTop - marginBottom; double plotHeight = height - marginTop - marginBottom;
@ -280,9 +303,9 @@ void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, i
// Draw axes // Draw axes
cairo_set_source_rgb(cr, axisColor.red, axisColor.green, axisColor.blue); cairo_set_source_rgb(cr, axisColor.red, axisColor.green, axisColor.blue);
cairo_set_line_width(cr, 2); cairo_set_line_width(cr, 2);
cairo_move_to(cr, marginLeft, marginTop); cairo_move_to(cr, marginLeft + plotWidth, marginTop);
cairo_line_to(cr, marginLeft, marginTop + plotHeight);
cairo_line_to(cr, marginLeft + plotWidth, marginTop + plotHeight); cairo_line_to(cr, marginLeft + plotWidth, marginTop + plotHeight);
cairo_line_to(cr, marginLeft, marginTop + plotHeight);
cairo_stroke(cr); cairo_stroke(cr);
// Draw axis labels // Draw axis labels
@ -297,12 +320,12 @@ void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, i
char tempStr[16]; char tempStr[16];
snprintf(tempStr, sizeof(tempStr), "%.0f°C", temp); 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_t extents;
cairo_text_extents(cr, tempStr, &extents); cairo_text_extents(cr, tempStr, &extents);
// Position text right-aligned, 8 pixels before the plot area // Position text 8 pixels after the plot area on the right
double textX = marginLeft - 8 - extents.width; double textX = marginLeft + plotWidth + 8;
cairo_move_to(cr, textX, y + 4); cairo_move_to(cr, textX, y + 4);
cairo_show_text(cr, tempStr); cairo_show_text(cr, tempStr);
} }
@ -331,7 +354,7 @@ void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, i
// Set series color // Set series color
cairo_set_source_rgba(cr, series.color.red, series.color.green, cairo_set_source_rgba(cr, series.color.red, series.color.green,
series.color.blue, series.color.alpha); series.color.blue, series.color.alpha);
cairo_set_line_width(cr, 2); cairo_set_line_width(cr, 2.0);
bool firstPoint = true; bool firstPoint = true;
for (const DataPoint &point : series.points) { for (const DataPoint &point : series.points) {
@ -347,17 +370,6 @@ void TemperatureChart::drawChart(GtkDrawingArea *area, cairo_t *cr, int width, i
} }
} }
cairo_stroke(cr); 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 // Draw title
@ -379,7 +391,7 @@ TemperatureChart::NearestPoint TemperatureChart::findNearestDataPoint(double mou
} }
// Same margins as in drawChart // 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 plotWidth = width - marginLeft - marginRight;
double plotHeight = height - marginTop - marginBottom; 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_widget_set_margin_bottom(tooltipLabel, 5);
gtk_popover_set_child(GTK_POPOVER(tooltipWindow), tooltipLabel); gtk_popover_set_child(GTK_POPOVER(tooltipWindow), tooltipLabel);
gtk_popover_set_position(GTK_POPOVER(tooltipWindow), GTK_POS_TOP); 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); 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; time_t timeSeconds = point.timestamp / 1000;
struct tm *timeinfo = localtime(&timeSeconds); struct tm *timeinfo = localtime(&timeSeconds);
char timeStr[32]; char timeStr[32] = "??:??:??";
if (timeinfo) {
strftime(timeStr, sizeof(timeStr), "%H:%M:%S", timeinfo); strftime(timeStr, sizeof(timeStr), "%H:%M:%S", timeinfo);
}
snprintf(tooltipText, sizeof(tooltipText), snprintf(tooltipText, sizeof(tooltipText),
"%s\n%.1f°C\n%s", "%s\n%.1f°C\n%s",
@ -450,15 +466,15 @@ void TemperatureChart::showTooltip(double x, double y, const NearestPoint &point
rect.height = 1; rect.height = 1;
gtk_popover_set_pointing_to(GTK_POPOVER(tooltipWindow), &rect); gtk_popover_set_pointing_to(GTK_POPOVER(tooltipWindow), &rect);
if (!gtk_widget_get_visible(tooltipWindow)) {
gtk_widget_set_visible(tooltipWindow, TRUE); gtk_widget_set_visible(tooltipWindow, TRUE);
} }
}
void TemperatureChart::hideTooltip() void TemperatureChart::hideTooltip()
{ {
if (tooltipWindow) { if (tooltipWindow && gtk_widget_get_visible(tooltipWindow)) {
gtk_widget_unparent(tooltipWindow); gtk_widget_set_visible(tooltipWindow, FALSE);
tooltipWindow = nullptr;
tooltipLabel = nullptr;
} }
} }