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.
1484 lines
51 KiB
1484 lines
51 KiB
#!/usr/bin/env python2
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""Youtubedlg module responsible for the main app window. """
|
|
|
|
from __future__ import unicode_literals
|
|
|
|
import os
|
|
import gettext
|
|
|
|
import wx
|
|
from wx.lib.pubsub import setuparg1 #NOTE Should remove deprecated
|
|
from wx.lib.pubsub import pub as Publisher
|
|
|
|
from wx.lib.mixins.listctrl import ListCtrlAutoWidthMixin
|
|
|
|
from .parsers import OptionsParser
|
|
|
|
from .optionsframe import (
|
|
OptionsFrame,
|
|
LogGUI
|
|
)
|
|
|
|
from .updatemanager import (
|
|
UPDATE_PUB_TOPIC,
|
|
UpdateThread
|
|
)
|
|
|
|
from .downloadmanager import (
|
|
MANAGER_PUB_TOPIC,
|
|
WORKER_PUB_TOPIC,
|
|
DownloadManager,
|
|
DownloadList,
|
|
DownloadItem
|
|
)
|
|
|
|
from .utils import (
|
|
YOUTUBEDL_BIN,
|
|
get_pixmaps_dir,
|
|
get_icon_file,
|
|
shutdown_sys,
|
|
remove_file,
|
|
open_file,
|
|
get_time
|
|
)
|
|
|
|
from .widgets import CustomComboBox
|
|
|
|
from .formats import (
|
|
DEFAULT_FORMATS,
|
|
VIDEO_FORMATS,
|
|
AUDIO_FORMATS,
|
|
FORMATS
|
|
)
|
|
|
|
from .info import (
|
|
__descriptionfull__,
|
|
__licensefull__,
|
|
__projecturl__,
|
|
__appname__,
|
|
__author__
|
|
)
|
|
|
|
from .version import __version__
|
|
|
|
|
|
class MainFrame(wx.Frame):
|
|
|
|
"""Main window class.
|
|
|
|
This class is responsible for creating the main app window
|
|
and binding the events.
|
|
|
|
Attributes:
|
|
FRAMES_MIN_SIZE (tuple): Tuple that contains the minumum width, height of the frame.
|
|
|
|
Labels area (strings): Strings for the widgets labels.
|
|
|
|
STATUSLIST_COLUMNS (dict): Python dictionary which holds informations
|
|
about the wxListCtrl columns. For more informations read the
|
|
comments above the STATUSLIST_COLUMNS declaration.
|
|
|
|
Args:
|
|
opt_manager (optionsmanager.OptionsManager): Object responsible for
|
|
handling the settings.
|
|
|
|
log_manager (logmanager.LogManager): Object responsible for handling
|
|
the log stuff.
|
|
|
|
parent (wx.Window): Frame parent.
|
|
|
|
"""
|
|
|
|
FRAMES_MIN_SIZE = (560, 360)
|
|
|
|
# Labels area
|
|
URLS_LABEL = _("Enter URLs below")
|
|
UPDATE_LABEL = _("Update")
|
|
OPTIONS_LABEL = _("Options")
|
|
STOP_LABEL = _("Stop")
|
|
INFO_LABEL = _("Info")
|
|
WELCOME_MSG = _("Welcome")
|
|
WARNING_LABEL = _("Warning")
|
|
|
|
ADD_LABEL = _("Add")
|
|
DOWNLOAD_LIST_LABEL = _("Download list")
|
|
DELETE_LABEL = _("Delete")
|
|
PLAY_LABEL = _("Play")
|
|
UP_LABEL = _("Up")
|
|
DOWN_LABEL = _("Down")
|
|
RELOAD_LABEL = _("Reload")
|
|
PAUSE_LABEL = _("Pause")
|
|
START_LABEL = _("Start")
|
|
ABOUT_LABEL = _("About")
|
|
VIEWLOG_LABEL = _("View Log")
|
|
|
|
SUCC_REPORT_MSG = _("Successfully downloaded {0} URL(s) in {1} "
|
|
"day(s) {2} hour(s) {3} minute(s) {4} second(s)")
|
|
DL_COMPLETED_MSG = _("Downloads completed")
|
|
URL_REPORT_MSG = _("Total Progress: {0:.1f}% | Queued ({1}) Paused ({2}) Active ({3}) Completed ({4}) Error ({5})")
|
|
CLOSING_MSG = _("Stopping downloads")
|
|
CLOSED_MSG = _("Downloads stopped")
|
|
PROVIDE_URL_MSG = _("You need to provide at least one URL")
|
|
DOWNLOAD_STARTED = _("Downloads started")
|
|
CHOOSE_DIRECTORY = _("Choose Directory")
|
|
|
|
DOWNLOAD_ACTIVE = _("Download in progress. Please wait for all downloads to complete")
|
|
UPDATE_ACTIVE = _("Update already in progress")
|
|
|
|
UPDATING_MSG = _("Downloading latest youtube-dl. Please wait...")
|
|
UPDATE_ERR_MSG = _("Youtube-dl download failed [{0}]")
|
|
UPDATE_SUCC_MSG = _("Successfully downloaded youtube-dl")
|
|
|
|
OPEN_DIR_ERR = _("Unable to open directory: '{dir}'. "
|
|
"The specified path does not exist")
|
|
SHUTDOWN_ERR = _("Error while shutting down. "
|
|
"Make sure you typed the correct password")
|
|
SHUTDOWN_MSG = _("Shutting down system")
|
|
|
|
VIDEO_LABEL = _("Title")
|
|
EXTENSION_LABEL = _("Extension")
|
|
SIZE_LABEL = _("Size")
|
|
PERCENT_LABEL = _("Percent")
|
|
ETA_LABEL = _("ETA")
|
|
SPEED_LABEL = _("Speed")
|
|
STATUS_LABEL = _("Status")
|
|
#################################
|
|
|
|
# STATUSLIST_COLUMNS
|
|
#
|
|
# Dictionary which contains the columns for the wxListCtrl widget.
|
|
# Each key represents a column and holds informations about itself.
|
|
# Structure informations:
|
|
# column_key: (column_number, column_label, minimum_width, is_resizable)
|
|
#
|
|
STATUSLIST_COLUMNS = {
|
|
'filename': (0, VIDEO_LABEL, 150, True),
|
|
'extension': (1, EXTENSION_LABEL, 60, False),
|
|
'filesize': (2, SIZE_LABEL, 80, False),
|
|
'percent': (3, PERCENT_LABEL, 65, False),
|
|
'eta': (4, ETA_LABEL, 45, False),
|
|
'speed': (5, SPEED_LABEL, 90, False),
|
|
'status': (6, STATUS_LABEL, 160, False)
|
|
}
|
|
|
|
def __init__(self, opt_manager, log_manager, parent=None):
|
|
super(MainFrame, self).__init__(parent, wx.ID_ANY, __appname__, size=opt_manager.options["main_win_size"])
|
|
self.opt_manager = opt_manager
|
|
self.log_manager = log_manager
|
|
self.download_manager = None
|
|
self.update_thread = None
|
|
self.app_icon = None #REFACTOR Get and set on __init__.py
|
|
|
|
self._download_list = DownloadList()
|
|
|
|
# Set up youtube-dl options parser
|
|
self._options_parser = OptionsParser()
|
|
|
|
# Get the pixmaps directory
|
|
self._pixmaps_path = get_pixmaps_dir()
|
|
|
|
# Set the Timer
|
|
self._app_timer = wx.Timer(self)
|
|
|
|
# Set the app icon
|
|
app_icon_path = get_icon_file()
|
|
if app_icon_path is not None:
|
|
self.app_icon = wx.Icon(app_icon_path, wx.BITMAP_TYPE_PNG)
|
|
self.SetIcon(self.app_icon)
|
|
|
|
bitmap_data = (
|
|
("down", "arrow_down_32px.png"),
|
|
("up", "arrow_up_32px.png"),
|
|
("play", "camera_32px.png"),
|
|
("start", "cloud_download_32px.png"),
|
|
("delete", "delete_32px.png"),
|
|
("folder", "folder_32px.png"),
|
|
("pause", "pause_32px.png"),
|
|
("resume", "play_arrow_32px.png"),
|
|
("reload", "reload_32px.png"),
|
|
("settings", "settings_20px.png"),
|
|
("stop", "stop_32px.png")
|
|
)
|
|
|
|
self._bitmaps = {}
|
|
|
|
for item in bitmap_data:
|
|
target, name = item
|
|
self._bitmaps[target] = wx.Bitmap(os.path.join(self._pixmaps_path, name))
|
|
|
|
# Set the data for all the wx.Button items
|
|
# name, label, size, event_handler
|
|
buttons_data = (
|
|
("delete", self.DELETE_LABEL, (-1, -1), self._on_delete, wx.BitmapButton),
|
|
("play", self.PLAY_LABEL, (-1, -1), self._on_play, wx.BitmapButton),
|
|
("up", self.UP_LABEL, (-1, -1), self._on_arrow_up, wx.BitmapButton),
|
|
("down", self.DOWN_LABEL, (-1, -1), self._on_arrow_down, wx.BitmapButton),
|
|
("reload", self.RELOAD_LABEL, (-1, -1), self._on_reload, wx.BitmapButton),
|
|
("pause", self.PAUSE_LABEL, (-1, -1), self._on_pause, wx.BitmapButton),
|
|
("start", self.START_LABEL, (-1, -1), self._on_start, wx.BitmapButton),
|
|
("savepath", "...", (35, -1), self._on_savepath, wx.Button),
|
|
("add", self.ADD_LABEL, (-1, -1), self._on_add, wx.Button)
|
|
)
|
|
|
|
# Set the data for the settings menu item
|
|
# label, event_handler
|
|
settings_menu_data = (
|
|
(self.OPTIONS_LABEL, self._on_options),
|
|
(self.UPDATE_LABEL, self._on_update),
|
|
(self.VIEWLOG_LABEL, self._on_viewlog),
|
|
(self.ABOUT_LABEL, self._on_about)
|
|
)
|
|
|
|
statuslist_menu_data = (
|
|
(_("Get URL"), self._on_geturl),
|
|
(_("Get command"), self._on_getcmd),
|
|
(_("Open destination"), self._on_open_dest),
|
|
(_("Re-enter"), self._on_reenter)
|
|
)
|
|
|
|
# Create options frame
|
|
self._options_frame = OptionsFrame(self)
|
|
|
|
# Create frame components
|
|
self._panel = wx.Panel(self)
|
|
|
|
self._url_text = self._create_statictext(self.URLS_LABEL)
|
|
|
|
#REFACTOR Move to buttons_data
|
|
self._settings_button = self._create_bitmap_button(self._bitmaps["settings"], (30, 30), self._on_settings)
|
|
|
|
self._url_list = self._create_textctrl(wx.TE_MULTILINE | wx.TE_DONTWRAP, self._on_urllist_edit)
|
|
|
|
self._folder_icon = self._create_static_bitmap(self._bitmaps["folder"], self._on_open_path)
|
|
|
|
self._path_combobox = ExtComboBox(self._panel, 5, style=wx.CB_READONLY)
|
|
self._videoformat_combobox = CustomComboBox(self._panel, style=wx.CB_READONLY)
|
|
|
|
self._download_text = self._create_statictext(self.DOWNLOAD_LIST_LABEL)
|
|
self._status_list = ListCtrl(self.STATUSLIST_COLUMNS,
|
|
parent=self._panel,
|
|
style=wx.LC_REPORT | wx.LC_HRULES | wx.LC_VRULES)
|
|
|
|
# Dictionary to store all the buttons
|
|
self._buttons = {}
|
|
|
|
for item in buttons_data:
|
|
name, label, size, evt_handler, parent = item
|
|
|
|
button = parent(self._panel, size=size)
|
|
|
|
if parent == wx.Button:
|
|
button.SetLabel(label)
|
|
elif parent == wx.BitmapButton:
|
|
button.SetToolTip(wx.ToolTip(label))
|
|
|
|
if name in self._bitmaps:
|
|
button.SetBitmap(self._bitmaps[name], wx.TOP)
|
|
|
|
if evt_handler is not None:
|
|
button.Bind(wx.EVT_BUTTON, evt_handler)
|
|
|
|
self._buttons[name] = button
|
|
|
|
self._status_bar = self.CreateStatusBar()
|
|
|
|
# Create extra components
|
|
self._settings_menu = self._create_menu_item(settings_menu_data)
|
|
self._statuslist_menu = self._create_menu_item(statuslist_menu_data)
|
|
|
|
# Overwrite the menu hover event to avoid changing the statusbar
|
|
self.Bind(wx.EVT_MENU_HIGHLIGHT, lambda event: None)
|
|
|
|
# Bind extra events
|
|
self.Bind(wx.EVT_LIST_ITEM_RIGHT_CLICK, self._on_statuslist_right_click, self._status_list)
|
|
self.Bind(wx.EVT_TEXT, self._update_savepath, self._path_combobox)
|
|
self.Bind(wx.EVT_LIST_ITEM_SELECTED, self._update_pause_button, self._status_list)
|
|
self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self._update_pause_button, self._status_list)
|
|
self.Bind(wx.EVT_CLOSE, self._on_close)
|
|
self.Bind(wx.EVT_TIMER, self._on_timer, self._app_timer)
|
|
|
|
self._videoformat_combobox.Bind(wx.EVT_COMBOBOX, self._update_videoformat)
|
|
|
|
# Set threads wxCallAfter handlers
|
|
self._set_publisher(self._update_handler, UPDATE_PUB_TOPIC)
|
|
self._set_publisher(self._download_worker_handler, WORKER_PUB_TOPIC)
|
|
self._set_publisher(self._download_manager_handler, MANAGER_PUB_TOPIC)
|
|
|
|
# Set up extra stuff
|
|
self.Center()
|
|
self.SetMinSize(self.FRAMES_MIN_SIZE)
|
|
|
|
self._status_bar_write(self.WELCOME_MSG)
|
|
|
|
self._update_videoformat_combobox()
|
|
self._path_combobox.LoadMultiple(self.opt_manager.options["save_path_dirs"])
|
|
self._path_combobox.SetValue(self.opt_manager.options["save_path"])
|
|
|
|
self._set_layout()
|
|
|
|
self._url_list.SetFocus()
|
|
|
|
def _create_menu_item(self, items):
|
|
menu = wx.Menu()
|
|
|
|
for item in items:
|
|
label, evt_handler = item
|
|
menu_item = menu.Append(-1, label)
|
|
|
|
menu.Bind(wx.EVT_MENU, evt_handler, menu_item)
|
|
|
|
return menu
|
|
|
|
def _on_statuslist_right_click(self, event):
|
|
selected = event.GetIndex()
|
|
|
|
if selected != -1:
|
|
self._status_list.deselect_all()
|
|
self._status_list.Select(selected, on=1)
|
|
|
|
self.PopupMenu(self._statuslist_menu)
|
|
|
|
def _on_reenter(self, event):
|
|
selected = self._status_list.get_selected()
|
|
|
|
if selected != -1:
|
|
object_id = self._status_list.GetItemData(selected)
|
|
download_item = self._download_list.get_item(object_id)
|
|
|
|
if download_item.stage != "Active":
|
|
self._status_list.remove_row(selected)
|
|
self._download_list.remove(object_id)
|
|
|
|
options = self._options_parser.parse(self.opt_manager.options)
|
|
|
|
download_item = DownloadItem(download_item.url, options)
|
|
download_item.path = self.opt_manager.options["save_path"]
|
|
|
|
if not self._download_list.has_item(download_item.object_id):
|
|
self._status_list.bind_item(download_item)
|
|
self._download_list.insert(download_item)
|
|
|
|
def reset(self):
|
|
self._update_videoformat_combobox()
|
|
self._path_combobox.LoadMultiple(self.opt_manager.options["save_path_dirs"])
|
|
self._path_combobox.SetValue(self.opt_manager.options["save_path"])
|
|
|
|
def _on_open_dest(self, event):
|
|
selected = self._status_list.get_selected()
|
|
|
|
if selected != -1:
|
|
object_id = self._status_list.GetItemData(selected)
|
|
download_item = self._download_list.get_item(object_id)
|
|
|
|
if download_item.path:
|
|
open_file(download_item.path)
|
|
|
|
def _on_open_path(self, event):
|
|
open_file(self._path_combobox.GetValue())
|
|
|
|
def _on_geturl(self, event):
|
|
selected = self._status_list.get_selected()
|
|
|
|
if selected != -1:
|
|
object_id = self._status_list.GetItemData(selected)
|
|
download_item = self._download_list.get_item(object_id)
|
|
|
|
url = download_item.url
|
|
|
|
if not wx.TheClipboard.IsOpened():
|
|
clipdata = wx.TextDataObject()
|
|
clipdata.SetText(url)
|
|
wx.TheClipboard.Open()
|
|
wx.TheClipboard.SetData(clipdata)
|
|
wx.TheClipboard.Close()
|
|
|
|
def _on_getcmd(self, event):
|
|
selected = self._status_list.get_selected()
|
|
|
|
if selected != -1:
|
|
object_id = self._status_list.GetItemData(selected)
|
|
download_item = self._download_list.get_item(object_id)
|
|
|
|
cmd = [YOUTUBEDL_BIN] + download_item.options + [download_item.url]
|
|
cmd = " ".join(cmd)
|
|
|
|
if not wx.TheClipboard.IsOpened():
|
|
clipdata = wx.TextDataObject()
|
|
clipdata.SetText(cmd)
|
|
wx.TheClipboard.Open()
|
|
wx.TheClipboard.SetData(clipdata)
|
|
wx.TheClipboard.Close()
|
|
|
|
def _on_timer(self, event):
|
|
total_percentage = 0.0
|
|
queued = paused = active = completed = error = 0
|
|
|
|
for item in self._download_list.get_items():
|
|
if item.stage == "Queued":
|
|
queued += 1
|
|
if item.stage == "Paused":
|
|
paused += 1
|
|
if item.stage == "Active":
|
|
active += 1
|
|
total_percentage += float(item.progress_stats["percent"].split('%')[0])
|
|
if item.stage == "Completed":
|
|
completed += 1
|
|
if item.stage == "Error":
|
|
error += 1
|
|
|
|
# REFACTOR Store percentage as float in the DownloadItem?
|
|
# REFACTOR DownloadList keep track for each item stage?
|
|
|
|
total_percentage += completed * 100.0 + error * 100.0
|
|
total_percentage /= active + completed + error + queued
|
|
|
|
msg = self.URL_REPORT_MSG.format(total_percentage, queued, paused, active, completed, error)
|
|
self._status_bar_write(msg)
|
|
|
|
def _update_pause_button(self, event):
|
|
selected_rows = self._status_list.get_all_selected()
|
|
|
|
label = _("Pause")
|
|
bitmap = self._bitmaps["pause"]
|
|
|
|
for row in selected_rows:
|
|
object_id = self._status_list.GetItemData(row)
|
|
download_item = self._download_list.get_item(object_id)
|
|
|
|
if download_item.stage == "Paused":
|
|
# If we find one or more items in Paused
|
|
# state set the button functionality to resume
|
|
label = _("Resume")
|
|
bitmap = self._bitmaps["resume"]
|
|
break
|
|
|
|
self._buttons["pause"].SetLabel(label)
|
|
self._buttons["pause"].SetToolTip(wx.ToolTip(label))
|
|
self._buttons["pause"].SetBitmap(bitmap, wx.TOP)
|
|
|
|
def _update_videoformat_combobox(self):
|
|
self._videoformat_combobox.Clear()
|
|
|
|
self._videoformat_combobox.add_items(list(DEFAULT_FORMATS.values()), False)
|
|
|
|
vformats = []
|
|
for vformat in self.opt_manager.options["selected_video_formats"]:
|
|
vformats.append(FORMATS[vformat])
|
|
|
|
aformats = []
|
|
for aformat in self.opt_manager.options["selected_audio_formats"]:
|
|
aformats.append(FORMATS[aformat])
|
|
|
|
if vformats:
|
|
self._videoformat_combobox.add_header(_("Video"))
|
|
self._videoformat_combobox.add_items(vformats)
|
|
|
|
if aformats:
|
|
self._videoformat_combobox.add_header(_("Audio"))
|
|
self._videoformat_combobox.add_items(aformats)
|
|
|
|
current_index = self._videoformat_combobox.FindString(FORMATS[self.opt_manager.options["selected_format"]])
|
|
|
|
if current_index == wx.NOT_FOUND:
|
|
self._videoformat_combobox.SetSelection(0)
|
|
else:
|
|
self._videoformat_combobox.SetSelection(current_index)
|
|
|
|
self._update_videoformat(None)
|
|
|
|
def _update_videoformat(self, event):
|
|
self.opt_manager.options["selected_format"] = selected_format = FORMATS[self._videoformat_combobox.GetValue()]
|
|
|
|
if selected_format in VIDEO_FORMATS:
|
|
self.opt_manager.options["video_format"] = selected_format
|
|
self.opt_manager.options["audio_format"] = "" #NOTE Set to default value, check parsers.py
|
|
elif selected_format in AUDIO_FORMATS:
|
|
self.opt_manager.options["video_format"] = DEFAULT_FORMATS[_("default")]
|
|
self.opt_manager.options["audio_format"] = selected_format
|
|
else:
|
|
self.opt_manager.options["video_format"] = DEFAULT_FORMATS[_("default")]
|
|
self.opt_manager.options["audio_format"] = ""
|
|
|
|
def _update_savepath(self, event):
|
|
self.opt_manager.options["save_path"] = self._path_combobox.GetValue()
|
|
|
|
def _on_delete(self, event):
|
|
index = self._status_list.get_next_selected()
|
|
|
|
if index == -1:
|
|
dlg = ButtonsChoiceDialog(self, [_("Remove all"), _("Remove completed")], _("No items selected. Please pick an action"), _("Delete"))
|
|
ret_code = dlg.ShowModal()
|
|
dlg.Destroy()
|
|
|
|
#REFACTOR Maybe add this functionality directly to DownloadList?
|
|
if ret_code == 1:
|
|
for ditem in self._download_list.get_items():
|
|
if ditem.stage != "Active":
|
|
self._status_list.remove_row(self._download_list.index(ditem.object_id))
|
|
self._download_list.remove(ditem.object_id)
|
|
|
|
if ret_code == 2:
|
|
for ditem in self._download_list.get_items():
|
|
if ditem.stage == "Completed":
|
|
self._status_list.remove_row(self._download_list.index(ditem.object_id))
|
|
self._download_list.remove(ditem.object_id)
|
|
else:
|
|
if self.opt_manager.options["confirm_deletion"]:
|
|
dlg = wx.MessageDialog(self, _("Are you sure you want to remove selected items?"), _("Delete"), wx.YES_NO | wx.ICON_QUESTION)
|
|
result = dlg.ShowModal() == wx.ID_YES
|
|
dlg.Destroy()
|
|
else:
|
|
result = True
|
|
|
|
if result:
|
|
while index >= 0:
|
|
object_id = self._status_list.GetItemData(index)
|
|
selected_download_item = self._download_list.get_item(object_id)
|
|
|
|
if selected_download_item.stage == "Active":
|
|
self._create_popup(_("Item is active, cannot remove"), self.WARNING_LABEL, wx.OK | wx.ICON_EXCLAMATION)
|
|
else:
|
|
#if selected_download_item.stage == "Completed":
|
|
#dlg = wx.MessageDialog(self, "Do you want to remove the files associated with this item?", "Remove files", wx.YES_NO | wx.ICON_QUESTION)
|
|
|
|
#result = dlg.ShowModal() == wx.ID_YES
|
|
#dlg.Destroy()
|
|
|
|
#if result:
|
|
#for cur_file in selected_download_item.get_files():
|
|
#remove_file(cur_file)
|
|
|
|
self._status_list.remove_row(index)
|
|
self._download_list.remove(object_id)
|
|
index -= 1
|
|
|
|
index = self._status_list.get_next_selected(index)
|
|
|
|
self._update_pause_button(None)
|
|
|
|
def _on_play(self, event):
|
|
selected_rows = self._status_list.get_all_selected()
|
|
|
|
if selected_rows:
|
|
for selected_row in selected_rows:
|
|
object_id = self._status_list.GetItemData(selected_row)
|
|
selected_download_item = self._download_list.get_item(object_id)
|
|
|
|
if selected_download_item.stage == "Completed":
|
|
if selected_download_item.filenames:
|
|
filename = selected_download_item.get_files()[-1]
|
|
open_file(filename)
|
|
else:
|
|
self._create_popup(_("Item is not completed"), self.INFO_LABEL, wx.OK | wx.ICON_INFORMATION)
|
|
|
|
def _on_arrow_up(self, event):
|
|
index = self._status_list.get_next_selected()
|
|
|
|
if index != -1:
|
|
while index >= 0:
|
|
object_id = self._status_list.GetItemData(index)
|
|
download_item = self._download_list.get_item(object_id)
|
|
|
|
new_index = index - 1
|
|
if new_index < 0:
|
|
new_index = 0
|
|
|
|
if not self._status_list.IsSelected(new_index):
|
|
self._download_list.move_up(object_id)
|
|
self._status_list.move_item_up(index)
|
|
self._status_list._update_from_item(new_index, download_item)
|
|
|
|
index = self._status_list.get_next_selected(index)
|
|
|
|
def _on_arrow_down(self, event):
|
|
index = self._status_list.get_next_selected(reverse=True)
|
|
|
|
if index != -1:
|
|
while index >= 0:
|
|
object_id = self._status_list.GetItemData(index)
|
|
download_item = self._download_list.get_item(object_id)
|
|
|
|
new_index = index + 1
|
|
if new_index >= self._status_list.GetItemCount():
|
|
new_index = self._status_list.GetItemCount() - 1
|
|
|
|
if not self._status_list.IsSelected(new_index):
|
|
self._download_list.move_down(object_id)
|
|
self._status_list.move_item_down(index)
|
|
self._status_list._update_from_item(new_index, download_item)
|
|
|
|
index = self._status_list.get_next_selected(index, True)
|
|
|
|
def _on_reload(self, event):
|
|
selected_rows = self._status_list.get_all_selected()
|
|
|
|
if not selected_rows:
|
|
for index, item in enumerate(self._download_list.get_items()):
|
|
if item.stage in ("Paused", "Completed", "Error"):
|
|
# Store the old savepath because reset is going to remove it
|
|
savepath = item.path
|
|
item.reset()
|
|
item.path = savepath
|
|
self._status_list._update_from_item(index, item)
|
|
else:
|
|
for selected_row in selected_rows:
|
|
object_id = self._status_list.GetItemData(selected_row)
|
|
item = self._download_list.get_item(object_id)
|
|
|
|
if item.stage in ("Paused", "Completed", "Error"):
|
|
# Store the old savepath because reset is going to remove it
|
|
savepath = item.path
|
|
item.reset()
|
|
item.path = savepath
|
|
self._status_list._update_from_item(selected_row, item)
|
|
|
|
self._update_pause_button(None)
|
|
|
|
def _on_pause(self, event):
|
|
selected_rows = self._status_list.get_all_selected()
|
|
|
|
if selected_rows:
|
|
#REFACTOR Use DoubleStageButton for this and check stage
|
|
if self._buttons["pause"].GetLabel() == _("Pause"):
|
|
new_state = "Paused"
|
|
else:
|
|
new_state = "Queued"
|
|
|
|
for selected_row in selected_rows:
|
|
object_id = self._status_list.GetItemData(selected_row)
|
|
download_item = self._download_list.get_item(object_id)
|
|
|
|
if download_item.stage == "Queued" or download_item.stage == "Paused":
|
|
self._download_list.change_stage(object_id, new_state)
|
|
|
|
self._status_list._update_from_item(selected_row, download_item)
|
|
|
|
self._update_pause_button(None)
|
|
|
|
def _on_start(self, event):
|
|
if self.update_thread is not None and self.update_thread.is_alive():
|
|
self._create_popup(_("Update in progress. Please wait for the update to complete"),
|
|
self.WARNING_LABEL,
|
|
wx.OK | wx.ICON_EXCLAMATION)
|
|
else:
|
|
if self.download_manager is None:
|
|
self._start_download()
|
|
else:
|
|
self.download_manager.stop_downloads()
|
|
|
|
def _on_savepath(self, event):
|
|
dlg = wx.DirDialog(self, self.CHOOSE_DIRECTORY, self._path_combobox.GetStringSelection(), wx.DD_CHANGE_DIR)
|
|
|
|
if dlg.ShowModal() == wx.ID_OK:
|
|
path = dlg.GetPath()
|
|
|
|
self._path_combobox.Append(path)
|
|
self._path_combobox.SetValue(path)
|
|
self._update_savepath(None)
|
|
|
|
dlg.Destroy()
|
|
|
|
def _on_add(self, event):
|
|
urls = self._get_urls()
|
|
|
|
if not urls:
|
|
self._create_popup(self.PROVIDE_URL_MSG,
|
|
self.WARNING_LABEL,
|
|
wx.OK | wx.ICON_EXCLAMATION)
|
|
else:
|
|
self._url_list.Clear()
|
|
options = self._options_parser.parse(self.opt_manager.options)
|
|
|
|
for url in urls:
|
|
download_item = DownloadItem(url, options)
|
|
download_item.path = self.opt_manager.options["save_path"]
|
|
|
|
if not self._download_list.has_item(download_item.object_id):
|
|
self._status_list.bind_item(download_item)
|
|
self._download_list.insert(download_item)
|
|
|
|
|
|
def _on_settings(self, event):
|
|
event_object_pos = event.EventObject.GetPosition()
|
|
event_object_height = event.EventObject.GetSize()[1]
|
|
event_object_pos = (event_object_pos[0], event_object_pos[1] + event_object_height)
|
|
self.PopupMenu(self._settings_menu, event_object_pos)
|
|
|
|
def _on_viewlog(self, event):
|
|
log_window = LogGUI(self)
|
|
log_window.load(self.log_manager.log_file)
|
|
log_window.Show()
|
|
|
|
def _on_about(self, event):
|
|
info = wx.AboutDialogInfo()
|
|
|
|
if self.app_icon is not None:
|
|
info.SetIcon(self.app_icon)
|
|
|
|
info.SetName(__appname__)
|
|
info.SetVersion(__version__)
|
|
info.SetDescription(__descriptionfull__)
|
|
info.SetWebSite(__projecturl__)
|
|
info.SetLicense(__licensefull__)
|
|
info.AddDeveloper(__author__)
|
|
|
|
wx.AboutBox(info)
|
|
|
|
def _set_publisher(self, handler, topic):
|
|
"""Sets a handler for the given topic.
|
|
|
|
Args:
|
|
handler (function): Can be any function with one parameter
|
|
the message that the caller sends.
|
|
|
|
topic (string): Can be any string that identifies the caller.
|
|
You can bind multiple handlers on the same topic or
|
|
multiple topics on the same handler.
|
|
|
|
"""
|
|
Publisher.subscribe(handler, topic)
|
|
|
|
def _create_statictext(self, label):
|
|
return wx.StaticText(self._panel, label=label)
|
|
|
|
def _create_bitmap_button(self, icon, size=(-1, -1), handler=None):
|
|
button = wx.BitmapButton(self._panel, bitmap=icon, size=size, style=wx.NO_BORDER)
|
|
|
|
if handler is not None:
|
|
button.Bind(wx.EVT_BUTTON, handler)
|
|
|
|
return button
|
|
|
|
def _create_static_bitmap(self, icon, event_handler=None):
|
|
static_bitmap = wx.StaticBitmap(self._panel, bitmap=icon)
|
|
|
|
if event_handler is not None:
|
|
static_bitmap.Bind(wx.EVT_LEFT_DCLICK, event_handler)
|
|
|
|
return static_bitmap
|
|
|
|
def _create_textctrl(self, style=None, event_handler=None):
|
|
if style is None:
|
|
textctrl = wx.TextCtrl(self._panel)
|
|
else:
|
|
textctrl = wx.TextCtrl(self._panel, style=style)
|
|
|
|
if event_handler is not None:
|
|
textctrl.Bind(wx.EVT_TEXT_PASTE, event_handler)
|
|
textctrl.Bind(wx.EVT_MIDDLE_DOWN, event_handler)
|
|
|
|
if os.name == 'nt':
|
|
# Enable CTRL+A on Windows
|
|
def win_ctrla_eventhandler(event):
|
|
if event.GetKeyCode() == wx.WXK_CONTROL_A:
|
|
event.GetEventObject().SelectAll()
|
|
|
|
event.Skip()
|
|
|
|
textctrl.Bind(wx.EVT_CHAR, win_ctrla_eventhandler)
|
|
|
|
return textctrl
|
|
|
|
def _create_popup(self, text, title, style):
|
|
wx.MessageBox(text, title, style)
|
|
|
|
def _set_layout(self):
|
|
"""Sets the layout of the main window. """
|
|
main_sizer = wx.BoxSizer()
|
|
panel_sizer = wx.BoxSizer(wx.VERTICAL)
|
|
|
|
top_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
top_sizer.Add(self._url_text, 0, wx.ALIGN_BOTTOM | wx.BOTTOM, 5)
|
|
top_sizer.AddSpacer((-1, -1), 1)
|
|
top_sizer.Add(self._settings_button)
|
|
panel_sizer.Add(top_sizer, 0, wx.EXPAND)
|
|
|
|
panel_sizer.Add(self._url_list, 1, wx.EXPAND)
|
|
|
|
mid_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
mid_sizer.Add(self._folder_icon)
|
|
mid_sizer.AddSpacer((3, -1))
|
|
mid_sizer.Add(self._path_combobox, 2, wx.ALIGN_CENTER_VERTICAL)
|
|
mid_sizer.AddSpacer((5, -1))
|
|
mid_sizer.Add(self._buttons["savepath"], flag=wx.ALIGN_CENTER_VERTICAL)
|
|
mid_sizer.AddSpacer((10, -1), 1)
|
|
mid_sizer.Add(self._videoformat_combobox, 1, wx.ALIGN_CENTER_VERTICAL)
|
|
mid_sizer.AddSpacer((5, -1))
|
|
mid_sizer.Add(self._buttons["add"], flag=wx.ALIGN_CENTER_VERTICAL)
|
|
panel_sizer.Add(mid_sizer, 0, wx.EXPAND | wx.ALL, 10)
|
|
|
|
panel_sizer.Add(self._download_text, 0, wx.BOTTOM, 5)
|
|
panel_sizer.Add(self._status_list, 2, wx.EXPAND)
|
|
|
|
bottom_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
bottom_sizer.Add(self._buttons["delete"])
|
|
bottom_sizer.AddSpacer((5, -1))
|
|
bottom_sizer.Add(self._buttons["play"])
|
|
bottom_sizer.AddSpacer((5, -1))
|
|
bottom_sizer.Add(self._buttons["up"])
|
|
bottom_sizer.AddSpacer((5, -1))
|
|
bottom_sizer.Add(self._buttons["down"])
|
|
bottom_sizer.AddSpacer((5, -1))
|
|
bottom_sizer.Add(self._buttons["reload"])
|
|
bottom_sizer.AddSpacer((5, -1))
|
|
bottom_sizer.Add(self._buttons["pause"])
|
|
bottom_sizer.AddSpacer((10, -1), 1)
|
|
bottom_sizer.Add(self._buttons["start"])
|
|
panel_sizer.Add(bottom_sizer, 0, wx.EXPAND | wx.TOP, 5)
|
|
|
|
main_sizer.Add(panel_sizer, 1, wx.ALL | wx.EXPAND, 10)
|
|
self._panel.SetSizer(main_sizer)
|
|
|
|
self._panel.Layout()
|
|
|
|
def _update_youtubedl(self):
|
|
"""Update youtube-dl binary to the latest version. """
|
|
if self.download_manager is not None and self.download_manager.is_alive():
|
|
self._create_popup(self.DOWNLOAD_ACTIVE,
|
|
self.WARNING_LABEL,
|
|
wx.OK | wx.ICON_EXCLAMATION)
|
|
elif self.update_thread is not None and self.update_thread.is_alive():
|
|
self._create_popup(self.UPDATE_ACTIVE,
|
|
self.INFO_LABEL,
|
|
wx.OK | wx.ICON_INFORMATION)
|
|
else:
|
|
self.update_thread = UpdateThread(self.opt_manager.options['youtubedl_path'])
|
|
|
|
def _status_bar_write(self, msg):
|
|
"""Display msg in the status bar. """
|
|
self._status_bar.SetStatusText(msg)
|
|
|
|
def _reset_widgets(self):
|
|
"""Resets GUI widgets after update or download process. """
|
|
self._buttons["start"].SetLabel(_("Start"))
|
|
self._buttons["start"].SetToolTip(wx.ToolTip(_("Start")))
|
|
self._buttons["start"].SetBitmap(self._bitmaps["start"], wx.TOP)
|
|
|
|
def _print_stats(self):
|
|
"""Display download stats in the status bar. """
|
|
suc_downloads = self.download_manager.successful
|
|
dtime = get_time(self.download_manager.time_it_took)
|
|
|
|
msg = self.SUCC_REPORT_MSG.format(suc_downloads,
|
|
dtime['days'],
|
|
dtime['hours'],
|
|
dtime['minutes'],
|
|
dtime['seconds'])
|
|
|
|
self._status_bar_write(msg)
|
|
|
|
def _after_download(self):
|
|
"""Run tasks after download process has been completed.
|
|
|
|
Note:
|
|
Here you can add any tasks you want to run after the
|
|
download process has been completed.
|
|
|
|
"""
|
|
if self.opt_manager.options['shutdown']:
|
|
dlg = ShutdownDialog(self, 60, _("Shutting down in {0} second(s)"), _("Shutdown"))
|
|
result = dlg.ShowModal() == wx.ID_OK
|
|
dlg.Destroy()
|
|
|
|
if result:
|
|
self.opt_manager.save_to_file()
|
|
success = shutdown_sys(self.opt_manager.options['sudo_password'])
|
|
|
|
if success:
|
|
self._status_bar_write(self.SHUTDOWN_MSG)
|
|
else:
|
|
self._status_bar_write(self.SHUTDOWN_ERR)
|
|
else:
|
|
if self.opt_manager.options["show_completion_popup"]:
|
|
self._create_popup(self.DL_COMPLETED_MSG, self.INFO_LABEL, wx.OK | wx.ICON_INFORMATION)
|
|
|
|
def _download_worker_handler(self, msg):
|
|
"""downloadmanager.Worker thread handler.
|
|
|
|
Handles messages from the Worker thread.
|
|
|
|
Args:
|
|
See downloadmanager.Worker _talk_to_gui() method.
|
|
|
|
"""
|
|
signal, data = msg.data
|
|
|
|
download_item = self._download_list.get_item(data["index"])
|
|
download_item.update_stats(data)
|
|
row = self._download_list.index(data["index"])
|
|
|
|
self._status_list._update_from_item(row, download_item)
|
|
|
|
def _download_manager_handler(self, msg):
|
|
"""downloadmanager.DownloadManager thread handler.
|
|
|
|
Handles messages from the DownloadManager thread.
|
|
|
|
Args:
|
|
See downloadmanager.DownloadManager _talk_to_gui() method.
|
|
|
|
"""
|
|
data = msg.data
|
|
|
|
if data == 'finished':
|
|
self._print_stats()
|
|
self._reset_widgets()
|
|
self.download_manager = None
|
|
self._app_timer.Stop()
|
|
self._after_download()
|
|
elif data == 'closed':
|
|
self._status_bar_write(self.CLOSED_MSG)
|
|
self._reset_widgets()
|
|
self.download_manager = None
|
|
self._app_timer.Stop()
|
|
elif data == 'closing':
|
|
self._status_bar_write(self.CLOSING_MSG)
|
|
elif data == 'report_active':
|
|
pass
|
|
#NOTE Remove from here and downloadmanager
|
|
#since now we have the wx.Timer to check progress
|
|
|
|
def _update_handler(self, msg):
|
|
"""updatemanager.UpdateThread thread handler.
|
|
|
|
Handles messages from the UpdateThread thread.
|
|
|
|
Args:
|
|
See updatemanager.UpdateThread _talk_to_gui() method.
|
|
|
|
"""
|
|
data = msg.data
|
|
|
|
if data[0] == 'download':
|
|
self._status_bar_write(self.UPDATING_MSG)
|
|
elif data[0] == 'error':
|
|
self._status_bar_write(self.UPDATE_ERR_MSG.format(data[1]))
|
|
elif data[0] == 'correct':
|
|
self._status_bar_write(self.UPDATE_SUCC_MSG)
|
|
else:
|
|
self._reset_widgets()
|
|
self.update_thread = None
|
|
|
|
def _get_urls(self):
|
|
"""Returns urls list. """
|
|
return [line for line in self._url_list.GetValue().split('\n') if line]
|
|
|
|
def _start_download(self):
|
|
if self._status_list.is_empty():
|
|
self._create_popup(_("No items to download"),
|
|
self.WARNING_LABEL,
|
|
wx.OK | wx.ICON_EXCLAMATION)
|
|
else:
|
|
self._app_timer.Start(100)
|
|
self.download_manager = DownloadManager(self._download_list, self.opt_manager, self.log_manager)
|
|
|
|
self._status_bar_write(self.DOWNLOAD_STARTED)
|
|
self._buttons["start"].SetLabel(self.STOP_LABEL)
|
|
self._buttons["start"].SetToolTip(wx.ToolTip(self.STOP_LABEL))
|
|
self._buttons["start"].SetBitmap(self._bitmaps["stop"], wx.TOP)
|
|
|
|
def _paste_from_clipboard(self):
|
|
"""Paste the content of the clipboard to the self._url_list widget.
|
|
It also adds a new line at the end of the data if not exist.
|
|
|
|
"""
|
|
if not wx.TheClipboard.IsOpened():
|
|
|
|
if wx.TheClipboard.Open():
|
|
if wx.TheClipboard.IsSupported(wx.DataFormat(wx.DF_TEXT)):
|
|
|
|
data = wx.TextDataObject()
|
|
wx.TheClipboard.GetData(data)
|
|
|
|
data = data.GetText()
|
|
|
|
if data[-1] != '\n':
|
|
data += '\n'
|
|
|
|
self._url_list.WriteText(data)
|
|
|
|
wx.TheClipboard.Close()
|
|
|
|
def _on_urllist_edit(self, event):
|
|
"""Event handler of the self._url_list widget.
|
|
|
|
This method is triggered when the users pastes text into
|
|
the URLs list either by using CTRL+V or by using the middle
|
|
click of the mouse.
|
|
|
|
"""
|
|
if event.GetEventType() == wx.EVT_TEXT_PASTE.typeId:
|
|
self._paste_from_clipboard()
|
|
else:
|
|
wx.TheClipboard.UsePrimarySelection(True)
|
|
self._paste_from_clipboard()
|
|
wx.TheClipboard.UsePrimarySelection(False)
|
|
|
|
def _on_update(self, event):
|
|
"""Event handler of the self._update_btn widget.
|
|
|
|
This method is used when the update button is pressed to start
|
|
the update process.
|
|
|
|
Note:
|
|
Currently there is not way to stop the update process.
|
|
|
|
"""
|
|
self._update_youtubedl()
|
|
|
|
def _on_options(self, event):
|
|
"""Event handler of the self._options_btn widget.
|
|
|
|
This method is used when the options button is pressed to show
|
|
the options window.
|
|
|
|
"""
|
|
self._options_frame.load_all_options()
|
|
self._options_frame.Show()
|
|
|
|
def _on_close(self, event):
|
|
"""Event handler for the wx.EVT_CLOSE event.
|
|
|
|
This method is used when the user tries to close the program
|
|
to save the options and make sure that the download & update
|
|
processes are not running.
|
|
|
|
"""
|
|
if self.opt_manager.options["confirm_exit"]:
|
|
dlg = wx.MessageDialog(self, _("Are you sure you want to exit?"), _("Exit"), wx.YES_NO | wx.ICON_QUESTION)
|
|
|
|
result = dlg.ShowModal() == wx.ID_YES
|
|
dlg.Destroy()
|
|
else:
|
|
result = True
|
|
|
|
if result:
|
|
self.close()
|
|
|
|
def close(self):
|
|
if self.download_manager is not None:
|
|
self.download_manager.stop_downloads()
|
|
self.download_manager.join()
|
|
|
|
if self.update_thread is not None:
|
|
self.update_thread.join()
|
|
|
|
# Store main-options frame size
|
|
self.opt_manager.options['main_win_size'] = self.GetSize()
|
|
self.opt_manager.options['opts_win_size'] = self._options_frame.GetSize()
|
|
|
|
self.opt_manager.options["save_path_dirs"] = self._path_combobox.GetStrings()
|
|
|
|
self._options_frame.save_all_options()
|
|
self.opt_manager.save_to_file()
|
|
|
|
self.Destroy()
|
|
|
|
|
|
class ListCtrl(wx.ListCtrl, ListCtrlAutoWidthMixin):
|
|
|
|
"""Custom ListCtrl widget.
|
|
|
|
Args:
|
|
columns (dict): See MainFrame class STATUSLIST_COLUMNS attribute.
|
|
|
|
"""
|
|
|
|
def __init__(self, columns, *args, **kwargs):
|
|
super(ListCtrl, self).__init__(*args, **kwargs)
|
|
ListCtrlAutoWidthMixin.__init__(self)
|
|
self.columns = columns
|
|
self._list_index = 0
|
|
self._url_list = set()
|
|
self._set_columns()
|
|
|
|
def remove_row(self, row_number):
|
|
self.DeleteItem(row_number)
|
|
self._list_index -= 1
|
|
|
|
def move_item_up(self, row_number):
|
|
self._move_item(row_number, row_number - 1)
|
|
|
|
def move_item_down(self, row_number):
|
|
self._move_item(row_number, row_number + 1)
|
|
|
|
def _move_item(self, cur_row, new_row):
|
|
self.Freeze()
|
|
item = self.GetItem(cur_row)
|
|
self.DeleteItem(cur_row)
|
|
|
|
item.SetId(new_row)
|
|
self.InsertItem(item)
|
|
|
|
self.Select(new_row)
|
|
self.Thaw()
|
|
|
|
def has_url(self, url):
|
|
"""Returns True if the url is aleady in the ListCtrl else False.
|
|
|
|
Args:
|
|
url (string): URL string.
|
|
|
|
"""
|
|
return url in self._url_list
|
|
|
|
def bind_item(self, download_item):
|
|
self.InsertStringItem(self._list_index, download_item.url)
|
|
|
|
self.SetItemData(self._list_index, download_item.object_id)
|
|
|
|
self._update_from_item(self._list_index, download_item)
|
|
|
|
self._list_index += 1
|
|
|
|
def _update_from_item(self, row, download_item):
|
|
progress_stats = download_item.progress_stats
|
|
|
|
for key in self.columns:
|
|
column = self.columns[key][0]
|
|
|
|
if key == "status" and progress_stats["playlist_index"]:
|
|
# Not the best place but we build the playlist status here
|
|
status = "{0} {1}/{2}".format(progress_stats["status"],
|
|
progress_stats["playlist_index"],
|
|
progress_stats["playlist_size"])
|
|
|
|
self.SetStringItem(row, column, status)
|
|
else:
|
|
self.SetStringItem(row, column, progress_stats[key])
|
|
|
|
def clear(self):
|
|
"""Clear the ListCtrl widget & reset self._list_index and
|
|
self._url_list. """
|
|
self.DeleteAllItems()
|
|
self._list_index = 0
|
|
self._url_list = set()
|
|
|
|
def is_empty(self):
|
|
"""Returns True if the list is empty else False. """
|
|
return self._list_index == 0
|
|
|
|
def get_selected(self):
|
|
return self.GetNextItem(-1, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED)
|
|
|
|
def get_all_selected(self):
|
|
return [index for index in xrange(self._list_index) if self.IsSelected(index)]
|
|
|
|
def deselect_all(self):
|
|
for index in xrange(self._list_index):
|
|
self.Select(index, on=0)
|
|
|
|
def get_next_selected(self, start=-1, reverse=False):
|
|
if start == -1:
|
|
start = self._list_index - 1 if reverse else 0
|
|
else:
|
|
# start from next item
|
|
if reverse:
|
|
start -= 1
|
|
else:
|
|
start += 1
|
|
|
|
end = -1 if reverse else self._list_index
|
|
step = -1 if reverse else 1
|
|
|
|
for index in xrange(start, end, step):
|
|
if self.IsSelected(index):
|
|
return index
|
|
|
|
return -1
|
|
|
|
def _set_columns(self):
|
|
"""Initializes ListCtrl columns.
|
|
See MainFrame STATUSLIST_COLUMNS attribute for more info. """
|
|
for column_item in sorted(self.columns.values()):
|
|
self.InsertColumn(column_item[0], column_item[1], width=wx.LIST_AUTOSIZE_USEHEADER)
|
|
|
|
# If the column width obtained from wxLIST_AUTOSIZE_USEHEADER
|
|
# is smaller than the minimum allowed column width
|
|
# then set the column width to the minumum allowed size
|
|
if self.GetColumnWidth(column_item[0]) < column_item[2]:
|
|
self.SetColumnWidth(column_item[0], column_item[2])
|
|
|
|
# Set auto-resize if enabled
|
|
if column_item[3]:
|
|
self.setResizeColumn(column_item[0])
|
|
|
|
# REFACTOR Extra widgets below should move to other module with widgets
|
|
|
|
class ExtComboBox(wx.ComboBox):
|
|
|
|
def __init__(self, parent, max_items=-1, *args, **kwargs):
|
|
super(ExtComboBox, self).__init__(parent, *args, **kwargs)
|
|
|
|
assert max_items > 0 or max_items == -1
|
|
self.max_items = max_items
|
|
|
|
def Append(self, new_value):
|
|
if self.FindString(new_value) == wx.NOT_FOUND:
|
|
super(ExtComboBox, self).Append(new_value)
|
|
|
|
if self.max_items != -1 and self.GetCount() > self.max_items:
|
|
self.SetItems(self.GetStrings()[1:])
|
|
|
|
def SetValue(self, new_value):
|
|
if self.FindString(new_value) == wx.NOT_FOUND:
|
|
self.Append(new_value)
|
|
|
|
self.SetSelection(self.FindString(new_value))
|
|
|
|
def LoadMultiple(self, items_list):
|
|
for item in items_list:
|
|
self.Append(item)
|
|
|
|
|
|
class DoubleStageButton(wx.Button):
|
|
|
|
def __init__(self, parent, labels, bitmaps, bitmap_pos=wx.TOP, *args, **kwargs):
|
|
super(DoubleStageButton, self).__init__(parent, *args, **kwargs)
|
|
|
|
assert isinstance(labels, tuple) and isinstance(bitmaps, tuple)
|
|
assert len(labels) == 2
|
|
assert len(bitmaps) == 0 or len(bitmaps) == 2
|
|
|
|
self.labels = labels
|
|
self.bitmaps = bitmaps
|
|
self.bitmap_pos = bitmap_pos
|
|
|
|
self._stage = 0
|
|
self._set_layout()
|
|
|
|
def _set_layout(self):
|
|
self.SetLabel(self.labels[self._stage])
|
|
|
|
if len(self.bitmaps):
|
|
self.SetBitmap(self.bitmaps[self._stage], self.bitmap_pos)
|
|
|
|
def change_stage(self):
|
|
self._stage = 0 if self._stage else 1
|
|
self._set_layout()
|
|
|
|
def set_stage(self, new_stage):
|
|
assert new_stage == 0 or new_stage == 1
|
|
|
|
self._stage = new_stage
|
|
self._set_layout()
|
|
|
|
|
|
class ButtonsChoiceDialog(wx.Dialog):
|
|
|
|
if os.name == "nt":
|
|
STYLE = wx.DEFAULT_DIALOG_STYLE
|
|
else:
|
|
STYLE = wx.DEFAULT_DIALOG_STYLE | wx.MAXIMIZE_BOX
|
|
|
|
BORDER = 10
|
|
|
|
def __init__(self, parent, choices, message, *args, **kwargs):
|
|
super(ButtonsChoiceDialog, self).__init__(parent, wx.ID_ANY, *args, style=self.STYLE, **kwargs)
|
|
|
|
buttons = []
|
|
|
|
# Create components
|
|
panel = wx.Panel(self)
|
|
|
|
info_bmp = wx.ArtProvider.GetBitmap(wx.ART_INFORMATION, wx.ART_MESSAGE_BOX)
|
|
|
|
info_icon = wx.StaticBitmap(panel, wx.ID_ANY, info_bmp)
|
|
msg_text = wx.StaticText(panel, wx.ID_ANY, message)
|
|
|
|
buttons.append(wx.Button(panel, wx.ID_CANCEL, _("Cancel")))
|
|
|
|
for index, label in enumerate(choices):
|
|
buttons.append(wx.Button(panel, index + 1, label))
|
|
|
|
# Get the maximum button width & height
|
|
max_width = max_height = -1
|
|
|
|
for button in buttons:
|
|
button_width, button_height = button.GetSize()
|
|
|
|
if button_width > max_width:
|
|
max_width = button_width
|
|
|
|
if button_height > max_height:
|
|
max_height = button_height
|
|
|
|
max_width += 10
|
|
|
|
# Set buttons width & bind events
|
|
for button in buttons:
|
|
if button != buttons[0]:
|
|
button.SetMinSize((max_width, max_height))
|
|
else:
|
|
# On Close button change only the height
|
|
button.SetMinSize((-1, max_height))
|
|
|
|
button.Bind(wx.EVT_BUTTON, self._on_close)
|
|
|
|
# Set sizers
|
|
vertical_sizer = wx.BoxSizer(wx.VERTICAL)
|
|
|
|
message_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
message_sizer.Add(info_icon)
|
|
message_sizer.AddSpacer((10, 10))
|
|
message_sizer.Add(msg_text, flag=wx.EXPAND)
|
|
|
|
vertical_sizer.Add(message_sizer, 1, wx.ALL, border=self.BORDER)
|
|
|
|
buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
for button in buttons[1:]:
|
|
buttons_sizer.Add(button)
|
|
buttons_sizer.AddSpacer((5, -1))
|
|
|
|
buttons_sizer.AddSpacer((-1, -1), 1)
|
|
buttons_sizer.Add(buttons[0], flag=wx.ALIGN_RIGHT)
|
|
vertical_sizer.Add(buttons_sizer, flag=wx.EXPAND | wx.ALL, border=self.BORDER)
|
|
|
|
panel.SetSizer(vertical_sizer)
|
|
|
|
width, height = panel.GetBestSize()
|
|
self.SetSize((width, height * 1.3))
|
|
|
|
self.Center()
|
|
|
|
def _on_close(self, event):
|
|
self.EndModal(event.GetEventObject().GetId())
|
|
|
|
|
|
class ButtonsGroup(object):
|
|
|
|
WIDTH = 0
|
|
HEIGHT = 1
|
|
|
|
def __init__(self, buttons_list=None, squared=False):
|
|
if buttons_list is None:
|
|
self._buttons_list = []
|
|
else:
|
|
self._buttons_list = buttons_list
|
|
|
|
self._squared = squared
|
|
|
|
def set_size(self, size):
|
|
assert len(size) == 2
|
|
|
|
width, height = size
|
|
|
|
if width == -1:
|
|
for button in self._buttons_list:
|
|
cur_width = button.GetSize()[self.WIDTH]
|
|
|
|
if cur_width > width:
|
|
width = cur_width
|
|
|
|
if height == -1:
|
|
for button in self._buttons_list:
|
|
cur_height = button.GetSize()[self.HEIGHT]
|
|
|
|
if cur_height > height:
|
|
height = cur_height
|
|
|
|
if self._squared:
|
|
width = height = (width if width > height else height)
|
|
|
|
for button in self._buttons_list:
|
|
button.SetMinSize((width, height))
|
|
|
|
def create_sizer(self, orient=wx.HORIZONTAL, space=-1):
|
|
box_sizer = wx.BoxSizer(orient)
|
|
|
|
for button in self._buttons_list:
|
|
box_sizer.Add(button)
|
|
|
|
if space != -1:
|
|
box_sizer.AddSpacer((space, space))
|
|
|
|
return box_sizer
|
|
|
|
def bind_event(self, event, event_handler):
|
|
for button in self._buttons_list:
|
|
button.Bind(event, event_handler)
|
|
|
|
def disable_all(self):
|
|
for button in self._buttons_list:
|
|
button.Enable(False)
|
|
|
|
def enable_all(self):
|
|
for button in self._buttons_list:
|
|
button.Enable(True)
|
|
|
|
def add(self, button):
|
|
self._buttons_list.append(button)
|
|
|
|
|
|
class ShutdownDialog(wx.Dialog):
|
|
|
|
if os.name == "nt":
|
|
STYLE = wx.DEFAULT_DIALOG_STYLE
|
|
else:
|
|
STYLE = wx.DEFAULT_DIALOG_STYLE | wx.MAXIMIZE_BOX
|
|
|
|
TIMER_INTERVAL = 1000 # milliseconds
|
|
|
|
BORDER = 10
|
|
|
|
def __init__(self, parent, timeout, message, *args, **kwargs):
|
|
super(ShutdownDialog, self).__init__(parent, wx.ID_ANY, *args, style=self.STYLE, **kwargs)
|
|
assert timeout > 0
|
|
|
|
self.timeout = timeout
|
|
self.message = message
|
|
|
|
# Create components
|
|
panel = wx.Panel(self)
|
|
|
|
info_bmp = wx.ArtProvider.GetBitmap(wx.ART_INFORMATION, wx.ART_MESSAGE_BOX)
|
|
info_icon = wx.StaticBitmap(panel, wx.ID_ANY, info_bmp)
|
|
|
|
self.msg_text = msg_text = wx.StaticText(panel, wx.ID_ANY, self._get_message())
|
|
ok_button = wx.Button(panel, wx.ID_OK, _("OK"))
|
|
cancel_button = wx.Button(panel, wx.ID_CANCEL, _("Cancel"))
|
|
|
|
# Set layout
|
|
vertical_sizer = wx.BoxSizer(wx.VERTICAL)
|
|
|
|
message_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
message_sizer.Add(info_icon)
|
|
message_sizer.AddSpacer((10, 10))
|
|
message_sizer.Add(msg_text, flag=wx.EXPAND)
|
|
|
|
vertical_sizer.Add(message_sizer, 1, wx.ALL, border=self.BORDER)
|
|
|
|
buttons_sizer = wx.BoxSizer(wx.HORIZONTAL)
|
|
buttons_sizer.Add(ok_button)
|
|
buttons_sizer.AddSpacer((5, -1))
|
|
buttons_sizer.Add(cancel_button)
|
|
|
|
vertical_sizer.Add(buttons_sizer, flag=wx.ALIGN_RIGHT | wx.ALL, border=self.BORDER)
|
|
|
|
panel.SetSizer(vertical_sizer)
|
|
|
|
width, height = panel.GetBestSize()
|
|
self.SetSize((width * 1.3, height * 1.3))
|
|
|
|
self.Center()
|
|
|
|
# Set up timer
|
|
self.timer = wx.Timer(self)
|
|
self.Bind(wx.EVT_TIMER, self._on_timer, self.timer)
|
|
self.timer.Start(self.TIMER_INTERVAL)
|
|
|
|
def _get_message(self):
|
|
return self.message.format(self.timeout)
|
|
|
|
def _on_timer(self, event):
|
|
self.timeout -= 1
|
|
self.msg_text.SetLabel(self._get_message())
|
|
|
|
if self.timeout <= 0:
|
|
self.EndModal(wx.ID_OK)
|
|
|
|
def Destroy(self):
|
|
self.timer.Stop()
|
|
return super(ShutdownDialog, self).Destroy()
|