-
Notifications
You must be signed in to change notification settings - Fork 5
/
nw_cocoa.lua
2755 lines (2318 loc) · 79.4 KB
/
nw_cocoa.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
--native windows - cococa backend.
--Written by Cosmin Apreutesei. Public domain.
local ffi = require'ffi'
local bit = require'bit'
local glue = require'glue'
local box2d = require'box2d'
local cbframe = require'cbframe'
local objc = require'objc'
objc.load'Foundation'
objc.load'AppKit'
objc.load'Carbon.HIToolbox' --for key codes
objc.load'ApplicationServices.CoreGraphics'
--objc.load'CoreGraphics' --for CGWindow*
objc.load'CoreFoundation' --for CFArray
local was_using_cbframe = objc.use_cbframe(true)
local was_using_properties = objc.use_properties(false)
local nw = {name = 'cocoa'}
--helpers --------------------------------------------------------------------
local function unpack_nsrect(r)
return r.origin.x, r.origin.y, r.size.width, r.size.height
end
local function override_rect(x, y, w, h, x1, y1, w1, h1)
return x1 or x, y1 or y, w1 or w, h1 or h
end
local function primary_screen_h()
return objc.NSScreen:screens():objectAtIndex(0):frame().size.height
end
--convert rect from bottom-up relative-to-main-screen space to top-down relative-to-main-screen space
local function flip_screen_rect(main_h, x, y, w, h)
main_h = main_h or primary_screen_h()
return x, main_h - h - y, w, h
end
--app object -----------------------------------------------------------------
local app = {}
nw.app = app
local App = objc.class('App', 'NSApplication <NSApplicationDelegate>')
function app:init(frontend)
self.frontend = frontend
--NOTE: we have to reference mainScreen() before using any of the
--display functions, or we will get NSRecursiveLock errors.
objc.NSScreen:mainScreen()
self.nsapp = App:sharedApplication()
self.nsapp.frontend = frontend
self.nsapp.backend = self
self.nsapp:setDelegate(self.nsapp)
--set it to be a normal app with dock and menu bar.
self.nsapp:setActivationPolicy(objc.NSApplicationActivationPolicyRegular)
--disable mouse coalescing so that mouse move events are not skipped.
objc.NSEvent:setMouseCoalescingEnabled(false)
--the menubar must be initialized _before_ the app is activated.
self:_init_menubar()
--activate the app before windows are created (see notes on app:activate() for why).
--activating the app now also gives the user the chance to activate
--another app if there's enough time to do that before the first window is shown.
--this is also how Windows behaves.
self:activate'force'
return self
end
--version checks -------------------------------------------------------------
function app:ver(what)
if what == 'osx' then
local s = objc.tolua(objc.NSProcessInfo:processInfo():operatingSystemVersionString()) --OSX 10.2+
return s:match'%d+%.%d+%.%d+'
end
end
--message loop ---------------------------------------------------------------
function app:run()
self.nsapp:run()
end
function app:poll()
--[[
local event = self.nsapp:nextEventMatchingMask_untilDate_inMode_dequeue(
objc.NSAnyEventMask, nil, objc.NSDefaultRunLoopMode, true)
if not event then return false end
--
return true
]]
error'NYI'
end
function app:stop()
self.nsapp:stop(nil)
--post a dummy event to ensure the stopping
local event = objc.NSEvent:
otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2(
objc.NSApplicationDefined, objc.NSMakePoint(0,0), 0, 0, 0, nil, 1, 1, 1)
self.nsapp:postEvent_atStart(event, true)
end
--quitting -------------------------------------------------------------------
--NOTE: quitting the app from the app's Dock menu calls applicationShouldTerminate, then calls close()
--on all windows, thus without calling windowShouldClose(), but only windowWillClose().
--NOTE: there's no windowDidClose() event and so windowDidResignKey() comes after windowWillClose().
--NOTE: applicationWillTerminate() is never called.
function App:applicationShouldTerminate()
self.frontend:_backend_quitting() --calls quit() which calls stop().
--we never terminate the app, we just stop the loop instead.
return false
end
--timers ---------------------------------------------------------------------
objc.addmethod('App', 'nw_timerEvent', function(self, timer)
if not timer.nw_func then return end
if timer.nw_func() == false then
timer:invalidate()
timer.nw_func = nil
end
end, 'v@:@')
function app:runevery(seconds, func)
local timer = objc.NSTimer:timerWithTimeInterval_target_selector_userInfo_repeats(
seconds, self.nsapp, 'nw_timerEvent', nil, true)
timer.nw_func = func
ffi.gc(timer, nil) --this is already a weak ref (the runloop keeps a ref)
objc.NSRunLoop:currentRunLoop():addTimer_forMode(timer, objc.NSRunLoopCommonModes)
end
--windows --------------------------------------------------------------------
local window = {}
app.window = window
local winmap = {} --Window->frontend_window
local Window = objc.class('Window', 'NSWindow <NSWindowDelegate, NSDraggingDestination>')
local cascadePoint
local function stylemask(t)
if t.frame == 'none' then
return objc.NSBorderlessWindowMask
else
return bit.bor(
objc.NSTitledWindowMask,
t.closeable and objc.NSClosableWindowMask or 0,
t.frame == 'toolbox' and t.minimizable and objc.NSMiniaturizableWindowMask or 0,
t.resizeable and objc.NSResizableWindowMask or 0
)
end
end
function window:new(app, frontend, t)
self = glue.update({app = app, frontend = frontend}, self)
--compute initial window style.
local style = stylemask(t)
if t.frame == 'none' then
--for frameless windows we have to handle maximization manually.
self._frameless = true
end
--convert frame rect to client rect.
local frame_rect = objc.NSMakeRect(flip_screen_rect(nil, t.x or 0, t.y or 0, t.w, t.h))
local content_rect = objc.NSWindow:contentRectForFrameRect_styleMask(frame_rect, style)
--create window (windows are created hidden).
self.nswin = Window:alloc():initWithContentRect_styleMask_backing_defer(
content_rect, style, objc.NSBackingStoreBuffered, false)
--init drawable content view.
self.nsview = self:_create_view(content_rect, frontend, false, t)
self.nswin:setContentView(self.nsview)
--we have to own the window because we use luavars.
self.nswin:setReleasedWhenClosed(false)
--fix bug where the sequence miniaturize()/close()/makeKeyAndOrderFront()
--results in hovering on titlebar buttons not working.
self.nswin:setOneShot(true)
--if position is not given, cascade window to emulate Windows behavior.
if not t.x and not t.y then
cascadePoint = cascadePoint or objc.NSMakePoint(10, 20)
cascadePoint = self.nswin:cascadeTopLeftFromPoint(cascadePoint)
end
if t.transparent then
self.nswin:setOpaque(false)
self.nswin:setBackgroundColor(objc.NSColor:clearColor())
--TODO: click-through option for transparent windows?
--NOTE: in windows this is done with window.transparent (WS_EX_TRANSPARENT) attribute.
--self.nswin:setIgnoresMouseEvents(true) --make it click-through
end
self._disabled = not t.enabled
--enable receiving events while moving and resizing.
--NOTE: this prevents the window from being moved if we're not processing
--messages and it makes moving the window a bit jerky. OTOH we get magnets
--and proper event sequence (when was Cocoa fast anyway?).
self.nswin:setMovable(false)
local toolbox = t.frame == 'toolbox'
--enable the fullscreen button.
if not toolbox and t.fullscreenable and self.app.frontend:ver'OSX 10.7' then
self.nswin:setCollectionBehavior(bit.bor(tonumber(self.nswin:collectionBehavior()),
objc.NSWindowCollectionBehaviorFullScreenPrimary)) --OSX 10.7+
end
--disable or hide the maximize and minimize buttons.
if toolbox or (not t.maximizable and not t.minimizable) then
--hide the minimize and maximize buttons when they're both disabled
--or if toolbox frame, to emulate Windows behavior.
local zb = self.nswin:standardWindowButton(objc.NSWindowZoomButton)
if zb then zb:setHidden(true) end
local rb = self.nswin:standardWindowButton(objc.NSWindowMiniaturizeButton)
if rb then rb:setHidden(true) end
else
if not t.minimizable then
self.nswin:standardWindowButton(objc.NSWindowMiniaturizeButton):setHidden(true)
end
if not t.maximizable then
self.nswin:standardWindowButton(objc.NSWindowZoomButton):setEnabled(false)
end
end
self.nswin:setTitle(t.title)
--enable keyboard API.
self.nswin:reset_keystate()
--set constraints.
if t.min_cw or t.min_ch then
self:set_minsize(t.min_cw, t.min_ch)
end
if t.max_cw or t.max_ch then
self:set_maxsize(t.max_cw, t.max_ch)
end
--set maximized state after setting constraints.
if t.maximized then
self:_maximize_frame()
end
--set topmost.
if t.topmost then
self:set_topmost(true)
end
--init drag & drop operation.
self:_init_drop()
--set visible state.
self._visible = false
--set minimized state
self._minimized = t.minimized
--set back references.
self.nswin.frontend = frontend
self.nswin.backend = self
self.nswin.app = app
--register window.
winmap[objc.nptr(self.nswin)] = self.frontend
--enable events.
self.nswin:setDelegate(self.nswin)
return self
end
--closing --------------------------------------------------------------------
--NOTE: close() doesn't call windowShouldClose.
--NOTE: fullscreen mode is a global state: closing a fullscreen window leaves
--that state inconsistent such that the next window will have the fullscreen
--bit set, which is why we have to exit fullscreen before attempting to close.
--NOTE: since we have to exit fullscreen before closing, this makes close()
--a potentially async operation.
function window:forceclose()
if self._entering_fs or self._exiting_fs or self:fullscreen() then
self._want_close = true --also acts as a state-changing barrier
self:_exit_fullscreen()
else
self:_forceclose()
end
end
function window:_forceclose()
self._closing = true
self._hiding = nil
self.nswin:close()
--if it was hidden (i.e. already closed), there was no closing event.
if self._closing then
self.nswin:windowWillClose(nil)
end
end
function Window:windowShouldClose()
return self.frontend:_backend_closing() or false
end
function Window:windowWillClose()
self.backend._closing = nil
if self.backend._hiding then
self.backend:_was_hidden()
return
end
--force-close child windows first to emulate Windows behavior.
if self.frontend:children'#' > 0 then
for i,win in ipairs(self.frontend:children()) do
win:close(true)
end
end
self.frontend:_backend_closed()
winmap[objc.nptr(self)] = nil --unregister
self:setDelegate(nil) --ignore further events
--release the view manually.
self.backend:_free_view(self.backend.nsview)
self.backend.nsview = nil
--release the window manually.
--NOTE: we must release the nswin reference, not self, because self
--is a weak reference and we can't release weak references.
--NOTE: this will free all luavars (including self.backend).
self.backend.nswin:release()
end
--activation -----------------------------------------------------------------
--NOTE: windows created after calling activateIgnoringOtherApps(false) go behind the active app.
--NOTE: windows created after calling activateIgnoringOtherApps(true) go in front of the active app.
--NOTE: the first call to nsapp:activateIgnoringOtherApps() doesn't also activate the main menu.
--but NSRunningApplication:currentApplication():activateWithOptions() does, so we use that instead!
function app:activate(mode)
if mode == 'force' then
objc.NSRunningApplication:currentApplication():activateWithOptions(
bit.bor(
objc.NSApplicationActivateIgnoringOtherApps,
objc.NSApplicationActivateAllWindows))
else
self.nsapp:requestUserAttention(mode =='alert' and objc.NSCriticalRequest or objc.NSInformationalRequest)
end
end
--NOTE: keyWindow() only returns the active window if the app itself is active.
function app:active_window()
return winmap[objc.nptr(self.nsapp:keyWindow())]
end
function app:active()
return self.nsapp:isActive()
end
function App:applicationDidBecomeActive()
self.frontend:_backend_changed()
end
--NOTE: applicationDidResignActive() is not sent on exit because the loop will be stopped at that time.
function App:applicationDidResignActive()
self.frontend:_backend_changed()
end
function Window:windowDidBecomeKey()
if self.backend._wait_enter_fs then
self.backend._wait_enter_fs = nil
self.backend:_enter_fullscreen()
end
self:reset_keystate()
self.frontend:_backend_changed()
end
function Window:windowDidResignKey()
self.dragging = false
self:reset_keystate()
self.frontend:_backend_changed()
end
--NOTE: makeKeyAndOrderFront() on an initially hidden window is ignored, but not on an orderOut() window.
--NOTE: makeKeyWindow() and makeKeyAndOrderFront() do the same thing (both bring the window to front).
--NOTE: makeKeyAndOrderFront() is deferred, if the app is not active, for when it becomes active.
--Only windows activated while the app is inactive will move to front when the app is activated,
--but other windows will not, unlike clicking the dock icon, which moves all the app's window in front.
--So only the windows made key after the call to activateIgnoringOtherApps(true) are moved to front!
--NOTE: windowDidBecomeKey event is triggered after the message loop is started on last window made key,
--unlike Windows which activates/deactivates windows directly without going through the message loop.
function window:activate()
self.nswin:makeKeyAndOrderFront(nil) --NOTE: async operation and can fail
end
function window:active()
return self.nswin:isKeyWindow()
end
--NOTE: by default, windows with NSBorderlessWindowMask can't become key.
function Window:canBecomeKeyWindow()
if not self.frontend or self.frontend:dead() then return true end --this is NOT a delegate method!
return self.frontend:activable()
end
--NOTE: by default, windows with NSBorderlessWindowMask can't become main.
function Window:canBecomeMainWindow()
if not self.frontend or self.frontend:dead() then return true end --this is NOT a delegate method!
return self.frontend:activable()
end
--remote wakeup --------------------------------------------------------------
function app:already_running()
return false --TODO
end
function app:wakeup_other_instances()
--TODO
end
--state/app visibility -------------------------------------------------------
function app:visible()
return not self.nsapp:isHidden()
end
function app:unhide() --NOTE: async operation
self.nsapp:unhide()
end
function app:hide() --NOTE: async operation
self.nsapp:hide(nil)
end
function App:applicationDidUnhide()
self.frontend:_backend_changed()
end
function App:applicationDidHide()
self.frontend:_backend_changed()
end
--state/visibility -----------------------------------------------------------
--NOTE: isVisible() returns false when the window is minimized.
--NOTE: isVisible() returns false when the app is hidden.
function window:visible()
return self._visible
end
--TODO: implement transitions from fullscreen mode instead of ignoring them.
function window:_fs_blocked()
return self._want_close or self._entering_fs or self._exiting_fs or self:fullscreen()
end
function window:show()
if self._visible then return end
if self:_fs_blocked() then return end
--hidden children are not added to the parent when the parent is shown,
--so they must be added when they are shown, but only if the parent is visible.
local parent = self.frontend:parent()
if parent and not parent:dead() and parent:visible() then
parent.backend.nswin:addChildWindow_ordered(self.nswin, objc.NSWindowAbove)
end
--add back visible child windows, removed on hide().
--not adding invisible children as that would make them visible automatically.
for i,win in ipairs(self.frontend:children()) do
if not win:dead() and win:visible() then
self.nswin:addChildWindow_ordered(win.backend.nswin, objc.NSWindowAbove)
end
end
if self._minimized then
--if it was minimized before hiding, minimize it back.
--orderBack() shows the window before minimizing it which sucks, but
--avoids a bug where the sequence minimize()/hide()/show()/restore()
--makes titlebar buttons not responding.
self.nswin:orderBack(nil)
self.nswin:miniaturize(nil)
else
self._visible = true
self.nswin:orderFront(nil) --NOTE: sync call
self.frontend:_backend_changed()
self.nswin:makeKeyWindow() --NOTE: async operation
end
end
--NOTE: orderOut() is ignored on a minimized window (known bug from 2008).
--NOTE: orderOut() is buggy: calling it before starting the message loop
--results in a window that is not hidden and doesn't respond to mouse events.
--NOTE: close() hides and removes all child windows.
function window:hide()
if not self._visible then return end
if self:_fs_blocked() then return end
self._minimized = self.nswin:isMiniaturized()
self._hiding = true --disambiguating close() from hide() in windowWillClose() event.
--remove child windows manually to prevent them from being hidden
--along with the parent, consistent with Windows and Linux.
for i,win in ipairs(self.frontend:children()) do
if not win:dead() then
self.nswin:removeChildWindow(win.backend.nswin)
end
end
self.nswin:close() --NOTE: sync call? better be (all ops check the _visible flag)
end
function window:_was_hidden() --windowWillClose() event for when self._hidden is set.
self._hiding = nil
self._visible = false
self.frontend:_backend_changed()
end
--state/minimizing -----------------------------------------------------------
--NOTE: isMiniaturized() returns false on a hidden window.
function window:minimized()
if self._minimized ~= nil then
return self._minimized
end
return self.nswin:isMiniaturized()
end
--NOTE: miniaturize() in fullscreen mode is ignored.
--NOTE: miniaturize() shows the window if hidden.
function window:minimize()
if self:_fs_blocked() then return end
if not self._visible then
--if it was hidden, minimize it again to show it.
--orderBack() shows the window before minimizing it, but not doing so
--hits another bug where the sequence hide()/minimize()/restore() makes
--hovering on titlebar buttons not working.
self.nswin:orderBack(nil)
end
self.nswin:miniaturize(nil) --NOTE: sync call
--windowDidMiniaturize() is not called from hidden.
if not self._visible then
self:_did_change_minimized()
end
end
--NOTE: deminiaturize() shows the window if it's hidden.
function window:_unminimize()
self.nswin:deminiaturize(nil)
--windowDidDeminiaturize() is not called from hidden.
if not self._visible then
self:_did_change_minimized()
end
end
function window:_did_change_minimized()
self._visible = true
self._minimized = nil
self.frontend:_backend_changed()
end
--NOTE: windowDidMiniaturize() is not called if minimizing from hidden state.
function Window:windowDidMiniaturize()
self.backend:_did_change_minimized()
end
--NOTE: windowDidDeminiaturize() is not called if restoring from hidden state.
function Window:windowDidDeminiaturize()
self.backend:_did_change_minimized()
end
--state/maximizing -----------------------------------------------------------
--NOTE: isZoomed() returns true for frameless windows.
--NOTE: isZoomed() returns true while in fullscreen mode.
--NOTE: isZoomed() calls windowWillResize_toSize(), believe it!
function window:maximized()
if self._maximized ~= nil then
return self._maximized
elseif self._frameless then
return self:_maximized_frame()
else
self.nswin.nw_zoomquery = true --nw_resizing() barrier
local zoomed = self.nswin:isZoomed()
self.nswin.nw_zoomquery = false
return zoomed
end
end
local function near(a, b)
return math.abs(a - b) < 10 --empirically found in OSX 10.9
end
--approximate the algorithm for isZoomed() for frameless windows.
function window:_maximized_frame()
local screen = self.nswin:screen()
if not screen then return false end --off-screen window
local sx, sy, sw, sh = unpack_nsrect(screen:visibleFrame())
local fx, fy, fw, fh = unpack_nsrect(self.nswin:frame())
local csw, csh = self:_constrain_size(sw, sh)
if csw < sw or csh < sh then
--constrained: size must match max. size
return near(fw, csw)
and near(fh, csh)
else
--unconstrained: position and size must match screen rect
return near(sx, fx)
and near(sy, fy)
and near(sx + sw, fx + fw)
and near(sy + sh, fy + fh)
end
end
--NOTE: zoom() on a minimized window is ignored.
--NOTE: zoom() on a fullscreen window is ignored.
--NOTE: zoom() on a frameless window is ignored.
--NOTE: zoom() on a hidden window works, and keeps the window hidden.
--NOTE: screen() on an initially hidden window works.
--NOTE: screen() on an orderOut() window is nil but on a closed window works!
--NOTE: screen() on a minimized window works!
--NOTE: screen() on an off-screen window is nil.
--maximize the window frame manually for when zoom() doesn't work.
--NOTE: off-screen windows maximize to the active screen.
--NOTE: hiding via orderOut() would make maximizing from hidden move the
--window to the active screen instead of the screen that matches the window's
--frame rect. Hiding via close() doesn't have this problem.
function window:_save_restore_frame()
self._restore_frame = self.nswin:frame()
end
function window:_maximize_frame_manually()
self:_save_restore_frame()
local screen = self.nswin:screen() or objc.NSScreen:mainScreen()
self.nswin:setFrame_display(screen:visibleFrame(), true)
self:_apply_constraints()
end
--unmaximize the window frame manually for when zoom() doesn't work.
function window:_unmaximize_frame_manually()
self.nswin:setFrame_display(self._restore_frame, true)
self._restore_frame = nil
self:_apply_constraints()
end
--maximize the window frame without changing its visibility.
--NOTE: frameless off-screen windows maximize to the active screen.
function window:_maximize_frame()
if self._frameless then
self:_maximize_frame_manually()
else
self:_save_restore_frame()
self.nswin.nw_zooming = true
self.nswin:zoom(nil) --NOTE: sync call
self.nswin.nw_zooming = false
self:_apply_constraints()
end
end
--unmaximize the window manually to the saved rect.
function window:_unmaximize_frame()
if self._frameless then
self:_unmaximize_frame_manually()
else
self.nswin.nw_zooming = true
self.nswin:zoom(nil) --NOTE: sync call
self.nswin.nw_zooming = false
self:_apply_constraints()
end
end
--zoom() doesn't work on a minimzied window, so we adjust the rect manually.
function window:_maximize_minimized()
self:_maximize_frame_manually()
self:_unminimize()
end
--zoom() doesn't work on a minimzied window, so we adjust the rect manually.
function window:_unmaximize_minimized()
self:_unmaximize_frame_manually()
self:_unminimize()
end
function window:maximize()
if self:_fs_blocked() then return end
if self:minimized() then
if self:maximized() then
self:_unminimize()
else
self:_maximize_minimized()
end
else
local maximized
if not self:maximized() then
self:_maximize_frame()
maximized = true
end
if not self:visible() then
self:show()
elseif maximized then
self.frontend:_backend_changed()
end
end
end
function window:_unmaximize()
self:_unmaximize_frame()
if not self:visible() then
self:show() --show posts changed event
else
self.frontend:_backend_changed()
end
end
--save normal rect before maximizing so we can maximize from minimized.
function Window.windowShouldZoom_toFrame(cpu)
--get arg1 from the ABI guts and set `true` as return value.
local self
if ffi.arch == 'x64' then
self = ffi.cast('id', cpu.RDI.p) --RDI = self
cpu.RAX.lo.i = true
else
self = ffi.cast('id', cpu.ESP.dp[1].p) --ESP[1] = self
cpu.EAX.i = true
end
if not self.backend then return end --not hooked yet
if not self._frameless then
self.backend:_save_restore_frame()
end
end
--state/restoring ------------------------------------------------------------
function window:restore()
if self._want_close then return end
if self:minimized() then
self:_unminimize()
elseif self:maximized() then
self:_unmaximize()
elseif not self:visible() then
self:show()
end
end
function window:shownormal()
if self:_fs_blocked() then return end
if self:minimized() and self:maximized() then
self:_unmaximize_minimized()
else
self:restore()
end
end
--state/fullscreen mode ------------------------------------------------------
function window:fullscreen()
return bit.band(tonumber(self.nswin:styleMask()),
objc.NSFullScreenWindowMask) == objc.NSFullScreenWindowMask
end
function window:enter_fullscreen()
if self._want_close then return end
if self._exiting_fs then
--there's no API to cancel an in-progress toggleFullScreen() animation.
--best we can do is to wait to let it finish and go from there.
self._enter_fs = true
elseif self._exit_fs then
--cancel the cancelation of enter_fullscreen().
self._exit_fs = false
elseif self._entering_fs then
--we're animating our ass off to that effect already.
elseif not self:visible() then
--let it show first, and tell it to go fullscreen then.
self._wait_enter_fs = true
self.nswin:makeKeyAndOrderFront(nil) --NOTE: async operation
elseif not self:fullscreen() then
self:_enter_fullscreen()
end
end
--NOTE: toggleFullScreen() on a minimized window works.
--NOTE: calling close() after toggleFullScreen() results in a crash.
--NOTE: toggleFullScreen() on a closed window works.
--NOTE: toggleFullScreen() while toggling is in progress is ignored.
--NOTE: toggleFullScreen() is async even though it doesn't appear so because
--it discards keyboard and mouse events while animating.
function window:_enter_fullscreen()
self._visible = true
self._minimized = nil
self._entering_fs = true
self.nswin:toggleFullScreen(nil) --NOTE: async operation
end
function window:exit_fullscreen()
if self._want_close then return end
self:_exit_fullscreen()
end
function window:_exit_fullscreen()
if self._entering_fs then
--there's no API to cancel an in-progress toggleFullScreen() animation.
--best we can do is to wait to let it finish and go from there.
self._exit_fs = true
elseif self._exiting_fs then
--we're animating our ass off to that effect already.
elseif self:fullscreen() then
self._exiting_fs = true
if not self:visible() then
self:show()
else
self.nswin:toggleFullScreen(nil) --NOTE: async operation
end
end
end
function Window:windowWillEnterFullScreen()
--fixate the maximized flag so that maximized() works while in fullscreen.
self.backend._maximized = self.backend:maximized()
--save the frame style and rect and change them for fullscreen.
self.nw_stylemask = self:styleMask()
self.nw_frame = self:frame()
self:setStyleMask(bit.bor(
objc.NSFullScreenWindowMask, --fullscreen appearance
objc.NSBorderlessWindowMask --remove the round corners
))
local screen = self:screen() or objc.NSScreen:mainScreen()
self:setFrame_display(screen:frame(), true)
self.backend:_apply_constraints()
end
function Window:windowDidEnterFullScreen()
self.backend._entering_fs = false
self.frontend:_backend_changed()
--great, now see if we have to exit already.
if self.backend._exit_fs then
self.backend._exit_fs = false
self.backend:_exit_fullscreen()
end
end
function Window:windowWillExitFullScreen()
--restore the frame style and rect to saved values.
self:setStyleMask(self.nw_stylemask)
self:setFrame_display(self.nw_frame, true)
--remove the fixated _maximized flag.
self.backend._maximized = nil
end
function Window:windowDidExitFullScreen()
self.backend._exiting_fs = false
self.frontend:_backend_changed()
--great, now see if we have to close or go back to fullscreen already.
if self.backend._want_close then
self.backend._enter_fs = false
self.backend:_forceclose()
elseif self.backend._enter_fs then
self.backend._enter_fs = false
--if we do it now we get a crash, so we queue it.
self.frontend.app:runafter(0, function()
self.backend:enter_fullscreen()
end)
end
end
function Window:windowDidFailToExitFullScreen()
--TODO: find a way to trigger this predictably so we know what to do here
if self.backend._want_close then
self.backend:_forceclose()
end
end
function Window:windowDidFailToEnterFullScreen()
--TODO: find a way to trigger this predictably so we know what to do here
if self.backend._want_close then
self.backend:_forceclose()
end
end
--state/enabled --------------------------------------------------------------
function window:get_enabled()
return not self._disabled
end
function window:set_enabled(enabled)
self._disabled = not enabled
end
--positioning/frame extents --------------------------------------------------
function app:frame_extents(frame, has_menu, resizeable)
--NOTE: these computations are done in non-flipped space (y=0 at the bottom)
local style = stylemask(frame)
local cx, cy, cw, ch = 200, 200, 400, 400
local rect = objc.NSMakeRect(cx, cy, cw, ch)
local rect = objc.NSWindow:frameRectForContentRect_styleMask(rect, style)
local x, y, w, h = unpack_nsrect(rect)
local l = cx-x
local b = cy-y
local t = h-ch-b
local r = w-cw-l
return l, t, r, b
end
--positioning/rectangles -----------------------------------------------------
function window:_flip_y(y)
return self.nswin:contentView():frame().size.height - y --flip y around contentView's height
end
function window:get_client_size()
local sz = self.nswin:contentView():bounds().size
return sz.width, sz.height
end
function window:get_client_pos() --OSX 10.7+
local y = self:_flip_y(0)
local x, y = flip_screen_rect(nil, unpack_nsrect(self.nswin:convertRectToScreen(objc.NSMakeRect(0, y, 0, 0))))
return x, y
end
function window:_set_frame_rect(x, y, w, h)
self.nswin:setFrame_display(objc.NSMakeRect(flip_screen_rect(nil, x, y, w, h)), true)
end
function window:get_frame_rect()
return flip_screen_rect(nil, unpack_nsrect(self.nswin:frame()))
end
function window:set_frame_rect(x, y, w, h)
self:_set_frame_rect(x, y, w, h)
self:_apply_constraints()
end
--NOTE: framed windows are constrained to screen bounds but frameless windows are not.
function window:get_normal_frame_rect()
--TODO: fix this
return flip_screen_rect(nil, unpack_nsrect(self.nswin:frame()))
end
--positioning/constraints ----------------------------------------------------
local function clean(x)
return x ~= 0 and x or nil
end
function window:get_minsize()
local sz = self.nswin:contentMinSize()
return clean(sz.width), clean(sz.height)
end
--clamp with optional min and max, where min takes precedence over max.
local function clamp(x, min, max)
if max and min and max < min then max = min end
if min then x = math.max(x, min) end
if max then x = math.min(x, max) end
return x
end
function window:_constrain_size(w, h)
local minw, minh = self:get_minsize()
local maxw, maxh = self:get_maxsize()
w = clamp(w, minw, maxw)
h = clamp(h, minh, maxh)
return w, h
end
local applying
function window:_apply_constraints()
if applying then return end
--get window position in case we need to set it back
local x1, y1 = self:get_normal_frame_rect()
--get and constrain size
local sz = self.nswin:contentView():bounds().size
sz.width, sz.height = self:_constrain_size(sz.width, sz.height)