mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-06 03:21:12 +07:00
touchbar: load scrubber items by parts (to speedup creation)
for example, git-branches popup of IDEA-project has thousands of items
This commit is contained in:
Binary file not shown.
@@ -1,10 +1,4 @@
|
||||
typedef void (*execute)(void);
|
||||
typedef id (*createItem)(const char * uid);
|
||||
|
||||
typedef struct ScrubberItemData {
|
||||
char * text;
|
||||
char * raster4ByteRGBA;
|
||||
int rasterW;
|
||||
int rasterH;
|
||||
execute action;
|
||||
} ScrubberItemData;
|
||||
typedef void (*executeScrubberItem)(int itemIndex);
|
||||
typedef int (*updateScrubberCache)(void);
|
||||
|
||||
@@ -11,7 +11,10 @@ static const int g_interItemSpacings = 5;
|
||||
@interface ScrubberItem : NSObject
|
||||
@property (retain, nonatomic) NSString * text;
|
||||
@property (retain, nonatomic) NSImage * img;
|
||||
@property (nonatomic) execute jaction;
|
||||
@property (nonatomic) int index;
|
||||
@property (nonatomic) int positionInsideScrubber;
|
||||
@property (nonatomic) bool visible;
|
||||
@property (nonatomic) bool enabled;
|
||||
@end
|
||||
|
||||
@implementation ScrubberItem
|
||||
@@ -21,49 +24,44 @@ static const int g_interItemSpacings = 5;
|
||||
{
|
||||
ScrubberItemView * _lastSelected;
|
||||
}
|
||||
@property (retain, nonatomic) NSMutableArray * items;
|
||||
@property (retain, nonatomic) NSMutableArray * itemsCache;
|
||||
@property (retain, nonatomic) NSMutableArray * visibleItems;
|
||||
@property (nonatomic) executeScrubberItem delegate;
|
||||
@property (nonatomic) updateScrubberCache updateCache;
|
||||
@end
|
||||
|
||||
static NSMutableArray * _convertItems(ScrubberItemData * items, int count) {
|
||||
NSMutableArray * nsarray = [[[NSMutableArray alloc] initWithCapacity:count] autorelease];
|
||||
for (int c = 0; c < count; ++c) {
|
||||
ScrubberItem * si = [[[ScrubberItem alloc] init] autorelease];
|
||||
si.text = createString(&items[c]);
|
||||
si.img = createImg(&items[c]);
|
||||
si.jaction = items[c].action;
|
||||
[nsarray addObject:si];
|
||||
}
|
||||
return nsarray;
|
||||
}
|
||||
|
||||
@implementation NSScrubberContainer
|
||||
|
||||
- (NSInteger)numberOfItemsForScrubber:(NSScrubber *)scrubber {
|
||||
// NOTE: called from AppKit
|
||||
nstrace(@"scrubber [%@]: items count %d", self.identifier, self.items.count);
|
||||
return self.items.count;
|
||||
nstrace(@"scrubber [%@]: items count %lu, visible %lu", self.identifier, self.itemsCache.count, self.visibleItems.count);
|
||||
return self.visibleItems.count;
|
||||
}
|
||||
|
||||
- (NSScrubberItemView *)scrubber:(NSScrubber *)scrubber viewForItemAtIndex:(NSInteger)itemIndex {
|
||||
// NOTE: called from AppKit-thread (creation when TB becomes visible), uses default autorelease-pool (create before event processing)
|
||||
nstrace(@"scrubber [%@]: create viewForItemAtIndex %d", self.identifier, itemIndex);
|
||||
nstrace(@"scrubber [%@]: create viewForItemAtIndex %lu", self.identifier, itemIndex);
|
||||
ScrubberItemView *itemView = [scrubber makeItemWithIdentifier:g_scrubberItemIdentifier owner:nil];
|
||||
|
||||
ScrubberItem * itemData = [self.items objectAtIndex:itemIndex];
|
||||
ScrubberItem * itemData = [self.visibleItems objectAtIndex:itemIndex];
|
||||
if (itemData == nil) {
|
||||
nserror(@"scrubber [%@]: null item-data at index %d", self.identifier, itemIndex);
|
||||
nserror(@"scrubber [%@]: null item-data at index %lu", self.identifier, itemIndex);
|
||||
return nil;
|
||||
}
|
||||
|
||||
[itemView setImgAndText:itemData.img text:itemData.text];
|
||||
[itemView setEnabled:itemData.enabled];
|
||||
|
||||
if (itemIndex == self.visibleItems.count - 1)
|
||||
(*self.updateCache)();
|
||||
return itemView;
|
||||
}
|
||||
|
||||
- (NSSize)scrubber:(NSScrubber *)scrubber layout:(NSScrubberFlowLayout *)layout sizeForItemAtIndex:(NSInteger)itemIndex {
|
||||
// NOTE: called from AppKit-thread (creation when TB becomes visible), uses default autorelease-pool (create before event processing)
|
||||
ScrubberItem * itemData = [self.items objectAtIndex:itemIndex];
|
||||
ScrubberItem * itemData = [self.visibleItems objectAtIndex:itemIndex];
|
||||
if (itemData == nil) {
|
||||
nserror(@"scrubber [%@]: null item-data at index %d", self.identifier, itemIndex);
|
||||
nserror(@"scrubber [%@]: null item-data at index %lu", self.identifier, itemIndex);
|
||||
return NSMakeSize(0, 0);
|
||||
}
|
||||
|
||||
@@ -77,34 +75,78 @@ static NSMutableArray * _convertItems(ScrubberItemData * items, int count) {
|
||||
}
|
||||
|
||||
- (void)scrubber:(NSScrubber *)scrubber didSelectItemAtIndex:(NSInteger)selectedIndex {
|
||||
ScrubberItem * itemData = [self.items objectAtIndex:selectedIndex];
|
||||
ScrubberItem * itemData = [self.visibleItems objectAtIndex:selectedIndex];
|
||||
if (itemData == nil) {
|
||||
nserror(@"scrubber [%@]: called didSelectItemAtIndex %d, but item-data at this index is null", self.identifier, selectedIndex);
|
||||
nserror(@"scrubber [%@]: called didSelectItemAtIndex %lu, but item-data at this index is null", self.identifier, selectedIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
NSScrubberItemView * view = [scrubber itemViewForItemAtIndex:selectedIndex];
|
||||
if (view != nil) {
|
||||
if (_lastSelected != nil)
|
||||
[_lastSelected setBackgroundSelected:false];
|
||||
|
||||
_lastSelected = (ScrubberItemView *) view;
|
||||
[_lastSelected setBackgroundSelected:true];
|
||||
if (view == nil) {
|
||||
nserror(@"scrubber [%@]: called didSelectItemAtIndex %lu, but item-view at this index is null", self.identifier, selectedIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
nstrace(@"scrubber [%@]: perform action of scrubber item at %d", self.identifier, selectedIndex);
|
||||
(*itemData.jaction)();
|
||||
if (_lastSelected != nil)
|
||||
[_lastSelected setBackgroundSelected:false];
|
||||
|
||||
if (((ScrubberItemView *)view).isEnabled) {
|
||||
_lastSelected = (ScrubberItemView *) view;
|
||||
[_lastSelected setBackgroundSelected:true];
|
||||
|
||||
nstrace(@"scrubber [%@]: perform action of scrubber item at %lu", self.identifier, selectedIndex);
|
||||
(*_delegate)(itemData.index);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static int _fillCache(NSMutableArray * cache, NSMutableArray * visibleItems, void* items, int byteCount) {
|
||||
const int prevCacheSize = cache.count;
|
||||
const int prevVisibleSize = visibleItems.count;
|
||||
const char * p = items;
|
||||
const int itemsCount = *((int*)p);
|
||||
p += 4;
|
||||
nstrace(@"items count = %d, bytes = %d", itemsCount, byteCount);
|
||||
for (int c = 0; c < itemsCount; ++c) {
|
||||
ScrubberItem * si = [[[ScrubberItem alloc] init] autorelease];
|
||||
const int txtLen = *((int*)p);
|
||||
p += 4;
|
||||
si.text = txtLen == 0 ? [NSString stringWithUTF8String:""] : [NSString stringWithUTF8String:p];
|
||||
p += txtLen + 1;
|
||||
//NSLog(@"\t len=%d, txt=%@, offset=%d", txtLen, si.text, p - (char*)items);
|
||||
|
||||
const int w = *((int*)p);
|
||||
p += 4;
|
||||
const int h = *((int*)p);
|
||||
p += 4;
|
||||
//NSLog(@"\t w=%d, h=%d", w, h);
|
||||
|
||||
si.img = w <= 0 || h <= 0 ? nil : createImgFrom4ByteRGBA((const unsigned char *)p, w, h);
|
||||
si.index = c + prevCacheSize;
|
||||
si.positionInsideScrubber = c + prevVisibleSize;
|
||||
si.visible = true;
|
||||
si.enabled = true;
|
||||
[cache addObject:si];
|
||||
[visibleItems addObject:si];
|
||||
|
||||
p += w*h*4;
|
||||
//NSLog(@"\t offset=%d", p - (char*)items);
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: called from AppKit-thread (creation when TB becomes visible), uses default autorelease-pool (create before event processing)
|
||||
id createScrubber(const char* uid, int itemWidth, ScrubberItemData * items, int count) {
|
||||
id createScrubber(const char* uid, int itemWidth, executeScrubberItem delegate, updateScrubberCache updater, void* packedItems, int byteCount) {
|
||||
NSString * nsid = [NSString stringWithUTF8String:uid];
|
||||
nstrace(@"create scrubber [%@] (thread: %@)", nsid, [NSThread currentThread]);
|
||||
|
||||
NSScrubberContainer * scrubberItem = [[NSScrubberContainer alloc] initWithIdentifier:nsid]; // create non-autorelease object to be owned by java-wrapper
|
||||
scrubberItem.items = _convertItems(items, count);
|
||||
scrubberItem.itemsCache = [[[NSMutableArray alloc] initWithCapacity:10/*empiric average popup items count*/] autorelease];
|
||||
scrubberItem.visibleItems = [[[NSMutableArray alloc] initWithCapacity:10/*empiric average popup items count*/] autorelease];
|
||||
scrubberItem.delegate = delegate;
|
||||
scrubberItem.updateCache = updater;
|
||||
|
||||
_fillCache(scrubberItem.itemsCache, scrubberItem.visibleItems, packedItems, byteCount);
|
||||
|
||||
NSScrubber *scrubber = [[[NSScrubber alloc] initWithFrame:NSMakeRect(0, 0, itemWidth, g_heightOfTouchBar)] autorelease];
|
||||
|
||||
@@ -130,19 +172,104 @@ id createScrubber(const char* uid, int itemWidth, ScrubberItemData * items, int
|
||||
return scrubberItem;
|
||||
}
|
||||
|
||||
void updateScrubber(id scrubObj, int itemWidth, ScrubberItemData * items, int count) {
|
||||
// NOTE: called from EDT (when update UI)
|
||||
NSScrubberContainer * container = scrubObj; // TODO: check types
|
||||
nstrace(@"async update scrubber [%@] (thread: %@)", container.identifier, [NSThread currentThread]);
|
||||
NSAutoreleasePool * edtPool = [[NSAutoreleasePool alloc] init];
|
||||
NSMutableArray * nsitems = _convertItems(items, count);
|
||||
// NOTE: called from AppKit (when show last cached item and need update cache with new items)
|
||||
void appendScrubberItems(id scrubObj, void* packedItems, int byteCount) {
|
||||
NSScrubberContainer *container = scrubObj;
|
||||
nstrace(@"scrubber [%@]: called appendScrubberItems", container.identifier);
|
||||
|
||||
const int visibleItemsCountPrev = container.visibleItems.count;
|
||||
_fillCache(container.itemsCache, container.visibleItems, packedItems, byteCount);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
container.items = nsitems;
|
||||
NSScrubber * scrubber = container.view; // TODO: check types
|
||||
// nstrace(@"\tinvalidate layout of scrubber [%@] (thread: %@)", container.identifier, [NSThread currentThread]);
|
||||
[scrubber.scrubberLayout invalidateLayout];
|
||||
// nstrace(@"\treload scrubber [%@] (thread: %@)", container.identifier, [NSThread currentThread]);
|
||||
[scrubber reloadData];
|
||||
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(visibleItemsCountPrev, container.visibleItems.count - visibleItemsCountPrev)];
|
||||
[container.view insertItemsAtIndexes:indexSet];
|
||||
});
|
||||
}
|
||||
|
||||
// NOTE: called from EDT (when update UI)
|
||||
void enableScrubberItems(id scrubObj, void* itemIndices, int count, bool enabled) {
|
||||
NSScrubberContainer *container = scrubObj;
|
||||
NSScrubber *scrubber = container.view;
|
||||
const int sizeInBytes = sizeof(int)*count;
|
||||
int *indices = malloc(sizeInBytes);
|
||||
memcpy(indices, itemIndices, sizeInBytes);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
for (int c = 0; c < count; ++c) {
|
||||
ScrubberItem * itemData = [container.itemsCache objectAtIndex:indices[c]];
|
||||
if (itemData == nil) {
|
||||
nserror(@"scrubber [%@]: called enableScrubberItem %d, but item-data at this index is null", container.identifier, indices[c]);
|
||||
continue;
|
||||
}
|
||||
itemData.enabled = enabled;
|
||||
if (itemData.positionInsideScrubber < 0) {
|
||||
nstrace(@"scrubber [%@]: called enableScrubberItem %d, but item is hidden", container.identifier, indices[c]);
|
||||
continue;
|
||||
}
|
||||
|
||||
NSScrubberItemView *view = [scrubber itemViewForItemAtIndex:itemData.positionInsideScrubber];
|
||||
if (view == nil) {
|
||||
nstrace(@"scrubber [%@]: called enableScrubberItem %d, but item-view at this index is null", container.identifier, indices[c]);
|
||||
continue;
|
||||
}
|
||||
[((ScrubberItemView *) view) setEnabled:enabled];
|
||||
}
|
||||
free(indices);
|
||||
});
|
||||
}
|
||||
|
||||
// NOTE: called from EDT (when update UI)
|
||||
void showScrubberItems(id scrubObj, void* itemIndices, int count, bool show) {
|
||||
NSScrubberContainer * container = scrubObj;
|
||||
NSScrubber *scrubber = container.view;
|
||||
|
||||
const int sizeInBytes = sizeof(int)*count;
|
||||
int *indices = malloc(sizeInBytes);
|
||||
memcpy(indices, itemIndices, sizeInBytes);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// 1. mark items
|
||||
for (int c = 0; c < count; ++c) {
|
||||
ScrubberItem *itemData = [container.itemsCache objectAtIndex:indices[c]];
|
||||
if (itemData == nil) {
|
||||
nserror(@"scrubber [%@]: called showScrubberItems %d, but item-data at this index is null", container.identifier, indices[c]);
|
||||
continue;
|
||||
}
|
||||
itemData.visible = show;
|
||||
}
|
||||
|
||||
// 2. recalculate positions
|
||||
NSMutableIndexSet *indexSet = [[[NSMutableIndexSet alloc] init] autorelease];
|
||||
[container.visibleItems removeAllObjects];
|
||||
int position = 0;
|
||||
for (int c = 0; c < container.itemsCache.count; ++c) {
|
||||
const ScrubberItem * si = [container.itemsCache objectAtIndex:c];
|
||||
if (si == nil)
|
||||
continue;
|
||||
if (si.visible) {
|
||||
if (si.positionInsideScrubber < 0) {
|
||||
// item is visible now => insert into scrubber
|
||||
[indexSet addIndex:position];
|
||||
}
|
||||
si.positionInsideScrubber = position++;
|
||||
[container.visibleItems addObject:si];
|
||||
} else {
|
||||
if (si.positionInsideScrubber >= 0) {
|
||||
// item is hidden now => remove from scrubber
|
||||
[indexSet addIndex:si.positionInsideScrubber];
|
||||
si.positionInsideScrubber = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (show) {
|
||||
nstrace(@"\t show %d items", [indexSet count]);
|
||||
[scrubber insertItemsAtIndexes:indexSet];
|
||||
} else {
|
||||
nstrace(@"\t hide %d items", [indexSet count]);
|
||||
[scrubber removeItemsAtIndexes:indexSet];
|
||||
}
|
||||
|
||||
free(indices);
|
||||
});
|
||||
[edtPool release];
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
@interface ScrubberItemView : NSScrubberItemView
|
||||
- (void)setImgAndText:(NSImage *)img text:(NSString *)txt;
|
||||
- (void)setBackgroundSelected:(bool)selected;
|
||||
- (void)setEnabled:(bool)enabled;
|
||||
- (bool)isEnabled;
|
||||
@end
|
||||
|
||||
extern const int g_marginImgText;
|
||||
|
||||
@@ -8,6 +8,7 @@ const int g_marginBorders = 10;
|
||||
|
||||
@interface ScrubberItemView() {
|
||||
bool _isSelected;
|
||||
bool _isEnabled;
|
||||
}
|
||||
@property (retain) NSImageView * imageView;
|
||||
@property (retain) NSTextField * textField;
|
||||
@@ -22,6 +23,7 @@ const int g_marginBorders = 10;
|
||||
self.imageView = [[[NSImageView alloc] initWithFrame:NSZeroRect] autorelease];
|
||||
|
||||
_isSelected = false;
|
||||
_isEnabled = true;
|
||||
|
||||
self.textField.font = [NSFont systemFontOfSize: 0]; // If size is 0 then macOS will give you the proper font metrics for the NSTouchBar.
|
||||
self.textField.textColor = [NSColor alternateSelectedControlTextColor];
|
||||
@@ -39,10 +41,20 @@ const int g_marginBorders = 10;
|
||||
|
||||
- (void)setBackgroundSelected:(bool)selected {
|
||||
_isSelected = selected;
|
||||
// NSLog(@"set selected %s [%@]", selected ? "true" : "false", self);
|
||||
// [self.view setNeedsDisplayInRect:self.bounds];
|
||||
}
|
||||
|
||||
- (void)setEnabled:(bool)enabled {
|
||||
_isEnabled = enabled;
|
||||
|
||||
self.textField.textColor = _isEnabled ? [NSColor alternateSelectedControlTextColor] : [NSColor disabledControlTextColor];
|
||||
}
|
||||
|
||||
- (bool)isEnabled {
|
||||
return _isEnabled;
|
||||
}
|
||||
|
||||
|
||||
- (void)drawRect:(NSRect)dirtyRect {
|
||||
// NOTE: simple addSubview:NSButton (with img and text and rounded bezel style) doesn't works
|
||||
NSRect rect = NSMakeRect([self bounds].origin.x, [self bounds].origin.y, [self bounds].size.width, [self bounds].size.height);
|
||||
|
||||
@@ -9,6 +9,4 @@ void nserror(NSString *format, ...);
|
||||
// Next functions create autorelease image/strings
|
||||
|
||||
NSImage * createImgFrom4ByteRGBA(const unsigned char *bytes, int w, int h);
|
||||
NSImage * createImg(ScrubberItemData *jdata);
|
||||
NSString * createString(ScrubberItemData *jdata);
|
||||
NSString * createStringFromUTF8(const char *utf8);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#import "Utils.h"
|
||||
|
||||
//#define LOGGING_ERRORS_ENABLED
|
||||
#define LOGGING_ERRORS_ENABLED
|
||||
//#define LOGGING_TRACE_ENABLED
|
||||
|
||||
void nserror(NSString *format, ...) {
|
||||
@@ -55,18 +55,6 @@ NSImage * createImgFrom4ByteRGBA(const unsigned char *bytes, int w, int h) {
|
||||
return nsimg;
|
||||
}
|
||||
|
||||
NSImage * createImg(ScrubberItemData *jdata) {
|
||||
if (jdata == NULL)
|
||||
return nil;
|
||||
return createImgFrom4ByteRGBA((const unsigned char *)jdata->raster4ByteRGBA, jdata->rasterW, jdata->rasterH);
|
||||
}
|
||||
|
||||
NSString * createString(ScrubberItemData *jdata) {
|
||||
if (jdata == NULL || jdata->text == NULL)
|
||||
return nil;
|
||||
return [NSString stringWithUTF8String:jdata->text];
|
||||
}
|
||||
|
||||
NSString * createStringFromUTF8(const char *utf8) {
|
||||
if (utf8 == NULL)
|
||||
return nil;
|
||||
|
||||
@@ -14,10 +14,12 @@ import com.sun.jna.Memory;
|
||||
import com.sun.jna.Native;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import sun.awt.image.WritableRasterNative;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.image.*;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -148,9 +150,10 @@ public class NST {
|
||||
return ourNSTLibrary.createPopover(uid, itemWidth, text, raster4ByteRGBA, w, h, tbObjExpand, tbObjTapAndHold); // called from AppKit, uses per-event autorelease-pool
|
||||
}
|
||||
|
||||
public static ID createScrubber(String uid, int itemWidth, List<TBItemScrubber.ItemData> items) {
|
||||
final NSTLibrary.ScrubberItemData[] vals = _makeItemsArray2(items);
|
||||
return ourNSTLibrary.createScrubber(uid, itemWidth, vals, vals != null ? vals.length : 0); // called from AppKit, uses per-event autorelease-pool
|
||||
public static ID createScrubber(String uid, int itemWidth, NSTLibrary.ScrubberDelegate delegate, NSTLibrary.ScrubberCacheUpdater updater, List<TBItemScrubber.ItemData> items, int itemsCount) {
|
||||
final Memory mem = _packItems(items, 0, itemsCount);
|
||||
final ID scrubberNativePeer = ourNSTLibrary.createScrubber(uid, itemWidth, delegate, updater, mem, (int)mem.size()); // called from AppKit, uses per-event autorelease-pool
|
||||
return scrubberNativePeer;
|
||||
}
|
||||
|
||||
public static ID createGroupItem(String uid, ID[] items, int count) {
|
||||
@@ -192,52 +195,93 @@ public class NST {
|
||||
}
|
||||
|
||||
public static void updateScrubber(ID scrubObj, int itemWidth, List<TBItemScrubber.ItemData> items) {
|
||||
final NSTLibrary.ScrubberItemData[] vals = _makeItemsArray2(items);
|
||||
ourNSTLibrary.updateScrubber(scrubObj, itemWidth, vals, vals != null ? vals.length : 0); // creates autorelease-pool internally
|
||||
LOG.error("updateScrubber masn't be called");
|
||||
}
|
||||
|
||||
private static NSTLibrary.ScrubberItemData[] _makeItemsArray2(List<TBItemScrubber.ItemData> items) {
|
||||
private static Memory _makeIndices(Collection<Integer> indices) {
|
||||
if (indices == null || indices.isEmpty())
|
||||
return null;
|
||||
final int step = Native.getNativeSize(Integer.class);
|
||||
final Memory mem = new Memory(indices.size()*step);
|
||||
int offset = 0;
|
||||
for (Integer i: indices) {
|
||||
mem.setInt(offset, i);
|
||||
offset += step;
|
||||
}
|
||||
return mem;
|
||||
}
|
||||
|
||||
static void appendScrubberItems(ID scrubObj, List<TBItemScrubber.ItemData> items, int fromIndex, int itemsCount) {
|
||||
final Memory mem = _packItems(items, fromIndex, itemsCount);
|
||||
ourNSTLibrary.appendScrubberItems(scrubObj, mem, (int)mem.size()); // called from AppKit, uses per-event autorelease-pool
|
||||
}
|
||||
public static void enableScrubberItem(ID scrubObj, Collection<Integer> indices, boolean enabled) {
|
||||
if (indices == null || indices.isEmpty())
|
||||
return;
|
||||
final Memory mem = _makeIndices(indices);
|
||||
ourNSTLibrary.enableScrubberItems(scrubObj, mem, indices.size(), enabled);
|
||||
}
|
||||
public static void showScrubberItem(ID scrubObj, Collection<Integer> indices, boolean show) {
|
||||
if (indices == null || indices.isEmpty())
|
||||
return;
|
||||
final Memory mem = _makeIndices(indices);
|
||||
ourNSTLibrary.showScrubberItems(scrubObj, mem, indices.size(), show);
|
||||
}
|
||||
|
||||
private static @Nullable Memory _packItems(List<TBItemScrubber.ItemData> items, int fromIndex, int itemsCount) {
|
||||
if (items == null)
|
||||
return null;
|
||||
|
||||
final NSTLibrary.ScrubberItemData scitem = new NSTLibrary.ScrubberItemData();
|
||||
// Structure.toArray allocates a contiguous block of memory internally (each array item is inside this block)
|
||||
// note that for large arrays, this can be extremely slow
|
||||
final NSTLibrary.ScrubberItemData[] result = (NSTLibrary.ScrubberItemData[])scitem.toArray(items.size());
|
||||
// 1. calculate size
|
||||
int byteCount = 4;
|
||||
for (int c = 0; c < itemsCount; ++c) {
|
||||
TBItemScrubber.ItemData id = items.get(fromIndex + c);
|
||||
byteCount += 4 + (id.getTextBytes() != null ? id.getTextBytes().length + 1 : 0);
|
||||
|
||||
int c = 0;
|
||||
for (TBItemScrubber.ItemData id : items) {
|
||||
NSTLibrary.ScrubberItemData out = result[c++];
|
||||
_fill(id, out);
|
||||
if (id.myIcon != null) {
|
||||
final int w = Math.round(id.myIcon.getIconWidth()*id.fMulX);
|
||||
final int h = Math.round(id.myIcon.getIconHeight()*id.fMulX);
|
||||
final int sizeInBytes = w * h * 4;
|
||||
final int totalSize = sizeInBytes + 8;
|
||||
byteCount += totalSize;
|
||||
} else
|
||||
byteCount += 4;
|
||||
}
|
||||
|
||||
// 2. write items
|
||||
final Memory result = new Memory(byteCount + 200);
|
||||
result.setInt(0, itemsCount);
|
||||
int offset = 4;
|
||||
for (int c = 0; c < itemsCount; ++c) {
|
||||
TBItemScrubber.ItemData id = items.get(fromIndex + c);
|
||||
offset = _fill(id, result, offset);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void _fill(TBItemScrubber.ItemData from, @NotNull NSTLibrary.ScrubberItemData out) {
|
||||
if (from.myText != null) {
|
||||
final byte[] data = Native.toByteArray(from.myText, "UTF8");
|
||||
out.text = new Memory(data.length + 1);
|
||||
out.text.write(0, data, 0, data.length);
|
||||
out.text.setByte(data.length, (byte)0);
|
||||
} else
|
||||
out.text = null;
|
||||
|
||||
if (from.myIcon != null) {
|
||||
final BufferedImage img = _getImg4ByteRGBA(from.myIcon);
|
||||
final byte[] raster = ((DataBufferByte)img.getRaster().getDataBuffer()).getData();
|
||||
out.raster4ByteRGBA = new Memory(raster.length);
|
||||
out.raster4ByteRGBA.write(0, raster, 0, raster.length);
|
||||
|
||||
out.rasterW = img.getWidth();
|
||||
out.rasterH = img.getHeight();
|
||||
private static int _fill(@NotNull TBItemScrubber.ItemData from, @NotNull Memory out, int offset) {
|
||||
if (from.getTextBytes() != null) {
|
||||
out.setInt(offset, from.getTextBytes().length);
|
||||
offset += 4;
|
||||
out.write(offset, from.getTextBytes(), 0, from.getTextBytes().length);
|
||||
offset += from.getTextBytes().length;
|
||||
out.setByte(offset, (byte)0);
|
||||
offset += 1;
|
||||
} else {
|
||||
out.raster4ByteRGBA = null;
|
||||
out.rasterW = 0;
|
||||
out.rasterH = 0;
|
||||
out.setInt(offset, 0);
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
out.action = from.myAction;
|
||||
if (from.myIcon != null) {
|
||||
offset += _writeIconRaster(from.myIcon, from.fMulX, out, offset, DataBuffer.TYPE_BYTE);
|
||||
} else {
|
||||
out.setInt(offset, 0);
|
||||
out.setInt(offset +4, 0);
|
||||
offset += 8;
|
||||
}
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
private static byte[] _getRaster(BufferedImage img) {
|
||||
@@ -283,4 +327,81 @@ public class NST {
|
||||
final float fMulX = app != null && UISettings.getInstance().getPresentationMode() ? 40.f/icon.getIconHeight() : (icon.getIconHeight() < 24 ? 40.f/16 : 44.f/icon.getIconHeight());
|
||||
return _getImg4ByteRGBA(icon, fMulX);
|
||||
}
|
||||
|
||||
// returns count of written bytes
|
||||
private static int _writeIconRaster(@NotNull Icon icon, float scale, @NotNull Memory memory, int offset, int dataBufferType) {
|
||||
final int w = Math.round(icon.getIconWidth()*scale);
|
||||
final int h = Math.round(icon.getIconHeight()*scale);
|
||||
|
||||
final int sizeInBytes = w * h * 4;
|
||||
final int totalSize = sizeInBytes + 8;
|
||||
if (memory.size() - offset < totalSize) {
|
||||
LOG.error("insufficient size of allocated memory: avail " + (memory.size() - offset) + ", needs " + totalSize);
|
||||
return 0;
|
||||
}
|
||||
|
||||
memory.setInt(offset, w);
|
||||
offset += 4;
|
||||
memory.setInt(offset, h);
|
||||
offset += 4;
|
||||
|
||||
memory.setMemory(offset, sizeInBytes, (byte)0);
|
||||
|
||||
final BufferedImage image;
|
||||
if (dataBufferType == DataBuffer.TYPE_BYTE) {
|
||||
DataBuffer dataBuffer = new DirectDataBufferByte(memory, offset);
|
||||
final ComponentColorModel colorModel = new ComponentColorModel(ColorModel.getRGBdefault().getColorSpace(), true, false, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE);
|
||||
final SampleModel sm = colorModel.createCompatibleSampleModel(w, h);
|
||||
final WritableRaster raster = WritableRasterNative.createNativeRaster(sm, dataBuffer);
|
||||
image = new BufferedImage(colorModel, raster, false, null);
|
||||
} else {
|
||||
DataBuffer dataBuffer = new DirectDataBufferInt(memory, offset);
|
||||
final DirectColorModel colorModel = new DirectColorModel(ColorModel.getRGBdefault().getColorSpace(), 32, 0x00FF0000, 0xFF00, 0xFF, 0xff000000/*alpha*/, false, DataBuffer.TYPE_BYTE);
|
||||
final SampleModel sm = colorModel.createCompatibleSampleModel(w, h);
|
||||
final WritableRaster raster = WritableRasterNative.createNativeRaster(sm, dataBuffer);
|
||||
image = new BufferedImage(colorModel, raster, false, null);
|
||||
}
|
||||
|
||||
final Graphics2D g = image.createGraphics();
|
||||
g.scale(scale, scale);
|
||||
g.setComposite(AlphaComposite.SrcOver);
|
||||
icon.paintIcon(null, g, 0, 0);
|
||||
g.dispose();
|
||||
|
||||
return totalSize;
|
||||
}
|
||||
}
|
||||
|
||||
class DirectDataBufferInt extends DataBuffer {
|
||||
protected Memory myMemory;
|
||||
private final int myOffset;
|
||||
|
||||
public DirectDataBufferInt(Memory memory, int offset) {
|
||||
super(TYPE_INT, (int)memory.size());
|
||||
this.myMemory = memory;
|
||||
this.myOffset = offset;
|
||||
}
|
||||
public int getElem(int bank, int i) {
|
||||
return myMemory.getInt(myOffset + i / 4);
|
||||
}
|
||||
public void setElem(int bank, int i, int val) {
|
||||
myMemory.setInt(myOffset + i / 4, val);
|
||||
}
|
||||
}
|
||||
|
||||
class DirectDataBufferByte extends DataBuffer {
|
||||
protected Memory myMemory;
|
||||
private final int myOffset;
|
||||
|
||||
public DirectDataBufferByte(Memory mem, int offset) {
|
||||
super(TYPE_BYTE, (int)mem.size());
|
||||
this.myMemory = mem;
|
||||
this.myOffset = offset;
|
||||
}
|
||||
public int getElem(int bank, int i) {
|
||||
return myMemory.getByte(myOffset + i);
|
||||
}
|
||||
public void setElem(int bank, int i, int val) {
|
||||
myMemory.setByte(myOffset + i, (byte)val);
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,7 @@ package com.intellij.ui.mac.touchbar;
|
||||
import com.intellij.ui.mac.foundation.ID;
|
||||
import com.sun.jna.Callback;
|
||||
import com.sun.jna.Library;
|
||||
import com.sun.jna.Pointer;
|
||||
import com.sun.jna.Structure;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import com.sun.jna.Memory;
|
||||
|
||||
public interface NSTLibrary extends Library {
|
||||
ID createTouchBar(String name, ItemCreator creator, String escId); // if defined escId => replace esc button with custom item
|
||||
@@ -21,30 +17,23 @@ public interface NSTLibrary extends Library {
|
||||
void execute();
|
||||
}
|
||||
|
||||
class ScrubberItemData extends Structure {
|
||||
@Override
|
||||
protected List<String> getFieldOrder() { return Arrays.asList("text", "raster4ByteRGBA", "rasterW", "rasterH", "action"); }
|
||||
|
||||
public static class ByRef extends ScrubberItemData implements Structure.ByReference {
|
||||
public ByRef() {}
|
||||
}
|
||||
|
||||
public Pointer text;
|
||||
public Pointer raster4ByteRGBA;
|
||||
public int rasterW;
|
||||
public int rasterH;
|
||||
public Action action;
|
||||
}
|
||||
|
||||
interface ItemCreator extends Callback {
|
||||
ID createItem(String uid);
|
||||
}
|
||||
|
||||
interface ScrubberDelegate extends Callback {
|
||||
void execute(int itemIndex);
|
||||
}
|
||||
|
||||
interface ScrubberCacheUpdater extends Callback {
|
||||
int update(); // NOTE: called from AppKit when last cached item become visible and we need to update native cache with new items
|
||||
}
|
||||
|
||||
// all creators are called from AppKit (when TB becomes visible and asks delegate to create objects) => autorelease objects are owned by default NSAutoReleasePool (of AppKit-thread)
|
||||
// creator returns non-autorelease obj to be owned by java-wrapper
|
||||
ID createButton(String uid, int buttWidth, int buttonFlags, String text, byte[] raster4ByteRGBA, int w, int h, Action action);
|
||||
ID createPopover(String uid, int itemWidth, String text, byte[] raster4ByteRGBA, int w, int h, ID tbObjExpand, ID tbObjTapAndHold);
|
||||
ID createScrubber(String uid, int itemWidth, ScrubberItemData[] items, int count);
|
||||
ID createScrubber(String uid, int itemWidth, ScrubberDelegate delegate, ScrubberCacheUpdater updater, Memory packedItems, int byteCount);
|
||||
ID createGroupItem(String uid, ID[] items, int count);
|
||||
|
||||
int BUTTON_UPDATE_LAYOUT = 1;
|
||||
@@ -75,7 +64,10 @@ public interface NSTLibrary extends Library {
|
||||
// C-implementation creates NSAutoReleasePool internally
|
||||
void updateButton(ID buttonObj, int updateOptions, int buttWidth, int buttonFlags, String text, byte[] raster4ByteRGBA, int w, int h, Action action);
|
||||
void updatePopover(ID popoverObj, int itemWidth, String text, byte[] raster4ByteRGBA, int w, int h, ID tbObjExpand, ID tbObjTapAndHold);
|
||||
void updateScrubber(ID scrubObj, int itemWidth, ScrubberItemData[] items, int count);
|
||||
|
||||
void enableScrubberItems(ID scrubObj, Memory itemIndices, int count, boolean enabled);
|
||||
void showScrubberItems(ID scrubObj, Memory itemIndices, int count, boolean show);
|
||||
void appendScrubberItems(ID scrubObj, Memory packedItems, int byteCount);
|
||||
|
||||
void setArrowImage(ID buttObj, byte[] raster4ByteRGBA, int w, int h);
|
||||
|
||||
|
||||
@@ -1,6 +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;
|
||||
|
||||
import com.intellij.ide.ui.UISettings;
|
||||
import com.intellij.openapi.application.Application;
|
||||
import com.intellij.openapi.application.ApplicationManager;
|
||||
import com.intellij.openapi.diagnostic.Logger;
|
||||
import com.intellij.openapi.util.IconLoader;
|
||||
import com.intellij.ui.mac.foundation.ID;
|
||||
import com.intellij.util.ui.EmptyIcon;
|
||||
@@ -8,23 +12,39 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
class TBItemScrubber extends TBItem {
|
||||
class TBItemScrubber extends TBItem implements NSTLibrary.ScrubberDelegate {
|
||||
private final int myWidth;
|
||||
private final NSTLibrary.ScrubberCacheUpdater myUpdater;
|
||||
private List<ItemData> myItems;
|
||||
private int myNativeItemsCount;
|
||||
|
||||
// NOTE: make scrubber with 'flexible' width when scrubWidth <= 0
|
||||
TBItemScrubber(@NotNull String uid, @Nullable ItemListener listener, int scrubWidth) {
|
||||
super(uid, listener);
|
||||
myWidth = scrubWidth;
|
||||
myUpdater = () -> {
|
||||
// NOTE: called from AppKit (when last cached item become visible)
|
||||
if (myItems == null || myItems.isEmpty())
|
||||
return 0;
|
||||
if (myNativeItemsCount >= myItems.size())
|
||||
return 0;
|
||||
|
||||
final int chunkSize = 25;
|
||||
final int newItemsCount = Math.min(chunkSize, myItems.size() - myNativeItemsCount);
|
||||
NST.appendScrubberItems(myNativePeer, myItems, myNativeItemsCount, newItemsCount);
|
||||
myNativeItemsCount += newItemsCount;
|
||||
return newItemsCount;
|
||||
};
|
||||
}
|
||||
|
||||
TBItemScrubber addItem(Icon icon, String text, Runnable action) {
|
||||
if (myItems == null)
|
||||
myItems = new ArrayList<>();
|
||||
final NSTLibrary.Action nativeAction = action == null && myListener == null ? null : ()-> {
|
||||
final Runnable nativeAction = action == null && myListener == null ? null : ()-> {
|
||||
if (action != null)
|
||||
action.run();
|
||||
if (myListener != null)
|
||||
@@ -39,23 +59,54 @@ class TBItemScrubber extends TBItem {
|
||||
return this;
|
||||
}
|
||||
|
||||
// NOTE: scrubber is immutable (at this moment) => update doesn't called => _create/_update can be unsyncronized
|
||||
// NOTE: scrubber is immutable container => update doesn't called => _create/_update can be unsyncronized
|
||||
|
||||
@Override
|
||||
protected void _updateNativePeer() { NST.updateScrubber(myNativePeer, myWidth, myItems); }
|
||||
protected void _updateNativePeer() {
|
||||
Logger.getInstance(TBItemScrubber.class).error("_updateNativePeer of scrubber masn't be called");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ID _createNativePeer() { return NST.createScrubber(myUid, myWidth, myItems); }
|
||||
protected ID _createNativePeer() {
|
||||
myNativeItemsCount = myItems == null || myItems.isEmpty() ? 0 : Math.min(30, myItems.size());
|
||||
return NST.createScrubber(myUid, myWidth, this, myUpdater, myItems, myNativeItemsCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(int itemIndex) {
|
||||
if (myItems == null || myItems.isEmpty() || itemIndex < 0 || itemIndex >= myItems.size())
|
||||
return;
|
||||
final ItemData id = myItems.get(itemIndex);
|
||||
if (id != null && id.myAction != null)
|
||||
id.myAction.run();
|
||||
}
|
||||
|
||||
static class ItemData {
|
||||
private byte[] myTextBytes; // cache
|
||||
|
||||
final Icon myIcon;
|
||||
final String myText;
|
||||
final NSTLibrary.Action myAction;
|
||||
final Runnable myAction;
|
||||
final float fMulX;
|
||||
|
||||
ItemData(Icon icon, String text, NSTLibrary.Action action) {
|
||||
ItemData(Icon icon, String text, Runnable action) {
|
||||
this.myIcon = icon;
|
||||
this.myText = text;
|
||||
this.myAction = action;
|
||||
|
||||
final Application app = ApplicationManager.getApplication();
|
||||
fMulX = myIcon == null ? 1.f : (app != null && UISettings.getInstance().getPresentationMode() ? 40.f / myIcon.getIconHeight() : (myIcon.getIconHeight() < 24 ? 40.f / 16 : 44.f / myIcon.getIconHeight()));
|
||||
}
|
||||
|
||||
byte[] getTextBytes() {
|
||||
if (myTextBytes == null && myText != null)
|
||||
try {
|
||||
myTextBytes = myText.getBytes("UTF8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return myTextBytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ package com.intellij.ui.mac.touchbar;
|
||||
|
||||
import com.intellij.openapi.util.IconLoader;
|
||||
import com.intellij.ui.mac.foundation.Foundation;
|
||||
import com.intellij.util.containers.hash.HashSet;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Collection;
|
||||
import java.util.Random;
|
||||
|
||||
public class TouchbarTest {
|
||||
private static Icon ourTestIcon = IconLoader.getIcon("/modules/edit.png");
|
||||
@@ -19,7 +20,7 @@ public class TouchbarTest {
|
||||
Foundation.init();
|
||||
NST.loadLibrary();
|
||||
|
||||
final TouchBar testTB = _createTestButtonsTouchbar();
|
||||
final TouchBar testTB = _createTestScrubberTouchbar();
|
||||
testTB.selectVisibleItemsToShow();
|
||||
NST.setTouchBar(testTB);
|
||||
|
||||
@@ -41,17 +42,65 @@ public class TouchbarTest {
|
||||
return testTB;
|
||||
}
|
||||
|
||||
private static Collection<Integer> ourIndices;
|
||||
private static Collection<Integer> _makeRandomCollection(int maxIndex) {
|
||||
if (ourIndices != null)
|
||||
return ourIndices;
|
||||
|
||||
final Random rnd = new Random(System.currentTimeMillis());
|
||||
final int size = rnd.nextInt(maxIndex/2);
|
||||
ourIndices = new HashSet<>();
|
||||
// System.out.println("generated test indices:");
|
||||
if (false) {
|
||||
ourIndices.add(4);
|
||||
ourIndices.add(6);
|
||||
ourIndices.add(7);
|
||||
ourIndices.add(11);
|
||||
System.out.println(ourIndices);
|
||||
return ourIndices;
|
||||
}
|
||||
|
||||
|
||||
for (int c = 0; c < size; ++c) {
|
||||
final int id = rnd.nextInt(maxIndex);
|
||||
ourIndices.add(id);
|
||||
// System.out.println("\t" + id);
|
||||
}
|
||||
|
||||
ourIndices.remove(1);
|
||||
ourIndices.remove(2);
|
||||
return ourIndices;
|
||||
}
|
||||
|
||||
private static boolean ourVisible = true;
|
||||
private static boolean ourEnabled = true;
|
||||
private static TouchBar _createTestScrubberTouchbar() {
|
||||
final TouchBar testTB = new TouchBar("test", false);
|
||||
testTB.addSpacing(true);
|
||||
|
||||
final TBItemScrubber scrubber = testTB.addScrubber();
|
||||
for (int c = 0; c < 11; ++c) {
|
||||
String txt;
|
||||
if (c == 3) txt = "very very long text";
|
||||
else txt = String.format("r%1.2f", Math.random());
|
||||
final int size = 130;
|
||||
for (int c = 0; c < size; ++c) {
|
||||
String txt = String.format("%d[%1.2f]", c, Math.random());
|
||||
int finalC = c;
|
||||
scrubber.addItem(ourTestIcon, txt, () -> System.out.println("performed action of scrubber item at index " + finalC + " [thread:" + Thread.currentThread() + "]"));
|
||||
Runnable action = () -> System.out.println("performed action of scrubber item at index " + finalC + " [thread:" + Thread.currentThread() + "]");
|
||||
if (c == 11) {
|
||||
txt = "very very long text";
|
||||
} else if (c == 1) {
|
||||
txt = "show";
|
||||
action = ()->SwingUtilities.invokeLater(()->{
|
||||
ourVisible = !ourVisible;
|
||||
NST.showScrubberItem(scrubber.myNativePeer, _makeRandomCollection(size - 1), ourVisible);
|
||||
});
|
||||
} else if (c == 2) {
|
||||
txt = "enable";
|
||||
action = ()->SwingUtilities.invokeLater(()->{
|
||||
ourEnabled = !ourEnabled;
|
||||
NST.enableScrubberItem(scrubber.myNativePeer, _makeRandomCollection(size - 1), ourEnabled);
|
||||
});
|
||||
}
|
||||
|
||||
scrubber.addItem(ourTestIcon, txt, action);
|
||||
}
|
||||
|
||||
return testTB;
|
||||
@@ -72,14 +121,12 @@ public class TouchbarTest {
|
||||
|
||||
expandTB.addButton().setIcon(ourTestIcon).setThreadSafeAction(createPrintTextCallback("pressed popover-image button"));
|
||||
final TBItemScrubber scrubber = expandTB.addScrubber();
|
||||
List<TBItemScrubber.ItemData> scrubberItems = new ArrayList<>();
|
||||
for (int c = 0; c < 15; ++c) {
|
||||
String txt;
|
||||
if (c == 7) txt = "very very long configuration name (debugging type)";
|
||||
else txt = String.format("r%1.2f", Math.random());
|
||||
int finalC = c;
|
||||
scrubberItems.add(new TBItemScrubber.ItemData(ourTestIcon, txt,
|
||||
() -> System.out.println("JAVA: performed action of scrubber item at index " + finalC + " [thread:" + Thread.currentThread() + "]")));
|
||||
scrubber.addItem(ourTestIcon, txt, () -> System.out.println("JAVA: performed action of scrubber item at index " + finalC + " [thread:" + Thread.currentThread() + "]"));
|
||||
}
|
||||
expandTB.selectVisibleItemsToShow();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user