diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index c70b6302f0..6c22c458f9 100755 --- a/makepanda/makepanda.py +++ b/makepanda/makepanda.py @@ -5848,7 +5848,7 @@ if not PkgSkip("PANDATOOL") and (GetTarget() == 'windows' or not PkgSkip("GTK3") TargetAdd('pstats.exe', input='libp3progbase.lib') TargetAdd('pstats.exe', input='libp3pandatoolbase.lib') TargetAdd('pstats.exe', input=COMMON_PANDA_LIBS) - TargetAdd('pstats.exe', opts=['SUBSYSTEM:WINDOWS', 'WINSOCK', 'WINIMM', 'WINGDI', 'WINKERNEL', 'WINOLDNAMES', 'WINUSER', 'WINMM', 'GTK3']) + TargetAdd('pstats.exe', opts=['SUBSYSTEM:WINDOWS', 'WINCOMCTL', 'WINSOCK', 'WINIMM', 'WINGDI', 'WINKERNEL', 'WINOLDNAMES', 'WINUSER', 'WINMM', 'GTK3']) # # DIRECTORY: pandatool/src/xfileprogs/ diff --git a/pandatool/src/gtk-stats/CMakeLists.txt b/pandatool/src/gtk-stats/CMakeLists.txt index 9c0c8c0944..f44270de6a 100644 --- a/pandatool/src/gtk-stats/CMakeLists.txt +++ b/pandatool/src/gtk-stats/CMakeLists.txt @@ -4,6 +4,7 @@ endif() set(GTKSTATS_HEADERS gtkStatsChartMenu.h + gtkStatsFlameGraph.h gtkStatsGraph.h gtkStatsLabel.h gtkStatsLabelStack.h @@ -17,6 +18,7 @@ set(GTKSTATS_HEADERS set(GTKSTATS_SOURCES gtkStats.cxx gtkStatsChartMenu.cxx + gtkStatsFlameGraph.cxx gtkStatsGraph.cxx gtkStatsLabel.cxx gtkStatsLabelStack.cxx diff --git a/pandatool/src/gtk-stats/gtkStatsChartMenu.cxx b/pandatool/src/gtk-stats/gtkStatsChartMenu.cxx index 6a67588b1e..3c23398962 100644 --- a/pandatool/src/gtk-stats/gtkStatsChartMenu.cxx +++ b/pandatool/src/gtk-stats/gtkStatsChartMenu.cxx @@ -116,20 +116,34 @@ do_update() { } } - // Also a menu item for a piano roll (following a separator). + // Also menu items for flame graph and piano roll (following a separator). GtkWidget *sep = gtk_separator_menu_item_new(); gtk_widget_show(sep); gtk_menu_shell_append(GTK_MENU_SHELL(_menu), sep); - GtkStatsMonitor::MenuDef smd(_thread_index, -1, false); - const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd); + { + GtkStatsMonitor::MenuDef smd(_thread_index, -2, false); + const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd); - GtkWidget *menu_item = gtk_menu_item_new_with_label("Piano Roll"); - gtk_widget_show(menu_item); - gtk_menu_shell_append(GTK_MENU_SHELL(_menu), menu_item); + GtkWidget *menu_item = gtk_menu_item_new_with_label("Flame Graph"); + gtk_widget_show(menu_item); + gtk_menu_shell_append(GTK_MENU_SHELL(_menu), menu_item); - g_signal_connect_swapped(G_OBJECT(menu_item), "activate", - G_CALLBACK(handle_menu), (void *)(const void *)menu_def); + g_signal_connect_swapped(G_OBJECT(menu_item), "activate", + G_CALLBACK(handle_menu), (void *)(const void *)menu_def); + } + + { + GtkStatsMonitor::MenuDef smd(_thread_index, -1, false); + const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd); + + GtkWidget *menu_item = gtk_menu_item_new_with_label("Piano Roll"); + gtk_widget_show(menu_item); + gtk_menu_shell_append(GTK_MENU_SHELL(_menu), menu_item); + + g_signal_connect_swapped(G_OBJECT(menu_item), "activate", + G_CALLBACK(handle_menu), (void *)(const void *)menu_def); + } } /** @@ -189,9 +203,13 @@ handle_menu(gpointer data) { return; } - if (menu_def->_collector_index < 0) { + if (menu_def->_collector_index == -2) { + monitor->open_flame_graph(menu_def->_thread_index); + } + else if (menu_def->_collector_index < 0) { monitor->open_piano_roll(menu_def->_thread_index); - } else { + } + else { monitor->open_strip_chart(menu_def->_thread_index, menu_def->_collector_index, menu_def->_show_level); diff --git a/pandatool/src/gtk-stats/gtkStatsFlameGraph.cxx b/pandatool/src/gtk-stats/gtkStatsFlameGraph.cxx new file mode 100644 index 0000000000..bb52f1374c --- /dev/null +++ b/pandatool/src/gtk-stats/gtkStatsFlameGraph.cxx @@ -0,0 +1,551 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file gtkStatsFlameGraph.cxx + * @author rdb + * @date 2022-02-02 + */ + +#include "gtkStatsFlameGraph.h" +#include "gtkStatsLabel.h" +#include "gtkStatsMonitor.h" +#include "pStatCollectorDef.h" + +static const int default_flame_graph_width = 800; +static const int default_flame_graph_height = 150; + +/** + * + */ +GtkStatsFlameGraph:: +GtkStatsFlameGraph(GtkStatsMonitor *monitor, int thread_index, + int collector_index) : + PStatFlameGraph(monitor, monitor->get_view(thread_index), + thread_index, collector_index, + default_flame_graph_width, + default_flame_graph_height), + GtkStatsGraph(monitor) +{ + // Let's show the units on the guide bar labels. There's room. + set_guide_bar_units(get_guide_bar_units() | GBU_show_units); + + // Put some stuff on top of the graph. + _top_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_box_pack_start(GTK_BOX(_graph_vbox), _top_hbox, + FALSE, FALSE, 0); + + _average_check_box = gtk_check_button_new_with_label("Average"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(_average_check_box), TRUE); + g_signal_connect(G_OBJECT(_average_check_box), "toggled", + G_CALLBACK(toggled_callback), this); + + // Add a DrawingArea widget on top of the graph, to display all of the scale + // units. + _scale_area = gtk_drawing_area_new(); + g_signal_connect(G_OBJECT(_scale_area), "draw", G_CALLBACK(draw_callback), this); + + _total_label = gtk_label_new(""); + gtk_box_pack_start(GTK_BOX(_top_hbox), _average_check_box, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(_top_hbox), _scale_area, TRUE, TRUE, 0); + gtk_box_pack_end(GTK_BOX(_top_hbox), _total_label, FALSE, FALSE, 0); + + gtk_widget_set_size_request(_graph_window, default_flame_graph_width, + default_flame_graph_height); + + // Add a fixed container to the overlay to allow arbitrary positioning + // of labels therein. + _fixed = gtk_fixed_new(); + gtk_overlay_add_overlay(GTK_OVERLAY(_graph_overlay), _fixed); + + gtk_widget_show_all(_window); + gtk_widget_show(_window); + + // Allow the window to be resized as small as the user likes. We have to do + // this after the window has been shown; otherwise, it will affect the + // window's initial size. + gtk_widget_set_size_request(_window, 0, 0); + + clear_region(); +} + +/** + * + */ +GtkStatsFlameGraph:: +~GtkStatsFlameGraph() { +} + +/** + * Called whenever a new Collector definition is received from the client. + */ +void GtkStatsFlameGraph:: +new_collector(int collector_index) { + GtkStatsGraph::new_collector(collector_index); +} + +/** + * Called as each frame's data is made available. There is no guarantee the + * frames will arrive in order, or that all of them will arrive at all. The + * monitor should be prepared to accept frames received out-of-order or + * missing. + */ +void GtkStatsFlameGraph:: +new_data(int thread_index, int frame_number) { + if (is_title_unknown()) { + std::string window_title = get_title_text(); + if (!is_title_unknown()) { + gtk_window_set_title(GTK_WINDOW(_window), window_title.c_str()); + } + } + + if (!_pause) { + update(); + + std::string text = format_number(get_horizontal_scale(), get_guide_bar_units(), get_guide_bar_unit_name()); + if (_net_value_text != text) { + _net_value_text = text; + gtk_label_set_text(GTK_LABEL(_total_label), _net_value_text.c_str()); + } + } +} + +/** + * Called when it is necessary to redraw the entire graph. + */ +void GtkStatsFlameGraph:: +force_redraw() { + PStatFlameGraph::force_redraw(); +} + +/** + * Called when the user has resized the window, forcing a resize of the graph. + */ +void GtkStatsFlameGraph:: +changed_graph_size(int graph_xsize, int graph_ysize) { + PStatFlameGraph::changed_size(graph_xsize, graph_ysize); +} + +/** + * Called when the user selects a new time units from the monitor pulldown + * menu, this should adjust the units for the graph to the indicated mask if + * it is a time-based graph. + */ +void GtkStatsFlameGraph:: +set_time_units(int unit_mask) { + int old_unit_mask = get_guide_bar_units(); + if ((old_unit_mask & (GBU_hz | GBU_ms)) != 0) { + unit_mask = unit_mask & (GBU_hz | GBU_ms); + unit_mask |= (old_unit_mask & GBU_show_units); + set_guide_bar_units(unit_mask); + + gtk_widget_queue_draw(_scale_area); + } +} + +/** + * Called when the user single-clicks on a label. + */ +void GtkStatsFlameGraph:: +on_click_label(int collector_index) { + int prev_collector_index = get_collector_index(); + if (collector_index == prev_collector_index && collector_index != 0) { + // Clicking on the top label means to go up to the parent level. + const PStatClientData *client_data = + GtkStatsGraph::_monitor->get_client_data(); + if (client_data->has_collector(collector_index)) { + const PStatCollectorDef &def = + client_data->get_collector_def(collector_index); + collector_index = def._parent_index; + set_collector_index(collector_index); + } + } + else { + // Clicking on any other label means to focus on that. + set_collector_index(collector_index); + } + + // Change the root collector to show the full name. + if (prev_collector_index != collector_index) { + auto it = _labels.find(prev_collector_index); + if (it != _labels.end()) { + it->second->update_text(false); + } + it = _labels.find(collector_index); + if (it != _labels.end()) { + it->second->update_text(true); + } + } +} + +/** + * Called when the user hovers the mouse over a label. + */ +void GtkStatsFlameGraph:: +on_enter_label(int collector_index) { + if (collector_index != _highlighted_index) { + _highlighted_index = collector_index; + } +} + +/** + * Called when the user's mouse cursor leaves a label. + */ +void GtkStatsFlameGraph:: +on_leave_label(int collector_index) { + if (collector_index == _highlighted_index && collector_index != -1) { + _highlighted_index = -1; + } +} + +/** + * Called when the mouse hovers over a label, and should return the text that + * should appear on the tooltip. + */ +std::string GtkStatsFlameGraph:: +get_label_tooltip(int collector_index) const { + return PStatFlameGraph::get_label_tooltip(collector_index); +} + +/** + * Repositions the labels. + */ +void GtkStatsFlameGraph:: +update_labels() { + PStatFlameGraph::update_labels(); +} + +/** + * Repositions a label. If width is 0, the label should be deleted. + */ +void GtkStatsFlameGraph:: +update_label(int collector_index, int row, int x, int width) { + GtkStatsLabel *label; + + auto it = _labels.find(collector_index); + if (it != _labels.end()) { + label = it->second; + if (width == 0) { + gtk_container_remove(GTK_CONTAINER(_fixed), label->get_widget()); + delete label; + _labels.erase(it); + return; + } + gtk_fixed_move(GTK_FIXED(_fixed), label->get_widget(), x, _ysize - (row + 1) * label->get_height()); + } + else { + if (width == 0) { + return; + } + label = new GtkStatsLabel(GtkStatsGraph::_monitor, this, _thread_index, collector_index, false, false); + _labels[collector_index] = label; + gtk_fixed_put(GTK_FIXED(_fixed), label->get_widget(), x, _ysize - (row + 1) * label->get_height()); + } + + gtk_widget_set_size_request(label->get_widget(), std::min(width, _xsize), label->get_height()); +} + +/** + * Calls update_guide_bars with parameters suitable to this kind of graph. + */ +void GtkStatsFlameGraph:: +normal_guide_bars() { + // We want vaguely 100 pixels between guide bars. + double res = gdk_screen_get_resolution(gdk_screen_get_default()); + int num_bars = (int)(get_xsize() / (100.0 * (res > 0 ? res / 96.0 : 1.0))); + + _guide_bars.clear(); + + double dist = get_horizontal_scale() / num_bars; + + for (int i = 1; i < num_bars; ++i) { + _guide_bars.push_back(make_guide_bar(i * dist)); + } + + _guide_bars_changed = true; +} + +/** + * Erases the chart area. + */ +void GtkStatsFlameGraph:: +clear_region() { + cairo_set_source_rgb(_cr, 1.0, 1.0, 1.0); + cairo_paint(_cr); +} + +/** + * Erases the chart area in preparation for drawing a bunch of bars. + */ +void GtkStatsFlameGraph:: +begin_draw() { + clear_region(); + + // Draw in the guide bars. + int num_guide_bars = get_num_guide_bars(); + for (int i = 0; i < num_guide_bars; i++) { + draw_guide_bar(_cr, get_guide_bar(i)); + } +} + +/** + * Called after all the bars have been drawn, this triggers a refresh event to + * draw it to the window. + */ +void GtkStatsFlameGraph:: +end_draw() { + gtk_widget_queue_draw(_graph_window); +} + +/** + * Called at the end of the draw cycle. + */ +void GtkStatsFlameGraph:: +idle() { +} + +/** + * This is called during the servicing of the draw event; it gives a derived + * class opportunity to do some further painting into the graph window. + */ +void GtkStatsFlameGraph:: +additional_graph_window_paint(cairo_t *cr) { + int num_user_guide_bars = get_num_user_guide_bars(); + for (int i = 0; i < num_user_guide_bars; i++) { + draw_guide_bar(cr, get_user_guide_bar(i)); + } +} + +/** + * Based on the mouse position within the window's client area, look for + * draggable things the mouse might be hovering over and return the + * apprioprate DragMode enum or DM_none if nothing is indicated. + */ +GtkStatsGraph::DragMode GtkStatsFlameGraph:: +consider_drag_start(int graph_x, int graph_y) { + if (graph_y >= 0 && graph_y < get_ysize()) { + if (graph_x >= 0 && graph_x < get_xsize()) { + // See if the mouse is over a user-defined guide bar. + int x = graph_x; + double from_height = pixel_to_height(x - 2); + double to_height = pixel_to_height(x + 2); + _drag_guide_bar = find_user_guide_bar(from_height, to_height); + if (_drag_guide_bar >= 0) { + return DM_guide_bar; + } + + } else { + // The mouse is left or right of the graph; maybe create a new guide + // bar. + return DM_new_guide_bar; + } + } + + return DM_none; +} + +/** + * Called when the mouse button is depressed within the graph window. + */ +gboolean GtkStatsFlameGraph:: +handle_button_press(GtkWidget *widget, int graph_x, int graph_y, + bool double_click) { + if (graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) { + if (double_click) { + // Clicking on whitespace in the graph goes to the parent. + on_click_label(get_collector_index()); + return TRUE; + } + } + + if (_potential_drag_mode == DM_none) { + set_drag_mode(DM_scale); + _drag_scale_start = pixel_to_height(graph_x); + // SetCapture(_graph_window); + return TRUE; + + } else if (_potential_drag_mode == DM_guide_bar && _drag_guide_bar >= 0) { + set_drag_mode(DM_guide_bar); + _drag_start_x = graph_x; + // SetCapture(_graph_window); + return TRUE; + } + + return GtkStatsGraph::handle_button_press(widget, graph_x, graph_y, + double_click); +} + +/** + * Called when the mouse button is released within the graph window. + */ +gboolean GtkStatsFlameGraph:: +handle_button_release(GtkWidget *widget, int graph_x, int graph_y) { + if (_drag_mode == DM_scale) { + set_drag_mode(DM_none); + // ReleaseCapture(); + return handle_motion(widget, graph_x, graph_y); + + } else if (_drag_mode == DM_guide_bar) { + if (graph_x < 0 || graph_x >= get_xsize()) { + remove_user_guide_bar(_drag_guide_bar); + } else { + move_user_guide_bar(_drag_guide_bar, pixel_to_height(graph_x)); + } + set_drag_mode(DM_none); + // ReleaseCapture(); + return handle_motion(widget, graph_x, graph_y); + } + + return GtkStatsGraph::handle_button_release(widget, graph_x, graph_y); +} + +/** + * Called when the mouse is moved within the graph window. + */ +gboolean GtkStatsFlameGraph:: +handle_motion(GtkWidget *widget, int graph_x, int graph_y) { + if (_drag_mode == DM_new_guide_bar) { + // We haven't created the new guide bar yet; we won't until the mouse + // comes within the graph's region. + if (graph_x >= 0 && graph_x < get_xsize()) { + set_drag_mode(DM_guide_bar); + _drag_guide_bar = add_user_guide_bar(pixel_to_height(graph_x)); + return TRUE; + } + } + else if (_drag_mode == DM_guide_bar) { + move_user_guide_bar(_drag_guide_bar, pixel_to_height(graph_x)); + return TRUE; + } + + return GtkStatsGraph::handle_motion(widget, graph_x, graph_y); +} + +/** + * Draws the line for the indicated guide bar on the graph. + */ +void GtkStatsFlameGraph:: +draw_guide_bar(cairo_t *cr, const PStatGraph::GuideBar &bar) { + int x = height_to_pixel(bar._height); + + if (x > 0 && x < get_xsize() - 1) { + // Only draw it if it's not too close to the top. + switch (bar._style) { + case GBS_target: + cairo_set_source_rgb(cr, rgb_light_gray[0], rgb_light_gray[1], rgb_light_gray[2]); + break; + + case GBS_user: + cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]); + break; + + case GBS_normal: + cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]); + break; + } + cairo_move_to(cr, x, 0); + cairo_line_to(cr, x, get_ysize()); + cairo_stroke(cr); + } +} + +/** + * This is called during the servicing of the draw event. + */ +void GtkStatsFlameGraph:: +draw_guide_labels(cairo_t *cr) { + int i; + int num_guide_bars = get_num_guide_bars(); + for (i = 0; i < num_guide_bars; i++) { + draw_guide_label(cr, get_guide_bar(i)); + } + + int num_user_guide_bars = get_num_user_guide_bars(); + for (i = 0; i < num_user_guide_bars; i++) { + draw_guide_label(cr, get_user_guide_bar(i)); + } +} + +/** + * Draws the text for the indicated guide bar label at the top of the graph. + */ +void GtkStatsFlameGraph:: +draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar) { + switch (bar._style) { + case GBS_target: + cairo_set_source_rgb(cr, rgb_light_gray[0], rgb_light_gray[1], rgb_light_gray[2]); + break; + + case GBS_user: + cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]); + break; + + case GBS_normal: + cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]); + break; + } + + int x = height_to_pixel(bar._height); + const std::string &label = bar._label; + + PangoLayout *layout = gtk_widget_create_pango_layout(_window, label.c_str()); + int width, height; + pango_layout_get_pixel_size(layout, &width, &height); + + if (bar._style != GBS_user) { + double from_height = pixel_to_height(x - width); + double to_height = pixel_to_height(x + width); + if (find_user_guide_bar(from_height, to_height) >= 0) { + // Omit the label: there's a user-defined guide bar in the same space. + g_object_unref(layout); + return; + } + } + + if (x >= 0 && x < get_xsize()) { + // Now convert our x to a coordinate within our drawing area. + int junk_y; + + // The x coordinate comes from the graph_window. + gtk_widget_translate_coordinates(_graph_window, _scale_area, + x, 0, + &x, &junk_y); + + GtkAllocation allocation; + gtk_widget_get_allocation(_scale_area, &allocation); + + int this_x = x - width / 2; + if (this_x >= 0 && this_x + width < allocation.width) { + cairo_move_to(cr, this_x, allocation.height - height); + pango_cairo_show_layout(cr, layout); + } + } + + g_object_unref(layout); +} + +/** + * Called when the average check box is toggled. + */ +void GtkStatsFlameGraph:: +toggled_callback(GtkToggleButton *button, gpointer data) { + GtkStatsFlameGraph *self = (GtkStatsFlameGraph *)data; + + bool active = gtk_toggle_button_get_active(button); + self->set_average_mode(active); +} + +/** + * Draws in the scale labels. + */ +gboolean GtkStatsFlameGraph:: +draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data) { + GtkStatsFlameGraph *self = (GtkStatsFlameGraph *)data; + self->draw_guide_labels(cr); + + return TRUE; +} diff --git a/pandatool/src/gtk-stats/gtkStatsFlameGraph.h b/pandatool/src/gtk-stats/gtkStatsFlameGraph.h new file mode 100644 index 0000000000..f6e57c3bce --- /dev/null +++ b/pandatool/src/gtk-stats/gtkStatsFlameGraph.h @@ -0,0 +1,81 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file gtkStatsFlameGraph.h + * @author rdb + * @date 2022-02-02 + */ + +#ifndef GTKSTATSFLAMEGRAPH_H +#define GTKSTATSFLAMEGRAPH_H + +#include "pandatoolbase.h" + +#include "gtkStatsGraph.h" +#include "pStatFlameGraph.h" + +class GtkStatsLabel; + +/** + * A window that draws a flame chart, which shows the collectors explicitly + * stopping and starting, one frame at a time. + */ +class GtkStatsFlameGraph : public PStatFlameGraph, public GtkStatsGraph { +public: + GtkStatsFlameGraph(GtkStatsMonitor *monitor, int thread_index, + int collector_index=0); + virtual ~GtkStatsFlameGraph(); + + virtual void new_collector(int collector_index); + virtual void new_data(int thread_index, int frame_number); + virtual void force_redraw(); + virtual void changed_graph_size(int graph_xsize, int graph_ysize); + + virtual void set_time_units(int unit_mask); + virtual void on_click_label(int collector_index); + virtual void on_enter_label(int collector_index); + virtual void on_leave_label(int collector_index); + virtual std::string get_label_tooltip(int collector_index) const; + +protected: + virtual void update_labels(); + virtual void update_label(int collector_index, int row, int x, int width); + virtual void normal_guide_bars(); + + void clear_region(); + virtual void begin_draw(); + virtual void end_draw(); + virtual void idle(); + + virtual void additional_graph_window_paint(cairo_t *cr); + virtual DragMode consider_drag_start(int graph_x, int graph_y); + + virtual gboolean handle_button_press(GtkWidget *widget, int graph_x, int graph_y, + bool double_click); + virtual gboolean handle_button_release(GtkWidget *widget, int graph_x, int graph_y); + virtual gboolean handle_motion(GtkWidget *widget, int graph_x, int graph_y); + +private: + void draw_guide_bar(cairo_t *cr, const PStatGraph::GuideBar &bar); + void draw_guide_labels(cairo_t *cr); + void draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar); + + static void toggled_callback(GtkToggleButton *button, gpointer data); + static gboolean draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data); + +private: + std::string _net_value_text; + pmap _labels; + + GtkWidget *_top_hbox; + GtkWidget *_average_check_box; + GtkWidget *_total_label; + GtkWidget *_fixed; +}; + +#endif diff --git a/pandatool/src/gtk-stats/gtkStatsGraph.cxx b/pandatool/src/gtk-stats/gtkStatsGraph.cxx index 0fb46af19d..4a34ca75fe 100644 --- a/pandatool/src/gtk-stats/gtkStatsGraph.cxx +++ b/pandatool/src/gtk-stats/gtkStatsGraph.cxx @@ -14,6 +14,7 @@ #include "gtkStatsGraph.h" #include "gtkStatsMonitor.h" #include "gtkStatsLabelStack.h" +#include "convert_srgb.h" const double GtkStatsGraph::rgb_light_gray[3] = { 0x9a / (double)0xff, 0x9a / (double)0xff, 0x9a / (double)0xff, @@ -84,15 +85,20 @@ GtkStatsGraph(GtkStatsMonitor *monitor) : g_signal_connect(G_OBJECT(_graph_window), "motion_notify_event", G_CALLBACK(motion_notify_event_callback), this); + // An overlay inside the frame, for charts that want to display widgets on + // top of the graph. + _graph_overlay = gtk_overlay_new(); + gtk_container_add(GTK_CONTAINER(_graph_overlay), _graph_window); + // A Frame to hold the graph. - GtkWidget *graph_frame = gtk_frame_new(nullptr); - gtk_frame_set_shadow_type(GTK_FRAME(graph_frame), GTK_SHADOW_IN); - gtk_container_add(GTK_CONTAINER(graph_frame), _graph_window); + _graph_frame = gtk_frame_new(nullptr); + gtk_frame_set_shadow_type(GTK_FRAME(_graph_frame), GTK_SHADOW_IN); + gtk_container_add(GTK_CONTAINER(_graph_frame), _graph_overlay); // A VBox to hold the graph's frame, and any numbers (scale legend? total?) // above it. _graph_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); - gtk_box_pack_end(GTK_BOX(_graph_vbox), graph_frame, + gtk_box_pack_end(GTK_BOX(_graph_vbox), _graph_frame, TRUE, TRUE, 0); // An HBox to hold the graph's frame, and the scale legend to the right of @@ -103,10 +109,11 @@ GtkStatsGraph(GtkStatsMonitor *monitor) : // An HPaned to hold the label stack and the graph hbox. _hpaned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL); + gtk_paned_set_wide_handle(GTK_PANED(_hpaned), TRUE); gtk_container_add(GTK_CONTAINER(_window), _hpaned); gtk_container_set_border_width(GTK_CONTAINER(_window), 8); - gtk_paned_pack1(GTK_PANED(_hpaned), _label_stack.get_widget(), TRUE, TRUE); + gtk_paned_pack1(GTK_PANED(_hpaned), _label_stack.get_widget(), FALSE, FALSE); gtk_paned_pack2(GTK_PANED(_hpaned), _graph_hbox, TRUE, TRUE); _drag_mode = DM_none; @@ -124,10 +131,11 @@ GtkStatsGraph:: _monitor = nullptr; release_surface(); - Brushes::iterator bi; - for (bi = _brushes.begin(); bi != _brushes.end(); ++bi) { - cairo_pattern_destroy((*bi).second); + for (auto &item : _brushes) { + cairo_pattern_destroy(item.second.first); + cairo_pattern_destroy(item.second.second); } + _brushes.clear(); _label_stack.clear_labels(); @@ -152,13 +160,6 @@ void GtkStatsGraph:: new_data(int thread_index, int frame_number) { } -/** - * Called when it is necessary to redraw the entire graph. - */ -void GtkStatsGraph:: -force_redraw() { -} - /** * Called when the user has resized the window, forcing a resize of the graph. */ @@ -207,7 +208,38 @@ user_guide_bars_changed() { * Called when the user single-clicks on a label. */ void GtkStatsGraph:: -clicked_label(int collector_index) { +on_click_label(int collector_index) { +} + +/** + * Called when the user hovers the mouse over a label. + */ +void GtkStatsGraph:: +on_enter_label(int collector_index) { + if (collector_index != _highlighted_index) { + _highlighted_index = collector_index; + force_redraw(); + } +} + +/** + * Called when the user's mouse cursor leaves a label. + */ +void GtkStatsGraph:: +on_leave_label(int collector_index) { + if (collector_index == _highlighted_index && collector_index != -1) { + _highlighted_index = -1; + force_redraw(); + } +} + +/** + * Called when the mouse hovers over a label, and should return the text that + * should appear on the tooltip. + */ +std::string GtkStatsGraph:: +get_label_tooltip(int collector_index) const { + return std::string(); } /** @@ -232,23 +264,30 @@ close() { * Returns a pattern suitable for drawing in the indicated collector's color. */ cairo_pattern_t *GtkStatsGraph:: -get_collector_pattern(int collector_index) { +get_collector_pattern(int collector_index, bool highlight) { Brushes::iterator bi; bi = _brushes.find(collector_index); if (bi != _brushes.end()) { - return (*bi).second; + return highlight ? (*bi).second.second : (*bi).second.first; } // Ask the monitor what color this guy should be. LRGBColor rgb = _monitor->get_collector_color(collector_index); - cairo_pattern_t *pattern = cairo_pattern_create_rgb(rgb[0], rgb[1], rgb[2]); + cairo_pattern_t *pattern = cairo_pattern_create_rgb( + encode_sRGB_float(rgb[0]), + encode_sRGB_float(rgb[1]), + encode_sRGB_float(rgb[2])); + cairo_pattern_t *hpattern = cairo_pattern_create_rgb( + encode_sRGB_float(rgb[0] * 0.75f), + encode_sRGB_float(rgb[1] * 0.75f), + encode_sRGB_float(rgb[2] * 0.75f)); - _brushes[collector_index] = pattern; - return pattern; + _brushes[collector_index] = std::make_pair(pattern, hpattern); + return highlight ? hpattern : pattern; } /** - * This is called during the servicing of expose_event; it gives a derived + * This is called during the servicing of the draw event; it gives a derived * class opportunity to do some further painting into the graph window. */ void GtkStatsGraph:: diff --git a/pandatool/src/gtk-stats/gtkStatsGraph.h b/pandatool/src/gtk-stats/gtkStatsGraph.h index ddf1f11543..3a3f123faf 100644 --- a/pandatool/src/gtk-stats/gtkStatsGraph.h +++ b/pandatool/src/gtk-stats/gtkStatsGraph.h @@ -44,7 +44,7 @@ public: virtual void new_collector(int collector_index); virtual void new_data(int thread_index, int frame_number); - virtual void force_redraw(); + virtual void force_redraw()=0; virtual void changed_graph_size(int graph_xsize, int graph_ysize); virtual void set_time_units(int unit_mask); @@ -52,11 +52,14 @@ public: void set_pause(bool pause); void user_guide_bars_changed(); - virtual void clicked_label(int collector_index); + virtual void on_click_label(int collector_index); + virtual void on_enter_label(int collector_index); + virtual void on_leave_label(int collector_index); + virtual std::string get_label_tooltip(int collector_index) const; protected: void close(); - cairo_pattern_t *get_collector_pattern(int collector_index); + cairo_pattern_t *get_collector_pattern(int collector_index, bool highlight = false); virtual void additional_graph_window_paint(cairo_t *cr); virtual DragMode consider_drag_start(int graph_x, int graph_y); @@ -69,12 +72,14 @@ protected: protected: // Table of patterns for our various collectors. - typedef pmap Brushes; + typedef pmap > Brushes; Brushes _brushes; GtkStatsMonitor *_monitor; GtkWidget *_parent_window; GtkWidget *_window; + GtkWidget *_graph_frame; + GtkWidget *_graph_overlay; GtkWidget *_graph_window; GtkWidget *_graph_hbox; GtkWidget *_graph_vbox; @@ -88,21 +93,14 @@ protected: cairo_t *_cr; int _surface_xsize, _surface_ysize; - /* - COLORREF _dark_color; - COLORREF _light_color; - COLORREF _user_guide_bar_color; - HPEN _dark_pen; - HPEN _light_pen; - HPEN _user_guide_bar_pen; - */ - DragMode _drag_mode; DragMode _potential_drag_mode; int _drag_start_x, _drag_start_y; double _drag_scale_start; int _drag_guide_bar; + int _highlighted_index = -1; + bool _pause; static const double rgb_white[3]; diff --git a/pandatool/src/gtk-stats/gtkStatsLabel.cxx b/pandatool/src/gtk-stats/gtkStatsLabel.cxx index 09c5f1a39e..4eb766246e 100644 --- a/pandatool/src/gtk-stats/gtkStatsLabel.cxx +++ b/pandatool/src/gtk-stats/gtkStatsLabel.cxx @@ -14,6 +14,7 @@ #include "gtkStatsLabel.h" #include "gtkStatsMonitor.h" #include "gtkStatsGraph.h" +#include "convert_srgb.h" int GtkStatsLabel::_left_margin = 2; int GtkStatsLabel::_right_margin = 2; @@ -25,19 +26,14 @@ int GtkStatsLabel::_bottom_margin = 2; */ GtkStatsLabel:: GtkStatsLabel(GtkStatsMonitor *monitor, GtkStatsGraph *graph, - int thread_index, int collector_index, bool use_fullname) : + int thread_index, int collector_index, bool use_fullname, + bool align_right) : _monitor(monitor), _graph(graph), _thread_index(thread_index), - _collector_index(collector_index) + _collector_index(collector_index), + _align_right(align_right) { - _widget = nullptr; - if (use_fullname) { - _text = _monitor->get_client_data()->get_collector_fullname(_collector_index); - } else { - _text = _monitor->get_client_data()->get_collector_name(_collector_index); - } - _widget = gtk_drawing_area_new(); gtk_widget_add_events(_widget, GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK | @@ -50,32 +46,41 @@ GtkStatsLabel(GtkStatsMonitor *monitor, GtkStatsGraph *graph, G_CALLBACK(leave_notify_event_callback), this); g_signal_connect(G_OBJECT(_widget), "button_press_event", G_CALLBACK(button_press_event_callback), this); + g_signal_connect(G_OBJECT(_widget), "query-tooltip", + G_CALLBACK(query_tooltip_callback), this); + gtk_widget_set_has_tooltip(_widget, TRUE); gtk_widget_show(_widget); - // Make up a PangoLayout to represent the text. - _layout = gtk_widget_create_pango_layout(_widget, _text.c_str()); - // Set the fg and bg colors on the label. - _bg_color = _monitor->get_collector_color(_collector_index); + LRGBColor rgb = _monitor->get_collector_color(_collector_index); + _bg_color = LRGBColor( + encode_sRGB_float(rgb[0]), + encode_sRGB_float(rgb[1]), + encode_sRGB_float(rgb[2])); + + _highlight_bg_color = LRGBColor( + encode_sRGB_float(rgb[0] * 0.75f), + encode_sRGB_float(rgb[1] * 0.75f), + encode_sRGB_float(rgb[2] * 0.75f)); // Should our foreground be black or white? - PN_stdfloat bright = _bg_color.dot(LRGBColor(0.299, 0.587, 0.114)); + PN_stdfloat bright = _bg_color.dot(LRGBColor(0.2126, 0.7152, 0.0722)); if (bright >= 0.5) { _fg_color = LRGBColor(0); } else { _fg_color = LRGBColor(1); } - - // What are the extents of the text? This determines the minimum size of - // our widget. - int width, height; - pango_layout_get_pixel_size(_layout, &width, &height); - gtk_widget_set_size_request(_widget, width + 8, height); + if (bright >= 0.5 * 0.75) { + _highlight_fg_color = LRGBColor(0); + } else { + _highlight_fg_color = LRGBColor(1); + } _highlight = false; _mouse_within = false; - _height = height; + + update_text(use_fullname); } /** @@ -83,7 +88,10 @@ GtkStatsLabel(GtkStatsMonitor *monitor, GtkStatsGraph *graph, */ GtkStatsLabel:: ~GtkStatsLabel() { - // DeleteObject(_bg_brush); + if (_layout) { + g_object_unref(_layout); + _layout = nullptr; + } } /** @@ -137,6 +145,33 @@ get_highlight() const { return _highlight; } +/** + * Set to true if the full name of the collector should be shown. + */ +void GtkStatsLabel:: +update_text(bool use_fullname) { + const PStatClientData *client_data = _monitor->get_client_data(); + if (use_fullname) { + _text = client_data->get_collector_fullname(_collector_index); + } else { + _text = client_data->get_collector_name(_collector_index); + } + + // Make up a PangoLayout to represent the text. + if (_layout) { + g_object_unref(_layout); + } + _layout = gtk_widget_create_pango_layout(_widget, _text.c_str()); + + // What are the extents of the text? This determines the minimum size of + // our widget. + int width, height; + pango_layout_get_pixel_size(_layout, &width, &height); + gtk_widget_set_size_request(_widget, width + 8, height); + _ideal_width = width; + _height = height; +} + /** * Used internally to indicate whether the mouse is within the label's widget. */ @@ -155,7 +190,16 @@ gboolean GtkStatsLabel:: draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data) { GtkStatsLabel *self = (GtkStatsLabel *)data; - cairo_set_source_rgb(cr, self->_bg_color[0], self->_bg_color[1], self->_bg_color[2]); + LRGBColor bg, fg; + if (self->_highlight || self->_mouse_within) { + bg = self->_highlight_bg_color; + fg = self->_highlight_fg_color; + } else { + bg = self->_bg_color; + fg = self->_fg_color; + + } + cairo_set_source_rgb(cr, bg[0], bg[1], bg[2]); GtkAllocation allocation; gtk_widget_get_allocation(widget, &allocation); @@ -163,19 +207,16 @@ draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data) { cairo_rectangle(cr, 0, 0, allocation.width, allocation.height); cairo_fill(cr); - // Center the text within the rectangle. int width, height; pango_layout_get_pixel_size(self->_layout, &width, &height); - cairo_set_source_rgb(cr, self->_fg_color[0], self->_fg_color[1], self->_fg_color[2]); - cairo_move_to(cr, (allocation.width - width) / 2, 0); - pango_cairo_show_layout(cr, self->_layout); - - // Now draw the highlight rectangle, if any. - if (self->_highlight || self->_mouse_within) { - cairo_rectangle(cr, 0, 0, allocation.width, allocation.height); - cairo_stroke(cr); + cairo_set_source_rgb(cr, fg[0], fg[1], fg[2]); + if (self->_align_right) { + cairo_move_to(cr, allocation.width - width, 0); + } else { + cairo_move_to(cr, 0, 0); } + pango_cairo_show_layout(cr, self->_layout); return TRUE; } @@ -211,7 +252,21 @@ button_press_event_callback(GtkWidget *widget, GdkEventButton *event, GtkStatsLabel *self = (GtkStatsLabel *)data; bool double_click = (event->type == GDK_2BUTTON_PRESS); if (double_click) { - self->_graph->clicked_label(self->_collector_index); + self->_graph->on_click_label(self->_collector_index); } return TRUE; } + +/** + * Called when a tooltip should be displayed. + */ +gboolean GtkStatsLabel:: +query_tooltip_callback(GtkWidget *widget, gint x, gint y, + gboolean keyboard_tip, GtkTooltip *tooltip, + gpointer data) { + GtkStatsLabel *self = (GtkStatsLabel *)data; + + std::string text = self->_graph->get_label_tooltip(self->_collector_index); + gtk_tooltip_set_text(tooltip, text.c_str()); + return !text.empty(); +} diff --git a/pandatool/src/gtk-stats/gtkStatsLabel.h b/pandatool/src/gtk-stats/gtkStatsLabel.h index 53737661d8..185b29a605 100644 --- a/pandatool/src/gtk-stats/gtkStatsLabel.h +++ b/pandatool/src/gtk-stats/gtkStatsLabel.h @@ -30,7 +30,8 @@ class GtkStatsGraph; class GtkStatsLabel { public: GtkStatsLabel(GtkStatsMonitor *monitor, GtkStatsGraph *graph, - int thread_index, int collector_index, bool use_fullname); + int thread_index, int collector_index, bool use_fullname, + bool align_right = true); ~GtkStatsLabel(); GtkWidget *get_widget() const; @@ -42,6 +43,8 @@ public: void set_highlight(bool highlight); bool get_highlight() const; + void update_text(bool use_fullname); + private: void set_mouse_within(bool mouse_within); static gboolean draw_callback(GtkWidget *widget, @@ -55,6 +58,9 @@ private: static gboolean button_press_event_callback(GtkWidget *widget, GdkEventButton *event, gpointer data); + static gboolean query_tooltip_callback(GtkWidget *widget, gint x, gint y, + gboolean keyboard_tip, GtkTooltip *tooltip, + gpointer data); GtkStatsMonitor *_monitor; GtkStatsGraph *_graph; @@ -63,19 +69,16 @@ private: std::string _text; GtkWidget *_widget; LRGBColor _fg_color; + LRGBColor _highlight_fg_color; LRGBColor _bg_color; - PangoLayout *_layout; - - /* - COLORREF _bg_color; - COLORREF _fg_color; - HBRUSH _bg_brush; - HBRUSH _highlight_brush; - */ + LRGBColor _highlight_bg_color; + PangoLayout *_layout = nullptr; + int _height; + int _ideal_width; bool _highlight; bool _mouse_within; - int _height; + bool _align_right; static int _left_margin, _right_margin; static int _top_margin, _bottom_margin; diff --git a/pandatool/src/gtk-stats/gtkStatsLabelStack.cxx b/pandatool/src/gtk-stats/gtkStatsLabelStack.cxx index 079190019c..5a08b2fe38 100644 --- a/pandatool/src/gtk-stats/gtkStatsLabelStack.cxx +++ b/pandatool/src/gtk-stats/gtkStatsLabelStack.cxx @@ -84,9 +84,7 @@ get_label_collector_index(int label_index) const { */ void GtkStatsLabelStack:: clear_labels(bool delete_widgets) { - Labels::iterator li; - for (li = _labels.begin(); li != _labels.end(); ++li) { - GtkStatsLabel *label = (*li); + for (GtkStatsLabel *label : _labels) { if (delete_widgets) { gtk_container_remove(GTK_CONTAINER(_widget), label->get_widget()); } diff --git a/pandatool/src/gtk-stats/gtkStatsMonitor.cxx b/pandatool/src/gtk-stats/gtkStatsMonitor.cxx index b312d5ccc3..c215397339 100644 --- a/pandatool/src/gtk-stats/gtkStatsMonitor.cxx +++ b/pandatool/src/gtk-stats/gtkStatsMonitor.cxx @@ -17,6 +17,7 @@ #include "gtkStatsStripChart.h" #include "gtkStatsChartMenu.h" #include "gtkStatsPianoRoll.h" +#include "gtkStatsFlameGraph.h" #include "gtkStatsMenuId.h" #include "pStatGraph.h" #include "pStatCollectorDef.h" @@ -251,6 +252,19 @@ open_piano_roll(int thread_index) { graph->set_pause(_pause); } +/** + * Opens a new flame graph showing the indicated data. + */ +void GtkStatsMonitor:: +open_flame_graph(int thread_index) { + GtkStatsFlameGraph *graph = new GtkStatsFlameGraph(this, thread_index); + add_graph(graph); + + graph->set_time_units(_time_units); + graph->set_scroll_speed(_scroll_speed); + graph->set_pause(_pause); +} + /** * Adds a new MenuDef to the monitor, or returns an existing one if there is * already one just like it. diff --git a/pandatool/src/gtk-stats/gtkStatsMonitor.h b/pandatool/src/gtk-stats/gtkStatsMonitor.h index ba872289a7..a79a6ad0ed 100644 --- a/pandatool/src/gtk-stats/gtkStatsMonitor.h +++ b/pandatool/src/gtk-stats/gtkStatsMonitor.h @@ -66,6 +66,7 @@ public: GtkWidget *get_window() const; void open_strip_chart(int thread_index, int collector_index, bool show_level); void open_piano_roll(int thread_index); + void open_flame_graph(int thread_index); const MenuDef *add_menu(const MenuDef &menu_def); diff --git a/pandatool/src/gtk-stats/gtkStatsPianoRoll.cxx b/pandatool/src/gtk-stats/gtkStatsPianoRoll.cxx index 720e5aa6ea..3f0bc320fa 100644 --- a/pandatool/src/gtk-stats/gtkStatsPianoRoll.cxx +++ b/pandatool/src/gtk-stats/gtkStatsPianoRoll.cxx @@ -16,7 +16,7 @@ #include "numeric_types.h" #include "gtkStatsLabelStack.h" -static const int default_piano_roll_width = 400; +static const int default_piano_roll_width = 600; static const int default_piano_roll_height = 200; /** @@ -126,7 +126,7 @@ set_time_units(int unit_mask) { * Called when the user single-clicks on a label. */ void GtkStatsPianoRoll:: -clicked_label(int collector_index) { +on_click_label(int collector_index) { if (collector_index >= 0) { GtkStatsGraph::_monitor->open_strip_chart(_thread_index, collector_index, false); } @@ -167,6 +167,18 @@ begin_draw() { } } +/** + * Should be overridden by the user class. This hook will be called before + * drawing any one row of bars. These bars correspond to the collector whose + * index is get_row_collector(row), and in the color get_row_color(row). + */ +void GtkStatsPianoRoll:: +begin_row(int row) { + int collector_index = get_label_collector(row); + cairo_set_source(_cr, get_collector_pattern(collector_index, + _highlighted_index == collector_index)); +} + /** * Draws a single bar on the chart. */ @@ -176,8 +188,6 @@ draw_bar(int row, int from_x, int to_x) { int y = _label_stack.get_label_y(row, _graph_window); int height = _label_stack.get_label_height(row); - int collector_index = get_label_collector(row); - cairo_set_source(_cr, get_collector_pattern(collector_index)); cairo_rectangle(_cr, from_x, y - height + 2, to_x - from_x, height - 4); cairo_fill(_cr); } @@ -203,7 +213,7 @@ idle() { } /** - * This is called during the servicing of expose_event; it gives a derived + * This is called during the servicing of the draw event; it gives a derived * class opportunity to do some further painting into the graph window. */ void GtkStatsPianoRoll:: @@ -251,7 +261,7 @@ handle_button_press(GtkWidget *widget, int graph_x, int graph_y, if (double_click) { // Double-clicking on a color bar in the graph is the same as double- // clicking on the corresponding label. - clicked_label(get_collector_under_pixel(graph_x, graph_y)); + on_click_label(get_collector_under_pixel(graph_x, graph_y)); return TRUE; } @@ -303,7 +313,9 @@ gboolean GtkStatsPianoRoll:: handle_motion(GtkWidget *widget, int graph_x, int graph_y) { if (_drag_mode == DM_none && _potential_drag_mode == DM_none) { // When the mouse is over a color bar, highlight it. - _label_stack.highlight_label(get_collector_under_pixel(graph_x, graph_y)); + int collector_index = get_collector_under_pixel(graph_x, graph_y); + _label_stack.highlight_label(collector_index); + on_enter_label(collector_index); /* // Now we want to get a WM_MOUSELEAVE when the mouse leaves the graph @@ -320,6 +332,7 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) { } else { // If the mouse is in some drag mode, stop highlighting. _label_stack.highlight_label(-1); + on_leave_label(_highlighted_index); } if (_drag_mode == DM_scale) { @@ -409,7 +422,7 @@ draw_guide_bar(cairo_t *cr, const PStatGraph::GuideBar &bar) { } /** - * This is called during the servicing of expose_event. + * This is called during the servicing of the draw event. */ void GtkStatsPianoRoll:: draw_guide_labels(cairo_t *cr) { diff --git a/pandatool/src/gtk-stats/gtkStatsPianoRoll.h b/pandatool/src/gtk-stats/gtkStatsPianoRoll.h index 97ad5ad597..a7cfd6afe8 100644 --- a/pandatool/src/gtk-stats/gtkStatsPianoRoll.h +++ b/pandatool/src/gtk-stats/gtkStatsPianoRoll.h @@ -38,12 +38,13 @@ public: virtual void changed_graph_size(int graph_xsize, int graph_ysize); virtual void set_time_units(int unit_mask); - virtual void clicked_label(int collector_index); + virtual void on_click_label(int collector_index); void set_horizontal_scale(double time_width); protected: void clear_region(); virtual void begin_draw(); + virtual void begin_row(int row); virtual void draw_bar(int row, int from_x, int to_x); virtual void end_draw(); virtual void idle(); diff --git a/pandatool/src/gtk-stats/gtkStatsStripChart.cxx b/pandatool/src/gtk-stats/gtkStatsStripChart.cxx index f66218b3c8..eb39d875a9 100644 --- a/pandatool/src/gtk-stats/gtkStatsStripChart.cxx +++ b/pandatool/src/gtk-stats/gtkStatsStripChart.cxx @@ -68,8 +68,15 @@ GtkStatsStripChart(GtkStatsMonitor *monitor, int thread_index, G_CALLBACK(draw_callback), this); gtk_box_pack_start(GTK_BOX(_graph_hbox), _scale_area, FALSE, FALSE, 0); - gtk_widget_set_size_request(_scale_area, 40, 0); + // Make it wide enough to display a typical label. + { + PangoLayout *layout = gtk_widget_create_pango_layout(_window, "99 ms"); + int width, height; + pango_layout_get_pixel_size(layout, &width, &height); + gtk_widget_set_size_request(_scale_area, width, 0); + g_object_unref(layout); + } gtk_widget_set_size_request(_graph_window, default_strip_chart_width, default_strip_chart_height); @@ -175,7 +182,7 @@ set_scroll_speed(double scroll_speed) { * Called when the user single-clicks on a label. */ void GtkStatsStripChart:: -clicked_label(int collector_index) { +on_click_label(int collector_index) { if (collector_index < 0) { // Clicking on whitespace in the graph is the same as clicking on the top // label. @@ -202,6 +209,15 @@ clicked_label(int collector_index) { } } +/** + * Called when the mouse hovers over a label, and should return the text that + * should appear on the tooltip. + */ +std::string GtkStatsStripChart:: +get_label_tooltip(int collector_index) const { + return PStatStripChart::get_label_tooltip(collector_index); +} + /** * Changes the value the height of the vertical axis represents. This may * force a redraw. @@ -287,7 +303,8 @@ draw_slice(int x, int w, const PStatStripChart::FrameData &fdata) { for (fi = fdata.begin(); fi != fdata.end(); ++fi) { const ColorData &cd = (*fi); overall_time += cd._net_value; - cairo_set_source(_cr, get_collector_pattern(cd._collector_index)); + cairo_set_source(_cr, get_collector_pattern(cd._collector_index, + _highlighted_index == cd._collector_index)); if (overall_time > get_vertical_scale()) { // Off the top. Go ahead and clamp it by hand, in case it's so far off @@ -346,7 +363,7 @@ end_draw(int from_x, int to_x) { } /** - * This is called during the servicing of expose_event; it gives a derived + * This is called during the servicing of the draw event; it gives a derived * class opportunity to do some further painting into the graph window. */ void GtkStatsStripChart:: @@ -418,7 +435,7 @@ handle_button_press(GtkWidget *widget, int graph_x, int graph_y, if (double_click) { // Double-clicking on a color bar in the graph is the same as double- // clicking on the corresponding label. - clicked_label(get_collector_under_pixel(graph_x, graph_y)); + on_click_label(get_collector_under_pixel(graph_x, graph_y)); return TRUE; } @@ -473,23 +490,14 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) { if (_drag_mode == DM_none && _potential_drag_mode == DM_none && graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) { // When the mouse is over a color bar, highlight it. - _label_stack.highlight_label(get_collector_under_pixel(graph_x, graph_y)); - - /* - // Now we want to get a WM_MOUSELEAVE when the mouse leaves the graph - // window. - TRACKMOUSEEVENT tme = { - sizeof(TRACKMOUSEEVENT), - TME_LEAVE, - _graph_window, - 0 - }; - TrackMouseEvent(&tme); - */ - - } else { + int collector_index = get_collector_under_pixel(graph_x, graph_y); + _label_stack.highlight_label(collector_index); + on_enter_label(collector_index); + } + else { // If the mouse is in some drag mode, stop highlighting. _label_stack.highlight_label(-1); + on_leave_label(_highlighted_index); } if (_drag_mode == DM_scale) { @@ -546,7 +554,7 @@ draw_guide_bar(cairo_t *cr, int from_x, int to_x, } /** - * This is called during the servicing of expose_event. + * This is called during the servicing of the draw event. */ void GtkStatsStripChart:: draw_guide_labels(cairo_t *cr) { diff --git a/pandatool/src/gtk-stats/gtkStatsStripChart.h b/pandatool/src/gtk-stats/gtkStatsStripChart.h index 466b6a7753..b1fc44b88e 100644 --- a/pandatool/src/gtk-stats/gtkStatsStripChart.h +++ b/pandatool/src/gtk-stats/gtkStatsStripChart.h @@ -40,7 +40,8 @@ public: virtual void set_time_units(int unit_mask); virtual void set_scroll_speed(double scroll_speed); - virtual void clicked_label(int collector_index); + virtual void on_click_label(int collector_index); + virtual std::string get_label_tooltip(int collector_index) const; void set_vertical_scale(double value_height); protected: diff --git a/pandatool/src/gtk-stats/gtkstats_composite1.cxx b/pandatool/src/gtk-stats/gtkstats_composite1.cxx index 61e1fa7ab7..fefd3eeff4 100644 --- a/pandatool/src/gtk-stats/gtkstats_composite1.cxx +++ b/pandatool/src/gtk-stats/gtkstats_composite1.cxx @@ -1,5 +1,6 @@ #include "gtkStats.cxx" #include "gtkStatsChartMenu.cxx" +#include "gtkStatsFlameGraph.cxx" #include "gtkStatsGraph.cxx" #include "gtkStatsLabel.cxx" #include "gtkStatsLabelStack.cxx" diff --git a/pandatool/src/pstatserver/CMakeLists.txt b/pandatool/src/pstatserver/CMakeLists.txt index 359f529b18..8017b7888e 100644 --- a/pandatool/src/pstatserver/CMakeLists.txt +++ b/pandatool/src/pstatserver/CMakeLists.txt @@ -4,6 +4,7 @@ endif() set(P3PSTATSERVER_HEADERS pStatClientData.h + pStatFlameGraph.h pStatFlameGraph.I pStatGraph.h pStatGraph.I pStatListener.h pStatMonitor.h pStatMonitor.I @@ -17,7 +18,9 @@ set(P3PSTATSERVER_HEADERS ) set(P3PSTATSERVER_SOURCES - pStatClientData.cxx pStatGraph.cxx + pStatClientData.cxx + pStatFlameGraph.cxx + pStatGraph.cxx pStatListener.cxx pStatMonitor.cxx pStatPianoRoll.cxx pStatReader.cxx pStatServer.cxx diff --git a/pandatool/src/pstatserver/p3pstatserver_composite1.cxx b/pandatool/src/pstatserver/p3pstatserver_composite1.cxx index aa310d92fc..079c6e6d22 100644 --- a/pandatool/src/pstatserver/p3pstatserver_composite1.cxx +++ b/pandatool/src/pstatserver/p3pstatserver_composite1.cxx @@ -1,4 +1,5 @@ #include "pStatClientData.cxx" +#include "pStatFlameGraph.cxx" #include "pStatGraph.cxx" #include "pStatListener.cxx" #include "pStatMonitor.cxx" diff --git a/pandatool/src/pstatserver/pStatFlameGraph.I b/pandatool/src/pstatserver/pStatFlameGraph.I new file mode 100644 index 0000000000..af7921ae4b --- /dev/null +++ b/pandatool/src/pstatserver/pStatFlameGraph.I @@ -0,0 +1,89 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file pStatFlameGraph.I + * @author rdb + * @date 2022-01-28 + */ + +/** + * Returns the View this chart represents. + */ +INLINE PStatView &PStatFlameGraph:: +get_view() const { + return _view; +} + +/** + * Returns the particular collector whose data this strip chart reflects. + */ +INLINE int PStatFlameGraph:: +get_collector_index() const { + return _collector_index; +} + +/** + * Returns the amount of total time the width of the horizontal axis + * represents. + */ +INLINE double PStatFlameGraph:: +get_horizontal_scale() const { + return _time_width; +} + +/** + * Changes the average_mode flag. When true, the strip chart will average out + * the color values over pstats_average_time seconds, which hides spikes and + * makes the overall trends easier to read. When false, the strip chart shows + * the actual data as it is happening. + */ +INLINE void PStatFlameGraph:: +set_average_mode(bool average_mode) { + if (_average_mode != average_mode) { + _average_mode = average_mode; + force_redraw(); + } +} + +/** + * Returns the current state of the average_mode flag. When true, the strip + * chart will average out the color values over pstats_average_time seconds, + * which hides spikes and makes the overall trends easier to read. When + * false, the strip chart shows the actual data as it is happening. + */ +INLINE bool PStatFlameGraph:: +get_average_mode() const { + return _average_mode; +} + +/** + * Converts a value (i.e. a "height" in the strip chart) to a horizontal + * pixel offset. + */ +INLINE int PStatFlameGraph:: +height_to_pixel(double value) const { + return (int)((double)_xsize * value / _time_width); +} + +/** + * Converts a horizontal pixel offset to a value (a "height" in the strip + * chart). + */ +INLINE double PStatFlameGraph:: +pixel_to_height(int x) const { + return _time_width * (double)x / (double)_xsize; +} + +/** + * Returns true if get_title_text() has never yet returned an answer, false if + * it has. + */ +INLINE bool PStatFlameGraph:: +is_title_unknown() const { + return _title_unknown; +} diff --git a/pandatool/src/pstatserver/pStatFlameGraph.cxx b/pandatool/src/pstatserver/pStatFlameGraph.cxx new file mode 100644 index 0000000000..16643345d7 --- /dev/null +++ b/pandatool/src/pstatserver/pStatFlameGraph.cxx @@ -0,0 +1,297 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file pStatFlameGraph.cxx + * @author rdb + * @date 2022-01-28 + */ + +#include "pStatFlameGraph.h" + +#include "pStatFrameData.h" +#include "pStatCollectorDef.h" +#include "string_utils.h" +#include "config_pstatclient.h" + +#include +#include + +/** + * + */ +PStatFlameGraph:: +PStatFlameGraph(PStatMonitor *monitor, PStatView &view, + int thread_index, int collector_index, int xsize, int ysize) : + PStatGraph(monitor, xsize, ysize), + _thread_index(thread_index), + _view(view), + _collector_index(collector_index) +{ + _average_mode = true; + _average_cursor = 0; + + _time_width = 1.0 / pstats_target_frame_rate; + _current_frame = -1; + + _title_unknown = true; + + _guide_bar_units = GBU_ms | GBU_hz | GBU_show_units; + normal_guide_bars(); +} + +/** + * + */ +PStatFlameGraph:: +~PStatFlameGraph() { +} + +/** + * Updates the chart with the latest data. + */ +void PStatFlameGraph:: +update() { + const PStatClientData *client_data = _monitor->get_client_data(); + + // Don't bother to update the thread data until we know at least something + // about the collectors and threads. + if (client_data->get_num_collectors() != 0 && + client_data->get_num_threads() != 0) { + const PStatThreadData *thread_data = + client_data->get_thread_data(_thread_index); + if (!thread_data->is_empty()) { + int frame_number = thread_data->get_latest_frame_number(); + if (frame_number != _current_frame) { + _current_frame = frame_number; + + update_data(); + force_redraw(); + update_labels(); + } + } + } + + idle(); +} + +/** + * Changes the collector represented by this flame graph. This may force a + * redraw. + */ +void PStatFlameGraph:: +set_collector_index(int collector_index) { + if (_collector_index != collector_index) { + _collector_index = collector_index; + _title_unknown = true; + update_data(); + force_redraw(); + update_labels(); + } +} + +/** + * Returns the text suitable for the title label on the top line. + */ +std::string PStatFlameGraph:: +get_title_text() { + std::string text; + + _title_unknown = false; + + const PStatClientData *client_data = _monitor->get_client_data(); + if (client_data->has_collector(_collector_index)) { + text = client_data->get_collector_fullname(_collector_index); + text += " flame graph"; + } else { + _title_unknown = true; + } + + if (_thread_index != 0) { + if (client_data->has_thread(_thread_index)) { + text += " (" + client_data->get_thread_name(_thread_index) + " thread)"; + } else { + _title_unknown = true; + } + } + + return text; +} + +/** + * Called when the mouse hovers over a label, and should return the text that + * should appear on the tooltip. + */ +std::string PStatFlameGraph:: +get_label_tooltip(int collector_index) const { + const PStatClientData *client_data = _monitor->get_client_data(); + if (!client_data->has_collector(collector_index)) { + return std::string(); + } + + std::ostringstream text; + text << client_data->get_collector_fullname(collector_index); + + Data::const_iterator it = _data.find(collector_index); + if (it != _data.end()) { + const CollectorData &cd = it->second; + text << " (" << format_number(cd._net_value, get_guide_bar_units(), get_guide_bar_unit_name()) << ")"; + } + + return text.str(); +} + +/** + * + */ +void PStatFlameGraph:: +update_data() { + // First clear the net values, so we'll know which labels should be deleted. + for (auto it = _data.begin(); it != _data.end(); ++it) { + it->second._net_value = 0; + } + + _view.set_to_frame(_current_frame); + + const PStatViewLevel *level = _view.get_level(_collector_index); + double offset = 0; + update_data(level, 0, offset); + + _time_width = (offset != 0) ? offset : 1.0 / pstats_target_frame_rate; + normal_guide_bars(); + + // Cycle through the ring buffers. + _average_cursor = (_average_cursor + 1) % _num_average_frames; +} + +/** + * Recursive helper for get_frame_data. + */ +void PStatFlameGraph:: +update_data(const PStatViewLevel *level, int depth, double &offset) { + double net_value = level->get_net_value(); + + Data::iterator it; + bool inserted; + std::tie(it, inserted) = _data.insert(std::make_pair(level->get_collector(), CollectorData())); + CollectorData &cd = it->second; + cd._offset = offset; + cd._depth = depth; + + if (inserted || !_average_mode) { + // Initialize the values array. + for (double &v : cd._values) { + v = net_value; + } + cd._net_value = net_value; + } else { + cd._values[_average_cursor] = net_value; + + // Calculate the average. + cd._net_value = 0; + for (double value : cd._values) { + cd._net_value += value; + } + cd._net_value /= _num_average_frames; + } + + if (cd._net_value != 0.0) { + cd._net_value = std::max(cd._net_value, 0.0); + + double child_offset = offset; + offset += cd._net_value; + + int num_children = level->get_num_children(); + for (int i = 0; i < num_children; i++) { + const PStatViewLevel *child = level->get_child(i); + update_data(child, depth + 1, child_offset); + } + } +} + +/** + * To be called by the user class when the widget size has changed. This + * updates the chart's internal data and causes it to issue redraw commands to + * reflect the new size. + */ +void PStatFlameGraph:: +changed_size(int xsize, int ysize) { + if (xsize != _xsize || ysize != _ysize) { + _xsize = xsize; + _ysize = ysize; + + normal_guide_bars(); + force_redraw(); + update_labels(); + } +} + +/** + * To be called by the user class when the whole thing needs to be redrawn for + * some reason. + */ +void PStatFlameGraph:: +force_redraw() { + begin_draw(); + end_draw(); +} + +/** + * Resets the list of labels. + */ +void PStatFlameGraph:: +update_labels() { + for (auto it = _data.begin(); it != _data.end(); ++it) { + int collector_index = it->first; + const CollectorData &cd = it->second; + + update_label(collector_index, cd._depth, height_to_pixel(cd._offset), height_to_pixel(cd._net_value)); + } +} + +/** + * Calls update_guide_bars with parameters suitable to this kind of graph. + */ +void PStatFlameGraph:: +normal_guide_bars() { + // We want vaguely 100 pixels between guide bars. + int num_bars = get_xsize() / 100; + + _guide_bars.clear(); + + double dist = _time_width / num_bars; + + for (int i = 1; i < num_bars; ++i) { + _guide_bars.push_back(make_guide_bar(i * dist)); + } + + _guide_bars_changed = true; +} + +/** + * Should be overridden by the user class. This hook will be called before + * drawing any bars in the chart. + */ +void PStatFlameGraph:: +begin_draw() { +} + +/** + * Should be overridden by the user class. This hook will be called after + * drawing a series of color bars in the chart. + */ +void PStatFlameGraph:: +end_draw() { +} + +/** + * Should be overridden by the user class to perform any other updates might + * be necessary after the bars have been redrawn. + */ +void PStatFlameGraph:: +idle() { +} diff --git a/pandatool/src/pstatserver/pStatFlameGraph.h b/pandatool/src/pstatserver/pStatFlameGraph.h new file mode 100644 index 0000000000..148e288af9 --- /dev/null +++ b/pandatool/src/pstatserver/pStatFlameGraph.h @@ -0,0 +1,107 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file pStatFlameGraph.h + * @author rdb + * @date 2022-01-28 + */ + +#ifndef PSTATFLAMEGRAPH_H +#define PSTATFLAMEGRAPH_H + +#include "pandatoolbase.h" + +#include "pStatGraph.h" +#include "pStatMonitor.h" +#include "pStatClientData.h" + +#include "pmap.h" +#include "pdeque.h" + +class PStatFrameData; + +/** + * This is an abstract class that presents the interface for drawing a flame + * chart: it shows the time spent in each of a number of collectors + * as a horizontal bar of color, with time as the horizontal axis. + * + * This class just manages all the flame chart logic; the actual nuts and bolts + * of drawing pixels is left to a user-derived class. + */ +class PStatFlameGraph : public PStatGraph { +public: + PStatFlameGraph(PStatMonitor *monitor, PStatView &view, + int thread_index, int collector_index, + int xsize, int ysize); + virtual ~PStatFlameGraph(); + + void update(); + + INLINE PStatView &get_view() const; + INLINE int get_collector_index() const; + void set_collector_index(int collector_index); + + INLINE double get_horizontal_scale() const; + + INLINE void set_average_mode(bool average_mode); + INLINE bool get_average_mode() const; + + INLINE int height_to_pixel(double value) const; + INLINE double pixel_to_height(int y) const; + + INLINE bool is_title_unknown() const; + std::string get_title_text(); + std::string get_label_tooltip(int collector_index) const; + +protected: + static const size_t _num_average_frames = 200; + + struct CollectorData { + double _offset; + double _net_value; + int _depth; + // This is updated like a ring buffer, initialized with all the same value + // at first, then always at _average_cursor. + double _values[_num_average_frames]; + }; + typedef pmap Data; + + void update_data(); + void update_data(const PStatViewLevel *level, int depth, double &offset); + void changed_size(int xsize, int ysize); + void force_redraw(); + virtual void update_labels(); + virtual void update_label(int collector_index, int row, int x, int width)=0; + virtual void normal_guide_bars(); + + virtual void begin_draw(); + virtual void end_draw(); + virtual void idle(); + +private: + void compute_page(const PStatFrameData &frame_data); + +protected: + int _thread_index; + +private: + PStatView &_view; + int _collector_index; + bool _average_mode; + size_t _average_cursor; + + Data _data; + + double _time_width; + int _current_frame; + bool _title_unknown; +}; + +#include "pStatFlameGraph.I" + +#endif diff --git a/pandatool/src/pstatserver/pStatStripChart.I b/pandatool/src/pstatserver/pStatStripChart.I index 791e722009..35d4c86265 100644 --- a/pandatool/src/pstatserver/pStatStripChart.I +++ b/pandatool/src/pstatserver/pStatStripChart.I @@ -156,6 +156,15 @@ pixel_to_height(int x) const { return _value_height * (double)(get_ysize() - x) / (double)get_ysize(); } +/** + * Returns true if get_title_text() has never yet returned an answer, false if + * it has. + */ +INLINE bool PStatStripChart:: +is_title_unknown() const { + return _title_unknown; +} + /** * Returns true if the indicated collector appears anywhere on the chart at * the current time, false otherwise. diff --git a/pandatool/src/pstatserver/pStatStripChart.cxx b/pandatool/src/pstatserver/pStatStripChart.cxx index c9ffcb3109..09dadc9545 100644 --- a/pandatool/src/pstatserver/pStatStripChart.cxx +++ b/pandatool/src/pstatserver/pStatStripChart.cxx @@ -284,12 +284,71 @@ get_title_text() { } /** - * Returns true if get_title_text() has never yet returned an answer, false if - * it has. + * Called when the mouse hovers over a label, and should return the text that + * should appear on the tooltip. */ -bool PStatStripChart:: -is_title_unknown() const { - return _title_unknown; +std::string PStatStripChart:: +get_label_tooltip(int collector_index) const { + const PStatClientData *client_data = _monitor->get_client_data(); + if (!client_data->has_collector(collector_index)) { + return std::string(); + } + + std::ostringstream text; + text << client_data->get_collector_fullname(collector_index); + + double value; + if (collector_index == _collector_index) { + value = get_average_net_value(); + } + else { + const PStatThreadData *thread_data = _view.get_thread_data(); + int now_i, then_i; + if (!thread_data->get_elapsed_frames(then_i, now_i)) { + return text.str(); + } + double now = _time_width + _start_time; + double then = now - pstats_average_time; + + double net_value = 0.0f; + double net_time = 0.0f; + + // We start with just the portion of frame then_i that actually does fall + // within our "then to now" window (usually some portion of it will). + const PStatFrameData &frame_data = thread_data->get_frame(then_i); + if (frame_data.get_end() > then) { + double this_time = (frame_data.get_end() - then); + _view.set_to_frame(frame_data); + + const PStatViewLevel *level = _view.get_level(collector_index); + if (level != nullptr) { + net_value += level->get_net_value() * this_time; + net_time += this_time; + } + } + // Then we get all of each of the remaining frames. + for (int frame_number = then_i + 1; + frame_number <= now_i; + frame_number++) { + const PStatFrameData &frame_data = thread_data->get_frame(frame_number); + double this_time = frame_data.get_net_time(); + _view.set_to_frame(frame_data); + + const PStatViewLevel *level = _view.get_level(collector_index); + if (level != nullptr) { + net_value += level->get_net_value() * this_time; + net_time += this_time; + } + } + + if (net_time == 0) { + return text.str(); + } + value = net_value / net_time; + } + + text << " (" << format_number(value, get_guide_bar_units(), get_guide_bar_unit_name()) << ")"; + return text.str(); } /** diff --git a/pandatool/src/pstatserver/pStatStripChart.h b/pandatool/src/pstatserver/pStatStripChart.h index 5cba0164e2..f98af23ac4 100644 --- a/pandatool/src/pstatserver/pStatStripChart.h +++ b/pandatool/src/pstatserver/pStatStripChart.h @@ -68,8 +68,9 @@ public: INLINE int height_to_pixel(double value) const; INLINE double pixel_to_height(int y) const; - std::string get_title_text(); bool is_title_unknown() const; + std::string get_title_text(); + std::string get_label_tooltip(int collector_index) const; protected: class ColorData { @@ -89,6 +90,7 @@ protected: void compute_average_pixel_data(PStatStripChart::FrameData &result, int &then_i, int &now_i, double now); double get_net_value(int frame_number) const; + double get_net_value(int frame_number, int collector_index) const; double get_average_net_value() const; void changed_size(int xsize, int ysize); diff --git a/pandatool/src/win-stats/CMakeLists.txt b/pandatool/src/win-stats/CMakeLists.txt index 0cd7768c3a..4c10e4f560 100644 --- a/pandatool/src/win-stats/CMakeLists.txt +++ b/pandatool/src/win-stats/CMakeLists.txt @@ -4,9 +4,10 @@ endif() set(WINSTATS_HEADERS winStatsChartMenu.h + winStatsFlameGraph.h winStatsGraph.h winStats.h - winStatsLabel.h + winStatsLabel.h winStatsLabel.I winStatsLabelStack.h winStatsMenuId.h winStatsMonitor.h winStatsMonitor.I @@ -18,6 +19,7 @@ set(WINSTATS_HEADERS set(WINSTATS_SOURCES winStatsChartMenu.cxx winStats.cxx + winStatsFlameGraph.cxx winStatsGraph.cxx winStatsLabel.cxx winStatsLabelStack.cxx @@ -29,7 +31,7 @@ set(WINSTATS_SOURCES composite_sources(win-stats WINSTATS_SOURCES) add_executable(win-stats ${WINSTATS_HEADERS} ${WINSTATS_SOURCES}) -target_link_libraries(win-stats p3progbase p3pstatserver) +target_link_libraries(win-stats p3progbase p3pstatserver comctl32.lib) # This program is NOT actually called win-stats. It's just pstats.exe set_target_properties(win-stats PROPERTIES OUTPUT_NAME "pstats") diff --git a/pandatool/src/win-stats/winStats.cxx b/pandatool/src/win-stats/winStats.cxx index e40830e189..5dabb7b71a 100644 --- a/pandatool/src/win-stats/winStats.cxx +++ b/pandatool/src/win-stats/winStats.cxx @@ -20,6 +20,11 @@ #define WIN32_LEAN_AND_MEAN 1 #endif #include +#include +#include + +// Enable common controls version 6, necessary for modern visual styles +#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") static const char *toplevel_class_name = "pstats"; static WinStatsServer *server = nullptr; @@ -82,6 +87,15 @@ create_toplevel_window(HINSTANCE application) { } int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { + // Initialize commctl32.dll. + INITCOMMONCONTROLSEX icc; + icc.dwICC = ICC_WIN95_CLASSES | ICC_STANDARD_CLASSES; + icc.dwSize = sizeof(INITCOMMONCONTROLSEX); + InitCommonControlsEx(&icc); + + // Signal DPI awareness. + SetProcessDPIAware(); + HINSTANCE application = GetModuleHandle(nullptr); HWND toplevel_window = create_toplevel_window(application); diff --git a/pandatool/src/win-stats/winStatsChartMenu.cxx b/pandatool/src/win-stats/winStatsChartMenu.cxx index c042acf8ba..026e082489 100644 --- a/pandatool/src/win-stats/winStatsChartMenu.cxx +++ b/pandatool/src/win-stats/winStatsChartMenu.cxx @@ -125,19 +125,32 @@ do_update() { } } - // Also a menu item for a piano roll (following a separator). + // Also menu items for flame graph and piano roll (following a separator). mii.fMask = MIIM_FTYPE; mii.fType = MFT_SEPARATOR; InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii); - WinStatsMonitor::MenuDef menu_def(_thread_index, -1, false); - int menu_id = _monitor->get_menu_id(menu_def); + { + WinStatsMonitor::MenuDef menu_def(_thread_index, -2, false); + int menu_id = _monitor->get_menu_id(menu_def); - mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID; - mii.fType = MFT_STRING; - mii.wID = menu_id; - mii.dwTypeData = "Piano Roll"; - InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii); + mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID; + mii.fType = MFT_STRING; + mii.wID = menu_id; + mii.dwTypeData = "Flame Graph"; + InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii); + } + + { + WinStatsMonitor::MenuDef menu_def(_thread_index, -1, false); + int menu_id = _monitor->get_menu_id(menu_def); + + mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID; + mii.fType = MFT_STRING; + mii.wID = menu_id; + mii.dwTypeData = "Piano Roll"; + InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii); + } } /** diff --git a/pandatool/src/win-stats/winStatsFlameGraph.cxx b/pandatool/src/win-stats/winStatsFlameGraph.cxx new file mode 100644 index 0000000000..90ad7cd280 --- /dev/null +++ b/pandatool/src/win-stats/winStatsFlameGraph.cxx @@ -0,0 +1,639 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file winStatsFlameGraph.cxx + * @author rdb + * @date 2022-01-28 + */ + +#include "winStatsFlameGraph.h" +#include "winStatsLabel.h" +#include "winStatsMonitor.h" +#include "pStatCollectorDef.h" + +#include + +static const int default_flame_graph_width = 800; +static const int default_flame_graph_height = 150; + +bool WinStatsFlameGraph::_window_class_registered = false; +const char * const WinStatsFlameGraph::_window_class_name = "flame"; + +/** + * + */ +WinStatsFlameGraph:: +WinStatsFlameGraph(WinStatsMonitor *monitor, int thread_index, + int collector_index) : + PStatFlameGraph(monitor, monitor->get_view(thread_index), + thread_index, collector_index, + monitor->get_pixel_scale() * default_flame_graph_width / 4, + monitor->get_pixel_scale() * default_flame_graph_height / 4), + WinStatsGraph(monitor) +{ + _left_margin = _pixel_scale * 2; + _right_margin = _pixel_scale * 2; + _top_margin = _pixel_scale * 6; + _bottom_margin = _pixel_scale * 2; + + // Let's show the units on the guide bar labels. There's room. + set_guide_bar_units(get_guide_bar_units() | GBU_show_units); + + _average_check_box = 0; + + create_window(); + clear_region(); +} + +/** + * + */ +WinStatsFlameGraph:: +~WinStatsFlameGraph() { +} + +/** + * Called as each frame's data is made available. There is no guarantee the + * frames will arrive in order, or that all of them will arrive at all. The + * monitor should be prepared to accept frames received out-of-order or + * missing. + */ +void WinStatsFlameGraph:: +new_data(int thread_index, int frame_number) { + if (is_title_unknown()) { + std::string window_title = get_title_text(); + if (!is_title_unknown()) { + SetWindowText(_window, window_title.c_str()); + } + } + + if (!_pause) { + update(); + + std::string text = format_number(get_horizontal_scale(), get_guide_bar_units(), get_guide_bar_unit_name()); + if (_net_value_text != text) { + _net_value_text = text; + RECT rect; + GetClientRect(_window, &rect); + rect.bottom = _top_margin; + InvalidateRect(_window, &rect, TRUE); + } + } +} + +/** + * Called when it is necessary to redraw the entire graph. + */ +void WinStatsFlameGraph:: +force_redraw() { + PStatFlameGraph::force_redraw(); +} + +/** + * Called when the user has resized the window, forcing a resize of the graph. + */ +void WinStatsFlameGraph:: +changed_graph_size(int graph_xsize, int graph_ysize) { + PStatFlameGraph::changed_size(graph_xsize, graph_ysize); +} + +/** + * Called when the user selects a new time units from the monitor pulldown + * menu, this should adjust the units for the graph to the indicated mask if + * it is a time-based graph. + */ +void WinStatsFlameGraph:: +set_time_units(int unit_mask) { + int old_unit_mask = get_guide_bar_units(); + if ((old_unit_mask & (GBU_hz | GBU_ms)) != 0) { + unit_mask = unit_mask & (GBU_hz | GBU_ms); + unit_mask |= (old_unit_mask & GBU_show_units); + set_guide_bar_units(unit_mask); + + RECT rect; + GetClientRect(_window, &rect); + rect.left = _right_margin; + InvalidateRect(_window, &rect, TRUE); + } +} + +/** + * Called when the user single-clicks on a label. + */ +void WinStatsFlameGraph:: +on_click_label(int collector_index) { + int prev_collector_index = get_collector_index(); + if (collector_index == prev_collector_index && collector_index != 0) { + // Clicking on the top label means to go up to the parent level. + const PStatClientData *client_data = + WinStatsGraph::_monitor->get_client_data(); + if (client_data->has_collector(collector_index)) { + const PStatCollectorDef &def = + client_data->get_collector_def(collector_index); + collector_index = def._parent_index; + set_collector_index(collector_index); + } + } + else { + // Clicking on any other label means to focus on that. + set_collector_index(collector_index); + } + + // Change the root collector to show the full name. + if (prev_collector_index != collector_index) { + auto it = _labels.find(prev_collector_index); + if (it != _labels.end()) { + it->second->update_text(false); + } + it = _labels.find(collector_index); + if (it != _labels.end()) { + it->second->update_text(true); + } + } +} + +/** + * Called when the user hovers the mouse over a label. + */ +void WinStatsFlameGraph:: +on_enter_label(int collector_index) { + if (collector_index != _highlighted_index) { + _highlighted_index = collector_index; + } +} + +/** + * Called when the user's mouse cursor leaves a label. + */ +void WinStatsFlameGraph:: +on_leave_label(int collector_index) { + if (collector_index == _highlighted_index && collector_index != -1) { + _highlighted_index = -1; + } +} + +/** + * Called when the mouse hovers over a label, and should return the text that + * should appear on the tooltip. + */ +std::string WinStatsFlameGraph:: +get_label_tooltip(int collector_index) const { + return PStatFlameGraph::get_label_tooltip(collector_index); +} + +/** + * Repositions the labels. + */ +void WinStatsFlameGraph:: +update_labels() { + if (_graph_window) { + PStatFlameGraph::update_labels(); + } +} + +/** + * Repositions a label. If width is 0, the label should be deleted. + */ +void WinStatsFlameGraph:: +update_label(int collector_index, int row, int x, int width) { + WinStatsLabel *label; + + auto it = _labels.find(collector_index); + if (it != _labels.end()) { + if (width == 0) { + delete it->second; + _labels.erase(it); + return; + } + label = it->second; + } else { + if (width == 0) { + return; + } + label = new WinStatsLabel(WinStatsGraph::_monitor, this, _thread_index, collector_index, false, false); + _labels[collector_index] = label; + label->setup(_graph_window); + } + + label->set_pos(x, _ysize - 2 - row * label->get_height(), std::min(width, _xsize - 2)); +} + +/** + * Calls update_guide_bars with parameters suitable to this kind of graph. + */ +void WinStatsFlameGraph:: +normal_guide_bars() { + // We want vaguely 100 pixels between guide bars. + int num_bars = get_xsize() / (_pixel_scale * 25); + + _guide_bars.clear(); + + double dist = get_horizontal_scale() / num_bars; + + for (int i = 1; i < num_bars; ++i) { + _guide_bars.push_back(make_guide_bar(i * dist)); + } + + _guide_bars_changed = true; +} + +/** + * Erases the chart area. + */ +void WinStatsFlameGraph:: +clear_region() { + RECT rect = { 0, 0, get_xsize(), get_ysize() }; + FillRect(_bitmap_dc, &rect, (HBRUSH)GetStockObject(WHITE_BRUSH)); +} + +/** + * Erases the chart area in preparation for drawing a bunch of bars. + */ +void WinStatsFlameGraph:: +begin_draw() { + clear_region(); + + // Draw in the guide bars. + int num_guide_bars = get_num_guide_bars(); + for (int i = 0; i < num_guide_bars; i++) { + draw_guide_bar(_bitmap_dc, get_guide_bar(i)); + } +} + +/** + * Called after all the bars have been drawn, this triggers a refresh event to + * draw it to the window. + */ +void WinStatsFlameGraph:: +end_draw() { + InvalidateRect(_graph_window, nullptr, FALSE); +} + +/** + * Called at the end of the draw cycle. + */ +void WinStatsFlameGraph:: +idle() { +} + +/** + * + */ +LONG WinStatsFlameGraph:: +window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { + switch (msg) { + case WM_LBUTTONDOWN: + if (_potential_drag_mode == DM_new_guide_bar) { + set_drag_mode(DM_new_guide_bar); + SetCapture(_graph_window); + return 0; + } + break; + + case WM_COMMAND: + switch (LOWORD(wparam)) { + case BN_CLICKED: + if ((HWND)lparam == _average_check_box) { + int result = SendMessage(_average_check_box, BM_GETCHECK, 0, 0); + set_average_mode(result == BST_CHECKED); + return 0; + } + break; + } + break; + + default: + break; + } + + return WinStatsGraph::window_proc(hwnd, msg, wparam, lparam); +} + +/** + * + */ +LONG WinStatsFlameGraph:: +graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { + switch (msg) { + case WM_LBUTTONDOWN: + if (_potential_drag_mode == DM_guide_bar && _drag_guide_bar >= 0) { + set_drag_mode(DM_guide_bar); + int16_t x = LOWORD(lparam); + _drag_start_x = x; + SetCapture(_graph_window); + return 0; + } + break; + + case WM_MOUSEMOVE: + if (_drag_mode == DM_new_guide_bar) { + // We haven't created the new guide bar yet; we won't until the mouse + // comes within the graph's region. + int16_t x = LOWORD(lparam); + if (x >= 0 && x < get_xsize()) { + set_drag_mode(DM_guide_bar); + _drag_guide_bar = add_user_guide_bar(pixel_to_height(x)); + return 0; + } + } + else if (_drag_mode == DM_guide_bar) { + int16_t x = LOWORD(lparam); + move_user_guide_bar(_drag_guide_bar, pixel_to_height(x)); + return 0; + } + break; + + case WM_LBUTTONUP: + if (_drag_mode == DM_guide_bar) { + int16_t x = LOWORD(lparam); + if (x < 0 || x >= get_xsize()) { + remove_user_guide_bar(_drag_guide_bar); + } else { + move_user_guide_bar(_drag_guide_bar, pixel_to_height(x)); + } + set_drag_mode(DM_none); + ReleaseCapture(); + return 0; + } + break; + + case WM_LBUTTONDBLCLK: + { + // Clicking on whitespace in the graph goes to the parent. + on_click_label(get_collector_index()); + return 0; + } + break; + + default: + break; + } + + return WinStatsGraph::graph_window_proc(hwnd, msg, wparam, lparam); +} + +/** + * This is called during the servicing of WM_PAINT; it gives a derived class + * opportunity to do some further painting into the window (the outer window, + * not the graph window). + */ +void WinStatsFlameGraph:: +additional_window_paint(HDC hdc) { + // Draw in the labels for the guide bars. + SelectObject(hdc, WinStatsGraph::_monitor->get_font()); + SetTextAlign(hdc, TA_LEFT | TA_BOTTOM); + SetBkMode(hdc, TRANSPARENT); + + int y = _top_margin - _pixel_scale / 2; + + int i; + int num_guide_bars = get_num_guide_bars(); + for (i = 0; i < num_guide_bars; i++) { + draw_guide_label(hdc, y, get_guide_bar(i)); + } + + int num_user_guide_bars = get_num_user_guide_bars(); + for (i = 0; i < num_user_guide_bars; i++) { + draw_guide_label(hdc, y, get_user_guide_bar(i)); + } + + RECT rect; + GetClientRect(_window, &rect); + + // Now draw the "net value" label at the top. + SetTextAlign(hdc, TA_RIGHT | TA_BOTTOM); + SetTextColor(hdc, RGB(0, 0, 0)); + TextOut(hdc, rect.right - _right_margin, y, + _net_value_text.data(), _net_value_text.length()); +} + +/** + * This is called during the servicing of WM_PAINT; it gives a derived class + * opportunity to do some further painting into the window (the outer window, + * not the graph window). + */ +void WinStatsFlameGraph:: +additional_graph_window_paint(HDC hdc) { + int num_user_guide_bars = get_num_user_guide_bars(); + for (int i = 0; i < num_user_guide_bars; i++) { + draw_guide_bar(hdc, get_user_guide_bar(i)); + } +} + +/** + * Based on the mouse position within the window's client area, look for + * draggable things the mouse might be hovering over and return the + * apprioprate DragMode enum or DM_none if nothing is indicated. + */ +WinStatsGraph::DragMode WinStatsFlameGraph:: +consider_drag_start(int mouse_x, int mouse_y, int width, int height) { + if (mouse_y >= _graph_top && mouse_y < _graph_top + get_ysize()) { + if (mouse_x >= _graph_left && mouse_x < _graph_left + get_xsize()) { + // See if the mouse is over a user-defined guide bar. + int x = mouse_x - _graph_left; + double from_height = pixel_to_height(x - 2); + double to_height = pixel_to_height(x + 2); + _drag_guide_bar = find_user_guide_bar(from_height, to_height); + if (_drag_guide_bar >= 0) { + return DM_guide_bar; + } + + } else if (mouse_x < _left_margin - 2 || + mouse_x > width - _right_margin + 2) { + // The mouse is left or right of the graph; maybe create a new guide + // bar. + return DM_new_guide_bar; + } + } + + // Don't upcall; there's no point resizing the margins. + return DM_none; +} + +/** + * Repositions the graph child window within the parent window according to + * the _margin variables. + */ +void WinStatsFlameGraph:: +move_graph_window(int graph_left, int graph_top, int graph_xsize, int graph_ysize) { + WinStatsGraph::move_graph_window(graph_left, graph_top, graph_xsize, graph_ysize); + if (_average_check_box != 0) { + SIZE size; + SendMessage(_average_check_box, BCM_GETIDEALSIZE, 0, (LPARAM)&size); + + SetWindowPos(_average_check_box, 0, + _left_margin, _top_margin - size.cy - _pixel_scale / 2, + size.cx, size.cy, + SWP_NOZORDER | SWP_SHOWWINDOW); + InvalidateRect(_average_check_box, nullptr, TRUE); + } +} + +/** + * Draws the line for the indicated guide bar on the graph. + */ +void WinStatsFlameGraph:: +draw_guide_bar(HDC hdc, const PStatGraph::GuideBar &bar) { + int x = height_to_pixel(bar._height); + + if (x > 0 && x < get_xsize() - 1) { + // Only draw it if it's not too close to either edge. + switch (bar._style) { + case GBS_target: + SelectObject(hdc, _light_pen); + break; + + case GBS_user: + SelectObject(hdc, _user_guide_bar_pen); + break; + + case GBS_normal: + SelectObject(hdc, _dark_pen); + break; + } + MoveToEx(hdc, x, 0, nullptr); + LineTo(hdc, x, get_ysize()); + } +} + +/** + * Draws the text for the indicated guide bar label at the top of the graph. + */ +void WinStatsFlameGraph:: +draw_guide_label(HDC hdc, int y, const PStatGraph::GuideBar &bar) { + switch (bar._style) { + case GBS_target: + SetTextColor(hdc, _light_color); + break; + + case GBS_user: + SetTextColor(hdc, _user_guide_bar_color); + break; + + case GBS_normal: + SetTextColor(hdc, _dark_color); + break; + } + + int x = height_to_pixel(bar._height); + const std::string &label = bar._label; + SIZE size; + GetTextExtentPoint32(hdc, label.data(), label.length(), &size); + + if (bar._style != GBS_user) { + double from_height = pixel_to_height(x - size.cx); + double to_height = pixel_to_height(x + size.cx); + if (find_user_guide_bar(from_height, to_height) >= 0) { + // Omit the label: there's a user-defined guide bar in the same space. + return; + } + } + + int this_x = _graph_left + x - size.cx / 2; + if (x >= 0 && x < get_xsize()) { + TextOut(hdc, this_x, y, + label.data(), label.length()); + } +} + +/** + * Creates the window for this strip chart. + */ +void WinStatsFlameGraph:: +create_window() { + if (_window) { + return; + } + + HINSTANCE application = GetModuleHandle(nullptr); + register_window_class(application); + + std::string window_title = get_title_text(); + + RECT win_rect = { + 0, 0, + _left_margin + get_xsize() + _right_margin, + _top_margin + get_ysize() + _bottom_margin + }; + + // compute window size based on desired client area size + AdjustWindowRect(&win_rect, graph_window_style, FALSE); + + _window = + CreateWindow(_window_class_name, window_title.c_str(), graph_window_style, + CW_USEDEFAULT, CW_USEDEFAULT, + win_rect.right - win_rect.left, + win_rect.bottom - win_rect.top, + WinStatsGraph::_monitor->get_window(), nullptr, application, 0); + if (!_window) { + nout << "Could not create FlameGraph window!\n"; + exit(1); + } + + SetWindowLongPtr(_window, 0, (LONG_PTR)this); + + _average_check_box = + CreateWindow(WC_BUTTON, "Average", WS_CHILD | BS_AUTOCHECKBOX, + 0, 0, 0, 0, + _window, nullptr, application, 0); + SendMessage(_average_check_box, WM_SETFONT, + (WPARAM)WinStatsGraph::_monitor->get_font(), TRUE); + + if (get_average_mode()) { + SendMessage(_average_check_box, BM_SETCHECK, BST_CHECKED, 0); + } + + // Ensure that the window is on top of the stack. + SetWindowPos(_window, HWND_TOP, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); +} + +/** + * Registers the window class for the FlameGraph window, if it has not already + * been registered. + */ +void WinStatsFlameGraph:: +register_window_class(HINSTANCE application) { + if (_window_class_registered) { + return; + } + + WNDCLASS wc; + + ZeroMemory(&wc, sizeof(WNDCLASS)); + wc.style = 0; + wc.lpfnWndProc = (WNDPROC)static_window_proc; + wc.hInstance = application; + wc.hCursor = LoadCursor(nullptr, IDC_ARROW); + wc.hbrBackground = (HBRUSH)COLOR_WINDOW; + wc.lpszMenuName = nullptr; + wc.lpszClassName = _window_class_name; + + // Reserve space to associate the this pointer with the window. + wc.cbWndExtra = sizeof(WinStatsFlameGraph *); + + if (!RegisterClass(&wc)) { + nout << "Could not register FlameGraph window class!\n"; + exit(1); + } + + _window_class_registered = true; +} + +/** + * + */ +LONG WINAPI WinStatsFlameGraph:: +static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { + WinStatsFlameGraph *self = (WinStatsFlameGraph *)GetWindowLongPtr(hwnd, 0); + if (self != nullptr && self->_window == hwnd) { + return self->window_proc(hwnd, msg, wparam, lparam); + } else { + return DefWindowProc(hwnd, msg, wparam, lparam); + } +} diff --git a/pandatool/src/win-stats/winStatsFlameGraph.h b/pandatool/src/win-stats/winStatsFlameGraph.h new file mode 100644 index 0000000000..262f05979e --- /dev/null +++ b/pandatool/src/win-stats/winStatsFlameGraph.h @@ -0,0 +1,80 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file winStatsFlameGraph.h + * @author rdb + * @date 2022-01-28 + */ + +#ifndef WINSTATSFLAMEGRAPH_H +#define WINSTATSFLAMEGRAPH_H + +#include "pandatoolbase.h" + +#include "winStatsGraph.h" +#include "pStatFlameGraph.h" + +class WinStatsLabel; + +/** + * A window that draws a flame chart, which shows the collectors explicitly + * stopping and starting, one frame at a time. + */ +class WinStatsFlameGraph : public PStatFlameGraph, public WinStatsGraph { +public: + WinStatsFlameGraph(WinStatsMonitor *monitor, int thread_index, + int collector_index=0); + virtual ~WinStatsFlameGraph(); + + virtual void new_data(int thread_index, int frame_number); + virtual void force_redraw(); + virtual void changed_graph_size(int graph_xsize, int graph_ysize); + + virtual void set_time_units(int unit_mask); + virtual void on_click_label(int collector_index); + virtual void on_enter_label(int collector_index); + virtual void on_leave_label(int collector_index); + virtual std::string get_label_tooltip(int collector_index) const; + +protected: + virtual void update_labels(); + virtual void update_label(int collector_index, int row, int x, int width); + virtual void normal_guide_bars(); + + void clear_region(); + virtual void begin_draw(); + virtual void end_draw(); + virtual void idle(); + + LONG window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); + virtual LONG graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); + virtual void additional_window_paint(HDC hdc); + virtual void additional_graph_window_paint(HDC hdc); + virtual DragMode consider_drag_start(int mouse_x, int mouse_y, + int width, int height); + virtual void move_graph_window(int graph_left, int graph_top, + int graph_xsize, int graph_ysize); + +private: + void draw_guide_bar(HDC hdc, const GuideBar &bar); + void draw_guide_label(HDC hdc, int y, const PStatGraph::GuideBar &bar); + void create_window(); + static void register_window_class(HINSTANCE application); + + static LONG WINAPI static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); + + std::string _net_value_text; + pmap _labels; + + HWND _average_check_box; + + static bool _window_class_registered; + static const char * const _window_class_name; +}; + +#endif diff --git a/pandatool/src/win-stats/winStatsGraph.cxx b/pandatool/src/win-stats/winStatsGraph.cxx index 4946bbc671..7fd3b251fa 100644 --- a/pandatool/src/win-stats/winStatsGraph.cxx +++ b/pandatool/src/win-stats/winStatsGraph.cxx @@ -14,9 +14,11 @@ #include "winStatsGraph.h" #include "winStatsMonitor.h" #include "winStatsLabelStack.h" +#include "convert_srgb.h" -bool WinStatsGraph::_graph_window_class_registered = false; -const char * const WinStatsGraph::_graph_window_class_name = "graph"; +#include + +#define IDC_GRAPH 100 DWORD WinStatsGraph::graph_window_style = WS_CHILD | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_OVERLAPPEDWINDOW | WS_VISIBLE; @@ -40,6 +42,8 @@ WinStatsGraph(WinStatsMonitor *monitor) : _bitmap_xsize = 0; _bitmap_ysize = 0; + _pixel_scale = monitor->get_pixel_scale(); + _dark_color = RGB(51, 51, 51); _light_color = RGB(154, 154, 154); _user_guide_bar_color = RGB(130, 150, 255); @@ -66,11 +70,11 @@ WinStatsGraph:: DeleteObject(_light_pen); DeleteObject(_user_guide_bar_pen); - Brushes::iterator bi; - for (bi = _brushes.begin(); bi != _brushes.end(); ++bi) { - HBRUSH brush = (*bi).second; - DeleteObject(brush); + for (auto &item : _brushes) { + DeleteObject(item.second.first); + DeleteObject(item.second.second); } + _brushes.clear(); if (_graph_window) { DestroyWindow(_graph_window); @@ -97,13 +101,6 @@ void WinStatsGraph:: new_data(int thread_index, int frame_number) { } -/** - * Called when it is necessary to redraw the entire graph. - */ -void WinStatsGraph:: -force_redraw() { -} - /** * Called when the user has resized the window, forcing a resize of the graph. */ @@ -150,7 +147,46 @@ user_guide_bars_changed() { * Called when the user single-clicks on a label. */ void WinStatsGraph:: -clicked_label(int collector_index) { +on_click_label(int collector_index) { +} + +/** + * Called when the user hovers the mouse over a label. + */ +void WinStatsGraph:: +on_enter_label(int collector_index) { + if (collector_index != _highlighted_index) { + _highlighted_index = collector_index; + force_redraw(); + } +} + +/** + * Called when the user's mouse cursor leaves a label. + */ +void WinStatsGraph:: +on_leave_label(int collector_index) { + if (collector_index == _highlighted_index && collector_index != -1) { + _highlighted_index = -1; + force_redraw(); + } +} + +/** + * Called when the mouse hovers over a label, and should return the text that + * should appear on the tooltip. + */ +std::string WinStatsGraph:: +get_label_tooltip(int collector_index) const { + return std::string(); +} + +/** + * Returns the window handle of the surrounding window. + */ +HWND WinStatsGraph:: +get_window() { + return _window; } /** @@ -184,8 +220,8 @@ move_label_stack() { RECT rect; GetClientRect(_window, &rect); - rect.left += 8; - rect.right = _left_margin - 8; + rect.left += _pixel_scale * 2; + rect.right = _left_margin - _pixel_scale * 2; rect.bottom -= _bottom_margin; _label_stack.set_pos(rect.left, rect.top, @@ -197,22 +233,27 @@ move_label_stack() { * Returns a brush suitable for drawing in the indicated collector's color. */ HBRUSH WinStatsGraph:: -get_collector_brush(int collector_index) { +get_collector_brush(int collector_index, bool highlight) { Brushes::iterator bi; bi = _brushes.find(collector_index); if (bi != _brushes.end()) { - return (*bi).second; + return highlight ? (*bi).second.second : (*bi).second.first; } // Ask the monitor what color this guy should be. LRGBColor rgb = _monitor->get_collector_color(collector_index); - int r = (int)(rgb[0] * 255.0f); - int g = (int)(rgb[1] * 255.0f); - int b = (int)(rgb[2] * 255.0f); + int r = (int)encode_sRGB_uchar(rgb[0]); + int g = (int)encode_sRGB_uchar(rgb[1]); + int b = (int)encode_sRGB_uchar(rgb[2]); HBRUSH brush = CreateSolidBrush(RGB(r, g, b)); - _brushes[collector_index] = brush; - return brush; + int hr = (int)encode_sRGB_uchar(rgb[0] * 0.75f); + int hg = (int)encode_sRGB_uchar(rgb[1] * 0.75f); + int hb = (int)encode_sRGB_uchar(rgb[2] * 0.75f); + HBRUSH hbrush = CreateSolidBrush(RGB(hr, hg, hb)); + + _brushes[collector_index] = std::make_pair(brush, hbrush); + return highlight ? hbrush : brush; } /** @@ -226,6 +267,20 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { close(); break; + case WM_GETMINMAXINFO: + { + WINDOWINFO winfo; + GetWindowInfo(hwnd, &winfo); + MINMAXINFO &minmax = *(MINMAXINFO *)lparam; + minmax.ptMinTrackSize.x = (winfo.rcClient.left - winfo.rcWindow.left) + + (winfo.rcWindow.right - winfo.rcClient.right) + + (_right_margin + _left_margin); + minmax.ptMinTrackSize.y = (winfo.rcClient.top - winfo.rcWindow.top) + + (winfo.rcWindow.bottom - winfo.rcClient.bottom) + + (_bottom_margin + _top_margin); + return 0; + } + case WM_SIZE: move_label_stack(); InvalidateRect(hwnd, nullptr, TRUE); @@ -319,8 +374,6 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { rect.bottom -= _bottom_margin; if (rect.right > rect.left && rect.bottom > rect.top) { - DrawEdge(hdc, &rect, EDGE_SUNKEN, BF_RECT | BF_ADJUST); - int graph_xsize = rect.right - rect.left; int graph_ysize = rect.bottom - rect.top; if (_bitmap_dc == 0 || @@ -339,6 +392,21 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { return 0; } + case WM_DRAWITEM: + if (wparam == IDC_GRAPH) { + const DRAWITEMSTRUCT &dis = *(DRAWITEMSTRUCT *)lparam; + + // Repaint the graph by copying the backing pixmap in. + BitBlt(dis.hDC, 0, 0, + _bitmap_xsize, _bitmap_ysize, + _bitmap_dc, 0, 0, + SRCCOPY); + + additional_graph_window_paint(dis.hDC); + return 0; + } + break; + default: break; } @@ -357,6 +425,10 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { force_redraw(); break; + case WM_NCHITTEST: + // Necessary for mouse events to work; default returns HTTRANSPARENT + return HTCLIENT; + case WM_LBUTTONDOWN: // Vector any uncaught WM_LBUTTONDOWN into the main window, so we can drag // margins, etc. @@ -372,28 +444,11 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { ReleaseCapture(); break; - case WM_PAINT: - { - // Repaint the graph by copying the backing pixmap in. - PAINTSTRUCT ps; - HDC hdc = BeginPaint(hwnd, &ps); - - BitBlt(hdc, 0, 0, - _bitmap_xsize, _bitmap_ysize, - _bitmap_dc, 0, 0, - SRCCOPY); - - additional_graph_window_paint(hdc); - - EndPaint(hwnd, &ps); - return 0; - } - default: break; } - return DefWindowProc(hwnd, msg, wparam, lparam); + return DefSubclassProc(hwnd, msg, wparam, lparam); } /** @@ -506,61 +561,29 @@ create_graph_window() { } HINSTANCE application = GetModuleHandle(nullptr); - register_graph_window_class(application); - std::string window_title = "graph"; - DWORD window_style = WS_CHILD | WS_CLIPSIBLINGS; + DWORD window_style = WS_CHILD | WS_CLIPSIBLINGS | + SS_SUNKEN | SS_OWNERDRAW; _graph_window = - CreateWindow(_graph_window_class_name, window_title.c_str(), window_style, - 0, 0, 0, 0, - _window, nullptr, application, 0); + CreateWindow(WC_STATIC, "", window_style, 0, 0, 0, 0, + _window, (HMENU)IDC_GRAPH, application, 0); if (!_graph_window) { nout << "Could not create graph window!\n"; exit(1); } - SetWindowLongPtr(_graph_window, 0, (LONG_PTR)this); -} + EnableWindow(_graph_window, TRUE); -/** - * Registers the window class for the stripChart window, if it has not already - * been registered. - */ -void WinStatsGraph:: -register_graph_window_class(HINSTANCE application) { - if (_graph_window_class_registered) { - return; - } - - WNDCLASS wc; - - ZeroMemory(&wc, sizeof(WNDCLASS)); - wc.style = CS_DBLCLKS; - wc.lpfnWndProc = (WNDPROC)static_graph_window_proc; - wc.hInstance = application; - wc.hCursor = LoadCursor(nullptr, IDC_ARROW); - wc.hbrBackground = nullptr; - wc.lpszMenuName = nullptr; - wc.lpszClassName = _graph_window_class_name; - - // Reserve space to associate the this pointer with the window. - wc.cbWndExtra = sizeof(WinStatsGraph *); - - if (!RegisterClass(&wc)) { - nout << "Could not register graph window class!\n"; - exit(1); - } - - _graph_window_class_registered = true; + SetWindowSubclass(_graph_window, &static_graph_subclass_proc, 1234, (DWORD_PTR)this); } /** * */ -LONG WINAPI WinStatsGraph:: -static_graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { - WinStatsGraph *self = (WinStatsGraph *)GetWindowLongPtr(hwnd, 0); +LRESULT WINAPI WinStatsGraph:: +static_graph_subclass_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam, UINT_PTR subclass, DWORD_PTR ref_data) { + WinStatsGraph *self = (WinStatsGraph *)ref_data; if (self != nullptr && self->_graph_window == hwnd) { return self->graph_window_proc(hwnd, msg, wparam, lparam); } else { diff --git a/pandatool/src/win-stats/winStatsGraph.h b/pandatool/src/win-stats/winStatsGraph.h index 82fb04f71d..388a78323f 100644 --- a/pandatool/src/win-stats/winStatsGraph.h +++ b/pandatool/src/win-stats/winStatsGraph.h @@ -48,7 +48,7 @@ public: virtual void new_collector(int collector_index); virtual void new_data(int thread_index, int frame_number); - virtual void force_redraw(); + virtual void force_redraw()=0; virtual void changed_graph_size(int graph_xsize, int graph_ysize); virtual void set_time_units(int unit_mask); @@ -56,7 +56,12 @@ public: void set_pause(bool pause); void user_guide_bars_changed(); - virtual void clicked_label(int collector_index); + virtual void on_click_label(int collector_index); + virtual void on_enter_label(int collector_index); + virtual void on_leave_label(int collector_index); + virtual std::string get_label_tooltip(int collector_index) const; + + HWND get_window(); protected: void close(); @@ -64,7 +69,7 @@ protected: void setup_label_stack(); void move_label_stack(); - HBRUSH get_collector_brush(int collector_index); + HBRUSH get_collector_brush(int collector_index, bool highlight = false); LONG window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); virtual LONG graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); @@ -80,7 +85,7 @@ protected: protected: // Table of brushes for our various collectors. - typedef pmap Brushes; + typedef pmap > Brushes; Brushes _brushes; WinStatsMonitor *_monitor; @@ -98,6 +103,7 @@ protected: int _bitmap_xsize, _bitmap_ysize; int _left_margin, _right_margin; int _top_margin, _bottom_margin; + int _pixel_scale; COLORREF _dark_color; COLORREF _light_color; @@ -112,18 +118,16 @@ protected: double _drag_scale_start; int _drag_guide_bar; + int _highlighted_index = -1; + bool _pause; private: void setup_bitmap(int xsize, int ysize); void release_bitmap(); void create_graph_window(); - static void register_graph_window_class(HINSTANCE application); - static LONG WINAPI static_graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam); - - static bool _graph_window_class_registered; - static const char * const _graph_window_class_name; + static LRESULT WINAPI static_graph_subclass_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam, UINT_PTR subclass, DWORD_PTR ref_data); protected: static DWORD graph_window_style; diff --git a/pandatool/src/win-stats/winStatsLabel.I b/pandatool/src/win-stats/winStatsLabel.I new file mode 100644 index 0000000000..a7cabe90b9 --- /dev/null +++ b/pandatool/src/win-stats/winStatsLabel.I @@ -0,0 +1,68 @@ +/** + * PANDA 3D SOFTWARE + * Copyright (c) Carnegie Mellon University. All rights reserved. + * + * All use of this software is subject to the terms of the revised BSD + * license. You should have received a copy of this license along + * with this source code in a file named "LICENSE." + * + * @file winStatsLabel.I + * @author rdb + * @date 2022-01-29 + */ + +/** + * Returns the x position of the label on its parent. + */ +INLINE int WinStatsLabel:: +get_x() const { + return _x; +} + +/** + * Returns the y position of the label on its parent. + */ +INLINE int WinStatsLabel:: +get_y() const { + return _y; +} + +/** + * Returns the width of the label as we requested it. + */ +INLINE int WinStatsLabel:: +get_width() const { + return _width; +} + +/** + * Returns the height of the label as we requested it. + */ +INLINE int WinStatsLabel:: +get_height() const { + return _height; +} + +/** + * Returns the width the label would really prefer to be. + */ +INLINE int WinStatsLabel:: +get_ideal_width() const { + return _ideal_width; +} + +/** + * Returns the collector this label represents. + */ +INLINE int WinStatsLabel:: +get_collector_index() const { + return _collector_index; +} + +/** + * Returns true if the visual highlight for this label is enabled. + */ +INLINE bool WinStatsLabel:: +get_highlight() const { + return _highlight; +} diff --git a/pandatool/src/win-stats/winStatsLabel.cxx b/pandatool/src/win-stats/winStatsLabel.cxx index 5cd744c501..00021e7376 100644 --- a/pandatool/src/win-stats/winStatsLabel.cxx +++ b/pandatool/src/win-stats/winStatsLabel.cxx @@ -14,6 +14,9 @@ #include "winStatsLabel.h" #include "winStatsMonitor.h" #include "winStatsGraph.h" +#include "convert_srgb.h" + +#include int WinStatsLabel::_left_margin = 2; int WinStatsLabel::_right_margin = 2; @@ -28,38 +31,45 @@ const char * const WinStatsLabel::_window_class_name = "label"; */ WinStatsLabel:: WinStatsLabel(WinStatsMonitor *monitor, WinStatsGraph *graph, - int thread_index, int collector_index, bool use_fullname) : + int thread_index, int collector_index, bool use_fullname, + bool align_right) : _monitor(monitor), _graph(graph), _thread_index(thread_index), - _collector_index(collector_index) + _collector_index(collector_index), + _align_right(align_right), + _window(0), + _tooltip_window(0) { - _window = 0; - if (use_fullname) { - _text = _monitor->get_client_data()->get_collector_fullname(_collector_index); - } else { - _text = _monitor->get_client_data()->get_collector_name(_collector_index); - } + update_text(use_fullname); LRGBColor rgb = _monitor->get_collector_color(_collector_index); - int r = (int)(rgb[0] * 255.0f); - int g = (int)(rgb[1] * 255.0f); - int b = (int)(rgb[2] * 255.0f); - _bg_color = RGB(r, g, b); + int r = (int)encode_sRGB_uchar(rgb[0]); + int g = (int)encode_sRGB_uchar(rgb[1]); + int b = (int)encode_sRGB_uchar(rgb[2]); _bg_brush = CreateSolidBrush(RGB(r, g, b)); + // Calculate the color when it is highlighted. + int hr = (int)encode_sRGB_uchar(rgb[0] * 0.75f); + int hg = (int)encode_sRGB_uchar(rgb[1] * 0.75f); + int hb = (int)encode_sRGB_uchar(rgb[2] * 0.75f); + _highlight_bg_brush = CreateSolidBrush(RGB(hr, hg, hb)); + // Should our foreground be black or white? double bright = - rgb[0] * 0.299 + - rgb[1] * 0.587 + - rgb[2] * 0.114; + rgb[0] * 0.2126 + + rgb[1] * 0.7152 + + rgb[2] * 0.0722; if (bright >= 0.5) { _fg_color = RGB(0, 0, 0); - _highlight_brush = (HBRUSH)GetStockObject(BLACK_BRUSH); } else { _fg_color = RGB(255, 255, 255); - _highlight_brush = (HBRUSH)GetStockObject(WHITE_BRUSH); + } + if (bright >= 0.5 * 0.75) { + _highlight_fg_color = RGB(0, 0, 0); + } else { + _highlight_fg_color = RGB(255, 255, 255); } _x = 0; @@ -77,6 +87,10 @@ WinStatsLabel(WinStatsMonitor *monitor, WinStatsGraph *graph, WinStatsLabel:: ~WinStatsLabel() { if (_window) { + if (_tooltip_window) { + DestroyWindow(_tooltip_window); + _tooltip_window = 0; + } DestroyWindow(_window); _window = 0; } @@ -96,7 +110,7 @@ setup(HWND parent_window) { create_window(parent_window); HDC hdc = GetDC(_window); - HFONT hfnt = (HFONT)GetStockObject(ANSI_VAR_FONT); + HFONT hfnt = _monitor->get_font(); SelectObject(hdc, hfnt); SIZE size; @@ -113,59 +127,13 @@ setup(HWND parent_window) { */ void WinStatsLabel:: set_pos(int x, int y, int width) { - _x = x; - _y = y; - _width = width; - SetWindowPos(_window, 0, x, y - _height, _width, _height, - SWP_NOZORDER | SWP_SHOWWINDOW); -} - -/** - * Returns the x position of the label on its parent. - */ -int WinStatsLabel:: -get_x() const { - return _x; -} - -/** - * Returns the y position of the label on its parent. - */ -int WinStatsLabel:: -get_y() const { - return _y; -} - -/** - * Returns the width of the label as we requested it. - */ -int WinStatsLabel:: -get_width() const { - return _width; -} - -/** - * Returns the height of the label as we requested it. - */ -int WinStatsLabel:: -get_height() const { - return _height; -} - -/** - * Returns the width the label would really prefer to be. - */ -int WinStatsLabel:: -get_ideal_width() const { - return _ideal_width; -} - -/** - * Returns the collector this label represents. - */ -int WinStatsLabel:: -get_collector_index() const { - return _collector_index; + if (x != _x || y != _y || width != _width) { + _x = x; + _y = y; + _width = width; + SetWindowPos(_window, 0, x, y - _height, _width, _height, + SWP_NOZORDER | SWP_SHOWWINDOW); + } } /** @@ -180,11 +148,29 @@ set_highlight(bool highlight) { } /** - * Returns true if the visual highlight for this label is enabled. + * Set to true if the full name of the collector should be shown. */ -bool WinStatsLabel:: -get_highlight() const { - return _highlight; +void WinStatsLabel:: +update_text(bool use_fullname) { + const PStatClientData *client_data = _monitor->get_client_data(); + _tooltip_text = client_data->get_collector_fullname(_collector_index); + if (use_fullname) { + _text = _tooltip_text; + } else { + _text = client_data->get_collector_name(_collector_index); + } + + // Recalculate the dimensions. + if (_window) { + HDC hdc = GetDC(_window); + HFONT hfnt = _monitor->get_font(); + SelectObject(hdc, hfnt); + + SIZE size; + GetTextExtentPoint32(hdc, _text.data(), _text.length(), &size); + _height = size.cy + _top_margin + _bottom_margin; + _ideal_width = size.cx + _left_margin + _right_margin; + } } /** @@ -220,6 +206,25 @@ create_window(HWND parent_window) { } SetWindowLongPtr(_window, 0, (LONG_PTR)this); + + // Create the tooltip window. This will cause a TTN_GETDISPINFO message to + // be sent to the window to acquire the tooltip text. + _tooltip_window = CreateWindow(TOOLTIPS_CLASS, nullptr, + WS_POPUP, + CW_USEDEFAULT, CW_USEDEFAULT, + CW_USEDEFAULT, CW_USEDEFAULT, + _window, nullptr, + application, nullptr); + + if (_tooltip_window != 0) { + TOOLINFO info = { 0 }; + info.cbSize = sizeof(info); + info.uFlags = TTF_IDISHWND | TTF_SUBCLASS; + info.hwnd = _window; + info.uId = (UINT_PTR)_window; + info.lpszText = LPSTR_TEXTCALLBACK; + SendMessage(_tooltip_window, TTM_ADDTOOL, 0, (LPARAM)&info); + } } /** @@ -274,13 +279,16 @@ LONG WinStatsLabel:: window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { switch (msg) { case WM_LBUTTONDBLCLK: - _graph->clicked_label(_collector_index); + _graph->on_click_label(_collector_index); return 0; case WM_MOUSEMOVE: { // When the mouse enters the label area, highlight the label. - set_mouse_within(true); + if (!_mouse_within) { + set_mouse_within(true); + _graph->on_enter_label(_collector_index); + } // Now we want to get a WM_MOUSELEAVE when the mouse leaves the label. TRACKMOUSEEVENT tme = { @@ -294,7 +302,10 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { break; case WM_MOUSELEAVE: - set_mouse_within(false); + if (_mouse_within) { + set_mouse_within(false); + _graph->on_leave_label(_collector_index); + } break; case WM_PAINT: @@ -303,26 +314,33 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { HDC hdc = BeginPaint(hwnd, &ps); RECT rect = { 0, 0, _width, _height }; - FillRect(hdc, &rect, _bg_brush); + FillRect(hdc, &rect, (_highlight || _mouse_within) ? _highlight_bg_brush : _bg_brush); - if (_highlight || _mouse_within) { - FrameRect(hdc, &rect, _highlight_brush); - } - - HFONT hfnt = (HFONT)GetStockObject(ANSI_VAR_FONT); + HFONT hfnt = _monitor->get_font(); SelectObject(hdc, hfnt); - SetTextAlign(hdc, TA_RIGHT | TA_TOP); + SetTextAlign(hdc, (_align_right ? TA_RIGHT : TA_LEFT) | TA_TOP); - SetBkColor(hdc, _bg_color); - SetBkMode(hdc, OPAQUE); - SetTextColor(hdc, _fg_color); + SetBkMode(hdc, TRANSPARENT); + SetTextColor(hdc, (_highlight || _mouse_within) ? _highlight_fg_color : _fg_color); - TextOut(hdc, _width - _right_margin, _top_margin, - _text.data(), _text.length()); + TextOut(hdc, _align_right ? (_width - _right_margin) : _left_margin, + _top_margin, _text.data(), _text.length()); EndPaint(hwnd, &ps); return 0; } + case WM_NOTIFY: + switch (((LPNMHDR)lparam)->code) { + case TTN_GETDISPINFO: + { + NMTTDISPINFO &info = *(NMTTDISPINFO *)lparam; + _tooltip_text = _graph->get_label_tooltip(_collector_index); + info.lpszText = (char *)_tooltip_text.c_str(); + } + return 0; + } + break; + default: break; } diff --git a/pandatool/src/win-stats/winStatsLabel.h b/pandatool/src/win-stats/winStatsLabel.h index 5642cdb8d0..a970d3e13e 100644 --- a/pandatool/src/win-stats/winStatsLabel.h +++ b/pandatool/src/win-stats/winStatsLabel.h @@ -32,22 +32,25 @@ class WinStatsGraph; class WinStatsLabel { public: WinStatsLabel(WinStatsMonitor *monitor, WinStatsGraph *graph, - int thread_index, int collector_index, bool use_fullname); + int thread_index, int collector_index, bool use_fullname, + bool align_right = true); ~WinStatsLabel(); void setup(HWND parent_window); void set_pos(int x, int y, int width); - int get_x() const; - int get_y() const; - int get_width() const; - int get_height() const; - int get_ideal_width() const; + INLINE int get_x() const; + INLINE int get_y() const; + INLINE int get_width() const; + INLINE int get_height() const; + INLINE int get_ideal_width() const; - int get_collector_index() const; + INLINE int get_collector_index() const; void set_highlight(bool highlight); - bool get_highlight() const; + INLINE bool get_highlight() const; + + void update_text(bool use_fullname); private: void set_mouse_within(bool mouse_within); @@ -63,11 +66,13 @@ private: int _thread_index; int _collector_index; std::string _text; + std::string _tooltip_text; HWND _window; - COLORREF _bg_color; + HWND _tooltip_window; COLORREF _fg_color; + COLORREF _highlight_fg_color; HBRUSH _bg_brush; - HBRUSH _highlight_brush; + HBRUSH _highlight_bg_brush; int _x; int _y; @@ -76,6 +81,7 @@ private: int _ideal_width; bool _highlight; bool _mouse_within; + bool _align_right; static int _left_margin, _right_margin; static int _top_margin, _bottom_margin; @@ -84,4 +90,6 @@ private: static const char * const _window_class_name; }; +#include "winStatsLabel.I" + #endif diff --git a/pandatool/src/win-stats/winStatsLabelStack.cxx b/pandatool/src/win-stats/winStatsLabelStack.cxx index af3bcccd00..e6633f025b 100644 --- a/pandatool/src/win-stats/winStatsLabelStack.cxx +++ b/pandatool/src/win-stats/winStatsLabelStack.cxx @@ -200,6 +200,90 @@ add_label(WinStatsMonitor *monitor, WinStatsGraph *graph, return label_index; } +/** + * Replaces the labels with the given collector indices. + */ +void WinStatsLabelStack:: +replace_labels(WinStatsMonitor *monitor, WinStatsGraph *graph, + int thread_index, const vector_int &collector_indices, + bool use_fullname) { + + _ideal_width = 0; + + // First skip the part of the stack that hasn't changed. + size_t li = 0; + size_t ci = 0; + while (ci < collector_indices.size() && li < _labels.size()) { + WinStatsLabel *label = _labels[li]; + if (collector_indices[ci] != label->get_collector_index()) { + // Mismatch. + break; + } + _ideal_width = std::max(_ideal_width, label->get_ideal_width()); + ++ci; + ++li; + } + + if (ci == collector_indices.size()) { + if (ci == _labels.size()) { + // Perfect, nothing changed. + return; + } + + // Simple case, just delete the rest. + while (li < _labels.size()) { + delete _labels[li++]; + } + _labels.resize(ci); + return; + } + + int yp = _height; + if (li > 0) { + WinStatsLabel *label = _labels[li - 1]; + yp = label->get_y() - label->get_height(); + } + + // Make a map of remaining labels. + std::map label_map; + for (size_t li2 = li; li2 < _labels.size(); ++li2) { + WinStatsLabel *label = _labels[li2]; + label_map[label->get_collector_index()] = label; + } + + _labels.resize(collector_indices.size()); + + while (ci < collector_indices.size()) { + int collector_index = collector_indices[ci++]; + + WinStatsLabel *label; + auto it = label_map.find(collector_index); + if (it == label_map.end()) { + // It's not in the map. Create a new label. + label = new WinStatsLabel(monitor, graph, thread_index, collector_index, use_fullname); + if (_window) { + label->setup(_window); + } + } else { + // Erase it from the map, so that it's not deleted. + label = it->second; + label_map.erase(it); + } + if (_window) { + label->set_pos(0, yp, _width); + } + _ideal_width = std::max(_ideal_width, label->get_ideal_width()); + yp -= label->get_height(); + + _labels[li++] = label; + } + + // Anything that's remaining in the label map should be deleted. + for (auto it = label_map.begin(); it != label_map.end(); ++it) { + delete it->second; + } +} + /** * Returns the number of labels in the stack. */ @@ -225,7 +309,6 @@ highlight_label(int collector_index) { } } - /** * Creates the window for this stack. */ @@ -306,7 +389,7 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { HDC hdc = BeginPaint(hwnd, &ps); RECT rect = { 0, 0, _width, _height }; - FillRect(hdc, &rect, (HBRUSH)COLOR_BACKGROUND); + FillRect(hdc, &rect, (HBRUSH)COLOR_WINDOW); EndPaint(hwnd, &ps); return 0; } diff --git a/pandatool/src/win-stats/winStatsLabelStack.h b/pandatool/src/win-stats/winStatsLabelStack.h index e7fd1da41f..48b9424754 100644 --- a/pandatool/src/win-stats/winStatsLabelStack.h +++ b/pandatool/src/win-stats/winStatsLabelStack.h @@ -16,6 +16,7 @@ #include "pandatoolbase.h" #include "pvector.h" +#include "vector_int.h" #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN 1 @@ -51,6 +52,9 @@ public: void clear_labels(); int add_label(WinStatsMonitor *monitor, WinStatsGraph *graph, int thread_index, int collector_index, bool use_fullname); + void replace_labels(WinStatsMonitor *monitor, WinStatsGraph *graph, + int thread_index, const vector_int &collector_indices, + bool use_fullname); int get_num_labels() const; void highlight_label(int collector_index); diff --git a/pandatool/src/win-stats/winStatsMonitor.cxx b/pandatool/src/win-stats/winStatsMonitor.cxx index 50c66362d1..7c877c28fd 100644 --- a/pandatool/src/win-stats/winStatsMonitor.cxx +++ b/pandatool/src/win-stats/winStatsMonitor.cxx @@ -15,6 +15,7 @@ #include "winStatsServer.h" #include "winStatsStripChart.h" #include "winStatsPianoRoll.h" +#include "winStatsFlameGraph.h" #include "winStatsChartMenu.h" #include "winStatsMenuId.h" #include "pStatGraph.h" @@ -37,6 +38,25 @@ WinStatsMonitor(WinStatsServer *server) : PStatMonitor(server) { _time_units = 0; _scroll_speed = 0.0; _pause = false; + + // Create the fonts used for rendering the UI. + NONCLIENTMETRICS metrics = {0}; + metrics.cbSize = sizeof(NONCLIENTMETRICS); + if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, 0, &metrics, 0)) { + _font = CreateFontIndirect(&metrics.lfMenuFont); + } else { + _font = (HFONT)GetStockObject(ANSI_VAR_FONT); + } + + HDC dc = GetDC(nullptr); + _pixel_scale = 0; + if (dc) { + _pixel_scale = GetDeviceCaps(dc, LOGPIXELSX) / (96 / 4); + } + if (_pixel_scale <= 0) { + _pixel_scale = 4; + } + ReleaseDC(nullptr, dc); } /** @@ -252,6 +272,22 @@ get_window() const { return _window; } +/** + * Returns the font that should be used for rendering text. + */ +HFONT WinStatsMonitor:: +get_font() const { + return _font; +} + +/** + * Returns the system DPI scaling as a fraction where 4 = no scaling. + */ +int WinStatsMonitor:: +get_pixel_scale() const { + return _pixel_scale; +} + /** * Opens a new strip chart showing the indicated data. */ @@ -279,6 +315,19 @@ open_piano_roll(int thread_index) { graph->set_pause(_pause); } +/** + * Opens a new flame graph showing the indicated data. + */ +void WinStatsMonitor:: +open_flame_graph(int thread_index) { + WinStatsFlameGraph *graph = new WinStatsFlameGraph(this, thread_index); + add_graph(graph); + + graph->set_time_units(_time_units); + graph->set_scroll_speed(_scroll_speed); + graph->set_pause(_pause); +} + /** * Returns the MenuDef properties associated with the indicated menu ID. This * specifies what we expect to do when the given menu has been selected. @@ -604,7 +653,7 @@ register_window_class(HINSTANCE application) { wc.lpfnWndProc = (WNDPROC)static_window_proc; wc.hInstance = application; wc.hCursor = LoadCursor(nullptr, IDC_ARROW); - wc.hbrBackground = (HBRUSH)COLOR_BACKGROUND; + wc.hbrBackground = (HBRUSH)COLOR_WINDOW; wc.lpszMenuName = nullptr; wc.lpszClassName = _window_class_name; @@ -701,9 +750,13 @@ handle_menu_command(int menu_id) { default: if (menu_id >= MI_new_chart) { const MenuDef &menu_def = lookup_menu(menu_id); - if (menu_def._collector_index < 0) { + if (menu_def._collector_index == -2) { + open_flame_graph(menu_def._thread_index); + } + else if (menu_def._collector_index < 0) { open_piano_roll(menu_def._thread_index); - } else { + } + else { open_strip_chart(menu_def._thread_index, menu_def._collector_index, menu_def._show_level); } diff --git a/pandatool/src/win-stats/winStatsMonitor.h b/pandatool/src/win-stats/winStatsMonitor.h index c70a71507e..9da5034a71 100644 --- a/pandatool/src/win-stats/winStatsMonitor.h +++ b/pandatool/src/win-stats/winStatsMonitor.h @@ -66,8 +66,12 @@ public: virtual void user_guide_bars_changed(); HWND get_window() const; + HFONT get_font() const; + int get_pixel_scale() const; + void open_strip_chart(int thread_index, int collector_index, bool show_level); void open_piano_roll(int thread_index); + void open_flame_graph(int thread_index); const MenuDef &lookup_menu(int menu_id) const; int get_menu_id(const MenuDef &menu_def); @@ -109,6 +113,9 @@ private: int _time_units; double _scroll_speed; bool _pause; + int _pixel_scale; + + HFONT _font; static bool _window_class_registered; static const char * const _window_class_name; diff --git a/pandatool/src/win-stats/winStatsPianoRoll.cxx b/pandatool/src/win-stats/winStatsPianoRoll.cxx index 859de11b68..0b22831184 100644 --- a/pandatool/src/win-stats/winStatsPianoRoll.cxx +++ b/pandatool/src/win-stats/winStatsPianoRoll.cxx @@ -15,7 +15,7 @@ #include "winStatsMonitor.h" #include "numeric_types.h" -static const int default_piano_roll_width = 400; +static const int default_piano_roll_width = 600; static const int default_piano_roll_height = 200; bool WinStatsPianoRoll::_window_class_registered = false; @@ -27,14 +27,14 @@ const char * const WinStatsPianoRoll::_window_class_name = "piano"; WinStatsPianoRoll:: WinStatsPianoRoll(WinStatsMonitor *monitor, int thread_index) : PStatPianoRoll(monitor, thread_index, - default_piano_roll_width, - default_piano_roll_height), + monitor->get_pixel_scale() * default_piano_roll_width / 4, + monitor->get_pixel_scale() * default_piano_roll_height / 4), WinStatsGraph(monitor) { - _left_margin = 128; - _right_margin = 8; - _top_margin = 16; - _bottom_margin = 8; + _left_margin = _pixel_scale * 32; + _right_margin = _pixel_scale * 2; + _top_margin = _pixel_scale * 5; + _bottom_margin = _pixel_scale * 2; // Let's show the units on the guide bar labels. There's room. set_guide_bar_units(get_guide_bar_units() | GBU_show_units); @@ -103,7 +103,7 @@ set_time_units(int unit_mask) { * Called when the user single-clicks on a label. */ void WinStatsPianoRoll:: -clicked_label(int collector_index) { +on_click_label(int collector_index) { if (collector_index >= 0) { WinStatsGraph::_monitor->open_strip_chart(_thread_index, collector_index, false); } @@ -123,6 +123,15 @@ set_horizontal_scale(double time_width) { InvalidateRect(_window, &rect, TRUE); } +/** + * Calls update_guide_bars with parameters suitable to this kind of graph. + */ +void WinStatsPianoRoll:: +normal_guide_bars() { + // We want vaguely 100 pixels between guide bars. + update_guide_bars(get_xsize() / (_pixel_scale * 25), get_horizontal_scale()); +} + /** * Erases the chart area. */ @@ -144,6 +153,21 @@ begin_draw() { for (int i = 0; i < num_guide_bars; i++) { draw_guide_bar(_bitmap_dc, get_guide_bar(i)); } + + SelectObject(_bitmap_dc, GetStockObject(NULL_PEN)); +} + +/** + * Should be overridden by the user class. This hook will be called before + * drawing any one row of bars. These bars correspond to the collector whose + * index is get_row_collector(row), and in the color get_row_color(row). + */ +void WinStatsPianoRoll:: +begin_row(int row) { + int collector_index = get_label_collector(row); + HBRUSH brush = get_collector_brush(collector_index, _highlighted_index == collector_index); + SelectObject(_bitmap_dc, brush); + SelectObject(_bitmap_dc, GetStockObject(NULL_PEN)); } /** @@ -155,13 +179,7 @@ draw_bar(int row, int from_x, int to_x) { int y = _label_stack.get_label_y(row) - _graph_top; int height = _label_stack.get_label_height(row); - RECT rect = { - from_x, y - height + 2, - to_x, y - 2, - }; - int collector_index = get_label_collector(row); - HBRUSH brush = get_collector_brush(collector_index); - FillRect(_bitmap_dc, &rect, brush); + RoundRect(_bitmap_dc, from_x, y - height + 2, to_x, y - 2, _pixel_scale, _pixel_scale); } } @@ -233,7 +251,10 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { // When the mouse is over a color bar, highlight it. int16_t x = LOWORD(lparam); int16_t y = HIWORD(lparam); - _label_stack.highlight_label(get_collector_under_pixel(x, y)); + + int collector_index = get_collector_under_pixel(x, y); + _label_stack.highlight_label(collector_index); + on_enter_label(collector_index); // Now we want to get a WM_MOUSELEAVE when the mouse leaves the graph // window. @@ -244,10 +265,11 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { 0 }; TrackMouseEvent(&tme); - - } else { + } + else { // If the mouse is in some drag mode, stop highlighting. _label_stack.highlight_label(-1); + on_leave_label(_highlighted_index); } if (_drag_mode == DM_scale) { @@ -278,6 +300,7 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { case WM_MOUSELEAVE: // When the mouse leaves the graph, stop highlighting. _label_stack.highlight_label(-1); + on_leave_label(_highlighted_index); break; case WM_LBUTTONUP: @@ -305,7 +328,7 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { // clicking on the corresponding label. int16_t x = LOWORD(lparam); int16_t y = HIWORD(lparam); - clicked_label(get_collector_under_pixel(x, y)); + on_click_label(get_collector_under_pixel(x, y)); return 0; } break; @@ -325,12 +348,11 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { void WinStatsPianoRoll:: additional_window_paint(HDC hdc) { // Draw in the labels for the guide bars. - HFONT hfnt = (HFONT)GetStockObject(ANSI_VAR_FONT); - SelectObject(hdc, hfnt); + SelectObject(hdc, WinStatsGraph::_monitor->get_font()); SetTextAlign(hdc, TA_LEFT | TA_BOTTOM); SetBkMode(hdc, TRANSPARENT); - int y = _top_margin; + int y = _top_margin - 2; int i; int num_guide_bars = get_num_guide_bars(); @@ -411,13 +433,8 @@ get_collector_under_pixel(int xpoint, int ypoint) { */ void WinStatsPianoRoll:: update_labels() { - _label_stack.clear_labels(); - for (int i = 0; i < get_num_labels(); i++) { - int label_index = - _label_stack.add_label(WinStatsGraph::_monitor, this, - _thread_index, - get_label_collector(i), true); - } + _label_stack.replace_labels(WinStatsGraph::_monitor, this, + _thread_index, _labels, true); _labels_changed = false; } @@ -551,7 +568,7 @@ register_window_class(HINSTANCE application) { wc.lpfnWndProc = (WNDPROC)static_window_proc; wc.hInstance = application; wc.hCursor = LoadCursor(nullptr, IDC_ARROW); - wc.hbrBackground = (HBRUSH)COLOR_BACKGROUND; + wc.hbrBackground = (HBRUSH)COLOR_WINDOW; wc.lpszMenuName = nullptr; wc.lpszClassName = _window_class_name; diff --git a/pandatool/src/win-stats/winStatsPianoRoll.h b/pandatool/src/win-stats/winStatsPianoRoll.h index b130c99b18..5a5a6f1f45 100644 --- a/pandatool/src/win-stats/winStatsPianoRoll.h +++ b/pandatool/src/win-stats/winStatsPianoRoll.h @@ -41,12 +41,14 @@ public: virtual void changed_graph_size(int graph_xsize, int graph_ysize); virtual void set_time_units(int unit_mask); - virtual void clicked_label(int collector_index); + virtual void on_click_label(int collector_index); void set_horizontal_scale(double time_width); protected: + virtual void normal_guide_bars(); void clear_region(); virtual void begin_draw(); + virtual void begin_row(int row); virtual void draw_bar(int row, int from_x, int to_x); virtual void end_draw(); virtual void idle(); diff --git a/pandatool/src/win-stats/winStatsStripChart.cxx b/pandatool/src/win-stats/winStatsStripChart.cxx index 879b920673..a7cf195a63 100644 --- a/pandatool/src/win-stats/winStatsStripChart.cxx +++ b/pandatool/src/win-stats/winStatsStripChart.cxx @@ -16,17 +16,13 @@ #include "pStatCollectorDef.h" #include "numeric_types.h" +#include + using std::string; static const int default_strip_chart_width = 400; static const int default_strip_chart_height = 100; -// Surely we aren't expected to hardcode the size of a normal checkbox. But -// Windows seems to require this data to be passed to CreateWindow(), so what -// else can I do? -size_t WinStatsStripChart::_check_box_height = 13; -size_t WinStatsStripChart::_check_box_width = 13; - bool WinStatsStripChart::_window_class_registered = false; const char * const WinStatsStripChart::_window_class_name = "strip"; @@ -40,16 +36,16 @@ WinStatsStripChart(WinStatsMonitor *monitor, int thread_index, show_level ? monitor->get_level_view(collector_index, thread_index) : monitor->get_view(thread_index), thread_index, collector_index, - default_strip_chart_width, - default_strip_chart_height), + monitor->get_pixel_scale() * default_strip_chart_width / 4, + monitor->get_pixel_scale() * default_strip_chart_height / 4), WinStatsGraph(monitor) { _brush_origin = 0; - _left_margin = 96; - _right_margin = 32; - _top_margin = 16; - _bottom_margin = 8; + _left_margin = _pixel_scale * 24; + _right_margin = _pixel_scale * 12; + _top_margin = _pixel_scale * 6; + _bottom_margin = _pixel_scale * 2; if (show_level) { // If it's a level-type graph, show the appropriate units. @@ -170,7 +166,7 @@ set_scroll_speed(double scroll_speed) { * Called when the user single-clicks on a label. */ void WinStatsStripChart:: -clicked_label(int collector_index) { +on_click_label(int collector_index) { if (collector_index < 0) { // Clicking on whitespace in the graph is the same as clicking on the top // label. @@ -197,6 +193,15 @@ clicked_label(int collector_index) { } } +/** + * Called when the mouse hovers over a label, and should return the text that + * should appear on the tooltip. + */ +std::string WinStatsStripChart:: +get_label_tooltip(int collector_index) const { + return PStatStripChart::get_label_tooltip(collector_index); +} + /** * Changes the value the height of the vertical axis represents. This may * force a redraw. @@ -218,11 +223,8 @@ void WinStatsStripChart:: update_labels() { PStatStripChart::update_labels(); - _label_stack.clear_labels(); - for (int i = 0; i < get_num_labels(); i++) { - _label_stack.add_label(WinStatsGraph::_monitor, this, _thread_index, - get_label_collector(i), false); - } + _label_stack.replace_labels(WinStatsGraph::_monitor, this, + _thread_index, _labels, false); _labels_changed = false; } @@ -273,7 +275,7 @@ draw_slice(int x, int w, const PStatStripChart::FrameData &fdata) { for (fi = fdata.begin(); fi != fdata.end(); ++fi) { const ColorData &cd = (*fi); overall_time += cd._net_value; - HBRUSH brush = get_collector_brush(cd._collector_index); + HBRUSH brush = get_collector_brush(cd._collector_index, cd._collector_index == _highlighted_index); if (overall_time > get_vertical_scale()) { // Off the top. Go ahead and clamp it by hand, in case it's so far off @@ -391,7 +393,10 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { // When the mouse is over a color bar, highlight it. int16_t x = LOWORD(lparam); int16_t y = HIWORD(lparam); - _label_stack.highlight_label(get_collector_under_pixel(x, y)); + + int collector_index = get_collector_under_pixel(x, y); + _label_stack.highlight_label(collector_index); + on_enter_label(collector_index); // Now we want to get a WM_MOUSELEAVE when the mouse leaves the graph // window. @@ -402,10 +407,11 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { 0 }; TrackMouseEvent(&tme); - - } else { + } + else { // If the mouse is in some drag mode, stop highlighting. _label_stack.highlight_label(-1); + on_leave_label(_highlighted_index); } if (_drag_mode == DM_scale) { @@ -436,6 +442,7 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { case WM_MOUSELEAVE: // When the mouse leaves the graph, stop highlighting. _label_stack.highlight_label(-1); + on_leave_label(_highlighted_index); break; case WM_LBUTTONUP: @@ -463,7 +470,7 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { // clicking on the corresponding label. int16_t x = LOWORD(lparam); int16_t y = HIWORD(lparam); - clicked_label(get_collector_under_pixel(x, y)); + on_click_label(get_collector_under_pixel(x, y)); return 0; } break; @@ -483,14 +490,13 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) { void WinStatsStripChart:: additional_window_paint(HDC hdc) { // Draw in the labels for the guide bars. - HFONT hfnt = (HFONT)GetStockObject(ANSI_VAR_FONT); - SelectObject(hdc, hfnt); + SelectObject(hdc, WinStatsGraph::_monitor->get_font()); SetTextAlign(hdc, TA_LEFT | TA_TOP); SetBkMode(hdc, TRANSPARENT); RECT rect; GetClientRect(_window, &rect); - int x = rect.right - _right_margin + 2; + int x = rect.right - _right_margin + _pixel_scale; int last_y = -100; int i; @@ -511,15 +517,8 @@ additional_window_paint(HDC hdc) { // Now draw the "net value" label at the top. SetTextAlign(hdc, TA_RIGHT | TA_BOTTOM); SetTextColor(hdc, RGB(0, 0, 0)); - TextOut(hdc, rect.right - _right_margin, _top_margin, + TextOut(hdc, rect.right - _right_margin - _pixel_scale, _top_margin - _pixel_scale / 2, _net_value_text.data(), _net_value_text.length()); - - // Also draw the "Smooth" label on the check box. This isn't part of the - // check box itself, because doing that doesn't use the right font! Surely - // this isn't the correct Windows(tm) way to do this sort of thing, but I - // don't know any better for now. - SetTextAlign(hdc, TA_LEFT | TA_BOTTOM); - TextOut(hdc, _left_margin + _check_box_width + 2, _top_margin, "Smooth", 6); } /** @@ -594,10 +593,13 @@ void WinStatsStripChart:: move_graph_window(int graph_left, int graph_top, int graph_xsize, int graph_ysize) { WinStatsGraph::move_graph_window(graph_left, graph_top, graph_xsize, graph_ysize); if (_smooth_check_box != 0) { + SIZE size; + SendMessage(_smooth_check_box, BCM_GETIDEALSIZE, 0, (LPARAM)&size); + SetWindowPos(_smooth_check_box, 0, - _left_margin, _top_margin - _check_box_height - 1, - 0, 0, - SWP_NOZORDER | SWP_NOSIZE | SWP_SHOWWINDOW); + _left_margin, _top_margin - size.cy - _pixel_scale / 2, + size.cx, size.cy, + SWP_NOZORDER | SWP_SHOWWINDOW); InvalidateRect(_smooth_check_box, nullptr, TRUE); } } @@ -715,10 +717,15 @@ create_window() { setup_label_stack(); _smooth_check_box = - CreateWindow("BUTTON", "", - WS_CHILD | BS_AUTOCHECKBOX, - 0, 0, _check_box_width, _check_box_height, + CreateWindow(WC_BUTTON, "Smooth", WS_CHILD | BS_AUTOCHECKBOX, + 0, 0, 0, 0, _window, nullptr, application, 0); + SendMessage(_smooth_check_box, WM_SETFONT, + (WPARAM)WinStatsGraph::_monitor->get_font(), TRUE); + + if (get_average_mode()) { + SendMessage(_smooth_check_box, BM_SETCHECK, BST_CHECKED, 0); + } // Ensure that the window is on top of the stack. SetWindowPos(_window, HWND_TOP, 0, 0, 0, 0, @@ -742,7 +749,7 @@ register_window_class(HINSTANCE application) { wc.lpfnWndProc = (WNDPROC)static_window_proc; wc.hInstance = application; wc.hCursor = LoadCursor(nullptr, IDC_ARROW); - wc.hbrBackground = (HBRUSH)COLOR_BACKGROUND; + wc.hbrBackground = (HBRUSH)COLOR_WINDOW; wc.lpszMenuName = nullptr; wc.lpszClassName = _window_class_name; diff --git a/pandatool/src/win-stats/winStatsStripChart.h b/pandatool/src/win-stats/winStatsStripChart.h index 9e6f91667f..dcff22a76e 100644 --- a/pandatool/src/win-stats/winStatsStripChart.h +++ b/pandatool/src/win-stats/winStatsStripChart.h @@ -43,7 +43,8 @@ public: virtual void set_time_units(int unit_mask); virtual void set_scroll_speed(double scroll_speed); - virtual void clicked_label(int collector_index); + virtual void on_click_label(int collector_index); + virtual std::string get_label_tooltip(int collector_index) const; void set_vertical_scale(double value_height); protected: @@ -79,7 +80,6 @@ private: std::string _net_value_text; HWND _smooth_check_box; - static size_t _check_box_height, _check_box_width; static bool _window_class_registered; static const char * const _window_class_name; diff --git a/pandatool/src/win-stats/winstats_composite1.cxx b/pandatool/src/win-stats/winstats_composite1.cxx index 3eac02e787..cc90367573 100644 --- a/pandatool/src/win-stats/winstats_composite1.cxx +++ b/pandatool/src/win-stats/winstats_composite1.cxx @@ -1,5 +1,6 @@ #include "winStats.cxx" #include "winStatsChartMenu.cxx" +#include "winStatsFlameGraph.cxx" #include "winStatsGraph.cxx" #include "winStatsLabel.cxx" #include "winStatsLabelStack.cxx"