~ubuntu-branches/ubuntu/natty/miro/natty

« back to all changes in this revision

Viewing changes to platform/osx/plat/frontends/widgets/control.py

  • Committer: Bazaar Package Importer
  • Author(s): Bryce Harrington
  • Date: 2011-01-22 02:46:33 UTC
  • mfrom: (1.4.10 upstream) (1.7.5 experimental)
  • Revision ID: james.westby@ubuntu.com-20110122024633-kjme8u93y2il5nmf
Tags: 3.5.1-1ubuntu1
* Merge from debian.  Remaining ubuntu changes:
  - Use python 2.7 instead of python 2.6
  - Relax dependency on python-dbus to >= 0.83.0

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Miro - an RSS based video player application
2
 
# Copyright (C) 2005-2010 Participatory Culture Foundation
3
 
#
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.
8
 
#
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.
13
 
#
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
17
 
#
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
20
 
# library.
21
 
#
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.
28
 
 
29
 
"""miro.plat.frontends.widgets.control - Controls."""
30
 
 
31
 
from AppKit import *
32
 
from Foundation import *
33
 
from objc import YES, NO, nil
34
 
 
35
 
import os
36
 
 
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
43
 
 
44
 
from miro import searchengines
45
 
from miro import config, prefs
46
 
 
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)
55
 
        else:
56
 
            raise ValueError("Unknown size: %s" % size)
57
 
        self.view.setFont_(font)
58
 
 
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()
70
 
        if initial_text:
71
 
            self.view.setStringValue_(initial_text)
72
 
            self.set_width(len(initial_text))
73
 
        else:
74
 
            self.set_width(10)
75
 
 
76
 
        self.notifications = NotificationForwarder.create(self.view)
77
 
 
78
 
        self.create_signal('activate')
79
 
        self.create_signal('changed')
80
 
        self.create_signal('validate')
81
 
 
82
 
    def focus(self):
83
 
        self.view.window().makeFirstResponder_(self.view)
84
 
 
85
 
    def viewport_created(self):
86
 
        SizedControl.viewport_created(self)
87
 
        self.notifications.connect(self.on_changed, 'NSControlTextDidChangeNotification')
88
 
 
89
 
    def remove_viewport(self):
90
 
        SizedControl.remove_viewport(self)
91
 
        self.notifications.disconnect()
92
 
 
93
 
    def baseline(self):
94
 
        return -self.view.font().descender() + 2
95
 
 
96
 
    def on_changed(self, notification):
97
 
        self.emit('changed')
98
 
 
99
 
    def calc_size_request(self):
100
 
        size = self.sizer_cell.cellSize()
101
 
        return size.width, size.height
102
 
 
103
 
    def set_text(self, text):
104
 
        self.view.setStringValue_(text)
105
 
        self.emit('changed')
106
 
 
107
 
    def get_text(self):
108
 
        return self.view.stringValue()
109
 
 
110
 
    def set_width(self, chars):
111
 
        self.sizer_cell.setStringValue_('X' * chars)
112
 
        self.invalidate_size_request()
113
 
 
114
 
    def set_activates_default(self, setting):
115
 
        pass
116
 
 
117
 
    def enable(self):
118
 
        SizedControl.enable(self)
119
 
        self.view.setEnabled_(True)
120
 
 
121
 
    def disable(self):
122
 
        SizedControl.disable(self)
123
 
        self.view.setEnabled_(False)
124
 
 
125
 
class MiroTextField(NSTextField):
126
 
    def becomeFirstResponder(self):
127
 
        wrappermap.wrapper(self).emit('activate')
128
 
        return NSTextField.becomeFirstResponder(self)
129
 
 
130
 
class TextEntry(BaseTextEntry):
131
 
    def make_view(self):
132
 
        return MiroTextField.alloc().init()
133
 
 
134
 
class MiroSecureTextField(NSSecureTextField):
135
 
    def becomeFirstResponder(self):
136
 
        wrappermap.wrapper(self).emit('activate')
137
 
        return NSSecureTextField.becomeFirstResponder(self)
138
 
 
139
 
class SecureTextEntry(BaseTextEntry):
140
 
    def make_view(self):
141
 
        return MiroSecureTextField.alloc().init()
142
 
 
143
 
class MiroSearchTextField(NSSearchField):
144
 
    def becomeFirstResponder(self):
145
 
        wrappermap.wrapper(self).emit('activate')
146
 
        return NSSearchField.becomeFirstResponder(self)
147
 
 
148
 
class SearchTextEntry(BaseTextEntry):
149
 
    def make_view(self):
150
 
        return MiroSearchTextField.alloc().init()
151
 
 
152
 
class MultilineTextEntry(Widget):
153
 
 
154
 
    def __init__(self, initial_text=None):
155
 
        Widget.__init__(self)
156
 
        if initial_text is None:
157
 
            initial_text = ""
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)
166
 
 
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)
172
 
        else:
173
 
            raise ValueError("Unknown size: %s" % size)
174
 
        self.view.setFont_(font)
175
 
 
176
 
    def viewport_created(self):
177
 
        Widget.viewport_created(self)
178
 
        self.notifications.connect(self.on_changed, 'NSTextDidChangeNotification')
179
 
        self.invalidate_size_request()
180
 
 
181
 
    def remove_viewport(self):
182
 
        Widget.remove_viewport(self)
183
 
        self.notifications.disconnect()
184
 
 
185
 
    def focus(self):
186
 
        self.view.window().makeFirstResponder_(self.view)
187
 
 
188
 
    def set_text(self, text):
189
 
        self.view.setString_(text)
190
 
        self.invalidate_size_request()
191
 
 
192
 
    def get_text(self):
193
 
        return self.view.string()
194
 
 
195
 
    def on_changed(self, notification):
196
 
        self.invalidate_size_request()
197
 
 
198
 
    def calc_size_request(self):
199
 
        if self.view.superview() is None:
200
 
            return (50, 50)
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)
206
 
    
207
 
class MiroButton(NSButton):
208
 
    
209
 
    def initWithSignal_(self, signal):
210
 
        self = super(MiroButton, self).init()
211
 
        self.signal = signal
212
 
        return self
213
 
    
214
 
    def sendAction_to_(self, action, to):
215
 
        # We override the Cocoa machinery here and just send it to our wrapper
216
 
        # widget.
217
 
        wrappermap.wrapper(self).emit(self.signal)
218
 
        return YES
219
 
 
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')
225
 
        self.label = label
226
 
        self.view = MiroButton.alloc().initWithSignal_('toggled')
227
 
        self.view.setButtonType_(NSSwitchButton)
228
 
        self.view.setTitle_(self.label)
229
 
 
230
 
    def calc_size_request(self):
231
 
        size = self.view.cell().cellSize()
232
 
        return (size.width, size.height)
233
 
 
234
 
    def baseline(self):
235
 
        return -self.view.font().descender() + 1
236
 
 
237
 
    def get_checked(self):
238
 
        return self.view.state() == NSOnState
239
 
 
240
 
    def set_checked(self, value):
241
 
        if value:
242
 
            self.view.setState_(NSOnState)
243
 
        else:
244
 
            self.view.setState_(NSOffState)
245
 
 
246
 
    def enable(self):
247
 
        SizedControl.enable(self)
248
 
        self.view.setEnabled_(True)
249
 
 
250
 
    def disable(self):
251
 
        SizedControl.disable(self)
252
 
        self.view.setEnabled_(False)
253
 
 
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)
258
 
        self.color = None
259
 
        self.title = label
260
 
        self.create_signal('clicked')
261
 
        self.view = MiroButton.alloc().initWithSignal_('clicked')
262
 
        self.view.setButtonType_(NSMomentaryPushInButton)
263
 
        self._set_title()
264
 
        self.setup_style(style)
265
 
        self.min_width = 0
266
 
 
267
 
    def set_text(self, label):
268
 
        self.title = label
269
 
        self._set_title()
270
 
 
271
 
    def set_color(self, color):
272
 
        self.color = self.make_color(color)
273
 
        self._set_title()
274
 
 
275
 
    def _set_title(self):
276
 
        if self.color is None:
277
 
            self.view.setTitle_(self.title)
278
 
        else:
279
 
            attributes = {
280
 
                NSForegroundColorAttributeName: self.color,
281
 
                NSFontAttributeName: self.view.font()
282
 
            }
283
 
            string = NSAttributedString.alloc().initWithString_attributes_(
284
 
                    self.title, attributes)
285
 
            self.view.setAttributedTitle_(string)
286
 
 
287
 
    def setup_style(self, style):
288
 
        if style == 'normal':
289
 
            self.view.setBezelStyle_(NSRoundedBezelStyle)
290
 
            self.pad_height = 0
291
 
            self.pad_width = 10
292
 
            self.min_width = 112
293
 
        elif style == 'smooth':
294
 
            self.view.setBezelStyle_(NSRoundRectBezelStyle)
295
 
            self.pad_width = 0
296
 
            self.pad_height = 4
297
 
        self.paragraph_style = NSMutableParagraphStyle.alloc().init()
298
 
        self.paragraph_style.setAlignment_(NSCenterTextAlignment)
299
 
 
300
 
    def make_default(self):
301
 
        self.view.setKeyEquivalent_("\r")
302
 
 
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
307
 
        return width, height
308
 
 
309
 
    def baseline(self):
310
 
        return -self.view.font().descender() + 10 + self.pad_height
311
 
 
312
 
    def enable(self):
313
 
        SizedControl.enable(self)
314
 
        self.view.setEnabled_(True)
315
 
 
316
 
    def disable(self):
317
 
        SizedControl.disable(self)
318
 
        self.view.setEnabled_(False)
319
 
 
320
 
class MiroPopupButton(NSPopUpButton):
321
 
 
322
 
    def init(self):
323
 
        self = super(MiroPopupButton, self).init()
324
 
        self.setTarget_(self)
325
 
        self.setAction_('handleChange:')
326
 
        return self
327
 
 
328
 
    def handleChange_(self, sender):
329
 
        wrappermap.wrapper(self).emit('changed', self.indexOfSelectedItem())
330
 
 
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)
339
 
 
340
 
    def baseline(self):
341
 
        if self.view.cell().controlSize() == NSRegularControlSize:
342
 
            return -self.view.font().descender() + 6
343
 
        else:
344
 
            return -self.view.font().descender() + 5
345
 
 
346
 
    def calc_size_request(self):
347
 
        return self.view.cell().cellSize()
348
 
 
349
 
    def set_selected(self, index):
350
 
        self.view.selectItemAtIndex_(index)
351
 
 
352
 
    def get_selected(self):
353
 
        return self.view.indexOfSelectedItem()
354
 
 
355
 
    def enable(self):
356
 
        SizedControl.enable(self)
357
 
        self.view.setEnabled_(True)
358
 
 
359
 
    def disable(self):
360
 
        SizedControl.disable(self)
361
 
        self.view.setEnabled_(False)
362
 
 
363
 
class RadioButtonGroup:
364
 
    def __init__(self):
365
 
        self._buttons = []
366
 
 
367
 
    def handle_click(self, widget):
368
 
        self.set_selected(widget)
369
 
 
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)
375
 
        else:
376
 
            button.view.setState_(NSOffState)
377
 
 
378
 
    def get_buttons(self):
379
 
        return self._buttons
380
 
 
381
 
    def get_selected(self):
382
 
        for mem in self._buttons:
383
 
            if mem.get_selected():
384
 
                return mem
385
 
 
386
 
    def set_selected(self, button):
387
 
        for mem in self._buttons:
388
 
            if button is mem:
389
 
                mem.view.setState_(NSOnState)
390
 
            else:
391
 
                mem.view.setState_(NSOffState)
392
 
 
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)
400
 
 
401
 
        if group is not None:
402
 
            self.group = group
403
 
        else:
404
 
            self.group = RadioButtonGroup() 
405
 
 
406
 
        self.group.add_button(self)
407
 
 
408
 
    def calc_size_request(self):
409
 
        size = self.view.cell().cellSize()
410
 
        return (size.width, size.height)
411
 
 
412
 
    def baseline(self):
413
 
        -self.view.font().descender() + 2
414
 
 
415
 
    def get_group(self):
416
 
        return self.group
417
 
 
418
 
    def get_selected(self):
419
 
        return self.view.state() == NSOnState
420
 
 
421
 
    def set_selected(self):
422
 
        self.group.set_selected(self)
423
 
 
424
 
    def enable(self):
425
 
        SizedControl.enable(self)
426
 
        self.view.setEnabled_(True)
427
 
 
428
 
    def disable(self):
429
 
        SizedControl.disable(self)
430
 
        self.view.setEnabled_(False)
431
 
 
432
 
 
433
 
class VideoSearchTextEntry (SearchTextEntry):
434
 
 
435
 
    def make_view(self):
436
 
        return NSVideoSearchField.alloc().init()
437
 
 
438
 
    def selected_engine(self):
439
 
        return self.view.currentItem.representedObject().name
440
 
 
441
 
    def select_engine(self, engine):
442
 
        self.view.selectEngine_(self.view.menuItemForEngine_(engine))
443
 
 
444
 
class NSVideoSearchField (MiroSearchTextField):
445
 
 
446
 
    def init(self):
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_("")
458
 
        return self
459
 
 
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)
469
 
        return menu
470
 
 
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))
478
 
 
479
 
    def search_(self, sender):
480
 
        if self.stringValue() != "":
481
 
            wrappermap.wrapper(self).emit('validate')
482
 
 
483
 
    def menuItemForEngine_(self, engine):
484
 
        return self._engineToMenuItem[engine]
485
 
 
486
 
class VideoSearchFieldCell (NSSearchFieldCell):
487
 
 
488
 
    def searchButtonRectForBounds_(self, bounds):
489
 
        return NSRect(NSPoint(8.0, 3.0), NSSize(25.0, 16.0))
490
 
 
491
 
    def searchTextRectForBounds_(self, bounds):
492
 
        textBounds = NSSearchFieldCell.searchTextRectForBounds_(self, bounds)
493
 
        cancelButtonBounds = NSSearchFieldCell.cancelButtonRectForBounds_(self, bounds)
494
 
        searchButtonBounds = self.searchButtonRectForBounds_(bounds)
495
 
 
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
500
 
 
501
 
        return ((x, y), (width, height))
502
 
 
503
 
NSVideoSearchField.setCellClass_(VideoSearchFieldCell)
504
 
 
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):
516
 
        return nil
517
 
    return NSImage.alloc().initByReferencingFile_(engineIconPath)
518
 
 
519
 
searchIcons = dict()
520
 
def _getSearchIcon(engine):
521
 
    if engine.name not in searchIcons:
522
 
        searchIcons[engine.name] = _makeSearchIcon(engine)
523
 
    return searchIcons[engine.name]        
524
 
 
525
 
def _makeSearchIcon(engine):
526
 
    popupRectangle = NSImage.imageNamed_(u'search_popup_triangle')
527
 
    popupRectangleSize = popupRectangle.size()
528
 
 
529
 
    engineIconPath = resources.path('images/search_icon_%s.png' % engine.name)
530
 
    if not os.path.exists(engineIconPath):
531
 
        return nil
532
 
    engineIcon = NSImage.alloc().initByReferencingFile_(engineIconPath)
533
 
    engineIconSize = engineIcon.size()
534
 
 
535
 
    searchIconSize = (engineIconSize.width + popupRectangleSize.width + 2, engineIconSize.height)
536
 
    searchIcon = NSImage.alloc().initWithSize_(searchIconSize)
537
 
    
538
 
    searchIcon.lockFocus()
539
 
    try:
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)
544
 
    finally:
545
 
        searchIcon.unlockFocus()
546
 
 
547
 
    return searchIcon