I am looking for a way how to constrain the NSDraggingItem position during dragging. I first checked if the enumerateDraggingItemsWithOptions: method could help. I tried to set the draggingFrame of each enumerated item to (my) snap position. It doesn't work. The documentation says that: The dragging frame provides the spatial relationship between NSDraggingItem instances when the dragging formation is set to NSDraggingFormationNone, and it seems like there is no way how to do it. Actually, any changes to the draggingFrame change the position of the dragging item(s). I call the enumerateDraggingItemsWithOptions: from - (NSDragOperation)draggingUpdated: (id)sender.
Thank you for any hint.
[sender enumerateDraggingItemsWithOptions: NSDraggingItemEnumerationConcurrent
forView: self
classes: #[NSPasteboardItem.class]
searchOptions: #{NSPasteboardURLReadingFileURLsOnlyKey: #(NO)}
usingBlock: ^(NSDraggingItem * _Nonnull draggingItem, NSInteger idx, BOOL * _Nonnull stop) {
NSRect newFrame = NSMakeRect(snapOrigin.x, snapOrigin, draggingItem.draggingFrame.size.width, draggingItem.draggingFrame.size.height);
draggingItem.draggingFrame = newFrame;
}];
Related
Is it possible to determine if the "Displays have separate spaces" option is checked in OSX Mavericks? I found the option is stored in com.apple.spaces.plist with name "spans-displays" but this code doesn't work with sandboxing:
NSUserDefaults *userDefaults = [[NSUserDefaults alloc] init];
[userDefaults addSuiteNamed:#"com.apple.spaces"];
NSLog(#"%i", [[userDefaults objectForKey:#"spans-displays"] integerValue]);
[userDefaults release];
Thanks!
To my knowledge there is no simple API to discover this, Apple have never provided comprehensive API relating to Spaces.
However, with a bit of lateral thinking you can figure it out.
What is a distinctive feature of displays having separate spaces?
There are multiple menubars.
So "Are there multiple menubars?" has the same answer as "Do displays have separate spaces?"
Is there an API to tell you if a screen has a menubar?
Again, not to my knowledge, but can we figure it out?
NSWindow has an instance method constrainFrameRect:toScreen: which given a rectangle, representing a window frame, and a screen returns an adjusted rectangle where at least part of the rectangle is visible on the screen. Furthermore, by definition if the top edge of the rectangle is above the menubar area the rectangle will be adjusted so the top edge abuts the menubar...
Which means if we pass it a rectangle abutting the top edge of the screen the returned rectangle will abut the top edge of the menubar, provided there is a menubar. If there is no menubar then the returned rectangle will have the same top edge.
So we can determine if there is a menubar and its height. One small wrinkle, as constrainFrameRect:toScreen: is an instance method we need a window, any window, to make our code work.
Here is one way to code this as a function:
CGFloat menuBarHeight(NSScreen *screen)
{
// A dummy window so we can call constrainFrameRect:toScreen
NSWindow *dummy = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 100, 100)
styleMask:NSTitledWindowMask
backing:NSBackingStoreBuffered defer:YES];
// create a small rectangle at the top left corner of the screen
NSRect screenFrame = screen.frame;
NSRect testFrame = NSMakeRect(NSMinX(screenFrame), NSMaxY(screenFrame)-30, 30, 30);
// constrain the rectangle to be visible
NSRect constrainedFrame = [dummy constrainFrameRect:testFrame toScreen:screen];
// did the top edge move? delta = 0 -> no, delta > 0 - yes and by the height of the menubar
CGFloat delta = NSMaxY(testFrame) - NSMaxY(constrainedFrame);
return delta;
}
So now we can tell if a particular screen has a menubar. How about more than one screen?
Well NSScreen's class method screens returns an array of all the available screens, so all we need to do is call our menuBarHeight function on each screen and see how many menubars we find.
If we find more than 1 then we've determined that displays have separate spaces.
Here is one way to code that, again as a function:
BOOL haveIndepenantScreens()
{
BOOL foundMenuBar = NO;
for (NSScreen *aScreen in NSScreen.screens)
{
if (menuBarHeight(aScreen) > 0)
{
if (foundMenuBar)
// second menu bar found
return YES;
else
// record found first menubar
foundMenuBar = YES;
}
}
return NO; // did not find multiple menubars
}
Job done :-)
use
NSScreen.screensHaveSeparateSpaces
Not sure when this appeared in the documents, but it is there as of 2021!
One advantage of this over #CRD's excellent answer is that it works even if the user has selected 'Automatically hide and show the menu bar'
credit for the answer to #chockenberry
Is there anyway to get coordinates for the current position of the keyboard cursor (caret) globally like you can for the mouse cursor position with mouseLocation?
No, there is no way to do it globally.
If you want to do it in your own app, like in an NSTextView, you'd do it like this:
NSRange range = [textView selectedRange];
NSRange newRange = [[textView layoutManager] glyphRangeForCharacterRange:range actualCharacterRange:NULL];
NSRect rect = [[textView layoutManager] boundingRectForGlyphRange:newRange inTextContainer:[textView textContainer]];
rect would be the rect of the selected text, or in the case where there is just an insertion point but no selection, rect.origin is the view-relative location of the insertion point.
The closest you can get would be to use OS X's Accessibility Protocol. This is intended to help disabled users operate the computer, but many applications don't support it, or do not support it very well.
The procedure would be something like:
appRef = AXUIElementCreateApplication(appPID);
focusElemRef = AXUIElementCopyAttributeValue(appRef,kAXFocusedUIElementAttribute, &theValue)
AXUIElementCopyAttributeValue(focusElemRef, kAXSelectedTextRangeAttribute, &selRangeValue);
AXUIElementCopyParameterizedAttributeValue(focusElemRef, kAXBoundsForRangeParameterizedAttribute, adjSelRangeValue, &boundsValue);
Due to the spotty support for the protocol, with many applications you won't get beyond the FocusedUIElementAttribute step, but this does work with some applications.
You can do this easily in macOS 10.0 and up.
For an NSTextView, override the drawInsertionPointInRect:color:turnedOn: method. To translate the caret position relative to the window, use the convertPoint:toView: method. Finally, you can store the translated position in an instance variable.
#interface MyTextView : NSTextView
#end
#implementation MyTextView
{
NSPoint _caretPositionInWindow;
}
- (void)drawInsertionPointInRect:(CGRect)rect color:(NSColor *)color turnedOn:(BOOL)flag
{
[super drawInsertionPointInRect:rect color:color turnedOn:flag];
_caretPositionInWindow = [self convertPoint:rect.origin toView:nil];
}
#end
As a test I made one layout that displays cells in a vertical line and another that displays them in a horizontal layout. When I call [collectionView setCollectionViewLayout:layout animated:YES]; it animates between the two positions very cleanly.
Now that I'd like to do is have all the views do a few spins, warps and flips around the screen (probably using CAKeyframeAnimations) before finally arriving at their destination, but I can't find a good place to hook this in.
I tried subclassing UICollectionViewLayoutAttributes to contain an animation property, then setting those animations in an overridden applyLayoutAttributes: method of the UICollectionViewCell I'm using. This works... EXCEPT it appears to happen only after the layout transition is complete. If I wanted to use this, I'd have to have the layout not change the current positions of the objects right away, only after the it reaches this apply attributes part of the code, and that seems like a lot of work...
Or I could subclass UICollectionView and override setCollectionViewLayout:animated:, but that also seems like a lot of state to keep around between layouts. Neither of these optins seems right, because there's such an easy way to animate additions/deletions of cells within a layout. I feel like there should be something similar for hooking into the animations between layouts.
Does anyone know the best way to get what I'm looking to accomplish?
#define degreesToRadians(x) (M_PI * (x) / 180.0)
UICollectionView *collectionView = self.viewController.collectionView;
HorizontalCollectionViewLayout *horizontalLayout = [HorizontalCollectionViewLayout new];
NSTimeInterval duration = 2;
[collectionView.visibleCells enumerateObjectsUsingBlock:^(UICollectionViewCell *cell, NSUInteger index, BOOL *stop)
{
CABasicAnimation *rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
rotationAnimation.toValue = #(degreesToRadians(360));
rotationAnimation.duration = duration;
rotationAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[cell.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
}];
[UIView animateWithDuration:duration
animations:^
{
collectionView.collectionViewLayout = horizontalLayout;
}];
To customize the default blue gradient highlight style I made a subclass of NSOutlineView and overrode the method -highlightSelectionInClipRect, like so:
- (void)highlightSelectionInClipRect:(NSRect)theClipRect
{
NSRange aVisibleRowIndexes = [self rowsInRect:theClipRect];
NSIndexSet *aSelectedRowIndexes = [self selectedRowIndexes];
NSInteger aRow = aVisibleRowIndexes.location;
NSInteger anEndRow = aRow + aVisibleRowIndexes.length;
for (int aRow; aRow < anEndRow; aRow++) {
if([aSelectedRowIndexes containsIndex:aRow]) {
// draw gradient
}
}
}
This works fine, but sometimes the background is not drawn. On the screenshot below you can see how the selection highlight is not drawn when clicking on the first item after the last one was selected.
It seems as if this only happens if the new selected item is not directly beneath or above the old selected one. Selecting five items in the order 1-2-3-4-5-4-3-2-1 always draws the appropriate background, anything else (e.g. 1-2-5) not.
Why is this happening? If you need any more details I will be glad to add some more code, but at the meantime I have no clue where to search for this behaviour.
here is my (very simple) solution using blocks:
- (void)highlightSelectionInClipRect:(NSRect)clipRect
{
[[self selectedRowIndexes] enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop)
{
// draw gradient
}];
}
I have an NSView which I am adding as a sub-view of another NSView. I want to be able to drag the first NSView around the parent view. I have some code that is partially working but there's an issue with the NSView moving in the opposite direction on the Y axis from my mouse drag. (i.e. I drag down, it goes up and the inverse of that).
Here's my code:
// -------------------- MOUSE EVENTS ------------------- \\
- (BOOL) acceptsFirstMouse:(NSEvent *)e {
return YES;
}
- (void)mouseDown:(NSEvent *) e {
//get the mouse point
lastDragLocation = [e locationInWindow];
}
- (void)mouseDragged:(NSEvent *)theEvent {
NSPoint newDragLocation = [theEvent locationInWindow];
NSPoint thisOrigin = [self frame].origin;
thisOrigin.x += (-lastDragLocation.x + newDragLocation.x);
thisOrigin.y += (-lastDragLocation.y + newDragLocation.y);
[self setFrameOrigin:thisOrigin];
lastDragLocation = newDragLocation;
}
The view is flipped, though I changed that back to the default and it didn't seem to make a difference. What am I doing wrong?
The best way to approach this problem is by starting with a solid understanding of coordinate spaces.
First, it is critical to understand that when we talk about the "frame" of a window, it is in the coordinate space of the superview. This means that adjusting the flippedness of the view itself won't actually make a difference, because we haven't been changing anything inside the view itself.
But your intuition that the flippedness is important here is correct.
By default your code, as typed, seems like it would work; perhaps your superview has been flipped (or not flipped), and it is in a different coordinate space than you expect.
Rather than just flipping and unflipping views at random, it is best to convert the points you're dealing with into a known coordinate space.
I've edited your above code to always convert into the superview's coordinate space, because we're working with the frame origin. This will work if your draggable view is placed in a flipped, or non-flipped superview.
// -------------------- MOUSE EVENTS ------------------- \\
- (BOOL) acceptsFirstMouse:(NSEvent *)e {
return YES;
}
- (void)mouseDown:(NSEvent *) e {
// Convert to superview's coordinate space
self.lastDragLocation = [[self superview] convertPoint:[e locationInWindow] fromView:nil];
}
- (void)mouseDragged:(NSEvent *)theEvent {
// We're working only in the superview's coordinate space, so we always convert.
NSPoint newDragLocation = [[self superview] convertPoint:[theEvent locationInWindow] fromView:nil];
NSPoint thisOrigin = [self frame].origin;
thisOrigin.x += (-self.lastDragLocation.x + newDragLocation.x);
thisOrigin.y += (-self.lastDragLocation.y + newDragLocation.y);
[self setFrameOrigin:thisOrigin];
self.lastDragLocation = newDragLocation;
}
Additionally, I'd recommend refactoring your code to simply deal with the original mouse-down location, and the current location of the pointer, rather than deal with the deltas between mouseDragged events. This could lead to unexpected results down the line.
Instead simply store the offset between the origin of dragged view and the mouse pointer (where the mouse pointer is within the view), and set the frame origin to the location of the mouse pointer, minus the offset.
Here is some additional reading:
Cocoa Drawing Guide
Cocoa Event Handling Guide
I think you should calculate according to the position of mouse, cause according to my test,it gets more smooth.Because The way like below only provide the position inside the application's window coordinate system:
[[self superview] convertPoint:[theEvent locationInWindow] fromView:nil];
What I am suggesting is something like this:
lastDrag = [NSEvent mouseLocation];
other codes are just the same.