diff options
Diffstat (limited to 'lvc/widgets/osx/control.py')
-rw-r--r-- | lvc/widgets/osx/control.py | 530 |
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) |