aboutsummaryrefslogtreecommitdiffstats
path: root/lvc/ui/widgets.py
diff options
context:
space:
mode:
Diffstat (limited to 'lvc/ui/widgets.py')
-rw-r--r--lvc/ui/widgets.py1540
1 files changed, 1540 insertions, 0 deletions
diff --git a/lvc/ui/widgets.py b/lvc/ui/widgets.py
new file mode 100644
index 0000000..7849e2a
--- /dev/null
+++ b/lvc/ui/widgets.py
@@ -0,0 +1,1540 @@
+import logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+import os
+import sys
+
+try:
+ import lvc
+except ImportError:
+ lvc_path = os.path.join(os.path.dirname(__file__), '..', '..')
+ sys.path.append(lvc_path)
+ import lvc
+
+import copy
+import tempfile
+import urllib
+import urlparse
+
+from lvc.widgets import (initialize, idle_add, mainloop_start, mainloop_stop,
+ attach_menubar, reveal_file, get_conversion_directory)
+from lvc.widgets import menus
+from lvc.widgets import widgetset
+from lvc.widgets import cellpack
+from lvc.widgets import widgetconst
+from lvc.widgets import widgetutil
+from lvc.widgets import app
+
+from lvc.converter import ConverterInfo
+from lvc.video import VideoFile
+from lvc.resources import image_path
+from lvc.utils import size_string, round_even, convert_path_for_subprocess
+from lvc import openfiles
+
+BUTTON_FONT = widgetutil.font_scale_from_osx_points(15.0)
+LARGE_FONT = widgetutil.font_scale_from_osx_points(13.0)
+SMALL_FONT = widgetutil.font_scale_from_osx_points(10.0)
+
+DEFAULT_FONT="Helvetica"
+
+CONVERT_TO_FONT = "Gill Sans Light"
+CONVERT_TO_FONTSIZE = widgetutil.font_scale_from_osx_points(14.0)
+
+SETTINGS_FONT = "Gill Sans Light"
+SETTINGS_FONTSIZE = widgetutil.font_scale_from_osx_points(13.0)
+
+CONVERT_NOW_FONT = "Gill Sans Light"
+CONVERT_NOW_FONTSIZE = widgetutil.font_scale_from_osx_points(18.0)
+
+DND_FONT = "Gill Sans Light"
+DND_LARGE_FONTSIZE = widgetutil.font_scale_from_osx_points(13.0)
+DND_SMALL_FONTSIZE = widgetutil.font_scale_from_osx_points(12.0)
+
+ITEM_TITLE_FONT = "Futura Medium"
+ITEM_TITLE_FONTSIZE = widgetutil.font_scale_from_osx_points(13.0)
+
+ITEM_ICONS_FONT= "Century Gothic"
+ITEM_ICONS_FONTSIZE= widgetutil.font_scale_from_osx_points(10.0)
+
+GRADIENT_TOP = widgetutil.css_to_color('#585f63')
+GRADIENT_BOTTOM = widgetutil.css_to_color('#383d40')
+
+DRAG_AREA = widgetutil.css_to_color('#2b2e31')
+
+TEXT_DISABLED = widgetutil.css_to_color('#333333')
+TEXT_ACTIVE = widgetutil.css_to_color('#ffffff')
+TEXT_CLICKED = widgetutil.css_to_color('#cccccc')
+TEXT_INFO = widgetutil.css_to_color('#808080')
+TEXT_COLOR = widgetutil.css_to_color('#ffffff')
+TEXT_SHADOW = widgetutil.css_to_color('#000000')
+
+TABLE_WIDTH, TABLE_HEIGHT = 470, 87
+
+class CustomLabel(widgetset.Background):
+ def __init__(self, text=''):
+ widgetset.Background.__init__(self)
+ self.text = text
+ self.font = DEFAULT_FONT
+ self.font_scale = LARGE_FONT
+ self.color = TEXT_COLOR
+
+ def set_text(self, text):
+ self.text = text
+ self.invalidate_size_request()
+
+ def set_color(self, color):
+ self.color = color
+ self.queue_redraw()
+
+ def set_font(self, font, font_scale):
+ self.font = font
+ self.font_scale = font_scale
+ self.invalidate_size_request()
+
+ def textbox(self, layout_manager):
+ layout_manager.set_text_color(self.color)
+ layout_manager.set_font(self.font_scale, family=self.font)
+ font = layout_manager.set_font(self.font_scale, family=self.font)
+ return layout_manager.textbox(self.text)
+
+ def draw(self, context, layout_manager):
+ layout_manager.set_text_color(self.color)
+ layout_manager.set_font(LARGE_FONT, family=self.font)
+ textbox = self.textbox(layout_manager)
+ size = textbox.get_size()
+ textbox.draw(context, 0, (context.height - size[1]) // 2,
+ context.width, context.height)
+
+ def size_request(self, layout_manager):
+ return self.textbox(layout_manager).get_size()
+
+class WebStyleButton(widgetset.CustomButton):
+ def __init__(self):
+ super(WebStyleButton, self).__init__()
+ self.set_cursor(widgetconst.CURSOR_POINTING_HAND)
+ self.text = ''
+ self.font = DEFAULT_FONT
+ self.font_scale = LARGE_FONT
+
+ def set_text(self, text):
+ self.text = text
+ self.invalidate_size_request()
+
+ def set_font(self, font, font_scale):
+ self.font = font
+ self.font_scale = font_scale
+ self.invalidate_size_request()
+
+ def textbox(self, layout_manager):
+ return layout_manager.textbox(self.text, underline=True)
+
+ def size_request(self, layout_manager):
+ textbox = self.textbox(layout_manager)
+ return textbox.get_size()
+
+ def draw(self, context, layout_manager):
+ layout_manager.set_text_color(TEXT_COLOR)
+ layout_manager.set_font(self.font_scale, family=self.font)
+ textbox = self.textbox(layout_manager)
+ size = textbox.get_size()
+ textbox.draw(context, 0, (context.height - size[1]) // 2,
+ context.width, context.height)
+
+class FileDropTarget(widgetset.SolidBackground):
+
+ dropoff_on = widgetset.ImageDisplay(widgetset.Image(
+ image_path("dropoff-icon-on.png")))
+ dropoff_off = widgetset.ImageDisplay(widgetset.Image(
+ image_path("dropoff-icon-off.png")))
+ dropoff_small_on = widgetset.ImageDisplay(widgetset.Image(
+ image_path("dropoff-icon-small-on.png")))
+ dropoff_small_off = widgetset.ImageDisplay(widgetset.Image(
+ image_path("dropoff-icon-small-off.png")))
+
+ def __init__(self):
+ super(FileDropTarget, self).__init__()
+ self.set_background_color(DRAG_AREA)
+ self.alignment = widgetset.Alignment(
+ xscale=0.0, yscale=0.5,
+ xalign=0.5, yalign=0.5,
+ top_pad=10, right_pad=40,
+ bottom_pad=10, left_pad=40)
+ self.add(self.alignment)
+
+ self.widgets = {
+ False: self.build_large_widgets(),
+ True: self.build_small_widgets()
+ }
+
+ self.normal, self.drag = self.widgets[False]
+ self.alignment.add(self.normal)
+
+ self.in_drag = False
+ self.small = False
+
+ def build_large_widgets(self):
+ height = 40 # arbitrary, but the same for both
+ normal = widgetset.VBox(spacing=20)
+ normal.pack_start(widgetutil.align_center(self.dropoff_off,
+ top_pad=60))
+ label = CustomLabel("Drag videos here or")
+ label.set_color(TEXT_COLOR)
+ label.set_font(DND_FONT, DND_LARGE_FONTSIZE)
+ hbox = widgetset.HBox(spacing=4)
+ hbox.pack_start(widgetutil.align_middle(label))
+
+ cfb = WebStyleButton()
+ cfb.set_font(DND_FONT, DND_LARGE_FONTSIZE)
+ cfb.set_text('Choose Files...')
+
+ cfb.connect('clicked', self.choose_file)
+ hbox.pack_start(widgetutil.align_middle(cfb))
+ hbox.set_size_request(-1, height)
+ normal.pack_start(hbox)
+
+ drag = widgetset.VBox(spacing=20)
+ drag.pack_start(widgetutil.align_center(self.dropoff_on,
+ top_pad=60))
+ hbox = widgetset.HBox(spacing=4)
+ hbox.pack_start(widgetutil.align_center(
+ widgetset.Label("Release button to drop off",
+ color=TEXT_COLOR)))
+ hbox.set_size_request(-1, height)
+ drag.pack_start(hbox)
+ return normal, drag
+
+ def build_small_widgets(self):
+ height = 40 # arbitrary, but the same for both
+ normal = widgetset.HBox(spacing=4)
+ normal.pack_start(widgetutil.align_middle(self.dropoff_small_off,
+ right_pad=7))
+ drag_label = CustomLabel('Drag more videos here or')
+ drag_label.set_font(DND_FONT, DND_SMALL_FONTSIZE)
+ drag_label.set_color(TEXT_COLOR)
+ normal.pack_start(widgetutil.align_middle(drag_label))
+ cfb = WebStyleButton()
+ cfb.set_text('Choose Files...')
+ cfb.set_font(DND_FONT, DND_SMALL_FONTSIZE)
+ cfb.connect('clicked', self.choose_file)
+ normal.pack_start(cfb)
+ normal.set_size_request(-1, height)
+
+ drop_label = CustomLabel('Release button to drop off')
+ drop_label.set_font(DND_FONT, DND_SMALL_FONTSIZE)
+ drop_label.set_color(TEXT_COLOR)
+ drag = widgetset.HBox(spacing=10)
+ drag.pack_start(widgetutil.align_middle(self.dropoff_small_on))
+ drag.pack_start(widgetutil.align_middle(drop_label))
+ drag.set_size_request(-1, height)
+
+ return normal, drag
+
+ def set_small(self, small):
+ if small != self.small:
+ self.small = small
+ self.normal, self.drag = self.widgets[small]
+ self.set_in_drag(self.in_drag, force=True)
+
+ def set_in_drag(self, in_drag, force=False):
+ if force or in_drag != self.in_drag:
+ self.in_drag = in_drag
+ if in_drag:
+ self.alignment.set_child(self.drag)
+ else:
+ self.alignment.set_child(self.normal)
+ self.queue_redraw()
+
+ def choose_file(self, widget):
+ app.widgetapp.choose_file()
+
+BUTTON_BACKGROUND = widgetutil.ThreeImageSurface('settings-base')
+
+class SettingsButton(widgetset.CustomButton):
+
+ arrow_on = widgetset.ImageSurface(widgetset.Image(
+ image_path('arrow-down-on.png')))
+ arrow_off = widgetset.ImageSurface(widgetset.Image(
+ image_path('arrow-down-off.png')))
+
+ def __init__(self, name):
+ super(SettingsButton, self).__init__()
+ if name != 'settings':
+ self.name = name.title()
+ else:
+ self.name = None
+ self.selected = False
+ if name != 'format':
+ self.surface_on = widgetset.ImageSurface(widgetset.Image(
+ image_path('%s-icon-on.png' % name)))
+ self.surface_off = widgetset.ImageSurface(widgetset.Image(
+ image_path('%s-icon-off.png' % name)))
+ if self.surface_on.height != self.surface_off.height:
+ raise ValueError('invalid surface: height mismatch')
+ self.image_padding = self.calc_image_padding(name)
+ else:
+ self.surface_on = self.surface_off = None
+
+ def calc_image_padding(self, name):
+ """Add some padding to the bottom of our image icon. This can be used
+ to fine tune where it gets placed.
+
+ :returns: padding in as a (top, right, bottom, left) tuple
+ """
+
+ # NOTE: we vertically center the images, so in order to move it X
+ # pickels up, we need X*2 pixels of bottom padding
+ if name == 'android':
+ return (0, 0, 2, 0)
+ elif name in ('apple', 'other'):
+ return (0, 0, 4, 0)
+ else:
+ return (0, 0, 0, 0)
+
+ def textbox(self, layout_manager):
+ layout_manager.set_font(SETTINGS_FONTSIZE, family=SETTINGS_FONT)
+ return layout_manager.textbox(self.name)
+
+ def size_request(self, layout_manager):
+ hbox = self.build_hbox(layout_manager)
+ size = hbox.get_size()
+ height = max(BUTTON_BACKGROUND.height, size[1])
+ return int(size[0]) + 2, int(height) + 2 # padding
+
+ def build_hbox(self, layout_manager):
+ hbox = cellpack.HBox(spacing=5)
+ if self.selected:
+ image = self.surface_on
+ arrow = self.arrow_on
+ layout_manager.set_text_color(TEXT_ACTIVE)
+ else:
+ image = self.surface_off
+ arrow = self.arrow_off
+ layout_manager.set_text_color(TEXT_DISABLED)
+ if image:
+ padding = cellpack.Padding(image, *self.image_padding)
+ hbox.pack(cellpack.Alignment(padding, xscale=0, yscale=0,
+ yalign=0.5))
+ if self.name:
+ vbox = cellpack.VBox()
+ textbox = self.textbox(layout_manager)
+ vbox.pack(textbox)
+ vbox.pack_space(1)
+ hbox.pack(cellpack.Alignment(vbox, yscale=0, yalign=0.5),
+ expand=True)
+ a = cellpack.Alignment(arrow, xscale=0, yscale=0, yalign=0.5)
+ hbox.pack(cellpack.Padding(a, left=5, right=12))
+ alignment = cellpack.Padding(hbox, left=5)
+ return alignment
+
+ def draw(self, context, layout_manager):
+ BUTTON_BACKGROUND.draw(context, 1, 1, context.width - 2)
+ alignment = self.build_hbox(layout_manager)
+ padding = cellpack.Padding(alignment, top=1, right=3, bottom=1, left=3)
+ padding.render_layout(context)
+
+ def set_selected(self, selected):
+ self.selected = selected
+ self.queue_redraw()
+
+
+class OptionMenuBackground(widgetset.Background):
+ def __init__(self):
+ widgetset.Background.__init__(self)
+ self.surface = widgetutil.ThreeImageSurface('settings-depth')
+
+ def set_child(self, child):
+ widgetset.Background.set_child(self, child)
+ # re-create the image surface and scale it as it needs to cover
+ # the whole of the height of the child
+ _, h = child.get_size_request()
+ self.surface = widgetutil.ThreeImageSurface('settings-depth', height=h)
+ self.invalidate_size_request()
+
+ def size_request(self, layout_manager):
+ return -1, self.surface.height
+
+ def draw(self, context, layout_manager):
+ child_width = self.child.get_size_request()[0]
+ self.surface.draw(context, 0, 0, child_width)
+
+
+class BottomBackground(widgetset.Background):
+
+ def draw(self, context, layout_manager):
+ gradient = widgetset.Gradient(0, 0, 0, context.height)
+ gradient.set_start_color(GRADIENT_TOP)
+ gradient.set_end_color(GRADIENT_BOTTOM)
+ context.rectangle(0, 0, context.width, context.height)
+ context.gradient_fill(gradient)
+
+
+class LabeledNumberEntry(widgetset.HBox):
+
+ def __init__(self, label):
+ super(LabeledNumberEntry, self).__init__(spacing=5)
+ self.label = widgetset.Label(label, color=TEXT_COLOR)
+ self.label.set_size(widgetconst.SIZE_SMALL)
+ self.entry = widgetset.NumberEntry()
+ self.entry.set_size_request(50, 20)
+ self.pack_start(self.label)
+ self.pack_start(self.entry)
+ self.entry.connect('focus-out', lambda x: self.emit('focus-out'))
+
+ def get_text(self):
+ return self.entry.get_text()
+
+ def set_text(self, text):
+ self.entry.set_text(text)
+
+ def get_value(self):
+ try:
+ return int(self.entry.get_text())
+ except ValueError:
+ return None
+
+
+class CustomOptions(widgetset.Background):
+
+ background = widgetset.ImageSurface(widgetset.Image(
+ image_path('settings-dropdown-bottom-bg.png')))
+
+ def __init__(self):
+ super(CustomOptions, self).__init__()
+ self.create_signal('setting-changed')
+ self.reset()
+
+ def reset(self):
+ self.options = {
+ 'destination': None,
+ 'custom-size': False,
+ 'width': None,
+ 'height': None,
+ 'custom-aspect': False,
+ 'aspect-ratio': 4.0/3.0,
+ 'dont-upsize': True
+ }
+
+ self.top = self.create_top()
+ self.top.set_size_request(390, 50)
+ self.left = self.create_left()
+ self.left.set_size_request(212, 70)
+ self.right = self.create_right()
+ self.right.set_size_request(178, 70)
+ vbox = widgetset.VBox()
+ vbox.pack_start(self.top)
+ hbox = widgetset.HBox()
+ hbox.pack_start(self.left)
+ hbox.pack_start(self.right)
+ vbox.pack_start(hbox)
+
+ self.box = widgetutil.align_left(vbox)
+
+ if self.child:
+ self.set_child(self.box)
+
+ def create_top(self):
+ hbox = widgetset.HBox(spacing=0)
+ path_label = WebStyleButton()
+ path_label.set_text('Show output folder')
+ path_label.set_font(DEFAULT_FONT, widgetconst.SIZE_SMALL)
+ path_label.connect('clicked', self.on_path_label_clicked)
+ create_thumbnails = widgetset.Checkbox('Create Thumbnails',
+ color=TEXT_COLOR)
+ create_thumbnails.set_size(widgetconst.SIZE_SMALL)
+ create_thumbnails.connect('toggled',
+ self.on_create_thumbnails_changed)
+
+ hbox.pack_start(widgetutil.align(path_label, xalign=0.5), expand=True)
+ hbox.pack_start(widgetutil.align(create_thumbnails, xalign=0.5),
+ expand=True)
+ # XXX: disabled until we can figure out how to do this properly.
+ #button = widgetset.Button('...')
+ #button.connect('clicked', self.on_destination_clicked)
+ #reset = widgetset.Button('Reset')
+ #reset.connect('clicked', self.on_destination_reset)
+ #hbox.pack_start(button)
+ #hbox.pack_start(reset)
+ return widgetutil.align(hbox, xscale=1.0, yalign=0.5)
+
+ def _get_save_to_path(self):
+ if self.options['destination'] is None:
+ return get_conversion_directory()
+ else:
+ return self.options['destination']
+
+ def on_path_label_clicked(self, label):
+ save_path = self._get_save_to_path()
+ save_path = convert_path_for_subprocess(save_path)
+ openfiles.reveal_folder(save_path)
+
+ def create_left(self):
+ self.custom_size = widgetset.Checkbox('Custom Size', color=TEXT_COLOR)
+ self.custom_size.set_size(widgetconst.SIZE_SMALL)
+ self.custom_size.connect('toggled', self.on_custom_size_changed)
+
+ dont_upsize = widgetset.Checkbox('Don\'t Upsize', color=TEXT_COLOR)
+ dont_upsize.set_checked(self.options['dont-upsize'])
+ dont_upsize.set_size(widgetconst.SIZE_SMALL)
+ dont_upsize.connect('toggled', self.on_dont_upsize_changed)
+
+ bottom = widgetset.HBox(spacing=5)
+ self.width_widget = LabeledNumberEntry('Width')
+ self.width_widget.connect('focus-out', self.on_width_changed)
+ self.width_widget.entry.connect('activate',
+ self.on_width_changed)
+ self.width_widget.disable()
+ self.height_widget = LabeledNumberEntry('Height')
+ self.height_widget.connect('focus-out', self.on_height_changed)
+ self.height_widget.entry.connect('activate',
+ self.on_height_changed)
+ self.height_widget.disable()
+ bottom.pack_start(self.width_widget)
+ bottom.pack_start(self.height_widget)
+
+ hbox = widgetset.HBox(spacing=5)
+ hbox.pack_start(self.custom_size)
+ hbox.pack_start(dont_upsize)
+
+ vbox = widgetset.VBox(spacing=5)
+ vbox.pack_start(widgetutil.align_left(hbox, left_pad=10))
+ vbox.pack_start(widgetutil.align_center(bottom))
+ return widgetutil.align_middle(vbox)
+
+ def create_right(self):
+ aspect = widgetset.Checkbox('Custom Aspect Ratio', color=TEXT_COLOR)
+ aspect.set_size(widgetconst.SIZE_SMALL)
+ aspect.connect('toggled', self.on_aspect_changed)
+ self.aspect_widget = aspect
+ self.button_group = widgetset.RadioButtonGroup()
+ b1 = widgetset.RadioButton('4:3', self.button_group, color=TEXT_COLOR)
+ b2 = widgetset.RadioButton('3:2', self.button_group, color=TEXT_COLOR)
+ b3 = widgetset.RadioButton('16:9', self.button_group, color=TEXT_COLOR)
+ b1.set_selected()
+ b1.set_size(widgetconst.SIZE_SMALL)
+ b2.set_size(widgetconst.SIZE_SMALL)
+ b3.set_size(widgetconst.SIZE_SMALL)
+ self.aspect_map = dict()
+ self.aspect_map[b1] = (4, 3)
+ self.aspect_map[b2] = (3, 2)
+ self.aspect_map[b3] = (16, 9)
+ hbox = widgetset.HBox(spacing=5)
+ # Because the custom size starts off as disabled, so should aspect
+ # ratio as aspect ratio is dependent on a custom size set.
+ self.aspect_widget.disable()
+ for button in self.button_group.get_buttons():
+ button.disable()
+ button.set_size(widgetconst.SIZE_SMALL)
+ hbox.pack_start(button)
+ button.connect('clicked', self.on_aspect_size_changed)
+
+ vbox = widgetset.VBox()
+ vbox.pack_start(widgetutil.align_center(aspect))
+ vbox.pack_start(widgetutil.align_center(hbox))
+ return widgetutil.align_middle(vbox)
+
+ def draw(self, context, layout_manager):
+ self.background.draw(context, 0, 0, self.background.width,
+ self.background.height)
+
+ def enable_custom_size(self):
+ self.custom_size.enable()
+
+ def disable_custom_size(self):
+ self.custom_size.disable()
+ self.custom_size.set_checked(False)
+
+ def update_setting(self, setting, value):
+ self.options[setting] = value
+ if setting in ('width', 'height'):
+ if value is not None:
+ widget_text = str(value)
+ else:
+ widget_text = ''
+ if setting == 'width':
+ self.width_widget.set_text(widget_text)
+ elif setting == 'height':
+ self.height_widget.set_text(widget_text)
+
+ def do_setting_changed(self, setting, value):
+ logging.info('setting-changed: %s -> %s', setting, value)
+
+ def _change_setting(self, setting, value):
+ """Handles setting changes in response to widget changes."""
+
+ self.options[setting] = value
+ self.emit('setting-changed', setting, value)
+
+ def force_width_to_aspect_ratio(self):
+ aspect_ratio = self.options['aspect-ratio']
+ width = self.width_widget.get_text()
+ height = self.height_widget.get_text()
+ if not height:
+ return
+ new_width = round_even(float(height) * aspect_ratio)
+ if new_width != width:
+ self.update_setting('width', new_width)
+ self.emit('setting-changed', 'width', new_width)
+
+ def force_height_to_aspect_ratio(self):
+ aspect_ratio = self.options['aspect-ratio']
+ width = self.width_widget.get_text()
+ height = self.height_widget.get_text()
+ if not width:
+ return
+ new_height = round_even(float(width) / aspect_ratio)
+ if new_height != height:
+ self.update_setting('height', new_height)
+ self.emit('setting-changed', 'height', new_height)
+
+ def show(self):
+ self.set_child(self.box)
+ self.set_size_request(self.background.width,
+ self.background.height + 28)
+ self.queue_redraw()
+
+ def hide(self):
+ self.remove()
+ self.set_size_request(0, 0)
+ self.queue_redraw()
+
+ def toggle(self):
+ if self.child:
+ self.hide()
+ else:
+ self.show()
+
+ # signal handlers
+ def on_destination_clicked(self, widget):
+ dialog = widgetset.DirectorySelectDialog('Destination Directory')
+ r = dialog.run()
+ if r == 0: # picked a directory
+ self._change_setting('destination', directory)
+
+ def on_destination_reset(self, widget):
+ self._change_setting('destination', None)
+
+ def on_dont_upsize_changed(self, widget):
+ self._change_setting('dont-upsize', widget.get_checked())
+
+ def on_custom_size_changed(self, widget):
+ self._change_setting('custom-size', widget.get_checked())
+ if widget.get_checked():
+ self.width_widget.enable()
+ self.height_widget.enable()
+ self.aspect_widget.enable()
+ self.on_aspect_changed(self.aspect_widget)
+ else:
+ self.width_widget.disable()
+ self.height_widget.disable()
+ self.aspect_widget.disable()
+ self.on_aspect_changed(self.aspect_widget)
+ for button in self.button_group.get_buttons():
+ button.disable()
+
+ def on_create_thumbnails_changed(self, widget):
+ self._change_setting('create-thumbnails', widget.get_checked())
+
+ def on_width_changed(self, widget):
+ self._change_setting('width', self.width_widget.get_value())
+ if self.options['custom-aspect']:
+ self.force_height_to_aspect_ratio()
+
+ def on_height_changed(self, widget):
+ self._change_setting('height', self.height_widget.get_value())
+ if self.options['custom-aspect']:
+ self.force_width_to_aspect_ratio()
+
+ def on_aspect_changed(self, widget):
+ self._change_setting('custom-aspect', widget.get_checked())
+ if widget.get_checked():
+ self.force_height_to_aspect_ratio()
+ for button in self.button_group.get_buttons():
+ button.enable()
+ else:
+ for button in self.button_group.get_buttons():
+ button.disable()
+
+ def on_aspect_size_changed(self, widget):
+ if self.options['custom-aspect']:
+ width_ratio, height_ratio = [float(v) for v in
+ self.aspect_map[widget]]
+ ratio = width_ratio / height_ratio
+ self._change_setting('aspect-ratio', ratio)
+ self.force_height_to_aspect_ratio()
+
+EMPTY_CONVERTER = ConverterInfo("")
+
+
+class ConversionModel(widgetset.TableModel):
+ def __init__(self):
+ super(ConversionModel, self).__init__(
+ 'text', # filename
+ 'numeric', # output_size
+ 'text', # converter
+ 'text', # status
+ 'numeric', # duration
+ 'numeric', # progress
+ 'numeric', # eta,
+ 'object', # image
+ 'object', # the actual conversion
+ )
+ self.conversion_to_iter = {}
+ self.thumbnail_to_image = {None: widgetset.Image(
+ image_path('audio.png'))}
+
+ def conversions(self):
+ return iter(self.conversion_to_iter)
+
+ def all_conversions_done(self):
+ has_conversions = any(self.conversions())
+ all_done = ((set(c.status for c in self.conversions()) -
+ set(['canceled', 'finished', 'failed'])) == set())
+ return all_done and has_conversions
+
+ def get_image(self, path):
+ if path not in self.thumbnail_to_image:
+ try:
+ image = widgetset.Image(path)
+ except ValueError:
+ image = self.thumbnail_to_image[None]
+ self.thumbnail_to_image[path] = image
+ return self.thumbnail_to_image[path]
+
+ def update_conversion(self, conversion):
+ try:
+ output_size = os.stat(conversion.output).st_size
+ except OSError:
+ output_size = 0
+
+ def complete():
+ # needs to do it on the update_conversion() from app object
+ # which calls model_changed() and redraws for us
+ app.widgetapp.update_conversion(conversion)
+
+ values = (conversion.video.filename,
+ output_size,
+ conversion.converter.name,
+ conversion.status,
+ conversion.duration or 0,
+ conversion.progress or 0,
+ conversion.eta or 0,
+ self.get_image(conversion.video.get_thumbnail(complete, 90, 70)),
+ conversion
+ )
+ iter_ = self.conversion_to_iter.get(conversion)
+ if iter_ is None:
+ self.conversion_to_iter[conversion] = self.append(*values)
+ else:
+ self.update(iter_, *values)
+
+ def remove(self, iter_):
+ conversion = self[iter_][-1]
+ del self.conversion_to_iter[conversion]
+
+ # XXX If we add/remove too quickly, we could still be processing
+ # thumbnails and this may return null, and the self.thumbnail_to_image
+ # dictionary may get out of sync
+ def complete(path):
+ logging.info('calling completion handler for get_thumbnail on '
+ 'removal')
+
+ thumbnail_path = conversion.video.get_thumbnail(complete, 90, 70)
+ if thumbnail_path:
+ del self.thumbnail_to_image[thumbnail_path]
+ return super(ConversionModel, self).remove(iter_)
+
+
+class IconWithText(cellpack.HBox):
+
+ def __init__(self, icon, textbox):
+ super(IconWithText, self).__init__(spacing=5)
+ self.pack(cellpack.Alignment(icon, yalign=0.5, xscale=0, yscale=0))
+ self.pack(textbox)
+
+
+class ConversionCellRenderer(widgetset.CustomCellRenderer):
+
+ IGNORE_PADDING = True
+
+ clear = widgetset.ImageSurface(widgetset.Image(
+ image_path("clear-icon.png")))
+ converted_to = widgetset.ImageSurface(widgetset.Image(
+ image_path("converted_to-icon.png")))
+ queued = widgetset.ImageSurface(widgetset.Image(
+ image_path("queued-icon.png")))
+ showfile = widgetset.ImageSurface(widgetset.Image(
+ image_path("showfile-icon.png")))
+ show_ffmpeg = widgetset.ImageSurface(widgetset.Image(
+ image_path("error-icon.png")))
+ progressbar_base = widgetset.ImageSurface(widgetset.Image(
+ image_path("progressbar-base.png")))
+ delete_on = widgetset.ImageSurface(widgetset.Image(
+ image_path("item-delete-button-on.png")))
+ delete_off = widgetset.ImageSurface(widgetset.Image(
+ image_path("item-delete-button-off.png")))
+ error = widgetset.ImageSurface(widgetset.Image(
+ image_path("item-error.png")))
+ completed = widgetset.ImageSurface(widgetset.Image(
+ image_path("item-completed.png")))
+
+ def __init__(self):
+ super(ConversionCellRenderer, self).__init__()
+ self.alignment = None
+
+ def get_size(self, style, layout_manager):
+ return TABLE_WIDTH, TABLE_HEIGHT
+
+ def render(self, context, layout_manager, selected, hotspot, hover):
+ left_right = cellpack.HBox()
+ top_bottom = cellpack.VBox()
+ left_right.pack(self.layout_left(layout_manager))
+ left_right.pack(top_bottom, expand=True)
+ layout_manager.set_text_color(TEXT_COLOR)
+ layout_manager.set_font(ITEM_TITLE_FONTSIZE, bold=True,
+ family=ITEM_TITLE_FONT)
+ title = layout_manager.textbox(os.path.basename(self.input))
+ title.set_wrap_style('truncated-char')
+ alignment = cellpack.Padding(cellpack.TruncatedTextLine(title),
+ top=25)
+ top_bottom.pack(alignment)
+ layout_manager.set_font(ITEM_ICONS_FONTSIZE, family=ITEM_ICONS_FONT)
+
+ bottom = self.layout_bottom(layout_manager, hotspot)
+ if bottom is not None:
+ top_bottom.pack(bottom)
+ left_right.pack(self.layout_right(layout_manager, hotspot))
+
+ alignment = cellpack.Alignment(left_right, yscale=0, yalign=0.5)
+ self.alignment = alignment
+
+ background = cellpack.Background(alignment)
+ background.set_callback(self.draw_background)
+ background.render_layout(context)
+
+ @staticmethod
+ def draw_background(context, x, y, width, height):
+ # draw main background
+ gradient = widgetset.Gradient(x, y, x, height)
+ gradient.set_start_color(GRADIENT_TOP)
+ gradient.set_end_color(GRADIENT_BOTTOM)
+ context.rectangle(x, y, width, height)
+ context.gradient_fill(gradient)
+ # draw bottom line
+ context.set_line_width(1)
+ context.set_color((0, 0, 0))
+ context.move_to(0, height-0.5)
+ context.line_to(context.width, height-0.5)
+ context.stroke()
+
+ def draw_progressbar(self, context, x, y, _, height, width):
+ # We're only drawing a certain amount of width, not however much we're
+ # allocated. So, we ignore the passed-in width and just use what we
+ # set in layout_bottom.
+ widgetutil.circular_rect(context, x, y, width-1, height-1)
+ context.set_color((1, 1, 1))
+ context.fill()
+
+ def layout_left(self, layout_manager):
+ surface = widgetset.ImageSurface(self.thumbnail)
+ return cellpack.Padding(surface, 10, 10, 10, 10)
+
+ def layout_right(self, layout_manager, hotspot):
+ alignment_kwargs = dict(
+ xalign=0.5,
+ xscale=0,
+ yalign=0.5,
+ yscale=0,
+ min_width=80)
+ if self.status == 'finished':
+ return cellpack.Alignment(self.completed, **alignment_kwargs)
+ elif self.status in ('canceled', 'failed'):
+ return cellpack.Alignment(self.error, **alignment_kwargs)
+ else:
+ if hotspot == 'cancel':
+ image = self.delete_on
+ else:
+ image = self.delete_off
+ return cellpack.Alignment(cellpack.Hotspot('cancel',
+ image),
+ **alignment_kwargs)
+
+ def layout_bottom(self, layout_manager, hotspot):
+ layout_manager.set_text_color(TEXT_COLOR)
+ if self.status in ('converting', 'staging'):
+ box = cellpack.HBox(spacing=5)
+ stack = cellpack.Stack()
+ stack.pack(cellpack.Alignment(self.progressbar_base,
+ yalign=0.5,
+ xscale=0, yscale=0))
+ percent = self.progress / self.duration
+ width = max(int(percent * self.progressbar_base.width),
+ 5)
+ stack.pack(cellpack.DrawingArea(
+ width, self.progressbar_base.height,
+ self.draw_progressbar, width))
+ box.pack(cellpack.Alignment(stack,
+ yalign=0.5,
+ xscale=0, yscale=0))
+ textbox = layout_manager.textbox("%d%%" % (
+ 100 * percent))
+ box.pack(textbox)
+ return box
+ elif self.status == 'initialized': # queued
+ vbox = cellpack.VBox()
+ vbox.pack_space(2)
+ vbox.pack(IconWithText(self.queued,
+ layout_manager.textbox("Queued")))
+ return vbox
+ elif self.status in ('finished', 'failed', 'canceled'):
+ vbox = cellpack.VBox(spacing=5)
+ vbox.pack_space(4)
+ top = cellpack.HBox(spacing=5)
+ if self.status == 'finished':
+ if hotspot == 'show-file':
+ layout_manager.set_text_color(TEXT_CLICKED)
+ top.pack(cellpack.Hotspot('show-file', IconWithText(
+ self.showfile,
+ layout_manager.textbox('Show File',
+ underline=True))))
+ elif self.status in ('failed', 'canceled'):
+ color = TEXT_CLICKED if hotspot == 'show-log' else TEXT_COLOR
+ layout_manager.set_text_color(color)
+ # XXX Missing grey error icon
+ if self.status == 'failed':
+ text = 'Error - Show FFmpeg Output'
+ else:
+ text = 'Canceled - Show FFmpeg Output'
+ top.pack(cellpack.Hotspot('show-log', IconWithText(
+ self.show_ffmpeg,
+ layout_manager.textbox(text, underline=True))))
+ color = TEXT_CLICKED if hotspot == 'clear' else TEXT_COLOR
+ layout_manager.set_text_color(color)
+ top.pack(cellpack.Hotspot('clear', IconWithText(
+ self.showfile,
+ layout_manager.textbox('Clear', underline=True))))
+ vbox.pack(top)
+ if self.status == 'finished':
+ layout_manager.set_text_color(TEXT_INFO)
+ vbox.pack(IconWithText(
+ self.converted_to,
+ layout_manager.textbox("Converted to %s" % (
+ size_string(self.output_size)))))
+ return vbox
+
+ def hotspot_test(self, style, layout_manager, x, y, width, height):
+ if self.alignment is None:
+ return
+ hotspot_info = self.alignment.find_hotspot(x, y, width, height)
+ if hotspot_info:
+ return hotspot_info[0]
+
+class ConvertButton(widgetset.CustomButton):
+ off = widgetset.ImageSurface(widgetset.Image(
+ image_path("convert-button-off.png")))
+ clear = widgetset.ImageSurface(widgetset.Image(
+ image_path("convert-button-off.png")))
+ on = widgetset.ImageSurface(widgetset.Image(
+ image_path("convert-button-on.png")))
+ stop = widgetset.ImageSurface(widgetset.Image(
+ image_path("convert-button-stop.png")))
+
+ def __init__(self):
+ super(ConvertButton, self).__init__()
+ self.hidden = False
+ self.set_off()
+
+ def set_on(self):
+ self.label = 'Convert to %s' % app.widgetapp.current_converter.name
+ self.image = self.on
+ self.set_cursor(widgetconst.CURSOR_POINTING_HAND)
+ self.queue_redraw()
+
+ def set_clear(self):
+ self.label = 'Clear and Start Over'
+ self.image = self.clear
+ self.set_cursor(widgetconst.CURSOR_POINTING_HAND)
+ self.queue_redraw()
+
+ def set_off(self):
+ self.label = 'Convert Now'
+ self.image = self.off
+ self.set_cursor(widgetconst.CURSOR_NORMAL)
+ self.queue_redraw()
+
+ def set_stop(self):
+ self.label = 'Stop All Conversions'
+ self.image = self.stop
+ self.set_cursor(widgetconst.CURSOR_POINTING_HAND)
+ self.queue_redraw()
+
+ def hide(self):
+ self.hidden = True
+ self.invalidate_size_request()
+ self.queue_redraw()
+
+ def show(self):
+ self.hidden = False
+ self.invalidate_size_request()
+ self.queue_redraw()
+
+ def size_request(self, layout_manager):
+ if self.hidden:
+ return 0, 0
+ return self.off.width, self.off.height
+
+ def draw(self, context, layout_manager):
+ if self.hidden:
+ return
+ self.image.draw(context, 0, 0, self.image.width, self.image.height)
+ layout_manager.set_font(CONVERT_NOW_FONTSIZE, family=CONVERT_NOW_FONT)
+ if self.image == self.off:
+ layout_manager.set_text_shadow(widgetutil.Shadow(TEXT_SHADOW,
+ 0.5, (-1, -1), 0))
+ layout_manager.set_text_color(TEXT_DISABLED)
+ else:
+ layout_manager.set_text_shadow(widgetutil.Shadow(TEXT_SHADOW,
+ 0.5, (1, 1), 0))
+ layout_manager.set_text_color(TEXT_ACTIVE)
+ textbox = layout_manager.textbox(self.label)
+ alignment = cellpack.Alignment(textbox, xalign=0.5, xscale=0.0,
+ yalign=0.5, yscale=0)
+ alignment.render_layout(context)
+
+# XXX do we want to export this for general purpose use?
+class TextDialog(widgetset.Dialog):
+ def __init__(self, title, description, window):
+ widgetset.Dialog.__init__(self, title, description)
+ self.set_transient_for(window)
+ self.add_button('OK')
+ self.textbox = widgetset.MultilineTextEntry()
+ self.textbox.set_editable(False)
+ scroller = widgetset.Scroller(False, True)
+ scroller.set_has_borders(True)
+ scroller.add(self.textbox)
+ scroller.set_size_request(400, 500)
+ self.set_extra_widget(scroller)
+
+ def set_text(self, text):
+ self.textbox.set_text(text)
+
+class Application(lvc.Application):
+ def __init__(self, simultaneous=None):
+ lvc.Application.__init__(self, simultaneous)
+ self.create_signal('window-shown')
+ self.sent_window_shown = False
+
+ def startup(self):
+ if self.started:
+ return
+
+ self.current_converter = EMPTY_CONVERTER
+
+ lvc.Application.startup(self)
+
+ self.menu_manager = menus.MenuManager()
+ self.menu_manager.setup_menubar(self.menubar)
+
+ self.window = widgetset.Window("Libre Video Converter")
+ self.window.connect('on-shown', self.on_window_shown)
+ self.window.connect('will-close', self.destroy)
+
+ # # table on top
+ self.model = ConversionModel()
+ self.table = widgetset.TableView(self.model)
+ self.table.draws_selection = False
+ self.table.set_row_spacing(0)
+ self.table.enable_album_view_focus_hack()
+ self.table.set_fixed_height(True)
+ self.table.set_grid_lines(False, False)
+ self.table.set_show_headers(False)
+
+ c = widgetset.TableColumn("Data", ConversionCellRenderer(),
+ **dict((n, v) for (v, n) in enumerate((
+ 'input', 'output_size', 'converter', 'status',
+ 'duration', 'progress', 'eta', 'thumbnail',
+ 'conversion'))))
+ c.set_min_width(TABLE_WIDTH)
+ self.table.add_column(c)
+ self.table.connect('hotspot-clicked', self.hotspot_clicked)
+
+ # bottom buttons
+ converter_types = ('apple', 'android', 'other', 'format')
+ converters = {}
+ for c in self.converter_manager.list_converters():
+ media_type = c.media_type
+ if media_type not in converter_types:
+ media_type = 'others'
+ brand = self.converter_manager.converter_to_brand(c)
+ # None = top level. Otherwise tack on the brand name.
+ if brand is None:
+ converters.setdefault(media_type, set()).add(c)
+ else:
+ converters.setdefault(media_type, set()).add(brand)
+
+ self.menus = []
+
+ self.button_bar = widgetset.HBox()
+ buttons = widgetset.HBox()
+
+ for type_ in converter_types:
+ options = []
+ more_devices = None
+ for c in converters[type_]:
+ if isinstance(c, str):
+ rconverters = self.converter_manager.brand_to_converters(c)
+ values = []
+ for r in rconverters:
+ values.append((r.name, r.identifier))
+ # yuck
+ if c == 'More Devices':
+ more_devices = (c, values)
+ else:
+ options.append((c, values))
+ else:
+ options.append((c.name, c.identifier))
+ # Don't sort if formats..
+ self.sort_converter_menu(type_, options)
+ if more_devices:
+ options.append(more_devices)
+ menu = SettingsButton(type_)
+ menu.connect('clicked', self.show_options_menu, options)
+ self.menus.append(menu)
+ buttons.pack_start(menu)
+ omb = OptionMenuBackground()
+ omb.set_child(widgetutil.pad(buttons, top=2, bottom=2,
+ left=2, right=2))
+ self.button_bar.pack_start(omb)
+
+ self.settings_button = SettingsButton('settings')
+ omb = OptionMenuBackground()
+ omb.set_child(widgetutil.pad(self.settings_button, top=2,
+ bottom=2, left=2, right=2))
+ self.button_bar.pack_end(omb)
+
+ self.drop_target = FileDropTarget()
+ self.drop_target.set_size_request(-1, 70)
+
+ # # finish up
+ vbox = widgetset.VBox()
+ self.vbox = vbox
+
+ # add menubars, if we're not on windows
+ if sys.platform != 'win32':
+ attach_menubar()
+
+ self.scroller = widgetset.Scroller(False, True)
+ self.scroller.set_size_request(0, 0)
+ self.scroller.set_background_color(DRAG_AREA)
+ self.scroller.add(self.table)
+ vbox.pack_start(self.scroller)
+ vbox.pack_start(self.drop_target, expand=True)
+
+ bottom = BottomBackground()
+ bottom_box = widgetset.VBox()
+ self.convert_label = CustomLabel('Convert to')
+ self.convert_label.set_font(CONVERT_TO_FONT, CONVERT_TO_FONTSIZE)
+ self.convert_label.set_color(TEXT_COLOR)
+ bottom_box.pack_start(widgetutil.align_left(self.convert_label,
+ top_pad=10,
+ bottom_pad=10))
+ bottom_box.pack_start(self.button_bar)
+
+ self.options = CustomOptions()
+ self.options.connect('setting-changed', self.on_setting_changed)
+ self.settings_button.connect('clicked', self.on_settings_toggle)
+ bottom_box.pack_start(widgetutil.align_right(self.options,
+ right_pad=5))
+
+ self.convert_button = ConvertButton()
+ self.convert_button.connect('clicked', self.convert)
+
+ bottom_box.pack_start(widgetutil.align(self.convert_button,
+ xalign=0.5, yalign=0.5,
+ top_pad=50, bottom_pad=50))
+ bottom.set_child(widgetutil.pad(bottom_box, left=20, right=20))
+ vbox.pack_start(bottom)
+ self.window.set_content_widget(vbox)
+
+ idle_add(self.conversion_manager.check_notifications, 1)
+
+ self.window.connect('file-drag-motion', self.drag_motion)
+ self.window.connect('file-drag-received', self.drag_data_received)
+ self.window.connect('file-drag-leave', self.drag_finished)
+ self.window.accept_file_drag(True)
+
+ self.window.center()
+ self.window.show()
+ self.update_table_size()
+
+ def sort_converter_menu(self, menu_type, options):
+ """Sort a list of converter options for the menus
+
+ :param menu_type: type of the menu
+ :param options: list of (name, menu) tuples, where menu is either a
+ ConverterInfo or list of ConverterInfos.
+ """
+ if menu_type == 'format':
+ order = ['Audio', 'Video', 'Ingest Formats', 'Same Format']
+ options.sort(key=lambda (name, menu): order.index(name))
+ else:
+ options.sort()
+
+ def drag_finished(self, widget):
+ self.drop_target.set_in_drag(False)
+
+ def drag_motion(self, widget):
+ self.drop_target.set_in_drag(True)
+
+ def drag_data_received(self, widget, values):
+ for uri in values:
+ parsed = urlparse.urlparse(uri)
+ if parsed.scheme == 'file':
+ pathname = urllib.url2pathname(parsed.path)
+ self.file_activated(widget, pathname)
+
+ def on_window_shown(self, window):
+ # only emit window-shown once, even if our window gets shown, hidden,
+ # and shown again
+ if not self.sent_window_shown:
+ self.emit("window-shown")
+ self.sent_window_shown = True
+
+ def destroy(self, widget):
+ for conversion in self.conversion_manager.in_progress.copy():
+ conversion.stop()
+ mainloop_stop()
+
+ def run(self):
+ mainloop_start()
+
+ def choose_file(self):
+ dialog = widgetset.FileOpenDialog('Choose Files...')
+ dialog.set_select_multiple(True)
+ if dialog.run() == 0: # success
+ for filename in dialog.get_filenames():
+ self.file_activated(None, filename)
+ dialog.destroy()
+
+ def about(self):
+ dialog = widgetset.AboutDialog()
+ dialog.set_transient_for(self.window)
+ try:
+ dialog.run()
+ finally:
+ dialog.destroy()
+
+ def quit(self):
+ self.window.close()
+
+ def _generate_suboptions_menu(self, widget, options):
+ submenu = []
+ for option, id_ in options:
+ callback = lambda x, i: self.on_select_converter(widget,
+ options[i][1])
+ value = (option, callback)
+ submenu.append(value)
+ return submenu
+
+ def show_options_menu(self, widget, options):
+ optionlist = []
+ identifiers = dict()
+ for option, submenu in options:
+ if isinstance(submenu, list):
+ callback = self._generate_suboptions_menu(widget, submenu)
+ else:
+ callback = lambda x, i: self.on_select_converter(widget,
+ options[i][1])
+ value = (option, callback)
+ optionlist.append(value)
+ menu = widgetset.ContextMenu(optionlist)
+ menu.popup()
+
+ def update_convert_button(self):
+ can_cancel = False
+ can_start = False
+ has_conversions = any(self.model.conversions())
+ all_done = self.model.all_conversions_done()
+ for c in self.model.conversions():
+ if c.status == 'converting':
+ can_cancel = True
+ break
+ elif c.status == 'initialized':
+ can_start = True
+ # if there are no conversions ... these can't be set
+ if not has_conversions:
+ for m in self.menus:
+ m.set_selected(False)
+ self.settings_button.set_selected(False)
+ self.convert_label.set_color(TEXT_DISABLED)
+ # Set the colors - all are enabled if all conversions complete, or
+ # if we have conversions conversions but the converter has not yet
+ # been set.
+ # the converter has not been set.
+ if ((self.current_converter is EMPTY_CONVERTER and has_conversions) or
+ all_done):
+ for m in self.menus:
+ m.set_selected(True)
+ self.settings_button.set_selected(True)
+ if self.current_converter is EMPTY_CONVERTER:
+ self.convert_label.set_text('Convert to')
+ elif can_cancel:
+ target = self.current_converter.name
+ self.convert_label.set_text('Converting to %s' % target)
+ elif can_start:
+ target = self.current_converter.name
+ self.convert_label.set_text('Will convert to %s' % target)
+ self.convert_label.set_color(TEXT_ACTIVE)
+ if all_done:
+ self.convert_button.set_clear()
+ elif (self.current_converter is EMPTY_CONVERTER or not
+ (can_cancel or can_start)):
+ self.convert_button.set_off()
+ elif (self.current_converter is not EMPTY_CONVERTER and
+ self.options.options['custom-size'] and
+ (not self.options.options['width'] or
+ not self.options.options['height'])):
+ self.convert_button.set_off()
+ else:
+ self.convert_button.set_on()
+ if can_cancel:
+ self.convert_button.set_stop()
+ self.button_bar.disable()
+ else:
+ if has_conversions:
+ self.button_bar.enable()
+ else:
+ self.button_bar.disable()
+
+ def file_activated(self, widget, filename):
+ filename = os.path.realpath(filename)
+ for c in self.model.conversions():
+ if c.video.filename == filename:
+ logger.info('ignoring duplicate: %r', filename)
+ return
+ # XXX disabled - don't want to allow individualized file outputs
+ # since the workflow isn't entirely clear for now.
+ #if self.options.options['destination'] is None:
+ # try:
+ # tempfile.TemporaryFile(dir=os.path.dirname(filename))
+ # except EnvironmentError:
+ # # can't write to the destination directory; ask for a new one
+ # self.options.on_destination_clicked(None)
+ try:
+ vf = VideoFile(filename)
+ except ValueError:
+ logging.info('invalid file %r, cannot parse', filename,
+ exc_info=True)
+ return
+ c = self.conversion_manager.get_conversion(
+ vf,
+ self.current_converter,
+ output_dir=self.options.options['destination'])
+ c.listen(self.update_conversion)
+ if self.conversion_manager.running:
+ # start running automatically if a conversion is already in
+ # progress
+ self.conversion_manager.run_conversion(c)
+ self.update_conversion(c)
+ self.update_table_size()
+
+ def on_select_converter(self, widget, identifier):
+ self.current_converter = self.converter_manager.get_by_id(identifier)
+ self.options.reset()
+
+ self.converter_changed(widget)
+
+ def converter_changed(self, widget):
+ if hasattr(self, '_doing_conversion_change'):
+ return
+ self._doing_conversion_change = True
+
+ # If all conversions are done, then change the status of them back
+ # to 'initialized'.
+ #
+ # XXX TODO: what happens if the state is 'failed'? Should we reset?
+ all_done = self.model.all_conversions_done()
+ if all_done:
+ for c in self.model.conversions():
+ c.status = 'initialized'
+
+ if self.current_converter is not EMPTY_CONVERTER:
+ self.convert_label.set_text(
+ 'Will convert to %s' % self.current_converter.name)
+ else:
+ self.convert_label.set_text('Convert to')
+
+ if not self.current_converter.audio_only:
+ self.options.enable_custom_size()
+ self.options.update_setting('width',
+ self.current_converter.width)
+ self.options.update_setting('height',
+ self.current_converter.height)
+ else:
+ self.options.disable_custom_size()
+
+ for c in self.model.conversions():
+ if c.status == 'initialized':
+ c.set_converter(self.current_converter)
+ self.model.update_conversion(c)
+
+ # We likely either reset the status or we've changed the conversion
+ # output so let's just reload the table model.
+ self.table.model_changed()
+
+ self.update_convert_button()
+
+ widget.set_selected(True)
+ for menu in self.menus:
+ if menu is not widget:
+ menu.set_selected(False)
+
+ del self._doing_conversion_change
+
+ def convert(self, widget):
+ self.convert_button.disable()
+ if not self.conversion_manager.running:
+ if self.current_converter is not EMPTY_CONVERTER:
+ valid_resolution = True
+ if (self.options.options['custom-size'] and
+ not (self.options.options['width'] and
+ self.options.options['height'])):
+ valid_resolution = False
+ if valid_resolution:
+ for conversion in self.model.conversions():
+ if conversion.status == 'initialized':
+ self.conversion_manager.run_conversion(conversion)
+ self.button_bar.disable()
+ # all done: no conversion job should be running at this point
+ all_done = self.model.all_conversions_done()
+ if all_done:
+ # take stuff off one by one from the list until we have none!
+ # might not be very efficient.
+ iter_ = self.model.first_iter()
+ while iter_ is not None:
+ conversion = self.model[iter_][-1]
+ if conversion.status in ('finished',
+ 'failed',
+ 'canceled',
+ 'initialized'):
+ try:
+ self.conversion_manager.remove(conversion)
+ except ValueError:
+ pass
+ iter_ = self.model.remove(iter_)
+ self.update_table_size()
+ else:
+ for conversion in self.model.conversions():
+ conversion.stop()
+ self.update_conversion(conversion)
+ self.conversion_manager.running = False
+ self.update_convert_button()
+ self.convert_button.enable()
+
+ def update_conversion(self, conversion):
+ self.model.update_conversion(conversion)
+ self.update_table_size()
+
+ def update_table_size(self):
+ conversions = len(self.model)
+ total_height = 380
+ if not conversions:
+ self.scroller.set_size_request(-1, 0)
+ self.drop_target.set_small(False)
+ self.drop_target.set_size_request(-1, total_height)
+ else:
+ height = min(TABLE_HEIGHT * conversions, 320)
+ self.scroller.set_size_request(-1, height)
+ self.drop_target.set_small(True)
+ self.drop_target.set_size_request(-1, total_height - height)
+ self.update_convert_button()
+ self.table.model_changed()
+
+ def hotspot_clicked(self, widget, name, iter_):
+ conversion = self.model[iter_][-1]
+ if name == 'show-file':
+ reveal_file(conversion.output)
+ elif name == 'clear':
+ self.model.remove(iter_)
+ self.update_table_size()
+ elif name == 'show-log':
+ lines = ''.join(conversion.lines)
+ d = TextDialog('Log', '', self.window)
+ d.set_text(lines)
+ try:
+ d.run()
+ finally:
+ d.destroy()
+ elif name == 'cancel':
+ if conversion.status == 'initialized':
+ self.model.remove(iter_)
+ try:
+ self.conversion_manager.remove(conversion)
+ except ValueError:
+ pass
+ self.update_table_size()
+ else:
+ conversion.stop()
+ self.update_conversion(conversion)
+
+ def on_settings_toggle(self, widget):
+ if not self.options.child:
+ # hidden, going to show
+ self.convert_button.hide()
+ self.options.toggle()
+ if not self.options.child:
+ # was shown, not hidden
+ self.convert_button.show()
+
+ def on_setting_changed(self, widget, setting, value):
+ if setting == 'destination':
+ for c in self.model.conversions():
+ if c.status == 'initialized':
+ if value is None:
+ c.output_dir = os.path.dirname(c.video.filename)
+ else:
+ c.output_dir = value
+ # update final path
+ c.set_converter(self.current_converter)
+ return
+ elif setting == 'dont-upsize':
+ setattr(self.current_converter, 'dont_upsize', value)
+ return
+
+ if (self.current_converter.identifier != 'custom' and
+ setting != 'create-thumbnails'):
+ if hasattr(self.current_converter, 'simple'):
+ self.current_converter = self.current_converter.simple(
+ self.current_converter.name)
+ else:
+ if self.current_converter is EMPTY_CONVERTER:
+ self.current_converter = copy.copy(self.converter_manager.get_by_id('sameformat'))
+ else:
+ self.current_converter = copy.copy(self.current_converter)
+ # If the current converter name is resize only, then we don't
+ # want to call it a custom conversion.
+ if self.current_converter.identifier != 'sameformat':
+ self.current_converter.name = 'Custom'
+ self.current_converter.width = self.options.options['width']
+ self.current_converter.height = self.options.options['height']
+ self.converter_changed(self.menus[-1]) # formats menu
+ if setting in ('width', 'height'):
+ setattr(self.current_converter, setting, value)
+ elif setting == 'custom-size':
+ if not value:
+ self.current_converter.old_size = (
+ self.current_converter.width,
+ self.current_converter.height)
+ self.current_converter.width = None
+ self.current_converter.height = None
+ elif hasattr(self.current_converter, 'old_size'):
+ old_size = self.current_converter.old_size
+ (self.current_converter.width,
+ self.current_converter.height) = old_size
+ elif setting == 'create-thumbnails':
+ self.conversion_manager.create_thumbnails = bool(value)
+
+if __name__ == "__main__":
+ sys.dont_write_bytecode = True
+ app.widgetapp = Application()
+ initialize(app.widgetapp)