Save preference to show or hide NSStatusItem - objective-c

I've got an application which runs as a normal app but also has a NSStausItem.
I wanted to implement the ability to set in the preferences a checkbox and when this checkbox is turned on the status item should be shown, but when the checkbox is off the status item should be removed or be invisible.
I found someone facing a similar problem in a forum here: How do you toggle the status item in the menubar on and off using a checkbox?
But the problem I have with this solution is that it does not work as expected. So I make this checkbox and all works fine, but when I open the application a second time the app does not recognize the choice I took at the first run. This is because the checkbox isn't bound to a BOOL or something, the checkbox only has an IBAction, which removes or adds the status item at runtime.
So my question is: how can I make a checkbox in the preferences which allows me to choose whether the status item should show up or not.
Ok actually i tried the following i copied the from the post i gave you the link
In AppDelegate.h :
NSStatusItem *item;
NSMenu *menu;
IBOutlet NSButton myStatusItemCheckbox;
and then in the Delegate.m :
- (BOOL)createStatusItem
{
NSStatusBar *bar = [NSStatusBar systemStatusBar];
//Replace NSVariableStatusItemLength with NSSquareStatusItemLength if you
//want the item to be square
item = [bar statusItemWithLength:NSVariableStatusItemLength];
if(!item)
return NO;
//As noted in the docs, the item must be retained as the receiver does not
//retain the item, so otherwise will be deallocated
[item retain];
//Set the properties of the item
[item setTitle:#"MenuItem"];
[item setHighlightMode:YES];
//If you want a menu to be shown when the user clicks on the item
[item setMenu:menu]; //Assuming 'menu' is a pointer to an NSMenu instance
return YES;
}
- (void)removeStatusItem
{
NSStatusBar *bar = [NSStatusBar systemStatusBar];
[bar removeStatusItem:item];
[item release];
}
- (IBAction)toggleStatusItem:(id)sender
{
BOOL checked = [sender state];
if(checked) {
BOOL createItem = [self createStatusItem];
if(!createItem) {
//Throw an error
[sender setState:NO];
}
}
else
[self removeStatusItem];
}
then in the IBaction i added this one :
[[NSUserDefaults standardUserDefaults] setInteger:[sender state]
forKey:#"MyApp_ShouldShowStatusItem"];
and in my awakefromnib i added this one : `
NSInteger statusItemState = [[NSUserDefaults standardUserDefaults] integerForKey:#"MyApp_ShouldShowStatusItem"];
[myStatusItemCheckbox setState:statusItemState];
Then in the interface builder i created a new checkbox connected it with "myStatusItemCheckbox" and added an IBaction also i clicked on the bindings inspector and set in the value the following bind to : NSUserDefaultController and as ModelKeyPath i set: MyApp_ShouldShowStatusItem.
Unfortunately this doesnt work at all what am i doing wrong ?

What you need to do is to use the User Defaults system. It makes it very easy to save and load preferences.
In the button's action, you will save its state:
- (IBAction)toggleStatusItem:(id)sender {
// Your existing code...
// A button's state is actually an NSInteger, not a BOOL, but
// you can save it that way if you prefer
[[NSUserDefaults standardUserDefaults] setInteger:[sender state]
forKey:#"MyApp_ShouldShowStatusItem"];
}
and in your app delegate's (or another appropriate object) awakeFromNib, you will read that value back out of the user defaults:
NSInteger statusItemState = [[NSUserDefaults standardUserDefaults] integerForKey:#"MyApp_ShouldShowStatusItem"];
[myStatusItemCheckbox setState:statusItemState];
and then make sure to call removeStatusItem if neccessary.
This procedure will apply to almost any preference you might want to save.

Related

context menu based on NSTableView cell

i would like to place a context menu onto a NSTableView. this part is done. what i would like to do is to show different menu entries based on the content of the right clicked cell, and do NOT show the context menu for specific columns.
this is:
column 0, and 1 no context menu
all other cells should have the context menu like this:
first entry: "delete " samerow.column1.value
second entry: "save " samecolumn.headertext
-EDIT-
the one on the right is how the context menu is supposed to look like for any given cell.
Theres a delegate for that! - No need to subclass
In IB if you drag an NSTableView onto your window/view you'll notice that theres a menu outlet for the table.
So a very easy way to implement the contextual menu is to connect that outlet to a stub menu and connect the delegate outlet of the menu to an object which implements the NSMenuDelegate protocol method - (void)menuNeedsUpdate:(NSMenu *)menu
Normally the delegate of the menu is the same object which provides the datasource/delegates to the table but it might also be the view controller which owns the table too.
Have a look at the docs for more info on this
Theres a bundle of clever stuff you can do in the protocol but a very simple implementation might be like below
#pragma mark tableview menu delegates
- (void)menuNeedsUpdate:(NSMenu *)menu
{
NSInteger clickedrow = [mytable clickedRow];
NSInteger clickedcol = [mytable clickedColumn];
if (clickedrow > -1 && clickedcol > -1) {
//construct a menu based on column and row
NSMenu *newmenu = [self constructMenuForRow:clickedrow andColumn:clickedcol];
//strip all the existing stuff
[menu removeAllItems];
//then repopulate with the menu that you just created
NSArray *itemarr = [NSArray arrayWithArray:[newmenu itemArray]];
for(NSMenuItem *item in itemarr)
{
[newmenu removeItem:[item retain]];
[menu addItem:item];
[item release];
}
}
}
And then a method to construct the menu.
-(NSMenu *)constructMenuForRow:(int)row andColumn:(int)col
{
NSMenu *contextMenu = [[[NSMenu alloc] initWithTitle:#"Context"] autorelease];
NSString *title1 = [NSString stringWithFormat:#"Delete %#",[self titleForRow:row]];
NSMenuItem *item1 = [[[NSMenuItem alloc] initWithTitle:title1 action:#selector(deleteObject:) keyEquivalent:#""] autorelease];
[contextMenu addItem:item1];
//
NSString *title2 = [NSString stringWithFormat:#"Save %#",[self titleForColumn:col]];
NSMenuItem *item2 = [[[NSMenuItem alloc] initWithTitle:title1 action:#selector(saveObject:) keyEquivalent:#""] autorelease];
[contextMenu addItem:item2];
return contextMenu;
}
How you choose to implement titleForRow: and titleForColumn: is up to you.
Note that NSMenuItem provides the property representedObject to allow you to bind an arbitrary object to the menu item and hence send information into your method (e.g deleteObject:)
EDIT
Watch out - implementing - (void)menuNeedsUpdate:(NSMenu *)menu in your NSDocument subclass will stop the Autosave/Versions menu that appears in the title bar appearing in 10.8.
It still works in in 10.7 so go figure. In any case the menu delegate will need to be something other than your NSDocument subclass.
Edit: The better way to do this than the below method is using delegate as shown in the accepted answer.
You can subclass your UITableView and implement menuForEvent: method:
-(NSMenu *)menuForEvent:(NSEvent *)event{
if (event.type==NSRightMouseDown) {
if (self.selectedColumn == 0 || self.selectedColumn ==1) {
return nil;
}else {
//create NSMenu programmatically or get a IBOutlet from one created in IB
NSMenu *menu=[[NSMenu alloc] initWithTitle:#"Custom"];
//code to set the menu items
//Instead of the following line get the value from your datasource array/dictionary
//I used this as I don't know how you have implemented your datasource, but this will also work
NSString *deleteValue = [[self preparedCellAtColumn:1 row:self.selectedRow] title];
NSString *deleteString = [NSString stringWithFormat:#"Delete %#",deleteValue];
NSMenuItem *deleteItem = [[NSMenuItem alloc] initWithTitle:deleteString action:#selector(deleteAction:) keyEquivalent:#""];
[menu addItem:deleteItem];
//save item
//similarly
[menu addItem:saveItem];
return menu;
}
}
return nil;
}
That should do it. I haven't tried out the code though. But this should give you an idea.
I also tried the solution posted by Warren Burton and it works fine.
But in my case I had to add the following to the menu items:
[item1 setTarget:self];
[item2 setTarget:self];
Setting no target explicitly causes the context menu to remain disabled.
Cheers!
Alex
PS: I would have posted this as a comment but I do not have enough reputation to do that :(
Warren Burton's answer is spot on. For those working in Swift, the following example fragment might save you the work of translating from Objective C. In my case I was adding the contextual menu to cells in an NSOutlineView rather than an NSTableView. In this example the menu constructor looks at the item and provides different options depending on item type and state. The delegate (set in IB) is a ViewController that manages an NSOutlineView.
func menuNeedsUpdate(menu: NSMenu) {
// get the row/column from the NSTableView (or a subclasse, as here, an NSOutlineView)
let row = outlineView.clickedRow
let col = outlineView.clickedColumn
if row < 0 || col < 0 {
return
}
let newItems = constructMenuForRow(row, andColumn: col)
menu.removeAllItems()
for item in newItems {
menu.addItem(item)
// target this object for handling the actions
item.target = self
}
}
func constructMenuForRow(row: Int, andColumn column: Int) -> [NSMenuItem]
{
let menuItemSeparator = NSMenuItem.separatorItem()
let menuItemRefresh = NSMenuItem(title: "Refresh", action: #selector(refresh), keyEquivalent: "")
let item = outlineView.itemAtRow(row)
if let block = item as? Block {
let menuItem1 = NSMenuItem(title: "Delete \(block.name)", action: #selector(deleteBlock), keyEquivalent: "")
let menuItem2 = NSMenuItem(title: "New List", action: #selector(addList), keyEquivalent: "")
return [menuItem1, menuItem2, menuItemSeparator, menuItemRefresh]
}
if let field = item as? Field {
let menuItem1 = NSMenuItem(title: "Delete \(field.name)", action: #selector(deleteField), keyEquivalent: "")
let menuItem2 = NSMenuItem(title: "New Field", action: #selector(addField), keyEquivalent: "")
return [menuItem1, menuItem2, menuItemSeparator, menuItemRefresh]
}
return [NSMenuItem]()
}
As TheGoonie mentioned, I also got the same experience- context menu items were remained disable. However the reason for items being disabled is 'Auto Enables Items' property.
Make 'Auto Enables Items' property to off. or set it programmatically to NO.
[mTableViewMenu setAutoenablesItems:NO];
Here is an example setting up an NSOutlineView programmatically within a view controller. This is all the plumbing you need to get the context menu up and running. No subclassing required.
I had previously subclassed NSOutlineView to override menu(for event: NSEvent), but came to a simpler set-up with the help of Graham's answer here and Warren's answer above.
class OutlineViewController: NSViewController
{
// ...
var outlineView: NSOutlineView!
var contextMenu: NSMenu!
override func viewDidLoad()
{
// ...
outlineView = NSOutlineView()
contextMenu = NSMenu()
contextMenu.delegate = self
outlineView.menu = contextMenu
}
}
extension OutlineViewController: NSMenuDelegate
{
func menuNeedsUpdate(_ menu: NSMenu) {
// clickedRow catches the right-click here
print("menuNeedsUpdate called. Clicked Row: \(outlineView.clickedRow)")
// ... Flesh out the context menu here
}
}
This is the easiest method for a custom/dynamic NSMenu I found, that also preserves the system look (the blue selection border). Subclass NSTableView and set your menu in menu(for:).
The important part is to set the menu on the table view, but return the menu from its super call.
override func menu(for event: NSEvent) -> NSMenu? {
let point = convert(event.locationInWindow, from: nil)
let clickedRow = self.row(at: point)
var menuRows = selectedRowIndexes
// The blue selection box should always reflect the
// returned row indexes.
if menuRows.isEmpty || !menuRows.contains(clickedRow) {
menuRows = [clickedRow]
}
// Build your custom menu based on the menuRows indexes
self.menu = <#myMenu#>
return super.menu(for: event)
}

How can I copy menu items in Xcode?

So I'm making a web browsing app in Xcode for OS X, and right now I am working on history. I have a menu called history in my MainMenu.xib, and I was wondering if it would be possible to add a menu item (through coding) to that every time the user loads a new page. Any help would be great.
Something like this should work:
- (void)addHistoryItemWithTitle:(NSString *)title URL:(NSURL *)historyURL
NSMenuItem *menuItem = [[[NSMenuItem alloc] initWithTitle:title action:#selector(goToHistoryItem:) keyEquivalent:#""] autorelease];
menuItem.representedObject = historyURL;
//Note: You would normally have an outlet for your History menu or use
// itemWithTag:, so that it works in localized versions of your app.
NSMenuItem *historyMenuItem = [[NSApp mainMenu] itemWithTitle:#"History"];
[[historyMenuItem submenu] addItem:menuItem];
}
In your action you could then retrieve the URL (or some other object) that you set as the representedObject before:
- (void)goToHistoryItem:(id)sender
{
NSURL *historyURL = [sender representedObject];
NSLog(#"history item selected: %#", historyURL);
}

Check if an NSStatusItem is currently shown in the OS X NSStatusBar main menubar

My app puts an NSStatusItem in the OS X menubar. At some point, I want to remove the menubar icon from the system NSStatusBar. (I want to still retain the NSStatusItem at this point, and send messages to it... just not have it shown.)
I'm using this method to remove the statusItem from the statusBar:
[[NSStatusBar systemStatusBar] removeStatusItem:statusItem];
I want to, at some later point, check if the statusItem is currently shown in the statusBar. I would prefer not to keep track of this via a boolean or etc.
I thought this check would work:
if ([[NSStatusBar systemStatusBar] isEqualTo:[statusItem statusBar]])
{
NSLog(#"statusItem's bar == system bar, before");
}
NSLog(#"removing from systemStatusBar");
[[NSStatusBar systemStatusBar] removeStatusItem:statusItem];
if ([[NSStatusBar systemStatusBar] isEqualTo:[statusItem statusBar]])
{
NSLog(#"statusItem's bar == system bar, after removal");
}
This outputs:
statusItem's bar == system bar, before removal
removing from systemStatusBar
statusItem's bar == system bar, after removal
So there is no apparent change in the statusItem' statusBar.
The NSStatusBar class reference does not appear to contain any applicable methods.
Is there any way to check if a certain NSStatusItem is in the main system bar?
I have found the private property _statusItems.
This is a little category I wrote, I'm not sure if it works, but you can try it out.
Status Bar Category
#implementation NSStatusBar (statusItemCheck)
- (NSArray *)items {
return [self valueForKey:#"_statusItems"];
}
- (BOOL)statusItemIsShown:(NSStatusItem *)statusItem {
if ([self items]) {
NSInteger index = [[self items] indexOfObject:statusItem];
if (index != -1) return YES;
}
return NO;
}
#end
Edit
You should consider adding a BOOL flag, rather than accessing private methods.
My category is only an example, if you want to upload your app to the MAS, you generally shouldn't use private methods.

setState not updating a checkbox's view

I'm trying to create a preferences window.
In it I have some checkbox style NSButtons. The problem is that they aren't updating when I call the setState: method. I access the standarUserDefaults when then window is initialized to get the state they should be in and was planning to change them depending on what state that key is in.
I know that they are in fact connected to both their IBOutlet and IBAction as I've tried some NSLog-ing to make sure of that.
I read something about Changing the value of a model property programmatically is not reflected in the user interface here but I'm not sure if that's the problem or frankly what they are referring to there.
I declared the checkbuttons in the .h file like so:
IBOutlet NSButton *defaultDateCheck;
IBOutlet NSButton *closeOnCreationCheck;
IBOutlet NSButton *allowEmptyNumberCheck;
IBOutlet NSPopUpButton *defaultJobSetting;
The init method looks like this:
-(id)initWithWindowNibName:(NSString *)windowNibName{
self = [super initWithWindowNibName:windowNibName];
if (self) {
NSUserDefaults *myDefaults = [NSUserDefaults standardUserDefaults];
[defaultDateCheck setState:[myDefaults boolForKey:#"useDefaultDate"]];
[closeOnCreationCheck setState:[myDefaults boolForKey:#"closeOnCreation"]];
[allowEmptyNumberCheck setState:[myDefaults boolForKey:#"allowEmptyProjectNumber"]];
[defaultJobSetting selectItemAtIndex:[myDefaults integerForKey:#"defaultJob"]];
}
return self;
}
I also tried the following format for setting the checkboxes but with no result:
if ([myDefaults integerForKey:#"useDefaultDate"] == YES) {
[defaultDateCheck setState:NSOnState];
}
else {
[defaultDateCheck setState:NSOffState];
}
The connected IBAction methods for the checkboxes looks like this:
-(IBAction)toggleCloseOnCreation:(id)sender{
[[NSUserDefaults standardUserDefaults] setBool:([closeOnCreationCheck state] == NSOnState) forKey:#"closeOnCreation"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
Set the state in -viewDidLoad method since in -init the view and it's subviews do not exist yet
When setting the value of your default use:
[myDefaults setObject:[NSNumber numberWithBool:YES] forKey:#"YourKey"];
When checking to see if the value is true or false, use:
[[[NSUserDefaults standardUserDefaults] objectForKey:#"YourKey"] boolValue]

Working backwards from null

I know that once you get better at coding you know what variables are and null popping out here and there may not occur. On the way to that state of mind are there any methods to corner your variable that's claiming to be null and verify that it is indeed null, or you just using the wrong code?
Example:
-(IBAction) startMotion: (id)sender {
NSLog(#"Forward or back button is being pressed.");
UIButton * buttonName = (UIButton *) sender;
NSLog(#"Button Name: %#", buttonName.currentTitle);
}
Button Name: (null) is what shows up in the console
Thanks
According to Apple's docs, the value for currentTitle may be nil. It may just not be set.
You can always do if (myObject == nil) to check, or in this case:
-(IBAction) startMotion: (id)sender {
NSLog(#"Forward or back button is being pressed.");
UIButton * buttonName = (UIButton *) sender;
if (buttonName != nil) {
NSString *title = buttonName.currentTitle;
NSLog(#"Button Name: %#", title);
}
}
Another way to check if the back or forward button is pressed, is check the id itself.
//in the interface, and connect it up in IB
//IBOutlet UIButton *fwdButton;
//IBOutlet UIButton *bckButton;
-(IBAction) startMotion: (id)sender {
NSLog(#"Forward or back button is being pressed.");
UIButton * buttonName = (UIButton *) sender;
if (buttonName == fwdButton) {
NSLog(#"FWD Button");
}
if (buttonName == bckButton) {
NSLog(#"BCK Button");
}
}
also, make sure your outlets and actions are all connected in IB, and that you save and re-build the project. I've gone where I changed somehting in IB, saved the .m file (not the nib) and was like "why isn't this working???"
I was using the wrong field in Interface Builder I was using Name from the Interface Builder Identity instead of Title from the button settings.
buttonName cannot be null, otherwise buttonName.currentTitle would produce an error.
Therefore the currentTitle attribute itself must be null.
Or, maybe currentTitle is a string with the value (null).
In general, in Objective-C, if you have [[[myObject aMethod] anotherMethod] xyz] and the result is null it's difficult to know which method returned null. But with the dot syntax . that's not the case.