Activity Indicator - Similar to Domain or Network Status LED/Orb

I’m back again…

I am attempting to program a status indicator similar to that of the Domain (Network Account Server) or Network connectivity status LED light (green/yellow/red, sometimes referred to as the ‘jelly’). I would like to attempt this on my own, but I cannot find where in the documentation it exists. I would like to use this functionality to tell users of my application that a file path (my application references other software that must be installed) exists on the machine. This indicator will let the user know it is okay to proceed with specific functions of the application (green), or that installation or troubleshooting should be attempted (red).

Does anyone know where to find this type of indicator as either a class or subclass, potentially even a method. Bonus points would be if I can also use the dialog popup from the >10.7 loginwindow that indicates that network accounts are unavailable. Is there any way for me to find this item specifically, i.e., some (unknown to me) database that matches classes that contain GUI items to their class names?

Thanks in advance for any insight. Even the correct terminology would be helpful so I can attempt to find a starting place for this functionality.

They’re not separate classes – they’re just images added to buttons or image wells. To change appearance, you change the image. Have a look in the Resources folder of something like Airport Utility.app to get an idea of what to use.

Hi,

short example, the variable statusImage is a NSImageView outlet (16 x 16 borderless)
Call the handler with values -1 (not available), 0 (partially available) or 1 (available)


on setImageForStatus_(status)
	if status = 1 then
		set imageName to current application's NSImageNameStatusAvailable
	else if status = 0 then
		set imageName to current application's NSImageNameStatusPartiallyAvailable
	else
		set imageName to current application's NSImageNameStatusUnavailable
	end if
	
	statusImage's setImage_(current application's NSImage's imageNamed_(imageName))
end setImageForStatus_

I had been looking at NSImageView for changing an image based on whether or not the file/folder path existed, so it seems I was on the right track. I originally got the idea from the tableview example in your book regarding multiple columns. I’ll re-read the section as I go to see what I can use. Thanks Shane. One more way your book has helped me immensely.

Stefan’s example helps me tremendously, and it looks like I was on the right path. I plan to add this as a repeating (read, background) task in the application, upon launch. You guys are awesome! I’ll work on my code tomorrow and post back with what I come up with for critiquing and documentation.

Ok guys, spent a little time on this…I think I have found the best way to check for each file’s status, now I am just wondering the best way to call the sender Stefan posted. Below is my current code for searching for each log file’s presence in the file system:


on applicationWillFinishLaunching_(aNotification)
-- Set First Log File Variable and Timer
set manager to current application's NSFileManager's defaultManager()
        set fileOne to "/Library/company/folder/place/subplace/log" as string
        current application's NSTimer's scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(30, me, "checkFileOne:", missing value, 1)

--Set Second Log File Variable and Timerr
set manager to current application's NSFileManager's defaultManager()
set fileTwo to "/Library/Logs/somefile.log" as string
current application's NSTimer's scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(30, me, "checkFileTwo:", missing value, 1)
 
-- Check for first log file's existence in the file system
        on checkFileOne_(sender)
            set var1 to manager's fileExistsAtPath_(fileOne)
            if var1 as integer = 1 then
                log "First Log File found"
                set fileOneStatus to 1
                sender's invalidate()
                else
                log "No Log File Found"
                set fileOneStatus to 0
            end if
        end checkFileOne_
        
-- Check for second log file's existence in the file system
        on checkFileTwo_(sender)
            set var1 to manager's fileExistsAtPath_(encFile)
            if var1 as integer = 1 then
                log "2nd Log File found"
                set fileTwoStatus to 1
                sender's invalidate() -- this stops the timer
            else
                log "No #2 Log File Found"
                set fileTwoStatus to 0
            end if
        end checkFileTwo_
end applicationWillFinishLaunching_

I think that it is better for my application’s function to break each file exists check up for each file. That being said, I will probably have 2 of stefan’s suggested code to set the image, one for each file. What is the best way to call that from the checkFileOne and checkFileTwo for each file? It would need to be called whether the file is present or not, so as to set the icon to available or not available indication.

you could also pass the reference to the image view (the name of the property) in the handler


on setImageOfImageView_forStatus_(imageView, status)
	if status = 1 then
		set imageName to current application's NSImageNameStatusAvailable
	else if status = 0 then
		set imageName to current application's NSImageNameStatusPartiallyAvailable
	else
		set imageName to current application's NSImageNameStatusUnavailable
	end if
	
	imageView's setImage_(current application's NSImage's imageNamed_(imageName))
end setImageOfImageView_forStatus_

Thanks Stefan,

If I leave these:

-- Check for first log file's existence in the file system
on checkFileOne_(sender)
set var1 to manager's fileExistsAtPath_(fileOne)
if var1 as integer = 1 then
log "First Log File found"
set fileOneStatus to 1
sender's invalidate()
else
log "No Log File Found"
set fileOneStatus to 0
end if
end checkFileOne_

-- Check for second log file's existence in the file system
on checkFileTwo_(sender)
set var1 to manager's fileExistsAtPath_(encFile)
if var1 as integer = 1 then
log "2nd Log File found"
set fileTwoStatus to 1
sender's invalidate() -- this stops the timer
else
log "No #2 Log File Found"
set fileTwoStatus to 0
end if
end checkFileTwo_
end applicationWillFinishLaunching_

under on applicationWillFinishLaunching_(aNotification), I get an error:

So I move them to another location in my applescript file, just under the property declarations, and this allows the build to succeed. I know it shouldn’t matter where in the script things reside as long as they are being called correctly, but what is a good way to troubleshoot that they are being called? I can set logs in all the right places, I’m just wondering if that is the best way to show something is being called.

I just set the logs and the checks are not being called by the timers. I’m trying to troubleshoot to see why.

where is on applicationWillFinishLaunching_() ??

you cannot nest handlers, handlers must be always on the top level of the script object


on applicationWillFinishLaunching_()
	--	
end applicationWillFinishLaunching_

on checkFileOne_(sender)
	--
end checkFileOne_

-- Check for second log file's existence in the file system
on checkFileTwo_(sender)
	--
end checkFileTwo_

Sorry Stefan, it escaped my copy/paste I believe. I think I have a pretty good solution in the code below, what do you think? I have only posted below what I have applied to one log file, for the second, the code is duplicated, and variables and handlers are changed accordingly.


on applicationWillFinishLaunching_(aNotification)
-- Set Log Files as variables for checkFiles repeated task below
        set logOneManager to current application's NSFileManager's defaultManager()
        set logOneFile to "/Library/company/folder/subfolder/place/file" as string
        current application's NSTimer's scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(300, me, "checkLogOne:", missing value, 1)

checkLogOne(me)
end applicationWillFinishLaunching_

on checkLogOne_(sender)
        log "Performing 5 minute First Log File Check"
        set var1 to logOneManager's fileExistsAtPath_(logOneFile)
        if var1 as integer = 1 then
            log "First Log File found"
            set logOneStatus to 1
        else
            log "First Log File Not Found"
            set logOneStatus to 0
        end if
        if logOneStatus = 1 then
            set imageName to current application's NSImageNameStatusAvailable
            else if logOneStatus = 0 then
            set imageName to current application's NSImageNameStatusUnavailable
            else
            set imageName to current application's NSImageNameStatusPartiallyAvailable
        end if
        logOneOrb's setImage_(current application's NSImage's imageNamed_(imageName))
end checkLogOne_

The above code gets me a repeating task every 300 seconds (5 minutes) to check the status of the Log File and display the indicator accordingly. It does not, however, run at startup. To get around that, I called the handler at the end of the application launch with checkLogOne(me), and it runs every 300 seconds afterwards. As I mentioned above, I duplicate the code for the 2nd log file, and change the variables and handlers accordingly (ie., logTwoStatus, checkLogTwo, etc.

A huge advantage of Cocoa over AppleScript is the presence of callbacks and notifications.
Here is a smarter way to monitor the directory. No timers, no loops.
FSEvents notify every time the directory changes, the callback invokes a delegate method (directoryDidChange) in the script.

First, create an Objective-C class in your project (⌘N), name it FSEDirectoryWatcher (subclass of NSObject)

Replace the contents of FSEDirectoryWatcher.h with


#import <Foundation/Foundation.h>

@class FSEDirectoryWatcher;

@protocol FSEDirectoryWatcherDelegate
- (void)directoryDidChange:(NSString *)path;
@end

@interface FSEDirectoryWatcher : NSObject

+ (id)directoryWatcherWithPath:(NSString *)path delegate:(id)delegate;
- (id)initWithPath:(NSString *)path delegate:(id)delegate;

- (void)start;
- (void)stop;

@property (copy) NSString *directory;
@property (weak) id<FSEDirectoryWatcherDelegate> delegate;

@end


Replace the contents of FSEDirectoryWatcher.m with


#import "FSEDirectoryWatcher.h"

static void fileSystemEventCallback(ConstFSEventStreamRef streamRef, void *userData, size_t numEvents, void *eventPaths,
                                    const FSEventStreamEventFlags eventFlags[], const FSEventStreamEventId eventIds[]);

@interface FSEDirectoryWatcher ()
{
    dispatch_queue_t queue;
	FSEventStreamRef stream;
}
@end

@implementation FSEDirectoryWatcher

+ (id)directoryWatcherWithPath:(NSString *)path delegate:(id)delegate

{
	return [[FSEDirectoryWatcher alloc] initWithPath:path delegate];
}

- (id)initWithPath:(NSString *)path delegate:(id)delegate
{
	self = [super init];
    if (self) {
		_directory = path;
		_delegate = delegate;
        queue = dispatch_queue_create("com.myself.FSEDirectoryWatcher", DISPATCH_QUEUE_SERIAL);
	}
	return self;
}

- (void) dealloc
{
    if (queue) dispatch_release(queue);
	[self stop];
}

- (void)start
{
	NSArray *pathsToWatch = @[self.directory];
	FSEventStreamContext context = {0, (__bridge void *)self, NULL, NULL, NULL};
    CFAbsoluteTime latency = 3.0; /* Latency in seconds */
	
	stream = FSEventStreamCreate(NULL, &fileSystemEventCallback, &context, (__bridge CFArrayRef)pathsToWatch,
								 kFSEventStreamEventIdSinceNow, latency, kFSEventStreamCreateFlagUseCFTypes);
	
	FSEventStreamSetDispatchQueue(stream, queue);
	FSEventStreamStart(stream);
}

- (void)stop
{
	if (stream) {
		FSEventStreamStop(stream);
		FSEventStreamInvalidate(stream);
		FSEventStreamRelease(stream);
        stream = nil;
	}
}

@end

static void fileSystemEventCallback(ConstFSEventStreamRef streamRef, void *userData, size_t numEvents, void *eventPaths,
                                    const FSEventStreamEventFlags eventFlags[],  const FSEventStreamEventId eventIds[]) {
    FSEDirectoryWatcher *directoryWatcher = (__bridge FSEDirectoryWatcher *)userData;
    NSArray *paths = (__bridge NSArray *)eventPaths;
    
    [paths enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        if (directoryWatcher.delegate) [directoryWatcher.delegate directoryDidChange:obj];
		
    }];
}


Note: the FSEDirectoryWatcher class requires ARC (automatic reference counting)

This is the AppleScript code


property DirectoryWatcher : class "FSEDirectoryWatcher"

property statusImage1 : missing value -- image view 1
property statusImage2 : missing value -- image view 2

on applicationWillFinishLaunching_(aNotification)
	set hotFolder to current application's NSString's stringWithString_("/Path/to/directory")
	directoryDidChange_(hotFolder)
	set watcher to DirectoryWatcher's directoryWatcherWithPath_delegate_(hotFolder, me)
	watcher's start()
end applicationWillFinishLaunching_

on imageForStatus_(status)
	if status = true then
		set imageName to current application's NSImageNameStatusAvailable
	else
		set imageName to current application's NSImageNameStatusUnavailable
	end if
	return current application's NSImage's imageNamed_(imageName)
end imageForStatus_

on directoryDidChange_(directoryPath)
	set filePath to directoryPath's stringByAppendingPathComponent_("logfile1.log")
	set fileExists to NSFileManager's defaultManager()'s fileExistsAtPath_(filePath) as boolean
	statusImage1's setImage_(imageForStatus_(fileExists))
	set filePath to directoryPath's stringByAppendingPathComponent_("logfile2.log")
	set fileExists to NSFileManager's defaultManager()'s fileExistsAtPath_(filePath) as boolean
	statusImage2's setImage_(imageForStatus_(fileExists))
end directoryDidChange_


It’s probably worth pointing out that that means it can only be used as-is under OS X 10.8.

Stefan:

¢ Have you made any ARC ASObjC apps, and if so have you seen any memory leaks?

¢ I like the design of your FSEDirectoryWatcher class, but have you considered using dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE …) instead of FSEvents?

Actually ARC is supported in Lion too (64 bit only) and even in Snow Leopard (no weak references).
But it’s very easy to create a Garbage Collection or Memory Management version

No, I’ve written a Cocoa app with a lot of ASOC classes, which I use frequently.
I’ll run Instruments the next time.

Yes, I know that too (as well as kqueue). FSEvents is preferable for multiple directories or a directory hierarchy,
for a single file or single directory indeed dispatch_source_create is the better solution

Yes, but you can’t use ARC with ASObjC under versions before 10.8. As I understand it, the AppleScriptObjC runtime does its own memory management in 10.8, so it works with ARC or GC (or manual management). But because it’s part of the runtime, it’s not available in earlier versions, so apps that are to run under them are restricted to GC or manual management.

Thanks!

I’m using StefanK’s Objectve-C program as a Cocoa Framework.It is easy to call FSEvents via StefanK’s program. This AppleScript runs on Script Editor or Script Debugger.

Thanks StefanK!


– Created by: Takaaki Naganoya
– Created on: 2019/07/23

– Copyright © 2019 Piyomaru Software, All Rights Reserved

use AppleScript version “2.4” – Yosemite (10.10) or later
use framework “Foundation”
use framework “AppKit”
use scripting additions
use framework “FSWatcher” --Original By StefanK http://macscripter.net/viewtopic.php?pid=161125

property filesInADir : {}
property currentFilesInADir : {}
property dlRes : “” --Watch Folder Path (POSIX path)

on run
set dlRes to getSafariDownloadFolder() of me
set hotFolder to current application’s NSString’s stringWithString:dlRes
set aWatcher to current application’s FSEDirectoryWatcher’s alloc()'s initWithPath:hotFolder delegate:me
my aWatcher’s start()
end run

on getSafariDownloadFolder()
set theID to id of application “Safari” → “com.apple.Safari”
set dlRes2 to getAppDefaultsValue(theID, “DownloadsPath”)
return dlRes2
end getSafariDownloadFolder

on getAppDefaultsValue(appBundleID, appKey)
set storedDefaults to (current application’s NSUserDefaults’s standardUserDefaults()'s persistentDomainForName:appBundleID)
set keyList to storedDefaults’s allKeys() as list
if appKey is not in keyList then return missing value
set dlRes to (storedDefaults’s valueForKeyPath:appKey)
set dlRes2 to (dlRes’s stringByExpandingTildeInPath()) as list of string or string
return dlRes2
end getAppDefaultsValue

on directoryDidChange:aDirPath
set aTmpPath to aDirPath as string
if aTmpPath is not equal to dlRes then
tell current application
display notification “Download Folder Changed with…” & return & aTmpPath
end tell
end if
end directoryDidChange:

Model: MacBook Pro
AppleScript: 2.5
Browser: Safari 605.1.15
Operating System: macOS 10.12