Browse Source

pstats: Another major update for PStats server UI, including:

- New powerful scrolling Timeline view for seeing all time events across all threads
- Redo flame graph to use stack-based nesting rather than the standard collector nesting
- Rewrite flame graph drawing to not use labels
- Status bar appears in main window showing top-level level collectors; double-clicking them brings up their chart and right-clicking them shows their children
- Context menus are added when right-clicking labels and charts
- Tooltips now appear when mouse hovers over collector area in a chart
- Strip chart windows now automatically determine the appropriate scale better
- Graph menus redone to allow opening flame chart anywhere as well as strip chart
- Instead of just ms everywhere, also use s / us / ns where appropriate
- Don't disable smoothing right away on mouse down on strip chart, only after dragging
- Windows: The MDI child windows are quite ugly and overlap with the status bar, so instead they are now top-level windows, but some code is added to make them spawn inside and move with the parent window, and minimize to its corner.  I can back this out if people prefer the old behavior despite the ugly decoration
- Windows: Label text shows ellipsis when cut off
- Windows: Graph windows no longer have icons
- Windows: Graph windows no longer spawn perfectly on top of each other, rather cascading
- GTK: Render at high resolution when GDK_SCALE is not 1
- GTK: Graph windows are forced to be floating in tiling WMs
- GTK: Flame chart window no longer has useless dividing bar
- GTK: Use more efficient cairo surface types
rdb 3 years ago
parent
commit
161ac4c2f7
53 changed files with 5303 additions and 734 deletions
  1. 1 0
      pandatool/src/gtk-stats/CMakeLists.txt
  2. 86 60
      pandatool/src/gtk-stats/gtkStatsChartMenu.cxx
  3. 0 1
      pandatool/src/gtk-stats/gtkStatsChartMenu.h
  4. 213 108
      pandatool/src/gtk-stats/gtkStatsFlameGraph.cxx
  5. 13 10
      pandatool/src/gtk-stats/gtkStatsFlameGraph.h
  6. 232 58
      pandatool/src/gtk-stats/gtkStatsGraph.cxx
  7. 43 16
      pandatool/src/gtk-stats/gtkStatsGraph.h
  8. 10 8
      pandatool/src/gtk-stats/gtkStatsLabel.cxx
  9. 7 3
      pandatool/src/gtk-stats/gtkStatsMonitor.I
  10. 227 7
      pandatool/src/gtk-stats/gtkStatsMonitor.cxx
  11. 27 2
      pandatool/src/gtk-stats/gtkStatsMonitor.h
  12. 130 43
      pandatool/src/gtk-stats/gtkStatsPianoRoll.cxx
  13. 9 5
      pandatool/src/gtk-stats/gtkStatsPianoRoll.h
  14. 147 63
      pandatool/src/gtk-stats/gtkStatsStripChart.cxx
  15. 9 4
      pandatool/src/gtk-stats/gtkStatsStripChart.h
  16. 812 0
      pandatool/src/gtk-stats/gtkStatsTimeline.cxx
  17. 91 0
      pandatool/src/gtk-stats/gtkStatsTimeline.h
  18. 1 0
      pandatool/src/gtk-stats/gtkstats_composite1.cxx
  19. 10 4
      pandatool/src/pstatserver/CMakeLists.txt
  20. 1 0
      pandatool/src/pstatserver/p3pstatserver_composite1.cxx
  21. 28 5
      pandatool/src/pstatserver/pStatFlameGraph.I
  22. 329 93
      pandatool/src/pstatserver/pStatFlameGraph.cxx
  23. 49 25
      pandatool/src/pstatserver/pStatFlameGraph.h
  24. 33 5
      pandatool/src/pstatserver/pStatGraph.cxx
  25. 1 0
      pandatool/src/pstatserver/pStatGraph.h
  26. 8 0
      pandatool/src/pstatserver/pStatPianoRoll.I
  27. 14 0
      pandatool/src/pstatserver/pStatPianoRoll.cxx
  28. 4 0
      pandatool/src/pstatserver/pStatPianoRoll.h
  29. 8 0
      pandatool/src/pstatserver/pStatStripChart.I
  30. 34 15
      pandatool/src/pstatserver/pStatStripChart.cxx
  31. 1 0
      pandatool/src/pstatserver/pStatStripChart.h
  32. 8 0
      pandatool/src/pstatserver/pStatThreadData.cxx
  33. 139 0
      pandatool/src/pstatserver/pStatTimeline.I
  34. 664 0
      pandatool/src/pstatserver/pStatTimeline.cxx
  35. 130 0
      pandatool/src/pstatserver/pStatTimeline.h
  36. 10 9
      pandatool/src/pstatserver/pStatView.cxx
  37. 2 0
      pandatool/src/win-stats/CMakeLists.txt
  38. 79 34
      pandatool/src/win-stats/winStatsChartMenu.cxx
  39. 176 84
      pandatool/src/win-stats/winStatsFlameGraph.cxx
  40. 7 6
      pandatool/src/win-stats/winStatsFlameGraph.h
  41. 134 3
      pandatool/src/win-stats/winStatsGraph.cxx
  42. 19 0
      pandatool/src/win-stats/winStatsGraph.h
  43. 18 4
      pandatool/src/win-stats/winStatsLabel.cxx
  44. 7 3
      pandatool/src/win-stats/winStatsMonitor.I
  45. 291 17
      pandatool/src/win-stats/winStatsMonitor.cxx
  46. 19 2
      pandatool/src/win-stats/winStatsMonitor.h
  47. 80 10
      pandatool/src/win-stats/winStatsPianoRoll.cxx
  48. 6 1
      pandatool/src/win-stats/winStatsPianoRoll.h
  49. 91 26
      pandatool/src/win-stats/winStatsStripChart.cxx
  50. 3 0
      pandatool/src/win-stats/winStatsStripChart.h
  51. 752 0
      pandatool/src/win-stats/winStatsTimeline.cxx
  52. 89 0
      pandatool/src/win-stats/winStatsTimeline.h
  53. 1 0
      pandatool/src/win-stats/winstats_composite1.cxx

+ 1 - 0
pandatool/src/gtk-stats/CMakeLists.txt

@@ -26,6 +26,7 @@ set(GTKSTATS_SOURCES
   gtkStatsPianoRoll.cxx
   gtkStatsPianoRoll.cxx
   gtkStatsServer.cxx
   gtkStatsServer.cxx
   gtkStatsStripChart.cxx
   gtkStatsStripChart.cxx
+  gtkStatsTimeline.cxx
 )
 )
 
 
 composite_sources(gtk-stats GTKSTATS_SOURCES)
 composite_sources(gtk-stats GTKSTATS_SOURCES)

+ 86 - 60
pandatool/src/gtk-stats/gtkStatsChartMenu.cxx

@@ -88,7 +88,25 @@ do_update() {
 
 
   // Now rebuild the menu with the new set of entries.
   // Now rebuild the menu with the new set of entries.
 
 
-  // The menu item(s) for the thread's frame time goes first.
+  if (_thread_index == 0) {
+    // Timeline goes first.
+    GtkStatsMonitor::MenuDef smd(_thread_index, -1, GtkStatsMonitor::CT_timeline, false);
+    const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd);
+
+    GtkWidget *menu_item = gtk_menu_item_new_with_label("Timeline");
+    gtk_widget_show(menu_item);
+    gtk_menu_shell_append(GTK_MENU_SHELL(_menu), menu_item);
+
+    g_signal_connect(G_OBJECT(menu_item), "activate",
+                     G_CALLBACK(GtkStatsMonitor::menu_activate),
+                     (void *)menu_def);
+
+    GtkWidget *sep = gtk_separator_menu_item_new();
+    gtk_widget_show(sep);
+    gtk_menu_shell_append(GTK_MENU_SHELL(_menu), sep);
+  }
+
+  // The menu item(s) for the thread's frame time goes second.
   add_view(_menu, view.get_top_level(), false);
   add_view(_menu, view.get_top_level(), false);
 
 
   bool needs_separator = true;
   bool needs_separator = true;
@@ -116,33 +134,22 @@ do_update() {
     }
     }
   }
   }
 
 
-  // Also menu items for flame graph and piano roll (following a separator).
+  // Also menu item for piano roll (following a separator).
   GtkWidget *sep = gtk_separator_menu_item_new();
   GtkWidget *sep = gtk_separator_menu_item_new();
   gtk_widget_show(sep);
   gtk_widget_show(sep);
   gtk_menu_shell_append(GTK_MENU_SHELL(_menu), sep);
   gtk_menu_shell_append(GTK_MENU_SHELL(_menu), sep);
 
 
   {
   {
-    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("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);
-  }
-
-  {
-    GtkStatsMonitor::MenuDef smd(_thread_index, -1, false);
+    GtkStatsMonitor::MenuDef smd(_thread_index, -1, GtkStatsMonitor::CT_piano_roll, false);
     const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd);
     const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd);
 
 
     GtkWidget *menu_item = gtk_menu_item_new_with_label("Piano Roll");
     GtkWidget *menu_item = gtk_menu_item_new_with_label("Piano Roll");
     gtk_widget_show(menu_item);
     gtk_widget_show(menu_item);
     gtk_menu_shell_append(GTK_MENU_SHELL(_menu), 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(G_OBJECT(menu_item), "activate",
+                     G_CALLBACK(GtkStatsMonitor::menu_activate),
+                     (void *)menu_def);
   }
   }
 }
 }
 
 
@@ -158,61 +165,80 @@ add_view(GtkWidget *parent_menu, const PStatViewLevel *view_level,
   const PStatClientData *client_data = _monitor->get_client_data();
   const PStatClientData *client_data = _monitor->get_client_data();
   std::string collector_name = client_data->get_collector_name(collector);
   std::string collector_name = client_data->get_collector_name(collector);
 
 
-  GtkStatsMonitor::MenuDef smd(_thread_index, collector, show_level);
-  const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd);
-
-  GtkWidget *menu_item = gtk_menu_item_new_with_label(collector_name.c_str());
-  gtk_widget_show(menu_item);
-  gtk_menu_shell_append(GTK_MENU_SHELL(parent_menu), menu_item);
+  int num_children = view_level->get_num_children();
+  if (show_level && num_children == 0) {
+    // For a level collector without children, no point in making a submenu.
+    GtkStatsMonitor::MenuDef smd(_thread_index, collector, GtkStatsMonitor::CT_strip_chart, show_level);
+    const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd);
 
 
-  g_signal_connect_swapped(G_OBJECT(menu_item), "activate",
-         G_CALLBACK(handle_menu), (void *)(const void *)menu_def);
+    GtkWidget *menu_item = gtk_menu_item_new_with_label(collector_name.c_str());
+    gtk_widget_show(menu_item);
+    gtk_menu_shell_append(GTK_MENU_SHELL(parent_menu), menu_item);
 
 
-  int num_children = view_level->get_num_children();
-  if (num_children > 1) {
-    // If the collector has more than one child, add a menu entry to go
-    // directly to each of its children.
-    std::string submenu_name = collector_name + " components";
+    g_signal_connect(G_OBJECT(menu_item), "activate",
+                     G_CALLBACK(GtkStatsMonitor::menu_activate),
+                     (void *)menu_def);
+    return;
+  }
 
 
-    GtkWidget *submenu_item = gtk_menu_item_new_with_label(submenu_name.c_str());
+  GtkWidget *menu;
+  if (!show_level && collector == 0 && num_children == 0) {
+    // Root collector without children, just add the options directly to the
+    // parent menu.
+    menu = parent_menu;
+  }
+  else {
+    // Create a submenu.
+    GtkWidget *submenu_item = gtk_menu_item_new_with_label(collector_name.c_str());
     gtk_widget_show(submenu_item);
     gtk_widget_show(submenu_item);
     gtk_menu_shell_append(GTK_MENU_SHELL(parent_menu), submenu_item);
     gtk_menu_shell_append(GTK_MENU_SHELL(parent_menu), submenu_item);
 
 
-    GtkWidget *submenu = gtk_menu_new();
-    gtk_widget_show(submenu);
-    gtk_menu_item_set_submenu(GTK_MENU_ITEM(submenu_item), submenu);
-
-    // Reverse the order since the menus are listed from the top down; we want
-    // to be visually consistent with the graphs, which list these labels from
-    // the bottom up.
-    for (int c = num_children - 1; c >= 0; c--) {
-      add_view(submenu, view_level->get_child(c), show_level);
-    }
+    menu = gtk_menu_new();
+    gtk_widget_show(menu);
+    gtk_menu_item_set_submenu(GTK_MENU_ITEM(submenu_item), menu);
   }
   }
-}
 
 
-/**
- * Callback when a menu item is selected.
- */
-void GtkStatsChartMenu::
-handle_menu(gpointer data) {
-  const GtkStatsMonitor::MenuDef *menu_def = (GtkStatsMonitor::MenuDef *)data;
-  GtkStatsMonitor *monitor = menu_def->_monitor;
+  {
+    GtkStatsMonitor::MenuDef smd(_thread_index, collector, GtkStatsMonitor::CT_strip_chart, show_level);
+    const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd);
 
 
-  if (monitor == nullptr) {
-    return;
-  }
+    GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Strip Chart");
+    gtk_widget_show(menu_item);
+    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
 
 
-  if (menu_def->_collector_index == -2) {
-    monitor->open_flame_graph(menu_def->_thread_index);
+    g_signal_connect(G_OBJECT(menu_item), "activate",
+                     G_CALLBACK(GtkStatsMonitor::menu_activate),
+                     (void *)menu_def);
   }
   }
-  else if (menu_def->_collector_index < 0) {
-    monitor->open_piano_roll(menu_def->_thread_index);
+
+  if (!show_level) {
+    if (collector == 0 && num_children == 0) {
+      collector = -1;
+    }
+
+    GtkStatsMonitor::MenuDef smd(_thread_index, collector, GtkStatsMonitor::CT_flame_graph, show_level);
+    const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd);
+
+    GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Flame Graph");
+    gtk_widget_show(menu_item);
+    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+
+    g_signal_connect(G_OBJECT(menu_item), "activate",
+                     G_CALLBACK(GtkStatsMonitor::menu_activate),
+                     (void *)menu_def);
   }
   }
-  else {
-    monitor->open_strip_chart(menu_def->_thread_index,
-            menu_def->_collector_index,
-            menu_def->_show_level);
+
+  if (num_children > 0) {
+    GtkWidget *sep = gtk_separator_menu_item_new();
+    gtk_widget_show(sep);
+    gtk_menu_shell_append(GTK_MENU_SHELL(menu), sep);
+
+    // Reverse the order since the menus are listed from the top down; we want
+    // to be visually consistent with the graphs, which list these labels from
+    // the bottom up.
+    for (int c = num_children - 1; c >= 0; c--) {
+      add_view(menu, view_level->get_child(c), show_level);
+    }
   }
   }
 }
 }
 
 

+ 0 - 1
pandatool/src/gtk-stats/gtkStatsChartMenu.h

@@ -40,7 +40,6 @@ private:
   void add_view(GtkWidget *parent_menu, const PStatViewLevel *view_level,
   void add_view(GtkWidget *parent_menu, const PStatViewLevel *view_level,
                 bool show_level);
                 bool show_level);
 
 
-  static void handle_menu(gpointer data);
   static void remove_menu_child(GtkWidget *widget, gpointer data);
   static void remove_menu_child(GtkWidget *widget, gpointer data);
 
 
   GtkStatsMonitor *_monitor;
   GtkStatsMonitor *_monitor;

+ 213 - 108
pandatool/src/gtk-stats/gtkStatsFlameGraph.cxx

@@ -25,11 +25,8 @@ static const int default_flame_graph_height = 150;
 GtkStatsFlameGraph::
 GtkStatsFlameGraph::
 GtkStatsFlameGraph(GtkStatsMonitor *monitor, int thread_index,
 GtkStatsFlameGraph(GtkStatsMonitor *monitor, int thread_index,
                    int collector_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)
+  PStatFlameGraph(monitor, thread_index, collector_index, 0, 0),
+  GtkStatsGraph(monitor, false)
 {
 {
   // Let's show the units on the guide bar labels.  There's room.
   // Let's show the units on the guide bar labels.  There's room.
   set_guide_bar_units(get_guide_bar_units() | GBU_show_units);
   set_guide_bar_units(get_guide_bar_units() | GBU_show_units);
@@ -54,13 +51,9 @@ GtkStatsFlameGraph(GtkStatsMonitor *monitor, int thread_index,
   gtk_box_pack_start(GTK_BOX(_top_hbox), _scale_area, TRUE, TRUE, 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_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_set_size_request(_graph_window,
+    default_flame_graph_width * monitor->get_resolution() / 96,
+    default_flame_graph_height * monitor->get_resolution() / 96);
 
 
   gtk_widget_show_all(_window);
   gtk_widget_show_all(_window);
   gtk_widget_show(_window);
   gtk_widget_show(_window);
@@ -71,6 +64,10 @@ GtkStatsFlameGraph(GtkStatsMonitor *monitor, int thread_index,
   gtk_widget_set_size_request(_window, 0, 0);
   gtk_widget_set_size_request(_window, 0, 0);
 
 
   clear_region();
   clear_region();
+
+  if (get_average_mode()) {
+    start_animation();
+  }
 }
 }
 
 
 /**
 /**
@@ -119,7 +116,9 @@ new_data(int thread_index, int frame_number) {
  */
  */
 void GtkStatsFlameGraph::
 void GtkStatsFlameGraph::
 force_redraw() {
 force_redraw() {
-  PStatFlameGraph::force_redraw();
+  if (_cr) {
+    PStatFlameGraph::force_redraw();
+  }
 }
 }
 
 
 /**
 /**
@@ -152,34 +151,7 @@ set_time_units(int unit_mask) {
  */
  */
 void GtkStatsFlameGraph::
 void GtkStatsFlameGraph::
 on_click_label(int collector_index) {
 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);
-    }
-  }
+  set_collector_index(collector_index);
 }
 }
 
 
 /**
 /**
@@ -189,6 +161,10 @@ void GtkStatsFlameGraph::
 on_enter_label(int collector_index) {
 on_enter_label(int collector_index) {
   if (collector_index != _highlighted_index) {
   if (collector_index != _highlighted_index) {
     _highlighted_index = collector_index;
     _highlighted_index = collector_index;
+
+    if (!get_average_mode()) {
+      PStatFlameGraph::force_redraw();
+    }
   }
   }
 }
 }
 
 
@@ -199,54 +175,11 @@ void GtkStatsFlameGraph::
 on_leave_label(int collector_index) {
 on_leave_label(int collector_index) {
   if (collector_index == _highlighted_index && collector_index != -1) {
   if (collector_index == _highlighted_index && collector_index != -1) {
     _highlighted_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;
+    if (!get_average_mode()) {
+      PStatFlameGraph::force_redraw();
     }
     }
-    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());
 }
 }
 
 
 /**
 /**
@@ -255,8 +188,7 @@ update_label(int collector_index, int row, int x, int width) {
 void GtkStatsFlameGraph::
 void GtkStatsFlameGraph::
 normal_guide_bars() {
 normal_guide_bars() {
   // We want vaguely 100 pixels between 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)));
+  int num_bars = get_xsize() / (_pixel_scale * 25);
 
 
   _guide_bars.clear();
   _guide_bars.clear();
 
 
@@ -292,6 +224,63 @@ begin_draw() {
   }
   }
 }
 }
 
 
+/**
+ * Should be overridden by the user class.  Should draw a single bar at the
+ * indicated location.
+ */
+void GtkStatsFlameGraph::
+draw_bar(int depth, int from_x, int to_x, int collector_index) {
+  double bottom = get_ysize() - depth * _pixel_scale * 5;
+  double top = bottom - _pixel_scale * 5;
+
+  bool is_highlighted = collector_index == _highlighted_index;
+  cairo_set_source(_cr, get_collector_pattern(collector_index, is_highlighted));
+
+  if (to_x < from_x + 3) {
+    // It's just a tiny sliver.  This is a more reliable way to draw it.
+    cairo_rectangle(_cr, from_x, top, to_x - from_x, bottom - top);
+    cairo_fill(_cr);
+  }
+  else {
+    double radius = std::min((double)_pixel_scale, (to_x - from_x) / 2.0);
+    cairo_new_sub_path(_cr);
+    cairo_arc(_cr, to_x - radius, top + radius, radius, -0.5 * M_PI, 0.0);
+    cairo_arc(_cr, to_x - radius, bottom - radius, radius, 0.0, 0.5 * M_PI);
+    cairo_arc(_cr, from_x + radius, bottom - radius, radius, 0.5 * M_PI, M_PI);
+    cairo_arc(_cr, from_x + radius, top + radius, radius, M_PI, 1.5 * M_PI);
+    cairo_close_path(_cr);
+    cairo_fill(_cr);
+
+    if ((to_x - from_x) >= _pixel_scale * 4) {
+      // Only bother drawing the text if we've got some space to draw on.
+      int left = std::max(from_x, 0) + _pixel_scale / 2;
+      int right = std::min(to_x, get_xsize()) - _pixel_scale / 2;
+
+      const PStatClientData *client_data = GtkStatsGraph::_monitor->get_client_data();
+      const std::string &name = client_data->get_collector_name(collector_index);
+
+      // Choose a suitable foreground color.
+      LRGBColor fg = get_collector_text_color(collector_index, is_highlighted);
+      cairo_set_source_rgb(_cr, fg[0], fg[1], fg[2]);
+
+      PangoLayout *layout = gtk_widget_create_pango_layout(_graph_window, name.c_str());
+      pango_layout_set_attributes(layout, _pango_attrs);
+      pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END);
+      pango_layout_set_width(layout, (right - left) * PANGO_SCALE);
+      pango_layout_set_height(layout, -1);
+
+      int width, height;
+      pango_layout_get_pixel_size(layout, &width, &height);
+
+      // Center the text vertically in the bar.
+      cairo_move_to(_cr, left, top + (bottom - top - height) / 2);
+      pango_cairo_show_layout(_cr, layout);
+
+      g_object_unref(layout);
+    }
+  }
+}
+
 /**
 /**
  * Called after all the bars have been drawn, this triggers a refresh event to
  * Called after all the bars have been drawn, this triggers a refresh event to
  * draw it to the window.
  * draw it to the window.
@@ -308,6 +297,15 @@ void GtkStatsFlameGraph::
 idle() {
 idle() {
 }
 }
 
 
+/**
+ * Overridden by a derived class to implement an animation.  If it returns
+ * false, the animation timer is stopped.
+ */
+bool GtkStatsFlameGraph::
+animate(double time, double dt) {
+  return PStatFlameGraph::animate(time, dt);
+}
+
 /**
 /**
  * This is called during the servicing of the draw 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.
  * class opportunity to do some further painting into the graph window.
@@ -320,6 +318,15 @@ additional_graph_window_paint(cairo_t *cr) {
   }
   }
 }
 }
 
 
+/**
+ * Called when the mouse hovers over the graph, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string GtkStatsFlameGraph::
+get_graph_tooltip(int mouse_x, int mouse_y) const {
+  return get_bar_tooltip(pixel_to_depth(mouse_y), mouse_x);
+}
+
 /**
 /**
  * Based on the mouse position within the window's client area, look for
  * Based on the mouse position within the window's client area, look for
  * draggable things the mouse might be hovering over and return the
  * draggable things the mouse might be hovering over and return the
@@ -352,12 +359,74 @@ consider_drag_start(int graph_x, int graph_y) {
  * Called when the mouse button is depressed within the graph window.
  * Called when the mouse button is depressed within the graph window.
  */
  */
 gboolean GtkStatsFlameGraph::
 gboolean GtkStatsFlameGraph::
-handle_button_press(GtkWidget *widget, int graph_x, int graph_y,
-        bool double_click) {
+handle_button_press(int graph_x, int graph_y, bool double_click, int button) {
   if (graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) {
   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());
+    int depth = pixel_to_depth(graph_y);
+    int collector_index = get_bar_collector(depth, graph_x);
+    if (button == 3) {
+      if (collector_index >= 0) {
+        GtkWidget *menu = gtk_menu_new();
+        _popup_index = collector_index;
+
+        std::string label = get_bar_tooltip(depth, graph_x);
+        if (!label.empty()) {
+          GtkWidget *menu_item = gtk_menu_item_new_with_label(label.c_str());
+          gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+          gtk_widget_set_sensitive(menu_item, FALSE);
+        }
+
+        {
+          GtkWidget *menu_item = gtk_menu_item_new_with_label("Set as Focus");
+          gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+
+          if (collector_index == 0 && get_collector_index() == 0) {
+            gtk_widget_set_sensitive(menu_item, FALSE);
+          } else {
+            g_signal_connect(G_OBJECT(menu_item), "activate",
+              G_CALLBACK(+[] (GtkWidget *widget, gpointer data) {
+                GtkStatsFlameGraph *self = (GtkStatsFlameGraph *)data;
+                self->set_collector_index(self->_popup_index);
+              }),
+              this);
+          }
+        }
+
+        {
+          const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({
+            get_thread_index(), collector_index,
+            GtkStatsMonitor::CT_strip_chart, false,
+          });
+
+          GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Strip Chart");
+          gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+          g_signal_connect(G_OBJECT(menu_item), "activate",
+                           G_CALLBACK(GtkStatsMonitor::menu_activate),
+                           (void *)menu_def);
+        }
+
+        {
+          const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({
+            get_thread_index(), collector_index,
+            GtkStatsMonitor::CT_flame_graph,
+          });
+
+          GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Flame Graph");
+          gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+          g_signal_connect(G_OBJECT(menu_item), "activate",
+                           G_CALLBACK(GtkStatsMonitor::menu_activate),
+                           (void *)menu_def);
+        }
+
+        gtk_widget_show_all(menu);
+        gtk_menu_popup_at_pointer(GTK_MENU(menu), nullptr);
+        return TRUE;
+      }
+      return FALSE;
+    }
+    else if (double_click && button == 1) {
+      // Double-clicking on a color bar in the graph will zoom the graph into
+      // that collector.
+      set_collector_index(collector_index);
       return TRUE;
       return TRUE;
     }
     }
   }
   }
@@ -375,21 +444,21 @@ handle_button_press(GtkWidget *widget, int graph_x, int graph_y,
     return TRUE;
     return TRUE;
   }
   }
 
 
-  return GtkStatsGraph::handle_button_press(widget, graph_x, graph_y,
-              double_click);
+  return GtkStatsGraph::handle_button_press(graph_x, graph_y,
+                                            double_click, button);
 }
 }
 
 
 /**
 /**
  * Called when the mouse button is released within the graph window.
  * Called when the mouse button is released within the graph window.
  */
  */
 gboolean GtkStatsFlameGraph::
 gboolean GtkStatsFlameGraph::
-handle_button_release(GtkWidget *widget, int graph_x, int graph_y) {
+handle_button_release(int graph_x, int graph_y) {
   if (_drag_mode == DM_scale) {
   if (_drag_mode == DM_scale) {
     set_drag_mode(DM_none);
     set_drag_mode(DM_none);
     // ReleaseCapture();
     // ReleaseCapture();
-    return handle_motion(widget, graph_x, graph_y);
-
-  } else if (_drag_mode == DM_guide_bar) {
+    return handle_motion(graph_x, graph_y);
+  }
+  else if (_drag_mode == DM_guide_bar) {
     if (graph_x < 0 || graph_x >= get_xsize()) {
     if (graph_x < 0 || graph_x >= get_xsize()) {
       remove_user_guide_bar(_drag_guide_bar);
       remove_user_guide_bar(_drag_guide_bar);
     } else {
     } else {
@@ -397,17 +466,30 @@ handle_button_release(GtkWidget *widget, int graph_x, int graph_y) {
     }
     }
     set_drag_mode(DM_none);
     set_drag_mode(DM_none);
     // ReleaseCapture();
     // ReleaseCapture();
-    return handle_motion(widget, graph_x, graph_y);
+    return handle_motion(graph_x, graph_y);
   }
   }
 
 
-  return GtkStatsGraph::handle_button_release(widget, graph_x, graph_y);
+  return GtkStatsGraph::handle_button_release(graph_x, graph_y);
 }
 }
 
 
 /**
 /**
  * Called when the mouse is moved within the graph window.
  * Called when the mouse is moved within the graph window.
  */
  */
 gboolean GtkStatsFlameGraph::
 gboolean GtkStatsFlameGraph::
-handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
+handle_motion(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.
+    int depth = pixel_to_depth(graph_y);
+    int collector_index = get_bar_collector(depth, graph_x);
+    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_new_guide_bar) {
   if (_drag_mode == DM_new_guide_bar) {
     // We haven't created the new guide bar yet; we won't until the mouse
     // We haven't created the new guide bar yet; we won't until the mouse
     // comes within the graph's region.
     // comes within the graph's region.
@@ -422,7 +504,25 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
     return TRUE;
     return TRUE;
   }
   }
 
 
-  return GtkStatsGraph::handle_motion(widget, graph_x, graph_y);
+  return GtkStatsGraph::handle_motion(graph_x, graph_y);
+}
+
+/**
+ * Called when the mouse has left the graph window.
+ */
+gboolean GtkStatsFlameGraph::
+handle_leave() {
+  _label_stack.highlight_label(-1);
+  on_leave_label(_highlighted_index);
+  return TRUE;
+}
+
+/**
+ * Converts a pixel to a depth index.
+ */
+int GtkStatsFlameGraph::
+pixel_to_depth(int y) const {
+  return (get_ysize() - 1 - y) / (_pixel_scale * 5);
 }
 }
 
 
 /**
 /**
@@ -443,7 +543,7 @@ draw_guide_bar(cairo_t *cr, const PStatGraph::GuideBar &bar) {
       cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]);
       cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]);
       break;
       break;
 
 
-    case GBS_normal:
+    default:
       cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]);
       cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]);
       break;
       break;
     }
     }
@@ -484,7 +584,7 @@ draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar) {
     cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]);
     cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]);
     break;
     break;
 
 
-  case GBS_normal:
+  default:
     cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]);
     cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]);
     break;
     break;
   }
   }
@@ -492,13 +592,13 @@ draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar) {
   int x = height_to_pixel(bar._height);
   int x = height_to_pixel(bar._height);
   const std::string &label = bar._label;
   const std::string &label = bar._label;
 
 
-  PangoLayout *layout = gtk_widget_create_pango_layout(_window, label.c_str());
+  PangoLayout *layout = gtk_widget_create_pango_layout(_scale_area, label.c_str());
   int width, height;
   int width, height;
   pango_layout_get_pixel_size(layout, &width, &height);
   pango_layout_get_pixel_size(layout, &width, &height);
 
 
   if (bar._style != GBS_user) {
   if (bar._style != GBS_user) {
-    double from_height = pixel_to_height(x - width);
-    double to_height = pixel_to_height(x + width);
+    double from_height = pixel_to_height(x - width * _cr_scale);
+    double to_height = pixel_to_height(x + width * _cr_scale);
     if (find_user_guide_bar(from_height, to_height) >= 0) {
     if (find_user_guide_bar(from_height, to_height) >= 0) {
       // Omit the label: there's a user-defined guide bar in the same space.
       // Omit the label: there's a user-defined guide bar in the same space.
       g_object_unref(layout);
       g_object_unref(layout);
@@ -510,6 +610,8 @@ draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar) {
     // Now convert our x to a coordinate within our drawing area.
     // Now convert our x to a coordinate within our drawing area.
     int junk_y;
     int junk_y;
 
 
+    x /= _cr_scale;
+
     // The x coordinate comes from the graph_window.
     // The x coordinate comes from the graph_window.
     gtk_widget_translate_coordinates(_graph_window, _scale_area,
     gtk_widget_translate_coordinates(_graph_window, _scale_area,
              x, 0,
              x, 0,
@@ -537,6 +639,9 @@ toggled_callback(GtkToggleButton *button, gpointer data) {
 
 
   bool active = gtk_toggle_button_get_active(button);
   bool active = gtk_toggle_button_get_active(button);
   self->set_average_mode(active);
   self->set_average_mode(active);
+  if (active) {
+    self->start_animation();
+  }
 }
 }
 
 
 /**
 /**

+ 13 - 10
pandatool/src/gtk-stats/gtkStatsFlameGraph.h

@@ -28,7 +28,7 @@ class GtkStatsLabel;
 class GtkStatsFlameGraph : public PStatFlameGraph, public GtkStatsGraph {
 class GtkStatsFlameGraph : public PStatFlameGraph, public GtkStatsGraph {
 public:
 public:
   GtkStatsFlameGraph(GtkStatsMonitor *monitor, int thread_index,
   GtkStatsFlameGraph(GtkStatsMonitor *monitor, int thread_index,
-                     int collector_index=0);
+                     int collector_index=-1);
   virtual ~GtkStatsFlameGraph();
   virtual ~GtkStatsFlameGraph();
 
 
   virtual void new_collector(int collector_index);
   virtual void new_collector(int collector_index);
@@ -40,27 +40,30 @@ public:
   virtual void on_click_label(int collector_index);
   virtual void on_click_label(int collector_index);
   virtual void on_enter_label(int collector_index);
   virtual void on_enter_label(int collector_index);
   virtual void on_leave_label(int collector_index);
   virtual void on_leave_label(int collector_index);
-  virtual std::string get_label_tooltip(int collector_index) const;
 
 
 protected:
 protected:
-  virtual void update_labels();
-  virtual void update_label(int collector_index, int row, int x, int width);
   virtual void normal_guide_bars();
   virtual void normal_guide_bars();
 
 
   void clear_region();
   void clear_region();
   virtual void begin_draw();
   virtual void begin_draw();
+  virtual void draw_bar(int depth, int from_x, int to_x, int collector_index);
   virtual void end_draw();
   virtual void end_draw();
   virtual void idle();
   virtual void idle();
 
 
+  virtual bool animate(double time, double dt);
+
   virtual void additional_graph_window_paint(cairo_t *cr);
   virtual void additional_graph_window_paint(cairo_t *cr);
+  virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const;
   virtual DragMode consider_drag_start(int graph_x, int graph_y);
   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);
+  virtual gboolean handle_button_press(int graph_x, int graph_y,
+                                       bool double_click, int button);
+  virtual gboolean handle_button_release(int graph_x, int graph_y);
+  virtual gboolean handle_motion(int graph_x, int graph_y);
+  virtual gboolean handle_leave();
 
 
 private:
 private:
+  int pixel_to_depth(int y) const;
   void draw_guide_bar(cairo_t *cr, const PStatGraph::GuideBar &bar);
   void draw_guide_bar(cairo_t *cr, const PStatGraph::GuideBar &bar);
   void draw_guide_labels(cairo_t *cr);
   void draw_guide_labels(cairo_t *cr);
   void draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar);
   void draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar);
@@ -70,12 +73,12 @@ private:
 
 
 private:
 private:
   std::string _net_value_text;
   std::string _net_value_text;
-  pmap<int, GtkStatsLabel *> _labels;
 
 
   GtkWidget *_top_hbox;
   GtkWidget *_top_hbox;
   GtkWidget *_average_check_box;
   GtkWidget *_average_check_box;
   GtkWidget *_total_label;
   GtkWidget *_total_label;
-  GtkWidget *_fixed;
+
+  int _popup_index = -1;
 };
 };
 
 
 #endif
 #endif

+ 232 - 58
pandatool/src/gtk-stats/gtkStatsGraph.cxx

@@ -30,7 +30,7 @@ const double GtkStatsGraph::rgb_user_guide_bar[3] = {
  *
  *
  */
  */
 GtkStatsGraph::
 GtkStatsGraph::
-GtkStatsGraph(GtkStatsMonitor *monitor) :
+GtkStatsGraph(GtkStatsMonitor *monitor, bool has_label_stack) :
   _monitor(monitor)
   _monitor(monitor)
 {
 {
   _parent_window = nullptr;
   _parent_window = nullptr;
@@ -40,21 +40,25 @@ GtkStatsGraph(GtkStatsMonitor *monitor) :
 
 
   GtkWidget *parent_window = monitor->get_window();
   GtkWidget *parent_window = monitor->get_window();
 
 
-  GdkDisplay *display = gdk_window_get_display(gtk_widget_get_window(parent_window));
+  GdkWindow *window = gtk_widget_get_window(parent_window);
+  GdkDisplay *display = gdk_window_get_display(window);
   _hand_cursor = gdk_cursor_new_for_display(display, GDK_HAND2);
   _hand_cursor = gdk_cursor_new_for_display(display, GDK_HAND2);
 
 
+  int scale = gdk_window_get_scale_factor(window);
+  _pixel_scale = scale * monitor->get_resolution() * 4 / 96;
+
   _cr_surface = nullptr;
   _cr_surface = nullptr;
   _cr = nullptr;
   _cr = nullptr;
+  _cr_scale = scale;
+  _pango_attrs = nullptr;
 
 
   _surface_xsize = 0;
   _surface_xsize = 0;
   _surface_ysize = 0;
   _surface_ysize = 0;
 
 
   _window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
   _window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
-
-  // These calls were intended to kind of emulate the Windows MDI behavior,
-  // but it's just weird.  gtk_window_set_transient_for(GTK_WINDOW(_window),
-  // GTK_WINDOW(parent_window));
-  // gtk_window_set_destroy_with_parent(GTK_WINDOW(_window), TRUE);
+  gtk_window_set_type_hint(GTK_WINDOW(_window), GDK_WINDOW_TYPE_HINT_UTILITY);
+  //gtk_window_set_transient_for(GTK_WINDOW(_window), GTK_WINDOW(parent_window));
+  //gtk_window_set_position(GTK_WINDOW(_window), GTK_WIN_POS_CENTER_ON_PARENT);
 
 
   gtk_widget_add_events(_window,
   gtk_widget_add_events(_window,
       GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
       GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
@@ -63,17 +67,17 @@ GtkStatsGraph(GtkStatsMonitor *monitor) :
        G_CALLBACK(window_delete_event), this);
        G_CALLBACK(window_delete_event), this);
   g_signal_connect(G_OBJECT(_window), "destroy",
   g_signal_connect(G_OBJECT(_window), "destroy",
        G_CALLBACK(window_destroy), this);
        G_CALLBACK(window_destroy), this);
-  g_signal_connect(G_OBJECT(_window), "button_press_event",
-       G_CALLBACK(button_press_event_callback), this);
-  g_signal_connect(G_OBJECT(_window), "button_release_event",
-       G_CALLBACK(button_release_event_callback), this);
-  g_signal_connect(G_OBJECT(_window), "motion_notify_event",
-       G_CALLBACK(motion_notify_event_callback), this);
+  //g_signal_connect(G_OBJECT(_window), "button_press_event",
+  //     G_CALLBACK(button_press_event_callback), this);
+  //g_signal_connect(G_OBJECT(_window), "button_release_event",
+  //     G_CALLBACK(button_release_event_callback), this);
+  //g_signal_connect(G_OBJECT(_window), "motion_notify_event",
+  //     G_CALLBACK(motion_notify_event_callback), this);
 
 
   _graph_window = gtk_drawing_area_new();
   _graph_window = gtk_drawing_area_new();
   gtk_widget_add_events(_graph_window,
   gtk_widget_add_events(_graph_window,
       GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
       GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
-      GDK_POINTER_MOTION_MASK);
+      GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
   g_signal_connect(G_OBJECT(_graph_window), "draw",
   g_signal_connect(G_OBJECT(_graph_window), "draw",
        G_CALLBACK(graph_draw_callback), this);
        G_CALLBACK(graph_draw_callback), this);
   g_signal_connect(G_OBJECT(_graph_window), "configure_event",
   g_signal_connect(G_OBJECT(_graph_window), "configure_event",
@@ -84,37 +88,41 @@ GtkStatsGraph(GtkStatsMonitor *monitor) :
        G_CALLBACK(button_release_event_callback), this);
        G_CALLBACK(button_release_event_callback), this);
   g_signal_connect(G_OBJECT(_graph_window), "motion_notify_event",
   g_signal_connect(G_OBJECT(_graph_window), "motion_notify_event",
        G_CALLBACK(motion_notify_event_callback), this);
        G_CALLBACK(motion_notify_event_callback), this);
+  g_signal_connect(G_OBJECT(_graph_window), "leave_notify_event",
+       G_CALLBACK(leave_notify_event_callback), this);
+  g_signal_connect(G_OBJECT(_graph_window), "query-tooltip",
+       G_CALLBACK(query_tooltip_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);
+  gtk_widget_set_has_tooltip(_graph_window, TRUE);
 
 
   // A Frame to hold the graph.
   // A Frame to hold the graph.
   _graph_frame = gtk_frame_new(nullptr);
   _graph_frame = gtk_frame_new(nullptr);
   gtk_frame_set_shadow_type(GTK_FRAME(_graph_frame), GTK_SHADOW_IN);
   gtk_frame_set_shadow_type(GTK_FRAME(_graph_frame), GTK_SHADOW_IN);
-  gtk_container_add(GTK_CONTAINER(_graph_frame), _graph_overlay);
+  gtk_container_add(GTK_CONTAINER(_graph_frame), _graph_window);
 
 
   // A VBox to hold the graph's frame, and any numbers (scale legend?  total?)
   // A VBox to hold the graph's frame, and any numbers (scale legend?  total?)
   // above it.
   // above it.
   _graph_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
   _graph_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
-  gtk_box_pack_end(GTK_BOX(_graph_vbox), _graph_frame,
-       TRUE, TRUE, 0);
+  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
   // An HBox to hold the graph's frame, and the scale legend to the right of
   // it.
   // it.
   _graph_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
   _graph_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
-  gtk_box_pack_start(GTK_BOX(_graph_hbox), _graph_vbox,
-         TRUE, TRUE, 0);
+  gtk_box_pack_start(GTK_BOX(_graph_hbox), _graph_vbox, TRUE, TRUE, 0);
 
 
   // An HPaned to hold the label stack and the graph hbox.
   // 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(), FALSE, FALSE);
-  gtk_paned_pack2(GTK_PANED(_hpaned), _graph_hbox, TRUE, TRUE);
+  if (has_label_stack) {
+    _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(), FALSE, FALSE);
+    gtk_paned_pack2(GTK_PANED(_hpaned), _graph_hbox, TRUE, TRUE);
+  }
+  else {
+    gtk_container_add(GTK_CONTAINER(_window), _graph_hbox);
+  }
 
 
   _drag_mode = DM_none;
   _drag_mode = DM_none;
   _potential_drag_mode = DM_none;
   _potential_drag_mode = DM_none;
@@ -128,6 +136,11 @@ GtkStatsGraph(GtkStatsMonitor *monitor) :
  */
  */
 GtkStatsGraph::
 GtkStatsGraph::
 ~GtkStatsGraph() {
 ~GtkStatsGraph() {
+  if (_timer_id != 0) {
+    gtk_widget_remove_tick_callback(_graph_window, _timer_id);
+    _timer_id = 0;
+  }
+
   _monitor = nullptr;
   _monitor = nullptr;
   release_surface();
   release_surface();
 
 
@@ -136,9 +149,15 @@ GtkStatsGraph::
     cairo_pattern_destroy(item.second.second);
     cairo_pattern_destroy(item.second.second);
   }
   }
   _brushes.clear();
   _brushes.clear();
+  _text_colors.clear();
 
 
   _label_stack.clear_labels();
   _label_stack.clear_labels();
 
 
+  if (_pango_attrs != nullptr) {
+    pango_attr_list_unref(_pango_attrs);
+    _pango_attrs = nullptr;
+  }
+
   if (_window != nullptr) {
   if (_window != nullptr) {
     GtkWidget *window = _window;
     GtkWidget *window = _window;
     _window = nullptr;
     _window = nullptr;
@@ -211,6 +230,13 @@ void GtkStatsGraph::
 on_click_label(int collector_index) {
 on_click_label(int collector_index) {
 }
 }
 
 
+/**
+ * Called when a pop-up menu should be shown for the label.
+ */
+void GtkStatsGraph::
+on_popup_label(int collector_index) {
+}
+
 /**
 /**
  * Called when the user hovers the mouse over a label.
  * Called when the user hovers the mouse over a label.
  */
  */
@@ -260,6 +286,29 @@ close() {
   }
   }
 }
 }
 
 
+/**
+ * Turns on the animation timer, if it hasn't already been turned on.
+ */
+void GtkStatsGraph::
+start_animation() {
+  if (_timer_id != 0) {
+    return;
+  }
+
+  _time = 0;
+  _timer_id = gtk_widget_add_tick_callback(_graph_window, tick_callback,
+                                           this, nullptr);
+}
+
+/**
+ * Overridden by a derived class to implement an animation.  If it returns
+ * false, the animation timer is stopped.
+ */
+bool GtkStatsGraph::
+animate(double time, double dt) {
+  return false;
+}
+
 /**
 /**
  * Returns a pattern suitable for drawing in the indicated collector's color.
  * Returns a pattern suitable for drawing in the indicated collector's color.
  */
  */
@@ -286,6 +335,29 @@ get_collector_pattern(int collector_index, bool highlight) {
   return highlight ? hpattern : pattern;
   return highlight ? hpattern : pattern;
 }
 }
 
 
+/**
+ * Returns a text color suitable for the given collector.
+ */
+LRGBColor GtkStatsGraph::
+get_collector_text_color(int collector_index, bool highlight) {
+  TextColors::iterator tci;
+  tci = _text_colors.find(collector_index);
+  if (tci != _text_colors.end()) {
+    return highlight ? (*tci).second.second : (*tci).second.first;
+  }
+
+  LRGBColor rgb = _monitor->get_collector_color(collector_index);
+  double bright =
+    rgb[0] * 0.2126 +
+    rgb[1] * 0.7152 +
+    rgb[2] * 0.0722;
+  LRGBColor color = bright >= 0.5 ? LRGBColor(0) : LRGBColor(1);
+  LRGBColor hcolor = bright * 0.75 >= 0.5 ? LRGBColor(0) : LRGBColor(1);
+
+  _text_colors[collector_index] = std::make_pair(color, hcolor);
+  return highlight ? hcolor : color;
+}
+
 /**
 /**
  * This is called during the servicing of the draw 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.
  * class opportunity to do some further painting into the graph window.
@@ -294,6 +366,15 @@ void GtkStatsGraph::
 additional_graph_window_paint(cairo_t *cr) {
 additional_graph_window_paint(cairo_t *cr) {
 }
 }
 
 
+/**
+ * Called when the mouse hovers over the graph, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string GtkStatsGraph::
+get_graph_tooltip(int mouse_x, int mouse_y) const {
+  return std::string();
+}
+
 /**
 /**
  * Based on the mouse position within the graph window, look for draggable
  * Based on the mouse position within the graph window, look for draggable
  * things the mouse might be hovering over and return the appropriate DragMode
  * things the mouse might be hovering over and return the appropriate DragMode
@@ -318,9 +399,8 @@ set_drag_mode(GtkStatsGraph::DragMode drag_mode) {
  * window.
  * window.
  */
  */
 gboolean GtkStatsGraph::
 gboolean GtkStatsGraph::
-handle_button_press(GtkWidget *widget, int graph_x, int graph_y,
-        bool double_click) {
-  if (_potential_drag_mode != DM_none) {
+handle_button_press(int graph_x, int graph_y, bool double_click, int button) {
+  if (_potential_drag_mode != DM_none && button == 1) {
     set_drag_mode(_potential_drag_mode);
     set_drag_mode(_potential_drag_mode);
     _drag_start_x = graph_x;
     _drag_start_x = graph_x;
     _drag_start_y = graph_y;
     _drag_start_y = graph_y;
@@ -334,18 +414,18 @@ handle_button_press(GtkWidget *widget, int graph_x, int graph_y,
  * window.
  * window.
  */
  */
 gboolean GtkStatsGraph::
 gboolean GtkStatsGraph::
-handle_button_release(GtkWidget *widget, int graph_x, int graph_y) {
+handle_button_release(int graph_x, int graph_y) {
   set_drag_mode(DM_none);
   set_drag_mode(DM_none);
   // ReleaseCapture();
   // ReleaseCapture();
 
 
-  return handle_motion(widget, graph_x, graph_y);
+  return handle_motion(graph_x, graph_y);
 }
 }
 
 
 /**
 /**
  * Called when the mouse is moved within the window, or any nested window.
  * Called when the mouse is moved within the window, or any nested window.
  */
  */
 gboolean GtkStatsGraph::
 gboolean GtkStatsGraph::
-handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
+handle_motion(int graph_x, int graph_y) {
   _potential_drag_mode = consider_drag_start(graph_x, graph_y);
   _potential_drag_mode = consider_drag_start(graph_x, graph_y);
 
 
   GdkWindow *window = gtk_widget_get_window(_window);
   GdkWindow *window = gtk_widget_get_window(_window);
@@ -353,29 +433,47 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
   if (_potential_drag_mode == DM_guide_bar ||
   if (_potential_drag_mode == DM_guide_bar ||
       _drag_mode == DM_guide_bar) {
       _drag_mode == DM_guide_bar) {
     gdk_window_set_cursor(window, _hand_cursor);
     gdk_window_set_cursor(window, _hand_cursor);
-
-  } else {
+  }
+  else {
     gdk_window_set_cursor(window, nullptr);
     gdk_window_set_cursor(window, nullptr);
   }
   }
 
 
   return TRUE;
   return TRUE;
 }
 }
 
 
+/**
+ * Called when the mouse has left the graph window.
+ */
+gboolean GtkStatsGraph::
+handle_leave() {
+  return FALSE;
+}
+
 /**
 /**
  * Sets up a backing-store bitmap of the indicated size.
  * Sets up a backing-store bitmap of the indicated size.
  */
  */
 void GtkStatsGraph::
 void GtkStatsGraph::
-setup_surface(int xsize, int ysize) {
+setup_surface(int xsize, int ysize, int scale) {
   release_surface();
   release_surface();
 
 
-  _surface_xsize = std::max(xsize, 0);
-  _surface_ysize = std::max(ysize, 0);
+  _surface_xsize = xsize;
+  _surface_ysize = ysize;
+  _pixel_scale = scale * _monitor->get_resolution() * 4 / 96;
 
 
-  _cr_surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, _surface_xsize, _surface_ysize);
+  GdkWindow *window = gtk_widget_get_window(_graph_window);
+  _cr_surface = gdk_window_create_similar_image_surface(window, CAIRO_FORMAT_RGB24, _surface_xsize, _surface_ysize, 1);
   _cr = cairo_create(_cr_surface);
   _cr = cairo_create(_cr_surface);
+  _cr_scale = scale;
 
 
   cairo_set_source_rgb(_cr, 1.0, 1.0, 1.0);
   cairo_set_source_rgb(_cr, 1.0, 1.0, 1.0);
   cairo_paint(_cr);
   cairo_paint(_cr);
+
+  // Cache the font scale attribute.
+  _pango_attrs = pango_attr_list_new();
+  PangoAttribute *attr = pango_attr_scale_new(scale * 0.9);
+  attr->start_index = 0;
+  attr->end_index = -1;
+  pango_attr_list_insert(_pango_attrs, attr);
 }
 }
 
 
 /**
 /**
@@ -386,6 +484,13 @@ release_surface() {
   if (_cr_surface != nullptr) {
   if (_cr_surface != nullptr) {
     cairo_surface_destroy(_cr_surface);
     cairo_surface_destroy(_cr_surface);
     cairo_destroy(_cr);
     cairo_destroy(_cr);
+    _cr_surface = nullptr;
+    _cr = nullptr;
+  }
+
+  if (_pango_attrs != nullptr) {
+    pango_attr_list_unref(_pango_attrs);
+    _pango_attrs = nullptr;
   }
   }
 }
 }
 
 
@@ -416,6 +521,8 @@ graph_draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data) {
   GtkStatsGraph *self = (GtkStatsGraph *)data;
   GtkStatsGraph *self = (GtkStatsGraph *)data;
 
 
   if (self->_cr_surface != nullptr) {
   if (self->_cr_surface != nullptr) {
+    double scale = 1.0 / self->_cr_scale;
+    cairo_scale(cr, scale, scale);
     cairo_set_source_surface(cr, self->_cr_surface, 0, 0);
     cairo_set_source_surface(cr, self->_cr_surface, 0, 0);
     cairo_paint(cr);
     cairo_paint(cr);
   }
   }
@@ -430,12 +537,21 @@ graph_draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data) {
  */
  */
 gboolean GtkStatsGraph::
 gboolean GtkStatsGraph::
 configure_graph_callback(GtkWidget *widget, GdkEventConfigure *event,
 configure_graph_callback(GtkWidget *widget, GdkEventConfigure *event,
-       gpointer data) {
+                         gpointer data) {
   GtkStatsGraph *self = (GtkStatsGraph *)data;
   GtkStatsGraph *self = (GtkStatsGraph *)data;
 
 
-  self->changed_graph_size(event->width, event->height);
-  self->setup_surface(event->width, event->height);
-  self->force_redraw();
+  GdkWindow *window = gtk_widget_get_window(widget);
+  int scale = gdk_window_get_scale_factor(window);
+  int scaled_xsize = std::max(event->width * scale, 0);
+  int scaled_ysize = std::max(event->height * scale, 0);
+
+  if (self->_cr == nullptr ||
+      self->_cr_scale != scale ||
+      self->_surface_xsize != scaled_xsize ||
+      self->_surface_ysize != scaled_ysize) {
+    self->setup_surface(scaled_xsize, scaled_ysize, scale);
+    self->changed_graph_size(scaled_xsize, scaled_ysize);
+  }
 
 
   return TRUE;
   return TRUE;
 }
 }
@@ -446,16 +562,19 @@ configure_graph_callback(GtkWidget *widget, GdkEventConfigure *event,
  */
  */
 gboolean GtkStatsGraph::
 gboolean GtkStatsGraph::
 button_press_event_callback(GtkWidget *widget, GdkEventButton *event,
 button_press_event_callback(GtkWidget *widget, GdkEventButton *event,
-          gpointer data) {
+                            gpointer data) {
   GtkStatsGraph *self = (GtkStatsGraph *)data;
   GtkStatsGraph *self = (GtkStatsGraph *)data;
   int graph_x, graph_y;
   int graph_x, graph_y;
   gtk_widget_translate_coordinates(widget, self->_graph_window,
   gtk_widget_translate_coordinates(widget, self->_graph_window,
-           (int)event->x, (int)event->y,
-           &graph_x, &graph_y);
+                                   (int)event->x, (int)event->y,
+                                   &graph_x, &graph_y);
+  graph_x *= self->_cr_scale;
+  graph_y *= self->_cr_scale;
 
 
   bool double_click = (event->type == GDK_2BUTTON_PRESS);
   bool double_click = (event->type == GDK_2BUTTON_PRESS);
 
 
-  return self->handle_button_press(widget, graph_x, graph_y, double_click);
+  return self->handle_button_press(graph_x, graph_y,
+                                   double_click, event->button);
 }
 }
 
 
 /**
 /**
@@ -464,14 +583,16 @@ button_press_event_callback(GtkWidget *widget, GdkEventButton *event,
  */
  */
 gboolean GtkStatsGraph::
 gboolean GtkStatsGraph::
 button_release_event_callback(GtkWidget *widget, GdkEventButton *event,
 button_release_event_callback(GtkWidget *widget, GdkEventButton *event,
-            gpointer data) {
+                              gpointer data) {
   GtkStatsGraph *self = (GtkStatsGraph *)data;
   GtkStatsGraph *self = (GtkStatsGraph *)data;
   int graph_x, graph_y;
   int graph_x, graph_y;
   gtk_widget_translate_coordinates(widget, self->_graph_window,
   gtk_widget_translate_coordinates(widget, self->_graph_window,
-           (int)event->x, (int)event->y,
-           &graph_x, &graph_y);
+                                   (int)event->x, (int)event->y,
+                                   &graph_x, &graph_y);
+  graph_x *= self->_cr_scale;
+  graph_y *= self->_cr_scale;
 
 
-  return self->handle_button_release(widget, graph_x, graph_y);
+  return self->handle_button_release(graph_x, graph_y);
 }
 }
 
 
 /**
 /**
@@ -479,12 +600,65 @@ button_release_event_callback(GtkWidget *widget, GdkEventButton *event,
  */
  */
 gboolean GtkStatsGraph::
 gboolean GtkStatsGraph::
 motion_notify_event_callback(GtkWidget *widget, GdkEventMotion *event,
 motion_notify_event_callback(GtkWidget *widget, GdkEventMotion *event,
-           gpointer data) {
+                             gpointer data) {
   GtkStatsGraph *self = (GtkStatsGraph *)data;
   GtkStatsGraph *self = (GtkStatsGraph *)data;
   int graph_x, graph_y;
   int graph_x, graph_y;
   gtk_widget_translate_coordinates(widget, self->_graph_window,
   gtk_widget_translate_coordinates(widget, self->_graph_window,
-           (int)event->x, (int)event->y,
-           &graph_x, &graph_y);
+                                   (int)event->x, (int)event->y,
+                                   &graph_x, &graph_y);
+  graph_x *= self->_cr_scale;
+  graph_y *= self->_cr_scale;
 
 
-  return self->handle_motion(widget, graph_x, graph_y);
+  return self->handle_motion(graph_x, graph_y);
+}
+
+/**
+ * Called when the mouse has left the graph window.
+ */
+gboolean GtkStatsGraph::
+leave_notify_event_callback(GtkWidget *widget, GdkEventCrossing *event,
+                            gpointer data) {
+  GtkStatsGraph *self = (GtkStatsGraph *)data;
+  return self->handle_leave();
+}
+
+/**
+ * Called when a tooltip should be displayed.
+ */
+gboolean GtkStatsGraph::
+query_tooltip_callback(GtkWidget *widget, gint x, gint y,
+                       gboolean keyboard_tip, GtkTooltip *tooltip,
+                       gpointer data) {
+  GtkStatsGraph *self = (GtkStatsGraph *)data;
+  x *= self->_cr_scale;
+  y *= self->_cr_scale;
+
+  std::string text = self->get_graph_tooltip(x, y);
+  gtk_tooltip_set_text(tooltip, text.c_str());
+  return !text.empty();
+}
+
+/**
+ * Called to update the animations.
+ */
+gboolean GtkStatsGraph::
+tick_callback(GtkWidget *widget, GdkFrameClock *clock, gpointer data) {
+  GtkStatsGraph *graph = (GtkStatsGraph *)data;
+  gint64 new_time = gdk_frame_clock_get_frame_time(clock);
+  if (graph->_time == 0) {
+    // First frame, so we don't have a dt yet.
+    graph->_time = new_time;
+    return TRUE;
+  }
+  gint64 delta = new_time - graph->_time;
+  if (delta == 0) {
+    return TRUE;
+  }
+  if (graph->animate(new_time / 1000000.0, delta / 1000000.0)) {
+    graph->_time = new_time;
+    return TRUE;
+  } else {
+    graph->_timer_id = 0;
+    return FALSE;
+  }
 }
 }

+ 43 - 16
pandatool/src/gtk-stats/gtkStatsGraph.h

@@ -17,6 +17,7 @@
 #include "pandatoolbase.h"
 #include "pandatoolbase.h"
 #include "gtkStatsLabelStack.h"
 #include "gtkStatsLabelStack.h"
 #include "pmap.h"
 #include "pmap.h"
+#include "luse.h"
 
 
 #include <gtk/gtk.h>
 #include <gtk/gtk.h>
 #include <cairo.h>
 #include <cairo.h>
@@ -36,10 +37,11 @@ public:
     DM_guide_bar,
     DM_guide_bar,
     DM_new_guide_bar,
     DM_new_guide_bar,
     DM_sizing,
     DM_sizing,
+    DM_pan,
   };
   };
 
 
 public:
 public:
-  GtkStatsGraph(GtkStatsMonitor *monitor);
+  GtkStatsGraph(GtkStatsMonitor *monitor, bool has_label_stack);
   virtual ~GtkStatsGraph();
   virtual ~GtkStatsGraph();
 
 
   virtual void new_collector(int collector_index);
   virtual void new_collector(int collector_index);
@@ -53,33 +55,43 @@ public:
 
 
   void user_guide_bars_changed();
   void user_guide_bars_changed();
   virtual void on_click_label(int collector_index);
   virtual void on_click_label(int collector_index);
+  virtual void on_popup_label(int collector_index);
   virtual void on_enter_label(int collector_index);
   virtual void on_enter_label(int collector_index);
   virtual void on_leave_label(int collector_index);
   virtual void on_leave_label(int collector_index);
   virtual std::string get_label_tooltip(int collector_index) const;
   virtual std::string get_label_tooltip(int collector_index) const;
 
 
 protected:
 protected:
   void close();
   void close();
+
+  void start_animation();
+  virtual bool animate(double time, double dt);
+
   cairo_pattern_t *get_collector_pattern(int collector_index, bool highlight = false);
   cairo_pattern_t *get_collector_pattern(int collector_index, bool highlight = false);
+  LRGBColor get_collector_text_color(int collector_index, bool highlight = false);
 
 
   virtual void additional_graph_window_paint(cairo_t *cr);
   virtual void additional_graph_window_paint(cairo_t *cr);
+  virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const;
   virtual DragMode consider_drag_start(int graph_x, int graph_y);
   virtual DragMode consider_drag_start(int graph_x, int graph_y);
   virtual void set_drag_mode(DragMode drag_mode);
   virtual void set_drag_mode(DragMode drag_mode);
 
 
-  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);
+  virtual gboolean handle_button_press(int graph_x, int graph_y,
+                                       bool double_click, int button);
+  virtual gboolean handle_button_release(int graph_x, int graph_y);
+  virtual gboolean handle_motion(int graph_x, int graph_y);
+  virtual gboolean handle_leave();
 
 
 protected:
 protected:
   // Table of patterns for our various collectors.
   // Table of patterns for our various collectors.
   typedef pmap<int, std::pair<cairo_pattern_t *, cairo_pattern_t *> > Brushes;
   typedef pmap<int, std::pair<cairo_pattern_t *, cairo_pattern_t *> > Brushes;
   Brushes _brushes;
   Brushes _brushes;
 
 
+  typedef pmap<int, std::pair<LRGBColor, LRGBColor> > TextColors;
+  TextColors _text_colors;
+
   GtkStatsMonitor *_monitor;
   GtkStatsMonitor *_monitor;
   GtkWidget *_parent_window;
   GtkWidget *_parent_window;
   GtkWidget *_window;
   GtkWidget *_window;
   GtkWidget *_graph_frame;
   GtkWidget *_graph_frame;
-  GtkWidget *_graph_overlay;
   GtkWidget *_graph_window;
   GtkWidget *_graph_window;
   GtkWidget *_graph_hbox;
   GtkWidget *_graph_hbox;
   GtkWidget *_graph_vbox;
   GtkWidget *_graph_vbox;
@@ -92,6 +104,9 @@ protected:
   cairo_surface_t *_cr_surface;
   cairo_surface_t *_cr_surface;
   cairo_t *_cr;
   cairo_t *_cr;
   int _surface_xsize, _surface_ysize;
   int _surface_xsize, _surface_ysize;
+  PangoAttrList *_pango_attrs;
+  int _cr_scale;
+  int _pixel_scale;
 
 
   DragMode _drag_mode;
   DragMode _drag_mode;
   DragMode _potential_drag_mode;
   DragMode _potential_drag_mode;
@@ -103,6 +118,9 @@ protected:
 
 
   bool _pause;
   bool _pause;
 
 
+  guint _timer_id = 0;
+  gint64 _time = 0;
+
   static const double rgb_white[3];
   static const double rgb_white[3];
   static const double rgb_light_gray[3];
   static const double rgb_light_gray[3];
   static const double rgb_dark_gray[3];
   static const double rgb_dark_gray[3];
@@ -110,27 +128,36 @@ protected:
   static const double rgb_user_guide_bar[3];
   static const double rgb_user_guide_bar[3];
 
 
 private:
 private:
-  void setup_surface(int xsize, int ysize);
+  void setup_surface(int xsize, int ysize, int scale);
   void release_surface();
   void release_surface();
 
 
   static gboolean window_delete_event(GtkWidget *widget, GdkEvent *event,
   static gboolean window_delete_event(GtkWidget *widget, GdkEvent *event,
-              gpointer data);
+                                      gpointer data);
   static void window_destroy(GtkWidget *widget, gpointer data);
   static void window_destroy(GtkWidget *widget, gpointer data);
   static gboolean graph_draw_callback(GtkWidget *widget,
   static gboolean graph_draw_callback(GtkWidget *widget,
-              cairo_t *cr, gpointer data);
+                                      cairo_t *cr, gpointer data);
   static gboolean configure_graph_callback(GtkWidget *widget,
   static gboolean configure_graph_callback(GtkWidget *widget,
-              GdkEventConfigure *event, gpointer data);
+                                           GdkEventConfigure *event,
+                                           gpointer data);
 
 
 protected:
 protected:
   static gboolean button_press_event_callback(GtkWidget *widget,
   static gboolean button_press_event_callback(GtkWidget *widget,
-                GdkEventButton *event,
-                gpointer data);
+                                              GdkEventButton *event,
+                                              gpointer data);
   static gboolean button_release_event_callback(GtkWidget *widget,
   static gboolean button_release_event_callback(GtkWidget *widget,
-            GdkEventButton *event,
-            gpointer data);
+                                                GdkEventButton *event,
+                                                gpointer data);
   static gboolean motion_notify_event_callback(GtkWidget *widget,
   static gboolean motion_notify_event_callback(GtkWidget *widget,
-                 GdkEventMotion *event,
-                 gpointer data);
+                                               GdkEventMotion *event,
+                                               gpointer data);
+  static gboolean leave_notify_event_callback(GtkWidget *widget,
+                                              GdkEventCrossing *event,
+                                              gpointer data);
+  static gboolean query_tooltip_callback(GtkWidget *widget, gint x, gint y,
+                                         gboolean keyboard_tip,
+                                         GtkTooltip *tooltip, gpointer data);
+  static gboolean tick_callback(GtkWidget *widget, GdkFrameClock *clock,
+                                gpointer data);
 };
 };
 
 
 #endif
 #endif

+ 10 - 8
pandatool/src/gtk-stats/gtkStatsLabel.cxx

@@ -50,7 +50,6 @@ GtkStatsLabel(GtkStatsMonitor *monitor, GtkStatsGraph *graph,
        G_CALLBACK(query_tooltip_callback), this);
        G_CALLBACK(query_tooltip_callback), this);
 
 
   gtk_widget_set_has_tooltip(_widget, TRUE);
   gtk_widget_set_has_tooltip(_widget, TRUE);
-  gtk_widget_show(_widget);
 
 
   // Set the fg and bg colors on the label.
   // Set the fg and bg colors on the label.
   LRGBColor rgb = _monitor->get_collector_color(_collector_index);
   LRGBColor rgb = _monitor->get_collector_color(_collector_index);
@@ -71,7 +70,7 @@ GtkStatsLabel(GtkStatsMonitor *monitor, GtkStatsGraph *graph,
   } else {
   } else {
     _fg_color = LRGBColor(1);
     _fg_color = LRGBColor(1);
   }
   }
-  if (bright >= 0.5 * 0.75) {
+  if (bright * 0.75 >= 0.5) {
     _highlight_fg_color = LRGBColor(0);
     _highlight_fg_color = LRGBColor(0);
   } else {
   } else {
     _highlight_fg_color = LRGBColor(1);
     _highlight_fg_color = LRGBColor(1);
@@ -81,6 +80,7 @@ GtkStatsLabel(GtkStatsMonitor *monitor, GtkStatsGraph *graph,
   _mouse_within = false;
   _mouse_within = false;
 
 
   update_text(use_fullname);
   update_text(use_fullname);
+  gtk_widget_show_all(_widget);
 }
 }
 
 
 /**
 /**
@@ -237,7 +237,7 @@ enter_notify_event_callback(GtkWidget *widget, GdkEventCrossing *event,
  */
  */
 gboolean GtkStatsLabel::
 gboolean GtkStatsLabel::
 leave_notify_event_callback(GtkWidget *widget, GdkEventCrossing *event,
 leave_notify_event_callback(GtkWidget *widget, GdkEventCrossing *event,
-          gpointer data) {
+                            gpointer data) {
   GtkStatsLabel *self = (GtkStatsLabel *)data;
   GtkStatsLabel *self = (GtkStatsLabel *)data;
   self->set_mouse_within(false);
   self->set_mouse_within(false);
   return TRUE;
   return TRUE;
@@ -248,12 +248,14 @@ leave_notify_event_callback(GtkWidget *widget, GdkEventCrossing *event,
  */
  */
 gboolean GtkStatsLabel::
 gboolean GtkStatsLabel::
 button_press_event_callback(GtkWidget *widget, GdkEventButton *event,
 button_press_event_callback(GtkWidget *widget, GdkEventButton *event,
-          gpointer data) {
+                            gpointer data) {
   GtkStatsLabel *self = (GtkStatsLabel *)data;
   GtkStatsLabel *self = (GtkStatsLabel *)data;
-  bool double_click = (event->type == GDK_2BUTTON_PRESS);
-  if (double_click) {
+  if (event->type == GDK_2BUTTON_PRESS && event->button == 1) {
     self->_graph->on_click_label(self->_collector_index);
     self->_graph->on_click_label(self->_collector_index);
   }
   }
+  else if (event->type == GDK_BUTTON_PRESS && event->button == 3) {
+    self->_graph->on_popup_label(self->_collector_index);
+  }
   return TRUE;
   return TRUE;
 }
 }
 
 
@@ -262,8 +264,8 @@ button_press_event_callback(GtkWidget *widget, GdkEventButton *event,
  */
  */
 gboolean GtkStatsLabel::
 gboolean GtkStatsLabel::
 query_tooltip_callback(GtkWidget *widget, gint x, gint y,
 query_tooltip_callback(GtkWidget *widget, gint x, gint y,
-                gboolean keyboard_tip, GtkTooltip *tooltip,
-                gpointer data) {
+                       gboolean keyboard_tip, GtkTooltip *tooltip,
+                       gpointer data) {
   GtkStatsLabel *self = (GtkStatsLabel *)data;
   GtkStatsLabel *self = (GtkStatsLabel *)data;
 
 
   std::string text = self->_graph->get_label_tooltip(self->_collector_index);
   std::string text = self->_graph->get_label_tooltip(self->_collector_index);

+ 7 - 3
pandatool/src/gtk-stats/gtkStatsMonitor.I

@@ -14,10 +14,11 @@
 /**
 /**
  *
  *
  */
  */
-GtkStatsMonitor::MenuDef::
-MenuDef(int thread_index, int collector_index, bool show_level) :
+INLINE GtkStatsMonitor::MenuDef::
+MenuDef(int thread_index, int collector_index, ChartType chart_type, bool show_level) :
   _thread_index(thread_index),
   _thread_index(thread_index),
   _collector_index(collector_index),
   _collector_index(collector_index),
+  _chart_type(chart_type),
   _show_level(show_level),
   _show_level(show_level),
   _monitor(nullptr)
   _monitor(nullptr)
 {
 {
@@ -26,7 +27,7 @@ MenuDef(int thread_index, int collector_index, bool show_level) :
 /**
 /**
  *
  *
  */
  */
-bool GtkStatsMonitor::MenuDef::
+INLINE bool GtkStatsMonitor::MenuDef::
 operator < (const MenuDef &other) const {
 operator < (const MenuDef &other) const {
   if (_thread_index != other._thread_index) {
   if (_thread_index != other._thread_index) {
     return _thread_index < other._thread_index;
     return _thread_index < other._thread_index;
@@ -34,5 +35,8 @@ operator < (const MenuDef &other) const {
   if (_collector_index != other._collector_index) {
   if (_collector_index != other._collector_index) {
     return _collector_index < other._collector_index;
     return _collector_index < other._collector_index;
   }
   }
+  if (_chart_type != other._chart_type) {
+    return _chart_type < other._chart_type;
+  }
   return (int)_show_level < (int)other._show_level;
   return (int)_show_level < (int)other._show_level;
 }
 }

+ 227 - 7
pandatool/src/gtk-stats/gtkStatsMonitor.cxx

@@ -18,10 +18,10 @@
 #include "gtkStatsChartMenu.h"
 #include "gtkStatsChartMenu.h"
 #include "gtkStatsPianoRoll.h"
 #include "gtkStatsPianoRoll.h"
 #include "gtkStatsFlameGraph.h"
 #include "gtkStatsFlameGraph.h"
+#include "gtkStatsTimeline.h"
 #include "gtkStatsMenuId.h"
 #include "gtkStatsMenuId.h"
 #include "pStatGraph.h"
 #include "pStatGraph.h"
 #include "pStatCollectorDef.h"
 #include "pStatCollectorDef.h"
-#include "indent.h"
 
 
 /**
 /**
  *
  *
@@ -34,6 +34,8 @@ GtkStatsMonitor(GtkStatsServer *server) : PStatMonitor(server) {
   _time_units = 0;
   _time_units = 0;
   _scroll_speed = 0.0;
   _scroll_speed = 0.0;
   _pause = false;
   _pause = false;
+
+  _resolution = gdk_screen_get_resolution(gdk_screen_get_default());
 }
 }
 
 
 /**
 /**
@@ -154,11 +156,13 @@ new_thread(int thread_index) {
  */
  */
 void GtkStatsMonitor::
 void GtkStatsMonitor::
 new_data(int thread_index, int frame_number) {
 new_data(int thread_index, int frame_number) {
-  Graphs::iterator gi;
-  for (gi = _graphs.begin(); gi != _graphs.end(); ++gi) {
-    GtkStatsGraph *graph = (*gi);
+  for (GtkStatsGraph *graph : _graphs) {
     graph->new_data(thread_index, frame_number);
     graph->new_data(thread_index, frame_number);
   }
   }
+
+  if (thread_index == 0) {
+    update_status_bar();
+  }
 }
 }
 
 
 /**
 /**
@@ -193,6 +197,10 @@ idle() {
     sprintf(buffer, "%0.1f ms / %0.1f Hz", 1000.0f / frame_rate, frame_rate);
     sprintf(buffer, "%0.1f ms / %0.1f Hz", 1000.0f / frame_rate, frame_rate);
 
 
     gtk_label_set_text(GTK_LABEL(_frame_rate_label), buffer);
     gtk_label_set_text(GTK_LABEL(_frame_rate_label), buffer);
+
+    if (!_status_bar_labels.empty()) {
+      gtk_label_set_text(GTK_LABEL(_status_bar_labels[0]), buffer);
+    }
   }
   }
 }
 }
 
 
@@ -225,6 +233,14 @@ get_window() const {
   return _window;
   return _window;
 }
 }
 
 
+/**
+ * Returns the screen DPI.
+ */
+double GtkStatsMonitor::
+get_resolution() const {
+  return _resolution;
+}
+
 /**
 /**
  * Opens a new strip chart showing the indicated data.
  * Opens a new strip chart showing the indicated data.
  */
  */
@@ -256,8 +272,21 @@ open_piano_roll(int thread_index) {
  * Opens a new flame graph showing the indicated data.
  * Opens a new flame graph showing the indicated data.
  */
  */
 void GtkStatsMonitor::
 void GtkStatsMonitor::
-open_flame_graph(int thread_index) {
-  GtkStatsFlameGraph *graph = new GtkStatsFlameGraph(this, thread_index);
+open_flame_graph(int thread_index, int collector_index) {
+  GtkStatsFlameGraph *graph = new GtkStatsFlameGraph(this, thread_index, collector_index);
+  add_graph(graph);
+
+  graph->set_time_units(_time_units);
+  graph->set_scroll_speed(_scroll_speed);
+  graph->set_pause(_pause);
+}
+
+/**
+ * Opens a new timeline.
+ */
+void GtkStatsMonitor::
+open_timeline() {
+  GtkStatsTimeline *graph = new GtkStatsTimeline(this);
   add_graph(graph);
   add_graph(graph);
 
 
   graph->set_time_units(_time_units);
   graph->set_time_units(_time_units);
@@ -371,7 +400,7 @@ create_window() {
   gtk_window_set_default_size(GTK_WINDOW(_window), 500, 360);
   gtk_window_set_default_size(GTK_WINDOW(_window), 500, 360);
 
 
   // Set up the menu.
   // Set up the menu.
-   GtkAccelGroup *accel_group = gtk_accel_group_new();
+  GtkAccelGroup *accel_group = gtk_accel_group_new();
   gtk_window_add_accel_group(GTK_WINDOW(_window), accel_group);
   gtk_window_add_accel_group(GTK_WINDOW(_window), accel_group);
   _menu_bar = gtk_menu_bar_new();
   _menu_bar = gtk_menu_bar_new();
   _next_chart_index = 2;
   _next_chart_index = 2;
@@ -390,8 +419,21 @@ create_window() {
   gtk_container_add(GTK_CONTAINER(_window), main_vbox);
   gtk_container_add(GTK_CONTAINER(_window), main_vbox);
   gtk_box_pack_start(GTK_BOX(main_vbox), _menu_bar, FALSE, TRUE, 0);
   gtk_box_pack_start(GTK_BOX(main_vbox), _menu_bar, FALSE, TRUE, 0);
 
 
+  // Create the status bar.
+  _status_bar = gtk_flow_box_new();
+  gtk_flow_box_set_activate_on_single_click(GTK_FLOW_BOX(_status_bar), FALSE);
+  gtk_flow_box_set_selection_mode(GTK_FLOW_BOX(_status_bar), GTK_SELECTION_NONE);
+  g_signal_connect(G_OBJECT(_status_bar), "button_press_event",
+    G_CALLBACK(status_bar_button_event), this);
+  gtk_box_pack_end(GTK_BOX(main_vbox), _status_bar, FALSE, FALSE, 0);
+  update_status_bar();
+
+  GtkWidget *sep = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL);
+  gtk_box_pack_end(GTK_BOX(main_vbox), sep, FALSE, FALSE, 0);
+
   gtk_widget_show_all(_window);
   gtk_widget_show_all(_window);
   gtk_widget_show(_window);
   gtk_widget_show(_window);
+  gtk_widget_realize(_window);
 }
 }
 
 
 /**
 /**
@@ -572,6 +614,7 @@ setup_frame_rate_label() {
   _frame_rate_menu_item = gtk_menu_item_new();
   _frame_rate_menu_item = gtk_menu_item_new();
   _frame_rate_label = gtk_label_new("");
   _frame_rate_label = gtk_label_new("");
   gtk_container_add(GTK_CONTAINER(_frame_rate_menu_item), _frame_rate_label);
   gtk_container_add(GTK_CONTAINER(_frame_rate_menu_item), _frame_rate_label);
+  gtk_widget_set_sensitive(_frame_rate_menu_item, FALSE);
 
 
   gtk_widget_show(_frame_rate_menu_item);
   gtk_widget_show(_frame_rate_menu_item);
   gtk_widget_show(_frame_rate_label);
   gtk_widget_show(_frame_rate_label);
@@ -579,3 +622,180 @@ setup_frame_rate_label() {
 
 
   gtk_menu_shell_append(GTK_MENU_SHELL(_menu_bar), _frame_rate_menu_item);
   gtk_menu_shell_append(GTK_MENU_SHELL(_menu_bar), _frame_rate_menu_item);
 }
 }
+
+/**
+ * Updates the status bar.
+ */
+void GtkStatsMonitor::
+update_status_bar() {
+  const PStatClientData *client_data = get_client_data();
+  if (client_data == nullptr) {
+    return;
+  }
+
+  const PStatThreadData *thread_data = get_client_data()->get_thread_data(0);
+  if (thread_data == nullptr || thread_data->is_empty()) {
+    return;
+  }
+  const PStatFrameData &frame_data = thread_data->get_latest_frame();
+
+  pvector<int> collectors;
+
+  // The first label displays the frame rate.
+  size_t li = 1;
+  collectors.push_back(0);
+  if (_status_bar_labels.empty()) {
+    GtkWidget *label = gtk_label_new("");
+    gtk_container_add(GTK_CONTAINER(_status_bar), label);
+    _status_bar_labels.push_back(label);
+  }
+
+  // Gather the top-level collector list.
+  int num_toplevel_collectors = client_data->get_num_toplevel_collectors();
+  for (int tc = 0; tc < num_toplevel_collectors; tc++) {
+    int collector = client_data->get_toplevel_collector(tc);
+    if (client_data->has_collector(collector) &&
+        client_data->get_collector_has_level(collector, 0)) {
+      PStatView &view = get_level_view(collector, 0);
+      view.set_to_frame(frame_data);
+      double value = view.get_net_value();
+      if (value == 0.0) {
+        // Don't include it unless we've included it before.
+        if (std::find(_status_bar_collectors.begin(), _status_bar_collectors.end(), collector) == _status_bar_collectors.end()) {
+          continue;
+        }
+      }
+
+      const PStatCollectorDef &def = client_data->get_collector_def(collector);
+      std::string text = def._name;
+      text += ": " + PStatGraph::format_number(value, PStatGraph::GBU_named | PStatGraph::GBU_show_units, def._level_units);
+
+      GtkWidget *label;
+      if (li < _status_bar_labels.size()) {
+        label = _status_bar_labels[li++];
+        gtk_label_set_text(GTK_LABEL(label), text.c_str());
+      }
+      else {
+        label = gtk_label_new(text.c_str());
+        gtk_container_add(GTK_CONTAINER(_status_bar), label);
+        _status_bar_labels.push_back(label);
+      }
+
+      collectors.push_back(collector);
+    }
+  }
+
+  _status_bar_collectors = std::move(collectors);
+
+  gtk_widget_show_all(_status_bar);
+}
+
+/**
+ * Handles clicks on a partion of the status bar.
+ */
+gboolean GtkStatsMonitor::
+status_bar_button_event(GtkWidget *widget, GdkEventButton *event, gpointer data) {
+  GtkStatsMonitor *monitor = (GtkStatsMonitor *)data;
+
+  GtkFlowBoxChild *child = gtk_flow_box_get_child_at_pos(
+    GTK_FLOW_BOX(monitor->_status_bar), event->x, event->y);
+  if (child == nullptr) {
+    return FALSE;
+  }
+
+  // Which child is this?
+  GList *children = gtk_container_get_children(GTK_CONTAINER(monitor->_status_bar));
+  int index = g_list_index(children, child);
+  g_list_free(children);
+  if (index < 0 || index >= monitor->_status_bar_labels.size()) {
+    return FALSE;
+  }
+
+  const PStatClientData *client_data = monitor->get_client_data();
+  if (client_data == nullptr) {
+    return FALSE;
+  }
+
+  int collector = monitor->_status_bar_collectors[index];
+
+  if (event->type == GDK_2BUTTON_PRESS && event->button == 1) {
+    monitor->open_strip_chart(0, collector, collector != 0);
+    return TRUE;
+  }
+  else if (event->type == GDK_BUTTON_PRESS && event->button == 3 && index > 0) {
+    PStatView &level_view = monitor->get_level_view(collector, 0);
+    const PStatViewLevel *view_level = level_view.get_top_level();
+    int num_children = view_level->get_num_children();
+    if (num_children == 0) {
+      return FALSE;
+    }
+
+    GtkWidget *menu = gtk_menu_new();
+
+    // Reverse the order since the menus are listed from the top down; we want
+    // to be visually consistent with the graphs, which list these labels from
+    // the bottom up.
+    for (int c = num_children - 1; c >= 0; c--) {
+      const PStatViewLevel *child_level = view_level->get_child(c);
+
+      int child_collector = child_level->get_collector();
+      const MenuDef *menu_def = monitor->add_menu({0, child_collector, CT_strip_chart, true});
+
+      double value = child_level->get_net_value();
+
+      const PStatCollectorDef &def = client_data->get_collector_def(child_collector);
+      std::string text = def._name;
+      text += ": " + PStatGraph::format_number(value, PStatGraph::GBU_named | PStatGraph::GBU_show_units, def._level_units);
+
+      GtkWidget *menu_item = gtk_menu_item_new_with_label(text.c_str());
+      gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+
+      g_signal_connect(G_OBJECT(menu_item), "activate",
+                       G_CALLBACK(menu_activate),
+                       (void *)menu_def);
+    }
+
+    gtk_widget_show_all(menu);
+
+    GtkWidget *label = monitor->_status_bar_labels[index];
+    gtk_menu_popup_at_widget(GTK_MENU(menu), label,
+                             GDK_GRAVITY_NORTH_WEST,
+                             GDK_GRAVITY_SOUTH_WEST, nullptr);
+    return TRUE;
+  }
+  return FALSE;
+}
+
+/**
+ * Callback when a menu item is selected.
+ */
+void GtkStatsMonitor::
+menu_activate(GtkWidget *widget, gpointer data) {
+  const MenuDef *menu_def = (const MenuDef *)data;
+  GtkStatsMonitor *monitor = menu_def->_monitor;
+
+  if (monitor == nullptr) {
+    return;
+  }
+
+  switch (menu_def->_chart_type) {
+  case CT_timeline:
+    monitor->open_timeline();
+    break;
+
+  case CT_strip_chart:
+    monitor->open_strip_chart(menu_def->_thread_index,
+                              menu_def->_collector_index,
+                              menu_def->_show_level);
+    break;
+
+  case CT_flame_graph:
+    monitor->open_flame_graph(menu_def->_thread_index,
+                              menu_def->_collector_index);
+    break;
+
+  case CT_piano_roll:
+    monitor->open_piano_roll(menu_def->_thread_index);
+    break;
+  }
+}

+ 27 - 2
pandatool/src/gtk-stats/gtkStatsMonitor.h

@@ -34,13 +34,22 @@ class GtkStatsChartMenu;
  */
  */
 class GtkStatsMonitor : public PStatMonitor {
 class GtkStatsMonitor : public PStatMonitor {
 public:
 public:
+  enum ChartType {
+    CT_timeline,
+    CT_strip_chart,
+    CT_flame_graph,
+    CT_piano_roll,
+  };
+
   class MenuDef {
   class MenuDef {
   public:
   public:
-    INLINE MenuDef(int thread_index, int collector_index, bool show_level);
+    INLINE MenuDef(int thread_index, int collector_index,
+                   ChartType chart_type, bool show_level = false);
     INLINE bool operator < (const MenuDef &other) const;
     INLINE bool operator < (const MenuDef &other) const;
 
 
     int _thread_index;
     int _thread_index;
     int _collector_index;
     int _collector_index;
+    ChartType _chart_type;
     bool _show_level;
     bool _show_level;
     GtkStatsMonitor *_monitor;
     GtkStatsMonitor *_monitor;
   };
   };
@@ -64,9 +73,12 @@ public:
   virtual void user_guide_bars_changed();
   virtual void user_guide_bars_changed();
 
 
   GtkWidget *get_window() const;
   GtkWidget *get_window() const;
+  double get_resolution() const;
+
   void open_strip_chart(int thread_index, int collector_index, bool show_level);
   void open_strip_chart(int thread_index, int collector_index, bool show_level);
   void open_piano_roll(int thread_index);
   void open_piano_roll(int thread_index);
-  void open_flame_graph(int thread_index);
+  void open_flame_graph(int thread_index, int collector_index = -1);
+  void open_timeline();
 
 
   const MenuDef *add_menu(const MenuDef &menu_def);
   const MenuDef *add_menu(const MenuDef &menu_def);
 
 
@@ -86,7 +98,16 @@ private:
   void setup_options_menu();
   void setup_options_menu();
   void setup_speed_menu();
   void setup_speed_menu();
   void setup_frame_rate_label();
   void setup_frame_rate_label();
+  void update_status_bar();
+  bool show_popup_menu(int collector);
 
 
+  static gboolean status_bar_button_event(GtkWidget *widget,
+                                          GdkEventButton *event,
+                                          gpointer data);
+public:
+  static void menu_activate(GtkWidget *widget, gpointer data);
+
+private:
   typedef pset<GtkStatsGraph *> Graphs;
   typedef pset<GtkStatsGraph *> Graphs;
   Graphs _graphs;
   Graphs _graphs;
 
 
@@ -103,10 +124,14 @@ private:
   int _next_chart_index;
   int _next_chart_index;
   GtkWidget *_frame_rate_menu_item;
   GtkWidget *_frame_rate_menu_item;
   GtkWidget *_frame_rate_label;
   GtkWidget *_frame_rate_label;
+  GtkWidget *_status_bar;
+  pvector<int> _status_bar_collectors;
+  pvector<GtkWidget *> _status_bar_labels;
   std::string _window_title;
   std::string _window_title;
   int _time_units;
   int _time_units;
   double _scroll_speed;
   double _scroll_speed;
   bool _pause;
   bool _pause;
+  double _resolution;
 
 
   friend class GtkStatsGraph;
   friend class GtkStatsGraph;
 };
 };

+ 130 - 43
pandatool/src/gtk-stats/gtkStatsPianoRoll.cxx

@@ -16,18 +16,16 @@
 #include "numeric_types.h"
 #include "numeric_types.h"
 #include "gtkStatsLabelStack.h"
 #include "gtkStatsLabelStack.h"
 
 
-static const int default_piano_roll_width = 600;
-static const int default_piano_roll_height = 200;
+static const int default_piano_roll_width = 800;
+static const int default_piano_roll_height = 400;
 
 
 /**
 /**
  *
  *
  */
  */
 GtkStatsPianoRoll::
 GtkStatsPianoRoll::
 GtkStatsPianoRoll(GtkStatsMonitor *monitor, int thread_index) :
 GtkStatsPianoRoll(GtkStatsMonitor *monitor, int thread_index) :
-  PStatPianoRoll(monitor, thread_index,
-                 default_piano_roll_width,
-                 default_piano_roll_height),
-  GtkStatsGraph(monitor)
+  PStatPianoRoll(monitor, thread_index, 0, 0),
+  GtkStatsGraph(monitor, true)
 {
 {
   // Let's show the units on the guide bar labels.  There's room.
   // Let's show the units on the guide bar labels.  There's room.
   set_guide_bar_units(get_guide_bar_units() | GBU_show_units);
   set_guide_bar_units(get_guide_bar_units() | GBU_show_units);
@@ -36,21 +34,21 @@ GtkStatsPianoRoll(GtkStatsMonitor *monitor, int thread_index) :
   // units.
   // units.
   _scale_area = gtk_drawing_area_new();
   _scale_area = gtk_drawing_area_new();
   g_signal_connect(G_OBJECT(_scale_area), "draw",
   g_signal_connect(G_OBJECT(_scale_area), "draw",
-       G_CALLBACK(draw_callback), this);
-  gtk_box_pack_start(GTK_BOX(_graph_vbox), _scale_area,
-         FALSE, FALSE, 0);
+                   G_CALLBACK(draw_callback), this);
+  gtk_box_pack_start(GTK_BOX(_graph_vbox), _scale_area, FALSE, FALSE, 0);
 
 
   // It should be large enough to display the labels.
   // It should be large enough to display the labels.
   {
   {
-    PangoLayout *layout = gtk_widget_create_pango_layout(_window, "0123456789 ms");
+    PangoLayout *layout = gtk_widget_create_pango_layout(_scale_area, "0123456789 ms");
     int width, height;
     int width, height;
     pango_layout_get_pixel_size(layout, &width, &height);
     pango_layout_get_pixel_size(layout, &width, &height);
     gtk_widget_set_size_request(_scale_area, 0, height + 1);
     gtk_widget_set_size_request(_scale_area, 0, height + 1);
     g_object_unref(layout);
     g_object_unref(layout);
   }
   }
 
 
-  gtk_widget_set_size_request(_graph_window, default_piano_roll_width,
-            default_piano_roll_height);
+  gtk_widget_set_size_request(_graph_window,
+    default_piano_roll_width * monitor->get_resolution() / 96,
+    default_piano_roll_height * monitor->get_resolution() / 96);
 
 
   const PStatClientData *client_data =
   const PStatClientData *client_data =
     GtkStatsGraph::_monitor->get_client_data();
     GtkStatsGraph::_monitor->get_client_data();
@@ -77,7 +75,7 @@ GtkStatsPianoRoll::
 }
 }
 
 
 /**
 /**
- * Called as each frame's data is made available.  There is no gurantee the
+ * 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
  * 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
  * monitor should be prepared to accept frames received out-of-order or
  * missing.
  * missing.
@@ -94,7 +92,9 @@ new_data(int thread_index, int frame_number) {
  */
  */
 void GtkStatsPianoRoll::
 void GtkStatsPianoRoll::
 force_redraw() {
 force_redraw() {
-  PStatPianoRoll::force_redraw();
+  if (_cr) {
+    PStatPianoRoll::force_redraw();
+  }
 }
 }
 
 
 /**
 /**
@@ -132,6 +132,57 @@ on_click_label(int collector_index) {
   }
   }
 }
 }
 
 
+/**
+ * Called when the user right-clicks on a label.
+ */
+void GtkStatsPianoRoll::
+on_popup_label(int collector_index) {
+  GtkWidget *menu = gtk_menu_new();
+
+  std::string label = get_label_tooltip(collector_index);
+  if (!label.empty()) {
+    GtkWidget *menu_item = gtk_menu_item_new_with_label(label.c_str());
+    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+    gtk_widget_set_sensitive(menu_item, FALSE);
+  }
+
+  {
+    const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({
+      _thread_index, collector_index, GtkStatsMonitor::CT_strip_chart, false,
+    });
+
+    GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Strip Chart");
+    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+    g_signal_connect(G_OBJECT(menu_item), "activate",
+                     G_CALLBACK(GtkStatsMonitor::menu_activate),
+                     (void *)menu_def);
+  }
+
+  {
+    const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({
+      _thread_index, collector_index, GtkStatsMonitor::CT_flame_graph,
+    });
+
+    GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Flame Graph");
+    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+    g_signal_connect(G_OBJECT(menu_item), "activate",
+                     G_CALLBACK(GtkStatsMonitor::menu_activate),
+                     (void *)menu_def);
+  }
+
+  gtk_widget_show_all(menu);
+  gtk_menu_popup_at_pointer(GTK_MENU(menu), nullptr);
+}
+
+/**
+ * Called when the mouse hovers over a label, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string GtkStatsPianoRoll::
+get_label_tooltip(int collector_index) const {
+  return PStatPianoRoll::get_label_tooltip(collector_index);
+}
+
 /**
 /**
  * Changes the amount of time the width of the horizontal axis represents.
  * Changes the amount of time the width of the horizontal axis represents.
  * This may force a redraw.
  * This may force a redraw.
@@ -188,7 +239,7 @@ draw_bar(int row, int from_x, int to_x) {
     int y = _label_stack.get_label_y(row, _graph_window);
     int y = _label_stack.get_label_y(row, _graph_window);
     int height = _label_stack.get_label_height(row);
     int height = _label_stack.get_label_height(row);
 
 
-    cairo_rectangle(_cr, from_x, y - height + 2, to_x - from_x, height - 4);
+    cairo_rectangle(_cr, from_x, (y - height + 2) * _cr_scale, to_x - from_x, (height - 4) * _cr_scale);
     cairo_fill(_cr);
     cairo_fill(_cr);
   }
   }
 }
 }
@@ -224,6 +275,19 @@ additional_graph_window_paint(cairo_t *cr) {
   }
   }
 }
 }
 
 
+/**
+ * Called when the mouse hovers over the graph, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string GtkStatsPianoRoll::
+get_graph_tooltip(int mouse_x, int mouse_y) const {
+  int collector_index = get_collector_under_pixel(mouse_x, mouse_y);
+  if (collector_index >= 0) {
+    return get_label_tooltip(collector_index);
+  }
+  return std::string();
+}
+
 /**
 /**
  * Based on the mouse position within the graph window, look for draggable
  * Based on the mouse position within the graph window, look for draggable
  * things the mouse might be hovering over and return the appropriate DragMode
  * things the mouse might be hovering over and return the appropriate DragMode
@@ -256,13 +320,24 @@ consider_drag_start(int graph_x, int graph_y) {
  * Called when the mouse button is depressed within the graph window.
  * Called when the mouse button is depressed within the graph window.
  */
  */
 gboolean GtkStatsPianoRoll::
 gboolean GtkStatsPianoRoll::
-handle_button_press(GtkWidget *widget, int graph_x, int graph_y,
-        bool double_click) {
-  if (double_click) {
-    // Double-clicking on a color bar in the graph is the same as double-
-    // clicking on the corresponding label.
-    on_click_label(get_collector_under_pixel(graph_x, graph_y));
-    return TRUE;
+handle_button_press(int graph_x, int graph_y, bool double_click, int button) {
+  if (graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) {
+    int collector_index = get_collector_under_pixel(graph_x, graph_y);
+    if (button == 3) {
+      // Right-clicking on a color bar in the graph is the same as right-
+      // clicking on the corresponding label.
+      if (collector_index >= 0) {
+        on_popup_label(collector_index);
+        return TRUE;
+      }
+      return FALSE;
+    }
+    else if (double_click && button == 1) {
+      // Double-clicking on a color bar in the graph is the same as double-
+      // clicking on the corresponding label.
+      on_click_label(get_collector_under_pixel(graph_x, graph_y));
+      return TRUE;
+    }
   }
   }
 
 
   if (_potential_drag_mode == DM_none) {
   if (_potential_drag_mode == DM_none) {
@@ -278,21 +353,21 @@ handle_button_press(GtkWidget *widget, int graph_x, int graph_y,
     return TRUE;
     return TRUE;
   }
   }
 
 
-  return GtkStatsGraph::handle_button_press(widget, graph_x, graph_y,
-              double_click);
+  return GtkStatsGraph::handle_button_press(graph_x, graph_y,
+                                            double_click, button);
 }
 }
 
 
 /**
 /**
  * Called when the mouse button is released within the graph window.
  * Called when the mouse button is released within the graph window.
  */
  */
 gboolean GtkStatsPianoRoll::
 gboolean GtkStatsPianoRoll::
-handle_button_release(GtkWidget *widget, int graph_x, int graph_y) {
+handle_button_release(int graph_x, int graph_y) {
   if (_drag_mode == DM_scale) {
   if (_drag_mode == DM_scale) {
     set_drag_mode(DM_none);
     set_drag_mode(DM_none);
     // ReleaseCapture();
     // ReleaseCapture();
-    return handle_motion(widget, graph_x, graph_y);
-
-  } else if (_drag_mode == DM_guide_bar) {
+    return handle_motion(graph_x, graph_y);
+  }
+  else if (_drag_mode == DM_guide_bar) {
     if (graph_x < 0 || graph_x >= get_xsize()) {
     if (graph_x < 0 || graph_x >= get_xsize()) {
       remove_user_guide_bar(_drag_guide_bar);
       remove_user_guide_bar(_drag_guide_bar);
     } else {
     } else {
@@ -300,17 +375,17 @@ handle_button_release(GtkWidget *widget, int graph_x, int graph_y) {
     }
     }
     set_drag_mode(DM_none);
     set_drag_mode(DM_none);
     // ReleaseCapture();
     // ReleaseCapture();
-    return handle_motion(widget, graph_x, graph_y);
+    return handle_motion(graph_x, graph_y);
   }
   }
 
 
-  return GtkStatsGraph::handle_button_release(widget, graph_x, graph_y);
+  return GtkStatsGraph::handle_button_release(graph_x, graph_y);
 }
 }
 
 
 /**
 /**
  * Called when the mouse is moved within the graph window.
  * Called when the mouse is moved within the graph window.
  */
  */
 gboolean GtkStatsPianoRoll::
 gboolean GtkStatsPianoRoll::
-handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
+handle_motion(int graph_x, int graph_y) {
   if (_drag_mode == DM_none && _potential_drag_mode == DM_none) {
   if (_drag_mode == DM_none && _potential_drag_mode == DM_none) {
     // When the mouse is over a color bar, highlight it.
     // When the mouse is over a color bar, highlight it.
     int collector_index = get_collector_under_pixel(graph_x, graph_y);
     int collector_index = get_collector_under_pixel(graph_x, graph_y);
@@ -328,8 +403,8 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
     };
     };
     TrackMouseEvent(&tme);
     TrackMouseEvent(&tme);
     */
     */
-
-  } else {
+  }
+  else {
     // If the mouse is in some drag mode, stop highlighting.
     // If the mouse is in some drag mode, stop highlighting.
     _label_stack.highlight_label(-1);
     _label_stack.highlight_label(-1);
     on_leave_label(_highlighted_index);
     on_leave_label(_highlighted_index);
@@ -341,8 +416,8 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
       set_horizontal_scale(_drag_scale_start / ratio);
       set_horizontal_scale(_drag_scale_start / ratio);
     }
     }
     return TRUE;
     return TRUE;
-
-  } else if (_drag_mode == DM_new_guide_bar) {
+  }
+  else if (_drag_mode == DM_new_guide_bar) {
     // We haven't created the new guide bar yet; we won't until the mouse
     // We haven't created the new guide bar yet; we won't until the mouse
     // comes within the graph's region.
     // comes within the graph's region.
     if (graph_x >= 0 && graph_x < get_xsize()) {
     if (graph_x >= 0 && graph_x < get_xsize()) {
@@ -356,7 +431,17 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
     return TRUE;
     return TRUE;
   }
   }
 
 
-  return GtkStatsGraph::handle_motion(widget, graph_x, graph_y);
+  return GtkStatsGraph::handle_motion(graph_x, graph_y);
+}
+
+/**
+ * Called when the mouse has left the graph window.
+ */
+gboolean GtkStatsPianoRoll::
+handle_leave() {
+  _label_stack.highlight_label(-1);
+  on_leave_label(_highlighted_index);
+  return TRUE;
 }
 }
 
 
 /**
 /**
@@ -364,14 +449,14 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
  * -1.
  * -1.
  */
  */
 int GtkStatsPianoRoll::
 int GtkStatsPianoRoll::
-get_collector_under_pixel(int xpoint, int ypoint) {
+get_collector_under_pixel(int xpoint, int ypoint) const {
   if (_label_stack.get_num_labels() == 0) {
   if (_label_stack.get_num_labels() == 0) {
     return -1;
     return -1;
   }
   }
 
 
   // Assume all of the labels are the same height.
   // Assume all of the labels are the same height.
   int height = _label_stack.get_label_height(0);
   int height = _label_stack.get_label_height(0);
-  int row = (get_ysize() - ypoint) / height;
+  int row = (get_ysize() - ypoint) / (height * _cr_scale);
   if (row >= 0 && row < _label_stack.get_num_labels()) {
   if (row >= 0 && row < _label_stack.get_num_labels()) {
     return _label_stack.get_label_collector_index(row);
     return _label_stack.get_label_collector_index(row);
   } else  {
   } else  {
@@ -411,7 +496,7 @@ draw_guide_bar(cairo_t *cr, const PStatGraph::GuideBar &bar) {
       cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]);
       cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]);
       break;
       break;
 
 
-    case GBS_normal:
+    default:
       cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]);
       cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]);
       break;
       break;
     }
     }
@@ -452,7 +537,7 @@ draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar) {
     cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]);
     cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]);
     break;
     break;
 
 
-  case GBS_normal:
+  default:
     cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]);
     cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]);
     break;
     break;
   }
   }
@@ -460,13 +545,13 @@ draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar) {
   int x = height_to_pixel(bar._height);
   int x = height_to_pixel(bar._height);
   const std::string &label = bar._label;
   const std::string &label = bar._label;
 
 
-  PangoLayout *layout = gtk_widget_create_pango_layout(_window, label.c_str());
+  PangoLayout *layout = gtk_widget_create_pango_layout(_scale_area, label.c_str());
   int width, height;
   int width, height;
   pango_layout_get_pixel_size(layout, &width, &height);
   pango_layout_get_pixel_size(layout, &width, &height);
 
 
   if (bar._style != GBS_user) {
   if (bar._style != GBS_user) {
-    double from_height = pixel_to_height(x - width);
-    double to_height = pixel_to_height(x + width);
+    double from_height = pixel_to_height(x - width * _cr_scale);
+    double to_height = pixel_to_height(x + width * _cr_scale);
     if (find_user_guide_bar(from_height, to_height) >= 0) {
     if (find_user_guide_bar(from_height, to_height) >= 0) {
       // Omit the label: there's a user-defined guide bar in the same space.
       // Omit the label: there's a user-defined guide bar in the same space.
       g_object_unref(layout);
       g_object_unref(layout);
@@ -478,6 +563,8 @@ draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar) {
     // Now convert our x to a coordinate within our drawing area.
     // Now convert our x to a coordinate within our drawing area.
     int junk_y;
     int junk_y;
 
 
+    x /= _cr_scale;
+
     // The x coordinate comes from the graph_window.
     // The x coordinate comes from the graph_window.
     gtk_widget_translate_coordinates(_graph_window, _scale_area,
     gtk_widget_translate_coordinates(_graph_window, _scale_area,
              x, 0,
              x, 0,

+ 9 - 5
pandatool/src/gtk-stats/gtkStatsPianoRoll.h

@@ -39,6 +39,8 @@ public:
 
 
   virtual void set_time_units(int unit_mask);
   virtual void set_time_units(int unit_mask);
   virtual void on_click_label(int collector_index);
   virtual void on_click_label(int collector_index);
+  virtual void on_popup_label(int collector_index);
+  virtual std::string get_label_tooltip(int collector_index) const;
   void set_horizontal_scale(double time_width);
   void set_horizontal_scale(double time_width);
 
 
 protected:
 protected:
@@ -50,15 +52,17 @@ protected:
   virtual void idle();
   virtual void idle();
 
 
   virtual void additional_graph_window_paint(cairo_t *cr);
   virtual void additional_graph_window_paint(cairo_t *cr);
+  virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const;
   virtual DragMode consider_drag_start(int graph_x, int graph_y);
   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);
+  virtual gboolean handle_button_press(int graph_x, int graph_y,
+                                       bool double_click, int button);
+  virtual gboolean handle_button_release(int graph_x, int graph_y);
+  virtual gboolean handle_motion(int graph_x, int graph_y);
+  virtual gboolean handle_leave();
 
 
 private:
 private:
-  int get_collector_under_pixel(int xpoint, int ypoint);
+  int get_collector_under_pixel(int xpoint, int ypoint) const;
   void update_labels();
   void update_labels();
   void draw_guide_bar(cairo_t *cr, const PStatGraph::GuideBar &bar);
   void draw_guide_bar(cairo_t *cr, const PStatGraph::GuideBar &bar);
   void draw_guide_labels(cairo_t *cr);
   void draw_guide_labels(cairo_t *cr);

+ 147 - 63
pandatool/src/gtk-stats/gtkStatsStripChart.cxx

@@ -26,12 +26,9 @@ GtkStatsStripChart::
 GtkStatsStripChart(GtkStatsMonitor *monitor, int thread_index,
 GtkStatsStripChart(GtkStatsMonitor *monitor, int thread_index,
                    int collector_index, bool show_level) :
                    int collector_index, bool show_level) :
   PStatStripChart(monitor,
   PStatStripChart(monitor,
-                  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),
-  GtkStatsGraph(monitor)
+                  show_level ? monitor->get_level_view(0, thread_index) : monitor->get_view(thread_index),
+                  thread_index, collector_index, 0, 0),
+  GtkStatsGraph(monitor, true)
 {
 {
   if (show_level) {
   if (show_level) {
     // If it's a level-type graph, show the appropriate units.
     // If it's a level-type graph, show the appropriate units.
@@ -48,38 +45,35 @@ GtkStatsStripChart(GtkStatsMonitor *monitor, int thread_index,
 
 
   // Put some stuff on top of the graph.
   // Put some stuff on top of the graph.
   _top_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
   _top_hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
-  gtk_box_pack_start(GTK_BOX(_graph_vbox), _top_hbox,
-         FALSE, FALSE, 0);
+  gtk_box_pack_start(GTK_BOX(_graph_vbox), _top_hbox, FALSE, FALSE, 0);
 
 
   _smooth_check_box = gtk_check_button_new_with_label("Smooth");
   _smooth_check_box = gtk_check_button_new_with_label("Smooth");
   g_signal_connect(G_OBJECT(_smooth_check_box), "toggled",
   g_signal_connect(G_OBJECT(_smooth_check_box), "toggled",
-       G_CALLBACK(toggled_callback), this);
+                   G_CALLBACK(toggled_callback), this);
 
 
   _total_label = gtk_label_new("");
   _total_label = gtk_label_new("");
-  gtk_box_pack_start(GTK_BOX(_top_hbox), _smooth_check_box,
-         FALSE, FALSE, 0);
-  gtk_box_pack_end(GTK_BOX(_top_hbox), _total_label,
-       FALSE, FALSE, 0);
+  gtk_box_pack_start(GTK_BOX(_top_hbox), _smooth_check_box, FALSE, FALSE, 0);
+  gtk_box_pack_end(GTK_BOX(_top_hbox), _total_label, FALSE, FALSE, 0);
 
 
   // Add a DrawingArea widget to the right of the graph, to display all of the
   // Add a DrawingArea widget to the right of the graph, to display all of the
   // scale units.
   // scale units.
   _scale_area = gtk_drawing_area_new();
   _scale_area = gtk_drawing_area_new();
   g_signal_connect(G_OBJECT(_scale_area), "draw",
   g_signal_connect(G_OBJECT(_scale_area), "draw",
-       G_CALLBACK(draw_callback), this);
-  gtk_box_pack_start(GTK_BOX(_graph_hbox), _scale_area,
-         FALSE, FALSE, 0);
+                   G_CALLBACK(draw_callback), this);
+  gtk_box_pack_start(GTK_BOX(_graph_hbox), _scale_area, FALSE, FALSE, 0);
 
 
   // Make it wide enough to display a typical label.
   // Make it wide enough to display a typical label.
   {
   {
-    PangoLayout *layout = gtk_widget_create_pango_layout(_window, "99 ms");
+    PangoLayout *layout = gtk_widget_create_pango_layout(_scale_area, "99 ms");
     int width, height;
     int width, height;
     pango_layout_get_pixel_size(layout, &width, &height);
     pango_layout_get_pixel_size(layout, &width, &height);
     gtk_widget_set_size_request(_scale_area, width, 0);
     gtk_widget_set_size_request(_scale_area, width, 0);
     g_object_unref(layout);
     g_object_unref(layout);
   }
   }
 
 
-  gtk_widget_set_size_request(_graph_window, default_strip_chart_width,
-            default_strip_chart_height);
+  gtk_widget_set_size_request(_graph_window,
+    default_strip_chart_width * monitor->get_resolution() / 96,
+    default_strip_chart_height * monitor->get_resolution() / 96);
 
 
   gtk_widget_show_all(_window);
   gtk_widget_show_all(_window);
   gtk_widget_show(_window);
   gtk_widget_show(_window);
@@ -87,7 +81,7 @@ GtkStatsStripChart(GtkStatsMonitor *monitor, int thread_index,
   // Allow the window to be resized as small as the user likes.  We have to do
   // 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
   // this after the window has been shown; otherwise, it will affect the
   // window's initial size.
   // window's initial size.
-  gtk_widget_set_size_request(_window, 0, 0);
+  gtk_widget_set_size_request(_graph_window, 0, 0);
 
 
   clear_region();
   clear_region();
 }
 }
@@ -108,7 +102,7 @@ new_collector(int collector_index) {
 }
 }
 
 
 /**
 /**
- * Called as each frame's data is made available.  There is no gurantee the
+ * 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
  * 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
  * monitor should be prepared to accept frames received out-of-order or
  * missing.
  * missing.
@@ -138,7 +132,9 @@ new_data(int thread_index, int frame_number) {
  */
  */
 void GtkStatsStripChart::
 void GtkStatsStripChart::
 force_redraw() {
 force_redraw() {
-  PStatStripChart::force_redraw();
+  if (_cr) {
+    PStatStripChart::force_redraw();
+  }
 }
 }
 
 
 /**
 /**
@@ -209,6 +205,66 @@ on_click_label(int collector_index) {
   }
   }
 }
 }
 
 
+/**
+ * Called when the user right-clicks on a label.
+ */
+void GtkStatsStripChart::
+on_popup_label(int collector_index) {
+  GtkWidget *menu = gtk_menu_new();
+  _popup_index = collector_index;
+
+  std::string label = get_label_tooltip(collector_index);
+  if (!label.empty()) {
+    GtkWidget *menu_item = gtk_menu_item_new_with_label(label.c_str());
+    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+    gtk_widget_set_sensitive(menu_item, FALSE);
+  }
+
+  {
+    GtkWidget *menu_item = gtk_menu_item_new_with_label("Set as Focus");
+    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+
+    if (collector_index == 0 && get_collector_index() == 0) {
+      gtk_widget_set_sensitive(menu_item, FALSE);
+    } else {
+      g_signal_connect(G_OBJECT(menu_item), "activate",
+        G_CALLBACK(+[] (GtkWidget *widget, gpointer data) {
+          GtkStatsStripChart *self = (GtkStatsStripChart *)data;
+          self->set_collector_index(self->_popup_index);
+        }),
+        this);
+    }
+  }
+
+  {
+    const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({
+      _thread_index, collector_index,
+      GtkStatsMonitor::CT_strip_chart, get_view().get_show_level(),
+    });
+
+    GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Strip Chart");
+    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+    g_signal_connect(G_OBJECT(menu_item), "activate",
+                     G_CALLBACK(GtkStatsMonitor::menu_activate),
+                     (void *)menu_def);
+  }
+
+  if (!get_view().get_show_level()) {
+    const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({
+      _thread_index, collector_index, GtkStatsMonitor::CT_flame_graph,
+    });
+
+    GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Flame Graph");
+    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+    g_signal_connect(G_OBJECT(menu_item), "activate",
+                     G_CALLBACK(GtkStatsMonitor::menu_activate),
+                     (void *)menu_def);
+  }
+
+  gtk_widget_show_all(menu);
+  gtk_menu_popup_at_pointer(GTK_MENU(menu), nullptr);
+}
+
 /**
 /**
  * Called when the mouse hovers over a label, and should return the text that
  * Called when the mouse hovers over a label, and should return the text that
  * should appear on the tooltip.
  * should appear on the tooltip.
@@ -263,8 +319,9 @@ copy_region(int start_x, int end_x, int dest_x) {
   // We are not allowed to copy a surface onto itself, so we have to create a
   // We are not allowed to copy a surface onto itself, so we have to create a
   // temporary surface to copy to.
   // temporary surface to copy to.
   end_x = std::min(end_x, get_xsize());
   end_x = std::min(end_x, get_xsize());
+  GdkWindow *window = gtk_widget_get_window(_graph_window);
   cairo_surface_t *temp_surface =
   cairo_surface_t *temp_surface =
-    cairo_image_surface_create(CAIRO_FORMAT_RGB24, end_x - start_x, get_ysize());
+    gdk_window_create_similar_image_surface(window, CAIRO_FORMAT_RGB24, end_x - start_x, get_ysize(), 1);
   {
   {
     cairo_t *temp_cr = cairo_create(temp_surface);
     cairo_t *temp_cr = cairo_create(temp_surface);
     cairo_set_source_surface(temp_cr, _cr_surface, -start_x, 0);
     cairo_set_source_surface(temp_cr, _cr_surface, -start_x, 0);
@@ -278,11 +335,7 @@ copy_region(int start_x, int end_x, int dest_x) {
 
 
   cairo_surface_destroy(temp_surface);
   cairo_surface_destroy(temp_surface);
 
 
-  GdkWindow *window = gtk_widget_get_window(_graph_window);
-  GdkRectangle rect = {
-    dest_x, 0, end_x - start_x, get_ysize()
-  };
-  gdk_window_invalidate_rect(window, &rect, FALSE);
+  gdk_window_invalidate_rect(window, nullptr, FALSE);
 }
 }
 
 
 /**
 /**
@@ -356,8 +409,9 @@ end_draw(int from_x, int to_x) {
   }
   }
 
 
   GdkWindow *window = gtk_widget_get_window(_graph_window);
   GdkWindow *window = gtk_widget_get_window(_graph_window);
+  int scale = gdk_window_get_scale_factor(window);
   GdkRectangle rect = {
   GdkRectangle rect = {
-    from_x, 0, to_x - from_x, get_ysize()
+    (from_x * scale) / scale, 0, (to_x - from_x) / scale, get_ysize() / scale
   };
   };
   gdk_window_invalidate_rect(window, &rect, FALSE);
   gdk_window_invalidate_rect(window, &rect, FALSE);
 }
 }
@@ -374,6 +428,18 @@ additional_graph_window_paint(cairo_t *cr) {
   }
   }
 }
 }
 
 
+/**
+ * Called when the mouse hovers over the graph, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string GtkStatsStripChart::
+get_graph_tooltip(int mouse_x, int mouse_y) const {
+  if (_highlighted_index != -1) {
+    return get_label_tooltip(_highlighted_index);
+  }
+  return std::string();
+}
+
 /**
 /**
  * Based on the mouse position within the graph window, look for draggable
  * Based on the mouse position within the graph window, look for draggable
  * things the mouse might be hovering over and return the appropriate DragMode
  * things the mouse might be hovering over and return the appropriate DragMode
@@ -409,19 +475,11 @@ void GtkStatsStripChart::
 set_drag_mode(GtkStatsGraph::DragMode drag_mode) {
 set_drag_mode(GtkStatsGraph::DragMode drag_mode) {
   GtkStatsGraph::set_drag_mode(drag_mode);
   GtkStatsGraph::set_drag_mode(drag_mode);
 
 
-  switch (_drag_mode) {
-  case DM_scale:
-  case DM_sizing:
-    // Disable smoothing for these expensive operations.
-    set_average_mode(false);
-    break;
-
-  default:
+  if (_drag_mode == DM_none) {
     // Restore smoothing according to the current setting of the check box.
     // Restore smoothing according to the current setting of the check box.
     bool active =
     bool active =
       gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(_smooth_check_box));
       gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(_smooth_check_box));
     set_average_mode(active);
     set_average_mode(active);
-    break;
   }
   }
 }
 }
 
 
@@ -429,10 +487,19 @@ set_drag_mode(GtkStatsGraph::DragMode drag_mode) {
  * Called when the mouse button is depressed within the graph window.
  * Called when the mouse button is depressed within the graph window.
  */
  */
 gboolean GtkStatsStripChart::
 gboolean GtkStatsStripChart::
-handle_button_press(GtkWidget *widget, int graph_x, int graph_y,
-        bool double_click) {
+handle_button_press(int graph_x, int graph_y, bool double_click, int button) {
   if (graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) {
   if (graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) {
-    if (double_click) {
+    int collector_index = get_collector_under_pixel(graph_x, graph_y);
+    if (button == 3) {
+      // Right-clicking on a color bar in the graph is the same as right-
+      // clicking on the corresponding label.
+      if (collector_index >= 0) {
+        on_popup_label(collector_index);
+        return TRUE;
+      }
+      return FALSE;
+    }
+    else if (double_click && button == 1) {
       // Double-clicking on a color bar in the graph is the same as double-
       // Double-clicking on a color bar in the graph is the same as double-
       // clicking on the corresponding label.
       // clicking on the corresponding label.
       on_click_label(get_collector_under_pixel(graph_x, graph_y));
       on_click_label(get_collector_under_pixel(graph_x, graph_y));
@@ -454,21 +521,21 @@ handle_button_press(GtkWidget *widget, int graph_x, int graph_y,
     return TRUE;
     return TRUE;
   }
   }
 
 
-  return GtkStatsGraph::handle_button_press(widget, graph_x, graph_y,
-              double_click);
+  return GtkStatsGraph::handle_button_press(graph_x, graph_y,
+                                            double_click, button);
 }
 }
 
 
 /**
 /**
  * Called when the mouse button is released within the graph window.
  * Called when the mouse button is released within the graph window.
  */
  */
 gboolean GtkStatsStripChart::
 gboolean GtkStatsStripChart::
-handle_button_release(GtkWidget *widget, int graph_x, int graph_y) {
+handle_button_release(int graph_x, int graph_y) {
   if (_drag_mode == DM_scale) {
   if (_drag_mode == DM_scale) {
     set_drag_mode(DM_none);
     set_drag_mode(DM_none);
     // ReleaseCapture();
     // ReleaseCapture();
-    return handle_motion(widget, graph_x, graph_y);
-
-  } else if (_drag_mode == DM_guide_bar) {
+    return handle_motion(graph_x, graph_y);
+  }
+  else if (_drag_mode == DM_guide_bar) {
     if (graph_y < 0 || graph_y >= get_ysize()) {
     if (graph_y < 0 || graph_y >= get_ysize()) {
       remove_user_guide_bar(_drag_guide_bar);
       remove_user_guide_bar(_drag_guide_bar);
     } else {
     } else {
@@ -476,17 +543,17 @@ handle_button_release(GtkWidget *widget, int graph_x, int graph_y) {
     }
     }
     set_drag_mode(DM_none);
     set_drag_mode(DM_none);
     // ReleaseCapture();
     // ReleaseCapture();
-    return handle_motion(widget, graph_x, graph_y);
+    return handle_motion(graph_x, graph_y);
   }
   }
 
 
-  return GtkStatsGraph::handle_button_release(widget, graph_x, graph_y);
+  return GtkStatsGraph::handle_button_release(graph_x, graph_y);
 }
 }
 
 
 /**
 /**
  * Called when the mouse is moved within the graph window.
  * Called when the mouse is moved within the graph window.
  */
  */
 gboolean GtkStatsStripChart::
 gboolean GtkStatsStripChart::
-handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
+handle_motion(int graph_x, int graph_y) {
   if (_drag_mode == DM_none && _potential_drag_mode == DM_none &&
   if (_drag_mode == DM_none && _potential_drag_mode == DM_none &&
       graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) {
       graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) {
     // When the mouse is over a color bar, highlight it.
     // When the mouse is over a color bar, highlight it.
@@ -501,13 +568,18 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
   }
   }
 
 
   if (_drag_mode == DM_scale) {
   if (_drag_mode == DM_scale) {
-    double ratio = 1.0f - ((double)graph_y / (double)get_ysize());
-    if (ratio > 0.0f) {
-      set_vertical_scale(_drag_scale_start / ratio);
+    double ratio = 1.0 - ((double)graph_y / (double)get_ysize());
+    if (ratio > 0.0) {
+      double new_scale = _drag_scale_start / ratio;
+      if (!IS_NEARLY_EQUAL(get_vertical_scale(), new_scale)) {
+        // Disable smoothing while we do this expensive operation.
+        set_average_mode(false);
+        set_vertical_scale(_drag_scale_start / ratio);
+      }
     }
     }
     return TRUE;
     return TRUE;
-
-  } else if (_drag_mode == DM_new_guide_bar) {
+  }
+  else if (_drag_mode == DM_new_guide_bar) {
     // We haven't created the new guide bar yet; we won't until the mouse
     // We haven't created the new guide bar yet; we won't until the mouse
     // comes within the graph's region.
     // comes within the graph's region.
     if (graph_y >= 0 && graph_y < get_ysize()) {
     if (graph_y >= 0 && graph_y < get_ysize()) {
@@ -515,13 +587,23 @@ handle_motion(GtkWidget *widget, int graph_x, int graph_y) {
       _drag_guide_bar = add_user_guide_bar(pixel_to_height(graph_y));
       _drag_guide_bar = add_user_guide_bar(pixel_to_height(graph_y));
       return TRUE;
       return TRUE;
     }
     }
-
-  } else if (_drag_mode == DM_guide_bar) {
+  }
+  else if (_drag_mode == DM_guide_bar) {
     move_user_guide_bar(_drag_guide_bar, pixel_to_height(graph_y));
     move_user_guide_bar(_drag_guide_bar, pixel_to_height(graph_y));
     return TRUE;
     return TRUE;
   }
   }
 
 
-  return GtkStatsGraph::handle_motion(widget, graph_x, graph_y);
+  return GtkStatsGraph::handle_motion(graph_x, graph_y);
+}
+
+/**
+ * Called when the mouse has left the graph window.
+ */
+gboolean GtkStatsStripChart::
+handle_leave() {
+  _label_stack.highlight_label(-1);
+  on_leave_label(_highlighted_index);
+  return TRUE;
 }
 }
 
 
 /**
 /**
@@ -543,7 +625,7 @@ draw_guide_bar(cairo_t *cr, int from_x, int to_x,
       cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]);
       cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]);
       break;
       break;
 
 
-    case GBS_normal:
+    default:
       cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]);
       cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]);
       break;
       break;
     }
     }
@@ -593,7 +675,7 @@ draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar, int last_y) {
     cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]);
     cairo_set_source_rgb(cr, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2]);
     break;
     break;
 
 
-  case GBS_normal:
+  default:
     cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]);
     cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]);
     break;
     break;
   }
   }
@@ -601,13 +683,13 @@ draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar, int last_y) {
   int y = height_to_pixel(bar._height);
   int y = height_to_pixel(bar._height);
   const std::string &label = bar._label;
   const std::string &label = bar._label;
 
 
-  PangoLayout *layout = gtk_widget_create_pango_layout(_window, label.c_str());
+  PangoLayout *layout = gtk_widget_create_pango_layout(_scale_area, label.c_str());
   int width, height;
   int width, height;
   pango_layout_get_pixel_size(layout, &width, &height);
   pango_layout_get_pixel_size(layout, &width, &height);
 
 
   if (bar._style != GBS_user) {
   if (bar._style != GBS_user) {
-    double from_height = pixel_to_height(y + height);
-    double to_height = pixel_to_height(y - height);
+    double from_height = pixel_to_height(y + height * _cr_scale);
+    double to_height = pixel_to_height(y - height * _cr_scale);
     if (find_user_guide_bar(from_height, to_height) >= 0) {
     if (find_user_guide_bar(from_height, to_height) >= 0) {
       // Omit the label: there's a user-defined guide bar in the same space.
       // Omit the label: there's a user-defined guide bar in the same space.
       g_object_unref(layout);
       g_object_unref(layout);
@@ -619,6 +701,8 @@ draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar, int last_y) {
     // Now convert our y to a coordinate within our drawing area.
     // Now convert our y to a coordinate within our drawing area.
     int junk_x;
     int junk_x;
 
 
+    y /= _cr_scale;
+
     // The y coordinate comes from the graph_window.
     // The y coordinate comes from the graph_window.
     gtk_widget_translate_coordinates(_graph_window, _scale_area,
     gtk_widget_translate_coordinates(_graph_window, _scale_area,
              0, y,
              0, y,

+ 9 - 4
pandatool/src/gtk-stats/gtkStatsStripChart.h

@@ -41,6 +41,7 @@ public:
   virtual void set_time_units(int unit_mask);
   virtual void set_time_units(int unit_mask);
   virtual void set_scroll_speed(double scroll_speed);
   virtual void set_scroll_speed(double scroll_speed);
   virtual void on_click_label(int collector_index);
   virtual void on_click_label(int collector_index);
+  virtual void on_popup_label(int collector_index);
   virtual std::string get_label_tooltip(int collector_index) const;
   virtual std::string get_label_tooltip(int collector_index) const;
   void set_vertical_scale(double value_height);
   void set_vertical_scale(double value_height);
 
 
@@ -56,13 +57,15 @@ protected:
   virtual void end_draw(int from_x, int to_x);
   virtual void end_draw(int from_x, int to_x);
 
 
   virtual void additional_graph_window_paint(cairo_t *cr);
   virtual void additional_graph_window_paint(cairo_t *cr);
+  virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const;
   virtual DragMode consider_drag_start(int graph_x, int graph_y);
   virtual DragMode consider_drag_start(int graph_x, int graph_y);
   virtual void set_drag_mode(DragMode drag_mode);
   virtual void set_drag_mode(DragMode drag_mode);
 
 
-  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);
+  virtual gboolean handle_button_press(int graph_x, int graph_y,
+                                       bool double_click, int button);
+  virtual gboolean handle_button_release(int graph_x, int graph_y);
+  virtual gboolean handle_motion(int graph_x, int graph_y);
+  virtual gboolean handle_leave();
 
 
 private:
 private:
   void draw_guide_bar(cairo_t *cr, int from_x, int to_x,
   void draw_guide_bar(cairo_t *cr, int from_x, int to_x,
@@ -79,6 +82,8 @@ private:
   GtkWidget *_top_hbox;
   GtkWidget *_top_hbox;
   GtkWidget *_smooth_check_box;
   GtkWidget *_smooth_check_box;
   GtkWidget *_total_label;
   GtkWidget *_total_label;
+
+  int _popup_index = -1;
 };
 };
 
 
 #endif
 #endif

+ 812 - 0
pandatool/src/gtk-stats/gtkStatsTimeline.cxx

@@ -0,0 +1,812 @@
+/**
+ * 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 gtkStatsTimeline.cxx
+ * @author rdb
+ * @date 2022-02-17
+ */
+
+#include "gtkStatsTimeline.h"
+#include "gtkStatsMonitor.h"
+#include "numeric_types.h"
+#include "gtkStatsLabelStack.h"
+
+static const int default_timeline_width = 1000;
+static const int default_timeline_height = 300;
+
+/**
+ *
+ */
+GtkStatsTimeline::
+GtkStatsTimeline(GtkStatsMonitor *monitor) :
+  PStatTimeline(monitor, 0, 0),
+  GtkStatsGraph(monitor, false)
+{
+  // Let's show the units on the guide bar labels.  There's room.
+  set_guide_bar_units(get_guide_bar_units() | GBU_show_units);
+
+  // 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(scale_area_draw_callback), this);
+  gtk_box_pack_start(GTK_BOX(_graph_vbox), _scale_area, FALSE, FALSE, 0);
+
+  // It should be large enough to display the labels.
+  {
+    PangoLayout *layout = gtk_widget_create_pango_layout(_scale_area, "0123456789 ms");
+    int width, height;
+    pango_layout_get_pixel_size(layout, &width, &height);
+    gtk_widget_set_size_request(_scale_area, 0, height + _pixel_scale / 2);
+    g_object_unref(layout);
+  }
+
+  // Add a drawing area to the left of the graph to show the thread labels.
+  _thread_area = gtk_drawing_area_new();
+  gtk_box_pack_start(GTK_BOX(_graph_hbox), _thread_area, FALSE, FALSE, 0);
+  gtk_box_reorder_child(GTK_BOX(_graph_hbox), _thread_area, 0);
+  g_signal_connect(G_OBJECT(_thread_area), "draw",
+                   G_CALLBACK(thread_area_draw_callback), this);
+
+  // Listen for mouse wheel and keyboard events.
+  gtk_widget_add_events(_graph_window, GDK_SCROLL_MASK |
+                                       GDK_KEY_PRESS_MASK |
+                                       GDK_KEY_RELEASE_MASK);
+  gtk_widget_set_can_focus(_graph_window, TRUE);
+  g_signal_connect(G_OBJECT(_graph_window), "scroll_event",
+                   G_CALLBACK(scroll_callback), this);
+  g_signal_connect(G_OBJECT(_graph_window), "key_press_event",
+                   G_CALLBACK(key_press_callback), this);
+  g_signal_connect(G_OBJECT(_graph_window), "key_release_event",
+                   G_CALLBACK(key_release_callback), this);
+
+  int min_height = 0;
+  if (!_threads.empty()) {
+    int num_rows = _threads.back()._row_offset + _threads.back()._rows.size();
+    double height = row_to_pixel(num_rows) + _pixel_scale * 2.5;
+    min_height = height / _cr_scale;
+  }
+
+  gtk_widget_set_size_request(_graph_window,
+    default_timeline_width * monitor->get_resolution() / 96,
+    std::max(min_height, (int)(default_timeline_height * monitor->get_resolution() / 96)));
+
+  gtk_window_set_title(GTK_WINDOW(_window), "Timeline");
+
+  _grid_pattern = cairo_pattern_create_rgb(0xdd / 255.0, 0xdd / 255.0, 0xdd / 255.0);
+
+  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(_graph_window, 0, min_height);
+
+  clear_region();
+}
+
+/**
+ *
+ */
+GtkStatsTimeline::
+~GtkStatsTimeline() {
+  cairo_pattern_destroy(_grid_pattern);
+}
+
+/**
+ * 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 GtkStatsTimeline::
+new_data(int thread_index, int frame_number) {
+  PStatTimeline::new_data(thread_index, frame_number);
+}
+
+/**
+ * Called when it is necessary to redraw the entire graph.
+ */
+void GtkStatsTimeline::
+force_redraw() {
+  assert(_cr);
+  if (_cr) {
+    PStatTimeline::force_redraw();
+  }
+}
+
+/**
+ * Called when the user has resized the window, forcing a resize of the graph.
+ */
+void GtkStatsTimeline::
+changed_graph_size(int graph_xsize, int graph_ysize) {
+  PStatTimeline::changed_size(graph_xsize, graph_ysize);
+}
+
+/**
+ * Erases the chart area.
+ */
+void GtkStatsTimeline::
+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 GtkStatsTimeline::
+begin_draw() {
+}
+
+/**
+ * Draws a horizontal separator.
+ */
+void GtkStatsTimeline::
+draw_separator(int row) {
+  cairo_set_source(_cr, _grid_pattern);
+  cairo_rectangle(_cr, 0, (row_to_pixel(row) + row_to_pixel(row + 1)) / 2.0,
+                  get_xsize(), _pixel_scale * 1 / 3);
+  cairo_fill(_cr);
+}
+
+/**
+ * Draws a vertical guide bar.  If the row is -1, draws it in all rows.
+ */
+void GtkStatsTimeline::
+draw_guide_bar(int x, GuideBarStyle style) {
+  double width = _pixel_scale / 3.0;
+  if (style == GBS_frame) {
+    width *= 2;
+  }
+
+  cairo_set_source(_cr, _grid_pattern);
+  cairo_rectangle(_cr, x - width / 2.0, 0, width, get_ysize());
+  cairo_fill(_cr);
+}
+
+/**
+ * Draws a single bar in the chart for the indicated row, in the color for the
+ * given collector, for the indicated horizontal pixel range.
+ */
+void GtkStatsTimeline::
+draw_bar(int row, int from_x, int to_x, int collector_index,
+         const std::string &collector_name) {
+  int top = row_to_pixel(row);
+  int bottom = row_to_pixel(row + 1);
+  int scale = _pixel_scale;
+
+  bool is_highlighted = row == _highlighted_row && _highlighted_x >= from_x && _highlighted_x < to_x;
+  cairo_set_source(_cr, get_collector_pattern(collector_index, is_highlighted));
+
+  if (to_x < from_x + 1) {
+    // Too tiny to draw.
+  }
+  else if (to_x < from_x + scale) {
+    // It's just a tiny sliver.  This is a more reliable way to draw it.
+    cairo_rectangle(_cr, from_x, top, to_x - from_x, bottom - top);
+    cairo_fill(_cr);
+  }
+  else {
+    int left = std::max(from_x, -scale - 1);
+    int right = std::min(std::max(to_x, from_x + 1), get_xsize() + scale);
+
+    double radius = std::min((double)scale, (right - left) / 2.0);
+    cairo_new_sub_path(_cr);
+    cairo_arc(_cr, right - radius, top + radius, radius, -0.5 * M_PI, 0.0);
+    cairo_arc(_cr, right - radius, bottom - radius, radius, 0.0, 0.5 * M_PI);
+    cairo_arc(_cr, left + radius, bottom - radius, radius, 0.5 * M_PI, M_PI);
+    cairo_arc(_cr, left + radius, top + radius, radius, M_PI, 1.5 * M_PI);
+    cairo_close_path(_cr);
+    cairo_fill(_cr);
+
+    if ((to_x - from_x) >= scale * 4) {
+      // Only bother drawing the text if we've got some space to draw on.
+      // Choose a suitable foreground color.
+      LRGBColor fg = get_collector_text_color(collector_index, is_highlighted);
+      cairo_set_source_rgb(_cr, fg[0], fg[1], fg[2]);
+
+      // Make sure that the text doesn't run off the chart.
+      int text_width, text_height;
+      PangoLayout *layout = gtk_widget_create_pango_layout(_graph_window, collector_name.c_str());
+      pango_layout_set_attributes(layout, _pango_attrs);
+      pango_layout_set_height(layout, -1);
+      pango_layout_get_pixel_size(layout, &text_width, &text_height);
+
+      double center = (from_x + to_x) / 2.0;
+      double text_left = std::max(from_x, 0) + scale / 2.0;
+      double text_right = std::min(to_x, get_xsize()) - scale / 2.0;
+      double text_top = top + (bottom - top - text_height) / 2.0;
+
+      if (text_width >= text_right - text_left) {
+        if (text_right - text_left < scale * 6) {
+          // It's a really tiny space.  Draw a single letter.
+          pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER);
+          pango_layout_set_text(layout, collector_name.c_str(), 1);
+        } else {
+          // It's going to be tricky to fit it, let pango figure it out.
+          pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END);
+        }
+        pango_layout_set_width(layout, (text_right - text_left) * PANGO_SCALE);
+        cairo_move_to(_cr, text_left, text_top);
+      }
+      else if (center - text_width / 2.0 < 0.0) {
+        // Put it against the left-most edge.
+        cairo_move_to(_cr, scale, text_top);
+      }
+      else if (center + text_width / 2.0 >= get_xsize()) {
+        // Put it against the right-most edge.
+        cairo_move_to(_cr, get_xsize() - scale - text_width, text_top);
+      }
+      else {
+        // It fits just fine, center it.
+        cairo_move_to(_cr, center - text_width / 2.0, text_top);
+      }
+
+      pango_cairo_show_layout(_cr, layout);
+      g_object_unref(layout);
+    }
+  }
+}
+
+/**
+ * Called after all the bars have been drawn, this triggers a refresh event to
+ * draw it to the window.
+ */
+void GtkStatsTimeline::
+end_draw() {
+  gtk_widget_queue_draw(_graph_window);
+
+  if (_threads_changed) {
+    // Make sure the window is large enough to fit all of the threads.
+    int num_rows = _threads.back()._row_offset + _threads.back()._rows.size();
+    double height = row_to_pixel(num_rows) + _pixel_scale * 2.5;
+    gtk_widget_set_size_request(_graph_window, 0, height / _cr_scale);
+
+    // Calculate the size of the thread area.
+    PangoLayout *layout = gtk_widget_create_pango_layout(_thread_area, "");
+
+    int max_width = 0;
+    for (const ThreadRow &thread_row : _threads) {
+      pango_layout_set_text(layout, thread_row._label.c_str(), thread_row._label.size());
+
+      int width, height;
+      pango_layout_get_pixel_size(layout, &width, &height);
+
+      if (width > max_width) {
+        max_width = width;
+      }
+    }
+
+    gtk_widget_set_size_request(_thread_area, max_width + _pixel_scale * 2, 0);
+    g_object_unref(layout);
+    gtk_widget_queue_draw(_thread_area);
+    _threads_changed = false;
+  }
+
+  if (_guide_bars_changed) {
+    gtk_widget_queue_draw(_scale_area);
+    _guide_bars_changed = false;
+  }
+}
+
+/**
+ * Called at the end of the draw cycle.
+ */
+void GtkStatsTimeline::
+idle() {
+}
+
+/**
+ * Overridden by a derived class to implement an animation.  If it returns
+ * false, the animation timer is stopped.
+ */
+bool GtkStatsTimeline::
+animate(double time, double dt) {
+  return PStatTimeline::animate(time, dt);
+}
+
+/**
+ * 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 GtkStatsTimeline::
+additional_graph_window_paint(cairo_t *cr) {
+}
+
+/**
+ * Called when the mouse hovers over the graph, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string GtkStatsTimeline::
+get_graph_tooltip(int mouse_x, int mouse_y) const {
+  return PStatTimeline::get_bar_tooltip(pixel_to_row(mouse_y), mouse_x);
+}
+
+/**
+ * Based on the mouse position within the graph window, look for draggable
+ * things the mouse might be hovering over and return the appropriate DragMode
+ * enum or DM_none if nothing is indicated.
+ */
+GtkStatsGraph::DragMode GtkStatsTimeline::
+consider_drag_start(int graph_x, int graph_y) {
+  return GtkStatsGraph::consider_drag_start(graph_x, graph_y);
+}
+
+/**
+ * Called when the mouse button is depressed within the graph window.
+ */
+gboolean GtkStatsTimeline::
+handle_button_press(int graph_x, int graph_y, bool double_click, int button) {
+  if (graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) {
+    if (button == 3) {
+      // Right-clicking a color bar brings up a context menu.
+      int row = pixel_to_row(graph_y);
+
+      ColorBar bar;
+      if (find_bar(row, graph_x, bar)) {
+        GtkWidget *menu = gtk_menu_new();
+        _popup_bar = bar;
+
+        std::string label = get_bar_tooltip(row, graph_x);
+        if (!label.empty()) {
+          GtkWidget *menu_item = gtk_menu_item_new_with_label(label.c_str());
+          gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+          gtk_widget_set_sensitive(menu_item, FALSE);
+        }
+
+        {
+          GtkWidget *menu_item = gtk_menu_item_new_with_label("Zoom To");
+          gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+          g_signal_connect(G_OBJECT(menu_item), "activate",
+            G_CALLBACK(+[] (GtkWidget *widget, gpointer data) {
+              GtkStatsTimeline *self = (GtkStatsTimeline *)data;
+              const ColorBar &bar = self->_popup_bar;
+              double width = bar._end - bar._start;
+              self->zoom_to(width * 1.5, (bar._end + bar._start) / 2.0);
+              self->scroll_to(bar._start - width / 4.0);
+              self->start_animation();
+            }),
+            this);
+        }
+
+        {
+          const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({
+            bar._thread_index, bar._collector_index,
+            GtkStatsMonitor::CT_strip_chart, false,
+          });
+
+          GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Strip Chart");
+          gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+          g_signal_connect(G_OBJECT(menu_item), "activate",
+                           G_CALLBACK(GtkStatsMonitor::menu_activate),
+                           (void *)menu_def);
+        }
+
+        {
+          const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({
+            bar._thread_index, bar._collector_index,
+            GtkStatsMonitor::CT_flame_graph,
+          });
+
+          GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Flame Graph");
+          gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+          g_signal_connect(G_OBJECT(menu_item), "activate",
+                           G_CALLBACK(GtkStatsMonitor::menu_activate),
+                           (void *)menu_def);
+        }
+
+        {
+          const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({
+            bar._thread_index, -1, GtkStatsMonitor::CT_piano_roll,
+          });
+
+          GtkWidget *menu_item = gtk_menu_item_new_with_label("Open Piano Roll");
+          gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+          g_signal_connect(G_OBJECT(menu_item), "activate",
+                           G_CALLBACK(GtkStatsMonitor::menu_activate),
+                           (void *)menu_def);
+        }
+
+        gtk_widget_show_all(menu);
+        gtk_menu_popup_at_pointer(GTK_MENU(menu), nullptr);
+        return TRUE;
+      }
+      return FALSE;
+    }
+    else if (double_click && button == 1) {
+      // Double-clicking on a color bar in the graph will zoom the graph into
+      // that collector.
+      int row = pixel_to_row(graph_y);
+      ColorBar bar;
+      if (find_bar(row, graph_x, bar)) {
+        double width = bar._end - bar._start;
+        zoom_to(width * 1.5, pixel_to_timestamp(graph_x));
+        scroll_to(bar._start - width / 4.0);
+      } else {
+        // Double-clicking the white area zooms out.
+        _zoom_speed -= 100.0;
+      }
+      start_animation();
+    }
+
+    if (_potential_drag_mode == DM_none) {
+      set_drag_mode(DM_pan);
+      _drag_start_x = graph_x;
+      _scroll_speed = 0.0;
+      _zoom_center = pixel_to_timestamp(graph_x);
+      return TRUE;
+    }
+  }
+
+  return GtkStatsGraph::handle_button_press(graph_x, graph_y,
+                                            double_click, button);
+}
+
+/**
+ * Called when the mouse button is released within the graph window.
+ */
+gboolean GtkStatsTimeline::
+handle_button_release(int graph_x, int graph_y) {
+  if (_drag_mode == DM_scale) {
+    set_drag_mode(DM_none);
+    // ReleaseCapture();
+    return handle_motion(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(graph_x, graph_y);
+  }
+
+  return GtkStatsGraph::handle_button_release(graph_x, graph_y);
+}
+
+/**
+ * Called when the mouse is moved within the graph window.
+ */
+gboolean GtkStatsTimeline::
+handle_motion(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.
+    int row = pixel_to_row(graph_y);
+    std::swap(_highlighted_x, graph_x);
+    std::swap(_highlighted_row, row);
+
+    if (row >= 0) {
+      PStatTimeline::force_redraw(row, graph_x, graph_x);
+    }
+    PStatTimeline::force_redraw(_highlighted_row, _highlighted_x, _highlighted_x);
+
+    if ((_keys_held & (F_w | F_s)) != 0) {
+      // Update the zoom center if we move the mouse while zooming with the
+      // keyboard.
+      _zoom_center = pixel_to_timestamp(graph_x);
+    }
+  }
+  else {
+    // If the mouse is in some drag mode, stop highlighting.
+    if (_highlighted_row != -1) {
+      int row = _highlighted_row;
+      _highlighted_row = -1;
+      PStatTimeline::force_redraw(row, _highlighted_x, _highlighted_x);
+    }
+  }
+
+  if (_drag_mode == DM_pan) {
+    int delta = _drag_start_x - graph_x;
+    _drag_start_x = graph_x;
+    set_horizontal_scroll(get_horizontal_scroll() + pixel_to_height(delta));
+    return 0;
+  }
+
+  return GtkStatsGraph::handle_motion(graph_x, graph_y);
+}
+
+/**
+ * Called when the mouse has left the graph window.
+ */
+gboolean GtkStatsTimeline::
+handle_leave() {
+  if (_highlighted_row != -1) {
+    int row = _highlighted_row;
+    _highlighted_row = -1;
+    PStatTimeline::force_redraw(row, _highlighted_x, _highlighted_x);
+  }
+  return TRUE;
+}
+
+/**
+ *
+ */
+gboolean GtkStatsTimeline::
+handle_scroll(int graph_x, int graph_y, double dx, double dy, bool ctrl_held) {
+  gboolean handled = FALSE;
+
+  if (ctrl_held && dy != 0.0) {
+    handled = TRUE;
+    zoom_by(dy, pixel_to_timestamp(graph_x));
+    start_animation();
+  }
+
+  if (dx != 0.0) {
+    _scroll_speed += dx * 10.0;
+    handled = TRUE;
+    start_animation();
+  }
+
+  return handled;
+}
+
+/**
+ *
+ */
+gboolean GtkStatsTimeline::
+handle_key(bool pressed, guint val, guint16 hw_code) {
+  // Accept WASD based on their position rather than their mapping
+  int flag = 0;
+  switch (hw_code) {
+  case 25:
+    flag = F_w;
+    break;
+  case 38:
+    flag = F_a;
+    break;
+  case 39:
+    flag = F_s;
+    break;
+  case 40:
+    flag = F_d;
+    break;
+  }
+  if (flag == 0) {
+    switch (val) {
+    case GDK_KEY_Left:
+      flag = F_left;
+      break;
+    case GDK_KEY_Right:
+      flag = F_right;
+      break;
+    case GDK_KEY_w:
+      flag = F_w;
+      break;
+    case GDK_KEY_a:
+      flag = F_a;
+      break;
+    case GDK_KEY_s:
+      flag = F_s;
+      break;
+    case GDK_KEY_d:
+      flag = F_d;
+      break;
+    }
+  }
+  if (flag != 0) {
+    if (pressed) {
+      if (flag & (F_w | F_s)) {
+        // Pfoo, GTK sure does make it hard to just get the cursor position.
+        GdkWindow *window = gtk_widget_get_window(_graph_window);
+        GdkDisplay *display = gdk_window_get_display(window);
+        GdkDeviceManager *device_manager = gdk_display_get_device_manager(display);
+        GdkDevice *device = gdk_device_manager_get_client_pointer(device_manager);
+        gint x, y;
+        gdk_window_get_device_position(window, device, &x, &y, nullptr);
+        _zoom_center = pixel_to_timestamp(x * _cr_scale);
+      }
+      if (_keys_held == 0) {
+        start_animation();
+      }
+      _keys_held |= flag;
+    }
+    else if (_keys_held != 0) {
+      _keys_held &= ~flag;
+    }
+    return TRUE;
+  }
+  return FALSE;
+}
+
+/**
+ * This is called during the servicing of the draw event.
+ */
+void GtkStatsTimeline::
+draw_guide_labels(cairo_t *cr) {
+  int num_guide_bars = get_num_guide_bars();
+  for (int i = 0; i < num_guide_bars; i++) {
+    draw_guide_label(cr, get_guide_bar(i));
+  }
+}
+
+/**
+ * Draws the text for the indicated guide bar label at the top of the graph.
+ */
+void GtkStatsTimeline::
+draw_guide_label(cairo_t *cr, const PStatGraph::GuideBar &bar) {
+  const std::string &label = bar._label;
+  if (label.empty()) {
+    return;
+  }
+
+  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_light_gray[0], rgb_light_gray[1], rgb_light_gray[2]);
+    break;
+
+  case GBS_frame:
+    cairo_set_source_rgb(cr, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2]);
+    break;
+  }
+
+  PangoLayout *layout = gtk_widget_create_pango_layout(_scale_area, label.c_str());
+
+  if (bar._style != GBS_frame) {
+    // Make the offsets slightly smaller.
+    PangoAttrList *attrs = pango_attr_list_new();
+    PangoAttribute *attr = pango_attr_scale_new(0.9);
+    attr->start_index = 0;
+    attr->end_index = -1;
+    pango_attr_list_insert(attrs, attr);
+    pango_layout_set_attributes(layout, attrs);
+    pango_attr_list_unref(attrs);
+  }
+
+  int width, height;
+  pango_layout_get_pixel_size(layout, &width, &height);
+
+  int x = timestamp_to_pixel(bar._height);
+  if (x >= 0 && x + width * _cr_scale < get_xsize()) {
+    // Now convert our x to a coordinate within our drawing area.
+    int junk_y;
+
+    x /= _cr_scale;
+
+    // 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);
+}
+
+/**
+ * This is called during the servicing of the draw event.
+ */
+void GtkStatsTimeline::
+draw_thread_labels(cairo_t *cr) {
+  for (const ThreadRow &thread_row : _threads) {
+    draw_thread_label(cr, thread_row);
+  }
+}
+
+/**
+ * Draws the text for the indicated thread on the side of the graph.
+ */
+void GtkStatsTimeline::
+draw_thread_label(cairo_t *cr, const ThreadRow &thread_row) {
+  int top = row_to_pixel(thread_row._row_offset);
+  if (top <= get_ysize()) {
+    // Now convert our y to a coordinate within our drawing area.
+    top /= _cr_scale;
+
+    // The y coordinate comes from the graph_window.
+    int junk_x;
+    gtk_widget_translate_coordinates(_graph_window, _thread_area,
+                                     0, top, &junk_x, &top);
+
+    GtkAllocation allocation;
+    gtk_widget_get_allocation(_thread_area, &allocation);
+
+    PangoLayout *layout = gtk_widget_create_pango_layout(_thread_area, thread_row._label.c_str());
+    pango_layout_set_alignment(layout, PANGO_ALIGN_RIGHT);
+    pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END);
+    pango_layout_set_width(layout, (allocation.width - _pixel_scale * 2) * PANGO_SCALE);
+
+    cairo_move_to(cr, _pixel_scale, top);
+    pango_cairo_show_layout(cr, layout);
+    g_object_unref(layout);
+  }
+}
+
+/**
+ * Draws in the scale labels.
+ */
+gboolean GtkStatsTimeline::
+scale_area_draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data) {
+  GtkStatsTimeline *self = (GtkStatsTimeline *)data;
+  self->draw_guide_labels(cr);
+
+  return TRUE;
+}
+
+/**
+ * Draws in the thread labels.
+ */
+gboolean GtkStatsTimeline::
+thread_area_draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data) {
+  GtkStatsTimeline *self = (GtkStatsTimeline *)data;
+  self->draw_thread_labels(cr);
+
+  return TRUE;
+}
+
+/**
+ *
+ */
+gboolean GtkStatsTimeline::
+scroll_callback(GtkWidget *widget, GdkEventScroll *event, gpointer data) {
+  GtkStatsTimeline *self = (GtkStatsTimeline *)data;
+
+  bool ctrl_held = (event->state & GDK_CONTROL_MASK) != 0;
+
+  double dx, dy;
+  if (event->direction == GDK_SCROLL_UP) {
+    dx = 0;
+    dy = 1;
+  }
+  else if (event->direction == GDK_SCROLL_DOWN) {
+    dx = 0;
+    dy = -1;
+  }
+  else if (event->direction == GDK_SCROLL_LEFT) {
+    dx = 1;
+    dy = 0;
+  }
+  else if (event->direction == GDK_SCROLL_RIGHT) {
+    dx = -1;
+    dy = 0;
+  }
+  else if (!gdk_event_get_scroll_deltas((GdkEvent *)event, &dx, &dy)) {
+    return FALSE;
+  }
+
+  int graph_x = (int)(event->x * self->_cr_scale);
+  int graph_y = (int)(event->y * self->_cr_scale);
+  return self->handle_scroll(graph_x, graph_y, dx, dy, ctrl_held);
+}
+
+/**
+ *
+ */
+gboolean GtkStatsTimeline::
+key_press_callback(GtkWidget *widget, GdkEventKey *event, gpointer data) {
+  GtkStatsTimeline *self = (GtkStatsTimeline *)data;
+  return self->handle_key(true, event->keyval, event->hardware_keycode);
+}
+
+/**
+ *
+ */
+gboolean GtkStatsTimeline::
+key_release_callback(GtkWidget *widget, GdkEventKey *event, gpointer data) {
+  GtkStatsTimeline *self = (GtkStatsTimeline *)data;
+  return self->handle_key(false, event->keyval, event->hardware_keycode);
+}

+ 91 - 0
pandatool/src/gtk-stats/gtkStatsTimeline.h

@@ -0,0 +1,91 @@
+/**
+ * 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 gtkStatsTimeline.h
+ * @author rdb
+ * @date 2022-02-17
+ */
+
+#ifndef GTKSTATSTIMELINE_H
+#define GTKSTATSTIMELINE_H
+
+#include "pandatoolbase.h"
+
+#include "gtkStatsGraph.h"
+#include "pStatTimeline.h"
+
+class GtkStatsMonitor;
+
+/**
+ * A window that draws all of the start/stop event pairs on each thread on a
+ * horizontal scrolling timeline, with concurrent start/stop pairs stacked
+ * underneath each other.
+ */
+class GtkStatsTimeline : public PStatTimeline, public GtkStatsGraph {
+public:
+  GtkStatsTimeline(GtkStatsMonitor *monitor);
+  virtual ~GtkStatsTimeline();
+
+  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);
+
+protected:
+  virtual void clear_region();
+  virtual void begin_draw();
+  virtual void draw_separator(int row);
+  virtual void draw_guide_bar(int x, GuideBarStyle style);
+  virtual void draw_bar(int row, int from_x, int to_x, int collector_index,
+                        const std::string &collector_name);
+  virtual void end_draw();
+  virtual void idle();
+
+  virtual bool animate(double time, double dt);
+
+  virtual void additional_graph_window_paint(cairo_t *cr);
+  virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const;
+  virtual DragMode consider_drag_start(int graph_x, int graph_y);
+
+  virtual gboolean handle_button_press(int graph_x, int graph_y,
+                                       bool double_click, int button);
+  virtual gboolean handle_button_release(int graph_x, int graph_y);
+  virtual gboolean handle_motion(int graph_x, int graph_y);
+  virtual gboolean handle_leave();
+  gboolean handle_scroll(int graph_x, int graph_y,
+                         double dx, double dy, bool ctrl_held);
+  gboolean handle_key(bool pressed, guint val, guint16 hw_code);
+
+private:
+  void draw_guide_labels(cairo_t *cr);
+  void draw_guide_label(cairo_t *cr, const GuideBar &bar);
+  void draw_thread_labels(cairo_t *cr);
+  void draw_thread_label(cairo_t *cr, const ThreadRow &thread_row);
+
+  static gboolean scale_area_draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data);
+  static gboolean thread_area_draw_callback(GtkWidget *widget, cairo_t *cr, gpointer data);
+  static gboolean scroll_callback(GtkWidget *widget, GdkEventScroll *event, gpointer data);
+  static gboolean key_press_callback(GtkWidget *widget, GdkEventKey *event, gpointer data);
+  static gboolean key_release_callback(GtkWidget *widget, GdkEventKey *event, gpointer data);
+
+  int row_to_pixel(int y) const {
+    return y * _pixel_scale * 5 + _pixel_scale;
+  }
+  int pixel_to_row(int y) const {
+    return (y - _pixel_scale) / (_pixel_scale * 5);
+  }
+
+  GtkWidget *_thread_area;
+
+  cairo_pattern_t *_grid_pattern;
+
+  int _highlighted_row = -1;
+  int _highlighted_x = 0;
+  ColorBar _popup_bar;
+};
+
+#endif

+ 1 - 0
pandatool/src/gtk-stats/gtkstats_composite1.cxx

@@ -8,3 +8,4 @@
 #include "gtkStatsPianoRoll.cxx"
 #include "gtkStatsPianoRoll.cxx"
 #include "gtkStatsServer.cxx"
 #include "gtkStatsServer.cxx"
 #include "gtkStatsStripChart.cxx"
 #include "gtkStatsStripChart.cxx"
+#include "gtkStatsTimeline.cxx"

+ 10 - 4
pandatool/src/pstatserver/CMakeLists.txt

@@ -13,6 +13,7 @@ set(P3PSTATSERVER_HEADERS
   pStatServer.h
   pStatServer.h
   pStatStripChart.h pStatStripChart.I
   pStatStripChart.h pStatStripChart.I
   pStatThreadData.h pStatThreadData.I
   pStatThreadData.h pStatThreadData.I
+  pStatTimeline.h pStatTimeline.I
   pStatView.h pStatView.I
   pStatView.h pStatView.I
   pStatViewLevel.h pStatViewLevel.I
   pStatViewLevel.h pStatViewLevel.I
 )
 )
@@ -22,10 +23,15 @@ set(P3PSTATSERVER_SOURCES
   pStatFlameGraph.cxx
   pStatFlameGraph.cxx
   pStatGraph.cxx
   pStatGraph.cxx
   pStatListener.cxx
   pStatListener.cxx
-  pStatMonitor.cxx pStatPianoRoll.cxx
-  pStatReader.cxx pStatServer.cxx
-  pStatStripChart.cxx pStatThreadData.cxx
-  pStatView.cxx pStatViewLevel.cxx
+  pStatMonitor.cxx
+  pStatPianoRoll.cxx
+  pStatReader.cxx
+  pStatServer.cxx
+  pStatStripChart.cxx
+  pStatThreadData.cxx
+  pStatTimeline.cxx
+  pStatView.cxx
+  pStatViewLevel.cxx
 )
 )
 
 
 composite_sources(p3pstatserver P3PSTATSERVER_SOURCES)
 composite_sources(p3pstatserver P3PSTATSERVER_SOURCES)

+ 1 - 0
pandatool/src/pstatserver/p3pstatserver_composite1.cxx

@@ -8,5 +8,6 @@
 #include "pStatServer.cxx"
 #include "pStatServer.cxx"
 #include "pStatStripChart.cxx"
 #include "pStatStripChart.cxx"
 #include "pStatThreadData.cxx"
 #include "pStatThreadData.cxx"
+#include "pStatTimeline.cxx"
 #include "pStatView.cxx"
 #include "pStatView.cxx"
 #include "pStatViewLevel.cxx"
 #include "pStatViewLevel.cxx"

+ 28 - 5
pandatool/src/pstatserver/pStatFlameGraph.I

@@ -12,15 +12,15 @@
  */
  */
 
 
 /**
 /**
- * Returns the View this chart represents.
+ * Returns the particular thread whose data this flame graph reflects.
  */
  */
-INLINE PStatView &PStatFlameGraph::
-get_view() const {
-  return _view;
+INLINE int PStatFlameGraph::
+get_thread_index() const {
+  return _thread_index;
 }
 }
 
 
 /**
 /**
- * Returns the particular collector whose data this strip chart reflects.
+ * Returns the particular collector whose data this flame graph reflects.
  */
  */
 INLINE int PStatFlameGraph::
 INLINE int PStatFlameGraph::
 get_collector_index() const {
 get_collector_index() const {
@@ -41,11 +41,19 @@ get_horizontal_scale() const {
  * the color values over pstats_average_time seconds, which hides spikes and
  * 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
  * makes the overall trends easier to read.  When false, the strip chart shows
  * the actual data as it is happening.
  * the actual data as it is happening.
+ *
+ * If you set this to true, you need to call animate() periodically so that the
+ * averages are smoothly updated over time.
  */
  */
 INLINE void PStatFlameGraph::
 INLINE void PStatFlameGraph::
 set_average_mode(bool average_mode) {
 set_average_mode(bool average_mode) {
   if (_average_mode != average_mode) {
   if (_average_mode != average_mode) {
     _average_mode = average_mode;
     _average_mode = average_mode;
+    _stack.reset_averages();
+    if (!average_mode) {
+      _time_width = _stack.get_net_value(false);
+      normal_guide_bars();
+    }
     force_redraw();
     force_redraw();
   }
   }
 }
 }
@@ -87,3 +95,18 @@ INLINE bool PStatFlameGraph::
 is_title_unknown() const {
 is_title_unknown() const {
   return _title_unknown;
   return _title_unknown;
 }
 }
+/**
+ * Returns the net value of this stack level.
+ */
+INLINE double PStatFlameGraph::StackLevel::
+get_net_value(bool average) const {
+  if (_collector_index >= 0) {
+    return average ? _avg_net_value : _net_value;
+  } else {
+    double sum = 0.0;
+    for (auto &item : _children) {
+      sum += item.second.get_net_value(average);
+    }
+    return sum;
+  }
+}

+ 329 - 93
pandatool/src/pstatserver/pStatFlameGraph.cxx

@@ -25,12 +25,12 @@
  *
  *
  */
  */
 PStatFlameGraph::
 PStatFlameGraph::
-PStatFlameGraph(PStatMonitor *monitor, PStatView &view,
+PStatFlameGraph(PStatMonitor *monitor,
                 int thread_index, int collector_index, int xsize, int ysize) :
                 int thread_index, int collector_index, int xsize, int ysize) :
   PStatGraph(monitor, xsize, ysize),
   PStatGraph(monitor, xsize, ysize),
   _thread_index(thread_index),
   _thread_index(thread_index),
-  _view(view),
-  _collector_index(collector_index)
+  _collector_index(collector_index),
+  _orig_collector_index(collector_index)
 {
 {
   _average_mode = true;
   _average_mode = true;
   _average_cursor = 0;
   _average_cursor = 0;
@@ -70,8 +70,6 @@ update() {
         _current_frame = frame_number;
         _current_frame = frame_number;
 
 
         update_data();
         update_data();
-        force_redraw();
-        update_labels();
       }
       }
     }
     }
   }
   }
@@ -85,12 +83,20 @@ update() {
  */
  */
 void PStatFlameGraph::
 void PStatFlameGraph::
 set_collector_index(int collector_index) {
 set_collector_index(int collector_index) {
+  if (collector_index == -1) {
+    // First go back to the collector where we originally opened this graph,
+    // and only then go back to the root.
+    collector_index = _orig_collector_index;
+    if (_collector_index == _orig_collector_index) {
+      collector_index = -1;
+      _orig_collector_index = -1;
+    }
+  }
   if (_collector_index != collector_index) {
   if (_collector_index != collector_index) {
     _collector_index = collector_index;
     _collector_index = collector_index;
     _title_unknown = true;
     _title_unknown = true;
+    _stack.clear();
     update_data();
     update_data();
-    force_redraw();
-    update_labels();
   }
   }
 }
 }
 
 
@@ -104,45 +110,61 @@ get_title_text() {
   _title_unknown = false;
   _title_unknown = false;
 
 
   const PStatClientData *client_data = _monitor->get_client_data();
   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)";
+  if (_collector_index >= 0) {
+    if (client_data->has_collector(_collector_index)) {
+      text = client_data->get_collector_fullname(_collector_index);
+      text += " flame graph";
     } else {
     } else {
       _title_unknown = true;
       _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;
+      }
+    }
+  }
+  else if (client_data->has_thread(_thread_index)) {
+    text += client_data->get_thread_name(_thread_index) + " thread flame graph";
+  }
+  else {
+    _title_unknown = true;
   }
   }
 
 
   return text;
   return text;
 }
 }
 
 
 /**
 /**
- * Called when the mouse hovers over a label, and should return the text that
+ * Called when the mouse hovers over the graph, and should return the text that
  * should appear on the tooltip.
  * should appear on the tooltip.
  */
  */
 std::string PStatFlameGraph::
 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();
+get_bar_tooltip(int depth, int x) const {
+  const StackLevel *level = _stack.locate(depth, pixel_to_height(x), _average_mode);
+  if (level != nullptr) {
+    const PStatClientData *client_data = _monitor->get_client_data();
+    if (client_data != nullptr && client_data->has_collector(level->_collector_index)) {
+      std::ostringstream text;
+      text << client_data->get_collector_fullname(level->_collector_index);
+      text << " (" << format_number(level->get_net_value(_average_mode), GBU_show_units | GBU_ms) << ")";
+      return text.str();
+    }
   }
   }
+  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()) << ")";
+/**
+ * Returns the collector index corresponding to the bar at the given location.
+ */
+int PStatFlameGraph::
+get_bar_collector(int depth, int x) const {
+  const StackLevel *level = _stack.locate(depth, pixel_to_height(x), _average_mode);
+  if (level != nullptr) {
+    return level->_collector_index;
   }
   }
-
-  return text.str();
+  return -1;
 }
 }
 
 
 /**
 /**
@@ -150,66 +172,73 @@ get_label_tooltip(int collector_index) const {
  */
  */
 void PStatFlameGraph::
 void PStatFlameGraph::
 update_data() {
 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;
+  const PStatClientData *client_data = _monitor->get_client_data();
+  if (client_data == nullptr) {
+    return;
   }
   }
 
 
-  _view.set_to_frame(_current_frame);
+  const PStatThreadData *thread_data = client_data->get_thread_data(_thread_index);
+  if (thread_data == nullptr || thread_data->is_empty()) {
+    return;
+  }
 
 
-  const PStatViewLevel *level = _view.get_level(_collector_index);
-  double offset = 0;
-  update_data(level, 0, offset);
+  const PStatFrameData &frame_data = thread_data->get_frame(_current_frame);
 
 
-  _time_width = (offset != 0) ? offset : 1.0 / pstats_target_frame_rate;
-  normal_guide_bars();
+  bool first_time = _stack._children.empty();
 
 
-  // Cycle through the ring buffers.
-  _average_cursor = (_average_cursor + 1) % _num_average_frames;
-}
+  StackLevel *top = &_stack;
+  top->reset();
 
 
-/**
- * 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;
+  size_t num_events = frame_data.get_num_events();
+  for (size_t ei = 0; ei < num_events; ++ei) {
+    int collector_index = frame_data.get_time_collector(ei);
+    double time = frame_data.get_time(ei);
 
 
-    // Calculate the average.
-    cd._net_value = 0;
-    for (double value : cd._values) {
-      cd._net_value += value;
+    if (frame_data.is_start(ei)) {
+      // If we have a collector index, use it to determine which bottom-level
+      // stack frames we are interested in.
+      if (_collector_index < 0 ||
+          _collector_index == collector_index ||
+          top != &_stack) {
+        top = top->start(collector_index, time);
+      }
+      else {
+        // Check whether one of the parents matches, perhaps.
+        int parent_index = collector_index;
+        do {
+          const PStatCollectorDef &def = client_data->get_collector_def(parent_index);
+          if (parent_index == def._parent_index) {
+            break;
+          }
+          parent_index = def._parent_index;
+          if (parent_index == _collector_index) {
+            // Yes, let it through.
+            top = top->start(collector_index, time);
+            break;
+          }
+        }
+        while (parent_index >= 0 && client_data->has_collector(parent_index));
+      }
+    }
+    else {
+      top = top->stop(collector_index, time);
     }
     }
-    cd._net_value /= _num_average_frames;
   }
   }
+  top = top->stop_all(frame_data.get_end());
+  nassertv(top == &_stack);
 
 
-  if (cd._net_value != 0.0) {
-    cd._net_value = std::max(cd._net_value, 0.0);
-
-    double child_offset = offset;
-    offset += cd._net_value;
+  if (first_time) {
+    _stack.reset_averages();
+  }
 
 
-    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);
+  if (!_average_mode) {
+    // Redraw right away, except in average mode, where it's done in animate().
+    _time_width = _stack.get_net_value(false);
+    if (_time_width == 0.0) {
+      _time_width = 1.0 / pstats_target_frame_rate;
     }
     }
+    normal_guide_bars();
+    force_redraw();
   }
   }
 }
 }
 
 
@@ -226,7 +255,6 @@ changed_size(int xsize, int ysize) {
 
 
     normal_guide_bars();
     normal_guide_bars();
     force_redraw();
     force_redraw();
-    update_labels();
   }
   }
 }
 }
 
 
@@ -237,22 +265,10 @@ changed_size(int xsize, int ysize) {
 void PStatFlameGraph::
 void PStatFlameGraph::
 force_redraw() {
 force_redraw() {
   begin_draw();
   begin_draw();
+  r_draw_level(_stack);
   end_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.
  * Calls update_guide_bars with parameters suitable to this kind of graph.
  */
  */
@@ -280,6 +296,14 @@ void PStatFlameGraph::
 begin_draw() {
 begin_draw() {
 }
 }
 
 
+/**
+ * Should be overridden by the user class.  Should draw a single bar at the
+ * indicated location.
+ */
+void PStatFlameGraph::
+draw_bar(int depth, int from_x, int to_x, int collector_index) {
+}
+
 /**
 /**
  * Should be overridden by the user class.  This hook will be called after
  * Should be overridden by the user class.  This hook will be called after
  * drawing a series of color bars in the chart.
  * drawing a series of color bars in the chart.
@@ -295,3 +319,215 @@ end_draw() {
 void PStatFlameGraph::
 void PStatFlameGraph::
 idle() {
 idle() {
 }
 }
+
+/**
+ * Should be called periodically to update any animated values.  Returns false
+ * to indicate that the animation is done and no longer needs to be called.
+ */
+bool PStatFlameGraph::
+animate(double time, double dt) {
+  if (!_average_mode) {
+    return false;
+  }
+
+  if (_stack.update_averages(_average_cursor)) {
+    _time_width = _stack.get_net_value(true);
+    if (_time_width == 0.0) {
+      _time_width = 1.0 / pstats_target_frame_rate;
+    }
+    normal_guide_bars();
+    force_redraw();
+  }
+
+  // Cycle through the ring buffers.
+  _average_cursor = (_average_cursor + 1) % _num_average_frames;
+  return true;
+}
+
+/**
+ * Resets all the nodes by setting their _net_value to 0.0.
+ */
+void PStatFlameGraph::StackLevel::
+reset() {
+  _start_time = 0.0;
+  _net_value = 0.0;
+  _started = false;
+
+  for (auto &item : _children) {
+    item.second.reset();
+  }
+}
+
+/**
+ * Starts the given collector, which starts a new stack frame as a child of
+ * the current one.  Returns the new child, which is the new stack top.
+ */
+PStatFlameGraph::StackLevel *PStatFlameGraph::StackLevel::
+start(int collector_index, double time) {
+  StackLevel &child = _children[collector_index];
+  child._parent = this;
+  child._collector_index = collector_index;
+  child._start_time = std::max(_start_time, time);
+  child._started = true;
+  return &child;
+}
+
+/**
+ * Stops the given collector, which is assumed to be somewhere up the
+ * hierarchy.  Should only be called on the top of the stack, usually.
+ * Returns the new top of the stack.
+ */
+PStatFlameGraph::StackLevel *PStatFlameGraph::StackLevel::
+stop(int collector_index, double time) {
+  StackLevel *new_top = r_stop(collector_index, time);
+  if (new_top != nullptr) {
+    return new_top;
+  }
+  // We have a stop event without a preceding start event.  Measure the
+  // the time from the start of the current stack frame to the stop time.
+  // Actually, don't do this, because it means that a child may end up with
+  // more time than the parent.  Need a better solution for this - or not?
+  //start(collector_index, _start_time)->stop(collector_index, time);
+  return this;
+}
+
+/**
+ * Stops all still started collectors.  Returns the bottom of the stack,
+ * which is also the new top of the stack.
+ */
+PStatFlameGraph::StackLevel *PStatFlameGraph::StackLevel::
+stop_all(double time) {
+  if (_parent != nullptr) {
+    nassertr(_started, this);
+    _net_value += time - _start_time;
+    return _parent->stop_all(time);
+  } else {
+    return this;
+  }
+}
+
+/**
+ * Resets the average calculator, used when first enabling average mode.
+ */
+void PStatFlameGraph::StackLevel::
+reset_averages() {
+  double net_value = get_net_value(false);
+
+  for (double &value : _values) {
+    value = net_value;
+  }
+  _avg_net_value = net_value;
+
+  for (auto &item : _children) {
+    item.second.reset_averages();
+  }
+}
+
+/**
+ * Recursively calculates the averages.  Returns true if any value was changed.
+ */
+bool PStatFlameGraph::StackLevel::
+update_averages(size_t cursor) {
+  _values[cursor] = get_net_value(false);
+
+  bool changed = false;
+
+  double sum = 0;
+  for (double value : _values) {
+    sum += value;
+  }
+  double avg = sum / _num_average_frames;
+  if (avg != _avg_net_value) {
+    _avg_net_value = avg;
+    changed = true;
+  }
+
+  for (auto &item : _children) {
+    if (item.second.update_averages(cursor)) {
+      changed = true;
+    }
+  }
+
+  return changed;
+}
+
+/**
+ * Locates a stack level at the given depth and the given time offset.
+ */
+const PStatFlameGraph::StackLevel *PStatFlameGraph::StackLevel::
+locate(int depth, double time, bool average) const {
+  if (time < 0.0) {
+    return nullptr;
+  }
+  for (const auto &item : _children) {
+    double value = item.second.get_net_value(average);
+    if (time < value) {
+      if (depth == 0) {
+        // This is it.
+        return &item.second;
+      } else {
+        // Recurse.
+        return item.second.locate(depth - 1, time, average);
+      }
+    }
+    time -= value;
+  }
+  return nullptr;
+}
+
+/**
+ * Clears everything.
+ */
+void PStatFlameGraph::StackLevel::
+clear() {
+  _children.clear();
+  _net_value = 0.0;
+}
+
+/**
+ * Recursive helper used by stop().
+ */
+PStatFlameGraph::StackLevel *PStatFlameGraph::StackLevel::
+r_stop(int collector_index, double time) {
+  if (_collector_index == collector_index) {
+    // Found it.
+    nassertr(_started, nullptr);
+    _net_value += time - _start_time;
+    _started = false;
+    nassertr(_parent != nullptr, nullptr);
+    return _parent;
+  }
+  else if (_parent != nullptr) {
+    StackLevel *level = _parent->r_stop(collector_index, time);
+    if (level != nullptr) {
+      nassertr(_started, nullptr);
+      _net_value += time - _start_time;
+      _started = false;
+      return level;
+    }
+  }
+  return nullptr;
+}
+
+/**
+ * Recursively draws a level.
+ */
+void PStatFlameGraph::
+r_draw_level(const StackLevel &level, int depth, double offset) {
+  for (const auto &item : level._children) {
+    const StackLevel &child = item.second;
+
+    double value = child.get_net_value(_average_mode);
+
+    int from_x = height_to_pixel(offset);
+    int to_x = height_to_pixel(offset + value);
+
+    // No need to recurse if the bars have become smaller than a pixel.
+    if (to_x > from_x) {
+      draw_bar(depth, from_x, to_x, child._collector_index);
+      r_draw_level(child, depth + 1, offset);
+    }
+
+    offset += value;
+  }
+}

+ 49 - 25
pandatool/src/pstatserver/pStatFlameGraph.h

@@ -35,14 +35,14 @@ class PStatFrameData;
  */
  */
 class PStatFlameGraph : public PStatGraph {
 class PStatFlameGraph : public PStatGraph {
 public:
 public:
-  PStatFlameGraph(PStatMonitor *monitor, PStatView &view,
+  PStatFlameGraph(PStatMonitor *monitor,
                   int thread_index, int collector_index,
                   int thread_index, int collector_index,
                   int xsize, int ysize);
                   int xsize, int ysize);
   virtual ~PStatFlameGraph();
   virtual ~PStatFlameGraph();
 
 
   void update();
   void update();
 
 
-  INLINE PStatView &get_view() const;
+  INLINE int get_thread_index() const;
   INLINE int get_collector_index() const;
   INLINE int get_collector_index() const;
   void set_collector_index(int collector_index);
   void set_collector_index(int collector_index);
 
 
@@ -56,47 +56,71 @@ public:
 
 
   INLINE bool is_title_unknown() const;
   INLINE bool is_title_unknown() const;
   std::string get_title_text();
   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<int, CollectorData> Data;
+  std::string get_bar_tooltip(int depth, int x) const;
+  int get_bar_collector(int depth, int x) const;
 
 
+protected:
   void update_data();
   void update_data();
-  void update_data(const PStatViewLevel *level, int depth, double &offset);
   void changed_size(int xsize, int ysize);
   void changed_size(int xsize, int ysize);
   void force_redraw();
   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 normal_guide_bars();
 
 
   virtual void begin_draw();
   virtual void begin_draw();
+  virtual void draw_bar(int depth, int from_x, int to_x, int collector_index);
   virtual void end_draw();
   virtual void end_draw();
   virtual void idle();
   virtual void idle();
 
 
+  bool animate(double time, double dt);
+
 private:
 private:
-  void compute_page(const PStatFrameData &frame_data);
+  static const size_t _num_average_frames = 150;
 
 
-protected:
-  int _thread_index;
+  class StackLevel {
+  public:
+    void reset();
 
 
-private:
-  PStatView &_view;
+    StackLevel *start(int collector_index, double time);
+    StackLevel *stop(int collector_index, double time);
+    StackLevel *stop_all(double time);
+
+    INLINE double get_net_value(bool average) const;
+    void reset_averages();
+    bool update_averages(size_t cursor);
+
+    const StackLevel *locate(int depth, double time, bool average) const;
+
+    void clear();
+
+  private:
+    StackLevel *r_stop(int collector_index, double time);
+
+    double _net_value = 0.0;
+    double _avg_net_value = 0.0;
+
+    // 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] = {0.0};
+
+    double _start_time = 0.0;
+    bool _started = false;
+
+    int _collector_index = -1;
+    StackLevel *_parent = nullptr;
+    pmap<int, StackLevel> _children;
+
+    friend class PStatFlameGraph;
+  };
+
+  void r_draw_level(const StackLevel &level, int depth = 0, double offset = 0.0);
+
+  StackLevel _stack;
+  int _thread_index;
   int _collector_index;
   int _collector_index;
+  int _orig_collector_index;
   bool _average_mode;
   bool _average_mode;
   size_t _average_cursor;
   size_t _average_cursor;
 
 
-  Data _data;
-
   double _time_width;
   double _time_width;
   int _current_frame;
   int _current_frame;
   bool _title_unknown;
   bool _title_unknown;

+ 33 - 5
pandatool/src/pstatserver/pStatGraph.cxx

@@ -178,7 +178,13 @@ format_number(double value, int guide_bar_units, const string &unit_name) {
 
 
   if ((guide_bar_units & GBU_named) != 0) {
   if ((guide_bar_units & GBU_named) != 0) {
     // Units are whatever is specified by unit_name, not a time unit at all.
     // Units are whatever is specified by unit_name, not a time unit at all.
-    label = format_number(value);
+    int int_value = (int)value;
+    if ((double)int_value == value) {
+      // Probably a counter or something, don't display .0 suffix.
+      label = format_string(int_value);
+    } else {
+      label = format_number(value);
+    }
     if ((guide_bar_units & GBU_show_units) != 0 && !unit_name.empty()) {
     if ((guide_bar_units & GBU_show_units) != 0 && !unit_name.empty()) {
       label += " ";
       label += " ";
       label += unit_name;
       label += unit_name;
@@ -187,10 +193,32 @@ format_number(double value, int guide_bar_units, const string &unit_name) {
   } else {
   } else {
     // Units are either milliseconds or hz, or both.
     // Units are either milliseconds or hz, or both.
     if ((guide_bar_units & GBU_ms) != 0) {
     if ((guide_bar_units & GBU_ms) != 0) {
-      double ms = value * 1000.0;
-      label += format_number(ms);
-      if ((guide_bar_units & GBU_show_units) != 0) {
-        label += " ms";
+      if ((guide_bar_units & GBU_show_units) != 0 &&
+          value > 0 && value < 0.000001) {
+        double ns = value * 1000000000.0;
+        label += format_number(ns);
+        label += " ns";
+      }
+      else if ((guide_bar_units & GBU_show_units) != 0 &&
+          value > 0 && value < 0.001) {
+        double us = value * 1000000.0;
+        label += format_number(us);
+#ifdef _WIN32
+        label += " \xb5s";
+#else
+        label += " us";
+#endif
+      }
+      else if ((guide_bar_units & GBU_show_units) == 0 || value < 1.0) {
+        double ms = value * 1000.0;
+        label += format_number(ms);
+        if ((guide_bar_units & GBU_show_units) != 0) {
+          label += " ms";
+        }
+      }
+      else {
+        label += format_number(value);
+        label += " s";
       }
       }
     }
     }
 
 

+ 1 - 0
pandatool/src/pstatserver/pStatGraph.h

@@ -52,6 +52,7 @@ public:
     GBS_normal,
     GBS_normal,
     GBS_target,
     GBS_target,
     GBS_user,
     GBS_user,
+    GBS_frame,
   };
   };
 
 
   class GuideBar {
   class GuideBar {

+ 8 - 0
pandatool/src/pstatserver/pStatPianoRoll.I

@@ -11,6 +11,14 @@
  * @date 2000-07-18
  * @date 2000-07-18
  */
  */
 
 
+/**
+ * Returns the particular thread whose data this piano roll reflects.
+ */
+INLINE int PStatPianoRoll::
+get_thread_index() const {
+  return _thread_index;
+}
+
 /**
 /**
  * Changes the amount of time the width of the horizontal axis represents.
  * Changes the amount of time the width of the horizontal axis represents.
  * This may force a redraw.
  * This may force a redraw.

+ 14 - 0
pandatool/src/pstatserver/pStatPianoRoll.cxx

@@ -128,6 +128,20 @@ update() {
   idle();
   idle();
 }
 }
 
 
+/**
+ * Called when the mouse hovers over a label, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string PStatPianoRoll::
+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();
+  }
+
+  return client_data->get_collector_fullname(collector_index);
+}
+
 /**
 /**
  * To be called by the user class when the widget size has changed.  This
  * 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
  * updates the chart's internal data and causes it to issue redraw commands to

+ 4 - 0
pandatool/src/pstatserver/pStatPianoRoll.h

@@ -43,6 +43,8 @@ public:
 
 
   void update();
   void update();
 
 
+  INLINE int get_thread_index() const;
+
   INLINE void set_horizontal_scale(double time_width);
   INLINE void set_horizontal_scale(double time_width);
   INLINE double get_horizontal_scale() const;
   INLINE double get_horizontal_scale() const;
 
 
@@ -51,6 +53,8 @@ public:
   INLINE int height_to_pixel(double value) const;
   INLINE int height_to_pixel(double value) const;
   INLINE double pixel_to_height(int y) const;
   INLINE double pixel_to_height(int y) const;
 
 
+  std::string get_label_tooltip(int collector_index) const;
+
 protected:
 protected:
   void changed_size(int xsize, int ysize);
   void changed_size(int xsize, int ysize);
   void force_redraw();
   void force_redraw();

+ 8 - 0
pandatool/src/pstatserver/pStatStripChart.I

@@ -19,6 +19,14 @@ get_view() const {
   return _view;
   return _view;
 }
 }
 
 
+/**
+ * Returns the particular thread whose data this strip chart reflects.
+ */
+INLINE int PStatStripChart::
+get_thread_index() const {
+  return _thread_index;
+}
+
 /**
 /**
  * Returns the particular collector whose data this strip chart reflects.
  * Returns the particular collector whose data this strip chart reflects.
  */
  */

+ 34 - 15
pandatool/src/pstatserver/pStatStripChart.cxx

@@ -56,7 +56,7 @@ PStatStripChart(PStatMonitor *monitor, PStatView &view,
     _unit_name = def._level_units;
     _unit_name = def._level_units;
   }
   }
 
 
-  set_default_vertical_scale();
+  set_auto_vertical_scale();
 }
 }
 
 
 /**
 /**
@@ -139,6 +139,7 @@ set_collector_index(int collector_index) {
     _title_unknown = true;
     _title_unknown = true;
     _data.clear();
     _data.clear();
     clear_label_usage();
     clear_label_usage();
+    set_auto_vertical_scale();
     force_redraw();
     force_redraw();
     update_labels();
     update_labels();
   }
   }
@@ -170,26 +171,44 @@ void PStatStripChart::
 set_auto_vertical_scale() {
 set_auto_vertical_scale() {
   const PStatThreadData *thread_data = _view.get_thread_data();
   const PStatThreadData *thread_data = _view.get_thread_data();
 
 
-  double max_value = 0.0;
+  // Calculate the median value.
+  std::vector<double> values;
 
 
-  int frame_number = -1;
-  for (int x = 0; x <= _xsize; x++) {
-    double time = pixel_to_timestamp(x);
-    frame_number =
-      thread_data->get_frame_number_at_time(time, frame_number);
+  if (thread_data != nullptr && !thread_data->is_empty()) {
+    // Find the oldest visible frame.
+    double start_time = pixel_to_timestamp(0);
+    int oldest_frame = std::max(
+      thread_data->get_frame_number_at_time(start_time),
+      thread_data->get_oldest_frame_number());
+    int latest_frame = thread_data->get_latest_frame_number();
 
 
-    if (thread_data->has_frame(frame_number)) {
-      double net_value = get_net_value(frame_number);
-      max_value = max(max_value, net_value);
+    for (int frame_number = oldest_frame; frame_number <= latest_frame; ++frame_number) {
+      if (thread_data->has_frame(frame_number)) {
+        values.push_back(get_net_value(frame_number));
+      }
     }
     }
   }
   }
 
 
-  // Ok, now we know what the max value visible in the chart is.  Choose a
-  // scale that will show all of this sensibly.
-  if (max_value == 0.0) {
-    set_vertical_scale(1.0);
+  if (values.empty()) {
+    set_default_vertical_scale();
+    return;
+  }
+
+  double median;
+  size_t half = values.size() / 2;
+  if (values.size() % 2 == 0) {
+    std::sort(values.begin(), values.end());
+    median = (values[half] + values[half + 1]) / 2.0;
+  } else {
+    std::nth_element(values.begin(), values.begin() + half, values.end());
+    median = values[half];
+  }
+
+  if (median > 0.0) {
+    // Take 1.5 times the median value as the vertical scale.
+    set_vertical_scale(median * 1.5);
   } else {
   } else {
-    set_vertical_scale(max_value * 1.1);
+    set_default_vertical_scale();
   }
   }
 }
 }
 
 

+ 1 - 0
pandatool/src/pstatserver/pStatStripChart.h

@@ -46,6 +46,7 @@ public:
   bool first_data() const;
   bool first_data() const;
 
 
   INLINE PStatView &get_view() const;
   INLINE PStatView &get_view() const;
+  INLINE int get_thread_index() const;
   INLINE int get_collector_index() const;
   INLINE int get_collector_index() const;
   void set_collector_index(int collector_index);
   void set_collector_index(int collector_index);
 
 

+ 8 - 0
pandatool/src/pstatserver/pStatThreadData.cxx

@@ -288,6 +288,14 @@ record_new_frame(int frame_number, PStatFrameData *frame_data) {
   }
   }
 
 
   int index = frame_number - _first_frame_number;
   int index = frame_number - _first_frame_number;
+
+  // It's possible to receive frames out of order.
+  while (index < 0) {
+    _frames.push_front(nullptr);
+    ++index;
+    --_first_frame_number;
+  }
+
   nassertv(index >= 0 && index < (int)_frames.size());
   nassertv(index >= 0 && index < (int)_frames.size());
 
 
   if (_frames[index] != nullptr) {
   if (_frames[index] != nullptr) {

+ 139 - 0
pandatool/src/pstatserver/pStatTimeline.I

@@ -0,0 +1,139 @@
+/**
+ * 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 pStatTimeline.I
+ * @author rdb
+ * @date 2022-02-11
+ */
+
+/**
+ * Changes the amount of time the width of the horizontal axis represents.
+ * This may force a redraw.
+ */
+INLINE void PStatTimeline::
+set_horizontal_scale(double time_width) {
+  double max_time_width = (_highest_end_time - _lowest_start_time) * 2.0;
+  time_width = std::min(time_width, max_time_width);
+
+  double scale = time_width / get_xsize();
+  if (_time_scale != scale) {
+    _time_scale = scale;
+    _target_time_scale = scale;
+    _zoom_speed = 0.0;
+    normal_guide_bars();
+    force_redraw();
+  }
+}
+
+/**
+ * Returns the amount of total time the width of the horizontal axis
+ * represents.
+ */
+INLINE double PStatTimeline::
+get_horizontal_scale() const {
+  return _time_scale * get_xsize();
+}
+
+/**
+ * This may force a redraw.
+ */
+INLINE void PStatTimeline::
+set_horizontal_scroll(double start_time) {
+  start_time = std::max(std::min(start_time, _highest_end_time), _lowest_start_time);
+  if (_start_time != start_time) {
+    _start_time = start_time;
+    _target_start_time = start_time;
+    _scroll_speed = 0.0;
+    normal_guide_bars();
+    force_redraw();
+  }
+}
+
+/**
+ * Returns the amount of total time the width of the horizontal axis
+ * represents.
+ */
+INLINE double PStatTimeline::
+get_horizontal_scroll() const {
+  return _start_time;
+}
+
+/**
+ * Smoothly zooms to the given time width, around the given focal point.
+ */
+INLINE void PStatTimeline::
+zoom_to(double time_width, double center) {
+  // Don't allow zooming out to beyond 2x the size of the entire timeline.
+  // There's a limit of zooming beyond 1 ns per bar, there's just no point...
+  double max_time_width = (_highest_end_time - _lowest_start_time) * 2.0;
+  time_width = std::min(std::max(1e-7, time_width), max_time_width);
+  _target_time_scale = time_width / get_xsize();
+  _zoom_center = center;
+
+  double pivot_x = (_zoom_center - _start_time) / _time_scale;
+  scroll_to(_zoom_center - pivot_x * _target_time_scale);
+}
+
+/**
+ * Smoothly zooms by the given amount, where 1.0 is a single "tick" of zooming
+ * in and -1.0 is a single "tick" of zooming out.
+ */
+INLINE void PStatTimeline::
+zoom_by(double amount, double center) {
+  zoom_to(_target_time_scale * pow(0.8, amount) * get_xsize(), center);
+}
+
+/**
+ * Smoothly scrolls to the given time point.
+ */
+INLINE void PStatTimeline::
+scroll_to(double start_time) {
+  _target_start_time = std::max(std::min(start_time, _highest_end_time), _lowest_start_time);
+}
+
+/**
+ * Smoothly scrolls by the given amount.
+ */
+INLINE void PStatTimeline::
+scroll_by(double delta) {
+  scroll_to(_target_start_time + delta);
+}
+
+/**
+ * Converts a timestamp to a horizontal pixel offset.
+ */
+INLINE int PStatTimeline::
+timestamp_to_pixel(double time) const {
+  return (int)((double)_xsize * (time - _start_time) / get_horizontal_scale());
+}
+
+/**
+ * Converts a horizontal pixel offset to a timestamp.
+ */
+INLINE double PStatTimeline::
+pixel_to_timestamp(int x) const {
+  return _time_scale * (double)x + _start_time;
+}
+
+/**
+ * Converts a value (i.e.  a "height" in the strip chart) to a horizontal
+ * pixel offset.
+ */
+INLINE int PStatTimeline::
+height_to_pixel(double value) const {
+  return (int)((double)_xsize * value / get_horizontal_scale());
+}
+
+/**
+ * Converts a horizontal pixel offset to a value (a "height" in the strip
+ * chart).
+ */
+INLINE double PStatTimeline::
+pixel_to_height(int x) const {
+  return _time_scale * (double)x;
+}

+ 664 - 0
pandatool/src/pstatserver/pStatTimeline.cxx

@@ -0,0 +1,664 @@
+/**
+ * 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 pStatTimeline.cxx
+ * @author rdb
+ * @date 2022-02-11
+ */
+
+#include "pStatTimeline.h"
+
+#include "pStatFrameData.h"
+#include "pStatCollectorDef.h"
+#include "string_utils.h"
+#include "config_pstatclient.h"
+
+#include <algorithm>
+
+/**
+ *
+ */
+PStatTimeline::
+PStatTimeline(PStatMonitor *monitor, int xsize, int ysize) :
+  PStatGraph(monitor, xsize, ysize)
+{
+  // Default to 1 millisecond per 10 pixels.
+  _time_scale = 1 / 10000.0;
+  _target_time_scale = _time_scale;
+
+  _guide_bar_units = GBU_ms | GBU_show_units;
+
+  // Load in the initial data, so that the user can see everything back to the
+  // beginning (or as far as pstats-history goes back to).
+  const PStatClientData *client_data = monitor->get_client_data();
+  if (client_data != nullptr) {
+    size_t row_offset = 0;
+
+    for (int thread_index = 0; thread_index < client_data->get_num_threads(); ++thread_index) {
+      _threads.emplace_back();
+      ThreadRow &thread_row = _threads.back();
+      thread_row._row_offset = row_offset;
+
+      const PStatThreadData *thread_data = client_data->get_thread_data(thread_index);
+      if (thread_data != nullptr) {
+        _threads_changed = true;
+
+        if (!thread_data->is_empty()) {
+          int oldest_frame = thread_data->get_oldest_frame_number();
+          int latest_frame = thread_data->get_latest_frame_number();
+
+          double oldest_start_time = thread_data->get_frame(oldest_frame).get_start();
+          double latest_end_time = thread_data->get_frame(latest_frame).get_end();
+
+          if (!_have_start_time) {
+            _have_start_time = true;
+            _lowest_start_time = oldest_start_time;
+          }
+          else {
+            _lowest_start_time = std::min(_lowest_start_time, oldest_start_time);
+          }
+          _highest_end_time = std::max(_highest_end_time, latest_end_time);
+
+          for (int frame = oldest_frame; frame <= latest_frame; ++frame) {
+            update_bars(thread_index, frame);
+          }
+        }
+      }
+
+      row_offset += thread_row._rows.size() + 1;
+    }
+  }
+
+  _start_time = _lowest_start_time;
+  _target_start_time = _start_time;
+}
+
+/**
+ *
+ */
+PStatTimeline::
+~PStatTimeline() {
+}
+
+/**
+ * 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 PStatTimeline::
+new_data(int thread_index, int frame_number) {
+  const PStatClientData *client_data = _monitor->get_client_data();
+
+  if (client_data != nullptr) {
+    const PStatThreadData *thread_data =
+      client_data->get_thread_data(thread_index);
+
+    if (thread_data != nullptr && !thread_data->is_empty()) {
+      const PStatFrameData &frame_data = thread_data->get_frame(frame_number);
+      double frame_start = frame_data.get_start();
+      double frame_end = frame_data.get_end();
+
+      if (!_have_start_time) {
+        _start_time = frame_start;
+        _have_start_time = true;
+        _lowest_start_time = _start_time;
+      }
+      else if (_start_time < _lowest_start_time) {
+        _lowest_start_time = _start_time;
+      }
+      if (frame_end > _highest_end_time) {
+        _highest_end_time = frame_end;
+      }
+
+      while (thread_index >= _threads.size()) {
+        _threads_changed = true;
+        if (_threads.size() == 0) {
+          _threads.resize(1);
+        } else {
+          _threads.resize(_threads.size() + 1);
+          _threads[_threads.size() - 1]._row_offset =
+            _threads[_threads.size() - 2]._row_offset +
+            _threads[_threads.size() - 2]._rows.size() + 1;
+        }
+      }
+
+      if (update_bars(thread_index, frame_number)) {
+        // The number of rows was changed.
+        // Change the offset of all subsequent ThreadRows.
+        ThreadRow &thread_row = _threads[thread_index];
+        size_t offset = thread_row._row_offset + thread_row._rows.size() + 1;
+        for (size_t ti = (size_t)(thread_index + 1); ti < _threads.size(); ++ti) {
+          _threads[ti]._row_offset = offset;
+          offset += _threads[ti]._rows.size() + 1;
+        }
+        _threads_changed = true;
+        normal_guide_bars();
+        force_redraw();
+      }
+      else if (frame_end >= _start_time || frame_start <= _start_time + get_horizontal_scale()) {
+        normal_guide_bars();
+        begin_draw();
+        draw_thread(thread_index, frame_start, frame_end);
+        end_draw();
+      }
+    }
+  }
+
+  idle();
+}
+
+/**
+ * Called by new_data().  Updates the bars without doing any drawing.  Returns
+ * true if the number of rows was changed (forcing a full redraw), false if
+ * only new bars were added on the right side.
+ */
+bool PStatTimeline::
+update_bars(int thread_index, int frame_number) {
+  const PStatClientData *client_data = _monitor->get_client_data();
+  const PStatThreadData *thread_data = client_data->get_thread_data(thread_index);
+  const PStatFrameData &frame_data = thread_data->get_frame(frame_number);
+  ThreadRow &thread_row = _threads[thread_index];
+  thread_row._label = client_data->get_thread_name(thread_index);
+  bool changed_num_rows = false;
+
+  // pair<int collector_index, double start_time>
+  pvector<std::pair<int, double> > stack;
+
+  size_t num_events = frame_data.get_num_events();
+  for (size_t i = 0; i < num_events; ++i) {
+    int collector_index = frame_data.get_time_collector(i);
+    double time = frame_data.get_time(i);
+
+    if (frame_data.is_start(i)) {
+      stack.push_back(std::make_pair(collector_index, std::max(time, _start_time)));
+      if (stack.size() > thread_row._rows.size()) {
+        thread_row._rows.resize(stack.size());
+        changed_num_rows = true;
+      }
+    }
+    else if (!stack.empty()) {
+      if (stack.back().first == collector_index) {
+        // Most likely case, ending the most recent collector that is still
+        // open.
+        double start_time = stack.back().second;
+        stack.pop_back();
+        thread_row._rows[stack.size()].push_back({
+          start_time, time, collector_index, thread_index, frame_number});
+
+        while (!stack.empty() && stack.back().first < 0) {
+          stack.pop_back();
+        }
+      }
+      else {
+        // Unlikely case: ending a collector before a "child" has ended.
+        // Go back and clear the row where this collector started.
+        // Don't decrement the row index.
+        for (size_t i = 0; i < stack.size(); ++i) {
+          auto &item = stack[stack.size() - 1 - i];
+
+          if (item.first == collector_index) {
+            thread_row._rows[stack.size() - 1 - i].push_back({
+              item.second, time, collector_index, thread_index, frame_number});
+            item.first = -1;
+            break;
+          }
+        }
+      }
+    }
+    else {
+      // Somehow, we got an end event for a collector we didn't start.
+      // This shouldn't really happen, so we just ignore it.
+    }
+  }
+
+  // Add all unclosed bars.
+  while (!stack.empty()) {
+    int collector_index = stack.back().first;
+    if (collector_index >= 0) {
+      double start_time = stack.back().second;
+      thread_row._rows[stack.size() - 1].push_back({
+        start_time, frame_data.get_end(),
+        collector_index, thread_index, frame_number,
+      });
+    }
+    stack.pop_back();
+  }
+
+  if (thread_row._last_frame >= 0 && frame_number < thread_row._last_frame) {
+    // Added a frame out of order.  Resort the rows.
+    for (Row &row : thread_row._rows) {
+      std::sort(row.begin(), row.end());
+    }
+  } else {
+    thread_row._last_frame = frame_number;
+  }
+
+  return changed_num_rows;
+}
+
+/**
+ * Called when the mouse hovers over the graph, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string PStatTimeline::
+get_bar_tooltip(int row, int x) const {
+  ColorBar bar;
+  if (find_bar(row, x, bar)) {
+    const PStatClientData *client_data = _monitor->get_client_data();
+    if (client_data != nullptr && client_data->has_collector(bar._collector_index)) {
+      std::ostringstream text;
+      text << client_data->get_collector_fullname(bar._collector_index);
+      text << " (" << format_number(bar._end - bar._start, GBU_show_units | GBU_ms) << ")";
+      return text.str();
+    }
+  }
+  return std::string();
+}
+
+/**
+ * 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 PStatTimeline::
+changed_size(int xsize, int ysize) {
+  if (xsize != _xsize || ysize != _ysize) {
+    _xsize = xsize;
+    _ysize = ysize;
+
+    normal_guide_bars();
+    force_redraw();
+  }
+}
+
+/**
+ * To be called by the user class when the whole thing needs to be redrawn for
+ * some reason.
+ */
+void PStatTimeline::
+force_redraw() {
+  clear_region();
+
+  begin_draw();
+
+  for (const GuideBar &bar : _guide_bars) {
+    int x = timestamp_to_pixel(bar._height);
+    if (x > 0 && x < get_xsize() - 1) {
+      draw_guide_bar(x, bar._style);
+    }
+  }
+
+  double start_time = _start_time;
+  double end_time = start_time + get_horizontal_scale();
+
+  int num_rows = 0;
+
+  for (size_t ti = 0; ti < _threads.size(); ++ti) {
+    ThreadRow &thread_row = _threads[ti];
+    for (size_t ri = 0; ri < thread_row._rows.size(); ++ri) {
+      draw_row((int)ti, (int)ri, start_time, end_time);
+      ++num_rows;
+    }
+    draw_separator(num_rows++);
+  }
+
+  end_draw();
+}
+
+/**
+ * To be called by the user class when the whole thing needs to be redrawn for
+ * some reason.
+ */
+void PStatTimeline::
+force_redraw(int row, int from_x, int to_x) {
+  double start_time = std::max(_start_time, pixel_to_timestamp(from_x));
+  double end_time = std::min(_start_time + get_horizontal_scale(), pixel_to_timestamp(to_x));
+
+  begin_draw();
+
+  for (size_t ti = 0; ti < _threads.size(); ++ti) {
+    ThreadRow &thread_row = _threads[ti];
+    if (thread_row._row_offset > row) {
+      break;
+    }
+
+    int row_index = row - (int)thread_row._row_offset;
+    if (row_index < thread_row._rows.size()) {
+      draw_row((int)ti, row_index, start_time, end_time);
+    }
+  }
+
+  end_draw();
+}
+
+/**
+ * Calls update_guide_bars with parameters suitable to this kind of graph.
+ */
+void PStatTimeline::
+normal_guide_bars() {
+  double start_time = get_horizontal_scroll();
+  double time_width = get_horizontal_scale();
+  double end_time = start_time + time_width;
+
+  // We want vaguely 150 pixels between guide bars.
+  int max_frames = get_xsize() / 100;
+  int l = (int)std::floor(3.0 * log10(pixel_to_height(150)) + 0.5);
+  double interval = pow(10.0, std::ceil(l / 3.0));
+  if ((l + 3000) % 3 == 1) {
+    interval /= 5;
+  }
+  else if ((l + 3000) % 3 == 2) {
+    interval /= 2;
+  }
+
+  _guide_bars.clear();
+
+  // Rather than getting the client data, we look in the color bar data for
+  // the first row, because the client data gets wiped after a while.
+  if (!_threads.empty() && !_threads[0]._rows.empty()) {
+    const Row &row = _threads[0]._rows[0];
+
+    // Look for the last Frame bar with end time lower than our start time.
+    Row::const_iterator it = std::lower_bound(row.begin(), row.end(), ColorBar {0.0, start_time});
+    while (it != row.end() && it->_collector_index != 0) {
+      ++it;
+    }
+
+    int num_frames = 0;
+
+    while (it != row.end() && it->_start <= end_time) {
+      double frame_start = it->_start;
+
+      if (frame_start > start_time) {
+        if (!_guide_bars.empty() && height_to_pixel(frame_start - _guide_bars.back()._height) < 30) {
+          // Get rid of last label, it is in the way.
+          _guide_bars.back()._label.clear();
+        }
+        std::string label = "#";
+        label += format_string(it->_frame_number);
+        _guide_bars.push_back(GuideBar(frame_start, label, GBS_frame));
+
+        if (++num_frames > max_frames) {
+          // Forget it, this is becoming too many lines.
+          _guide_bars.clear();
+          break;
+        }
+      }
+
+      do {
+        ++it;
+      }
+      while (it != row.end() && it->_collector_index != 0);
+
+      double frame_width;
+      if (it != row.end()) {
+        // Only go up to the start of the next frame, limiting to however much
+        // fits in the graph.
+        frame_width = std::min(it->_start - frame_start, end_time - frame_start);
+      } else {
+        // Reached the end; just continue to the end of the graph.
+        frame_width = end_time - frame_start;
+      }
+
+      if (interval > 0.0) {
+        int first_bar = std::max((int)((start_time - frame_start) / interval), 1);
+        int num_bars = (int)std::round(frame_width / interval);
+
+        for (int i = first_bar; i < num_bars; ++i) {
+          double offset = i * interval;
+          std::string label = "+";
+          label += format_number(offset, GBU_show_units | GBU_ms);
+          _guide_bars.push_back(GuideBar(frame_start + offset, label, GBS_normal));
+        }
+      }
+    }
+  }
+
+  if (_guide_bars.empty() && interval > 0.0) {
+    int first_bar = std::max((int)(start_time / interval), 1);
+    int num_bars = (int)std::round(end_time / interval);
+
+    for (int i = first_bar; i < num_bars; ++i) {
+      double time = i * interval;
+      std::string label = format_number(time, GBU_show_units | GBU_ms);
+      _guide_bars.push_back(GuideBar(time, label, GBS_frame));
+    }
+  }
+
+  _guide_bars_changed = true;
+}
+
+/**
+ * Should be overridden by the user class to wipe out the entire strip chart
+ * region.
+ */
+void PStatTimeline::
+clear_region() {
+}
+
+/**
+ * Should be overridden by the user class.  This hook will be called before
+ * drawing any bars in the chart.
+ */
+void PStatTimeline::
+begin_draw() {
+}
+
+/**
+ *
+ */
+void PStatTimeline::
+draw_thread(int thread_index, double start_time, double end_time) {
+  if (thread_index < 0 || (size_t)thread_index > _threads.size()) {
+    return;
+  }
+
+  ThreadRow &thread_row = _threads[(size_t)thread_index];
+  for (size_t ri = 0; ri < thread_row._rows.size(); ++ri) {
+    draw_row(thread_index, (int)ri, start_time, end_time);
+  }
+}
+
+/**
+ *
+ */
+void PStatTimeline::
+draw_row(int thread_index, int row_index, double start_time, double end_time) {
+  ThreadRow &thread_row = _threads[thread_index];
+  Row &row = thread_row._rows[row_index];
+
+  const PStatClientData *client_data = _monitor->get_client_data();
+
+  // Find the first element whose end time is larger than our start time.
+  // Then iterate until at least the end of the frame.
+  Row::iterator it = std::lower_bound(row.begin(), row.end(), ColorBar {0.0, start_time});
+  if (it == row.end()) {
+    return;
+  }
+
+  int frame_number = it->_frame_number;
+  do {
+    ColorBar &bar = *it;
+
+    int from_x = timestamp_to_pixel(bar._start);
+    int to_x = timestamp_to_pixel(bar._end);
+
+    if (to_x >= 0 && to_x > from_x && from_x < get_xsize()) {
+      if (bar._collector_index != 0) {
+        draw_bar(thread_row._row_offset + row_index, from_x, to_x,
+                 bar._collector_index,
+                 client_data->get_collector_name(bar._collector_index));
+      } else {
+        draw_bar(thread_row._row_offset + row_index, from_x, to_x,
+                 bar._collector_index,
+                 std::string("Frame ") + format_string(bar._frame_number));
+      }
+    }
+
+    ++it;
+  }
+  while (it != row.end() && (it->_start <= end_time || it->_frame_number == frame_number));
+}
+
+/**
+ * Draws a horizontal separator.
+ */
+void PStatTimeline::
+draw_separator(int) {
+}
+
+/**
+ * Draws a vertical guide bar.  If the row is -1, draws it in all rows.
+ */
+void PStatTimeline::
+draw_guide_bar(int x, GuideBarStyle style) {
+}
+
+/**
+ * Draws a single bar in the chart for the indicated row, in the color for the
+ * given collector, for the indicated horizontal pixel range.
+ */
+void PStatTimeline::
+draw_bar(int, int, int, int, const std::string &) {
+}
+
+/**
+ * Should be overridden by the user class.  This hook will be called after
+ * drawing a series of color bars in the chart.
+ */
+void PStatTimeline::
+end_draw() {
+}
+
+/**
+ * Should be overridden by the user class to perform any other updates might
+ * be necessary after the bars have been redrawn.
+ */
+void PStatTimeline::
+idle() {
+}
+
+/**
+ * Should be called periodically to update any animated values.  Returns false
+ * to indicate that the animation is done and no longer needs to be called.
+ */
+bool PStatTimeline::
+animate(double time, double dt) {
+  int hmove = ((_keys_held & (F_right | F_d)) != 0)
+            - ((_keys_held & (F_left | F_a)) != 0);
+  int vmove = ((_keys_held & F_w) != 0)
+            - ((_keys_held & F_s) != 0);
+
+  if (hmove > 0) {
+    if (_scroll_speed < 0) {
+      _scroll_speed = 1.0;
+    }
+    _scroll_speed += 1.0;
+  }
+  else if (hmove < 0) {
+    if (_scroll_speed > 0) {
+      _scroll_speed = -1.0;
+    }
+    _scroll_speed -= 1.0;
+  }
+  else if (_scroll_speed != 0.0) {
+    _scroll_speed *= std::exp(-12.0 * dt);
+    if (std::abs(_scroll_speed) < 0.2) {
+      _scroll_speed = 0.0;
+    }
+  }
+
+  if (vmove > 0) {
+    if (_zoom_speed < 0) {
+      _zoom_speed = 1.0;
+    }
+    _zoom_speed += 1.0;
+  }
+  else if (vmove < 0) {
+    if (_zoom_speed > 0) {
+      _zoom_speed = -1.0;
+    }
+    _zoom_speed -= 1.0;
+  }
+  else if (_zoom_speed != 0.0) {
+    _zoom_speed *= std::exp(-12.0 * dt);
+    if (std::abs(_zoom_speed) < 0.2) {
+      _zoom_speed = 0.0;
+    }
+  }
+
+  if (_zoom_speed != 0.0) {
+    zoom_to(get_horizontal_scale() * pow(0.5, _zoom_speed * dt), _zoom_center);
+  }
+
+  if (_scroll_speed != 0.0) {
+    scroll_by(_scroll_speed * 300 * _time_scale * dt);
+  }
+
+  if (_target_start_time != _start_time) {
+    double dist = _target_start_time - _start_time;
+    // When the difference is less than 2 pixels, snap to target position.
+    if (std::abs(dist) < _time_scale * 2) {
+      _start_time = _target_start_time;
+    } else {
+      dist *= 1.0 - std::exp(-12.0 * dt);
+      _start_time += dist;
+    }
+  }
+
+  if (_target_time_scale != _time_scale) {
+    //double dist = std::log(_target_time_scale) - std::log(_time_scale);
+    double dist = _target_time_scale - _time_scale;
+    if (_target_start_time == _start_time && std::abs(dist) < 0.01) {
+      _time_scale = _target_time_scale;
+    } else {
+      dist *= 1.0 - std::exp(-12.0 * dt);
+      //_time_scale *= std::exp(dist);
+      _time_scale += dist;
+    }
+  }
+
+  normal_guide_bars();
+  force_redraw();
+
+  // Stop the animation when the speed is 0 and no key is still held.
+  return _keys_held != 0
+      || _scroll_speed != 0
+      || _zoom_speed != 0
+      || _target_start_time != _start_time
+      || _target_time_scale != _time_scale;
+}
+
+/**
+ * Return the ColorBar at the indicated position.
+ */
+bool PStatTimeline::
+find_bar(int row, int x, ColorBar &bar) const {
+  double time = pixel_to_timestamp(x);
+
+  for (size_t ti = 0; ti < _threads.size(); ++ti) {
+    const ThreadRow &thread_row = _threads[ti];
+    if (thread_row._row_offset > row) {
+      break;
+    }
+
+    int row_index = row - (int)thread_row._row_offset;
+    if (row_index < thread_row._rows.size()) {
+      // Find the first element whose end time is larger than the given time.
+      const Row &bars = thread_row._rows[row_index];
+      Row::const_iterator it = std::lower_bound(bars.begin(), bars.end(), ColorBar {time, time});
+      if (it != bars.end() && it->_start <= time) {
+        bar = *it;
+        return true;
+      }
+    }
+  }
+
+  return false;
+}

+ 130 - 0
pandatool/src/pstatserver/pStatTimeline.h

@@ -0,0 +1,130 @@
+/**
+ * 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 pStatTimeline.h
+ * @author rdb
+ * @date 2022-02-11
+ */
+
+#ifndef PSTATTIMELINE_H
+#define PSTATTIMELINE_H
+
+#include "pandatoolbase.h"
+
+#include "pStatGraph.h"
+#include "pStatMonitor.h"
+#include "pStatClientData.h"
+#include "pdeque.h"
+
+class PStatFrameData;
+
+/**
+ * This is an abstract class that presents the interface for drawing a piano-
+ * roll type 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 pnages all the piano-roll logic; the actual nuts and bolts
+ * of drawing pixels is left to a user-derived class.
+ */
+class PStatTimeline : public PStatGraph {
+public:
+  PStatTimeline(PStatMonitor *monitor, int xsize, int ysize);
+  virtual ~PStatTimeline();
+
+  void new_data(int thread_index, int frame_number);
+  bool update_bars(int thread_index, int frame_number);
+
+  INLINE void set_horizontal_scale(double time_width);
+  INLINE double get_horizontal_scale() const;
+  INLINE void set_horizontal_scroll(double time_start);
+  INLINE double get_horizontal_scroll() const;
+
+  INLINE void zoom_to(double time_width, double pivot);
+  INLINE void zoom_by(double amount, double center);
+  INLINE void scroll_to(double time_start);
+  INLINE void scroll_by(double time_start);
+
+  INLINE int timestamp_to_pixel(double time) const;
+  INLINE double pixel_to_timestamp(int x) const;
+  INLINE int height_to_pixel(double value) const;
+  INLINE double pixel_to_height(int y) const;
+
+  std::string get_bar_tooltip(int row, int x) const;
+
+protected:
+  void changed_size(int xsize, int ysize);
+  void force_redraw();
+  void force_redraw(int row, int from_x, int to_x);
+  void normal_guide_bars();
+
+  virtual void clear_region();
+  virtual void begin_draw();
+  void draw_thread(int thread_index, double start_time, double end_time);
+  void draw_row(int thread_index, int row_index, double start_time, double end_time);
+  virtual void draw_separator(int row);
+  virtual void draw_guide_bar(int x, GuideBarStyle style);
+  virtual void draw_bar(int row, int from_x, int to_x, int collector_index,
+                        const std::string &collector_name);
+  virtual void end_draw();
+  virtual void idle();
+
+  bool animate(double time, double dt);
+
+  class ColorBar {
+  public:
+    double _start, _end;
+    int _collector_index;
+    int _thread_index;
+    int _frame_number;
+
+    bool operator < (const ColorBar &other) const {
+      return _end < other._end;
+    }
+  };
+  typedef pvector<ColorBar> Row;
+  typedef pvector<Row> Rows;
+
+  bool find_bar(int row, int x, ColorBar &bar) const;
+
+  class ThreadRow {
+  public:
+    std::string _label;
+    Rows _rows;
+    size_t _row_offset = 0;
+    int _last_frame = -1;
+  };
+  typedef pvector<ThreadRow> ThreadRows;
+  ThreadRows _threads;
+  bool _threads_changed = true;
+
+  enum KeyFlag {
+    F_left = 1,
+    F_right = 2,
+    F_w = 4,
+    F_a = 8,
+    F_s = 16,
+    F_d = 32,
+  };
+  int _keys_held = 0;
+  double _scroll_speed = 0.0;
+  double _zoom_speed = 0.0;
+  double _zoom_center = 0.0;
+
+private:
+  double _time_scale;
+  double _start_time = 0.0;
+  double _lowest_start_time = 0.0;
+  double _highest_end_time = 0.0;
+  bool _have_start_time = false;
+  double _target_start_time = 0.0;
+  double _target_time_scale;
+};
+
+#include "pStatTimeline.I"
+
+#endif

+ 10 - 9
pandatool/src/pstatserver/pStatView.cxx

@@ -520,15 +520,16 @@ reset_level(PStatViewLevel *level) {
 
 
     if (level->_parent == nullptr) {
     if (level->_parent == nullptr) {
       // This level didn't know its parent before, but now it does.
       // This level didn't know its parent before, but now it does.
-      PStatViewLevel *parent_level = get_level(parent_index);
-      nassertr(parent_level != level, true);
-
-      level->_parent = parent_level;
-      parent_level->_children.push_back(level);
-      parent_level->sort_children(_client_data);
-      any_changed = true;
-
-    } else if (level->_parent->_collector != parent_index) {
+      if (level->_collector != 0 || parent_index != 0) {
+        PStatViewLevel *parent_level = get_level(parent_index);
+        nassertr(parent_level != level, true);
+        level->_parent = parent_level;
+        parent_level->_children.push_back(level);
+        parent_level->sort_children(_client_data);
+        any_changed = true;
+      }
+    }
+    else if (level->_parent->_collector != parent_index) {
       // This level knew about its parent, but now it's something different.
       // This level knew about its parent, but now it's something different.
       PStatViewLevel *old_parent_level = level->_parent;
       PStatViewLevel *old_parent_level = level->_parent;
       nassertr(old_parent_level != level, true);
       nassertr(old_parent_level != level, true);

+ 2 - 0
pandatool/src/win-stats/CMakeLists.txt

@@ -14,6 +14,7 @@ set(WINSTATS_HEADERS
   winStatsPianoRoll.h
   winStatsPianoRoll.h
   winStatsServer.h
   winStatsServer.h
   winStatsStripChart.h
   winStatsStripChart.h
+  winStatsTimeline.h
 )
 )
 
 
 set(WINSTATS_SOURCES
 set(WINSTATS_SOURCES
@@ -27,6 +28,7 @@ set(WINSTATS_SOURCES
   winStatsPianoRoll.cxx
   winStatsPianoRoll.cxx
   winStatsServer.cxx
   winStatsServer.cxx
   winStatsStripChart.cxx
   winStatsStripChart.cxx
+  winStatsTimeline.cxx
 )
 )
 
 
 composite_sources(win-stats WINSTATS_SOURCES)
 composite_sources(win-stats WINSTATS_SOURCES)

+ 79 - 34
pandatool/src/win-stats/winStatsChartMenu.cxx

@@ -93,14 +93,30 @@ do_update() {
   }
   }
 
 
   // Now rebuild the menu with the new set of entries.
   // Now rebuild the menu with the new set of entries.
+  MENUITEMINFO mii;
+  memset(&mii, 0, sizeof(mii));
+  mii.cbSize = sizeof(mii);
+
+  if (_thread_index == 0) {
+    // Timeline goes first.
+    WinStatsMonitor::MenuDef menu_def(_thread_index, -1, WinStatsMonitor::CT_timeline, false);
+    int menu_id = _monitor->get_menu_id(menu_def);
 
 
-  // The menu item(s) for the thread's frame time goes first.
+    mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID;
+    mii.fType = MFT_STRING;
+    mii.wID = menu_id;
+    mii.dwTypeData = "Timeline";
+    InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii);
+
+    mii.fMask = MIIM_FTYPE;
+    mii.fType = MFT_SEPARATOR;
+    InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii);
+  }
+
+  // The menu item(s) for the thread's frame time goes second.
   add_view(_menu, view.get_top_level(), false);
   add_view(_menu, view.get_top_level(), false);
 
 
   bool needs_separator = true;
   bool needs_separator = true;
-  MENUITEMINFO mii;
-  memset(&mii, 0, sizeof(mii));
-  mii.cbSize = sizeof(mii);
 
 
   // And then the menu item(s) for each of the level values.
   // And then the menu item(s) for each of the level values.
   const PStatClientData *client_data = _monitor->get_client_data();
   const PStatClientData *client_data = _monitor->get_client_data();
@@ -125,24 +141,13 @@ do_update() {
     }
     }
   }
   }
 
 
-  // Also menu items for flame graph and piano roll (following a separator).
+  // Also menu item for piano roll (following a separator).
   mii.fMask = MIIM_FTYPE;
   mii.fMask = MIIM_FTYPE;
   mii.fType = MFT_SEPARATOR;
   mii.fType = MFT_SEPARATOR;
   InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii);
   InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii);
 
 
   {
   {
-    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 = "Flame Graph";
-    InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii);
-  }
-
-  {
-    WinStatsMonitor::MenuDef menu_def(_thread_index, -1, false);
+    WinStatsMonitor::MenuDef menu_def(_thread_index, -1, WinStatsMonitor::CT_piano_roll, false);
     int menu_id = _monitor->get_menu_id(menu_def);
     int menu_id = _monitor->get_menu_id(menu_def);
 
 
     mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID;
     mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID;
@@ -164,37 +169,77 @@ add_view(HMENU parent_menu, const PStatViewLevel *view_level, bool show_level) {
   const PStatClientData *client_data = _monitor->get_client_data();
   const PStatClientData *client_data = _monitor->get_client_data();
   std::string collector_name = client_data->get_collector_name(collector);
   std::string collector_name = client_data->get_collector_name(collector);
 
 
-  WinStatsMonitor::MenuDef menu_def(_thread_index, collector, show_level);
-  int menu_id = _monitor->get_menu_id(menu_def);
-
   MENUITEMINFO mii;
   MENUITEMINFO mii;
   memset(&mii, 0, sizeof(mii));
   memset(&mii, 0, sizeof(mii));
   mii.cbSize = sizeof(mii);
   mii.cbSize = sizeof(mii);
 
 
-  mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID;
-  mii.fType = MFT_STRING;
-  mii.wID = menu_id;
-  mii.dwTypeData = (char *)collector_name.c_str();
-  InsertMenuItem(parent_menu, GetMenuItemCount(parent_menu), TRUE, &mii);
-
   int num_children = view_level->get_num_children();
   int num_children = view_level->get_num_children();
-  if (num_children > 1) {
-    // If the collector has more than one child, add a menu entry to go
-    // directly to each of its children.
-    HMENU submenu = CreatePopupMenu();
-    std::string submenu_name = collector_name + " components";
+  if (show_level && num_children == 0) {
+    // For a level collector without children, no point in making a submenu.
+    WinStatsMonitor::MenuDef menu_def(_thread_index, collector, WinStatsMonitor::CT_strip_chart, show_level);
+    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 = (char *)collector_name.c_str();
+    InsertMenuItem(parent_menu, GetMenuItemCount(parent_menu), TRUE, &mii);
+    return;
+  }
+
+  HMENU menu;
+  if (!show_level && collector == 0 && num_children == 0) {
+    // Root collector without children, just add the options directly to the
+    // parent menu.
+    menu = parent_menu;
+  }
+  else {
+    // Create a submenu.
+    menu = CreatePopupMenu();
 
 
     mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_SUBMENU;
     mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_SUBMENU;
     mii.fType = MFT_STRING;
     mii.fType = MFT_STRING;
-    mii.hSubMenu = submenu;
-    mii.dwTypeData = (char *)submenu_name.c_str();
+    mii.hSubMenu = menu;
+    mii.dwTypeData = (char *)collector_name.c_str();
     InsertMenuItem(parent_menu, GetMenuItemCount(parent_menu), TRUE, &mii);
     InsertMenuItem(parent_menu, GetMenuItemCount(parent_menu), TRUE, &mii);
+  }
+
+  {
+    WinStatsMonitor::MenuDef menu_def(_thread_index, collector, WinStatsMonitor::CT_strip_chart, show_level);
+    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 = "Open Strip Chart";
+    InsertMenuItem(menu, GetMenuItemCount(menu), TRUE, &mii);
+  }
+
+  if (!show_level) {
+    if (collector == 0 && num_children == 0) {
+      collector = -1;
+    }
+
+    WinStatsMonitor::MenuDef menu_def(_thread_index, collector, WinStatsMonitor::CT_flame_graph);
+    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 = "Open Flame Graph";
+    InsertMenuItem(menu, GetMenuItemCount(menu), TRUE, &mii);
+  }
+
+  if (num_children > 0) {
+    mii.fMask = MIIM_FTYPE;
+    mii.fType = MFT_SEPARATOR;
+    InsertMenuItem(menu, GetMenuItemCount(menu), TRUE, &mii);
 
 
     // Reverse the order since the menus are listed from the top down; we want
     // Reverse the order since the menus are listed from the top down; we want
     // to be visually consistent with the graphs, which list these labels from
     // to be visually consistent with the graphs, which list these labels from
     // the bottom up.
     // the bottom up.
     for (int c = num_children - 1; c >= 0; c--) {
     for (int c = num_children - 1; c >= 0; c--) {
-      add_view(submenu, view_level->get_child(c), show_level);
+      add_view(menu, view_level->get_child(c), show_level);
     }
     }
   }
   }
 }
 }

+ 176 - 84
pandatool/src/win-stats/winStatsFlameGraph.cxx

@@ -18,8 +18,8 @@
 
 
 #include <commctrl.h>
 #include <commctrl.h>
 
 
-static const int default_flame_graph_width = 800;
-static const int default_flame_graph_height = 150;
+static const int default_flame_graph_width = 1085;
+static const int default_flame_graph_height = 210;
 
 
 bool WinStatsFlameGraph::_window_class_registered = false;
 bool WinStatsFlameGraph::_window_class_registered = false;
 const char * const WinStatsFlameGraph::_window_class_name = "flame";
 const char * const WinStatsFlameGraph::_window_class_name = "flame";
@@ -30,7 +30,7 @@ const char * const WinStatsFlameGraph::_window_class_name = "flame";
 WinStatsFlameGraph::
 WinStatsFlameGraph::
 WinStatsFlameGraph(WinStatsMonitor *monitor, int thread_index,
 WinStatsFlameGraph(WinStatsMonitor *monitor, int thread_index,
                    int collector_index) :
                    int collector_index) :
-  PStatFlameGraph(monitor, monitor->get_view(thread_index),
+  PStatFlameGraph(monitor,
                   thread_index, collector_index,
                   thread_index, collector_index,
                   monitor->get_pixel_scale() * default_flame_graph_width / 4,
                   monitor->get_pixel_scale() * default_flame_graph_width / 4,
                   monitor->get_pixel_scale() * default_flame_graph_height / 4),
                   monitor->get_pixel_scale() * default_flame_graph_height / 4),
@@ -127,34 +127,7 @@ set_time_units(int unit_mask) {
  */
  */
 void WinStatsFlameGraph::
 void WinStatsFlameGraph::
 on_click_label(int collector_index) {
 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);
-    }
-  }
+  set_collector_index(collector_index);
 }
 }
 
 
 /**
 /**
@@ -164,6 +137,11 @@ void WinStatsFlameGraph::
 on_enter_label(int collector_index) {
 on_enter_label(int collector_index) {
   if (collector_index != _highlighted_index) {
   if (collector_index != _highlighted_index) {
     _highlighted_index = collector_index;
     _highlighted_index = collector_index;
+    clear_graph_tooltip();
+
+    if (!get_average_mode()) {
+      PStatFlameGraph::force_redraw();
+    }
   }
   }
 }
 }
 
 
@@ -174,53 +152,11 @@ void WinStatsFlameGraph::
 on_leave_label(int collector_index) {
 on_leave_label(int collector_index) {
   if (collector_index == _highlighted_index && collector_index != -1) {
   if (collector_index == _highlighted_index && collector_index != -1) {
     _highlighted_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;
+    if (!get_average_mode()) {
+      PStatFlameGraph::force_redraw();
     }
     }
-    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));
 }
 }
 
 
 /**
 /**
@@ -263,6 +199,56 @@ begin_draw() {
   for (int i = 0; i < num_guide_bars; i++) {
   for (int i = 0; i < num_guide_bars; i++) {
     draw_guide_bar(_bitmap_dc, get_guide_bar(i));
     draw_guide_bar(_bitmap_dc, get_guide_bar(i));
   }
   }
+
+  SelectObject(_bitmap_dc, WinStatsGraph::_monitor->get_font());
+  SelectObject(_bitmap_dc, GetStockObject(NULL_PEN));
+  SetBkMode(_bitmap_dc, TRANSPARENT);
+  SetTextAlign(_bitmap_dc, TA_LEFT | TA_TOP | TA_NOUPDATECP);
+}
+
+/**
+ * Should be overridden by the user class.  Should draw a single bar at the
+ * indicated location.
+ */
+void WinStatsFlameGraph::
+draw_bar(int depth, int from_x, int to_x, int collector_index) {
+  int bottom = get_ysize() - 1 - depth * _pixel_scale * 5;
+  int top = bottom - _pixel_scale * 5;
+
+  bool is_highlighted = collector_index == _highlighted_index;
+  HBRUSH brush = get_collector_brush(collector_index, is_highlighted);
+
+  if (to_x < from_x + 2) {
+    // It's just a tiny sliver.  This is a more reliable way to draw it.
+    RECT rect = {from_x, top + 1, from_x + 1, bottom - 1};
+    FillRect(_bitmap_dc, &rect, brush);
+  }
+  else {
+    SelectObject(_bitmap_dc, brush);
+    RoundRect(_bitmap_dc,
+              std::max(from_x, -_pixel_scale - 1),
+              top,
+              std::min(std::max(to_x, from_x + 1), get_xsize() + _pixel_scale),
+              bottom,
+              _pixel_scale,
+              _pixel_scale);
+
+    int left = std::max(from_x, 0) + _pixel_scale / 2;
+    int right = std::min(to_x, get_xsize()) - _pixel_scale / 2;
+
+    if ((to_x - from_x) >= _pixel_scale * 4) {
+      // Only bother drawing the text if we've got some space to draw on.
+      // Choose a suitable foreground color.
+      SetTextColor(_bitmap_dc, get_collector_text_color(collector_index, is_highlighted));
+
+      const PStatClientData *client_data = WinStatsGraph::_monitor->get_client_data();
+      const std::string &name = client_data->get_collector_name(collector_index);
+
+      RECT rect = {left, top, right, bottom};
+      DrawText(_bitmap_dc, name.data(), name.size(),
+               &rect, DT_LEFT | DT_END_ELLIPSIS | DT_SINGLELINE | DT_VCENTER);
+    }
+  }
 }
 }
 
 
 /**
 /**
@@ -281,6 +267,15 @@ void WinStatsFlameGraph::
 idle() {
 idle() {
 }
 }
 
 
+/**
+ * Overridden by a derived class to implement an animation.  If it returns
+ * false, the animation timer is stopped.
+ */
+bool WinStatsFlameGraph::
+animate(double time, double dt) {
+  return PStatFlameGraph::animate(time, dt);
+}
+
 /**
 /**
  *
  *
  */
  */
@@ -300,10 +295,27 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
     case BN_CLICKED:
     case BN_CLICKED:
       if ((HWND)lparam == _average_check_box) {
       if ((HWND)lparam == _average_check_box) {
         int result = SendMessage(_average_check_box, BM_GETCHECK, 0, 0);
         int result = SendMessage(_average_check_box, BM_GETCHECK, 0, 0);
-        set_average_mode(result == BST_CHECKED);
+        if (result == BST_CHECKED) {
+          set_average_mode(true);
+          start_animation();
+        } else {
+          set_average_mode(false);
+        }
         return 0;
         return 0;
       }
       }
       break;
       break;
+
+    case 101:
+      set_collector_index(_popup_index);
+      return 0;
+
+    case 102:
+      WinStatsGraph::_monitor->open_strip_chart(get_thread_index(), _popup_index, false);
+      return 0;
+
+    case 103:
+      WinStatsGraph::_monitor->open_flame_graph(get_thread_index(), _popup_index);
+      return 0;
     }
     }
     break;
     break;
 
 
@@ -331,7 +343,25 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
     break;
     break;
 
 
   case WM_MOUSEMOVE:
   case WM_MOUSEMOVE:
-    if (_drag_mode == DM_new_guide_bar) {
+    if (_drag_mode == DM_none && _potential_drag_mode == DM_none) {
+      // When the mouse is over a color bar, highlight it.
+      int x = LOWORD(lparam);
+      int y = HIWORD(lparam);
+
+      int collector_index = get_bar_collector(pixel_to_depth(y), x);
+      on_enter_label(collector_index);
+
+      // 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 if (_drag_mode == DM_new_guide_bar) {
       // We haven't created the new guide bar yet; we won't until the mouse
       // We haven't created the new guide bar yet; we won't until the mouse
       // comes within the graph's region.
       // comes within the graph's region.
       int16_t x = LOWORD(lparam);
       int16_t x = LOWORD(lparam);
@@ -348,6 +378,13 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
     }
     }
     break;
     break;
 
 
+  case WM_MOUSELEAVE:
+    // When the mouse leaves the graph, stop highlighting.
+    if (_highlighted_index != -1) {
+      on_leave_label(_highlighted_index);
+    }
+    break;
+
   case WM_LBUTTONUP:
   case WM_LBUTTONUP:
     if (_drag_mode == DM_guide_bar) {
     if (_drag_mode == DM_guide_bar) {
       int16_t x = LOWORD(lparam);
       int16_t x = LOWORD(lparam);
@@ -364,8 +401,42 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
 
 
   case WM_LBUTTONDBLCLK:
   case WM_LBUTTONDBLCLK:
     {
     {
-      // Clicking on whitespace in the graph goes to the parent.
-      on_click_label(get_collector_index());
+      // Double-clicking on a color bar in the graph will zoom the graph into
+      // that collector.
+      int16_t x = LOWORD(lparam);
+      int16_t y = HIWORD(lparam);
+      set_collector_index(get_bar_collector(pixel_to_depth(y), x));
+      return 0;
+    }
+    break;
+
+  case WM_CONTEXTMENU:
+    {
+      POINT point;
+      if (GetCursorPos(&point)) {
+        POINT graph_point =  point;
+        if (ScreenToClient(_graph_window, &graph_point)) {
+          int depth = pixel_to_depth(graph_point.y);
+          int collector_index = get_bar_collector(depth, graph_point.x);
+          if (collector_index >= 0) {
+            _popup_index = collector_index;
+            HMENU popup = CreatePopupMenu();
+
+            std::string label = get_bar_tooltip(depth, graph_point.x);
+            if (!label.empty()) {
+              AppendMenu(popup, MF_STRING | MF_DISABLED, 0, label.c_str());
+            }
+            if (collector_index == get_collector_index()) {
+              AppendMenu(popup, MF_STRING | MF_DISABLED, 101, "Set as Focus");
+            } else {
+              AppendMenu(popup, MF_STRING, 101, "Set as Focus");
+            }
+            AppendMenu(popup, MF_STRING, 102, "Open Strip Chart");
+            AppendMenu(popup, MF_STRING, 103, "Open Flame Graph");
+            TrackPopupMenu(popup, TPM_LEFTBUTTON, point.x, point.y, 0, _window, nullptr);
+          }
+        }
+      }
       return 0;
       return 0;
     }
     }
     break;
     break;
@@ -425,6 +496,15 @@ additional_graph_window_paint(HDC hdc) {
   }
   }
 }
 }
 
 
+/**
+ * Called when the mouse hovers over the graph, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string WinStatsFlameGraph::
+get_graph_tooltip(int mouse_x, int mouse_y) const {
+  return get_bar_tooltip(pixel_to_depth(mouse_y), mouse_x);
+}
+
 /**
 /**
  * Based on the mouse position within the window's client area, look for
  * Based on the mouse position within the window's client area, look for
  * draggable things the mouse might be hovering over and return the
  * draggable things the mouse might be hovering over and return the
@@ -474,6 +554,14 @@ move_graph_window(int graph_left, int graph_top, int graph_xsize, int graph_ysiz
   }
   }
 }
 }
 
 
+/**
+ * Converts a pixel to a depth index.
+ */
+int WinStatsFlameGraph::
+pixel_to_depth(int y) const {
+  return (get_ysize() - 1 - y) / (_pixel_scale * 5);
+}
+
 /**
 /**
  * Draws the line for the indicated guide bar on the graph.
  * Draws the line for the indicated guide bar on the graph.
  */
  */
@@ -554,6 +642,7 @@ create_window() {
   register_window_class(application);
   register_window_class(application);
 
 
   std::string window_title = get_title_text();
   std::string window_title = get_title_text();
+  POINT window_pos = WinStatsGraph::_monitor->get_new_window_pos();
 
 
   RECT win_rect = {
   RECT win_rect = {
     0, 0,
     0, 0,
@@ -565,11 +654,13 @@ create_window() {
   AdjustWindowRect(&win_rect, graph_window_style, FALSE);
   AdjustWindowRect(&win_rect, graph_window_style, FALSE);
 
 
   _window =
   _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);
+    CreateWindowEx(WS_EX_DLGMODALFRAME, _window_class_name,
+                   window_title.c_str(), graph_window_style,
+                   window_pos.x, window_pos.y,
+                   win_rect.right - win_rect.left,
+                   win_rect.bottom - win_rect.top,
+                   WinStatsGraph::_monitor->get_window(),
+                   nullptr, application, 0);
   if (!_window) {
   if (!_window) {
     nout << "Could not create FlameGraph window!\n";
     nout << "Could not create FlameGraph window!\n";
     exit(1);
     exit(1);
@@ -586,6 +677,7 @@ create_window() {
 
 
   if (get_average_mode()) {
   if (get_average_mode()) {
     SendMessage(_average_check_box, BM_SETCHECK, BST_CHECKED, 0);
     SendMessage(_average_check_box, BM_SETCHECK, BST_CHECKED, 0);
+    start_animation();
   }
   }
 
 
   // Ensure that the window is on top of the stack.
   // Ensure that the window is on top of the stack.

+ 7 - 6
pandatool/src/win-stats/winStatsFlameGraph.h

@@ -28,7 +28,7 @@ class WinStatsLabel;
 class WinStatsFlameGraph : public PStatFlameGraph, public WinStatsGraph {
 class WinStatsFlameGraph : public PStatFlameGraph, public WinStatsGraph {
 public:
 public:
   WinStatsFlameGraph(WinStatsMonitor *monitor, int thread_index,
   WinStatsFlameGraph(WinStatsMonitor *monitor, int thread_index,
-                     int collector_index=0);
+                     int collector_index=-1);
   virtual ~WinStatsFlameGraph();
   virtual ~WinStatsFlameGraph();
 
 
   virtual void new_data(int thread_index, int frame_number);
   virtual void new_data(int thread_index, int frame_number);
@@ -39,28 +39,30 @@ public:
   virtual void on_click_label(int collector_index);
   virtual void on_click_label(int collector_index);
   virtual void on_enter_label(int collector_index);
   virtual void on_enter_label(int collector_index);
   virtual void on_leave_label(int collector_index);
   virtual void on_leave_label(int collector_index);
-  virtual std::string get_label_tooltip(int collector_index) const;
 
 
 protected:
 protected:
-  virtual void update_labels();
-  virtual void update_label(int collector_index, int row, int x, int width);
   virtual void normal_guide_bars();
   virtual void normal_guide_bars();
 
 
   void clear_region();
   void clear_region();
   virtual void begin_draw();
   virtual void begin_draw();
+  virtual void draw_bar(int depth, int from_x, int to_x, int collector_index);
   virtual void end_draw();
   virtual void end_draw();
   virtual void idle();
   virtual void idle();
 
 
+  virtual bool animate(double time, double dt);
+
   LONG window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
   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 LONG graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
   virtual void additional_window_paint(HDC hdc);
   virtual void additional_window_paint(HDC hdc);
   virtual void additional_graph_window_paint(HDC hdc);
   virtual void additional_graph_window_paint(HDC hdc);
+  virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const;
   virtual DragMode consider_drag_start(int mouse_x, int mouse_y,
   virtual DragMode consider_drag_start(int mouse_x, int mouse_y,
                                        int width, int height);
                                        int width, int height);
   virtual void move_graph_window(int graph_left, int graph_top,
   virtual void move_graph_window(int graph_left, int graph_top,
                                  int graph_xsize, int graph_ysize);
                                  int graph_xsize, int graph_ysize);
 
 
 private:
 private:
+  int pixel_to_depth(int y) const;
   void draw_guide_bar(HDC hdc, const GuideBar &bar);
   void draw_guide_bar(HDC hdc, const GuideBar &bar);
   void draw_guide_label(HDC hdc, int y, const PStatGraph::GuideBar &bar);
   void draw_guide_label(HDC hdc, int y, const PStatGraph::GuideBar &bar);
   void create_window();
   void create_window();
@@ -69,9 +71,8 @@ private:
   static LONG WINAPI static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
   static LONG WINAPI static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
 
 
   std::string _net_value_text;
   std::string _net_value_text;
-  pmap<int, WinStatsLabel *> _labels;
-
   HWND _average_check_box;
   HWND _average_check_box;
+  int _popup_index = -1;
 
 
   static bool _window_class_registered;
   static bool _window_class_registered;
   static const char * const _window_class_name;
   static const char * const _window_class_name;

+ 134 - 3
pandatool/src/win-stats/winStatsGraph.cxx

@@ -14,6 +14,7 @@
 #include "winStatsGraph.h"
 #include "winStatsGraph.h"
 #include "winStatsMonitor.h"
 #include "winStatsMonitor.h"
 #include "winStatsLabelStack.h"
 #include "winStatsLabelStack.h"
+#include "trueClock.h"
 #include "convert_srgb.h"
 #include "convert_srgb.h"
 
 
 #include <commctrl.h>
 #include <commctrl.h>
@@ -21,7 +22,7 @@
 #define IDC_GRAPH 100
 #define IDC_GRAPH 100
 
 
 DWORD WinStatsGraph::graph_window_style =
 DWORD WinStatsGraph::graph_window_style =
-WS_CHILD | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_OVERLAPPEDWINDOW | WS_VISIBLE;
+  WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_OVERLAPPEDWINDOW | WS_VISIBLE;
 
 
 /**
 /**
  *
  *
@@ -32,6 +33,7 @@ WinStatsGraph(WinStatsMonitor *monitor) :
 {
 {
   _window = 0;
   _window = 0;
   _graph_window = 0;
   _graph_window = 0;
+  _tooltip_window = 0;
   _sizewe_cursor = LoadCursor(nullptr, IDC_SIZEWE);
   _sizewe_cursor = LoadCursor(nullptr, IDC_SIZEWE);
   _hand_cursor = LoadCursor(nullptr, IDC_HAND);
   _hand_cursor = LoadCursor(nullptr, IDC_HAND);
   _bitmap = 0;
   _bitmap = 0;
@@ -47,9 +49,11 @@ WinStatsGraph(WinStatsMonitor *monitor) :
   _dark_color = RGB(51, 51, 51);
   _dark_color = RGB(51, 51, 51);
   _light_color = RGB(154, 154, 154);
   _light_color = RGB(154, 154, 154);
   _user_guide_bar_color = RGB(130, 150, 255);
   _user_guide_bar_color = RGB(130, 150, 255);
+  _frame_guide_bar_color = RGB(255, 10, 10);
   _dark_pen = CreatePen(PS_SOLID, 1, _dark_color);
   _dark_pen = CreatePen(PS_SOLID, 1, _dark_color);
   _light_pen = CreatePen(PS_SOLID, 1, _light_color);
   _light_pen = CreatePen(PS_SOLID, 1, _light_color);
   _user_guide_bar_pen = CreatePen(PS_DASH, 1, _user_guide_bar_color);
   _user_guide_bar_pen = CreatePen(PS_DASH, 1, _user_guide_bar_color);
+  _frame_guide_bar_pen = CreatePen(PS_DASH, 1, _frame_guide_bar_color);
 
 
   _drag_mode = DM_none;
   _drag_mode = DM_none;
   _potential_drag_mode = DM_none;
   _potential_drag_mode = DM_none;
@@ -69,12 +73,14 @@ WinStatsGraph::
   DeleteObject(_dark_pen);
   DeleteObject(_dark_pen);
   DeleteObject(_light_pen);
   DeleteObject(_light_pen);
   DeleteObject(_user_guide_bar_pen);
   DeleteObject(_user_guide_bar_pen);
+  DeleteObject(_frame_guide_bar_pen);
 
 
   for (auto &item : _brushes) {
   for (auto &item : _brushes) {
     DeleteObject(item.second.first);
     DeleteObject(item.second.first);
     DeleteObject(item.second.second);
     DeleteObject(item.second.second);
   }
   }
   _brushes.clear();
   _brushes.clear();
+  _text_colors.clear();
 
 
   if (_graph_window) {
   if (_graph_window) {
     DestroyWindow(_graph_window);
     DestroyWindow(_graph_window);
@@ -85,6 +91,11 @@ WinStatsGraph::
     DestroyWindow(_window);
     DestroyWindow(_window);
     _window = 0;
     _window = 0;
   }
   }
+
+  if (_tooltip_window) {
+    DestroyWindow(_tooltip_window);
+    _tooltip_window = 0;
+  }
 }
 }
 
 
 /**
 /**
@@ -150,6 +161,13 @@ void WinStatsGraph::
 on_click_label(int collector_index) {
 on_click_label(int collector_index) {
 }
 }
 
 
+/**
+ * Called when a pop-up menu should be shown for the label.
+ */
+void WinStatsGraph::
+on_popup_label(int collector_index) {
+}
+
 /**
 /**
  * Called when the user hovers the mouse over a label.
  * Called when the user hovers the mouse over a label.
  */
  */
@@ -157,6 +175,7 @@ void WinStatsGraph::
 on_enter_label(int collector_index) {
 on_enter_label(int collector_index) {
   if (collector_index != _highlighted_index) {
   if (collector_index != _highlighted_index) {
     _highlighted_index = collector_index;
     _highlighted_index = collector_index;
+    clear_graph_tooltip();
     force_redraw();
     force_redraw();
   }
   }
 }
 }
@@ -168,6 +187,7 @@ void WinStatsGraph::
 on_leave_label(int collector_index) {
 on_leave_label(int collector_index) {
   if (collector_index == _highlighted_index && collector_index != -1) {
   if (collector_index == _highlighted_index && collector_index != -1) {
     _highlighted_index = -1;
     _highlighted_index = -1;
+    clear_graph_tooltip();
     force_redraw();
     force_redraw();
   }
   }
 }
 }
@@ -181,6 +201,16 @@ get_label_tooltip(int collector_index) const {
   return std::string();
   return std::string();
 }
 }
 
 
+/**
+ * Hides the graph tooltip.
+ */
+void WinStatsGraph::
+clear_graph_tooltip() {
+  if (_tooltip_window != 0) {
+    SendMessage(_tooltip_window, TTM_POP, 0, 0);
+  }
+}
+
 /**
 /**
  * Returns the window handle of the surrounding window.
  * Returns the window handle of the surrounding window.
  */
  */
@@ -229,6 +259,28 @@ move_label_stack() {
   }
   }
 }
 }
 
 
+/**
+ * Turns on the animation timer, if it hasn't already been turned on.
+ */
+void WinStatsGraph::
+start_animation() {
+  if (!_timer_running) {
+    TrueClock *clock = TrueClock::get_global_ptr();
+    _time = clock->get_short_time();
+    SetTimer(_window, 0x100, 16, nullptr);
+    _timer_running = true;
+  }
+}
+
+/**
+ * Overridden by a derived class to implement an animation.  If it returns
+ * false, the animation timer is stopped.
+ */
+bool WinStatsGraph::
+animate(double time, double dt) {
+  return false;
+}
+
 /**
 /**
  * Returns a brush suitable for drawing in the indicated collector's color.
  * Returns a brush suitable for drawing in the indicated collector's color.
  */
  */
@@ -256,6 +308,29 @@ get_collector_brush(int collector_index, bool highlight) {
   return highlight ? hbrush : brush;
   return highlight ? hbrush : brush;
 }
 }
 
 
+/**
+ * Returns a text color suitable for the given collector.
+ */
+COLORREF WinStatsGraph::
+get_collector_text_color(int collector_index, bool highlight) {
+  TextColors::iterator tci;
+  tci = _text_colors.find(collector_index);
+  if (tci != _text_colors.end()) {
+    return highlight ? (*tci).second.second : (*tci).second.first;
+  }
+
+  LRGBColor rgb = _monitor->get_collector_color(collector_index);
+  double bright =
+    rgb[0] * 0.2126 +
+    rgb[1] * 0.7152 +
+    rgb[2] * 0.0722;
+  COLORREF color = bright >= 0.5 ? RGB(0, 0, 0) : RGB(255, 255, 255);
+  COLORREF hcolor = bright * 0.75 >= 0.5 ? RGB(0, 0, 0) : RGB(255, 255, 255);
+
+  _text_colors[collector_index] = std::make_pair(color, hcolor);
+  return highlight ? hcolor : color;
+}
+
 /**
 /**
  * This window_proc should be called up to by the derived classes for any
  * This window_proc should be called up to by the derived classes for any
  * messages that are not specifically handled by the derived class.
  * messages that are not specifically handled by the derived class.
@@ -339,8 +414,9 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
   case WM_MOUSEMOVE:
   case WM_MOUSEMOVE:
     if (_drag_mode == DM_left_margin) {
     if (_drag_mode == DM_left_margin) {
       int16_t x = LOWORD(lparam);
       int16_t x = LOWORD(lparam);
-      _left_margin += (x - _drag_start_x);
-      _drag_start_x = x;
+      int new_left_margin = _left_margin + (x - _drag_start_x);
+      _left_margin = std::max(new_left_margin, _pixel_scale * 2);
+      _drag_start_x = x - (new_left_margin - _left_margin);
       InvalidateRect(hwnd, nullptr, TRUE);
       InvalidateRect(hwnd, nullptr, TRUE);
       move_label_stack();
       move_label_stack();
       return 0;
       return 0;
@@ -407,6 +483,33 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
     }
     }
     break;
     break;
 
 
+  case WM_TIMER:
+    {
+      TrueClock *clock = TrueClock::get_global_ptr();
+      double new_time = clock->get_short_time();
+      if (!animate(new_time, new_time - _time)) {
+        KillTimer(hwnd, 0x100);
+        _timer_running = false;
+      }
+      _time = new_time;
+    }
+    return 0;
+
+  case WM_NOTIFY:
+    switch (((LPNMHDR)lparam)->code) {
+    case TTN_GETDISPINFO:
+      {
+        NMTTDISPINFO &info = *(NMTTDISPINFO *)lparam;
+        POINT point;
+        if (GetCursorPos(&point) && ScreenToClient(_graph_window, &point)) {
+          _tooltip_text = get_graph_tooltip(point.x, point.y);
+          info.lpszText = (char *)_tooltip_text.c_str();
+        }
+      }
+      return 0;
+    }
+    break;
+
   default:
   default:
     break;
     break;
   }
   }
@@ -468,6 +571,15 @@ void WinStatsGraph::
 additional_graph_window_paint(HDC hdc) {
 additional_graph_window_paint(HDC hdc) {
 }
 }
 
 
+/**
+ * Called when the mouse hovers over the graph, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string WinStatsGraph::
+get_graph_tooltip(int mouse_x, int mouse_y) const {
+  return std::string();
+}
+
 /**
 /**
  * Based on the mouse position within the window's client area, look for
  * Based on the mouse position within the window's client area, look for
  * draggable things the mouse might be hovering over and return the
  * draggable things the mouse might be hovering over and return the
@@ -576,6 +688,25 @@ create_graph_window() {
   EnableWindow(_graph_window, TRUE);
   EnableWindow(_graph_window, TRUE);
 
 
   SetWindowSubclass(_graph_window, &static_graph_subclass_proc, 1234, (DWORD_PTR)this);
   SetWindowSubclass(_graph_window, &static_graph_subclass_proc, 1234, (DWORD_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)_graph_window;
+    info.lpszText = LPSTR_TEXTCALLBACK;
+    SendMessage(_tooltip_window, TTM_ADDTOOL, 0, (LPARAM)&info);
+  }
 }
 }
 
 
 /**
 /**

+ 19 - 0
pandatool/src/win-stats/winStatsGraph.h

@@ -40,6 +40,7 @@ public:
     DM_guide_bar,
     DM_guide_bar,
     DM_new_guide_bar,
     DM_new_guide_bar,
     DM_sizing,
     DM_sizing,
+    DM_pan,
   };
   };
 
 
 public:
 public:
@@ -57,10 +58,13 @@ public:
 
 
   void user_guide_bars_changed();
   void user_guide_bars_changed();
   virtual void on_click_label(int collector_index);
   virtual void on_click_label(int collector_index);
+  virtual void on_popup_label(int collector_index);
   virtual void on_enter_label(int collector_index);
   virtual void on_enter_label(int collector_index);
   virtual void on_leave_label(int collector_index);
   virtual void on_leave_label(int collector_index);
   virtual std::string get_label_tooltip(int collector_index) const;
   virtual std::string get_label_tooltip(int collector_index) const;
 
 
+  void clear_graph_tooltip();
+
   HWND get_window();
   HWND get_window();
 
 
 protected:
 protected:
@@ -69,13 +73,18 @@ protected:
   void setup_label_stack();
   void setup_label_stack();
   void move_label_stack();
   void move_label_stack();
 
 
+  void start_animation();
+  virtual bool animate(double time, double dt);
+
   HBRUSH get_collector_brush(int collector_index, bool highlight = false);
   HBRUSH get_collector_brush(int collector_index, bool highlight = false);
+  COLORREF get_collector_text_color(int collector_index, bool highlight = false);
 
 
   LONG window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
   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 LONG graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
 
 
   virtual void additional_window_paint(HDC hdc);
   virtual void additional_window_paint(HDC hdc);
   virtual void additional_graph_window_paint(HDC hdc);
   virtual void additional_graph_window_paint(HDC hdc);
+  virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const;
   virtual DragMode consider_drag_start(int mouse_x, int mouse_y,
   virtual DragMode consider_drag_start(int mouse_x, int mouse_y,
                                        int width, int height);
                                        int width, int height);
   virtual void set_drag_mode(DragMode drag_mode);
   virtual void set_drag_mode(DragMode drag_mode);
@@ -88,10 +97,15 @@ protected:
   typedef pmap<int, std::pair<HBRUSH, HBRUSH> > Brushes;
   typedef pmap<int, std::pair<HBRUSH, HBRUSH> > Brushes;
   Brushes _brushes;
   Brushes _brushes;
 
 
+  typedef pmap<int, std::pair<COLORREF, COLORREF> > TextColors;
+  TextColors _text_colors;
+
   WinStatsMonitor *_monitor;
   WinStatsMonitor *_monitor;
   HWND _window;
   HWND _window;
   HWND _graph_window;
   HWND _graph_window;
+  HWND _tooltip_window;
   WinStatsLabelStack _label_stack;
   WinStatsLabelStack _label_stack;
+  std::string _tooltip_text;
 
 
   HCURSOR _sizewe_cursor;
   HCURSOR _sizewe_cursor;
   HCURSOR _hand_cursor;
   HCURSOR _hand_cursor;
@@ -108,9 +122,11 @@ protected:
   COLORREF _dark_color;
   COLORREF _dark_color;
   COLORREF _light_color;
   COLORREF _light_color;
   COLORREF _user_guide_bar_color;
   COLORREF _user_guide_bar_color;
+  COLORREF _frame_guide_bar_color;
   HPEN _dark_pen;
   HPEN _dark_pen;
   HPEN _light_pen;
   HPEN _light_pen;
   HPEN _user_guide_bar_pen;
   HPEN _user_guide_bar_pen;
+  HPEN _frame_guide_bar_pen;
 
 
   DragMode _drag_mode;
   DragMode _drag_mode;
   DragMode _potential_drag_mode;
   DragMode _potential_drag_mode;
@@ -122,6 +138,9 @@ protected:
 
 
   bool _pause;
   bool _pause;
 
 
+  bool _timer_running = false;
+  double _time;
+
 private:
 private:
   void setup_bitmap(int xsize, int ysize);
   void setup_bitmap(int xsize, int ysize);
   void release_bitmap();
   void release_bitmap();

+ 18 - 4
pandatool/src/win-stats/winStatsLabel.cxx

@@ -66,7 +66,7 @@ WinStatsLabel(WinStatsMonitor *monitor, WinStatsGraph *graph,
   } else {
   } else {
     _fg_color = RGB(255, 255, 255);
     _fg_color = RGB(255, 255, 255);
   }
   }
-  if (bright >= 0.5 * 0.75) {
+  if (bright * 0.75 >= 0.5) {
     _highlight_fg_color = RGB(0, 0, 0);
     _highlight_fg_color = RGB(0, 0, 0);
   } else {
   } else {
     _highlight_fg_color = RGB(255, 255, 255);
     _highlight_fg_color = RGB(255, 255, 255);
@@ -282,6 +282,10 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
     _graph->on_click_label(_collector_index);
     _graph->on_click_label(_collector_index);
     return 0;
     return 0;
 
 
+  case WM_CONTEXTMENU:
+    _graph->on_popup_label(_collector_index);
+    return 0;
+
   case WM_MOUSEMOVE:
   case WM_MOUSEMOVE:
     {
     {
       // When the mouse enters the label area, highlight the label.
       // When the mouse enters the label area, highlight the label.
@@ -318,13 +322,23 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
 
 
       HFONT hfnt = _monitor->get_font();
       HFONT hfnt = _monitor->get_font();
       SelectObject(hdc, hfnt);
       SelectObject(hdc, hfnt);
-      SetTextAlign(hdc, (_align_right ? TA_RIGHT : TA_LEFT) | TA_TOP);
+      SetTextAlign(hdc, TA_LEFT | TA_TOP | TA_NOUPDATECP);
 
 
       SetBkMode(hdc, TRANSPARENT);
       SetBkMode(hdc, TRANSPARENT);
       SetTextColor(hdc, (_highlight || _mouse_within) ? _highlight_fg_color : _fg_color);
       SetTextColor(hdc, (_highlight || _mouse_within) ? _highlight_fg_color : _fg_color);
 
 
-      TextOut(hdc, _align_right ? (_width - _right_margin) : _left_margin,
-              _top_margin, _text.data(), _text.length());
+      if (_width > 8) {
+        UINT format = DT_END_ELLIPSIS | DT_SINGLELINE;
+        if (_align_right) {
+          format |= DT_RIGHT;
+        } else {
+          format |= DT_LEFT;
+        }
+
+        RECT margins = { _left_margin, _top_margin, _width - _right_margin, _height - _bottom_margin };
+        DrawText(hdc, _text.data(), _text.length(), &margins, format);
+      }
+
       EndPaint(hwnd, &ps);
       EndPaint(hwnd, &ps);
       return 0;
       return 0;
     }
     }

+ 7 - 3
pandatool/src/win-stats/winStatsMonitor.I

@@ -14,10 +14,11 @@
 /**
 /**
  *
  *
  */
  */
-WinStatsMonitor::MenuDef::
-MenuDef(int thread_index, int collector_index, bool show_level) :
+INLINE WinStatsMonitor::MenuDef::
+MenuDef(int thread_index, int collector_index, ChartType chart_type, bool show_level) :
   _thread_index(thread_index),
   _thread_index(thread_index),
   _collector_index(collector_index),
   _collector_index(collector_index),
+  _chart_type(chart_type),
   _show_level(show_level)
   _show_level(show_level)
 {
 {
 }
 }
@@ -25,7 +26,7 @@ MenuDef(int thread_index, int collector_index, bool show_level) :
 /**
 /**
  *
  *
  */
  */
-bool WinStatsMonitor::MenuDef::
+INLINE bool WinStatsMonitor::MenuDef::
 operator < (const MenuDef &other) const {
 operator < (const MenuDef &other) const {
   if (_thread_index != other._thread_index) {
   if (_thread_index != other._thread_index) {
     return _thread_index < other._thread_index;
     return _thread_index < other._thread_index;
@@ -33,5 +34,8 @@ operator < (const MenuDef &other) const {
   if (_collector_index != other._collector_index) {
   if (_collector_index != other._collector_index) {
     return _collector_index < other._collector_index;
     return _collector_index < other._collector_index;
   }
   }
+  if (_chart_type != other._chart_type) {
+    return _chart_type < other._chart_type;
+  }
   return (int)_show_level < (int)other._show_level;
   return (int)_show_level < (int)other._show_level;
 }
 }

+ 291 - 17
pandatool/src/win-stats/winStatsMonitor.cxx

@@ -16,11 +16,16 @@
 #include "winStatsStripChart.h"
 #include "winStatsStripChart.h"
 #include "winStatsPianoRoll.h"
 #include "winStatsPianoRoll.h"
 #include "winStatsFlameGraph.h"
 #include "winStatsFlameGraph.h"
+#include "winStatsTimeline.h"
 #include "winStatsChartMenu.h"
 #include "winStatsChartMenu.h"
 #include "winStatsMenuId.h"
 #include "winStatsMenuId.h"
+#include "pStatFrameData.h"
 #include "pStatGraph.h"
 #include "pStatGraph.h"
 #include "pStatCollectorDef.h"
 #include "pStatCollectorDef.h"
-#include "indent.h"
+
+#include <algorithm>
+
+#include <commctrl.h>
 
 
 bool WinStatsMonitor::_window_class_registered = false;
 bool WinStatsMonitor::_window_class_registered = false;
 const char * const WinStatsMonitor::_window_class_name = "monitor";
 const char * const WinStatsMonitor::_window_class_name = "monitor";
@@ -191,13 +196,14 @@ new_thread(int thread_index) {
  */
  */
 void WinStatsMonitor::
 void WinStatsMonitor::
 new_data(int thread_index, int frame_number) {
 new_data(int thread_index, int frame_number) {
-  Graphs::iterator gi;
-  for (gi = _graphs.begin(); gi != _graphs.end(); ++gi) {
-    WinStatsGraph *graph = (*gi);
+  for (WinStatsGraph *graph : _graphs) {
     graph->new_data(thread_index, frame_number);
     graph->new_data(thread_index, frame_number);
   }
   }
-}
 
 
+  if (thread_index == 0) {
+    update_status_bar();
+  }
+}
 
 
 /**
 /**
  * Called whenever the connection to the client has been lost.  This is a
  * Called whenever the connection to the client has been lost.  This is a
@@ -230,16 +236,21 @@ idle() {
   const PStatThreadData *thread_data = get_client_data()->get_thread_data(0);
   const PStatThreadData *thread_data = get_client_data()->get_thread_data(0);
   double frame_rate = thread_data->get_frame_rate();
   double frame_rate = thread_data->get_frame_rate();
   if (frame_rate != 0.0f) {
   if (frame_rate != 0.0f) {
+    // The leading tab centers the text in the status bar.
     char buffer[128];
     char buffer[128];
-    sprintf(buffer, "%0.1f ms / %0.1f Hz", 1000.0f / frame_rate, frame_rate);
+    sprintf(buffer, "\t%0.1f ms / %0.1f Hz", 1000.0f / frame_rate, frame_rate);
 
 
     MENUITEMINFO mii;
     MENUITEMINFO mii;
     memset(&mii, 0, sizeof(mii));
     memset(&mii, 0, sizeof(mii));
     mii.cbSize = sizeof(mii);
     mii.cbSize = sizeof(mii);
     mii.fMask = MIIM_STRING;
     mii.fMask = MIIM_STRING;
-    mii.dwTypeData = buffer;
+    mii.dwTypeData = buffer + 1; // chop off leading tab
     SetMenuItemInfo(_menu_bar, MI_frame_rate_label, FALSE, &mii);
     SetMenuItemInfo(_menu_bar, MI_frame_rate_label, FALSE, &mii);
     DrawMenuBar(_window);
     DrawMenuBar(_window);
+
+    if (_status_bar) {
+      SendMessage(_status_bar, WM_SETTEXT, 0, (LPARAM)buffer);
+    }
   }
   }
 }
 }
 
 
@@ -288,6 +299,18 @@ get_pixel_scale() const {
   return _pixel_scale;
   return _pixel_scale;
 }
 }
 
 
+/**
+ * Returns an amount by which to offset the next window position.
+ */
+POINT WinStatsMonitor::
+get_new_window_pos() {
+  int offset = _graphs.size() * 10 * _pixel_scale;
+  POINT pt;
+  pt.x = offset + _client_origin.x;
+  pt.y = offset + _client_origin.y;
+  return pt;
+}
+
 /**
 /**
  * Opens a new strip chart showing the indicated data.
  * Opens a new strip chart showing the indicated data.
  */
  */
@@ -319,8 +342,21 @@ open_piano_roll(int thread_index) {
  * Opens a new flame graph showing the indicated data.
  * Opens a new flame graph showing the indicated data.
  */
  */
 void WinStatsMonitor::
 void WinStatsMonitor::
-open_flame_graph(int thread_index) {
-  WinStatsFlameGraph *graph = new WinStatsFlameGraph(this, thread_index);
+open_flame_graph(int thread_index, int collector_index) {
+  WinStatsFlameGraph *graph = new WinStatsFlameGraph(this, thread_index, collector_index);
+  add_graph(graph);
+
+  graph->set_time_units(_time_units);
+  graph->set_scroll_speed(_scroll_speed);
+  graph->set_pause(_pause);
+}
+
+/**
+ * Opens a new timeline.
+ */
+void WinStatsMonitor::
+open_timeline() {
+  WinStatsTimeline *graph = new WinStatsTimeline(this);
   add_graph(graph);
   add_graph(graph);
 
 
   graph->set_time_units(_time_units);
   graph->set_time_units(_time_units);
@@ -334,7 +370,7 @@ open_flame_graph(int thread_index) {
  */
  */
 const WinStatsMonitor::MenuDef &WinStatsMonitor::
 const WinStatsMonitor::MenuDef &WinStatsMonitor::
 lookup_menu(int menu_id) const {
 lookup_menu(int menu_id) const {
-  static MenuDef invalid(0, 0, false);
+  static MenuDef invalid(0, 0, CT_strip_chart, false);
   int menu_index = menu_id - MI_new_chart;
   int menu_index = menu_id - MI_new_chart;
   nassertr(menu_index >= 0 && menu_index < (int)_menu_by_id.size(), invalid);
   nassertr(menu_index >= 0 && menu_index < (int)_menu_by_id.size(), invalid);
   return _menu_by_id[menu_index];
   return _menu_by_id[menu_index];
@@ -516,10 +552,16 @@ create_window() {
 
 
   SetWindowLongPtr(_window, 0, (LONG_PTR)this);
   SetWindowLongPtr(_window, 0, (LONG_PTR)this);
 
 
+  create_status_bar(application);
+
   // For some reason, SW_SHOWNORMAL doesn't always work, but SW_RESTORE seems
   // For some reason, SW_SHOWNORMAL doesn't always work, but SW_RESTORE seems
   // to.
   // to.
   ShowWindow(_window, SW_RESTORE);
   ShowWindow(_window, SW_RESTORE);
   SetForegroundWindow(_window);
   SetForegroundWindow(_window);
+
+  _client_origin.x = 0;
+  _client_origin.y = 0;
+  ClientToScreen(_window, &_client_origin);
 }
 }
 
 
 /**
 /**
@@ -636,6 +678,165 @@ setup_frame_rate_label() {
   InsertMenuItem(_menu_bar, GetMenuItemCount(_menu_bar), TRUE, &mii);
   InsertMenuItem(_menu_bar, GetMenuItemCount(_menu_bar), TRUE, &mii);
 }
 }
 
 
+/**
+ * Sets up a status bar at the bottom of the screen showing assorted level
+ * values.
+ */
+void WinStatsMonitor::
+create_status_bar(HINSTANCE application) {
+  _status_bar = CreateWindow(STATUSCLASSNAME, nullptr,
+                             SBARS_SIZEGRIP | WS_CHILD | WS_VISIBLE,
+                             0, 0, 0, 0,
+                             _window, (HMENU)0, application, nullptr);
+
+  update_status_bar();
+
+
+  ShowWindow(_status_bar, SW_SHOW);
+  UpdateWindow(_status_bar);
+
+  InvalidateRect(_status_bar, NULL, TRUE);
+}
+
+/**
+ * Updates the status bar.
+ */
+void WinStatsMonitor::
+update_status_bar() {
+  const PStatClientData *client_data = get_client_data();
+  if (client_data == nullptr) {
+    return;
+  }
+
+  const PStatThreadData *thread_data = get_client_data()->get_thread_data(0);
+  if (thread_data == nullptr || thread_data->is_empty()) {
+    return;
+  }
+  const PStatFrameData &frame_data = thread_data->get_latest_frame();
+
+  // Gather the top-level collector list.
+  pvector<std::string> parts;
+  pvector<int> collectors;
+  size_t total_chars = 0;
+
+  int num_toplevel_collectors = client_data->get_num_toplevel_collectors();
+  for (int tc = 0; tc < num_toplevel_collectors; tc++) {
+    int collector = client_data->get_toplevel_collector(tc);
+    if (client_data->has_collector(collector) &&
+        client_data->get_collector_has_level(collector, 0)) {
+      PStatView &view = get_level_view(collector, 0);
+      view.set_to_frame(frame_data);
+      double value = view.get_net_value();
+      if (value == 0.0) {
+        // Don't include it unless we've included it before.
+        if (std::find(_status_bar_collectors.begin(), _status_bar_collectors.end(), collector) == _status_bar_collectors.end()) {
+          continue;
+        }
+      }
+
+      const PStatCollectorDef &def = client_data->get_collector_def(collector);
+      std::string text = "\t" + def._name;
+      text += ": " + PStatGraph::format_number(value, PStatGraph::GBU_named | PStatGraph::GBU_show_units, def._level_units);
+      total_chars += text.size();
+      parts.push_back(text);
+      collectors.push_back(collector);
+    }
+  }
+
+  size_t cur_size = _status_bar_collectors.size();
+  _status_bar_collectors = std::move(collectors);
+
+  // Allocate an array for holding the right edge coordinates.
+  HLOCAL hloc = LocalAlloc(LHND, sizeof(int) * (parts.size() + 1));
+  PINT sizes = (PINT)LocalLock(hloc);
+
+  // Allocate the left-most slot for the framerate indicator.
+  double offset = 28.0 * _pixel_scale;
+  sizes[0] = (int)(offset + 0.5);
+
+  if (!parts.empty()) {
+    // Distribute the sizes roughly based on the number of characters.  It's not
+    // as good as measuring the text, but it's good enough.
+    RECT rect;
+    double width_per_char = 0;
+    GetClientRect(_status_bar, &rect);
+    // Leave room for the grip.
+    rect.right -= _pixel_scale * 4;
+    width_per_char = (rect.right - rect.left - offset) / (double)total_chars;
+
+    // If we get below a minimum width, start chopping parts off.
+    while (!parts.empty() && width_per_char < _pixel_scale * 1.5) {
+      total_chars -= parts.back().size();
+      parts.pop_back();
+      width_per_char = (rect.right - rect.left - offset) / (double)total_chars;
+    }
+
+    if (!parts.empty()) {
+      for (size_t i = 0; i < parts.size(); ++i) {
+        offset += (parts[i].size()) * width_per_char;
+        sizes[i + 1] = (int)(offset + 0.5);
+      }
+    } else {
+      // No room for any collectors; the framerate can take up the whole width.
+      sizes[0] = rect.right - rect.left;
+    }
+  }
+
+  SendMessage(_status_bar, SB_SETPARTS, (WPARAM)(parts.size() + 1), (LPARAM)sizes);
+
+  LocalUnlock(hloc);
+  LocalFree(hloc);
+
+  for (size_t i = 0; i < parts.size(); ++i) {
+    SendMessage(_status_bar, SB_SETTEXT, i + 1, (LPARAM)parts[i].c_str());
+  }
+}
+
+/**
+ * Called when someone right-clicks on a part of the status bar.
+ */
+void WinStatsMonitor::
+show_popup_menu(int collector) {
+  POINT point;
+  if (!GetCursorPos(&point)) {
+    return;
+  }
+
+  const PStatClientData *client_data = get_client_data();
+  if (client_data == nullptr) {
+    return;
+  }
+
+  PStatView &level_view = get_level_view(collector, 0);
+  const PStatViewLevel *view_level = level_view.get_top_level();
+  int num_children = view_level->get_num_children();
+  if (num_children == 0) {
+    return;
+  }
+
+  HMENU popup = CreatePopupMenu();
+
+  // Reverse the order since the menus are listed from the top down; we want
+  // to be visually consistent with the graphs, which list these labels from
+  // the bottom up.
+  for (int c = num_children - 1; c >= 0; c--) {
+    const PStatViewLevel *child_level = view_level->get_child(c);
+
+    int child_collector = child_level->get_collector();
+    MenuDef menu_def(0, child_collector, CT_strip_chart, true);
+    int menu_id = get_menu_id(menu_def);
+
+    double value = child_level->get_net_value();
+
+    const PStatCollectorDef &def = client_data->get_collector_def(child_collector);
+    std::string text = def._name;
+    text += ": " + PStatGraph::format_number(value, PStatGraph::GBU_named | PStatGraph::GBU_show_units, def._level_units);
+    AppendMenu(popup, MF_STRING, menu_id, text.c_str());
+  }
+
+  TrackPopupMenu(popup, TPM_LEFTBUTTON, point.x, point.y, 0, _window, nullptr);
+}
+
 /**
 /**
  * Registers the window class for the monitor window, if it has not already
  * Registers the window class for the monitor window, if it has not already
  * been registered.
  * been registered.
@@ -691,6 +892,71 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
     close();
     close();
     break;
     break;
 
 
+  case WM_WINDOWPOSCHANGED:
+    if (!_graphs.empty()) {
+      RECT status_bar_rect;
+      GetWindowRect(_status_bar, &status_bar_rect);
+
+      RECT client_rect;
+      GetClientRect(_window, &client_rect);
+      MapWindowPoints(_window, nullptr, (POINT *)&client_rect, 2);
+
+      int delta_x = client_rect.left - _client_origin.x;
+      int delta_y = client_rect.top - _client_origin.y;
+      _client_origin.x = client_rect.left;
+      _client_origin.y = client_rect.top;
+
+      int iconic_offset = 0;
+
+      for (WinStatsGraph *graph : _graphs) {
+        RECT child_rect;
+        HWND window = graph->get_window();
+        if (GetWindowRect(window, &child_rect)) {
+          if (IsIconic(window)) {
+            // Keep it glued to the bottom-left corner of the parent window.
+            child_rect.left = client_rect.left + iconic_offset;
+            child_rect.top = client_rect.bottom - (child_rect.bottom - child_rect.top) - (status_bar_rect.bottom - status_bar_rect.top);
+            iconic_offset += (child_rect.right - child_rect.left);
+          } else {
+            child_rect.left += delta_x;
+            child_rect.top += delta_y;
+          }
+          SetWindowPos(window, 0, child_rect.left, child_rect.top, 0, 0,
+                       SWP_NOOWNERZORDER | SWP_NOZORDER | SWP_NOSIZE | SWP_NOREDRAW | SWP_NOACTIVATE);
+        }
+      }
+    }
+    break;
+
+  case WM_SIZE:
+    if (_status_bar) {
+      SendMessage(_status_bar, WM_SIZE, 0, 0);
+      update_status_bar();
+    }
+    break;
+
+  case WM_NOTIFY:
+    if (((LPNMHDR)lparam)->code == NM_DBLCLK) {
+      NMMOUSE &mouse = *(NMMOUSE *)lparam;
+      if (mouse.dwItemSpec == 0) {
+        open_strip_chart(0, 0, false);
+      }
+      else if (mouse.dwItemSpec >= 1 && mouse.dwItemSpec <= _status_bar_collectors.size()) {
+        int collector = _status_bar_collectors[mouse.dwItemSpec - 1];
+        open_strip_chart(0, collector, true);
+      }
+      return TRUE;
+    }
+    else if (((LPNMHDR)lparam)->code == NM_RCLICK) {
+      NMMOUSE &mouse = *(NMMOUSE *)lparam;
+      if (mouse.dwItemSpec >= 1 &&
+          mouse.dwItemSpec <= _status_bar_collectors.size()) {
+        int collector = _status_bar_collectors[mouse.dwItemSpec - 1];
+        show_popup_menu(collector);
+      }
+    }
+    break;
+
   case WM_COMMAND:
   case WM_COMMAND:
     if (HIWORD(wparam) <= 1) {
     if (HIWORD(wparam) <= 1) {
       int menu_id = LOWORD(wparam);
       int menu_id = LOWORD(wparam);
@@ -750,15 +1016,23 @@ handle_menu_command(int menu_id) {
   default:
   default:
     if (menu_id >= MI_new_chart) {
     if (menu_id >= MI_new_chart) {
       const MenuDef &menu_def = lookup_menu(menu_id);
       const MenuDef &menu_def = lookup_menu(menu_id);
-      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 {
+      switch (menu_def._chart_type) {
+      case CT_timeline:
+        open_timeline();
+        break;
+
+      case CT_strip_chart:
         open_strip_chart(menu_def._thread_index, menu_def._collector_index,
         open_strip_chart(menu_def._thread_index, menu_def._collector_index,
                          menu_def._show_level);
                          menu_def._show_level);
+        break;
+
+      case CT_flame_graph:
+        open_flame_graph(menu_def._thread_index, menu_def._collector_index);
+        break;
+
+      case CT_piano_roll:
+        open_piano_roll(menu_def._thread_index);
+        break;
       }
       }
     }
     }
   }
   }

+ 19 - 2
pandatool/src/win-stats/winStatsMonitor.h

@@ -37,13 +37,22 @@ class WinStatsChartMenu;
  */
  */
 class WinStatsMonitor : public PStatMonitor {
 class WinStatsMonitor : public PStatMonitor {
 public:
 public:
+  enum ChartType {
+    CT_timeline,
+    CT_strip_chart,
+    CT_flame_graph,
+    CT_piano_roll,
+  };
+
   class MenuDef {
   class MenuDef {
   public:
   public:
-    INLINE MenuDef(int thread_index, int collector_index, bool show_level);
+    INLINE MenuDef(int thread_index, int collector_index,
+                   ChartType chart_type, bool show_level = false);
     INLINE bool operator < (const MenuDef &other) const;
     INLINE bool operator < (const MenuDef &other) const;
 
 
     int _thread_index;
     int _thread_index;
     int _collector_index;
     int _collector_index;
+    ChartType _chart_type;
     bool _show_level;
     bool _show_level;
   };
   };
 
 
@@ -68,10 +77,12 @@ public:
   HWND get_window() const;
   HWND get_window() const;
   HFONT get_font() const;
   HFONT get_font() const;
   int get_pixel_scale() const;
   int get_pixel_scale() const;
+  POINT get_new_window_pos();
 
 
   void open_strip_chart(int thread_index, int collector_index, bool show_level);
   void open_strip_chart(int thread_index, int collector_index, bool show_level);
   void open_piano_roll(int thread_index);
   void open_piano_roll(int thread_index);
-  void open_flame_graph(int thread_index);
+  void open_flame_graph(int thread_index, int collector_index = -1);
+  void open_timeline();
 
 
   const MenuDef &lookup_menu(int menu_id) const;
   const MenuDef &lookup_menu(int menu_id) const;
   int get_menu_id(const MenuDef &menu_def);
   int get_menu_id(const MenuDef &menu_def);
@@ -88,6 +99,9 @@ private:
   void setup_options_menu();
   void setup_options_menu();
   void setup_speed_menu();
   void setup_speed_menu();
   void setup_frame_rate_label();
   void setup_frame_rate_label();
+  void create_status_bar(HINSTANCE application);
+  void update_status_bar();
+  void show_popup_menu(int collector);
   static void register_window_class(HINSTANCE application);
   static void register_window_class(HINSTANCE application);
 
 
   static LONG WINAPI static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
   static LONG WINAPI static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
@@ -109,6 +123,9 @@ private:
   HMENU _menu_bar;
   HMENU _menu_bar;
   HMENU _options_menu;
   HMENU _options_menu;
   HMENU _speed_menu;
   HMENU _speed_menu;
+  HWND _status_bar;
+  POINT _client_origin;
+  pvector<int> _status_bar_collectors;
   std::string _window_title;
   std::string _window_title;
   int _time_units;
   int _time_units;
   double _scroll_speed;
   double _scroll_speed;

+ 80 - 10
pandatool/src/win-stats/winStatsPianoRoll.cxx

@@ -15,8 +15,8 @@
 #include "winStatsMonitor.h"
 #include "winStatsMonitor.h"
 #include "numeric_types.h"
 #include "numeric_types.h"
 
 
-static const int default_piano_roll_width = 600;
-static const int default_piano_roll_height = 200;
+static const int default_piano_roll_width = 800;
+static const int default_piano_roll_height = 400;
 
 
 bool WinStatsPianoRoll::_window_class_registered = false;
 bool WinStatsPianoRoll::_window_class_registered = false;
 const char * const WinStatsPianoRoll::_window_class_name = "piano";
 const char * const WinStatsPianoRoll::_window_class_name = "piano";
@@ -51,7 +51,7 @@ WinStatsPianoRoll::
 }
 }
 
 
 /**
 /**
- * Called as each frame's data is made available.  There is no gurantee the
+ * 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
  * 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
  * monitor should be prepared to accept frames received out-of-order or
  * missing.
  * missing.
@@ -109,6 +109,36 @@ on_click_label(int collector_index) {
   }
   }
 }
 }
 
 
+/**
+ * Called when the user right-clicks on a label.
+ */
+void WinStatsPianoRoll::
+on_popup_label(int collector_index) {
+  POINT point;
+  if (collector_index >= 0 && GetCursorPos(&point)) {
+    _popup_index = collector_index;
+
+    HMENU popup = CreatePopupMenu();
+
+    std::string label = get_label_tooltip(collector_index);
+    if (!label.empty()) {
+      AppendMenu(popup, MF_STRING | MF_DISABLED, 0, label.c_str());
+    }
+    AppendMenu(popup, MF_STRING, 102, "Open Strip Chart");
+    AppendMenu(popup, MF_STRING, 103, "Open Flame Graph");
+    TrackPopupMenu(popup, TPM_LEFTBUTTON, point.x, point.y, 0, _window, nullptr);
+  }
+}
+
+/**
+ * Called when the mouse hovers over a label, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string WinStatsPianoRoll::
+get_label_tooltip(int collector_index) const {
+  return PStatPianoRoll::get_label_tooltip(collector_index);
+}
+
 /**
 /**
  * Changes the amount of time the width of the horizontal axis represents.
  * Changes the amount of time the width of the horizontal axis represents.
  * This may force a redraw.
  * This may force a redraw.
@@ -216,6 +246,18 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
     }
     }
     break;
     break;
 
 
+  case WM_COMMAND:
+    switch (LOWORD(wparam)) {
+    case 102:
+      WinStatsGraph::_monitor->open_strip_chart(get_thread_index(), _popup_index, false);
+      return 0;
+
+    case 103:
+      WinStatsGraph::_monitor->open_flame_graph(get_thread_index(), _popup_index);
+      return 0;
+    }
+    break;
+
   default:
   default:
     break;
     break;
   }
   }
@@ -333,6 +375,19 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
     }
     }
     break;
     break;
 
 
+  case WM_CONTEXTMENU:
+    {
+      POINT point;
+      if (GetCursorPos(&point) && ScreenToClient(_graph_window, &point)) {
+        int collector_index = get_collector_under_pixel(point.x, point.y);
+        if (collector_index >= 0) {
+          on_popup_label(collector_index);
+        }
+      }
+      return 0;
+    }
+    break;
+
   default:
   default:
     break;
     break;
   }
   }
@@ -379,6 +434,19 @@ additional_graph_window_paint(HDC hdc) {
   }
   }
 }
 }
 
 
+/**
+ * Called when the mouse hovers over the graph, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string WinStatsPianoRoll::
+get_graph_tooltip(int mouse_x, int mouse_y) const {
+  int collector_index = get_collector_under_pixel(mouse_x, mouse_y);
+  if (collector_index >= 0) {
+    return get_label_tooltip(collector_index);
+  }
+  return std::string();
+}
+
 /**
 /**
  * Based on the mouse position within the window's client area, look for
  * Based on the mouse position within the window's client area, look for
  * draggable things the mouse might be hovering over and return the
  * draggable things the mouse might be hovering over and return the
@@ -413,7 +481,7 @@ consider_drag_start(int mouse_x, int mouse_y, int width, int height) {
  * -1.
  * -1.
  */
  */
 int WinStatsPianoRoll::
 int WinStatsPianoRoll::
-get_collector_under_pixel(int xpoint, int ypoint) {
+get_collector_under_pixel(int xpoint, int ypoint) const {
   if (_label_stack.get_num_labels() == 0) {
   if (_label_stack.get_num_labels() == 0) {
     return -1;
     return -1;
   }
   }
@@ -521,7 +589,7 @@ create_window() {
     WinStatsGraph::_monitor->get_client_data();
     WinStatsGraph::_monitor->get_client_data();
   std::string thread_name = client_data->get_thread_name(_thread_index);
   std::string thread_name = client_data->get_thread_name(_thread_index);
   std::string window_title = thread_name + " thread piano roll";
   std::string window_title = thread_name + " thread piano roll";
-
+  POINT window_pos = WinStatsGraph::_monitor->get_new_window_pos();
 
 
   RECT win_rect = {
   RECT win_rect = {
     0, 0,
     0, 0,
@@ -533,11 +601,13 @@ create_window() {
   AdjustWindowRect(&win_rect, graph_window_style, FALSE);
   AdjustWindowRect(&win_rect, graph_window_style, FALSE);
 
 
   _window =
   _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);
+    CreateWindowEx(WS_EX_DLGMODALFRAME, _window_class_name,
+                   window_title.c_str(), graph_window_style,
+                   window_pos.x, window_pos.y,
+                   win_rect.right - win_rect.left,
+                   win_rect.bottom - win_rect.top,
+                   WinStatsGraph::_monitor->get_window(),
+                   nullptr, application, 0);
   if (!_window) {
   if (!_window) {
     nout << "Could not create PianoRoll window!\n";
     nout << "Could not create PianoRoll window!\n";
     exit(1);
     exit(1);

+ 6 - 1
pandatool/src/win-stats/winStatsPianoRoll.h

@@ -42,6 +42,8 @@ public:
 
 
   virtual void set_time_units(int unit_mask);
   virtual void set_time_units(int unit_mask);
   virtual void on_click_label(int collector_index);
   virtual void on_click_label(int collector_index);
+  virtual void on_popup_label(int collector_index);
+  virtual std::string get_label_tooltip(int collector_index) const;
   void set_horizontal_scale(double time_width);
   void set_horizontal_scale(double time_width);
 
 
 protected:
 protected:
@@ -57,11 +59,12 @@ protected:
   virtual LONG graph_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_window_paint(HDC hdc);
   virtual void additional_graph_window_paint(HDC hdc);
   virtual void additional_graph_window_paint(HDC hdc);
+  virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const;
   virtual DragMode consider_drag_start(int mouse_x, int mouse_y,
   virtual DragMode consider_drag_start(int mouse_x, int mouse_y,
                                        int width, int height);
                                        int width, int height);
 
 
 private:
 private:
-  int get_collector_under_pixel(int xpoint, int ypoint);
+  int get_collector_under_pixel(int xpoint, int ypoint) const;
   void update_labels();
   void update_labels();
   void draw_guide_bar(HDC hdc, const GuideBar &bar);
   void draw_guide_bar(HDC hdc, const GuideBar &bar);
   void draw_guide_label(HDC hdc, int y, const PStatGraph::GuideBar &bar);
   void draw_guide_label(HDC hdc, int y, const PStatGraph::GuideBar &bar);
@@ -71,6 +74,8 @@ private:
 
 
   static LONG WINAPI static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
   static LONG WINAPI static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
 
 
+  int _popup_index = -1;
+
   static bool _window_class_registered;
   static bool _window_class_registered;
   static const char * const _window_class_name;
   static const char * const _window_class_name;
 };
 };

+ 91 - 26
pandatool/src/win-stats/winStatsStripChart.cxx

@@ -18,8 +18,6 @@
 
 
 #include <commctrl.h>
 #include <commctrl.h>
 
 
-using std::string;
-
 static const int default_strip_chart_width = 400;
 static const int default_strip_chart_width = 400;
 static const int default_strip_chart_height = 100;
 static const int default_strip_chart_height = 100;
 
 
@@ -33,7 +31,7 @@ WinStatsStripChart::
 WinStatsStripChart(WinStatsMonitor *monitor, int thread_index,
 WinStatsStripChart(WinStatsMonitor *monitor, int thread_index,
                    int collector_index, bool show_level) :
                    int collector_index, bool show_level) :
   PStatStripChart(monitor,
   PStatStripChart(monitor,
-                  show_level ? monitor->get_level_view(collector_index, thread_index) : monitor->get_view(thread_index),
+                  show_level ? monitor->get_level_view(0, thread_index) : monitor->get_view(thread_index),
                   thread_index,
                   thread_index,
                   collector_index,
                   collector_index,
                   monitor->get_pixel_scale() * default_strip_chart_width / 4,
                   monitor->get_pixel_scale() * default_strip_chart_width / 4,
@@ -82,7 +80,7 @@ new_collector(int collector_index) {
 }
 }
 
 
 /**
 /**
- * Called as each frame's data is made available.  There is no gurantee the
+ * 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
  * 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
  * monitor should be prepared to accept frames received out-of-order or
  * missing.
  * missing.
@@ -90,7 +88,7 @@ new_collector(int collector_index) {
 void WinStatsStripChart::
 void WinStatsStripChart::
 new_data(int thread_index, int frame_number) {
 new_data(int thread_index, int frame_number) {
   if (is_title_unknown()) {
   if (is_title_unknown()) {
-    string window_title = get_title_text();
+    std::string window_title = get_title_text();
     if (!is_title_unknown()) {
     if (!is_title_unknown()) {
       SetWindowText(_window, window_title.c_str());
       SetWindowText(_window, window_title.c_str());
     }
     }
@@ -99,7 +97,7 @@ new_data(int thread_index, int frame_number) {
   if (!_pause) {
   if (!_pause) {
     update();
     update();
 
 
-    string text = format_number(get_average_net_value(), get_guide_bar_units(), get_guide_bar_unit_name());
+    std::string text = format_number(get_average_net_value(), get_guide_bar_units(), get_guide_bar_unit_name());
     if (_net_value_text != text) {
     if (_net_value_text != text) {
       _net_value_text = text;
       _net_value_text = text;
       RECT rect;
       RECT rect;
@@ -193,6 +191,36 @@ on_click_label(int collector_index) {
   }
   }
 }
 }
 
 
+/**
+ * Called when the user right-clicks on a label.
+ */
+void WinStatsStripChart::
+on_popup_label(int collector_index) {
+  POINT point;
+  if (collector_index >= 0 && GetCursorPos(&point)) {
+    _popup_index = collector_index;
+
+    HMENU popup = CreatePopupMenu();
+
+    std::string label = get_label_tooltip(collector_index);
+    if (!label.empty()) {
+      AppendMenu(popup, MF_STRING | MF_DISABLED, 0, label.c_str());
+    }
+    if (collector_index == 0 && get_collector_index() == 0) {
+      AppendMenu(popup, MF_STRING | MF_DISABLED, 101, "Set as Focus");
+    } else {
+      AppendMenu(popup, MF_STRING, 101, "Set as Focus");
+    }
+    AppendMenu(popup, MF_STRING, 102, "Open Strip Chart");
+    if (get_view().get_show_level()) {
+      AppendMenu(popup, MF_STRING | MF_DISABLED, 103, "Open Flame Graph");
+    } else {
+      AppendMenu(popup, MF_STRING, 103, "Open Flame Graph");
+    }
+    TrackPopupMenu(popup, TPM_LEFTBUTTON, point.x, point.y, 0, _window, nullptr);
+  }
+}
+
 /**
 /**
  * Called when the mouse hovers over a label, and should return the text that
  * Called when the mouse hovers over a label, and should return the text that
  * should appear on the tooltip.
  * should appear on the tooltip.
@@ -355,6 +383,19 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
         return 0;
         return 0;
       }
       }
       break;
       break;
+
+    case 101:
+      set_collector_index(_popup_index);
+      break;
+
+    case 102:
+      WinStatsGraph::_monitor->open_strip_chart(get_thread_index(), _popup_index,
+                                                get_view().get_show_level());
+      return 0;
+
+    case 103:
+      WinStatsGraph::_monitor->open_flame_graph(get_thread_index(), _popup_index);
+      return 0;
     }
     }
     break;
     break;
 
 
@@ -416,9 +457,14 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
 
 
     if (_drag_mode == DM_scale) {
     if (_drag_mode == DM_scale) {
       int16_t y = HIWORD(lparam);
       int16_t y = HIWORD(lparam);
-      double ratio = 1.0f - ((double)y / (double)get_ysize());
-      if (ratio > 0.0f) {
-        set_vertical_scale(_drag_scale_start / ratio);
+      double ratio = 1.0 - ((double)y / (double)get_ysize());
+      if (ratio > 0.0) {
+        double new_scale = _drag_scale_start / ratio;
+        if (!IS_NEARLY_EQUAL(get_vertical_scale(), new_scale)) {
+          // Disable smoothing while we do this expensive operation.
+          set_average_mode(false);
+          set_vertical_scale(_drag_scale_start / ratio);
+        }
       }
       }
       return 0;
       return 0;
 
 
@@ -475,6 +521,19 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
     }
     }
     break;
     break;
 
 
+  case WM_CONTEXTMENU:
+    {
+      POINT point;
+      if (GetCursorPos(&point) && ScreenToClient(_graph_window, &point)) {
+        int collector_index = get_collector_under_pixel(point.x, point.y);
+        if (collector_index >= 0) {
+          on_popup_label(collector_index);
+        }
+      }
+      return 0;
+    }
+    break;
+
   default:
   default:
     break;
     break;
   }
   }
@@ -534,6 +593,18 @@ additional_graph_window_paint(HDC hdc) {
   }
   }
 }
 }
 
 
+/**
+ * Called when the mouse hovers over the graph, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string WinStatsStripChart::
+get_graph_tooltip(int mouse_x, int mouse_y) const {
+  if (_highlighted_index != -1) {
+    return get_label_tooltip(_highlighted_index);
+  }
+  return std::string();
+}
+
 /**
 /**
  * Based on the mouse position within the window's client area, look for
  * Based on the mouse position within the window's client area, look for
  * draggable things the mouse might be hovering over and return the
  * draggable things the mouse might be hovering over and return the
@@ -569,16 +640,7 @@ void WinStatsStripChart::
 set_drag_mode(WinStatsGraph::DragMode drag_mode) {
 set_drag_mode(WinStatsGraph::DragMode drag_mode) {
   WinStatsGraph::set_drag_mode(drag_mode);
   WinStatsGraph::set_drag_mode(drag_mode);
 
 
-  switch (_drag_mode) {
-  case DM_scale:
-  case DM_left_margin:
-  case DM_right_margin:
-  case DM_sizing:
-    // Disable smoothing for these expensive operations.
-    set_average_mode(false);
-    break;
-
-  default:
+  if (_drag_mode == DM_none) {
     // Restore smoothing according to the current setting of the check box.
     // Restore smoothing according to the current setting of the check box.
     int result = SendMessage(_smooth_check_box, BM_GETCHECK, 0, 0);
     int result = SendMessage(_smooth_check_box, BM_GETCHECK, 0, 0);
     set_average_mode(result == BST_CHECKED);
     set_average_mode(result == BST_CHECKED);
@@ -654,7 +716,7 @@ draw_guide_label(HDC hdc, int x, const PStatGraph::GuideBar &bar, int last_y) {
   }
   }
 
 
   int y = height_to_pixel(bar._height);
   int y = height_to_pixel(bar._height);
-  const string &label = bar._label;
+  const std::string &label = bar._label;
   SIZE size;
   SIZE size;
   GetTextExtentPoint32(hdc, label.data(), label.length(), &size);
   GetTextExtentPoint32(hdc, label.data(), label.length(), &size);
 
 
@@ -691,7 +753,8 @@ create_window() {
   HINSTANCE application = GetModuleHandle(nullptr);
   HINSTANCE application = GetModuleHandle(nullptr);
   register_window_class(application);
   register_window_class(application);
 
 
-  string window_title = get_title_text();
+  std::string window_title = get_title_text();
+  POINT window_pos = WinStatsGraph::_monitor->get_new_window_pos();
 
 
   RECT win_rect = {
   RECT win_rect = {
     0, 0,
     0, 0,
@@ -703,11 +766,13 @@ create_window() {
   AdjustWindowRect(&win_rect, graph_window_style, FALSE);
   AdjustWindowRect(&win_rect, graph_window_style, FALSE);
 
 
   _window =
   _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);
+    CreateWindowEx(WS_EX_DLGMODALFRAME, _window_class_name,
+                   window_title.c_str(), graph_window_style,
+                   window_pos.x, window_pos.y,
+                   win_rect.right - win_rect.left,
+                   win_rect.bottom - win_rect.top,
+                   WinStatsGraph::_monitor->get_window(),
+                   nullptr, application, 0);
   if (!_window) {
   if (!_window) {
     nout << "Could not create StripChart window!\n";
     nout << "Could not create StripChart window!\n";
     exit(1);
     exit(1);

+ 3 - 0
pandatool/src/win-stats/winStatsStripChart.h

@@ -44,6 +44,7 @@ public:
   virtual void set_time_units(int unit_mask);
   virtual void set_time_units(int unit_mask);
   virtual void set_scroll_speed(double scroll_speed);
   virtual void set_scroll_speed(double scroll_speed);
   virtual void on_click_label(int collector_index);
   virtual void on_click_label(int collector_index);
+  virtual void on_popup_label(int collector_index);
   virtual std::string get_label_tooltip(int collector_index) const;
   virtual std::string get_label_tooltip(int collector_index) const;
   void set_vertical_scale(double value_height);
   void set_vertical_scale(double value_height);
 
 
@@ -62,6 +63,7 @@ protected:
   virtual LONG graph_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_window_paint(HDC hdc);
   virtual void additional_graph_window_paint(HDC hdc);
   virtual void additional_graph_window_paint(HDC hdc);
+  virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const;
   virtual DragMode consider_drag_start(int mouse_x, int mouse_y,
   virtual DragMode consider_drag_start(int mouse_x, int mouse_y,
                                        int width, int height);
                                        int width, int height);
   virtual void set_drag_mode(DragMode drag_mode);
   virtual void set_drag_mode(DragMode drag_mode);
@@ -80,6 +82,7 @@ private:
   std::string _net_value_text;
   std::string _net_value_text;
 
 
   HWND _smooth_check_box;
   HWND _smooth_check_box;
+  int _popup_index = -1;
 
 
   static bool _window_class_registered;
   static bool _window_class_registered;
   static const char * const _window_class_name;
   static const char * const _window_class_name;

+ 752 - 0
pandatool/src/win-stats/winStatsTimeline.cxx

@@ -0,0 +1,752 @@
+/**
+ * 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 winStatsTimeline.cxx
+ * @author rdb
+ * @date 2022-02-11
+ */
+
+#include "winStatsTimeline.h"
+#include "winStatsMonitor.h"
+#include "numeric_types.h"
+
+static const int default_timeline_width = 1000;
+static const int default_timeline_height = 500;
+
+bool WinStatsTimeline::_window_class_registered = false;
+const char * const WinStatsTimeline::_window_class_name = "timeline";
+
+/**
+ *
+ */
+WinStatsTimeline::
+WinStatsTimeline(WinStatsMonitor *monitor) :
+  PStatTimeline(monitor,
+                monitor->get_pixel_scale() * default_timeline_width / 4,
+                monitor->get_pixel_scale() * default_timeline_height / 4),
+  WinStatsGraph(monitor)
+{
+  _left_margin = _pixel_scale * 24;
+  _right_margin = _pixel_scale * 2;
+  _top_margin = _pixel_scale * 5;
+  _bottom_margin = _pixel_scale * 2;
+
+  normal_guide_bars();
+
+  create_window();
+  clear_region();
+
+  _grid_brush = CreateSolidBrush(RGB(0xdd, 0xdd, 0xdd));
+}
+
+/**
+ *
+ */
+WinStatsTimeline::
+~WinStatsTimeline() {
+}
+
+/**
+ * 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 WinStatsTimeline::
+new_data(int thread_index, int frame_number) {
+  PStatTimeline::new_data(thread_index, frame_number);
+}
+
+/**
+ * Called when it is necessary to redraw the entire graph.
+ */
+void WinStatsTimeline::
+force_redraw() {
+  PStatTimeline::force_redraw();
+}
+
+/**
+ * Called when the user has resized the window, forcing a resize of the graph.
+ */
+void WinStatsTimeline::
+changed_graph_size(int graph_xsize, int graph_ysize) {
+  PStatTimeline::changed_size(graph_xsize, graph_ysize);
+}
+
+/**
+ * Erases the chart area.
+ */
+void WinStatsTimeline::
+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 WinStatsTimeline::
+begin_draw() {
+  SelectObject(_bitmap_dc, WinStatsGraph::_monitor->get_font());
+  SelectObject(_bitmap_dc, GetStockObject(NULL_PEN));
+  SetBkMode(_bitmap_dc, TRANSPARENT);
+  SetTextAlign(_bitmap_dc, TA_LEFT | TA_TOP | TA_NOUPDATECP);
+}
+
+/**
+ * Draws a horizontal separator.
+ */
+void WinStatsTimeline::
+draw_separator(int row) {
+  int y = (row_to_pixel(row) + row_to_pixel(row + 1)) / 2;
+  RECT rect = {0, y, get_xsize(), y + _pixel_scale / 3};
+  FillRect(_bitmap_dc, &rect, _grid_brush);
+}
+
+/**
+ * Draws a vertical guide bar.  If the row is -1, draws it in all rows.
+ */
+void WinStatsTimeline::
+draw_guide_bar(int x, GuideBarStyle style) {
+  int x1 = x - _pixel_scale / 6;
+  int x2 = x1 + _pixel_scale / 3;
+  if (style == GBS_frame) {
+    ++x2;
+  }
+  RECT rect = {x1, 0, x2, get_ysize()};
+  FillRect(_bitmap_dc, &rect, _grid_brush);
+}
+
+/**
+ * Draws a single bar in the chart for the indicated row, in the color for the
+ * given collector, for the indicated horizontal pixel range.
+ */
+void WinStatsTimeline::
+draw_bar(int row, int from_x, int to_x, int collector_index,
+         const std::string &collector_name) {
+
+  int top = row_to_pixel(row);
+  int bottom = row_to_pixel(row + 1);
+
+  bool is_highlighted = row == _highlighted_row && _highlighted_x >= from_x && _highlighted_x < to_x;
+  HBRUSH brush = get_collector_brush(collector_index, is_highlighted);
+
+  if (to_x < from_x + 2) {
+    // It's just a tiny sliver.  This is a more reliable way to draw it.
+    RECT rect = {from_x, top + 1, from_x + 1, bottom - 1};
+    FillRect(_bitmap_dc, &rect, brush);
+
+    //if (to_x <= from_x + 2) {
+    //  // Draw an arrow pointing to it, if it's so small.
+    //  POINT vertices[] = {{to_x, bottom}, {to_x - _pixel_scale, bottom + _pixel_scale * 2}, {to_x + _pixel_scale, bottom + _pixel_scale * 2}};
+    //  Polygon(_bitmap_dc, vertices, 3);
+    //}
+  }
+  else {
+    SelectObject(_bitmap_dc, brush);
+    RoundRect(_bitmap_dc,
+              std::max(from_x, -_pixel_scale - 1),
+              top,
+              std::min(std::max(to_x, from_x + 1), get_xsize() + _pixel_scale),
+              bottom,
+              _pixel_scale,
+              _pixel_scale);
+
+    if ((to_x - from_x) >= _pixel_scale * 4) {
+      // Only bother drawing the text if we've got some space to draw on.
+      // Choose a suitable foreground color.
+      SetTextColor(_bitmap_dc, get_collector_text_color(collector_index, is_highlighted));
+
+      // Make sure that the text doesn't run off the chart.
+      SIZE size;
+      GetTextExtentPoint32(_bitmap_dc, collector_name.data(), collector_name.size(), &size);
+      int center = (from_x + to_x) / 2;
+      int left = std::max(from_x, 0) + _pixel_scale / 2;
+      int right = std::min(to_x, get_xsize()) - _pixel_scale / 2;
+
+      if (size.cx >= right - left) {
+        if (right - left < _pixel_scale * 6) {
+          // It's a really tiny space.  Draw a single letter.
+          RECT rect = {left, top, right, bottom};
+          DrawText(_bitmap_dc, collector_name.data(), 1,
+                   &rect, DT_CENTER | DT_SINGLELINE | DT_VCENTER);
+        } else {
+          // It's going to be tricky to fit it, let Windows figure it out via
+          // the more expensive DrawText call.
+          RECT rect = {left, top, right, bottom};
+          DrawText(_bitmap_dc, collector_name.data(), collector_name.size(),
+                   &rect, DT_CENTER | DT_END_ELLIPSIS | DT_SINGLELINE | DT_VCENTER);
+        }
+      }
+      else {
+        int text_top = top + (bottom - top - size.cy) / 2;
+        if (center - size.cx / 2 < 0) {
+          // Put it against the left-most edge.
+          TextOut(_bitmap_dc, _pixel_scale, text_top,
+                  collector_name.data(), collector_name.length());
+        }
+        else if (center + size.cx / 2 >= get_xsize()) {
+          // Put it against the right-most edge.
+          TextOut(_bitmap_dc, get_xsize() - _pixel_scale - size.cx, text_top,
+                  collector_name.data(), collector_name.length());
+        }
+        else {
+          // It fits just fine, center it.
+          TextOut(_bitmap_dc, center - size.cx / 2, text_top,
+                  collector_name.data(), collector_name.length());
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Called after all the bars have been drawn, this triggers a refresh event to
+ * draw it to the window.
+ */
+void WinStatsTimeline::
+end_draw() {
+  InvalidateRect(_graph_window, nullptr, FALSE);
+
+  if (_threads_changed) {
+    RECT rect;
+    GetClientRect(_window, &rect);
+    rect.top = _top_margin;
+    rect.right = _left_margin;
+    InvalidateRect(_window, &rect, TRUE);
+    _threads_changed = false;
+  }
+
+  if (_guide_bars_changed) {
+    RECT rect;
+    GetClientRect(_window, &rect);
+    rect.bottom = _top_margin;
+    InvalidateRect(_window, &rect, TRUE);
+    _guide_bars_changed = false;
+  }
+}
+
+/**
+ * Called at the end of the draw cycle.
+ */
+void WinStatsTimeline::
+idle() {
+}
+
+/**
+ * Overridden by a derived class to implement an animation.  If it returns
+ * false, the animation timer is stopped.
+ */
+bool WinStatsTimeline::
+animate(double time, double dt) {
+  return PStatTimeline::animate(time, dt);
+}
+
+/**
+ *
+ */
+LONG WinStatsTimeline::
+window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
+  switch (msg) {
+  case WM_MOUSELEAVE:
+    SetFocus(nullptr);
+    break;
+
+  default:
+    break;
+  }
+
+  return WinStatsGraph::window_proc(hwnd, msg, wparam, lparam);
+}
+
+/**
+ *
+ */
+LONG WinStatsTimeline::
+graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
+  switch (msg) {
+  case WM_LBUTTONDOWN:
+    if (_potential_drag_mode == DM_none) {
+      set_drag_mode(DM_pan);
+      int16_t x = LOWORD(lparam);
+      _drag_start_x = x;
+      _scroll_speed = 0.0;
+      _zoom_center = pixel_to_timestamp(x);
+      SetCapture(_graph_window);
+      return 0;
+    }
+    break;
+
+  case WM_MOUSEMOVE:
+    // Make sure we can accept keyboard events, except if we're inactive.
+    if (GetActiveWindow() == _window) {
+      SetFocus(hwnd);
+    }
+
+    if (_drag_mode == DM_none && _potential_drag_mode == DM_none) {
+      // When the mouse is over a color bar, highlight it.
+      int x = LOWORD(lparam);
+      int y = HIWORD(lparam);
+      double time = pixel_to_timestamp(x);
+
+      int row = pixel_to_row(y);
+
+      if (row != _highlighted_row) {
+        clear_graph_tooltip();
+      }
+      else if (_highlighted_row >= 0) {
+        // Is the mouse on the same bar?  If not, clear the tooltip.
+        ColorBar bar;
+        if (find_bar(row, x, bar)) {
+          double prev_time = pixel_to_timestamp(_highlighted_x);
+          if (prev_time < bar._start || prev_time > bar._end) {
+            clear_graph_tooltip();
+          }
+        } else {
+          clear_graph_tooltip();
+        }
+      }
+
+      std::swap(_highlighted_x, x);
+      std::swap(_highlighted_row, row);
+
+      if (row >= 0) {
+        PStatTimeline::force_redraw(row, x, x);
+      }
+      PStatTimeline::force_redraw(_highlighted_row, _highlighted_x, _highlighted_x);
+
+      if ((_keys_held & (F_w | F_s)) != 0) {
+        // Update the zoom center if we move the mouse while zooming with the
+        // keyboard.
+        _zoom_center = time;
+      }
+
+      // 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 {
+      // If the mouse is in some drag mode, stop highlighting.
+      if (_highlighted_row != -1) {
+        int row = _highlighted_row;
+        _highlighted_row = -1;
+        PStatTimeline::force_redraw(row, _highlighted_x, _highlighted_x);
+        clear_graph_tooltip();
+      }
+    }
+
+    if (_drag_mode == DM_pan) {
+      int16_t x = LOWORD(lparam);
+      int delta = _drag_start_x - x;
+      set_horizontal_scroll(get_horizontal_scroll() + pixel_to_height(delta));
+      _drag_start_x = x;
+      return 0;
+    }
+    break;
+
+  case WM_MOUSELEAVE:
+    // When the mouse leaves the graph, stop highlighting.
+    if (_highlighted_row != -1) {
+      int row = _highlighted_row;
+      _highlighted_row = -1;
+      PStatTimeline::force_redraw(row, _highlighted_x, _highlighted_x);
+      clear_graph_tooltip();
+    }
+    SetFocus(nullptr);
+    break;
+
+  case WM_LBUTTONUP:
+    if (_drag_mode == DM_pan) {
+      set_drag_mode(DM_none);
+      ReleaseCapture();
+      return 0;
+    }
+    break;
+
+  case WM_LBUTTONDBLCLK:
+    {
+      // Double-clicking on a color bar in the graph will zoom the graph into
+      // that collector.
+      int16_t x = LOWORD(lparam);
+      int16_t y = HIWORD(lparam);
+      int row = pixel_to_row(y);
+      ColorBar bar;
+      if (find_bar(row, x, bar)) {
+        double width = bar._end - bar._start;
+        zoom_to(width * 1.5, pixel_to_timestamp(x));
+        scroll_to(bar._start - width / 4.0);
+      } else {
+        // Double-clicking the white area zooms out.
+        _zoom_speed -= 100.0;
+      }
+      start_animation();
+      return 0;
+    }
+    break;
+
+  case WM_CONTEXTMENU:
+    {
+      // Right-clicking a color bar brings up a context menu.
+      POINT point;
+      if (GetCursorPos(&point)) {
+        POINT graph_point =  point;
+        if (ScreenToClient(_graph_window, &graph_point)) {
+          int row = pixel_to_row(graph_point.y);
+          ColorBar bar;
+          if (find_bar(row, graph_point.x, bar)) {
+            _popup_bar = bar;
+
+            HMENU popup = CreatePopupMenu();
+
+            std::string label = get_bar_tooltip(row, graph_point.x);
+            if (!label.empty()) {
+              AppendMenu(popup, MF_STRING | MF_DISABLED, 0, label.c_str());
+            }
+            AppendMenu(popup, MF_STRING, 101, "Zoom To");
+            AppendMenu(popup, MF_STRING, 102, "Open Strip Chart");
+            AppendMenu(popup, MF_STRING, 103, "Open Flame Graph");
+            AppendMenu(popup, MF_STRING, 104, "Open Piano Roll");
+            TrackPopupMenu(popup, TPM_LEFTBUTTON, point.x, point.y, 0, _graph_window, nullptr);
+          }
+        }
+      }
+      return 0;
+    }
+    break;
+
+  case WM_COMMAND:
+    switch (LOWORD(wparam)) {
+    case 101:
+      {
+        double width = _popup_bar._end - _popup_bar._start;
+        zoom_to(width * 1.5, (_popup_bar._end + _popup_bar._start) / 2.0);
+        scroll_to(_popup_bar._start - width / 4.0);
+        start_animation();
+      }
+      return 0;
+
+    case 102:
+      WinStatsGraph::_monitor->open_strip_chart(_popup_bar._thread_index, _popup_bar._collector_index, false);
+      return 0;
+
+    case 103:
+      WinStatsGraph::_monitor->open_flame_graph(_popup_bar._thread_index, _popup_bar._collector_index);
+      return 0;
+
+    case 104:
+      WinStatsGraph::_monitor->open_piano_roll(_popup_bar._thread_index);
+      return 0;
+    }
+    break;
+
+  case WM_MOUSEWHEEL:
+    {
+      if (GET_KEYSTATE_WPARAM(wparam) & MK_CONTROL) {
+        // Zoom in/out around the cursor position.
+        POINT point;
+        if (GetCursorPos(&point) && ScreenToClient(_graph_window, &point)) {
+          int delta = GET_WHEEL_DELTA_WPARAM(wparam);
+          zoom_by(delta / 120.0, pixel_to_timestamp(point.x));
+          start_animation();
+        }
+      }
+      return 0;
+    }
+    break;
+
+  case WM_MOUSEHWHEEL:
+    {
+      int delta = GET_WHEEL_DELTA_WPARAM(wparam);
+      _scroll_speed += delta / 12.0;
+      start_animation();
+      return 0;
+    }
+    break;
+
+  case WM_KEYDOWN:
+  case WM_KEYUP:
+    {
+      int flag = 0;
+      int vsc = (lparam & 0xff0000) >> 16;
+      if ((lparam & 0x1000000) == 0) {
+        // Accept WASD based on their position rather than their mapping
+        switch (vsc) {
+        case 17:
+          flag = F_w;
+          break;
+        case 30:
+          flag = F_a;
+          break;
+        case 31:
+          flag = F_s;
+          break;
+        case 32:
+          flag = F_d;
+          break;
+        }
+      }
+      if (flag == 0) {
+        switch (wparam) {
+        case VK_LEFT:
+          flag = F_left;
+          break;
+        case VK_RIGHT:
+          flag = F_right;
+          break;
+        case 'W':
+          flag = F_w;
+          break;
+        case 'A':
+          flag = F_a;
+          break;
+        case 'S':
+          flag = F_s;
+          break;
+        case 'D':
+          flag = F_d;
+          break;
+        }
+      }
+      if (flag != 0) {
+        if (msg == WM_KEYDOWN) {
+          if (flag & (F_w | F_s)) {
+            POINT point;
+            if (GetCursorPos(&point) && ScreenToClient(_graph_window, &point)) {
+              _zoom_center = pixel_to_timestamp(point.x);
+            } else {
+              _zoom_center = get_horizontal_scroll() + get_horizontal_scale() / 2.0;
+            }
+          }
+          if (_keys_held == 0) {
+            start_animation();
+          }
+          _keys_held |= flag;
+        }
+        else if (_keys_held != 0) {
+          _keys_held &= ~flag;
+        }
+      }
+    }
+    break;
+
+  case WM_KILLFOCUS:
+    _keys_held = 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 WinStatsTimeline::
+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 - 2;
+
+  int num_guide_bars = get_num_guide_bars();
+  for (int i = 0; i < num_guide_bars; ++i) {
+    draw_guide_label(hdc, y, get_guide_bar(i));
+  }
+
+  SetTextColor(hdc, _dark_color);
+  SetTextAlign(hdc, TA_LEFT | TA_TOP | TA_NOUPDATECP);
+
+  for (const ThreadRow &thread_row : _threads) {
+    draw_thread_label(hdc, thread_row);
+  }
+}
+
+/**
+ * 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 WinStatsTimeline::
+additional_graph_window_paint(HDC hdc) {
+}
+
+/**
+ * Called when the mouse hovers over the graph, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string WinStatsTimeline::
+get_graph_tooltip(int mouse_x, int mouse_y) const {
+  return PStatTimeline::get_bar_tooltip(pixel_to_row(mouse_y), mouse_x);
+}
+
+/**
+ * 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 WinStatsTimeline::
+consider_drag_start(int mouse_x, int mouse_y, int width, int height) {
+  DragMode mode = WinStatsGraph::consider_drag_start(mouse_x, mouse_y, width, height);
+  if (mode == DM_right_margin) {
+    mode = DM_none;
+  }
+  return mode;
+}
+
+/**
+ * Draws the text for the indicated guide bar label at the top of the graph.
+ */
+void WinStatsTimeline::
+draw_guide_label(HDC hdc, int y, const PStatGraph::GuideBar &bar) {
+  const std::string &label = bar._label;
+  if (label.empty()) {
+    return;
+  }
+
+  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, _light_color);
+    break;
+
+  case GBS_frame:
+    SetTextColor(hdc, _dark_color);
+    break;
+  }
+
+  int x = timestamp_to_pixel(bar._height);
+  SIZE size;
+  GetTextExtentPoint32(hdc, label.data(), label.length(), &size);
+
+  int this_x = _graph_left + x - size.cx / 2;
+  if (x >= 0 && x < get_xsize()) {
+    TextOut(hdc, this_x, y,
+            label.data(), label.length());
+  }
+}
+
+/**
+ * Draws the text for the indicated thread on the side of the graph.
+ */
+void WinStatsTimeline::
+draw_thread_label(HDC hdc, const ThreadRow &thread_row) {
+  int top = row_to_pixel(thread_row._row_offset + 1);
+  int bottom = row_to_pixel(thread_row._row_offset + 2);
+
+  RECT rect = {_pixel_scale * 2, top, _left_margin - _pixel_scale * 2, bottom};
+  DrawText(hdc, thread_row._label.data(), thread_row._label.size(),
+           &rect, DT_RIGHT | DT_END_ELLIPSIS | DT_SINGLELINE | DT_VCENTER);
+}
+
+/**
+ * Creates the window for this strip chart.
+ */
+void WinStatsTimeline::
+create_window() {
+  if (_window) {
+    return;
+  }
+
+  HINSTANCE application = GetModuleHandle(nullptr);
+  register_window_class(application);
+
+  POINT window_pos = WinStatsGraph::_monitor->get_new_window_pos();
+
+  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 =
+    CreateWindowEx(WS_EX_DLGMODALFRAME, _window_class_name,
+                   "Timeline", graph_window_style,
+                   window_pos.x, window_pos.y,
+                   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 timeline window!\n";
+    exit(1);
+  }
+
+  SetWindowLongPtr(_window, 0, (LONG_PTR)this);
+
+  // Ensure that the window is on top of the stack.
+  SetWindowPos(_window, HWND_TOP, 0, 0, 0, 0,
+               SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
+
+  SetFocus(_window);
+}
+
+/**
+ * Registers the window class for the Timeline window, if it has not already
+ * been registered.
+ */
+void WinStatsTimeline::
+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(WinStatsTimeline *);
+
+  if (!RegisterClass(&wc)) {
+    nout << "Could not register Timeline window class!\n";
+    exit(1);
+  }
+
+  _window_class_registered = true;
+}
+
+/**
+ *
+ */
+LONG WINAPI WinStatsTimeline::
+static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
+  WinStatsTimeline *self = (WinStatsTimeline *)GetWindowLongPtr(hwnd, 0);
+  if (self != nullptr && self->_window == hwnd) {
+    return self->window_proc(hwnd, msg, wparam, lparam);
+  } else {
+    return DefWindowProc(hwnd, msg, wparam, lparam);
+  }
+}

+ 89 - 0
pandatool/src/win-stats/winStatsTimeline.h

@@ -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 winStatsTimeline.h
+ * @author rdb
+ * @date 2022-02-11
+ */
+
+#ifndef WINSTATSTIMELINE_H
+#define WINSTATSTIMELINE_H
+
+#include "pandatoolbase.h"
+
+#include "winStatsGraph.h"
+#include "pStatTimeline.h"
+
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN 1
+#endif
+#include <windows.h>
+
+class WinStatsMonitor;
+
+/**
+ * A window that draws all of the start/stop event pairs on each thread on a
+ * horizontal scrolling timeline, with concurrent start/stop pairs stacked
+ * underneath each other.
+ */
+class WinStatsTimeline : public PStatTimeline, public WinStatsGraph {
+public:
+  WinStatsTimeline(WinStatsMonitor *monitor);
+  virtual ~WinStatsTimeline();
+
+  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);
+
+protected:
+  virtual void clear_region();
+  virtual void begin_draw();
+  virtual void draw_separator(int row);
+  virtual void draw_guide_bar(int x, GuideBarStyle style);
+  virtual void draw_bar(int row, int from_x, int to_x, int collector_index,
+                        const std::string &collector_name);
+  virtual void end_draw();
+  virtual void idle();
+
+  virtual bool animate(double time, double dt);
+
+  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 std::string get_graph_tooltip(int mouse_x, int mouse_y) const;
+  virtual DragMode consider_drag_start(int mouse_x, int mouse_y,
+                                       int width, int height);
+
+private:
+  void draw_guide_label(HDC hdc, int y, const GuideBar &bar);
+  void draw_thread_label(HDC hdc, const ThreadRow &thread_row);
+
+  void create_window();
+  static void register_window_class(HINSTANCE application);
+
+  static LONG WINAPI static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
+
+  int row_to_pixel(int y) const {
+    return y * _pixel_scale * 5 + _pixel_scale;
+  }
+  int pixel_to_row(int y) const {
+    return (y - _pixel_scale) / (_pixel_scale * 5);
+  }
+
+  static bool _window_class_registered;
+  static const char * const _window_class_name;
+
+  HBRUSH _grid_brush;
+
+  int _highlighted_row = -1;
+  int _highlighted_x = 0;
+  ColorBar _popup_bar;
+};
+
+#endif

+ 1 - 0
pandatool/src/win-stats/winstats_composite1.cxx

@@ -6,5 +6,6 @@
 #include "winStatsLabelStack.cxx"
 #include "winStatsLabelStack.cxx"
 #include "winStatsMonitor.cxx"
 #include "winStatsMonitor.cxx"
 #include "winStatsPianoRoll.cxx"
 #include "winStatsPianoRoll.cxx"
+#include "winStatsTimeline.cxx"
 #include "winStatsServer.cxx"
 #include "winStatsServer.cxx"
 #include "winStatsStripChart.cxx"
 #include "winStatsStripChart.cxx"