///********************************************************************************************************************************** /// GCWindowMenu.m /// GCDrawKitUI /// /// Created by graham on 27/03/07. /// Released under the Creative Commons license 2006 Apptree.net. /// /// /// This work is licensed under the Creative Commons Attribution-ShareAlike 2.5 License. /// To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/2.5/ or send a letter to /// Creative Commons, 543 Howard Street, 5th Floor, San Francisco, California, 94105, USA. /// ///********************************************************************************************************************************** #import "GCWindowMenu.h" #import "GCOneShotEffectTimer.h" #import "Logging.h" @interface GCWindowMenu (Private) + (void) popUpWindowMenu:(GCWindowMenu*) menu withEvent:(NSEvent*) event; + (void) popUpWindowMenu:(GCWindowMenu*) menu atPoint:(NSPoint) loc withEvent:(NSEvent*) event; - (void) trackWithEvent:(NSEvent*) event; - (NSEvent*) transmogrify:(NSEvent*) event; @end #define kGCDefaultWindowMenuSize (NSMakeRect(0, 0, 100, 28 )) #define kGCMenuContentInset 2 @implementation GCWindowMenu ///********************************************************************************************************************* /// /// method: popUpWindowMenu:withEvent:forView: /// scope: private class method /// overrides: /// description: pops up a custom popup menu, tracks it, then hides it again with a fadeout /// /// parameters: the custom popup window to display /// the event to start the display with (usually from a mouse down) /// result: none /// /// notes: the menu is positioned with its top, left point just to the left of, and slightly below, the /// point given in the event /// ///******************************************************************************************************************** + (void) popUpWindowMenu:(GCWindowMenu*) menu withEvent:(NSEvent*) event { NSPoint loc = [event locationInWindow]; loc.x -= 10; loc.y -= 5; [self popUpWindowMenu:menu atPoint:loc withEvent:event]; } ///********************************************************************************************************************* /// /// method: popUpWindowMenu:atPoint:withEvent:forView: /// scope: private class method /// overrides: /// description: pops up a custom popup menu, tracks it, then hides it again with a fadeout /// /// parameters: the custom popup window to display /// the location within the window at which to display the menu (top, left of menu) /// the event to start the display with (usually from a mouse down) /// result: none /// /// notes: /// ///******************************************************************************************************************** + (void) popUpWindowMenu:(GCWindowMenu*) menu atPoint:(NSPoint) loc withEvent:(NSEvent*) event { if ( menu == nil ) menu = [GCWindowMenu windowMenu]; NSRect rect = NSMakeRect(loc.x, loc.y, 0.0, 0.0); rect = [[event window] convertRectToScreen:rect]; loc = NSMakePoint(rect.origin.x, rect.origin.y); [menu setFrameTopLeftPoint:loc]; [[event window] addChildWindow:menu ordered:NSWindowAbove]; // show the "menu" [menu orderFront:self]; // track the menu (keeps control in its own event loop): [menu trackWithEvent:event]; // all done, tear down - remove with a fade effect [GCOneShotEffectTimer oneShotWithTime:0.15 forDelegate:menu]; } ///********************************************************************************************************************* /// /// method: windowMenu /// scope: public class method /// overrides: /// description: makes a window menu that can be popped up using the above methods. /// /// parameters: none /// result: a new poppable window menu /// /// notes: this method just makes an empy window with the default size. It's up to you to add some useful /// content before displaying it /// ///******************************************************************************************************************** + (GCWindowMenu*) windowMenu { GCWindowMenu* fi = [[GCWindowMenu alloc] initWithContentRect:NSZeroRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:YES]; // note - because windows are all sent a -close message at quit time, set it // not to be released at that time, otherwise the release from the autorelease pool // will cause a crash due to the stale reference [fi setReleasedWhenClosed:NO]; // **** important!! **** return fi; } ///********************************************************************************************************************* /// /// method: windowMenuWithContentView: /// scope: public class method /// overrides: /// description: makes a window menu that can be popped up using the above methods. /// /// parameters: the view to display within the menu /// result: a new poppable window menu containing the given view /// /// notes: the window is sized to fit the frame of the view you pass. /// ///******************************************************************************************************************** + (GCWindowMenu*) windowMenuWithContentView:(NSView*) view { GCWindowMenu* menu = [self windowMenu]; [menu setMainView:view sizeToFit:YES]; return menu; } ///********************************************************************************************************************* /// /// method: popUpAtPoint:withEvent:forView: /// scope: public instance method /// overrides: /// description: pops up a custom popup menu, tracks it, then hides it again with a fadeout /// /// parameters:

the location within the window at which to display the menu (top, left of menu) /// the event to start the display with (usually from a mouse down in some view) /// result: none /// /// notes: /// ///******************************************************************************************************************** - (void) popUpAtPoint:(NSPoint) p withEvent:(NSEvent*) event { [[self class] popUpWindowMenu:self atPoint:p withEvent:event]; } ///********************************************************************************************************************* /// /// method: popUpWithEvent:forView: /// scope: public instance method /// overrides: /// description: pops up a custom popup menu, tracks it, then hides it again with a fadeout /// /// parameters: the event to start the display with (usually from a mouse down in some view) /// result: none /// /// notes: /// ///******************************************************************************************************************** - (void) popUpWithEvent:(NSEvent*) event { [[self class] popUpWindowMenu:self withEvent:event]; } ///********************************************************************************************************************* /// /// method: initWithContentRect:styleMask:backing:defer: /// scope: public instance method /// overrides: NSWindow /// description: designated initializer. /// /// parameters: <> see NSWindow /// result: the window /// /// notes: /// ///******************************************************************************************************************** - (id) initWithContentRect:(NSRect) contentRect styleMask:(NSUInteger) styleMask backing:(NSBackingStoreType) bufferingType defer:(BOOL) deferCreation { if ((self = [super initWithContentRect:contentRect styleMask:styleMask backing:bufferingType defer:deferCreation]) != nil ) { [self setLevel:NSPopUpMenuWindowLevel]; [self setHasShadow:YES]; [self setAlphaValue:0.95]; [self setReleasedWhenClosed:YES]; [self setFrame:kGCDefaultWindowMenuSize display:NO]; _mainView = nil; _passFirstClick = YES; _oneShotTracking = YES; } return self; } ///********************************************************************************************************************* /// /// method: trackWithEvent: /// scope: public instance method /// overrides: /// description: track the mouse in the menu /// /// parameters: the initial starting event (will usually be a mouse down) /// result: none /// /// notes: tracking calls the main view's usual mouseDown/dragged/up methods, and tries to do so as compatibly /// as possible with the usual view behaviours. /// ///******************************************************************************************************************** - (void) trackWithEvent:(NSEvent*) event { // tracks the "menu" by keeping control until a mouse up (or down, if menu 'clicked' into being) DLog(@"starting tracking; initial event = %@", event); // [NSEvent startPeriodicEventsAfterDelay:1.0 withPeriod:0.1]; NSTimeInterval startTime = [event timestamp]; [self setAcceptsMouseMovedEvents:YES]; if ( _passFirstClick ) { [[self mainView] mouseDown:[self transmogrify:event]]; // the view may have trapped the mouse down and implemented its own tracking. // Standard NSControls do that for example. In that case we don't want to track // ourselves, so need to detect that and abort. if ([[self currentEvent] timestamp] - startTime > 0.25 ) return; } NSEvent* theEvent; BOOL keepOn = YES; unsigned int mask; BOOL invertedTracking = NO; mask = NSLeftMouseUpMask | NSLeftMouseDraggedMask | NSRightMouseUpMask | NSRightMouseDraggedMask | NSAppKitDefinedMask | NSFlagsChangedMask | NSScrollWheelMask | NSPeriodicMask; while (keepOn) { theEvent = [self transmogrify:[self nextEventMatchingMask:mask]]; switch ([theEvent type]) { case NSMouseMoved: [[self mainView] mouseMoved:theEvent]; break; case NSRightMouseDragged: case NSLeftMouseDragged: [[self mainView] mouseDragged:theEvent]; break; case NSRightMouseUp: case NSLeftMouseUp: // if this is within a very short time of the mousedown, leave the menu up but track it // using mouse moved and mouse down to end. if ([theEvent timestamp] - startTime < 0.25 || !_passFirstClick ) { invertedTracking = YES; mask |= ( NSLeftMouseDownMask | NSRightMouseDownMask | NSMouseMovedMask ); } else { [[self mainView] mouseUp:theEvent]; keepOn = NO; } break; case NSRightMouseDown: case NSLeftMouseDown: if ( ! NSPointInRect([theEvent locationInWindow], [[self mainView] frame])) keepOn = NO; else { [[self mainView] mouseDown:theEvent]; if ( _oneShotTracking ) keepOn = NO; } break; case NSPeriodic: break; case NSFlagsChanged: [[self mainView] flagsChanged:theEvent]; break; case NSAppKitDefined: if([theEvent subtype] == NSApplicationDeactivatedEventType ) keepOn = NO; break; case NSScrollWheel: [[self mainView] scrollWheel:theEvent]; break; default: /* Ignore any other kind of event. */ break; } } [self discardEventsMatchingMask:NSAnyEventMask beforeEvent:theEvent]; //[NSEvent stopPeriodicEvents]; DLog(@"tracking ended"); } ///********************************************************************************************************************* /// /// method: transmogrify: /// scope: private instance method /// overrides: /// description: convert the event to the local window if necessary /// /// parameters: an event /// result: the same event, or a modified version /// /// notes: ensures that events received while tracking are always targetted at the right window /// ///******************************************************************************************************************** - (NSEvent*) transmogrify:(NSEvent*) event { if(([event window] != self) && [event isMouseEventType]) { NSPoint pt = [event locationInWindow]; NSRect rect = NSMakeRect(pt.x, pt.y, 0.0, 0.0); rect = [[event window] convertRectToScreen:rect]; NSPoint glob = NSMakePoint(rect.origin.x, rect.origin.y); rect = NSMakeRect(glob.x, glob.y, 0.0, 0.0); rect = [self convertRectFromScreen:rect]; pt = NSMakePoint(rect.origin.x, rect.origin.y); return [NSEvent mouseEventWithType: [event type] location: pt modifierFlags: [event modifierFlags] timestamp: [event timestamp] windowNumber: [self windowNumber] context: [event context] eventNumber: [event eventNumber] clickCount: [event clickCount] pressure: [event pressure]]; } else return event; } ///********************************************************************************************************************* /// /// method: canBecomeMainWindow /// scope: public instance method /// overrides: NSWindow /// description: /// /// parameters: none /// result: return YES /// /// notes: /// ///******************************************************************************************************************** - (BOOL) canBecomeMainWindow { return NO; } ///********************************************************************************************************************* /// /// method: setMainView:sizeToFit: /// scope: public instance method /// overrides: /// description: sets the pop-up window's content to the given view, and optionally sizes the window to fit /// /// parameters: any view already created to be displayed in the menu /// if YES, window is sized to the view's frame. If NO, the window size is not changed /// result: none /// /// notes: main view is additionally retained so outlet from NIB may be directly passed in /// ///******************************************************************************************************************** - (void) setMainView:(NSView*) aView sizeToFit:(BOOL) stf { _mainView = aView; // add as a subview which retains it as well [[self contentView] addSubview:aView]; // if stf, position the view at top, left corner of the window and // make the window the size of the view if ( stf ) { NSRect fr = [self frameRectForContentRect:NSInsetRect( [aView frame], -kGCMenuContentInset, -kGCMenuContentInset )]; fr.origin = NSZeroPoint; [aView setFrameOrigin:NSMakePoint( kGCMenuContentInset, kGCMenuContentInset )]; [self setFrame:fr display:YES]; } [_mainView setNeedsDisplay:YES]; // if the view added is an NSControl, set first click to NO by default if ([aView isKindOfClass:[NSControl class]]) [self setMainViewWantsFirstClick:NO]; } ///********************************************************************************************************************* /// /// method: mainView /// scope: public instance method /// overrides: /// description: get the main view /// /// parameters: none /// result: the main view /// /// notes: /// ///******************************************************************************************************************** - (NSView*) mainView { return _mainView; } ///********************************************************************************************************************* /// /// method: setMainViewWantsFirstClick: /// scope: public instance method /// overrides: /// description: sets whether the main view should receive a mouse down on entry to the tracking loop /// /// parameters: YES to get the first click /// result: none /// /// notes: normally should be YES (the default). However views such as NSControl derivatives that implement /// their own tracking should set NO. If NO, the popup can only be operated by clicking to open, then /// clicking and dragging within - the continuous click to open, drag through and release operation /// wont work because the control doesn't get a mouse down to start with. /// ///******************************************************************************************************************** - (void) setMainViewWantsFirstClick:(BOOL) firstClick { _passFirstClick = firstClick; } ///********************************************************************************************************************* /// /// method: setShouldCloseWhenViewTrackingReturns: /// scope: public instance method /// overrides: /// description: sets whether popup should close or remain visible after main view completes its own tracking /// /// parameters: YES close on return from view tracking, NO to remain visble /// result: none /// /// notes: this affects tracking with views that implement their own tracking, such as NSControl. If YES, you /// get one shot at the control - after operating it, it will be hidden. If NO, the control may be /// changed as often as you want but you must manually click outside the menu to close it. /// ///******************************************************************************************************************** - (void) setShouldCloseWhenViewTrackingReturns:(BOOL) cmup { _oneShotTracking = cmup; } ///********************************************************************************************************************* /// /// method: oneShotHasReachedInverse: /// scope: public instance method /// overrides: NSObject (OneShotDelegate) /// description: callback from fade out effect /// /// parameters: goes from 1..0 /// result: none /// /// notes: /// ///******************************************************************************************************************** - (void) oneShotHasReachedInverse:(float) relpos { [self setAlphaValue:relpos]; } ///********************************************************************************************************************* /// /// method: oneShotComplete /// scope: public instance method /// overrides: NSObject (OneShotDelegate) /// description: callback from fade out effect /// /// parameters: none /// result: none /// /// notes: removes the window from screen - oneshot will then release it /// ///******************************************************************************************************************** - (void) oneShotComplete { [[self parentWindow] removeChildWindow:self]; [self orderOut:self]; } @end @implementation NSEvent (GCAdditions) ///********************************************************************************************************************* /// /// method: isMouseEventType: /// scope: public instance method /// overrides: /// description: checks event to see if it's any mouse event /// /// parameters: none /// result: YES if the event is a mouse event of any kind /// /// notes: /// ///******************************************************************************************************************** - (BOOL) isMouseEventType { // returns YES if type is any mouse type NSEventType t = [self type]; return ( t == NSLeftMouseDown || t == NSLeftMouseUp || t == NSRightMouseDown || t == NSRightMouseUp || t == NSLeftMouseDragged || t == NSRightMouseDragged || t == NSOtherMouseDown || t == NSOtherMouseUp || t == NSOtherMouseDragged ); } @end