views:

665

answers:

4

It seems that if I want to create a very basic Cocoa application with a dock icon and the like, I would have to use Xcode and the GUI builder (w/ PyObjC).

The application I am intending to write is largely concerned with algorithms and basic IO - and thus, not mostly related to Apple specific stuff.

Basically the application is supposed to run periodically (say, every 3 minutes) .. pull some information via AppleScript and write HTML files to a particular directory. I would like to add a Dock icon for this application .. mainly to showing the "status" of the process (for example, if there is an error .. the dock icon would have a red flag on it). Another advantage of the dock icon is that I can make it run on startup.

Additional bonus for defining the dock right-click menu in a simple way (eg: using Python lists of callables).

Can I achieve this without using Xcode or GUI builders but simply using Emacs and Python?

A: 

The closest I found was EasyDialogs but unfortunately that does not have anything to do with dock icons or dock menu.

Sridhar Ratnakumar
... and EasyDialogs is one of the deprecated Macintosh modules removed in Python 3.
Ned Deily
+2  A: 

PyObjC, which is included with Mac OS X 10.5 and 10.6, is pretty close to what you're looking for.

Chuck
Sure I know about PyObjC. The tutorial developer.apple.com/cocoa/pyobjc.html suggests the use of Xcode and its GUI builder .. whereas all I want is a module (as easy as EasyDialogs) that can be used in Python without extra tools.
Sridhar Ratnakumar
@Sridhar: If you look at the PyObjC site I linked to, you'll see it doesn't require Xcode at all.
Chuck
The documentation presumes that one needs Xcode. http://pyobjc.sourceforge.net/documentation/pyobjc-core/tutorial/index.html
Sridhar Ratnakumar
Also note that I am not implying that "pyobjc requires xcode", just that the tutorials and docs are written using Xcode and GUI builders .. whereas I am wanting to write pure Python in a standalone text-editor to create a simple application with dock icon and menu. I hope you understand the specifics of my question.
Sridhar Ratnakumar
@Sridhar: It says you need Xcode installed in order to have the necessary supporting tools. And yes, you do need to use Interface Builder if you want to build an interface. But it doesn't require you to use Xcode at any point. You can write Python in a standalone text editor and create a simple application with a dock icon and a menu.
Chuck
A: 

Chuck is correct about PyObjC.

You should then read about this NSApplication method to change your icon.

-(void)setApplicationIconImage:(NSImage *)anImage;

For the dock menu, implement the following in an application delegate. You can build an NSMenu programmatically to avoid using InterfaceBuilder.

-(NSMenu *)applicationDockMenu:(NSApplication *)sender;

nall
+4  A: 

Install the latest py2app, then make a new directory -- cd to it -- in it make a HelloWorld.py file such as:

# generic Python imports
import datetime
import os
import sched
import sys
import tempfile
import threading
import time

# need PyObjC on sys.path...:
for d in sys.path:
  if 'Extras' in d:
    sys.path.append(d + '/PyObjC')
    break

# objc-related imports
import objc
from Foundation import *
from AppKit import *
from PyObjCTools import AppHelper

# all stuff related to the repeating-action
thesched = sched.scheduler(time.time, time.sleep)

def tick(n, writer):
  writer(n)
  thesched.enter(20.0, 10, tick, (n+1, writer))
  fd, name = tempfile.mkstemp('.txt', 'hello', '/tmp');
  print 'writing %r' % name
  f = os.fdopen(fd, 'w')
  f.write(datetime.datetime.now().isoformat())
  f.write('\n')
  f.close()

def schedule(writer):
  pool = NSAutoreleasePool.alloc().init()
  thesched.enter(0.0, 10, tick, (1, writer))
  thesched.run()
  # normally you'd want pool.drain() here, but since this function never
  # ends until end of program (thesched.run never returns since each tick
  # schedules a new one) that pool.drain would never execute here;-).

# objc-related stuff
class TheDelegate(NSObject):

  statusbar = None
  state = 'idle'

  def applicationDidFinishLaunching_(self, notification):
    statusbar = NSStatusBar.systemStatusBar()
    self.statusitem = statusbar.statusItemWithLength_(
        NSVariableStatusItemLength)
    self.statusitem.setHighlightMode_(1)
    self.statusitem.setToolTip_('Example')
    self.statusitem.setTitle_('Example')

    self.menu = NSMenu.alloc().init()
    menuitem = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(
        'Quit', 'terminate:', '')
    self.menu.addItem_(menuitem)
    self.statusitem.setMenu_(self.menu)

  def writer(self, s):
    self.badge.setBadgeLabel_(str(s))


if __name__ == "__main__":
  # prepare and set our delegate
  app = NSApplication.sharedApplication()
  delegate = TheDelegate.alloc().init()
  app.setDelegate_(delegate)
  delegate.badge = app.dockTile()
  delegate.writer(0)

  # on a separate thread, run the scheduler
  t = threading.Thread(target=schedule, args=(delegate.writer,))
  t.setDaemon(1)
  t.start()

  # let her rip!-)
  AppHelper.runEventLoop()

Of course, in your real code, you'll be performing your own periodic actions every 3 minutes (rather than writing a temp file every 20 seconds as I'm doing here), doing your own status updates (rather than just showing a counter of the number of files written so far), etc, etc, but I hope this example shows you a viable starting point.

Then in Terminal.App cd to the directory containing this source file, py2applet --make-setup HelloWorld.py, python setup.py py2app -A -p PyObjC.

You now have in subdirectory dist a directory HelloWorld.app; open dist and drag the icon to the Dock, and you're all set (on your own machine -- distributing to other machines may not work due to the -A flag, but I had trouble building without it, probably due to mis-installed egg files laying around this machine;-). No doubt you'll want to customize your icon &c.

This doesn't do the "extra credit" thingy you asked for -- it's already a lot of code and I decided to stop here (the extra credit thingy may warrant a new question). Also, I'm not quite sure that all the incantations I'm performing here are actually necessary or useful; the docs are pretty latitant for making a pyobjc .app without Xcode, as you require, so I hacked this together from bits and pieces of example code found on the net plus a substantial amount of trial and error. Still, I hope it helps!-)

Alex Martelli
Thank you; that gives a good start for me.
Sridhar Ratnakumar
You'll want to drain the NSAutoreleasePool created in schedule() at the end of that function with pool.drain().
nall
@nail, schedule never ends, because scheduler.run never returns (since every tick schedules another), so I thought it made no sense to put anything after the call to scheduler.run - think there should be a comment there to clarify that...?
Alex Martelli
On second thought, I went ahead and added the comment, as it does make the program more understandable anyway.
Alex Martelli
I don't know exactly how the sched module works, but according to the memory management rules of Cocoa you've alloced, and therefore must release (ie. NSAutoreleasePool drain). I would argue putting it there anyway. What if tick() changes and no longer schedules infinitely? This avoids being burnt by things like that.
nall