diff options
Diffstat (limited to 'mvc/ui/widgets.py')
-rw-r--r-- | mvc/ui/widgets.py | 1540 |
1 files changed, 1540 insertions, 0 deletions
diff --git a/mvc/ui/widgets.py b/mvc/ui/widgets.py new file mode 100644 index 0000000..67dffc8 --- /dev/null +++ b/mvc/ui/widgets.py @@ -0,0 +1,1540 @@ +import logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +import os +import sys + +try: + import mvc +except ImportError: + mvc_path = os.path.join(os.path.dirname(__file__), '..', '..') + sys.path.append(mvc_path) + import mvc + +import copy +import tempfile +import urllib +import urlparse + +from mvc.widgets import (initialize, idle_add, mainloop_start, mainloop_stop, + attach_menubar, reveal_file, get_conversion_directory) +from mvc.widgets import menus +from mvc.widgets import widgetset +from mvc.widgets import cellpack +from mvc.widgets import widgetconst +from mvc.widgets import widgetutil +from mvc.widgets import app + +from mvc.converter import ConverterInfo +from mvc.video import VideoFile +from mvc.resources import image_path +from mvc.utils import size_string, round_even, convert_path_for_subprocess +from mvc 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(mvc.Application): + def __init__(self, simultaneous=None): + mvc.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 + + mvc.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) |