pstats: Add macOS port

Closes #1531
This commit is contained in:
rdb 2023-08-28 20:17:02 +02:00
parent aa13953f2b
commit 71154492b9
37 changed files with 7926 additions and 2 deletions

View File

@ -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/

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleIdentifier</key>
<string>org.panda3d.pstats</string>
<key>CFBundleExecutable</key>
<string>pstats</string>
<key>CFBundleName</key>
<string>PStats</string>
<key>CFBundleDisplayName</key>
<string>PStats</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>LSHasLocalizedDisplayName</key>
<false/>
<key>NSAppleScriptEnabled</key>
<false/>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>PStats session file</string>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSTypeIsPackage</key>
<false/>
<key>CFBundleTypeExtensions</key>
<array>
<string>pstats</string>
</array>
<key>CFBundleTypeMIMETypes</key>
<array>
<string>application/vnd.panda3d.pstats</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@ -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 <Cocoa/Cocoa.h>
// 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 <NSObject>
- (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

View File

@ -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

View File

@ -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 <Carbon/Carbon.h>
#include <objc/runtime.h>
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;
}

View File

@ -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 <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
class MacStatsServer;
@interface MacStatsAppDelegate : NSObject<NSApplicationDelegate, NSUserNotificationCenterDelegate> {
@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

View File

@ -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

View File

@ -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 <Cocoa/Cocoa.h>
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<std::pair<NSMenuItem *, NSMenu *> > _collector_items;
int _time_items_end = 0;
int _level_items_end = 0;
};
#endif

View File

@ -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;
}

View File

@ -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 <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
class MacStatsMonitor;
@interface MacStatsChartMenuDelegate : NSObject<NSMenuDelegate> {
@private
MacStatsMonitor *_monitor;
int _thread_index;
}
- (id)initWithMonitor:(MacStatsMonitor *)monitor threadIndex:(int)index;
@end
#endif

View File

@ -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

View File

@ -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<int> _back_stack;
};
#endif

View File

@ -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

View File

@ -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

View File

@ -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;
}
}

View File

@ -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<NSViewToolTipOwner> {
@public
MacStatsGraph *_graph;
}
- (id)initWithGraph:(MacStatsGraph *)graph;
- (void)drawRect:(NSRect)dirtyRect;
@end
#endif

View File

@ -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

View File

@ -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 <Cocoa/Cocoa.h>
class MacStatsGraph;
@interface MacStatsGraphViewController : NSViewController<NSToolbarDelegate> {
@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

View File

@ -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

View File

@ -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<NSViewToolTipOwner> {
@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

View File

@ -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

View File

@ -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 <Cocoa/Cocoa.h>
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<MacStatsLabel *> Labels;
Labels _labels;
};
#endif

View File

@ -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];
}
}
}

View File

@ -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 <Cocoa/Cocoa.h>
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<MacStatsGraph *> Graphs;
Graphs _graphs;
typedef pvector<MacStatsChartMenu *> 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<int, ColorSet> Colors;
Colors _colors;
friend class MacStatsGraph;
friend class MacStatsServer;
};
#endif

View File

@ -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;
}

View File

@ -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

View File

@ -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);
}

View File

@ -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 <Cocoa/Cocoa.h>
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

View File

@ -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

View File

@ -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 <Cocoa/Cocoa.h>
/**
* 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

View File

@ -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 <unistd.h>
/**
*
*/
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];
}

View File

@ -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<int> _back_stack;
};
#endif

View File

@ -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

View File

@ -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<std::pair<NSTextField *, NSLayoutConstraint *> > _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

View File

@ -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

View File

@ -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"

View File

@ -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;