From c8088d801a99109b213a416da20a9e5d712ca492 Mon Sep 17 00:00:00 2001
From: der richter <der.richter@gmx.de>
Date: Sun, 17 Mar 2024 17:12:09 +0100
Subject: [PATCH] mac/touchbar: rewrite touch bar in swift

---
 meson.build               |   2 +-
 osdep/mac/application.m   |  12 +-
 osdep/mac/touch_bar.swift | 343 ++++++++++++++++++++++++++++++++++++++
 osdep/mac/touchbar.h      |  44 -----
 osdep/mac/touchbar.m      | 342 -------------------------------------
 5 files changed, 345 insertions(+), 398 deletions(-)
 create mode 100644 osdep/mac/touch_bar.swift
 delete mode 100644 osdep/mac/touchbar.h
 delete mode 100644 osdep/mac/touchbar.m

diff --git a/meson.build b/meson.build
index 159d1feebc..40c5551fe0 100644
--- a/meson.build
+++ b/meson.build
@@ -1556,7 +1556,7 @@ macos_touchbar = get_option('macos-touchbar').require(
 )
 features += {'macos-touchbar': macos_touchbar.allowed()}
 if features['macos-touchbar']
-    sources += files('osdep/mac/touchbar.m')
+    swift_sources += files('osdep/mac/touch_bar.swift')
 endif
 
 if features['swift'] and swift_sources.length() > 0
diff --git a/osdep/mac/application.m b/osdep/mac/application.m
index bd71b6042c..5eef3a9fa3 100644
--- a/osdep/mac/application.m
+++ b/osdep/mac/application.m
@@ -30,9 +30,6 @@
 #include "osdep/threads.h"
 #include "osdep/main-fn.h"
 
-#if HAVE_MACOS_TOUCHBAR
-#import "osdep/mac/touchbar.h"
-#endif
 #if HAVE_SWIFT
 #include "osdep/mac/swift.h"
 #endif
@@ -161,14 +158,7 @@ static const char mac_icon[] =
 #if HAVE_MACOS_TOUCHBAR
 - (NSTouchBar *)makeTouchBar
 {
-    TouchBar *tBar = [[TouchBar alloc] init];
-    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;
+    return [[TouchBar alloc] init];
 }
 #endif
 
diff --git a/osdep/mac/touch_bar.swift b/osdep/mac/touch_bar.swift
new file mode 100644
index 0000000000..dcebb24970
--- /dev/null
+++ b/osdep/mac/touch_bar.swift
@@ -0,0 +1,343 @@
+/*
+ * 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/>.
+ */
+
+extension NSTouchBar.CustomizationIdentifier {
+    public static let customId: NSTouchBar.CustomizationIdentifier = "io.mpv.touchbar"
+}
+
+extension NSTouchBarItem.Identifier {
+    public static let seekBar = NSTouchBarItem.Identifier(custom: ".seekbar")
+    public static let play = NSTouchBarItem.Identifier(custom: ".play")
+    public static let nextItem = NSTouchBarItem.Identifier(custom: ".nextItem")
+    public static let previousItem = NSTouchBarItem.Identifier(custom: ".previousItem")
+    public static let nextChapter = NSTouchBarItem.Identifier(custom: ".nextChapter")
+    public static let previousChapter = NSTouchBarItem.Identifier(custom: ".previousChapter")
+    public static let cycleAudio = NSTouchBarItem.Identifier(custom: ".cycleAudio")
+    public static let cycleSubtitle = NSTouchBarItem.Identifier(custom: ".cycleSubtitle")
+    public static let currentPosition = NSTouchBarItem.Identifier(custom: ".currentPosition")
+    public static let timeLeft = NSTouchBarItem.Identifier(custom: ".timeLeft")
+
+    init(custom: String) {
+        self.init(NSTouchBar.CustomizationIdentifier.customId + custom)
+    }
+}
+
+extension TouchBar {
+    enum `Type` {
+        case button
+        case text
+        case slider
+    }
+
+    struct Config {
+        let name: String
+        let type: Type
+        let command: String
+        var view: NSView?
+        var item: NSCustomTouchBarItem?
+        var constraint: NSLayoutConstraint?
+        let image: NSImage
+        let imageAlt: NSImage
+
+        init(
+            name: String = "",
+            type: Type = .button,
+            command: String = "",
+            view: NSView? = nil,
+            item: NSCustomTouchBarItem? = nil,
+            constraint: NSLayoutConstraint? = nil,
+            image: NSImage? = nil,
+            imageAlt: NSImage? = nil
+        ) {
+            self.name = name
+            self.type = type
+            self.command = command
+            self.view = view
+            self.item = item
+            self.constraint = constraint
+            self.image = image ?? NSImage(size: NSSize(width: 1, height: 1))
+            self.imageAlt = imageAlt ?? NSImage(size: NSSize(width: 1, height: 1))
+        }
+    }
+}
+
+class TouchBar: NSTouchBar, NSTouchBarDelegate {
+    var configs: [NSTouchBarItem.Identifier:Config] = [:]
+    var isPaused: Bool = false
+    var position: Double = 0
+    var duration: Double = 0
+
+    override init() {
+        super.init()
+
+        configs = [
+            .seekBar: Config(name: "Seek Bar", type: .slider, command: "seek %f absolute-percent"),
+            .currentPosition: Config(name: "Current Position", type: .text),
+            .timeLeft: Config(name: "Time Left", type: .text),
+            .play: Config(
+                name: "Play Button",
+                type: .button,
+                command: "cycle pause",
+                image: .init(named: NSImage.touchBarPauseTemplateName),
+                imageAlt: .init(named: NSImage.touchBarPlayTemplateName)
+            ),
+            .previousItem: Config(
+                name: "Previous Playlist Item",
+                type: .button,
+                command: "playlist-prev",
+                image: .init(named: NSImage.touchBarGoBackTemplateName)
+            ),
+            .nextItem: Config(
+                name: "Next Playlist Item",
+                type: .button,
+                command: "playlist-next",
+                image: .init(named: NSImage.touchBarGoForwardTemplateName)
+            ),
+            .previousChapter: Config(
+                name: "Previous Chapter",
+                type: .button,
+                command: "add chapter -1",
+                image: .init(named: NSImage.touchBarSkipBackTemplateName)
+            ),
+            .nextChapter: Config(
+                name: "Next Chapter",
+                type: .button,
+                command: "add chapter 1",
+                image: .init(named: NSImage.touchBarSkipAheadTemplateName)
+            ),
+            .cycleAudio: Config(
+                name: "Cycle Audio",
+                type: .button,
+                command: "cycle audio",
+                image: .init(named: NSImage.touchBarAudioInputTemplateName)
+                ),
+            .cycleSubtitle: Config(
+                name: "Cycle Subtitle",
+                type: .button,
+                command: "cycle sub",
+                image: .init(named: NSImage.touchBarComposeTemplateName)
+            )
+        ]
+
+        delegate = self
+        customizationIdentifier = .customId;
+        defaultItemIdentifiers = [.play, .previousItem, .nextItem, .seekBar]
+        customizationAllowedItemIdentifiers = [.play, .seekBar, .previousItem, .nextItem,
+            .previousChapter, .nextChapter, .cycleAudio, .cycleSubtitle, .currentPosition, .timeLeft]
+        addObserver(self, forKeyPath: "visible", options: [.new], context: nil)
+    }
+
+    required init?(coder: NSCoder) {
+        super.init(coder: coder)
+    }
+
+    func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItem.Identifier) -> NSTouchBarItem? {
+        guard let config = configs[identifier] else { return nil }
+
+        switch config.type {
+        case .button:
+            let item = NSCustomTouchBarItem(identifier: identifier)
+            let image = config.image
+            let button = NSButton(image: image, target: self, action: #selector(buttonAction(_:)))
+            item.view = button;
+            item.customizationLabel = config.name
+            configs[identifier]?.view = button
+            configs[identifier]?.item = item
+            item.addObserver(self, forKeyPath: "visible", options: [.new], context: nil)
+            return item
+        case .text:
+            let item = NSCustomTouchBarItem(identifier: identifier)
+            let text = NSTextField(labelWithString: "0:00")
+            text.alignment = .center
+            item.view = text;
+            item.customizationLabel = config.name
+            configs[identifier]?.view = text
+            configs[identifier]?.item = item
+            item.addObserver(self, forKeyPath: "visible", options: [.new], context: nil)
+            return item
+        case .slider:
+            let item = NSCustomTouchBarItem(identifier: identifier)
+            let slider = NSSlider(target: self, action: #selector(seekbarChanged(_:)))
+            slider.minValue = 0
+            slider.maxValue = 100
+            item.view = slider;
+            item.customizationLabel = config.name
+            configs[identifier]?.view = slider
+            configs[identifier]?.item = item
+            item.addObserver(self, forKeyPath: "visible", options: [.new], context: nil)
+            return item
+        }
+    }
+
+    override func observeValue(
+        forKeyPath keyPath: String?,
+        of object: Any?,
+        change: [NSKeyValueChangeKey:Any]?,
+        context: UnsafeMutableRawPointer?
+    ) {
+        guard let visible = change?[.newKey] as? Bool else { return }
+        if keyPath == "isVisible" && visible {
+            updateTouchBarTimeItems()
+            updatePlayButton()
+        }
+    }
+
+    func updateTouchBarTimeItems() {
+        if !isVisible { return }
+        updateSlider()
+        updateTimeLeft()
+        updateCurrentPosition()
+    }
+
+    func updateSlider() {
+        guard let config = configs[.seekBar], let slider = config.view as? NSSlider else { return }
+        if !(config.item?.isVisible ?? false) { return }
+
+        if duration <= 0 {
+            slider.isEnabled = false
+            slider.doubleValue = 0
+        } else {
+            slider.isEnabled = true
+            if (!slider.isHighlighted) {
+                slider.doubleValue = (position / duration) * 100
+            }
+        }
+    }
+
+    func updateTimeLeft() {
+        guard let config = configs[.timeLeft], let text = config.view as? NSTextField else { return }
+        if !(config.item?.isVisible ?? false) { return }
+
+        removeConstraintFor(identifier: .timeLeft)
+        if duration <= 0 {
+            text.stringValue = ""
+        } else {
+            let left = Int(floor(duration) - floor(position))
+            let leftFormat = format(time: left)
+            let durationFormat = format(time: Int(duration))
+            text.stringValue = "-\(leftFormat)"
+            applyConstraintFrom(string: "-\(durationFormat)", identifier: .timeLeft)
+        }
+    }
+
+    func updateCurrentPosition() {
+        guard let config = configs[.currentPosition], let text = config.view as? NSTextField else { return }
+        if !(config.item?.isVisible ?? false) { return }
+
+        text.stringValue = format(time: Int(floor(position)))
+        removeConstraintFor(identifier: .currentPosition)
+        if duration <= 0 {
+            applyConstraintFrom(string: format(time: Int(position)), identifier: .currentPosition)
+        } else {
+            applyConstraintFrom(string: format(time: Int(duration)), identifier: .currentPosition)
+        }
+    }
+
+    func updatePlayButton() {
+        guard let config = configs[.play], let button = config.view as? NSButton else { return }
+        if !isVisible || !(config.item?.isVisible ?? false) { return }
+
+        if isPaused {
+            button.image = configs[.play]?.imageAlt
+        } else {
+            button.image = configs[.play]?.image
+        }
+    }
+
+    @objc func buttonAction(_ button: NSButton) {
+        guard let identifier = getIdentifierFrom(view: button), let command = configs[identifier]?.command else { return }
+        EventsResponder.sharedInstance().inputHelper.command(command)
+    }
+
+    @objc func seekbarChanged(_ slider: NSSlider) {
+        guard let identifier = getIdentifierFrom(view: slider), let command = configs[identifier]?.command else { return }
+        EventsResponder.sharedInstance().inputHelper.command(String(format: command, slider.doubleValue))
+    }
+
+    func format(time: Int) -> String {
+        let seconds = time % 60
+        let minutes = (time / 60) % 60
+        let hours = time / (60 * 60)
+
+        var stime = hours > 0 ? "\(hours):" : ""
+        stime = (stime.count > 0 || minutes > 9) ? String(format: "%@%02d:", stime, minutes) : "\(minutes):"
+        stime = String(format: "%@%02d", stime, seconds)
+
+        return stime
+    }
+
+    func removeConstraintFor(identifier: NSTouchBarItem.Identifier) {
+        guard let text = configs[identifier]?.view as? NSTextField,
+              let constraint = configs[identifier]?.constraint as? NSLayoutConstraint else { return }
+        text.removeConstraint(constraint)
+    }
+
+    func applyConstraintFrom(string: String, identifier: NSTouchBarItem.Identifier) {
+        guard let text = configs[identifier]?.view as? NSTextField else { return }
+        let fString = string.components(separatedBy: .decimalDigits).joined(separator: "0")
+        let textField = NSTextField(labelWithString: fString)
+        let size = textField.frame.size
+
+        let con = NSLayoutConstraint(item: text, attribute: .width, relatedBy: .equal, toItem: nil,
+            attribute: .notAnAttribute, multiplier: 1.0, constant: ceil(size.width * 1.1))
+        text.addConstraint(con)
+        configs[identifier]?.constraint = con
+    }
+
+    func getIdentifierFrom(view: NSView) -> NSTouchBarItem.Identifier? {
+        for (identifier, config) in configs {
+            if config.view == view {
+                return identifier
+            }
+        }
+        return nil
+    }
+
+    @objc func processEvent(_ event: UnsafeMutablePointer<mpv_event>) {
+        switch event.pointee.event_id {
+        case MPV_EVENT_END_FILE:
+            position = 0
+            duration = 0
+        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 "time-pos" where property.format == MPV_FORMAT_DOUBLE:
+            let newPosition = max(LibmpvHelper.mpvDoubleToDouble(property.data) ?? 0, 0)
+            if Int(floor(newPosition) - floor(position)) != 0 {
+                position = newPosition
+                updateTouchBarTimeItems()
+            }
+        case "duration" where property.format == MPV_FORMAT_DOUBLE:
+            duration = LibmpvHelper.mpvDoubleToDouble(property.data) ?? 0
+            updateTouchBarTimeItems()
+        case "pause" where property.format == MPV_FORMAT_FLAG:
+            isPaused = LibmpvHelper.mpvFlagToBool(property.data) ?? false
+            updatePlayButton()
+        default:
+            break
+        }
+    }
+}
diff --git a/osdep/mac/touchbar.h b/osdep/mac/touchbar.h
deleted file mode 100644
index 4cd711ffeb..0000000000
--- a/osdep/mac/touchbar.h
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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>
-
-#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) NSDictionary *touchbarItems;
-@property(nonatomic, assign) double duration;
-@property(nonatomic, assign) double position;
-@property(nonatomic, assign) int pause;
-
-@end
diff --git a/osdep/mac/touchbar.m b/osdep/mac/touchbar.m
deleted file mode 100644
index 18fe896428..0000000000
--- a/osdep/mac/touchbar.m
+++ /dev/null
@@ -1,342 +0,0 @@
-/*
- * 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 "osdep/mac/touchbar.h"
-#import "osdep/mac/events_objc.h"
-
-#include "config.h"
-
-#if HAVE_SWIFT
-#include "osdep/mac/swift.h"
-#endif
-
-@implementation TouchBar
-
-@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];
-    [[EventsResponder sharedInstance].inputHelper command:self.touchbarItems[identifier][@"cmd"]];
-}
-
-- (void)seekbarChanged:(NSSlider *)slider
-{
-    NSString *identifier = [self getIdentifierFromView:slider];
-    NSString *seek = [NSString stringWithFormat:
-        self.touchbarItems[identifier][@"cmd"], slider.doubleValue];
-    [[EventsResponder sharedInstance].inputHelper command:seek];
-}
-
-- (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;
-    }
-    default:
-        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