diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py
index e70a0ef790..b90c53ec24 100755
--- a/makepanda/makepanda.py
+++ b/makepanda/makepanda.py
@@ -1077,6 +1077,9 @@ if (COMPILER=="GCC"):
LibName("COCOA", "-framework Cocoa")
# Fix for a bug in OSX Leopard:
LibName("GL", "-dylib_file /System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib:/System/Library/Frameworks/OpenGL.framework/Versions/A/Libraries/libGL.dylib")
+ # When using pre-11.0 SDKs, for PStats
+ if os.path.basename(SDK["MACOSX"]).startswith("MacOSX10."):
+ LibName("COCOA", "-Wl,-U,_OBJC_CLASS_$_NSTrackingSeparatorToolbarItem")
# Temporary exceptions to removal of this flag
if not PkgSkip("FFMPEG"):
@@ -5911,10 +5914,13 @@ if not PkgSkip("PANDATOOL"):
# DIRECTORY: pandatool/src/gtk-stats/
#
-if not PkgSkip("PANDATOOL") and (GetTarget() == 'windows' or not PkgSkip("GTK3")):
+if not PkgSkip("PANDATOOL") and (GetTarget() in ('windows', 'darwin') or not PkgSkip("GTK3")):
if GetTarget() == 'windows':
OPTS=['DIR:pandatool/src/win-stats']
TargetAdd('pstats_composite1.obj', opts=OPTS, input='winstats_composite1.cxx')
+ elif GetTarget() == 'darwin':
+ OPTS=['DIR:pandatool/src/mac-stats']
+ TargetAdd('pstats_composite1.obj', opts=OPTS, input='macstats_composite1.mm')
else:
OPTS=['DIR:pandatool/src/gtk-stats', 'GTK3']
TargetAdd('pstats_composite1.obj', opts=OPTS, input='gtkstats_composite1.cxx')
@@ -5923,7 +5929,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', 'WINCOMDLG', 'WINSOCK', 'WINIMM', 'WINGDI', 'WINKERNEL', 'WINOLDNAMES', 'WINUSER', 'WINMM', 'UXTHEME', 'GTK3'])
+ TargetAdd('pstats.exe', opts=['SUBSYSTEM:WINDOWS', 'WINCOMCTL', 'WINCOMDLG', 'WINSOCK', 'WINIMM', 'WINGDI', 'WINKERNEL', 'WINOLDNAMES', 'WINUSER', 'WINMM', 'UXTHEME', 'GTK3', 'COCOA', 'CARBON', 'QUARTZ'])
#
# DIRECTORY: pandatool/src/xfileprogs/
diff --git a/pandatool/src/mac-stats/Info.plist b/pandatool/src/mac-stats/Info.plist
new file mode 100644
index 0000000000..2550b7e518
--- /dev/null
+++ b/pandatool/src/mac-stats/Info.plist
@@ -0,0 +1,47 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ English
+ CFBundleIdentifier
+ org.panda3d.pstats
+ CFBundleExecutable
+ pstats
+ CFBundleName
+ PStats
+ CFBundleDisplayName
+ PStats
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundlePackageType
+ APPL
+ LSHasLocalizedDisplayName
+
+ NSAppleScriptEnabled
+
+ NSPrincipalClass
+ NSApplication
+ CFBundleDocumentTypes
+
+
+ CFBundleTypeName
+ PStats session file
+ CFBundleTypeRole
+ Editor
+ LSHandlerRank
+ Owner
+ LSTypeIsPackage
+
+ CFBundleTypeExtensions
+
+ pstats
+
+ CFBundleTypeMIMETypes
+
+ application/vnd.panda3d.pstats
+
+
+
+
+
diff --git a/pandatool/src/mac-stats/cocoa_compat.h b/pandatool/src/mac-stats/cocoa_compat.h
new file mode 100644
index 0000000000..b535b08d3c
--- /dev/null
+++ b/pandatool/src/mac-stats/cocoa_compat.h
@@ -0,0 +1,45 @@
+/**
+ * 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 cocoa_compat.h
+ * @author rdb
+ * @date 2023-08-28
+ */
+
+#ifndef COCOA_COMPAT_H
+#define COCOA_COMPAT_H
+
+#import
+
+// Allow building with older SDKs.
+#if __MAC_OS_X_VERSION_MAX_ALLOWED < 110000
+typedef NS_ENUM(NSInteger, NSWindowToolbarStyle) {
+ NSWindowToolbarStyleAutomatic,
+ NSWindowToolbarStyleExpanded,
+ NSWindowToolbarStylePreference,
+ NSWindowToolbarStyleUnified,
+ NSWindowToolbarStyleUnifiedCompact
+} API_AVAILABLE(macos(11.0));
+
+API_AVAILABLE(macos(11.0)) API_UNAVAILABLE(ios)
+@interface NSTrackingSeparatorToolbarItem : NSToolbarItem
++ (instancetype)trackingSeparatorToolbarItemWithIdentifier:(NSString *)identifier splitView:(NSSplitView *)splitView dividerIndex:(NSInteger)dividerIndex API_UNAVAILABLE(ios);
+@property (strong) NSSplitView *splitView API_UNAVAILABLE(ios);
+@property NSInteger dividerIndex API_UNAVAILABLE(ios);
+@end
+
+#endif // __MAC_OS_X_VERSION_MAX_ALLOWED
+
+#if __MAC_OS_X_VERSION_MAX_ALLOWED < 101400
+@protocol NSViewToolTipOwner
+- (NSString *)view:(NSView *)view stringForToolTip:(NSToolTipTag)tag point:(NSPoint)point userData:(nullable void *)data;
+@end
+
+#endif // __MAC_OS_X_VERSION_MAX_ALLOWED
+
+#endif // COCOA_COMPAT_H
diff --git a/pandatool/src/mac-stats/macStats.h b/pandatool/src/mac-stats/macStats.h
new file mode 100644
index 0000000000..a5e2371b6a
--- /dev/null
+++ b/pandatool/src/mac-stats/macStats.h
@@ -0,0 +1,19 @@
+/**
+ * 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 macStats.h
+ * @author rdb
+ * @date 2023-08-17
+ */
+
+#ifndef MACSTATS_H
+#define MACSTATS_H
+
+#include "pStatServer.h"
+
+#endif
diff --git a/pandatool/src/mac-stats/macStats.mm b/pandatool/src/mac-stats/macStats.mm
new file mode 100644
index 0000000000..cbaee08265
--- /dev/null
+++ b/pandatool/src/mac-stats/macStats.mm
@@ -0,0 +1,73 @@
+/**
+ * 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 macStats.mm
+ * @author rdb
+ * @date 2023-08-17
+ */
+
+#include "pandatoolbase.h"
+#include "macStats.h"
+#include "macStatsServer.h"
+#include "config_pstatclient.h"
+
+#include
+#include
+
+extern "C" {
+ OSStatus CPSSetProcessName(ProcessSerialNumber *psn, char *name);
+};
+
+@implementation NSBundle(swizzle)
+- (NSString *)__bundleIdentifier {
+ if (self == [NSBundle mainBundle]) {
+ return @"org.panda3d.pstats";
+ } else {
+ return [self __bundleIdentifier];
+ }
+}
+@end
+
+void
+keyboard_interrupt(int sig) {
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) {
+ [NSApp terminate:NSApp];
+ });
+}
+
+int
+main(int argc, char *argv[]) {
+ // This hack is necessary to allow showing notifications when run as a console app.
+ Class cls = objc_getClass("NSBundle");
+ if (cls) {
+ method_exchangeImplementations(class_getInstanceMethod(cls, @selector(bundleIdentifier)),
+ class_getInstanceMethod(cls, @selector(__bundleIdentifier)));
+ }
+
+ // Set the bundle name of the application.
+ ProcessSerialNumber psn;
+ GetCurrentProcess(&psn);
+ CPSSetProcessName(&psn, (char *)"PStats");
+
+ @autoreleasepool {
+ // Create the server application.
+ MacStatsServer *server = new MacStatsServer;
+
+ // Register a SIGINT handler to terminate the application correctly.
+ // Otherwise, notifications may linger in the notification center.
+ struct sigaction act = {};
+ act.sa_handler = &keyboard_interrupt;
+ act.sa_flags = SA_RESETHAND;
+ sigaction(SIGINT, &act, nullptr);
+
+ // Get lost in the Cocoa main loop.
+ server->run(argc, argv);
+ }
+
+ return 0;
+}
diff --git a/pandatool/src/mac-stats/macStatsAppDelegate.h b/pandatool/src/mac-stats/macStatsAppDelegate.h
new file mode 100644
index 0000000000..e79211784f
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsAppDelegate.h
@@ -0,0 +1,37 @@
+/**
+ * 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 macStatsAppDelegate.h
+ * @author rdb
+ * @date 2023-08-17
+ */
+
+#ifndef MACSTATSAPPDELEGATE_H
+#define MACSTATSAPPDELEGATE_H
+
+#import
+#import
+
+class MacStatsServer;
+
+@interface MacStatsAppDelegate : NSObject {
+ @private
+ MacStatsServer *_server;
+ NSTimer *_timer;
+}
+
+- (id)initWithServer:(MacStatsServer *)server;
+- (void)applicationDidFinishLaunching:(NSApplication *)sender;
+- (BOOL)applicationShouldTerminate:(NSApplication *)sender;
+- (void)applicationWillTerminate:(NSApplication *)sender;
+- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center
+ shouldPresentNotification:(NSUserNotification *)notification;
+
+@end
+
+#endif
diff --git a/pandatool/src/mac-stats/macStatsAppDelegate.mm b/pandatool/src/mac-stats/macStatsAppDelegate.mm
new file mode 100644
index 0000000000..7831549c17
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsAppDelegate.mm
@@ -0,0 +1,201 @@
+/**
+ * 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 macStatsAppDelegate.mm
+ * @author rdb
+ * @date 2023-08-17
+ */
+
+#include "macStatsAppDelegate.h"
+#include "macStatsServer.h"
+#include "pStatGraph.h"
+
+@implementation MacStatsAppDelegate
+
+- (id)initWithServer:(MacStatsServer *)server {
+ if (self = [super init]) {
+ _server = server;
+ _timer = nil;
+ }
+
+ return self;
+}
+
+- (BOOL)applicationSupportsSecureRestorableState:(NSApplication *)app {
+ // Squelches an annoying warning.
+ return YES;
+}
+
+- (BOOL)application:(NSApplication *)sender
+ openFile:(NSString *)filename {
+ Filename fn([filename UTF8String]);
+ fn.set_binary();
+ return _server->open_session(fn);
+}
+
+- (void)applicationDidFinishLaunching:(NSApplication *)sender {
+ // Set this object as delegate for user notifications.
+ {
+ NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
+ center.delegate = self;
+ }
+
+ // Register default preferences.
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+ NSDictionary *dict = [NSDictionary
+ dictionaryWithObjects:@[@"", [NSNumber numberWithBool:YES], [NSNumber numberWithInt:PStatGraph::GBU_ms]]
+ forKeys:@[@"Appearance", @"ShowStatusItem", @"TimeUnits"]];
+ [defaults registerDefaults:dict];
+
+ if (_server != nil) {
+ // Apply preferences.
+ [self applyDefaults:nil];
+
+ // Create a timer to poll the server.
+ _timer = [NSTimer scheduledTimerWithTimeInterval:0.2
+ target:self
+ selector:@selector(pollServer)
+ userInfo:nil
+ repeats:YES];
+
+ // Start new session if we don't have one yet.
+ if (_server->get_monitor() == nullptr) {
+ _server->new_session();
+ }
+ }
+
+ // Watch for defaults change.
+ {
+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+ [center addObserver:self
+ selector:@selector(applyDefaults:)
+ name:NSUserDefaultsDidChangeNotification
+ object:nil];
+ }
+}
+
+- (void)applyDefaults:(NSNotification *)notification {
+ if (_server != nil) {
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+ _server->set_show_status_item([defaults boolForKey:@"ShowStatusItem"]);
+ _server->set_appearance([defaults stringForKey:@"Appearance"]);
+ _server->set_time_units([defaults integerForKey:@"TimeUnits"]);
+ }
+}
+
+- (void)pollServer {
+ _server->poll();
+}
+
+- (BOOL)applicationShouldTerminate:(NSApplication *)sender {
+ if (_server != nil) {
+ return _server->close_session();
+ }
+ return YES;
+}
+
+- (void)applicationWillTerminate:(NSApplication *)sender {
+ if (_timer != nil) {
+ [_timer invalidate];
+ _timer = nil;
+ }
+
+ if (_server != nil) {
+ delete _server;
+ _server = nil;
+ }
+}
+
+- (BOOL)userNotificationCenter:(NSUserNotificationCenter *)center
+ shouldPresentNotification:(NSUserNotification *)notification {
+ return YES;
+}
+
+- (void)userNotificationCenter:(NSUserNotificationCenter *)center
+ didActivateNotification:(NSUserNotification *)notification {
+ NSUserNotificationAction *action = notification.additionalActivationAction;
+ if (action != nil) {
+ NSString *ident = action.identifier;
+ if ([ident isEqual:@"quit"]) {
+ [NSApp terminate:self];
+ }
+ else if ([ident isEqual:@"new"]) {
+ _server->new_session();
+ }
+ else if ([ident isEqual:@"open"]) {
+ _server->open_session();
+ }
+ else if ([ident isEqual:@"openLast"]) {
+ _server->open_last_session();
+ }
+ }
+}
+
+- (void)handleNewSession:(NSMenuItem *)item {
+ _server->new_session();
+}
+
+- (void)handleOpenSession:(NSMenuItem *)item {
+ _server->open_session();
+}
+
+- (void)handleOpenLastSession:(NSMenuItem *)item {
+ _server->open_last_session();
+}
+
+- (void)handleSaveSession:(NSMenuItem *)item {
+ _server->save_session();
+}
+
+- (void)handleCloseSession:(NSMenuItem *)item {
+ _server->close_session();
+}
+
+- (void)handleExportSession:(NSMenuItem *)item {
+ _server->export_session();
+}
+
+- (void)handleToggleSettingsBool:(NSMenuItem *)item {
+ [[NSUserDefaults standardUserDefaults] setBool:(item.state != NSOnState) forKey:item.representedObject];
+}
+
+- (void)handleSettingsInteger:(NSMenuItem *)item {
+ [[NSUserDefaults standardUserDefaults] setInteger:item.tag forKey:item.representedObject];
+}
+
+- (void)handleSettingsAppearance:(NSMenuItem *)item {
+ [[NSUserDefaults standardUserDefaults] setObject:item.representedObject forKey:@"Appearance"];
+}
+
+- (void)handleSpeed:(NSMenuItem *)item {
+ MacStatsMonitor *monitor = _server->get_monitor();
+ if (monitor != nullptr) {
+ monitor->set_scroll_speed(item.tag);
+ }
+}
+
+- (void)handlePause:(NSMenuItem *)item {
+ MacStatsMonitor *monitor = _server->get_monitor();
+ if (monitor != nullptr) {
+ monitor->set_pause(!item.state);
+ }
+}
+
+- (void)handleClickStatusItem:(id)sender {
+ [NSApp activateIgnoringOtherApps:YES];
+}
+
+- (void)handleChooseCollectorColor:(NSColorPanel *)panel {
+ MacStatsMonitor *monitor = _server->get_monitor();
+ if (monitor != nullptr) {
+ NSColor *color = panel.color;
+ monitor->handle_choose_collector_color(LRGBColor(color.redComponent, color.greenComponent, color.blueComponent));
+ }
+}
+
+@end
diff --git a/pandatool/src/mac-stats/macStatsChartMenu.h b/pandatool/src/mac-stats/macStatsChartMenu.h
new file mode 100644
index 0000000000..a8b3bceaa8
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsChartMenu.h
@@ -0,0 +1,60 @@
+/**
+ * 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 macStatsChartMenu.h
+ * @author rdb
+ * @date 2023-08-18
+ */
+
+#ifndef MACSTATSCHARTMENU_H
+#define MACSTATSCHARTMENU_H
+
+#include "pandatoolbase.h"
+#include "macStatsMonitor.h"
+
+#include
+
+class PStatView;
+class PStatViewLevel;
+
+/**
+ * A pulldown menu of charts available for a particular thread.
+ */
+class MacStatsChartMenu {
+public:
+ MacStatsChartMenu(MacStatsMonitor *monitor, int thread_index);
+ ~MacStatsChartMenu();
+
+ int get_thread_index() const { return _thread_index; }
+
+ void add_to_menu(NSMenu *menu, int position);
+ void remove_from_menu(NSMenu *menu);
+
+ void check_update();
+ void do_update();
+
+private:
+ bool add_view(NSMenu *parent_menu, const PStatViewLevel *view_level,
+ bool show_level, int insert_at);
+ NSMenuItem *make_menu_item(NSMenu *parent_menu, int insert_at,
+ const char *label, SEL action,
+ int collector_index = -1);
+
+ MacStatsMonitor *_monitor;
+ int _thread_index;
+
+ int _last_level_index;
+ NSMenu *_menu;
+
+ // Pair of menu item, submenu
+ std::vector > _collector_items;
+ int _time_items_end = 0;
+ int _level_items_end = 0;
+};
+
+#endif
diff --git a/pandatool/src/mac-stats/macStatsChartMenu.mm b/pandatool/src/mac-stats/macStatsChartMenu.mm
new file mode 100644
index 0000000000..8df12743f1
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsChartMenu.mm
@@ -0,0 +1,246 @@
+/**
+ * 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 macStatsChartMenu.mm
+ * @author rdb
+ * @date 2023-08-18
+ */
+
+#include "macStatsChartMenu.h"
+#include "macStatsChartMenuDelegate.h"
+#include "macStatsMonitor.h"
+
+/**
+ *
+ */
+MacStatsChartMenu::
+MacStatsChartMenu(MacStatsMonitor *monitor, int thread_index) :
+ _monitor(monitor),
+ _thread_index(thread_index)
+{
+ _menu = [[NSMenu alloc] init];
+ _menu.delegate = [[MacStatsChartMenuDelegate alloc] initWithMonitor:monitor threadIndex:thread_index];
+
+ if (thread_index == 0) {
+ _menu.title = @"Graphs";
+
+ // Timeline goes first.
+ make_menu_item(_menu, -1, "Timeline", @selector(handleOpenTimeline:));
+
+ // Then the piano roll (even though it's not very useful nowadays)
+ make_menu_item(_menu, -1, "Piano Roll", @selector(handleOpenPianoRoll:));
+ }
+ else {
+ make_menu_item(_menu, -1, "Open Strip Chart", @selector(handleOpenStripChart:), 0);
+ make_menu_item(_menu, -1, "Open Flame Graph", @selector(handleOpenFlameGraph:));
+ }
+
+ [_menu addItem:[NSMenuItem separatorItem]];
+ _time_items_end = 3;
+
+ // Put a separator between time items and level items.
+ [_menu addItem:[NSMenuItem separatorItem]];
+ _level_items_end = _time_items_end + 1;
+
+ // For the main thread menu, also some options relating to all graph windows.
+ if (thread_index == 0) {
+ [_menu addItem:[NSMenuItem separatorItem]];
+ make_menu_item(_menu, -1, "Close All Graphs", @selector(handleCloseAllGraphs:));
+ make_menu_item(_menu, -1, "Reopen Default Graphs", @selector(handleReopenDefaultGraphs:));
+ make_menu_item(_menu, -1, "Save Current Layout as Default", @selector(handleSaveDefaultGraphs:));
+ }
+
+ do_update();
+}
+
+/**
+ *
+ */
+MacStatsChartMenu::
+~MacStatsChartMenu() {
+ MacStatsChartMenuDelegate *delegate = (MacStatsChartMenuDelegate *)_menu.delegate;
+ [_menu release];
+ [delegate release];
+}
+
+/**
+ * Adds the menu to the end of the indicated menu bar.
+ */
+void MacStatsChartMenu::
+add_to_menu(NSMenu *menu, int position) {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ [menu insertItem:item atIndex:position];
+ [menu setSubmenu:_menu forItem:item];
+ [item release];
+}
+
+/**
+ * Removes the menu from the menu bar.
+ */
+void MacStatsChartMenu::
+remove_from_menu(NSMenu *menu) {
+ int index = [menu indexOfItemWithSubmenu:_menu];
+ if (index >= 0) {
+ [menu removeItemAtIndex:index];
+ }
+}
+
+/**
+ * Checks to see if the menu needs to be updated (e.g. because of new data
+ * from the client), and updates it if necessary.
+ */
+void MacStatsChartMenu::
+check_update() {
+ PStatView &view = _monitor->get_view(_thread_index);
+ if (view.get_level_index() != _last_level_index) {
+ do_update();
+ }
+}
+
+/**
+ * Unconditionally updates the menu with the latest data from the client.
+ */
+void MacStatsChartMenu::
+do_update() {
+ PStatView &view = _monitor->get_view(_thread_index);
+ _last_level_index = view.get_level_index();
+
+ const PStatClientData *client_data = _monitor->get_client_data();
+ if (_thread_index != 0) {
+ std::string thread_name = client_data->get_thread_name(_thread_index);
+ _menu.title = [NSString stringWithUTF8String:thread_name.c_str()];
+ }
+
+ if (client_data->get_num_collectors() > _collector_items.size()) {
+ _collector_items.resize(client_data->get_num_collectors(), std::make_pair(nullptr, nullptr));
+ }
+
+ // The menu item(s) for the thread's frame time goes second.
+ const PStatViewLevel *view_level = view.get_top_level();
+ if (_thread_index == 0) {
+ if (add_view(_menu, view_level, false, _time_items_end)) {
+ ++_time_items_end;
+ ++_level_items_end;
+ }
+ } else {
+ for (int c = 0; c < view_level->get_num_children(); ++c) {
+ if (add_view(_menu, view_level->get_child(c), false, _time_items_end)) {
+ ++_time_items_end;
+ ++_level_items_end;
+ }
+ }
+ }
+
+ // And then the menu item(s) for each of the level values.
+ int num_toplevel_collectors = client_data->get_num_toplevel_collectors();
+ for (int tc = 0; tc < num_toplevel_collectors; tc++) {
+ int collector = client_data->get_toplevel_collector(tc);
+ if (client_data->has_collector(collector) &&
+ client_data->get_collector_has_level(collector, _thread_index)) {
+
+ PStatView &level_view = _monitor->get_level_view(collector, _thread_index);
+ add_view(_menu, level_view.get_top_level(), true, _level_items_end);
+ }
+ }
+}
+
+/**
+ * Adds a new entry or entries to the menu for the indicated view and its
+ * children. Returns true if an item was added, false if not.
+ */
+bool MacStatsChartMenu::
+add_view(NSMenu *parent_menu, const PStatViewLevel *view_level,
+ bool show_level, int insert_at) {
+ int collector = view_level->get_collector();
+
+ NSMenuItem *&menu_item = _collector_items[collector].first;
+ NSMenu *&menu = _collector_items[collector].second;
+
+ const PStatClientData *client_data = _monitor->get_client_data();
+
+ int num_children = view_level->get_num_children();
+ if (menu == nullptr && num_children == 0) {
+ // For a collector without children, no point in making a submenu. We just
+ // have the item open a strip chart directly (no point in creating a flame
+ // graph if there are no children).
+ if (menu_item != nullptr) {
+ // Already exists.
+ return false;
+ }
+
+ std::string collector_name = client_data->get_collector_name(collector);
+ if (show_level) {
+ menu_item = make_menu_item(parent_menu, insert_at,
+ collector_name.c_str(), @selector(handleOpenStripChartLevel:), collector);
+ } else {
+ menu_item = make_menu_item(parent_menu, insert_at,
+ collector_name.c_str(), @selector(handleOpenStripChart:), collector);
+ }
+ return true;
+ }
+ else if (menu_item != nullptr && menu == nullptr) {
+ // Unhook the signal handler, we are creating a submenu.
+ menu_item.action = nil;
+ }
+
+ // Create a submenu.
+ bool added_item = false;
+ if (menu_item == nullptr) {
+ std::string collector_name = client_data->get_collector_name(collector);
+ menu_item = make_menu_item(parent_menu, insert_at, collector_name.c_str(), nil);
+ added_item = true;
+ }
+
+ if (menu == nullptr) {
+ menu = [[NSMenu alloc] init];
+ [parent_menu setSubmenu:menu forItem:menu_item];
+
+ if (show_level) {
+ make_menu_item(menu, -1, "Open Strip Chart",
+ @selector(handleOpenStripChartLevel:), collector);
+ } else {
+ make_menu_item(menu, -1, "Open Strip Chart",
+ @selector(handleOpenStripChart:), collector);
+
+ if (collector == 0) {
+ collector = -1;
+ }
+
+ make_menu_item(menu, -1, "Open Flame Graph",
+ @selector(handleOpenFlameGraph:), collector);
+ }
+
+ [menu addItem:[NSMenuItem separatorItem]];
+ [menu release];
+ }
+
+ for (int c = 0; c < num_children; ++c) {
+ add_view(menu, view_level->get_child(c), show_level, 2 + !show_level);
+ }
+
+ return added_item;
+}
+
+/**
+ *
+ */
+NSMenuItem *MacStatsChartMenu::
+make_menu_item(NSMenu *parent_menu, int insert_at, const char *label, SEL action, int collector_index) {
+ NSMenuItem *menu_item = [[NSMenuItem alloc] init];
+ menu_item.title = [NSString stringWithUTF8String:label];
+ menu_item.target = _menu.delegate;
+ menu_item.action = action;
+ menu_item.tag = collector_index;
+ if (insert_at >= 0) {
+ [parent_menu insertItem:menu_item atIndex:insert_at];
+ } else {
+ [parent_menu addItem:menu_item];
+ }
+ [menu_item release];
+ return menu_item;
+}
diff --git a/pandatool/src/mac-stats/macStatsChartMenuDelegate.h b/pandatool/src/mac-stats/macStatsChartMenuDelegate.h
new file mode 100644
index 0000000000..05f86302c6
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsChartMenuDelegate.h
@@ -0,0 +1,32 @@
+/**
+ * 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 macStatsChartMenuDelegate.h
+ * @author rdb
+ * @date 2023-08-18
+ */
+
+#ifndef MACSTATSCHARTMENUDELEGATE_H
+#define MACSTATSCHARTMENUDELEGATE_H
+
+#import
+#import
+
+class MacStatsMonitor;
+
+@interface MacStatsChartMenuDelegate : NSObject {
+ @private
+ MacStatsMonitor *_monitor;
+ int _thread_index;
+}
+
+- (id)initWithMonitor:(MacStatsMonitor *)monitor threadIndex:(int)index;
+
+@end
+
+#endif
diff --git a/pandatool/src/mac-stats/macStatsChartMenuDelegate.mm b/pandatool/src/mac-stats/macStatsChartMenuDelegate.mm
new file mode 100644
index 0000000000..67c12fcac0
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsChartMenuDelegate.mm
@@ -0,0 +1,61 @@
+/**
+ * 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 macStatsChartMenuDelegate.mm
+ * @author rdb
+ * @date 2023-08-18
+ */
+
+#include "macStatsChartMenuDelegate.h"
+#include "macStatsMonitor.h"
+
+@implementation MacStatsChartMenuDelegate
+
+- (id)initWithMonitor:(MacStatsMonitor *)monitor threadIndex:(int)index {
+ if (self = [super init]) {
+ _monitor = monitor;
+ _thread_index = index;
+ }
+
+ return self;
+}
+
+- (void)handleOpenTimeline:(NSMenuItem *)item {
+ _monitor->open_timeline();
+}
+
+- (void)handleOpenStripChart:(NSMenuItem *)item {
+ _monitor->open_strip_chart(_thread_index, item.tag, NO);
+}
+
+- (void)handleOpenStripChartLevel:(NSMenuItem *)item {
+ _monitor->open_strip_chart(_thread_index, item.tag, YES);
+}
+
+- (void)handleOpenFlameGraph:(NSMenuItem *)item {
+ _monitor->open_flame_graph(_thread_index, item.tag);
+}
+
+- (void)handleOpenPianoRoll:(NSMenuItem *)item {
+ _monitor->open_piano_roll(_thread_index);
+}
+
+- (void)handleCloseAllGraphs:(NSMenuItem *)item {
+ _monitor->close_all_graphs();
+}
+
+- (void)handleReopenDefaultGraphs:(NSMenuItem *)item {
+ _monitor->close_all_graphs();
+ _monitor->open_default_graphs();
+}
+
+- (void)handleSaveDefaultGraphs:(NSMenuItem *)item {
+ _monitor->save_default_graphs();
+}
+
+@end
diff --git a/pandatool/src/mac-stats/macStatsFlameGraph.h b/pandatool/src/mac-stats/macStatsFlameGraph.h
new file mode 100644
index 0000000000..25df1f96c8
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsFlameGraph.h
@@ -0,0 +1,88 @@
+/**
+ * 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 macStatsFlameGraph.h
+ * @author rdb
+ * @date 2023-08-18
+ */
+
+#ifndef MACSTATSFLAMEGRAPH_H
+#define MACSTATSFLAMEGRAPH_H
+
+#include "macStatsGraph.h"
+#include "pStatFlameGraph.h"
+#include "macStatsChartMenuDelegate.h"
+
+/**
+ * A window that draws a flame chart, which shows the collectors explicitly
+ * stopping and starting, one frame at a time.
+ */
+class MacStatsFlameGraph final : public PStatFlameGraph, public MacStatsGraph {
+public:
+ MacStatsFlameGraph(MacStatsMonitor *monitor, int thread_index,
+ int collector_index=-1);
+ virtual ~MacStatsFlameGraph();
+
+ virtual void new_collector(int collector_index);
+ virtual void new_data(int thread_index, int frame_number);
+ virtual void force_redraw();
+ virtual void changed_graph_size(int graph_xsize, int graph_ysize);
+
+ virtual void set_time_units(int unit_mask);
+ virtual void on_click_label(int collector_index);
+ virtual void on_enter_label(int collector_index);
+ virtual void on_leave_label(int collector_index);
+ virtual NSMenu *get_label_menu(int collector_index) const;
+
+protected:
+ virtual void normal_guide_bars();
+
+ void clear_region();
+ virtual void begin_draw();
+ virtual void draw_bar(int depth, int from_x, int to_x,
+ int collector_index, int parent_index);
+ virtual void end_draw();
+ virtual void idle();
+
+ 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 NSMenu *get_graph_menu(int mouse_x, int mouse_y) const;
+ virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const;
+ virtual DragMode consider_drag_start(int graph_x, int graph_y);
+
+ virtual void handle_button_press(int graph_x, int graph_y,
+ bool double_click, int button);
+ virtual void handle_button_release(int graph_x, int graph_y);
+ virtual void handle_motion(int graph_x, int graph_y);
+ virtual void handle_leave();
+ virtual void handle_draw_graph(CGContextRef ctx, NSRect rect);
+ virtual void handle_back();
+
+public:
+ void handle_toggle_average(bool state);
+
+private:
+ int pixel_to_depth(int y) const;
+ void draw_guide_bar(CGContextRef ctx, const PStatGraph::GuideBar &bar);
+ void draw_guide_labels(CGContextRef ctx);
+ void draw_guide_label(CGContextRef ctx, const PStatGraph::GuideBar &bar);
+
+private:
+ NSToolbarItem *_total_item;
+
+ MacStatsChartMenuDelegate *_menu_delegate;
+
+ std::vector _back_stack;
+};
+
+#endif
diff --git a/pandatool/src/mac-stats/macStatsFlameGraph.mm b/pandatool/src/mac-stats/macStatsFlameGraph.mm
new file mode 100644
index 0000000000..60314147f8
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsFlameGraph.mm
@@ -0,0 +1,871 @@
+/**
+ * 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 macStatsFlameGraph.mm
+ * @author rdb
+ * @date 2023-08-18
+ */
+
+#include "macStatsFlameGraph.h"
+#include "macStatsMonitor.h"
+#include "macStatsGraphView.h"
+#include "macStatsScaleArea.h"
+#include "pStatCollectorDef.h"
+#include "cocoa_compat.h"
+
+@interface MacStatsFlameGraphViewController : MacStatsGraphViewController
+@end
+
+static const int default_flame_graph_width = 800;
+static const int default_flame_graph_height = 250;
+
+/**
+ *
+ */
+MacStatsFlameGraph::
+MacStatsFlameGraph(MacStatsMonitor *monitor, int thread_index,
+ int collector_index) :
+ PStatFlameGraph(monitor, thread_index, collector_index, 0, 0),
+ MacStatsGraph(monitor, [MacStatsFlameGraphViewController alloc])
+{
+ // Used for popup menus.
+ _menu_delegate = [[MacStatsChartMenuDelegate alloc] initWithMonitor:monitor threadIndex:thread_index];
+
+ // Set the initial size of the graph.
+ int height = default_flame_graph_height + _window.frame.size.height - _window.contentLayoutRect.size.height;
+ _graph_view.frame = NSMakeRect(0, 0, default_flame_graph_width, height);
+ _graph_view_controller.view.frame = NSMakeRect(0, 0, default_flame_graph_width, height);
+
+ _total_item = nil;
+ if (@available(macOS 11.0, *)) {
+ NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:@""];
+ toolbar.delegate = _graph_view_controller;
+ toolbar.displayMode = NSToolbarDisplayModeIconOnly;
+ _window.toolbar = toolbar;
+ [_window setToolbarStyle:NSWindowToolbarStyleUnifiedCompact];
+
+ for (NSToolbarItem *item in toolbar.items) {
+ if ([item.itemIdentifier isEqual:@"total"]) {
+ _total_item = item;
+ break;
+ }
+ }
+ [toolbar release];
+ }
+
+ //MacStatsScaleAreaController *scale_area_controller = [[MacStatsScaleAreaController alloc] initWithGraph:this];
+ //scale_area_controller.layoutAttribute = NSLayoutAttributeRight;
+ //_scale_area = scale_area_controller.view;
+ //[_window addTitlebarAccessoryViewController:scale_area_controller];
+
+ _window.contentViewController = _graph_view_controller;
+
+ _graph_view_controller.backToolbarItemVisible = NO;
+
+ // Let's show the units on the guide bar labels. There's room.
+ set_guide_bar_units(get_guide_bar_units() | GBU_show_units);
+
+ if (get_average_mode()) {
+ start_animation();
+ }
+
+ if (is_title_unknown()) {
+ std::string window_title = get_title_text();
+ if (!is_title_unknown()) {
+ _window.title = [NSString stringWithUTF8String:window_title.c_str()];
+ }
+ }
+
+ if (@available(macOS 11.0, *)) {
+ std::string text = format_number(get_horizontal_scale(), get_guide_bar_units(), get_guide_bar_unit_name());
+ [_total_item setTitle:[NSString stringWithUTF8String:text.c_str()]];
+ }
+
+ [_window makeKeyAndOrderFront:nil];
+}
+
+/**
+ *
+ */
+MacStatsFlameGraph::
+~MacStatsFlameGraph() {
+ [_menu_delegate release];
+}
+
+/**
+ * Called whenever a new Collector definition is received from the client.
+ */
+void MacStatsFlameGraph::
+new_collector(int collector_index) {
+ MacStatsGraph::new_collector(collector_index);
+}
+
+/**
+ * Called as each frame's data is made available. There is no guarantee the
+ * frames will arrive in order, or that all of them will arrive at all. The
+ * monitor should be prepared to accept frames received out-of-order or
+ * missing.
+ */
+void MacStatsFlameGraph::
+new_data(int thread_index, int frame_number) {
+ if (is_title_unknown()) {
+ std::string window_title = get_title_text();
+ if (!is_title_unknown()) {
+ _window.title = [NSString stringWithUTF8String:window_title.c_str()];
+ }
+ }
+
+ if (!_pause) {
+ update();
+
+ if (@available(macOS 11.0, *)) {
+ std::string text = format_number(get_horizontal_scale(), get_guide_bar_units(), get_guide_bar_unit_name());
+ [_total_item setTitle:[NSString stringWithUTF8String:text.c_str()]];
+ }
+ }
+}
+
+/**
+ * Called when it is necessary to redraw the entire graph.
+ */
+void MacStatsFlameGraph::
+force_redraw() {
+ if (_ctx) {
+ PStatFlameGraph::force_redraw();
+ }
+}
+
+/**
+ * Called when the user has resized the window, forcing a resize of the graph.
+ */
+void MacStatsFlameGraph::
+changed_graph_size(int graph_xsize, int graph_ysize) {
+ PStatFlameGraph::changed_size(graph_xsize, graph_ysize);
+}
+
+/**
+ * Called when the user selects a new time units from the monitor pulldown
+ * menu, this should adjust the units for the graph to the indicated mask if
+ * it is a time-based graph.
+ */
+void MacStatsFlameGraph::
+set_time_units(int unit_mask) {
+ int old_unit_mask = get_guide_bar_units();
+ if ((old_unit_mask & (GBU_hz | GBU_ms)) != 0) {
+ unit_mask = unit_mask & (GBU_hz | GBU_ms);
+ unit_mask |= (old_unit_mask & GBU_show_units);
+ set_guide_bar_units(unit_mask);
+
+ if (@available(macOS 11.0, *)) {
+ std::string text = format_number(get_horizontal_scale(), get_guide_bar_units(), get_guide_bar_unit_name());
+ [_total_item setTitle:[NSString stringWithUTF8String:text.c_str()]];
+ }
+
+ //_scale_area.needsDisplay = YES;
+ }
+}
+
+/**
+ * Called when the user single-clicks on a label.
+ */
+void MacStatsFlameGraph::
+on_click_label(int collector_index) {
+ int current = get_collector_index();
+ if (collector_index != current) {
+ if (_back_stack.empty()) {
+ _graph_view_controller.backToolbarItemVisible = YES;
+ }
+ _back_stack.push_back(current);
+ set_collector_index(collector_index);
+
+ std::string window_title = get_title_text();
+ if (!is_title_unknown()) {
+ _window.title = [NSString stringWithUTF8String:window_title.c_str()];
+ }
+ }
+}
+
+/**
+ * Called when the user hovers the mouse over a label.
+ */
+void MacStatsFlameGraph::
+on_enter_label(int collector_index) {
+ if (collector_index != _highlighted_index) {
+ _highlighted_index = collector_index;
+
+ if (!get_average_mode()) {
+ PStatFlameGraph::force_redraw();
+ }
+ }
+}
+
+/**
+ * Called when the user's mouse cursor leaves a label.
+ */
+void MacStatsFlameGraph::
+on_leave_label(int collector_index) {
+ if (collector_index == _highlighted_index && collector_index != -1) {
+ _highlighted_index = -1;
+
+ if (!get_average_mode()) {
+ PStatFlameGraph::force_redraw();
+ }
+ }
+}
+
+/**
+ * Called when the mouse right-clicks on a label, and should return the menu
+ * that should pop up.
+ */
+NSMenu *MacStatsFlameGraph::
+get_label_menu(int collector_index) const {
+ NSMenu *menu = [[[NSMenu alloc] init] autorelease];
+
+ std::string label = get_label_tooltip(collector_index);
+ if (!label.empty()) {
+ if (@available(macOS 14.0, *)) {
+ [menu addItem:[NSMenuItem sectionHeaderWithTitle:[NSString stringWithUTF8String:label.c_str()]]];
+ } else {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:label.c_str()] action:nil keyEquivalent:@""];
+ item.enabled = NO;
+ [menu addItem:item];
+ [item release];
+ }
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Set as Focus" action:@selector(handleSetAsFocus:) keyEquivalent:@""];
+ item.target = _graph_view_controller;
+ item.tag = collector_index;
+ item.enabled = (collector_index != 0 || get_collector_index() != 0);
+ [menu addItem:item];
+ [item release];
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Open Strip Chart" action:@selector(handleOpenStripChart:) keyEquivalent:@""];
+ item.target = _menu_delegate;
+ item.tag = collector_index;
+ [menu addItem:item];
+ [item release];
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Open Flame Graph" action:@selector(handleOpenFlameGraph:) keyEquivalent:@""];
+ item.target = _menu_delegate;
+ item.tag = collector_index;
+ [menu addItem:item];
+ [item release];
+ }
+
+ [menu addItem:[NSMenuItem separatorItem]];
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Change Color\u2026" action:@selector(handleChangeColor:) keyEquivalent:@""];
+ item.target = _graph_view_controller;
+ item.tag = collector_index;
+ [menu addItem:item];
+ [item release];
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Reset Color" action:@selector(handleResetColor:) keyEquivalent:@""];
+ item.target = _graph_view_controller;
+ item.tag = collector_index;
+ [menu addItem:item];
+ [item release];
+ }
+
+ return menu;
+}
+
+/**
+ * Calls update_guide_bars with parameters suitable to this kind of graph.
+ */
+void MacStatsFlameGraph::
+normal_guide_bars() {
+ // We want vaguely 100 pixels between guide bars.
+ int num_bars = get_xsize() / 100;
+
+ _guide_bars.clear();
+
+ double dist = get_horizontal_scale() / num_bars;
+
+ for (int i = 1; i < num_bars; ++i) {
+ _guide_bars.push_back(make_guide_bar(i * dist));
+ }
+
+ _guide_bars_changed = true;
+
+ //nassertv_always(_scale_area != nullptr);
+ //_scale_area.needsDisplay = YES;
+}
+
+/**
+ * Erases the chart area.
+ */
+void MacStatsFlameGraph::
+clear_region() {
+ if (_ctx) {
+ CGContextSetFillColorWithColor(_ctx, _background_color);
+ CGContextFillRect(_ctx, CGRectMake(0, 0, get_xsize(), get_ysize()));
+ }
+}
+
+/**
+ * Erases the chart area in preparation for drawing a bunch of bars.
+ */
+void MacStatsFlameGraph::
+begin_draw() {
+ if (!_ctx) {
+ return;
+ }
+
+ clear_region();
+
+ // isFlipped is true in the NSView, so flip the text again
+ CGContextSetTextMatrix(_ctx, CGAffineTransformMakeScale(1, -1));
+
+ // Draw in the guide bars.
+ int num_guide_bars = get_num_guide_bars();
+ for (int i = 0; i < num_guide_bars; i++) {
+ const GuideBar &bar = get_guide_bar(i);
+ draw_guide_bar(_ctx, bar);
+ draw_guide_label(_ctx, bar);
+ }
+}
+
+/**
+ * Should be overridden by the user class. Should draw a single bar at the
+ * indicated location.
+ */
+void MacStatsFlameGraph::
+draw_bar(int depth, int from_x, int to_x, int collector_index, int parent_index) {
+ double bottom = get_ysize() - depth * 4.000 * 5;
+ double top = bottom - 4.000 * 5;
+
+ top += 1;
+
+ MacStatsMonitor *monitor = MacStatsGraph::_monitor;
+
+ bool is_highlighted = collector_index == _highlighted_index;
+ CGContextSetFillColorWithColor(_ctx,
+ monitor->get_collector_color(collector_index, is_highlighted));
+
+ if (to_x < from_x + 3) {
+ // It's just a tiny sliver. This is a more reliable way to draw it.
+ CGRect rect = CGRectMake(from_x, top, to_x - from_x, bottom - top);
+ CGContextFillRect(_ctx, rect);
+ }
+ else {
+ double radius = std::min((double)4.000, (to_x - from_x) / 2.0);
+ CGContextBeginPath(_ctx);
+ CGContextAddArc(_ctx, to_x - radius, top + radius, radius, -0.5 * M_PI, 0.0, NO);
+ CGContextAddArc(_ctx, to_x - radius, bottom - radius, radius, 0.0, 0.5 * M_PI, NO);
+ CGContextAddArc(_ctx, from_x + radius, bottom - radius, radius, 0.5 * M_PI, M_PI, NO);
+ CGContextAddArc(_ctx, from_x + radius, top + radius, radius, M_PI, 1.5 * M_PI, NO);
+ CGContextClosePath(_ctx);
+ CGContextFillPath(_ctx);
+
+ if ((to_x - from_x) >= 4.000 * 4) {
+ // Only bother drawing the text if we've got some space to draw on.
+ int left = std::max(from_x, 0) + 4.000 / 2;
+ int right = std::min(to_x, get_xsize()) - 4.000 / 2;
+
+ const PStatClientData *client_data = monitor->get_client_data();
+ const PStatCollectorDef &def = client_data->get_collector_def(collector_index);
+
+ const CFStringRef keys[] = {
+ (__bridge CFStringRef)NSForegroundColorAttributeName,
+ (__bridge CFStringRef)NSFontAttributeName,
+ };
+ const void *values[] = {
+ monitor->get_collector_text_color(collector_index, is_highlighted),
+ [NSFont systemFontOfSize:0.0],
+ };
+ CFDictionaryRef attribs = CFDictionaryCreate(kCFAllocatorDefault, (const void **)keys, (const void **)values, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+
+ CFStringRef str = CFStringCreateWithCString(kCFAllocatorDefault, def._name.c_str(), kCFStringEncodingUTF8);
+ CFAttributedStringRef astr = CFAttributedStringCreate(kCFAllocatorDefault, str, attribs);
+
+ CTLineRef line = CTLineCreateWithAttributedString(astr);
+ CGRect bounds = CTLineGetImageBounds(line, _ctx);
+ CFRelease(astr);
+ CFRelease(str);
+
+ if (bounds.size.width < right - left) {
+ // We have room for more. Show the collector's actual parent, if it's
+ // different than the block it's shown above.
+ if (def._parent_index > 0 && def._parent_index != parent_index) {
+ const PStatCollectorDef &parent_def = client_data->get_collector_def(def._parent_index);
+ std::string long_name = parent_def._name + ":" + def._name;
+
+ CFStringRef long_str = CFStringCreateWithCString(kCFAllocatorDefault, long_name.c_str(), kCFStringEncodingUTF8);
+ CFAttributedStringRef long_astr = CFAttributedStringCreate(kCFAllocatorDefault, long_str, attribs);
+
+ CTLineRef long_line = CTLineCreateWithAttributedString((CFAttributedStringRef)long_astr);
+ CGRect long_bounds = CTLineGetImageBounds(long_line, _ctx);
+
+ if (long_bounds.size.width < right - left) {
+ CFRelease(line);
+ line = long_line;
+ bounds = long_bounds;
+ } else {
+ CFRelease(long_line);
+ }
+ CFRelease(long_astr);
+ CFRelease(long_str);
+ }
+ }
+ else {
+ static CFStringRef token_str = CFSTR("\u2026");
+ CFAttributedStringRef token_astr = CFAttributedStringCreate(kCFAllocatorDefault, token_str, attribs);
+ CTLineRef token_line = CTLineCreateWithAttributedString(token_astr);
+ CTLineRef trunc_line = CTLineCreateTruncatedLine(line, right - left, kCTLineTruncationEnd, token_line);
+ CFRelease(line);
+ CFRelease(token_astr);
+ CFRelease(token_line);
+ line = trunc_line;
+ }
+
+ // Center the text vertically in the bar.
+ if (line != nullptr) {
+ CGContextSetTextPosition(_ctx, left, top + (bottom - top + bounds.size.height) / 2);
+ CTLineDraw(line, _ctx);
+ CFRelease(line);
+ }
+ CFRelease(attribs);
+ }
+ }
+}
+
+/**
+ * Called after all the bars have been drawn, this triggers a refresh event to
+ * draw it to the window.
+ */
+void MacStatsFlameGraph::
+end_draw() {
+ _graph_view.needsDisplay = YES;
+}
+
+/**
+ * Called at the end of the draw cycle.
+ */
+void MacStatsFlameGraph::
+idle() {
+}
+
+/**
+ * Overridden by a derived class to implement an animation. If it returns
+ * false, the animation timer is stopped.
+ */
+bool MacStatsFlameGraph::
+animate(double time, double dt) {
+ return PStatFlameGraph::animate(time, dt);
+}
+
+/**
+ * Returns the current window dimensions.
+ */
+bool MacStatsFlameGraph::
+get_window_state(int &x, int &y, int &width, int &height,
+ bool &maximized, bool &minimized) const {
+ MacStatsGraph::get_window_state(x, y, width, height, maximized, minimized);
+ return true;
+}
+
+/**
+ * Called to restore the graph window to its previous dimensions.
+ */
+void MacStatsFlameGraph::
+set_window_state(int x, int y, int width, int height,
+ bool maximized, bool minimized) {
+ MacStatsGraph::set_window_state(x, y, width, height, maximized, minimized);
+}
+
+/**
+ * Called when the mouse right-clicks on the graph, and should return the menu
+ * that should pop up.
+ */
+NSMenu *MacStatsFlameGraph::
+get_graph_menu(int mouse_x, int mouse_y) const {
+ int collector_index = get_bar_collector(pixel_to_depth(mouse_y), mouse_x);
+ if (collector_index != -1) {
+ return get_label_menu(collector_index);
+ }
+ return nil;
+}
+
+/**
+ * Called when the mouse hovers over the graph, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string MacStatsFlameGraph::
+get_graph_tooltip(int mouse_x, int mouse_y) const {
+ return get_bar_tooltip(pixel_to_depth(mouse_y), mouse_x);
+}
+
+/**
+ * Based on the mouse position within the window's client area, look for
+ * draggable things the mouse might be hovering over and return the
+ * apprioprate DragMode enum or DM_none if nothing is indicated.
+ */
+MacStatsGraph::DragMode MacStatsFlameGraph::
+consider_drag_start(int graph_x, int graph_y) {
+ if (graph_y >= 0 && graph_y < get_ysize()) {
+ if (graph_x >= 0 && graph_x < get_xsize()) {
+ // See if the mouse is over a user-defined guide bar.
+ int x = graph_x;
+ double from_height = pixel_to_height(x - 2);
+ double to_height = pixel_to_height(x + 2);
+ _drag_guide_bar = find_user_guide_bar(from_height, to_height);
+ if (_drag_guide_bar >= 0) {
+ return DM_guide_bar;
+ }
+
+ } else {
+ // The mouse is left or right of the graph; maybe create a new guide
+ // bar.
+ return DM_new_guide_bar;
+ }
+ }
+
+ return DM_none;
+}
+
+/**
+ * Called when the mouse button is depressed within the graph window.
+ */
+void MacStatsFlameGraph::
+handle_button_press(int graph_x, int graph_y, bool double_click, int button) {
+ if (graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) {
+ int depth = pixel_to_depth(graph_y);
+ int collector_index = get_bar_collector(depth, graph_x);
+ if (double_click && button == 0) {
+ // Double-clicking on a color bar in the graph will zoom the graph into
+ // that collector.
+ if (collector_index >= 0) {
+ on_click_label(collector_index);
+ } else {
+ if (!_back_stack.empty()) {
+ _back_stack.clear();
+ _graph_view_controller.backToolbarItemVisible = NO;
+ }
+ set_collector_index(-1);
+ std::string window_title = get_title_text();
+ if (!is_title_unknown()) {
+ _window.title = [NSString stringWithUTF8String:window_title.c_str()];
+ }
+ }
+ return;
+ }
+ }
+
+ if (_potential_drag_mode == DM_none) {
+ set_drag_mode(DM_scale);
+ _drag_scale_start = pixel_to_height(graph_x);
+ // SetCapture(_graph_window);
+ return;
+ }
+ else if (_potential_drag_mode == DM_guide_bar && _drag_guide_bar >= 0) {
+ set_drag_mode(DM_guide_bar);
+ _drag_start_x = graph_x;
+ // SetCapture(_graph_window);
+ return;
+ }
+
+ return MacStatsGraph::handle_button_press(graph_x, graph_y,
+ double_click, button);
+}
+
+/**
+ * Called when the mouse button is released within the graph window.
+ */
+void MacStatsFlameGraph::
+handle_button_release(int graph_x, int graph_y) {
+ if (_drag_mode == DM_scale) {
+ set_drag_mode(DM_none);
+ // ReleaseCapture();
+ return handle_motion(graph_x, graph_y);
+ }
+ else if (_drag_mode == DM_guide_bar) {
+ if (graph_x < 0 || graph_x >= get_xsize()) {
+ remove_user_guide_bar(_drag_guide_bar);
+ } else {
+ move_user_guide_bar(_drag_guide_bar, pixel_to_height(graph_x));
+ }
+ set_drag_mode(DM_none);
+ // ReleaseCapture();
+ return handle_motion(graph_x, graph_y);
+ }
+
+ return MacStatsGraph::handle_button_release(graph_x, graph_y);
+}
+
+/**
+ * Called when the mouse is moved within the graph window.
+ */
+void MacStatsFlameGraph::
+handle_motion(int graph_x, int graph_y) {
+ if (_drag_mode == DM_none && _potential_drag_mode == DM_none &&
+ graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) {
+ // When the mouse is over a color bar, highlight it.
+ int depth = pixel_to_depth(graph_y);
+ int collector_index = get_bar_collector(depth, graph_x);
+ on_enter_label(collector_index);
+ }
+ else {
+ // If the mouse is in some drag mode, stop highlighting.
+ _label_stack.highlight_label(-1);
+ on_leave_label(_highlighted_index);
+ }
+
+ if (_drag_mode == DM_new_guide_bar) {
+ // We haven't created the new guide bar yet; we won't until the mouse
+ // comes within the graph's region.
+ if (graph_x >= 0 && graph_x < get_xsize()) {
+ set_drag_mode(DM_guide_bar);
+ _drag_guide_bar = add_user_guide_bar(pixel_to_height(graph_x));
+ return;
+ }
+ }
+ else if (_drag_mode == DM_guide_bar) {
+ move_user_guide_bar(_drag_guide_bar, pixel_to_height(graph_x));
+ return;
+ }
+
+ return MacStatsGraph::handle_motion(graph_x, graph_y);
+}
+
+/**
+ * Called when the mouse has left the graph window.
+ */
+void MacStatsFlameGraph::
+handle_leave() {
+ _label_stack.highlight_label(-1);
+ on_leave_label(_highlighted_index);
+ return;
+}
+
+/**
+ * Fills in the graph window.
+ */
+void MacStatsFlameGraph::
+handle_draw_graph(CGContextRef ctx, NSRect rect) {
+ MacStatsGraph::handle_draw_graph(ctx, rect);
+
+ CGContextSetTextMatrix(ctx, CGAffineTransformMakeScale(1, -1));
+
+ int num_user_guide_bars = get_num_user_guide_bars();
+ for (int i = 0; i < num_user_guide_bars; i++) {
+ const GuideBar &bar = get_user_guide_bar(i);
+ draw_guide_bar(ctx, bar);
+ draw_guide_label(ctx, bar);
+ }
+}
+
+/**
+ * Called when the mouse clicks the back button in the toolbar.
+ */
+void MacStatsFlameGraph::
+handle_back() {
+ if (!_back_stack.empty()) {
+ int collector_index = _back_stack.back();
+ _back_stack.pop_back();
+ set_collector_index(collector_index);
+
+ if (_back_stack.empty()) {
+ _graph_view_controller.backToolbarItemVisible = NO;
+ }
+
+ std::string window_title = get_title_text();
+ if (!is_title_unknown()) {
+ _window.title = [NSString stringWithUTF8String:window_title.c_str()];
+ }
+ }
+}
+
+/**
+ * Called when the mouse toggles the "Average" checkbox in the toolbar.
+ */
+void MacStatsFlameGraph::
+handle_toggle_average(bool state) {
+ set_average_mode(state);
+ if (state) {
+ start_animation();
+ }
+}
+
+
+/**
+ * Converts a pixel to a depth index.
+ */
+int MacStatsFlameGraph::
+pixel_to_depth(int y) const {
+ return (get_ysize() - 1 - y) / (4.000 * 5);
+}
+
+/**
+ * Draws the line for the indicated guide bar on the graph.
+ */
+void MacStatsFlameGraph::
+draw_guide_bar(CGContextRef ctx, const PStatGraph::GuideBar &bar) {
+ int x = height_to_pixel(bar._height);
+
+ if (x > 0 && x < get_xsize() - 1) {
+ // Only draw it if it's not too close to the top.
+ CGContextSetStrokeColorWithColor(ctx, [NSColor gridColor].CGColor);
+ /*switch (bar._style) {
+ case GBS_target:
+ CGContextSetRGBStrokeColor(ctx, rgb_light_gray[0], rgb_light_gray[1], rgb_light_gray[2], 1.0);
+ break;
+
+ case GBS_user:
+ CGContextSetRGBStrokeColor(ctx, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2], 1.0);
+ break;
+
+ default:
+ CGContextSetRGBStrokeColor(ctx, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2], 1.0);
+ break;
+ }*/
+ CGContextBeginPath(ctx);
+ CGContextMoveToPoint(ctx, x, 0);
+ CGContextAddLineToPoint(ctx, x, get_ysize());
+ CGContextStrokePath(ctx);
+ }
+}
+
+/**
+ * This is called during the servicing of the draw event.
+ */
+void MacStatsFlameGraph::
+draw_guide_labels(CGContextRef ctx) {
+ int num_guide_bars = get_num_guide_bars();
+ for (int i = 0; i < num_guide_bars; i++) {
+ draw_guide_label(ctx, get_guide_bar(i));
+ }
+
+ int num_user_guide_bars = get_num_user_guide_bars();
+ for (int i = 0; i < num_user_guide_bars; i++) {
+ draw_guide_label(ctx, get_user_guide_bar(i));
+ }
+}
+
+/**
+ * Draws the text for the indicated guide bar label at the top of the graph.
+ */
+void MacStatsFlameGraph::
+draw_guide_label(CGContextRef ctx, const PStatGraph::GuideBar &bar) {
+ NSColor *color;
+ color = [NSColor tertiaryLabelColor];
+ /*switch (bar._style) {
+ case GBS_target:
+ color = [NSColor colorWithDeviceRed:rgb_light_gray[0] green:rgb_light_gray[1] blue:rgb_light_gray[2] alpha:1.0];
+ break;
+
+ case GBS_user:
+ color = [NSColor colorWithDeviceRed:rgb_user_guide_bar[0] green:rgb_user_guide_bar[1] blue:rgb_user_guide_bar[2] alpha:1.0];
+ break;
+
+ default:
+ color = [NSColor colorWithDeviceRed:rgb_dark_gray[0] green:rgb_dark_gray[1] blue:rgb_dark_gray[2] alpha:1.0];
+ break;
+ }*/
+
+ int x = height_to_pixel(bar._height);
+ const std::string &label = bar._label;
+
+ const CFStringRef keys[] = {
+ (__bridge CFStringRef)NSForegroundColorAttributeName,
+ (__bridge CFStringRef)NSFontAttributeName,
+ };
+ const void *values[] = {
+ color,
+ [NSFont systemFontOfSize:0.0],
+ };
+ CFDictionaryRef attribs = CFDictionaryCreate(kCFAllocatorDefault, (const void **)keys, (const void **)values, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+
+ CFStringRef str = CFStringCreateWithCString(kCFAllocatorDefault, label.c_str(), kCFStringEncodingUTF8);
+ CFAttributedStringRef astr = CFAttributedStringCreate(kCFAllocatorDefault, str, attribs);
+ CFRelease(attribs);
+ CFRelease(str);
+
+ CTLineRef line = CTLineCreateWithAttributedString(astr);
+ CFRelease(astr);
+ CGRect bounds = CTLineGetImageBounds(line, ctx);
+ int width = bounds.size.width;
+
+ if (bar._style != GBS_user) {
+ double from_height = pixel_to_height(x - width);
+ double to_height = pixel_to_height(x + width);
+ if (find_user_guide_bar(from_height, to_height) >= 0) {
+ // Omit the label: there's a user-defined guide bar in the same space.
+ CFRelease(line);
+ return;
+ }
+ }
+
+ if (x >= 0 && x < get_xsize()) {
+ int y = bounds.size.height;
+ if (@available(macOS 11.0, *)) {
+ // Account for underlap of title bar
+ y += _window.frame.size.height - _window.contentLayoutRect.size.height;
+ }
+ CGContextSetTextPosition(ctx, x + 6, y + 6);
+ CTLineDraw(line, ctx);
+ }
+
+ CFRelease(line);
+}
+
+@implementation MacStatsFlameGraphViewController
+
+- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar {
+ return @[@"back", @"average", @"total"];
+}
+
+- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar {
+ return @[@"average", @"total"];
+}
+
+- (NSToolbarItem *) toolbar:(NSToolbar *)toolbar
+ itemForItemIdentifier:(NSString *)ident
+ willBeInsertedIntoToolbar:(BOOL)flag {
+
+ if (@available(macOS 11.0, *)) {
+ if ([ident isEqual:@"average"]) {
+ NSButton *button = [NSButton buttonWithTitle:@"Average" target:self action:@selector(handleToggleAverage:)];
+ button.image = [NSImage imageWithSystemSymbolName:@"sum" accessibilityDescription:@""];
+ button.bezelStyle = NSBezelStyleTexturedRounded;
+ button.buttonType = NSButtonTypePushOnPushOff;
+ button.bordered = YES;
+ button.toolTip = @"Average";
+ button.state = NSOffState;
+ NSToolbarItem *item = [[[NSToolbarItem alloc] initWithItemIdentifier:ident] autorelease];
+ item.label = @"Average";
+ item.view = button;
+ return item;
+ }
+ if ([ident isEqual:@"total"]) {
+ NSToolbarItem *item = [[[NSToolbarItem alloc] initWithItemIdentifier:ident] autorelease];
+ item.label = @"Total";
+ item.enabled = NO;
+ [item setBordered:YES];
+ return item;
+ }
+ }
+
+ return [super toolbar:toolbar itemForItemIdentifier:ident willBeInsertedIntoToolbar:flag];
+}
+
+- (void)handleToggleAverage:(NSButton *)button {
+ MacStatsFlameGraph *graph = (MacStatsFlameGraph *)_graph;
+ graph->handle_toggle_average(button.state == NSOnState);
+}
+
+@end
diff --git a/pandatool/src/mac-stats/macStatsGraph.h b/pandatool/src/mac-stats/macStatsGraph.h
new file mode 100644
index 0000000000..2905f60d5a
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsGraph.h
@@ -0,0 +1,139 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University. All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license. You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file macStatsGraph.h
+ * @author rdb
+ * @date 2023-08-17
+ */
+
+#ifndef MACSTATSGRAPH_H
+#define MACSTATSGRAPH_H
+
+#include "pandatoolbase.h"
+#include "macStatsGraphViewController.h"
+#include "macStatsLabelStack.h"
+#include "pmap.h"
+#include "luse.h"
+
+class MacStatsMonitor;
+
+/**
+ * This is just an abstract base class to provide a common pointer type for
+ * the various kinds of graphs that may be created for a MacStatsMonitor.
+ */
+class MacStatsGraph {
+public:
+ // What is the user adjusting by dragging the mouse in a window?
+ enum DragMode {
+ DM_none,
+ DM_scale,
+ DM_guide_bar,
+ DM_new_guide_bar,
+ DM_sizing,
+ DM_pan,
+ };
+
+public:
+ MacStatsGraph(MacStatsMonitor *monitor, MacStatsGraphViewController *controller);
+ virtual ~MacStatsGraph();
+
+ void close();
+
+ MacStatsMonitor *get_monitor() { return _monitor; }
+ NSSplitView *get_split_view() { return _split_view; }
+
+ virtual void new_collector(int collector_index);
+ virtual void new_data(int thread_index, int frame_number);
+ virtual void force_redraw()=0;
+ virtual void changed_graph_size(int graph_xsize, int graph_ysize);
+
+ virtual void set_time_units(int unit_mask);
+ virtual void set_scroll_speed(double scroll_speed);
+ void set_pause(bool pause);
+
+ void user_guide_bars_changed();
+ virtual void on_click_label(int collector_index);
+ virtual void on_enter_label(int collector_index);
+ virtual void on_leave_label(int collector_index);
+ virtual NSMenu *get_label_menu(int collector_index) const;
+ virtual std::string get_label_tooltip(int collector_index) const;
+
+ void reset_collector_color(int collector_index);
+
+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);
+
+public:
+ // These must be public, because we can't declare Objective-C friends here.
+ virtual NSMenu *get_graph_menu(int mouse_x, int mouse_y) const;
+ virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const;
+ virtual DragMode consider_drag_start(int graph_x, int graph_y);
+ virtual void set_drag_mode(DragMode drag_mode);
+
+ virtual bool handle_key(int graph_x, int graph_y, bool pressed,
+ UniChar c, unsigned short key_code);
+ virtual void handle_button_press(int graph_x, int graph_y,
+ bool double_click, int button);
+ virtual void handle_button_release(int graph_x, int graph_y);
+ virtual void handle_motion(int graph_x, int graph_y);
+ virtual void handle_leave();
+ virtual void handle_scroll();
+ virtual void handle_wheel(int graph_x, int graph_y, double dx, double dy);
+ virtual void handle_magnify(int graph_x, int graph_y, double scale);
+ virtual void handle_draw_graph(CGContextRef ctx, NSRect rect);
+ virtual void handle_draw_graph_overhang(CGContextRef ctx, NSRect rect);
+ virtual void handle_draw_scale_area(CGContextRef ctx, NSRect rect);
+ virtual void handle_back();
+ virtual void handle_timer();
+
+protected:
+ CGColorRef _background_color;
+
+ MacStatsMonitor *_monitor;
+ NSWindow *_window;
+ NSView *_graph_view;
+ MacStatsGraphViewController *_graph_view_controller;
+ NSView *_scale_area = nullptr;
+ NSSplitView *_split_view = nullptr;
+ NSScrollView *_sidebar_view = nullptr;
+ MacStatsLabelStack _label_stack;
+
+ CGContextRef _ctx = nullptr;
+ int _bitmap_xsize, _bitmap_ysize;
+
+ DragMode _drag_mode;
+ DragMode _potential_drag_mode;
+ int _drag_start_x, _drag_start_y;
+ double _drag_scale_start;
+ int _drag_guide_bar;
+
+ int _highlighted_index = -1;
+
+ bool _pause;
+
+ NSTimer *_animation_timer = nil;
+ double _time = 0.0;
+
+ static const CGFloat rgb_white[4];
+ static const CGFloat rgb_light_gray[4];
+ static const CGFloat rgb_dark_gray[4];
+ static const CGFloat rgb_black[4];
+ static const CGFloat rgb_user_guide_bar[4];
+
+private:
+ void setup_bitmap(int xsize, int ysize, double scale);
+ void release_bitmap();
+};
+
+#endif
diff --git a/pandatool/src/mac-stats/macStatsGraph.mm b/pandatool/src/mac-stats/macStatsGraph.mm
new file mode 100644
index 0000000000..466d875df3
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsGraph.mm
@@ -0,0 +1,491 @@
+/**
+ * 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 macStatsGraph.mm
+ * @author rdb
+ * @date 2023-08-17
+ */
+
+#include "macStatsGraph.h"
+#include "macStatsGraphView.h"
+#include "macStatsMonitor.h"
+#include "macStatsLabelStack.h"
+
+const CGFloat MacStatsGraph::rgb_light_gray[4] = {
+ 0x9a / (CGFloat)0xff, 0x9a / (CGFloat)0xff, 0x9a / (CGFloat)0xff, 1.0,
+};
+const CGFloat MacStatsGraph::rgb_dark_gray[4] = {
+ 0xb0 / (CGFloat)0xff, 0xb0 / (CGFloat)0xff, 0xb0 / (CGFloat)0xff, 1.0,
+};
+const CGFloat MacStatsGraph::rgb_user_guide_bar[4] = {
+ 0x82 / (CGFloat)0xff, 0x96 / (CGFloat)0xff, 0xff / (CGFloat)0xff, 1.0,
+};
+
+static const NSInteger style_mask = NSWindowStyleMaskTitled
+ | NSWindowStyleMaskClosable
+ | NSWindowStyleMaskMiniaturizable
+ | NSWindowStyleMaskResizable;
+
+/**
+ *
+ */
+MacStatsGraph::
+MacStatsGraph(MacStatsMonitor *monitor, MacStatsGraphViewController *controller) :
+ _monitor(monitor)
+{
+ _background_color = CGColorCreateGenericRGB(0.0, 0.0, 0.0, 0.0);
+
+ _drag_mode = DM_none;
+ _potential_drag_mode = DM_none;
+ _drag_scale_start = 0.0f;
+
+ _pause = false;
+
+ NSInteger this_style_mask = style_mask;
+ if (@available(macOS 11.0, *)) {
+ this_style_mask |= NSWindowStyleMaskFullSizeContentView;
+ }
+
+ _window = [NSWindow alloc];
+ [_window initWithContentRect:NSMakeRect(100, 500, 500, 150)
+ styleMask:this_style_mask
+ backing:NSBackingStoreBuffered
+ defer:NO];
+ _window.releasedWhenClosed = NO;
+ _window.titlebarAppearsTransparent = NO;
+ _window.excludedFromWindowsMenu = NO;
+
+ _window.contentMinSize = NSMakeSize(68, _window.contentView.frame.size.height - _window.contentLayoutRect.size.height);
+
+ _graph_view_controller = [controller initWithGraph:this];
+ _graph_view = ((MacStatsGraphViewController *)_graph_view_controller).graphView;
+
+ // Get notified when the window closes.
+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+ [center addObserver:_graph_view_controller
+ selector:@selector(windowWillClose:)
+ name:NSWindowWillCloseNotification
+ object:_window];
+}
+
+/**
+ *
+ */
+MacStatsGraph::
+~MacStatsGraph() {
+ if (_animation_timer != nil) {
+ [_animation_timer invalidate];
+ [_animation_timer release];
+ _animation_timer = nil;
+ }
+
+ _monitor = nullptr;
+ release_bitmap();
+
+ _label_stack.clear_labels();
+
+ [_graph_view_controller release];
+ [_window release];
+
+ CGColorRelease(_background_color);
+}
+
+/**
+ *
+ */
+void MacStatsGraph::
+close() {
+ [_window close];
+}
+
+/**
+ * Called whenever a new Collector definition is received from the client.
+ */
+void MacStatsGraph::
+new_collector(int new_collector) {
+}
+
+/**
+ * Called whenever new data arrives.
+ */
+void MacStatsGraph::
+new_data(int thread_index, int frame_number) {
+}
+
+/**
+ * Called when the user has resized the window, forcing a resize of the graph.
+ */
+void MacStatsGraph::
+changed_graph_size(int graph_xsize, int graph_ysize) {
+}
+
+/**
+ * Called when the user selects a new time units from the monitor pulldown
+ * menu, this should adjust the units for the graph to the indicated mask if
+ * it is a time-based graph.
+ */
+void MacStatsGraph::
+set_time_units(int unit_mask) {
+}
+
+/**
+ * Called when the user selects a new scroll speed from the monitor pulldown
+ * menu, this should adjust the speed for the graph to the indicated value.
+ */
+void MacStatsGraph::
+set_scroll_speed(double scroll_speed) {
+}
+
+/**
+ * Changes the pause flag for the graph. When this flag is true, the graph
+ * does not update in response to new data.
+ */
+void MacStatsGraph::
+set_pause(bool pause) {
+ _pause = pause;
+}
+
+/**
+ * Called when the user guide bars have been changed.
+ */
+void MacStatsGraph::
+user_guide_bars_changed() {
+ if (_scale_area != nullptr) {
+ _scale_area.needsDisplay = YES;
+ }
+ _graph_view.needsDisplay = YES;
+}
+
+/**
+ * Called when the user single-clicks on a label.
+ */
+void MacStatsGraph::
+on_click_label(int collector_index) {
+}
+
+/**
+ * Called when the user hovers the mouse over a label.
+ */
+void MacStatsGraph::
+on_enter_label(int collector_index) {
+ if (collector_index != _highlighted_index) {
+ _highlighted_index = collector_index;
+ force_redraw();
+ }
+}
+
+/**
+ * Called when the user's mouse cursor leaves a label.
+ */
+void MacStatsGraph::
+on_leave_label(int collector_index) {
+ if (collector_index == _highlighted_index && collector_index != -1) {
+ _highlighted_index = -1;
+ force_redraw();
+ }
+}
+
+/**
+ * Called when the mouse right-clicks on a label, and should return the menu
+ * that should pop up.
+ */
+NSMenu *MacStatsGraph::
+get_label_menu(int collector_index) const {
+ return nil;
+}
+
+/**
+ * Called when the mouse hovers over a label, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string MacStatsGraph::
+get_label_tooltip(int collector_index) const {
+ return std::string();
+}
+
+/**
+ * Turns on the animation timer, if it hasn't already been turned on.
+ */
+void MacStatsGraph::
+start_animation() {
+ if (_animation_timer != nil) {
+ return;
+ }
+
+ _time = 0.0;
+ _animation_timer = [NSTimer scheduledTimerWithTimeInterval:1 / 60.0 target:_graph_view selector:@selector(handleTimer:) userInfo:nil repeats:YES];
+ [_animation_timer retain];
+}
+
+/**
+ * Overridden by a derived class to implement an animation. If it returns
+ * false, the animation timer is stopped.
+ */
+bool MacStatsGraph::
+animate(double time, double dt) {
+ return false;
+}
+
+/**
+ * Returns the current window dimensions.
+ */
+void MacStatsGraph::
+get_window_state(int &x, int &y, int &width, int &height,
+ bool &maximized, bool &minimized) const {
+
+ NSRect screen_frame = _window.screen.visibleFrame;
+
+ NSRect frame = _window.frame;
+ x = frame.origin.x - screen_frame.origin.x;
+ y = (screen_frame.origin.y + screen_frame.size.height) - (frame.origin.y + frame.size.height);
+ width = frame.size.width;
+ height = frame.size.height;
+ maximized = _window.zoomed;
+ minimized = _window.miniaturized;
+}
+
+/**
+ * Called to restore the graph window to its previous dimensions.
+ */
+void MacStatsGraph::
+set_window_state(int x, int y, int width, int height,
+ bool maximized, bool minimized) {
+
+ NSRect screen_frame = _window.screen.visibleFrame;
+
+ NSRect frame;
+ frame.origin.x = screen_frame.origin.x + x;
+ frame.origin.y = (screen_frame.origin.y + screen_frame.size.height) - (y + height);
+ frame.size.width = width;
+ frame.size.height = height;
+ [_window setFrame:frame display:NO];
+
+ if (maximized != _window.zoomed) {
+ [_window zoom:_window];
+ }
+
+ if (minimized != _window.miniaturized) {
+ if (minimized) {
+ [_window miniaturize:_window];
+ } else {
+ [_window deminiaturize:_window];
+ }
+ }
+}
+
+/**
+ * Called when the given collector has changed colors.
+ */
+void MacStatsGraph::
+reset_collector_color(int collector_index) {
+ force_redraw();
+ _label_stack.update_label_color(collector_index);
+}
+
+/**
+ * Called when the mouse right-clicks on the graph, and should return the menu
+ * that should pop up.
+ */
+NSMenu *MacStatsGraph::
+get_graph_menu(int mouse_x, int mouse_y) const {
+ return nil;
+}
+
+/**
+ * Called when the mouse hovers over the graph, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string MacStatsGraph::
+get_graph_tooltip(int mouse_x, int mouse_y) const {
+ return std::string();
+}
+
+/**
+ * Based on the mouse position within the graph window, look for draggable
+ * things the mouse might be hovering over and return the appropriate DragMode
+ * enum or DM_none if nothing is indicated.
+ */
+MacStatsGraph::DragMode MacStatsGraph::
+consider_drag_start(int graph_x, int graph_y) {
+ return DM_none;
+}
+
+/**
+ * This should be called whenever the drag mode needs to change state. It
+ * provides hooks for a derived class to do something special.
+ */
+void MacStatsGraph::
+set_drag_mode(MacStatsGraph::DragMode drag_mode) {
+ _drag_mode = drag_mode;
+}
+
+/**
+ *
+ */
+bool MacStatsGraph::
+handle_key(int graph_x, int graph_y, bool pressed, UniChar c, unsigned short key_code) {
+ return false;
+}
+
+/**
+ * Called when the mouse button is depressed within the window, or any nested
+ * window.
+ */
+void MacStatsGraph::
+handle_button_press(int graph_x, int graph_y, bool double_click, int button) {
+ if (_potential_drag_mode != DM_none && button == 1) {
+ set_drag_mode(_potential_drag_mode);
+ _drag_start_x = graph_x;
+ _drag_start_y = graph_y;
+ // SetCapture(_window);
+ }
+}
+
+/**
+ * Called when the mouse button is released within the window, or any nested
+ * window.
+ */
+void MacStatsGraph::
+handle_button_release(int graph_x, int graph_y) {
+ set_drag_mode(DM_none);
+ // ReleaseCapture();
+
+ return handle_motion(graph_x, graph_y);
+}
+
+/**
+ * Called when the mouse is moved within the window, or any nested window.
+ */
+void MacStatsGraph::
+handle_motion(int graph_x, int graph_y) {
+ _potential_drag_mode = consider_drag_start(graph_x, graph_y);
+}
+
+/**
+ * Called when the mouse has left the graph window.
+ */
+void MacStatsGraph::
+handle_leave() {
+}
+
+/**
+ * Called when the mouse is moved within the graph window.
+ */
+void MacStatsGraph::
+handle_scroll() {
+ force_redraw();
+}
+
+/**
+ *
+ */
+void MacStatsGraph::
+handle_wheel(int graph_x, int graph_y, double dx, double dy) {
+}
+
+/**
+ *
+ */
+void MacStatsGraph::
+handle_magnify(int graph_x, int graph_y, double scale) {
+}
+
+/**
+ *
+ */
+void MacStatsGraph::
+handle_timer() {
+ _time += 1.0 / 60.0;
+
+ if (!animate(_time, 1.0 / 60.0)) {
+ [_animation_timer invalidate];
+ [_animation_timer release];
+ _animation_timer = nil;
+ }
+}
+
+/**
+ * Fills in the graph window.
+ */
+void MacStatsGraph::
+handle_draw_graph(CGContextRef ctx, NSRect rect) {
+ CGContextSetBlendMode(ctx, kCGBlendModeCopy);
+ CGContextSetInterpolationQuality(ctx, kCGInterpolationNone);
+
+ // Quantize this so that changed_graph_size will always call force_redraw()
+ NSRect full_rect = _graph_view.bounds;
+ full_rect.size.width = (int)full_rect.size.width;
+ full_rect.size.height = (int)full_rect.size.height;
+
+ CGSize size = CGContextConvertSizeToDeviceSpace(ctx, full_rect.size);
+ int width = abs((int)size.width);
+ int height = abs((int)size.height);
+
+ if (_ctx == nullptr || _bitmap_xsize != width || _bitmap_ysize != height) {
+ if (_ctx == nullptr && _scale_area != nullptr) {
+ _scale_area.needsDisplay = YES;
+ }
+ setup_bitmap(width, height, _window.backingScaleFactor);
+
+ changed_graph_size(full_rect.size.width, full_rect.size.height);
+ }
+
+ CGImageRef image = CGBitmapContextCreateImage(_ctx);
+ CGContextDrawImage(ctx, CGRectMake(0, 0, full_rect.size.width, full_rect.size.height), image);
+ CGImageRelease(image);
+}
+
+/**
+ * Fills in the graph window overhang, which is the area outside the graph
+ * bounds that may become visible momentarily due to scroll elasticity.
+ */
+void MacStatsGraph::
+handle_draw_graph_overhang(CGContextRef ctx, NSRect rect) {
+}
+
+/**
+ * Fills in the scale area.
+ */
+void MacStatsGraph::
+handle_draw_scale_area(CGContextRef ctx, NSRect rect) {
+}
+
+/**
+ * Called when the mouse clicks the back button in the toolbar.
+ */
+void MacStatsGraph::
+handle_back() {
+}
+
+/**
+ * Sets up a backing-store bitmap of the indicated size.
+ */
+void MacStatsGraph::
+setup_bitmap(int xsize, int ysize, double scale) {
+ release_bitmap();
+
+ _bitmap_xsize = xsize;
+ _bitmap_ysize = ysize;
+
+ // Ostensibly, a layer context is more efficient, but I tried it and the
+ // performance was horrible compared to a bitmap context.
+ _ctx = CGBitmapContextCreate(nullptr, xsize, ysize, 8, 0, CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB), kCGImageAlphaPremultipliedLast);
+ CGContextSetBlendMode(_ctx, kCGBlendModeCopy);
+ CGContextScaleCTM(_ctx, scale, scale);
+}
+
+/**
+ * Frees the backing-store bitmap created by setup_bitmap().
+ */
+void MacStatsGraph::
+release_bitmap() {
+ if (_ctx != nullptr) {
+ CGContextRelease(_ctx);
+ _ctx = nullptr;
+ }
+}
diff --git a/pandatool/src/mac-stats/macStatsGraphView.h b/pandatool/src/mac-stats/macStatsGraphView.h
new file mode 100644
index 0000000000..efd0d0c6b0
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsGraphView.h
@@ -0,0 +1,31 @@
+/**
+ * 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 macStatsGraphView.h
+ * @author rdb
+ * @date 2023-08-18
+ */
+
+#ifndef MACSTATSGRAPHVIEW_H
+#define MACSTATSGRAPHVIEW_H
+
+#include "cocoa_compat.h"
+
+class MacStatsGraph;
+
+@interface MacStatsGraphView : NSView {
+ @public
+ MacStatsGraph *_graph;
+}
+
+- (id)initWithGraph:(MacStatsGraph *)graph;
+- (void)drawRect:(NSRect)dirtyRect;
+
+@end
+
+#endif
diff --git a/pandatool/src/mac-stats/macStatsGraphView.mm b/pandatool/src/mac-stats/macStatsGraphView.mm
new file mode 100644
index 0000000000..013b424139
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsGraphView.mm
@@ -0,0 +1,158 @@
+/**
+ * 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 macStatsGraphView.mm
+ * @author rdb
+ * @date 2023-08-18
+ */
+
+#include "macStatsGraphView.h"
+#include "macStatsGraph.h"
+#include "macStatsStripChart.h"
+#include "macStatsFlameGraph.h"
+#include "macStatsTimeline.h"
+#include "macStatsScaleArea.h"
+
+@implementation MacStatsGraphView
+
+- (id)initWithGraph:(MacStatsGraph *)graph {
+ if (self = [super init]) {
+ _graph = graph;
+
+ self.translatesAutoresizingMaskIntoConstraints = NO;
+
+ NSTrackingArea *area = [[NSTrackingArea alloc] initWithRect:NSZeroRect options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInKeyWindow | NSTrackingInVisibleRect) owner:self userInfo:nil];
+ [self addTrackingArea:area];
+ [area release];
+
+ [self addToolTipRect:NSMakeRect(0, 0, 1000, 1000) owner:self userData:nil];
+ }
+
+ return self;
+}
+
+- (BOOL)isFlipped {
+ return YES;
+}
+
+- (BOOL)acceptsFirstResponder {
+ return YES;
+}
+
+- (void)keyDown:(NSEvent *)event {
+ if (!event.isARepeat) {
+ NSPoint pos = [self convertPoint:event.locationInWindow fromView:nil];
+ UniChar c = 0;
+ NSString *str = [event charactersIgnoringModifiers];
+ if (str != nil && str.length == 1) {
+ c = [str characterAtIndex:0];
+ }
+ _graph->handle_key(pos.x, pos.y, true, c, event.keyCode);
+ }
+}
+
+- (void)keyUp:(NSEvent *)event {
+ NSPoint pos = [self convertPoint:event.locationInWindow fromView:nil];
+ UniChar c = 0;
+ NSString *str = [event charactersIgnoringModifiers];
+ if (str != nil && str.length == 1) {
+ c = [str characterAtIndex:0];
+ }
+ _graph->handle_key(pos.x, pos.y, false, c, event.keyCode);
+}
+
+- (void)mouseDown:(NSEvent *)event {
+ NSPoint pos = [self convertPoint:event.locationInWindow fromView:nil];
+ _graph->handle_button_press(pos.x, pos.y, event.clickCount > 1, event.buttonNumber);
+}
+
+- (void)mouseUp:(NSEvent *)event {
+ NSPoint pos = [self convertPoint:event.locationInWindow fromView:nil];
+ _graph->handle_button_release(pos.x, pos.y);
+}
+
+- (void)mouseDragged:(NSEvent *)event {
+ NSPoint pos = [self convertPoint:event.locationInWindow fromView:nil];
+ _graph->handle_motion(pos.x, pos.y);
+}
+
+- (void)mouseMoved:(NSEvent *)event {
+ NSPoint pos = [self convertPoint:event.locationInWindow fromView:nil];
+ _graph->handle_motion(pos.x, pos.y);
+}
+
+- (void)mouseExited:(NSEvent *)event {
+ _graph->handle_leave();
+}
+
+- (void)scrollWheel:(NSEvent *)event {
+ [super scrollWheel:event];
+ if (event.deltaX != 0) {
+ NSPoint pos = [self convertPoint:event.locationInWindow fromView:nil];
+ _graph->handle_wheel(pos.x, pos.y, event.deltaX, event.deltaY);
+ }
+}
+
+- (void)magnifyWithEvent:(NSEvent *)event {
+ NSPoint pos = [self convertPoint:event.locationInWindow fromView:nil];
+ _graph->handle_magnify(pos.x, pos.y, event.magnification);
+}
+
+- (void)handleTimer:(NSTimer *)timer {
+ _graph->handle_timer();
+}
+
+- (void)viewDidChangeEffectiveAppearance {
+ if (_graph != nullptr) {
+ // Don't call this initially
+ if (self.window != nil) {
+ [NSAppearance setCurrentAppearance:self.effectiveAppearance];
+ _graph->force_redraw();
+ }
+ }
+}
+
+- (void)drawRect:(NSRect)dirtyRect {
+ if (_graph != nullptr) {
+ CGContextRef ctx = [NSGraphicsContext currentContext].CGContext;
+ _graph->handle_draw_graph(ctx, dirtyRect);
+ }
+}
+
+// Not called when building with macOS 14 SDK due to change in clipsToBounds
+// (but there it simply calls drawRect with the overhang region)
+#if __MAC_OS_X_VERSION_MAX_ALLOWED < 140000
+- (void)drawBackgroundOverhangInRect:(NSRect)dirtyRect {
+ if (_graph != nullptr) {
+ CGContextRef ctx = [NSGraphicsContext currentContext].CGContext;
+ _graph->handle_draw_graph_overhang(ctx, dirtyRect);
+ }
+}
+#endif
+
+- (NSMenu *)menuForEvent:(NSEvent *)event {
+ if (_graph != nullptr) {
+ NSPoint pos = [self convertPoint:event.locationInWindow fromView:nil];
+ return _graph->get_graph_menu(pos.x, pos.y);
+ }
+ return nil;
+}
+
+- (NSString *)view:(NSView *)view
+ stringForToolTip:(NSToolTipTag)tag
+ point:(NSPoint)point
+ userData:(void *)data {
+
+ if (_graph != nullptr) {
+ std::string text = _graph->get_graph_tooltip(point.x, point.y);
+ return [NSString stringWithUTF8String:text.c_str()];
+ }
+ return @"";
+}
+
+@end
diff --git a/pandatool/src/mac-stats/macStatsGraphViewController.h b/pandatool/src/mac-stats/macStatsGraphViewController.h
new file mode 100644
index 0000000000..d688a656f3
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsGraphViewController.h
@@ -0,0 +1,42 @@
+/**
+ * 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 macStatsGraphViewController.h
+ * @author rdb
+ * @date 2023-08-28
+ */
+
+#ifndef MACSTATSGRAPHVIEWCONTROLLER_H
+#define MACSTATSGRAPHVIEWCONTROLLER_H
+
+#include "macStatsGraphView.h"
+
+#import
+
+class MacStatsGraph;
+
+@interface MacStatsGraphViewController : NSViewController {
+ @protected
+ MacStatsGraph *_graph;
+}
+
+- (id)initWithGraph:(MacStatsGraph *)graph;
+- (MacStatsGraphView *)graphView;
+- (BOOL)backToolbarItemVisible;
+- (void)setBackToolbarItemVisible:(BOOL)show;
+
+@end
+
+@interface MacStatsScrollableGraphViewController : MacStatsGraphViewController
+
+- (MacStatsGraphView *)graphView;
+- (NSClipView *)clipView;
+
+@end
+
+#endif
diff --git a/pandatool/src/mac-stats/macStatsGraphViewController.mm b/pandatool/src/mac-stats/macStatsGraphViewController.mm
new file mode 100644
index 0000000000..84f450f717
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsGraphViewController.mm
@@ -0,0 +1,224 @@
+/**
+ * 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 macStatsGraphViewController.mm
+ * @author rdb
+ * @date 2023-08-28
+ */
+
+#include "macStatsGraphViewController.h"
+#include "macStatsGraph.h"
+#include "macStatsMonitor.h"
+#include "cocoa_compat.h"
+
+@implementation MacStatsGraphViewController
+
+- (id)initWithGraph:(MacStatsGraph *)graph {
+ if (self = [super init]) {
+ _graph = graph;
+ }
+
+ return self;
+}
+
+- (void)windowWillClose:(NSNotification *)notification {
+ MacStatsGraph *graph = _graph;
+ if (graph != nullptr) {
+ MacStatsMonitor *monitor = graph->get_monitor();
+ if (monitor != nullptr) {
+ _graph = nullptr;
+ monitor->remove_graph(graph);
+ }
+ }
+}
+
+- (void)loadView {
+ NSView *graph_view = [[MacStatsGraphView alloc] initWithGraph:_graph];
+ NSView *background;
+ if (@available(macOS 10.14, *)) {
+ NSVisualEffectView *effect_view = [[NSVisualEffectView alloc] init];
+ effect_view.material = (NSVisualEffectMaterial)18;//NSVisualEffectMaterialContentBackground;
+ background = effect_view;
+ } else {
+ background = [[NSView alloc] init];
+ background.wantsLayer = YES;
+ background.layer.backgroundColor = [NSColor controlBackgroundColor].CGColor;
+ }
+ [background addSubview:graph_view];
+ self.view = background;
+
+ [graph_view.widthAnchor constraintEqualToAnchor:background.widthAnchor].active = YES;
+ [graph_view.heightAnchor constraintEqualToAnchor:background.heightAnchor].active = YES;
+ [graph_view release];
+ [background release];
+}
+
+- (MacStatsGraphView *)graphView {
+ return (MacStatsGraphView *)self.view.subviews[0];
+}
+
+- (void)handleSplitViewResize:(NSNotification *)notification {
+ NSSplitView *split_view = (NSSplitView *)notification.object;
+ NSWindow *window = split_view.window;
+ NSToolbar *toolbar = window.toolbar;
+ if ([split_view isSubviewCollapsed:split_view.arrangedSubviews[0]]) {
+ if (toolbar.items[0].itemIdentifier != NSToolbarToggleSidebarItemIdentifier) {
+ [toolbar insertItemWithItemIdentifier:NSToolbarToggleSidebarItemIdentifier atIndex:0];
+ }
+ } else {
+ if (toolbar.items[0].itemIdentifier == NSToolbarToggleSidebarItemIdentifier) {
+ [toolbar removeItemAtIndex:0];
+ }
+ }
+}
+
+- (BOOL)backToolbarItemVisible {
+ NSToolbar *toolbar = self.view.window.toolbar;
+ if ([toolbar.items[0].itemIdentifier isEqual:@"back"] ||
+ [toolbar.items[1].itemIdentifier isEqual:@"back"]) {
+ return YES;
+ } else {
+ return NO;
+ }
+}
+
+- (void)setBackToolbarItemVisible:(BOOL)show {
+ NSToolbar *toolbar = self.view.window.toolbar;
+ if ([toolbar.items[1].itemIdentifier isEqual:@"back"]) {
+ if (!show) {
+ [toolbar removeItemAtIndex:1];
+ }
+ }
+ else if ([toolbar.items[0].itemIdentifier isEqual:@"back"]) {
+ if (!show) {
+ [toolbar removeItemAtIndex:0];
+ }
+ }
+ else if (show) {
+ // Insert it after the sidebar toggle, if we have one.
+ int index = ([toolbar.items[0].itemIdentifier isEqual:NSToolbarToggleSidebarItemIdentifier]);
+ [toolbar insertItemWithItemIdentifier:@"back" atIndex:index];
+ }
+}
+
+- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar {
+ return @[];
+}
+
+- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar {
+ return @[];
+}
+
+- (NSToolbarItem *) toolbar:(NSToolbar *)toolbar
+ itemForItemIdentifier:(NSString *)ident
+ willBeInsertedIntoToolbar:(BOOL)flag {
+
+ if (@available(macOS 11.0, *)) {
+ if ([ident isEqual:@"sep"]) {
+ return [NSTrackingSeparatorToolbarItem trackingSeparatorToolbarItemWithIdentifier:@"sep" splitView:_graph->get_split_view() dividerIndex:0];
+ }
+
+ if ([ident isEqual:@"back"]) {
+ NSToolbarItem *item = [[[NSToolbarItem alloc] initWithItemIdentifier:ident] autorelease];
+ item.label = @"Back";
+ item.image = [NSImage imageWithSystemSymbolName:@"chevron.left" accessibilityDescription:@""];
+ item.target = self;
+ item.action = @selector(handleBack:);
+ [item setNavigational:YES];
+ [item setBordered:YES];
+ return item;
+ }
+ }
+
+ return nil;
+}
+
+- (void)handleSetAsFocus:(NSMenuItem *)item {
+ _graph->on_click_label(item.tag);
+}
+
+- (void)handleChangeColor:(NSMenuItem *)item {
+ _graph->get_monitor()->choose_collector_color(item.tag);
+}
+
+- (void)handleResetColor:(NSMenuItem *)item {
+ _graph->get_monitor()->reset_collector_color(item.tag);
+}
+
+- (void)handleBack:(id)sender {
+ _graph->handle_back();
+}
+
+@end
+
+@implementation MacStatsScrollableGraphViewController
+
+- (void)loadView {
+ NSView *graph_view = [[MacStatsGraphView alloc] initWithGraph:_graph];
+ NSScrollView *scroll = [[NSScrollView alloc] init];
+ scroll.hasHorizontalScroller = NO;
+ scroll.hasVerticalScroller = YES;
+ scroll.horizontalScrollElasticity = NSScrollElasticityNone;
+ scroll.usesPredominantAxisScrolling = NO;
+ scroll.drawsBackground = YES;
+ scroll.scrollerStyle = NSScrollerStyleOverlay;
+ scroll.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
+ //scroll.translatesAutoresizingMaskIntoConstraints = NO;
+ scroll.automaticallyAdjustsContentInsets = YES;
+ scroll.documentView = graph_view;
+ self.view = scroll;
+
+ [graph_view.widthAnchor constraintEqualToAnchor:scroll.widthAnchor].active = YES;
+
+ if (@available(macOS 11.0, *)) {
+ [graph_view.heightAnchor constraintGreaterThanOrEqualToAnchor:((NSLayoutGuide *)[scroll safeAreaLayoutGuide]).heightAnchor].active = YES;
+ } else {
+ [graph_view.heightAnchor constraintGreaterThanOrEqualToAnchor:scroll.heightAnchor].active = YES;
+ }
+
+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+ [center addObserver:self
+ selector:@selector(handleScroll:)
+ name:NSScrollViewDidLiveScrollNotification
+ object:scroll];
+ [scroll release];
+ [graph_view release];
+}
+
+- (MacStatsGraphView *)graphView {
+ return (MacStatsGraphView *)((NSScrollView *)self.view).documentView;
+}
+
+- (NSClipView *)clipView {
+ return ((NSScrollView *)self.view).contentView;
+}
+
+- (void)viewDidLayout {
+ if (_graph != nullptr) {
+ _graph->handle_scroll();
+ }
+}
+
+- (void)handleScroll:(NSNotification *)notification {
+ if (_graph != nullptr) {
+ _graph->handle_scroll();
+ }
+}
+
+- (void)handleSideScroll:(NSNotification *)notification {
+ // Graph view is flipped, side bar isn't, so we need to convert coordinates
+ NSScrollView *side_sv = ((NSScrollView *)notification.object);
+ NSScrollView *graph_sv = (NSScrollView *)self.view;
+ NSPoint point;
+ point.x = 0;
+ point.y = self.graphView.frame.size.height - (side_sv.documentVisibleRect.size.height + side_sv.documentVisibleRect.origin.y) - graph_sv.contentInsets.top;
+ [graph_sv.contentView scrollToPoint:point];
+ [graph_sv reflectScrolledClipView:graph_sv.contentView];
+}
+
+@end
diff --git a/pandatool/src/mac-stats/macStatsLabel.h b/pandatool/src/mac-stats/macStatsLabel.h
new file mode 100644
index 0000000000..1171ba25a3
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsLabel.h
@@ -0,0 +1,58 @@
+/**
+ * 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 macStatsLabel.h
+ * @author rdb
+ * @date 2023-08-18
+ */
+
+#ifndef MACSTATSLABEL_H
+#define MACSTATSLABEL_H
+
+#include "pandatoolbase.h"
+#include "luse.h"
+
+#include "cocoa_compat.h"
+
+class MacStatsMonitor;
+class MacStatsGraph;
+
+/**
+ * A text label that will draw in color appropriate for a particular
+ * collector. It also responds when the user double-clicks on it. This is
+ * handy for putting colored labels on strip charts.
+ */
+@interface MacStatsLabel : NSTextField {
+ @private
+ MacStatsGraph *_graph;
+ int _thread_index;
+ int _collector_index;
+ bool _highlight;
+ bool _mouse_within;
+ NSColor *_fg_color;
+ NSColor *_highlight_fg_color;
+ NSColor *_bg_color;
+ NSColor *_highlight_bg_color;
+}
+
+- (id)initWithText:(NSString *)text
+ graph:(MacStatsGraph *)graph
+ threadIndex:(int)thread_index
+ collectorIndex:(int)collector_index;
+
+- (int)threadIndex;
+- (int)collectorIndex;
+
+- (void)updateColor;
+
+- (BOOL)highlight;
+- (void)setHighlight:(BOOL)highlight;
+
+@end
+
+#endif
diff --git a/pandatool/src/mac-stats/macStatsLabel.mm b/pandatool/src/mac-stats/macStatsLabel.mm
new file mode 100644
index 0000000000..3169c58f8d
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsLabel.mm
@@ -0,0 +1,137 @@
+/**
+ * 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 macStatsLabel.mm
+ * @author rdb
+ * @date 2023-08-18
+ */
+
+#include "macStatsLabel.h"
+#include "macStatsMonitor.h"
+#include "macStatsGraph.h"
+
+@implementation MacStatsLabel
+
+- (id)initWithText:(NSString *)text
+ graph:(MacStatsGraph *)graph
+ threadIndex:(int)thread_index
+ collectorIndex:(int)collector_index {
+ if (self = [super init]) {
+ _graph = graph;
+ _thread_index = thread_index;
+ _collector_index = collector_index;
+ _bg_color = nil;
+ _highlight_bg_color = nil;
+
+ [self setStringValue:text];
+ self.bezeled = NO;
+ self.drawsBackground = YES;
+ self.selectable = NO;
+ self.editable = NO;
+ self.lineBreakMode = NSLineBreakByTruncatingTail;
+ //self.autoresizingMask = NSViewWidthSizable | NSViewMaxXMargin | NSViewMinXMargin;
+ //self.translatesAutoresizingMaskIntoConstraints = NO;
+
+ NSTrackingArea *area = [[NSTrackingArea alloc] initWithRect:NSZeroRect options:(NSTrackingMouseEnteredAndExited | NSTrackingActiveInKeyWindow | NSTrackingInVisibleRect) owner:self userInfo:nil];
+ [self addTrackingArea:area];
+ [area release];
+
+ [self addToolTipRect:NSMakeRect(0, 0, 1000, 1000) owner:self userData:nil];
+
+ [self updateColor];
+ }
+
+ return self;
+}
+
+- (void)dealloc {
+ [_bg_color release];
+ [_highlight_bg_color release];
+ [super dealloc];
+}
+
+- (NSSize)intrinsicContentSize {
+ // Allow resizing down.
+ NSSize size = [super intrinsicContentSize];
+ return NSMakeSize(NSViewNoIntrinsicMetric, size.height);
+}
+
+- (int)threadIndex {
+ return _thread_index;
+}
+
+- (int)collectorIndex {
+ return _collector_index;
+}
+
+- (void)updateColor {
+ if (_bg_color != nil) {
+ [_bg_color release];
+ }
+ if (_highlight_bg_color != nil) {
+ [_highlight_bg_color release];
+ }
+
+ _bg_color = [NSColor colorWithCGColor:_graph->get_monitor()->get_collector_color(_collector_index, false)];
+ _highlight_bg_color = [NSColor colorWithCGColor:_graph->get_monitor()->get_collector_color(_collector_index, true)];
+
+ [_bg_color retain];
+ [_highlight_bg_color retain];
+
+ _fg_color = _graph->get_monitor()->get_collector_text_color(_collector_index, false);
+ _highlight_fg_color = _graph->get_monitor()->get_collector_text_color(_collector_index, true);
+
+ [self setHighlight:_highlight];
+}
+
+- (BOOL)highlight {
+ return _highlight;
+}
+
+- (void)setHighlight:(BOOL)highlight {
+ _highlight = highlight;
+ if (highlight || _mouse_within) {
+ self.backgroundColor = _highlight_bg_color;
+ self.textColor = _highlight_fg_color;
+ } else {
+ self.backgroundColor = _bg_color;
+ self.textColor = _fg_color;
+ }
+}
+
+- (void)mouseEntered:(NSEvent *)event {
+ _mouse_within = true;
+ self.backgroundColor = _highlight_bg_color;
+ self.textColor = _highlight_fg_color;
+}
+
+- (void)mouseExited:(NSEvent *)event {
+ _mouse_within = false;
+ [self setHighlight:_highlight];
+}
+
+- (void)mouseDown:(NSEvent *)event {
+ if (event.buttonNumber == 0 && event.clickCount == 2) {
+ _graph->on_click_label(_collector_index);
+ }
+}
+
+- (NSMenu *)menuForEvent:(NSEvent *)event {
+ return _graph->get_label_menu(_collector_index);
+}
+
+- (NSString *)view:(NSView *)view
+ stringForToolTip:(NSToolTipTag)tag
+ point:(NSPoint)point
+ userData:(void *)data {
+
+ std::string text = _graph->get_label_tooltip(_collector_index);
+ return [NSString stringWithUTF8String:text.c_str()];
+}
+
+@end
diff --git a/pandatool/src/mac-stats/macStatsLabelStack.h b/pandatool/src/mac-stats/macStatsLabelStack.h
new file mode 100644
index 0000000000..9aabb3b2f2
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsLabelStack.h
@@ -0,0 +1,57 @@
+/**
+ * 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 macStatsLabelStack.h
+ * @author rdb
+ * @date 2023-08-17
+ */
+
+#ifndef MACSTATSLABELSTACK_H
+#define MACSTATSLABELSTACK_H
+
+#include "pandatoolbase.h"
+#include "pvector.h"
+#include "macStatsLabel.h"
+
+#include
+
+class MacStatsMonitor;
+class MacStatsGraph;
+
+/**
+ * A widget that contains a stack of labels from bottom to top.
+ */
+class MacStatsLabelStack {
+public:
+ MacStatsLabelStack();
+ ~MacStatsLabelStack();
+
+ NSView *get_view() const;
+
+ int get_label_y(int label_index, NSView *target_view) const;
+ int get_label_height(int label_index) const;
+ int get_label_collector_index(int label_index) const;
+
+ void clear_labels();
+ int add_label(MacStatsMonitor *monitor, MacStatsGraph *graph,
+ int thread_index, int collector_index, bool use_fullname);
+ int get_num_labels() const;
+
+ void highlight_label(int collector_index);
+ void update_label_color(int collector_index);
+
+private:
+ NSStackView *_stack_view;
+ NSLayoutConstraint *_constraint;
+ int _highlight_label;
+
+ typedef pvector Labels;
+ Labels _labels;
+};
+
+#endif
diff --git a/pandatool/src/mac-stats/macStatsLabelStack.mm b/pandatool/src/mac-stats/macStatsLabelStack.mm
new file mode 100644
index 0000000000..6bfa9c3fb3
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsLabelStack.mm
@@ -0,0 +1,174 @@
+/**
+ * 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 macStatsLabelStack.mm
+ * @author rdb
+ * @date 2023-08-17
+ */
+
+#include "macStatsLabelStack.h"
+#include "macStatsLabel.h"
+#include "macStatsMonitor.h"
+
+static const NSEdgeInsets insets = {8, 8, 8, 8};
+
+/**
+ *
+ */
+MacStatsLabelStack::
+MacStatsLabelStack() {
+ _stack_view = [[NSStackView alloc] init];
+ _stack_view.edgeInsets = insets;
+ _stack_view.orientation = NSUserInterfaceLayoutOrientationVertical;
+ _stack_view.alignment = NSLayoutAttributeRight;//NSLayoutAttributeCenterX;
+ _stack_view.distribution = NSStackViewDistributionGravityAreas;
+ _stack_view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
+ _stack_view.translatesAutoresizingMaskIntoConstraints = NO;
+ _stack_view.spacing = 0;
+ _highlight_label = -1;
+
+ // May never be wider than the widest label, or 68, whichever is larger
+ //FIXME
+ _constraint = [_stack_view.widthAnchor constraintLessThanOrEqualToConstant:68];
+ _constraint.active = NO;
+ [_constraint retain];
+}
+
+/**
+ *
+ */
+MacStatsLabelStack::
+~MacStatsLabelStack() {
+ clear_labels();
+ [_constraint release];
+ [_stack_view release];
+}
+
+/**
+ * Returns the view for this stack.
+ */
+NSView *MacStatsLabelStack::
+get_view() const {
+ return _stack_view;
+}
+
+/**
+ * Returns the y position of the indicated label's bottom edge, relative to
+ * the indicated target widget.
+ */
+int MacStatsLabelStack::
+get_label_y(int label_index, NSView *target_view) const {
+ nassertr(label_index >= 0 && label_index < (int)_labels.size(), 0);
+
+ MacStatsLabel *label = _labels[label_index];
+ NSPoint pos = [target_view convertPoint:NSMakePoint(0, label.frame.size.height) fromView:label];
+ return pos.y;
+}
+
+/**
+ * Returns the height of the indicated label.
+ */
+int MacStatsLabelStack::
+get_label_height(int label_index) const {
+ nassertr(label_index >= 0 && label_index < (int)_labels.size(), 0);
+ return _labels[label_index].frame.size.height;
+}
+
+/**
+ * Returns the collector index associated with the indicated label.
+ */
+int MacStatsLabelStack::
+get_label_collector_index(int label_index) const {
+ nassertr(label_index >= 0 && label_index < (int)_labels.size(), -1);
+ return _labels[label_index].collectorIndex;
+}
+
+/**
+ * Removes the set of labels and starts a new set.
+ */
+void MacStatsLabelStack::
+clear_labels() {
+ for (MacStatsLabel *label : _labels) {
+ [_stack_view removeView:label];
+ [label release];
+ }
+ _labels.clear();
+
+ _constraint.constant = 68;
+
+ NSRect frame = _stack_view.frame;
+ frame.size.width = 0;
+ _stack_view.frame = frame;
+}
+
+/**
+ * Adds a new label to the top of the stack; returns the new label index.
+ */
+int MacStatsLabelStack::
+add_label(MacStatsMonitor *monitor, MacStatsGraph *graph,
+ int thread_index, int collector_index, bool use_fullname) {
+ const PStatClientData *client_data = monitor->get_client_data();
+ std::string text;
+ if (use_fullname) {
+ text = client_data->get_collector_fullname(collector_index);
+ } else {
+ text = client_data->get_collector_name(collector_index);
+ }
+
+ MacStatsLabel *label = [MacStatsLabel alloc];
+ [label initWithText:[NSString stringWithUTF8String:text.c_str()]
+ graph:graph
+ threadIndex:thread_index
+ collectorIndex:collector_index];
+
+ [_stack_view insertView:label atIndex:0 inGravity:NSStackViewGravityBottom];
+ [label.leadingAnchor constraintEqualToAnchor:_stack_view.leadingAnchor constant:8].active = YES;
+ [label.trailingAnchor constraintLessThanOrEqualToAnchor:_stack_view.trailingAnchor constant:-8].active = YES;
+
+ _constraint.constant = std::max(_constraint.constant, label.intrinsicContentSize.width);
+
+ int label_index = (int)_labels.size();
+ _labels.push_back(label);
+
+ return label_index;
+}
+
+/**
+ * Returns the number of labels in the stack.
+ */
+int MacStatsLabelStack::
+get_num_labels() const {
+ return _labels.size();
+}
+
+/**
+ * Draws a highlight around the label representing the indicated collector,
+ * and removes the highlight from any other label. Specify -1 to remove the
+ * highlight from all labels.
+ */
+void MacStatsLabelStack::
+highlight_label(int collector_index) {
+ if (_highlight_label != collector_index) {
+ _highlight_label = collector_index;
+ for (MacStatsLabel *label : _labels) {
+ label.highlight = (label.collectorIndex == _highlight_label);
+ }
+ }
+}
+
+/**
+ * Refreshes the color of the label with the given index.
+ */
+void MacStatsLabelStack::
+update_label_color(int collector_index) {
+ for (MacStatsLabel *label : _labels) {
+ if (label.collectorIndex == collector_index) {
+ [label updateColor];
+ }
+ }
+}
diff --git a/pandatool/src/mac-stats/macStatsMonitor.h b/pandatool/src/mac-stats/macStatsMonitor.h
new file mode 100644
index 0000000000..8e12677963
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsMonitor.h
@@ -0,0 +1,117 @@
+/**
+ * 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 macStatsMonitor.h
+ * @author rdb
+ * @date 2023-08-17
+ */
+
+#ifndef MACSTATSMONITOR_H
+#define MACSTATSMONITOR_H
+
+#include "pandatoolbase.h"
+
+#include "pStatMonitor.h"
+#include "pointerTo.h"
+#include "pset.h"
+#include "pvector.h"
+#include "pmap.h"
+
+#import
+
+class MacStatsGraph;
+class MacStatsServer;
+class MacStatsChartMenu;
+
+/**
+ * This class represents a connection to a PStatsClient and manages the data
+ * exchange with the client.
+ */
+class MacStatsMonitor : public PStatMonitor {
+public:
+ MacStatsMonitor(MacStatsServer *server);
+ virtual ~MacStatsMonitor();
+
+ void close();
+
+ virtual std::string get_monitor_name();
+
+ virtual void initialized();
+ virtual void got_hello();
+ virtual void got_bad_version(int client_major, int client_minor,
+ int server_major, int server_minor);
+ virtual void new_collector(int collector_index);
+ virtual void new_thread(int thread_index);
+ virtual void new_data(int thread_index, int frame_number);
+ virtual void remove_thread(int thread_index);
+ virtual void lost_connection();
+ virtual void idle();
+ virtual bool has_idle();
+
+ virtual void user_guide_bars_changed();
+
+ 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);
+
+ CGColorRef get_collector_color(int collector_index, bool highlight = false);
+ NSColor *get_collector_text_color(int collector_index, bool highlight = false);
+
+ void choose_collector_color(int collector_index);
+ void handle_choose_collector_color(const LRGBColor &color);
+ void reset_collector_color(int collector_index);
+
+ void set_show_status_item(bool show);
+ void set_time_units(int unit_mask);
+ void set_scroll_speed(double scroll_speed);
+ void set_pause(bool pause);
+
+ void add_graph(MacStatsGraph *graph);
+ void remove_graph(MacStatsGraph *graph);
+ void close_all_graphs();
+
+private:
+ void setup_speed_menu();
+ void update_status_bar();
+
+private:
+ typedef pset Graphs;
+ Graphs _graphs;
+
+ typedef pvector ChartMenus;
+ ChartMenus _chart_menus;
+
+ NSUserNotification *_notification = nullptr;
+ NSMenu *_main_menu;
+ NSMenuItem *_speed_menu_item = nullptr;
+ NSMenuItem *_speed_menu_item_1;
+ NSMenuItem *_speed_menu_item_2;
+ NSMenuItem *_speed_menu_item_3;
+ NSMenuItem *_speed_menu_item_6;
+ NSMenuItem *_speed_menu_item_12;
+ NSMenuItem *_speed_menu_item_pause;
+ int _next_chart_index;
+ NSStatusItem *_frame_rate_status_item = nullptr;
+ double _scroll_speed;
+ bool _pause;
+ bool _have_data = false;
+ int _choosing_color_collector_index;
+
+ struct ColorSet {
+ CGColorRef _bg[2];
+ NSColor *_fg[2];
+ };
+ typedef pmap Colors;
+ Colors _colors;
+
+ friend class MacStatsGraph;
+ friend class MacStatsServer;
+};
+
+#endif
diff --git a/pandatool/src/mac-stats/macStatsMonitor.mm b/pandatool/src/mac-stats/macStatsMonitor.mm
new file mode 100644
index 0000000000..b64ae21382
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsMonitor.mm
@@ -0,0 +1,650 @@
+/**
+ * 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 macStatsMonitor.mm
+ * @author rdb
+ * @date 2023-08-17
+ */
+
+#include "macStatsMonitor.h"
+#include "macStats.h"
+#include "macStatsServer.h"
+#include "macStatsStripChart.h"
+#include "macStatsChartMenu.h"
+#include "macStatsPianoRoll.h"
+#include "macStatsFlameGraph.h"
+#include "macStatsTimeline.h"
+#include "pStatGraph.h"
+#include "pStatCollectorDef.h"
+
+#include "convert_srgb.h"
+
+/**
+ *
+ */
+MacStatsMonitor::
+MacStatsMonitor(MacStatsServer *server) : PStatMonitor(server) {
+ _main_menu = NSApp.mainMenu;
+
+ // These will be filled in later when the menu is created.
+ _scroll_speed = 0.0;
+ _pause = false;
+ _next_chart_index = 2;
+
+ setup_speed_menu();
+
+ if ([[NSUserDefaults standardUserDefaults] boolForKey:@"ShowStatusItem"]) {
+ set_show_status_item(true);
+ }
+}
+
+/**
+ *
+ */
+MacStatsMonitor::
+~MacStatsMonitor() {
+ close();
+}
+
+/**
+ * Closes all the graphs associated with this monitor.
+ */
+void MacStatsMonitor::
+close() {
+ PStatMonitor::close();
+
+ if (_notification != nil) {
+ NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
+ [center removeDeliveredNotification:_notification];
+ [_notification release];
+ _notification = nil;
+ }
+
+ close_all_graphs();
+
+ if (_speed_menu_item != nullptr) {
+ [_main_menu removeItem:_speed_menu_item];
+ [_speed_menu_item release];
+ _speed_menu_item = nullptr;
+ }
+
+ for (MacStatsChartMenu *chart_menu : _chart_menus) {
+ chart_menu->remove_from_menu(_main_menu);
+ delete chart_menu;
+ }
+ _chart_menus.clear();
+
+ set_show_status_item(false);
+
+ for (auto &item : _colors) {
+ CGColorRelease(item.second._bg[0]);
+ CGColorRelease(item.second._bg[1]);
+ }
+ _colors.clear();
+
+ _next_chart_index = 2;
+}
+
+/**
+ * Should be redefined to return a descriptive name for the type of
+ * PStatsMonitor this is.
+ */
+std::string MacStatsMonitor::
+get_monitor_name() {
+ return "MacStats";
+}
+
+/**
+ * Called after the monitor has been fully set up. At this time, it will have
+ * a valid _client_data pointer, and things like is_alive() and close() will
+ * be meaningful. However, we may not yet know who we're connected to
+ * (is_client_known() may return false), and we may not know anything about
+ * the threads or collectors we're about to get data on.
+ */
+void MacStatsMonitor::
+initialized() {
+}
+
+/**
+ * Called when the "hello" message has been received from the client. At this
+ * time, the client's hostname and program name will be known.
+ */
+void MacStatsMonitor::
+got_hello() {
+ std::string progname = get_client_progname();
+ std::string hostname = get_client_hostname();
+
+ NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
+ _notification = [[NSUserNotification alloc] init];
+ _notification.title = @"PStats Server";
+ _notification.informativeText = [NSString
+ stringWithFormat:@"Connected to %s on %s", progname.c_str(), hostname.c_str()];
+
+ [center deliverNotification:_notification];
+}
+
+/**
+ * Like got_hello(), this is called when the "hello" message has been received
+ * from the client. At this time, the client's hostname and program name will
+ * be known. However, the client appears to be an incompatible version and
+ * the connection will be terminated; the monitor should issue a message to
+ * that effect.
+ */
+void MacStatsMonitor::
+got_bad_version(int client_major, int client_minor,
+ int server_major, int server_minor) {
+ std::ostringstream str;
+ str << "Unable to honor connection attempt from "
+ << get_client_progname() << " on " << get_client_hostname()
+ << ": unsupported PStats version "
+ << client_major << "." << client_minor;
+
+ if (server_minor == 0) {
+ str << " (server understands version " << server_major
+ << "." << server_minor << " only).";
+ } else {
+ str << " (server understands versions " << server_major
+ << ".0 through " << server_major << "." << server_minor << ").";
+ }
+
+ std::string message = str.str();
+
+ NSAlert *alert = [[NSAlert alloc] init];
+ alert.messageText = @"PStats Error";
+ alert.informativeText = [NSString stringWithUTF8String:message.c_str()];
+ alert.alertStyle = NSCriticalAlertStyle;
+ [alert runModal];
+ [alert release];
+}
+
+/**
+ * Called whenever a new Collector definition is received from the client.
+ * Generally, the client will send all of its collectors over shortly after
+ * connecting, but there's no guarantee that they will all be received before
+ * the first frames are received. The monitor should be prepared to accept
+ * new Collector definitions midstream.
+ */
+void MacStatsMonitor::
+new_collector(int collector_index) {
+ for (MacStatsGraph *graph : _graphs) {
+ graph->new_collector(collector_index);
+ }
+
+ // We might need to update our menus.
+ for (MacStatsChartMenu *chart_menu : _chart_menus) {
+ chart_menu->check_update();
+ }
+}
+
+/**
+ * Called whenever a new Thread definition is received from the client.
+ * Generally, the client will send all of its threads over shortly after
+ * connecting, but there's no guarantee that they will all be received before
+ * the first frames are received. The monitor should be prepared to accept
+ * new Thread definitions midstream.
+ */
+void MacStatsMonitor::
+new_thread(int thread_index) {
+ MacStatsChartMenu *chart_menu = new MacStatsChartMenu(this, thread_index);
+ chart_menu->add_to_menu(_main_menu, _next_chart_index);
+ ++_next_chart_index;
+ _chart_menus.push_back(chart_menu);
+}
+
+/**
+ * Called as each frame's data is made available. There is no guarantee the
+ * frames will arrive in order, or that all of them will arrive at all. The
+ * monitor should be prepared to accept frames received out-of-order or
+ * missing.
+ */
+void MacStatsMonitor::
+new_data(int thread_index, int frame_number) {
+ for (MacStatsGraph *graph : _graphs) {
+ graph->new_data(thread_index, frame_number);
+ }
+
+ if (!_have_data) {
+ open_default_graphs();
+ _have_data = true;
+ }
+}
+
+/**
+ * Called when a thread should be removed from the list of threads.
+ */
+void MacStatsMonitor::
+remove_thread(int thread_index) {
+ for (ChartMenus::iterator it = _chart_menus.begin(); it != _chart_menus.end(); ++it) {
+ MacStatsChartMenu *chart_menu = *it;
+ if (chart_menu->get_thread_index() == thread_index) {
+ chart_menu->remove_from_menu(_main_menu);
+ delete chart_menu;
+ _chart_menus.erase(it);
+ --_next_chart_index;
+ return;
+ }
+ }
+}
+
+/**
+ * Called whenever the connection to the client has been lost. This is a
+ * permanent state change. The monitor should update its display to represent
+ * this, and may choose to close down automatically.
+ */
+void MacStatsMonitor::
+lost_connection() {
+ NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
+
+ if (_notification != nil) {
+ [center removeDeliveredNotification:_notification];
+ [_notification release];
+ }
+
+ std::string hostname = get_client_hostname();
+
+ _notification = [[NSUserNotification alloc] init];
+ _notification.title = @"PStats Server";
+ _notification.informativeText = [NSString
+ stringWithFormat:@"Lost connection to %s", hostname.c_str()];
+
+ _notification.additionalActions = @[
+ [NSUserNotificationAction actionWithIdentifier:@"new" title:@"New Session"],
+ [NSUserNotificationAction actionWithIdentifier:@"quit" title:@"Quit PStats"]
+ ];
+
+ [center deliverNotification:_notification];
+}
+
+/**
+ * If has_idle() returns true, this will be called periodically to allow the
+ * monitor to update its display or whatever it needs to do.
+ */
+void MacStatsMonitor::
+idle() {
+ // Check if any of our chart menus need updating.
+ for (MacStatsChartMenu *chart_menu : _chart_menus) {
+ chart_menu->check_update();
+ }
+
+ // Update the frame rate label from the main thread (thread 0).
+ if (_frame_rate_status_item != nil) {
+ const PStatThreadData *thread_data = get_client_data()->get_thread_data(0);
+ double frame_rate = thread_data->get_frame_rate();
+ if (frame_rate != 0.0f) {
+ _frame_rate_status_item.button.title =
+ [NSString stringWithFormat:@"%0.1f ms / %0.1f Hz", 1000.0f / frame_rate, frame_rate];
+ }
+ }
+}
+
+/**
+ * Should be redefined to return true if you want to redefine idle() and
+ * expect it to be called.
+ */
+bool MacStatsMonitor::
+has_idle() {
+ return true;
+}
+
+/**
+ * Called when the user guide bars have been changed.
+ */
+void MacStatsMonitor::
+user_guide_bars_changed() {
+ for (MacStatsGraph *graph : _graphs) {
+ graph->user_guide_bars_changed();
+ }
+}
+
+/**
+ * Opens a new timeline.
+ */
+PStatGraph *MacStatsMonitor::
+open_timeline() {
+ MacStatsTimeline *graph = new MacStatsTimeline(this);
+ add_graph(graph);
+ return graph;
+}
+
+/**
+ * Opens a new flame graph showing the indicated data.
+ */
+PStatGraph *MacStatsMonitor::
+open_flame_graph(int thread_index, int collector_index) {
+ MacStatsFlameGraph *graph =
+ new MacStatsFlameGraph(this, thread_index, collector_index);
+ add_graph(graph);
+ return graph;
+}
+
+/**
+ * Opens a new strip chart showing the indicated data.
+ */
+PStatGraph *MacStatsMonitor::
+open_strip_chart(int thread_index, int collector_index, bool show_level) {
+ MacStatsStripChart *graph =
+ new MacStatsStripChart(this, thread_index, collector_index, show_level);
+ add_graph(graph);
+ return graph;
+}
+
+/**
+ * Opens a new piano roll showing the indicated data.
+ */
+PStatGraph *MacStatsMonitor::
+open_piano_roll(int thread_index) {
+ MacStatsPianoRoll *graph = new MacStatsPianoRoll(this, thread_index);
+ add_graph(graph);
+ return graph;
+}
+
+/**
+ * Returns a color suitable for drawing the indicated collector.
+ */
+CGColorRef MacStatsMonitor::
+get_collector_color(int collector_index, bool highlight) {
+ Colors::iterator bi;
+ bi = _colors.find(collector_index);
+ if (bi != _colors.end()) {
+ return (*bi).second._bg[highlight];
+ }
+
+ LRGBColor rgb = PStatMonitor::get_collector_color(collector_index);
+ rgb[0] = encode_sRGB_float(rgb[0]);
+ rgb[1] = encode_sRGB_float(rgb[1]);
+ rgb[2] = encode_sRGB_float(rgb[2]);
+
+ PN_stdfloat bright = rgb.dot(LRGBColor(0.2126, 0.7152, 0.0722));
+ CGColorRef color = CGColorCreateGenericRGB(rgb[0], rgb[1], rgb[2], 1.0);
+
+ rgb *= 0.75;
+ CGColorRef hcolor = CGColorCreateGenericRGB(rgb[0], rgb[1], rgb[2], 1.0);
+
+ ColorSet &set = _colors[collector_index];
+ set._bg[0] = color;
+ set._bg[1] = hcolor;
+
+ if (bright >= 0.5) {
+ set._fg[0] = [NSColor blackColor];
+ } else {
+ set._fg[0] = [NSColor whiteColor];
+ }
+
+ if (bright * 0.75 >= 0.5) {
+ set._fg[1] = [NSColor blackColor];
+ } else {
+ set._fg[1] = [NSColor whiteColor];
+ }
+
+ return highlight ? hcolor : color;
+}
+
+/**
+ * Returns a color suitable for drawing text on the indicated collector.
+ */
+NSColor *MacStatsMonitor::
+get_collector_text_color(int collector_index, bool highlight) {
+ get_collector_color(collector_index, false);
+ return _colors[collector_index]._fg[highlight];
+}
+
+/**
+ * Opens a dialog to change the given collector color.
+ */
+void MacStatsMonitor::
+choose_collector_color(int collector_index) {
+ _choosing_color_collector_index = collector_index;
+
+ const LRGBColor &rgb = PStatMonitor::get_collector_color(collector_index);
+ NSColor *color = [NSColor colorWithSRGBRed:rgb[0] green:rgb[1] blue:rgb[2] alpha:1.0];
+
+ NSColorPanel *panel = [NSColorPanel sharedColorPanel];
+ panel.target = [NSApp delegate];
+ panel.action = @selector(handleChooseCollectorColor:);
+ panel.showsAlpha = NO;
+ panel.color = color;
+ [NSApp orderFrontColorPanel:panel];
+}
+
+/**
+ * Sets a custom color associated with the given collector.
+ */
+void MacStatsMonitor::
+handle_choose_collector_color(const LRGBColor &color) {
+ int collector_index = _choosing_color_collector_index;
+
+ PStatMonitor::set_collector_color(collector_index, color);
+
+ if (_colors.erase(collector_index)) {
+ for (MacStatsGraph *graph : _graphs) {
+ graph->reset_collector_color(collector_index);
+ }
+ }
+}
+
+/**
+ * Resets the color of the given collector to the default.
+ */
+void MacStatsMonitor::
+reset_collector_color(int collector_index) {
+ PStatMonitor::clear_collector_color(collector_index);
+
+ if (_colors.erase(collector_index)) {
+ for (MacStatsGraph *graph : _graphs) {
+ graph->reset_collector_color(collector_index);
+ }
+ }
+}
+
+/**
+ * Enables the frame rate label on the right end of the menu bar. This is
+ * used as a text label to display the main thread's frame rate to the user,
+ * although it is implemented as a right-justified toplevel menu item that
+ * doesn't open to anything.
+ */
+void MacStatsMonitor::
+set_show_status_item(bool show) {
+ if (show && _frame_rate_status_item == nil) {
+ _frame_rate_status_item = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
+ _frame_rate_status_item.button.action = @selector(handleClickStatusItem:);
+ [_frame_rate_status_item retain];
+
+ const PStatClientData *client_data = get_client_data();
+ if (client_data != nullptr) {
+ const PStatThreadData *thread_data = client_data->get_thread_data(0);
+ if (thread_data != nullptr) {
+ double frame_rate = thread_data->get_frame_rate();
+ if (frame_rate != 0.0f) {
+ _frame_rate_status_item.button.title =
+ [NSString stringWithFormat:@"%0.1f ms / %0.1f Hz", 1000.0f / frame_rate, frame_rate];
+ }
+ }
+ }
+ }
+ if (!show && _frame_rate_status_item != nil) {
+ // Careful: for some reason, this can get called recursively.
+ NSStatusItem *status_item = _frame_rate_status_item;
+ _frame_rate_status_item = nil;
+ [[NSStatusBar systemStatusBar] removeStatusItem:status_item];
+ [status_item release];
+ status_item = nil;
+ }
+}
+
+/**
+ * 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 MacStatsMonitor::
+set_time_units(int unit_mask) {
+ for (MacStatsGraph *graph : _graphs) {
+ graph->set_time_units(unit_mask);
+ }
+}
+
+/**
+ * Called when the user selects a new scroll speed from the monitor pulldown
+ * menu, this should adjust the speeds for all graphs to the indicated value.
+ */
+void MacStatsMonitor::
+set_scroll_speed(double scroll_speed) {
+ _scroll_speed = scroll_speed;
+
+ // First, change all of the open graphs appropriately.
+ for (MacStatsGraph *graph : _graphs) {
+ graph->set_scroll_speed(_scroll_speed);
+ }
+
+ // Then, change the state of the menu items.
+ _speed_menu_item_1.state = (scroll_speed == 1.0);
+ _speed_menu_item_2.state = (scroll_speed == 2.0);
+ _speed_menu_item_3.state = (scroll_speed == 3.0);
+ _speed_menu_item_6.state = (scroll_speed == 6.0);
+ _speed_menu_item_12.state = (scroll_speed == 12.0);
+}
+
+/**
+ * Called when the user selects a pause on or pause off option from the menu.
+ */
+void MacStatsMonitor::
+set_pause(bool pause) {
+ _pause = pause;
+
+ // First, change all of the open graphs appropriately.
+ for (MacStatsGraph *graph : _graphs) {
+ graph->set_pause(_pause);
+ }
+
+ // Then, change the state of the menu item.
+ _speed_menu_item_pause.state = pause;
+}
+
+/**
+ * Adds the newly-created graph to the list of managed graphs.
+ */
+void MacStatsMonitor::
+add_graph(MacStatsGraph *graph) {
+ _graphs.insert(graph);
+
+ int units = [[NSUserDefaults standardUserDefaults] integerForKey:@"TimeUnits"];
+ graph->set_time_units(units);
+ graph->set_scroll_speed(_scroll_speed);
+ graph->set_pause(_pause);
+}
+
+/**
+ * Deletes the indicated graph.
+ */
+void MacStatsMonitor::
+remove_graph(MacStatsGraph *graph) {
+ Graphs::iterator gi = _graphs.find(graph);
+ if (gi != _graphs.end()) {
+ _graphs.erase(gi);
+ delete graph;
+ }
+}
+
+/**
+ * Asks all open graphs to close.
+ */
+void MacStatsMonitor::
+close_all_graphs() {
+ while (!_graphs.empty()) {
+ (*_graphs.begin())->close();
+ }
+}
+
+/**
+ * Creates the "Speed" pulldown menu.
+ */
+void MacStatsMonitor::
+setup_speed_menu() {
+ NSMenu *menu = [[NSMenu alloc] init];
+ menu.title = @"Speed";
+ menu.autoenablesItems = NO;
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(handleSpeed:);
+ item.title = @"1";
+ item.tag = 1;
+ [menu addItem:item];
+ [item release];
+
+ _speed_menu_item_1 = item;
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(handleSpeed:);
+ item.title = @"2";
+ item.tag = 2;
+ [menu addItem:item];
+ [item release];
+
+ _speed_menu_item_2 = item;
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(handleSpeed:);
+ item.title = @"3";
+ item.tag = 3;
+ item.state = NSOnState;
+ [menu addItem:item];
+ [item release];
+
+ _speed_menu_item_3 = item;
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(handleSpeed:);
+ item.title = @"6";
+ item.tag = 6;
+ [menu addItem:item];
+ [item release];
+
+ _speed_menu_item_6 = item;
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(handleSpeed:);
+ item.title = @"12";
+ item.tag = 12;
+ [menu addItem:item];
+ [item release];
+
+ _speed_menu_item_12 = item;
+ }
+
+ [menu addItem:[NSMenuItem separatorItem]];
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(handlePause:);
+ item.title = @"pause";
+ [menu addItem:item];
+ [item release];
+
+ _speed_menu_item_pause = item;
+ }
+
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ _speed_menu_item = item;
+ [_main_menu addItem:item];
+ [_main_menu setSubmenu:menu forItem:item];
+ [menu release];
+
+ set_scroll_speed(3);
+ set_pause(false);
+
+ ++_next_chart_index;
+}
diff --git a/pandatool/src/mac-stats/macStatsPianoRoll.h b/pandatool/src/mac-stats/macStatsPianoRoll.h
new file mode 100644
index 0000000000..df156619f0
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsPianoRoll.h
@@ -0,0 +1,84 @@
+/**
+ * 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 macStatsPianoRoll.h
+ * @author rdb
+ * @date 2023-08-19
+ */
+
+#ifndef MACSTATSPIANOROLL_H
+#define MACSTATSPIANOROLL_H
+
+#include "macStatsGraph.h"
+#include "pStatPianoRoll.h"
+#include "macStatsChartMenuDelegate.h"
+
+class MacStatsMonitor;
+
+/**
+ * A window that draws a piano-roll style chart, which shows the collectors
+ * explicitly stopping and starting, one frame at a time.
+ */
+class MacStatsPianoRoll final : public PStatPianoRoll, public MacStatsGraph {
+public:
+ MacStatsPianoRoll(MacStatsMonitor *monitor, int thread_index);
+ virtual ~MacStatsPianoRoll();
+
+ virtual void new_data(int thread_index, int frame_number);
+ virtual void force_redraw();
+ virtual void changed_graph_size(int graph_xsize, int graph_ysize);
+
+ virtual void set_time_units(int unit_mask);
+ virtual void on_click_label(int collector_index);
+ virtual NSMenu *get_label_menu(int collector_index) const;
+ virtual std::string get_label_tooltip(int collector_index) const;
+ void set_horizontal_scale(double time_width);
+
+protected:
+ void clear_region();
+ virtual void begin_draw();
+ virtual void begin_row(int row);
+ virtual void draw_bar(int row, int from_x, int to_x);
+ virtual void end_draw();
+ virtual void idle();
+
+ 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 NSMenu *get_graph_menu(int mouse_x, int mouse_y) const;
+ virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const;
+ virtual DragMode consider_drag_start(int graph_x, int graph_y);
+
+ virtual void handle_button_press(int graph_x, int graph_y,
+ bool double_click, int button);
+ virtual void handle_button_release(int graph_x, int graph_y);
+ virtual void handle_motion(int graph_x, int graph_y);
+ virtual void handle_leave();
+ virtual void handle_scroll();
+ virtual void handle_magnify(int graph_x, int graph_y, double scale);
+ virtual void handle_draw_graph(CGContextRef ctx, NSRect rect);
+ virtual void handle_draw_graph_overhang(CGContextRef ctx, NSRect rect);
+ virtual void handle_draw_scale_area(CGContextRef ctx, NSRect rect);
+
+private:
+ int get_collector_under_pixel(int xpoint, int ypoint) const;
+ void update_labels();
+ void draw_guide_bars(CGContextRef ctx, int y, int height);
+ void draw_guide_bar(CGContextRef ctx, const PStatGraph::GuideBar &bar,
+ int y, int height);
+ void draw_guide_labels(CGContextRef ctx);
+ void draw_guide_label(CGContextRef ctx, const PStatGraph::GuideBar &bar);
+
+private:
+ MacStatsChartMenuDelegate *_menu_delegate;
+ NSScrollView *_sidebar_scroll_view;
+};
+
+#endif
diff --git a/pandatool/src/mac-stats/macStatsPianoRoll.mm b/pandatool/src/mac-stats/macStatsPianoRoll.mm
new file mode 100644
index 0000000000..fe620b7f56
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsPianoRoll.mm
@@ -0,0 +1,764 @@
+/**
+ * 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 macStatsPianoRoll.mm
+ * @author rdb
+ * @date 2023-08-19
+ */
+
+#include "macStatsPianoRoll.h"
+#include "macStatsMonitor.h"
+#include "macStatsLabelStack.h"
+#include "macStatsScaleArea.h"
+
+static const int default_piano_roll_width = 800;
+static const int default_piano_roll_height = 400;
+
+static const int minimum_piano_roll_sidebar_width = 68;
+static const int default_piano_roll_sidebar_width = 200;
+
+/**
+ *
+ */
+MacStatsPianoRoll::
+MacStatsPianoRoll(MacStatsMonitor *monitor, int thread_index) :
+ PStatPianoRoll(monitor, thread_index, 0, 0),
+ MacStatsGraph(monitor, [MacStatsScrollableGraphViewController alloc])
+{
+ // Used for popup menus.
+ _menu_delegate = [[MacStatsChartMenuDelegate alloc] initWithMonitor:monitor threadIndex:thread_index];
+
+ // Let's show the units on the guide bar labels. There's room.
+ set_guide_bar_units(get_guide_bar_units() | GBU_show_units);
+
+ const PStatClientData *client_data =
+ MacStatsGraph::_monitor->get_client_data();
+ std::string thread_name = client_data->get_thread_name(_thread_index);
+ std::string window_title = thread_name + " thread piano roll";
+ _window.title = [NSString stringWithUTF8String:window_title.c_str()];
+
+ if (@available(macOS 11.0, *)) {
+ _window.titleVisibility = NSWindowTitleHidden;
+ }
+
+ // Set the initial size of the graph.
+ _graph_view.frame = NSMakeRect(0, 0, default_piano_roll_width, default_piano_roll_height);
+ _graph_view_controller.view.frame = NSMakeRect(0, 0, default_piano_roll_width, default_piano_roll_height);
+
+ // It's put inside a scroll view that tracks the main scroll view.
+ NSScrollView *scroll_view = [[NSScrollView alloc] initWithFrame:NSMakeRect(0, 0, default_piano_roll_sidebar_width, 0)];
+ scroll_view.documentView = _label_stack.get_view();
+ scroll_view.drawsBackground = NO;
+ scroll_view.automaticallyAdjustsContentInsets = YES;
+ //scroll_view.translatesAutoresizingMaskIntoConstraints = NO;
+ scroll_view.hasHorizontalScroller = NO;
+ scroll_view.hasVerticalScroller = NO;
+ _sidebar_scroll_view = scroll_view;
+
+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+ [center addObserver:_graph_view_controller
+ selector:@selector(handleSideScroll:)
+ name:NSScrollViewDidLiveScrollNotification
+ object:scroll_view];
+
+ NSViewController *sidebar_controller = [[NSViewController alloc] init];
+ sidebar_controller.view = scroll_view;
+
+ NSSplitViewController *svc = [[NSSplitViewController alloc] init];
+ [svc addSplitViewItem:[NSSplitViewItem sidebarWithViewController:sidebar_controller]];
+ [svc addSplitViewItem:[NSSplitViewItem splitViewItemWithViewController:_graph_view_controller]];
+
+ svc.splitViewItems[0].minimumThickness = minimum_piano_roll_sidebar_width;
+ svc.splitViewItems[0].canCollapse = NO;
+
+ NSSplitView *split_view = svc.splitView;
+ split_view.vertical = YES;
+ split_view.dividerStyle = NSSplitViewDividerStyleThin;
+ split_view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
+ _split_view = split_view;
+
+ _window.contentViewController = svc;
+
+ [svc release];
+ [sidebar_controller release];
+
+ // Scale area goes on top, as a titlebar accessory view.
+ MacStatsScaleAreaController *scale_area_controller = [[MacStatsScaleAreaController alloc] initWithGraph:this];
+ scale_area_controller.fullScreenMinHeight = 20;
+ scale_area_controller.layoutAttribute = NSLayoutAttributeRight;
+ _scale_area = scale_area_controller.view;
+ [_window addTitlebarAccessoryViewController:scale_area_controller];
+ [scale_area_controller release];
+
+ [_label_stack.get_view().widthAnchor constraintEqualToAnchor:scroll_view.widthAnchor].active = YES;
+ [_label_stack.get_view().heightAnchor constraintEqualToAnchor:_graph_view.heightAnchor].active = YES;
+
+ idle();
+
+ [_window makeKeyAndOrderFront:nil];
+}
+
+/**
+ *
+ */
+MacStatsPianoRoll::
+~MacStatsPianoRoll() {
+ [_sidebar_scroll_view release];
+ [_menu_delegate release];
+}
+
+/**
+ * Called as each frame's data is made available. There is no guarantee the
+ * frames will arrive in order, or that all of them will arrive at all. The
+ * monitor should be prepared to accept frames received out-of-order or
+ * missing.
+ */
+void MacStatsPianoRoll::
+new_data(int thread_index, int frame_number) {
+ if (!_pause) {
+ update();
+ }
+}
+
+/**
+ * Called when it is necessary to redraw the entire graph.
+ */
+void MacStatsPianoRoll::
+force_redraw() {
+ if (_ctx) {
+ PStatPianoRoll::force_redraw();
+ }
+}
+
+/**
+ * Called when the user has resized the window, forcing a resize of the graph.
+ */
+void MacStatsPianoRoll::
+changed_graph_size(int graph_xsize, int graph_ysize) {
+ PStatPianoRoll::changed_size(graph_xsize, graph_ysize);
+}
+
+/**
+ * Called when the user selects a new time units from the monitor pulldown
+ * menu, this should adjust the units for the graph to the indicated mask if
+ * it is a time-based graph.
+ */
+void MacStatsPianoRoll::
+set_time_units(int unit_mask) {
+ int old_unit_mask = get_guide_bar_units();
+ if ((old_unit_mask & (GBU_hz | GBU_ms)) != 0) {
+ unit_mask = unit_mask & (GBU_hz | GBU_ms);
+ unit_mask |= (old_unit_mask & GBU_show_units);
+ set_guide_bar_units(unit_mask);
+ }
+}
+
+/**
+ * Called when the user single-clicks on a label.
+ */
+void MacStatsPianoRoll::
+on_click_label(int collector_index) {
+ if (collector_index >= 0) {
+ MacStatsGraph::_monitor->open_strip_chart(_thread_index, collector_index, false);
+ }
+}
+
+/**
+ * Called when the mouse right-clicks on a label, and should return the menu
+ * that should pop up.
+ */
+NSMenu *MacStatsPianoRoll::
+get_label_menu(int collector_index) const {
+ NSMenu *menu = [[[NSMenu alloc] init] autorelease];
+
+ std::string label = get_label_tooltip(collector_index);
+ if (!label.empty()) {
+ if (@available(macOS 14.0, *)) {
+ [menu addItem:[NSMenuItem sectionHeaderWithTitle:[NSString stringWithUTF8String:label.c_str()]]];
+ } else {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:label.c_str()] action:nil keyEquivalent:@""];
+ item.enabled = NO;
+ [menu addItem:item];
+ [item release];
+ }
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Open Strip Chart" action:@selector(handleOpenStripChart:) keyEquivalent:@""];
+ item.target = _menu_delegate;
+ item.tag = collector_index;
+ [menu addItem:item];
+ [item release];
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Open Flame Graph" action:@selector(handleOpenFlameGraph:) keyEquivalent:@""];
+ item.target = _menu_delegate;
+ item.tag = collector_index;
+ [menu addItem:item];
+ [item release];
+ }
+
+ [menu addItem:[NSMenuItem separatorItem]];
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Change Color\u2026" action:@selector(handleChangeColor:) keyEquivalent:@""];
+ item.target = _graph_view_controller;
+ item.tag = collector_index;
+ [menu addItem:item];
+ [item release];
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Reset Color" action:@selector(handleResetColor:) keyEquivalent:@""];
+ item.target = _graph_view_controller;
+ item.tag = collector_index;
+ [menu addItem:item];
+ [item release];
+ }
+
+ return menu;
+}
+
+/**
+ * Called when the mouse hovers over a label, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string MacStatsPianoRoll::
+get_label_tooltip(int collector_index) const {
+ return PStatPianoRoll::get_label_tooltip(collector_index);
+}
+
+/**
+ * Changes the amount of time the width of the horizontal axis represents.
+ * This may force a redraw.
+ */
+void MacStatsPianoRoll::
+set_horizontal_scale(double time_width) {
+ PStatPianoRoll::set_horizontal_scale(time_width);
+
+ _graph_view.needsDisplay = YES;
+}
+
+/**
+ * Erases the chart area.
+ */
+void MacStatsPianoRoll::
+clear_region() {
+ if (_ctx) {
+ CGContextSetFillColorWithColor(_ctx, _background_color);
+ CGContextFillRect(_ctx, CGRectMake(0, 0, get_xsize(), get_ysize()));
+ }
+}
+
+/**
+ * Erases the chart area in preparation for drawing a bunch of bars.
+ */
+void MacStatsPianoRoll::
+begin_draw() {
+ clear_region();
+
+ // Draw in the guide bars.
+ CGContextSetStrokeColorWithColor(_ctx, [NSColor gridColor].CGColor);
+ int num_guide_bars = get_num_guide_bars();
+ for (int i = 0; i < num_guide_bars; ++i) {
+ draw_guide_bar(_ctx, get_guide_bar(i), 0, get_ysize());
+ }
+}
+
+/**
+ * Should be overridden by the user class. This hook will be called before
+ * drawing any one row of bars. These bars correspond to the collector whose
+ * index is get_row_collector(row), and in the color get_row_color(row).
+ */
+void MacStatsPianoRoll::
+begin_row(int row) {
+ int collector_index = get_label_collector(row);
+ bool is_highlighted = collector_index == _highlighted_index;
+ CGContextSetFillColorWithColor(_ctx,
+ MacStatsGraph::_monitor->get_collector_color(collector_index, is_highlighted));
+}
+
+/**
+ * Draws a single bar on the chart.
+ */
+void MacStatsPianoRoll::
+draw_bar(int row, int from_x, int to_x) {
+ if (row >= 0 && row < _label_stack.get_num_labels()) {
+ int y = _label_stack.get_label_y(row, _graph_view);
+ int height = _label_stack.get_label_height(row);
+
+ CGContextFillRect(_ctx, CGRectMake(from_x, (y - height + 2), to_x - from_x, (height - 4)));
+ }
+}
+
+/**
+ * Called after all the bars have been drawn, this triggers a refresh event to
+ * draw it to the window.
+ */
+void MacStatsPianoRoll::
+end_draw() {
+ _graph_view.needsDisplay = YES;
+
+ if (_guide_bars_changed) {
+ _scale_area.needsDisplay = YES;
+ _guide_bars_changed = false;
+ }
+}
+
+/**
+ * Called at the end of the draw cycle.
+ */
+void MacStatsPianoRoll::
+idle() {
+ if (_labels_changed) {
+ update_labels();
+ }
+}
+
+/**
+ * Returns the current window dimensions.
+ */
+bool MacStatsPianoRoll::
+get_window_state(int &x, int &y, int &width, int &height,
+ bool &maximized, bool &minimized) const {
+ MacStatsGraph::get_window_state(x, y, width, height, maximized, minimized);
+ return true;
+}
+
+/**
+ * Called to restore the graph window to its previous dimensions.
+ */
+void MacStatsPianoRoll::
+set_window_state(int x, int y, int width, int height,
+ bool maximized, bool minimized) {
+ MacStatsGraph::set_window_state(x, y, width, height, maximized, minimized);
+}
+
+/**
+ * Called when the mouse right-clicks on the graph, and should return the menu
+ * that should pop up.
+ */
+NSMenu *MacStatsPianoRoll::
+get_graph_menu(int mouse_x, int mouse_y) const {
+ int collector_index = get_collector_under_pixel(mouse_x, mouse_y);
+ if (collector_index >= 0) {
+ return get_label_menu(collector_index);
+ }
+ return nil;
+}
+
+/**
+ * Called when the mouse hovers over the graph, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string MacStatsPianoRoll::
+get_graph_tooltip(int mouse_x, int mouse_y) const {
+ int collector_index = get_collector_under_pixel(mouse_x, mouse_y);
+ if (collector_index >= 0) {
+ return get_label_tooltip(collector_index);
+ }
+ return std::string();
+}
+
+/**
+ * Based on the mouse position within the graph window, look for draggable
+ * things the mouse might be hovering over and return the appropriate DragMode
+ * enum or DM_none if nothing is indicated.
+ */
+MacStatsGraph::DragMode MacStatsPianoRoll::
+consider_drag_start(int graph_x, int graph_y) {
+ if (graph_y >= 0 && graph_y < get_ysize()) {
+ if (graph_x >= 0 && graph_x < get_xsize()) {
+ // See if the mouse is over a user-defined guide bar.
+ int x = graph_x;
+ double from_height = pixel_to_height(x - 2);
+ double to_height = pixel_to_height(x + 2);
+ _drag_guide_bar = find_user_guide_bar(from_height, to_height);
+ if (_drag_guide_bar >= 0) {
+ return DM_guide_bar;
+ }
+
+ } else {
+ // The mouse is left or right of the graph; maybe create a new guide
+ // bar.
+ return DM_new_guide_bar;
+ }
+ }
+
+ return MacStatsGraph::consider_drag_start(graph_x, graph_y);
+}
+
+/**
+ * Called when the mouse button is depressed within the graph window.
+ */
+void MacStatsPianoRoll::
+handle_button_press(int graph_x, int graph_y, bool double_click, int button) {
+ if (graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) {
+ if (double_click && button == 1) {
+ // Double-clicking on a color bar in the graph is the same as double-
+ // clicking on the corresponding label.
+ on_click_label(get_collector_under_pixel(graph_x, graph_y));
+ return;
+ }
+ }
+
+ if (_potential_drag_mode == DM_none) {
+ set_drag_mode(DM_scale);
+ _drag_scale_start = pixel_to_height(graph_x);
+ // SetCapture(_graph_window);
+ return;
+
+ } else if (_potential_drag_mode == DM_guide_bar && _drag_guide_bar >= 0) {
+ set_drag_mode(DM_guide_bar);
+ _drag_start_x = graph_x;
+ // SetCapture(_graph_window);
+ return;
+ }
+
+ return MacStatsGraph::handle_button_press(graph_x, graph_y,
+ double_click, button);
+}
+
+/**
+ * Called when the mouse button is released within the graph window.
+ */
+void MacStatsPianoRoll::
+handle_button_release(int graph_x, int graph_y) {
+ if (_drag_mode == DM_scale) {
+ set_drag_mode(DM_none);
+ // ReleaseCapture();
+ return handle_motion(graph_x, graph_y);
+ }
+ else if (_drag_mode == DM_guide_bar) {
+ if (graph_x < 0 || graph_x >= get_xsize()) {
+ remove_user_guide_bar(_drag_guide_bar);
+ } else {
+ move_user_guide_bar(_drag_guide_bar, pixel_to_height(graph_x));
+ }
+ set_drag_mode(DM_none);
+ // ReleaseCapture();
+ return handle_motion(graph_x, graph_y);
+ }
+
+ return MacStatsGraph::handle_button_release(graph_x, graph_y);
+}
+
+/**
+ * Called when the mouse is moved within the graph window.
+ */
+void MacStatsPianoRoll::
+handle_motion(int graph_x, int graph_y) {
+ if (_drag_mode == DM_none && _potential_drag_mode == DM_none) {
+ // When the mouse is over a color bar, highlight it.
+ int collector_index = get_collector_under_pixel(graph_x, graph_y);
+ _label_stack.highlight_label(collector_index);
+ on_enter_label(collector_index);
+
+ /*
+ // Now we want to get a WM_MOUSELEAVE when the mouse leaves the graph
+ // window.
+ TRACKMOUSEEVENT tme = {
+ sizeof(TRACKMOUSEEVENT),
+ TME_LEAVE,
+ _graph_window,
+ 0
+ };
+ TrackMouseEvent(&tme);
+ */
+ }
+ else {
+ // If the mouse is in some drag mode, stop highlighting.
+ _label_stack.highlight_label(-1);
+ on_leave_label(_highlighted_index);
+ }
+
+ if (_drag_mode == DM_scale) {
+ double ratio = (double)graph_x / (double)get_xsize();
+ if (ratio > 0.0f) {
+ set_horizontal_scale(_drag_scale_start / ratio);
+ }
+ return;
+ }
+ else if (_drag_mode == DM_new_guide_bar) {
+ // We haven't created the new guide bar yet; we won't until the mouse
+ // comes within the graph's region.
+ if (graph_x >= 0 && graph_x < get_xsize()) {
+ set_drag_mode(DM_guide_bar);
+ _drag_guide_bar = add_user_guide_bar(pixel_to_height(graph_x));
+ return;
+ }
+
+ } else if (_drag_mode == DM_guide_bar) {
+ move_user_guide_bar(_drag_guide_bar, pixel_to_height(graph_x));
+ return;
+ }
+
+ return MacStatsGraph::handle_motion(graph_x, graph_y);
+}
+
+/**
+ * Called when the mouse has left the graph window.
+ */
+void MacStatsPianoRoll::
+handle_leave() {
+ _label_stack.highlight_label(-1);
+ on_leave_label(_highlighted_index);
+}
+
+/**
+ * Called when the mouse is moved within the graph window.
+ */
+void MacStatsPianoRoll::
+handle_scroll() {
+ // Graph view is flipped, side bar isn't, so we need to convert coordinates
+ NSPoint point;
+ point.x = 0;
+ point.y = _graph_view.frame.size.height - (((NSScrollView *)_graph_view_controller.view).documentVisibleRect.size.height + ((NSScrollView *)_graph_view_controller.view).documentVisibleRect.origin.y);
+ [_sidebar_scroll_view.contentView scrollToPoint:point];
+ [_sidebar_scroll_view reflectScrolledClipView:_sidebar_scroll_view.contentView];
+}
+
+/**
+ *
+ */
+void MacStatsPianoRoll::
+handle_magnify(int graph_x, int graph_y, double scale) {
+ set_horizontal_scale(get_horizontal_scale() * (1.0 - scale));
+}
+
+/**
+ * Fills in the graph window.
+ */
+void MacStatsPianoRoll::
+handle_draw_graph(CGContextRef ctx, NSRect rect) {
+#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 140000
+ draw_guide_bars(ctx, rect.origin.y, rect.size.height);
+#endif
+
+ // Copy the drawn bars into the graph.
+ MacStatsGraph::handle_draw_graph(ctx, rect);
+
+ // Draw the scale area.
+/*
+ CGContextSetRGBStrokeColor(ctx, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2], 1.0);
+ CGContextSetStrokeColor(ctx, rgb_dark_gray);
+ CGContextBeginPath(ctx);
+ CGContextMoveToPoint(ctx, 0, header_height);
+ CGContextAddLineToPoint(ctx, get_xsize(), header_height);
+ CGContextStrokePath(ctx);*/
+
+ int num_user_guide_bars = get_num_user_guide_bars();
+ for (int i = 0; i < num_user_guide_bars; ++i) {
+ draw_guide_bar(ctx, get_user_guide_bar(i), rect.origin.y, rect.size.height);
+ }
+
+ NSRect scale_frame = _scale_area.frame;
+ NSRect graph_frame = _graph_view.frame;
+ if (scale_frame.size.width != graph_frame.size.width) {
+ scale_frame.size.width = graph_frame.size.width;
+ _scale_area.frame = scale_frame;
+ }
+}
+
+/**
+ * Fills in the graph window overhang, which is the area outside the graph
+ * bounds that may become visible momentarily due to scroll elasticity.
+ */
+void MacStatsPianoRoll::
+handle_draw_graph_overhang(CGContextRef ctx, NSRect rect) {
+ CGContextSetFillColorWithColor(ctx, _background_color);
+ CGContextFillRect(ctx, rect);
+
+ draw_guide_bars(ctx, rect.origin.y, rect.size.height);
+}
+
+/**
+ * Fills in the scale area.
+ */
+void MacStatsPianoRoll::
+handle_draw_scale_area(CGContextRef ctx, NSRect rect) {
+/*
+ CGContextSetRGBStrokeColor(ctx, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2], 1.0);
+ CGContextSetStrokeColor(ctx, rgb_dark_gray);
+ CGContextBeginPath(ctx);
+ CGContextMoveToPoint(ctx, 0, header_height);
+ CGContextAddLineToPoint(ctx, get_xsize(), header_height);
+ CGContextStrokePath(ctx);*/
+
+ draw_guide_bars(ctx, rect.origin.y, rect.size.height);
+ draw_guide_labels(ctx);
+}
+
+/**
+ * Returns the collector index associated with the indicated vertical row, or
+ * -1.
+ */
+int MacStatsPianoRoll::
+get_collector_under_pixel(int xpoint, int ypoint) const {
+ if (_label_stack.get_num_labels() == 0) {
+ return -1;
+ }
+
+ // Assume all of the labels are the same height.
+ int origin = _label_stack.get_label_y(0, _graph_view);
+ int height = _label_stack.get_label_height(0);
+ int row = (origin - ypoint) / height;
+ if (row >= 0 && row < _label_stack.get_num_labels()) {
+ return _label_stack.get_label_collector_index(row);
+ } else {
+ return -1;
+ }
+}
+
+/**
+ * Resets the list of labels.
+ */
+void MacStatsPianoRoll::
+update_labels() {
+ _label_stack.clear_labels();
+ for (int i = 0; i < get_num_labels(); ++i) {
+ _label_stack.add_label(MacStatsGraph::_monitor, this,
+ _thread_index,
+ get_label_collector(i), true);
+ }
+ _labels_changed = false;
+}
+
+/**
+ *
+ */
+void MacStatsPianoRoll::
+draw_guide_bars(CGContextRef ctx, int y, int height) {
+ CGContextSetStrokeColorWithColor(ctx, [NSColor gridColor].CGColor);
+
+ int num_guide_bars = get_num_guide_bars();
+ for (int i = 0; i < num_guide_bars; ++i) {
+ draw_guide_bar(ctx, get_guide_bar(i), y, height);
+ }
+
+ int num_user_guide_bars = get_num_user_guide_bars();
+ for (int i = 0; i < num_user_guide_bars; ++i) {
+ draw_guide_bar(ctx, get_user_guide_bar(i), y, height);
+ }
+}
+
+/**
+ * Draws the line for the indicated guide bar on the graph.
+ */
+void MacStatsPianoRoll::
+draw_guide_bar(CGContextRef ctx, const PStatGraph::GuideBar &bar,
+ int y, int height) {
+ int x = height_to_pixel(bar._height);
+
+ if (x > 0 && x < get_xsize() - 1) {
+ // Only draw it if it's not too close to the top.
+ /*switch (bar._style) {
+ case GBS_target:
+ CGContextSetRGBStrokeColor(ctx, rgb_light_gray[0], rgb_light_gray[1], rgb_light_gray[2], 1.0);
+ break;
+
+ case GBS_user:
+ CGContextSetRGBStrokeColor(ctx, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2], 1.0);
+ break;
+
+ default:
+ CGContextSetRGBStrokeColor(ctx, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2], 1.0);
+ break;
+ }*/
+ CGContextBeginPath(ctx);
+ CGContextMoveToPoint(ctx, x, y);
+ CGContextAddLineToPoint(ctx, x, y + height);
+ CGContextStrokePath(ctx);
+ }
+}
+
+/**
+ * This is called during the servicing of the draw event.
+ */
+void MacStatsPianoRoll::
+draw_guide_labels(CGContextRef ctx) {
+ int i;
+ int num_guide_bars = get_num_guide_bars();
+ for (i = 0; i < num_guide_bars; ++i) {
+ draw_guide_label(ctx, get_guide_bar(i));
+ }
+
+ int num_user_guide_bars = get_num_user_guide_bars();
+ for (i = 0; i < num_user_guide_bars; ++i) {
+ draw_guide_label(ctx, get_user_guide_bar(i));
+ }
+}
+
+/**
+ * Draws the text for the indicated guide bar label at the top of the graph.
+ */
+void MacStatsPianoRoll::
+draw_guide_label(CGContextRef ctx, const PStatGraph::GuideBar &bar) {
+ NSColor *color;
+ if (@available(macOS 11.0, *)) {
+ color = [NSColor tertiaryLabelColor];
+ } else {
+ // Otherwise it's hard to see on the dark titlebars
+ color = [NSColor windowFrameTextColor];
+ }
+ /*
+ switch (bar._style) {
+ case GBS_target:
+ color = [NSColor colorWithDeviceRed:rgb_light_gray[0] green:rgb_light_gray[1] blue:rgb_light_gray[2] alpha:1.0];
+ break;
+
+ case GBS_user:
+ color = [NSColor colorWithDeviceRed:rgb_user_guide_bar[0] green:rgb_user_guide_bar[1] blue:rgb_user_guide_bar[2] alpha:1.0];
+ break;
+
+ default:
+ color = [NSColor colorWithDeviceRed:rgb_dark_gray[0] green:rgb_dark_gray[1] blue:rgb_dark_gray[2] alpha:1.0];
+ break;
+ }*/
+
+ int x = height_to_pixel(bar._height);
+ const std::string &label = bar._label;
+
+ const CFStringRef keys[] = {
+ (__bridge CFStringRef)NSForegroundColorAttributeName,
+ (__bridge CFStringRef)NSFontAttributeName,
+ };
+ const void *values[] = {
+ color,
+ [NSFont systemFontOfSize:0.0],
+ };
+ CFDictionaryRef attribs = CFDictionaryCreate(kCFAllocatorDefault, (const void **)keys, (const void **)values, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+
+ CFStringRef str = CFStringCreateWithCString(kCFAllocatorDefault, label.c_str(), kCFStringEncodingUTF8);
+ CFAttributedStringRef astr = CFAttributedStringCreate(kCFAllocatorDefault, str, attribs);
+ CFRelease(attribs);
+ CFRelease(str);
+
+ CTLineRef line = CTLineCreateWithAttributedString(astr);
+ CFRelease(astr);
+ CGRect bounds = CTLineGetImageBounds(line, ctx);
+ int width = bounds.size.width;
+
+ if (bar._style != GBS_user) {
+ double from_height = pixel_to_height(x - width);
+ double to_height = pixel_to_height(x + width);
+ if (find_user_guide_bar(from_height, to_height) >= 0) {
+ // Omit the label: there's a user-defined guide bar in the same space.
+ CFRelease(line);
+ return;
+ }
+ }
+
+ if (x >= 0 && x < get_xsize()) {
+ CGContextSetTextPosition(ctx, x + 6, 6);
+ CTLineDraw(line, ctx);
+ }
+
+ CFRelease(line);
+}
diff --git a/pandatool/src/mac-stats/macStatsScaleArea.h b/pandatool/src/mac-stats/macStatsScaleArea.h
new file mode 100644
index 0000000000..cf7ae2f062
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsScaleArea.h
@@ -0,0 +1,41 @@
+/**
+ * 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 macStatsScaleArea.h
+ * @author rdb
+ * @date 2023-08-18
+ */
+
+#ifndef MACSTATSSCALEAREA_H
+#define MACSTATSSCALEAREA_H
+
+#import
+
+class MacStatsGraph;
+
+@interface MacStatsScaleArea : NSView {
+ @private
+ MacStatsGraph *_graph;
+}
+
+- (id)initWithGraph:(MacStatsGraph *)graph frame:(NSRect)rect;
+- (void)drawRect:(NSRect)dirtyRect;
+
+@end
+
+@interface MacStatsScaleAreaController : NSTitlebarAccessoryViewController {
+ @private
+ MacStatsGraph *_graph;
+}
+
+- (id)initWithGraph:(MacStatsGraph *)graph;
+- (void)loadView;
+
+@end
+
+#endif
diff --git a/pandatool/src/mac-stats/macStatsScaleArea.mm b/pandatool/src/mac-stats/macStatsScaleArea.mm
new file mode 100644
index 0000000000..4502946f02
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsScaleArea.mm
@@ -0,0 +1,50 @@
+/**
+ * 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 macStatsScaleArea.mm
+ * @author rdb
+ * @date 2023-08-18
+ */
+
+#include "macStatsScaleArea.h"
+#include "macStatsGraph.h"
+#include "macStatsStripChart.h"
+
+@implementation MacStatsScaleArea
+
+- (id)initWithGraph:(MacStatsGraph *)graph frame:(NSRect)rect {
+ if (self = [super initWithFrame:rect]) {
+ _graph = graph;
+
+ [self addTrackingArea:[[NSTrackingArea alloc] initWithRect:NSZeroRect options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInKeyWindow | NSTrackingInVisibleRect) owner:self userInfo:nil]];
+ }
+
+ return self;
+}
+
+- (void)drawRect:(NSRect)dirtyRect {
+ _graph->handle_draw_scale_area([NSGraphicsContext currentContext].CGContext, dirtyRect);
+}
+
+@end
+
+@implementation MacStatsScaleAreaController
+
+- (id)initWithGraph:(MacStatsGraph *)graph {
+ if (self = [super init]) {
+ _graph = graph;
+ }
+
+ return self;
+}
+
+- (void)loadView {
+ self.view = [[MacStatsScaleArea alloc] initWithGraph:_graph frame:NSMakeRect(0, 0, 100, 100)];
+}
+
+@end
diff --git a/pandatool/src/mac-stats/macStatsServer.h b/pandatool/src/mac-stats/macStatsServer.h
new file mode 100644
index 0000000000..8c5ba47e1e
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsServer.h
@@ -0,0 +1,82 @@
+/**
+ * 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 macStatsServer.h
+ * @author rdb
+ * @date 2023-08-17
+ */
+
+#ifndef MACSTATSSERVER_H
+#define MACSTATSSERVER_H
+
+#include "pandatoolbase.h"
+#include "programBase.h"
+#include "pStatServer.h"
+#include "macStatsMonitor.h"
+#include "macStatsAppDelegate.h"
+
+#include
+
+/**
+ * The class that owns the main loop, waiting for client connections.
+ */
+class MacStatsServer : public PStatServer, public ProgramBase {
+public:
+ MacStatsServer();
+
+ MacStatsMonitor *get_monitor() { return _monitor; }
+
+ void run(int argc, char *argv[]);
+
+protected:
+ virtual bool handle_args(Args &args) override;
+
+ virtual PStatMonitor *make_monitor(const NetAddress &address) override;
+ virtual void lost_connection(PStatMonitor *monitor) override;
+
+public:
+ bool new_session();
+ bool open_session(const Filename &fn);
+ bool open_session();
+ bool open_last_session();
+ bool save_session();
+ bool export_session();
+ bool close_session();
+
+ void set_show_status_item(bool show);
+ void set_appearance(NSString *name);
+ void set_time_units(int unit_mask);
+
+private:
+ void create_app();
+ void setup_session_menu();
+
+private:
+ PT(MacStatsMonitor) _monitor;
+
+ Filename _last_session;
+ Filename _save_filename;
+
+ int _port = -1;
+ NSApplication *_app = nil;
+ NSUserNotification *_listening_notification = nil;
+ NSMenu *_main_menu = nil;
+ NSMenuItem *_show_status_item_menu_item = nil;
+ NSMenuItem *_appearance_system_menu_item = nil;
+ NSMenuItem *_appearance_aqua_menu_item = nil;
+ NSMenuItem *_appearance_dark_aqua_menu_item = nil;
+ NSMenuItem *_units_ms_menu_item = nil;
+ NSMenuItem *_units_hz_menu_item = nil;
+ NSMenuItem *_new_session_menu_item = nil;
+ NSMenuItem *_open_last_session_menu_item = nil;
+ NSMenuItem *_save_session_menu_item = nil;
+ NSMenuItem *_close_session_menu_item = nil;
+ NSMenuItem *_export_session_menu_item = nil;
+};
+
+#endif
diff --git a/pandatool/src/mac-stats/macStatsServer.mm b/pandatool/src/mac-stats/macStatsServer.mm
new file mode 100644
index 0000000000..8b7611fe5d
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsServer.mm
@@ -0,0 +1,732 @@
+/**
+ * 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 macStatsServer.mm
+ * @author rdb
+ * @date 2023-08-17
+ */
+
+#include "macStatsServer.h"
+#include "macStatsMonitor.h"
+#include "macStatsAppDelegate.h"
+#include "pandaVersion.h"
+#include "pStatGraph.h"
+#include "config_pstatclient.h"
+
+#include
+
+/**
+ *
+ */
+MacStatsServer::
+MacStatsServer() : _port(pstats_port) {
+ set_program_brief("macOS PStats client");
+ set_program_description
+ ("This is a GUI-based PStats server that listens on a TCP port for a "
+ "connection from a PStatClient in a Panda3D application. It offers "
+ "various graphs for showing the timing information sent by the client."
+ "\n\n"
+ "The full documentation is available online:\n "
+#ifdef HAVE_PYTHON
+ "https://docs.panda3d.org/" PANDA_ABI_VERSION_STR "/python/optimization/pstats"
+#else
+ "https://docs.panda3d.org/" PANDA_ABI_VERSION_STR "/cpp/optimization/pstats"
+#endif
+ "");
+
+ add_option
+ ("p", "port", 0,
+ "Specify the TCP port to listen for connections on. By default, this "
+ "is taken from the pstats-port Config variable.",
+ &ProgramBase::dispatch_int, nullptr, &_port);
+
+ add_runline("[-p 5185]");
+ add_runline("session.pstats");
+
+ _last_session = Filename::expand_from(
+ "$HOME/Library/Caches/Panda3D-" PANDA_ABI_VERSION_STR "/last-session.pstats");
+ _last_session.set_binary();
+
+ create_app();
+}
+
+/**
+ * Runs the server.
+ */
+void MacStatsServer::
+run(int argc, char *argv[]) {
+ if (parse_command_line(argc, argv, isatty(STDERR_FILENO)) == ProgramBase::EC_failure) {
+ NSAlert *alert = [[NSAlert alloc] init];
+ alert.messageText = @"PStats Error";
+ alert.informativeText = @"Failed to parse command-line options.";
+ alert.alertStyle = NSCriticalAlertStyle;
+ [alert runModal];
+ [alert release];
+ return;
+ }
+
+ [_app run];
+}
+
+/**
+ * Does something with the additional arguments on the command line (after all
+ * the -options have been parsed). Returns true if the arguments are good,
+ * false otherwise.
+ */
+bool MacStatsServer::
+handle_args(ProgramBase::Args &args) {
+ if (args.empty()) {
+ // applicationDidFinishLaunching will call new_session().
+ return true;
+ }
+ else if (args.size() == 1) {
+ // Handled by application:openFile:
+ return true;
+ }
+ else {
+ nout << "At most one filename may be specified on the command-line.\n";
+ return false;
+ }
+}
+
+/**
+ *
+ */
+PStatMonitor *MacStatsServer::
+make_monitor(const NetAddress &address) {
+ // Enable the "New Session", "Save Session" and "Close Session" menu items.
+ _new_session_menu_item.enabled = YES;
+ _save_session_menu_item.enabled = YES;
+ _close_session_menu_item.enabled = YES;
+ _export_session_menu_item.enabled = YES;
+
+ NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
+ if (_listening_notification != nil) {
+ [center removeDeliveredNotification:_listening_notification];
+ [_listening_notification release];
+ _listening_notification = nil;
+ }
+
+ _monitor = new MacStatsMonitor(this);
+ return _monitor;
+}
+
+/**
+ * Called when connection has been lost.
+ */
+void MacStatsServer::
+lost_connection(PStatMonitor *monitor) {
+ if (_monitor != nullptr && !_monitor->_have_data) {
+ // We didn't have any data yet. Just silently restart the session.
+ _monitor->close();
+ _monitor = nullptr;
+ if (new_session()) {
+ return;
+ }
+ } else {
+ // 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();
+}
+
+/**
+ * Starts a new session.
+ */
+bool MacStatsServer::
+new_session() {
+ if (!close_session()) {
+ return false;
+ }
+
+ if (listen(_port)) {
+ NSUserNotification *notification = [[NSUserNotification alloc] init];
+ notification.title = @"PStats Server";
+ notification.informativeText = [NSString stringWithFormat:@"Waiting for client to connect on port %d\u2026", _port];
+
+ // Quick way to do something else if the user prefers.
+ notification.additionalActions = @[
+ [NSUserNotificationAction actionWithIdentifier:@"open" title:@"Open Session\u2026"],
+ [NSUserNotificationAction actionWithIdentifier:@"openLast" title:@"Open Last Session"],
+ [NSUserNotificationAction actionWithIdentifier:@"quit" title:@"Quit PStats"]
+ ];
+
+ NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
+ [center deliverNotification:notification];
+ _listening_notification = notification;
+
+ _new_session_menu_item.enabled = NO;
+ _save_session_menu_item.enabled = NO;
+ _close_session_menu_item.enabled = YES;
+ _export_session_menu_item.enabled = NO;
+
+ return true;
+ }
+
+ NSAlert *alert = [[NSAlert alloc] init];
+ alert.messageText = @"PStats Error";
+ alert.informativeText = [NSString stringWithFormat:@"Unable to open port %d. Try specifying a different port number using pstats-port in your Config file or the -p option on the command-line.", _port];
+ alert.alertStyle = NSCriticalAlertStyle;
+ [alert runModal];
+ [alert release];
+
+ return false;
+}
+
+/**
+ * Opens a session with the given filename.
+ */
+bool MacStatsServer::
+open_session(const Filename &fn) {
+ if (!close_session()) {
+ return false;
+ }
+
+ MacStatsMonitor *monitor = new MacStatsMonitor(this);
+ if (!monitor->read(fn)) {
+ delete monitor;
+ return false;
+ }
+
+ _save_filename = fn;
+ _new_session_menu_item.enabled = YES;
+ _save_session_menu_item.enabled = YES;
+ _close_session_menu_item.enabled = YES;
+ _export_session_menu_item.enabled = YES;
+
+ _monitor = monitor;
+
+ // If the file contained no graphs, open the default graphs.
+ if (monitor->_graphs.empty()) {
+ monitor->open_default_graphs();
+ }
+
+ return true;
+}
+
+/**
+ * Offers to open an existing session.
+ */
+bool MacStatsServer::
+open_session() {
+ if (!close_session()) {
+ return false;
+ }
+
+ NSOpenPanel *panel = [NSOpenPanel openPanel];
+ panel.title = @"Open Session";
+
+ if ([panel runModal] == NSModalResponseOK) {
+ NSString *path = [[panel.URLs firstObject] path];
+ Filename fn([path UTF8String]);
+ fn.set_binary();
+ if (open_session(fn)) {
+ return true;
+ }
+
+ NSAlert *alert = [[NSAlert alloc] init];
+ alert.messageText = @"PStats Error";
+ alert.informativeText = [NSString stringWithFormat:@"Failed to load session file: %s", fn.c_str()];
+ alert.alertStyle = NSCriticalAlertStyle;
+ [alert runModal];
+ [alert release];
+ }
+
+ return false;
+}
+
+/**
+ * Opens the last session, if any.
+ */
+bool MacStatsServer::
+open_last_session() {
+ if (open_session(_last_session)) {
+ return true;
+ }
+
+ NSAlert *alert = [[NSAlert alloc] init];
+ alert.messageText = @"PStats Error";
+ alert.informativeText = [NSString stringWithFormat:@"Failed to load session file: %s", _last_session.c_str()];
+ alert.alertStyle = NSCriticalAlertStyle;
+ [alert runModal];
+ [alert release];
+
+ return false;
+}
+
+/**
+ * Offers to save the current session.
+ */
+bool MacStatsServer::
+save_session() {
+ nassertr_always(_monitor != nullptr, true);
+
+ NSSavePanel *panel = [NSSavePanel savePanel];
+ panel.title = @"Save Session";
+
+ if (_save_filename.empty()) {
+ panel.nameFieldStringValue = @"session.pstats";
+ } else {
+ std::string dirname = _save_filename.get_dirname();
+ std::string basename = _save_filename.get_basename();
+ panel.nameFieldStringValue = [NSString stringWithUTF8String:basename.c_str()];
+ panel.directoryURL = [NSURL fileURLWithPath:[NSString stringWithUTF8String:dirname.c_str()]];
+ }
+
+ if ([panel runModal] == NSModalResponseOK) {
+ NSString *path = [panel.URL path];
+ Filename fn([path UTF8String]);
+ fn.set_binary();
+
+ if (!_monitor->write(fn)) {
+ NSAlert *alert = [[NSAlert alloc] init];
+ alert.messageText = @"PStats Error";
+ alert.informativeText = [NSString stringWithFormat:@"Failed to save session file: %s", fn.c_str()];
+ alert.alertStyle = NSCriticalAlertStyle;
+ [alert runModal];
+ [alert release];
+
+ return false;
+ }
+ _save_filename = fn;
+ _monitor->get_client_data()->clear_dirty();
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Offers to export the current session as a JSON file.
+ */
+bool MacStatsServer::
+export_session() {
+ nassertr_always(_monitor != nullptr, true);
+
+ NSSavePanel *panel = [NSSavePanel savePanel];
+ panel.title = @"Export Session";
+ panel.nameFieldStringValue = @"session.json";
+
+ if ([panel runModal] == NSModalResponseOK) {
+ NSString *path = [panel.URL path];
+ Filename fn([path UTF8String]);
+ fn.set_binary();
+
+ std::ofstream stream;
+ if (!fn.open_write(stream)) {
+ NSAlert *alert = [[NSAlert alloc] init];
+ alert.messageText = @"PStats Error";
+ alert.informativeText = [NSString stringWithFormat:@"Failed to open file for export: %s", fn.c_str()];
+ alert.alertStyle = NSCriticalAlertStyle;
+ [alert runModal];
+ [alert release];
+
+ 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 MacStatsServer::
+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";
+ }
+ }
+
+ // Make sure the alert goes on top.
+ [_app activateIgnoringOtherApps:YES];
+
+ NSAlert *alert = [[NSAlert alloc] init];
+ alert.messageText = @"Unsaved Data";
+ alert.informativeText = @"Would you like to save the currently open session?";
+ [alert addButtonWithTitle:@"Save\u2026"];
+ [alert addButtonWithTitle:@"Don't Save"];
+ [alert addButtonWithTitle:@"Cancel"];
+ int response = [alert runModal];
+ [alert release];
+
+ if ((response != 1000 && response != 1001) ||
+ (response == 1000 && !save_session())) {
+ return false;
+ }
+ }
+
+ _monitor->close();
+ _monitor = nullptr;
+ }
+
+ _save_filename = Filename();
+ stop_listening();
+
+ if (_listening_notification != nil) {
+ NSUserNotificationCenter *center = [NSUserNotificationCenter defaultUserNotificationCenter];
+ [center removeDeliveredNotification:_listening_notification];
+ [_listening_notification release];
+ _listening_notification = nil;
+ }
+
+ _new_session_menu_item.enabled = YES;
+ if (wrote_last_session) {
+ _open_last_session_menu_item.enabled = YES;
+ }
+ _save_session_menu_item.enabled = NO;
+ _close_session_menu_item.enabled = NO;
+ _export_session_menu_item.enabled = NO;
+ return true;
+}
+
+/**
+ * Enables the frame rate label on the right end of the menu bar. This is
+ * used as a text label to display the main thread's frame rate to the user,
+ * although it is implemented as a right-justified toplevel menu item that
+ * doesn't open to anything.
+ */
+void MacStatsServer::
+set_show_status_item(bool show) {
+ if (_monitor != nullptr) {
+ _monitor->set_show_status_item(show);
+ }
+
+ _show_status_item_menu_item.state = show ? NSOnState : NSOffState;
+}
+
+/**
+ *
+ */
+void MacStatsServer::
+set_appearance(NSString *name) {
+ if (@available(macOS 10.14, *)) {
+ if (name == nil || name.length == 0) {
+ [_app setAppearance:nil];
+ _appearance_system_menu_item.state = NSOnState;
+ } else {
+ [_app setAppearance:[NSAppearance appearanceNamed:name]];
+ _appearance_system_menu_item.state = NSOffState;
+ }
+
+ _appearance_aqua_menu_item.state = (name != nil && [name isEqual:@"NSAppearanceNameAqua"]) ? NSOnState : NSOffState;
+ _appearance_dark_aqua_menu_item.state = (name != nil && [name isEqual:@"NSAppearanceNameDarkAqua"]) ? NSOnState : NSOffState;
+ }
+}
+
+/**
+ * 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 MacStatsServer::
+set_time_units(int unit_mask) {
+ if (_monitor != nullptr) {
+ _monitor->set_time_units(unit_mask);
+ }
+
+ _units_ms_menu_item.state = (unit_mask & PStatGraph::GBU_ms) ? NSOnState : NSOffState;
+ _units_hz_menu_item.state = (unit_mask & PStatGraph::GBU_hz) ? NSOnState : NSOffState;
+}
+
+/**
+ * Creates the menu bar for this monitor.
+ */
+void MacStatsServer::
+create_app() {
+ _app = [NSApplication sharedApplication];
+
+ MacStatsAppDelegate *delegate = [[MacStatsAppDelegate alloc] initWithServer:this];
+ [_app setDelegate:delegate];
+ [_app setActivationPolicy:NSApplicationActivationPolicyRegular];
+
+ _main_menu = [[NSMenu alloc] init];
+
+ NSMenu *app_menu = [[NSMenu alloc] init];
+
+ NSMenu *settings_menu = [[NSMenu alloc] init];
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(handleToggleSettingsBool:);
+ item.representedObject = @"ShowStatusItem";
+ item.title = @"Show in Status Bar";
+ item.state = NSOnState;
+ [settings_menu addItem:item];
+
+ _show_status_item_menu_item = item;
+ [item release];
+ }
+
+ NSMenu *units_menu;
+ if (@available(macOS 14.0, *)) {
+ // On Sonnoma, use a section with header.
+ [settings_menu addItem:[NSMenuItem separatorItem]];
+ [settings_menu addItem:[NSMenuItem sectionHeaderWithTitle:@"Time Units"]];
+ units_menu = settings_menu;
+ } else {
+ // On older versions, create a sub-menu.
+ units_menu = [[NSMenu alloc] init];
+ units_menu.autoenablesItems = NO;
+
+ NSMenuItem *units_item = [[NSMenuItem alloc] init];
+ units_item.title = @"Time Units";
+
+ [settings_menu addItem:units_item];
+ [settings_menu setSubmenu:units_menu forItem:units_item];
+ [units_item release];
+ [units_menu release];
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(handleSettingsInteger:);
+ item.representedObject = @"TimeUnits";
+ item.tag = PStatGraph::GBU_ms;
+ item.title = @"ms";
+ item.state = NSOnState;
+ [units_menu addItem:item];
+
+ _units_ms_menu_item = item;
+ [item release];
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(handleSettingsInteger:);
+ item.representedObject = @"TimeUnits";
+ item.tag = PStatGraph::GBU_hz;
+ item.title = @"Hz";
+ item.state = NSOffState;
+ [units_menu addItem:item];
+
+ _units_hz_menu_item = item;
+ [item release];
+ }
+
+ // Dark mode available from Mojave.
+ if (@available(macOS 10.14, *)) {
+ NSMenu *appearance_menu;
+ if (@available(macOS 14.0, *)) {
+ // On Sonnoma, use a section with header.
+ [settings_menu addItem:[NSMenuItem separatorItem]];
+ [settings_menu addItem:[NSMenuItem sectionHeaderWithTitle:@"Appearance"]];
+ appearance_menu = settings_menu;
+ } else {
+ // On older versions, create a sub-menu.
+ appearance_menu = [[NSMenu alloc] init];
+ appearance_menu.autoenablesItems = NO;
+
+ NSMenuItem *appearance_item = [[NSMenuItem alloc] init];
+ appearance_item.title = @"Appearance";
+
+ [settings_menu addItem:appearance_item];
+ [settings_menu setSubmenu:appearance_menu forItem:appearance_item];
+ [appearance_item release];
+ [appearance_menu release];
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(handleSettingsAppearance:);
+ item.representedObject = @"";
+ item.title = @"System";
+ item.state = NSOnState;
+ [appearance_menu addItem:item];
+ _appearance_system_menu_item = item;
+ [item release];
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(handleSettingsAppearance:);
+ item.representedObject = @"NSAppearanceNameAqua";
+ item.title = @"Aqua";
+ item.state = NSOffState;
+ [appearance_menu addItem:item];
+ _appearance_aqua_menu_item = item;
+ [item release];
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(handleSettingsAppearance:);
+ item.representedObject = @"NSAppearanceNameDarkAqua";
+ item.title = @"Dark Aqua";
+ item.state = NSOffState;
+ [appearance_menu addItem:item];
+ _appearance_dark_aqua_menu_item = item;
+ [item release];
+ }
+ }
+
+ NSMenuItem *settings_item = [[NSMenuItem alloc] init];
+ settings_item.title = @"Settings";
+ [app_menu addItem:settings_item];
+ [app_menu setSubmenu:settings_menu forItem:settings_item];
+ [settings_item release];
+ [settings_menu release];
+
+ [app_menu addItem:[NSMenuItem separatorItem]];
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(hide:);
+ item.keyEquivalent = @"h";
+ item.title = @"Hide PStats";
+ [app_menu addItem:item];
+ [item release];
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(hideOtherApplications:);
+ item.keyEquivalent = @"h";
+ item.keyEquivalentModifierMask |= NSAlternateKeyMask;
+ item.title = @"Hide Others";
+ [app_menu addItem:item];
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(unhideAllApplications:);
+ item.title = @"Show All";
+ [app_menu addItem:item];
+ [item release];
+ }
+
+ [app_menu addItem:[NSMenuItem separatorItem]];
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(terminate:);
+ item.keyEquivalent = @"q";
+ item.title = @"Quit PStats";
+ [app_menu addItem:item];
+ [item release];
+ }
+
+ NSMenuItem *app_menu_item = [[NSMenuItem alloc] init];
+ [_main_menu addItem:app_menu_item];
+ [_main_menu setSubmenu:app_menu forItem:app_menu_item];
+ [app_menu_item release];
+ [app_menu release];
+
+ setup_session_menu();
+
+ [_app setMainMenu:_main_menu];
+ [_main_menu release];
+
+ //[_app activateIgnoringOtherApps:YES];
+ //[_app finishLaunching];
+
+ set_time_units(PStatGraph::GBU_ms);
+}
+
+/**
+ * Creates the "Session" pulldown menu.
+ */
+void MacStatsServer::
+setup_session_menu() {
+ NSMenu *submenu = [[NSMenu alloc] init];
+ submenu.title = @"Session";
+ submenu.autoenablesItems = NO;
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(handleNewSession:);
+ item.keyEquivalent = @"n";
+ item.title = @"New Session";
+ [submenu addItem:item];
+
+ _new_session_menu_item = item;
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(handleOpenSession:);
+ item.keyEquivalent = @"o";
+ item.title = @"Open Session\u2026";
+ [submenu addItem:item];
+ [item release];
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(handleOpenLastSession:);
+ item.title = @"Open Last Session";
+ item.enabled = _last_session.exists();
+ [submenu addItem:item];
+
+ _open_last_session_menu_item = item;
+ [item release];
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(handleSaveSession:);
+ item.keyEquivalent = @"s";
+ item.title = @"Save Session\u2026";
+ item.enabled = NO;
+ [submenu addItem:item];
+
+ _save_session_menu_item = item;
+ [item release];
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(handleCloseSession:);
+ item.keyEquivalent = @"w";
+ item.title = @"Close Session";
+ item.enabled = NO;
+ [submenu addItem:item];
+
+ _close_session_menu_item = item;
+ [item release];
+ }
+
+ [submenu addItem:[NSMenuItem separatorItem]];
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ item.action = @selector(handleExportSession:);
+ item.title = @"Export as JSON\u2026";
+ item.enabled = NO;
+ [submenu addItem:item];
+
+ _export_session_menu_item = item;
+ [item release];
+ }
+
+ NSMenuItem *item = [[NSMenuItem alloc] init];
+ [_main_menu addItem:item];
+ [_main_menu setSubmenu:submenu forItem:item];
+}
diff --git a/pandatool/src/mac-stats/macStatsStripChart.h b/pandatool/src/mac-stats/macStatsStripChart.h
new file mode 100644
index 0000000000..685a5f6618
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsStripChart.h
@@ -0,0 +1,90 @@
+/**
+ * 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 macStatsStripChart.h
+ * @author rdb
+ * @date 2023-08-18
+ */
+
+#ifndef MACSTATSSTRIPCHART_H
+#define MACSTATSSTRIPCHART_H
+
+#include "macStatsGraph.h"
+#include "pStatStripChart.h"
+#include "macStatsChartMenuDelegate.h"
+
+class MacStatsMonitor;
+
+/**
+ * A window that draws a strip chart, given a view.
+ */
+class MacStatsStripChart final : public PStatStripChart, public MacStatsGraph {
+public:
+ MacStatsStripChart(MacStatsMonitor *monitor,
+ int thread_index, int collector_index, bool show_level);
+ virtual ~MacStatsStripChart();
+
+ virtual void new_collector(int collector_index);
+ virtual void new_data(int thread_index, int frame_number);
+ virtual void force_redraw();
+ virtual void changed_graph_size(int graph_xsize, int graph_ysize);
+
+ virtual void set_time_units(int unit_mask);
+ virtual void set_scroll_speed(double scroll_speed);
+ virtual void on_click_label(int collector_index);
+ virtual NSMenu *get_label_menu(int collector_index) const;
+ virtual std::string get_label_tooltip(int collector_index) const;
+ void set_vertical_scale(double value_height);
+ void set_auto_vertical_scale();
+
+protected:
+ virtual void update_labels();
+
+ virtual void clear_region();
+ virtual void draw_slice(int x, int w,
+ const PStatStripChart::FrameData &fdata);
+ virtual void draw_empty(int x, int w);
+ 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 NSMenu *get_graph_menu(int mouse_x, int mouse_y) const;
+ virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const;
+ virtual DragMode consider_drag_start(int graph_x, int graph_y);
+ virtual void set_drag_mode(DragMode drag_mode);
+
+ virtual void handle_button_press(int graph_x, int graph_y,
+ bool double_click, int button);
+ virtual void handle_button_release(int graph_x, int graph_y);
+ virtual void handle_motion(int graph_x, int graph_y);
+ virtual void handle_leave();
+ virtual void handle_magnify(int graph_x, int graph_y, double scale);
+ virtual void handle_draw_graph(CGContextRef ctx, NSRect rect);
+ virtual void handle_draw_scale_area(CGContextRef ctx, NSRect rect);
+ virtual void handle_back();
+
+private:
+ void draw_guide_bar(CGContextRef ctx, int from_x, int to_x,
+ const PStatGraph::GuideBar &bar);
+ void draw_guide_labels(CGContextRef ctx);
+ int draw_guide_label(CGContextRef ctx, const PStatGraph::GuideBar &bar, int last_y);
+
+private:
+ MacStatsChartMenuDelegate *_menu_delegate;
+ //NSButton *_smooth_checkbox;
+ //NSTextField *_total_label;
+ NSToolbarItem *_total_item;
+
+ std::vector _back_stack;
+};
+
+#endif
diff --git a/pandatool/src/mac-stats/macStatsStripChart.mm b/pandatool/src/mac-stats/macStatsStripChart.mm
new file mode 100644
index 0000000000..068c41ea37
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsStripChart.mm
@@ -0,0 +1,964 @@
+/**
+ * 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 macStatsStripChart.mm
+ * @author rdb
+ * @date 2023-08-18
+ */
+
+#include "macStatsStripChart.h"
+#include "macStatsMonitor.h"
+#include "macStatsScaleArea.h"
+#include "pStatCollectorDef.h"
+#include "cocoa_compat.h"
+
+@interface MacStatsStripChartViewController : MacStatsGraphViewController
+@end
+
+static const int default_strip_chart_width = 400;
+static const int default_strip_chart_height = 200;
+
+static const int minimum_strip_chart_sidebar_width = 116;
+static const int default_strip_chart_sidebar_width = 116;
+
+/**
+ *
+ */
+MacStatsStripChart::
+MacStatsStripChart(MacStatsMonitor *monitor, int thread_index,
+ int collector_index, bool show_level) :
+ PStatStripChart(monitor, thread_index, collector_index, show_level, 0, 0),
+ MacStatsGraph(monitor, [MacStatsStripChartViewController alloc])
+{
+ // Used for popup menus.
+ _menu_delegate = [[MacStatsChartMenuDelegate alloc] initWithMonitor:monitor threadIndex:thread_index];
+
+ // Set the initial size of the graph.
+ int height = default_strip_chart_height + _window.frame.size.height - _window.contentLayoutRect.size.height;
+ _graph_view.frame = NSMakeRect(0, 0, default_strip_chart_width, height);
+ _graph_view_controller.view.frame = NSMakeRect(0, 0, default_strip_chart_width, height);
+
+ // It's put inside a scroll view that tracks the main scroll view.
+ NSScrollView *scroll_view = [[NSScrollView alloc] initWithFrame:NSMakeRect(0, 0, default_strip_chart_sidebar_width, 0)];
+ scroll_view.documentView = _label_stack.get_view();
+ scroll_view.drawsBackground = NO;
+ scroll_view.automaticallyAdjustsContentInsets = YES;
+ scroll_view.hasHorizontalScroller = NO;
+ scroll_view.hasVerticalScroller = NO;
+
+ NSViewController *sidebar_controller = [[NSViewController alloc] init];
+ sidebar_controller.view = scroll_view;
+
+ // Add a view to the right of the graph, to display all of the scale units.
+ // Calculate how wide it should be to display a typical label.
+ CGFloat width = [NSTextField labelWithString:@"999 ms"].frame.size.width + 8;
+ _scale_area = [[MacStatsScaleArea alloc] initWithGraph:this frame:NSMakeRect(0, 0, width, 0)];
+ _scale_area.autoresizingMask = NSViewHeightSizable;
+
+ NSViewController *scale_area_controller = [[NSViewController alloc] init];
+ scale_area_controller.view = _scale_area;
+
+ NSSplitViewController *svc = [[NSSplitViewController alloc] init];
+ [svc addSplitViewItem:[NSSplitViewItem sidebarWithViewController:sidebar_controller]];
+ [svc addSplitViewItem:[NSSplitViewItem splitViewItemWithViewController:_graph_view_controller]];
+ [svc addSplitViewItem:[NSSplitViewItem splitViewItemWithViewController:scale_area_controller]];
+
+ svc.splitViewItems[0].minimumThickness = minimum_strip_chart_sidebar_width;
+ svc.splitViewItems[2].minimumThickness = width;
+ svc.splitViewItems[2].maximumThickness = width;
+
+ NSSplitView *split_view = svc.splitView;
+ split_view.vertical = YES;
+ split_view.dividerStyle = NSSplitViewDividerStyleThin;
+ split_view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
+ _split_view = split_view;
+
+ // When sidebar collapses, show a sidebar icon in the menu bar
+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+ [center addObserver:_graph_view_controller
+ selector:@selector(handleSplitViewResize:)
+ name:NSSplitViewDidResizeSubviewsNotification
+ object:split_view];
+
+ _window.contentViewController = svc;
+
+ _graph_view_controller.backToolbarItemVisible = NO;
+
+ [svc release];
+ [sidebar_controller release];
+
+ _total_item = nil;
+ if (@available(macOS 11.0, *)) {
+ NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:@""];
+ toolbar.delegate = _graph_view_controller;
+ toolbar.displayMode = NSToolbarDisplayModeIconOnly;
+ _window.toolbar = toolbar;
+ [_window setToolbarStyle:NSWindowToolbarStyleUnifiedCompact];
+
+ for (NSToolbarItem *item in toolbar.items) {
+ if ([item.itemIdentifier isEqual:@"total"]) {
+ _total_item = item;
+ break;
+ }
+ }
+ [toolbar release];
+ }
+
+ /*{
+ _smooth_checkbox = [NSButton checkboxWithTitle:@"Smooth"
+ target:_graph_view_controller
+ action:@selector(handleToggleAverageMode:)];
+
+ NSStackView *stack_view = [NSStackView stackViewWithViews:@[_smooth_checkbox]];
+ stack_view.translatesAutoresizingMaskIntoConstraints = NO;
+
+ NSTitlebarAccessoryViewController *accessory_controller = [[NSTitlebarAccessoryViewController alloc] init];
+ //accessory_controller.layoutAttribute = NSLayoutAttributeLeft;
+ [accessory_controller.view addSubview:stack_view];
+ //accessory_controller.automaticallyAdjustsSize = NO;
+ [_window addTitlebarAccessoryViewController:accessory_controller];
+ [accessory_controller release];
+
+ [stack_view.leftAnchor constraintEqualToAnchor:_graph_view.leftAnchor constant:8].active = YES;
+ [stack_view.bottomAnchor constraintEqualToAnchor:((NSLayoutGuide *)_window.contentLayoutGuide).topAnchor constant:-8].active = YES;
+ //[stack_view.topAnchor constraintEqualToAnchor:svc.view.topAnchor constant:-8].active = YES;
+ }*/
+ /*{
+ _total_label = [NSTextField labelWithString:@""];
+
+ NSStackView *stack_view = [NSStackView stackViewWithViews:@[_total_label]];
+ stack_view.translatesAutoresizingMaskIntoConstraints = NO;
+
+ NSTitlebarAccessoryViewController *accessory_controller = [[NSTitlebarAccessoryViewController alloc] init];
+ accessory_controller.layoutAttribute = NSLayoutAttributeRight;
+ [accessory_controller.view addSubview:stack_view];
+ //accessory_controller.automaticallyAdjustsSize = NO;
+ [_window addTitlebarAccessoryViewController:accessory_controller];
+ [accessory_controller release];
+
+ [stack_view.rightAnchor constraintEqualToAnchor:_graph_view.rightAnchor].active = YES;
+ [stack_view.bottomAnchor constraintEqualToAnchor:((NSLayoutGuide *)_window.contentLayoutGuide).topAnchor].active = YES;
+ [stack_view.topAnchor constraintEqualToAnchor:svc.view.topAnchor].active = YES;
+ }*/
+
+ if (show_level) {
+ // If it's a level-type graph, show the appropriate units.
+ if (_unit_name.empty()) {
+ set_guide_bar_units(GBU_named);
+ } else {
+ set_guide_bar_units(GBU_named | GBU_show_units);
+ }
+ } else {
+ // If it's a time-type graph, show the ms / Hz units.
+ set_guide_bar_units(get_guide_bar_units() | GBU_show_units);
+ }
+
+ [_label_stack.get_view().widthAnchor constraintEqualToAnchor:scroll_view.widthAnchor].active = YES;
+
+ // Update window title and total label.
+ new_data(0, 0);
+
+ [_window makeKeyAndOrderFront:nil];
+}
+
+/**
+ *
+ */
+MacStatsStripChart::
+~MacStatsStripChart() {
+ [_menu_delegate release];
+}
+
+/**
+ * Called whenever a new Collector definition is received from the client.
+ */
+void MacStatsStripChart::
+new_collector(int collector_index) {
+ MacStatsGraph::new_collector(collector_index);
+}
+
+/**
+ * Called as each frame's data is made available. There is no guarantee the
+ * frames will arrive in order, or that all of them will arrive at all. The
+ * monitor should be prepared to accept frames received out-of-order or
+ * missing.
+ */
+void MacStatsStripChart::
+new_data(int thread_index, int frame_number) {
+ if (is_title_unknown()) {
+ std::string window_title = get_title_text();
+ if (!is_title_unknown()) {
+ _window.title = [NSString stringWithUTF8String:window_title.c_str()];
+ }
+ }
+
+ if (!_pause) {
+ update();
+
+ if (@available(macOS 10.15, *)) {
+ std::string text = get_total_text();
+ [_total_item setTitle:[NSString stringWithUTF8String:text.c_str()]];
+ }
+ }
+}
+
+/**
+ * Called when it is necessary to redraw the entire graph.
+ */
+void MacStatsStripChart::
+force_redraw() {
+ if (_ctx) {
+ PStatStripChart::force_redraw();
+ }
+
+ _scale_area.needsDisplay = YES;
+}
+
+/**
+ * Called when the user has resized the window, forcing a resize of the graph.
+ */
+void MacStatsStripChart::
+changed_graph_size(int graph_xsize, int graph_ysize) {
+ PStatStripChart::changed_size(graph_xsize, graph_ysize);
+}
+
+/**
+ * Called when the user selects a new time units from the monitor pulldown
+ * menu, this should adjust the units for the graph to the indicated mask if
+ * it is a time-based graph.
+ */
+void MacStatsStripChart::
+set_time_units(int unit_mask) {
+ int old_unit_mask = get_guide_bar_units();
+ if ((old_unit_mask & (GBU_hz | GBU_ms)) != 0) {
+ unit_mask = unit_mask & (GBU_hz | GBU_ms);
+ unit_mask |= (old_unit_mask & GBU_show_units);
+ set_guide_bar_units(unit_mask);
+
+ if (@available(macOS 10.15, *)) {
+ std::string text = get_total_text();
+ [_total_item setTitle:[NSString stringWithUTF8String:text.c_str()]];
+ }
+
+ _scale_area.needsDisplay = YES;
+ }
+}
+
+/**
+ * Called when the user selects a new scroll speed from the monitor pulldown
+ * menu, this should adjust the speed for the graph to the indicated value.
+ */
+void MacStatsStripChart::
+set_scroll_speed(double scroll_speed) {
+ // The speed factor indicates chart widths per minute.
+ if (scroll_speed != 0.0f) {
+ set_horizontal_scale(60.0f / scroll_speed);
+ }
+}
+
+/**
+ * Called when the user single-clicks on a label.
+ */
+void MacStatsStripChart::
+on_click_label(int collector_index) {
+ if (collector_index < 0) {
+ // Clicking on whitespace in the graph is the same as clicking on the top
+ // label.
+ collector_index = get_collector_index();
+ }
+
+ if (collector_index == get_collector_index()) {
+ // Clicking on the top label means to go up to the parent level.
+ if (collector_index != 0) {
+ const PStatClientData *client_data =
+ MacStatsGraph::_monitor->get_client_data();
+ if (client_data->has_collector(collector_index)) {
+ const PStatCollectorDef &def =
+ client_data->get_collector_def(collector_index);
+ if (def._parent_index == 0 && get_view().get_show_level()) {
+ // Unless the parent is "Frame", and we're not a time collector.
+ }
+ else if (def._parent_index != get_collector_index()) {
+ // If we were previously at the parent, pop it from the stack.
+ if (!_back_stack.empty() && _back_stack.back() == def._parent_index) {
+ _back_stack.pop_back();
+ if (_back_stack.empty()) {
+ _graph_view_controller.backToolbarItemVisible = NO;
+ }
+ } else {
+ if (_back_stack.empty()) {
+ _graph_view_controller.backToolbarItemVisible = YES;
+ }
+ _back_stack.push_back(get_collector_index());
+ }
+
+ set_collector_index(def._parent_index);
+ }
+ }
+ }
+ }
+ else {
+ if (_back_stack.empty()) {
+ _graph_view_controller.backToolbarItemVisible = YES;
+ }
+ _back_stack.push_back(get_collector_index());
+
+ // Clicking on any other label means to focus on that.
+ set_collector_index(collector_index);
+ }
+
+ // Update window title and total label.
+ new_data(0, 0);
+
+ _scale_area.needsDisplay = YES;
+}
+
+/**
+ * Called when the mouse right-clicks on a label, and should return the menu
+ * that should pop up.
+ */
+NSMenu *MacStatsStripChart::
+get_label_menu(int collector_index) const {
+ NSMenu *menu = [[[NSMenu alloc] init] autorelease];
+
+ std::string label = get_label_tooltip(collector_index);
+ if (!label.empty()) {
+ if (@available(macOS 14.0, *)) {
+ [menu addItem:[NSMenuItem sectionHeaderWithTitle:[NSString stringWithUTF8String:label.c_str()]]];
+ } else {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:label.c_str()] action:nil keyEquivalent:@""];
+ item.enabled = NO;
+ [menu addItem:item];
+ [item release];
+ }
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Set as Focus" action:@selector(handleSetAsFocus:) keyEquivalent:@""];
+ item.target = _graph_view_controller;
+ item.tag = collector_index;
+ item.enabled = (collector_index != 0 || get_collector_index() != 0);
+ [menu addItem:item];
+ [item release];
+ }
+
+ {
+ SEL action = get_view().get_show_level() ? @selector(handleOpenStripChartLevel:) : @selector(handleOpenStripChart:);
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Open Strip Chart" action:action keyEquivalent:@""];
+ item.target = _menu_delegate;
+ item.tag = collector_index;
+ [menu addItem:item];
+ [item release];
+ }
+
+ if (!get_view().get_show_level()) {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Open Flame Graph" action:@selector(handleOpenFlameGraph:) keyEquivalent:@""];
+ item.target = _menu_delegate;
+ item.tag = collector_index;
+ [menu addItem:item];
+ [item release];
+ }
+
+ [menu addItem:[NSMenuItem separatorItem]];
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Change Color\u2026" action:@selector(handleChangeColor:) keyEquivalent:@""];
+ item.target = _graph_view_controller;
+ item.tag = collector_index;
+ [menu addItem:item];
+ [item release];
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Reset Color" action:@selector(handleResetColor:) keyEquivalent:@""];
+ item.target = _graph_view_controller;
+ item.tag = collector_index;
+ [menu addItem:item];
+ [item release];
+ }
+
+ return menu;
+}
+
+/**
+ * Called when the mouse hovers over a label, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string MacStatsStripChart::
+get_label_tooltip(int collector_index) const {
+ return PStatStripChart::get_label_tooltip(collector_index);
+}
+
+/**
+ * Changes the value the height of the vertical axis represents. This may
+ * force a redraw.
+ */
+void MacStatsStripChart::
+set_vertical_scale(double value_height) {
+ PStatStripChart::set_vertical_scale(value_height);
+
+ _graph_view.needsDisplay = YES;
+ _scale_area.needsDisplay = YES;
+}
+
+/**
+ * Sets the vertical scale to make all the data visible.
+ */
+void MacStatsStripChart::
+set_auto_vertical_scale() {
+ PStatStripChart::set_auto_vertical_scale();
+ set_vertical_scale(get_vertical_scale() * 1.5);
+
+ _graph_view.needsDisplay = YES;
+ _scale_area.needsDisplay = YES;
+}
+
+/**
+ * Resets the list of labels.
+ */
+void MacStatsStripChart::
+update_labels() {
+ PStatStripChart::update_labels();
+
+ _label_stack.clear_labels();
+ for (int i = 0; i < get_num_labels(); i++) {
+ _label_stack.add_label(MacStatsGraph::_monitor, this, _thread_index,
+ get_label_collector(i), false);
+ }
+ _labels_changed = false;
+}
+
+/**
+ * Erases the chart area.
+ */
+void MacStatsStripChart::
+clear_region() {
+ if (_ctx) {
+ //CGContextSetFillColorWithColor(_ctx, _background_color);
+ //CGContextFillRect(_ctx, CGRectMake(0, 0, get_xsize(), get_ysize()));
+ }
+}
+
+/**
+ * Draws a single vertical slice of the strip chart, at the given pixel
+ * position, and corresponding to the indicated level data.
+ */
+void MacStatsStripChart::
+draw_slice(int x, int w, const PStatStripChart::FrameData &fdata) {
+ if (!_ctx) {
+ return;
+ }
+
+ // Start by clearing the band first.
+ CGContextSetFillColorWithColor(_ctx, _background_color);
+ CGContextFillRect(_ctx, CGRectMake(x, 0, w, get_ysize()));
+
+ double overall_time = 0.0;
+ int y = get_ysize();
+
+ FrameData::const_iterator fi;
+ for (fi = fdata.begin(); fi != fdata.end(); ++fi) {
+ const ColorData &cd = (*fi);
+ overall_time += cd._net_value;
+
+ bool is_highlighted = cd._collector_index == _highlighted_index;
+ CGContextSetFillColorWithColor(_ctx,
+ MacStatsGraph::_monitor->get_collector_color(cd._collector_index, is_highlighted));
+
+ if (overall_time > get_vertical_scale()) {
+ // Off the top. Go ahead and clamp it by hand, in case it's so far off
+ // the top we'd overflow the 16-bit pixel value.
+ CGContextFillRect(_ctx, CGRectMake(x, 0, w, y));
+ // And we can consider ourselves done now.
+ return;
+ }
+
+ int top_y = height_to_pixel(overall_time);
+ CGContextFillRect(_ctx, CGRectMake(x, top_y, w, y - top_y));
+ y = top_y;
+ }
+}
+
+/**
+ * Draws a single vertical slice of background color.
+ */
+void MacStatsStripChart::
+draw_empty(int x, int w) {
+ if (!_ctx) {
+ return;
+ }
+
+ CGContextSetFillColorWithColor(_ctx, _background_color);
+ CGContextFillRect(_ctx, CGRectMake(x, 0, w, get_ysize()));
+}
+
+/**
+ * Draws a single vertical slice of foreground color.
+ */
+void MacStatsStripChart::
+draw_cursor(int x) {
+ if (!_ctx) {
+ return;
+ }
+
+ CGContextBeginPath(_ctx);
+ CGContextMoveToPoint(_ctx, x, 0);
+ CGContextAddLineToPoint(_ctx, x, get_ysize());
+ CGContextStrokePath(_ctx);
+}
+
+/**
+ * Should be overridden by the user class. This hook will be called after
+ * drawing a series of color bars in the strip chart; it gives the pixel range
+ * that was just redrawn.
+ */
+void MacStatsStripChart::
+end_draw(int from_x, int to_x) {
+ _graph_view.needsDisplay = YES;
+}
+
+/**
+ * Returns the current window dimensions.
+ */
+bool MacStatsStripChart::
+get_window_state(int &x, int &y, int &width, int &height,
+ bool &maximized, bool &minimized) const {
+ MacStatsGraph::get_window_state(x, y, width, height, maximized, minimized);
+ return true;
+}
+
+/**
+ * Called to restore the graph window to its previous dimensions.
+ */
+void MacStatsStripChart::
+set_window_state(int x, int y, int width, int height,
+ bool maximized, bool minimized) {
+ MacStatsGraph::set_window_state(x, y, width, height, maximized, minimized);
+}
+
+/**
+ * Called when the mouse right-clicks on the graph, and should return the menu
+ * that should pop up.
+ */
+NSMenu *MacStatsStripChart::
+get_graph_menu(int mouse_x, int mouse_y) const {
+ NSMenu *menu = nullptr;
+ if (_highlighted_index != -1) {
+ menu = get_label_menu(_highlighted_index);
+ }
+/*
+ if (menu != nullptr) {
+ [menu addItem:[NSMenuItem separatorItem]];
+ } else {
+ menu = [[[NSMenu alloc] init] autorelease];
+ }
+
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Smooth" action:@selector(handleToggleStripChartAverage:) keyEquivalent:@""];
+ item.target = _graph_view_controller;
+ item.state = get_average_mode() ? NSOnState : NSOffState;
+ [menu addItem:item];
+ [item release];*/
+ return menu;
+}
+
+/**
+ * Called when the mouse hovers over the graph, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string MacStatsStripChart::
+get_graph_tooltip(int mouse_x, int mouse_y) const {
+ if (_highlighted_index != -1) {
+ return get_label_tooltip(_highlighted_index);
+ }
+ return std::string();
+}
+
+/**
+ * Based on the mouse position within the graph window, look for draggable
+ * things the mouse might be hovering over and return the appropriate DragMode
+ * enum or DM_none if nothing is indicated.
+ */
+MacStatsGraph::DragMode MacStatsStripChart::
+consider_drag_start(int graph_x, int graph_y) {
+ // See if the mouse is over a user-defined guide bar.
+ int y = graph_y;
+ double from_height = pixel_to_height(y + 2);
+ double to_height = pixel_to_height(y - 2);
+ _drag_guide_bar = find_user_guide_bar(from_height, to_height);
+ if (_drag_guide_bar >= 0) {
+ return DM_guide_bar;
+ }
+
+ return MacStatsGraph::consider_drag_start(graph_x, graph_y);
+}
+
+/**
+ * This should be called whenever the drag mode needs to change state. It
+ * provides hooks for a derived class to do something special.
+ */
+void MacStatsStripChart::
+set_drag_mode(MacStatsGraph::DragMode drag_mode) {
+ MacStatsGraph::set_drag_mode(drag_mode);
+}
+
+/**
+ * Called when the mouse button is depressed within the graph window.
+ */
+void MacStatsStripChart::
+handle_button_press(int graph_x, int graph_y, bool double_click, int button) {
+ if (graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) {
+ if (double_click && button == 0) {
+ // Double-clicking on a color bar in the graph is the same as double-
+ // clicking on the corresponding label.
+ on_click_label(get_collector_under_pixel(graph_x, graph_y));
+ return;
+ }
+
+ if (_potential_drag_mode == DM_none) {
+ set_drag_mode(DM_scale);
+ _drag_scale_start = pixel_to_height(graph_y);
+ // SetCapture(_graph_window);
+ return;
+ }
+ }
+
+ if (_potential_drag_mode == DM_guide_bar && _drag_guide_bar >= 0) {
+ set_drag_mode(DM_guide_bar);
+ _drag_start_y = graph_y;
+ // SetCapture(_graph_window);
+ return;
+ }
+
+ return MacStatsGraph::handle_button_press(graph_x, graph_y,
+ double_click, button);
+}
+
+/**
+ * Called when the mouse button is released within the graph window.
+ */
+void MacStatsStripChart::
+handle_button_release(int graph_x, int graph_y) {
+ if (_drag_mode == DM_scale) {
+ set_drag_mode(DM_none);
+ // ReleaseCapture();
+ return handle_motion(graph_x, graph_y);
+ }
+ else if (_drag_mode == DM_guide_bar) {
+ if (graph_y < 0 || graph_y >= get_ysize()) {
+ remove_user_guide_bar(_drag_guide_bar);
+ } else {
+ move_user_guide_bar(_drag_guide_bar, pixel_to_height(graph_y));
+ }
+ set_drag_mode(DM_none);
+ // ReleaseCapture();
+ return handle_motion(graph_x, graph_y);
+ }
+
+ return MacStatsGraph::handle_button_release(graph_x, graph_y);
+}
+
+/**
+ * Called when the mouse is moved within the graph window.
+ */
+void MacStatsStripChart::
+handle_motion(int graph_x, int graph_y) {
+ if (_drag_mode == DM_none && _potential_drag_mode == DM_none &&
+ graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) {
+ // When the mouse is over a color bar, highlight it.
+ int collector_index = get_collector_under_pixel(graph_x, graph_y);
+ _label_stack.highlight_label(collector_index);
+ on_enter_label(collector_index);
+ }
+ else {
+ // If the mouse is in some drag mode, stop highlighting.
+ _label_stack.highlight_label(-1);
+ on_leave_label(_highlighted_index);
+ }
+
+ if (_drag_mode == DM_scale) {
+ double ratio = 1.0 - ((double)graph_y / (double)get_ysize());
+ if (ratio > 0.0) {
+ double new_scale = _drag_scale_start / ratio;
+ if (!IS_NEARLY_EQUAL(get_vertical_scale(), new_scale)) {
+ // Disable smoothing while we do this expensive operation.
+ set_vertical_scale(_drag_scale_start / ratio);
+ }
+ }
+ return;
+ }
+ else if (_drag_mode == DM_new_guide_bar) {
+ // We haven't created the new guide bar yet; we won't until the mouse
+ // comes within the graph's region.
+ if (graph_y >= 0 && graph_y < get_ysize()) {
+ set_drag_mode(DM_guide_bar);
+ _drag_guide_bar = add_user_guide_bar(pixel_to_height(graph_y));
+ return;
+ }
+ }
+ else if (_drag_mode == DM_guide_bar) {
+ move_user_guide_bar(_drag_guide_bar, pixel_to_height(graph_y));
+ return;
+ }
+
+ MacStatsGraph::handle_motion(graph_x, graph_y);
+}
+
+/**
+ * Called when the mouse has left the graph window.
+ */
+void MacStatsStripChart::
+handle_leave() {
+ _label_stack.highlight_label(-1);
+ on_leave_label(_highlighted_index);
+ return;
+}
+
+/**
+ *
+ */
+void MacStatsStripChart::
+handle_magnify(int graph_x, int graph_y, double scale) {
+ set_vertical_scale(get_vertical_scale() * (1.0 - scale));
+}
+
+/**
+ * Fills in the graph window.
+ */
+void MacStatsStripChart::
+handle_draw_graph(CGContextRef ctx, NSRect rect) {
+ MacStatsGraph::handle_draw_graph(ctx, rect);
+
+ int width = get_xsize();
+
+ // Draw in the guide bars.
+ int num_guide_bars = get_num_guide_bars();
+ for (int i = 0; i < num_guide_bars; i++) {
+ draw_guide_bar(ctx, 0, width, get_guide_bar(i));
+ }
+
+ int num_user_guide_bars = get_num_user_guide_bars();
+ for (int i = 0; i < num_user_guide_bars; i++) {
+ draw_guide_bar(ctx, 0, width, get_user_guide_bar(i));
+ }
+}
+
+/**
+ * Fills in the scale area.
+ */
+void MacStatsStripChart::
+handle_draw_scale_area(CGContextRef ctx, NSRect rect) {
+ MacStatsGraph::handle_draw_scale_area(ctx, rect);
+ draw_guide_labels(ctx);
+}
+
+/**
+ * Called when the mouse clicks the back button in the toolbar.
+ */
+void MacStatsStripChart::
+handle_back() {
+ if (!_back_stack.empty()) {
+ int collector_index = _back_stack.back();
+ _back_stack.pop_back();
+ set_collector_index(collector_index);
+
+ // Update window title and total label.
+ new_data(0, 0);
+ _scale_area.needsDisplay = YES;
+ }
+
+ if (_back_stack.empty()) {
+ _graph_view_controller.backToolbarItemVisible = NO;
+ }
+}
+
+/**
+ * Draws the line for the indicated guide bar on the graph.
+ */
+void MacStatsStripChart::
+draw_guide_bar(CGContextRef ctx, int from_x, int to_x,
+ const PStatGraph::GuideBar &bar) {
+ int y = height_to_pixel(bar._height);
+
+ if (y > 1) {
+ // Only draw it if it's not too close to the top.
+ /*switch (bar._style) {
+ case GBS_target:
+ CGContextSetRGBStrokeColor(ctx, rgb_light_gray[0], rgb_light_gray[1], rgb_light_gray[2], 1.0);
+ break;
+
+ case GBS_user:
+ CGContextSetRGBStrokeColor(ctx, rgb_user_guide_bar[0], rgb_user_guide_bar[1], rgb_user_guide_bar[2], 1.0);
+ break;
+
+ default:
+ CGContextSetRGBStrokeColor(ctx, rgb_dark_gray[0], rgb_dark_gray[1], rgb_dark_gray[2], 1.0);
+ break;
+ }*/
+ CGContextSetStrokeColorWithColor(ctx, [NSColor gridColor].CGColor);
+ CGContextBeginPath(ctx);
+ CGContextMoveToPoint(ctx, from_x, y);
+ CGContextAddLineToPoint(ctx, to_x, y);
+ CGContextStrokePath(ctx);
+ }
+}
+
+/**
+ * This is called during the servicing of the draw event.
+ */
+void MacStatsStripChart::
+draw_guide_labels(CGContextRef ctx) {
+ // Draw in the labels for the guide bars.
+ int last_y = 0;
+
+ int i;
+ int num_guide_bars = get_num_guide_bars();
+ for (i = 0; i < num_guide_bars; i++) {
+ last_y = draw_guide_label(ctx, get_guide_bar(i), last_y);
+ }
+
+ GuideBar top_value = make_guide_bar(get_vertical_scale());
+ draw_guide_label(ctx, top_value, last_y);
+
+ last_y = 0;
+ int num_user_guide_bars = get_num_user_guide_bars();
+ for (i = 0; i < num_user_guide_bars; i++) {
+ last_y = draw_guide_label(ctx, get_user_guide_bar(i), last_y);
+ }
+}
+
+/**
+ * Draws the text for the indicated guide bar label to the right of the graph,
+ * unless it would overlap with the indicated last label, whose top pixel
+ * value is given. Returns the top pixel value of the new label.
+ */
+int MacStatsStripChart::
+draw_guide_label(CGContextRef ctx, const PStatGraph::GuideBar &bar, int last_y) {
+ NSColor *color;
+ switch (bar._style) {
+ case GBS_target:
+ color = [NSColor secondaryLabelColor];
+ break;
+
+ case GBS_user:
+ color = [NSColor tertiaryLabelColor];
+ break;
+
+ default:
+ color = [NSColor labelColor];
+ break;
+ }
+
+ int y = height_to_pixel(bar._height);
+ const std::string &label = bar._label;
+
+ const CFStringRef keys[] = {
+ (__bridge CFStringRef)NSForegroundColorAttributeName,
+ (__bridge CFStringRef)NSFontAttributeName,
+ };
+ const void *values[] = {
+ color,
+ [NSFont systemFontOfSize:0.0],
+ };
+ CFDictionaryRef attribs = CFDictionaryCreate(kCFAllocatorDefault, (const void **)keys, (const void **)values, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+
+ CFStringRef str = CFStringCreateWithCString(kCFAllocatorDefault, label.c_str(), kCFStringEncodingUTF8);
+ CFAttributedStringRef astr = CFAttributedStringCreate(kCFAllocatorDefault, str, attribs);
+ CFRelease(attribs);
+ CFRelease(str);
+
+ CTLineRef line = CTLineCreateWithAttributedString(astr);
+ CFRelease(astr);
+ CGRect bounds = CTLineGetImageBounds(line, ctx);
+ int height = bounds.size.height;
+
+ if (bar._style != GBS_user) {
+ double from_height = pixel_to_height(y + height);
+ double to_height = pixel_to_height(y - height);
+ if (find_user_guide_bar(from_height, to_height) >= 0) {
+ // Omit the label: there's a user-defined guide bar in the same space.
+ CFRelease(line);
+ return last_y;
+ }
+ }
+
+ if (y > height && y < get_ysize() - height) {
+ // Now convert our y to a coordinate within our drawing area.
+
+ int this_y = y - height / 2;
+ if (last_y < this_y || last_y > this_y + height) {
+ CGContextSetTextPosition(ctx, 4, get_ysize() - this_y - height);
+ CTLineDraw(line, ctx);
+
+ last_y = this_y;
+ }
+ }
+
+ CFRelease(line);
+ return last_y;
+}
+
+@implementation MacStatsStripChartViewController
+
+- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar {
+ if (@available(macOS 10.15, *)) {
+ return @[@"smooth", @"sep", @"total"];
+ } else {
+ return @[@"smooth", @"sep"];
+ }
+}
+
+- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar {
+ if (@available(macOS 10.15, *)) {
+ return @[@"smooth", @"sep", @"total"];
+ } else {
+ return @[@"smooth", @"sep"];
+ }
+}
+
+- (NSToolbarItem *) toolbar:(NSToolbar *)toolbar
+ itemForItemIdentifier:(NSString *)ident
+ willBeInsertedIntoToolbar:(BOOL)flag {
+
+ if (@available(macOS 11.0, *)) {
+ if ([ident isEqual:@"smooth"]) {
+ NSButton *button = [NSButton buttonWithTitle:@"Smooth" target:self action:@selector(handleToggleSmooth:)];
+ button.image = [NSImage imageWithSystemSymbolName:@"alternatingcurrent" accessibilityDescription:@""];
+ button.bezelStyle = NSBezelStyleTexturedRounded;
+ button.buttonType = NSButtonTypePushOnPushOff;
+ button.bordered = YES;
+ button.toolTip = @"Smooth";
+ button.state = NSOffState;
+ NSToolbarItem *item = [[[NSToolbarItem alloc] initWithItemIdentifier:ident] autorelease];
+ item.label = @"Smooth";
+ item.view = button;
+ return item;
+ }
+ if ([ident isEqual:@"total"]) {
+ NSToolbarItem *item = [[[NSToolbarItem alloc] initWithItemIdentifier:ident] autorelease];
+ item.label = @"Total";
+ item.action = @selector(handleClickTotal:);
+ item.target = self;
+ [item setBordered:YES];
+ return item;
+ }
+ }
+
+ return [super toolbar:toolbar itemForItemIdentifier:ident willBeInsertedIntoToolbar:flag];
+}
+
+- (void)handleToggleSmooth:(NSMenuItem *)item {
+ MacStatsStripChart *graph = (MacStatsStripChart *)_graph;
+ graph->set_average_mode(item.state == NSOnState);
+}
+
+- (void)handleClickTotal:(NSButton *)button {
+ MacStatsStripChart *graph = (MacStatsStripChart *)_graph;
+ graph->set_auto_vertical_scale();
+}
+
+@end
diff --git a/pandatool/src/mac-stats/macStatsTimeline.h b/pandatool/src/mac-stats/macStatsTimeline.h
new file mode 100644
index 0000000000..e26d5e7cae
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsTimeline.h
@@ -0,0 +1,100 @@
+/**
+ * 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 macStatsTimeline.h
+ * @author rdb
+ * @date 2023-08-19
+ */
+
+#ifndef MACSTATSTIMELINE_H
+#define MACSTATSTIMELINE_H
+
+#include "macStatsGraph.h"
+#include "pStatTimeline.h"
+
+class MacStatsMonitor;
+
+/**
+ * A window that draws all of the start/stop event pairs on each thread on a
+ * horizontal scrolling timeline, with concurrent start/stop pairs stacked
+ * underneath each other.
+ */
+class MacStatsTimeline final : public PStatTimeline, public MacStatsGraph {
+public:
+ MacStatsTimeline(MacStatsMonitor *monitor);
+ virtual ~MacStatsTimeline();
+
+ virtual void new_data(int thread_index, int frame_number);
+ virtual void force_redraw();
+ virtual void changed_graph_size(int graph_xsize, int graph_ysize);
+
+protected:
+ virtual void clear_region();
+ virtual void begin_draw();
+ virtual void draw_separator(int row);
+ virtual void draw_guide_bar(int x, GuideBarStyle style);
+ virtual void draw_bar(int row, int from_x, int to_x, int collector_index,
+ const std::string &collector_name);
+ virtual void end_draw();
+ virtual void idle();
+
+ virtual bool animate(double time, double dt);
+
+ virtual 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 NSMenu *get_graph_menu(int mouse_x, int mouse_y) const;
+ virtual std::string get_graph_tooltip(int mouse_x, int mouse_y) const;
+ virtual DragMode consider_drag_start(int graph_x, int graph_y);
+
+ virtual bool handle_key(int graph_x, int graph_y, bool pressed,
+ UniChar c, unsigned short key_code);
+ virtual void handle_button_press(int graph_x, int graph_y,
+ bool double_click, int button);
+ virtual void handle_button_release(int graph_x, int graph_y);
+ virtual void handle_motion(int graph_x, int graph_y);
+ virtual void handle_leave();
+ virtual void handle_scroll();
+ virtual void handle_wheel(int graph_x, int graph_y, double dx, double dy);
+ virtual void handle_magnify(int graph_x, int graph_y, double scale);
+ virtual void handle_draw_graph(CGContextRef ctx, NSRect rect);
+ virtual void handle_draw_graph_overhang(CGContextRef ctx, NSRect rect);
+ virtual void handle_draw_scale_area(CGContextRef ctx, NSRect rect);
+
+public:
+ void handle_zoom_to();
+ void handle_open_strip_chart();
+ void handle_open_flame_graph();
+ void handle_open_piano_roll();
+
+private:
+ void draw_guide_bar(CGContextRef ctx, GuideBarStyle style, int x, int y, int height);
+ void draw_guide_labels(CGContextRef ctx);
+ void draw_guide_label(CGContextRef ctx, const GuideBar &bar);
+
+ int row_to_pixel(int row) const {
+ return row * 4 * 5 + 4 - _scroll;
+ }
+ int pixel_to_row(int y) const {
+ return (y + _scroll - 4) / (4 * 5);
+ }
+
+ NSView *_thread_area;
+ pvector > _thread_labels;
+ NSLayoutConstraint *_graph_height_constraint;
+ NSScrollView *_sidebar_scroll_view;
+
+ int _highlighted_row = -1;
+ int _highlighted_x = 0;
+ int _scroll = 0;
+ mutable ColorBar _popup_bar;
+};
+
+#endif
diff --git a/pandatool/src/mac-stats/macStatsTimeline.mm b/pandatool/src/mac-stats/macStatsTimeline.mm
new file mode 100644
index 0000000000..4a971e7a2b
--- /dev/null
+++ b/pandatool/src/mac-stats/macStatsTimeline.mm
@@ -0,0 +1,932 @@
+/**
+ * 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 macStatsTimeline.mm
+ * @author rdb
+ * @date 2023-08-19
+ */
+
+#include "macStatsTimeline.h"
+#include "macStatsMonitor.h"
+#include "macStatsLabelStack.h"
+#include "macStatsScaleArea.h"
+#include "pStatCollectorDef.h"
+
+@interface MacStatsTimelineViewController : MacStatsScrollableGraphViewController
+@end
+
+static const int default_timeline_width = 1000;
+static const int default_timeline_height = 300;
+
+static const int minimum_timeline_sidebar_width = 68;
+static const int default_timeline_sidebar_width = 100;
+
+/**
+ *
+ */
+MacStatsTimeline::
+MacStatsTimeline(MacStatsMonitor *monitor) :
+ PStatTimeline(monitor, 0, 0),
+ MacStatsGraph(monitor, [MacStatsTimelineViewController alloc])
+{
+ // Let's show the units on the guide bar labels. There's room.
+ set_guide_bar_units(get_guide_bar_units() | GBU_show_units);
+
+ _window.title = @"Timeline";
+
+ if (@available(macOS 11.0, *)) {
+ _window.titleVisibility = NSWindowTitleHidden;
+ }
+
+ // Set the initial size of the graph.
+ const ThreadRow &last_thread_row = _threads.back();
+ int height = row_to_pixel(last_thread_row._row_offset + last_thread_row._rows.size());
+ if (height < default_timeline_height) {
+ height = default_timeline_height;
+ }
+ height += _window.frame.size.height - _window.contentLayoutRect.size.height;
+ _graph_view.frame = NSMakeRect(0, 0, default_timeline_width, height);
+ _graph_view_controller.view.frame = NSMakeRect(0, 0, default_timeline_width, height);
+
+ // Add a drawing area to the left of the graph to show the thread labels.
+ _thread_area = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, default_timeline_sidebar_width, 0)];
+ _thread_area.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
+ _thread_area.translatesAutoresizingMaskIntoConstraints = NO;
+
+ // It's put inside a scroll view that tracks the main scroll view.
+ NSScrollView *scroll_view = [[NSScrollView alloc] initWithFrame:NSMakeRect(0, 0, default_timeline_sidebar_width, 0)];
+ scroll_view.documentView = _thread_area;
+ scroll_view.drawsBackground = NO;
+ scroll_view.automaticallyAdjustsContentInsets = YES;
+ //scroll_view.translatesAutoresizingMaskIntoConstraints = NO;
+ scroll_view.hasHorizontalScroller = NO;
+ scroll_view.hasVerticalScroller = NO;
+ _sidebar_scroll_view = scroll_view;
+
+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+ [center addObserver:_graph_view_controller
+ selector:@selector(handleSideScroll:)
+ name:NSScrollViewDidLiveScrollNotification
+ object:scroll_view];
+
+ NSViewController *sidebar_controller = [[NSViewController alloc] init];
+ sidebar_controller.view = scroll_view;
+
+ NSSplitViewController *svc = [[NSSplitViewController alloc] init];
+ [svc addSplitViewItem:[NSSplitViewItem sidebarWithViewController:sidebar_controller]];
+ [svc addSplitViewItem:[NSSplitViewItem splitViewItemWithViewController:_graph_view_controller]];
+
+ svc.splitViewItems[0].minimumThickness = minimum_timeline_sidebar_width;
+ svc.splitViewItems[0].canCollapse = NO;
+
+ NSSplitView *split_view = svc.splitView;
+ split_view.vertical = YES;
+ split_view.dividerStyle = NSSplitViewDividerStyleThin;
+ split_view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
+ _split_view = split_view;
+
+ _window.contentViewController = svc;
+
+ [svc release];
+ [sidebar_controller release];
+
+ // Scale area goes on top, as a titlebar accessory view.
+ MacStatsScaleAreaController *scale_area_controller = [[MacStatsScaleAreaController alloc] initWithGraph:this];
+ scale_area_controller.fullScreenMinHeight = 20;
+ scale_area_controller.layoutAttribute = NSLayoutAttributeRight;
+ _scale_area = scale_area_controller.view;
+ [_window addTitlebarAccessoryViewController:scale_area_controller];
+ [scale_area_controller release];
+
+ [_thread_area.widthAnchor constraintEqualToAnchor:scroll_view.widthAnchor].active = YES;
+ [_thread_area.heightAnchor constraintEqualToAnchor:_graph_view.heightAnchor].active = YES;
+
+ _graph_height_constraint = [_graph_view.heightAnchor constraintGreaterThanOrEqualToConstant:0];
+ _graph_height_constraint.active = YES;
+
+ [_window makeKeyAndOrderFront:nil];
+}
+
+/**
+ *
+ */
+MacStatsTimeline::
+~MacStatsTimeline() {
+ [_thread_area release];
+ [_sidebar_scroll_view release];
+}
+
+/**
+ * Called as each frame's data is made available. There is no guarantee the
+ * frames will arrive in order, or that all of them will arrive at all. The
+ * monitor should be prepared to accept frames received out-of-order or
+ * missing.
+ */
+void MacStatsTimeline::
+new_data(int thread_index, int frame_number) {
+ PStatTimeline::new_data(thread_index, frame_number);
+}
+
+/**
+ * Called when it is necessary to redraw the entire graph.
+ */
+void MacStatsTimeline::
+force_redraw() {
+ if (_ctx) {
+ PStatTimeline::force_redraw();
+ }
+}
+
+/**
+ * Called when the user has resized the window, forcing a resize of the graph.
+ */
+void MacStatsTimeline::
+changed_graph_size(int graph_xsize, int graph_ysize) {
+ PStatTimeline::changed_size(graph_xsize, graph_ysize);
+}
+
+/**
+ * Erases the chart area.
+ */
+void MacStatsTimeline::
+clear_region() {
+ if (_ctx) {
+ CGContextSetFillColorWithColor(_ctx, _background_color);
+ CGContextFillRect(_ctx, CGRectMake(0, 0, get_xsize(), get_ysize()));
+
+ CGContextSetTextMatrix(_ctx, CGAffineTransformMakeScale(1, -1));
+ //draw_guide_labels(_ctx);
+ }
+}
+
+/**
+ * Erases the chart area in preparation for drawing a bunch of bars.
+ */
+void MacStatsTimeline::
+begin_draw() {
+}
+
+/**
+ * Draws a horizontal separator.
+ */
+void MacStatsTimeline::
+draw_separator(int row) {
+ if (_ctx) {
+ CGContextSetFillColorWithColor(_ctx, [NSColor gridColor].CGColor);
+ CGContextFillRect(_ctx, CGRectMake(0, (row_to_pixel(row) + row_to_pixel(row + 1)) / 2.0, get_xsize(), 4.0 / 3.0));
+ }
+}
+
+/**
+ * Draws a vertical guide bar. If the row is -1, draws it in all rows.
+ */
+void MacStatsTimeline::
+draw_guide_bar(int x, GuideBarStyle style) {
+ draw_guide_bar(_ctx, style, x, 0, get_ysize());
+}
+
+/**
+ * Draws a single bar in the chart for the indicated row, in the color for the
+ * given collector, for the indicated horizontal pixel range.
+ */
+void MacStatsTimeline::
+draw_bar(int row, int from_x, int to_x, int collector_index,
+ const std::string &collector_name) {
+ int top = row_to_pixel(row);
+ int bottom = row_to_pixel(row + 1);
+ int scale = 4;
+
+ top += 1;
+
+ MacStatsMonitor *monitor = MacStatsGraph::_monitor;
+
+ bool is_highlighted = row == _highlighted_row && _highlighted_x >= from_x && _highlighted_x < to_x;
+ CGContextSetFillColorWithColor(_ctx,
+ monitor->get_collector_color(collector_index, is_highlighted));
+
+ if (to_x < from_x + 1) {
+ // Too tiny to draw.
+ }
+ else if (to_x < from_x + scale) {
+ // It's just a tiny sliver. This is a more reliable way to draw it.
+ CGRect rect = CGRectMake(from_x, top, to_x - from_x, bottom - top);
+ CGContextFillRect(_ctx, rect);
+ }
+ else {
+ int left = std::max(from_x, -scale - 1);
+ int right = std::min(std::max(to_x, from_x + 1), get_xsize() + scale);
+
+ double radius = std::min((double)scale, (right - left) / 2.0);
+ CGContextBeginPath(_ctx);
+ CGContextAddArc(_ctx, right - radius - 0.5, top + radius, radius, -0.5 * M_PI, 0.0, NO);
+ CGContextAddArc(_ctx, right - radius - 0.5, bottom - radius, radius, 0.0, 0.5 * M_PI, NO);
+ CGContextAddArc(_ctx, left + radius, bottom - radius, radius, 0.5 * M_PI, M_PI, NO);
+ CGContextAddArc(_ctx, left + radius, top + radius, radius, M_PI, 1.5 * M_PI, NO);
+ CGContextClosePath(_ctx);
+ CGContextFillPath(_ctx);
+
+ if ((to_x - from_x) >= scale * 4) {
+ // Only bother drawing the text if we've got some space to draw on.
+ const PStatClientData *client_data = monitor->get_client_data();
+ const PStatCollectorDef &def = client_data->get_collector_def(collector_index);
+
+ const CFStringRef keys[] = {
+ (__bridge CFStringRef)NSForegroundColorAttributeName,
+ (__bridge CFStringRef)NSFontAttributeName,
+ };
+ const void *values[] = {
+ monitor->get_collector_text_color(collector_index, is_highlighted),
+ [NSFont systemFontOfSize:0.0],
+ };
+ CFDictionaryRef attribs = CFDictionaryCreate(kCFAllocatorDefault, (const void **)keys, (const void **)values, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+
+ CFStringRef str = CFStringCreateWithCString(kCFAllocatorDefault, def._name.c_str(), kCFStringEncodingUTF8);
+ CFAttributedStringRef astr = CFAttributedStringCreate(kCFAllocatorDefault, str, attribs);
+
+ CTLineRef line = CTLineCreateWithAttributedString(astr);
+ CGRect bounds = CTLineGetImageBounds(line, _ctx);
+ CFRelease(astr);
+ CFRelease(str);
+
+ int text_width = bounds.size.width;
+ int text_height = bounds.size.height;
+
+ double center = (from_x + to_x) / 2.0;
+ double text_left = std::max(from_x, 0) + scale / 2.0;
+ double text_right = std::min(to_x, get_xsize()) - scale / 2.0;
+ double text_top = top + (bottom - top - text_height) / 2.0 + text_height;
+/*
+ if (text_width >= text_right - text_left) {
+ size_t c = collector_name.rfind(':');
+ if (text_right - text_left < scale * 6) {
+ // It's a really tiny space. Draw a single letter.
+ const char *ch = collector_name.data() + (c != std::string::npos ? c + 1 : 0);
+ pango_layout_set_alignment(layout, PANGO_ALIGN_CENTER);
+ pango_layout_set_text(layout, ch, 1);
+ } else {
+ // Maybe just use everything after the last colon.
+ if (c != std::string::npos) {
+ pango_layout_set_text(layout, collector_name.data() + c + 1,
+ collector_name.size() - c - 1);
+ pango_layout_get_pixel_size(layout, &text_width, &text_height);
+ }
+ }
+ }*/
+
+ if (text_width >= text_right - text_left) {
+ // Have CoreText truncate to the correct length.
+ static CFStringRef token_str = CFSTR("\u2026");
+ CFAttributedStringRef token_astr = CFAttributedStringCreate(kCFAllocatorDefault, token_str, attribs);
+ CTLineRef token_line = CTLineCreateWithAttributedString(token_astr);
+ CTLineRef trunc_line = CTLineCreateTruncatedLine(line, text_right - text_left, kCTLineTruncationEnd, token_line);
+ CFRelease(line);
+ CFRelease(token_astr);
+ CFRelease(token_line);
+ line = trunc_line;
+ CGContextSetTextPosition(_ctx, text_left, text_top);
+ }
+ else if (center - text_width / 2.0 < 0.0) {
+ // Put it against the left-most edge.
+ CGContextSetTextPosition(_ctx, scale, text_top);
+ }
+ else if (center + text_width / 2.0 >= get_xsize()) {
+ // Put it against the right-most edge.
+ CGContextSetTextPosition(_ctx, get_xsize() - scale - text_width, text_top);
+ }
+ else {
+ // It fits just fine, center it.
+ CGContextSetTextPosition(_ctx, center - text_width / 2.0, text_top);
+ }
+
+ if (line != nullptr) {
+ CTLineDraw(line, _ctx);
+ CFRelease(line);
+ }
+ CFRelease(attribs);
+ }
+ }
+}
+
+/**
+ * Called after all the bars have been drawn, this triggers a refresh event to
+ * draw it to the window.
+ */
+void MacStatsTimeline::
+end_draw() {
+ // Recalculate the size of the graph.
+ if (!_threads.empty()) {
+ const ThreadRow &last_thread_row = _threads.back();
+ int new_height = row_to_pixel(last_thread_row._row_offset + last_thread_row._rows.size());
+ _graph_height_constraint.constant = new_height;
+ }
+
+ _graph_view.needsDisplay = YES;
+
+ // If we scroll sideways while we're also scrolling vertically such that the
+ // overhang becomes visible due to elasticity, the overhang doesn't update.
+ // I could only find this private method for fixing this problem.
+ // Not needed as of macOS 14 and up, since the normal drawRect DTRT there.
+#if __MAC_OS_X_VERSION_MAX_ALLOWED < 140000
+ NSClipView *clip_view = ((MacStatsScrollableGraphViewController *)_graph_view_controller).clipView;
+ if ([clip_view respondsToSelector:@selector(_setNeedsDisplayInOverhang:)]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wobjc-method-access"
+ [clip_view _setNeedsDisplayInOverhang:YES];
+#pragma clang diagnostic pop
+ }
+#endif
+
+ if (_threads_changed) {
+ while (_thread_labels.size() < _threads.size()) {
+ NSTextField *label = [NSTextField labelWithString:@"Thread"];
+ label.translatesAutoresizingMaskIntoConstraints = NO;
+ [_thread_area addSubview:label];
+
+ [label.rightAnchor constraintEqualToAnchor:_thread_area.rightAnchor constant:-8].active = YES;
+ [_thread_area.widthAnchor constraintGreaterThanOrEqualToAnchor:label.widthAnchor constant:16].active = YES;
+
+ NSLayoutConstraint *constraint;
+ if (@available(macOS 11.0, *)) {
+ constraint = [label.topAnchor constraintEqualToAnchor:_thread_area.topAnchor];
+ } else {
+ constraint = [label.topAnchor constraintEqualToAnchor:_thread_area.topAnchor];
+ }
+ constraint.active = YES;
+
+ _thread_labels.push_back(std::make_pair(label, constraint));
+ }
+
+ for (size_t i = 0; i < _threads.size(); ++i) {
+ const ThreadRow &thread_row = _threads[i];
+ NSTextField *label = _thread_labels[i].first;
+ NSLayoutConstraint *label_constraint = _thread_labels[i].second;
+
+ label.stringValue = [NSString stringWithUTF8String:thread_row._label.c_str()];
+ label_constraint.constant = row_to_pixel(thread_row._row_offset);
+ }
+
+ _thread_area.needsDisplay = YES;
+ _threads_changed = false;
+ }
+
+ if (_guide_bars_changed) {
+ _scale_area.needsDisplay = YES;
+ _guide_bars_changed = false;
+ }
+}
+
+/**
+ * Called at the end of the draw cycle.
+ */
+void MacStatsTimeline::
+idle() {
+}
+
+/**
+ * Overridden by a derived class to implement an animation. If it returns
+ * false, the animation timer is stopped.
+ */
+bool MacStatsTimeline::
+animate(double time, double dt) {
+ return PStatTimeline::animate(time, dt);
+}
+
+/**
+ * Returns the current window dimensions.
+ */
+bool MacStatsTimeline::
+get_window_state(int &x, int &y, int &width, int &height,
+ bool &maximized, bool &minimized) const {
+ MacStatsGraph::get_window_state(x, y, width, height, maximized, minimized);
+ return true;
+}
+
+/**
+ * Called to restore the graph window to its previous dimensions.
+ */
+void MacStatsTimeline::
+set_window_state(int x, int y, int width, int height,
+ bool maximized, bool minimized) {
+ MacStatsGraph::set_window_state(x, y, width, height, maximized, minimized);
+}
+
+/**
+ * Called when the mouse right-clicks on the graph, and should return the menu
+ * that should pop up.
+ */
+NSMenu *MacStatsTimeline::
+get_graph_menu(int graph_x, int graph_y) const {
+ int row = pixel_to_row(graph_y);
+ ColorBar bar;
+ if (!find_bar(row, graph_x, bar)) {
+ return nil;
+ }
+
+ _popup_bar = bar;
+
+ NSMenu *menu = [[[NSMenu alloc] init] autorelease];
+
+ std::string label = get_bar_tooltip(row, graph_x);
+ if (!label.empty()) {
+ if (@available(macOS 14.0, *)) {
+ [menu addItem:[NSMenuItem sectionHeaderWithTitle:[NSString stringWithUTF8String:label.c_str()]]];
+ } else {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:[NSString stringWithUTF8String:label.c_str()] action:nil keyEquivalent:@""];
+ item.enabled = NO;
+ [menu addItem:item];
+ [item release];
+ }
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Zoom To" action:@selector(handleZoomTo:) keyEquivalent:@""];
+ item.target = _graph_view_controller;
+ [menu addItem:item];
+ [item release];
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Open Strip Chart" action:@selector(handleOpenStripChart:) keyEquivalent:@""];
+ item.target = _graph_view_controller;
+ [menu addItem:item];
+ [item release];
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Open Flame Graph" action:@selector(handleOpenFlameGraph:) keyEquivalent:@""];
+ item.target = _graph_view_controller;
+ [menu addItem:item];
+ [item release];
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Open Piano Roll" action:@selector(handleOpenPianoRoll:) keyEquivalent:@""];
+ item.target = _graph_view_controller;
+ [menu addItem:item];
+ [item release];
+ }
+
+ [menu addItem:[NSMenuItem separatorItem]];
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Change Color\u2026" action:@selector(handleChangeColor:) keyEquivalent:@""];
+ item.target = _graph_view_controller;
+ item.tag = bar._collector_index;
+ [menu addItem:item];
+ [item release];
+ }
+
+ {
+ NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"Reset Color" action:@selector(handleResetColor:) keyEquivalent:@""];
+ item.target = _graph_view_controller;
+ item.tag = bar._collector_index;
+ [menu addItem:item];
+ [item release];
+ }
+
+ return menu;
+}
+
+/**
+ * Called when the mouse hovers over the graph, and should return the text that
+ * should appear on the tooltip.
+ */
+std::string MacStatsTimeline::
+get_graph_tooltip(int mouse_x, int mouse_y) const {
+ return PStatTimeline::get_bar_tooltip(pixel_to_row(mouse_y), mouse_x);
+}
+
+/**
+ * Based on the mouse position within the graph window, look for draggable
+ * things the mouse might be hovering over and return the appropriate DragMode
+ * enum or DM_none if nothing is indicated.
+ */
+MacStatsGraph::DragMode MacStatsTimeline::
+consider_drag_start(int graph_x, int graph_y) {
+ return MacStatsGraph::consider_drag_start(graph_x, graph_y);
+}
+
+/**
+ *
+ */
+bool MacStatsTimeline::
+handle_key(int graph_x, int graph_y, bool pressed, UniChar c, unsigned short key_code) {
+ // Accept WASD based on their position rather than their mapping
+ int flag = 0;
+ switch (key_code) {
+ case 13:
+ flag = F_w;
+ break;
+ case 0:
+ flag = F_a;
+ break;
+ case 1:
+ flag = F_s;
+ break;
+ case 2:
+ flag = F_d;
+ break;
+ }
+ if (flag == 0) {
+ switch (c) {
+ case 0x1c:
+ case NSLeftArrowFunctionKey:
+ flag = F_left;
+ break;
+ case 0x1d:
+ case NSRightArrowFunctionKey:
+ flag = F_right;
+ break;
+ case 'w':
+ flag = F_w;
+ break;
+ case 'a':
+ flag = F_a;
+ break;
+ case 's':
+ flag = F_s;
+ break;
+ case 'd':
+ flag = F_d;
+ break;
+ }
+ }
+ if (flag != 0) {
+ if (pressed) {
+ if (flag & (F_w | F_s)) {
+ _zoom_center = pixel_to_timestamp(graph_x);
+ }
+ if (_keys_held == 0) {
+ start_animation();
+ }
+ _keys_held |= flag;
+ }
+ else if (_keys_held != 0) {
+ _keys_held &= ~flag;
+ }
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Called when the mouse button is depressed within the graph window.
+ */
+void MacStatsTimeline::
+handle_button_press(int graph_x, int graph_y, bool double_click, int button) {
+ if (graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) {
+ if (double_click && button == 0) {
+ // Double-clicking on a color bar in the graph will zoom the graph into
+ // that collector.
+ int row = pixel_to_row(graph_y);
+ ColorBar bar;
+ if (find_bar(row, graph_x, bar)) {
+ double width = bar._end - bar._start;
+ zoom_to(width * 1.5, pixel_to_timestamp(graph_x));
+ scroll_to(bar._start - width / 4.0);
+ } else {
+ // Double-clicking the white area zooms out.
+ _zoom_speed -= 100.0;
+ }
+ start_animation();
+ }
+
+ if (_potential_drag_mode == DM_none) {
+ set_drag_mode(DM_pan);
+ _drag_start_x = graph_x;
+ _scroll_speed = 0.0;
+ _zoom_center = pixel_to_timestamp(graph_x);
+ return;
+ }
+ }
+
+ return MacStatsGraph::handle_button_press(graph_x, graph_y,
+ double_click, button);
+}
+
+/**
+ * Called when the mouse button is released within the graph window.
+ */
+void MacStatsTimeline::
+handle_button_release(int graph_x, int graph_y) {
+ if (_drag_mode == DM_scale) {
+ set_drag_mode(DM_none);
+ // ReleaseCapture();
+ return handle_motion(graph_x, graph_y);
+ }
+ else if (_drag_mode == DM_guide_bar) {
+ if (graph_x < 0 || graph_x >= get_xsize()) {
+ remove_user_guide_bar(_drag_guide_bar);
+ } else {
+ move_user_guide_bar(_drag_guide_bar, pixel_to_height(graph_x));
+ }
+ set_drag_mode(DM_none);
+ // ReleaseCapture();
+ return handle_motion(graph_x, graph_y);
+ }
+
+ return MacStatsGraph::handle_button_release(graph_x, graph_y);
+}
+
+/**
+ * Called when the mouse is moved within the graph window.
+ */
+void MacStatsTimeline::
+handle_motion(int graph_x, int graph_y) {
+ if (_drag_mode == DM_none && _potential_drag_mode == DM_none &&
+ graph_x >= 0 && graph_y >= 0 && graph_x < get_xsize() && graph_y < get_ysize()) {
+ // When the mouse is over a color bar, highlight it.
+ int row = pixel_to_row(graph_y);
+ std::swap(_highlighted_x, graph_x);
+ std::swap(_highlighted_row, row);
+
+ if (row >= 0) {
+ PStatTimeline::force_redraw(row, graph_x, graph_x);
+ }
+ PStatTimeline::force_redraw(_highlighted_row, _highlighted_x, _highlighted_x);
+
+ if ((_keys_held & (F_w | F_s)) != 0) {
+ // Update the zoom center if we move the mouse while zooming with the
+ // keyboard.
+ _zoom_center = pixel_to_timestamp(graph_x);
+ }
+ }
+ else {
+ // If the mouse is in some drag mode, stop highlighting.
+ if (_highlighted_row != -1) {
+ int row = _highlighted_row;
+ _highlighted_row = -1;
+ PStatTimeline::force_redraw(row, _highlighted_x, _highlighted_x);
+ }
+ }
+
+ if (_drag_mode == DM_pan) {
+ int delta = _drag_start_x - graph_x;
+ _drag_start_x = graph_x;
+ set_horizontal_scroll(get_horizontal_scroll() + pixel_to_height(delta));
+ return;
+ }
+
+ return MacStatsGraph::handle_motion(graph_x, graph_y);
+}
+
+/**
+ * Called when the mouse has left the graph window.
+ */
+void MacStatsTimeline::
+handle_leave() {
+ if (_highlighted_row != -1) {
+ int row = _highlighted_row;
+ _highlighted_row = -1;
+ PStatTimeline::force_redraw(row, _highlighted_x, _highlighted_x);
+ }
+}
+
+/**
+ * Called when the mouse is moved within the graph window.
+ */
+void MacStatsTimeline::
+handle_scroll() {
+ // Graph view is flipped, side bar isn't, so we need to convert coordinates
+ NSPoint point;
+ point.x = 0;
+ point.y = _graph_view.frame.size.height - (((NSScrollView *)_graph_view_controller.view).documentVisibleRect.size.height + ((NSScrollView *)_graph_view_controller.view).documentVisibleRect.origin.y);
+ [_sidebar_scroll_view.contentView scrollToPoint:point];
+ [_sidebar_scroll_view reflectScrolledClipView:_sidebar_scroll_view.contentView];
+}
+
+/**
+ *
+ */
+void MacStatsTimeline::
+handle_wheel(int graph_x, int graph_y, double dx, double dy) {
+ if (dx != 0.0) {
+ _scroll_speed -= dx;
+ start_animation();
+ }
+}
+
+/**
+ *
+ */
+void MacStatsTimeline::
+handle_magnify(int graph_x, int graph_y, double scale) {
+ zoom_by(scale * 4.0, pixel_to_timestamp(graph_x));
+ start_animation();
+}
+
+/**
+ * Fills in the graph window.
+ */
+void MacStatsTimeline::
+handle_draw_graph(CGContextRef ctx, NSRect rect) {
+#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 140000
+ for (const GuideBar &bar : _guide_bars) {
+ int x = timestamp_to_pixel(bar._height);
+ draw_guide_bar(ctx, bar._style, x, rect.origin.y, rect.size.height);
+ }
+#endif
+
+ MacStatsGraph::handle_draw_graph(ctx, rect);
+
+ NSRect scale_frame = _scale_area.frame;
+ NSRect graph_frame = _graph_view.frame;
+ if (scale_frame.size.width != graph_frame.size.width) {
+ scale_frame.size.width = graph_frame.size.width;
+ _scale_area.frame = scale_frame;
+ }
+}
+
+/**
+ * Fills in the graph window overhang, which is the area outside the graph
+ * bounds that may become visible momentarily due to scroll elasticity.
+ */
+void MacStatsTimeline::
+handle_draw_graph_overhang(CGContextRef ctx, NSRect rect) {
+ CGContextSetFillColorWithColor(ctx, _background_color);
+ CGContextFillRect(ctx, rect);
+
+ for (const GuideBar &bar : _guide_bars) {
+ int x = timestamp_to_pixel(bar._height);
+ draw_guide_bar(ctx, bar._style, x, rect.origin.y, rect.size.height);
+ }
+}
+
+/**
+ * Fills in the scale area.
+ */
+void MacStatsTimeline::
+handle_draw_scale_area(CGContextRef ctx, NSRect rect) {
+ MacStatsGraph::handle_draw_scale_area(ctx, rect);
+
+ draw_guide_labels(ctx);
+
+ CGContextSetFillColorWithColor(ctx, [NSColor gridColor].CGColor);
+
+ for (const GuideBar &bar : _guide_bars) {
+ int x = timestamp_to_pixel(bar._height);
+ x = [_scale_area convertPoint:NSMakePoint(x, 0) fromView:_graph_view].x;
+ draw_guide_bar(ctx, bar._style, x, rect.origin.y, rect.size.height);
+ }
+}
+
+/**
+ *
+ */
+void MacStatsTimeline::
+handle_zoom_to() {
+ const ColorBar &bar = _popup_bar;
+ double width = bar._end - bar._start;
+ zoom_to(width * 1.5, (bar._end + bar._start) / 2.0);
+ scroll_to(bar._start - width / 4.0);
+ start_animation();
+}
+
+/**
+ *
+ */
+void MacStatsTimeline::
+handle_open_strip_chart() {
+ const ColorBar &bar = _popup_bar;
+ MacStatsGraph::_monitor->open_strip_chart(bar._thread_index, bar._collector_index, false);
+}
+
+/**
+ *
+ */
+void MacStatsTimeline::
+handle_open_flame_graph() {
+ const ColorBar &bar = _popup_bar;
+ MacStatsGraph::_monitor->open_flame_graph(bar._thread_index, bar._collector_index);
+}
+
+/**
+ *
+ */
+void MacStatsTimeline::
+handle_open_piano_roll() {
+ const ColorBar &bar = _popup_bar;
+ MacStatsGraph::_monitor->open_piano_roll(bar._thread_index);
+}
+
+/**
+ * Draws a vertical guide bar. If the row is -1, draws it in all rows.
+ */
+void MacStatsTimeline::
+draw_guide_bar(CGContextRef ctx, GuideBarStyle style, int x, int y, int height) {
+ double width = 1.0;
+ if (style == GBS_frame) {
+ width *= 2;
+ }
+
+ CGContextSetFillColorWithColor(ctx, [NSColor gridColor].CGColor);
+ CGContextFillRect(ctx, CGRectMake(x - width / 2.0, y, width, height));
+}
+
+/**
+ * This is called during the servicing of the draw event.
+ */
+void MacStatsTimeline::
+draw_guide_labels(CGContextRef ctx) {
+ int num_guide_bars = get_num_guide_bars();
+ for (int i = 0; i < num_guide_bars; i++) {
+ draw_guide_label(ctx, get_guide_bar(i));
+ }
+}
+
+/**
+ * Draws the text for the indicated guide bar label at the top of the graph.
+ */
+void MacStatsTimeline::
+draw_guide_label(CGContextRef ctx, const PStatGraph::GuideBar &bar) {
+ const std::string &label = bar._label;
+ if (label.empty()) {
+ return;
+ }
+
+ NSColor *color;
+ if (@available(macOS 11.0, *)) {
+ color = [NSColor tertiaryLabelColor];
+ } else {
+ // Otherwise it's hard to see on the dark titlebars
+ color = [NSColor windowFrameTextColor];
+ }
+ /*switch (bar._style) {
+ case GBS_target:
+ color = [NSColor colorWithDeviceRed:rgb_light_gray[0] green:rgb_light_gray[1] blue:rgb_light_gray[2] alpha:1.0];
+ break;
+
+ case GBS_user:
+ color = [NSColor colorWithDeviceRed:rgb_user_guide_bar[0] green:rgb_user_guide_bar[1] blue:rgb_user_guide_bar[2] alpha:1.0];
+ break;
+
+ case GBS_normal:
+ color = [NSColor colorWithDeviceRed:rgb_light_gray[0] green:rgb_light_gray[1] blue:rgb_light_gray[2] alpha:1.0];
+ break;
+
+ case GBS_frame:
+ color = [NSColor colorWithDeviceRed:rgb_dark_gray[0] green:rgb_dark_gray[1] blue:rgb_dark_gray[2] alpha:1.0];
+ break;
+ }*/
+
+ const CFStringRef keys[] = {
+ (__bridge CFStringRef)NSForegroundColorAttributeName,
+ (__bridge CFStringRef)NSFontAttributeName,
+ };
+ const void *values[] = {
+ color,
+ [NSFont systemFontOfSize:0.0],
+ };
+ CFDictionaryRef attribs = CFDictionaryCreate(kCFAllocatorDefault, (const void **)keys, (const void **)values, 2, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+
+ CFStringRef str = CFStringCreateWithCString(kCFAllocatorDefault, label.c_str(), kCFStringEncodingUTF8);
+ CFAttributedStringRef astr = CFAttributedStringCreate(kCFAllocatorDefault, str, attribs);
+ CFRelease(attribs);
+ CFRelease(str);
+
+ CTLineRef line = CTLineCreateWithAttributedString(astr);
+ CFRelease(astr);
+ CGRect bounds = CTLineGetImageBounds(line, ctx);
+ //int height = bounds.size.height;
+ int width = bounds.size.width;
+
+ NSRect graph_bounds = _graph_view.bounds;
+
+ int x = timestamp_to_pixel(bar._height);
+ x = [_scale_area convertPoint:NSMakePoint(x, 0) fromView:_graph_view].x;
+
+ if (x + width >= 0 && x + width < get_xsize()) {
+ if (x + width < graph_bounds.size.width) {
+ CGContextSetTextPosition(ctx, x + 6, 6);
+ CTLineDraw(line, ctx);
+ }
+ }
+
+ CFRelease(line);
+}
+
+@implementation MacStatsTimelineViewController
+
+- (void)handleZoomTo:(NSMenuItem *)item {
+ ((MacStatsTimeline *)_graph)->handle_zoom_to();
+}
+
+- (void)handleOpenStripChart:(NSMenuItem *)item {
+ ((MacStatsTimeline *)_graph)->handle_open_strip_chart();
+}
+
+- (void)handleOpenFlameGraph:(NSMenuItem *)item {
+ ((MacStatsTimeline *)_graph)->handle_open_flame_graph();
+}
+
+- (void)handleOpenPianoRoll:(NSMenuItem *)item {
+ ((MacStatsTimeline *)_graph)->handle_open_piano_roll();
+}
+
+@end
+
diff --git a/pandatool/src/mac-stats/macstats_composite1.mm b/pandatool/src/mac-stats/macstats_composite1.mm
new file mode 100644
index 0000000000..5ae8136ab5
--- /dev/null
+++ b/pandatool/src/mac-stats/macstats_composite1.mm
@@ -0,0 +1,16 @@
+#include "macStats.mm"
+#include "macStatsAppDelegate.mm"
+#include "macStatsChartMenu.mm"
+#include "macStatsChartMenuDelegate.mm"
+#include "macStatsFlameGraph.mm"
+#include "macStatsGraph.mm"
+#include "macStatsGraphView.mm"
+#include "macStatsGraphViewController.mm"
+#include "macStatsLabel.mm"
+#include "macStatsLabelStack.mm"
+#include "macStatsMonitor.mm"
+#include "macStatsPianoRoll.mm"
+#include "macStatsScaleArea.mm"
+#include "macStatsServer.mm"
+#include "macStatsStripChart.mm"
+#include "macStatsTimeline.mm"
diff --git a/pandatool/src/pstatserver/pStatStripChart.cxx b/pandatool/src/pstatserver/pStatStripChart.cxx
index f04d0b3249..7861ae8d90 100644
--- a/pandatool/src/pstatserver/pStatStripChart.cxx
+++ b/pandatool/src/pstatserver/pStatStripChart.cxx
@@ -988,7 +988,12 @@ draw_frames(int first_frame, int last_frame) {
if (_scroll_mode) {
// In scrolling mode, slide the world back.
int slide_pixels = last_pixel - _xsize;
+ // This is really slow on macOS, just redraw instead
+#ifdef __APPLE__
+ draw_pixels(0, first_pixel - slide_pixels);
+#else
copy_region(slide_pixels, first_pixel, 0);
+#endif
first_pixel -= slide_pixels;
last_pixel -= slide_pixels;
_start_time += (double)slide_pixels / (double)_xsize * _time_width;