Wonky Objective C pointer problems with a handy modeless window routine in Applescript

I thought I would post this under a new thread, but creating a modeless window is an old topic. KniazidisR posted a great solution about three years ago and then disappeared from the board. I grew dependent on it until it started screwing up my file specs due to a well known Applescript bug, so I encapsulated the AppKit/Foundation stuff in a subscript and yanked some of the pointers (“missing value”) into properties.

That works great for opening and closing the window (but don’t turn the pointers into globals or everything really blows up).

Then I got ambitious. I want to update the text contents periodically without thrashing memory by closing and opening a new window. I managed to do this in the first code stub, which minimally alters KniazidisR’s work. However, once I started elaborating on it in various ways, the behavior became highly irregular. Note where and how the additions trigger failures based on my comments. Perhaps it’s just my older version of Applescript, but I suspect some kind of memory reliability problem.

Below that, you can find my subscript work that functions well in a broader toolkit library. But once I tried to extend it with an update feature, that worked only very, very rarely.

Bonus points if anybody can help me toss RTF text into the window or easily add a scroll bar.

use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "AppKit"

property givingUpAfter : 2 -- seconds
property wController : missing value -- outlet equivalent in AsObjC

-- with or without these next three items, it works the same
property aView : missing value
property aWin : missing value
property newView : missing value

set notificationTitle to "" -- no title
set {aWidth, aHeight} to {1200, 600}
set notificationText to "Happy New Year!!!"

--set notificationText to current application's NSString's stringWithString:notificationText
set paramObj to {aWidth, aHeight, notificationTitle, notificationText}
my performSelectorOnMainThread:"displayNotification:" withObject:(paramObj) waitUntilDone:true
newView's setString:"Message 2-------" -- < This totally works!! (and sometimes it doesn't)
delay givingUpAfter

set shortlist1 to "▶︎Time 1" & return & " Time 2" & return & " Time3"
set shortlist2 to " Time 1" & return & "▶︎Time 2" & return & " Time3"
set shortlist3 to " Time 1" & return & " Time 2" & return & "▶︎Time3"
--newView's setString:shortlist1 -- doesn't work

-- uncomment this block and the once successful "Message 2" change fails
--UpdateDisplayNotification(shortlist1)
--delay givingUpAfter
--UpdateDisplayNotification(shortlist2)
--delay givingUpAfter
--UpdateDisplayNotification(shortlist3)
--delay givingUpAfter

-- uncomment this block and the once successful "Message 2" change fails
--newView's setString:shortlist1
--delay givingUpAfter
--newView's setString:shortlist2
--delay givingUpAfter
--newView's setString:shortlist3
--delay givingUpAfter

-- commenting out this next line and using ClosedisplayNotification(wController) instead
-- suddenly causes unreliable behavior in the previous newView's setString:"Message 2" line
my wController's |close|()
--ClosedisplayNotification(wController)

on ClosedisplayNotification(wController)
	my wController's |close|()
end ClosedisplayNotification

on UpdateDisplayNotification(newText)
	newView's setString:newText
end UpdateDisplayNotification

on displayNotification:paramObj
	copy paramObj to {aWidth, aHeight, aTitle, notificationText}
	set aColor to current application's NSColor's colorWithDeviceRed:1 green:1 blue:1 alpha:1 -- white
	set aView to current application's NSTextView's alloc()'s initWithFrame:(current application's NSMakeRect(0, 0, aWidth, aHeight))
	aView's setRichText:true
	aView's useAllLigatures:true
	aView's setTextColor:(current application's NSColor's blackColor()) --
	aView's setBackgroundColor:aColor
	aView's setEditable:false
	aView's setFont:(current application's NSFont's fontWithName:"Courier" |size|:14)
	set aWin to makeWinWithView(aView, aWidth, aHeight, aTitle, 1.0)
	aView's setString:notificationText
	set my wController to current application's NSWindowController's alloc()
	my (wController's initWithWindow:aWin)
	my (wController's showWindow:me)
	delay givingUpAfter
	copy aView to newView
end displayNotification:

on makeWinWithView(aView, aWinWidth, aWinHeight, aTitle, alphaV)
	set aScreen to current application's NSScreen's mainScreen()
	set aFrame to {{0, 0}, {aWinWidth, aWinHeight}}
	set aBacking to current application's NSTitledWindowMask
	set aDefer to current application's NSBackingStoreBuffered
	set aWin to current application's NSWindow's alloc()
	(aWin's initWithContentRect:aFrame styleMask:aBacking backing:aDefer defer:false screen:aScreen)
	aWin's setTitle:aTitle
	aWin's setDelegate:me
	aWin's setDisplaysWhenScreenProfileChanges:true
	aWin's setHasShadow:true
	aWin's setIgnoresMouseEvents:false
	aWin's setLevel:(current application's NSNormalWindowLevel)
	aWin's setOpaque:false
	aWin's setAlphaValue:alphaV --append
	aWin's setReleasedWhenClosed:true
	aWin's |center|()
	aWin's setContentView:aView
	return aWin
end makeWinWithView


------------------------------- Second version in a handy encapsulating subscript

---- opens and closes a modeless window but can't consistently update its contents (only rarely works)
use AppleScript version "2.4"
use scripting additions

property aController : missing value -- outlet equivalent in AsObjC
property theView : missing value
property theWin : missing value
property StackView : missing value

set shortlist1 to "▶︎Time 1" & return & " Time 2" & return & " Time3"
set shortlist2 to " Time 1" & return & "▶︎Time 2" & return & " Time3"
set shortlist3 to " Time 1" & return & " Time 2" & return & "▶︎Time3"

-- these work fine and illustrate what I'm trying to do in the next block
set {aController, theView, theWin} to DisplayNotificationWindow("Playing Clipboard Times", shortlist1, 650, 450, aController, theView, theWin)
delay 1
closeNotification(aController, theView, theWin)
set {aController, theView, theWin} to DisplayNotificationWindow("Playing Clipboard Times", shortlist2, 650, 450, aController, theView, theWin)
delay 1
closeNotification(aController, theView, theWin)
set {aController, theView, theWin} to DisplayNotificationWindow("Playing Clipboard Times", shortlist3, 650, 450, aController, theView, theWin)
delay 1
closeNotification(aController, theView, theWin)

try
	set {aController, theView, theWin} to DisplayNotificationWindow("Playing Clipboard Times", shortlist1, 650, 450, aController, theView, theWin)
	reportlog()
	delay 1
	UpdateNotificationWindow(shortlist2, aController, theView, theWin)
	--DisplayNotificationWindow("", shortlist2, 650, 450, aController, theView, theWin)
	delay 1
	UpdateNotificationWindow(shortlist3, aController, theView, theWin)
	delay 1
	closeNotification(aController, theView, theWin)
on error errorMssg number errorNumber
	log "***Bomb***" & errorMssg & "   #" & errorNumber
	closeNotification(aController, theView, theWin)
end try

-- handlers using Foundation and Appkit must be individually wrapped like this because of a bug that damages file references and script-loading in the main code
-- this handler combines two code stubs on modeless message windows and script wrappers that evade nasty ' Use framework "Foundation" ' bugs
-- Now I just need a scroll bar and some .rtf output and I'll be cooking with real gas, as they say...
on DisplayNotificationWindow(NTitle, nText, Nx, NY, CloseController, someView, someWin) -- was aView when it worked (but that only happened once)
	script theScript
		property parent : a reference to current application
		property wController : missing value -- outlet equivalent in AsObjC
		property aView : missing value
		property aWin : missing value
		property notificationTitle : "" -- only needed for displayNotification
		property notificationText : "" -- only needed for displayNotification
		property aWidth : 300 -- only needed for displayNotification
		property aHeight : 60 -- only needed for displayNotification
		use framework "Foundation"
		use framework "AppKit"
		on DisplayNotificationWindow(NTitle, nText, Nx, NY, CloseController, someView, someWin)
			if (NTitle is "") and (nText is "") then
				tell me to CloseController's |close|() -- finding this exact construction drove me batty
				return
			else if NTitle is "" then
              -- log checks confirm the pointers someWin & someView are correct across calls
				someWin's setContentView:someView
				someView's setString:nText -- This combination worked once but then never again		
				return
			else
				set {notificationTitle, notificationText, aWidth, aHeight} to {NTitle, nText, Nx, NY}
				my performSelectorOnMainThread:"displayNotification:" withObject:({aWidth, aHeight, notificationTitle, current application's NSString's stringWithString:notificationText}) waitUntilDone:true
			end if
			return {wController, aView, aWin}
		end DisplayNotificationWindow
		
		on displayNotification:paramObj
			copy paramObj to {aWidth, aHeight, aTitle, notificationText}
			set aColor to current application's NSColor's colorWithDeviceRed:1 green:1 blue:1 alpha:1 -- white
			set aView to current application's NSTextView's alloc()'s initWithFrame:(current application's NSMakeRect(0, 0, aWidth, aHeight))
			aView's setRichText:true
			aView's useAllLigatures:true
			aView's setTextColor:(current application's NSColor's blackColor()) -- could be cyanColor, whiteColor, etc...
			aView's setBackgroundColor:aColor
			aView's setEditable:false
			aView's setFont:(current application's NSFont's fontWithName:"Courier" |size|:14)
			set aScreen to current application's NSScreen's mainScreen()
			set aFrame to {{0, 0}, {aWidth, aHeight}}
			set aBacking to current application's NSTitledWindowMask
			set aDefer to current application's NSBackingStoreBuffered
			set aWin to current application's NSWindow's alloc()
			(aWin's initWithContentRect:aFrame styleMask:aBacking backing:aDefer defer:false screen:aScreen)
			aWin's setTitle:aTitle
			aWin's setDelegate:me
			aWin's setDisplaysWhenScreenProfileChanges:true
			aWin's setHasShadow:true
			aWin's setIgnoresMouseEvents:false
			aWin's setLevel:(current application's NSNormalWindowLevel)
			aWin's setOpaque:false
			aWin's setAlphaValue:0.9 -- append to 1.0 for opaque
			aWin's setReleasedWhenClosed:true
			aWin's |center|()
			aWin's setContentView:aView
			aView's setString:notificationText -- < I need to alter this field without closing/opening a new window
			set my wController to current application's NSWindowController's alloc()
			my (wController's initWithWindow:aWin)
			my (wController's showWindow:me)
			delay 1
			set astring to current application's NSString's stringWithString:(reverse of (characters of notificationText) as string) -- nada
			--aWin's setContentView:aView
			aView's setString:"Message 2" -- this fails here but it works in a more primitive setting
			copy aView to StackView -- didn't help either
		end displayNotification:
		
	end script
	return theScript's DisplayNotificationWindow(NTitle, nText, Nx, NY, CloseController, someView, someWin)
end DisplayNotificationWindow

on UpdateNotificationWindow(newText, theController, theView, theWin)
	my DisplayNotificationWindow("", newText, 0, 0, theController, theView, theWin)
end UpdateNotificationWindow

on closeNotification(theController, theView, theWin)
	my DisplayNotificationWindow("", "", 0, 0, theController, theView, theWin)
end closeNotification

The first half of your script works for me in Tahoe, so you might need to add some details about what you are actually doing. Pasting RTF will work if you make the textView editable, although you can only paste where there is already text.

Thanks. Does the first half work when you put the blocks commented out back to work?

Does the second half not work at all?

I’m trying to swap out the text field in an existing window data structure to update a modeless window progress report without having to close the window and open a new one. I’m presently doing exactly that in a larger project, but I’d rather be a little more memory efficient if I could.

I uncommented the first couple of blocks, but didn’t try the part with the window controller. Is there a particular reason for wanting to use a window controller? How are you running this, in a stay-open application?

I’m using a windows controller because that’s how the code stub was offered three years ago and I don’t know Objective C.

I’m testing this currently in the interpreter with code that will eventually be a stand-alone app, but it doesn’t need to run in the background. Though, as a library function, I’d want it to do that safely. There’s been some interest in this kind of function over the years so I was thinking of releasing this handler and a few others into the wild. I recently cracked a few problems with iTunes, like how to insert delays between playlist entries.

If you are running from a script editor, it might be an issue with what is running (or not) on the main thread. I initially ran your example in the foreground in Script Editor, but for Script Debugger, only the UI creation stuff is performed on the main thread. I started getting weird results depending on where and how the text was updated, but using the main thread worked reliably.

I rewrote your example using some general-purpose UI handlers to split things up a bit more to (hopefully) make it easier to mix-and-match. UI updates are grouped so that they are run on the main thread, and there is also additional setup for a scrolling textView. Note that although the script returns immediately, the UI changes continue since the window and the delayed performSelector methods are kept by the editor.

use AppleScript version "2.5" -- Sierra (10.12) or later for new enumerations
use framework "Foundation"
use scripting additions

# UI item outlets
property mainWindow : missing value
property textView : missing value
property scrollView : missing value

# script properties
property outcome : missing value -- result (if needed)
property failure : missing value -- error record with keys {errorMessage, errorNumber}

on run -- example can be run as app and from a script editor 
   try
      if current application's NSThread's isMainThread() as boolean then
         doStuff()
      else
         my performSelectorOnMainThread:"doStuff" withObject:(missing value) waitUntilDone:true
      end if
      if failure is not missing value then error
      log "done"
      my performSelectorOnMainThread:"update:" withObject:"So far, so good..." waitUntilDone:true
   on error errmess number errnum
      if failure is missing value then -- use passed arguments
         display alert "Example Script Error " & errnum message quoted form of errmess
      else -- use keys from the failure record
         display alert "Example Script Error " & failure's errorNumber message quoted form of failure's errorMessage
      end if
   end try
end run

# UI stuff needs to be performed on the main thread.
# Any error is placed in a property, since `performSelectorOnMainThread` doesn't return anything.
on doStuff()
   try
      initialize() -- set up the UI
      set loremText to "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque vestibulum venenatis velit, non commodo diam pretium sed. Etiam viverra erat a lacus molestie id euismod magna lacinia. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Vestibulum ac augue magna, eu pharetra leo. Donec tortor tortor, tristique in ornare nec, feugiat vel justo. Nunc iaculis interdum pellentesque. Quisque vel rutrum nibh. Phasellus malesuada ipsum quis diam ullamcorper rutrum. Nullam tincidunt porta ante, in aliquet odio molestie eget. Donec mollis, nibh euismod pulvinar fermentum, magna nunc consectetur risus, id dictum odio leo non velit. Vestibulum vitae nunc pulvinar augue commodo sollicitudin."
      my performSelector:"update:" withObject:("▶︎Testing 1" & return & " Testing 2" & return & " Testing 3") afterDelay:2
      my performSelector:"update:" withObject:(" Testing 1" & return & "▶︎Testing 2" & return & " Testing 3") afterDelay:4
      my performSelector:"update:" withObject:(" Testing 1" & return & " Testing 2" & return & "▶︎Testing 3") afterDelay:6
      my performSelector:"update:" withObject:loremText afterDelay:8
      mainWindow's performSelector:"close" withObject:(missing value) afterDelay:12 -- will still need to quit an app
   on error errmess number errnum
      set my failure to {errorMessage:errmess, errorNumber:errnum}
   end try
end doStuff

to initialize()
   set {x, y} to {400, 210} -- base window content view size
   set textFont to current application's NSFont's fontWithName:"Courier" |size|:14
   set bgColor to current application's NSColor's colorWithDeviceRed:0.8 green:0.7 blue:0.8 alpha:1.0
   set my mainWindow to makeWindow at {} given contentSize:{x, y}, title:"Example", alphaValue:0.9
   set my textView to makeTextView at {20, 20} with drawsBackground given dimensions:{x - 40, y - 40}, textFont:textFont, backgroundColor:bgColor
   set my scrollView to makeScrollView for textView with wrapping
   mainWindow's contentView's addSubview:scrollView
   mainWindow's setInitialFirstResponder:(mainWindow's contentView) -- or whatever
   mainWindow's makeKeyAndOrderFront:me
end initialize

to update:theString
   textView's setString:theString
end update:

##################################################
#  UI object creation handlers
##################################################

# Make and return a NSWindow or NSPanel.
# Default styleMask is titled (1) + closable (2) + miniaturizable (4) + resizable (8).
# If no origin is given the window will be centered.
to makeWindow at (origin as list) given contentSize:(contentSize as list) : {400, 200}, styleMask:(styleMask as integer) : 15, title:(title as text) : "", panel:(panel as boolean) : false, floats:(floats as boolean) : false, aShadow:(aShadow as boolean) : true, minimumSize:(minimumSize as list) : {}, maximumSize:(maximumSize as list) : {}, backgroundColor:backgroundColor : missing value, alphaValue:(alphaValue as real) : 1.0 -- parameters with default values are optional
   tell current application to set theClass to item ((panel as integer) + 1) of {its NSWindow, its NSPanel}
   tell (theClass's alloc()'s initWithContentRect:{{0, 0}, contentSize} styleMask:styleMask backing:2 defer:true)
      if origin is {} then
         tell it to |center|()
      else
         its setFrameOrigin:origin
      end if
      if title is not "" then its setTitle:title
      if panel and floats then its setFloatingPanel:true
      its setHasShadow:aShadow
      if minimumSize is not {} then its setContentMinSize:minimumSize
      if maximumSize is not {} then its setContentMaxSize:maximumSize
      if backgroundColor is not missing value then its setBackgroundColor:backgroundColor
      its setAlphaValue:alphaValue
      its setAutorecalculatesKeyViewLoop:true -- add content items to the key loop
      return it
   end tell
end makeWindow

# Make and return a NSTextView.
to makeTextView at (origin as list) given dimensions:(dimensions as list) : {200, 28}, textString:(textString as text) : "", textFont:textFont : missing value, textColor:textColor : missing value, backgroundColor:backgroundColor : missing value, drawsBackground:(drawsBackground as boolean) : false, editable:(editable as boolean) : true, selectable:(selectable as boolean) : true -- parameters with default values are optional
   tell (current application's NSTextView's alloc's initWithFrame:{origin, dimensions})
      its setAutoresizingMask:(current application's NSViewWidthSizable)
      its setHorizontallyResizable:true
      its setTextContainerInset:{5, 5}
      if textFont is not missing value then its setFont:textFont
      if textColor is not missing value then its setTextColor:textColor
      if backgroundColor is not missing value then its setBackgroundColor:backgroundColor
      its setDrawsBackground:drawsBackground
      its setEditable:editable
      its setSelectable:selectable
      its setString:textString
      return it
   end tell
end makeTextView

# Make and return a NSScrollView for the given textView.
# The scrollers are shown according to preferences.
to makeScrollView for textView given borderType:borderType : missing value, verticalScroller:(verticalScroller as boolean) : true, wrapping:(wrapping as boolean) : true -- parameters with default values are optional
   if textView is missing value then return missing value
   tell (current application's NSScrollView's alloc's initWithFrame:(textView's frame))
      its setAutoresizingMask:(((current application's NSViewWidthSizable) as integer) + ((current application's NSViewHeightSizable) as integer))
      if borderType is not missing value then its setBorderType:borderType -- 0-3 or NSBorderType enum
      its setHasVerticalScroller:verticalScroller
      its setDocumentView:textView
      my (setWrapMode for textView given wrapping:wrapping)
      return it
   end tell
end makeScrollView

# (re)set a wrapping mode for the given textView.
to setWrapMode for textView given wrapping:(wrapping as boolean) : true -- parameters with default values are optional
   if textView is missing value then return missing value
   if wrapping then -- wrap at textView width
      set theWidth to width of first item of ((textView's enclosingScrollView's contentSize) as list)
      set layoutSize to current application's NSMakeSize(theWidth, 1.0E+5)
      textView's enclosingScrollView's setHasHorizontalScroller:false
      textView's textContainer's setWidthTracksTextView:true
   else -- no wrapping
      set layoutSize to current application's NSMakeSize(1.0E+5, 1.0E+5)
      textView's setMaxSize:layoutSize
      textView's enclosingScrollView's setHasHorizontalScroller:true
      textView's textContainer's setWidthTracksTextView:false
   end if
   textView's textContainer's setContainerSize:layoutSize
   textView's enclosingScrollView's setNeedsDisplay:true
   return wrapping
end setWrapMode

Fantastic. Thanks so much. I’ll wrap this in a subscript and give that a whack in a few days and get back to you.

Delenda est Carthago.

Wrapping it up in a subscript appears to work very well with the minor annoyance of a random lag in the update provision. There are still some issues that pop up beyond my novice understanding, so I need to make sure I’m not letting anything out into the wild that will cause strange memory issues.

The package runs fine like it is below but if you comment (•) back in the top level

use framework “Foundation”

and comment out the premature return, you may see what I’m talking about. The behavior of the first block becomes more unpredictable, sometimes even failing to close the window, although the second block is more predictable - except for the close. But note the lack of any returned errors, no matter how off the execution.

In the interest of time, I unwisely compressed the error reporting, which compromises error handling, then I zeroed out the ‘afterdelay’ clauses and I initialized the window with a text field. Other than that, I think your code is the same and it mostly works very well. I think it could serve a missing need in vanilla Applescript for displaying information in modeless windows for progress updates and help files. My only concern is that I made some elementary mistake somewhere and if somebody relies on this subscript in a script already referencing the Foundation framework, it will throw these strange errors, perhaps doing more than just compromising the interface.

Also, other than eliminating the close button in the created window, I’m not quite certain how someone might handle a user closing the window and then the code having to test for that and maybe reopen it. Coders can always leave a permanent floating window, I suppose.
Thanks.

use AppleScript version "2.5" -- Sierra (10.12) or later for new enumerations
--use framework "Foundation"  -- comment this back in and comment out the premature return •
use scripting additions

# UI item outlets
property mainWindow : missing value -- needed by main (init/close)
property textView : missing value -- needed by main (update clause)
property scrollView : missing value

# script properties
property fails : missing value

on run -- example can be run as app and from a script editor 
	try
		
		set shortlist1 to "▶︎Time 1" & return & " Time 2" & return & " Time3"
		set shortlist2 to " Time 1" & return & "▶︎Time 2" & return & " Time3"
		set shortlist3 to " Time 1" & return & " Time 2" & return & "▶︎Time3"
		set longtext to ""
		repeat with x from 1 to 50
			set longtext to longtext & "123456789012345678901234567890123456789012345678901234567890 This is help file line # " & x & return
		end repeat
		
		set fails to missing value
		set {fails, mainWindow, textView} to ShowScrollWindow("Playing Clipboard Times", "Beginning...", 650, 450, mainWindow, textView)
		delay 2
		
		-- one little odd problem here when using the top-level use framework "Foundation":
       -- this update is often skipped along with one or more of the next two updates
		-- the delays don't work either; sometimes there is no delay or they fall short
		set fails to UpDateScrollWindow(shortlist1, mainWindow, textView)
		log fails
		delay 2
		set fails to UpDateScrollWindow(shortlist2, mainWindow, textView)
		log fails
		delay 2
		set fails to UpDateScrollWindow(shortlist3, mainWindow, textView)
		log fails
		delay 2
		set fails to UpDateScrollWindow(longtext, mainWindow, textView)
		delay 5
		beep
		set fails to CloseScrollWindow(mainWindow, textView)
		log "Close window result: " & fails
		delay 3

		-- due to this odd behavior, I tried running through it again with less wrapper involved:
		
		return -- comment out this line • and comment back in the top level 'use framework "Foundation"'
		set {mainWindow, textView, scrollView, fails} to {missing value, missing value, missing value, missing value}
		set {fails, mainWindow, textView} to ShowScrollWindow("Pass # 2", "Trying again...", 850, 300, mainWindow, textView)
		delay 2
		tell me to textView's setString:shortlist1 -- the shortlists are ogten skipped
		delay 2
		tell me to textView's setString:shortlist2
		delay 2
		tell me to textView's setString:shortlist3
		delay 2
		tell me to textView's setString:longtext
		delay 5
		beep
		mainWindow's performSelector:"close" withObject:(missing value) afterDelay:0 -- this fails and the longtext only shows up after beep#2
		
	on error errmess number errnum
		if fails is missing value then
			display alert "Example Script Error '" & errmess & "'  #" & errnum
		else 
			display alert "Example Script " & fails
		end if
	end try
end run


-- adapted by me from code provided by RedMenace in a debugging chat:
-- <https://www.macscripter.net/t/wonky-objective-c-pointer-problems-with-a-handy-modeless-window-routine-in-applescript/78039/7>
-- wrapped up in a subscript to quarantine the Foundation framework stuff and prevent a file spec bug from bedeviling your main code
-- you may wish to place some of the defaults like background color and font specs into the handler parameters for more flexibility
-- If your main code juggles multiple open windows, you'll want to consider running a dynamic list for those windows/view specifiers  
on ShowScrollWindow(WinTitle, WinText, SizeX, SizeY, thisWindow, thisView)
	script aScript
		use AppleScript version "2.5" -- Sierra (10.12) or later for new enumerations
		use framework "Foundation"
		use scripting additions
		property parent : a reference to current application
		# UI item outlets
		property mainWindow : missing value -- needed by main (init/close)
		property textView : missing value -- needed by main (update clause)
		property scrollView : missing value
		# script properties
		property failure : missing value -- error record with keys {errorMessage, errorNumber}
		
		property aTitle : ""
		property aText : ""
		property aWidth : 300
		property aHeight : 60
		
		on ShowScrollWindow(WinTitle, WinText, SizeX, SizeY, thisWindow, thisView)
			if (WinTitle is "") and (WinText is "") then -- close the window
				try
					thisWindow's performSelector:"close" withObject:(missing value) afterDelay:0
					return ""
				on error errmess number errnum
					return ("Close ScrollWindow Error '" & errmess & "'  #" & errnum) as string
				end try
			else if WinTitle is "" then -- update the text field
				try
					tell me to thisView's setString:WinText
					return ""
					--	my performSelector:"update:" withObject:WinText afterDelay:0
				on error errmess number errnum
					return ("Update ScrollWindow Error '" & errmess & "'  #" & errnum) as string
				end try
			else -- create the window
				try
					set {mainWindow, textView, scrollView} to {missing value, missing value, missing value}
					copy {WinTitle, WinText, SizeX, SizeY} to {aTitle, aText, aWidth, aHeight}
					log mainWindow
					if current application's NSThread's isMainThread() as boolean then -- then this is the compiled version...
						doStuff()
					else -- this is the interpreter fork:
						my performSelectorOnMainThread:"doStuff" withObject:(missing value) waitUntilDone:true
					end if
					if failure is not missing value then error
					log "done"
				on error errmess number errnum
					if failure is missing value then -- use passed arguments
						return {("ShowScrollWindow Error #" & errnum & "'" & errmess & "'") as string, missing value, missing value}
					else 
						return {("Create ShowScrollWindow Error #" & failure's errorNumber & "'" & failure's errorMessage & "'") as string, missing value, missing value}
					end if
				end try
				return {failure, mainWindow, textView}
			end if
		end ShowScrollWindow
		
		# UI stuff needs to be performed on the main thread.
		# Any error is placed in a property, since `performSelectorOnMainThread` doesn't return anything.
		on doStuff()
			try
				-- set up the UI
				set textFont to current application's NSFont's fontWithName:"Courier" |size|:14 --                              as with many of these settings, easy to turn them into
				set bgColor to current application's NSColor's colorWithDeviceRed:1.0 green:1.0 blue:1.0 alpha:1.0 -- a parameter in the handler if you need more options
				set my mainWindow to makeWindow at {} given contentSize:{aWidth, aHeight}, title:WinTitle, alphaValue:1.0
				set my textView to makeTextView at {20, 20} with drawsBackground given dimensions:{aWidth - 40, aHeight - 40}, textString:aText, textFont:textFont, backgroundColor:bgColor
				set my scrollView to makeScrollView for textView with wrapping
				mainWindow's contentView's addSubview:scrollView
				mainWindow's setInitialFirstResponder:(mainWindow's contentView)
				mainWindow's makeKeyAndOrderFront:me
				log mainWindow -- remains valid before a closing call even when users dismiss the window themselves
				-- mainWindow's performSelector:"close" withObject:(missing value) afterDelay:0 -- will still need to quit an app
			on error errmess number errnum
				set my failure to "Error '" & errmess & "'  #" & errnum
			end try
		end doStuff
		
		to update:theString
			textView's setString:theString
		end update:
		
		##################################################
		#  UI object creation handlers  by Red Menace
		##################################################
		
		# Make and return a NSWindow or NSPanel.
		# Default styleMask is titled (1) + closable (2) + miniaturizable (4) + resizable (8).
		# If no origin is given the window will be centered.
		to makeWindow at (origin as list) given contentSize:(contentSize as list) : {400, 200}, styleMask:(styleMask as integer) : 15, title:(title as text) : "", panel:(panel as boolean) : false, floats:(floats as boolean) : false, aShadow:(aShadow as boolean) : true, minimumSize:(minimumSize as list) : {}, maximumSize:(maximumSize as list) : {}, backgroundColor:backgroundColor : missing value, alphaValue:(alphaValue as real) : 1.0 -- parameters with default values are optional
			tell current application to set theClass to item ((panel as integer) + 1) of {its NSWindow, its NSPanel}
			tell (theClass's alloc()'s initWithContentRect:{{0, 0}, contentSize} styleMask:styleMask backing:2 defer:true)
				if origin is {} then
					tell it to |center|()
				else
					its setFrameOrigin:origin
				end if
				if title is not "" then its setTitle:title
				if panel and floats then its setFloatingPanel:true
				its setHasShadow:aShadow
				if minimumSize is not {} then its setContentMinSize:minimumSize
				if maximumSize is not {} then its setContentMaxSize:maximumSize
				if backgroundColor is not missing value then its setBackgroundColor:backgroundColor
				its setAlphaValue:alphaValue
				its setAutorecalculatesKeyViewLoop:true -- add content items to the key loop
				return it
			end tell
		end makeWindow
		
		# Make and return a NSTextView.
		to makeTextView at (origin as list) given dimensions:(dimensions as list) : {200, 28}, textString:(textString as text) : "", textFont:textFont : missing value, textColor:textColor : missing value, backgroundColor:backgroundColor : missing value, drawsBackground:(drawsBackground as boolean) : false, editable:(editable as boolean) : true, selectable:(selectable as boolean) : true -- parameters with default values are optional
			tell (current application's NSTextView's alloc's initWithFrame:{origin, dimensions})
				its setAutoresizingMask:(current application's NSViewWidthSizable)
				its setHorizontallyResizable:true
				its setTextContainerInset:{5, 5}
				if textFont is not missing value then its setFont:textFont
				if textColor is not missing value then its setTextColor:textColor
				if backgroundColor is not missing value then its setBackgroundColor:backgroundColor
				its setDrawsBackground:drawsBackground
				its setEditable:editable
				its setSelectable:selectable
				its setString:textString
				return it
			end tell
		end makeTextView
		
		# Make and return a NSScrollView for the given textView.
		# The scrollers are shown according to preferences.
		to makeScrollView for textView given borderType:borderType : missing value, verticalScroller:(verticalScroller as boolean) : true, wrapping:(wrapping as boolean) : true -- parameters with default values are optional
			if textView is missing value then return missing value
			tell (current application's NSScrollView's alloc's initWithFrame:(textView's frame))
				its setAutoresizingMask:(((current application's NSViewWidthSizable) as integer) + ((current application's NSViewHeightSizable) as integer))
				if borderType is not missing value then its setBorderType:borderType -- 0-3 or NSBorderType enum
				its setHasVerticalScroller:verticalScroller
				its setDocumentView:textView
				my (setWrapMode for textView given wrapping:wrapping)
				return it
			end tell
		end makeScrollView
		
		# (re)set a wrapping mode for the given textView.
		to setWrapMode for textView given wrapping:(wrapping as boolean) : true -- parameters with default values are optional
			if textView is missing value then return missing value
			if wrapping then -- wrap at textView width
				set theWidth to width of first item of ((textView's enclosingScrollView's contentSize) as list)
				set layoutSize to current application's NSMakeSize(theWidth, 1.0E+5)
				textView's enclosingScrollView's setHasHorizontalScroller:false
				textView's textContainer's setWidthTracksTextView:true
			else -- no wrapping
				set layoutSize to current application's NSMakeSize(1.0E+5, 1.0E+5)
				textView's setMaxSize:layoutSize
				textView's enclosingScrollView's setHasHorizontalScroller:true
				textView's textContainer's setWidthTracksTextView:false
			end if
			textView's textContainer's setContainerSize:layoutSize
			textView's enclosingScrollView's setNeedsDisplay:true
			return wrapping
		end setWrapMode
		
	end script
	return aScript's ShowScrollWindow(WinTitle, WinText, SizeX, SizeY, thisWindow, thisView)
end ShowScrollWindow

on UpDateScrollWindow(newText, thisWindow, thisView)
	return ShowScrollWindow("", newText, 0, 0, thisWindow, thisView)
end UpDateScrollWindow

on CloseScrollWindow(thisWindow, thisView)
	return ShowScrollWindow("", "", 0, 0, thisWindow, thisView)
end CloseScrollWindow

Important correction. I failed to fully account for the pointer problems with properties in the interpreter setting, so reset those just to be sure before first creating any window in the main body. Insert the following line:

set {fails, mainWindow, textView, scrollView} to {missing value, missing value, missing value, missing value}
set {fails, mainWindow, textView} to ShowScrollWindow("Playing Clipboard Times", "Beginning...", 650, 450, mainWindow, textView)
delay 2

My code pretty much does the same things as the original, I just grouped related stuff together so that it is a little easier to see where to change things.

I don’t think that multiple ASObjC script objects in a script will work the way you think, at least I never had any luck. I’m sure that someone can explain better than I can, but the ASObjC script objects need to be kept separated so that AppleScript can link them together however it needs to, for example put the core routines in a script library (which can be placed in any of the script library locations, including in a script bundle or application) and call the library handlers from the main script.

By default a NSWindow will be be released when it is closed, so by setting the property to false, for example mainWindow’s setReleasedWhenClosed:false, the outlet will remain when the user closes the window, so you can just bring it to the front when needed.

Well, thanks for all the help. There’s something still missing on my end, though. I suspect the algorithm may be resistant to repackaging because all the pointers check out. Compiled, this code runs fine except for a glitch in the font type occuring with the update to shortlist2 despite no explicit instruction to change the font. I’ve tried a number of ways to force the font on each call, but the bizarre shift still pops up then soemtimes shifts back.

This same font glitch recurs with the interpreter version plus updates are routinely and randomly compressed or skipped altogether. The patterns of error change somewhat on each run. Sometimes it runs flawlessly and other times it skips through every update or hurries through a few. Bugs are particularly hard to reproduce when I work in another app and switch back. I’m somewhat concerned this evidence points to an internal memory problem that will shorten up time for the greater app.

OK, give this a try.

Instead of trying to use multiple ASObjC script objects in a single script, I split the window stuff into a separate library script. Script libraries are usually put in the user’s ~/Library/Script Libraries folder, but they can also be placed in the Resources/Script Libraries folder in an application or script bundle.

There can still be some weirdness since the library script instance can be attached to the controlling script running in the editor, but the display (at least with my testing) seems to be consistent. Note that if you make changes to the library script, the controlling script needs to be recompiled to load the updated version.

As I mentioned earlier, I think most of your issues have been related to main thread stuff, so the library does everything on the main thread and redraws the textView when it gets updated. Depending on how you are running things, if the library script instance is saved in the main script, the current setup will remain between runs, so I added a handler to close the window and clear the window outlet, otherwise the window is just shown or hidden. If hidden (or the user has closed it), the window will be brought to the front if the textView is updated, otherwise if it is visible it will be left where it is (even if it is obscured by other windows).

To use this example, place a copy of the following library script in ~/Library/Script Libraries. Name it however you want (I used the name ExampleWindowLib), just use the same name in the controller script.

# ExampleWindowLib
# Displays a NSWindow containing a scrolling textView at the specified location.
# Currently only a single window is supported.
# The window properties can also be changed or reset as needed via the outlets.

use AppleScript version "2.5" -- Sierra (10.12) or later for new enumerations
use framework "Foundation"
use scripting additions

# UI item outlets
property mainWindow : missing value
property textView : missing value
property scrollView : missing value

# script properties
property failure : missing value -- error record with keys {errorMessage, errorNumber}


on run
   initializeScrollWindow given title:"" -- need at least one argument to use optionals
end run

to initializeScrollWindow given topLeft:(topLeft as list) : {}, width:(width as integer) : 300, height:(height as integer) : 100, title:(title as text) : "Example", textString:(textString as text) : "", wrapping:(wrapping as boolean) : true
   try
      set arguments to {topLeft, width, height, title, textString, wrapping}
      if mainWindow is not missing value then -- already exists
         my performSelectorOnMainThread:"resetScrollWindow:" withObject:arguments waitUntilDone:true
         return
      end if
      if current application's NSThread's isMainThread() as boolean then
         my makeScrollWindow:arguments
      else
         my performSelectorOnMainThread:"makeScrollWindow:" withObject:arguments waitUntilDone:true
      end if
      if failure is not missing value then error
   on error errmess number errnum
      if failure is missing value then -- use passed arguments
         display alert "Dialog Window Library Error " & errnum message quoted form of errmess
      else -- use keys from the failure record
         display alert "Dialog Window Library Error " & failure's errorNumber message quoted form of failure's errorMessage
      end if
      set failure to missing value -- reset
   end try
   return "window initialized"
end initializeScrollWindow

to makeScrollWindow:arguments -- must be performed on the main thread
   set {topLeft, width, height, title, textString, wrapping} to arguments as list
   set {x, y} to {width, height} -- base window content view size
   set textFont to current application's NSFont's fontWithName:"Courier" |size|:14
   set bgColor to current application's NSColor's colorWithDeviceRed:0.8 green:0.7 blue:0.8 alpha:1.0
   set my mainWindow to makeWindow at topLeft given contentSize:{x, y}, title:title as text, alphaValue:0.9
   set my textView to makeTextView at {20, 20} with drawsBackground given dimensions:{x - 40, y - 40}, textString:textString as text, textFont:textFont, backgroundColor:bgColor
   set my scrollView to makeScrollView for textView given wrapping:wrapping
   mainWindow's contentView's addSubview:scrollView
   mainWindow's setInitialFirstResponder:(mainWindow's contentView) -- or whatever
   mainWindow's display()
   mainWindow's makeKeyAndOrderFront:me
end makeScrollWindow:

to resetScrollWindow:arguments -- must be performed on the main thread
   set {topLeft, width, height, title, textString, wrapping} to arguments as list
   if mainWindow is missing value then
      initializeScrollWindow given title:""
      return
   end if
   showScrollWindow()
   if topLeft is in {{}, missing value} then
      mainWindow's |center|()
   else
      mainWindow's setFrameTopLeftPoint:topLeft
   end if
   mainWindow's setTitle:title
   mainWindow's setContentSize:{width, height}
   textField's setFrame:{{20, 20}, {width - 40, height - 40}}
   scrollView's setFrame:{{20, 20}, {width - 40, height - 40}}
   setWrapMode for textView given wrapping:wrapping
   textView's setString:textString
   mainWindow's display()
   return "window reset"
end resetScrollWindow:

to updateScrollWindow(newText)
   if mainWindow is missing value then return
   showScrollWindow()
   textView's performSelectorOnMainThread:"setString:" withObject:newText waitUntilDone:true
   textView's display()
   return "window updated"
end updateScrollWindow

to showScrollWindow()
   if mainWindow is not missing value then if not (mainWindow's isVisible) then mainWindow's performSelectorOnMainThread:"orderFront:" withObject:me waitUntilDone:true -- bring to front if hidden or closed
   return "window shown"
end showScrollWindow

to hideScrollWindow()
   if mainWindow is not missing value then mainWindow's performSelectorOnMainThread:"orderOut:" withObject:me waitUntilDone:true -- remove from the screen list
   return "window hidden"
end hideScrollWindow

to closeScrollWindow()
   if mainWindow is not missing value then mainWindow's performSelectorOnMainThread:"close" withObject:(missing value) waitUntilDone:true
   set mainWindow to missing value -- reset for next time
   return "window closed"
end closeScrollWindow


##################################################
#  UI object creation handlers
##################################################

# Make and return a NSWindow or NSPanel.
# Default styleMask is titled (1) + closable (2) + miniaturizable (4) + resizable (8).
# If a topleft location is not given the window will be centered.
to makeWindow at (topLeft as list) given contentSize:(contentSize as list) : {400, 200}, styleMask:(styleMask as integer) : 15, title:(title as text) : "", panel:(panel as boolean) : false, floats:(floats as boolean) : false, aShadow:(aShadow as boolean) : true, minimumSize:(minimumSize as list) : {}, maximumSize:(maximumSize as list) : {}, backgroundColor:backgroundColor : missing value, alphaValue:(alphaValue as real) : 1.0 -- parameters with default values are optional
   tell current application to set theClass to item ((panel as integer) + 1) of {its NSWindow, its NSPanel}
   tell (theClass's alloc()'s initWithContentRect:{{0, 0}, contentSize} styleMask:styleMask backing:2 defer:true)
      if topLeft is {} then
         tell it to |center|()
      else
         its setFrameTopLeftPoint:topLeft
      end if
      if title is not "" then its setTitle:title
      if panel and floats then its setFloatingPanel:true
      its setHasShadow:aShadow
      its setReleasedWhenClosed:false
      if minimumSize is not {} then its setContentMinSize:minimumSize
      if maximumSize is not {} then its setContentMaxSize:maximumSize
      if backgroundColor is not missing value then its setBackgroundColor:backgroundColor
      its setAlphaValue:alphaValue
      its setAutorecalculatesKeyViewLoop:true -- add content items to the key loop
      return it
   end tell
end makeWindow

# Make and return a NSTextView.
to makeTextView at (origin as list) given dimensions:(dimensions as list) : {200, 28}, textString:(textString as text) : "", textFont:textFont : missing value, textColor:textColor : missing value, backgroundColor:backgroundColor : missing value, drawsBackground:(drawsBackground as boolean) : false, editable:(editable as boolean) : true, selectable:(selectable as boolean) : true -- parameters with default values are optional
   tell (current application's NSTextView's alloc's initWithFrame:{origin, dimensions})
      its setAutoresizingMask:(current application's NSViewWidthSizable)
      its setHorizontallyResizable:true
      its setTextContainerInset:{5, 5}
      if textFont is not missing value then its setFont:textFont
      if textColor is not missing value then its setTextColor:textColor
      if backgroundColor is not missing value then its setBackgroundColor:backgroundColor
      its setDrawsBackground:drawsBackground
      its setEditable:editable
      its setSelectable:selectable
      its setString:textString
      return it
   end tell
end makeTextView

# Make and return a NSScrollView for the given textView.
# The scrollers are shown according to preferences.
to makeScrollView for textView given borderType:borderType : missing value, verticalScroller:(verticalScroller as boolean) : true, wrapping:(wrapping as boolean) : true -- parameters with default values are optional
   if textView is missing value then return missing value
   tell (current application's NSScrollView's alloc's initWithFrame:(textView's frame))
      its setAutoresizingMask:(((current application's NSViewWidthSizable) as integer) + ((current application's NSViewHeightSizable) as integer))
      if borderType is not missing value then its setBorderType:borderType -- 0-3 or NSBorderType enum
      its setHasVerticalScroller:verticalScroller
      its setDocumentView:textView
      my (setWrapMode for textView given wrapping:wrapping)
      return it
   end tell
end makeScrollView

# (re)set a wrapping mode for the given textView.
to setWrapMode for textView given wrapping:(wrapping as boolean) : true -- parameters with default values are optional
   if textView is missing value then return missing value
   if wrapping then -- wrap at textView width
      set theWidth to width of first item of ((textView's enclosingScrollView's contentSize) as list)
      set layoutSize to current application's NSMakeSize(theWidth, 1.0E+5)
      textView's enclosingScrollView's setHasHorizontalScroller:false
      textView's textContainer's setWidthTracksTextView:true
   else -- no wrapping
      set layoutSize to current application's NSMakeSize(1.0E+5, 1.0E+5)
      textView's setMaxSize:layoutSize
      textView's enclosingScrollView's setHasHorizontalScroller:true
      textView's textContainer's setWidthTracksTextView:false
   end if
   textView's textContainer's setContainerSize:layoutSize
   textView's enclosingScrollView's setNeedsDisplay:true
   return wrapping
end setWrapMode

Next is a sample script that calls the library script to do whatever. Although there aren’t any ASObjC statements in this particular script, they can be used as usual without any “quarantine” needed.

# ExampleWindow control script
# Uses the ExampleWindowLib script library to show a custom window.
# The library script must be located in one of the Script Library locations.

use AppleScript version "2.5" -- Sierra (10.12) or later for new enumerations
use WindowLib : script "ExampleWindowLib"
use scripting additions


on run -- example
   try
      set shortlist1 to "▶︎Time 1" & return & " Time 2" & return & " Time 3"
      set shortlist2 to " Time 1" & return & "▶︎Time 2" & return & " Time 3"
      set shortlist3 to " Time 1" & return & " Time 2" & return & "▶︎Time 3"
      set longtext to ""
      repeat with x from 1 to 50
         set longtext to longtext & "123456789012345678901234567890123456789012345678901234567890 This is help file line # " & x & return
      end repeat
      
      tell WindowLib
         (initializeScrollWindow without wrapping given topLeft:{100, 900}, width:650, height:450, title:"Playing Clipboard Times", textString:"Beginning...")
         tell me to delay 2
         updateScrollWindow(shortlist1)
         tell me to delay 2
         updateScrollWindow(shortlist2)
         tell me to delay 2
         updateScrollWindow(shortlist3)
         tell me to delay 2
         updateScrollWindow(longtext)
         tell me to delay 5
         closeScrollWindow() -- or hideScrollWindow() to leave for next time
      end tell
      
   on error errmess number errnum
      display alert "ExampleWindow Script Error" message errmess
   end try
end run
1 Like

This weekend I thought, maybe I should stick this in a library. So I tried that and functionality cratered because I don’t know exactly what I’m doing; I spent an afternoon banging my head against a wall. I did manage to clear up some of the erratic update behavior with a “tell me to activate” line, though this nixed its nice background operation.

Then you came back and made it all work. So thanks for helping me out. This will benefit far more people besides myself. I can confirm that bundling the calls in a library does, in fact, quarantine the file spec (and other) bugs and it does so better than the subscript cut and paste version. Thus far everything works very well except for sometimes the font shifting around on my system unpredictably and an initial windows positioning issue. I’ll just center the windows and let the caller customize the font. There’s no apparent problem sticking with the default font.

Since coders have been demanding a self-contained progress bar for years in these forums, I’ve taken the liberty of adding some routines to provide precisely that. I think this makes Applescript history here. I’ve also added the best version of a spinning clock I could cook up. Casual coders should really get plenty of use out of this.

I agree, as you hinted, that any scripters using multiple windows at once will probably have to spin out multiple versions of these libraries. I got some strange save problems from the editor when I added just the wrong text to the library, so I’d be hesitant to add much more. OBvious simple enhancements include support for colors, transparency and so on - even flashing text.

Whatever your grand plans for extending its functionality, for stability’s sake I recommend creating a second library that runs on top of this one and placing as many of those extensions as possible in the new library.

I may churn out a second library for a kind of floating help file reader, which won’t necessarily need to worry about frequent updates and font changes. We’ve been needing scroll bars for long text files quite a while. I could even introduce raster graphics and primitives using emojis on an unkerned text grid. The animation potential is, well, like a trip back to the Trash 80 years.

Bundled with Dialog Toolkit, Applescript can now put together a pretty full-featured app. Others, one day, might want to extend this work into a floating icon palette, RTF reader plus a million other ideas now that we can make one side of development play nicely with the other.

I’m posting the modified library version and a demo here.

Menacia Rufa salutamus.


# ExampleWindow control script
# Uses the ExampleWindowLib script library to show a custom window.
# The library script must be located in one of the Script Library locations.

use AppleScript version "2.5" -- Sierra (10.12) or later for new enumerations
use ScrollWindowSweet : script "ScrollWindowSweet"
use scripting additions

property AnimationSets : {"🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚🕛", "🕜🕝🕞🕟🕠🕡🕢🕣🕤🕥🕦🕧", "▤▧▥▨", ((ASCII character 92) & (ASCII character 124) & (ASCII character 47) & "-"), "⇐⇖⇑⇗⇒⇘⇓⇙", "←↖︎↑↗︎→↘︎↓↙︎", "▁▂▃▄▅▆▇█▉▊▋▌▍▎▏", " ▁▂▃▄▅▆▇█", " ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁", "▉▊▋▌▍▎▏ ", "▉▊▋▌▍▎▏ ▏▎▍▌▋▊▉", "◰◳◲◱", "◴◷◶◵", "◯◎⌾⃝⊚⨀⦿⦾⊙⌾○⚪︎⬤⚫︎●𐩑𐩒◉◎⃝"}

on run -- example
	try
		set shortlist1 to "▶︎Time 1" & return & " Time 2" & return & " Time 3"
		set shortlist2 to " Time 1" & return & "▶︎Time 2" & return & " Time 3"
		set shortlist3 to " Time 1" & return & " Time 2" & return & "▶︎Time 3"
		set longtext to ""
		repeat with x from 1 to 50
			set longtext to longtext & "123456789012345678901234567890123456789012345678901234567890 This is help file line # " & x & return
		end repeat
		
		
		tell ScrollWindowSweet
			OpenStandingProgressReport("Progress Bar", return & "The task is this far from completion: ", 1000, 200, true, 50)
			repeat with x from 1 to 100
				UpdateStandingProgressReport(x)
				delay 0.1
			end repeat
			CloseStandingProgressReport()
			
			
			SetSWFont("Courier", 128)
			OpenSpinningClocks("Please Wait...", 0, 400, 200, "", 25, 5, "")
			
			--SetSWFont("Courier", 32)
			--OpenSpinningClocks("Please Wait...", 2, 1200, 200, "", 25, 5, item 10 of AnimationSets)
			
			repeat with x from 1 to 20
				delay 0.5
				UpdateSpinningClocks()
			end repeat
			CloseSpinningClocks()
			return
			
			log SetSWFont("Courier", 14)
			(initializeScrollWindow with wrapping given width:650, height:650, title:"Playing Clipboard Times", textString:"Beginning...") -- topLeft:{400, 100}
			tell me to delay 2
			updateScrollWindow(shortlist1)
			tell me to delay 2
			updateScrollWindow(shortlist2)
			tell me to delay 2
			updateScrollWindow(shortlist3)
			tell me to delay 2
			updateScrollWindow(longtext)
			tell me to delay 5
			closeScrollWindow() -- or hideScrollWindow() to leave for next time
		end tell
		
	on error errmess number errnum
		display alert "ExampleWindow Script Error" message errmess
	end try
end run


--------------------------------- Library begins here -------------------------

--script "ScrollWindowSweet"
-- For the first time in known Applescript history, a progress bar written entirely in Applescript and scoll-bar text display
-- Windowing innards courtesy of Red Menace, as outlined in the chat <https://www.macscripter.net/t/wonky-objective-c-pointer-problems-with-a-handy-modeless-window-routine-in-applescript/78039>
-- extraneous extravagances bolted on by Wade Riddick

# ExampleWindowLib
# Displays a NSWindow containing a scrolling textView at the specified location.
# Currently only a single window is supported.
# The window properties can also be changed or reset as needed via the outlets.

-- possible additions by more advanced coders might include:

--   RTF file support

--   rasterized graphics and drawing primitives via unkerned text blocks (and tiny fonts)

-- Consensus is don't expand through this bare-bones library too much
-- Instead, extend functions by building extra libraries on top of this one; there are still some strange save errors on my system

-- To use multiple windows at once, create extra copies of this library and rename each

use AppleScript version "2.5" -- Sierra (10.12) or later for new enumerations
use framework "Foundation"
use scripting additions

# UI item outlets
property mainWindow : missing value
property textView : missing value
property scrollView : missing value
property theFont : missing value
property theFontName : missing value
property theFontSize : 14
property ProgText : ""
property ShowProgBar : true
property ProgBarLength : 50
property BarBlank : "⬜️" -- ☐
property BarSolid : "⬛️"
property AnimationCharSet : ""
property ClockCounter : 1
property ClockCounterType : 1
property aClockPrefix : ""
property aClockLineLength : 50
property aClockNumlines : 3
property SpinningClocks : "🕐🕑🕒🕓🕔🕕🕖🕗🕘🕙🕚🕛" -- the default
-- yes, I know, the hour hand moves; that's all emoji gives me...

# script properties
property failure : missing value -- error record with keys {errorMessage, errorNumber}

--on run
--	initializeScrollWindow given title:"" -- need at least one argument to use optionals
--end run

-- don't open a regular scroll window until the special case Progress Window is done
on OpenStandingProgressReport(ProgTitle, ProgExplanation, xSize, ySize, ProgBar, ProgLength)
	set ProgText to ProgExplanation
	set ShowProgBar to ProgBar
	set ProgBarLength to ProgLength
	(initializeScrollWindow without wrapping given width:xSize, height:ySize, title:ProgTitle, textString:ProgText)
end OpenStandingProgressReport

on UpdateStandingProgressReport(HowMuchDone) -- out of 100
	set tempText to ProgText & HowMuchDone & " %" -- let the user center the numerical indicator with padded spaces
	if ShowProgBar then set tempText to tempText & return & return & DrawTextBar(HowMuchDone)
	updateScrollWindow(tempText)
end UpdateStandingProgressReport

on CloseStandingProgressReport()
	-- Perhaps say, "Process done!", beep and wait a second
	closeScrollWindow()
end CloseStandingProgressReport

on DrawTextBar(HowMuchDone as integer)
	set PercMark to ((HowMuchDone / 100) * ProgBarLength) as integer
	return (FillOutString(BarSolid, PercMark) & FillOutString(BarBlank, ProgBarLength - PercMark)) as string
end DrawTextBar

on FillOutString(aChar, NumberRepeat)
	if NumberRepeat is 0 then return ""
	set aString to ""
	repeat with x from 1 to NumberRepeat
		set aString to aString & aChar
	end repeat
	return aString
end FillOutString

-- Xoffset, Yoffset, linelength, numlines, whichcharset 
on OpenSpinningClocks(ClockTitle, ClockType, SizeX, SizeY, prefixString, ClockLineLength, Clocknumlines, CharSetString)
	if CharSetString is not "" then
		set AnimationCharSet to CharSetString
	else
		set AnimationCharSet to SpinningClocks
	end if
	set aClockPrefix to prefixString
	set aClockNumlines to Clocknumlines
	set aClockLineLength to ClockLineLength
	set ClockCounter to 1
	set ClockCounterType to ClockType
	(initializeScrollWindow without wrapping given width:SizeX, height:SizeY, title:ClockTitle, textString:DrawClocks(ClockCounter))
end OpenSpinningClocks

on UpdateSpinningClocks() -- out of 100
	set ClockCounter to ClockCounter + 1
	if ClockCounter > (length of AnimationCharSet) then set ClockCounter to 1
	updateScrollWindow(DrawClocks(ClockCounter))
end UpdateSpinningClocks

on CloseSpinningClocks()
	-- Perhaps say, "Process done!", beep and wait a second
	closeScrollWindow()
end CloseSpinningClocks

on DrawClocks(whichClock)
	set linelength to 50
	if ClockCounterType = 0 then
		set aString to aClockPrefix & (item whichClock of AnimationCharSet)
	else if ClockCounterType = 1 then
		set aString to aClockPrefix & FillOutString(item whichClock of AnimationCharSet, aClockLineLength)
	else if ClockCounterType = 2 then
		set aString to aClockPrefix
		repeat with x from 1 to aClockNumlines
			set aString to aString & FillOutString(item whichClock of AnimationCharSet, aClockLineLength) & return
		end repeat
	else if ClockCounterType = 3 then
		set {x, aString} to {whichClock, aClockPrefix}
		repeat with z from 1 to aClockNumlines
			repeat with y from 1 to aClockLineLength
				set aString to aString & (item x of AnimationCharSet)
				set x to x + 1
				if x > (length of AnimationCharSet) then set x to 1
			end repeat
			set aString to aString & return
		end repeat
	end if
	return aString
end DrawClocks

-- other window/text characteristics can be removed from the primitives and set like Font/size in library properties
on SetSWFont(aFontName as string, aFontSize as integer)
	if aFontName is in {missing value, ""} then
		set theFont to missing value
		return
	end if
	set {oldFont, OldFontName, OldFontSize} to {theFont, theFontName, theFontSize}
	if (aFontSize < 8) or (aFontSize > 128) then set aFontSize to 14
	try
		set theFont to current application's NSFont's fontWithName:aFontName |size|:aFontSize
		set {theFontName, theFontSize} to {aFontName, aFontSize}
	on error errorMssg number errorNumber
		set {theFont, theFontName, theFontSize} to {oldFont, OldFontName, OldFontSize}
		return ("SetSWFont error: '" & errorMssg & "' #" & errorNumber) as string
	end try
	log "Received request to change font to '" & aFontName & "' Size:" & aFontSize
	log "Set the ScrollWindowFont to '" & theFontName & "' Size:" & theFontSize
end SetSWFont

--to initializeScrollWindow given topLeft:(topLeft as list) : {}, width:(width as integer) : 300, height:(height as integer) : 300, title:(title as text) : "Example", textString:(textString as text) : "", wrapping:(wrapping as boolean) : true
to initializeScrollWindow given topLeft:(topLeft as list) : {}, width:(width as integer), height:(height as integer), title:(title as text), textString:(textString as text), wrapping:(wrapping as boolean)
	try
		set arguments to {topLeft, width, height, title, textString, wrapping}
		if mainWindow is not missing value then -- already exists
			my performSelectorOnMainThread:"resetScrollWindow:" withObject:arguments waitUntilDone:true
			return
		end if
		if current application's NSThread's isMainThread() as boolean then
			my makeScrollWindow:arguments
		else
			my performSelectorOnMainThread:"makeScrollWindow:" withObject:arguments waitUntilDone:true
		end if
		if failure is not missing value then error
	on error errmess number errnum
		if failure is missing value then -- use passed arguments
			display alert "Dialog Window Library Error " & errnum message quoted form of errmess
		else -- use keys from the failure record
			display alert "Dialog Window Library Error " & failure's errorNumber message quoted form of failure's errorMessage
		end if
		set failure to missing value -- reset
	end try
	return "window initialized"
end initializeScrollWindow

to makeScrollWindow:arguments -- must be performed on the main thread
	set {topLeft, width, height, title, textString, wrapping} to arguments as list
	set {x, y} to {width, height} -- base window content view size
	--	set textFont to current application's NSFont's fontWithName:"Courier" |size|:14
	set bgColor to current application's NSColor's colorWithDeviceRed:1.0 green:1.0 blue:1.0 alpha:1.0
	set my mainWindow to makeWindow at topLeft given contentSize:{x, y}, title:title as text, alphaValue:1.0
	set my textView to makeTextView at {20, 20} with drawsBackground given dimensions:{x - 40, y - 40}, textString:textString as text, textFont:theFont, backgroundColor:bgColor
	set my scrollView to makeScrollView for textView given wrapping:wrapping
	mainWindow's contentView's addSubview:scrollView
	mainWindow's setInitialFirstResponder:(mainWindow's contentView) -- or whatever
	mainWindow's display()
	mainWindow's makeKeyAndOrderFront:me
end makeScrollWindow:

to resetScrollWindow:arguments -- must be performed on the main thread
	set {topLeft, width, height, title, textString, wrapping} to arguments as list
	if mainWindow is missing value then
		initializeScrollWindow given title:""
		return
	end if
	showScrollWindow()
	if topLeft is in {{}, missing value} then
		mainWindow's |center|()
	else
		mainWindow's setFrameTopLeftPoint:topLeft
	end if
	mainWindow's setTitle:title
	mainWindow's setContentSize:{width, height}
	textField's setFrame:{{20, 20}, {width - 40, height - 40}}
	scrollView's setFrame:{{20, 20}, {width - 40, height - 40}}
	setWrapMode for textView given wrapping:wrapping
	textView's setString:textString
	mainWindow's display()
	return "window reset"
end resetScrollWindow:

to updateScrollWindow(newText)
	if mainWindow is missing value then return
	showScrollWindow()
	textView's performSelectorOnMainThread:"setString:" withObject:newText waitUntilDone:true
	textView's display()
	return "window updated"
end updateScrollWindow

to showScrollWindow()
	if mainWindow is not missing value then if not (mainWindow's isVisible) then mainWindow's performSelectorOnMainThread:"orderFront:" withObject:me waitUntilDone:true -- bring to front if hidden or closed
	return "window shown"
end showScrollWindow

to hideScrollWindow()
	if mainWindow is not missing value then mainWindow's performSelectorOnMainThread:"orderOut:" withObject:me waitUntilDone:true -- remove from the screen list
	return "window hidden"
end hideScrollWindow

to closeScrollWindow()
	if mainWindow is not missing value then mainWindow's performSelectorOnMainThread:"close" withObject:(missing value) waitUntilDone:true
	set mainWindow to missing value -- reset for next time
	return "window closed"
end closeScrollWindow


##################################################
#  UI object creation handlers
##################################################

# Make and return a NSWindow or NSPanel.
# Default styleMask is titled (1) + closable (2) + miniaturizable (4) + resizable (8).
# If a topleft location is not given the window will be centered.
to makeWindow at (topLeft as list) given contentSize:(contentSize as list) : {300, 200}, styleMask:(styleMask as integer) : 15, title:(title as text) : "", panel:(panel as boolean) : false, floats:(floats as boolean) : false, aShadow:(aShadow as boolean) : true, minimumSize:(minimumSize as list) : {}, maximumSize:(maximumSize as list) : {}, backgroundColor:backgroundColor : missing value, alphaValue:(alphaValue as real) : 1.0 -- parameters with default values are optional
	tell current application to set theClass to item ((panel as integer) + 1) of {its NSWindow, its NSPanel}
	tell (theClass's alloc()'s initWithContentRect:{{0, 0}, contentSize} styleMask:styleMask backing:2 defer:true)
		if topLeft is {} then
			tell it to |center|()
		else
			its setFrameTopLeftPoint:topLeft
		end if
		if title is not "" then its setTitle:title
		if panel and floats then its setFloatingPanel:true
		its setHasShadow:aShadow
		its setReleasedWhenClosed:false
		if minimumSize is not {} then its setContentMinSize:minimumSize
		if maximumSize is not {} then its setContentMaxSize:maximumSize
		if backgroundColor is not missing value then its setBackgroundColor:backgroundColor
		its setAlphaValue:alphaValue
		its setAutorecalculatesKeyViewLoop:true -- add content items to the key loop
		return it
	end tell
end makeWindow

# Make and return a NSTextView.
to makeTextView at (origin as list) given dimensions:(dimensions as list) : {200, 28}, textString:(textString as text) : "", textFont:textFont : missing value, textColor:textColor : missing value, backgroundColor:backgroundColor : missing value, drawsBackground:(drawsBackground as boolean) : false, editable:(editable as boolean) : true, selectable:(selectable as boolean) : true -- parameters with default values are optional
	tell (current application's NSTextView's alloc's initWithFrame:{origin, dimensions})
		its setAutoresizingMask:(current application's NSViewWidthSizable)
		its setHorizontallyResizable:true
		its setTextContainerInset:{5, 5}
		if textFont is not missing value then its setFont:theFont
		if textColor is not missing value then its setTextColor:textColor
		if backgroundColor is not missing value then its setBackgroundColor:backgroundColor
		its setDrawsBackground:drawsBackground
		its setEditable:editable
		its setSelectable:selectable
		its setString:textString
		return it
	end tell
end makeTextView

# Make and return a NSScrollView for the given textView.
# The scrollers are shown according to preferences.
to makeScrollView for textView given borderType:borderType : missing value, verticalScroller:(verticalScroller as boolean) : true, wrapping:(wrapping as boolean) : true -- parameters with default values are optional
	if textView is missing value then return missing value
	tell (current application's NSScrollView's alloc's initWithFrame:(textView's frame))
		its setAutoresizingMask:(((current application's NSViewWidthSizable) as integer) + ((current application's NSViewHeightSizable) as integer))
		if borderType is not missing value then its setBorderType:borderType -- 0-3 or NSBorderType enum
		its setHasVerticalScroller:verticalScroller
		its setDocumentView:textView
		my (setWrapMode for textView given wrapping:wrapping)
		return it
	end tell
end makeScrollView

# (re)set a wrapping mode for the given textView.
to setWrapMode for textView given wrapping:(wrapping as boolean) : true -- parameters with default values are optional
	if textView is missing value then return missing value
	if wrapping then -- wrap at textView width
		set theWidth to width of first item of ((textView's enclosingScrollView's contentSize) as list)
		set layoutSize to current application's NSMakeSize(theWidth, 1.0E+5)
		textView's enclosingScrollView's setHasHorizontalScroller:false
		textView's textContainer's setWidthTracksTextView:true
	else -- no wrapping
		set layoutSize to current application's NSMakeSize(1.0E+5, 1.0E+5)
		textView's setMaxSize:layoutSize
		textView's enclosingScrollView's setHasHorizontalScroller:true
		textView's textContainer's setWidthTracksTextView:false
	end if
	textView's textContainer's setContainerSize:layoutSize
	textView's enclosingScrollView's setNeedsDisplay:true
	return wrapping
end setWrapMode