aboutsummaryrefslogtreecommitdiffstats
path: root/lvc/widgets/osx
diff options
context:
space:
mode:
Diffstat (limited to 'lvc/widgets/osx')
-rw-r--r--lvc/widgets/osx/Resources-Widgets/MainMenu.nib/designable.nib145
-rw-r--r--lvc/widgets/osx/Resources-Widgets/MainMenu.nib/keyedobjects.nibbin0 -> 1609 bytes
-rw-r--r--lvc/widgets/osx/__init__.py74
-rw-r--r--lvc/widgets/osx/base.py367
-rw-r--r--lvc/widgets/osx/const.py44
-rw-r--r--lvc/widgets/osx/contextmenu.py84
-rw-r--r--lvc/widgets/osx/control.py530
-rw-r--r--lvc/widgets/osx/customcontrol.py436
-rw-r--r--lvc/widgets/osx/drawing.py289
-rw-r--r--lvc/widgets/osx/drawingwidgets.py67
-rw-r--r--lvc/widgets/osx/fasttypes.c540
-rw-r--r--lvc/widgets/osx/helpers.py95
-rw-r--r--lvc/widgets/osx/layout.py748
-rw-r--r--lvc/widgets/osx/layoutmanager.py445
-rw-r--r--lvc/widgets/osx/osxmenus.py571
-rw-r--r--lvc/widgets/osx/rect.py78
-rw-r--r--lvc/widgets/osx/simple.py376
-rw-r--r--lvc/widgets/osx/tablemodel.py532
-rw-r--r--lvc/widgets/osx/tableview.py1629
-rw-r--r--lvc/widgets/osx/utils.py2
-rw-r--r--lvc/widgets/osx/viewport.py101
-rw-r--r--lvc/widgets/osx/widgetset.py58
-rw-r--r--lvc/widgets/osx/widgetupdates.py72
-rw-r--r--lvc/widgets/osx/window.py896
-rw-r--r--lvc/widgets/osx/wrappermap.py48
25 files changed, 8227 insertions, 0 deletions
diff --git a/lvc/widgets/osx/Resources-Widgets/MainMenu.nib/designable.nib b/lvc/widgets/osx/Resources-Widgets/MainMenu.nib/designable.nib
new file mode 100644
index 0000000..b7fefd6
--- /dev/null
+++ b/lvc/widgets/osx/Resources-Widgets/MainMenu.nib/designable.nib
@@ -0,0 +1,145 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<archive type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="8.00">
+ <data>
+ <int key="IBDocument.SystemTarget">1060</int>
+ <string key="IBDocument.SystemVersion">12A269</string>
+ <string key="IBDocument.InterfaceBuilderVersion">2549</string>
+ <string key="IBDocument.AppKitVersion">1187</string>
+ <string key="IBDocument.HIToolboxVersion">624.00</string>
+ <object class="NSMutableDictionary" key="IBDocument.PluginVersions">
+ <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="NS.object.0">2549</string>
+ </object>
+ <array key="IBDocument.IntegratedClassDependencies">
+ <string>NSCustomObject</string>
+ <string>NSMenu</string>
+ <string>NSMenuItem</string>
+ </array>
+ <array key="IBDocument.PluginDependencies">
+ <string>com.apple.InterfaceBuilder.CocoaPlugin</string>
+ </array>
+ <object class="NSMutableDictionary" key="IBDocument.Metadata">
+ <string key="NS.key.0">PluginDependencyRecalculationVersion</string>
+ <integer value="1" key="NS.object.0"/>
+ </object>
+ <array class="NSMutableArray" key="IBDocument.RootObjects" id="864178278">
+ <object class="NSCustomObject" id="422340081">
+ <object class="NSMutableString" key="NSClassName">
+ <characters key="NS.bytes">NSApplication</characters>
+ </object>
+ </object>
+ <object class="NSCustomObject" id="99063961">
+ <string key="NSClassName">FirstResponder</string>
+ </object>
+ <object class="NSCustomObject" id="399126242">
+ <string key="NSClassName">NSApplication</string>
+ </object>
+ <object class="NSMenu" id="603720448">
+ <string key="NSTitle">MainMenu</string>
+ <array class="NSMutableArray" key="NSMenuItems">
+ <object class="NSMenuItem" id="726726549">
+ <reference key="NSMenu" ref="603720448"/>
+ <string key="NSTitle">Libre Video Converter</string>
+ <string key="NSKeyEquiv"/>
+ <int key="NSKeyEquivModMask">1048576</int>
+ <int key="NSMnemonicLoc">2147483647</int>
+ <object class="NSCustomResource" key="NSOnImage">
+ <string key="NSClassName">NSImage</string>
+ <string key="NSResourceName">NSMenuCheckmark</string>
+ </object>
+ <object class="NSCustomResource" key="NSMixedImage">
+ <string key="NSClassName">NSImage</string>
+ <string key="NSResourceName">NSMenuMixedState</string>
+ </object>
+ <string key="NSAction">submenuAction:</string>
+ <object class="NSMenu" key="NSSubmenu" id="530441688">
+ <string key="NSTitle">Libre Video Converter</string>
+ <array class="NSMutableArray" key="NSMenuItems"/>
+ <string key="NSName">_NSAppleMenu</string>
+ </object>
+ </object>
+ </array>
+ <string key="NSName">_NSMainMenu</string>
+ </object>
+ </array>
+ <object class="IBObjectContainer" key="IBDocument.Objects">
+ <array class="NSMutableArray" key="connectionRecords"/>
+ <object class="IBMutableOrderedSet" key="objectRecords">
+ <array key="orderedObjects">
+ <object class="IBObjectRecord">
+ <int key="objectID">0</int>
+ <array key="object" id="0"/>
+ <reference key="children" ref="864178278"/>
+ <nil key="parent"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">-2</int>
+ <reference key="object" ref="422340081"/>
+ <reference key="parent" ref="0"/>
+ <string key="objectName">File's Owner</string>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">-1</int>
+ <reference key="object" ref="99063961"/>
+ <reference key="parent" ref="0"/>
+ <string key="objectName">First Responder</string>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">29</int>
+ <reference key="object" ref="603720448"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="726726549"/>
+ </array>
+ <reference key="parent" ref="0"/>
+ <string key="objectName">MainMenu</string>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">56</int>
+ <reference key="object" ref="726726549"/>
+ <array class="NSMutableArray" key="children">
+ <reference ref="530441688"/>
+ </array>
+ <reference key="parent" ref="603720448"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">57</int>
+ <reference key="object" ref="530441688"/>
+ <reference key="parent" ref="726726549"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">-3</int>
+ <reference key="object" ref="399126242"/>
+ <reference key="parent" ref="0"/>
+ <string key="objectName">Application</string>
+ </object>
+ </array>
+ </object>
+ <dictionary class="NSMutableDictionary" key="flattenedProperties">
+ <string key="-1.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="-2.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="-3.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="29.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="56.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ <string key="57.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
+ </dictionary>
+ <dictionary class="NSMutableDictionary" key="unlocalizedProperties"/>
+ <nil key="activeLocalization"/>
+ <dictionary class="NSMutableDictionary" key="localizations"/>
+ <nil key="sourceID"/>
+ <int key="maxID">248</int>
+ </object>
+ <object class="IBClassDescriber" key="IBDocument.Classes"/>
+ <int key="IBDocument.localizationMode">0</int>
+ <string key="IBDocument.TargetRuntimeIdentifier">IBCocoaFramework</string>
+ <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies">
+ <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaPlugin.macosx</string>
+ <real value="1060" key="NS.object.0"/>
+ </object>
+ <bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
+ <int key="IBDocument.defaultPropertyAccessControl">3</int>
+ <dictionary class="NSMutableDictionary" key="IBDocument.LastKnownImageSizes">
+ <string key="NSMenuCheckmark">{11, 11}</string>
+ <string key="NSMenuMixedState">{10, 3}</string>
+ </dictionary>
+ </data>
+</archive>
diff --git a/lvc/widgets/osx/Resources-Widgets/MainMenu.nib/keyedobjects.nib b/lvc/widgets/osx/Resources-Widgets/MainMenu.nib/keyedobjects.nib
new file mode 100644
index 0000000..963b444
--- /dev/null
+++ b/lvc/widgets/osx/Resources-Widgets/MainMenu.nib/keyedobjects.nib
Binary files differ
diff --git a/lvc/widgets/osx/__init__.py b/lvc/widgets/osx/__init__.py
new file mode 100644
index 0000000..86653eb
--- /dev/null
+++ b/lvc/widgets/osx/__init__.py
@@ -0,0 +1,74 @@
+import sys
+
+from objc import *
+from Foundation import *
+from AppKit import *
+
+from PyObjCTools import AppHelper
+
+size_request_manager = None
+
+class AppController(NSObject):
+ def applicationDidFinishLaunching_(self, notification):
+ from lvc.widgets.osx.osxmenus import MenuBar
+ self.portableApp.menubar = MenuBar()
+ self.portableApp.startup()
+ self.portableApp.run()
+
+ def setPortableApp_(self, portableApp):
+ self.portableApp = portableApp
+
+ def handleMenuActivate_(self, menu_item):
+ from lvc.widgets.osx import osxmenus
+ osxmenus.handle_menu_activate(menu_item)
+
+def initialize(app):
+ nsapp = NSApplication.sharedApplication()
+ delegate = AppController.alloc().init()
+ delegate.setPortableApp_(app)
+ nsapp.setDelegate_(delegate)
+
+ global size_request_manager
+ from lvc.widgets.osx.widgetupdates import SizeRequestManager
+ size_request_manager = SizeRequestManager()
+
+ NSApplicationMain(sys.argv)
+
+def attach_menubar():
+ pass
+
+def mainloop_start():
+ pass
+
+def mainloop_stop():
+ NSApplication.sharedApplication().terminate_(nil)
+
+def idle_add(callback, periodic=None):
+ def wrapper():
+ callback()
+ if periodic is not None:
+ AppHelper.callLater(periodic, wrapper)
+ if periodic is not None and periodic < 0:
+ raise ValueError('periodic cannot be negative')
+ # XXX: we have a lousy thread API that doesn't allocate pools for us...
+ pool = NSAutoreleasePool.alloc().init()
+ if periodic is not None:
+ AppHelper.callLater(periodic, wrapper)
+ else:
+ AppHelper.callAfter(wrapper)
+ del pool
+
+def idle_remove(id_):
+ pass
+
+def reveal_file(filename):
+ # XXX: dumb lousy type conversions ...
+ path = NSURL.fileURLWithPath_(filename.decode('utf-8')).path()
+ NSWorkspace.sharedWorkspace().selectFile_inFileViewerRootedAtPath_(
+ path, nil)
+
+def get_conversion_directory():
+ url, error = NSFileManager.defaultManager().URLForDirectory_inDomain_appropriateForURL_create_error_(NSMoviesDirectory, NSUserDomainMask, nil, YES, None)
+ if error:
+ return None
+ return url.path().encode('utf-8')
diff --git a/lvc/widgets/osx/base.py b/lvc/widgets/osx/base.py
new file mode 100644
index 0000000..30536aa
--- /dev/null
+++ b/lvc/widgets/osx/base.py
@@ -0,0 +1,367 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+""".base.py -- Widget base classes."""
+
+from AppKit import *
+from Foundation import *
+from objc import YES, NO, nil
+
+from lvc import signals
+import wrappermap
+from .viewport import Viewport, BorrowedViewport
+
+class Widget(signals.SignalEmitter):
+ """Base class for Cocoa widgets.
+
+ attributes:
+
+ CREATES_VIEW -- Does the widget create a view for itself? If this is True
+ the widget must have an attribute named view, which is the view that the
+ widget uses.
+
+ placement -- What portion of view the widget occupies.
+ """
+
+ CREATES_VIEW = True
+
+ def __init__(self):
+ signals.SignalEmitter.__init__(self, 'size-request-changed',
+ 'size-allocated', 'key-press', 'focus-out')
+ self.create_signal('place-in-scroller')
+ self.viewport = None
+ self.parent_is_scroller = False
+ self.manual_size_request = None
+ self.cached_size_request = None
+ self._disabled = False
+
+ def set_can_focus(self, allow):
+ assert isinstance(self.view, NSControl)
+ self.view.setRefusesFirstResponder_(not allow)
+
+ def set_size_request(self, width, height):
+ self.manual_size_request = (width, height)
+ self.invalidate_size_request()
+
+ def clear_size_request_cache(self):
+ from lvc.widgets.osx import size_request_manager
+ if size_request_manager is not None:
+ while size_request_manager.widgets_to_request:
+ size_request_manager._run_requests()
+
+ def get_size_request(self):
+ if self.manual_size_request:
+ width, height = self.manual_size_request
+ if width == -1:
+ width = self.get_natural_size_request()[0]
+ if height == -1:
+ height = self.get_natural_size_request()[1]
+ return width, height
+ return self.get_natural_size_request()
+
+ def get_natural_size_request(self):
+ if self.cached_size_request:
+ return self.cached_size_request
+ else:
+ self.cached_size_request = self.calc_size_request()
+ return self.cached_size_request
+
+ def invalidate_size_request(self):
+ from lvc.widgets.osx import size_request_manager
+ if size_request_manager is not None:
+ size_request_manager.add_widget(self)
+
+ def do_invalidate_size_request(self):
+ """Recalculate the size request for this widget."""
+ old_size_request = self.cached_size_request
+ self.cached_size_request = None
+ self.emit('size-request-changed', old_size_request)
+
+ def calc_size_request(self):
+ """Return the minimum size needed to display this widget.
+ Must be Implemented by subclasses.
+ """
+ raise NotImplementedError()
+
+ def _debug_size_request(self, nesting_level=0):
+ """Debug size request calculations.
+
+ This method recursively prints out the size request for each widget.
+ """
+ request = self.calc_size_request()
+ width = int(request[0])
+ height = int(request[1])
+ indent = ' ' * nesting_level
+ me = str(self.__class__).split('.')[-1]
+ print '%s%s: %sx%s' % (indent, me, width, height)
+
+ def place(self, rect, containing_view):
+ """Place this widget on a view. """
+ if self.viewport is None:
+ if self.CREATES_VIEW:
+ self.viewport = Viewport(self.view, rect)
+ containing_view.addSubview_(self.view)
+ wrappermap.add(self.view, self)
+ else:
+ self.viewport = BorrowedViewport(containing_view, rect)
+ self.viewport_created()
+ else:
+ if not self.viewport.at_position(rect):
+ self.viewport.reposition(rect)
+ self.viewport_repositioned()
+ self.emit('size-allocated', rect.size.width, rect.size.height)
+
+ def remove_viewport(self):
+ if self.viewport is not None:
+ self.viewport.remove()
+ self.viewport = None
+ if self.CREATES_VIEW:
+ wrappermap.remove(self.view)
+
+ def viewport_created(self):
+ """Called after we first create a viewport. Subclasses can override
+ this method if they want to handle this event.
+ """
+
+ def viewport_repositioned(self):
+ """Called when we reposition our viewport. Subclasses can override
+ this method if they want to handle this event.
+ """
+
+ def viewport_scrolled(self):
+ """Called by the Scroller widget on it's child widget when it is
+ scrolled.
+ """
+
+ def get_width(self):
+ return int(self.viewport.get_width())
+ width = property(get_width)
+
+ def get_height(self):
+ return int(self.viewport.get_height())
+ height = property(get_height)
+
+ def get_window(self):
+ if not self.viewport.view:
+ return None
+ return wrappermap.wrapper(self.viewport.view.window())
+
+ def queue_redraw(self):
+ if self.viewport:
+ self.viewport.queue_redraw()
+
+ def redraw_now(self):
+ if self.viewport:
+ self.viewport.redraw_now()
+
+ def relative_position(self, other_widget):
+ """Get the position of another widget, relative to this widget."""
+ basePoint = self.viewport.view.convertPoint_fromView_(
+ other_widget.viewport.area().origin,
+ other_widget.viewport.view)
+ return (basePoint.x - self.viewport.area().origin.x,
+ basePoint.y - self.viewport.area().origin.y)
+
+ def make_color(self, (red, green, blue)):
+ return NSColor.colorWithDeviceRed_green_blue_alpha_(red, green, blue,
+ 1.0)
+
+ def enable(self):
+ self._disabled = False
+
+ def disable(self):
+ self._disabled = True
+
+ def set_disabled(self, disabled):
+ if disabled:
+ self.disable()
+ else:
+ self.enable()
+
+ def get_disabled(self):
+ return self._disabled
+
+class Container(Widget):
+ """Widget that holds other widgets. """
+
+ def __init__(self):
+ Widget.__init__(self)
+ self.callback_handles = {}
+
+ def on_child_size_request_changed(self, child, old_size):
+ self.invalidate_size_request()
+
+ def connect_child_signals(self, child):
+ handle = child.connect_weak('size-request-changed',
+ self.on_child_size_request_changed)
+ self.callback_handles[child] = handle
+
+ def disconnect_child_signals(self, child):
+ child.disconnect(self.callback_handles.pop(child))
+
+ def remove_viewport(self):
+ for child in self.children:
+ child.remove_viewport()
+ Widget.remove_viewport(self)
+
+ def child_added(self, child):
+ """Must be called by subclasses when a child is added to the
+ Container."""
+ self.connect_child_signals(child)
+ self.children_changed()
+
+ def child_removed(self, child):
+ """Must be called by subclasses when a child is removed from the
+ Container."""
+ self.disconnect_child_signals(child)
+ child.remove_viewport()
+ self.children_changed()
+
+ def child_changed(self, old_child, new_child):
+ """Must be called by subclasses when a child is replaced by a new
+ child in the Container. To simplify things a bit for subclasses,
+ old_child can be None in which case this is the same as
+ child_added(new_child).
+ """
+ if old_child is not None:
+ self.disconnect_child_signals(old_child)
+ old_child.remove_viewport()
+ self.connect_child_signals(new_child)
+ self.children_changed()
+
+ def children_changed(self):
+ """Invoked when the set of children for this widget changes."""
+ self.do_invalidate_size_request()
+
+ def do_invalidate_size_request(self):
+ Widget.do_invalidate_size_request(self)
+ if self.viewport:
+ self.place_children()
+
+ def viewport_created(self):
+ self.place_children()
+
+ def viewport_repositioned(self):
+ self.place_children()
+
+ def viewport_scrolled(self):
+ for child in self.children:
+ child.viewport_scrolled()
+
+ def place_children(self):
+ """Layout our child widgets. Must be implemented by subclasses."""
+ raise NotImplementedError()
+
+ def _debug_size_request(self, nesting_level=0):
+ for child in self.children:
+ child._debug_size_request(nesting_level+1)
+ Widget._debug_size_request(self, nesting_level)
+
+class Bin(Container):
+ """Container that only has one child widget."""
+
+ def __init__(self, child=None):
+ Container.__init__(self)
+ self.child = None
+ if child is not None:
+ self.add(child)
+
+ def get_children(self):
+ if self.child:
+ return [self.child]
+ else:
+ return []
+ children = property(get_children)
+
+ def add(self, child):
+ if self.child is not None:
+ raise ValueError("Already have a child: %s" % self.child)
+ self.child = child
+ self.child_added(self.child)
+
+ def remove(self):
+ if self.child is not None:
+ old_child = self.child
+ self.child = None
+ self.child_removed(old_child)
+
+ def set_child(self, new_child):
+ old_child = self.child
+ self.child = new_child
+ self.child_changed(old_child, new_child)
+
+ def enable(self):
+ Container.enable(self)
+ self.child.enable()
+
+ def disable(self):
+ Container.disable(self)
+ self.child.disable()
+
+class SimpleBin(Bin):
+ """Bin that whose child takes up it's entire space."""
+
+ def calc_size_request(self):
+ if self.child is None:
+ return (0, 0)
+ else:
+ return self.child.get_size_request()
+
+ def place_children(self):
+ if self.child:
+ self.child.place(self.viewport.area(), self.viewport.view)
+
+class FlippedView(NSView):
+ """Flipped NSView. We use these internally to lessen the differences
+ between Cocoa and GTK.
+ """
+
+ def init(self):
+ self = super(FlippedView, self).init()
+ self.background = None
+ return self
+
+ def initWithFrame_(self, rect):
+ self = super(FlippedView, self).initWithFrame_(rect)
+ self.background = None
+ return self
+
+ def isFlipped(self):
+ return YES
+
+ def isOpaque(self):
+ return self.background is not None
+
+ def setBackgroundColor_(self, color):
+ self.background = color
+
+ def drawRect_(self, rect):
+ if self.background:
+ self.background.set()
+ NSBezierPath.fillRect_(rect)
diff --git a/lvc/widgets/osx/const.py b/lvc/widgets/osx/const.py
new file mode 100644
index 0000000..ae0da40
--- /dev/null
+++ b/lvc/widgets/osx/const.py
@@ -0,0 +1,44 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+from AppKit import *
+
+"""const.py -- Constants"""
+
+DRAG_ACTION_NONE = NSDragOperationNone
+DRAG_ACTION_COPY = NSDragOperationCopy
+DRAG_ACTION_MOVE = NSDragOperationMove
+DRAG_ACTION_LINK = NSDragOperationLink
+DRAG_ACTION_ALL = (DRAG_ACTION_COPY | DRAG_ACTION_MOVE | DRAG_ACTION_LINK)
+
+ITEM_TITLE_FONT = "Helvetica"
+ITEM_DESC_FONT = "Helvetica"
+ITEM_INFO_FONT = "Lucida Grande"
+
+TOOLBAR_GRAY = (0.19, 0.19, 0.19)
diff --git a/lvc/widgets/osx/contextmenu.py b/lvc/widgets/osx/contextmenu.py
new file mode 100644
index 0000000..7a8fa55
--- /dev/null
+++ b/lvc/widgets/osx/contextmenu.py
@@ -0,0 +1,84 @@
+from AppKit import *
+from objc import nil
+
+from .base import Widget
+
+class ContextMenuHandler(NSObject):
+ def initWithCallback_widget_i_(self, callback, widget, i):
+ self = super(ContextMenuHandler, self).init()
+ self.callback = callback
+ self.widget = widget
+ self.i = i
+ return self
+
+ def handleMenuItem_(self, sender):
+ self.callback(self.widget, self.i)
+
+
+class MiroContextMenu(NSMenu):
+ # Works exactly like NSMenu, except it keeps a reference to the menu
+ # handler objects.
+ def init(self):
+ self = super(MiroContextMenu, self).init()
+ self.handlers = set()
+ return self
+
+ def addItem_(self, item):
+ if isinstance(item.target(), ContextMenuHandler):
+ self.handlers.add(item.target())
+ return NSMenu.addItem_(self, item)
+
+
+class ContextMenu(object):
+
+ def __init__(self, options):
+ super(ContextMenu, self).__init__()
+ self.menu = MiroContextMenu.alloc().init()
+ for i, item_info in enumerate(options):
+ if item_info is None:
+ nsitem = NSMenuItem.separatorItem()
+ else:
+ label, callback = item_info
+ nsitem = NSMenuItem.alloc().init()
+ font_size = NSFont.systemFontSize()
+ font = NSFont.fontWithName_size_("Lucida Sans Italic", font_size)
+ if font is None:
+ font = NSFont.systemFontOfSize_(font_size)
+ attributes = {NSFontAttributeName: font}
+ attributed_label = NSAttributedString.alloc().initWithString_attributes_(label, attributes)
+ nsitem.setAttributedTitle_(attributed_label)
+ else:
+ nsitem.setTitle_(label)
+ if isinstance(callback, list):
+ submenu = ContextMenu(callback)
+ self.menu.setSubmenu_forItem_(submenu.menu, nsitem)
+ else:
+ handler = ContextMenuHandler.alloc().initWithCallback_widget_i_(callback, self, i)
+ nsitem.setTarget_(handler)
+ nsitem.setAction_('handleMenuItem:')
+ self.menu.addItem_(nsitem)
+
+ def popup(self):
+ # support for non-window based popups thanks to
+ # http://stackoverflow.com/questions/9033534/how-can-i-pop-up-nsmenu-at-mouse-cursor-position
+ location = NSEvent.mouseLocation()
+ frame = NSMakeRect(location.x, location.y, 200, 200)
+ window = NSWindow.alloc().initWithContentRect_styleMask_backing_defer_(
+ frame,
+ NSBorderlessWindowMask,
+ NSBackingStoreBuffered,
+ NO)
+ window.setAlphaValue_(0)
+ window.makeKeyAndOrderFront_(NSApp)
+ location_in_window = window.convertScreenToBase_(location)
+ event = NSEvent.mouseEventWithType_location_modifierFlags_timestamp_windowNumber_context_eventNumber_clickCount_pressure_(
+ NSLeftMouseDown,
+ location_in_window,
+ 0,
+ 0,
+ window.windowNumber(),
+ nil,
+ 0,
+ 0,
+ 0)
+ NSMenu.popUpContextMenu_withEvent_forView_(self.menu, event, window.contentView())
diff --git a/lvc/widgets/osx/control.py b/lvc/widgets/osx/control.py
new file mode 100644
index 0000000..63419d5
--- /dev/null
+++ b/lvc/widgets/osx/control.py
@@ -0,0 +1,530 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+""".control - Controls."""
+
+from AppKit import *
+from Foundation import *
+from objc import YES, NO, nil
+
+from lvc.widgets import widgetconst
+import wrappermap
+from .base import Widget
+from .helpers import NotificationForwarder
+
+class SizedControl(Widget):
+ def set_size(self, size):
+ if size == widgetconst.SIZE_NORMAL:
+ self.view.cell().setControlSize_(NSRegularControlSize)
+ font = NSFont.systemFontOfSize_(NSFont.systemFontSize())
+ self.font_size = NSFont.systemFontSize()
+ elif size == widgetconst.SIZE_SMALL:
+ font = NSFont.systemFontOfSize_(NSFont.smallSystemFontSize())
+ self.view.cell().setControlSize_(NSSmallControlSize)
+ self.font_size = NSFont.smallSystemFontSize()
+ else:
+ self.view.cell().setControlSize_(NSRegularControlSize)
+ font = NSFont.systemFontOfSize_(NSFont.systemFontSize() * size)
+ self.font_size = NSFont.systemFontSize() * size
+ self.view.setFont_(font)
+
+class BaseTextEntry(SizedControl):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self, initial_text=None):
+ SizedControl.__init__(self)
+ self.view = self.make_view()
+ self.font = NSFont.systemFontOfSize_(NSFont.systemFontSize())
+ self.view.setFont_(self.font)
+ self.view.setEditable_(YES)
+ self.view.cell().setScrollable_(YES)
+ self.view.cell().setLineBreakMode_(NSLineBreakByClipping)
+ self.sizer_cell = self.view.cell().copy()
+ if initial_text:
+ self.view.setStringValue_(initial_text)
+ self.set_width(len(initial_text))
+ else:
+ self.set_width(10)
+
+ self.notifications = NotificationForwarder.create(self.view)
+
+ self.create_signal('activate')
+ self.create_signal('changed')
+ self.create_signal('validate')
+
+ def focus(self):
+ if self.view.window() is not None:
+ self.view.window().makeFirstResponder_(self.view)
+
+ def start_editing(self, initial_text):
+ self.set_text(initial_text)
+ self.focus()
+ # unselect the text and locate the cursor at the end of the entry
+ text_field = self.view.window().fieldEditor_forObject_(YES, self.view)
+ text_field.setSelectedRange_(NSMakeRange(len(self.get_text()), 0))
+
+ def viewport_created(self):
+ SizedControl.viewport_created(self)
+ self.notifications.connect(self.on_changed, 'NSControlTextDidChangeNotification')
+ self.notifications.connect(self.on_end_editing,
+ 'NSControlTextDidEndEditingNotification')
+
+ def remove_viewport(self):
+ SizedControl.remove_viewport(self)
+ self.notifications.disconnect()
+
+ def baseline(self):
+ return -self.view.font().descender() + 2
+
+ def on_changed(self, notification):
+ self.emit('changed')
+
+ def on_end_editing(self, notification):
+ self.emit('focus-out')
+
+ def calc_size_request(self):
+ size = self.sizer_cell.cellSize()
+ return size.width, size.height
+
+ def set_text(self, text):
+ self.view.setStringValue_(text)
+ self.emit('changed')
+
+ def get_text(self):
+ return self.view.stringValue()
+
+ def set_width(self, chars):
+ self.sizer_cell.setStringValue_('X' * chars)
+ self.invalidate_size_request()
+
+ def set_activates_default(self, setting):
+ pass
+
+ def enable(self):
+ SizedControl.enable(self)
+ self.view.setEnabled_(True)
+
+ def disable(self):
+ SizedControl.disable(self)
+ self.view.setEnabled_(False)
+
+class MiroTextField(NSTextField):
+ def textDidEndEditing_(self, notification):
+ wrappermap.wrapper(self).emit('activate')
+ return NSTextField.textDidEndEditing_(self, notification)
+
+class TextEntry(BaseTextEntry):
+ def make_view(self):
+ return MiroTextField.alloc().init()
+
+class NumberEntry(BaseTextEntry):
+ def make_view(self):
+ return MiroTextField.alloc().init()
+
+ def set_max_length(self, length):
+ # TODO
+ pass
+
+ def _filter_value(self):
+ """Discard any non-numeric characters"""
+ digits = ''.join(x for x in self.view.stringValue() if x.isdigit())
+ self.view.setStringValue_(digits)
+
+ def on_changed(self, notification):
+ # overriding on_changed rather than connecting to it ensures that we
+ # filter the value before anything else connected to the signal sees it
+ self._filter_value()
+ BaseTextEntry.on_changed(self, notification)
+
+ def get_text(self):
+ # handles get_text between when text is entered and when on_changed
+ # filters it, in case that's possible
+ self._filter_value()
+ return BaseTextEntry.get_text(self)
+
+class MiroSecureTextField(NSSecureTextField):
+ def textDidEndEditing_(self, notification):
+ wrappermap.wrapper(self).emit('activate')
+ return NSSecureTextField.textDidEndEditing_(self, notification)
+
+class SecureTextEntry(BaseTextEntry):
+ def make_view(self):
+ return MiroSecureTextField.alloc().init()
+
+class MultilineTextEntry(Widget):
+ def __init__(self, initial_text=None):
+ Widget.__init__(self)
+ if initial_text is None:
+ initial_text = ""
+ self.view = NSTextView.alloc().initWithFrame_(NSRect((0,0),(50,50)))
+ self.view.setMaxSize_((1.0e7, 1.0e7))
+ self.view.setHorizontallyResizable_(NO)
+ self.view.setVerticallyResizable_(YES)
+ self.notifications = NotificationForwarder.create(self.view)
+ self.create_signal('changed')
+ self.create_signal('focus-out')
+ if initial_text is not None:
+ self.set_text(initial_text)
+ self.set_size(widgetconst.SIZE_NORMAL)
+
+ def set_size(self, size):
+ if size == widgetconst.SIZE_NORMAL:
+ font = NSFont.systemFontOfSize_(NSFont.systemFontSize())
+ elif size == widgetconst.SIZE_SMALL:
+ self.view.cell().setControlSize_(NSSmallControlSize)
+ else:
+ raise ValueError("Unknown size: %s" % size)
+ self.view.setFont_(font)
+
+ def viewport_created(self):
+ Widget.viewport_created(self)
+ self.notifications.connect(self.on_changed, 'NSTextDidChangeNotification')
+ self.notifications.connect(self.on_end_editing,
+ 'NSControlTextDidEndEditingNotification')
+ self.invalidate_size_request()
+
+ def remove_viewport(self):
+ Widget.remove_viewport(self)
+ self.notifications.disconnect()
+
+ def focus(self):
+ if self.view.window() is not None:
+ self.view.window().makeFirstResponder_(self.view)
+
+ def set_text(self, text):
+ self.view.setString_(text)
+ self.invalidate_size_request()
+
+ def get_text(self):
+ return self.view.string()
+
+ def on_changed(self, notification):
+ self.invalidate_size_request()
+ self.emit("changed")
+
+ def on_end_editing(self, notification):
+ self.emit("focus-out")
+
+ def calc_size_request(self):
+ layout_manager = self.view.layoutManager()
+ text_container = self.view.textContainer()
+ # The next line is there just to force cocoa to layout the text
+ layout_manager.glyphRangeForTextContainer_(text_container)
+ rect = layout_manager.usedRectForTextContainer_(text_container)
+ return rect.size.width, rect.size.height
+
+ def set_editable(self, editable):
+ if editable:
+ self.view.setEditable_(YES)
+ else:
+ self.view.setEditable_(NO)
+
+
+class MiroButton(NSButton):
+
+ def initWithSignal_(self, signal):
+ self = super(MiroButton, self).init()
+ self.signal = signal
+ return self
+
+ def sendAction_to_(self, action, to):
+ # We override the Cocoa machinery here and just send it to our wrapper
+ # widget.
+ wrappermap.wrapper(self).emit(self.signal)
+ return YES
+
+class Checkbox(SizedControl):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self, text="", bold=False, color=None):
+ SizedControl.__init__(self)
+ self.create_signal('toggled')
+ self.view = MiroButton.alloc().initWithSignal_('toggled')
+ self.view.setButtonType_(NSSwitchButton)
+ self.bold = bold
+ self.title = text
+ self.font_size = NSFont.systemFontSize()
+ self.color = self.make_color(color)
+ self._set_title()
+
+ def set_size(self, size):
+ SizedControl.set_size(self, size)
+ self._set_title()
+
+ def _set_title(self):
+ if self.color is None:
+ self.view.setTitle_(self.title)
+ else:
+ attributes = {
+ NSForegroundColorAttributeName: self.color,
+ NSFontAttributeName: NSFont.systemFontOfSize_(self.font_size)
+ }
+ string = NSAttributedString.alloc().initWithString_attributes_(
+ self.title, attributes)
+ self.view.setAttributedTitle_(string)
+
+ def calc_size_request(self):
+ if self.manual_size_request:
+ width, height = self.manual_size_request
+ if width == -1:
+ width = 10000
+ if height == -1:
+ height = 10000
+ size = self.view.cell().cellSizeForBounds_(
+ NSRect((0, 0), (width, height)))
+ else:
+ size = self.view.cell().cellSize()
+ return (size.width, size.height)
+
+ def baseline(self):
+ return -self.view.font().descender() + 1
+
+ def get_checked(self):
+ return self.view.state() == NSOnState
+
+ def set_checked(self, value):
+ if value:
+ self.view.setState_(NSOnState)
+ else:
+ self.view.setState_(NSOffState)
+
+ def enable(self):
+ SizedControl.enable(self)
+ self.view.setEnabled_(True)
+
+ def disable(self):
+ SizedControl.disable(self)
+ self.view.setEnabled_(False)
+
+ def get_text_padding(self):
+ """
+ Returns the amount of space the checkbox takes up before the label.
+ """
+ # XXX FIXME
+ return 18
+
+class Button(SizedControl):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self, label, style='normal', width=0):
+ SizedControl.__init__(self)
+ self.color = None
+ self.title = label
+ self.create_signal('clicked')
+ self.view = MiroButton.alloc().initWithSignal_('clicked')
+ self.view.setButtonType_(NSMomentaryPushInButton)
+ self._set_title()
+ self.setup_style(style)
+ self.min_width = width
+
+ def set_text(self, label):
+ self.title = label
+ self._set_title()
+
+ def set_color(self, color):
+ self.color = self.make_color(color)
+ self._set_title()
+
+ def _set_title(self):
+ if self.color is None:
+ self.view.setTitle_(self.title)
+ else:
+ attributes = {
+ NSForegroundColorAttributeName: self.color,
+ NSFontAttributeName: self.view.font()
+ }
+ string = NSAttributedString.alloc().initWithString_attributes_(
+ self.title, attributes)
+ self.view.setAttributedTitle_(string)
+
+ def setup_style(self, style):
+ if style == 'normal':
+ self.view.setBezelStyle_(NSRoundedBezelStyle)
+ self.pad_height = 0
+ self.pad_width = 10
+ self.min_width = 112
+ elif style == 'smooth':
+ self.view.setBezelStyle_(NSRoundRectBezelStyle)
+ self.pad_width = 0
+ self.pad_height = 4
+ self.paragraph_style = NSMutableParagraphStyle.alloc().init()
+ self.paragraph_style.setAlignment_(NSCenterTextAlignment)
+
+ def make_default(self):
+ self.view.setKeyEquivalent_("\r")
+
+ def calc_size_request(self):
+ size = self.view.cell().cellSize()
+ width = max(self.min_width, size.width + self.pad_width)
+ height = size.height + self.pad_height
+ return width, height
+
+ def baseline(self):
+ return -self.view.font().descender() + 10 + self.pad_height
+
+ def enable(self):
+ SizedControl.enable(self)
+ self.view.setEnabled_(True)
+
+ def disable(self):
+ SizedControl.disable(self)
+ self.view.setEnabled_(False)
+
+class MiroPopupButton(NSPopUpButton):
+
+ def init(self):
+ self = super(MiroPopupButton, self).init()
+ self.setTarget_(self)
+ self.setAction_('handleChange:')
+ return self
+
+ def handleChange_(self, sender):
+ wrappermap.wrapper(self).emit('changed', self.indexOfSelectedItem())
+
+class OptionMenu(SizedControl):
+ def __init__(self, options):
+ SizedControl.__init__(self)
+ self.create_signal('changed')
+ self.view = MiroPopupButton.alloc().init()
+ self.options = options
+ for option, value in options:
+ self.view.addItemWithTitle_(option)
+
+ def baseline(self):
+ if self.view.cell().controlSize() == NSRegularControlSize:
+ return -self.view.font().descender() + 6
+ else:
+ return -self.view.font().descender() + 5
+
+ def calc_size_request(self):
+ return self.view.cell().cellSize()
+
+ def set_selected(self, index):
+ self.view.selectItemAtIndex_(index)
+
+ def get_selected(self):
+ return self.view.indexOfSelectedItem()
+
+ def enable(self):
+ SizedControl.enable(self)
+ self.view.setEnabled_(True)
+
+ def disable(self):
+ SizedControl.disable(self)
+ self.view.setEnabled_(False)
+
+ def set_width(self, width):
+ # TODO
+ pass
+
+class RadioButtonGroup:
+ def __init__(self):
+ self._buttons = []
+
+ def handle_click(self, widget):
+ self.set_selected(widget)
+
+ def add_button(self, button):
+ self._buttons.append(button)
+ button.connect('clicked', self.handle_click)
+ if len(self._buttons) == 1:
+ button.view.setState_(NSOnState)
+ else:
+ button.view.setState_(NSOffState)
+
+ def get_buttons(self):
+ return self._buttons
+
+ def get_selected(self):
+ for mem in self._buttons:
+ if mem.get_selected():
+ return mem
+
+ def set_selected(self, button):
+ for mem in self._buttons:
+ if button is mem:
+ mem.view.setState_(NSOnState)
+ else:
+ mem.view.setState_(NSOffState)
+
+class RadioButton(SizedControl):
+ def __init__(self, label, group=None, bold=False, color=None):
+ SizedControl.__init__(self)
+ self.create_signal('clicked')
+ self.view = MiroButton.alloc().initWithSignal_('clicked')
+ self.view.setButtonType_(NSRadioButton)
+ self.color = self.make_color(color)
+ self.title = label
+ self.bold = bold
+ self.font_size = NSFont.systemFontSize()
+ self._set_title()
+
+ if group is not None:
+ self.group = group
+ else:
+ self.group = RadioButtonGroup()
+
+ self.group.add_button(self)
+
+ def set_size(self, size):
+ SizedControl.set_size(self, size)
+ self._set_title()
+
+ def _set_title(self):
+ if self.color is None:
+ self.view.setTitle_(self.title)
+ else:
+ attributes = {
+ NSForegroundColorAttributeName: self.color,
+ NSFontAttributeName: NSFont.systemFontOfSize_(self.font_size)
+ }
+ string = NSAttributedString.alloc().initWithString_attributes_(
+ self.title, attributes)
+ self.view.setAttributedTitle_(string)
+
+ def calc_size_request(self):
+ size = self.view.cell().cellSize()
+ return (size.width, size.height)
+
+ def baseline(self):
+ -self.view.font().descender() + 2
+
+ def get_group(self):
+ return self.group
+
+ def get_selected(self):
+ return self.view.state() == NSOnState
+
+ def set_selected(self):
+ self.group.set_selected(self)
+
+ def enable(self):
+ SizedControl.enable(self)
+ self.view.setEnabled_(True)
+
+ def disable(self):
+ SizedControl.disable(self)
+ self.view.setEnabled_(False)
diff --git a/lvc/widgets/osx/customcontrol.py b/lvc/widgets/osx/customcontrol.py
new file mode 100644
index 0000000..4a32b8e
--- /dev/null
+++ b/lvc/widgets/osx/customcontrol.py
@@ -0,0 +1,436 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+""".customcontrol -- CustomControl handlers. """
+
+import collections
+
+from AppKit import *
+from Foundation import *
+from objc import YES, NO, nil
+
+from lvc.widgets import widgetconst
+import wrappermap
+from .base import Widget
+import drawing
+from .layoutmanager import LayoutManager
+
+class DrawableButtonCell(NSButtonCell):
+ def startTrackingAt_inView_(self, point, view):
+ view.setState_(NSOnState)
+ return YES
+
+ def continueTracking_at_inView_(self, lastPoint, at, view):
+ view.setState_(NSOnState)
+ return YES
+
+ def stopTracking_at_inView_mouseIsUp_(self, lastPoint, at, view, mouseIsUp):
+ if not mouseIsUp:
+ view.mouse_inside = False
+ view.setState_(NSOffState)
+
+class DrawableButton(NSButton):
+ def init(self):
+ self = super(DrawableButton, self).init()
+ self.layout_manager = LayoutManager()
+ self.tracking_area = None
+ self.mouse_inside = False
+ self.custom_cursor = None
+ return self
+
+ def resetCursorRects(self):
+ if self.custom_cursor is not None:
+ self.addCursorRect_cursor_(self.visibleRect(), self.custom_cursor)
+ self.custom_cursor.setOnMouseEntered_(YES)
+
+ def updateTrackingAreas(self):
+ # remove existing tracking area if needed
+ if self.tracking_area:
+ self.removeTrackingArea_(self.tracking_area)
+
+ # create a new tracking area for the entire view. This allows us to
+ # get mouseMoved events whenever the mouse is inside our view.
+ self.tracking_area = NSTrackingArea.alloc()
+ self.tracking_area.initWithRect_options_owner_userInfo_(
+ self.visibleRect(),
+ NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved |
+ NSTrackingActiveInKeyWindow,
+ self,
+ nil)
+ self.addTrackingArea_(self.tracking_area)
+
+ def mouseEntered_(self, event):
+ window = self.window()
+ if window is not nil and window.isMainWindow():
+ self.mouse_inside = True
+ self.setNeedsDisplay_(YES)
+
+ def mouseExited_(self, event):
+ window = self.window()
+ if window is not nil and window.isMainWindow():
+ self.mouse_inside = False
+ self.setNeedsDisplay_(YES)
+
+ def isOpaque(self):
+ return wrappermap.wrapper(self).is_opaque()
+
+ def drawRect_(self, rect):
+ context = drawing.DrawingContext(self, self.bounds(), rect)
+ context.style = drawing.DrawingStyle()
+ wrapper = wrappermap.wrapper(self)
+ wrapper.state = 'normal'
+ disabled = wrapper.get_disabled()
+ if not disabled:
+ if self.state() == NSOnState:
+ wrapper.state = 'pressed'
+ elif self.mouse_inside:
+ wrapper.state = 'hover'
+ else:
+ wrapper.state = 'normal'
+
+ wrapper.draw(context, self.layout_manager)
+ self.layout_manager.reset()
+
+ def sendAction_to_(self, action, to):
+ # We override the Cocoa machinery here and just send it to our wrapper
+ # widget.
+ wrapper = wrappermap.wrapper(self)
+ disabled = wrapper.get_disabled()
+ if not disabled:
+ wrapper.emit('clicked')
+ # Tell Cocoa we handled it anyway, just not emit the actual clicked
+ # event.
+ return YES
+DrawableButton.setCellClass_(DrawableButtonCell)
+
+class ContinousButtonCell(DrawableButtonCell):
+ def stopTracking_at_inView_mouseIsUp_(self, lastPoint, at, view, mouseIsUp):
+ view.onStopTracking(at)
+ NSButtonCell.stopTracking_at_inView_mouseIsUp_(self, lastPoint, at,
+ view, mouseIsUp)
+
+class ContinuousDrawableButton(DrawableButton):
+ def init(self):
+ self = super(ContinuousDrawableButton, self).init()
+ self.setContinuous_(YES)
+ return self
+
+ def mouseDown_(self, event):
+ self.releaseInbounds = self.stopTracking = self.firedOnce = False
+ self.cell().trackMouse_inRect_ofView_untilMouseUp_(event,
+ self.bounds(), self, YES)
+ wrapper = wrappermap.wrapper(self)
+ if not wrapper.get_disabled():
+ if self.firedOnce:
+ wrapper.emit('released')
+ elif self.releaseInbounds:
+ wrapper.emit('clicked')
+
+ def sendAction_to_(self, action, to):
+ if self.stopTracking:
+ return NO
+ self.firedOnce = True
+ wrapper = wrappermap.wrapper(self)
+ if not wrapper.get_disabled():
+ wrapper.emit('held-down')
+ return YES
+
+ def onStopTracking(self, mouseLocation):
+ self.releaseInbounds = NSPointInRect(mouseLocation, self.bounds())
+ self.stopTracking = True
+ContinuousDrawableButton.setCellClass_(ContinousButtonCell)
+
+class DragableButtonCell(NSButtonCell):
+ def startTrackingAt_inView_(self, point, view):
+ self.start_x = point.x
+ return YES
+
+ def continueTracking_at_inView_(self, lastPoint, at, view):
+ DRAG_THRESHOLD = 15
+ wrapper = wrappermap.wrapper(view)
+ if not wrapper.get_disabled():
+ if (view.last_drag_event != 'right' and
+ at.x > self.start_x + DRAG_THRESHOLD):
+ wrapper.emit("dragged-right")
+ view.last_drag_event = 'right'
+ elif (view.last_drag_event != 'left' and
+ at.x < self.start_x - DRAG_THRESHOLD):
+ view.last_drag_event = 'left'
+ wrapper.emit("dragged-left")
+ return YES
+
+class DragableDrawableButton(DrawableButton):
+ def mouseDown_(self, event):
+ self.last_drag_event = None
+ self.cell().trackMouse_inRect_ofView_untilMouseUp_(event,
+ self.bounds(), self, YES)
+
+ def sendAction_to_(self, action, to):
+ # only send the click event if we didn't send a
+ # dragged-left/dragged-right event
+ wrapper = wrappermap.wrapper(self)
+ if self.last_drag_event is None and not wrapper.get_disabled():
+ wrapper.emit('clicked')
+ return YES
+DragableDrawableButton.setCellClass_(DragableButtonCell)
+
+MouseTrackingInfo = collections.namedtuple("MouseTrackingInfo",
+ "start_pos click_pos")
+
+class CustomSliderCell(NSSliderCell):
+ def calc_slider_amount(self, view, pos, size):
+ slider_size = wrappermap.wrapper(view).slider_size()
+ pos -= slider_size / 2
+ size -= slider_size
+ return max(0, min(1, float(pos) / size))
+
+ def get_slider_pos(self, view, value=None):
+ if value is None:
+ value = view.floatValue()
+ if view.isVertical():
+ size = view.bounds().size.height
+ else:
+ size = view.bounds().size.width
+ slider_size = view.knobThickness()
+ size -= slider_size
+ start_pos = slider_size / 2.0
+ ratio = ((value - view.minValue()) /
+ view.maxValue() - view.minValue())
+ return start_pos + (ratio * size)
+
+ def startTrackingAt_inView_(self, at, view):
+ wrapper = wrappermap.wrapper(view)
+ start_pos = self.get_slider_pos(view)
+ if self.isVertical():
+ click_pos = at.y
+ else:
+ click_pos = at.x
+ # only move the cursor if the click was outside the slider
+ if abs(click_pos - start_pos) > view.knobThickness() / 2:
+ self.moveSliderTo(view, click_pos)
+ start_pos = click_pos
+ view.mouse_tracking_info = MouseTrackingInfo(start_pos, click_pos)
+ if not wrapper.get_disabled():
+ wrapper.emit('pressed')
+ return YES
+
+ def moveSliderTo(self, view, pos):
+ if view.isVertical():
+ size = view.bounds().size.height
+ else:
+ size = view.bounds().size.width
+
+ slider_amount = self.calc_slider_amount(view, pos, size)
+ value = (self.maxValue() - self.minValue()) * slider_amount
+ self.setFloatValue_(value)
+ wrapper = wrappermap.wrapper(view)
+ if not wrapper.get_disabled():
+ wrapper.emit('moved', value)
+ if self.isContinuous():
+ wrapper.emit('changed', value)
+
+ def continueTracking_at_inView_(self, lastPoint, at, view):
+ if view.isVertical():
+ mouse_pos = at.y
+ else:
+ mouse_pos = at.x
+
+ info = view.mouse_tracking_info
+ new_pos = info.start_pos + (mouse_pos - info.click_pos)
+ self.moveSliderTo(view, new_pos)
+ return YES
+
+ def stopTracking_at_inView_mouseIsUp_(self, lastPoint, at, view, mouseUp):
+ wrapper = wrappermap.wrapper(view)
+ if not wrapper.get_disabled():
+ wrapper.emit('released')
+ view.mouse_tracking_info = None
+
+class CustomSliderView(NSSlider):
+ def init(self):
+ self = super(CustomSliderView, self).init()
+ self.layout_manager = LayoutManager()
+ self.custom_cursor = None
+ self.mouse_tracking_info = None
+ return self
+
+ def get_slider_pos(self, value=None):
+ return self.cell().get_slider_pos(self, value)
+
+ def resetCursorRects(self):
+ if self.custom_cursor is not None:
+ self.addCursorRect_cursor_(self.visibleRect(), self.custom_cursor)
+ self.custom_cursor.setOnMouseEntered_(YES)
+
+ def isOpaque(self):
+ return wrappermap.wrapper(self).is_opaque()
+
+ def knobThickness(self):
+ return wrappermap.wrapper(self).slider_size()
+
+ def scrollWheel_(self, event):
+ wrapper = wrappermap.wrapper(self)
+ if wrapper.get_disabled():
+ return
+ # NOTE: we ignore the scroll_step value passed into set_increments()
+ # and calculate the change using deltaY, which is in device
+ # coordinates.
+ slider_size = wrapper.slider_size()
+ if wrapper.is_horizontal():
+ size = self.bounds().size.width
+ else:
+ size = self.bounds().size.height
+ size -= slider_size
+
+ range = self.maxValue() - self.minValue()
+ value_change = (event.deltaY() / size) * range
+ self.setFloatValue_(self.floatValue() + value_change)
+ wrapper.emit('pressed')
+ wrapper.emit('changed', self.floatValue())
+ wrapper.emit('released')
+
+ def isVertical(self):
+ return not wrappermap.wrapper(self).is_horizontal()
+
+ def drawRect_(self, rect):
+ context = drawing.DrawingContext(self, self.bounds(), rect)
+ context.style = drawing.DrawingStyle()
+ wrappermap.wrapper(self).draw(context, self.layout_manager)
+ self.layout_manager.reset()
+
+ def sendAction_to_(self, action, to):
+ # We override the Cocoa machinery here and just send it to our wrapper
+ # widget.
+ wrapper = wrappermap.wrapper(self)
+ disabled = wrapper.get_disabled()
+ if not disabled:
+ wrapper.emit('changed', self.floatValue())
+ # Total Cocoa we handled it anyway to prevent the event passed to
+ # upper layer.
+ return YES
+CustomSliderView.setCellClass_(CustomSliderCell)
+
+class CustomControlBase(drawing.DrawingMixin, Widget):
+ def set_cursor(self, cursor):
+ if cursor == widgetconst.CURSOR_NORMAL:
+ self.view.custom_cursor = None
+ elif cursor == widgetconst.CURSOR_POINTING_HAND:
+ self.view.custom_cursor = NSCursor.pointingHandCursor()
+ else:
+ raise ValueError("Unknown cursor: %s" % cursor)
+ if self.view.window():
+ self.view.window().invalidateCursorRectsForView_(self.view)
+
+class CustomButton(CustomControlBase):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self):
+ CustomControlBase.__init__(self)
+ self.create_signal('clicked')
+ self.view = DrawableButton.alloc().init()
+ self.view.setRefusesFirstResponder_(NO)
+ self.view.setEnabled_(True)
+
+ def enable(self):
+ Widget.enable(self)
+ self.view.setNeedsDisplay_(YES)
+
+ def disable(self):
+ Widget.disable(self)
+ self.view.setNeedsDisplay_(YES)
+
+class ContinuousCustomButton(CustomButton):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self):
+ CustomButton.__init__(self)
+ self.create_signal('held-down')
+ self.create_signal('released')
+ self.view = ContinuousDrawableButton.alloc().init()
+ self.view.setRefusesFirstResponder_(NO)
+
+ def set_delays(self, initial, repeat):
+ self.view.cell().setPeriodicDelay_interval_(initial, repeat)
+
+class DragableCustomButton(CustomButton):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self):
+ CustomButton.__init__(self)
+ self.create_signal('dragged-left')
+ self.create_signal('dragged-right')
+ self.view = DragableDrawableButton.alloc().init()
+
+class CustomSlider(CustomControlBase):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self):
+ CustomControlBase.__init__(self)
+ self.create_signal('pressed')
+ self.create_signal('released')
+ self.create_signal('changed')
+ self.create_signal('moved')
+ self.view = CustomSliderView.alloc().init()
+ self.view.setRefusesFirstResponder_(NO)
+ if self.is_continuous():
+ self.view.setContinuous_(YES)
+ else:
+ self.view.setContinuous_(NO)
+ self.view.setEnabled_(True)
+
+ def get_slider_pos(self, value=None):
+ return self.view.get_slider_pos(value)
+
+ def viewport_created(self):
+ self.view.cell().setKnobThickness_(self.slider_size())
+
+ def get_value(self):
+ return self.view.floatValue()
+
+ def set_value(self, value):
+ self.view.setFloatValue_(value)
+
+ def get_range(self):
+ return self.view.minValue(), self.view.maxValue()
+
+ def set_range(self, min_value, max_value):
+ self.view.setMinValue_(min_value)
+ self.view.setMaxValue_(max_value)
+
+ def set_increments(self, small_step, big_step, scroll_step=None):
+ # NOTE: we ignore all of these parameters.
+ #
+ # Cocoa doesn't have a concept of changing the increments for
+ # NSScroller. scroll_step is isn't really compatible with
+ # the event object that's passed to scrollWheel_()
+ pass
+
+ def enable(self):
+ Widget.enable(self)
+ self.view.setNeedsDisplay_(YES)
+
+ def disable(self):
+ Widget.disable(self)
+ self.view.setNeedsDisplay_(YES)
diff --git a/lvc/widgets/osx/drawing.py b/lvc/widgets/osx/drawing.py
new file mode 100644
index 0000000..aaad1e9
--- /dev/null
+++ b/lvc/widgets/osx/drawing.py
@@ -0,0 +1,289 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+"""miro.plat.frontend.widgets.drawing -- Draw on Views."""
+
+import math
+
+from Foundation import *
+from AppKit import *
+#from Quartz import *
+from objc import YES, NO, nil
+
+
+class ImageSurface:
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self, image):
+ """Create a new ImageSurface."""
+ self.image = image.nsimage.copy()
+ self.width = image.width
+ self.height = image.height
+
+ def get_size(self):
+ return self.width, self.height
+
+ def draw(self, context, x, y, width, height, fraction=1.0):
+ if self.width == 0 or self.height == 0:
+ return
+ current_context = NSGraphicsContext.currentContext()
+ current_context.setShouldAntialias_(YES)
+ current_context.setImageInterpolation_(NSImageInterpolationHigh)
+ current_context.saveGraphicsState()
+ flip_context(y + height)
+ dest_rect = NSMakeRect(x, 0, width, height)
+ if self.width >= width and self.height >= height:
+ # drawing to area smaller than our image
+ source_rect = NSMakeRect(0, 0, width, height)
+ self.image.drawInRect_fromRect_operation_fraction_(
+ dest_rect, source_rect, NSCompositeSourceOver, fraction)
+ else:
+ # drawing to area larger than our image. Need to tile it.
+ NSColor.colorWithPatternImage_(self.image).set()
+ current_context.setPatternPhase_(
+ self._calc_pattern_phase(context, x, y))
+ NSBezierPath.fillRect_(dest_rect)
+ current_context.restoreGraphicsState()
+
+ def draw_rect(self, context, dest_x, dest_y, source_x, source_y, width,
+ height, fraction=1.0):
+ if width == 0 or height == 0:
+ return
+ current_context = NSGraphicsContext.currentContext()
+ current_context.setShouldAntialias_(YES)
+ current_context.setImageInterpolation_(NSImageInterpolationHigh)
+ current_context.saveGraphicsState()
+ flip_context(dest_y + height)
+ dest_y = 0
+ dest_rect = NSMakeRect(dest_x, dest_y, width, height)
+ source_rect = NSMakeRect(source_x, self.height-source_y-height,
+ width, height)
+ self.image.drawInRect_fromRect_operation_fraction_(
+ dest_rect, source_rect, NSCompositeSourceOver, fraction)
+ current_context.restoreGraphicsState()
+
+ def _calc_pattern_phase(self, context, x, y):
+ """Calculate the pattern phase to draw tiled images.
+
+ When we draw with a pattern, we want the image in the pattern to start
+ at the top-left of where we're drawing to. This function does the
+ dirty work necessary.
+
+ :returns: NSPoint to send to setPatternPhase_
+ """
+ # convert to view coords
+ view_point = NSPoint(context.origin.x + x, context.origin.y + y)
+ # convert to window coords, which is setPatternPhase_ uses
+ return context.view.convertPoint_toView_(view_point, nil)
+
+def convert_cocoa_color(color):
+ rgb = color.colorUsingColorSpaceName_(NSDeviceRGBColorSpace)
+ return (rgb.redComponent(), rgb.greenComponent(), rgb.blueComponent())
+
+def convert_widget_color(color, alpha=1.0):
+ return NSColor.colorWithDeviceRed_green_blue_alpha_(color[0], color[1],
+ color[2], alpha)
+def flip_context(height):
+ """Make the current context's coordinates flipped.
+
+ This is useful for drawing images, since they use the normal cocoa
+ coordinates and we use flipped versions.
+
+ :param height: height of the current area we are drawing to.
+ """
+ xform = NSAffineTransform.transform()
+ xform.translateXBy_yBy_(0, height)
+ xform.scaleXBy_yBy_(1.0, -1.0)
+ xform.concat()
+
+class DrawingStyle(object):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self, bg_color=None, text_color=None):
+ self.use_custom_style = True
+ if text_color is None:
+ self.text_color = self.default_text_color
+ else:
+ self.text_color = convert_cocoa_color(text_color)
+ if bg_color is None:
+ self.bg_color = self.default_bg_color
+ else:
+ self.bg_color = convert_cocoa_color(bg_color)
+
+ default_text_color = convert_cocoa_color(NSColor.textColor())
+ default_bg_color = convert_cocoa_color(NSColor.textBackgroundColor())
+
+class DrawingContext:
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self, view, drawing_area, rect):
+ self.view = view
+ self.path = NSBezierPath.bezierPath()
+ self.color = NSColor.blackColor()
+ self.width = drawing_area.size.width
+ self.height = drawing_area.size.height
+ self.origin = drawing_area.origin
+ if drawing_area.origin != NSZeroPoint:
+ xform = NSAffineTransform.transform()
+ xform.translateXBy_yBy_(drawing_area.origin.x,
+ drawing_area.origin.y)
+ xform.concat()
+
+ def move_to(self, x, y):
+ self.path.moveToPoint_(NSPoint(x, y))
+
+ def rel_move_to(self, dx, dy):
+ self.path.relativeMoveToPoint_(NSPoint(dx, dy))
+
+ def line_to(self, x, y):
+ self.path.lineToPoint_(NSPoint(x, y))
+
+ def rel_line_to(self, dx, dy):
+ self.path.relativeLineToPoint_(NSPoint(dx, dy))
+
+ def curve_to(self, x1, y1, x2, y2, x3, y3):
+ self.path.curveToPoint_controlPoint1_controlPoint2_(
+ NSPoint(x3, y3), NSPoint(x1, y1), NSPoint(x2, y2))
+
+ def rel_curve_to(self, dx1, dy1, dx2, dy2, dx3, dy3):
+ self.path.relativeCurveToPoint_controlPoint1_controlPoint2_(
+ NSPoint(dx3, dy3), NSPoint(dx1, dy1), NSPoint(dx2, dy2))
+
+ def arc(self, x, y, radius, angle1, angle2):
+ angle1 = (angle1 * 360) / (2 * math.pi)
+ angle2 = (angle2 * 360) / (2 * math.pi)
+ center = NSPoint(x, y)
+ self.path.appendBezierPathWithArcWithCenter_radius_startAngle_endAngle_(center, radius, angle1, angle2)
+
+ def arc_negative(self, x, y, radius, angle1, angle2):
+ angle1 = (angle1 * 360) / (2 * math.pi)
+ angle2 = (angle2 * 360) / (2 * math.pi)
+ center = NSPoint(x, y)
+ self.path.appendBezierPathWithArcWithCenter_radius_startAngle_endAngle_clockwise_(center, radius, angle1, angle2, YES)
+
+ def rectangle(self, x, y, width, height):
+ rect = NSMakeRect(x, y, width, height)
+ self.path.appendBezierPathWithRect_(rect)
+
+ def set_color(self, color, alpha=1.0):
+ self.color = convert_widget_color(color, alpha)
+ self.color.set()
+
+ def set_shadow(self, color, opacity, offset, blur_radius):
+ shadow = NSShadow.alloc().init()
+ # shadow offset is always in the cocoa coordinates, so we need to
+ # reverse the y part
+ shadow.setShadowOffset_(NSPoint(offset[0], -offset[1]))
+ shadow.setShadowBlurRadius_(blur_radius)
+ shadow.setShadowColor_(convert_widget_color(color, opacity))
+ shadow.set()
+
+ def set_line_width(self, width):
+ self.path.setLineWidth_(width)
+
+ def stroke(self):
+ self.path.stroke()
+ self.path.removeAllPoints()
+
+ def stroke_preserve(self):
+ self.path.stroke()
+
+ def fill(self):
+ self.path.fill()
+ self.path.removeAllPoints()
+
+ def fill_preserve(self):
+ self.path.fill()
+
+ def clip(self):
+ self.path.addClip()
+ self.path.removeAllPoints()
+
+ def save(self):
+ NSGraphicsContext.currentContext().saveGraphicsState()
+
+ def restore(self):
+ NSGraphicsContext.currentContext().restoreGraphicsState()
+
+ def gradient_fill(self, gradient):
+ self.gradient_fill_preserve(gradient)
+ self.path.removeAllPoints()
+
+ def gradient_fill_preserve(self, gradient):
+ context = NSGraphicsContext.currentContext()
+ context.saveGraphicsState()
+ self.path.addClip()
+ gradient.draw()
+ context.restoreGraphicsState()
+
+class Gradient(object):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self, x1, y1, x2, y2):
+ self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
+ self.start_color = None
+ self.end_color = None
+
+ def set_start_color(self, (red, green, blue)):
+ self.start_color = (red, green, blue)
+
+ def set_end_color(self, (red, green, blue)):
+ self.end_color = (red, green, blue)
+
+ def draw(self):
+ start_color = convert_widget_color(self.start_color)
+ end_color = convert_widget_color(self.end_color)
+ nsgradient = NSGradient.alloc().initWithStartingColor_endingColor_(start_color, end_color)
+ start_point = NSPoint(self.x1, self.y1)
+ end_point = NSPoint(self.x2, self.y2)
+ nsgradient.drawFromPoint_toPoint_options_(start_point, end_point, 0)
+
+class DrawingMixin(object):
+ def calc_size_request(self):
+ return self.size_request(self.view.layout_manager)
+
+ # squish width / squish height only make sense on GTK
+ def set_squish_width(self, setting):
+ pass
+
+ def set_squish_height(self, setting):
+ pass
+
+ # Default implementations for methods that subclasses override.
+
+ def is_opaque(self):
+ return False
+
+ def size_request(self, layout_manager):
+ return 0, 0
+
+ def draw(self, context, layout_manager):
+ pass
+
+ def viewport_repositioned(self):
+ # since this is a Mixin class, we want to make sure that our other
+ # classes see the viewport_repositioned() call.
+ super(DrawingMixin, self).viewport_repositioned()
+ self.queue_redraw()
diff --git a/lvc/widgets/osx/drawingwidgets.py b/lvc/widgets/osx/drawingwidgets.py
new file mode 100644
index 0000000..74e8232
--- /dev/null
+++ b/lvc/widgets/osx/drawingwidgets.py
@@ -0,0 +1,67 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+"""drawingviews.py -- views that support custom drawing."""
+
+import wrappermap
+import drawing
+from .base import Widget, SimpleBin, FlippedView
+from .layoutmanager import LayoutManager
+
+class DrawingView(FlippedView):
+ def init(self):
+ self = super(DrawingView, self).init()
+ self.layout_manager = LayoutManager()
+ return self
+
+ def isOpaque(self):
+ return wrappermap.wrapper(self).is_opaque()
+
+ def drawRect_(self, rect):
+ context = drawing.DrawingContext(self, self.bounds(), rect)
+ context.style = drawing.DrawingStyle()
+ wrappermap.wrapper(self).draw(context, self.layout_manager)
+
+class DrawingArea(drawing.DrawingMixin, Widget):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self):
+ Widget.__init__(self)
+ self.view = DrawingView.alloc().init()
+
+class Background(drawing.DrawingMixin, SimpleBin):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self):
+ SimpleBin.__init__(self)
+ self.view = DrawingView.alloc().init()
+
+ def calc_size_request(self):
+ drawing_size = drawing.DrawingMixin.calc_size_request(self)
+ container_size = SimpleBin.calc_size_request(self)
+ return (max(container_size[0], drawing_size[0]),
+ max(container_size[1], drawing_size[1]))
diff --git a/lvc/widgets/osx/fasttypes.c b/lvc/widgets/osx/fasttypes.c
new file mode 100644
index 0000000..72d3b5b
--- /dev/null
+++ b/lvc/widgets/osx/fasttypes.c
@@ -0,0 +1,540 @@
+/*
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+ */
+
+#include <Python.h>
+
+/*
+ * fasttypes.c
+ *
+ * Datastructures written in C to be fast. This used to be a big C++ file
+ * that depended on boost. Nowadays we only define LinkedList, which is easy
+ * enough to implement in pure C.
+ */
+
+static int nodes_deleted = 0; // debugging only
+
+/* forward define python type objects */
+
+static PyTypeObject LinkedListType;
+static PyTypeObject LinkedListIterType;
+
+/* Structure definitions */
+
+typedef struct LinkedListNode {
+ PyObject *obj;
+ struct LinkedListNode* next;
+ struct LinkedListNode* prev;
+ int deleted; // Has this node been removed?
+ int iter_count; // How many LinkedListIters point to this node?
+} LinkedListNode;
+
+typedef struct {
+ PyObject_HEAD
+ int count;
+ LinkedListNode* sentinal;
+ // sentinal object to make list operations simpler/faster and equivalent
+ // to the boost API. It's prev node is the last element in the list and
+ // it's next node is the first
+} LinkedListObject;
+
+typedef struct {
+ PyObject_HEAD
+ LinkedListNode* node;
+ LinkedListObject* list;
+} LinkedListIterObject;
+
+/* LinkedListNode */
+
+void check_node_deleted(LinkedListNode* node)
+{
+ if(node->iter_count <= 0 && node->deleted) {
+ free(node);
+ nodes_deleted += 1;
+ }
+}
+
+static int remove_node(LinkedListObject* self, LinkedListNode* node)
+{
+ if(node->obj == NULL) {
+ PyErr_SetString(PyExc_IndexError, "can't remove lastIter()");
+ return 0;
+ }
+ node->next->prev = node->prev;
+ node->prev->next = node->next;
+ node->deleted = 1;
+ self->count -= 1;
+ Py_DECREF(node->obj);
+ check_node_deleted(node);
+ return 1;
+}
+
+/* LinkedListIter */
+
+void switch_node(LinkedListIterObject* self, LinkedListNode* new_node)
+{
+ LinkedListNode* old_node;
+
+ old_node = self->node;
+ self->node = new_node;
+ old_node->iter_count--;
+ self->node->iter_count++;
+ check_node_deleted(old_node);
+}
+
+// Note that we don't expose the new method to python. We create
+// LinkedListIters in the factory methods firstIter() and lastIter()
+static LinkedListIterObject* LinkedListIterObject_new(LinkedListObject*list,
+ LinkedListNode* node)
+{
+ LinkedListIterObject* self;
+
+ self = (LinkedListIterObject*)PyType_GenericAlloc(&LinkedListIterType, 0);
+ if(self != NULL) {
+ self->node = node;
+ self->list = list;
+ node->iter_count++;
+ }
+ return self;
+}
+
+static void LinkedListIterObject_dealloc(LinkedListIterObject* self)
+{
+ self->node->iter_count--;
+ check_node_deleted(self->node);
+}
+
+static PyObject *LinkedListIter_forward(LinkedListIterObject* self, PyObject *obj)
+{
+ switch_node(self, self->node->next);
+ Py_RETURN_NONE;
+}
+
+static PyObject *LinkedListIter_back(LinkedListIterObject* self, PyObject *obj)
+{
+ switch_node(self, self->node->prev);
+ Py_RETURN_NONE;
+}
+
+static PyObject *LinkedListIter_value(LinkedListIterObject* self, PyObject *obj)
+{
+ PyObject* retval;
+
+ if(self->node->deleted) {
+ PyErr_SetString(PyExc_ValueError, "Node deleted");
+ return NULL;
+ }
+ retval = self->node->obj;
+ if(retval == NULL) {
+ PyErr_SetString(PyExc_IndexError, "can't get value of lastIter()");
+ return NULL;
+ }
+ Py_INCREF(retval);
+ return retval;
+}
+
+static PyObject *LinkedListIter_copy(LinkedListIterObject* self, PyObject *obj)
+{
+ return (PyObject*)LinkedListIterObject_new(self->list, self->node);
+}
+
+static PyObject *LinkedListIter_valid(LinkedListIterObject* self, PyObject *obj)
+{
+ return PyBool_FromLong(self->node->deleted == 0);
+}
+
+PyObject* LinkedListIter_richcmp(LinkedListIterObject *o1,
+ LinkedListIterObject *o2, int opid)
+{
+ if(!PyObject_TypeCheck(o1, &LinkedListIterType) ||
+ !PyObject_TypeCheck(o2, &LinkedListIterType)) {
+ return Py_NotImplemented;
+ }
+ switch(opid) {
+ case Py_EQ:
+ if(o1->node == o2->node) Py_RETURN_TRUE;
+ else Py_RETURN_FALSE;
+ case Py_NE:
+ if(o1->node != o2->node) Py_RETURN_TRUE;
+ else Py_RETURN_FALSE;
+ default:
+ return Py_NotImplemented;
+ }
+}
+
+static PyMethodDef LinkedListIter_methods[] = {
+ {"forward", (PyCFunction)LinkedListIter_forward, METH_NOARGS,
+ "Move to the next element",
+ },
+ {"back", (PyCFunction)LinkedListIter_back, METH_NOARGS,
+ "Move to the previous element",
+ },
+ {"value", (PyCFunction)LinkedListIter_value, METH_NOARGS,
+ "Return the current element",
+ },
+ {"copy", (PyCFunction)LinkedListIter_copy, METH_NOARGS,
+ "Duplicate iter",
+ },
+ {"valid", (PyCFunction)LinkedListIter_valid, METH_NOARGS,
+ "Test if the iter is valid",
+ },
+ {NULL},
+};
+
+static PyTypeObject LinkedListIterType = {
+ PyObject_HEAD_INIT(NULL)
+ 0, /* ob_size */
+ "fasttypes.LinkedListIter", /* tp_name */
+ sizeof(LinkedListIterObject), /* tp_basicsize */
+ 0, /* tp_itemsize */
+ (destructor)LinkedListIterObject_dealloc, /* tp_dealloc */
+ 0, /* tp_print */
+ 0, /* tp_getattr */
+ 0, /* tp_setattr */
+ 0, /* tp_compare */
+ 0, /* tp_repr */
+ 0, /* tp_as_number */
+ 0, /* tp_as_sequence */
+ 0, /* tp_as_mapping */
+ 0, /* tp_hash */
+ 0, /* tp_call */
+ 0, /* tp_str */
+ 0, /* tp_getattro */
+ 0, /* tp_setattro */
+ 0, /* tp_as_buffer */
+ Py_TPFLAGS_DEFAULT|Py_TPFLAGS_HAVE_RICHCOMPARE, /* tp_flags */
+ "fasttypes LinkedListIter", /* tp_doc */
+ 0, /* tp_traverse */
+ 0, /* tp_clear */
+ (richcmpfunc)LinkedListIter_richcmp, /* tp_richcompare */
+ 0, /* tp_weaklistoffset */
+ 0, /* tp_iter */
+ 0, /* tp_iternext */
+ LinkedListIter_methods, /* tp_methods */
+ 0, /* tp_members */
+ 0, /* tp_getset */
+ 0, /* tp_base */
+ 0, /* tp_dict */
+ 0, /* tp_descr_get */
+ 0, /* tp_descr_set */
+ 0, /* tp_dictoffset */
+ 0, /* tp_init */
+ 0, /* tp_alloc */
+ 0, /* tp_new */
+};
+
+/* LinkedList */
+
+LinkedListNode* make_new_node(PyObject* obj, LinkedListNode* prev,
+ LinkedListNode* next)
+{
+ LinkedListNode* retval;
+ retval = malloc(sizeof(LinkedListNode));
+ if(!retval) {
+ PyErr_SetString(PyExc_MemoryError, "can't create new node");
+ return NULL;
+ }
+ Py_XINCREF(obj);
+ retval->obj = obj;
+ retval->prev = prev;
+ retval->next = next;
+ retval->iter_count = retval->deleted = 0;
+ return retval;
+}
+
+void set_iter_type_error(PyObject* obj)
+{
+ // Set an exception when we expected a LinkedListIter and got something
+ // else
+ PyObject* args;
+ PyObject* fmt;
+ PyObject* err_str;
+
+ args = Py_BuildValue("(O)", obj);
+ fmt = PyString_FromString("Expected LinkedListIter, got %r");
+ err_str = PyString_Format(fmt, args);
+ PyErr_SetObject(PyExc_TypeError, err_str);
+ Py_DECREF(fmt);
+ Py_DECREF(err_str);
+ Py_DECREF(args);
+}
+
+static PyObject* insert_before(LinkedListObject* self, LinkedListNode* node,
+ PyObject* obj)
+{
+ LinkedListNode* new_node;
+ PyObject* retval;
+
+ new_node = make_new_node(obj, node->prev, node);
+ if(!new_node) return NULL;
+ node->prev->next = new_node;
+ node->prev = new_node;
+ self->count += 1;
+ retval = (PyObject*)LinkedListIterObject_new(self, new_node);
+ return retval;
+}
+
+static PyObject* LinkedList_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
+{
+ LinkedListObject *self;
+ LinkedListNode *sentinal;
+
+ self = (LinkedListObject *)type->tp_alloc(type, 0);
+ if (self == NULL) return NULL;
+
+ sentinal = make_new_node(NULL, NULL, NULL);
+ if(!sentinal) {
+ Py_DECREF(self);
+ return NULL;
+ }
+ self->sentinal = sentinal->next = sentinal->prev = sentinal;
+ sentinal->iter_count = 1; // prevent the sentinal from being deleted
+ self->count = 0;
+
+ return (PyObject *)self;
+}
+
+static void LinkedList_dealloc(LinkedListObject* self)
+{
+ LinkedListNode *node, *tmp;
+
+ node = self->sentinal->next;
+ while(node != self->sentinal) {
+ node->deleted = 1;
+ tmp = node->next;
+ check_node_deleted(node);
+ node = tmp;
+ }
+
+ self->sentinal->iter_count -= 1;
+ check_node_deleted(self->sentinal);
+ return;
+}
+
+static int LinkedList_init(LinkedListObject *self)
+{
+ self->count = 0;
+ return 0;
+}
+
+static Py_ssize_t LinkedList_len(LinkedListObject *self)
+{
+ return self->count;
+}
+
+static PyObject* LinkedList_get(LinkedListObject *self,
+ LinkedListIterObject *iter)
+{
+ if(!PyObject_TypeCheck(iter, &LinkedListIterType)) {
+ set_iter_type_error((PyObject*)iter);
+ return NULL;
+ }
+ return PyObject_CallMethod((PyObject*)iter, "value", "()");
+}
+int LinkedList_set(LinkedListObject *self, LinkedListIterObject *iter,
+ PyObject *value)
+{
+ if(!PyObject_TypeCheck(iter, &LinkedListIterType)) {
+ set_iter_type_error((PyObject*)iter);
+ return -1;
+ }
+ if(iter->node->deleted) {
+ PyErr_SetString(PyExc_ValueError, "Node deleted");
+ return -1;
+ }
+ if(iter->node->obj == NULL) {
+ PyErr_SetString(PyExc_IndexError, "can't set value of lastIter()");
+ return -1;
+ }
+ if(value == NULL) {
+ if(!remove_node(self, iter->node)) return -1;
+ return 0;
+ }
+ Py_INCREF(value);
+ Py_DECREF(iter->node->obj);
+ iter->node->obj = value;
+ return 0;
+}
+
+static PyObject *LinkedList_insertBefore(LinkedListObject* self, PyObject *args)
+{
+ LinkedListIterObject *iter;
+ PyObject *obj;
+
+ if(!PyArg_ParseTuple(args, "OO", &iter, &obj)) return NULL;
+ if(!PyObject_TypeCheck(iter, &LinkedListIterType)) {
+ set_iter_type_error(obj);
+ return NULL;
+ }
+
+ return insert_before(self, iter->node, obj);
+}
+
+static PyObject *LinkedList_append(LinkedListObject* self, PyObject *obj)
+{
+ return insert_before(self, self->sentinal, obj);
+}
+
+static PyObject *LinkedList_remove(LinkedListObject* self,
+ LinkedListIterObject *iter)
+{
+ LinkedListNode* next_node;
+ if(!PyObject_TypeCheck(iter, &LinkedListIterType)) {
+ set_iter_type_error((PyObject*)iter);
+ return NULL;
+ }
+
+ next_node = iter->node->next;
+ if(!remove_node(self, iter->node)) return NULL;
+ return (PyObject*)LinkedListIterObject_new(self, next_node);
+}
+
+static PyObject *LinkedList_firstIter(LinkedListObject* self, PyObject *obj)
+{
+ PyObject* retval;
+ retval = (PyObject*)LinkedListIterObject_new(self, self->sentinal->next);
+ return retval;
+}
+
+static PyObject *LinkedList_lastIter(LinkedListObject* self, PyObject *obj)
+{
+ PyObject* retval;
+ retval = (PyObject*)LinkedListIterObject_new(self, self->sentinal);
+ return retval;
+}
+
+static PyMappingMethods LinkedListMappingMethods = {
+ (lenfunc)LinkedList_len,
+ (binaryfunc)LinkedList_get,
+ (objobjargproc)LinkedList_set,
+};
+
+static PyMethodDef LinkedList_methods[] = {
+ {"insertBefore", (PyCFunction)LinkedList_insertBefore, METH_VARARGS,
+ "insert an element before iter",
+ },
+ {"append", (PyCFunction)LinkedList_append, METH_O,
+ "append an element to the list",
+ },
+ {"remove", (PyCFunction)LinkedList_remove, METH_O,
+ "remove an element to the list",
+ },
+ {"firstIter", (PyCFunction)LinkedList_firstIter, METH_NOARGS,
+ "get an iter pointing to the first element in the list",
+ },
+ {"lastIter", (PyCFunction)LinkedList_lastIter, METH_NOARGS,
+ "get an iter pointing to the last element in the list",
+ },
+ {NULL},
+};
+
+static PyTypeObject LinkedListType = {
+ PyObject_HEAD_INIT(NULL)
+ 0, /* ob_size */
+ "fasttypes.LinkedList", /* tp_name */
+ sizeof(LinkedListObject), /* tp_basicsize */
+ 0, /* tp_itemsize */
+ (destructor)LinkedList_dealloc, /* tp_dealloc */
+ 0, /* tp_print */
+ 0, /* tp_getattr */
+ 0, /* tp_setattr */
+ 0, /* tp_compare */
+ 0, /* tp_repr */
+ 0, /* tp_as_number */
+ 0, /* tp_as_sequence */
+ &LinkedListMappingMethods, /* tp_as_mapping */
+ 0, /* tp_hash */
+ 0, /* tp_call */
+ 0, /* tp_str */
+ 0, /* tp_getattro */
+ 0, /* tp_setattro */
+ 0, /* tp_as_buffer */
+ Py_TPFLAGS_DEFAULT, /* tp_flags */
+ "fasttypes LinkedList", /* tp_doc */
+ 0, /* tp_traverse */
+ 0, /* tp_clear */
+ 0, /* tp_richcompare */
+ 0, /* tp_weaklistoffset */
+ 0, /* tp_iter */
+ 0, /* tp_iternext */
+ LinkedList_methods, /* tp_methods */
+ 0, /* tp_members */
+ 0, /* tp_getset */
+ 0, /* tp_base */
+ 0, /* tp_dict */
+ 0, /* tp_descr_get */
+ 0, /* tp_descr_set */
+ 0, /* tp_dictoffset */
+ (initproc)LinkedList_init, /* tp_init */
+ 0, /* tp_alloc */
+ LinkedList_new, /* tp_new */
+};
+
+/* Module-level stuff */
+
+static PyObject *count_nodes_deleted(PyObject *obj)
+{
+ return PyInt_FromLong(nodes_deleted);
+}
+
+static PyObject *reset_nodes_deleted(PyObject *obj)
+{
+ nodes_deleted = 0;
+ Py_RETURN_NONE;
+}
+
+
+static PyMethodDef FasttypesMethods[] =
+{
+ {"_count_nodes_deleted", (PyCFunction)count_nodes_deleted, METH_NOARGS,
+ "get a count of how many nodes have been deleted (DEBUGGING ONLY)",
+ },
+ {"_reset_nodes_deleted", (PyCFunction)reset_nodes_deleted, METH_NOARGS,
+ "reset the count of how many nodes have been deleted (DEBUGGING ONLY)",
+ },
+ { NULL, NULL, 0, NULL }
+};
+
+PyMODINIT_FUNC initfasttypes(void)
+{
+ PyObject *m;
+
+ if (PyType_Ready(&LinkedListType) < 0)
+ return;
+
+ if (PyType_Ready(&LinkedListIterType) < 0)
+ return;
+
+ m = Py_InitModule("fasttypes", FasttypesMethods);
+
+ Py_INCREF(&LinkedListType);
+ Py_INCREF(&LinkedListIterType);
+ PyModule_AddObject(m, "LinkedList", (PyObject *)&LinkedListType);
+}
diff --git a/lvc/widgets/osx/helpers.py b/lvc/widgets/osx/helpers.py
new file mode 100644
index 0000000..e4aa23a
--- /dev/null
+++ b/lvc/widgets/osx/helpers.py
@@ -0,0 +1,95 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+"""helper classes."""
+
+import logging
+import traceback
+
+from Foundation import *
+from objc import nil
+
+class NotificationForwarder(NSObject):
+ """Forward notifications from a Cocoa object to a python class.
+ """
+
+ def initWithNSObject_center_(self, nsobject, center):
+ """Initialize the NotificationForwarder nsobject is the NSObject to
+ forward notifications for. It can be nil in which case notifications
+ from all objects will be forwarded.
+
+ center is the NSNotificationCenter to get notifications from. It can
+ be None, in which cas the default notification center is used.
+ """
+ self.nsobject = nsobject
+ self.callback_map = {}
+ if center is None:
+ self.center = NSNotificationCenter.defaultCenter()
+ else:
+ self.center = center
+ return self
+
+ @classmethod
+ def create(cls, object, center=None):
+ """Helper method to call aloc() then initWithNSObject_center_()."""
+ return cls.alloc().initWithNSObject_center_(object, center)
+
+ def connect(self, callback, name):
+ """Register to listen for notifications.
+ Only one callback for each notification name can be connected.
+ """
+
+ if name in self.callback_map:
+ raise ValueError("%s already connected" % name)
+
+ self.callback_map[name] = callback
+ self.center.addObserver_selector_name_object_(self, 'observe:', name,
+ self.nsobject)
+
+ def disconnect(self, name=None):
+ if name is not None:
+ self.center.removeObserver_name_object_(self, name, self.nsobject)
+ self.callback_map.pop(name)
+ else:
+ self.center.removeObserver_(self)
+ self.callback_map.clear()
+
+ def observe_(self, notification):
+ name = notification.name()
+ callback = self.callback_map[name]
+ if callback is None:
+ logging.warn("Callback for %s is dead", name)
+ self.center.removeObverser_name_object_(self, name, self.nsobject)
+ return
+ try:
+ callback(notification)
+ except:
+ logging.warn("Callback for %s raised exception:%s\n",
+ name.encode('utf-8'),
+ traceback.format_exc())
diff --git a/lvc/widgets/osx/layout.py b/lvc/widgets/osx/layout.py
new file mode 100644
index 0000000..f18a47f
--- /dev/null
+++ b/lvc/widgets/osx/layout.py
@@ -0,0 +1,748 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+""".layout -- Widgets that handle laying out other
+widgets.
+
+We basically follow GTK's packing model. Widgets are packed into vboxes,
+hboxes or other container widgets. The child widgets request a minimum size,
+and the container widgets allocate space for their children. Widgets may get
+more size then they requested in which case they have to deal with it. In
+rare cases, widgets may get less size then they requested in which case they
+should just make sure they don't throw an exception or segfault.
+
+Check out the GTK tutorial for more info.
+"""
+
+import itertools
+
+from AppKit import *
+from Foundation import *
+from objc import YES, NO, nil, signature, loadBundle
+
+import tableview
+import wrappermap
+from .base import Container, Bin, FlippedView
+from lvc.utils import Matrix
+
+# These don't seem to be in pyobjc's AppKit (yet)
+NSScrollerKnobStyleDefault = 0
+NSScrollerKnobStyleDark = 1
+NSScrollerKnobStyleLight = 2
+
+NSScrollerStyleLegacy = 0
+NSScrollerStyleOverlay = 1
+
+def _extra_space_iter(extra_length, count):
+ """Utility function to allocate extra space left over in containers."""
+ if count == 0:
+ return
+ extra_space, leftover = divmod(extra_length, count)
+ while leftover >= 1:
+ yield extra_space + 1
+ leftover -= 1
+ yield extra_space + leftover
+ while True:
+ yield extra_space
+
+class BoxPacking:
+ """Utility class to store how we are packing a single widget."""
+
+ def __init__(self, widget, expand, padding):
+ self.widget = widget
+ self.expand = expand
+ self.padding = padding
+
+class Box(Container):
+ """Base class for HBox and VBox. """
+ CREATES_VIEW = False
+
+ def __init__(self, spacing=0):
+ self.spacing = spacing
+ Container.__init__(self)
+ self.packing_start = []
+ self.packing_end = []
+ self.expand_count = 0
+
+ def packing_both(self):
+ return itertools.chain(self.packing_start, self.packing_end)
+
+ def get_children(self):
+ for packing in self.packing_both():
+ yield packing.widget
+ children = property(get_children)
+
+ # Internally Boxes use a (length, breadth) coordinate system. length and
+ # breadth will be either x or y depending on which way the box is
+ # oriented. The subclasses must provide methods to translate between the
+ # 2 coordinate systems.
+
+ def translate_size(self, size):
+ """Translate a (width, height) tulple to (length, breadth)."""
+ raise NotImplementedError()
+
+ def untranslate_size(self, size):
+ """Reverse the work of translate_size."""
+ raise NotImplementedError()
+
+ def make_child_rect(self, position, length):
+ """Create a rect to position a child with."""
+ raise NotImplementedError()
+
+ def pack_start(self, child, expand=False, padding=0):
+ self.packing_start.append(BoxPacking(child, expand, padding))
+ if expand:
+ self.expand_count += 1
+ self.child_added(child)
+
+ def pack_end(self, child, expand=False, padding=0):
+ self.packing_end.append(BoxPacking(child, expand, padding))
+ if expand:
+ self.expand_count += 1
+ self.child_added(child)
+
+ def _remove_from_packing(self, child):
+ for i in xrange(len(self.packing_start)):
+ if self.packing_start[i].widget is child:
+ return self.packing_start.pop(i)
+ for i in xrange(len(self.packing_end)):
+ if self.packing_end[i].widget is child:
+ return self.packing_end.pop(i)
+ raise LookupError("%s not found" % child)
+
+ def remove(self, child):
+ packing = self._remove_from_packing(child)
+ if packing.expand:
+ self.expand_count -= 1
+ self.child_removed(child)
+
+ def translate_widget_size(self, widget):
+ return self.translate_size(widget.get_size_request())
+
+ def calc_size_request(self):
+ length = breadth = 0
+ for packing in self.packing_both():
+ child_length, child_breadth = \
+ self.translate_widget_size(packing.widget)
+ length += child_length
+ if packing.padding:
+ length += packing.padding * 2 # Need to pad on both sides
+ breadth = max(breadth, child_breadth)
+ spaces = max(0, len(self.packing_start) + len(self.packing_end) - 1)
+ length += spaces * self.spacing
+ return self.untranslate_size((length, breadth))
+
+ def place_children(self):
+ request_length, request_breadth = self.translate_widget_size(self)
+ ps = self.viewport.placement.size
+ total_length, dummy = self.translate_size((ps.width, ps.height))
+ total_extra_space = total_length - request_length
+ extra_space_iter = _extra_space_iter(total_extra_space,
+ self.expand_count)
+ start_end = self._place_packing_list(self.packing_start,
+ extra_space_iter, 0)
+ if self.expand_count == 0 and total_extra_space > 0:
+ # account for empty space after the end of pack_start list and
+ # before the pack_end list.
+ self.draw_empty_space(start_end, total_extra_space)
+ start_end += total_extra_space
+ self._place_packing_list(reversed(self.packing_end), extra_space_iter,
+ start_end)
+
+ def draw_empty_space(self, start, length):
+ empty_rect = self.make_child_rect(start, length)
+ my_view = self.viewport.view
+ opaque_view = my_view.opaqueAncestor()
+ if opaque_view is not None:
+ empty_rect2 = opaque_view.convertRect_fromView_(empty_rect, my_view)
+ opaque_view.setNeedsDisplayInRect_(empty_rect2)
+
+ def _place_packing_list(self, packing_list, extra_space_iter, position):
+ for packing in packing_list:
+ child_length, child_breadth = \
+ self.translate_widget_size(packing.widget)
+ if packing.expand:
+ child_length += extra_space_iter.next()
+ if packing.padding: # space before
+ self.draw_empty_space(position, packing.padding)
+ position += packing.padding
+ child_rect = self.make_child_rect(position, child_length)
+ if packing.padding: # space after
+ self.draw_empty_space(position, packing.padding)
+ position += packing.padding
+ packing.widget.place(child_rect, self.viewport.view)
+ position += child_length
+ if self.spacing > 0:
+ self.draw_empty_space(position, self.spacing)
+ position += self.spacing
+ return position
+
+ def enable(self):
+ Container.enable(self)
+ for mem in self.children:
+ mem.enable()
+
+ def disable(self):
+ Container.disable(self)
+ for mem in self.children:
+ mem.disable()
+
+class VBox(Box):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def translate_size(self, size):
+ return (size[1], size[0])
+
+ def untranslate_size(self, size):
+ return (size[1], size[0])
+
+ def make_child_rect(self, position, length):
+ placement = self.viewport.placement
+ return NSMakeRect(placement.origin.x, placement.origin.y + position,
+ placement.size.width, length)
+
+class HBox(Box):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def translate_size(self, size):
+ return (size[0], size[1])
+
+ def untranslate_size(self, size):
+ return (size[0], size[1])
+
+ def make_child_rect(self, position, length):
+ placement = self.viewport.placement
+ return NSMakeRect(placement.origin.x + position, placement.origin.y,
+ length, placement.size.height)
+
+class Alignment(Bin):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ CREATES_VIEW = False
+
+ def __init__(self, xalign=0.0, yalign=0.0, xscale=0.0, yscale=0.0,
+ top_pad=0, bottom_pad=0, left_pad=0, right_pad=0):
+ Bin.__init__(self)
+ self.xalign = xalign
+ self.yalign = yalign
+ self.xscale = xscale
+ self.yscale = yscale
+ self.top_pad = top_pad
+ self.bottom_pad = bottom_pad
+ self.left_pad = left_pad
+ self.right_pad = right_pad
+ if self.child is not None:
+ self.place_children()
+
+ def set(self, xalign=0.0, yalign=0.0, xscale=0.0, yscale=0.0):
+ self.xalign = xalign
+ self.yalign = yalign
+ self.xscale = xscale
+ self.yscale = yscale
+ if self.child is not None:
+ self.place_children()
+
+ def set_padding(self, top_pad=0, bottom_pad=0, left_pad=0, right_pad=0):
+ self.top_pad = top_pad
+ self.bottom_pad = bottom_pad
+ self.left_pad = left_pad
+ self.right_pad = right_pad
+ if self.child is not None and self.viewport is not None:
+ self.place_children()
+
+ def vertical_pad(self):
+ return self.top_pad + self.bottom_pad
+
+ def horizontal_pad(self):
+ return self.left_pad + self.right_pad
+
+ def calc_size_request(self):
+ if self.child:
+ child_width, child_height = self.child.get_size_request()
+ return (child_width + self.horizontal_pad(),
+ child_height + self.vertical_pad())
+ else:
+ return (0, 0)
+
+ def calc_size(self, requested, total, scale):
+ extra_width = max(0, total - requested)
+ return requested + int(round(extra_width * scale))
+
+ def calc_position(self, size, total, align):
+ return int(round((total - size) * align))
+
+ def place_children(self):
+ if self.child is None:
+ return
+
+ total_width = self.viewport.placement.size.width
+ total_height = self.viewport.placement.size.height
+ total_width -= self.horizontal_pad()
+ total_height -= self.vertical_pad()
+ request_width, request_height = self.child.get_size_request()
+
+ child_width = self.calc_size(request_width, total_width, self.xscale)
+ child_height = self.calc_size(request_height, total_height, self.yscale)
+ child_x = self.calc_position(child_width, total_width, self.xalign)
+ child_y = self.calc_position(child_height, total_height, self.yalign)
+ child_x += self.left_pad
+ child_y += self.top_pad
+
+ my_origin = self.viewport.area().origin
+ child_rect = NSMakeRect(my_origin.x + child_x, my_origin.y + child_y, child_width, child_height)
+ self.child.place(child_rect, self.viewport.view)
+ # Make sure the space not taken up by our child is redrawn.
+ self.viewport.queue_redraw()
+
+class DetachedWindowHolder(Alignment):
+ def __init__(self):
+ Alignment.__init__(self, bottom_pad=16, xscale=1.0, yscale=1.0)
+
+class _TablePacking(object):
+ """Utility class to help with packing Table widgets."""
+ def __init__(self, widget, column, row, column_span, row_span):
+ self.widget = widget
+ self.column = column
+ self.row = row
+ self.column_span = column_span
+ self.row_span = row_span
+
+ def column_indexes(self):
+ return range(self.column, self.column + self.column_span)
+
+ def row_indexes(self):
+ return range(self.row, self.row + self.row_span)
+
+class Table(Container):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ CREATES_VIEW = False
+
+ def __init__(self, columns, rows):
+ Container.__init__(self)
+ self._cells = Matrix(columns, rows)
+ self._children = [] # List of _TablePacking objects
+ self._children_sorted = True
+ self.rows = rows
+ self.columns = columns
+ self.row_spacing = self.column_spacing = 0
+
+ def _ensure_children_sorted(self):
+ if not self._children_sorted:
+ def cell_area(table_packing):
+ return table_packing.column_span * table_packing.row_span
+ self._children.sort(key=cell_area)
+ self._children_sorted = True
+
+ def get_children(self):
+ return [cell.widget for cell in self._children]
+ children = property(get_children)
+
+ def calc_size_request(self):
+ self._ensure_children_sorted()
+ self._calc_dimensions()
+ return self.total_width, self.total_height
+
+ def _calc_dimensions(self):
+ self.column_widths = [0] * self.columns
+ self.row_heights = [0] * self.rows
+
+ for tp in self._children:
+ child_width, child_height = tp.widget.get_size_request()
+ # recalc the width of the child's columns
+ self._recalc_dimension(child_width, self.column_widths,
+ tp.column_indexes())
+ # recalc the height of the child's rows
+ self._recalc_dimension(child_height, self.row_heights,
+ tp.row_indexes())
+
+ self.total_width = (self.column_spacing * (self.columns - 1) +
+ sum(self.column_widths))
+ self.total_height = (self.row_spacing * (self.rows - 1) +
+ sum(self.row_heights))
+
+ def _recalc_dimension(self, child_size, size_array, positions):
+ current_size = sum(size_array[p] for p in positions)
+ child_size_needed = child_size - current_size
+ if child_size_needed > 0:
+ iter = _extra_space_iter(child_size_needed, len(positions))
+ for p in positions:
+ size_array[p] += iter.next()
+
+ def place_children(self):
+ # This method depepnds on us calling _calc_dimensions() in
+ # calc_size_request(). Ensure that this happens.
+ if self.cached_size_request is None:
+ self.get_size_request()
+ column_positions = [0]
+ for width in self.column_widths[:-1]:
+ column_positions.append(width + column_positions[-1] + self.column_spacing)
+ row_positions = [0]
+ for height in self.row_heights[:-1]:
+ row_positions.append(height + row_positions[-1] + self.row_spacing)
+
+ my_x= self.viewport.placement.origin.x
+ my_y = self.viewport.placement.origin.y
+ for tp in self._children:
+ x = my_x + column_positions[tp.column]
+ y = my_y + row_positions[tp.row]
+ width = sum(self.column_widths[i] for i in tp.column_indexes())
+ height = sum(self.row_heights[i] for i in tp.row_indexes())
+ rect = NSMakeRect(x, y, width, height)
+ tp.widget.place(rect, self.viewport.view)
+
+ def pack(self, widget, column, row, column_span=1, row_span=1):
+ tp = _TablePacking(widget, column, row, column_span, row_span)
+ for c in tp.column_indexes():
+ for r in tp.row_indexes():
+ if self._cells[c, r]:
+ raise ValueError("Cell %d x %d is already taken" % (c, r))
+ self._cells[column, row] = widget
+ self._children.append(tp)
+ self._children_sorted = False
+ self.child_added(widget)
+
+ def remove(self, child):
+ for i in xrange(len(self._children)):
+ if self._children[i].widget is child:
+ self._children.remove(i)
+ break
+ else:
+ raise ValueError("%s is not a child of this Table" % child)
+ self._cells.remove(child)
+ self.child_removed(widget)
+
+ def set_column_spacing(self, spacing):
+ self.column_spacing = spacing
+ self.invalidate_size_request()
+
+ def set_row_spacing(self, spacing):
+ self.row_spacing = spacing
+ self.invalidate_size_request()
+
+ def enable(self, row=None, column=None):
+ Container.enable(self)
+ if row != None and column != None:
+ if self._cells[column, row]:
+ self._cells[column, row].enable()
+ elif row != None:
+ for mem in self._cells.row(row):
+ if mem: mem.enable()
+ elif column != None:
+ for mem in self._cells.column(column):
+ if mem: mem.enable()
+ else:
+ for mem in self._cells:
+ if mem: mem.enable()
+
+ def disable(self, row=None, column=None):
+ Container.disable(self)
+ if row != None and column != None:
+ if self._cells[column, row]:
+ self._cells[column, row].disable()
+ elif row != None:
+ for mem in self._cells.row(row):
+ if mem: mem.disable()
+ elif column != None:
+ for mem in self._cells.column(column):
+ if mem: mem.disable()
+ else:
+ for mem in self._cells:
+ if mem: mem.disable()
+
+class MiroScrollView(NSScrollView):
+ def tile(self):
+ NSScrollView.tile(self)
+ # tile is called when we need to layout our child view and scrollers.
+ # This probably means that we've either hidden or shown a scrollbar so
+ # call invalidate_size_request to ensure that things get re-layed out
+ # correctly. (#see 13842)
+ wrapper = wrappermap.wrapper(self)
+ if wrapper is not None:
+ wrapper.invalidate_size_request()
+
+class Scroller(Bin):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self, horizontal, vertical):
+ Bin.__init__(self)
+ self.view = MiroScrollView.alloc().init()
+ self.view.setAutohidesScrollers_(YES)
+ self.view.setHasHorizontalScroller_(horizontal)
+ self.view.setHasVerticalScroller_(vertical)
+ self.document_view = FlippedView.alloc().init()
+ self.view.setDocumentView_(self.document_view)
+
+ def prepare_for_dark_content(self):
+ try:
+ self.view.setScrollerKnobStyle_(NSScrollerKnobStyleLight)
+ except AttributeError:
+ # This only works on 10.7 and abvoe
+ pass
+
+ def set_has_borders(self, has_border):
+ self.view.setBorderType_(NSBezelBorder)
+
+ def viewport_repositioned(self):
+ # If the window is resized, this translates to a
+ # viewport_repositioned() event. Instead of calling
+ # place_children() one, which is what our suporclass does, we need
+ # some extra logic here. place the chilren to work out if we need a
+ # scrollbar, then get the new size, then replace the children (which
+ # now takes into account of scrollbar size.)
+ super(Scroller, self).viewport_repositioned()
+ self.cached_size_request = self.calc_size_request()
+ self.place_children()
+
+ def set_background_color(self, color):
+ self.view.setBackgroundColor_(self.make_color(color))
+
+ def add(self, child):
+ child.parent_is_scroller = True
+ Bin.add(self, child)
+
+ def remove(self):
+ child.parent_is_scroller = False
+ Bin.remove(self)
+
+ def children_changed(self):
+ # since our size isn't dependent on our children, don't call
+ # invalidate_size_request() here. Just call place_children() so that
+ # they get positioned correctly in the document view.
+ #
+ # XXX dodgy - why are we laying out the children twice? When the
+ # children change, the scroller could appear/disappear. But you have
+ # no idea if that's going to happen without knowing how big your
+ # children are. So we lay it out, get the size, then, place the
+ # children again. This makes sure that the right side of the children
+ # are redrawn. There's got to be a better way??
+ self.place_children()
+ self.cached_size_request = self.calc_size_request()
+ self.place_children()
+
+ def calc_size_request(self):
+ if self.child:
+ width = height = 0
+ try:
+ legacy = self.view.scrollerStyle() == NSScrollerStyleLegacy
+ except AttributeError:
+ legacy = True
+ if not self.view.hasHorizontalScroller():
+ width = self.child.get_size_request()[0]
+ if not self.view.hasVerticalScroller():
+ height = self.child.get_size_request()[1]
+ # Add a little room for the scrollbars (if necessary)
+ if legacy and self.view.hasHorizontalScroller():
+ height += NSScroller.scrollerWidth()
+ if legacy and self.view.hasVerticalScroller():
+ width += NSScroller.scrollerWidth()
+ return width, height
+ else:
+ return 0, 0
+
+ def place_children(self):
+ if self.child is not None:
+ scroll_view_size = self.view.contentView().frame().size
+ child_width, child_height = self.child.get_size_request()
+ child_width = max(child_width, scroll_view_size.width)
+ child_height = max(child_height, scroll_view_size.height)
+ frame = NSRect(NSPoint(0,0), NSSize(child_width, child_height))
+ if isinstance(self.child, tableview.TableView) and self.child.is_showing_headers():
+ # Hack to allow the content of a table view to scroll, but not
+ # the headers
+ self.child.place(frame, self.document_view)
+ if self.view.documentView() is not self.child.tableview:
+ self.view.setDocumentView_(self.child.tableview)
+ else:
+ self.child.place(frame, self.document_view)
+ self.document_view.setFrame_(frame)
+ self.document_view.setNeedsDisplay_(YES)
+ self.view.setNeedsDisplay_(YES)
+ self.child.emit('place-in-scroller')
+
+class ExpanderView(FlippedView):
+ def init(self):
+ self = super(ExpanderView, self).init()
+ self.label_rect = None
+ self.content_view = None
+ self.button = NSButton.alloc().init()
+ self.button.setState_(NSOffState)
+ self.button.setTitle_("")
+ self.button.setBezelStyle_(NSDisclosureBezelStyle)
+ self.button.setButtonType_(NSPushOnPushOffButton)
+ self.button.sizeToFit()
+ self.addSubview_(self.button)
+ self.button.setTarget_(self)
+ self.button.setAction_('buttonChanged:')
+ self.content_view = FlippedView.alloc().init()
+ return self
+
+ def buttonChanged_(self, button):
+ if button.state() == NSOnState:
+ self.addSubview_(self.content_view)
+ else:
+ self.content_view.removeFromSuperview()
+ if self.window():
+ wrappermap.wrapper(self).invalidate_size_request()
+
+ def mouseDown_(self, event):
+ pass # Just need to respond to the selector so we get mouseUp_
+
+ def mouseUp_(self, event):
+ position = event.locationInWindow()
+ window_label_rect = self.convertRect_toView_(self.label_rect, None)
+ if NSPointInRect(position, window_label_rect):
+ self.button.setNextState()
+ self.buttonChanged_(self.button)
+
+class Expander(Bin):
+ BUTTON_PAD_TOP = 2
+ BUTTON_PAD_LEFT = 4
+ LABEL_SPACING = 4
+
+ def __init__(self, child):
+ Bin.__init__(self)
+ if child:
+ self.add(child)
+ self.label = None
+ self.spacing = 0
+ self.view = ExpanderView.alloc().init()
+ self.button = self.view.button
+ self.button.setFrameOrigin_(NSPoint(self.BUTTON_PAD_LEFT,
+ self.BUTTON_PAD_TOP))
+ self.content_view = self.view.content_view
+
+ def remove_viewport(self):
+ Bin.remove_viewport(self)
+ if self.label is not None:
+ self.label.remove_viewport()
+
+ def set_spacing(self, spacing):
+ self.spacing = spacing
+
+ def set_label(self, widget):
+ if self.label is not None:
+ self.label.remove_viewport()
+ self.label = widget
+ self.children_changed()
+
+ def set_expanded(self, expanded):
+ if expanded:
+ self.button.setState_(NSOnState)
+ else:
+ self.button.setState_(NSOffState)
+ self.view.buttonChanged_(self.button)
+
+ def calc_top_size(self):
+ width = self.button.bounds().size.width
+ height = self.button.bounds().size.height
+ if self.label is not None:
+ label_width, label_height = self.label.get_size_request()
+ width += self.LABEL_SPACING + label_width
+ height = max(height, label_height)
+ width += self.BUTTON_PAD_LEFT
+ height += self.BUTTON_PAD_TOP
+ return width, height
+
+ def calc_size_request(self):
+ width, height = self.calc_top_size()
+ if self.child is not None and self.button.state() == NSOnState:
+ child_width, child_height = self.child.get_size_request()
+ width = max(width, child_width)
+ height += self.spacing + child_height
+ return width, height
+
+ def place_children(self):
+ top_width, top_height = self.calc_top_size()
+ if self.label:
+ label_width, label_height = self.label.get_size_request()
+ button_width = self.button.bounds().size.width
+ label_x = self.BUTTON_PAD_LEFT + button_width + self.LABEL_SPACING
+ label_rect = NSMakeRect(label_x, self.BUTTON_PAD_TOP,
+ label_width, label_height)
+ self.label.place(label_rect, self.viewport.view)
+ self.view.label_rect = label_rect
+ if self.child:
+ size = self.viewport.area().size
+ child_rect = NSMakeRect(0, 0, size.width, size.height -
+ top_height)
+ self.content_view.setFrame_(NSMakeRect(0, top_height, size.width,
+ size.height - top_height))
+ self.child.place(child_rect, self.content_view)
+
+
+class TabViewDelegate(NSObject):
+ def tabView_willSelectTabViewItem_(self, tab_view, tab_view_item):
+ try:
+ wrapper = wrappermap.wrapper(tab_view)
+ except KeyError:
+ pass # The NSTabView hasn't been placed yet, don't worry about it.
+ else:
+ wrapper.place_child_with_item(tab_view_item)
+
+class TabContainer(Container):
+ def __init__(self):
+ Container.__init__(self)
+ self.children = []
+ self.item_to_child = {}
+ self.view = NSTabView.alloc().init()
+ self.view.setAllowsTruncatedLabels_(NO)
+ self.delegate = TabViewDelegate.alloc().init()
+ self.view.setDelegate_(self.delegate)
+
+ def append_tab(self, child_widget, label, image):
+ item = NSTabViewItem.alloc().init()
+ item.setLabel_(label)
+ item.setView_(FlippedView.alloc().init())
+ self.view.addTabViewItem_(item)
+ self.children.append(child_widget)
+ self.child_added(child_widget)
+ self.item_to_child[item] = child_widget
+
+ def select_tab(self, index):
+ self.view.selectTabViewItemAtIndex_(index)
+
+ def place_children(self):
+ self.place_child_with_item(self.view.selectedTabViewItem())
+
+ def place_child_with_item(self, tab_view_item):
+ child = self.item_to_child[tab_view_item]
+ child_view = tab_view_item.view()
+ content_rect =self.view.contentRect()
+ child_view.setFrame_(content_rect)
+ child.place(child_view.bounds(), child_view)
+
+ def calc_size_request(self):
+ tab_size = self.view.minimumSize()
+ # make sure there's enough room for the tabs, plus a little extra
+ # space to make things look good
+ max_width = tab_size.width + 60
+ max_height = 0
+ for child in self.children:
+ width, height = child.get_size_request()
+ max_width = max(width, max_width)
+ max_height = max(height, max_height)
+ max_height += tab_size.height
+
+ return max_width, max_height
diff --git a/lvc/widgets/osx/layoutmanager.py b/lvc/widgets/osx/layoutmanager.py
new file mode 100644
index 0000000..de4301b
--- /dev/null
+++ b/lvc/widgets/osx/layoutmanager.py
@@ -0,0 +1,445 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+"""textlayout.py -- Contains the LayoutManager class. It handles laying text,
+buttons, getting font metrics and other tasks that are required to size
+things.
+"""
+import logging
+import math
+
+from AppKit import *
+from Foundation import *
+from objc import YES, NO, nil
+
+import drawing
+
+INFINITE = 1000000 # size of an "infinite" dimension
+
+class MiroLayoutManager(NSLayoutManager):
+ """Overide NSLayoutManager to draw better underlines."""
+
+ def drawUnderlineForGlyphRange_underlineType_baselineOffset_lineFragmentRect_lineFragmentGlyphRange_containerOrigin_(self, glyph_range, typ, offset, line_rect, line_glyph_range, container_origin):
+ container, _ = self.textContainerForGlyphAtIndex_effectiveRange_(glyph_range.location, None)
+ rect = self.boundingRectForGlyphRange_inTextContainer_(glyph_range, container)
+ x = container_origin.x + rect.origin.x
+ y = (container_origin.y + rect.origin.y + rect.size.height - offset)
+ underline_height, offset = self.calc_underline_extents(glyph_range)
+ y = math.ceil(y + offset) + underline_height / 2.0
+ path = NSBezierPath.bezierPath()
+ path.setLineWidth_(underline_height)
+ path.moveToPoint_(NSPoint(x, y))
+ path.relativeLineToPoint_(NSPoint(rect.size.width, 0))
+ path.stroke()
+
+ def calc_underline_extents(self, line_glyph_range):
+ index = self.characterIndexForGlyphAtIndex_(line_glyph_range.location)
+ font, _ = self.textStorage().attribute_atIndex_effectiveRange_(NSFontAttributeName, index, None)
+ # we use a couple of magic numbers that seems to work okay. I (BDK)
+ # got it from some old mozilla code.
+ height = font.ascender() - font.descender()
+ height = max(1.0, round(0.05 * height))
+ offset = max(1.0, round(0.1 * height))
+ return height, offset
+
+class TextBoxPool(object):
+ """Handles a pool of TextBox objects. We monitor the TextBox objects and
+ when those objects die, we reclaim them for the pool.
+
+ Creating TextBoxes is fairly expensive and NSLayoutManager do a lot of
+ caching, so it's useful to keep them around rather than destroying them.
+ """
+
+ def __init__(self):
+ self.used_text_boxes = []
+ self.available_text_boxes = []
+
+ def get(self):
+ """Get a NSLayoutManager, either from the pool or by creating a new
+ one.
+ """
+ try:
+ rv = self.available_text_boxes.pop()
+ except IndexError:
+ rv = TextBox()
+ self.used_text_boxes.append(rv)
+ return rv
+
+ def reclaim_textboxes(self):
+ """Move used TextBoxes back to the available pool. This should be
+ called after the code using text boxes is done using all of them.
+ """
+ self.available_text_boxes.extend(self.used_text_boxes)
+ self.used_text_boxes[:] = []
+
+text_box_pool = TextBoxPool()
+
+class Font(object):
+ line_height_sizer = NSLayoutManager.alloc().init()
+
+ def __init__(self, nsfont):
+ self.nsfont = nsfont
+
+ def ascent(self):
+ return self.nsfont.ascender()
+
+ def descent(self):
+ return -self.nsfont.descender()
+
+ def line_height(self):
+ return Font.line_height_sizer.defaultLineHeightForFont_(self.nsfont)
+
+class FontPool(object):
+ def __init__(self):
+ self._cached_fonts = {}
+
+ def get(self, scale_factor, bold, italic, family):
+ cache_key = (scale_factor, bold, italic, family)
+ try:
+ return self._cached_fonts[cache_key]
+ except KeyError:
+ font = self._create(scale_factor, bold, italic, family)
+ self._cached_fonts[cache_key] = font
+ return font
+
+ def _create(self, scale_factor, bold, italic, family):
+ size = round(scale_factor * NSFont.systemFontSize())
+ nsfont = None
+ if family is not None:
+ if bold:
+ nsfont = NSFont.fontWithName_size_(family + " Bold", size)
+ else:
+ nsfont = NSFont.fontWithName_size_(family, size)
+ if nsfont is None:
+ logging.error('FontPool: family %s scale %s bold %s '
+ 'italic %s not found',
+ family, scale_factor, bold, italic)
+ # Att his point either we have requested a custom font that failed
+ # to load or the system font was requested.
+ if nsfont is None:
+ if bold:
+ nsfont = NSFont.boldSystemFontOfSize_(size)
+ else:
+ nsfont = NSFont.systemFontOfSize_(size)
+ return Font(nsfont)
+
+class LayoutManager(object):
+ font_pool = FontPool()
+ default_font = font_pool.get(1.0, False, False, None)
+
+ def __init__(self):
+ self.current_font = self.default_font
+ self.set_text_color((0, 0, 0))
+ self.set_text_shadow(None)
+
+ def font(self, scale_factor, bold=False, italic=False, family=None):
+ return self.font_pool.get(scale_factor, bold, italic, family)
+
+ def set_font(self, scale_factor, bold=False, italic=False, family=None):
+ self.current_font = self.font(scale_factor, bold, italic, family)
+
+ def set_text_color(self, color):
+ self.text_color = color
+
+ def set_text_shadow(self, shadow):
+ self.shadow = shadow
+
+ def textbox(self, text, underline=False):
+ text_box = text_box_pool.get()
+ color = NSColor.colorWithDeviceRed_green_blue_alpha_(self.text_color[0], self.text_color[1], self.text_color[2], 1.0)
+ text_box.reset(text, self.current_font, color, self.shadow, underline)
+ return text_box
+
+ def button(self, text, pressed=False, disabled=False, style='normal'):
+ if style == 'webby':
+ return StyledButton(text, self.current_font, pressed, disabled)
+ else:
+ return NativeButton(text, self.current_font, pressed, disabled)
+
+ def reset(self):
+ text_box_pool.reclaim_textboxes()
+ self.current_font = self.default_font
+ self.text_color = (0, 0, 0)
+ self.shadow = None
+
+class TextBox(object):
+ def __init__(self):
+ self.layout_manager = MiroLayoutManager.alloc().init()
+ container = NSTextContainer.alloc().init()
+ container.setLineFragmentPadding_(0)
+ self.layout_manager.addTextContainer_(container)
+ self.layout_manager.setUsesFontLeading_(NO)
+ self.text_storage = NSTextStorage.alloc().init()
+ self.text_storage.addLayoutManager_(self.layout_manager)
+ self.text_container = self.layout_manager.textContainers()[0]
+
+ def reset(self, text, font, color, shadow, underline):
+ """Reset the text box so it's ready to be used by a new owner."""
+ self.text_storage.deleteCharactersInRange_(NSRange(0,
+ self.text_storage.length()))
+ self.text_container.setContainerSize_(NSSize(INFINITE, INFINITE))
+ self.paragraph_style = NSMutableParagraphStyle.alloc().init()
+ self.font = font
+ self.color = color
+ self.shadow = shadow
+ self.width = None
+ self.set_text(text, underline=underline)
+
+ def make_attr_string(self, text, color, font, underline):
+ attributes = NSMutableDictionary.alloc().init()
+ if color is not None:
+ nscolor = NSColor.colorWithDeviceRed_green_blue_alpha_(color[0], color[1], color[2], 1.0)
+ attributes.setObject_forKey_(nscolor, NSForegroundColorAttributeName)
+ else:
+ attributes.setObject_forKey_(self.color, NSForegroundColorAttributeName)
+ if font is not None:
+ attributes.setObject_forKey_(font.nsfont, NSFontAttributeName)
+ else:
+ attributes.setObject_forKey_(self.font.nsfont, NSFontAttributeName)
+ if underline:
+ attributes.setObject_forKey_(NSUnderlineStyleSingle, NSUnderlineStyleAttributeName)
+ attributes.setObject_forKey_(self.paragraph_style.copy(), NSParagraphStyleAttributeName)
+ if text is None:
+ text = ""
+ return NSAttributedString.alloc().initWithString_attributes_(text, attributes)
+
+ def set_text(self, text, color=None, font=None, underline=False):
+ string = self.make_attr_string(text, color, font, underline)
+ self.text_storage.setAttributedString_(string)
+
+ def append_text(self, text, color=None, font=None, underline=False):
+ string = self.make_attr_string(text, color, font, underline)
+ self.text_storage.appendAttributedString_(string)
+
+ def set_width(self, width):
+ if width is not None:
+ self.text_container.setContainerSize_(NSSize(width, INFINITE))
+ else:
+ self.text_container.setContainerSize_(NSSize(INFINITE, INFINITE))
+ self.width = width
+
+ def update_paragraph_style(self):
+ attr = NSParagraphStyleAttributeName
+ value = self.paragraph_style.copy()
+ rnge = NSMakeRange(0, self.text_storage.length())
+ self.text_storage.addAttribute_value_range_(attr, value, rnge)
+
+ def set_wrap_style(self, wrap):
+ if wrap == 'word':
+ self.paragraph_style.setLineBreakMode_(NSLineBreakByWordWrapping)
+ elif wrap == 'char':
+ self.paragraph_style.setLineBreakMode_(NSLineBreakByCharWrapping)
+ elif wrap == 'truncated-char':
+ self.paragraph_style.setLineBreakMode_(NSLineBreakByTruncatingTail)
+ else:
+ raise ValueError("Unknown wrap value: %s" % wrap)
+ self.update_paragraph_style()
+
+ def set_alignment(self, align):
+ if align == 'left':
+ self.paragraph_style.setAlignment_(NSLeftTextAlignment)
+ elif align == 'right':
+ self.paragraph_style.setAlignment_(NSRightTextAlignment)
+ elif align == 'center':
+ self.paragraph_style.setAlignment_(NSCenterTextAlignment)
+ else:
+ raise ValueError("Unknown align value: %s" % align)
+ self.update_paragraph_style()
+
+ def get_size(self):
+ # The next line is there just to force cocoa to layout the text
+ self.layout_manager.glyphRangeForTextContainer_(self.text_container)
+ rect = self.layout_manager.usedRectForTextContainer_(self.text_container)
+ return rect.size.width, rect.size.height
+
+ def char_at(self, x, y):
+ width, height = self.get_size()
+ if 0 <= x < width and 0 <= y < height:
+ index, _ = self.layout_manager.glyphIndexForPoint_inTextContainer_fractionOfDistanceThroughGlyph_(NSPoint(x, y), self.text_container, None)
+ return index
+ else:
+ return None
+
+ def draw(self, context, x, y, width, height):
+ if self.shadow is not None:
+ context.save()
+ context.set_shadow(self.shadow.color, self.shadow.opacity, self.shadow.offset, self.shadow.blur_radius)
+ self.width = width
+ self.text_container.setContainerSize_(NSSize(width, height))
+ glyph_range = self.layout_manager.glyphRangeForTextContainer_(self.text_container)
+ self.layout_manager.drawGlyphsForGlyphRange_atPoint_(glyph_range, NSPoint(x, y))
+ if self.shadow is not None:
+ context.restore()
+ context.path.removeAllPoints()
+
+class NativeButton(object):
+
+ def __init__(self, text, font, pressed, disabled=False):
+ self.min_width = 0
+ self.cell = NSButtonCell.alloc().init()
+ self.cell.setBezelStyle_(NSRoundRectBezelStyle)
+ self.cell.setButtonType_(NSMomentaryPushInButton)
+ self.cell.setFont_(font.nsfont)
+ self.cell.setEnabled_(not disabled)
+ self.cell.setTitle_(text)
+ if pressed:
+ self.cell.setState_(NSOnState)
+ else:
+ self.cell.setState_(NSOffState)
+ self.cell.setImagePosition_(NSImageLeft)
+
+ def set_icon(self, icon):
+ image = icon.image.copy()
+ image.setFlipped_(NO)
+ self.cell.setImage_(image)
+
+ def get_size(self):
+ size = self.cell.cellSize()
+ return size.width, size.height
+
+ def draw(self, context, x, y, width, height):
+ rect = NSMakeRect(x, y, width, height)
+ NSGraphicsContext.currentContext().saveGraphicsState()
+ self.cell.drawWithFrame_inView_(rect, context.view)
+ NSGraphicsContext.currentContext().restoreGraphicsState()
+ context.path.removeAllPoints()
+
+class StyledButton(object):
+ PAD_HORIZONTAL = 11
+ BIG_PAD_VERTICAL = 4
+ SMALL_PAD_VERTICAL = 2
+ TOP_COLOR = (1, 1, 1)
+ BOTTOM_COLOR = (0.86, 0.86, 0.86)
+ LINE_COLOR_TOP = (0.71, 0.71, 0.71)
+ LINE_COLOR_BOTTOM = (0.45, 0.45, 0.45)
+ TEXT_COLOR = (0.19, 0.19, 0.19)
+ DISABLED_COLOR = (0.86, 0.86, 0.86)
+ DISABLED_TEXT_COLOR = (0.43, 0.43, 0.43)
+ ICON_PAD = 8
+
+ def __init__(self, text, font, pressed, disabled=False):
+ self.pressed = pressed
+ self.disabled = disabled
+ attributes = NSMutableDictionary.alloc().init()
+ attributes.setObject_forKey_(font.nsfont, NSFontAttributeName)
+ if self.disabled:
+ color = self.DISABLED_TEXT_COLOR
+ else:
+ color = self.TEXT_COLOR
+ nscolor = NSColor.colorWithDeviceRed_green_blue_alpha_(color[0], color[1], color[2], 1.0)
+ attributes.setObject_forKey_(nscolor, NSForegroundColorAttributeName)
+ self.title = NSAttributedString.alloc().initWithString_attributes_(text, attributes)
+ self.image = None
+
+ def set_icon(self, icon):
+ self.image = icon.image.copy()
+ self.image.setFlipped_(YES)
+
+ def get_size(self):
+ width, height = self.get_text_size()
+ if self.image is not None:
+ width += self.image.size().width + self.ICON_PAD
+ height = max(height, self.image.size().height)
+ height += self.BIG_PAD_VERTICAL * 2
+ else:
+ height += self.SMALL_PAD_VERTICAL * 2
+ if height % 2 == 1:
+ # make height even so that the radius of our circle is whole
+ height += 1
+ width += self.PAD_HORIZONTAL * 2
+ return width, height
+
+ def get_text_size(self):
+ size = self.title.size()
+ return size.width, size.height
+
+ def draw(self, context, x, y, width, height):
+ self._draw_button(context, x, y, width, height)
+ self._draw_title(context, x, y)
+ context.path.removeAllPoints()
+
+ def _draw_button(self, context, x, y, width, height):
+ radius = height / 2
+ self._draw_path(context, x, y, width, height, radius)
+ if self.disabled:
+ end_color = self.DISABLED_COLOR
+ start_color = self.DISABLED_COLOR
+ elif self.pressed:
+ end_color = self.TOP_COLOR
+ start_color = self.BOTTOM_COLOR
+ else:
+ context.set_line_width(1)
+ start_color = self.TOP_COLOR
+ end_color = self.BOTTOM_COLOR
+ gradient = drawing.Gradient(x, y, x, y+height)
+ gradient.set_start_color(start_color)
+ gradient.set_end_color(end_color)
+ context.gradient_fill(gradient)
+ self._draw_border(context, x, y, width, height, radius)
+
+ def _draw_path(self, context, x, y, width, height, radius):
+ inner_width = width - radius * 2
+ context.move_to(x + radius, y)
+ context.rel_line_to(inner_width, 0)
+ context.arc(x + width - radius, y+radius, radius, -math.pi/2, math.pi/2)
+ context.rel_line_to(-inner_width, 0)
+ context.arc(x + radius, y+radius, radius, math.pi/2, -math.pi/2)
+
+ def _draw_path_reverse(self, context, x, y, width, height, radius):
+ inner_width = width - radius * 2
+ context.move_to(x + radius, y)
+ context.arc_negative(x + radius, y+radius, radius, -math.pi/2, math.pi/2)
+ context.rel_line_to(inner_width, 0)
+ context.arc_negative(x + width - radius, y+radius, radius, math.pi/2, -math.pi/2)
+ context.rel_line_to(-inner_width, 0)
+
+ def _draw_border(self, context, x, y, width, height, radius):
+ self._draw_path(context, x, y, width, height, radius)
+ self._draw_path_reverse(context, x+1, y+1, width-2, height-2, radius-1)
+ gradient = drawing.Gradient(x, y, x, y+height)
+ gradient.set_start_color(self.LINE_COLOR_TOP)
+ gradient.set_end_color(self.LINE_COLOR_BOTTOM)
+ context.save()
+ context.clip()
+ context.rectangle(x, y, width, height)
+ context.gradient_fill(gradient)
+ context.restore()
+
+ def _draw_title(self, context, x, y):
+ c_width, c_height = self.get_size()
+ t_width, t_height = self.get_text_size()
+ x = x + self.PAD_HORIZONTAL
+ y = y + (c_height - t_height) / 2
+ if self.image is not None:
+ self.image.drawAtPoint_fromRect_operation_fraction_(
+ NSPoint(x, y+3), NSZeroRect, NSCompositeSourceOver, 1.0)
+ x += self.image.size().width + self.ICON_PAD
+ else:
+ y += 0.5
+ self.title.drawAtPoint_(NSPoint(x, y))
diff --git a/lvc/widgets/osx/osxmenus.py b/lvc/widgets/osx/osxmenus.py
new file mode 100644
index 0000000..91baf3a
--- /dev/null
+++ b/lvc/widgets/osx/osxmenus.py
@@ -0,0 +1,571 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+"""menus.py -- Menu handling code."""
+
+import logging
+import struct
+
+from objc import nil, NO, YES
+import AppKit
+from AppKit import *
+from Foundation import *
+
+from lvc import signals
+from lvc.widgets import keyboard
+# import these names directly into our namespace for easy access
+from lvc.widgets.keyboard import Shortcut, MOD
+
+# XXX hacks
+def _(text, *params):
+ if params:
+ return text % params[0]
+ return text
+
+MODIFIERS_MAP = {
+ keyboard.MOD: NSCommandKeyMask,
+ keyboard.CMD: NSCommandKeyMask,
+ keyboard.SHIFT: NSShiftKeyMask,
+ keyboard.CTRL: NSControlKeyMask,
+ keyboard.ALT: NSAlternateKeyMask
+}
+
+if isinstance(NSBackspaceCharacter, int):
+ backspace = NSBackspaceCharacter
+else:
+ backspace = ord(NSBackspaceCharacter)
+
+KEYS_MAP = {
+ keyboard.SPACE: " ",
+ keyboard.ENTER: "\r",
+ keyboard.BKSPACE: struct.pack("H", backspace),
+ keyboard.DELETE: NSDeleteFunctionKey,
+ keyboard.RIGHT_ARROW: NSRightArrowFunctionKey,
+ keyboard.LEFT_ARROW: NSLeftArrowFunctionKey,
+ keyboard.UP_ARROW: NSUpArrowFunctionKey,
+ keyboard.DOWN_ARROW: NSDownArrowFunctionKey,
+ '.': '.',
+ ',': ','
+}
+# add function keys
+for i in range(1, 13):
+ portable_key = getattr(keyboard, "F%s" % i)
+ osx_key = getattr(AppKit, "NSF%sFunctionKey" % i)
+ KEYS_MAP[portable_key] = osx_key
+
+REVERSE_MODIFIERS_MAP = dict((i[1], i[0]) for i in MODIFIERS_MAP.items())
+REVERSE_KEYS_MAP = dict((i[1], i[0]) for i in KEYS_MAP.items()
+ if i[0] != keyboard.BKSPACE)
+REVERSE_KEYS_MAP[u'\x7f'] = keyboard.BKSPACE
+REVERSE_KEYS_MAP[u'\x1b'] = keyboard.ESCAPE
+
+def make_modifier_mask(shortcut):
+ mask = 0
+ for modifier in shortcut.modifiers:
+ mask |= MODIFIERS_MAP[modifier]
+ return mask
+
+VIEW_ITEM_MAP = {}
+
+def _remove_mnemonic(label):
+ """Remove the underscore used by GTK for mnemonics.
+
+ We totally ignore them on OSX, since they are now deprecated.
+ """
+ return label.replace("_", "")
+
+def handle_menu_activate(ns_menu_item):
+ """Handle a menu item being activated.
+
+ This gets called by our application delegate.
+ """
+
+ menu_item = ns_menu_item.representedObject()
+ menu_item.emit("activate")
+ menubar = menu_item._find_menubar()
+ if menubar is not None:
+ menubar.emit("activate", menu_item.name)
+
+class MenuItemBase(signals.SignalEmitter):
+ """Base class for MenuItem and Separator"""
+ def __init__(self):
+ signals.SignalEmitter.__init__(self)
+ self.name = None
+ self.parent = None
+
+ def show(self):
+ self._menu_item.setHidden_(False)
+
+ def hide(self):
+ self._menu_item.setHidden_(True)
+
+ def enable(self):
+ self._menu_item.setEnabled_(True)
+
+ def disable(self):
+ self._menu_item.setEnabled_(False)
+
+ def remove_from_parent(self):
+ """Remove this menu item from it's parent Menu."""
+ if self.parent is not None:
+ self.parent.remove(self)
+
+class MenuItem(MenuItemBase):
+ """See the GTK version of this method for the current docstring."""
+
+ # map Miro action names to standard OSX actions.
+ _STD_ACTION_MAP = {
+ "HideMiro": (NSApp(), 'hide:'),
+ "HideOthers": (NSApp(), 'hideOtherApplications:'),
+ "ShowAll": (NSApp(), 'unhideAllApplications:'),
+ "Cut": (nil, 'cut:'),
+ "Copy": (nil, 'copy:'),
+ "Paste": (nil, 'paste:'),
+ "Delete": (nil, 'delete:'),
+ "SelectAll": (nil, 'selectAll:'),
+ "Zoom": (nil, 'performZoom:'),
+ "Minimize": (nil, 'performMiniaturize:'),
+ "BringAllToFront": (nil, 'arrangeInFront:'),
+ "CloseWindow": (nil, 'performClose:'),
+ }
+
+ def __init__(self, label, name, shortcut=None):
+ MenuItemBase.__init__(self)
+ self.name = name
+ self._menu_item = self._make_menu_item(label)
+ self.create_signal('activate')
+ self._setup_shortcut(shortcut)
+
+ def _make_menu_item(self, label):
+ menu_item = NSMenuItem.alloc().init()
+ menu_item.setTitle_(_remove_mnemonic(label))
+ # we set ourselves as the represented object for the menu item so we
+ # can easily translate one to the other
+ menu_item.setRepresentedObject_(self)
+ if self.name in self._STD_ACTION_MAP:
+ menu_item.setTarget_(self._STD_ACTION_MAP[self.name][0])
+ menu_item.setAction_(self._STD_ACTION_MAP[self.name][1])
+ else:
+ menu_item.setTarget_(NSApp().delegate())
+ menu_item.setAction_('handleMenuActivate:')
+ return menu_item
+
+ def _setup_shortcut(self, shortcut):
+ if shortcut is None:
+ key = ''
+ modifier_mask = 0
+ elif isinstance(shortcut.shortcut, str):
+ key = shortcut.shortcut
+ modifier_mask = make_modifier_mask(shortcut)
+ elif shortcut.shortcut in KEYS_MAP:
+ key = KEYS_MAP[shortcut.shortcut]
+ modifier_mask = make_modifier_mask(shortcut)
+ else:
+ logging.warn("Don't know how to handle shortcut: %s", shortcut)
+ return
+ self._menu_item.setKeyEquivalent_(key)
+ self._menu_item.setKeyEquivalentModifierMask_(modifier_mask)
+
+ def _change_shortcut(self, shortcut):
+ self._setup_shortcut(shortcut)
+
+ def set_label(self, new_label):
+ self._menu_item.setTitle_(new_label)
+
+ def get_label(self):
+ self._menu_item.title()
+
+ def _find_menubar(self):
+ """Remove this menu item from it's parent Menu."""
+ menu_item = self
+ while menu_item.parent is not None:
+ menu_item = menu_item.parent
+ if isinstance(menu_item, MenuBar):
+ return menu_item
+ else:
+ return None
+
+class CheckMenuItem(MenuItem):
+ """See the GTK version of this method for the current docstring."""
+ def set_state(self, active):
+ if active is None:
+ state = NSMixedState
+ elif active:
+ state = NSOnState
+ else:
+ state = NSOffState
+ self._menu_item.setState_(state)
+
+ def get_state(self):
+ return self._menu_item.state() == NSOnState
+
+ def do_activate(self):
+ if self._menu_item.state() == NSOffState:
+ self._menu_item.setState_(NSOnState)
+ else:
+ self._menu_item.setState_(NSOffState)
+
+class RadioMenuItem(CheckMenuItem):
+ """See the GTK version of this method for the current docstring."""
+ def __init__(self, label, name, shortcut=None):
+ CheckMenuItem.__init__(self, label, name, shortcut)
+ # The leader of a radio group stores the list of all items in the
+ # group
+ self.group_leader = None
+ self.others_in_group = set()
+
+ def set_group(self, group_item):
+ if self.group_leader is not None:
+ raise ValueError("%s is already in a group" % self)
+ if group_item.group_leader is None:
+ group_leader = group_item
+ else:
+ group_leader = group_item.group_leader
+ if group_leader.group_leader is not None:
+ raise AssertionError("group_leader structure is wrong")
+ self.group_leader = group_leader
+ group_leader.others_in_group.add(self)
+
+ def remove_from_group(self):
+ """Remove this RadioMenuItem from its current group."""
+ if self.group_leader is not None:
+ # we have a group leader, remove ourself from their list.
+ # Note that this code will work even if we're the last item in
+ # others_in_group.
+ self.group_leader.others_in_group.remove(self)
+ self.group_leader = None
+ elif len(self.others_in_group) > 1:
+ # we're the group leader, hand off the leader to a different item
+ first_item = iter(self.others_in_group).next()
+ for other in self.others_in_group:
+ if other is first_item:
+ other.others_in_group = self.others_in_group
+ other.others_in_group.remove(first_item)
+ other.group_leader = None
+ else:
+ other.group_leader = first_item
+ self.others_in_group = set()
+ elif len(self.others_in_group) == 1:
+ # we're the group leader, but there's only 1 other item. unset
+ # everything.
+ for other in self.others_in_group:
+ other.group_leader = None
+ self.others_in_group = set()
+
+ def _items_in_group(self):
+ if self.group_leader is not None: # we have a group leader
+ yield self.group_leader
+ for other in self.group_leader.others_in_group:
+ yield other
+ elif self.others_in_group: # we're the group leader
+ yield self
+ for other in self.others_in_group:
+ yield other
+ else: # we don't have a group set
+ yield self
+
+ def do_activate(self):
+ for item in self._items_in_group():
+ if item is not self:
+ item.set_state(False)
+ CheckMenuItem.do_activate(self)
+
+class Separator(MenuItemBase):
+ """See the GTK version of this method for the current docstring."""
+ def __init__(self):
+ MenuItemBase.__init__(self)
+ self._menu_item = NSMenuItem.separatorItem()
+
+class MenuShell(signals.SignalEmitter):
+ def __init__(self, nsmenu):
+ signals.SignalEmitter.__init__(self)
+ self._menu = nsmenu
+ self.children = []
+ self.parent = None
+
+ def append(self, menu_item):
+ """Add a menu item to the end of this menu."""
+ self.children.append(menu_item)
+ self._menu.addItem_(menu_item._menu_item)
+ menu_item.parent = self
+
+ def insert(self, index, menu_item):
+ """Insert a menu item in the middle of this menu."""
+ self.children.insert(index, menu_item)
+ self._menu.insertItem_atIndex_(menu_item._menu_item, index)
+ menu_item.parent = self
+
+ def index(self, name):
+ """Find the position of a child menu item."""
+ for i, menu_item in enumerate(self.children):
+ if menu_item.name == name:
+ return i
+ return -1
+
+ def remove(self, menu_item):
+ """Remove a child menu item.
+
+ :raises ValueError: menu_item is not a child of this menu
+ """
+ self.children.remove(menu_item)
+ self._menu.removeItem_(menu_item._menu_item)
+ menu_item.parent = None
+
+ def get_children(self):
+ """Get the child menu items in order."""
+ return list(self.children)
+
+ def find(self, name):
+ """Search for a menu or menu item
+
+ This method recursively searches the entire menu structure for a Menu
+ or MenuItem object with a given name.
+
+ :raises KeyError: name not found
+ """
+ found = self._find(name)
+ if found is None:
+ raise KeyError(name)
+ else:
+ return found
+
+ def _find(self, name):
+ """Low-level helper-method for find().
+
+ :returns: found menu item or None.
+ """
+ for menu_item in self.get_children():
+ if menu_item.name == name:
+ return menu_item
+ if isinstance(menu_item, Menu):
+ submenu_find = menu_item._find(name)
+ if submenu_find is not None:
+ return submenu_find
+ return None
+
+class Menu(MenuShell):
+ """See the GTK version of this method for the current docstring."""
+ def __init__(self, label, name, child_items=None):
+ MenuShell.__init__(self, NSMenu.alloc().init())
+ self._menu.setTitle_(_remove_mnemonic(label))
+ # we will enable/disable menu items manually
+ self._menu.setAutoenablesItems_(False)
+ self.name = name
+ if child_items is not None:
+ for item in child_items:
+ self.append(item)
+ self._menu_item = NSMenuItem.alloc().init()
+ self._menu_item.setTitle_(_remove_mnemonic(label))
+ self._menu_item.setSubmenu_(self._menu)
+ # Hack to set the services menu
+ if name == "ServicesMenu":
+ NSApp().setServicesMenu_(self._menu_item)
+
+ def show(self):
+ self._menu_item.setHidden_(False)
+
+ def hide(self):
+ self._menu_item.setHidden_(True)
+
+ def enable(self):
+ self._menu_item.setEnabled_(True)
+
+ def disable(self):
+ self._menu_item.setEnabled_(False)
+
+class AppMenu(MenuShell):
+ """Wrapper for the application menu (AKA the Miro menu)
+
+ We need to special case this because OSX automatically creates the menu
+ item.
+ """
+ def __init__(self):
+ MenuShell.__init__(self, NSApp().mainMenu().itemAtIndex_(0).submenu())
+ self.name = "Libre Video Converter"
+
+class MenuBar(MenuShell):
+ """See the GTK version of this method for the current docstring."""
+ def __init__(self):
+ MenuShell.__init__(self, NSApp().mainMenu())
+ self.create_signal('activate')
+ self._add_app_menu()
+
+ def _add_app_menu(self):
+ """Add the app menu to this menu bar.
+
+ We need to special case this because OSX automatically adds the
+ NSMenuItem for the app menu, we just need to set up our wrappers.
+ """
+ self._app_menu = AppMenu()
+ self.children.append(self._app_menu)
+ self._app_menu.parent = self
+
+ def add_initial_menus(self, menus):
+ for menu in menus:
+ self.append(menu)
+ self._modify_initial_menus()
+
+ def _extract_menu_item(self, name):
+ """Helper method for changing the portable menu structure."""
+ menu_item = self.find(name)
+ menu_item.remove_from_parent()
+ return menu_item
+
+ def _modify_initial_menus(self):
+ short_appname = "Libre Video Converter" # XXX
+
+ # Application menu
+ miroMenuItems = [
+ self._extract_menu_item("About"),
+ Separator(),
+ self._extract_menu_item("Quit")
+ ]
+
+ for item in miroMenuItems:
+ self._app_menu.append(item)
+
+ self._app_menu.find("Quit").set_label(_("Quit %(appname)s",
+ {"appname": short_appname}))
+
+ # Help Menu
+ #helpItem = self.find("Help")
+ #helpItem.set_label(_("%(appname)s Help", {"appname": short_appname}))
+ #helpItem._change_shortcut(Shortcut("?", MOD))
+
+ self._update_present_menu()
+ self._connect_to_signals()
+
+ def do_activate(self, name):
+ # We handle a couple OSX-specific actions here
+ if name == "PresentActualSize":
+ NSApp().delegate().present_movie('natural-size')
+ elif name == "PresentDoubleSize":
+ NSApp().delegate().present_movie('double-size')
+ elif name == "PresentHalfSize":
+ NSApp().delegate().present_movie('half-size')
+ elif name == "ShowMain":
+ app.widgetapp.window.nswindow.makeKeyAndOrderFront_(self)
+
+ def _connect_to_signals(self):
+ return
+ app.playback_manager.connect("will-play", self._on_playback_change)
+ app.playback_manager.connect("will-stop", self._on_playback_change)
+
+ def _on_playback_change(self, playback_manager, *args):
+ self._update_present_menu()
+
+ def _update_present_menu(self):
+ return
+ if self._should_enable_present_menu():
+ for menu_item in self.present_menu.get_children():
+ menu_item.enable()
+ else:
+ for menu_item in self.present_menu.get_children():
+ menu_item.disable()
+
+ def _should_enable_present_menu(self):
+ return False
+ if (app.playback_manager.is_playing and
+ not app.playback_manager.is_playing_audio):
+ # we're currently playing video, allow the user to fullscreen
+ return True
+ selection_info = app.item_list_controller_manager.get_selection_info()
+ if (selection_info.has_download and
+ selection_info.has_file_type('video')):
+ # A downloaded video is selected, allow the user to start playback
+ # in fullscreen
+ return True
+ return False
+
+#class ContextMenuHandler(NSObject):
+# def initWithCallback_(self, callback):
+# self = super(ContextMenuHandler, self).init()
+# self.callback = callback
+# return self
+#
+# def handleMenuItem_(self, sender):
+# self.callback()
+#
+#class MiroContextMenu(NSMenu):
+# # Works exactly like NSMenu, except it keeps a reference to the menu
+# # handler objects.
+# def init(self):
+# self = super(MiroContextMenu, self).init()
+# self.handlers = set()
+# return self
+#
+# def addItem_(self, item):
+# if isinstance(item.target(), ContextMenuHandler):
+# self.handlers.add(item.target())
+# return NSMenu.addItem_(self, item)
+#
+def make_context_menu(menu_items):
+ nsmenu = MiroContextMenu.alloc().init()
+ for item in menu_items:
+ if item is None:
+ nsitem = NSMenuItem.separatorItem()
+ else:
+ label, callback = item
+ nsitem = NSMenuItem.alloc().init()
+ if isinstance(label, tuple) and len(label) == 2:
+ label, icon_path = label
+ image = NSImage.alloc().initWithContentsOfFile_(icon_path)
+ nsitem.setImage_(image)
+ if callback is None:
+ font_size = NSFont.systemFontSize()
+ font = NSFont.fontWithName_size_("Lucida Sans Italic", font_size)
+ if font is None:
+ font = NSFont.systemFontOfSize_(font_size)
+ attributes = {NSFontAttributeName: font}
+ attributed_label = NSAttributedString.alloc().initWithString_attributes_(label, attributes)
+ nsitem.setAttributedTitle_(attributed_label)
+ else:
+ nsitem.setTitle_(label)
+ if isinstance(callback, list):
+ submenu = make_context_menu(callback)
+ nsmenu.setSubmenu_forItem_(submenu, nsitem)
+ else:
+ handler = ContextMenuHandler.alloc().initWithCallback_(callback)
+ nsitem.setTarget_(handler)
+ nsitem.setAction_('handleMenuItem:')
+ nsmenu.addItem_(nsitem)
+ return nsmenu
+
+def translate_event_modifiers(event):
+ mods = set()
+ flags = event.modifierFlags()
+ if flags & NSCommandKeyMask:
+ mods.add(keyboard.CMD)
+ if flags & NSControlKeyMask:
+ mods.add(keyboard.CTRL)
+ if flags & NSAlternateKeyMask:
+ mods.add(keyboard.ALT)
+ if flags & NSShiftKeyMask:
+ mods.add(keyboard.SHIFT)
+ return mods
diff --git a/lvc/widgets/osx/rect.py b/lvc/widgets/osx/rect.py
new file mode 100644
index 0000000..3c8d448
--- /dev/null
+++ b/lvc/widgets/osx/rect.py
@@ -0,0 +1,78 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+""".rect -- Simple Rectangle class."""
+
+from Foundation import NSMakeRect, NSRectFromString
+
+class Rect(object):
+ @classmethod
+ def from_string(cls, rect_string):
+ if rect_string.startswith('{{'):
+ return NSRectWrapper(NSRectFromString(rect_string))
+ else:
+ try:
+ items = [int(i) for i in rect_string.split(',')]
+ return Rect(*items)
+ except:
+ return None
+
+ def __init__(self, x, y, width, height):
+ self.nsrect = NSMakeRect(x, y, width, height)
+
+ def get_x(self):
+ return self.nsrect.origin.x
+ def set_x(self, x):
+ self.nsrect.origin.x = x
+ x = property(get_x, set_x)
+
+ def get_y(self):
+ return self.nsrect.origin.y
+ def set_y(self, y):
+ self.nsrect.origin.x = y
+ y = property(get_y, set_y)
+
+ def get_width(self):
+ return self.nsrect.size.width
+ def set_width(self, width):
+ self.nsrect.size.width = width
+ width = property(get_width, set_width)
+
+ def get_height(self):
+ return self.nsrect.size.height
+ def set_height(self, height):
+ self.nsrect.size.height = height
+ height = property(get_height, set_height)
+
+ def __str__(self):
+ return "%d,%d,%d,%d" % (self.nsrect.origin.x, self.nsrect.origin.y, self.nsrect.size.width, self.nsrect.size.height)
+
+class NSRectWrapper(Rect):
+ def __init__(self, nsrect):
+ self.nsrect = nsrect
diff --git a/lvc/widgets/osx/simple.py b/lvc/widgets/osx/simple.py
new file mode 100644
index 0000000..37407a1
--- /dev/null
+++ b/lvc/widgets/osx/simple.py
@@ -0,0 +1,376 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+from __future__ import division
+import logging
+import math
+
+from AppKit import *
+from Foundation import *
+from objc import YES, NO, nil
+
+from lvc.widgets import widgetconst
+from .base import Widget, SimpleBin, FlippedView
+from .utils import filename_to_unicode
+import drawing
+import wrappermap
+
+"""A collection of various simple widgets."""
+
+class Image(object):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self, path):
+ self._set_image(NSImage.alloc().initByReferencingFile_(
+ filename_to_unicode(path)))
+
+ def _set_image(self, nsimage):
+ self.nsimage = nsimage
+ self.width = self.nsimage.size().width
+ self.height = self.nsimage.size().height
+ if self.width * self.height == 0:
+ raise ValueError('Image has invalid size: (%d, %d)' % (
+ self.width, self.height))
+
+ def resize(self, width, height):
+ return ResizedImage(self, width, height)
+
+ def crop_and_scale(self, src_x, src_y, src_width, src_height, dest_width,
+ dest_height):
+ if dest_width <= 0 or dest_height <= 0:
+ logging.stacktrace("invalid dest sizes: %s %s" % (dest_width,
+ dest_height))
+ return TransformedImage(self.nsimage)
+
+ source_rect = NSMakeRect(src_x, src_y, src_width, src_height)
+ dest_rect = NSMakeRect(0, 0, dest_width, dest_height)
+
+ dest = NSImage.alloc().initWithSize_(NSSize(dest_width, dest_height))
+ dest.lockFocus()
+ try:
+ NSGraphicsContext.currentContext().setImageInterpolation_(
+ NSImageInterpolationHigh)
+ self.nsimage.drawInRect_fromRect_operation_fraction_(dest_rect,
+ source_rect, NSCompositeCopy, 1.0)
+ finally:
+ dest.unlockFocus()
+ return TransformedImage(dest)
+
+ def resize_for_space(self, width, height):
+ """Returns an image scaled to fit into the specified space at the
+ correct height/width ratio.
+ """
+ # this prevents division by 0.
+ if self.width == 0 and self.height == 0:
+ return self
+ elif self.width == 0:
+ ratio = height / self.height
+ return self.resize(self.width, ratio * self.height)
+ elif self.height == 0:
+ ratio = width / self.width
+ return self.resize(ratio * self.width, self.height)
+
+ ratio = min(width / self.width, height / self.height)
+ return self.resize(ratio * self.width, ratio * self.height)
+
+class ResizedImage(Image):
+ def __init__(self, image, width, height):
+ nsimage = image.nsimage.copy()
+ nsimage.setCacheMode_(NSImageCacheNever)
+ nsimage.setScalesWhenResized_(YES)
+ nsimage.setSize_(NSSize(width, height))
+ self._set_image(nsimage)
+
+class TransformedImage(Image):
+ def __init__(self, nsimage):
+ self._set_image(nsimage)
+
+class NSImageDisplay(NSView):
+ def init(self):
+ self = super(NSImageDisplay, self).init()
+ self.border = False
+ self.image = None
+ return self
+
+ def isFlipped(self):
+ return YES
+
+ def set_border(self, border):
+ self.border = border
+
+ def set_image(self, image):
+ self.image = image
+
+ def drawRect_(self, dest_rect):
+ if self.image is not None:
+ source_rect = self.calculateSourceRectFromDestRect_(dest_rect)
+ context = NSGraphicsContext.currentContext()
+ context.setShouldAntialias_(YES)
+ context.setImageInterpolation_(NSImageInterpolationHigh)
+ context.saveGraphicsState()
+ drawing.flip_context(self.bounds().size.height)
+ self.image.nsimage.drawInRect_fromRect_operation_fraction_(
+ dest_rect, source_rect, NSCompositeSourceOver, 1.0)
+ context.restoreGraphicsState()
+ if self.border:
+ context = drawing.DrawingContext(self, self.bounds(), dest_rect)
+ context.style = drawing.DrawingStyle()
+ context.set_line_width(1)
+ context.set_color((0, 0, 0)) # black
+ context.rectangle(0, 0, context.width, context.height)
+ context.stroke()
+
+ def calculateSourceRectFromDestRect_(self, dest_rect):
+ """Calulate where dest_rect maps to on our image.
+
+ This is tricky because our image might be scaled up, in which case
+ the rect from our image will be smaller than dest_rect.
+ """
+ view_size = self.frame().size
+ x_scale = float(self.image.width) / view_size.width
+ y_scale = float(self.image.height) / view_size.height
+
+ return NSMakeRect(dest_rect.origin.x * x_scale,
+ dest_rect.origin.y * y_scale,
+ dest_rect.size.width * x_scale,
+ dest_rect.size.height * y_scale)
+
+ # XXX FIXME: should track mouse movement - mouseDown is not the correct
+ # event.
+ def mouseDown_(self, event):
+ wrappermap.wrapper(self).emit('clicked')
+
+class ImageDisplay(Widget):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self, image=None):
+ Widget.__init__(self)
+ self.create_signal('clicked')
+ self.view = NSImageDisplay.alloc().init()
+ self.set_image(image)
+
+ def set_image(self, image):
+ self.image = image
+ if image:
+ image.nsimage.setCacheMode_(NSImageCacheNever)
+ self.view.set_image(image)
+ self.invalidate_size_request()
+
+ def set_border(self, border):
+ self.view.set_border(border)
+
+ def calc_size_request(self):
+ if self.image is not None:
+ return self.image.width, self.image.height
+ else:
+ return 0, 0
+
+class ClickableImageButton(ImageDisplay):
+ def __init__(self, image_path, max_width=None, max_height=None):
+ ImageDisplay.__init__(self)
+ self.set_border(True)
+ self.max_width = max_width
+ self.max_height = max_height
+ self.image = None
+ self._width, self._height = None, None
+ if image_path:
+ self.set_path(image_path)
+
+ def set_path(self, path):
+ image = Image(path)
+ if self.max_width:
+ image = image.resize_for_space(self.max_width, self.max_height)
+ super(ClickableImageButton, self).set_image(image)
+
+ def calc_size_request(self):
+ if self.max_width:
+ return self.max_width, self.max_height
+ else:
+ return ImageDisplay.calc_size_request(self)
+
+class MiroImageView(NSImageView):
+ def viewWillMoveToWindow_(self, aWindow):
+ self.setAnimates_(not aWindow == nil)
+
+class AnimatedImageDisplay(Widget):
+ def __init__(self, path):
+ Widget.__init__(self)
+ self.nsimage = NSImage.alloc().initByReferencingFile_(
+ filename_to_unicode(path))
+ self.view = MiroImageView.alloc().init()
+ self.view.setImage_(self.nsimage)
+ # enabled when viewWillMoveToWindow:aWindow invoked
+ self.view.setAnimates_(NO)
+
+ def calc_size_request(self):
+ return self.nsimage.size().width, self.nsimage.size().height
+
+class Label(Widget):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self, text="", wrap=False, color=None):
+ Widget.__init__(self)
+ self.view = NSTextField.alloc().init()
+ self.view.setEditable_(NO)
+ self.view.setBezeled_(NO)
+ self.view.setBordered_(NO)
+ self.view.setDrawsBackground_(NO)
+ self.wrap = wrap
+ self.bold = False
+ self.size = NSFont.systemFontSize()
+ self.sizer_cell = self.view.cell().copy()
+ self.set_font()
+ self.set_text(text)
+ self.__color = self.view.textColor()
+ if color is not None:
+ self.set_color(color)
+
+ def get_width(self):
+ return self.calc_size_request()[0]
+
+ def set_bold(self, bold):
+ self.bold = bold
+ self.set_font()
+
+ def set_size(self, size):
+ if size > 0:
+ self.size = NSFont.systemFontSize() * size
+ elif size == widgetconst.SIZE_SMALL:
+ self.size = NSFont.smallSystemFontSize()
+ elif size == widgetconst.SIZE_NORMAL:
+ self.size = NSFont.systemFontSize()
+ else:
+ raise ValueError("Unknown size constant: %s" % size)
+
+ self.set_font()
+
+ def set_color(self, color):
+ self.__color = self.make_color(color)
+
+ if self.view.isEnabled():
+ self.view.setTextColor_(self.__color)
+ else:
+ self.view.setTextColor_(self.__color.colorWithAlphaComponent_(0.5))
+
+ def set_background_color(self, color):
+ self.view.setBackgroundColor_(self.make_color(color))
+ self.view.setDrawsBackground_(YES)
+
+ def set_font(self):
+ if self.bold:
+ font = NSFont.boldSystemFontOfSize_(self.size)
+ else:
+ font= NSFont.systemFontOfSize_(self.size)
+ self.view.setFont_(font)
+ self.sizer_cell.setFont_(font)
+ self.invalidate_size_request()
+
+ def calc_size_request(self):
+ if (self.wrap and self.manual_size_request is not None and
+ self.manual_size_request[0] > 0):
+ wrap_width = self.manual_size_request[0]
+ size = self.sizer_cell.cellSizeForBounds_(NSMakeRect(0, 0,
+ wrap_width, 10000))
+ else:
+ size = self.sizer_cell.cellSize()
+ return math.ceil(size.width), math.ceil(size.height)
+
+ def baseline(self):
+ return -self.view.font().descender()
+
+ def set_text(self, text):
+ self.view.setStringValue_(text)
+ self.sizer_cell.setStringValue_(text)
+ self.invalidate_size_request()
+
+ def get_text(self):
+ val = self.view.stringValue()
+ if not val:
+ val = u''
+ return val
+
+ def set_selectable(self, val):
+ self.view.setSelectable_(val)
+
+ def set_alignment(self, alignment):
+ self.view.setAlignment_(alignment)
+
+ def get_alignment(self, alignment):
+ return self.view.alignment()
+
+ def set_wrap(self, wrap):
+ self.wrap = True
+ self.invalidate_size_request()
+
+ def enable(self):
+ Widget.enable(self)
+ self.view.setTextColor_(self.__color)
+ self.view.setEnabled_(True)
+
+ def disable(self):
+ Widget.disable(self)
+ self.view.setTextColor_(self.__color.colorWithAlphaComponent_(0.5))
+ self.view.setEnabled_(False)
+
+class SolidBackground(SimpleBin):
+ def __init__(self, color=None):
+ SimpleBin.__init__(self)
+ self.view = FlippedView.alloc().init()
+ if color is not None:
+ self.set_background_color(color)
+
+ def set_background_color(self, color):
+ self.view.setBackgroundColor_(self.make_color(color))
+
+class ProgressBar(Widget):
+ def __init__(self):
+ Widget.__init__(self)
+ self.view = NSProgressIndicator.alloc().init()
+ self.view.setMaxValue_(1.0)
+ self.view.setIndeterminate_(False)
+
+ def calc_size_request(self):
+ return 20, 20
+
+ def set_progress(self, fraction):
+ self.view.setIndeterminate_(False)
+ self.view.setDoubleValue_(fraction)
+
+ def start_pulsing(self):
+ self.view.setIndeterminate_(True)
+ self.view.startAnimation_(nil)
+
+ def stop_pulsing(self):
+ self.view.stopAnimation_(nil)
+
+class HLine(Widget):
+ def __init__(self):
+ Widget.__init__(self)
+ self.view = NSBox.alloc().init()
+ self.view.setBoxType_(NSBoxSeparator)
+
+ def calc_size_request(self):
+ return self.view.frame().size.width, self.view.frame().size.height
diff --git a/lvc/widgets/osx/tablemodel.py b/lvc/widgets/osx/tablemodel.py
new file mode 100644
index 0000000..d81a8f5
--- /dev/null
+++ b/lvc/widgets/osx/tablemodel.py
@@ -0,0 +1,532 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+"""tablemodel.py -- Model classes for TableView. """
+
+import logging
+
+from AppKit import (NSDragOperationNone, NSDragOperationAll, NSTableViewDropOn,
+ NSOutlineViewDropOnItemIndex, protocols)
+from Foundation import NSObject, NSNotFound, NSMutableIndexSet
+from objc import YES, NO, nil
+
+from lvc import signals
+from lvc.errors import WidgetActionError
+import fasttypes
+import wrappermap
+
+MIRO_DND_ITEM_LOCAL = 'miro-local-item'
+
+# XXX need unsigned but value comes out as signed.
+NSDragOperationEvery = NSDragOperationAll
+
+def list_from_nsindexset(index_set):
+ rows = list()
+ index = index_set.firstIndex()
+ while (index != NSNotFound):
+ rows.append(index)
+ index = index_set.indexGreaterThanIndex_(index)
+ return rows
+
+class RowList(object):
+ """RowList is a Linked list that has some optimizations for looking up
+ rows by index number.
+ """
+ def __init__(self):
+ self.list = fasttypes.LinkedList()
+ self.iter_cache = []
+
+ def firstIter(self):
+ return self.list.firstIter()
+
+ def lastIter(self):
+ return self.list.lastIter()
+
+ def insertBefore(self, iter, value):
+ self.iter_cache = []
+ if iter is None:
+ return self.list.append(value)
+ else:
+ return self.list.insertBefore(iter, value)
+
+ def append(self, value):
+ return self.list.append(value)
+
+ def __len__(self):
+ return len(self.list)
+
+ def __getitem__(self, iter):
+ return self.list[iter]
+
+ def __iter__(self):
+ iter = self.firstIter()
+ while iter != self.lastIter():
+ yield iter.value()
+ iter.forward()
+
+ def remove(self, iter):
+ self.iter_cache = []
+ return self.list.remove(iter)
+
+ def nth_iter(self, index):
+ if index < 0:
+ raise IndexError(index)
+ elif index >= len(self):
+ raise LookupError()
+ if len(self.iter_cache) == 0:
+ self.iter_cache.append(self.firstIter())
+ try:
+ return self.iter_cache[index].copy()
+ except IndexError:
+ pass
+ iter = self.iter_cache[-1].copy()
+ index -= len(self.iter_cache) - 1
+ for x in xrange(index):
+ iter.forward()
+ self.iter_cache.append(iter.copy())
+ return iter
+
+class TableModelBase(signals.SignalEmitter):
+ """Base class for TableModel and TreeTableModel."""
+ def __init__(self, *column_types):
+ signals.SignalEmitter.__init__(self)
+ self.row_list = RowList()
+ self.column_types = column_types
+ self.create_signal('row-changed')
+ self.create_signal('structure-will-change')
+
+ def check_column_values(self, column_values):
+ if len(self.column_types) != len(column_values):
+ raise ValueError("Wrong number of columns")
+ # We might want to do more typechecking here
+
+ def get_column_data(self, row, column):
+ attr_map = column.attrs()
+ return dict((name, row[index]) for name, index in attr_map.items())
+
+ def update_value(self, iter, index, value):
+ iter.value().values[index] = value
+ self.emit('row-changed', iter)
+
+ def update(self, iter, *column_values):
+ iter.value().update_values(column_values)
+ self.emit('row-changed', iter)
+
+ def remove(self, iter):
+ self.emit('structure-will-change')
+ row_list = self.containing_list(iter)
+ rv = row_list.remove(iter)
+ if rv == row_list.lastIter():
+ rv = None
+ return rv
+
+ def nth_iter(self, index):
+ return self.row_list.nth_iter(index)
+
+ def next_iter(self, iter):
+ row_list = self.containing_list(iter)
+ retval = iter.copy()
+ retval.forward()
+ if retval == row_list.lastIter():
+ return None
+ else:
+ return retval
+
+ def first_iter(self):
+ if len(self.row_list) > 0:
+ return self.row_list.firstIter()
+ else:
+ return None
+
+ def __len__(self):
+ return len(self.row_list)
+
+ def __getitem__(self, iter):
+ return iter.value()
+
+ def __iter__(self):
+ return iter(self.row_list)
+
+class TableRow(object):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPITableView for a description of the API for this class."""
+ def __init__(self, column_values):
+ self.update_values(column_values)
+
+ def update_values(self, column_values):
+ self.values = list(column_values)
+
+ def __getitem__(self, index):
+ return self.values[index]
+
+ def __len__(self):
+ return len(self.values)
+
+ def __iter__(self):
+ return iter(self.values)
+
+class TableModel(TableModelBase):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPITableView for a description of the API for this class."""
+ def __init__(self, *column_types):
+ TableModelBase.__init__(self, column_types)
+ self.row_indexes = {}
+
+ def remember_row_at_index(self, row, index):
+ if row not in self.row_indexes:
+ self.row_indexes[row] = index
+
+ def row_of_iter(self, tableview, iter):
+ row = iter.value()
+ try:
+ return self.row_indexes[row]
+ except KeyError:
+ iter = self.row_list.firstIter()
+ index = 0
+ while iter != self.row_list.lastIter():
+ current_row = iter.value()
+ self.row_indexes[current_row] = index
+ if current_row is row:
+ return index
+ index += 1
+ iter.forward()
+ raise LookupError("%s is not in this table" % row)
+
+ def containing_list(self, iter):
+ return self.row_list
+
+ def append(self, *column_values):
+ self.emit('structure-will-change')
+ self.row_indexes = {}
+ retval = self.row_list.append(TableRow(column_values))
+ return retval
+
+ def remove(self, iter):
+ self.row_indexes = {}
+ return TableModelBase.remove(self, iter)
+
+ def insert_before(self, iter, *column_values):
+ self.emit('structure-will-change')
+ self.row_indexes = {}
+ row = TableRow(column_values)
+ retval = self.row_list.insertBefore(iter, row)
+ return retval
+
+ def iter_for_row(self, tableview, row):
+ return self.row_list.nth_iter(row)
+
+
+class TreeNode(NSObject, TableRow):
+ """A row in a TreeTableModel"""
+
+ # Implementation note: these need to be NSObjects because we return them
+ # to the NSOutlineView.
+
+ def initWithValues_parent_(self, column_values, parent):
+ self.children = RowList()
+ self.update_values(column_values)
+ self.parent = parent
+ return self
+
+ @staticmethod
+ def create_(values, parent):
+ return TreeNode.alloc().initWithValues_parent_(values, parent)
+
+ def iterchildren(self):
+ return iter(self.children)
+
+class TreeTableModel(TableModelBase):
+ """https://develop.participatoryculture.org/index.php/WidgetAPITableView"""
+ def __init__(self, *column_values):
+ TableModelBase.__init__(self, *column_values)
+ self.iter_for_item = {}
+
+ def containing_list(self, iter):
+ return self.row_list_for_iter(iter.value().parent)
+
+ def row_list_for_iter(self, iter):
+ """Return the rows of all direct children of iter."""
+ if iter is None:
+ return self.row_list
+ else:
+ return iter.value().children
+
+ def remember_iter(self, iter):
+ self.iter_for_item[iter.value()] = iter
+ return iter
+
+ def append(self, *column_values):
+ self.emit('structure-will-change')
+ retval = self.row_list.append(TreeNode.create_(column_values, None))
+ return self.remember_iter(retval)
+
+ def forget_iter_for_item(self, item):
+ del self.iter_for_item[item]
+ for child in item.children:
+ self.forget_iter_for_item(child)
+
+ def remove(self, iter):
+ item = iter.value()
+ rv = TableModelBase.remove(self, iter)
+ self.forget_iter_for_item(item)
+ return rv
+
+ def insert_before(self, iter, *column_values):
+ self.emit('structure-will-change')
+ row = TreeNode.create_(column_values, self.parent_iter(iter))
+ retval = self.containing_list(iter).insertBefore(iter, row)
+ return self.remember_iter(retval)
+
+ def append_child(self, iter, *column_values):
+ self.emit('structure-will-change')
+ row_list = self.row_list_for_iter(iter)
+ retval = row_list.append(TreeNode.create_(column_values, iter))
+ return self.remember_iter(retval)
+
+ def child_iter(self, iter):
+ row_list = iter.value().children
+ if len(row_list) == 0:
+ return None
+ else:
+ return row_list.firstIter()
+
+ def nth_child_iter(self, iter, index):
+ row_list = self.row_list_for_iter(iter)
+ return row_list.nth_iter(index)
+
+ def has_child(self, iter):
+ return len(iter.value().children) > 0
+
+ def children_count(self, iter):
+ if iter is not None:
+ return len(iter.value().children)
+ else:
+ return len(self.row_list)
+
+ def children_iters(self, iter):
+ return self.iters_in_rowlist(self.row_list_for_iter(iter))
+
+ def parent_iter(self, iter):
+ return iter.value().parent
+
+ def iter_for_row(self, tableview, row):
+ item = tableview.itemAtRow_(row)
+ if item in self.iter_for_item:
+ return self.iter_for_item[item]
+ elif item == -1:
+ raise WidgetActionError("no item at row %s" % row)
+ else:
+ raise WidgetActionError("no iter for item %s at row %s" %
+ (repr(item), row))
+
+ def row_of_iter(self, tableview, iter):
+ item = iter.value()
+ row = tableview.rowForItem_(item)
+ if row == -1:
+ raise LookupError("%s is not in this table" % repr(item))
+ return row
+
+ def get_path(self, iter_):
+ """Not implemented (yet?) for Cocoa. Currently the only place this is
+ needed is tablistmanager, where the situation that uses paths results
+ from GTK peculiarities.
+ """
+ return NotImplemented
+
+class DataSourceBase(NSObject):
+ def initWithModel_(self, model):
+ self.model = model
+ self.drag_source = None
+ self.drag_dest = None
+ return self
+
+ def setDragSource_(self, drag_source):
+ self.drag_source = drag_source
+
+ def setDragDest_(self, drag_dest):
+ self.drag_dest = drag_dest
+
+ def view_writeColumnData_ToPasteboard_(self, view, data, pasteboard):
+ if not self.drag_source:
+ return NO
+ wrapper = wrappermap.wrapper(view)
+ drag_data = self.drag_source.begin_drag(wrapper, data)
+ if not drag_data:
+ return NO
+ pasteboard.declareTypes_owner_((MIRO_DND_ITEM_LOCAL,), self)
+ for typ, value in drag_data.items():
+ stringval = repr((repr(value), typ))
+ pasteboard.setString_forType_(stringval, MIRO_DND_ITEM_LOCAL)
+ return YES
+
+ def calcType_(self, drag_info):
+ source_actions = drag_info.draggingSourceOperationMask()
+ if not (self.drag_dest and
+ (self.drag_dest.allowed_actions() | source_actions)):
+ return None
+ types = self.drag_dest.allowed_types()
+ available = drag_info.draggingPasteboard().availableTypeFromArray_(
+ (MIRO_DND_ITEM_LOCAL,))
+ if available:
+ # XXX using eval() sucks.
+ data = eval(drag_info.draggingPasteboard().stringForType_(
+ MIRO_DND_ITEM_LOCAL))
+ if data:
+ _, typ = data
+ return typ
+ return None
+
+ def validateDrop_dragInfo_parentIter_position_(self, view, drag_info,
+ parent, position):
+ typ = self.calcType_(drag_info)
+ if typ:
+ wrapper = wrappermap.wrapper(view)
+ drop_action = self.drag_dest.validate_drop(
+ wrapper, self.model, typ,
+ drag_info.draggingSourceOperationMask(), parent,
+ position)
+ if not drop_action:
+ return NSDragOperationNone
+ if isinstance(drop_action, (tuple, list)):
+ drop_action, iter = drop_action
+ view.setDropRow_dropOperation_(
+ self.model.row_of_iter(view, iter),
+ NSTableViewDropOn)
+ return drop_action
+ else:
+ return NSDragOperationNone
+
+ def acceptDrop_dragInfo_parentIter_position_(self, view, drag_info,
+ parent, position):
+ typ = self.calcType_(drag_info)
+ if typ:
+ # XXX using eval sucks.
+ data = eval(drag_info.draggingPasteboard().stringForType_(MIRO_DND_ITEM_LOCAL))
+ ids, _ = data
+ ids = eval(ids)
+ wrapper = wrappermap.wrapper(view)
+ self.drag_dest.accept_drop(wrapper, self.model, typ,
+ drag_info.draggingSourceOperationMask(), parent,
+ position, ids)
+ return YES
+ else:
+ return NO
+
+class MiroTableViewDataSource(DataSourceBase, protocols.NSTableDataSource):
+ def numberOfRowsInTableView_(self, table_view):
+ return len(self.model)
+
+ def tableView_objectValueForTableColumn_row_(self, table_view, column, row):
+ node = self.model.nth_iter(row).value()
+ self.model.remember_row_at_index(node, row)
+ return self.model.get_column_data(node.values, column)
+
+ def tableView_writeRowsWithIndexes_toPasteboard_(self, tableview, rowIndexes,
+ pasteboard):
+ indexes = list_from_nsindexset(rowIndexes)
+ data = [self.model[self.model.nth_iter(i)] for i in indexes]
+ return self.view_writeColumnData_ToPasteboard_(tableview, data,
+ pasteboard)
+
+ def translateRow_operation_(self, row, operation):
+ if operation == NSTableViewDropOn:
+ return self.model.nth_iter(row), -1
+ else:
+ return None, row
+
+ def tableView_validateDrop_proposedRow_proposedDropOperation_(self,
+ tableview, drag_info, row, operation):
+ parent, position = self.translateRow_operation_(row, operation)
+ drop_action = self.validateDrop_dragInfo_parentIter_position_(tableview,
+ drag_info, parent, position)
+ if isinstance(drop_action, (list, tuple)):
+ # XXX nothing uses this yet
+ drop_action, iter = drop_action
+ tableview.setDropRow_dropOperation_(
+ self.model.row_of_iter(tableview, iter),
+ NSTableViewDropOn)
+ return drop_action
+
+ def tableView_acceptDrop_row_dropOperation_(self,
+ tableview, drag_info, row, operation):
+ parent, position = self.translateRow_operation_(row, operation)
+ return self.acceptDrop_dragInfo_parentIter_position_(tableview,
+ drag_info, parent, position)
+
+
+class MiroOutlineViewDataSource(DataSourceBase, protocols.NSOutlineViewDataSource):
+ def outlineView_child_ofItem_(self, view, child, item):
+ if item is nil:
+ row_list = self.model.row_list
+ else:
+ row_list = item.children
+ return row_list.nth_iter(child).value()
+
+ def outlineView_isItemExpandable_(self, view, item):
+ if item is not nil and hasattr(item, 'children'):
+ return len(item.children) > 0
+ else:
+ return len(self.model) > 0
+
+ def outlineView_numberOfChildrenOfItem_(self, view, item):
+ if item is not nil and hasattr(item, 'children'):
+ return len(item.children)
+ else:
+ return len(self.model)
+
+ def outlineView_objectValueForTableColumn_byItem_(self, view, column,
+ item):
+ return self.model.get_column_data(item.values, column)
+
+ def outlineView_writeItems_toPasteboard_(self, outline_view, items,
+ pasteboard):
+ data = [i.values for i in items]
+ return self.view_writeColumnData_ToPasteboard_(outline_view, data,
+ pasteboard)
+
+ def outlineView_validateDrop_proposedItem_proposedChildIndex_(self,
+ outlineview, drag_info, item, child_index):
+ if item is None:
+ iter = None
+ else:
+ iter = self.model.iter_for_item[item]
+ drop_action = self.validateDrop_dragInfo_parentIter_position_(
+ outlineview, drag_info, iter, child_index)
+ if isinstance(drop_action, (tuple, list)):
+ drop_action, iter = drop_action
+ outlineview.setDropItem_dropChildIndex_(
+ iter.value(), NSOutlineViewDropOnItemIndex)
+ return drop_action
+
+ def outlineView_acceptDrop_item_childIndex_(self, outlineview, drag_info,
+ item, child_index):
+ if item is None:
+ iter = None
+ else:
+ iter = self.model.iter_for_item[item]
+ return self.acceptDrop_dragInfo_parentIter_position_(outlineview,
+ drag_info, iter, child_index)
diff --git a/lvc/widgets/osx/tableview.py b/lvc/widgets/osx/tableview.py
new file mode 100644
index 0000000..9f490d2
--- /dev/null
+++ b/lvc/widgets/osx/tableview.py
@@ -0,0 +1,1629 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+""".tableview -- TableView widget and it's
+associated classes.
+"""
+
+import math
+import logging
+from contextlib import contextmanager
+from collections import namedtuple
+
+from AppKit import *
+from Foundation import *
+from objc import YES, NO, nil
+
+from lvc import signals
+from lvc import errors
+from lvc.widgets import widgetconst
+from lvc.widgets.tableselection import SelectionOwnerMixin
+from lvc.widgets.tablescroll import ScrollbarOwnerMixin
+from .utils import filename_to_unicode
+import wrappermap
+import tablemodel
+import osxmenus
+from .base import Widget
+from .simple import Image
+from .drawing import DrawingContext, DrawingStyle, Gradient, ImageSurface
+from .helpers import NotificationForwarder
+from .layoutmanager import LayoutManager
+
+EXPANDER_PADDING = 6
+HEADER_HEIGHT = 17
+CUSTOM_HEADER_HEIGHT = 25
+
+def iter_range(ns_range):
+ """Iterate over an NSRange object"""
+ return xrange(ns_range.location, ns_range.location + ns_range.length)
+
+Rect = namedtuple('Rect', 'x y width height')
+def NSRectToRect(nsrect):
+ origin, size = nsrect.origin, nsrect.size
+ return Rect(origin.x, origin.y, size.width, size.height)
+
+Point = namedtuple('Point', 'x y')
+def NSPointToPoint(nspoint):
+ return Point(int(nspoint.x), int(nspoint.y))
+
+class HotspotTracker(object):
+ """Contains the info on the currently tracked hotspot. See:
+ https://develop.participatoryculture.org/index.php/WidgetAPITableView
+ """
+ def __init__(self, tableview, point):
+ self.tableview = tableview
+ self.row = tableview.rowAtPoint_(point)
+ self.column = tableview.columnAtPoint_(point)
+ if self.row == -1 or self.column == -1:
+ self.hit = False
+ return
+ model = tableview.dataSource().model
+ self.iter = model.iter_for_row(tableview, self.row)
+ self.table_column = tableview.tableColumns()[self.column]
+ self.cell = self.table_column.dataCell()
+ self.update_position(point)
+ if isinstance(self.cell, CustomTableCell):
+ self.name = self.calc_hotspot()
+ else:
+ self.name = None
+ self.hit = (self.name is not None)
+
+ def is_for_context_menu(self):
+ return self.name == '#show-context-menu'
+
+ def calc_cell_hotspot(self, column, row):
+ if (self.hit and self.column == column and self.row == row):
+ return self.name
+ else:
+ return None
+
+ def update_position(self, point):
+ cell_frame = self.tableview.frameOfCellAtColumn_row_(self.column,
+ self.row)
+ self.pos = NSPoint(point.x - cell_frame.origin.x,
+ point.y - cell_frame.origin.y)
+
+ def update_hit(self):
+ old_hit = self.hit
+ self.hit = (self.calc_hotspot() == self.name)
+ if old_hit != self.hit:
+ self.redraw_cell()
+
+ def set_cell_data(self):
+ model = self.tableview.dataSource().model
+ row = model[self.iter]
+ value_dict = model.get_column_data(row, self.table_column)
+ self.cell.setObjectValue_(value_dict)
+ self.cell.set_wrapper_data()
+
+ def calc_hotspot(self):
+ self.set_cell_data()
+ cell_frame = self.tableview.frameOfCellAtColumn_row_(self.column,
+ self.row)
+ style = self.cell.make_drawing_style(cell_frame, self.tableview)
+ layout_manager = self.cell.layout_manager
+ layout_manager.reset()
+ return self.cell.wrapper.hotspot_test(style, layout_manager,
+ self.pos.x, self.pos.y, cell_frame.size.width,
+ cell_frame.size.height)
+
+ def redraw_cell(self):
+ # Check to see if we removed the table in response to a hotspot click.
+ if self.tableview.superview() is not nil:
+ cell_frame = self.tableview.frameOfCellAtColumn_row_(self.column,
+ self.row)
+ self.tableview.setNeedsDisplayInRect_(cell_frame)
+
+def _calc_interior_frame(total_frame, tableview):
+ """Calculate the inner cell area for a table cell.
+
+ We tell cocoa that the intercell spacing is (0, 0) and instead handle the
+ spacing ourselves. This method calculates the area that a cell should
+ render to, given the total spacing.
+ """
+ return NSMakeRect(total_frame.origin.x + tableview.column_spacing // 2,
+ total_frame.origin.y + tableview.row_spacing // 2,
+ total_frame.size.width - tableview.column_spacing,
+ total_frame.size.height - tableview.row_spacing)
+
+class MiroTableCell(NSTextFieldCell):
+ def init(self):
+ return super(MiroTableCell, self).initTextCell_('')
+
+ def calcHeight_(self, view):
+ font = self.font()
+ return math.ceil(font.ascender() + abs(font.descender()) +
+ font.leading())
+
+ def highlightColorWithFrame_inView_(self, frame, view):
+ return nil
+
+ def setObjectValue_(self, value_dict):
+ if isinstance(value_dict, dict):
+ NSCell.setObjectValue_(self, value_dict['value'])
+ else:
+ # OS X calls setObjectValue_('') on intialization
+ NSCell.setObjectValue_(self, value_dict)
+
+ def drawInteriorWithFrame_inView_(self, frame, view):
+ return NSTextFieldCell.drawInteriorWithFrame_inView_(self,
+ _calc_interior_frame(frame, view), view)
+
+class MiroTableInfoListTextCell(MiroTableCell):
+ def initWithAttrGetter_(self, attr_getter):
+ self = self.init()
+ self.setWraps_(NO)
+ self.attr_getter = attr_getter
+ self._textColor = self.textColor()
+ return self
+
+ def drawWithFrame_inView_(self, frame, view):
+ # adjust frame based on the cell spacing
+ frame = _calc_interior_frame(frame, view)
+ if (self.isHighlighted() and frame is not None and
+ (view.isDescendantOf_(view.window().firstResponder()) or
+ view.gradientHighlight) and view.window().isMainWindow()):
+ self.setTextColor_(NSColor.whiteColor())
+ else:
+ self.setTextColor_(self._textColor)
+ return MiroTableCell.drawWithFrame_inView_(self, frame, view)
+
+ def titleRectForBounds_(self, rect):
+ frame = MiroTableCell.titleRectForBounds_(self, rect)
+ text_size = self.attributedStringValue().size()
+ frame.origin.y = rect.origin.y + (rect.size.height - text_size.height) / 2.0
+ return frame
+
+ def drawInteriorWithFrame_inView_(self, frame, view):
+ rect = self.titleRectForBounds_(frame)
+ self.attributedStringValue().drawInRect_(rect)
+
+ def setObjectValue_(self, value):
+ if isinstance(value, tuple):
+ info, attrs, group_info = value
+ cell_text = self.attr_getter(info)
+ NSCell.setObjectValue_(self, cell_text)
+ else:
+ # Getting set to a something other than a model row, usually this
+ # happens in initialization
+ NSCell.setObjectValue_(self, '')
+
+class MiroTableImageCell(NSImageCell):
+ def calcHeight_(self, view):
+ return self.value_dict['image'].size().height
+
+ def highlightColorWithFrame_inView_(self, frame, view):
+ return nil
+
+ def setObjectValue_(self, value_dict):
+ NSImageCell.setObjectValue_(self, value_dict['image'])
+
+ def drawInteriorWithFrame_inView_(self, frame, view):
+ return NSImageCell.drawInteriorWithFrame_inView_(self,
+ _calc_interior_frame(frame, view), view)
+
+class MiroCheckboxCell(NSButtonCell):
+ def init(self):
+ self = super(MiroCheckboxCell, self).init()
+ self.setButtonType_(NSSwitchButton)
+ self.setTitle_('')
+ return self
+
+ def calcHeight_(self, view):
+ return self.cellSize().height
+
+ def highlightColorWithFrame_inView_(self, frame, view):
+ return nil
+
+ def setObjectValue_(self, value_dict):
+ if isinstance(value_dict, dict):
+ NSButtonCell.setObjectValue_(self, value_dict['value'])
+ else:
+ # OS X calls setObjectValue_('') on intialization
+ NSCell.setObjectValue_(self, value_dict)
+
+ def startTrackingAt_inView_(self, point, view):
+ return YES
+
+ def continueTracking_at_inView_(self, lastPoint, at, view):
+ return YES
+
+ def stopTracking_at_inView_mouseIsUp_(self, lastPoint, at, tableview, mouseIsUp):
+ if mouseIsUp:
+ column = tableview.columnAtPoint_(at)
+ row = tableview.rowAtPoint_(at)
+ if column != -1 and row != -1:
+ wrapper = wrappermap.wrapper(tableview)
+ column = wrapper.columns[column]
+ itr = wrapper.model.iter_for_row(tableview, row)
+ column.renderer.emit('clicked', itr)
+ return NSButtonCell.stopTracking_at_inView_mouseIsUp_(self, lastPoint,
+ at, tableview, mouseIsUp)
+
+ def drawInteriorWithFrame_inView_(self, frame, view):
+ return NSButtonCell.drawInteriorWithFrame_inView_(self,
+ _calc_interior_frame(frame, view), view)
+
+class CellRendererBase(object):
+ DRAW_BACKGROUND = True
+
+ def set_index(self, index):
+ self.index = index
+
+ def get_index(self):
+ return self.index
+
+class CellRenderer(CellRendererBase):
+ def __init__(self):
+ self.cell = self.build_cell()
+ self._font_scale_factor = 1.0
+ self._font_bold = False
+ self.set_align('left')
+
+ def build_cell(self):
+ return MiroTableCell.alloc().init()
+
+ def setDataCell_(self, column):
+ column.setDataCell_(self.cell)
+
+ def set_text_size(self, size):
+ if size == widgetconst.SIZE_NORMAL:
+ self._font_scale_factor = 1.0
+ elif size == widgetconst.SIZE_SMALL:
+ # make the scale factor such so that the font size is 11.0
+ self._font_scale_factor = 11.0 / NSFont.systemFontSize()
+ else:
+ raise ValueError("Unknown size: %s" % size)
+ self._set_font()
+
+ def set_font_scale(self, scale_factor):
+ self._font_scale_factor = scale_factor
+ self._set_font()
+
+ def set_bold(self, bold):
+ self._font_bold = bold
+ self._set_font()
+
+ def _set_font(self):
+ size = NSFont.systemFontSize() * self._font_scale_factor
+ if self._font_bold:
+ font = NSFont.boldSystemFontOfSize_(size)
+ else:
+ font = NSFont.systemFontOfSize_(size)
+ self.cell.setFont_(font)
+
+ def set_color(self, color):
+ color = NSColor.colorWithDeviceRed_green_blue_alpha_(color[0],
+ color[1], color[2], 1.0)
+ self.cell._textColor = color
+ self.cell.setTextColor_(color)
+
+ def set_align(self, align):
+ if align == 'left':
+ ns_alignment = NSLeftTextAlignment
+ elif align == 'center':
+ ns_alignment = NSCenterTextAlignment
+ elif align == 'right':
+ ns_alignment = NSRightTextAlignment
+ else:
+ raise ValueError("unknown alignment: %s", align)
+ self.cell.setAlignment_(ns_alignment)
+
+class ImageCellRenderer(CellRendererBase):
+ def setDataCell_(self, column):
+ column.setDataCell_(MiroTableImageCell.alloc().init())
+
+class CheckboxCellRenderer(CellRendererBase, signals.SignalEmitter):
+ def __init__(self):
+ signals.SignalEmitter.__init__(self, 'clicked')
+ self.size = widgetconst.SIZE_NORMAL
+
+ def set_control_size(self, size):
+ self.size = size
+
+ def setDataCell_(self, column):
+ cell = MiroCheckboxCell.alloc().init()
+ if self.size == widgetconst.SIZE_SMALL:
+ cell.setControlSize_(NSSmallControlSize)
+ column.setDataCell_(cell)
+
+class CustomTableCell(NSCell):
+ def init(self):
+ self = super(CustomTableCell, self).init()
+ self.layout_manager = LayoutManager()
+ self.hotspot = None
+ self.default_drawing_style = DrawingStyle()
+ return self
+
+ def highlightColorWithFrame_inView_(self, frame, view):
+ return nil
+
+ def calcHeight_(self, view):
+ self.layout_manager.reset()
+ self.set_wrapper_data()
+ cell_size = self.wrapper.get_size(self.default_drawing_style,
+ self.layout_manager)
+ return cell_size[1]
+
+ def make_drawing_style(self, frame, view):
+ text_color = None
+ if (self.isHighlighted() and frame is not None and
+ (view.isDescendantOf_(view.window().firstResponder()) or
+ view.gradientHighlight) and view.window().isMainWindow()):
+ text_color = NSColor.whiteColor()
+ return DrawingStyle(text_color=text_color)
+
+ def drawInteriorWithFrame_inView_(self, frame, view):
+ NSGraphicsContext.currentContext().saveGraphicsState()
+ if not self.wrapper.IGNORE_PADDING:
+ # adjust frame based on the cell spacing. We also have to adjust
+ # the hover position to account for the new frame
+ original_frame = frame
+ frame = _calc_interior_frame(frame, view)
+ hover_adjustment = (frame.origin.x - original_frame.origin.x,
+ frame.origin.y - original_frame.origin.y)
+ else:
+ hover_adjustment = (0, 0)
+ if self.wrapper.outline_column:
+ pad_left = EXPANDER_PADDING
+ else:
+ pad_left = 0
+ drawing_rect = NSMakeRect(frame.origin.x + pad_left, frame.origin.y,
+ frame.size.width - pad_left, frame.size.height)
+ context = DrawingContext(view, drawing_rect, drawing_rect)
+ context.style = self.make_drawing_style(frame, view)
+ self.layout_manager.reset()
+ self.set_wrapper_data()
+ column = self.wrapper.get_index()
+ hover_pos = view.get_hover(self.row, column)
+ if hover_pos is not None:
+ hover_pos = [hover_pos[0] - hover_adjustment[0],
+ hover_pos[1] - hover_adjustment[1]]
+ self.wrapper.render(context, self.layout_manager, self.isHighlighted(),
+ self.hotspot, hover_pos)
+ NSGraphicsContext.currentContext().restoreGraphicsState()
+
+ def setObjectValue_(self, value):
+ self.object_value = value
+
+ def set_wrapper_data(self):
+ self.wrapper.__dict__.update(self.object_value)
+
+class CustomCellRenderer(CellRendererBase):
+ CellClass = CustomTableCell
+
+ IGNORE_PADDING = False
+
+ def __init__(self):
+ self.outline_column = False
+ self.index = None
+
+ def setDataCell_(self, column):
+ # Note that the ownership is the opposite of what happens in widgets.
+ # The NSObject owns it's wrapper widget. This happens for a couple
+ # reasons:
+ # 1) The data cell gets copied a bunch of times, so wrappermap won't
+ # work with it.
+ # 2) The Wrapper should only needs to stay around as long as the
+ # NSCell that it's wrapping is around. Once the column gets removed
+ # from the table, the wrapper can be deleted.
+ nscell = self.CellClass.alloc().init()
+ nscell.wrapper = self
+ column.setDataCell_(nscell)
+
+ def hotspot_test(self, style, layout, x, y, width, height):
+ return None
+
+class InfoListTableCell(CustomTableCell):
+ def set_wrapper_data(self):
+ self.wrapper.info, self.wrapper.attrs, self.wrapper.group_info = \
+ self.object_value
+
+class InfoListRenderer(CustomCellRenderer):
+ CellClass = InfoListTableCell
+
+ def hotspot_test(self, style, layout, x, y, width, height):
+ return None
+
+class InfoListRendererText(CellRenderer):
+ def build_cell(self):
+ cell = MiroTableInfoListTextCell.alloc()
+ return cell.initWithAttrGetter_(self.get_value)
+
+def calc_row_height(view, model_row):
+ max_height = 0
+ model = view.dataSource().model
+ for column in view.tableColumns():
+ cell = column.dataCell()
+ data = model.get_column_data(model_row, column)
+ cell.setObjectValue_(data)
+ cell_height = cell.calcHeight_(view)
+ max_height = max(max_height, cell_height)
+ if max_height == 0:
+ max_height = 12
+ return max_height + view.row_spacing
+
+class TableViewDelegate(NSObject):
+ def tableView_willDisplayCell_forTableColumn_row_(self, view, cell,
+ column, row):
+ column = view.column_index_map[column]
+ cell.column = column
+ cell.row = row
+ if view.hotspot_tracker:
+ cell.hotspot = view.hotspot_tracker.calc_cell_hotspot(column, row)
+ else:
+ cell.hotspot = None
+
+ def tableView_didClickTableColumn_(self, tableview, column):
+ wrapper = wrappermap.wrapper(tableview)
+ for column_wrapper in wrapper.columns:
+ if column_wrapper._column is column:
+ column_wrapper.emit('clicked')
+
+ def tableView_toolTipForCell_rect_tableColumn_row_mouseLocation_(self, tableview, cell, rect, column, row, location):
+ wrapper = wrappermap.wrapper(tableview)
+ iter = tableview.dataSource().model.iter_for_row(tableview, row)
+ for wrapper_column in wrapper.columns:
+ if wrapper_column._column is column:
+ break
+ return (wrapper.get_tooltip(iter, wrapper_column), rect)
+
+class VariableHeightTableViewDelegate(TableViewDelegate):
+ def tableView_heightOfRow_(self, table_view, row):
+ model = table_view.dataSource().model
+ iter = model.iter_for_row(table_view, row)
+ if iter is None:
+ return 12
+ return calc_row_height(table_view, model[iter])
+
+
+# TableViewCommon is a hack to do a Mixin class. We want the same behaviour
+# for our table views and our outline views. Normally we would use a Mixin,
+# but that doesn't work with pyobjc. Instead we define the common code in
+# TableViewCommon, then copy it into MiroTableView and MiroOutlineView
+
+class TableViewCommon(object):
+ def init(self):
+ self = super(self.__class__, self).init()
+ self.hotspot_tracker = None
+ self._tracking_rects = []
+ self.hover_info = None
+ self.column_index_map = {}
+ self.setFocusRingType_(NSFocusRingTypeNone)
+ self.handled_last_mouse_down = False
+ self.gradientHighlight = False
+ self.tracking_area = None
+ self.group_lines_enabled = False
+ self.group_line_width = 1
+ self.group_line_color = (0, 0, 0, 1.0)
+ # we handle cell spacing manually
+ self.setIntercellSpacing_(NSSize(0, 0))
+ self.column_spacing = 3
+ self.row_spacing = 2
+ return self
+
+ def updateTrackingAreas(self):
+ # remove existing tracking area if needed
+ if self.tracking_area:
+ self.removeTrackingArea_(self.tracking_area)
+
+ # create a new tracking area for the entire view. This allows us to
+ # get mouseMoved events whenever the mouse is inside our view.
+ self.tracking_area = NSTrackingArea.alloc()
+ self.tracking_area.initWithRect_options_owner_userInfo_(
+ self.visibleRect(),
+ NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved |
+ NSTrackingActiveInKeyWindow,
+ self,
+ nil)
+ self.addTrackingArea_(self.tracking_area)
+
+ def addTableColumn_(self, column):
+ index = len(self.tableColumns())
+ column.set_index(index)
+ self.column_index_map[column._column] = index
+ self.SuperClass.addTableColumn_(self, column._column)
+
+ def removeTableColumn_(self, column):
+ self.SuperClass.removeTableColumn_(self, column)
+ removed = self.column_index_map.pop(column)
+ for key, index in self.column_index_map.items():
+ if index > removed:
+ self.column_index_map[key] -= 1
+
+ def moveColumn_toColumn_(self, src, dest):
+ # Need to switch the TableColumn objects too
+ columns = wrappermap.wrapper(self).columns
+ columns[src], columns[dest] = columns[dest], columns[src]
+ for index, column in enumerate(columns):
+ column.set_index(index)
+ self.SuperClass.moveColumn_toColumn_(self, src, dest)
+
+ def highlightSelectionInClipRect_(self, rect):
+ if wrappermap.wrapper(self).draws_selection:
+ if not self.gradientHighlight:
+ return self.SuperClass.highlightSelectionInClipRect_(self,
+ rect)
+ context = NSGraphicsContext.currentContext()
+ focused = self.isDescendantOf_(self.window().firstResponder())
+ for row in tablemodel.list_from_nsindexset(self.selectedRowIndexes()):
+ self.drawBackgroundGradient(context, focused, row)
+
+ def setFrameSize_(self, size):
+ if size.height == 0:
+ size.height = 4
+ self.SuperClass.setFrameSize_(self, size)
+
+ def drawBackgroundGradient(self, context, focused, row):
+ widget = wrappermap.wrapper(self)
+ window = widget.get_window()
+ if window and window.is_active():
+ if focused:
+ start_color = (0.588, 0.717, 0.843)
+ end_color = (0.416, 0.568, 0.713)
+ top_line_color = (0.416, 0.569, 0.714, 1.0)
+ bottom_line_color = (0.416, 0.569, 0.714, 1.0)
+ else:
+ start_color = (168 / 255.0, 188 / 255.0, 208 / 255.0)
+ end_color = (129 / 255.0, 152 / 255.0, 176 / 255.0)
+ top_line_color = (129 / 255.0, 152 / 255.0, 175 / 255.0, 1.0)
+ bottom_line_color = (0.416, 0.569, 0.714, 1.0)
+ else:
+ start_color = (0.675, 0.722, 0.765)
+ end_color = (0.592, 0.659, 0.710)
+ top_line_color = (0.596, 0.635, 0.671, 1.0)
+ bottom_line_color = (0.522, 0.576, 0.620, 1.0)
+
+ rect = self.rectOfRow_(row)
+ top = NSMakeRect(rect.origin.x, rect.origin.y, rect.size.width, 1)
+ context.saveGraphicsState()
+ # draw the top line
+ NSColor.colorWithDeviceRed_green_blue_alpha_(*top_line_color).set()
+ NSRectFill(top)
+ bottom = NSMakeRect(rect.origin.x, rect.origin.y + rect.size.height - 2,
+ rect.size.width, 1)
+ NSColor.colorWithDeviceRed_green_blue_alpha_(*bottom_line_color).set()
+ NSRectFill(bottom)
+ highlight = NSMakeRect(rect.origin.x, rect.origin.y + rect.size.height - 1,
+ rect.size.width, 1)
+ NSColor.colorWithDeviceRed_green_blue_alpha_(0.918, 0.925, 0.941, 1.0).set()
+ NSRectFill(highlight)
+
+ # draw the gradient
+ rect.origin.y += 1
+ rect.size.height -= 3
+ NSRectClip(rect)
+ gradient = Gradient(rect.origin.x, rect.origin.y,
+ rect.origin.x, rect.origin.y + rect.size.height)
+ gradient.set_start_color(start_color)
+ gradient.set_end_color(end_color)
+ gradient.draw()
+ context.restoreGraphicsState()
+
+ def drawBackgroundInClipRect_(self, clip_rect):
+ # save our graphics state, since we are about to modify the clip path
+ graphics_context = NSGraphicsContext.currentContext()
+ graphics_context.saveGraphicsState()
+ # create a NSBezierPath that contains the rects of the columns with
+ # DRAW_BACKGROUND True.
+ clip_path = NSBezierPath.bezierPath()
+ number_of_columns = len(self.tableColumns())
+ for col_index in iter_range(self.columnsInRect_(clip_rect)):
+ column = wrappermap.wrapper(self.tableColumns()[col_index])
+ column_rect = None
+ if column.renderer.DRAW_BACKGROUND:
+ # We should draw the background for this column, add it's rect
+ # to our clip rect.
+ column_rect = self.rectOfColumn_(col_index)
+ clip_path.appendBezierPathWithRect_(column_rect)
+ else:
+ # We shouldn't draw the background for this column. Don't add
+ # anything to the clip rect, but do draw the area before the
+ # first row and after the last row.
+ self.drawBackgroundOutsideContent_clipRect_(col_index,
+ clip_rect)
+ if col_index == number_of_columns - 1: # last column
+ if not column_rect:
+ column_rect = self.rectOfColumn_(col_index)
+ column_right = column_rect.origin.x + column_rect.size.width
+ clip_right = clip_rect.origin.x + clip_rect.size.width
+ if column_right < clip_right:
+ # there's space to the right, so add that to the clip_rect
+ remaining = clip_right - column_right
+ left_rect = NSMakeRect(column_right, clip_rect.origin.y,
+ remaining, clip_rect.size.height)
+ clip_path.appendBezierPathWithRect_(left_rect)
+ # clip to that path
+ clip_path.addClip()
+ # do the default drawing
+ self.SuperClass.drawBackgroundInClipRect_(self, clip_rect)
+ # restore graphics state
+ graphics_context.restoreGraphicsState()
+
+ def drawBackgroundOutsideContent_clipRect_(self, index, clip_rect):
+ """Draw our background outside the rows with content
+
+ We call this for cells with DRAW_BACKGROUND set to False. For those,
+ we let the cell draw their own background, but we still need to draw
+ the background before the first cell and after the last cell.
+ """
+
+ self.backgroundColor().set()
+
+ total_rect = NSIntersectionRect(self.rectOfColumn_(index), clip_rect)
+
+ if self.numberOfRows() == 0:
+ # if no rows are selected, draw the background over everything
+ NSRectFill(total_rect)
+ return
+
+ # fill the area above the first row
+ first_row_rect = self.rectOfRow_(0)
+ if first_row_rect.origin.y > total_rect.origin.y:
+ height = first_row_rect.origin.y - total_rect.origin.y
+ NSRectFill(NSMakeRect(total_rect.origin.x, total_rect.origin.y,
+ total_rect.size.width, height))
+
+ # fill the area below the last row
+ last_row_rect = self.rectOfRow_(self.numberOfRows()-1)
+ if NSMaxY(last_row_rect) < NSMaxY(total_rect):
+ y = NSMaxY(last_row_rect) + 1
+ height = NSMaxY(total_rect) - NSMaxY(last_row_rect)
+ NSRectFill(NSMakeRect(total_rect.origin.x, y,
+ total_rect.size.width, height))
+
+ def drawRow_clipRect_(self, row, clip_rect):
+ self.SuperClass.drawRow_clipRect_(self, row, clip_rect)
+ if self.group_lines_enabled:
+ self.drawGroupLine_(row)
+
+ def drawGroupLine_(self, row):
+ infolist = wrappermap.wrapper(self).model
+ if (not isinstance(infolist, tablemodel.InfoListModel) or
+ infolist.get_grouping() is None):
+ return
+
+ info, attrs, group_info = infolist[row]
+ if group_info[0] == group_info[1] - 1:
+ rect = self.rectOfRow_(row)
+ rect.origin.y = NSMaxY(rect) - self.group_line_width
+ rect.size.height = self.group_line_width
+ NSColor.colorWithDeviceRed_green_blue_alpha_(
+ *self.group_line_color).set()
+ NSRectFill(rect)
+
+ def canDragRowsWithIndexes_atPoint_(self, indexes, point):
+ return YES
+
+ def draggingSourceOperationMaskForLocal_(self, local):
+ drag_source = wrappermap.wrapper(self).drag_source
+ if drag_source and local:
+ return drag_source.allowed_actions()
+ return NSDragOperationNone
+
+ def mouseMoved_(self, event):
+ location = self.convertPoint_fromView_(event.locationInWindow(), nil)
+ row = self.rowAtPoint_(location)
+ column = self.columnAtPoint_(location)
+ if (self.hover_info is not None and self.hover_info != (row, column)):
+ # left a cell, redraw it the old one
+ rect = self.frameOfCellAtColumn_row_(self.hover_info[1],
+ self.hover_info[0])
+ self.setNeedsDisplayInRect_(rect)
+ if row == -1 or column == -1:
+ # corner case: we got a mouseMoved_ event, but the pointer is
+ # outside the view
+ self.hover_pos = self.hover_info = None
+ return
+ # queue a redraw on the cell currently hovered over
+ rect = self.frameOfCellAtColumn_row_(column, row)
+ self.setNeedsDisplayInRect_(rect)
+ # recalculate hover_pos and hover_info
+ self.hover_pos = (location[0] - rect[0][0],
+ location[0] - rect[0][1])
+ self.hover_info = (row, column)
+
+ def mouseExited_(self, event):
+ if self.hover_info:
+ # mouse left our window, unset hover and redraw the cell that the
+ # mouse was in
+ rect = self.frameOfCellAtColumn_row_(self.hover_info[1],
+ self.hover_info[0])
+ self.setNeedsDisplayInRect_(rect)
+ self.hover_pos = self.hover_info = None
+
+ def get_hover(self, row, column):
+ if self.hover_info == (row, column):
+ return self.hover_pos
+ else:
+ return None
+
+ def mouseDown_(self, event):
+ if event.modifierFlags() & NSControlKeyMask:
+ self.handleContextMenu_(event)
+ self.handled_last_mouse_down = True
+ return
+
+ point = self.convertPoint_fromView_(event.locationInWindow(), nil)
+
+ if event.clickCount() == 2:
+ if self.handled_last_mouse_down:
+ return
+ wrapper = wrappermap.wrapper(self)
+ row = self.rowAtPoint_(point)
+ if (row != -1 and self.point_should_click(point, row)):
+ iter = wrapper.model.iter_for_row(self, row)
+ wrapper.emit('row-activated', iter)
+ return
+
+ # Like clickCount() == 2 but keep running so we can get to run the
+ # hotspot tracker et al.
+ if event.clickCount() == 1:
+ wrapper = wrappermap.wrapper(self)
+ row = self.rowAtPoint_(point)
+ if (row != -1 and self.point_should_click(point, row)):
+
+ iter = wrapper.model.iter_for_row(self, row)
+ wrapper.emit('row-clicked', iter)
+
+ hotspot_tracker = HotspotTracker(self, point)
+ if hotspot_tracker.hit:
+ self.hotspot_tracker = hotspot_tracker
+ self.hotspot_tracker.redraw_cell()
+ self.handled_last_mouse_down = True
+ if hotspot_tracker.is_for_context_menu():
+ self.popup_context_menu(self.hotspot_tracker.row, event)
+ # once we're out of that call, we know the context menu is
+ # gone
+ hotspot_tracker.redraw_cell()
+ self.hotspot_tracker = None
+ else:
+ self.handled_last_mouse_down = False
+ self.SuperClass.mouseDown_(self, event)
+
+ def point_should_click(self, point, row):
+ """Should a click on a point result in a row-clicked signal?
+
+ Subclasses can override if not every point should result in a click.
+ """
+ return True
+
+ def rightMouseDown_(self, event):
+ self.handleContextMenu_(event)
+
+ def handleContextMenu_(self, event):
+ self.window().makeFirstResponder_(self)
+ point = self.convertPoint_fromView_(event.locationInWindow(), nil)
+ column = self.columnAtPoint_(point)
+ row = self.rowAtPoint_(point)
+ if self.group_lines_enabled and column == 0:
+ self.selectAllItemsInGroupForRow_(row)
+ self.popup_context_menu(row, event)
+
+ def selectAllItemsInGroupForRow_(self, row):
+ wrapper = wrappermap.wrapper(self)
+ infolist = wrapper.model
+ if (not isinstance(infolist, tablemodel.InfoListModel) or
+ infolist.get_grouping() is None):
+ return
+
+ info, attrs, group_info = infolist[row]
+ select_range = NSMakeRange(row - group_info[0], group_info[1])
+ index_set = NSIndexSet.indexSetWithIndexesInRange_(select_range)
+ self.selectRowIndexes_byExtendingSelection_(index_set, NO)
+
+ def popup_context_menu(self, row, event):
+ selection = self.selectedRowIndexes()
+ if row != -1 and not selection.containsIndex_(row):
+ index_set = NSIndexSet.alloc().initWithIndex_(row)
+ self.selectRowIndexes_byExtendingSelection_(index_set, NO)
+ wrapper = wrappermap.wrapper(self)
+ if wrapper.context_menu_callback is not None:
+ menu_items = wrapper.context_menu_callback(wrapper)
+ menu = osxmenus.make_context_menu(menu_items)
+ NSMenu.popUpContextMenu_withEvent_forView_(menu, event, self)
+
+ def mouseDragged_(self, event):
+ if self.hotspot_tracker is not None:
+ point = self.convertPoint_fromView_(event.locationInWindow(), nil)
+ self.hotspot_tracker.update_position(point)
+ self.hotspot_tracker.update_hit()
+ else:
+ self.SuperClass.mouseDragged_(self, event)
+
+ def mouseUp_(self, event):
+ if self.hotspot_tracker is not None:
+ point = self.convertPoint_fromView_(event.locationInWindow(), nil)
+ self.hotspot_tracker.update_position(point)
+ self.hotspot_tracker.update_hit()
+ if self.hotspot_tracker.hit:
+ wrappermap.wrapper(self).send_hotspot_clicked()
+ if self.hotspot_tracker:
+ self.hotspot_tracker.redraw_cell()
+ self.hotspot_tracker = None
+ else:
+ self.SuperClass.mouseUp_(self, event)
+
+ def keyDown_(self, event):
+ mods = osxmenus.translate_event_modifiers(event)
+ if event.charactersIgnoringModifiers() == ' ' and len(mods) == 0:
+ # handle spacebar with no modifiers by sending the row-activated
+ # signal
+ wrapper = wrappermap.wrapper(self)
+ row = self.selectedRow()
+ if row >= 0:
+ iter = wrapper.model.iter_for_row(self, row)
+ wrapper.emit('row-activated', iter)
+ else:
+ self.SuperClass.keyDown_(self, event)
+
+class TableColumn(signals.SignalEmitter):
+ def __init__(self, title, renderer, header=None, **attrs):
+ signals.SignalEmitter.__init__(self)
+ self.create_signal('clicked')
+ self._column = MiroTableColumn.alloc().initWithIdentifier_(title)
+ self._column.set_attrs(attrs)
+ wrappermap.add(self._column, self)
+ header_cell = MiroTableHeaderCell.alloc().init()
+ self.custom_header = False
+ if header:
+ header_cell.set_widget(header)
+ self.custom_header = True
+ self._column.setHeaderCell_(header_cell)
+ self._column.headerCell().setStringValue_(title)
+ self._column.setEditable_(NO)
+ self._column.setResizingMask_(NSTableColumnNoResizing)
+ self.renderer = renderer
+ self.sort_order_ascending = True
+ self.sort_indicator_visible = False
+ self.do_horizontal_padding = True
+ self.min_width = self.max_width = None
+ renderer.setDataCell_(self._column)
+
+ def set_do_horizontal_padding(self, horizontal_padding):
+ self.do_horizontal_padding = horizontal_padding
+
+ def set_right_aligned(self, right_aligned):
+ if right_aligned:
+ self._column.headerCell().setAlignment_(NSRightTextAlignment)
+ else:
+ self._column.headerCell().setAlignment_(NSLeftTextAlignment)
+
+ def set_min_width(self, width):
+ self.min_width = width
+
+ def set_max_width(self, width):
+ self.max_width = width
+
+ def set_width(self, width):
+ self._column.setWidth_(width)
+
+ def get_width(self):
+ return self._column.width()
+
+ def set_resizable(self, resizable):
+ mask = 0
+ if resizable:
+ mask |= NSTableColumnUserResizingMask
+ self._column.setResizingMask_(mask)
+
+ def set_sort_indicator_visible(self, visible):
+ self.sort_indicator_visible = visible
+ self._column.tableView().headerView().setNeedsDisplay_(True)
+
+ def get_sort_indicator_visible(self):
+ return self.sort_indicator_visible
+
+ def set_sort_order(self, ascending):
+ self.sort_order_ascending = ascending
+ self._column.tableView().headerView().setNeedsDisplay_(True)
+
+ def get_sort_order_ascending(self):
+ return self.sort_order_ascending
+
+ def set_index(self, index):
+ self.index = index
+ self.renderer.set_index(index)
+
+class MiroTableColumn(NSTableColumn):
+ def set_attrs(self, attrs):
+ self._attrs = attrs
+
+ def attrs(self):
+ return self._attrs
+
+class MiroTableView(NSTableView):
+ SuperClass = NSTableView
+ for name, value in TableViewCommon.__dict__.items():
+ locals()[name] = value
+
+class MiroTableHeaderView(NSTableHeaderView):
+ def initWithFrame_(self, frame):
+ # frame is not used
+ self = super(MiroTableHeaderView, self).initWithFrame_(frame)
+ self.selected = None
+ self.custom_header = False
+ return self
+
+ def drawRect_(self, rect):
+ wrapper = wrappermap.wrapper(self.tableView())
+ if self.selected:
+ self.selected.set_selected(False)
+ for column in wrapper.columns:
+ if column.sort_indicator_visible:
+ self.selected = column._column.headerCell()
+ self.selected.set_selected(True)
+ self.selected.set_ascending(column.sort_order_ascending)
+ break
+ NSTableHeaderView.drawRect_(self, rect)
+ if self.custom_header:
+ NSGraphicsContext.currentContext().saveGraphicsState()
+ # Draw the separator between the header and the contents.
+ context = DrawingContext(self, rect, rect)
+ context.set_line_width(1)
+ context.set_color((2 / 255.0, 2 / 255.0, 2 / 255.0))
+ context.move_to(0, context.height - 0.5)
+ context.rel_line_to(context.width, 0)
+ context.stroke()
+ NSGraphicsContext.currentContext().restoreGraphicsState()
+
+class MiroTableHeaderCell(NSTableHeaderCell):
+ def init(self):
+ self = super(MiroTableHeaderCell, self).init()
+ self.layout_manager = LayoutManager()
+ self.button = None
+ return self
+
+ def set_selected(self, selected):
+ self.button._enabled = selected
+
+ def set_ascending(self, ascending):
+ self.button._ascending = ascending
+
+ def set_widget(self, widget):
+ self.button = widget
+
+ def drawWithFrame_inView_(self, frame, view):
+ if self.button is None:
+ # use the default behavior when set_widget hasn't been called
+ return NSTableHeaderCell.drawWithFrame_inView_(self, frame, view)
+
+ NSGraphicsContext.currentContext().saveGraphicsState()
+ drawing_rect = NSMakeRect(frame.origin.x, frame.origin.y,
+ frame.size.width, frame.size.height)
+ context = DrawingContext(view, drawing_rect, drawing_rect)
+ context.style = self.make_drawing_style(frame, view)
+ self.layout_manager.reset()
+ columns = wrappermap.wrapper(view.tableView()).columns
+ header_cells = [c._column.headerCell() for c in columns]
+ background_only = not self in header_cells
+ self.button.draw(context, self.layout_manager, background_only)
+ NSGraphicsContext.currentContext().restoreGraphicsState()
+
+ def make_drawing_style(self, frame, view):
+ text_color = None
+ if (self.isHighlighted() and frame is not None and
+ (view.isDescendantOf_(view.window().firstResponder()) or
+ view.gradientHighlight)):
+ text_color = NSColor.whiteColor()
+ return DrawingStyle(text_color=text_color)
+
+class CocoaSelectionOwnerMixin(SelectionOwnerMixin):
+ """Cocoa-specific methods for selection management.
+
+ This subclass should not define any behavior. Methods that cannot be
+ completed in this widget state should raise WidgetActionError.
+ """
+
+ def _set_allow_multiple_select(self, allow):
+ self.tableview.setAllowsMultipleSelection_(allow)
+
+ def _get_allow_multiple_select(self):
+ return self.tableview.allowsMultipleSelection()
+
+ def _get_selected_iters(self):
+ selection = self.tableview.selectedRowIndexes()
+ selrows = tablemodel.list_from_nsindexset(selection)
+ return [self.model.iter_for_row(self.tableview, row) for row in selrows]
+
+ def _get_selected_iter(self):
+ row = self.tableview.selectedRow()
+ if row == -1:
+ return None
+ return self.model.iter_for_row(self.tableview, row)
+
+ def _get_selected_rows(self):
+ return [self.model[i] for i in self._get_selected_iters()]
+
+ @property
+ def num_rows_selected(self):
+ return self.tableview.numberOfSelectedRows()
+
+ def _is_selected(self, iter_):
+ row = self.row_of_iter(iter_)
+ return self.tableview.isRowSelected_(row)
+
+ def _select(self, iter_):
+ row = self.row_of_iter(iter_)
+ index_set = NSIndexSet.alloc().initWithIndex_(row)
+ self.tableview.selectRowIndexes_byExtendingSelection_(index_set, YES)
+
+ def _unselect(self, iter_):
+ self.tableview.deselectRow_(self.row_of_iter(iter_))
+
+ def _unselect_all(self):
+ self.tableview.deselectAll_(nil)
+
+ def _iter_to_string(self, iter_):
+ return unicode(self.model.row_of_iter(self.tableview, iter_))
+
+ def _iter_from_string(self, row):
+ return self.model.iter_for_row(self.tableview, int(row))
+
+class CocoaScrollbarOwnerMixin(ScrollbarOwnerMixin):
+ """Manages a TableView's scroll position."""
+ def __init__(self):
+ ScrollbarOwnerMixin.__init__(self, _work_around_17153=True)
+ self.connect('place-in-scroller', self.on_place_in_scroller)
+ self.scroll_position = (0, 0)
+ self.clipview_notifications = None
+ self._position_set = False
+
+ def _set_scroll_position(self, scroll_to):
+ """Restore a saved scroll position."""
+ self.scroll_position = scroll_to
+ try:
+ scroller = self._scroller
+ except errors.WidgetNotReadyError:
+ return
+ self._position_set = True
+ clipview = scroller.contentView()
+ if not self.clipview_notifications:
+ self.clipview_notifications = NotificationForwarder.create(clipview)
+ # NOTE: intentional changes are BoundsChanged; bad changes are
+ # FrameChanged
+ clipview.setPostsFrameChangedNotifications_(YES)
+ self.clipview_notifications.connect(self.on_scroll_changed,
+ 'NSViewFrameDidChangeNotification')
+ # NOTE: scrollPoint_ just scrolls the point into view; we want to
+ # scroll the view so that the point becomes the origin
+ size = self.tableview.visibleRect().size
+ size = (size.width, size.height)
+ rect = NSMakeRect(scroll_to[0], scroll_to[1], size[0], size[1])
+ self.tableview.scrollRectToVisible_(rect)
+
+ def on_place_in_scroller(self, scroller):
+ # workaround for 17153.1
+ if not self._position_set:
+ self._set_scroll_position(self.scroll_position)
+
+ @property
+ def _manually_scrolled(self):
+ """Return whether the view has been scrolled explicitly by the user
+ since the last time it was set automatically. Ignores X coords.
+ """
+ auto_y = self.scroll_position[1]
+ real_y = self.get_scroll_position()[1]
+ return abs(auto_y - real_y) > 5
+
+ def _get_item_area(self, iter_):
+ rect = self.tableview.rectOfRow_(self.row_of_iter(iter_))
+ return NSRectToRect(rect)
+
+ def _get_visible_area(self):
+ return NSRectToRect(self._scroller.contentView().documentVisibleRect())
+
+ def _get_scroll_position(self):
+ point = self._scroller.contentView().documentVisibleRect().origin
+ return NSPointToPoint(point)
+
+ def on_scroll_changed(self, notification):
+ # we get this notification when the scroll position has been reset (when
+ # it should not have been); put it back
+ self.set_scroll_position(self.scroll_position)
+ # this notification also serves as the Cocoa equivalent to
+ # on_scroll_range_changed, which tells super when we may be ready to
+ # scroll to an iter
+ self.emit('scroll-range-changed')
+
+ def set_scroller(self, scroller):
+ """For GTK; Cocoa tableview knows its enclosingScrollView"""
+
+ @property
+ def _scroller(self):
+ """Return an NSScrollView or raise WidgetNotReadyError"""
+ scroller = self.tableview.enclosingScrollView()
+ if not scroller:
+ raise errors.WidgetNotReadyError('enclosingScrollView')
+ return scroller
+
+class SorterPadding(NSView):
+ # Why is this a Mac only widget? Because the wrappermap mechanism requires
+ # us to layout the widgets (so that we may call back to the portable API
+ # hooks of the widget. Since we only set the view component, this fake
+ # widget is never placed so the wrappermap mechanism fails to work.
+ #
+ # So far, this is okay because only the Mac uses custom headers.
+ def init(self):
+ self = super(SorterPadding, self).init()
+ image = Image(resources.path('images/headertoolbar.png'))
+ self.image = ImageSurface(image)
+ return self
+
+ def isFlipped(self):
+ return YES
+
+ def drawRect_(self, rect):
+ context = DrawingContext(self, self.bounds(), rect)
+ context.style = DrawingStyle()
+ self.image.draw(context, 0, 0, context.width, context.height)
+ # XXX this color doesn't take into account enable/disabled state
+ # of the sorting widgets.
+ edge = 72.0 / 255
+ context.set_color((edge, edge, edge))
+ context.set_line_width(1)
+ context.move_to(0.5, 0)
+ context.rel_line_to(0, context.height)
+ context.stroke()
+
+class TableView(CocoaSelectionOwnerMixin, CocoaScrollbarOwnerMixin, Widget):
+ """Displays data as a tabular list. TableView follows the GTK TreeView
+ widget fairly closely.
+ """
+
+ CREATES_VIEW = False
+ # Bit of a hack. We create several views. By setting CREATES_VIEW to
+ # False, we get to position the views manually.
+
+ draws_selection = True
+
+ def __init__(self, model, custom_headers=False):
+ Widget.__init__(self)
+ CocoaSelectionOwnerMixin.__init__(self)
+ CocoaScrollbarOwnerMixin.__init__(self)
+ self.create_signal('hotspot-clicked')
+ self.create_signal('row-clicked')
+ self.create_signal('row-activated')
+ self.create_signal('reallocate-columns')
+ self.model = model
+ self.columns = []
+ self.drag_source = self.drag_dest = None
+ self.context_menu_callback = None
+ self.tableview = MiroTableView.alloc().init()
+ self.data_source = tablemodel.MiroTableViewDataSource.alloc()
+ types = (tablemodel.MIRO_DND_ITEM_LOCAL,)
+ self.tableview.registerForDraggedTypes_(types)
+ self.view = self.tableview
+ self.data_source.initWithModel_(self.model)
+ self.tableview.setDataSource_(self.data_source)
+ self.tableview.setVerticalMotionCanBeginDrag_(YES)
+ self.set_columns_draggable(False)
+ self.set_auto_resizes(False)
+ self.row_height_set = False
+ self.set_fixed_height(False)
+ self.auto_resizing = False
+ self.header_view = MiroTableHeaderView.alloc().initWithFrame_(
+ NSMakeRect(0, 0, 0, 0))
+ self.tableview.setCornerView_(None)
+ self.custom_header = False
+ self.header_height = HEADER_HEIGHT
+ self.set_show_headers(True)
+ self.notifications = NotificationForwarder.create(self.tableview)
+ self.model_signal_ids = [
+ self.model.connect_weak('row-changed', self.on_row_change),
+ self.model.connect_weak('structure-will-change',
+ self.on_model_structure_change),
+ ]
+ self.iters_to_update = []
+ self.height_changed = self.reload_needed = False
+ self.old_selection = None
+ self._resizing = False
+ if custom_headers:
+ self._enable_custom_headers()
+
+ def unset_model(self):
+ for signal_id in self.model_signal_ids:
+ self.model.disconnect(signal_id)
+ self.model = None
+ self.tableview.setDataSource_(None)
+ self.data_source = None
+
+ def _enable_custom_headers(self):
+ self.custom_header = True
+ self.header_height = CUSTOM_HEADER_HEIGHT
+ self.header_view.custom_header = True
+ self.tableview.setCornerView_(SorterPadding.alloc().init())
+
+ def enable_album_view_focus_hack(self):
+ # this only matters on GTK
+ pass
+
+ def focus(self):
+ if self.tableview.window() is not None:
+ self.tableview.window().makeFirstResponder_(self.tableview)
+
+ def send_hotspot_clicked(self):
+ tracker = self.tableview.hotspot_tracker
+ self.emit('hotspot-clicked', tracker.name, tracker.iter)
+
+ def on_row_change(self, model, iter):
+ self.iters_to_update.append(iter)
+ if not self.fixed_height:
+ self.height_changed = True
+ if self.tableview.hotspot_tracker is not None:
+ self.tableview.hotspot_tracker.update_hit()
+
+ def on_model_structure_change(self, model):
+ self.will_need_reload()
+ self.cancel_hotspot_track()
+
+ def will_need_reload(self):
+ if not self.reload_needed:
+ self.reload_needed = True
+ self.old_selection = self._get_selected_rows()
+
+ def cancel_hotspot_track(self):
+ if self.tableview.hotspot_tracker is not None:
+ self.tableview.hotspot_tracker.redraw_cell()
+ self.tableview.hotspot_tracker = None
+
+ def on_expanded(self, notification):
+ self.invalidate_size_request()
+ item = notification.userInfo()['NSObject']
+ iter_ = self.model.iter_for_item[item]
+ self.emit('row-expanded', iter_, self.model.get_path(iter_))
+
+ def on_collapsed(self, notification):
+ self.invalidate_size_request()
+ item = notification.userInfo()['NSObject']
+ iter_ = self.model.iter_for_item[item]
+ self.emit('row-collapsed', iter_, self.model.get_path(iter_))
+
+ def on_column_resize(self, notification):
+ if self.auto_resizing or self._resizing:
+ return
+ self._resizing = True
+ try:
+ column = notification.userInfo()['NSTableColumn']
+ label = column.headerCell().stringValue()
+ self.emit('reallocate-columns', {label: column.width()})
+ finally:
+ self._resizing = False
+
+ def is_tree(self):
+ return isinstance(self.model, tablemodel.TreeTableModel)
+
+ def set_row_expanded(self, iter, expanded):
+ """Expand or collapse the specified row. Succeeds or raises
+ WidgetActionError.
+ """
+ item = iter.value()
+ if expanded:
+ self.tableview.expandItem_(item)
+ else:
+ self.tableview.collapseItem_(item)
+ if self.tableview.isItemExpanded_(item) != expanded:
+ raise errors.WidgetActionError(
+ "cannot expand iter. expandable: %r" % (
+ self.tableview.isExpandable_(item),))
+ self.invalidate_size_request()
+
+ def is_row_expanded(self, iter):
+ return self.tableview.isItemExpanded_(iter.value())
+
+ def calc_size_request(self):
+ self.tableview.tile()
+ height = self.tableview.frame().size.height
+ if self._show_headers:
+ height += self.header_height
+ return self.calc_width(), height
+
+ def viewport_repositioned(self):
+ self._do_layout()
+
+ def viewport_created(self):
+ wrappermap.add(self.tableview, self)
+ self._do_layout()
+ self._add_views()
+ self.notifications.connect(self.on_selection_changed,
+ 'NSTableViewSelectionDidChangeNotification')
+ self.notifications.connect(self.on_column_resize,
+ 'NSTableViewColumnDidResizeNotification')
+ # scroll has been unset
+ self._position_set = False
+
+ def remove_viewport(self):
+ if self.viewport is not None:
+ self._remove_views()
+ wrappermap.remove(self.tableview)
+ self.notifications.disconnect()
+ self.viewport = None
+ if self.clipview_notifications:
+ self.clipview_notifications.disconnect()
+ self.clipview_notifications = None
+
+ def _should_place_header_view(self):
+ return self._show_headers and not self.parent_is_scroller
+
+ def _add_views(self):
+ self.viewport.view.addSubview_(self.tableview)
+ if self._should_place_header_view():
+ self.viewport.view.addSubview_(self.header_view)
+
+ def _remove_views(self):
+ self.tableview.removeFromSuperview()
+ self.header_view.removeFromSuperview()
+
+ def _do_layout(self):
+ x = self.viewport.placement.origin.x
+ y = self.viewport.placement.origin.y
+ width = self.viewport.get_width()
+ height = self.viewport.get_height()
+ if self._should_place_header_view():
+ self.header_view.setFrame_(NSMakeRect(x, y,
+ width, self.header_height))
+ self.tableview.setFrame_(NSMakeRect(x, y + self.header_height,
+ width, height - self.header_height))
+ else:
+ self.header_view.setFrame_(NSMakeRect(x, y,
+ width, self.header_height))
+ self.tableview.setFrame_(NSMakeRect(x, y, width, height))
+
+ if self.auto_resize:
+ self.auto_resizing = True
+ # ListView sizes itself in do_size_allocated;
+ # this is necessary for tablist and StandardView
+ columns = self.tableview.tableColumns()
+ if len(columns) == 1:
+ columns[0].setWidth_(self.viewport.area().size.width)
+ self.auto_resizing = False
+ self.queue_redraw()
+
+ def calc_width(self):
+ if self.column_count() == 0:
+ return 0
+ width = 0
+ columns = self.tableview.tableColumns()
+ if self.auto_resize:
+ # Table auto-resizes, we can shrink to min-width for each column
+ width = sum(column.minWidth() for column in columns)
+ width += self.tableview.column_spacing * self.column_count()
+ else:
+ # Table doesn't auto-resize, the columns can't get smaller than
+ # their current width
+ width = sum(column.width() for column in columns)
+ return width
+
+ def start_bulk_change(self):
+ # stop our model from emitting signals, which is slow if we're
+ # adding/removing/changing a bunch of rows. Instead, just reload the
+ # model afterwards.
+ self.will_need_reload()
+ self.cancel_hotspot_track()
+ self.model.freeze_signals()
+
+ def model_changed(self):
+ if not self.row_height_set and self.fixed_height:
+ self.try_to_set_row_height()
+ self.model.thaw_signals()
+ size_changed = False
+ if self.reload_needed:
+ self.tableview.reloadData()
+ new_selection = self._get_selected_rows()
+ if new_selection != self.old_selection:
+ self.on_selection_changed(self.tableview)
+ self.old_selection = None
+ size_changed = True
+ elif self.iters_to_update:
+ if self.fixed_height or not self.height_changed:
+ # our rows don't change height, just update cell areas
+ if self.is_tree():
+ for iter in self.iters_to_update:
+ self.tableview.reloadItem_(iter.value())
+ else:
+ for iter in self.iters_to_update:
+ row = self.row_of_iter(iter)
+ rect = self.tableview.rectOfRow_(row)
+ self.tableview.setNeedsDisplayInRect_(rect)
+ else:
+ # our rows can change height inform Cocoa that their heights
+ # might have changed (this will redraw them)
+ index_set = NSMutableIndexSet.alloc().init()
+ for iter in self.iters_to_update:
+ try:
+ index_set.addIndex_(self.row_of_iter(iter))
+ except LookupError:
+ # This happens when the iter's parent is unexpanded,
+ # just ignore.
+ pass
+ self.tableview.noteHeightOfRowsWithIndexesChanged_(index_set)
+ size_changed = True
+ else:
+ return
+ if size_changed:
+ self.invalidate_size_request()
+ self.height_changed = self.reload_needed = False
+ self.iters_to_update = []
+
+ def width_for_columns(self, width):
+ """If the table is width pixels big, how much width is available for
+ the table's columns.
+ """
+ # XXX this used to do some calculation with the spacing of each column,
+ # but it doesn't appear like we need it to be that complicated anymore
+ # (see #18273)
+ return width - 2
+
+ def set_column_spacing(self, column_spacing):
+ self.tableview.column_spacing = column_spacing
+
+ def set_row_spacing(self, row_spacing):
+ self.tableview.row_spacing = row_spacing
+
+ def set_alternate_row_backgrounds(self, setting):
+ self.tableview.setUsesAlternatingRowBackgroundColors_(setting)
+
+ def set_grid_lines(self, horizontal, vertical):
+ mask = 0
+ if horizontal:
+ mask |= NSTableViewSolidHorizontalGridLineMask
+ if vertical:
+ mask |= NSTableViewSolidVerticalGridLineMask
+ self.tableview.setGridStyleMask_(mask)
+
+ def set_gradient_highlight(self, setting):
+ self.tableview.gradientHighlight = setting
+
+ def set_group_lines_enabled(self, enabled):
+ self.tableview.group_lines_enabled = enabled
+ self.queue_redraw()
+
+ def set_group_line_style(self, color, width):
+ self.tableview.group_line_color = color + (1.0,)
+ self.tableview.group_line_width = width
+ self.queue_redraw()
+
+ def get_tooltip(self, iter, column):
+ return None
+
+ def add_column(self, column):
+ if not self.custom_header == column.custom_header:
+ raise ValueError('Column header does not match type '
+ 'required by TableView')
+ self.columns.append(column)
+ self.tableview.addTableColumn_(column)
+ self._set_min_max_column_widths(column)
+ # Adding a column means that each row could have a different height.
+ # call noteNumberOfRowsChanged() to have OS X recalculate the heights
+ self.tableview.noteNumberOfRowsChanged()
+ self.invalidate_size_request()
+ self.try_to_set_row_height()
+
+ def _set_min_max_column_widths(self, column):
+ if column.do_horizontal_padding:
+ spacing = self.tableview.column_spacing
+ else:
+ spacing = 0
+ if column.min_width > 0:
+ column._column.setMinWidth_(column.min_width + spacing)
+ if column.max_width > 0:
+ column._column.setMaxWidth_(column.max_width + spacing)
+
+ def column_count(self):
+ return len(self.tableview.tableColumns())
+
+ def remove_column(self, index):
+ column = self.columns.pop(index)
+ self.tableview.removeTableColumn_(column._column)
+ self.invalidate_size_request()
+
+ def get_columns(self):
+ titles = []
+ columns = self.tableview.tableColumns()
+ for column in columns:
+ titles.append(column.headerCell().stringValue())
+ return titles
+
+ def set_background_color(self, (red, green, blue)):
+ color = NSColor.colorWithDeviceRed_green_blue_alpha_(red, green, blue,
+ 1.0)
+ self.tableview.setBackgroundColor_(color)
+
+ def set_show_headers(self, show):
+ self._show_headers = show
+ if show:
+ self.tableview.setHeaderView_(self.header_view)
+ else:
+ self.tableview.setHeaderView_(None)
+ if self.viewport is not None:
+ self._remove_views()
+ self._do_layout()
+ self._add_views()
+ self.invalidate_size_request()
+ self.queue_redraw()
+
+ def is_showing_headers(self):
+ return self._show_headers
+
+ def set_search_column(self, model_index):
+ pass
+
+ def try_to_set_row_height(self):
+ if len(self.model) > 0:
+ first_iter = self.model.first_iter()
+ height = calc_row_height(self.tableview, self.model[first_iter])
+ self.tableview.setRowHeight_(height)
+ self.row_height_set = True
+
+ def set_auto_resizes(self, setting):
+ self.auto_resize = setting
+
+ def set_columns_draggable(self, dragable):
+ self.tableview.setAllowsColumnReordering_(dragable)
+
+ def set_fixed_height(self, fixed):
+ if fixed:
+ self.fixed_height = True
+ delegate_class = TableViewDelegate
+ self.row_height_set = False
+ self.try_to_set_row_height()
+ else:
+ self.fixed_height = False
+ delegate_class = VariableHeightTableViewDelegate
+ self.delegate = delegate_class.alloc().init()
+ self.tableview.setDelegate_(self.delegate)
+ self.tableview.reloadData()
+
+ def row_of_iter(self, iter):
+ return self.model.row_of_iter(self.tableview, iter)
+
+ def set_context_menu_callback(self, callback):
+ self.context_menu_callback = callback
+
+ # disable the drag when the cells are constantly updating. Mac OS X
+ # deals badly with this..
+ def set_volatile(self, volatile):
+ if volatile:
+ self.data_source.setDragSource_(None)
+ self.data_source.setDragDest_(None)
+ else:
+ self.data_source.setDragSource_(self.drag_source)
+ self.data_source.setDragDest_(self.drag_dest)
+
+ def set_drag_source(self, drag_source):
+ self.drag_source = drag_source
+ self.data_source.setDragSource_(drag_source)
+
+ def set_drag_dest(self, drag_dest):
+ self.drag_dest = drag_dest
+ if drag_dest is None:
+ self.data_source.setDragDest_(None)
+ else:
+ types = drag_dest.allowed_types()
+ self.data_source.setDragDest_(drag_dest)
diff --git a/lvc/widgets/osx/utils.py b/lvc/widgets/osx/utils.py
new file mode 100644
index 0000000..c0c2d85
--- /dev/null
+++ b/lvc/widgets/osx/utils.py
@@ -0,0 +1,2 @@
+def filename_to_unicode(filename):
+ return filename.decode('utf8')
diff --git a/lvc/widgets/osx/viewport.py b/lvc/widgets/osx/viewport.py
new file mode 100644
index 0000000..e6564d4
--- /dev/null
+++ b/lvc/widgets/osx/viewport.py
@@ -0,0 +1,101 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+""".viewport.py -- Viewport classes
+
+A Viewport represents the area where a Widget is located.
+"""
+
+from objc import YES, NO, nil
+from Foundation import *
+
+class Viewport(object):
+ """Used when a widget creates it's own NSView."""
+ def __init__(self, view, initial_frame):
+ self.view = view
+ self.view.setFrame_(initial_frame)
+ self.placement = initial_frame
+
+ def at_position(self, rect):
+ """Check if a viewport is currently positioned at rect."""
+ return self.placement == rect
+
+ def reposition(self, rect):
+ """Move the viewport to a different position."""
+ self.view.setFrame_(rect)
+ self.placement = rect
+
+ def remove(self):
+ self.view.removeFromSuperview()
+
+ def area(self):
+ """Area of our view that is occupied by the viewport."""
+ return NSRect(self.view.bounds().origin, self.placement.size)
+
+ def get_width(self):
+ return self.view.frame().size.width
+
+ def get_height(self):
+ return self.view.frame().size.height
+
+ def queue_redraw(self):
+ opaque_view = self.view.opaqueAncestor()
+ if opaque_view is not None:
+ rect = opaque_view.convertRect_fromView_(self.area(), self.view)
+ opaque_view.setNeedsDisplayInRect_(rect)
+
+ def redraw_now(self):
+ self.view.displayRect_(self.area())
+
+class BorrowedViewport(Viewport):
+ """Used when a widget uses the NSView of one of it's ancestors. We store
+ the view that we borrow as well as an NSRect specifying where on that view
+ we are placed.
+ """
+ def __init__(self, view, placement):
+ self.view = view
+ self.placement = placement
+
+ def at_position(self, rect):
+ return self.placement == rect
+
+ def reposition(self, rect):
+ self.placement = rect
+
+ def remove(self):
+ pass
+
+ def area(self):
+ return self.placement
+
+ def get_width(self):
+ return self.placement.size.width
+
+ def get_height(self):
+ return self.placement.size.height
diff --git a/lvc/widgets/osx/widgetset.py b/lvc/widgets/osx/widgetset.py
new file mode 100644
index 0000000..1203566
--- /dev/null
+++ b/lvc/widgets/osx/widgetset.py
@@ -0,0 +1,58 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+""".widgetset -- Contains all the
+platform-specific widgets. This module doesn't have any actual code in it, it
+just imports the widgets from their actual locations.
+"""
+
+from .const import *
+from .control import (TextEntry, NumberEntry,
+ SecureTextEntry, MultilineTextEntry)
+from .control import Checkbox, Button, OptionMenu, RadioButtonGroup, RadioButton
+from .customcontrol import (CustomButton,
+ ContinuousCustomButton, CustomSlider, DragableCustomButton)
+from .contextmenu import ContextMenu
+from .drawing import DrawingContext, ImageSurface, Gradient
+from .drawingwidgets import DrawingArea, Background
+from .rect import Rect
+from .layout import VBox, HBox, Alignment, Table, Scroller, Expander, TabContainer, DetachedWindowHolder
+from .window import Window, MainWindow, Dialog, FileSaveDialog, FileOpenDialog
+from .window import DirectorySelectDialog, AboutDialog, AlertDialog, PreferencesWindow, DonateWindow, DialogWindow, get_first_time_dialog_coordinates
+from .simple import (Image, ImageDisplay, Label,
+ SolidBackground, ClickableImageButton, AnimatedImageDisplay,
+ ProgressBar, HLine)
+from .tableview import (TableView, TableColumn,
+ CellRenderer, CustomCellRenderer, ImageCellRenderer,
+ CheckboxCellRenderer,
+ CUSTOM_HEADER_HEIGHT)
+from .tablemodel import (TableModel,
+ TreeTableModel)
+from .osxmenus import (MenuBar, Menu, Separator, MenuItem, RadioMenuItem, CheckMenuItem)
+from .base import Widget
diff --git a/lvc/widgets/osx/widgetupdates.py b/lvc/widgets/osx/widgetupdates.py
new file mode 100644
index 0000000..30677c2
--- /dev/null
+++ b/lvc/widgets/osx/widgetupdates.py
@@ -0,0 +1,72 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+"""widgetupdates.py -- Handle updates to our widgets
+"""
+
+from PyObjCTools import AppHelper
+
+class SizeRequestManager(object):
+ """Helper object to manage size requests
+
+ If something changes in a widget that makes us want to request a new size,
+ we avoid calculating it immediately. The reason is that the
+ new-size-request will cascade all the way up the widget tree, and then
+ result in our widget being placed. We don't necessary want all of this
+ action to happen while we are in the middle of handling an event
+ (especially with TableView). It's also inefficient to calculate things
+ immediately, since we might do something else to invalidate the size
+ request in the current event.
+
+ SizeRequestManager stores which widgets need to have their size
+ recalculated, then calls do_invalidate_size_request() using callAfter
+ """
+
+ def __init__(self):
+ self.widgets_to_request = set()
+ #app.widgetapp.connect("event-processed", self._on_event_processed)
+
+ def add_widget(self, widget):
+ if len(self.widgets_to_request) == 0:
+ AppHelper.callAfter(self._run_requests)
+ self.widgets_to_request.add(widget)
+
+ def _run_requests(self):
+ this_run = self.widgets_to_request
+ self.widgets_to_request = set()
+ for widget in this_run:
+ widget.do_invalidate_size_request()
+
+ def _on_event_processed(self, app):
+ # once we finishing handling an event, process our size requests ASAP
+ # to avoid any potential weirdness. Note: that we also schedule a
+ # call using callAfter(), often that will do nothing, but it's
+ # possible size requests get scheduled outside of an event
+ while self.widgets_to_request:
+ self._run_requests()
diff --git a/lvc/widgets/osx/window.py b/lvc/widgets/osx/window.py
new file mode 100644
index 0000000..53b1091
--- /dev/null
+++ b/lvc/widgets/osx/window.py
@@ -0,0 +1,896 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+""".window -- Top-level Window class. """
+
+import logging
+
+from AppKit import *
+from Foundation import *
+from objc import YES, NO, nil
+from PyObjCTools import AppHelper
+
+from lvc import signals
+from lvc.widgets import widgetconst
+import wrappermap
+import osxmenus
+from .helpers import NotificationForwarder
+from .base import Widget, FlippedView
+from .layout import VBox, HBox, Alignment
+from .control import Button
+from .simple import Label
+from .rect import Rect, NSRectWrapper
+from .utils import filename_to_unicode
+
+# Tracks all windows that haven't been destroyed. This makes sure there
+# object stay alive as long as the window is alive.
+alive_windows = set()
+
+class MiroResponderInterceptor(NSResponder):
+ """Intercepts cocoa events and gives our wrappers and chance to handle
+ them first.
+ """
+
+ def initWithResponder_(self, responder):
+ """Initialize a MiroResponderInterceptor
+
+ We will give the wrapper for responder a chance to handle the event,
+ then pass it along to responder.
+ """
+ self = super(MiroResponderInterceptor, self).init()
+ self.responder = responder
+ return self
+
+ def keyDown_(self, event):
+ if self.sendKeyDownToWrapper_(event):
+ return # signal handler returned True, stop processing
+
+ # If our responder is the last in the chain, we can stop intercepting
+ if self.responder.nextResponder() is None:
+ self.responder.keyDown_(event)
+ return
+
+ # Here's the tricky part, we want to call keyDown_ on our responder,
+ # but if it doesn't handle the event, then it will pass it along to
+ # it's next responder. We need to set things up so that we will
+ # intercept that call.
+
+ # Make a new MiroResponderInterceptor whose responder is the next
+ # responder down the chain.
+ next_intercepter = MiroResponderInterceptor.alloc().initWithResponder_(
+ self.responder.nextResponder())
+ # Install the interceptor
+ self.responder.setNextResponder_(next_intercepter)
+ # Send event along
+ self.responder.keyDown_(event)
+ # Restore old nextResponder value
+ self.responder.setNextResponder_(next_intercepter.responder)
+
+ def sendKeyDownToWrapper_(self, event):
+ """Give a keyDown event to the wrapper for our responder
+
+ Return True if the wrapper handled the event
+ """
+ key = event.charactersIgnoringModifiers()
+ if len(key) != 1 or not key.isalnum():
+ key = osxmenus.REVERSE_KEYS_MAP.get(key)
+ mods = osxmenus.translate_event_modifiers(event)
+ wrapper = wrappermap.wrapper(self.responder)
+ if isinstance(wrapper, Widget) or isinstance(wrapper, Window):
+ if wrapper.emit('key-press', key, mods):
+ return True
+ return False
+
+class MiroWindow(NSWindow):
+ def initWithContentRect_styleMask_backing_defer_(self, rect, mask,
+ backing, defer):
+ self = NSWindow.initWithContentRect_styleMask_backing_defer_(self,
+ rect, mask, backing, defer)
+ self._last_focus_chain = None
+ return self
+
+ def handleKeyDown_(self, event):
+ if self.handle_tab_navigation(event):
+ return
+ interceptor = MiroResponderInterceptor.alloc().initWithResponder_(
+ self.firstResponder())
+ interceptor.keyDown_(event)
+
+ def handle_tab_navigation(self, event):
+ """Handle tab navigation through the window.
+
+ :returns: True if we handled the event
+ """
+ keystr = event.charactersIgnoringModifiers()
+ if keystr[0] == NSTabCharacter:
+ # handle cycling through views with Tab.
+ self.focusNextKeyView_(True)
+ return True
+ elif keystr[0] == NSBackTabCharacter:
+ self.focusNextKeyView_(False)
+ return True
+ return False
+
+ def acceptsMouseMovedEvents(self):
+ # HACK: for some reason calling setAcceptsMouseMovedEvents_() doesn't
+ # work, we have to forcefully override this method.
+ return NO
+
+ def sendEvent_(self, event):
+ if event.type() == NSKeyDown:
+ self.handleKeyDown_(event)
+ else:
+ NSWindow.sendEvent_(self, event)
+
+ def _calc_current_focus_wrapper(self):
+ responder = self.firstResponder()
+ while responder:
+ wrapper = wrappermap.wrapper(responder)
+ # check if we have a wrapper for the view, if not try the parent
+ # view
+ if wrapper is not None:
+ return wrapper
+ responder = responder.superview()
+ return None
+
+ def focusNextKeyView_(self, is_forward):
+ current_focus = self._calc_current_focus_wrapper()
+ my_wrapper = wrappermap.wrapper(self)
+ next_focus = my_wrapper.get_next_tab_focus(current_focus, is_forward)
+ if next_focus is not None:
+ next_focus.focus()
+
+ def draggingEntered_(self, info):
+ wrapper = wrappermap.wrapper(self)
+ return wrapper.draggingEntered_(info) or NSDragOperationNone
+
+ def draggingUpdated_(self, info):
+ wrapper = wrappermap.wrapper(self)
+ return wrapper.draggingUpdated_(info) or NSDragOperationNone
+
+ def draggingExited_(self, info):
+ wrapper = wrappermap.wrapper(self)
+ wrapper.draggingExited_(info)
+
+ def prepareForDragOperation_(self, info):
+ wrapper = wrappermap.wrapper(self)
+ return wrapper.prepareForDragOperation_(info) or NO
+
+ def performDragOperation_(self, info):
+ wrapper = wrappermap.wrapper(self)
+ return wrapper.performDragOperation_(info) or NO
+
+class MainMiroWindow(MiroWindow):
+ def isMovableByWindowBackground(self):
+ return YES
+
+class Window(signals.SignalEmitter):
+ """See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
+ def __init__(self, title, rect=None):
+ signals.SignalEmitter.__init__(self)
+ self.create_signal('active-change')
+ self.create_signal('will-close')
+ self.create_signal('did-move')
+ self.create_signal('key-press')
+ self.create_signal('show')
+ self.create_signal('hide')
+ self.create_signal('on-shown')
+ self.create_signal('file-drag-motion')
+ self.create_signal('file-drag-received')
+ self.create_signal('file-drag-leave')
+ self.is_closing = False
+ if rect is None:
+ rect = Rect(0, 0, 470, 600)
+ self.nswindow = MainMiroWindow.alloc().initWithContentRect_styleMask_backing_defer_(
+ rect.nsrect,
+ self.get_style_mask(),
+ NSBackingStoreBuffered,
+ NO)
+ self.nswindow.setTitle_(title)
+ self.nswindow.setMinSize_(NSSize(470, 600))
+ self.nswindow.setReleasedWhenClosed_(NO)
+ self.content_view = FlippedView.alloc().initWithFrame_(rect.nsrect)
+ self.content_view.setAutoresizesSubviews_(NO)
+ self.nswindow.setContentView_(self.content_view)
+ self.content_widget = None
+ self.view_notifications = NotificationForwarder.create(self.content_view)
+ self.view_notifications.connect(self.on_frame_change, 'NSViewFrameDidChangeNotification')
+ self.window_notifications = NotificationForwarder.create(self.nswindow)
+ self.window_notifications.connect(self.on_activate, 'NSWindowDidBecomeMainNotification')
+ self.window_notifications.connect(self.on_deactivate, 'NSWindowDidResignMainNotification')
+ self.window_notifications.connect(self.on_did_move, 'NSWindowDidMoveNotification')
+ self.window_notifications.connect(self.on_will_close, 'NSWindowWillCloseNotification')
+ wrappermap.add(self.nswindow, self)
+ alive_windows.add(self)
+
+ def get_next_tab_focus(self, current, is_forward):
+ """Return the next widget to cycle through for keyboard focus
+
+ Subclasses can override this to for find-grained control of keyboard
+ focus.
+
+ :param current: currently-focused widget
+ :param is_forward: are we tabbing forward?
+ """
+ return None
+
+ # XXX Use MainWindow not Window for MVCStyle/MiroStyle
+ def get_style_mask(self):
+ return (NSTitledWindowMask | NSClosableWindowMask |
+ NSMiniaturizableWindowMask)
+
+ def set_title(self, title):
+ self.nswindow.setTitle_(title)
+
+ def get_title(self):
+ return self.nswindow.title()
+
+ def on_frame_change(self, notification):
+ self.place_child()
+
+ def on_activate(self, notification):
+ self.emit('active-change')
+
+ def on_deactivate(self, notification):
+ self.emit('active-change')
+
+ def on_did_move(self, notification):
+ self.emit('did-move')
+
+ def on_will_close(self, notification):
+ # unset the first responder. This allows text entry widgets to get
+ # the NSControlTextDidEndEditingNotification
+ if self.is_closing:
+ logging.info('on_will_close: already closing')
+ return
+ self.is_closing = True
+ self.nswindow.makeFirstResponder_(nil)
+ self.emit('will-close')
+ self.emit('hide')
+ self.is_closing = False
+
+ def is_active(self):
+ return self.nswindow.isMainWindow()
+
+ def is_visible(self):
+ return self.nswindow.isVisible()
+
+ def show(self):
+ if self not in alive_windows:
+ raise ValueError("Window destroyed")
+ self.nswindow.makeKeyAndOrderFront_(nil)
+ self.nswindow.makeMainWindow()
+ self.emit('show')
+ # Cocoa doesn't apply default selections as forcefully as GTK, so
+ # currently there's no need for on-shown to actually wait until the
+ # window has been shown here
+ self.emit('on-shown')
+
+ def close(self):
+ self.nswindow.close()
+
+ def destroy(self):
+ self.close()
+ self.window_notifications.disconnect()
+ self.view_notifications.disconnect()
+ self.nswindow.setContentView_(nil)
+ wrappermap.remove(self.nswindow)
+ alive_windows.discard(self)
+ self.nswindow = None
+
+ def place_child(self):
+ rect = self.nswindow.contentRectForFrameRect_(self.nswindow.frame())
+ self.content_widget.place(NSRect(NSPoint(0, 0), rect.size),
+ self.content_view)
+
+ def hookup_content_widget_signals(self):
+ self.size_req_handler = self.content_widget.connect('size-request-changed',
+ self.on_content_widget_size_request_change)
+
+ def unhook_content_widget_signals(self):
+ self.content_widget.disconnect(self.size_req_handler)
+ self.size_req_handler = None
+
+ def on_content_widget_size_request_change(self, widget, old_size):
+ self.update_size_constraints()
+
+ def set_content_widget(self, widget):
+ if self.content_widget:
+ self.content_widget.remove_viewport()
+ self.unhook_content_widget_signals()
+ self.content_widget = widget
+ self.hookup_content_widget_signals()
+ self.place_child()
+ self.update_size_constraints()
+
+ def update_size_constraints(self):
+ width, height = self.content_widget.get_size_request()
+ # It is possible the window is torn down between the size invalidate
+ # request and the actual size invalidation invocation. So check
+ # to see if nswindow is there if not then do not do anything.
+ if self.nswindow:
+ # FIXME: I'm not sure that this code does what we want it to do.
+ # It enforces the min-size when the user drags the window, but I
+ # think it should also call setContentSize_ if the window is
+ # currently too small to fit the content - BDK
+ self.nswindow.setContentMinSize_(NSSize(width, height))
+ rect = self.nswindow.contentRectForFrameRect_(self.nswindow.frame())
+ if rect.size.width < width or rect.size.height < height:
+ logging.warn("Content widget too large for this window "
+ "size available: %dx%d widget size: %dx%d",
+ rect.size.width, rect.size.height, width, height)
+
+ def get_content_widget(self):
+ return self.content_widget
+
+ def get_frame(self):
+ frame = self.nswindow.frame()
+ frame.size.height -= 22
+ return NSRectWrapper(frame)
+
+ def connect_menu_keyboard_shortcuts(self):
+ # All OS X windows are connected to the menu shortcuts
+ pass
+
+ def accept_file_drag(self, val):
+ if not val:
+ self.drag_dest = None
+ else:
+ self.drag_dest = NSDragOperationCopy
+ self.nswindow.registerForDraggedTypes_([NSFilenamesPboardType])
+
+ def prepareForDragOperation_(self, info):
+ return NO if self.drag_dest is None else YES
+
+ def performDragOperation_(self, info):
+ pb = info.draggingPasteboard()
+ available_types = set(pb.types()) & set([NSFilenamesPboardType])
+ drag_ok = False
+ if available_types:
+ type_ = available_types.pop()
+ # DANCE! Everybody dance for portable Python code!
+ values = [unicode(
+ NSURL.fileURLWithPath_(v).filePathURL()).encode('utf-8')
+ for v in list(pb.propertyListForType_(type_))]
+ self.emit('file-drag-received', values)
+ drag_ok = True
+ self.draggingExited_(info)
+ return drag_ok
+
+ def draggingEntered_(self, info):
+ return self.draggingUpdated_(info)
+
+ def draggingUpdated_(self, info):
+ self.emit('file-drag-motion')
+ return self.drag_dest
+
+ def draggingExited_(self, info):
+ self.emit('file-drag-leave')
+
+ def center(self):
+ self.nswindow.center()
+
+class MainWindow(Window):
+ def __init__(self, title, rect):
+ Window.__init__(self, title, rect)
+ self.nswindow.setReleasedWhenClosed_(NO)
+
+ def close(self):
+ self.nswindow.orderOut_(nil)
+
+class DialogBase(object):
+ def __init__(self):
+ self.sheet_parent = None
+ def set_transient_for(self, window):
+ self.sheet_parent = window
+
+class MiroPanel(NSPanel):
+ def cancelOperation_(self, event):
+ wrappermap.wrapper(self).end_with_code(-1)
+
+class Dialog(DialogBase):
+ def __init__(self, title, description=None):
+ DialogBase.__init__(self)
+ self.title = title
+ self.description = description
+ self.buttons = []
+ self.extra_widget = None
+ self.window = None
+ self.running = False
+
+ def add_button(self, text):
+ button = Button(text)
+ button.set_size(widgetconst.SIZE_NORMAL)
+ button.connect('clicked', self.on_button_clicked, len(self.buttons))
+ self.buttons.append(button)
+
+ def on_button_clicked(self, button, code):
+ self.end_with_code(code)
+
+ def end_with_code(self, code):
+ if self.sheet_parent is not None:
+ NSApp().endSheet_returnCode_(self.window, code)
+ else:
+ NSApp().stopModalWithCode_(code)
+
+ def build_text(self):
+ vbox = VBox(spacing=6)
+ if self.description is not None:
+ description_label = Label(self.description, wrap=True)
+ description_label.set_bold(True)
+ description_label.set_size_request(360, -1)
+ vbox.pack_start(description_label)
+ return vbox
+
+ def build_buttons(self):
+ hbox = HBox(spacing=12)
+ for button in reversed(self.buttons):
+ hbox.pack_start(button)
+ alignment = Alignment(xalign=1.0, yscale=1.0)
+ alignment.add(hbox)
+ return alignment
+
+ def build_content(self):
+ vbox = VBox(spacing=12)
+ vbox.pack_start(self.build_text())
+ if self.extra_widget:
+ vbox.pack_start(self.extra_widget)
+ vbox.pack_start(self.build_buttons())
+ alignment = Alignment(xscale=1.0, yscale=1.0)
+ alignment.set_padding(12, 12, 17, 17)
+ alignment.add(vbox)
+ return alignment
+
+ def build_window(self):
+ self.content_widget = self.build_content()
+ width, height = self.content_widget.get_size_request()
+ width = max(width, 400)
+ window = MiroPanel.alloc()
+ window.initWithContentRect_styleMask_backing_defer_(
+ NSMakeRect(400, 400, width, height),
+ NSTitledWindowMask, NSBackingStoreBuffered, NO)
+ view = FlippedView.alloc().initWithFrame_(NSMakeRect(0, 0, width,
+ height))
+ window.setContentView_(view)
+ window.setTitle_(self.title)
+ self.content_widget.place(view.frame(), view)
+ if self.buttons:
+ self.buttons[0].make_default()
+ return window
+
+ def hookup_content_widget_signals(self):
+ self.size_req_handler = self.content_widget.connect(
+ 'size-request-changed',
+ self.on_content_widget_size_request_change)
+
+ def unhook_content_widget_signals(self):
+ self.content_widget.disconnect(self.size_req_handler)
+ self.size_req_handler = None
+
+ def on_content_widget_size_request_change(self, widget, old_size):
+ width, height = self.content_widget.get_size_request()
+ # It is possible the window is torn down between the size invalidate
+ # request and the actual size invalidation invocation. So check
+ # to see if nswindow is there if not then do not do anything.
+ if self.window and (width, height) != old_size:
+ self.change_content_size(width, height)
+
+ def change_content_size(self, width, height):
+ content_rect = self.window.contentRectForFrameRect_(
+ self.window.frame())
+ # Cocoa's coordinate system is funky, adjust y so that the top stays
+ # in place
+ content_rect.origin.y += (content_rect.size.height - height)
+ # change our frame to fit the new content. It would be nice to
+ # animate the change, but timers don't work when we are displaying a
+ # modal dialog
+ content_rect.size = NSSize(width, height)
+ new_frame = self.window.frameRectForContentRect_(content_rect)
+ self.window.setFrame_display_(new_frame, NO)
+ # Need to call place() again, since our window has changed size
+ contentView = self.window.contentView()
+ self.content_widget.place(contentView.frame(), contentView)
+
+ def run(self):
+ self.window = self.build_window()
+ wrappermap.add(self.window, self)
+ self.hookup_content_widget_signals()
+ self.running = True
+ if self.sheet_parent is None:
+ response = NSApp().runModalForWindow_(self.window)
+ if self.window:
+ self.window.close()
+ else:
+ delegate = SheetDelegate.alloc().init()
+ NSApp().beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_(
+ self.window, self.sheet_parent.nswindow,
+ delegate, 'sheetDidEnd:returnCode:contextInfo:', 0)
+ response = NSApp().runModalForWindow_(self.window)
+ if self.window:
+ # self.window won't be around if we call destroy() to cancel
+ # the dialog
+ self.window.orderOut_(nil)
+ self.running = False
+ self.unhook_content_widget_signals()
+
+ if response < 0:
+ return -1
+ return response
+
+ def destroy(self):
+ if self.running:
+ NSApp().stopModalWithCode_(-1)
+
+ if self.window is not None:
+ self.window.setContentView_(None)
+ self.window.close()
+ self.window = None
+ self.buttons = None
+ self.extra_widget = None
+
+ def set_extra_widget(self, widget):
+ self.extra_widget = widget
+
+ def get_extra_widget(self):
+ return self.extra_widget
+
+class SheetDelegate(NSObject):
+ @AppHelper.endSheetMethod
+ def sheetDidEnd_returnCode_contextInfo_(self, sheet, return_code, info):
+ NSApp().stopModalWithCode_(return_code)
+
+class FileDialogBase(DialogBase):
+ def __init__(self):
+ DialogBase.__init__(self)
+ self._types = None
+ self._filename = None
+ self._directory = None
+ self._filter_on_run = True
+
+ def run(self):
+ self._panel.setAllowedFileTypes_(self._types)
+ if self.sheet_parent is None:
+ if self._filter_on_run:
+ response = self._panel.runModalForDirectory_file_types_(self._directory, self._filename, self._types)
+ else:
+ response = self._panel.runModalForDirectory_file_(self._directory, self._filename)
+ else:
+ delegate = SheetDelegate.alloc().init()
+ if self._filter_on_run:
+ self._panel.beginSheetForDirectory_file_types_modalForWindow_modalDelegate_didEndSelector_contextInfo_(
+ self._directory, self._filename, self._types,
+ self.sheet_parent.nswindow, delegate, 'sheetDidEnd:returnCode:contextInfo:', 0)
+ else:
+ self._panel.beginSheetForDirectory_file_modalForWindow_modalDelegate_didEndSelector_contextInfo_(
+ self._directory, self._filename,
+ self.sheet_parent.nswindow, delegate, 'sheetDidEnd:returnCode:contextInfo:', 0)
+ response = NSApp().runModalForWindow_(self._panel)
+ self._panel.orderOut_(nil)
+ return response
+
+class FileSaveDialog(FileDialogBase):
+ def __init__(self, title):
+ FileDialogBase.__init__(self)
+ self._title = title
+ self._panel = NSSavePanel.savePanel()
+ self._panel.setCanChooseFiles_(YES)
+ self._panel.setCanChooseDirectories_(NO)
+ self._filename = None
+ self._filter_on_run = False
+
+ def set_filename(self, s):
+ self._filename = filename_to_unicode(s)
+
+ def get_filename(self):
+ # Use encode('utf-8') instead of unicode_to_filename, because
+ # unicode_to_filename has code to make sure nextFilename works, but it's
+ # more important here to not change the filename.
+ return self._filename.encode('utf-8')
+
+ def run(self):
+ response = FileDialogBase.run(self)
+ if response == NSFileHandlingPanelOKButton:
+ self._filename = self._panel.filename()
+ return 0
+ self._filename = ""
+
+ def destroy(self):
+ self._panel = None
+
+ set_path = set_filename
+ get_path = get_filename
+
+class FileOpenDialog(FileDialogBase):
+ def __init__(self, title):
+ FileDialogBase.__init__(self)
+ self._title = title
+ self._panel = NSOpenPanel.openPanel()
+ self._panel.setCanChooseFiles_(YES)
+ self._panel.setCanChooseDirectories_(NO)
+ self._filenames = None
+
+ def set_select_multiple(self, value):
+ if value:
+ self._panel.setAllowsMultipleSelection_(YES)
+ else:
+ self._panel.setAllowsMultipleSelection_(NO)
+
+ def set_directory(self, d):
+ self._directory = filename_to_unicode(d)
+
+ def set_filename(self, s):
+ self._filename = filename_to_unicode(s)
+
+ def add_filters(self, filters):
+ self._types = []
+ for _, t in filters:
+ self._types += t
+
+ def get_filename(self):
+ if self._filenames is None:
+ # canceled
+ return None
+ return self.get_filenames()[0]
+
+ def get_filenames(self):
+ if self._filenames is None:
+ # canceled
+ return []
+ # Use encode('utf-8') instead of unicode_to_filename, because
+ # unicode_to_filename has code to make sure nextFilename works, but it's
+ # more important here to not change the filename.
+ return [f.encode('utf-8') for f in self._filenames]
+
+ def run(self):
+ response = FileDialogBase.run(self)
+ if response == NSFileHandlingPanelOKButton:
+ self._filenames = self._panel.filenames()
+ return 0
+ self._filename = ''
+ self._filenames = None
+
+ def destroy(self):
+ self._panel = None
+
+ set_path = set_filename
+ get_path = get_filename
+
+class DirectorySelectDialog(FileDialogBase):
+ def __init__(self, title):
+ FileDialogBase.__init__(self)
+ self._title = title
+ self._panel = NSOpenPanel.openPanel()
+ self._panel.setCanChooseFiles_(NO)
+ self._panel.setCanChooseDirectories_(YES)
+ self._directory = None
+
+ def set_directory(self, d):
+ self._directory = filename_to_unicode(d)
+
+ def get_directory(self):
+ # Use encode('utf-8') instead of unicode_to_filename, because
+ # unicode_to_filename has code to make sure nextFilename
+ # works, but it's more important here to not change the
+ # filename.
+ return self._directory.encode('utf-8')
+
+ def run(self):
+ response = FileDialogBase.run(self)
+ if response == NSFileHandlingPanelOKButton:
+ self._directory = self._panel.filenames()[0]
+ return 0
+ self._directory = ""
+
+ def destroy(self):
+ self._panel = None
+
+ set_path = set_directory
+ get_path = get_directory
+
+class AboutDialog(DialogBase):
+ def run(self):
+ optionsDictionary = dict()
+ #revision = app.config.get(prefs.APP_REVISION_NUM)
+ #if revision:
+ # optionsDictionary['Version'] = revision
+ if not optionsDictionary:
+ optionsDictionary = nil
+ NSApplication.sharedApplication().orderFrontStandardAboutPanelWithOptions_(optionsDictionary)
+ def destroy(self):
+ pass
+
+class AlertDialog(DialogBase):
+ def __init__(self, title, message, alert_type):
+ DialogBase.__init__(self)
+ self._nsalert = NSAlert.alloc().init();
+ self._nsalert.setMessageText_(title)
+ self._nsalert.setInformativeText_(message)
+ self._nsalert.setAlertStyle_(alert_type)
+ def add_button(self, text):
+ self._nsalert.addButtonWithTitle_(text)
+ def run(self):
+ self._nsalert.runModal()
+ def destroy(self):
+ self._nsalert = nil
+
+class PreferenceItem(NSToolbarItem):
+
+ def setPanel_(self, panel):
+ self.panel = panel
+
+class PreferenceToolbarDelegate(NSObject):
+
+ def initWithPanels_identifiers_window_(self, panels, identifiers, window):
+ self = super(PreferenceToolbarDelegate, self).init()
+ self.panels = panels
+ self.identifiers = identifiers
+ self.window = window
+ return self
+
+ def toolbarAllowedItemIdentifiers_(self, toolbar):
+ return self.identifiers
+
+ def toolbarDefaultItemIdentifiers_(self, toolbar):
+ return self.identifiers
+
+ def toolbarSelectableItemIdentifiers_(self, toolbar):
+ return self.identifiers
+
+ def toolbar_itemForItemIdentifier_willBeInsertedIntoToolbar_(self, toolbar,
+ itemIdentifier,
+ flag):
+ panel = self.panels[itemIdentifier]
+ item = PreferenceItem.alloc().initWithItemIdentifier_(itemIdentifier)
+ item.setLabel_(unicode(panel[1]))
+ item.setImage_(NSImage.imageNamed_(u"pref_tab_%s" % itemIdentifier))
+ item.setAction_("switchPreferenceView:")
+ item.setTarget_(self)
+ item.setPanel_(panel[0])
+ return item
+
+ def validateToolbarItem_(self, item):
+ return YES
+
+ def switchPreferenceView_(self, sender):
+ self.window.do_select_panel(sender.panel, YES)
+
+class DialogWindow(Window):
+ def __init__(self, title, rect, allow_miniaturize=False):
+ self.allow_miniaturize = allow_miniaturize
+ Window.__init__(self, title, rect)
+ self.nswindow.setShowsToolbarButton_(NO)
+
+ def get_style_mask(self):
+ mask = (NSTitledWindowMask | NSClosableWindowMask)
+ if self.allow_miniaturize:
+ mask |= NSMiniaturizableWindowMask
+ return mask
+
+class DonateWindow(Window):
+ def __init__(self, title):
+ Window.__init__(self, title, Rect(0, 0, 640, 440))
+ self.panels = dict()
+ self.identifiers = list()
+ self.first_show = True
+ self.nswindow.setShowsToolbarButton_(NO)
+ self.nswindow.setReleasedWhenClosed_(NO)
+ self.app_notifications = NotificationForwarder.create(NSApp())
+ self.app_notifications.connect(self.on_app_quit,
+ 'NSApplicationWillTerminateNotification')
+
+ def destroy(self):
+ super(PreferencesWindow, self).destroy()
+ self.app_notifications.disconnect()
+
+ def get_style_mask(self):
+ return (NSTitledWindowMask | NSClosableWindowMask |
+ NSMiniaturizableWindowMask)
+
+ def show(self):
+ if self.first_show:
+ self.nswindow.center()
+ self.first_show = False
+ Window.show(self)
+
+ def on_app_quit(self, notification):
+ self.close()
+
+class PreferencesWindow(Window):
+ def __init__(self, title):
+ Window.__init__(self, title, Rect(0, 0, 640, 440))
+ self.panels = dict()
+ self.identifiers = list()
+ self.first_show = True
+ self.nswindow.setShowsToolbarButton_(NO)
+ self.nswindow.setReleasedWhenClosed_(NO)
+ self.app_notifications = NotificationForwarder.create(NSApp())
+ self.app_notifications.connect(self.on_app_quit,
+ 'NSApplicationWillTerminateNotification')
+
+ def destroy(self):
+ super(PreferencesWindow, self).destroy()
+ self.app_notifications.disconnect()
+
+ def get_style_mask(self):
+ return (NSTitledWindowMask | NSClosableWindowMask |
+ NSMiniaturizableWindowMask)
+
+ def append_panel(self, name, panel, title, image_name):
+ self.panels[name] = (panel, title)
+ self.identifiers.append(name)
+
+ def finish_panels(self):
+ self.tbdelegate = PreferenceToolbarDelegate.alloc().initWithPanels_identifiers_window_(self.panels, self.identifiers, self)
+ toolbar = NSToolbar.alloc().initWithIdentifier_(u"Preferences")
+ toolbar.setAllowsUserCustomization_(NO)
+ toolbar.setDelegate_(self.tbdelegate)
+
+ self.nswindow.setToolbar_(toolbar)
+
+ def select_panel(self, index):
+ panel = self.identifiers[index]
+ self.nswindow.toolbar().setSelectedItemIdentifier_(panel)
+ self.do_select_panel(self.panels[panel][0], NO)
+
+ def do_select_panel(self, panel, animate):
+ wframe = self.nswindow.frame()
+ vsize = list(panel.get_size_request())
+ if vsize[0] < 650:
+ vsize[0] = 650
+ if vsize[1] < 200:
+ vsize[1] = 200
+
+ toolbarHeight = wframe.size.height - self.nswindow.contentView().frame().size.height
+ wframe.origin.y += wframe.size.height - vsize[1] - toolbarHeight
+ wframe.size = (vsize[0], vsize[1] + toolbarHeight)
+
+ self.set_content_widget(panel)
+ self.nswindow.setFrame_display_animate_(wframe, YES, animate)
+
+ def show(self):
+ if self.first_show:
+ self.nswindow.center()
+ self.first_show = False
+ Window.show(self)
+
+ def on_app_quit(self, notification):
+ self.close()
+
+def get_first_time_dialog_coordinates(width, height):
+ """Returns the coordinates for the first time dialog.
+ """
+ # windowFrame is None on first run. in that case, we want
+ # to put librevideoconverter in the middle.
+ mainscreen = NSScreen.mainScreen()
+ rect = mainscreen.frame()
+
+ x = (rect.size.width - width) / 2
+ y = (rect.size.height - height) / 2
+
+ return x, y
diff --git a/lvc/widgets/osx/wrappermap.py b/lvc/widgets/osx/wrappermap.py
new file mode 100644
index 0000000..624a496
--- /dev/null
+++ b/lvc/widgets/osx/wrappermap.py
@@ -0,0 +1,48 @@
+# @Base: Miro - an RSS based video player application
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011
+# Participatory Culture Foundation
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+#
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+""".wrappermap -- Map NSViews and NSWindows to the
+Widget that wraps them.
+"""
+
+# Maps NSViews/NSWinows -> wrapper objects.
+wrapper_mapping = dict()
+
+def wrapper(wrapped):
+ """Find the wrapper object for an NSView/NSWindow."""
+ try:
+ return wrapper_mapping[wrapped]
+ except KeyError:
+ return None
+
+def add(wrapped, wrapper):
+ wrapper_mapping[wrapped] = wrapper
+
+def remove(wrapped):
+ del wrapper_mapping[wrapped]