Vuo  2.0.2
VuoEditorWindowToolbar.mm
Go to the documentation of this file.
1 
11 #include "ui_VuoEditorWindow.h"
12 
13 #include "VuoActivityIndicator.hh"
14 #include "VuoEditor.hh"
15 #include "VuoEditorWindow.hh"
16 #include "VuoErrorDialog.hh"
17 #include "VuoCodeWindow.hh"
18 #include "VuoCodeEditorStages.hh"
19 
20 #if defined(slots)
21 #undef slots
22 #endif
23 
24 #ifndef NS_RETURNS_INNER_POINTER
25 #define NS_RETURNS_INNER_POINTER
26 #endif
27 #include <Cocoa/Cocoa.h>
28 #include <objc/message.h>
29 
30 
34 @interface VuoEditorZoomButtons : NSSegmentedControl
35 {
36  QMacToolBarItem *_toolBarItem;
37  bool _isDark;
39 }
40 @end
41 
42 @implementation VuoEditorZoomButtons
46 - (id)initWithQMacToolBarItem:(QMacToolBarItem *)toolBarItem isCodeEditor:(bool)codeEditor
47 {
48  if (!(self = [super init]))
49  return nil;
50 
51  _toolBarItem = toolBarItem;
52  _isDark = false;
53  _isCodeEditor = codeEditor;
54 
55  NSImage *zoomOutImage = [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"zoom-out" ofType:@"pdf"]];
56  [zoomOutImage setTemplate:YES];
57  NSImage *zoomFitImage = [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"zoom-fit" ofType:@"pdf"]];
58  [zoomFitImage setTemplate:YES];
59  NSImage *zoomActualImage = [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"zoom-actual" ofType:@"pdf"]];
60  [zoomActualImage setTemplate:YES];
61  NSImage *zoomInImage = [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"zoom-in" ofType:@"pdf"]];
62  [zoomInImage setTemplate:YES];
63 
64  [self setSegmentCount:4];
65  int segmentCount = 0;
66 
67  [self setImage:zoomOutImage forSegment:segmentCount];
68  [self.cell setToolTip:[NSString stringWithUTF8String:VuoEditor::tr("Zoom Out").toUtf8().data()] forSegment:segmentCount];
69  [zoomOutImage release];
70  ++segmentCount;
71 
72  [self setImage:zoomActualImage forSegment:segmentCount];
73  [self.cell setToolTip:[NSString stringWithUTF8String:VuoEditor::tr("Actual Size").toUtf8().data()] forSegment:segmentCount];
74  [zoomActualImage release];
75  ++segmentCount;
76 
77  if (!codeEditor)
78  {
79  [self setImage:zoomFitImage forSegment:segmentCount];
80  [self.cell setToolTip:[NSString stringWithUTF8String:VuoEditor::tr("Zoom to Fit").toUtf8().data()] forSegment:segmentCount];
81  [zoomFitImage release];
82  ++segmentCount;
83  }
84 
85  [self setImage:zoomInImage forSegment:segmentCount];
86  [self.cell setToolTip:[NSString stringWithUTF8String:VuoEditor::tr("Zoom In").toUtf8().data()] forSegment:segmentCount];
87  [zoomInImage release];
88  ++segmentCount;
89 
90  [self setSegmentCount:segmentCount];
91 
92  [[self cell] setTrackingMode:NSSegmentSwitchTrackingMomentary];
93 
94  return self;
95 }
96 
100 - (NSRect)bounds
101 {
102  return NSMakeRect(0, 0, [self segmentCount] == 4 ? 149 : 109, 24);
103 }
104 
108 - (NSString *)itemIdentifier
109 {
110  if (_isCodeEditor)
111  {
112  VuoCodeWindow *window = static_cast<VuoCodeWindow *>(_toolBarItem->parent()->parent());
113  if ([self isSelectedForSegment:0])
114  window->getZoomOutAction()->trigger();
115  else if ([self isSelectedForSegment:1])
116  window->getZoom11Action()->trigger();
117  else if ([self isSelectedForSegment:2])
118  window->getZoomInAction()->trigger();
119  }
120  else
121  {
122  VuoEditorWindow *window = static_cast<VuoEditorWindow *>(_toolBarItem->parent()->parent());
123  if ([self isSelectedForSegment:0])
124  window->getZoomOutAction()->trigger();
125  else if ([self isSelectedForSegment:1])
126  window->getZoom11Action()->trigger();
127  else if ([self isSelectedForSegment:2])
128  window->getZoomToFitAction()->trigger();
129  else if ([self isSelectedForSegment:3])
130  window->getZoomInAction()->trigger();
131  }
132 
133  return QString::number(qulonglong(_toolBarItem)).toNSString();
134 }
135 
139 - (void)drawRect:(NSRect)dirtyRect
140 {
141  NSColor *color = [NSColor colorWithCalibratedWhite:1 alpha:(_isDark ? .2 : 1)];
142  [color setFill];
143 
144  NSRect rect = [self bounds];
145  rect.origin.y += 1;
146  rect.size.width -= 2;
147  rect.size.height -= 1;
148  NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:rect xRadius:5.0 yRadius:5.0];
149  [path fill];
150 
151  [[self cell] drawInteriorWithFrame:dirtyRect inView:self];
152 }
153 
157 - (void)updateColor:(bool)isDark
158 {
159  _isDark = isDark;
160 
161  // Disable image-templating in dark mode, since it makes the icons too faint.
162  int segments = self.segmentCount;
163  for (int i = 0; i < segments; ++i)
164  [[self imageForSegment:i] setTemplate:!isDark];
165 
166  self.needsDisplay = YES;
167 }
168 @end
169 
170 
174 @interface VuoEditorEventsButton : NSView
175 {
176  QMacToolBarItem *_toolBarItem;
177  NSButton *button;
178  bool _isDark;
179 }
180 - (id)initWithQMacToolBarItem:(QMacToolBarItem *)toolBarItem;
181 - (NSString *)itemIdentifier;
182 - (void)setState:(bool)state;
183 - (NSButton *)button;
184 @end
185 
186 @implementation VuoEditorEventsButton
190 - (id)initWithQMacToolBarItem:(QMacToolBarItem *)toolBarItem
191 {
192  if (!(self = [super init]))
193  return nil;
194 
195  _toolBarItem = toolBarItem;
196  _isDark = false;
197 
198  button = [NSButton new];
199  [button setFrame:NSMakeRect(0,1,32,24)];
200 
201  [button setButtonType:NSMomentaryChangeButton];
202  [button setBordered:NO];
203 
204  [button setAction:@selector(itemIdentifier)];
205  [button setTarget:self];
206 
207  [[button cell] setImageScaling:NSImageScaleNone];
208  [[button cell] setShowsStateBy:NSContentsCellMask];
209 
210 
211  // Wrap the button in a fixed-width subview, so the toolbar item doesn't change width when the label changes.
212  [self setFrame:NSMakeRect(0,0,32,24)];
213  [self addSubview:button];
214 
215  return self;
216 }
217 
221 - (NSRect)bounds
222 {
223  // So the left side of the Zoom widget lines up with the left side of the canvas.
224  double width = 68;
225 
226 #ifdef VUO_PRO
227  VuoEditorEventsButton_Pro();
228 #endif
229 
230  NSRect frame = button.frame;
231  frame.origin.x = (width - frame.size.width) / 2.;
232  button.frame = frame;
233 
234  return NSMakeRect(0, 0, width, 24);
235 }
236 
240 - (NSString *)itemIdentifier
241 {
242  VuoEditorWindow *window = static_cast<VuoEditorWindow *>(_toolBarItem->parent()->parent());
243  window->getShowEventsAction()->trigger();
244 
245  return QString::number(qulonglong(_toolBarItem)).toNSString();
246 }
247 
251 - (void)setState:(bool)state
252 {
253  [button setState:state];
254 
255  NSImage *offImage = [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"showEvents" ofType:@"pdf"]];
256  if (state)
257  {
258  // Manage state manually, since we need to use NSMomentaryChangeButton to prevent the grey background when clicking.
259  NSImage *onImage = [offImage copy];
260  [onImage lockFocus];
261  [[NSColor colorWithCalibratedRed:29./255 green:106./255 blue:229./255 alpha:1] set]; // #1d6ae5
262  NSRectFillUsingOperation(NSMakeRect(0, 0, [onImage size].width, [onImage size].height), NSCompositeSourceAtop);
263  [onImage unlockFocus];
264  [button setImage:onImage];
265  [onImage release];
266  }
267  else
268  {
269  [offImage setTemplate:YES];
270  [button setImage:offImage];
271  }
272 
273  [offImage release];
274 }
275 
279 - (NSButton *)button
280 {
281  return button;
282 }
283 
287 - (void)drawRect:(NSRect)dirtyRect
288 {
289  NSColor *color = [NSColor colorWithCalibratedWhite:1 alpha:(_isDark ? .2 : 1)];
290  [color setFill];
291 
292  NSRect rect = [button frame];
293  rect.origin.y -= 1;
294  rect.size.height -= 1;
295  NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:rect xRadius:5.0 yRadius:5.0];
296  [path fill];
297 
298  [super drawRect:dirtyRect];
299 }
300 
304 - (void)updateColor:(bool)isDark
305 {
306  _isDark = isDark;
307  self.needsDisplay = YES;
308 }
309 @end
310 
317 NSRect windowWillPositionSheetUsingRect(id self, SEL _cmd, NSWindow *window, NSWindow *sheet, NSRect rect)
318 {
319  float titleAndToolbarHeight = window.frame.size.height - [window contentRectForFrameRect:window.frame].size.height;
320  rect.origin.y -= titleAndToolbarHeight;
321  return rect;
322 }
323 
329 @interface VuoEditorTitleCell : NSTextFieldCell
330 @end
331 @implementation VuoEditorTitleCell
332 - (NSRect)titleRectForBounds:(NSRect)frame
333 {
334  CGFloat stringHeight = self.attributedStringValue.size.height;
335  NSRect titleRect = [super titleRectForBounds:frame];
336  CGFloat oldOriginY = frame.origin.y;
337  titleRect.origin.y = frame.origin.y + (frame.size.height - stringHeight) / 2.0;
338  titleRect.size.height = titleRect.size.height - (titleRect.origin.y - oldOriginY);
339  return titleRect;
340 }
341 - (void)drawInteriorWithFrame:(NSRect)cFrame inView:(NSView*)cView
342 {
343  [super drawInteriorWithFrame:[self titleRectForBounds:cFrame] inView:cView];
344 }
345 @end
346 
350 VuoEditorWindowToolbar::VuoEditorWindowToolbar(QMainWindow *window, bool isCodeEditor)
351 {
352  NSView *nsView = (NSView *)window->winId();
353  nsWindow = [nsView window];
354 
355  activityIndicatorTimer = NULL;
356 
357  running = false;
358  buildInProgress = false;
359  buildPending = false;
360  stopInProgress = false;
361 
362  titleView = [NSTextField new];
363  titleView.cell = [VuoEditorTitleCell new];
364  titleView.font = [NSFont titleBarFontOfSize:0];
365  titleView.selectable = NO;
366  titleViewController = nil;
367 
368  toolBar = new QMacToolBar(window);
369  {
370  NSToolbar *tb = toolBar->nativeToolbar();
371 
372  // Workaround for apparent Qt bug, where QCocoaIntegration::clearToolbars() releases the toolbar after it's already been deallocated.
373  [tb retain];
374 
375  [tb setAllowsUserCustomization:NO];
376  }
377 
378 
379  {
380  runImage = [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"run" ofType:@"pdf"]];
381  [runImage setTemplate:YES];
382 
383  toolbarRunItem = toolBar->addItem(QIcon(), VuoEditor::tr("Run"));
384  NSToolbarItem *ti = toolbarRunItem->nativeToolBarItem();
385  [ti setImage:runImage];
386  [ti setAutovalidates:NO];
387  ti.toolTip = [NSString stringWithUTF8String:VuoEditor::tr("Compile and launch this composition.").toUtf8().data()];
388 
389  connect(toolbarRunItem, SIGNAL(activated()), window, SLOT(on_runComposition_triggered()));
390  }
391 
392 
393  {
394  stopImage = [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"stop" ofType:@"pdf"]];
395  [stopImage setTemplate:YES];
396 
397  toolbarStopItem = toolBar->addItem(QIcon(), VuoEditor::tr("Stop"));
398  NSToolbarItem *ti = toolbarStopItem->nativeToolBarItem();
399  [ti setImage:stopImage];
400  [ti setAutovalidates:NO];
401  ti.toolTip = [NSString stringWithUTF8String:VuoEditor::tr("Shut down this composition.").toUtf8().data()];
402 
403  connect(toolbarStopItem, SIGNAL(activated()), window, SLOT(on_stopComposition_triggered()));
404  }
405 
406 
407  if (isCodeEditor)
408  {
409  eventsButton = NULL;
410  toolbarEventsItem = NULL;
411  toolBar->addSeparator();
412  }
413  else
414  {
415  toolbarEventsItem = toolBar->addItem(QIcon(), VuoEditor::tr("Show Events"));
416  NSToolbarItem *ti = toolbarEventsItem->nativeToolBarItem();
417  eventsButton = [[VuoEditorEventsButton alloc] initWithQMacToolBarItem:toolbarEventsItem];
418  [ti setView:eventsButton];
419  [ti setMinSize:[eventsButton bounds].size];
420  ti.toolTip = [NSString stringWithUTF8String:VuoEditor::tr("Toggle whether the canvas shows event flow by highlighting trigger ports and nodes.").toUtf8().data()];
421  }
422 
423 
424  {
425  toolbarZoomItem = toolBar->addItem(QIcon(), VuoEditor::tr("Zoom"));
426  NSToolbarItem *ti = toolbarZoomItem->nativeToolBarItem();
427  zoomButtons = [[VuoEditorZoomButtons alloc] initWithQMacToolBarItem:toolbarZoomItem isCodeEditor:isCodeEditor];
428  [ti setView:zoomButtons];
429  [ti setMinSize:[zoomButtons bounds].size];
430  }
431 
432 
433  VuoEditor *editor = (VuoEditor *)qApp;
434  connect(editor, &VuoEditor::darkInterfaceToggled, this, &VuoEditorWindowToolbar::updateColor);
435  updateColor(editor->isInterfaceDark());
436 
437 #if VUO_PRO
438  VuoEditorWindowToolbar_Pro();
439 #endif
440 
441  toolBar->attachToWindow(window->windowHandle());
442 
443 
444  if (NSProcessInfo.processInfo.operatingSystemVersion.minorVersion == 13)
445  class_addMethod(nsWindow.delegate.class,
446  @selector(window:willPositionSheet:usingRect:),
448  "{CGRect={CGPoint=dd}{CGSize=dd}}@:@@{CGRect={CGPoint=dd}{CGSize=dd}}");
449 }
450 
455 {
456 #if VUO_PRO
457  VuoEditorWindowToolbarDestructor_Pro();
458 #endif
459 }
460 
464 void VuoEditorWindowToolbar::update(bool eventsShown, bool zoomedToActualSize, bool zoomedToFit)
465 {
466 #if VUO_PRO
467  update_Pro();
468 #endif
469 
470  if (!toolbarRunItem)
471  return;
472 
473  NSToolbarItem *runItem = toolbarRunItem->nativeToolBarItem();
474  NSToolbarItem *stopItem = toolbarStopItem->nativeToolBarItem();
475 
476  if (stopInProgress)
477  {
478  [runItem setEnabled:!buildPending];
479  [stopItem setEnabled:NO];
480  }
481  else if (buildInProgress)
482  {
483  [runItem setEnabled:NO];
484  [stopItem setEnabled:YES];
485  }
486  else if (running)
487  {
488  [runItem setEnabled:NO];
489  [stopItem setEnabled:YES];
490  }
491  else
492  {
493  [runItem setEnabled:YES];
494  [stopItem setEnabled:NO];
495  }
496 
497  if (toolbarEventsItem)
498  {
499  NSToolbarItem *eventsItem = toolbarEventsItem->nativeToolBarItem();
500  [eventsItem setLabel:(eventsShown
501  ? [NSString stringWithUTF8String:VuoEditor::tr("Hide Events").toUtf8().data()]
502  : [NSString stringWithUTF8String:VuoEditor::tr("Show Events").toUtf8().data()])];
503  VuoEditorEventsButton *eventsButton = (VuoEditorEventsButton *)[eventsItem view];
504  [eventsButton setState:eventsShown];
505  }
506 
507  // Enable/disable the zoom11 (actual size) segment.
508  [(NSSegmentedControl *)[toolbarZoomItem->nativeToolBarItem() view] setEnabled:!zoomedToActualSize forSegment:1];
509 
510  // Enable/disable the "Zoom to Fit" segment.
511  [(NSSegmentedControl *)[toolbarZoomItem->nativeToolBarItem() view] setEnabled:!zoomedToFit forSegment:2];
512 
513  updateActivityIndicators();
514 }
515 
521 {
522  buildPending = true;
523 }
524 
529 {
530  buildPending = false;
531  buildInProgress = true;
532 }
533 
538 {
539  buildInProgress = false;
540  running = true;
541 }
542 
547 {
548  stopInProgress = true;
549 }
550 
555 {
556  buildInProgress = false;
557  running = false;
558  stopInProgress = false;
559 }
560 
565 {
566  return buildPending;
567 }
568 
573 {
574  return buildInProgress;
575 }
576 
581 {
582  return running;
583 }
584 
589 {
590  return stopInProgress;
591 }
592 
598 {
599  return [NSScroller preferredScrollerStyle] == NSScrollerStyleOverlay;
600 }
601 
605 void VuoEditorWindowToolbar::updateActivityIndicators(void)
606 {
607  if (buildInProgress || stopInProgress)
608  {
609  if (! activityIndicatorTimer)
610  {
611  activityIndicatorFrame = 0;
612 
613  activityIndicatorTimer = new QTimer(this);
614  activityIndicatorTimer->setObjectName("VuoEditorWindowToolbar::activityIndicatorTimer");
615  connect(activityIndicatorTimer, SIGNAL(timeout()), this, SLOT(updateActivityIndicators()));
616  activityIndicatorTimer->start(250);
617  }
618  }
619  else
620  {
621  if (activityIndicatorTimer)
622  {
623  activityIndicatorTimer->stop();
624  delete activityIndicatorTimer;
625  activityIndicatorTimer = NULL;
626  }
627  }
628 
629  if (buildInProgress)
630  {
631  VuoActivityIndicator *iconEngine = new VuoActivityIndicator(activityIndicatorFrame++);
632  toolbarRunItem->setIcon(QIcon(iconEngine));
633  }
634  else
635  [(NSToolbarItem *)toolbarRunItem->nativeToolBarItem() setImage:runImage];
636 
637  if (stopInProgress)
638  {
639  VuoActivityIndicator *iconEngine = new VuoActivityIndicator(activityIndicatorFrame++);
640  toolbarStopItem->setIcon(QIcon(iconEngine));
641  }
642  else
643  [(NSToolbarItem *)toolbarStopItem->nativeToolBarItem() setImage:stopImage];
644 }
645 
649 void VuoEditorWindowToolbar::updateColor(bool isDark)
650 {
651  [(VuoEditorEventsButton *)eventsButton updateColor:isDark];
652  [(VuoEditorEventsButton *)zoomButtons updateColor:isDark];
653 
654  {
655  // Request a transparent titlebar (and toolbar) so the window's background color (set below) shows through.
656  // "Previously NSWindow would make the titlebar transparent for certain windows with NSWindowStyleMaskTexturedBackground set,
657  // even if titlebarAppearsTransparent was NO. When linking against the 10.13 SDK, textured windows must
658  // explicitly set titlebarAppearsTransparent to YES for this behavior."
659  // - https://developer.apple.com/library/content/releasenotes/AppKit/RN-AppKit/
660  objc_msgSend(nsWindow, sel_getUid("setTitlebarAppearsTransparent:"), true);
661 
662  // Requesting a transparent titlebar causes toolbar drawing to glitch (icons change color when defocused; text looks anemic).
663  // Enabling Core Animation seems to avoid that Cocoa bug.
664  ((NSView *)nsWindow.contentView).wantsLayer = YES;
665 
666  if (NSProcessInfo.processInfo.operatingSystemVersion.minorVersion >= 12)
667  {
668  // "A window with titlebarAppearsTransparent is normally opted-out of automatic window tabbing."
669  // - https://developer.apple.com/library/archive/releasenotes/AppKit/RN-AppKit/index.html
670  // …but it works fine for us, so reenable it.
671  // https://b33p.net/kosada/node/15412
672  objc_msgSend(nsWindow, sel_getUid("setTabbingMode:"), 0 /*NSWindowTabbingModeAutomatic*/);
673  objc_msgSend(nsWindow, sel_getUid("setTabbingIdentifier:"), @"Vuo Composition");
674  }
675  }
676 
677  if (isDark)
678  [nsWindow setBackgroundColor:[NSColor colorWithCalibratedWhite:.47 alpha:1]];
679  else
680  [nsWindow setBackgroundColor:[NSColor colorWithCalibratedWhite:.92 alpha:1]];
681 }