446 lines
11 KiB
Objective-C
446 lines
11 KiB
Objective-C
//
|
|
// file: StatusBarMenu.m
|
|
// project: OverSight (login item)
|
|
// description: menu handler for status bar icon
|
|
//
|
|
// created by Patrick Wardle
|
|
// copyright (c) 2017 Objective-See. All rights reserved.
|
|
//
|
|
|
|
#import "consts.h"
|
|
#import "utilities.h"
|
|
#import "AppDelegate.h"
|
|
#import "StatusBarItem.h"
|
|
#import "StatusBarPopoverController.h"
|
|
|
|
/* GLOBALS */
|
|
|
|
//log handle
|
|
extern os_log_t logHandle;
|
|
|
|
//menu items
|
|
enum menuItems
|
|
{
|
|
status = 100,
|
|
devices,
|
|
toggle,
|
|
prefs,
|
|
rules,
|
|
quit,
|
|
uninstall,
|
|
end
|
|
};
|
|
|
|
//tag for active device
|
|
#define TAG_ACTIVE_DEVICE 1000
|
|
|
|
@implementation StatusBarItem
|
|
|
|
@synthesize isDisabled;
|
|
@synthesize statusItem;
|
|
|
|
//init method
|
|
// set some intial flags
|
|
-(id)init:(NSMenu*)menu
|
|
{
|
|
//token
|
|
static dispatch_once_t onceToken = 0;
|
|
|
|
//super
|
|
self = [super init];
|
|
if(self != nil)
|
|
{
|
|
//create item
|
|
[self createStatusItem:menu];
|
|
|
|
//only once
|
|
// show popover
|
|
dispatch_once(&onceToken, ^{
|
|
|
|
//first time?
|
|
// show popover
|
|
if(YES == [NSProcessInfo.processInfo.arguments containsObject:INITIAL_LAUNCH])
|
|
{
|
|
//dbg msg
|
|
os_log_debug(logHandle, "initial launch, will show popover");
|
|
|
|
//show
|
|
[self showPopover];
|
|
}
|
|
|
|
});
|
|
|
|
//set state based on (existing) preferences
|
|
self.isDisabled = [NSUserDefaults.standardUserDefaults boolForKey:PREF_IS_DISABLED];
|
|
|
|
//set initial menu state
|
|
[self setState];
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
//create status item
|
|
-(void)createStatusItem:(NSMenu*)menu
|
|
{
|
|
//init status item
|
|
statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
|
|
|
|
//set menu
|
|
self.statusItem.menu = menu;
|
|
|
|
//set action handler for all menu items
|
|
for(int i=toggle; i<end; i++)
|
|
{
|
|
//set action
|
|
[self.statusItem.menu itemWithTag:i].action = @selector(handler:);
|
|
|
|
//set state
|
|
[self.statusItem.menu itemWithTag:i].enabled = YES;
|
|
|
|
//set target
|
|
[self.statusItem.menu itemWithTag:i].target = self;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
//update status item menu
|
|
-(void)setActiveDevices:(NSArray*)activeDevices
|
|
{
|
|
//active menu item
|
|
NSMenuItem* activeDeviceMenuItem = nil;
|
|
|
|
//start index
|
|
NSInteger menuIndex = -1;
|
|
|
|
//string for device name/emoji
|
|
NSMutableString* deviceDetails = nil;
|
|
|
|
//get menu item
|
|
activeDeviceMenuItem = [self.statusItem.menu itemWithTag:devices];
|
|
|
|
//get menu item index start
|
|
menuIndex = [self.statusItem.menu indexOfItemWithTag:devices];
|
|
|
|
//iterate over menu
|
|
// remove all (prev) active devices
|
|
for(NSInteger i = self.statusItem.menu.itemArray.count-1; i>= 0; --i)
|
|
{
|
|
//remove active devices
|
|
if(TAG_ACTIVE_DEVICE == [[self.statusItem.menu itemAtIndex:i] tag])
|
|
{
|
|
//remove
|
|
[self.statusItem.menu removeItemAtIndex:i];
|
|
}
|
|
}
|
|
|
|
//no active devices?
|
|
// set title and then bail
|
|
if(0 == activeDevices.count)
|
|
{
|
|
//set title
|
|
activeDeviceMenuItem.title = @"No Active Devices";
|
|
|
|
//gone
|
|
goto bail;
|
|
}
|
|
|
|
//set title
|
|
activeDeviceMenuItem.title = @"Active Devices:";
|
|
|
|
//inc
|
|
menuIndex++;
|
|
|
|
//add each
|
|
for(AVCaptureDevice* activeDevice in activeDevices)
|
|
{
|
|
//menu item
|
|
NSMenuItem* item = nil;
|
|
|
|
//init string for name/etc
|
|
deviceDetails = [NSMutableString string];
|
|
|
|
//mic?
|
|
if(YES == [activeDevice isKindOfClass:NSClassFromString(@"AVCaptureHALDevice")])
|
|
{
|
|
//add
|
|
[deviceDetails appendString:@" 🎙️ "];
|
|
}
|
|
//camera
|
|
else
|
|
{
|
|
//add
|
|
[deviceDetails appendString:@" 📸 "];
|
|
}
|
|
|
|
//add name
|
|
[deviceDetails appendString:activeDevice.localizedName];
|
|
|
|
//init item
|
|
item = [[NSMenuItem alloc] initWithTitle:deviceDetails action:nil keyEquivalent:@""];
|
|
|
|
//set tag
|
|
item.tag = TAG_ACTIVE_DEVICE;
|
|
|
|
//add item to menu
|
|
[self.statusItem.menu insertItem:item atIndex:menuIndex];
|
|
|
|
//inc
|
|
menuIndex++;
|
|
}
|
|
|
|
bail:
|
|
|
|
return;
|
|
}
|
|
|
|
//remove status item
|
|
-(void)removeStatusItem
|
|
{
|
|
//remove item
|
|
[[NSStatusBar systemStatusBar] removeStatusItem:self.statusItem];
|
|
|
|
//unset
|
|
self.statusItem = nil;
|
|
|
|
return;
|
|
}
|
|
|
|
//show popver
|
|
-(void)showPopover
|
|
{
|
|
//alloc popover
|
|
self.popover = [[NSPopover alloc] init];
|
|
|
|
//don't want highlight for popover
|
|
self.statusItem.button.cell.highlighted = NO;
|
|
|
|
//set target
|
|
self.statusItem.button.target = self;
|
|
|
|
//set view controller
|
|
self.popover.contentViewController = [[StatusBarPopoverController alloc] initWithNibName:@"StatusBarPopover" bundle:nil];
|
|
|
|
//set behavior
|
|
// auto-close if user clicks button in status bar
|
|
self.popover.behavior = NSPopoverBehaviorTransient;
|
|
|
|
//set delegate
|
|
self.popover.delegate = self;
|
|
|
|
//show popover
|
|
// have to wait cuz...
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1.5 * NSEC_PER_SEC), dispatch_get_main_queue(),
|
|
^{
|
|
//show
|
|
[self.popover showRelativeToRect:self.statusItem.button.bounds ofView:self.statusItem.button preferredEdge:NSMinYEdge];
|
|
});
|
|
|
|
//wait a bit
|
|
// then automatically hide popup if user has not closed it
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 3.5 * NSEC_PER_SEC), dispatch_get_main_queue(),
|
|
^{
|
|
//still visible?
|
|
// close it then...
|
|
if(YES == self.popover.shown)
|
|
{
|
|
//close
|
|
[self.popover performClose:nil];
|
|
}
|
|
|
|
//remove action handler
|
|
self.statusItem.button.action = nil;
|
|
|
|
//reset highlight mode
|
|
((NSButtonCell*)self.statusItem.button.cell).highlightsBy = NSContentsCellMask | NSChangeBackgroundCellMask;
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
//cleanup popover
|
|
-(void)popoverDidClose:(NSNotification *)notification
|
|
{
|
|
//unset
|
|
self.popover = nil;
|
|
|
|
//reset highlight mode
|
|
((NSButtonCell*)self.statusItem.button.cell).highlightsBy = NSContentsCellMask | NSChangeBackgroundCellMask;
|
|
|
|
return;
|
|
}
|
|
|
|
//menu handler
|
|
-(void)handler:(id)sender
|
|
{
|
|
//dbg msg
|
|
os_log_debug(logHandle, "user clicked status menu item %lu", ((NSMenuItem*)sender).tag);
|
|
|
|
//handle user selection
|
|
switch(((NSMenuItem*)sender).tag)
|
|
{
|
|
//toggle
|
|
case toggle:
|
|
|
|
//dbg msg
|
|
os_log_debug(logHandle, "toggling (%d)", self.isDisabled);
|
|
|
|
//invert since toggling
|
|
self.isDisabled = !self.isDisabled;
|
|
|
|
//set menu state
|
|
[self setState];
|
|
|
|
//set & sync
|
|
[NSUserDefaults.standardUserDefaults setBool:self.isDisabled forKey:PREF_IS_DISABLED];
|
|
[NSUserDefaults.standardUserDefaults synchronize];
|
|
|
|
//stop monitor
|
|
if(YES == self.isDisabled)
|
|
{
|
|
//dbg msg
|
|
os_log_debug(logHandle, "will stop monitor");
|
|
|
|
//stop
|
|
[((AppDelegate*)[[NSApplication sharedApplication] delegate]).avMonitor stop];
|
|
}
|
|
//(re)start monitor
|
|
else
|
|
{
|
|
//dbg msg
|
|
os_log_debug(logHandle, "will (re)start monitor");
|
|
|
|
//start
|
|
[((AppDelegate*)[[NSApplication sharedApplication] delegate]).avMonitor start];
|
|
}
|
|
|
|
break;
|
|
|
|
//rules
|
|
case rules:
|
|
|
|
//show rules
|
|
[((AppDelegate*)[[NSApplication sharedApplication] delegate]) showRules:nil];
|
|
|
|
break;
|
|
|
|
//prefs
|
|
case prefs:
|
|
|
|
//show prefs
|
|
[((AppDelegate*)[[NSApplication sharedApplication] delegate]) showPreferences:nil];
|
|
|
|
break;
|
|
|
|
//prefs
|
|
case quit:
|
|
|
|
//exit
|
|
[NSApp terminate:self];
|
|
|
|
break;
|
|
|
|
//uninstall
|
|
case uninstall:
|
|
{
|
|
//uninstaller path
|
|
NSURL* uninstaller = nil;
|
|
|
|
//config options
|
|
NSWorkspaceOpenConfiguration* configuration = nil;
|
|
|
|
//init path to uninstaller
|
|
uninstaller = [NSBundle.mainBundle URLForResource:@"OverSight Installer" withExtension:@".app"];
|
|
if(nil == uninstaller)
|
|
{
|
|
//err msg
|
|
os_log_debug(logHandle, "failed to find uninstaller");
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
//init configuration
|
|
configuration = [[NSWorkspaceOpenConfiguration alloc] init];
|
|
|
|
//set args
|
|
configuration.arguments = @[CMD_UNINSTALL_VIA_UI];
|
|
|
|
//dbg msg
|
|
os_log_debug(logHandle, "launching uninstaller %{public}@", uninstaller);
|
|
|
|
@try
|
|
{
|
|
|
|
//launch (in)/(un)installer
|
|
[NSWorkspace.sharedWorkspace openApplicationAtURL:uninstaller configuration:configuration completionHandler:^(NSRunningApplication * _Nullable app, NSError * _Nullable error) {
|
|
|
|
//dbg msg
|
|
os_log_debug(logHandle, "launched uninstaller: %{public}@ (error: %{public}@)", app, error);
|
|
|
|
}];
|
|
|
|
}
|
|
@catch(NSException *exception)
|
|
{
|
|
//err msg
|
|
os_log_debug(logHandle, "failed to launch task (%{public}@)", exception);
|
|
|
|
//bail
|
|
goto bail;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
|
|
break;
|
|
}
|
|
|
|
bail:
|
|
|
|
return;
|
|
}
|
|
|
|
//set menu status
|
|
// logic based on 'isEnabled' iVar
|
|
-(void)setState
|
|
{
|
|
//dbg msg
|
|
os_log_debug(logHandle, "setting state to: %@", (self.isDisabled) ? @"disabled" : @"enabled");
|
|
|
|
//set to disabled
|
|
if(YES == self.isDisabled)
|
|
{
|
|
//update status
|
|
[self.statusItem.menu itemWithTag:status].title = [NSString stringWithFormat:@"%@: disabled", PRODUCT_NAME];
|
|
|
|
//set icon
|
|
self.statusItem.button.image = [NSImage imageNamed:@"StatusInactive"];
|
|
self.statusItem.button.image.template = YES;
|
|
|
|
//change toggle text
|
|
[self.statusItem.menu itemWithTag:toggle].title = @"Enable";
|
|
}
|
|
|
|
//set to enabled
|
|
else
|
|
{
|
|
//update status
|
|
[self.statusItem.menu itemWithTag:status].title = [NSString stringWithFormat:@"%@: enabled", PRODUCT_NAME];
|
|
|
|
//set icon
|
|
self.statusItem.button.image = [NSImage imageNamed:@"StatusActive"];
|
|
self.statusItem.button.image.template = YES;
|
|
|
|
//change toggle text
|
|
[self.statusItem.menu itemWithTag:toggle].title = @"Disable";
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
@end
|