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:
Artem Bochkarev
2019-02-17 18:11:35 +03:00
parent e03ccd53a4
commit a82d09f74e
11 changed files with 477 additions and 145 deletions

Binary file not shown.

View File

@@ -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);

View File

@@ -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];
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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;
}
}
}

View File

@@ -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();