You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1499 lines
51 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
7 years ago
10 years ago
10 years ago
10 years ago
8 years ago
8 years ago
10 years ago
10 years ago
10 years ago
8 years ago
8 years ago
10 years ago
8 years ago
8 years ago
10 years ago
10 years ago
10 years ago
10 years ago
8 years ago
10 years ago
8 years ago
7 years ago
8 years ago
8 years ago
8 years ago
7 years ago
8 years ago
10 years ago
8 years ago
10 years ago
8 years ago
8 years ago
8 years ago
10 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
8 years ago
10 years ago
8 years ago
8 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. #!/usr/bin/env python2
  2. # -*- coding: utf-8 -*-
  3. """Youtubedlg module responsible for the main app window. """
  4. from __future__ import unicode_literals
  5. import os
  6. import gettext
  7. import wx
  8. from wx.lib.pubsub import setuparg1 #NOTE Should remove deprecated
  9. from wx.lib.pubsub import pub as Publisher
  10. from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
  11. from .parsers import OptionsParser
  12. from .optionsframe import (
  13. OptionsFrame,
  14. LogGUI
  15. )
  16. from .updatemanager import (
  17. UPDATE_PUB_TOPIC,
  18. UpdateThread
  19. )
  20. from .downloadmanager import (
  21. MANAGER_PUB_TOPIC,
  22. WORKER_PUB_TOPIC,
  23. DownloadManager,
  24. DownloadList,
  25. DownloadItem
  26. )
  27. from .utils import (
  28. get_pixmaps_dir,
  29. build_command,
  30. get_icon_file,
  31. shutdown_sys,
  32. remove_file,
  33. open_file,
  34. get_time
  35. )
  36. from .widgets import CustomComboBox
  37. from .formats import (
  38. DEFAULT_FORMATS,
  39. VIDEO_FORMATS,
  40. AUDIO_FORMATS,
  41. FORMATS
  42. )
  43. from .info import (
  44. __descriptionfull__,
  45. __licensefull__,
  46. __projecturl__,
  47. __appname__,
  48. __author__
  49. )
  50. from .version import __version__
  51. class MainFrame(wx.Frame):
  52. """Main window class.
  53. This class is responsible for creating the main app window
  54. and binding the events.
  55. Attributes:
  56. FRAMES_MIN_SIZE (tuple): Tuple that contains the minumum width, height of the frame.
  57. Labels area (strings): Strings for the widgets labels.
  58. STATUSLIST_COLUMNS (dict): Python dictionary which holds informations
  59. about the wxListCtrl columns. For more informations read the
  60. comments above the STATUSLIST_COLUMNS declaration.
  61. Args:
  62. opt_manager (optionsmanager.OptionsManager): Object responsible for
  63. handling the settings.
  64. log_manager (logmanager.LogManager): Object responsible for handling
  65. the log stuff.
  66. parent (wx.Window): Frame parent.
  67. """
  68. FRAMES_MIN_SIZE = (560, 360)
  69. # Labels area
  70. URLS_LABEL = _("Enter URLs below")
  71. UPDATE_LABEL = _("Update")
  72. OPTIONS_LABEL = _("Options")
  73. STOP_LABEL = _("Stop")
  74. INFO_LABEL = _("Info")
  75. WELCOME_MSG = _("Welcome")
  76. WARNING_LABEL = _("Warning")
  77. ADD_LABEL = _("Add")
  78. DOWNLOAD_LIST_LABEL = _("Download list")
  79. DELETE_LABEL = _("Delete")
  80. PLAY_LABEL = _("Play")
  81. UP_LABEL = _("Up")
  82. DOWN_LABEL = _("Down")
  83. RELOAD_LABEL = _("Reload")
  84. PAUSE_LABEL = _("Pause")
  85. START_LABEL = _("Start")
  86. ABOUT_LABEL = _("About")
  87. VIEWLOG_LABEL = _("View Log")
  88. SUCC_REPORT_MSG = _("Successfully downloaded {0} URL(s) in {1} "
  89. "day(s) {2} hour(s) {3} minute(s) {4} second(s)")
  90. DL_COMPLETED_MSG = _("Downloads completed")
  91. URL_REPORT_MSG = _("Total Progress: {0:.1f}% | Queued ({1}) Paused ({2}) Active ({3}) Completed ({4}) Error ({5})")
  92. CLOSING_MSG = _("Stopping downloads")
  93. CLOSED_MSG = _("Downloads stopped")
  94. PROVIDE_URL_MSG = _("You need to provide at least one URL")
  95. DOWNLOAD_STARTED = _("Downloads started")
  96. CHOOSE_DIRECTORY = _("Choose Directory")
  97. DOWNLOAD_ACTIVE = _("Download in progress. Please wait for all downloads to complete")
  98. UPDATE_ACTIVE = _("Update already in progress")
  99. UPDATING_MSG = _("Downloading latest youtube-dl. Please wait...")
  100. UPDATE_ERR_MSG = _("Youtube-dl download failed [{0}]")
  101. UPDATE_SUCC_MSG = _("Successfully downloaded youtube-dl")
  102. OPEN_DIR_ERR = _("Unable to open directory: '{dir}'. "
  103. "The specified path does not exist")
  104. SHUTDOWN_ERR = _("Error while shutting down. "
  105. "Make sure you typed the correct password")
  106. SHUTDOWN_MSG = _("Shutting down system")
  107. VIDEO_LABEL = _("Title")
  108. EXTENSION_LABEL = _("Extension")
  109. SIZE_LABEL = _("Size")
  110. PERCENT_LABEL = _("Percent")
  111. ETA_LABEL = _("ETA")
  112. SPEED_LABEL = _("Speed")
  113. STATUS_LABEL = _("Status")
  114. #################################
  115. # STATUSLIST_COLUMNS
  116. #
  117. # Dictionary which contains the columns for the wxListCtrl widget.
  118. # Each key represents a column and holds informations about itself.
  119. # Structure informations:
  120. # column_key: (column_number, column_label, minimum_width, is_resizable)
  121. #
  122. STATUSLIST_COLUMNS = {
  123. 'filename': (0, VIDEO_LABEL, 150, True),
  124. 'extension': (1, EXTENSION_LABEL, 60, False),
  125. 'filesize': (2, SIZE_LABEL, 80, False),
  126. 'percent': (3, PERCENT_LABEL, 65, False),
  127. 'eta': (4, ETA_LABEL, 45, False),
  128. 'speed': (5, SPEED_LABEL, 90, False),
  129. 'status': (6, STATUS_LABEL, 160, False)
  130. }
  131. def __init__(self, opt_manager, log_manager, parent=None):
  132. super(MainFrame, self).__init__(parent, wx.ID_ANY, __appname__, size=opt_manager.options["main_win_size"])
  133. self.opt_manager = opt_manager
  134. self.log_manager = log_manager
  135. self.download_manager = None
  136. self.update_thread = None
  137. self.app_icon = None #REFACTOR Get and set on __init__.py
  138. self._download_list = DownloadList()
  139. # Set up youtube-dl options parser
  140. self._options_parser = OptionsParser()
  141. # Get the pixmaps directory
  142. self._pixmaps_path = get_pixmaps_dir()
  143. # Set the Timer
  144. self._app_timer = wx.Timer(self)
  145. # Set the app icon
  146. app_icon_path = get_icon_file()
  147. if app_icon_path is not None:
  148. self.app_icon = wx.Icon(app_icon_path, wx.BITMAP_TYPE_PNG)
  149. self.SetIcon(self.app_icon)
  150. bitmap_data = (
  151. ("down", "arrow_down_32px.png"),
  152. ("up", "arrow_up_32px.png"),
  153. ("play", "camera_32px.png"),
  154. ("start", "cloud_download_32px.png"),
  155. ("delete", "delete_32px.png"),
  156. ("folder", "folder_32px.png"),
  157. ("pause", "pause_32px.png"),
  158. ("resume", "play_arrow_32px.png"),
  159. ("reload", "reload_32px.png"),
  160. ("settings", "settings_20px.png"),
  161. ("stop", "stop_32px.png")
  162. )
  163. self._bitmaps = {}
  164. for item in bitmap_data:
  165. target, name = item
  166. self._bitmaps[target] = wx.Bitmap(os.path.join(self._pixmaps_path, name))
  167. # Set the data for all the wx.Button items
  168. # name, label, size, event_handler
  169. buttons_data = (
  170. ("delete", self.DELETE_LABEL, (-1, -1), self._on_delete, wx.BitmapButton),
  171. ("play", self.PLAY_LABEL, (-1, -1), self._on_play, wx.BitmapButton),
  172. ("up", self.UP_LABEL, (-1, -1), self._on_arrow_up, wx.BitmapButton),
  173. ("down", self.DOWN_LABEL, (-1, -1), self._on_arrow_down, wx.BitmapButton),
  174. ("reload", self.RELOAD_LABEL, (-1, -1), self._on_reload, wx.BitmapButton),
  175. ("pause", self.PAUSE_LABEL, (-1, -1), self._on_pause, wx.BitmapButton),
  176. ("start", self.START_LABEL, (-1, -1), self._on_start, wx.BitmapButton),
  177. ("savepath", "...", (35, -1), self._on_savepath, wx.Button),
  178. ("add", self.ADD_LABEL, (-1, -1), self._on_add, wx.Button)
  179. )
  180. # Set the data for the settings menu item
  181. # label, event_handler
  182. settings_menu_data = (
  183. (self.OPTIONS_LABEL, self._on_options),
  184. (self.UPDATE_LABEL, self._on_update),
  185. (self.VIEWLOG_LABEL, self._on_viewlog),
  186. (self.ABOUT_LABEL, self._on_about)
  187. )
  188. statuslist_menu_data = (
  189. (_("Get URL"), self._on_geturl),
  190. (_("Get command"), self._on_getcmd),
  191. (_("Open destination"), self._on_open_dest),
  192. (_("Re-enter"), self._on_reenter)
  193. )
  194. # Create options frame
  195. self._options_frame = OptionsFrame(self)
  196. # Create frame components
  197. self._panel = wx.Panel(self)
  198. self._url_text = self._create_statictext(self.URLS_LABEL)
  199. #REFACTOR Move to buttons_data
  200. self._settings_button = self._create_bitmap_button(self._bitmaps["settings"], (30, 30), self._on_settings)
  201. self._url_list = self._create_textctrl(wx.TE_MULTILINE | wx.TE_DONTWRAP, self._on_urllist_edit)
  202. self._folder_icon = self._create_static_bitmap(self._bitmaps["folder"], self._on_open_path)
  203. self._path_combobox = ExtComboBox(self._panel, 5, style=wx.CB_READONLY)
  204. self._videoformat_combobox = CustomComboBox(self._panel, style=wx.CB_READONLY)
  205. self._download_text = self._create_statictext(self.DOWNLOAD_LIST_LABEL)
  206. self._status_list = ListCtrl(self.STATUSLIST_COLUMNS,
  207. parent=self._panel,
  208. style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
  209. # Dictionary to store all the buttons
  210. self._buttons = {}
  211. for item in buttons_data:
  212. name, label, size, evt_handler, parent = item
  213. button = parent(self._panel, size=size)
  214. if parent == wx.Button:
  215. button.SetLabel(label)
  216. elif parent == wx.BitmapButton:
  217. button.SetToolTip(wx.ToolTip(label))
  218. if name in self._bitmaps:
  219. button.SetBitmap(self._bitmaps[name], wx.TOP)
  220. if evt_handler is not None:
  221. button.Bind(wx.EVT_BUTTON, evt_handler)
  222. self._buttons[name] = button
  223. self._status_bar = self.CreateStatusBar()
  224. # Create extra components
  225. self._settings_menu = self._create_menu_item(settings_menu_data)
  226. self._statuslist_menu = self._create_menu_item(statuslist_menu_data)
  227. # Overwrite the menu hover event to avoid changing the statusbar
  228. self.Bind(wx.EVT_MENU_HIGHLIGHT, lambda event: None)
  229. # Bind extra events
  230. self.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self._on_statuslist_right_click, self._status_list)
  231. self.Bind(wx.EVT_TEXT, self._update_savepath, self._path_combobox)
  232. self.Bind(wx.EVT_LIST_ITEM_SELECTED, self._update_pause_button, self._status_list)
  233. self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self._update_pause_button, self._status_list)
  234. self.Bind(wx.EVT_CLOSE, self._on_close)
  235. self.Bind(wx.EVT_TIMER, self._on_timer, self._app_timer)
  236. self._videoformat_combobox.Bind(wx.EVT_COMBOBOX, self._update_videoformat)
  237. # Set threads wxCallAfter handlers
  238. self._set_publisher(self._update_handler, UPDATE_PUB_TOPIC)
  239. self._set_publisher(self._download_worker_handler, WORKER_PUB_TOPIC)
  240. self._set_publisher(self._download_manager_handler, MANAGER_PUB_TOPIC)
  241. # Set up extra stuff
  242. self.Center()
  243. self.SetMinSize(self.FRAMES_MIN_SIZE)
  244. self._status_bar_write(self.WELCOME_MSG)
  245. self._update_videoformat_combobox()
  246. self._path_combobox.LoadMultiple(self.opt_manager.options["save_path_dirs"])
  247. self._path_combobox.SetValue(self.opt_manager.options["save_path"])
  248. self._set_layout()
  249. self._url_list.SetFocus()
  250. def _create_menu_item(self, items):
  251. menu = wx.Menu()
  252. for item in items:
  253. label, evt_handler = item
  254. menu_item = menu.Append(-1, label)
  255. menu.Bind(wx.EVT_MENU, evt_handler, menu_item)
  256. return menu
  257. def _on_statuslist_right_click(self, event):
  258. selected = event.GetIndex()
  259. if selected != -1:
  260. self._status_list.deselect_all()
  261. self._status_list.Select(selected, on=1)
  262. self.PopupMenu(self._statuslist_menu)
  263. def _on_reenter(self, event):
  264. selected = self._status_list.get_selected()
  265. if selected != -1:
  266. object_id = self._status_list.GetItemData(selected)
  267. download_item = self._download_list.get_item(object_id)
  268. if download_item.stage != "Active":
  269. self._status_list.remove_row(selected)
  270. self._download_list.remove(object_id)
  271. options = self._options_parser.parse(self.opt_manager.options)
  272. download_item = DownloadItem(download_item.url, options)
  273. download_item.path = self.opt_manager.options["save_path"]
  274. if not self._download_list.has_item(download_item.object_id):
  275. self._status_list.bind_item(download_item)
  276. self._download_list.insert(download_item)
  277. def reset(self):
  278. self._update_videoformat_combobox()
  279. self._path_combobox.LoadMultiple(self.opt_manager.options["save_path_dirs"])
  280. self._path_combobox.SetValue(self.opt_manager.options["save_path"])
  281. def _on_open_dest(self, event):
  282. selected = self._status_list.get_selected()
  283. if selected != -1:
  284. object_id = self._status_list.GetItemData(selected)
  285. download_item = self._download_list.get_item(object_id)
  286. if download_item.path:
  287. open_file(download_item.path)
  288. def _on_open_path(self, event):
  289. open_file(self._path_combobox.GetValue())
  290. def _on_geturl(self, event):
  291. selected = self._status_list.get_selected()
  292. if selected != -1:
  293. object_id = self._status_list.GetItemData(selected)
  294. download_item = self._download_list.get_item(object_id)
  295. url = download_item.url
  296. if not wx.TheClipboard.IsOpened():
  297. clipdata = wx.TextDataObject()
  298. clipdata.SetText(url)
  299. wx.TheClipboard.Open()
  300. wx.TheClipboard.SetData(clipdata)
  301. wx.TheClipboard.Close()
  302. def _on_getcmd(self, event):
  303. selected = self._status_list.get_selected()
  304. if selected != -1:
  305. object_id = self._status_list.GetItemData(selected)
  306. download_item = self._download_list.get_item(object_id)
  307. cmd = build_command(download_item.options, download_item.url)
  308. if not wx.TheClipboard.IsOpened():
  309. clipdata = wx.TextDataObject()
  310. clipdata.SetText(cmd)
  311. wx.TheClipboard.Open()
  312. wx.TheClipboard.SetData(clipdata)
  313. wx.TheClipboard.Close()
  314. def _on_timer(self, event):
  315. total_percentage = 0.0
  316. queued = paused = active = completed = error = 0
  317. for item in self._download_list.get_items():
  318. if item.stage == "Queued":
  319. queued += 1
  320. if item.stage == "Paused":
  321. paused += 1
  322. if item.stage == "Active":
  323. active += 1
  324. total_percentage += float(item.progress_stats["percent"].split('%')[0])
  325. if item.stage == "Completed":
  326. completed += 1
  327. if item.stage == "Error":
  328. error += 1
  329. # REFACTOR Store percentage as float in the DownloadItem?
  330. # REFACTOR DownloadList keep track for each item stage?
  331. items_count = active + completed + error + queued
  332. total_percentage += completed * 100.0 + error * 100.0
  333. if items_count:
  334. total_percentage /= items_count
  335. msg = self.URL_REPORT_MSG.format(total_percentage, queued, paused, active, completed, error)
  336. if self.update_thread is None:
  337. # Dont overwrite the update messages
  338. self._status_bar_write(msg)
  339. def _update_pause_button(self, event):
  340. selected_rows = self._status_list.get_all_selected()
  341. label = _("Pause")
  342. bitmap = self._bitmaps["pause"]
  343. for row in selected_rows:
  344. object_id = self._status_list.GetItemData(row)
  345. download_item = self._download_list.get_item(object_id)
  346. if download_item.stage == "Paused":
  347. # If we find one or more items in Paused
  348. # state set the button functionality to resume
  349. label = _("Resume")
  350. bitmap = self._bitmaps["resume"]
  351. break
  352. self._buttons["pause"].SetLabel(label)
  353. self._buttons["pause"].SetToolTip(wx.ToolTip(label))
  354. self._buttons["pause"].SetBitmap(bitmap, wx.TOP)
  355. def _update_videoformat_combobox(self):
  356. self._videoformat_combobox.Clear()
  357. self._videoformat_combobox.add_items(list(DEFAULT_FORMATS.values()), False)
  358. vformats = []
  359. for vformat in self.opt_manager.options["selected_video_formats"]:
  360. vformats.append(FORMATS[vformat])
  361. aformats = []
  362. for aformat in self.opt_manager.options["selected_audio_formats"]:
  363. aformats.append(FORMATS[aformat])
  364. if vformats:
  365. self._videoformat_combobox.add_header(_("Video"))
  366. self._videoformat_combobox.add_items(vformats)
  367. if aformats:
  368. self._videoformat_combobox.add_header(_("Audio"))
  369. self._videoformat_combobox.add_items(aformats)
  370. current_index = self._videoformat_combobox.FindString(FORMATS[self.opt_manager.options["selected_format"]])
  371. if current_index == wx.NOT_FOUND:
  372. self._videoformat_combobox.SetSelection(0)
  373. else:
  374. self._videoformat_combobox.SetSelection(current_index)
  375. self._update_videoformat(None)
  376. def _update_videoformat(self, event):
  377. self.opt_manager.options["selected_format"] = selected_format = FORMATS[self._videoformat_combobox.GetValue()]
  378. if selected_format in VIDEO_FORMATS:
  379. self.opt_manager.options["video_format"] = selected_format
  380. self.opt_manager.options["audio_format"] = "" #NOTE Set to default value, check parsers.py
  381. elif selected_format in AUDIO_FORMATS:
  382. self.opt_manager.options["video_format"] = DEFAULT_FORMATS[_("default")]
  383. self.opt_manager.options["audio_format"] = selected_format
  384. else:
  385. self.opt_manager.options["video_format"] = DEFAULT_FORMATS[_("default")]
  386. self.opt_manager.options["audio_format"] = ""
  387. def _update_savepath(self, event):
  388. self.opt_manager.options["save_path"] = self._path_combobox.GetValue()
  389. def _on_delete(self, event):
  390. index = self._status_list.get_next_selected()
  391. if index == -1:
  392. dlg = ButtonsChoiceDialog(self, [_("Remove all"), _("Remove completed")], _("No items selected. Please pick an action"), _("Delete"))
  393. ret_code = dlg.ShowModal()
  394. dlg.Destroy()
  395. #REFACTOR Maybe add this functionality directly to DownloadList?
  396. if ret_code == 1:
  397. for ditem in self._download_list.get_items():
  398. if ditem.stage != "Active":
  399. self._status_list.remove_row(self._download_list.index(ditem.object_id))
  400. self._download_list.remove(ditem.object_id)
  401. if ret_code == 2:
  402. for ditem in self._download_list.get_items():
  403. if ditem.stage == "Completed":
  404. self._status_list.remove_row(self._download_list.index(ditem.object_id))
  405. self._download_list.remove(ditem.object_id)
  406. else:
  407. if self.opt_manager.options["confirm_deletion"]:
  408. dlg = wx.MessageDialog(self, _("Are you sure you want to remove selected items?"), _("Delete"), wx.YES_NO | wx.ICON_QUESTION)
  409. result = dlg.ShowModal() == wx.ID_YES
  410. dlg.Destroy()
  411. else:
  412. result = True
  413. if result:
  414. while index >= 0:
  415. object_id = self._status_list.GetItemData(index)
  416. selected_download_item = self._download_list.get_item(object_id)
  417. if selected_download_item.stage == "Active":
  418. self._create_popup(_("Item is active, cannot remove"), self.WARNING_LABEL, wx.OK | wx.ICON_EXCLAMATION)
  419. else:
  420. #if selected_download_item.stage == "Completed":
  421. #dlg = wx.MessageDialog(self, "Do you want to remove the files associated with this item?", "Remove files", wx.YES_NO | wx.ICON_QUESTION)
  422. #result = dlg.ShowModal() == wx.ID_YES
  423. #dlg.Destroy()
  424. #if result:
  425. #for cur_file in selected_download_item.get_files():
  426. #remove_file(cur_file)
  427. self._status_list.remove_row(index)
  428. self._download_list.remove(object_id)
  429. index -= 1
  430. index = self._status_list.get_next_selected(index)
  431. self._update_pause_button(None)
  432. def _on_play(self, event):
  433. selected_rows = self._status_list.get_all_selected()
  434. if selected_rows:
  435. for selected_row in selected_rows:
  436. object_id = self._status_list.GetItemData(selected_row)
  437. selected_download_item = self._download_list.get_item(object_id)
  438. if selected_download_item.stage == "Completed":
  439. if selected_download_item.filenames:
  440. filename = selected_download_item.get_files()[-1]
  441. open_file(filename)
  442. else:
  443. self._create_popup(_("Item is not completed"), self.INFO_LABEL, wx.OK | wx.ICON_INFORMATION)
  444. def _on_arrow_up(self, event):
  445. index = self._status_list.get_next_selected()
  446. if index != -1:
  447. while index >= 0:
  448. object_id = self._status_list.GetItemData(index)
  449. download_item = self._download_list.get_item(object_id)
  450. new_index = index - 1
  451. if new_index < 0:
  452. new_index = 0
  453. if not self._status_list.IsSelected(new_index):
  454. self._download_list.move_up(object_id)
  455. self._status_list.move_item_up(index)
  456. self._status_list._update_from_item(new_index, download_item)
  457. index = self._status_list.get_next_selected(index)
  458. def _on_arrow_down(self, event):
  459. index = self._status_list.get_next_selected(reverse=True)
  460. if index != -1:
  461. while index >= 0:
  462. object_id = self._status_list.GetItemData(index)
  463. download_item = self._download_list.get_item(object_id)
  464. new_index = index + 1
  465. if new_index >= self._status_list.GetItemCount():
  466. new_index = self._status_list.GetItemCount() - 1
  467. if not self._status_list.IsSelected(new_index):
  468. self._download_list.move_down(object_id)
  469. self._status_list.move_item_down(index)
  470. self._status_list._update_from_item(new_index, download_item)
  471. index = self._status_list.get_next_selected(index, True)
  472. def _on_reload(self, event):
  473. selected_rows = self._status_list.get_all_selected()
  474. if not selected_rows:
  475. for index, item in enumerate(self._download_list.get_items()):
  476. if item.stage in ("Paused", "Completed", "Error"):
  477. # Store the old savepath because reset is going to remove it
  478. savepath = item.path
  479. item.reset()
  480. item.path = savepath
  481. self._status_list._update_from_item(index, item)
  482. else:
  483. for selected_row in selected_rows:
  484. object_id = self._status_list.GetItemData(selected_row)
  485. item = self._download_list.get_item(object_id)
  486. if item.stage in ("Paused", "Completed", "Error"):
  487. # Store the old savepath because reset is going to remove it
  488. savepath = item.path
  489. item.reset()
  490. item.path = savepath
  491. self._status_list._update_from_item(selected_row, item)
  492. self._update_pause_button(None)
  493. def _on_pause(self, event):
  494. selected_rows = self._status_list.get_all_selected()
  495. if selected_rows:
  496. #REFACTOR Use DoubleStageButton for this and check stage
  497. if self._buttons["pause"].GetLabel() == _("Pause"):
  498. new_state = "Paused"
  499. else:
  500. new_state = "Queued"
  501. for selected_row in selected_rows:
  502. object_id = self._status_list.GetItemData(selected_row)
  503. download_item = self._download_list.get_item(object_id)
  504. if download_item.stage == "Queued" or download_item.stage == "Paused":
  505. self._download_list.change_stage(object_id, new_state)
  506. self._status_list._update_from_item(selected_row, download_item)
  507. self._update_pause_button(None)
  508. def _on_start(self, event):
  509. if self.download_manager is None:
  510. if self.update_thread is not None and self.update_thread.is_alive():
  511. self._create_popup(_("Update in progress. Please wait for the update to complete"),
  512. self.WARNING_LABEL,
  513. wx.OK | wx.ICON_EXCLAMATION)
  514. else:
  515. self._start_download()
  516. else:
  517. self.download_manager.stop_downloads()
  518. def _on_savepath(self, event):
  519. dlg = wx.DirDialog(self, self.CHOOSE_DIRECTORY, self._path_combobox.GetStringSelection())
  520. if dlg.ShowModal() == wx.ID_OK:
  521. path = dlg.GetPath()
  522. self._path_combobox.Append(path)
  523. self._path_combobox.SetValue(path)
  524. self._update_savepath(None)
  525. dlg.Destroy()
  526. def _on_add(self, event):
  527. urls = self._get_urls()
  528. if not urls:
  529. self._create_popup(self.PROVIDE_URL_MSG,
  530. self.WARNING_LABEL,
  531. wx.OK | wx.ICON_EXCLAMATION)
  532. else:
  533. self._url_list.Clear()
  534. options = self._options_parser.parse(self.opt_manager.options)
  535. for url in urls:
  536. download_item = DownloadItem(url, options)
  537. download_item.path = self.opt_manager.options["save_path"]
  538. if not self._download_list.has_item(download_item.object_id):
  539. self._status_list.bind_item(download_item)
  540. self._download_list.insert(download_item)
  541. def _on_settings(self, event):
  542. event_object_pos = event.EventObject.GetPosition()
  543. event_object_height = event.EventObject.GetSize()[1]
  544. event_object_pos = (event_object_pos[0], event_object_pos[1] + event_object_height)
  545. self.PopupMenu(self._settings_menu, event_object_pos)
  546. def _on_viewlog(self, event):
  547. if self.log_manager is None:
  548. self._create_popup(_("Logging is disabled"),
  549. self.WARNING_LABEL,
  550. wx.OK | wx.ICON_EXCLAMATION)
  551. else:
  552. log_window = LogGUI(self)
  553. log_window.load(self.log_manager.log_file)
  554. log_window.Show()
  555. def _on_about(self, event):
  556. info = wx.AboutDialogInfo()
  557. if self.app_icon is not None:
  558. info.SetIcon(self.app_icon)
  559. info.SetName(__appname__)
  560. info.SetVersion(__version__)
  561. info.SetDescription(__descriptionfull__)
  562. info.SetWebSite(__projecturl__)
  563. info.SetLicense(__licensefull__)
  564. info.AddDeveloper(__author__)
  565. wx.AboutBox(info)
  566. def _set_publisher(self, handler, topic):
  567. """Sets a handler for the given topic.
  568. Args:
  569. handler (function): Can be any function with one parameter
  570. the message that the caller sends.
  571. topic (string): Can be any string that identifies the caller.
  572. You can bind multiple handlers on the same topic or
  573. multiple topics on the same handler.
  574. """
  575. Publisher.subscribe(handler, topic)
  576. def _create_statictext(self, label):
  577. return wx.StaticText(self._panel, label=label)
  578. def _create_bitmap_button(self, icon, size=(-1, -1), handler=None):
  579. button = wx.BitmapButton(self._panel, bitmap=icon, size=size, style=wx.NO_BORDER)
  580. if handler is not None:
  581. button.Bind(wx.EVT_BUTTON, handler)
  582. return button
  583. def _create_static_bitmap(self, icon, event_handler=None):
  584. static_bitmap = wx.StaticBitmap(self._panel, bitmap=icon)
  585. if event_handler is not None:
  586. static_bitmap.Bind(wx.EVT_LEFT_DCLICK, event_handler)
  587. return static_bitmap
  588. def _create_textctrl(self, style=None, event_handler=None):
  589. if style is None:
  590. textctrl = wx.TextCtrl(self._panel)
  591. else:
  592. textctrl = wx.TextCtrl(self._panel, style=style)
  593. if event_handler is not None:
  594. textctrl.Bind(wx.EVT_TEXT_PASTE, event_handler)
  595. textctrl.Bind(wx.EVT_MIDDLE_DOWN, event_handler)
  596. if os.name == 'nt':
  597. # Enable CTRL+A on Windows
  598. def win_ctrla_eventhandler(event):
  599. if event.GetKeyCode() == wx.WXK_CONTROL_A:
  600. event.GetEventObject().SelectAll()
  601. event.Skip()
  602. textctrl.Bind(wx.EVT_CHAR, win_ctrla_eventhandler)
  603. return textctrl
  604. def _create_popup(self, text, title, style):
  605. wx.MessageBox(text, title, style)
  606. def _set_layout(self):
  607. """Sets the layout of the main window. """
  608. main_sizer = wx.BoxSizer()
  609. panel_sizer = wx.BoxSizer(wx.VERTICAL)
  610. top_sizer = wx.BoxSizer(wx.HORIZONTAL)
  611. top_sizer.Add(self._url_text, 0, wx.ALIGN_BOTTOM | wx.BOTTOM, 5)
  612. top_sizer.AddSpacer((-1, -1), 1)
  613. top_sizer.Add(self._settings_button)
  614. panel_sizer.Add(top_sizer, 0, wx.EXPAND)
  615. panel_sizer.Add(self._url_list, 1, wx.EXPAND)
  616. mid_sizer = wx.BoxSizer(wx.HORIZONTAL)
  617. mid_sizer.Add(self._folder_icon)
  618. mid_sizer.AddSpacer((3, -1))
  619. mid_sizer.Add(self._path_combobox, 2, wx.ALIGN_CENTER_VERTICAL)
  620. mid_sizer.AddSpacer((5, -1))
  621. mid_sizer.Add(self._buttons["savepath"], flag=wx.ALIGN_CENTER_VERTICAL)
  622. mid_sizer.AddSpacer((10, -1), 1)
  623. mid_sizer.Add(self._videoformat_combobox, 1, wx.ALIGN_CENTER_VERTICAL)
  624. mid_sizer.AddSpacer((5, -1))
  625. mid_sizer.Add(self._buttons["add"], flag=wx.ALIGN_CENTER_VERTICAL)
  626. panel_sizer.Add(mid_sizer, 0, wx.EXPAND | wx.ALL, 10)
  627. panel_sizer.Add(self._download_text, 0, wx.BOTTOM, 5)
  628. panel_sizer.Add(self._status_list, 2, wx.EXPAND)
  629. bottom_sizer = wx.BoxSizer(wx.HORIZONTAL)
  630. bottom_sizer.Add(self._buttons["delete"])
  631. bottom_sizer.AddSpacer((5, -1))
  632. bottom_sizer.Add(self._buttons["play"])
  633. bottom_sizer.AddSpacer((5, -1))
  634. bottom_sizer.Add(self._buttons["up"])
  635. bottom_sizer.AddSpacer((5, -1))
  636. bottom_sizer.Add(self._buttons["down"])
  637. bottom_sizer.AddSpacer((5, -1))
  638. bottom_sizer.Add(self._buttons["reload"])
  639. bottom_sizer.AddSpacer((5, -1))
  640. bottom_sizer.Add(self._buttons["pause"])
  641. bottom_sizer.AddSpacer((10, -1), 1)
  642. bottom_sizer.Add(self._buttons["start"])
  643. panel_sizer.Add(bottom_sizer, 0, wx.EXPAND | wx.TOP, 5)
  644. main_sizer.Add(panel_sizer, 1, wx.ALL | wx.EXPAND, 10)
  645. self._panel.SetSizer(main_sizer)
  646. self._panel.Layout()
  647. def _update_youtubedl(self):
  648. """Update youtube-dl binary to the latest version. """
  649. if self.download_manager is not None and self.download_manager.is_alive():
  650. self._create_popup(self.DOWNLOAD_ACTIVE,
  651. self.WARNING_LABEL,
  652. wx.OK | wx.ICON_EXCLAMATION)
  653. elif self.update_thread is not None and self.update_thread.is_alive():
  654. self._create_popup(self.UPDATE_ACTIVE,
  655. self.INFO_LABEL,
  656. wx.OK | wx.ICON_INFORMATION)
  657. else:
  658. self.update_thread = UpdateThread(self.opt_manager.options['youtubedl_path'])
  659. def _status_bar_write(self, msg):
  660. """Display msg in the status bar. """
  661. self._status_bar.SetStatusText(msg)
  662. def _reset_widgets(self):
  663. """Resets GUI widgets after update or download process. """
  664. self._buttons["start"].SetLabel(_("Start"))
  665. self._buttons["start"].SetToolTip(wx.ToolTip(_("Start")))
  666. self._buttons["start"].SetBitmap(self._bitmaps["start"], wx.TOP)
  667. def _print_stats(self):
  668. """Display download stats in the status bar. """
  669. suc_downloads = self.download_manager.successful
  670. dtime = get_time(self.download_manager.time_it_took)
  671. msg = self.SUCC_REPORT_MSG.format(suc_downloads,
  672. dtime['days'],
  673. dtime['hours'],
  674. dtime['minutes'],
  675. dtime['seconds'])
  676. self._status_bar_write(msg)
  677. def _after_download(self):
  678. """Run tasks after download process has been completed.
  679. Note:
  680. Here you can add any tasks you want to run after the
  681. download process has been completed.
  682. """
  683. if self.opt_manager.options['shutdown']:
  684. dlg = ShutdownDialog(self, 60, _("Shutting down in {0} second(s)"), _("Shutdown"))
  685. result = dlg.ShowModal() == wx.ID_OK
  686. dlg.Destroy()
  687. if result:
  688. self.opt_manager.save_to_file()
  689. success = shutdown_sys(self.opt_manager.options['sudo_password'])
  690. if success:
  691. self._status_bar_write(self.SHUTDOWN_MSG)
  692. else:
  693. self._status_bar_write(self.SHUTDOWN_ERR)
  694. else:
  695. if self.opt_manager.options["show_completion_popup"]:
  696. self._create_popup(self.DL_COMPLETED_MSG, self.INFO_LABEL, wx.OK | wx.ICON_INFORMATION)
  697. def _download_worker_handler(self, msg):
  698. """downloadmanager.Worker thread handler.
  699. Handles messages from the Worker thread.
  700. Args:
  701. See downloadmanager.Worker _talk_to_gui() method.
  702. """
  703. signal, data = msg.data
  704. download_item = self._download_list.get_item(data["index"])
  705. download_item.update_stats(data)
  706. row = self._download_list.index(data["index"])
  707. self._status_list._update_from_item(row, download_item)
  708. def _download_manager_handler(self, msg):
  709. """downloadmanager.DownloadManager thread handler.
  710. Handles messages from the DownloadManager thread.
  711. Args:
  712. See downloadmanager.DownloadManager _talk_to_gui() method.
  713. """
  714. data = msg.data
  715. if data == 'finished':
  716. self._print_stats()
  717. self._reset_widgets()
  718. self.download_manager = None
  719. self._app_timer.Stop()
  720. self._after_download()
  721. elif data == 'closed':
  722. self._status_bar_write(self.CLOSED_MSG)
  723. self._reset_widgets()
  724. self.download_manager = None
  725. self._app_timer.Stop()
  726. elif data == 'closing':
  727. self._status_bar_write(self.CLOSING_MSG)
  728. elif data == 'report_active':
  729. pass
  730. #NOTE Remove from here and downloadmanager
  731. #since now we have the wx.Timer to check progress
  732. def _update_handler(self, msg):
  733. """updatemanager.UpdateThread thread handler.
  734. Handles messages from the UpdateThread thread.
  735. Args:
  736. See updatemanager.UpdateThread _talk_to_gui() method.
  737. """
  738. data = msg.data
  739. if data[0] == 'download':
  740. self._status_bar_write(self.UPDATING_MSG)
  741. elif data[0] == 'error':
  742. self._status_bar_write(self.UPDATE_ERR_MSG.format(data[1]))
  743. elif data[0] == 'correct':
  744. self._status_bar_write(self.UPDATE_SUCC_MSG)
  745. else:
  746. self._reset_widgets()
  747. self.update_thread = None
  748. def _get_urls(self):
  749. """Returns urls list. """
  750. return [line for line in self._url_list.GetValue().split('\n') if line]
  751. def _start_download(self):
  752. if self._status_list.is_empty():
  753. self._create_popup(_("No items to download"),
  754. self.WARNING_LABEL,
  755. wx.OK | wx.ICON_EXCLAMATION)
  756. else:
  757. self._app_timer.Start(100)
  758. self.download_manager = DownloadManager(self, self._download_list, self.opt_manager, self.log_manager)
  759. self._status_bar_write(self.DOWNLOAD_STARTED)
  760. self._buttons["start"].SetLabel(self.STOP_LABEL)
  761. self._buttons["start"].SetToolTip(wx.ToolTip(self.STOP_LABEL))
  762. self._buttons["start"].SetBitmap(self._bitmaps["stop"], wx.TOP)
  763. def _paste_from_clipboard(self):
  764. """Paste the content of the clipboard to the self._url_list widget.
  765. It also adds a new line at the end of the data if not exist.
  766. """
  767. if not wx.TheClipboard.IsOpened():
  768. if wx.TheClipboard.Open():
  769. if wx.TheClipboard.IsSupported(wx.DataFormat(wx.DF_TEXT)):
  770. data = wx.TextDataObject()
  771. wx.TheClipboard.GetData(data)
  772. data = data.GetText()
  773. if data[-1] != '\n':
  774. data += '\n'
  775. self._url_list.WriteText(data)
  776. wx.TheClipboard.Close()
  777. def _on_urllist_edit(self, event):
  778. """Event handler of the self._url_list widget.
  779. This method is triggered when the users pastes text into
  780. the URLs list either by using CTRL+V or by using the middle
  781. click of the mouse.
  782. """
  783. if event.GetEventType() == wx.EVT_TEXT_PASTE.typeId:
  784. self._paste_from_clipboard()
  785. else:
  786. wx.TheClipboard.UsePrimarySelection(True)
  787. self._paste_from_clipboard()
  788. wx.TheClipboard.UsePrimarySelection(False)
  789. def _on_update(self, event):
  790. """Event handler of the self._update_btn widget.
  791. This method is used when the update button is pressed to start
  792. the update process.
  793. Note:
  794. Currently there is not way to stop the update process.
  795. """
  796. if self.opt_manager.options["disable_update"]:
  797. self._create_popup(_("Updates are disabled for your system. Please use the system's package manager to update youtube-dl."),
  798. self.INFO_LABEL,
  799. wx.OK | wx.ICON_INFORMATION)
  800. else:
  801. self._update_youtubedl()
  802. def _on_options(self, event):
  803. """Event handler of the self._options_btn widget.
  804. This method is used when the options button is pressed to show
  805. the options window.
  806. """
  807. self._options_frame.load_all_options()
  808. self._options_frame.Show()
  809. def _on_close(self, event):
  810. """Event handler for the wx.EVT_CLOSE event.
  811. This method is used when the user tries to close the program
  812. to save the options and make sure that the download & update
  813. processes are not running.
  814. """
  815. if self.opt_manager.options["confirm_exit"]:
  816. dlg = wx.MessageDialog(self, _("Are you sure you want to exit?"), _("Exit"), wx.YES_NO | wx.ICON_QUESTION)
  817. result = dlg.ShowModal() == wx.ID_YES
  818. dlg.Destroy()
  819. else:
  820. result = True
  821. if result:
  822. self.close()
  823. def close(self):
  824. if self.download_manager is not None:
  825. self.download_manager.stop_downloads()
  826. self.download_manager.join()
  827. if self.update_thread is not None:
  828. self.update_thread.join()
  829. # Store main-options frame size
  830. self.opt_manager.options['main_win_size'] = self.GetSize()
  831. self.opt_manager.options['opts_win_size'] = self._options_frame.GetSize()
  832. self.opt_manager.options["save_path_dirs"] = self._path_combobox.GetStrings()
  833. self._options_frame.save_all_options()
  834. self.opt_manager.save_to_file()
  835. self.Destroy()
  836. class ListCtrl(wx.ListCtrl, ListCtrlAutoWidthMixin):
  837. """Custom ListCtrl widget.
  838. Args:
  839. columns (dict): See MainFrame class STATUSLIST_COLUMNS attribute.
  840. """
  841. def __init__(self, columns, *args, **kwargs):
  842. super(ListCtrl, self).__init__(*args, **kwargs)
  843. ListCtrlAutoWidthMixin.__init__(self)
  844. self.columns = columns
  845. self._list_index = 0
  846. self._url_list = set()
  847. self._set_columns()
  848. def remove_row(self, row_number):
  849. self.DeleteItem(row_number)
  850. self._list_index -= 1
  851. def move_item_up(self, row_number):
  852. self._move_item(row_number, row_number - 1)
  853. def move_item_down(self, row_number):
  854. self._move_item(row_number, row_number + 1)
  855. def _move_item(self, cur_row, new_row):
  856. self.Freeze()
  857. item = self.GetItem(cur_row)
  858. self.DeleteItem(cur_row)
  859. item.SetId(new_row)
  860. self.InsertItem(item)
  861. self.Select(new_row)
  862. self.Thaw()
  863. def has_url(self, url):
  864. """Returns True if the url is aleady in the ListCtrl else False.
  865. Args:
  866. url (string): URL string.
  867. """
  868. return url in self._url_list
  869. def bind_item(self, download_item):
  870. self.InsertStringItem(self._list_index, download_item.url)
  871. self.SetItemData(self._list_index, download_item.object_id)
  872. self._update_from_item(self._list_index, download_item)
  873. self._list_index += 1
  874. def _update_from_item(self, row, download_item):
  875. progress_stats = download_item.progress_stats
  876. for key in self.columns:
  877. column = self.columns[key][0]
  878. if key == "status" and progress_stats["playlist_index"]:
  879. # Not the best place but we build the playlist status here
  880. status = "{0} {1}/{2}".format(progress_stats["status"],
  881. progress_stats["playlist_index"],
  882. progress_stats["playlist_size"])
  883. self.SetStringItem(row, column, status)
  884. else:
  885. self.SetStringItem(row, column, progress_stats[key])
  886. def clear(self):
  887. """Clear the ListCtrl widget & reset self._list_index and
  888. self._url_list. """
  889. self.DeleteAllItems()
  890. self._list_index = 0
  891. self._url_list = set()
  892. def is_empty(self):
  893. """Returns True if the list is empty else False. """
  894. return self._list_index == 0
  895. def get_selected(self):
  896. return self.GetNextItem(-1, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED)
  897. def get_all_selected(self):
  898. return [index for index in xrange(self._list_index) if self.IsSelected(index)]
  899. def deselect_all(self):
  900. for index in xrange(self._list_index):
  901. self.Select(index, on=0)
  902. def get_next_selected(self, start=-1, reverse=False):
  903. if start == -1:
  904. start = self._list_index - 1 if reverse else 0
  905. else:
  906. # start from next item
  907. if reverse:
  908. start -= 1
  909. else:
  910. start += 1
  911. end = -1 if reverse else self._list_index
  912. step = -1 if reverse else 1
  913. for index in xrange(start, end, step):
  914. if self.IsSelected(index):
  915. return index
  916. return -1
  917. def _set_columns(self):
  918. """Initializes ListCtrl columns.
  919. See MainFrame STATUSLIST_COLUMNS attribute for more info. """
  920. for column_item in sorted(self.columns.values()):
  921. self.InsertColumn(column_item[0], column_item[1], width=wx.LIST_AUTOSIZE_USEHEADER)
  922. # If the column width obtained from wxLIST_AUTOSIZE_USEHEADER
  923. # is smaller than the minimum allowed column width
  924. # then set the column width to the minumum allowed size
  925. if self.GetColumnWidth(column_item[0]) < column_item[2]:
  926. self.SetColumnWidth(column_item[0], column_item[2])
  927. # Set auto-resize if enabled
  928. if column_item[3]:
  929. self.setResizeColumn(column_item[0])
  930. # REFACTOR Extra widgets below should move to other module with widgets
  931. class ExtComboBox(wx.ComboBox):
  932. def __init__(self, parent, max_items=-1, *args, **kwargs):
  933. super(ExtComboBox, self).__init__(parent, *args, **kwargs)
  934. assert max_items > 0 or max_items == -1
  935. self.max_items = max_items
  936. def Append(self, new_value):
  937. if self.FindString(new_value) == wx.NOT_FOUND:
  938. super(ExtComboBox, self).Append(new_value)
  939. if self.max_items != -1 and self.GetCount() > self.max_items:
  940. self.SetItems(self.GetStrings()[1:])
  941. def SetValue(self, new_value):
  942. if self.FindString(new_value) == wx.NOT_FOUND:
  943. self.Append(new_value)
  944. self.SetSelection(self.FindString(new_value))
  945. def LoadMultiple(self, items_list):
  946. for item in items_list:
  947. self.Append(item)
  948. class DoubleStageButton(wx.Button):
  949. def __init__(self, parent, labels, bitmaps, bitmap_pos=wx.TOP, *args, **kwargs):
  950. super(DoubleStageButton, self).__init__(parent, *args, **kwargs)
  951. assert isinstance(labels, tuple) and isinstance(bitmaps, tuple)
  952. assert len(labels) == 2
  953. assert len(bitmaps) == 0 or len(bitmaps) == 2
  954. self.labels = labels
  955. self.bitmaps = bitmaps
  956. self.bitmap_pos = bitmap_pos
  957. self._stage = 0
  958. self._set_layout()
  959. def _set_layout(self):
  960. self.SetLabel(self.labels[self._stage])
  961. if len(self.bitmaps):
  962. self.SetBitmap(self.bitmaps[self._stage], self.bitmap_pos)
  963. def change_stage(self):
  964. self._stage = 0 if self._stage else 1
  965. self._set_layout()
  966. def set_stage(self, new_stage):
  967. assert new_stage == 0 or new_stage == 1
  968. self._stage = new_stage
  969. self._set_layout()
  970. class ButtonsChoiceDialog(wx.Dialog):
  971. if os.name == "nt":
  972. STYLE = wx.DEFAULT_DIALOG_STYLE
  973. else:
  974. STYLE = wx.DEFAULT_DIALOG_STYLE | wx.MAXIMIZE_BOX
  975. BORDER = 10
  976. def __init__(self, parent, choices, message, *args, **kwargs):
  977. super(ButtonsChoiceDialog, self).__init__(parent, wx.ID_ANY, *args, style=self.STYLE, **kwargs)
  978. buttons = []
  979. # Create components
  980. panel = wx.Panel(self)
  981. info_bmp = wx.ArtProvider.GetBitmap(wx.ART_INFORMATION, wx.ART_MESSAGE_BOX)
  982. info_icon = wx.StaticBitmap(panel, wx.ID_ANY, info_bmp)
  983. msg_text = wx.StaticText(panel, wx.ID_ANY, message)
  984. buttons.append(wx.Button(panel, wx.ID_CANCEL, _("Cancel")))
  985. for index, label in enumerate(choices):
  986. buttons.append(wx.Button(panel, index + 1, label))
  987. # Get the maximum button width & height
  988. max_width = max_height = -1
  989. for button in buttons:
  990. button_width, button_height = button.GetSize()
  991. if button_width > max_width:
  992. max_width = button_width
  993. if button_height > max_height:
  994. max_height = button_height
  995. max_width += 10
  996. # Set buttons width & bind events
  997. for button in buttons:
  998. if button != buttons[0]:
  999. button.SetMinSize((max_width, max_height))
  1000. else:
  1001. # On Close button change only the height
  1002. button.SetMinSize((-1, max_height))
  1003. button.Bind(wx.EVT_BUTTON, self._on_close)
  1004. # Set sizers
  1005. vertical_sizer = wx.BoxSizer(wx.VERTICAL)
  1006. message_sizer = wx.BoxSizer(wx.HORIZONTAL)
  1007. message_sizer.Add(info_icon)
  1008. message_sizer.AddSpacer((10, 10))
  1009. message_sizer.Add(msg_text, flag=wx.EXPAND)
  1010. vertical_sizer.Add(message_sizer, 1, wx.ALL, border=self.BORDER)
  1011. buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
  1012. for button in buttons[1:]:
  1013. buttons_sizer.Add(button)
  1014. buttons_sizer.AddSpacer((5, -1))
  1015. buttons_sizer.AddSpacer((-1, -1), 1)
  1016. buttons_sizer.Add(buttons[0], flag=wx.ALIGN_RIGHT)
  1017. vertical_sizer.Add(buttons_sizer, flag=wx.EXPAND | wx.ALL, border=self.BORDER)
  1018. panel.SetSizer(vertical_sizer)
  1019. width, height = panel.GetBestSize()
  1020. self.SetSize((width, height * 1.3))
  1021. self.Center()
  1022. def _on_close(self, event):
  1023. self.EndModal(event.GetEventObject().GetId())
  1024. class ButtonsGroup(object):
  1025. WIDTH = 0
  1026. HEIGHT = 1
  1027. def __init__(self, buttons_list=None, squared=False):
  1028. if buttons_list is None:
  1029. self._buttons_list = []
  1030. else:
  1031. self._buttons_list = buttons_list
  1032. self._squared = squared
  1033. def set_size(self, size):
  1034. assert len(size) == 2
  1035. width, height = size
  1036. if width == -1:
  1037. for button in self._buttons_list:
  1038. cur_width = button.GetSize()[self.WIDTH]
  1039. if cur_width > width:
  1040. width = cur_width
  1041. if height == -1:
  1042. for button in self._buttons_list:
  1043. cur_height = button.GetSize()[self.HEIGHT]
  1044. if cur_height > height:
  1045. height = cur_height
  1046. if self._squared:
  1047. width = height = (width if width > height else height)
  1048. for button in self._buttons_list:
  1049. button.SetMinSize((width, height))
  1050. def create_sizer(self, orient=wx.HORIZONTAL, space=-1):
  1051. box_sizer = wx.BoxSizer(orient)
  1052. for button in self._buttons_list:
  1053. box_sizer.Add(button)
  1054. if space != -1:
  1055. box_sizer.AddSpacer((space, space))
  1056. return box_sizer
  1057. def bind_event(self, event, event_handler):
  1058. for button in self._buttons_list:
  1059. button.Bind(event, event_handler)
  1060. def disable_all(self):
  1061. for button in self._buttons_list:
  1062. button.Enable(False)
  1063. def enable_all(self):
  1064. for button in self._buttons_list:
  1065. button.Enable(True)
  1066. def add(self, button):
  1067. self._buttons_list.append(button)
  1068. class ShutdownDialog(wx.Dialog):
  1069. if os.name == "nt":
  1070. STYLE = wx.DEFAULT_DIALOG_STYLE
  1071. else:
  1072. STYLE = wx.DEFAULT_DIALOG_STYLE | wx.MAXIMIZE_BOX
  1073. TIMER_INTERVAL = 1000 # milliseconds
  1074. BORDER = 10
  1075. def __init__(self, parent, timeout, message, *args, **kwargs):
  1076. super(ShutdownDialog, self).__init__(parent, wx.ID_ANY, *args, style=self.STYLE, **kwargs)
  1077. assert timeout > 0
  1078. self.timeout = timeout
  1079. self.message = message
  1080. # Create components
  1081. panel = wx.Panel(self)
  1082. info_bmp = wx.ArtProvider.GetBitmap(wx.ART_INFORMATION, wx.ART_MESSAGE_BOX)
  1083. info_icon = wx.StaticBitmap(panel, wx.ID_ANY, info_bmp)
  1084. self.msg_text = msg_text = wx.StaticText(panel, wx.ID_ANY, self._get_message())
  1085. ok_button = wx.Button(panel, wx.ID_OK, _("OK"))
  1086. cancel_button = wx.Button(panel, wx.ID_CANCEL, _("Cancel"))
  1087. # Set layout
  1088. vertical_sizer = wx.BoxSizer(wx.VERTICAL)
  1089. message_sizer = wx.BoxSizer(wx.HORIZONTAL)
  1090. message_sizer.Add(info_icon)
  1091. message_sizer.AddSpacer((10, 10))
  1092. message_sizer.Add(msg_text, flag=wx.EXPAND)
  1093. vertical_sizer.Add(message_sizer, 1, wx.ALL, border=self.BORDER)
  1094. buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
  1095. buttons_sizer.Add(ok_button)
  1096. buttons_sizer.AddSpacer((5, -1))
  1097. buttons_sizer.Add(cancel_button)
  1098. vertical_sizer.Add(buttons_sizer, flag=wx.ALIGN_RIGHT | wx.ALL, border=self.BORDER)
  1099. panel.SetSizer(vertical_sizer)
  1100. width, height = panel.GetBestSize()
  1101. self.SetSize((width * 1.3, height * 1.3))
  1102. self.Center()
  1103. # Set up timer
  1104. self.timer = wx.Timer(self)
  1105. self.Bind(wx.EVT_TIMER, self._on_timer, self.timer)
  1106. self.timer.Start(self.TIMER_INTERVAL)
  1107. def _get_message(self):
  1108. return self.message.format(self.timeout)
  1109. def _on_timer(self, event):
  1110. self.timeout -= 1
  1111. self.msg_text.SetLabel(self._get_message())
  1112. if self.timeout <= 0:
  1113. self.EndModal(wx.ID_OK)
  1114. def Destroy(self):
  1115. self.timer.Stop()
  1116. return super(ShutdownDialog, self).Destroy()