1 /*
2  * This file is part of mpv.
3  *
4  * mpv is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 2.1 of the License, or (at your option) any later version.
8  *
9  * mpv is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with mpv.  If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 import Cocoa
19 
20 class View: NSView {
21     unowned var common: Common
22     var mpv: MPVHelper? { get { return common.mpv } }
23 
24     var tracker: NSTrackingArea?
25     var hasMouseDown: Bool = false
26 
27     override var isFlipped: Bool { return true }
28     override var acceptsFirstResponder: Bool { return true }
29 
30 
31     init(frame: NSRect, common com: Common) {
32         common = com
33         super.init(frame: frame)
34         autoresizingMask = [.width, .height]
35         wantsBestResolutionOpenGLSurface = true
36         registerForDraggedTypes([ .fileURLCompat, .URLCompat, .string ])
37     }
38 
39     required init?(coder: NSCoder) {
40         fatalError("init(coder:) has not been implemented")
41     }
42 
updateTrackingAreasnull43     override func updateTrackingAreas() {
44         if let tracker = self.tracker {
45             removeTrackingArea(tracker)
46         }
47 
48         tracker = NSTrackingArea(rect: bounds,
49             options: [.activeAlways, .mouseEnteredAndExited, .mouseMoved, .enabledDuringMouseDrag],
50             owner: self, userInfo: nil)
51         // here tracker is guaranteed to be none-nil
52         addTrackingArea(tracker!)
53 
54         if containsMouseLocation() {
55             cocoa_put_key_with_modifiers(SWIFT_KEY_MOUSE_LEAVE, 0)
56         }
57     }
58 
draggingEnterednull59     override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
60         guard let types = sender.draggingPasteboard.types else { return [] }
61         if types.contains(.fileURLCompat) || types.contains(.URLCompat) || types.contains(.string) {
62             return .copy
63         }
64         return []
65     }
66 
isURLnull67     func isURL(_ str: String) -> Bool {
68         // force unwrapping is fine here, regex is guarnteed to be valid
69         let regex = try! NSRegularExpression(pattern: "^(https?|ftp)://[^\\s/$.?#].[^\\s]*$",
70                                              options: .caseInsensitive)
71         let isURL = regex.numberOfMatches(in: str,
72                                      options: [],
73                                        range: NSRange(location: 0, length: str.count))
74         return isURL > 0
75     }
76 
performDragOperationnull77     override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
78         let pb = sender.draggingPasteboard
79         guard let types = pb.types else { return false }
80 
81         if types.contains(.fileURLCompat) || types.contains(.URLCompat) {
82             if let urls = pb.readObjects(forClasses: [NSURL.self]) as? [URL] {
83                 let files = urls.map { $0.absoluteString }
84                 EventsResponder.sharedInstance().handleFilesArray(files)
85                 return true
86             }
87         } else if types.contains(.string) {
88             guard let str = pb.string(forType: .string) else { return false }
89             var filesArray: [String] = []
90 
91             for val in str.components(separatedBy: "\n") {
92                 let url = val.trimmingCharacters(in: .whitespacesAndNewlines)
93                 let path = (url as NSString).expandingTildeInPath
94                 if isURL(url) {
95                     filesArray.append(url)
96                 } else if path.starts(with: "/") {
97                     filesArray.append(path)
98                 }
99             }
100             EventsResponder.sharedInstance().handleFilesArray(filesArray)
101             return true
102         }
103         return false
104     }
105 
acceptsFirstMousenull106     override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
107         return true
108     }
109 
becomeFirstRespondernull110     override func becomeFirstResponder() -> Bool {
111         return true
112     }
113 
resignFirstRespondernull114     override func resignFirstResponder() -> Bool {
115         return true
116     }
117 
mouseEnterednull118     override func mouseEntered(with event: NSEvent) {
119         if mpv?.mouseEnabled() ?? true {
120             cocoa_put_key_with_modifiers(SWIFT_KEY_MOUSE_ENTER, 0)
121         }
122         common.updateCursorVisibility()
123     }
124 
mouseExitednull125     override func mouseExited(with event: NSEvent) {
126         if mpv?.mouseEnabled() ?? true {
127             cocoa_put_key_with_modifiers(SWIFT_KEY_MOUSE_LEAVE, 0)
128         }
129         common.titleBar?.hide()
130         common.setCursorVisiblility(true)
131     }
132 
mouseMovednull133     override func mouseMoved(with event: NSEvent) {
134         if mpv?.mouseEnabled() ?? true {
135             signalMouseMovement(event)
136         }
137         common.titleBar?.show()
138     }
139 
mouseDraggednull140     override func mouseDragged(with event: NSEvent) {
141         if mpv?.mouseEnabled() ?? true {
142             signalMouseMovement(event)
143         }
144     }
145 
mouseDownnull146     override func mouseDown(with event: NSEvent) {
147         if mpv?.mouseEnabled() ?? true {
148             signalMouseDown(event)
149         }
150     }
151 
mouseUpnull152     override func mouseUp(with event: NSEvent) {
153         if mpv?.mouseEnabled() ?? true {
154             signalMouseUp(event)
155         }
156         common.window?.isMoving = false
157     }
158 
rightMouseDownnull159     override func rightMouseDown(with event: NSEvent) {
160         if mpv?.mouseEnabled() ?? true {
161             signalMouseDown(event)
162         }
163     }
164 
rightMouseUpnull165     override func rightMouseUp(with event: NSEvent) {
166         if mpv?.mouseEnabled() ?? true {
167             signalMouseUp(event)
168         }
169     }
170 
otherMouseDownnull171     override func otherMouseDown(with event: NSEvent) {
172         if mpv?.mouseEnabled() ?? true {
173             signalMouseDown(event)
174         }
175     }
176 
otherMouseUpnull177     override func otherMouseUp(with event: NSEvent) {
178         if mpv?.mouseEnabled() ?? true {
179             signalMouseUp(event)
180         }
181     }
182 
magnifynull183     override func magnify(with event: NSEvent) {
184         event.phase == .ended ?
185             common.windowDidEndLiveResize() : common.windowWillStartLiveResize()
186 
187         common.window?.addWindowScale(Double(event.magnification))
188     }
189 
signalMouseDownnull190     func signalMouseDown(_ event: NSEvent) {
191         signalMouseEvent(event, MP_KEY_STATE_DOWN)
192         if event.clickCount > 1 {
193             signalMouseEvent(event, MP_KEY_STATE_UP)
194         }
195     }
196 
signalMouseUpnull197     func signalMouseUp(_ event: NSEvent) {
198         signalMouseEvent(event, MP_KEY_STATE_UP)
199     }
200 
signalMouseEventnull201     func signalMouseEvent(_ event: NSEvent, _ state: UInt32) {
202         hasMouseDown = state == MP_KEY_STATE_DOWN
203         let mpkey = getMpvButton(event)
204         cocoa_put_key_with_modifiers((mpkey | Int32(state)), Int32(event.modifierFlags.rawValue))
205     }
206 
signalMouseMovementnull207     func signalMouseMovement(_ event: NSEvent) {
208         var point = convert(event.locationInWindow, from: nil)
209         point = convertToBacking(point)
210         point.y = -point.y
211 
212         common.window?.updateMovableBackground(point)
213         if !(common.window?.isMoving ?? false) {
214             mpv?.setMousePosition(point)
215         }
216     }
217 
preciseScrollnull218     func preciseScroll(_ event: NSEvent) {
219         var delta: Double
220         var cmd: Int32
221 
222         if abs(event.deltaY) >= abs(event.deltaX) {
223             delta = Double(event.deltaY) * 0.1
224             cmd = delta > 0 ? SWIFT_WHEEL_UP : SWIFT_WHEEL_DOWN
225         } else {
226             delta = Double(event.deltaX) * 0.1
227             cmd = delta > 0 ? SWIFT_WHEEL_RIGHT : SWIFT_WHEEL_LEFT
228         }
229 
230         mpv?.putAxis(cmd, delta: abs(delta))
231     }
232 
scrollWheelnull233     override func scrollWheel(with event: NSEvent) {
234         if !(mpv?.mouseEnabled() ?? true) {
235             return
236         }
237 
238         if event.hasPreciseScrollingDeltas {
239             preciseScroll(event)
240         } else {
241             let modifiers = event.modifierFlags
242             let deltaX = modifiers.contains(.shift) ? event.scrollingDeltaY : event.scrollingDeltaX
243             let deltaY = modifiers.contains(.shift) ? event.scrollingDeltaX : event.scrollingDeltaY
244             var mpkey: Int32
245 
246             if abs(deltaY) >= abs(deltaX) {
247                 mpkey = deltaY > 0 ? SWIFT_WHEEL_UP : SWIFT_WHEEL_DOWN
248             } else {
249                 mpkey = deltaX > 0 ? SWIFT_WHEEL_RIGHT : SWIFT_WHEEL_LEFT
250             }
251 
252             cocoa_put_key_with_modifiers(mpkey, Int32(modifiers.rawValue))
253         }
254     }
255 
containsMouseLocationnull256     func containsMouseLocation() -> Bool {
257         var topMargin: CGFloat = 0.0
258         let menuBarHeight = NSApp.mainMenu?.menuBarHeight ?? 23.0
259 
260         guard let window = common.window else { return false }
261         guard var vF = window.screen?.frame else { return false }
262 
263         if window.isInFullscreen && (menuBarHeight > 0) {
264             topMargin = TitleBar.height + 1 + menuBarHeight
265         }
266 
267         vF.size.height -= topMargin
268 
269         let vFW = window.convertFromScreen(vF)
270         let vFV = convert(vFW, from: nil)
271         let pt = convert(window.mouseLocationOutsideOfEventStream, from: nil)
272 
273         var clippedBounds = bounds.intersection(vFV)
274         if !window.isInFullscreen {
275             clippedBounds.origin.y += TitleBar.height
276             clippedBounds.size.height -= TitleBar.height
277         }
278         return clippedBounds.contains(pt)
279     }
280 
canHideCursornull281     func canHideCursor() -> Bool {
282         guard let window = common.window else { return false }
283         return !hasMouseDown && containsMouseLocation() && window.isKeyWindow
284     }
285 
getMpvButtonnull286     func getMpvButton(_ event: NSEvent) -> Int32 {
287         let buttonNumber = event.buttonNumber
288         switch (buttonNumber) {
289             case 0:  return SWIFT_MBTN_LEFT
290             case 1:  return SWIFT_MBTN_RIGHT
291             case 2:  return SWIFT_MBTN_MID
292             case 3:  return SWIFT_MBTN_BACK
293             case 4:  return SWIFT_MBTN_FORWARD
294             default: return SWIFT_MBTN9 + Int32(buttonNumber - 5)
295         }
296     }
297 }
298