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;