AppleScript’s display dialog command opens a dialog window of a fixed width that is independent of the width of the lines of its dialog and default answer text. Any text lines that are too long to fit into their allotted space wrap onto the next line. As described in a post several years ago, this text-wrapping behavior can be avoided by widening the window’s button labels, which has the effect of widening the dialog window and its text fields. The handler described in the current post, called displayDialog, replicates the behavior of AppleScript’s display dialog command in all respects except that it automatically widens the dialog window sufficiently with the button label-widening technique to prevent the dialog and/or default answer text from wrapping. It utilizes information gathered from “reverse engineering” of the standard display dialog window to determine how much widening is needed for a given combination of dialog text, default answer text, and window buttons. It has been found to work successfully for macOS through 10.15 Catalina. Because the solution is user interface-dependent, the handler may need to be updated if Apple introduces significant formatting changes to the display dialog window in future macOS releases.
The handler’s input argument is an AppleScript record whose properties are analogous to the input parameters of AppleScript’s display dialog command, and its return value is identical to the display dialog command’s return value. One input record property, dialogText, is required and corresponds to the display dialog command’s unnamed direct parameter. The remaining input record properties are optional and correspond to the analogously named optional parameters of the display dialog command. Any optional property missing from the input record will be assigned a default value identical to the default value of the analogously named optional parameter of the display dialog command.
The input record properties are as follows:
Required property:
dialogText → corresponds to display dialog’s unnamed direct parameter
Optional properties:
defaultAnswer → corresponds to display dialog’s default answer parameter
hiddenAnswer → corresponds to display dialog’s hidden answer parameter
theButtons → corresponds to display dialog’s buttons parameter
defaultButton → corresponds to display dialog’s default button parameter
cancelButton → corresponds to display dialog’s cancel button parameter
withTitle → corresponds to display dialog’s with title parameter
withIcon → corresponds to display dialog’s with icon parameter
givingUpAfter → corresponds to display dialog’s giving up after parameter
Please refer to the StandardAdditions dictionary entry for the display dialog command for valid values for the input record properties.
Details of how the handler works are described in the handler’s comments. Some highlights are as follows:
- The handler performs input argument validation.
- The handler will catch and display any execution error in a dialog window.
- The font used for the dialog text in display dialog’s window is obtained from NSFont’s systemFontOfSize method with its parameter set to zero (i.e., the default size.)
- The font used in the dialog window’s default answer text is also NSFont’s system font, but its size varies with different window configurations in unpredictable ways. Since the size has never been observed to exceed that of the dialog text, the handler conservatively estimates its size to be that of the dialog text (i.e., system font of default size) so that the window will always be sufficiently widened to prevent the default answer text from wrapping. In practice, the window is almost always widened a little more than needed, except in the case where the hiddenAnswer input property is set to true, in which case the default answer consists of a string of black circle characters in the system font.
- The handler properly handles the additional widening of the dialog window resulting from (1) the inclusion of an icon in the window, and (2) the use of a certain window button configuration, specifically 3 buttons in which button 2 is the cancel button and button 3 is the default button.
- The window is never widened beyond the margins of the display screen (whose width is obtained from NSScreen’s mainScreen’s frame property), even if text lines end up being wrapped.
- The button labels are widened with leading and trailing pads of hair space characters (character id 8202), chosen because they are the thinnest Unicode space characters available and thus allow the most precise label widening. A middle dot character (character id 183) is added to either end of the widened label. Without this latter addition, spurious off-center positioning of the widened label within the button frame may at unpredictable times be observed, most commonly when there is only one window button. The addition of a visible character at either end of the widened label prevents this problem. Any visible character may be used. The middle dot character was chosen because of its inconspicuousness.
- Because there are far too many possible optional parameter configurations to allow the display dialog command to be hard-coded in the handler, instead a text representation of the command is generated and then executed with a run script command. The results of the run script command, which are identical to those of the display dialog command that it executes, are returned to the calling program, except that the clicked button’s original label, not the widened label, is returned.
One final point to mention is that this solution is based on my best attempt at “reverse engineering” AppleScript’s display dialog window. Obviously, not all possible combinations of dialog text, default answer text, and window buttons could be tested. Should any cases of handler failure be found, it would be helpful to learn of them so that the handler can be improved.
Examples of handler usage:
-- These "use" statements are required in the script containing the displayDialog handler
use framework "Foundation"
use scripting additions
-- The following example shows that the only required input record property is "dialogText"; default values will be supplied for all missing optional properties, just as is the case with the standard "display dialog" command
set myDialogText to "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do." & return & "Phasellus vestibulum lorem sed risus, integer eget aliquet eiusmod tempor praesent tristique." & return & "Ornare quam viverra orci sagittis, purus viverra accumsan in nisl nisi scelerisque eu, condimentum mattis pellentesque id nibh."
display dialog myDialogText
--> the dialog text's three sentences wrap onto seven lines in AppleScript's standard dialog window
displayDialog({dialogText:myDialogText})
--> the dialog text's three sentences don't wrap in the widened dialog window produced by the displayDialog handler
-- Just to have some fun and show that the handler can handle multibyte characters without problems
set myDialogText to "┇‡⋘⋙▉⫶·±⊕⊞§÷צôt↓➨┇‡⋘⋙▉⫶·±⊕⊞§÷צôt↓➨°½②①éⵘ≅─⌘⌥┇‡⋘⋙▉⫶┇‡⋘⋙▉⫶·±⊕⊞§÷צôt↓➨‡⋘⋙▉⫶·±⊕⊞§÷צôt↓➨┇‡⋘⋙▉⫶·±⊕⊞§÷צôt↓➨°½②①éⵘ≅─⌘⌥┇‡⋘⋙▉⫶┇‡⋘⋙▉⫶·±⊕⊞§÷צôt↓➨"
set myButtons to {"Quit", "Proceed"}
set myDefaultButton to "Proceed"
display dialog myDialogText buttons myButtons default button myDefaultButton
--> the dialog text wraps onto four lines in AppleScript's standard dialog window
displayDialog({dialogText:myDialogText, theButtons:myButtons, defaultButton:myDefaultButton})
--> the dialog text doesn't wrap in the widened dialog window produced by the displayDialog handler
-- An example with a default answer, a window icon, and the special three-button configuration that produces increased spacing between the first and second window buttons
set myDialogText to "Make any desired changes:"
set myDefaultAnswer to "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud."
set myButtons to {"Maybe", "No", "Yes"}
set myDefaultButton to "Yes"
set myCancelButton to "No"
set myTitle to "Silly Example"
set myIcon to caution
display dialog myDialogText default answer myDefaultAnswer buttons myButtons default button myDefaultButton cancel button myCancelButton with title myTitle with icon myIcon
--> the default answer wraps onto four lines in AppleScript's standard dialog window
displayDialog({dialogText:myDialogText, defaultAnswer:myDefaultAnswer, theButtons:myButtons, defaultButton:myDefaultButton, cancelButton:myCancelButton, withTitle:myTitle, withIcon:myIcon})
--> the default answer doesn't wrap in the widened dialog window produced by the displayDialog handler
on displayDialog(inputRecord)
-- ...place the handler code described below here...
end displayDialog
Here is the displayDialog handler:
use framework "Foundation"
use scripting additions
on displayDialog(inputRecord)
-- Replicates the behavior of AppleScript's "display dialog" command, except that the dialog window is widened as necessary to prevent any wrapping of dialog and/or default answer text lines
script main
-- "display dialog" window properties
property dialogTextFontAttribute : missing value -- the font used for the window's dialog text
property hiddenAnswerChar : missing value -- the character used to hide the default answer text
property hiddenAnswerCharWidth : missing value -- the pixel width of the hidden answer character
property nonwidenedDialogWindowWidth : missing value -- the pixel width of a standard, non-widened dialog window
property buttonToWindowWidth : missing value -- the pixel width of the space between each outermost button and the adjacent window margin
property buttonToButtonWidth : missing value -- the pixel width of the space between buttons
property buttonToLabelWidth : missing value -- the pixel width of the space between either end of a button label and the adjacent button margin
property iconAdjustmentWidth : missing value -- the pixel width of the extra space between the leftmost button and the adjacent window margin due to the presence of a window icon
property specialThreeButtonAdjustmentWidth : missing value -- the pixel width of the extra space between the first and second buttons when all of the following conditions apply: (A) there are three buttons, (B) the rightmost button is the default button, and (C) the middle button is the cancel button
-- Other properties
property hairSpaceChar : missing value -- the hair space character (character id 8202)
property hairSpaceCharWidth : missing value -- the pixel width of the hair space character
property middleDotChar : missing value -- the middle dot character (character id 183)
property middleDotCharWidth : missing value -- the pixel width of the middle dot character
property screenWidth : missing value -- the pixel width of the display screen
-- Utility handlers
on getStringWidth(theString)
-- Returns the pixel width of a string in the font used for the "display dialog" window's dialog text = NSFont's system font of default size
return ((current application's NSString's stringWithString:theString)'s sizeWithAttributes:(my dialogTextFontAttribute))'s width as real
end getStringWidth
on replicateCharacter(theChar, nReps)
-- Returns a string consisting of a character replicated a specified number of times
return ((current application's NSString's stringWithString:"")'s stringByPaddingToLength:nReps withString:theChar startingAtIndex:0) as text
end replicateCharacter
on textRep(theValue)
-- Returns the text representation of any AppleScript value
try
|| of {theValue}
on error m
try
set {tid, AppleScript's text item delimiters} to {AppleScript's text item delimiters, "{"}
set m to m's text items 2 thru -1 as text
set AppleScript's text item delimiters to "}"
set valueAsText to m's text items 1 thru -2 as text
set AppleScript's text item delimiters to tid
on error
set AppleScript's text item delimiters to tid
error "Problem with utility handler textRep:" & return & return & "Could not get the text representation of the input value."
end try
end try
return valueAsText
end textRep
on widenButtonLabel(theLabel, labelWidth, targetWidth)
-- Widens a button label to a target pixel width using left and right hair space character pads along with a middle dot character at either end of the widened label, the latter to prevent spurious off-center positioning of the label within the button
-- Notes:
-- The hair space character is used for padding because it is the narrowest Unicode character available and thus allows the most precise widening
-- Any visible character may be used at the outer margins of the label; the middle dot character was chosen because of its inconspicuousness
set nJustifyingHairSpaceChars to ((targetWidth - labelWidth - 2 * (my middleDotCharWidth)) / (my hairSpaceCharWidth)) as integer
set justifiedString to theLabel
if nJustifyingHairSpaceChars > 0 then
set nLeftHairSpaceChars to nJustifyingHairSpaceChars div 2
set nRightHairSpaceChars to nJustifyingHairSpaceChars - nLeftHairSpaceChars
set justifiedString to (my middleDotChar) & (my replicateCharacter(my hairSpaceChar, nLeftHairSpaceChars)) & theLabel & (my replicateCharacter(my hairSpaceChar, nRightHairSpaceChars)) & (my middleDotChar)
end if
return justifiedString
end widenButtonLabel
on run
-- Assign constant values to the "display dialog" window properties
set my dialogTextFontAttribute to current application's NSDictionary's dictionaryWithObject:(current application's NSFont's systemFontOfSize:0) forKey:(current application's NSFontAttributeName)
set my hiddenAnswerChar to character id 9679 -- 9679 = the black circle character used by the "display dialog" window to hide the default answer's text when that option is specified
set my hiddenAnswerCharWidth to my getStringWidth(my hiddenAnswerChar)
set my nonwidenedDialogWindowWidth to 420
set my buttonToWindowWidth to 22
set my buttonToButtonWidth to 12
set my buttonToLabelWidth to 12
set my iconAdjustmentWidth to 78
set my specialThreeButtonAdjustmentWidth to 26
-- Assign constant values to the other properties
set my hairSpaceChar to character id 8202
set my hairSpaceCharWidth to my getStringWidth(my hairSpaceChar)
set my middleDotChar to character id 183
set my middleDotCharWidth to my getStringWidth(my middleDotChar)
set my screenWidth to current application's NSScreen's mainScreen()'s frame()'s second item's first item as real
-- Validate and process the input record
try
tell (inputRecord & {defaultAnswer:missing value, hiddenAnswer:missing value, theButtons:missing value, defaultButton:missing value, cancelButton:missing value, withTitle:missing value, withIcon:missing value, givingUpAfter:missing value})
if length ≠ 9 then error
set {dialogText, defaultAnswer, hiddenAnswer, theButtons, defaultButton, cancelButton, withTitle, withIcon, givingUpAfter} to {its dialogText, its defaultAnswer, its hiddenAnswer, its theButtons, its defaultButton, its cancelButton, its withTitle, its withIcon, its givingUpAfter}
end tell
on error
error "The handler input argument must be a record with the following properties:" & return & return & "Required property (equivalent to \"display dialog\"'s direct parameter):" & return & return & tab & "dialogText" & return & return & "Optional properties (equivalent to \"display dialog\"'s analogously named optional parameters):" & return & return & tab & "defaultAnswer" & return & tab & "hiddenAnswer" & return & tab & "theButtons" & return & tab & "defaultButton" & return & tab & "cancelButton" & return & tab & "withTitle" & return & tab & "withIcon" & return & tab & "givingUpAfter"
end try
-- Validate and process the input record properties
if dialogText's class ≠ text then error "The input property dialogText must be a text string."
tell defaultAnswer to if (it ≠ missing value) and (its class ≠ text) then error "The input property defaultAnswer must be the missing value or a text string."
tell hiddenAnswer to if (it ≠ missing value) and (its class ≠ boolean) then error "The input property hiddenAnswer must be the missing value or a boolean true or false value."
tell theButtons
if it = missing value then
set {theButtons, defaultButton, cancelButton} to {{"Cancel", "OK"}, 2, 1}
else if its class ≠ list then
set theButtons to {it}
end if
end tell
tell theButtons
try
if (length < 1) or (length > 3) then error
repeat with i from 1 to length
set currButton to item i
if currButton's class ≠ text then error
if currButton = defaultButton then set defaultButton to i
if currButton = cancelButton then set cancelButton to i
end repeat
set nButtons to length
on error
error "The input property theButtons must be the missing value, a text string (i.e., one button), or a list of one to three text strings."
end try
end tell
try
tell defaultButton to if not ((it = missing value) or ((its class = integer) and (it ≥ 1) and (it ≤ nButtons))) then error
on error
error "The button specified by the input property defaultButton does not exist."
end try
try
tell cancelButton to if not ((it = missing value) or ((its class = integer) and (it ≥ 1) and (it ≤ nButtons))) then error
on error
error "The button specified by the input property cancelButton does not exist."
end try
tell withTitle to if (it ≠ missing value) and (its class ≠ text) then error "The input property withTitle must be the missing value or a text string."
tell withIcon
try
if not (({it} is in {missing value, stop, note, caution, 0, 1, 2}) or ((its class = alias) and ((it as text) ends with ".icns"))) then error
on error
error "The input property withTitle must be one of the following values:" & return & tab & "missing value" & return & tab & "stop or 0" & return & tab & "note or 1" & return & tab & "caution or 2" & return & tab & "AppleScript alias to a \".icns\" file"
end try
end tell
tell givingUpAfter
try
if (it ≠ missing value) and (its class ≠ integer) then set givingUpAfter to it as integer
on error
error "The input property givingUpAfter can't be transformed into an integer."
end try
end tell
-- Get the maximum pixel width of the dialog and default answer text lines; in the case of a hidden default answer, this value consists of the pixel width of a string of black circle characters (character id 9679) replicated to the number of characters in the default answer's text
set maxTextWidth to 0
repeat with currLine in (get dialogText's paragraphs)
tell my getStringWidth(currLine's contents) to if it > maxTextWidth then set maxTextWidth to it
end repeat
tell defaultAnswer
if it ≠ missing value then
if hiddenAnswer = true then
tell length * (my hiddenAnswerCharWidth) to if it > maxTextWidth then set maxTextWidth to it
else
repeat with currLine in (get its paragraphs)
tell my getStringWidth(currLine's contents) to if it > maxTextWidth then set maxTextWidth to it
end repeat
end if
end if
end tell
-- If an icon is to be displayed in the "display dialog" window, account for the increased width between the leftmost button and the left window margin
set buttonToWindowAdjustmentWidth to 0
if withIcon ≠ missing value then set buttonToWindowAdjustmentWidth to my iconAdjustmentWidth
-- Determine if the "display dialog" window needs to be widened to prevent wrapping of dialog and/or default answer text
set windowNeedsWidening to maxTextWidth > ((my nonwidenedDialogWindowWidth) - 2 * (my buttonToWindowWidth) - buttonToWindowAdjustmentWidth)
-- If the window needs to be widened, do so by widening the window's button labels
set modifiedButtons to theButtons
if windowNeedsWidening then
-- Don't widen the "display dialog" window beyond the display screen's width; this is accomplished by setting an upper limit to the maximum text width value
tell ((my screenWidth) - 2 * (my buttonToWindowWidth) - buttonToWindowAdjustmentWidth) to if maxTextWidth > it then set maxTextWidth to it
-- Account for an increased width between the first and second buttons if all of the following conditions apply: (A) there are three buttons, (B) the rightmost button is the default button, and (C) the middle button is the cancel button
set buttonToButtonAdjustmentWidth to 0
if (defaultButton = 3) and (cancelButton = 2) then set buttonToButtonAdjustmentWidth to my specialThreeButtonAdjustmentWidth
-- Calculate the target width for button labels that will widen the "display dialog" window just enough to prevent text wrapping
set targetWidth to (maxTextWidth + (my buttonToButtonWidth) - buttonToButtonAdjustmentWidth) / nButtons - (my buttonToButtonWidth) - 2 * (my buttonToLabelWidth)
-- Widen each button label to the target width
set modifiedButtons to {}
repeat with currButton in theButtons
set end of modifiedButtons to my widenButtonLabel(currButton's contents, my getStringWidth(currButton's contents), targetWidth)
end repeat
end if
-- Create a text representation of the "display dialog" command, incorporating any optional parameters specified in the input record
set theCommand to "activate" & return & "display dialog " & my textRep(dialogText) & " buttons " & my textRep(modifiedButtons)
if defaultButton ≠ missing value then set theCommand to theCommand & " default button " & my textRep(defaultButton)
if cancelButton ≠ missing value then set theCommand to theCommand & " cancel button " & my textRep(cancelButton)
if defaultAnswer ≠ missing value then set theCommand to theCommand & " default answer " & my textRep(defaultAnswer)
if hiddenAnswer ≠ missing value then set theCommand to theCommand & " hidden answer " & my textRep(hiddenAnswer)
if withTitle ≠ missing value then set theCommand to theCommand & " with title " & my textRep(withTitle)
if withIcon ≠ missing value then set theCommand to theCommand & " with icon " & my textRep(withIcon)
if givingUpAfter ≠ missing value then set theCommand to theCommand & " giving up after " & my textRep(givingUpAfter)
-- Execute the text representation of the "display dialog" command with a "run script" command, and capture the returned values
set returnedValues to (run script theCommand)
-- Set the "button returned" return value to the original button name, not the widened button name
if windowNeedsWidening then
set buttonReturned to returnedValues's button returned
repeat with i from 1 to modifiedButtons's length
if modifiedButtons's item i = buttonReturned then
set returnedValues's button returned to theButtons's item i
exit repeat
end if
end repeat
end if
-- Return the "display dialog" command's returned values to the calling program
return returnedValues
end run
end script
-- Wrap the entire executed handler code in a try block so that any execution error may be captured and displayed
try
run main
on error m number n
if n = -128 then error number -128
if n ≠ -2700 then set m to "(" & n & ") " & m
tell application "System Events"
activate
display dialog ("Problem with handler displayDialog:" & return & return & m) buttons "OK" default button "OK" cancel button "OK"
end tell
end try
end displayDialog
Edit note: Two changes were made to the “Examples of handler usage” section from the original post: (1) the “Lorem ipsum” example was lengthened to three sentences to demonstrate the helpfulness of opening the dialog window without text wrapping, and (2) the multibyte character string example was modified by removing certain multibyte characters that were not being displayed properly by the MacScripter webpage generator.