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.
31
from objc import nil, YES, NO, IBOutlet
33
from Foundation import *
34
from PyObjCTools import AppHelper
37
from miro import messages
38
from miro.gtcache import gettext as _
39
from miro.frontends.widgets.widgetconst import MAX_VOLUME
40
from miro.plat import resources
41
from miro.plat.frontends.widgets import threads
42
from miro.plat.frontends.widgets import drawing
43
from miro.plat.frontends.widgets import osxmenus
45
###############################################################################
47
class OverlayPaletteWindow (NSWindow):
49
def initWithContentRect_styleMask_backing_defer_(self, rect, style, backing, defer):
50
self = super(OverlayPaletteWindow, self).initWithContentRect_styleMask_backing_defer_(
52
NSBorderlessWindowMask,
55
self.setBackgroundColor_(NSColor.clearColor())
56
self.setAlphaValue_(1.0)
60
def canBecomeKeyWindow(self):
63
def canBecomeMainWindow(self):
66
###############################################################################
70
class OverlayPalette (NSWindowController):
72
titleLabel = IBOutlet('titleLabel')
73
feedLabel = IBOutlet('feedLabel')
74
shareButton = IBOutlet('shareButton')
75
keepButton = IBOutlet('keepButton')
76
deleteButton = IBOutlet('deleteButton')
77
fsButton = IBOutlet('fsButton')
78
popInOutButton = IBOutlet('popInOutButton')
79
popInOutLabel = IBOutlet('popInOutLabel')
81
playbackControls = IBOutlet('playbackControls')
82
playPauseButton = IBOutlet('playPauseButton')
83
progressSlider = IBOutlet('progressSlider')
84
seekBackwardButton = IBOutlet('seekBackwardButton')
85
skipBackwardButton = IBOutlet('skipBackwardButton')
86
seekForwardButton = IBOutlet('seekForwardButton')
87
skipForwardButton = IBOutlet('skipForwardButton')
88
timeIndicator = IBOutlet('timeIndicator')
89
volumeSlider = IBOutlet('volumeSlider')
94
def get_instance(cls):
97
overlay = OverlayPalette.alloc().init()
101
self = super(OverlayPalette, self).initWithWindowNibName_owner_('OverlayPalette', self)
102
self.item_info = None
103
self.autoHidingTimer = nil
104
self.updateTimer = nil
105
self.holdStartTime = 0.0
107
self.wasPlaying = False
108
self.revealing = False
111
self.in_fullscreen = False
113
app.playback_manager.connect('will-play', self.video_will_play)
114
app.playback_manager.connect('will-pause', self.video_will_pause)
118
def awakeFromNib(self):
119
self.shareButton.setImage_(getOverlayButtonImage(self.shareButton.bounds().size))
120
self.shareButton.setAlternateImage_(getOverlayButtonAlternateImage(self.shareButton.bounds().size))
121
self.shareButton.setTitle_(_("Share"))
123
self.keepButton.setImage_(getOverlayButtonImage(self.keepButton.bounds().size))
124
self.keepButton.setAlternateImage_(getOverlayButtonAlternateImage(self.keepButton.bounds().size))
125
self.keepButton.setTitle_(_("Keep"))
127
self.deleteButton.setImage_(getOverlayButtonImage(self.deleteButton.bounds().size))
128
self.deleteButton.setAlternateImage_(getOverlayButtonAlternateImage(self.deleteButton.bounds().size))
129
self.deleteButton.setTitle_(_("Delete"))
131
self.seekForwardButton.setCell_(SkipSeekButtonCell.cellFromButtonCell_direction_delay_(self.seekForwardButton.cell(), 1, 0.0))
132
self.seekForwardButton.cell().setAllowsSkipping(False)
133
self.seekBackwardButton.setCell_(SkipSeekButtonCell.cellFromButtonCell_direction_delay_(self.seekBackwardButton.cell(), -1, 0.0))
134
self.seekBackwardButton.cell().setAllowsSkipping(False)
136
self.progressSlider.cursor = NSImage.imageNamed_(u'fs-progress-slider')
137
self.progressSlider.sliderWasClicked = self.progressSliderWasClicked
138
self.progressSlider.sliderWasDragged = self.progressSliderWasDragged
139
self.progressSlider.sliderWasReleased = self.progressSliderWasReleased
140
self.progressSlider.setShowCursor_(True)
142
self.volumeSlider.cursor = NSImage.imageNamed_(u'fs-volume-slider')
143
self.volumeSlider.sliderWasClicked = self.volumeSliderWasClicked
144
self.volumeSlider.sliderWasDragged = self.volumeSliderWasDragged
145
self.volumeSlider.setShowCursor_(True)
147
def setup(self, item_info, renderer, video_window):
148
from miro.frontends.widgets import widgetutil
149
self.item_info = item_info
150
self.renderer = renderer
151
self.titleLabel.setStringValue_(item_info.name)
153
self.feedLabel.setStringValue_(widgetutil.get_feed_info(item_info.feed_id).name)
155
self.feedLabel.setStringValue_("")
156
self.keepButton.setEnabled_(item_info.can_be_saved)
157
self.shareButton.setEnabled_(item_info.has_sharable_url)
158
self.adjustContent(video_window, False)
160
self.suspendAutoHiding()
161
self.reveal(video_window)
163
def on_items_changed(self, changed):
164
for item_info in changed:
165
if item_info.id == self.item_info.id:
166
self.keepButton.setEnabled_(item_info.can_be_saved)
167
self.shareButton.setEnabled_(item_info.has_sharable_url)
170
def enter_fullscreen(self, videoWindow):
171
self.in_fullscreen = True
172
if self.window().isVisible():
173
self.adjustContent(videoWindow, True)
175
NSCursor.setHiddenUntilMouseMoves_(YES)
177
def exit_fullscreen(self, videoWindow):
178
self.in_fullscreen = False
179
if self.window().isVisible():
180
self.adjustContent(videoWindow, True)
182
def showSubtitlesMenu_(self, sender):
183
menu = NSMenu.alloc().init()
184
menu.setAutoenablesItems_(NO)
185
subtitles_tracks = app.playback_manager.player.get_subtitle_tracks()
186
osxmenus.populate_subtitles_menu(menu, subtitles_tracks)
187
NSMenu.popUpContextMenu_withEvent_forView_(menu, NSApp().currentEvent(), self.window().contentView())
189
def getHorizontalPosition(self, videoWindow, width):
190
parentFrame = videoWindow.frame()
191
return parentFrame.origin.x + ((parentFrame.size.width - width) / 2.0)
193
def adjustPosition(self, videoWindow):
194
parentFrame = videoWindow.frame()
195
x = self.getHorizontalPosition(videoWindow, self.window().frame().size.width)
196
y = parentFrame.origin.y + 60
197
self.window().setFrameOrigin_(NSPoint(x, y))
199
def adjustContent(self, videoWindow, animate):
200
if videoWindow.is_fullscreen:
201
self.popInOutButton.setHidden_(YES)
202
self.popInOutLabel.setHidden_(YES)
203
self.fsButton.setImage_(NSImage.imageNamed_('fs-button-exitfullscreen'))
204
self.fsButton.setAlternateImage_(NSImage.imageNamed_('fs-button-exitfullscreen-alt'))
206
if app.playback_manager.detached_window is None:
207
image_path = resources.path('images/popout.png')
210
image_path = resources.path('images/popin.png')
212
self.popInOutButton.setImage_(NSImage.alloc().initWithContentsOfFile_(image_path))
213
self.popInOutButton.setHidden_(NO)
214
self.popInOutLabel.setHidden_(NO)
215
self.popInOutLabel.setStringValue_(label)
216
self.fsButton.setImage_(NSImage.imageNamed_('fs-button-enterfullscreen'))
217
self.fsButton.setAlternateImage_(NSImage.imageNamed_('fs-button-enterfullscreen-alt'))
219
newFrame = self.window().frame()
220
if videoWindow.is_fullscreen or app.playback_manager.detached_window is not None:
221
self.titleLabel.setHidden_(NO)
222
self.feedLabel.setHidden_(NO)
223
newFrame.size.height = 198
225
self.titleLabel.setHidden_(YES)
226
self.feedLabel.setHidden_(YES)
227
newFrame.size.height = 144
228
newFrame.origin.x = self.getHorizontalPosition(videoWindow, newFrame.size.width)
229
self.window().setFrame_display_animate_(newFrame, YES, animate)
230
self.playbackControls.setNeedsDisplay_(YES)
232
def fit_in_video_window(self, video_window):
233
return self.window().frame().size.width <= video_window.frame().size.width
235
def reveal(self, videoWindow):
236
threads.warn_if_not_on_main_thread('OverlayPalette.reveal')
237
self.resetAutoHiding()
238
if (not self.window().isVisible() and not self.revealing) or (self.window().isVisible() and self.hiding):
240
if self.renderer.movie is not None:
241
self.set_volume(self.renderer.movie.volume())
243
self.adjustPosition(videoWindow)
244
self.adjustContent(videoWindow, False)
246
if self.hiding and self.anim is not None:
247
self.anim.stopAnimation()
250
self.window().setAlphaValue_(0.0)
252
self.window().orderFront_(nil)
253
videoWindow.parentWindow().addChildWindow_ordered_(self.window(), NSWindowAbove)
255
self.revealing = True
256
params = {NSViewAnimationTargetKey: self.window(), NSViewAnimationEffectKey: NSViewAnimationFadeInEffect}
257
self.animate(params, 0.3)
259
self.resumeAutoHiding()
262
threads.warn_if_not_on_main_thread('OverlayPalette.hide')
264
if self.autoHidingTimer is not nil:
265
self.autoHidingTimer.invalidate()
266
self.autoHidingTimer = nil
268
if self.revealing and self.anim is not None:
269
self.anim.stopAnimation()
270
self.revealing = False
273
params = {NSViewAnimationTargetKey: self.window(), NSViewAnimationEffectKey: NSViewAnimationFadeOutEffect}
274
self.animate(params, 0.5)
276
def hideAfterDelay_(self, timer):
277
if time.time() - self.holdStartTime > self.HOLD_TIME:
280
def resetAutoHiding(self):
281
self.holdStartTime = time.time()
283
def suspendAutoHiding(self):
284
if self.autoHidingTimer is not nil:
285
self.autoHidingTimer.invalidate()
286
self.autoHidingTimer = nil
288
def resumeAutoHiding(self):
289
self.resetAutoHiding()
290
if self.autoHidingTimer is None:
291
self.autoHidingTimer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
292
1.0, self, 'hideAfterDelay:', nil, YES)
294
def animate(self, params, duration):
295
self.anim = NSViewAnimation.alloc().initWithDuration_animationCurve_(duration, 0)
296
self.anim.setViewAnimations_(NSArray.arrayWithObject_(params))
297
self.anim.setDelegate_(self)
298
self.anim.startAnimation()
300
def animationDidEnd_(self, anim):
301
parent = self.window().parentWindow()
303
self.resumeAutoHiding()
304
self.updateTimer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
305
0.5, self, 'update:', nil, YES)
306
NSRunLoop.currentRunLoop().addTimer_forMode_(self.updateTimer, NSEventTrackingRunLoopMode)
308
if parent is not None and self.in_fullscreen:
309
NSCursor.setHiddenUntilMouseMoves_(NO)
311
if parent is not None and self.in_fullscreen:
312
NSCursor.setHiddenUntilMouseMoves_(YES)
316
self.revealing = False
319
def set_volume(self, volume):
320
self.volumeSlider.setFloatValue_(volume / MAX_VOLUME)
322
def keep_(self, sender):
323
messages.KeepVideo(self.item_info.id).send_to_backend()
325
def expireNow_(self, sender):
326
item_info = self.item_info
327
app.playback_manager.on_movie_finished()
328
app.widgetapp.remove_items([item_info])
330
def share_(self, sender):
331
item_info = self.item_info
332
app.widgetapp.share_item(item_info)
334
def handleShareItem_(self, sender):
337
def toggleFullScreen_(self, sender):
338
app.playback_manager.toggle_fullscreen()
340
def toggleAttachedDetached_(self, sender):
341
app.playback_manager.toggle_detached_mode()
343
def skipBackward_(self, sender):
344
app.playback_manager.play_prev_movie()
346
def fastBackward_(self, sender):
349
def skipForward_(self, sender):
350
app.playback_manager.play_next_movie()
352
def fastForward_(self, sender):
355
def fastSeek(self, direction):
357
app.playback_manager.set_playback_rate(rate)
358
self.suspendAutoHiding()
360
def stopSeeking(self):
362
if app.playback_manager.is_paused:
364
app.playback_manager.set_playback_rate(rate)
365
self.resumeAutoHiding()
367
def stop_(self, sender):
369
app.playback_manager.stop()
371
def playPause_(self, sender):
372
app.playback_manager.play_pause()
374
def update_(self, timer):
375
if self.renderer.movie is None:
377
elapsed = self.renderer.get_elapsed_playback_time()
378
total = self.renderer.get_total_playback_time()
379
progress = u"%d:%02d" % divmod(int(round(elapsed)), 60)
380
self.timeIndicator.setStringValue_(progress)
381
self.progressSlider.setFloatValue_(elapsed / total)
383
def progressSliderWasClicked(self, slider):
384
if app.playback_manager.is_playing:
385
self.wasPlaying = True
386
self.renderer.pause()
387
app.playback_manager.seek_to(slider.floatValue())
388
self.resetAutoHiding()
390
def progressSliderWasDragged(self, slider):
391
app.playback_manager.seek_to(slider.floatValue())
392
self.resetAutoHiding()
394
def progressSliderWasReleased(self, slider):
396
self.wasPlaying = False
399
def volumeSliderWasDragged(self, slider):
400
volume = slider.floatValue() * MAX_VOLUME
401
app.playback_manager.set_volume(volume)
402
app.widgetapp.window.videobox.volume_slider.set_value(volume)
403
self.resetAutoHiding()
405
def volumeSliderWasClicked(self, slider):
406
self.volumeSliderWasDragged(slider)
408
def video_will_play(self, obj, duration):
409
self.playPauseButton.setImage_(NSImage.imageNamed_(u'fs-button-pause'))
410
self.playPauseButton.setAlternateImage_(NSImage.imageNamed_(u'fs-button-pause-alt'))
412
def video_will_pause(self, obj):
413
self.playPauseButton.setImage_(NSImage.imageNamed_(u'fs-button-play'))
414
self.playPauseButton.setAlternateImage_(NSImage.imageNamed_(u'fs-button-play-alt'))
417
threads.warn_if_not_on_main_thread('OverlayPalette.remove')
418
self.suspendAutoHiding()
419
if self.updateTimer is not nil:
420
self.updateTimer.invalidate()
421
self.updateTimer = nil
422
if self.window().parentWindow() is not nil:
423
self.window().parentWindow().removeChildWindow_(self.window())
424
self.window().orderOut_(nil)
426
###############################################################################
428
class OverlayPaletteView (NSView):
430
def drawRect_(self, rect):
433
rect = NSInsetRect(self.frame(), radius+lineWidth, radius+lineWidth)
435
path = NSBezierPath.bezierPath()
436
path.appendBezierPathWithArcWithCenter_radius_startAngle_endAngle_(NSPoint(NSMinX(rect), NSMinY(rect)), radius, 180.0, 270.0)
437
path.appendBezierPathWithArcWithCenter_radius_startAngle_endAngle_(NSPoint(NSMaxX(rect), NSMinY(rect)), radius, 270.0, 360.0)
438
path.appendBezierPathWithArcWithCenter_radius_startAngle_endAngle_(NSPoint(NSMaxX(rect), NSMaxY(rect)), radius, 0.0, 90.0)
439
path.appendBezierPathWithArcWithCenter_radius_startAngle_endAngle_(NSPoint(NSMinX(rect), NSMaxY(rect)), radius, 90.0, 180.0)
442
transform = NSAffineTransform.transform()
443
transform.translateXBy_yBy_(0.5, 0.5)
444
path.transformUsingAffineTransform_(transform)
446
NSColor.colorWithDeviceWhite_alpha_(0.0, 0.6).set()
449
NSColor.colorWithDeviceWhite_alpha_(1.0, 0.6).set()
450
path.setLineWidth_(lineWidth)
453
###############################################################################
455
class OverlayPaletteControlsView (NSView):
457
def drawRect_(self, rect):
458
bounds = self.bounds()
459
NSColor.colorWithDeviceWhite_alpha_(1.0, 0.6).set()
461
path = NSBezierPath.bezierPath()
462
path.moveToPoint_(NSPoint(0.5, 0.5))
463
path.relativeLineToPoint_(NSPoint(bounds.size.width, 0))
464
path.setLineWidth_(2)
467
if app.playback_manager.is_fullscreen or app.playback_manager.detached_window is not None:
468
path = NSBezierPath.bezierPath()
469
path.moveToPoint_(NSPoint(0.5, bounds.size.height-1.5))
470
path.relativeLineToPoint_(NSPoint(bounds.size.width, 0))
471
path.setLineWidth_(2)
474
def hitTest_(self, point):
475
# Our buttons have transparent parts, but we still want mouse clicks
476
# to be detected if they happen there, so we override hit testing and
477
# simply test for button frames.
478
for subview in self.subviews():
479
if NSPointInRect(self.convertPoint_fromView_(point, nil), subview.frame()):
483
###############################################################################
485
class Slider (NSView):
487
def initWithFrame_(self, frame):
488
self = super(Slider, self).initWithFrame_(frame)
490
self.showCursor = False
491
self.dragging = False
492
self.sliderWasClicked = None
493
self.sliderWasDragged = None
494
self.sliderWasReleased = None
497
def setFloatValue_(self, value):
498
threads.warn_if_not_on_main_thread('Slider.setFloatValue_')
500
self.setNeedsDisplay_(YES)
502
def floatValue(self):
505
def setShowCursor_(self, showCursor):
506
self.showCursor = showCursor
508
def drawRect_(self, rect):
516
def drawCursor(self):
517
x = self.getCursorPosition()
518
self.cursor.compositeToPoint_operation_((abs(x)+0.5, 0), NSCompositeSourceOver)
520
def getCursorPosition(self):
521
return (self.bounds().size.width - self.cursor.size().width) * self.value
523
def mouseDown_(self, event):
525
location = self.convertPoint_fromView_(event.locationInWindow(), nil)
526
if NSPointInRect(location, self.bounds()):
528
self.setFloatValue_(self.getValueForClickLocation(location))
529
if self.sliderWasClicked is not None:
530
self.sliderWasClicked(self)
532
def mouseDragged_(self, event):
533
if self.showCursor and self.dragging:
534
location = self.convertPoint_fromView_(event.locationInWindow(), nil)
535
self.setFloatValue_(self.getValueForClickLocation(location))
536
if self.sliderWasDragged is not None:
537
self.sliderWasDragged(self)
539
def mouseUp_(self, event):
541
self.dragging = False
542
if self.sliderWasReleased is not None:
543
self.sliderWasReleased(self)
544
self.setNeedsDisplay_(YES)
546
def getValueForClickLocation(self, location):
547
min = self.cursor.size().width / 2.0
548
max = self.bounds().size.width - min
555
return (offset - min) / span
557
class OverlayPaletteSlider (Slider):
560
from miro.frontends.widgets import widgetutil
562
ctx = drawing.DrawingContext(self, rect, rect)
563
ctx.set_color((1,1,1), 0.4)
564
widgetutil.circular_rect(ctx, 0, 2, rect.size.width, rect.size.height - 4)
567
###############################################################################
569
class SkipSeekButtonCell (NSButtonCell):
572
def cellFromButtonCell_direction_delay_(self, cell, direction, delay):
573
newCell = SkipSeekButtonCell.alloc().initWithPrimaryAction_direction_delay_(cell.action(), direction, delay)
574
newCell.setType_(cell.type())
575
newCell.setBezeled_(cell.isBezeled())
576
newCell.setBezelStyle_(cell.bezelStyle())
577
newCell.setBordered_(cell.isBordered())
578
newCell.setTransparent_(cell.isTransparent())
579
newCell.setImage_(cell.image())
580
newCell.setAlternateImage_(cell.alternateImage())
581
newCell.setState_(cell.state())
582
newCell.setHighlightsBy_(cell.highlightsBy())
583
newCell.setShowsStateBy_(cell.showsStateBy())
584
newCell.setEnabled_(cell.isEnabled())
585
newCell.setTarget_(cell.target())
586
newCell.setAction_(nil)
589
def initWithPrimaryAction_direction_delay_(self, action, direction, delay):
590
self = super(SkipSeekButtonCell, self).init()
591
self.primaryAction = action
592
self.direction = direction
594
self.seekDelay = delay
595
self.allowSkipping = True
596
self.allowSeeking = True
599
def setAllowsFastSeeking(self, allow):
600
self.allowSeeking = allow
602
def setAllowsSkipping(self, allow):
603
self.allowSkipping = allow
605
def trackMouse_inRect_ofView_untilMouseUp_(self, event, frame, control, untilMouseUp):
606
if self.allowSeeking:
607
if self.seekDelay > 0.0:
608
self.seekTimer = NSTimer.timerWithTimeInterval_target_selector_userInfo_repeats_(self.seekDelay, self, 'fastSeek:', nil, NO)
609
NSRunLoop.currentRunLoop().addTimer_forMode_(self.seekTimer, NSEventTrackingRunLoopMode)
613
mouseIsUp = NSButtonCell.trackMouse_inRect_ofView_untilMouseUp_(self, event, frame, control, YES)
615
if self.seekTimer is not nil or not self.allowSeeking:
616
self.resetSeekTimer()
617
control.sendAction_to_(self.primaryAction, self.target())
619
self.target().stopSeeking()
623
def fastSeek_(self, timer):
624
self.target().fastSeek(self.direction)
625
self.resetSeekTimer()
627
def resetSeekTimer(self):
628
if self.seekTimer is not nil:
629
self.seekTimer.invalidate()
632
###############################################################################
634
def getOverlayButtonImage(size):
635
fillColor = NSColor.colorWithDeviceWhite_alpha_(190.0/255.0, 0.8)
636
strokeColor = NSColor.colorWithDeviceWhite_alpha_(76.0/255.0, 0.8)
637
return makeOverlayButtonImage(size, fillColor, strokeColor)
639
def getOverlayButtonAlternateImage(size):
640
fillColor = NSColor.colorWithDeviceWhite_alpha_(220.0/255.0, 0.8)
641
strokeColor = NSColor.colorWithDeviceWhite_alpha_(106.0/255.0, 0.8)
642
return makeOverlayButtonImage(size, fillColor, strokeColor)
644
def makeOverlayButtonImage(size, fillColor, strokeColor):
645
radius = (size.height-1) / 2.0
646
path = NSBezierPath.bezierPath()
647
path.appendBezierPathWithArcWithCenter_radius_startAngle_endAngle_(NSPoint(radius+1.5, radius+0.5), radius, 90.0, 270.0)
648
path.appendBezierPathWithArcWithCenter_radius_startAngle_endAngle_(NSPoint(size.width - radius - 1.5, radius+0.5), radius, 270.0, 90.0)
651
image = NSImage.alloc().initWithSize_(size)