> Am 09.12.2019 um 10:22 schrieb David Chisnall <gnus...@theravensnest.org>: > > Hi, > > The problem that you identify (input and output coordinate spaces are > different) is also a problem with CoreAnimation and with X11's XRENDER > extension: in both you can apply an affine transform to the output, but input > coordinates are not transformed. > > This is something that you can work around by modifying the responder chain. > NSView is an NSResponder and is responsible for transforming input events > into the child view's coordinate space before delegating them. You can apply > the inverse of the affine transform to the coordinates of the event (after > determining which view actually handles them).
Looks quite fragile... And I am a friend of the KISS principle. Do you know some example code that is general enough? I have attached my current experimental code, maybe it becomes more clear what I want to do. To use it: 1. add a (simple) header file 2. make the NSScaleRotateFlipView a subview/documentView of an NSClipView embedded in some NSScrollView 3. add a subview to the NSScaleRotateFlipView, e.g. an NSImageView 4. connect buttons to the action methods BR, Nikolaus > > David > > On 08/12/2019 19:17, H. Nikolaus Schaller wrote: >> Hi, >> I am currently working on some CAD tool for GNUstep/mySTEP >> and for that I would need a NSView class that can become >> the documentView of a NSClipView, embedded in some >> NSScrollView. And the view class I am looking for should >> allow to rotate, flip and scale a subview (where I do the >> drawing). >> There is no standard class which can do that in Cocoa or OpenSTEP. >> I have experimented a little on Cocoa and got scaling work >> (by setting the bounds of the drawing view scaled relative to >> its frame) but flipping and rotation is difficult to achieve. >> It partially works with setBoundsRotation or scaleUnitSquareToSize, >> but as a side-effect that breaks operation of the scrollers of >> the NSScrollView. >> Scroller size and position seems to assume that the frame and >> bounds are not rotated so that changing the bounds origin can >> simply move around the view under the NSClipView. >> The standard recommendation is to set a transform matrix in >> drawRect: and by that I could make drawing work, but coordinate >> transforms for mouse clicks do not take this into account. >> And scrollers do not adjust for different scaling. >> Finally, this is not a general approach which can rotate, >> flip and scale an arbitrary subview. >> Before I invest more time in this topic, I'd like to ask >> if someone knows an open source implementation of such a >> general NSView subclass. >> Thanks, >> Nikolaus > @implementation NSScaleRotateFlipView - (id) initWithFrame:(NSRect)frame { if((self = [super initWithFrame:frame])) { [self setAutoresizingMask:0]; // do not use autoresizing - setFrame/setScale also define new bounds [self setScale:1.0]; // initialize bounds } return self; } - (void) viewDidMoveToSuperview { // set default scale - we now have a superview and enclosingScrollView [self setScale:10.0]; } - (BOOL) wantsDefaultClipping; { return NO; } // do not clip subview to our bounds - (BOOL) isFlipped; { return _isFlipped; } - (void) setFlipped:(BOOL) flag; { _isFlipped=flag; [self setNeedsDisplay:YES]; } - (NSView *) contentView; { NSArray *a=[self subviews]; return [a count] ? [a objectAtIndex:0]:nil; } - (void) setContentView:(NSView *) object; { // replace subview or add subview NSView *cv=[self contentView]; if(!cv) [self addSubview:object]; else if(cv != object) [self replaceSubview:cv with:object]; [self setNeedsDisplay:YES]; } - (NSPoint) center { // get center of currently visible area NSScrollView *scrollView=[self enclosingScrollView]; if(scrollView) { NSClipView *clipView=[scrollView contentView]; NSRect cvbounds=[clipView bounds]; NSPoint cvcenter=NSMakePoint(NSMidX(cvbounds), NSMidY(cvbounds)); NSPoint center=[self convertPoint:cvcenter fromView:clipView]; return center; } return NSZeroPoint; } - (void) setFrame:(NSRect) frame { NSClipView *clipView=[[self enclosingScrollView] contentView]; NSView *cv=[self contentView]; if(clipView && cv) { NSRect clipFrame=[clipView frame]; // "window" of ClipView NSRect frame=clipFrame, bounds; NSRect area=[cv bounds]; // document bounds frame.size.width *= _scale; frame.size.height *= _scale; // CHECKME: there may be an upper limit how big frame and bounds can become! [super setFrame:frame]; bounds.size.width=clipFrame.size.width; bounds.size.height=clipFrame.size.height; bounds.origin.x=NSMidX(area)-0.5*bounds.size.width; // center area bounds.origin.y=NSMidY(area)-0.5*bounds.size.height; [self setBounds:bounds]; // apply scaling #if 1 [cv setFrameRotation:_rotationAngle]; double rad=M_PI*_rotationAngle/180; double s=sin(rad); double c=cos(rad); bounds.origin.x += 0.5*bounds.size.width*(1-c) + 0.5*bounds.size.height*s; bounds.origin.y += 0.5*bounds.size.height*(1-c) - 0.5*bounds.size.width*s; [cv setFrame:bounds]; #else // does not work properly NSPoint center=[self center]; // [cv translateOriginToPoint:center]; [cv setBoundsRotation:_rotationAngle]; // [self scaleUnitSquareToSize:NSMakeSize((_flags & SHOW_HFLIPPED)?-1.0:1.0, (_flags & SHOW_VFLIPPED)?-1.0:1.0)]; // [cv translateOriginToPoint:NSMakePoint(-center.x, -center.y)]; #endif } else [super setFrame:frame]; } - (float) scale; { return _scale; } - (void) setScale:(float) scale { if(scale < 1e-3 || scale > 1e3) return; // ignore _scale=scale; [self setFrame:[self frame]]; // trigger update of bounds [self setNeedsDisplay:YES]; } - (void) zoom:(float) factor atCenter:(NSPoint) center { // zoom to absolute scale NSScrollView *scrollView=[self enclosingScrollView]; if(scrollView) { NSClipView *clipView=[scrollView contentView]; NSRect bounds=[self bounds]; NSPoint origin; [self setScale:factor]; // may change our bounds! bounds=[self bounds]; // scaled bounds origin.x = 0.5*NSWidth(bounds)*(factor-1.0) + factor*(center.x - NSMidX(bounds)); origin.y = 0.5*NSHeight(bounds)*(factor-1.0) + factor*(center.y - NSMidY(bounds)); [clipView scrollToPoint:origin]; // irgendwie beachten ob der subview geflippt ist! // das wirkt sich auf den scroller aus [scrollView reflectScrolledClipView:clipView]; [self setNeedsDisplay:YES]; } else [self setScale:factor]; } - (void) zoomRectToVisible:(NSRect) area; { NSRect frame=[[[self enclosingScrollView] contentView] frame]; if(!NSIsEmptyRect(area)) [self zoom:0.5*MIN(NSWidth(frame)/NSWidth(area), NSHeight(frame)/NSHeight(area)) atCenter:(NSPoint) { NSMidX(area), NSMidY(area) }]; } - (int) rotationAngle; { return _rotationAngle; } - (void) setRotationAngle:(int) angle; { _rotationAngle = ((angle % 360) + 360) % 360; // also works for negative values [self setFrame:[self frame]]; // trigger update of bounds [self setNeedsDisplay:YES]; } /* menu actions */ - (IBAction) center:(id) sender; { // center the main view (independently of scaling) NSRect area=[[self contentView] frame]; [self zoom:[self scale] atCenter:NSMakePoint(NSMidX(area), NSMidY(area))]; } - (IBAction) zoomFit:(id) sender; { NSRect area=[[self contentView] frame]; NSRect frame=[[[self enclosingScrollView] contentView] frame]; // NSClipView frame if(!NSIsEmptyRect(area)) [self zoom:MIN(NSWidth(frame)/NSWidth(area), NSHeight(frame)/NSHeight(area)) atCenter:NSMakePoint(NSMidX(area), NSMidY(area))]; } - (IBAction) zoomIn:(id) sender; { [self zoom:sqrt(2.0)*[self scale] atCenter:[self center]]; } - (IBAction) zoomOut:(id) sender; { [self zoom:sqrt(0.5)*[self scale] atCenter:[self center]]; } - (IBAction) zoomUnity:(id) sender; { [self setScale:1.0]; [self center:sender]; } - (IBAction) rotateImageLeft:(id) sender; { [self setRotationAngle:[self rotationAngle]+10]; } - (IBAction) rotateImageRight:(id) sender; { [self setRotationAngle:[self rotationAngle]-10]; } - (IBAction) rotateNormal:(id) sender; { [self setRotationAngle:0]; } - (IBAction) flipHorizontal:(id) sender; { [self flipVertical:sender]; [self rotateImageLeft:sender]; [self rotateImageLeft:sender]; } - (IBAction) flipVertical:(id) sender; { [self setFlipped:![self isFlipped]]; } - (IBAction) unflip:(id) sender; { [self setFlipped:NO]; }