/*
GSToolbarView.m
The toolbar view class.
Copyright (C) 2004-2020 Free Software Foundation, Inc.
Author: Quentin Mathe
Date: January 2004
This file is part of the GNUstep GUI Library.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library 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
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; see the file COPYING.LIB.
If not, see or write to the
Free Software Foundation, 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#import
#import
#import
#import
#import
#import
#import
#import
#import "AppKit/NSButton.h"
#import "AppKit/NSClipView.h"
#import "AppKit/NSDragging.h"
#import "AppKit/NSEvent.h"
#import "AppKit/NSImage.h"
#import "AppKit/NSMenu.h"
#import "AppKit/NSPasteboard.h"
// It contains GSMovableToolbarItemPboardType declaration
#import "AppKit/NSToolbarItem.h"
#import "AppKit/NSView.h"
#import "AppKit/NSWindow.h"
#import "GNUstepGUI/GSTheme.h"
#import "GNUstepGUI/GSToolbarView.h"
#import "NSToolbarFrameworkPrivate.h"
typedef enum {
ToolbarViewDefaultHeight = 62,
ToolbarViewRegularHeight = 62,
ToolbarViewSmallHeight = 52
} ToolbarViewHeight;
// Borrow this from NSToolbarItem.m
static const int InsetItemViewX = 10;
static const int ClippedItemsViewWidth = 28;
static NSUInteger draggedItemIndex = NSNotFound;
/*
* Toolbar related code
*/
@interface GSToolbarButton
- (NSToolbarItem *) toolbarItem;
@end
@interface GSToolbarBackView
- (NSToolbarItem *) toolbarItem;
@end
@interface GSToolbarClippedItemsButton : NSButton
{
NSToolbar *_toolbar;
}
- (id) init;
// Accessors
- (NSMenu *) overflowMenu;
/* This method cannot be called "menu" otherwise it would override NSResponder
method with the same name. */
- (void) layout;
- (void) setToolbar: (NSToolbar *)toolbar;
@end
@implementation GSToolbarClippedItemsButton
- (id) init
{
NSImage *image = [NSImage imageNamed: @"common_ToolbarClippedItemsMark"];
NSRect dummyRect = NSMakeRect(0, 0, ClippedItemsViewWidth, 100);
// The correct height will be set by the layout method
if ((self = [super initWithFrame: dummyRect]) != nil)
{
[self setBordered: NO];
[[self cell] setHighlightsBy: NSChangeGrayCellMask
| NSChangeBackgroundCellMask];
[self setAutoresizingMask: NSViewNotSizable];
[self setImagePosition: NSImageOnly];
[image setScalesWhenResized: YES];
// [image setSize: NSMakeSize(20, 20)];
[self setImage: image];
return self;
}
return nil;
}
/*
* Not really used, it is here to be used by the developer who want to adjust
* easily a toolbar view attached to a toolbar which is not bind to a window.
*/
- (void) layout
{
NSSize layoutSize = NSMakeSize([self frame].size.width,
[[_toolbar _toolbarView] _heightFromLayout]);
[self setFrameSize: layoutSize];
}
- (void) mouseDown: (NSEvent *)event
{
NSMenu *clippedItemsMenu = [self menuForEvent: event];
[super highlight: YES];
if (clippedItemsMenu != nil)
{
[NSMenu popUpContextMenu: clippedItemsMenu withEvent: event
forView: self];
}
[super highlight: NO];
}
- (NSMenu *) menuForEvent: (NSEvent *)event
{
if ([event type] == NSLeftMouseDown)
{
return [self overflowMenu];
}
return nil;
}
- (NSMenu *) overflowMenu
{
/* This method cannot be called "menu" otherwise it would
override NSResponder method with the same name. */
NSMenu *menu = [[NSMenu alloc] initWithTitle: @""];
NSEnumerator *e;
id item;
NSArray *visibleItems;
visibleItems = [_toolbar visibleItems];
e = [[_toolbar items] objectEnumerator];
while ((item = [e nextObject]) != nil)
{
if (![visibleItems containsObject: item])
{
id menuItem;
menuItem = [item menuFormRepresentation];
if (menuItem == nil)
menuItem = [item _defaultMenuFormRepresentation];
if (menuItem != nil)
{
[item validate];
[menu addItem: menuItem];
}
}
}
return AUTORELEASE(menu);
}
// Accessors
- (void) setToolbar: (NSToolbar *)toolbar
{
// Don't do an ASSIGN here, the toolbar view retains us.
_toolbar = toolbar;
}
@end
// ---
// Implementation GSToolbarView
@implementation GSToolbarView
+ (void) initialize
{
if (self == [GSToolbarView class])
{
}
}
- (id) initWithFrame: (NSRect)frame
{
if ((self = [super initWithFrame: frame]) == nil)
{
return nil;
}
_heightFromLayout = ToolbarViewDefaultHeight;
[self setFrame: NSMakeRect(frame.origin.x, frame.origin.y,
frame.size.width, _heightFromLayout)];
_clipView = [[NSClipView alloc] initWithFrame:
NSMakeRect(0, 0, frame.size.width,
_heightFromLayout)];
[_clipView setAutoresizingMask: (NSViewWidthSizable | NSViewHeightSizable)];
[_clipView setDrawsBackground: NO];
[self addSubview: _clipView];
// Adjust the clip view frame
[self setBorderMask: GSToolbarViewTopBorder | GSToolbarViewBottomBorder
| GSToolbarViewRightBorder | GSToolbarViewLeftBorder];
_clippedItemsMark = [[GSToolbarClippedItemsButton alloc] init];
[self registerForDraggedTypes:
[NSArray arrayWithObject: GSMovableToolbarItemPboardType]];
return self;
}
- (void) dealloc
{
//NSLog(@"Toolbar view dealloc");
[[NSNotificationCenter defaultCenter] removeObserver: self];
RELEASE(_clippedItemsMark);
RELEASE(_clipView);
[super dealloc];
}
// Dragging related methods
+ (NSUInteger) draggedItemIndex
{
return draggedItemIndex;
}
+ (void) setDraggedItemIndex:(NSUInteger)sourceIndex
{
draggedItemIndex = sourceIndex;
}
- (int) _insertionIndexAtPoint: (NSPoint)location
{
NSUInteger index;
NSArray *visibleBackViews = [self _visibleBackViews];
location = [_clipView convertPoint:location fromView:nil];
if (draggedItemIndex == NSNotFound)
{
//simply locate the nearest location between existing items
for (index = 0; index < [visibleBackViews count]; index++)
{
NSRect itemRect = [[visibleBackViews objectAtIndex:index] frame];
if (location.x < (itemRect.origin.x + (itemRect.size.width/2)))
{
NSLog(@"At location %lu", (unsigned long)index);
return index;
}
}
return [visibleBackViews count];
}
else
{
// don't return a different index unless drag has crossed the midpoint of its neighbor
NSRect itemRect;
BOOL draggingLeft = YES;
if (draggedItemIndex < [visibleBackViews count])
{
itemRect = [[visibleBackViews objectAtIndex:draggedItemIndex] frame];
draggingLeft = (location.x < (itemRect.origin.x + (itemRect.size.width/2)));
}
if (draggingLeft)
{
// dragging to the left of dragged item's current location
for (index=0; index < draggedItemIndex && index < [visibleBackViews count]; index++)
{
itemRect = [[visibleBackViews objectAtIndex:index] frame];
if (location.x < (itemRect.origin.x + (itemRect.size.width/2)))
{
return index;
}
}
}
else
{
// dragging to the right of current location
// Never called for [visibleBackViews count] == 0
for (index=[visibleBackViews count]-1; index > draggedItemIndex; index--)
{
itemRect = [[visibleBackViews objectAtIndex:index] frame];
if (location.x > (itemRect.origin.x + (itemRect.size.width/2)))
{
return index;
}
}
}
return draggedItemIndex;
}
}
#define OUTSIDE_INDEX (NSNotFound - 1)
- (NSDragOperation) updateItemWhileDragging:(id )info exited:(BOOL)exited
{
NSToolbarItem *item = [[info draggingSource] toolbarItem];
NSString *identifier = [item itemIdentifier];
NSToolbar *toolbar = [self toolbar];
NSArray *allowedItemIdentifiers = [toolbar _allowedItemIdentifiers];
int newIndex;
// don't accept any dragging if the customization palette isn't running for this toolbar
if (![toolbar customizationPaletteIsRunning] || ![allowedItemIdentifiers containsObject: identifier])
{
return NSDragOperationNone;
}
if (draggedItemIndex == NSNotFound) // initialize the index for this drag session
{
// if duplicate items aren't allowed, see if we already have such an item
if (![item allowsDuplicatesInToolbar])
{
NSArray *items = [toolbar items];
NSUInteger index;
for (index=0; index<[items count]; index++)
{
NSToolbarItem *anItem = [items objectAtIndex:index];
if ([[anItem itemIdentifier] isEqual:identifier])
{
draggedItemIndex = index; // drag the existing item
break;
}
}
}
}
else if (draggedItemIndex == OUTSIDE_INDEX)
{
// re-entering after being dragged off -- treat as unknown location
draggedItemIndex = NSNotFound;
}
newIndex = [self _insertionIndexAtPoint: [info draggingLocation]];
if (draggedItemIndex != NSNotFound)
{
// existing item being dragged -- either move or remove it
if (exited)
{
[toolbar _removeItemAtIndex:draggedItemIndex broadcast:YES];
draggedItemIndex = OUTSIDE_INDEX; // no longer in our items
}
else
{
if (newIndex != draggedItemIndex)
{
[toolbar _moveItemFromIndex: draggedItemIndex toIndex: newIndex broadcast: YES];
draggedItemIndex = newIndex;
}
}
}
else if (!exited)
{
// new item being dragged in -- add it
[toolbar _insertItemWithItemIdentifier: identifier
atIndex: newIndex
broadcast: YES];
draggedItemIndex = newIndex;
}
return NSDragOperationGeneric;
}
- (NSDragOperation) draggingEntered: (id )info
{
return [self updateItemWhileDragging: info exited: NO];
}
- (NSDragOperation) draggingUpdated: (id )info
{
return [self updateItemWhileDragging: info exited: NO];
}
- (void) draggingEnded: (id )info
{
draggedItemIndex = NSNotFound;
}
- (void) draggingExited: (id )info
{
[self updateItemWhileDragging: info exited: YES];
}
- (BOOL) prepareForDragOperation: (id )info
{
return YES;
}
- (BOOL) performDragOperation: (id )info
{
NSToolbar *toolbar = [self toolbar];
[self updateItemWhileDragging: info exited: NO];
draggedItemIndex = NSNotFound;
// save the configuration...
[toolbar _saveConfig];
return YES;
}
- (void) concludeDragOperation: (id )info
{
// Nothing to do currently
}
// More overrided methods
- (void) drawRect: (NSRect)aRect
{
[[GSTheme theme] drawToolbarRect: aRect
frame: [self frame]
borderMask: _borderMask];
}
- (BOOL) isOpaque
{
if ([[[GSTheme theme] toolbarBackgroundColor] alphaComponent] < 1.0)
{
return NO;
}
else
{
return YES;
}
}
- (void) windowDidResize: (NSNotification *)notification
{
if ([self superview] == nil)
return;
[self _reload];
}
- (void) viewWillMoveToSuperview: (NSView *)newSuperview
{
[super viewWillMoveToSuperview: newSuperview];
[_toolbar _toolbarViewWillMoveToSuperview: newSuperview];
// Allow to update the validation system which is window specific
}
- (void) viewDidMoveToWindow
{
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
/* NSView method called when a view is moved to a window (NSView has a
variable _window). */
[super viewDidMoveToWindow];
[nc removeObserver: self name: NSWindowDidResizeNotification object: nil];
[nc addObserver: self selector: @selector(windowDidResize:)
name: NSWindowDidResizeNotification
object: _window];
}
// Accessors
- (unsigned int) borderMask
{
return _borderMask;
}
- (NSToolbar *) toolbar
{
return _toolbar;
}
- (void) setBorderMask: (unsigned int)borderMask
{
NSRect toolbarViewFrame = [self frame];
NSRect rect = NSMakeRect(0, 0, toolbarViewFrame.size.width,
toolbarViewFrame.size.height);
_borderMask = borderMask;
// Take in account the border
if (_borderMask & GSToolbarViewBottomBorder)
{
rect = NSMakeRect(rect.origin.x, ++rect.origin.y, rect.size.width,
--rect.size.height);
}
if (_borderMask & GSToolbarViewTopBorder)
{
rect = NSMakeRect(rect.origin.x, rect.origin.y, rect.size.width,
--rect.size.height);
}
if (_borderMask & GSToolbarViewLeftBorder)
{
rect = NSMakeRect(++rect.origin.x, rect.origin.y, --rect.size.width,
rect.size.height);
}
if (_borderMask & GSToolbarViewRightBorder)
{
rect = NSMakeRect(rect.origin.x, rect.origin.y, --rect.size.width,
rect.size.height);
}
[_clipView setFrame: rect];
}
- (void) setToolbar: (NSToolbar *)toolbar
{
if (_toolbar == toolbar)
return;
_toolbar = toolbar;
[_clippedItemsMark setToolbar: _toolbar];
// Load the toolbar in the toolbar view
[self _reload];
}
// Private methods
- (void) _handleBackViewsFrame
{
CGFloat x = 0;
CGFloat newHeight = 0;
NSArray *subviews = [_clipView subviews];
NSEnumerator *e = [[_toolbar items] objectEnumerator];
NSToolbarItem *item;
while ((item = [e nextObject]) != nil)
{
NSView *itemBackView;
NSRect itemBackViewFrame;
itemBackView = [item _backView];
if ([subviews containsObject: itemBackView] == NO
|| [item _isModified]
|| [item _isFlexibleSpace])
{
// When a label is changed, _isModified returns YES to let us known we
// must recalculate the text length and then the size for the edited
// item back view
[item _layout];
}
itemBackViewFrame = [itemBackView frame];
[itemBackView setFrame: NSMakeRect(x, itemBackViewFrame.origin.y,
itemBackViewFrame.size.width, itemBackViewFrame.size.height)];
x += [itemBackView frame].size.width;
if (itemBackViewFrame.size.height > newHeight)
newHeight = itemBackViewFrame.size.height;
}
if (newHeight > 0)
_heightFromLayout = newHeight;
}
- (void) _takeInAccountFlexibleSpaces
{
NSArray *items = [_toolbar items];
NSEnumerator *e;
NSToolbarItem *item;
NSView *backView, *view;
CGFloat lengthAvailable;
BOOL mustAdjustNext = NO;
CGFloat x = 0, visibleItemsMinWidth = 0, backViewsWidth = 0;
NSMutableArray *variableWidthItems = [NSMutableArray array];
unsigned flexibleItemsCount = 0, maxWidthItemsCount = 0;
CGFloat spacePerFlexItem, extraSpace = 0;
CGFloat toolbarWidth = [self frame].size.width;
NSUInteger i, n;
NSMutableArray *visibleItems = [NSMutableArray array];
static const int FlexItemWeight = 4; // non-space flexible item counts as much as 4 flexible spaces
n = [items count];
if (n == 0)
return;
// First determine which items can fit in toolbar if all are at their minimum width.
// We'd like to show as many items as possible. These are our visibleItems.
for (i=0; i < n; i++)
{
item = [items objectAtIndex:i];
backView = [item _backView];
view = [item view];
if (view != nil)
backViewsWidth += [item minSize].width + 2*InsetItemViewX;
else
backViewsWidth += [backView frame].size.width;
if ((backViewsWidth + ClippedItemsViewWidth <= toolbarWidth)
|| (i == n - 1 && backViewsWidth <= toolbarWidth))
{
visibleItemsMinWidth = backViewsWidth;
[visibleItems addObject:item];
}
else
{
break;
}
}
// next, figure out how much additional space there is for expanding flexible items
lengthAvailable = toolbarWidth - visibleItemsMinWidth;
if ([visibleItems count] < n)
lengthAvailable -= ClippedItemsViewWidth;
if (lengthAvailable < 1)
return;
// We want to divide available space evenly among all flexible items, but some items may
// reach their maximum width, making more space available for the other items.
// To do this, first we count the flexible items, gathering a list of those that may
// have a maximum width.
// To match observed behavior on Cocoa (which is NOT as documented!) we allocate only 1/4
// as much space to flexible spaces as we do to other flexible items.
e = [visibleItems objectEnumerator];
while ((item = [e nextObject]) != nil)
{
if ([item _isFlexibleSpace])
{
flexibleItemsCount++;
}
else
{
CGFloat minWidth = [item minSize].width;
CGFloat maxWidth = [item maxSize].width;
if (minWidth < maxWidth)
{
[variableWidthItems addObject:item];
flexibleItemsCount += FlexItemWeight; // gets FlexItemWeight times the weight of a flexible space
}
}
}
if (flexibleItemsCount == 0)
return;
// Now go through any variableWidthItems to see if the available space per item would
// cause any of them to exceed their maximum width, and calculate the extra space available
spacePerFlexItem = MAX(lengthAvailable / flexibleItemsCount, 0);
e = [variableWidthItems objectEnumerator];
while ((item = [e nextObject]) != nil)
{
CGFloat minWidth = [item minSize].width;
CGFloat maxWidth = [item maxSize].width;
if (maxWidth-minWidth < spacePerFlexItem * FlexItemWeight)
{
extraSpace += spacePerFlexItem * FlexItemWeight - (maxWidth-minWidth); // give back unneeded space
maxWidthItemsCount += FlexItemWeight;
}
}
// Recalculate spacePerFlexItem (unless all flexible items are going to their max width)
if (flexibleItemsCount > maxWidthItemsCount)
spacePerFlexItem += extraSpace / (flexibleItemsCount-maxWidthItemsCount);
// Finally, go through all items, adjusting their width and positioning them as needed
e = [items objectEnumerator];
while ((item = [e nextObject]) != nil)
{
backView = [item _backView];
if ([item _isFlexibleSpace])
{
NSRect backViewFrame = [backView frame];
NSRect newFrameRect = NSMakeRect(x, backViewFrame.origin.y,
spacePerFlexItem,
backViewFrame.size.height);
[backView setFrame: [self centerScanRect:newFrameRect]];
mustAdjustNext = YES;
}
else if ([variableWidthItems indexOfObjectIdenticalTo:item] != NSNotFound)
{
NSRect backViewFrame = [backView frame];
CGFloat maxFlex = [item maxSize].width - [item minSize].width;
CGFloat flexAmount = MIN(maxFlex, spacePerFlexItem * FlexItemWeight);
CGFloat newWidth = [item minSize].width + flexAmount + 2 * InsetItemViewX;
NSRect newFrameRect = NSMakeRect(x, backViewFrame.origin.y,
newWidth,
backViewFrame.size.height);
[backView setFrame: [self centerScanRect: newFrameRect]];
mustAdjustNext = YES;
}
else if (mustAdjustNext)
{
NSRect backViewFrame = [backView frame];
NSRect newFrameRect = NSMakeRect(x, backViewFrame.origin.y,
backViewFrame.size.width,
backViewFrame.size.height);
[backView setFrame: [self centerScanRect: newFrameRect]];
}
view = [item view];
if (view != nil)
{
NSRect viewFrame = [view frame];
// Subtract InsetItemViewX
viewFrame.size.width = [backView frame].size.width - 2 * InsetItemViewX;
viewFrame.origin.x = InsetItemViewX;
[view setFrame: viewFrame];
}
x += [backView frame].size.width;
}
}
- (void) _handleViewsVisibility
{
NSArray *backViews;
NSArray *subviews;
NSEnumerator *e;
NSView *backView;
/* The back views which are associated with each toolbar item (the toolbar
items doesn't reflect the toolbar view content) */
backViews = [[_toolbar items] valueForKey: @"_backView"];
// We remove each back view associated with a removed toolbar item
e = [[_clipView subviews] objectEnumerator];
while ((backView = [e nextObject]) != nil)
{
if ([backViews containsObject: backView] == NO)
{
if ([backView superview] != nil)
[backView removeFromSuperview];
}
}
// We add each backView associated with an added toolbar item
subviews = [_clipView subviews];
e = [backViews objectEnumerator];
while ((backView = [e nextObject]) != nil)
{
if ([subviews containsObject: backView] == NO)
{
[_clipView addSubview: backView];
}
}
}
- (void) _manageClipView
{
NSRect clipViewFrame = [_clipView frame];
NSUInteger count = [[_toolbar items] count];
// Retrieve the back views which should be visible now that the resize
// process has been taken in account
NSArray *visibleBackViews = [self _visibleBackViews];
if ([visibleBackViews count] < count)
{
NSView *lastVisibleBackView = [visibleBackViews lastObject];
float width = 0;
// Resize the clip view
if (lastVisibleBackView != nil)
width = NSMaxX([lastVisibleBackView frame]);
[_clipView setFrame: NSMakeRect(clipViewFrame.origin.x,
clipViewFrame.origin.y,
width,
clipViewFrame.size.height)];
// Adjust the clipped items mark frame handling
[_clippedItemsMark layout];
// We get the new _clipView frame
clipViewFrame = [_clipView frame];
[_clippedItemsMark setFrameOrigin: NSMakePoint(
[self frame].size.width - ClippedItemsViewWidth, clipViewFrame.origin.y)];
if ([_clippedItemsMark superview] == nil)
[self addSubview: _clippedItemsMark];
}
else if (([_clippedItemsMark superview] != nil)
&& ([visibleBackViews count] == count))
{
[_clippedItemsMark removeFromSuperview];
[_clipView setFrame: NSMakeRect(clipViewFrame.origin.x,
clipViewFrame.origin.y,
[self frame].size.width,
clipViewFrame.size.height)];
}
}
- (void) _reload
{
// First, we resize
[self _handleBackViewsFrame];
[self _takeInAccountFlexibleSpaces];
[self _handleViewsVisibility];
/* We manage the clipped items view in the case it should become visible or
invisible */
[self _manageClipView];
[self setNeedsDisplay: YES];
}
// Accessors private methods
- (CGFloat) _heightFromLayout
{
CGFloat height = _heightFromLayout;
if (_borderMask & GSToolbarViewBottomBorder)
{
height++;
}
if (_borderMask & GSToolbarViewTopBorder)
{
height++;
}
return height;
}
/*
* Will return the visible (not clipped) back views in the toolbar view even
* when the toolbar is not visible.
* May be should be renamed _notClippedBackViews method.
*/
- (NSArray *) _visibleBackViews
{
NSArray *items = [_toolbar items];
NSView *backView, *view;
NSUInteger i, n;
float backViewsWidth = 0, toolbarWidth = [self frame].size.width;
NSMutableArray *visibleBackViews = [NSMutableArray array];
n = [items count];
for (i = 0; i < n; i++)
{
NSToolbarItem *item = [items objectAtIndex:i];
backView = [item _backView];
view = [item view];
if (view != nil)
backViewsWidth += [item minSize].width + 2*InsetItemViewX;
else
backViewsWidth += [backView frame].size.width;
if ((backViewsWidth + ClippedItemsViewWidth <= toolbarWidth)
|| (i == n - 1 && backViewsWidth <= toolbarWidth))
{
[visibleBackViews addObject: backView];
}
}
return visibleBackViews;
}
- (NSColor *) standardBackgroundColor
{
NSLog(@"Use of deprecated method %@", NSStringFromSelector(_cmd));
return nil;
}
- (BOOL) _usesStandardBackgroundColor
{
NSLog(@"Use of deprecated method %@", NSStringFromSelector(_cmd));
return NO;
}
- (void) _setUsesStandardBackgroundColor: (BOOL)standard
{
NSLog(@"Use of deprecated method %@", NSStringFromSelector(_cmd));
}
- (NSMenu *) menuForEvent: (NSEvent *)event
{
NSMenu *menu = [[NSMenu alloc] initWithTitle: @""];
id customize = [menu insertItemWithTitle: _(@"Customize Toolbar")
action: @selector(runCustomizationPalette:)
keyEquivalent: @""
atIndex: 0];
[customize setTarget: _toolbar];
return AUTORELEASE(menu);
}
@end