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.window -- Top-level Window class. """
32
from Foundation import *
33
from objc import YES, NO, nil
34
from PyObjCTools import AppHelper
36
from miro import signals
37
from miro.frontends.widgets import widgetconst
38
from miro.plat.frontends.widgets import osxmenus
39
from miro.plat.frontends.widgets import wrappermap
40
from miro.plat.frontends.widgets.helpers import NotificationForwarder
41
from miro.plat.frontends.widgets.base import Widget, Container, FlippedView
42
from miro.plat.frontends.widgets.layout import VBox, HBox, Alignment
43
from miro.plat.frontends.widgets.control import Button
44
from miro.plat.frontends.widgets.simple import Label
45
from miro.plat.frontends.widgets.rect import Rect, NSRectWrapper
46
from miro.plat.utils import filename_to_unicode
48
# Tracks all windows that haven't been destroyed. This makes sure there
49
# object stay alive as long as the window is alive.
52
class MiroWindow(NSWindow):
53
def handle_keyDown(self, event):
54
key = event.charactersIgnoringModifiers()
55
if len(key) != 1 or not key.isalpha():
56
key = osxmenus.REVERSE_KEYS_MAP.get(key)
57
mods = osxmenus.translate_event_modifiers(event)
58
responder = self.firstResponder()
59
while responder is not None:
60
wrapper = wrappermap.wrapper(responder)
61
if isinstance(wrapper, Widget) or isinstance(wrapper, Window):
62
if wrapper.emit('key-press', key, mods):
63
return True # signal handler returned True, stop processing
64
responder = responder.nextResponder()
66
def sendEvent_(self, event):
67
if event.type() == NSKeyDown:
68
# We grab the KeyDown event here instead of with a keyDown handler
69
# because some keys (notable Cmd-> and Cmd-< aren't caught by that
71
if self.handle_keyDown(event):
73
return NSWindow.sendEvent_(self, event)
75
class Window(signals.SignalEmitter):
76
"""See https://develop.participatoryculture.org/index.php/WidgetAPI for a description of the API for this class."""
77
def __init__(self, title, rect):
78
signals.SignalEmitter.__init__(self)
79
self.create_signal('active-change')
80
self.create_signal('will-close')
81
self.create_signal('did-move')
82
self.create_signal('key-press')
83
self.create_signal('show')
84
self.create_signal('hide')
85
self.nswindow = MiroWindow.alloc().initWithContentRect_styleMask_backing_defer_(
87
self.get_style_mask(),
88
NSBackingStoreBuffered,
90
self.nswindow.setTitle_(title)
91
self.nswindow.setMinSize_(NSSize(800, 600))
92
self.nswindow.setReleasedWhenClosed_(NO)
93
self.content_view = FlippedView.alloc().initWithFrame_(rect.nsrect)
94
self.content_view.setAutoresizesSubviews_(NO)
95
self.nswindow.setContentView_(self.content_view)
96
self.content_widget = None
97
self.view_notifications = NotificationForwarder.create(self.content_view)
98
self.view_notifications.connect(self.on_frame_change, 'NSViewFrameDidChangeNotification')
99
self.window_notifications = NotificationForwarder.create(self.nswindow)
100
self.window_notifications.connect(self.on_activate, 'NSWindowDidBecomeMainNotification')
101
self.window_notifications.connect(self.on_deactivate, 'NSWindowDidResignMainNotification')
102
self.window_notifications.connect(self.on_did_move, 'NSWindowDidMoveNotification')
103
self.window_notifications.connect(self.on_will_close, 'NSWindowWillCloseNotification')
104
wrappermap.add(self.nswindow, self)
105
alive_windows.add(self)
107
def get_style_mask(self):
108
return NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask | NSResizableWindowMask
110
def set_title(self, title):
111
self.nswindow.setTitle_(title)
113
def on_frame_change(self, notification):
116
def on_activate(self, notification):
117
self.emit('active-change')
119
def on_deactivate(self, notification):
120
self.emit('active-change')
122
def on_did_move(self, notification):
123
self.emit('did-move')
125
def on_will_close(self, notification):
126
self.emit('will-close')
130
return self.nswindow.isMainWindow()
133
if self not in alive_windows:
134
raise ValueError("Window destroyed")
135
self.nswindow.makeKeyAndOrderFront_(nil)
136
self.nswindow.makeMainWindow()
140
self.nswindow.close()
143
self.window_notifications.disconnect()
144
self.view_notifications.disconnect()
145
self.nswindow.setContentView_(nil)
146
wrappermap.remove(self.nswindow)
147
alive_windows.discard(self)
150
def place_child(self):
151
rect = self.nswindow.contentRectForFrameRect_(self.nswindow.frame())
152
self.content_widget.place(NSRect(NSPoint(0, 0), rect.size),
155
def hookup_content_widget_signals(self):
156
self.size_req_handler = self.content_widget.connect('size-request-changed',
157
self.on_content_widget_size_request_change)
159
def unhook_content_widget_signals(self):
160
self.content_widget.disconnect(self.size_req_handler)
161
self.size_req_handler = None
163
def on_content_widget_size_request_change(self, widget, old_size):
164
self.update_size_constraints()
166
def set_content_widget(self, widget):
167
if self.content_widget:
168
self.content_widget.remove_viewport()
169
self.unhook_content_widget_signals()
170
self.content_widget = widget
171
self.hookup_content_widget_signals()
173
self.update_size_constraints()
175
def update_size_constraints(self):
176
width, height = self.content_widget.get_size_request()
177
self.nswindow.setContentMinSize_(NSSize(width, height))
179
def get_content_widget(self):
180
return self.content_widget
183
frame = self.nswindow.frame()
184
frame.size.height -= 22
185
return NSRectWrapper(frame)
187
def connect_menu_keyboard_shortcuts(self):
188
# All OS X windows are connected to the menu shortcuts
191
class MainWindow(Window):
192
def __init__(self, title, rect):
193
Window.__init__(self, title, rect)
194
self.nswindow.setReleasedWhenClosed_(NO)
196
self.nswindow.orderOut_(nil)
198
class DialogBase(object):
200
self.sheet_parent = None
201
def set_transient_for(self, window):
202
self.sheet_parent = window
204
class Dialog(DialogBase):
205
def __init__(self, title, description=None):
206
DialogBase.__init__(self)
208
self.description = description
210
self.extra_widget = None
214
def add_button(self, text):
215
button = Button(text)
216
button.set_size(widgetconst.SIZE_NORMAL)
217
button.connect('clicked', self.on_button_clicked, len(self.buttons))
218
self.buttons.append(button)
220
def on_button_clicked(self, button, code):
221
if self.sheet_parent is not None:
222
NSApp().endSheet_returnCode_(self.window, code)
224
NSApp().stopModalWithCode_(code)
226
def build_text(self):
227
vbox = VBox(spacing=6)
229
description_label = Label(self.description, wrap=True)
230
description_label.set_bold(True)
231
description_label.set_size_request(360, -1)
232
vbox.pack_start(description_label)
235
def build_buttons(self):
236
hbox = HBox(spacing=12)
237
for button in reversed(self.buttons):
238
hbox.pack_start(button)
239
alignment = Alignment(xalign=1.0, yscale=1.0)
243
def build_content(self):
244
vbox = VBox(spacing=12)
245
vbox.pack_start(self.build_text())
246
if self.extra_widget:
247
vbox.pack_start(self.extra_widget)
248
vbox.pack_start(self.build_buttons())
249
alignment = Alignment(xscale=1.0, yscale=1.0)
250
alignment.set_padding(12, 12, 17, 17)
254
def build_window(self):
255
self.content_widget = self.build_content()
256
width, height = self.content_widget.get_size_request()
257
width = max(width, 400)
258
window = NSPanel.alloc()
259
window.initWithContentRect_styleMask_backing_defer_(
260
NSMakeRect(400, 400, width, height),
261
NSTitledWindowMask, NSBackingStoreBuffered, NO)
262
view = FlippedView.alloc().initWithFrame_(NSMakeRect(0, 0, width,
264
window.setContentView_(view)
265
window.setTitle_(self.title)
266
self.content_widget.place(view.frame(), view)
268
self.buttons[0].make_default()
272
self.window = self.build_window()
274
if self.sheet_parent is None:
275
response = NSApp().runModalForWindow_(self.window)
277
delegate = SheetDelegate.alloc().init()
278
NSApp().beginSheet_modalForWindow_modalDelegate_didEndSelector_contextInfo_(
279
self.window, self.sheet_parent.nswindow,
280
delegate, 'sheetDidEnd:returnCode:contextInfo:', 0)
281
response = NSApp().runModalForWindow_(self.window)
283
# self.window won't be around if we call destroy() to cancel
285
self.window.orderOut_(nil)
294
NSApp().stopModalWithCode_(-1)
296
self.window.setContentView_(None)
300
self.extra_widget = None
302
def set_extra_widget(self, widget):
303
self.extra_widget = widget
305
def get_extra_widget(self):
306
return self.extra_widget
308
class SheetDelegate(NSObject):
309
@AppHelper.endSheetMethod
310
def sheetDidEnd_returnCode_contextInfo_(self, sheet, return_code, info):
311
NSApp().stopModalWithCode_(return_code)
313
class FileDialogBase(DialogBase):
315
DialogBase.__init__(self)
317
self._filename = None
318
self._directory = None
319
self._filter_on_run = True
322
self._panel.setAllowedFileTypes_(self._types)
323
if self.sheet_parent is None:
324
if self._filter_on_run:
325
response = self._panel.runModalForDirectory_file_types_(self._directory, self._filename, self._types)
327
response = self._panel.runModalForDirectory_file_(self._directory, self._filename)
329
delegate = SheetDelegate.alloc().init()
330
if self._filter_on_run:
331
self._panel.beginSheetForDirectory_file_types_modalForWindow_modalDelegate_didEndSelector_contextInfo_(
332
self._directory, self._filename, self._types,
333
self.sheet_parent.nswindow, delegate, 'sheetDidEnd:returnCode:contextInfo:', 0)
335
self._panel.beginSheetForDirectory_file_modalForWindow_modalDelegate_didEndSelector_contextInfo_(
336
self._directory, self._filename,
337
self.sheet_parent.nswindow, delegate, 'sheetDidEnd:returnCode:contextInfo:', 0)
338
response = NSApp().runModalForWindow_(self._panel)
339
self._panel.orderOut_(nil)
342
class FileSaveDialog(FileDialogBase):
343
def __init__(self, title):
344
FileDialogBase.__init__(self)
346
self._panel = NSSavePanel.savePanel()
347
self._panel.setCanChooseFiles_(YES)
348
self._panel.setCanChooseDirectories_(NO)
349
self._filename = None
350
self._filter_on_run = False
352
def set_filename(self, s):
353
self._filename = filename_to_unicode(s)
355
def get_filename(self):
356
# Use encode('utf-8') instead of unicode_to_filename, because
357
# unicode_to_filename has code to make sure nextFilename works, but it's
358
# more important here to not change the filename.
359
return self._filename.encode('utf-8')
362
response = FileDialogBase.run(self)
363
if response == NSFileHandlingPanelOKButton:
364
self._filename = self._panel.filename()
371
class FileOpenDialog(FileDialogBase):
372
def __init__(self, title):
373
FileDialogBase.__init__(self)
375
self._panel = NSOpenPanel.openPanel()
376
self._panel.setCanChooseFiles_(YES)
377
self._panel.setCanChooseDirectories_(NO)
378
self._filenames = None
380
def set_select_multiple(self, value):
382
self._panel.setAllowsMultipleSelection_(YES)
384
self._panel.setAllowsMultipleSelection_(NO)
386
def set_directory(self, d):
387
self._directory = filename_to_unicode(d)
389
def set_filename(self, s):
390
self._filename = filename_to_unicode(s)
392
def add_filters(self, filters):
397
def get_filename(self):
398
return self.get_filenames()[0]
400
def get_filenames(self):
401
# Use encode('utf-8') instead of unicode_to_filename, because
402
# unicode_to_filename has code to make sure nextFilename works, but it's
403
# more important here to not change the filename.
404
return [f.encode('utf-8') for f in self._filenames]
407
response = FileDialogBase.run(self)
408
if response == NSFileHandlingPanelOKButton:
409
self._filenames = self._panel.filenames()
412
self._filenames = None
417
class DirectorySelectDialog(FileDialogBase):
418
def __init__(self, title):
419
FileDialogBase.__init__(self)
421
self._panel = NSOpenPanel.openPanel()
422
self._panel.setCanChooseFiles_(NO)
423
self._panel.setCanChooseDirectories_(YES)
424
self._directory = None
426
def set_directory(self, d):
427
self._directory = filename_to_unicode(d)
429
def get_directory(self):
430
# Use encode('utf-8') instead of unicode_to_filename, because
431
# unicode_to_filename has code to make sure nextFilename
432
# works, but it's more important here to not change the
434
return self._directory.encode('utf-8')
437
response = FileDialogBase.run(self)
438
if response == NSFileHandlingPanelOKButton:
439
self._directory = self._panel.filenames()[0]
446
class AboutDialog(DialogBase):
448
NSApplication.sharedApplication().orderFrontStandardAboutPanel_(nil)
452
class AlertDialog(DialogBase):
453
def __init__(self, title, message, alert_type):
454
DialogBase.__init__(self)
455
self._nsalert = NSAlert.alloc().init();
456
self._nsalert.setMessageText_(title)
457
self._nsalert.setInformativeText_(message)
458
self._nsalert.setAlertStyle_(alert_type)
459
def add_button(self, text):
460
self._nsalert.addButtonWithTitle_(text)
462
self._nsalert.runModal()
466
class PreferenceItem (NSToolbarItem):
468
def setPanel_(self, panel):
471
class ToolbarDelegate (NSObject):
473
def initWithPanels_identifiers_window_(self, panels, identifiers, window):
474
self = super(ToolbarDelegate, self).init()
476
self.identifiers = identifiers
480
def toolbarAllowedItemIdentifiers_(self, toolbar):
481
return self.identifiers
483
def toolbarDefaultItemIdentifiers_(self, toolbar):
484
return self.identifiers
486
def toolbarSelectableItemIdentifiers_(self, toolbar):
487
return self.identifiers
489
def toolbar_itemForItemIdentifier_willBeInsertedIntoToolbar_(self, toolbar, itemIdentifier, flag):
490
panel = self.panels[itemIdentifier]
491
item = PreferenceItem.alloc().initWithItemIdentifier_(itemIdentifier)
492
item.setLabel_(panel[1])
493
item.setImage_(NSImage.imageNamed_(u"pref_item_%s" % itemIdentifier))
494
item.setAction_("switchPreferenceView:")
495
item.setTarget_(self)
496
item.setPanel_(panel[0])
499
def validateToolbarItem_(self, item):
502
def switchPreferenceView_(self, sender):
503
self.window.do_select_panel(sender.panel, YES)
505
class PreferencesWindow (Window):
506
def __init__(self, title):
507
Window.__init__(self, title, Rect(0, 0, 640, 440))
509
self.identifiers = list()
510
self.first_show = True
511
self.nswindow.setShowsToolbarButton_(NO)
512
self.nswindow.setReleasedWhenClosed_(NO)
513
self.app_notifications = NotificationForwarder.create(NSApp())
514
self.app_notifications.connect(self.on_app_quit,
515
'NSApplicationWillTerminateNotification')
518
super(PreferencesWindow, self).destroy()
519
self.app_notifications.disconnect()
521
def get_style_mask(self):
522
return NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask
524
def append_panel(self, name, panel, title, image_name):
525
self.panels[name] = (panel, title)
526
self.identifiers.append(name)
528
def finish_panels(self):
529
self.tbdelegate = ToolbarDelegate.alloc().initWithPanels_identifiers_window_(self.panels, self.identifiers, self)
530
toolbar = NSToolbar.alloc().initWithIdentifier_(u"Preferences")
531
toolbar.setAllowsUserCustomization_(NO)
532
toolbar.setDelegate_(self.tbdelegate)
534
self.nswindow.setToolbar_(toolbar)
536
def select_panel(self, index):
537
panel = self.identifiers[index]
538
self.nswindow.toolbar().setSelectedItemIdentifier_(panel)
539
self.do_select_panel(self.panels[panel][0], NO)
541
def do_select_panel(self, panel, animate):
542
wframe = self.nswindow.frame()
543
vsize = list(panel.get_size_request())
549
toolbarHeight = wframe.size.height - self.nswindow.contentView().frame().size.height
550
wframe.origin.y += wframe.size.height - vsize[1] - toolbarHeight
551
wframe.size = (vsize[0], vsize[1] + toolbarHeight)
553
self.set_content_widget(panel)
554
self.nswindow.setFrame_display_animate_(wframe, YES, animate)
558
self.nswindow.center()
559
self.first_show = False
562
def on_app_quit(self, notification):