From dd7a3348943193e82d91dd1da45ddb0b9f9ad254 Mon Sep 17 00:00:00 2001 From: Artem Bochkarev Date: Tue, 6 Mar 2018 14:29:35 +0700 Subject: [PATCH] IDEA-163208: initial impl of touchbar support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NOTES: объект-делегат (для создания TB-итемов по запросу) определен в нативной либе, присоединяется к NSApplication в момент инициализации; в качестве альтернативы можно создавать и настраивать объекты типа TouchBar и TouchBarItem через Foundation, однако такое решение выглядит менее гибким (+ неудобочитаемый и некомпактный код) --- bin/mac/libnst64.dylib | Bin 0 -> 14984 bytes native/MacTouchBar/nst.m | 117 ++++++++++++++++++ .../ide/MacOSApplicationProvider.java | 2 + .../src/com/intellij/ide/NSTLibrary.java | 8 ++ .../src/com/intellij/ide/TBItemCallback.java | 36 ++++++ .../intellij/ui/mac/touchbar/TouchBar.java | 103 +++++++++++++++ .../ui/mac/touchbar/TouchBarItem.java | 10 ++ .../util/resources/misc/registry.properties | 2 + 8 files changed, 278 insertions(+) create mode 100755 bin/mac/libnst64.dylib create mode 100644 native/MacTouchBar/nst.m create mode 100644 platform/platform-impl/src/com/intellij/ide/NSTLibrary.java create mode 100644 platform/platform-impl/src/com/intellij/ide/TBItemCallback.java create mode 100644 platform/platform-impl/src/com/intellij/ui/mac/touchbar/TouchBar.java create mode 100644 platform/platform-impl/src/com/intellij/ui/mac/touchbar/TouchBarItem.java diff --git a/bin/mac/libnst64.dylib b/bin/mac/libnst64.dylib new file mode 100755 index 0000000000000000000000000000000000000000..dc8d3a9788151e29919d4333db3312c511eea29c GIT binary patch literal 14984 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 literal 0 HcmV?d00001 diff --git a/native/MacTouchBar/nst.m b/native/MacTouchBar/nst.m new file mode 100644 index 000000000000..7784fac9e756 --- /dev/null +++ b/native/MacTouchBar/nst.m @@ -0,0 +1,117 @@ +#import + +typedef void (*callback)(void); + +@interface ItemDesc : NSObject +{ + NSString * uid; + NSString * type; + NSString * text; + callback action; +} +- (id)init:(char*)puid type:(char*)ptype text:(char*)ptext act:(callback)act; + +// TODO: make props non-atomic +@property (readonly) NSString * getUid; +@property (readonly) NSString * getType; +@property (readonly) NSString * getText; +@property (readonly) callback getAction; + +@end + +@implementation ItemDesc +- (id)init:(char*)puid type:(char*)ptype text:(char*)ptext act:(callback)act { + self = [super init]; + if (self) { + uid = [NSString stringWithUTF8String:puid]; + type = [NSString stringWithUTF8String:ptype]; + text = [NSString stringWithUTF8String:ptext]; + action = act; + } + return self; +} +@synthesize getUid = uid, getType = type, getText = text, getAction = action; +@end + +static NSMutableDictionary *g_uid2desc = nil; + +void registerItem(char* uid, char* type, char* text, callback action) { + ItemDesc * idesc = [[ItemDesc alloc] init:uid type:type text:text act:action]; + if (idesc == nil) { + // TODO: log error + return; + } + if (g_uid2desc == nil) + g_uid2desc = [[NSMutableDictionary alloc] init]; + g_uid2desc[idesc.getUid] = idesc; +} + +@interface NSButtonWithID : NSButton +{ + NSString * uid; +} +@property (retain) NSString * uid; +@end + +@implementation NSButtonWithID +@synthesize uid; +@end + +@interface NSTDelegate : NSObject +- (void)buttonExec:(id)sender; +@end + + +// TODO: implement normal error logging +@implementation NSTDelegate + +// This gets called while the NSTouchBar is being constructed, for each NSTouchBarItem to be created. +- (nullable NSTouchBarItem *)touchBar:(NSTouchBar *)touchBar makeItemForIdentifier:(NSTouchBarItemIdentifier)identifier +{ + if (g_uid2desc == nil) { + NSLog(@"ERROR: try makeTouchBarItem for item '%@', but global items registry is empty", identifier); + return nil; + } + ItemDesc * idesc = g_uid2desc[identifier]; + if (idesc == nil) { + NSLog(@"ERROR: called makeTouchBarItem for item '%@' that wasn't registered", identifier); + return nil; + } + + if ([idesc.getType isEqualToString:@"label"]) { + NSTextField *theLabel = [NSTextField labelWithString:NSLocalizedString(@"MyLabel", @"")]; + + NSCustomTouchBarItem *customItemForLabel = + [[NSCustomTouchBarItem alloc] initWithIdentifier:idesc.getText]; + customItemForLabel.view = theLabel; + + // We want this label to always be visible no matter how many items are in the NSTouchBar instance. + customItemForLabel.visibilityPriority = NSTouchBarItemPriorityHigh; + + return customItemForLabel; + } + if ([idesc.getType isEqualToString:@"button"]) { + SEL selAction = @selector(buttonExec:); + NSButtonWithID *theButton = [NSButtonWithID buttonWithTitle:idesc.getText target:self action:selAction]; + theButton.uid = idesc.getUid; + + NSCustomTouchBarItem *customItemForButton = + [[NSCustomTouchBarItem alloc] initWithIdentifier:identifier]; + customItemForButton.view = theButton; + + // We want this label to always be visible no matter how many items are in the NSTouchBar instance. + customItemForButton.visibilityPriority = NSTouchBarItemPriorityHigh; + return customItemForButton; + } + + NSLog(@"ERROR: called makeTouchBarItem for item '%@' that has unsopported type '%@'", identifier, idesc.getType); + return nil; +} + +- (void)buttonExec:(id)sender { + NSButtonWithID * butSender = (NSButtonWithID *)sender; + ItemDesc * idesc = g_uid2desc[butSender.uid]; + (*idesc.getAction)(); +} + +@end diff --git a/platform/platform-impl/src/com/intellij/ide/MacOSApplicationProvider.java b/platform/platform-impl/src/com/intellij/ide/MacOSApplicationProvider.java index 2db279716f42..15da149cf400 100644 --- a/platform/platform-impl/src/com/intellij/ide/MacOSApplicationProvider.java +++ b/platform/platform-impl/src/com/intellij/ide/MacOSApplicationProvider.java @@ -22,6 +22,7 @@ import com.intellij.openapi.wm.IdeFocusManager; import com.intellij.platform.PlatformProjectOpenProcessor; import com.intellij.ui.mac.foundation.Foundation; import com.intellij.ui.mac.foundation.ID; +import com.intellij.ui.mac.touchbar.TouchBar; import com.sun.jna.Callback; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -136,6 +137,7 @@ public class MacOSApplicationProvider { }); }); installAutoUpdateMenu(); + TouchBar.initialize(); } private static void installAutoUpdateMenu() { diff --git a/platform/platform-impl/src/com/intellij/ide/NSTLibrary.java b/platform/platform-impl/src/com/intellij/ide/NSTLibrary.java new file mode 100644 index 000000000000..37d5f5ed080d --- /dev/null +++ b/platform/platform-impl/src/com/intellij/ide/NSTLibrary.java @@ -0,0 +1,8 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.ide; + +import com.sun.jna.Library; + +public interface NSTLibrary extends Library { + void registerItem(String uid, String type, String text, TBItemCallback action); +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/ide/TBItemCallback.java b/platform/platform-impl/src/com/intellij/ide/TBItemCallback.java new file mode 100644 index 000000000000..d4844a1d95af --- /dev/null +++ b/platform/platform-impl/src/com/intellij/ide/TBItemCallback.java @@ -0,0 +1,36 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.ide; + +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.actionSystem.ex.ActionManagerEx; +import com.intellij.openapi.application.ApplicationManager; +import com.sun.jna.Callback; + +import java.awt.*; +import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; + +import static java.awt.event.ComponentEvent.COMPONENT_FIRST; + +public class TBItemCallback implements Callback { + final String myActionId; + + public TBItemCallback(String actionId) { + myActionId = actionId; + } + + public void callback () { + ApplicationManager.getApplication().invokeLater(() -> _performAction()); + } + + private void _performAction() { + final ActionManagerEx actionManagerEx = ActionManagerEx.getInstanceEx(); + final AnAction act = ActionManager.getInstance().getAction(myActionId); + final KeyboardFocusManager focusManager=KeyboardFocusManager.getCurrentKeyboardFocusManager(); + final Component focusOwner = focusManager.getFocusedWindow(); + + // TODO: create key-event with proper key-codes OR try to use ActionCommand.getInputEvent + InputEvent ie = new KeyEvent(focusOwner, COMPONENT_FIRST, System.currentTimeMillis(), 0, 0, '\0'); + actionManagerEx.tryToExecute(act, ie, focusOwner, ActionPlaces.UNKNOWN, false); + } +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/ui/mac/touchbar/TouchBar.java b/platform/platform-impl/src/com/intellij/ui/mac/touchbar/TouchBar.java new file mode 100644 index 000000000000..15ef810ddc34 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/ui/mac/touchbar/TouchBar.java @@ -0,0 +1,103 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.ui.mac.touchbar; + +import com.intellij.ide.TBItemCallback; +import com.intellij.ide.NSTLibrary; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.util.SystemInfo; +import com.intellij.openapi.util.registry.Registry; +import com.intellij.ui.mac.foundation.Foundation; +import com.intellij.ui.mac.foundation.ID; +import com.intellij.util.lang.UrlClassLoader; +import com.sun.jna.Native; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; + +public class TouchBar { + private static final Logger ourLog = Logger.getInstance(TouchBar.class); + + private static final NSTLibrary ourNSTLibrary; + private static final Map ourActions = new HashMap<>(); + + static { + // TODO: check macOS version with use of CoreServices: + // SInt32 major, minor, bugfix; + // Gestalt(gestaltSystemVersionMajor, &major); + // Gestalt(gestaltSystemVersionMinor, &minor); + // Gestalt(gestaltSystemVersionBugFix, &bugfix); + // NSString *systemVersion = [NSString stringWithFormat:@"%d.%d.%d", major, minor, bugfix]; + + // NOTE: can also check existence of process 'ControlStrip' to determine touchbar availability + + final boolean isTouchbarAvailable = SystemInfo.isMac && Registry.is("ide.mac.touchbar.use", false); + NSTLibrary lib = null; + if (isTouchbarAvailable) { + try { + UrlClassLoader.loadPlatformLibrary("nst"); + + // Set JNA to convert java.lang.String to char* using UTF-8, and match that with + // the way we tell CF to interpret our char* + // May be removed if we use toStringViaUTF16 + System.setProperty("jna.encoding", "UTF8"); + + final Map nstOptions = new HashMap<>(); + lib = Native.loadLibrary("nst", NSTLibrary.class, nstOptions); + } catch (Throwable e) { + ourLog.error("Failed to load nst library for touchbar: ", e); + } + } + ourNSTLibrary = lib; + + // TODO: must show only functional-keys when corresponding settings key is true + } + + public static boolean isAvailable() { return ourNSTLibrary != null; } + + public static void initialize() { + if (!isAvailable()) + return; + + final String[] testItems = new String[] {"Run"}; + final String[] testItemIds = new String[testItems.length]; + + // fill available actions + // register action on native side + for (int c = 0; c < testItems.length; ++c) { + String key = testItems[c]; + final TBItemCallback act = new TBItemCallback(key); + ourActions.put(key, act); + final String uid = "button."+key; + testItemIds[c] = uid; + ourNSTLibrary.registerItem(uid, "button", key, act); + } + + final ID pool = Foundation.invoke("NSAutoreleasePool", "new"); + try { + final ID app = Foundation.invoke("NSApplication", "sharedApplication"); + Foundation.invoke(app, "setAutomaticCustomizeTouchBarMenuItemEnabled:", true); + + final ID tb = Foundation.invoke(Foundation.invoke("NSTouchBar", "alloc"), "init"); + final ID tbd = Foundation.invoke(Foundation.invoke("NSTDelegate", "alloc"), "init"); + Foundation.invoke(tb, "setDelegate:", tbd); + + final Object[] nsTestItemIds = _str2ids(testItemIds); + final ID items = Foundation.invoke("NSArray", "arrayWithObjects:", nsTestItemIds); + + Foundation.invoke(tb, "setDefaultItemIdentifiers:", items); + + // TODO: select best placement in responder-chain (probably need attach to main-window controller) + Foundation.invoke(app, "setTouchBar:", tb); + } finally { + Foundation.invoke(pool, "release"); + } + } + + private static @NotNull Object[] _str2ids(@NotNull String[] strs) { + Object[] result = new Object[strs.length]; + for (int c = 0; c < strs.length; ++c) + result[c] = Foundation.nsString(strs[c]); + return result; + } +} diff --git a/platform/platform-impl/src/com/intellij/ui/mac/touchbar/TouchBarItem.java b/platform/platform-impl/src/com/intellij/ui/mac/touchbar/TouchBarItem.java new file mode 100644 index 000000000000..07c6f93244c9 --- /dev/null +++ b/platform/platform-impl/src/com/intellij/ui/mac/touchbar/TouchBarItem.java @@ -0,0 +1,10 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.ui.mac.touchbar; + +public class TouchBarItem { + final String myName; + + public TouchBarItem(String name) { + myName = name; + } +} diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index e51f4bce9339..81af343a7496 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -1204,6 +1204,8 @@ markdown.clear.cache.interval=600000 ide.mac.new.color.picker=false +ide.mac.touchbar.use = false + trace.focus.on.app.activation=false trace.focus.on.app.activation.description=Focus tracing on application activation and deactivation trace.focus.on.app.restartRequired=false