ampersand magic

All underline handling in Tk standard dialogs is now done with "magic ampersands".

[As of which version?] [What TIP discusses this?] [The TCT did not require a TIP because it is internal to Tk - seems like it might/would impact widget writers or people developing extensions or modifications to Tk though - so in some people's opinion, it SHOULD have been TIPed.]

The experts in this are A/AK and DKF. The latter has explained, "'Ampersand magic' is a way of encoding the character to underline in a button, label, menu etc. in the string. It is easiest to see how it works by looking at an example:

  • If the string to show is 'Save As...' and the letter to underline is the 'A' in 'As...' then the ampersand-magic-ed string is 'Save &As...'
  • If the string to show is 'Quit' and the letter to underline is 'Q', then the ampersand-magic-ed string is '&Quit'.

IIRC, this sort of thing came originally from Windows, and it is rather a good idea because it makes it much easier to set up underlines for labels (where real ampersands tend to be fairly rare), particularly compared with counting characters to get to the right spot. I don't remember the exact details of Tk's implementation but I think it is done through extra procedures and is just used for strings for Tk's standard dialogs coming from the message catalog."


[Bring Anton's example code from "msgcat and shortcut keys" here.]


[But Perl/Tk uses '~' in place of '&'? What's going on?]

DKF writes: Tk went with '&' because it is the character used on Windows for this sort of thing. I've no idea why pTk uses '~' even though to me one character is as good as another.


RS thought the TCT had decided to use the Unicode "non-spacing underline" for this function? The advantage would have been that the hot key could be easily parsed out, while such a string would not have to be modified at all to display the underline - self fulfilling data prophecy...

A/AK: Unicode "non-spacing underline" seems to be a very good choice. It doesn't matter now, as all this underline machinery is internal to tk. But it would be fine for application programmers; and if(when) non-spacing underline is handled at the C level, I will be glad to remove all these ugly ampersand kludges from the script level.

DKF: At the moment, it only matters to people working on the core itself or its message catalogs. At such time as we add the unicode magic underline support to Tk (i.e. if it ever happens), we can easily convert the rest of the core to use it.

HaO: A reason might be that BWidget uses the ampersand.


Ro coughs up some code:

  # ::whatUnderlined --
  #
  #
  # Description
  #
  #   Finds out what character index to underline from a given label.
  #   The character after the "&" is the one to be underlined.
  #   The first character has index 0.
  #   Only one character can be underlined.
  #
  #   e.g.:  Given "Forma&t" it will return {5 Format}
  #          Given "Edit"    it will return {0 Edit}
  #
  #   Note: If the "&" character is missing, 0 is the index returned.
  #
  #
  # Arguments
  #
  #   s          The label to parse for single character underlining.
  #   
  #
  # Results
  #
  #   Returns two elements in list format, the first is the index of the 
  #   character to be underlined, the other is the label to be displayed.
  #
  #   If no character is to be underlined, the character index 
  #   returned is 0, and the label returned is the original given label.
  #
  #
  # Side Effects
  #
  #   none
  #
  #
  proc ::whatUnderlined {s} {
  
    #
    # Get the index of the "&" character.
    #
    set i [string first & $s]
  
    #
    # If there is no "&" character in the label, then return the
    # index 0 and the original given label.
    #
    if {[string equal "-1" $i]} {
      return [list 0 $s]
    } 
  
    #
    # If there is a "&" character, then return the index of the
    # explicitely determined character to underline and the display
    # formatted label.
    #  
    set s [string replace $s $i $i]
    return [list $i $s]
  
  }

MGS [2003/05/08] - I think the above code should return -1 instead of 0, if no & is found. I humbly offer my own shortened version:

proc amp {s} {
  set i [string first & $s]
  return [list $i [string replace $s $i $i]]
}

EKB [7 Aug 2005] - Taking MGS's code and extending it a bit so the result can easily be inserted into a menu item:

proc menumsg {m} {
  set s [msgcat::mc $m] ;# Or import msgcat::mc and use [mc $m]
  set i [string first & $s]
  set retval "-label \{[string replace $s $i $i]\}"
  if {$i != -1} {
     set retval "$retval -underline $i"
  }
  return $retval ;# RS added a $'s worth of improvement :)
}

Thanks, RS! And I fixed up a couple of other things as well...

Then if a string is in a message catalog, it can be put into a call to menu as:

eval menupath add command [menumsg mymsg] -command {...} ...

MGS [2005/08/08] I think this is slightly tidier (no worries about passing back -underline -1):

proc menulabel {string {char &}} {
  set s [msgcat::mc $string]
  set i [string first $char $s]
  return [list -label [string replace $s $i $i] -underline $i]
}

Then you can do:

menu .menu
. configure -menu .menu
set s "Save &As ..."
eval [list .menu add command] [menulabel $s]

HaO: If you have BWidget loaded anyway, you may use the internal ampersand-magic function as:

.menu add command {*}[MainFrame::_parse_name [mc %s]]

HoMi-(2010-10-14) What to do, if I want to have a labeltext like "Search & Destroy" with a underlined D ?

AMG: Try Search && &Destroy. That should do the trick.

MG offers

proc menu_label {str {char "&"}} {
  set curr -1
  while 1 {
    set curr [string first $char $str $curr]
    if { $curr == -1 } {
      break
    } elseif { [string index $str $curr+1] ne $char } {
      break
    } else {
      incr curr 2
    }
  }
  return [list -label [string replace $str $curr $curr] -underline $curr];
}

HoMi-(2010-10-15) Question to AMG: Where should I do your recommendation with the doubled ampersand? I´ve tried this with Tcl/Tk 8.5.6 and also with Ttk 8.5.6. The result in both cases was a menu label showing "Search && &Destroy". Thats not what I want!

I´ve also tried MGs offer.

If I use this with the input string Search && &Destroy it produces:

-label {Search && Destroy} -underline 10

Used within a .menu add command deklaration the D is underlined (that´s correct) but it shows a doubled ampersand (that´s wrong).

And if I use it with Search & &Destroy it returns:

-label {Search &Destroy} -underline 7

Used within a .menu add command deklaration this goes completly wrong. It underlines the space before the ampersand.

I think the following little change will do the job:

proc menu_label {str {char "&"}} {
  set curr -1
  while 1 {
    set curr [string first $char $str $curr]
    if { $curr == -1 } {
      break
    } elseif { [string index $str $curr+1] ne $char } {
      break
    } else {
      incr curr
      set str [string replace $str $curr $curr]
    }
  }
  return [list -label [string replace $str $curr $curr] -underline $curr];
}

This produces:

-label {Search & Destroy} -underline 9

MG Woops - that's what I get for not testing it first. Yeah, that's about what I meant.