Browse Source

pstats: Third significant update to PStats server UI, including:

* Windows stay open after client disconnects, for further inspection
* Ability to save session results to a file, and reopening those files
* Ability to save the current graph window layout for new sessions
* Ability to change colors (by right-clicking on bar)
* SI prefixes for Hz units (kHz, MHz, etc.)
* Ability to export session to Chrome Tracing JSON format
* "Close All Graphs" menu option
* Graphs now properly show data when opened while Pause is on
* Some fixes for weird graph window minimize behavior on Windows
rdb 3 years ago
parent
commit
bb6976d558
74 changed files with 4056 additions and 1007 deletions
  1. 2 1
      makepanda/makepanda.py
  2. 2 1
      panda/src/pstatclient/pStatCollectorDef.h
  3. 1 1
      panda/src/pstatclient/pStatFrameData.cxx
  4. 2 2
      panda/src/pstatclient/pStatFrameData.h
  5. 0 1
      pandatool/src/gtk-stats/CMakeLists.txt
  6. 3 78
      pandatool/src/gtk-stats/gtkStats.cxx
  7. 0 4
      pandatool/src/gtk-stats/gtkStats.h
  8. 96 25
      pandatool/src/gtk-stats/gtkStatsChartMenu.cxx
  9. 5 0
      pandatool/src/gtk-stats/gtkStatsChartMenu.h
  10. 50 0
      pandatool/src/gtk-stats/gtkStatsFlameGraph.cxx
  11. 5 0
      pandatool/src/gtk-stats/gtkStatsFlameGraph.h
  12. 41 0
      pandatool/src/gtk-stats/gtkStatsGraph.cxx
  13. 7 0
      pandatool/src/gtk-stats/gtkStatsGraph.h
  14. 34 25
      pandatool/src/gtk-stats/gtkStatsLabel.cxx
  15. 1 0
      pandatool/src/gtk-stats/gtkStatsLabel.h
  16. 13 3
      pandatool/src/gtk-stats/gtkStatsLabelStack.cxx
  17. 1 0
      pandatool/src/gtk-stats/gtkStatsLabelStack.h
  18. 0 40
      pandatool/src/gtk-stats/gtkStatsMenuId.h
  19. 236 214
      pandatool/src/gtk-stats/gtkStatsMonitor.cxx
  20. 20 16
      pandatool/src/gtk-stats/gtkStatsMonitor.h
  21. 50 1
      pandatool/src/gtk-stats/gtkStatsPianoRoll.cxx
  22. 5 0
      pandatool/src/gtk-stats/gtkStatsPianoRoll.h
  23. 676 2
      pandatool/src/gtk-stats/gtkStatsServer.cxx
  24. 47 1
      pandatool/src/gtk-stats/gtkStatsServer.h
  25. 51 3
      pandatool/src/gtk-stats/gtkStatsStripChart.cxx
  26. 5 0
      pandatool/src/gtk-stats/gtkStatsStripChart.h
  27. 50 0
      pandatool/src/gtk-stats/gtkStatsTimeline.cxx
  28. 5 0
      pandatool/src/gtk-stats/gtkStatsTimeline.h
  29. 165 10
      pandatool/src/pstatserver/pStatClientData.cxx
  30. 14 2
      pandatool/src/pstatserver/pStatClientData.h
  31. 51 3
      pandatool/src/pstatserver/pStatFlameGraph.cxx
  32. 3 0
      pandatool/src/pstatserver/pStatFlameGraph.h
  33. 85 10
      pandatool/src/pstatserver/pStatGraph.cxx
  34. 8 0
      pandatool/src/pstatserver/pStatGraph.h
  35. 1 1
      pandatool/src/pstatserver/pStatListener.cxx
  36. 16 0
      pandatool/src/pstatserver/pStatMonitor.I
  37. 472 0
      pandatool/src/pstatserver/pStatMonitor.cxx
  38. 27 2
      pandatool/src/pstatserver/pStatMonitor.h
  39. 42 1
      pandatool/src/pstatserver/pStatPianoRoll.cxx
  40. 3 0
      pandatool/src/pstatserver/pStatPianoRoll.h
  41. 1 0
      pandatool/src/pstatserver/pStatReader.cxx
  42. 17 4
      pandatool/src/pstatserver/pStatServer.cxx
  43. 5 1
      pandatool/src/pstatserver/pStatServer.h
  44. 66 4
      pandatool/src/pstatserver/pStatStripChart.cxx
  45. 6 2
      pandatool/src/pstatserver/pStatStripChart.h
  46. 40 9
      pandatool/src/pstatserver/pStatThreadData.cxx
  47. 12 6
      pandatool/src/pstatserver/pStatThreadData.h
  48. 44 5
      pandatool/src/pstatserver/pStatTimeline.cxx
  49. 3 0
      pandatool/src/pstatserver/pStatTimeline.h
  50. 4 4
      pandatool/src/text-stats/textMonitor.cxx
  51. 1 1
      pandatool/src/text-stats/textStats.cxx
  52. 1 1
      pandatool/src/text-stats/textStats.h
  53. 1 1
      pandatool/src/win-stats/CMakeLists.txt
  54. 3 81
      pandatool/src/win-stats/winStats.cxx
  55. 41 16
      pandatool/src/win-stats/winStatsChartMenu.cxx
  56. 56 2
      pandatool/src/win-stats/winStatsFlameGraph.cxx
  57. 7 0
      pandatool/src/win-stats/winStatsFlameGraph.h
  58. 88 0
      pandatool/src/win-stats/winStatsGraph.cxx
  59. 8 0
      pandatool/src/win-stats/winStatsGraph.h
  60. 48 29
      pandatool/src/win-stats/winStatsLabel.cxx
  61. 3 2
      pandatool/src/win-stats/winStatsLabel.h
  62. 18 12
      pandatool/src/win-stats/winStatsLabelStack.cxx
  63. 1 0
      pandatool/src/win-stats/winStatsLabelStack.h
  64. 13 0
      pandatool/src/win-stats/winStatsMenuId.h
  65. 233 353
      pandatool/src/win-stats/winStatsMonitor.cxx
  66. 17 20
      pandatool/src/win-stats/winStatsMonitor.h
  67. 32 1
      pandatool/src/win-stats/winStatsPianoRoll.cxx
  68. 5 0
      pandatool/src/win-stats/winStatsPianoRoll.h
  69. 853 2
      pandatool/src/win-stats/winStatsServer.cxx
  70. 51 1
      pandatool/src/win-stats/winStatsServer.h
  71. 43 3
      pandatool/src/win-stats/winStatsStripChart.cxx
  72. 5 0
      pandatool/src/win-stats/winStatsStripChart.h
  73. 30 0
      pandatool/src/win-stats/winStatsTimeline.cxx
  74. 5 0
      pandatool/src/win-stats/winStatsTimeline.h

+ 2 - 1
makepanda/makepanda.py

@@ -613,6 +613,7 @@ if (COMPILER == "MSVC"):
     LibName("WINSOCK2", "ws2_32.lib")
     LibName("WINCOMCTL", "comctl32.lib")
     LibName("WINCOMDLG", "comdlg32.lib")
+    LibName("UXTHEME", "uxtheme.lib")
     LibName("WINUSER", "user32.lib")
     LibName("WINMM", "winmm.lib")
     LibName("WINIMM", "imm32.lib")
@@ -5910,7 +5911,7 @@ if not PkgSkip("PANDATOOL") and (GetTarget() == 'windows' or not PkgSkip("GTK3")
     TargetAdd('pstats.exe', input='libp3progbase.lib')
     TargetAdd('pstats.exe', input='libp3pandatoolbase.lib')
     TargetAdd('pstats.exe', input=COMMON_PANDA_LIBS)
-    TargetAdd('pstats.exe', opts=['SUBSYSTEM:WINDOWS', 'WINCOMCTL', 'WINSOCK', 'WINIMM', 'WINGDI', 'WINKERNEL', 'WINOLDNAMES', 'WINUSER', 'WINMM', 'GTK3'])
+    TargetAdd('pstats.exe', opts=['SUBSYSTEM:WINDOWS', 'WINCOMCTL', 'WINCOMDLG', 'WINSOCK', 'WINIMM', 'WINGDI', 'WINKERNEL', 'WINOLDNAMES', 'WINUSER', 'WINMM', 'UXTHEME', 'GTK3'])
 
 #
 # DIRECTORY: pandatool/src/xfileprogs/

+ 2 - 1
panda/src/pstatclient/pStatCollectorDef.h

@@ -33,7 +33,8 @@ public:
   void set_parent(const PStatCollectorDef &parent);
 
   void write_datagram(Datagram &destination) const;
-  void read_datagram(DatagramIterator &source, PStatClientVersion *version);
+  void read_datagram(DatagramIterator &source,
+                     PStatClientVersion *version = nullptr);
 
   struct ColorDef {
     float r, g, b;

+ 1 - 1
panda/src/pstatclient/pStatFrameData.cxx

@@ -81,5 +81,5 @@ read_datagram(DatagramIterator &source, PStatClientVersion *) {
     dp._value = source.get_float32();
     _level_data.push_back(dp);
   }
-  nassertv(source.get_remaining_size() == 0);
+  //nassertv(source.get_remaining_size() == 0);
 }

+ 2 - 2
panda/src/pstatclient/pStatFrameData.h

@@ -57,8 +57,8 @@ public:
   INLINE int get_level_collector(size_t n) const;
   INLINE double get_level(size_t n) const;
 
-  bool write_datagram(Datagram &destination, PStatClient *client) const;
-  void read_datagram(DatagramIterator &source, PStatClientVersion *version);
+  bool write_datagram(Datagram &destination, PStatClient *client = nullptr) const;
+  void read_datagram(DatagramIterator &source, PStatClientVersion *version = nullptr);
 
 private:
   class DataPoint {

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

@@ -8,7 +8,6 @@ set(GTKSTATS_HEADERS
   gtkStatsGraph.h
   gtkStatsLabel.h
   gtkStatsLabelStack.h
-  gtkStatsMenuId.h
   gtkStatsMonitor.h gtkStatsMonitor.I
   gtkStatsPianoRoll.h
   gtkStatsServer.h

+ 3 - 78
pandatool/src/gtk-stats/gtkStats.cxx

@@ -16,88 +16,13 @@
 #include "gtkStatsServer.h"
 #include "config_pstatclient.h"
 
-GtkWidget *main_window;
-static GtkStatsServer *server = nullptr;
-
-static gboolean
-delete_event(GtkWidget *widget,
-       GdkEvent *event, gpointer data) {
-  // Returning FALSE to indicate we should destroy the main window when the
-  // user selects "close".
-  return FALSE;
-}
-
-static void
-destroy(GtkWidget *widget, gpointer data) {
-  gtk_main_quit();
-}
-
-static gboolean
-timer(gpointer data) {
-  static int count = 0;
-  server->poll();
-
-  if (++count == 5) {
-    count = 0;
-    // Every once in a while, say once a second, we call this function, which
-    // should force gdk to make all changes visible.  We do this in case we
-    // are getting starved and falling behind, so that the user still gets a
-    // chance to see *something* happen onscreen, even if it's just
-    // increasingly old data.
-    //gdk_window_process_all_updates();
-  }
-
-  return TRUE;
-}
-
 int
 main(int argc, char *argv[]) {
   gtk_init(&argc, &argv);
 
-  main_window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
-
-  gtk_window_set_title(GTK_WINDOW(main_window), "PStats");
-
-  // Connect the delete and destroy events, so the user can exit the
-  // application by closing the main window.
-  g_signal_connect(G_OBJECT(main_window), "delete_event",
-       G_CALLBACK(delete_event), nullptr);
-
-  g_signal_connect(G_OBJECT(main_window), "destroy",
-       G_CALLBACK(destroy), nullptr);
-
-  std::ostringstream stream;
-  stream << "Listening on port " << pstats_port;
-  std::string str = stream.str();
-  GtkWidget *label = gtk_label_new(str.c_str());
-  gtk_container_add(GTK_CONTAINER(main_window), label);
-  gtk_widget_show(label);
-
-  // Create the server object.
-  server = new GtkStatsServer;
-  if (!server->listen()) {
-    std::ostringstream stream;
-    stream
-      << "Unable to open port " << pstats_port
-      << ".  Try specifying a different\n"
-      << "port number using pstats-port in your Config file.";
-    std::string str = stream.str();
-
-    GtkWidget *dialog =
-      gtk_message_dialog_new(GTK_WINDOW(main_window),
-           GTK_DIALOG_DESTROY_WITH_PARENT,
-           GTK_MESSAGE_ERROR,
-           GTK_BUTTONS_CLOSE,
-           "%s", str.c_str());
-    gtk_dialog_run(GTK_DIALOG(dialog));
-    gtk_widget_destroy(dialog);
-    exit(1);
-  }
-
-  gtk_widget_show(main_window);
-
-  // Set up a timer to poll the pstats every so often.
-  g_timeout_add(200, timer, nullptr);
+  // Create the server window.
+  GtkStatsServer *server = new GtkStatsServer;
+  server->new_session();
 
   // Now get lost in the message loop.
   gtk_main();

+ 0 - 4
pandatool/src/gtk-stats/gtkStats.h

@@ -16,8 +16,4 @@
 
 #include "pStatServer.h"
 
-#include <gtk/gtk.h>
-
-extern GtkWidget *main_window;
-
 #endif

+ 96 - 25
pandatool/src/gtk-stats/gtkStatsChartMenu.cxx

@@ -56,11 +56,19 @@ add_to_menu_bar(GtkWidget *menu_bar, int position) {
     thread_name = client_data->get_thread_name(_thread_index);
   }
 
-  GtkWidget *menu_item = gtk_menu_item_new_with_label(thread_name.c_str());
-  gtk_widget_show(menu_item);
-  gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), _menu);
+  _menu_item = gtk_menu_item_new_with_label(thread_name.c_str());
+  gtk_widget_show(_menu_item);
+  gtk_menu_item_set_submenu(GTK_MENU_ITEM(_menu_item), _menu);
 
-  gtk_menu_shell_insert(GTK_MENU_SHELL(menu_bar), menu_item, position);
+  gtk_menu_shell_insert(GTK_MENU_SHELL(menu_bar), _menu_item, position);
+}
+
+/**
+ * Removes the menu from the menu bar.
+ */
+void GtkStatsChartMenu::
+remove_from_menu_bar(GtkWidget *menu_bar) {
+  gtk_container_remove(GTK_CONTAINER(menu_bar), _menu_item);
 }
 
 /**
@@ -90,16 +98,32 @@ do_update() {
 
   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);
+    {
+      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);
+      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);
+      g_signal_connect(G_OBJECT(menu_item), "activate",
+                       G_CALLBACK(GtkStatsMonitor::menu_activate),
+                       (void *)menu_def);
+    }
+
+    // And the piano roll (even though it's not very useful nowadays)
+    {
+      GtkStatsMonitor::MenuDef smd(_thread_index, -1, GtkStatsMonitor::CT_piano_roll, false);
+      const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd);
+
+      GtkWidget *menu_item = gtk_menu_item_new_with_label("Piano Roll");
+      gtk_widget_show(menu_item);
+      gtk_menu_shell_append(GTK_MENU_SHELL(_menu), menu_item);
+
+      g_signal_connect(G_OBJECT(menu_item), "activate",
+                       G_CALLBACK(GtkStatsMonitor::menu_activate),
+                       (void *)menu_def);
+    }
 
     GtkWidget *sep = gtk_separator_menu_item_new();
     gtk_widget_show(sep);
@@ -134,22 +158,41 @@ do_update() {
     }
   }
 
-  // Also menu item for piano roll (following a separator).
-  GtkWidget *sep = gtk_separator_menu_item_new();
-  gtk_widget_show(sep);
-  gtk_menu_shell_append(GTK_MENU_SHELL(_menu), sep);
+  // For the main thread menu, also some options relating to all graph windows.
+  if (_thread_index == 0) {
+    GtkWidget *sep = gtk_separator_menu_item_new();
+    gtk_widget_show(sep);
+    gtk_menu_shell_append(GTK_MENU_SHELL(_menu), sep);
 
-  {
-    GtkStatsMonitor::MenuDef smd(_thread_index, -1, GtkStatsMonitor::CT_piano_roll, false);
-    const GtkStatsMonitor::MenuDef *menu_def = _monitor->add_menu(smd);
+    {
+      GtkWidget *menu_item = gtk_menu_item_new_with_label("Close All Graphs");
+      gtk_widget_show(menu_item);
+      gtk_menu_shell_append(GTK_MENU_SHELL(_menu), menu_item);
 
-    GtkWidget *menu_item = gtk_menu_item_new_with_label("Piano Roll");
-    gtk_widget_show(menu_item);
-    gtk_menu_shell_append(GTK_MENU_SHELL(_menu), menu_item);
+      g_signal_connect(G_OBJECT(menu_item), "activate",
+                       G_CALLBACK(activate_close_all),
+                       (void *)_monitor);
+    }
 
-    g_signal_connect(G_OBJECT(menu_item), "activate",
-                     G_CALLBACK(GtkStatsMonitor::menu_activate),
-                     (void *)menu_def);
+    {
+      GtkWidget *menu_item = gtk_menu_item_new_with_label("Reopen Default Graphs");
+      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(activate_reopen_default),
+                       (void *)_monitor);
+    }
+
+    {
+      GtkWidget *menu_item = gtk_menu_item_new_with_label("Save Current Layout as Default");
+      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(activate_save_default),
+                       (void *)_monitor);
+    }
   }
 }
 
@@ -250,3 +293,31 @@ remove_menu_child(GtkWidget *widget, gpointer data) {
   GtkWidget *menu = (GtkWidget *)data;
   gtk_container_remove(GTK_CONTAINER(menu), widget);
 }
+
+/**
+ * Callback for Close All Graphs.
+ */
+void GtkStatsChartMenu::
+activate_close_all(GtkWidget *widget, gpointer data) {
+  GtkStatsMonitor *monitor = (GtkStatsMonitor *)data;
+  monitor->remove_all_graphs();
+}
+
+/**
+ * Callback for Reopen Default Graphs.
+ */
+void GtkStatsChartMenu::
+activate_reopen_default(GtkWidget *widget, gpointer data) {
+  GtkStatsMonitor *monitor = (GtkStatsMonitor *)data;
+  monitor->remove_all_graphs();
+  monitor->open_default_graphs();
+}
+
+/**
+ * Callback for Save Current Layout as Default.
+ */
+void GtkStatsChartMenu::
+activate_save_default(GtkWidget *widget, gpointer data) {
+  GtkStatsMonitor *monitor = (GtkStatsMonitor *)data;
+  monitor->save_default_graphs();
+}

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

@@ -32,6 +32,7 @@ public:
 
   GtkWidget *get_menu_widget();
   void add_to_menu_bar(GtkWidget *menu_bar, int position);
+  void remove_from_menu_bar(GtkWidget *menu_bar);
 
   void check_update();
   void do_update();
@@ -41,12 +42,16 @@ private:
                 bool show_level);
 
   static void remove_menu_child(GtkWidget *widget, gpointer data);
+  static void activate_close_all(GtkWidget *widget, gpointer data);
+  static void activate_reopen_default(GtkWidget *widget, gpointer data);
+  static void activate_save_default(GtkWidget *widget, gpointer data);
 
   GtkStatsMonitor *_monitor;
   int _thread_index;
 
   int _last_level_index;
   GtkWidget *_menu;
+  GtkWidget *_menu_item = nullptr;
 };
 
 #endif

+ 50 - 0
pandatool/src/gtk-stats/gtkStatsFlameGraph.cxx

@@ -306,6 +306,25 @@ animate(double time, double dt) {
   return PStatFlameGraph::animate(time, dt);
 }
 
+/**
+ * Returns the current window dimensions.
+ */
+bool GtkStatsFlameGraph::
+get_window_state(int &x, int &y, int &width, int &height,
+                 bool &maximized, bool &minimized) const {
+  GtkStatsGraph::get_window_state(x, y, width, height, maximized, minimized);
+  return true;
+}
+
+/**
+ * Called to restore the graph window to its previous dimensions.
+ */
+void GtkStatsFlameGraph::
+set_window_state(int x, int y, int width, int height,
+                 bool maximized, bool minimized) {
+  GtkStatsGraph::set_window_state(x, y, width, height, maximized, minimized);
+}
+
 /**
  * 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.
@@ -417,6 +436,37 @@ handle_button_press(int graph_x, int graph_y, bool double_click, int button) {
                            (void *)menu_def);
         }
 
+        {
+          GtkWidget *menu_item = gtk_separator_menu_item_new();
+          gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+        }
+
+        {
+          const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({
+            -1, collector_index,
+            GtkStatsMonitor::CT_choose_color,
+          });
+
+          GtkWidget *menu_item = gtk_menu_item_new_with_label("Change Color...");
+          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({
+            -1, collector_index,
+            GtkStatsMonitor::CT_reset_color,
+          });
+
+          GtkWidget *menu_item = gtk_menu_item_new_with_label("Reset Color");
+          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;

+ 5 - 0
pandatool/src/gtk-stats/gtkStatsFlameGraph.h

@@ -52,6 +52,11 @@ protected:
 
   virtual bool animate(double time, double dt);
 
+  virtual bool get_window_state(int &x, int &y, int &width, int &height,
+                                bool &maximized, bool &minimized) const;
+  virtual void set_window_state(int x, int y, int width, int height,
+                                bool maximized, bool minimized);
+
   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);

+ 41 - 0
pandatool/src/gtk-stats/gtkStatsGraph.cxx

@@ -309,6 +309,36 @@ animate(double time, double dt) {
   return false;
 }
 
+/**
+ * Returns the current window dimensions.
+ */
+void GtkStatsGraph::
+get_window_state(int &x, int &y, int &width, int &height,
+                 bool &maximized, bool &minimized) const {
+  GtkWindow *window = GTK_WINDOW(_window);
+  gtk_window_get_position(window, &x, &y);
+  gtk_window_get_size(window, &width, &height);
+  maximized = gtk_window_is_maximized(window);
+  minimized = false;
+}
+
+/**
+ * Called to restore the graph window to its previous dimensions.
+ */
+void GtkStatsGraph::
+set_window_state(int x, int y, int width, int height,
+                 bool maximized, bool minimized) {
+  GtkWindow *window = GTK_WINDOW(_window);
+  gtk_window_move(window, x, y);
+  gtk_window_resize(window, width, height);
+  if (maximized) {
+    gtk_window_maximize(window);
+  }
+  if (minimized) {
+    gtk_window_iconify(window);
+  }
+}
+
 /**
  * Returns a pattern suitable for drawing in the indicated collector's color.
  */
@@ -358,6 +388,17 @@ get_collector_text_color(int collector_index, bool highlight) {
   return highlight ? hcolor : color;
 }
 
+/**
+ * Called when the given collector has changed colors.
+ */
+void GtkStatsGraph::
+reset_collector_color(int collector_index) {
+  _brushes.erase(collector_index);
+  _text_colors.erase(collector_index);
+  force_redraw();
+  _label_stack.update_label_color(collector_index);
+}
+
 /**
  * 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.

+ 7 - 0
pandatool/src/gtk-stats/gtkStatsGraph.h

@@ -60,12 +60,19 @@ public:
   virtual void on_leave_label(int collector_index);
   virtual std::string get_label_tooltip(int collector_index) const;
 
+  void reset_collector_color(int collector_index);
+
 protected:
   void close();
 
   void start_animation();
   virtual bool animate(double time, double dt);
 
+  void get_window_state(int &x, int &y, int &width, int &height,
+                        bool &maximized, bool &minimized) const;
+  void set_window_state(int x, int y, int width, int height,
+                        bool maximized, bool minimized);
+
   cairo_pattern_t *get_collector_pattern(int collector_index, bool highlight = false);
   LRGBColor get_collector_text_color(int collector_index, bool highlight = false);
 

+ 34 - 25
pandatool/src/gtk-stats/gtkStatsLabel.cxx

@@ -51,34 +51,10 @@ GtkStatsLabel(GtkStatsMonitor *monitor, GtkStatsGraph *graph,
 
   gtk_widget_set_has_tooltip(_widget, TRUE);
 
-  // Set the fg and bg colors on the label.
-  LRGBColor rgb = _monitor->get_collector_color(_collector_index);
-  _bg_color = LRGBColor(
-    encode_sRGB_float((float)rgb[0]),
-    encode_sRGB_float((float)rgb[1]),
-    encode_sRGB_float((float)rgb[2]));
-
-  _highlight_bg_color = LRGBColor(
-    encode_sRGB_float((float)rgb[0] * 0.75f),
-    encode_sRGB_float((float)rgb[1] * 0.75f),
-    encode_sRGB_float((float)rgb[2] * 0.75f));
-
-  // Should our foreground be black or white?
-  PN_stdfloat bright = _bg_color.dot(LRGBColor(0.2126, 0.7152, 0.0722));
-  if (bright >= 0.5) {
-    _fg_color = LRGBColor(0);
-  } else {
-    _fg_color = LRGBColor(1);
-  }
-  if (bright * 0.75 >= 0.5) {
-    _highlight_fg_color = LRGBColor(0);
-  } else {
-    _highlight_fg_color = LRGBColor(1);
-  }
-
   _highlight = false;
   _mouse_within = false;
 
+  update_color();
   update_text(use_fullname);
   gtk_widget_show_all(_widget);
 }
@@ -145,6 +121,39 @@ get_highlight() const {
   return _highlight;
 }
 
+/**
+ * Updates the colors.
+ */
+void GtkStatsLabel::
+update_color() {
+  // Set the fg and bg colors on the label.
+  LRGBColor rgb = _monitor->get_collector_color(_collector_index);
+  _bg_color = LRGBColor(
+    encode_sRGB_float((float)rgb[0]),
+    encode_sRGB_float((float)rgb[1]),
+    encode_sRGB_float((float)rgb[2]));
+
+  _highlight_bg_color = LRGBColor(
+    encode_sRGB_float((float)rgb[0] * 0.75f),
+    encode_sRGB_float((float)rgb[1] * 0.75f),
+    encode_sRGB_float((float)rgb[2] * 0.75f));
+
+  // Should our foreground be black or white?
+  PN_stdfloat bright = _bg_color.dot(LRGBColor(0.2126, 0.7152, 0.0722));
+  if (bright >= 0.5) {
+    _fg_color = LRGBColor(0);
+  } else {
+    _fg_color = LRGBColor(1);
+  }
+  if (bright * 0.75 >= 0.5) {
+    _highlight_fg_color = LRGBColor(0);
+  } else {
+    _highlight_fg_color = LRGBColor(1);
+  }
+
+  gtk_widget_queue_draw(_widget);
+}
+
 /**
  * Set to true if the full name of the collector should be shown.
  */

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

@@ -43,6 +43,7 @@ public:
   void set_highlight(bool highlight);
   bool get_highlight() const;
 
+  void update_color();
   void update_text(bool use_fullname);
 
 private:

+ 13 - 3
pandatool/src/gtk-stats/gtkStatsLabelStack.cxx

@@ -128,10 +128,20 @@ void GtkStatsLabelStack::
 highlight_label(int collector_index) {
   if (_highlight_label != collector_index) {
     _highlight_label = collector_index;
-    Labels::iterator li;
-    for (li = _labels.begin(); li != _labels.end(); ++li) {
-      GtkStatsLabel *label = (*li);
+    for (GtkStatsLabel *label : _labels) {
       label->set_highlight(label->get_collector_index() == _highlight_label);
     }
   }
 }
+
+/**
+ * Refreshes the color of the label with the given index.
+ */
+void GtkStatsLabelStack::
+update_label_color(int collector_index) {
+  for (GtkStatsLabel *label : _labels) {
+    if (label->get_collector_index() == collector_index) {
+      label->update_color();
+    }
+  }
+}

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

@@ -43,6 +43,7 @@ public:
   int get_num_labels() const;
 
   void highlight_label(int collector_index);
+  void update_label_color(int collector_index);
 
 private:
   GtkWidget *_widget;

+ 0 - 40
pandatool/src/gtk-stats/gtkStatsMenuId.h

@@ -1,40 +0,0 @@
-/**
- * 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 gtkStatsMenuId.h
- * @author drose
- * @date 2006-01-16
- */
-
-#ifndef GTKSTATSMENUID_H
-#define GTKSTATSMENUID_H
-
-#include "pandatoolbase.h"
-
-/**
- * The enumerated values here are used for menu ID's for the various pulldown
- * menus in the application.
- */
-enum GtkStatsMenuId {
-  MI_none,
-  MI_time_ms,
-  MI_time_hz,
-  MI_frame_rate_label,
-  MI_speed_1,
-  MI_speed_2,
-  MI_speed_3,
-  MI_speed_6,
-  MI_speed_12,
-  MI_pause,
-
-  // This one is last and represents the beginning of the range for the
-  // various "new chart" menu options.
-  MI_new_chart
-};
-
-#endif

+ 236 - 214
pandatool/src/gtk-stats/gtkStatsMonitor.cxx

@@ -19,23 +19,28 @@
 #include "gtkStatsPianoRoll.h"
 #include "gtkStatsFlameGraph.h"
 #include "gtkStatsTimeline.h"
-#include "gtkStatsMenuId.h"
 #include "pStatGraph.h"
 #include "pStatCollectorDef.h"
 
+#include "convert_srgb.h"
+
 /**
  *
  */
 GtkStatsMonitor::
 GtkStatsMonitor(GtkStatsServer *server) : PStatMonitor(server) {
-  _window = nullptr;
+  _window = server->get_window();
+  _menu_bar = server->get_menu_bar();
+  _status_bar = server->get_status_bar();
 
   // These will be filled in later when the menu is created.
-  _time_units = 0;
   _scroll_speed = 0.0;
   _pause = false;
+  _next_chart_index = 2;
 
   _resolution = gdk_screen_get_resolution(gdk_screen_get_default());
+  setup_speed_menu();
+  setup_frame_rate_label();
 }
 
 /**
@@ -43,7 +48,41 @@ GtkStatsMonitor(GtkStatsServer *server) : PStatMonitor(server) {
  */
 GtkStatsMonitor::
 ~GtkStatsMonitor() {
-  shutdown();
+  close();
+}
+
+/**
+ * Closes all the graphs associated with this monitor.
+ */
+void GtkStatsMonitor::
+close() {
+  PStatMonitor::close();
+
+  remove_all_graphs();
+
+  for (GtkWidget *label : _status_bar_labels) {
+    gtk_container_remove(GTK_CONTAINER(_status_bar), label);
+  }
+  _status_bar_collectors.clear();
+  _status_bar_labels.clear();
+
+  if (_speed_menu_item != nullptr) {
+    gtk_container_remove(GTK_CONTAINER(_menu_bar), _speed_menu_item);
+    _speed_menu_item = nullptr;
+  }
+
+  for (GtkStatsChartMenu *chart_menu : _chart_menus) {
+    chart_menu->remove_from_menu_bar(_menu_bar);
+    delete chart_menu;
+  }
+  _chart_menus.clear();
+
+  if (_frame_rate_menu_item != nullptr) {
+    gtk_container_remove(GTK_CONTAINER(_menu_bar), _frame_rate_menu_item);
+    _frame_rate_menu_item = nullptr;
+  }
+
+  _next_chart_index = 2;
 }
 
 /**
@@ -72,8 +111,6 @@ initialized() {
  */
 void GtkStatsMonitor::
 got_hello() {
-  create_window();
-  open_strip_chart(0, 0, false);
 }
 
 /**
@@ -102,7 +139,7 @@ got_bad_version(int client_major, int client_minor,
 
   std::string message = str.str();
   GtkWidget *dialog =
-    gtk_message_dialog_new(GTK_WINDOW(main_window),
+    gtk_message_dialog_new(GTK_WINDOW(_window),
                            GTK_DIALOG_DESTROY_WITH_PARENT,
                            GTK_MESSAGE_ERROR,
                            GTK_BUTTONS_CLOSE,
@@ -120,16 +157,13 @@ got_bad_version(int client_major, int client_minor,
  */
 void GtkStatsMonitor::
 new_collector(int collector_index) {
-  Graphs::iterator gi;
-  for (gi = _graphs.begin(); gi != _graphs.end(); ++gi) {
-    GtkStatsGraph *graph = (*gi);
+  for (GtkStatsGraph *graph : _graphs) {
     graph->new_collector(collector_index);
   }
 
   // We might need to update our menus.
-  ChartMenus::iterator mi;
-  for (mi = _chart_menus.begin(); mi != _chart_menus.end(); ++mi) {
-    (*mi)->do_update();
+  for (GtkStatsChartMenu *chart_menu : _chart_menus) {
+    chart_menu->do_update();
   }
 }
 
@@ -163,6 +197,14 @@ new_data(int thread_index, int frame_number) {
   if (thread_index == 0) {
     update_status_bar();
   }
+
+  if (!_have_data) {
+    open_default_graphs();
+    _have_data = true;
+
+    // Flash the window.
+    gtk_window_set_urgency_hint(GTK_WINDOW(_window), TRUE);
+  }
 }
 
 /**
@@ -173,8 +215,6 @@ new_data(int thread_index, int frame_number) {
 void GtkStatsMonitor::
 lost_connection() {
   nout << "Lost connection to " << get_client_hostname() << "\n";
-
-  shutdown();
 }
 
 /**
@@ -184,9 +224,8 @@ lost_connection() {
 void GtkStatsMonitor::
 idle() {
   // Check if any of our chart menus need updating.
-  ChartMenus::iterator mi;
-  for (mi = _chart_menus.begin(); mi != _chart_menus.end(); ++mi) {
-    (*mi)->check_update();
+  for (GtkStatsChartMenu *chart_menu : _chart_menus) {
+    chart_menu->check_update();
   }
 
   // Update the frame rate label from the main thread (thread 0).
@@ -218,9 +257,7 @@ has_idle() {
  */
 void GtkStatsMonitor::
 user_guide_bars_changed() {
-  Graphs::iterator gi;
-  for (gi = _graphs.begin(); gi != _graphs.end(); ++gi) {
-    GtkStatsGraph *graph = (*gi);
+  for (GtkStatsGraph *graph : _graphs) {
     graph->user_guide_bars_changed();
   }
 }
@@ -241,57 +278,91 @@ get_resolution() const {
   return _resolution;
 }
 
+/**
+ * Opens a new timeline.
+ */
+PStatGraph *GtkStatsMonitor::
+open_timeline() {
+  GtkStatsTimeline *graph = new GtkStatsTimeline(this);
+  add_graph(graph);
+  return graph;
+}
+
+/**
+ * Opens a new flame graph showing the indicated data.
+ */
+PStatGraph *GtkStatsMonitor::
+open_flame_graph(int thread_index, int collector_index) {
+  GtkStatsFlameGraph *graph = new GtkStatsFlameGraph(this, thread_index, collector_index);
+  add_graph(graph);
+  return graph;
+}
+
 /**
  * Opens a new strip chart showing the indicated data.
  */
-void GtkStatsMonitor::
+PStatGraph *GtkStatsMonitor::
 open_strip_chart(int thread_index, int collector_index, bool show_level) {
   GtkStatsStripChart *graph =
     new GtkStatsStripChart(this, thread_index, collector_index, show_level);
   add_graph(graph);
-
-  graph->set_time_units(_time_units);
-  graph->set_scroll_speed(_scroll_speed);
-  graph->set_pause(_pause);
+  return graph;
 }
 
 /**
  * Opens a new piano roll showing the indicated data.
  */
-void GtkStatsMonitor::
+PStatGraph *GtkStatsMonitor::
 open_piano_roll(int thread_index) {
   GtkStatsPianoRoll *graph = new GtkStatsPianoRoll(this, thread_index);
   add_graph(graph);
-
-  graph->set_time_units(_time_units);
-  graph->set_scroll_speed(_scroll_speed);
-  graph->set_pause(_pause);
+  return graph;
 }
 
 /**
- * Opens a new flame graph showing the indicated data.
+ * Opens a dialog to change the given collector color.
  */
 void GtkStatsMonitor::
-open_flame_graph(int thread_index, int collector_index) {
-  GtkStatsFlameGraph *graph = new GtkStatsFlameGraph(this, thread_index, collector_index);
-  add_graph(graph);
+choose_collector_color(int collector_index) {
+  const LRGBColor &current = get_collector_color(collector_index);
+
+  GtkWidget *chooser = gtk_color_chooser_dialog_new(nullptr, GTK_WINDOW(_window));
+  gtk_color_chooser_set_use_alpha(GTK_COLOR_CHOOSER(chooser), FALSE);
+
+  GdkRGBA rgba;
+  rgba.red = encode_sRGB_float((float)current[0]);
+  rgba.green = encode_sRGB_float((float)current[1]);
+  rgba.blue = encode_sRGB_float((float)current[2]);
+  rgba.alpha = 1.0;
+  gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(chooser), &rgba);
+
+  if (gtk_dialog_run(GTK_DIALOG(chooser)) == GTK_RESPONSE_OK) {
+    gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(chooser), &rgba);
+    LRGBColor result(
+      decode_sRGB_float((float)rgba.red),
+      decode_sRGB_float((float)rgba.green),
+      decode_sRGB_float((float)rgba.blue));
+
+    set_collector_color(collector_index, result);
+
+    for (GtkStatsGraph *graph : _graphs) {
+      graph->reset_collector_color(collector_index);
+    }
+  }
 
-  graph->set_time_units(_time_units);
-  graph->set_scroll_speed(_scroll_speed);
-  graph->set_pause(_pause);
+  gtk_widget_destroy(chooser);
 }
 
 /**
- * Opens a new timeline.
+ * Resets the color of the given collector to the default.
  */
 void GtkStatsMonitor::
-open_timeline() {
-  GtkStatsTimeline *graph = new GtkStatsTimeline(this);
-  add_graph(graph);
+reset_collector_color(int collector_index) {
+  clear_collector_color(collector_index);
 
-  graph->set_time_units(_time_units);
-  graph->set_scroll_speed(_scroll_speed);
-  graph->set_pause(_pause);
+  for (GtkStatsGraph *graph : _graphs) {
+    graph->reset_collector_color(collector_index);
+  }
 }
 
 /**
@@ -317,13 +388,8 @@ add_menu(const MenuDef &menu_def) {
  */
 void GtkStatsMonitor::
 set_time_units(int unit_mask) {
-  _time_units = unit_mask;
-
-  // First, change all of the open graphs appropriately.
-  Graphs::iterator gi;
-  for (gi = _graphs.begin(); gi != _graphs.end(); ++gi) {
-    GtkStatsGraph *graph = (*gi);
-    graph->set_time_units(_time_units);
+  for (GtkStatsGraph *graph : _graphs) {
+    graph->set_time_units(unit_mask);
   }
 }
 
@@ -336,9 +402,7 @@ set_scroll_speed(double scroll_speed) {
   _scroll_speed = scroll_speed;
 
   // First, change all of the open graphs appropriately.
-  Graphs::iterator gi;
-  for (gi = _graphs.begin(); gi != _graphs.end(); ++gi) {
-    GtkStatsGraph *graph = (*gi);
+  for (GtkStatsGraph *graph : _graphs) {
     graph->set_scroll_speed(_scroll_speed);
   }
 }
@@ -351,9 +415,7 @@ set_pause(bool pause) {
   _pause = pause;
 
   // First, change all of the open graphs appropriately.
-  Graphs::iterator gi;
-  for (gi = _graphs.begin(); gi != _graphs.end(); ++gi) {
-    GtkStatsGraph *graph = (*gi);
+  for (GtkStatsGraph *graph : _graphs) {
     graph->set_pause(_pause);
   }
 }
@@ -364,6 +426,10 @@ set_pause(bool pause) {
 void GtkStatsMonitor::
 add_graph(GtkStatsGraph *graph) {
   _graphs.insert(graph);
+
+  graph->set_time_units(((GtkStatsServer *)_server)->get_time_units());
+  graph->set_scroll_speed(_scroll_speed);
+  graph->set_pause(_pause);
 }
 
 /**
@@ -379,145 +445,14 @@ remove_graph(GtkStatsGraph *graph) {
 }
 
 /**
- * Creates the window for this monitor.
+ * Deletes all open graphs.
  */
 void GtkStatsMonitor::
-create_window() {
-  if (_window != nullptr) {
-    return;
-  }
-
-  _window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
-
-  g_signal_connect(G_OBJECT(_window), "delete_event",
-       G_CALLBACK(window_delete_event), this);
-  g_signal_connect(G_OBJECT(_window), "destroy",
-       G_CALLBACK(window_destroy), this);
-
-  _window_title = get_client_progname() + " on " + get_client_hostname();
-  gtk_window_set_title(GTK_WINDOW(_window), _window_title.c_str());
-
-  gtk_window_set_default_size(GTK_WINDOW(_window), 500, 360);
-
-  // Set up the menu.
-  GtkAccelGroup *accel_group = gtk_accel_group_new();
-  gtk_window_add_accel_group(GTK_WINDOW(_window), accel_group);
-  _menu_bar = gtk_menu_bar_new();
-  _next_chart_index = 2;
-
-  setup_options_menu();
-  setup_speed_menu();
-  setup_frame_rate_label();
-
-  for (GtkStatsChartMenu *chart_menu : _chart_menus) {
-    chart_menu->add_to_menu_bar(_menu_bar, _next_chart_index);
-    ++_next_chart_index;
-  }
-
-  // Pack the menu into the window.
-  GtkWidget *main_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 1);
-  gtk_container_add(GTK_CONTAINER(_window), main_vbox);
-  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(_window);
-  gtk_widget_realize(_window);
-}
-
-/**
- * Closes all the graphs associated with this monitor.
- */
-void GtkStatsMonitor::
-shutdown() {
-  Graphs::iterator gi;
-  for (gi = _graphs.begin(); gi != _graphs.end(); ++gi) {
-    delete (*gi);
+remove_all_graphs() {
+  for (GtkStatsGraph *graph : _graphs) {
+    delete graph;
   }
   _graphs.clear();
-
-  ChartMenus::iterator mi;
-  for (mi = _chart_menus.begin(); mi != _chart_menus.end(); ++mi) {
-    delete (*mi);
-  }
-  _chart_menus.clear();
-
-  if (_window != nullptr) {
-    gtk_widget_destroy(_window);
-    _window = nullptr;
-  }
-
-#ifdef DEVELOP_GTKSTATS
-  // For GtkStats developers, exit when the first monitor closes.
-  gtk_main_quit();
-#endif
-}
-
-/**
- * Callback when the window is closed by the user.
- */
-gboolean GtkStatsMonitor::
-window_delete_event(GtkWidget *widget, GdkEvent *event, gpointer data) {
-  // Returning FALSE to indicate we should destroy the window when the user
-  // selects "close".
-  return FALSE;
-}
-
-/**
- * Callback when the window is destroyed by the system (or by delete_event).
- */
-void GtkStatsMonitor::
-window_destroy(GtkWidget *widget, gpointer data) {
-  GtkStatsMonitor *self = (GtkStatsMonitor *)data;
-  self->close();
-}
-
-
-/**
- * Creates the "Options" pulldown menu.
- */
-void GtkStatsMonitor::
-setup_options_menu() {
-  _options_menu = gtk_menu_new();
-
-  GtkWidget *item = gtk_menu_item_new_with_label("Options");
-  gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), _options_menu);
-  gtk_menu_shell_append(GTK_MENU_SHELL(_menu_bar), item);
-
-  GtkWidget *units_menu = gtk_menu_new();
-  item = gtk_menu_item_new_with_label("Units");
-  gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), units_menu);
-  gtk_menu_shell_append(GTK_MENU_SHELL(_options_menu), item);
-
-  item = gtk_radio_menu_item_new_with_label(nullptr, "ms");
-  gtk_menu_shell_append(GTK_MENU_SHELL(units_menu), item);
-  g_signal_connect(G_OBJECT(item), "activate",
-    G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
-      GtkStatsMonitor *self = (GtkStatsMonitor *)data;
-      self->set_time_units(PStatGraph::GBU_ms);
-    }), this);
-
-  item = gtk_radio_menu_item_new_with_label(
-    gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(item)), "Hz");
-  gtk_menu_shell_append(GTK_MENU_SHELL(units_menu), item);
-  g_signal_connect(G_OBJECT(item), "activate",
-    G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
-      GtkStatsMonitor *self = (GtkStatsMonitor *)data;
-      self->set_time_units(PStatGraph::GBU_hz);
-    }), this);
-
-  set_time_units(PStatGraph::GBU_ms);
 }
 
 /**
@@ -525,15 +460,16 @@ setup_options_menu() {
  */
 void GtkStatsMonitor::
 setup_speed_menu() {
-  _speed_menu = gtk_menu_new();
+  GtkWidget *menu = gtk_menu_new();
 
-  GtkWidget *item = gtk_menu_item_new_with_label("Speed");
-  gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), _speed_menu);
-  gtk_menu_shell_append(GTK_MENU_SHELL(_menu_bar), item);
+  _speed_menu_item = gtk_menu_item_new_with_label("Speed");
+  gtk_menu_item_set_submenu(GTK_MENU_ITEM(_speed_menu_item), menu);
+  gtk_menu_shell_append(GTK_MENU_SHELL(_menu_bar), _speed_menu_item);
 
   GSList *group = nullptr;
+  GtkWidget *item;
   item = gtk_radio_menu_item_new_with_label(group, "1");
-  gtk_menu_shell_append(GTK_MENU_SHELL(_speed_menu), item);
+  gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
   g_signal_connect(G_OBJECT(item), "toggled",
     G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
       if (gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(item))) {
@@ -544,7 +480,7 @@ setup_speed_menu() {
   group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(item));
 
   item = gtk_radio_menu_item_new_with_label(group, "2");
-  gtk_menu_shell_append(GTK_MENU_SHELL(_speed_menu), item);
+  gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
   g_signal_connect(G_OBJECT(item), "toggled",
     G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
       if (gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(item))) {
@@ -556,7 +492,7 @@ setup_speed_menu() {
 
   item = gtk_radio_menu_item_new_with_label(group, "3");
   gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), TRUE);
-  gtk_menu_shell_append(GTK_MENU_SHELL(_speed_menu), item);
+  gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
   g_signal_connect(G_OBJECT(item), "toggled",
     G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
       if (gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(item))) {
@@ -567,7 +503,7 @@ setup_speed_menu() {
   group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(item));
 
   item = gtk_radio_menu_item_new_with_label(group, "6");
-  gtk_menu_shell_append(GTK_MENU_SHELL(_speed_menu), item);
+  gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
   g_signal_connect(G_OBJECT(item), "toggled",
     G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
       if (gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(item))) {
@@ -578,7 +514,7 @@ setup_speed_menu() {
   group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(item));
 
   item = gtk_radio_menu_item_new_with_label(group, "12");
-  gtk_menu_shell_append(GTK_MENU_SHELL(_speed_menu), item);
+  gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
   g_signal_connect(G_OBJECT(item), "toggled",
     G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
       if (gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(item))) {
@@ -589,10 +525,10 @@ setup_speed_menu() {
   group = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(item));
 
   item = gtk_separator_menu_item_new();
-  gtk_menu_shell_append(GTK_MENU_SHELL(_speed_menu), item);
+  gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
 
   item = gtk_check_menu_item_new_with_label("pause");
-  gtk_menu_shell_append(GTK_MENU_SHELL(_speed_menu), item);
+  gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
   g_signal_connect(G_OBJECT(item), "toggled",
     G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
       GtkStatsMonitor *self = (GtkStatsMonitor *)data;
@@ -601,6 +537,9 @@ setup_speed_menu() {
 
   set_scroll_speed(3);
   set_pause(false);
+
+  gtk_widget_show_all(_speed_menu_item);
+  ++_next_chart_index;
 }
 
 /**
@@ -715,7 +654,7 @@ status_bar_button_event(GtkWidget *widget, GdkEventButton *event, gpointer data)
   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()) {
+  if (index < 0 || (size_t)index >= monitor->_status_bar_labels.size()) {
     return FALSE;
   }
 
@@ -790,31 +729,114 @@ status_bar_button_event(GtkWidget *widget, GdkEventButton *event, gpointer data)
  */
 void GtkStatsMonitor::
 menu_activate(GtkWidget *widget, gpointer data) {
-  const MenuDef *menu_def = (const MenuDef *)data;
-  GtkStatsMonitor *monitor = menu_def->_monitor;
+  const MenuDef &menu_def = *(const MenuDef *)data;
+  GtkStatsMonitor *monitor = menu_def._monitor;
 
   if (monitor == nullptr) {
     return;
   }
 
-  switch (menu_def->_chart_type) {
+  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);
+    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);
+    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);
+    monitor->open_piano_roll(menu_def._thread_index);
+    break;
+
+  case CT_choose_color:
+    monitor->choose_collector_color(menu_def._collector_index);
+    break;
+
+  case CT_reset_color:
+    monitor->reset_collector_color(menu_def._collector_index);
     break;
   }
 }
+
+/**
+ * Called when a status bar item is double-clicked.
+ */
+void GtkStatsMonitor::
+handle_status_bar_click(int item) {
+  if (item == 0) {
+    open_strip_chart(0, 0, false);
+  }
+  else if (item >= 1 && (size_t)item < _status_bar_collectors.size()) {
+    int collector = _status_bar_collectors[item];
+    open_strip_chart(0, collector, true);
+
+    // Also open a strip chart for other threads with data for this
+    // collector.
+    const PStatClientData *client_data = get_client_data();
+    for (int thread_index = 1; thread_index < client_data->get_num_threads(); ++thread_index) {
+      PStatView &view = get_level_view(collector, thread_index);
+      if (view.get_net_value() > 0.0) {
+        open_strip_chart(thread_index, collector, true);
+      }
+    }
+  }
+}
+
+/**
+ * Called when a status bar item is right-clicked.
+ */
+void GtkStatsMonitor::
+handle_status_bar_popup(int item) {
+  if (item >= 0 && (size_t)item < _status_bar_collectors.size()) {
+    int collector = _status_bar_collectors[item];
+
+    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;
+    }
+
+    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.
+    const PStatClientData *client_data = get_client_data();
+    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 = 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 = _status_bar_labels[item];
+    gtk_menu_popup_at_widget(GTK_MENU(menu), label,
+                             GDK_GRAVITY_NORTH_WEST,
+                             GDK_GRAVITY_SOUTH_WEST, nullptr);
+
+  }
+}

+ 20 - 16
pandatool/src/gtk-stats/gtkStatsMonitor.h

@@ -39,6 +39,9 @@ public:
     CT_strip_chart,
     CT_flame_graph,
     CT_piano_roll,
+
+    CT_choose_color,
+    CT_reset_color,
   };
 
   class MenuDef {
@@ -57,6 +60,8 @@ public:
   GtkStatsMonitor(GtkStatsServer *server);
   virtual ~GtkStatsMonitor();
 
+  void close();
+
   virtual std::string get_monitor_name();
 
   virtual void initialized();
@@ -75,10 +80,13 @@ public:
   GtkWidget *get_window() const;
   double get_resolution() const;
 
-  void open_strip_chart(int thread_index, int collector_index, bool show_level);
-  void open_piano_roll(int thread_index);
-  void open_flame_graph(int thread_index, int collector_index = -1);
-  void open_timeline();
+  PStatGraph *open_timeline();
+  PStatGraph *open_strip_chart(int thread_index, int collector_index, bool show_level);
+  PStatGraph *open_flame_graph(int thread_index, int collector_index = -1);
+  PStatGraph *open_piano_roll(int thread_index);
+
+  void choose_collector_color(int collector_index);
+  void reset_collector_color(int collector_index);
 
   const MenuDef *add_menu(const MenuDef &menu_def);
 
@@ -86,26 +94,22 @@ public:
   void set_scroll_speed(double scroll_speed);
   void set_pause(bool pause);
 
-private:
   void add_graph(GtkStatsGraph *graph);
   void remove_graph(GtkStatsGraph *graph);
+  void remove_all_graphs();
 
-  void create_window();
-  void shutdown();
-  static gboolean window_delete_event(GtkWidget *widget, GdkEvent *event,
-              gpointer data);
-  static void window_destroy(GtkWidget *widget, gpointer data);
-  void setup_options_menu();
+private:
   void setup_speed_menu();
   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);
+  void handle_status_bar_click(int item);
+  void handle_status_bar_popup(int item);
 
 private:
   typedef pset<GtkStatsGraph *> Graphs;
@@ -119,21 +123,21 @@ private:
 
   GtkWidget *_window;
   GtkWidget *_menu_bar;
-  GtkWidget *_options_menu;
-  GtkWidget *_speed_menu;
+  GtkWidget *_speed_menu_item = nullptr;
   int _next_chart_index;
-  GtkWidget *_frame_rate_menu_item;
+  GtkWidget *_frame_rate_menu_item = nullptr;
   GtkWidget *_frame_rate_label;
   GtkWidget *_status_bar;
   pvector<int> _status_bar_collectors;
   pvector<GtkWidget *> _status_bar_labels;
   std::string _window_title;
-  int _time_units;
   double _scroll_speed;
   bool _pause;
+  bool _have_data = false;
   double _resolution;
 
   friend class GtkStatsGraph;
+  friend class GtkStatsServer;
 };
 
 #include "gtkStatsMonitor.I"

+ 50 - 1
pandatool/src/gtk-stats/gtkStatsPianoRoll.cxx

@@ -64,7 +64,8 @@ GtkStatsPianoRoll(GtkStatsMonitor *monitor, int thread_index) :
   // window's initial size.
   gtk_widget_set_size_request(_window, 0, 0);
 
-  clear_region();
+  force_redraw();
+  idle();
 }
 
 /**
@@ -170,6 +171,35 @@ on_popup_label(int collector_index) {
                      (void *)menu_def);
   }
 
+  {
+    GtkWidget *menu_item = gtk_separator_menu_item_new();
+    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+  }
+
+  {
+    const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({
+      -1, collector_index, GtkStatsMonitor::CT_choose_color,
+    });
+
+    GtkWidget *menu_item = gtk_menu_item_new_with_label("Change Color...");
+    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({
+      -1, collector_index, GtkStatsMonitor::CT_reset_color,
+    });
+
+    GtkWidget *menu_item = gtk_menu_item_new_with_label("Reset Color");
+    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);
 }
@@ -263,6 +293,25 @@ idle() {
   }
 }
 
+/**
+ * Returns the current window dimensions.
+ */
+bool GtkStatsPianoRoll::
+get_window_state(int &x, int &y, int &width, int &height,
+                 bool &maximized, bool &minimized) const {
+  GtkStatsGraph::get_window_state(x, y, width, height, maximized, minimized);
+  return true;
+}
+
+/**
+ * Called to restore the graph window to its previous dimensions.
+ */
+void GtkStatsPianoRoll::
+set_window_state(int x, int y, int width, int height,
+                 bool maximized, bool minimized) {
+  GtkStatsGraph::set_window_state(x, y, width, height, maximized, minimized);
+}
+
 /**
  * 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.

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

@@ -51,6 +51,11 @@ protected:
   virtual void end_draw();
   virtual void idle();
 
+  virtual bool get_window_state(int &x, int &y, int &width, int &height,
+                                bool &maximized, bool &minimized) const;
+  virtual void set_window_state(int x, int y, int width, int height,
+                                bool maximized, bool minimized);
+
   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);

+ 676 - 2
pandatool/src/gtk-stats/gtkStatsServer.cxx

@@ -13,11 +13,685 @@
 
 #include "gtkStatsServer.h"
 #include "gtkStatsMonitor.h"
+#include "pandaVersion.h"
+#include "pStatGraph.h"
+#include "config_pstatclient.h"
+
+/**
+ *
+ */
+GtkStatsServer::
+GtkStatsServer() {
+#ifdef __APPLE__
+  _last_session = Filename::expand_from(
+    "$HOME/Library/Caches/Panda3D-" PANDA_ABI_VERSION_STR "/last-session.pstats");
+#else
+  _last_session = Filename::expand_from("$XDG_CACHE_HOME/panda3d/last-session.pstats");
+#endif
+  _last_session.set_binary();
+
+  _window = nullptr;
+  _menu_bar = nullptr;
+  _options_menu = nullptr;
+
+  _time_units = 0;
+
+  create_window();
+}
 
 /**
  *
  */
 PStatMonitor *GtkStatsServer::
-make_monitor() {
-  return new GtkStatsMonitor(this);
+make_monitor(const NetAddress &address) {
+  // Enable the "New Session", "Save Session" and "Close Session" menu items.
+  gtk_widget_set_sensitive(_new_session_menu_item, TRUE);
+  gtk_widget_set_sensitive(_save_session_menu_item, TRUE);
+  gtk_widget_set_sensitive(_close_session_menu_item, TRUE);
+  gtk_widget_set_sensitive(_export_session_menu_item, TRUE);
+
+  std::ostringstream strm;
+  strm << "PStats Server (connected to " << address << ")";
+  std::string title = strm.str();
+  gtk_window_set_title(GTK_WINDOW(_window), title.c_str());
+
+  if (_status_bar_label != nullptr) {
+    gtk_container_remove(GTK_CONTAINER(_status_bar), _status_bar_label);
+    _status_bar_label = nullptr;
+  }
+
+  _monitor = new GtkStatsMonitor(this);
+  return _monitor;
+}
+
+/**
+ * Called when connection has been lost.
+ */
+void GtkStatsServer::
+lost_connection(PStatMonitor *monitor) {
+  // Store a backup now, in case PStats crashes or something.
+  _last_session.make_dir();
+  if (monitor->write(_last_session)) {
+    nout << "Wrote to " << _last_session << "\n";
+  } else {
+    nout << "Failed to write to " << _last_session << "\n";
+  }
+
+  stop_listening();
+
+  gtk_window_set_title(GTK_WINDOW(_window), "PStats Server (disconnected)");
+}
+
+/**
+ * Starts a new session.
+ */
+bool GtkStatsServer::
+new_session() {
+  if (!close_session()) {
+    return false;
+  }
+
+  if (listen()) {
+    {
+      std::ostringstream strm;
+      strm << "PStats Server (listening on port " << pstats_port << ")";
+      std::string title = strm.str();
+      gtk_window_set_title(GTK_WINDOW(_window), title.c_str());
+    }
+    {
+      std::ostringstream strm;
+      strm << "Waiting for client to connect on port " << pstats_port << "...";
+      std::string title = strm.str();
+      _status_bar_label = gtk_label_new(title.c_str());
+      gtk_container_add(GTK_CONTAINER(_status_bar), _status_bar_label);
+      gtk_widget_show(_status_bar_label);
+    }
+
+    gtk_widget_set_sensitive(_new_session_menu_item, FALSE);
+    gtk_widget_set_sensitive(_save_session_menu_item, FALSE);
+    gtk_widget_set_sensitive(_close_session_menu_item, TRUE);
+    gtk_widget_set_sensitive(_export_session_menu_item, FALSE);
+
+    return true;
+  }
+
+  gtk_window_set_title(GTK_WINDOW(_window), "PStats Server");
+
+  GtkWidget *dialog =
+    gtk_message_dialog_new(GTK_WINDOW(_window),
+       GTK_DIALOG_DESTROY_WITH_PARENT,
+       GTK_MESSAGE_ERROR,
+       GTK_BUTTONS_CLOSE,
+       "Unable to open port %d.  Try specifying a different port number "
+       "using pstats-port in your Config file.", pstats_port.get_value());
+  gtk_dialog_run(GTK_DIALOG(dialog));
+  gtk_widget_destroy(dialog);
+
+  return false;
+}
+
+/**
+ * Offers to open an existing session.
+ */
+bool GtkStatsServer::
+open_session() {
+  if (!close_session()) {
+    return false;
+  }
+
+  GtkFileChooserNative *native = gtk_file_chooser_native_new(
+    "Open Session",
+    GTK_WINDOW(_window),
+    GTK_FILE_CHOOSER_ACTION_SAVE,
+    "_Open", "Cancel");
+  GtkFileChooser *chooser = GTK_FILE_CHOOSER(native);
+
+  GtkFileFilter *filter = gtk_file_filter_new();
+  gtk_file_filter_set_name(filter, "PStats Session Files");
+  gtk_file_filter_add_pattern(filter, "*.pstats");
+  gtk_file_chooser_add_filter(chooser, filter);
+
+  gint res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native));
+  if (res == GTK_RESPONSE_ACCEPT) {
+    char *buffer = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(chooser));
+    Filename fn = Filename::from_os_specific(buffer);
+    fn.set_binary();
+    g_free(buffer);
+    g_object_unref(native);
+
+    GtkStatsMonitor *monitor = new GtkStatsMonitor(this);
+    if (!monitor->read(fn)) {
+      delete monitor;
+
+      GtkWidget *dialog =
+        gtk_message_dialog_new(GTK_WINDOW(_window),
+           GTK_DIALOG_DESTROY_WITH_PARENT,
+           GTK_MESSAGE_ERROR,
+           GTK_BUTTONS_CLOSE,
+           "Failed to load session file: %s", fn.c_str());
+      gtk_dialog_run(GTK_DIALOG(dialog));
+      gtk_widget_destroy(dialog);
+      return false;
+    }
+    _save_filename = fn;
+
+    gtk_widget_set_sensitive(_new_session_menu_item, TRUE);
+    gtk_widget_set_sensitive(_save_session_menu_item, TRUE);
+    gtk_widget_set_sensitive(_close_session_menu_item, TRUE);
+    gtk_widget_set_sensitive(_export_session_menu_item, TRUE);
+
+    _monitor = monitor;
+    return true;
+  }
+
+  g_object_unref(native);
+  return false;
+}
+
+/**
+ * Opens the last session, if any.
+ */
+bool GtkStatsServer::
+open_last_session() {
+  if (!close_session()) {
+    return false;
+  }
+
+  Filename fn = _last_session;
+  GtkStatsMonitor *monitor = new GtkStatsMonitor(this);
+  if (!monitor->read(fn)) {
+    delete monitor;
+
+    GtkWidget *dialog =
+      gtk_message_dialog_new(GTK_WINDOW(_window),
+         GTK_DIALOG_DESTROY_WITH_PARENT,
+         GTK_MESSAGE_ERROR,
+         GTK_BUTTONS_CLOSE,
+         "Failed to load session file: %s", fn.c_str());
+    gtk_dialog_run(GTK_DIALOG(dialog));
+    gtk_widget_destroy(dialog);
+
+    return false;
+  }
+  _monitor = monitor;
+
+  // Enable the "New Session", "Save Session" and "Close Session" menu items.
+  gtk_widget_set_sensitive(_new_session_menu_item, TRUE);
+  gtk_widget_set_sensitive(_save_session_menu_item, TRUE);
+  gtk_widget_set_sensitive(_close_session_menu_item, TRUE);
+  gtk_widget_set_sensitive(_export_session_menu_item, TRUE);
+
+  // If the file contained no graphs, open the default graphs.
+  if (monitor->_graphs.empty()) {
+    monitor->open_default_graphs();
+  }
+
+  return true;
+}
+
+/**
+ * Offers to save the current session.
+ */
+bool GtkStatsServer::
+save_session() {
+  nassertr_always(_monitor != nullptr, true);
+
+  GtkFileChooserNative *native = gtk_file_chooser_native_new(
+    "Save Session",
+    GTK_WINDOW(_window),
+    GTK_FILE_CHOOSER_ACTION_SAVE,
+    "_Save", "Cancel");
+  GtkFileChooser *chooser = GTK_FILE_CHOOSER(native);
+
+  gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE);
+
+  GtkFileFilter *filter = gtk_file_filter_new();
+  gtk_file_filter_set_name(filter, "PStats Session Files");
+  gtk_file_filter_add_pattern(filter, "*.pstats");
+  gtk_file_chooser_add_filter(chooser, filter);
+
+  if (_save_filename.empty()) {
+    gtk_file_chooser_set_current_name(chooser, "session.pstats");
+  }
+  else {
+    gtk_file_chooser_set_filename(chooser, _save_filename.c_str());
+  }
+
+  gint res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native));
+  if (res == GTK_RESPONSE_ACCEPT) {
+    char *buffer = gtk_file_chooser_get_filename(chooser);
+    Filename fn = Filename::from_os_specific(buffer);
+    fn.set_binary();
+    g_free(buffer);
+    g_object_unref(native);
+
+    if (!_monitor->write(fn)) {
+      GtkWidget *dialog =
+        gtk_message_dialog_new(GTK_WINDOW(_window),
+           GTK_DIALOG_DESTROY_WITH_PARENT,
+           GTK_MESSAGE_ERROR,
+           GTK_BUTTONS_CLOSE,
+           "Failed to save session file: %s", fn.c_str());
+      gtk_dialog_run(GTK_DIALOG(dialog));
+      gtk_widget_destroy(dialog);
+      return false;
+    }
+    _save_filename = fn;
+    _monitor->get_client_data()->clear_dirty();
+    return true;
+  }
+
+  g_object_unref(native);
+  return false;
+}
+
+/**
+ * Offers to export the current session as a JSON file.
+ */
+bool GtkStatsServer::
+export_session() {
+  nassertr_always(_monitor != nullptr, true);
+
+  GtkFileChooserNative *native = gtk_file_chooser_native_new(
+    "Export Session",
+    GTK_WINDOW(_window),
+    GTK_FILE_CHOOSER_ACTION_SAVE,
+    "_Export", "Cancel");
+  GtkFileChooser *chooser = GTK_FILE_CHOOSER(native);
+
+  gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE);
+
+  GtkFileFilter *filter = gtk_file_filter_new();
+  gtk_file_filter_set_name(filter, "JSON files");
+  gtk_file_filter_add_pattern(filter, "*.json");
+  gtk_file_chooser_add_filter(chooser, filter);
+
+  gtk_file_chooser_set_current_name(chooser, "session.json");
+
+  gint res = gtk_native_dialog_run(GTK_NATIVE_DIALOG(native));
+  if (res == GTK_RESPONSE_ACCEPT) {
+    char *buffer = gtk_file_chooser_get_filename(chooser);
+    Filename fn = Filename::from_os_specific(buffer);
+    fn.set_text();
+    g_free(buffer);
+    g_object_unref(native);
+
+    std::ofstream stream;
+    if (!fn.open_write(stream)) {
+      GtkWidget *dialog =
+        gtk_message_dialog_new(GTK_WINDOW(_window),
+           GTK_DIALOG_DESTROY_WITH_PARENT,
+           GTK_MESSAGE_ERROR,
+           GTK_BUTTONS_CLOSE,
+           "Failed to open file for export: %s", fn.c_str());
+      gtk_dialog_run(GTK_DIALOG(dialog));
+      gtk_widget_destroy(dialog);
+      return false;
+    }
+
+    int pid = _monitor->get_client_pid();
+    _monitor->get_client_data()->write_json(stream, std::max(0, pid));
+    stream.close();
+    return true;
+  }
+
+  g_object_unref(native);
+  return false;
+}
+
+/**
+ * Closes the current session.
+ */
+bool GtkStatsServer::
+close_session() {
+  bool wrote_last_session = false;
+
+  if (_monitor != nullptr) {
+    const PStatClientData *client_data = _monitor->get_client_data();
+    if (client_data != nullptr && client_data->is_dirty()) {
+      if (!_monitor->has_read_filename()) {
+        _last_session.make_dir();
+        if (_monitor->write(_last_session)) {
+          nout << "Wrote to " << _last_session << "\n";
+          wrote_last_session = true;
+        }
+        else {
+          nout << "Failed to write to " << _last_session << "\n";
+        }
+      }
+
+      GtkWidget *dialog =
+        gtk_message_dialog_new(GTK_WINDOW(_window),
+           GTK_DIALOG_DESTROY_WITH_PARENT,
+           GTK_MESSAGE_QUESTION,
+           GTK_BUTTONS_YES_NO,
+           "Would you like to save the currently open session?");
+      gint response = gtk_dialog_run(GTK_DIALOG(dialog));
+      gtk_widget_destroy(dialog);
+
+      if (response == GTK_RESPONSE_CANCEL ||
+          (response == GTK_RESPONSE_YES && !save_session())) {
+        return false;
+      }
+    }
+
+    _monitor->close();
+    _monitor = nullptr;
+  }
+
+  _save_filename = Filename();
+  stop_listening();
+
+  gtk_window_set_title(GTK_WINDOW(_window), "PStats Server");
+
+  if (_status_bar_label != nullptr) {
+    gtk_container_remove(GTK_CONTAINER(_status_bar), _status_bar_label);
+    _status_bar_label = nullptr;
+  }
+
+  gtk_widget_set_sensitive(_new_session_menu_item, TRUE);
+  if (wrote_last_session) {
+    gtk_widget_set_sensitive(_open_last_session_menu_item, TRUE);
+  }
+  gtk_widget_set_sensitive(_save_session_menu_item, FALSE);
+  gtk_widget_set_sensitive(_close_session_menu_item, FALSE);
+  gtk_widget_set_sensitive(_export_session_menu_item, FALSE);
+  return true;
+}
+
+
+/**
+ * Returns the window handle to the server's window.
+ */
+GtkWidget *GtkStatsServer::
+get_window() const {
+  return _window;
+}
+
+/**
+ * Returns the menu handle to the server's menu bar.
+ */
+GtkWidget *GtkStatsServer::
+get_menu_bar() const {
+  return _menu_bar;
+}
+
+/**
+ * Returns the window handle to the server's status bar.
+ */
+GtkWidget *GtkStatsServer::
+get_status_bar() const {
+  return _status_bar;
+}
+
+/**
+ *
+ */
+int GtkStatsServer::
+get_time_units() const {
+  return _time_units;
+}
+
+/**
+ * Called when the user selects a new time units from the monitor pulldown
+ * menu, this should adjust the units for all graphs to the indicated mask if
+ * it is a time-based graph.
+ */
+void GtkStatsServer::
+set_time_units(int unit_mask) {
+  _time_units = unit_mask;
+
+  if (_monitor != nullptr) {
+    _monitor->set_time_units(unit_mask);
+  }
+}
+
+/**
+ * Creates the window for this monitor.
+ */
+void GtkStatsServer::
+create_window() {
+  _window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+  gtk_window_set_title(GTK_WINDOW(_window), "PStats Server");
+  gtk_window_set_default_size(GTK_WINDOW(_window), 500, 360);
+
+  // Connect the delete and destroy events, so the user can exit the
+  // application by closing the main window.
+  g_signal_connect(G_OBJECT(_window), "delete_event",
+    G_CALLBACK(+[](GtkWidget *widget, GdkEvent *event, gpointer data) -> gboolean {
+      GtkStatsServer *self = (GtkStatsServer *)data;
+      return self->close_session() ? FALSE : TRUE;
+    }), this);
+
+  g_signal_connect(G_OBJECT(_window), "destroy",
+    G_CALLBACK(+[](GtkWidget *widget, GdkEvent *event, gpointer data) -> gboolean {
+      gtk_main_quit();
+      return FALSE;
+    }), this);
+
+  // Set up the menu.
+  GtkAccelGroup *accel_group = gtk_accel_group_new();
+  gtk_window_add_accel_group(GTK_WINDOW(_window), accel_group);
+  _menu_bar = gtk_menu_bar_new();
+
+  setup_session_menu();
+  setup_options_menu();
+
+  // Pack the menu into the window.
+  GtkWidget *main_vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 1);
+  gtk_container_add(GTK_CONTAINER(_window), main_vbox);
+  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);
+
+  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(_window);
+  gtk_widget_realize(_window);
+
+  // Set up a timer to poll the pstats every so often.
+  g_timeout_add(200,
+    G_SOURCE_FUNC(+[](gpointer data) -> gboolean {
+      GtkStatsServer *self = (GtkStatsServer *)data;
+      self->poll();
+      return TRUE;
+    }), this);
+}
+
+/**
+ * Creates the "Session" pulldown menu.
+ */
+void GtkStatsServer::
+setup_session_menu() {
+  _session_menu = gtk_menu_new();
+
+  GtkWidget *item = gtk_menu_item_new_with_mnemonic("_Session");
+  gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), _session_menu);
+  gtk_menu_shell_append(GTK_MENU_SHELL(_menu_bar), item);
+
+  item = gtk_menu_item_new_with_mnemonic("_New Session");
+  _new_session_menu_item = item;
+  gtk_menu_shell_append(GTK_MENU_SHELL(_session_menu), item);
+  g_signal_connect(G_OBJECT(item), "activate",
+    G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
+      GtkStatsServer *self = (GtkStatsServer *)data;
+      self->new_session();
+    }), this);
+
+  item = gtk_menu_item_new_with_mnemonic("_Open Session...");
+  gtk_menu_shell_append(GTK_MENU_SHELL(_session_menu), item);
+  g_signal_connect(G_OBJECT(item), "activate",
+    G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
+      GtkStatsServer *self = (GtkStatsServer *)data;
+      self->open_session();
+    }), this);
+
+  item = gtk_menu_item_new_with_mnemonic("Open _Last Session");
+  _open_last_session_menu_item = item;
+  gtk_menu_shell_append(GTK_MENU_SHELL(_session_menu), item);
+  g_signal_connect(G_OBJECT(item), "activate",
+    G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
+      GtkStatsServer *self = (GtkStatsServer *)data;
+      self->open_last_session();
+    }), this);
+
+  if (!_last_session.exists()) {
+    gtk_widget_set_sensitive(item, FALSE);
+  }
+
+  item = gtk_menu_item_new_with_mnemonic("_Save Session...");
+  _save_session_menu_item = item;
+  gtk_widget_set_sensitive(item, FALSE);
+  gtk_menu_shell_append(GTK_MENU_SHELL(_session_menu), item);
+  g_signal_connect(G_OBJECT(item), "activate",
+    G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
+      GtkStatsServer *self = (GtkStatsServer *)data;
+      self->save_session();
+    }), this);
+
+  item = gtk_menu_item_new_with_mnemonic("_Close Session");
+  _close_session_menu_item = item;
+  gtk_widget_set_sensitive(item, FALSE);
+  gtk_menu_shell_append(GTK_MENU_SHELL(_session_menu), item);
+  g_signal_connect(G_OBJECT(item), "activate",
+    G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
+      GtkStatsServer *self = (GtkStatsServer *)data;
+      self->close_session();
+    }), this);
+
+  GtkWidget *sep = gtk_separator_menu_item_new();
+  gtk_menu_shell_append(GTK_MENU_SHELL(_session_menu), sep);
+
+  item = gtk_menu_item_new_with_mnemonic("_Export as JSON...");
+  _export_session_menu_item = item;
+  gtk_widget_set_sensitive(item, FALSE);
+  gtk_menu_shell_append(GTK_MENU_SHELL(_session_menu), item);
+  g_signal_connect(G_OBJECT(item), "activate",
+    G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
+      GtkStatsServer *self = (GtkStatsServer *)data;
+      self->export_session();
+    }), this);
+
+  sep = gtk_separator_menu_item_new();
+  gtk_menu_shell_append(GTK_MENU_SHELL(_session_menu), sep);
+
+  item = gtk_menu_item_new_with_mnemonic("E_xit");
+  gtk_menu_shell_append(GTK_MENU_SHELL(_session_menu), item);
+  g_signal_connect(G_OBJECT(item), "activate",
+    G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
+      GtkStatsServer *self = (GtkStatsServer *)data;
+      if (self->close_session()) {
+        gtk_main_quit();
+      }
+    }), this);
+
+  gtk_widget_show_all(_session_menu);
+
+  /*
+  _session_menu = CreatePopupMenu();
+
+  MENUITEMINFO mii;
+  memset(&mii, 0, sizeof(mii));
+  mii.cbSize = sizeof(mii);
+
+  mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_SUBMENU;
+  mii.fType = MFT_STRING;
+  mii.hSubMenu = _session_menu;
+
+  mii.dwTypeData = "&Session";
+  InsertMenuItem(_menu_bar, GetMenuItemCount(_menu_bar), TRUE, &mii);
+
+  AppendMenu(_session_menu, MF_STRING, MI_session_new, "&New Session\tCtrl+N");
+  AppendMenu(_session_menu, MF_STRING, MI_session_open, "&Open Session...\tCtrl+O");
+
+  if (_last_session.exists()) {
+    AppendMenu(_session_menu, MF_STRING, MI_session_open_last, "Open &Last Session");
+  } else {
+    AppendMenu(_session_menu, MF_STRING | MF_DISABLED, MI_session_open_last, "Open &Last Session");
+  }
+
+  AppendMenu(_session_menu, MF_STRING | MF_DISABLED, MI_session_save, "&Save Session...\tCtrl+S");
+  AppendMenu(_session_menu, MF_STRING | MF_DISABLED, MI_session_close, "&Close Session\tCtrl+W");
+
+  AppendMenu(_session_menu, MF_SEPARATOR, 0, nullptr);
+  AppendMenu(_session_menu, MF_STRING | MF_DISABLED, MI_session_export_json, "&Export as JSON...");
+
+  AppendMenu(_session_menu, MF_SEPARATOR, 0, nullptr);
+  AppendMenu(_session_menu, MF_STRING, MI_exit, "E&xit");*/
+}
+
+/**
+ * Creates the "Options" pulldown menu.
+ */
+void GtkStatsServer::
+setup_options_menu() {
+  _options_menu = gtk_menu_new();
+
+  GtkWidget *item = gtk_menu_item_new_with_label("Options");
+  gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), _options_menu);
+  gtk_menu_shell_append(GTK_MENU_SHELL(_menu_bar), item);
+
+  GtkWidget *units_menu = gtk_menu_new();
+  item = gtk_menu_item_new_with_label("Units");
+  gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), units_menu);
+  gtk_menu_shell_append(GTK_MENU_SHELL(_options_menu), item);
+
+  item = gtk_radio_menu_item_new_with_label(nullptr, "ms");
+  gtk_menu_shell_append(GTK_MENU_SHELL(units_menu), item);
+  g_signal_connect(G_OBJECT(item), "activate",
+    G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
+      GtkStatsServer *self = (GtkStatsServer *)data;
+      self->set_time_units(PStatGraph::GBU_ms);
+    }), this);
+
+  item = gtk_radio_menu_item_new_with_label(
+    gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(item)), "Hz");
+  gtk_menu_shell_append(GTK_MENU_SHELL(units_menu), item);
+  g_signal_connect(G_OBJECT(item), "activate",
+    G_CALLBACK(+[](GtkMenuItem *item, gpointer data) {
+      GtkStatsServer *self = (GtkStatsServer *)data;
+      self->set_time_units(PStatGraph::GBU_hz);
+    }), this);
+
+  set_time_units(PStatGraph::GBU_ms);
+}
+
+/**
+ * Handles clicks on a partion of the status bar.
+ */
+gboolean GtkStatsServer::
+status_bar_button_event(GtkWidget *widget, GdkEventButton *event, gpointer data) {
+  GtkStatsServer *server = (GtkStatsServer *)data;
+
+  GtkFlowBoxChild *child = gtk_flow_box_get_child_at_pos(
+    GTK_FLOW_BOX(server->_status_bar), event->x, event->y);
+  if (child == nullptr) {
+    return FALSE;
+  }
+
+  // Which child is this?
+  GList *children = gtk_container_get_children(GTK_CONTAINER(server->_status_bar));
+  int index = g_list_index(children, child);
+  g_list_free(children);
+  if (index < 0) {
+    return FALSE;
+  }
+
+  if (event->type == GDK_2BUTTON_PRESS && event->button == 1) {
+    server->_monitor->handle_status_bar_click(index);
+    return TRUE;
+  }
+  else if (event->type == GDK_BUTTON_PRESS && event->button == 3 && index > 0) {
+    server->_monitor->handle_status_bar_popup(index);
+    return TRUE;
+  }
+  return FALSE;
 }

+ 47 - 1
pandatool/src/gtk-stats/gtkStatsServer.h

@@ -16,13 +16,59 @@
 
 #include "pandatoolbase.h"
 #include "pStatServer.h"
+#include "gtkStatsMonitor.h"
 
 /**
  * The class that owns the main loop, waiting for client connections.
  */
 class GtkStatsServer : public PStatServer {
 public:
-  virtual PStatMonitor *make_monitor();
+  GtkStatsServer();
+
+  virtual PStatMonitor *make_monitor(const NetAddress &address);
+  virtual void lost_connection(PStatMonitor *monitor);
+
+  bool new_session();
+  bool open_session();
+  bool open_last_session();
+  bool save_session();
+  bool export_session();
+  bool close_session();
+
+  GtkWidget *get_window() const;
+  GtkWidget *get_menu_bar() const;
+  GtkWidget *get_status_bar() const;
+
+  int get_time_units() const;
+  void set_time_units(int unit_mask);
+
+private:
+  void create_window();
+  void setup_session_menu();
+  void setup_options_menu();
+
+  static gboolean status_bar_button_event(GtkWidget *widget,
+                                          GdkEventButton *event,
+                                          gpointer data);
+
+private:
+  PT(GtkStatsMonitor) _monitor;
+
+  Filename _last_session;
+  Filename _save_filename;
+
+  GtkWidget *_window;
+  GtkWidget *_menu_bar;
+  GtkWidget *_session_menu;
+  GtkWidget *_options_menu;
+  GtkWidget *_status_bar;
+  GtkWidget *_status_bar_label = nullptr;
+  GtkWidget *_new_session_menu_item;
+  GtkWidget *_open_last_session_menu_item;
+  GtkWidget *_save_session_menu_item;
+  GtkWidget *_close_session_menu_item;
+  GtkWidget *_export_session_menu_item;
+  int _time_units;
 };
 
 #endif

+ 51 - 3
pandatool/src/gtk-stats/gtkStatsStripChart.cxx

@@ -25,9 +25,7 @@ static const int default_strip_chart_height = 100;
 GtkStatsStripChart::
 GtkStatsStripChart(GtkStatsMonitor *monitor, int thread_index,
                    int collector_index, bool show_level) :
-  PStatStripChart(monitor,
-                  show_level ? monitor->get_level_view(0, thread_index) : monitor->get_view(thread_index),
-                  thread_index, collector_index, 0, 0),
+  PStatStripChart(monitor, thread_index, collector_index, show_level, 0, 0),
   GtkStatsGraph(monitor, true)
 {
   if (show_level) {
@@ -84,6 +82,8 @@ GtkStatsStripChart(GtkStatsMonitor *monitor, int thread_index,
   gtk_widget_set_size_request(_graph_window, 0, 0);
 
   clear_region();
+
+  update();
 }
 
 /**
@@ -261,6 +261,35 @@ on_popup_label(int collector_index) {
                      (void *)menu_def);
   }
 
+  {
+    GtkWidget *menu_item = gtk_separator_menu_item_new();
+    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+  }
+
+  {
+    const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({
+      -1, collector_index, GtkStatsMonitor::CT_choose_color,
+    });
+
+    GtkWidget *menu_item = gtk_menu_item_new_with_label("Change Color...");
+    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({
+      -1, collector_index, GtkStatsMonitor::CT_reset_color,
+    });
+
+    GtkWidget *menu_item = gtk_menu_item_new_with_label("Reset Color");
+    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);
 }
@@ -416,6 +445,25 @@ end_draw(int from_x, int to_x) {
   gdk_window_invalidate_rect(window, &rect, FALSE);
 }
 
+/**
+ * Returns the current window dimensions.
+ */
+bool GtkStatsStripChart::
+get_window_state(int &x, int &y, int &width, int &height,
+                 bool &maximized, bool &minimized) const {
+  GtkStatsGraph::get_window_state(x, y, width, height, maximized, minimized);
+  return true;
+}
+
+/**
+ * Called to restore the graph window to its previous dimensions.
+ */
+void GtkStatsStripChart::
+set_window_state(int x, int y, int width, int height,
+                 bool maximized, bool minimized) {
+  GtkStatsGraph::set_window_state(x, y, width, height, maximized, minimized);
+}
+
 /**
  * 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.

+ 5 - 0
pandatool/src/gtk-stats/gtkStatsStripChart.h

@@ -56,6 +56,11 @@ protected:
   virtual void draw_cursor(int x);
   virtual void end_draw(int from_x, int to_x);
 
+  virtual bool get_window_state(int &x, int &y, int &width, int &height,
+                                bool &maximized, bool &minimized) const;
+  virtual void set_window_state(int x, int y, int width, int height,
+                                bool maximized, bool minimized);
+
   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);

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

@@ -312,6 +312,25 @@ animate(double time, double dt) {
   return PStatTimeline::animate(time, dt);
 }
 
+/**
+ * Returns the current window dimensions.
+ */
+bool GtkStatsTimeline::
+get_window_state(int &x, int &y, int &width, int &height,
+                 bool &maximized, bool &minimized) const {
+  GtkStatsGraph::get_window_state(x, y, width, height, maximized, minimized);
+  return true;
+}
+
+/**
+ * Called to restore the graph window to its previous dimensions.
+ */
+void GtkStatsTimeline::
+set_window_state(int x, int y, int width, int height,
+                 bool maximized, bool minimized) {
+  GtkStatsGraph::set_window_state(x, y, width, height, maximized, minimized);
+}
+
 /**
  * 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.
@@ -414,6 +433,37 @@ handle_button_press(int graph_x, int graph_y, bool double_click, int button) {
                            (void *)menu_def);
         }
 
+        {
+          GtkWidget *menu_item = gtk_separator_menu_item_new();
+          gtk_menu_shell_append(GTK_MENU_SHELL(menu), menu_item);
+        }
+
+        {
+          const GtkStatsMonitor::MenuDef *menu_def = GtkStatsGraph::_monitor->add_menu({
+            -1, bar._collector_index,
+            GtkStatsMonitor::CT_choose_color,
+          });
+
+          GtkWidget *menu_item = gtk_menu_item_new_with_label("Change Color...");
+          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({
+            -1, bar._collector_index,
+            GtkStatsMonitor::CT_reset_color,
+          });
+
+          GtkWidget *menu_item = gtk_menu_item_new_with_label("Reset Color");
+          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;

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

@@ -47,6 +47,11 @@ protected:
 
   virtual bool animate(double time, double dt);
 
+  virtual bool get_window_state(int &x, int &y, int &width, int &height,
+                                bool &maximized, bool &minimized) const;
+  virtual void set_window_state(int x, int y, int width, int height,
+                                bool maximized, bool minimized);
+
   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);

+ 165 - 10
pandatool/src/pstatserver/pStatClientData.cxx

@@ -12,6 +12,7 @@
  */
 
 #include "pStatClientData.h"
+#include "pStatFrameData.h"
 #include "pStatReader.h"
 
 #include "pStatCollectorDef.h"
@@ -20,16 +21,15 @@ using std::string;
 
 PStatCollectorDef PStatClientData::_null_collector(-1, "Unknown");
 
-
-
 /**
  *
  */
 PStatClientData::
 PStatClientData(PStatReader *reader) :
+  _is_alive(true),
+  _is_dirty(false),
   _reader(reader)
 {
-  _is_alive = true;
 }
 
 /**
@@ -37,12 +37,28 @@ PStatClientData(PStatReader *reader) :
  */
 PStatClientData::
 ~PStatClientData() {
-  Collectors::const_iterator ci;
-  for (ci = _collectors.begin(); ci != _collectors.end(); ++ci) {
-    delete (*ci)._def;
+  for (Collector &collector : _collectors) {
+    delete collector._def;
   }
 }
 
+/**
+ * Clears the is_dirty() flag.
+ */
+void PStatClientData::
+clear_dirty() const {
+  _is_dirty = false;
+}
+
+/**
+ * Returns true if the data was modified since the last time clear_dirty() was
+ * called.
+ */
+bool PStatClientData::
+is_dirty() const {
+  return _is_dirty;
+}
+
 /**
  * Returns true if the data is actively getting filled by a connected client,
  * or false if the client has terminated.
@@ -83,6 +99,28 @@ has_collector(int index) const {
           _collectors[index]._def != nullptr);
 }
 
+/**
+ * Returns the index of the collector with the given full name, or -1 if no
+ * such collector has been defined by the client.
+ */
+int PStatClientData::
+find_collector(const std::string &fullname) const {
+  // Take the last bit, we can compare it more cheaply, only check the full
+  // name if the basename matches.
+  const char *colon = strrchr(fullname.c_str(), ':');
+  std::string name(colon != nullptr ? colon + 1 : fullname.c_str());
+
+  for (int index = 0; index < get_num_collectors(); ++index) {
+    const PStatCollectorDef *def = _collectors[index]._def;
+    if (def != nullptr && def->_name == name &&
+        get_collector_fullname(index) == fullname) {
+      return index;
+    }
+  }
+
+  return -1;
+}
+
 /**
  * Returns the nth collector definition.
  */
@@ -153,6 +191,10 @@ set_collector_has_level(int index, int thread_index, bool flag) {
     }
   }
 
+  if (any_changed) {
+    _is_dirty = true;
+  }
+
   return any_changed;
 }
 
@@ -206,6 +248,21 @@ has_thread(int index) const {
           !_threads[index]._name.empty());
 }
 
+/**
+ * Returns the index of the thread with the given name, or -1 if no such thread
+ * has yet been defined.
+ */
+int PStatClientData::
+find_thread(const std::string &name) const {
+  for (int index = 0; index < get_num_threads(); ++index) {
+    if (_threads[index]._name == name) {
+      return index;
+    }
+  }
+
+  return -1;
+}
+
 /**
  * Returns the name of the indicated thread.
  */
@@ -280,6 +337,8 @@ add_collector(PStatCollectorDef *def) {
       set_collector_has_level(def->_parent_index, thread_index, true);
     }
   }
+
+  _is_dirty = true;
 }
 
 /**
@@ -303,8 +362,9 @@ define_thread(int thread_index, const string &name) {
   if (_threads[thread_index]._data.is_null()) {
     _threads[thread_index]._data = new PStatThreadData(this);
   }
-}
 
+  _is_dirty = true;
+}
 
 /**
  * Makes room for and stores a new frame's worth of data associated with some
@@ -319,6 +379,102 @@ record_new_frame(int thread_index, int frame_number,
   define_thread(thread_index);
   nassertv(thread_index >= 0 && thread_index < (int)_threads.size());
   _threads[thread_index]._data->record_new_frame(frame_number, frame_data);
+  _is_dirty = true;
+}
+
+/**
+ * Writes the client data in the form of a JSON output that can be loaded into
+ * Chrome's event tracer.
+ */
+void PStatClientData::
+write_json(std::ostream &out, int pid) const {
+  out << "[\n";
+
+  for (int thread_index = 0; thread_index < get_num_threads(); ++thread_index) {
+    const Thread &thread = _threads[thread_index];
+
+    if (thread_index == 0) {
+      out << "{\"name\":\"thread_name\",\"ph\":\"M\",\"pid\":" << pid
+          << ",\"tid\":0,\"args\":{\"name\":\"Main\"}}";
+    }
+    else if (!thread._name.empty()) {
+      out
+        << ",\n{\"name\":\"thread_name\",\"ph\":\"M\",\"pid\":" << pid
+        << ",\"tid\":" << thread_index << ",\"args\":{\"name\":\""
+        << thread._name << "\"}}";
+    }
+    if (thread._data == nullptr || thread._data->is_empty()) {
+      continue;
+    }
+
+    int first_frame = thread._data->get_oldest_frame_number();
+    int last_frame = thread._data->get_latest_frame_number();
+    for (int frame_number = first_frame; frame_number <= last_frame; ++frame_number) {
+      const PStatFrameData &frame_data = thread._data->get_frame(frame_number);
+      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);
+        out
+          << ",\n{\"name\":\"" << get_collector_fullname(collector_index)
+          << "\",\"ts\":" << (uint64_t)(frame_data.get_time(i) * 1000000)
+          << ",\"ph\":\"" << (frame_data.is_start(i) ? 'B' : 'E') << "\""
+          << ",\"pid\":" << pid << ",\"tid\":" << thread_index << "}";
+      }
+    }
+  }
+
+  out << "\n]\n";
+  out.flush();
+}
+
+/**
+ * Writes the client data to a datagram.
+ */
+void PStatClientData::
+write_datagram(Datagram &dg) const {
+  for (const Collector &collector : _collectors) {
+    PStatCollectorDef *def = collector._def;
+    if (def != nullptr && def->_index != -1) {
+      def->write_datagram(dg);
+      collector._is_level.write_datagram(nullptr, dg);
+    }
+  }
+  dg.add_int16(-1);
+
+  int thread_index = 0;
+  for (const Thread &thread : _threads) {
+    if (thread._data != nullptr) {
+      dg.add_int16(thread_index);
+      dg.add_string(thread._name);
+      thread._data->write_datagram(dg);
+    }
+    ++thread_index;
+  }
+  dg.add_int16(-1);
+}
+
+/**
+ * Restores the client data from a datagram.
+ */
+void PStatClientData::
+read_datagram(DatagramIterator &scan) {
+  while (scan.peek_int16() != -1) {
+    PStatCollectorDef *def = new PStatCollectorDef;
+    def->read_datagram(scan);
+    add_collector(def);
+    _collectors[def->_index]._is_level.read_datagram(scan, nullptr);
+  }
+  scan.skip_bytes(2);
+
+  int thread_index;
+  while ((thread_index = scan.get_int16()) != -1) {
+    std::string name = scan.get_string();
+    define_thread(thread_index, name);
+
+    _threads[thread_index]._data->read_datagram(scan);
+  }
+
+  update_toplevel_collectors();
 }
 
 /**
@@ -344,9 +500,8 @@ void PStatClientData::
 update_toplevel_collectors() {
   _toplevel_collectors.clear();
 
-  Collectors::const_iterator ci;
-  for (ci = _collectors.begin(); ci != _collectors.end(); ++ci) {
-    PStatCollectorDef *def = (*ci)._def;
+  for (Collector &collector : _collectors) {
+    PStatCollectorDef *def = collector._def;
     if (def != nullptr && def->_parent_index == 0) {
       _toplevel_collectors.push_back(def->_index);
     }

+ 14 - 2
pandatool/src/pstatserver/pStatClientData.h

@@ -35,14 +35,19 @@ class PStatReader;
  */
 class PStatClientData : public PStatClientVersion {
 public:
+  PStatClientData() = default;
   PStatClientData(PStatReader *reader);
   ~PStatClientData();
 
+  void clear_dirty() const;
+  bool is_dirty() const;
+
   bool is_alive() const;
   void close();
 
   int get_num_collectors() const;
   bool has_collector(int index) const;
+  int find_collector(const std::string &fullname) const;
   const PStatCollectorDef &get_collector_def(int index) const;
   std::string get_collector_name(int index) const;
   std::string get_collector_fullname(int index) const;
@@ -54,6 +59,7 @@ public:
 
   int get_num_threads() const;
   bool has_thread(int index) const;
+  int find_thread(const std::string &name) const;
   std::string get_thread_name(int index) const;
   const PStatThreadData *get_thread_data(int index) const;
 
@@ -65,13 +71,19 @@ public:
 
   void record_new_frame(int thread_index, int frame_number,
                         PStatFrameData *frame_data);
+
+  void write_json(std::ostream &out, int pid = 0) const;
+  void write_datagram(Datagram &dg) const;
+  void read_datagram(DatagramIterator &scan);
+
 private:
   void slot_collector(int collector_index);
   void update_toplevel_collectors();
 
 private:
-  bool _is_alive;
-  PStatReader *_reader;
+  bool _is_alive = false;
+  mutable bool _is_dirty = false;
+  PStatReader *_reader = nullptr;
 
   class Collector {
   public:

+ 51 - 3
pandatool/src/pstatserver/pStatFlameGraph.cxx

@@ -34,14 +34,22 @@ PStatFlameGraph(PStatMonitor *monitor,
 {
   _average_mode = true;
   _average_cursor = 0;
-
-  _time_width = 1.0 / pstats_target_frame_rate;
   _current_frame = -1;
 
   _title_unknown = true;
 
+  // NB. This won't call force_redraw() (which we can't do yet) because average
+  // mode is true
+  update_data();
+  _time_width = _stack.get_net_value(false);
+  if (_time_width == 0.0) {
+    _time_width = 1.0 / pstats_target_frame_rate;
+  }
+
   _guide_bar_units = GBU_ms | GBU_hz | GBU_show_units;
   normal_guide_bars();
+
+  monitor->_flame_graphs.insert(this);
 }
 
 /**
@@ -49,6 +57,7 @@ PStatFlameGraph(PStatMonitor *monitor,
  */
 PStatFlameGraph::
 ~PStatFlameGraph() {
+  _monitor->_flame_graphs.erase(this);
 }
 
 /**
@@ -97,6 +106,15 @@ set_collector_index(int collector_index) {
     _title_unknown = true;
     _stack.clear();
     update_data();
+
+    if (_average_mode) {
+      _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();
+    }
   }
 }
 
@@ -167,6 +185,34 @@ get_bar_collector(int depth, int x) const {
   return -1;
 }
 
+/**
+ * Writes the graph state to a datagram.
+ */
+void PStatFlameGraph::
+write_datagram(Datagram &dg) const {
+  dg.add_int16(_orig_collector_index);
+  dg.add_float64(_time_width);
+  dg.add_bool(_average_mode);
+
+  PStatGraph::write_datagram(dg);
+}
+
+/**
+ * Restores the graph state from a datagram.
+ */
+void PStatFlameGraph::
+read_datagram(DatagramIterator &scan) {
+  _orig_collector_index = scan.get_int16();
+  _time_width = scan.get_float64();
+  _average_mode = scan.get_bool();
+
+  PStatGraph::read_datagram(scan);
+
+  _current_frame = -1;
+  normal_guide_bars();
+  update();
+}
+
 /**
  *
  */
@@ -336,9 +382,11 @@ animate(double time, double dt) {
       _time_width = 1.0 / pstats_target_frame_rate;
     }
     normal_guide_bars();
-    force_redraw();
   }
 
+  // Always use force_redraw, since the mouse position may have changed.
+  force_redraw();
+
   // Cycle through the ring buffers.
   _average_cursor = (_average_cursor + 1) % _num_average_frames;
   return true;

+ 3 - 0
pandatool/src/pstatserver/pStatFlameGraph.h

@@ -60,6 +60,9 @@ public:
   std::string get_bar_tooltip(int depth, int x) const;
   int get_bar_collector(int depth, int x) const;
 
+  virtual void write_datagram(Datagram &dg) const final;
+  virtual void read_datagram(DatagramIterator &scan) final;
+
 protected:
   void update_data();
   void changed_size(int xsize, int ysize);

+ 85 - 10
pandatool/src/pstatserver/pStatGraph.cxx

@@ -189,8 +189,8 @@ format_number(double value, int guide_bar_units, const string &unit_name) {
       label += " ";
       label += unit_name;
     }
-
-  } else {
+  }
+  else {
     // Units are either milliseconds or hz, or both.
     if ((guide_bar_units & GBU_ms) != 0) {
       if ((guide_bar_units & GBU_show_units) != 0 &&
@@ -225,15 +225,36 @@ format_number(double value, int guide_bar_units, const string &unit_name) {
     if ((guide_bar_units & GBU_hz) != 0) {
       double hz = 1.0 / value;
 
-      if ((guide_bar_units & GBU_ms) != 0) {
-        label += " (";
-      }
-      label += format_number(hz);
-      if ((guide_bar_units & GBU_show_units) != 0) {
-        label += " Hz";
+      if ((guide_bar_units & GBU_show_units) != 0 &&
+          (guide_bar_units & GBU_ms) == 0) {
+        if (hz >= 1000000000) {
+          label += format_number(hz / 1000000000);
+          label += " GHz";
+        }
+        else if (hz >= 1000000) {
+          label += format_number(hz / 1000000);
+          label += " MHz";
+        }
+        else if (hz >= 1000) {
+          label += format_number(hz / 1000);
+          label += " kHz";
+        }
+        else {
+          label += format_number(hz);
+          label += " Hz";
+        }
       }
-      if ((guide_bar_units & GBU_ms) != 0) {
-        label += ")";
+      else {
+        if ((guide_bar_units & GBU_ms) != 0) {
+          label += " (";
+        }
+        label += format_number(hz);
+        if ((guide_bar_units & GBU_show_units) != 0) {
+          label += " Hz";
+        }
+        if ((guide_bar_units & GBU_ms) != 0) {
+          label += ")";
+        }
       }
     }
   }
@@ -241,6 +262,43 @@ format_number(double value, int guide_bar_units, const string &unit_name) {
   return label;
 }
 
+/**
+ * Writes the graph state to a datagram.
+ */
+void PStatGraph::
+write_datagram(Datagram &dg) const {
+  int x, y, width, height;
+  bool minimized, maximized;
+  if (get_window_state(x, y, width, height, minimized, maximized)) {
+    dg.add_bool(true);
+    dg.add_int32(x);
+    dg.add_int32(y);
+    dg.add_int32(width);
+    dg.add_int32(height);
+    dg.add_bool(minimized);
+    dg.add_bool(maximized);
+  }
+  else {
+    dg.add_bool(false);
+  }
+}
+
+/**
+ * Restores the graph state from a datagram.
+ */
+void PStatGraph::
+read_datagram(DatagramIterator &scan) {
+  if (scan.get_bool()) {
+    int x = scan.get_int32();
+    int y = scan.get_int32();
+    int width = scan.get_int32();
+    int height = scan.get_int32();
+    bool minimized = scan.get_bool();
+    bool maximized = scan.get_bool();
+    set_window_state(x, y, width, height, minimized, maximized);
+  }
+}
+
 /**
  * Resets the list of guide bars.
  */
@@ -294,3 +352,20 @@ make_guide_bar(double value, PStatGraph::GuideBarStyle style) const {
 
   return GuideBar(value, label, style);
 }
+
+/**
+ * Returns the current window dimensions.
+ */
+bool PStatGraph::
+get_window_state(int &x, int &y, int &width, int &height,
+                 bool &maximized, bool &minimized) const {
+  return false;
+}
+
+/**
+ * Called to restore the graph window to its previous dimensions.
+ */
+void PStatGraph::
+set_window_state(int x, int y, int width, int height,
+                 bool maximized, bool minimized) {
+}

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

@@ -91,6 +91,14 @@ public:
   static std::string format_number(double value, int guide_bar_units,
                               const std::string &unit_name = std::string());
 
+  virtual void write_datagram(Datagram &dg) const;
+  virtual void read_datagram(DatagramIterator &scan);
+
+  virtual bool get_window_state(int &x, int &y, int &width, int &height,
+                                bool &maximized, bool &minimized) const;
+  virtual void set_window_state(int x, int y, int width, int height,
+                                bool maximized, bool minimized);
+
 protected:
   virtual void normal_guide_bars()=0;
   void update_guide_bars(int num_bars, double scale);

+ 1 - 1
pandatool/src/pstatserver/pStatListener.cxx

@@ -33,7 +33,7 @@ void PStatListener::
 connection_opened(const PT(Connection) &,
                   const NetAddress &address,
                   const PT(Connection) &new_connection) {
-  PStatMonitor *monitor = _manager->make_monitor();
+  PStatMonitor *monitor = _manager->make_monitor(address);
   if (monitor == nullptr) {
     nout << "Couldn't create monitor!\n";
     return;

+ 16 - 0
pandatool/src/pstatserver/pStatMonitor.I

@@ -77,3 +77,19 @@ INLINE int PStatMonitor::
 get_client_pid() const {
   return _client_pid;
 }
+
+/**
+ * Returns true if the session data was loaded from a file.
+ */
+INLINE bool PStatMonitor::
+has_read_filename() const {
+  return !_read_filename.empty();
+}
+
+/**
+ * Returns the filename this session data was loaded from.
+ */
+INLINE const Filename &PStatMonitor::
+get_read_filename() const {
+  return _read_filename;
+}

+ 472 - 0
pandatool/src/pstatserver/pStatMonitor.cxx

@@ -13,10 +13,29 @@
 
 #include "pStatMonitor.h"
 
+#include "datagramInputFile.h"
+#include "datagramOutputFile.h"
+#include "pandaVersion.h"
 #include "pStatCollectorDef.h"
+#include "pStatTimeline.h"
+#include "pStatStripChart.h"
+#include "pStatFlameGraph.h"
+#include "pStatPianoRoll.h"
 
 using std::string;
 
+static const string session_file_header("pstat\0\n\r", 8);
+static const string layout_file_header("pslyt\0\n\r", 8);
+
+static const Filename layout_filename = Filename::binary_filename(
+#ifdef _WIN32
+  Filename::expand_from("$USER_APPDATA/Panda3D-" PANDA_ABI_VERSION_STR "/pstats-layout")
+#elif defined(__APPLE__)
+  Filename::expand_from("$HOME/Library/Caches/Panda3D-" PANDA_ABI_VERSION_STR "/pstats-layout")
+#else
+  Filename::expand_from("$XDG_CACHE_HOME/panda3d/pstats-layout")
+#endif
+);
 
 /**
  *
@@ -31,6 +50,7 @@ PStatMonitor(PStatServer *server) : _server(server) {
  */
 PStatMonitor::
 ~PStatMonitor() {
+  close();
 }
 
 /**
@@ -74,6 +94,283 @@ set_client_data(PStatClientData *client_data) {
   initialized();
 }
 
+/**
+ * Writes the data and the UI state to the given file.
+ */
+bool PStatMonitor::
+write(const Filename &fn) const {
+  DatagramOutputFile dof;
+  if (!dof.open(fn)) {
+    return false;
+  }
+  if (!dof.write_header(session_file_header)) {
+    return false;
+  }
+  Datagram dg;
+  dg.set_stdfloat_double(false);
+  dg.add_uint16(1);
+  dg.add_uint16(1);
+  write_datagram(dg);
+  dof.put_datagram(dg);
+  dof.close();
+  return true;
+}
+
+/**
+ * Reads the data and the UI state from the given file.
+ */
+bool PStatMonitor::
+read(const Filename &fn) {
+  close();
+
+  DatagramInputFile dif;
+  if (!dif.open(fn)) {
+    nout << "Failed to open " << fn << " for reading.\n";
+    return false;
+  }
+  string header;
+  if (!dif.read_header(header, 8) || header != session_file_header) {
+    nout << "Session file contains invalid header.\n";
+    return false;
+  }
+  Datagram dg;
+  dg.set_stdfloat_double(false);
+
+  if (!dif.get_datagram(dg)) {
+    nout << "Failed to read datagram from session file.\n";
+    return false;
+  }
+
+  DatagramIterator scan(dg);
+  int version = scan.get_uint16();
+  if (version != 1) {
+    nout << "Unsupported session file version " << version << ".\n";
+    return false;
+  }
+  // Room for a minor version number
+  scan.get_uint16();
+
+  read_datagram(scan);
+  dif.close();
+
+  idle();
+
+  _read_filename = fn;
+  _client_data->clear_dirty();
+
+  return true;
+}
+
+/**
+ * Opens the default set of graphs.
+ */
+void PStatMonitor::
+open_default_graphs() {
+  DatagramInputFile dif;
+  if (!dif.open(layout_filename)) {
+    open_strip_chart(0, 0, false);
+    return;
+  }
+  string header;
+  if (!dif.read_header(header, 8) || header != layout_file_header) {
+    nout << "Layout file contains invalid header.\n";
+    open_strip_chart(0, 0, false);
+    return;
+  }
+  Datagram dg;
+  dg.set_stdfloat_double(false);
+
+  if (!dif.get_datagram(dg)) {
+    nout << "Failed to read datagram from layout file.\n";
+    open_strip_chart(0, 0, false);
+    return;
+  }
+
+  DatagramIterator scan(dg);
+  int version = scan.get_uint16();
+  if (version != 1) {
+    nout << "Unsupported layout file version " << version << ".\n";
+    open_strip_chart(0, 0, false);
+    return;
+  }
+  // Room for a minor version number
+  scan.get_uint16();
+
+  size_t num_colors = scan.get_uint32();
+  for (size_t i = 0; i < num_colors; ++i) {
+    int key = scan.get_int32();
+    _colors[key].read_datagram(scan);
+  }
+
+  PStatGraph *graph;
+
+  size_t num_timelines = scan.get_uint16();
+  for (size_t i = 0; i < num_timelines; ++i) {
+    int x = scan.get_int32();
+    int y = scan.get_int32();
+    int width = scan.get_int32();
+    int height = scan.get_int32();
+    bool minimized = scan.get_bool();
+    bool maximized = scan.get_bool();
+    graph = open_timeline();
+    graph->set_window_state(x, y, width, height, minimized, maximized);
+  }
+
+  size_t num_strip_charts = scan.get_uint16();
+  for (size_t i = 0; i < num_strip_charts; ++i) {
+    std::string thread_name = scan.get_string();
+    std::string collector_name = scan.get_string();
+    bool show_level = scan.get_bool();
+    int thread_index = _client_data->find_thread(thread_name);
+    int collector_index = _client_data->find_collector(collector_name);
+    int x = scan.get_int32();
+    int y = scan.get_int32();
+    int width = scan.get_int32();
+    int height = scan.get_int32();
+    bool minimized = scan.get_bool();
+    bool maximized = scan.get_bool();
+    if (thread_index != -1) {
+      graph = open_strip_chart(thread_index, collector_index, show_level);
+      graph->set_window_state(x, y, width, height, minimized, maximized);
+    }
+  }
+
+  size_t num_flame_graphs = scan.get_uint16();
+  for (size_t i = 0; i < num_flame_graphs; ++i) {
+    std::string thread_name = scan.get_string();
+    std::string collector_name = scan.get_string();
+    int thread_index = _client_data->find_thread(thread_name);
+    int collector_index = collector_name.empty() ? -1 : _client_data->find_collector(collector_name);
+    int x = scan.get_int32();
+    int y = scan.get_int32();
+    int width = scan.get_int32();
+    int height = scan.get_int32();
+    bool minimized = scan.get_bool();
+    bool maximized = scan.get_bool();
+    if (thread_index != -1) {
+      graph = open_flame_graph(thread_index, collector_index);
+      graph->set_window_state(x, y, width, height, minimized, maximized);
+    }
+  }
+
+  size_t num_piano_rolls = scan.get_uint16();
+  for (size_t i = 0; i < num_piano_rolls; ++i) {
+    std::string thread_name = scan.get_string();
+    int thread_index = _client_data->find_thread(thread_name);
+    int x = scan.get_int32();
+    int y = scan.get_int32();
+    int width = scan.get_int32();
+    int height = scan.get_int32();
+    bool minimized = scan.get_bool();
+    bool maximized = scan.get_bool();
+    if (thread_index != -1) {
+      graph = open_piano_roll(thread_index);
+      graph->set_window_state(x, y, width, height, minimized, maximized);
+    }
+  }
+
+  dif.close();
+  if (num_timelines + num_strip_charts + num_flame_graphs + num_piano_rolls == 0) {
+    open_strip_chart(0, 0, false);
+  }
+}
+
+/**
+ * Saves the current graph layout as the default graph layout.
+ */
+bool PStatMonitor::
+save_default_graphs() const {
+  layout_filename.make_dir();
+
+  DatagramOutputFile dof;
+  if (!dof.open(layout_filename)) {
+    return false;
+  }
+  if (!dof.write_header(layout_file_header)) {
+    return false;
+  }
+  Datagram dg;
+  dg.set_stdfloat_double(false);
+  dg.add_uint16(1);
+  dg.add_uint16(1);
+
+  dg.add_uint32((uint32_t)_colors.size());
+  for (const auto &item : _colors) {
+    dg.add_int32(item.first);
+    item.second.write_datagram(dg);
+  }
+
+  dg.add_uint16(_timelines.size());
+  for (PStatGraph *graph : _timelines) {
+    int x, y, width, height;
+    bool minimized, maximized;
+    if (graph->get_window_state(x, y, width, height, minimized, maximized)) {
+      dg.add_int32(x);
+      dg.add_int32(y);
+      dg.add_int32(width);
+      dg.add_int32(height);
+      dg.add_bool(minimized);
+      dg.add_bool(maximized);
+    }
+  }
+
+  dg.add_uint16(_strip_charts.size());
+  for (PStatGraph *graph : _strip_charts) {
+    int x, y, width, height;
+    bool minimized, maximized;
+    if (graph->get_window_state(x, y, width, height, minimized, maximized)) {
+      dg.add_string(_client_data->get_thread_name(((PStatStripChart *)graph)->get_thread_index()));
+      dg.add_string(_client_data->get_collector_fullname(((PStatStripChart *)graph)->get_collector_index()));
+      dg.add_bool(((PStatStripChart *)graph)->get_view().get_show_level());
+      dg.add_int32(x);
+      dg.add_int32(y);
+      dg.add_int32(width);
+      dg.add_int32(height);
+      dg.add_bool(minimized);
+      dg.add_bool(maximized);
+    }
+  }
+
+  dg.add_uint16(_flame_graphs.size());
+  for (PStatGraph *graph : _flame_graphs) {
+    int x, y, width, height;
+    bool minimized, maximized;
+    if (graph->get_window_state(x, y, width, height, minimized, maximized)) {
+      int collector_index = ((PStatFlameGraph *)graph)->get_collector_index();
+      dg.add_string(_client_data->get_thread_name(((PStatFlameGraph *)graph)->get_thread_index()));
+      dg.add_string(collector_index >= 0 ? _client_data->get_collector_fullname(collector_index) : "");
+      dg.add_int32(x);
+      dg.add_int32(y);
+      dg.add_int32(width);
+      dg.add_int32(height);
+      dg.add_bool(minimized);
+      dg.add_bool(maximized);
+    }
+  }
+
+  dg.add_uint16(_piano_rolls.size());
+  for (PStatGraph *graph : _piano_rolls) {
+    int x, y, width, height;
+    bool minimized, maximized;
+    if (graph->get_window_state(x, y, width, height, minimized, maximized)) {
+      dg.add_string(_client_data->get_thread_name(((PStatPianoRoll *)graph)->get_thread_index()));
+      dg.add_int32(x);
+      dg.add_int32(y);
+      dg.add_int32(width);
+      dg.add_int32(height);
+      dg.add_bool(minimized);
+      dg.add_bool(maximized);
+    }
+  }
+
+  // Reserved for future graph type
+  dg.add_uint16(0);
+
+  dof.put_datagram(dg);
+  dof.close();
+  return true;
+}
+
 /**
  * Returns true if the client is alive and connected, false otherwise.
  */
@@ -145,6 +442,22 @@ get_collector_color(int collector_index) {
   return (*ci).second;
 }
 
+/**
+ * Sets a custom color associated with the given collector.
+ */
+void PStatMonitor::
+set_collector_color(int collector_index, const LRGBColor &color) {
+  _colors[collector_index] = color;
+}
+
+/**
+ * Clears any custom custom color associated with the given collector.
+ */
+void PStatMonitor::
+clear_collector_color(int collector_index) {
+  _colors.erase(collector_index);
+}
+
 /**
  * Returns a view on the given thread index.  If there is no such view already
  * for the indicated thread, this will create one.  This view can be used to
@@ -293,3 +606,162 @@ is_thread_safe() {
 void PStatMonitor::
 user_guide_bars_changed() {
 }
+
+/**
+ * Opens a new timeline.
+ */
+PStatGraph *PStatMonitor::
+open_timeline() {
+  return nullptr;
+}
+
+/**
+ * Opens a new strip chart showing the indicated data.
+ */
+PStatGraph *PStatMonitor::
+open_strip_chart(int thread_index, int collector_index, bool show_level) {
+  return nullptr;
+}
+
+/**
+ * Opens a new flame graph showing the indicated data.
+ */
+PStatGraph *PStatMonitor::
+open_flame_graph(int thread_index, int collector_index) {
+  return nullptr;
+}
+
+/**
+ * Opens a new piano roll showing the indicated data.
+ */
+PStatGraph *PStatMonitor::
+open_piano_roll(int thread_index) {
+  return nullptr;
+}
+
+/**
+ * Writes the client data and open graphs to a datagram.
+ */
+void PStatMonitor::
+write_datagram(Datagram &dg) const {
+  dg.add_bool(_client_known);
+  dg.add_string(_client_hostname);
+  dg.add_string(_client_progname);
+  dg.add_int32(_client_pid);
+
+  get_client_data()->write_datagram(dg);
+
+  dg.add_uint32((uint32_t)_colors.size());
+  for (const auto &item : _colors) {
+    dg.add_int32(item.first);
+    item.second.write_datagram(dg);
+  }
+
+  dg.add_uint16(_timelines.size());
+  for (PStatGraph *graph : _timelines) {
+    graph->write_datagram(dg);
+  }
+
+  dg.add_uint16(_strip_charts.size());
+  for (PStatGraph *graph : _strip_charts) {
+    dg.add_int16(((PStatStripChart *)graph)->get_thread_index());
+    dg.add_int16(((PStatStripChart *)graph)->get_collector_index());
+    dg.add_bool(((PStatStripChart *)graph)->get_view().get_show_level());
+    graph->write_datagram(dg);
+  }
+
+  dg.add_uint16(_flame_graphs.size());
+  for (PStatGraph *graph : _flame_graphs) {
+    dg.add_int16(((PStatFlameGraph *)graph)->get_thread_index());
+    dg.add_int16(((PStatFlameGraph *)graph)->get_collector_index());
+    graph->write_datagram(dg);
+  }
+
+  dg.add_uint16(_piano_rolls.size());
+  for (PStatGraph *graph : _piano_rolls) {
+    dg.add_int16(((PStatPianoRoll *)graph)->get_thread_index());
+    graph->write_datagram(dg);
+  }
+
+  // Reserved for future graph type
+  dg.add_uint16(0);
+}
+
+/**
+ * Restores the client data and open graphs from a datagram.
+ */
+void PStatMonitor::
+read_datagram(DatagramIterator &scan) {
+  _client_known = scan.get_bool();
+  _client_hostname = scan.get_string();
+  _client_progname = scan.get_string();
+  _client_pid = scan.get_int32();
+
+  PStatClientData *client_data = new PStatClientData;
+  client_data->read_datagram(scan);
+  set_client_data(client_data);
+
+  size_t num_colors = scan.get_uint32();
+  for (size_t i = 0; i < num_colors; ++i) {
+    int key = scan.get_int32();
+    _colors[key].read_datagram(scan);
+  }
+
+  int num_collectors = client_data->get_num_collectors();
+  for (int collector_index = 0; collector_index < num_collectors; ++collector_index) {
+    if (client_data->has_collector(collector_index)) {
+      new_collector(collector_index);
+    }
+  }
+
+  int num_threads = client_data->get_num_threads();
+  for (int thread_index = 0; thread_index < num_threads; ++thread_index) {
+    if (client_data->has_thread(thread_index)) {
+      const PStatThreadData *thread_data = client_data->get_thread_data(thread_index);
+      if (!thread_data->is_empty()) {
+        const PStatFrameData &frame_data = thread_data->get_latest_frame();
+        get_view(thread_index).set_to_frame(frame_data);
+
+        int num_collectors = client_data->get_num_toplevel_collectors();
+        for (int i = 0; i < num_collectors; ++i) {
+          int collector_index = client_data->get_toplevel_collector(i);
+          if (client_data->has_collector(collector_index)) {
+            get_level_view(collector_index, thread_index).set_to_frame(thread_data->get_latest_frame());
+          }
+        }
+      }
+      new_thread(thread_index);
+    }
+  }
+
+  PStatGraph *graph;
+
+  size_t num_timelines = scan.get_uint16();
+  for (size_t i = 0; i < num_timelines; ++i) {
+    graph = open_timeline();
+    graph->read_datagram(scan);
+  }
+
+  size_t num_strip_charts = scan.get_uint16();
+  for (size_t i = 0; i < num_strip_charts; ++i) {
+    int thread_index = scan.get_int16();
+    int collector_index = scan.get_int16();
+    graph = open_strip_chart(thread_index, collector_index, scan.get_bool());
+    graph->read_datagram(scan);
+  }
+
+  size_t num_flame_graphs = scan.get_uint16();
+  for (size_t i = 0; i < num_flame_graphs; ++i) {
+    int thread_index = scan.get_int16();
+    int collector_index = scan.get_int16();
+    graph = open_flame_graph(thread_index, collector_index);
+    graph->read_datagram(scan);
+  }
+
+  size_t num_piano_rolls = scan.get_uint16();
+  for (size_t i = 0; i < num_piano_rolls; ++i) {
+    int thread_index = scan.get_int16();
+    graph = open_piano_roll(thread_index);
+    graph->read_datagram(scan);
+  }
+}

+ 27 - 2
pandatool/src/pstatserver/pStatMonitor.h

@@ -26,6 +26,7 @@
 #include "pmap.h"
 
 class PStatCollectorDef;
+class PStatGraph;
 class PStatServer;
 
 /**
@@ -40,7 +41,7 @@ class PStatMonitor : public ReferenceCount {
 public:
   // The following functions are primarily for use by internal classes to set
   // up the monitor.
-  PStatMonitor(PStatServer *server);
+  PStatMonitor(PStatServer *server = nullptr);
   virtual ~PStatMonitor();
 
   void hello_from(const std::string &hostname, const std::string &progname,
@@ -51,6 +52,11 @@ public:
                    int server_major, int server_minor);
   void set_client_data(PStatClientData *client_data);
 
+  bool write(const Filename &fn) const;
+  bool read(const Filename &fn);
+
+  void open_default_graphs();
+  bool save_default_graphs() const;
 
   // The following functions are for use by user code to determine information
   // about the client data available.
@@ -61,16 +67,19 @@ public:
   INLINE const PStatClientData *get_client_data() const;
   INLINE std::string get_collector_name(int collector_index);
   const LRGBColor &get_collector_color(int collector_index);
+  void set_collector_color(int collector_index, const LRGBColor &color);
+  void clear_collector_color(int collector_index);
 
   INLINE bool is_client_known() const;
   INLINE std::string get_client_hostname() const;
   INLINE std::string get_client_progname() const;
   INLINE int get_client_pid() const;
+  INLINE bool has_read_filename() const;
+  INLINE const Filename &get_read_filename() const;
 
   PStatView &get_view(int thread_index);
   PStatView &get_level_view(int collector_index, int thread_index);
 
-
   // The following virtual methods may be overridden by a derived monitor
   // class to customize behavior.
 
@@ -92,6 +101,14 @@ public:
 
   virtual void user_guide_bars_changed();
 
+  virtual PStatGraph *open_timeline();
+  virtual PStatGraph *open_strip_chart(int thread_index, int collector_index, bool show_level);
+  virtual PStatGraph *open_flame_graph(int thread_index, int collector_index = -1);
+  virtual PStatGraph *open_piano_roll(int thread_index);
+
+  void write_datagram(Datagram &dg) const;
+  void read_datagram(DatagramIterator &scan);
+
 protected:
   PStatServer *_server;
 
@@ -102,6 +119,7 @@ private:
   std::string _client_hostname;
   std::string _client_progname;
   int _client_pid;
+  Filename _read_filename;
 
   typedef pmap<int, PStatView> Views;
   Views _views;
@@ -110,6 +128,13 @@ private:
 
   typedef pmap<int, LRGBColor> Colors;
   Colors _colors;
+
+public:
+  typedef pset<PStatGraph *> Graphs;
+  Graphs _timelines;
+  Graphs _strip_charts;
+  Graphs _flame_graphs;
+  Graphs _piano_rolls;
 };
 
 #include "pStatMonitor.I"

+ 42 - 1
pandatool/src/pstatserver/pStatPianoRoll.cxx

@@ -89,10 +89,24 @@ PStatPianoRoll(PStatMonitor *monitor, int thread_index, int xsize, int ysize) :
 {
   _time_width = 1.0 / pstats_target_frame_rate;
   _start_time = 0.0;
-
   _current_frame = -1;
+
+  // If we already have data, load it in now.
+  const PStatClientData *client_data = _monitor->get_client_data();
+  if (client_data->get_num_collectors() != 0 &&
+      client_data->get_num_threads() != 0) {
+    const PStatThreadData *thread_data =
+      client_data->get_thread_data(thread_index);
+    if (!thread_data->is_empty()) {
+      int frame_number = thread_data->get_latest_frame_number();
+      compute_page(thread_data->get_frame(frame_number));
+    }
+  }
+
   _guide_bar_units = GBU_ms | GBU_hz | GBU_show_units;
   normal_guide_bars();
+
+  monitor->_piano_rolls.insert(this);
 }
 
 /**
@@ -100,6 +114,7 @@ PStatPianoRoll(PStatMonitor *monitor, int thread_index, int xsize, int ysize) :
  */
 PStatPianoRoll::
 ~PStatPianoRoll() {
+  _monitor->_piano_rolls.erase(this);
 }
 
 /**
@@ -142,6 +157,32 @@ get_label_tooltip(int collector_index) const {
   return client_data->get_collector_fullname(collector_index);
 }
 
+/**
+ * Writes the graph state to a datagram.
+ */
+void PStatPianoRoll::
+write_datagram(Datagram &dg) const {
+  dg.add_float64(_time_width);
+  dg.add_float64(_start_time);
+
+  PStatGraph::write_datagram(dg);
+}
+
+/**
+ * Restores the graph state from a datagram.
+ */
+void PStatPianoRoll::
+read_datagram(DatagramIterator &scan) {
+  _time_width = scan.get_float64();
+  _start_time = scan.get_float64();
+
+  PStatGraph::read_datagram(scan);
+
+  _current_frame = -1;
+  normal_guide_bars();
+  update();
+}
+
 /**
  * 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

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

@@ -55,6 +55,9 @@ public:
 
   std::string get_label_tooltip(int collector_index) const;
 
+  virtual void write_datagram(Datagram &dg) const final;
+  virtual void read_datagram(DatagramIterator &scan) final;
+
 protected:
   void changed_size(int xsize, int ysize);
   void force_redraw();

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

@@ -94,6 +94,7 @@ void PStatReader::
 lost_connection() {
   _client_data->_is_alive = false;
   _monitor->lost_connection();
+  _manager->lost_connection(_monitor);
   _client_data.clear();
 
   _manager->close_connection(_tcp_connection);

+ 17 - 4
pandatool/src/pstatserver/pStatServer.cxx

@@ -30,10 +30,11 @@ PStatServer() {
  */
 PStatServer::
 ~PStatServer() {
+  stop_listening();
+
   delete _listener;
 }
 
-
 /**
  * Establishes a port number that the manager will listen on for TCP
  * connections.  This may be called more than once to listen simulataneously
@@ -47,20 +48,22 @@ PStatServer::
  */
 bool PStatServer::
 listen(int port) {
+  stop_listening();
+
   if (port < 0) {
     port = pstats_port;
   }
 
   // Now try to listen to the port.
-  PT(Connection) rendezvous = open_TCP_server_rendezvous(port, 5);
+  _rendezvous = open_TCP_server_rendezvous(port, 5);
 
-  if (rendezvous.is_null()) {
+  if (_rendezvous.is_null()) {
     // Couldn't get it.
     return false;
   }
 
   // Tell the listener about the new port.
-  _listener->add_connection(rendezvous);
+  _listener->add_connection(_rendezvous);
 
   if (_next_udp_port == 0) {
     _next_udp_port = port + 1;
@@ -68,6 +71,16 @@ listen(int port) {
   return true;
 }
 
+/**
+ * Stops listening.
+ */
+void PStatServer::
+stop_listening() {
+  if (_rendezvous != nullptr) {
+    close_connection(_rendezvous);
+    _rendezvous.clear();
+  }
+}
 
 /**
  * Checks for any network activity and handles it, if appropriate, and then

+ 5 - 1
pandatool/src/pstatserver/pStatServer.h

@@ -39,11 +39,14 @@ public:
   ~PStatServer();
 
   bool listen(int port = -1);
+  void stop_listening();
 
   void poll();
   void main_loop(bool *interrupt_flag = nullptr);
 
-  virtual PStatMonitor *make_monitor()=0;
+  virtual PStatMonitor *make_monitor(const NetAddress &address)=0;
+  virtual void lost_connection(PStatMonitor *monitor) {}
+
   void add_reader(Connection *connection, PStatReader *reader);
   void remove_reader(Connection *connection, PStatReader *reader);
 
@@ -67,6 +70,7 @@ private:
   void user_guide_bars_changed();
 
   PStatListener *_listener;
+  PT(Connection) _rendezvous;
 
   typedef pmap<PT(Connection), PStatReader *> Readers;
   Readers _readers;

+ 66 - 4
pandatool/src/pstatserver/pStatStripChart.cxx

@@ -29,11 +29,12 @@ using std::min;
  *
  */
 PStatStripChart::
-PStatStripChart(PStatMonitor *monitor, PStatView &view,
-                int thread_index, int collector_index, int xsize, int ysize) :
+PStatStripChart(PStatMonitor *monitor,
+                int thread_index, int collector_index, bool show_level,
+                int xsize, int ysize) :
   PStatGraph(monitor, xsize, ysize),
   _thread_index(thread_index),
-  _view(view),
+  _view(show_level ? monitor->get_level_view(0, thread_index) : monitor->get_view(thread_index)),
   _collector_index(collector_index)
 {
   _scroll_mode = pstats_scroll_mode;
@@ -57,6 +58,8 @@ PStatStripChart(PStatMonitor *monitor, PStatView &view,
   }
 
   set_auto_vertical_scale();
+
+  monitor->_strip_charts.insert(this);
 }
 
 /**
@@ -64,6 +67,7 @@ PStatStripChart(PStatMonitor *monitor, PStatView &view,
  */
 PStatStripChart::
 ~PStatStripChart() {
+  _monitor->_strip_charts.erase(this);
 }
 
 /**
@@ -370,6 +374,65 @@ get_label_tooltip(int collector_index) const {
   return text.str();
 }
 
+/**
+ * Writes the graph state to a datagram.
+ */
+void PStatStripChart::
+write_datagram(Datagram &dg) const {
+  dg.add_bool(_scroll_mode);
+  dg.add_bool(_average_mode);
+  dg.add_float64(_time_width);
+  dg.add_float64(_start_time);
+  dg.add_float64(_value_height);
+
+  // Not really necessary, we reconstructed this from the client data.
+  //for (const auto &item : _data) {
+  //  dg.add_int32(item.first);
+  //  dg.add_uint32(item.second.size());
+  //
+  //  for (const ColorData &cd : item.second) {
+  //    dg.add_uint16(cd._collector_index);
+  //    dg.add_uint16(cd._i);
+  //    dg.add_float64(cd._net_value);
+  //  }
+  //}
+  //dg.add_int32(-1);
+
+  PStatGraph::write_datagram(dg);
+}
+
+/**
+ * Restores the graph state from a datagram.
+ */
+void PStatStripChart::
+read_datagram(DatagramIterator &scan) {
+  _next_frame = 0;
+  force_reset();
+
+  _scroll_mode = scan.get_bool();
+  _average_mode = scan.get_bool();
+  _time_width = scan.get_float64();
+  _start_time = scan.get_float64();
+  _value_height = scan.get_float64();
+
+  //int key;
+  //while ((key = scan.get_int32()) != -1) {
+  //  FrameData &fdata = _data[key];
+  //  fdata.resize(scan.get_uint32());
+  //
+  //  for (ColorData &cd : fdata) {
+  //    cd._collector_index = scan.get_uint16();
+  //    cd._i = scan.get_uint16();
+  //    cd._net_value = scan.get_float64();
+  //  }
+  //}
+
+  PStatGraph::read_datagram(scan);
+
+  normal_guide_bars();
+  update();
+}
+
 /**
  * Adds the data from additional into the data from fdata, after applying the
  * scale weight.
@@ -784,7 +847,6 @@ void PStatStripChart::
 idle() {
 }
 
-
 // STL function object for sorting labels in order by the collector's sort
 // index, used in update_labels(), below.
 class SortCollectorLabels2 {

+ 6 - 2
pandatool/src/pstatserver/pStatStripChart.h

@@ -37,8 +37,9 @@ class PStatView;
  */
 class PStatStripChart : public PStatGraph {
 public:
-  PStatStripChart(PStatMonitor *monitor, PStatView &view,
-                  int thread_index, int collector_index, int xsize, int ysize);
+  PStatStripChart(PStatMonitor *monitor,
+                  int thread_index, int collector_index, bool show_level,
+                  int xsize, int ysize);
   virtual ~PStatStripChart();
 
   void new_data(int frame_number);
@@ -73,6 +74,9 @@ public:
   std::string get_title_text();
   std::string get_label_tooltip(int collector_index) const;
 
+  virtual void write_datagram(Datagram &dg) const final;
+  virtual void read_datagram(DatagramIterator &scan) final;
+
 protected:
   class ColorData {
   public:

+ 40 - 9
pandatool/src/pstatserver/pStatThreadData.cxx

@@ -202,7 +202,7 @@ get_latest_frame() const {
 bool PStatThreadData::
 get_elapsed_frames(int &then_i, int &now_i) const {
   if (!_computed_elapsed_frames) {
-    ((PStatThreadData *)this)->compute_elapsed_frames();
+    compute_elapsed_frames();
   }
 
   now_i = _now_i;
@@ -308,17 +308,48 @@ record_new_frame(int frame_number, PStatFrameData *frame_data) {
 }
 
 /**
- * Computes the frame numbers returned by get_elapsed_frames().  This is non-
- * const, but only updates cached values, so may safely be called from a const
- * method.
+ * Writes the thread data to a datagram.
  */
 void PStatThreadData::
-compute_elapsed_frames() {
+write_datagram(Datagram &dg) const {
+  int frame_number = _first_frame_number;
+
+  for (PStatFrameData *frame_data : _frames) {
+    if (frame_data != nullptr) {
+      dg.add_int32(frame_number);
+      frame_data->write_datagram(dg);
+    }
+    ++frame_number;
+  }
+  dg.add_int32(-1);
+}
+
+/**
+ * Restores the thread data from a datagram.
+ */
+void PStatThreadData::
+read_datagram(DatagramIterator &scan) {
+  int frame_number;
+  while ((frame_number = scan.get_int32()) != -1) {
+    PStatFrameData *frame_data = new PStatFrameData;
+    frame_data->read_datagram(scan);
+
+    record_new_frame(frame_number, frame_data);
+  }
+
+  compute_elapsed_frames();
+}
+
+/**
+ * Computes the frame numbers returned by get_elapsed_frames().
+ */
+void PStatThreadData::
+compute_elapsed_frames() const {
   if (_frames.empty()) {
     // No frames in the data at all.
     _got_elapsed_frames = false;
-
-  } else {
+  }
+  else {
     _now_i = _frames.size() - 1;
     while (_now_i > 0 && _frames[_now_i] == nullptr) {
       _now_i--;
@@ -326,8 +357,8 @@ compute_elapsed_frames() {
     if (_now_i < 0) {
       // No frames have any real data.
       _got_elapsed_frames = false;
-
-    } else {
+    }
+    else {
       nassertv(_frames[_now_i] != nullptr);
 
       double now = _frames[_now_i]->get_end();

+ 12 - 6
pandatool/src/pstatserver/pStatThreadData.h

@@ -15,7 +15,8 @@
 #define PSTATTHREADDATA_H
 
 #include "pandatoolbase.h"
-
+#include "datagram.h"
+#include "datagramIterator.h"
 #include "referenceCount.h"
 
 #include "pdeque.h"
@@ -61,8 +62,12 @@ public:
 
   void record_new_frame(int frame_number, PStatFrameData *frame_data);
 
+  void write_datagram(Datagram &dg) const;
+  void read_datagram(DatagramIterator &scan);
+
 private:
-  void compute_elapsed_frames();
+  void compute_elapsed_frames() const;
+
   const PStatClientData *_client_data;
 
   typedef pdeque<PStatFrameData *> Frames;
@@ -70,10 +75,11 @@ private:
   int _first_frame_number;
   double _history;
 
-  bool _computed_elapsed_frames;
-  bool _got_elapsed_frames;
-  int _then_i;
-  int _now_i;
+  // Cached values, updated by compute_elapsed_frames().
+  mutable bool _computed_elapsed_frames;
+  mutable bool _got_elapsed_frames;
+  mutable int _then_i;
+  mutable int _now_i;
 
   static PStatFrameData _null_frame;
 };

+ 44 - 5
pandatool/src/pstatserver/pStatTimeline.cxx

@@ -76,6 +76,8 @@ PStatTimeline(PStatMonitor *monitor, int xsize, int ysize) :
 
   _start_time = _lowest_start_time;
   _target_start_time = _start_time;
+
+  monitor->_timelines.insert(this);
 }
 
 /**
@@ -83,6 +85,7 @@ PStatTimeline(PStatMonitor *monitor, int xsize, int ysize) :
  */
 PStatTimeline::
 ~PStatTimeline() {
+  _monitor->_timelines.erase(this);
 }
 
 /**
@@ -116,7 +119,7 @@ new_data(int thread_index, int frame_number) {
         _highest_end_time = frame_end;
       }
 
-      while (thread_index >= _threads.size()) {
+      while (thread_index >= (int)_threads.size()) {
         _threads_changed = true;
         if (_threads.size() == 0) {
           _threads.resize(1);
@@ -261,6 +264,42 @@ get_bar_tooltip(int row, int x) const {
   return std::string();
 }
 
+/**
+ * Writes the graph state to a datagram.
+ */
+void PStatTimeline::
+write_datagram(Datagram &dg) const {
+  dg.add_float64(_time_scale);
+  dg.add_float64(_start_time);
+  dg.add_float64(_lowest_start_time);
+  dg.add_float64(_highest_end_time);
+
+  PStatGraph::write_datagram(dg);
+}
+
+/**
+ * Restores the graph state from a datagram.
+ */
+void PStatTimeline::
+read_datagram(DatagramIterator &scan) {
+  _time_scale = scan.get_float64();
+  _start_time = scan.get_float64();
+  _lowest_start_time = scan.get_float64();
+  _highest_end_time = scan.get_float64();
+
+  _scroll_speed = 0.0;
+  _zoom_speed = 0.0;
+
+  _have_start_time = true;
+  _target_start_time = _start_time;
+  _target_time_scale = _time_scale;
+
+  PStatGraph::read_datagram(scan);
+
+  normal_guide_bars();
+  force_redraw();
+}
+
 /**
  * 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
@@ -324,12 +363,12 @@ force_redraw(int row, int from_x, int to_x) {
 
   for (size_t ti = 0; ti < _threads.size(); ++ti) {
     ThreadRow &thread_row = _threads[ti];
-    if (thread_row._row_offset > row) {
+    if ((int)thread_row._row_offset > row) {
       break;
     }
 
     int row_index = row - (int)thread_row._row_offset;
-    if (row_index < thread_row._rows.size()) {
+    if (row_index < (int)thread_row._rows.size()) {
       draw_row((int)ti, row_index, start_time, end_time);
     }
   }
@@ -644,12 +683,12 @@ find_bar(int row, int x, ColorBar &bar) const {
 
   for (size_t ti = 0; ti < _threads.size(); ++ti) {
     const ThreadRow &thread_row = _threads[ti];
-    if (thread_row._row_offset > row) {
+    if ((int)thread_row._row_offset > row) {
       break;
     }
 
     int row_index = row - (int)thread_row._row_offset;
-    if (row_index < thread_row._rows.size()) {
+    if (row_index < (int)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});

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

@@ -56,6 +56,9 @@ public:
 
   std::string get_bar_tooltip(int row, int x) const;
 
+  virtual void write_datagram(Datagram &dg) const final;
+  virtual void read_datagram(DatagramIterator &scan) final;
+
 protected:
   void changed_size(int xsize, int ysize);
   void force_redraw();

+ 4 - 4
pandatool/src/text-stats/textMonitor.cxx

@@ -121,8 +121,8 @@ new_data(int thread_index, int frame_number) {
       }
 
       const PStatFrameData &frame_data = thread_data->get_frame(frame_number);
-      int num_events = frame_data.get_num_events();
-      for (int i = 0; i < num_events; ++i) {
+      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);
         (*_outStream)
           << "{\"name\":\"" << client_data->get_collector_fullname(collector_index)
@@ -141,8 +141,8 @@ new_data(int thread_index, int frame_number) {
       if (_show_raw_data) {
         const PStatFrameData &frame_data = thread_data->get_frame(frame_number);
         (*_outStream) << "raw data:\n";
-        int num_events = frame_data.get_num_events();
-        for (int i = 0; i < num_events; ++i) {
+        size_t num_events = frame_data.get_num_events();
+        for (size_t i = 0; i < num_events; ++i) {
           // The iomanipulators are much too clumsy.
           char formatted[32];
           sprintf(formatted, "%15.06lf", frame_data.get_time(i));

+ 1 - 1
pandatool/src/text-stats/textStats.cxx

@@ -69,7 +69,7 @@ TextStats() {
  *
  */
 PStatMonitor *TextStats::
-make_monitor() {
+make_monitor(const NetAddress &address) {
 
   return new TextMonitor(this, _outFile, _show_raw_data, _json);
 }

+ 1 - 1
pandatool/src/text-stats/textStats.h

@@ -30,7 +30,7 @@ class TextStats : public ProgramBase, public PStatServer {
 public:
   TextStats();
 
-  virtual PStatMonitor *make_monitor();
+  virtual PStatMonitor *make_monitor(const NetAddress &address);
 
   void run();
 

+ 1 - 1
pandatool/src/win-stats/CMakeLists.txt

@@ -33,7 +33,7 @@ set(WINSTATS_SOURCES
 
 composite_sources(win-stats WINSTATS_SOURCES)
 add_executable(win-stats ${WINSTATS_HEADERS} ${WINSTATS_SOURCES})
-target_link_libraries(win-stats p3progbase p3pstatserver comctl32.lib)
+target_link_libraries(win-stats p3progbase p3pstatserver comctl32.lib uxtheme.lib)
 
 # This program is NOT actually called win-stats. It's just pstats.exe
 set_target_properties(win-stats PROPERTIES OUTPUT_NAME "pstats")

+ 3 - 81
pandatool/src/win-stats/winStats.cxx

@@ -26,66 +26,6 @@
 // Enable common controls version 6, necessary for modern visual styles
 #pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
 
-static const char *toplevel_class_name = "pstats";
-static WinStatsServer *server = nullptr;
-
-/**
- *
- */
-static LONG WINAPI
-toplevel_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
-  switch (msg) {
-  case WM_TIMER:
-    server->poll();
-    break;
-
-  case WM_DESTROY:
-    PostQuitMessage(0);
-    break;
-
-  default:
-    break;
-  }
-
-  return DefWindowProc(hwnd, msg, wparam, lparam);
-}
-
-
-/**
- * Creates the initial, toplevel window for the application.
- */
-static HWND
-create_toplevel_window(HINSTANCE application) {
-  WNDCLASS wc;
-
-  ZeroMemory(&wc, sizeof(WNDCLASS));
-  wc.lpfnWndProc = (WNDPROC)toplevel_window_proc;
-  wc.hInstance = application;
-  wc.lpszClassName = toplevel_class_name;
-
-  if (!RegisterClass(&wc)) {
-    nout << "Could not register window class!\n";
-    exit(1);
-  }
-
-  DWORD window_style = WS_POPUP | WS_SYSMENU | WS_ICONIC;
-
-  std::ostringstream strm;
-  strm << "PStats " << pstats_port;
-  std::string window_name = strm.str();
-
-  HWND toplevel_window =
-    CreateWindow(toplevel_class_name, window_name.c_str(), window_style,
-                 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
-                 nullptr, nullptr, application, 0);
-  if (!toplevel_window) {
-    nout << "Could not create toplevel window!\n";
-    exit(1);
-  }
-
-  return toplevel_window;
-}
-
 int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
   // Initialize commctl32.dll.
   INITCOMMONCONTROLSEX icc;
@@ -96,27 +36,9 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) {
   // Signal DPI awareness.
   SetProcessDPIAware();
 
-  HINSTANCE application = GetModuleHandle(nullptr);
-  HWND toplevel_window = create_toplevel_window(application);
-
-  ShowWindow(toplevel_window, SW_SHOWMINIMIZED);
-
-  // Create the server object.
-  server = new WinStatsServer;
-  if (!server->listen()) {
-    std::ostringstream stream;
-    stream
-      << "Unable to open port " << pstats_port
-      << ".  Try specifying a different\n"
-      << "port number using pstats-port in your Config file.";
-    std::string str = stream.str();
-    MessageBox(toplevel_window, str.c_str(), "PStats error",
-               MB_OK | MB_ICONEXCLAMATION);
-    exit(1);
-  }
-
-  // Set up a timer to poll the pstats every so often.
-  SetTimer(toplevel_window, 1, 200, nullptr);
+  // Create the server window.
+  WinStatsServer *server = new WinStatsServer;
+  server->new_session();
 
   // Now get lost in the Windows message loop.
   MSG msg;

+ 41 - 16
pandatool/src/win-stats/winStatsChartMenu.cxx

@@ -12,6 +12,7 @@
  */
 
 #include "winStatsChartMenu.h"
+#include "winStatsMenuId.h"
 #include "winStatsMonitor.h"
 
 /**
@@ -31,6 +32,7 @@ WinStatsChartMenu(WinStatsMonitor *monitor, int thread_index) :
  */
 WinStatsChartMenu::
 ~WinStatsChartMenu() {
+  DestroyMenu(_menu);
 }
 
 /**
@@ -99,14 +101,28 @@ do_update() {
 
   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);
+    {
+      WinStatsMonitor::MenuDef menu_def(_thread_index, -1, WinStatsMonitor::CT_timeline, 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 = "Timeline";
+      InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii);
+    }
 
-    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);
+    // And the piano roll (even though it's not very useful nowadays)
+    {
+      WinStatsMonitor::MenuDef menu_def(_thread_index, -1, WinStatsMonitor::CT_piano_roll, false);
+      int menu_id = _monitor->get_menu_id(menu_def);
+
+      mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID;
+      mii.fType = MFT_STRING;
+      mii.wID = menu_id;
+      mii.dwTypeData = "Piano Roll";
+      InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii);
+    }
 
     mii.fMask = MIIM_FTYPE;
     mii.fType = MFT_SEPARATOR;
@@ -141,19 +157,28 @@ do_update() {
     }
   }
 
-  // Also menu item for piano roll (following a separator).
-  mii.fMask = MIIM_FTYPE;
-  mii.fType = MFT_SEPARATOR;
-  InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii);
+  // For the main thread menu, also some options relating to all graph windows.
+  if (_thread_index == 0) {
+    mii.fMask = MIIM_FTYPE;
+    mii.fType = MFT_SEPARATOR;
+    InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii);
+
+    mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID;
+    mii.fType = MFT_STRING;
+    mii.wID = MI_graphs_close_all;
+    mii.dwTypeData = "Close All Graphs";
+    InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii);
 
-  {
-    WinStatsMonitor::MenuDef menu_def(_thread_index, -1, WinStatsMonitor::CT_piano_roll, false);
-    int menu_id = _monitor->get_menu_id(menu_def);
+    mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID;
+    mii.fType = MFT_STRING;
+    mii.wID = MI_graphs_reopen_default;
+    mii.dwTypeData = "Reopen Default Graphs";
+    InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii);
 
     mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID;
     mii.fType = MFT_STRING;
-    mii.wID = menu_id;
-    mii.dwTypeData = "Piano Roll";
+    mii.wID = MI_graphs_save_default;
+    mii.dwTypeData = "Save Current Layout as Default";
     InsertMenuItem(_menu, GetMenuItemCount(_menu), TRUE, &mii);
   }
 }

+ 56 - 2
pandatool/src/win-stats/winStatsFlameGraph.cxx

@@ -47,7 +47,7 @@ WinStatsFlameGraph(WinStatsMonitor *monitor, int thread_index,
   _average_check_box = 0;
 
   create_window();
-  clear_region();
+  force_redraw();
 }
 
 /**
@@ -117,7 +117,7 @@ set_time_units(int unit_mask) {
 
     RECT rect;
     GetClientRect(_window, &rect);
-    rect.left = _right_margin;
+    rect.bottom = _top_margin;
     InvalidateRect(_window, &rect, TRUE);
   }
 }
@@ -159,6 +159,22 @@ on_leave_label(int collector_index) {
   }
 }
 
+/**
+ * Changes the collector represented by this flame graph.  This may force a
+ * redraw.
+ */
+void WinStatsFlameGraph::
+set_collector_index(int collector_index) {
+  PStatFlameGraph::set_collector_index(collector_index);
+
+  if (is_title_unknown()) {
+    std::string window_title = get_title_text();
+    if (!is_title_unknown()) {
+      SetWindowText(_window, window_title.c_str());
+    }
+  }
+}
+
 /**
  * Calls update_guide_bars with parameters suitable to this kind of graph.
  */
@@ -176,6 +192,11 @@ normal_guide_bars() {
   }
 
   _guide_bars_changed = true;
+
+  RECT rect;
+  GetClientRect(_window, &rect);
+  rect.bottom = _top_margin;
+  InvalidateRect(_window, &rect, TRUE);
 }
 
 /**
@@ -276,6 +297,28 @@ animate(double time, double dt) {
   return PStatFlameGraph::animate(time, dt);
 }
 
+/**
+ * Returns the current window dimensions.
+ */
+bool WinStatsFlameGraph::
+get_window_state(int &x, int &y, int &width, int &height,
+                 bool &maximized, bool &minimized) const {
+  WinStatsGraph::get_window_state(x, y, width, height, maximized, minimized);
+  return true;
+}
+
+/**
+ * Called to restore the graph window to its previous dimensions.
+ */
+void WinStatsFlameGraph::
+set_window_state(int x, int y, int width, int height,
+                 bool maximized, bool minimized) {
+  WinStatsGraph::set_window_state(x, y, width, height, maximized, minimized);
+
+  // Set the state of the checkbox.
+  SendMessage(_average_check_box, BM_SETCHECK, get_average_mode() ? BST_CHECKED : BST_UNCHECKED, 0);
+}
+
 /**
  *
  */
@@ -316,6 +359,14 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
     case 103:
       WinStatsGraph::_monitor->open_flame_graph(get_thread_index(), _popup_index);
       return 0;
+
+    case 104:
+      WinStatsGraph::_monitor->choose_collector_color(_popup_index);
+      return 0;
+
+    case 105:
+      WinStatsGraph::_monitor->reset_collector_color(_popup_index);
+      return 0;
     }
     break;
 
@@ -433,6 +484,9 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
             }
             AppendMenu(popup, MF_STRING, 102, "Open Strip Chart");
             AppendMenu(popup, MF_STRING, 103, "Open Flame Graph");
+            AppendMenu(popup, MF_STRING | MF_SEPARATOR, 0, nullptr);
+            AppendMenu(popup, MF_STRING, 104, "Change Color...");
+            AppendMenu(popup, MF_STRING, 105, "Reset Color");
             TrackPopupMenu(popup, TPM_LEFTBUTTON, point.x, point.y, 0, _window, nullptr);
           }
         }

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

@@ -40,6 +40,8 @@ public:
   virtual void on_enter_label(int collector_index);
   virtual void on_leave_label(int collector_index);
 
+  void set_collector_index(int collector_index);
+
 protected:
   virtual void normal_guide_bars();
 
@@ -51,6 +53,11 @@ protected:
 
   virtual bool animate(double time, double dt);
 
+  virtual bool get_window_state(int &x, int &y, int &width, int &height,
+                                bool &maximized, bool &minimized) const;
+  virtual void set_window_state(int x, int y, int width, int height,
+                                bool maximized, bool minimized);
+
   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);

+ 88 - 0
pandatool/src/win-stats/winStatsGraph.cxx

@@ -14,6 +14,7 @@
 #include "winStatsGraph.h"
 #include "winStatsMonitor.h"
 #include "winStatsLabelStack.h"
+#include "winStatsServer.h"
 #include "trueClock.h"
 #include "convert_srgb.h"
 
@@ -281,6 +282,57 @@ animate(double time, double dt) {
   return false;
 }
 
+/**
+ * Returns the current window dimensions.
+ */
+void WinStatsGraph::
+get_window_state(int &x, int &y, int &width, int &height,
+                 bool &maximized, bool &minimized) const {
+  WinStatsServer *server = (WinStatsServer *)_monitor->get_server();
+  POINT client_origin = server->get_client_origin();
+  WINDOWPLACEMENT wp;
+  wp.length = sizeof(WINDOWPLACEMENT);
+  GetWindowPlacement(_window, &wp);
+  x = wp.rcNormalPosition.left - client_origin.x;
+  y = wp.rcNormalPosition.top - client_origin.y;
+  width = wp.rcNormalPosition.right - wp.rcNormalPosition.left;
+  height = wp.rcNormalPosition.bottom - wp.rcNormalPosition.top;
+  maximized = (wp.showCmd == SW_SHOWMAXIMIZED || (wp.flags & WPF_RESTORETOMAXIMIZED) != 0);
+  minimized = (wp.showCmd == SW_SHOWMINIMIZED);
+}
+
+/**
+ * Called to restore the graph window to its previous dimensions.
+ */
+void WinStatsGraph::
+set_window_state(int x, int y, int width, int height,
+                 bool maximized, bool minimized) {
+  WinStatsServer *server = (WinStatsServer *)_monitor->get_server();
+  POINT client_origin = server->get_client_origin();
+  WINDOWPLACEMENT wp;
+  wp.length = sizeof(WINDOWPLACEMENT);
+  wp.flags = maximized ? WPF_RESTORETOMAXIMIZED : 0;
+  wp.showCmd = minimized ? SW_SHOWMINIMIZED : (maximized ? SW_SHOWMAXIMIZED : SW_SHOWNORMAL);
+  wp.ptMinPosition.x = -1;
+  wp.ptMinPosition.y = -1;
+  wp.ptMaxPosition.x = -1;
+  wp.ptMaxPosition.y = -1;
+  wp.rcNormalPosition.left = client_origin.x + x;
+  wp.rcNormalPosition.top = client_origin.y + y;
+  wp.rcNormalPosition.right = wp.rcNormalPosition.left + width;
+  wp.rcNormalPosition.bottom = wp.rcNormalPosition.top + height;
+
+  if (minimized) {
+    int x, y;
+    _monitor->calc_iconic_graph_window_pos(this, x, y);
+    wp.ptMinPosition.x = x;
+    wp.ptMinPosition.y = y;
+    wp.flags |= WPF_SETMINPOSITION;
+  }
+
+  SetWindowPlacement(_window, &wp);
+}
+
 /**
  * Returns a brush suitable for drawing in the indicated collector's color.
  */
@@ -331,6 +383,17 @@ get_collector_text_color(int collector_index, bool highlight) {
   return highlight ? hcolor : color;
 }
 
+/**
+ * Called when the given collector has changed colors.
+ */
+void WinStatsGraph::
+reset_collector_color(int collector_index) {
+  _brushes.erase(collector_index);
+  _text_colors.erase(collector_index);
+  force_redraw();
+  _label_stack.update_label_color(collector_index);
+}
+
 /**
  * This window_proc should be called up to by the derived classes for any
  * messages that are not specifically handled by the derived class.
@@ -356,6 +419,31 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
       return 0;
     }
 
+  case WM_WINDOWPOSCHANGING:
+    {
+      WINDOWPOS &pos = *(WINDOWPOS *)lparam;
+      if ((pos.flags & (SWP_NOMOVE | SWP_NOSIZE)) == 0 && IsIconic(hwnd)) {
+        _monitor->calc_iconic_graph_window_pos(this, pos.x, pos.y);
+      }
+    }
+    break;
+
+  case WM_SYSCOMMAND:
+    if (wparam == SC_MINIMIZE) {
+      WINDOWPLACEMENT wp;
+      if (GetWindowPlacement(hwnd, &wp)) {
+        int x, y;
+        _monitor->calc_iconic_graph_window_pos(this, x, y);
+        wp.showCmd = SW_SHOWMINIMIZED;
+        wp.flags |= WPF_SETMINPOSITION;
+        wp.ptMinPosition.x = x;
+        wp.ptMinPosition.y = y;
+        SetWindowPlacement(hwnd, &wp);
+      }
+      return 0;
+    }
+    break;
+
   case WM_SIZE:
     move_label_stack();
     InvalidateRect(hwnd, nullptr, TRUE);

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

@@ -23,6 +23,7 @@
 #endif
 #include <windows.h>
 
+class PStatGraph;
 class WinStatsMonitor;
 
 /**
@@ -67,6 +68,8 @@ public:
 
   HWND get_window();
 
+  void reset_collector_color(int collector_index);
+
 protected:
   void close();
 
@@ -76,6 +79,11 @@ protected:
   void start_animation();
   virtual bool animate(double time, double dt);
 
+  void get_window_state(int &x, int &y, int &width, int &height,
+                        bool &maximized, bool &minimized) const;
+  void set_window_state(int x, int y, int width, int height,
+                        bool maximized, bool minimized);
+
   HBRUSH get_collector_brush(int collector_index, bool highlight = false);
   COLORREF get_collector_text_color(int collector_index, bool highlight = false);
 

+ 48 - 29
pandatool/src/win-stats/winStatsLabel.cxx

@@ -42,35 +42,7 @@ WinStatsLabel(WinStatsMonitor *monitor, WinStatsGraph *graph,
   _tooltip_window(0)
 {
   update_text(use_fullname);
-
-  LRGBColor rgb = _monitor->get_collector_color(_collector_index);
-  int r = (int)encode_sRGB_uchar((float)rgb[0]);
-  int g = (int)encode_sRGB_uchar((float)rgb[1]);
-  int b = (int)encode_sRGB_uchar((float)rgb[2]);
-  _bg_brush = CreateSolidBrush(RGB(r, g, b));
-
-  // Calculate the color when it is highlighted.
-  int hr = (int)encode_sRGB_uchar((float)rgb[0] * 0.75f);
-  int hg = (int)encode_sRGB_uchar((float)rgb[1] * 0.75f);
-  int hb = (int)encode_sRGB_uchar((float)rgb[2] * 0.75f);
-  _highlight_bg_brush = CreateSolidBrush(RGB(hr, hg, hb));
-
-  // Should our foreground be black or white?
-  double bright =
-    rgb[0] * 0.2126 +
-    rgb[1] * 0.7152 +
-    rgb[2] * 0.0722;
-
-  if (bright >= 0.5) {
-    _fg_color = RGB(0, 0, 0);
-  } else {
-    _fg_color = RGB(255, 255, 255);
-  }
-  if (bright * 0.75 >= 0.5) {
-    _highlight_fg_color = RGB(0, 0, 0);
-  } else {
-    _highlight_fg_color = RGB(255, 255, 255);
-  }
+  update_color();
 
   _x = 0;
   _y = 0;
@@ -95,6 +67,7 @@ WinStatsLabel::
     _window = 0;
   }
   DeleteObject(_bg_brush);
+  DeleteObject(_highlight_bg_brush);
 }
 
 /**
@@ -147,6 +120,52 @@ set_highlight(bool highlight) {
   }
 }
 
+/**
+ * Updates the colors.
+ */
+void WinStatsLabel::
+update_color() {
+  if (_bg_brush != 0) {
+    DeleteObject(_bg_brush);
+  }
+  if (_highlight_bg_brush != 0) {
+    DeleteObject(_highlight_bg_brush);
+  }
+
+  LRGBColor rgb = _monitor->get_collector_color(_collector_index);
+  int r = (int)encode_sRGB_uchar((float)rgb[0]);
+  int g = (int)encode_sRGB_uchar((float)rgb[1]);
+  int b = (int)encode_sRGB_uchar((float)rgb[2]);
+  _bg_brush = CreateSolidBrush(RGB(r, g, b));
+
+  // Calculate the color when it is highlighted.
+  int hr = (int)encode_sRGB_uchar((float)rgb[0] * 0.75f);
+  int hg = (int)encode_sRGB_uchar((float)rgb[1] * 0.75f);
+  int hb = (int)encode_sRGB_uchar((float)rgb[2] * 0.75f);
+  _highlight_bg_brush = CreateSolidBrush(RGB(hr, hg, hb));
+
+  // Should our foreground be black or white?
+  double bright =
+    rgb[0] * 0.2126 +
+    rgb[1] * 0.7152 +
+    rgb[2] * 0.0722;
+
+  if (bright >= 0.5) {
+    _fg_color = RGB(0, 0, 0);
+  } else {
+    _fg_color = RGB(255, 255, 255);
+  }
+  if (bright * 0.75 >= 0.5) {
+    _highlight_fg_color = RGB(0, 0, 0);
+  } else {
+    _highlight_fg_color = RGB(255, 255, 255);
+  }
+
+  if (_window) {
+    InvalidateRect(_window, nullptr, TRUE);
+  }
+}
+
 /**
  * Set to true if the full name of the collector should be shown.
  */

+ 3 - 2
pandatool/src/win-stats/winStatsLabel.h

@@ -50,6 +50,7 @@ public:
   void set_highlight(bool highlight);
   INLINE bool get_highlight() const;
 
+  void update_color();
   void update_text(bool use_fullname);
 
 private:
@@ -71,8 +72,8 @@ private:
   HWND _tooltip_window;
   COLORREF _fg_color;
   COLORREF _highlight_fg_color;
-  HBRUSH _bg_brush;
-  HBRUSH _highlight_bg_brush;
+  HBRUSH _bg_brush = 0;
+  HBRUSH _highlight_bg_brush = 0;
 
   int _x;
   int _y;

+ 18 - 12
pandatool/src/win-stats/winStatsLabelStack.cxx

@@ -57,9 +57,7 @@ setup(HWND parent_window) {
   create_window(parent_window);
 
   _ideal_width = 0;
-  Labels::iterator li;
-  for (li = _labels.begin(); li != _labels.end(); ++li) {
-    WinStatsLabel *label = (*li);
+  for (WinStatsLabel *label : _labels) {
     label->setup(_window);
     _ideal_width = std::max(_ideal_width, label->get_ideal_width());
   }
@@ -85,10 +83,8 @@ set_pos(int x, int y, int width, int height) {
   SetWindowPos(_window, 0, x, y, _width, _height,
                SWP_NOZORDER | SWP_SHOWWINDOW);
 
-  Labels::iterator li;
   int yp = height;
-  for (li = _labels.begin(); li != _labels.end(); ++li) {
-    WinStatsLabel *label = (*li);
+  for (WinStatsLabel *label : _labels) {
     label->set_pos(0, yp, _width);
     yp -= label->get_height();
   }
@@ -167,9 +163,8 @@ get_label_collector_index(int label_index) const {
  */
 void WinStatsLabelStack::
 clear_labels() {
-  Labels::iterator li;
-  for (li = _labels.begin(); li != _labels.end(); ++li) {
-    delete (*li);
+  for (WinStatsLabel *label : _labels) {
+    delete label;
   }
   _labels.clear();
   _ideal_width = 0;
@@ -301,14 +296,25 @@ void WinStatsLabelStack::
 highlight_label(int collector_index) {
   if (_highlight_label != collector_index) {
     _highlight_label = collector_index;
-    Labels::iterator li;
-    for (li = _labels.begin(); li != _labels.end(); ++li) {
-      WinStatsLabel *label = (*li);
+
+    for (WinStatsLabel *label : _labels) {
       label->set_highlight(label->get_collector_index() == _highlight_label);
     }
   }
 }
 
+/**
+ * Refreshes the color of the label with the given index.
+ */
+void WinStatsLabelStack::
+update_label_color(int collector_index) {
+  for (WinStatsLabel *label : _labels) {
+    if (label->get_collector_index() == collector_index) {
+      label->update_color();
+    }
+  }
+}
+
 /**
  * Creates the window for this stack.
  */

+ 1 - 0
pandatool/src/win-stats/winStatsLabelStack.h

@@ -58,6 +58,7 @@ public:
   int get_num_labels() const;
 
   void highlight_label(int collector_index);
+  void update_label_color(int collector_index);
 
 private:
   void create_window(HWND parent_window);

+ 13 - 0
pandatool/src/win-stats/winStatsMenuId.h

@@ -22,6 +22,15 @@
  */
 enum WinStatsMenuId {
   MI_none,
+
+  MI_session_new,
+  MI_session_open,
+  MI_session_open_last,
+  MI_session_save,
+  MI_session_close,
+  MI_session_export_json,
+  MI_exit,
+
   MI_time_ms,
   MI_time_hz,
   MI_frame_rate_label,
@@ -32,6 +41,10 @@ enum WinStatsMenuId {
   MI_speed_12,
   MI_pause,
 
+  MI_graphs_close_all,
+  MI_graphs_reopen_default,
+  MI_graphs_save_default,
+
   // This one is last and represents the beginning of the range for the
   // various "new chart" menu options.
   MI_new_chart

+ 233 - 353
pandatool/src/win-stats/winStatsMonitor.cxx

@@ -23,45 +23,29 @@
 #include "pStatGraph.h"
 #include "pStatCollectorDef.h"
 
+#include "convert_srgb.h"
+
 #include <algorithm>
 
 #include <commctrl.h>
-
-bool WinStatsMonitor::_window_class_registered = false;
-const char * const WinStatsMonitor::_window_class_name = "monitor";
+#include <commdlg.h>
+#include <uxtheme.h>
 
 /**
  *
  */
 WinStatsMonitor::
 WinStatsMonitor(WinStatsServer *server) : PStatMonitor(server) {
-  _window = 0;
-  _menu_bar = 0;
-  _options_menu = 0;
+  _window = server->get_window();
+  _menu_bar = server->get_menu_bar();
+  _status_bar = server->get_status_bar();
 
   // These will be filled in later when the menu is created.
-  _time_units = 0;
   _scroll_speed = 0.0;
   _pause = false;
 
-  // Create the fonts used for rendering the UI.
-  NONCLIENTMETRICS metrics = {0};
-  metrics.cbSize = sizeof(NONCLIENTMETRICS);
-  if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, 0, &metrics, 0)) {
-    _font = CreateFontIndirect(&metrics.lfMenuFont);
-  } else {
-    _font = (HFONT)GetStockObject(ANSI_VAR_FONT);
-  }
-
-  HDC dc = GetDC(nullptr);
-  _pixel_scale = 0;
-  if (dc) {
-    _pixel_scale = GetDeviceCaps(dc, LOGPIXELSX) / (96 / 4);
-  }
-  if (_pixel_scale <= 0) {
-    _pixel_scale = 4;
-  }
-  ReleaseDC(nullptr, dc);
+  setup_speed_menu();
+  setup_frame_rate_label();
 }
 
 /**
@@ -69,27 +53,28 @@ WinStatsMonitor(WinStatsServer *server) : PStatMonitor(server) {
  */
 WinStatsMonitor::
 ~WinStatsMonitor() {
-  Graphs::iterator gi;
-  for (gi = _graphs.begin(); gi != _graphs.end(); ++gi) {
-    delete (*gi);
-  }
-  _graphs.clear();
+  close();
+}
 
-  ChartMenus::iterator mi;
-  for (mi = _chart_menus.begin(); mi != _chart_menus.end(); ++mi) {
-    delete (*mi);
-  }
-  _chart_menus.clear();
+/**
+ * Closes the client connection if it is active.
+ */
+void WinStatsMonitor::
+close() {
+  PStatMonitor::close();
+
+  remove_all_graphs();
+
+  RemoveMenu(_menu_bar, 2, MF_BYPOSITION);
+  RemoveMenu(_menu_bar, 2, MF_BYPOSITION);
 
-  if (_window) {
-    DestroyWindow(_window);
-    _window = 0;
+  for (WinStatsChartMenu *chart_menu : _chart_menus) {
+    RemoveMenu(_menu_bar, 2, MF_BYPOSITION);
+    delete chart_menu;
   }
+  _chart_menus.clear();
 
-#ifdef DEVELOP_WINSTATS
-  // For Winstats developers, exit when the first monitor closes.
-  exit(0);
-#endif
+  DrawMenuBar(_window);
 }
 
 /**
@@ -118,8 +103,6 @@ initialized() {
  */
 void WinStatsMonitor::
 got_hello() {
-  create_window();
-  open_strip_chart(0, 0, false);
 }
 
 /**
@@ -160,16 +143,13 @@ got_bad_version(int client_major, int client_minor,
  */
 void WinStatsMonitor::
 new_collector(int collector_index) {
-  Graphs::iterator gi;
-  for (gi = _graphs.begin(); gi != _graphs.end(); ++gi) {
-    WinStatsGraph *graph = (*gi);
+  for (WinStatsGraph *graph : _graphs) {
     graph->new_collector(collector_index);
   }
 
   // We might need to update our menus.
-  ChartMenus::iterator mi;
-  for (mi = _chart_menus.begin(); mi != _chart_menus.end(); ++mi) {
-    (*mi)->do_update();
+  for (WinStatsChartMenu *chart_menu : _chart_menus) {
+    chart_menu->do_update();
   }
 }
 
@@ -186,6 +166,10 @@ new_thread(int thread_index) {
   chart_menu->add_to_menu_bar(_menu_bar, MI_frame_rate_label);
   _chart_menus.push_back(chart_menu);
   DrawMenuBar(_window);
+
+  if (thread_index == 0) {
+    update_status_bar();
+  }
 }
 
 /**
@@ -203,6 +187,11 @@ new_data(int thread_index, int frame_number) {
   if (thread_index == 0) {
     update_status_bar();
   }
+
+  if (!_have_data) {
+    open_default_graphs();
+    _have_data = true;
+  }
 }
 
 /**
@@ -213,11 +202,6 @@ new_data(int thread_index, int frame_number) {
 void WinStatsMonitor::
 lost_connection() {
   nout << "Lost connection to " << get_client_hostname() << "\n";
-
-  if (_window) {
-    DestroyWindow(_window);
-    _window = 0;
-  }
 }
 
 /**
@@ -227,9 +211,8 @@ lost_connection() {
 void WinStatsMonitor::
 idle() {
   // Check if any of our chart menus need updating.
-  ChartMenus::iterator mi;
-  for (mi = _chart_menus.begin(); mi != _chart_menus.end(); ++mi) {
-    (*mi)->check_update();
+  for (WinStatsChartMenu *chart_menu : _chart_menus) {
+    chart_menu->check_update();
   }
 
   // Update the frame rate label from the main thread (thread 0).
@@ -268,9 +251,7 @@ has_idle() {
  */
 void WinStatsMonitor::
 user_guide_bars_changed() {
-  Graphs::iterator gi;
-  for (gi = _graphs.begin(); gi != _graphs.end(); ++gi) {
-    WinStatsGraph *graph = (*gi);
+  for (WinStatsGraph *graph : _graphs) {
     graph->user_guide_bars_changed();
   }
 }
@@ -288,7 +269,7 @@ get_window() const {
  */
 HFONT WinStatsMonitor::
 get_font() const {
-  return _font;
+  return ((WinStatsServer *)_server)->get_font();
 }
 
 /**
@@ -296,7 +277,7 @@ get_font() const {
  */
 int WinStatsMonitor::
 get_pixel_scale() const {
-  return _pixel_scale;
+  return ((WinStatsServer *)_server)->get_pixel_scale();
 }
 
 /**
@@ -304,64 +285,100 @@ get_pixel_scale() const {
  */
 POINT WinStatsMonitor::
 get_new_window_pos() {
-  int offset = _graphs.size() * 10 * _pixel_scale;
+  WinStatsServer *server = (WinStatsServer *)_server;
+  int offset = _graphs.size() * 10 * server->get_pixel_scale();
+  POINT client_origin = server->get_client_origin();
   POINT pt;
-  pt.x = offset + _client_origin.x;
-  pt.y = offset + _client_origin.y;
+  pt.x = offset + client_origin.x;
+  pt.y = offset + client_origin.y;
   return pt;
 }
 
+/**
+ * Opens a new timeline.
+ */
+PStatGraph *WinStatsMonitor::
+open_timeline() {
+  WinStatsTimeline *graph = new WinStatsTimeline(this);
+  add_graph(graph);
+  return graph;
+}
+
 /**
  * Opens a new strip chart showing the indicated data.
  */
-void WinStatsMonitor::
+PStatGraph *WinStatsMonitor::
 open_strip_chart(int thread_index, int collector_index, bool show_level) {
   WinStatsStripChart *graph =
     new WinStatsStripChart(this, thread_index, collector_index, show_level);
   add_graph(graph);
+  return graph;
+}
 
-  graph->set_time_units(_time_units);
-  graph->set_scroll_speed(_scroll_speed);
-  graph->set_pause(_pause);
+/**
+ * Opens a new flame graph showing the indicated data.
+ */
+PStatGraph *WinStatsMonitor::
+open_flame_graph(int thread_index, int collector_index) {
+  WinStatsFlameGraph *graph = new WinStatsFlameGraph(this, thread_index, collector_index);
+  add_graph(graph);
+  return graph;
 }
 
 /**
  * Opens a new piano roll showing the indicated data.
  */
-void WinStatsMonitor::
+PStatGraph *WinStatsMonitor::
 open_piano_roll(int thread_index) {
   WinStatsPianoRoll *graph = new WinStatsPianoRoll(this, thread_index);
   add_graph(graph);
-
-  graph->set_time_units(_time_units);
-  graph->set_scroll_speed(_scroll_speed);
-  graph->set_pause(_pause);
+  return graph;
 }
 
 /**
- * Opens a new flame graph showing the indicated data.
+ * Opens a dialog to change the given collector color.
  */
 void WinStatsMonitor::
-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);
+choose_collector_color(int collector_index) {
+  const LRGBColor &current = get_collector_color(collector_index);
+  static COLORREF custom_colors[16] = {0};
+
+  CHOOSECOLORA cc = {
+    sizeof(CHOOSECOLORA),
+    _window,
+    0,
+    RGB(encode_sRGB_uchar(current[0]), encode_sRGB_uchar(current[1]), encode_sRGB_uchar(current[2])),
+    (LPDWORD)custom_colors,
+    CC_FULLOPEN | CC_RGBINIT,
+    0,
+    nullptr,
+    nullptr,
+  };
+
+  if (ChooseColorA(&cc)) {
+    LRGBColor result(
+      decode_sRGB_float(GetRValue(cc.rgbResult)),
+      decode_sRGB_float(GetGValue(cc.rgbResult)),
+      decode_sRGB_float(GetBValue(cc.rgbResult)));
+
+    set_collector_color(collector_index, result);
+
+    for (WinStatsGraph *graph : _graphs) {
+      graph->reset_collector_color(collector_index);
+    }
+  }
 }
 
 /**
- * Opens a new timeline.
+ * Resets the color of the given collector to the default.
  */
 void WinStatsMonitor::
-open_timeline() {
-  WinStatsTimeline *graph = new WinStatsTimeline(this);
-  add_graph(graph);
+reset_collector_color(int collector_index) {
+  clear_collector_color(collector_index);
 
-  graph->set_time_units(_time_units);
-  graph->set_scroll_speed(_scroll_speed);
-  graph->set_pause(_pause);
+  for (WinStatsGraph *graph : _graphs) {
+    graph->reset_collector_color(collector_index);
+  }
 }
 
 /**
@@ -404,28 +421,9 @@ get_menu_id(const MenuDef &menu_def) {
  */
 void WinStatsMonitor::
 set_time_units(int unit_mask) {
-  _time_units = unit_mask;
-
-  // First, change all of the open graphs appropriately.
-  Graphs::iterator gi;
-  for (gi = _graphs.begin(); gi != _graphs.end(); ++gi) {
-    WinStatsGraph *graph = (*gi);
-    graph->set_time_units(_time_units);
+  for (WinStatsGraph *graph : _graphs) {
+    graph->set_time_units(unit_mask);
   }
-
-  // Now change the checkmark on the pulldown menu.
-  MENUITEMINFO mii;
-  memset(&mii, 0, sizeof(mii));
-  mii.cbSize = sizeof(mii);
-  mii.fMask = MIIM_STATE;
-
-  mii.fState = ((_time_units & PStatGraph::GBU_ms) != 0) ?
-    MFS_CHECKED : MFS_UNCHECKED;
-  SetMenuItemInfo(_options_menu, MI_time_ms, FALSE, &mii);
-
-  mii.fState = ((_time_units & PStatGraph::GBU_hz) != 0) ?
-    MFS_CHECKED : MFS_UNCHECKED;
-  SetMenuItemInfo(_options_menu, MI_time_hz, FALSE, &mii);
 }
 
 /**
@@ -437,9 +435,7 @@ set_scroll_speed(double scroll_speed) {
   _scroll_speed = scroll_speed;
 
   // First, change all of the open graphs appropriately.
-  Graphs::iterator gi;
-  for (gi = _graphs.begin(); gi != _graphs.end(); ++gi) {
-    WinStatsGraph *graph = (*gi);
+  for (WinStatsGraph *graph : _graphs) {
     graph->set_scroll_speed(_scroll_speed);
   }
 
@@ -478,9 +474,7 @@ set_pause(bool pause) {
   _pause = pause;
 
   // First, change all of the open graphs appropriately.
-  Graphs::iterator gi;
-  for (gi = _graphs.begin(); gi != _graphs.end(); ++gi) {
-    WinStatsGraph *graph = (*gi);
+  for (WinStatsGraph *graph : _graphs) {
     graph->set_pause(_pause);
   }
 
@@ -500,6 +494,10 @@ set_pause(bool pause) {
 void WinStatsMonitor::
 add_graph(WinStatsGraph *graph) {
   _graphs.insert(graph);
+
+  graph->set_time_units(((WinStatsServer *)_server)->get_time_units());
+  graph->set_scroll_speed(_scroll_speed);
+  graph->set_pause(_pause);
 }
 
 /**
@@ -515,91 +513,14 @@ remove_graph(WinStatsGraph *graph) {
 }
 
 /**
- * Creates the window for this monitor.
+ * Deletes all open graphs.
  */
 void WinStatsMonitor::
-create_window() {
-  if (_window) {
-    return;
-  }
-
-  HINSTANCE application = GetModuleHandle(nullptr);
-  register_window_class(application);
-
-  _menu_bar = CreateMenu();
-
-  setup_options_menu();
-  setup_speed_menu();
-  setup_frame_rate_label();
-
-  ChartMenus::iterator mi;
-  for (mi = _chart_menus.begin(); mi != _chart_menus.end(); ++mi) {
-    (*mi)->add_to_menu_bar(_menu_bar, MI_frame_rate_label);
-  }
-
-  _window_title = get_client_progname() + " on " + get_client_hostname();
-  DWORD window_style = WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN |
-    WS_CLIPSIBLINGS | WS_VISIBLE;
-
-  _window =
-    CreateWindow(_window_class_name, _window_title.c_str(), window_style,
-                 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
-                 nullptr, _menu_bar, application, 0);
-  if (!_window) {
-    nout << "Could not create monitor window!\n";
-    exit(1);
+remove_all_graphs() {
+  for (WinStatsGraph *graph : _graphs) {
+    delete graph;
   }
-
-  SetWindowLongPtr(_window, 0, (LONG_PTR)this);
-
-  create_status_bar(application);
-
-  // For some reason, SW_SHOWNORMAL doesn't always work, but SW_RESTORE seems
-  // to.
-  ShowWindow(_window, SW_RESTORE);
-  SetForegroundWindow(_window);
-
-  _client_origin.x = 0;
-  _client_origin.y = 0;
-  ClientToScreen(_window, &_client_origin);
-}
-
-/**
- * Creates the "Options" pulldown menu.
- */
-void WinStatsMonitor::
-setup_options_menu() {
-  _options_menu = CreatePopupMenu();
-
-  MENUITEMINFO mii;
-  memset(&mii, 0, sizeof(mii));
-  mii.cbSize = sizeof(mii);
-
-  mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_SUBMENU;
-  mii.fType = MFT_STRING;
-  mii.hSubMenu = _options_menu;
-
-  // One day, when there is more than one option here, we will actually
-  // present this to the user as the "Options" menu.  For now, the only option
-  // we have is time units.  mii.dwTypeData = "Options";
-  mii.dwTypeData = "Units";
-  InsertMenuItem(_menu_bar, GetMenuItemCount(_menu_bar), TRUE, &mii);
-
-
-  mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID | MIIM_CHECKMARKS | MIIM_STATE;
-  mii.fType = MFT_STRING | MFT_RADIOCHECK;
-  mii.hbmpChecked = nullptr;
-  mii.hbmpUnchecked = nullptr;
-  mii.fState = MFS_UNCHECKED;
-  mii.wID = MI_time_ms;
-  mii.dwTypeData = "ms";
-  InsertMenuItem(_options_menu, GetMenuItemCount(_options_menu), TRUE, &mii);
-
-  mii.wID = MI_time_hz;
-  mii.dwTypeData = "Hz";
-  InsertMenuItem(_options_menu, GetMenuItemCount(_options_menu), TRUE, &mii);
-
-  set_time_units(PStatGraph::GBU_ms);
+  _graphs.clear();
 }
 
 /**
@@ -678,26 +599,6 @@ setup_frame_rate_label() {
   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.
  */
@@ -758,8 +659,10 @@ update_status_bar() {
   HLOCAL hloc = LocalAlloc(LHND, sizeof(int) * (parts.size() + 1));
   PINT sizes = (PINT)LocalLock(hloc);
 
+  int pixel_scale = get_pixel_scale();
+
   // Allocate the left-most slot for the framerate indicator.
-  double offset = 28.0 * _pixel_scale;
+  double offset = 28.0 * pixel_scale;
   sizes[0] = (int)(offset + 0.5);
 
   if (!parts.empty()) {
@@ -769,11 +672,11 @@ update_status_bar() {
     double width_per_char = 0;
     GetClientRect(_status_bar, &rect);
     // Leave room for the grip.
-    rect.right -= _pixel_scale * 4;
+    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) {
+    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;
@@ -846,152 +749,89 @@ show_popup_menu(int collector) {
 }
 
 /**
- * Registers the window class for the monitor window, if it has not already
- * been registered.
+ * Called when a graph window is iconified or moved in the iconified state.
  */
 void WinStatsMonitor::
-register_window_class(HINSTANCE application) {
-  if (_window_class_registered) {
-    return;
-  }
+calc_iconic_graph_window_pos(WinStatsGraph *moved_graph, int &x, int &y) {
+  MINIMIZEDMETRICS metrics;
+  metrics.cbSize = sizeof(metrics);
+  SystemParametersInfoA(SPI_GETMINIMIZEDMETRICS, 0, &metrics, 0);
 
-  WNDCLASS wc;
+  int height = GetThemeSysSize(nullptr, SM_CYSIZE) + GetThemeSysSize(nullptr, SM_CXPADDEDBORDER) * 2;
 
-  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;
+  RECT client_rect;
+  GetClientRect(_window, &client_rect);
+  MapWindowPoints(_window, nullptr, (POINT *)&client_rect, 2);
 
-  // Reserve space to associate the this pointer with the window.
-  wc.cbWndExtra = sizeof(WinStatsMonitor *);
+  // Remove the status bar from the client rectangle.
+  RECT status_bar_rect;
+  GetWindowRect(_status_bar, &status_bar_rect);
+  client_rect.bottom -= (status_bar_rect.bottom - status_bar_rect.top);
 
-  if (!RegisterClass(&wc)) {
-    nout << "Could not register monitor window class!\n";
-    exit(1);
-  }
+  int iconic_offset = 0;
 
-  _window_class_registered = true;
+  for (WinStatsGraph *graph : _graphs) {
+    RECT child_rect;
+    HWND window = graph->get_window();
+    if (graph == moved_graph) {
+      x = client_rect.left + iconic_offset;
+      y = client_rect.bottom - height;
+      iconic_offset += metrics.iWidth + metrics.iHorzGap;
+    }
+    else if (IsIconic(window) && GetWindowRect(window, &child_rect)) {
+      // 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 - height;
+      iconic_offset += metrics.iWidth + metrics.iHorzGap;
+
+      SetWindowPos(window, 0, child_rect.left, child_rect.top, 0, 0,
+                   SWP_NOOWNERZORDER | SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE | SWP_NOSENDCHANGING);
+    }
+  }
 }
 
 /**
- *
+ * Called when the server window is moved.
  */
-LONG WINAPI WinStatsMonitor::
-static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
-  WinStatsMonitor *self = (WinStatsMonitor *)GetWindowLongPtr(hwnd, 0);
-  if (self != nullptr && self->_window == hwnd) {
-    return self->window_proc(hwnd, msg, wparam, lparam);
-  } else {
-    return DefWindowProc(hwnd, msg, wparam, lparam);
+void WinStatsMonitor::
+handle_window_moved(const RECT &client_rect, int delta_x, int delta_y) {
+  if (_graphs.empty()) {
+    return;
   }
-}
 
-/**
- *
- */
-LONG WinStatsMonitor::
-window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
-  switch (msg) {
-  case WM_DESTROY:
-    close();
-    break;
+  MINIMIZEDMETRICS metrics;
+  metrics.cbSize = sizeof(metrics);
+  SystemParametersInfoA(SPI_GETMINIMIZEDMETRICS, 0, &metrics, 0);
 
-  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;
+  int height = GetThemeSysSize(nullptr, SM_CYSIZE) + GetThemeSysSize(nullptr, SM_CXPADDEDBORDER) * 2;
 
-  case WM_SIZE:
-    if (_status_bar) {
-      SendMessage(_status_bar, WM_SIZE, 0, 0);
-      update_status_bar();
-    }
-    break;
+  int iconic_offset = 0;
 
-  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);
-
-        // Also open a strip chart for other threads with data for this
-        // collector.
-        const PStatClientData *client_data = get_client_data();
-        for (int thread_index = 1; thread_index < client_data->get_num_threads(); ++thread_index) {
-          PStatView &view = get_level_view(collector, thread_index);
-          if (view.get_net_value() > 0.0) {
-            open_strip_chart(thread_index, 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);
+  for (WinStatsGraph *graph : _graphs) {
+    WINDOWPLACEMENT wp;
+    HWND window = graph->get_window();
+    if (GetWindowPlacement(window, &wp)) {
+      if (wp.showCmd == SW_SHOWMINIMIZED) {
+        // Keep the minimized window title bar glued to the bottom-left
+        // corner of the parent window.
+        wp.flags |= WPF_SETMINPOSITION;
+        wp.ptMinPosition.x = client_rect.left + iconic_offset;
+        wp.ptMinPosition.y = client_rect.bottom - height;
+        iconic_offset += metrics.iWidth + metrics.iHorzGap;
       }
-    }
-    break;
 
-  case WM_COMMAND:
-    if (HIWORD(wparam) <= 1) {
-      int menu_id = LOWORD(wparam);
-      handle_menu_command(menu_id);
-      return 0;
+      // Move the "restored" window (even when it's currently min/maximized).
+      wp.rcNormalPosition.left += delta_x;
+      wp.rcNormalPosition.top += delta_y;
+      wp.rcNormalPosition.right += delta_x;
+      wp.rcNormalPosition.bottom += delta_y;
+      SetWindowPlacement(window, &wp);
     }
-    break;
-
-  default:
-    break;
   }
-
-  return DefWindowProc(hwnd, msg, wparam, lparam);
 }
 
 /**
- *
+ * Called when a menu item is clicked.
  */
 void WinStatsMonitor::
 handle_menu_command(int menu_id) {
@@ -999,14 +839,6 @@ handle_menu_command(int menu_id) {
   case MI_none:
     break;
 
-  case MI_time_ms:
-    set_time_units(PStatGraph::GBU_ms);
-    break;
-
-  case MI_time_hz:
-    set_time_units(PStatGraph::GBU_hz);
-    break;
-
   case MI_speed_1:
     set_scroll_speed(1);
     break;
@@ -1031,6 +863,19 @@ handle_menu_command(int menu_id) {
     set_pause(!_pause);
     break;
 
+  case MI_graphs_close_all:
+    remove_all_graphs();
+    break;
+
+  case MI_graphs_reopen_default:
+    remove_all_graphs();
+    open_default_graphs();
+    break;
+
+  case MI_graphs_save_default:
+    save_default_graphs();
+    break;
+
   default:
     if (menu_id >= MI_new_chart) {
       const MenuDef &menu_def = lookup_menu(menu_id);
@@ -1055,3 +900,38 @@ handle_menu_command(int menu_id) {
     }
   }
 }
+
+/**
+ * Called when a status bar item is double-clicked.
+ */
+void WinStatsMonitor::
+handle_status_bar_click(int item) {
+  if (item == 0) {
+    open_strip_chart(0, 0, false);
+  }
+  else if (item >= 1 && item <= _status_bar_collectors.size()) {
+    int collector = _status_bar_collectors[item - 1];
+    open_strip_chart(0, collector, true);
+
+    // Also open a strip chart for other threads with data for this
+    // collector.
+    const PStatClientData *client_data = get_client_data();
+    for (int thread_index = 1; thread_index < client_data->get_num_threads(); ++thread_index) {
+      PStatView &view = get_level_view(collector, thread_index);
+      if (view.get_net_value() > 0.0) {
+        open_strip_chart(thread_index, collector, true);
+      }
+    }
+  }
+}
+
+/**
+ * Called when a status bar item is right-clicked.
+ */
+void WinStatsMonitor::
+handle_status_bar_popup(int item) {
+  if (item >= 1 && item <= _status_bar_collectors.size()) {
+    int collector = _status_bar_collectors[item - 1];
+    show_popup_menu(collector);
+  }
+}

+ 17 - 20
pandatool/src/win-stats/winStatsMonitor.h

@@ -59,6 +59,8 @@ public:
   WinStatsMonitor(WinStatsServer *server);
   virtual ~WinStatsMonitor();
 
+  void close();
+
   virtual std::string get_monitor_name();
 
   virtual void initialized();
@@ -79,10 +81,13 @@ public:
   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_piano_roll(int thread_index);
-  void open_flame_graph(int thread_index, int collector_index = -1);
-  void open_timeline();
+  PStatGraph *open_timeline();
+  PStatGraph *open_strip_chart(int thread_index, int collector_index, bool show_level);
+  PStatGraph *open_flame_graph(int thread_index, int collector_index = -1);
+  PStatGraph *open_piano_roll(int thread_index);
+
+  void choose_collector_color(int collector_index);
+  void reset_collector_color(int collector_index);
 
   const MenuDef &lookup_menu(int menu_id) const;
   int get_menu_id(const MenuDef &menu_def);
@@ -91,22 +96,21 @@ public:
   void set_scroll_speed(double scroll_speed);
   void set_pause(bool pause);
 
-private:
   void add_graph(WinStatsGraph *graph);
   void remove_graph(WinStatsGraph *graph);
+  void remove_all_graphs();
 
-  void create_window();
-  void setup_options_menu();
+private:
   void setup_speed_menu();
   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 LONG WINAPI static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
-  LONG window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
+  void calc_iconic_graph_window_pos(WinStatsGraph *graph, int &x, int &y);
+  void handle_window_moved(const RECT &client_rect, int delta_x, int delta_y);
   void handle_menu_command(int menu_id);
+  void handle_status_bar_click(int item);
+  void handle_status_bar_popup(int item);
 
   typedef pset<WinStatsGraph *> Graphs;
   Graphs _graphs;
@@ -121,23 +125,16 @@ private:
 
   HWND _window;
   HMENU _menu_bar;
-  HMENU _options_menu;
   HMENU _speed_menu;
   HWND _status_bar;
-  POINT _client_origin;
   pvector<int> _status_bar_collectors;
   std::string _window_title;
-  int _time_units;
   double _scroll_speed;
   bool _pause;
-  int _pixel_scale;
-
-  HFONT _font;
-
-  static bool _window_class_registered;
-  static const char * const _window_class_name;
+  bool _have_data = false;
 
   friend class WinStatsGraph;
+  friend class WinStatsServer;
 };
 
 #include "winStatsMonitor.I"

+ 32 - 1
pandatool/src/win-stats/winStatsPianoRoll.cxx

@@ -40,7 +40,8 @@ WinStatsPianoRoll(WinStatsMonitor *monitor, int thread_index) :
   set_guide_bar_units(get_guide_bar_units() | GBU_show_units);
 
   create_window();
-  clear_region();
+  force_redraw();
+  idle();
 }
 
 /**
@@ -126,6 +127,9 @@ on_popup_label(int collector_index) {
     }
     AppendMenu(popup, MF_STRING, 102, "Open Strip Chart");
     AppendMenu(popup, MF_STRING, 103, "Open Flame Graph");
+    AppendMenu(popup, MF_STRING | MF_SEPARATOR, 0, nullptr);
+    AppendMenu(popup, MF_STRING, 104, "Change Color...");
+    AppendMenu(popup, MF_STRING, 105, "Reset Color");
     TrackPopupMenu(popup, TPM_LEFTBUTTON, point.x, point.y, 0, _window, nullptr);
   }
 }
@@ -232,6 +236,25 @@ idle() {
   }
 }
 
+/**
+ * Returns the current window dimensions.
+ */
+bool WinStatsPianoRoll::
+get_window_state(int &x, int &y, int &width, int &height,
+                 bool &maximized, bool &minimized) const {
+  WinStatsGraph::get_window_state(x, y, width, height, maximized, minimized);
+  return true;
+}
+
+/**
+ * Called to restore the graph window to its previous dimensions.
+ */
+void WinStatsPianoRoll::
+set_window_state(int x, int y, int width, int height,
+                 bool maximized, bool minimized) {
+  WinStatsGraph::set_window_state(x, y, width, height, maximized, minimized);
+}
+
 /**
  *
  */
@@ -255,6 +278,14 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
     case 103:
       WinStatsGraph::_monitor->open_flame_graph(get_thread_index(), _popup_index);
       return 0;
+
+    case 104:
+      WinStatsGraph::_monitor->choose_collector_color(_popup_index);
+      return 0;
+
+    case 105:
+      WinStatsGraph::_monitor->reset_collector_color(_popup_index);
+      return 0;
     }
     break;
 

+ 5 - 0
pandatool/src/win-stats/winStatsPianoRoll.h

@@ -55,6 +55,11 @@ protected:
   virtual void end_draw();
   virtual void idle();
 
+  virtual bool get_window_state(int &x, int &y, int &width, int &height,
+                                bool &maximized, bool &minimized) const;
+  virtual void set_window_state(int x, int y, int width, int height,
+                                bool maximized, bool minimized);
+
   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);

+ 853 - 2
pandatool/src/win-stats/winStatsServer.cxx

@@ -12,12 +12,863 @@
  */
 
 #include "winStatsServer.h"
+#include "winStatsMenuId.h"
 #include "winStatsMonitor.h"
+#include "pandaVersion.h"
+#include "pStatGraph.h"
+#include "config_pstatclient.h"
+
+#include <commctrl.h>
+#include <commdlg.h>
+
+bool WinStatsServer::_window_class_registered = false;
+const char *const WinStatsServer::_window_class_name = "server";
+
+/**
+ *
+ */
+WinStatsServer::
+WinStatsServer() {
+  _last_session = Filename::expand_from(
+    "$USER_APPDATA/Panda3D-" PANDA_ABI_VERSION_STR "/last-session.pstats");
+  _last_session.set_binary();
+
+  _window = 0;
+  _menu_bar = 0;
+  _options_menu = 0;
+
+  _time_units = 0;
+
+  // Create the fonts used for rendering the UI.
+  NONCLIENTMETRICS metrics = {0};
+  metrics.cbSize = sizeof(NONCLIENTMETRICS);
+  if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, 0, &metrics, 0)) {
+    _font = CreateFontIndirect(&metrics.lfMenuFont);
+  } else {
+    _font = (HFONT)GetStockObject(ANSI_VAR_FONT);
+  }
+
+  create_window();
+}
 
 /**
  *
  */
 PStatMonitor *WinStatsServer::
-make_monitor() {
-  return new WinStatsMonitor(this);
+make_monitor(const NetAddress &address) {
+  // Enable the "New Session", "Save Session" and "Close Session" menu items.
+  MENUITEMINFO mii;
+  memset(&mii, 0, sizeof(mii));
+  mii.cbSize = sizeof(mii);
+  mii.fMask = MIIM_STATE;
+  mii.fState = MFS_ENABLED;
+  SetMenuItemInfoA(_session_menu, MI_session_new, FALSE, &mii);
+  SetMenuItemInfoA(_session_menu, MI_session_save, FALSE, &mii);
+  SetMenuItemInfoA(_session_menu, MI_session_close, FALSE, &mii);
+  SetMenuItemInfoA(_session_menu, MI_session_export_json, FALSE, &mii);
+
+  std::ostringstream strm;
+  strm << "PStats Server (connected to " << address << ")";
+  std::string title = strm.str();
+  SetWindowTextA(_window, title.c_str());
+
+  _monitor = new WinStatsMonitor(this);
+  return _monitor;
+}
+
+/**
+ * Called when connection has been lost.
+ */
+void WinStatsServer::
+lost_connection(PStatMonitor *monitor) {
+  // Store a backup now, in case PStats crashes or something.
+  _last_session.make_dir();
+  if (monitor->write(_last_session)) {
+    nout << "Wrote to " << _last_session << "\n";
+  } else {
+    nout << "Failed to write to " << _last_session << "\n";
+  }
+
+  stop_listening();
+
+  SetWindowTextA(_window, "PStats Server (disconnected)");
+}
+
+/**
+ * Starts a new session.
+ */
+bool WinStatsServer::
+new_session() {
+  if (!close_session()) {
+    return false;
+  }
+
+  if (listen()) {
+    {
+      std::ostringstream strm;
+      strm << "PStats Server (listening on port " << pstats_port << ")";
+      std::string title = strm.str();
+      SetWindowTextA(_window, title.c_str());
+    }
+    {
+      std::ostringstream strm;
+      strm << "Waiting for client to connect on port " << pstats_port << "...";
+      std::string title = strm.str();
+      int part = -1;
+      SendMessage(_status_bar, SB_SETPARTS, 1, (LPARAM)&part);
+      SendMessage(_status_bar, WM_SETTEXT, 0, (LPARAM)title.c_str());
+    }
+
+    // Disable the "New Session" menu item.
+    MENUITEMINFO mii;
+    memset(&mii, 0, sizeof(mii));
+    mii.cbSize = sizeof(mii);
+    mii.fMask = MIIM_STATE;
+    mii.fState = MFS_DISABLED;
+    SetMenuItemInfoA(_session_menu, MI_session_new, FALSE, &mii);
+
+    // Disable the "Save Session" menu item.
+    mii.fState = MFS_DISABLED;
+    SetMenuItemInfoA(_session_menu, MI_session_save, FALSE, &mii);
+
+    // Enable the "Close Session" menu item.
+    mii.fState = MFS_ENABLED;
+    SetMenuItemInfoA(_session_menu, MI_session_close, FALSE, &mii);
+
+    // Disable the "Export Session" menu item.
+    mii.fState = MFS_DISABLED;
+    SetMenuItemInfoA(_session_menu, MI_session_export_json, FALSE, &mii);
+
+    return true;
+  }
+
+  SetWindowTextA(_window, "PStats Server");
+
+  std::ostringstream stream;
+  stream
+    << "Unable to open port " << pstats_port
+    << ".  Try specifying a different\n"
+    << "port number using pstats-port in your Config file.";
+  std::string str = stream.str();
+  MessageBox(_window, str.c_str(), "PStats Error",
+             MB_OK | MB_ICONEXCLAMATION);
+  return false;
+}
+
+/**
+ * Offers to open an existing session.
+ */
+bool WinStatsServer::
+open_session() {
+  if (!close_session()) {
+    return false;
+  }
+
+  char buffer[4096];
+  buffer[0] = '\0';
+
+  OPENFILENAMEA ofn = {
+    sizeof(OPENFILENAMEA),
+    _window,
+    nullptr,
+    "PStats Session Files (*.pstats)\0*.pstats\0All Files (*.*)\0*.*\0",
+    nullptr,
+    0,
+    0,
+    buffer,
+    sizeof(buffer),
+    nullptr,
+    0,
+    nullptr,
+    "Open Session",
+    OFN_HIDEREADONLY | OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST,
+    0,
+  };
+
+  if (GetOpenFileNameA(&ofn)) {
+    Filename fn = Filename::from_os_specific(buffer);
+    fn.set_binary();
+
+    WinStatsMonitor *monitor = new WinStatsMonitor(this);
+    if (!monitor->read(fn)) {
+      delete monitor;
+
+      std::ostringstream stream;
+      stream << "Failed to load session file: " << fn;
+      std::string str = stream.str();
+      MessageBox(_window, str.c_str(), "PStats Error",
+                 MB_OK | MB_ICONEXCLAMATION);
+      return false;
+    }
+    _monitor = monitor;
+
+    // Enable the "New Session", "Save Session" and "Close Session" menu items.
+    MENUITEMINFO mii;
+    memset(&mii, 0, sizeof(mii));
+    mii.cbSize = sizeof(mii);
+    mii.fMask = MIIM_STATE;
+    mii.fState = MFS_ENABLED;
+    SetMenuItemInfoA(_session_menu, MI_session_new, FALSE, &mii);
+    SetMenuItemInfoA(_session_menu, MI_session_save, FALSE, &mii);
+    SetMenuItemInfoA(_session_menu, MI_session_close, FALSE, &mii);
+    SetMenuItemInfoA(_session_menu, MI_session_export_json, FALSE, &mii);
+
+    return true;
+  }
+
+  return false;
+}
+
+/**
+ * Opens the last session, if any.
+ */
+bool WinStatsServer::
+open_last_session() {
+  if (!close_session()) {
+    return false;
+  }
+
+  Filename fn = _last_session;
+  WinStatsMonitor *monitor = new WinStatsMonitor(this);
+  if (!monitor->read(fn)) {
+    delete monitor;
+
+    std::ostringstream stream;
+    stream << "Failed to load session file: " << fn;
+    std::string str = stream.str();
+    MessageBox(_window, str.c_str(), "PStats Error",
+               MB_OK | MB_ICONEXCLAMATION);
+    return false;
+  }
+  _monitor = monitor;
+
+  // Enable the "New Session", "Save Session" and "Close Session" menu items.
+  MENUITEMINFO mii;
+  memset(&mii, 0, sizeof(mii));
+  mii.cbSize = sizeof(mii);
+  mii.fMask = MIIM_STATE;
+  mii.fState = MFS_ENABLED;
+  SetMenuItemInfoA(_session_menu, MI_session_new, FALSE, &mii);
+  SetMenuItemInfoA(_session_menu, MI_session_save, FALSE, &mii);
+  SetMenuItemInfoA(_session_menu, MI_session_close, FALSE, &mii);
+  SetMenuItemInfoA(_session_menu, MI_session_export_json, FALSE, &mii);
+
+  // If the file contained no graphs, open the default graphs.
+  if (monitor->_graphs.empty()) {
+    monitor->open_default_graphs();
+  }
+
+  return true;
+}
+
+/**
+ * Offers to save the current session.
+ */
+bool WinStatsServer::
+save_session() {
+  nassertr_always(_monitor != nullptr, true);
+
+  char buffer[4096];
+  buffer[0] = '\0';
+
+  OPENFILENAMEA ofn = {
+    sizeof(OPENFILENAMEA),
+    _window,
+    0,
+    "PStats Session Files\0*.pstats\0",
+    nullptr,
+    0,
+    0,
+    buffer,
+    sizeof(buffer),
+    nullptr,
+    0,
+    nullptr,
+    "Save Session",
+    OFN_OVERWRITEPROMPT,
+    0,
+    0,
+    "pstats",
+    0,
+  };
+
+  if (GetSaveFileNameA(&ofn)) {
+    Filename fn = Filename::from_os_specific(buffer);
+    fn.set_binary();
+
+    if (!_monitor->write(fn)) {
+      std::ostringstream stream;
+      stream << "Failed to save session file: " << fn;
+      std::string str = stream.str();
+      MessageBox(_window, str.c_str(), "PStats Error",
+                 MB_OK | MB_ICONEXCLAMATION);
+      return false;
+    }
+    _monitor->get_client_data()->clear_dirty();
+    return true;
+  }
+
+  return false;
+}
+
+/**
+ * Offers to export the current session as a JSON file.
+ */
+bool WinStatsServer::
+export_session() {
+  nassertr_always(_monitor != nullptr, true);
+
+  char buffer[4096];
+  buffer[0] = '\0';
+
+  OPENFILENAMEA ofn = {
+    sizeof(OPENFILENAMEA),
+    _window,
+    0,
+    "JSON files\0*.json\0",
+    nullptr,
+    0,
+    0,
+    buffer,
+    sizeof(buffer),
+    nullptr,
+    0,
+    nullptr,
+    "Export Session",
+    OFN_OVERWRITEPROMPT,
+    0,
+    0,
+    "json",
+    0,
+  };
+
+  if (GetSaveFileNameA(&ofn)) {
+    Filename fn = Filename::from_os_specific(buffer);
+    fn.set_text();
+
+    std::ofstream stream;
+    if (!fn.open_write(stream)) {
+      std::ostringstream stream;
+      stream << "Failed to open file for export: " << fn;
+      std::string str = stream.str();
+      MessageBox(_window, str.c_str(), "PStats Error",
+                 MB_OK | MB_ICONEXCLAMATION);
+      return false;
+    }
+
+    int pid = _monitor->get_client_pid();
+    _monitor->get_client_data()->write_json(stream, std::max(0, pid));
+    stream.close();
+    return true;
+  }
+
+  return false;
+}
+
+/**
+ * Closes the current session.
+ */
+bool WinStatsServer::
+close_session() {
+  bool wrote_last_session = false;
+
+  if (_monitor != nullptr) {
+    const PStatClientData *client_data = _monitor->get_client_data();
+    if (client_data != nullptr && client_data->is_dirty()) {
+      if (!_monitor->has_read_filename()) {
+        _last_session.make_dir();
+        if (_monitor->write(_last_session)) {
+          nout << "Wrote to " << _last_session << "\n";
+          wrote_last_session = true;
+        }
+        else {
+          nout << "Failed to write to " << _last_session << "\n";
+        }
+      }
+
+      int result = MessageBox(_window,
+                              "Would you like to save the currently open session?",
+                              "Unsaved Data", MB_YESNOCANCEL | MB_ICONQUESTION);
+      if (result == IDCANCEL || (result == IDYES && !save_session())) {
+        return false;
+      }
+    }
+
+    _monitor->close();
+    _monitor = nullptr;
+  }
+
+  stop_listening();
+
+  SetWindowTextA(_window, "PStats Server");
+
+  int part = -1;
+  SendMessage(_status_bar, SB_SETPARTS, 1, (LPARAM)&part);
+  SendMessage(_status_bar, WM_SETTEXT, 0, (LPARAM)"");
+
+  // Enable the "New Session" menu item.
+  MENUITEMINFO mii;
+  memset(&mii, 0, sizeof(mii));
+  mii.cbSize = sizeof(mii);
+  mii.fMask = MIIM_STATE;
+  mii.fState = MFS_ENABLED;
+  SetMenuItemInfoA(_session_menu, MI_session_new, FALSE, &mii);
+
+  if (wrote_last_session) {
+    // And the "Open Last Session" menu item.
+    mii.fState = MFS_ENABLED;
+    SetMenuItemInfoA(_session_menu, MI_session_open_last, FALSE, &mii);
+  }
+
+  // Disable the "Save Session" menu item.
+  mii.fState = MFS_DISABLED;
+  SetMenuItemInfoA(_session_menu, MI_session_save, FALSE, &mii);
+
+  // Disable the "Close Session" menu item.
+  mii.fState = MFS_DISABLED;
+  SetMenuItemInfoA(_session_menu, MI_session_close, FALSE, &mii);
+
+  // Disable the "Export Session" menu item.
+  mii.fState = MFS_DISABLED;
+  SetMenuItemInfoA(_session_menu, MI_session_export_json, FALSE, &mii);
+
+  return true;
+}
+
+/**
+ * Returns the window handle to the server's window.
+ */
+HWND WinStatsServer::
+get_window() const {
+  return _window;
+}
+
+/**
+ * Returns the menu handle to the server's menu bar.
+ */
+HMENU WinStatsServer::
+get_menu_bar() const {
+  return _menu_bar;
+}
+
+/**
+ * Returns the window handle to the server's status bar.
+ */
+HWND WinStatsServer::
+get_status_bar() const {
+  return _status_bar;
+}
+
+/**
+ * Returns the font that should be used for rendering text.
+ */
+HFONT WinStatsServer::
+get_font() const {
+  return _font;
+}
+
+/**
+ * Returns the system DPI scaling as a fraction where 4 = no scaling.
+ */
+int WinStatsServer::
+get_pixel_scale() const {
+  return _pixel_scale;
+}
+
+/**
+ * Returns the origin of the window's client area.
+ */
+POINT WinStatsServer::
+get_client_origin() const {
+  return _client_origin;
+}
+
+/**
+ *
+ */
+int WinStatsServer::
+get_time_units() const {
+  return _time_units;
+}
+
+
+/**
+ * Called when the user selects a new time units from the monitor pulldown
+ * menu, this should adjust the units for all graphs to the indicated mask if
+ * it is a time-based graph.
+ */
+void WinStatsServer::
+set_time_units(int unit_mask) {
+  _time_units = unit_mask;
+
+  if (_monitor != nullptr) {
+    _monitor->set_time_units(unit_mask);
+  }
+
+  // Now change the checkmark on the pulldown menu.
+  MENUITEMINFO mii;
+  memset(&mii, 0, sizeof(mii));
+  mii.cbSize = sizeof(mii);
+  mii.fMask = MIIM_STATE;
+
+  mii.fState = ((_time_units & PStatGraph::GBU_ms) != 0) ?
+    MFS_CHECKED : MFS_UNCHECKED;
+  SetMenuItemInfo(_options_menu, MI_time_ms, FALSE, &mii);
+
+  mii.fState = ((_time_units & PStatGraph::GBU_hz) != 0) ?
+    MFS_CHECKED : MFS_UNCHECKED;
+  SetMenuItemInfo(_options_menu, MI_time_hz, FALSE, &mii);
+}
+
+/**
+ * Creates the window for this server.
+ */
+void WinStatsServer::
+create_window() {
+  HINSTANCE application = GetModuleHandle(nullptr);
+  register_window_class(application);
+
+  _menu_bar = CreateMenu();
+
+  setup_session_menu();
+  setup_options_menu();
+
+  DWORD window_style = WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN |
+    WS_CLIPSIBLINGS | WS_VISIBLE;
+
+  _window =
+    CreateWindow(_window_class_name, "PStats Server", window_style,
+                 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
+                 nullptr, _menu_bar, application, 0);
+  if (!_window) {
+    nout << "Could not create server window!\n";
+    exit(1);
+  }
+
+  SetWindowLongPtr(_window, 0, (LONG_PTR)this);
+
+  create_status_bar(application);
+
+  // For some reason, SW_SHOWNORMAL doesn't always work, but SW_RESTORE seems
+  // to.
+  ShowWindow(_window, SW_RESTORE);
+  SetForegroundWindow(_window);
+
+  HDC dc = GetDC(_window);
+  _pixel_scale = 0;
+  if (dc) {
+    _pixel_scale = GetDeviceCaps(dc, LOGPIXELSX) / (96 / 4);
+  }
+  if (_pixel_scale <= 0) {
+    _pixel_scale = 4;
+  }
+  ReleaseDC(_window, dc);
+
+  _client_origin.x = 0;
+  _client_origin.y = 0;
+  ClientToScreen(_window, &_client_origin);
+
+  // Set up a timer to poll the pstats every so often.
+  SetTimer(_window, 1, 200, nullptr);
+}
+
+/**
+ * Creates the "Session" pulldown menu.
+ */
+void WinStatsServer::
+setup_session_menu() {
+  _session_menu = CreatePopupMenu();
+
+  MENUITEMINFO mii;
+  memset(&mii, 0, sizeof(mii));
+  mii.cbSize = sizeof(mii);
+
+  mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_SUBMENU;
+  mii.fType = MFT_STRING;
+  mii.hSubMenu = _session_menu;
+
+  mii.dwTypeData = "&Session";
+  InsertMenuItem(_menu_bar, GetMenuItemCount(_menu_bar), TRUE, &mii);
+
+  AppendMenu(_session_menu, MF_STRING, MI_session_new, "&New Session\tCtrl+N");
+  AppendMenu(_session_menu, MF_STRING, MI_session_open, "&Open Session...\tCtrl+O");
+
+  if (_last_session.exists()) {
+    AppendMenu(_session_menu, MF_STRING, MI_session_open_last, "Open &Last Session");
+  } else {
+    AppendMenu(_session_menu, MF_STRING | MF_DISABLED, MI_session_open_last, "Open &Last Session");
+  }
+
+  AppendMenu(_session_menu, MF_STRING | MF_DISABLED, MI_session_save, "&Save Session...\tCtrl+S");
+  AppendMenu(_session_menu, MF_STRING | MF_DISABLED, MI_session_close, "&Close Session\tCtrl+W");
+
+  AppendMenu(_session_menu, MF_SEPARATOR, 0, nullptr);
+  AppendMenu(_session_menu, MF_STRING | MF_DISABLED, MI_session_export_json, "&Export as JSON...");
+
+  AppendMenu(_session_menu, MF_SEPARATOR, 0, nullptr);
+  AppendMenu(_session_menu, MF_STRING, MI_exit, "E&xit");
+}
+
+/**
+ * Creates the "Options" pulldown menu.
+ */
+void WinStatsServer::
+setup_options_menu() {
+  _options_menu = CreatePopupMenu();
+
+  MENUITEMINFO mii;
+  memset(&mii, 0, sizeof(mii));
+  mii.cbSize = sizeof(mii);
+
+  mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_SUBMENU;
+  mii.fType = MFT_STRING;
+  mii.hSubMenu = _options_menu;
+
+  // One day, when there is more than one option here, we will actually
+  // present this to the user as the "Options" menu.  For now, the only option
+  // we have is time units.  mii.dwTypeData = "Options";
+  mii.dwTypeData = "Units";
+  InsertMenuItem(_menu_bar, GetMenuItemCount(_menu_bar), TRUE, &mii);
+
+  mii.fMask = MIIM_STRING | MIIM_FTYPE | MIIM_ID | MIIM_CHECKMARKS | MIIM_STATE;
+  mii.fType = MFT_STRING | MFT_RADIOCHECK;
+  mii.hbmpChecked = nullptr;
+  mii.hbmpUnchecked = nullptr;
+  mii.fState = MFS_UNCHECKED;
+  mii.wID = MI_time_ms;
+  mii.dwTypeData = "ms";
+  InsertMenuItem(_options_menu, GetMenuItemCount(_options_menu), TRUE, &mii);
+
+  mii.wID = MI_time_hz;
+  mii.dwTypeData = "Hz";
+  InsertMenuItem(_options_menu, GetMenuItemCount(_options_menu), TRUE, &mii);
+
+  set_time_units(PStatGraph::GBU_ms);
+}
+
+/**
+ * Sets up a status bar at the bottom of the screen showing assorted level
+ * values.
+ */
+void WinStatsServer::
+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);
+
+  ShowWindow(_status_bar, SW_SHOW);
+  UpdateWindow(_status_bar);
+
+  InvalidateRect(_status_bar, NULL, TRUE);
+}
+
+/**
+ * Registers the window class for the server window, if it has not already
+ * been registered.
+ */
+void WinStatsServer::
+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(WinStatsServer *);
+
+  if (!RegisterClass(&wc)) {
+    nout << "Could not register server window class!\n";
+    exit(1);
+  }
+
+  _window_class_registered = true;
+}
+
+/**
+ *
+ */
+LONG WINAPI WinStatsServer::
+static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
+  WinStatsServer *self = (WinStatsServer *)GetWindowLongPtr(hwnd, 0);
+  if (self != nullptr && self->_window == hwnd) {
+    return self->window_proc(hwnd, msg, wparam, lparam);
+  } else {
+    return DefWindowProc(hwnd, msg, wparam, lparam);
+  }
+}
+
+/**
+ *
+ */
+LONG WinStatsServer::
+window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
+  switch (msg) {
+  case WM_TIMER:
+    poll();
+    break;
+
+  case WM_CLOSE:
+    if (!close_session()) {
+      return 0;
+    }
+    break;
+
+  case WM_DESTROY:
+    PostQuitMessage(0);
+    break;
+
+  case WM_WINDOWPOSCHANGED:
+    {
+      RECT client_rect;
+      GetClientRect(_window, &client_rect);
+      MapWindowPoints(_window, nullptr, (POINT *)&client_rect, 2);
+
+      if (_monitor != nullptr) {
+        // Remove the status bar from the client rectangle.
+        RECT status_bar_rect;
+        GetWindowRect(_status_bar, &status_bar_rect);
+        client_rect.bottom -= (status_bar_rect.bottom - status_bar_rect.top);
+
+        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;
+
+        _monitor->handle_window_moved(client_rect, delta_x, delta_y);
+      }
+      else {
+        _client_origin.x = client_rect.left;
+        _client_origin.y = client_rect.top;
+      }
+    }
+    break;
+
+  case WM_SIZE:
+    if (_status_bar) {
+      SendMessage(_status_bar, WM_SIZE, 0, 0);
+      if (_monitor != nullptr) {
+        _monitor->update_status_bar();
+      }
+    }
+    break;
+
+  case WM_NOTIFY:
+    if (_monitor != nullptr) {
+      if (((LPNMHDR)lparam)->code == NM_DBLCLK) {
+        NMMOUSE &mouse = *(NMMOUSE *)lparam;
+        _monitor->handle_status_bar_click(mouse.dwItemSpec);
+        return TRUE;
+      }
+      else if (((LPNMHDR)lparam)->code == NM_RCLICK) {
+        NMMOUSE &mouse = *(NMMOUSE *)lparam;
+        _monitor->handle_status_bar_popup(mouse.dwItemSpec);
+        return TRUE;
+      }
+    }
+    break;
+
+  case WM_KEYDOWN:
+    if ((lparam & 0x40000000) == 0 && GetKeyState(VK_CONTROL) < 0) {
+      switch (wparam) {
+      case 'N':
+        new_session();
+        return 0;
+
+      case 'O':
+        open_session();
+        return 0;
+
+      case 'S':
+        save_session();
+        return 0;
+
+      case 'W':
+        close_session();
+        return 0;
+
+      default:
+        break;
+      }
+    }
+    break;
+
+  case WM_COMMAND:
+    if (HIWORD(wparam) <= 1) {
+      int menu_id = LOWORD(wparam);
+      handle_menu_command(menu_id);
+      return 0;
+    }
+    break;
+
+  default:
+    break;
+  }
+
+  return DefWindowProc(hwnd, msg, wparam, lparam);
+}
+
+/**
+ *
+ */
+void WinStatsServer::
+handle_menu_command(int menu_id) {
+  switch (menu_id) {
+  case MI_none:
+    break;
+
+  case MI_session_new:
+    new_session();
+    break;
+
+  case MI_session_open:
+    open_session();
+    break;
+
+  case MI_session_open_last:
+    open_last_session();
+    break;
+
+  case MI_session_save:
+    save_session();
+    break;
+
+  case MI_session_close:
+    close_session();
+    break;
+
+  case MI_session_export_json:
+    export_session();
+    break;
+
+  case MI_exit:
+    if (close_session()) {
+      exit(0);
+    }
+    break;
+
+  case MI_time_ms:
+    set_time_units(PStatGraph::GBU_ms);
+    break;
+
+  case MI_time_hz:
+    set_time_units(PStatGraph::GBU_hz);
+    break;
+
+  default:
+    if (_monitor != nullptr) {
+      _monitor->handle_menu_command(menu_id);
+    }
+    break;
+  }
 }

+ 51 - 1
pandatool/src/win-stats/winStatsServer.h

@@ -16,13 +16,63 @@
 
 #include "pandatoolbase.h"
 #include "pStatServer.h"
+#include "winStatsMonitor.h"
 
 /**
  * The class that owns the main loop, waiting for client connections.
  */
 class WinStatsServer : public PStatServer {
 public:
-  virtual PStatMonitor *make_monitor();
+  WinStatsServer();
+
+  virtual PStatMonitor *make_monitor(const NetAddress &address);
+  virtual void lost_connection(PStatMonitor *monitor);
+
+  bool new_session();
+  bool open_session();
+  bool open_last_session();
+  bool save_session();
+  bool export_session();
+  bool close_session();
+
+  HWND get_window() const;
+  HMENU get_menu_bar() const;
+  HWND get_status_bar() const;
+  HFONT get_font() const;
+  int get_pixel_scale() const;
+  POINT get_client_origin() const;
+
+  int get_time_units() const;
+  void set_time_units(int unit_mask);
+
+private:
+  void create_window();
+  void setup_session_menu();
+  void setup_options_menu();
+  void create_status_bar(HINSTANCE application);
+  static void register_window_class(HINSTANCE application);
+
+  static LONG WINAPI static_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
+  LONG window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);
+  void handle_menu_command(int menu_id);
+
+  PT(WinStatsMonitor) _monitor;
+
+  Filename _last_session;
+
+  HWND _window = 0;
+  HMENU _menu_bar = 0;
+  HMENU _session_menu = 0;
+  HMENU _options_menu = 0;
+  HWND _status_bar;
+  POINT _client_origin;
+  int _time_units;
+  int _pixel_scale;
+
+  HFONT _font;
+
+  static bool _window_class_registered;
+  static const char * const _window_class_name;
 };
 
 #endif

+ 43 - 3
pandatool/src/win-stats/winStatsStripChart.cxx

@@ -31,9 +31,7 @@ WinStatsStripChart::
 WinStatsStripChart(WinStatsMonitor *monitor, int thread_index,
                    int collector_index, bool show_level) :
   PStatStripChart(monitor,
-                  show_level ? monitor->get_level_view(0, thread_index) : monitor->get_view(thread_index),
-                  thread_index,
-                  collector_index,
+                  thread_index, collector_index, show_level,
                   monitor->get_pixel_scale() * default_strip_chart_width / 4,
                   monitor->get_pixel_scale() * default_strip_chart_height / 4),
   WinStatsGraph(monitor)
@@ -62,6 +60,8 @@ WinStatsStripChart(WinStatsMonitor *monitor, int thread_index,
 
   create_window();
   clear_region();
+
+  update();
 }
 
 /**
@@ -217,6 +217,9 @@ on_popup_label(int collector_index) {
     } else {
       AppendMenu(popup, MF_STRING, 103, "Open Flame Graph");
     }
+    AppendMenu(popup, MF_STRING | MF_SEPARATOR, 0, nullptr);
+    AppendMenu(popup, MF_STRING, 104, "Change Color...");
+    AppendMenu(popup, MF_STRING, 105, "Reset Color");
     TrackPopupMenu(popup, TPM_LEFTBUTTON, point.x, point.y, 0, _window, nullptr);
   }
 }
@@ -239,6 +242,13 @@ set_collector_index(int collector_index) {
   if (get_collector_index() != collector_index) {
     PStatStripChart::set_collector_index(collector_index);
 
+    if (is_title_unknown()) {
+      std::string window_title = get_title_text();
+      if (!is_title_unknown()) {
+        SetWindowText(_window, window_title.c_str());
+      }
+    }
+
     // Redraw the scale labels.
     RECT rect;
     GetClientRect(_window, &rect);
@@ -377,6 +387,28 @@ end_draw(int from_x, int to_x) {
   InvalidateRect(_graph_window, &rect, FALSE);
 }
 
+/**
+ * Returns the current window dimensions.
+ */
+bool WinStatsStripChart::
+get_window_state(int &x, int &y, int &width, int &height,
+                 bool &maximized, bool &minimized) const {
+  WinStatsGraph::get_window_state(x, y, width, height, maximized, minimized);
+  return true;
+}
+
+/**
+ * Called to restore the graph window to its previous dimensions.
+ */
+void WinStatsStripChart::
+set_window_state(int x, int y, int width, int height,
+                 bool maximized, bool minimized) {
+  WinStatsGraph::set_window_state(x, y, width, height, maximized, minimized);
+
+  // Set the state of the checkbox.
+  SendMessage(_smooth_check_box, BM_SETCHECK, get_average_mode() ? BST_CHECKED : BST_UNCHECKED, 0);
+}
+
 /**
  *
  */
@@ -413,6 +445,14 @@ window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
     case 103:
       WinStatsGraph::_monitor->open_flame_graph(get_thread_index(), _popup_index);
       return 0;
+
+    case 104:
+      WinStatsGraph::_monitor->choose_collector_color(_popup_index);
+      return 0;
+
+    case 105:
+      WinStatsGraph::_monitor->reset_collector_color(_popup_index);
+      return 0;
     }
     break;
 

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

@@ -61,6 +61,11 @@ protected:
   virtual void draw_cursor(int x);
   virtual void end_draw(int from_x, int to_x);
 
+  virtual bool get_window_state(int &x, int &y, int &width, int &height,
+                                bool &maximized, bool &minimized) const;
+  virtual void set_window_state(int x, int y, int width, int height,
+                                bool maximized, bool minimized);
+
   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);

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

@@ -247,6 +247,25 @@ animate(double time, double dt) {
   return PStatTimeline::animate(time, dt);
 }
 
+/**
+ * Returns the current window dimensions.
+ */
+bool WinStatsTimeline::
+get_window_state(int &x, int &y, int &width, int &height,
+                 bool &maximized, bool &minimized) const {
+  WinStatsGraph::get_window_state(x, y, width, height, maximized, minimized);
+  return true;
+}
+
+/**
+ * Called to restore the graph window to its previous dimensions.
+ */
+void WinStatsTimeline::
+set_window_state(int x, int y, int width, int height,
+                 bool maximized, bool minimized) {
+  WinStatsGraph::set_window_state(x, y, width, height, maximized, minimized);
+}
+
 /**
  *
  */
@@ -417,6 +436,9 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
             AppendMenu(popup, MF_STRING, 102, "Open Strip Chart");
             AppendMenu(popup, MF_STRING, 103, "Open Flame Graph");
             AppendMenu(popup, MF_STRING, 104, "Open Piano Roll");
+            AppendMenu(popup, MF_STRING | MF_SEPARATOR, 0, nullptr);
+            AppendMenu(popup, MF_STRING, 105, "Change Color...");
+            AppendMenu(popup, MF_STRING, 106, "Reset Color");
             TrackPopupMenu(popup, TPM_LEFTBUTTON, point.x, point.y, 0, _graph_window, nullptr);
           }
         }
@@ -447,6 +469,14 @@ graph_window_proc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) {
     case 104:
       WinStatsGraph::_monitor->open_piano_roll(_popup_bar._thread_index);
       return 0;
+
+    case 105:
+      WinStatsGraph::_monitor->choose_collector_color(_popup_bar._collector_index);
+      return 0;
+
+    case 106:
+      WinStatsGraph::_monitor->reset_collector_color(_popup_bar._collector_index);
+      return 0;
     }
     break;
 

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

@@ -52,6 +52,11 @@ protected:
 
   virtual bool animate(double time, double dt);
 
+  virtual bool get_window_state(int &x, int &y, int &width, int &height,
+                                bool &maximized, bool &minimized) const;
+  virtual void set_window_state(int x, int y, int width, int height,
+                                bool maximized, bool minimized);
+
   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);