diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 20:36:56 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 20:36:56 +0000 |
commit | 51de1d8436100f725f3576aefa24a2bd2057bc28 (patch) | |
tree | c6d1d5264b6d40a8d7ca34129f36b7d61e188af3 /osdep/macos | |
parent | Initial commit. (diff) | |
download | mpv-upstream/0.37.0.tar.xz mpv-upstream/0.37.0.zip |
Adding upstream version 0.37.0.upstream/0.37.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | osdep/macos/libmpv_helper.swift | 250 | ||||
-rw-r--r-- | osdep/macos/log_helper.swift | 47 | ||||
-rw-r--r-- | osdep/macos/mpv_helper.swift | 156 | ||||
-rw-r--r-- | osdep/macos/precise_timer.swift | 153 | ||||
-rw-r--r-- | osdep/macos/remote_command_center.swift | 191 | ||||
-rw-r--r-- | osdep/macos/swift_compat.swift | 36 | ||||
-rw-r--r-- | osdep/macos/swift_extensions.swift | 58 | ||||
-rw-r--r-- | osdep/macosx_application.h | 55 | ||||
-rw-r--r-- | osdep/macosx_application.m | 375 | ||||
-rw-r--r-- | osdep/macosx_application_objc.h | 40 | ||||
-rw-r--r-- | osdep/macosx_events.h | 36 | ||||
-rw-r--r-- | osdep/macosx_events.m | 408 | ||||
-rw-r--r-- | osdep/macosx_events_objc.h | 45 | ||||
-rw-r--r-- | osdep/macosx_menubar.h | 30 | ||||
-rw-r--r-- | osdep/macosx_menubar.m | 853 | ||||
-rw-r--r-- | osdep/macosx_menubar_objc.h | 25 | ||||
-rw-r--r-- | osdep/macosx_touchbar.h | 46 | ||||
-rw-r--r-- | osdep/macosx_touchbar.m | 334 |
18 files changed, 3138 insertions, 0 deletions
diff --git a/osdep/macos/libmpv_helper.swift b/osdep/macos/libmpv_helper.swift new file mode 100644 index 0000000..8b1c697 --- /dev/null +++ b/osdep/macos/libmpv_helper.swift @@ -0,0 +1,250 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +import Cocoa +import OpenGL.GL +import OpenGL.GL3 + +let glDummy: @convention(c) () -> Void = {} + +class LibmpvHelper { + var log: LogHelper + var mpvHandle: OpaquePointer? + var mpvRenderContext: OpaquePointer? + var macOptsPtr: UnsafeMutableRawPointer? + var macOpts: macos_opts = macos_opts() + var fbo: GLint = 1 + let deinitLock = NSLock() + + init(_ mpv: OpaquePointer, _ mpLog: OpaquePointer?) { + mpvHandle = mpv + log = LogHelper(mpLog) + + guard let app = NSApp as? Application, + let ptr = mp_get_config_group(nil, + mp_client_get_global(mpvHandle), + app.getMacOSConf()) else + { + log.sendError("macOS config group couldn't be retrieved'") + exit(1) + } + macOptsPtr = ptr + macOpts = UnsafeMutablePointer<macos_opts>(OpaquePointer(ptr)).pointee + } + + func initRender() { + let advanced: CInt = 1 + let api = UnsafeMutableRawPointer(mutating: (MPV_RENDER_API_TYPE_OPENGL as NSString).utf8String) + let pAddress = mpv_opengl_init_params(get_proc_address: getProcAddress, + get_proc_address_ctx: nil) + + MPVHelper.withUnsafeMutableRawPointers([pAddress, advanced]) { (pointers: [UnsafeMutableRawPointer?]) in + var params: [mpv_render_param] = [ + mpv_render_param(type: MPV_RENDER_PARAM_API_TYPE, data: api), + mpv_render_param(type: MPV_RENDER_PARAM_OPENGL_INIT_PARAMS, data: pointers[0]), + mpv_render_param(type: MPV_RENDER_PARAM_ADVANCED_CONTROL, data: pointers[1]), + mpv_render_param() + ] + + if (mpv_render_context_create(&mpvRenderContext, mpvHandle, ¶ms) < 0) { + log.sendError("Render context init has failed.") + exit(1) + } + } + + } + + let getProcAddress: (@convention(c) (UnsafeMutableRawPointer?, UnsafePointer<Int8>?) + -> UnsafeMutableRawPointer?) = + { + (ctx: UnsafeMutableRawPointer?, name: UnsafePointer<Int8>?) + -> UnsafeMutableRawPointer? in + let symbol: CFString = CFStringCreateWithCString( + kCFAllocatorDefault, name, kCFStringEncodingASCII) + let identifier = CFBundleGetBundleWithIdentifier("com.apple.opengl" as CFString) + let addr = CFBundleGetFunctionPointerForName(identifier, symbol) + + if symbol as String == "glFlush" { + return unsafeBitCast(glDummy, to: UnsafeMutableRawPointer.self) + } + + return addr + } + + func setRenderUpdateCallback(_ callback: @escaping mpv_render_update_fn, context object: AnyObject) { + if mpvRenderContext == nil { + log.sendWarning("Init mpv render context first.") + } else { + mpv_render_context_set_update_callback(mpvRenderContext, callback, MPVHelper.bridge(obj: object)) + } + } + + func setRenderControlCallback(_ callback: @escaping mp_render_cb_control_fn, context object: AnyObject) { + if mpvRenderContext == nil { + log.sendWarning("Init mpv render context first.") + } else { + mp_render_context_set_control_callback(mpvRenderContext, callback, MPVHelper.bridge(obj: object)) + } + } + + func reportRenderFlip() { + if mpvRenderContext == nil { return } + mpv_render_context_report_swap(mpvRenderContext) + } + + func isRenderUpdateFrame() -> Bool { + deinitLock.lock() + if mpvRenderContext == nil { + deinitLock.unlock() + return false + } + let flags: UInt64 = mpv_render_context_update(mpvRenderContext) + deinitLock.unlock() + return flags & UInt64(MPV_RENDER_UPDATE_FRAME.rawValue) > 0 + } + + func drawRender(_ surface: NSSize, _ depth: GLint, _ ctx: CGLContextObj, skip: Bool = false) { + deinitLock.lock() + if mpvRenderContext != nil { + var i: GLint = 0 + let flip: CInt = 1 + let skip: CInt = skip ? 1 : 0 + let ditherDepth = depth + glGetIntegerv(GLenum(GL_DRAW_FRAMEBUFFER_BINDING), &i) + // CAOpenGLLayer has ownership of FBO zero yet can return it to us, + // so only utilize a newly received FBO ID if it is nonzero. + fbo = i != 0 ? i : fbo + + let data = mpv_opengl_fbo(fbo: Int32(fbo), + w: Int32(surface.width), + h: Int32(surface.height), + internal_format: 0) + + MPVHelper.withUnsafeMutableRawPointers([data, flip, ditherDepth, skip]) { (pointers: [UnsafeMutableRawPointer?]) in + var params: [mpv_render_param] = [ + mpv_render_param(type: MPV_RENDER_PARAM_OPENGL_FBO, data: pointers[0]), + mpv_render_param(type: MPV_RENDER_PARAM_FLIP_Y, data: pointers[1]), + mpv_render_param(type: MPV_RENDER_PARAM_DEPTH, data: pointers[2]), + mpv_render_param(type: MPV_RENDER_PARAM_SKIP_RENDERING, data: pointers[3]), + mpv_render_param() + ] + mpv_render_context_render(mpvRenderContext, ¶ms); + } + } else { + glClearColor(0, 0, 0, 1) + glClear(GLbitfield(GL_COLOR_BUFFER_BIT)) + } + + if !skip { CGLFlushDrawable(ctx) } + + deinitLock.unlock() + } + + func setRenderICCProfile(_ profile: NSColorSpace) { + if mpvRenderContext == nil { return } + guard var iccData = profile.iccProfileData else { + log.sendWarning("Invalid ICC profile data.") + return + } + iccData.withUnsafeMutableBytes { (ptr: UnsafeMutableRawBufferPointer) in + guard let baseAddress = ptr.baseAddress, ptr.count > 0 else { return } + + let u8Ptr = baseAddress.assumingMemoryBound(to: UInt8.self) + let iccBstr = bstrdup(nil, bstr(start: u8Ptr, len: ptr.count)) + var icc = mpv_byte_array(data: iccBstr.start, size: iccBstr.len) + withUnsafeMutableBytes(of: &icc) { (ptr: UnsafeMutableRawBufferPointer) in + let params = mpv_render_param(type: MPV_RENDER_PARAM_ICC_PROFILE, data: ptr.baseAddress) + mpv_render_context_set_parameter(mpvRenderContext, params) + } + } + } + + func setRenderLux(_ lux: Int) { + if mpvRenderContext == nil { return } + var light = lux + withUnsafeMutableBytes(of: &light) { (ptr: UnsafeMutableRawBufferPointer) in + let params = mpv_render_param(type: MPV_RENDER_PARAM_AMBIENT_LIGHT, data: ptr.baseAddress) + mpv_render_context_set_parameter(mpvRenderContext, params) + } + } + + func commandAsync(_ cmd: [String?], id: UInt64 = 1) { + if mpvHandle == nil { return } + var mCmd = cmd + mCmd.append(nil) + var cargs = mCmd.map { $0.flatMap { UnsafePointer<Int8>(strdup($0)) } } + mpv_command_async(mpvHandle, id, &cargs) + for ptr in cargs { free(UnsafeMutablePointer(mutating: ptr)) } + } + + // Unsafe function when called while using the render API + func command(_ cmd: String) { + if mpvHandle == nil { return } + mpv_command_string(mpvHandle, cmd) + } + + func getBoolProperty(_ name: String) -> Bool { + if mpvHandle == nil { return false } + var value = Int32() + mpv_get_property(mpvHandle, name, MPV_FORMAT_FLAG, &value) + return value > 0 + } + + func getIntProperty(_ name: String) -> Int { + if mpvHandle == nil { return 0 } + var value = Int64() + mpv_get_property(mpvHandle, name, MPV_FORMAT_INT64, &value) + return Int(value) + } + + func getStringProperty(_ name: String) -> String? { + guard let mpv = mpvHandle else { return nil } + guard let value = mpv_get_property_string(mpv, name) else { return nil } + let str = String(cString: value) + mpv_free(value) + return str + } + + func deinitRender() { + mpv_render_context_set_update_callback(mpvRenderContext, nil, nil) + mp_render_context_set_control_callback(mpvRenderContext, nil, nil) + deinitLock.lock() + mpv_render_context_free(mpvRenderContext) + mpvRenderContext = nil + deinitLock.unlock() + } + + func deinitMPV(_ destroy: Bool = false) { + if destroy { + mpv_destroy(mpvHandle) + } + ta_free(macOptsPtr) + macOptsPtr = nil + mpvHandle = nil + } + + // *(char **) MPV_FORMAT_STRING on mpv_event_property + class func mpvStringArrayToString(_ obj: UnsafeMutableRawPointer) -> String? { + let cstr = UnsafeMutablePointer<UnsafeMutablePointer<Int8>>(OpaquePointer(obj)) + return String(cString: cstr[0]) + } + + // MPV_FORMAT_FLAG + class func mpvFlagToBool(_ obj: UnsafeMutableRawPointer) -> Bool? { + return UnsafePointer<Bool>(OpaquePointer(obj))?.pointee + } +} diff --git a/osdep/macos/log_helper.swift b/osdep/macos/log_helper.swift new file mode 100644 index 0000000..9464075 --- /dev/null +++ b/osdep/macos/log_helper.swift @@ -0,0 +1,47 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +import Cocoa + +class LogHelper: NSObject { + var log: OpaquePointer? + + init(_ log: OpaquePointer?) { + self.log = log + } + + func sendVerbose(_ msg: String) { + send(message: msg, type: MSGL_V) + } + + func sendInfo(_ msg: String) { + send(message: msg, type: MSGL_INFO) + } + + func sendWarning(_ msg: String) { + send(message: msg, type: MSGL_WARN) + } + + func sendError(_ msg: String) { + send(message: msg, type: MSGL_ERR) + } + + func send(message msg: String, type t: Int) { + let args: [CVarArg] = [ (msg as NSString).utf8String ?? "NO MESSAGE"] + mp_msg_va(log, Int32(t), "%s\n", getVaList(args)) + } +} diff --git a/osdep/macos/mpv_helper.swift b/osdep/macos/mpv_helper.swift new file mode 100644 index 0000000..3b2a716 --- /dev/null +++ b/osdep/macos/mpv_helper.swift @@ -0,0 +1,156 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +import Cocoa + +typealias swift_wakeup_cb_fn = (@convention(c) (UnsafeMutableRawPointer?) -> Void)? + +class MPVHelper { + var log: LogHelper + var vo: UnsafeMutablePointer<vo> + var optsCachePtr: UnsafeMutablePointer<m_config_cache> + var optsPtr: UnsafeMutablePointer<mp_vo_opts> + var macOptsCachePtr: UnsafeMutablePointer<m_config_cache> + var macOptsPtr: UnsafeMutablePointer<macos_opts> + + // these computed properties return a local copy of the struct accessed: + // - don't use if you rely on the pointers + // - only for reading + var vout: vo { get { return vo.pointee } } + var optsCache: m_config_cache { get { return optsCachePtr.pointee } } + var opts: mp_vo_opts { get { return optsPtr.pointee } } + var macOptsCache: m_config_cache { get { return macOptsCachePtr.pointee } } + var macOpts: macos_opts { get { return macOptsPtr.pointee } } + + var input: OpaquePointer { get { return vout.input_ctx } } + + init(_ vo: UnsafeMutablePointer<vo>, _ log: LogHelper) { + self.vo = vo + self.log = log + + guard let app = NSApp as? Application, + let cache = m_config_cache_alloc(vo, vo.pointee.global, app.getVoSubConf()) else + { + log.sendError("NSApp couldn't be retrieved") + exit(1) + } + + optsCachePtr = cache + optsPtr = UnsafeMutablePointer<mp_vo_opts>(OpaquePointer(cache.pointee.opts)) + + guard let macCache = m_config_cache_alloc(vo, + vo.pointee.global, + app.getMacOSConf()) else + { + // will never be hit, mp_get_config_group asserts for invalid groups + exit(1) + } + macOptsCachePtr = macCache + macOptsPtr = UnsafeMutablePointer<macos_opts>(OpaquePointer(macCache.pointee.opts)) + } + + func canBeDraggedAt(_ pos: NSPoint) -> Bool { + let canDrag = !mp_input_test_dragging(input, Int32(pos.x), Int32(pos.y)) + return canDrag + } + + func mouseEnabled() -> Bool { + return mp_input_mouse_enabled(input) + } + + func setMousePosition(_ pos: NSPoint) { + mp_input_set_mouse_pos(input, Int32(pos.x), Int32(pos.y)) + } + + func putAxis(_ mpkey: Int32, delta: Double) { + mp_input_put_wheel(input, mpkey, delta) + } + + func nextChangedOption(property: inout UnsafeMutableRawPointer?) -> Bool { + return m_config_cache_get_next_changed(optsCachePtr, &property) + } + + func setOption(fullscreen: Bool) { + optsPtr.pointee.fullscreen = fullscreen + _ = withUnsafeMutableBytes(of: &optsPtr.pointee.fullscreen) { (ptr: UnsafeMutableRawBufferPointer) in + m_config_cache_write_opt(optsCachePtr, ptr.baseAddress) + } + } + + func setOption(minimized: Bool) { + optsPtr.pointee.window_minimized = minimized + _ = withUnsafeMutableBytes(of: &optsPtr.pointee.window_minimized) { (ptr: UnsafeMutableRawBufferPointer) in + m_config_cache_write_opt(optsCachePtr, ptr.baseAddress) + } + } + + func setOption(maximized: Bool) { + optsPtr.pointee.window_maximized = maximized + _ = withUnsafeMutableBytes(of: &optsPtr.pointee.window_maximized) { (ptr: UnsafeMutableRawBufferPointer) in + m_config_cache_write_opt(optsCachePtr, ptr.baseAddress) + } + } + + func setMacOptionCallback(_ callback: swift_wakeup_cb_fn, context object: AnyObject) { + m_config_cache_set_wakeup_cb(macOptsCachePtr, callback, MPVHelper.bridge(obj: object)) + } + + func nextChangedMacOption(property: inout UnsafeMutableRawPointer?) -> Bool { + return m_config_cache_get_next_changed(macOptsCachePtr, &property) + } + + func command(_ cmd: String) { + let cCmd = UnsafePointer<Int8>(strdup(cmd)) + let mpvCmd = mp_input_parse_cmd(input, bstr0(cCmd), "") + mp_input_queue_cmd(input, mpvCmd) + free(UnsafeMutablePointer(mutating: cCmd)) + } + + // (__bridge void*) + class func bridge<T: AnyObject>(obj: T) -> UnsafeMutableRawPointer { + return UnsafeMutableRawPointer(Unmanaged.passUnretained(obj).toOpaque()) + } + + // (__bridge T*) + class func bridge<T: AnyObject>(ptr: UnsafeRawPointer) -> T { + return Unmanaged<T>.fromOpaque(ptr).takeUnretainedValue() + } + + class func withUnsafeMutableRawPointers(_ arguments: [Any], + pointers: [UnsafeMutableRawPointer?] = [], + closure: (_ pointers: [UnsafeMutableRawPointer?]) -> Void) { + if arguments.count > 0 { + let args = Array(arguments.dropFirst(1)) + var newPtrs = pointers + var firstArg = arguments.first + withUnsafeMutableBytes(of: &firstArg) { (ptr: UnsafeMutableRawBufferPointer) in + newPtrs.append(ptr.baseAddress) + withUnsafeMutableRawPointers(args, pointers: newPtrs, closure: closure) + } + + return + } + + closure(pointers) + } + + class func getPointer<T>(_ value: inout T) -> UnsafeMutableRawPointer? { + return withUnsafeMutableBytes(of: &value) { (ptr: UnsafeMutableRawBufferPointer) in + ptr.baseAddress + } + } +} diff --git a/osdep/macos/precise_timer.swift b/osdep/macos/precise_timer.swift new file mode 100644 index 0000000..f4ad3bb --- /dev/null +++ b/osdep/macos/precise_timer.swift @@ -0,0 +1,153 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +import Cocoa + +struct Timing { + let time: UInt64 + let closure: () -> () +} + +class PreciseTimer { + unowned var common: Common + var mpv: MPVHelper? { get { return common.mpv } } + + let nanoPerSecond: Double = 1e+9 + let machToNano: Double = { + var timebase: mach_timebase_info = mach_timebase_info() + mach_timebase_info(&timebase) + return Double(timebase.numer) / Double(timebase.denom) + }() + + let condition = NSCondition() + var events: [Timing] = [] + var isRunning: Bool = true + var isHighPrecision: Bool = false + + var thread: pthread_t! + var threadPort: thread_port_t = thread_port_t() + let policyFlavor = thread_policy_flavor_t(THREAD_TIME_CONSTRAINT_POLICY) + let policyCount = MemoryLayout<thread_time_constraint_policy>.size / + MemoryLayout<integer_t>.size + var typeNumber: mach_msg_type_number_t { + return mach_msg_type_number_t(policyCount) + } + var threadAttr: pthread_attr_t = { + var attr = pthread_attr_t() + var param = sched_param() + pthread_attr_init(&attr) + param.sched_priority = sched_get_priority_max(SCHED_FIFO) + pthread_attr_setschedparam(&attr, ¶m) + pthread_attr_setschedpolicy(&attr, SCHED_FIFO) + return attr + }() + + init?(common com: Common) { + common = com + + pthread_create(&thread, &threadAttr, entryC, MPVHelper.bridge(obj: self)) + if thread == nil { + common.log.sendWarning("Couldn't create pthread for high precision timer") + return nil + } + + threadPort = pthread_mach_thread_np(thread) + } + + func updatePolicy(periodSeconds: Double = 1 / 60.0) { + let period = periodSeconds * nanoPerSecond / machToNano + var policy = thread_time_constraint_policy( + period: UInt32(period), + computation: UInt32(0.75 * period), + constraint: UInt32(0.85 * period), + preemptible: 1 + ) + + let success = withUnsafeMutablePointer(to: &policy) { + $0.withMemoryRebound(to: integer_t.self, capacity: policyCount) { + thread_policy_set(threadPort, policyFlavor, $0, typeNumber) + } + } + + isHighPrecision = success == KERN_SUCCESS + if !isHighPrecision { + common.log.sendWarning("Couldn't create a high precision timer") + } + } + + func terminate() { + condition.lock() + isRunning = false + condition.signal() + condition.unlock() + pthread_kill(thread, SIGALRM) + pthread_join(thread, nil) + } + + func scheduleAt(time: UInt64, closure: @escaping () -> ()) { + condition.lock() + let firstEventTime = events.first?.time ?? 0 + let lastEventTime = events.last?.time ?? 0 + events.append(Timing(time: time, closure: closure)) + + if lastEventTime > time { + events.sort{ $0.time < $1.time } + } + + condition.signal() + condition.unlock() + + if firstEventTime > time { + pthread_kill(thread, SIGALRM) + } + } + + let threadSignal: @convention(c) (Int32) -> () = { (sig: Int32) in } + + let entryC: @convention(c) (UnsafeMutableRawPointer) -> UnsafeMutableRawPointer? = { (ptr: UnsafeMutableRawPointer) in + let ptimer: PreciseTimer = MPVHelper.bridge(ptr: ptr) + ptimer.entry() + return nil + } + + func entry() { + signal(SIGALRM, threadSignal) + + while isRunning { + condition.lock() + while events.count == 0 && isRunning { + condition.wait() + } + + if !isRunning { break } + + guard let event = events.first else { + continue + } + condition.unlock() + + mach_wait_until(event.time) + + condition.lock() + if events.first?.time == event.time && isRunning { + event.closure() + events.removeFirst() + } + condition.unlock() + } + } +} diff --git a/osdep/macos/remote_command_center.swift b/osdep/macos/remote_command_center.swift new file mode 100644 index 0000000..6fb2229 --- /dev/null +++ b/osdep/macos/remote_command_center.swift @@ -0,0 +1,191 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +import MediaPlayer + +class RemoteCommandCenter: NSObject { + enum KeyType { + case normal + case repeatable + } + + var config: [MPRemoteCommand:[String:Any]] = [ + MPRemoteCommandCenter.shared().pauseCommand: [ + "mpKey": MP_KEY_PAUSEONLY, + "keyType": KeyType.normal + ], + MPRemoteCommandCenter.shared().playCommand: [ + "mpKey": MP_KEY_PLAYONLY, + "keyType": KeyType.normal + ], + MPRemoteCommandCenter.shared().stopCommand: [ + "mpKey": MP_KEY_STOP, + "keyType": KeyType.normal + ], + MPRemoteCommandCenter.shared().nextTrackCommand: [ + "mpKey": MP_KEY_NEXT, + "keyType": KeyType.normal + ], + MPRemoteCommandCenter.shared().previousTrackCommand: [ + "mpKey": MP_KEY_PREV, + "keyType": KeyType.normal + ], + MPRemoteCommandCenter.shared().togglePlayPauseCommand: [ + "mpKey": MP_KEY_PLAY, + "keyType": KeyType.normal + ], + MPRemoteCommandCenter.shared().seekForwardCommand: [ + "mpKey": MP_KEY_FORWARD, + "keyType": KeyType.repeatable, + "state": MP_KEY_STATE_UP + ], + MPRemoteCommandCenter.shared().seekBackwardCommand: [ + "mpKey": MP_KEY_REWIND, + "keyType": KeyType.repeatable, + "state": MP_KEY_STATE_UP + ], + ] + + var nowPlayingInfo: [String: Any] = [ + MPNowPlayingInfoPropertyMediaType: NSNumber(value: MPNowPlayingInfoMediaType.video.rawValue), + MPNowPlayingInfoPropertyDefaultPlaybackRate: NSNumber(value: 1), + MPNowPlayingInfoPropertyPlaybackProgress: NSNumber(value: 0.0), + MPMediaItemPropertyPlaybackDuration: NSNumber(value: 0), + MPMediaItemPropertyTitle: "mpv", + MPMediaItemPropertyAlbumTitle: "mpv", + MPMediaItemPropertyArtist: "mpv", + ] + + let disabledCommands: [MPRemoteCommand] = [ + MPRemoteCommandCenter.shared().changePlaybackRateCommand, + MPRemoteCommandCenter.shared().changeRepeatModeCommand, + MPRemoteCommandCenter.shared().changeShuffleModeCommand, + MPRemoteCommandCenter.shared().skipForwardCommand, + MPRemoteCommandCenter.shared().skipBackwardCommand, + MPRemoteCommandCenter.shared().changePlaybackPositionCommand, + MPRemoteCommandCenter.shared().enableLanguageOptionCommand, + MPRemoteCommandCenter.shared().disableLanguageOptionCommand, + MPRemoteCommandCenter.shared().ratingCommand, + MPRemoteCommandCenter.shared().likeCommand, + MPRemoteCommandCenter.shared().dislikeCommand, + MPRemoteCommandCenter.shared().bookmarkCommand, + ] + + var mpInfoCenter: MPNowPlayingInfoCenter { get { return MPNowPlayingInfoCenter.default() } } + var isPaused: Bool = false { didSet { updatePlaybackState() } } + + @objc override init() { + super.init() + + for cmd in disabledCommands { + cmd.isEnabled = false + } + } + + @objc func start() { + for (cmd, _) in config { + cmd.isEnabled = true + cmd.addTarget { [unowned self] event in + return self.cmdHandler(event) + } + } + + if let app = NSApp as? Application, let icon = app.getMPVIcon() { + let albumArt = MPMediaItemArtwork(boundsSize: icon.size) { _ in + return icon + } + nowPlayingInfo[MPMediaItemPropertyArtwork] = albumArt + } + + mpInfoCenter.nowPlayingInfo = nowPlayingInfo + mpInfoCenter.playbackState = .playing + + NotificationCenter.default.addObserver( + self, + selector: #selector(self.makeCurrent), + name: NSApplication.willBecomeActiveNotification, + object: nil + ) + } + + @objc func stop() { + for (cmd, _) in config { + cmd.isEnabled = false + cmd.removeTarget(nil) + } + + mpInfoCenter.nowPlayingInfo = nil + mpInfoCenter.playbackState = .unknown + } + + @objc func makeCurrent(notification: NSNotification) { + mpInfoCenter.playbackState = .paused + mpInfoCenter.playbackState = .playing + updatePlaybackState() + } + + func updatePlaybackState() { + mpInfoCenter.playbackState = isPaused ? .paused : .playing + } + + func cmdHandler(_ event: MPRemoteCommandEvent) -> MPRemoteCommandHandlerStatus { + guard let cmdConfig = config[event.command], + let mpKey = cmdConfig["mpKey"] as? Int32, + let keyType = cmdConfig["keyType"] as? KeyType else + { + return .commandFailed + } + + var state = cmdConfig["state"] as? UInt32 ?? 0 + + if let currentState = cmdConfig["state"] as? UInt32, keyType == .repeatable { + state = MP_KEY_STATE_DOWN + config[event.command]?["state"] = MP_KEY_STATE_DOWN + if currentState == MP_KEY_STATE_DOWN { + state = MP_KEY_STATE_UP + config[event.command]?["state"] = MP_KEY_STATE_UP + } + } + + EventsResponder.sharedInstance().handleMPKey(mpKey, withMask: Int32(state)) + + return .success + } + + @objc func processEvent(_ event: UnsafeMutablePointer<mpv_event>) { + switch event.pointee.event_id { + case MPV_EVENT_PROPERTY_CHANGE: + handlePropertyChange(event) + default: + break + } + } + + func handlePropertyChange(_ event: UnsafeMutablePointer<mpv_event>) { + let pData = OpaquePointer(event.pointee.data) + guard let property = UnsafePointer<mpv_event_property>(pData)?.pointee else { + return + } + + switch String(cString: property.name) { + case "pause" where property.format == MPV_FORMAT_FLAG: + isPaused = LibmpvHelper.mpvFlagToBool(property.data) ?? false + default: + break + } + } +} diff --git a/osdep/macos/swift_compat.swift b/osdep/macos/swift_compat.swift new file mode 100644 index 0000000..83059da --- /dev/null +++ b/osdep/macos/swift_compat.swift @@ -0,0 +1,36 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + + +#if !swift(>=5.0) +extension Data { + mutating func withUnsafeMutableBytes<Type>(_ body: (UnsafeMutableRawBufferPointer) throws -> Type) rethrows -> Type { + let dataCount = count + return try withUnsafeMutableBytes { (ptr: UnsafeMutablePointer<UInt8>) throws -> Type in + try body(UnsafeMutableRawBufferPointer(start: ptr, count: dataCount)) + } + } +} +#endif + +#if !swift(>=4.2) +extension NSDraggingInfo { + var draggingPasteboard: NSPasteboard { + get { return draggingPasteboard() } + } +} +#endif diff --git a/osdep/macos/swift_extensions.swift b/osdep/macos/swift_extensions.swift new file mode 100644 index 0000000..127c568 --- /dev/null +++ b/osdep/macos/swift_extensions.swift @@ -0,0 +1,58 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +import Cocoa + +extension NSDeviceDescriptionKey { + static let screenNumber = NSDeviceDescriptionKey("NSScreenNumber") +} + +extension NSScreen { + + public var displayID: CGDirectDisplayID { + get { + return deviceDescription[.screenNumber] as? CGDirectDisplayID ?? 0 + } + } +} + +extension NSColor { + + convenience init(hex: String) { + let int = Int(hex.dropFirst(), radix: 16) ?? 0 + let alpha = CGFloat((int >> 24) & 0x000000FF)/255 + let red = CGFloat((int >> 16) & 0x000000FF)/255 + let green = CGFloat((int >> 8) & 0x000000FF)/255 + let blue = CGFloat((int) & 0x000000FF)/255 + + self.init(calibratedRed: red, green: green, blue: blue, alpha: alpha) + } +} + +extension Bool { + + init(_ int32: Int32) { + self.init(int32 != 0) + } +} + +extension Int32 { + + init(_ bool: Bool) { + self.init(bool ? 1 : 0) + } +} diff --git a/osdep/macosx_application.h b/osdep/macosx_application.h new file mode 100644 index 0000000..753b9f0 --- /dev/null +++ b/osdep/macosx_application.h @@ -0,0 +1,55 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef MPV_MACOSX_APPLICATION +#define MPV_MACOSX_APPLICATION + +#include "osdep/macosx_menubar.h" +#include "options/m_option.h" + +enum { + FRAME_VISIBLE = 0, + FRAME_WHOLE, +}; + +enum { + RENDER_TIMER_CALLBACK = 0, + RENDER_TIMER_PRECISE, + RENDER_TIMER_SYSTEM, +}; + +struct macos_opts { + int macos_title_bar_style; + int macos_title_bar_appearance; + int macos_title_bar_material; + struct m_color macos_title_bar_color; + int macos_fs_animation_duration; + bool macos_force_dedicated_gpu; + int macos_app_activation_policy; + int macos_geometry_calculation; + int macos_render_timer; + int cocoa_cb_sw_renderer; + bool cocoa_cb_10bit_context; +}; + +// multithreaded wrapper for mpv_main +int cocoa_main(int argc, char *argv[]); +void cocoa_register_menu_item_action(MPMenuKey key, void* action); + +extern const struct m_sub_options macos_conf; + +#endif /* MPV_MACOSX_APPLICATION */ diff --git a/osdep/macosx_application.m b/osdep/macosx_application.m new file mode 100644 index 0000000..73503ad --- /dev/null +++ b/osdep/macosx_application.m @@ -0,0 +1,375 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <stdio.h> +#include "config.h" +#include "mpv_talloc.h" + +#include "common/msg.h" +#include "input/input.h" +#include "player/client.h" +#include "options/m_config.h" +#include "options/options.h" + +#import "osdep/macosx_application_objc.h" +#import "osdep/macosx_events_objc.h" +#include "osdep/threads.h" +#include "osdep/main-fn.h" + +#if HAVE_MACOS_TOUCHBAR +#import "osdep/macosx_touchbar.h" +#endif +#if HAVE_MACOS_COCOA_CB +#include "osdep/macOS_swift.h" +#endif + +#define MPV_PROTOCOL @"mpv://" + +#define OPT_BASE_STRUCT struct macos_opts +const struct m_sub_options macos_conf = { + .opts = (const struct m_option[]) { + {"macos-title-bar-appearance", OPT_CHOICE(macos_title_bar_appearance, + {"auto", 0}, {"aqua", 1}, {"darkAqua", 2}, + {"vibrantLight", 3}, {"vibrantDark", 4}, + {"aquaHighContrast", 5}, {"darkAquaHighContrast", 6}, + {"vibrantLightHighContrast", 7}, + {"vibrantDarkHighContrast", 8})}, + {"macos-title-bar-material", OPT_CHOICE(macos_title_bar_material, + {"titlebar", 0}, {"selection", 1}, {"menu", 2}, + {"popover", 3}, {"sidebar", 4}, {"headerView", 5}, + {"sheet", 6}, {"windowBackground", 7}, {"hudWindow", 8}, + {"fullScreen", 9}, {"toolTip", 10}, {"contentBackground", 11}, + {"underWindowBackground", 12}, {"underPageBackground", 13}, + {"dark", 14}, {"light", 15}, {"mediumLight", 16}, + {"ultraDark", 17})}, + {"macos-title-bar-color", OPT_COLOR(macos_title_bar_color)}, + {"macos-fs-animation-duration", + OPT_CHOICE(macos_fs_animation_duration, {"default", -1}), + M_RANGE(0, 1000)}, + {"macos-force-dedicated-gpu", OPT_BOOL(macos_force_dedicated_gpu)}, + {"macos-app-activation-policy", OPT_CHOICE(macos_app_activation_policy, + {"regular", 0}, {"accessory", 1}, {"prohibited", 2})}, + {"macos-geometry-calculation", OPT_CHOICE(macos_geometry_calculation, + {"visible", FRAME_VISIBLE}, {"whole", FRAME_WHOLE})}, + {"macos-render-timer", OPT_CHOICE(macos_render_timer, + {"callback", RENDER_TIMER_CALLBACK}, {"precise", RENDER_TIMER_PRECISE}, + {"system", RENDER_TIMER_SYSTEM})}, + {"cocoa-cb-sw-renderer", OPT_CHOICE(cocoa_cb_sw_renderer, + {"auto", -1}, {"no", 0}, {"yes", 1})}, + {"cocoa-cb-10bit-context", OPT_BOOL(cocoa_cb_10bit_context)}, + {0} + }, + .size = sizeof(struct macos_opts), + .defaults = &(const struct macos_opts){ + .macos_title_bar_color = {0, 0, 0, 0}, + .macos_fs_animation_duration = -1, + .cocoa_cb_sw_renderer = -1, + .cocoa_cb_10bit_context = true + }, +}; + +// Whether the NSApplication singleton was created. If this is false, we are +// running in libmpv mode, and cocoa_main() was never called. +static bool application_instantiated; + +static mp_thread playback_thread_id; + +@interface Application () +{ + EventsResponder *_eventsResponder; +} + +@end + +static Application *mpv_shared_app(void) +{ + return (Application *)[Application sharedApplication]; +} + +static void terminate_cocoa_application(void) +{ + dispatch_async(dispatch_get_main_queue(), ^{ + [NSApp hide:NSApp]; + [NSApp terminate:NSApp]; + }); +} + +@implementation Application +@synthesize menuBar = _menu_bar; +@synthesize openCount = _open_count; +@synthesize cocoaCB = _cocoa_cb; + +- (void)sendEvent:(NSEvent *)event +{ + if ([self modalWindow] || ![_eventsResponder processKeyEvent:event]) + [super sendEvent:event]; + [_eventsResponder wakeup]; +} + +- (id)init +{ + if (self = [super init]) { + _eventsResponder = [EventsResponder sharedInstance]; + + NSAppleEventManager *em = [NSAppleEventManager sharedAppleEventManager]; + [em setEventHandler:self + andSelector:@selector(getUrl:withReplyEvent:) + forEventClass:kInternetEventClass + andEventID:kAEGetURL]; + } + + return self; +} + +- (void)dealloc +{ + NSAppleEventManager *em = [NSAppleEventManager sharedAppleEventManager]; + [em removeEventHandlerForEventClass:kInternetEventClass + andEventID:kAEGetURL]; + [em removeEventHandlerForEventClass:kCoreEventClass + andEventID:kAEQuitApplication]; + [super dealloc]; +} + +static const char macosx_icon[] = +#include "TOOLS/osxbundle/icon.icns.inc" +; + +- (NSImage *)getMPVIcon +{ + // The C string contains a trailing null, so we strip it away + NSData *icon_data = [NSData dataWithBytesNoCopy:(void *)macosx_icon + length:sizeof(macosx_icon) - 1 + freeWhenDone:NO]; + return [[NSImage alloc] initWithData:icon_data]; +} + +#if HAVE_MACOS_TOUCHBAR +- (NSTouchBar *)makeTouchBar +{ + TouchBar *tBar = [[TouchBar alloc] init]; + [tBar setApp:self]; + tBar.delegate = tBar; + tBar.customizationIdentifier = customID; + tBar.defaultItemIdentifiers = @[play, previousItem, nextItem, seekBar]; + tBar.customizationAllowedItemIdentifiers = @[play, seekBar, previousItem, + nextItem, previousChapter, nextChapter, cycleAudio, cycleSubtitle, + currentPosition, timeLeft]; + return tBar; +} +#endif + +- (void)processEvent:(struct mpv_event *)event +{ +#if HAVE_MACOS_TOUCHBAR + [(TouchBar *)self.touchBar processEvent:event]; +#endif + if (_cocoa_cb) { + [_cocoa_cb processEvent:event]; + } +} + +- (void)setMpvHandle:(struct mpv_handle *)ctx +{ +#if HAVE_MACOS_COCOA_CB + [NSApp setCocoaCB:[[CocoaCB alloc] init:ctx]]; +#endif +} + +- (const struct m_sub_options *)getMacOSConf +{ + return &macos_conf; +} + +- (const struct m_sub_options *)getVoSubConf +{ + return &vo_sub_opts; +} + +- (void)queueCommand:(char *)cmd +{ + [_eventsResponder queueCommand:cmd]; +} + +- (void)stopMPV:(char *)cmd +{ + if (![_eventsResponder queueCommand:cmd]) + terminate_cocoa_application(); +} + +- (void)applicationWillFinishLaunching:(NSNotification *)notification +{ + NSAppleEventManager *em = [NSAppleEventManager sharedAppleEventManager]; + [em setEventHandler:self + andSelector:@selector(handleQuitEvent:withReplyEvent:) + forEventClass:kCoreEventClass + andEventID:kAEQuitApplication]; +} + +- (void)handleQuitEvent:(NSAppleEventDescriptor *)event + withReplyEvent:(NSAppleEventDescriptor *)replyEvent +{ + [self stopMPV:"quit"]; +} + +- (void)getUrl:(NSAppleEventDescriptor *)event + withReplyEvent:(NSAppleEventDescriptor *)replyEvent +{ + NSString *url = + [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; + + url = [url stringByReplacingOccurrencesOfString:MPV_PROTOCOL + withString:@"" + options:NSAnchoredSearch + range:NSMakeRange(0, [MPV_PROTOCOL length])]; + + url = [url stringByRemovingPercentEncoding]; + [_eventsResponder handleFilesArray:@[url]]; +} + +- (void)application:(NSApplication *)sender openFiles:(NSArray *)filenames +{ + if (mpv_shared_app().openCount > 0) { + mpv_shared_app().openCount--; + return; + } + [self openFiles:filenames]; +} + +- (void)openFiles:(NSArray *)filenames +{ + SEL cmpsel = @selector(localizedStandardCompare:); + NSArray *files = [filenames sortedArrayUsingSelector:cmpsel]; + [_eventsResponder handleFilesArray:files]; +} +@end + +struct playback_thread_ctx { + int *argc; + char ***argv; +}; + +static void cocoa_run_runloop(void) +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + [NSApp run]; + [pool drain]; +} + +static MP_THREAD_VOID playback_thread(void *ctx_obj) +{ + mp_thread_set_name("core/playback"); + @autoreleasepool { + struct playback_thread_ctx *ctx = (struct playback_thread_ctx*) ctx_obj; + int r = mpv_main(*ctx->argc, *ctx->argv); + terminate_cocoa_application(); + // normally never reached - unless the cocoa mainloop hasn't started yet + exit(r); + } +} + +void cocoa_register_menu_item_action(MPMenuKey key, void* action) +{ + if (application_instantiated) + [[NSApp menuBar] registerSelector:(SEL)action forKey:key]; +} + +static void init_cocoa_application(bool regular) +{ + NSApp = mpv_shared_app(); + [NSApp setDelegate:NSApp]; + [NSApp setMenuBar:[[MenuBar alloc] init]]; + + // Will be set to Regular from cocoa_common during UI creation so that we + // don't create an icon when playing audio only files. + [NSApp setActivationPolicy: regular ? + NSApplicationActivationPolicyRegular : + NSApplicationActivationPolicyAccessory]; + + atexit_b(^{ + // Because activation policy has just been set to behave like a real + // application, that policy must be reset on exit to prevent, among + // other things, the menubar created here from remaining on screen. + dispatch_async(dispatch_get_main_queue(), ^{ + [NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited]; + }); + }); +} + +static bool bundle_started_from_finder() +{ + NSString* bundle = [[[NSProcessInfo processInfo] environment] objectForKey:@"MPVBUNDLE"]; + return [bundle isEqual:@"true"]; +} + +static bool is_psn_argument(char *arg_to_check) +{ + NSString *arg = [NSString stringWithUTF8String:arg_to_check]; + return [arg hasPrefix:@"-psn_"]; +} + +static void setup_bundle(int *argc, char *argv[]) +{ + if (*argc > 1 && is_psn_argument(argv[1])) { + *argc = 1; + argv[1] = NULL; + } + + NSDictionary *env = [[NSProcessInfo processInfo] environment]; + NSString *path_bundle = [env objectForKey:@"PATH"]; + NSString *path_new = [NSString stringWithFormat:@"%@:%@:%@:%@:%@", + path_bundle, + @"/usr/local/bin", + @"/usr/local/sbin", + @"/opt/local/bin", + @"/opt/local/sbin"]; + setenv("PATH", [path_new UTF8String], 1); +} + +int cocoa_main(int argc, char *argv[]) +{ + @autoreleasepool { + application_instantiated = true; + [[EventsResponder sharedInstance] setIsApplication:YES]; + + struct playback_thread_ctx ctx = {0}; + ctx.argc = &argc; + ctx.argv = &argv; + + if (bundle_started_from_finder()) { + setup_bundle(&argc, argv); + init_cocoa_application(true); + } else { + for (int i = 1; i < argc; i++) + if (argv[i][0] != '-') + mpv_shared_app().openCount++; + init_cocoa_application(false); + } + + mp_thread_create(&playback_thread_id, playback_thread, &ctx); + [[EventsResponder sharedInstance] waitForInputContext]; + cocoa_run_runloop(); + + // This should never be reached: cocoa_run_runloop blocks until the + // process is quit + fprintf(stderr, "There was either a problem " + "initializing Cocoa or the Runloop was stopped unexpectedly. " + "Please report this issues to a developer.\n"); + mp_thread_join(playback_thread_id); + return 1; + } +} diff --git a/osdep/macosx_application_objc.h b/osdep/macosx_application_objc.h new file mode 100644 index 0000000..11959a8 --- /dev/null +++ b/osdep/macosx_application_objc.h @@ -0,0 +1,40 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#import <Cocoa/Cocoa.h> +#include "osdep/macosx_application.h" +#import "osdep/macosx_menubar_objc.h" + +@class CocoaCB; +struct mpv_event; +struct mpv_handle; + +@interface Application : NSApplication + +- (NSImage *)getMPVIcon; +- (void)processEvent:(struct mpv_event *)event; +- (void)queueCommand:(char *)cmd; +- (void)stopMPV:(char *)cmd; +- (void)openFiles:(NSArray *)filenames; +- (void)setMpvHandle:(struct mpv_handle *)ctx; +- (const struct m_sub_options *)getMacOSConf; +- (const struct m_sub_options *)getVoSubConf; + +@property(nonatomic, retain) MenuBar *menuBar; +@property(nonatomic, assign) size_t openCount; +@property(nonatomic, retain) CocoaCB *cocoaCB; +@end diff --git a/osdep/macosx_events.h b/osdep/macosx_events.h new file mode 100644 index 0000000..9188c8b --- /dev/null +++ b/osdep/macosx_events.h @@ -0,0 +1,36 @@ +/* + * Cocoa Application Event Handling + * + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef MACOSX_EVENTS_H +#define MACOSX_EVENTS_H +#include "input/keycodes.h" + +struct input_ctx; +struct mpv_handle; + +void cocoa_put_key(int keycode); +void cocoa_put_key_with_modifiers(int keycode, int modifiers); + +void cocoa_init_media_keys(void); +void cocoa_uninit_media_keys(void); + +void cocoa_set_input_context(struct input_ctx *input_context); +void cocoa_set_mpv_handle(struct mpv_handle *ctx); + +#endif diff --git a/osdep/macosx_events.m b/osdep/macosx_events.m new file mode 100644 index 0000000..627077a --- /dev/null +++ b/osdep/macosx_events.m @@ -0,0 +1,408 @@ +/* + * Cocoa Application Event Handling + * + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +// Carbon header is included but Carbon is NOT linked to mpv's binary. This +// file only needs this include to use the keycode definitions in keymap. +#import <Carbon/Carbon.h> + +// Media keys definitions +#import <IOKit/hidsystem/ev_keymap.h> +#import <Cocoa/Cocoa.h> + +#include "mpv_talloc.h" +#include "input/event.h" +#include "input/input.h" +#include "player/client.h" +#include "input/keycodes.h" +// doesn't make much sense, but needed to access keymap functionality +#include "video/out/vo.h" + +#import "osdep/macosx_events_objc.h" +#import "osdep/macosx_application_objc.h" + +#include "config.h" + +#if HAVE_MACOS_COCOA_CB +#include "osdep/macOS_swift.h" +#endif + +@interface EventsResponder () +{ + struct input_ctx *_inputContext; + struct mpv_handle *_ctx; + BOOL _is_application; + NSCondition *_input_lock; +} + +- (NSEvent *)handleKey:(NSEvent *)event; +- (BOOL)setMpvHandle:(struct mpv_handle *)ctx; +- (void)readEvents; +- (void)startMediaKeys; +- (void)stopMediaKeys; +- (int)mapKeyModifiers:(int)cocoaModifiers; +- (int)keyModifierMask:(NSEvent *)event; +@end + + +#define NSLeftAlternateKeyMask (0x000020 | NSEventModifierFlagOption) +#define NSRightAlternateKeyMask (0x000040 | NSEventModifierFlagOption) + +static bool LeftAltPressed(int mask) +{ + return (mask & NSLeftAlternateKeyMask) == NSLeftAlternateKeyMask; +} + +static bool RightAltPressed(int mask) +{ + return (mask & NSRightAlternateKeyMask) == NSRightAlternateKeyMask; +} + +static const struct mp_keymap keymap[] = { + // special keys + {kVK_Return, MP_KEY_ENTER}, {kVK_Escape, MP_KEY_ESC}, + {kVK_Delete, MP_KEY_BACKSPACE}, {kVK_Option, MP_KEY_BACKSPACE}, + {kVK_Control, MP_KEY_BACKSPACE}, {kVK_Shift, MP_KEY_BACKSPACE}, + {kVK_Tab, MP_KEY_TAB}, + + // cursor keys + {kVK_UpArrow, MP_KEY_UP}, {kVK_DownArrow, MP_KEY_DOWN}, + {kVK_LeftArrow, MP_KEY_LEFT}, {kVK_RightArrow, MP_KEY_RIGHT}, + + // navigation block + {kVK_Help, MP_KEY_INSERT}, {kVK_ForwardDelete, MP_KEY_DELETE}, + {kVK_Home, MP_KEY_HOME}, {kVK_End, MP_KEY_END}, + {kVK_PageUp, MP_KEY_PAGE_UP}, {kVK_PageDown, MP_KEY_PAGE_DOWN}, + + // F-keys + {kVK_F1, MP_KEY_F + 1}, {kVK_F2, MP_KEY_F + 2}, {kVK_F3, MP_KEY_F + 3}, + {kVK_F4, MP_KEY_F + 4}, {kVK_F5, MP_KEY_F + 5}, {kVK_F6, MP_KEY_F + 6}, + {kVK_F7, MP_KEY_F + 7}, {kVK_F8, MP_KEY_F + 8}, {kVK_F9, MP_KEY_F + 9}, + {kVK_F10, MP_KEY_F + 10}, {kVK_F11, MP_KEY_F + 11}, {kVK_F12, MP_KEY_F + 12}, + {kVK_F13, MP_KEY_F + 13}, {kVK_F14, MP_KEY_F + 14}, {kVK_F15, MP_KEY_F + 15}, + {kVK_F16, MP_KEY_F + 16}, {kVK_F17, MP_KEY_F + 17}, {kVK_F18, MP_KEY_F + 18}, + {kVK_F19, MP_KEY_F + 19}, {kVK_F20, MP_KEY_F + 20}, + + // numpad + {kVK_ANSI_KeypadPlus, '+'}, {kVK_ANSI_KeypadMinus, '-'}, + {kVK_ANSI_KeypadMultiply, '*'}, {kVK_ANSI_KeypadDivide, '/'}, + {kVK_ANSI_KeypadEnter, MP_KEY_KPENTER}, + {kVK_ANSI_KeypadDecimal, MP_KEY_KPDEC}, + {kVK_ANSI_Keypad0, MP_KEY_KP0}, {kVK_ANSI_Keypad1, MP_KEY_KP1}, + {kVK_ANSI_Keypad2, MP_KEY_KP2}, {kVK_ANSI_Keypad3, MP_KEY_KP3}, + {kVK_ANSI_Keypad4, MP_KEY_KP4}, {kVK_ANSI_Keypad5, MP_KEY_KP5}, + {kVK_ANSI_Keypad6, MP_KEY_KP6}, {kVK_ANSI_Keypad7, MP_KEY_KP7}, + {kVK_ANSI_Keypad8, MP_KEY_KP8}, {kVK_ANSI_Keypad9, MP_KEY_KP9}, + + {0, 0} +}; + +static int convert_key(unsigned key, unsigned charcode) +{ + int mpkey = lookup_keymap_table(keymap, key); + if (mpkey) + return mpkey; + return charcode; +} + +void cocoa_init_media_keys(void) +{ + [[EventsResponder sharedInstance] startMediaKeys]; +} + +void cocoa_uninit_media_keys(void) +{ + [[EventsResponder sharedInstance] stopMediaKeys]; +} + +void cocoa_put_key(int keycode) +{ + [[EventsResponder sharedInstance] putKey:keycode]; +} + +void cocoa_put_key_with_modifiers(int keycode, int modifiers) +{ + keycode |= [[EventsResponder sharedInstance] mapKeyModifiers:modifiers]; + cocoa_put_key(keycode); +} + +void cocoa_set_input_context(struct input_ctx *input_context) +{ + [[EventsResponder sharedInstance] setInputContext:input_context]; +} + +static void wakeup(void *context) +{ + [[EventsResponder sharedInstance] readEvents]; +} + +void cocoa_set_mpv_handle(struct mpv_handle *ctx) +{ + if ([[EventsResponder sharedInstance] setMpvHandle:ctx]) { + mpv_observe_property(ctx, 0, "duration", MPV_FORMAT_DOUBLE); + mpv_observe_property(ctx, 0, "time-pos", MPV_FORMAT_DOUBLE); + mpv_observe_property(ctx, 0, "pause", MPV_FORMAT_FLAG); + mpv_set_wakeup_callback(ctx, wakeup, NULL); + } +} + +@implementation EventsResponder + +@synthesize remoteCommandCenter = _remoteCommandCenter; + ++ (EventsResponder *)sharedInstance +{ + static EventsResponder *responder = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + responder = [EventsResponder new]; + }); + return responder; +} + +- (id)init +{ + self = [super init]; + if (self) { + _input_lock = [NSCondition new]; + } + return self; +} + +- (void)waitForInputContext +{ + [_input_lock lock]; + while (!_inputContext) + [_input_lock wait]; + [_input_lock unlock]; +} + +- (void)setInputContext:(struct input_ctx *)ctx +{ + [_input_lock lock]; + _inputContext = ctx; + [_input_lock signal]; + [_input_lock unlock]; +} + +- (void)wakeup +{ + [_input_lock lock]; + if (_inputContext) + mp_input_wakeup(_inputContext); + [_input_lock unlock]; +} + +- (bool)queueCommand:(char *)cmd +{ + bool r = false; + [_input_lock lock]; + if (_inputContext) { + mp_cmd_t *cmdt = mp_input_parse_cmd(_inputContext, bstr0(cmd), ""); + mp_input_queue_cmd(_inputContext, cmdt); + r = true; + } + [_input_lock unlock]; + return r; +} + +- (void)putKey:(int)keycode +{ + [_input_lock lock]; + if (_inputContext) + mp_input_put_key(_inputContext, keycode); + [_input_lock unlock]; +} + +- (BOOL)useAltGr +{ + BOOL r = YES; + [_input_lock lock]; + if (_inputContext) + r = mp_input_use_alt_gr(_inputContext); + [_input_lock unlock]; + return r; +} + +- (void)setIsApplication:(BOOL)isApplication +{ + _is_application = isApplication; +} + +- (BOOL)setMpvHandle:(struct mpv_handle *)ctx +{ + if (_is_application) { + dispatch_sync(dispatch_get_main_queue(), ^{ + _ctx = ctx; + [NSApp setMpvHandle:ctx]; + }); + return YES; + } else { + mpv_destroy(ctx); + return NO; + } +} + +- (void)readEvents +{ + dispatch_async(dispatch_get_main_queue(), ^{ + while (_ctx) { + mpv_event *event = mpv_wait_event(_ctx, 0); + if (event->event_id == MPV_EVENT_NONE) + break; + [self processEvent:event]; + } + }); +} + +-(void)processEvent:(struct mpv_event *)event +{ + if(_is_application) { + [NSApp processEvent:event]; + } + + if (_remoteCommandCenter) { + [_remoteCommandCenter processEvent:event]; + } + + switch (event->event_id) { + case MPV_EVENT_SHUTDOWN: { +#if HAVE_MACOS_COCOA_CB + if ([(Application *)NSApp cocoaCB].isShuttingDown) { + _ctx = nil; + return; + } +#endif + mpv_destroy(_ctx); + _ctx = nil; + break; + } + } +} + +- (void)startMediaKeys +{ +#if HAVE_MACOS_MEDIA_PLAYER + if (_remoteCommandCenter == nil) { + _remoteCommandCenter = [[RemoteCommandCenter alloc] init]; + } +#endif + + [_remoteCommandCenter start]; +} + +- (void)stopMediaKeys +{ + [_remoteCommandCenter stop]; +} + +- (int)mapKeyModifiers:(int)cocoaModifiers +{ + int mask = 0; + if (cocoaModifiers & NSEventModifierFlagShift) + mask |= MP_KEY_MODIFIER_SHIFT; + if (cocoaModifiers & NSEventModifierFlagControl) + mask |= MP_KEY_MODIFIER_CTRL; + if (LeftAltPressed(cocoaModifiers) || + (RightAltPressed(cocoaModifiers) && ![self useAltGr])) + mask |= MP_KEY_MODIFIER_ALT; + if (cocoaModifiers & NSEventModifierFlagCommand) + mask |= MP_KEY_MODIFIER_META; + return mask; +} + +- (int)mapTypeModifiers:(NSEventType)type +{ + NSDictionary *map = @{ + @(NSEventTypeKeyDown) : @(MP_KEY_STATE_DOWN), + @(NSEventTypeKeyUp) : @(MP_KEY_STATE_UP), + }; + return [map[@(type)] intValue]; +} + +- (int)keyModifierMask:(NSEvent *)event +{ + return [self mapKeyModifiers:[event modifierFlags]] | + [self mapTypeModifiers:[event type]]; +} + +-(BOOL)handleMPKey:(int)key withMask:(int)mask +{ + if (key > 0) { + cocoa_put_key(key | mask); + if (mask & MP_KEY_STATE_UP) + cocoa_put_key(MP_INPUT_RELEASE_ALL); + return YES; + } else { + return NO; + } +} + +- (NSEvent*)handleKey:(NSEvent *)event +{ + if ([event isARepeat]) return nil; + + NSString *chars; + + if ([self useAltGr] && RightAltPressed([event modifierFlags])) { + chars = [event characters]; + } else { + chars = [event charactersIgnoringModifiers]; + } + + struct bstr t = bstr0([chars UTF8String]); + int key = convert_key([event keyCode], bstr_decode_utf8(t, &t)); + + if (key > -1) + [self handleMPKey:key withMask:[self keyModifierMask:event]]; + + return nil; +} + +- (bool)processKeyEvent:(NSEvent *)event +{ + if (event.type == NSEventTypeKeyDown || event.type == NSEventTypeKeyUp){ + if (![[NSApp mainMenu] performKeyEquivalent:event]) + [self handleKey:event]; + return true; + } + return false; +} + +- (void)handleFilesArray:(NSArray *)files +{ + enum mp_dnd_action action = [NSEvent modifierFlags] & + NSEventModifierFlagShift ? DND_APPEND : DND_REPLACE; + + size_t num_files = [files count]; + char **files_utf8 = talloc_array(NULL, char*, num_files); + [files enumerateObjectsUsingBlock:^(NSString *p, NSUInteger i, BOOL *_){ + if ([p hasPrefix:@"file:///.file/id="]) + p = [[NSURL URLWithString:p] path]; + char *filename = (char *)[p UTF8String]; + size_t bytes = [p lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + files_utf8[i] = talloc_memdup(files_utf8, filename, bytes + 1); + }]; + [_input_lock lock]; + if (_inputContext) + mp_event_drop_files(_inputContext, num_files, files_utf8, action); + [_input_lock unlock]; + talloc_free(files_utf8); +} + +@end diff --git a/osdep/macosx_events_objc.h b/osdep/macosx_events_objc.h new file mode 100644 index 0000000..9394fe7 --- /dev/null +++ b/osdep/macosx_events_objc.h @@ -0,0 +1,45 @@ +/* + * Cocoa Application Event Handling + * + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#import <Cocoa/Cocoa.h> +#include "osdep/macosx_events.h" + +@class RemoteCommandCenter; +struct input_ctx; + +@interface EventsResponder : NSObject + ++ (EventsResponder *)sharedInstance; +- (void)setInputContext:(struct input_ctx *)ctx; +- (void)setIsApplication:(BOOL)isApplication; + +/// Blocks until inputContext is present. +- (void)waitForInputContext; +- (void)wakeup; +- (void)putKey:(int)keycode; +- (void)handleFilesArray:(NSArray *)files; + +- (bool)queueCommand:(char *)cmd; +- (bool)processKeyEvent:(NSEvent *)event; + +- (BOOL)handleMPKey:(int)key withMask:(int)mask; + +@property(nonatomic, retain) RemoteCommandCenter *remoteCommandCenter; + +@end diff --git a/osdep/macosx_menubar.h b/osdep/macosx_menubar.h new file mode 100644 index 0000000..509083d --- /dev/null +++ b/osdep/macosx_menubar.h @@ -0,0 +1,30 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef MPV_MACOSX_MENU +#define MPV_MACOSX_MENU + +// Menu Keys identifying menu items +typedef enum { + MPM_H_SIZE, + MPM_N_SIZE, + MPM_D_SIZE, + MPM_MINIMIZE, + MPM_ZOOM, +} MPMenuKey; + +#endif /* MPV_MACOSX_MENU */ diff --git a/osdep/macosx_menubar.m b/osdep/macosx_menubar.m new file mode 100644 index 0000000..5c6cd47 --- /dev/null +++ b/osdep/macosx_menubar.m @@ -0,0 +1,853 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" +#include "common/common.h" + +#import "macosx_menubar_objc.h" +#import "osdep/macosx_application_objc.h" + +@implementation MenuBar +{ + NSArray *menuTree; +} + +- (id)init +{ + if (self = [super init]) { + NSUserDefaults *userDefaults =[NSUserDefaults standardUserDefaults]; + [userDefaults setBool:NO forKey:@"NSFullScreenMenuItemEverywhere"]; + [userDefaults setBool:YES forKey:@"NSDisabledDictationMenuItem"]; + [userDefaults setBool:YES forKey:@"NSDisabledCharacterPaletteMenuItem"]; + [NSWindow setAllowsAutomaticWindowTabbing: NO]; + + menuTree = @[ + @{ + @"name": @"Apple", + @"menu": @[ + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"About mpv", + @"action" : @"about", + @"key" : @"", + @"target" : self + }], + @{ @"name": @"separator" }, + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Preferences…", + @"action" : @"preferences:", + @"key" : @",", + @"target" : self, + @"file" : @"mpv.conf", + @"alertTitle1": @"No Application found to open your config file.", + @"alertText1" : @"Please open the mpv.conf file with " + "your preferred text editor in the now " + "open folder to edit your config.", + @"alertTitle2": @"No config file found.", + @"alertText2" : @"Please create a mpv.conf file with your " + "preferred text editor in the now open folder.", + @"alertTitle3": @"No config path or file found.", + @"alertText3" : @"Please create the following path ~/.config/mpv/ " + "and a mpv.conf file within with your preferred " + "text editor." + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Keyboard Shortcuts Config…", + @"action" : @"preferences:", + @"key" : @"", + @"target" : self, + @"file" : @"input.conf", + @"alertTitle1": @"No Application found to open your config file.", + @"alertText1" : @"Please open the input.conf file with " + "your preferred text editor in the now " + "open folder to edit your config.", + @"alertTitle2": @"No config file found.", + @"alertText2" : @"Please create a input.conf file with your " + "preferred text editor in the now open folder.", + @"alertTitle3": @"No config path or file found.", + @"alertText3" : @"Please create the following path ~/.config/mpv/ " + "and a input.conf file within with your preferred " + "text editor." + }], + @{ @"name": @"separator" }, + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Services", + @"key" : @"", + }], + @{ @"name": @"separator" }, + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Hide mpv", + @"action" : @"hide:", + @"key" : @"h", + @"target" : NSApp + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Hide Others", + @"action" : @"hideOtherApplications:", + @"key" : @"h", + @"modifiers" : [NSNumber numberWithUnsignedInteger: + NSEventModifierFlagCommand | + NSEventModifierFlagOption], + @"target" : NSApp + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Show All", + @"action" : @"unhideAllApplications:", + @"key" : @"", + @"target" : NSApp + }], + @{ @"name": @"separator" }, + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Quit and Remember Position", + @"action" : @"quit:", + @"key" : @"", + @"target" : self, + @"cmd" : @"quit-watch-later" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Quit mpv", + @"action" : @"quit:", + @"key" : @"q", + @"target" : self, + @"cmd" : @"quit" + }] + ] + }, + @{ + @"name": @"File", + @"menu": @[ + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Open File…", + @"action" : @"openFile", + @"key" : @"o", + @"target" : self + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Open URL…", + @"action" : @"openURL", + @"key" : @"O", + @"target" : self + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Open Playlist…", + @"action" : @"openPlaylist", + @"key" : @"", + @"target" : self + }], + @{ @"name": @"separator" }, + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Close", + @"action" : @"performClose:", + @"key" : @"w" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Save Screenshot", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"async screenshot" + }] + ] + }, + @{ + @"name": @"Edit", + @"menu": @[ + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Undo", + @"action" : @"undo:", + @"key" : @"z" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Redo", + @"action" : @"redo:", + @"key" : @"Z" + }], + @{ @"name": @"separator" }, + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Cut", + @"action" : @"cut:", + @"key" : @"x" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Copy", + @"action" : @"copy:", + @"key" : @"c" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Paste", + @"action" : @"paste:", + @"key" : @"v" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Select All", + @"action" : @"selectAll:", + @"key" : @"a" + }] + ] + }, + @{ + @"name": @"View", + @"menu": @[ + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Toggle Fullscreen", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"cycle fullscreen" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Toggle Float on Top", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"cycle ontop" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Toggle Visibility on All Workspaces", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"cycle on-all-workspaces" + }], +#if HAVE_MACOS_TOUCHBAR + @{ @"name": @"separator" }, + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Customize Touch Bar…", + @"action" : @"toggleTouchBarCustomizationPalette:", + @"key" : @"", + @"target" : NSApp + }] +#endif + ] + }, + @{ + @"name": @"Video", + @"menu": @[ + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Zoom Out", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"add panscan -0.1" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Zoom In", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"add panscan 0.1" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Reset Zoom", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"set panscan 0" + }], + @{ @"name": @"separator" }, + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Aspect Ratio 4:3", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"set video-aspect-override \"4:3\"" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Aspect Ratio 16:9", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"set video-aspect-override \"16:9\"" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Aspect Ratio 1.85:1", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"set video-aspect-override \"1.85:1\"" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Aspect Ratio 2.35:1", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"set video-aspect-override \"2.35:1\"" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Reset Aspect Ratio", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"set video-aspect-override \"-1\"" + }], + @{ @"name": @"separator" }, + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Rotate Left", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"cycle-values video-rotate 0 270 180 90" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Rotate Right", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"cycle-values video-rotate 90 180 270 0" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Reset Rotation", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"set video-rotate 0" + }], + @{ @"name": @"separator" }, + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Half Size", + @"key" : @"0", + @"cmdSpecial" : [NSNumber numberWithInt:MPM_H_SIZE] + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Normal Size", + @"key" : @"1", + @"cmdSpecial" : [NSNumber numberWithInt:MPM_N_SIZE] + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Double Size", + @"key" : @"2", + @"cmdSpecial" : [NSNumber numberWithInt:MPM_D_SIZE] + }] + ] + }, + @{ + @"name": @"Audio", + @"menu": @[ + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Next Audio Track", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"cycle audio" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Previous Audio Track", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"cycle audio down" + }], + @{ @"name": @"separator" }, + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Toggle Mute", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"cycle mute" + }], + @{ @"name": @"separator" }, + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Play Audio Later", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"add audio-delay 0.1" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Play Audio Earlier", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"add audio-delay -0.1" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Reset Audio Delay", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"set audio-delay 0.0 " + }] + ] + }, + @{ + @"name": @"Subtitle", + @"menu": @[ + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Next Subtitle Track", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"cycle sub" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Previous Subtitle Track", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"cycle sub down" + }], + @{ @"name": @"separator" }, + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Toggle Force Style", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"cycle-values sub-ass-override \"force\" \"no\"" + }], + @{ @"name": @"separator" }, + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Display Subtitles Later", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"add sub-delay 0.1" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Display Subtitles Earlier", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"add sub-delay -0.1" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Reset Subtitle Delay", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"set sub-delay 0.0" + }] + ] + }, + @{ + @"name": @"Playback", + @"menu": @[ + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Toggle Pause", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"cycle pause" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Increase Speed", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"add speed 0.1" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Decrease Speed", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"add speed -0.1" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Reset Speed", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"set speed 1.0" + }], + @{ @"name": @"separator" }, + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Show Playlist", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"script-message osc-playlist" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Show Chapters", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"script-message osc-chapterlist" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Show Tracks", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"script-message osc-tracklist" + }], + @{ @"name": @"separator" }, + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Next File", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"playlist-next" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Previous File", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"playlist-prev" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Toggle Loop File", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"cycle-values loop-file \"inf\" \"no\"" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Toggle Loop Playlist", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"cycle-values loop-playlist \"inf\" \"no\"" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Shuffle", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"playlist-shuffle" + }], + @{ @"name": @"separator" }, + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Next Chapter", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"add chapter 1" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Previous Chapter", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"add chapter -1" + }], + @{ @"name": @"separator" }, + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Step Forward", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"frame-step" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Step Backward", + @"action" : @"cmd:", + @"key" : @"", + @"target" : self, + @"cmd" : @"frame-back-step" + }] + ] + }, + @{ + @"name": @"Window", + @"menu": @[ + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Minimize", + @"key" : @"m", + @"cmdSpecial" : [NSNumber numberWithInt:MPM_MINIMIZE] + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Zoom", + @"key" : @"z", + @"cmdSpecial" : [NSNumber numberWithInt:MPM_ZOOM] + }] + ] + }, + @{ + @"name": @"Help", + @"menu": @[ + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"mpv Website…", + @"action" : @"url:", + @"key" : @"", + @"target" : self, + @"url" : @"https://mpv.io" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"mpv on github…", + @"action" : @"url:", + @"key" : @"", + @"target" : self, + @"url" : @"https://github.com/mpv-player/mpv" + }], + @{ @"name": @"separator" }, + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Online Manual…", + @"action" : @"url:", + @"key" : @"", + @"target" : self, + @"url" : @"https://mpv.io/manual/master/" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Online Wiki…", + @"action" : @"url:", + @"key" : @"", + @"target" : self, + @"url" : @"https://github.com/mpv-player/mpv/wiki" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Release Notes…", + @"action" : @"url:", + @"key" : @"", + @"target" : self, + @"url" : @"https://github.com/mpv-player/mpv/blob/master/RELEASE_NOTES" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Keyboard Shortcuts…", + @"action" : @"url:", + @"key" : @"", + @"target" : self, + @"url" : @"https://github.com/mpv-player/mpv/blob/master/etc/input.conf" + }], + @{ @"name": @"separator" }, + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Report Issue…", + @"action" : @"url:", + @"key" : @"", + @"target" : self, + @"url" : @"https://github.com/mpv-player/mpv/issues/new/choose" + }], + [NSMutableDictionary dictionaryWithDictionary:@{ + @"name" : @"Show log File…", + @"action" : @"showFile:", + @"key" : @"", + @"target" : self, + @"file" : @"~/Library/Logs/mpv.log", + @"alertTitle" : @"No log File found.", + @"alertText" : @"You deactivated logging for the Bundle." + }] + ] + } + ]; + + [NSApp setMainMenu:[self mainMenu]]; + } + + return self; +} + +- (NSMenu *)mainMenu +{ + NSMenu *mainMenu = [[NSMenu alloc] initWithTitle:@"MainMenu"]; + [NSApp setServicesMenu:[[NSMenu alloc] init]]; + NSString* bundle = [[[NSProcessInfo processInfo] environment] objectForKey:@"MPVBUNDLE"]; + + for(id mMenu in menuTree) { + NSMenu *menu = [[NSMenu alloc] initWithTitle:mMenu[@"name"]]; + NSMenuItem *mItem = [mainMenu addItemWithTitle:mMenu[@"name"] + action:nil + keyEquivalent:@""]; + [mainMenu setSubmenu:menu forItem:mItem]; + + for(id subMenu in mMenu[@"menu"]) { + NSString *name = subMenu[@"name"]; + NSString *action = subMenu[@"action"]; + +#if HAVE_MACOS_TOUCHBAR + if ([action isEqual:@"toggleTouchBarCustomizationPalette:"]) { + continue; + } +#endif + + if ([name isEqual:@"Show log File…"] && ![bundle isEqual:@"true"]) { + continue; + } + + if ([name isEqual:@"separator"]) { + [menu addItem:[NSMenuItem separatorItem]]; + } else { + NSMenuItem *iItem = [menu addItemWithTitle:name + action:NSSelectorFromString(action) + keyEquivalent:subMenu[@"key"]]; + [iItem setTarget:subMenu[@"target"]]; + [subMenu setObject:iItem forKey:@"menuItem"]; + + NSNumber *m = subMenu[@"modifiers"]; + if (m) { + [iItem setKeyEquivalentModifierMask:m.unsignedIntegerValue]; + } + + if ([subMenu[@"name"] isEqual:@"Services"]) { + iItem.submenu = [NSApp servicesMenu]; + } + } + } + } + + return mainMenu; +} + +- (void)about +{ + NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys: + @"mpv", @"ApplicationName", + [(Application *)NSApp getMPVIcon], @"ApplicationIcon", + [NSString stringWithUTF8String:mpv_copyright], @"Copyright", + [NSString stringWithUTF8String:mpv_version], @"ApplicationVersion", + nil]; + [NSApp orderFrontStandardAboutPanelWithOptions:options]; +} + +- (void)preferences:(NSMenuItem *)menuItem +{ + NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSMutableDictionary *mItemDict = [self getDictFromMenuItem:menuItem]; + NSArray *configPaths = @[ + [NSString stringWithFormat:@"%@/.mpv/", NSHomeDirectory()], + [NSString stringWithFormat:@"%@/.config/mpv/", NSHomeDirectory()]]; + + for (id path in configPaths) { + NSString *fileP = [path stringByAppendingString:mItemDict[@"file"]]; + if ([fileManager fileExistsAtPath:fileP]){ + if ([workspace openFile:fileP]) + return; + [workspace openFile:path]; + [self alertWithTitle:mItemDict[@"alertTitle1"] + andText:mItemDict[@"alertText1"]]; + return; + } + if ([workspace openFile:path]) { + [self alertWithTitle:mItemDict[@"alertTitle2"] + andText:mItemDict[@"alertText2"]]; + return; + } + } + + [self alertWithTitle:mItemDict[@"alertTitle3"] + andText:mItemDict[@"alertText3"]]; +} + +- (void)quit:(NSMenuItem *)menuItem +{ + NSString *cmd = [self getDictFromMenuItem:menuItem][@"cmd"]; + [(Application *)NSApp stopMPV:(char *)[cmd UTF8String]]; +} + +- (void)openFile +{ + NSOpenPanel *panel = [[NSOpenPanel alloc] init]; + [panel setCanChooseDirectories:YES]; + [panel setAllowsMultipleSelection:YES]; + + if ([panel runModal] == NSModalResponseOK){ + NSMutableArray *fileArray = [[NSMutableArray alloc] init]; + for (id url in [panel URLs]) + [fileArray addObject:[url path]]; + [(Application *)NSApp openFiles:fileArray]; + } +} + +- (void)openPlaylist +{ + NSOpenPanel *panel = [[NSOpenPanel alloc] init]; + + if ([panel runModal] == NSModalResponseOK){ + NSString *pl = [NSString stringWithFormat:@"loadlist \"%@\"", + [panel URLs][0].path]; + [(Application *)NSApp queueCommand:(char *)[pl UTF8String]]; + } +} + +- (void)openURL +{ + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:@"Open URL"]; + [alert addButtonWithTitle:@"Ok"]; + [alert addButtonWithTitle:@"Cancel"]; + [alert setIcon:[(Application *)NSApp getMPVIcon]]; + + NSTextField *input = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 300, 24)]; + [input setPlaceholderString:@"URL"]; + [alert setAccessoryView:input]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + [input becomeFirstResponder]; + }); + + if ([alert runModal] == NSAlertFirstButtonReturn && [input stringValue].length > 0) { + NSArray *url = [NSArray arrayWithObjects:[input stringValue], nil]; + [(Application *)NSApp openFiles:url]; + } +} + +- (void)cmd:(NSMenuItem *)menuItem +{ + NSString *cmd = [self getDictFromMenuItem:menuItem][@"cmd"]; + [(Application *)NSApp queueCommand:(char *)[cmd UTF8String]]; +} + +- (void)url:(NSMenuItem *)menuItem +{ + NSString *url = [self getDictFromMenuItem:menuItem][@"url"]; + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]]; +} + +- (void)showFile:(NSMenuItem *)menuItem +{ + NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSMutableDictionary *mItemDict = [self getDictFromMenuItem:menuItem]; + NSString *file = [mItemDict[@"file"] stringByExpandingTildeInPath]; + + if ([fileManager fileExistsAtPath:file]){ + NSURL *url = [NSURL fileURLWithPath:file]; + NSArray *urlArray = [NSArray arrayWithObjects:url, nil]; + + [workspace activateFileViewerSelectingURLs:urlArray]; + return; + } + + [self alertWithTitle:mItemDict[@"alertTitle"] + andText:mItemDict[@"alertText"]]; +} + +- (void)alertWithTitle:(NSString *)title andText:(NSString *)text +{ + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:title]; + [alert setInformativeText:text]; + [alert addButtonWithTitle:@"Ok"]; + [alert setIcon:[(Application *)NSApp getMPVIcon]]; + [alert runModal]; +} + +- (NSMutableDictionary *)getDictFromMenuItem:(NSMenuItem *)menuItem +{ + for(id mMenu in menuTree) { + for(id subMenu in mMenu[@"menu"]) { + if([subMenu[@"menuItem"] isEqual:menuItem]) + return subMenu; + } + } + + return nil; +} + +- (void)registerSelector:(SEL)action forKey:(MPMenuKey)key +{ + for(id mMenu in menuTree) { + for(id subMenu in mMenu[@"menu"]) { + if([subMenu[@"cmdSpecial"] isEqual:[NSNumber numberWithInt:key]]) { + [subMenu[@"menuItem"] setAction:action]; + return; + } + } + } +} + +@end diff --git a/osdep/macosx_menubar_objc.h b/osdep/macosx_menubar_objc.h new file mode 100644 index 0000000..072fef8 --- /dev/null +++ b/osdep/macosx_menubar_objc.h @@ -0,0 +1,25 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#import <Cocoa/Cocoa.h> +#include "osdep/macosx_menubar.h" + +@interface MenuBar : NSObject + +- (void)registerSelector:(SEL)action forKey:(MPMenuKey)key; + +@end diff --git a/osdep/macosx_touchbar.h b/osdep/macosx_touchbar.h new file mode 100644 index 0000000..a03b68c --- /dev/null +++ b/osdep/macosx_touchbar.h @@ -0,0 +1,46 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#import <Cocoa/Cocoa.h> +#import "osdep/macosx_application_objc.h" + +#define BASE_ID @"io.mpv.touchbar" +static NSTouchBarCustomizationIdentifier customID = BASE_ID; +static NSTouchBarItemIdentifier seekBar = BASE_ID ".seekbar"; +static NSTouchBarItemIdentifier play = BASE_ID ".play"; +static NSTouchBarItemIdentifier nextItem = BASE_ID ".nextItem"; +static NSTouchBarItemIdentifier previousItem = BASE_ID ".previousItem"; +static NSTouchBarItemIdentifier nextChapter = BASE_ID ".nextChapter"; +static NSTouchBarItemIdentifier previousChapter = BASE_ID ".previousChapter"; +static NSTouchBarItemIdentifier cycleAudio = BASE_ID ".cycleAudio"; +static NSTouchBarItemIdentifier cycleSubtitle = BASE_ID ".cycleSubtitle"; +static NSTouchBarItemIdentifier currentPosition = BASE_ID ".currentPosition"; +static NSTouchBarItemIdentifier timeLeft = BASE_ID ".timeLeft"; + +struct mpv_event; + +@interface TouchBar : NSTouchBar <NSTouchBarDelegate> + +-(void)processEvent:(struct mpv_event *)event; + +@property(nonatomic, retain) Application *app; +@property(nonatomic, retain) NSDictionary *touchbarItems; +@property(nonatomic, assign) double duration; +@property(nonatomic, assign) double position; +@property(nonatomic, assign) int pause; + +@end diff --git a/osdep/macosx_touchbar.m b/osdep/macosx_touchbar.m new file mode 100644 index 0000000..ccce8f7 --- /dev/null +++ b/osdep/macosx_touchbar.m @@ -0,0 +1,334 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * mpv is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "player/client.h" +#import "macosx_touchbar.h" + +@implementation TouchBar + +@synthesize app = _app; +@synthesize touchbarItems = _touchbar_items; +@synthesize duration = _duration; +@synthesize position = _position; +@synthesize pause = _pause; + +- (id)init +{ + if (self = [super init]) { + self.touchbarItems = @{ + seekBar: [NSMutableDictionary dictionaryWithDictionary:@{ + @"type": @"slider", + @"name": @"Seek Bar", + @"cmd": @"seek %f absolute-percent" + }], + play: [NSMutableDictionary dictionaryWithDictionary:@{ + @"type": @"button", + @"name": @"Play Button", + @"cmd": @"cycle pause", + @"image": [NSImage imageNamed:NSImageNameTouchBarPauseTemplate], + @"imageAlt": [NSImage imageNamed:NSImageNameTouchBarPlayTemplate] + }], + previousItem: [NSMutableDictionary dictionaryWithDictionary:@{ + @"type": @"button", + @"name": @"Previous Playlist Item", + @"cmd": @"playlist-prev", + @"image": [NSImage imageNamed:NSImageNameTouchBarGoBackTemplate] + }], + nextItem: [NSMutableDictionary dictionaryWithDictionary:@{ + @"type": @"button", + @"name": @"Next Playlist Item", + @"cmd": @"playlist-next", + @"image": [NSImage imageNamed:NSImageNameTouchBarGoForwardTemplate] + }], + previousChapter: [NSMutableDictionary dictionaryWithDictionary:@{ + @"type": @"button", + @"name": @"Previous Chapter", + @"cmd": @"add chapter -1", + @"image": [NSImage imageNamed:NSImageNameTouchBarSkipBackTemplate] + }], + nextChapter: [NSMutableDictionary dictionaryWithDictionary:@{ + @"type": @"button", + @"name": @"Next Chapter", + @"cmd": @"add chapter 1", + @"image": [NSImage imageNamed:NSImageNameTouchBarSkipAheadTemplate] + }], + cycleAudio: [NSMutableDictionary dictionaryWithDictionary:@{ + @"type": @"button", + @"name": @"Cycle Audio", + @"cmd": @"cycle audio", + @"image": [NSImage imageNamed:NSImageNameTouchBarAudioInputTemplate] + }], + cycleSubtitle: [NSMutableDictionary dictionaryWithDictionary:@{ + @"type": @"button", + @"name": @"Cycle Subtitle", + @"cmd": @"cycle sub", + @"image": [NSImage imageNamed:NSImageNameTouchBarComposeTemplate] + }], + currentPosition: [NSMutableDictionary dictionaryWithDictionary:@{ + @"type": @"text", + @"name": @"Current Position" + }], + timeLeft: [NSMutableDictionary dictionaryWithDictionary:@{ + @"type": @"text", + @"name": @"Time Left" + }] + }; + + [self addObserver:self forKeyPath:@"visible" options:0 context:nil]; + } + return self; +} + +- (nullable NSTouchBarItem *)touchBar:(NSTouchBar *)touchBar + makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier +{ + if ([self.touchbarItems[identifier][@"type"] isEqualToString:@"slider"]) { + NSCustomTouchBarItem *tbItem = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier]; + NSSlider *slider = [NSSlider sliderWithTarget:self action:@selector(seekbarChanged:)]; + slider.minValue = 0.0f; + slider.maxValue = 100.0f; + tbItem.view = slider; + tbItem.customizationLabel = self.touchbarItems[identifier][@"name"]; + [self.touchbarItems[identifier] setObject:slider forKey:@"view"]; + [self.touchbarItems[identifier] setObject:tbItem forKey:@"tbItem"]; + [tbItem addObserver:self forKeyPath:@"visible" options:0 context:nil]; + return tbItem; + } else if ([self.touchbarItems[identifier][@"type"] isEqualToString:@"button"]) { + NSCustomTouchBarItem *tbItem = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier]; + NSImage *tbImage = self.touchbarItems[identifier][@"image"]; + NSButton *tbButton = [NSButton buttonWithImage:tbImage target:self action:@selector(buttonAction:)]; + tbItem.view = tbButton; + tbItem.customizationLabel = self.touchbarItems[identifier][@"name"]; + [self.touchbarItems[identifier] setObject:tbButton forKey:@"view"]; + [self.touchbarItems[identifier] setObject:tbItem forKey:@"tbItem"]; + [tbItem addObserver:self forKeyPath:@"visible" options:0 context:nil]; + return tbItem; + } else if ([self.touchbarItems[identifier][@"type"] isEqualToString:@"text"]) { + NSCustomTouchBarItem *tbItem = [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier]; + NSTextField *tbText = [NSTextField labelWithString:@"0:00"]; + tbText.alignment = NSTextAlignmentCenter; + tbItem.view = tbText; + tbItem.customizationLabel = self.touchbarItems[identifier][@"name"]; + [self.touchbarItems[identifier] setObject:tbText forKey:@"view"]; + [self.touchbarItems[identifier] setObject:tbItem forKey:@"tbItem"]; + [tbItem addObserver:self forKeyPath:@"visible" options:0 context:nil]; + return tbItem; + } + + return nil; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary<NSKeyValueChangeKey,id> *)change + context:(void *)context { + if ([keyPath isEqualToString:@"visible"]) { + NSNumber *visible = [object valueForKey:@"visible"]; + if (visible.boolValue) { + [self updateTouchBarTimeItems]; + [self updatePlayButton]; + } + } +} + +- (void)updateTouchBarTimeItems +{ + if (!self.isVisible) + return; + + [self updateSlider]; + [self updateTimeLeft]; + [self updateCurrentPosition]; +} + +- (void)updateSlider +{ + NSCustomTouchBarItem *tbItem = self.touchbarItems[seekBar][@"tbItem"]; + if (!tbItem.visible) + return; + + NSSlider *seekSlider = self.touchbarItems[seekBar][@"view"]; + + if (self.duration <= 0) { + seekSlider.enabled = NO; + seekSlider.doubleValue = 0; + } else { + seekSlider.enabled = YES; + if (!seekSlider.highlighted) + seekSlider.doubleValue = (self.position/self.duration)*100; + } +} + +- (void)updateTimeLeft +{ + NSCustomTouchBarItem *tbItem = self.touchbarItems[timeLeft][@"tbItem"]; + if (!tbItem.visible) + return; + + NSTextField *timeLeftItem = self.touchbarItems[timeLeft][@"view"]; + + [self removeConstraintForIdentifier:timeLeft]; + if (self.duration <= 0) { + timeLeftItem.stringValue = @""; + } else { + int left = (int)(floor(self.duration)-floor(self.position)); + NSString *leftFormat = [self formatTime:left]; + NSString *durFormat = [self formatTime:self.duration]; + timeLeftItem.stringValue = [NSString stringWithFormat:@"-%@", leftFormat]; + [self applyConstraintFromString:[NSString stringWithFormat:@"-%@", durFormat] + forIdentifier:timeLeft]; + } +} + +- (void)updateCurrentPosition +{ + NSCustomTouchBarItem *tbItem = self.touchbarItems[currentPosition][@"tbItem"]; + if (!tbItem.visible) + return; + + NSTextField *curPosItem = self.touchbarItems[currentPosition][@"view"]; + NSString *posFormat = [self formatTime:(int)floor(self.position)]; + curPosItem.stringValue = posFormat; + + [self removeConstraintForIdentifier:currentPosition]; + if (self.duration <= 0) { + [self applyConstraintFromString:[self formatTime:self.position] + forIdentifier:currentPosition]; + } else { + NSString *durFormat = [self formatTime:self.duration]; + [self applyConstraintFromString:durFormat forIdentifier:currentPosition]; + } +} + +- (void)updatePlayButton +{ + NSCustomTouchBarItem *tbItem = self.touchbarItems[play][@"tbItem"]; + if (!self.isVisible || !tbItem.visible) + return; + + NSButton *playButton = self.touchbarItems[play][@"view"]; + if (self.pause) { + playButton.image = self.touchbarItems[play][@"imageAlt"]; + } else { + playButton.image = self.touchbarItems[play][@"image"]; + } +} + +- (void)buttonAction:(NSButton *)sender +{ + NSString *identifier = [self getIdentifierFromView:sender]; + [self.app queueCommand:(char *)[self.touchbarItems[identifier][@"cmd"] UTF8String]]; +} + +- (void)seekbarChanged:(NSSlider *)slider +{ + NSString *identifier = [self getIdentifierFromView:slider]; + NSString *seek = [NSString stringWithFormat: + self.touchbarItems[identifier][@"cmd"], slider.doubleValue]; + [self.app queueCommand:(char *)[seek UTF8String]]; +} + +- (NSString *)formatTime:(int)time +{ + int seconds = time % 60; + int minutes = (time / 60) % 60; + int hours = time / (60 * 60); + + NSString *stime = hours > 0 ? [NSString stringWithFormat:@"%d:", hours] : @""; + stime = (stime.length > 0 || minutes > 9) ? + [NSString stringWithFormat:@"%@%02d:", stime, minutes] : + [NSString stringWithFormat:@"%d:", minutes]; + stime = [NSString stringWithFormat:@"%@%02d", stime, seconds]; + + return stime; +} + +- (void)removeConstraintForIdentifier:(NSTouchBarItemIdentifier)identifier +{ + NSTextField *field = self.touchbarItems[identifier][@"view"]; + [field removeConstraint:self.touchbarItems[identifier][@"constrain"]]; +} + +- (void)applyConstraintFromString:(NSString *)string + forIdentifier:(NSTouchBarItemIdentifier)identifier +{ + NSTextField *field = self.touchbarItems[identifier][@"view"]; + if (field) { + NSString *fString = [[string componentsSeparatedByCharactersInSet: + [NSCharacterSet decimalDigitCharacterSet]] componentsJoinedByString:@"0"]; + NSTextField *textField = [NSTextField labelWithString:fString]; + NSSize size = [textField frame].size; + + NSLayoutConstraint *con = + [NSLayoutConstraint constraintWithItem:field + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1.0 + constant:(int)ceil(size.width*1.1)]; + [field addConstraint:con]; + [self.touchbarItems[identifier] setObject:con forKey:@"constrain"]; + } +} + +- (NSString *)getIdentifierFromView:(id)view +{ + NSString *identifier; + for (identifier in self.touchbarItems) + if([self.touchbarItems[identifier][@"view"] isEqual:view]) + break; + return identifier; +} + +- (void)processEvent:(struct mpv_event *)event +{ + switch (event->event_id) { + case MPV_EVENT_END_FILE: { + self.position = 0; + self.duration = 0; + break; + } + case MPV_EVENT_PROPERTY_CHANGE: { + [self handlePropertyChange:(mpv_event_property *)event->data]; + break; + } + } +} + +- (void)handlePropertyChange:(struct mpv_event_property *)property +{ + NSString *name = [NSString stringWithUTF8String:property->name]; + mpv_format format = property->format; + + if ([name isEqualToString:@"time-pos"] && format == MPV_FORMAT_DOUBLE) { + double newPosition = *(double *)property->data; + newPosition = newPosition < 0 ? 0 : newPosition; + if ((int)(floor(newPosition) - floor(self.position)) != 0) { + self.position = newPosition; + [self updateTouchBarTimeItems]; + } + } else if ([name isEqualToString:@"duration"] && format == MPV_FORMAT_DOUBLE) { + self.duration = *(double *)property->data; + [self updateTouchBarTimeItems]; + } else if ([name isEqualToString:@"pause"] && format == MPV_FORMAT_FLAG) { + self.pause = *(int *)property->data; + [self updatePlayButton]; + } +} + +@end |