widgets.py 59 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558
  1. import os
  2. import sys
  3. import copy
  4. import tempfile
  5. import urllib
  6. import urlparse
  7. from lvc.converter import ConverterInfo
  8. from lvc.video import VideoFile
  9. from lvc.resources import image_path
  10. from lvc.utils import size_string, round_even, convert_path_for_subprocess
  11. from lvc import openfiles
  12. from lvc.widgets import (initialize, idle_add, mainloop_start, mainloop_stop,
  13. attach_menubar, reveal_file, get_conversion_directory)
  14. from lvc.widgets import menus
  15. from lvc.widgets import widgetset
  16. from lvc.widgets import cellpack
  17. from lvc.widgets import widgetconst
  18. from lvc.widgets import widgetutil
  19. from lvc.widgets import app
  20. import logging
  21. logging.basicConfig(level=logging.INFO)
  22. logger = logging.getLogger(__name__)
  23. try:
  24. import lvc
  25. except ImportError:
  26. lvc_path = os.path.join(os.path.dirname(__file__), '..', '..')
  27. sys.path.append(lvc_path)
  28. import lvc
  29. BUTTON_FONT = widgetutil.font_scale_from_osx_points(15.0)
  30. LARGE_FONT = widgetutil.font_scale_from_osx_points(13.0)
  31. SMALL_FONT = widgetutil.font_scale_from_osx_points(10.0)
  32. DEFAULT_FONT = "Helvetica"
  33. CONVERT_TO_FONT = "Gill Sans Light"
  34. CONVERT_TO_FONTSIZE = widgetutil.font_scale_from_osx_points(14.0)
  35. SETTINGS_FONT = "Gill Sans Light"
  36. SETTINGS_FONTSIZE = widgetutil.font_scale_from_osx_points(13.0)
  37. CONVERT_NOW_FONT = "Gill Sans Light"
  38. CONVERT_NOW_FONTSIZE = widgetutil.font_scale_from_osx_points(18.0)
  39. DND_FONT = "Gill Sans Light"
  40. DND_LARGE_FONTSIZE = widgetutil.font_scale_from_osx_points(13.0)
  41. DND_SMALL_FONTSIZE = widgetutil.font_scale_from_osx_points(12.0)
  42. ITEM_TITLE_FONT = "Futura Medium"
  43. ITEM_TITLE_FONTSIZE = widgetutil.font_scale_from_osx_points(13.0)
  44. ITEM_ICONS_FONT = "Century Gothic"
  45. ITEM_ICONS_FONTSIZE = widgetutil.font_scale_from_osx_points(10.0)
  46. GRADIENT_TOP = widgetutil.css_to_color('#585f63')
  47. GRADIENT_BOTTOM = widgetutil.css_to_color('#383d40')
  48. DRAG_AREA = widgetutil.css_to_color('#2b2e31')
  49. TEXT_DISABLED = widgetutil.css_to_color('#333333')
  50. TEXT_ACTIVE = widgetutil.css_to_color('#ffffff')
  51. TEXT_CLICKED = widgetutil.css_to_color('#cccccc')
  52. TEXT_INFO = widgetutil.css_to_color('#808080')
  53. TEXT_COLOR = widgetutil.css_to_color('#ffffff')
  54. TEXT_SHADOW = widgetutil.css_to_color('#000000')
  55. TABLE_WIDTH, TABLE_HEIGHT = 470, 87
  56. class CustomLabel(widgetset.Background):
  57. def __init__(self, text=''):
  58. widgetset.Background.__init__(self)
  59. self.text = text
  60. self.font = DEFAULT_FONT
  61. self.font_scale = LARGE_FONT
  62. self.color = TEXT_COLOR
  63. def set_text(self, text):
  64. self.text = text
  65. self.invalidate_size_request()
  66. def set_color(self, color):
  67. self.color = color
  68. self.queue_redraw()
  69. def set_font(self, font, font_scale):
  70. self.font = font
  71. self.font_scale = font_scale
  72. self.invalidate_size_request()
  73. def textbox(self, layout_manager):
  74. layout_manager.set_text_color(self.color)
  75. layout_manager.set_font(self.font_scale, family=self.font)
  76. font = layout_manager.set_font(self.font_scale, family=self.font)
  77. return layout_manager.textbox(self.text)
  78. def draw(self, context, layout_manager):
  79. layout_manager.set_text_color(self.color)
  80. layout_manager.set_font(LARGE_FONT, family=self.font)
  81. textbox = self.textbox(layout_manager)
  82. size = textbox.get_size()
  83. textbox.draw(context, 0, (context.height - size[1]) // 2,
  84. context.width, context.height)
  85. def size_request(self, layout_manager):
  86. return self.textbox(layout_manager).get_size()
  87. class WebStyleButton(widgetset.CustomButton):
  88. def __init__(self):
  89. super(WebStyleButton, self).__init__()
  90. self.set_cursor(widgetconst.CURSOR_POINTING_HAND)
  91. self.text = ''
  92. self.font = DEFAULT_FONT
  93. self.font_scale = LARGE_FONT
  94. def set_text(self, text):
  95. self.text = text
  96. self.invalidate_size_request()
  97. def set_font(self, font, font_scale):
  98. self.font = font
  99. self.font_scale = font_scale
  100. self.invalidate_size_request()
  101. def textbox(self, layout_manager):
  102. return layout_manager.textbox(self.text, underline=True)
  103. def size_request(self, layout_manager):
  104. textbox = self.textbox(layout_manager)
  105. return textbox.get_size()
  106. def draw(self, context, layout_manager):
  107. layout_manager.set_text_color(TEXT_COLOR)
  108. layout_manager.set_font(self.font_scale, family=self.font)
  109. textbox = self.textbox(layout_manager)
  110. size = textbox.get_size()
  111. textbox.draw(context, 0, (context.height - size[1]) // 2,
  112. context.width, context.height)
  113. class FileDropTarget(widgetset.SolidBackground):
  114. dropoff_on = widgetset.ImageDisplay(widgetset.Image(
  115. image_path("dropoff-icon-on.png")))
  116. dropoff_off = widgetset.ImageDisplay(widgetset.Image(
  117. image_path("dropoff-icon-off.png")))
  118. dropoff_small_on = widgetset.ImageDisplay(widgetset.Image(
  119. image_path("dropoff-icon-small-on.png")))
  120. dropoff_small_off = widgetset.ImageDisplay(widgetset.Image(
  121. image_path("dropoff-icon-small-off.png")))
  122. def __init__(self):
  123. super(FileDropTarget, self).__init__()
  124. self.set_background_color(DRAG_AREA)
  125. self.alignment = widgetset.Alignment(
  126. xscale=0.0, yscale=0.5,
  127. xalign=0.5, yalign=0.5,
  128. top_pad=10, right_pad=40,
  129. bottom_pad=10, left_pad=40)
  130. self.add(self.alignment)
  131. self.widgets = {
  132. False: self.build_large_widgets(),
  133. True: self.build_small_widgets()
  134. }
  135. self.normal, self.drag = self.widgets[False]
  136. self.alignment.add(self.normal)
  137. self.in_drag = False
  138. self.small = False
  139. def build_large_widgets(self):
  140. height = 40 # arbitrary, but the same for both
  141. normal = widgetset.VBox(spacing=20)
  142. normal.pack_start(widgetutil.align_center(self.dropoff_off,
  143. top_pad=60))
  144. label = CustomLabel("Drag videos here or")
  145. label.set_color(TEXT_COLOR)
  146. label.set_font(DND_FONT, DND_LARGE_FONTSIZE)
  147. hbox = widgetset.HBox(spacing=4)
  148. hbox.pack_start(widgetutil.align_middle(label))
  149. cfb = WebStyleButton()
  150. cfb.set_font(DND_FONT, DND_LARGE_FONTSIZE)
  151. cfb.set_text('Choose Files...')
  152. cfb.connect('clicked', self.choose_file)
  153. hbox.pack_start(widgetutil.align_middle(cfb))
  154. hbox.set_size_request(-1, height)
  155. normal.pack_start(hbox)
  156. drag = widgetset.VBox(spacing=20)
  157. drag.pack_start(widgetutil.align_center(self.dropoff_on,
  158. top_pad=60))
  159. hbox = widgetset.HBox(spacing=4)
  160. hbox.pack_start(widgetutil.align_center(
  161. widgetset.Label("Release button to drop off",
  162. color=TEXT_COLOR)))
  163. hbox.set_size_request(-1, height)
  164. drag.pack_start(hbox)
  165. return normal, drag
  166. def build_small_widgets(self):
  167. height = 40 # arbitrary, but the same for both
  168. normal = widgetset.HBox(spacing=4)
  169. normal.pack_start(widgetutil.align_middle(self.dropoff_small_off,
  170. right_pad=7))
  171. drag_label = CustomLabel('Drag more videos here or')
  172. drag_label.set_font(DND_FONT, DND_SMALL_FONTSIZE)
  173. drag_label.set_color(TEXT_COLOR)
  174. normal.pack_start(widgetutil.align_middle(drag_label))
  175. cfb = WebStyleButton()
  176. cfb.set_text('Choose Files...')
  177. cfb.set_font(DND_FONT, DND_SMALL_FONTSIZE)
  178. cfb.connect('clicked', self.choose_file)
  179. normal.pack_start(cfb)
  180. normal.set_size_request(-1, height)
  181. drop_label = CustomLabel('Release button to drop off')
  182. drop_label.set_font(DND_FONT, DND_SMALL_FONTSIZE)
  183. drop_label.set_color(TEXT_COLOR)
  184. drag = widgetset.HBox(spacing=10)
  185. drag.pack_start(widgetutil.align_middle(self.dropoff_small_on))
  186. drag.pack_start(widgetutil.align_middle(drop_label))
  187. drag.set_size_request(-1, height)
  188. return normal, drag
  189. def set_small(self, small):
  190. if small != self.small:
  191. self.small = small
  192. self.normal, self.drag = self.widgets[small]
  193. self.set_in_drag(self.in_drag, force=True)
  194. def set_in_drag(self, in_drag, force=False):
  195. if force or in_drag != self.in_drag:
  196. self.in_drag = in_drag
  197. if in_drag:
  198. self.alignment.set_child(self.drag)
  199. else:
  200. self.alignment.set_child(self.normal)
  201. self.queue_redraw()
  202. def choose_file(self, widget):
  203. app.widgetapp.choose_file()
  204. BUTTON_BACKGROUND = widgetutil.ThreeImageSurface('settings-base')
  205. class SettingsButton(widgetset.CustomButton):
  206. arrow_on = widgetset.ImageSurface(widgetset.Image(
  207. image_path('arrow-down-on.png')))
  208. arrow_off = widgetset.ImageSurface(widgetset.Image(
  209. image_path('arrow-down-off.png')))
  210. def __init__(self, name):
  211. super(SettingsButton, self).__init__()
  212. if name != 'settings':
  213. self.name = name.title()
  214. else:
  215. self.name = None
  216. self.selected = False
  217. if name != 'format':
  218. self.surface_on = widgetset.ImageSurface(widgetset.Image(
  219. image_path('%s-icon-on.png' % name)))
  220. self.surface_off = widgetset.ImageSurface(widgetset.Image(
  221. image_path('%s-icon-off.png' % name)))
  222. if self.surface_on.height != self.surface_off.height:
  223. raise ValueError('invalid surface: height mismatch')
  224. self.image_padding = self.calc_image_padding(name)
  225. else:
  226. self.surface_on = self.surface_off = None
  227. def calc_image_padding(self, name):
  228. """Add some padding to the bottom of our image icon. This can be used
  229. to fine tune where it gets placed.
  230. :returns: padding in as a (top, right, bottom, left) tuple
  231. """
  232. # NOTE: we vertically center the images, so in order to move it X
  233. # pickels up, we need X*2 pixels of bottom padding
  234. if name == 'android':
  235. return (0, 0, 2, 0)
  236. elif name in ('apple', 'other'):
  237. return (0, 0, 4, 0)
  238. else:
  239. return (0, 0, 0, 0)
  240. def textbox(self, layout_manager):
  241. layout_manager.set_font(SETTINGS_FONTSIZE, family=SETTINGS_FONT)
  242. return layout_manager.textbox(self.name)
  243. def size_request(self, layout_manager):
  244. hbox = self.build_hbox(layout_manager)
  245. size = hbox.get_size()
  246. height = max(BUTTON_BACKGROUND.height, size[1])
  247. return int(size[0]) + 2, int(height) + 2 # padding
  248. def build_hbox(self, layout_manager):
  249. hbox = cellpack.HBox(spacing=5)
  250. if self.selected:
  251. image = self.surface_on
  252. arrow = self.arrow_on
  253. layout_manager.set_text_color(TEXT_ACTIVE)
  254. else:
  255. image = self.surface_off
  256. arrow = self.arrow_off
  257. layout_manager.set_text_color(TEXT_DISABLED)
  258. if image:
  259. padding = cellpack.Padding(image, *self.image_padding)
  260. hbox.pack(cellpack.Alignment(padding, xscale=0, yscale=0,
  261. yalign=0.5))
  262. if self.name:
  263. vbox = cellpack.VBox()
  264. textbox = self.textbox(layout_manager)
  265. vbox.pack(textbox)
  266. vbox.pack_space(1)
  267. hbox.pack(cellpack.Alignment(vbox, yscale=0, yalign=0.5),
  268. expand=True)
  269. a = cellpack.Alignment(arrow, xscale=0, yscale=0, yalign=0.5)
  270. hbox.pack(cellpack.Padding(a, left=5, right=12))
  271. alignment = cellpack.Padding(hbox, left=5)
  272. return alignment
  273. def draw(self, context, layout_manager):
  274. BUTTON_BACKGROUND.draw(context, 1, 1, context.width - 2)
  275. alignment = self.build_hbox(layout_manager)
  276. padding = cellpack.Padding(alignment, top=1, right=3, bottom=1, left=3)
  277. padding.render_layout(context)
  278. def set_selected(self, selected):
  279. self.selected = selected
  280. self.queue_redraw()
  281. class OptionMenuBackground(widgetset.Background):
  282. def __init__(self):
  283. widgetset.Background.__init__(self)
  284. self.surface = widgetutil.ThreeImageSurface('settings-depth')
  285. def set_child(self, child):
  286. widgetset.Background.set_child(self, child)
  287. # re-create the image surface and scale it as it needs to cover
  288. # the whole of the height of the child
  289. _, h = child.get_size_request()
  290. self.surface = widgetutil.ThreeImageSurface('settings-depth', height=h)
  291. self.invalidate_size_request()
  292. def size_request(self, layout_manager):
  293. return -1, self.surface.height
  294. def draw(self, context, layout_manager):
  295. child_width = self.child.get_size_request()[0]
  296. self.surface.draw(context, 0, 0, child_width)
  297. class BottomBackground(widgetset.Background):
  298. def draw(self, context, layout_manager):
  299. gradient = widgetset.Gradient(0, 0, 0, context.height)
  300. gradient.set_start_color(GRADIENT_TOP)
  301. gradient.set_end_color(GRADIENT_BOTTOM)
  302. context.rectangle(0, 0, context.width, context.height)
  303. context.gradient_fill(gradient)
  304. class LabeledNumberEntry(widgetset.HBox):
  305. def __init__(self, label):
  306. super(LabeledNumberEntry, self).__init__(spacing=5)
  307. self.label = widgetset.Label(label, color=TEXT_COLOR)
  308. self.label.set_size(widgetconst.SIZE_SMALL)
  309. self.entry = widgetset.NumberEntry()
  310. self.entry.set_size_request(50, 20)
  311. self.pack_start(self.label)
  312. self.pack_start(self.entry)
  313. self.entry.connect('focus-out', lambda x: self.emit('focus-out'))
  314. def get_text(self):
  315. return self.entry.get_text()
  316. def set_text(self, text):
  317. self.entry.set_text(text)
  318. def get_value(self):
  319. try:
  320. return int(self.entry.get_text())
  321. except ValueError:
  322. return None
  323. class CustomOptions(widgetset.Background):
  324. background = widgetset.ImageSurface(widgetset.Image(
  325. image_path('settings-dropdown-bottom-bg.png')))
  326. def __init__(self):
  327. super(CustomOptions, self).__init__()
  328. self.create_signal('setting-changed')
  329. self.reset()
  330. def reset(self):
  331. self.options = {
  332. 'destination': None,
  333. 'custom-size': False,
  334. 'width': None,
  335. 'height': None,
  336. 'custom-aspect': False,
  337. 'aspect-ratio': 4.0/3.0,
  338. 'dont-upsize': True
  339. }
  340. self.top = self.create_top()
  341. self.top.set_size_request(390, 50)
  342. self.left = self.create_left()
  343. self.left.set_size_request(212, 70)
  344. self.right = self.create_right()
  345. self.right.set_size_request(178, 70)
  346. vbox = widgetset.VBox()
  347. vbox.pack_start(self.top)
  348. hbox = widgetset.HBox()
  349. hbox.pack_start(self.left)
  350. hbox.pack_start(self.right)
  351. vbox.pack_start(hbox)
  352. self.box = widgetutil.align_left(vbox)
  353. if self.child:
  354. self.set_child(self.box)
  355. def create_top(self):
  356. hbox = widgetset.HBox(spacing=0)
  357. path_label = WebStyleButton()
  358. path_label.set_text('Show output folder')
  359. path_label.set_font(DEFAULT_FONT, widgetconst.SIZE_SMALL)
  360. path_label.connect('clicked', self.on_path_label_clicked)
  361. create_thumbnails = widgetset.Checkbox('Create Thumbnails',
  362. color=TEXT_COLOR)
  363. create_thumbnails.set_size(widgetconst.SIZE_SMALL)
  364. create_thumbnails.connect('toggled',
  365. self.on_create_thumbnails_changed)
  366. hbox.pack_start(widgetutil.align(path_label, xalign=0.5), expand=True)
  367. hbox.pack_start(widgetutil.align(create_thumbnails, xalign=0.5),
  368. expand=True)
  369. # XXX: disabled until we can figure out how to do this properly.
  370. # button = widgetset.Button('...')
  371. # button.connect('clicked', self.on_destination_clicked)
  372. # reset = widgetset.Button('Reset')
  373. # reset.connect('clicked', self.on_destination_reset)
  374. # hbox.pack_start(button)
  375. # hbox.pack_start(reset)
  376. return widgetutil.align(hbox, xscale=1.0, yalign=0.5)
  377. def _get_save_to_path(self):
  378. if self.options['destination'] is None:
  379. return get_conversion_directory()
  380. else:
  381. return self.options['destination']
  382. def on_path_label_clicked(self, label):
  383. save_path = self._get_save_to_path()
  384. save_path = convert_path_for_subprocess(save_path)
  385. openfiles.reveal_folder(save_path)
  386. def create_left(self):
  387. self.custom_size = widgetset.Checkbox('Custom Size', color=TEXT_COLOR)
  388. self.custom_size.set_size(widgetconst.SIZE_SMALL)
  389. self.custom_size.connect('toggled', self.on_custom_size_changed)
  390. dont_upsize = widgetset.Checkbox('Don\'t Upsize', color=TEXT_COLOR)
  391. dont_upsize.set_checked(self.options['dont-upsize'])
  392. dont_upsize.set_size(widgetconst.SIZE_SMALL)
  393. dont_upsize.connect('toggled', self.on_dont_upsize_changed)
  394. bottom = widgetset.HBox(spacing=5)
  395. self.width_widget = LabeledNumberEntry('Width')
  396. self.width_widget.connect('focus-out', self.on_width_changed)
  397. self.width_widget.entry.connect('activate',
  398. self.on_width_changed)
  399. self.width_widget.disable()
  400. self.height_widget = LabeledNumberEntry('Height')
  401. self.height_widget.connect('focus-out', self.on_height_changed)
  402. self.height_widget.entry.connect('activate',
  403. self.on_height_changed)
  404. self.height_widget.disable()
  405. bottom.pack_start(self.width_widget)
  406. bottom.pack_start(self.height_widget)
  407. hbox = widgetset.HBox(spacing=5)
  408. hbox.pack_start(self.custom_size)
  409. hbox.pack_start(dont_upsize)
  410. vbox = widgetset.VBox(spacing=5)
  411. vbox.pack_start(widgetutil.align_left(hbox, left_pad=10))
  412. vbox.pack_start(widgetutil.align_center(bottom))
  413. return widgetutil.align_middle(vbox)
  414. def create_right(self):
  415. aspect = widgetset.Checkbox('Custom Aspect Ratio', color=TEXT_COLOR)
  416. aspect.set_size(widgetconst.SIZE_SMALL)
  417. aspect.connect('toggled', self.on_aspect_changed)
  418. self.aspect_widget = aspect
  419. self.button_group = widgetset.RadioButtonGroup()
  420. b1 = widgetset.RadioButton('4:3', self.button_group, color=TEXT_COLOR)
  421. b2 = widgetset.RadioButton('3:2', self.button_group, color=TEXT_COLOR)
  422. b3 = widgetset.RadioButton('16:9', self.button_group, color=TEXT_COLOR)
  423. b1.set_selected()
  424. b1.set_size(widgetconst.SIZE_SMALL)
  425. b2.set_size(widgetconst.SIZE_SMALL)
  426. b3.set_size(widgetconst.SIZE_SMALL)
  427. self.aspect_map = dict()
  428. self.aspect_map[b1] = (4, 3)
  429. self.aspect_map[b2] = (3, 2)
  430. self.aspect_map[b3] = (16, 9)
  431. hbox = widgetset.HBox(spacing=5)
  432. # Because the custom size starts off as disabled, so should aspect
  433. # ratio as aspect ratio is dependent on a custom size set.
  434. self.aspect_widget.disable()
  435. for button in self.button_group.get_buttons():
  436. button.disable()
  437. button.set_size(widgetconst.SIZE_SMALL)
  438. hbox.pack_start(button)
  439. button.connect('clicked', self.on_aspect_size_changed)
  440. vbox = widgetset.VBox()
  441. vbox.pack_start(widgetutil.align_center(aspect))
  442. vbox.pack_start(widgetutil.align_center(hbox))
  443. return widgetutil.align_middle(vbox)
  444. def draw(self, context, layout_manager):
  445. self.background.draw(context, 0, 0, self.background.width,
  446. self.background.height)
  447. def enable_custom_size(self):
  448. self.custom_size.enable()
  449. def disable_custom_size(self):
  450. self.custom_size.disable()
  451. self.custom_size.set_checked(False)
  452. def update_setting(self, setting, value):
  453. self.options[setting] = value
  454. if setting in ('width', 'height'):
  455. if value is not None:
  456. widget_text = str(value)
  457. else:
  458. widget_text = ''
  459. if setting == 'width':
  460. self.width_widget.set_text(widget_text)
  461. elif setting == 'height':
  462. self.height_widget.set_text(widget_text)
  463. def do_setting_changed(self, setting, value):
  464. logging.info('setting-changed: %s -> %s', setting, value)
  465. def _change_setting(self, setting, value):
  466. """Handles setting changes in response to widget changes."""
  467. self.options[setting] = value
  468. self.emit('setting-changed', setting, value)
  469. def force_width_to_aspect_ratio(self):
  470. aspect_ratio = self.options['aspect-ratio']
  471. width = self.width_widget.get_text()
  472. height = self.height_widget.get_text()
  473. if not height:
  474. return
  475. new_width = round_even(float(height) * aspect_ratio)
  476. if new_width != width:
  477. self.update_setting('width', new_width)
  478. self.emit('setting-changed', 'width', new_width)
  479. def force_height_to_aspect_ratio(self):
  480. aspect_ratio = self.options['aspect-ratio']
  481. width = self.width_widget.get_text()
  482. height = self.height_widget.get_text()
  483. if not width:
  484. return
  485. new_height = round_even(float(width) / aspect_ratio)
  486. if new_height != height:
  487. self.update_setting('height', new_height)
  488. self.emit('setting-changed', 'height', new_height)
  489. def show(self):
  490. self.set_child(self.box)
  491. self.set_size_request(self.background.width,
  492. self.background.height + 28)
  493. self.queue_redraw()
  494. def hide(self):
  495. self.remove()
  496. self.set_size_request(0, 0)
  497. self.queue_redraw()
  498. def toggle(self):
  499. if self.child:
  500. self.hide()
  501. else:
  502. self.show()
  503. # signal handlers
  504. def on_destination_clicked(self, widget):
  505. dialog = widgetset.DirectorySelectDialog('Destination Directory')
  506. r = dialog.run()
  507. if r == 0: # picked a directory
  508. self._change_setting('destination', directory)
  509. def on_destination_reset(self, widget):
  510. self._change_setting('destination', None)
  511. def on_dont_upsize_changed(self, widget):
  512. self._change_setting('dont-upsize', widget.get_checked())
  513. def on_custom_size_changed(self, widget):
  514. self._change_setting('custom-size', widget.get_checked())
  515. if widget.get_checked():
  516. self.width_widget.enable()
  517. self.height_widget.enable()
  518. self.aspect_widget.enable()
  519. self.on_aspect_changed(self.aspect_widget)
  520. else:
  521. self.width_widget.disable()
  522. self.height_widget.disable()
  523. self.aspect_widget.disable()
  524. self.on_aspect_changed(self.aspect_widget)
  525. for button in self.button_group.get_buttons():
  526. button.disable()
  527. def on_create_thumbnails_changed(self, widget):
  528. self._change_setting('create-thumbnails', widget.get_checked())
  529. def on_width_changed(self, widget):
  530. self._change_setting('width', self.width_widget.get_value())
  531. if self.options['custom-aspect']:
  532. self.force_height_to_aspect_ratio()
  533. def on_height_changed(self, widget):
  534. self._change_setting('height', self.height_widget.get_value())
  535. if self.options['custom-aspect']:
  536. self.force_width_to_aspect_ratio()
  537. def on_aspect_changed(self, widget):
  538. self._change_setting('custom-aspect', widget.get_checked())
  539. if widget.get_checked():
  540. self.force_height_to_aspect_ratio()
  541. for button in self.button_group.get_buttons():
  542. button.enable()
  543. else:
  544. for button in self.button_group.get_buttons():
  545. button.disable()
  546. def on_aspect_size_changed(self, widget):
  547. if self.options['custom-aspect']:
  548. width_ratio, height_ratio = [float(v) for v in
  549. self.aspect_map[widget]]
  550. ratio = width_ratio / height_ratio
  551. self._change_setting('aspect-ratio', ratio)
  552. self.force_height_to_aspect_ratio()
  553. EMPTY_CONVERTER = ConverterInfo("")
  554. class ConversionModel(widgetset.TableModel):
  555. def __init__(self):
  556. super(ConversionModel, self).__init__(
  557. 'text', # filename
  558. 'numeric', # output_size
  559. 'text', # converter
  560. 'text', # status
  561. 'numeric', # duration
  562. 'numeric', # progress
  563. 'numeric', # eta,
  564. 'object', # image
  565. 'object', # the actual conversion
  566. )
  567. self.conversion_to_iter = {}
  568. self.thumbnail_to_image = {None: widgetset.Image(
  569. image_path('audio.png'))}
  570. def conversions(self):
  571. return iter(self.conversion_to_iter)
  572. def all_conversions_done(self):
  573. has_conversions = any(self.conversions())
  574. all_done = ((set(c.status for c in self.conversions()) -
  575. set(['canceled', 'finished', 'failed'])) == set())
  576. return all_done and has_conversions
  577. def get_image(self, path):
  578. if path not in self.thumbnail_to_image:
  579. try:
  580. image = widgetset.Image(path)
  581. except ValueError:
  582. image = self.thumbnail_to_image[None]
  583. self.thumbnail_to_image[path] = image
  584. return self.thumbnail_to_image[path]
  585. def update_conversion(self, conversion):
  586. try:
  587. output_size = os.stat(conversion.output).st_size
  588. except OSError:
  589. output_size = 0
  590. def complete():
  591. # needs to do it on the update_conversion() from app object
  592. # which calls model_changed() and redraws for us
  593. app.widgetapp.update_conversion(conversion)
  594. values = (conversion.video.filename,
  595. output_size,
  596. conversion.converter.name,
  597. conversion.status,
  598. conversion.duration or 0,
  599. conversion.progress or 0,
  600. conversion.eta or 0,
  601. self.get_image(conversion.video.get_thumbnail(complete,
  602. 90, 70)),
  603. conversion)
  604. iter_ = self.conversion_to_iter.get(conversion)
  605. if iter_ is None:
  606. self.conversion_to_iter[conversion] = self.append(*values)
  607. else:
  608. self.update(iter_, *values)
  609. def remove(self, iter_):
  610. conversion = self[iter_][-1]
  611. del self.conversion_to_iter[conversion]
  612. # XXX If we add/remove too quickly, we could still be processing
  613. # thumbnails and this may return null, and the self.thumbnail_to_image
  614. # dictionary may get out of sync
  615. def complete(path):
  616. logging.info('calling completion handler for get_thumbnail on '
  617. 'removal')
  618. thumbnail_path = conversion.video.get_thumbnail(complete, 90, 70)
  619. if thumbnail_path:
  620. del self.thumbnail_to_image[thumbnail_path]
  621. return super(ConversionModel, self).remove(iter_)
  622. class IconWithText(cellpack.HBox):
  623. def __init__(self, icon, textbox):
  624. super(IconWithText, self).__init__(spacing=5)
  625. self.pack(cellpack.Alignment(icon, yalign=0.5, xscale=0, yscale=0))
  626. self.pack(textbox)
  627. class ConversionCellRenderer(widgetset.CustomCellRenderer):
  628. IGNORE_PADDING = True
  629. clear = widgetset.ImageSurface(widgetset.Image(
  630. image_path("clear-icon.png")))
  631. converted_to = widgetset.ImageSurface(widgetset.Image(
  632. image_path("converted_to-icon.png")))
  633. queued = widgetset.ImageSurface(widgetset.Image(
  634. image_path("queued-icon.png")))
  635. showfile = widgetset.ImageSurface(widgetset.Image(
  636. image_path("showfile-icon.png")))
  637. show_ffmpeg = widgetset.ImageSurface(widgetset.Image(
  638. image_path("error-icon.png")))
  639. progressbar_base = widgetset.ImageSurface(widgetset.Image(
  640. image_path("progressbar-base.png")))
  641. delete_on = widgetset.ImageSurface(widgetset.Image(
  642. image_path("item-delete-button-on.png")))
  643. delete_off = widgetset.ImageSurface(widgetset.Image(
  644. image_path("item-delete-button-off.png")))
  645. error = widgetset.ImageSurface(widgetset.Image(
  646. image_path("item-error.png")))
  647. completed = widgetset.ImageSurface(widgetset.Image(
  648. image_path("item-completed.png")))
  649. def __init__(self):
  650. super(ConversionCellRenderer, self).__init__()
  651. self.alignment = None
  652. def get_size(self, style, layout_manager):
  653. return TABLE_WIDTH, TABLE_HEIGHT
  654. def render(self, context, layout_manager, selected, hotspot, hover):
  655. left_right = cellpack.HBox()
  656. top_bottom = cellpack.VBox()
  657. left_right.pack(self.layout_left(layout_manager))
  658. left_right.pack(top_bottom, expand=True)
  659. layout_manager.set_text_color(TEXT_COLOR)
  660. layout_manager.set_font(ITEM_TITLE_FONTSIZE, bold=True,
  661. family=ITEM_TITLE_FONT)
  662. title = layout_manager.textbox(os.path.basename(self.input))
  663. title.set_wrap_style('truncated-char')
  664. alignment = cellpack.Padding(cellpack.TruncatedTextLine(title),
  665. top=25)
  666. top_bottom.pack(alignment)
  667. layout_manager.set_font(ITEM_ICONS_FONTSIZE, family=ITEM_ICONS_FONT)
  668. bottom = self.layout_bottom(layout_manager, hotspot)
  669. if bottom is not None:
  670. top_bottom.pack(bottom)
  671. left_right.pack(self.layout_right(layout_manager, hotspot))
  672. alignment = cellpack.Alignment(left_right, yscale=0, yalign=0.5)
  673. self.alignment = alignment
  674. background = cellpack.Background(alignment)
  675. background.set_callback(self.draw_background)
  676. background.render_layout(context)
  677. @staticmethod
  678. def draw_background(context, x, y, width, height):
  679. # draw main background
  680. gradient = widgetset.Gradient(x, y, x, height)
  681. gradient.set_start_color(GRADIENT_TOP)
  682. gradient.set_end_color(GRADIENT_BOTTOM)
  683. context.rectangle(x, y, width, height)
  684. context.gradient_fill(gradient)
  685. # draw bottom line
  686. context.set_line_width(1)
  687. context.set_color((0, 0, 0))
  688. context.move_to(0, height-0.5)
  689. context.line_to(context.width, height-0.5)
  690. context.stroke()
  691. def draw_progressbar(self, context, x, y, _, height, width):
  692. # We're only drawing a certain amount of width, not however much we're
  693. # allocated. So, we ignore the passed-in width and just use what we
  694. # set in layout_bottom.
  695. widgetutil.circular_rect(context, x, y, width-1, height-1)
  696. context.set_color((1, 1, 1))
  697. context.fill()
  698. def layout_left(self, layout_manager):
  699. surface = widgetset.ImageSurface(self.thumbnail)
  700. return cellpack.Padding(surface, 10, 10, 10, 10)
  701. def layout_right(self, layout_manager, hotspot):
  702. alignment_kwargs = dict(
  703. xalign=0.5,
  704. xscale=0,
  705. yalign=0.5,
  706. yscale=0,
  707. min_width=80)
  708. if self.status == 'finished':
  709. return cellpack.Alignment(self.completed, **alignment_kwargs)
  710. elif self.status in ('canceled', 'failed'):
  711. return cellpack.Alignment(self.error, **alignment_kwargs)
  712. else:
  713. if hotspot == 'cancel':
  714. image = self.delete_on
  715. else:
  716. image = self.delete_off
  717. return cellpack.Alignment(cellpack.Hotspot('cancel',
  718. image),
  719. **alignment_kwargs)
  720. def layout_bottom(self, layout_manager, hotspot):
  721. layout_manager.set_text_color(TEXT_COLOR)
  722. if self.status in ('converting', 'staging'):
  723. box = cellpack.HBox(spacing=5)
  724. stack = cellpack.Stack()
  725. stack.pack(cellpack.Alignment(self.progressbar_base,
  726. yalign=0.5,
  727. xscale=0, yscale=0))
  728. percent = self.progress / self.duration
  729. width = max(int(percent * self.progressbar_base.width),
  730. 5)
  731. stack.pack(cellpack.DrawingArea(
  732. width, self.progressbar_base.height,
  733. self.draw_progressbar, width))
  734. box.pack(cellpack.Alignment(stack,
  735. yalign=0.5,
  736. xscale=0, yscale=0))
  737. textbox = layout_manager.textbox("%d%%" % (
  738. 100 * percent))
  739. box.pack(textbox)
  740. return box
  741. elif self.status == 'initialized': # queued
  742. vbox = cellpack.VBox()
  743. vbox.pack_space(2)
  744. vbox.pack(IconWithText(self.queued,
  745. layout_manager.textbox("Queued")))
  746. return vbox
  747. elif self.status in ('finished', 'failed', 'canceled'):
  748. vbox = cellpack.VBox(spacing=5)
  749. vbox.pack_space(4)
  750. top = cellpack.HBox(spacing=5)
  751. if self.status == 'finished':
  752. if hotspot == 'show-file':
  753. layout_manager.set_text_color(TEXT_CLICKED)
  754. top.pack(cellpack.Hotspot('show-file', IconWithText(
  755. self.showfile,
  756. layout_manager.textbox('Show File',
  757. underline=True))))
  758. elif self.status in ('failed', 'canceled'):
  759. color = TEXT_CLICKED if hotspot == 'show-log' else TEXT_COLOR
  760. layout_manager.set_text_color(color)
  761. # XXX Missing grey error icon
  762. if self.status == 'failed':
  763. text = 'Error - Show FFmpeg Output'
  764. else:
  765. text = 'Canceled - Show FFmpeg Output'
  766. top.pack(cellpack.Hotspot('show-log', IconWithText(
  767. self.show_ffmpeg,
  768. layout_manager.textbox(text, underline=True))))
  769. color = TEXT_CLICKED if hotspot == 'clear' else TEXT_COLOR
  770. layout_manager.set_text_color(color)
  771. top.pack(cellpack.Hotspot('clear', IconWithText(
  772. self.showfile,
  773. layout_manager.textbox('Clear', underline=True))))
  774. vbox.pack(top)
  775. if self.status == 'finished':
  776. layout_manager.set_text_color(TEXT_INFO)
  777. vbox.pack(IconWithText(
  778. self.converted_to,
  779. layout_manager.textbox("Converted to %s" % (
  780. size_string(self.output_size)))))
  781. return vbox
  782. def hotspot_test(self, style, layout_manager, x, y, width, height):
  783. if self.alignment is None:
  784. return
  785. hotspot_info = self.alignment.find_hotspot(x, y, width, height)
  786. if hotspot_info:
  787. return hotspot_info[0]
  788. class ConvertButton(widgetset.CustomButton):
  789. off = widgetset.ImageSurface(widgetset.Image(
  790. image_path("convert-button-off.png")))
  791. clear = widgetset.ImageSurface(widgetset.Image(
  792. image_path("convert-button-off.png")))
  793. on = widgetset.ImageSurface(widgetset.Image(
  794. image_path("convert-button-on.png")))
  795. stop = widgetset.ImageSurface(widgetset.Image(
  796. image_path("convert-button-stop.png")))
  797. def __init__(self):
  798. super(ConvertButton, self).__init__()
  799. self.hidden = False
  800. self.set_off()
  801. def set_on(self):
  802. self.label = 'Convert to %s' % app.widgetapp.current_converter.name
  803. self.image = self.on
  804. self.set_cursor(widgetconst.CURSOR_POINTING_HAND)
  805. self.queue_redraw()
  806. def set_clear(self):
  807. self.label = 'Clear and Start Over'
  808. self.image = self.clear
  809. self.set_cursor(widgetconst.CURSOR_POINTING_HAND)
  810. self.queue_redraw()
  811. def set_off(self):
  812. self.label = 'Convert Now'
  813. self.image = self.off
  814. self.set_cursor(widgetconst.CURSOR_NORMAL)
  815. self.queue_redraw()
  816. def set_stop(self):
  817. self.label = 'Stop All Conversions'
  818. self.image = self.stop
  819. self.set_cursor(widgetconst.CURSOR_POINTING_HAND)
  820. self.queue_redraw()
  821. def hide(self):
  822. self.hidden = True
  823. self.invalidate_size_request()
  824. self.queue_redraw()
  825. def show(self):
  826. self.hidden = False
  827. self.invalidate_size_request()
  828. self.queue_redraw()
  829. def size_request(self, layout_manager):
  830. if self.hidden:
  831. return 0, 0
  832. return self.off.width, self.off.height
  833. def draw(self, context, layout_manager):
  834. if self.hidden:
  835. return
  836. self.image.draw(context, 0, 0, self.image.width, self.image.height)
  837. layout_manager.set_font(CONVERT_NOW_FONTSIZE, family=CONVERT_NOW_FONT)
  838. if self.image == self.off:
  839. layout_manager.set_text_shadow(widgetutil.Shadow(TEXT_SHADOW,
  840. 0.5, (-1, -1), 0))
  841. layout_manager.set_text_color(TEXT_DISABLED)
  842. else:
  843. layout_manager.set_text_shadow(widgetutil.Shadow(TEXT_SHADOW,
  844. 0.5, (1, 1), 0))
  845. layout_manager.set_text_color(TEXT_ACTIVE)
  846. textbox = layout_manager.textbox(self.label)
  847. alignment = cellpack.Alignment(textbox, xalign=0.5, xscale=0.0,
  848. yalign=0.5, yscale=0)
  849. alignment.render_layout(context)
  850. # XXX do we want to export this for general purpose use?
  851. class TextDialog(widgetset.Dialog):
  852. def __init__(self, title, description, window):
  853. widgetset.Dialog.__init__(self, title, description)
  854. self.set_transient_for(window)
  855. self.add_button('OK')
  856. self.textbox = widgetset.MultilineTextEntry()
  857. self.textbox.set_editable(False)
  858. scroller = widgetset.Scroller(False, True)
  859. scroller.set_has_borders(True)
  860. scroller.add(self.textbox)
  861. scroller.set_size_request(400, 500)
  862. self.set_extra_widget(scroller)
  863. def set_text(self, text):
  864. self.textbox.set_text(text)
  865. class Application(lvc.Application):
  866. def __init__(self, simultaneous=None):
  867. lvc.Application.__init__(self, simultaneous)
  868. self.create_signal('window-shown')
  869. self.sent_window_shown = False
  870. def startup(self):
  871. if self.started:
  872. return
  873. self.current_converter = EMPTY_CONVERTER
  874. lvc.Application.startup(self)
  875. self.menu_manager = menus.MenuManager()
  876. self.menu_manager.setup_menubar(self.menubar)
  877. self.window = widgetset.Window("Libre Video Converter")
  878. self.window.connect('on-shown', self.on_window_shown)
  879. self.window.connect('will-close', self.destroy)
  880. # # table on top
  881. self.model = ConversionModel()
  882. self.table = widgetset.TableView(self.model)
  883. self.table.draws_selection = False
  884. self.table.set_row_spacing(0)
  885. self.table.enable_album_view_focus_hack()
  886. self.table.set_fixed_height(True)
  887. self.table.set_grid_lines(False, False)
  888. self.table.set_show_headers(False)
  889. c = widgetset.TableColumn("Data", ConversionCellRenderer(),
  890. **dict((n, v) for (v, n) in enumerate((
  891. 'input', 'output_size', 'converter',
  892. 'status', 'duration', 'progress',
  893. 'eta', 'thumbnail', 'conversion'))))
  894. c.set_min_width(TABLE_WIDTH)
  895. self.table.add_column(c)
  896. self.table.connect('hotspot-clicked', self.hotspot_clicked)
  897. # bottom buttons
  898. converter_types = ('apple', 'android', 'other', 'format')
  899. converters = {}
  900. for c in self.converter_manager.list_converters():
  901. media_type = c.media_type
  902. if media_type not in converter_types:
  903. media_type = 'others'
  904. brand = self.converter_manager.converter_to_brand(c)
  905. # None = top level. Otherwise tack on the brand name.
  906. if brand is None:
  907. converters.setdefault(media_type, set()).add(c)
  908. else:
  909. converters.setdefault(media_type, set()).add(brand)
  910. self.menus = []
  911. self.button_bar = widgetset.HBox()
  912. buttons = widgetset.HBox()
  913. for type_ in converter_types:
  914. options = []
  915. more_devices = None
  916. for c in converters[type_]:
  917. if isinstance(c, str):
  918. rconverters = self.converter_manager.brand_to_converters(c)
  919. values = []
  920. for r in rconverters:
  921. values.append((r.name, r.identifier))
  922. # yuck
  923. if c == 'More Devices':
  924. more_devices = (c, values)
  925. else:
  926. options.append((c, values))
  927. else:
  928. options.append((c.name, c.identifier))
  929. # Don't sort if formats..
  930. self.sort_converter_menu(type_, options)
  931. if more_devices:
  932. options.append(more_devices)
  933. menu = SettingsButton(type_)
  934. menu.connect('clicked', self.show_options_menu, options)
  935. self.menus.append(menu)
  936. buttons.pack_start(menu)
  937. omb = OptionMenuBackground()
  938. omb.set_child(widgetutil.pad(buttons, top=2, bottom=2,
  939. left=2, right=2))
  940. self.button_bar.pack_start(omb)
  941. self.settings_button = SettingsButton('settings')
  942. omb = OptionMenuBackground()
  943. omb.set_child(widgetutil.pad(self.settings_button, top=2,
  944. bottom=2, left=2, right=2))
  945. self.button_bar.pack_end(omb)
  946. self.drop_target = FileDropTarget()
  947. self.drop_target.set_size_request(-1, 70)
  948. # # finish up
  949. vbox = widgetset.VBox()
  950. self.vbox = vbox
  951. # add menubars, if we're not on windows
  952. if sys.platform != 'win32':
  953. attach_menubar()
  954. self.scroller = widgetset.Scroller(False, True)
  955. self.scroller.set_size_request(0, 0)
  956. self.scroller.set_background_color(DRAG_AREA)
  957. self.scroller.add(self.table)
  958. vbox.pack_start(self.scroller)
  959. vbox.pack_start(self.drop_target, expand=True)
  960. bottom = BottomBackground()
  961. bottom_box = widgetset.VBox()
  962. self.convert_label = CustomLabel('Convert to')
  963. self.convert_label.set_font(CONVERT_TO_FONT, CONVERT_TO_FONTSIZE)
  964. self.convert_label.set_color(TEXT_COLOR)
  965. bottom_box.pack_start(widgetutil.align_left(self.convert_label,
  966. top_pad=10,
  967. bottom_pad=10))
  968. bottom_box.pack_start(self.button_bar)
  969. self.options = CustomOptions()
  970. self.options.connect('setting-changed', self.on_setting_changed)
  971. self.settings_button.connect('clicked', self.on_settings_toggle)
  972. bottom_box.pack_start(widgetutil.align_right(self.options,
  973. right_pad=5))
  974. self.convert_button = ConvertButton()
  975. self.convert_button.connect('clicked', self.convert)
  976. bottom_box.pack_start(widgetutil.align(self.convert_button,
  977. xalign=0.5, yalign=0.5,
  978. top_pad=50, bottom_pad=50))
  979. bottom.set_child(widgetutil.pad(bottom_box, left=20, right=20))
  980. vbox.pack_start(bottom)
  981. self.window.set_content_widget(vbox)
  982. idle_add(self.conversion_manager.check_notifications, 1)
  983. self.window.connect('file-drag-motion', self.drag_motion)
  984. self.window.connect('file-drag-received', self.drag_data_received)
  985. self.window.connect('file-drag-leave', self.drag_finished)
  986. self.window.accept_file_drag(True)
  987. self.window.center()
  988. self.window.show()
  989. self.update_table_size()
  990. def sort_converter_menu(self, menu_type, options):
  991. """Sort a list of converter options for the menus
  992. :param menu_type: type of the menu
  993. :param options: list of (name, menu) tuples, where menu is either a
  994. ConverterInfo or list of ConverterInfos.
  995. """
  996. if menu_type == 'format':
  997. order = ['Audio', 'Video', 'Ingest Formats', 'Same Format']
  998. options.sort(key=lambda (name, menu): order.index(name))
  999. else:
  1000. options.sort()
  1001. def drag_finished(self, widget):
  1002. self.drop_target.set_in_drag(False)
  1003. def drag_motion(self, widget):
  1004. self.drop_target.set_in_drag(True)
  1005. def drag_data_received(self, widget, values):
  1006. for uri in values:
  1007. parsed = urlparse.urlparse(uri)
  1008. if parsed.scheme == 'file':
  1009. pathname = urllib.url2pathname(parsed.path)
  1010. self.file_activated(widget, pathname)
  1011. def on_window_shown(self, window):
  1012. # only emit window-shown once, even if our window gets shown, hidden,
  1013. # and shown again
  1014. if not self.sent_window_shown:
  1015. self.emit("window-shown")
  1016. self.sent_window_shown = True
  1017. def destroy(self, widget):
  1018. for conversion in self.conversion_manager.in_progress.copy():
  1019. conversion.stop()
  1020. mainloop_stop()
  1021. def run(self):
  1022. mainloop_start()
  1023. def choose_file(self):
  1024. dialog = widgetset.FileOpenDialog('Choose Files...')
  1025. dialog.set_select_multiple(True)
  1026. if dialog.run() == 0: # success
  1027. for filename in dialog.get_filenames():
  1028. self.file_activated(None, filename)
  1029. dialog.destroy()
  1030. def about(self):
  1031. dialog = widgetset.AboutDialog()
  1032. dialog.set_transient_for(self.window)
  1033. try:
  1034. dialog.run()
  1035. finally:
  1036. dialog.destroy()
  1037. def quit(self):
  1038. self.window.close()
  1039. def _generate_suboptions_menu(self, widget, options):
  1040. submenu = []
  1041. for option, id_ in options:
  1042. def callback(x, i):
  1043. return self.on_select_converter(widget, options[i][1])
  1044. # callback = lambda x, i: self.on_select_converter(widget,
  1045. # options[i][1])
  1046. value = (option, callback)
  1047. submenu.append(value)
  1048. return submenu
  1049. def show_options_menu(self, widget, options):
  1050. optionlist = []
  1051. identifiers = dict()
  1052. for option, submenu in options:
  1053. if isinstance(submenu, list):
  1054. callback = self._generate_suboptions_menu(widget, submenu)
  1055. else:
  1056. def callback(x, i):
  1057. return self.on_select_converter(widget, options[i][1])
  1058. # callback = lambda x, i: self.on_select_converter(widget,
  1059. # options[i][1])
  1060. value = (option, callback)
  1061. optionlist.append(value)
  1062. menu = widgetset.ContextMenu(optionlist)
  1063. menu.popup()
  1064. def update_convert_button(self):
  1065. can_cancel = False
  1066. can_start = False
  1067. has_conversions = any(self.model.conversions())
  1068. all_done = self.model.all_conversions_done()
  1069. for c in self.model.conversions():
  1070. if c.status == 'converting':
  1071. can_cancel = True
  1072. break
  1073. elif c.status == 'initialized':
  1074. can_start = True
  1075. # if there are no conversions ... these can't be set
  1076. if not has_conversions:
  1077. for m in self.menus:
  1078. m.set_selected(False)
  1079. self.settings_button.set_selected(False)
  1080. self.convert_label.set_color(TEXT_DISABLED)
  1081. # Set the colors - all are enabled if all conversions complete, or
  1082. # if we have conversions conversions but the converter has not yet
  1083. # been set.
  1084. # the converter has not been set.
  1085. if ((self.current_converter is EMPTY_CONVERTER and has_conversions) or
  1086. all_done):
  1087. for m in self.menus:
  1088. m.set_selected(True)
  1089. self.settings_button.set_selected(True)
  1090. if self.current_converter is EMPTY_CONVERTER:
  1091. self.convert_label.set_text('Convert to')
  1092. elif can_cancel:
  1093. target = self.current_converter.name
  1094. self.convert_label.set_text('Converting to %s' % target)
  1095. elif can_start:
  1096. target = self.current_converter.name
  1097. self.convert_label.set_text('Will convert to %s' % target)
  1098. self.convert_label.set_color(TEXT_ACTIVE)
  1099. if all_done:
  1100. self.convert_button.set_clear()
  1101. elif (self.current_converter is EMPTY_CONVERTER or not
  1102. (can_cancel or can_start)):
  1103. self.convert_button.set_off()
  1104. elif (self.current_converter is not EMPTY_CONVERTER and
  1105. self.options.options['custom-size'] and
  1106. (not self.options.options['width'] or
  1107. not self.options.options['height'])):
  1108. self.convert_button.set_off()
  1109. else:
  1110. self.convert_button.set_on()
  1111. if can_cancel:
  1112. self.convert_button.set_stop()
  1113. self.button_bar.disable()
  1114. else:
  1115. if has_conversions:
  1116. self.button_bar.enable()
  1117. else:
  1118. self.button_bar.disable()
  1119. def file_activated(self, widget, filename):
  1120. filename = os.path.realpath(filename)
  1121. for c in self.model.conversions():
  1122. if c.video.filename == filename:
  1123. logger.info('ignoring duplicate: %r', filename)
  1124. return
  1125. # XXX disabled - don't want to allow individualized file outputs
  1126. # since the workflow isn't entirely clear for now.
  1127. # if self.options.options['destination'] is None:
  1128. # try:
  1129. # tempfile.TemporaryFile(dir=os.path.dirname(filename))
  1130. # except EnvironmentError:
  1131. # # can't write to the destination directory; ask for a new one
  1132. # self.options.on_destination_clicked(None)
  1133. try:
  1134. vf = VideoFile(filename)
  1135. except ValueError:
  1136. logging.info('invalid file %r, cannot parse', filename,
  1137. exc_info=True)
  1138. return
  1139. c = self.conversion_manager.get_conversion(
  1140. vf,
  1141. self.current_converter,
  1142. output_dir=self.options.options['destination'])
  1143. c.listen(self.update_conversion)
  1144. if self.conversion_manager.running:
  1145. # start running automatically if a conversion is already in
  1146. # progress
  1147. self.conversion_manager.run_conversion(c)
  1148. self.update_conversion(c)
  1149. self.update_table_size()
  1150. def on_select_converter(self, widget, identifier):
  1151. self.current_converter = self.converter_manager.get_by_id(identifier)
  1152. self.options.reset()
  1153. self.converter_changed(widget)
  1154. def converter_changed(self, widget):
  1155. if hasattr(self, '_doing_conversion_change'):
  1156. return
  1157. self._doing_conversion_change = True
  1158. # If all conversions are done, then change the status of them back
  1159. # to 'initialized'.
  1160. #
  1161. # XXX TODO: what happens if the state is 'failed'? Should we reset?
  1162. all_done = self.model.all_conversions_done()
  1163. if all_done:
  1164. for c in self.model.conversions():
  1165. c.status = 'initialized'
  1166. if self.current_converter is not EMPTY_CONVERTER:
  1167. self.convert_label.set_text(
  1168. 'Will convert to %s' % self.current_converter.name)
  1169. else:
  1170. self.convert_label.set_text('Convert to')
  1171. if not self.current_converter.audio_only:
  1172. self.options.enable_custom_size()
  1173. self.options.update_setting('width',
  1174. self.current_converter.width)
  1175. self.options.update_setting('height',
  1176. self.current_converter.height)
  1177. else:
  1178. self.options.disable_custom_size()
  1179. for c in self.model.conversions():
  1180. if c.status == 'initialized':
  1181. c.set_converter(self.current_converter)
  1182. self.model.update_conversion(c)
  1183. # We likely either reset the status or we've changed the conversion
  1184. # output so let's just reload the table model.
  1185. self.table.model_changed()
  1186. self.update_convert_button()
  1187. widget.set_selected(True)
  1188. for menu in self.menus:
  1189. if menu is not widget:
  1190. menu.set_selected(False)
  1191. del self._doing_conversion_change
  1192. def convert(self, widget):
  1193. self.convert_button.disable()
  1194. if not self.conversion_manager.running:
  1195. if self.current_converter is not EMPTY_CONVERTER:
  1196. valid_resolution = True
  1197. if (self.options.options['custom-size'] and
  1198. not (self.options.options['width'] and
  1199. self.options.options['height'])):
  1200. valid_resolution = False
  1201. if valid_resolution:
  1202. for conversion in self.model.conversions():
  1203. if conversion.status == 'initialized':
  1204. self.conversion_manager.run_conversion(conversion)
  1205. self.button_bar.disable()
  1206. # all done: no conversion job should be running at this point
  1207. all_done = self.model.all_conversions_done()
  1208. if all_done:
  1209. # take stuff off one by one from the list
  1210. # until we have none!
  1211. # might not be very efficient.
  1212. iter_ = self.model.first_iter()
  1213. while iter_ is not None:
  1214. conversion = self.model[iter_][-1]
  1215. if conversion.status in ('finished',
  1216. 'failed',
  1217. 'canceled',
  1218. 'initialized'):
  1219. try:
  1220. self.conversion_manager.remove(conversion)
  1221. except ValueError:
  1222. pass
  1223. iter_ = self.model.remove(iter_)
  1224. self.update_table_size()
  1225. else:
  1226. for conversion in self.model.conversions():
  1227. conversion.stop()
  1228. self.update_conversion(conversion)
  1229. self.conversion_manager.running = False
  1230. self.update_convert_button()
  1231. self.convert_button.enable()
  1232. def update_conversion(self, conversion):
  1233. self.model.update_conversion(conversion)
  1234. self.update_table_size()
  1235. def update_table_size(self):
  1236. conversions = len(self.model)
  1237. total_height = 380
  1238. if not conversions:
  1239. self.scroller.set_size_request(-1, 0)
  1240. self.drop_target.set_small(False)
  1241. self.drop_target.set_size_request(-1, total_height)
  1242. else:
  1243. height = min(TABLE_HEIGHT * conversions, 320)
  1244. self.scroller.set_size_request(-1, height)
  1245. self.drop_target.set_small(True)
  1246. self.drop_target.set_size_request(-1, total_height - height)
  1247. self.update_convert_button()
  1248. self.table.model_changed()
  1249. def hotspot_clicked(self, widget, name, iter_):
  1250. conversion = self.model[iter_][-1]
  1251. if name == 'show-file':
  1252. reveal_file(conversion.output)
  1253. elif name == 'clear':
  1254. self.model.remove(iter_)
  1255. self.update_table_size()
  1256. elif name == 'show-log':
  1257. lines = ''.join(conversion.lines)
  1258. d = TextDialog('Log', '', self.window)
  1259. d.set_text(lines)
  1260. try:
  1261. d.run()
  1262. finally:
  1263. d.destroy()
  1264. elif name == 'cancel':
  1265. if conversion.status == 'initialized':
  1266. self.model.remove(iter_)
  1267. try:
  1268. self.conversion_manager.remove(conversion)
  1269. except ValueError:
  1270. pass
  1271. self.update_table_size()
  1272. else:
  1273. conversion.stop()
  1274. self.update_conversion(conversion)
  1275. def on_settings_toggle(self, widget):
  1276. if not self.options.child:
  1277. # hidden, going to show
  1278. self.convert_button.hide()
  1279. self.options.toggle()
  1280. if not self.options.child:
  1281. # was shown, not hidden
  1282. self.convert_button.show()
  1283. def on_setting_changed(self, widget, setting, value):
  1284. if setting == 'destination':
  1285. for c in self.model.conversions():
  1286. if c.status == 'initialized':
  1287. if value is None:
  1288. c.output_dir = os.path.dirname(c.video.filename)
  1289. else:
  1290. c.output_dir = value
  1291. # update final path
  1292. c.set_converter(self.current_converter)
  1293. return
  1294. elif setting == 'dont-upsize':
  1295. setattr(self.current_converter, 'dont_upsize', value)
  1296. return
  1297. if (self.current_converter.identifier != 'custom' and
  1298. setting != 'create-thumbnails'):
  1299. if hasattr(self.current_converter, 'simple'):
  1300. self.current_converter = self.current_converter.simple(
  1301. self.current_converter.name)
  1302. else:
  1303. if self.current_converter is EMPTY_CONVERTER:
  1304. self.current_converter = copy.copy(
  1305. self.converter_manager.get_by_id('sameformat'))
  1306. else:
  1307. self.current_converter = copy.copy(self.current_converter)
  1308. # If the current converter name is resize only, then we don't
  1309. # want to call it a custom conversion.
  1310. if self.current_converter.identifier != 'sameformat':
  1311. self.current_converter.name = 'Custom'
  1312. self.current_converter.width = self.options.options['width']
  1313. self.current_converter.height = self.options.options['height']
  1314. self.converter_changed(self.menus[-1]) # formats menu
  1315. if setting in ('width', 'height'):
  1316. setattr(self.current_converter, setting, value)
  1317. elif setting == 'custom-size':
  1318. if not value:
  1319. self.current_converter.old_size = (
  1320. self.current_converter.width,
  1321. self.current_converter.height)
  1322. self.current_converter.width = None
  1323. self.current_converter.height = None
  1324. elif hasattr(self.current_converter, 'old_size'):
  1325. old_size = self.current_converter.old_size
  1326. (self.current_converter.width,
  1327. self.current_converter.height) = old_size
  1328. elif setting == 'create-thumbnails':
  1329. self.conversion_manager.create_thumbnails = bool(value)
  1330. if __name__ == "__main__":
  1331. sys.dont_write_bytecode = True
  1332. app.widgetapp = Application()
  1333. initialize(app.widgetapp)