I'm trying to write an application that allows the user to drag files from the Finder and drop them onto an NSStatusItem. So far, I've created a custom view that implements the drag and drop interface. When I add this view as a subview of an NSWindow it all works correctly -- the mouse cursor gives appropriate feedback, and when dropped my code gets executed.
However, when I use the same view as an NSStatusItem's view it doesn't behave correctly. The mouse cursor gives appropriate feedback indicating that the file can be dropped, but when I drop the file my drop code never gets executed.
Is there something special I need to do to enable drag and drop with an NSStatusItem?
I finally got around to testing this and it works perfectly, so there's definitely something wrong with your code.
Here's a custom view that allows dragging:
#implementation DragStatusView
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
//register for drags
[self registerForDraggedTypes:[NSArray arrayWithObjects: NSFilenamesPboardType, nil]];
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect
{
//the status item will just be a yellow rectangle
[[NSColor yellowColor] set];
NSRectFill([self bounds]);
}
//we want to copy the files
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender
{
return NSDragOperationCopy;
}
//perform the drag and log the files that are dropped
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
NSPasteboard *pboard;
NSDragOperation sourceDragMask;
sourceDragMask = [sender draggingSourceOperationMask];
pboard = [sender draggingPasteboard];
if ( [[pboard types] containsObject:NSFilenamesPboardType] ) {
NSArray *files = [pboard propertyListForType:NSFilenamesPboardType];
NSLog(#"Files: %#",files);
}
return YES;
}
#end
Here's how you'd create the status item:
NSStatusItem* item = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
DragStatusView* dragView = [[DragStatusView alloc] initWithFrame:NSMakeRect(0, 0, 24, 24)];
[item setView:dragView];
[dragView release];
Since Yosemite, the method for setting a view on NSStatusItem is deprecated but fortunately there is a much nicer way using the new NSStatusItemButton property on NSStatusItem:
- (void)applicationDidFinishLaunching: (NSNotification *)notification {
NSImage *icon = [NSImage imageNamed:#"iconName"];
//This is the only way to be compatible to all ~30 menu styles (e.g. dark mode) available in Yosemite
[normalImage setTemplate:YES];
statusItem.button.image = normalImage;
// register with an array of types you'd like to accept
[statusItem.button.window registerForDraggedTypes:#[NSFilenamesPboardType]];
statusItem.button.window.delegate = self;
}
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
return NSDragOperationCopy;
}
- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender {
//drag handling logic
}
Please be aware that the button property is only available starting in 10.10 and you might have to keep your old solution if you support 10.9 Mavericks or below.
Related
I have subclassed NSWindow and I have a MYWindow class implementing the following method:
-(void)resetCursorRects {
NSImage *image = [NSImage imageNamed:#"cursor.png"];
[image setSize:NSMakeSize(32, 32)];
NSCursor *cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSMakePoint(1, 1)];
[super resetCursorRects];
[self addCursorRect:[self bounds] cursor:cursor];
}
This will change the cursor for the whole window and I will see cursor.png instead of the default mouse pointer. The problem is that this only works if the MYWindow is set to the key window which is of course non trivial to make it.
In the beginning of my project I just have one main window but now I need to have two different MYWindow. The problem with two windows it is not possible to set both as the key window and hence the custom mouse pointer is only displayed on the active window. I need to click the other window to make the cursor appear.
Is there any way around this? So I get a custom cursor on both windows?
Edit: Tried NSTrackingArea
I added this to my content view's init method:
self.trackingArea = [[NSTrackingArea alloc] initWithRect:[self frame] options: (NSTrackingCursorUpdate | NSTrackingActiveAlways | NSTrackingMouseMoved) owner:self userInfo:nil];
[self addTrackingArea:self.trackingArea];
Then I overrided cursorUpdate: like this:
-(void)cursorUpdate:(NSEvent *)event {
NSLog(#"event : %#", event);
[[NSCursor crosshairCursor] set];
}
This makes the crosshairCursor show when the NSWindow that contains the NSImageView derived class is key window. But if I make another NSWindow within the app the key window, the cursor returns to the standard cursor again. Am I doing something wrong?
I struggled with this problem for a long period of time and I think there is only one way to change mouse cursor over inactive application (over non-foreground window). This is hacky and magic way.
Before calling pretty standard:
[[NSCursor pointingHandCursor] push];
You have to call:
void CGSSetConnectionProperty(int, int, int, int);
int CGSCreateCString(char *);
int CGSCreateBoolean(BOOL);
int _CGSDefaultConnection();
void CGSReleaseObj(int);
int propertyString, boolVal;
propertyString = CGSCreateCString("SetsCursorInBackground");
boolVal = CGSCreateBoolean(TRUE);
CGSSetConnectionProperty(_CGSDefaultConnection(), _CGSDefaultConnection(), propertyString, boolVal);
CGSReleaseObj(propertyString);
CGSReleaseObj(boolVal);
Or if you are using Swift:
Put this in your YourApp-Bridging-Header.h:
typedef int CGSConnectionID;
CGError CGSSetConnectionProperty(CGSConnectionID cid, CGSConnectionID targetCID, CFStringRef key, CFTypeRef value);
int _CGSDefaultConnection();
And then call:
let propertyString = CFStringCreateWithCString(kCFAllocatorDefault, "SetsCursorInBackground", 0)
CGSSetConnectionProperty(_CGSDefaultConnection(), _CGSDefaultConnection(), propertyString, kCFBooleanTrue)
You should be able to add an NSTrackingArea that changes the cursor, as long as you don’t want it to also change when the app is inactive (that is essentially impossible).
Edit:
I was able to get this working with the following code:
- (vod)someSetup;
{
NSTrackingArea *const trackingArea = [[NSTrackingArea alloc] initWithRect:NSZeroRect options: (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways | NSTrackingInVisibleRect) owner:self userInfo:nil];
[self.view addTrackingArea:trackingArea];
}
- (void)mouseEntered:(NSEvent *)theEvent;
{
[[NSCursor IBeamCursor] push];
}
- (void)mouseExited:(NSEvent *)theEvent;
{
[[NSCursor IBeamCursor] pop];
}
Now I finally found a solution that works. I don't know if this will bite me in the tail in the future but at least this seem to work when testing.
Thanks Wil for the example it got me half way there. But it was only when I finally combined it with resetCursorRects and also defined a cursor rect in each view with the specific cursor. This took me a long time to figure out and I don't know if the solution is optimal (suggestions of improvement are welcome)
Below is the full example that made it work for me in the end (self.cursor is an instance of the cursor)
- (void)viewWillMoveToWindow:(NSWindow *)newWindow {
NSTrackingArea *const trackingArea = [[NSTrackingArea alloc] initWithRect:NSZeroRect options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways | NSTrackingInVisibleRect) owner:self userInfo:nil];
[self addTrackingArea:trackingArea];
[self.window invalidateCursorRectsForView:self];
}
- (void)resetCursorRects {
[super resetCursorRects];
[self addCursorRect:self.bounds cursor:self.cursor];
}
- (void)mouseEntered:(NSEvent *)theEvent {
[super mouseEntered:theEvent];
[self.cursor push];
}
- (void)mouseExited:(NSEvent *)theEvent {
[super mouseExited:theEvent];
[self.cursor pop];
}
I am trying to customize an NSImageCell for NSTableView using NSArrayController and bindings to change the background of the cell which is selected. So, I created two NSImage images and retain them as normalImage and activeImage in the cell instance, which means I should release these two images when the cell calls its dealloc method. And I override
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView
and
- (void) setObjectValue:(id) inObject
But I find that when I click any cell in the tableview, the cell's dealloc method is called.
So I put NSLog(#"%#", self); in the dealloc method and - (void)drawInteriorWithFrame:inView: and I find that these two instance are not same.
Can anyone tell me why dealloc is called every time I click any cell? Why are these two instances not the same? What does OS X do when I customize the cell in NSTableView?
BTW: I found that the -init is called only once. Why?
EDIT:
My cell code
#implementation SETableCell {
NSImage *_bgNormal;
NSImage *_bgActive;
NSString *_currentString;
}
- (id)init {
if (self = [super init]) {
NSLog(#"setup: %#", self);
_bgNormal = [[NSImage imageNamed:#"bg_normal"] retain];
_bgActive = [[NSImage imageNamed:#"bg_active"] retain];
}
return self;
}
- (void)dealloc {
// [_bgActive release]; _bgActive = nil;
// [_bgNormal release]; _bgNormal = nil;
// [_currentString release]; _currentString = nil;
NSLog(#"dealloc: %#", self);
[super dealloc];
}
- (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
NSLog(#"draw: %#", self);
NSPoint point = cellFrame.origin;
NSImage *bgImg = self.isHighlighted ? _bgActive : _bgNormal;
[bgImg drawAtPoint:p fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
NSPoint strPoint = cellFrame.origin;
strPoint.x += 30;
strPoint.y += 30;
[_currentString drawAtPoint:strPoint withAttributes:nil];
}
- (void) setObjectValue:(id) inObject {
if (inObject != nil && ![inObject isEqualTo:_currentString]) {
[self setCurrentInfo:inObject];
}
}
- (void)setCurrentInfo:(NSString *)info {
if (_currentString != info) {
[_currentString release];
_currentString = [info copy];
}
}
#end
As a normal recommendation, you should move to ARC as it takes cares of most of the memory management tasks that you do manually, like retain, releases. My answers will assume that you are using manual memory management:
Can anyone tell me why dealloc is called every time I click any cell ?
The only way for this to happen, is if you are releasing or auto-releasing your cell. If you are re-using cells, they shouldn't be deallocated.
Why these tow instance are not same ?
If you are re-using them, the cell that you clicked, and the cell that has been deallocated, they should be different. Pay close attention to both your questions, in one you assume that you are releasing the same cell when you click on it, on the second you are seeing that they are different.
What does Apple do when I custom the cell in NSTableView ?
Apple as a company? Or Apple as in the native frameworks you are using? I am assuming you are going for the second one: a custom cell is just a subclass of something that the NSTableView is expecting, it should behave the same as a normal one plus your custom implementation.
BTW: I found that the init is called only once, and why ?
Based on this, you are probably re-using cells, and only in the beginning they are actually being initialised.
It would be very useful to see some parts of your code:
Your Cell's code
Your NSTableView cell's creation code.
I'm trying to create a simple NSView that will allow a folder from Finder to be dragged onto it. A folder path is the only thing I want the view to accept as a draggable item. I've been trying to follow the Apple documentation, but so far nothing's working. So far, I've just tried to get the view to work with any file type, but I can't even seem to do that. Here's what I have so far:
-(id) initWithFrame:(NSRect)frameRect
{
if (self = [super initWithFrame:frameRect])
{
NSLog(#"getting called");
[self registerForDraggedTypes:[NSArray arrayWithObjects:NSPasteboardTypeString,
NSPasteboardTypePDF,
NSPasteboardTypeTIFF,
NSPasteboardTypePNG,
NSPasteboardTypeRTF,
NSPasteboardTypeRTFD,
NSPasteboardTypeHTML,
NSPasteboardTypeTabularText,
NSPasteboardTypeFont,
NSPasteboardTypeRuler,
NSPasteboardTypeColor,
NSPasteboardTypeSound,
NSPasteboardTypeMultipleTextSelection,
NSPasteboardTypeFindPanelSearchOptions, nil]];
}
return self;
}
-(BOOL) prepareForDragOperation: (id<NSDraggingInfo>) sender
{
NSLog(#"preparing for drag");
return YES;
}
The initWithFrame: method is getting called, but when I try to drag into the view the prepareForDragOperation: method doesn't ever seem to get called. My questions:
What am I doing wrong? Why isn't prepareForDragOperation: ever getting called?
What do I need to do to get the drag operation to only support dragging folders?
Update
I updated my registerForDraggedTypes: method with every type I could find. It now looks like this:
[self registerForDraggedTypes:[NSArray arrayWithObjects:NSPasteboardTypeString,
NSPasteboardTypePDF,
NSPasteboardTypeTIFF,
NSPasteboardTypePNG,
NSPasteboardTypeRTF,
NSPasteboardTypeRTFD,
NSPasteboardTypeHTML,
NSPasteboardTypeTabularText,
NSPasteboardTypeFont,
NSPasteboardTypeRuler,
NSPasteboardTypeColor,
NSPasteboardTypeSound,
NSPasteboardTypeMultipleTextSelection,
NSPasteboardTypeFindPanelSearchOptions,
NSStringPboardType,
NSFilenamesPboardType,
NSPostScriptPboardType,
NSTIFFPboardType,
NSRTFPboardType,
NSTabularTextPboardType,
NSFontPboardType,
NSRulerPboardType,
NSFileContentsPboardType,
NSColorPboardType,
NSRTFDPboardType,
NSHTMLPboardType,
NSURLPboardType,
NSPDFPboardType,
NSVCardPboardType,
NSFilesPromisePboardType,
NSMultipleTextSelectionPboardType, nil]];
I've noticed that the prepareForDragOperation: method isn't getting called when I drag a folder into the view. Did I miss a step?
Here's a simple little drag & drop view meeting those criteria:
MDDragDropView.h:
#interface MDDragDropView : NSView {
BOOL isHighlighted;
}
#property (assign, setter=setHighlighted:) BOOL isHighlighted;
#end
MDDragDropView.m:
#implementation MDDragDropView
#dynamic isHighlighted;
- (void)awakeFromNib {
NSLog(#"[%# %#]", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
[self registerForDraggedTypes:[NSArray arrayWithObjects:NSFilenamesPboardType, nil]];
}
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender {
NSLog(#"[%# %#]", NSStringFromClass([self class]), NSStringFromSelector(_cmd));
NSPasteboard *pboard = [sender draggingPasteboard];
if ([[pboard types] containsObject:NSFilenamesPboardType]) {
NSArray *paths = [pboard propertyListForType:NSFilenamesPboardType];
for (NSString *path in paths) {
NSError *error = nil;
NSString *utiType = [[NSWorkspace sharedWorkspace]
typeOfFile:path error:&error];
if (![[NSWorkspace sharedWorkspace]
type:utiType conformsToType:(id)kUTTypeFolder]) {
[self setHighlighted:NO];
return NSDragOperationNone;
}
}
}
[self setHighlighted:YES];
return NSDragOperationEvery;
}
And the rest of the methods:
- (void)draggingExited:(id <NSDraggingInfo>)sender {
[self setHighlighted:NO];
}
- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender {
return YES;
}
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender {
[self setHighlighted:NO];
return YES;
}
- (BOOL)isHighlighted {
return isHighlighted;
}
- (void)setHighlighted:(BOOL)value {
isHighlighted = value;
[self setNeedsDisplay:YES];
}
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
if (isHighlighted) {
[NSBezierPath setDefaultLineWidth:6.0];
[[NSColor keyboardFocusIndicatorColor] set];
[NSBezierPath strokeRect:self.frame];
}
}
#end
The reason prepareForDragOperation: isn't being called is that the dragging destination sequence follows a precise set of steps, and if the earlier steps aren't implemented, or are implemented but return a "stop the drag operation" type of answer, the later methods are never reached. (In your case, it doesn't appear that you've implemented the draggingEntered: method, which would need to return something other than NSDragOperationNone to continue on in the sequence).
Before prepareForDragOperation: is sent, the view is first sent a series of dragging destination messages:
A single - (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender.
Depending on the NSDragOperation mask returned from that method, the following will be called if it's implemented in your class:
Multiple - (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender.
Depending on the NSDragOperation mask returned from that method, then prepareForDragOperation: will be called.
I'm using NSURLPboardType to register for stuff being dropped from the Finder (when I drag a file or a folder to my application, it receives them as urls)
Try this. And if it works, it'll solve your second problem : just check if the URL is a folder to accept or reject the drop :
// if item is an NSURL * :
CFURLHasDirectoryPath((CFURLRef)item)
// returns true if item is the URL of a folder.
UIBarButtonItem does not extend UIView, so there is nothing like a frame property.
But is there any way I can get what is it's CGRect frame, relative to the application UIWindow?
Do you like to use private APIs? If yes,
UIView* view = thatItem.view;
return [view convertRect:view.bounds toView:nil];
Of course no one wants this when targeting the AppStore. A more unreliable method, and also uses undocumented features, but will pass Apple's test, is to loop through the subviews to look for the corresponding button item.
NSMutableArray* buttons = [[NSMutableArray alloc] init];
for (UIControl* btn in theToolbarOrNavbar.subviews)
if ([btn isKindOfClass:[UIControl class]])
[buttons addObject:btn];
UIView* view = [buttons objectAtIndex:index];
[buttons release];
return [view convertRect:view.bounds toView:nil];
The index is the index to your bar item in the array of .items, after removing all blank items. This assumes the buttons are arranged in increasing order, which may not be. A more reliable method is to sort the buttons array in increasing .origin.x value. Of course this still assumes the bar button item must inherit the UIControl class, and are direct subviews of the toolbar/nav-bar, which again may not be.
As you can see, there are a lot of uncertainty when dealing with undocumented features. However, you just want to pop up something under the finger right? The UIBarButtonItem's .action can be a selector of the form:
-(void)buttonClicked:(UIBarButtonItem*)sender event:(UIEvent*)event;
note the event argument — you can obtain the position of touch with
[[event.allTouches anyObject] locationInView:theWindow]
or the button view with
[[event.allTouches anyObject] view]
Therefore, there's no need to iterate the subviews or use undocumented features for what you want to do.
I didn't see this option posted (which in my opinion is much simpler), so here it is:
UIView *barButtonView = [barButtonItem valueForKey:#"view"];
In iOS 3.2, there's a much easier way to show an Action Sheet popover from a toolbar button. Merely do something like this:
- (IBAction)buttonClicked:(UIBarButtonItem *)sender event:(UIEvent *)event
{
UIActionSheet *popupSheet;
// Prepare your action sheet
[popupSheet showFromBarButtonItem:sender animated:YES];
}
This is the implementation I use for my WEPopover project: (https://github.com/werner77/WEPopover):
#implementation UIBarButtonItem(WEPopover)
- (CGRect)frameInView:(UIView *)v {
UIView *theView = self.customView;
if (!theView.superview && [self respondsToSelector:#selector(view)]) {
theView = [self performSelector:#selector(view)];
}
UIView *parentView = theView.superview;
NSArray *subviews = parentView.subviews;
NSUInteger indexOfView = [subviews indexOfObject:theView];
NSUInteger subviewCount = subviews.count;
if (subviewCount > 0 && indexOfView != NSNotFound) {
UIView *button = [parentView.subviews objectAtIndex:indexOfView];
return [button convertRect:button.bounds toView:v];
} else {
return CGRectZero;
}
}
#end
As long as UIBarButtonItem (and UITabBarItem) does not inherit from UIView—for historical reasons UIBarItem inherits from NSObject—this craziness continues (as of this writing, iOS 8.2 and counting ... )
The best answer in this thread is obviously #KennyTM's. Don't be silly and use the private API to find the view.
Here's a oneline Swift solution to get an origin.x sorted array (like Kenny's answer suggests):
let buttonFrames = myToolbar.subviews.filter({
$0 is UIControl
}).sorted({
$0.frame.origin.x < $1.frame.origin.x
}).map({
$0.convertRect($0.bounds, toView:nil)
})
The array is now origin.x sorted with the UIBarButtonItem frames.
(If you feel the need to read more about other people's struggles with UIBarButtonItem, I recommend Ash Furrow's blog post from 2012: Exploring UIBarButtonItem)
I was able to get Werner Altewischer's WEpopover to work by passing up the toolbar along with the
UIBarButton:
Mod is in WEPopoverController.m
- (void)presentPopoverFromBarButtonItem:(UIBarButtonItem *)item toolBar:(UIToolbar *)toolBar
permittedArrowDirections:(UIPopoverArrowDirection)arrowDirections
animated:(BOOL)animated
{
self.currentUIControl = nil;
self.currentView = nil;
self.currentBarButtonItem = item;
self.currentArrowDirections = arrowDirections;
self.currentToolBar = toolBar;
UIView *v = [self keyView];
UIButton *button = nil;
for (UIView *subview in toolBar.subviews)
{
if ([[subview class].description isEqualToString:#"UIToolbarButton"])
{
for (id target in [(UIButton *)subview allTargets])
{
if (target == item)
{
button = (UIButton *)subview;
break;
}
}
if (button != nil) break;
}
}
CGRect rect = [button.superview convertRect:button.frame toView:v];
[self presentPopoverFromRect:rect inView:v permittedArrowDirections:arrowDirections animated:animated];
}
-(CGRect) getBarItemRc :(UIBarButtonItem *)item{
UIView *view = [item valueForKey:#"view"];
return [view frame];
}
You can get it from the UINavigationBar view. The navigationBar is a UIView which has 2 or 3 custom subviews for the parts on the bar.
If you know that the UIBarButtonItem is currently shown in the navbar on the right, you can get its frame from navbar's subviews array.
First you need the navigationBar which you can get from the navigationController which you can get from the UIViewController. Then find the right most subview:
UINavigationBar* navbar = curViewController.navigationController.navigationBar;
UIView* rightView = nil;
for (UIView* v in navbar.subviews) {
if (rightView==nil) {
rightView = v;
} else if (v.frame.origin.x > rightView.frame.origin.x) {
rightView = v; // this view is further right
}
}
// at this point rightView contains the right most subview of the navbar
I haven't compiled this code so YMMV.
This is not the best solution and from some point of view it's not right solution and we can't do like follow because we access to object inside UIBarBattonItem implicitly, but you can try to do something like:
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 30, 30)];
[button setImage:[UIImage imageNamed:#"Menu_Icon"] forState:UIControlStateNormal];
[button addTarget:self action:#selector(didPressitem) forControlEvents:UIControlEventTouchUpInside];
UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:button];
self.navigationItem.rightBarButtonItem = item;
CGPoint point = [self.view convertPoint:button.center fromView:(UIView *)self.navigationItem.rightBarButtonItem];
//this is like view because we use UIButton like "base" obj for
//UIBarButtonItem, but u should note that UIBarButtonItem base class
//is NSObject class not UIView class, for hiding warning we implicity
//cast UIBarButtonItem created with UIButton to UIView
NSLog(#"point %#", NSStringFromCGPoint(point));
as result i got next:
point {289, 22}
Before implement this code, be sure to call [window makeKeyAndVisible] in your Applition delegate application:didFinishLaunchingWithOptions: method!
- (void) someMethod
{
CGRect rect = [barButtonItem convertRect:barButtonItem.customview.bounds toView:[self keyView]];
}
- (UIView *)keyView {
UIWindow *w = [[UIApplication sharedApplication] keyWindow];
if (w.subviews.count > 0) {
return [w.subviews objectAtIndex:0];
} else {
return w;
}
}
I handled it as follows:
- (IBAction)buttonClicked:(UIBarButtonItem *)sender event:(UIEvent *)event
{
UIView* view = [sender valueForKey:#"view"]; //use KVO to return the view
CGRect rect = [view convertRect:view.bounds toView:self.view];
//do stuff with the rect
}
I'm trying to subclass NSScroller in order to draw my own scroller knob. To do this, I've subclassex NSScrollView and usex the following code to instantiate my custom NSScrollers:
- (void)awakeFromNib;
{
NSRect horizontalScrollerFrame = [[self horizontalScroller] frame];
NSRect verticalScrollerFrame = [[self verticalScroller] frame];
NSString *scrollBarVariant = [[[NSUserDefaults standardUserDefaults] persistentDomainForName:NSGlobalDomain] valueForKey:#"AppleScrollBarVariant"];
if (![scrollBarVariant isEqualToString:#"DoubleBoth"]) {
[self setVerticalScroller:[[[TRScroller alloc] initWithFrame:verticalScrollerFrame] autorelease]];
[self setHorizontalScroller:[[[TRScroller alloc] initWithFrame:horizontalScrollerFrame] autorelease]];
}
}
This works and my NSScrollers display correctly. But I'm occasionally seeing rendering issues upon first loading my application. Within Interface Builder I have laid out a number of NSScrollViews with their scrollbars set to hide automatically. The issue I'm seeing is that when the application first loads, the scrollbar backgrounds are rendered across the NSScrollViews contents.
alt text http://www.freeimagehosting.net/uploads/1d3fc75db8.png
I believe this is because I instantiate my NSScroll subclass (TRSubclass) via awakeFromNib, which means that the scrollbars are given the frame of the NSScrollView before it is automatically resized to meet the windows saved location and size (in other words, it's using the frame that's assigned by default within Interface Builder). What's the best way around this?
I've tried forcing the NSScrollView to redisplay (using setNeedsDisplay: and display:) but with no luck. Has anyone else come across a similar issue?
I'm using the same schema in my applications and I fighted this issues a lot. I use the same trick: scrollers are substituted in [scrollView awakeFromNib] methods, but I don't face such rendering issues at the moment. You can try to play with "draws background" property of the NSScrollView - it really helps sometimes
- (void)changeSubs
{
// change clip view
// ...
// change scrollers
NSRect horizontalScrollerFrame = [[self horizontalScroller] frame];
NSRect verticalScrollerFrame = [[self verticalScroller] frame];
if (![[self verticalScroller] isKindOfClass:[CRScroller class]])
[self setVerticalScroller:[[[CRScroller alloc] initWithFrame:verticalScrollerFrame] autorelease]];
if (![[self horizontalScroller] isKindOfClass:[CRScroller class]])
[self setHorizontalScroller:[[[CRScroller alloc] initWithFrame:horizontalScrollerFrame] autorelease]];
}
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
[self changeSubs];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder
{
NSKeyedUnarchiver* unpacker = (id)aDecoder;
[unpacker setClass:[CRClipView class] forClassName:[NSClipView className]];
[unpacker setClass:[CRScroller class] forClassName:[NSScroller className]];
self = [super initWithCoder:aDecoder];
if (self)
{
}
return self;
}
- (void)awakeFromNib
{
[self changeSubs];
}
There are few tricks here, they work depending on a way NSScrollView is created. 'isKindOfClass' check helps to avoid double-swap.