aboutsummaryrefslogtreecommitdiffstats
path: root/lvc/widgets/osx/control.py
diff options
context:
space:
mode:
Diffstat (limited to 'lvc/widgets/osx/control.py')
-rw-r--r--lvc/widgets/osx/control.py530
1 files changed, 530 insertions, 0 deletions
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)