From: "Mattias Engdegård" <mattiase@acm.org>
To: 45502@debbugs.gnu.org
Subject: bug#45502: [PATCH] Prettier key bindings in NS menu entries
Date: Mon, 28 Dec 2020 15:23:25 +0100 [thread overview]
Message-ID: <A879EF2E-3B21-4C73-9253-02083031FCCF@acm.org> (raw)
[-- Attachment #1: Type: text/plain, Size: 587 bytes --]
The NS port shows key bindings in a rather cluttered way, with the key in brackets directly after the menu entry. The Mac port of Emacs is much neater with the bindings all aligned at a common tab position. We could do the same, but having done some experiments I actually prefer a right-alignment of the keys. Proof-of-concept patch attached.
The alignment is made by padding with spaces, and then with hair spaces for extra precision; the result is not perfect but probably better than what we have now. If I get some time, I might do an experiment with more precise formatting.
[-- Attachment #2: right-justify-keys.diff --]
[-- Type: application/octet-stream, Size: 6382 bytes --]
diff --git a/src/nsmenu.m b/src/nsmenu.m
index 3f0cd0c6ed..6c96f5f123 100644
--- a/src/nsmenu.m
+++ b/src/nsmenu.m
@@ -450,32 +450,14 @@ - (BOOL)performKeyEquivalent: (NSEvent *)theEvent
}
-/* Parse a widget_value's key rep (examples: 's-p', 's-S', '(C-x C-s)', '<f13>')
- into an accelerator string. We are only able to display a single character
- for an accelerator, together with an optional modifier combination. (Under
- Carbon more control was possible, but in Cocoa multi-char strings passed to
- NSMenuItem get ignored. For now we try to display a super-single letter
- combo, and return the others as strings to be appended to the item title.
- (This is signaled by setting keyEquivModMask to 0 for now.) */
--(NSString *)parseKeyEquiv: (const char *)key
+static const char *
+skipspc (const char *s)
{
- const char *tpos = key;
- keyEquivModMask = NSEventModifierFlagCommand;
-
- if (!key || !*key)
- return @"";
-
- while (*tpos == ' ' || *tpos == '(')
- tpos++;
- if ((*tpos == 's') && (*(tpos+1) == '-'))
- {
- return [NSString stringWithFormat: @"%c", tpos[2]];
- }
- keyEquivModMask = 0; /* signal */
- return [NSString stringWithUTF8String: tpos];
+ while (*s == ' ')
+ s++;
+ return s;
}
-
- (NSMenuItem *)addItemWithWidgetValue: (void *)wvptr
{
NSMenuItem *item;
@@ -488,31 +470,20 @@ - (NSMenuItem *)addItemWithWidgetValue: (void *)wvptr
}
else
{
- NSString *title, *keyEq;
+ NSString *title;
title = [NSString stringWithUTF8String: wv->name];
if (title == nil)
title = @"< ? >"; /* (get out in the open so we know about it) */
- keyEq = [self parseKeyEquiv: wv->key];
-#ifdef NS_IMPL_COCOA
- /* macOS mangles modifier strings longer than one character. */
- if (keyEquivModMask == 0)
- {
- title = [title stringByAppendingFormat: @" (%@)", keyEq];
- item = [self addItemWithTitle: (NSString *)title
- action: @selector (menuDown:)
- keyEquivalent: @""];
- }
- else
- {
-#endif
- item = [self addItemWithTitle: (NSString *)title
- action: @selector (menuDown:)
- keyEquivalent: keyEq];
-#ifdef NS_IMPL_COCOA
- }
-#endif
- [item setKeyEquivalentModifierMask: keyEquivModMask];
+ /* Cocoa only permits a single key (with modifiers) as
+ keyEquivalent, so we stick with the standard Emacs notation
+ for all key bindings for the sake of uniformity. */
+ if (wv->key)
+ title = [title stringByAppendingString:
+ [NSString stringWithUTF8String: skipspc (wv->key)]];
+ item = [self addItemWithTitle: (NSString *)title
+ action: @selector (menuDown:)
+ keyEquivalent: @""];
[item setEnabled: wv->enabled];
@@ -557,14 +528,69 @@ -(void)removeAllItems
- (void)fillWithWidgetValue: (void *)wvptr
{
- widget_value *wv = (widget_value *)wvptr;
+ widget_value *first_wv = (widget_value *)wvptr;
+ NSFont *menuFont = [NSFont menuFontOfSize:0];
+ NSDictionary <NSString *, id> *attributes =
+ [NSDictionary dictionaryWithObject:menuFont forKey:NSFontAttributeName];
+ NSSize spaceSize = [@" " sizeWithAttributes:attributes];
+ CGFloat spaceWidth = spaceSize.width;
+ const char hair_str[] = "\u200a"; // HAIR SPACE
+ int hair_bytes = sizeof hair_str - 1;
+ NSSize hairSize = [[NSString stringWithUTF8String: hair_str]
+ sizeWithAttributes:attributes];
+ CGFloat hairWidth = spaceSize.width;
+ CGFloat maxNameWidth = 0;
+ CGFloat maxKeyWidth = 0;
+
+ /* Determine maximum width of menu items. */
+ for (widget_value *wv = first_wv; wv != NULL; wv = wv->next)
+ if (!menu_separator_name_p (wv->name))
+ {
+ NSString *name = [NSString stringWithUTF8String: wv->name];
+ NSSize nameSize = [name sizeWithAttributes: attributes];
+ CGFloat nameWidth = nameSize.width;
+ maxNameWidth = MAX(maxNameWidth, nameWidth);
+ if (wv->key)
+ {
+ NSString *key = [NSString stringWithUTF8String: skipspc (wv->key)];
+ NSSize keySize = [key sizeWithAttributes: attributes];
+ CGFloat keyWidth = keySize.width;
+ maxKeyWidth = MAX(maxKeyWidth, keyWidth);
+ }
+ }
+ CGFloat maxWidth = maxNameWidth + maxKeyWidth;
/* clear existing contents */
[self removeAllItems];
/* add new contents */
- for (; wv != NULL; wv = wv->next)
+ for (widget_value *wv = first_wv; wv != NULL; wv = wv->next)
{
+ if (wv->key && !menu_separator_name_p (wv->name))
+ {
+ /* Modify name to include padding to right-justify key binding. */
+ NSString *name = [NSString stringWithUTF8String: wv->name];
+ NSSize nameSize = [name sizeWithAttributes: attributes];
+ CGFloat nameWidth = nameSize.width;
+ NSString *key = [NSString stringWithUTF8String: skipspc (wv->key)];
+ NSSize keySize = [key sizeWithAttributes: attributes];
+ CGFloat keyWidth = keySize.width;
+ CGFloat padWidth = maxWidth - (nameWidth + keyWidth);
+ /* Use plain spaces as far as possible, and hair spaces for
+ the rest. */
+ int extra_spaces = 4;
+ int nspaces = padWidth / spaceWidth + extra_spaces;
+ int nhairs = fmod(padWidth, spaceWidth) / hairWidth + 0.5;
+ int name_len = strlen (wv->name);
+ Lisp_Object s = make_uninit_string (name_len + nspaces
+ + nhairs * hair_bytes);
+ memcpy (SSDATA (s), wv->name, name_len);
+ memset (SDATA (s) + name_len, ' ', nspaces);
+ for (int i = 0; i < nhairs; i++)
+ memcpy (SDATA (s) + name_len + nspaces + i * hair_bytes, hair_str,
+ hair_bytes);
+ wv->name = SSDATA (s);
+ }
NSMenuItem *item = [self addItemWithWidgetValue: wv];
if (wv->contents)
diff --git a/src/nsterm.h b/src/nsterm.h
index b7b4d3b047..c4873b6082 100644
--- a/src/nsterm.h
+++ b/src/nsterm.h
@@ -515,7 +515,6 @@ #define NS_DRAW_TO_BUFFER 1
@interface EmacsMenu : NSMenu <NSMenuDelegate>
{
- unsigned long keyEquivModMask;
BOOL needsUpdate;
}
next reply other threads:[~2020-12-28 14:23 UTC|newest]
Thread overview: 16+ messages / expand[flat|nested] mbox.gz Atom feed top
2020-12-28 14:23 Mattias Engdegård [this message]
2020-12-28 18:36 ` bug#45502: [PATCH] Prettier key bindings in NS menu entries Alan Third
2020-12-29 12:02 ` Mattias Engdegård
2020-12-29 13:53 ` Alan Third
2020-12-29 14:41 ` Mattias Engdegård
2020-12-29 15:50 ` Alan Third
2020-12-29 17:34 ` Mattias Engdegård
2020-12-29 21:24 ` Alan Third
2020-12-29 22:53 ` Mattias Engdegård
2020-12-29 23:49 ` Alan Third
2020-12-30 12:19 ` Mattias Engdegård
2020-12-30 12:46 ` Alan Third
2020-12-30 13:09 ` Alan Third
2020-12-30 15:53 ` Mattias Engdegård
2020-12-30 13:12 ` Mattias Engdegård
2020-12-28 22:46 ` Unknown
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
List information: https://www.gnu.org/software/emacs/
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=A879EF2E-3B21-4C73-9253-02083031FCCF@acm.org \
--to=mattiase@acm.org \
--cc=45502@debbugs.gnu.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
Code repositories for project(s) associated with this public inbox
https://git.savannah.gnu.org/cgit/emacs.git
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).