I need to draw the current user annotation (the blue dot) on top of all other annotations. Right now it is getting drawn underneath my other annotations and getting hidden. I'd like to adjust the z-index of this annotation view (the blue dot) and bring it to the top, does anyone know how?
Update:
So I can now get a handle on the MKUserLocationView, but how do I bring it forward?
- (void) mapView:(MKMapView *)aMapView didAddAnnotationViews:(NSArray *)views {
for (MKAnnotationView *view in views) {
if ([[view annotation] isKindOfClass:[MKUserLocation class]]) {
// How do I bring this view to the front of all other annotations?
// [???? bringSubviewToFront:????];
}
}
}
Finally got it to work using the code listed below thanks to the help from Paul Tiarks. The problem I ran into is that the MKUserLocation annotation gets added to the map first before any others, so when you add the other annotations their order appears to be random and would still end up on top of the MKUserLocation annotation. To fix this I had to move all the other annotations to the back as well as move the MKUserLocation annotation to the front.
- (void) mapView:(MKMapView *)aMapView didAddAnnotationViews:(NSArray *)views
{
for (MKAnnotationView *view in views)
{
if ([[view annotation] isKindOfClass:[MKUserLocation class]])
{
[[view superview] bringSubviewToFront:view];
}
else
{
[[view superview] sendSubviewToBack:view];
}
}
}
Update: You may want to add the code below to ensure the blue dot is drawn on top when scrolling it off the viewable area of the map.
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
{
for (NSObject *annotation in [mapView annotations])
{
if ([annotation isKindOfClass:[MKUserLocation class]])
{
NSLog(#"Bring blue location dot to front");
MKAnnotationView *view = [mapView viewForAnnotation:(MKUserLocation *)annotation];
[[view superview] bringSubviewToFront:view];
}
}
}
Another solution:
setup annotation view layer's zPosition (annotationView.layer.zPosition) in:
- (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views;
The official answer to that thread is wrong... using zPosition is indeed the best approach and fastest vs using regionDidChangeAnimated...
else you would suffer big performance impact with many annotations on map (as every change of frame would rescan all annotations). and been testing it...
so when creating the view of the annotation (or in didAddAnnotationViews) set :
self.layer.zPosition = -1; (below all others)
and as pointed out by yuf:
This makes the pin cover callouts from other pins – yuf Dec 5 '13 at 20:25
i.e. the annotation view will appear below other pins.
to fix, simply reput the zPosition to 0 when you have a selection
-(void) mapView:(MKMapView*)mapView didSelectAnnotationView:(MKAnnotationView*)view {
if ([view isKindOfClass:[MyCustomAnnotationView class]])
view.layer.zPosition = 0;
...
}
-(void) mapView:(MKMapView*)mapView didDeselectAnnotationView:(MKAnnotationView*)view {
if ([view isKindOfClass:[MyCustomAnnotationView class]])
view.layer.zPosition = -1;
...
}
Update for iOS 14
I know it's an old post, but the question is still applicable and you end up here when typing it into your favorite search engine.
Starting with iOS 14, Apple introduced a zPriority property to MKAnnotationView. You can use it to set up the z-index for your annotations using predefined constants or floats.
Also, Apple made it possible to finally create the view for the user location on our own and provided MKUserLocationView as a subclass of MKAnnotationView.
From the documentation for MKUserLocationView:
If you want to specify additional configuration, such as zPriority,
create this annotation view directly. To display the annotation view,
return the instance from mapView(_:viewFor:).
The following code snippet shows how this can be done (add the code to your MKMapViewDelegate):
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
// Alter the MKUserLocationView (iOS 14+)
if #available(iOS 14.0, *), annotation is MKUserLocation {
// Try to reuse the existing view that we create below
let reuseIdentifier = "userLocation"
if let existingView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier) {
return existingView
}
let view = MKUserLocationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
view.zPriority = .max // Show user location above other annotations
view.isEnabled = false // Ignore touch events and do not show callout
return view
}
// Create views for other annotations or return nil to use the default representation
return nil
}
Note that per default, the user location annotation shows a callout when tapping on it. Now that the user location overlays your other annotations, you'd probably want to disable this, which is done in the code by setting .isEnabled to false.
Just use the .layer.anchorPointZ property.
Example:
func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
views.forEach {
if let _ = $0.annotation as? MKUserLocation {
$0.layer.anchorPointZ = 0
} else {
$0.layer.anchorPointZ = 1
}
}
}
Here is there reference https://developer.apple.com/documentation/quartzcore/calayer/1410796-anchorpointz
Try, getting a reference to the user location annotation (perhaps in mapView: didAddAnnotationViews:) and then bring that view to the front of the mapView after all of your annotations have been added.
Swift 3:
internal func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
for annotationView in views {
if annotationView.annotation is MKUserLocation {
annotationView.bringSubview(toFront: view)
return
}
annotationView.sendSubview(toBack: view)
}
}
Here is a way to do it using predicates. I think it should be faster
NSPredicate *userLocationPredicate = [NSPredicate predicateWithFormat:#"class == %#", [MKUserLocation class]];
NSArray* userLocation = [[self.mapView annotations] filteredArrayUsingPredicate:userLocationPredicate];
if([userLocation count]) {
NSLog(#"Bring blue location dot to front");
MKAnnotationView *view = [self.mapView viewForAnnotation:(MKUserLocation *)[userLocation objectAtIndex:0]];
[[view superview] bringSubviewToFront:view];
}
Using Underscore.m
_.array(mapView.annotations).
filter(^ BOOL (id<MKAnnotation> annotation) { return [annotation
isKindOfClass:[MKUserLocation class]]; })
.each(^ (id<MKAnnotation> annotation) { [[[mapView
viewForAnnotation:annotation] superview] bringSubviewToFront:[mapView
viewForAnnotation:annotation]]; });
Related
I'm loading the pdf (Having multiple Hyperlinks) document in UIWebview. I have to show UIPopover over hyperlinks dynamically.
I'm able to capture the coordinates of hyperlink using TapGesture Action method
- (void)tapAction:(UITapGestureRecognizer *)sender
{
self.point = [sender locationInView:self.myWebView];
}
And presenting the UIPopover over hyperlink by using below method
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSURL *rqstUrl = [request URL];
if (([[rqstUrl scheme] isEqualToString: #"https"] || [[rqstUrl scheme] isEqualToString: #"http"]) && (navigationType == UIWebViewNavigationTypeLinkClicked))
{
[self.myWebView stopLoading];
CGRect rect = CGRectMake(self.point.x,self.point.y-5, 5, 5);
UIPopoverController *popController = [[UIPopoverController alloc] initWithContentViewController:contentViewController];
popController.popoverContentSize = CGSizeMake(500, 200);
self.popController = popController;
self.popController.delegate =self;
UIPopoverArrowDirection direction = UIPopoverArrowDirectionUp|UIPopoverArrowDirectionDown;
self.popController.popoverLayoutMargins = UIEdgeInsetsMake(0, rect.origin.x, 1, 1);
[self.popController presentPopoverFromRect:rect inView:webView permittedArrowDirections:direction animated:YES];
}
return YES;
}
But the problem is if I tapped in two different locations within 1 or 2 seconds like First Tap is On Hyperlink and Second Tap is on "somewhere else in UIWebview", UIPopover is presenting at second tap location only not in hyperlink location.
I have to show UIPopover based on the Hyperlink position only, not in other location.How can I resolve this issue?
Use an overlay view
Replace your method to register the tap location by an overlay with a tap through. UITapGestureRecognizer has these limitations:
When a tap occurs outside of an hyperlink, it does registers its location, thanks to the UITapGestureRecognizer.
Unfortunately, a UIWebview Hyperlink taps take precedence over the gesture recognizer, and you never get the centroid. This is the real problem, causing the popover to appear misplaced.
UIPopoverController is deprecated in iOS 9.
"UIPopoverController is deprecated. Popovers are now implemented as UIViewController presentations. Use a modal presentation style of UIModalPresentationPopover and UIPopoverPresentationController."
tapAction and shouldStartLoadWithRequest are not coupled, and can occur independently of each other. Furthermore, they are basically mutually exclusive.
Use the overlay to register location in that view, and tap-though to the views underneath. If your overlay and web view have the same frame, you can use the tap position interchangeably. The overlay will guarantee tight coupling, and the rest of your method will work as designed.
class TapOverlayView: UIView {
var centroid:CGRect = CGRect.zero
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
centroid = CGRect(origin: point, size: CGSize(width: 1, height: 1))
return nil // tap through
}
}
Delegate
extension ViewController: UIWebViewDelegate {
public func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
let rqstUrl = request.url
if( rqstUrl!.scheme?.contains("http"))! && ( .linkClicked == navigationType) {
webView.stopLoading()
let contentViewController = storyboard!.instantiateViewController(withIdentifier: "popover")
contentViewController.modalPresentationStyle = .popover
contentViewController.preferredContentSize = CGSize(width: 200, height: 40)
if let popController = contentViewController.popoverPresentationController {
popController.permittedArrowDirections = .down
popController.sourceView = webView
popController.sourceRect = CGRect(origin: tap.centroid.origin, size: CGSize.zero)
present(contentViewController, animated: true, completion: nil)
}
}
return true
}
}
► Find this solution on GitHub and additional details on Swift Recipes.
I have a map view that has annotations on it. With each annotation, I need to show an overlay that is just a circle. This circle needs to be one of two different colours depending on a value. The user can select something else on the screen, and all annotations and overlays need to be removed, and new ones need to be added based on the newly selected item.
The first time I load the controller having the map view, everything works great. When the user selects a new item, the old annotations and overlays are removed, and the new annotations and overlays are added, but the overlays don't appear. When I put breakpoints in, I don't see the mapview:rendererForOverlay method getting called after a user has selected a new item. Below is the code I am using:
In viewDidLoad, I have the following:
_mapview.delegate = self;
[self placePinsOnMap];
The placePinsOnMap: method has the following:
- (void)placePinsOnMap {
AMapAnnotation* annotation;
for (APlace* place in _selectedItem.places) {
annotation = [[AMapAnnotation alloc] initWithTitle:place.name subtitle:place.subtitle coordinate:place.location];
annotation.object = place;
[_mapview addAnnotation:annotation];
[_mapview addOverlay:[[AMapOverlay alloc] initWithCenterCoordinate:place.location radius:[_place.size floatValue]*1000 object:place] level:MKOverlayLevelAboveRoads];
}
}
Each time an overlay is added, the mapview:rendererForOverlay is called. This looks like this:
- (MKOverlayRenderer*)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay {
MKCircleRenderer* circleRenderer;
AMapOverlay* mapOverlay;
APlace* place;
mapOverlay = (AMapOverlay*)overlay;
place = mapOverlay.object;
circleRenderer = [[MKCircleRenderer alloc] initWithCircle:mapOverlay];
if ([place.radius floatValue] >= 5 && [place.radius floatValue] <= 10) {
circleRenderer.fillColor = [[UIColor greenColor] colorWithAlphaComponent:0.2];
} else {
circleRenderer.fillColor = [[UIColor redColor] colorWithAlphaComponent:0.2];
}
return circleRenderer;
}
When a user selects a new item, I remove all annotations and overlays, and I place the new annotations and their overlays for the new item selected:
- (void)onScrollviewTap:(UITapGestureRecognizer*)recognizer {
CGPoint point;
point = [recognizer locationInView:_scrollview];
for (ACard* card in _cards) {
if (CGRectContainsPoint(card.frame, point)) {
card.selected = YES;
_selectedItem = card.item;
NSArray* array = [_mapview annotations];
for (id<MKAnnotation> annotation in array) {
if (annotation != _userAnnotation) {
[_mapview removeAnnotation:annotation];
}
}
[_mapview removeOverlays:[_mapview overlays]];
[self placePinsOnMap];
} else {
card.selected = NO;
}
}
}
The AMapOverlay class is a subclass of MKCircle and simply holds the coordinate, a radius value and a place object which is used to determine what colour the overlay should be. I have the boundingMapRect overridden in the AMapOverlay class:
- (MKMapRect)boundingMapRect {
MKMapPoint upperLeft = MKMapPointForCoordinate(self.coordinate);
MKMapRect bounds = MKMapRectMake(upperLeft.x, upperLeft.y, self.radius*2, self.radius*2);
return bounds;
}
Just to reiterate what my problem is: The first time I load the controller having the map view, everything works great. I see the annotations along with their overlays. When the user selects a new item, the old annotations and overlays are removed, and the new annotations are added along with the overlays, but the overlays don't appear.
Does anyone have any ideas why this would be?
I am working on a project that has the concept of draggable controls, everything is working fine except that NSView seems to employ a fade in/out animation when calling setHidden:.
I have been able to work around the problem by changing the line session.animatesToStartingPositionsOnCancelOrFail = YES; to NO and implementing the image snapback myself with a custom animated NSWindow subclass. it looks great, but I know there must be an easier way.
I have tried:
using NSAnimationContext grouping with duration of 0 around the setHidden: calls
setting the view animations dictionary using various keys (alpha, hidden, isHidden) on the control and superview
overriding animationForKey: for both the control and its superview
I am not using CALayers and have even tried explicitly setting wantsLayer: to NO.
Does anybody know how to either disable this animation, or have a simpler solution then my animated NSWindow?
here is my stripped down altered code with the bare minimum to see what I'm talking about.
#implementation NSControl (DragControl)
- (NSDraggingSession*)beginDraggingSessionWithDraggingCell:(NSActionCell <NSDraggingSource> *)cell event:(NSEvent*) theEvent
{
NSImage* image = [self imageForCell:cell];
NSDraggingItem* di = [[NSDraggingItem alloc] initWithPasteboardWriter:image];
NSRect dragFrame = [self frameForCell:cell];
dragFrame.size = image.size;
[di setDraggingFrame:dragFrame contents:image];
NSArray* items = [NSArray arrayWithObject:di];
[self setHidden:YES];
return [self beginDraggingSessionWithItems:items event:theEvent source:cell];
}
- (NSRect)frameForCell:(NSCell*)cell
{
// override in multi-cell cubclasses!
return self.bounds;
}
- (NSImage*)imageForCell:(NSCell*)cell
{
return [self imageForCell:cell highlighted:[cell isHighlighted]];
}
- (NSImage*)imageForCell:(NSCell*)cell highlighted:(BOOL) highlight
{
// override in multicell cubclasses to just get an image of the dragged cell.
// for any single cell control we can just make sure that cell is the controls cell
if (cell == self.cell || cell == nil) { // nil signifies entire control
// basically a bitmap of the control
// NOTE: the cell is irrelevant when dealing with a single cell control
BOOL isHighlighted = [cell isHighlighted];
[cell setHighlighted:highlight];
NSRect cellFrame = [self frameForCell:cell];
// We COULD just draw the cell, to an NSImage, but button cells draw their content
// in a special way that would complicate that implementation (ex text alignment).
// subclasses that have multiple cells may wish to override this to only draw the cell
NSBitmapImageRep* rep = [self bitmapImageRepForCachingDisplayInRect:cellFrame];
NSImage* image = [[NSImage alloc] initWithSize:rep.size];
[self cacheDisplayInRect:cellFrame toBitmapImageRep:rep];
[image addRepresentation:rep];
// reset the original cell state
[cell setHighlighted:isHighlighted];
return image;
}
// cell doesnt belong to this control!
return nil;
}
#pragma mark NSDraggingDestination
- (void)draggingEnded:(id < NSDraggingInfo >)sender
{
[self setHidden:NO];
}
#end
#implementation NSActionCell (DragCell)
- (void)setControlView:(NSView *)view
{
// this is a bit of a hack, but the easiest way to make the control dragging work.
// force the control to accept image drags.
// the control will forward us the drag destination events via our DragControl category
[view registerForDraggedTypes:[NSImage imagePasteboardTypes]];
[super setControlView:view];
}
- (BOOL)trackMouse:(NSEvent *)theEvent inRect:(NSRect)cellFrame ofView:(NSView *)controlView untilMouseUp:(BOOL)untilMouseUp
{
BOOL result = NO;
NSPoint currentPoint = theEvent.locationInWindow;
BOOL done = NO;
BOOL trackContinously = [self startTrackingAt:currentPoint inView:controlView];
BOOL mouseIsUp = NO;
NSEvent *event = nil;
while (!done)
{
NSPoint lastPoint = currentPoint;
event = [NSApp nextEventMatchingMask:(NSLeftMouseUpMask|NSLeftMouseDraggedMask)
untilDate:[NSDate distantFuture]
inMode:NSEventTrackingRunLoopMode
dequeue:YES];
if (event)
{
currentPoint = event.locationInWindow;
// Send continueTracking.../stopTracking...
if (trackContinously)
{
if (![self continueTracking:lastPoint
at:currentPoint
inView:controlView])
{
done = YES;
[self stopTracking:lastPoint
at:currentPoint
inView:controlView
mouseIsUp:mouseIsUp];
}
if (self.isContinuous)
{
[NSApp sendAction:self.action
to:self.target
from:controlView];
}
}
mouseIsUp = (event.type == NSLeftMouseUp);
done = done || mouseIsUp;
if (untilMouseUp)
{
result = mouseIsUp;
} else {
// Check if the mouse left our cell rect
result = NSPointInRect([controlView
convertPoint:currentPoint
fromView:nil], cellFrame);
if (!result)
done = YES;
}
if (done && result && ![self isContinuous])
[NSApp sendAction:self.action
to:self.target
from:controlView];
else {
done = YES;
result = YES;
// this bit-o-magic executes on either a drag event or immidiately following timer expiration
// this initiates the control drag event using NSDragging protocols
NSControl* cv = (NSControl*)self.controlView;
NSDraggingSession* session = [cv beginDraggingSessionWithDraggingCell:self
event:theEvent];
// Note that you will get an ugly flash effect when the image returns if this is set to yes
// you can work around it by setting NO and faking the release by animating an NSWindowSubclass with the image as the content
// create the window in the drag ended method for NSDragOperationNone
// there is [probably a better and easier way around this behavior by playing with view animation properties.
session.animatesToStartingPositionsOnCancelOrFail = YES;
}
}
}
return result;
}
#pragma mark - NSDraggingSource Methods
- (NSDragOperation)draggingSession:(NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context
{
switch(context) {
case NSDraggingContextOutsideApplication:
return NSDragOperationNone;
break;
case NSDraggingContextWithinApplication:
default:
return NSDragOperationPrivate;
break;
}
}
- (void)draggingSession:(NSDraggingSession *)session endedAtPoint:(NSPoint)screenPoint operation:(NSDragOperation)operation
{
// now tell the control view the drag ended so it can do any cleanup it needs
// this is somewhat hackish
[self.controlView draggingEnded:nil];
}
#end
There must be a layer enabled somewhere in your view hierarchy, otherwise there wouldn't be a fade animation. Here is my way of disabling such animations:
#interface NoAnimationImageView : NSImageView
#end
#implementation NoAnimationImageView
+ (id)defaultAnimationForKey: (NSString *)key
{
return nil;
}
#end
The solution you already tried by setting the view animations dictionary should work. But not for the keys you mention but for the following. Use it somewhere before the animation is triggered the first time. If you have to do it on the window or view or both, I don't know.
NSMutableDictionary *animations = [NSMutableDictionary dictionaryWithDictionary:[[theViewOrTheWindow animator] animations];
[animations setObject:[NSNull null] forKey: NSAnimationTriggerOrderIn];
[animations setObject:[NSNull null] forKey: NSAnimationTriggerOrderOut];
[[theViewOrTheWindow animator] setAnimations:animations];
Or also just remove the keys if they are there (might not be the case as they are implicit / default):
NSMutableDictionary *animations = [NSMutableDictionary dictionaryWithDictionary:[[theViewOrTheWindow animator] animations];
[animations removeObjectForKey:NSAnimationTriggerOrderIn];
[animations removeObjectForKey:NSAnimationTriggerOrderOut];
[[theViewOrTheWindow animator] setAnimations:animations];
Ok. I figured out that the animation I'm seeing is not the control, the superview, nor the control's window. It appears that animatesToStartingPositionsOnCancelOrFail causes NSDraggingSession to create a window (observed with QuartzDebug) and put the drag image in it and it is this window that animates back to the origin and fades out before the setHidden: call is executed (i.e. before the drag operation is concluded).
Unfortunately, the window that it creates is not an NSWindow so creating a category on NSWindow doesn't disable the fade animation.
Secondly, there is no public way that I know of to get a handle on the window, so I can't attempt directly manipulating the window instance.
It looks like maybe my workaround is the best way to do this, after all its not far from what AppKit does for you anyway.
If anybody knows how to get a handle on this window, or what class it is I would be interested to know.
I have created a simple NSStatusBar with a NSMenu set as the menu. I have also added a few NSMenuItems to this menu, which work fine (including selectors and highlighting) but as soon as I add a custom view (setView:) no highlighting occurs.
CustomMenuItem *menuItem = [[CustomMenuItem alloc] initWithTitle:#"" action:#selector(openPreferences:) keyEquivalent:#""];
[menuItem foo];
[menuItem setTarget:self];
[statusMenu insertItem:menuItem atIndex:0];
[menuItem release];
And my foo method is:
- (void)foo {
NSView *view = [[NSView alloc] initWithFrame:CGRectMake(5, 10, 100, 20)];
[self setView:view];
}
If I remove the setView method, it will highlight.
I have searched and searched and cannot find a way of implementing/enabling this.
Edit
I implemented highlight by following the code in this question in my NSView SubClass:
An NSMenuItem's view (instance of an NSView subclass) isn't highlighting on hover
#define menuItem ([self enclosingMenuItem])
- (void) drawRect: (NSRect) rect {
BOOL isHighlighted = [menuItem isHighlighted];
if (isHighlighted) {
[[NSColor selectedMenuItemColor] set];
[NSBezierPath fillRect:rect];
} else {
[super drawRect: rect];
}
}
Here's a rather less long-winded version of the above. It's worked well for me. (backgroundColour is an ivar.)
- (void)drawRect:(NSRect)rect
{
if ([[self enclosingMenuItem] isHighlighted]) {
[[NSColor selectedMenuItemColor] set];
} else if (backgroundColour) {
[backgroundColour set];
}
NSRectFill(rect);
}
Update for 2019:
class CustomMenuItemView: NSView {
private var effectView: NSVisualEffectView
override init(frame: NSRect) {
effectView = NSVisualEffectView()
effectView.state = .active
effectView.material = .selection
effectView.isEmphasized = true
effectView.blendingMode = .behindWindow
super.init(frame: frame)
addSubview(effectView)
effectView.frame = bounds
}
required init?(coder decoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ dirtyRect: NSRect) {
effectView.isHidden = !(enclosingMenuItem?.isHighlighted ?? false)
}
}
Set one of those to your menuItem.view.
(Credit belongs to Sam Soffes who helped me figure this out and sent me almost that code verbatim.)
If you're adding a view to a menu item, that view has to draw the highlight itself. You don't get that for free, I'm afraid. From the Menu Programming Topics:
A menu item with a view does not draw its title, state, font, or other standard drawing attributes, and assigns drawing responsibility entirely to the view.
Yes, as mentioned earlier you must draw it yourself. I use AppKit's NSDrawThreePartImage(…) to draw, and also include checks to use the user's control appearance (blue or graphite.) To get the images, I just took them from a screenshot (if anyone knows a better way, please add a comment.) Here's a piece of my MenuItemView's drawRect:
// draw the highlight gradient
if ([[self menuItem] isHighlighted]) {
NSInteger tint = [[NSUserDefaults standardUserDefaults] integerForKey:#"AppleAquaColorVariant"];
NSImage *image = (AppleAquaColorGraphite == tint) ? menuItemFillGray : menuItemFillBlue;
NSDrawThreePartImage(dirtyRect, nil, image, nil, NO,
NSCompositeSourceOver, 1.0, [self isFlipped]);
}
else if ([self backgroundColor]) {
[[self backgroundColor] set];
NSRectFill(dirtyRect);
}
EDIT
Should have defined these:
enum AppleAquaColorVariant {
AppleAquaColorBlue = 1,
AppleAquaColorGraphite = 6,
};
These correspond to the two appearance options in System Preferences. Also, menuItemFillGray & menuItemFillBlue are just NSImages of the standard menu item fill gradients.
I'm using a textured window that has a tab bar along the top of it, just below the title bar.
I've used -setContentBorderThickness:forEdge: on the window to make the gradient look right, and to make sure sheets slide out from the right position.
What's not working however, is dragging the window around. It works if I click and drag the area that's actually the title bar, but since the title bar gradient spills into a (potentially/often empty) tab bar, it's really easy to click too low and it feels really frustrating when you try to drag and realise the window is not moving.
I notice NSToolbar, while occupying roughly the same amount of space below the title bar, allows the window to be dragged around when the cursor is over it. How does one implement this?
Thanks.
I tried the mouseDownCanMoveWindow solution (https://stackoverflow.com/a/4564146/901641) but it didn't work for me. I got rid of that method and instead added this to my window subclass:
- (BOOL)isMovableByWindowBackground {
return YES;
}
which worked like a charm.
I found this here:
-(void)mouseDown:(NSEvent *)theEvent {
NSRect windowFrame = [[self window] frame];
initialLocation = [NSEvent mouseLocation];
initialLocation.x -= windowFrame.origin.x;
initialLocation.y -= windowFrame.origin.y;
}
- (void)mouseDragged:(NSEvent *)theEvent {
NSPoint currentLocation;
NSPoint newOrigin;
NSRect screenFrame = [[NSScreen mainScreen] frame];
NSRect windowFrame = [self frame];
currentLocation = [NSEvent mouseLocation];
newOrigin.x = currentLocation.x - initialLocation.x;
newOrigin.y = currentLocation.y - initialLocation.y;
// Don't let window get dragged up under the menu bar
if( (newOrigin.y+windowFrame.size.height) > (screenFrame.origin.y+screenFrame.size.height) ){
newOrigin.y=screenFrame.origin.y + (screenFrame.size.height-windowFrame.size.height);
}
//go ahead and move the window to the new location
[[self window] setFrameOrigin:newOrigin];
}
It works fine, though I'm not 100% sure I'm doing it correctly. There's one bug I've found so far, and that's if the drag begins inside a subview (a tab itself) and then enters the superview (the tab bar). The window jumps around. Some -hitTest: magic, or possibly even just invalidating initialLocation on mouseUp should probably fix that.
As of macOS 10.11, the simplest way to do this is to utilize the new -[NSWindow performWindowDragWithEvent:] method:
#interface MyView () {
BOOL movingWindow;
}
#end
#implementation MyView
...
- (BOOL)mouseDownCanMoveWindow
{
return NO;
}
- (void)mouseDown:(NSEvent *)event
{
movingWindow = NO;
CGPoint point = [self convertPoint:event.locationInWindow
fromView:nil];
// The area in your view where you want the window to move:
CGRect movableRect = CGRectMake(0, 0, 100, 100);
if (self.window.movableByWindowBackground &&
CGRectContainsPoint(movableRect, point)) {
[self.window performWindowDragWithEvent:event];
movingWindow = YES;
return;
}
// Handle the -mouseDown: as usual
}
- (void)mouseDragged:(NSEvent *)event
{
if (movingWindow) return;
// Handle the -mouseDragged: as usual
}
#end
Here, -performWindowDragWithEvent: will handle the correct behavior of not overlapping the menu bar, and will also snap to edges on macOS 10.12 and later. Be sure to include a BOOL movingWindow instance variable with your view's private interface so you can avoid -mouseDragged: events once you determined you don't want to process them.
Here, we are also checking that -[NSWindow movableByWindowBackground] is set to YES so that this view can be used in non-movable-by-window-background windows, but that is optional.
Have you tried overriding the NSView method mouseDownCanMoveWindow to return YES?
It works for me after TWO steps:
Subclass NSView, override the mouseDownCanMoveWindow to return YES.
Subclass NSWindow, override the isMovableByWindowBackground to return YES.
It's quite easy:
override mouseDownCanMoveWindow property
override var mouseDownCanMoveWindow:Bool {
return false
}
If you got a NSTableView in your window, with selection enabled, overriding the mouseDownCanMoveWindow property won't work.
You need instead to create a NSTableView subclass and override the following mouse events (and use the performWindowDragWithEvent: mentioned in Dimitri answer):
#interface WindowDraggableTableView : NSTableView
#end
#implementation WindowDraggableTableView
{
BOOL _draggingWindow;
NSEvent *_mouseDownEvent;
}
- (void)mouseDown:(NSEvent *)event
{
if (self.window.movableByWindowBackground == NO) {
[super mouseDown:event]; // Normal behavior.
return;
}
_draggingWindow = NO;
_mouseDownEvent = event;
}
- (void)mouseDragged:(NSEvent *)event
{
if (self.window.movableByWindowBackground == NO) {
[super mouseDragged:event]; // Normal behavior.
return;
}
assert(_mouseDownEvent);
_draggingWindow = YES;
[self.window performWindowDragWithEvent:_mouseDownEvent];
}
- (void)mouseUp:(NSEvent *)event
{
if (self.window.movableByWindowBackground == NO) {
[super mouseUp:event]; // Normal behavior.
return;
}
if (_draggingWindow == YES) {
_draggingWindow = NO;
return; // Event already handled by `performWindowDragWithEvent`.
}
// Triggers regular table selection.
NSPoint locationInWindow = event.locationInWindow;
NSPoint locationInTable = [self convertPoint:locationInWindow fromView:nil];
NSInteger row = [self rowAtPoint:locationInTable];
if (row >= 0 && [self.delegate tableView:self shouldSelectRow:row])
{
NSIndexSet *rowIndex = [NSIndexSet indexSetWithIndex:row];
[self selectRowIndexes:rowIndex byExtendingSelection:NO];
}
}
#end
Also don't forget to set the corresponding window movableByWindowBackground property as well:
self.window.movableByWindowBackground = YES;
When you set property isMovableByWindowBackground in viewDidLoad, it may not work because the window property of the view is not yet set. In that case, try this:
override func viewDidAppear() {
self.view.window?.isMovableByWindowBackground = true
}
Thank you! Dimitris' answer solved my issue but I needed it in swift 5. Here is what I came up with.
final class BlahField: NSTextView {
var movingWindow = false
override func mouseDown(with event: NSEvent) {
movingWindow = false
let point = self.convert(event.locationInWindow, from: nil)
if (self.window!.isMovableByWindowBackground && self.frame.contains(point)) {
self.window?.performDrag(with: event)
movingWindow = true
return
}
}
override func mouseDragged(with event: NSEvent) {
if (movingWindow) {
return
}
}
}