Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
branch: master
Fetching contributors…

Cannot retrieve contributors at this time

558 lines (450 sloc) 18.741 kb
# MusicPlayer, https://github.com/albertz/music-player
# Copyright (c) 2012, Albert Zeyer, www.az2000.de
# All rights reserved.
# This code is under the 2-clause BSD license, see License.txt in the root directory of this project.
import sys, os
if sys.platform != "darwin":
print "GUI: your platform is probably not supported yet"
from _guiCocoa import *
from guiCocoaCommon import *
from utils import *
from weakref import ref
import Traits
def setupAppleMenu():
# http://www.cocoabuilder.com/archive/cocoa/192181-initializing-the-menubar-without-interface-builder.html
# By Robert Nikander
mainMenu = AppKit.NSMenu.alloc().initWithTitle_("MainMenu")
mi = mainMenu.addItemWithTitle_action_keyEquivalent_("Apple", None, "")
m = AppKit.NSMenu.alloc().initWithTitle_("Apple")
# strange hack
AppKit.NSApp.setAppleMenu_(m)
mainMenu.setSubmenu_forItem_(m, mi)
m.addItemWithTitle_action_keyEquivalent_('About MusicPlayer', 'about:', '')
m.addItem_(AppKit.NSMenuItem.separatorItem())
m.addItemWithTitle_action_keyEquivalent_('Preferences...', 'openPrefWindow:', ',')
m.addItem_(AppKit.NSMenuItem.separatorItem())
m.addItemWithTitle_action_keyEquivalent_('Hide MusicPlayer', 'hide:', 'h')
mii = m.addItemWithTitle_action_keyEquivalent_('Hide Others', 'hideOtherApplications:', 'h')
mii.setKeyEquivalentModifierMask_(AppKit.NSAlternateKeyMask|AppKit.NSCommandKeyMask)
m.addItemWithTitle_action_keyEquivalent_('Show All', 'unhideAllApplications:', '')
m.addItem_(AppKit.NSMenuItem.separatorItem())
m.addItemWithTitle_action_keyEquivalent_('Quit', 'terminate:', 'q')
# new supermenu
mi = mainMenu.addItemWithTitle_action_keyEquivalent_("Edit", None, "")
m = AppKit.NSMenu.alloc().initWithTitle_("Edit")
mainMenu.setSubmenu_forItem_(m, mi)
m.addItemWithTitle_action_keyEquivalent_('Cut', 'cut:', 'x')
m.addItemWithTitle_action_keyEquivalent_('Copy', 'copy:', 'c')
m.addItemWithTitle_action_keyEquivalent_('Paste', 'paste:', 'v')
m.addItemWithTitle_action_keyEquivalent_('Select all', 'selectText:', 'a')
# new supermenu
mi = mainMenu.addItemWithTitle_action_keyEquivalent_("Window", None, "")
m = AppKit.NSMenu.alloc().initWithTitle_("Window")
mainMenu.setSubmenu_forItem_(m, mi)
m.addItemWithTitle_action_keyEquivalent_('Main window', 'openMainWindow:', '1')
m.addItemWithTitle_action_keyEquivalent_('Search window', 'openSearchWindow:', '2')
m.addItemWithTitle_action_keyEquivalent_('Song edit window', 'openSongEditWindow:', 'i')
m.addItem_(AppKit.NSMenuItem.separatorItem())
m.addItemWithTitle_action_keyEquivalent_('Minimize window', 'miniaturize:', 'm')
m.addItemWithTitle_action_keyEquivalent_('Close window', 'performClose:', 'w')
# new supermenu
mi = mainMenu.addItemWithTitle_action_keyEquivalent_("Control", None, "")
m = AppKit.NSMenu.alloc().initWithTitle_("Control")
mainMenu.setSubmenu_forItem_(m, mi)
m.addItemWithTitle_action_keyEquivalent_('no song yet', '', '')
m.addItemWithTitle_action_keyEquivalent_('Play', 'playPause:', '')
m.addItemWithTitle_action_keyEquivalent_('Next song', 'nextSong:', '')
AppKit.NSApp.delegate().setDockMenu_(m)
AppKit.NSApp.setDockMenu_(m)
# new supermenu
mi = mainMenu.addItemWithTitle_action_keyEquivalent_("Debug", None, "")
m = AppKit.NSMenu.alloc().initWithTitle_("Debug")
mainMenu.setSubmenu_forItem_(m, mi)
m.addItemWithTitle_action_keyEquivalent_('reset player', 'resetPlayer:', '')
AppKit.NSApp.setMainMenu_(mainMenu)
def setupAfterAppFinishedLaunching():
setupAppleMenu()
setupMainWindow()
AppKit.NSApp.updateWindows()
import main
main.handleApplicationInit()
def getWindow(name):
global windows
if windows.get(name, None):
return windows[name].nativeGuiObject.window()
return None
def buildControlAction(control):
button = AppKit.NSButton.alloc().initWithFrame_(((0,0), (50.0, 25.0)))
button.setBezelStyle_(AppKit.NSRoundedBezelStyle)
actionTarget = ButtonActionHandler.alloc().initWithArgs(control.attr, control.parent.subjectObject)
control.buttonActionHandler = actionTarget # keep ref here. button.target() is only a weakref
button.setTarget_(actionTarget)
button.setAction_("click")
def do_update(): button.setTitle_(control.attr.name.decode("utf-8"))
do_update()
button.sizeToFit() # to get height
#button.setFrameSize_((50, button.frame().size.height))
def update(): do_in_mainthread(do_update, wait=False)
control.nativeGuiObject = button
control.updateContent = update
return control
def backgroundColor(control):
if any([(c.attr and c.attr.highlight) for c in control.allParents()]):
return AppKit.NSColor.blueColor()
return None
def foregroundColor(control):
if any([(c.attr and c.attr.lowlight) for c in control.allParents()]):
return AppKit.NSColor.disabledControlTextColor()
return AppKit.NSColor.blackColor()
def buildControlEditableText(control):
w = control.attr.width or 30
h = control.attr.height or 22
label = NSExtendedTextField.alloc().initWithFrame_(((0, 0), (w, h)))
if control.attr.searchLook:
label.setCell_(AppKit.NSSearchFieldCell.alloc().init())
label.setBordered_(False)
label.setBezeled_(True)
label.setBezelStyle_(AppKit.NSTextFieldRoundedBezel)
label.setDrawsBackground_(True)
label.setEditable_(True)
label.cell().setUsesSingleLineMode_(True)
#label.cell().setLineBreakMode_(NSLineBreakByTruncatingTail)
control.nativeGuiObject = label
control.getTextObj = lambda: control.subjectObject()
def update():
control.subjectObject = control.attr.__get__(control.parent.subjectObject)
s = "???"
try:
labelContent = control.getTextObj()
s = convertToUnicode(labelContent)
except Exception:
sys.excepthook(*sys.exc_info())
def do_update():
label.setStringValue_(s)
do_in_mainthread(do_update, wait=False)
control.updateContent = update
def onTextChange():
try:
control.subjectObject = control.attr.__get__(control.parent.subjectObject)
newText = unicode(label.stringValue())
control.subjectObject(updateText = newText)
except Exception:
sys.excepthook(*sys.exc_info())
label.onTextChange = onTextChange
return control
# used by buildControlList for the list items
class ListItem_AttrWrapper(UserAttrib):
def __init__(self, index, value, parent):
UserAttrib.__init__(self)
self.index = index
self.value = value
def __get__(self, inst):
return self.value
def buildControlTable(control):
scrollview = AppKit.NSScrollView.alloc().initWithFrame_(((0.0, 0.0), (80.0, 80.0)))
scrollview.setAutoresizingMask_(AppKit.NSViewWidthSizable|AppKit.NSViewHeightSizable)
scrollview.contentView().setAutoresizingMask_(AppKit.NSViewWidthSizable|AppKit.NSViewHeightSizable)
scrollview.setHasVerticalScroller_(True)
scrollview.setDrawsBackground_(False)
scrollview.setBorderType_(AppKit.NSBezelBorder)
view = NSFlippedView.alloc().initWithFrame_(scrollview.frame())
view.setAutoresizingMask_(AppKit.NSViewWidthSizable|AppKit.NSViewHeightSizable)
view.addSubview_(scrollview)
view.control = ref(control)
control.nativeGuiObject = view
table = AppKit.NSTableView.alloc().initWithFrame_(((0,0),(80,80)))
scrollview.setDocumentView_(table)
scrollview.documentView().setAutoresizingMask_(AppKit.NSViewWidthSizable)
#array = NSArrayController.alloc().init()
dataSource = TableViewDataSource.alloc().init()
dataSource.data = []
dataSource.formaters = control.attr.type.formaters
control.tableDataSource = dataSource # save ref here because table.dataSource() is only a weakref
table.setDataSource_(dataSource)
if control.attr.selectionChangeHandler:
delegate = TableViewDelegate.alloc().init()
delegate.onSelectionChange = lambda sel: control.attr.selectionChangeHandler(control.parent.subjectObject, sel)
control.tableDelegate = delegate
table.setDelegate_(delegate)
table.setColumnAutoresizingStyle_(AppKit.NSTableViewUniformColumnAutoresizingStyle)
for key in control.attr.type.keys:
column = AppKit.NSTableColumn.alloc().initWithIdentifier_(key)
column.headerCell().setStringValue_(convertToUnicode(key.capitalize())) # title
column.setEditable_(False)
column.setMinWidth_(30)
column.setSortDescriptorPrototype_(AppKit.NSSortDescriptor.sortDescriptorWithKey_ascending_(key, True))
table.addTableColumn_(column)
table.setAllowsMultipleSelection_(True)
table.setAutosaveName_(control.name)
table.setAutosaveTableColumns_(True)
def update():
control.subjectObject = control.attr.__get__(control.parent.subjectObject)
value = control.subjectObject
def setData():
with dataSource.lock:
# Do this in main thread to workaround the problem that
# the tableView knows the wrong number of rows. This could
# happen otherwise because it checks the number of rows in
# its redrawing code and that is not covered by this lock.
# See also the related comment in TableViewDataSource.
dataSource.data = value
dataSource.resort(table) # initial sort
table.reloadData()
do_in_mainthread(setData, wait=False)
control.updateContent = update
update() # initial fill
return control
def buildControlReal(control):
w,h = control.attr.width, control.attr.height
if not w: w = 70
if not h: h = 20
slider = NSExtendedSlider.alloc().initWithFrame_(((0.0, 0.0), (w, h)))
slider.setMinValue_(control.attr.type.min)
slider.setMaxValue_(control.attr.type.max)
slider.setNumberOfTickMarks_(3)
control.nativeGuiObject = slider
def update():
control.subjectObject = control.attr.__get__(control.parent.subjectObject)
value = control.subjectObject
do_in_mainthread(lambda: slider.setDoubleValue_(value), wait=False)
control.updateContent = update
def onValueChange(newValue):
control.attr.__set__(control.parent.subjectObject, newValue)
slider.onValueChange = onValueChange
return control
def SongDisplayView_MouseClickCallback(x):
from State import state
song = state.player.curSong
if not song: return
if not song.duration: return
if song.duration < 0: return
state.player.seekAbs(x * song.duration)
def buildControlSongDisplay(control):
userAttr = control.attr
inst = control.parent.subjectObject
try:
class SongDisplayView(AppKit.NSBox):
def mouseDown_(self, event):
location = self.convertPoint_fromView_(event.locationInWindow(), None)
if AppKit.NSPointInRect(location, self.bounds()):
x = float(location.x) / self.bounds().size.width
if x < 0 or x > 1: return
SongDisplayView_MouseClickCallback(x)
except Exception:
SongDisplayView = objc.lookUpClass("SongDisplayView") # already defined earlier
subview = SongDisplayView.alloc().initWithFrame_(((10.0, 10.0), (80.0, 80.0)))
subview.setTitlePosition_(AppKit.NSNoTitle)
#subview.setContentViewMargins_((0,0))
imgview = AppKit.NSImageView.alloc().initWithFrame_(subview.contentView().bounds())
imgview.setImageScaling_(AppKit.NSScaleToFit)
imgview2 = AppKit.NSImageView.alloc().initWithFrame_(((0,0), (10, subview.contentView().bounds().size.height)))
imgview2.setImageScaling_(AppKit.NSScaleToFit)
subview.contentView().addSubview_(imgview)
subview.contentView().addSubview_(imgview2)
imgview.setAutoresizingMask_(AppKit.NSViewWidthSizable|AppKit.NSViewHeightSizable)
imgview2.setAutoresizingMask_(AppKit.NSViewHeightSizable|AppKit.NSViewMinXMargin|AppKit.NSViewMaxXMargin)
from threading import Lock
from State import state
class SongDisplay:
def __init__(self):
self.lock = Lock()
self.curSong = None
def initSongCursorImg(self):
img2 = AppKit.NSImage.alloc().initWithSize_((5,1))
img2.lockFocus()
for i in range(5):
a = 100 - abs(i - 2) * 50
AppKit.NSColor.colorWithDeviceRed_green_blue_alpha_(0.0,0.0,0.0,a).setFill()
AppKit.NSBezierPath.fillRect_(((i,0),(1,1)))
img2.unlockFocus()
do_in_mainthread(lambda: imgview2.setImage_(img2))
def setSongBitmap(self, bmpData, wait=True):
with self.lock:
if state.player.curSong is not self.curSong: return None
data = AppKit.NSData.alloc().initWithBytes_length_(bmpData, len(bmpData))
img = AppKit.NSImage.alloc().initWithData_(data)
do_in_mainthread(lambda: imgview.setImage_(img), wait=wait)
def getBmpData(self):
better_exchook.install()
pool = AppKit.NSAutoreleasePool.alloc().init() # for setSongBitmap
bmpData = None
with self.lock:
if state.player.curSong is not self.curSong: return None
if getattr(self.curSong, "bmpThumbnail", None):
bmpData = self.curSong.bmpThumbnail
else:
# create song copy for calcBitmapThumbnail
from Song import Song
song = Song(url=self.curSong.url)
if bmpData:
self.setSongBitmap(bmpData)
del pool
return
do_in_mainthread(lambda: imgview.setImage_(None), wait=False)
def doBmpCalc(queue):
try:
def calcBmpCallback(song, completion, duration, bmpData):
if subview.window() is None: return False # window was closed
with self.lock:
if song != self.curSong: return False
queue.put((duration, bmpData))
return True
song._useDb = False
song.openFile()
import musicplayer
bmpThumbRet = musicplayer.calcBitmapThumbnail(song, 600, 81, procCallback = calcBmpCallback)
if bmpThumbRet:
queue.put(bmpThumbRet)
except Exception:
print "doBmpCalc raised exception"
sys.excepthook(*sys.exc_info())
queue.put(None)
queue = AsyncTask(func=doBmpCalc, name="doBmpCalc for Cocoa")
while True:
bmpThumbRet = queue.get()
if bmpThumbRet is None: break
duration, bmpData = bmpThumbRet
with self.lock:
self.curSong.duration = duration
self.curSong.bmpThumbnail = bmpData
self.setSongBitmap(bmpData, wait=False)
del pool
def playCursorUpdater(self):
better_exchook.install()
pool = AppKit.NSAutoreleasePool.alloc().init()
def updateCursor():
with self.lock:
if self.curSong is None: return
if state.player.curSong is not self.curSong: return
w = imgview2.frame().size.width
h = imgview2.frame().size.height
x = subview.contentView().bounds().size.width * state.player.curSongPos / self.curSong.duration - w / 2
y = imgview2.frame().origin.y
imgview2.setFrame_(((x,y),(w,h)))
import time
i = 0
while True:
i += 1
time.sleep(0.1)
if subview.window() is None: return # window was closed
with self.lock:
if self.curSong is None: continue
if self.curSong is not state.player.curSong: continue
do_in_mainthread(updateCursor, wait=False)
# another hack: update time
control.parent.childs["curSongPos"].updateContent()
del pool
def update(self):
#if ev is PlayerEventCallbacks.onSongChange:
with self.lock:
if self.curSong is state.player.curSong: return # song not changed
self.curSong = state.player.curSong
if not self.curSong:
do_in_mainthread(lambda: imgview.setImage_(None), wait=False)
return
utils.daemonThreadCall(self.getBmpData, name="GUI song bitmap loader")
songDisplay = SongDisplay()
songDisplay.initSongCursorImg()
utils.daemonThreadCall(songDisplay.playCursorUpdater, name="GUI play cursor updater")
control.nativeGuiObject = subview
control.updateContent = songDisplay.update
return control
@DoInMainthreadDecorator
def buildControl(userAttr, parent):
control = CocoaGuiObject()
control.root = parent.root
control.parent = parent
control.attr = userAttr
control.subjectObject = userAttr.__get__(parent.subjectObject)
typeName = userAttr.getTypeClass().__name__
assert userAttr.getTypeClass() is getattr(Traits, typeName)
buildFuncName = "buildControl" + typeName
buildFunc = globals().get(buildFuncName, None)
if buildFunc:
return buildFunc(control)
else:
raise NotImplementedError, "%r not handled yet" % userAttr.type
try:
windows
except NameError:
windows = {}
def setupWindow(subjectObject, windowName, title, isMainWindow=False):
# some example code: http://lists.apple.com/archives/cocoa-dev/2004/Jan/msg01389.html
# also, these might be helpful:
# https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ControlCell/ControlCell.html#//apple_ref/doc/uid/10000015i
# http://cocoadev.com/wiki/FlowLayoutView
assert AppKit.NSThread.isMainThread()
if getWindow(windowName):
getWindow(windowName).makeKeyAndOrderFront_(None)
return
win = AppKit.NSWindow.alloc()
win.initWithContentRect_styleMask_backing_defer_(
((200.0, 500.0), (400.0, 600.0)),
AppKit.NSTitledWindowMask |
AppKit.NSClosableWindowMask |
AppKit.NSMiniaturizableWindowMask |
AppKit.NSResizableWindowMask,
AppKit.NSBackingStoreBuffered, False)
win.setContentView_(NSFlippedView.alloc().init())
win.contentView().setAutoresizingMask_(AppKit.NSViewWidthSizable|AppKit.NSViewHeightSizable)
win.setTitle_(title)
window = CocoaGuiObject()
window.root = window
window.subjectObject = subjectObject
window.nativeGuiObject = win.contentView()
assert window.root.nativeGuiObject.window() is not None
w,h = window.setupChilds()
win.setContentMinSize_((w,h))
win.setFrameUsingName_(windowName)
win.setFrameAutosaveName_(windowName)
win.display()
win.orderFrontRegardless()
win.makeMainWindow()
win.makeKeyWindow()
AppKit.NSApp.activateIgnoringOtherApps_(True)
# see http://stackoverflow.com/questions/12292151/crash-in-class-getname-in-applicationopenuntitledfile
win.retain()
global windows
windows[windowName] = window
def setupMainWindow():
from State import state
import appinfo
setupWindow(state, windowName="mainWindow", title=appinfo.progname, isMainWindow=True)
def setupSearchWindow():
from Search import search
setupWindow(search, windowName="searchWindow", title="Search")
def setupSongEditWindow():
from SongEdit import SongEdit
import gui
ctx = gui.ctx()
if not getattr(ctx, "songEdit", None):
ctx.songEdit = SongEdit(ctx)
setupWindow(ctx.songEdit, windowName="songEditWindow", title="Song edit")
def setupPrefWindow():
from Preferences import prefs
setupWindow(prefs, windowName="prefWindow", title="Preferences")
def locateFile(filename):
ws = AppKit.NSWorkspace.sharedWorkspace()
ws.selectFile_inFileViewerRootedAtPath_(filename, None)
def guiMain():
from player import PlayerEventCallbacks
pool = AppKit.NSAutoreleasePool.alloc().init()
from State import state
for ev,args,kwargs in state.updates.read():
try:
# Note: This shouldn't be needed. Each GUI object should
# get an event itself about some change.
global windows
for w in windows.values():
w.updateContent()
if ev is PlayerEventCallbacks.onPlayingStateChange or ev is PlayerEventCallbacks.onSongChange:
updateControlMenu()
except Exception:
sys.excepthook(*sys.exc_info())
del pool
if "isReload" in globals():
isReload = True
else:
isReload = False
if isReload:
print "Reloading GUI not really supported..."
Jump to Line
Something went wrong with that request. Please try again.