Merge pull request #12697 from tellowkrinkle:FasterCocoaWindows
authortellowkrinkle <tellowkrinkle@users.noreply.github.com>
Sun, 14 Oct 2018 18:18:05 +0000 (13:18 -0500)
committerAlexander Alekhin <alexander.a.alekhin@gmail.com>
Sun, 14 Oct 2018 18:18:05 +0000 (21:18 +0300)
* Make cocoa windows draw faster

* Use a CALayer for rendering when possible Uses GPU to scale images, which is important because retina macs will want window sizes much larger (in pixels) than the image

* Fix mouse logic for cocoa windows

* Only halve resolution on retina if image is larger than display

modules/highgui/src/window_cocoa.mm

index 73959fd..4e6da5a 100644 (file)
@@ -74,7 +74,6 @@ CV_IMPL int cvWaitKey (int maxWait) {return 0;}
 
 #include <iostream>
 
-const int TOP_BORDER  = 7;
 const int MIN_SLIDER_WIDTH=200;
 
 static NSApplication *application = nil;
@@ -82,10 +81,10 @@ static NSAutoreleasePool *pool = nil;
 static NSMutableDictionary *windows = nil;
 static bool wasInitialized = false;
 
-@interface CVView : NSView {
-    NSImage *image;
-}
+@interface CVView : NSView
+@property(retain) NSView *imageView;
 @property(retain) NSImage *image;
+@property int sliderHeight;
 - (void)setImageData:(CvArr *)arr;
 @end
 
@@ -221,32 +220,38 @@ CV_IMPL void cvShowImage( const char* name, const CvArr* arr)
     if(window)
     {
         bool empty = [[window contentView] image] == nil;
-        NSRect rect = [window frame];
         NSRect vrectOld = [[window contentView] frame];
-
+        NSSize oldImageSize = [[[window contentView] image] size];
         [[window contentView] setImageData:(CvArr *)arr];
         if([window autosize] || [window firstContent] || empty)
         {
-            //Set new view size considering sliders (reserve height and min width)
-            NSRect vrectNew = vrectOld;
-            int slider_height = 0;
-            if ([window respondsToSelector:@selector(sliders)]) {
-                for(NSString *key in [window sliders]) {
-                    slider_height += [[[window sliders] valueForKey:key] frame].size.height;
+            NSSize imageSize = [[[window contentView] image] size];
+            // Only adjust the image size if the new image is a different size from the previous
+            if (oldImageSize.height != imageSize.height || oldImageSize.width != imageSize.width)
+            {
+                //Set new view size considering sliders (reserve height and min width)
+                NSSize scaledImageSize;
+                if ([[window contentView] respondsToSelector:@selector(convertSizeFromBacking:)])
+                {
+                    // Only resize for retina displays if the image is bigger than the screen
+                    NSSize screenSize = NSScreen.mainScreen.visibleFrame.size;
+                    CGFloat titleBarHeight = window.frame.size.height - [window contentRectForFrameRect:window.frame].size.height;
+                    screenSize.height -= titleBarHeight;
+                    if (imageSize.width > screenSize.width || imageSize.height > screenSize.height)
+                    {
+                        scaledImageSize = [[window contentView] convertSizeFromBacking:imageSize];
+                    }
+                }
+                else
+                {
+                    scaledImageSize = imageSize;
                 }
+                NSSize contentSize = vrectOld.size;
+                contentSize.height = scaledImageSize.height + [window contentView].sliderHeight;
+                contentSize.width = std::max<int>(scaledImageSize.width, MIN_SLIDER_WIDTH);
+                [window setContentSize:contentSize]; //adjust sliders to fit new window size
             }
-            vrectNew.size.height = [[[window contentView] image] size].height + slider_height;
-            vrectNew.size.width = std::max<int>([[[window contentView] image] size].width, MIN_SLIDER_WIDTH);
-            [[window contentView]  setFrameSize:vrectNew.size]; //adjust sliders to fit new window size
-
-            rect.size.width += vrectNew.size.width - vrectOld.size.width;
-            rect.size.height += vrectNew.size.height - vrectOld.size.height;
-            rect.origin.y -= vrectNew.size.height - vrectOld.size.height;
-
-            [window setFrame:rect display:YES];
         }
-        else
-            [window display];
         [window setFirstContent:NO];
     }
     [localpool drain];
@@ -259,10 +264,9 @@ CV_IMPL void cvResizeWindow( const char* name, int width, int height)
     NSAutoreleasePool* localpool = [[NSAutoreleasePool alloc] init];
     CVWindow *window = cvGetWindow(name);
     if(window && ![window autosize]) {
-        NSRect frame = [window frame];
-        frame.size.width = width;
-        frame.size.height = height;
-        [window setFrame:frame display:YES];
+        height += [window contentView].sliderHeight;
+        NSSize size = { width, height };
+        [window setContentSize:size];
     }
     [localpool drain];
 }
@@ -532,7 +536,7 @@ CV_IMPL int cvNamedWindow( const char* name, int flags )
     NSScreen* mainDisplay = [NSScreen mainScreen];
 
     NSString *windowName = [NSString stringWithFormat:@"%s", name];
-    NSUInteger showResize = (flags == CV_WINDOW_AUTOSIZE) ? 0: NSResizableWindowMask ;
+    NSUInteger showResize = NSResizableWindowMask;
     NSUInteger styleMask = NSTitledWindowMask|NSMiniaturizableWindowMask|showResize;
     CGFloat windowWidth = [NSWindow minFrameWidthWithTitle:windowName styleMask:styleMask];
     NSRect initContentRect = NSMakeRect(0, 0, windowWidth, 0);
@@ -728,6 +732,22 @@ void cv::setWindowTitle(const String& winname, const String& title)
     [localpool drain];
 }
 
+static NSSize constrainAspectRatio(NSSize base, NSSize constraint) {
+    CGFloat heightDiff = (base.height / constraint.height);
+    CGFloat widthDiff = (base.width / constraint.width);
+    if (widthDiff == heightDiff) {
+        return base;
+    }
+    else if (widthDiff > heightDiff) {
+        NSSize out = { constraint.width / constraint.height * base.height, base.height };
+        return out;
+    }
+    else {
+        NSSize out = { base.width, constraint.height / constraint.width * base.width };
+        return out;
+    }
+}
+
 @implementation CVWindow
 
 @synthesize mouseCallback;
@@ -743,22 +763,19 @@ void cv::setWindowTitle(const String& winname, const String& title)
     NSPoint mp = [NSEvent mouseLocation];
     //NSRect visible = [[self contentView] frame];
     mp = [self convertScreenToBase: mp];
-    double viewHeight = [self contentView].frame.size.height;
-    double viewWidth = [self contentView].frame.size.width;
-    CVWindow *window = (CVWindow *)[[self contentView] window];
-    if ([window respondsToSelector:@selector(sliders)]) {
-        for(NSString *key in [window sliders]) {
-            NSSlider *slider = [[window sliders] valueForKey:key];
-            viewHeight = std::min(viewHeight, (double)([slider frame].origin.y));
-        }
+    CVView *contentView = [self contentView];
+    NSSize viewSize = contentView.frame.size;
+    if (contentView.imageView) {
+        viewSize = contentView.imageView.frame.size;
+    }
+    else {
+        viewSize.height -= contentView.sliderHeight;
     }
-    viewHeight -= TOP_BORDER;
-    mp.y = viewHeight - mp.y;
+    mp.y = viewSize.height - mp.y;
 
-    NSImage* image = ((CVView*)[self contentView]).image;
-    NSSize imageSize = [image size];
-    mp.x = mp.x * imageSize.width / std::max(viewWidth, 1.);
-    mp.y = mp.y * imageSize.height / std::max(viewHeight, 1.);
+    NSSize imageSize = contentView.image.size;
+    mp.y *= (imageSize.height / std::max(viewSize.height, 1.));
+    mp.x *= (imageSize.width / std::max(viewSize.width, 1.));
 
     if( mp.x >= 0 && mp.y >= 0 && mp.x < imageSize.width && mp.y < imageSize.height )
         mouseCallback(type, mp.x, mp.y, flags, mouseParam);
@@ -862,17 +879,14 @@ void cv::setWindowTitle(const String& winname, const String& title)
     viewSize.width = std::max<int>(viewSize.width, MIN_SLIDER_WIDTH);
 
     // Update slider sizes
-    [[self contentView] setFrameSize:viewSize];
-    [[self contentView] setNeedsDisplay:YES];
-
-    //update window size to contain sliders
-    NSRect rect = [self frame];
-    rect.size.height += [slider frame].size.height;
-    rect.size.width = std::max<int>(rect.size.width, MIN_SLIDER_WIDTH);
-    [self setFrame:rect display:YES];
-
+    [self contentView].sliderHeight += sliderSize.height;
 
+    if ([[self contentView] image] && ![[self contentView] imageView]) {
+        [[self contentView] setNeedsDisplay:YES];
+    }
 
+    //update window size to contain sliders
+    [self setContentSize: viewSize];
 }
 
 - (CVView *)contentView {
@@ -888,20 +902,15 @@ void cv::setWindowTitle(const String& winname, const String& title)
 - (id)init {
     //cout << "CVView init" << endl;
     [super init];
-    image = [[NSImage alloc] init];
     return self;
 }
 
 - (void)setImageData:(CvArr *)arr {
     //cout << "setImageData" << endl;
     NSAutoreleasePool* localpool = [[NSAutoreleasePool alloc] init];
-    CvMat *arrMat, *cvimage, stub;
+    CvMat *arrMat, dst, stub;
 
     arrMat = cvGetMat(arr, &stub);
-
-    cvimage = cvCreateMat(arrMat->rows, arrMat->cols, CV_8UC3);
-    cvConvertImage(arrMat, cvimage, CV_CVTIMG_SWAP_RB);
-
     /*CGColorSpaceRef colorspace = NULL;
     CGDataProviderRef provider = NULL;
     int width = cvimage->width;
@@ -922,42 +931,77 @@ void cv::setWindowTitle(const String& winname, const String& title)
     }*/
 
     NSBitmapImageRep *bitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
-                pixelsWide:cvimage->width
-                pixelsHigh:cvimage->height
+                pixelsWide:arrMat->cols
+                pixelsHigh:arrMat->rows
                 bitsPerSample:8
                 samplesPerPixel:3
                 hasAlpha:NO
                 isPlanar:NO
                 colorSpaceName:NSDeviceRGBColorSpace
-                bytesPerRow:(cvimage->width * 4)
-                bitsPerPixel:32];
-
-    int        pixelCount = cvimage->width * cvimage->height;
-    unsigned char *src = cvimage->data.ptr;
-    unsigned char *dst = [bitmap bitmapData];
+                bitmapFormat: kCGImageAlphaNone
+                bytesPerRow:((arrMat->cols * 3 + 3) & -4)
+                bitsPerPixel:24];
 
-    for( int i = 0; i < pixelCount; i++ )
-    {
-        dst[i * 4 + 0] = src[i * 3 + 0];
-        dst[i * 4 + 1] = src[i * 3 + 1];
-        dst[i * 4 + 2] = src[i * 3 + 2];
+    if (bitmap) {
+        cvInitMatHeader(&dst, arrMat->rows, arrMat->cols, CV_8UC3, [bitmap bitmapData], [bitmap bytesPerRow]);
+        cvConvertImage(arrMat, &dst, CV_CVTIMG_SWAP_RB);
+    }
+    else {
+        // It's not guaranteed to like the bitsPerPixel:24, but this is a lot slower so we'd rather not do it
+        bitmap = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
+            pixelsWide:arrMat->cols
+            pixelsHigh:arrMat->rows
+            bitsPerSample:8
+            samplesPerPixel:3
+            hasAlpha:NO
+            isPlanar:NO
+            colorSpaceName:NSDeviceRGBColorSpace
+            bytesPerRow:(arrMat->cols * 4)
+            bitsPerPixel:32];
+        uint8_t *data = [bitmap bitmapData];
+        cvInitMatHeader(&dst, arrMat->rows, arrMat->cols, CV_8UC3, data, (arrMat->cols * 3));
+        cvConvertImage(arrMat, &dst, CV_CVTIMG_SWAP_RB);
+        for (int i = (arrMat->rows * arrMat->cols) - 1; i >= 0; i--) {
+            memmove(data + i * 4, data + i * 3, 3);
+            data[i * 4 + 3] = 0;
+        }
     }
 
-    if( image )
+    if( image ) {
         [image release];
+    }
 
     image = [[NSImage alloc] init];
     [image addRepresentation:bitmap];
     [bitmap release];
 
+    // This isn't supported on older versions of macOS
+    // The performance issues this solves are mainly on newer versions of macOS, so that's fine
+    if( floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_5 ) {
+        if (![self imageView]) {
+            [self setImageView:[[NSView alloc] init]];
+            [[self imageView] setWantsLayer:true];
+            [self addSubview:[self imageView]];
+        }
+
+        [[[self imageView] layer] setContents:image];
+
+        NSRect imageViewFrame = [self frame];
+        imageViewFrame.size.height -= [self sliderHeight];
+        NSRect constrainedFrame = { imageViewFrame.origin, constrainAspectRatio(imageViewFrame.size, [image size]) };
+        [[self imageView] setFrame:constrainedFrame];
+    }
+    else {
+        NSRect redisplayRect = [self frame];
+        redisplayRect.size.height -= [self sliderHeight];
+        [self setNeedsDisplayInRect:redisplayRect];
+    }
+
     /*CGColorSpaceRelease(colorspace);
     CGDataProviderRelease(provider);
     CGImageRelease(imageRef);*/
-    cvReleaseMat(&cvimage);
-    [localpool drain];
-
-    [self setNeedsDisplay:YES];
 
+    [localpool drain];
 }
 
 - (void)setFrameSize:(NSSize)size {
@@ -970,41 +1014,49 @@ void cv::setWindowTitle(const String& winname, const String& title)
     CVWindow *cvwindow = (CVWindow *)[self window];
     if ([cvwindow respondsToSelector:@selector(sliders)]) {
         for(NSString *key in [cvwindow sliders]) {
-            NSSlider *slider = [[cvwindow sliders] valueForKey:key];
+            CVSlider *slider = [[cvwindow sliders] valueForKey:key];
             NSRect r = [slider frame];
             r.origin.y = height - r.size.height;
             r.size.width = [[cvwindow contentView] frame].size.width;
+
+            CGRect sliderRect = slider.slider.frame;
+            CGFloat targetWidth = r.size.width - (sliderRect.origin.x + 10);
+            sliderRect.size.width = targetWidth < 0 ? 0 : targetWidth;
+            slider.slider.frame = sliderRect;
+
             [slider setFrame:r];
             height -= r.size.height;
         }
     }
+    NSRect frame = self.frame;
+    if (frame.size.height < self.sliderHeight) {
+        frame.size.height = self.sliderHeight;
+        self.frame = frame;
+    }
+    if ([self imageView]) {
+        NSRect imageViewFrame = frame;
+        imageViewFrame.size.height -= [self sliderHeight];
+        NSRect constrainedFrame = { imageViewFrame.origin, constrainAspectRatio(imageViewFrame.size, [image size]) };
+        [[self imageView] setFrame:constrainedFrame];
+    }
     [localpool drain];
 }
 
 - (void)drawRect:(NSRect)rect {
     //cout << "drawRect" << endl;
     [super drawRect:rect];
-
-    NSAutoreleasePool* localpool = [[NSAutoreleasePool alloc] init];
-    CVWindow *cvwindow = (CVWindow *)[self window];
-    int height = 0;
-    if ([cvwindow respondsToSelector:@selector(sliders)]) {
-        for(NSString *key in [cvwindow sliders]) {
-            height += [[[cvwindow sliders] valueForKey:key] frame].size.height;
+    // If imageView exists, all drawing will be done by it and nothing needs to happen here
+    if ([self image] && ![self imageView]) {
+        NSAutoreleasePool* localpool = [[NSAutoreleasePool alloc] init];
+
+        if(image != nil) {
+            [image drawInRect: [self frame]
+                     fromRect: NSZeroRect
+                    operation: NSCompositeSourceOver
+                     fraction: 1.0];
         }
+        [localpool release];
     }
-
-
-    NSRect imageRect = {{0,0}, {[image size].width, [image size].height}};
-
-    if(image != nil) {
-        [image drawInRect: imageRect
-                              fromRect: NSZeroRect
-                             operation: NSCompositeSourceOver
-                              fraction: 1.0];
-    }
-    [localpool release];
-
 }
 
 @end