JBR-3131: support custom view for system menu items

(cherry picked from commit 78d509ac0f)
(cherry picked from commit ca05fefb28)
This commit is contained in:
Artem Bochkarev
2021-02-02 22:40:55 +03:00
committed by jbrbot
parent 462b5326c9
commit 32d6ee2659
6 changed files with 340 additions and 2 deletions

View File

@@ -100,6 +100,17 @@ final class ScreenMenuItem extends MenuItem
fMenuItem.removeComponentListener(this);
}
static void syncAcceleratorText(MenuItem menuItem, JMenuItem fMenuItem) {
Object acceleratorText = fMenuItem.getClientProperty("accelerator.text");
if (acceleratorText instanceof String) {
Object peer = AWTAccessor.getMenuComponentAccessor().getPeer(menuItem);
if (peer instanceof CMenuItem) {
final CMenuItem cmi = (CMenuItem) peer;
cmi.setAcceleratorText((String)acceleratorText);
}
}
}
static void syncLabelAndKS(MenuItem menuItem, String label, KeyStroke ks) {
Object peer = AWTAccessor.getMenuComponentAccessor().getPeer(menuItem);
if (!(peer instanceof CMenuItem)) {
@@ -123,6 +134,12 @@ final class ScreenMenuItem extends MenuItem
@Override
public void setAccelerator(final KeyStroke ks) {
syncLabelAndKS(this, fMenuItem.getText(), ks);
syncAcceleratorText(this, fMenuItem);
}
@Override
public void setAcceleratorText(String acceleratorText) {
syncAcceleratorText(this, fMenuItem);
}
public void actionPerformed(final ActionEvent e) {

View File

@@ -123,6 +123,12 @@ final class ScreenMenuItemCheckbox extends CheckboxMenuItem
@Override
public void setAccelerator(final KeyStroke ks) {
ScreenMenuItem.syncLabelAndKS(this, fMenuItem.getText(), ks);
ScreenMenuItem.syncAcceleratorText(this, fMenuItem);
}
@Override
public void setAcceleratorText(String acceleratorText) {
ScreenMenuItem.syncAcceleratorText(this, fMenuItem);
}
public void actionPerformed(final ActionEvent e) {

View File

@@ -36,6 +36,7 @@ interface ScreenMenuPropertyHandler {
public void setLabel(String f);
public void setIcon(Icon icon);
public void setAccelerator(KeyStroke ks);
default void setAcceleratorText(String acceleratorText) {}
public void setToolTipText(String tooltip);
public void setChildVisible(javax.swing.JMenuItem child, boolean b);
public void setIndeterminate(boolean indeterminate);

View File

@@ -67,6 +67,11 @@ class ScreenMenuPropertyListener implements PropertyChangeListener {
return;
}
if ("accelerator.text".equals(propertyName)) {
fMenu.setAcceleratorText((String)e.getNewValue());
return;
}
if (AbstractButton.TEXT_CHANGED_PROPERTY.equals(propertyName)) {
fMenu.setLabel((String)e.getNewValue());
return;

View File

@@ -104,6 +104,10 @@ public class CMenuItem extends CMenuComponent implements MenuItemPeer {
setLabel(label, (char)0, KeyEvent.VK_UNDEFINED, 0);
}
public void setAcceleratorText(String acceleratorText) {
execute(ptr -> nativeSetAcceleratorText(ptr, acceleratorText));
}
/**
* This is new API that we've added to AWT menu items
* because AWT menu items are used for Swing screen menu bars
@@ -150,6 +154,7 @@ public class CMenuItem extends CMenuComponent implements MenuItemPeer {
private native void nativeSetImage(long modelPtr, long image);
private native void nativeSetTooltip(long modelPtr, String text);
private native void nativeSetEnabled(long modelPtr, boolean b);
private native void nativeSetAcceleratorText(long modelPtr, String acceleratorText);
// native callbacks
void handleAction(final long when, final int modifiers) {

View File

@@ -36,6 +36,277 @@
#define NOT_A_CHECKBOXMENU -2
@interface CustomMenuItemView : NSView {
int16_t fireTimes;
BOOL isSelected;
NSSize shortcutSize;
NSSize textSize;
NSTrackingArea * trackingArea;
CMenuItem * owner;
}
@property (retain) NSString * keyShortcut;
@end
@implementation CustomMenuItemView
static CGFloat menuItemHeight = 18.f;
static CGFloat marginLeft = 20.f;
static CGFloat marginRight = 10.f;
static CGFloat gapTxtIcon = 5.f;
static CGFloat gapTxtShortcut = 23.f;
static NSFont * menuFont;
static NSFont * menuShortcutFont;
static NSColor * customBg = nil;
+ (void)initialize {
menuFont = [NSFont menuBarFontOfSize:(0)];
menuShortcutFont = [NSFont menuBarFontOfSize:(0)];
NSDictionary * attributes = [NSDictionary dictionaryWithObjectsAndKeys:menuFont, NSFontAttributeName, nil];
NSSize qSize = [[[[NSAttributedString alloc] initWithString:@"Q" attributes:attributes] autorelease] size];
// use empiric proportions (to look like default view)
menuItemHeight = qSize.height * 1.1f;
marginLeft = menuItemHeight * 1.1f;
marginRight = menuItemHeight * 0.55f;
gapTxtIcon = menuItemHeight * 0.27f;
gapTxtShortcut = menuItemHeight * 1.2f;
// Initialize custom bg color (for light theme with enabled accessibility.reduceTransparency)
// If we use transparent bg than we will see visual inconsistency
// And it seems that we can't obtain this color from system
NSUserDefaults * defs = [NSUserDefaults standardUserDefaults];
NSDictionary<NSString *,id> * dict = [defs persistentDomainForName:@"com.apple.universalaccess.plist"];
if (dict != nil) {
id reduceVal = [dict valueForKey:@"reduceTransparency"];
if (reduceVal != nil && [reduceVal isKindOfClass:[NSNumber class]] && [reduceVal intValue] != 0) {
NSString * mode = [defs stringForKey:@"AppleInterfaceStyle"];
if (mode == nil) { // light system theme
customBg = [NSColor colorWithCalibratedWhite:246.f/255 alpha:1.f];
[customBg retain];
// NSLog(@"\treduceTransparency is enabled (use custom background color for menu items)");
}
}
}
// NSLog(@"\tmenuItemHeight=%1.2f, marginLeft=%1.2f, marginRight=%1.2f, gapTxtIcon=%1.2f, gapTxtShortcut=%1.2f",
// menuItemHeight, marginLeft, marginRight, gapTxtIcon, gapTxtShortcut);
}
- (id)initWithOwner:(CMenuItem *)menuItem {
NSRect viewRect = NSMakeRect(0, 0, /* width autoresizes */ 1, menuItemHeight);
self = [super initWithFrame:viewRect];
if (self == nil) {
return self;
}
owner = menuItem;
self.autoresizingMask = NSViewWidthSizable;
self.keyShortcut = nil;
fireTimes = 0;
isSelected = NO;
trackingArea = nil;
shortcutSize = NSZeroSize;
textSize = NSZeroSize;
return self;
}
- (void)dealloc {
if(trackingArea != nil) {
[trackingArea release];
}
[super dealloc];
}
- (void)mouseEntered:(NSEvent*)event {
if ([owner isEnabled] && !isSelected) {
isSelected = YES;
[self setNeedsDisplay:YES];
}
}
- (void)mouseExited:(NSEvent *)event {
if (isSelected) {
isSelected = NO;
[self setNeedsDisplay:YES];
}
}
- (void)mouseUp:(NSEvent*)event {
if (![owner isEnabled])
return;
fireTimes = 0;
isSelected = !isSelected;
[self setNeedsDisplay:YES];
NSTimer *timer = [NSTimer timerWithTimeInterval:0.05 target:self selector:@selector(animateDismiss:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSEventTrackingRunLoopMode];
}
-(void)updateTrackingAreas {
[super updateTrackingAreas];
if(trackingArea != nil) {
[self removeTrackingArea:trackingArea];
[trackingArea release];
}
int opts = (NSTrackingMouseEnteredAndExited | NSTrackingActiveAlways);
trackingArea = [[NSTrackingArea alloc] initWithRect:[self bounds]
options:opts
owner:self
userInfo:nil];
[self addTrackingArea:trackingArea];
}
-(void)animateDismiss:(NSTimer *)aTimer {
if (fireTimes <= 2) {
isSelected = !isSelected;
[self setNeedsDisplay:YES];
} else {
[aTimer invalidate];
[self sendAction];
}
fireTimes++;
}
- (void)sendAction {
NSMenuItem * mi = owner.menuItem;
[NSApp sendAction:[mi action] to:[mi target] from:mi];
NSMenu *menu = [mi menu];
[menu cancelTracking];
// NOTE: we can also invoke handler directly [owner handleAction:[owner menuItem]];
}
//#define VISUAL_DEBUG_CUSTOM_ITEM_VIEW
- (void) drawRect:(NSRect)dirtyRect {
NSRect rectBounds = [self bounds];
NSString * text = owner.menuItem.title;
#ifdef VISUAL_DEBUG_CUSTOM_ITEM_VIEW
[[NSColor yellowColor] set];
NSFrameRectWithWidth([self bounds], 1.0f);
#endif // VISUAL_DEBUG_CUSTOM_ITEM_VIEW
const BOOL isEnabled = [owner isEnabled];
NSColor * textColor = [NSColor textColor];
NSColor * bgColor = customBg != nil ? customBg : [NSColor clearColor];
if (!isEnabled) {
textColor = [NSColor grayColor];
} else if (isSelected) {
if (@available(macOS 10.14, *)) {
bgColor = [NSColor controlAccentColor];
} else {
bgColor = [NSColor selectedControlColor];
}
textColor = [NSColor selectedMenuItemTextColor];
}
// 1. draw bg
[bgColor set];
NSRectFill(rectBounds);
// 2. draw icon if presented
CGFloat x = rectBounds.origin.x + marginLeft;
NSImage * image = owner.menuItem.image;
if (image != nil) {
NSRect imageBounds = rectBounds;
imageBounds.origin.x = x;
imageBounds.size.width = image.size.width;
[image drawInRect:imageBounds];
x += image.size.width + gapTxtIcon;
#ifdef VISUAL_DEBUG_CUSTOM_ITEM_VIEW
[[NSColor redColor] set];
NSFrameRectWithWidth(imageBounds, 1.0f);
#endif // VISUAL_DEBUG_CUSTOM_ITEM_VIEW
}
// 3. draw text
[textColor set];
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
menuFont, NSFontAttributeName,
textColor, NSForegroundColorAttributeName,
nil];
NSRect txtBounds = rectBounds;
txtBounds.origin.x = x;
txtBounds.size.width = textSize.width;
[text drawInRect:txtBounds withAttributes:attributes];
#ifdef VISUAL_DEBUG_CUSTOM_ITEM_VIEW
[[NSColor blackColor] set];
NSFrameRectWithWidth(txtBounds, 1.0f);
#endif // VISUAL_DEBUG_CUSTOM_ITEM_VIEW
if (self.keyShortcut != nil) {
// 4.1 draw shortcut
NSRect keyBounds = rectBounds;
keyBounds.origin.x = keyBounds.size.width - marginRight - shortcutSize.width;
keyBounds.size.width = shortcutSize.width;
NSDictionary *keyAttr = [NSDictionary dictionaryWithObjectsAndKeys:
menuShortcutFont, NSFontAttributeName,
textColor, NSForegroundColorAttributeName,
nil];
[self.keyShortcut drawInRect:keyBounds withAttributes:keyAttr];
#ifdef VISUAL_DEBUG_CUSTOM_ITEM_VIEW
[[NSColor magentaColor] set];
NSFrameRectWithWidth(keyBounds, 1.0f);
#endif // VISUAL_DEBUG_CUSTOM_ITEM_VIEW
} else {
if ([owner isKindOfClass:CMenu.class]) {
// 4.2 draw arrow-image of submenu
NSImage *arrow = [NSImage imageNamed:NSImageNameRightFacingTriangleTemplate]; // TODO: use correct triangle image
NSRect arrowBounds = rectBounds;
arrowBounds.origin.x = rectBounds.size.width - marginRight - arrow.size.width;
arrowBounds.origin.y = rectBounds.origin.y + (rectBounds.size.height - arrow.size.height) / 2;
arrowBounds.size = arrow.size;
[arrow drawInRect:arrowBounds];
#ifdef VISUAL_DEBUG_CUSTOM_ITEM_VIEW
[[NSColor magentaColor] set];
NSFrameRectWithWidth(arrowBounds, 1.0f);
#endif // VISUAL_DEBUG_CUSTOM_ITEM_VIEW
}
}
}
- (void)recalcSizes {
NSString * text = owner.menuItem.title;
NSImage * image = owner.menuItem.image;
NSDictionary * attributes = [NSDictionary dictionaryWithObjectsAndKeys:menuFont, NSFontAttributeName, nil];
textSize = [[[[NSAttributedString alloc] initWithString:text attributes:attributes] autorelease] size];
NSSize resultSize = NSMakeSize(textSize.width + marginLeft + marginRight, menuItemHeight);
if (image != nil) {
NSSize imgSize = image.size;
resultSize.width += imgSize.width + gapTxtIcon;
}
if (self.keyShortcut != nil) {
NSDictionary * ksa = [NSDictionary dictionaryWithObjectsAndKeys:menuShortcutFont, NSFontAttributeName, nil];
shortcutSize = [[[[NSAttributedString alloc] initWithString:self.keyShortcut attributes:ksa] autorelease] size];
resultSize.width += shortcutSize.width + gapTxtShortcut;
}
[self.widthAnchor constraintGreaterThanOrEqualToConstant:resultSize.width].active = YES;
}
@end
@implementation CMenuItem
@@ -112,6 +383,20 @@
JNI_COCOA_EXIT(env);
}
- (void) setAcceleratorText:(NSString *)acceleratorText {
if ([acceleratorText isEqualToString:@""]) {
acceleratorText = nil;
}
[ThreadUtilities performOnMainThreadWaiting:NO block:^(){
CustomMenuItemView *menuItemView = fMenuItem.view;
if (menuItemView == nil) {
fMenuItem.view = menuItemView = [[[CustomMenuItemView alloc] initWithOwner:self] autorelease];
}
menuItemView.keyShortcut = acceleratorText;
[menuItemView recalcSizes];
}];
}
- (void) setJavaLabel:(NSString *)theLabel shortcut:(NSString *)theKeyEquivalent modifierMask:(jint)modifiers {
NSUInteger modifierMask = 0;
@@ -138,13 +423,18 @@
[fMenuItem setKeyEquivalent:theKeyEquivalent];
[fMenuItem setKeyEquivalentModifierMask:modifierMask];
[fMenuItem setTitle:theLabel];
if ([fMenuItem.view isKindOfClass:CustomMenuItemView.class]) {
[(CustomMenuItemView *)fMenuItem.view recalcSizes];
}
}];
}
- (void) setJavaImage:(NSImage *)theImage {
[ThreadUtilities performOnMainThreadWaiting:NO block:^(){
[fMenuItem setImage:theImage];
if ([fMenuItem.view isKindOfClass:CustomMenuItemView.class]) {
[(CustomMenuItemView *)fMenuItem.view recalcSizes];
}
}];
}
@@ -155,7 +445,6 @@
}];
}
- (void)setJavaEnabled:(BOOL) enabled {
[ThreadUtilities performOnMainThreadWaiting:NO block:^(){
@@ -330,6 +619,21 @@ Java_sun_lwawt_macosx_CMenuItem_nativeSetLabel
JNI_COCOA_EXIT(env);
}
/*
* Class: sun_lwawt_macosx_CMenuItem
* Method: nativeSetAcceleratorText
* Signature: (JLjava/lang/String;)V
*/
JNIEXPORT void JNICALL
Java_sun_lwawt_macosx_CMenuItem_nativeSetAcceleratorText
(JNIEnv *env, jobject peer, jlong menuItemObj, jstring acceleratorText)
{
JNI_COCOA_ENTER(env);
NSString *theText = JavaStringToNSString(env, acceleratorText);
[((CMenuItem *)jlong_to_ptr(menuItemObj)) setAcceleratorText:theText];
JNI_COCOA_EXIT(env);
}
/*
* Class: sun_lwawt_macosx_CMenuItem
* Method: nativeSetTooltip