1
# Miro - an RSS based video player application
2
# Copyright (C) 2005-2010 Participatory Culture Foundation
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18
# In addition, as a special exception, the copyright holders give
19
# permission to link the code of portions of this program with the OpenSSL
22
# You must obey the GNU General Public License in all respects for all of
23
# the code used other than OpenSSL. If you modify file(s) with this
24
# exception, you may extend this exception to your version of the file(s),
25
# but you are not obligated to do so. If you do not wish to do so, delete
26
# this exception statement from your version. If you delete this exception
27
# statement from all source files in the program, then also delete it here.
29
"""miro.plat.frontends.widgets.control - Controls."""
32
from Foundation import *
33
from objc import YES, NO, nil
37
from miro.frontends.widgets import widgetconst
38
from miro.plat import resources
39
from miro.plat.frontends.widgets import wrappermap
40
from miro.plat.frontends.widgets import layoutmanager
41
from miro.plat.frontends.widgets.base import Widget
42
from miro.plat.frontends.widgets.helpers import NotificationForwarder
44
from miro import searchengines
45
from miro import config, prefs
47
class SizedControl(Widget):
48
def set_size(self, size):
49
if size == widgetconst.SIZE_NORMAL:
50
self.view.cell().setControlSize_(NSRegularControlSize)
51
font = NSFont.systemFontOfSize_(NSFont.systemFontSize())
52
elif size == widgetconst.SIZE_SMALL:
53
font = NSFont.systemFontOfSize_(NSFont.smallSystemFontSize())
54
self.view.cell().setControlSize_(NSSmallControlSize)
56
raise ValueError("Unknown size: %s" % size)
57
self.view.setFont_(font)
59
class BaseTextEntry(SizedControl):
60
"""See https://develop.participatoryculture.org/trac/democracy/wiki/WidgetAPI for a description of the API for this class."""
61
def __init__(self, initial_text=None):
62
SizedControl.__init__(self)
63
self.view = self.make_view()
64
self.font = NSFont.systemFontOfSize_(NSFont.systemFontSize())
65
self.view.setFont_(self.font)
66
self.view.setEditable_(YES)
67
self.view.cell().setScrollable_(YES)
68
self.view.cell().setLineBreakMode_(NSLineBreakByClipping)
69
self.sizer_cell = self.view.cell().copy()
71
self.view.setStringValue_(initial_text)
72
self.set_width(len(initial_text))
76
self.notifications = NotificationForwarder.create(self.view)
78
self.create_signal('activate')
79
self.create_signal('changed')
80
self.create_signal('validate')
83
self.view.window().makeFirstResponder_(self.view)
85
def viewport_created(self):
86
SizedControl.viewport_created(self)
87
self.notifications.connect(self.on_changed, 'NSControlTextDidChangeNotification')
89
def remove_viewport(self):
90
SizedControl.remove_viewport(self)
91
self.notifications.disconnect()
94
return -self.view.font().descender() + 2
96
def on_changed(self, notification):
99
def calc_size_request(self):
100
size = self.sizer_cell.cellSize()
101
return size.width, size.height
103
def set_text(self, text):
104
self.view.setStringValue_(text)
108
return self.view.stringValue()
110
def set_width(self, chars):
111
self.sizer_cell.setStringValue_('X' * chars)
112
self.invalidate_size_request()
114
def set_activates_default(self, setting):
118
SizedControl.enable(self)
119
self.view.setEnabled_(True)
122
SizedControl.disable(self)
123
self.view.setEnabled_(False)
125
class MiroTextField(NSTextField):
126
def becomeFirstResponder(self):
127
wrappermap.wrapper(self).emit('activate')
128
return NSTextField.becomeFirstResponder(self)
130
class TextEntry(BaseTextEntry):
132
return MiroTextField.alloc().init()
134
class MiroSecureTextField(NSSecureTextField):
135
def becomeFirstResponder(self):
136
wrappermap.wrapper(self).emit('activate')
137
return NSSecureTextField.becomeFirstResponder(self)
139
class SecureTextEntry(BaseTextEntry):
141
return MiroSecureTextField.alloc().init()
143
class MiroSearchTextField(NSSearchField):
144
def becomeFirstResponder(self):
145
wrappermap.wrapper(self).emit('activate')
146
return NSSearchField.becomeFirstResponder(self)
148
class SearchTextEntry(BaseTextEntry):
150
return MiroSearchTextField.alloc().init()
152
class MultilineTextEntry(Widget):
154
def __init__(self, initial_text=None):
155
Widget.__init__(self)
156
if initial_text is None:
158
self.view = NSTextView.alloc().initWithFrame_(NSRect((0,0),(50,50)))
159
self.view.setMaxSize_((1.0e7, 1.0e7))
160
self.view.setHorizontallyResizable_(NO)
161
self.view.setVerticallyResizable_(YES)
162
self.notifications = NotificationForwarder.create(self.view)
163
if initial_text is not None:
164
self.set_text(initial_text)
165
self.set_size(widgetconst.SIZE_NORMAL)
167
def set_size(self, size):
168
if size == widgetconst.SIZE_NORMAL:
169
font = NSFont.systemFontOfSize_(NSFont.systemFontSize())
170
elif size == widgetconst.SIZE_SMALL:
171
self.view.cell().setControlSize_(NSSmallControlSize)
173
raise ValueError("Unknown size: %s" % size)
174
self.view.setFont_(font)
176
def viewport_created(self):
177
Widget.viewport_created(self)
178
self.notifications.connect(self.on_changed, 'NSTextDidChangeNotification')
179
self.invalidate_size_request()
181
def remove_viewport(self):
182
Widget.remove_viewport(self)
183
self.notifications.disconnect()
186
self.view.window().makeFirstResponder_(self.view)
188
def set_text(self, text):
189
self.view.setString_(text)
190
self.invalidate_size_request()
193
return self.view.string()
195
def on_changed(self, notification):
196
self.invalidate_size_request()
198
def calc_size_request(self):
199
if self.view.superview() is None:
201
width = self.view.superview().frame().size.width
202
height = self.view.frame().size.height
203
if self.parent_is_scroller:
204
width -= NSScroller.scrollerWidth()
205
return (width, height)
207
class MiroButton(NSButton):
209
def initWithSignal_(self, signal):
210
self = super(MiroButton, self).init()
214
def sendAction_to_(self, action, to):
215
# We override the Cocoa machinery here and just send it to our wrapper
217
wrappermap.wrapper(self).emit(self.signal)
220
class Checkbox(SizedControl):
221
"""See https://develop.participatoryculture.org/trac/democracy/wiki/WidgetAPI for a description of the API for this class."""
222
def __init__(self, label):
223
SizedControl.__init__(self)
224
self.create_signal('toggled')
226
self.view = MiroButton.alloc().initWithSignal_('toggled')
227
self.view.setButtonType_(NSSwitchButton)
228
self.view.setTitle_(self.label)
230
def calc_size_request(self):
231
size = self.view.cell().cellSize()
232
return (size.width, size.height)
235
return -self.view.font().descender() + 1
237
def get_checked(self):
238
return self.view.state() == NSOnState
240
def set_checked(self, value):
242
self.view.setState_(NSOnState)
244
self.view.setState_(NSOffState)
247
SizedControl.enable(self)
248
self.view.setEnabled_(True)
251
SizedControl.disable(self)
252
self.view.setEnabled_(False)
254
class Button(SizedControl):
255
"""See https://develop.participatoryculture.org/trac/democracy/wiki/WidgetAPI for a description of the API for this class."""
256
def __init__(self, label, style='normal'):
257
SizedControl.__init__(self)
260
self.create_signal('clicked')
261
self.view = MiroButton.alloc().initWithSignal_('clicked')
262
self.view.setButtonType_(NSMomentaryPushInButton)
264
self.setup_style(style)
267
def set_text(self, label):
271
def set_color(self, color):
272
self.color = self.make_color(color)
275
def _set_title(self):
276
if self.color is None:
277
self.view.setTitle_(self.title)
280
NSForegroundColorAttributeName: self.color,
281
NSFontAttributeName: self.view.font()
283
string = NSAttributedString.alloc().initWithString_attributes_(
284
self.title, attributes)
285
self.view.setAttributedTitle_(string)
287
def setup_style(self, style):
288
if style == 'normal':
289
self.view.setBezelStyle_(NSRoundedBezelStyle)
293
elif style == 'smooth':
294
self.view.setBezelStyle_(NSRoundRectBezelStyle)
297
self.paragraph_style = NSMutableParagraphStyle.alloc().init()
298
self.paragraph_style.setAlignment_(NSCenterTextAlignment)
300
def make_default(self):
301
self.view.setKeyEquivalent_("\r")
303
def calc_size_request(self):
304
size = self.view.cell().cellSize()
305
width = max(self.min_width, size.width + self.pad_width)
306
height = size.height + self.pad_height
310
return -self.view.font().descender() + 10 + self.pad_height
313
SizedControl.enable(self)
314
self.view.setEnabled_(True)
317
SizedControl.disable(self)
318
self.view.setEnabled_(False)
320
class MiroPopupButton(NSPopUpButton):
323
self = super(MiroPopupButton, self).init()
324
self.setTarget_(self)
325
self.setAction_('handleChange:')
328
def handleChange_(self, sender):
329
wrappermap.wrapper(self).emit('changed', self.indexOfSelectedItem())
331
class OptionMenu(SizedControl):
332
def __init__(self, options):
333
SizedControl.__init__(self)
334
self.create_signal('changed')
335
self.view = MiroPopupButton.alloc().init()
336
self.options = options
337
for option in options:
338
self.view.addItemWithTitle_(option)
341
if self.view.cell().controlSize() == NSRegularControlSize:
342
return -self.view.font().descender() + 6
344
return -self.view.font().descender() + 5
346
def calc_size_request(self):
347
return self.view.cell().cellSize()
349
def set_selected(self, index):
350
self.view.selectItemAtIndex_(index)
352
def get_selected(self):
353
return self.view.indexOfSelectedItem()
356
SizedControl.enable(self)
357
self.view.setEnabled_(True)
360
SizedControl.disable(self)
361
self.view.setEnabled_(False)
363
class RadioButtonGroup:
367
def handle_click(self, widget):
368
self.set_selected(widget)
370
def add_button(self, button):
371
self._buttons.append(button)
372
button.connect('clicked', self.handle_click)
373
if len(self._buttons) == 1:
374
button.view.setState_(NSOnState)
376
button.view.setState_(NSOffState)
378
def get_buttons(self):
381
def get_selected(self):
382
for mem in self._buttons:
383
if mem.get_selected():
386
def set_selected(self, button):
387
for mem in self._buttons:
389
mem.view.setState_(NSOnState)
391
mem.view.setState_(NSOffState)
393
class RadioButton(SizedControl):
394
def __init__(self, label, group=None):
395
SizedControl.__init__(self)
396
self.create_signal('clicked')
397
self.view = MiroButton.alloc().initWithSignal_('clicked')
398
self.view.setButtonType_(NSRadioButton)
399
self.view.setTitle_(label)
401
if group is not None:
404
self.group = RadioButtonGroup()
406
self.group.add_button(self)
408
def calc_size_request(self):
409
size = self.view.cell().cellSize()
410
return (size.width, size.height)
413
-self.view.font().descender() + 2
418
def get_selected(self):
419
return self.view.state() == NSOnState
421
def set_selected(self):
422
self.group.set_selected(self)
425
SizedControl.enable(self)
426
self.view.setEnabled_(True)
429
SizedControl.disable(self)
430
self.view.setEnabled_(False)
433
class VideoSearchTextEntry (SearchTextEntry):
436
return NSVideoSearchField.alloc().init()
438
def selected_engine(self):
439
return self.view.currentItem.representedObject().name
441
def select_engine(self, engine):
442
self.view.selectEngine_(self.view.menuItemForEngine_(engine))
444
class NSVideoSearchField (MiroSearchTextField):
447
self = super(NSVideoSearchField, self).init()
448
self._engineToMenuItem = {}
449
self.currentItem = nil
450
self.setTarget_(self)
451
self.setAction_('search:')
452
self.cell().setBezeled_(YES)
453
self.cell().setSearchMenuTemplate_(self.makeSearchMenuTemplate())
454
self.cell().setSendsWholeSearchString_(YES)
455
self.cell().setSendsSearchStringImmediately_(NO)
456
self.cell().setScrollable_(YES)
457
self.setStringValue_("")
460
def makeSearchMenuTemplate(self):
461
menu = NSMenu.alloc().initWithTitle_("Search Menu")
462
for engine in reversed(searchengines.get_search_engines()):
463
nsitem = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_(engine.title, 'selectEngine:', '')
464
nsitem.setTarget_(self)
465
nsitem.setImage_(_getEngineIcon(engine))
466
nsitem.setRepresentedObject_(engine)
467
self._engineToMenuItem[engine.name] = nsitem
468
menu.insertItem_atIndex_(nsitem, 0)
471
def selectEngine_(self, sender):
472
if self.currentItem is not nil:
473
self.currentItem.setState_(NSOffState)
474
self.currentItem = sender
475
sender.setState_(NSOnState)
476
engine = sender.representedObject()
477
self.cell().searchButtonCell().setImage_(_getSearchIcon(engine))
479
def search_(self, sender):
480
if self.stringValue() != "":
481
wrappermap.wrapper(self).emit('validate')
483
def menuItemForEngine_(self, engine):
484
return self._engineToMenuItem[engine]
486
class VideoSearchFieldCell (NSSearchFieldCell):
488
def searchButtonRectForBounds_(self, bounds):
489
return NSRect(NSPoint(8.0, 3.0), NSSize(25.0, 16.0))
491
def searchTextRectForBounds_(self, bounds):
492
textBounds = NSSearchFieldCell.searchTextRectForBounds_(self, bounds)
493
cancelButtonBounds = NSSearchFieldCell.cancelButtonRectForBounds_(self, bounds)
494
searchButtonBounds = self.searchButtonRectForBounds_(bounds)
496
x = searchButtonBounds.origin.x + searchButtonBounds.size.width + 4
497
y = textBounds.origin.y
498
width = bounds.size.width - x - cancelButtonBounds.size.width
499
height = textBounds.size.height
501
return ((x, y), (width, height))
503
NSVideoSearchField.setCellClass_(VideoSearchFieldCell)
505
def _getEngineIcon(engine):
506
engineIconPath = resources.path('images/search_icon_%s.png' % engine.name)
507
if config.get(prefs.THEME_NAME) and engine.filename:
508
if engine.filename.startswith(resources.theme_path(
509
config.get(prefs.THEME_NAME), 'searchengines')):
510
# this search engine came from a theme; look up the icon in the
511
# theme directory instead
512
engineIconPath = resources.theme_path(
513
config.get(prefs.THEME_NAME),
514
'images/search_icon_%s.png' % engine.name)
515
if not os.path.exists(engineIconPath):
517
return NSImage.alloc().initByReferencingFile_(engineIconPath)
520
def _getSearchIcon(engine):
521
if engine.name not in searchIcons:
522
searchIcons[engine.name] = _makeSearchIcon(engine)
523
return searchIcons[engine.name]
525
def _makeSearchIcon(engine):
526
popupRectangle = NSImage.imageNamed_(u'search_popup_triangle')
527
popupRectangleSize = popupRectangle.size()
529
engineIconPath = resources.path('images/search_icon_%s.png' % engine.name)
530
if not os.path.exists(engineIconPath):
532
engineIcon = NSImage.alloc().initByReferencingFile_(engineIconPath)
533
engineIconSize = engineIcon.size()
535
searchIconSize = (engineIconSize.width + popupRectangleSize.width + 2, engineIconSize.height)
536
searchIcon = NSImage.alloc().initWithSize_(searchIconSize)
538
searchIcon.lockFocus()
540
engineIcon.compositeToPoint_operation_((0,0), NSCompositeSourceOver)
541
popupRectangleX = engineIconSize.width + 2
542
popupRectangleY = (engineIconSize.height - popupRectangleSize.height) / 2
543
popupRectangle.compositeToPoint_operation_((popupRectangleX, popupRectangleY), NSCompositeSourceOver)
545
searchIcon.unlockFocus()