/* GormMenuEditor.m * * Copyright (C) 2000 Free Software Foundation, Inc. * * Author: Richard Frith-Macdonald * Date: 2000 * * This file is part of GNUstep. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ #include "../../GormPrivate.h" @implementation NSMenu (GormObjectAdditions) - (NSString*) editorClassName { return @"GormMenuEditor"; } /* * Method to return the image that should be used to display menus within * the matrix containing the objects in a document. */ - (NSImage*) imageForViewer { static NSImage *image = nil; if (image == nil) { NSBundle *bundle = [NSBundle mainBundle]; NSString *path = [bundle pathForImageResource: @"GormMenu"]; image = [[NSImage alloc] initWithContentsOfFile: path]; } return image; } @end @interface NSButtonCell (GormObjectAdditions) - (NSRect) gormTitleRectForFrame: (NSRect) cellFrame inView: (NSView *)controlView; @end @interface GormMenuEditor : NSMenuView { id document; NSMenu *edited; id original; NSMenuView *rep; NSMutableArray *selection; id subeditor; BOOL isLinkSource; BOOL isClosed; NSPasteboard *dragPb; NSString *dragType; } - (BOOL) acceptsTypeFromArray: (NSArray*)types; - (BOOL) activate; - (id) initWithObject: (id)anObject inDocument: (id)aDocument; - (void) close; - (void) closeSubeditors; - (void) copySelection; - (void) deactivate; - (void) deleteSelection; - (id) document; - (void) draggedImage: (NSImage*)i endedAt: (NSPoint)p deposited: (BOOL)f; - (unsigned int) draggingSourceOperationMaskForLocal: (BOOL)flag; - (id) editedObject; - (void) makeSelectionVisible: (BOOL)flag; - (id) openSubeditorForObject: (id)anObject; - (void) orderFront; - (void) pasteInSelection; - (void) resetObject: (id)anObject; - (void) selectObjects: (NSArray*)objects; - (void) validateEditing; - (BOOL) wantsSelection; - (NSWindow*) window; @end @implementation GormMenuEditor - (BOOL) acceptsFirstMouse: (NSEvent*)theEvent { return YES; } - (void) encodeWithCoder: (NSCoder*)aCoder { [NSException raise: NSInternalInconsistencyException format: @"Argh - encoding menu editor"]; } /* * Intercepting events in the view and handling them */ - (NSView*) hitTest: (NSPoint)loc { /* * We grab all events in the window. */ if ([super hitTest: loc] != nil) { return self; } return nil; } - (BOOL) resignFirstResponder { return NO; } - (void) mouseDown: (NSEvent*)theEvent { NSPoint loc = [theEvent locationInWindow]; NSView *hit = [super hitTest: loc]; [[self window] becomeMainWindow]; [[self window] makeFirstResponder: self]; if (hit == rep) { int pos = [rep indexOfItemAtPoint: loc]; if (pos >= 0) { NSMenuItem *item = [edited itemAtIndex: pos]; if ([theEvent clickCount] == 2) { id cell; NSTextField *tf; NSRect frame; [self makeSelectionVisible: NO]; [self selectObjects: [NSArray array]]; cell = [rep menuItemCellForItemAtIndex: pos]; tf = [[NSTextField alloc] initWithFrame: [self bounds]]; frame = (NSRect)[cell titleRectForBounds: [rep rectOfItemAtIndex: pos]]; NSLog(@"cell %@ (%@)", cell, [cell stringValue]); frame.origin.y += 3; frame.size.height -= 5; frame.origin.x += 1; frame.size.width += 3; [tf setFrame: frame]; [tf setEditable: YES]; [tf setBezeled: NO]; [tf setBordered: NO]; // [tf setAlignment: [_EO alignment]]; // [tf setFont: [_EO font]]; [self addSubview: tf]; [tf setStringValue: [[cell menuItem] title]]; [self editTextField: tf withEvent: theEvent]; [[cell menuItem] setTitle: [tf stringValue]]; [tf removeFromSuperview]; RELEASE(tf); return; } [self makeSelectionVisible: NO]; if ([theEvent modifierFlags] & NSShiftKeyMask) { NSMutableArray *array; array = [NSMutableArray arrayWithArray: selection]; if ([array containsObject: item] == YES) { [array removeObject: item]; } else { [array addObject: item]; } [self selectObjects: array]; [self makeSelectionVisible: YES]; return; } [self selectObjects: [NSArray arrayWithObject: item]]; if ([theEvent modifierFlags] & NSControlKeyMask) { NSPoint dragPoint = [theEvent locationInWindow]; NSPasteboard *pb; NSString *name = [document nameForObject: item]; pb = [NSPasteboard pasteboardWithName: NSDragPboard]; [pb declareTypes: [NSArray arrayWithObject: GormLinkPboardType] owner: self]; [pb setString: name forType: GormLinkPboardType]; [NSApp displayConnectionBetween: item and: nil]; isLinkSource = YES; [self dragImage: [NSApp linkImage] at: dragPoint offset: NSZeroSize event: theEvent pasteboard: pb source: self slideBack: YES]; isLinkSource = NO; } else { NSDate *future = [NSDate distantFuture]; unsigned eventMask; NSEvent *e; NSEventType eType; BOOL acceptsMouseMoved; NSRect frame = [rep innerRect]; float maxMouse = NSMaxY(frame); float minMouse = NSMinY(frame); NSPoint lastPoint = loc; NSPoint point = loc; NSRect lastRect = [rep rectOfItemAtIndex: pos]; id cell = [rep menuItemCellForItemAtIndex: pos]; int newPos; eventMask = NSLeftMouseUpMask | NSLeftMouseDraggedMask | NSMouseMovedMask | NSPeriodicMask; [[self window] setAcceptsMouseMovedEvents: YES]; /* * Save window state info. */ acceptsMouseMoved = [[self window] acceptsMouseMovedEvents]; [rep lockFocus]; /* * Track mouse movements until left mouse up. * While we keep track of all mouse movements, * we only act on a movement when a periodic * event arives (every 20th of a second) * in order to avoid excessive amounts of drawing. */ [NSEvent startPeriodicEventsAfterDelay: 0.1 withPeriod: 0.05]; e = [NSApp nextEventMatchingMask: eventMask untilDate: future inMode: NSEventTrackingRunLoopMode dequeue: YES]; eType = [e type]; while (eType != NSLeftMouseUp) { if (eType != NSPeriodic) { point = [e locationInWindow]; } else if (NSEqualPoints(point, lastPoint) == NO) { /* * Limit mouse movement. */ point.x = NSMinX(frame); if (point.y < minMouse) point.y = minMouse; if (point.y > maxMouse) point.y = maxMouse; if (NSEqualPoints(point, lastPoint) == NO) { [[self window] disableFlushWindow]; /* * Redraw cells under area being changed. */ [rep drawRect: lastRect]; /* * Update location. */ lastRect.origin.y += point.y - lastPoint.y; lastPoint = point; /* * Draw highlighted item being moved. */ [cell highlight: YES withFrame: lastRect inView: rep]; [cell setHighlighted: NO]; /* * Flush any drawing performed for this event. */ [[self window] enableFlushWindow]; [[self window] flushWindow]; } } e = [NSApp nextEventMatchingMask: eventMask untilDate: future inMode: NSEventTrackingRunLoopMode dequeue: YES]; eType = [e type]; } [NSEvent stopPeriodicEvents]; [rep drawRect: lastRect]; [rep unlockFocus]; newPos = [rep indexOfItemAtPoint: point]; if (newPos < pos) { NSMenuItem *item = [edited itemAtIndex: pos]; RETAIN(item); if (newPos < 0) newPos = 0; [edited removeItemAtIndex: pos]; [edited insertItem: item atIndex: newPos]; RELEASE(item); } else if (newPos > pos) { NSMenuItem *item = [edited itemAtIndex: pos]; RETAIN(item); [edited removeItemAtIndex: pos]; [edited insertItem: item atIndex: newPos]; RELEASE(item); } [edited sizeToFit]; [edited display]; /* * Restore state to what it was on entry. */ [[self window] setAcceptsMouseMovedEvents: acceptsMouseMoved]; } [self makeSelectionVisible: YES]; } } else { /* * The mouse down wasn't over the menu items, so we just let the menu * handle it - but make sure the menu is selected in the editor first. */ [[document parentEditorForEditor: self] selectObjects: [NSArray arrayWithObject: edited]]; [hit mouseDown: theEvent]; } } - (BOOL) acceptsTypeFromArray: (NSArray*)types { /* * A menu editor can accept menu items pasted in to it. */ return [types containsObject: IBMenuPboardType]; } - (BOOL) activate { NSAssert(isClosed == NO, NSInternalInconsistencyException); if (original == nil) { NSWindow *w; NSEnumerator *enumerator; NSView *sub; NSMenuItem *item; /* * Swap ourselves in as a replacement for the original window * content view. */ w = [rep window]; original = RETAIN([w contentView]); [self setFrame: [original frame]]; enumerator = [[original subviews] objectEnumerator]; while ((sub = [enumerator nextObject]) != nil) { [self addSubview: sub]; } [w setContentView: self]; /* * Line up submenu with parent menu. */ item = [document parentOfObject: edited]; if (item != nil) { NSMenu *parent = [document parentOfObject: item]; NSRect frame = [[[parent menuRepresentation] window] frame]; NSPoint tl; tl = frame.origin; tl.x += frame.size.width; tl.y += frame.size.height; [edited sizeToFit]; [[[edited menuRepresentation] window] setFrameTopLeftPoint: tl]; } [edited display]; return NO; } return YES; } - (void) close { NSAssert(isClosed == NO, NSInternalInconsistencyException); isClosed = YES; [[NSNotificationCenter defaultCenter] removeObserver: self]; [self makeSelectionVisible: NO]; if ([(id)NSApp selectionOwner] == self) { [document resignSelectionForEditor: self]; } [self closeSubeditors]; [self deactivate]; [edited close]; [document editor: self didCloseForObject: edited]; } - (void) closeSubeditors { if (subeditor != nil) { [subeditor close]; DESTROY(subeditor); } } - (void) copySelection { if ([selection count] > 0) { [document copyObjects: selection type: IBMenuPboardType toPasteboard: [NSPasteboard generalPasteboard]]; } } - (void) deactivate { if (original != nil) { NSEnumerator *enumerator; NSView *sub; RETAIN(self); /* * Swap ourselves out and the original window content view in. */ [self makeSelectionVisible: NO]; [original setFrame: [self frame]]; [[rep window] setContentView: original]; enumerator = [[self subviews] objectEnumerator]; while ((sub = [enumerator nextObject]) != nil) { [original addSubview: sub]; } DESTROY(original); RELEASE(self); } } - (void) dealloc { if (isClosed == NO) { [self close]; } RELEASE(edited); RELEASE(selection); RELEASE(subeditor); RELEASE(document); [super dealloc]; } - (void) deleteSelection { if ([selection count] > 0) { NSArray *s = [NSArray arrayWithArray: selection]; NSEnumerator *e = [s objectEnumerator]; NSMenuItem *i; [self makeSelectionVisible: NO]; [self selectObjects: [NSArray array]]; while ((i = [e nextObject]) != nil) { [edited removeItem: i]; } [document detachObjects: s]; } } /* * Dragging source protocol implementation */ - (void) draggedImage: (NSImage*)i endedAt: (NSPoint)p deposited: (BOOL)f { /* * FIXME - handle this. * Notification that a drag failed/succeeded. */ } - (unsigned int) draggingSourceOperationMaskForLocal: (BOOL)flag { if (isLinkSource == YES) return NSDragOperationLink; else return NSDragOperationCopy; } - (unsigned) draggingEntered: (id)sender { NSArray *types; dragPb = [sender draggingPasteboard]; types = [dragPb types]; if ([types containsObject: IBMenuPboardType] == YES) { dragType = IBMenuPboardType; } else if ([types containsObject: GormLinkPboardType] == YES) { dragType = GormLinkPboardType; } else { dragType = nil; } return [self draggingUpdated: sender]; } - (unsigned) draggingUpdated: (id)sender { if (dragType == IBMenuPboardType) { return NSDragOperationCopy; } else if (dragType == GormLinkPboardType) { NSPoint loc = [sender draggingLocation]; int pos = [rep indexOfItemAtPoint: loc]; id item = nil; if (pos >= 0) { item = [edited itemAtIndex: pos]; } if (item == [NSApp connectSource]) { item = nil; } [NSApp displayConnectionBetween: [NSApp connectSource] and: item]; return NSDragOperationLink; } else { return 0; } } - (void) draggingExited: (id)sender { if (dragType == GormLinkPboardType) { [NSApp displayConnectionBetween: [NSApp connectSource] and: nil]; } } - (void) drawSelection { } - (id) document { return document; } - (id) editedObject { return edited; } - (id) initWithObject: (id)anObject inDocument: (id)aDocument { self = [super init]; ASSIGN(document, aDocument); ASSIGN(edited, anObject); selection = [NSMutableArray new]; rep = [edited menuRepresentation]; /* * Permit views and connections to be dragged in to the window. */ [self registerForDraggedTypes: [NSArray arrayWithObjects: IBMenuPboardType, GormLinkPboardType, nil]]; /* * Make sure that all our menu items are attached in the document. */ [document attachObjects: [edited itemArray] toParent: edited]; return self; } - (void) makeSelectionVisible: (BOOL)flag { if (flag == NO) { if ([selection count] > 0) { NSEnumerator *enumerator = [selection objectEnumerator]; NSMenuItem *item; [[self window] disableFlushWindow]; [rep lockFocus]; while ((item = [enumerator nextObject]) != nil) { int pos = [edited indexOfItem: item]; id cell = [rep menuItemCellForItemAtIndex: pos]; NSRect rect = [rep rectOfItemAtIndex: pos]; [cell highlight: NO withFrame: rect inView: rep]; } [rep unlockFocus]; [[self window] enableFlushWindow]; [[self window] flushWindowIfNeeded]; } } else { if ([selection count] > 0) { NSEnumerator *enumerator = [selection objectEnumerator]; NSMenuItem *item; [[self window] disableFlushWindow]; [rep lockFocus]; while ((item = [enumerator nextObject]) != nil) { int pos = [edited indexOfItem: item]; id cell = [rep menuItemCellForItemAtIndex: pos]; NSRect rect = [rep rectOfItemAtIndex: pos]; [cell highlight: YES withFrame: rect inView: rep]; } [rep unlockFocus]; [[self window] enableFlushWindow]; [[self window] flushWindowIfNeeded]; } } } - (id) openSubeditorForObject: (id)anObject { return nil; } - (void) orderFront { [[edited window] orderFront: self]; } - (void) pasteInSelection { NSPasteboard *pb = [NSPasteboard generalPasteboard]; NSArray *items; NSEnumerator *enumerator; NSMenuItem *item; /* * Ask the document to get the copied items from the pasteboard and add * them to it's collection of known objects. */ items = [document pasteType: IBMenuPboardType fromPasteboard: pb parent: edited]; enumerator = [items objectEnumerator]; while ((item = [enumerator nextObject]) != nil) { NSString *title = [item title]; if ([edited indexOfItemWithTitle: title] > 0) { [document detachObject: item]; /* Already exists */ } else { if ([_menu _ownedByPopUp]) { [item setOnStateImage: nil]; [item setMixedStateImage: nil]; } [edited addItem: item]; } } [edited sizeToFit]; [edited display]; } - (BOOL) performDragOperation: (id)sender { NSRect f = [rep frame]; if (dragType == IBMenuPboardType) { NSPoint loc = [sender draggedImageLocation]; NSArray *items; NSEnumerator *enumerator; NSMenuItem *item; int pos; /* * Adjust location so that it lies within horizontal bounds, and so that * it appears about half an item higher than it is. That way, we treat * a drop in the lower half of an item as an insertion below it, and a * drop in the upper half as an insertion above it. */ if (loc.x < NSMinX(f)) loc.x = NSMinX(f); if (loc.x > NSMaxX(f)) loc.x = NSMaxX(f); loc.y += 10; pos = [rep indexOfItemAtPoint: loc] + 1; [self makeSelectionVisible: NO]; /* * Ask the document to get the dragged views from the pasteboard and add * them to it's collection of known objects. */ items = [document pasteType: IBMenuPboardType fromPasteboard: dragPb parent: edited]; enumerator = [items objectEnumerator]; while ((item = [enumerator nextObject]) != nil) { // NSString *title = [item title]; // if ([edited indexOfItemWithTitle: title] > 0) // { // [document detachObject: item]; /* Already exists */ // } // else // { if ([edited _ownedByPopUp]) { NSLog(@"owned by popup"); [item setOnStateImage: nil]; [item setMixedStateImage: nil]; } else NSLog(@"not owned by popup"); [edited insertItem: item atIndex: pos++]; // } } [edited sizeToFit]; [edited display]; [self selectObjects: items]; [self makeSelectionVisible: YES]; } else if (dragType == GormLinkPboardType) { NSPoint loc = [sender draggingLocation]; int pos = [rep indexOfItemAtPoint: loc]; NSLog(@"Link at index: %d (%@)", pos, NSStringFromPoint(loc)); if (pos >= 0) { id item = [edited itemAtIndex: pos]; [NSApp displayConnectionBetween: [NSApp connectSource] and: item]; [NSApp startConnecting]; } } else { NSLog(@"Drop with unrecognized type (%@)!", dragType); dragType = nil; return NO; } dragType = nil; return YES; } - (BOOL) prepareForDragOperation: (id)sender { /* * Tell the source that we will accept the drop if we can. */ if (dragType == IBMenuPboardType) { /* * We can accept menus dropped anywhere. */ return YES; } else if (dragType == GormLinkPboardType) { /* * We can accept a link dropped on any of our items. */ return YES; } return NO; } /* * Return the rectangle in which an objects link status will be displayed. */ - (NSRect) rectForObject: (id)anObject { int pos = [edited indexOfItem: anObject]; NSRect rect; if (pos >= 0) { rect = [rep rectOfItemAtIndex: pos]; rect = [rep convertRect: rect toView: nil]; } else { rect = [self frame]; } return rect; } - (void) resetObject: (id)anObject { [[self window] makeKeyAndOrderFront: self]; } - (void) selectObjects: (NSArray*)anArray { if ([anArray isEqual: selection] == NO) { unsigned count; NSMenuItem *item; [selection removeAllObjects]; NSLog(@"selectObjects %@ %@", selection, anArray); [selection addObjectsFromArray: anArray]; count = [selection count]; /* * We can only select items in our menu - discard others. */ while (count-- > 0) { id o = [selection objectAtIndex: count]; if ([edited indexOfItem: o] == NSNotFound) { [selection removeObjectAtIndex: count]; } } item = [selection lastObject]; if ([selection count] != 1 || [item hasSubmenu] == NO) { [self closeSubeditors]; } else { NSMenu *menu; id editor; /* * A single item with a submenu is selected - * Make sure the submenu is registered in the document and * open an editor for it Close any existing subeditor. */ menu = [item submenu]; if ([document containsObject: menu] == NO) { [document attachObject: menu toParent: item]; } editor = [document editorForObject: menu create: YES]; if (subeditor != nil && subeditor != editor) { [self closeSubeditors]; } [editor orderFront]; [editor activate]; ASSIGN(subeditor, editor); } } /* * Now we must let the document (and hence the rest of the app) know * about our new selection. If there is nothing in it, make sure * that our edited window is selected instead. */ if ([selection count] > 0) { [document setSelectionFromEditor: self]; } else { GormObjectEditor *ed; ed = [GormObjectEditor editorForDocument: document]; [ed selectObjects: [NSArray arrayWithObject: edited]]; } } - (NSArray*) selection { return [NSArray arrayWithArray: selection]; } - (unsigned) selectionCount { return [selection count]; } - (void) validateEditing { } - (BOOL) wantsSelection { /* * We only want to be the selection owner if we are active (have been * swapped for the windows original content view) and if we have some * object selected. */ if (original == nil) return NO; if ([selection count] == 0) return NO; return YES; } - (NSWindow*) window { return [super window]; } @end static BOOL done_editing; @implementation GormMenuEditor (EditingAdditions) - (void) handleNotification: (NSNotification*)aNotification { NSString *name = [aNotification name]; if ([name isEqual: NSControlTextDidEndEditingNotification] == YES) { done_editing = YES; } } /* Edit a textfield. If it's not already editable, make it so, then edit it */ - (NSEvent *) editTextField: view withEvent: (NSEvent *)theEvent { unsigned eventMask; BOOL wasEditable; BOOL didDrawBackground; NSTextField *editField; NSRect frame; NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; NSDate *future = [NSDate distantFuture]; NSEvent *e; editField = view; frame = [editField frame]; wasEditable = [editField isEditable]; [editField setEditable: YES]; didDrawBackground = [editField drawsBackground]; [editField setDrawsBackground: YES]; // [editField display]; [nc addObserver: self selector: @selector(handleNotification:) name: NSControlTextDidEndEditingNotification object: nil]; /* Do some modal editing */ [editField selectText: self]; eventMask = NSLeftMouseDownMask | NSLeftMouseUpMask | NSKeyDownMask | NSKeyUpMask | NSFlagsChangedMask; done_editing = NO; while (!done_editing) { NSEventType eType; e = [NSApp nextEventMatchingMask: eventMask untilDate: future inMode: NSEventTrackingRunLoopMode dequeue: YES]; eType = [e type]; switch (eType) { case NSLeftMouseDown: { NSPoint dp = [self convertPoint: [e locationInWindow] fromView: nil]; if (NSMouseInRect(dp, frame, NO) == NO) { done_editing = YES; break; } } [[editField currentEditor] mouseDown: e]; break; case NSLeftMouseUp: [[editField currentEditor] mouseUp: e]; break; case NSLeftMouseDragged: [[editField currentEditor] mouseDragged: e]; break; case NSKeyDown: [[editField currentEditor] keyDown: e]; break; case NSKeyUp: [[editField currentEditor] keyUp: e]; break; case NSFlagsChanged: [[editField currentEditor] flagsChanged: e]; break; default: NSLog(@"Internal Error: Unhandled event during editing: %@", e); break; } } [editField setEditable: wasEditable]; [editField setDrawsBackground: didDrawBackground]; [nc removeObserver: self name: NSControlTextDidEndEditingNotification object: nil]; [[editField currentEditor] resignFirstResponder]; [self setNeedsDisplay: YES]; return e; } @end