diff --git a/bin/mac/libnst64.dylib b/bin/mac/libnst64.dylib new file mode 100755 index 000000000000..dc8d3a978815 Binary files /dev/null and b/bin/mac/libnst64.dylib differ 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