From f8a223b7aca08491e22438df0f1ea233d602907c Mon Sep 17 00:00:00 2001 From: Akemi Date: Sat, 25 Feb 2017 21:56:59 +0100 Subject: osx: initial Touch Bar support --- osdep/macosx_application.m | 46 +++++++ osdep/macosx_application_objc.h | 5 + osdep/macosx_events.h | 2 + osdep/macosx_events.m | 61 +++++++++ osdep/macosx_events_objc.h | 2 + osdep/macosx_touchbar.h | 45 +++++++ osdep/macosx_touchbar.m | 272 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 433 insertions(+) create mode 100644 osdep/macosx_touchbar.h create mode 100644 osdep/macosx_touchbar.m (limited to 'osdep') diff --git a/osdep/macosx_application.m b/osdep/macosx_application.m index d7cdf4b832..cccd60b4e2 100644 --- a/osdep/macosx_application.m +++ b/osdep/macosx_application.m @@ -17,10 +17,12 @@ #include #include +#include "config.h" #include "mpv_talloc.h" #include "common/msg.h" #include "input/input.h" +#include "player/client.h" #import "osdep/macosx_application_objc.h" #include "osdep/macosx_compat.h" @@ -28,6 +30,10 @@ #include "osdep/threads.h" #include "osdep/main-fn.h" +#if HAVE_MACOS_TOUCHBAR +#import "osdep/macosx_touchbar.h" +#endif + #define MPV_PROTOCOL @"mpv://" // Whether the NSApplication singleton was created. If this is false, we are @@ -106,6 +112,38 @@ static void terminate_cocoa_application(void) [super dealloc]; } +#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; +} + +- (void)toggleTouchBarMenu +{ + [NSApp toggleTouchBarCustomizationPalette:self]; +} +#endif + +- (void)processEvent:(struct mpv_event *)event +{ +#if HAVE_MACOS_TOUCHBAR + [(TouchBar *)self.touchBar processEvent:event]; +#endif +} + +- (void)queueCommand:(char *)cmd +{ + [_eventsResponder queueCommand:cmd]; +} + #define _R(P, T, E, K) \ { \ NSMenuItem *tmp = [self menuItemWithParent:(P) title:(T) \ @@ -139,6 +177,13 @@ static void terminate_cocoa_application(void) NSMenu *menu = [[NSMenu alloc] initWithTitle:@"Window"]; _R(menu, @"Minimize", @"m", MPM_MINIMIZE) _R(menu, @"Zoom", @"z", MPM_ZOOM) + +#if HAVE_MACOS_TOUCHBAR + [menu addItem:[NSMenuItem separatorItem]]; + [self menuItemWithParent:menu title:@"Customize Touch Bar…" + action:@selector(toggleTouchBarMenu) keyEquivalent: @""]; +#endif + return [menu autorelease]; } @@ -322,6 +367,7 @@ int cocoa_main(int argc, char *argv[]) { @autoreleasepool { application_instantiated = true; + [[EventsResponder sharedInstance] setIsApplication:YES]; struct playback_thread_ctx ctx = {0}; ctx.argc = &argc; diff --git a/osdep/macosx_application_objc.h b/osdep/macosx_application_objc.h index 4741a14a64..8bbe26d359 100644 --- a/osdep/macosx_application_objc.h +++ b/osdep/macosx_application_objc.h @@ -18,10 +18,15 @@ #import #include "osdep/macosx_application.h" +struct mpv_event; + @interface Application : NSApplication + - (void)initialize_menu; - (void)registerSelector:(SEL)selector forKey:(MPMenuKey)key; - (void)stopPlayback; +- (void)processEvent:(struct mpv_event *)event; +- (void)queueCommand:(char *)cmd; @property(nonatomic, retain) NSMutableDictionary *menuItems; @property(nonatomic, retain) NSArray *files; diff --git a/osdep/macosx_events.h b/osdep/macosx_events.h index a6bfbfef95..019f24feef 100644 --- a/osdep/macosx_events.h +++ b/osdep/macosx_events.h @@ -22,6 +22,7 @@ #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); @@ -36,5 +37,6 @@ 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 index cd1d9df9d8..47448fd965 100644 --- a/osdep/macosx_events.m +++ b/osdep/macosx_events.m @@ -28,18 +28,22 @@ #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" #include "osdep/macosx_compat.h" #import "osdep/macosx_events_objc.h" +#import "osdep/macosx_application_objc.h" #include "config.h" @interface EventsResponder () { struct input_ctx *_inputContext; + struct mpv_handle *_ctx; + BOOL _is_application; NSCondition *_input_lock; CFMachPortRef _mk_tap_port; #if HAVE_APPLE_REMOTE @@ -49,6 +53,8 @@ - (BOOL)handleMediaKey:(NSEvent *)event; - (NSEvent *)handleKey:(NSEvent *)event; +- (void)setMpvHandle:(struct mpv_handle *)ctx; +- (void)readEvents; - (void)startEventMonitor; - (void)startAppleRemote; - (void)stopAppleRemote; @@ -210,6 +216,20 @@ 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) +{ + [[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 + (EventsResponder *)sharedInstance @@ -286,6 +306,47 @@ void cocoa_set_input_context(struct input_ctx *input_context) return r; } +- (void)setIsApplication:(BOOL)isApplication +{ + _is_application = isApplication; +} + +- (void)setMpvHandle:(struct mpv_handle *)ctx +{ + if (_is_application) { + dispatch_sync(dispatch_get_main_queue(), ^{ _ctx = ctx; }); + } else { + _ctx = ctx; + } +} + +- (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 +{ + switch (event->event_id) { + case MPV_EVENT_SHUTDOWN: { + mpv_detach_destroy(_ctx); + _ctx = nil; + break; + } + } + + if(_is_application) { + [NSApp processEvent:event]; + } +} + - (void)startEventMonitor { [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskKeyDown|NSEventMaskKeyUp diff --git a/osdep/macosx_events_objc.h b/osdep/macosx_events_objc.h index 70a058e651..99f00dcc1e 100644 --- a/osdep/macosx_events_objc.h +++ b/osdep/macosx_events_objc.h @@ -29,6 +29,8 @@ struct input_ctx; - (void)setInputContext:(struct input_ctx *)ctx; +- (void)setIsApplication:(BOOL)isApplication; + /// Blocks until inputContext is present. - (void)waitForInputContext; diff --git a/osdep/macosx_touchbar.h b/osdep/macosx_touchbar.h new file mode 100644 index 0000000000..9a5611859b --- /dev/null +++ b/osdep/macosx_touchbar.h @@ -0,0 +1,45 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with mpv. If not, see . + */ + +#import +#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 + +-(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; + +@end diff --git a/osdep/macosx_touchbar.m b/osdep/macosx_touchbar.m new file mode 100644 index 0000000000..846ce6a933 --- /dev/null +++ b/osdep/macosx_touchbar.m @@ -0,0 +1,272 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with mpv. If not, see . + */ + +#include "player/client.h" +#import "macosx_touchbar.h" + +@implementation TouchBar + +@synthesize app = _app; +@synthesize touchbarItems = _touchbar_items; +@synthesize duration = _duration; +@synthesize position = _position; + +- (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" + }] + }; + } + return self; +} + +-(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) { + self.position = *(double *)property->data; + self.position = self.position < 0 ? 0 : self.position; + [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) { + NSButton *playButton = self.touchbarItems[play][@"view"]; + if (*(int *)property->data) { + playButton.image = self.touchbarItems[play][@"imageAlt"]; + } else { + playButton.image = self.touchbarItems[play][@"image"]; + } + } +} + +- (nullable NSTouchBarItem *)touchBar:(NSTouchBar *)touchBar + makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier +{ + if ([self.touchbarItems[identifier][@"type"] isEqualToString:@"slider"]) { + NSSliderTouchBarItem *tbItem = [[NSSliderTouchBarItem alloc] initWithIdentifier:identifier]; + tbItem.slider.minValue = 0.0f; + tbItem.slider.maxValue = 100.0f; + tbItem.target = self; + tbItem.action = @selector(seekbarChanged:); + tbItem.customizationLabel = self.touchbarItems[identifier][@"name"]; + [self.touchbarItems[identifier] setObject:tbItem.slider forKey:@"view"]; + 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"]; + 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"]; + return tbItem; + } + + return nil; +} + +- (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"]; + } +} + +- (void)updateTouchBarTimeItemConstrains +{ + [self removeConstraintForIdentifier:currentPosition]; + [self removeConstraintForIdentifier:timeLeft]; + + if (self.duration <= 0) { + [self applyConstraintFromString:[self formatTime:self.position] + forIdentifier:currentPosition]; + } else { + NSString *durFormat = [self formatTime:self.duration]; + + [self applyConstraintFromString:durFormat forIdentifier:currentPosition]; + [self applyConstraintFromString:[NSString stringWithFormat:@"-%@", durFormat] + forIdentifier:timeLeft]; + } +} + +- (void)updateTouchBarTimeItems +{ + NSSlider *seekSlider = self.touchbarItems[seekBar][@"view"]; + NSTextField *curPosItem = self.touchbarItems[currentPosition][@"view"]; + NSTextField *timeLeftItem = self.touchbarItems[timeLeft][@"view"]; + + if (self.duration <= 0) { + seekSlider.enabled = NO; + seekSlider.doubleValue = 0; + timeLeftItem.stringValue = @""; + } + else { + seekSlider.enabled = YES; + if (!seekSlider.highlighted) + seekSlider.doubleValue = (self.position/self.duration)*100; + int left = (int)(floor(self.duration)-floor(self.position)); + NSString *leftFormat = [self formatTime:left]; + timeLeftItem.stringValue = [NSString stringWithFormat:@"-%@", leftFormat]; + } + NSString *posFormat = [self formatTime:(int)floor(self.position)]; + curPosItem.stringValue = posFormat; + + [self updateTouchBarTimeItemConstrains]; +} + +- (NSString *)getIdentifierFromView:(id)view +{ + NSString *identifier; + for (identifier in self.touchbarItems) + if([self.touchbarItems[identifier][@"view"] isEqual:view]) + break; + return identifier; +} + +- (void)buttonAction:(NSButton *)sender +{ + NSString *identifier = [self getIdentifierFromView:sender]; + [self.app queueCommand:(char *)[self.touchbarItems[identifier][@"cmd"] UTF8String]]; +} + +- (void)seekbarChanged:(NSSliderTouchBarItem *)sender +{ + NSString *identifier = [self getIdentifierFromView:sender.slider]; + NSString *seek = [NSString stringWithFormat: + self.touchbarItems[identifier][@"cmd"], sender.slider.doubleValue]; + [self.app queueCommand:(char *)[seek UTF8String]]; +} + +@end -- cgit v1.2.3