tartube/tartube/wizwin.py

1788 lines
50 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019-2021 A S Lewis
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""Wizard window classes."""
# Import Gtk modules
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject, Gdk, GdkPixbuf
# Import other modules
import os
import re
# Import our modules
import __main__
import formats
import mainapp
import utils
# Use same gettext translations
from mainapp import _
# Classes
class GenericWizWin(Gtk.Window):
"""Generic Python class for windows in which the user can modify various
system settings.
Unlike classes inheriting from GenericEditWin, widgets are arranged in
pages, with only page visible at a time. The user can cycle through pages
(when allowed) by clicking the 'Next' or 'Previous' buttons.
Modifications are usually applied immediately, but the code provides an
.apply_changes() function for anything that should be applied, when the
user has cycled through all the pages.
"""
# Standard class methods
# def __init__(): # Provided by child object
# Public class methods
def is_duplicate(self, app_obj):
"""Called by self.__init__.
Don't open this wizard window, if another wizard window (if any class)
is already open.
Args:
app_obj (mainapp.TartubeApp): The main application object
Return values:
True if a duplicate is found, False if not
"""
if app_obj.main_win_obj.wiz_win_obj:
# Duplicate found
app_obj.main_win_obj.wiz_win_obj.present()
return True
else:
# Not a duplicate
return False
def setup(self):
"""Called by self.__init__().
Sets up the wizard window when it opens.
"""
# Set the default window size
self.set_default_size(
self.app_obj.config_win_width,
self.app_obj.config_win_height,
)
# Set the window's Gtk icon list
self.set_icon_list(self.app_obj.main_win_obj.win_pixbuf_list)
# Set up main widgets
self.setup_grid()
self.setup_button_strip()
# Set up the first page (a widget layout on self.inner_grid)
self.setup_page()
# Procedure complete
self.show_all()
# Inform the main window of this window's birth (so that Tartube
# doesn't allow an operation to start until all configuration windows
# have closed)
self.app_obj.main_win_obj.add_child_window(self)
# Add a callback so we can inform the main window of this window's
# destruction
self.connect('destroy', self.close)
def setup_grid(self):
"""Called by self.setup().
Sets up two Gtk.Grids. The first one contains an inner grid, and a
button strip.
"""
box = Gtk.Box()
self.add(box)
box.set_border_width(self.spacing_size)
self.grid = Gtk.Grid()
box.add(self.grid)
self.grid.set_row_spacing(self.spacing_size)
self.grid.set_column_spacing(self.spacing_size)
frame = Gtk.Frame()
self.grid.attach(frame, 0, 0, 1, 1)
frame.set_hexpand(True)
frame.set_vexpand(True)
self.vbox = Gtk.VBox()
frame.add(self.vbox)
self.vbox.set_border_width(self.spacing_size * 2)
self.inner_grid = Gtk.Grid()
self.vbox.pack_start(self.inner_grid, True, False, 0)
self.inner_grid.set_row_spacing(self.spacing_size)
self.inner_grid.set_column_spacing(self.spacing_size)
def setup_button_strip(self):
"""Called by self.setup().
Creates a strip of buttons at the bottom of the window: a 'cancel'
button on the left, and 'next'/'previous' buttons on the right.
The window is closed by using the 'cancel' button, or by clicking the
'next' button on the last page.
"""
hbox = Gtk.HBox()
self.grid.attach(hbox, 0, 1, 1, 1)
# 'Cancel' button
self.cancel_button = Gtk.Button(_('Cancel'))
hbox.pack_start(self.cancel_button, False, False, 0)
self.cancel_button.get_child().set_width_chars(10)
self.cancel_button.set_tooltip_text(
_('Close this window without completing it'),
);
self.cancel_button.connect('clicked', self.on_button_cancel_clicked)
# 'Next' button
self.next_button = Gtk.Button(_('Next'))
hbox.pack_end(self.next_button, False, False, 0)
self.next_button.get_child().set_width_chars(10)
self.next_button.set_tooltip_text(_('Go to the next page'));
self.next_button.connect('clicked', self.on_button_next_clicked)
# 'Previous' button
self.prev_button = Gtk.Button(_('Previous'))
hbox.pack_end(self.prev_button, False, False, self.spacing_size)
self.prev_button.get_child().set_width_chars(10)
self.prev_button.set_tooltip_text(_('Go to the previous page'));
self.prev_button.connect('clicked', self.on_button_prev_clicked)
def setup_page(self):
"""Called initially by self.setup(), then by .on_button_next_clicked()
or .on_button_prev_clicked().
Sets up the page specified by self.current_page.
"""
index = self.current_page
page_func = self.page_list[self.current_page]
if page_func is None:
# Emergency fallback
index = 0
page_func = self.page_list[0]
if len(self.page_list) <= 1:
self.next_button.set_sensitive(False)
self.prev_button.set_sensitive(False)
elif index == 0:
self.next_button.set_sensitive(True)
self.prev_button.set_sensitive(False)
else:
self.next_button.set_sensitive(True)
self.prev_button.set_sensitive(True)
if index >= len(self.page_list) - 1:
self.next_button.set_label(_('OK'))
else:
self.next_button.set_label(_('Next'))
self.next_button.get_child().set_width_chars(10)
# Replace the inner grid...
self.vbox.remove(self.inner_grid)
self.inner_grid = Gtk.Grid()
self.vbox.pack_start(self.inner_grid, True, False, 0)
self.inner_grid.set_row_spacing(self.spacing_size)
self.inner_grid.set_column_spacing(self.spacing_size)
# ...and then refill it, with the widget layout for the new page
method = getattr(self, page_func)
method()
self.show_all()
def convert_next_button(self):
"""Can be called by anything.
Converts the 'Next' to an 'OK' button, and sensitises it.
Should usually be called from the last page, when the code is ready to
let the window finish the wizard.
"""
self.next_button.set_label(_('Finish'))
self.next_button.get_child().set_width_chars(10)
self.next_button.set_sensitive(True)
def apply_changes(self):
"""Called by self.on_button_next_clicked().
The default function is empty. Any changes that need to be applied,
when the wizard window closes, can be applied in a function with this
name.
"""
pass
def reset_changes(self):
"""Called by self.on_button_cancel_clicked().
The default function is empty. Any changes that need to be applied,
when the wizard window is closed by clicking the 'Cancel' button, can
be applied in a function with this name.
"""
pass
def close(self, widget):
"""Called from callback in self.setup().
Inform the main application that this window is closing.
Args:
widget (GenericWizWin): This window
"""
self.app_obj.main_win_obj.del_child_window(self)
# (Add widgets)
def add_image(self, image_path, x, y, wid, hei):
"""Called by various functions in the child wizard window.
Adds a Gtk.Image to self.inner_grid, with more than the usual padding.
Args:
image_path (str): Full path to the image file to load
x, y, wid, hei (int): Position on the grid at which the widget is
placed
Returns:
The Gtk.Box containing the image
"""
box = Gtk.Box()
self.inner_grid.attach(box, x, y, wid, hei)
box.set_border_width(self.spacing_size * 2)
image = Gtk.Image()
box.add(image)
image.set_from_pixbuf(
self.app_obj.file_manager_obj.load_to_pixbuf(image_path),
)
image.set_hexpand(True)
return box
def add_label(self, text, x, y, wid, hei):
"""Called by various functions in the child wizard window.
Adds a Gtk.Label to self.inner_grid.
Args:
text (str): Pango markup displayed in the label
x, y, wid, hei (int): Position on the grid at which the widget is
placed
Returns:
The label widget created
"""
label = Gtk.Label()
self.inner_grid.attach(label, x, y, wid, hei)
label.set_markup(text)
label.set_hexpand(True)
label.set_alignment(0.5, 0.5)
return label
def add_empty_label(self, x, y, wid, hei):
"""Called by various functions in the child wizard window.
Adds an empty Gtk.Label (for spacing) to self.inner_grid.
Args:
x, y, wid, hei (int): Position on the grid at which the widget is
placed
Returns:
The label widget created
"""
label = Gtk.Label()
self.inner_grid.attach(label, x, y, wid, hei)
# (Using a space, rather than an empty string, better preserves the
# intended layout)
label.set_text(' ')
label.set_hexpand(True)
label.set_alignment(0.5, 0.5)
return label
def add_radiobutton(self, prev_button, text, x, y, wid, hei):
"""Called by various functions in the child wizard window.
Adds a Gtk.RadioButton to self.inner_grid.
Args:
prev_button (Gtk.RadioButton or None): When this is the first
radio button in the group, None. Otherwise, the previous
radio button in the group. Use of this argument links the radio
buttons together, ensuring that only one of them can be active
at any time
text (string or None): The text to display in the radiobutton's
label. No label is used if 'text' is an empty string or None
x, y, wid, hei (int): Position on the grid at which the widget is
placed
Returns:
The radiobutton widget created
"""
radiobutton = Gtk.RadioButton.new_from_widget(prev_button)
self.inner_grid.attach(radiobutton, x, y, wid, hei)
radiobutton.set_hexpand(True)
if text is not None and text != '':
radiobutton.set_label(text)
return radiobutton
def add_textview(self, x, y, wid, hei):
"""Called by various functions in the child wizard window.
Adds a Gtk.TextView to self.inner_grid.
Args:
x, y, wid, hei (int): Position on the grid at which the widget is
placed
Returns:
The scroller, textview and textbuffer widgets created
"""
frame = Gtk.Frame()
self.inner_grid.attach(frame, x, y, wid, hei)
scrolled = Gtk.ScrolledWindow()
frame.add(scrolled)
scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
scrolled.set_size_request(-1, 150)
textview = Gtk.TextView()
scrolled.add(textview)
textview.set_wrap_mode(Gtk.WrapMode.WORD)
textview.set_editable(False)
textview.set_cursor_visible(False)
return scrolled, textview, textview.get_buffer()
# Callback class methods
def on_button_cancel_clicked(self, button):
"""Called from a callback in self.setup_button_strip().
Closes the wizard window without applying any changes.
Args:
button (Gtk.Button): The widget clicked
"""
self.reset_changes()
self.destroy()
def on_button_next_clicked(self, button):
"""Called from a callback in self.setup_button_strip().
Goes to the the next page, or (if already on the last page) applies
any changes waiting to be applied, then closes the window.
Args:
button (Gtk.Button): The widget clicked
"""
if self.current_page >= (len(self.page_list) - 1):
self.apply_changes()
self.destroy()
else:
self.current_page += 1
self.setup_page()
def on_button_prev_clicked(self, button):
"""Called from a callback in self.setup_button_strip().
Goes to the previous page.
Args:
button (Gtk.Button): The widget clicked
"""
if self.current_page > 0:
self.current_page -= 1
self.setup_page()
class SetupWizWin(GenericWizWin):
"""Python class for a 'wizard window' displayed when Tartube starts and
no config file can be loaded (meaning that this is a new installation).
Args:
app_obj (mainapp.TartubeApp): The main application object
"""
# Standard class methods
def __init__(self, app_obj):
Gtk.Window.__init__(self, title=_('Tartube setup'))
if self.is_duplicate(app_obj):
return
# IV list - class objects
# -----------------------
# The mainapp.TartubeApp object
self.app_obj = app_obj
# IV list - Gtk widgets
# ---------------------
self.grid = None # Gtk.Grid
self.vbox = None # Gtk.VBox
self.inner_grid = None # Gtk.Grid
self.cancel_button = None # Gtk.Button
self.next_button = None # Gtk.Button
self.prev_button = None # Gtk.Button
# (Textbuffers used to display output of an update operation, and the
# buttons used to initiate that operation)
self.downloader_button = None # Gtk.Button
self.update_button = None # Gtk.Button
self.update_combo = None # Gtk.ComboBox
self.update_liststore = None # Gtk.ListStore
self.downloader_scrolled = None # Gtk.ScrolledWindow
self.downloader_textview = None # Gtk.TextView
self.downloader_textbuffer = None # Gtk.TextBuffer
self.ffmpeg_button = None # Gtk.Button
self.ffmpeg_scrolled = None # Gtk.ScrolledWindow
self.ffmpeg_textview = None # Gtk.TextView
self.ffmpeg_textbuffer = None # Gtk.TextBuffer
# IV list - other
# ---------------
# Size (in pixels) of gaps between preference window widgets
self.spacing_size = self.app_obj.default_spacing_size
# Make the code a little simpler, by checking for MS Windows just once
# (set below)
self.mswin_flag = False
# List of 'pages' (widget layouts on self.inner_grid). Each item in the
# list is the function to call
self.page_list = [] # Set below
# The number of the current page (the first is 0), matching an index in
# self.page_list
self.current_page = 0
# User choices; they are applied when the window is closed (and
# self.apply_changes() is called)
# Path to Tartube's data directory
self.data_dir = None
# The name of the youtube-dl fork to use, by default ('None' when
# youtube-dl itself should be used)
self.ytdl_fork = None
# The new value of mainapp.TartubeApp.ytdl_update_current(), if any.
self.ytdl_update_current = None
# Flag set to True, once the 'More options' button has been clicked,
# so that it is never visible again
self.more_options_flag = False
# Code
# ----
# Check for MS Windows
if os.name == 'nt':
self.mswin_flag = True
# Set the page list, which depends on operating system and packaging
self.page_list = [
'setup_start_page',
'setup_db_page',
'setup_set_downloader_page',
]
if self.mswin_flag:
self.page_list.append('setup_fetch_downloader_page')
self.page_list.append('setup_fetch_ffmpeg_page')
self.page_list.append('setup_finish_page_mswin')
elif __main__.__pkg_strict_install_flag__:
self.page_list.append('setup_finish_page_strict')
else:
self.page_list.append('setup_fetch_downloader_page')
self.page_list.append('setup_finish_page_default')
# Set up the wizard window
self.setup()
# Public class methods
# def is_duplicate(): # Inherited from GenericWizWin
# def setup(): # Inherited from GenericWizWin
# def setup_grid(): # Inherited from GenericWizWin
# def setup_button_strip(): # Inherited from GenericWizWin
# def setup_page(): # Inherited from GenericWizWin
# def convert_next_button(): # Inherited from GenericWizWin
def apply_changes(self):
"""Called by self.on_button_next_clicked().
Apply the settings the user has specified.
"""
if self.data_dir is not None:
self.app_obj.set_data_dir(self.data_dir)
self.app_obj.set_data_dir_alt_list( [ self.data_dir ] )
self.app_obj.update_data_dirs()
# (None values are acceptable)
self.app_obj.set_ytdl_fork(self.ytdl_fork)
# (A None value, only if it hasn't been changed)
if self.ytdl_update_current is not None:
self.app_obj.set_ytdl_update_current(self.ytdl_update_current)
# Continue with general initialisation
self.app_obj.open_wiz_win_continue()
def reset_changes(self):
"""Called by self.on_button_cancel_clicked().
Tartube needs to be shut down (unless an update operation is running,
in which case we stop it.)
"""
if self.app_obj.update_manager_obj:
self.app_obj.update_manager_obj.stop_update_operation()
else:
# (Prevent the shutdown code from saving the config file and/or
# database)
self.app_obj.disable_load_save()
# (Delete the config file, so that this window will appear again,
# the next time Tartube runs)
config_path = self.app_obj.get_config_path()
if os.path.isfile(config_path):
os.remove(config_path)
# Shut down Tartube
self.app_obj.stop()
# def close(): # Inherited from GenericWizWin
# (Setup pages)
def setup_start_page(self):
"""Called by self.setup_page().
Sets up the widget layout for a page.
"""
self.add_image(
self.app_obj.main_win_obj.icon_dict['system_icon'],
0, 0, 1, 1,
)
self.add_label(
'<span font_size="large" font_weight="bold">' \
+ _('Welcome to Tartube!') + '</span>',
0, 1, 1, 1,
)
if __main__.__pkg_no_download_flag__:
edition = _('Video downloads are disabled in this package')
elif __main__.__pkg_strict_install_flag__:
edition = _(
'For this package, youtube-dl(c) and FFmpeg must be' \
+ ' installed separately',
)
elif __main__.__pkg_install_flag__:
edition = _('Package edition')
elif os.name == 'nt':
edition = _('MS Windows edition')
else:
edition = _('Standard edition')
self.add_label(
'<span style="italic">' + edition + '</span>',
0, 2, 1, 1,
)
# (Empty label for spacing)
self.add_empty_label(0, 3, 1, 1)
self.add_label(
'<span font_size="large" style="italic">' \
+ _('Please take a few moments to set up the application') \
+ '</span>',
0, 4, 1, 1,
)
self.add_label(
'<span font_size="large" style="italic">' \
+ _('Click the <b>Next</b> button to get started') \
+ '</span>',
0, 5, 1, 1,
)
def setup_db_page(self):
"""Called by self.setup_page().
Sets up the widget layout for a page.
"""
grid_width = 3
self.add_label(
'<span font_size="large" style="italic">' \
+ _('Tartube stores all of its downloads in one place.') \
+ '</span>',
0, 0, grid_width, 1,
)
# (Empty label for spacing)
self.add_empty_label(0, 1, grid_width, 1)
if not self.mswin_flag:
msg = utils.tidy_up_long_string(
_(
'If you don\'t want to use the default location, then' \
+ ' click <b>Choose</b> to select a different one.',
),
60,
)
msg2 = utils.tidy_up_long_string(
_(
'If you have used Tartube before, you can select an' \
+ ' existing directory, instead of creating a new one.',
),
60,
)
else:
msg = _('Click <b>Choose</b> to create a new folder.')
msg2 = utils.tidy_up_long_string(
_(
'If you have used Tartube before, you can select an' \
+ ' existing folder, instead of creating a new one.',
),
60,
)
self.add_label(
'<span font_size="large" style="italic">' + msg + '</span>',
0, 2, grid_width, 1,
)
# (Empty label for spacing)
self.add_empty_label(0, 3, grid_width, 1)
self.add_label(
'<span font_size="large" style="italic">' + msg2 + '</span>',
0, 4, grid_width, 1,
)
# (Empty label for spacing)
self.add_empty_label(0, 5, grid_width, 1)
button = Gtk.Button(_('Choose'))
self.inner_grid.attach(button, 1, 6, 1, 1)
# (Signal connect appears below)
if not self.mswin_flag:
button2 = Gtk.Button(_('Use default location'))
self.inner_grid.attach(button2, 1, 7, 1, 1)
# (Signal connect appears below)
# (Empty label for spacing)
self.add_empty_label(0, 8, grid_width, 1)
# The specified path appears here, after it has been selected
if self.data_dir is None:
label = self.add_label(
'',
0, 9, grid_width, 1,
)
else:
label = self.add_label(
'<span font_size="large" font_weight="bold">' \
+ self.data_dir + '</span>',
0, 9, grid_width, 1,
)
# (Signal connects from above)
button.connect(
'clicked',
self.on_button_choose_folder_clicked,
label,
)
if not self.mswin_flag:
button2.connect(
'clicked',
self.on_button_default_folder_clicked,
label,
)
# Disable the Next button until a folder has been created/selected
if self.data_dir is None:
self.next_button.set_sensitive(False)
def setup_set_downloader_page(self):
"""Called by self.setup_page().
Sets up the widget layout for a page.
"""
grid_width = 3
self.add_label(
'<span font_size="large" style="italic">' \
+ _('Choose which downloader to use.') \
+ '</span>',
0, 0, grid_width, 1,
)
# youtube-dlc
radiobutton = self.setup_set_downloader_page_add_button(
2, # Row number
'<b>youtube-dlc</b>: <i>' \
+ self.app_obj.ytdl_fork_descrip_dict['youtube-dlc'] \
+ '</i>',
_('Use youtube-dlc'),
)
# youtube-dl
radiobutton2 = self.setup_set_downloader_page_add_button(
1, # Row number
'<b>youtube-dl</b>: <i>' \
+ self.app_obj.ytdl_fork_descrip_dict['youtube-dl'] \
+ '</i>',
_('Use youtube-dl'),
radiobutton,
)
# Any other fork
radiobutton3, entry = self.setup_set_downloader_page_add_button(
3, # Row number
self.app_obj.ytdl_fork_descrip_dict['custom'],
_('Use a different fork of youtube-dl'),
radiobutton2,
True, # Show an entry
)
# Set widgets to their initial state
if self.ytdl_fork is None or self.ytdl_fork == 'youtube-dl':
radiobutton2.set_active(True)
entry.set_sensitive(False)
elif self.ytdl_fork == 'youtube-dlc':
radiobutton.set_active(True)
entry.set_sensitive(False)
else:
radiobutton3.set_active(True)
if self.ytdl_fork is not None:
entry.set_text(self.ytdl_fork)
else:
entry.set_text('')
entry.set_sensitive(True)
# (Signal connects from the call to
# self.setup_set_downloader_page_add_button() )
radiobutton.connect(
'toggled',
self.on_button_ytdl_fork_toggled,
entry,
'youtube-dlc',
)
radiobutton2.connect(
'toggled',
self.on_button_ytdl_fork_toggled,
entry,
'youtube-dl',
)
radiobutton3.connect(
'toggled',
self.on_button_ytdl_fork_toggled,
entry,
)
entry.connect(
'changed',
self.on_entry_ytdl_fork_changed,
radiobutton3,
)
def setup_set_downloader_page_add_button(self, row, label_text, radio_text,
radiobutton=None, custom_flag=False):
"""Called by self.setup_set_downloader_page().
Adds widgets for a single downloader option.
Args:
row (int): Row number in self.inner_grid
label_text (str): Text to use in a Gtk.Label
radio_text (str): Text to use in a Gtk.RadioButton
radiobutton (Gtk.RadioButton): The previous radiobutton in the same
group
custom_flag (bool): True for the third option, in which case we
add an extra Gtk.Entry
Return values:
If custom_flag is False, returns the radiobutton. If custom_flag is
True, returns the radiobutton and entry box as a list
"""
if not custom_flag:
grid_width = 1
else:
grid_width = 2
# (Use an event box so the downloader can be selected by clicking
# anywhere in the frame)
event_box = Gtk.EventBox()
self.inner_grid.attach(event_box, 1, row, 1, 1)
# (Signal connect appears below)
frame = Gtk.Frame()
event_box.add(frame)
frame.set_border_width(self.spacing_size)
frame.set_hexpand(False)
grid = Gtk.Grid()
frame.add(grid)
grid.set_border_width(self.spacing_size)
grid.set_row_spacing(self.spacing_size)
label = Gtk.Label()
grid.attach(label, 0, 0, grid_width, 1)
label.set_markup(utils.tidy_up_long_string(label_text))
label.set_hexpand(False)
label.set_alignment(0, 0.5)
radiobutton2 = Gtk.RadioButton.new_from_widget(radiobutton)
grid.attach(radiobutton2, 0, 1, 1, 1)
radiobutton2.set_hexpand(False)
radiobutton2.set_label(' ' + radio_text)
# (Signal connect appears in the calling function)
# (Signal connect from above)
event_box.connect(
'button-press-event',
self.on_frame_downloader_clicked,
radiobutton2,
)
if not custom_flag:
return radiobutton2
else:
# For other forks, add an entry, and return it with the radiobutton
entry = Gtk.Entry()
grid.attach(entry, 1, 1, 1, 1)
entry.set_hexpand(False)
entry.set_editable(True)
# (Signal connect appears in the calling function)
return radiobutton2, entry
def setup_fetch_downloader_page(self):
"""Called by self.setup_page().
Sets up the widget layout for a page.
"""
grid_width = 3
self.add_label(
'<span font_size="large" style="italic">' \
+ _('Click the button to install or update the downloader.') \
+ '</span>',
0, 0, grid_width, 1,
)
self.add_label(
'<span font_size="large" style="italic">' \
+ _(
'You should do this, even if you think it is already' \
+ ' installed.',
) + '</span>',
0, 1, grid_width, 1,
)
# (Empty label for spacing)
self.add_empty_label(0, 2, grid_width, 1)
self.downloader_button = Gtk.Button(_('Install and update downloader'))
self.inner_grid.attach(self.downloader_button, 1, 3, 1, 1)
self.downloader_button.set_hexpand(False)
# (Signal connect appears below)
self.update_button = Gtk.Button(_('More options'))
# (Making the button invisible doesn't work, so instead don't add it
# to the grid at all)
if os.name != 'nt' and not self.more_options_flag:
self.inner_grid.attach(self.update_button, 1, 4, 1, 1)
self.update_button.set_hexpand(False)
# (Signal connect appears below)
# (When the update button is clicked, it is made invisible, and this
# widget is made visible instead)
self.update_liststore = Gtk.ListStore(str, str)
for item in self.app_obj.ytdl_update_list:
self.update_liststore.append(
[item, formats.YTDL_UPDATE_DICT[item]],
)
self.update_combo = Gtk.ComboBox.new_with_model(self.update_liststore)
if os.name != 'nt':
self.inner_grid.attach(self.update_combo, 1, 4, 1, 1)
if not self.more_options_flag:
self.update_combo.set_visible(False)
renderer_text = Gtk.CellRendererText()
self.update_combo.pack_start(renderer_text, True)
self.update_combo.add_attribute(renderer_text, 'text', 1)
self.update_combo.set_entry_text_column(1)
if self.ytdl_update_current is not None:
ytdl_update_current = self.ytdl_update_current
else:
ytdl_update_current = self.app_obj.ytdl_update_current
self.update_combo.set_active(
self.app_obj.ytdl_update_list.index(ytdl_update_current),
)
# (Signal connect appears below)
# Update the combo, so that the youtube-dl fork, rather than
# youtube-dl itself, is visible (if applicable)
self.refresh_update_combo()
# (Empty label for spacing)
self.add_empty_label(0, 4, grid_width, 1)
self.downloader_scrolled, self.downloader_textview, \
self.downloader_textbuffer = self.add_textview(
0, 5, grid_width, 1,
)
# (Signal connects from above)
self.downloader_button.connect(
'clicked',
self.on_button_fetch_downloader_clicked,
)
self.update_button.connect(
'clicked',
self.on_button_update_path_clicked,
)
# (Signal connects from above)
self.update_combo.connect('changed', self.on_combo_update_changed)
def setup_fetch_ffmpeg_page(self):
"""Called by self.setup_page().
Sets up the widget layout for a page.
"""
grid_width = 3
self.add_label(
'<span font_size="large" style="italic">' \
+ _('Click the button to install FFmpeg.') \
+ '</span>',
0, 0, grid_width, 1,
)
self.add_label(
'<span font_size="large" style="italic">' \
+ utils.tidy_up_long_string(
_(
'Without FFmpeg, Tartube cannot download high-resolution' \
+ ' videos, and cannot display video thumbnails from' \
+ ' YouTube.',
),
60,
) + '</span>',
0, 1, grid_width, 1,
)
# (Empty label for spacing)
self.add_empty_label(0, 2, grid_width, 1)
self.add_label(
'<span font_size="large" style="italic">' \
+ _(
'The operation might take several minutes. Please be' \
+ ' patient.',
) + '</span>',
0, 3, grid_width, 1,
)
# (Empty label for spacing)
self.add_empty_label(0, 4, grid_width, 1)
self.ffmpeg_button = Gtk.Button(_('Install FFmpeg'))
self.inner_grid.attach(self.ffmpeg_button, 1, 5, 1, 1)
self.ffmpeg_button.set_hexpand(False)
# (Signal connect appears below)
# (Empty label for spacing)
self.add_empty_label(0, 6, grid_width, 1)
self.ffmpeg_scrolled, self.ffmpeg_textview, self.ffmpeg_textbuffer \
= self.add_textview(
0, 7, grid_width, 1,
)
# (Signal connects from above)
self.ffmpeg_button.connect(
'clicked',
self.on_button_fetch_ffmpeg_clicked,
)
def setup_finish_page_mswin(self):
"""Called by self.setup_page().
Sets up the widget layout for a page, shown only on MS Windows.
"""
self.add_image(
self.app_obj.main_win_obj.icon_dict['ready_icon'],
0, 0, 1, 1,
)
self.add_label(
'<span font_size="large" font_weight="bold">' \
+ _('All done!') + '</span>',
0, 1, 1, 1,
)
# (Empty label for spacing)
self.add_empty_label(0, 2, 1, 1)
self.add_label(
'<span font_size="large" style="italic">' \
+ utils.tidy_up_long_string(
_(
'If you need to re-install or update the downloader or' \
+ ' FFmpeg, you can do it from the main window\'s menu.',
),
60,
) + '</span>',
0, 3, 1, 1,
)
# (Empty label for spacing)
self.add_empty_label(0, 4, 1, 1)
self.add_label(
'<span font_size="large" style="italic">' \
+ _('Click the <b>OK</b> button to start Tartube!') \
+ '</span>',
0, 5, 1, 1,
)
def setup_finish_page_strict(self):
"""Called by self.setup_page().
Sets up the widget layout for a page, shown only after a STRICT install
from a DEB/RPM package.
"""
self.add_image(
self.app_obj.main_win_obj.icon_dict['ready_icon'],
0, 0, 1, 1,
)
self.add_label(
'<span font_size="large" font_weight="bold">' \
+ _('All done!') + '</span>',
0, 1, 1, 1,
)
# (Empty label for spacing)
self.add_empty_label(0, 2, 1, 1)
self.add_label(
'<span font_size="large" style="italic">' \
+ utils.tidy_up_long_string(
_(
'You must install the downloader on your system, before' \
+ ' you can use Tartube.'
),
60,
) + '</span>',
0, 3, 1, 1,
)
# (Empty label for spacing)
self.add_empty_label(0, 4, 1, 1)
self.add_label(
'<span font_size="large" style="italic">' \
+ 'It is strongly recommended that you install FFmpeg.</span>',
0, 5, 1, 1,
)
self.add_label(
'<span font_size="large" style="italic">' \
+ utils.tidy_up_long_string(
_(
'Without FFmpeg, Tartube cannot download high-resolution' \
+ ' videos, and cannot display video thumbnails from' \
+ ' YouTube.',
),
60,
) + '</span>',
0, 6, 1, 1,
)
# (Empty label for spacing)
self.add_empty_label(0, 7, 1, 1)
self.add_label(
'<span font_size="large" style="italic">' \
+ _('Click the <b>OK</b> button to start Tartube!') \
+ '</span>',
0, 8, 1, 1,
)
def setup_finish_page_default(self):
"""Called by self.setup_page().
Sets up the widget layout for a page, for all operating systems except
MS Windows.
"""
self.add_image(
self.app_obj.main_win_obj.icon_dict['ready_icon'],
0, 0, 1, 1,
)
self.add_label(
'<span font_size="large" font_weight="bold">' \
+ _('All done!') + '</span>',
0, 1, 1, 1,
)
# (Empty label for spacing)
self.add_empty_label(0, 2, 1, 1)
self.add_label(
'<span font_size="large" style="italic">' \
+ 'It is stronly recommended that you install FFmpeg.</span>',
0, 3, 1, 1,
)
self.add_label(
'<span font_size="large" style="italic">' \
+ utils.tidy_up_long_string(
_(
'Without FFmpeg, Tartube cannot download high-resolution' \
+ ' videos, and cannot display video thumbnails from' \
+ ' YouTube.',
),
60,
) + '</span>',
0, 4, 1, 1,
)
# (Empty label for spacing)
self.add_empty_label(0, 5, 1, 1)
self.add_label(
'<span font_size="large" style="italic">' \
+ _('Click the <b>OK</b> button to start Tartube!') \
+ '</span>',
0, 6, 1, 1,
)
# (Support functions)
def downloader_page_write(self, msg):
"""Called by updates.UpdateManager.install_ytdl_write_output() or
self.downloader_fetch_finished().
When installing/updating youtube-dl (or a fork), write a message in the
textview.
Args:
msg (str): The message to display
"""
# v2.2.209 This install sometimes freezes on MS Windows, due to some
# Gtk problem or other. Solution is to split up long messages in the
# textview
msg = utils.tidy_up_long_string(msg)
for line in msg.split('\n'):
self.downloader_textbuffer.insert(
self.downloader_textbuffer.get_end_iter(),
line + '\n',
)
adjust = self.downloader_scrolled.get_vadjustment()
adjust.set_value(adjust.get_upper())
self.downloader_textview.queue_draw()
def downloader_fetch_finished(self, msg):
"""Called by mainapp.TartubeApp.update_manager_finished().
Display the success/failure message in the textview, and re-sensitise
buttons.
Args:
msg (str): The success/failure message to display
"""
self.downloader_page_write(msg)
self.downloader_button.set_sensitive(True)
self.next_button.set_sensitive(True)
self.prev_button.set_sensitive(True)
def ffmpeg_page_write(self, msg):
"""Called by updates.UpdateManager.install_ytdl_write_output() or
self.ffmpeg_fetch_finished().
When installing FFmpeg, write a message in the textview.
Args:
msg (str): The message to display
"""
# v2.2.209 This install sometimes freezes on MS Windows, due to some
# Gtk problem or other. Solution is to split up long messages in the
# textview
msg = utils.tidy_up_long_string(msg)
for line in msg.split('\n'):
self.ffmpeg_textbuffer.insert(
self.ffmpeg_textbuffer.get_end_iter(),
line + '\n',
)
adjust = self.ffmpeg_scrolled.get_vadjustment()
adjust.set_value(adjust.get_upper())
self.downloader_textview.queue_draw()
def ffmpeg_fetch_finished(self, msg):
"""Called by mainapp.TartubeApp.update_manager_finished().
Display the success/failure message in the textview, and re-sensitise
buttons.
Args:
msg (str): The success/failure message to display
"""
self.ffmpeg_page_write(msg)
self.ffmpeg_button.set_sensitive(True)
self.next_button.set_sensitive(True)
self.prev_button.set_sensitive(True)
def refresh_update_combo(self):
"""Called by self.setup_fetch_downloader_page().
When the youtube-dl fork is changed, updates the contents of the
combo created by self.setup_fetch_downloader_page().
"""
fork = standard = 'youtube-dl'
if self.ytdl_fork is not None:
fork = self.ytdl_fork
count = -1
for item in self.app_obj.ytdl_update_list:
count += 1
descrip = re.sub(standard, fork, formats.YTDL_UPDATE_DICT[item])
self.update_liststore.set(
self.update_liststore.get_iter(Gtk.TreePath(count)),
1,
descrip,
)
# (Callbacks)
def on_button_cancel_clicked(self, button):
"""Modified version of the standard function, called from a callback in
self.setup_button_strip().
Closes the wizard window without applying any changes, unless an
update operation is in progress.
Args:
button (Gtk.Button): The widget clicked
"""
self.reset_changes()
if not self.app_obj.update_manager_obj:
self.destroy()
def on_button_choose_folder_clicked(self, button, label):
"""Called from a callback in self.setup_db_page().
Opens a file chooser dialogue, so the user can set the location of
Tartube's data directory.
Args:
button (Gtk.Button): The widget clicked
label (Gtk.Label): Once set, the path to the directory is displayed
in this label
"""
if not self.mswin_flag:
title = _('Select Tartube\'s data directory')
else:
title = _('Select Tartube\'s data folder')
dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser(
title,
self,
'folder',
)
# Get the user's response
response = dialogue_win.run()
if response == Gtk.ResponseType.OK:
self.data_dir = dialogue_win.get_filename()
label.set_markup(
'<span font_size="large" font_weight="bold">' + self.data_dir \
+ '</span>',
)
# Data directory set, so re-enable the Next button
self.next_button.set_sensitive(True)
dialogue_win.destroy()
def on_button_default_folder_clicked(self, button, label):
"""Called from a callback in self.setup_db_page().
Sets the default location for Tartube's data directory (not on MS
Windows).
Args:
button (Gtk.Button): The widget clicked
label (Gtk.Label): Once set, the path to the directory is displayed
in this label
"""
self.data_dir = self.app_obj.data_dir
label.set_markup(
'<span font_size="large" font_weight="bold">' \
+ self.app_obj.data_dir + '</span>',
)
# Data directory set, so re-enable the Next button
self.next_button.set_sensitive(True)
def on_button_fetch_downloader_clicked(self, button):
"""Called from a callback in self.setup_fetch_page().
Starts an update operation to download and install the selected fork of
youtube-dl.
Args:
button (Gtk.Button): The widget clicked
"""
# Desensitise buttons until the operation is complete
button.set_sensitive(False)
self.next_button.set_sensitive(False)
self.prev_button.set_sensitive(False)
# Start the update operation
if not self.app_obj.update_manager_start_from_wizwin(self, 'ytdl'):
# Operation did not start
button.set_sensitive(True)
self.next_button.set_sensitive(True)
self.prev_button.set_sensitive(True)
def on_button_fetch_ffmpeg_clicked(self, button):
"""Called from a callback in self.setup_fetch_page().
Starts an update operation to download and install FFmpeg.
Args:
button (Gtk.Button): The widget clicked
"""
# Desensitise buttons until the operation is complete
button.set_sensitive(False)
self.next_button.set_sensitive(False)
self.prev_button.set_sensitive(False)
# Start the update operation
if not self.app_obj.update_manager_start_from_wizwin(self, 'ffmpeg'):
# Operation did not start
button.set_sensitive(True)
self.next_button.set_sensitive(True)
self.prev_button.set_sensitive(True)
def on_button_update_path_clicked(self, button):
"""Called from a callback in self.setup_fetch_page().
Makes the 'More options' button invisible, and the update path combo
visible.
Args:
button (Gtk.Button): The widget clicked
"""
button.set_visible(False)
self.update_combo.set_visible(True)
# Flag set to True, once the 'More options' button has been clicked,
# so that it is never visible again
self.more_options_flag = True
def on_button_ytdl_fork_toggled(self, radiobutton, entry, fork_type=None):
"""Called from callback in self.setup_set_downloader_page().
Sets the youtube-dl fork to be used. See also
self.on_entry_ytdl_fork_changed().
Args:
radiobutton (Gtk.Radiobutton): The widget clicked
entry (Gtk.Entry): Another widget to be updated
fork_type (str): 'youtube-dlc', 'youtube-dl', or None for any other
fork
"""
if radiobutton.get_active():
if fork_type is None:
fork_name = entry.get_text()
# (As in the preference window, if the 'other fork' option is
# selected, but nothing is entered in the entry box, use
# youtube-dl as the downloader)
if fork_name == '':
self.ytdl_fork = None
else:
self.ytdl_fork = fork_name
entry.set_sensitive(True)
elif fork_type == 'youtube-dl':
self.ytdl_fork = None
entry.set_text('')
entry.set_sensitive(False)
elif fork_type == 'youtube-dlc':
self.ytdl_fork = fork_type
entry.set_text('')
entry.set_sensitive(False)
def on_combo_update_changed(self, combo):
"""Called from callback in self.setup_fetch_downloader_page().
Sets the youtube-dl install/update method.
Args:
combo (Gtk.ComboBox): The widget clicked
"""
tree_iter = combo.get_active_iter()
model = combo.get_model()
self.ytdl_update_current = model[tree_iter][0]
def on_entry_ytdl_fork_changed(self, entry, radiobutton):
"""Called from callback in self.setup_set_downloader_page().
Sets the youtube-dl fork to be used. See also
self.on_button_ytdl_fork_toggled().
Args:
entry (Gtk.Entry): The widget changed
"""
if radiobutton.get_active():
entry_text = utils.strip_whitespace(entry.get_text())
# (As in the preference window, if the 'other fork' option is
# selected, but nothing is entered in the entry box, use
# youtube-dl as the system default)
if entry_text == '':
self.ytdl_fork = None
else:
self.ytdl_fork = entry_text
def on_frame_downloader_clicked(self, event_box, event_button,
radiobutton):
"""Called from callback in self.setup_set_downloader_page().
Enables/disables selecting a downloader by clicking anywhere in its
containing frame.
Args:
event_box (Gtk.EventBox): Ignored
event_button (Gdk.EventButton): Ignored
radiobutton (Gtk.RadioButton): The radiobutton inside the clicked
frame, which should be made active
"""
if not radiobutton.get_active():
radiobutton.set_active(True)