Reviewed by Kevin Decker.
[WebKit-https.git] / WebKit / Misc / WebNSWindowExtras.m
1 /*
2  * Copyright (C) 2005 Apple Computer, Inc.  All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  *
8  * 1.  Redistributions of source code must retain the above copyright
9  *     notice, this list of conditions and the following disclaimer. 
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *     notice, this list of conditions and the following disclaimer in the
12  *     documentation and/or other materials provided with the distribution. 
13  * 3.  Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14  *     its contributors may be used to endorse or promote products derived
15  *     from this software without specific prior written permission. 
16  *
17  * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20  * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24  * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26  * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27  */
28
29 #import "WebNSWindowExtras.h"
30
31 #import "WebKitLogging.h"
32 #import <JavaScriptCore/Assertions.h>
33 #import <objc/objc-runtime.h>
34
35 #define DISPLAY_REFRESH_INTERVAL (1.0 / 60.0)
36
37 static NSString *WebKitDisplayThrottleRunLoopMode = @"WebKitDisplayThrottleRunLoopMode";
38
39 static BOOL throttlingWindowDisplay;
40 static CFMutableDictionaryRef windowDisplayInfoDictionary;
41 static IMP oldNSWindowPostWindowNeedsDisplayIMP;
42 static IMP oldNSWindowCloseIMP;
43 static IMP oldNSWindowFlushWindowIMP;
44
45 typedef struct {
46     NSWindow *window;
47     CFTimeInterval lastFlushTime;
48     NSTimer *displayTimer;
49 } WindowDisplayInfo;
50
51 @interface NSWindow (WebExtrasInternal)
52 static IMP swizzleInstanceMethod(Class class, SEL selector, IMP newImplementation);
53 static void replacementPostWindowNeedsDisplay(id self, SEL cmd);
54 static void replacementClose(id self, SEL cmd);
55 static void replacementFlushWindow(id self, SEL cmd);
56 static WindowDisplayInfo *getWindowDisplayInfo(NSWindow *window);
57 static BOOL requestWindowDisplay(NSWindow *window);
58 static void cancelPendingWindowDisplay(WindowDisplayInfo *displayInfo);
59 - (void)_webkit_doPendingPostWindowNeedsDisplay:(NSTimer *)timer;
60 @end
61
62 @interface NSWindow (AppKitSecretsIKnow)
63 - (void)_postWindowNeedsDisplay;
64 @end
65
66 @implementation NSWindow (WebExtras)
67
68 + (void)_webkit_enableWindowDisplayThrottle
69 {
70     if (throttlingWindowDisplay)
71         return;
72         
73     windowDisplayInfoDictionary = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, NULL);
74     ASSERT(windowDisplayInfoDictionary);
75
76     // Override -[NSWindow _postWindowNeedsDisplay]
77     ASSERT(!oldNSWindowPostWindowNeedsDisplayIMP);
78     oldNSWindowPostWindowNeedsDisplayIMP = swizzleInstanceMethod(self, @selector(_postWindowNeedsDisplay), (IMP)replacementPostWindowNeedsDisplay);
79     ASSERT(oldNSWindowPostWindowNeedsDisplayIMP);
80     
81     // Override -[NSWindow close]    
82     ASSERT(!oldNSWindowCloseIMP);
83     oldNSWindowCloseIMP = swizzleInstanceMethod(self, @selector(close), (IMP)replacementClose);
84     ASSERT(oldNSWindowCloseIMP);
85
86     // Override -[NSWindow flushWindow]    
87     ASSERT(!oldNSWindowFlushWindowIMP);
88     oldNSWindowFlushWindowIMP = swizzleInstanceMethod(self, @selector(flushWindow), (IMP)replacementFlushWindow);
89     ASSERT(oldNSWindowFlushWindowIMP);
90     
91 //    NSLog(@"Throttling window display to %.3f times per second", 1.0 / DISPLAY_REFRESH_INTERVAL);
92     
93     throttlingWindowDisplay = YES;
94 }
95
96 static void disableWindowDisplayThrottleApplierFunction(const void *key, const void *value, void *context)
97 {
98     WindowDisplayInfo *displayInfo = (WindowDisplayInfo *)value;
99     
100     // Display immediately
101     cancelPendingWindowDisplay(displayInfo);
102     [displayInfo->window _postWindowNeedsDisplay];
103     
104     free(displayInfo);
105 }
106
107 + (void)_webkit_displayThrottledWindows
108 {
109     if (!throttlingWindowDisplay)
110         return;
111
112     // Force all throttle timers to fire by running the runloop in WebKitDisplayThrottleRunLoopMode until there are
113     // no more runloop timers/sources for that mode.
114     while (CFRunLoopRunInMode((CFStringRef)WebKitDisplayThrottleRunLoopMode, 1.0 / DISPLAY_REFRESH_INTERVAL, true) == kCFRunLoopRunHandledSource) {}
115 }
116
117 + (void)_webkit_disableWindowDisplayThrottle
118 {
119     if (!throttlingWindowDisplay)
120         return;
121     
122     //
123     // Restore NSWindow method implementations first.  When window display throttling is disabled, we display any windows
124     // with pending displays.  If the window re-displays during our call to -_postWindowNeedsDisplay, we want those displays to
125     // not be throttled.
126     //
127     
128     // Restore -[NSWindow _postWindowNeedsDisplay]
129     ASSERT(oldNSWindowPostWindowNeedsDisplayIMP);
130     swizzleInstanceMethod(self, @selector(_postWindowNeedsDisplay), oldNSWindowPostWindowNeedsDisplayIMP);
131     oldNSWindowPostWindowNeedsDisplayIMP = NULL;
132     
133     // Restore -[NSWindow close]    
134     ASSERT(oldNSWindowCloseIMP);
135     swizzleInstanceMethod(self, @selector(close), oldNSWindowCloseIMP);
136     oldNSWindowCloseIMP = NULL;
137
138     // Restore -[NSWindow flushWindow]    
139     ASSERT(oldNSWindowFlushWindowIMP);
140     swizzleInstanceMethod(self, @selector(flushWindow), oldNSWindowFlushWindowIMP);
141     oldNSWindowFlushWindowIMP = NULL;
142
143     CFDictionaryApplyFunction(windowDisplayInfoDictionary, disableWindowDisplayThrottleApplierFunction, NULL);
144     CFRelease(windowDisplayInfoDictionary);
145     windowDisplayInfoDictionary = NULL;
146     
147     throttlingWindowDisplay = NO;
148 }
149
150 - (void)centerOverMainWindow
151 {
152     NSRect frameToCenterOver;
153     if ([NSApp mainWindow]) {
154         frameToCenterOver = [[NSApp mainWindow] frame];
155     } else {
156         frameToCenterOver = [[NSScreen mainScreen] visibleFrame];
157     }
158     
159     NSSize size = [self frame].size;
160     NSPoint origin;
161     origin.y = NSMaxY(frameToCenterOver)
162         - (frameToCenterOver.size.height - size.height) / 3
163         - size.height;
164     origin.x = frameToCenterOver.origin.x
165         + (frameToCenterOver.size.width - size.width) / 2;
166     [self setFrameOrigin:origin];
167 }
168
169 @end
170
171 @implementation NSWindow (WebExtrasInternal)
172
173 // Returns the old method implementation
174 static IMP swizzleInstanceMethod(Class class, SEL selector, IMP newImplementation)
175 {
176     Method method = class_getInstanceMethod(class, selector);
177     ASSERT(method);
178     IMP oldIMP;
179 #if OBJC_API_VERSION > 0
180     oldIMP = method_setImplementation(method, newImplementation);
181 #else
182     oldIMP = method->method_imp;
183     method->method_imp = newImplementation;
184 #endif
185     return oldIMP;
186 }
187
188 static void replacementPostWindowNeedsDisplay(id self, SEL cmd)
189 {
190     ASSERT(throttlingWindowDisplay);
191
192     // Do not call into -[NSWindow _postWindowNeedsDisplay] if requestWindowDisplay() returns NO.  In that case, requestWindowDisplay()
193     // will schedule a timer to display at the appropriate time.
194     if (requestWindowDisplay(self))
195         oldNSWindowPostWindowNeedsDisplayIMP(self, cmd);
196 }
197
198 static void replacementClose(id self, SEL cmd)
199 {
200     ASSERT(throttlingWindowDisplay);
201
202     // Remove WindowDisplayInfo for this window
203     WindowDisplayInfo *displayInfo = (WindowDisplayInfo *)CFDictionaryGetValue(windowDisplayInfoDictionary, self);
204     if (displayInfo) {
205         cancelPendingWindowDisplay(displayInfo);
206         free(displayInfo);
207         CFDictionaryRemoveValue(windowDisplayInfoDictionary, self);
208     }
209     
210     oldNSWindowCloseIMP(self, cmd);
211 }
212
213 static void replacementFlushWindow(id self, SEL cmd)
214 {
215     ASSERT(throttlingWindowDisplay);
216
217     oldNSWindowFlushWindowIMP(self, cmd);
218     getWindowDisplayInfo(self)->lastFlushTime = CFAbsoluteTimeGetCurrent();
219 }
220
221 static WindowDisplayInfo *getWindowDisplayInfo(NSWindow *window)
222 {
223     ASSERT(throttlingWindowDisplay);
224     ASSERT(windowDisplayInfoDictionary);
225     
226     // Get the WindowDisplayInfo for this window, or create it if it does not exist
227     WindowDisplayInfo *displayInfo = (WindowDisplayInfo *)CFDictionaryGetValue(windowDisplayInfoDictionary, window);
228     if (!displayInfo) {
229         displayInfo = (WindowDisplayInfo *)malloc(sizeof(WindowDisplayInfo));
230         displayInfo->window = window;
231         displayInfo->lastFlushTime = 0;
232         displayInfo->displayTimer = nil;
233         CFDictionarySetValue(windowDisplayInfoDictionary, window, displayInfo);
234     }
235     
236     return displayInfo;
237 }
238
239 static BOOL requestWindowDisplay(NSWindow *window)
240 {
241     ASSERT(throttlingWindowDisplay);
242
243     // Defer display if there is already a pending display
244     WindowDisplayInfo *displayInfo = getWindowDisplayInfo(window);
245     if (displayInfo->displayTimer)
246         return NO;
247         
248     // Defer display if it hasn't been at least DISPLAY_REFRESH_INTERVAL seconds since the last display
249     CFTimeInterval now = CFAbsoluteTimeGetCurrent();
250     CFTimeInterval timeSinceLastDisplay = now - displayInfo->lastFlushTime;
251     if (timeSinceLastDisplay < DISPLAY_REFRESH_INTERVAL) {
252         // Redisplay soon -- if we redisplay too quickly, we'll block due to pending CG coalesced updates
253         displayInfo->displayTimer = [[NSTimer timerWithTimeInterval:(DISPLAY_REFRESH_INTERVAL - timeSinceLastDisplay)
254                                                              target:window
255                                                            selector:@selector(_webkit_doPendingPostWindowNeedsDisplay:)
256                                                            userInfo:nil
257                                                             repeats:NO] retain];
258         
259         // The NSWindow autodisplay mechanism is documented to only work for NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, and NSModalPanelRunLoopMode
260         NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
261         [runLoop addTimer:displayInfo->displayTimer forMode:NSDefaultRunLoopMode];
262         [runLoop addTimer:displayInfo->displayTimer forMode:NSEventTrackingRunLoopMode];
263         [runLoop addTimer:displayInfo->displayTimer forMode:NSModalPanelRunLoopMode];
264         
265         // Schedule the timer in WebKitDisplayThrottleRunLoopMode so that +_webkit_displayThrottledWindows can
266         // force all window throttle timers to fire by running in just that mode
267         [runLoop addTimer:displayInfo->displayTimer forMode:WebKitDisplayThrottleRunLoopMode];
268         
269         return NO;
270     }
271
272     return YES;
273 }
274
275 static void cancelPendingWindowDisplay(WindowDisplayInfo *displayInfo)
276 {
277     ASSERT(throttlingWindowDisplay);
278
279     if (!displayInfo->displayTimer)
280         return;
281
282     [displayInfo->displayTimer invalidate];
283     [displayInfo->displayTimer release];
284     displayInfo->displayTimer = nil;
285 }
286
287 - (void)_webkit_doPendingPostWindowNeedsDisplay:(NSTimer *)timer
288 {
289     WindowDisplayInfo *displayInfo = getWindowDisplayInfo(self);
290     ASSERT(timer == displayInfo->displayTimer);
291     ASSERT(throttlingWindowDisplay);
292
293     // -_postWindowNeedsDisplay will short-circuit if there is a displayTimer, so invalidate it first.
294     cancelPendingWindowDisplay(displayInfo);
295
296     [self _postWindowNeedsDisplay];
297 }
298
299 @end