discuss-gnustep
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: scaling, rotating, flipping subview


From: H. Nikolaus Schaller
Subject: Re: scaling, rotating, flipping subview
Date: Mon, 9 Dec 2019 11:33:37 +0100

> Am 09.12.2019 um 10:22 schrieb David Chisnall <gnustep@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];
}






reply via email to

[Prev in Thread] Current Thread [Next in Thread]