#!/opt/local/bin/python313
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019-2025 A S Lewis
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the Free
# Software Foundation; either version 2.1 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 Lesser General Public License for more
# details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.


"""Configuration 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 datetime
import html
import json
import os
import pickle
import re
import stat
import sys
import urllib.parse


# Import our modules
import __main__
import downloads
import formats
import mainapp
import mainwin
import media
import platform
import ttutils
# Use same gettext translations
from mainapp import _


# Import matplotlib stuff
if mainapp.HAVE_MATPLOTLIB_FLAG:
    from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg
    from matplotlib.figure import Figure
    from matplotlib.ticker import MaxNLocator


# Classes


class GenericConfigWin(Gtk.Window):

    """Generic Python class for windows in which the user can modify various
    settings.

    Inherited by two types of window - 'preference windows' (in which changes
    are applied immediately), and 'edit windowS' (in which changes are stored
    temporarily, and only applied once the user has finished making changes.
    """


    # Standard class methods


#   def __init__():             # Provided by child object


    # Public class methods


#   def is_duplicate():         # Provided by child object


    def setup(self):

        """Called by self.__init__().

        Sets up the config 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.config_win_pixbuf_list)

        # Set up main widgets
        self.setup_grid()
        self.setup_notebook()
        self.setup_button_strip()
        self.setup_gap()

        # Set up tabs
        self.setup_tabs()

        # 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 a Gtk.Grid, on which a notebook and a button strip will be
        placed. (Each of the notebook's tabs also has its own Gtk.Grid.)
        """

        self.grid = Gtk.Grid()
        self.add(self.grid)


    def setup_notebook(self):

        """Called by self.setup().

        Sets up a Gtk.Notebook, after which self.setup_tabs() is called to fill
        it with tabs.
        """

        self.notebook = Gtk.Notebook()
        self.grid.attach(self.notebook, 0, 1, 1, 1)
        self.notebook.set_border_width(self.spacing_size)
        # It shouldn't be necessary to scroll the notebook's tabs, but we'll
        #   make it possible anyway
        self.notebook.set_scrollable(True)


    def add_notebook_tab(self, name, border_width=None):

        """Called by various functions in the child edit/preference window.

        Adds a tab to the main Gtk.Notebook, creating a Gtk.Grid inside it, on
        which the calling function can add more widgets.

        Args:

            name (str): The name of the tab

            border_width (int): If specified, the border width for the
                Gtk.Grid contained in this tab (usually specified when an inner
                Gtk.Notebook is to be added to this tab). If not specified, a
                default width is used

        Return values:

            The tab created (in the form of a Gtk.Box) and its Gtk.Grid

        """

        if border_width is None:
            border_width = self.spacing_size

        tab = Gtk.Box()
        self.notebook.append_page(tab, Gtk.Label.new_with_mnemonic(name))
        tab.set_hexpand(True)
        tab.set_vexpand(True)
        tab.set_border_width(self.spacing_size)

        scrolled = Gtk.ScrolledWindow()
        tab.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)

        grid = Gtk.Grid()
        scrolled.add_with_viewport(grid)
        grid.set_border_width(border_width)
        grid.set_column_spacing(self.spacing_size)
        grid.set_row_spacing(self.spacing_size)

        return tab, grid


    def add_inner_notebook(self, grid):

        """Called by various functions in the child edit/preference window.

        Adds an inner Gtk.Notebook to a tab inside the main Gtk.Notebook.

        Args:

            grid (Gtk.Grid): The widget to which the notebook is added

        Return values:

            Returns the new Gtk.Notebook

        """

        inner_notebook = Gtk.Notebook()
        grid.attach(inner_notebook, 0, 1, 1, 1)
        # It shouldn't be necessary to scroll the notebook's tabs, but we'll
        #   make it possible anyway
        inner_notebook.set_scrollable(True)

        return inner_notebook


    def add_inner_notebook_tab(self, name, notebook):

        """Called by various functions in the child edit/preference window.

        A modified form of self.add_notebook_tab, for tabs to be placd in the
        inner notebook created by a call to self.add_inner_notebook.

        Adds a tab to the specified Gtk.Notebook, creating a Gtk.Grid inside
        it, on which the calling function can add more widgets.

        Args:

            name (str): The name of the tab

            notebook (Gtk.Notebook): The notebook to which the tab is added

        Return values:

            The tab created (in the form of a Gtk.Box) and its Gtk.Grid

        """

        tab = Gtk.Box()
        notebook.append_page(tab, Gtk.Label.new_with_mnemonic(name))
        tab.set_hexpand(True)
        tab.set_vexpand(True)

        scrolled = Gtk.ScrolledWindow()
        tab.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)

        grid = Gtk.Grid()
        scrolled.add_with_viewport(grid)
        grid.set_border_width(self.spacing_size)
        grid.set_column_spacing(self.spacing_size)
        grid.set_row_spacing(self.spacing_size)

        return tab, grid


#   def setup_button_strip():   # Provided by child object


    def setup_gap(self):

        """Called by self.setup().

        Adds an empty box beneath the button strip for aesthetic purposes.
        """

        hbox = Gtk.HBox()
        self.grid.attach(hbox, 0, 3, 1, 1)
        hbox.set_border_width(self.spacing_size)


    def close(self, also_self):

        """Called from callback in self.setup().

        Inform the main window that this window is closing.

        Args:

            also_self (an object inheriting from config.GenericConfigWin):
                Another copy of self

        """

        self.app_obj.main_win_obj.del_child_window(self)


    # (Add widgets)


    def add_secondary_grid(self, grid, x, y, wid, hei):

        """Called by various functions in the child edit window.

        Adds another Gtk.Grid, to be placed inside the tab's main grid, in
        order to avoid messing up widget spacing elsewhere in the tab.

        Args:

            grid (Gtk.Grid): The existing grid on which the new grid will be
                placed

            x, y, wid, hei (int): Position on the existing grid at which the
                new grid is placed

        Return values:

            The new Gtk.Grid

        """

        grid2 = Gtk.Grid()
        grid.attach(grid2, x, y, wid, hei)
        grid2.set_vexpand(False)
        grid2.set_column_spacing(self.spacing_size)
        grid2.set_row_spacing(self.spacing_size)

        return grid2


    def add_image(self, grid, image_path, x, y, wid, hei):

        """Called by various functions in the child edit window.

        Adds a Gtk.Image to the tab's Gtk.Grid.

        Args:

            grid (Gtk.Grid): The grid on which this widget will be placed

            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

        Return values:

            The Gtk.Frame containing the image

        """

        frame = Gtk.Frame()
        grid.attach(frame, x, y, wid, hei)

        image = Gtk.Image()
        frame.add(image)
        image.set_from_pixbuf(
            self.app_obj.file_manager_obj.load_to_pixbuf(image_path),
        )

        return frame


    def add_pixbuf(self, grid, pixbuf_name, x, y, wid, hei):

        """Called by various functions in the child config window.

        Adds a Gtk.Image to the tab's Gtk.Grid. A modified version of
        self.add_image(), which is called with a path to an image file; this
        function is called with one of the pixbuf names specified by
        mainwin.MainWin.pixbuf_dict, e.g. 'video_both_large'.

        Args:

            grid (Gtk.Grid): The grid on which this widget will be placed

            pixbuf_name (str): One of the keys in
                mainwin.MainWin.pixbuf_dict, e.g. 'video_both_large'.

            x, y, wid, hei (int): Position on the grid at which the widget is
                placed

        Return values:

            The Gtk.Frame containing the image

        """

        frame = Gtk.Frame()
        grid.attach(frame, x, y, wid, hei)

        image = Gtk.Image()
        frame.add(image)

        main_win_obj = self.app_obj.main_win_obj
        if pixbuf_name in main_win_obj.pixbuf_dict:
            image.set_from_pixbuf(main_win_obj.pixbuf_dict[pixbuf_name])
        else:
            # Unrecognised pixbuf name
            image.set_from_pixbuf(
                main_win_obj.pixbuf_dict['question_large'],
            )

        return frame


    def add_treeview(self, grid, x, y, wid, hei):

        """Called by various functions in the child preference/edit window.

        Adds a single-column Gtk.Treeview to the tab's Gtk.Grid. No callback
        function is created by this function; it's up to the calling code to
        supply one.

        Args:

            grid (Gtk.Grid): The grid on which this widget will be placed

            x, y, wid, hei (int): Position on the grid at which the widget is
                placed

        Return values:

            A list containing the treeview widget and liststore created

        """

        frame = Gtk.Frame()
        grid.attach(frame, x, y, wid, hei)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)

        treeview = Gtk.TreeView()
        scrolled.add(treeview)
        treeview.set_headers_visible(False)

        renderer_text = Gtk.CellRendererText()
        column_text = Gtk.TreeViewColumn(
            '',
            renderer_text,
            text=0,
        )
        treeview.append_column(column_text)

        liststore = Gtk.ListStore(str)
        treeview.set_model(liststore)

        return treeview, liststore


    # (Shared support functions)


    def add_combos_for_graphs(self, grid, row):

        """Called by tabs in ChannelPlaylistEditWin, FolderEditWin and
        SystemPrefWin.

        The tabs that draw graphs share a standard set of comboboxes, with
        which the graph is customised.

        This function is called to create the comboboxes, and to set up
        callbacks so that changes to the settings are remembered between
        windows.

        Args:

            grid (Gtk.Grid): The grid on which widgets are arranged in their
                tab

            row (int): The grid row on which these combos appear

        """

        if isinstance(self, GenericPrefWin):
            pref_win_flag = True
        else:
            pref_win_flag = False

        # Add combos to customise the graph
        combo_list = [
            [_('Downloads'), 'receive'],
            [_('Uploads'), 'upload'],
            [_('File size'), 'size'],
            [_('Duration'), 'duration'],
        ]

        if pref_win_flag:

            combo = self.add_combo_with_data(grid,
                combo_list,
                self.app_obj.graph_data_type,
                0, row, 1, 1,
            )

        else:

            combo = self.add_combo_with_data(grid,
                combo_list,
                None,
                0, row, 1, 1,
            )

            count = -1
            for mini_list in combo_list:
                count += 1
                if mini_list[1] == self.app_obj.graph_data_type:
                    combo.set_active(count)
                    break

        combo.set_hexpand(True)

        combo_list2 = [
            [_('Graph'), 'graph'],
            [_('Bar chart'), 'chart'],
        ]

        if pref_win_flag:

            combo2 = self.add_combo_with_data(grid,
                combo_list2,
                self.app_obj.graph_plot_type,
                1, row, 1, 1,
            )

        else:

            combo2 = self.add_combo_with_data(grid,
                combo_list2,
                None,
                1, row, 1, 1,
            )

            count = -1
            for mini_list in combo_list2:
                count += 1
                if mini_list[1] == self.app_obj.graph_plot_type:
                    combo2.set_active(count)
                    break

        combo2.set_hexpand(True)

        combo_list3 = [
            [_('Show decade'), 60*60*24*365*10],
            [_('Show year'), 60*60*24*365],
            [_('Show quarters'), 60*60*24*90],
            [_('Show month'), 60*60*24*30],
            [_('Show week'), 60*60*24*7],
            [_('Show day'), 60*60*24],
        ]

        if pref_win_flag:

            combo3 = self.add_combo_with_data(grid,
                combo_list3,
                self.app_obj.graph_time_period_secs,
                2, row, 1, 1,
            )

        else:

            combo3 = self.add_combo_with_data(grid,
                combo_list3,
                None,
                2, row, 1, 1,
            )

            count = -1
            for mini_list in combo_list3:
                count += 1
                if mini_list[1] == self.app_obj.graph_time_period_secs:
                    combo3.set_active(count)
                    break

        combo3.set_hexpand(True)
        if self.app_obj.graph_data_type != 'receive' \
        and self.app_obj.graph_data_type != 'upload':
            combo3.set_sensitive(False)

        combo_list4 = [
            [_('Quarters'), 60*60*24*90],
            [_('Months'), 60*60*24*30],
            [_('Weeks'), 60*60*24*7],
            [_('Days'), 60*60*24],
            [_('Hours'), 60*60],
        ]

        if pref_win_flag:

            combo4 = self.add_combo_with_data(grid,
                combo_list4,
                self.app_obj.graph_time_unit_secs,
                3, row, 1, 1,
            )

        else:

            combo4 = self.add_combo_with_data(grid,
                combo_list4,
                None,
                3, row, 1, 1,
            )

            count = -1
            for mini_list in combo_list4:
                count += 1
                if mini_list[1] == self.app_obj.graph_time_unit_secs:
                    combo4.set_active(count)
                    break

        combo4.set_hexpand(True)
        if self.app_obj.graph_data_type != 'receive' \
        and self.app_obj.graph_data_type != 'upload':
            combo4.set_sensitive(False)

        combo_list5 = [
            [_('Red'), 'red'],
            [_('Green'), 'green'],
            [_('Blue'), 'blue'],
            [_('Black'), 'black'],
            [_('White'), 'white'],
        ]

        if pref_win_flag:

            combo5 = self.add_combo_with_data(grid,
                combo_list5,
                self.app_obj.graph_ink_colour,
                4, row, 1, 1,
            )

        else:

            combo5 = self.add_combo_with_data(grid,
                combo_list5,
                None,
                4, row, 1, 1,
            )

            count = -1
            for mini_list in combo_list5:
                count += 1
                if mini_list[1] == self.app_obj.graph_ink_colour:
                    combo5.set_active(count)
                    break

        combo5.set_hexpand(True)

        # (Signal connects from above)
        combo.connect(
            'changed',
            self.on_combo_graph_changed,
            'data_type',
            combo3,
            combo4,
        )
        combo2.connect(
            'changed',
            self.on_combo_graph_changed,
            'plot_type',
            combo3,
            combo4,
        )
        combo3.connect(
            'changed',
            self.on_combo_graph_changed,
            'time_period',
            combo3,
            combo4,
        )
        combo4.connect(
            'changed',
            self.on_combo_graph_changed,
            'time_unit',
            combo3,
            combo4,
        )
        combo5.connect(
            'changed',
            self.on_combo_graph_changed,
            'ink_colour',
            combo3,
            combo4,
        )

        return combo, combo2, combo3, combo4, combo5


    def add_youtube_warning(self, grid, x, y, wid, hei):

        """Can be called by any config window tab, especially those that
        provide a way to pass user credentials to the website (such as
        --username/--password, imported browser cookies, .netrc files etc).

        As of October 2024, some methods will not work, and some may even
        cause the deletion of a YouTube account. The user should consult the
        latest yt-dlp warnings about this issue, before downloading restricted
        videos:

        https://github.com/yt-dlp/yt-dlp/issues/3766
                Return values:

        Args:

            grid (Gtk.Grid): The grid on which widgets are arranged in their
                tab

            x, y, wid, hei (int): Position on the grid at which the warning is
                placed

        Return values:

            The new Gtk.Grid on which the warning is placed

        """

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, x, y, wid, hei)

        frame = self.add_pixbuf(grid2,
            'attention_large',
            0, 0, 1, 1,
        )
        frame.set_hexpand(False)
        frame.set_border_width(self.spacing_size)
        frame.set_size_request(75, -1)

        frame2 = Gtk.Frame()
        grid2.attach(frame2, 1, 0, 1, 1)

        grid3 = Gtk.Grid()
        frame2.add(grid3)
        grid3.set_border_width(self.spacing_size * 2)
        grid3.set_column_spacing(self.spacing_size * 2)
        grid3.set_row_spacing(self.spacing_size)

        label = Gtk.Label()
        grid3.attach(label, 0, 0, 1, 1)
        label.set_markup(
            _(
                'WARNING! YouTube is trying to block applications like' \
                + ' Tartube!',
            ),
        )
        label.set_hexpand(True)
        label.set_alignment(0, 0.5)

        label2 = Gtk.Label()
        grid3.attach(label2, 0, 1, 1, 1)
        label2.set_markup(
            '<a href="' \
            + html.escape('https://github.com/yt-dlp/yt-dlp/issues/3766') \
            + '" title="' \
            + _('yt-dlp Known Issues FAQ') \
            + '">' \
            + _('Before trying to download restricted videos, read the FAQ!') \
            + '</a>',
        )
        label2.set_hexpand(True)
        label2.set_alignment(0, 0.5)

        return grid2


    def get_options_applied_text(self, options_obj):

        """ Called by OptionsEditWin.setup_name_tab() and
        SystemPrefWin.setup_options_dl_list_tab_add_row().

        Generates text displaying the media data object(s) to which a
        download options manager has been applied.

        Args:

            options_obj (options.OptionsManager): The download options manager

        Return values:

            A single line of displayable text

        """

        if not options_obj.dbid_list:

            # Failsafe; calling code has already checked that
            return ''

        # Database auto-fix: see comments in the called function
        self.get_options_applied_text_autofix(options_obj)

        if len(options_obj.dbid_list) == 1:

            dbid = options_obj.dbid_list[0]
            media_data_obj = self.app_obj.media_reg_dict[dbid]
            return media_data_obj.get_translated_type(True) \
            + ': ' + media_data_obj.name

        else:

            video_count = 0
            channel_count = 0
            playlist_count = 0
            folder_count = 0

            for dbid in options_obj.dbid_list:
                media_data_obj = self.app_obj.media_reg_dict[dbid]
                if isinstance(media_data_obj, media.Video):
                    video_count += 1
                elif isinstance(media_data_obj, media.Channel):
                    channel_count += 1
                elif isinstance(media_data_obj, media.Playlist):
                    playlist_count += 1
                elif isinstance(media_data_obj, media.Folder):
                    folder_count += 1

            msg = ''
            if video_count:
                msg = _('Videos') + ': ' + str(video_count) + ' '

            if channel_count:
                msg = msg + _('Channels') + ': ' + str(channel_count) + ' '

            if playlist_count:
                msg = msg + _('Playlists') + ': ' + str(playlist_count) + ' '

            if folder_count:
                msg = msg + _('Folders') + ': ' + str(folder_count) + ' '

            return msg


    def get_options_applied_text_autofix(self, options_obj):

        """Called by self.get_options_applied_text().

        Git #456 - options.OptionsObj.dbid_list includes .dbid of non-existent
        media data objects.

        Since the cause of the issue is not known, do a quick auto-fix
        whenever the prefwin opens. (The miniscule hit to performance is better
        than a broken preferences window.)

        Args:

            options_obj (options.OptionsManager): The download options manager

        """

        dbid_list = []
        fix_flag = False

        for dbid in options_obj.dbid_list:
            if not dbid in self.app_obj.media_reg_dict:
                fix_flag = True
            else:
                dbid_list.append(dbid)

        if fix_flag:
            options_obj.dbid_list = dbid_list
            self.app_obj.system_error(
                406,
                'Detected and auto-fixed download options applied to non-' \
                + ' existent media. Please do a full database check: in' \
                + ' Tartube\'s menu, click File > Check database integrity',
            )


    def plot_graph(self, hbox, plot_type, data_type, ink_colour, x_label,
    y_label, x_list, y_list):

        """Called by self.on_button_draw_graph_clicked().

        Plots a graph, using the specified settings and data points.

        Args:

            hbox (Gtk.HBox): The container widget

            plot_type (str): 'graph' or 'chart'

            data_type (str): 'receive', 'upload', 'size' or 'duration'

            ink_colour (str): 'red', 'green', 'blue', 'black' or white'

            x_label, y_label (str): Text for each axis on the graph

            x_list (list): List of data points along the x axis

            y_list (list): List of data points along the y axis (both lists
                should contain the same number of items)

        """

        # Sanity check: when the video counts (values in y_list) are all 0,
        #   matplotlib will try to draw a y-axis with fractional values
        # Check for that, so we can prevent it
        simplify_y_axis_flag = True
        for num in y_list:
            if num:
                simplify_y_axis_flag = False
                break

        # Remove the old figure
        for child in hbox.get_children():
            hbox.remove(child)

        # Plot a new figure
        fig = Figure(dpi = 100)

        ax = fig.add_subplot(1, 1, 1)

        if plot_type == 'graph':
            ax.plot(x_list, y_list, color = ink_colour)
        else:
            ax.bar(x_list, y_list, color = ink_colour)

        # Set up the axes
        ax.set_xlabel(x_label)
        ax.set_ylabel(y_label)
        # (Negative values are meaningless, so don't allow them to appear on
        #   the x/y axes)
        if simplify_y_axis_flag:
            ax.set_ylim([0, 1])
        else:
            ax.set_ylim(ymin=0)
        if data_type == 'receive' or data_type == 'upload':
            ax.set_xlim(xmin=0)
        # (Fractional values are also meaningless)
        ax.xaxis.set_major_locator(MaxNLocator(integer = True))
        ax.yaxis.set_major_locator(MaxNLocator(integer = True))
        # (For time graphs, reverse the X axis, to show days ago, etc)
        if data_type == 'receive' or data_type == 'upload':
            ax.set_xlim(ax.get_xlim()[::-1])
        # (For some reason, the x-axis label is drawn below the visible area.
        #   Not sure how to fix that, so move it to the top, in which there is
        #   empty space)
        ax.xaxis.set_label_position('top')
        # (Reduce wasted space around the edges)
        fig.tight_layout()

        canvas = FigureCanvasGTK3Agg(fig)  # a Gtk.DrawingArea
        hbox.add(canvas)

        self.show_all()


    def show_spinbutton_leading_zeroes(self, spinbutton, columns):

        """Callback which can be connected to any Gtk.SpinButton.

        Adds leading zeroes to the value displayed in the spinbutton, which is
        expected to be an integer.

        Args:

            spinbutton (Gtk.SpinButton): The widget to modify

            columns (int): A value, 1 or above; e.g. use 2 to show the
                components in a 24-hour clock

        """

        if type(columns) is not int or columns < 1:
            return False

        adjustment = spinbutton.get_adjustment()
        format_str = '{:0' + str(columns) + 'd}'
        spinbutton.set_text(format_str.format(int(adjustment.get_value())))

        return True


    def ytdlp_only(self):

        """Shorcut function, call for various underlined text.

        Return values:

            The formatted string 'yt-dlp only'

        """

        return ' <b>[' + _('yt-dlp only') + ']</b>'


    # (Shared callbacks)


    def on_button_draw_graph_clicked(self, button, hbox, combo, combo2,
    combo3, combo4, combo5):

        """Called from callbacks in ChannelPlaylistEditWin,
        FolderEditWin and SystemPrefWin.

        Prepares data for a graph using the specified settings, then calls
        self.plot_graph() to actually draw it.

        Args:

            button (Gtk.Button): The widget clicked

            hbox (Gtk.HBox): The container widget for the graph

            combo, combo2, combo3, combo4, combo5 (Gtk.ComboBox): Five combos
                specifying the data to view:

                The data type ('receive' for download times, 'upload' for
                upload times, 'size' for file size, 'duration' for video
                duration)

                The type of graph to plot ('graph' for a line plot graph, or
                'chart' for a bar chart)

                The period of time used as the span of the x-axis (in seconds,
                e.g. 31536000 is the equivalent of a year)

                The time unit to use (in seconds, e.g. 604800 is the equivalent
                of a week). We count the number of videos for the time unit
                and use it as a single point on the x-axis

                The colour to use ('red', 'green', 'blue', 'black', white')

        """

        # Extract data from the combos

        # Get the data type ('receive', 'upload', 'size' or 'duration')
        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        data_name = model[tree_iter][0]
        data_type = model[tree_iter][1]

        # Get type of graph to plot ('graph' or 'chart')
        tree_iter2 = combo2.get_active_iter()
        model2 = combo2.get_model()
        plot_type = model2[tree_iter2][1]

        # Get the period of time used as the span of the x-axis, in seconds
        tree_iter3 = combo3.get_active_iter()
        model3 = combo3.get_model()
        time_period_secs = int(model3[tree_iter3][1])

        # Get the time unit to use, in seconds
        tree_iter4 = combo4.get_active_iter()
        model4 = combo4.get_model()
        time_unit = model4[tree_iter4][0]
        time_unit_secs = int(model4[tree_iter4][1])
        # The time unit must not be larger than the total period of time
        if time_unit_secs > time_period_secs:
            time_unit_secs = time_period_secs

        # Get the colour to use ('red', 'green', 'blue', 'black', white')
        tree_iter5 = combo5.get_active_iter()
        model5 = combo5.get_model()
        ink_colour = model5[tree_iter5][1]

        # Compile a dictionary of video counts
        # For 'receive' and 'upload', dictonary in the form
        #   frequency_dict[time_unit_increment] = number_of_videos
        # For 'size', dictionary in the form
        #   frequency_dict[size_category] = number_of_videos
        # For 'duration', dictionary in the form
        #   frequency_dict[duration_category] = number_of_videos
        # When called by SystemPrefWin, use the entire database
        # When called by ChannelPlaylistEditWin or FolderEditWin, use only the
        #   children of that container
        # 'size_category' and 'duration_category' are arbitrary ranges of
        #   values, chosen for aesthetic reasons
        if isinstance(self, GenericEditWin):

            if data_type == 'receive' or data_type == 'upload':

                frequency_dict = self.edit_obj.compile_all_videos_by_frequency(
                    data_type,
                    time_unit_secs,
                    {},
                )

            elif data_type == 'size':

                frequency_dict = self.edit_obj.compile_all_videos_by_size(
                    {},
                )

            elif data_type == 'duration':

                frequency_dict = self.edit_obj.compile_all_videos_by_duration(
                    {},
                )

        else:

            frequency_dict = {}

            for dbid in self.app_obj.container_reg_dict.keys():

                media_data_obj = self.app_obj.media_reg_dict[dbid]
                # Ignore private (system) folders, because they contain
                #   media.Video objects also stored in public folders
                if not isinstance(media_data_obj, media.Folder) \
                or not media_data_obj.priv_flag:

                    if data_type == 'receive' or data_type == 'upload':

                        frequency_dict \
                        = media_data_obj.compile_all_videos_by_frequency(
                            data_type,
                            time_unit_secs,
                            frequency_dict,
                        )

                    elif data_type == 'size':

                        frequency_dict \
                        = media_data_obj.compile_all_videos_by_size(
                            frequency_dict,
                        )

                    elif data_type == 'duration':

                        frequency_dict \
                        = media_data_obj.compile_all_videos_by_duration(
                            frequency_dict,
                        )

        # Compile two lists, with each index giving the x and y coordinates
        #   for the graph to be plotted
        if data_type == 'receive' or data_type == 'upload':

            period_list = []
            frequency_list = []

            for i in range(0, int((time_period_secs / time_unit_secs) + 1)):

                period_list.append(i)
                if i in frequency_dict:
                    frequency_list.append(frequency_dict[i])
                else:
                    frequency_list.append(0)

            # Draw the graph
            self.plot_graph(
                hbox,
                plot_type,
                data_type,
                ink_colour,
                time_unit,
                data_name,
                period_list,
                frequency_list,
            )

        else:

            # NB If these labels are changed, when the corresponding literal
            #   values in media.GenericContainer.compile_all_videos_by_size()
            #   and .compile_all_videos_by_duration() must be changed too
            if data_type == 'size':

                label_list = [
                    '10MB', '25MB', '50MB', '100MB', '250MB',
                    '500MB', '1GB', '2GB', '5GB', '5GB+',
                ]

            else:

                label_list = [
                    '10s', '1m', '5m', '10m', '20m',
                    '30m', '1h', '2h', '5h', '5h+',
                ]

            frequency_list = []
            for label in label_list:

                if label in frequency_dict:
                    frequency_list.append(frequency_dict[label])
                else:
                    frequency_list.append(0)

            # Draw the graph
            self.plot_graph(
                hbox,
                plot_type,
                data_type,
                ink_colour,
                data_name,
                _('Videos'),
                label_list,
                frequency_list,
            )


    def on_combo_graph_changed(self, combo, combo_type, combo2, combo3):

        """Called from callback in self.add_combos_for_graphs().

        Graphs are drawn with five standard combos for customising the graph.
        When the user selects a new setting in a combo, store the value.

        In some cases, one or more of the combos must be (de)sensitised.

        Args:

            combo (Gtk.ComboBox): The widget clicked

            combo_type (str): A string describing which IV to update:
                'data_type', 'plot_type', 'time_period', 'time_unit',
                'ink_colour'

            combo2, combo3 (Gtk.ComboBox): Other combos to be (de)sensitised

        """

        # Extract data from the combo
        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        value = model[tree_iter][1]
        # Update IVs
        self.app_obj.set_graph_values(combo_type, value)

        # (De)sensitise other combos, as appropriate
        if combo_type == 'data_type':

            if (value == 'receive' or value == 'upload'):
                combo2.set_sensitive(True)
                combo3.set_sensitive(True)
            else:
                combo2.set_sensitive(False)
                combo3.set_sensitive(False)


class GenericEditWin(GenericConfigWin):

    """Generic Python class for windows in which the user can modify various
    settings in a class object (such as a media.Video or an
    options.OptionsManager object).

    The modifications are stored temporarily, and only applied once the user
    has finished making changes.
    """


    # Standard class methods


#   def __init__():             # Provided by child object


    # Public class methods


    def is_duplicate(self, app_obj, edit_obj):

        """Called by self.__init__.

        Don't open this edit window, if another with the same .edit_obj is
        already open.

        Args:

            app_obj (mainapp.TartubeApp): The main application object

            edit_obj (options.OptionsManager): The object whose attributes will
                be edited in this window

        Return values:

            True if a duplicate is found, False if not

        """

        for config_win_obj in app_obj.main_win_obj.config_win_list:

            if isinstance(config_win_obj, GenericEditWin) \
            and config_win_obj.edit_obj == edit_obj:

                # Duplicate found
                config_win_obj.present()
                return True

        # Not a duplicate
        return False


#   def setup():                # Inherited from GenericConfigWin


#   def setup_grid():           # Inherited from GenericConfigWin


#   def setup_notebook():       # Inherited from GenericConfigWin


#   def add_notebook_tab():     # Inherited from GenericConfigWin


    def setup_button_strip(self):

        """Called by self.setup().

        Creates a strip of buttons at the bottom of the window. Any changes the
        user has made are applied by clicking the 'OK' or 'Apply' buttons, and
        cancelled by using the 'Reset' or 'Cancel' buttons.

        The window is closed by using the 'OK' and 'Cancel' buttons.

        If self.multi_button_flag is True, only the 'OK' button is created.
        """

        hbox = Gtk.HBox()
        self.grid.attach(hbox, 0, 2, 1, 1)

        if self.multi_button_flag:

            # 'Reset' button
            self.reset_button = Gtk.Button(_('Reset'))
            hbox.pack_start(self.reset_button, False, False, self.spacing_size)
            self.reset_button.get_child().set_width_chars(10)
            self.reset_button.set_tooltip_text(
                _('Reset changes without closing the window'),
            );
            self.reset_button.connect('clicked', self.on_button_reset_clicked)

            # 'Apply' button
            self.apply_button = Gtk.Button(_('Apply'))
            hbox.pack_start(self.apply_button, False, False, self.spacing_size)
            self.apply_button.get_child().set_width_chars(10)
            self.apply_button.set_tooltip_text(
                _('Apply changes without closing the window'),
            );
            self.apply_button.connect('clicked', self.on_button_apply_clicked)

        # 'OK' button
        self.ok_button = Gtk.Button(_('OK'))
        hbox.pack_end(self.ok_button, False, False, self.spacing_size)
        self.ok_button.get_child().set_width_chars(10)
        self.ok_button.set_tooltip_text(_('Apply changes'));
        self.ok_button.connect('clicked', self.on_button_ok_clicked)

        if self.multi_button_flag:

            # 'Cancel' button
            self.cancel_button = Gtk.Button(_('Cancel'))
            hbox.pack_end(self.cancel_button, False, False, self.spacing_size)
            self.cancel_button.get_child().set_width_chars(10)
            self.cancel_button.set_tooltip_text(_('Cancel changes'));
            self.cancel_button.connect(
                'clicked',
                self.on_button_cancel_clicked,
            )


#   def setup_gap():            # Inherited from GenericConfigWin


    # (Non-widget functions)


    def apply_changes(self):

        """Called by self.on_button_ok_clicked() and
        self.on_button_apply_clicked().

        Any changes the user has made are temporarily stored in self.edit_dict.
        Apply to those changes to the object being edited.
        """

        # Apply any changes the user has made
        for key in self.edit_dict.keys():
            setattr(self.edit_obj, key, self.edit_dict[key])

        # The changes can now be cleared
        self.edit_dict = {}


    def reset_with_new_edit_obj(self, new_edit_obj):

        """Can be called by anything.

        Resets the object whose values are being edited in this window, i.e.
        self.edit_obj, to the specified object.

        Then redraws the window itself, as if the user had clicked the 'Reset'
        button at the bottom of the window. This makes new_edit_obj's IVs
        visible in the edit window, without the need to destroy the old one and
        replace it with a new one.

        Args:

            new_edit_obj (class): The replacement edit object

        """

        self.edit_obj = new_edit_obj

        # The rest of this function is copied from
        #   self.on_button_reset_clicked()

        # Remove all existing tabs from the notebook
        number = self.notebook.get_n_pages()
        if number:

            for count in range(0, number):
                self.notebook.remove_page(0)

        # Empty self.edit_dict, destroying any changes the user has made
        self.edit_dict = {}

        # Re-draw all the tabs
        self.setup_tabs()

        # Render the changes
        self.show_all()


    def retrieve_val(self, name):

        """Can be called by anything.

        Any changes the user has made are temporarily stored in self.edit_dict.

        Each key corresponds to an attribute in the object being edited,
        self.edit_obj.

        If 'name' exists as a key in that dictionary, retrieve the
        corresponding value and return it. Otherwise, the user hasn't yet
        modified the value, so retrieve directly from the attribute in the
        object being edited.

        Args:

            name (str): The name of the attribute in the object being edited

        Return values:

            The original or modified value of that attribute

        """

        if name in self.edit_dict:
            return self.edit_dict[name]
        else:
            attrib = getattr(self.edit_obj, name)
            if type(attrib) is list or type(attrib) is dict:
                return attrib.copy()
            else:
                return attrib


    # (Add widgets)


    def add_checkbutton(self, grid, text, prop, x, y, wid, hei):

        """Called by various functions in the child edit window.

        Adds a Gtk.CheckButton to the tab's Gtk.Grid.

        Args:

            grid (Gtk.Grid): The grid on which this widget will be placed

            text (string or None): The text to display in the checkbutton's
                label. No label is used if 'text' is an empty string or None

            prop (string or None): The name of the attribute in self.edit_obj
                whose value will be set to the contents of this widget. If
                None, no changes are made to self.edit_dict; it's up to the
                calling function to provide a .connect()

            x, y, wid, hei (int): Position on the grid at which the widget is
                placed

        Return values:

            The checkbutton widget created

        """

        checkbutton = Gtk.CheckButton()
        grid.attach(checkbutton, x, y, wid, hei)
        checkbutton.set_hexpand(True)
        if text is not None and text != '':
            checkbutton.set_label(text)

        if prop is not None:
            checkbutton.set_active(self.retrieve_val(prop))
            checkbutton.connect('toggled', self.on_checkbutton_toggled, prop)

        return checkbutton


    def add_combo(self, grid, combo_list, prop, x, y, wid, hei):

        """Called by various functions in the child edit window.

        Adds a simple Gtk.ComboBox to the tab's Gtk.Grid.

        Args:

            grid (Gtk.Grid): The grid on which this widget will be placed

            combo_list (list): A list of values to display in the combobox.
                This function expects a simple, one-dimensional list. For
                something more complex, see self.add_combo_with_data()

            prop (string or None): The name of the attribute in self.edit_obj
                whose value will be set to the contents of this widget. If
                None, no changes are made to self.edit_dict; it's up to the
                calling function to provide a .connect()

            x, y, wid, hei (int): Position on the grid at which the widget is
                placed

        Return values:

            The combobox widget created

        """

        store = Gtk.ListStore(str)
        for string in combo_list:
            store.append( [str(string)] )

        combo = Gtk.ComboBox.new_with_model(store)
        grid.attach(combo, x, y, wid, hei)
        renderer_text = Gtk.CellRendererText()
        combo.pack_start(renderer_text, True)
        combo.add_attribute(renderer_text, 'text', 0)
        combo.set_entry_text_column(0)

        if prop is not None:
            val = self.retrieve_val(prop)
            if val in combo_list:
                index = combo_list.index(val)
                combo.set_active(index)

            combo.connect('changed', self.on_combo_changed, prop)

        return combo


    def add_combo_with_data(self, grid, combo_list, prop, x, y, wid, hei):

        """Called by various functions in the child edit window.

        Adds a more complex Gtk.ComboBox to the tab's Gtk.Grid. This function
        expects a list of values in the form

            [ [val1, val2], [val1, val2], ... ]

        The combobox displays the 'val1' values. If one of them is selected,
        the corresponding 'val2' is used to set the attribute described by
        'prop'.

        Args:

            grid (Gtk.Grid): The grid on which this widget will be placed

            combo_list (list): The list described above. For something more
                simple, see self.add_combo()

            prop (string or None): The name of the attribute in self.edit_obj
                whose value will be set to the contents of this widget. If
                None, no changes are made to self.edit_dict; it's up to the
                calling function to provide a .connect()

            x, y, wid, hei (int): Position on the grid at which the widget is
                placed

        Return values:

            The combobox widget created

        """

        store = Gtk.ListStore(str, str)

        index_list = []
        for mini_list in combo_list:
            store.append( [ str(mini_list[0]), str(mini_list[1]) ] )
            index_list.append(mini_list[1])

        combo = Gtk.ComboBox.new_with_model(store)
        grid.attach(combo, x, y, wid, hei)
        renderer_text = Gtk.CellRendererText()
        combo.pack_start(renderer_text, True)
        combo.add_attribute(renderer_text, 'text', 0)
        combo.set_entry_text_column(0)

        if prop is not None:
            val = self.retrieve_val(prop)
            if val in index_list:
                index = index_list.index(val)
                combo.set_active(index)

            combo.connect('changed', self.on_combo_with_data_changed, prop)

        return combo


    def add_entry(self, grid, prop, x, y, wid, hei):

        """Called by various functions in the child edit window.

        Adds a Gtk.Entry to the tab's Gtk.Grid.

        Args:

            grid (Gtk.Grid): The grid on which this widget will be placed

            prop (string or None): The name of the attribute in self.edit_obj
                whose value will be set to the contents of this widget. If
                None, no changes are made to self.edit_dict; it's up to the
                calling function to provide a .connect()

            x, y, wid, hei (int): Position on the grid at which the widget is
                placed

        Return values:

            The entry widget created

        """

        entry = Gtk.Entry()
        grid.attach(entry, x, y, wid, hei)
        entry.set_hexpand(True)

        if prop is not None:
            value = self.retrieve_val(prop)
            if value is not None:
                entry.set_text(str(value))

            entry.connect('changed', self.on_entry_changed, prop)

        return entry


#   def add_image               # Inherited from GenericConfigWin


    def add_label(self, grid, text, x, y, wid, hei):

        """Called by various functions in the child edit window.

        Adds a Gtk.Label to the tab's Gtk.Grid.

        Args:

            grid (Gtk.Grid): The grid on which this widget will be placed

            text (str): Pango markup displayed in the label

            x, y, wid, hei (int): Position on the grid at which the widget is
                placed

        Return values:

            The label widget created

        """

        label = Gtk.Label()
        grid.attach(label, x, y, wid, hei)
        label.set_markup(text)
        label.set_hexpand(True)
        label.set_alignment(0, 0.5)

        return label


    def add_radiobutton(self, grid, prev_button, text, prop, value, x, y, \
    wid, hei):

        """Called by various functions in the child edit window.

        Adds a Gtk.RadioButton to the tab's Gtk.Grid.

        Args:

            grid (Gtk.Grid): The grid on which this widget will be placed

            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

            prop (string or None): The name of the attribute in self.edit_obj
                whose value will be set to the contents of this widget. If
                None, no changes are made to self.edit_dict; it's up to the
                calling function to provide a .connect()

            value (any): When this radiobutton becomes the active one, and if
                'prop' is not None, then 'prop' and 'value' are added as a new
                key-value pair to self.edit_dict

            x, y, wid, hei (int): Position on the grid at which the widget is
                placed

        Return values:

            The radiobutton widget created

        """

        radiobutton = Gtk.RadioButton.new_from_widget(prev_button)
        grid.attach(radiobutton, x, y, wid, hei)
        radiobutton.set_hexpand(True)
        if text is not None and text != '':
            radiobutton.set_label(text)

        if prop is not None:
            if value is not None and self.retrieve_val(prop) == value:
                radiobutton.set_active(True)

            radiobutton.connect(
                'toggled',
                self.on_radiobutton_toggled, prop, value,
            )

        return radiobutton


    def add_spinbutton(self, grid, min_val, max_val, step, prop, x, y, wid, \
    hei):

        """Called by various functions in the child edit window.

        Adds a Gtk.SpinButton to the tab's Gtk.Grid.

        Args:

            grid (Gtk.Grid): The grid on which this widget will be placed

            min_val (int): The minimum permitted in the spinbutton

            max_val (int or None): The maximum values permitted in the
                spinbutton. If None, this function assigns a very large maximum
                value (a billion)

            step (int): Clicking the up/down arrows in the spin button
                increments/decrements the value by this much

            prop (string or None): The name of the attribute in self.edit_obj
                whose value will be set to the contents of this widget. If
                None, no changes are made to self.edit_dict; it's up to the
                calling function to provide a .connect()

            x, y, wid, hei (int): Position on the grid at which the widget is
                placed

        Return values:

            The spinbutton widget created

        """

        # If the specified value of 'max_valu' was none, just use a very big
        #   number (as Gtk.SpinButton won't accept the None argument)
        if max_val is None:
            max_val = 1000000000

        spinbutton = Gtk.SpinButton.new_with_range(min_val, max_val, step)
        grid.attach(spinbutton, x, y, wid, hei)
        spinbutton.set_hexpand(False)

        if prop is not None:
            spinbutton.set_value(self.retrieve_val(prop))
            spinbutton.connect(
                'value-changed',
                self.on_spinbutton_changed,
                prop,
            )

        return spinbutton


    def add_textview(self, grid, prop, x, y, wid, hei):

        """Called by various functions in the child edit window.

        Adds a Gtk.TextView to the tab's Gtk.Grid. The contents of the textview
        are used as a single string (perhaps including newline characters) to
        set the value of a string attribute.

        Args:

            grid (Gtk.Grid): The grid on which this widget will be placed

            prop (string or None): The name of the attribute in self.edit_obj
                whose value will be set to the contents of this widget. The
                attribute can be an integer, string, list or tuple. If None, no
                changes are made to self.edit_dict; it's up to the calling
                function to provide a .connect()

            x, y, wid, hei (int): Position on the grid at which the widget is
                placed

        Return values:

            The textview and textbuffer widgets created

        """

        frame = Gtk.Frame()
        grid.attach(frame, x, y, wid, hei)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)

        textview = Gtk.TextView()
        scrolled.add(textview)

        textbuffer = textview.get_buffer()

        if prop is not None:
            value = self.retrieve_val(prop)
            if value is not None:
                if type(value) is list or type(value) is tuple:
                    textbuffer.set_text(str.join('\n', value))
                else:
                    textbuffer.set_text(str(value))

            textbuffer.connect('changed', self.on_textview_changed, prop)

        return textview, textbuffer


#   def add_treeview            # Inherited from GenericConfigWin


    # Callback class methods


    def on_button_apply_clicked(self, button):

        """Called from a callback in self.setup_button_strip().

        Applies any changes made by the user and re-draws the window's tabs,
        showing their new values.

        Args:

            button (Gtk.Button): The widget clicked

        """

        # Apply any changes the user has made
        self.apply_changes()

        # Remove all existing tabs from the notebook
        number = self.notebook.get_n_pages()
        if number:

            for count in range(0, number):
                self.notebook.remove_page(0)

        # Re-draw all the tabs
        self.setup_tabs()

        # Render the changes
        self.show_all()


    def on_button_cancel_clicked(self, button):

        """Called from a callback in self.setup_button_strip().

        Destroys any changes made by the user and re-draws the window's tabs,
        showing their original values.

        Args:

            button (Gtk.Button): The widget clicked

        """

        # Destroy the window
        self.destroy()


    def on_button_ok_clicked(self, button):

        """Called from a callback in self.setup_button_strip().

        Destroys any changes made by the user and then closes the window.

        Args:

            button (Gtk.Button): The widget clicked

        """

        # Apply any changes the user has made
        self.apply_changes()

        # Destroy the window
        self.destroy()


    def on_button_reset_clicked(self, button):

        """Called from a callback in self.setup_button_strip().

        Destroys any changes made by the user and re-draws the window's tabs,
        showing their original values.

        Args:

            button (Gtk.Button): The widget clicked

        """

        # Remove all existing tabs from the notebook
        number = self.notebook.get_n_pages()
        if number:

            for count in range(0, number):
                self.notebook.remove_page(0)

        # Empty self.edit_dict, destroying any changes the user has made
        self.edit_dict = {}

        # Re-draw all the tabs
        self.setup_tabs()

        # Render the changes
        self.show_all()


    def on_checkbutton_toggled(self, checkbutton, prop):

        """Called from a callback in self.add_checkbutton().

        Adds a key-value pair to self.edit_dict, using True if the button is
        selected, False if not.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            prop (str): The attribute in self.edit_obj to modify

        """

        if not checkbutton.get_active():
            self.edit_dict[prop] = False
        else:
            self.edit_dict[prop] = True


    def on_combo_changed(self, combo, prop):

        """Called from a callback in self.add_combo().

        Temporarily stores the contents of the widget in self.edit_dict.

        Args:

            combo (Gtk.ComboBox): The widget clicked

            prop (str): The attribute in self.edit_obj to modify

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        self.edit_dict[prop] = model[tree_iter][0]


    def on_combo_with_data_changed(self, combo, prop):

        """Called from a callback in self.add_combo_with_data().

        Extracts the value visible in the widget, converts it into another
        value, and stores the later in self.edit_dict.

        Args:

            combo (Gtk.ComboBox): The widget clicked

            prop (str): The attribute in self.edit_obj to modify

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        self.edit_dict[prop] = model[tree_iter][1]


    def on_entry_changed(self, entry, prop):

        """Called from a callback in self.add_entry().

        Temporarily stores the contents of the widget in self.edit_dict.

        Args:

            entry (Gtk.Entry): The widget clicked

            prop (str): The attribute in self.edit_obj to modify

        """

        self.edit_dict[prop] = entry.get_text()


    def on_radiobutton_toggled(self, radiobutton, prop, value):

        """Called from a callback in self.add_radiobutton().

        Adds a key-value pair to self.edit_dict, but only if this radiobutton
        (from those in the group) is the selected one.

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

            prop (str): The attribute in self.edit_obj to modify

            value (-): The attribute's new value

        """

        if radiobutton.get_active():
            self.edit_dict[prop] = value


    def on_spinbutton_changed(self, spinbutton, prop):

        """Called from a callback in self.add_spinbutton().

        Temporarily stores the contents of the widget in self.edit_dict.

        Args:

            spinbutton (Gtk.SpinkButton): The widget clicked

            prop (str): The attribute in self.edit_obj to modify

        """

        self.edit_dict[prop] = int(spinbutton.get_value())


    def on_textview_changed(self, textbuffer, prop):

        """Called from a callback in self.add_textview().

        Temporarily stores the contents of the widget in self.edit_dict.

        Args:

            textbuffer (Gtk.TextBuffer): The widget modified

            prop (str): The attribute in self.edit_obj to modify

        """

        text = textbuffer.get_text(
            textbuffer.get_start_iter(),
            textbuffer.get_end_iter(),
            # Don't include hidden characters
            False,
        )

        old_value = self.retrieve_val(prop)

        if type(old_value) is list or type(old_value) is tuple:
            self.edit_dict[prop] = text.splitlines()
        else:
            self.edit_dict[prop] = text


    # (Shared support functions)


    def add_container_properties(self, grid):

        """Called by VideoEditWin.setup_general_tab(),
        ChannelPlaylistEditWin.setup_general_tab() and
        FolderEditWin.setup_general_tab().

        Adds widgets common to those edit windows.

        Args:

            grid (Gtk.Grid): The grid on which widgets are arranged in their
                tab

        """

        entry = self.add_entry(grid,
            None,
            0, 1, 1, 1,
        )
        entry.set_text('#' + str(self.edit_obj.dbid))
        entry.set_editable(False)
        entry.set_hexpand(False)
        entry.set_width_chars(8)

        main_win_obj = self.app_obj.main_win_obj
        if isinstance(self.edit_obj, media.Video):
            icon_path = main_win_obj.icon_dict['video_small']
        elif isinstance(self.edit_obj, media.Channel):
            icon_path = main_win_obj.icon_dict['channel_small']
        elif isinstance(self.edit_obj, media.Playlist):
            icon_path = main_win_obj.icon_dict['playlist_small']
        else:

            if self.edit_obj.priv_flag:
                icon_path = main_win_obj.icon_dict['folder_red_small']
            elif self.edit_obj.temp_flag:
                icon_path = main_win_obj.icon_dict['folder_blue_small']
            elif self.edit_obj.fixed_flag:
                icon_path = main_win_obj.icon_dict['folder_green_small']
            else:
                icon_path = main_win_obj.icon_dict['folder_small']

        frame = self.add_image(grid,
            icon_path,
            1, 1, 1, 1,
        )
        # (The frame looks cramped without this. The icon itself is 16x16)
        frame.set_size_request(
            16 + (self.spacing_size * 2),
            -1,
        )

        entry2 = self.add_entry(grid,
            'name',
            2, 1, 1, 1,
        )
        entry2.set_editable(False)

        label = self.add_label(grid,
            _('Listed as'),
            0, 2, 1, 1,
        )
        label.set_hexpand(False)

        entry3 = self.add_entry(grid,
            'nickname',
            2, 2, 1, 1,
        )
        entry3.set_editable(True)

        label2 = self.add_label(grid,
            _('Contained in'),
            0, 3, 1, 1,
        )
        label2.set_hexpand(False)

        parent_obj = self.edit_obj.parent_obj
        if parent_obj:
            if isinstance(parent_obj, media.Channel):
                icon_path2 = main_win_obj.icon_dict['channel_small']
            elif isinstance(parent_obj, media.Playlist):
                icon_path2 = main_win_obj.icon_dict['playlist_small']
            else:

                if parent_obj.priv_flag:
                    icon_path2 = main_win_obj.icon_dict['folder_red_small']
                elif parent_obj.temp_flag:
                    icon_path2 = main_win_obj.icon_dict['folder_blue_small']
                elif parent_obj.fixed_flag:
                    icon_path2 = main_win_obj.icon_dict['folder_green_small']
                else:
                    icon_path2 = main_win_obj.icon_dict['folder_small']

        else:
            icon_path2 = main_win_obj.icon_dict['folder_black_small']

        frame2 = self.add_image(grid,
            icon_path2,
            1, 3, 1, 1,
        )
        frame2.set_size_request(
            16 + (self.spacing_size * 2),
            -1,
        )

        entry4 = self.add_entry(grid,
            None,
            2, 3, 1, 1,
        )
        entry4.set_editable(False)
        if parent_obj:
            entry4.set_text(parent_obj.name)


    def add_destination_properties(self, grid):

        """Called by ChannelPlaylistEditWin.setup_general_tab() and
        FolderEditWin.setup_general_tab().

        Adds widgets common to those edit windows.

        Args:

            grid (Gtk.Grid): The grid on which widgets are arranged in their
                tab

        """

        label = self.add_label(grid,
            _('Download to'),
            0, 5, 1, 1,
        )
        label.set_hexpand(False)

        main_win_obj = self.app_obj.main_win_obj
        dest_obj = self.app_obj.media_reg_dict[self.edit_obj.master_dbid]

        if self.edit_obj.external_dir is not None:
            if self.edit_obj.dbid in self.app_obj.container_unavailable_dict:
                icon_path = main_win_obj.icon_dict['unavailable_small']
            else:
                icon_path = main_win_obj.icon_dict['external_small']
        elif isinstance(dest_obj, media.Channel):
            icon_path = main_win_obj.icon_dict['channel_small']
        elif isinstance(dest_obj, media.Playlist):
            icon_path = main_win_obj.icon_dict['playlist_small']
        else:

            if dest_obj.priv_flag:
                icon_path = main_win_obj.icon_dict['folder_red_small']
            elif dest_obj.temp_flag:
                icon_path = main_win_obj.icon_dict['folder_blue_small']
            elif dest_obj.fixed_flag:
                icon_path = main_win_obj.icon_dict['folder_green_small']
            else:
                icon_path = main_win_obj.icon_dict['folder_small']

        frame = self.add_image(grid,
            icon_path,
            1, 5, 1, 1,
        )
        frame.set_size_request(
            16 + (self.spacing_size * 2),
            -1,
        )

        entry = self.add_entry(grid,
            None,
            2, 5, 1, 1,
        )
        entry.set_editable(False)
        if self.edit_obj.external_dir is not None:
            entry.set_text(self.edit_obj.external_dir)
        else:
            entry.set_text(dest_obj.name)

        label2 = self.add_label(grid,
            _('Default location'),
            0, 6, 2, 1,
        )
        label2.set_hexpand(False)

        entry2 = self.add_entry(grid,
            None,
            2, 6, 1, 1,
        )
        entry2.set_editable(False)
        entry2.set_text(self.edit_obj.get_default_dir(self.app_obj))


    def add_source_properties(self, grid):

        """Called by VideoEditWin.setup_general_tab() and
        ChannelPlaylistEditWin.setup_general_tab().

        Adds widgets common to those edit windows.

        Args:

            grid (Gtk.Grid): The grid on which widgets are arranged in their
                tab

        """

        media_type = self.edit_obj.get_type()
        if media_type == 'channel':
            string = _('Channel URL')
        elif media_type == 'playlist':
            string = _('Playlist URL')
        else:
            string = _('Video URL')

        label = self.add_label(grid,
            string,
            0, 4, 1, 1,
        )
        label.set_hexpand(False)

        entry = self.add_entry(grid,
            'source',
            2, 4, 1, 1,
        )
        entry.set_editable(False)


    def setup_download_options_tab(self):

        """Called by VideoEditWin.setup_tabs(),
        ChannelPlaylistEditWin.setup_tabs() and FolderEditWin.setup_tabs().

        Sets up the 'Download options' tab.
        """

        tab, grid = self.add_notebook_tab(_('_Options'))

        # (Many buttons are not clickable when a channel/playlist/folder's
        #   external directory is marked unavailable)
        unavailable_flag = False
        if isinstance(self.edit_obj, media.Video):
            if self.edit_obj.parent_obj.dbid \
            in self.app_obj.container_unavailable_dict:
                unavailable_flag = True
        elif self.edit_obj.dbid in self.app_obj.container_unavailable_dict:
            unavailable_flag = True

        # Download options
        self.add_label(grid,
            '<u>' + _('Download options') + '</u>',
            0, 0, 2, 1,
        )

        self.apply_options_button = Gtk.Button(_('Apply download options'))
        grid.attach(self.apply_options_button, 0, 1, 1, 1)
        self.apply_options_button.connect(
            'clicked',
            self.on_button_apply_options_clicked,
        )

        self.edit_options_button = Gtk.Button(_('Edit download options'))
        grid.attach(self.edit_options_button, 1, 1, 1, 1)
        self.edit_options_button.connect(
            'clicked',
            self.on_button_edit_options_clicked,
        )

        self.remove_options_button = Gtk.Button(_('Remove download options'))
        grid.attach(self.remove_options_button, 1, 2, 1, 1)
        self.remove_options_button.connect(
            'clicked',
            self.on_button_remove_options_clicked,
        )

        if self.edit_obj.options_obj or unavailable_flag:
            self.apply_options_button.set_sensitive(False)

        if not self.edit_obj.options_obj or unavailable_flag:
            self.edit_options_button.set_sensitive(False)
            self.remove_options_button.set_sensitive(False)


    # (Shared callbacks)


    def on_button_apply_options_clicked(self, button):

        """Called from callback in self.setup_download_options_tab().

        Apply download options to the media data object.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if self.edit_obj.options_obj:
            return self.app_obj.system_error(
                401,
                'Download options already applied',
            )

        # Apply download options to the media data object
        self.app_obj.apply_download_options(self.edit_obj)
        # (De)sensitise buttons appropriately
        self.apply_options_button.set_sensitive(False)
        self.edit_options_button.set_sensitive(True)
        self.remove_options_button.set_sensitive(True)


    def on_button_edit_options_clicked(self, button):

        """Called from callback in self.setup_download_options_tab().

        Edit download options for the media data object.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if not self.edit_obj.options_obj:
            return self.app_obj.system_error(
                402,
                'Download options not already applied',
            )

        # Open an edit window to show the options immediately
        OptionsEditWin(
            self.app_obj,
            self.edit_obj.options_obj,
        )


    def on_button_remove_options_clicked(self, button):

        """Called from callback in self.setup_download_options_tab().

        Remove download options from the media data object.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if not self.edit_obj.options_obj:
            return self.app_obj.system_error(
                403,
                'Download options not already applied',
            )

        # Remove download options from the media data object
        self.app_obj.remove_download_options(self.edit_obj)
        # (De)sensitise buttons appropriately
        self.apply_options_button.set_sensitive(True)
        self.edit_options_button.set_sensitive(False)
        self.remove_options_button.set_sensitive(False)


class GenericPrefWin(GenericConfigWin):

    """Generic Python class for windows in which the user can modify various
    system settings.

    Any modifications are applied immediately (unlike in an 'edit window', in
    which the modifications are stored temporarily, and only applied once the
    user has finished making changes).
    """


    # 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 preference window, if another preference window of the
        same class is already open.

        Args:

            app_obj (mainapp.TartubeApp): The main application object

        Return values:

            True if a duplicate is found, False if not

        """

        for config_win_obj in app_obj.main_win_obj.config_win_list:

            if type(self) == type(config_win_obj):

                # Duplicate found
                config_win_obj.present()
                return True

        # Not a duplicate
        return False


#   def setup():                # Inherited from GenericConfigWin


#   def setup_grid():           # Inherited from GenericConfigWin


#   def setup_notebook():       # Inherited from GenericConfigWin


#   def add_notebook_tab():     # Inherited from GenericConfigWin


    def setup_button_strip(self):

        """Called by self.setup().

        Creates a strip of buttons at the bottom of the window. For preference
        windows, there is only a single 'OK' button, which closes the window.
        """

        hbox = Gtk.HBox()
        self.grid.attach(hbox, 0, 2, 1, 1)

        # 'OK' button
        self.ok_button = Gtk.Button(_('OK'))
        hbox.pack_end(self.ok_button, False, False, self.spacing_size)
        self.ok_button.get_child().set_width_chars(10)
        self.ok_button.set_tooltip_text(_('Close this window'));
        self.ok_button.connect('clicked', self.on_button_ok_clicked)


#   def setup_gap():            # Inherited from GenericConfigWin


    # (Non-widget functions)


    def reset_window(self):

        """Can be called by anything.

        Redraws the window, without the need to destroy the old one and replace
        it with a new one.
        """

        # This code is copied from
        #   config.GenericEditWin.on_button_reset_clicked()

        # Remove all existing tabs from the notebook
        number = self.notebook.get_n_pages()
        if number:

            for count in range(0, number):
                self.notebook.remove_page(0)

        # Re-draw all the tabs
        self.setup_tabs()

        # Render the changes
        self.show_all()


    # (Add widgets)


    def add_checkbutton(self, grid, text, set_flag, mod_flag, x, y, wid, hei):

        """Called by various functions in the child preference window.

        Adds a Gtk.CheckButton to the tab's Gtk.Grid.

        Args:

            grid (Gtk.Grid): The grid on which this widget will be placed

            text (string or None): The text to display in the checkbutton's
                label. No label is used if 'text' is an empty string or None

            set_flag (bool): True if the checkbutton is selected

            mod_flag (bool): True if the checkbutton can be toggled by the user

            x, y, wid, hei (int): Position on the grid at which the widget is
                placed

        Return values:

            The checkbutton widget created

        """

        checkbutton = Gtk.CheckButton()
        grid.attach(checkbutton, x, y, wid, hei)
        checkbutton.set_active(set_flag)
        checkbutton.set_sensitive(mod_flag)
        checkbutton.set_hexpand(True)
        if text is not None and text != '':
            checkbutton.set_label(text)

        return checkbutton


    def add_combo(self, grid, combo_list, active_val, x, y, wid, hei):

        """Called by various functions in the child preference window.

        Adds a simple Gtk.ComboBox to the tab's Gtk.Grid.

        Args:

            grid (Gtk.Grid): The grid on which this widget will be placed

            combo_list (list): A list of values to display in the combobox.
                This function expects a simple, one-dimensional list

            active_val (string or None): If not None, a value matching one of
                the items in combo_list, that should be the active row in the
                combobox

            x, y, wid, hei (int): Position on the grid at which the widget is
                placed

        Return values:

            The combobox widget created

        """

        store = Gtk.ListStore(str)

        count = -1
        active_index = 0
        for string in combo_list:
            store.append( [string] )

            count += 1
            if active_val is not None and active_val == string:
                active_index = count

        combo = Gtk.ComboBox.new_with_model(store)
        grid.attach(combo, x, y, wid, hei)
        renderer_text = Gtk.CellRendererText()
        combo.pack_start(renderer_text, True)
        combo.add_attribute(renderer_text, 'text', 0)
        combo.set_entry_text_column(0)
        combo.set_active(active_index)

        return combo


    def add_combo_with_data(self, grid, combo_list, active_val, x, y, wid,
    hei):

        """Called by various functions in the child preference window.

        Adds a more complex Gtk.ComboBox to the tab's Gtk.Grid. This function
        expects a list of values in the form

            [ [val1, val2], [val1, val2], ... ]

        Args:

            grid (Gtk.Grid): The grid on which this widget will be placed

            combo_list (list): The list described above. For something more
                simple, see self.add_combo()

            active_val (string or None): If not None, a value matching a
                the second item ('val2') in one of the combo_list pairs; the
                specified pair is the active row in the combobox

            x, y, wid, hei (int): Position on the grid at which the widget is
                placed

        Return values:

            The combobox widget created

        """

        store = Gtk.ListStore(str, str)

        count = -1
        active_index = 0
        for mini_list in combo_list:
            store.append( [ str(mini_list[0]), str(mini_list[1]) ] )

            count += 1
            if active_val is not None and active_val == mini_list[1]:
                active_index = count

        combo = Gtk.ComboBox.new_with_model(store)
        grid.attach(combo, x, y, wid, hei)
        renderer_text = Gtk.CellRendererText()
        combo.pack_start(renderer_text, True)
        combo.add_attribute(renderer_text, 'text', 0)
        combo.set_entry_text_column(0)
        combo.set_active(active_index)

        return combo


    def add_entry(self, grid, text, edit_flag, x, y, wid, hei):

        """Called by various functions in the child preference window.

        Adds a Gtk.Entry to the tab's Gtk.Grid.

        Args:

            grid (Gtk.Grid): The grid on which this widget will be placed

            text (string or None): The initial contents of the entry.

            edit_flag (bool): True if the contents of the entry can be edited

            x, y, wid, hei (int): Position on the grid at which the widget is
                placed

        Return values:

            The entry widget created

        """

        entry = Gtk.Entry()
        grid.attach(entry, x, y, wid, hei)
        entry.set_hexpand(True)

        if text is not None:
            entry.set_text(str(text))

        if not edit_flag:
            entry.set_editable(False)

        return entry


#   def add_image               # Inherited from GenericConfigWin


    def add_label(self, grid, text, x, y, wid, hei):

        """Called by various functions in the child preference window.

        Adds a Gtk.Label to the tab's Gtk.Grid.

        Args:

            grid (Gtk.Grid): The grid on which this widget will be placed

            text (str): Pango markup displayed in the label

            x, y, wid, hei (int): Position on the grid at which the widget is
                placed

        Return values:

            The label widget created

        """

        label = Gtk.Label()
        grid.attach(label, x, y, wid, hei)
        label.set_markup(text)
        label.set_hexpand(True)
        label.set_alignment(0, 0.5)

        return label


    def add_radiobutton(self, grid, prev_button, text, x, y, wid, hei):

        """Called by various functions in the child preference window.

        Adds a Gtk.RadioButton to the tab's Gtk.Grid.

        Args:

            grid (Gtk.Grid): The grid on which this widget will be placed

            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

        Return values:

            The radiobutton widget created

        """

        radiobutton = Gtk.RadioButton.new_from_widget(prev_button)
        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_spinbutton(self, grid, min_val, max_val, step, val, x, y, wid, \
    hei):

        """Called by various functions in the child preference window.

        Adds a Gtk.SpinButton to the tab's Gtk.Grid.

        Args:

            grid (Gtk.Grid): The grid on which this widget will be placed

            min_val (int): The minimum permitted in the spinbutton

            max_val (int or None): The maximum values permitted in the
                spinbutton. If None, this function assigns a very large maximum
                value (a billion)

            step (int): Clicking the up/down arrows in the spin button
                increments/decrements the value by this much

            val (int): The current value of the spinbutton

            x, y, wid, hei (int): Position on the grid at which the widget is
                placed

        Return values:

            The spinbutton widget created

        """

        # If the specified value of 'max_valu' was none, just use a very big
        #   number (as Gtk.SpinButton won't accept the None argument)
        if max_val is None:
            max_val = 1000000000

        spinbutton = Gtk.SpinButton.new_with_range(min_val, max_val, step)
        grid.attach(spinbutton, x, y, wid, hei)
        spinbutton.set_value(val)
        spinbutton.set_hexpand(False)

        return spinbutton


    def add_textview(self, grid, contents_list, x, y, wid, hei):

        """Called by various functions in the child preference window.

        Adds a Gtk.TextView to the tab's Gtk.Grid.

        Args:

            grid (Gtk.Grid): The grid on which this widget will be placed

            contents_list (list): The initial contents of the textview. Each
                item in the list is a line in the textview.

            x, y, wid, hei (int): Position on the grid at which the widget is
                placed

        Return values:

            The textview and textbuffer widgets created

        """

        frame = Gtk.Frame()
        grid.attach(frame, x, y, wid, hei)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)

        textview = Gtk.TextView()
        scrolled.add(textview)

        textbuffer = textview.get_buffer()

        if contents_list:
            textbuffer.set_text(str.join('\n', contents_list))

        return textview, textbuffer


#   def add_treeview            # Inherited from GenericConfigWin


    # Callback class methods


    def on_button_ok_clicked(self, button):

        """Called from a callback in self.setup_button_strip().

        Closes the window.

        Args:

            button (Gtk.Button): The button clicked

        """

        # Destroy the window
        self.destroy()


class CustomDLEditWin(GenericEditWin):

    """Python class for an 'edit window' to modify values in a
    downloads.CustomDLManager object.

    Args:

        app_obj (mainapp.TartubeApp): The main application object

        edit_obj (downloads.CustomDLManager): The object whose attributes will
            be edited in this window

    """


    # Standard class methods


    def __init__(self, app_obj, edit_obj):

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Custom downloads window starts here.' \
            + ' In the menu, click Edit > System preferences...' \
            + ' > Operations > Custom > Edit'
        )

        Gtk.Window.__init__(self, title=_('Custom download settings'))

        if self.is_duplicate(app_obj, edit_obj):
            return

        # IV list - class objects
        # -----------------------
        # The mainapp.TartubeApp object
        self.app_obj = app_obj
        # The downloads.CustomDLManager object being edited
        self.edit_obj = edit_obj


        # IV list - Gtk widgets
        # ---------------------
        self.grid = None                        # Gtk.Grid
        self.notebook = None                    # Gtk.Notebook
        self.reset_button = None                # Gtk.Button
        self.apply_button = None                # Gtk.Button
        self.ok_button = None                   # Gtk.Button
        self.cancel_button = None               # Gtk.Button
        # (From self.setup_name_tab)
        self.button = None                      # Gtk.Button
        self.button2 = None                     # Gtk.Button
        self.checkbutton = None                 # Gtk.CheckButton
        self.checkbutton2 = None                # Gtk.CheckButton
        # (From self.setup_subtitles_tab)
        self.checkbutton3 = None                # Gtk.CheckButton
        self.checkbutton4 = None                # Gtk.CheckButton
        self.treeview = None                    # Gtk.TreeView
        self.liststore = None                   # Gtk.ListStore
        self.button3 = None                     # Gtk.Button
        self.treeview2 = None                   # Gtk.TreeView
        self.liststore2 = None                  # Gtk.ListStore
        self.button4 = None                     # Gtk.Button
        # (From self.setup_clips_tab)
        self.checkbutton5 = None                # Gtk.CheckButton
        # (From self.setup_slices_tab)
        self.checkbutton6 = None                # Gtk.CheckButton
        self.liststore3 = None                  # Gtk.ListStore
        self.button5 = None                     # Gkt.Button
        self.button6 = None                     # Gkt.Button
        # (From self.setup_delay_tab)
        self.checkbutton7 = None                # Gtk.CheckButton
        self.spinbutton = None                  # Gtk.SpinButton
        self.spinbutton2 = None                 # Gtk.SpinButton
        # (From self.setup_mirrors_tab)
        self.radiobutton = None                 # Gtk.RadioButton
        self.radiobutton2 = None                # Gtk.RadioButton
        self.radiobutton3 = None                # Gtk.RadioButton
        self.radiobutton4 = None                # Gtk.RadioButton
        # (From self.setup_livestreams_tab)
        self.checkbutton8 = None                # Gtk.CheckButton
        self.checkbutton9 = None                # Gtk.CheckButton
        self.checkbutton10 = None               # Gtk.CheckButton
        self.checkbutton11 = None               # Gtk.CheckButton


        # IV list - other
        # ---------------
        # Size (in pixels) of gaps between edit window widgets
        self.spacing_size = self.app_obj.default_spacing_size
        # Flag set to True if all four buttons ('Reset', 'Apply', 'Cancel' and
        #   'OK') are required, or False if just the 'OK' button is required
        self.multi_button_flag = True

        # When the user changes a value, it is not applied to self.edit_obj
        #   immediately; instead, it is stored temporarily in this dictionary
        # If the user clicks the 'OK' or 'Apply' buttons at the bottom of the
        #   window, the changes are applied to self.edit_obj
        # If the user clicks the 'Reset' or 'Cancel' buttons, the dictionary
        #   is emptied and the changes are lost
        self.edit_dict = {}


        # Code
        # ----

        # Set up the edit window
        self.setup()


    # Public class methods


#   def is_duplicate():         # Inherited from GenericConfigWin


#   def setup():                # Inherited from GenericConfigWin


#   def setup_grid():           # Inherited from GenericConfigWin


#   def setup_notebook():       # Inherited from GenericConfigWin


#   def add_notebook_tab():     # Inherited from GenericConfigWin


#   def setup_button_strip():   # Inherited from GenericEditWin


#   def setup_gap():            # Inherited from GenericConfigWin


    # (Non-widget functions)


    def apply_changes(self):

        """Called by self.on_button_ok_clicked() and
        self.on_button_apply_clicked().

        Any changes the user has made are temporarily stored in self.edit_dict.
        Apply to those changes to the object being edited.
        """

        # Import the main window (for convenience)
        main_win_obj = self.app_obj.main_win_obj

        # Apply any changes the user has made
        for key in self.edit_dict.keys():
            setattr(self.edit_obj, key, self.edit_dict[key])


        # If 'divert_mode' has changed, the Video Catalogue must be redrawn
        if 'divert_mode' in self.edit_dict \
        and self.app_obj.catalogue_mode_type != 'simple' \
        and main_win_obj.video_index_current_dbid is not None:

            main_win_obj.video_catalogue_redraw_all(
                main_win_obj.video_index_current_dbid,
                main_win_obj.catalogue_toolbar_current_page,
            )

        # The changes can now be cleared
        self.edit_dict = {}


#   def retrieve_val():         # Inherited from GenericConfigWin


    # (Setup tabs)


    def setup_tabs(self):

        """Called by self.setup(), .on_button_apply_clicked() and
        .on_button_reset_clicked().

        Sets up the tabs for this edit window.
        """

        self.setup_name_tab()
        self.setup_subtitles_tab()
        self.setup_clips_tab()
        self.setup_slices_tab()
        self.setup_delay_tab()
        self.setup_mirrors_tab()
        self.setup_livestreams_tab()

        # Unusual step - signal connects go here, after all widgets have been
        #   created

        # (From self.setup_name_tab)
        self.button.connect('clicked', self.on_clone_settings_clicked)
        self.button2.connect('clicked', self.on_reset_settings_clicked)
        self.checkbutton.connect(
            'toggled',
            self.on_dl_by_video_button_toggled,
        )
        self.checkbutton2.connect(
            'toggled',
            self.on_dl_precede_button_toggled,
        )

        # (From self.setup_subtitles_tab)
        self.checkbutton3.connect(
            'toggled',
            self.on_dl_if_subs_button_toggled,
        )
        self.button3.connect('clicked', self.on_add_language_clicked)
        self.button4.connect('clicked', self.on_remove_language_clicked)

        # (From self.setup_clips_tab)
        self.checkbutton5.connect(
            'toggled',
            self.on_split_button_toggled,
        )

        # (From self.setup_slices_tab)
        self.checkbutton6.connect(
            'toggled',
            self.on_slice_button_toggled,
        )
        self.button5.connect('clicked', self.on_select_all_button_clicked)
        self.button6.connect('clicked', self.on_unselect_all_button_clicked)

        # (From self.setup_delay_tab)
        self.checkbutton7.connect(
            'toggled',
            self.on_delay_button_toggled,
        )
        self.spinbutton.connect(
            'value-changed',
            self.on_delay_spinbutton_changed,
        )

        # (From self.setup_mirrors_tab)
        self.radiobutton.connect(
            'toggled',
            self.on_divert_button_toggled,
        )
        self.radiobutton2.connect(
            'toggled',
            self.on_divert_button_toggled,
        )
        self.radiobutton3.connect(
            'toggled',
            self.on_divert_button_toggled,
        )
        self.radiobutton4.connect(
            'toggled',
            self.on_divert_button_toggled,
        )

        # (From self.setup_livestreams_tab)
        self.checkbutton8.connect(
            'toggled',
            self.on_ignore_live_button_toggled,
        )
        self.checkbutton9.connect(
            'toggled',
            self.on_ignore_old_live_button_toggled,
        )
        self.checkbutton10.connect(
            'toggled',
            self.on_dl_if_live_button_toggled,
        )
        self.checkbutton11.connect(
            'toggled',
            self.on_dl_if_old_live_button_toggled,
        )


    def setup_name_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Name' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Custom downloads > Name'
        )

        tab, grid = self.add_notebook_tab(_('_Name'))
        grid_width = 4

        label = self.add_label(grid,
            _('Name'),
            0, 0, 2, 1,
        )

        entry = self.add_entry(grid,
            'name',
            2, 0, 1, 1,
        )
        entry.set_hexpand(True)

        entry2 = self.add_entry(grid,
            None,
            3, 0, 1, 1,
        )
        entry2.set_text('#' + str(self.edit_obj.uid))
        entry2.set_hexpand(False)

        label2 = self.add_label(grid,
            _('Usage'),
            0, 1, 2, 1,
        )

        entry3 = self.add_entry(grid,
            None,
            2, 1, 2, 1,
        )
        entry3.set_editable(False)

        if self.edit_obj == self.app_obj.general_custom_dl_obj:
            entry3.set_text(
                _('Applies everywhere except the Classic Mode tab'),
            )
        elif self.edit_obj == self.app_obj.classic_custom_dl_obj:
            entry3.set_text(_('Applies to the Classic Mode tab'))
        else:
            entry3.set_text(_('Applies when selected'))

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 2, grid_width, 1)
        grid_width2 = 3
        grid2.set_vexpand(True)

        # (Empty label for spacing)
        label3 = Gtk.Label()
        grid2.attach(label3, 0, 0, grid_width2, 1)
        label3.set_hexpand(True)

        # (Frame containing text)
        frame = Gtk.Frame()
        grid2.attach(frame, 1, 1, 1, 1)
        frame.set_hexpand(False)

        grid3 = Gtk.Grid()
        frame.add(grid3)
        grid3.set_border_width(self.spacing_size * 2)
        grid3.set_column_spacing(self.spacing_size)
        grid3.set_row_spacing(self.spacing_size)
        grid3.set_hexpand(False)

        self.add_label(grid3,
            '<b>' + _('HINT') + '</b>: ' + _('Enable these settings first!'),
            0, 0, 1, 1,
        )

        self.checkbutton = self.add_checkbutton(grid3,
            _('Download each video independently of its channel or playlist'),
            None,
            0, 1, 1, 1,
        )
        self.checkbutton.set_active(self.edit_obj.dl_by_video_flag)

        self.checkbutton2 = self.add_checkbutton(grid3,
            _(
                'Check channels/playlists/folders before each custom' \
                + ' download (recommended)',
            ),
            None,
            0, 2, 1, 1,
        )
        self.checkbutton2.set_active(self.edit_obj.dl_precede_flag)
        if not self.edit_obj.dl_by_video_flag \
        or (
            self.app_obj.classic_custom_dl_obj is not None \
            and self.app_obj.classic_custom_dl_obj == self.edit_obj
        ):
            self.checkbutton2.set_sensitive(False)

        # (Frame containing text)
        frame2 = Gtk.Frame()
        grid2.attach(frame2, 1, 2, 1, 1)
        frame.set_hexpand(False)

        grid4 = Gtk.Grid()
        frame2.add(grid4)
        grid4.set_border_width(self.spacing_size * 2)
        grid4.set_column_spacing(self.spacing_size)
        grid4.set_row_spacing(self.spacing_size)
        grid4.set_hexpand(False)

        self.add_label(grid4,
            '<b>' + _('HINT') + '</b>: ' + _(
                'The <b>Check all</b> and <b>Download all</b> buttons will' \
                + ' not start a custom download!',
            ),
            0, 0, 1, 1,
        )
        self.add_label(grid4,
            _(
                'Use the main menu, or right-click a video, channel,' \
                + ' playlist or folder!',
            ),
            0, 1, 1, 1,
        )

        # (Strips of widgets at the bottom of the primary grid)
        frame3 = self.add_pixbuf(grid,
            'copy_large',
            0, 3, 1, 1,
        )
        frame3.set_hexpand(False)

        self.button = Gtk.Button(
            _(
                'Import settings from the general custom download into this' \
                + ' window',
            ),
        )
        grid.attach(self.button, 1, 3, (grid_width - 1), 1)
        self.button.set_hexpand(True)
        if self.edit_obj == self.app_obj.general_custom_dl_obj:
            # No point cloning the General Custom Download Manager onto itself
            self.button.set_sensitive(False)

        frame4 = self.add_pixbuf(grid,
            'warning_large',
            0, 4, 1, 1,
        )
        frame4.set_hexpand(False)

        self.button2 = Gtk.Button(
            _('Completely reset all settings to their default values'),
        )
        grid.attach(self.button2, 1, 4, (grid_width - 1), 1)
        self.button2.set_hexpand(True)


    def setup_subtitles_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Subtitles' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Custom downloads > Subtitles'
        )

        tab, grid = self.add_notebook_tab(_('_Subtitles'))
        grid.set_column_homogeneous(True)
        grid.set_row_homogeneous(False)
        grid_width = 2

        if not self.edit_obj.dl_by_video_flag \
        or not self.edit_obj.dl_precede_flag:
            desens_flag = True
        else:
            desens_flag = False

        # Subtitles settings
        self.add_label(grid,
            '<u>' + _('Subtitles settings') + '</u>',
            0, 0, grid_width, 1,
        )

        self.add_label(grid,
            '<i>' + _(
                'Note: this tab downloads videos. To download subtitles, use' \
                + ' the download options windows',
            ) + '</i>',
            0, 1, grid_width, 1,
        )

        self.checkbutton3 = self.add_checkbutton(grid,
            _('Only download videos with available subtitles'),
            None,
            0, 2, grid_width, 1,
        )
        self.checkbutton3.set_active(self.edit_obj.dl_if_subs_flag)
        if desens_flag:
            self.checkbutton3.set_sensitive(False)

        self.checkbutton4 = self.add_checkbutton(grid,
            _(
            'During the custom download, don\'t add videos without subtitles' \
            + ' to the database at all',
            ),
            'ignore_if_no_subs_flag',
            0, 3, grid_width, 1,
        )
        self.checkbutton4.set_active(self.edit_obj.ignore_if_no_subs_flag)
        if desens_flag or not self.edit_obj.dl_if_subs_flag:
            self.checkbutton4.set_sensitive(False)

        self.add_label(grid,
            _(
            'Require subtitles in these languages (leave empty to download' \
            + ' videos with any subtitles):',
            ),
            0, 4, grid_width, 1,
        )

        self.treeview, self.liststore = self.add_treeview(grid,
            0, 5, 1, 1)
        self.treeview.set_vexpand(True)

        for language in formats.LANGUAGE_CODE_LIST:
            self.liststore.append([
                language + ' [' + formats.LANGUAGE_CODE_DICT[language] + ']',
            ])

        self.button3 = Gtk.Button(_('Add language') + ' >>>')
        grid.attach(self.button3, 0, 6, 1, 1)
        if desens_flag or not self.edit_obj.dl_if_subs_flag:
            self.button3.set_sensitive(False)

        self.treeview2, self.liststore2 = self.add_treeview(grid,
            1, 5, 1, 1)
        self.treeview2.set_vexpand(True)

        # Initialise the right-hand treeview
        self.setup_subtitles_tab_redraw_list()

        self.button4 = Gtk.Button('<<< ' + _('Remove language'))
        grid.attach(self.button4, 1, 6, 1, 1)
        if desens_flag or not self.edit_obj.dl_if_subs_flag:
            self.button4.set_sensitive(False)


    def setup_subtitles_tab_redraw_list(self):

        """Called by self.setup_subtitles_tab() and then again by
        self.on_add_languages_clicked() and .on_remove_languages_clicked().

        Update the Gtk.ListStore containing the user's preferred video/audio
        formats.
        """

        # Empty the treeview
        self.liststore2.clear()

        # (Need to reverse formats.LANGUAGE_CODE_DICT for quick lookup)
        rev_dict = {}
        for key in formats.LANGUAGE_CODE_DICT:
            rev_dict[formats.LANGUAGE_CODE_DICT[key]] = key

        # Refill the treeview
        lang_list = self.retrieve_val('dl_if_subs_list')
        for lang_code in lang_list:
            self.liststore2.append([
                rev_dict[lang_code] + ' [' + lang_code + ']',
            ])


    def setup_clips_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Clips' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Custom downloads > Clips'
        )

        tab, grid = self.add_notebook_tab(_('_Clips'))

        # Clip settings
        self.add_label(grid,
            '<u>' + _('Clip settings') + '</u>',
            0, 0, 1, 1,
        )

        self.checkbutton5 = self.add_checkbutton(grid,
            _(
                'Split videos into video clips using timestamps (requires' \
                + ' FFmpeg)',
            ),
            None,
            0, 1, 1, 1,
        )
        self.checkbutton5.set_active(self.edit_obj.split_flag)
        if not self.edit_obj.dl_by_video_flag:
            self.checkbutton5.set_sensitive(False)


    def setup_slices_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Slices' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Custom downloads > Slices'
        )

        tab, grid = self.add_notebook_tab(_('S_lices'))
        grid_width = 2

        # Slice settings
        self.add_label(grid,
            '<u>' + _('Slice settings') + '</u>',
            0, 0, grid_width, 1,
        )

        self.checkbutton6 = self.add_checkbutton(grid,
            _(
                'Remove slices from the video using SponsorBlock data' \
                + ' (requires FFmpeg)'),
            None,
            0, 1, grid_width, 1,
        )
        self.checkbutton6.set_active(self.edit_obj.slice_flag)
        if not self.edit_obj.dl_by_video_flag \
        or self.edit_obj.split_flag:
            self.checkbutton6.set_sensitive(False)

        # (GenericConfigWin.add_treeview() doesn't support multiple columns, so
        #   we'll do everything ourselves)
        frame = Gtk.Frame()
        grid.attach(frame, 0, 2, 1, 8)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)
        scrolled.set_hexpand(True)

        treeview = Gtk.TreeView()
        scrolled.add(treeview)
        treeview.set_headers_visible(True)

        for i, column_title in enumerate(
            [ _('Remove'), _('Type'), ]
        ):
            if i == 0:
                renderer_toggle = Gtk.CellRendererToggle()
                column_toggle = Gtk.TreeViewColumn(
                    column_title,
                    renderer_toggle,
                    active=i,
                )
                treeview.append_column(column_toggle)
                column_toggle.set_resizable(False)
                renderer_toggle.set_sensitive(True)
                renderer_toggle.set_activatable(True)
                renderer_toggle.connect(
                    'toggled',
                    self.on_treeview_button_toggled,
                )
            else:
                renderer_text = Gtk.CellRendererText()
                column_text = Gtk.TreeViewColumn(
                    column_title,
                    renderer_text,
                    text=i,
                )
                treeview.append_column(column_text)
                column_text.set_resizable(True)

        self.liststore3 = Gtk.ListStore(bool, str)
        treeview.set_model(self.liststore3)

        # Initialise the list
        self.setup_slices_tab_update_treeview()

        # Editing buttons
        self.button5 = Gtk.Button(_('Remove all'))
        grid.attach(self.button5, 1, 2, 1, 1)
        self.button5.set_hexpand(True)
        if not self.edit_obj.slice_flag:
            self.button5.set_sensitive(False)

        self.button6 = Gtk.Button(_('Remove none'))
        grid.attach(self.button6, 1, 3, 1, 1)
        self.button6.set_hexpand(True)
        if not self.edit_obj.slice_flag:
            self.button6.set_sensitive(False)

        # (Empty labels for aesthetics)
        for i in range(8):
            self.add_label(grid,
                '',
                1, (4 + i), 1, 1,
            )


    def setup_slices_tab_update_treeview(self):

        """ Called by self.setup_downloads_tab.

        Fills or updates the treeview.
        """

        self.liststore3.clear()

        slice_dict = self.retrieve_val('slice_dict')
        for category in formats.SPONSORBLOCK_CATEGORY_LIST:

            row_list = []
            if slice_dict[category]:
                row_list.append(True)
            else:
                row_list.append(False)

            row_list.append(category)

            self.liststore3.append(row_list)


    def setup_delay_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Delays' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Custom downloads > Delay'
        )

        tab, grid = self.add_notebook_tab(_('_Delays'))
        grid_width = 2

        # Download delay settings
        self.add_label(grid,
            '<u>' + _('Download delay settings') + '</u>',
            0, 0, grid_width, 1,
        )

        self.checkbutton7 = self.add_checkbutton(grid,
            _('Apply a delay after each video/channel/playlist is downloaded'),
            None,
            0, 1, grid_width, 1,
        )
        self.checkbutton7.set_active(self.edit_obj.delay_flag)

        self.add_label(grid,
            _('Maximum delay to apply (in minutes)'),
            0, 2, 1, 1,
        )

        self.spinbutton = self.add_spinbutton(grid,
            0.2,
            None,
            0.2,                    # Step
            'delay_max',
            1, 2, 1, 1,
        )
        if not self.edit_obj.delay_flag:
            self.spinbutton.set_sensitive(False)

        self.add_label(grid,
            _(
            'Minimum delay to apply (in minutes; randomises the actual delay)'
            ),
            0, 3, 1, 1,
        )

        self.spinbutton2 = self.add_spinbutton(grid,
            0,
            None,
            0.2,                    # Step
            'delay_min',
            1, 3, 1, 1,
        )
        if not self.edit_obj.delay_flag:
            self.spinbutton2.set_sensitive(False)


    def setup_mirrors_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Mirrors' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Custom downloads > Mirrors'
        )

        tab, grid = self.add_notebook_tab(_('_Mirrors'))
        grid_width = 2

        # Mirror settings
        self.add_label(grid,
            '<u>' + _('Mirror settings') + '</u>',
            0, 0, grid_width, 1,
        )

        self.radiobutton = self.add_radiobutton(grid,
            None,
            _('Obtain a YouTube video from the original website'),
            None,
            None,
            0, 1, grid_width, 1,
        )

        self.radiobutton2 = self.add_radiobutton(grid,
            self.radiobutton,
            _('Obtain the video from HookTube rather than YouTube'),
            None,
            None,
            0, 2, grid_width, 1,
        )
        if self.edit_obj.divert_mode == 'hooktube':
            self.radiobutton2.set_active(True)

        self.radiobutton3 = self.add_radiobutton(grid,
            self.radiobutton2,
            _('Obtain the video from Invidious rather than YouTube'),
            None,
            None,
            0, 3, grid_width, 1,
        )
        if self.edit_obj.divert_mode == 'invidious':
            self.radiobutton3.set_active(True)

        self.radiobutton4 = self.add_radiobutton(grid,
            self.radiobutton3,
            _('Obtain the video from this YouTube front-end:'),
            None,
            None,
            0, 4, 1, 1,
        )
        if self.edit_obj.divert_mode == 'other':
            self.radiobutton4.set_active(True)

        self.entry = self.add_entry(grid,
            'divert_website',
            1, 4, 1, 1,
        )
        self.entry.set_hexpand(True)
        if not self.edit_obj.divert_mode == 'other':
            self.entry.set_sensitive(False)

        msg = _('Type the exact text that replaces www.youtube.com e.g.')
        msg = re.sub('www.youtube.com', '   <b>www.youtube.com</b>   ', msg)

        self.add_label(grid,
            '<i>' + msg + '   <b>hooktube.com</b></i>',
            0, 6, grid_width, 1,
        )

        if not self.edit_obj.dl_by_video_flag:
            self.radiobutton.set_sensitive(False)
            self.radiobutton2.set_sensitive(False)
            self.radiobutton3.set_sensitive(False)
            self.radiobutton4.set_sensitive(False)
            self.entry.set_sensitive(False)


    def setup_livestreams_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Livestreams' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Custom downloads > Livestreams'
        )

        tab, grid = self.add_notebook_tab(_('L_ivestreams'))

        # Livestream settings
        self.add_label(grid,
            '<u>' + _('Livestream settings') + '</u>',
            0, 0, 1, 1,
        )

        self.checkbutton8 = self.add_checkbutton(grid,
            _('Don\'t download broadcasting livestreams'),
            None,
            0, 1, 1, 1,
        )
        self.checkbutton8.set_active(self.edit_obj.ignore_stream_flag)
        if not self.edit_obj.dl_by_video_flag:
            self.checkbutton8.set_sensitive(False)

        self.checkbutton9 = self.add_checkbutton(grid,
            _('Don\'t download finished livestreams'),
            None,
            0, 2, 1, 1,
        )
        self.checkbutton9.set_active(self.edit_obj.ignore_old_stream_flag)
        if not self.edit_obj.dl_by_video_flag:
            self.checkbutton9.set_sensitive(False)

        self.checkbutton10 = self.add_checkbutton(grid,
            _('Only download broadcasting livestreams'),
            None,
            0, 3, 1, 1,
        )
        self.checkbutton10.set_active(self.edit_obj.ignore_stream_flag)
        if not self.edit_obj.dl_by_video_flag:
            self.checkbutton10.set_sensitive(False)

        self.checkbutton11 = self.add_checkbutton(grid,
            _('Only download finished livestreams'),
            None,
            0, 4, 1, 1,
        )
        self.checkbutton11.set_active(self.edit_obj.ignore_old_stream_flag)
        if not self.edit_obj.dl_by_video_flag:
            self.checkbutton11.set_sensitive(False)


    # Callback class methods


    def on_add_language_clicked(self, button):

        """Called by callback in self.setup_tabs().

        Args:

            button (Gtk.Button): The widget clicked

        """

        selection = self.treeview.get_selection()
        (model, tree_iter) = selection.get_selected()
        if tree_iter is None:

            # Nothing selected
            return

        name = model[tree_iter][0]
        # From a string in the form 'English [en]', remove the language code to
        #    get a key in formats.LANGUAGE_CODE_DICT, e.g. 'English'
        match = re.search(r'^(.*)\s\[', name)
        if match:

            language = match.groups()[0]
            if language in formats.LANGUAGE_CODE_DICT:

                lang_code = formats.LANGUAGE_CODE_DICT[language]
                new_list = self.retrieve_val('dl_if_subs_list')
                # (Don't add duplicates)
                if lang_code not in new_list:

                    new_list.append(lang_code)

                    self.edit_dict['dl_if_subs_list'] = new_list

                    # Update the treeview
                    self.setup_subtitles_tab_redraw_list()


    def on_clone_settings_clicked(self, button):

        """Called by callback in self.setup_tabs().

        Args:

            button (Gtk.Button): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' Custom downloads > Name'
        )

        self.app_obj.dialogue_manager_obj.show_msg_dialogue(
            _(
            'This procedure cannot be reversed. Are you sure you want to' \
            + ' continue?',
            ),
            'question',
            'yes-no',
            self,           # Parent window is this window
            {
                'yes': 'clone_custom_dl_manager_from_window',
                'data': [self, self.edit_obj],
            },
        )


    def on_delay_button_toggled(self, checkbutton):

        """Called from callback in self.setup_tabs().

        Enables/disables delays between downloads of individual media data
        objects.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active():

            self.edit_dict['delay_flag'] = True

            self.spinbutton.set_sensitive(True)
            self.spinbutton2.set_sensitive(True)

        else:

            self.edit_dict['delay_flag'] = False

            self.spinbutton.set_sensitive(False)
            self.spinbutton2.set_sensitive(False)


    def on_delay_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_tabs().

        Updates both delay spinbutton widgets, as well as setting the IV.

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        value = spinbutton.get_value()
        self.edit_dict['delay_max'] = value
        self.spinbutton2.set_range(0, value)


    def on_divert_button_toggled(self, radiobutton):

        """Called from callback in self.setup_tabs().

        Sets the YouTube mirror from which downloads are obtained.

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

        """

        if self.radiobutton.get_active():
            self.edit_dict['divert_mode'] = 'default'
        elif self.radiobutton2.get_active():
            self.edit_dict['divert_mode'] = 'hooktube'
        elif self.radiobutton3.get_active():
            self.edit_dict['divert_mode'] = 'invidious'
        elif self.radiobutton4.get_active():
            self.edit_dict['divert_mode'] = 'other'

        if self.radiobutton4.get_active():
            self.entry.set_sensitive(True)
        else:
            self.entry.set_sensitive(False)
            self.entry.set_text('')


    def on_dl_by_video_button_toggled(self, checkbutton):

        """Called from callback in self.setup_tabs().

        Enables/disables downloading videos independently of their channels/
        playlists.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active():

            self.edit_dict['dl_by_video_flag'] = True

            self.checkbutton2.set_active(True)
            self.checkbutton2.set_sensitive(True)
            if not self.retrieve_val('dl_precede_flag'):
                self.checkbutton3.set_active(False)
                self.checkbutton3.set_sensitive(False)
                self.checkbutton4.set_active(False)
                self.checkbutton4.set_sensitive(False)
                self.button3.set_sensitive(False)
                self.button4.set_sensitive(False)
            else:
                self.checkbutton3.set_sensitive(True)
                if not self.retrieve_val('dl_if_subs_flag'):
                    self.checkbutton4.set_active(False)
                    self.checkbutton4.set_sensitive(False)
                    self.button3.set_sensitive(False)
                    self.button4.set_sensitive(False)
                else:
                    if not self.retrieve_val('ignore_if_no_subs_flag'):
                        self.checkbutton4.set_active(False)
                        self.checkbutton4.set_sensitive(False)
                    else:
                        self.checkbutton4.set_sensitive(True)
                    self.button3.set_sensitive(True)
                    self.button4.set_sensitive(True)
            self.checkbutton5.set_sensitive(True)
            if self.retrieve_val('split_flag'):
                self.checkbutton6.set_sensitive(False)
            else:
                self.checkbutton6.set_sensitive(True)
            self.radiobutton.set_sensitive(True)
            self.radiobutton2.set_sensitive(True)
            self.radiobutton3.set_sensitive(True)
            self.radiobutton4.set_sensitive(True)
            if self.retrieve_val('divert_mode') == 'other':
                self.entry.set_sensitive(True)
            else:
                self.entry.set_sensitive(False)
            self.checkbutton8.set_sensitive(True)
            self.checkbutton9.set_sensitive(True)
            self.checkbutton10.set_sensitive(True)
            self.checkbutton11.set_sensitive(True)

        else:

            self.edit_dict['dl_by_video_flag'] = False

            self.checkbutton2.set_sensitive(False)
            self.checkbutton2.set_active(False)
            self.checkbutton3.set_sensitive(False)
            self.checkbutton3.set_active(False)
            self.checkbutton4.set_sensitive(False)
            self.checkbutton4.set_active(False)
            self.button3.set_sensitive(False)
            self.button4.set_sensitive(False)
            self.checkbutton5.set_sensitive(False)
            self.checkbutton5.set_active(False)
            self.checkbutton6.set_sensitive(False)
            self.checkbutton6.set_active(False)
            self.radiobutton.set_sensitive(False)
            self.radiobutton2.set_sensitive(False)
            self.radiobutton3.set_sensitive(False)
            self.radiobutton4.set_sensitive(False)
            self.entry.set_sensitive(False)
            self.checkbutton8.set_active(False)
            self.checkbutton8.set_sensitive(False)
            self.checkbutton9.set_active(False)
            self.checkbutton9.set_sensitive(False)
            self.checkbutton10.set_active(False)
            self.checkbutton10.set_sensitive(False)
            self.checkbutton11.set_active(False)
            self.checkbutton11.set_sensitive(False)


    def on_dl_if_live_button_toggled(self, checkbutton):

        """Called from callback in self.setup_tabs().

        Enables/disables only downloading broadcasting livestreams.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active():

            self.edit_dict['dl_if_stream_flag'] = True
            self.checkbutton8.set_active(False)

        else:

            self.edit_dict['dl_if_stream_flag'] = False


    def on_dl_if_old_live_button_toggled(self, checkbutton):

        """Called from callback in self.setup_tabs().

        Enables/disables only downloading finished livestreams.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active():

            self.edit_dict['dl_if_old_stream_flag'] = True
            self.checkbutton9.set_active(False)

        else:

            self.edit_dict['dl_if_old_stream_flag'] = False


    def on_dl_if_subs_button_toggled(self, checkbutton):

        """Called from callback in self.setup_tabs().

        Enables/disables downloading only videos with subtitles.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active():

            self.edit_dict['dl_if_subs_flag'] = True

            self.checkbutton4.set_sensitive(True)
            self.button3.set_sensitive(True)
            self.button4.set_sensitive(True)

        else:

            self.edit_dict['dl_if_subs_flag'] = False

            self.checkbutton4.set_sensitive(False)
            self.checkbutton4.set_active(False)
            self.button3.set_sensitive(False)
            self.button4.set_sensitive(False)


    def on_dl_precede_button_toggled(self, checkbutton):

        """Called from callback in self.setup_tabs().

        Enables/disables checking videos, before downloading them independently
        of their channels/playlists.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active():

            self.edit_dict['dl_precede_flag'] = True

            self.checkbutton3.set_sensitive(True)
            if not self.retrieve_val('dl_if_subs_flag'):
                self.checkbutton4.set_active(False)
                self.checkbutton4.set_sensitive(False)
                self.button3.set_sensitive(False)
                self.button4.set_sensitive(False)
            else:
                self.checkbutton4.set_sensitive(True)
                self.button3.set_sensitive(True)
                self.button4.set_sensitive(True)

        else:

            self.edit_dict['dl_precede_flag'] = False

            self.checkbutton3.set_sensitive(False)
            self.checkbutton3.set_active(False)
            self.checkbutton4.set_sensitive(False)
            self.checkbutton4.set_active(False)
            self.button3.set_sensitive(False)
            self.button4.set_sensitive(False)


    def on_ignore_live_button_toggled(self, checkbutton):

        """Called from callback in self.setup_tabs().

        Enables/disables not downloading broadcasting livestreams.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active():

            self.edit_dict['ignore_stream_flag'] = True
            self.checkbutton10.set_active(False)

        else:

            self.edit_dict['ignore_stream_flag'] = False


    def on_ignore_old_live_button_toggled(self, checkbutton):

        """Called from callback in self.setup_tabs().

        Enables/disables not downloading finished livestreams.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active():

            self.edit_dict['ignore_old_stream_flag'] = True
            self.checkbutton11.set_active(False)

        else:

            self.edit_dict['ignore_old_stream_flag'] = False


    def on_remove_language_clicked(self, button):

        """Called by callback in self.setup_tabs().

        Args:

            button (Gtk.Button): The widget clicked

        """

        selection = self.treeview2.get_selection()
        (model, tree_iter) = selection.get_selected()
        if tree_iter is None:

            # Nothing selected
            return

        name = model[tree_iter][0]
        # From a string in the form 'English [en]', remove the language name to
        #   get a value in formats.LANGUAGE_CODE_DICT, e.g. 'en'
        match = re.search(r'^.*\s\[(.*)\]', name)
        if match:

            lang_code = match.groups()[0]
            old_list = self.retrieve_val('dl_if_subs_list')
            new_list = []
            for other_code in old_list:
                if other_code != lang_code:
                    new_list.append(other_code)

            self.edit_dict['dl_if_subs_list'] = new_list

            # Update the treeview
            self.setup_subtitles_tab_redraw_list()


    def on_reset_settings_clicked(self, button):

        """Called by callback in self.setup_tabs().

        Args:

            button (Gtk.Button): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' Custom downloads > Name'
        )

        self.app_obj.dialogue_manager_obj.show_msg_dialogue(
            _(
            'This procedure cannot be reversed. Are you sure you want to' \
            + ' continue?',
            ),
            'question',
            'yes-no',
            self,           # Parent window is this window
            {
                'yes': 'reset_custom_dl_manager',
                # (Reset this edit window, if the user clicks 'yes')
                'data': [self],
            },
        )


    def on_select_all_button_clicked(self, button):

        """Called by callback in self.setup_tabs().

        Args:

            button (Gtk.Button): The widget clicked

        """

        slice_dict = self.retrieve_val('slice_dict')
        for key in slice_dict:
            slice_dict[key] = True

        self.edit_dict['slice_dict'] = slice_dict

        # Update the treeview
        self.setup_slices_tab_update_treeview()


    def on_slice_button_toggled(self, checkbutton):

        """Called from callback in self.setup_tabs().

        Enables/disables removing slices from a video.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active():

            self.edit_dict['slice_flag'] = True

            self.button5.set_sensitive(True)
            self.button6.set_sensitive(True)

        else:

            self.edit_dict['split_flag'] = False

            self.button5.set_sensitive(False)
            self.button6.set_sensitive(False)


    def on_split_button_toggled(self, checkbutton):

        """Called from callback in self.setup_tabs().

        Enables/disables splitting a video into video clips.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active():

            self.edit_dict['split_flag'] = True

            self.checkbutton6.set_sensitive(False)
            self.checkbutton6.set_active(False)

        else:

            self.edit_dict['split_flag'] = False

            self.checkbutton6.set_sensitive(True)


    def on_treeview_button_toggled(self, renderer_toggle, tree_path):

        """Called from callback in self.setup_slices_tab().

        Enables/disables a category of video slice.

        Args:

            renderer_toggle (Gtk.CellRendererToggle): The widget clicked

            tree_path (Gtk.TreePath): Path to the clicked row

        """

        # (This condition makes the treeview insensitive, when other widgets
        #   in the same tab are insensitive)
#        if self.retrieve_val('slice_flag'):
        if self.checkbutton6.get_active():

            self.liststore3[tree_path][0] = not self.liststore3[tree_path][0]

            slice_dict = self.retrieve_val('slice_dict')
            slice_dict[self.liststore3[tree_path][1]] \
            = self.liststore3[tree_path][0]

            self.edit_dict['slice_dict'] = slice_dict


    def on_unselect_all_button_clicked(self, button):

        """Called by callback in self.setup_tabs().

        Args:

            button (Gtk.Button): The widget clicked

        """

        slice_dict = self.retrieve_val('slice_dict')
        for key in slice_dict:
            slice_dict[key] = False

        self.edit_dict['slice_dict'] = slice_dict

        # Update the treeview
        self.setup_slices_tab_update_treeview()


class OptionsEditWin(GenericEditWin):

    """Python class for an 'edit window' to modify values in an
    options.OptionsManager object.

    Args:

        app_obj (mainapp.TartubeApp): The main application object

        edit_obj (options.OptionsManager): The object whose attributes will be
            edited in this window

        init_mode (str or None): If specified, a tab is automatically selected;
            one of the values specified in the comments to self.select_tab()

    """


    # Standard class methods


    def __init__(self, app_obj, edit_obj, init_mode=None):

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options window starts here.' \
            + ' In the menu, click Edit > General download options...'
        )

        Gtk.Window.__init__(self, title=_('Download options'))

        if self.is_duplicate(app_obj, edit_obj, init_mode):
            return

        # IV list - class objects
        # -----------------------
        # The mainapp.TartubeApp object
        self.app_obj = app_obj
        # The options.OptionManager object being edited
        self.edit_obj = edit_obj


        # IV list - Gtk widgets
        # ---------------------
        self.grid = None                        # Gtk.Grid
        self.notebook = None                    # Gtk.Notebook
        self.reset_button = None                # Gtk.Button
        self.apply_button = None                # Gtk.Button
        self.ok_button = None                   # Gtk.Button
        self.cancel_button = None               # Gtk.Button
        # The 'embed_subs' option appears in two different places
        self.embed_checkbutton = None           # Gtk.CheckButton
        self.embed_checkbutton2 = None          # Gtk.CheckButton
        # The Gtk.ListStore containing the user's preferred video/audio formats
        #   (which must be redrawn when self.apply_changes() is called)
        self.formats_liststore = None           # Gtk.ListStore
        # The Gtk.CheckButton specifying whether obsolete formats should be
        #   hidden in that list
        self.formats_checkbutton = None         # Gtk.CheckButton

        # IV list - other
        # ---------------
        # Size (in pixels) of gaps between edit window widgets
        self.spacing_size = self.app_obj.default_spacing_size
        # Flag set to True if all four buttons ('Reset', 'Apply', 'Cancel' and
        #   'OK') are required, or False if just the 'OK' button is required
        self.multi_button_flag = True

        # When the user changes a value, it is not applied to self.edit_obj
        #   immediately; instead, it is stored temporarily in this dictionary
        # If the user clicks the 'OK' or 'Apply' buttons at the bottom of the
        #   window, the changes are applied to self.edit_obj
        # If the user clicks the 'Reset' or 'Cancel' buttons, the dictionary
        #   is emptied and the changes are lost
        # In this edit window, the key-value pairs directly correspond to those
        #   in options.OptionsManager.options_dict, rather than corresponding
        #   directly to attributes in the options.OptionsManager object
        # Because of that, we use our own .apply_changes() and .retrieve_val()
        #   functions, rather than relying on the generic functions
        # Key-value pairs are added to this dictionary whenever the user
        #   makes a change (so if no changes are made when the window is
        #   closed, the dictionary will still be empty)
        self.edit_dict = {}

        # IVs used to keep track of widget changes in the 'Files' tab
        # Flag set to to False when that tab's output template widgets are
        #   desensitised, True when sensitised
        self.template_flag = False
        # A list of Gtk widgets to (de)sensitise in when the flag changes
        self.template_widget_list = []

        # Code
        # ----

        # Set up the edit window
        self.setup()

        # Automatically open a particular tab, if required
        self.select_tab(init_mode)


    # Public class methods


    def is_duplicate(self, app_obj, edit_obj, init_mode):

        """Called by self.__init__.

        Don't open this edit window, if another with the same .edit_obj is
        already open.

        If 'init_mode' is specified, switch the visible tab in the existing
        preference window (if any).

        Args:

            app_obj (mainapp.TartubeApp): The main application object

            edit_obj (options.OptionsManager): The object whose attributes will
                be edited in this window

            init_mode (str or None): One of the values specified in the
                comments to self.select_tab()

        Return values:

            True if a duplicate is found, False if not

        """

        for config_win_obj in app_obj.main_win_obj.config_win_list:

            if isinstance(config_win_obj, GenericEditWin) \
            and config_win_obj.edit_obj == edit_obj:

                # Duplicate found. Make it prominent...
                config_win_obj.present()
                # ...and switch to a particular tab, if required
                config_win_obj.select_tab(init_mode)

                return True

        # Not a duplicate
        return False


#   def setup():                # Inherited from GenericConfigWin


#   def setup_grid():           # Inherited from GenericConfigWin


#   def setup_notebook():       # Inherited from GenericConfigWin


#   def add_notebook_tab():     # Inherited from GenericConfigWin


#   def setup_button_strip():   # Inherited from GenericEditWin


#   def setup_gap():            # Inherited from GenericConfigWin


    def select_tab(self, init_mode=None):

        """Called by self.__init__().

        On startup, automatically open a particular tab, if required.

        Args:

            init_mode (str or None): If specified:
                - 'formats' - media formats
                - 'subs' - subtitles
                - (any other value is ignored)

        """

        if init_mode is not None:

            if init_mode == 'formats':
                self.select_formats_tab()
            elif init_mode == 'subs':
                self.select_subs_tab()


    def select_formats_tab(self):

        """Can be called by anything.

        Makes the visible tab the one on which the media format options are
        displayed.
        """

        # Opens tab: self.setup_formats_preferred_tab()
        self.notebook.set_current_page(3)


    def select_subs_tab(self):

        """Can be called by anything.

        Makes the visible tab the one on which the subtitle options are
        displayed.
        """

        # Opens tab: self.setup_subtitles_options_tab()
        self.notebook.set_current_page(5)


    # (Non-widget functions)


    def apply_changes(self):

        """Called by self.on_button_ok_clicked() and
        self.on_button_apply_clicked().

        Any changes the user has made are temporarily stored in self.edit_dict.
        Apply to those changes to the object being edited.

        In this edit window we apply changes to self.edit_obj.options_dict
        (rather than to self.edit_obj's attributes directly, as in the generic
        function.)
        """

        # Apply any changes the user has made
        for key in self.edit_dict.keys():

            if key in self.edit_obj.options_dict:
                self.edit_obj.options_dict[key] = self.edit_dict[key]

        # The name can also be updated, if it has been changed (but it the
        #   entry was blank, keep the old name)
        if 'name' in self.edit_dict \
        and self.edit_dict['name'] != '':
            self.edit_obj.name = self.edit_dict['name']
        # The description can also be updated, even if it is blank
        if 'descrip' in self.edit_dict:
            self.edit_obj.descrip = self.edit_dict['descrip']

        # The changes can now be cleared
        self.edit_dict = {}

        # The user can specify multiple video/audio formats. If a mixture of
        #   both is specified, then video formats must be listed before audio
        #   formats (or youtube-dl won't download them all)
        # Tell the options.OptionManager object to rearrange them, if
        #   necessary
        self.edit_obj.rearrange_formats()
        # ...then redraw the textview in the Formats tab
        self.formats_tab_redraw_list()

        # Update the associated mainwin.DropZoneBox in the main window's
        #   Drag and Drop tab
        if self.edit_obj.uid in self.app_obj.classic_dropzone_list:
            dropzone_obj \
            = self.app_obj.main_win_obj.drag_drop_dict[self.edit_obj.uid]
            dropzone_obj.update_widgets()


    def retrieve_val(self, name):

        """Can be called by anything.

        Any changes the user has made are temporarily stored in self.edit_dict.

        In the generic function, each key corresponds to an attribute in the
        object being edited, self.edit_obj. In this window, it corresponds to a
        key in self.edit_obj.options_dict.

        If 'name' exists as a key in that dictionary, retrieve the
        corresponding value and return it. Otherwise, the user hasn't yet
        modified the value, so retrieve directly from the attribute in the
        object being edited.

        Args:

            name (str): The name of the attribute in the object being edited

        Return values:

            The original or modified value of that attribute

        """

        if name in self.edit_dict:

            return self.edit_dict[name]

        elif name == 'uid' \
        or name == 'name' \
        or name == 'descrip' \
        or name == 'dbid_list':

            return getattr(self.edit_obj, name)

        elif name in self.edit_obj.options_dict:

            value = self.edit_obj.options_dict[name]
            if type(value) is list or type(value) is dict:
                return value.copy()
            else:
                return value

        else:

            return self.app_obj.system_error(
                404,
                'Unrecognised property name \'' + name + '\'',
            )


    def add_tooltip(self, text, widget=None, widget2=None, widget3=None, \
    widget4=None, widget5=None):

        """Called by various tabs, to show the equivalent youtube-dl switches
        for each Tartube download option.

        Adds the tooltip 'text' to any specified widgets (maximum four).

        Args:

            text (str): The text to use in the tooltip

            widget, widget2, widget3, widget4, widget5 (widget or None): Any
                Gtk widget for which we can call .set_tooltip_text()

        """

        if widget is not None:
            widget.set_tooltip_text(text)
        if widget2 is not None:
            widget2.set_tooltip_text(text)
        if widget3 is not None:
            widget3.set_tooltip_text(text)
        if widget4 is not None:
            widget4.set_tooltip_text(text)
        if widget5 is not None:
            widget5.set_tooltip_text(text)


    # (Setup tabs)


    def setup_tabs(self):

        """Called by self.setup(), .on_button_apply_clicked() and
        .on_button_reset_clicked().

        Sets up the tabs for this edit window.
        """

        self.setup_name_tab()
        self.setup_downloads_tab()
        self.setup_files_tab()
        self.setup_formats_tab()
        if not self.app_obj.simple_options_flag:
            self.setup_post_process_tab()
        else:
            self.setup_convert_tab()
        self.setup_subtitles_tab()
        if not self.app_obj.simple_options_flag:
            self.setup_advanced_tab()


    def setup_name_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Name' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Name'
        )

        tab, grid = self.add_notebook_tab(_('_Name'))
        grid_width = 3

        label = self.add_label(grid,
            _('Name for these download options'),
            0, 0, 1, 1,
        )

        entry = self.add_entry(grid,
            'name',
            1, 0, 1, 1,
        )
        entry.set_hexpand(True)

        entry2 = self.add_entry(grid,
            None,
            2, 0, 1, 1,
        )
        entry2.set_text('#' + str(self.edit_obj.uid))
        entry2.set_hexpand(False)
        entry2.set_max_length(8)

        label2 = self.add_label(grid,
            _('Description'),
            0, 1, 1, 1,
        )

        entry3 = self.add_entry(grid,
            'descrip',
            1, 1, 2, 1,
        )
        entry3.set_hexpand(True)

        label3 = self.add_label(grid,
            _('Download options applied to'),
            0, 2, 1, 1,
        )

        entry4 = self.add_entry(grid,
            None,
            1, 2, 2, 1,
        )
        entry4.set_editable(False)

        if self.edit_obj == self.app_obj.general_options_obj:
            entry4.set_text(_('All channels, playlists and folders'))
        elif self.edit_obj == self.app_obj.classic_options_obj:
            entry4.set_text(_('Downloads in the Classic Mode tab'))
        elif self.edit_obj.dbid_list:
            entry4.set_text(self.get_options_applied_text(self.edit_obj))
        else:
            entry4.set_text(_('These options are not applied to anything'))

        if self.app_obj.simple_options_flag:

            label4 = self.add_label(grid,
                _(
                'Additional download options, e.g. --write-subs (do not use' \
                + ' -o or --output)',
                ),
                0, 3, grid_width, 1,
            )

        else:

            label4 = self.add_label(grid,
                _('Additional download options'),
                0, 3, 1, 1,
            )

            if os.name == 'nt':

                checkbutton = self.add_checkbutton(grid,
                    _(
                    'Use ONLY these options (Tartube adds the output folder)',
                    ),
                    None,
                    1, 3, 2, 1,
                )
                # (Signal connect appears below)

            else:

                checkbutton = self.add_checkbutton(grid,
                    _(
                    'Use ONLY these options (Tartube adds the output' \
                    + ' directory)',
                    ),
                    None,
                    1, 3, 2, 1,
                )
                # (Signal connect appears below)

            checkbutton.set_active(self.retrieve_val('direct_cmd_flag'))

            checkbutton2 = self.add_checkbutton(grid,
                _('If URLs are specified below, use only those URLs'),
                'direct_url_flag',
                1, 4, 2, 1,
            )

            # (Signal connects from above)
            checkbutton.connect(
                'toggled',
                self.on_direct_cmd_toggled,
                checkbutton2,
            )

        textview, textbuffer = self.add_textview(grid,
            'extra_cmd_string',
            0, 5, grid_width, 1,
        )
        self.add_tooltip(
            _(
                'Arguments containing special shell characters' \
                + '(+ - & < > = ? * ! : ; \\ / and brackets) should be' \
                + ' enclosed within quotes, e.g. -f "(137/136)"',
            ),
            label4,
            textview,
        )

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 6, grid_width, 1)

        if self.app_obj.simple_options_flag:
            frame = self.add_pixbuf(grid2,
                'hand_right_large',
                0, 0, 1, 1,
            )
            frame.set_hexpand(False)
            frame.set_size_request(75, -1)

        else:
            frame = self.add_pixbuf(grid2,
                'hand_left_large',
                0, 0, 1, 1,
            )
            frame.set_hexpand(False)
            frame.set_size_request(75, -1)

        button = Gtk.Button()
        grid2.attach(button, 1, 0, 1, 1)
        if not self.app_obj.simple_options_flag:
            button.set_label(_('Hide advanced download options'))
        else:
            button.set_label(_('Show advanced download options'))
        button.set_hexpand(True)
        button.connect('clicked', self.on_simple_options_clicked)

        frame2 = self.add_pixbuf(grid2,
            'copy_large',
            0, 1, 1, 1,
        )
        frame2.set_hexpand(False)

        button2 = Gtk.Button(
            _('Import general download options into this window'),
        )
        grid2.attach(button2, 1, 1, 1, 1)
        button2.set_hexpand(True)
        button2.connect('clicked', self.on_clone_options_clicked)
        if self.edit_obj == self.app_obj.general_options_obj:
            # No point cloning the General Options Manager onto itself
            button2.set_sensitive(False)

        frame3 = self.add_pixbuf(grid2,
            'warning_large',
            0, 2, 1, 1,
        )
        frame3.set_hexpand(False)

        button3 = Gtk.Button(
            _('Completely reset all download options to their default values'),
        )
        grid2.attach(button3, 1, 2, 1, 1)
        button3.set_hexpand(True)
        button3.connect('clicked', self.on_reset_options_clicked)


    def setup_downloads_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Downloads' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Downloads'
        )

        # Simple options only
        if self.app_obj.simple_options_flag:

            tab, grid = self.add_notebook_tab(_('_Downloads'))

            row_count = self.downloads_age_widgets(grid, 0)
            row_count = self.downloads_size_limit_widgets(grid, row_count)
            row_count = self.downloads_date_widgets(grid, row_count)
            row_count = self.downloads_views_widgets(grid, row_count)

        # All options
        else:

            # Add this tab...
            tab, grid = self.add_notebook_tab(_('_Downloads'), 0)

            # ...and an inner notebook...
            inner_notebook = self.add_inner_notebook(grid)

            # ...with its own tabs
            self.setup_downloads_general_tab(inner_notebook)
            self.setup_downloads_videos_tab(inner_notebook)
            self.setup_downloads_live_tab(inner_notebook)
            self.setup_downloads_playlists_tab(inner_notebook)
            self.setup_downloads_limits_tab(inner_notebook)
            self.setup_downloads_merge_tab(inner_notebook)
            self.setup_downloads_extractor_tab(inner_notebook)
            self.setup_downloads_filtering_tab(inner_notebook)
            self.setup_downloads_external_tab(inner_notebook)


    def setup_downloads_general_tab(self, inner_notebook):

        """Called by self.setup_downloads_tab().

        Sets up the 'General' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Downloads > General' \
            + ' (to make hidden tabs visible, click the \'Show advanced' \
            + ' download options\' button in the Name tab)'
        )

        tab, grid = self.add_inner_notebook_tab('_General', inner_notebook)

        # General download options
        self.add_label(grid,
            '<u>' + _('General download options') + '</u>',
            0, 0, 1, 1,
        )

        row_count = 1
        row_count = self.downloads_general_widgets(grid, row_count)

        # General download options (yt-dlp only)
        self.add_label(grid,
            '<u>' + _('General download options') + '</u>' + self.ytdlp_only(),
            0, (row_count + 1), 2, 1,
        )

        label = self.add_label(grid,
            _(
                'Number of fragments of a DASH/HLS video to download' \
                + ' concurrently',
            ),
            0, (row_count + 2), 1, 1
        )

        spinbutton = self.add_spinbutton(grid,
            1, None, 1,
            'concurrent_fragments',
            1, (row_count + 2), 1, 1
        )
        self.add_tooltip('-N, --concurrent-fragments N', label, spinbutton)

        label2 = self.add_label(grid,
            _(
                'Minimum download rate (bytes/sec) below which throttling' \
                + ' is assumed',
            ),
            0, (row_count + 3), 1, 1
        )

        spinbutton2 = self.add_spinbutton(grid,
            0, None, 100,
            'throttled_rate',
            1, (row_count + 3), 1, 1
        )
        self.add_tooltip('--throttled-rate RATE', label2, spinbutton2)


    def setup_downloads_videos_tab(self, inner_notebook):

        """Called by self.setup_downloads_tab().

        Sets up the 'Videos' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Downloads > Videos'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Videos'),
            inner_notebook,
        )
        grid_width = 2

        # Video selection options (yt-dlp only)
        self.add_label(grid,
            '<u>' + _('Video selection options') + '</u>' + self.ytdlp_only(),
            0, 0, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Stop download process when encountering a file in the archive'),
            'break_on_existing',
            0, 1, grid_width, 1,
        )
        self.add_tooltip('--break-on-existing', checkbutton)

        checkbutton2 = self.add_checkbutton(grid,
            _(
                'Stop download process when encountering a file that has' \
                + ' been filtered out',
            ),
            'break_on_reject',
            0, 2, grid_width, 1,
        )
        self.add_tooltip('--break-on-reject', checkbutton2)

        label = self.add_label(grid,
            _('Number of failures allowed before rest of playlist is skipped'),
            0, 3, 1, 1
        )

        spinbutton = self.add_spinbutton(grid,
            0, None, 1,
            'skip_playlist_after_errors',
            1, 3, 1, 1
        )
        self.add_tooltip('--skip-playlist-after-errors N', label, spinbutton)

        # Verbosity and simulation options (yt-dlp only)
        self.add_label(grid,
            '<u>' + _('Verbosity and simulation options') + '</u>' \
            + self.ytdlp_only(),
            0, 4, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _(
                'Ignore \'No video formats\' error (useful for extracting' \
                + ' metadata from unavailable videos)',
            ),
            'ignore_no_formats_error',
            0, 5, grid_width, 1,
        )
        self.add_tooltip('--ignore-no-formats-error', checkbutton)

        checkbutton2 = self.add_checkbutton(grid,
            _(
                'Force download archive entries to be written as long as' \
                + ' no errors occur',
            ),
            'force_write_archive',
            0, 6, grid_width, 1,
        )
        self.add_tooltip('--force-write-archive', checkbutton2)


    def setup_downloads_live_tab(self, inner_notebook):

        """Called by self.setup_downloads_tab().

        Sets up the 'Live' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Downloads > Live'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Live'),
            inner_notebook,
        )
        grid_width = 2

        # Livestream options (yt-dlp only)
        self.add_label(grid,
            '<u>' + _('Livestream options') + '</u>' + self.ytdlp_only(),
            0, 0, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Download livestreams from start (experimental, YouTube only)'),
            'live_from_start',
            0, 1, grid_width, 1,
        )
        self.add_tooltip('--live-from-start', checkbutton)

        label = self.add_label(grid,
            _('Minimum seconds to wait for scheduled streams'),
            0, 2, 1, 1
        )

        spinbutton = self.add_spinbutton(grid,
            0, None, 1,
            'wait_for_video_min',
            1, 2, 1, 1
        )
        self.add_tooltip('--wait-for-video MIN', label, spinbutton)


    def setup_downloads_playlists_tab(self, inner_notebook):

        """Called by self.setup_downloads_tab().

        Sets up the 'Playlists' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Downloads > Playlists'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Playlists'),
            inner_notebook,
        )

        row_count = self.downloads_playlist_widgets(grid, 0)


    def setup_downloads_limits_tab(self, inner_notebook):

        """Called by self.setup_downloads_tab().

        Sets up the 'Limits' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Downloads > Limits'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('L_imits'),
            inner_notebook,
        )

        row_count = self.downloads_age_widgets(grid, 0)
        row_count = self.downloads_size_limit_widgets(grid, row_count)
        row_count = self.downloads_date_widgets(grid, row_count)
        row_count = self.downloads_views_widgets(grid, row_count)


    def setup_downloads_merge_tab(self, inner_notebook):

        """Called by self.setup_downloads_tab().

        Sets up the 'Merge' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Downloads > Merge'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Merge'),
            inner_notebook,
        )

        # Video/audio merge options (yt-dlp only)
        self.add_label(grid,
            '<u>' + _('Video/audio merge options') + '</u>' \
            + self.ytdlp_only(),
            0, 0, 1, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Allow multiple video streams to be merged into a single file'),
            'video_multistreams',
            0, 1, 1, 1,
        )
        self.add_tooltip('--video-multistreams', checkbutton)

        checkbutton2 = self.add_checkbutton(grid,
            _('Allow multiple audio streams to be merged into a single file'),
            'audio_multistreams',
            0, 2, 1, 1,
        )
        self.add_tooltip('--audio-multistreams', checkbutton2)


    def setup_downloads_extractor_tab(self, inner_notebook):

        """Called by self.setup_downloads_tab().

        Sets up the 'Extractor' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Downloads > Extractor'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Extractor'),
            inner_notebook,
        )
        grid_width = 2

        # Extractor options (yt-dlp only)
        self.add_label(grid,
            '<u>' + _('Extractor options') + '</u>' + self.ytdlp_only(),
            0, 0, grid_width, 1,
        )

        label = self.add_label(grid,
            'Number of retries for known extractor errors',
            0, 1, 1, 1,
        )

        combo_list = [
            '', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'infinite',
        ]

        combo = self.add_combo(grid,
            combo_list,
            'extractor_retries',
            1, 1, 1, 1
        )
        combo.set_hexpand(True)
        self.add_tooltip('--extractor-retries RETRIES', label, combo)

        checkbutton = self.add_checkbutton(grid,
            _('Do not process dynamic DASH manifests'),
            'no_allow_dynamic_mpd',
            0, 2, grid_width, 1,
        )
        self.add_tooltip('--no-allow-dynamic-mpd', checkbutton)

        checkbutton2 = self.add_checkbutton(grid,
            _(
                'Split HLS playlists to different formats at discontinuities' \
                + ' such as ad breaks',
            ),
            'hls_split_discontinuity',
            0, 3, grid_width, 1,
        )
        self.add_tooltip('--hls-split-discontinuity', checkbutton2)

        # Extractor arguments
        self.add_label(grid,
            '<u>' + _('Extractor arguments') + '</u>',
            0, 4, grid_width, 1,
        )

        label2 = self.add_label(grid,
            '<i>' + _('One argument per line, e.g.') \
            + '</i> youtube:skip=dash,hls;player_client=android',
            0, 5, grid_width, 1,
        )

        textview, textbuffer = self.add_textview(grid,
            'extractor_args_list',
            0, 6, grid_width, 1,
        )
        self.add_tooltip('--extractor-args KEY:ARGS', label2, textview)


    def setup_downloads_filtering_tab(self, inner_notebook):

        """Called by self.setup_downloads_tab().

        Sets up the 'Filtering' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Downloads > Filtering'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Filtering'),
            inner_notebook,
        )

        row_count = self.downloads_filtering_widgets(grid, 0)


    def setup_downloads_external_tab(self, inner_notebook):

        """Called by self.setup_downloads_tab().

        Sets up the 'External' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Downloads > External'
        )

        tab, grid = self.add_inner_notebook_tab(_('E_xternal'), inner_notebook)

        row_count = self.downloads_external_widgets(grid, 0)


    def setup_files_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Files' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Files'
        )

        # Add this tab...
        tab, grid = self.add_notebook_tab(_('_Files'), 0)

        # ...and an inner notebook...
        inner_notebook = self.add_inner_notebook(grid)

        # ...with its own tabs
        self.setup_files_names_tab(inner_notebook)
        if not self.app_obj.simple_options_flag:
            self.setup_files_override_tab(inner_notebook)
            self.setup_files_paths_tab(inner_notebook)
        self.setup_files_filesystem_tab(inner_notebook)
        self.setup_files_cookies_tab(inner_notebook)
        if not self.app_obj.simple_options_flag:
            self.setup_files_shortcuts_tab(inner_notebook)
        self.setup_files_write_move_tab(inner_notebook)
        self.setup_files_keep_tab(inner_notebook)


    def setup_files_names_tab(self, inner_notebook):

        """Called by self.setup_files_tab().

        Sets up the 'File names' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Files > File names'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('File _names'),
            inner_notebook,
        )
        grid_width = 2

        # File name options
        self.add_label(grid,
            '<u>' + _('File name options') + '</u>',
            0, 0, 2, 1,
        )

        label = self.add_label(grid,
            _('Format for video file names'),
            0, 1, 1, 1,
        )
        label.set_hexpand(False)

        store = Gtk.ListStore(int, str)
        num_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
        for num in num_list:
            store.append( [num, formats.FILE_OUTPUT_NAME_DICT[num]] )

        current_format = self.edit_obj.options_dict['output_format']
        current_template = formats.FILE_OUTPUT_CONVERT_DICT[current_format]
        if current_template is None:
            current_template = self.edit_obj.options_dict['output_template']

        combo = Gtk.ComboBox.new_with_model(store)
        grid.attach(combo, 0, 2, 1, 1)
        renderer_text = Gtk.CellRendererText()
        combo.pack_start(renderer_text, True)
        combo.add_attribute(renderer_text, "text", 1)
        combo.set_entry_text_column(1)
        combo.set_active(num_list.index(current_format))
        combo.set_hexpand(False)
        # (Signal connect appears below)
        self.add_tooltip('-o, --output TEMPLATE', combo)

        label2 = self.add_label(grid,
            _('File output template'),
            1, 1, 1, 1,
        )
        label2.set_hexpand(True)

        entry = self.add_entry(grid,
            None,
            1, 2, 1, 1,
        )
        entry.set_text(current_template)
        entry.set_hexpand(True)
        # (Signal connect appears below)

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 3, grid_width, 1)

        # Add widgets to a list, so we can sensitise them when a custom
        #   template is selected, and desensitise them the rest of the time
        self.template_widget_list = [entry]

        self.add_label(grid2,
            _('Add to template:'),
            0, 0, 1, 1,
        )

        master_list = [
            _('Video properties'),
            [
                'id',               _('Video ID'),
                'title',            _('Video title'),
                'display_id',       _('Alternative video ID'),
                'alt_title',        _('Secondary video title'),
                'url',              _('Video URL'),
                'ext',              _('Video filename extension'),
                'license',          _('Video licence'),
                'age_limit',        _('Age restriction (years)'),
                'is_live',          _('Is a livestream'),
                'video_autonumber', _('Autonumber videos'),
                'playlist_autonumber',
                                    _('Autonumber videos (playlists)'),
            ],
            _('Creator/uploader'),
            [
                'uploader',         _('Full name of video uploader'),
                'uploader_id',      _('Uploader ID'),
                'creator',          _('Nickname/ID of video uploader'),
                'channel',          _('Channel name'),
                'channel_id',       _('Channel ID'),
                'playlist',         _('Playlist name'),
                'playlist_id',      _('Playlist ID'),
                'playlist_index',   _('Video index in playlist'),
            ],
            _('Date/time/location'),
            [
                'release_date',     _('Release date (YYYYMMDD)'),
                'release_date_custom',
                                    _('Release date (custom format)'),
                'timestamp',        _('Release time (UNIX timestamp)'),
                'timestamp_custom', _('Release time (custom format)'),
                'upload_date',      _('Upload date (YYYYMMDD)'),
                'upload_date_custom',
                                    _('Upload date (custom format)'),
                'duration',         _('Video length (seconds)'),
                'duration_custom',  _('Video length (custom format)'),
                'location',         _('Filming location'),
            ],
            _('Time format'),
            [],
            _('Video format'),
            [
                'format',           _('Video format'),
                'format_id',        _('Video format code'),
                'width',            _('Video width'),
                'height',           _('Video height'),
                'resolution',       _('Video resolution'),
                'fps',              _('Video frame rate'),
                'tbr',              _('Average video/audio bitrate (KiB/s)'),
                'vbr',              _('Average video bitrate (KiB/s)'),
                'abr',              _('Average audio bitrate (KiB/s)'),
            ],
            _('Ratings/comments'),
            [
                'view_count',       _('Number of views'),
                'like_count',       _('Number of positive ratings'),
                'dislike_count',    _('Number of negative ratings'),
                'average_rating',   _('Average rating'),
                'repost_count',     _('Number of reposts'),
                'comment_count',    _('Number of comments'),
            ],
        ]

        # (Create the entry and its button first, so they are available to the
        #   callbacks)
        entry2 = Gtk.Entry()
        entry2.set_hexpand(True)

        button = Gtk.Button(_('Reset'))
        button.set_sensitive(False)
        # (Signal connect appears below)

        row_num = -1
        while master_list:

            row_num += 1

            this_title = master_list.pop(0)
            this_store_list = master_list.pop(0)

            self.add_label(grid2,
                this_title,
                1, row_num, 1, 1,
            )

            if this_title == _('Time format'):

                grid2.attach(entry2, 2, row_num, 1, 1)
                grid2.attach(button, 3, row_num, 1, 1)

                self.template_widget_list.append(entry2)
                self.template_widget_list.append(button)

            else:

                this_store = Gtk.ListStore(str)
                # (The dictionary is used by
                #   self.on_file_tab_add_button_clicked() to translate the
                #   visible string into the string youtube-dl uses)
                this_store_dict = {}
                while this_store_list:
                    item = this_store_list.pop(0)
                    mod_item = this_store_list.pop(0)

                    this_store_dict[mod_item] = item
                    this_store.append( [mod_item] )

                this_combo = Gtk.ComboBox.new_with_model(this_store)
                grid2.attach(this_combo, 2, row_num, 1, 1)
                this_renderer_text = Gtk.CellRendererText()
                this_combo.pack_start(this_renderer_text, True)
                this_combo.add_attribute(this_renderer_text, "text", 0)
                this_combo.set_entry_text_column(0)
                this_combo.set_active(0)

                this_button = Gtk.Button(_('Add'))
                grid2.attach(this_button, 3, row_num, 1, 1)
                this_button.connect(
                    'clicked',
                    self.on_file_tab_add_button_clicked,
                    entry,
                    entry2,
                    this_combo,
                    this_store_dict,
                )

                self.template_widget_list.append(this_combo)
                self.template_widget_list.append(this_button)

            if this_title == _('Date/time/location'):

                # (Signal connects from above)
                this_combo.connect(
                    'changed',
                    self.on_date_time_combo_changed,
                    entry2,
                    button,
                    this_store_dict,
                )

                button.connect(
                    'clicked',
                    self.on_file_tab_reset_button_clicked,
                    entry2,
                    this_combo,
                    this_store_dict,
                )

        # (Signal connects from above)
        combo.connect(
            'changed',
            self.on_file_tab_main_combo_changed,
            entry,
            entry2,
            button,
        )
        entry.connect('changed', self.on_file_tab_main_entry_changed)

        # (De)sensitise widgets in self.template_widget_list
        if current_format == 0:
            self.file_tab_sensitise_widgets(True)
        else:
            self.file_tab_sensitise_widgets(False)


    def setup_files_override_tab(self, inner_notebook):

        """Called by self.setup_files_tab().

        Sets up the 'Override' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Files > Override'
        )


        tab, grid = self.add_inner_notebook_tab(
            _('_Override'),
            inner_notebook,
        )
        grid_width = 4

        # List of output filename templates (yt-dlp only)
        self.add_label(grid,
            '<u>' + _('List of output filename templates') + '</u>' \
            + self.ytdlp_only(),
            0, 0, grid_width, 1,
        )

        self.add_label(grid,
            '<i>' + _(
                'Overrides the output template in the \'File names\' tab',
            ) + '</i>',
            0, 1, grid_width, 1,
        )

        # (GenericConfigWin.add_treeview() doesn't support multiple columns, so
        #   we'll do everything ourselves)
        frame = Gtk.Frame()
        grid.attach(frame, 0, 2, grid_width, 1)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)

        treeview = Gtk.TreeView()
        scrolled.add(treeview)
        treeview.set_headers_visible(True)

        for i, column_title in enumerate([ _('Type'), _('Template') ]):

            renderer_text = Gtk.CellRendererText()
            column_text = Gtk.TreeViewColumn(
                column_title,
                renderer_text,
                text=i,
            )
            treeview.append_column(column_text)
            column_text.set_resizable(True)

        liststore = Gtk.ListStore(str, str)
        treeview.set_model(liststore)

        # Initialise the list
        self.setup_files_override_tab_update_treeview(liststore)

        # Add editing buttons
        label = self.add_label(grid,
            _('Output type'),
            0, 3, 1, 1,
        )

        combo_list = formats.YTDLP_OUTPUT_TYPE_LIST.copy()
        combo = self.add_combo(grid,
            combo_list,
            None,
            1, 3, 1, 1,
        )
        combo.set_active(0)

        label2 = self.add_label(grid,
            _('Output template'),
            0, 4, 1, 1,
        )

        entry = self.add_entry(grid,
            None,
            1, 4, (grid_width - 1), 1,
        )

        button = Gtk.Button()
        grid.attach(button, 0, 5, 1, 1)
        button.set_label(_('Add'))
        button.connect(
            'clicked',
            self.on_ytdlp_output_add_button_clicked,
            liststore,
            combo,
            entry,
        )

        button2 = Gtk.Button()
        grid.attach(button2, 1, 5, 1, 1)
        button2.set_label(_('Delete'))
        button2.connect(
            'clicked',
            self.on_ytdlp_output_delete_button_clicked,
            treeview,
        )

        button3 = Gtk.Button()
        grid.attach(button3, 3, 5, 1, 1)
        button3.set_label(_('Refresh list'))
        button3.connect(
            'clicked',
            self.on_ytdlp_output_refresnh_button_clicked,
            treeview,
        )


    def setup_files_override_tab_update_treeview(self, liststore):

        """Can be called by anything.

        Fills or updates the treeview.

        Args:

            liststore (Gtk.ListStore): The treeview's model

        """

        liststore.clear()

        for item in self.retrieve_val('output_format_list'):

            # (Each 'item' is in the form TYPES:TEMPLATE)
            match = re.search(r'^([^\:]+)\:(.*)', item)
            if match:
                liststore.append([ match.groups()[0], match.groups()[1] ])


    def setup_files_paths_tab(self, inner_notebook):

        """Called by self.setup_files_tab().

        Sets up the 'Paths' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Files > Paths'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Paths'),
            inner_notebook,
        )
        grid_width = 5

        # List of output paths (yt-dlp only)
        self.add_label(grid,
            '<u>' + _('List of output paths') + '</u>' + self.ytdlp_only(),
            0, 0, grid_width, 1,
        )

        # (GenericConfigWin.add_treeview() doesn't support multiple columns, so
        #   we'll do everything ourselves)
        frame = Gtk.Frame()
        grid.attach(frame, 0, 1, grid_width, 1)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)

        treeview = Gtk.TreeView()
        scrolled.add(treeview)
        treeview.set_headers_visible(True)

        for i, column_title in enumerate([ _('Type'), _('Path') ]):

            renderer_text = Gtk.CellRendererText()
            column_text = Gtk.TreeViewColumn(
                column_title,
                renderer_text,
                text=i,
            )
            treeview.append_column(column_text)
            column_text.set_resizable(True)

        liststore = Gtk.ListStore(str, str)
        treeview.set_model(liststore)

        # Initialise the list
        self.setup_files_paths_tab_update_treeview(liststore)

        # Add editing buttons
        label = self.add_label(grid,
            _('Output type'),
            0, 2, 1, 1,
        )

        combo_list = formats.YTDLP_OUTPUT_TYPE_LIST.copy()
        combo_list.insert(0, 'home')
        combo_list.insert(1, 'temp')
        combo = self.add_combo(grid,
            combo_list,
            None,
            1, 2, 1, 1,
        )
        combo.set_active(0)

        label2 = self.add_label(grid,
            _('Output path'),
            0, 3, 1, 1,
        )

        entry = self.add_entry(grid,
            None,
            1, 3, (grid_width - 2), 1,
        )
        entry.set_editable(False)

        button = Gtk.Button()
        grid.attach(button, 4, 3, 1, 1)
        button.set_label(_('Set'))
        button.connect(
            'clicked',
            self.on_ytdlp_paths_set_button_clicked,
            entry,
        )

        button2 = Gtk.Button()
        grid.attach(button2, 0, 4, 1, 1)
        button2.set_label(_('Add'))
        button2.connect(
            'clicked',
            self.on_ytdlp_paths_add_button_clicked,
            liststore,
            combo,
            entry,
        )

        button3 = Gtk.Button()
        grid.attach(button3, 1, 4, 1, 1)
        button3.set_label(_('Delete'))
        button3.connect(
            'clicked',
            self.on_ytdlp_paths_delete_button_clicked,
            treeview,
        )

        button4 = Gtk.Button()
        grid.attach(button4, 3, 4, 2, 1)
        button4.set_label(_('Refresh list'))
        button4.connect(
            'clicked',
            self.on_ytdlp_paths_refresh_button_clicked,
            treeview,
        )


    def setup_files_paths_tab_update_treeview(self, liststore):

        """Can be called by anything.

        Fills or updates the treeview.

        Args:

            liststore (Gtk.ListStore): The treeview's model

        """

        liststore.clear()

        for item in self.retrieve_val('output_path_list'):

            # (Each 'item' is in the form TYPES:PATH)
            match = re.search(r'^([^\:]+)\:(.*)', item)
            if match:
                liststore.append([ match.groups()[0], match.groups()[1] ])


    def setup_files_filesystem_tab(self, inner_notebook):

        """Called by self.setup_files_tab().

        Sets up the 'Filesystem' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Files > Filesystem'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Filesystem'),
            inner_notebook,
        )
        grid_width = 2

        # Filesystem options
        self.add_label(grid,
            '<u>' + _('Filesystem options') + '</u>',
            0, 0, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Restrict filenames to ASCII characters'),
            'restrict_filenames',
            0, 1, grid_width, 1,
        )
        self.add_tooltip('--restrict-filenames', checkbutton)

        if not self.app_obj.simple_options_flag:

            checkbutton2 = self.add_checkbutton(grid,
                _('Don\'t use the server\'s file modification time'),
                'nomtime',
                0, 2, grid_width, 1,
            )
            self.add_tooltip('--no-mtime', checkbutton2)

        checkbutton3 = self.add_checkbutton(grid,
            _('Download all videos into this folder'),
            None,
            0, 3, 1, 1,
        )
        # (Signal connect appears below)

        # (Currently, only three fixed folders are elligible for this mode, so
        #   we'll just add them individually)
        store = Gtk.ListStore(GdkPixbuf.Pixbuf, int, str)
        # (Guard against mainapp.TartubeApp.debug_open_options_win_flag being
        #   set...)
        count = 0
        if self.app_obj.fixed_misc_folder:
            store.append([
                self.app_obj.main_win_obj.pixbuf_dict['folder_green_small'],
                self.app_obj.fixed_misc_folder.dbid,
                self.app_obj.fixed_misc_folder.name,
            ])
            misc_index = count
            count += 1

        if self.app_obj.fixed_clips_folder:
            store.append([
                self.app_obj.main_win_obj.pixbuf_dict['folder_green_small'],
                self.app_obj.fixed_clips_folder.dbid,
                self.app_obj.fixed_clips_folder.name,
            ])
            clips_index = count
            count += 1

        if self.app_obj.fixed_temp_folder:
            store.append([
                self.app_obj.main_win_obj.pixbuf_dict['folder_blue_small'],
                self.app_obj.fixed_temp_folder.dbid,
                self.app_obj.fixed_temp_folder.name,
            ])
            temp_index = count
            count += 1

        combo = Gtk.ComboBox.new_with_model(store)
        grid.attach(combo, 1, 3, 1, 1)

        renderer_pixbuf = Gtk.CellRendererPixbuf()
        combo.pack_start(renderer_pixbuf, False)
        combo.add_attribute(renderer_pixbuf, 'pixbuf', 0)

        renderer_text = Gtk.CellRendererText()
        combo.pack_start(renderer_text, True)
        combo.add_attribute(renderer_text, 'text', 2)

        combo.set_entry_text_column(0)
        combo.set_hexpand(True)
        # (Signal connect appears below)

        # (Acceptable values are 'temp', 'misc', 'clips'
        current_override = self.edit_obj.options_dict['use_fixed_folder']
        if current_override is None:
            checkbutton3.set_active(False)
            combo.set_sensitive(False)
            combo.set_active(0)

        else:
            checkbutton3.set_active(True)
            combo.set_sensitive(True)
            if current_override == 'temp':
                combo.set_active(temp_index)
            elif current_override == 'misc':
                combo.set_active(misc_index)
            elif current_override == 'clips':
                combo.set_active(clips_index)
            else:
                # Default to the 'Unsorted Videos' folder
                combo.set_active(misc_index)

        # (Signal connects from above)
        checkbutton3.connect('toggled', self.on_fixed_folder_toggled, combo)
        combo.connect('changed', self.on_fixed_folder_changed)

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 4, grid_width, 1)

        # Filesystem options (yt-dlp only)
        self.add_label(grid2,
            '<u>' + _('Filesystem options') + '</u>' + self.ytdlp_only(),
            0, 0, grid_width, 1,
        )

        checkbutton4 = self.add_checkbutton(grid2,
            _('Force filenames to be MS Windows compatible'),
            'windows_filenames',
            0, 1, grid_width, 1,
        )
        self.add_tooltip('--windows-filenames', checkbutton4)

        label = self.add_label(grid2,
            _(
                'Limit filename length (excluding extension) to this' \
                + ' many characters',
            ),
            0, 2, 1, 1
        )

        spinbutton = self.add_spinbutton(grid2,
            0, None, 1,
            'trim_filenames',
            1, 2, 1, 1
        )
        self.add_tooltip('--trim-filenames', label, spinbutton)

        if os.name != 'nt':
            msg = _(
                'WARNING: The filename length includes the length of the' \
                + ' folder name!',
            )
        else:
            msg = _(
                'WARNING: The filename length includes the length of the' \
                + ' directory name!',
            )

        self.add_label(grid2,
            '<i>' + msg + '</i>',
            0, 3, grid_width, 1,
        )

        if not self.app_obj.simple_options_flag:

            checkbutton5 = self.add_checkbutton(grid2,
                _('Do not overwrite any files'),
                'no_overwrites',
                0, 4, grid_width, 1,
            )
            self.add_tooltip('--no-overwrites', checkbutton5)

            checkbutton6 = self.add_checkbutton(grid2,
                _(
                    'Overwrite all video and metadata files (includes' \
                    + ' \'--no-continue\')',
                ),
                'force_overwrites',
                0, 5, grid_width, 1,
            )
            self.add_tooltip('--force-overwrites', checkbutton6)

            checkbutton7 = self.add_checkbutton(grid2,
                _('Write playlist metadata in addition to video metadata'),
                'write_playlist_metafiles',
                0, 6, grid_width, 1,
            )
            self.add_tooltip('--write-playlist-metafiles', checkbutton7)

            checkbutton8 = self.add_checkbutton(grid2,
                _(
                    'Write all fields, including private fields, to the' \
                    + ' .info.json file',
                ),
                'no_clean_info_json',
                0, 7, grid_width, 1,
            )
            self.add_tooltip('--no-clean-infojson', checkbutton8)


    def setup_files_cookies_tab(self, inner_notebook):

        """Called by self.setup_files_tab().

        Sets up the 'Cookies' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Files > Cookies'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Cookies'),
            inner_notebook,
        )
        grid_width = 3

        # Cookies options
        self.add_label(grid,
            '<u>' + _('Cookies options') + '</u>',
            0, 0, grid_width, 1,
        )

        label = self.add_label(grid,
            _('Path to the downloader\'s cookie jar file'),
            0, 1, 1, 1,
        )

        set_button = Gtk.Button(_('Set'))
        grid.attach(set_button, 1, 1, 1, 1)
        # (Signal connect appears below)

        reset_button = Gtk.Button(_('Reset'))
        grid.attach(reset_button, 2, 1, 1, 1)
        # (Signal connect appears below)

        entry = self.add_entry(grid,
            None,
            0, 2, grid_width, 1,
        )
        entry.set_editable(False)
        self.add_tooltip('--cookies FILE', label, entry)

        init_path = self.retrieve_val('cookies_path')
        if init_path == '':

            entry.set_text(
                os.path.abspath(
                    os.path.join(
                        self.app_obj.data_dir,
                        self.app_obj.cookie_file_name,
                    ),
                ),
            )

        else:

            entry.set_text(init_path)

        # (Signal connects from above)
        set_button.connect(
            'clicked',
            self.on_cookies_set_button_clicked,
            entry,
        )
        reset_button.connect(
            'clicked',
            self.on_cookies_reset_button_clicked,
            entry,
        )

        if not self.app_obj.simple_options_flag:

            # (To avoid messing up the neat format of the rows above, add a
            #   secondary grid, and put the next set of widgets inside it)
            grid2 = self.add_secondary_grid(grid, 0, 3, grid_width, 1)

            # Cookie options (yt-dlp only)
            self.add_label(grid2,
                '<u>' + _('Cookie options') + '</u>' + self.ytdlp_only(),
                0, 0, 1, 1,
            )

            checkbutton = self.add_checkbutton(grid2,
                _('Do not read/dump cookies from/to the cookiejar file'),
                'no_cookies',
                0, 1, 1, 1,
            )
            self.add_tooltip('--no-cookies', checkbutton)

            checkbutton2 = self.add_checkbutton(grid2,
                _('Do not load cookies from browser'),
                'no_cookies_from_browser',
                0, 2, 1, 1,
            )
            self.add_tooltip('--no-cookies-from-browser', checkbutton2)

            # (To avoid messing up the neat format of the rows above, add a
            #   secondary grid, and put the next set of widgets inside it)
            grid3 = self.add_secondary_grid(grid, 0, 4, grid_width, 1)

            label2 = self.add_label(grid3,
                _('Retrieve cookies from browser'),
                0, 0, 1, 1
            )
            label2.set_hexpand(False)

            entry2 = self.add_entry(grid3,
                None,
                1, 0, 1, 1,
            )
            entry2.set_hexpand(True)
            entry2.set_editable(False)
            entry2.set_text(self.retrieve_val('cookies_from_browser'))

            # (To avoid messing up the neat format of the rows above, add a
            #   secondary grid, and put the next set of widgets inside it)
            grid4 = self.add_secondary_grid(grid, 0, 5, grid_width, 1)

            self.add_label(grid4,
                _('Browser'),
                0, 0, 1, 1
            )

            combo_list = [
                '', 'brave', 'chrome', 'chromium', 'edge', 'firefox', 'opera',
                'safari', 'vivaldi',
            ]
            combo = self.add_combo(grid4,
                combo_list,
                None,
                1, 0, 1, 1
            )
            combo.set_active(0)
            combo.set_hexpand(True)
            # (Signal connect appears below)

            self.add_label(grid4,
                _('Chromium keyring'),
                2, 0, 1, 1
            )

            combo_list2 = ['', 'basictext', 'gnomekeyring', 'kwallet']
            combo2 = self.add_combo(grid4,
                combo_list2,
                None,
                3, 0, 1, 1
            )
            combo2.set_active(0)
            combo2.set_hexpand(True)
            # (Signal connect appears below)

            # (To avoid messing up the neat format of the rows above, add a
            #   secondary grid, and put the next set of widgets inside it)
            grid5 = self.add_secondary_grid(grid, 0, 6, grid_width, 1)

            label3 = self.add_label(grid5,
                _('Browser profile name (optional)'),
                0, 0, 1, 1
            )
            label3.set_hexpand(False)

            entry3 = self.add_entry(grid5,
                None,
                1, 0, 3, 1,
            )
            entry3.set_hexpand(True)
            # (Signal connect appears below)

            label4 = self.add_label(grid5,
                _('Browser profile path (optional)'),
                0, 1, 1, 1
            )
            label4.set_hexpand(False)

            entry4 = self.add_entry(grid5,
                None,
                1, 1, 1, 1,
            )
            entry4.set_hexpand(True)
            entry4.set_editable(False)

            set_button = Gtk.Button(_('Set'))
            grid5.attach(set_button, 2, 1, 1, 1)
            # (Signal connect appears below)

            reset_button = Gtk.Button(_('Reset'))
            grid5.attach(reset_button, 3, 1, 1, 1)
            # (Signal connect appears below)

            self.add_tooltip(
                '--cookies-from-browser BROWSER[+KEYRING][:PROFILE]',
                entry2,
                combo,
                combo2,
                entry3,
                entry4,
            )

            # Set the initial values for those widgets, retrieving them from
            #   the 'cookies_from_browser' download option
            match = re.search(
                r'^([^+:]+)(\+[^+:]+)?(\:.*)?',
                self.retrieve_val('cookies_from_browser'),
            )
            if match:
                browser = match.groups()[0]
                keyring = match.groups()[1]
                profile = match.groups()[2]

                if browser != '':

                    for i in range(len(combo_list)):
                        if combo_list[i] == browser:
                            combo.set_active(i)
                            break

                    if keyring != '' and keyring is not None:

                        # (Remove initial +/: characters)
                        keyring = keyring[1:]

                        for i in range(len(combo_list2)):
                            if combo_list2[i] == keyring:
                                combo2.set_active(i)
                                break

                    if profile != '' and profile is not None:

                        # (Remove initial +/: characters)
                        profile = profile[1:]

                        # This might be a profile name, or a profile path.
                        #   Assume it's a name unless the path exists
                        if not os.path.isfile(profile):
                            entry3.set_text(profile)
                        else:
                            entry4.set_text(profile)

            # (Signal connects from above)
            combo.connect(
                'changed',
                self.setup_files_cookies_tab_update,
                checkbutton2,
                entry2,
                combo,
                combo2,
                entry3,
                entry4,
            )
            combo2.connect(
                'changed',
                self.setup_files_cookies_tab_update,
                checkbutton2,
                entry2,
                combo,
                combo2,
                entry3,
                entry4,
            )
            entry3.connect(
                'changed',
                self.on_cookies_ytdlp_entry_changed,
                entry2,
                combo,
                combo2,
                entry3,
                entry4,
            )
            set_button.connect(
                'clicked',
                self.on_cookies_ytdlp_set_button_clicked,
                entry2,
                combo,
                combo2,
                entry3,
                entry4,
            )
            reset_button.connect(
                'clicked',
                self.on_cookies_ytdlp_reset_button_clicked,
                entry2,
                combo,
                combo2,
                entry3,
                entry4,
            )


    def setup_files_cookies_tab_update(self, widget, checkbutton, entry,
    combo, combo2, entry2, entry3):

        """Called by self.setup_files_cookies_tab() to set the
        'cookies_from_browser' download option, updating several widgets.

        Also called by several callbacks.

        Args:

            widget (Gtk.Entry or Gtk.Combo): Ignored

            checkbutton (Gtk.CheckButton): The 'Do not load cookies from
                browser' checkbutton

            entry (Gtk.Entry): The entry box displaying the download option

            combo, combo2, entry2, entry3 (Gtk.Combo, Gtk.Entry): Widgets whose
                settings are combined to set the 'cookies_from_browser'
                download option

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        browser = model[tree_iter][0]

        if browser == '':

            # Special case: reset the other two widgets, which causes further
            #   calls to this function, in which the download option is updated
            entry2.set_text('')
            combo2.set_active(0)
            return

        # Otherwise, set the new value of 'cookies_from_browser' now
        tree_iter2 = combo2.get_active_iter()
        model2 = combo2.get_model()
        keyring = model2[tree_iter2][0]

        profile_name = entry2.get_text()
        profile_path = entry3.get_text()

        value = browser
        if keyring != '':
            value += '+' + keyring

        if profile_name != '':
            value += ':' + profile_name
        elif profile_path != '':
            value += ':' + profile_path

        self.edit_dict['cookies_from_browser'] = value
        entry.set_text(value)

        # (To avoid confusion, reset the 'Do not load cookies from browser'
        #   checkbutton; the user can re-enable it, if they want)
        checkbutton.set_active(False)


    def setup_files_shortcuts_tab(self, inner_notebook):

        """Called by self.setup_files_tab().

        Sets up the 'Shortcuts' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Files > Shortcuts'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Shortcuts'),
            inner_notebook,
        )

        # Internet shortcut options (yt-dlp only)
        self.add_label(grid,
            '<u>' + _('Internet shortcut options') + '</u>' \
            + self.ytdlp_only(),
            0, 0, 1, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Write an internet shortcut file (.url, .webloc or .desktop)'),
            'write_link',
            0, 1, 1, 1,
        )
        self.add_tooltip('--write-link', checkbutton)

        checkbutton2 = self.add_checkbutton(grid,
            _('Write a Windows internet shortcut file (.url)'),
            'write_url_link',
            0, 2, 1, 1,
        )
        self.add_tooltip('--write-url-link', checkbutton2)

        checkbutton3 = self.add_checkbutton(grid,
            _('Write a macOS inernet shortcut file (.webloc)'),
            'write_webloc_link',
            0, 3, 1, 1,
        )
        self.add_tooltip('--write-webloc-link', checkbutton3)

        checkbutton4 = self.add_checkbutton(grid,
            _('Write a Linux internet shortcut file (.desktop)'),
            'write_desktop_link',
            0, 4, 1, 1,
        )
        self.add_tooltip('--write-desktop-link', checkbutton4)


    def setup_files_write_move_tab(self, inner_notebook):

        """Called by self.setup_files_tab().

        Sets up the 'Write/move' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Files > Write/move'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Write/move'),
            inner_notebook,
        )
        grid_width = 2

        # File write options
        self.add_label(grid,
            '<u>' + _('File write options') + '</u>',
            0, 0, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Write video\'s description to a .description file'),
            'write_description',
            0, 1, grid_width, 1,
        )
        self.add_tooltip('--write-description', checkbutton)

        checkbutton2 = self.add_checkbutton(grid,
            _('Write video\'s metadata to an .info.json file'),
            'write_info',
            0, 2, grid_width, 1,
        )
        self.add_tooltip('--write-info-json', checkbutton2)

        checkbutton3 = self.add_checkbutton(grid,
            _(
            'Write video\'s annotations to an .annotations.xml file',
            ),
            'write_annotations',
            0, 3, grid_width, 1,
        )
        self.add_tooltip('--write-annotations', checkbutton3)

        self.add_label(grid,
            '<i>' + _(
            'Annotations are not downloaded when checking videos/channels/' \
            + 'playlists/folders'
            ) + '</i>',
            1, 4, 1, 1,
        )

        checkbutton4 = self.add_checkbutton(grid,
            _('Write the video\'s thumbnail to the same folder'),
            'write_thumbnail',
            0, 5, grid_width, 1,
        )
        self.add_tooltip('--write-thumbnail', checkbutton4)

        # File move options
        self.add_label(grid,
            '<u>' + _('File move options') + '</u>',
            0, 6, grid_width, 1,
        )

        self.add_checkbutton(grid,
            _('Move video\'s description file into a sub-folder'),
            'move_description',
            0, 7, grid_width, 1,
        )

        self.add_checkbutton(grid,
            _('Write video\'s metadata file into a sub-folder'),
            'move_info',
            0, 8, grid_width, 1,
        )

        self.add_checkbutton(grid,
            _('Write video\'s annotations file into a sub-folder'),
            'move_annotations',
            0, 9, grid_width, 1,
        )

        self.add_checkbutton(grid,
            _('Write the video\'s thumbnail into a sub-folder'),
            'move_thumbnail',
            0, 10, grid_width, 1,
        )


    def setup_files_keep_tab(self, inner_notebook):

        """Called by self.setup_files_tab().

        Sets up the 'Keep' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Files > Keep'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Keep'),
            inner_notebook,
        )

        # Options during real (not simulated) downloads
        self.add_label(grid,
            '<u>' + _('Options during real (not simulated) downloads') \
            + '</u>',
            0, 0, 1, 1,
        )

        self.add_checkbutton(grid,
            _('Keep the description file after the download has finished'),
            'keep_description',
            0, 1, 1, 1,
        )

        self.add_checkbutton(grid,
            _('Keep the metadata file after the download has finished'),
            'keep_info',
            0, 2, 1, 1,
        )

        self.add_checkbutton(grid,
            _('Keep the annotations file after the download has finished'),
            'keep_annotations',
            0, 3, 1, 1,
        )

        self.add_checkbutton(grid,
            _('Keep the thumbnail file after the download has finished'),
            'keep_thumbnail',
            0, 4, 1, 1,
        )

        # Options during simulated (not real) downloads
        self.add_label(grid,
            '<u>' + _('Options during simulated (not real) downloads') \
            + '</u>',
            0, 5, 1, 1,
        )

        self.add_checkbutton(grid,
            _('Keep the description file after the download has finished'),
            'sim_keep_description',
            0, 6, 1, 1,
        )

        self.add_checkbutton(grid,
            _('Keep the metadata file after the download has finished'),
            'sim_keep_info',
            0, 7, 1, 1,
        )

        self.add_checkbutton(grid,
            _('Keep the annotations file after the download has finished'),
            'sim_keep_annotations',
            0, 8, 1, 1,
        )

        self.add_checkbutton(grid,
            _('Keep the thumbnail file after the download has finished'),
            'sim_keep_thumbnail',
            0, 9, 1, 1,
        )


    def setup_formats_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Formats' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Formats'
        )

        # Add this tab...
        tab, grid = self.add_notebook_tab(_('F_ormats'), 0)

        # When advanced download options are visible, use an inner tab;
        #   otherwise display the same content in the main tab
        if self.app_obj.simple_options_flag:

            self.setup_formats_tab_add_grid(grid)

        else:

            # ...and an inner notebook...
            inner_notebook = self.add_inner_notebook(grid)

            # ...with its own tabs
            self.setup_formats_preferred_tab(inner_notebook)
            self.setup_formats_advanced_tab(inner_notebook)


    def setup_formats_preferred_tab(self, inner_notebook):

        """Called by self.setup_formats_tab().

        Sets up the 'Preferred' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        tab, grid = self.add_inner_notebook_tab(
            _('_Preferred'),
            inner_notebook,
        )

        self.setup_formats_tab_add_grid(grid)


    def setup_formats_tab_add_grid(self, grid):

        """Called by self.setup_formats_tab() and
         .setup_formats_preferred_tab().

        Adds widgets to the specified grid.

        Args:

            grid (Gtk.Grid): The main grid for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Formats > Preferred'
        )

        # (Force both treeviews to take half the available width)
        grid.set_column_homogeneous(True)
        grid_width = 4

        # Preferred format options
        self.add_label(grid,
            '<u>' + _('Preferred format options') + '</u>',
            0, 0, grid_width, 1,
        )

        if self.app_obj.simple_options_flag:

            # Newbies frequently get confused by this tab, so add an
            #   explanatory warning

            hbox = Gtk.HBox()
            grid.attach(hbox, 0, 1, grid_width, 1)

            frame = Gtk.Frame()
            hbox.pack_start(frame, False, False, 0)
            frame.set_hexpand(False)

            hbox2 = Gtk.HBox()
            frame.add(hbox2)
            hbox2.set_border_width(self.spacing_size)

            image = Gtk.Image()
            hbox2.pack_start(image, False, False, 0)
            image.set_from_pixbuf(
                self.app_obj.main_win_obj.pixbuf_dict['attention_large'],
            )

            frame2 = Gtk.Frame()
            hbox.pack_start(frame2, True, True, self.spacing_size)
            frame2.set_hexpand(True)

            vbox = Gtk.VBox()
            frame2.add(vbox)
            vbox.set_border_width(self.spacing_size)

            label = Gtk.Label()
            vbox.pack_start(label, True, True, 0)
            label.set_markup(
                '<b>' + _(
                    'If your preferred formats are not available, the' \
                    + ' download will fail!',
                ) + '</b>',
            )

            label2 = Gtk.Label()
            vbox.pack_start(label2, True, True, 0)
            label2.set_markup(
                '<b>' + _(
                    'If you want a specific format, install FFmpeg and use' \
                    + ' the Convert tab!',
                ) + '</b>',
            )

        # Left column
        self.add_label(grid,
            _('Recognised video/audio formats'),
            0, 2, 2, 1,
        )

        treeview, liststore = self.add_treeview(grid,
            0, 3, 2, 1,
        )
        self.setup_formats_tab_update_list(liststore)

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 4, 2, 1)

        button = Gtk.Button(_('Add format') + ' >>>')
        grid2.attach(button, 0, 0, 1, 1)
        button.set_hexpand(True)
        # (Signal connect appears below)

        button2 = Gtk.Button()
        grid2.attach(button2, 1, 0, 1, 1)
        button2.set_hexpand(False)
        button2.set_image(
            Gtk.Image.new_from_pixbuf(
                self.app_obj.main_win_obj.pixbuf_dict['keyboard_small'],
            ),
        )
        button2.set_tooltip_text(_('Type extractor code directly'))
        # (Signal connect appears below)

        self.formats_checkbutton = self.add_checkbutton(grid,
            _('Hide obsolete YouTube formats'),
            None,
            0, 5, 2, 1,
        )
        if self.app_obj.hide_obsolete_formats_flag:
            self.formats_checkbutton.set_active(True)
        self.formats_checkbutton.connect(
            'toggled',
            self.on_obsolete_format_checkbutton_toggled,
            liststore,
        )

        # Right column
        self.add_label(grid,
            _('List of preferred formats'),
            2, 2, 2, 1,
        )

        treeview2, self.formats_liststore = self.add_treeview(grid,
            2, 3, 2, 1,
        )
        self.add_tooltip('-f, --format FORMAT', treeview, treeview2)

        # There are multiple possible format options, any or all of which might
        #   be set
        self.formats_tab_redraw_list()

        button3 = Gtk.Button('<<< ' + _('Remove format'))
        grid.attach(button3, 2, 4, 2, 1)
        # (Signal connect appears below)

        button4 = Gtk.Button('^ ' + _('Move up'))
        grid.attach(button4, 2, 5, 1, 1)
        # (Signal connect appears below)

        button5 = Gtk.Button('v ' + _('Move down'))
        grid.attach(button5, 3, 5, 1, 1)
        # (Signal connect appears below)

        # (Signal connects from above)
        # 'Add format'
        button.connect(
            'clicked',
            self.on_formats_tab_add_clicked,
            button3,
            button4,
            button5,
            treeview,
        )
        # 'Type extractor code directly'
        button2.connect(
            'clicked',
            self.on_formats_tab_type_clicked,
            button3,
            button4,
            button5,
            treeview,
        )
        # 'Remove format'
        button3.connect(
            'clicked',
            self.on_formats_tab_remove_clicked,
            button,
            button2,
            button4,
            button5,
            treeview2,
        )
        # 'Move up'
        button4.connect(
            'clicked',
            self.on_formats_tab_up_clicked,
            treeview2,
        )
        # 'Move down'
        button5.connect(
            'clicked',
            self.on_formats_tab_down_clicked,
            treeview2,
        )

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid3 = self.add_secondary_grid(grid, 0, 6, grid_width, 1)

        # Desensitise buttons, as appropriate
        format_count = self.formats_tab_count_formats()
        if format_count == 0:
            button3.set_sensitive(False)
            button4.set_sensitive(False)
            button5.set_sensitive(False)

        label3 = self.add_label(grid3,
            _(
            'If a merge is required after post-processing, output to this' \
            + ' format:',
            ),
            0, 0, 1, 1,
        )

        combo_list = ['', 'flv', 'mkv', 'mp4', 'ogg', 'webm']
        combo = self.add_combo(grid3,
            combo_list,
            None,
            1, 0, 1, 1,
        )
        combo.set_active(
            combo_list.index(self.retrieve_val('merge_output_format')),
        )
        combo.set_hexpand(True)
        combo.connect('changed', self.on_formats_tab_combo_changed)
        self.add_tooltip('--merge-output-format FORMAT', label3, combo)


    def setup_formats_tab_update_list(self, liststore):

        """Called by self.setup_formats_tab_add_grid(), etc.

        Updates the (full) list of formats on the left-hand side.

        Args:

            liststore (Gtk.ListStore): The liststore to update

        """

        liststore.clear()

        for key in formats.VIDEO_OPTION_LIST:

            value = formats.VIDEO_OPTION_DICT[key]

            if not self.app_obj.hide_obsolete_formats_flag or \
            not formats.VIDEO_OPTION_OBSOLETE_DICT[value]:
                liststore.append([key])


    def setup_formats_advanced_tab(self, inner_notebook):

        """Called by self.setup_formats_tab().

        Sets up the 'Advanced' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Formats > Advanced'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Advanced'),
            inner_notebook,
        )
        grid_width = 2
        extra_row = 0

        # Multiple format options
        self.add_label(grid,
            '<u>' + _('Multiple format options') + '</u>',
            0, 0, grid_width, 1,
        )

        if self.app_obj.allow_ytdl_archive_flag:

            extra_row = 1
            self.add_label(grid,
                '<i>' + _(
                    'Multiple formats will not be downloaded, because an' \
                    + ' archive file will be created'
                ) + '\n' + _(
                    'The archive file can be disabled in the System' \
                    ' Preferences window',
                ) + '</i>',
                0, 1, grid_width, 1,
            )

        radiobutton = self.add_radiobutton(grid,
            None,
            _(
            'For each video, download the first available format from the' \
            + ' preferred list',
            ),
            None,
            None,
            0, (1 + extra_row), grid_width, 1,
        )
        if self.retrieve_val('video_format_mode') == 'single':
            radiobutton.set_active(True)
        # (Signal connect appears below)

        radiobutton2 = self.add_radiobutton(grid,
            radiobutton,
            _(
            'For each video, combine the first video and first audio format' \
            + ' from the preferred list',
            ),
            None,
            None,
            0, (2 + extra_row), grid_width, 1,
        )
        if self.retrieve_val('video_format_mode') == 'combine':
            radiobutton2.set_active(True)
        # (Signal connect appears below)

        radiobutton3 = self.add_radiobutton(grid,
            radiobutton2,
            _(
            'For each video, download all available formats from the' \
            + ' preferred list',
            ),
            None,
            None,
            0, (3 + extra_row), grid_width, 1,
        )
        if self.retrieve_val('video_format_mode') == 'multiple':
            radiobutton3.set_active(True)
        # (Signal connect appears below)

        radiobutton4 = self.add_radiobutton(grid,
            radiobutton2,
            _('Download all available formats for all videos'),
            None,
            None,
            0, (4 + extra_row), grid_width, 1,
        )
        if self.retrieve_val('video_format_mode') == 'all':
            radiobutton4.set_active(True)
        # (Signal connect appears below)

        # (Signal connects from above)
        radiobutton.connect(
            'toggled',
            self.on_video_format_mode_toggled,
            'single',
        )
        radiobutton2.connect(
            'toggled',
            self.on_video_format_mode_toggled,
            'combine',
        )
        radiobutton3.connect(
            'toggled',
            self.on_video_format_mode_toggled,
            'multiple',
        )
        radiobutton4.connect(
            'toggled',
            self.on_video_format_mode_toggled,
            'all',
        )

        # Other format options
        self.add_label(grid,
            '<u>' + _('Other format options') + '</u>',
            0, (5 + extra_row), grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Prefer free video formats, unless one is specified above'),
            'prefer_free_formats',
            0, (6 + extra_row), grid_width, 1,
        )
        self.add_tooltip('--prefer-free-formats', checkbutton)

        checkbutton2 = self.add_checkbutton(grid,
            _('Do not download DASH-related data for YouTube videos'),
            'yt_skip_dash',
            0, (7 + extra_row), grid_width, 1,
        )
        self.add_tooltip('--youtube-skip-dash-manifest', checkbutton2)

        # Other format options (yt-dlp only)
        self.add_label(grid,
            '<u>' + _('Other format options') + '</u>' \
            + self.ytdlp_only(),
            0, (8 + extra_row), grid_width, 1,
        )

        checkbutton3 = self.add_checkbutton(grid,
            _(
                'Check formats selected are actually downloadable' \
                + ' (Experimental)',
            ),
            'check_formats',
            0, (9 + extra_row), grid_width, 1,
        )
        self.add_tooltip('--check-formats', checkbutton3)

        checkbutton4 = self.add_checkbutton(grid,
            _(
                'Allow unplayable formats to be listed and downloaded (also' \
                + ' disables post-processing)',
            ),
            'allow_unplayable_formats',
            0, (10 + extra_row), grid_width, 1,
        )
        self.add_tooltip('--allow-unplayable-formats', checkbutton4)


    def setup_convert_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Convert' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Convert'
        )

        tab, grid = self.add_notebook_tab(_('_Convert'))
        grid_width = 4

        # Convert to video
        self.add_label(grid,
            '<u>' + _('Convert to video') + '</u>',
            0, 0, grid_width, 1,
        )

        label = self.add_label(grid,
            _('Convert downloaded video to another format'),
            0, 1, 2, 1,
        )

        combo_list = ['', 'avi', 'flv', 'mkv', 'mp4', 'ogg', 'webm']
        combo = self.add_combo(grid,
            combo_list,
            'recode_video',
            2, 1, 2, 1,
        )
        self.add_tooltip('--recode-video FORMAT', label, combo)

        # Convert to audio
        self.add_label(grid,
            '<u>' + _('Convert to audio') + '</u>',
            0, 2, grid_width, 1,
        )

        # (The MS Windows installer includes FFmpeg)
        text = _(
            'Download each video, extract the sound, and then discard the' \
            + ' original video',
        )
        if os.name != 'nt':
            text += '\n' + _(
                '(requires that FFmpeg or AVConv is installed on your system)'
            )

        checkbutton = self.add_checkbutton(grid,
            text,
            'extract_audio',
            0, 3, grid_width, 1,
        )
        self.add_tooltip('-x, --extract-audio', checkbutton)

        label = self.add_label(grid,
            _('Use this audio format:') + ' ',
            0, 4, 1, 1,
        )
        label.set_hexpand(False)

        combo_list = formats.AUDIO_FORMAT_LIST.copy()
        combo_list.insert(0, '')
        combo = self.add_combo(grid,
            combo_list,
            'audio_format',
            1, 4, 1, 1,
        )
        combo.set_hexpand(True)
        self.add_tooltip('--audio-format FORMAT', label, combo)

        label2 = self.add_label(grid,
            _('Use this audio quality:') + ' ',
            2, 4, 1, 1,
        )
        label2.set_hexpand(False)

        combo2_list = [
            [_('High'), '0'],
            [_('Medium'), '5'],
            [_('Low'), '9'],
        ]

        combo2 = self.add_combo_with_data(grid,
            combo2_list,
            'audio_quality',
            3, 4, 1, 1,
        )
        combo2.set_hexpand(True)
        self.add_tooltip('--audio-quality QUALITY', label2, combo2)


    def setup_post_process_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Post-processing' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Post-processing'
        )

        # Add this tab...
        tab, grid = self.add_notebook_tab(_('_Post-processing'), 0)

        # ...and an inner notebook...
        inner_notebook = self.add_inner_notebook(grid)

        # ...with its own tabs
        self.setup_post_process_general_tab(inner_notebook)
        self.setup_post_process_ytdlp_tab(inner_notebook)


    def setup_post_process_general_tab(self, inner_notebook):

        """Called by self.setup_post_process_tab().

        Sets up the 'General' inner notebook tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Post-processing > General'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_General'),
            inner_notebook,
        )
        grid_width = 2
        grid.set_column_homogeneous(True)

        # Post-processing options
        self.add_label(grid,
            '<u>' + _('Post-processing options') + '</u>',
            0, 0, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _(
            'Post-process video files to convert them to audio-only files',
            ),
            'extract_audio',
            0, 1, grid_width, 1,
        )
        self.add_tooltip('-x, --extract-audio', checkbutton)

        checkbutton2 = self.add_checkbutton(grid,
            _('Prefer AVConv over FFmpeg'),
            'prefer_avconv',
            0, 2, 1, 1,
        )
        if os.name == 'nt':
            checkbutton2.set_sensitive(False)
        self.add_tooltip('-prefer-avconv', checkbutton2)

        checkbutton3 = self.add_checkbutton(grid,
            _('Prefer FFmpeg over AVConv (default)'),
            'prefer_ffmpeg',
            1, 2, 1, 1,
        )
        if os.name == 'nt':
            checkbutton3.set_sensitive(False)
        self.add_tooltip('--prefer-ffmpeg', checkbutton3)

        label = self.add_label(grid,
            _('Audio format of the post-processed file'),
            0, 3, 1, 1,
        )

        combo_list = formats.AUDIO_FORMAT_LIST.copy()
        combo_list.insert(0, '')
        combo = self.add_combo(grid,
            combo_list,
            'audio_format',
            1, 3, 1, 1,
        )
        self.add_tooltip('--audio-format FORMAT', label, combo)

        label2 = self.add_label(grid,
            _('Audio quality of the post-processed file'),
            0, 4, 1, 1,
        )

        combo2_list = [
            [_('High VBR'), '0'],
            [_('Medium VBR'), '5'],
            [_('Low VBR'), '9'],
            [_('320 kb/s'), '320k'],
            [_('256 kb/s'), '256k'],
            [_('192 kb/s'), '192k'],
            [_('128 kb/s'), '128k'],
            [_('96 kb/s'), '96k'],
        ]

        combo2 = self.add_combo_with_data(grid,
            combo2_list,
            'audio_quality',
            1, 4, 1, 1,
        )
        self.add_tooltip('--audio-quality QUALITY', label2, combo2)

        label3 = self.add_label(grid,
            _('Encode video to another format, if necessary'),
            0, 5, 1, 1,
        )

        combo_list3 = ['', 'avi', 'flv', 'mkv', 'mp4', 'ogg', 'webm']
        combo3 = self.add_combo(grid,
            combo_list3,
            'recode_video',
            1, 5, 1, 1,
        )
        self.add_tooltip('--recode-video FORMAT', label3, combo3)

        label4 = self.add_label(grid,
            _('Arguments to pass to post-processor'),
            0, 6, 1, 1,
        )

        entry = self.add_entry(grid,
            'pp_args',
            1, 6, 1, 1,
        )
        self.add_tooltip('--postprocessor-args ARGS', label4, entry)

        checkbutton4 = self.add_checkbutton(grid,
            _('Keep original file after processing it'),
            'keep_video',
            0, 7, 1, 1,
        )
        self.add_tooltip('-k, --keep-video', checkbutton4)

        # (This option can also be modified in the Post-process tab)
        self.embed_checkbutton = self.add_checkbutton(grid,
            _('Merge subtitles file with video (.mp4 only)'),
            None,
            1, 7, 1, 1,
        )
        self.embed_checkbutton.set_active(self.retrieve_val('embed_subs'))
        self.embed_checkbutton.connect(
            'toggled',
            self.on_embed_checkbutton_toggled,
        )
        self.add_tooltip('--embed-subs', self.embed_checkbutton)

        checkbutton5 = self.add_checkbutton(grid,
            _('Embed thumbnail in audio file as cover art'),
            'embed_thumbnail',
            0, 8, 1, 1,
        )
        self.add_tooltip('--embed-thumbnail', checkbutton5)

        checkbutton6 = self.add_checkbutton(grid,
            _('Write metadata to the video file'),
            'add_metadata',
            1, 8, 1, 1,
        )
        self.add_tooltip('--add-metadata', checkbutton6)

        label5 = self.add_label(grid,
            _('Automatically correct known faults of the file'),
            0, 9, 1, 1,
        )

        combo_list4 = [
            ['', ''],
            [_('Do nothing'), 'never'],
            [_('Warn, but do nothing'), 'warn'],
            [_('Fix if possible, otherwise warn'), 'detect_or_warn'],
        ]
        combo4 = self.add_combo_with_data(grid,
            combo_list4,
            'fixup_policy',
            1, 9, 1, 1,
        )
        self.add_tooltip('--fixup POLICY', combo4)


    def setup_post_process_ytdlp_tab(self, inner_notebook):

        """Called by self.setup_post_process_tab().

        Sets up the 'yt-dlp' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Post-processing > yt-dlp'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_yt-dlp'),
            inner_notebook,
        )
        grid_width = 2

        # Post-processing options (yt-dlp only)
        self.add_label(grid,
            '<u>' + _('Post-processing options') + '</u>' + self.ytdlp_only(),
            0, 0, grid_width, 1,
        )

        label = self.add_label(grid,
            _('Remux video into another container if necessary'),
            0, 1, 1, 1
        )

        combo_list = [
            '', 'mp4', 'mkv', 'flv', 'webm', 'mov', 'avi',
            'mp3', 'mka', 'm4a', 'ogg', 'opus',
        ]

        combo = self.add_combo(grid,
            combo_list,
            'remux_video',
            1, 1, 1, 1
        )
        combo.set_hexpand(True)
        self.add_tooltip('--remux-video FORMAT', label, combo)

        checkbutton = self.add_checkbutton(grid,
            _(
                'Embed metadata including chapter markers (if supported by' \
                + ' format)',
            ),
            'embed_metadata',
            0, 2, grid_width, 1,
        )
        self.add_tooltip('--embed-metadata', checkbutton)

        label2 = self.add_label(grid,
            _('Convert thumbnails to another format'),
            0, 3, 1, 1
        )

        combo_list2 = ['', 'jpg', 'png']
        combo2 = self.add_combo(grid,
            combo_list2,
            'convert_thumbnails',
            1, 3, 1, 1
        )
        combo2.set_hexpand(True)
        self.add_tooltip('--convert-thumbnails FORMAT', label2, combo2)

        checkbutton2 = self.add_checkbutton(grid,
            _(
                'Split video into multiple files based on internal chapters',
            ),
            'split_chapters',
            0, 4, grid_width, 1,
        )
        self.add_tooltip('--split-chapters', checkbutton2)

        self.add_label(grid,
            '<i>' + _(
                'N.B. The \'chapter\' prefix can be used in the \'Output\'' \
                + ' and \'Paths\' tabs',
            ) + '</i>',
            0, 5, grid_width, 1
        )


    def setup_subtitles_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Subtitles' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Subtitles'
        )

        # Add this tab...
        tab, grid = self.add_notebook_tab(_('_Subtitles'), 0)

        # ...and an inner notebook...
        inner_notebook = self.add_inner_notebook(grid)

        # ...with its own tabs
        self.setup_subtitles_options_tab(inner_notebook)
        self.setup_subtitles_more_options_tab(inner_notebook)


    def setup_subtitles_options_tab(self, inner_notebook):

        """Called by self.setup_subtitles_tab().

        Sets up the 'Options' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Subtitles > Options'
        )

        tab, grid = self.add_inner_notebook_tab(_('_Options'), inner_notebook)

        # Subtitles options
        self.add_label(grid,
            '<u>' + _('Subtitles options') + '</u>',
            0, 0, 2, 1,
        )

        radiobutton = self.add_radiobutton(grid,
            None,
            _('Don\'t download the subtitles file'),
            None,
            None,
            0, 1, 2, 1,
        )
        if self.retrieve_val('write_subs') is False:
            radiobutton.set_active(True)
        # (Signal connect appears below)

        radiobutton2 = self.add_radiobutton(grid,
            radiobutton,
            _('Download the automatic subtitles file (YouTube only)'),
            None,
            None,
            0, 2, 2, 1,
        )
        if self.retrieve_val('write_subs') is True \
        and self.retrieve_val('write_auto_subs') is True:
            radiobutton2.set_active(True)
        # (Signal connect appears below)
        self.add_tooltip('--write-sub, --write-auto-sub', radiobutton2)

        radiobutton3 = self.add_radiobutton(grid,
            radiobutton2,
            _('Download all available subtitle files'),
            None,
            None,
            0, 3, 2, 1,
        )
        if self.retrieve_val('write_subs') is True \
        and self.retrieve_val('write_all_subs') is True:
            radiobutton3.set_active(True)
        # (Signal connect appears below)
        self.add_tooltip('--write-sub, -all-subs', radiobutton3)

        radiobutton4 = self.add_radiobutton(grid,
            radiobutton3,
            _('Download subtitle file for these languages:'),
            None,
            None,
            0, 4, 2, 1,
        )
        if self.retrieve_val('write_subs') is True \
        and self.retrieve_val('write_auto_subs') is False \
        and self.retrieve_val('write_all_subs') is False:
            radiobutton4.set_active(True)
        # (Signal connect appears below)
        self.add_tooltip('--write-sub', radiobutton4)

        treeview, liststore = self.add_treeview(grid,
            0, 5, 1, 1,
        )

        for language in formats.LANGUAGE_CODE_LIST:
            liststore.append([
                language + ' [' + formats.LANGUAGE_CODE_DICT[language] + ']',
            ])

        # We need a reverse dictionary for quick lookup
        rev_dict = {}
        for key in formats.LANGUAGE_CODE_DICT:
            val = formats.LANGUAGE_CODE_DICT[key]
            rev_dict[val] = key

        button = Gtk.Button(_('Add language') + ' >>>')
        grid.attach(button, 0, 6, 1, 1)
        # (Signal connect appears below)

        treeview2, liststore2 = self.add_treeview(grid,
            1, 5, 1, 1,
        )
        lang_list = self.retrieve_val('subs_lang_list')
        # The option stores values from formats.LANGUAGE_CODE_DICT, e.g. 'en',
        #   'live_chat'. Convert them to the corresponding values, e.g.
        #   'English'
        for lang_code in lang_list:
            liststore2.append([
                rev_dict[lang_code] + ' [' + lang_code + ']',
            ])

        self.add_tooltip('--sub-lang LANGS', treeview, treeview2)

        button2 = Gtk.Button('<<< ' + _('Remove language'))
        grid.attach(button2, 1, 6, 1, 1)
        # (Signal connect appears below)

        # Desensitise the buttons, if the matching radiobutton isn't active
        if not radiobutton4.get_active():
            button.set_sensitive(False)
            button2.set_sensitive(False)

        # (Signal connects from above)
        button.connect(
            'clicked',
            self.on_subtitles_tab_add_clicked,
            treeview,
            liststore2,
            rev_dict,
        )
        button2.connect(
            'clicked',
            self.on_subtitles_tab_remove_clicked,
            treeview2,
            liststore2,
            rev_dict,
        )
        radiobutton.connect(
            'toggled',
            self.on_subtitles_toggled,
            button, button2,
            'write_subs',
        )
        radiobutton2.connect(
            'toggled',
            self.on_subtitles_toggled,
            button, button2,
            'write_auto_subs',
        )
        radiobutton3.connect(
            'toggled',
            self.on_subtitles_toggled,
            button, button2,
            'write_all_subs',
        )
        radiobutton4.connect(
            'toggled',
            self.on_subtitles_toggled,
            button, button2,
            'subs_lang',
        )


    def setup_subtitles_more_options_tab(self, inner_notebook):

        """Called by self.setup_subtitles_tab().

        Sets up the 'More options' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Subtitles > More options'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_More options'),
            inner_notebook,
        )

        # Subtitle format options
        self.add_label(grid,
            '<u>' + _('Subtitle format options') + '</u>',
            0, 0, 1, 1,
        )

        label = self.add_label(grid,
            _(
            'Preferred subtitle format(s), e.g. \'srt\', \'vtt\',' \
            + ' \'srt/ass/vtt/lrc/best\'',
            ),
            0, 1, 1, 1,
        )

        entry = self.add_entry(grid,
            'subs_format',
            0, 2, 1, 1,
        )
        self.add_tooltip('--sub-format FORMAT', label, entry)

        # Post-processing options
        self.add_label(grid,
            '<u>' + _('Post-processing options') + '</u>',
            0, 3, 1, 1,
        )

        self.add_label(grid,
            '<i>' + _('Applies to .mp4 videos only; requires FFmpeg/AVConv') \
            + '</i>',
            0, 4, 1, 1,
        )

        # (This option can also be modified in the Post-process tab)
        self.embed_checkbutton2 = self.add_checkbutton(grid,
            _('During post-processing, merge subtitles file with video'),
            None,
            0, 5, 1, 1,
        )
        self.embed_checkbutton2.set_active(self.retrieve_val('embed_subs'))
        self.embed_checkbutton2.connect(
            'toggled',
            self.on_embed_checkbutton_toggled,
        )
        self.add_tooltip('--embed-subs', self.embed_checkbutton2)


    def setup_advanced_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Advanced' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Advanced'
        )

        # Add this tab...
        tab, grid = self.add_notebook_tab(_('_Advanced'), 0)

        # ...and an inner notebook...
        inner_notebook = self.add_inner_notebook(grid)

        # ...with its own tabs
        self.setup_advanced_configuration_tab(inner_notebook)
        self.setup_advanced_authentication_tab(inner_notebook)
        self.setup_advanced_netrc_tab(inner_notebook)
        self.setup_advanced_network_tab(inner_notebook)
        self.setup_advanced_georestrict_tab(inner_notebook)
        self.setup_advanced_workaround_tab(inner_notebook)


    def setup_advanced_configuration_tab(self, inner_notebook):

        """Called by self.setup_advanced_tab().

        Sets up the 'Configuration' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Advanced > Configurations'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Configurations'),
            inner_notebook,
        )
        grid_width = 3

        # Configuration file options
        self.add_label(grid,
            '<u>' + _('Configuration file options') + '</u>',
            0, 0, grid_width, 1,
        )

        self.add_checkbutton(grid,
            _('Use the downloader\'s configuration file'),
            'downloader_config',
            0, 1, grid_width, 1,
        )

        textview, textbuffer = self.add_textview(grid,
            None,
            0, 2, grid_width, 1,
        )

        label = self.add_label(grid,
            _('File loaded from:'),
            0, 3, 1, 1,
        )
        label.set_hexpand(False)

        entry = self.add_entry(grid,
            None,
            1, 3, 1, 1,
        )
        entry.set_hexpand(True)

        msg = _('Save file')
        button = Gtk.Button(msg)
        grid.attach(button, 2, 3, 1, 1)
        button.get_child().set_width_chars(len(msg) + 6)
        # (Signal connect appears below)

        # (If the downloader's configuration file exists, load it and update
        #   the textview/entry)
        dl_config_path = ttutils.get_dl_config_path(self.app_obj)
        if os.path.isfile(dl_config_path):

            line_list = []

            try:
                with open(dl_config_path) as fh:
                    line_list = fh.readlines()

            except:
                pass

            textbuffer.set_text(str.join('', line_list))
            entry.set_text(dl_config_path)

        # (Signal connect from above)
        button.connect(
            'clicked',
            self.on_dl_config_button_clicked,
            entry,
            textbuffer,
            dl_config_path,
        )


    def setup_advanced_authentication_tab(self, inner_notebook):

        """Called by self.setup_advanced_tab().

        Sets up the 'Authentication' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Advanced > Authentication'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Authentication'),
            inner_notebook,
        )
        grid_width = 2

        # Authentication options
        self.add_label(grid,
            '<u>' + _('Authentication options') + '</u>',
            0, 0, grid_width, 1,
        )

        label = self.add_label(grid,
            _('Username with which to log in'),
            0, 1, 1, 1,
        )

        entry = self.add_entry(grid,
            'username',
            1, 1, 1, 1,
        )
        self.add_tooltip('-u, --username USERNAME', label, entry)

        label2 = self.add_label(grid,
            _('Password with which to log in'),
            0, 2, 1, 1,
        )

        entry2 = self.add_entry(grid,
            'password',
            1, 2, 1, 1,
        )
        self.add_tooltip('-p, --password PASSWORD', label2, entry2)

        label3 = self.add_label(grid,
            _('Password required for this URL'),
            0, 3, 1, 1,
        )

        entry3 = self.add_entry(grid,
            'video_password',
            1, 3, 1, 1,
        )
        self.add_tooltip('--video-password PASSWORD', label3, entry3)

        label4 = self.add_label(grid,
            _('Two-factor authentication code'),
            0, 4, 1, 1,
        )

        entry4 = self.add_entry(grid,
            'two_factor',
            1, 4, 1, 1,
        )
        self.add_tooltip('-2, --twofactor TWOFACTOR', label4, entry4)

        label5 = self.add_label(grid,
            _(
                'Adobe Pass multiple-system operator (TV provider)' \
                + ' identifier',
            ),
            0, 5, 1, 1
        )

        entry5 = self.add_entry(grid,
            'ap_mso',
            1, 5, 1, 1,
        )
        self.add_tooltip('--ap-mso MSO', label5, entry5)

        label6 = self.add_label(grid,
            _(' Adobe Pass multiple-system operator account login'),
            0, 6, 1, 1
        )

        entry6 = self.add_entry(grid,
            'ap_username',
            1, 6, 1, 1,
        )
        self.add_tooltip('--ap-username USERNAME', label6, entry6)

        label7 = self.add_label(grid,
            _('Adobe Pass multiple-system operator account password'),
            0, 7, 1, 1
        )

        entry7 = self.add_entry(grid,
            'ap_password',
            1, 7, 1, 1,
        )
        self.add_tooltip('--ap-password PASSWORD', label7, entry7)

        self.add_youtube_warning(grid, 0, 8, 2, 1)


    def setup_advanced_netrc_tab(self, inner_notebook):

        """Called by self.setup_advanced_tab().

        Sets up the '.netrc' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Advanced > .netrc'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('._netrc'),
            inner_notebook,
        )
        grid_width = 3

        # .netrc options
        self.add_label(grid,
            '<u>' + _('.netrc options') + '</u>',
            0, 0, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Use .netrc authentication data'),
            'net_rc',
            0, 1, grid_width, 1,
        )
        self.add_tooltip('-n, --netrc', checkbutton)

        textview, textbuffer = self.add_textview(grid,
            None,
            0, 2, grid_width, 1,
        )

        label = self.add_label(grid,
            _('File loaded from:'),
            0, 3, 1, 1,
        )
        label.set_hexpand(False)

        entry = self.add_entry(grid,
            None,
            1, 3, 1, 1,
        )
        entry.set_hexpand(True)

        msg = _('Save file')
        button = Gtk.Button(msg)
        grid.attach(button, 2, 3, 1, 1)
        button.get_child().set_width_chars(len(msg) + 6)
        # (Signal connect appears below)

        # (If the .netrc file exists, load it and update the textview/entry)
        netrc_path = os.path.abspath(
            os.path.join(
                os.path.expanduser('~'),
                '.netrc',
            ),
        )
        if os.path.isfile(netrc_path):

            line_list = []

            try:
                with open(netrc_path) as fh:
                    line_list = fh.readlines()

            except:
                pass

            textbuffer.set_text(str.join('', line_list))
            entry.set_text(netrc_path)

        self.add_youtube_warning(grid, 0, 4, grid_width, 1)

        # (Signal connect from above)
        button.connect(
            'clicked',
            self.on_netrc_button_clicked,
            entry,
            textbuffer,
            netrc_path,
        )


    def setup_advanced_network_tab(self, inner_notebook):

        """Called by self.setup_advanced_tab().

        Sets up the 'Network' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Advanced > Network'
        )

        tab, grid = self.add_inner_notebook_tab(_('N_etwork'), inner_notebook)
        grid_width = 2

        # Network options
        self.add_label(grid,
            '<u>' + _('Network options') + '</u>',
            0, 0, grid_width, 1,
        )

        label = self.add_label(grid,
            _(
            'Use this HTTP/HTTPS proxy (if set, overrides the proxies in' \
            + ' Tartube\'s preferences window)',
            ),
            0, 1, grid_width, 1,
        )

        entry = self.add_entry(grid,
            'proxy',
            0, 2, grid_width, 1,
        )
        self.add_tooltip('--proxy URL', label, entry)

        label2 = self.add_label(grid,
            _('Time to wait for socket connection, before giving up'),
            0, 3, 1, 1,
        )

        entry2 = self.add_entry(grid,
            'socket_timeout',
            1, 3, 1, 1,
        )
        self.add_tooltip('--socket-timeout SECONDS', label2, entry2)

        label3 = self.add_label(grid,
            _('Bind with this Client-side IP address'),
            0, 4, 1, 1,
        )

        entry3 = self.add_entry(grid,
            'source_address',
            1, 4, 1, 1,
        )
        self.add_tooltip('--source-address IP', label3, entry3)

        checkbutton = self.add_checkbutton(grid,
            _('Connect using IPv4 only'),
            'force_ipv4',
            0, 5, grid_width, 1,
        )
        self.add_tooltip('-4, --force-ipv4', checkbutton)

        checkbutton2 = self.add_checkbutton(grid,
            _('Connect using IPv6 only'),
            'force_ipv6',
            0, 6, grid_width, 1,
        )
        self.add_tooltip('-6, --force-ipv6', checkbutton2)


    def setup_advanced_georestrict_tab(self, inner_notebook):

        """Called by self.setup_advanced_tab().

        Sets up the 'Geo-restriction' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Advanced > Geo-restriction'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Geo-restriction'),
            inner_notebook,
        )
        grid_width = 2

        # Geo-restriction options
        self.add_label(grid,
            '<u>' + _('Geo-restriction options') + '</u>',
            0, 0, grid_width, 1,
        )

        label = self.add_label(grid,
            _('Use this proxy to verify IP address'),
            0, 1, 1, 1,
        )

        entry = self.add_entry(grid,
            'geo_verification_proxy',
            1, 1, 1, 1,
        )
        self.add_tooltip('--geo-verification-proxy URL', label, entry)

        checkbutton = self.add_checkbutton(grid,
            _('Bypass using fake X-Forwarded-For HTTP header'),
            'geo_bypass',
            0, 2, 1, 1,
        )
        self.add_tooltip('--geo-bypass', checkbutton)

        checkbutton2 = self.add_checkbutton(grid,
            _('Don\'t bypass using fake HTTP header'),
            'no_geo_bypass',
            1, 2, 1, 1,
        )
        self.add_tooltip('--no-geo-bypass', checkbutton2)

        label2 = self.add_label(grid,
            _('Bypass geo-restriction with ISO 3166-2 country code'),
            0, 3, 1, 1,
        )

        entry2 = self.add_entry(grid,
            'geo_bypass_country',
            1, 3, 1, 1,
        )
        self.add_tooltip('--geo-bypass-country CODE', label2, entry2)

        label3 = self.add_label(grid,
            _('Bypass with explicit IP block in CIDR notation'),
            0, 4, 1, 1,
        )

        entry3 = self.add_entry(grid,
            'geo_bypass_ip_block',
            1, 4, 1, 1,
        )
        self.add_tooltip('--geo-bypass-ip-block IP_BLOCK', label3, entry3)


    def setup_advanced_workaround_tab(self, inner_notebook):

        """Called by self.setup_advanced_tab().

        Sets up the 'Workaround' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Advanced > Workaround'
        )

        tab, grid = self.add_inner_notebook_tab('_Workaround', inner_notebook)
        grid_width = 2

        # Workaround options
        self.add_label(grid,
            '<u>' + _('Workaround options') + '</u>',
            0, 0, grid_width, 1,
        )

        label = self.add_label(grid,
            _('Custom user agent'),
            0, 1, 1, 1,
        )

        entry = self.add_entry(grid,
            'user_agent',
            1, 1, 1, 1,
        )
        self.add_tooltip('--user-agent UA', label, entry)

        label2 = self.add_label(grid,
            _('Custom referer if video access has restricted domain'),
            0, 2, 1, 1,
        )

        entry2 = self.add_entry(grid,
            'referer',
            1, 2, 1, 1,
        )
        self.add_tooltip('--referer URL', label2, entry2)

        label3 = self.add_label(grid,
            _('Minimum seconds to sleep before each download'),
            0, 3, 1, 1,
        )

        spinbutton = self.add_spinbutton(grid,
            0, 3600, 1,
            None,
            1, 3, 1, 1
        )
        spinbutton.set_value(self.edit_obj.options_dict['min_sleep_interval'])
        # (Signal connect appears below)
        self.add_tooltip('--sleep-interval SECONDS', label3, spinbutton)

        label4 = self.add_label(grid,
            _('Maximum seconds to sleep before each download'),
            0, 4, 1, 1,
        )

        spinbutton2 = self.add_spinbutton(grid,
            0, 3600, 1,
            'max_sleep_interval',
            1, 4, 1, 1
        )
        if self.edit_obj.options_dict['min_sleep_interval'] == 0:
            spinbutton2.set_sensitive(False)
        self.add_tooltip('--max-sleep-interval SECONDS', label4, spinbutton2)

        # (Signal connect from above)
        spinbutton.connect(
            'value-changed',
            self.on_sleep_button_changed,
            spinbutton2,
        )

        label5 = self.add_label(grid,
            _('Force this encoding (experimental)'),
            0, 5, 1, 1,
        )

        entry3 = self.add_entry(grid,
            'force_encoding',
            1, 5, 1, 1,
        )
        self.add_tooltip('--encoding ENCODING', label5, entry3)

        checkbutton = self.add_checkbutton(grid,
            _('Suppress HTTPS certificate validation'),
            'no_check_certificate',
            0, 6, grid_width, 1,
        )
        self.add_tooltip('--no-check-certificate', checkbutton)

        checkbutton2 = self.add_checkbutton(grid,
            _(
            'Use an unencrypted connection to retrieve information about' \
            + ' videos (YouTube only)',
            ),
            'prefer_insecure',
            0, 7, grid_width, 1,
        )
        self.add_tooltip('--prefer-insecure', checkbutton2)

        # Workaround options (yt-dlp only)
        self.add_label(grid,
            '<u>' + _('Workaround options') + '</u>' + self.ytdlp_only(),
            0, 8, grid_width, 1,
        )

        label = self.add_label(grid,
            _(
                'Number of seconds to sleep between requests during data' \
                + ' extraction',
            ),
            0, 9, 1, 1
        )

        spinbutton3 = self.add_spinbutton(grid,
            0, None, 1,
            'sleep_requests',
            1, 9, 1, 1
        )
        self.add_tooltip('--sleep-requests SECONDS', label, spinbutton3)

        label2 = self.add_label(grid,
            _(
                'Number of seconds to sleep before each download (or minimum' \
                + ' time)',
            ),
            0, 10, 1, 1
        )

        spinbutton4 = self.add_spinbutton(grid,
            0, None, 1,
            'sleep_subtitles',
            1, 10, 1, 1
        )
        self.add_tooltip('--sleep-subtitles SECONDS', label2, spinbutton4)


    # (Tab support functions - general)


    def file_tab_sensitise_widgets(self, flag):

        """Called by self.setup_files_names_tab() and
        self.on_file_tab_combo_changed().

        Sensitises or desensitises a list of widgets in response to the user's
        interactions with widgets on that tab.

        Also resets comboboxes to show their first items.

        Args:

            flag (bool): True to sensitise the widgets, False to desensitise
                them

        """

        self.template_flag = flag
        for widget in self.template_widget_list:

            widget.set_sensitive(flag)

            # All combos in this tab (except for the one in the top-left
            #   corner, which is not in self.template_widget_list) must be
            #   reset to show their first item
            if isinstance(widget, Gtk.ComboBox):
                widget.set_active(0)


    def file_tab_update_time_format(self, value, entry, button):

        """Called by self.on_date_time_combo_changed() and
        .on_file_tab_reset_button_clicked().

        Updates the contents of the 'Time format' entry box, and (de)sensitises
        widgets.

        Args:

            value (str): A component used in youtube-dl's output template

            entry (Gtk.Entry): A widget to update

            button (Gtk.Button): Another widget to update

        """

        if value == 'release_date_custom' \
        or value == 'upload_date_custom':

            entry.set_text('%Y-%m-%d')
            entry.set_sensitive(True)
            button.set_sensitive(True)

        elif value == 'timestamp_custom' \
        or value == 'duration_custom':

            entry.set_text('%H-%M-%S')
            entry.set_sensitive(True)
            button.set_sensitive(True)

        else:

            entry.set_text('')
            entry.set_sensitive(False)
            button.set_sensitive(False)


    def formats_tab_count_formats(self):

        """Called by several parts of self.setup_formats_tab().

        Counts the number of video/audio formats that are set.

        Return values:

            An integer in the range 0-3

        """

        format_list = self.retrieve_val('video_format_list')

        return len(format_list)


    def formats_tab_redraw_list(self):

        """Called by self.setup_formats_tab() and then again by
        self.apply_changes().

        Update the Gtk.ListStore containing the user's preferred video/audio
        formats.
        """

        # Empty the treeview
        self.formats_liststore.clear()

        # (Need to reverse formats.VIDEO_OPTION_DICT for quick lookup)
        rev_dict = {}
        for key in formats.VIDEO_OPTION_DICT:
            rev_dict[formats.VIDEO_OPTION_DICT[key]] = key

        # Refill the treeview
        format_list = self.retrieve_val('video_format_list')
        for item in format_list:

            if item in rev_dict:
                self.formats_liststore.append([rev_dict[item]])
            else:
                # Non-standard format, not specified by
                #   formats.VIDEO_OPTION_DICT
                self.formats_liststore.append([item])


    # (Tab support functions - Downloads tab)


    def downloads_age_widgets(self, grid, row_count):

        """Called by various parts of the Downloads tabs."""

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Downloads > Limits'
        )

        # Video age options
        self.add_label(grid,
            '<u>' + _('Video age options') + '</u>',
            0, row_count, 1, 1,
        )

        label = self.add_label(grid,
            _('Download videos suitable for this age'),
            0, (row_count + 1), 1, 1,
        )

        entry = self.add_entry(grid,
            'age_limit',
            1, (row_count + 1), 1, 1,
        )
        self.add_tooltip('--age-limit YEARS', label, entry)

        return row_count + 2


    def downloads_date_widgets(self, grid, row_count):

        """Called by various parts of the Downloads tabs."""

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Downloads > Limits'
        )

        grid_width = 3

        # Video date options
        self.add_label(grid,
            '<u>' + _('Video date options') + '</u>',
            0, row_count, grid_width, 1,
        )

        label = self.add_label(grid,
            _('Only videos uploaded on this date'),
            0, (row_count + 1), (grid_width - 2), 1,
        )

        entry = self.add_entry(grid,
            'date',
            (grid_width - 2), (row_count + 1), 1, 1,
        )
        entry.set_editable(False)

        button = Gtk.Button(_('Set'))
        grid.attach(button, (grid_width - 1), (row_count + 1), 1, 1)
        button.connect(
            'clicked',
            self.on_button_set_date_clicked,
            entry,
            'date',
        )
        self.add_tooltip('--date DATE', label, entry, button)

        label2 = self.add_label(grid,
            _('Only videos uploaded before this date'),
            0, (row_count + 2), (grid_width - 2), 1,
        )

        entry2 = self.add_entry(grid,
            'date_before',
            (grid_width - 2), (row_count + 2), 1, 1,
        )
        entry2.set_editable(False)

        button2 = Gtk.Button(_('Set'))
        grid.attach(button2, (grid_width - 1), (row_count + 2), 1, 1)
        button2.connect(
            'clicked',
            self.on_button_set_date_clicked,
            entry2,
            'date_before',
        )
        self.add_tooltip('--datebefore DATE', label2, entry2, button2)

        label3 = self.add_label(grid,
            _('Only videos uploaded after this date'),
            0, (row_count + 3), (grid_width - 2), 1,
        )

        entry3 = self.add_entry(grid,
            'date_after',
            (grid_width - 2), (row_count + 3), 1, 1,
        )
        entry3.set_editable(False)

        button3 = Gtk.Button(_('Set'))
        grid.attach(button3, (grid_width - 1), (row_count + 3), 1, 1)
        button3.connect(
            'clicked',
            self.on_button_set_date_clicked,
            entry3,
            'date_after',
        )
        self.add_tooltip('--dateafter DATE', label3, entry3, button3)

        return row_count + 4


    def downloads_external_widgets(self, grid, row_count):

        """Called by various parts of the Downloads tabs."""

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Downloads > External'
        )

        grid_width = 2

        # External downloader options
        self.add_label(grid,
            '<u>' + _('External downloader options') + '</u>',
            0, row_count, grid_width, 1,
        )

        label = self.add_label(grid,
            _('Use this external downloader'),
            0, (row_count + 1), 1, 1,
        )

        ext_list = [
            '', 'aria2c', 'avconv', 'axel', 'curl', 'ffmpeg', 'httpie',
            'wget',
        ]

        combo = self.add_combo(grid,
            ext_list,
            'external_downloader',
            1, (row_count + 1), 1, 1,
        )
        combo.set_hexpand(True)
        self.add_tooltip('--external-downloader COMMAND', label, combo)

        label2 = self.add_label(grid,
            _('Arguments to pass to external downloader'),
            0, (row_count + 2), grid_width, 1,
        )

        entry = self.add_entry(grid,
            'external_arg_string',
            0, (row_count + 3), grid_width, 1,
        )
        self.add_tooltip('--external-downloader-args ARGS', label2, entry)

        return row_count + 4


    def downloads_filtering_widgets(self, grid, row_count):

        """Called by various parts of the Downloads tabs."""

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Downloads > Filtering'
        )

        grid_width = 3

        # Video filtering options
        self.add_label(grid,
            '<u>' + _('Video filtering options') + '</u>',
            0, row_count, grid_width, 1,
        )

        label = self.add_label(grid,
            _('Download only matching titles (regex or caseless substring)'),
            0, (row_count + 1), grid_width, 1,
        )

        textview, textbuffer = self.add_textview(grid,
            'match_title_list',
            0, (row_count + 2), grid_width, 1,
        )
        self.add_tooltip('--match-title REGEX', label, textview)

        label2 = self.add_label(grid,
            _(
            'Don\'t download only matching titles (regex or caseless' \
            + ' substring)',
            ),
            0, (row_count + 3), grid_width, 1,
        )

        textview2, textbuffer2 = self.add_textview(grid,
            'reject_title_list',
            0, (row_count + 4), grid_width, 1,
        )
        self.add_tooltip('--reject-title REGEX', label2, textview2)

        label3 = self.add_label(grid,
            _('Generic video filter, for example:') + ' like_count > 100',
            0, (row_count + 5), grid_width, 1,
        )

        entry = self.add_entry(grid,
            'match_filter',
            0, (row_count + 6), grid_width, 1,
        )
        self.add_tooltip('--match-filter FILTER', label3, entry)

        return row_count + 7


    def downloads_general_widgets(self, grid, row_count):

        """Called by various parts of the Downloads tabs."""

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Downloads > General'
        )

        grid_width = 3

        checkbutton = self.add_checkbutton(grid,
            _('Prefer HLS (HTTP Live Streaming)'),
            'native_hls',
            0, row_count, grid_width, 1,
        )
        self.add_tooltip('--hls-prefer-native', checkbutton)

        checkbutton2 = self.add_checkbutton(grid,
            _('Prefer FFMpeg over native HLS downloader'),
            'hls_prefer_ffmpeg',
            0, (row_count + 1), grid_width, 1,
        )
        self.add_tooltip('--hls-prefer-ffmpeg', checkbutton2)

        checkbutton3 = self.add_checkbutton(grid,
            _('Include advertisements (experimental feature)'),
            'include_ads',
            0, (row_count + 2), grid_width, 1,
        )
        self.add_tooltip('--include-ads', checkbutton3)

        checkbutton4 = self.add_checkbutton(grid,
            _('Ignore errors and continue the download operation'),
            'ignore_errors',
            0, (row_count + 3), grid_width, 1,
        )
        self.add_tooltip('-i, --ignore-errors', checkbutton4)

        checkbutton5 = self.add_checkbutton(grid,
            _('Abort video download if fragments are unavailable'),
            'abort_on_unavailable_fragment',
            0, (row_count + 4), grid_width, 1,
        )
        self.add_tooltip('--abort-on-unavailable-fragment', checkbutton5)

        label = self.add_label(grid,
            _('Number of retries'),
            0, (row_count + 5), 1, 1,
        )

        spinbutton = self.add_spinbutton(grid,
            1, 99, 1,
            'retries',
            1, (row_count + 5), 1, 1,
        )
        self.add_tooltip('-R, --retries RETRIES', label, spinbutton)

        return row_count + 6


    def downloads_playlist_widgets(self, grid, row_count):

        """Called by various parts of the Downloads tabs."""

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Downloads > Playlists'
        )

        grid_width = 2

        # Playlist options
        self.add_label(grid,
            '<u>' + _('Playlist options') + '</u>',
            0, row_count, grid_width, 1,
        )

        self.add_label(grid,
            '<i>' + _(
                'Channels and playlists are handled in the same way, so' \
                + ' these options can be used with both',
            ) + '</i>',
            0, (row_count + 1), grid_width, 1,
        )

        label = self.add_label(grid,
            _('Start downloading playlist from index'),
            0, (row_count + 2), 1, 1,
           )

        spinbutton = self.add_spinbutton(grid,
            1, None, 1,
            'playlist_start',
            1, (row_count + 2), 1, 1,
        )
        self.add_tooltip('--playlist-start NUMBER', label, spinbutton)

        label2 = self.add_label(grid,
            _('Stop downloading playlist at index'),
            0, (row_count + 3), 1, 1,
        )

        spinbutton2 = self.add_spinbutton(grid,
            0, None, 1,
            'playlist_end',
            1, (row_count + 3), 1, 1,
        )
        self.add_tooltip('--playlist-end NUMBER', label2, spinbutton2)

        label3 = self.add_label(grid,
            _('Download playlist range, in form START:STOP:STEP'),
            0, (row_count + 4), 1, 1,
        )

        entry = self.add_entry(grid,
            'playlist_items',
            1, (row_count + 4), 1, 1,
        )
        entry.set_hexpand(True)
        self.add_tooltip('--playlist-items ITEM_SPEC', label3, entry)

        label4 = self.add_label(grid,
            _('Abort operation after downloading this many videos'),
            0, (row_count + 5), 1, 1,
        )

        spinbutton3 = self.add_spinbutton(grid,
            0, None, 1,
            'max_downloads',
            1, (row_count + 5), 1, 1,
        )
        self.add_tooltip('--max-downloads NUMBER', label4, spinbutton3)

        checkbutton = self.add_checkbutton(grid,
            _('Abort downloading the playlist if an error occurs'),
            'abort_on_error',
            0, (row_count + 6), grid_width, 1,
        )
        self.add_tooltip('--abort-on-error', checkbutton)

        checkbutton2 = self.add_checkbutton(grid,
            _('Download playlist in reverse order'),
            'playlist_reverse',
            0, (row_count + 7), grid_width, 1,
        )
        self.add_tooltip('--playlist-reverse', checkbutton2)

        checkbutton3 = self.add_checkbutton(grid,
            _('Download playlist in random order'),
            'playlist_random',
            0, (row_count + 8), grid_width, 1,
        )
        self.add_tooltip('--playlist-random', checkbutton3)

        return row_count + 8


    def downloads_size_limit_widgets(self, grid, row_count):

        """Called by various parts of the Downloads tabs."""

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Downloads > Limits'
        )

        grid_width = 3

        # Video size limit options
        self.add_label(grid,
            '<u>' + _('Video size limit options') + '</u>',
            0, row_count, grid_width, 1,
        )

        label = self.add_label(grid,
            _('Minimum file size for video downloads'),
            0, (row_count + 1), (grid_width - 2), 1,
        )

        spinbutton = self.add_spinbutton(grid,
            0, None, 1,
            'min_filesize',
               (grid_width - 2), (row_count + 1), 1, 1,
        )

        combo = self.add_combo_with_data(grid,
            formats.FILE_SIZE_UNIT_LIST,
            'min_filesize_unit',
            (grid_width - 1), (row_count + 1), 1, 1,
        )
        self.add_tooltip('--min-filesize SIZE', label, spinbutton, combo)

        label2 = self.add_label(grid,
            _('Maximum file size for video downloads'),
            0, (row_count + 2), (grid_width - 2), 1,
        )

        spinbutton2 = self.add_spinbutton(grid,
            0, None, 1,
            'max_filesize',
            (grid_width - 2), (row_count + 2), 1, 1,
        )

        combo2 = self.add_combo_with_data(grid,
            formats.FILE_SIZE_UNIT_LIST,
            'max_filesize_unit',
            (grid_width - 1), (row_count + 2), 1, 1,
        )
        self.add_tooltip('--max-filesize SIZE', label2, spinbutton2, combo2)

        return row_count + 3


    def downloads_views_widgets(self, grid, row_count):

        """Called by various parts of the Downloads tabs."""

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download options > Downloads > Limits'
        )

        grid_width = 3

        # Video views options
        self.add_label(grid,
            '<u>' + _('Video views options') + '</u>',
            0, row_count, grid_width, 1,
        )

        label = self.add_label(grid,
            _('Minimum number of views'),
            0, (row_count + 1), (grid_width - 2), 1,
        )

        spinbutton = self.add_spinbutton(grid,
            0, None, 1,
            'min_views',
            (grid_width - 2), (row_count + 1), 1, 1,
        )
        self.add_tooltip('--min-views COUNT', label, spinbutton)

        label2 = self.add_label(grid,
            _('Maximum number of views'),
            0, (row_count + 2), (grid_width - 2), 1,
        )

        spinbutton2 = self.add_spinbutton(grid,
            0, None, 1,
            'max_views',
            (grid_width - 2), (row_count + 2), 1, 1,
        )
        self.add_tooltip('--max-views COUNT', label2, spinbutton2)

        # (This improves layout a little)
        if not self.app_obj.simple_options_flag:
            spinbutton.set_hexpand(True)
            spinbutton2.set_hexpand(True)

        return row_count + 3


    # Callback class methods


    def on_button_set_date_clicked(self, button, entry, prop):

        """Called by callback in self.downloads_date_widgets().

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Another widget to be modified by this function

            prop (str): The attribute in self.edit_dict to modify

        """

        # Prompt the user for a new calendar date
        dialogue_win = mainwin.CalendarDialogue(
            self,
            self.retrieve_val(prop),
        )

        response = dialogue_win.run()

        # Retrieve user choices from the dialogue window, before destroying it
        if response == Gtk.ResponseType.OK:
            date_tuple = dialogue_win.calendar.get_date()

        dialogue_win.destroy()

        if response == Gtk.ResponseType.OK and date_tuple:

            year = str(date_tuple[0])           # e.g. 2011
            month = str(date_tuple[1] + 1)      # Values in range 0-11
            day = str(date_tuple[2])            # Values in range 1-31

            entry.set_text(
                year.zfill(4) + month.zfill(2) + day.zfill(2)
            )

        else:

            entry.set_text('')


    def on_clone_options_clicked(self, button):

        """Called by callback in self.setup_name_tab().

        Args:

            button (Gtk.Button): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' Download options > Name'
        )

        self.app_obj.dialogue_manager_obj.show_msg_dialogue(
            _(
            'This procedure cannot be reversed. Are you sure you want to' \
            + ' continue?',
            ),
            'question',
            'yes-no',
            self,           # Parent window is this window
            {
                'yes': 'clone_download_options_from_window',
                'data': [self, self.edit_obj],
            },
        )


    def on_cookies_set_button_clicked(self, button, entry):

        """Called by callback in self.setup_files_cookies_tab().

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Another widget to be modified by this function

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' Download options > Files > Cookies'
        )

        # Prompt the user for a new file
        dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser(
            _('Select the cookie jar file'),
            self,
            'open',
        )

        cookie_path = self.retrieve_val('cookies_path')

        if cookie_path == '':
            cookie_dir = self.app_obj.data_dir
        else:
            cookie_dir, cookie_file = os.path.split(cookie_path)

        dialogue_win.set_current_folder(cookie_dir)

        # Get the user's response
        response = dialogue_win.run()
        if response == Gtk.ResponseType.OK:
            new_path = dialogue_win.get_filename()

        dialogue_win.destroy()
        if response == Gtk.ResponseType.OK:

            self.edit_dict['cookies_path'] = new_path
            entry.set_text(new_path)


    def on_cookies_reset_button_clicked(self, button, entry):

        """Called by callback in self.setup_files_cookies_tab().

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Another widget to be modified by this function

        """

        self.edit_dict['cookies_path'] = ''
        entry.set_text(
            os.path.abspath(
                os.path.join(
                    self.app_obj.data_dir,
                    self.app_obj.cookie_file_name,
                ),
            ),
        )


    def on_cookies_ytdlp_entry_changed(self, widget, entry, combo, \
    combo2, entry2, entry3):

        """Called by callback in self.setup_files_cookies_tab().

        Args:

            widget (Gtk.Entry): The widget modified (ignored)

            entry (Gtk.Entry): The entry box displaying the download option

            combo, combo2, entry2, entry3 (Gtk.Combo, Gtk.Entry): Widgets whose
                settings are combined to set the 'cookies_from_browser'
                download option

        """

        # (The other entry also specifies the profile; at least one of them
        #   must be empty)
        if entry2.get_text() != '':
            entry3.set_text('')

        # Update the download option, and display it in 'entry'
        self.setup_files_cookies_tab_update(
            None,
            entry,
            combo,
            combo2,
            entry2,
            entry3,
        )


    def on_cookies_ytdlp_reset_button_clicked(self, button, entry, combo, \
    combo2, entry2, entry3):

        """Called by callback in self.setup_files_cookies_tab().

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): The entry box displaying the download option

            combo, combo2, entry2, entry3 (Gtk.Combo, Gtk.Entry): Widgets whose
                settings are combined to set the 'cookies_from_browser'
                download option

        """

        entry3.set_text('')

        # Update the download option, and display it in 'entry'
        self.setup_files_cookies_tab_update(
            None,
            entry,
            combo,
            combo2,
            entry2,
            entry3,
        )


    def on_cookies_ytdlp_set_button_clicked(self, button, entry, combo, \
    combo2, entry2, entry3):

        """Called by callback in self.setup_files_cookies_tab().

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): The entry box displaying the download option

            combo, combo2, entry2, entry3 (Gtk.Combo, Gtk.Entry): Widgets whose
                settings are combined to set the 'cookies_from_browser'
                download option

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' Download options > Files > Cookies'
        )

        # Prompt the user for a new file
        dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser(
            _('Select the browser profile'),
            self,
            'open',
        )

        # Get the user's response
        response = dialogue_win.run()
        if response == Gtk.ResponseType.OK:
            new_path = dialogue_win.get_filename()

        dialogue_win.destroy()
        if response == Gtk.ResponseType.OK:
            entry3.set_text(new_path)

            # (The other entry also specifies the profile; at least one of them
            #   must be empty)
            if new_path != '':
                entry2.set_text('')

        # Update the download option, and display it in 'entry'
        self.setup_files_cookies_tab_update(
            None,
            entry,
            combo,
            combo2,
            entry2,
            entry3,
        )


    def on_date_time_combo_changed(self, combo, entry, button, trans_dict):

        """Called by callback in self.setup_files_filesystem_tab().

        The 'Date/time/location' combo sets or resets the entry just beneath
        it, every time the user selects a new combo item.

        Args:

            combo (Gtk.ComboBox): The widget clicked

            entry (Gtk.Entry): Another widget to update

            button (Gtk.Button): Another widget to update

            trans_dict (dict): Converts a translated string into the string
                used by youtube-dl

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        value = trans_dict[model[tree_iter][0]]

        self.file_tab_update_time_format(value, entry, button)


    def on_direct_cmd_toggled(self, checkbutton, checkbutton2):

        """Called by callback in self.setup_name_tab().

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2 (Gtk.CheckButton): Another widget to modify

        """

        if not checkbutton.get_active():
            self.edit_dict['direct_cmd_flag'] = False
            checkbutton2.set_active(False)
            checkbutton2.set_sensitive(False)

        else:

            self.edit_dict['direct_cmd_flag'] = True
            checkbutton2.set_sensitive(True)


    def on_dl_config_button_clicked(self, button, entry, textbuffer, \
    dl_config_path):

        """Called by callback in self.setup_advanced_configuration_tab().

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Another widget to modify

            textbuffer (Gtk.TextBuffer): Buffer for the textview containing
                the text to be saved

            dl_config_path (str): FUll path to the downloader's configuration
                file

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' Download options > Advanced > Configurations'
        )

        # Save the file
        try:

            dir_path = os.path.dirname(dl_config_path)
            if not os.path.isdir(dir_path):
                self.app_obj.make_directory(dir_path)

            fh = open(dl_config_path, 'w')
            fh.write(
                textbuffer.get_text(
                    textbuffer.get_start_iter(),
                    textbuffer.get_end_iter(),
                    # Don't include hidden characters
                    False,
                ),
            )
            fh.close()

        except:

            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('Could not save the downloader\'s configuration file'),
                'error',
                'ok',
                self,           # Parent window is this window
            )

            return

        entry.set_text(dl_config_path)

        self.app_obj.dialogue_manager_obj.show_msg_dialogue(
            _('Downloader\'s configuration file saved'),
            'info',
            'ok',
            self,           # Parent window is this window
        )


    def on_embed_checkbutton_toggled(self, checkbutton):

        """Called by callback in self.setup_post_process_tab() or
        setup_subtitles_more_options_tab().

        The 'embed_subs' option appears in both the Formats and Subtitles tabs.
        When one widget is modified, we need to set the other widgets to match
        without starting an infinite loop of signal connects.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        prop = 'embed_subs'

        if checkbutton == self.embed_checkbutton2 \
        and self.embed_checkbutton is None:

            # An easy case; the Formats tab isn't visible, so there is only one
            #   widget to think about
            if not checkbutton.get_active():
                self.edit_dict[prop] = False
            else:
                self.edit_dict[prop] = True

        else:

            # We get around the infinite loop problem by setting the other
            #   checkbutton, if it's in the opposite state to this checkbutton
            flag = checkbutton.get_active()

            if checkbutton == self.embed_checkbutton:

                if self.embed_checkbutton2.get_active() != flag:
                    self.embed_checkbutton2.set_active(flag)
                elif not checkbutton.get_active():
                    self.edit_dict[prop] = False
                else:
                    self.edit_dict[prop] = True

            else:

                if self.embed_checkbutton.get_active() != flag:
                    self.embed_checkbutton.set_active(flag)
                elif not checkbutton.get_active():
                    self.edit_dict[prop] = False
                else:
                    self.edit_dict[prop] = True


    def on_fixed_folder_changed(self, combo):

        """Called by callback in self.setup_files_filesystem_tab().

        Args:

            combo (Gtk.ComboBox): The widget clicked

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        dbid = model[tree_iter][1]
        if dbid == self.app_obj.fixed_temp_folder.dbid:
            self.edit_dict['use_fixed_folder'] = 'temp'
        elif dbid == self.app_obj.fixed_misc_folder.dbid:
            self.edit_dict['use_fixed_folder'] = 'misc'
        elif dbid == self.app_obj.fixed_clips_folder.dbid:
            self.edit_dict['use_fixed_folder'] = 'clips'
        else:
            # Failsafe
            self.edit_dict['use_fixed_folder'] = None


    def on_fixed_folder_toggled(self, checkbutton, combo):

        """Called by callback in self.setup_files_filesystem_tab().

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            combo (Gtk.ComboBox): Another widget to be modified by this
                function

        """

        if not checkbutton.get_active():
            self.edit_dict['use_fixed_folder'] = None
            combo.set_sensitive(False)

        else:

            tree_iter = combo.get_active_iter()
            model = combo.get_model()
            dbid = model[tree_iter][1]
            if dbid == self.app_obj.fixed_temp_folder.dbid:
                self.edit_dict['use_fixed_folder'] = 'temp'
            elif dbid == self.app_obj.fixed_misc_folder.dbid:
                self.edit_dict['use_fixed_folder'] = 'misc'
            elif dbid == self.app_obj.fixed_clips_folder.dbid:
                self.edit_dict['use_fixed_folder'] = 'clips'
            else:
                # Failsafe
                self.edit_dict['use_fixed_folder'] = None

            combo.set_sensitive(True)


    def on_file_tab_add_button_clicked(self, button, entry, entry2, combo, \
    trans_dict):

        """Called by callback in self.setup_files_names_tab().

        Args:

            button (Gtk.Button): The widget clicked

            entry, entry2 (Gtk.Entry): Other widgets to be modified by this
                function

            combo (Gtk.ComboBox): Another widget to be modified by this
                function

            trans_dict (dict): Converts a translated string into the string
                used by youtube-dl

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        value = trans_dict[model[tree_iter][0]]

        # The new component should be inserted at the end of the filename, and
        #   before the file extension (if possible)
        output_template = file_name = self.retrieve_val('output_template')
        file_ext = ''
        if value != 'ext' and output_template:

            match = re.search(r'(.*)(\.\%\(ext\)s)\s*$', output_template)
            if match:

                file_name = match.groups()[0]
                file_ext = match.groups()[1]

        if not output_template or output_template[-1] == os.sep:
            prefix = ''
        elif value == 'ext':
            prefix = '.'
        else:
            prefix = '-'

        # e.g. to generate '(upload_date>%Y-%m-%d)s'
        time_format = entry2.get_text()
        if time_format != '':

            if value == 'release_date_custom':
                value = 'release_date>' + time_format
            elif value == 'upload_date_custom':
                value = 'upload_date>' + time_format
            elif value == 'timestamp_custom':
                value = 'timestamp>' + time_format
            elif value == 'duration_custom':
                value = 'duration>' + time_format

        if value == 'video_autonumber':
            formatted = '{0}%({1})3d'.format(prefix, value)
        else:
            formatted = '{0}%({1})s'.format(prefix, value)

        # (Setting the entry updates self.edit_dict)
        if value == 'ext':
            entry.set_text(file_name + file_ext + formatted)
        else:
            entry.set_text(file_name + formatted + file_ext)


    def on_file_tab_reset_button_clicked(self, button, entry, combo, \
    trans_dict):

        """Called by callback in self.setup_files_names_tab().

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Another widget to be modified by this function

            combo (Gtk.ComboBox): Another widget to be modified by this
                function

            trans_dict (dict): Converts a translated string into the string
                used by youtube-dl

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        value = trans_dict[model[tree_iter][0]]

        self.file_tab_update_time_format(value, entry, button)


    def on_file_tab_main_combo_changed(self, combo, entry, entry2, button):

        """Called by callback in self.setup_files_names_tab().

        Args:

            combo (Gtk.ComboBox): The widget clicked

            entry, entry2 (Gtk.Entry): Other widgets to be modified

            button (Gtk.Button): Another widget to be modified

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        row_id, name = model[tree_iter][:2]

        self.edit_dict['output_format'] = row_id

        # The custom template is associated with the index 0
        if row_id == 0:

            self.file_tab_sensitise_widgets(True)
            # (The call to that function sensitises the 'Time format' widgets,
            #   but they must be desensitised when a custom format is not
            #   selected)
            entry2.set_sensitive(False)
            button.set_sensitive(False)

            entry.set_text(self.retrieve_val('output_template'))

        else:

            self.file_tab_sensitise_widgets(False)
            entry.set_text(formats.FILE_OUTPUT_CONVERT_DICT[row_id])


    def on_file_tab_main_entry_changed(self, entry):

        """Called by callback in self.setup_files_names_tab().

        Args:

            entry (Gtk.Entry): The widget clicked

        """

        # Only set 'output_template' when option 3 is selected, which is when
        #   the entry is sensitised
        if self.template_flag:
            self.edit_dict['output_template'] = entry.get_text()


    def on_formats_tab_add_clicked(self, add_button, remove_button, \
    up_button, down_button, treeview):

        """Called by callback in self.setup_formats_tab_add_grid().

        Args:

            add_button (Gtk.Button): The widget clicked

            remove_button, up_button, down_button (Gtk.Button): Other widgets
                to be modified by this function

            treeview (Gtk.TreeView): The treeview on the left side of the tab

        """

        selection = treeview.get_selection()
        (model, tree_iter) = selection.get_selected()
        if tree_iter is None:

            # Nothing selected
            return

        else:

            name = model[tree_iter][0]
            # Convert string e.g. 'mp4 [360p]' to the extractor code e.g. '18'
            extract_code = formats.VIDEO_OPTION_DICT[name]

        # Update the option
        format_list = self.retrieve_val('video_format_list')
        if extract_code in format_list:
            return
        else:
            format_list.append(extract_code)
            self.edit_dict['video_format_list'] = format_list

        # Update the other treeview, adding the format to it (and don't modify
        #   this treeview)
        self.formats_liststore.append([name])

        # Update other widgets, as required
        remove_button.set_sensitive(True)
        up_button.set_sensitive(True)
        down_button.set_sensitive(True)


    def on_formats_tab_combo_changed(self, combo):

        """Called by callback in self.setup_formats_tab_add_grid().

        Args:

            combo (Gtk.ComboBox): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' Download options > Formats > Preferred'
        )

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        val = model[tree_iter][0]

        self.edit_dict['merge_output_format'] = val

        # For some reason, this youtube-dl download option doesn't work if the
        #   specified format (e.g. 'mp4') isn't also specified in the list of
        #   preferred formats
        # Warn the user about that, where appropriate
        format_list = self.retrieve_val('video_format_list')
        if val != '' and not val in format_list:

            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _(
                'This option won\'t work unless the format is also added to' \
                + ' the list of preferred formats above',
                ),
                'warning',
                'ok',
                self,           # Parent window is this window
            )


    def on_formats_tab_down_clicked(self, down_button, treeview):

        """Called by callback in self.setup_formats_tab_add_grid().

        Args:

            down_button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeView): Another widget to be modified by this
                function

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if not path_list:

            # Nothing selected
            return

        else:

            this_iter = model.get_iter(path_list[0])
            name = model[this_iter][0]
            # Convert string e.g. 'mp4 [360p]' to the extractor code e.g. '18',
            #   unless it's a non-standard extractor code
            if name in formats.VIDEO_OPTION_DICT:
                extract_code = formats.VIDEO_OPTION_DICT[name]
            else:
                extract_code = name

        # Update the option
        format_list = self.retrieve_val('video_format_list')
        if extract_code in format_list:

            index = format_list.index(extract_code)
            if index < (len(format_list) - 1):
                format_list.remove(extract_code)
                format_list.insert((index + 1), extract_code)

                self.edit_dict['video_format_list'] = format_list

                # Update the other treeview
                this_path = path_list[0]
                next_path = this_path[0]+1
                model.move_after(
                    model.get_iter(this_path),
                    model.get_iter(next_path),
                )


    def on_formats_tab_remove_clicked(self, remove_button, add_button, \
    type_button, up_button, down_button, other_treeview):

        """Called by callback in self.setup_formats_tab_add_grid().

        Args:

            remove_button (Gtk.Button): The widget clicked

            add_button, type_button, up_button, down_button (Gtk.Button): Other
                widgets to be modified by this function

            other_treeview (Gtk.TreeView): The treeview on the right side of
                the tab

        """

        selection = other_treeview.get_selection()
        (model, tree_iter) = selection.get_selected()
        if tree_iter is None:

            # Nothing selected
            return

        else:

            name = model[tree_iter][0]
            # Convert string e.g. 'mp4 [360p]' to the extractor code e.g. '18',
            #   unless it's a non-standard extractor code
            if name in formats.VIDEO_OPTION_DICT:
                extract_code = formats.VIDEO_OPTION_DICT[name]
            else:
                extract_code = name

        # Update the option
        format_list = self.retrieve_val('video_format_list')
        if extract_code in format_list:
            format_list.remove(extract_code)

            self.edit_dict['video_format_list'] = format_list

            # Update the right-hand side treeview
            model.remove(tree_iter)

            # Update other widgets, as required
            add_button.set_sensitive(True)
            type_button.set_sensitive(True)
            if not format_list:

                # No formats left to remove
                remove_button.set_sensitive(False)
                up_button.set_sensitive(False)
                down_button.set_sensitive(False)


    def on_formats_tab_type_clicked(self, type_button, remove_button, \
    up_button, down_button, treeview):

        """Called by callback in self.setup_formats_tab_add_grid().

        Args:

            type_button (Gtk.Button): The widget clicked

            remove_button, up_button, down_button (Gtk.Button): Other widgets
                to be modified by this function

            treeview (Gtk.TreeView): The treeview on the left side of the tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' Download options > Formats > Preferred > Type extractor code' \
            + ' directly',
        )

        # Prompt the user to type an extractor code directly
        dialogue_win = mainwin.ExtractorCodeDialogue(self)
        response = dialogue_win.run()

        # Retrieve user choices from the dialogue window, before destroying it
        if response == Gtk.ResponseType.OK:
            extract_code = dialogue_win.extract_code

        dialogue_win.destroy()

        if response == Gtk.ResponseType.OK and extract_code is not None:

            # Update the option (code copied from
            #   self.on_formats_tab_add_clicked() above)
            format_list = self.retrieve_val('video_format_list')
            if extract_code in format_list:
                return
            else:
                format_list.append(extract_code)
                self.edit_dict['video_format_list'] = format_list

            # Update the other treeview, adding the specified format to it (but
            #   don't modify this treeview)
            add_flag = False
            for name in formats.VIDEO_OPTION_DICT.keys():
                if formats.VIDEO_OPTION_DICT[name] == extract_code:
                    self.formats_liststore.append([name])
                    add_flag = True
                    break

            if not add_flag:
                # Non-standard format, with no entry in
                #   formats.VIDEO_OPTION_DICT
                self.formats_liststore.append([extract_code])

            # Update other widgets, as required
            remove_button.set_sensitive(True)
            up_button.set_sensitive(True)
            down_button.set_sensitive(True)


    def on_formats_tab_up_clicked(self, up_button, treeview):

        """Called by callback in self.setup_formats_tab_add_grid().

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeView): Another widget to be modified by this
                function

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if not path_list:

            # Nothing selected
            return

        else:

            this_iter = model.get_iter(path_list[0])
            name = model[this_iter][0]
            # Convert string e.g. 'mp4 [360p]' to the extractor code e.g. '18',
            #   unless it's a non-standard extractor code
            if name in formats.VIDEO_OPTION_DICT:
                extract_code = formats.VIDEO_OPTION_DICT[name]
            else:
                extract_code = name

        # Update the option
        format_list = self.retrieve_val('video_format_list')
        if extract_code in format_list:

            index = format_list.index(extract_code)
            if index > 0:
                format_list.remove(extract_code)
                format_list.insert((index - 1), extract_code)

                self.edit_dict['video_format_list'] = format_list

                # Update the other treeview
                this_path = path_list[0]
                prev_path = this_path[0]-1
                model.move_before(
                    model.get_iter(this_path),
                    model.get_iter(prev_path),
                )


    def on_netrc_button_clicked(self, button, entry, textbuffer, netrc_path):

        """Called by callback in self.setup_advanced_netrc_tab().

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Another widget to modify

            textbuffer (Gtk.TextBuffer): Buffer for the textview containing
                the text to be saved

            netrc_path (str): FUll path to the .netrc file

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' Download options > Advanced > .netrc'
        )

        if not os.path.isfile(netrc_path):
            new_flag = True
        else:
            new_flag = False

        # Save the file
        try:
            fh = open(netrc_path, 'w')
            fh.write(
                textbuffer.get_text(
                    textbuffer.get_start_iter(),
                    textbuffer.get_end_iter(),
                    # Don't include hidden characters
                    False,
                ),
            )
            fh.close()

        except:

            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('Could not save the .netrc file'),
                'error',
                'ok',
                self,           # Parent window is this window
            )

            return

        if new_flag:

            # For a newly-created file, set read/write permissions, as
            #   described in the youtube-dl documentation
            os.chmod(netrc_path, stat.S_IREAD | stat.S_IWRITE)

        entry.set_text(netrc_path)

        self.app_obj.dialogue_manager_obj.show_msg_dialogue(
            _('.netrc file saved'),
            'info',
            'ok',
            self,           # Parent window is this window
        )


    def on_obsolete_format_checkbutton_toggled(self, checkbutton, liststore):

        """Called by callback in self.setup_formats_tab_add_grid().

        Toggles the display of obsolete YouTube formats in the tab's list.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            liststore (Gtk.ListStore): The widget to update

        """

        self.app_obj.set_hide_obsolete_formats_flag(checkbutton.get_active())
        self.setup_formats_tab_update_list(liststore)

        # Also update any other OptionsEditWin windows that are currently open
        for config_win_obj in self.app_obj.main_win_obj.config_win_list:

            if isinstance(config_win_obj, OptionsEditWin) \
            and config_win_obj.edit_obj != self.edit_obj \
            and config_win_obj.formats_checkbutton.get_active \
            != self.app_obj.hide_obsolete_formats_flag:
                config_win_obj.formats_checkbutton.set_active(
                    self.app_obj.hide_obsolete_formats_flag,
                )


    def on_reset_options_clicked(self, button):

        """Called by callback in self.setup_name_tab().

        Args:

            button (Gtk.Button): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' Download options > Name'
        )

        self.app_obj.dialogue_manager_obj.show_msg_dialogue(
            _(
            'This procedure cannot be reversed. Are you sure you want to' \
            + ' continue?',
            ),
            'question',
            'yes-no',
            self,           # Parent window is this window
            {
                'yes': 'reset_download_options',
                # (Reset this edit window, if the user clicks 'yes')
                'data': [self],
            },
        )


    def on_simple_options_clicked(self, button):

        """Called by callback in self.setup_general_tab().

        Args:

            button (Gtk.Button): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' Download options > Name'
        )

        if not self.app_obj.simple_options_flag:

            self.app_obj.set_simple_options_flag(True)

            if not self.edit_dict:
                # User has not changed any options, so redraw the window to
                #   show the same options.OptionsManager object
                self.reset_with_new_edit_obj(self.edit_obj)

            else:
                # User has already changed some options. We don't want to lose
                #   them, so wait for the window to close and be re-opened,
                #   before switching between simple/advanced options
                self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                    _(
                    'Fewer download options will be visible when you click' \
                    + ' the \'Apply\' or \'Reset\' buttons (or when you' \
                    + ' close and then re-open the window)',
                    ),
                    'info',
                    'ok',
                    self,           # Parent window is this window
                )

                button.set_label(
                    _('Show advanced download options (when window re-opens)'),
                )

        else:

            self.app_obj.set_simple_options_flag(False)

            if not self.edit_dict:
                self.reset_with_new_edit_obj(self.edit_obj)

            else:
                self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                    _(
                    'More download options will be visible when you click' \
                    + ' the \'Apply\' or \'Reset\' buttons (or when you' \
                    + ' close and then re-open the window)',
                    ),
                    'info',
                    'ok',
                    self,           # Parent window is this window
                )

                button.set_label(
                    _('Hide advanced download options (when window re-opens)'),
                )


    def on_sleep_button_changed(self, spinbutton, spinbutton2):

        """Called by callback in self.setup_advanced_workaround_tab().

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

            spinbutton2 (Gtk.SpinButton2): Another widget to update

        """

        value = int(spinbutton.get_value())

        self.edit_dict['min_sleep_interval'] = value
        if value == 0:
            spinbutton2.set_value(0)
            spinbutton2.set_sensitive(False)
        else:
            spinbutton2.set_sensitive(True)


    def on_subtitles_tab_add_clicked(self, button, treeview, other_liststore,
    rev_dict):

        """Called by callback in self.setup_subtitles_options_tab().

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeView): The treeview on the left side of the tab

            other_liststore (Gtk.ListStore): The liststore belonging to the
                treeview on the right side of the tab

            rev_dict (dict): A reversed formats.LANGUAGE_CODE_DICT

        """

        selection = treeview.get_selection()
        (model, tree_iter) = selection.get_selected()
        if tree_iter is None:

            # Nothing selected
            return

        name = model[tree_iter][0]
        # From a string in the form 'English [en]', remove the language code to
        #    get a key in formats.LANGUAGE_CODE_DICT, e.g. 'English'
        match = re.search(r'^(.*)\s\[', name)
        if match:

            lang_name = match.groups()[0]
            if lang_name in formats.LANGUAGE_CODE_DICT:

                lang_code = formats.LANGUAGE_CODE_DICT[lang_name]

                # Retrieve the existing list of languages
                lang_code_list = self.retrieve_val('subs_lang_list')
                if not lang_code in lang_code_list:

                    lang_code_list.append(lang_code)

                # Sort by language name, not by language code
                lang_list = []
                mod_code_list = []
                for this_code in lang_code_list:
                    lang_list.append(rev_dict[this_code])

                lang_list.sort()
                for this_lang in lang_list:
                    mod_code_list.append(formats.LANGUAGE_CODE_DICT[this_lang])

                # Update the option...
                self.edit_dict['subs_lang_list'] = mod_code_list
                # ...and the treeview
                other_liststore.clear()
                for this_lang in lang_list:
                    other_liststore.append([
                        this_lang + ' [' \
                        + formats.LANGUAGE_CODE_DICT[this_lang] + ']',
                    ])


    def on_subtitles_tab_remove_clicked(self, button, other_treeview,
    other_liststore, rev_dict):

        """Called by callback in self.setup_subtitles_options_tab().

        Args:

            button (Gtk.Button): The widget clicked

            other_treeview (Gtk.TreeView): The treeview on the right side of
                the tab

            other_liststore (Gtk.ListStore): The liststore belonging to that
                treeview

            rev_dict (dict): A reversed formats.LANGUAGE_CODE_DICT

        """

        selection = other_treeview.get_selection()
        (model, tree_iter) = selection.get_selected()
        if tree_iter is None:

            # Nothing selected
            return

        name = model[tree_iter][0]
        # From a string in the form 'English [en]', remove the language name to
        #   get a value in formats.LANGUAGE_CODE_DICT, e.g. 'en'
        match = re.search(r'^.*\s\[(.*)\]', name)
        if match:

            lang_code = match.groups()[0]

            # Retrieve the existing list of languages
            lang_code_list = self.retrieve_val('subs_lang_list')
            if lang_code in lang_code_list:

                lang_code_list.remove(lang_code)

            # Sort by language name, not by language code
            lang_list = []
            mod_code_list = []
            for this_code in lang_code_list:
                lang_list.append(rev_dict[this_code])

            lang_list.sort()
            for this_lang in lang_list:
                mod_code_list.append(formats.LANGUAGE_CODE_DICT[this_lang])

            # Update the option...
            self.edit_dict['subs_lang_list'] = mod_code_list
            # ...and the treeview
            other_liststore.clear()
            for this_lang in lang_list:
                other_liststore.append([
                    this_lang + ' [' \
                    + formats.LANGUAGE_CODE_DICT[this_lang] + ']',
                ])


    def on_subtitles_toggled(self, radiobutton, button, button2, prop):

        """Called by callback in self.setup_subtitles_options_tab().

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

            button, button2 (Gtk.Button): Other widgets to be modified by this
                function

            prop (str): The attribute in self.edit_dict to modify

        """

        if radiobutton.get_active():

            if prop == 'write_subs':
                self.edit_dict['write_subs'] = False
                self.edit_dict['write_auto_subs'] = False
                self.edit_dict['write_all_subs'] = False
                button.set_sensitive(False)
                button2.set_sensitive(False)

            elif prop == 'write_auto_subs':
                self.edit_dict['write_subs'] = True
                self.edit_dict['write_auto_subs'] = True
                self.edit_dict['write_all_subs'] = False
                button.set_sensitive(False)
                button2.set_sensitive(False)

            elif prop == 'write_all_subs':
                self.edit_dict['write_subs'] = True
                self.edit_dict['write_auto_subs'] = False
                self.edit_dict['write_all_subs'] = True
                button.set_sensitive(False)
                button2.set_sensitive(False)

            elif prop == 'subs_lang':
                self.edit_dict['write_subs'] = True
                self.edit_dict['write_auto_subs'] = False
                self.edit_dict['write_all_subs'] = False
                button.set_sensitive(True)
                button2.set_sensitive(True)


    def on_video_format_mode_toggled(self, radiobutton, value):

        """Called by callback in self.setup_formats_advanced_tab().

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

            prop (str): The attribute in self.edit_dict to modify

        """

        if radiobutton.get_active():
            self.edit_dict['video_format_mode'] = value


    def on_ytdlp_output_add_button_clicked(self, button, liststore, combo, \
    entry):

        """Called from callback in self.setup_files_override_tab().

        Adds a template to the 'output_format_list' option.

        Args:

            button (Gtk.Button): The widget clicked

            liststore (Gtk.ListStore): The treeview's model

            combo (Gtk.ComboBox): Widget providing the output type

            entry (Gtk.Entry): Widget providiing the output template

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        output_type = model[tree_iter][0]

        output_template = entry.get_text()
        if output_template == '':
            return

        # Items in the list are in the form TYPES:TEMPLATE
        # However, every item should have a uniquE TYPES component. If an item
        #   with a TYPES component matching 'output_type' already exists,
        #   overwrite it. Otherwise, add a new item to the end of the list
        output_format_list = self.retrieve_val('output_format_list')
        mod_list = []
        match_flag = False

        for item in output_format_list:

            match = re.search(r'^([^\:]+)\:', item)
            if match:
                this_output_type = match.groups()[0]

                if this_output_type == output_type:
                    mod_list.append(output_type + ':' + output_template)
                    match_flag = True

                else:
                    mod_list.append(item)

        if not match_flag:
            mod_list.append(output_type + ':' + output_template)

        # Update the option
        self.edit_dict['output_format_list'] = mod_list
        # Update the treeview
        self.setup_files_override_tab_update_treeview(liststore)


    def on_ytdlp_output_delete_button_clicked(self, button, treeview):

        """Called from a callback in self.setup_files_override_tab().

        Deletes the selected template to the 'output_format_list' option.

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeVies): The treeview displaying the template list

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if not path_list:
            return

        # (Multiple selection is not enabled)
        this_iter = model.get_iter(path_list[0])
        if this_iter is None:
            return

        # Items in the list are in the form TYPES:TEMPLATE
        output_type = model[this_iter][0]
        output_template = model[this_iter][1]
        item = output_type + ':' + output_template
        # Walk the list, and delete the first matching group
        output_format_list = self.retrieve_val('output_format_list')
        mod_list = []
        match_flag = False

        for this_item in output_format_list:

            if not match_flag and this_item == item:
                match_flag = True   # Delete this one
            else:
                mod_list.append(this_item)

        # Update the option
        self.edit_dict['output_format_list'] = mod_list
        # Update the treeview
        self.setup_files_override_tab_update_treeview(treeview.get_model())


    def on_ytdlp_output_refresnh_button_clicked(self, button, treeview):

        """Called from a callback in self.setup_files_override_tab().

        Refreshes the treeview.

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeVies): The treeview displaying the template list

        """

        # Update the treeview
        self.setup_files_override_tab_update_treeview(treeview.get_model())


    def on_ytdlp_paths_add_button_clicked(self, button, liststore, combo, \
    entry):

        """Called from callback in self.setup_files_paths_tab().

        Adds a path to the 'output_path_list' option.

        Args:

            button (Gtk.Button): The widget clicked

            liststore (Gtk.ListStore): The treeview's model

            combo (Gtk.ComboBox): Widget providing the output type

            entry (Gtk.Entry): Widget providiing the output path

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        output_type = model[tree_iter][0]

        output_path = entry.get_text()
        if output_path == '':
            return

        # Items in the list are in the form TYPES:PATH
        # However, every item should have a uniquE TYPES component. If an item
        #   with a TYPES component matching 'output_type' already exists,
        #   overwrite it. Otherwise, add a new item to the end of the list
        output_path_list = self.retrieve_val('output_path_list')
        mod_list = []
        match_flag = False

        for item in output_path_list:

            match = re.search(r'^([^\:]+)\:', item)
            if match:
                this_output_type = match.groups()[0]

                if this_output_type == output_type:
                    mod_list.append(output_type + ':' + output_path)
                    match_flag = True

                else:
                    mod_list.append(item)

        if not match_flag:
            mod_list.append(output_type + ':' + output_path)

        # Update the option
        self.edit_dict['output_path_list'] = mod_list
        # Update the treeview
        self.setup_files_paths_tab_update_treeview(liststore)


    def on_ytdlp_paths_delete_button_clicked(self, button, treeview):

        """Called from a callback in self.setup_files_paths_tab().

        Deletes the selected path to the 'output_path_list' option.

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeVies): The treeview displaying the path list

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if not path_list:
            return

        # (Multiple selection is not enabled)
        this_iter = model.get_iter(path_list[0])
        if this_iter is None:
            return

        # Items in the list are in the form TYPES:PATH
        output_type = model[this_iter][0]
        output_path = model[this_iter][1]
        item = output_type + ':' + output_path
        # Walk the list, and delete the first matching group
        output_path_list = self.retrieve_val('output_path_list')
        mod_list = []
        match_flag = False

        for this_item in output_path_list:

            if not match_flag and this_item == item:
                match_flag = True   # Delete this one
            else:
                mod_list.append(this_item)

        # Update the option
        self.edit_dict['output_path_list'] = mod_list
        # Update the treeview
        self.setup_files_paths_tab_update_treeview(treeview.get_model())


    def on_ytdlp_paths_refresh_button_clicked(self, button, treeview):

        """Called from a callback in self.setup_files_paths_tab().

        Refreshes the treeview.

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeVies): The treeview displaying the path list

        """

        # Update the treeview
        self.setup_files_paths_tab_update_treeview(treeview.get_model())


    def on_ytdlp_paths_set_button_clicked(self, button, entry):

        """Called from a callback in self.setup_files_paths_tab().

        Opens a file chooser dialogue to set the contents of the entry box.

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Another widget to update

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' Download options > Files > Paths'
        )

        # Prompt the user for a new file
        if os.name == 'nt':
            msg = _('Select the output folder')
        else:
            msg = _('Select the output directory')

        dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser(
            msg,
            self,
            'folder',
        )

        current_dir = entry.get_text()
        if current_dir:
            dialogue_win.set_current_folder(current_dir)

        response = dialogue_win.run()
        output_dir = dialogue_win.get_filename()
        dialogue_win.destroy()

        if response == Gtk.ResponseType.OK and output_dir != '':

            entry.set_text(output_dir)


class FFmpegOptionsEditWin(GenericEditWin):

    """Python class for an 'edit window' to modify values in an
    ffmpeg_tartube.FFmpegOptionsManager object.

    Adapted from FFmpeg Command Line Wizard, by AndreKR
        (https://github.com/AndreKR/ffmpeg-command-line-wizard).

    Args:

        app_obj (mainapp.TartubeApp): The main application object

        edit_obj (ffmpeg_tartube.FFmpegOptionsManager): The object whose
            attributes will be edited in this window

        video_list (list): An optional list of media.Video objects. If not
            empty, when the edit window closes, a process operation will
            start, using the FFmpeg options specified by 'edit_obj' to process
            all the videos in the list. If empty, no operation is started; the
            modified FFmpeg options are just updated as normal

    """


    # Standard class methods


    def __init__(self, app_obj, edit_obj, video_list=[]):

        ignore_me = _(
            'TRANSLATOR\'S NOTE: FFmpeg options window starts here.' \
            + ' In the main window, in the Videos tab, right-click a video' \
            + ' and select Special > Process with FFmpeg...'
        )

        Gtk.Window.__init__(self, title=_('FFmpeg options'))

        if self.is_duplicate(app_obj, edit_obj):
            return

        # IV list - class objects
        # -----------------------
        # The mainapp.TartubeApp object
        self.app_obj = app_obj
        # The ffmpeg_tartube.FFmpegOptionsManager object being edited
        self.edit_obj = edit_obj
        # An optional list of media.Video objects. If not empty, when the edit
        #   window closes, a process operation will start, using the FFmpeg
        #   options specified by 'edit_obj' to process all the videos in the
        #   list. If empty, no operation is started; the modified FFmpeg
        #   options are just updated as normal
        self.video_list = video_list


        # IV list - Gtk widgets
        # ---------------------
        self.grid = None                        # Gtk.Grid
        self.notebook = None                    # Gtk.Notebook
        self.reset_button = None                # Gtk.Button
        self.apply_button = None                # Gtk.Button
        self.ok_button = None                   # Gtk.Button
        self.cancel_button = None               # Gtk.Button
        # (Because of the need to (de)sensitise widgets so often, more of them
        #   than usual have their own IVs)
        # (Name tab)
        self.extra_cmd_string_textview = None   # Gtk.TextView
        self.extra_cmd_string_textbuffer = None # Gtk.TextBuffer
        self.result_textview = None             # Gtk.TextView
        self.results_textbuffer = None          # Gtk.TextBuffer
        # (File tab)
        self.add_end_filename_entry = None      # Gtk.Entry
        self.regex_match_filename_entry = None  # Gtk.Entry
        self.regex_apply_subst_entry = None     # Gtk.Entry
        self.rename_both_flag_checkbutton = None
                                                # Gtk.CheckButton
        self.change_file_ext_entry = None       # Gtk.Entry
        self.delete_original_flag_checkbutton = None
                                                # Gtk.CheckButton
        # (Settings tab)
        self.input_mode_radiobutton = None      # Gtk.RadioButton
        self.input_mode_radiobutton2 = None     # Gtk.RadioButton
        self.audio_flag_checkbutton = None      # Gtk.CheckButton
        self.output_mode_radiobutton = None     # Gtk.RadioButton
        self.output_mode_radiobutton2 = None    # Gtk.RadioButton
        self.output_mode_radiobutton3 = None    # Gtk.RadioButton
        self.output_mode_radiobutton4 = None    # Gtk.RadioButton
        self.output_mode_radiobutton5 = None    # Gtk.RadioButton
        self.output_mode_radiobutton6 = None    # Gtk.RadioButton
        self.h264_grid = None                   # Gtk.Grid
        self.gif_grid = None                    # Gtk.Grid
        self.clip_grid = None                   # Gtk.Grid
        self.slice_grid = None                  # Gtk.Grid
        self.merge_grid = None                  # Gtk.Grid
        self.thumb_grid = None                  # Gtk.Grid
        # (Settings tab, H.264 grid)
        self.audio_bitrate_spinbutton = None    # Gtk.SpinButton
        self.quality_mode_radiobutton = None    # Gtk.RadioButton
        self.quality_mode_radiobutton2 = None   # Gtk.RadioButton
        self.rate_factor_scale = None           # Gtk.Scale
        self.dummy_file_combo = None            # Gtk.ComboBox
        self.patience_preset_combo = None       # Gtk.ComboBox
        self.gpu_encoding_combo = None          # Gtk.ComboBox
        self.hw_accel_combo = None              # Gtk.ComboBox
        # (Settings tab, GIF grid)
        self.palette_mode_radiobutton = None    # Gtk.RadioButton
        self.palette_mode_radiobutton2 = None   # Gtk.RadioButton
        # (Settings tab, split grid)
        self.split_mode_radiobutton = None      # Gtk.RadioButton
        self.split_mode_radiobutton2 = None     # Gtk.RadioButton
        self.split_mode_liststore = None        # Gtk.ListStore
        self.start_stamp_entry = None           # Gtk.Entry
        self.stop_stamp_entry = None            # Gtk.Entry
        self.clip_title_entry = None            # Gtk.Entry
        self.add_timestamp_button = None        # Gtk.Button
        self.delete_timestamp_button = None     # Gtk.Button
        self.show_prefs_button = None           # Gtk.Button
        self.clear_timestamp_button = None      # Gtk.Button
        # (Settings tab, slice grid)
        self.slice_mode_radiobutton = None      # Gtk.RadioButton
        self.slice_mode_radiobutton2 = None     # Gtk.RadioButton
        self.slice_mode_liststore = None        # Gtk.ListStore
        self.category_combo = None              # Gtk.ComboBox
        self.action_combo = None                # Gtk.ComboBox
        self.slice_start_entry = None           # Gtk.Entry
        self.slice_stop_entry = None            # Gtk.Entry
        self.add_slice_button = None            # Gtk.Button
        self.delete_slice_button = None         # Gtk.Button
        self.show_settings_button = None        # Gtk.Button
        self.clear_slice_button = None          # Gtk.Button
        # (Optimise tab)
        self.seek_flag_checkbutton = None       # Gtk.CheckButton
        self.tuning_film_flag_checkbutton = None
                                                # Gtk.CheckButton
        self.tuning_animation_flag_checkbutton = None
                                                # Gtk.CheckButton
        self.tuning_grain_flag_checkbutton = None
                                                # Gtk.CheckButton
        self.tuning_still_image_flag_checkbutton = None
                                                # Gtk.CheckButton
        self.tuning_fast_decode_flag_checkbutton = None
                                                # Gtk.CheckButton
        self.profile_flag_checkbutton = None    # Gtk.CheckButton
        self.fast_start_flag_checkbutton = None # Gtk.CheckButton
        self.tuning_zero_latency_flag_checkbutton = None
                                                # Gtk.CheckButton
        self.limit_flag_checkbutton = None      # Gtk.CheckButton
        self.limit_mbps_spinbutton = None       # Gtk.SpinButton
        self.limit_buffer_spinbutton = None     # Gtk.SpinButton
        # (Clips tab)
        self.simple_split_mode_checkbutton = None
                                                # Gtk.CheckButton
        self.simple_split_mode_radiobutton = None
                                                # Gtk.RadioButton
        self.simple_split_mode_radiobutton2 = None
                                                # Gtk.RadioButton
        self.simple_split_mode_liststore = None # Gtk.ListStore
        self.simple_start_stamp_entry = None
                                                # Gtk.Entry
        self.simple_stop_stamp_entry = None     # Gtk.Entry
        self.simple_clip_title_entry = None     # Gtk.Entry
        self.simple_add_timestamp_button = None # Gtk.Button
        self.simple_delete_timestamp_button = None
                                                # Gtk.Button
        self.simple_show_prefs_button = None    # Gtk.Button
        self.simple_clear_timestamp_button = None
                                                # Gtk.Button
        # (Slices tab)
        self.simple_slice_mode_checkbutton = None
                                                # Gtk.CheckButton
        self.simple_slice_mode_radiobutton = None
                                                # Gtk.RadioButton
        self.simple_slice_mode_radiobutton2 = None
                                                # Gtk.RadioButton
        self.simple_slice_mode_liststore = None # Gtk.ListStore
        self.simple_category_combo = None       # Gtk.ComboBox
        self.simple_action_combo = None         # Gtk.ComboBox
        self.simple_slice_start_entry = None    # Gtk.Entry
        self.simple_slice_stop_entry = None     # Gtk.Entry
        self.simple_show_settings_button = None # Gtk.Button
        # (Videox tab)
        self.video_liststore = None             # Gtk.ListStore

        # IV list - other
        # ---------------
        # Size (in pixels) of gaps between edit window widgets
        self.spacing_size = self.app_obj.default_spacing_size
        # Flag set to True if all four buttons ('Reset', 'Apply', 'Cancel' and
        #   'OK') are required, or False if just the 'OK' button is required
        self.multi_button_flag = True

        # When the user changes a value, it is not applied to self.edit_obj
        #   immediately; instead, it is stored temporarily in this dictionary
        # If the user clicks the 'OK' or 'Apply' buttons at the bottom of the
        #   window, the changes are applied to self.edit_obj
        # If the user clicks the 'Reset' or 'Cancel' buttons, the dictionary
        #   is emptied and the changes are lost
        # In this edit window, the key-value pairs directly correspond to those
        #   in ffmpeg_tartube.FFmpegOptionsManager.options_dict, rather than
        #   corresponding directly to attributes in the
        #   ffmpeg_tartube.FFmpegOptionsManager object
        # Because of that, we use our own .apply_changes() and .retrieve_val()
        #   functions, rather than relying on the generic functions
        # Key-value pairs are added to this dictionary whenever the user
        #   makes a change (so if no changes are made when the window is
        #   closed, the dictionary will still be empty)
        self.edit_dict = {}


        # Code
        # ----

        # Set up the edit window
        self.setup()


    # Public class methods


#   def is_duplicate():         # Inherited from GenericConfigWin


#   def setup():                # Inherited from GenericConfigWin


#   def setup_grid():           # Inherited from GenericConfigWin


#   def setup_notebook():       # Inherited from GenericConfigWin


#   def add_notebook_tab():     # Inherited from GenericConfigWin


#   def setup_button_strip():   # Inherited from GenericEditWin


#   def setup_gap():            # Inherited from GenericConfigWin


    # (Non-widget functions)


    def apply_changes(self, apply_button_flag=False):

        """Called by self.on_button_ok_clicked() and
        self.on_button_apply_clicked().

        Any changes the user has made are temporarily stored in self.edit_dict.
        Apply to those changes to the object being edited.

        In this edit window we apply changes to self.edit_obj.options_dict
        (rather than to self.edit_obj's attributes directly, as in the generic
        function.)

        Args:

            apply_button_flag (bool): True when self.apply_button was clicked,
                False when self.ok_button was clicked. When True, we do not
                start a process operation

        """

        # For 'change_file_ext', remove the initial . (e.g. in '.mp4', if
        #   specified
        if 'change_file_ext' in self.edit_dict:
            self.edit_dict['change_file_ext'] = re.sub(
                r'^\.',
                '',
                self.edit_dict['change_file_ext'],
            )

        # Apply any changes the user has made
        for key in self.edit_dict.keys():

            if key in self.edit_obj.options_dict:
                self.edit_obj.options_dict[key] = self.edit_dict[key]

        # The name can also be updated, if it has been changed (but it the
        #   entry was blank, keep the old name)
        if 'name' in self.edit_dict \
        and self.edit_dict['name'] != '':
            self.edit_obj.name = self.edit_dict['name']

        # The changes can now be cleared
        self.edit_dict = {}

        # If a list of videos was supplied, start a process operation
        if self.video_list and not apply_button_flag:

            # Check that every media.Video object still exists, eliminating any
            #   that don't
            mod_list = []
            for video_obj in self.video_list:

                # (Special case: 'dummy' video objects (those downloaded in the
                #   Classic Mode tab) use different IVs)
                if video_obj.dummy_flag \
                or (
                    video_obj.dbid in self.app_obj.media_reg_dict \
                    and self.app_obj.media_reg_dict[video_obj.dbid] \
                    == video_obj
                ):
                    mod_list.append(video_obj)

            if mod_list:

                self.app_obj.process_manager_start(
                    self.edit_obj,
                    mod_list,
                )


    def retrieve_val(self, name):

        """Can be called by anything.

        Any changes the user has made are temporarily stored in self.edit_dict.

        In the generic function, each key corresponds to an attribute in the
        object being edited, self.edit_obj. In this window, it corresponds to a
        key in self.edit_obj.options_dict.

        If 'name' exists as a key in that dictionary, retrieve the
        corresponding value and return it. Otherwise, the user hasn't yet
        modified the value, so retrieve directly from the attribute in the
        object being edited.

        Args:

            name (str): The name of the attribute in the object being edited

        Return values:

            The original or modified value of that attribute

        """

        if name in self.edit_dict:

            return self.edit_dict[name]

        elif name == 'uid' or name == 'name':

            return getattr(self.edit_obj, name)

        elif name in self.edit_obj.options_dict:

            value = self.edit_obj.options_dict[name]
            if type(value) is list or type(value) is dict:
                return value.copy()
            else:
                return value

        else:

            return self.app_obj.system_error(
                405,
                'Unrecognised property name \'' + name + '\'',
            )


    # (Setup tabs)


    def setup_tabs(self):

        """Called by self.setup(), .on_button_apply_clicked() and
        .on_button_reset_clicked().

        Sets up the tabs for this edit window.
        """

        self.setup_name_tab()
        self.setup_file_tab()

        if not self.app_obj.simple_ffmpeg_options_flag:
            self.setup_settings_tab()
            self.setup_optimise_tab()
        else:
            self.setup_clips_tab()
            self.setup_slices_tab()

        self.setup_videos_tab()

        # Unusual step: if a list of media.Video objects to be processed has
        #   been supplied, use a different label for the OK button
        if self.video_list:

            self.ok_button.set_label(_('Process files'))
            self.ok_button.set_tooltip_text(
                _('Process the files with FFmpeg'),
            )
            self.ok_button.get_child().set_width_chars(15)


    def setup_name_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Name' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: FFmpeg options > Name'
        )

        tab, grid = self.add_notebook_tab(_('_Name'))
        grid_width = 4

        self.add_label(grid,
            _('Name for these FFmpeg options'),
            0, 0, 2, 1,
        )

        entry = self.add_entry(grid,
            'name',
            2, 0, 1, 1,
        )
        entry.set_hexpand(True)

        entry2 = self.add_entry(grid,
            None,
            3, 0, 1, 1,
        )
        entry2.set_text('#' + str(self.edit_obj.uid))
        entry2.set_hexpand(False)

        # (To avoid messing up the neat format of the rows above and below, add
        #   a secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 1, grid_width, 1)

        self.add_label(grid2,
            _('Extra command line options (e.g. --help)'),
            0, 0, 1, 1,
        )

        checkbutton = self.add_checkbutton(grid2,
            _('Use these options exclusively'),
            'extra_override_flag',
            1, 0, 2, 1,
        )

        self.extra_cmd_string_textview, \
        self.extra_cmd_string_textbuffer = self.add_textview(grid,
            'extra_cmd_string',
            0, 2, grid_width, 1,
        )

        self.add_label(grid,
            _('System command, based on all FFmpeg options in this window:'),
            0, 3, grid_width, 1,
        )

        self.result_textview, self.results_textbuffer = self.add_textview(grid,
            None,
            0, 4, grid_width, 1,
        )
        self.result_textview.set_editable(False)
        self.result_textview.set_wrap_mode(Gtk.WrapMode.WORD)
        self.result_textview.set_can_focus(False)
        # (Set the system command, as it stands)
        self.update_system_cmd()

        if self.app_obj.simple_options_flag:
            frame = self.add_pixbuf(grid,
                'hand_right_large',
                0, 5, 1, 1,
            )
            frame.set_hexpand(False)

        else:
            frame = self.add_pixbuf(grid,
                'hand_left_large',
                0, 5, 1, 1,
            )
            frame.set_hexpand(False)

        button = Gtk.Button()
        grid.attach(button, 1, 5, (grid_width - 1), 1)
        if not self.app_obj.simple_ffmpeg_options_flag:
            button.set_label(_('Show fewer FFmpeg options'))
        else:
            button.set_label(_('Show more FFmpeg options'))
        button.connect('clicked', self.on_simple_options_clicked)

        frame2 = self.add_pixbuf(grid,
            'copy_large',
            0, 6, 1, 1,
        )
        frame2.set_hexpand(False)

        button2 = Gtk.Button(
            _('Import current FFmpeg options into this window'),
        )
        grid.attach(button2, 1, 6, (grid_width - 1), 1)
        button2.connect('clicked', self.on_clone_options_clicked)
        if self.edit_obj == self.app_obj.ffmpeg_options_obj:
            # No point cloning the current options manager into itself
            button2.set_sensitive(False)

        frame3 = self.add_pixbuf(grid,
            'warning_large',
            0, 7, 1, 1,
        )
        frame3.set_hexpand(False)

        button3 = Gtk.Button(
            _('Completely reset all FFmpeg options to their default values'),
        )
        grid.attach(button3, 1, 7, (grid_width - 1), 1)
        button3.connect('clicked', self.on_reset_options_clicked)


    def setup_file_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'File' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: FFmpeg options > File'
        )

        tab, grid = self.add_notebook_tab(_('_File'))

        self.add_label(grid,
            _('Add to end of filename:'),
            0, 0, 1, 1,
        )

        self.add_end_filename_entry = self.add_entry(grid,
            'add_end_filename',
            1, 0, 1, 1,
        )

        self.add_label(grid,
            _('If regex matches filename:'),
            0, 1, 1, 1,
        )

        self.regex_match_filename_entry = self.add_entry(grid,
            None,
            1, 1, 1, 1,
        )
        self.regex_match_filename_entry.set_text(
            self.retrieve_val('regex_match_filename'),
        )
        # (Signal connect appears below)

        self.add_label(grid,
            _('...then apply substitution:'),
            0, 2, 1, 1,
        )

        self.regex_apply_subst_entry = self.add_entry(grid,
            'regex_apply_subst',
            1, 2, 1, 1,
        )
        if self.retrieve_val('regex_match_filename') == '':
            self.regex_apply_subst_entry.set_sensitive(False)

        self.rename_both_flag_checkbutton = self.add_checkbutton(grid,
            _(
            'If the video/audio file is renamed, also rename the thumbnail' \
            + ' (but not vice-versa)',
            ),
            'rename_both_flag',
            0, 3, 2, 1,
        )
        if self.retrieve_val('add_end_filename') == '' \
        and self.retrieve_val('regex_match_filename') == '':
            self.rename_both_flag_checkbutton.set_sensitive(False)

        self.add_label(grid,
            _('Change file extension:'),
            0, 4, 1, 1,
        )

        self.change_file_ext_entry = self.add_entry(grid,
            None,
            1, 4, 1, 1,
        )
        self.change_file_ext_entry.set_text(
            self.retrieve_val('change_file_ext'),
        )
        # (Signal connect appears below)

        self.delete_original_flag_checkbutton = self.add_checkbutton(grid,
            _('After changing the file extension, delete the original file'),
            'delete_original_flag',
            0, 5, 1, 1,
        )
        if self.retrieve_val('change_file_ext') == '':
            self.delete_original_flag_checkbutton.set_sensitive(False)

        # (Signal connects from above)
        self.regex_match_filename_entry.connect(
            'changed',
            self.on_regex_match_filename_entry_changed,
        )

        self.change_file_ext_entry.connect(
            'changed',
            self.on_change_file_ext_entry_changed,
        )

        # (De)sensitise all of these widgets, depending on the value of the
        #   'output_mode' setting
        if self.retrieve_val('output_mode') == 'split':
            self.setup_file_tab_set_sensitive(False)
        else:
            self.setup_file_tab_set_sensitive(True)


    def setup_file_tab_set_sensitive(self, sens_flag):

        """Called by self.setup_file_tab() and various callbacks.

        (De)sensitises all widgets in the tab, as required.

        Args:

            sens_flag (bool): True to sensitise widgets, False to desensitise
                them

        """

        self.add_end_filename_entry.set_sensitive(sens_flag)
        self.regex_match_filename_entry.set_sensitive(sens_flag)

        if self.retrieve_val('regex_match_filename') == '':
            self.regex_apply_subst_entry.set_sensitive(False)
        else:
            self.regex_apply_subst_entry.set_sensitive(sens_flag)

        if self.retrieve_val('add_end_filename') == '' \
        and self.retrieve_val('regex_match_filename') == '':
            self.rename_both_flag_checkbutton.set_sensitive(False)
        else:
            self.rename_both_flag_checkbutton.set_sensitive(sens_flag)

        if self.retrieve_val('output_mode') == 'gif':
            self.change_file_ext_entry.set_sensitive(False)
        else:
            self.change_file_ext_entry.set_sensitive(sens_flag)

        if self.retrieve_val('change_file_ext') == '':
            self.delete_original_flag_checkbutton.set_sensitive(False)
        else:
            self.delete_original_flag_checkbutton.set_sensitive(sens_flag)


    def setup_settings_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Settings' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: FFmpeg options > Settings (to make hidden' \
            ' tabs visible, click the \'Show more FFmpeg options\' button' \
            ' in the Name tab'
        )

        tab, grid = self.add_notebook_tab(_('_Settings'))
        grid_width = 7
        self.settings_grid = grid

        # Source
        label = self.add_label(grid,
            '<u>' + _('Source') + '</u>',
            0, 0, 1, 1,
        )
        label.set_hexpand(False)

        self.input_mode_radiobutton = self.add_radiobutton(grid,
            None,
            _('Downloaded video/audio'),
            None,
            None,
            1, 0, 4, 1,
        )
        self.input_mode_radiobutton.set_hexpand(False)
        # (Signal connect appears below)

        self.audio_flag_checkbutton = self.add_checkbutton(grid,
            _('with audio'),
            None,
            5, 0, 1, 1,
        )
        self.audio_flag_checkbutton.set_hexpand(False)
        if self.retrieve_val('audio_flag'):
            self.audio_flag_checkbutton.set_active(True)
        if self.retrieve_val('input_mode') != 'video':
            self.audio_flag_checkbutton.set_sensitive(False)
        # (Signal connect appears below)

        self.input_mode_radiobutton2 = self.add_radiobutton(grid,
            self.input_mode_radiobutton,
            _('Thumbnail'),
            None,
            None,
            6, 0, 1, 1,
        )
        self.input_mode_radiobutton2.set_hexpand(False)
        if self.retrieve_val('input_mode') == 'thumb':
            self.input_mode_radiobutton2.set_active(True)
        # (Signal connect appears below)

        # Output
        label2 = self.add_label(grid,
            '<u>' + _('Output') + '</u>',
            0, 1, 1, 1,
        )
        label2.set_hexpand(False)

        self.output_mode_radiobutton = self.add_radiobutton(grid,
            None,
            'H.264',
            None,
            None,
            1, 1, 1, 1,
        )
        self.output_mode_radiobutton.set_hexpand(False)
        # (Signal connect appears below)

        self.output_mode_radiobutton2 = self.add_radiobutton(grid,
            self.output_mode_radiobutton,
            'GIF',
            None,
            None,
            2, 1, 1, 1,
        )
        self.output_mode_radiobutton2.set_hexpand(False)
        if self.retrieve_val('output_mode') == 'gif':
            self.output_mode_radiobutton2.set_active(True)
        # (Signal connect appears below)

        self.output_mode_radiobutton3 = self.add_radiobutton(grid,
            self.output_mode_radiobutton2,
            _('Video clip'),
            None,
            None,
            3, 1, 1, 1,
        )
        self.output_mode_radiobutton3.set_hexpand(False)
        if self.retrieve_val('output_mode') == 'split':
            self.output_mode_radiobutton3.set_active(True)
        # (Signal connect appears below)

        self.output_mode_radiobutton4 = self.add_radiobutton(grid,
            self.output_mode_radiobutton3,
            _('Video slice'),
            None,
            None,
            4, 1, 1, 1,
        )
        self.output_mode_radiobutton4.set_hexpand(False)
        if self.retrieve_val('output_mode') == 'slice':
            self.output_mode_radiobutton4.set_active(True)
        # (Signal connect appears below)

        self.output_mode_radiobutton5 = self.add_radiobutton(grid,
            self.output_mode_radiobutton4,
            _('Merge video/audio'),
            None,
            None,
            5, 1, 1, 1,
        )
        self.output_mode_radiobutton5.set_hexpand(False)
        if self.retrieve_val('output_mode') == 'merge':
            self.output_mode_radiobutton5.set_active(True)
        # (Signal connect appears below)

        self.output_mode_radiobutton6 = self.add_radiobutton(grid,
            self.output_mode_radiobutton5,
            _('Thumbnail'),
            None,
            None,
            6, 1, 1, 1,
        )
        self.output_mode_radiobutton6.set_hexpand(False)
        if self.retrieve_val('output_mode') == 'thumb':
            self.output_mode_radiobutton6.set_active(True)
        # (Signal connect appears below)

        # Supplementary grids: one for each 'output_mode'
        # Only one of them is visible at any time (this saves a lot of time
        #   (de)sensitising widgets)
        self.h264_grid = self.setup_settings_tab_h264_grid(2, grid_width)
        self.gif_grid = self.setup_settings_tab_gif_grid(2, grid_width)
        self.clip_grid = self.setup_settings_tab_clip_grid(2, grid_width)
        self.slice_grid = self.setup_settings_tab_slice_grid(2, grid_width)
        self.merge_grid = self.setup_settings_tab_merge_grid(2, grid_width)
        self.thumb_grid = self.setup_settings_tab_thumb_grid(2, grid_width)

        # (Signal connects from above)
        self.input_mode_radiobutton.connect(
            'toggled',
            self.on_input_mode_radiobutton_toggled,
        )
        self.audio_flag_checkbutton.connect(
            'toggled',
            self.on_audio_flag_checkbutton_toggled,
        )
        self.input_mode_radiobutton2.connect(
            'toggled',
            self.on_input_mode_radiobutton_toggled,
        )

        self.output_mode_radiobutton.connect(
            'toggled',
            self.on_output_mode_radiobutton_toggled,
            grid_width,
        )
        self.output_mode_radiobutton2.connect(
            'toggled',
            self.on_output_mode_radiobutton_toggled,
            grid_width,
        )
        self.output_mode_radiobutton3.connect(
            'toggled',
            self.on_output_mode_radiobutton_toggled,
            grid_width,
        )
        self.output_mode_radiobutton4.connect(
            'toggled',
            self.on_output_mode_radiobutton_toggled,
            grid_width,
        )
        self.output_mode_radiobutton5.connect(
            'toggled',
            self.on_output_mode_radiobutton_toggled,
            grid_width,
        )
        self.output_mode_radiobutton6.connect(
            'toggled',
            self.on_output_mode_radiobutton_toggled,
            grid_width,
        )


    def setup_settings_tab_h264_grid(self, row, outer_width):

        """Called by self.setup_settings_tab().

        Creates a supplementary grid, within the tab's outer grid, which can be
        swapped in and out as the 'output_mode' option is changed.

        This supplementary grid is visible when 'output_mode' is 'h264'.

        Args:

            row (int): The row on the tab's outer grid, on which the
                supplementary grid is to be placed

            outer_width (int): The width of the tab's outer grid

        Return values:

            The new Gtk.Grid().

        """

        grid = Gtk.Grid()
        if self.retrieve_val('output_mode') == 'h264':
            self.settings_grid.attach(grid, 0, row, outer_width, 1)
        grid.set_border_width(self.spacing_size)
        grid.set_column_spacing(self.spacing_size)
        grid.set_row_spacing(self.spacing_size)

        inner_width = 3

        self.add_label(grid,
            _('Audio bitrate'),
            0, 0, 1, 1,
        )

        self.audio_bitrate_spinbutton = self.add_spinbutton(grid,
            16, None, 16,
            'audio_bitrate',
            1, 0, 1, 1,
        )
        if self.retrieve_val('input_mode') != 'video' \
        or not self.retrieve_val('audio_flag'):
            self.audio_bitrate_spinbutton.set_sensitive(False)

        label = self.add_label(grid,
            _('How to set the quality') + ' ⓘ',
            0, 1, 1, 1,
        )
        label.set_tooltip_text(
            _(
            'FFmpeg always encodes according to a Rate Factor that specifies' \
            + ' the quality of the result.',
            ) + '\n\n' + _(
            'Instead of directly specifying the Rate Factor, an average bit' \
            + ' rate can be specified. FFmpeg will then determine the' \
            + ' optimal Rate Factor in a first pass.',
            ) + '\n\n' + _(
            'In fact the first pass is only used for determining the Rate' \
            + ' Factor, no other data is carried over into the second pass.',
            ) + '\n\n' + _(
            'Specifying an average bitrate but running only one pass is' \
            + ' possible, but not recommended. FFmpeg would then encode the' \
            + ' beginning of the video with a random Rate Factor and then' \
            + ' change it near the end of the video to eventually reach the' \
            + ' target bitrate.',
            ),
        )

        # N.B. In the original 'FFmpeg command line wizard', the second of this
        #   pair of radiobuttons are disabled (for unknown reasons); here it is
        #   enabled
        self.quality_mode_radiobutton = self.add_radiobutton(grid,
            None,
            _('Manual rate factor'),
            None,
            None,
            1, 1, (inner_width - 1), 1,
        )
        # (Signal connect appears below)

        self.quality_mode_radiobutton2 = self.add_radiobutton(grid,
            self.quality_mode_radiobutton,
            _('Determine from target bitrate (2-Pass)'),
            None,
            None,
            1, 2, (inner_width - 1), 1,
        )
        if self.retrieve_val('quality_mode') == 'abr':
            self.quality_mode_radiobutton2.set_active(True)
        # (Signal connect appears below)

        self.add_label(grid,
            _('Rate factor'),
            0, 3, 1, 1,
        )

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 1, 3, (inner_width - 1), 1)

        label2 = self.add_label(grid2,
            _('Lossless') + '\n' + _('Large file'),
            0, 0, 1, 1,
        )
        label2.set_hexpand(False)

        self.rate_factor_scale = Gtk.Scale().new_with_range(
            Gtk.Orientation.HORIZONTAL,
            0,
            51,
            1,
        )
        grid2.attach(self.rate_factor_scale, 1, 0, 1, 1)
        self.rate_factor_scale.set_draw_value(True)
        self.rate_factor_scale.set_value(
            self.retrieve_val('rate_factor'),
        )
        self.rate_factor_scale.set_hexpand(True)
        if self.retrieve_val('quality_mode') == 'abr':
            self.rate_factor_scale.set_sensitive(False)
        # (Signal connect appears below)

        label3 = self.add_label(grid2,
            _('Bad quality') + '\n' + _('Small file'),
            2, 0, 1, 1,
        )
        label3.set_hexpand(False)
        # (End of the yet another grid)

        label4 = self.add_label(grid,
            _('Name of dummy file') + ' ⓘ',
            0, 4, 1, 1,
        )
        label4.set_tooltip_text(
            _('A dummy file is created during the first pass.'),
        )

        combo_list = [
            [_('Use the output file'), 'output'],
            [_('Dummy'), 'dummy'],
            [_('/dev/null (Linux)'), '/dev/null'],
            [_('NUL (MS Windows)'), 'NUL'],
        ]

        self.dummy_file_combo = self.add_combo_with_data(grid,
            combo_list,
            'dummy_file',
            1, 4, (inner_width - 1), 1,
        )
        if self.retrieve_val('quality_mode') != 'abr':
            self.dummy_file_combo.set_sensitive(False)

        self.add_label(grid,
            _('Patience preset'),
            0, 5, 1, 1,
        )

        combo_list2 = [
            [_('Ultra fast'), 'ultrafast'],
            [_('Super fast'), 'superfast'],
            [_('Very fast'), 'veryfast'],
            [_('Faster'), 'faster'],
            [_('Fast'), 'fast'],
            [_('Medium (default)'), 'medium'],
            [_('Slow (file about 5-10% smaller than medium)'), 'slow'],
            [_('Slower (file about 15% smaller than medium)'), 'slower'],
            [_('Very slow (file about 17% smaller than medium)'), 'veryslow'],
        ]

        self.patience_preset_combo = self.add_combo_with_data(grid,
            combo_list2,
            'patience_preset',
            1, 5, (inner_width - 1), 1,
        )
        self.patience_preset_combo.set_hexpand(False)

        self.add_label(grid,
            _('GPU encoding'),
            0, 6, 1, 1,
        )

        combo_list3 = [
            'libx264', 'libx265', 'h264_amf', 'hevc_amf', 'h264_nvenc',
            'hevc_nvenc',
        ]

        self.gpu_encoding_combo = self.add_combo(grid,
            combo_list3,
            'gpu_encoding',
            1, 6, (inner_width - 1), 1,
        )

        self.add_label(grid,
            _('Hardware acceleration'),
            0, 7, 1, 1,
        )

        combo_list4 = ['none', 'auto', 'vdpau', 'dxva2', 'vaapi', 'qsv']

        self.hw_accel_combo = self.add_combo(grid,
            combo_list4,
            'hw_accel',
            1, 7, (inner_width - 1), 1,
        )

        # (Signal connects from above)
        self.quality_mode_radiobutton.connect(
            'toggled',
            self.on_quality_mode_radiobutton_toggled,
        )
        self.quality_mode_radiobutton2.connect(
            'toggled',
            self.on_quality_mode_radiobutton_toggled,
        )

        self.rate_factor_scale.connect(
            'value-changed',
            self.on_rate_factor_scale_changed,
        )

        return grid


    def setup_settings_tab_gif_grid(self, row, outer_width):

        """Called by self.setup_settings_tab().

        Creates a supplementary grid, within the tab's outer grid, which can be
        swapped in and out as the 'output_mode' option is changed.

        This supplementary grid is visible when 'output_mode' is 'gif'.

        Args:

            row (int): The row on the tab's outer grid, on which the
                supplementary grid is to be placed

            outer_width (int): The width of the tab's outer grid

        Return values:

            The new Gtk.Grid().

        """

        grid = Gtk.Grid()
        if self.retrieve_val('output_mode') == 'gif':
            self.settings_grid.attach(grid, 0, row, outer_width, 1)
        grid.set_border_width(self.spacing_size)
        grid.set_column_spacing(self.spacing_size)
        grid.set_row_spacing(self.spacing_size)

        self.add_label(grid,
            _('Palette:'),
            0, 0, 1, 1,
        )

        self.palette_mode_radiobutton = self.add_radiobutton(grid,
            None,
            _('Faster') + '\n' \
            + _('Uses dithering to a standard palette provided by FFmpeg') \
            + '\n' + _('Can cause dithering artefacts and slight banding'),
            None,
            None,
            1, 0, 1, 1,
        )
        # (Signal connect appears below)

        self.palette_mode_radiobutton2 = self.add_radiobutton(grid,
            self.palette_mode_radiobutton,
            _('Better') + '\n' \
            + _('Determines an optimized palette for the video') + '\n' \
            + _('Uses two passes and a temporary file for the palette'),
            None,
            None,
            1, 1, 1, 1,
        )
        if self.retrieve_val('palette_mode') == 'better':
            self.palette_mode_radiobutton2.set_active(True)
        # (Signal connect appears below)

        # (Signal connects from above)
        self.palette_mode_radiobutton.connect(
            'toggled',
            self.on_palette_mode_radiobutton_toggled,
        )
        self.palette_mode_radiobutton2.connect(
            'toggled',
            self.on_palette_mode_radiobutton_toggled,
        )

        return grid


    def setup_settings_tab_clip_grid(self, row, outer_width):

        """Called by self.setup_settings_tab().

        Creates a supplementary grid, within the tab's outer grid, which can be
        swapped in and out as the 'output_mode' option is changed.

        This supplementary grid is visible when 'output_mode' is 'split'.

        Args:

            row (int): The row on the tab's outer grid, on which the
                supplementary grid is to be placed

            outer_width (int): The width of the tab's outer grid

        Return values:

            The new Gtk.Grid().

        """

        grid = Gtk.Grid()
        if self.retrieve_val('output_mode') == 'split':
            self.settings_grid.attach(grid, 0, row, outer_width, 1)
        grid.set_border_width(self.spacing_size)
        grid.set_column_spacing(self.spacing_size)
        grid.set_row_spacing(self.spacing_size)

        grid_width = 4

        self.split_mode_radiobutton = self.add_radiobutton(grid,
            None,
            _('Split videos using their own timestamps'),
            None,
            None,
            0, 0, 1, 1,
        )
        # (Signal connect appears below)

        self.split_mode_radiobutton2 = self.add_radiobutton(grid,
            self.split_mode_radiobutton,
            _('Split videos using these timestamps'),
            None,
            None,
            1, 0, 1, 1,
        )
        if self.retrieve_val('split_mode') == 'custom':
            self.split_mode_radiobutton2.set_active(True)
        # (Signal connect appears below)

        # (Signal connects from above)
        self.split_mode_radiobutton.connect(
            'toggled',
            self.on_split_mode_radiobutton_toggled,
        )
        self.split_mode_radiobutton2.connect(
            'toggled',
            self.on_split_mode_radiobutton_toggled,
        )

        # (GenericConfigWin.add_treeview() doesn't support multiple columns, so
        #   we'll do everything ourselves)
        frame = Gtk.Frame()
        grid.attach(frame, 0, 1, grid_width, 1)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)

        treeview = Gtk.TreeView()
        scrolled.add(treeview)
        treeview.set_headers_visible(True)

        for i, column_title in enumerate(
            [ _('Start'), _('Stop'), _('Clip title') ],
        ):
            renderer_text = Gtk.CellRendererText()
            column_text = Gtk.TreeViewColumn(
                column_title,
                renderer_text,
                text=i,
            )
            treeview.append_column(column_text)
            column_text.set_resizable(True)

        self.split_mode_liststore = Gtk.ListStore(str, str, str)
        treeview.set_model(self.split_mode_liststore)

        # Initialise the list
        self.setup_settings_tab_update_clip_treeview()

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 2, grid_width, 1)

        # Strip of widgets at the bottom
        label = self.add_label(grid2,
            _('Start timestamp (e.g. 15:29)'),
            0, 0, 1, 1,
        )
        label.set_hexpand(False)

        if self.retrieve_val('split_mode') == 'video':
            custom_flag = False
        else:
            custom_flag = True

        self.start_stamp_entry = self.add_entry(grid2,
            None,
            1, 0, 1, 1,
        )
        self.start_stamp_entry.set_width_chars(12)
        self.start_stamp_entry.set_hexpand(False)
        if not custom_flag:
            self.start_stamp_entry.set_sensitive(False)

        label2 = self.add_label(grid2,
            _('Stop timestamp (optional)'),
            2, 0, 1, 1,
        )
        label2.set_hexpand(False)

        self.stop_stamp_entry = self.add_entry(grid2,
            None,
            3, 0, 1, 1,
        )
        self.stop_stamp_entry.set_width_chars(12)
        self.stop_stamp_entry.set_hexpand(False)
        if not custom_flag:
            self.stop_stamp_entry.set_sensitive(False)

        label3 = self.add_label(grid2,
            _('Clip title (optional)'),
            0, 1, 1, 1,
        )
        label3.set_hexpand(False)

        self.clip_title_entry = self.add_entry(grid2,
            None,
            1, 1, (grid_width - 1), 1,
        )
        self.clip_title_entry.set_hexpand(True)
        if not custom_flag:
            self.clip_title_entry.set_sensitive(False)

        self.add_timestamp_button = Gtk.Button(_('Add timestamp'))
        grid2.attach(self.add_timestamp_button, 0, 2, 1, 1)
        self.add_timestamp_button.connect(
            'clicked',
            self.on_add_timestamp_clicked,
        )
        if not custom_flag:
            self.add_timestamp_button.set_sensitive(False)

        self.delete_timestamp_button = Gtk.Button(_('Delete timestamp'))
        grid2.attach(self.delete_timestamp_button, 1, 2, 1, 1)
        self.delete_timestamp_button.connect(
            'clicked',
            self.on_delete_timestamp_clicked,
            treeview,
        )
        if not custom_flag:
            self.delete_timestamp_button.set_sensitive(False)

        self.show_prefs_button = Gtk.Button(_('Clip preferences'))
        grid2.attach(self.show_prefs_button, 2, 2, 1, 1)
        self.show_prefs_button.connect(
            'clicked',
            self.on_clip_prefs_clicked,
        )

        self.clear_timestamp_button = Gtk.Button(_('Clear list'))
        grid2.attach(self.clear_timestamp_button, 3, 2, 1, 1)
        self.clear_timestamp_button.connect(
            'clicked',
            self.on_clear_timestamp_clicked,
        )
        if not custom_flag:
            self.clear_timestamp_button.set_sensitive(False)

        return grid


    def setup_settings_tab_slice_grid(self, row, outer_width):

        """Called by self.setup_settings_tab().

        Creates a supplementary grid, within the tab's outer grid, which can be
        swapped in and out as the 'output_mode' option is changed.

        This supplementary grid is visible when 'output_mode' is 'slice'.

        Args:

            row (int): The row on the tab's outer grid, on which the
                supplementary grid is to be placed

            outer_width (int): The width of the tab's outer grid

        Return values:

            The new Gtk.Grid().

        """

        grid = Gtk.Grid()
        if self.retrieve_val('output_mode') == 'slice':
            self.settings_grid.attach(grid, 0, row, outer_width, 1)
        grid.set_border_width(self.spacing_size)
        grid.set_column_spacing(self.spacing_size)
        grid.set_row_spacing(self.spacing_size)

        grid_width = 4

        self.slice_mode_radiobutton = self.add_radiobutton(grid,
            None,
            _('Use the videos\' own slice data'),
            None,
            None,
            0, 0, 1, 1,
        )
        # (Signal connect appears below)

        self.slice_mode_radiobutton2 = self.add_radiobutton(grid,
            self.slice_mode_radiobutton,
            _('Use this slice data'),
            None,
            None,
            1, 0, 1, 1,
        )
        if self.retrieve_val('slice_mode') == 'custom':
            self.slice_mode_radiobutton2.set_active(True)
        # (Signal connect appears below)

        # (Signal connects from above)
        self.slice_mode_radiobutton.connect(
            'toggled',
            self.on_slice_mode_radiobutton_toggled,
        )
        self.slice_mode_radiobutton2.connect(
            'toggled',
            self.on_slice_mode_radiobutton_toggled,
        )

        # (GenericConfigWin.add_treeview() doesn't support multiple columns, so
        #   we'll do everything ourselves)
        frame = Gtk.Frame()
        grid.attach(frame, 0, 1, grid_width, 1)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)

        treeview = Gtk.TreeView()
        scrolled.add(treeview)
        treeview.set_headers_visible(True)

        for i, column_title in enumerate(
            [ _('Category'), _('Action type'), _('Start'), _('Stop') ],
        ):
            renderer_text = Gtk.CellRendererText()
            column_text = Gtk.TreeViewColumn(
                column_title,
                renderer_text,
                text=i,
            )
            treeview.append_column(column_text)
            column_text.set_resizable(True)

        self.slice_mode_liststore = Gtk.ListStore(str, str, str, str)
        treeview.set_model(self.slice_mode_liststore)

        # Initialise the list
        self.setup_settings_tab_update_slice_treeview()

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 2, grid_width, 1)

        # Strip of widgets at the bottom
        label = self.add_label(grid2,
            _('Category'),
            0, 0, 1, 1,
        )
        label.set_hexpand(False)

        if self.retrieve_val('slice_mode') == 'video':
            custom_flag = False
        else:
            custom_flag = True

        self.category_combo = self.add_combo(grid2,
            formats.SPONSORBLOCK_CATEGORY_LIST,
            None,
            1, 0, 1, 1,
        )
        self.category_combo.set_active(0)
        if not custom_flag:
            self.category_combo.set_sensitive(False)

        label2 = self.add_label(grid2,
            _('Action type'),
            2, 0, 1, 1,
        )
        label2.set_hexpand(False)

        self.action_combo = self.add_combo(grid2,
            formats.SPONSORBLOCK_ACTION_LIST,
            None,
            3, 0, 1, 1,
        )
        self.action_combo.set_active(0)
        if not custom_flag:
            self.action_combo.set_sensitive(False)

        label3 = self.add_label(grid2,
            _('Start (timestamp or seconds)'),
            0, 1, 1, 1,
        )
        label3.set_hexpand(False)

        self.slice_start_entry = self.add_entry(grid2,
            None,
            1, 1, 1, 1,
        )
        if not custom_flag:
            self.slice_start_entry.set_sensitive(False)

        label4 = self.add_label(grid2,
            _('Stop (optional)'),
            2, 1, 1, 1,
        )
        label4.set_hexpand(False)

        self.slice_stop_entry = self.add_entry(grid2,
            None,
            3, 1, 1, 1,
        )
        if not custom_flag:
            self.slice_stop_entry.set_sensitive(False)

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid3 = self.add_secondary_grid(grid, 0, 3, grid_width, 1)

        self.add_slice_button = Gtk.Button(_('Add slice'))
        grid3.attach(self.add_slice_button, 0, 0, 1, 1)
        self.add_slice_button.set_hexpand(True)
        self.add_slice_button.connect(
            'clicked',
            self.on_add_slice_clicked,
        )
        if not custom_flag:
            self.add_slice_button.set_sensitive(False)

        self.delete_slice_button = Gtk.Button(_('Delete sliuce'))
        grid3.attach(self.delete_slice_button, 1, 0, 1, 1)
        self.delete_slice_button.set_hexpand(True)
        self.delete_slice_button.connect(
            'clicked',
            self.on_delete_slice_clicked,
            treeview,
        )
        if not custom_flag:
            self.delete_slice_button.set_sensitive(False)

        self.show_settings_button = Gtk.Button(_('SponsorBlock settings'))
        grid3.attach(self.show_settings_button, 2, 0, 1, 1)
        self.show_settings_button.set_hexpand(True)
        self.show_settings_button.connect(
            'clicked',
            self.on_slice_settings_clicked,
        )

        self.clear_slice_button = Gtk.Button(_('Clear list'))
        grid3.attach(self.clear_slice_button, 3, 0, 1, 1)
        self.clear_slice_button.set_hexpand(True)
        self.clear_slice_button.connect(
            'clicked',
            self.on_clear_slice_clicked,
        )
        if not custom_flag:
            self.clear_slice_button.set_sensitive(False)

        return grid


    def setup_settings_tab_update_clip_treeview(self):

        """ Called by self.setup_settings_tab_clip_grid().

        Fills or updates the treeview.
        """

        self.split_mode_liststore.clear()

        # Add each timestamp/clip title to the treeview, one row at a time
        for mini_list in self.retrieve_val('split_list'):

            start_stamp = mini_list[0]

            if mini_list[1] is None:
                stop_stamp = ''
            else:
                stop_stamp = mini_list[1]

            if mini_list[2] is None:
                clip_title = ''
            else:
                clip_title = mini_list[1]

            self.split_mode_liststore.append(
                [ start_stamp, stop_stamp, clip_title ],
            )


    def setup_settings_tab_update_slice_treeview(self):

        """ Called by self.setup_settings_tab_slice_grid().

        Fills or updates the treeview.
        """

        self.slice_mode_liststore.clear()

        # Add slice data to the treeview, one row at a time
        for mini_dict in self.retrieve_val('slice_list'):

            self.slice_mode_liststore.append(
                [
                    mini_dict['category'],
                    mini_dict['action'],
                    str(mini_dict['start_time']),
                    str(mini_dict['stop_time']),
                ],
            )


    def setup_settings_tab_merge_grid(self, row, outer_width):

        """Called by self.setup_settings_tab().

        Creates a supplementary grid, within the tab's outer grid, which can be
        swapped in and out as the 'output_mode' option is changed.

        This supplementary grid is visible when 'output_mode' is 'merge'.

        Args:

            row (int): The row on the tab's outer grid, on which the
                supplementary grid is to be placed

            outer_width (int): The width of the tab's outer grid

        Return values:

            The new Gtk.Grid().

        """

        grid = Gtk.Grid()
        if self.retrieve_val('output_mode') == 'merge':
            self.settings_grid.attach(grid, 0, row, outer_width, 1)
        grid.set_border_width(self.spacing_size)
        grid.set_column_spacing(self.spacing_size)
        grid.set_row_spacing(self.spacing_size)

        self.add_label(grid,
            '<i>' + _(
                'This merges a video and audio file with the same name' \
                + ' into a single video file,\nusing the extension' \
                + ' specified in the File tab',
            ) + '</i>',
            0, 0, 1, 1,
        )

        return grid


    def setup_settings_tab_thumb_grid(self, row, outer_width):

        """Called by self.setup_settings_tab().

        Creates a supplementary grid, within the tab's outer grid, which can be
        swapped in and out as the 'output_mode' option is changed.

        This supplementary grid is visible when 'output_mode' is 'thumb'.

        Args:

            row (int): The row on the tab's outer grid, on which the
                supplementary grid is to be placed

            outer_width (int): The width of the tab's outer grid

        Return values:

            The new Gtk.Grid().

        """

        grid = Gtk.Grid()
        if self.retrieve_val('output_mode') == 'thumb':
            self.settings_grid.attach(grid, 0, row, outer_width, 1)
        grid.set_border_width(self.spacing_size)
        grid.set_column_spacing(self.spacing_size)
        grid.set_row_spacing(self.spacing_size)

        self.add_label(grid,
            '<i>' + _(
                'The thumbnail\'s format can be changed in the File tab',
            ) + '</i>',
            0, 0, 1, 1,
        )

        return grid


    def setup_optimise_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Optimisations' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: FFmpeg options > Optimisations'
        )

        tab, grid = self.add_notebook_tab(_('_Optimisations'))
        grid_width = 2

        self.seek_flag_checkbutton = self.add_checkbutton(grid,
            _(
                'Optimise for fast seeking (shorter keyframe interval, about' \
                + ' 10% larger file)',
            ),
            'seek_flag',
            0, 0, grid_width, 1,
        )

        self.tuning_film_flag_checkbutton = self.add_checkbutton(grid,
            _('Input video is a high-quality movie'),
            'tuning_film_flag',
            0, 1, grid_width, 1,
        )

        self.tuning_animation_flag_checkbutton = self.add_checkbutton(grid,
            _('Input video is an animated movie'),
            'tuning_animation_flag',
            0, 2, grid_width, 1,
        )

        self.tuning_grain_flag_checkbutton = self.add_checkbutton(grid,
            _('Input video contains film grain'),
            'tuning_grain_flag',
            0, 3, grid_width, 1,
        )

        self.tuning_still_image_flag_checkbutton = self.add_checkbutton(grid,
            _('Input video is an image slideshow'),
            'tuning_still_image_flag',
            0, 4, grid_width, 1,
        )

        self.tuning_fast_decode_flag_checkbutton = self.add_checkbutton(grid,
            _('Optimise for really weak CPU playback devices'),
            'tuning_fast_decode_flag',
            0, 5, grid_width, 1,
        )

        self.profile_flag_checkbutton = self.add_checkbutton(grid,
            _(
                'Optimise for really old devices (requires rate factor' \
                + ' above 0)',
            ),
            'profile_flag',
            0, 6, grid_width, 1,
        )
        if not self.retrieve_val('rate_factor'):
            self.profile_flag_checkbutton.set_sensitive(False)

        self.fast_start_flag_checkbutton = self.add_checkbutton(grid,
            _(
                'Move headers to beginning of file (so it can play while' \
                + ' still downloading)',
            ),
            'fast_start_flag',
            0, 7, grid_width, 1,
        )

        self.tuning_zero_latency_flag_checkbutton = self.add_checkbutton(grid,
            _('Fast encoding and low latency streaming'),
            'tuning_zero_latency_flag',
            0, 8, grid_width, 1,
        )

        self.limit_flag_checkbutton = self.add_checkbutton(grid,
            _('Limit bitrate (Mbit/s)'),
            None,
            0, 9, 1, 1,
        )
        if self.retrieve_val('limit_flag'):
            self.limit_flag_checkbutton.set_active(True)
        # (Signal connect appears below)

        self.limit_mbps_spinbutton = self.add_spinbutton(grid,
            0, None, 0.2,
            'limit_mbps',
            1, 9, 1, 1,
        )
        if not self.retrieve_val('limit_flag'):
            self.limit_mbps_spinbutton.set_sensitive(False)

        self.add_label(grid,
            '          ' + _('Assuming a receiving buffer (seconds)'),
            0, 10, 1, 1,
        )

        self.limit_buffer_spinbutton = self.add_spinbutton(grid,
            0, None, 0.2,
            'limit_buffer',
            1, 10, 1, 1,
        )
        if not self.retrieve_val('limit_flag'):
            self.limit_buffer_spinbutton.set_sensitive(False)

        # (De)sensitise all of these widgets, depending on the value of the
        #   'output_mode' setting
        if self.retrieve_val('output_mode') == 'h264':
            self.setup_optimise_tab_set_sensitive(True)
        else:
            self.setup_optimise_tab_set_sensitive(False)

        # (Signal connects from above)
        self.limit_flag_checkbutton.connect(
            'toggled',
            self.on_limit_flag_checkbutton_toggled,
        )


    def setup_clips_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Clips' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: FFmpeg options > Clips'
        )

        tab, grid = self.add_notebook_tab(_('_Clips'))
        grid_width = 2

        output_mode = self.retrieve_val('output_mode')
        split_mode = self.retrieve_val('split_mode')

        # N.B. I tried moving the equivalent code from
        #   self.setup_settings_tab_clip_grid() into a function, that could
        #   also be called from here, but that created too many complications
        # However, both calling functions will use the same set of callbacks

        self.simple_split_mode_checkbutton = self.add_checkbutton(grid,
            _('Split the video(s) to create video clips'),
            None,
            0, 0, 1, 1,
        )
        if output_mode == 'split':
            self.simple_split_mode_checkbutton.set_active(True)
        # (Signal connect appears below)

        self.simple_split_mode_radiobutton = self.add_radiobutton(grid,
            None,
            _('Split videos using their own timestamps'),
            None,
            None,
            1, 0, 1, 1,
        )
        if output_mode != 'split':
            self.simple_split_mode_radiobutton.set_sensitive(False)
        # (Signal connect appears below)

        self.simple_split_mode_radiobutton2 = self.add_radiobutton(grid,
            self.simple_split_mode_radiobutton,
            _('Split videos using these timestamps'),
            None,
            None,
            1, 1, 1, 1,
        )
        if output_mode != 'split':
            self.simple_split_mode_radiobutton2.set_sensitive(False)
        if split_mode == 'custom':
            self.simple_split_mode_radiobutton2.set_active(True)
        # (Signal connect appears below)

        # (Signal connects from above)
        self.simple_split_mode_checkbutton.connect(
            'toggled',
            self.on_simple_split_toggled,
        )
        self.simple_split_mode_radiobutton.connect(
            'toggled',
            self.on_split_mode_radiobutton_toggled,
        )

        # (GenericConfigWin.add_treeview() doesn't support multiple columns, so
        #   we'll do everything ourselves)
        frame = Gtk.Frame()
        grid.attach(frame, 0, 2, grid_width, 1)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)

        treeview = Gtk.TreeView()
        scrolled.add(treeview)
        treeview.set_headers_visible(True)

        for i, column_title in enumerate(
            [ _('Start'), _('Stop'), _('Clip title') ],
        ):
            renderer_text = Gtk.CellRendererText()
            column_text = Gtk.TreeViewColumn(
                column_title,
                renderer_text,
                text=i,
            )
            treeview.append_column(column_text)
            column_text.set_resizable(True)

        self.simple_split_mode_liststore = Gtk.ListStore(str, str, str)
        treeview.set_model(self.simple_split_mode_liststore)

        # Initialise the list
        self.setup_clips_tab_update_treeview()

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 3, grid_width, 1)

        # Strip of widgets at the bottom
        label = self.add_label(grid2,
            _('Start timestamp (e.g. 15:29)'),
            0, 0, 1, 1,
        )
        label.set_hexpand(False)

        if split_mode == 'video':
            custom_flag = False
        else:
            custom_flag = True

        self.simple_start_stamp_entry = self.add_entry(grid2,
            None,
            1, 0, 1, 1,
        )
        self.simple_start_stamp_entry.set_width_chars(12)
        self.simple_start_stamp_entry.set_hexpand(False)
        if not custom_flag:
            self.simple_start_stamp_entry.set_sensitive(False)

        label2 = self.add_label(grid2,
            _('Stop timestamp (optional)'),
            2, 0, 1, 1,
        )
        label2.set_hexpand(False)

        self.simple_stop_stamp_entry = self.add_entry(grid2,
            None,
            3, 0, 1, 1,
        )
        self.simple_stop_stamp_entry.set_width_chars(12)
        self.simple_stop_stamp_entry.set_hexpand(False)
        if not custom_flag:
            self.simple_stop_stamp_entry.set_sensitive(False)

        label3 = self.add_label(grid2,
            _('Clip title (optional)'),
            0, 1, 1, 1,
        )
        label3.set_hexpand(False)

        self.simple_clip_title_entry = self.add_entry(grid2,
            None,
            1, 1, (grid_width - 1), 1,
        )
        self.simple_clip_title_entry.set_hexpand(True)
        if not custom_flag:
            self.simple_clip_title_entry.set_sensitive(False)

        self.simple_add_timestamp_button = Gtk.Button(_('Add timestamp'))
        grid2.attach(self.simple_add_timestamp_button, 0, 2, 1, 1)
        self.simple_add_timestamp_button.connect(
            'clicked',
            self.on_add_timestamp_clicked,
        )
        if not custom_flag:
            self.simple_add_timestamp_button.set_sensitive(False)

        self.simple_delete_timestamp_button = Gtk.Button(_('Delete timestamp'))
        grid2.attach(self.simple_delete_timestamp_button, 1, 2, 1, 1)
        self.simple_delete_timestamp_button.connect(
            'clicked',
            self.on_delete_timestamp_clicked,
            treeview,
        )
        if not custom_flag:
            self.simple_delete_timestamp_button.set_sensitive(False)

        self.simple_show_prefs_button = Gtk.Button(_('Clip preferences'))
        grid2.attach(self.simple_show_prefs_button, 2, 2, 1, 1)
        self.simple_show_prefs_button.connect(
            'clicked',
            self.on_clip_prefs_clicked,
        )

        self.simple_clear_timestamp_button = Gtk.Button(_('Clear list'))
        grid2.attach(self.simple_clear_timestamp_button, 3, 2, 1, 1)
        self.simple_clear_timestamp_button.connect(
            'clicked',
            self.on_clear_timestamp_clicked,
        )
        if not custom_flag:
            self.simple_clear_timestamp_button.set_sensitive(False)


    def setup_clips_tab_update_treeview(self):

        """ Called by self.setup_clips_tab().

        Fills or updates the treeview.
        """

        self.simple_split_mode_liststore.clear()

        # Add each timestamp/title to the treeview, one row at a time
        for mini_list in self.retrieve_val('split_list'):

            start_stamp = mini_list[0]

            if mini_list[1] is None:
                stop_stamp = ''
            else:
                stop_stamp = mini_list[1]

            if mini_list[2] is None:
                clip_title = ''
            else:
                clip_title = mini_list[1]

            self.simple_split_mode_liststore.append(
                [ start_stamp, stop_stamp, clip_title ],
            )


    def setup_slices_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Slices' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: FFmpeg options > Slices'
        )

        tab, grid = self.add_notebook_tab(_('_Slices'))
        grid_width = 2

        output_mode = self.retrieve_val('output_mode')
        slice_mode = self.retrieve_val('split_mode')

        self.simple_slice_mode_checkbutton = self.add_checkbutton(grid,
            _('Remove slices from the video(s)'),
            None,
            0, 0, 1, 1,
        )
        if output_mode == 'slice':
            self.simple_slice_mode_checkbutton.set_active(True)
        # (Signal connect appears below)

        self.simple_slice_mode_radiobutton = self.add_radiobutton(grid,
            None,
            _('Use the videos\' own slice data'),
            None,
            None,
            1, 0, 1, 1,
        )
        if output_mode != 'slice':
            self.simple_slice_mode_radiobutton.set_sensitive(False)
        # (Signal connect appears below)

        self.simple_slice_mode_radiobutton2 = self.add_radiobutton(grid,
            self.simple_slice_mode_radiobutton,
            _('Use this slice data'),
            None,
            None,
            1, 1, 1, 1,
        )
        if output_mode != 'slice':
            self.simple_slice_mode_radiobutton2.set_sensitive(False)
        if slice_mode == 'custom':
            self.simple_slice_mode_radiobutton2.set_active(True)
        # (Signal connect appears below)

        # (Signal connects from above)
        self.simple_slice_mode_checkbutton.connect(
            'toggled',
            self.on_simple_slice_toggled,
        )
        self.simple_slice_mode_radiobutton.connect(
            'toggled',
            self.on_slice_mode_radiobutton_toggled,
        )

        # (GenericConfigWin.add_treeview() doesn't support multiple columns, so
        #   we'll do everything ourselves)
        frame = Gtk.Frame()
        grid.attach(frame, 0, 2, grid_width, 1)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)

        treeview = Gtk.TreeView()
        scrolled.add(treeview)
        treeview.set_headers_visible(True)

        for i, column_title in enumerate(
            [ _('Category'), _('Action type'), _('Start'), _('Stop') ],
        ):
            renderer_text = Gtk.CellRendererText()
            column_text = Gtk.TreeViewColumn(
                column_title,
                renderer_text,
                text=i,
            )
            treeview.append_column(column_text)
            column_text.set_resizable(True)

        self.simple_slice_mode_liststore = Gtk.ListStore(str, str, str, str)
        treeview.set_model(self.simple_slice_mode_liststore)

        # Initialise the list
        self.setup_slices_tab_update_treeview()

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 3, grid_width, 1)

        # Strip of widgets at the bottom
        label = self.add_label(grid2,
            _('Category'),
            0, 0, 1, 1,
        )
        label.set_hexpand(False)

        if slice_mode == 'video':
            custom_flag = False
        else:
            custom_flag = True

        self.simple_category_combo = self.add_combo(grid2,
            formats.SPONSORBLOCK_CATEGORY_LIST,
            None,
            1, 0, 1, 1,
        )
        self.simple_category_combo.set_active(0)
        if not custom_flag:
            self.simple_category_combo.set_sensitive(False)

        label2 = self.add_label(grid2,
            _('Action type'),
            2, 0, 1, 1,
        )
        label2.set_hexpand(False)

        self.simple_action_combo = self.add_combo(grid2,
            formats.SPONSORBLOCK_ACTION_LIST,
            None,
            3, 0, 1, 1,
        )
        self.simple_action_combo.set_active(0)
        if not custom_flag:
            self.simple_action_combo.set_sensitive(False)

        label3 = self.add_label(grid2,
            _('Start (timestamp or seconds)'),
            0, 1, 1, 1,
        )
        label3.set_hexpand(False)

        self.simple_slice_start_entry = self.add_entry(grid2,
            None,
            1, 1, 1, 1,
        )
        if not custom_flag:
            self.simple_slice_start_entry.set_sensitive(False)

        label4 = self.add_label(grid2,
            _('Stop (optional)'),
            2, 1, 1, 1,
        )
        label4.set_hexpand(False)

        self.simple_slice_stop_entry = self.add_entry(grid2,
            None,
            3, 1, 1, 1,
        )
        if not custom_flag:
            self.simple_slice_stop_entry.set_sensitive(False)

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid3 = self.add_secondary_grid(grid, 0, 4, grid_width, 1)

        self.simple_add_slice_button = Gtk.Button(_('Add slice'))
        grid3.attach(self.simple_add_slice_button, 0, 2, 1, 1)
        self.simple_add_slice_button.set_hexpand(True)
        self.simple_add_slice_button.connect(
            'clicked',
            self.on_add_slice_clicked,
        )
        if not custom_flag:
            self.simple_add_slice_button.set_sensitive(False)

        self.simple_delete_slice_button = Gtk.Button(_('Delete slice'))
        grid3.attach(self.simple_delete_slice_button, 1, 2, 1, 1)
        self.simple_delete_slice_button.set_hexpand(True)
        self.simple_delete_slice_button.connect(
            'clicked',
            self.on_delete_slice_clicked,
            treeview,
        )
        if not custom_flag:
            self.simple_delete_slice_button.set_sensitive(False)

        self.simple_show_settings_button \
        = Gtk.Button(_('SponsorBlock settings'))
        grid3.attach(self.simple_show_settings_button, 2, 2, 1, 1)
        self.simple_show_settings_button.set_hexpand(True)
        self.simple_show_settings_button.connect(
            'clicked',
            self.on_slice_settings_clicked,
        )

        self.simple_clear_slice_button = Gtk.Button(_('Clear list'))
        grid3.attach(self.simple_clear_slice_button, 3, 2, 1, 1)
        self.simple_clear_slice_button.set_hexpand(True)
        self.simple_clear_slice_button.connect(
            'clicked',
            self.on_clear_slice_clicked,
        )
        if not custom_flag:
            self.simple_clear_slice_button.set_sensitive(False)


    def setup_slices_tab_update_treeview(self):

        """ Called by self.setup_slices_tab().

        Fills or updates the treeview.
        """

        self.simple_slice_mode_liststore.clear()

        # Add slice data to the treeview, one row at a time
        for mini_dict in self.retrieve_val('slice_list'):

            if 'category' in mini_dict:
                category = mini_dict['category']
            else:
                category = 'n/a'

            if 'action' in mini_dict:
                action = mini_dict['action']
            else:
                action = 'n/a'

            if 'start_time' in mini_dict:
                start_time = mini_dict['start_time']
            else:
                start_time = 'n/a'

            if 'stop_time' in mini_dict \
            and mini_dict['stop_time'] is not None:
                stop_time = mini_dict['stop_time']
            else:
                stop_time = 'n/a'

            self.simple_slice_mode_liststore.append(
                [ category, action, str(start_time), str(stop_time) ],
            )


    def setup_optimise_tab_set_sensitive(self, sens_flag):

        """Called by self.setup_optimise_tab() and various callbacks.

        (De)sensitises all widgets in the tab, as required.

        Args:

            sens_flag (bool): True to sensitise widgets, False to desensitise
                them

        """

        self.seek_flag_checkbutton.set_sensitive(sens_flag)
        self.tuning_film_flag_checkbutton.set_sensitive(sens_flag)
        self.tuning_animation_flag_checkbutton.set_sensitive(sens_flag)
        self.tuning_grain_flag_checkbutton.set_sensitive(sens_flag)
        self.tuning_still_image_flag_checkbutton.set_sensitive(sens_flag)
        self.tuning_fast_decode_flag_checkbutton.set_sensitive(sens_flag)

        if not self.retrieve_val('rate_factor'):
            self.profile_flag_checkbutton.set_sensitive(False)
        else:
            self.profile_flag_checkbutton.set_sensitive(sens_flag)

        self.fast_start_flag_checkbutton.set_sensitive(sens_flag)

        self.limit_flag_checkbutton.set_sensitive(sens_flag)
        self.tuning_zero_latency_flag_checkbutton.set_sensitive(sens_flag)

        if not self.retrieve_val('limit_flag'):
            self.limit_mbps_spinbutton.set_sensitive(False)
        else:
            self.limit_mbps_spinbutton.set_sensitive(sens_flag)

        if not self.retrieve_val('limit_flag'):
            self.limit_buffer_spinbutton.set_sensitive(False)
        else:
            self.limit_buffer_spinbutton.set_sensitive(sens_flag)


    def setup_videos_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Videos' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: FFmpeg options > Videos'
        )

        tab, grid = self.add_notebook_tab(_('_Videos'))
        grid_width = 2

        # List of videos to be processed
        self.add_label(grid,
            '<u>' + _('List of videos to be processed') + '</u>',
            0, 0, grid_width, 1,
        )

        # (GenericConfigWin.add_treeview() doesn't support multiple columns, so
        #   we'll do everything ourselves)
        frame = Gtk.Frame()
        grid.attach(frame, 0, 1, grid_width, 1)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)

        treeview = Gtk.TreeView()
        scrolled.add(treeview)
        treeview.set_headers_visible(True)

        for i, column_title in enumerate(
            [ '#', _('Video'), _('Thumbnail'), _('Name') ]
        ):
            if i == 1 or i == 2:
                renderer_toggle = Gtk.CellRendererToggle()
                column_toggle = Gtk.TreeViewColumn(
                    column_title,
                    renderer_toggle,
                    active=i,
                )
                treeview.append_column(column_toggle)
                column_toggle.set_resizable(False)
            else:
                renderer_text = Gtk.CellRendererText()
                column_text = Gtk.TreeViewColumn(
                    column_title,
                    renderer_text,
                    text=i,
                )
                treeview.append_column(column_text)
                column_text.set_resizable(True)

        self.video_liststore = Gtk.ListStore(str, bool, bool, str)
        treeview.set_model(self.video_liststore)

        # Allow drag and drop from the Video Catalogue, or an external
        #   application, hoping to receive full paths to a video/audio file
        #   and/or URLs, which are associated with a media.Video object
        scrolled.connect(
            'drag-data-received',
            self.on_video_drag_data_received,
        )
        # (Without this line, we get Gtk warnings on some systems)
        scrolled.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
        # (Continuing)
        scrolled.drag_dest_set_target_list(None)
        scrolled.drag_dest_add_text_targets()

        # Initialise the list
        self.setup_videos_tab_update_treeview()

        # Add editing buttons
        button = Gtk.Button()
        grid.attach(button, 0, 2, 1, 1)
        button.set_label(_('Show video properties and timestamps'))
        button.connect(
            'clicked',
            self.on_video_show_button_clicked,
            treeview,
        )

        button2 = Gtk.Button()
        grid.attach(button2, 1, 2, 1, 1)
        button2.set_label(_('Remove video from list'))
        button2.connect(
            'clicked',
            self.on_video_remove_button_clicked,
            treeview,
        )


    def setup_videos_tab_update_treeview(self):

        """Called by self.setup_videos_tab().

        Fills or updates the treeview.
        """

        self.video_liststore.clear()

        # Sort the video list by .dbid (so the Videos tab looks nice)
        self.video_list.sort(key=lambda x: x.dbid)

        # Add a row for each video in the list
        for video_obj in self.video_list:
            self.setup_videos_tab_add_row(video_obj)


    def setup_videos_tab_add_row(self, video_obj):

        """Called by self.setup_videos_tab_update_treeview().

        Adds a row to the treeview for a specified media.Video object.

        Args:

            video_obj (media.Video): The video to add

        """

        if ttutils.find_thumbnail(self.app_obj, video_obj):
            thumb_flag = True
        else:
            thumb_flag = True

        if video_obj.dummy_flag:

            # Special case: 'dummy' video objects (those downloaded in the
            #   Classic Mode tab) use different IVs
            if video_obj.dummy_path is not None \
            and os.path.isfile(video_obj.dummy_path):
                dl_flag = True
            else:
                dl_flag = False

            self.video_liststore.append(
                [
                    'n/a',
                    dl_flag,
                    thumb_flag,
                    video_obj.dummy_path,
                ],
            )

        else:

            # All other media.Video objects
            self.video_liststore.append(
                [
                    str(video_obj.dbid),
                    video_obj.dl_flag,
                    thumb_flag,
                       video_obj.name,
                ],
            )


    # (Tab support functions)


    def update_system_cmd(self):

        """Called after any widget is manipulated.

        Updates the contents of the textview showing a specimen system command,
        incorporating the modified value.
        """

        # This particular call returns a list inside a tuple, for no obvious
        #   reason (and an identical call from ProcessManager.process_video()
        #   does not)
        # Don't know why, but the FFmpeg system command, as a list, is at
        #   [0][2])
        result_list = self.edit_obj.get_system_cmd(
            self.app_obj,
            None,           # Use a specimen source file
            None,           # ...and specimen timestamps
            None,
            None,
            None,
            self.edit_dict,
        ),

        if not result_list:
            self.results_textbuffer.set_text('')

        else:
            text = ' '.join(result_list[0][2])
            if self.retrieve_val('output_mode') == 'slice':

                # Show the concatenation command on a second line
                concat_list = [
                    self.app_obj.ffmpeg_manager_obj.get_executable(),
                    '-safe',
                    '0',
                    '-f',
                    'concat',
                    '-i',
                    'clips.txt',
                    '-c',
                    'copy',
                    result_list[0][2][-1],      # Path to the output file
                ]

                text += '\n' + ' '.join(concat_list)

            self.results_textbuffer.set_text(text)


    # Callback class methods


    def on_add_slice_clicked(self, button):

        """Called from a callback in self.setup_settings_tab_slice_grid().

        In simple mode, called from a callback in self.setup_slices_tab().

        Adds a new slice to the video's slice list.

        Args:

            button (Gtk.Button): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' FFmpeg options > Slices'
        )

        if not self.app_obj.simple_ffmpeg_options_flag:

            tree_iter = self.category_combo.get_active_iter()
            model = self.category_combo.get_model()
            category = model[tree_iter][0]

            tree_iter2 = self.action_combo.get_active_iter()
            model2 = self.action_combo.get_model()
            action_type = model2[tree_iter2][0]

            start_time = ttutils.strip_whitespace(
                self.slice_start_entry.get_text(),
            )

            stop_time = ttutils.strip_whitespace(
                self.slice_stop_entry.get_text(),
            )

        else:

            tree_iter = self.simple_category_combo.get_active_iter()
            model = self.simple_category_combo.get_model()
            category = model[tree_iter][0]

            tree_iter2 = self.simple_action_combo.get_active_iter()
            model2 = self.simple_action_combo.get_model()
            action_type = model2[tree_iter2][0]

            start_time = ttutils.strip_whitespace(
                self.simple_slice_start_entry.get_text(),
            )

            stop_time = ttutils.strip_whitespace(
                self.simple_slice_stop_entry.get_text(),
            )

        # Do nothing if specified timestamps aren't valid ('stop_time' is NOT
        #   optional)
        start_time = float(
            ttutils.timestamp_convert_to_seconds(self.app_obj, start_time),
        )

        if stop_time == '':
            stop_time = None
        else:
            stop_time = float(
                ttutils.timestamp_convert_to_seconds(self.app_obj, stop_time),
            )

        try:
            ignore = float(start_time)
            if stop_time is not None:
                ignore = float(stop_time)

        except:
            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('Invalid start/stop times'),
                'error',
                'ok',
                self,           # Parent window is this window
            )

            return

        if stop_time is not None and stop_time <= start_time:
            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('Invalid start/stop times'),
                'error',
                'ok',
                self,           # Parent window is this window
            )

            return

        # Compile the mini-dictionary in the format described by
        #   media.Video.__init__()
        mini_dict = {
            'category': category,
            'action': action_type,
            'start_time': start_time,
            'stop_time': stop_time,
            'duration': 0,
        }

        # Add it to the list
        slice_list = self.retrieve_val('slice_list')
        slice_list.append(mini_dict)
        slice_list = list(sorted(slice_list, key=lambda x:x['start_time']))
        self.edit_dict['slice_list'] = slice_list

        # Show changes, and empty entry boxes
        if not self.app_obj.simple_ffmpeg_options_flag:

            self.setup_settings_tab_update_slice_treeview()
            self.slice_start_entry.set_text('')
            self.slice_stop_entry.set_text('')

        else:

            self.setup_slices_tab_update_treeview()
            self.simple_slice_start_entry.set_text('')
            self.simple_slice_stop_entry.set_text('')

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_add_timestamp_clicked(self, button):

        """Called from a callback in self.setup_settings_tab_clip_grid().

        In simple mode, called from a callback in self.setup_clips_tab().

        Adds a new timestamp to the video's timestamp list, optionally with a
        clip title.

        Args:

            button (Gtk.Button): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' FFmpeg options > Clips'
        )

        if not self.app_obj.simple_ffmpeg_options_flag:

            start_stamp = ttutils.strip_whitespace(
                self.start_stamp_entry.get_text(),
            )
            stop_stamp = ttutils.strip_whitespace(
                self.stop_stamp_entry.get_text(),
            )
            clip_title = ttutils.strip_whitespace(
                self.clip_title_entry.get_text(),
            )

        else:

            start_stamp = ttutils.strip_whitespace(
                self.simple_start_stamp_entry.get_text(),
            )
            stop_stamp = ttutils.strip_whitespace(
                self.simple_stop_stamp_entry.get_text(),
            )
            clip_title = ttutils.strip_whitespace(
                self.simple_clip_title_entry.get_text(),
            )

        # (Values are stored as None, rather than empty strings)
        if stop_stamp == '':
            stop_stamp = None

        if clip_title == '':
            clip_title = None

        # Do nothing if specified timestamps aren't valid ('stop_stamp' is
        #   optional)
        regex = '^' + self.app_obj.timestamp_regex + '$'
        if re.search(regex, start_stamp) \
        and (stop_stamp is None or re.search(regex, stop_stamp)) \
        and ttutils.timestamp_compare(self.app_obj, start_stamp, stop_stamp):

            # Add leading zeroes to the minutes and seconds components, so
            #   that .stamp_list gets sorted correctly (and doesn't look
            #   weird)
            start_stamp = ttutils.timestamp_format(self.app_obj, start_stamp)
            if stop_stamp is not None:
                stop_stamp = ttutils.timestamp_format(self.app_obj, stop_stamp)

            # Timestamps stored in groups of three, in the form
            #   (start_stamp, stop_stamp, clip_title)
            # If a group with the same 'start_stamp' timestamp already exists,
            #   don't replace it; allow duplicates (as the user may actually
            #   want that)
            split_list = self.retrieve_val('split_list')
            split_list.append([ start_stamp, stop_stamp, clip_title ])
            split_list.sort()
            self.edit_dict['split_list'] = split_list

            # (Show changes, and update entry boxes. 'stop_stamp', if
            #   specified, becomes 'start_stamp' for the next group)
            if not self.app_obj.simple_ffmpeg_options_flag:

                self.setup_settings_tab_update_clip_treeview()

                if stop_stamp is not None:
                    self.start_stamp_entry.set_text(
                        ttutils.timestamp_add_second(self.app_obj, stop_stamp),
                    )
                else:
                    self.start_stamp_entry.set_text('')

                self.stop_stamp_entry.set_text('')
                self.clip_title_entry.set_text('')

            else:

                self.setup_clips_tab_update_treeview()
                if stop_stamp is not None:
                    self.simple_start_stamp_entry.set_text(
                        ttutils.timestamp_add_second(self.app_obj, stop_stamp),
                    )
                else:
                    self.simple_start_stamp_entry.set_text('')

                self.simple_stop_stamp_entry.set_text('')
                self.simple_clip_title_entry.set_text('')

        else:

            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('Invalid timestamp(s)'),
                'error',
                'ok',
                self,           # Parent window is this window
                )

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_audio_flag_checkbutton_toggled(self, checkbutton):

        """Called by callback in self.setup_settings_tab().

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if not checkbutton.get_active():

            self.edit_dict['audio_flag'] = False

            self.audio_bitrate_spinbutton.set_sensitive(False)

        else:

            self.edit_dict['audio_flag'] = True

            if self.retrieve_val('input_mode') == 'video':
                self.audio_bitrate_spinbutton.set_sensitive(True)
            else:
                self.audio_bitrate_spinbutton.set_sensitive(False)

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_change_file_ext_entry_changed(self, entry):

        """Called by callback in self.setup_file_tab().

        Args:

            entry (Gtk.Entry): The widget clicked

        """

        value = entry.get_text()

        self.edit_dict['change_file_ext'] = value
        if value == '':

            self.delete_original_flag_checkbutton.set_active(False)
            self.delete_original_flag_checkbutton.set_sensitive(False)

        else:
            self.delete_original_flag_checkbutton.set_sensitive(True)


        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_clear_slice_clicked(self, button):

        """Called from a callback in self.setup_settings_tab_slice_grid().

        Empties the slice list.

        Args:

            button (Gtk.Button): The widget clicked

        """

        self.edit_dict['slice_list'] = []
        if not self.app_obj.simple_ffmpeg_options_flag:
            self.setup_settings_tab_update_slice_treeview()
        else:
            self.setup_slices_tab_update_treeview()

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_clear_timestamp_clicked(self, button):

        """Called from a callback in self.setup_settings_tab_clip_grid().

        Empties the timestamp list.

        Args:

            button (Gtk.Button): The widget clicked

        """

        self.edit_dict['split_list'] = []
        if not self.app_obj.simple_ffmpeg_options_flag:
            self.setup_settings_tab_update_clip_treeview()
        else:
            self.setup_clips_tab_update_treeview()

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_clip_prefs_clicked(self, button):

        """Called from a callback in self.setup_settings_tab_clip_grid() and
        .setup_clips_tab().

        Opens the preferences window to show clip settings.

        Args:

            button (Gtk.Button): The widget clicked

        """

        SystemPrefWin(self.app_obj, 'clips')


    def on_clone_options_clicked(self, button):

        """Called by callback in self.setup_name_tab().

        Args:

            button (Gtk.Button): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' FFmpeg options > Name'
        )

        self.app_obj.dialogue_manager_obj.show_msg_dialogue(
            _(
            'This procedure cannot be reversed. Are you sure you want to' \
            + ' continue?',
            ),
            'question',
            'yes-no',
            self,           # Parent window is this window
            {
                'yes': 'clone_ffmpeg_options_from_window',
                'data': [self, self.edit_obj],
            },
        )


    def on_delete_slice_clicked(self, button, treeview):

        """Called from a callback in self.setup_settings_tab_slice_grid() and
        .setup_slices_tab().

        Deletes the selected slices from the slice list.

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeVies): The treeview displaying the slice list

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if not path_list:

            return

        # (Multiple selection is not enabled)
        this_iter = model.get_iter(path_list[0])
        if this_iter is None:

            return

        category = model[this_iter][0]
        action_type = model[this_iter][1]
        start_time = float(model[this_iter][2])
        stop_time = float(model[this_iter][3])

        # Slices are stored as a list of mini-dictionaries, in the form
        #   described by media.Video.__init__()
        # Walk the list, and delete the first matching mini-dictionary
        slice_list = self.retrieve_val('slice_list')
        mod_list = []
        match_flag = False

        for mini_dict in slice_list:

            if not match_flag \
            and mini_dict['category'] == category \
            and mini_dict['action'] == action_type \
            and mini_dict['start_time'] == start_time \
            and mini_dict['stop_time'] == stop_time:
                match_flag = True   # Delete this one
            else:
                mod_list.append(mini_dict)

        self.edit_dict['slice_list'] = mod_list

        # (Show changes)
        if not self.app_obj.simple_ffmpeg_options_flag:
            self.setup_settings_tab_update_slice_treeview()
        else:
            self.setup_slices_tab_update_treeview()

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_delete_timestamp_clicked(self, button, treeview):

        """Called from a callback in self.setup_settings_tab_clip_grid() and
        .setup_clips_tab().

        Deletes the selected timestamps from the timestamp list.

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeVies): The treeview displaying the timestamp list

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if not path_list:

            return

        # (Multiple selection is not enabled)
        this_iter = model.get_iter(path_list[0])
        if this_iter is None:

            return

        start_stamp = model[this_iter][0]
        stop_stamp = model[this_iter][1]
        clip_title = model[this_iter][2]

        # Timestamps stored in groups of three, in the form
        #   (start_stamp, stop_stamp, clip_title)
        # Walk the list, and delete the first matchng group
        split_list = self.retrieve_val('split_list')
        mod_list = []
        match_flag = False

        for mini_list in split_list:

            if not match_flag \
            and mini_list[0] == start_stamp \
            and mini_list[1] == stop_stamp \
            and mini_list[2] == clip_title:
                match_flag = True   # Delete this one
            else:
                mod_list.append(mini_list)

        self.edit_dict['split_list'] = mod_list

        # (Show changes)
        if not self.app_obj.simple_ffmpeg_options_flag:
            self.setup_settings_tab_update_clip_treeview()
        else:
            self.setup_clips_tab_update_treeview()

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_input_mode_radiobutton_toggled(self, radiobutton):

        """Called by callback in self.setup_settings_tab().

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

        """

        if self.input_mode_radiobutton.get_active():

            self.edit_dict['input_mode'] = 'video'

            self.output_mode_radiobutton.set_active(True)
            self.output_mode_radiobutton.set_sensitive(True)
            self.output_mode_radiobutton2.set_sensitive(True)
            self.output_mode_radiobutton3.set_sensitive(True)
            self.output_mode_radiobutton4.set_sensitive(True)
            self.output_mode_radiobutton5.set_sensitive(False)

            self.audio_flag_checkbutton.set_sensitive(True)
            if not self.retrieve_val('audio_flag'):
                self.audio_bitrate_spinbutton.set_sensitive(False)
            else:
                self.audio_bitrate_spinbutton.set_sensitive(True)

        else:

            self.edit_dict['input_mode'] = 'thumb'

            self.output_mode_radiobutton4.set_active(True)
            self.output_mode_radiobutton.set_sensitive(False)
            self.output_mode_radiobutton2.set_sensitive(False)
            self.output_mode_radiobutton3.set_sensitive(False)
            self.output_mode_radiobutton4.set_sensitive(False)
            self.output_mode_radiobutton5.set_sensitive(True)

            self.audio_flag_checkbutton.set_sensitive(False)
            self.audio_bitrate_spinbutton.set_sensitive(False)

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_limit_flag_checkbutton_toggled(self, checkbutton):

        """Called by callback in self.setup_optimise_tab().

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if not checkbutton.get_active():

            self.edit_dict['limit_flag'] = False

            self.limit_mbps_spinbutton.set_sensitive(False)
            self.limit_buffer_spinbutton.set_sensitive(False)

        else:

            self.edit_dict['audio_flag'] = True

            self.limit_mbps_spinbutton.set_sensitive(True)
            self.limit_buffer_spinbutton.set_sensitive(True)

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_output_mode_radiobutton_toggled(self, radiobutton, grid_width):

        """Called by callback in self.setup_settings_tab().

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

            grid_width (int): The width of self.settings_grid

        """

        old_value = self.retrieve_val('output_mode')
        if old_value == 'h264':
            self.settings_grid.remove(self.h264_grid)
        elif old_value == 'gif':
            self.settings_grid.remove(self.gif_grid)
        elif old_value == 'split':
            self.settings_grid.remove(self.clip_grid)
        elif old_value == 'slice':
            self.settings_grid.remove(self.slice_grid)
        elif old_value == 'merge':
            self.settings_grid.remove(self.merge_grid)
        else:
            self.settings_grid.remove(self.thumb_grid)

        if self.output_mode_radiobutton.get_active():

            self.edit_dict['output_mode'] = 'h264'

            self.settings_grid.attach(self.h264_grid, 0, 2, grid_width, 1)
            self.setup_file_tab_set_sensitive(True)
            self.setup_optimise_tab_set_sensitive(True)

        elif self.output_mode_radiobutton2.get_active():

            self.edit_dict['output_mode'] = 'gif'

            self.settings_grid.attach(self.gif_grid, 0, 2, grid_width, 1)
            self.setup_file_tab_set_sensitive(True)
            self.setup_optimise_tab_set_sensitive(False)

        elif self.output_mode_radiobutton3.get_active():

            self.edit_dict['output_mode'] = 'split'

            self.settings_grid.attach(self.clip_grid, 0, 2, grid_width, 1)
            self.setup_file_tab_set_sensitive(False)
            self.setup_optimise_tab_set_sensitive(False)

        elif self.output_mode_radiobutton4.get_active():

            self.edit_dict['output_mode'] = 'slice'

            self.settings_grid.attach(self.slice_grid, 0, 2, grid_width, 1)
            self.setup_file_tab_set_sensitive(False)
            self.setup_optimise_tab_set_sensitive(False)

        elif self.output_mode_radiobutton5.get_active():

            self.edit_dict['output_mode'] = 'merge'

            self.settings_grid.attach(self.merge_grid, 0, 2, grid_width, 1)
            self.setup_file_tab_set_sensitive(True)
            self.setup_optimise_tab_set_sensitive(False)

        elif self.output_mode_radiobutton6.get_active():

            self.edit_dict['output_mode'] = 'thumb'

            self.settings_grid.attach(self.thumb_grid, 0, 2, grid_width, 1)
            self.setup_file_tab_set_sensitive(True)
            self.setup_optimise_tab_set_sensitive(False)

        self.show_all()

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_palette_mode_radiobutton_toggled(self, radiobutton):

        """Called by callback in self.setup_settings_tab_gif_grid().

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

        """

        if self.palette_mode_radiobutton.get_active():
            self.edit_dict['palette_mode'] = 'faster'
        else:
            self.edit_dict['palette_mode'] = 'better'

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_quality_mode_radiobutton_toggled(self, radiobutton):

        """Called by callback in self.setup_settings_tab_h264_grid().

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

        """

        if self.quality_mode_radiobutton.get_active():

            self.edit_dict['quality_mode'] = 'crf'

            self.rate_factor_scale.set_sensitive(True)
            self.dummy_file_combo.set_sensitive(False)

        else:

            self.edit_dict['quality_mode'] = 'abr'

            self.rate_factor_scale.set_sensitive(False)
            self.dummy_file_combo.set_sensitive(True)

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_rate_factor_scale_changed(self, scale):

        """Called by callback in self.setup_settings_tab_h264_grid().

        Args:

            scale (Gtk.Scale): The widget clicked

        """

        value = int(self.rate_factor_scale.get_value())

        self.edit_dict['rate_factor'] = value

        if not value:
            self.profile_flag_checkbutton.set_sensitive(False)
        else:
            self.profile_flag_checkbutton.set_sensitive(True)

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_regex_match_filename_entry_changed(self, entry):

        """Called by callback in self.setup_file_tab().

        Args:

            entry (Gtk.Entry): The widget clicked

        """

        value = entry.get_text()

        self.edit_dict['regex_match_filename'] = value
        if value == '':

            self.regex_apply_subst_entry.set_text('')
            self.regex_apply_subst_entry.set_sensitive(False)

        else:

            self.regex_apply_subst_entry.set_sensitive(True)

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_reset_options_clicked(self, button):

        """Called by callback in self.setup_name_tab().

        Args:

            button (Gtk.Button): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' FFmpeg options > Name'
        )

        self.app_obj.dialogue_manager_obj.show_msg_dialogue(
            _(
            'This procedure cannot be reversed. Are you sure you want to' \
            + ' continue?',
            ),
            'question',
            'yes-no',
            self,           # Parent window is this window
            {
                'yes': 'reset_ffmpeg_options',
                # (Reset this edit window, if the user clicks 'yes')
                'data': [self],
            },
        )


    def on_simple_options_clicked(self, button):

        """Called by callback in self.setup_name_tab().

        Args:

            button (Gtk.Button): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' FFmpeg options > Name'
        )

        redraw_flag = False
        if not self.app_obj.simple_ffmpeg_options_flag:

            self.app_obj.set_simple_ffmpeg_options_flag(True)

            if not self.edit_dict:

                # User has not changed any options, so redraw the window to
                #   show the same options.OptionsManager object
                self.reset_with_new_edit_obj(self.edit_obj)

            else:

                # User has already changed some options. We don't want to lose
                #   them, so wait for the window to close and be re-opened,
                #   before switching between simple/advanced options
                redraw_flag = True

                self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                    _(
                    'Fewer FFmpeg options will be visible when you click the' \
                    + ' \'Apply\' or \'Reset\' buttons (or when you close' \
                    + ' and then re-open the window)',
                    ),
                    'info',
                    'ok',
                    self,           # Parent window is this window
                )

                button.set_label(
                    _('Show more FFmpeg options (when window re-opens)'),
                )

        else:

            self.app_obj.set_simple_ffmpeg_options_flag(False)

            if not self.edit_dict:

                self.reset_with_new_edit_obj(self.edit_obj)

            else:

                redraw_flag = True

                self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                    _(
                    'More FFmpeg options will be visible when you click the' \
                    + ' \'Apply\' or \'Reset\' buttons (or when you close' \
                    + ' and then re-open the window)',
                    ),
                    'info',
                    'ok',
                    self,           # Parent window is this window
                )

                button.set_label(
                    _('Show fewer FFmpeg options (when window re-opens)'),
                )

        if redraw_flag:

            # Discard the list of videos, so that this becomes an ordinary
            #   edit window, with an 'OK' button that stores changes (and no
            #   'Process files' button that starts a process operation)
            self.video_list = []

            self.ok_button.set_label(_('OK'))
            self.ok_button.get_child().set_width_chars(10)
            self.ok_button.set_tooltip_text(
                _('Apply changes'),
            )


    def on_simple_slice_toggled(self, checkbutton):

        """Called by callback in self.setup_slices_tab().

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if not checkbutton.get_active():

            self.edit_dict['output_mode'] = 'h264'
            radio_sens_flag = False
            sens_flag = False

        else:

            # (Update the corresponding checkbutton in the 'Clips' tab)
            self.simple_split_mode_checkbutton.set_active(False)

            # (Respond to clicks on this checkbutton)
            self.edit_dict['output_mode'] = 'slice'
            radio_sens_flag = True

            if self.retrieve_val('slice_mode') == 'video':
                sens_flag = False
            else:
                sens_flag = True

        # (De)sensitise widgets
        self.simple_slice_mode_radiobutton.set_sensitive(radio_sens_flag)
        self.simple_slice_mode_radiobutton2.set_sensitive(radio_sens_flag)
        self.simple_category_combo.set_sensitive(sens_flag)
        self.simple_action_combo.set_sensitive(sens_flag)
        self.simple_slice_start_entry.set_sensitive(sens_flag)
        self.simple_slice_stop_entry.set_sensitive(sens_flag)

        self.simple_add_slice_button.set_sensitive(sens_flag)
        self.simple_delete_slice_button.set_sensitive(sens_flag)
        self.simple_show_settings_button.set_sensitive(True)
        self.simple_clear_slice_button.set_sensitive(sens_flag)

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_simple_split_toggled(self, checkbutton):

        """Called by callback in self.setup_clips_tab().

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if not checkbutton.get_active():

            self.edit_dict['output_mode'] = 'h264'
            radio_sens_flag = False
            sens_flag = False

        else:

            # (Update the corresponding checkbutton in the 'Slices' tab)
            self.simple_slice_mode_checkbutton.set_active(False)

            # (Respond to clicks on this checkbutton)
            self.edit_dict['output_mode'] = 'split'
            radio_sens_flag = True

            if self.retrieve_val('split_mode') == 'video':
                sens_flag = False
            else:
                sens_flag = True

        # (De)sensitise widgets
        self.simple_split_mode_radiobutton.set_sensitive(radio_sens_flag)
        self.simple_split_mode_radiobutton2.set_sensitive(radio_sens_flag)
        self.simple_start_stamp_entry.set_sensitive(sens_flag)
        self.simple_stop_stamp_entry.set_sensitive(sens_flag)
        self.simple_clip_title_entry.set_sensitive(sens_flag)
        self.simple_add_timestamp_button.set_sensitive(sens_flag)
        self.simple_delete_timestamp_button.set_sensitive(sens_flag)
        self.simple_show_prefs_button.set_sensitive(True)
        self.simple_clear_timestamp_button.set_sensitive(sens_flag)

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_slice_mode_radiobutton_toggled(self, radiobutton):

        """Called by callback in self.setup_settings_tab_slice_grid().

        In simple mode, called from a callback in self.setup_slices_tab().

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

        """

        if not self.app_obj.simple_ffmpeg_options_flag:

            if self.slice_mode_radiobutton.get_active():

                self.edit_dict['slice_mode'] = 'video'
                sens_flag = False

            else:

                self.edit_dict['slice_mode'] = 'custom'
                sens_flag = True

            # (De)sensitise widgets
            self.category_combo.set_sensitive(sens_flag)
            self.action_combo.set_sensitive(sens_flag)
            self.slice_start_entry.set_sensitive(sens_flag)
            self.slice_stop_entry.set_sensitive(sens_flag)
            self.add_slice_button.set_sensitive(sens_flag)
            self.delete_slice_button.set_sensitive(sens_flag)
            self.show_settings_button.set_sensitive(True)
            self.clear_slice_button.set_sensitive(sens_flag)

        else:

            if self.simple_slice_mode_radiobutton.get_active():

                self.edit_dict['slice_mode'] = 'video'
                sens_flag = False

            else:

                self.edit_dict['slice_mode'] = 'custom'
                if self.retrieve_val('output_mode') == 'slice':
                    sens_flag = True
                else:
                    sens_flag = False

            # (De)sensitise widgets
            self.simple_category_combo.set_sensitive(sens_flag)
            self.simple_action_combo.set_sensitive(sens_flag)
            self.simple_slice_start_entry.set_sensitive(sens_flag)
            self.simple_slice_stop_entry.set_sensitive(sens_flag)
            self.simple_add_slice_button.set_sensitive(sens_flag)
            self.simple_delete_slice_button.set_sensitive(sens_flag)
            self.simple_show_settings_button.set_sensitive(True)
            self.simple_clear_slice_button.set_sensitive(sens_flag)

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_slice_settings_clicked(self, button):

        """Called from a callback in self.setup_settings_tab_slice_grid() and
        .setup_slices_tab().

        Opens the preferences window to show slice settings.

        Args:

            button (Gtk.Button): The widget clicked

        """

        SystemPrefWin(self.app_obj, 'slices')


    def on_split_mode_radiobutton_toggled(self, radiobutton):

        """Called by callback in self.setup_settings_tab_clip_grid().

        In simple mode, called from a callback in self.setup_clips_tab().

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

        """

        if not self.app_obj.simple_ffmpeg_options_flag:

            if self.split_mode_radiobutton.get_active():

                self.edit_dict['split_mode'] = 'video'
                sens_flag = False

            else:

                self.edit_dict['split_mode'] = 'custom'
                sens_flag = True

            # (De)sensitise widgets
            self.start_stamp_entry.set_sensitive(sens_flag)
            self.stop_stamp_entry.set_sensitive(sens_flag)
            self.clip_title_entry.set_sensitive(sens_flag)
            self.add_timestamp_button.set_sensitive(sens_flag)
            self.delete_timestamp_button.set_sensitive(sens_flag)
            self.show_prefs_button.set_sensitive(True)
            self.clear_timestamp_button.set_sensitive(sens_flag)

        else:

            if self.simple_split_mode_radiobutton.get_active():

                self.edit_dict['split_mode'] = 'video'
                sens_flag = False

            else:

                self.edit_dict['split_mode'] = 'custom'
                if self.retrieve_val('output_mode') == 'split':
                    sens_flag = True
                else:
                    sens_flag = False

            # (De)sensitise widgets
            self.simple_start_stamp_entry.set_sensitive(sens_flag)
            self.simple_stop_stamp_entry.set_sensitive(sens_flag)
            self.simple_clip_title_entry.set_sensitive(sens_flag)
            self.simple_add_timestamp_button.set_sensitive(sens_flag)
            self.simple_delete_timestamp_button.set_sensitive(sens_flag)
            self.simple_show_prefs_button.set_sensitive(True)
            self.simple_clear_timestamp_button.set_sensitive(sens_flag)

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_video_drag_data_received(self, widget, context, x, y, data, info,
    time):

        """Called from callback in self.setup_videos_tab().

        This function is required for detecting when the user drags and drops
        data into the Videos tab.

        If the data contains full paths to a video/audio file and/or URLs,
        then we can search the media data registry, looking for matching
        media.Video objects.

        Those objects can then be added to self.video_list.

        Args:

            widget (mainwin.MainWin): The widget into which something has been
                dragged

            drag_context (GdkX11.X11DragContext): Data from the drag procedure

            x, y (int): Where the drop happened

            data (Gtk.SelectionData): The object to be filled with drag data

            info (int): Info that has been registered with the target in the
                Gtk.TargetList

            time (int): A timestamp

        """

        text = None
        if info == 0:
            text = data.get_text()

        if text is not None:

            # Hopefully, 'text' contains one or more valid URLs or paths to
            #   video/audio files
            line_list = text.splitlines()
            mod_list = []

            for line in line_list:
                mod_line = ttutils.strip_whitespace(urllib.parse.unquote(line))
                if mod_line != '':

                    # On Linux, URLs are received as expected, but paths to
                    #   media data files are received as 'file://PATH'
                    match = re.search(r'^file\:\/\/(.*)', mod_line)
                    if match:
                        mod_list.append(match.group(1))
                    else:
                        mod_list.append(mod_line)

            # The True argument means to include 'dummy' media.Videos from the
            #   Classic Mode tab in the search
            video_list = self.app_obj.retrieve_videos_from_db(mod_list, True)

            # (Remember if the video list is currently empty, or not)
            old_size = len(self.video_list)

            # Add videos to the list, but don't add duplicates
            for video_obj in video_list:

                if not video_obj in self.video_list:
                    self.video_list.append(video_obj)

            # Redraw the whole video list by calling this function, which also
            #   sorts self.video_list nicely
            self.setup_videos_tab_update_treeview()

            if old_size == 0 and self.video_list:

                # Replace the 'OK' button with a 'Process files' button
                self.ok_button.set_label(_('Process files'))
                self.ok_button.set_tooltip_text(
                    _('Process the files with FFmpeg'),
                )
                self.ok_button.get_child().set_width_chars(15)

        # Without this line, the user's cursor is permanently stuck in drag
        #   and drop mode
        context.finish(True, False, time)


    def on_video_remove_button_clicked(self, button, treeview):

        """Called from callback in self.setup_videos_tab().

        Removes a video from the list of videos to be processed by FFmpeg.

        If there are no videos left, this edit window reverts to its default
        state, in which we just save any changes to the FFmpeg options.

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeView): The treeview to be updated

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if not path_list:

            return

        # (Multiple selection is not enabled)
        this_iter = model.get_iter(path_list[0])
        if this_iter is None:

            return

        dbid = int(model[this_iter][0])
        for video_obj in self.video_list:

            if video_obj.dbid == dbid:
                self.video_list.remove(video_obj)
                break

        # Update the visible list
        self.setup_videos_tab_update_treeview()
        # If all videos have been removed, restore the OK button
        if not self.video_list:

            self.ok_button.set_label(_('OK'))
            self.ok_button.get_child().set_width_chars(10)
            self.ok_button.set_tooltip_text(
                _('Apply changes'),
            )


    def on_video_show_button_clicked(self, button, treeview):

        """Called from callback in self.setup_videos_tab().

        Opens the video properties window.

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeView): The treeview to be updated

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if not path_list:

            return

        # (Multiple selection is not enabled)
        this_iter = model.get_iter(path_list[0])
        if this_iter is None:

            return

        dbid = int(model[this_iter][0])
        if dbid in self.app_obj.media_reg_dict:
            VideoEditWin(
                self.app_obj,
                self.app_obj.media_reg_dict[dbid],
            )


    # (Redefined button strip callbacks)


    def on_button_apply_clicked(self, button):

        """Called from a callback in self.setup_button_strip().

        Applies any changes made by the user and re-draws the window's tabs,
        showing their new values.

        Args:

            button (Gtk.Button): The widget clicked

        """

        # Apply any changes the user has made. The True argument identifies
        #   this function as the caller, and prevents a process operation from
        #   starting
        self.apply_changes(True)

        # Remove all existing tabs from the notebook
        number = self.notebook.get_n_pages()
        if number:

            for count in range(0, number):
                self.notebook.remove_page(0)

        # Re-draw all the tabs
        self.setup_tabs()

        # Render the changes
        self.show_all()


    # (Redefined generic callbacks)


    def on_checkbutton_toggled(self, checkbutton, prop):

        """Modified form of the GenericEditWin callback, in which we
        automatically update the system command visible in the 'Name' tab.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            prop (str): The attribute in self.edit_obj to modify

        """

        if not checkbutton.get_active():
            self.edit_dict[prop] = False
        else:
            self.edit_dict[prop] = True

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_combo_with_data_changed(self, combo, prop):

        """Modified form of the GenericEditWin callback, in which we
        automatically update the system command visible in the 'Name' tab.

        Args:

            combo (Gtk.ComboBox): The widget clicked

            prop (str): The attribute in self.edit_obj to modify

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        self.edit_dict[prop] = model[tree_iter][1]

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_entry_changed(self, entry, prop):

        """Modified form of the GenericEditWin callback, in which we
        automatically update the system command visible in the 'Name' tab.

        Args:

            entry (Gtk.Entry): The widget clicked

            prop (str): The attribute in self.edit_obj to modify

        """

        self.edit_dict[prop] = entry.get_text()

        # (De)sensitise the checkbutton for 'rename_both_flag', if required
        if entry == self.add_end_filename_entry \
        or entry == self.regex_match_filename_entry:

            if self.retrieve_val('add_end_filename') == '' \
            and self.retrieve_val('regex_match_filename') == '':
                self.rename_both_flag_checkbutton.set_active(False)
                self.rename_both_flag_checkbutton.set_sensitive(False)
            else:
                self.rename_both_flag_checkbutton.set_sensitive(True)

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_spinbutton_changed(self, spinbutton, prop):

        """Modified form of the GenericEditWin callback, in which we
        automatically update the system command visible in the 'Name' tab.

        Args:

            spinbutton (Gtk.SpinkButton): The widget clicked

            prop (str): The attribute in self.edit_obj to modify

        """

        self.edit_dict[prop] = int(spinbutton.get_value())

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


    def on_textview_changed(self, textbuffer, prop):

        """Modified form of the GenericEditWin callback, in which we
        automatically update the system command visible in the 'Name' tab.

        Args:

            textbuffer (Gtk.TextBuffer): The widget modified

            prop (str): The attribute in self.edit_obj to modify

        """

        text = textbuffer.get_text(
            textbuffer.get_start_iter(),
            textbuffer.get_end_iter(),
            # Don't include hidden characters
            False,
        )

        old_value = self.retrieve_val(prop)

        if type(old_value) is list:
            self.edit_dict[prop] = text.split()
        elif type(old_value) is tuple:
            self.edit_dict[prop] = text.split()
        else:
             self.edit_dict[prop] = text

        # Update the system command in the 'Name' tab
        self.update_system_cmd()


class VideoEditWin(GenericEditWin):

    """Python class for an 'edit window' to modify values in a media.Video
    object.

    Args:

        app_obj (mainapp.TartubeApp): The main application object

        edit_obj (media.Video): The object whose attributes will be edited in
            this window

    """


    # Standard class methods


    def __init__(self, app_obj, edit_obj):

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Video properties window starts here.' \
            + ' In the main window, in the Videos tab, right-click a video' \
            + ' and select Show video > Properties...'
        )

        Gtk.Window.__init__(self, title=_('Video properties'))

        if self.is_duplicate(app_obj, edit_obj):
            return

        # IV list - class objects
        # -----------------------
        # The mainapp.TartubeApp object
        self.app_obj = app_obj
        # The media.Video object being edited
        self.edit_obj = edit_obj


        # IV list - Gtk widgets
        # ---------------------
        self.grid = None                        # Gtk.Grid
        self.notebook = None                    # Gtk.Notebook
        self.reset_button = None                # Gtk.Button
        self.apply_button = None                # Gtk.Button
        self.ok_button = None                   # Gtk.Button
        self.cancel_button = None               # Gtk.Button
        # (Non-standard widgets)
        self.apply_options_button = None        # Gtk.Button
        self.edit_options_button = None         # Gtk.Button
        self.remove_options_button = None       # Gtk.Button
        # (Widgets used in the Timestamps tab)
        self.timestamp_liststore = None         # Gtk.ListStore
        # (Widgets used in the Slices tab)
        self.slice_liststore = None             # Gtk.ListStore
        # (Widgets used in the Comments tab)
        self.comment_scrolled = None            # Gtk.ScrolledWindow
        self.comment_treeview = None            # Gtk.TreeView
        self.comment_liststore = None           # Gtk.ListStore
        self.comment_listbox = None             # Gtk.ListBox
        self.filter_entry = None                # Gtk.Entry
        self.filter_togglebutton = None         # Gtk.ToggleButon
        self.filter_author_checkbutton = None   # Gtk.CheckButton
        self.filter_comment_checkbutton = None  # Gtk.CheckButton
        self.filter_uploader_checkbutton = None # Gtk.CheckButton
        self.filter_apply_button = None         # Gtk.ToolButton
        self.filter_cancel_button = None        # Gtk.ToolButton


        # IV list - other
        # ---------------
        # Size (in pixels) of gaps between edit window widgets
        self.spacing_size = self.app_obj.default_spacing_size
        # Flag set to True if all four buttons ('Reset', 'Apply', 'Cancel' and
        #   'OK') are required, or False if just the 'OK' button is required
        self.multi_button_flag = False

        # When the user changes a value, it is not applied to self.edit_obj
        #   immediately; instead, it is stored temporarily in this dictionary
        # If the user clicks the 'OK' or 'Apply' buttons at the bottom of the
        #   window, the changes are applied to self.edit_obj
        # If the user clicks the 'Reset' or 'Cancel' buttons, the dictionary
        #   is emptied and the changes are lost
        # The key-value pairs in the dictionary correspond directly to the
        #   names of attributes, and their values in self.edit_obj
        # Key-value pairs are added to this dictionary whenever the user makes
        #   a change (so if no changes are made when the window is closed, the
        #   dictionary will still be empty)
        self.edit_dict = {}

        # String identifying the media type
        self.media_type = 'video'


        # Code
        # ----

        # Set up the edit window
        self.setup()


    # Public class methods


#   def is_duplicate():         # Inherited from GenericConfigWin


#   def setup():                # Inherited from GenericConfigWin


#   def setup_grid():           # Inherited from GenericConfigWin


#   def setup_notebook():       # Inherited from GenericConfigWin


#   def add_notebook_tab():     # Inherited from GenericConfigWin


#   def setup_button_strip():   # Inherited from GenericEditWin


#   def setup_gap():            # Inherited from GenericConfigWin


    # (Non-widget functions)


    def apply_changes(self):

        """Called by self.on_button_ok_clicked() and
        self.on_button_apply_clicked().

        Any changes the user has made are temporarily stored in self.edit_dict.
        Apply to those changes to the object being edited.
        """

        # Apply any changes the user has made
        for key in self.edit_dict.keys():
            setattr(self.edit_obj, key, self.edit_dict[key])

        # The changes can now be cleared
        self.edit_dict = {}

        # Redraw this media.Video in the Video Catalogue, if it's visible
        GObject.timeout_add(
            0,
            self.app_obj.main_win_obj.video_catalogue_update_video,
            self.edit_obj,
        )


#   def retrieve_val():         # Inherited from GenericConfigWin


    # (Setup tabs)


    def setup_tabs(self):

        """Called by self.setup(), .on_button_apply_clicked() and
        .on_button_reset_clicked().

        Sets up the tabs for this edit window.
        """

        self.setup_general_tab()
        self.setup_download_options_tab()
        self.setup_livestream_tab()
        self.setup_descrip_tab()
        self.setup_timestamps_tab()
        self.setup_slices_tab()
        self.setup_comments_tab()
        self.setup_errors_warnings_tab()


    def setup_general_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'General' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Video properties > General'
        )

        tab, grid = self.add_notebook_tab(_('_General'))
        grid_width = 3

        # General properties
        self.add_label(grid,
            '<u>' + _('General properties') + '</u>',
            0, 0, grid_width, 1,
        )

        # The first sets of widgets are shared by multiple edit windows
        self.add_container_properties(grid)
        self.add_source_properties(grid)

        label = self.add_label(grid,
            _('File'),
            0, 5, 1, 1,
        )
        label.set_hexpand(False)

        frame = self.add_image(grid,
            self.app_obj.main_win_obj.icon_dict['stock_file'],
            1, 5, 1, 1,
        )
        # (The frame looks cramped without this. The icon itself is 16x16)
        frame.set_size_request(
            16 + (self.spacing_size * 2),
            -1,
        )

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 2, 5, 1, 1)

        entry = self.add_entry(grid2,
            None,
            0, 0, 1, 1,
        )
        entry.set_editable(False)
        if self.edit_obj.file_name:
            entry.set_text(self.edit_obj.get_actual_path(self.app_obj))

        if not self.app_obj.show_custom_icons_flag:
            button = Gtk.Button.new_from_icon_name(
                Gtk.STOCK_FILE,
                Gtk.IconSize.BUTTON,
            )
        else:
            button = Gtk.Button.new()
            button.set_image(
                Gtk.Image.new_from_pixbuf(
                    self.app_obj.main_win_obj.pixbuf_dict['stock_add'],
                ),
            )

        grid2.attach(button, 1, 0, 1, 1)
        button.set_tooltip_text(_('Set the file (if this is the wrong one)'))
        if self.edit_obj.parent_obj.name \
        in self.app_obj.container_unavailable_dict:
            button.set_sensitive(False)
        # (Signal connect appears below)

        # (Back to the main grid)
        label2 = self.add_label(grid,
            _('Metadata file'),
            0, 6, 2, 1,
        )
        label2.set_hexpand(False)

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid3 = self.add_secondary_grid(grid, 2, 6, 1, 1)

        entry2 = self.add_entry(grid3,
            None,
            0, 0, 1, 1,
        )
        entry2.set_editable(False)

        metadata_path = None
        if self.retrieve_val('file_name') is not None:
            metadata_path = self.edit_obj.get_actual_path_by_ext(
                self.app_obj,
                '.info.json',
            )
            if metadata_path:
                entry2.set_text(metadata_path)

        if not self.app_obj.show_custom_icons_flag:
            button2 = Gtk.Button.new_from_icon_name(
                Gtk.STOCK_FILE,
                Gtk.IconSize.BUTTON,
            )
        else:
            button2 = Gtk.Button.new()
            button2.set_image(
                Gtk.Image.new_from_pixbuf(
                    self.app_obj.main_win_obj.pixbuf_dict['stock_file'],
                ),
            )

        grid3.attach(button2, 1, 0, 1, 1)
        button2.set_tooltip_text(
            _('Update database using the video\'s metadata file'),
        )
        if not metadata_path:
            button2.set_sensitive(False)
        # (Signal connect appears below)

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid4 = self.add_secondary_grid(grid, 0, 7, grid_width, 1)

        checkbutton = self.add_checkbutton(grid4,
            _('Video downloaded'),
            'dl_flag',
            0, 0, 1, 1,
        )
        checkbutton.set_sensitive(False)

        checkbutton2 = self.add_checkbutton(grid4,
            _('Video unwatched'),
            'new_flag',
            1, 0, 1, 1,
        )
        checkbutton2.set_sensitive(False)

        checkbutton3 = self.add_checkbutton(grid4,
            _('Video has been split from an original'),
            'split_flag',
            0, 1, 2, 1,
        )
        checkbutton3.set_sensitive(False)

        checkbutton4 = self.add_checkbutton(grid4,
            _('Video is archived'),
            'archive_flag',
            0, 2, 1, 1,
        )
        checkbutton4.set_sensitive(False)

        checkbutton5 = self.add_checkbutton(grid4,
            _('Video is bookmarked'),
            'bookmark_flag',
            1, 2, 1, 1,
        )
        checkbutton5.set_sensitive(False)

        checkbutton6 = self.add_checkbutton(grid4,
            _('Video is favourite'),
            'fav_flag',
            0, 3, 1, 1,
        )
        checkbutton6.set_sensitive(False)

        checkbutton7 = self.add_checkbutton(grid4,
            _('Video is in waiting list'),
            'waiting_flag',
            1, 3, 1, 1,
        )
        checkbutton7.set_sensitive(False)

        checkbutton8 = self.add_checkbutton(grid4,
            _('Video is blocked/censored/age-restricted'),
            'block_flag',
            0, 4, 2, 1,
        )
        checkbutton8.set_sensitive(False)

        checkbutton9 = self.add_checkbutton(grid4,
            _('Always simulate download of this video'),
            'dl_sim_flag',
            0, 5, 2, 1,
        )
        checkbutton9.set_sensitive(False)

        label3 = self.add_label(grid4,
            _('Video ID'),
            2, 0, 1, 1,
        )
        label3.set_hexpand(False)

        entry3 = self.add_entry(grid4,
            None,
            3, 0, 1, 1,
        )
        entry3.set_editable(False)
        if self.edit_obj.vid is not None:
            entry3.set_text(self.edit_obj.vid)

        label4 = self.add_label(grid4,
            _('Duration'),
            2, 1, 1, 1,
        )
        label4.set_hexpand(False)

        entry4 = self.add_entry(grid4,
            None,
            3, 1, 1, 1,
        )
        entry4.set_editable(False)
        if self.edit_obj.duration is not None:
            entry4.set_text(
                ttutils.convert_seconds_to_string(self.edit_obj.duration),
            )

        label5 = self.add_label(grid4,
            _('File size'),
            2, 2, 1, 1,
        )
        label5.set_hexpand(False)

        entry5 = self.add_entry(grid4,
            None,
            3, 2, 1, 1,
        )
        entry5.set_editable(False)
        if self.edit_obj.file_size is not None:
            entry5.set_text(self.edit_obj.get_file_size_string())

        label6 = self.add_label(grid4,
            _('Upload time'),
            2, 3, 1, 1,
        )
        label6.set_hexpand(False)

        entry6 = self.add_entry(grid4,
            None,
            3, 3, 1, 1,
        )
        entry6.set_editable(False)
        if self.edit_obj.upload_time is not None:
            entry6.set_text(self.edit_obj.get_upload_time_string())

        label7 = self.add_label(grid4,
            _('Receive time'),
            2, 4, 1, 1,
        )
        label7.set_hexpand(False)

        entry7 = self.add_entry(grid4,
            None,
            3, 4, 1, 1,
        )
        entry7.set_editable(False)
        if self.edit_obj.receive_time is not None:
            entry7.set_text(self.edit_obj.get_receive_time_string())

        label8 = self.add_label(grid4,
            _('Subtitles'),
            2, 5, 1, 1,
        )
        label8.set_hexpand(False)

        entry8 = self.add_entry(grid4,
            None,
            3, 5, 1, 1,
        )
        entry8.set_editable(False)
        entry8.set_text(' '.join(self.edit_obj.subs_list))

        # (Signal connect from above)
        button.connect('clicked', self.on_file_button_clicked)
        button2.connect('clicked', self.on_metadata_button_clicked)


#   def setup_download_options_tab():   # Inherited from GenericConfigWin


    def setup_livestream_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Livestream' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Video properties > Live'
        )

        tab, grid = self.add_notebook_tab(_('_Live'))
        grid_width = 2

        # Livestream properties
        self.add_label(grid,
            '<u>' + _('Livestream properties') + '</u>',
            0, 0, grid_width, 1,
        )

        label = self.add_label(grid,
            _('Livestream status'),
            0, 1, 1, 1,
        )
        label.set_hexpand(False)

        entry = self.add_entry(grid,
            None,
            1, 1, 1, 1,
        )
        entry.set_editable(False)
        if self.edit_obj.live_mode == 1:
            entry.set_text(_('Waiting to start'))
        elif self.edit_obj.live_mode == 2:
            entry.set_text(_('Livestream has started'))
        elif self.edit_obj.was_live_flag:
            entry.set_text(_('Livestream has finished'))
        else:
            entry.set_text(_('Not a livestream'))

        label2 = self.add_label(grid,
            _('Livestream message'),
            0, 2, 1, 1,
        )
        label2.set_hexpand(False)

        entry2 = self.add_entry(grid,
            None,
            1, 2, 1, 1,
        )
        entry2.set_text(self.edit_obj.live_msg)
        entry2.set_editable(False)

        checkbutton = Gtk.CheckButton()
        grid.attach(checkbutton, 0, 3, grid_width, 1)
        checkbutton.set_label(
            _('Video is pre-recorded'),
        )
        if self.edit_obj.live_debut_flag:
            checkbutton.set_active(True)
        checkbutton.set_sensitive(False)

        if self.edit_obj.live_mode and not (
            self.edit_obj.parent_obj.dbid \
            in self.app_obj.container_unavailable_dict
        ):
            # Livestream actions
            self.add_label(grid,
                '<u>' + _('Livestream actions') + '</u>',
                0, 4, grid_width, 1,
            )

            checkbutton2 = Gtk.CheckButton()
            grid.attach(checkbutton2, 0, 5, grid_width, 1)
            checkbutton2.set_label(
                _('When the livestream starts, show a desktop notification'),
            )
            if self.edit_obj.dbid in self.app_obj.media_reg_auto_notify_dict:
                checkbutton2.set_active(True)
            checkbutton2.set_sensitive(False)

            checkbutton3 = Gtk.CheckButton()
            grid.attach(checkbutton3, 0, 6, grid_width, 1)
            checkbutton3.set_label(
                _('When the livestream starts, play an alarm'),
            )
            if self.edit_obj.dbid in self.app_obj.media_reg_auto_alarm_dict:
                checkbutton3.set_active(True)
            checkbutton3.set_sensitive(False)

            checkbutton4 = Gtk.CheckButton()
            grid.attach(checkbutton4, 0, 7, grid_width, 1)
            checkbutton4.set_label(
                _(
                'When the livestream starts, open it in the system\'s web' \
                + ' browser',
                ),
            )
            if self.edit_obj.dbid in self.app_obj.media_reg_auto_open_dict:
                checkbutton4.set_active(True)
            checkbutton4.set_sensitive(False)

            checkbutton5 = Gtk.CheckButton()
            grid.attach(checkbutton5, 0, 8, grid_width, 1)
            checkbutton5.set_label(
                _(
                'When the livestream starts, begin downloading it immediately',
                ),
            )
            if self.edit_obj.dbid in self.app_obj.media_reg_auto_dl_start_dict:
                checkbutton5.set_active(True)
            checkbutton5.set_sensitive(False)

            checkbutton6 = Gtk.CheckButton()
            grid.attach(checkbutton6, 0, 9, grid_width, 1)
            checkbutton6.set_label(
                _(
                'When a livestream stops, download it (overwriting any' \
                + ' earlier file)',
                ),
            )
            if self.edit_obj.dbid in self.app_obj.media_reg_auto_dl_stop_dict:
                checkbutton6.set_active(True)
            checkbutton6.set_sensitive(False)


    def setup_descrip_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Description' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Video properties > Description'
        )

        tab, grid = self.add_notebook_tab(_('_Description'))
        grid_width = 2

        # Video description
        self.add_label(grid,
            '<u>' + _('Video description') + '</u>',
            0, 0, grid_width, 1,
        )

        textview, textbuffer = self.add_textview(grid,
            'descrip',
            0, 1, grid_width, 1,
        )
        textview.set_editable(False)
        textview.set_wrap_mode(Gtk.WrapMode.WORD)
        textview.set_can_focus(False)

        button = Gtk.Button.new_with_label(
            _('Update from the description file, and set the line length to:'),
        )
        grid.attach(button, 0, 2, 1, 1)
        # (Signal connect appears below)

        min_value = self.app_obj.main_win_obj.medium_string_max_len
        max_value = self.app_obj.main_win_obj.descrip_line_max_len
        if max_value < min_value:
            max_value = min_value

        spinbutton = self.add_spinbutton(grid,
            min_value,
            max_value,
            1,
            None,
            1, 2, 1, 1,
        )
        spinbutton.set_value(self.app_obj.main_win_obj.descrip_line_max_len)

        button2 = Gtk.Button.new_with_label(
            _('Clear the description (does not modify the file)'),
        )
        grid.attach(button2, 0, 3, grid_width, 1)
        # (Signal connect appears below)

        # (Signal connects from above)
        button.connect(
            'clicked',
            self.on_load_descrip_button_clicked,
            spinbutton,
            textbuffer,
        )

        button2.connect(
            'clicked',
            self.on_clear_descrip_button_clicked,
            textbuffer,
        )


    def setup_timestamps_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Timestamps' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Video properties > Timestamps'
        )

        tab, grid = self.add_notebook_tab(_('_Timestamps'))
        grid_width = 4

        # Timestamps
        self.add_label(grid,
            '<u>' + _('Timestamps') + '</u>',
            0, 0, grid_width, 1,
        )

        self.add_label(grid,
            '<i>' + _(
                'Timestamps can be used to download or create video clips',
            ) + '</i>',
            0, 1, grid_width, 1,
        )

        # (GenericConfigWin.add_treeview() doesn't support multiple columns, so
        #   we'll do everything ourselves)
        frame = Gtk.Frame()
        grid.attach(frame, 0, 2, grid_width, 1)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)

        treeview = Gtk.TreeView()
        scrolled.add(treeview)
        treeview.set_headers_visible(True)

        for i, column_title in enumerate(
            [ _('Start'), _('Stop'), _('Clip title') ],
        ):
            renderer_text = Gtk.CellRendererText()
            column_text = Gtk.TreeViewColumn(
                column_title,
                renderer_text,
                text=i,
            )
            treeview.append_column(column_text)
            column_text.set_resizable(True)

        self.timestamp_liststore = Gtk.ListStore(str, str, str)
        treeview.set_model(self.timestamp_liststore)

        # Initialise the list
        self.setup_timestamps_tab_update_treeview()

        # Strip of widgets at the bottom
        label = self.add_label(grid,
            _('Start timestamp (e.g. 15:29)'),
            0, 3, 1, 1,
        )
        label.set_hexpand(False)

        entry = self.add_entry(grid,
            None,
            1, 3, 1, 1,
        )
        entry.set_width_chars(12)
        entry.set_hexpand(False)

        label2 = self.add_label(grid,
            _('Stop timestamp (optional)'),
            2, 3, 1, 1,
        )
        label2.set_hexpand(False)

        entry2 = self.add_entry(grid,
            None,
            3, 3, 1, 1,
        )
        entry2.set_width_chars(12)
        entry2.set_hexpand(False)

        label3 = self.add_label(grid,
            _('Clip title (optional)'),
            0, 4, 1, 1,
        )
        label3.set_hexpand(False)

        entry3 = self.add_entry(grid,
            None,
            1, 4, 3, 1,
        )
        entry3.set_hexpand(True)

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 5, grid_width, 1)

        button = Gtk.Button(_('Add timestamp'))
        grid2.attach(button, 0, 0, 1, 1)
        button.set_hexpand(True)
        button.connect(
            'clicked',
            self.on_add_stamp_button_clicked,
            entry,
            entry2,
            entry3,
        )

        button2 = Gtk.Button(_('Delete timestamp'))
        grid2.attach(button2, 1, 0, 1, 1)
        button2.set_hexpand(True)
        button2.connect(
            'clicked',
            self.on_delete_stamp_button_clicked,
            treeview,
        )

        button3 = Gtk.Button(_('Clip preferences'))
        grid2.attach(button3, 2, 0, 1, 1)
        button3.set_hexpand(True)
        button3.connect(
            'clicked',
            self.on_clip_prefs_clicked,
        )

        button4 = Gtk.Button(_('Clear list'))
        grid2.attach(button4, 3, 0, 1, 1)
        button4.set_hexpand(True)
        button4.connect(
            'clicked',
            self.on_clear_stamp_button_clicked,
        )

        button5 = Gtk.Button(_('Reset list using copied text'))
        grid2.attach(button5, 0, 1, 2, 1)
        button5.set_hexpand(True)
        button5.connect(
            'clicked',
            self.on_copy_stamp_button_clicked,
        )

        button6 = Gtk.Button(_('Reset list using video description'))
        grid2.attach(button6, 2, 1, 2, 1)
        button6.set_hexpand(True)
        button6.connect(
            'clicked',
            self.on_extract_stamp_button_clicked,
        )


    def setup_timestamps_tab_update_treeview(self):

        """ Called by self.setup_timestamps_tab().

        Fills or updates the treeview.
        """

        self.timestamp_liststore.clear()

        # Add each timestamp/title to the treeview, one row at a time
        for mini_list in self.edit_obj.stamp_list:

            start_stamp = mini_list[0]

            if mini_list[1] is None:
                stop_stamp = ''
            else:
                stop_stamp = mini_list[1]

            if mini_list[2] is None:
                clip_title = ''
            else:
                clip_title = mini_list[2]

            self.timestamp_liststore.append(
                [ start_stamp, stop_stamp, clip_title ],
            )


    def setup_slices_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Slices' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Video properties > Slices'
        )

        tab, grid = self.add_notebook_tab(_('_Slices'))
        grid_width = 4

        # Video slices
        self.add_label(grid,
            '<u>' + _('Video slices') + '</u>',
            0, 0, grid_width, 1,
        )

        self.add_label(grid,
            '<i>' + _(
                'SponsorBlock provides a list of slices that can be' \
                + ' removed from a video',
            ) + '</i>',
            0, 1, grid_width, 1,
        )

        # (GenericConfigWin.add_treeview() doesn't support multiple columns, so
        #   we'll do everything ourselves)
        frame = Gtk.Frame()
        grid.attach(frame, 0, 2, grid_width, 1)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)

        treeview = Gtk.TreeView()
        scrolled.add(treeview)
        treeview.set_headers_visible(True)

        for i, column_title in enumerate(
            [ _('Category'), _('Action type'), _('Start'), _('Stop') ],
        ):
            renderer_text = Gtk.CellRendererText()
            column_text = Gtk.TreeViewColumn(
                column_title,
                renderer_text,
                text=i,
            )
            treeview.append_column(column_text)
            column_text.set_resizable(True)

        self.slice_liststore = Gtk.ListStore(str, str, str, str)
        treeview.set_model(self.slice_liststore)

        # Initialise the list
        self.setup_slices_tab_update_treeview()

        # Strip of widgets at the bottom
        self.add_label(grid,
            _('Category'),
            0, 3, 1, 1
        )

        combo = self.add_combo(grid,
            formats.SPONSORBLOCK_CATEGORY_LIST,
            None,
            1, 3, 1, 1,
        )
        combo.set_active(0)

        self.add_label(grid,
            _('Action type'),
            2, 3, 1, 1
        )

        combo2 = self.add_combo(grid,
            formats.SPONSORBLOCK_ACTION_LIST,
            None,
            3, 3, 1, 1,
        )
        combo2.set_active(0)

        label = self.add_label(grid,
            _('Start (timestamp or seconds)'),
            0, 4, 1, 1,
        )
        label.set_hexpand(False)

        entry = self.add_entry(grid,
            None,
            1, 4, 1, 1,
        )
        entry.set_hexpand(False)

        label2 = self.add_label(grid,
            _('Stop (optional)'),
            2, 4, 1, 1,
        )
        label2.set_hexpand(False)

        entry2 = self.add_entry(grid,
            None,
            3, 4, 1, 1,
        )
        entry2.set_hexpand(False)

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 5, grid_width, 1)

        button = Gtk.Button(_('Add slice'))
        grid2.attach(button, 0, 0, 1, 1)
        button.set_hexpand(True)
        button.connect(
            'clicked',
            self.on_add_slice_button_clicked,
            combo,
            combo2,
            entry,
            entry2,
        )

        button2 = Gtk.Button(_('Delete slice'))
        grid2.attach(button2, 1, 0, 1, 1)
        button2.set_hexpand(True)
        button2.connect(
            'clicked',
            self.on_delete_slice_button_clicked,
            treeview,
        )

        button3 = Gtk.Button(_('SponsorBlock settings'))
        grid2.attach(button3, 2, 0, 1, 1)
        button3.set_hexpand(True)
        button3.connect(
            'clicked',
            self.on_block_prefs_clicked,
        )

        button4 = Gtk.Button(_('Clear list'))
        grid2.attach(button4, 3, 0, 1, 1)
        button4.set_hexpand(True)
        button4.connect(
            'clicked',
            self.on_clear_slice_button_clicked,
        )

        button5 = Gtk.Button(_('Contact SponsorBlock to reset list'))
        grid2.attach(button5, 2, 1, 2, 1)
        button5.set_hexpand(True)
        button5.connect(
            'clicked',
            self.on_contact_sblock_clicked,
        )


    def setup_slices_tab_update_treeview(self):

        """ Called by self.setup_slices_tab().

        Fills or updates the treeview.
        """

        self.slice_liststore.clear()

        # Add each slice to the treeview, one row at a time
        for mini_dict in self.edit_obj.slice_list:

            if 'category' in mini_dict:
                category = mini_dict['category']
            else:
                category = 'n/a'

            if 'action' in mini_dict:
                action = mini_dict['action']
            else:
                action = 'n/a'

            if 'start_time' in mini_dict:
                start_time = mini_dict['start_time']
            else:
                start_time = 'n/a'

            if 'stop_time' in mini_dict \
            and mini_dict['stop_time'] is not None:
                stop_time = mini_dict['stop_time']
            else:
                stop_time = 'n/a'

            self.slice_liststore.append(
                [ category, action, str(start_time), str(stop_time) ],
            )


    def setup_comments_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Comments' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Video properties > Comments'
        )

        tab, grid = self.add_notebook_tab(_('_Comments'))
        grid_width = 3

        # Comments (yt-dlp only)
        label = self.add_label(grid,
            '<u>' + _('Comments') + '</u>' + self.ytdlp_only(),
            0, 0, 1, 1,
        )
        label.set_hexpand(True)

        label2 = self.add_label(grid,
            _('Total comments:'),
            1, 0, 1, 1,
        )
        label2.set_hexpand(False)

        entry = self.add_entry(grid,
            None,
            2, 0, 1, 1,
        )
        entry.set_hexpand(False)
        entry.set_max_width_chars(8)
        entry.set_text(str(len(self.edit_obj.comment_list)))

        frame = Gtk.Frame()
        grid.attach(frame, 0, 1, grid_width, 1)

        self.comment_scrolled = Gtk.ScrolledWindow()
        frame.add(self.comment_scrolled)
        self.comment_scrolled.set_vexpand(True)
        self.comment_scrolled.set_policy(
            Gtk.PolicyType.AUTOMATIC,
            Gtk.PolicyType.AUTOMATIC,
        )

        # For a flat list, use a Gtk.TreeView. For a formmated list, use a
        #   Gtk.ListBox
        if not self.app_obj.comment_show_formatted_flag:
            self.setup_comments_tab_add_treeview()
        else:
            self.setup_comments_tab_add_listbox()

        # (The list is initialised below)

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 2, grid_width, 1)

        # Editing widgets
        checkbutton = self.add_checkbutton(grid2,
            _('Show formatted list'),
            None,
            0, 0, 1, 1,
        )
        checkbutton.set_hexpand(False)
        if self.app_obj.comment_show_formatted_flag:
            checkbutton.set_active(True)
        checkbutton.connect('toggled', self.on_format_checkbutton_toggled)

        radiobutton = self.add_radiobutton(grid2,
            None,
            _('Show comment times as text'),
            None,
            None,
            1, 0, 1, 1,
        )
        radiobutton.set_hexpand(False)
        # (Signal connect appears below)

        radiobutton2 = self.add_radiobutton(grid2,
            radiobutton,
            _('Show comment timestamps'),
            None,
            None,
            2, 0, 1, 1,
        )
        radiobutton2.set_hexpand(False)
        if not self.app_obj.comment_show_text_time_flag:
            radiobutton2.set_active(True)
        # (Signal connect appears below)

        # (Signal connects from above)
        radiobutton.connect(
            'toggled',
            self.on_time_radiobutton_toggled,
        )

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid3 = self.add_secondary_grid(grid, 0, 3, grid_width, 1)

        label3 = self.add_label(grid3,
            _('Filter'),
            0, 0, 1, 1,
        )
        label3.set_hexpand(False)

        self.filter_entry = self.add_entry(grid3,
            None,
            1, 0, 1, 1,
        )
        self.filter_entry.set_hexpand(False)
        self.filter_entry.set_tooltip_text(_('Enter search text'))
        self.filter_entry.set_width_chars(16)

        self.filter_togglebutton = Gtk.ToggleButton.new_with_label(_('Regex'))
        grid3.attach(self.filter_togglebutton, 2, 0, 1, 1)

        self.filter_author_checkbutton = self.add_checkbutton(grid3,
            _('Author'),
            None,
            3, 0, 1, 1,
        )
        self.filter_author_checkbutton.set_hexpand(False)

        self.filter_comment_checkbutton = self.add_checkbutton(grid3,
            _('Comment'),
            None,
            4, 0, 1, 1,
        )
        self.filter_comment_checkbutton.set_hexpand(False)
        self.filter_comment_checkbutton.set_active(True)

        self.filter_uploader_checkbutton = self.add_checkbutton(grid3,
            _('Uploader'),
            None,
            5, 0, 1, 1,
        )
        self.filter_uploader_checkbutton.set_hexpand(False)

        if not self.app_obj.show_custom_icons_flag:
            self.filter_apply_button = Gtk.ToolButton.new_from_stock(
                Gtk.STOCK_FIND,
            )
        else:
            self.filter_apply_button = Gtk.ToolButton.new()
            self.filter_apply_button.set_icon_widget(
                Gtk.Image.new_from_pixbuf(
                    self.app_obj.main_win_obj.pixbuf_dict['stock_find'],
                ),
            )
        grid3.attach(self.filter_apply_button, 6, 0, 1, 1)
        self.filter_apply_button.connect(
            'clicked',
            self.on_apply_filter_button_clicked,
        )

        if not self.app_obj.show_custom_icons_flag:
            self.filter_cancel_button = Gtk.ToolButton.new_from_stock(
                Gtk.STOCK_CANCEL,
            )
        else:
            self.filter_cancel_button = Gtk.ToolButton.new()
            self.filter_cancel_button.set_icon_widget(
                Gtk.Image.new_from_pixbuf(
                    self.app_obj.main_win_obj.pixbuf_dict['stock_cancel'],
                ),
            )
        grid3.attach(self.filter_cancel_button, 7, 0, 1, 1)
        self.filter_cancel_button.set_sensitive(False)
        self.filter_cancel_button.connect(
            'clicked',
            self.on_cancel_filter_button_clicked,
        )

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid4 = self.add_secondary_grid(grid, 0, 4, grid_width, 1)

        button = Gtk.Button.new_with_label(
            _('Update from the metadata file'),
        )
        grid4.attach(button, 0, 0, 1, 1)
        button.set_hexpand(True)
        button.connect(
            'clicked',
            self.on_load_comments_button_clicked,
        )
        # (The call to .check_actual_path_by_ext() requires a value of
        #   media.Video.file_name that is not None)
        if self.edit_obj.file_name is None \
        or self.edit_obj.file_ext is None:
            button.set_sensitive(False)
        else:
            json_path = self.edit_obj.check_actual_path_by_ext(
                self.app_obj,
                '.info.json',
            )
            if json_path is None:
                button.set_sensitive(False)

        button2 = Gtk.Button.new_with_label(
            _('Clear comments (does not modify the file)'),
        )
        grid4.attach(button2, 1, 0, 1, 1)
        button2.set_hexpand(True)
        button2.connect(
            'clicked',
            self.on_clear_comments_button_clicked,
        )

        # Initialise the list
        self.setup_comments_tab_update_list()


    def setup_comments_tab_add_treeview(self):

        """Called by self.setup_comments_tab().

        For a flat list, we use a Gtk.TreeView.
        """

        # (This treeview replaces the old treeview or Gtk.ListBox)
        self.setup_comments_tab_remove_child()

        self.comment_treeview = Gtk.TreeView()
        self.comment_scrolled.add(self.comment_treeview)
        self.comment_treeview.set_headers_visible(True)

        for i, column_title in enumerate(
            [
                _('Time'), _('Author'), _('Comment'), _('Likes'),
                _('Favourite'), _('Uploader'), '',
            ],
        ):
            if i < 4:
                renderer_text = Gtk.CellRendererText()
                column_text = Gtk.TreeViewColumn(
                    column_title,
                    renderer_text,
                    text=i,
                )
                self.comment_treeview.append_column(column_text)
                column_text.set_resizable(True)
                # (Employ a twin strategy to cope with spam: split long values
                #   into multiple lines, and limit the (default) column size)
                if i == 1:
                    column_text.set_min_width(100)
                    column_text.set_max_width(200)
                elif i == 2:
                    column_text.set_min_width(100)
                else:
                    column_text.set_min_width(50)
            elif i < 6:
                renderer_toggle = Gtk.CellRendererToggle()
                column_toggle = Gtk.TreeViewColumn(
                    column_title,
                    renderer_toggle,
                    active=i,
                )
                self.comment_treeview.append_column(column_toggle)
                column_toggle.set_resizable(False)
                column_toggle.set_min_width(50)
            else:
                # (Prevent the 'Uploader' column expanding to fill available
                #   space, especially when the window is maximised)
                renderer_text = Gtk.CellRendererText()
                column_text = Gtk.TreeViewColumn(
                    column_title,
                    renderer_text,
                    text=i,
                )
                self.comment_treeview.append_column(column_text)
                column_text.set_resizable(False)

        self.comment_liststore = Gtk.ListStore(
            str, str, str, str, bool, bool,
        )
        self.comment_treeview.set_model(self.comment_liststore)


    def setup_comments_tab_add_listbox(self):

        """Called by self.setup_comments_tab().

        For a formatted list, we use a Gtk.ListBox.
        """

        # (This listbox replaces the old treeview or Gtk.ListBox)
        self.setup_comments_tab_remove_child()

        self.comment_listbox = Gtk.ListBox()
        self.comment_scrolled.add(self.comment_listbox)
        self.comment_listbox.set_can_focus(False)
        self.comment_listbox.set_vexpand(True)


    def setup_comments_tab_remove_child(self):

        """Called by self.setup_comments_tab_add_treeview() and
        self.setup_comments_tab_add_listbox().

        Removes the containing Gtk.Frame's textview or listbox, before adding
        a new child widget.
        """

        if self.comment_treeview is not None:
            self.comment_scrolled.remove(self.comment_treeview)
            self.comment_treeview = None
            self.comment_liststore = None
        elif self.comment_listbox is not None:
            self.comment_scrolled.remove(self.comment_listbox)
            self.comment_listbox = None


    def setup_comments_tab_update_list(self):

        """Can be called by anything.

        Fills or updates either the treeview or the listbox, whichever is
        visible at the moment.
        """

        if self.comment_treeview is not None:
            self.setup_comments_tab_update_treeview()
        else:
            self.setup_comments_tab_update_listbox()

        # (The Gtk.ListBox won't appear filled without this line)
        self.show_all()


    def setup_comments_tab_update_treeview(self):

        """ Called by self.setup_comments_tab().

        Fills or updates the treeview.
        """

        self.comment_liststore.clear()

        shorter = 30
        longer = 80

        # Set up filtering
        filter_dict = {
            'search_text': self.filter_entry.get_text(),
            'lower_text': self.filter_entry.get_text().lower(),
            'regex_flag': self.filter_togglebutton.get_active(),
            'author_flag': self.filter_author_checkbutton.get_active(),
            'comment_flag': self.filter_comment_checkbutton.get_active(),
            'uploader_flag': self.filter_uploader_checkbutton.get_active(),
        }

        # Add each comment to the treeview, one row at a time
        # (Employ a twin strategy to cope with spam: split long values into
        #   multiple lines, and limit the (default) column size)
        longest = 0
        for mini_dict in self.edit_obj.comment_list:

            if not self.setup_comments_tab_check_filter(
                mini_dict,
                filter_dict,
            ):
                continue

            # (The keys 'id' and 'text' are compulsory)
            if not self.app_obj.comment_show_text_time_flag \
            and 'timestamp' in mini_dict:
                ts = datetime.datetime.fromtimestamp(mini_dict['timestamp'])
                ts.replace(tzinfo=datetime.timezone.utc).astimezone(tz=None)
                time = ts.strftime('%Y-%m-%d %H:%M:%S')

            elif self.app_obj.comment_show_text_time_flag \
            and 'time' in mini_dict:
                time = mini_dict['time']

            else:
                time = 'n/a'

            if 'author' in mini_dict:
                author = mini_dict['author']
            else:
                author = 'n/a'

            if 'likes' in mini_dict:
                likes = mini_dict['likes']
            else:
                likes = '0'

            if 'fav_flag' in mini_dict:
                fav_flag = mini_dict['fav_flag']
            else:
                fav_flag = False

            if 'ul_flag' in mini_dict:
                ul_flag = mini_dict['ul_flag']
            else:
                ul_flag = False

            self.comment_liststore.append([
                time,
                ttutils.shorten_string(author, shorter),
                ttutils.tidy_up_long_string(mini_dict['text'], longer),
                str(likes),
                fav_flag,
                ul_flag,
            ])


    def setup_comments_tab_update_listbox(self):

        """ Called by self.setup_comments_tab().

        Fills or updates the listbox.
        """

        for child in self.comment_listbox.get_children():
            self.comment_listbox.remove(child)

        shorter = 30
        longer = 80
        # Import the main window (for convenience)
        main_win_obj = self.app_obj.main_win_obj

        # The media.Video object's .comment_list is a flat list. Compile a
        #   dictionary, so we can find each commment's parents
        check_dict = {}
        for mini_dict in self.edit_obj.comment_list:
            check_dict[mini_dict['id']] = mini_dict

        # Set up filtering
        filter_dict = {
            'search_text': self.filter_entry.get_text(),
            'lower_text': self.filter_entry.get_text().lower(),
            'regex_flag': self.filter_togglebutton.get_active(),
            'author_flag': self.filter_author_checkbutton.get_active(),
            'comment_flag': self.filter_comment_checkbutton.get_active(),
            'uploader_flag': self.filter_uploader_checkbutton.get_active(),
        }

        # Add each comment to the listbox, one row at a time
        for mini_dict in self.edit_obj.comment_list:

            if not self.setup_comments_tab_check_filter(
                mini_dict,
                filter_dict,
            ):
                continue

            row = Gtk.ListBoxRow()

            hbox = Gtk.HBox()
            row.add(hbox)
            # (self.spacing_size is a little too big)
            hbox.set_border_width(3)

            # Indent the comment, depending on how many parents this comment
            #   has
            # The indentation is applied by adding a Gtk.Label of the right
            #   length, up to a sensible maximum
            # (Don't indent comments when the filter is applied)
            if not self.filter_cancel_button.get_sensitive():
                count = 0
                this_dict = mini_dict
                while count <= 8 and this_dict['parent'] is not None:
                    this_dict = check_dict[this_dict['parent']]
                    count += 1

                if count:
                    label = Gtk.Label.new()
                    hbox.pack_start(label, False, False, 0)
                    label.set_text('        ' * count)

            box = Gtk.Box()
            hbox.add(box)

            vbox = Gtk.VBox()
            box.add(vbox)

            hbox2 = Gtk.HBox()
            vbox.pack_start(hbox2, False, False, 0)

            if 'ul_flag' in mini_dict and mini_dict['ul_flag'] is True:
                image = Gtk.Image.new_from_pixbuf(
                    main_win_obj.pixbuf_dict['uploader_small'],
                )
                hbox2.pack_start(image, False, False, 0)

            if 'author' in mini_dict:
                msg = '<b>' \
                + html.escape(
                    ttutils.shorten_string(mini_dict['author'], shorter),
                    quote=False,
                ) + '</b>'
            else:
                msg = '<b>Anonymous</b>'

            if not self.app_obj.comment_show_text_time_flag \
            and 'timestamp' in mini_dict:
                ts = datetime.datetime.fromtimestamp(mini_dict['timestamp'])
                ts.replace(tzinfo=datetime.timezone.utc).astimezone(tz=None)
                time = ts.strftime('%Y-%m-%d %H:%M:%S')

            elif self.app_obj.comment_show_text_time_flag \
            and 'time' in mini_dict:
                time = mini_dict['time']

            else:
                time = 'Unknown time'

            msg += ' <i>' + time + '</i>'

            label2 = Gtk.Label()
            hbox2.pack_start(label2, False, False, 0)
            label2.set_markup(msg)
            label2.set_alignment(0, 0.5)

            if 'likes' in mini_dict and mini_dict['likes'] != 0:
                image = Gtk.Image.new_from_pixbuf(
                    main_win_obj.pixbuf_dict['likes_small'],
                )
                hbox2.pack_start(image, False, False, self.spacing_size)

                label3 = Gtk.Label()
                hbox2.pack_start(label3, False, False, 0)
                label3.set_text(str(mini_dict['likes']))
                label3.set_alignment(0, 0.5)

            if 'fav_flag' in mini_dict and mini_dict['fav_flag'] is True:
                image = Gtk.Image.new_from_pixbuf(
                    main_win_obj.pixbuf_dict['favourite_small'],
                )
                hbox2.pack_start(image, False, False, self.spacing_size)

            hbox3 = Gtk.HBox()
            vbox.pack_start(hbox3, False, False, 0)

            label4 = Gtk.Label()
            hbox3.pack_start(label4, False, False, 0)
            label4.set_text(
                html.escape(
                    ttutils.tidy_up_long_string(mini_dict['text'], longer),
                    quote=False
                    ,
                ),
            )
            label4.set_alignment(0, 0.5)

            self.comment_listbox.add(row)


    def setup_comments_tab_check_filter(self, comment_dict, filter_dict):

        """Called by self.setup_comments_tab_update_treeview() and
        self.setup_comments_tab_update_listbox().

        Checks each comment against the filter, if it is active.

        Args:

            comment_dict (dict): An item in media.Video.comment_list,
                supplying details about a single comment associated with this
                window's video

            filter_dict (dict): Summary of the state of widgets on this tab,
                supplying the keys 'search_text', 'lower_text', 'regex_flag',
                'author_flag', 'comment_flag', 'uploader_flag'

        Return values:

            True to display the comment, False to filter it out

        """

        comment = comment_dict['text']

        if not filter_dict['regex_flag']:

            if (
                filter_dict['author_flag'] \
                and comment_dict['author'].lower().find(
                    filter_dict['lower_text']
                ) > -1
            ) or (
                filter_dict['comment_flag'] \
                and comment_dict['text'].lower().find(
                    filter_dict['lower_text']
                ) > -1
            ) or (
                filter_dict['uploader_flag'] \
                and comment_dict['ul_flag']
            ):
                return True

        else:

            if (
                filter_dict['author_flag'] \
                and re.search(
                    filter_dict['search_text'],
                    comment_dict['author'],
                    re.IGNORECASE,
                )
            ) or (
                filter_dict['comment_flag'] \
                and re.search(
                    filter_dict['search_text'],
                    comment_dict['text'],
                    re.IGNORECASE
                )
            ) or (
                filter_dict['uploader_flag'] \
                and comment_dict['ul_flag']
            ):
                return True

        # No match
        return False


    def setup_errors_warnings_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Errors / Warnings' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Video properties > Errors / Warnings'
        )

        tab, grid = self.add_notebook_tab(_('_Errors / Warnings'))

        # Errors / Warnings
        self.add_label(grid,
            '<u>' + _('Errors / Warnings') + '</u>',
            0, 0, 1, 1,
        )

        self.add_label(grid,
            '<i>' + _(
                'Error messages produced the last time this video was' \
                + ' checked/downloaded',
            ) + '</i>',
            0, 1, 1, 1,
        )

        textview, textbuffer = self.add_textview(grid,
            'error_list',
            0, 2, 1, 1,
        )
        textview.set_editable(False)
        textview.set_wrap_mode(Gtk.WrapMode.WORD)
        textview.set_can_focus(False)

        self.add_label(grid,
            '<i>' + _(
                'Warning messages produced the last time this video was' \
                + ' checked/downloaded',
            ) + '</i>',
            0, 3, 1, 1,
        )

        textview2, textbuffer2 = self.add_textview(grid,
            'warning_list',
            0, 4, 1, 1,
        )
        textview2.set_editable(False)
        textview2.set_wrap_mode(Gtk.WrapMode.WORD)
        textview2.set_can_focus(False)


    # Callback class methods


#   def on_button_apply_options_clicked():  # Inherited from GenericConfigWin


#   def on_button_edit_options_clicked():   # Inherited from GenericConfigWin


#   def on_button_remove_options_clicked(): # Inherited from GenericConfigWin


    def on_add_slice_button_clicked(self, button, combo, combo2, entry, \
    entry2):

        """Called from a callback in self.setup_slicess_tab().

        Adds a new slice to the video's slice list.

        Args:

            button (Gtk.Button): The widget clicked

            combo, combo2 (Gtk.Entry): Other widgets to modify

            entry, entry2 (Gtk.Entry): Other widgets to modify

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' Video properties > Slices'
        )

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        category = model[tree_iter][0]

        tree_iter2 = combo2.get_active_iter()
        model2 = combo2.get_model()
        action_type = model2[tree_iter2][0]

        start_time = ttutils.strip_whitespace(entry.get_text())
        stop_time = ttutils.strip_whitespace(entry2.get_text())

        start_time = float(
            ttutils.timestamp_convert_to_seconds(self.app_obj, start_time),
        )

        if stop_time == '':
            stop_time = None
        else:
            stop_time = float(
                ttutils.timestamp_convert_to_seconds(self.app_obj, stop_time),
            )

        # Do nothing if specified timestamps aren't valid
        try:
            ignore = float(start_time)
            if stop_time is not None:
                ignore = float(stop_time)

        except:
            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('Invalid start/stop times'),
                'error',
                'ok',
                self,           # Parent window is this window
            )

            return

        if stop_time is not None and stop_time <= start_time:
            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('Invalid start/stop times'),
                'error',
                'ok',
                self,           # Parent window is this window
            )

            return

        # Compile the mini-dictionary in the format returned by SponsorBlock
        mini_dict = {
            'category': category,
            'action': action_type,
            'start_time': start_time,
            'stop_time': stop_time,
            'duration': 0,
        }

        # Add it to the list
        slice_list = self.retrieve_val('slice_list')
        slice_list.append(mini_dict)

        # (The called function will sort the list)
        self.edit_obj.set_slices(slice_list)

        # (Show changes, and empty entry boxes)
        self.setup_slices_tab_update_treeview()
        entry.set_text('')
        entry2.set_text('')


    def on_add_stamp_button_clicked(self, button, entry, entry2, entry3):

        """Called from a callback in self.setup_timestamps_tab().

        Adds a new timestamp to video's timestamp list, optionally with a
        clip title.

        Args:

            button (Gtk.Button): The widget clicked

            entry, entry2, entry3 (Gtk.Entry): Other widgets to modify

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' Video properties > Timestamps'
        )

        start_stamp = ttutils.strip_whitespace(entry.get_text())
        stop_stamp = ttutils.strip_whitespace(entry2.get_text())
        clip_title = ttutils.strip_whitespace(entry3.get_text())

        # (Values are stored as None, rather than empty strings)
        if stop_stamp == '':
            stop_stamp = None

        if clip_title == '':
            clip_title = None

        # Do nothing if specified timestamps aren't valid ('stop_stamp' is
        #   optional)
        regex = '^' + self.app_obj.timestamp_regex + '$'
        if re.search(regex, start_stamp) \
        and (stop_stamp is None or re.search(regex, stop_stamp)) \
        and ttutils.timestamp_compare(self.app_obj, start_stamp, stop_stamp):

            # Add leading zeroes to the minutes and seconds components, so
            #   that .stamp_list gets sorted correctly (and doesn't look
            #   weird)
            start_stamp = ttutils.timestamp_format(self.app_obj, start_stamp)
            if stop_stamp is not None:
                stop_stamp = ttutils.timestamp_format(self.app_obj, stop_stamp)

            # Timestamps stored in groups of three, in the form
            #   (start_stamp, stop_stamp, clip_title)
            # If a group with the same 'start_stamp' timestamp already exists,
            #   don't replace it; allow duplicates (as the user may actually
            #   want that)
            stamp_list = self.retrieve_val('stamp_list')
            stamp_list.append([ start_stamp, stop_stamp, clip_title ])

            # (The called function will sort the list)
            self.edit_obj.set_timestamps(stamp_list)

            # (Show changes, and empty entry boxes. The 'stop' timestamp, if
            #   specified, becomes the 'start' timestamp for the next group)
            self.setup_timestamps_tab_update_treeview()

            if stop_stamp is None:
                entry.set_text('')
            else:
                entry.set_text(
                    ttutils.timestamp_add_second(self.app_obj, stop_stamp),
                )

            entry2.set_text('')
            entry3.set_text('')

        else:

            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('Invalid timestamp(s)'),
                'error',
                'ok',
                self,           # Parent window is this window
            )


    def on_apply_filter_button_clicked(self, button):

        """Called from a callback in self.setup_comments_tab().

        Filters the list of comments to those matching the search text.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if not self.filter_author_checkbutton.get_active() \
        and not self.filter_comment_checkbutton.get_active() \
        and not self.filter_uploader_checkbutton.get_active():
            self.filter_entry.set_text('')
            self.filter_cancel_button.set_sensitive(False)
        elif self.filter_entry.get_text() == '':
            self.filter_cancel_button.set_sensitive(False)
        else:
            self.filter_cancel_button.set_sensitive(True)

        self.setup_comments_tab_update_list()


    def on_block_prefs_clicked(self, button):

        """Called from a callback in self.setup_slices_tab().

        Opens the preferences window to show (Sponsor)Block settings.

        Args:

            button (Gtk.Button): The widget clicked

        """

        SystemPrefWin(self.app_obj, 'slices')


    def on_cancel_filter_button_clicked(self, button):

        """Called from a callback in self.setup_comments_tab().

        Cancels the filter for comments.

        Args:

            button (Gtk.Button): The widget clicked

        """

        self.filter_entry.set_text('')
        self.filter_cancel_button.set_sensitive(False)
        self.setup_comments_tab_update_list()


    def on_clear_comments_button_clicked(self, button):

        """Called from a callback in self.setup_descrip_tab().

        Clears the video's .comment_list IV (but doesn't modify the
        .info.json file itself).

        Args:

            button (Gtk.Button): The widget clicked

        """

        self.edit_obj.reset_comments()
        self.setup_comments_tab_update_list()


    def on_clear_descrip_button_clicked(self, button, textbuffer):

        """Called from a callback in self.setup_descrip_tab().

        Clears the video's .descrip IV (but doesn't modify the .description
        file itself).

        Args:

            button (Gtk.Button): The widget clicked

            textbuffer (Gtk.TextBuffer): The textview's textbuffer

        """

        self.edit_obj.reset_video_descrip()
        textbuffer.set_text('')


    def on_clear_slice_button_clicked(self, button):

        """Called from a callback in self.setup_slices_tab().

        Empties the video's slice list.

        Args:

            button (Gtk.Button): The widget clicked

        """

        self.edit_obj.reset_slices()
        self.setup_slices_tab_update_treeview()


    def on_clear_stamp_button_clicked(self, button):

        """Called from a callback in self.setup_timestamps_tab().

        Empties the video's timestamp list.

        Args:

            button (Gtk.Button): The widget clicked

        """

        self.edit_obj.reset_timestamps()
        self.setup_timestamps_tab_update_treeview()


    def on_clip_prefs_clicked(self, button):

        """Called from a callback in self.setup_timestamps_tab().

        Opens the preferences window to show clip settings.

        Args:

            button (Gtk.Button): The widget clicked

        """

        SystemPrefWin(self.app_obj, 'clips')


    def on_contact_sblock_clicked(self, button):

        """Called from a callback in self.setup_slices_tab().

        Contacts SponsorBlock to reset the video's slice list.

        Args:

            button (Gtk.Button): The widget clicked

        """

        ttutils.fetch_slice_data(
            self.app_obj,
            self.edit_obj,
        )

        self.setup_slices_tab_update_treeview()


    def on_copy_stamp_button_clicked(self, button):

        """Called from a callback in self.setup_timestamps_tab().

        Updates the video's timestamp list using text the user has copied and
        pasted into a dialogue window.

        Args:

            button (Gtk.Button): The widget clicked

        """

        # Open the dialogue window
        dialogue_win = mainwin.AddStampDialogue(
            self,
            self.app_obj.main_win_obj,
        )
        response = dialogue_win.run()

        # Retrieve user choices from the dialogue window
        if response == Gtk.ResponseType.OK:

            text = dialogue_win.textbuffer.get_text(
                dialogue_win.textbuffer.get_start_iter(),
                dialogue_win.textbuffer.get_end_iter(),
                # Don't include hidden characters
                False,
            )

            # (Do not modify the existing list of timestampes, if no text was
            #   added to the dialogue window)
            if text != '':
                self.edit_obj.extract_timestamps_from_descrip(
                    self.app_obj,
                    text,
                )

                self.setup_timestamps_tab_update_treeview()

        # ...before destroying the dialogue window
        dialogue_win.destroy()


    def on_delete_slice_button_clicked(self, button, treeview):

        """Called from a callback in self.setup_slices_tab().

        Deletes the selected slice from the video's slice list.

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeVies): The treeview displaying the slice list

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if not path_list:

            return

        # (Multiple selection is not enabled)
        this_iter = model.get_iter(path_list[0])
        if this_iter is None:

            return

        category = model[this_iter][0]
        action_type = model[this_iter][1]
        start_time = float(model[this_iter][2])
        stop_time = float(model[this_iter][3])

        # Slices are stored as a list of mini-dictionaries, in the form
        #   described by self.on_add_slice_button_clicked()
        # Walk the list, and delete the first matching mini-dictionary
        slice_list = self.retrieve_val('slice_list')
        mod_list = []
        match_flag = False

        for mini_dict in slice_list:

            if not match_flag \
            and mini_dict['category'] == category \
            and mini_dict['action'] == action_type \
            and mini_dict['start_time'] == start_time \
            and mini_dict['stop_time'] == stop_time:
                match_flag = True   # Delete this one
            else:
                mod_list.append(mini_dict)

        # (The called function will sort the list)
        self.edit_obj.set_slices(mod_list)

        # (Show changes)
        self.setup_slices_tab_update_treeview()


    def on_delete_stamp_button_clicked(self, button, treeview):

        """Called from a callback in self.setup_timestamps_tab().

        Deletes the selected timestamp(s) from the video's timestamp list.

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeVies): The treeview displaying the timestamp list

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if not path_list:

            return

        # (Multiple selection is not enabled)
        this_iter = model.get_iter(path_list[0])
        if this_iter is None:
            return

        start_stamp = model[this_iter][0]
        stop_stamp = model[this_iter][1]
        clip_title = model[this_iter][2]

        # Timestamps stored in groups of three, in the form
        #   (start_stamp, stop_stamp, clip_title)
        # Walk the list, and delete the first matchng group
        stamp_list = self.retrieve_val('stamp_list')
        mod_list = []
        match_flag = False

        for mini_list in stamp_list:

            if not match_flag \
            and mini_list[0] == start_stamp \
            and (mini_list[1] is None or mini_list[1] == stop_stamp) \
            and (mini_list[2] is None or mini_list[2] == clip_title):
                match_flag = True   # Delete this one
            else:
                mod_list.append(mini_list)

        # (The called function will sort the list)
        self.edit_obj.set_timestamps(mod_list)

        # (Show changes)
        self.setup_timestamps_tab_update_treeview()


    def on_extract_stamp_button_clicked(self, button):

        """Called from a callback in self.setup_timestamps_tab().

        Updates the video's timestamp list from its description, then displays
        that list in the treeview.

        Args:

            button (Gtk.Button): The widget clicked

        """

        self.edit_obj.extract_timestamps_from_descrip(self.app_obj)
        self.setup_timestamps_tab_update_treeview()


    def on_file_button_clicked(self, button):

        """Called from a callback in self.setup_general_tab().

        Prompts the user to choose a new video/audio file. If a valid one is
        selected, update the media.Video object to use it

        Args:

            button (Gtk.Button): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' Video properties > General'
        )

        # Prompt the user for a new file
        dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser(
            _('Select the correct video/audio file'),
            self,
            'open',
        )

        if self.edit_obj.file_name is not None:
            old_path = self.edit_obj.get_actual_path(self.app_obj)
            old_dir, old_name = os.path.split(old_path)
            dialogue_win.set_current_folder(old_dir)

        # Get the user's response
        response = dialogue_win.run()
        if response == Gtk.ResponseType.OK:
            new_path = os.path.abspath(dialogue_win.get_filename())

        dialogue_win.destroy()
        if response == Gtk.ResponseType.OK:

            # The user must not set a video that's in a different directory
            file_dir, file_name = os.path.split(new_path)
            parent_obj = self.edit_obj.parent_obj
            if file_dir != parent_obj.get_actual_dir(self.app_obj) \
            and file_dir != parent_obj.get_default_dir(self.app_obj):

                self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                    _(
                    'The replacement video/audio file must be in the same' \
                    + ' channel, playlist or folder',
                    ),
                    'error',
                    'ok',
                    self,           # Parent window is this window
                 )

                return

            # The new file must be in a recognised video/audio format
            file_name, file_ext = os.path.splitext(new_path)
            short_ext = file_ext[1:]

            if not short_ext in formats.VIDEO_FORMAT_LIST \
            and not short_ext in formats.AUDIO_FORMAT_LIST:

                self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                    _('You must select a valid video/audio file'),
                    'error',
                    'ok',
                    self,           # Parent window is this window
                )

                return

            # Set the new file path
            self.edit_obj.set_file_from_path(new_path)

            # Extract video statistics from the metadata file
            self.app_obj.update_video_from_json(self.edit_obj)

            # Set the new file's size, duration, and so on. The True argument
            #   instructs the function to override existing values
            if self.edit_obj.dl_flag:
                self.app_obj.update_video_from_filesystem(
                    self.edit_obj,
                    new_path,
                    True,
                )

            # If the video exists, then we can mark it as downloaded
            if not self.edit_obj.dl_flag:
                self.app_obj.mark_video_downloaded(self.edit_obj, True)

            # Redraw the video in the Video Catalogue straight away
            GObject.timeout_add(
                0,
                self.app_obj.main_win_obj.video_catalogue_update_video,
                self.edit_obj,
            )

            # Reset this window by abusing the generic code
            self.reset_with_new_edit_obj(self.edit_obj)


    def on_format_checkbutton_toggled(self, checkbutton):

        """Called from callback in self.setup_comments_tab().

        Updates the mainapp.TartubeApp IV, and redraws the treeview.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if not checkbutton.get_active():
            self.app_obj.set_comment_show_formatted_flag(False)
            self.setup_comments_tab_add_treeview()
        else:
            self.app_obj.set_comment_show_formatted_flag(True)
            self.setup_comments_tab_add_listbox()

        self.setup_comments_tab_update_list()
        self.show_all()


    def on_load_comments_button_clicked(self, button):

        """Called from a callback in self.setup_comments_tab().

        Updates the video's comments from its .info.json file.

        Args:

            button (Gtk.Button): The widget clicked

        """

        self.app_obj.update_video_from_json(self.edit_obj, 'comments')
        self.setup_comments_tab_update_list()


    def on_load_descrip_button_clicked(self, button, spinbutton, textbuffer):

        """Called from a callback in self.setup_descrip_tab().

        Updates the video's description from its .description file.

        Args:

            button (Gtk.Button): The widget clicked

            spinbutton (Gtk.SpinButton): Widget setting the maximum line
                length

            textbuffer (Gtk.TextBuffer): The textview's textbuffer

        """

        self.edit_obj.read_video_descrip(
            self.app_obj,
            int(spinbutton.get_value()),
        )

        textbuffer.set_text(self.edit_obj.descrip)


    def on_metadata_button_clicked(self, button):

        """Called from a callback in self.setup_general_tab().

        Prompts the user to choose a new metadata file. If a valid one is
        selected, update the media.Video object to use it

        Args:

            button (Gtk.Button): The widget clicked

        """

        metadata_path = self.edit_obj.get_actual_path_by_ext(
            self.app_obj,
            '.info.json',
        )
        if metadata_path is not None:

            # Extract video statistics from the metadata file
            self.app_obj.update_video_from_json(self.edit_obj)

            # Set the new file's size, duration, and so on. The True argument
            #   instructs the function to override existing values
            if self.edit_obj.dl_flag:
                self.app_obj.update_video_from_filesystem(
                    self.edit_obj,
                    self.edit_obj.get_actual_path(self.app_obj),
                    True,
                )

            # Redraw the video in the Video Catalogue straight away
            GObject.timeout_add(
                0,
                self.app_obj.main_win_obj.video_catalogue_update_video,
                self.edit_obj,
            )

            # Reset this window by abusing the generic code
            self.reset_with_new_edit_obj(self.edit_obj)


    def on_time_radiobutton_toggled(self, radiobutton):

        """Called from callback in self.setup_comments_tab().

        Updates the mainapp.TartubeApp IV, and redraws the treeview.

        Args:

            radiobutton (Gtk.RadioButton): The clicked widget

        """

        if radiobutton.get_active():
            self.app_obj.set_comment_show_text_time_flag(True)
        else:
            self.app_obj.set_comment_show_text_time_flag(False)

        self.setup_comments_tab_update_list()


class ChannelPlaylistEditWin(GenericEditWin):

    """Python class for an 'edit window' to modify values in a media.Channel or
    media.Playlist object.

    Args:

        app_obj (mainapp.TartubeApp): The main application object

        edit_obj (media.Channel, media.Playlist): The object whose attributes
            will be edited in this window

    """


    # Standard class methods


    def __init__(self, app_obj, edit_obj):

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Channel/playlist properties window starts' \
            + ' here. In the main window, in the Videos tab, right-click' \
            + ' a channel or playlist and select Show > Channel' \
            + ' properties... of Show > Playlist properties...'
        )

        if isinstance(edit_obj, media.Channel):
            media_type = 'channel'
            win_title = _('Channel properties')
        else:
            media_type = 'playlist'
            win_title = _('Playlist properties')

        Gtk.Window.__init__(self, title=win_title)

        if self.is_duplicate(app_obj, edit_obj):
            return

        # IV list - class objects
        # -----------------------
        # The mainapp.TartubeApp object
        self.app_obj = app_obj
        # The media.Channel or media.Playlist object being edited
        self.edit_obj = edit_obj


        # IV list - Gtk widgets
        # ---------------------
        self.grid = None                        # Gtk.Grid
        self.notebook = None                    # Gtk.Notebook
        self.reset_button = None                # Gtk.Button
        self.apply_button = None                # Gtk.Button
        self.ok_button = None                   # Gtk.Button
        self.cancel_button = None               # Gtk.Button
        # (Non-standard widgets)
        self.apply_options_button = None        # Gtk.Button
        self.edit_options_button = None         # Gtk.Button
        self.remove_options_button = None       # Gtk.Button
        # (Widgets used in the Playlists tab)
        self.playlists_liststore = None         # Gtk.ListStore

        # IV list - other
        # ---------------
        # Size (in pixels) of gaps between edit window widgets
        self.spacing_size = self.app_obj.default_spacing_size
        # Flag set to True if all four buttons ('Reset', 'Apply', 'Cancel' and
        #   'OK') are required, or False if just the 'OK' button is required
        self.multi_button_flag = False

        # When the user changes a value, it is not applied to self.edit_obj
        #   immediately; instead, it is stored temporarily in this dictionary
        # If the user clicks the 'OK' or 'Apply' buttons at the bottom of the
        #   window, the changes are applied to self.edit_obj
        # If the user clicks the 'Reset' or 'Cancel' buttons, the dictionary
        #   is emptied and the changes are lost
        # The key-value pairs in the dictionary correspond directly to
        #   the names of attributes, and their balues in self.edit_obj
        # Key-value pairs are added to this dictionary whenever the user
        #   makes a change (so if no changes are made when the window is
        #   closed, the dictionary will still be empty)
        self.edit_dict = {}

        # String set to 'channel' or 'playlist'
        self.media_type = media_type


        # Code
        # ----

        # Set up the edit window
        self.setup()


    # Public class methods


#   def is_duplicate():         # Inherited from GenericConfigWin


#   def setup():                # Inherited from GenericConfigWin


#   def setup_grid():           # Inherited from GenericConfigWin


#   def setup_notebook():       # Inherited from GenericConfigWin


#   def add_notebook_tab():     # Inherited from GenericConfigWin


#   def setup_button_strip():   # Inherited from GenericEditWin


#   def setup_gap():            # Inherited from GenericConfigWin


    # (Non-widget functions)


    def apply_changes(self):

        """Called by self.on_button_ok_clicked() and
        self.on_button_apply_clicked().

        Any changes the user has made are temporarily stored in self.edit_dict.
        Apply to those changes to the object being edited.
        """

        # Apply any changes the user has made
        for key in self.edit_dict.keys():
            setattr(self.edit_obj, key, self.edit_dict[key])

        # The changes can now be cleared
        self.edit_dict = {}

        # Update this media.Channel/media.Playlist in the Video Index
        GObject.timeout_add(
            0,
            self.app_obj.main_win_obj.video_index_update_row_text,
            self.edit_obj,
        )


#   def retrieve_val():         # Inherited from GenericConfigWin


    # (Setup tabs)


    def setup_tabs(self):

        """Called by self.setup(), .on_button_apply_clicked() and
        .on_button_reset_clicked().

        Sets up the tabs for this edit window.
        """

        self.setup_general_tab()
        self.setup_download_options_tab()
        if self.edit_obj.enhanced:
            self.setup_assoc_playlist_tab()
        if mainapp.HAVE_MATPLOTLIB_FLAG:
            self.setup_history_tab()
        self.setup_rss_feed_tab()
        self.setup_errors_warnings_tab()


    def setup_general_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'General' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Channel properties > General'
        )

        tab, grid = self.add_notebook_tab(_('_General'))

        # General properties
        self.add_label(grid,
            '<u>' + _('General properties') + '</u>',
            0, 0, 3, 1,
        )

        # The first sets of widgets are shared by multiple edit windows
        self.add_container_properties(grid)
        self.add_source_properties(grid)
        self.add_destination_properties(grid)

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 7, 3, 1)

        if self.media_type == 'channel':
            string = _(
                'Don\'t add videos in this channel to Tartube\'s database',
            )
        else:
            string = _(
                'Don\'t add videos in this playlist to Tartube\'s database',
            )

        checkbutton = self.add_checkbutton(grid2,
            string,
            'dl_no_db_flag',
            0, 0, 1, 1,
        )
        checkbutton.set_sensitive(False)

        if self.media_type == 'channel':
            string = _('Always simulate download of videos in this channel')
        else:
            string = _('Always simulate download of videos in this playlist')

        checkbutton2 = self.add_checkbutton(grid2,
            string,
            'dl_sim_flag',
            0, 1, 1, 1,
        )
        checkbutton2.set_sensitive(False)

        if self.media_type == 'channel':
            string = _('Disable checking/downloading for this channel')
        else:
            string = _('Disable checking/downloading for this playlist')

        checkbutton3 = self.add_checkbutton(grid2,
            string,
            'dl_disable_flag',
            0, 2, 1, 1,
        )
        checkbutton3.set_sensitive(False)

        if self.media_type == 'channel':
            string = _('This channel is marked as a favourite')
        else:
            string = _('This playlist is marked as a favourite')

        checkbutton4 = self.add_checkbutton(grid2,
            string,
            'fav_flag',
            0, 3, 1, 1,
        )
        checkbutton4.set_sensitive(False)

        self.add_label(grid2,
            _('Total videos'),
            1, 0, 1, 1,
        )
        entry = self.add_entry(grid2,
            'vid_count',
            2, 0, 1, 1,
        )
        entry.set_editable(False)
        entry.set_width_chars(8)
        entry.set_hexpand(False)

        self.add_label(grid2,
            _('New videos'),
            1, 1, 1, 1,
        )
        entry2 = self.add_entry(grid2,
            'new_count',
            2, 1, 1, 1,
        )
        entry2.set_editable(False)
        entry2.set_width_chars(8)
        entry2.set_hexpand(False)

        self.add_label(grid2,
            _('Favourite videos'),
            1, 2, 1, 1,
        )
        entry3 = self.add_entry(grid2,
            'fav_count',
            2, 2, 1, 1,
        )
        entry3.set_editable(False)
        entry3.set_width_chars(8)
        entry3.set_hexpand(False)

        self.add_label(grid2,
            _('Downloaded videos'),
            1, 3, 1, 1,
        )
        entry4 = self.add_entry(grid2,
            'dl_count',
            2, 3, 1, 1,
        )
        entry4.set_editable(False)
        entry4.set_width_chars(8)
        entry4.set_hexpand(False)


#   def setup_download_options_tab():   # Inherited from GenericConfigWin


    def setup_assoc_playlist_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Associated Playlists' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Channel properties > Associated playlists.' \
            + ' Only visible for the compatible websites (e.g. YouTube)',
        )

        tab, grid = self.add_notebook_tab(_('Associated _Playlists'))
        grid_width = 4

        # Associated playlists
        self.add_label(grid,
            '<u>' + _('Associated playlists') + '</u>',
            0, 0, grid_width, 1,
        )

        self.add_label(grid,
            '<i>' + _(
                'When a video is associated with a playlist, the playlist\'s' \
                + ' ID is stored here',
            ) + '</i>',
            0, 1, grid_width, 1,
        )

        # (GenericConfigWin.add_treeview() doesn't support multiple columns, so
        #   we'll do everything ourselves)
        frame = Gtk.Frame()
        grid.attach(frame, 0, 2, grid_width, 1)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)

        treeview = Gtk.TreeView()
        scrolled.add(treeview)
        treeview.set_headers_visible(True)

        for i, column_title in enumerate(
            [ _('Playlist ID'), _('Playlist Title') ],
        ):
            renderer_text = Gtk.CellRendererText()
            column_text = Gtk.TreeViewColumn(
                column_title,
                renderer_text,
                text=i,
            )
            treeview.append_column(column_text)
            column_text.set_resizable(True)

        self.playlists_liststore = Gtk.ListStore(str, str)
        treeview.set_model(self.playlists_liststore)

        # Initialise the list
        self.setup_assoc_playlist_tab_update_treeview()

        # Strip of widgets at the bottom
        checkbutton = self.add_checkbutton(grid,
            _('Set the channel as the download destination'),
            None,
            0, 3, 2, 1,
        )
        if self.media_type != 'channel':
            checkbutton.set_sensitive(False)

        button = Gtk.Button(_('Add selected playlist'))
        grid.attach(button, 2, 3, 1, 1)
        button.connect(
            'clicked',
            self.on_add_assoc_playlist_button_clicked,
            treeview,
            checkbutton,
        )

        button2 = Gtk.Button(_('Add all playlists'))
        grid.attach(button2, 3, 3, 1, 1)
        button2.connect(
            'clicked',
            self.on_all_assoc_playlist_button_clicked,
            checkbutton,
        )

        button3 = Gtk.Button(_('Download preferences'))
        grid.attach(button3, 0, 4, 1, 1)
        button3.connect(
            'clicked',
            self.on_assoc_playlist_prefs_button_clicked,
        )

        button4 = Gtk.Button(_('Clear list'))
        grid.attach(button4, 1, 4, 1, 1)
        button4.connect(
            'clicked',
            self.on_clear_assoc_playlist_button_clicked,
        )

        # (This button works even if mainapp.TartubeApp.store_playlist_id_flag
        #   is False)
        button5 = Gtk.Button(_('Reset list using metadata file'))
        grid.attach(button5, 2, 4, 2, 1)
        button5.connect(
            'clicked',
            self.on_reset_assoc_playlist_button_clicked,
        )


    def setup_assoc_playlist_tab_update_treeview(self):

        """ Called by self.setup_assoc_playlist_tab().

        Fills or updates the treeview.
        """

        self.playlists_liststore.clear()

        # Add each playlist to the treeview, one row at a time
        # (The treeview needs to be sorted by its second column, corresponding
        #   to values in the IV's key-value pairs)
        sort_list = sorted(
            self.edit_obj.playlist_id_dict.items(), key=lambda x:x[1],
        )
        sort_dict = dict(sort_list)

        for playlist_id in sort_dict.keys():

            playlist_title = self.edit_obj.playlist_id_dict[playlist_id]

            if playlist_title is None:
                self.playlists_liststore.append(
                    [ playlist_id, '<' + _('Unnamed playlist') + '>' ],
                )

            else:
                self.playlists_liststore.append(
                    [ playlist_id, playlist_title ],
                )


    def setup_history_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'History' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Channel properties > History'
        )

        tab, grid = self.add_notebook_tab(_('_History'))
        grid_width = 6

        # Download history
        self.add_label(grid,
            '<u>' + _('Download history') + '</u>',
            0, 0, grid_width, 1,
        )

        # Add combos to customise the graph
        combo, combo2, combo3, combo4, combo5 = self.add_combos_for_graphs(
            grid,
            1,
        )

        # Add a button which, when clicked, draws the graph using the
        #   customisation options specified by the combos
        button = Gtk.Button()
        grid.attach(button, 5, 1, 1, 1)
        button.set_label(_('Draw'))
        # (Signal connect appears below)

        # Add a box, inside which we draw graphs
        hbox = Gtk.HBox()
        grid.attach(hbox, 0, 2, grid_width, 1)
        hbox.set_hexpand(True)
        hbox.set_vexpand(True)

        # (Signal connects from above)
        button.connect(
            'clicked', self.on_button_draw_graph_clicked,
            hbox,
            combo,
            combo2,
            combo3,
            combo4,
            combo5,
        )


    def setup_rss_feed_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'RSS feed' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Channel properties > RSS feed'
        )

        tab, grid = self.add_notebook_tab(_('_RSS feed'))
        grid_width = 3

        # RSS feed (used to detect livestreams)
        self.add_label(grid,
            '<u>' + _('RSS feed (used to detect livestreams)') + '</u>',
            0, 0, grid_width, 1,
        )

        if self.media_type == 'channel':
            msg = _(
                'If Tartube cannot detect the channel\'s RSS feed, you' \
                + ' can enter the URL here',
            )
        else:
            msg = _(
                'If Tartube cannot detect the playlist\'s RSS feed, you' \
                + ' can enter the URL here',
            )

        self.add_label(grid,
            '<i>' + msg + '</i>',
            0, 1, grid_width, 1,
        )

        entry = self.add_entry(grid,
            None,
            0, 2, grid_width, 1,
        )
        entry.set_editable(True)
        entry.set_hexpand(True)
        if self.edit_obj.rss:
            entry.set_text(self.edit_obj.rss)
        entry.connect('changed', self.on_rss_entry_changed)

        button = Gtk.Button(_('Open in web browser'))
        grid.attach(button, 0, 3, 1, 1)
        button.set_hexpand(False)
        button.connect('clicked', self.on_open_feed_button_clicked)

        button2 = Gtk.Button(_('Set to default feed'))
        grid.attach(button2, 1, 3, 1, 1)
        button2.set_hexpand(False)
        if not self.edit_obj.source or not self.edit_obj.enhanced:
            button2.set_sensitive(False)
        button2.connect('clicked', self.on_set_feed_button_clicked, entry)

        button3 = Gtk.Button(_('Reset feed'))
        grid.attach(button3, 2, 3, 1, 1)
        button3.set_hexpand(False)
        button3.connect('clicked', self.on_reset_feed_button_clicked, entry)

        self.add_label(grid,
            '<i>' + _(
                'N.B. The <b>Set to default feed</b> button won\'t work if' \
                + ' the RSS feed was obtained from video metadata',
            ) + '</i>',
            0, 4, grid_width, 1,
        )


    def setup_errors_warnings_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Errors / Warnings' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Channel properties > Errors / Warnings'
        )

        tab, grid = self.add_notebook_tab(_('_Errors / Warnings'))

        # Errors / Warnings
        self.add_label(grid,
            '<u>' + _('Errors / Warnings') + '</u>',
            0, 0, 1, 1,
        )

        if self.media_type == 'channel':
            string = _(
                'Error messages produced the last time this channel was' \
                + ' checked/downloaded',
            )
        else:
            string = _(
                'Error messages produced the last time this playlist was' \
                + ' checked/downloaded',
            )

        self.add_label(grid,
            '<i>' + string + '</i>',
            0, 1, 1, 1,
        )

        textview, textbuffer = self.add_textview(grid,
            'error_list',
            0, 2, 1, 1,
        )
        textview.set_editable(False)
        textview.set_wrap_mode(Gtk.WrapMode.WORD)
        textview.set_can_focus(False)

        if self.media_type == 'channel':
            string = _(
                'Warning messages produced the last time this channel was' \
                + ' checked/downloaded',
            )
        else:
            string = _(
                'Warning messages produced the last time this playlist was' \
                + ' checked/downloaded',
            )

        self.add_label(grid,
            '<i>' + string + '</i>',
            0, 3, 1, 1,
        )

        textview2, textbuffer2 = self.add_textview(grid,
            'warning_list',
            0, 4, 1, 1,
        )
        textview2.set_editable(False)
        textview2.set_wrap_mode(Gtk.WrapMode.WORD)
        textview2.set_can_focus(False)


    # (Support functions)


#   def add_combos_for_graphs():            # Inherited from GenericConfigWin


#   def plot_graph():                       # Inherited from GenericConfigWin


    # Callback class methods


    def on_add_assoc_playlist_button_clicked(self, button, treeview, \
    checkbutton):

        """Called from a callback in self.setup_assoc_playlist_tab().

        The selected associated playlist is added to Tartube's database (if
        possible).

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeVies): The treeview displaying the associated
                playlist list

            checkbutton (Gtk.CheckButton): Another widget to check

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' Channel properties > Associated playlists'
        )

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if not path_list:

            return

        # (Multiple selection is not enabled)
        this_iter = model.get_iter(path_list[0])
        if this_iter is None:

            return

        playlist_id = model[this_iter][0]
        playlist_title = model[this_iter][1]

        # If 'playlist_title' is specified, then only create a playlist if no
        #   other container has the same URL or name
        # If 'playlist_title' is not specified, the just check for duplicate
        #   URLs; if a new playlist is created, create an artificial title
        if playlist_title != '' \
        and not self.app_obj.is_container(playlist_title):

            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('The name \'{0}\' is already in use').format(playlist_title),
                'error',
                'ok',
                self,           # Parent window is this window
            )

            return

        url = ttutils.convert_enhanced_template_from_json(
            'convert_playlist_list',
            self.edit_obj.enhanced,
            {
                'playlist_id': playlist_id,
                'playlist_title': playlist_title,
            },
        )
        if url is None:

            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('Unable to extrapolate the URL for this playlist'),
                'error',
                'ok',
                self,           # Parent window is this window
            )

            return

        for dbid in self.app_obj.container_reg_dict.keys():

            media_data_obj = self.app_obj.media_reg_dict[dbid]
            if not isinstance(media_data_obj, media.Folder) \
            and media_data_obj.source == url:

                self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                    _('The playlist is already in Tartube\'s database'),
                    'error',
                    'ok',
                    self,           # Parent window is this window
                )

                return

        # Name an unnamed playlist
        if playlist_title == '':
            playlist_title \
            = ttutils.find_available_name(self, 'playlist', 1, -1)

        # Create the new playlist, with the same parent as this window's
        #   channel/playlist
        playlist_obj = self.app_obj.add_playlist(
            playlist_title,
            self.edit_obj.parent_obj,       # May be 'None'
            url,
        )
        if not playlist_obj:

            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('Unable to create new playlist'),
                'error',
                'ok',
                self,           # Parent window is this window
            )

            return

        else:

            # This window's channel is the download destination for the
            #   playlist, if required
            if checkbutton.get_active():
                playlist_obj.set_master_dbid(self.app_obj, self.edit_obj.dbid)

            # Update the Video Index. The True argument tells the function not
            #   to select the new playlist
            self.app_obj.main_win_obj.video_index_add_row(playlist_obj, True)

            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _(
                'Added playlist \'{0}\' to Tartube\'s database',
                ).format(playlist_title),
                'info',
                'ok',
                self,           # Parent window is this window
            )

            return


    def on_all_assoc_playlist_button_clicked(self, button, checkbutton):

        """Called from a callback in self.setup_assoc_playlist_tab().

        All associated playlists are added to Tartube's database (where
        possible).

        Args:

            button (Gtk.Button): The widget clicked

            checkbutton (Gtk.CheckButton): Another widget to check

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' Channel properties > Associated playlists'
        )

        success_count = 0
        fail_count = 0

        if not self.edit_obj.playlist_id_dict:
            # No playlists to add
            return

        # Go through the list of associated playlists, eliminating any which
        #   already exist (duplicate names or duplicate source URLs), and
        #   assigning a name to any unnamed playlist
        for playlist_id in self.edit_obj.playlist_id_dict.keys():

            playlist_title = self.edit_obj.playlist_id_dict[playlist_id]

            if playlist_title != '' \
            and not self.app_obj.is_container(playlist_title):
                fail_count += 1
                continue

            url = ttutils.convert_enhanced_template_from_json(
                'convert_playlist_list',
                self.edit_obj.enhanced,
                {
                    'playlist_id': playlist_id,
                    'playlist_title': playlist_title,
                },
            )
            if url is None:
                continue

            match_flag = False
            for dbid in self.app_obj.container_reg_dict.keys():

                media_data_obj = self.app_obj.media_reg_dict[dbid]
                if not isinstance(media_data_obj, media.Folder) \
                and media_data_obj.source == url:
                    match_flag = True
                    break

            if match_flag:
                fail_count += 1
                continue

            # Name an unnamed playlist
            if playlist_title == '':
                playlist_title = ttutils.find_available_name(
                    self,
                    'playlist',
                    1,
                    -1,
                ),

            # Create the new playlist, with the same parent as this window's
            #   channel/playlist
            playlist_obj = self.app_obj.add_playlist(
                playlist_title,
                self.edit_obj.parent_obj,       # May be 'None'
                url,
            )
            if not playlist_obj:
                fail_count += 1

            else:
                # This window's channel is the download destination for the
                #   playlist, if required
                if checkbutton.get_active():
                    playlist_obj.set_master_dbid(
                        self.app_obj,
                        self.edit_obj.dbid,
                    )

                # Update the Video Index. The True argument tells the function
                #   not to select the new playlist
                self.app_obj.main_win_obj.video_index_add_row(
                    playlist_obj,
                    True,
                )

                success_count += 1

        # Show confirmation
        msg = _('Playlists added: {0}').format(success_count) \
        + '\n' + _('Playlists not added: {0}').format(fail_count)

        self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue(
            msg,
           'info',
           'ok',
            self,           # Parent window is this window
        )

        return


    def on_assoc_playlist_prefs_button_clicked(self, button):

        """Called from a callback in self.setup_assoc_playlist_tab().

        Opens the preferences window to show associated playlist settings.

        Args:

            button (Gtk.Button): The widget clicked

        """

        SystemPrefWin(self.app_obj, 'downloads')


#   def on_button_apply_options_clicked():  # Inherited from GenericConfigWin


#   def on_button_edit_options_clicked():   # Inherited from GenericConfigWin


#   def on_button_remove_options_clicked(): # Inherited from GenericConfigWin


    def on_clear_assoc_playlist_button_clicked(self, button):

        """Called from a callback in self.setup_assoc_playlist_tab().

        Empties the channel/playlist's associated playlist list.

        Args:

            button (Gtk.Button): The widget clicked

        """

        self.edit_obj.reset_playlist_id()
        self.setup_assoc_playlist_tab_update_treeview()


    def on_open_feed_button_clicked(self, button):

        """Called from a callback in self.setup_rss_feed_tab().

        Opens the RSS feed in a web browser.

        Args:

            button (Gtk.Button): The widget clicked

        """

        rss = self.retrieve_val('rss')
        if rss:
            ttutils.open_file(self.app_obj, rss)


    def on_reset_assoc_playlist_button_clicked(self, button):

        """Called from a callback in self.setup_assoc_playlist_tab().

        Restores the channel/playlist's associated playlist list, extracting
        playlist IDs from the metadata file of each child video.

        Args:

            button (Gtk.Button): The widget clicked

        """

        self.edit_obj.extract_playlist_id(self.app_obj)
        self.setup_assoc_playlist_tab_update_treeview()


    def on_rss_entry_changed(self, entry):

        """Called by callback in self.setup_rss_feed_tab().

        Sets the RSS feed.

        Args:
            entry (Gtk.Entry): The entry box modified

        """

        rss = entry.get_text()
        if rss == '':
            self.edit_dict['rss'] = None
        else:
            self.edit_dict['rss'] = rss


    def on_set_feed_button_clicked(self, button, entry):

        """Called from a callback in self.setup_rss_feed_tab().

        Sets the RSS feed to its expected value.

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Another widget to modify

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Channel properties > RSS feed'
        )

        rss = self.retrieve_val('rss')
        # Update code won't work if the .rss IV is set
        self.edit_obj.reset_rss()

        # Try to set the RSS feed using the channel/playlist URL
        self.edit_obj.update_rss_from_url(self.retrieve_val('source'))
        # Try to set the RSS feed using a child video, if any
        if not self.edit_obj.rss and self.edit_obj.child_list:

            # Look for the first video whose URL is set, then give up
            for child_obj in self.edit_obj.child_list:
                if child_obj.source:
                    self.edit_obj.update_rss_from_url(child_obj.source)
                    break

        if not self.edit_obj.rss:

            # Failed. Restore the previous value
            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('Could not set the RSS feed'),
                'error',
                'ok',
                self,           # Parent window is this window
            )

            if rss:
                entry.set_text(rss)

        else:

            # Succeeded
            entry.set_text(self.edit_obj.rss)


    def on_reset_feed_button_clicked(self, button, entry):

        """Called from a callback in self.setup_rss_feed_tab().

        Resets the RSS feed.

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Another widget to modify

        """

        # (Updating the entry sets self.edit_dict)
        entry.set_text('')


class FolderEditWin(GenericEditWin):

    """Python class for an 'edit window' to modify values in a media.Folder
    object.

    Args:

        app_obj (mainapp.TartubeApp): The main application object

        edit_obj (media.Folder): The object whose attributes will be edited in
            this window

    """


    # Standard class methods


    def __init__(self, app_obj, edit_obj):

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Folder properties window starts here.' \
            + ' In the main window, in the Videos tab, right-click a folder' \
            + ' and select Show > Folder properties...'
        )

        Gtk.Window.__init__(self, title=_('Folder properties'))

        if self.is_duplicate(app_obj, edit_obj):
            return

        # IV list - class objects
        # -----------------------
        # The mainapp.TartubeApp object
        self.app_obj = app_obj
        # The media.Folder object being edited
        self.edit_obj = edit_obj


        # IV list - Gtk widgets
        # ---------------------
        self.grid = None                        # Gtk.Grid
        self.notebook = None                    # Gtk.Notebook
        self.reset_button = None                # Gtk.Button
        self.apply_button = None                # Gtk.Button
        self.ok_button = None                   # Gtk.Button
        self.cancel_button = None               # Gtk.Button
        # (Non-standard widgets)
        self.apply_options_button = None        # Gtk.Button
        self.edit_options_button = None         # Gtk.Button
        self.remove_options_button = None       # Gtk.Button


        # IV list - other
        # ---------------
        # Size (in pixels) of gaps between edit window widgets
        self.spacing_size = self.app_obj.default_spacing_size
        # Flag set to True if all four buttons ('Reset', 'Apply', 'Cancel' and
        #   'OK') are required, or False if just the 'OK' button is required
        self.multi_button_flag = False

        # When the user changes a value, it is not applied to self.edit_obj
        #   immediately; instead, it is stored temporarily in this dictionary
        # If the user clicks the 'OK' or 'Apply' buttons at the bottom of the
        #   window, the changes are applied to self.edit_obj
        # If the user clicks the 'Reset' or 'Cancel' buttons, the dictionary
        #   is emptied and the changes are lost
        # The key-value pairs in the dictionary correspond directly to
        #   the names of attributes, and their balues in self.edit_obj
        # Key-value pairs are added to this dictionary whenever the user
        #   makes a change (so if no changes are made when the window is
        #   closed, the dictionary will still be empty)
        self.edit_dict = {}

        # String identifying the media type
        self.media_type = 'folder'


        # Code
        # ----

        # Set up the edit window
        self.setup()


    # Public class methods


#   def is_duplicate():         # Inherited from GenericConfigWin


#   def setup():                # Inherited from GenericConfigWin


#   def setup_grid():           # Inherited from GenericConfigWin


#   def setup_notebook():       # Inherited from GenericConfigWin


#   def add_notebook_tab():     # Inherited from GenericConfigWin


#   def setup_button_strip():   # Inherited from GenericEditWin


#   def setup_gap():            # Inherited from GenericConfigWin


    # (Non-widget functions)


    def apply_changes(self):

        """Called by self.on_button_ok_clicked() and
        self.on_button_apply_clicked().

        Any changes the user has made are temporarily stored in self.edit_dict.
        Apply to those changes to the object being edited.
        """

        # Apply any changes the user has made
        for key in self.edit_dict.keys():
            setattr(self.edit_obj, key, self.edit_dict[key])

        # The changes can now be cleared
        self.edit_dict = {}

        # Update this media.Channel/media.Playlist in the Video Index
        GObject.timeout_add(
            0,
            self.app_obj.main_win_obj.video_index_update_row_text,
            self.edit_obj,
        )



#   def retrieve_val():         # Inherited from GenericConfigWin


    # (Setup tabs)


    def setup_tabs(self):

        """Called by self.setup(), .on_button_apply_clicked() and
        .on_button_reset_clicked().

        Sets up the tabs for this edit window.
        """

        self.setup_general_tab()
        self.setup_download_options_tab()
        self.setup_statistics_tab()
        if mainapp.HAVE_MATPLOTLIB_FLAG:
            self.setup_history_tab()
        if self.edit_obj == self.app_obj.fixed_recent_folder:
            self.setup_recent_tab()


    def setup_general_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'General' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Folder properties > General'
        )

        tab, grid = self.add_notebook_tab(_('_General'))

        # General properties
        self.add_label(grid,
            '<u>' + _('General properties') + '</u>',
            0, 0, 3, 1,
        )

        # The first sets of widgets are shared by multiple edit windows
        self.add_container_properties(grid)
        self.add_destination_properties(grid)

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 7, 3, 1)

        checkbutton = self.add_checkbutton(grid2,
            _('Don\'t add videos to Tartube\'s database'),
            'dl_no_db_flag',
            0, 0, 2, 1,
        )
        checkbutton.set_sensitive(False)

        checkbutton2 = self.add_checkbutton(grid2,
            _('Always simulate download of videos'),
            'dl_sim_flag',
            0, 1, 2, 1,
        )
        checkbutton2.set_sensitive(False)

        checkbutton3 = self.add_checkbutton(grid2,
            _('Disable checking/downloading'),
            'dl_disable_flag',
            0, 2, 2, 1,
        )
        checkbutton3.set_sensitive(False)

        checkbutton4 = self.add_checkbutton(grid2,
            _('This folder is marked as a favourite'),
            'fav_flag',
            0, 3, 2, 1,
        )
        checkbutton4.set_sensitive(False)

        checkbutton5 = self.add_checkbutton(grid2,
            _('This folder is hidden'),
            'hidden_flag',
            2, 0, 1, 1,
        )
        checkbutton5.set_sensitive(False)

        checkbutton6 = self.add_checkbutton(grid2,
            _('This folder can\'t be deleted by the user'),
            'fixed_flag',
            2, 1, 1, 1,
        )
        checkbutton6.set_sensitive(False)

        checkbutton7 = self.add_checkbutton(grid2,
            _('This is a system-controlled folder'),
            'priv_flag',
            2, 2, 1, 1,
        )
        checkbutton7.set_sensitive(False)

        checkbutton8 = self.add_checkbutton(grid2,
            _('All contents deleted when Tartube shuts down'),
            'temp_flag',
            2, 3, 1, 1,
        )
        checkbutton8.set_sensitive(False)

        label = self.add_label(grid2,
            _('Restrictions:'),
            0, 4, 1, 1,
        )
        label.set_hexpand(False)

        entry = self.add_entry(grid2,
            None,
            1, 4, 1, 1,
        )
        entry.set_editable(False)
        if self.edit_obj.restrict_mode == 'full':
            entry.set_text(_('Can only contain videos'))
        elif self.edit_obj.restrict_mode == 'partial':
            entry.set_text(_('Can contain folders and videos'))
        else:
            entry.set_text(_('Can contain anything'))


    def setup_statistics_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Statistics' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Folder properties > Statistics'
        )

        tab, grid = self.add_notebook_tab(_('_Statistics'))
        grid_width = 4

        # Statistics
        self.add_label(grid,
            '<u>' + _('Statistics') + '</u>',
            0, 0, grid_width, 1,
        )

        self.add_label(grid,
            _('This folder contains:'),
            0, 1, grid_width, 1,
        )

        self.add_label(grid,
            _('Videos'),
            0, 2, 1, 1,
        )

        entry = self.add_entry(grid,
            None,
            1, 2, 1, 1,
        )
        entry.set_editable(False)

        self.add_label(grid,
            _('Downloaded'),
            0, 3, 1, 1,
        )

        entry2 = self.add_entry(grid,
            None,
            1, 3, 1, 1,
        )
        entry2.set_editable(False)

        self.add_label(grid,
            _('Other'),
            0, 4, 1, 1,
        )

        entry3 = self.add_entry(grid,
            None,
            1, 4, 1, 1,
        )
        entry3.set_editable(False)

        self.add_label(grid,
            _('Channels'),
            2, 2, 1, 1,
        )

        entry4 = self.add_entry(grid,
            None,
            3, 2, 1, 1,
        )
        entry4.set_editable(False)

        self.add_label(grid,
            _('Playlists'),
            2, 3, 1, 1,
        )

        entry5 = self.add_entry(grid,
            None,
            3, 3, 1, 1,
        )
        entry5.set_editable(False)

        self.add_label(grid,
            _('Sub-folders'),
            2, 4, 1, 1,
        )

        entry6 = self.add_entry(grid,
            None,
            3, 4, 1, 1,
        )
        entry6.set_editable(False)

        # Initialise the entries
        self.setup_statistics_tab_recalculate(
            entry,
            entry2,
            entry3,
            entry4,
            entry5,
            entry6,
        )

        button = Gtk.Button()
        grid.attach(button, 3, 5, 1, 1)
        button.set_label(_('Recalculate'))
        button.connect(
            'clicked',
            self.on_button_recalculate_clicked,
            entry,
            entry2,
            entry3,
            entry4,
            entry5,
            entry6,
        )


    def setup_statistics_tab_recalculate(self, entry, entry2, entry3, entry4,
    entry5, entry6):

        """Called by self.setup_statistics_tab and
        .on_recalculate_button_clicked().

        Args:

            entry, entry2, entry3, entry4, entry5, entry6 (Gtk.Entry): The
                entry boxes to update

        """

        # Get number of videos, channels, playlists and sub-folders
        total_count, video_count, channel_count, playlist_count, \
        folder_count = self.edit_obj.count_descendants( [0, 0, 0, 0, 0] )

        # Calculate downloaded/not downloaded videos
        dl_count = 0
        not_dl_count = 0
        child_list = self.edit_obj.compile_all_videos( [] )

        for video_obj in child_list:

            if video_obj.dl_flag:
                dl_count += 1
            else:
                not_dl_count += 1

        entry.set_text(str(video_count))
        entry2.set_text(str(dl_count))
        entry3.set_text(str(not_dl_count))
        entry4.set_text(str(channel_count))
        entry5.set_text(str(playlist_count))
        entry6.set_text(str(folder_count))


    def setup_history_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'History' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Folder properties > History'
        )

        tab, grid = self.add_notebook_tab(_('_History'))
        grid_width = 6

        # Download history
        self.add_label(grid,
            '<u>' + _('Download history') + '</u>',
            0, 0, grid_width, 1,
        )

        # Add combos to customise the graph
        combo, combo2, combo3, combo4, combo5 = self.add_combos_for_graphs(
            grid,
            1,
        )

        # Add a button which, when clicked, draws the graph using the
        #   customisation options specified by the combos
        button = Gtk.Button()
        grid.attach(button, 5, 1, 1, 1)
        button.set_label(_('Draw'))
        # (Signal connect appears below)

        # Add a box, inside which we draw graphs
        hbox = Gtk.HBox()
        grid.attach(hbox, 0, 2, grid_width, 1)
        hbox.set_hexpand(True)
        hbox.set_vexpand(True)

        # (Signal connects from above)
        button.connect(
            'clicked', self.on_button_draw_graph_clicked,
            hbox,
            combo,
            combo2,
            combo3,
            combo4,
            combo5,
        )


    def setup_recent_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Recent Videos' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Folder properties > Recent videos. Only' \
            + ' visible for the \'Recent videos\' folder'
        )

        tab, grid = self.add_notebook_tab(_('_Recent Videos'))
        grid_width = 2

        # Recent videos
        self.add_label(grid,
            '<u>' + _('Recent videos') + '</u>',
            0, 0, grid_width, 1,
        )

        self.add_label(grid,
            '<i>' \
            + _('When videos are checked/downloaded, older downloaded videos' \
            ' are removed from this folder',
            ) + '</i>',
            0, 1, grid_width, 1,
        )

        radiobutton = self.add_radiobutton(grid,
            None,
            _('Empty the whole folder'),
            None,
            None,
            0, 2, grid_width, 1,
        )
        # (Signal connect appears below)

        radiobutton2 = self.add_radiobutton(grid,
            radiobutton,
            _('Remove downloaded videos after days'),
            None,
            None,
            0, 3, 1, 1,
        )

        spinbutton = self.add_spinbutton(grid,
            1,
            14,
            1,
            None,
            1, 3, 1, 1,
        )
        # (Signal connect appears below)

        if not self.app_obj.fixed_recent_folder_days:
            spinbutton.set_sensitive(False)
        else:
            radiobutton2.set_active(True)
            spinbutton.set_value(
                self.app_obj.fixed_recent_folder_days,
            )

        # (Signal connects from above)
        radiobutton.connect(
            'toggled',
            self.on_radiobutton_toggled,
            spinbutton,
        )

        spinbutton.connect(
            'value-changed',
            self.on_spinbutton_changed,
        )


#   def setup_download_options_tab():       # Inherited from GenericConfigWin


    # (Support functions)


#   def add_combos_for_graphs():            # Inherited from GenericConfigWin


#   def plot_graph():                       # Inherited from GenericConfigWin


    # Callback class methods


#   def on_button_apply_options_clicked():  # Inherited from GenericConfigWin


#   def on_button_edit_options_clicked():   # Inherited from GenericConfigWin


#   def on_button_remove_options_clicked(): # Inherited from GenericConfigWin


#   def on_button_draw_graph_clicked():     # Inherited from GenericConfigWin


    def on_button_recalculate_clicked(self, button, entry, entry2, entry3,
    entry4, entry5, entry6):

        """Called from callback in self.setup_statistics_tab().

        Recalculates the number of child media data objects, and updates the
        entry boxes.

        Args:

            button (Gtk.Button): The widget clicked

            entry, entry2, entry3, entry4, entry5, entry6 (Gtk.Entry): The
                entry boxes to update

        """

        self.setup_statistics_tab_recalculate(
            entry,
            entry2,
            entry3,
            entry4,
            entry5,
            entry6,
        )


    def on_radiobutton_toggled(self, radiobutton, spinbutton):

        """Called from callback in self.setup_recent_tab().

        (De)sensitises the spinbutton, depending on which radiobutton is
        selected. Then updates the IV.

        Args:

            radiobutton (Gtk.RadioButton): The clicked widget

            spinbutton (Gtk.SpinButton): Another widget to modify

        """

        if radiobutton.get_active():

            spinbutton.set_sensitive(False)
            self.app_obj.set_fixed_recent_folder_days(0)

        else:

            spinbutton.set_sensitive(True)
#            spinbutton.set_value(self.app_obj.fixed_recent_folder_days)
            self.app_obj.set_fixed_recent_folder_days(
                int(spinbutton.get_value())
            )


    def on_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_recent_tab().

        Sets the time after which videos are removed from the fixed 'Recent
        Videos' folder.

        Args:

            spinbutton (Gtk.SpinButton): The clicked widget

        """

        self.app_obj.set_fixed_recent_folder_days(
            int(spinbutton.get_value()),
        )


class ScheduledEditWin(GenericEditWin):

    """Python class for an 'edit window' to modify values in a media.Scheduled
    object.

    Args:

        app_obj (mainapp.TartubeApp): The main application object

        edit_obj (media.Scheduled): The object whose attributes will be edited
            in this window

    """


    # Standard class methods


    def __init__(self, app_obj, edit_obj):

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Scheduled downloads window starts here.' \
            + ' In the menu, click Edit > System preferences...' \
            + ' Scheduling > Start. In the \'Scheduled download name\'' \
            + ' box, add a name. Then click the Add button'
        )

        Gtk.Window.__init__(self, title=_('Scheduled download'))

        if self.is_duplicate(app_obj, edit_obj):
            return

        # IV list - class objects
        # -----------------------
        # The mainapp.TartubeApp object
        self.app_obj = app_obj
        # The media.Scheduled object being edited
        self.edit_obj = edit_obj


        # IV list - Gtk widgets
        # ---------------------
        self.grid = None                        # Gtk.Grid
        self.notebook = None                    # Gtk.Notebook
        self.reset_button = None                # Gtk.Button
        self.apply_button = None                # Gtk.Button
        self.ok_button = None                   # Gtk.Button
        self.cancel_button = None               # Gtk.Button
        # (IVs used to handle widget changes in the 'Media' tab)
        self.radiobutton = None                 # Gtk.RadioButton
        self.radiobutton2 = None                # Gtk.RadioButton

        # IV list - other
        # ---------------
        # Size (in pixels) of gaps between edit window widgets
        self.spacing_size = self.app_obj.default_spacing_size
        # Flag set to True if all four buttons ('Reset', 'Apply', 'Cancel' and
        #   'OK') are required, or False if just the 'OK' button is required
        self.multi_button_flag = False

        # When the user changes a value, it is not applied to self.edit_obj
        #   immediately; instead, it is stored temporarily in this dictionary
        # If the user clicks the 'OK' or 'Apply' buttons at the bottom of the
        #   window, the changes are applied to self.edit_obj
        # If the user clicks the 'Reset' or 'Cancel' buttons, the dictionary
        #   is emptied and the changes are lost
        # The key-value pairs in the dictionary correspond directly to
        #   the names of attributes, and their balues in self.edit_obj
        # Key-value pairs are added to this dictionary whenever the user
        #   makes a change (so if no changes are made when the window is
        #   closed, the dictionary will still be empty)
        self.edit_dict = {}

        # String identifying the media type
        self.media_type = 'scheduled'


        # Code
        # ----

        # Set up the edit window
        self.setup()


    # Public class methods


#   def is_duplicate():         # Inherited from GenericConfigWin


#   def setup():                # Inherited from GenericConfigWin


#   def setup_grid():           # Inherited from GenericConfigWin


#   def setup_notebook():       # Inherited from GenericConfigWin


#   def add_notebook_tab():     # Inherited from GenericConfigWin


#   def setup_button_strip():   # Inherited from GenericEditWin


#   def setup_gap():            # Inherited from GenericConfigWin


    # (Non-widget functions)


    def apply_changes(self):

        """Called by self.on_button_ok_clicked() and
        self.on_button_apply_clicked().

        Any changes the user has made are temporarily stored in self.edit_dict.
        Apply to those changes to the object being edited.
        """

        # Apply any changes the user has made
        for key in self.edit_dict.keys():
            setattr(self.edit_obj, key, self.edit_dict[key])

        # The changes can now be cleared
        self.edit_dict = {}

        # Since the edit window opened, channels/playlists/folders may have
        #   been deleted. Check that any items in the .media_list IV still
        #   exist
        for dbid in self.edit_obj.media_list:

            if not dbid in self.app_obj.container_reg_dict:
                self.edit_obj.media_list.remove(dbid)

        # Update the parent preference window's list of scheduled downloads
        for win_obj in self.app_obj.main_win_obj.config_win_list:

            if isinstance(win_obj, SystemPrefWin):
                win_obj.setup_scheduling_start_tab_update_treeview()


#   def retrieve_val():         # Inherited from GenericConfigWin


    # (Setup tabs)


    def setup_tabs(self):

        """Called by self.setup(), .on_button_apply_clicked() and
        .on_button_reset_clicked().

        Sets up the tabs for this edit window.
        """

        self.setup_general_tab()
        self.setup_start_tab()
        self.setup_stop_tab()
        self.setup_conflicts_tab()
        self.setup_media_tab()
        self.setup_limits_tab()
        self.setup_other_tab()


    def setup_general_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'General' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Scheduled downloads > General'
        )

        tab, grid = self.add_notebook_tab(_('_General'))
        grid_width = 3

        # General properties
        self.add_label(grid,
            '<u>' + _('General properties') + '</u>',
            0, 0, grid_width, 1,
        )

        self.add_label(grid,
            _('Scheduled download name'),
            0, 1, 1, 1,
        )

        entry = self.add_entry(grid,
            None,
            1, 1, (grid_width - 1), 1,
        )
        entry.set_text(self.edit_obj.name)
        entry.set_editable(False)

        self.add_label(grid,
            _('Download mode'),
            0, 2, 1, 1,
        )

        combo_list = [
            [_('Check channels, playlist and folders'), 'sim'],
            [_('Download channels, playlists and folders'), 'real'],
            [_('Perform a custom download'), 'custom_real'],
        ]

        combo = self.add_combo_with_data(grid,
            combo_list,
            'dl_mode',
            1, 2, (grid_width - 1), 1,
        )
        combo.set_hexpand(True)
        # (Signal connect appears below, overriding the generic one)

        self.add_label(grid,
            _('Custom download name'),
            0, 3, 1, 1,
        )

        combo_list2 = [
            [
                '',
                '',     # self.on_custom_dl_combo_changed() converts it to None
            ],
            [
                self.app_obj.general_custom_dl_obj.name,
                self.app_obj.general_custom_dl_obj.uid,
            ],
        ]
        if self.app_obj.classic_custom_dl_obj is not None:
            combo_list2.append([
                self.app_obj.classic_custom_dl_obj.name,
                self.app_obj.classic_custom_dl_obj.uid,
            ])

        for custom_dl_obj in self.app_obj.custom_dl_reg_dict.values():
            if self.app_obj.general_custom_dl_obj != custom_dl_obj \
            and (
                not self.app_obj.classic_custom_dl_obj \
                or self.app_obj.classic_custom_dl_obj != custom_dl_obj
            ):
                combo_list2.append([custom_dl_obj.name, custom_dl_obj.uid])

        combo2 = self.add_combo_with_data(grid,
            combo_list2,
            'custom_dl_uid',
            1, 3, (grid_width - 1), 1,
        )
        combo2.set_hexpand(True)
        if self.edit_obj.dl_mode != 'custom_real':
            combo2.set_sensitive(False)
        # (Signal connect appears below, overriding the generic one)

        # (Signal connects from above)
        combo.connect(
            'changed',
            self.on_dl_mode_combo_changed,
            combo2,
        )
        combo2.connect(
            'changed',
            self.on_custom_dl_combo_changed,
        )


    def setup_start_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Start' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Scheduled downloads > Start'
        )

        tab, grid = self.add_notebook_tab(_('_Start'))
        grid_width = 4

        # Start conditions
        self.add_label(grid,
            '<u>' + _('Start conditions') + '</u>',
            0, 0, grid_width, 1,
        )

        self.add_label(grid,
            _('Start mode'),
            0, 1, 1, 1,
        )

        combo_list = [
            [_('Perform this download at regular intervals'), 'repeat'],
            [_('Perform this download when Tartube starts'), 'start'],
            [
                _('Perform this download some time after Tartube starts'),
                'start_after',
            ],
            [_('Perform this download at specified times'), 'timetable'],
            [_('Disable this scheduled download'), 'disabled'],
        ]

        combo = self.add_combo_with_data(grid,
            combo_list,
            None,
            1, 1, (grid_width - 1), 1,
        )
        combo.set_hexpand(True)
        # (Signal connect appears below)

        label = self.add_label(grid,
            '',
            0, 2, 1, 1,
        )

        spinbutton = self.add_spinbutton(grid,
            1, None, 1,
            'wait_value',
            1, 2, 1, 1,
        )

        combo2_list = []
        for unit in formats.TIME_METRIC_LIST:
            if unit != 'seconds':
                combo2_list.append(
                    [ formats.TIME_METRIC_TRANS_DICT[unit], unit ],
                )

        combo2 = self.add_combo_with_data(grid,
            combo2_list,
            'wait_unit',
            2, 2, 2, 1,
        )
        combo2.set_hexpand(True)

        label2 = self.add_label(grid,
            '',
            0, 3, 1, 1,
        )

        treeview, liststore = self.add_treeview(grid,
            1, 3, 1, 10,
        )
        self.setup_start_tab_update_treeview(liststore)

        combo3_list = []
        for key in formats.SPECIFIED_DAYS_LIST:
            combo3_list.append(
                [ formats.SPECIFIED_DAYS_DICT[key], key ],
            )

        combo3 = self.add_combo_with_data(grid,
            combo3_list,
            None,
            2, 3, 2, 1,
        )
        combo3.set_hexpand(True)
        combo3.set_active(0)

        label3 = self.add_label(grid,
            _('Hours'),
            2, 4, 1, 1,
        )
        label3.set_hexpand(False)

        spinbutton2 = self.add_spinbutton(grid,
            0, 23, 1,
            None,
            3, 4, 1, 1,
        )
        # (Signal connect appears below)

        label4 = self.add_label(grid,
            _('Minutes'),
            2, 5, 1, 1,
        )
        label4.set_hexpand(False)

        spinbutton3 = self.add_spinbutton(grid,
            0, 55, 5,
            None,
            3, 5, 1, 1,
        )
        # (Signal connect appears below)

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 2, 6, 2, 1)

        button = Gtk.Button()
        grid2.attach(button, 0, 0, 1, 1)
        button.set_label(_('Add'))
        button.set_hexpand(True)
        # (Signal connect appears below)

        button2 = Gtk.Button()
        grid2.attach(button2, 1, 0, 1, 1)
        button2.set_label(_('Remove'))
        button2.set_hexpand(True)
        # (Signal connect appears below)

        # (Set up widgets in their initial state)
        if self.edit_obj.start_mode == 'repeat':
            combo.set_active(0)
        elif self.edit_obj.start_mode == 'start':
            combo.set_active(1)
        elif self.edit_obj.start_mode == 'start_after':
            combo.set_active(2)
        elif self.edit_obj.start_mode == 'timetable':
            combo.set_active(3)
        else:
            # .start_mode is 'disabled'
            combo.set_active(4)

        self.setup_start_tab_update_widgets(
            label,
            spinbutton,
            combo2,
            label2,
            combo3,
            spinbutton2,
            spinbutton3,
            button,
            button2,
        )

        # (Signal connects from above)
        combo.connect(
            'changed',
            self.on_start_mode_combo_changed,
            label,
            spinbutton,
            combo2,
            label2,
            combo3,
            spinbutton2,
            spinbutton3,
            button,
            button2,
        )

        # (Signal for showing leading zeroes for both hours and minutes)
        spinbutton2.connect('output', self.show_spinbutton_leading_zeroes, 2)
        spinbutton3.connect('output', self.show_spinbutton_leading_zeroes, 2)

        button.connect(
            'clicked',
            self.on_add_timetable_button_clicked,
            liststore,
            combo3,
            spinbutton2,
            spinbutton3,
        )
        button2.connect(
            'clicked',
            self.on_remove_timetable_button_clicked,
            treeview,
            liststore,
        )


    def setup_start_tab_update_widgets(self, label, spinbutton, combo2, \
    label2, combo3, spinbutton2, spinbutton3, button, button2):

        """Called by self.setup_start_tab() and .on_start_mode_combo_changed().

        Sets up widgets on opening, and when the first combo (marked 'Start
        Mode') changes.

        Args:

            label (Gtk.Label): A widget to be modified

            spinbutton (Gtk.SpinButton): Another widget to be modified

            combo2 (Gtk.Combo): Another widget to be modified

            label2 (Gtk.Label): Another widget to be modified

            combo3 (Gtk.Combo): Another widget to be modified

            spinbutton2, spinbutton3 (Gtk.SpinButton): Other widgets to be
                modified

            button, button (Gtk.Button): Other widgets to be modified

        """

        label.set_markup('')
        spinbutton.set_sensitive(False)
        combo2.set_sensitive(False)
        label2.set_markup('')
        combo3.set_sensitive(False)
        spinbutton2.set_sensitive(False)
        spinbutton3.set_sensitive(False)
        button.set_sensitive(False)
        button2.set_sensitive(False)

        start_mode = self.retrieve_val('start_mode')
        if start_mode == 'repeat':
            label.set_markup(_('Interval time'))
            spinbutton.set_sensitive(True)
            combo2.set_sensitive(True)
        elif start_mode == 'start':
            pass
        elif start_mode == 'start_after':
            label.set_markup('Time after startup')
            spinbutton.set_sensitive(True)
            combo2.set_sensitive(True)
        elif start_mode == 'timetable':
            label2.set_markup(_('Start times'))
            combo3.set_sensitive(True)
            spinbutton2.set_sensitive(True)
            spinbutton3.set_sensitive(True)
            button.set_sensitive(True)
            button2.set_sensitive(True)
        else:
            # 'start_mode' is 'disabled'
            pass


    def setup_start_tab_update_treeview(self, liststore):

        """Called by self.setup_start_tab(), .on_add_timetable_button_clicked()
        and .on_remove_timetable_button_clicked().

        Updates the treeview showing timetabled start times.

        Args:

            liststore (Gtk.ListStore): The treeview's model

        """

        timetable_list = self.retrieve_val('timetable_list')
        liststore.clear()

        # Each 'mini_list' is in the form [ day_string, time_string ]
        for mini_list in timetable_list:
            liststore.append([
                formats.SPECIFIED_DAYS_DICT[mini_list[0]] + ' ' + mini_list[1],
            ])


    def setup_stop_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Stop' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Scheduled downloads > Stop'
        )

        tab, grid = self.add_notebook_tab(_('_Stop'))
        grid_width = 4

        # Stop conditions
        self.add_label(grid,
            '<u>' + _('Stop conditions') + '</u>',
            0, 0, grid_width, 1,
        )

        self.add_label(grid,
            '<i>' + _(
            'N.B. When this setting is triggered, the entire download' \
            + ' operation stops',
            ) + '</i>',
            0, 1, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Stop download operation after this much time'),
            None,
            0, 2, 1, 1,
        )
        checkbutton.set_active(self.edit_obj.autostop_time_flag)
        # (Signal connect appears below)

        spinbutton = self.add_spinbutton(grid,
            1, None, 1,
            None,
            1, 2, 1, 1,
        )
        spinbutton.set_value(self.edit_obj.autostop_time_value)
        if not self.edit_obj.autostop_time_flag:
            spinbutton.set_sensitive(False)
        # (Signal connect appears below)

        store = Gtk.ListStore(str, str)
        for string in formats.TIME_METRIC_LIST:
            store.append( [string, formats.TIME_METRIC_TRANS_DICT[string]] )

        combo = Gtk.ComboBox.new_with_model(store)
        grid.attach(combo, 2, 2, 1, 1)

        renderer_text = Gtk.CellRendererText()
        combo.pack_start(renderer_text, True)
        combo.add_attribute(renderer_text, 'text', 1)
        combo.set_entry_text_column(1)
        combo.set_active(
            formats.TIME_METRIC_LIST.index(
                self.edit_obj.autostop_time_unit,
            )
        )
        if not self.edit_obj.autostop_time_flag:
            combo.set_sensitive(False)
        # (Signal connect appears below)

        # (Signal connects from above)
        checkbutton.connect(
            'toggled',
            self.on_autostop_time_button_toggled,
            spinbutton,
            combo,
        )
        spinbutton.connect(
            'value-changed',
            self.on_autostop_time_spinbutton_changed,
        )
        combo.connect('changed', self.on_autostop_time_combo_changed)

        checkbutton2 = self.add_checkbutton(grid,
            _('Stop download operation after this many videos'),
            None,
            0, 3, 1, 1,
        )
        checkbutton2.set_active(self.edit_obj.autostop_videos_flag)
        # (Signal connect appears below)

        spinbutton2 = self.add_spinbutton(grid,
            1, None, 1,
            None,
            1, 3, 1, 1,
        )
        spinbutton2.set_value(self.edit_obj.autostop_videos_value)
        if not self.edit_obj.autostop_videos_flag:
            spinbutton2.set_sensitive(False)
        # (Signal connect appears below)

        # (Signal connects from above)
        checkbutton2.connect(
            'toggled',
            self.on_autostop_videos_button_toggled,
            spinbutton2,
        )
        spinbutton2.connect(
            'value-changed',
            self.on_autostop_videos_spinbutton_changed,
        )

        checkbutton3 = self.add_checkbutton(grid,
            _('Stop download operation after this much disk space'),
            None,
            0, 4, 1, 1,
        )
        checkbutton3.set_active(self.edit_obj.autostop_size_flag)
        # (Signal connect appears below)

        spinbutton3 = self.add_spinbutton(grid,
            1, None, 1,
            None,
            1, 4, 1, 1,
        )
        spinbutton3.set_value(self.edit_obj.autostop_size_value)
        if not self.edit_obj.autostop_size_flag:
            spinbutton3.set_sensitive(False)
        # (Signal connect appears below)

        combo3 = self.add_combo(grid,
            formats.FILESIZE_METRIC_LIST,
            None,
            2, 4, 1, 1,
        )
        combo3.set_active(
            formats.FILESIZE_METRIC_LIST.index(
                self.edit_obj.autostop_size_unit,
            )
        )
        if not self.edit_obj.autostop_size_flag:
            combo3.set_sensitive(False)
        # (Signal connect appears below)

        # (Signal connects from above)
        checkbutton3.connect(
            'toggled',
            self.on_autostop_size_button_toggled,
            spinbutton3,
            combo3,
        )
        spinbutton3.connect(
            'value-changed',
            self.on_autostop_size_spinbutton_changed,
        )
        combo3.connect('changed', self.on_autostop_size_combo_changed)

        self.add_label(grid,
            '<i>' + _(
                'N.B. Disk space is estimated. This setting does not apply' \
                + ' to simulated downloads',
            ) + '</i>',
            0, 5, grid_width, 1,
        )


    def setup_conflicts_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Conflicts' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Scheduled downloads > Conflicts'
        )

        tab, grid = self.add_notebook_tab(_('_Conflicts'))

        # Conflict settings
        self.add_label(grid,
            '<u>' + _('Conflict settings') + '</u>',
            0, 0, 1, 1,
        )

        self.add_label(grid,
            _('If another scheduled download is running:'),
            0, 1, 1, 1,
        )

        combo4_list = [
            [
                _(
                'Add channels, playlists and folders to the end of the queue',
                ),
                'join',
            ],
            [
                _(
                'Add channels, playlists and folders to the beginning of the' \
                + ' queue',
                ),
                'priority',
            ],
            [
                _(
                'Do nothing, just wait until the next scheduled download' \
                + ' time',
                ),
                'skip',
            ],
        ]

        combo4 = self.add_combo_with_data(grid,
            combo4_list,
            'join_mode',
            0, 2, 1, 1,
        )
        combo4.set_hexpand(True)

        self.add_checkbutton(grid,
            _('This scheduled download takes priority over others') \
            + '\n' \
            + _(
            'Other scheduled downloads won\'t start until this one is' \
            + ' finished',
            ),
            'exclusive_flag',
            0, 3, 1, 1,
        )


    def setup_media_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'General' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Scheduled downloads > Media'
        )

        tab, grid = self.add_notebook_tab(_('_Media'))
        grid_width = 4

        # Media to download
        self.add_label(grid,
            '<u>' + _('Media to download') + '</u>',
            0, 0, grid_width, 1,
        )

        self.radiobutton = self.add_radiobutton(grid,
            None,
            _('Check/download everything'),
            None,
            None,
            0, 1, grid_width, 1,
        )

        self.radiobutton2 = self.add_radiobutton(grid,
            self.radiobutton,
            _('Only check/download the media below'),
            None,
            None,
            0, 2, grid_width, 1,
        )
        if not self.edit_obj.all_flag:
            self.radiobutton2.set_active(True)
        self.radiobutton2.connect(
            'toggled',
            self.on_all_flag_toggled,
        )

        self.add_label(grid,
            '<i>' + _(
            'Hint: you can drag and drop channels, playlists and your own' \
            + ' folders here',
            ) + '</i>',
            0, 3, grid_width, 1,
        )

        # Create a treeview, containing the .dbid (invisible) and .name
        #   (visible) for each media data object added
        frame = Gtk.Frame()
        grid.attach(frame, 0, 4, grid_width, 1)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)

        treestore = Gtk.ListStore(int, str)

        treeview = Gtk.TreeView()
        scrolled.add(treeview)
        treeview.set_model(treestore)
        treeview.set_headers_visible(False)
        treeview.set_hexpand(True)

        renderer_text = Gtk.CellRendererText()
        column_text = Gtk.TreeViewColumn(
            '',
            renderer_text,
            text=1,
        )
        treeview.append_column(column_text)

        # Initialise the treeview
        self.setup_media_tab_update_treeview(treestore)

        # Set up drag and drop into the treeview
        drag_target_list = [('text/plain', 0, 0)]
        treeview.enable_model_drag_dest(
            # Table of targets the drag procedure supports, and array length
            drag_target_list,
            # Bitmask of possible actions for a drag from this widget
            Gdk.DragAction.DEFAULT,
        )
        treeview.connect(
            'drag-drop',
            self.on_video_index_drag_drop,
        )
        treeview.connect(
            'drag-data-received',
            self.on_video_index_drag_data_received,
        )

        # Editing widgets
        obj_list = []
        for media_data_obj in self.app_obj.container_reg_dict.values():

            if not isinstance(media_data_obj, media.Folder) \
            or not media_data_obj.fixed_flag:
                obj_list.append(media_data_obj)

        obj_list.sort(key=lambda x: x.name.lower())

        combostore = Gtk.ListStore(int, str)
        for media_data_obj in obj_list:
            combostore.append( [media_data_obj.dbid, media_data_obj.name] )

        combo = Gtk.ComboBox.new_with_model(combostore)
        grid.attach(combo, 0, 5, 1, 1)

        renderer_text = Gtk.CellRendererText()
        combo.pack_start(renderer_text, True)
        combo.add_attribute(renderer_text, 'text', 1)

        combo.set_entry_text_column(1)
        combo.set_active(0)

        button = Gtk.Button()
        grid.attach(button, 1, 5, 1, 1)
        button.set_label(_('Add'))
        button.connect(
            'clicked',
            self.on_add_media_button_clicked,
            combo,
            treestore,
        )

        button2 = Gtk.Button()
        grid.attach(button2, 2, 5, 1, 1)
        button2.set_label(_('Remove'))
        button2.connect(
            'clicked',
            self.on_remove_media_button_clicked,
            treeview,
        )

        button3 = Gtk.Button()
        grid.attach(button3, 3, 5, 1, 1)
        button3.set_label(_('Clear list'))
        button3.connect(
            'clicked',
            self.on_clear_media_button_clicked,
            treestore,
        )


    def setup_media_tab_update_treeview(self, liststore):

        """Called by self.setup_media_tab() and some callbacks.

        Updates the treeview to display the media.Scheduled object's
        .media_list IV, first checking that any specified media data objects
        still exist.

        Args:

            liststore (Gtk.ListStore): The treeview's model

        """

        liststore.clear()

        media_list = self.retrieve_val('media_list')
        for dbid in media_list:

            # This media data object may be deleted while the window is open
            #   (but the .media_list IV is checked, when the 'Save' or 'Apply'
            #   buttons are clicked)
            if dbid in self.app_obj.container_reg_dict:
                media_data_obj = self.app_obj.media_reg_dict[dbid]
                liststore.append([media_data_obj.dbid, media_data_obj.name])


    def setup_limits_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Limits' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Scheduled downloads > Limits'
        )

        tab, grid = self.add_notebook_tab(_('_Limits'))
        grid_width = 3

        # Performance limits
        self.add_label(grid,
            '<u>' + _('Performance limits') + '</u>',
            0, 0, grid_width, 1,
        )

        self.add_label(grid,
            '<i>' + _('Limits are applied when you start downloading a' \
            + ' video/channel/playlist') + '</i>',
            0, 1, grid_width, 1,
        )

        self.add_label(grid,
            '<i>' + _('These limits override the default and alternative' \
            + ' limits specified elsewhere') + '</i>',
            0, 2, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Limit simultaneous downloads to'),
            'scheduled_num_worker_apply_flag',
            0, 3, 1, 1,
        )
        checkbutton.set_hexpand(False)

        spinbutton = self.add_spinbutton(grid,
            self.app_obj.num_worker_min,
            self.app_obj.num_worker_max,
            1,                  # Step
            'scheduled_num_worker',
            1, 3, 1, 1,
        )

        checkbutton2 = self.add_checkbutton(grid,
            _('Limit download speed to'),
            'scheduled_bandwidth_apply_flag',
            0, 4, 1, 1,
        )
        checkbutton2.set_hexpand(False)

        spinbutton2 = self.add_spinbutton(grid,
            self.app_obj.bandwidth_min,
            self.app_obj.bandwidth_max,
            1,                  # Step
            'scheduled_bandwidth',
            1, 4, 1, 1,
        )

        self.add_label(grid,
            'KiB/s',
            2, 3, 1, 1,
        )


    def setup_other_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Other' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Scheduled downloads > Other'
        )

        tab, grid = self.add_notebook_tab(_('_Other'))

        # Other settings
        self.add_label(grid,
            '<u>' + _('Other settings') + '</u>',
            0, 0, 1, 1,
        )

        self.add_checkbutton(grid,
            _(
            'Ignore time-saving preferences, and check/download the whole' \
            + ' channel/playlist/folder',
            ),
            'ignore_limits_flag',
            0, 1, 1, 1,
        )

        self.add_checkbutton(grid,
            _('Shut down Tartube when this scheduled download has finished'),
            'shutdown_flag',
            0, 2, 1, 1,
        )


    # Callback class methods


#   def on_button_apply_options_clicked():  # Inherited from GenericConfigWin


#   def on_button_edit_options_clicked():   # Inherited from GenericConfigWin


#   def on_button_remove_options_clicked(): # Inherited from GenericConfigWin


    def on_add_media_button_clicked(self, button, combo, liststore):

        """Called by callback in self.setup_media_tab().

        Args:

            button (Gtk.Button): The widget clicked

            combo (Gtk.ComboBox): A combo in which the user has selected a new
                media data object

            liststore (Gtk.ListStore): The treeview's model

        """

        combo_iter = combo.get_active_iter()
        combo_model = combo.get_model()
        dbid = combo_model[combo_iter][0]

        # Check the media data object hasn't already been added to the list,
        #   and that is still exists in the media data registry
        media_list = self.retrieve_val('media_list')

        if not dbid in media_list \
        and dbid in self.app_obj.container_reg_dict:

            media_list.append(dbid)
            self.edit_dict['media_list'] = media_list

            self.radiobutton2.set_active(True)

            # Update the treeview
            self.setup_media_tab_update_treeview(liststore)


    def on_add_timetable_button_clicked(self, button, liststore, combo, \
    spinbutton, spinbutton2):

        """Called by callback in self.setup_start_tab().

        Args:

            button (Gtk.Button): The widget clicked

            liststore (Gtk.ListStore): The treeview's model

            combo (Gtk.ComboBox): A widget to modify

            spinbutton, spinbutton2 (Gtk.SpinButton): Other widgets to modify

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        day_str = model[tree_iter][1]

        hours = int(spinbutton.get_value())
        minutes = int(spinbutton2.get_value())

        # Each 'mini_list' is in the form [ day_string, time_string ]
        timetable_list = self.retrieve_val('timetable_list')
        mini_list = [
            day_str,
            '{:02d}'.format(hours) + ':' + '{:02d}'.format(minutes),
        ]
        # Check for duplicates
        for other_list in timetable_list:
            if other_list[0] == mini_list[0] \
            and other_list[1] == mini_list[1]:
                return

        # No duplicates found
        timetable_list.append(mini_list)
        self.edit_dict['timetable_list'] = timetable_list
        self.setup_start_tab_update_treeview(liststore)


    def on_all_flag_toggled(self, radiobutton):

        """Called from callback in self.setup_media_tab().

        Enables/disables checking/downloading all media.

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

        """

        if radiobutton.get_active():
            self.edit_obj.all_flag = False
        else:
            self.edit_obj.all_flag = True


    def on_autostop_size_button_toggled(self, checkbutton, spinbutton, combo):

        """Called from callback in self.setup_stop_tab().

        Enables/disables auto-stopping a download operation after a certain
        amount of disk space.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            spinbutton (Gtk.SpinButton): Another widget to modify

            combo (Gtk.ComboBox): Another widget to modify

        """

        if not checkbutton.get_active():
            self.edit_dict['autostop_size_flag'] = False
            spinbutton.set_sensitive(False)
            combo.set_sensitive(False)
        else:
            self.edit_dict['autostop_size_flag'] = True
            spinbutton.set_sensitive(True)
            combo.set_sensitive(True)


    def on_autostop_size_combo_changed(self, combo):

        """Called from a callback in self.setup_stop_tab().

        Sets the disk space unit at which a download operation is auto-stopped.

        Args:

            combo (Gtk.ComboBox): The widget clicked

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        self.edit_dict['autostop_size_unit'] = model[tree_iter][0]


    def on_autostop_size_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_stop_tab().

        Sets the disk space value at which a download operation is
        auto-stopped.

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        self.edit_dict['autostop_size_value'] = spinbutton.get_value()


    def on_autostop_time_button_toggled(self, checkbutton, spinbutton, combo):

        """Called from callback in self.setup_stop_tab().

        Enables/disables auto-stopping a download operation after a certain
        time.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            spinbutton (Gtk.SpinButton): Another widget to modify

            combo (Gtk.ComboBox): Another widget to modify

        """

        if not checkbutton.get_active():
            self.edit_dict['autostop_time_flag'] = False
            spinbutton.set_sensitive(False)
            combo.set_sensitive(False)
        else:
            self.edit_dict['autostop_time_flag'] = True
            spinbutton.set_sensitive(True)
            combo.set_sensitive(True)


    def on_autostop_time_combo_changed(self, combo):

        """Called from a callback in self.setup_stop_tab().

        Sets the time unit at which a download operation is auto-stopped.

        Args:

            combo (Gtk.ComboBox): The widget clicked

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        self.edit_dict['autostop_time_unit'] = model[tree_iter][0]


    def on_autostop_time_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_stop_tab().

        Sets the time value at which a download operation is auto-stopped.

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        self.edit_dict['autostop_time_value'] = spinbutton.get_value()


    def on_autostop_videos_button_toggled(self, checkbutton, spinbutton):

        """Called from callback in self.setup_stop_tab().

        Enables/disables auto-stopping a download operation after a certain
        number of videos.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            spinbutton (Gtk.SpinButton): Another widget to modify

        """

        if not checkbutton.get_active():
            self.edit_dict['autostop_videos_flag'] = False
            spinbutton.set_sensitive(False)
        else:
            self.edit_dict['autostop_videos_flag'] = True
            spinbutton.set_sensitive(True)


    def on_autostop_videos_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_stop_tab().

        Sets the number of videos at which a download operation is
        auto-stopped.

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        self.edit_dict['autostop_videos_value'] = spinbutton.get_value()


    def on_clear_media_button_clicked(self, button, liststore):

        """Called by callback in self.setup_media_tab().

        Args:

            button (Gtk.Button): The widget clicked

            liststore (Gtk.ListStore): The treeview's model

        """

        # Update the IV
        self.edit_dict['media_list'] = []
        # Update widgets
        liststore.clear()
        self.radiobutton.set_active(True)


    def on_custom_dl_combo_changed(self, combo):

        """Called from callback in self.setup_general_tab().

        Sets the IV.

        Args:

            combo (Gtk.ComboBox): The widget clicked

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        value = model[tree_iter][1]

        if value is None or value == '':
            self.edit_dict['custom_dl_uid'] = None
        else:
            self.edit_dict['custom_dl_uid'] = int(value)


    def on_dl_mode_combo_changed(self, combo, combo2):

        """Called from callback in self.setup_general_tab().

        Sets the IV, and (de)sensitises other widgets.

        Args:

            combo (Gtk.ComboBox): The widget clicked

            combo2 (Gtk.ComboBox): Another widget to update

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        self.edit_dict['dl_mode'] = model[tree_iter][1]

        if self.edit_dict['dl_mode'] == 'custom_real':
            combo2.set_sensitive(True)
        else:
            combo2.set_active(0)
            combo2.set_sensitive(False)


    def on_remove_media_button_clicked(self, button, treeview):

        """Called by callback in self.setup_media_tab().

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeView): The list of media data objects

        """

        selection = treeview.get_selection()
        (model, tree_iter) = selection.get_selected()
        if tree_iter is None:

            # Nothing selected
            return

        else:

            dbid = model[tree_iter][0]

        # Check the media data object exists in the list and in the media data
        #   registry
        media_list = self.retrieve_val('media_list')
        if dbid in media_list:

            media_list.remove(dbid)
            self.edit_dict['media_list'] = media_list

            # Update widgets
            self.setup_media_tab_update_treeview(treeview.get_model())
            if not media_list:
                self.radiobutton.set_active(True)


    def on_remove_timetable_button_clicked(self, button, treeview, liststore):

        """Called by callback in self.setup_start_tab().

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeView): The treeview to be updated

            liststore (Gtk.ListStore): The treeview's model

        """

        selection = treeview.get_selection()
        (model, tree_iter) = selection.get_selected()
        if tree_iter is None:
            # Nothing selected
            return

        display_str = model[tree_iter][0]

        match = re.search(r'^(.*)\s(\d\d\:\d\d)', display_str)
        if match:

            translated_str = match.groups()[0]
            time_str = match.groups()[1]

            # Compile a reversed dictionary for lookup
            rev_dict = {}
            for key in formats.SPECIFIED_DAYS_DICT.keys():
                rev_dict[formats.SPECIFIED_DAYS_DICT[key]] = key

            if not translated_str in rev_dict:
                return

            # Each 'mini_list' is in the form [ day_string, time_string ]
            # 'display_str' contains 'time_string', and a translated version of
            #   'day_string'
            # Compile the 'mini_list' for the new entry
            mini_list = [ rev_dict[translated_str], time_str ]

            # Look for a match in the IV
            new_list = []
            for other_list in self.retrieve_val('timetable_list'):
                if other_list[0] != mini_list[0] \
                or other_list[1] != mini_list[1]:
                    new_list.append(other_list)

            # Update the IV and the treeview
            self.edit_dict['timetable_list'] = new_list
            self.setup_start_tab_update_treeview(liststore)


    def on_start_mode_combo_changed(self, combo, label, spinbutton, combo2, \
    label2, combo3, spinbutton2, spinbutton3, button, button2):

        """Called from callback in self.setup_start_tab().

        Sets the IV, and (de)sensitises other widgets.

        Args:

            label (Gtk.Label): A widget to be modified

            spinbutton (Gtk.SpinButton): Another widget to be modified

            combo2 (Gtk.Combo): Another widget to be modified

            label2 (Gtk.Label): Another widget to be modified

            combo3 (Gtk.Combo): Another widget to be modified

            spinbutton2, spinbutton3 (Gtk.SpinButton): Other widgets to be
                modified

            button, button (Gtk.Button): Other widgets to be modified

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        self.edit_dict['start_mode'] = model[tree_iter][1]

        self.setup_start_tab_update_widgets(
            label,
            spinbutton,
            combo2,
            label2,
            combo3,
            spinbutton2,
            spinbutton3,
            button,
            button2,
        )


    def on_video_index_drag_drop(self, treeview, drag_context, x, y, time):

        """Called from callback in self.setup_media_tab().

        Override the usual Gtk handler, and allow
        self.on_video_index_drag_data_received() to collect the results of the
        drag procedure.

        Args:

            treeview (Gtk.TreeView): This tab's treeview

            drag_context (GdkX11.X11DragContext): Data from the drag procedure

            x, y (int): Cell coordinates in the treeview

            time (int): A timestamp

        """

        # Must override the usual Gtk handler
        treeview.stop_emission('drag_drop')

        # The second of these lines cause the 'drag-data-received' signal to be
        #   emitted
        target_list = drag_context.list_targets()
        treeview.drag_get_data(drag_context, target_list[-1], time)


    def on_video_index_drag_data_received(self, treeview, drag_context, x, y, \
    selection_data, info, timestamp):

        """Called from callback in self.setup_media_tab().

        Retrieve the media data object being dragged. update the
        media.Scheduled object, and update the treeview itself.

        Args:

            treeview (Gtk.TreeView): This tab's treeview

            drag_context (GdkX11.X11DragContext): Data from the drag procedure

            x, y (int): Cell coordinates in the treeview

            selection_data (Gtk.SelectionData): Data from the dragged row

            info (int): Ignored

            timestamp (int): Ignored

        """

        # Must override the usual Gtk handler
        treeview.stop_emission('drag_data_received')

        # Get the dragged media data object
        old_selection \
        = self.app_obj.main_win_obj.video_index_treeview.get_selection()
        (model, start_iter) = old_selection.get_selected()
        if start_iter is not None:

            drag_dbid = model[start_iter][0]

            # Check the media data object hasn't already been added to the
            #   list, and that is still exists in the media data registry
            media_list = self.retrieve_val('media_list')
            if not drag_dbid in media_list \
            and drag_dbid in self.app_obj.container_reg_dict:

                # (System folders can't be dragged here)
                media_data_obj = self.app_obj.media_reg_dict[drag_dbid]

                if not isinstance(media_data_obj, media.Folder) \
                or not media_data_obj.fixed_flag:

                    media_list.append(drag_dbid)
                    self.edit_dict['media_list'] = media_list

                    self.radiobutton2.set_active(True)

                    # Update the treeview
                    self.setup_media_tab_update_treeview(treeview.get_model())


class SystemPrefWin(GenericPrefWin):

    """Python class for a 'preference window' to modify various system
    settings.

    Args:

        app_obj (mainapp.TartubeApp): The main application object

        init_mode (str or None): If specified, a tab is automatically selected;
            one of the values specified in the comments to self.select_tab()

    """


    # Standard class methods


    def __init__(self, app_obj, init_mode=None):

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences window starts here.' \
            + ' In the menu, click Edit > System preferences...'
        )

        Gtk.Window.__init__(self, title=_('System preferences'))

        if self.is_duplicate(app_obj, init_mode):
            return

        # IV list - class objects
        # -----------------------
        # The mainapp.TartubeApp object
        self.app_obj = app_obj


        # IV list - Gtk widgets
        # ---------------------
        self.grid = None                        # Gtk.Grid
        self.notebook = None                    # Gtk.Notebook
        self.files_inner_notebook = None        # Gtk.Notebook
        self.operations_inner_notebook = None   # Gtk.Notebook
        self.downloader_inner_notebook = None   # Gtk.Notebook
        self.ok_button = None                   # Gtk.Button
        # (IVs used to handle widget changes in the 'General' tab)
        self.radiobutton = None                 # Gtk.RadioButton
        self.radiobutton2 = None                # Gtk.RadioButton
        self.radiobutton3 = None                # Gtk.RadioButton
        self.radiobutton4 = None                # Gtk.RadioButton
        self.radiobutton5 = None                # Gtk.RadioButton
        self.spinbutton = None                  # Gtk.SpinButton
        self.spinbutton2 = None                 # Gtk.SpinButton
        self.spinbutton3 = None                 # Gtk.SpinButton
        self.spinbutton4 = None                 # Gtk.SpinButton
        # (IVs used to handle widget changes in the 'Files' tab)
        self.entry = None                       # Gtk.Entry
        self.entry2 = None                      # Gtk.Entry
        self.url_liststore = None               # Gtk.ListStore
        # (IVs used to handle widget changes in the 'Custom' tab)
        self.custom_liststore = None            # Gtk.ListStore
        # (IVs used to handle widget changes in the 'Livestream' tab)
        self.livestream_radiobutton = None      # Gtk.RadioButton
        self.livestream_radiobutton2 = None     # Gtk.RadioButton
        self.livestream_radiobutton3 = None     # Gtk.RadioButton
        self.livestream_radiobutton4 = None     # Gtk.RadioButton
        self.livestream_radiobutton5 = None     # Gtk.RadioButton
        # (IVs used to hanle widget changes in the 'Forks' tab)
        self.forks_radiobutton = None           # Gtk.RadioButton
        self.forks_radiobutton2 = None          # Gtk.RadioButton
        self.forks_radiobutton3 = None          # Gtk.RadioButton
        self.forks_entry = None                 # Gtk.RadioButton
        # (IVs used to hanle widget changes in the 'File paths' tab)
        self.filepaths_combo = None             # Gkt.ComboBox
        # (IVs used to handle widget changes in the 'Downloaders' tab)
        self.path_liststore = None              # Gtk.ListStore
        self.cmd_liststore = None               # Gtk.ListStore
        # (IVs used to handle widget changes in the 'Scheduling' tab)
        self.schedule_liststore = None          # Gtk.ListStore
        # (IVs used to handle widget changes in the 'Options' tab)
        self.options_liststore = None           # Gtk.ListStore
        self.ffmpeg_liststore = None            # Gtk.ListStore
        # (IVs used to open the window at a particular tab)
        self.filesinner_notebook = None         # Gtk.Notebook
        self.operations_inner_notebook = None   # Gtk.Notebook


        # IV list - other
        # ---------------
        # Size (in pixels) of gaps between preference window widgets
        self.spacing_size = self.app_obj.default_spacing_size

        # Code
        # ----

        # Set up the preference window
        self.setup()

        # Automatically open a particular tab, if required
        self.select_tab(init_mode)


    # Public class methods


    def is_duplicate(self, app_obj, init_mode=None):

        """Called by self.__init__.

        Don't open this preference window, if another preference window of the
        same class is already open.

        If 'init_mode' is specified, switch the visible tab in the existing
        preference window (if any).

        Args:

            app_obj (mainapp.TartubeApp): The main application object

            init_mode (str or None): One of the values specified in the
                comments to self.select_tab()

        Return values:

            True if a duplicate is found, False if not

        """

        for config_win_obj in app_obj.main_win_obj.config_win_list:

            if type(self) == type(config_win_obj):

                # Duplicate found. Make it prominent...
                config_win_obj.present()
                # ...and switch to a particular tab, if required
                config_win_obj.select_tab(init_mode)

                return True

        # Not a duplicate
        return False


#   def setup():                # Inherited from GenericConfigWin


#   def setup_grid():           # Inherited from GenericConfigWin


#   def setup_notebook():       # Inherited from GenericConfigWin


#   def add_notebook_tab():     # Inherited from GenericConfigWin


#   def setup_button_strip():   # Inherited from GenericPrefWin


#   def setup_gap():            # Inherited from GenericConfigWin


    def select_tab(self, init_mode=None):

        """Called by self.__init__().

        On startup, automatically open a particular tab, if required.

        Args:

            init_mode (str or None): If specified:
                - 'clips' - video clip preferences
                - 'custom_dl' - custom download preferences
                - 'db' - options for switching the Tartube database
                - 'downloads' - Download operation preferences
                - 'forks' - youtube-dl forks,
                - 'live' - livestream options
                - 'options' - the list of download options
                - 'paths' - youtube-dl paths,
                - 'slices' - video slice preferences
                - (any other value is ignored)

        """

        if init_mode is not None:

            if init_mode == 'clips':
                self.select_clips_tab()
            elif init_mode == 'custom_dl':
                self.select_custom_dl_tab()
            elif init_mode == 'db':
                self.select_switch_db_tab()
            elif init_mode == 'downloads':
                self.select_downloads_tab()
            elif init_mode == 'forks':
                self.select_forks_tab()
            elif init_mode == 'live':
                self.select_livestream_tab()
            elif init_mode == 'options':
                self.select_options_tab()
            elif init_mode == 'paths':
                self.select_paths_tab()
            elif init_mode == 'slices':
                self.select_slices_tab()


    def select_clips_tab(self):

        """Can be called by anything.

        Makes the visible tab the one on which the video clip preferences are
        displayed.
        """

        # Opens tab: self.setup_operations_clips_tab()
        if not self.app_obj.simple_prefs_flag:
            self.notebook.set_current_page(4)
            self.operations_inner_notebook.set_current_page(8)
        else:
            self.notebook.set_current_page(4)
            self.operations_inner_notebook.set_current_page(7)


    def select_custom_dl_tab(self):

        """Can be called by anything.

        Makes the visible tab the one on which the custom download preferences
        are displayed.
        """

        # Opens tab: self.setup_operations_custom_dl_tab()
        self.notebook.set_current_page(4)
        self.operations_inner_notebook.set_current_page(4)


    def select_switch_db_tab(self):

        """Can be called by anything.

        Makes the visible tab the one on which the user can set Tartube's
        data directory (which contains the Tartube database file).
        """

        # Opens tab: self.setup_files_database_tab()
        if not self.app_obj.simple_prefs_flag:
            self.notebook.set_current_page(1)
            self.files_inner_notebook.set_current_page(2)
        else:
            self.notebook.set_current_page(1)
            self.files_inner_notebook.set_current_page(0)


    def select_downloads_tab(self):

        """Can be called by anything.

        Makes the visible tab the one on which the custom download preferences
        are displayed.
        """

        # Opens tab: self.setup_operations_downloads_tab()
        self.notebook.set_current_page(4)
        self.operations_inner_notebook.set_current_page(2)


    def select_forks_tab(self):

        """Can be called by anything.

        Makes the visible tab the one on which the user can set youtube-dl
        forks.
        """

        # Opens tab: self.setup_downloader_forks_tab()
        self.notebook.set_current_page(5)
        self.downloader_inner_notebook.set_current_page(0)


    def select_livestream_tab(self):

        """Can be called by anything.

        Makes the visible tab the one on which the user can set livestream
        options.
        """

        # Opens tab: self.setup_operations_livestreams_tab()
        if not self.app_obj.simple_prefs_flag:
            self.notebook.set_current_page(4)
            self.operations_inner_notebook.set_current_page(6)
        else:
            self.notebook.set_current_page(4)
            self.operations_inner_notebook.set_current_page(5)


    def select_options_tab(self):

        """Can be called by anything.

        Makes the visible tab the one on which the list of download options is
        displayed.
        """

        # Opens tab: self.setup_options_dl_list_tab()
        self.notebook.set_current_page(6)


    def select_paths_tab(self):

        """Can be called by anything.

        Makes the visible tab the one on which the user can set youtube-dl
        paths.
        """

        # Opens tab: self.setup_downloader_paths_tab()
        self.notebook.set_current_page(5)
        self.downloader_inner_notebook.set_current_page(1)


    def select_slices_tab(self):

        """Can be called by anything.

        Makes the visible tab the one on which the video slice preferences are
        displayed.
        """

        # Opens tab: self.setup_operations_slices_tab()
        if not self.app_obj.simple_prefs_flag:
            self.notebook.set_current_page(4)
            self.operations_inner_notebook.set_current_page(9)
        else:
            self.notebook.set_current_page(4)
            self.operations_inner_notebook.set_current_page(8)


    # (Setup tabs)


    def setup_tabs(self):

        """Called by self.setup(), .on_button_apply_clicked() and
        .on_button_reset_clicked().

        Sets up the tabs for this preference window.
        """

        self.setup_general_tab()
        self.setup_files_tab()
        self.setup_windows_tab()
        self.setup_scheduling_tab()
        self.setup_operations_tab()
        self.setup_downloader_tab()
        self.setup_options_tab()
        self.setup_output_tab()


    def setup_general_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'General' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > General'
        )

        # Add this tab...
        tab, grid = self.add_notebook_tab(_('_General'), 0)

        # ...and an inner notebook...
        inner_notebook = self.add_inner_notebook(grid)

        # ...with its own tabs
        self.setup_general_application_tab(inner_notebook)
        if not self.app_obj.simple_prefs_flag:
            self.setup_general_modules_tab(inner_notebook)


    def setup_general_application_tab(self, inner_notebook):

        """Called by self.setup_general_tab().

        Sets up the 'Application' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > General > Application'
        )

        # Import the main window (for convenience)
        main_win_obj = self.app_obj.main_win_obj

        tab, grid = self.add_inner_notebook_tab(
            _('_Application'),
            inner_notebook,
        )
        grid_width = 3

        # Application details
        self.add_label(grid,
            '<u>' + _('Application details') + '</u>',
            0, 0, grid_width, 1,
        )

        label = self.add_label(grid,
            _('Version'),
            0, 1, 1, 1,
        )
        label.set_hexpand(False)

        self.entry = self.add_entry(grid,
            __main__.__version__,
            False,
            1, 1, 1, 1,
        )

        self.entry = self.add_entry(grid,
            __main__.__date__,
            False,
            2, 1, 1, 1,
        )

        label = self.add_label(grid,
            _('Locale override'),
            0, 2, 1, 1,
        )
        label.set_hexpand(False)

        store = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str)
        store.append(
            [
                self.app_obj.main_win_obj.pixbuf_dict['slice_small'],
                _('Use your system locale'),
                ''
            ]
        )

        # (This list is for a second combo, using the same list as the one
        #   above, but not clickable by the user)
        label2 = self.add_label(grid,
            _('Current locale'),
            0, 3, 1, 1,
        )
        label2.set_hexpand(False)

        store2 = Gtk.ListStore(GdkPixbuf.Pixbuf, str, str)
        store2.append(
            [
                self.app_obj.main_win_obj.pixbuf_dict['slice_small'],
                _('Unrecognised locale'),
                ''
            ]
        )

        for this_locale in formats.LOCALE_LIST:

            # (Some locales are in format "en_GB", some in format "fr")
            if not 'flag_' + this_locale in \
            self.app_obj.main_win_obj.pixbuf_dict:
                flag_locale = this_locale[:2]
            else:
                flag_locale = this_locale

            pixbuf = \
            self.app_obj.main_win_obj.pixbuf_dict['flag_' + flag_locale]

            store.append(
                [ pixbuf, formats.LOCALE_DICT[this_locale], this_locale ],
            )
            store2.append(
                [ pixbuf, formats.LOCALE_DICT[this_locale], this_locale ],
            )

        combo = Gtk.ComboBox.new_with_model(store)
        grid.attach(combo, 1, 2, 1, 1)
        combo.set_hexpand(False)

        renderer_pixbuf = Gtk.CellRendererPixbuf()
        combo.pack_start(renderer_pixbuf, False)
        combo.add_attribute(renderer_pixbuf, 'pixbuf', 0)

        renderer_text = Gtk.CellRendererText()
        combo.pack_start(renderer_text, True)
        combo.add_attribute(renderer_text, 'text', 1)

        if self.app_obj.override_locale is None:
            combo.set_active(0)
        else:
            combo.set_active(
                formats.LOCALE_LIST.index(self.app_obj.override_locale) + 1
            )
        combo.connect('changed', self.on_locale_combo_changed, grid)

        combo2 = Gtk.ComboBox.new_with_model(store2)
        grid.attach(combo2, 1, 3, 1, 1)
        combo2.set_hexpand(False)
        combo2.set_sensitive(False)

        renderer_pixbuf = Gtk.CellRendererPixbuf()
        combo2.pack_start(renderer_pixbuf, False)
        combo2.add_attribute(renderer_pixbuf, 'pixbuf', 0)

        renderer_text = Gtk.CellRendererText()
        combo2.pack_start(renderer_text, True)
        combo2.add_attribute(renderer_text, 'text', 1)

        if self.app_obj.current_locale is None:
            combo2.set_active(0)

        elif self.app_obj.current_locale in formats.LOCALE_LIST:
            combo2.set_active(
                formats.LOCALE_LIST.index(self.app_obj.current_locale) + 1
            )

        else:

            # (Some locales are in format "en_GB", some in format "fr")
            alt_locale = self.app_obj.current_locale[:2]
            if alt_locale in formats.LOCALE_LIST:
                combo2.set_active(
                    formats.LOCALE_LIST.index(alt_locale) + 1
                )

            else:
                combo2.set_active(0)

        # N.B. The call to self.on_locale_combo_changed() can create another
        #   secondary grid at 0, 4

        # Preferences mode
        self.add_label(grid,
            '<u>' + _('Preferences mode') + '</u>',
            0, 5, grid_width, 1,
        )

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 6, grid_width, 1)

        if self.app_obj.simple_prefs_flag:
            frame = self.add_pixbuf(grid2,
                'hand_right_large',
                0, 0, 1, 1,
            )
            frame.set_hexpand(False)
            frame.set_size_request(75, -1)

        else:
            frame = self.add_pixbuf(grid2,
                'hand_left_large',
                0, 0, 1, 1,
            )
            frame.set_hexpand(False)
            frame.set_size_request(75, -1)

        button = Gtk.Button()
        grid2.attach(button, 1, 0, 1, 1)
        if not self.app_obj.simple_prefs_flag:
            button.set_label(_('Hide advanced preferences'))
        else:
            button.set_label(_('Show advanced preferences'))
        button.set_hexpand(True)
        button.connect('clicked', self.on_simple_prefs_clicked)


    def setup_general_modules_tab(self, inner_notebook):

        """Called by self.setup_general_tab().

        Sets up the 'Modules' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > General > Modules'
        )

        tab, grid = self.add_inner_notebook_tab(_('_Modules'), inner_notebook)
        grid_width = 2

        # Module availability
        self.add_label(grid,
            '<u>' + _('Module availability') + '</u>',
            0, 0, grid_width, 1,
        )

        self.add_checkbutton(grid,
            _(
            'feedparser module is available (required for detecting' \
            + ' livestreams)',
            ),
            mainapp.HAVE_FEEDPARSER_FLAG,
            False,                      # Can't be toggled by user
            0, 1, grid_width, 1,
        )

        self.add_checkbutton(grid,
            _('matplotlib module is available (draws graphs)'),
            mainapp.HAVE_MATPLOTLIB_FLAG,
            False,                      # Can't be toggled by user
            0, 2, grid_width, 1,
        )

        self.add_checkbutton(grid,
            _(
            'moviepy module is available (finds the length of videos, if' \
            + ' unknown)',
            ),
            mainapp.HAVE_MOVIEPY_FLAG,
            False,                      # Can't be toggled by user
            0, 3, grid_width, 1,
        )

        self.add_checkbutton(grid,
            _(
            'playsound module is available (sound an alarm when a livestream' \
            + ' starts)',
            ),
            mainapp.HAVE_PLAYSOUND_FLAG,
            False,                      # Can't be toggled by user
            0, 4, grid_width, 1,
        )

        self.add_checkbutton(grid,
            _(
            'XDG module is available (saves the config file in the standard' \
            + ' location)',
            ),
            mainapp.HAVE_XDG_FLAG,
            False,                      # Can't be toggled by user
            0, 5, grid_width, 1,
        )

        self.add_checkbutton(grid,
            _(
            'Notify module is available (shows desktop notifications; Linux/' \
            + '*BSD only)',
            ),
            mainapp.HAVE_NOTIFY_FLAG,
            False,                      # Can't be toggled by user
            0, 6, grid_width, 1,
        )

        # Module preferences
        self.add_label(grid,
            '<u>' + _('Module preferences') + '</u>',
            0, 7, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _(
            'Use \'moviepy\' module to get a video\'s duration, if not known'
            + ' (may be slow)',
            ),
            self.app_obj.use_module_moviepy_flag,
            True,                   # Can be toggled by user
            0, 8, grid_width, 1,
        )
        checkbutton.connect('toggled', self.on_moviepy_button_toggled)
        if not mainapp.HAVE_MOVIEPY_FLAG:
            checkbutton.set_sensitive(False)

        self.add_label(grid,
            _('Timeout applied when moviepy checks a video file'),
            0, 9, 1, 1,
        )

        spinbutton = self.add_spinbutton(grid,
            0,
            60,
            1,                  # Step
            self.app_obj.refresh_moviepy_timeout,
            1, 9, 1, 1,
        )
        spinbutton.connect(
            'value-changed',
            self.on_moviepy_timeout_spinbutton_changed,
        )


    def setup_files_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Files' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Files > xxx'
        )

        # Add this tab...
        tab, grid = self.add_notebook_tab(_('_Files'), 0)

        # ...and an inner notebook...
        self.files_inner_notebook = self.add_inner_notebook(grid)

        # ...with its own tabs
        if not self.app_obj.simple_prefs_flag:
            self.setup_files_device_tab(self.files_inner_notebook)
            self.setup_files_config_tab(self.files_inner_notebook)
        self.setup_files_database_tab(self.files_inner_notebook)
        self.setup_files_backups_tab(self.files_inner_notebook)
        if not self.app_obj.simple_prefs_flag:
            self.setup_files_videos_tab(self.files_inner_notebook)
        self.setup_files_delete_tab(self.files_inner_notebook)
        self.setup_files_update_tab(self.files_inner_notebook)
        self.setup_files_urls_tab(self.files_inner_notebook)
        self.setup_files_temp_folders_tab(self.files_inner_notebook)
        if not self.app_obj.simple_prefs_flag:
            self.setup_files_statistics_tab(self.files_inner_notebook)
            if mainapp.HAVE_MATPLOTLIB_FLAG:
                self.setup_files_history_tab(self.files_inner_notebook)


    def setup_files_device_tab(self, inner_notebook):

        """Called by self.setup_files_tab().

        Sets up the 'Device' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Files > Device'
        )

        tab, grid = self.add_inner_notebook_tab(_('_Device'), inner_notebook)
        grid_width = 3

        # Device preferences
        self.add_label(grid,
            '<u>' + _('Device preferences') + '</u>',
            0, 0, grid_width, 1,
        )

        self.add_label(grid,
            _('Size of device'),
            0, 3, 1, 1,
        )

        self.entry = self.add_entry(grid,
            str(ttutils.disk_get_total_space(self.app_obj.data_dir)),
            False,
            1, 3, 1, 1,
        )
        self.entry.set_sensitive(False)

        self.add_label(grid,
            'GiB',
            2, 3, 1, 1,
        )

        self.add_label(grid,
            _('Free space on device'),
            0, 4, 1, 1,
        )

        self.entry2 = self.add_entry(grid,
            str(ttutils.disk_get_free_space(self.app_obj.data_dir)),
            False,
            1, 4, 1, 1,
        )
        self.entry2.set_sensitive(False)

        self.add_label(grid,
            'GiB',
            2, 4, 1, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _(
                'Before checking/downloading videos, warn user if disk space' \
                + ' is less than',
            ),
            self.app_obj.disk_space_warn_flag,
            True,                   # Can be toggled by user
            0, 5, grid_width, 1,
        )
        # (Signal connect appears below)

        spinbutton = self.add_spinbutton(grid,
            0, None,
            self.app_obj.disk_space_increment,
            self.app_obj.disk_space_warn_limit,
            1, 6, 1, 1,
        )
        if not self.app_obj.disk_space_warn_flag:
            spinbutton.set_sensitive(False)
        # (Signal connect appears below)

        self.add_label(grid,
            'GiB',
            2, 6, 1, 1,
        )

        checkbutton2 = self.add_checkbutton(grid,
            _('Halt downloads if disk space is less than'),
            self.app_obj.disk_space_stop_flag,
            True,                   # Can be toggled by user
            0, 7, 1, 1,
        )
        # (Signal connect appears below)

        spinbutton2 = self.add_spinbutton(grid,
            0, None,
            self.app_obj.disk_space_increment,
            self.app_obj.disk_space_stop_limit,
            1, 7, 1, 1,
        )
        if not self.app_obj.disk_space_stop_flag:
            spinbutton2.set_sensitive(False)
        # (Signal connect appears below)

        self.add_label(grid,
            'GiB',
            2, 7, 1, 1,
        )

        # (Signal connects from above)
        checkbutton.connect(
            'toggled',
            self.on_disk_warn_button_toggled,
            spinbutton,
        )
        spinbutton.connect(
            'value-changed',
            self.on_disk_warn_spinbutton_changed,
        )
        checkbutton2.connect(
            'toggled',
            self.on_disk_stop_button_toggled,
            spinbutton2,
        )
        spinbutton2.connect(
            'value-changed',
            self.on_disk_stop_spinbutton_changed,
        )


    def setup_files_config_tab(self, inner_notebook):

        """Called by self.setup_files_tab().

        Sets up the 'Config' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Files > Config'
        )

        tab, grid = self.add_inner_notebook_tab(_('_Config'), inner_notebook)

        # Configuration preferences
        self.add_label(grid,
            '<u>' + _('Configuration preferences') + '</u>',
            0, 0, 1, 1,
        )

        self.add_label(grid,
            _('Tartube configuration file loaded from'),
            0, 1, 1, 1,
        )

        entry = self.add_entry(grid,
            self.app_obj.get_config_path(),
            False,
            0, 2, 1, 1,
        )
        entry.set_sensitive(False)

        self.add_label(grid,
            _('Default location for Tartube configuration'),
            0, 3, 1, 1,
        )

        entry2 = self.add_entry(grid,
            self.app_obj.config_file_xdg_path,
            False,
            0, 4, 1, 1,
        )
        entry2.set_sensitive(False)

        self.add_label(grid,
            _('Alternative location for Tartube configuration'),
            0, 5, 1, 1,
        )

        entry3 = self.add_entry(grid,
            self.app_obj.config_file_path,
            False,
            0, 6, 1, 1,
        )
        entry3.set_sensitive(False)


    def setup_files_database_tab(self, inner_notebook):

        """Called by self.setup_files_tab().

        Sets up the 'Database' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Files > Database'
        )

        tab, grid = self.add_inner_notebook_tab(_('D_atabase'), inner_notebook)
        grid_width = 2

        # Database preferences
        self.add_label(grid,
            '<u>' + _('Database preferences') + '</u>',
            0, 0, grid_width, 1,
        )

        label = self.add_label(grid,
            _('Current data folder'),
            0, 1, 1, 1,
        )
        label.set_hexpand(False)

        entry = self.add_entry(grid,
            self.app_obj.data_dir,
            False,
            1, 1, 1, 1,
        )
        entry.set_sensitive(False)

        label2 = self.add_label(grid,
            _('Current database'),
            0, 2, 1, 1,
        )
        label2.set_hexpand(False)

        entry2 = self.add_entry(grid,
            os.path.abspath(
                os.path.join(
                    self.app_obj.data_dir, self.app_obj.db_file_name,
                ),
            ),
            False,
            1, 2, 1, 1,
        )
        entry2.set_sensitive(False)

        label3 = self.add_label(grid,
            _('Recent data folders'),
            0, 3, 1, 1,
        )
        label3.set_hexpand(False)

        treeview, liststore = self.add_treeview(grid,
            1, 3, 1, 1,
        )
        treeview.set_vexpand(False)
        for item in self.app_obj.data_dir_alt_list:
            liststore.append([item])

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 4, grid_width, 1)

        button = Gtk.Button(_('Add new database'))
        grid2.attach(button, 0, 0, 1, 1)
        button.set_tooltip_text(_('Change to a different data folder'))
        button.set_hexpand(True)
        button.connect(
            'clicked',
            self.on_data_dir_change_button_clicked,
        )

        button2 = Gtk.Button(_('Check and repair database'))
        grid2.attach(button2, 1, 0, 2, 1)
        button2.set_tooltip_text(
            _('Check for inconsistencies, and repair them'),
        )
        button2.set_hexpand(True)
        button2.connect('clicked', self.on_data_check_button_clicked)

        button3 = Gtk.Button(_('Dump database to JSON'))
        grid2.attach(button3, 3, 0, 2, 1)
        button3.set_tooltip_text(
            _('Convert databases to JSON, even when Tartube can\'t load them'),
        )
        button3.set_hexpand(True)
        button3.connect('clicked', self.on_dump_db_button_clicked, treeview)

        button4 = Gtk.Button(_('Switch to this database'))
        grid2.attach(button4, 0, 1, 1, 1)
        button4.set_tooltip_text(_('Switch to the selected data folder'))
        button4.set_hexpand(True)
        button4.set_sensitive(False)
        button4.connect(
            'clicked',
            self.on_data_dir_switch_button_clicked,
            button,
            treeview,
        )

        button5 = Gtk.Button(_('Forget'))
        grid2.attach(button5, 1, 1, 1, 1)
        button5.set_tooltip_text(
            _('Remove the selected data folder from the list'),
        )
        button5.set_hexpand(True)
        button5.set_sensitive(False)
        button5.connect(
            'clicked',
            self.on_data_dir_forget_button_clicked,
            treeview,
        )

        button6 = Gtk.Button(_('Forget all'))
        grid2.attach(button6, 2, 1, 1, 1)
        button6.set_tooltip_text(
            _('Forget every folder in this list (except the current one)'),
        )
        button6.set_hexpand(True)
        if len(self.app_obj.data_dir_alt_list) <= 1:
            button6.set_sensitive(False)
        button6.connect(
            'clicked',
            self.on_data_dir_forget_all_button_clicked,
            treeview,
        )

        button7 = Gtk.Button(_('Move up'))
        grid2.attach(button7, 3, 1, 1, 1)
        button7.set_tooltip_text(
            _('Move the selected folder up the list'),
        )
        button7.set_hexpand(True)
        button7.set_sensitive(False)
        # (Signal connect appears below)

        button8 = Gtk.Button(_('Move down'))
        grid2.attach(button8, 4, 1, 1, 1)
        button8.set_tooltip_text(
            _('Move the selected folder down the list'),
        )
        button8.set_hexpand(True)
        button8.set_sensitive(False)
        # (Signal connect appears below)

        # (Signal connects from above)
        button7.connect(
            'clicked',
            self.on_data_dir_move_up_button_clicked,
            treeview,
            liststore,
            button8,
        )
        button8.connect(
            'clicked',
            self.on_data_dir_move_down_button_clicked,
            treeview,
            liststore,
            button7,
        )

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid3 = self.add_secondary_grid(grid, 0, 5, grid_width, 1)

        checkbutton = self.add_checkbutton(grid3,
            _(
            'On startup, load the first database on the list (not the most' \
            + ' recently-use one)',
            ),
            self.app_obj.data_dir_use_first_flag,
            True,               # Can be toggled by user
            0, 0, 2, 1,
        )
        checkbutton.connect('toggled', self.on_use_first_button_toggled)

        checkbutton2 = self.add_checkbutton(grid3,
            _('If one database is in use, try to load others'),
            self.app_obj.data_dir_use_list_flag,
            True,               # Can be toggled by user
            0, 1, 1, 1,
        )
        checkbutton2.connect('toggled', self.on_use_list_button_toggled)

        checkbutton3 = self.add_checkbutton(grid3,
            _('New databases are added to this list'),
            self.app_obj.data_dir_add_from_list_flag,
            True,               # Can be toggled by user
            1, 1, 1, 1,
        )
        checkbutton3.connect('toggled', self.on_add_from_list_button_toggled)

        # Everything must be desensitised, if load/save is disabled
        if self.app_obj.disable_load_save_flag:
            button.set_sensitive(False)
            button2.set_sensitive(False)
            button3.set_sensitive(False)
            button4.set_sensitive(False)
            button5.set_sensitive(False)
            button6.set_sensitive(False)
            button7.set_sensitive(False)
            button8.set_sensitive(False)
            checkbutton.set_sensitive(False)
            checkbutton2.set_sensitive(False)
            checkbutton3.set_sensitive(False)

        # (More signal connects from above)
        treeview.connect(
            'cursor-changed',
            self.on_data_dir_cursor_changed,
            button4,    # Switch
            button5,    # Forget
            button6,    # Forget all
            button7,    # Move up
            button8,    # Move down
        )


    def setup_files_backups_tab(self, inner_notebook):

        """Called by self.setup_files_tab().

        Sets up the 'Backups' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Files > Backups'
        )

        tab, grid = self.add_inner_notebook_tab(_('_Backups'), inner_notebook)
        grid_width = 3

        # Backup preferences
        self.add_label(grid,
            '<u>' + _('Backup preferences') + '</u>',
            0, 0, grid_width, 1,
        )
        self.add_label(grid,
            '<i>' + _(
                'When saving the database, Tartube makes a backup copy of' \
                + ' its database file',
            ) + '</i>',
            0, 1, grid_width, 1,
        )

        radiobutton = self.add_radiobutton(grid,
            None,
            _('Delete the backup as soon as the database has been saved'),
            0, 2, grid_width, 1,
        )
        # (Signal connect appears below)

        radiobutton2 = self.add_radiobutton(grid,
            radiobutton,
            _('Keep the backup file, replacing any previous backup file'),
            0, 3, grid_width, 1,
        )
        if self.app_obj.db_backup_mode == 'single':
            radiobutton2.set_active(True)
        # (Signal connect appears below)

        radiobutton3 = self.add_radiobutton(grid,
            radiobutton2,
            _('Make a new backup file once per day'),
            0, 4, grid_width, 1,
        )
        if self.app_obj.db_backup_mode == 'daily':
            radiobutton3.set_active(True)
        # (Signal connect appears below)

        radiobutton4 = self.add_radiobutton(grid,
            radiobutton3,
            _('Make a new backup file every time the database is saved'),
            0, 5, grid_width, 1,
        )
        if self.app_obj.db_backup_mode == 'always':
            radiobutton4.set_active(True)
        # (Signal connect appears below)

        # (Signal connects from above)
        radiobutton.connect(
            'toggled',
            self.on_backup_button_toggled,
            'default',
        )
        radiobutton2.connect(
            'toggled',
            self.on_backup_button_toggled,
            'single',
        )
        radiobutton3.connect(
            'toggled',
            self.on_backup_button_toggled,
            'daily',
        )
        radiobutton4.connect(
            'toggled',
            self.on_backup_button_toggled,
            'always',
        )

        if not self.app_obj.simple_prefs_flag:

            # Export preferences
            self.add_label(grid,
                '<u>' + _('Export preferences') + '</u>',
                0, 6, grid_width, 1,
            )

            label = self.add_label(grid,
                _('Separator used in CSV exports'),
                0, 7, 1, 1,
            )
            label.set_hexpand(False)

            # (At the moment, Tartube only offers two choices of CSV separator)
            combo = self.add_combo(grid,
                ['|', ','],
                self.app_obj.export_csv_separator,
                1, 7, 1, 1,
            )
            combo.set_hexpand(False)
            combo.connect('changed', self.on_separator_combo_changed)

            # (Empty label for spacing)
            label = self.add_label(grid,
                '',
                2, 1, 1, 1,
            )
            label.set_hexpand(True)


    def setup_files_videos_tab(self, inner_notebook):

        """Called by self.setup_files_tab().

        Sets up the 'Videos' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Files > Videos'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Videos'),
            inner_notebook,
        )
        grid_width = 3

        # Video matching preferences
        self.add_label(grid,
            '<u>' + _('Video matching preferences') + '</u>',
            0, 0, grid_width, 1,
        )

        self.add_label(grid,
            _('When matching videos on the filesystem:'),
            0, 1, grid_width, 1,
        )

        self.radiobutton3 = self.add_radiobutton(grid,
            None,
            _('The video names must match exactly'),
            0, 2, 1, 1,
        )
        # (Signal connect appears below)

        self.radiobutton4 = self.add_radiobutton(grid,
            self.radiobutton3,
            _('The first # characters must match exactly'),
            0, 3, 1, 1,
        )
        # (Signal connect appears below)

        self.spinbutton3 = self.add_spinbutton(grid,
            1, 999, 1, self.app_obj.match_first_chars,
            1, 3, 2, 1,
        )
        # (Signal connect appears below)

        self.radiobutton5 = self.add_radiobutton(grid,
            self.radiobutton4,
            _(
            'Ignore the last # characters; the remaining name must match' \
            + ' exactly',
            ),
            0, 4, 1, 1,
        )
        # (Signal connect appears below)

        self.spinbutton4 = self.add_spinbutton(grid,
            1, 999, 1, self.app_obj.match_ignore_chars,
            1, 4, 2, 1,
        )
        # (Signal connect appears below)

        # (Widgets are sensitised/desensitised, based on the radiobutton)
        if self.app_obj.match_method == 'exact_match':
            self.spinbutton3.set_sensitive(False)
            self.spinbutton4.set_sensitive(False)
        elif self.app_obj.match_method == 'match_first':
            self.radiobutton4.set_active(True)
            self.spinbutton4.set_sensitive(False)
        else:
            self.radiobutton5.set_active(True)
            self.spinbutton3.set_sensitive(False)

        # (Signal connects from above)
        self.radiobutton3.connect('toggled', self.on_match_button_toggled)
        self.radiobutton4.connect('toggled', self.on_match_button_toggled)
        self.radiobutton5.connect('toggled', self.on_match_button_toggled)
        self.spinbutton3.connect(
            'value-changed',
            self.on_match_spinbutton_changed,
        )
        self.spinbutton4.connect(
            'value-changed',
            self.on_match_spinbutton_changed,
        )

        self.add_label(grid,
            '<u>' + _('Video matching recommended preferences') + '</u>',
            0, 5, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _(
                'Check the video\'s original name and the downloaded file' \
                + ' name',
            ),
            self.app_obj.match_nickname_flag,
            True,               # Can be toggled by user
            0, 6, grid_width, 1,
        )
        checkbutton.set_hexpand(False)
        checkbutton.connect('toggled', self.on_match_nickname_button_toggled)

        self.add_label(grid,
            '<i>' + _(
                'N.B. If disabled, custom file templates will interfere' \
                + ' with video matching'
            ) + '</i>',
            0, 7, grid_width, 1,
        )


    def setup_files_delete_tab(self, inner_notebook):

        """Called by self.setup_files_tab().

        Sets up the 'Delete' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Files > Delete'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('D_elete'),
            inner_notebook,
        )
        grid_width = 3

        # Automatic video deletion/removal preferences
        self.add_label(grid,
            '<u>' + _('Automatic video deletion/removal preferences') + '</u>',
            0, 0, grid_width, 1,
        )

        self.add_label(grid,
            '<i>' + _(
                'Deleted videos are re-downloaded without an archive file.' \
                + ' See the Operations > Archive tab',
            ) + '</i>',
            0, 1, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Automatically delete downloaded videos'),
            self.app_obj.auto_delete_flag,
            True,               # Can be toggled by user
            0, 2, 1, 1,
        )
        # (Signal connect appears below)

        self.spinbutton = self.add_spinbutton(grid,
            0, 999, 1, self.app_obj.auto_delete_days,
            1, 2, 1, 1,
        )
        if not self.app_obj.auto_delete_flag:
            self.spinbutton.set_sensitive(False)
        # (Signal connect appears below)

        self.add_label(grid,
            _('days'),
            2, 2, 1, 1,
        )

        checkbutton2 = self.add_checkbutton(grid,
            _(
                'Remove downloaded videos from the database (but don\'t' \
                + ' delete files)',
            ),
            self.app_obj.auto_remove_flag,
            True,               # Can be toggled by user
            0, 3, 1, 1,
        )
        # (Signal connect appears below)

        self.spinbutton2 = self.add_spinbutton(grid,
            0, 999, 1, self.app_obj.auto_remove_days,
            1, 3, 1, 1,
        )
        if not self.app_obj.auto_remove_flag:
            self.spinbutton2.set_sensitive(False)
        # (Signal connect appears below)

        self.add_label(grid,
            _('days'),
            2, 3, 1, 1,
        )

        checkbutton3 = self.add_checkbutton(grid,
            _('Only delete/remove videos which have been watched'),
            self.app_obj.auto_delete_watched_flag,
            True,               # Can be toggled by user
            0, 4, grid_width, 1,
        )
        # (Signal connect appears below)

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 5, grid_width, 1)

        self.add_label(grid2,
            _('Delete/remove files:'),
            0, 0, 1, 1,
        )

        self.radiobutton = self.add_radiobutton(grid2,
            None,
            _('When the database is loaded'),
            1, 0, 1, 1,
        )
        # (Signal connect appears below)

        self.radiobutton2 = self.add_radiobutton(grid2,
            self.radiobutton,
            _('After every download operation'),
            2, 0, 1, 1,
        )
        if self.app_obj.auto_delete_asap_flag:
            self.radiobutton2.set_active(True)

        if not self.app_obj.auto_delete_flag \
        and not self.app_obj.auto_remove_flag:
            checkbutton3.set_sensitive(False)
            self.radiobutton.set_sensitive(False)
            self.radiobutton2.set_sensitive(False)

        # (Signal connects from above)
        checkbutton.connect(
            'toggled',
            self.on_auto_delete_videos_button_toggled,
            self.spinbutton,
            checkbutton2,
            checkbutton3,
            self.radiobutton,
            self.radiobutton2,
        )
        self.spinbutton.connect(
            'value-changed',
            self.on_auto_delete_videos_spinbutton_changed,
        )
        checkbutton2.connect(
            'toggled',
            self.on_auto_remove_videos_button_toggled,
            self.spinbutton2,
            checkbutton,
            checkbutton3,
            self.radiobutton,
            self.radiobutton2,
        )
        self.spinbutton2.connect(
            'value-changed',
            self.on_auto_remove_videos_spinbutton_changed,
        )
        checkbutton3.connect('toggled', self.on_delete_watched_button_toggled)
        self.radiobutton.connect('toggled', self.on_delete_asap_button_toggled)

        # Manual video deletion/removal preferences
        self.add_label(grid,
            '<u>' + _('Manual video deletion/removal preferences') + '</u>',
            0, 6, grid_width, 1,
        )

        checkbutton4 = self.add_checkbutton(grid,
            _('Show dialogue window before removing video(s)'),
            self.app_obj.show_delete_video_dialogue_flag,
            True,               # Can be toggled by user
            0, 7, grid_width, 1,
        )
        checkbutton4.connect(
            'toggled',
            self.on_show_delete_video_button_toggled,
        )

        checkbutton5 = self.add_checkbutton(grid,
            _('When removing videos, remove all files from the filesystem'),
            self.app_obj.delete_video_files_flag,
            True,               # Can be toggled by user
            0, 8, grid_width, 1,
        )
        checkbutton5.connect(
            'toggled',
            self.on_remove_video_file_button_toggled,
        )

        checkbutton6 = self.add_checkbutton(grid,
            _(
                'Show dialogue window before removing channels/playlists' \
                + '/folders',
            ),
            self.app_obj.show_delete_container_dialogue_flag,
            True,               # Can be toggled by user
            0, 9, grid_width, 1,
        )
        checkbutton6.connect(
            'toggled',
            self.on_show_delete_container_button_toggled,
        )

        checkbutton7 = self.add_checkbutton(grid,
            _(
                'When removing containers, remove all files from the' \
                + ' filesystem',
            ),
            self.app_obj.delete_container_files_flag,
            True,               # Can be toggled by user
            0, 10, grid_width, 1,
        )
        checkbutton7.connect(
            'toggled',
            self.on_remove_container_file_button_toggled,
        )


    def setup_files_update_tab(self, inner_notebook):

        """Called by self.setup_files_tab().

        Sets up the 'Update' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Files > Update'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Update'),
            inner_notebook,
        )
        grid_width = 2

        # Update video descriptions
        self.add_label(grid,
            '<u>' + _('Update video descriptions') + '</u>',
            0, 0, grid_width, 1,
        )

        self.add_label(grid,
            '<i>' \
            + _(
                'These procedures might take a long time on a large database',
            ) \
            + '</i>',
            0, 1, grid_width, 1,
        )

        button = Gtk.Button.new_with_label(
            _('Update from description files, and set the line lengths to:'),
        )
        grid.attach(button, 0, 2, 1, 1)
        # (Signal connect appears below)

        min_value = self.app_obj.main_win_obj.medium_string_max_len
        max_value = self.app_obj.main_win_obj.descrip_line_max_len
        if max_value < min_value:
            max_value = min_value

        spinbutton = self.add_spinbutton(grid,
            min_value,
            max_value,
            1,
            self.app_obj.main_win_obj.descrip_line_max_len,
            1, 2, 1, 1,
        )

        button2 = Gtk.Button.new_with_label(
            _('Clear descriptions (does not modify the description files)'),
        )
        grid.attach(button2, 0, 3, grid_width, 1)
        # (Signal connect appears below)

        # (Signal connects from above)
        button.connect(
            'clicked',
            self.on_load_descrips_button_clicked,
            spinbutton,
        )

        button2.connect(
            'clicked',
            self.on_clear_descrips_button_clicked,
        )

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 4, grid_width, 1)

        # Video timestamps
        self.add_label(grid2,
            '<u>' + _('Video timestamps') + '</u>',
            0, 4, grid_width, 1,
        )

        button3 = Gtk.Button(_('Extract timestamps for all videos'))
        grid2.attach(button3, 0, 5, 1, 1)
        button3.set_hexpand(False)
        button3.connect(
            'clicked',
            self.on_extract_stamps_button_clicked,
        )

        button4 = Gtk.Button(_('Remove timestamps from all videos'))
        grid2.attach(button4, 1, 5, 1, 1)
        button4.set_hexpand(False)
        button4.connect(
            'clicked',
            self.on_remove_stamps_button_clicked,
        )

        # Video comments
        self.add_label(grid2,
            '<u>' + _('Video comments') + '</u>',
            0, 6, grid_width, 1,
        )

        button5 = Gtk.Button(_('Extract comments for all videos'))
        grid2.attach(button5, 0, 7, 1, 1)
        button5.set_hexpand(False)
        button5.connect(
            'clicked',
            self.on_extract_comments_button_clicked,
        )

        button6 = Gtk.Button(_('Remove comments from all videos'))
        grid2.attach(button6, 1, 7, 1, 1)
        button6.set_hexpand(False)
        button6.connect(
            'clicked',
            self.on_remove_comments_button_clicked,
        )

        self.add_label(grid,
            '<i>' + _(
                'Comments are extracted from each video\'s metadata file,' \
                + ' so this procedure may take a long time',
            ) + '</i>',
            0, 8, grid_width, 1,
        )


    def setup_files_urls_tab(self, inner_notebook):

        """Called by self.setup_files_tab().

        Sets up the 'URLs' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Files > URLs'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('U_RLs'),
            inner_notebook,
        )
        grid_width = 2

        # Update channel/playlist URLs
        self.add_label(grid,
            '<u>' + _('Update channel/playlist URLs') + '</u>',
            0, 0, (grid_width - 1), 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Confirm every change'),
            self.app_obj.url_change_confirm_flag,
            True,               # Can be toggled by user
            (grid_width - 1), 0, 1, 1,
        )
        checkbutton.set_hexpand(False)
        checkbutton.connect(
            'toggled',
            self.on_confirm_url_button_toggled,
        )

        # (GenericConfigWin.add_treeview() doesn't support multiple columns, so
        #   we'll do everything ourselves)
        frame = Gtk.Frame()
        grid.attach(frame, 0, 1, grid_width, 1)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)

        treeview = Gtk.TreeView()
        scrolled.add(treeview)
        treeview.set_headers_visible(True)
        # (Allow multiple selection)
        treeview.set_can_focus(True)
        selection = treeview.get_selection()
        selection.set_mode(Gtk.SelectionMode.MULTIPLE)

        for i, column_title in enumerate(
            [ 'hide', '', _('Name'), _('URL') ],
        ):
            if i == 1:
                renderer_pixbuf = Gtk.CellRendererPixbuf()
                column_pixbuf = Gtk.TreeViewColumn(
                    column_title,
                    renderer_pixbuf,
                    pixbuf=i,
                )
                treeview.append_column(column_pixbuf)
                column_pixbuf.set_resizable(False)
            else:
                renderer_text = Gtk.CellRendererText()
                column_text = Gtk.TreeViewColumn(
                    column_title,
                    renderer_text,
                    text=i,
                )
                treeview.append_column(column_text)
                column_text.set_resizable(True)
                if i == 0:
                    column_text.set_visible(False)
                elif i == 2:
                    renderer_text.set_property('editable', True)
                    renderer_text.connect(
                        'edited',
                        self.on_container_name_edited,
                        treeview,
                        checkbutton,
                    )
                elif i == 3:
                    renderer_text.set_property('editable', True)
                    renderer_text.connect(
                        'edited',
                        self.on_container_url_edited,
                        treeview,
                        checkbutton,
                    )

        self.url_liststore = Gtk.ListStore(
            int, GdkPixbuf.Pixbuf, str, str,
        )
        treeview.set_model(self.url_liststore)

        # Initialise the list
        self.setup_files_urls_tab_update_treeview()

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 2, grid_width, 1)

        # Strip of widgets beneath the list
        self.add_label(grid2,
            _('Pattern'),
            0, 0, 1, 1,
        )

        entry = self.add_entry(grid2,
            None,
            True,
            1, 0, 1, 1,
        )

        self.add_label(grid2,
            _('Substitution'),
            2, 0, 1, 1,
        )

        entry2 = self.add_entry(grid2,
            None,
            True,
            3, 0, 1, 1,
        )

        checkbutton2 = self.add_checkbutton(grid2,
            _('This pattern is a regex'),
            self.app_obj.url_change_regex_flag,
            True,               # Can be toggled by user
            0, 1, 2, 1,
        )
        checkbutton2.set_hexpand(False)
        checkbutton2.connect(
            'toggled',
            self.on_url_regex_button_toggled,
        )

        button = Gtk.Button(
            _('Search and replace text in the selected URLs'),
        )
        grid2.attach(button, 2, 1, 2, 1)
        button.set_hexpand(True)
        button.connect(
            'clicked',
            self.on_container_url_multiple_edited,
            entry,
            entry2,
            treeview,
        )

        button2 = Gtk.Button()
        grid2.attach(button2, 2, 2, 1, 1)
        button2.set_hexpand(True)
        button2.set_label(_('Open URLs'))
        button2.connect(
            'clicked',
            self.on_open_url_clicked,
            treeview,
        )

        button3 = Gtk.Button()
        grid2.attach(button3, 3, 2, 1, 1)
        button3.set_hexpand(True)
        button3.set_label(_('Refresh list'))
        button3.connect(
            'clicked',
            self.setup_files_urls_tab_update_treeview,
        )


    def setup_files_urls_tab_update_treeview(self, button=None):

        """ Called by self.setup_files_urls_tab().

        Fills or updates the treeview.

        Args:

            button (Gtk.Button): The widget clicked (if applicable)

        """

        self.url_liststore.clear()

        # Prepare a sorted list of channels/playlists to display in the
        #   treeview
        obj_list = []
        for media_data_obj in self.app_obj.container_reg_dict.values():

            if isinstance(media_data_obj, media.Channel) \
            or isinstance(media_data_obj, media.Playlist):
                obj_list.append(media_data_obj)

        obj_list.sort(key=lambda x: x.name.lower())

        # Add each channel/playlist to the treeview, one row at a time
        for media_data_obj in obj_list:
            self.setup_files_urls_tab_add_row(media_data_obj)


    def setup_files_urls_tab_add_row(self, media_data_obj):

        """Called by self.setup_scheduling_start_tab_update_treeview() and
        .on_scheduled_add_button_clicked().

        Adds a row to the treeview.

        Args:

            media_data_obj (media.Channel, media.Playlist): The media data
                object to display on this row

        """

        if isinstance(media_data_obj, media.Channel):
            pixbuf = self.app_obj.main_win_obj.pixbuf_dict['channel_small']
        else:
            pixbuf = self.app_obj.main_win_obj.pixbuf_dict['playlist_small']

        row_list = []
        row_list.append(media_data_obj.dbid)
        row_list.append(pixbuf)
        row_list.append(media_data_obj.name)
        row_list.append(media_data_obj.source)

        self.url_liststore.append(row_list)


    def setup_files_temp_folders_tab(self, inner_notebook):

        """Called by self.setup_files_tab().

        Sets up the 'Temporary folders' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Files > Temporary'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Temporary'),
            inner_notebook,
        )

        # Temporary folder preferences
        self.add_label(grid,
            '<u>' + _('Temporary folder preferences') + '</u>',
            0, 0, 1, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Empty temporary folders when Tartube shuts down'),
            self.app_obj.delete_on_shutdown_flag,
            True,               # Can be toggled by user
            0, 1, 1, 1,
        )
        # (Signal connect appears below)

        self.add_label(grid,
            '<i>' + _(
                '(N.B. Temporary folders are always emptied when Tartube' \
                + ' starts up)',
            ) + '</i>',
            0, 2, 1, 1,
        )

        checkbutton2 = self.add_checkbutton(grid,
            _(
            'Open temporary folders (on the desktop) when Tartube shuts down',
            ),
            self.app_obj.open_temp_on_desktop_flag,
            True,               # Can be toggled by user
            0, 3, 1, 1,
        )
        checkbutton2.connect('toggled', self.on_open_desktop_button_toggled)
        if self.app_obj.delete_on_shutdown_flag:
            checkbutton2.set_sensitive(False)

        # (Signal connects from above)
        checkbutton.connect(
            'toggled',
            self.on_delete_shutdown_button_toggled,
            checkbutton2,
        )


    def setup_files_statistics_tab(self, inner_notebook):

        """Called by self.setup_files_tab().

        Sets up the 'Statistics' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Files > Statistics'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Statistics'),
            inner_notebook,
        )
        grid_width = 4

        # Statistics
        self.add_label(grid,
            '<u>' + _('Statistics') + '</u>',
            0, 0, grid_width, 1,
        )

        self.add_label(grid,
            _('The Tartube database contains:'),
            0, 1, grid_width, 1,
        )

        self.add_label(grid,
            _('Videos'),
            0, 2, 1, 1,
        )

        entry = self.add_entry(grid,
            None,
            False,
            1, 2, 1, 1,
        )

        self.add_label(grid,
            _('Downloaded'),
            0, 3, 1, 1,
        )

        entry2 = self.add_entry(grid,
            None,
            False,
            1, 3, 1, 1,
        )

        self.add_label(grid,
            _('Other'),
            0, 4, 1, 1,
        )

        entry3 = self.add_entry(grid,
            None,
            False,
            1, 4, 1, 1,
        )

        self.add_label(grid,
            _('Channels'),
            2, 2, 1, 1,
        )

        entry4 = self.add_entry(grid,
            None,
            False,
            3, 2, 1, 1,
        )

        self.add_label(grid,
            _('Playlists'),
            2, 3, 1, 1,
        )

        entry5 = self.add_entry(grid,
            None,
            False,
            3, 3, 1, 1,
        )

        self.add_label(grid,
            _('Custom folders'),
            2, 4, 1, 1,
        )

        entry6 = self.add_entry(grid,
            None,
            False,
            3, 4, 1, 1,
        )

        # Initialise the entries. Commented out so that the preference window
        #   will still appear quickly for enormous databases
#        self.setup_files_statistics_tab_recalculate(
#            entry,
#            entry2,
#            entry3,
#            entry4,
#            entry5,
#            entry6,
#        )

        button = Gtk.Button()
        grid.attach(button, 3, 5, 1, 1)
        button.set_label(_('Calculate'))
        button.connect(
            'clicked',
            self.on_recalculate_stats_button_clicked,
            entry,
            entry2,
            entry3,
            entry4,
            entry5,
            entry6,
        )


    def setup_files_statistics_tab_recalculate(self, entry, entry2, entry3,
    entry4, entry5, entry6):

        """Called by self.setup_files_statistics_tab and
        .on_recalculate_stats_button_clicked().

        Args:

            entry, entry2, entry3, entry4, entry5, entry6 (Gtk.Entry): The
                entry boxes to update

        """

        video_count = 0
        dl_count = 0
        not_dl_count = 0
        channel_count = 0
        playlist_count = 0
        folder_count = 0

        # Get number of videos, channels, playlists and sub-folders, and also
        #   downloaded/not downloaded videos
        # Ignore fixed (system) folders
        for media_data_obj in self.app_obj.media_reg_dict.values():

            if isinstance(media_data_obj, media.Video):

                video_count += 1

                if media_data_obj.dl_flag:
                    dl_count += 1
                else:
                    not_dl_count += 1

            elif isinstance(media_data_obj, media.Channel):

                channel_count += 1

            elif isinstance(media_data_obj, media.Playlist):

                playlist_count += 1

            elif isinstance(media_data_obj, media.Folder) \
            and not media_data_obj.fixed_flag:

                folder_count += 1

        entry.set_text(str(video_count))
        entry2.set_text(str(dl_count))
        entry3.set_text(str(not_dl_count))
        entry4.set_text(str(channel_count))
        entry5.set_text(str(playlist_count))
        entry6.set_text(str(folder_count))


    def setup_files_history_tab(self, inner_notebook):

        """Called by self.setup_files_tab().

        Sets up the 'History' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Files > History'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_History'),
            inner_notebook,
        )
        grid_width = 6

        # Download history
        self.add_label(grid,
            '<u>' + _('Download history') + '</u>',
            0, 0, grid_width, 1,
        )

        # Add combos to customise the graph
        combo, combo2, combo3, combo4, combo5 = self.add_combos_for_graphs(
            grid,
            1,
        )

        # Add a button which, when clicked, draws the graph using the
        #   customisation options specified by the combos
        button = Gtk.Button()
        grid.attach(button, 5, 1, 1, 1)
        button.set_label(_('Draw'))
        # (Signal connect appears below)

        # Add a box, inside which we draw graphs
        hbox = Gtk.HBox()
        grid.attach(hbox, 0, 2, grid_width, 1)
        hbox.set_hexpand(True)
        hbox.set_vexpand(True)

        # (Signal connects from above)
        button.connect(
            'clicked', self.on_button_draw_graph_clicked,
            hbox,
            combo,
            combo2,
            combo3,
            combo4,
            combo5,
        )


    def setup_windows_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Window' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Windows'
        )

        # Add this tab...
        tab, grid = self.add_notebook_tab(_('_Windows'), 0)

        # ...and an inner notebook...
        inner_notebook = self.add_inner_notebook(grid)

        # ...with its own tabs
        self.setup_windows_main_window_tab(inner_notebook)
        self.setup_windows_videos_tab(inner_notebook)
        self.setup_windows_drag_tab(inner_notebook)
        self.setup_windows_system_tray_tab(inner_notebook)
        if not self.app_obj.simple_prefs_flag:
            self.setup_windows_dialogues_tab(inner_notebook)
            self.setup_windows_colours_tab(inner_notebook)
        self.setup_windows_errors_warnings_tab(inner_notebook)
        self.setup_windows_websites_tab(inner_notebook)


    def setup_windows_main_window_tab(self, inner_notebook):

        """Called by self.setup_windows_tab().

        Sets up the 'Main Window' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Windows > Main Window'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Main window'),
            inner_notebook,
        )
        grid_width = 3

        # Main window preferences
        self.add_label(grid,
            '<u>' + _('Main window preferences') + '</u>',
            0, 0, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Remember size of the main window'),
            self.app_obj.main_win_save_size_flag,
            True,                   # Can be toggled by user
            0, 1, 1, 1,
        )
        # (Signal connect appears below)

        checkbutton2 = self.add_checkbutton(grid,
            _('Remember slider positions'),
            self.app_obj.main_win_save_slider_flag,
            True,                   # Can be toggled by user
            1, 1, 1, 1,
        )
        checkbutton2.connect('toggled', self.on_remember_slider_button_toggled)
        if not self.app_obj.main_win_save_size_flag:
            checkbutton2.set_sensitive(False)

        button = Gtk.Button(_('Reset both'))
        grid.attach(button, 2, 1, 1, 1)
        button.set_hexpand(True)
        button.connect(
            'clicked',
            self.on_reset_size_clicked,
        )

        # (Signal connect from above)
        checkbutton.connect(
            'toggled',
            self.on_remember_size_button_toggled,
            checkbutton2,
        )

        checkbutton3 = self.add_checkbutton(grid,
            _('Don\'t show the main window toolbar'),
            self.app_obj.toolbar_hide_flag,
            True,                   # Can be toggled by user
            0, 2, 1, 1,
        )
        # (Signal connect appears below)

        if not self.app_obj.simple_prefs_flag:

            checkbutton4 = self.add_checkbutton(grid,
                _('Don\'t show labels in the main window toolbar'),
                self.app_obj.toolbar_squeeze_flag,
                True,                   # Can be toggled by user
                1, 2, 2, 1,
            )
            checkbutton4.connect('toggled', self.on_squeeze_button_toggled)
            if self.app_obj.toolbar_hide_flag:
                checkbutton4.set_sensitive(False)

            # (Signal connect from above)
            checkbutton3.connect(
                'toggled',
                self.on_hide_toolbar_button_toggled,
                checkbutton4,
            )

        else:
            checkbutton3.connect(
                'toggled',
                self.on_hide_toolbar_button_toggled,
                None,
            )

        checkbutton5 = self.add_checkbutton(grid,
            _(
            'Replace stock icons with custom icons (in case stock icons' \
            + ' are not visible)',
            ),
            self.app_obj.show_custom_icons_flag,
            True,                   # Can be toggled by user
            0, 3, grid_width, 1,
        )
        checkbutton5.connect('toggled', self.on_show_custom_icons_toggled)

        if not self.app_obj.simple_prefs_flag:

            checkbutton6 = self.add_checkbutton(grid,
                _('Show tooltips for videos, channels, playlists and folders'),
                self.app_obj.show_tooltips_flag,
                True,                   # Can be toggled by user
                0, 4, grid_width, 1,
            )
            # (Signal connect appears below)

            checkbutton7 = self.add_checkbutton(grid,
                _(
                    'Show errors/warnings in tooltips (but not in the Videos' \
                    + ' tab)',
                ),
                self.app_obj.show_tooltips_extra_flag,
                True,                   # Can be toggled by user
                0, 5, grid_width, 1,
            )
            checkbutton7.connect(
                'toggled',
                self.on_show_tooltips_extra_toggled,
            )
            if not self.app_obj.show_tooltips_flag:
                checkbutton7.set_sensitive(False)

            # (Signal connect from above)
            checkbutton6.connect(
                'toggled',
                self.on_show_tooltips_toggled,
                checkbutton7,
            )

        checkbutton8 = self.add_checkbutton(grid,
            _(
            'Disable the download buttons in the toolbar and the Videos tab',
            ),
            self.app_obj.disable_dl_all_flag,
            True,                   # Can be toggled by user
            0, 6, grid_width, 1,
        )
        checkbutton8.connect('toggled', self.on_disable_dl_all_toggled)

        checkbutton9 = self.add_checkbutton(grid,
            _(
            'In the Progress tab, hide finished downloads',
            ),
            self.app_obj.progress_list_hide_flag,
            True,                   # Can be toggled by user
            0, 7, 1, 1,
        )
        checkbutton9.connect('toggled', self.on_hide_button_toggled)

        checkbutton10 = self.add_checkbutton(grid,
            _('Show downloads in reverse order'),
            self.app_obj.results_list_reverse_flag,
            True,                   # Can be toggled by user
            1, 7, 2, 1,
        )
        checkbutton10.connect('toggled', self.on_reverse_button_toggled)

        checkbutton11 = self.add_checkbutton(grid,
            _(
                'In the Progress/Classic Mode tabs, remember the width of' \
                + ' (some) columns',
            ),
            self.app_obj.progress_list_remember_width_flag,
            True,                   # Can be toggled by user
            0, 8, grid_width, 1,
        )
        checkbutton11.connect('toggled', self.on_remember_width_button_toggled)

        checkbutton12 = self.add_checkbutton(grid,
            _('When Tartube starts, automatically open the Classic Mode tab'),
            self.app_obj.show_classic_tab_on_startup_flag,
            True,               # Can be toggled by user
            0, 9, grid_width, 1,
        )
        checkbutton12.connect(
            'toggled',
            self.on_show_classic_mode_button_toggled,
        )
        if __main__.__pkg_no_download_flag__:
            checkbutton12.set_sensitive(False)

        if not self.app_obj.simple_prefs_flag:

            checkbutton13 = self.add_checkbutton(grid,
                _(
                'In the Classic Mode tab, when adding URLs, remove' \
                + ' duplicates rather than retaining them',
                ),
                self.app_obj.classic_duplicate_remove_flag,
                True,                   # Can be toggled by user
                0, 10, grid_width, 1,
            )
            checkbutton13.connect(
                'toggled',
                self.on_remove_duplicate_button_toggled,
            )

            checkbutton14 = self.add_checkbutton(grid,
                _(
                'In the Errors/Warnings tab, don\'t reset the tab title when' \
                + ' it is clicked',
                ),
                self.app_obj.system_msg_keep_totals_flag,
                True,                   # Can be toggled by user
                0, 11, grid_width, 1,
            )
            checkbutton14.connect(
                'toggled',
                self.on_system_keep_button_toggled,
            )


    def setup_windows_videos_tab(self, inner_notebook):

        """Called by self.setup_windows_tab().

        Sets up the 'Tabs' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Windows > Videos'
        )

        tab, grid = self.add_inner_notebook_tab(_('_Videos'), inner_notebook)
        grid_width = 2

        # Video Index (left side of the Videos tab)
        self.add_label(grid,
            '<u>' + _('Video Index (left side of the Videos tab)') + '</u>',
            0, 0, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Show a \'Custom download all\' button'),
            self.app_obj.show_custom_dl_button_flag,
            True,                   # Can be toggled by user
            0, 1, grid_width, 1,
        )
        checkbutton.connect('toggled', self.on_show_custom_dl_button_toggled)

        if not self.app_obj.simple_prefs_flag:

            checkbutton2 = self.add_checkbutton(grid,
                _('While checking/downloading videos, show free disk space'),
                self.app_obj.show_free_space_flag,
                True,                   # Can be toggled by user
                0, 2, grid_width, 1,
            )
            checkbutton2.connect(
                'toggled',
                self.on_show_free_space_button_toggled,
            )

            checkbutton3 = self.add_checkbutton(grid,
                _('Allow each row to be marked for checking/downloading'),
                self.app_obj.show_marker_in_index_flag,
                True,                   # Can be toggled by user
                0, 3, grid_width, 1,
            )
            checkbutton3.connect(
                'toggled',
                self.on_show_selector_button_toggled,
            )

            checkbutton4 = self.add_checkbutton(grid,
                _('Show smaller icons'),
                self.app_obj.show_small_icons_in_index_flag,
                True,                   # Can be toggled by user
                0, 4, grid_width, 1,
            )
            checkbutton4.connect('toggled', self.on_show_small_icons_toggled)

            checkbutton5 = self.add_checkbutton(grid,
                _(
                'Show detailed statistics about the videos in each channel' \
                + ' / playlist / folder',
                ),
                self.app_obj.complex_index_flag,
                True,               # Can be toggled by user
                0, 5, grid_width, 1,
            )
            checkbutton5.connect('toggled', self.on_complex_button_toggled)

        checkbutton6 = self.add_checkbutton(grid,
            _(
            'After clicking on a folder, automatically expand/collapse the' \
            + ' tree around it',
            ),
            self.app_obj.auto_expand_video_index_flag,
            True,                   # Can be toggled by user
            0, 6, grid_width, 1,
        )
        # (Signal connect appears below)

        checkbutton7 = self.add_checkbutton(grid,
            _(
            'Expand the whole tree, not just the level beneath the clicked' \
            + ' folder',
            ),
            self.app_obj.full_expand_video_index_flag,
            True,                   # Can be toggled by user
            0, 7, grid_width, 1,
        )
        if not self.app_obj.auto_expand_video_index_flag:
            checkbutton7.set_sensitive(False)
        # (Signal connect appears below)

        # (Signal connects from above)
        checkbutton6.connect(
            'toggled',
            self.on_expand_tree_toggled,
            checkbutton7,
        )
        checkbutton7.connect('toggled', self.on_expand_full_tree_toggled)

        # Video Catalogue (right side of the Videos tab)
        self.add_label(grid,
            '<u>' + _('Video Catalogue (right side of the Videos tab)') \
            + '</u>',
            0, 8, grid_width, 1,
        )

        checkbutton8 = self.add_checkbutton(grid,
            _('Show \'today\' and \'yesterday\' as the date, when possible'),
            self.app_obj.show_pretty_dates_flag,
            True,                   # Can be toggled by user
            0, 9, grid_width, 1,
        )
        checkbutton8.connect('toggled', self.on_pretty_date_button_toggled)

        checkbutton9 = self.add_checkbutton(grid,
            _('Show livestreams with a different background colour'),
            self.app_obj.livestream_use_colour_flag,
            True,                   # Can be toggled by user
            0, 10, grid_width, 1,
        )
        # (Signal connect appears below)

        checkbutton10 = self.add_checkbutton(grid,
            _('Use same background colours for livestream and debut videos'),
            self.app_obj.livestream_simple_colour_flag,
            True,                   # Can be toggled by user
            0, 11, grid_width, 1,
        )
        if not self.app_obj.livestream_use_colour_flag:
            checkbutton10.set_sensitive(False)
        # (Signal connect appears below)

        # (Signal connects from above)
        checkbutton9.connect(
            'toggled',
            self.on_livestream_colour_button_toggled,
            checkbutton10,
        )
        checkbutton10.connect(
            'toggled',
            self.on_livestream_simple_button_toggled,
        )

        if not self.app_obj.simple_prefs_flag:

            checkbutton11 = self.add_checkbutton(grid,
                _('Channel and playlist names are clickable (grid mode only)'),
                self.app_obj.catalogue_clickable_container_flag,
                True,                   # Can be toggled by user
                0, 12, grid_width, 1,
            )
            checkbutton11.connect('toggled', self.on_clickable_button_toggled)

        checkbutton12 = self.add_checkbutton(grid,
            _('Show nicknames (not video file names)'),
            self.app_obj.catalogue_show_nickname_flag,
            True,                   # Can be toggled by user
            0, 13, grid_width, 1,
        )
        checkbutton12.connect('toggled', self.on_nickname_button_toggled)


    def setup_windows_drag_tab(self, inner_notebook):

        """Called by self.setup_windows_tab().

        Sets up the 'Drag' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Windows > Drag'
        )

        tab, grid = self.add_inner_notebook_tab(_('_Drag'), inner_notebook)

        # Drag and drop preferences
        self.add_label(grid,
            '<u>' + _('Drag and drop preferences') + '</u>',
            0, 0, 1, 1,
        )

        self.add_label(grid,
            '<i>' + _(
            'When dragging and dropping videos to an external application...',
            ) + '</i>',
            0, 1, 2, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Transfer the video\'s full file path'),
            self.app_obj.drag_video_path_flag,
            True,                   # Can be toggled by user
            0, 2, 1, 1,
        )
        checkbutton.connect('toggled', self.on_drag_path_button_toggled)

        checkbutton2 = self.add_checkbutton(grid,
            _('Transfer the video\'s source URL'),
            self.app_obj.drag_video_source_flag,
            True,                   # Can be toggled by user
            0, 3, 1, 1,
        )
        checkbutton2.connect('toggled', self.on_drag_source_button_toggled)

        checkbutton3 = self.add_checkbutton(grid,
            _('Transfer the video\'s name'),
            self.app_obj.drag_video_name_flag,
            True,                   # Can be toggled by user
            0, 4, 1, 1,
        )
        checkbutton3.connect('toggled', self.on_drag_name_button_toggled)

        checkbutton4 = self.add_checkbutton(grid,
            _('Transfer error/warning messages'),
            self.app_obj.drag_video_msg_flag,
            True,                   # Can be toggled by user
            1, 2, 1, 1,
        )
        checkbutton4.connect('toggled', self.on_drag_msg_button_toggled)

        checkbutton5 = self.add_checkbutton(grid,
            _('Transfer the thumbnail\'s full file path'),
            self.app_obj.drag_thumb_path_flag,
            True,                   # Can be toggled by user
            1, 3, 1, 1,
        )
        checkbutton5.connect('toggled', self.on_drag_thumb_button_toggled)

        checkbutton6 = self.add_checkbutton(grid,
            _('Add a text separator before each item'),
            self.app_obj.drag_video_separator_flag,
            True,                   # Can be toggled by user
            1, 4, 1, 1,
        )
        checkbutton6.connect('toggled', self.on_drag_separator_button_toggled)

        self.add_label(grid,
            '<i>' + _(
            'When dragging and dropping messages from the Errors/Warnings' \
            + ' tab to an external application...',
            ) + '</i>',
            0, 5, 2, 1,
        )

        checkbutton7 = self.add_checkbutton(grid,
            _('Transfer the video/channel/playlist file path'),
            self.app_obj.drag_error_path_flag,
            True,                   # Can be toggled by user
            0, 6, 1, 1,
        )
        checkbutton7.connect('toggled', self.on_drag_error_path_button_toggled)

        checkbutton8 = self.add_checkbutton(grid,
            _('Transfer the video/channel/playlist URL'),
            self.app_obj.drag_error_source_flag,
            True,                   # Can be toggled by user
            0, 7, 1, 1,
        )
        checkbutton8.connect(
            'toggled',
            self.on_drag_error_source_button_toggled,
        )

        checkbutton9 = self.add_checkbutton(grid,
            _('Transfer the video/channel/playlist name'),
            self.app_obj.drag_error_name_flag,
            True,                   # Can be toggled by user
            0, 8, 1, 1,
        )
        checkbutton9.connect(
            'toggled',
            self.on_drag_error_name_button_toggled,
        )

        checkbutton10 = self.add_checkbutton(grid,
            _('Transfer error/warning messages'),
            self.app_obj.drag_error_msg_flag,
            True,                   # Can be toggled by user
            1, 6, 1, 1,
        )
        checkbutton10.connect(
            'toggled',
            self.on_drag_error_msg_button_toggled,
        )

        checkbutton11 = self.add_checkbutton(grid,
            _('Add a text separator before each item'),
            self.app_obj.drag_error_separator_flag,
            True,                   # Can be toggled by user
            1, 7, 1, 1,
        )
        checkbutton11.connect(
            'toggled',
            self.on_drag_error_separator_button_toggled,
        )


    def setup_windows_system_tray_tab(self, inner_notebook):

        """Called by self.setup_windows_tab().

        Sets up the 'System tray' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Windows > Tray'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Tray'),
            inner_notebook,
        )

        # System tray preferences
        self.add_label(grid,
            '<u>' + _('System tray preferences') + '</u>',
            0, 0, 1, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Show Tartube in the system tray'),
            self.app_obj.show_status_icon_flag,
            True,               # Can be toggled by user
            0, 1, 1, 1,
        )
        checkbutton.set_hexpand(False)
        # (Signal connect appears below)

        checkbutton2 = self.add_checkbutton(grid,
            _('Start Tartube in the system tray'),
            self.app_obj.open_in_tray_flag,
            True,               # Can be toggled by user
            0, 2, 1, 1,
        )
        checkbutton2.set_hexpand(False)
        # (Signal connect appears below)
        if not self.app_obj.show_status_icon_flag:
            checkbutton2.set_sensitive(False)

        checkbutton3 = self.add_checkbutton(grid,
            _('Close to the tray, rather than closing the application'),
            self.app_obj.close_to_tray_flag,
            True,               # Can be toggled by user
            0, 3, 1, 1,
        )
        checkbutton3.set_hexpand(False)
        # (Signal connect appears below)
        if not self.app_obj.show_status_icon_flag:
            checkbutton3.set_sensitive(False)

        checkbutton4 = self.add_checkbutton(grid,
            _(
            'After closing to the tray, restore the window\'s position' \
            + ' (does not work on Wayland)',
            ),
            self.app_obj.restore_posn_from_tray_flag,
            True,               # Can be toggled by user
            0, 4, 1, 1,
        )
        checkbutton4.set_hexpand(False)
        # (Signal connect appears below)
        if not self.app_obj.show_status_icon_flag \
        or not self.app_obj.close_to_tray_flag:
            checkbutton4.set_sensitive(False)

        # (Signal connects from above)
        checkbutton.connect(
            'toggled',
            self.on_show_status_icon_toggled,
            checkbutton2,
            checkbutton3,
            checkbutton4,
        )
        checkbutton2.connect('toggled', self.on_open_in_tray_toggled)
        checkbutton3.connect(
            'toggled',
            self.on_close_to_tray_toggled,
            checkbutton4,
        )
        checkbutton4.connect('toggled', self.on_restore_from_tray_toggled)


    def setup_windows_dialogues_tab(self, inner_notebook):

        """Called by self.setup_windows_tab().

        Sets up the 'Dialogues' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Windows > Dialogues'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('D_ialogues'),
            inner_notebook,
        )

        # Add media preferences
        self.add_label(grid,
            '<u>' + _('Add media preferences') + '</u>',
            0, 0, 1, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('When adding channels/playlists, keep the dialogue window open'),
            self.app_obj.dialogue_keep_open_flag,
            True,               # Can be toggled by user
            0, 1, 1, 1,
        )
        checkbutton.set_hexpand(False)
        # (Signal connect appears below)

        checkbutton2 = self.add_checkbutton(grid,
            _(
            'When the dialogue window opens, add URLs from the system' \
            + ' clipboard',
            ),
            self.app_obj.dialogue_copy_clipboard_flag,
            True,               # Can be toggled by user
            0, 2, 1, 1,
        )
        checkbutton2.set_hexpand(False)
        checkbutton2.connect('toggled', self.on_clipboard_button_toggled)
        if self.app_obj.dialogue_keep_open_flag:
            checkbutton2.set_sensitive(False)

        # (Signal connect from above)
        checkbutton.connect(
            'toggled',
            self.on_keep_open_button_toggled,
            checkbutton2,
        )

        checkbutton3 = self.add_checkbutton(grid,
            _(
            'When adding YouTube channels, remind the user to copy the' \
            + ' correct URL',
            ),
            self.app_obj.dialogue_yt_remind_flag,
            True,               # Can be toggled by user
            0, 3, 1, 1,
        )
        checkbutton3.set_hexpand(False)
        checkbutton3.connect('toggled', self.on_yt_remind_button_toggled)

        # Move media preferences
        self.add_label(grid,
            '<u>' + _('Move media preferences') + '</u>',
            0, 4, 1, 1,
        )

        checkbutton4 = self.add_checkbutton(grid,
            _(
            'Prompt the user before moving channels/playlists/folders in' \
            + ' the Video Index',
            ),
            self.app_obj.dialogue_move_container_flag,
            True,               # Can be toggled by user
            0, 5, 1, 1,
        )
        checkbutton4.set_hexpand(False)
        checkbutton4.connect('toggled', self.on_move_container_button_toggled)

        checkbutton5 = self.add_checkbutton(grid,
            _('Prompt the user before moving videos in the Video Index'),
            self.app_obj.dialogue_move_video_flag,
            True,               # Can be toggled by user
            0, 6, 1, 1,
        )
        checkbutton5.set_hexpand(False)
        checkbutton5.connect('toggled', self.on_move_video_button_toggled)

        checkbutton6 = self.add_checkbutton(grid,
            _(
                'After moving, switch to the destination in the Video' \
                + ' Catalogue',
            ),
            self.app_obj.dialogue_move_select_flag,
            True,               # Can be toggled by user
            0, 7, 1, 1,
        )
        checkbutton6.set_hexpand(False)
        checkbutton6.connect('toggled', self.on_move_select_button_toggled)

        # Debugging preferences
        self.add_label(grid,
            '<u>' + _('Debugging preferences') + '</u>',
            0, 8, 1, 1,
        )

        checkbutton7 = self.add_checkbutton(grid,
            _(
            'Temporarily disable message dialogue windows (display messages' \
            + ' in terminal instead)',
            ),
            self.app_obj.dialogue_disable_msg_flag,
            True,               # Can be toggled by user
            0, 9, 1, 1,
        )
        checkbutton7.set_hexpand(False)
        checkbutton7.connect('toggled', self.on_dialogue_disable_toggled)

        self.add_label(grid,
            '<i>' + _(
            'N.B. Tartube shows a dialogue window after checking or' \
            + ' downloading videos',
            ) + '</i>',
            0, 10, 1, 1,
        )

        self.add_label(grid,
            '<i>' + _(
            'That dialogue window can be disabled in the Operations >' \
            + ' Actions tab',
            ) + '</i>',
            0, 11, 1, 1,
        )


    def setup_windows_colours_tab(self, inner_notebook):

        """Called by self.setup_windows_tab().

        Sets up the 'Colours' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Windows > Colours'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Colours'),
            inner_notebook,
        )
        grid_width = 5

        # Video catalogue colour preferences
        self.add_label(grid,
            '<u>' + _('Video catalogue colour preferences') + '</u>',
            0, 0, grid_width, 1,
        )

        self.setup_windows_colours_tab_add_row(grid,
            1,
            # Key in mainapp.TartubeApp.custom_bg_table
            'live_wait',
            _('Waiting livestreams'),
        )

        self.setup_windows_colours_tab_add_row(grid,
            2,
            'live_now',
            _('Broadcasting livestreams'),
        )

        self.setup_windows_colours_tab_add_row(grid,
            3,
            'debut_wait',
            _('Waiting debut videos'),
        )

        self.setup_windows_colours_tab_add_row(grid,
            4,
            'debut_now',
            _('Broadcasting debut videos'),
        )

        self.setup_windows_colours_tab_add_row(grid,
            5,
            'select',
            _('Selected videos'),
        )

        self.setup_windows_colours_tab_add_row(grid,
            6,
            'select_wait',
            _('Selected waiting videos'),
        )

        self.setup_windows_colours_tab_add_row(grid,
            7,
            'select_live',
            _('Selected broadcasting videos'),
        )

        self.setup_windows_colours_tab_add_row(grid,
            8,
            'drag_drop_notify',
            _('Drag and Drop notification'),
        )

        self.setup_windows_colours_tab_add_row(grid,
            9,
            'drag_drop_odd',
            _('Drag and Drop background 1'),
        )

        self.setup_windows_colours_tab_add_row(grid,
            10,
            'drag_drop_even',
            _('Drag and Drop background 2'),
        )


    def setup_windows_colours_tab_add_row(self, grid, row_num, key, descrip):

        """Called by self.setup_windows_colours_tab_add_row().

        Sets up a single row of widgets corresponding to a single key in
        mainapp.TartubeApp.custom_bg_table.

        Args:

            grid (Gtk.Grid): The grid on which widgets are attached

            row_num (int): Coordinates on the grid on which these widgets are
                place

            key (str): A key in mainapp.TartubeApp.custom_bg_table

            descrip (str): The label to use for this row

        """

        label = self.add_label(grid,
            descrip,
            0, row_num, 1, 1,
        )
        label.set_hexpand(False)

        label2 = self.add_label(grid,
            '<i>' + _('Custom colour:') + '</i>',
            1, row_num, 1, 1,
        )
        label2.set_hexpand(False)

        colorbutton = Gtk.ColorButton.new()
        grid.attach(colorbutton, 2, row_num, 1, 1)
        colorbutton.connect(
            'color-set',
            self.on_custom_colour_button_clicked,
            key,
        )

        mini_list = self.app_obj.custom_bg_table[key]
        custom_rgba_obj = Gdk.RGBA(
            mini_list[0],
            mini_list[1],
            mini_list[2],
            mini_list[3],
        )
        colorbutton.set_rgba(custom_rgba_obj)

        label3 = self.add_label(grid,
            '<i>' + _('Default colour:') + '</i>',
            3, row_num, 1, 1,
        )
        label3.set_hexpand(False)

        colorbutton2 = Gtk.ColorButton.new()
        grid.attach(colorbutton2, 4, row_num, 1, 1)
        colorbutton2.connect(
            'button-press-event',
            self.on_default_colour_button_clicked,
            colorbutton,
            key,
        )

        mini_list2 = self.app_obj.default_bg_table[key]
        default_rgba_obj = Gdk.RGBA(
            mini_list2[0],
            mini_list2[1],
            mini_list2[2],
            mini_list[3],
        )
        colorbutton2.set_rgba(default_rgba_obj)


    def setup_windows_errors_warnings_tab(self, inner_notebook):

        """Called by self.setup_windows_tab().

        Sets up the 'Errors/Warnings' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Windows >' \
            + ' Errors/Warnings'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Errors/Warnings'),
            inner_notebook,
        )

        # Errors/Warnings tab preferences
        self.add_label(grid,
            '<u>' + _('Errors/Warnings tab preferences') + '</u>',
            0, 0, 1, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Show Tartube errors'),
            self.app_obj.system_error_show_flag,
            True,                   # Can be toggled by user
            0, 1, 1, 1,
        )
        checkbutton.connect('toggled', self.on_system_error_button_toggled)

        checkbutton2 = self.add_checkbutton(grid,
            _('Show Tartube warnings'),
            self.app_obj.system_warning_show_flag,
            True,                   # Can be toggled by user
            0, 2, 1, 1,
        )
        checkbutton2.connect('toggled', self.on_system_warning_button_toggled)

        checkbutton3 = self.add_checkbutton(grid,
            _('Show operation errors'),
            self.app_obj.operation_error_show_flag,
            True,                   # Can be toggled by user
            0, 3, 1, 1,
        )
        checkbutton3.connect(
            'toggled',
            self.on_operation_error_button_toggled,
        )

        checkbutton4 = self.add_checkbutton(grid,
            _('Show operation warnings'),
            self.app_obj.operation_warning_show_flag,
            True,                   # Can be toggled by user
            0, 4, 1, 1,
        )
        checkbutton4.connect(
            'toggled',
            self.on_operation_warning_button_toggled,
        )

        checkbutton5 = self.add_checkbutton(grid,
            _('Show dates'),
            self.app_obj.system_msg_show_date_flag,
            True,                   # Can be toggled by user
            0, 5, 1, 1,
        )
        checkbutton5.connect(
            'toggled',
            self.on_system_date_button_toggled,
        )

        checkbutton6 = self.add_checkbutton(grid,
            _('Show channel/playlist/folder names'),
            self.app_obj.system_msg_show_container_flag,
            True,                   # Can be toggled by user
            0, 6, 1, 1,
        )
        checkbutton6.connect(
            'toggled',
            self.on_system_container_button_toggled,
        )

        checkbutton7 = self.add_checkbutton(grid,
            _('Show video names'),
            self.app_obj.system_msg_show_video_flag,
            True,                   # Can be toggled by user
            0, 7, 1, 1,
        )
        checkbutton7.connect(
            'toggled',
            self.on_system_video_button_toggled,
        )

        checkbutton8 = self.add_checkbutton(grid,
            _('Show full messages'),
            self.app_obj.system_msg_show_multi_line_flag,
            True,                   # Can be toggled by user
            0, 8, 1, 1,
        )
        checkbutton8.connect(
            'toggled',
            self.on_system_multi_line_button_toggled,
        )


    def setup_windows_websites_tab(self, inner_notebook):

        """Called by self.setup_windows_tab().

        Sets up the 'Websites' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Windows > Websites'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Websites'),
            inner_notebook,
        )
        grid_width = 2

        # YouTube error/warning preferences
        self.add_label(grid,
            '<u>' + _('YouTube error/warning preferences') + '</u>',
            0, 0, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Ignore YouTube copyright errors'),
            self.app_obj.ignore_yt_copyright_flag,
            True,                   # Can be toggled by user
            0, 1, 1, 1,
        )
        checkbutton.connect('toggled', self.on_copyright_button_toggled)

        checkbutton2 = self.add_checkbutton(grid,
            _('Ignore YouTube age-restriction errors'),
            self.app_obj.ignore_yt_age_restrict_flag,
            True,                   # Can be toggled by user
            0, 2, 1, 1,
        )
        checkbutton2.connect('toggled', self.on_age_restrict_button_toggled)

        checkbutton3 = self.add_checkbutton(grid,
            _('Ignore YouTube deletion by uploader errors'),
            self.app_obj.ignore_yt_uploader_deleted_flag,
            True,                   # Can be toggled by user
            1, 1, 1, 1,
        )
        checkbutton3.connect('toggled', self.on_uploader_button_toggled)

        checkbutton4 = self.add_checkbutton(grid,
            _('Ignore YouTube payment errors'),
            self.app_obj.ignore_yt_payment_flag,
            True,                   # Can be toggled by user
            1, 2, 1, 1,
        )
        checkbutton4.connect('toggled', self.on_payment_button_toggled)

        # General preferences
        self.add_label(grid,
            '<u>' + _('General preferences') + '</u>',
            0, 4, grid_width, 1,
        )

        self.add_label(grid,
            '<i>' + _(
                'Ignore any errors/warnings which match lines in this list' \
                + ' (applies to all websites)',
            ) + '</i>',
            0, 5, grid_width, 1,
        )

        textview, textbuffer = self.add_textview(grid,
            self.app_obj.ignore_custom_msg_list,
            0, 6, grid_width, 1
        )
        # (Signal connect appears below)

        radiobutton = self.add_radiobutton(grid,
            None,
            _('These are ordinary strings'),
            0, 7, 1, 1,
        )
        # (Signal connect appears below)

        radiobutton2 = self.add_radiobutton(grid,
            radiobutton,
            _('These are regular expressions (regexes)'),
            1, 7, 1, 1,
        )
        if self.app_obj.ignore_custom_regex_flag:
            radiobutton2.set_active(True)
        # (Signal connect appears below)

        # (Signal connects from above)
        textbuffer.connect('changed', self.on_custom_textview_changed)
        radiobutton.connect(
            'toggled',
            self.on_regex_button_toggled,
            False,
        )
        radiobutton2.connect(
            'toggled',
            self.on_regex_button_toggled,
            True,
        )


    def setup_scheduling_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Scheduling' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Scheduling'
        )

        # Add this tab...
        tab, grid = self.add_notebook_tab(_('_Scheduling'), 0)

        # ...and an inner notebook...
        inner_notebook = self.add_inner_notebook(grid)

        # ...with its own tabs
        self.setup_scheduling_start_tab(inner_notebook)
        self.setup_scheduling_stop_tab(inner_notebook)


    def setup_scheduling_start_tab(self, inner_notebook):

        """Called by self.setup_scheduling_tab().

        Sets up the 'Start' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Scheduling > Start'
        )

        tab, grid = self.add_inner_notebook_tab(_('_Start'), inner_notebook)
        grid_width = 5

        # Scheduled download preferences
        self.add_label(grid,
            '<u>' + _('Scheduled download preferences') + '</u>',
            0, 0, grid_width, 1,
        )

        # (GenericConfigWin.add_treeview() doesn't support multiple columns, so
        #   we'll do everything ourselves)
        frame = Gtk.Frame()
        grid.attach(frame, 0, 1, grid_width, 1)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)

        treeview = Gtk.TreeView()
        scrolled.add(treeview)
        treeview.set_headers_visible(True)

        for i, column_title in enumerate(
            [
                _('Name'), _('Download'), _('Start mode'), _('Time'),
                _('Priority'), _('Whole'), _('Shutdown'), _('D/L All'),
                _('Join mode'),
            ]
        ):
            if i >= 4 and i <= 7:
                renderer_toggle = Gtk.CellRendererToggle()
                column_toggle = Gtk.TreeViewColumn(
                    column_title,
                    renderer_toggle,
                    active=i,
                )
                treeview.append_column(column_toggle)
                column_toggle.set_resizable(False)
            else:
                renderer_text = Gtk.CellRendererText()
                column_text = Gtk.TreeViewColumn(
                    column_title,
                    renderer_text,
                    text=i,
                )
                treeview.append_column(column_text)
                column_text.set_resizable(True)

        self.schedule_liststore = Gtk.ListStore(
            str, str, str, str, bool, bool, bool, bool, str,
        )
        treeview.set_model(self.schedule_liststore)

        # Initialise the list
        self.setup_scheduling_start_tab_update_treeview()

        # Add editing widgets
        label = self.add_label(grid,
            _('Scheduled download name'),
            0, 2, 1, 1,
        )
        label.set_hexpand(False)

        entry = self.add_entry(grid,
            None,
            True,
            1, 2, (grid_width - 2), 1,
        )

        button = Gtk.Button()
        grid.attach(button, (grid_width - 1), 2, 1, 1)
        button.set_label(_('Add'))
        button.connect(
            'clicked',
            self.on_scheduled_add_button_clicked,
            entry,
        )

        button2 = Gtk.Button()
        grid.attach(button2, 1, 3, 1, 1)
        button2.set_label(_('Edit'))
        button2.connect(
            'clicked',
            self.on_scheduled_edit_button_clicked,
            treeview,
        )

        button3 = Gtk.Button()
        grid.attach(button3, 2, 3, 1, 1)
        button3.set_label(_('Move up'))
        button3.connect(
            'clicked',
            self.on_scheduled_move_up_button_clicked,
            treeview,
        )

        button4 = Gtk.Button()
        grid.attach(button4, 3, 3, 1, 1)
        button4.set_label(_('Move down'))
        button4.connect(
            'clicked',
            self.on_scheduled_move_down_button_clicked,
            treeview,
        )

        button5 = Gtk.Button()
        grid.attach(button5, 4, 3, 1, 1)
        button5.set_label(_('Delete'))
        button5.connect(
            'clicked',
            self.on_scheduled_delete_button_clicked,
            treeview,
        )


    def setup_scheduling_start_tab_update_treeview(self):

        """ Called by self.setup_scheduling_start_tab() and
        mainapp.TartubeApp.del_scheduled_list().

        Fills or updates the treeview.
        """

        self.schedule_liststore.clear()

        for scheduled_obj in self.app_obj.scheduled_list:
            self.setup_scheduling_start_tab_add_row(scheduled_obj)


    def setup_scheduling_start_tab_add_row(self, scheduled_obj):

        """Called by self.setup_scheduling_start_tab_update_treeview() and
        .on_scheduled_add_button_clicked().

        Adds a row to the treeview.

        Args:

            scheduled_obj (media.Scheduled) - The scheduled download object to
                display on this row

        """

        row_list = []

        row_list.append(scheduled_obj.name)

        if scheduled_obj.dl_mode == 'sim':
            row_list.append(_('Check'))
        elif scheduled_obj.dl_mode == 'real':
            row_list.append(_('Download'))
        else:
            row_list.append(_('Custom'))

        row_list.append(scheduled_obj.start_mode)

        if scheduled_obj.start_mode != 'timetable':

            row_list.append(
                str(scheduled_obj.wait_value) + ' ' + scheduled_obj.wait_unit
            )

        elif scheduled_obj.timetable_list:

            # (Show the first day/time combination only)
            mini_list = scheduled_obj.timetable_list[0]
            row_list.append(
                formats.SPECIFIED_DAYS_DICT[mini_list[0]] + ' ' + mini_list[1],
            )

        else:

            row_list.append('')

        row_list.append(scheduled_obj.exclusive_flag)
        row_list.append(scheduled_obj.ignore_limits_flag)
        row_list.append(scheduled_obj.shutdown_flag)
        row_list.append(scheduled_obj.all_flag)
        row_list.append(scheduled_obj.join_mode)

        self.schedule_liststore.append(row_list)


    def setup_scheduling_stop_tab(self, inner_notebook):

        """Called by self.setup_scheduling_tab().

        Sets up the 'Stop' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Scheduling > Stop'
        )

        tab, grid = self.add_inner_notebook_tab(_('S_top'), inner_notebook)
        grid_width = 3

        # Scheduled stop preferences
        self.add_label(grid,
            '<u>' + _('Scheduled stop preferences') + '</u>',
            0, 0, grid_width, 1,
        )

        self.add_label(grid,
            '<i>' + _(
            'N.B. Each scheduled download also has its own \'stop\' settings' \
            + ' which override these settings',
            ) + '</i>',
            0, 1, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Stop all download operations after this much time'),
            self.app_obj.autostop_time_flag,
            True,                   # Can be toggled by user
            0, 2, 1, 1,
        )
        # (Signal connect appears below)

        spinbutton = self.add_spinbutton(grid,
            1, None, 1, self.app_obj.autostop_time_value,
            1, 2, 1, 1,
        )
        if not self.app_obj.autostop_time_flag:
            spinbutton.set_sensitive(False)

        store = Gtk.ListStore(str, str)
        for string in formats.TIME_METRIC_LIST:
            store.append( [string, formats.TIME_METRIC_TRANS_DICT[string]] )

        combo = Gtk.ComboBox.new_with_model(store)
        grid.attach(combo, 2, 2, 1, 1)

        renderer_text = Gtk.CellRendererText()
        combo.pack_start(renderer_text, True)
        combo.add_attribute(renderer_text, 'text', 1)
        combo.set_entry_text_column(1)
        combo.set_active(
            formats.TIME_METRIC_LIST.index(
                self.app_obj.autostop_time_unit,
            )
        )
        if not self.app_obj.autostop_time_flag:
            combo.set_sensitive(False)
        # (Signal connect appears below)

        # (Signal connects from above)
        checkbutton.connect(
            'toggled',
            self.on_autostop_time_button_toggled,
            spinbutton,
            combo,
        )
        spinbutton.connect(
            'value-changed',
            self.on_autostop_time_spinbutton_changed,
        )
        combo.connect('changed', self.on_autostop_time_combo_changed)

        checkbutton2 = self.add_checkbutton(grid,
            _('Stop all download operations after this many videos'),
            self.app_obj.autostop_videos_flag,
            True,                   # Can be toggled by user
            0, 3, 1, 1,
        )
        # (Signal connect appears below)

        spinbutton2 = self.add_spinbutton(grid,
            1, None, 1, self.app_obj.autostop_videos_value,
            1, 3, 1, 1,
        )
        if not self.app_obj.autostop_videos_flag:
            spinbutton2.set_sensitive(False)
        # (Signal connect appears below)

        # (Signal connects from above)
        checkbutton2.connect(
            'toggled',
            self.on_autostop_videos_button_toggled,
            spinbutton2,
        )
        spinbutton2.connect(
            'value-changed',
            self.on_autostop_videos_spinbutton_changed,
        )

        checkbutton3 = self.add_checkbutton(grid,
            _('Stop all download operations after this much disk space'),
            self.app_obj.autostop_size_flag,
            True,                   # Can be toggled by user
            0, 4, 1, 1,
        )
        # (Signal connect appears below)

        spinbutton3 = self.add_spinbutton(grid,
            1, None, 1, self.app_obj.autostop_size_value,
            1, 4, 1, 1,
        )
        if not self.app_obj.autostop_size_flag:
            spinbutton3.set_sensitive(False)

        combo3 = self.add_combo(grid,
            formats.FILESIZE_METRIC_LIST,
            None,
            2, 4, 1, 1,
        )
        combo3.set_active(
            formats.FILESIZE_METRIC_LIST.index(
                self.app_obj.autostop_size_unit,
            )
        )
        if not self.app_obj.autostop_size_flag:
            combo3.set_sensitive(False)
        # (Signal connect appears below)

        # (Signal connects from above)
        checkbutton3.connect(
            'toggled',
            self.on_autostop_size_button_toggled,
            spinbutton3,
            combo3,
        )
        spinbutton3.connect(
            'value-changed',
            self.on_autostop_size_spinbutton_changed,
        )
        combo3.connect('changed', self.on_autostop_size_combo_changed)

        self.add_label(grid,
            '<i>' + _(
                'N.B. Disk space is estimated. This setting does not apply' \
                + ' to simulated downloads',
            ) + '</i>',
            0, 5, grid_width, 1,
        )


    def setup_operations_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Operations' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Operations'
        )

        # Add this tab...
        tab, grid = self.add_notebook_tab(_('_Operations'), 0)

        # ...and an inner notebook...
        self.operations_inner_notebook = self.add_inner_notebook(grid)

        # ...with its own tabs
        self.setup_operations_limits_tab(self.operations_inner_notebook)
        self.setup_operations_stop_tab(self.operations_inner_notebook)
        self.setup_operations_downloads_tab(self.operations_inner_notebook)
        self.setup_operations_ignore_tab(self.operations_inner_notebook)
        self.setup_operations_custom_dl_tab(self.operations_inner_notebook)
        if not self.app_obj.simple_prefs_flag:
            self.setup_operations_archive_tab(self.operations_inner_notebook)
        self.setup_operations_livestreams_tab(self.operations_inner_notebook)
        self.setup_operations_actions_tab(self.operations_inner_notebook)
        self.setup_operations_clips_tab(self.operations_inner_notebook)
        self.setup_operations_slices_tab(self.operations_inner_notebook)
        self.setup_operations_comments_tab(self.operations_inner_notebook)
        if not self.app_obj.simple_prefs_flag:
            self.setup_operations_mirrors_tab(self.operations_inner_notebook)
            self.setup_operations_proxies_tab(self.operations_inner_notebook)
        if not self.app_obj.simple_prefs_flag:
            self.setup_operations_prefs_tab(self.operations_inner_notebook)
        else:
            self.setup_operations_missing_tab(self.operations_inner_notebook)


    def setup_operations_limits_tab(self, inner_notebook):

        """Called by self.setup_operations_tab().

        Sets up the 'Limits' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Operations > Limits'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Limits'),
            inner_notebook,
        )
        grid_width = 3

        # Performance limits
        self.add_label(grid,
            '<u>' + _('Performance limits') + '</u>',
            0, 0, grid_width, 1,
        )

        self.add_label(grid,
            '<i>' + _('Limits are applied when you start downloading a' \
            + ' video/channel/playlist') + '</i>',
            0, 1, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Limit simultaneous downloads to'),
            self.app_obj.num_worker_apply_flag,
            True,               # Can be toggled by user
            0, 2, 1, 1,
        )
        checkbutton.set_hexpand(False)
        checkbutton.connect('toggled', self.on_worker_button_toggled)

        spinbutton = self.add_spinbutton(grid,
            self.app_obj.num_worker_min,
            self.app_obj.num_worker_max,
            1,                  # Step
            self.app_obj.num_worker_default,
            1, 2, 1, 1,
        )
        spinbutton.connect('value-changed', self.on_worker_spinbutton_changed)

        checkbutton2 = self.add_checkbutton(grid,
            _('Limit download speed to'),
            self.app_obj.bandwidth_apply_flag,
            True,               # Can be toggled by user
            0, 3, 1, 1,
        )
        checkbutton2.set_hexpand(False)
        checkbutton2.connect('toggled', self.on_bandwidth_button_toggled)

        spinbutton2 = self.add_spinbutton(grid,
            self.app_obj.bandwidth_min,
            self.app_obj.bandwidth_max,
            1,                  # Step
            self.app_obj.bandwidth_default,
            1, 3, 1, 1,
        )
        spinbutton2.connect(
            'value-changed',
            self.on_bandwidth_spinbutton_changed,
        )

        self.add_label(grid,
            'KiB/s',
            2, 3, 1, 1,
        )

        checkbutton3 = self.add_checkbutton(grid,
            _('Overriding video format options, limit video resolution to'),
            self.app_obj.video_res_apply_flag,
            True,               # Can be toggled by user
            0, 4, 1, 1,
        )
        checkbutton3.set_hexpand(False)
        checkbutton3.connect('toggled', self.on_video_res_button_toggled)

        combo = self.add_combo(grid,
            formats.VIDEO_RESOLUTION_LIST,
            None,
            1, 4, 1, 1,
        )
        combo.set_active(
            formats.VIDEO_RESOLUTION_LIST.index(
                self.app_obj.video_res_default,
            )
        )
        combo.connect('changed', self.on_video_res_combo_changed)

        # Alternative performance limits
        self.add_label(grid,
            '<u>' + _('Alternative performance limits') + '</u>',
            0, 5, grid_width, 1,
        )

        checkbutton4 = self.add_checkbutton(grid,
            _('Limit simultaneous downloads to'),
            self.app_obj.alt_num_worker_apply_flag,
            True,               # Can be toggled by user
            0, 6, 1, 1,
        )
        checkbutton4.set_hexpand(False)
        checkbutton4.connect('toggled', self.on_worker_button_toggled, True)

        spinbutton3 = self.add_spinbutton(grid,
            self.app_obj.num_worker_min,
            self.app_obj.num_worker_max,
            1,                  # Step
            self.app_obj.alt_num_worker,
            1, 6, 1, 1,
        )
        spinbutton3.connect(
            'value-changed',
            self.on_worker_spinbutton_changed,
            True,
        )

        checkbutton5 = self.add_checkbutton(grid,
            _('Limit download speed to'),
            self.app_obj.alt_bandwidth_apply_flag,
            True,               # Can be toggled by user
            0, 7, 1, 1,
        )
        checkbutton5.set_hexpand(False)
        checkbutton5.connect('toggled', self.on_bandwidth_button_toggled, True)

        spinbutton4 = self.add_spinbutton(grid,
            self.app_obj.bandwidth_min,
            self.app_obj.bandwidth_max,
            1,                  # Step
            self.app_obj.alt_bandwidth,
            1, 7, 1, 1,
        )
        spinbutton4.connect(
            'value-changed',
            self.on_bandwidth_spinbutton_changed,
            True,
        )

        self.add_label(grid,
            'KiB/s',
            2, 7, 1, 1,
        )

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 8, (grid_width - 1), 1)

        label = self.add_label(grid2,
            _('Alternative limits apply between') + '   ',
            0, 0, 1, 1,
        )

        # (Hours in format '00' to '23')
        start_time = self.app_obj.alt_start_time
        stop_time = self.app_obj.alt_stop_time

        hour_list = []
        for h in range(24):
            hour_list.append('{:02d}'.format(h))

        # (Minutes in format '00', '05', 10' .. '55')
        minute_list = []
        for n in range(12):
            minute_list.append('{:02d}'.format(n*5))

        combo2 = self.add_combo(grid2,
            hour_list,
            None,
            1, 0, 1, 1,
        )
        combo2.set_active(hour_list.index(start_time[0:2]))
        combo2.connect('changed', self.on_alt_time_combo_changed, 'start_hour')

        label2 = self.add_label(grid2,
            ' : ',
            2, 0, 1, 1,
        )
        label2.set_hexpand(False)

        combo3 = self.add_combo(grid2,
            minute_list,
            None,
            3, 0, 1, 1,
        )
        combo3.set_active(minute_list.index(start_time[3:5]))
        combo3.connect('changed', self.on_alt_time_combo_changed, 'start_min')

        label3 = self.add_label(grid2,
            '   ' + _('and') + '   ',
            4, 0, 1, 1,
        )
        label3.set_hexpand(False)

        combo4 = self.add_combo(grid2,
            hour_list,
            None,
            5, 0, 1, 1,
        )
        combo4.set_active(hour_list.index(stop_time[0:2]))
        combo4.connect('changed', self.on_alt_time_combo_changed, 'stop_hour')

        label4 = self.add_label(grid2,
            ' : ',
            6, 0, 1, 1,
        )
        label4.set_hexpand(False)

        combo5 = self.add_combo(grid2,
            minute_list,
            None,
            7, 0, 1, 1,
        )
        combo5.set_active(minute_list.index(stop_time[3:5]))
        combo5.connect('changed', self.on_alt_time_combo_changed, 'stop_min')

        label5 = self.add_label(grid2,
            _('On days') + '   ',
            0, 1, 1, 1,
        )

        combo6_list = []
        for s in formats.SPECIFIED_DAYS_LIST:
            combo6_list.append( [formats.SPECIFIED_DAYS_DICT[s], s] )

        combo6 = self.add_combo_with_data(grid2,
            combo6_list,
            None,
            1, 1, 7, 1,
        )
        combo6.set_hexpand(False)
        combo6.set_active(
            formats.SPECIFIED_DAYS_LIST.index(self.app_obj.alt_day_string),
        )
        combo6.connect('changed', self.on_alt_days_combo_changed)


    def setup_operations_stop_tab(self, inner_notebook):

        """Called by self.setup_operations_tab().

        Sets up the 'Stop' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Operations > Stop'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Stop'),
            inner_notebook,
        )
        grid_width = 2

        # Time-saving settings
        self.add_label(grid,
            '<u>' + _('Time-saving settings') + '</u>',
            0, 0, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _(
            'Stop checking/downloading a channel/playlist when it finds' \
            + ' videos you already have',
            ),
            self.app_obj.operation_limit_flag,
            True,               # Can be toggled by user
            0, 1, grid_width, 1,
        )
        checkbutton.set_hexpand(False)
        # (Signal connect appears below)

        self.add_label(grid,
            _('Stop after this many videos (when checking)'),
            0, 2, 1, 1,
        )

        entry = self.add_entry(grid,
            self.app_obj.operation_check_limit,
            True,
            1, 2, 1, 1,
        )
        entry.set_width_chars(4)
        if not self.app_obj.operation_limit_flag:
            entry.set_sensitive(False)
        # (Signal connect appears below)

        self.add_label(grid,
            _('Stop after this many videos (when downloading)'),
            0, 3, 1, 1,
        )

        entry2 = self.add_entry(grid,
            self.app_obj.operation_download_limit,
            True,
            1, 3, 1, 1,
        )
        entry2.set_width_chars(4)
        if not self.app_obj.operation_limit_flag:
            entry2.set_sensitive(False)
        # (Signal connect appears below)

        checkbutton2 = self.add_checkbutton(grid,
            _('Include videos filtered by upload date, views or age limit'),
            self.app_obj.operation_limit_include_out_of_range_flag,
            True,               # Can be toggled by user
            0, 4, grid_width, 1,
        )
        checkbutton2.set_hexpand(False)
        # (Signal connect appears below)

        # (Signal connects from above)
        checkbutton.connect(
            'toggled',
            self.on_limit_button_toggled,
            entry,
            entry2,
            checkbutton2,
        )
        entry.connect('changed', self.on_check_limit_changed)
        entry2.connect('changed', self.on_dl_limit_changed)
        checkbutton2.connect('toggled', self.on_limit_range_button_toggled)


    def setup_operations_downloads_tab(self, inner_notebook):

        """Called by self.setup_operations_tab().

        Sets up the 'Downloads' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Operations > Downloads'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Downloads'),
            inner_notebook,
        )
        grid_width = 2

        # Download operation preferences
        self.add_label(grid,
            '<u>' + _('Download operation preferences') + '</u>',
            0, 0, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _(
            'Automatically update downloader before every download operation',
            ),
            self.app_obj.operation_auto_update_flag,
            True,                   # Can be toggled by user
            0, 1, grid_width, 1,
        )
        checkbutton.connect('toggled', self.on_auto_update_button_toggled)
        if __main__.__pkg_strict_install_flag__:
            checkbutton.set_sensitive(False)

        checkbutton2 = self.add_checkbutton(grid,
            _(
            'Automatically save files at the end of all operations',
            ),
            self.app_obj.operation_save_flag,
            True,                   # Can be toggled by user
            0, 2, grid_width, 1,
        )
        checkbutton2.connect('toggled', self.on_save_button_toggled)

        if not self.app_obj.simple_prefs_flag:

            checkbutton3 = self.add_checkbutton(grid,
                _(
                'For simulated downloads, don\'t check a video in a folder' \
                + ' more than once',
                ),
                self.app_obj.operation_sim_shortcut_flag,
                True,                   # Can be toggled by user
                0, 3, grid_width, 1,
            )
            checkbutton3.connect(
                'toggled',
                self.on_operation_sim_button_toggled,
            )

            checkbutton4 = self.add_checkbutton(grid,
                _(
                'If a download stalls, restart it after this many minutes',
                ),
                self.app_obj.operation_auto_restart_flag,
                True,                   # Can be toggled by user
                0, 4, 1, 1,
            )
            # (Signal connect appears below)

            spinbutton = self.add_spinbutton(grid,
                1,
                None,
                1,                     # Step
                self.app_obj.operation_auto_restart_time,
                1, 4, 1, 1,
            )
            # (Signal connect appears below)
            if not self.app_obj.operation_auto_restart_flag:
                spinbutton.set_sensitive(False)

            self.add_label(grid,
                '     ' \
                + _(
                'Maximum restarts after a stalled download (0 for no maximum)',
                ),
                0, 5, 1, 1,
            )

            spinbutton2 = self.add_spinbutton(grid,
                0,
                None,
                1,                     # Step
                self.app_obj.operation_auto_restart_max,
                1, 5, 1, 1,
            )
            # (Signal connect appears below)
            if not self.app_obj.operation_auto_restart_flag:
                spinbutton2.set_sensitive(False)

            # (Signal connects from above)
            checkbutton4.connect(
                'toggled',
                self.on_auto_restart_button_toggled,
                spinbutton,
                spinbutton2,
            )
            spinbutton.connect(
                'value-changed',
                self.on_auto_restart_time_spinbutton_changed,
            )
            spinbutton2.connect(
                'value-changed',
                self.on_auto_restart_max_spinbutton_changed,
            )

            checkbutton5 = self.add_checkbutton(grid,
                _('Apply a timeout (in minutes) when checking a video'),
                self.app_obj.apply_json_timeout_flag,
                True,                   # Can be toggled by user
                0, 6, grid_width, 1,
            )
            # (Signal connect appears below)

            # (To avoid messing up the neat format of the rows above, add a
            #   secondary grid, and put the next set of widgets inside it)
            grid2 = self.add_secondary_grid(grid, 0, 7, grid_width, 1)

            label = self.add_label(grid2,
                _('Without comments'),
                0, 0, 1, 1,
            )

            spinbutton3 = self.add_spinbutton(grid2,
                1,
                None,
                1,                     # Step
                self.app_obj.json_timeout_no_comments_time,
                1, 0, 1, 1,
            )
            # (Signal connect appears below)
            if not self.app_obj.apply_json_timeout_flag:
                spinbutton3.set_sensitive(False)

            label2 = self.add_label(grid2,
                _('With comments'),
                2, 0, 1, 1,
            )

            spinbutton4 = self.add_spinbutton(grid2,
                1,
                None,
                1,                     # Step
                self.app_obj.json_timeout_with_comments_time,
                3, 0, 1, 1,
            )
            # (Signal connect appears below)
            if not self.app_obj.apply_json_timeout_flag:
                spinbutton4.set_sensitive(False)

            # (Signal connects from above)
            checkbutton5.connect(
                'toggled',
                self.on_json_button_toggled,
                spinbutton3,
                spinbutton4,
            )
            spinbutton3.connect(
                'value-changed',
                self.on_timeout_no_comments_spinbutton_changed,
            )
            spinbutton4.connect(
                'value-changed',
                self.on_timeout_with_comments_spinbutton_changed,
            )

            checkbutton6 = self.add_checkbutton(grid,
                _(
                'Assign anonymous error/warning messages to the most' \
                + ' probable video',
                ),
                self.app_obj.auto_assign_errors_warnings_flag,
                True,                   # Can be toggled by user
                0, 8, grid_width, 1,
            )
            checkbutton6.connect('toggled', self.on_auto_assign_button_toggled)

        checkbutton7 = self.add_checkbutton(grid,
            _(
            'Add censored, age-restricted and other blocked videos to the' \
            + ' database',
            ),
            self.app_obj.add_blocked_videos_flag,
            True,                   # Can be toggled by user
            0, 9, grid_width, 1,
        )
        checkbutton7.connect('toggled', self.on_add_blocked_button_toggled)

        if not self.app_obj.simple_prefs_flag:

            checkbutton8 = self.add_checkbutton(grid,
                _(
                'Extract playlist IDs from each video, and store them in the' \
                + ' parent channel/playlist',
                ),
                self.app_obj.store_playlist_id_flag,
                True,                   # Can be toggled by user
                0, 10, grid_width, 1,
            )
            checkbutton8.connect('toggled', self.on_store_playlist_id_toggled)

            checkbutton9 = self.add_checkbutton(grid,
                _(
                'Convert .webp thumbnails into .jpg thumbnails (using' \
                + '  FFmpeg) after downloading them',
                ),
                self.app_obj.ffmpeg_convert_webp_flag,
                True,                   # Can be toggled by user
                0, 11, grid_width, 1,
            )
            # (Signal connect appears below)

            checkbutton10 = self.add_checkbutton(grid,
                _(
                    '...but don\'t delete the original thumbnails (enable' \
                    + ' before embedding thumbnails in videos)',
                ),
                self.app_obj.ffmpeg_retain_webp_flag,
                True,                   # Can be toggled by user
                0, 12, grid_width, 1,
            )
            if not self.app_obj.ffmpeg_convert_webp_flag:
                checkbutton.set_sensitive(False)
            checkbutton10.connect(
                'toggled',
                self.on_ffmpeg_retain_flag_toggled,
            )

            # (Signal connects from above)
            checkbutton9.connect(
                'toggled',
                self.on_ffmpeg_convert_flag_toggled,
                checkbutton10,
            )


    def setup_operations_ignore_tab(self, inner_notebook):

        """Called by self.setup_operations_tab().

        Sets up the 'Ignore' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Operations > Ignore'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Ignore'),
            inner_notebook,
        )
        grid_width = 2

        # Ignore downloader errors/warnings
        self.add_label(grid,
            '<u>' + _('Ignore downloader errors/warnings') + '</u>',
            0, 0, grid_width, 1,
        )

        ignore_me = _(
            'TRANSLATOR\'S NOTE: These error messages are always in English',
        )

        checkbutton = self.add_checkbutton(grid,
            _('Ignore \'Child process exited with non-zero code\' errors'),
            self.app_obj.ignore_child_process_exit_flag,
            True,                   # Can be toggled by user
            0, 1, grid_width, 1,
        )
        checkbutton.connect('toggled', self.on_child_process_button_toggled)

        checkbutton2 = self.add_checkbutton(grid,
            _(
            'Ignore \'Unable to download video data\' and \'Unable to' \
            + ' extract video data\' errors',
            ),
            self.app_obj.ignore_http_404_error_flag,
            True,                   # Can be toggled by user
            0, 2, grid_width, 1,
        )
        checkbutton2.connect('toggled', self.on_http_404_button_toggled)

        checkbutton3 = self.add_checkbutton(grid,
            _('Ignore \'Did not get any data blocks\' errors'),
            self.app_obj.ignore_data_block_error_flag,
            True,                   # Can be toggled by user
            0, 3, grid_width, 1,
        )
        checkbutton3.connect('toggled', self.on_data_block_button_toggled)

        checkbutton4 = self.add_checkbutton(grid,
            _(
            'Ignore \'Requested formats are incompatible for merge\' warnings',
            ),
            self.app_obj.ignore_merge_warning_flag,
            True,                   # Can be toggled by user
            0, 4, grid_width, 1,
        )
        checkbutton4.connect('toggled', self.on_merge_button_toggled)

        checkbutton5 = self.add_checkbutton(grid,
            _('Ignore \'No video formats found\' errors'),
            self.app_obj.ignore_missing_format_error_flag,
            True,                   # Can be toggled by user
            0, 5, grid_width, 1,
        )
        checkbutton5.connect('toggled', self.on_missing_format_button_toggled)

        checkbutton6 = self.add_checkbutton(grid,
            _('Ignore \'There are no annotations to write\' warnings'),
            self.app_obj.ignore_no_annotations_flag,
            True,                   # Can be toggled by user
            0, 6, grid_width, 1,
        )
        checkbutton6.connect('toggled', self.on_no_annotations_button_toggled)

        checkbutton7 = self.add_checkbutton(grid,
            _('Ignore \'Video doesn\'t have subtitles\' warnings'),
            self.app_obj.ignore_no_subtitles_flag,
            True,                   # Can be toggled by user
            0, 7, grid_width, 1,
        )
        checkbutton7.connect('toggled', self.on_no_subtitles_button_toggled)

        checkbutton8 = self.add_checkbutton(grid,
            _('Ignore \'A channel/user page was given\' warnings'),
            self.app_obj.ignore_page_given_flag,
            True,                   # Can be toggled by user
            0, 8, grid_width, 1,
        )
        checkbutton8.connect('toggled', self.on_page_given_button_toggled)

        checkbutton9 = self.add_checkbutton(grid,
            _('Ignore \'There\'s no playlist description to write\' warnings'),
            self.app_obj.ignore_no_descrip_flag,
            True,                   # Can be toggled by user
            0, 9, grid_width, 1,
        )
        checkbutton9.connect('toggled', self.on_no_descrip_button_toggled)

        checkbutton10 = self.add_checkbutton(grid,
            _(
            'Ignore \'Unable to download video thumbnail: HTTP Error 404:' \
            + ' Not Found\' warnings',
            ),
            self.app_obj.ignore_thumb_404_flag,
            True,                   # Can be toggled by user
            0, 10, grid_width, 1,
        )
        checkbutton10.connect('toggled', self.on_thumb_404_button_toggled)

        checkbutton11 = self.add_checkbutton(grid,
            _(
            'Ignore \'The channel is not currently live\' warnings on Twitch',
            ),
            self.app_obj.ignore_twitch_not_live_flag,
            True,                   # Can be toggled by user
            0, 11, grid_width, 1,
        )
        checkbutton11.connect('toggled', self.on_twitch_live_button_toggled)


    def setup_operations_custom_dl_tab(self, inner_notebook):

        """Called by self.setup_operations_tab().

        Sets up the 'Custom' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Operations > Custom'
        )

        tab, grid = self.add_inner_notebook_tab(_('_Custom'), inner_notebook)
        grid_width = 4

        # Custom downloads
        self.add_label(grid,
            '<u>' + _('Custom downloads') + '</u>',
            0, 0, grid_width, 1,
        )

        # (GenericConfigWin.add_treeview() doesn't support multiple columns, so
        #   we'll do everything ourselves)
        frame = Gtk.Frame()
        grid.attach(frame, 0, 1, grid_width, 1)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)

        treeview = Gtk.TreeView()
        scrolled.add(treeview)
        treeview.set_headers_visible(True)

        # (The final column is deliberately empty, so that the previous column
        #   doesn't expand to fill the whole available area)
        for i, column_title in enumerate(
            [ '#', _('Name'), _('Default'), _('Classic Mode'), '']
        ):
            if i == 2 or i == 3:
                renderer_toggle = Gtk.CellRendererToggle()
                column_toggle = Gtk.TreeViewColumn(
                    column_title,
                    renderer_toggle,
                    active=i,
                )
                treeview.append_column(column_toggle)
                column_toggle.set_resizable(False)
            else:
                renderer_text = Gtk.CellRendererText()
                column_text = Gtk.TreeViewColumn(
                    column_title,
                    renderer_text,
                    text=i,
                )
                treeview.append_column(column_text)
                column_text.set_resizable(True)

        self.custom_liststore = Gtk.ListStore(int, str, bool, bool, str)
        treeview.set_model(self.custom_liststore)

        # Initialise the list
        self.setup_operations_custom_dl_tab_update_treeview()

        # Add editing buttons
        self.add_label(grid,
            'Name',
            0, 2, 1, 1,
        )

        entry = self.add_entry(grid,
            None,
            True,
            1, 2, 1, 1,
        )

        button = Gtk.Button()
        grid.attach(button, 2, 2, 1, 1)
        button.set_label(_('Add'))
        button.connect(
            'clicked',
            self.on_custom_dl_add_button_clicked,
            entry,
        )

        button2 = Gtk.Button()
        grid.attach(button2, 3, 2, 1, 1)
        button2.set_label(_('Import'))
        button2.connect(
            'clicked',
            self.on_custom_dl_import_button_clicked,
            entry,
        )

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 3, grid_width, 1)

        button3 = Gtk.Button()
        grid2.attach(button3, 0, 0, 1, 1)
        button3.set_label(_('Edit'))
        button3.connect(
            'clicked',
            self.on_custom_dl_edit_button_clicked,
            treeview,
        )

        button4 = Gtk.Button()
        grid2.attach(button4, 1, 0, 1, 1)
        button4.set_label(_('Export'))
        button4.connect(
            'clicked',
            self.on_custom_dl_export_button_clicked,
            treeview,
        )

        button5 = Gtk.Button()
        grid2.attach(button5, 2, 0, 1, 1)
        button5.set_label(_('Clone'))
        button5.connect(
            'clicked',
            self.on_custom_dl_clone_button_clicked,
            treeview,
        )

        button6 = Gtk.Button()
        grid2.attach(button6, 3, 0, 1, 1)
        button6.set_label(_('Use in Classic Mode tab'))
        button6.connect(
            'clicked',
            self.on_custom_dl_use_classic_button_clicked,
            treeview,
        )

        button7 = Gtk.Button()
        grid2.attach(button7, 4, 0, 1, 1)
        button7.set_label(_('Delete'))
        button7.connect(
            'clicked',
            self.on_custom_dl_delete_button_clicked,
            treeview,
        )

        # (Use an empty label for spacing)
        label = self.add_label(grid2,
            '',
            5, 0, 1, 1,
        )
        label.set_hexpand(True)

        button8 = Gtk.Button()
        grid2.attach(button8, 6, 0, 1, 1)
        button8.set_label(_('Refresh list'))
        button8.connect(
            'clicked',
            self.setup_operations_custom_dl_tab_update_treeview,
        )


    def setup_operations_custom_dl_tab_update_treeview(self):

        """Can be called by anything.

        Fills or updates the treeview.

        """

        self.custom_liststore.clear()

        for uid in sorted(self.app_obj.custom_dl_reg_dict):
            self.setup_operations_custom_dl_tab_add_row(
                self.app_obj.custom_dl_reg_dict[uid],
            )


    def setup_operations_custom_dl_tab_add_row(self, custom_dl_obj):

        """Can be called by anything.

        Adds a row to the treeview.

        Args:

            custom_dl_obj (downloads.CustomDLManager): The custom download
                manager object to display on this row

        """

        row_list = []

        row_list.append(custom_dl_obj.uid)
        row_list.append(
            ttutils.tidy_up_long_string(
                custom_dl_obj.name,
                self.app_obj.main_win_obj.short_string_max_len,
            ),
        )

        if self.app_obj.general_custom_dl_obj \
        and self.app_obj.general_custom_dl_obj == custom_dl_obj:
            row_list.append(True)
        else:
            row_list.append(False)

        if self.app_obj.classic_custom_dl_obj \
        and self.app_obj.classic_custom_dl_obj == custom_dl_obj:
            row_list.append(True)
        else:
            row_list.append(False)

        # (The final column is deliberately empty, so that the previous column
        #   doesn't expand to fill the whole available area)
        row_list.append('')

        self.custom_liststore.append(row_list)


    def setup_operations_archive_tab(self, inner_notebook):

        """Called by self.setup_operations_tab().

        Sets up the 'Archive' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Operations > Archive'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Archive'),
            inner_notebook,
        )

        grid_width = 4

        # Archive file preferences
        self.add_label(grid,
            '<u>' + _('Archive file preferences') + '</u>',
            0, 0, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _(
            'Allow downloader to create its own archive file (so deleted' \
            + ' videos are not re-downloaded)',
            ),
            self.app_obj.allow_ytdl_archive_flag,
            True,                   # Can be toggled by user
            0, 1, grid_width, 1,
        )
        # (Signal connect appears below)

        # (Empty label for spacing)
        label = self.add_label(grid,
            '     ',
            0, 2, 1, 1,
        )
        label.set_hexpand(False)

        radiobutton = self.add_radiobutton(grid,
            None,
            _(
            'Store the archive file in the same location as the video',
            ),
            1, 2, (grid_width - 1), 1,
        )
        # (Signal connect appears below)

        self.add_label(grid,
            '<i>' + _(
                'N.B. Archive files are never stored in system folders like' \
                + ' \'Unsorted Videos\'',
            ) + '</i>',
            1, 3, (grid_width - 1), 1,
        )

        radiobutton2 = self.add_radiobutton(grid,
            radiobutton,
            _(
            'Store the archive file in Tartube\'s data directory',
            ),
            1, 4, (grid_width - 1), 1,
        )
        if self.app_obj.allow_ytdl_archive_mode == 'top':
            radiobutton2.set_active(True)
        # (Signal connect appears below)

        radiobutton3 = self.add_radiobutton(grid,
            radiobutton2,
            _(
            'Store the archive file at this location:',
            ),
            1, 5, (grid_width - 1), 1,
        )
        if self.app_obj.allow_ytdl_archive_mode == 'custom':
            radiobutton3.set_active(True)
        # (Signal connect appears below)

        entry = self.add_entry(grid,
            None,
            True,
            1, 6, 1, 1,
        )
        if self.app_obj.allow_ytdl_archive_path != None:
            entry.set_text(self.app_obj.allow_ytdl_archive_path)
        entry.set_hexpand(True)
        entry.set_editable(False)

        button = Gtk.Button(_('Set'))
        grid.attach(button, 2, 6, 1, 1)
        # (Signal connect appears below)

        button2 = Gtk.Button(_('Reset'))
        grid.attach(button2, 3, 6, 1, 1)
        # (Signal connect appears below)

        checkbutton2 = self.add_checkbutton(grid,
            _(
            'Update the archive file when videos are moved into a database' \
            + ' folder (YouTube only)',
            ),
            self.app_obj.update_ytdl_archive_on_move_flag,
            True,                   # Can be toggled by user
            0, 7, grid_width, 1,
        )
        checkbutton2.connect('toggled', self.on_archive_update_toggled)

        # (Signal connects from above)
        checkbutton.connect(
            'toggled',
            self.on_archive_button_toggled,
            checkbutton2,
            radiobutton,
            radiobutton2,
            radiobutton3,
            button,
            button2,
        )
        radiobutton.connect(
            'toggled',
            self.on_archive_radiobutton_toggled,
            radiobutton,
            radiobutton2,
            radiobutton3,
            entry,
            button,
            button2,
        )
        radiobutton2.connect(
            'toggled',
            self.on_archive_radiobutton_toggled,
            radiobutton,
            radiobutton2,
            radiobutton3,
            entry,
            button,
            button2,
        )
        radiobutton3.connect(
            'toggled',
            self.on_archive_radiobutton_toggled,
            radiobutton,
            radiobutton2,
            radiobutton3,
            entry,
            button,
            button2,
        )
        button.connect('clicked', self.on_set_archive_button_clicked, entry)
        button2.connect('clicked', self.on_reset_archive_button_clicked, entry)

        if not self.app_obj.allow_ytdl_archive_flag:
            radiobutton.set_sensitive(False)
            radiobutton2.set_sensitive(False)
            radiobutton3.set_sensitive(False)
        if not self.app_obj.allow_ytdl_archive_flag \
        or self.app_obj.allow_ytdl_archive_mode != 'custom':
            button.set_sensitive(False)
            button2.set_sensitive(False)

        # Classic Mode tab preferences
        self.add_label(grid,
            '<u>' + _('Classic Mode tab preferences') + '</u>',
            0, 8, grid_width, 1,
        )

        checkbutton2 = self.add_checkbutton(grid,
            _(
            'Create an archive file when downloading from the Classic Mode' \
            + ' tab',
            ),
            self.app_obj.classic_ytdl_archive_flag,
            True,                   # Can be toggled by user
            0, 9, grid_width, 1,
        )
        checkbutton2.connect('toggled', self.on_archive_classic_button_toggled)

        self.add_label(grid,
            '<i>' + _(
                'This setting should only be enabled when downloading' \
                + ' channels and playlists',
            ) + '</i>',
            0, 10, grid_width, 1,
        )


    def setup_operations_livestreams_tab(self, inner_notebook):

        """Called by self.setup_operations_tab().

        Sets up the 'Streams' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Operations > Livestreams'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('Li_vestreams'),
            inner_notebook,
        )
        grid_width = 2

        # Livestream preferences (compatible websites only)
        self.add_label(grid,
            '<u>' + _(
                'Livestream preferences (compatible websites only)',
            ) + '</u>',
            0, 0, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Do not check/download any livestream [yt-dlp only]'),
            self.app_obj.block_livestreams_flag,
            True,                   # Can be toggled by user
            0, 1, 1, 1,
        )
        checkbutton.connect(
            'toggled',
            self.on_block_livestreams_button_toggled,
        )

        checkbutton2 = self.add_checkbutton(grid,
            _('Detect livestreams announced within this many days'),
            self.app_obj.enable_livestreams_flag,
            True,                   # Can be toggled by user
            0, 2, 1, 1,
        )
        # (Signal connect appears below)
        spinbutton = self.add_spinbutton(grid,
            0, None, 1, self.app_obj.livestream_max_days,
            1, 2, 1, 1,
        )
        if not self.app_obj.enable_livestreams_flag:
            spinbutton.set_sensitive(False)
        # (Signal connect appears below)

        checkbutton3 = self.add_checkbutton(grid,
            _('How often to check the status of livestreams (in minutes)'),
            self.app_obj.scheduled_livestream_flag,
            True,                   # Can be toggled by user
            0, 3, 1, 1,
        )
        if not self.app_obj.enable_livestreams_flag:
            checkbutton3.set_sensitive(False)
        # (Signal connect appears below)

        spinbutton2 = self.add_spinbutton(grid,
            1, None, 1, self.app_obj.scheduled_livestream_wait_mins,
            1, 3, 1, 1,
        )
        if not self.app_obj.enable_livestreams_flag \
        or not self.app_obj.scheduled_livestream_flag:
            spinbutton2.set_sensitive(False)
        # (Signal connect appears below)

        checkbutton4 = self.add_checkbutton(grid,
            _('Check more frequently when a livestream is due to start'),
            self.app_obj.scheduled_livestream_extra_flag,
            True,                   # Can be toggled by user
            0, 4, grid_width, 1,
        )
        if not self.app_obj.enable_livestreams_flag \
        or not self.app_obj.scheduled_livestream_flag:
            checkbutton4.set_sensitive(False)
        checkbutton4.connect(
            'toggled',
            self.on_extra_livestreams_button_toggled,
        )

        # (Signal connects from above)
        checkbutton2.connect(
            'toggled',
            self.on_enable_livestreams_button_toggled,
            checkbutton3,
            checkbutton4,
            spinbutton,
            spinbutton2,
        )

        spinbutton.connect(
            'value-changed',
            self.on_livestream_max_days_spinbutton_changed,
        )

        checkbutton3.connect(
            'toggled',
            self.on_scheduled_livestreams_button_toggled,
            checkbutton4,
            spinbutton2,
        )

        spinbutton2.connect(
            'value-changed',
            self.on_scheduled_livestreams_spinbutton_changed,
        )

        # Broadcast preferences (compatible websites only)
        self.add_label(grid,
            '<u>' + _(
                'Broadcasting livestream preferences (compatible websites' \
                + ' only)',
            ) + '</u>',
            0, 5, grid_width, 1,
        )

        self.add_label(grid,
            '<i>' + _(
                'These settings apply when downloading videos individually,' \
                + ' for example with a custom download',
            ) + '</i>',
            0, 6, grid_width, 1,
        )

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 7, grid_width, 1)

        label = self.add_label(grid2,
            _('Download using:'),
            0, 0, 1, 1,
        )
        label.set_hexpand(False)

        self.livestream_radiobutton = self.add_radiobutton(grid2,
            None,
            '',
            1, 0, 1, 1,
        )
        self.livestream_radiobutton.set_hexpand(False)
        # (Signal connect appears below)

        self.livestream_radiobutton2 = self.add_radiobutton(grid2,
            self.livestream_radiobutton,
            _('.m3u manifest'),
            2, 0, 1, 1,
        )
        self.livestream_radiobutton2.set_hexpand(False)
        if self.app_obj.livestream_dl_mode == 'default_m3u':
            self.livestream_radiobutton2.set_active(True)
        # (Signal connect appears below)

        self.livestream_radiobutton3 = self.add_radiobutton(grid2,
            self.livestream_radiobutton2,
            'streamlink',
            3, 0, 1, 1,
        )
        self.livestream_radiobutton3.set_hexpand(False)
        if self.app_obj.livestream_dl_mode == 'streamlink':
            self.livestream_radiobutton3.set_active(True)
        # (Signal connect appears below)

        # (Set labels for those widgets, and replace them every time the
        #   downloader changes)
        self.setup_operations_livestreams_tab_update()

        # (Signal connects from above)
        self.livestream_radiobutton.connect(
            'toggled',
            self.on_livestream_mode_button_toggled,
            'default',
        )
        self.livestream_radiobutton2.connect(
            'toggled',
            self.on_livestream_mode_button_toggled,
            'default_m3u',
        )
        self.livestream_radiobutton3.connect(
            'toggled',
            self.on_livestream_mode_button_toggled,
            'streamlink',
        )

        if not self.app_obj.simple_prefs_flag:

            # (To avoid messing up the neat format of the rows above, add a
            #   secondary grid, and put the next set of widgets inside it)
            grid3 = self.add_secondary_grid(grid, 0, 8, grid_width, 1)

            self.livestream_radiobutton4 = self.add_radiobutton(grid3,
                None,
                _('Replace a partially-downloaded livestream'),
                1, 0, 1, 1,
            )
            self.livestream_radiobutton4.set_hexpand(False)

            self.livestream_radiobutton5 = self.add_radiobutton(grid3,
                self.livestream_radiobutton4,
                _('Resume a partially-downloaded livestream'),
                2, 0, 1, 1,
            )
            self.livestream_radiobutton5.set_hexpand(False)
            if not self.app_obj.livestream_replace_flag:
                self.livestream_radiobutton5.set_active(True)
            if self.app_obj.livestream_dl_mode == 'streamlink':
                self.livestream_radiobutton5.set_sensitive(False)
            # (Signal connect appears below)

            # (Signal connects from above)
            self.livestream_radiobutton4.connect(
                'toggled',
                self.on_livestream_replace_button_toggled,
            )

            # (More widgets)
            checkbutton5 = self.add_checkbutton(grid,
                _(
                'Bypass usual limits on simultaneous downloads, so that' \
                + ' all livestreams can be downloaded',
                ),
                self.app_obj.num_worker_bypass_flag,
                True,                   # Can be toggled by user
                0, 9, grid_width, 1,
            )
            checkbutton5.connect(
                'toggled',
                self.on_worker_bypass_button_toggled,
            )

            self.add_label(grid,
                _('Timeout after this many minutes of inactivity'),
                0, 10, 1, 1,
            )

            spinbutton3 = self.add_spinbutton(grid,
                1, None, 0.2,
                self.app_obj.livestream_dl_timeout,
                1, 10, 1, 1,
            )
            spinbutton3.connect(
                'value-changed',
                self.on_livestream_timeout_spinbutton_changed,
            )

            checkbutton6 = self.add_checkbutton(grid,
                _(
                'When the livestream download is stopped manually, mark the' \
                + ' video as downloaded',
                ),
                self.app_obj.livestream_stop_is_final_flag,
                True,                   # Can be toggled by user
                0, 11, grid_width, 1,
            )
            checkbutton6.connect(
                'toggled',
                self.on_livestream_stop_button_toggled,
            )

            checkbutton7 = self.add_checkbutton(grid,
                _(
                'Check a video before the livestream download (ensures' \
                + ' metadata is downloaded)',
                ),
                self.app_obj.livestream_force_check_flag,
                True,                   # Can be toggled by user
                0, 12, grid_width, 1,
            )
            checkbutton7.connect(
                'toggled',
                self.on_livestream_force_check_button_toggled,
            )

            self.add_label(grid,
                '   <i>' + _(
                    'N.B. This setting is ignored in the Classic Mode tab',
                ) + '</i>',
                0, 13, grid_width, 1,
            )


    def setup_operations_livestreams_tab_update(self):

        """Called initially by self.setup_operations_livestreams_tab, and
        subsequently by self.update_ytdl_combos().

        Updates labels in that tab to show the current downloader.
        """

        downloader = self.app_obj.get_downloader()

        self.livestream_radiobutton.set_label(
            downloader + ' (' + _('not recommended') + ')',
        )


    def setup_operations_actions_tab(self, inner_notebook):

        """Called by self.setup_scheduling_tab().

        Sets up the 'Actions' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Operations > Actions'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('Ac_tions'),
            inner_notebook,
        )
        grid_width = 3

        # Livestream actions (can be toggled for individual videos)
        self.add_label(grid,
            '<u>' + _(
                'Livestream actions (can be toggled for individual videos)',
            ) + '</u>',
            0, 0, grid_width, 1,
        )

        # Currently disabled on MS Windows
        if os.name == 'nt':
            string = ' ' + _('(currently disabled on MS Windows)')
        else:
            string = ''

        checkbutton = self.add_checkbutton(grid,
            _('When a livestream starts, show a desktop notification') \
            + string,
            self.app_obj.livestream_auto_notify_flag,
            True,                   # Can be toggled by user
            0, 1, grid_width, 1,
        )
        checkbutton.connect(
            'toggled',
            self.on_livestream_auto_notify_button_toggled,
        )
        if os.name == 'nt':
            checkbutton.set_sensitive(False)

        checkbutton2 = self.add_checkbutton(grid,
            _('When a livestream starts, sound an alarm'),
            self.app_obj.livestream_auto_alarm_flag,
            True,                   # Can be toggled by user
            0, 2, 1, 1,
        )
        if not mainapp.HAVE_PLAYSOUND_FLAG \
        or self.app_obj.sound_dir is None \
        or not self.app_obj.sound_list:
            checkbutton2.set_sensitive(False)
        checkbutton2.connect(
            'toggled',
            self.on_livestream_auto_alarm_button_toggled,
        )

        combo = self.add_combo(grid,
            self.app_obj.sound_list,
            self.app_obj.sound_custom,
            1, 2, 1, 1,
        )
        combo.connect('changed', self.on_sound_custom_changed)
        if not mainapp.HAVE_PLAYSOUND_FLAG \
        or self.app_obj.sound_dir is None \
        or not self.app_obj.sound_list:
            combo.set_visible(False)

        button = Gtk.Button(_('Test'))
        grid.attach(button, 2, 2, 1, 1)
        button.set_tooltip_text(_('Plays the selected sound effect'))
        button.connect('clicked', self.on_test_sound_clicked, combo)
        if not mainapp.HAVE_PLAYSOUND_FLAG \
        or self.app_obj.sound_dir is None \
        or not self.app_obj.sound_list:
            button.set_sensitive(False)

        checkbutton3 = self.add_checkbutton(grid,
            _(
            'When a livestream starts, open it in the system\'s web browser',
            ),
            self.app_obj.livestream_auto_open_flag,
            True,                   # Can be toggled by user
            0, 3, grid_width, 1,
        )
        checkbutton3.connect(
            'toggled',
            self.on_livestream_auto_open_button_toggled,
        )

        checkbutton4 = self.add_checkbutton(grid,
            _('When a livestream starts, begin downloading it immediately'),
            self.app_obj.livestream_auto_dl_start_flag,
            True,                   # Can be toggled by user
            0, 4, grid_width, 1,
        )
        checkbutton4.connect(
            'toggled',
            self.on_livestream_auto_dl_start_button_toggled,
        )

        checkbutton5 = self.add_checkbutton(grid,
            _(
            'When a livestream stops, download it (overwriting any earlier' \
            + ' file)',
            ),
            self.app_obj.livestream_auto_dl_stop_flag,
            True,                   # Can be toggled by user
            0, 5, grid_width, 1,
        )
        checkbutton5.connect(
            'toggled',
            self.on_livestream_auto_dl_stop_button_toggled,
        )

        if not self.app_obj.simple_prefs_flag:

            # Desktop notification preferences
            self.add_label(grid,
                '<u>' + _('Desktop notification preferences') + '</u>',
                0, 6, 1, 1,
            )

            radiobutton = self.add_radiobutton(grid,
                None,
                _('Show a dialogue window at the end of an operation'),
                0, 7, 1, 1,
            )
            # (Signal connect appears below)

            if platform.system() != 'Windows' \
            and platform.system() != 'Darwin':
                text = 'Show a desktop notification at the end of an operation'
            else:
                text = 'Show a desktop notification (Linux/*BSD only)'

            radiobutton2 = self.add_radiobutton(grid,
                radiobutton,
                _(text),
                0, 8, 1, 1,
            )
            if self.app_obj.operation_dialogue_mode == 'desktop':
                radiobutton2.set_active(True)
            if platform.system() == 'Windows' or platform.system() == 'Darwin':
                radiobutton2.set_sensitive(False)
            # (Signal connect appears below)

            radiobutton3 = self.add_radiobutton(grid,
                radiobutton2,
                _('Don\'t notify the user at the end of an operation'),
                0, 9, 1, 1,
            )
            if self.app_obj.operation_dialogue_mode == 'default':
                radiobutton3.set_active(True)
            # (Signal connect appears below)

            # (Signal connects from above)
            radiobutton.connect(
                'toggled',
                self.on_dialogue_button_toggled,
                'dialogue',
            )
            radiobutton2.connect(
                'toggled',
                self.on_dialogue_button_toggled,
                'desktop',
            )
            radiobutton3.connect(
                'toggled',
                self.on_dialogue_button_toggled,
                'default',
            )


    def setup_operations_clips_tab(self, inner_notebook):

        """Called by self.setup_operations_tab().

        Sets up the 'Clips' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Operations > Clips'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('Cli_ps'),
            inner_notebook,
        )
        grid_width = 2

        # Timestamps
        self.add_label(grid,
            '<u>' + _('Timestamps') + '</u>',
            0, 0, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _(
            'When a video is checked/downloaded, automatically extract' \
            + ' timestamps from its metadata file',
            ),
            self.app_obj.video_timestamps_extract_json_flag,
            True,                   # Can be toggled by user
            0, 1, grid_width, 1,
        )
        checkbutton.connect('toggled', self.on_extract_json_flag_toggled)

        checkbutton2 = self.add_checkbutton(grid,
            _(
            'When a video is checked/downloaded, automatically extract' \
            + ' timestamps from its description',
            ),
            self.app_obj.video_timestamps_extract_descrip_flag,
            True,                   # Can be toggled by user
            0, 2, grid_width, 1,
        )
        checkbutton2.connect('toggled', self.on_extract_descrip_flag_toggled)

        if not self.app_obj.simple_prefs_flag:

            checkbutton3 = self.add_checkbutton(grid,
                _('If timestamps have already been extracted, replace them'),
                self.app_obj.video_timestamps_replace_flag,
                True,                   # Can be toggled by user
                0, 3, grid_width, 1,
            )
            checkbutton3.connect(
                'toggled',
                self.on_replace_stamps_flag_toggled,
            )

            checkbutton4 = self.add_checkbutton(grid,
                _(
                'If no timestamps have been extracted, try again before' \
                + ' splitting a video',
                ),
                self.app_obj.video_timestamps_re_extract_flag,
                True,                   # Can be toggled by user
                0, 4, grid_width, 1,
            )
            checkbutton4.connect(
                'toggled',
                self.on_reextract_stamps_flag_toggled,
            )

        # Video clips (requires FFmpeg)
        self.add_label(grid,
            '<u>' + _('Video clips (requires FFmpeg or yt-dlp)') + '</u>',
            0, 5, grid_width, 1,
        )

        # (N.B. This setting can be more conveniently changed in
        #   mainwin.PrepareClipDialogue)
        if not self.app_obj.simple_prefs_flag:

            radiobutton = self.add_radiobutton(grid,
                None,
                _('Download video clips using FFmpeg'),
                0, 6, 1, 1,
            )
            # (Signal connect appears below)

            radiobutton2 = self.add_radiobutton(grid,
                radiobutton,
                _('Download video clips using yt-dlp'),
                1, 6, 1, 1,
            )
            if self.app_obj.video_timestamps_dl_mode == 'downloader':
                radiobutton2.set_active(True)

            # (Signal connects from above)
            radiobutton.connect(
                'toggled',
                self.on_clips_dl_mode_button_toggled,
            )

        self.add_label(grid,
            _('Format of video clip filenames'),
            0, 7, 1, 1,
        )

        combo_list = [
            _('Number'), 'num',
            _('Clip Title'), 'clip',
            _('Number + Clip Title'), 'num_clip',
            _('Clip Title + Number'), 'clip_num',
            _('Original Title'), 'orig',
            _('Original Title + Number'), 'orig_num',
            _('Original Title + Clip Title'), 'orig_clip',
            _('Original Title + Number + Clip Title'), 'orig_num_clip',
            _('Original Title + Clip Title + Number'), 'orig_clip_num',
        ]

        store = Gtk.ListStore(str, str)
        count = -1
        line_num = 0
        while combo_list:

            count += 1
            descrip = combo_list.pop(0)
            mode = combo_list.pop(0)
            if mode == self.app_obj.split_video_name_mode:
                line_num = count

            store.append([ descrip, mode])

        combo = Gtk.ComboBox.new_with_model(store)
        grid.attach(combo, 1, 7, 1, 1)
        combo.set_hexpand(True)

        renderer_text = Gtk.CellRendererText()
        combo.pack_start(renderer_text, True)
        combo.add_attribute(renderer_text, 'text', 0)
        combo.set_active(line_num)
        combo.connect('changed', self.on_split_mode_combo_changed)

        self.add_label(grid,
            _('Generic title for video clips'),
            0, 8, 1, 1,
        )

        entry = self.add_entry(grid,
            None,
            True,
            1, 8, 1, 1,
        )
        entry.set_text(self.app_obj.split_video_custom_title)
        entry.connect(
            'changed',
            self.on_custom_title_changed,
        )

        radiobutton3 = self.add_radiobutton(grid,
            None,
            _('Move clips to the Video Clips folder'),
            0, 9, 1, 1,
        )
        # (Signal connect appears below)

        radiobutton4 = self.add_radiobutton(grid,
            radiobutton3,
            _('Keep clips with their original video'),
            1, 9, 1, 1,
        )
        if not self.app_obj.split_video_clips_dir_flag:
            radiobutton4.set_active(True)

        # (Signal connects from above)
        radiobutton3.connect(
            'toggled',
            self.on_clips_dir_button_toggled,
        )

        checkbutton5 = self.add_checkbutton(grid,
            _('...but place new files inside a sub-directory'),
            self.app_obj.split_video_subdir_flag,
            True,                   # Can be toggled by user
            0, 10, grid_width, 1,
        )
        checkbutton5.connect(
            'toggled',
            self.on_split_subdir_flag_toggled,
        )

        checkbutton6 = self.add_checkbutton(grid,
            _('Add new files to Tartube\'s database'),
            self.app_obj.split_video_add_db_flag,
            True,                   # Can be toggled by user
            0, 11, 1, 1,
        )
        checkbutton6.connect(
            'toggled',
            self.on_add_db_flag_toggled,
        )

        checkbutton7 = self.add_checkbutton(grid,
            _('Use the original video\'s thumbnail'),
            self.app_obj.split_video_copy_thumb_flag,
            True,                   # Can be toggled by user
            1, 11, 1, 1,
        )
        checkbutton7.connect('toggled', self.on_copy_thumb_flag_toggled)

        if not self.app_obj.simple_prefs_flag:

            checkbutton8 = self.add_checkbutton(grid,
                _(
                    'Force keyframes at cuts (slower, but fewer video' \
                    + ' artefacts before and after each cut)',
                ),
                self.app_obj.split_video_force_keyframe_flag,
                True,                   # Can be toggled by user
                0, 12, grid_width, 1,
            )
            checkbutton8.connect(
                'toggled',
                self.on_split_keyframe_flag_toggled,
            )

        if os.name == 'nt':
            msg = _('After splitting a video, open the destination folder')
        else:
            msg = _('After splitting a video, open the destination directory')

        checkbutton9 = self.add_checkbutton(grid,
            msg,
            self.app_obj.split_video_auto_open_flag,
            True,                   # Can be toggled by user
            0, 13, grid_width, 1,
        )
        checkbutton9.connect('toggled', self.on_auto_open_flag_toggled)

        checkbutton10 = self.add_checkbutton(grid,
            _(
            'After splitting a video, delete the original (ignored for' \
            + ' videos in channels/playlists)',
            ),
            self.app_obj.split_video_auto_delete_flag,
            True,                   # Can be toggled by user
            0, 14, grid_width, 1,
        )
        checkbutton10.connect('toggled', self.on_auto_delete_flag_toggled)


    def setup_operations_slices_tab(self, inner_notebook):

        """Called by self.setup_operations_tab().

        Sets up the 'Slices' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Operations > Slices'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('Slic_es'),
            inner_notebook,
        )
        grid_width = 1

        # Video slices (requires FFmpeg)
        self.add_label(grid,
            '<u>' + _('Video slices (requires FFmpeg)') + '</u>',
            0, 0, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _(
            'While checking/downloading videos, check each video against' \
            + ' the SponsorBlock server',
            ),
            self.app_obj.sblock_fetch_flag,
            True,               # Can be toggled by user
            0, 1, grid_width, 1,
        )
        checkbutton.connect('toggled', self.on_sblock_fetch_button_toggled)

        checkbutton2 = self.add_checkbutton(grid,
            _(
            'When contacting the server, obfuscate each video\'s ID' \
            + ' (recommended)',
            ),
            self.app_obj.sblock_obfuscate_flag,
            True,               # Can be toggled by user
            0, 2, grid_width, 1,
        )
        checkbutton2.connect(
            'toggled',
            self.on_sblock_obfuscate_button_toggled,
        )

        if not self.app_obj.simple_prefs_flag:

            checkbutton3 = self.add_checkbutton(grid,
                _(
                'If slices have already been extracted, replace the old list',
                ),
                self.app_obj.sblock_replace_flag,
                True,               # Can be toggled by user
                0, 3, grid_width, 1,
            )
            checkbutton3.connect(
                'toggled',
                self.on_sblock_replace_button_toggled,
            )

            checkbutton4 = self.add_checkbutton(grid,
                _(
                'If slices have been extracted, contact the server again' \
                + ' before removing more slices from the video',
                ),
                self.app_obj.sblock_re_extract_flag,
                True,               # Can be toggled by user
                0, 4, grid_width, 1,
            )
            checkbutton4.connect(
                'toggled',
                self.on_sblock_re_extract_button_toggled,
            )

            checkbutton5 = self.add_checkbutton(grid,
                _(
                'Force keyframes at cuts (slower, but fewer video artefacts' \
                + ' before and after each cut)',
                ),
                self.app_obj.slice_video_force_keyframe_flag,
                True,               # Can be toggled by user
                0, 5, grid_width, 1,
            )
            checkbutton5.connect(
                'toggled',
                self.on_slice_keyframe_flag_toggled,
            )

            checkbutton6 = self.add_checkbutton(grid,
                _(
                'After removing slices from a video, reset all timestamp and' \
                + ' slice data (recommended)',
                ),
                self.app_obj.slice_video_cleanup_flag,
                True,               # Can be toggled by user
                0, 6, grid_width, 1,
            )
            checkbutton6.connect(
                'toggled',
                self.on_slice_cleanup_button_toggled,
            )


    def setup_operations_comments_tab(self, inner_notebook):

        """Called by self.setup_operations_tab().

        Sets up the 'Comments' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Operations > Comments'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('C_omments'),
            inner_notebook,
        )
        grid_width = 1

        # Video comments (yt-dlp only)
        self.add_label(grid,
            '<u>' + _('Video comments') + '</u>' + self.ytdlp_only(),
            0, 0, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('When checking videos, store comments in the metadata file'),
            self.app_obj.check_comment_fetch_flag,
            True,               # Can be toggled by user
            0, 1, grid_width, 1,
        )
        # (Signal connect appears below)

        checkbutton2 = self.add_checkbutton(grid,
            _('When downloading videos, store comments in the metadata file'),
            self.app_obj.dl_comment_fetch_flag,
            True,               # Can be toggled by user
            0, 2, grid_width, 1,
        )
        # (Signal connect appears below)

        self.add_label(grid,
            '<i>' + _('Warning: fetching comments will increase the download' \
            + ' time, perhaps by a lot!') + '</i>',
            0, 3, grid_width, 1,
        )

        checkbutton3 = self.add_checkbutton(grid,
            _('Also store comments in the Tartube database'),
            self.app_obj.comment_store_flag,
            True,               # Can be toggled by user
            0, 4, grid_width, 1,
        )
        # (Signal connect appears below)
        if not self.app_obj.check_comment_fetch_flag \
        and not self.app_obj.dl_comment_fetch_flag:
            checkbutton3.set_sensitive(False)

        # (Signal connects from above)
        checkbutton.connect(
            'toggled',
            self.on_check_comment_fetch_button_toggled,
            checkbutton3,
        )
        checkbutton2.connect(
            'toggled',
            self.on_dl_comment_fetch_button_toggled,
            checkbutton3,
        )
        checkbutton3.connect('toggled', self.on_comment_store_button_toggled)

        self.add_label(grid,
            '<i>' + _(
                'Warning: storing comments will increase the size of' \
                + ' Tartube\'s datbase, perhaps by a lot!',
            ) + '</i>',
            0, 5, grid_width, 1,
        )


    def setup_operations_mirrors_tab(self, inner_notebook):

        """Called by self.setup_scheduling_tab().

        Sets up the 'Mirrors' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Operations > Mirrors'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Mirrors'),
            inner_notebook,
        )
        grid_width = 2

        # Invidious mirror
        self.add_label(grid,
            '<u>' + _('Invidious mirror') + '</u>',
            0, 0, grid_width, 1,
        )

        self.add_label(grid,
            _(
                'To find an updated list of Invidious mirrors, use any' \
                + ' search engine!',
            ),
            0, 1, grid_width, 1,
        )

        entry = self.add_entry(grid,
            self.app_obj.custom_invidious_mirror,
            True,
            0, 2, 1, 1,
        )
        entry.connect('changed', self.on_invidious_mirror_changed)

        button = Gtk.Button(_('Reset'))
        grid.attach(button, 1, 2, 1, 1)
        button.set_tooltip_text(_('Use the default Invidious mirror'))
        button.set_hexpand(False)
        button.connect('clicked', self.on_reset_invidious_clicked, entry)

        msg = _('Type the exact text that replaces www.youtube.com e.g.')
        msg = re.sub('www.youtube.com', '   <b>www.youtube.com</b>   ', msg)

        self.add_label(grid,
            '<i>' + msg + '   <b>' + self.app_obj.default_invidious_mirror \
            + '</b></i>',
            0, 3, grid_width, 1,
        )

        # SponsorBlock API mirror
        self.add_label(grid,
            '<u>' + _('SponsorBlock API mirror') + '</u>',
            0, 4, grid_width, 1,
        )

        entry2 = self.add_entry(grid,
            self.app_obj.custom_sblock_mirror,
            True,
            0, 5, 1, 1,
        )
        entry2.connect('changed', self.on_sblock_mirror_changed)

        button2 = Gtk.Button(_('Reset'))
        grid.attach(button2, 1, 5, 1, 1)
        button2.set_tooltip_text(_('Use the default SponsorBlock URL'))
        button2.connect('clicked', self.on_reset_sblock_clicked, entry2)


    def setup_operations_proxies_tab(self, inner_notebook):

        """Called by self.setup_scheduling_tab().

        Sets up the 'Proxies' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Operations > Proxies'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('P_roxies'),
            inner_notebook,
        )

        # Proxies
        self.add_label(grid,
            '<u>' + _('Proxies') + '</u>',
            0, 0, 1, 1,
        )

        self.add_label(grid,
            '<i>' \
            + _(
            'During a download operation, Tartube will cycle betwween the' \
            + ' proxies in this list',
            ) + '</i>',
            0, 1, 1, 1,
        )

        textview, textbuffer = self.add_textview(grid,
            self.app_obj.dl_proxy_list,
            0, 2, 1, 1
        )
        textbuffer.connect('changed', self.on_proxy_textview_changed)


    def setup_operations_prefs_tab(self, inner_notebook):

        """Called by self.setup_operations_tab().

        Sets up the 'Preferences' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Operations > Preferences'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('Pre_ferences'),
            inner_notebook,
        )
        grid_width = 3

        # URL flexibility preferences
        self.add_label(grid,
            '<u>' + _('URL flexibility preferences') + '</u>',
            0, 0, grid_width, 1,
        )

        radiobutton = self.add_radiobutton(grid,
            None,
            _(
            'If a video\'s URL represents a channel/playlist, not a video,' \
            + ' don\'t download it',
            ),
            0, 1, grid_width, 1,
        )
        # (Signal connect appears below)

        radiobutton2 = self.add_radiobutton(grid,
            radiobutton,
            _('...or, download multiple videos into the containing folder'),
            0, 2, grid_width, 1,
        )
        if self.app_obj.operation_convert_mode == 'multi':
            radiobutton2.set_active(True)
        # (Signal connect appears below)

        radiobutton3 = self.add_radiobutton(grid,
            radiobutton2,
            _(
            '...or, create a new channel, and download the videos into that',
            ),
            0, 3, grid_width, 1,
        )
        if self.app_obj.operation_convert_mode == 'channel':
            radiobutton3.set_active(True)
        # (Signal connect appears below)

        radiobutton4 = self.add_radiobutton(grid,
            radiobutton3,
            _(
            '...or, create a new playlist, and download the videos into that',
            ),
            0, 4, grid_width, 1,
        )
        if self.app_obj.operation_convert_mode == 'playlist':
            radiobutton4.set_active(True)
        # (Signal connect appears below)

        # (Signal connects from above)
        radiobutton.connect(
            'toggled',
            self.on_convert_from_button_toggled,
            'disable',
        )
        radiobutton2.connect(
            'toggled',
            self.on_convert_from_button_toggled,
            'multi',
        )
        radiobutton3.connect(
            'toggled',
            self.on_convert_from_button_toggled,
            'channel',
        )
        radiobutton4.connect(
            'toggled',
            self.on_convert_from_button_toggled,
            'playlist',
        )

        # Missing video preferences
        self.add_label(grid,
            '<u>' + _('Missing video preferences') + '</u>',
            0, 5, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _(
            'Add videos which have been removed from a channel/playlist to' \
            + ' the Missing Videos folder',
            ),
            self.app_obj.track_missing_videos_flag,
            True,                   # Can be toggled by user
            0, 6, grid_width, 1,
        )
        # (Signal connect appears below)

        checkbutton2 = self.add_checkbutton(grid,
            _(
            'Only add videos that were uploaded within this many days',
            ),
            self.app_obj.track_missing_time_flag,
            True,                   # Can be toggled by user
            0, 7, 1, 1,
        )
        if not self.app_obj.track_missing_videos_flag:
            checkbutton2.set_sensitive(False)
        # (Signal connect appears below)

        spinbutton = self.add_spinbutton(grid,
            0,
            365,
            1,                  # Step
            self.app_obj.track_missing_time_days,
            1, 7, 2, 1,
        )
        spinbutton.set_hexpand(True)
        if not self.app_obj.track_missing_videos_flag \
        or not self.app_obj.track_missing_time_flag:
            spinbutton.set_sensitive(False)
        # (Signal connect appears below)

        # (Signal connects from above)
        checkbutton.connect(
            'toggled',
            self.on_missing_videos_button_toggled,
            checkbutton2,
            spinbutton,
        )
        checkbutton2.connect(
            'toggled',
            self.on_missing_time_button_toggled,
            spinbutton,
        )
        spinbutton.connect(
            'value-changed',
            self.on_missing_time_spinbutton_changed,
        )


    def setup_operations_missing_tab(self, inner_notebook):

        """Called by self.setup_operations_tab().

        Sets up the 'Missing' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Operations > Missing'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('Missi_ng'),
            inner_notebook,
        )
        grid_width = 3

        # Missing video preferences
        self.add_label(grid,
            '<u>' + _('Missing video preferences') + '</u>',
            0, 0, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _(
            'Add videos which have been removed from a channel/playlist to' \
            + ' the Missing Videos folder',
            ),
            self.app_obj.track_missing_videos_flag,
            True,                   # Can be toggled by user
            0, 1, grid_width, 1,
        )
        # (Signal connect appears below)

        checkbutton2 = self.add_checkbutton(grid,
            _(
            'Only add videos that were uploaded within this many days',
            ),
            self.app_obj.track_missing_time_flag,
            True,                   # Can be toggled by user
            0, 2, 1, 1,
        )
        if not self.app_obj.track_missing_videos_flag:
            checkbutton2.set_sensitive(False)
        # (Signal connect appears below)

        spinbutton = self.add_spinbutton(grid,
            0,
            365,
            1,                  # Step
            self.app_obj.track_missing_time_days,
            1, 2, 2, 1,
        )
        spinbutton.set_hexpand(True)
        if not self.app_obj.track_missing_videos_flag \
        or not self.app_obj.track_missing_time_flag:
            spinbutton.set_sensitive(False)
        # (Signal connect appears below)

        # (Signal connects from above)
        checkbutton.connect(
            'toggled',
            self.on_missing_videos_button_toggled,
            checkbutton2,
            spinbutton,
        )
        checkbutton2.connect(
            'toggled',
            self.on_missing_time_button_toggled,
            spinbutton,
        )
        spinbutton.connect(
            'value-changed',
            self.on_missing_time_spinbutton_changed,
        )


    def setup_downloader_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'youtube-dl' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Downloaders'
        )

        # Add this tab...
        tab, grid = self.add_notebook_tab('_Downloaders')

        # ...and an inner notebook...
        self.downloader_inner_notebook = self.add_inner_notebook(grid)

        # ...with its own tabs
        self.setup_downloader_forks_tab(self.downloader_inner_notebook)
        self.setup_downloader_paths_tab(self.downloader_inner_notebook)
        if not self.app_obj.simple_prefs_flag:
            self.setup_downloader_ffmpeg_tab(self.downloader_inner_notebook)
            self.setup_downloader_streamlink_tab(
                self.downloader_inner_notebook,
            )


    def setup_downloader_forks_tab(self, inner_notebook):

        """Called by self.setup_downloader_tab().

        Sets up the 'Forks' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Downloaders > Forks'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Forks'),
            inner_notebook,
        )

        # Forks of youtube-dl
        self.add_label(grid,
            '<u>' + _('Forks of youtube-dl') + '</u>',
            0, 0, 1, 1,
        )

        # yt-dlp. Use an event box so the downloader can be selected by
        #   clicking anywhere in the frame
        event_box = Gtk.EventBox()
        grid.attach(event_box, 0, 1, 1, 1)
        # (Signal connect appears below)

        frame = Gtk.Frame()
        event_box.add(frame)
        frame.set_border_width(self.spacing_size)

        grid2 = Gtk.Grid()
        frame.add(grid2)
        grid2.set_border_width(self.spacing_size)

        self.add_label(grid2,
            ttutils.tidy_up_long_string(
                '<b>yt-dlp</b>: <i>' \
                + self.app_obj.ytdl_fork_descrip_dict['yt-dlp'] \
                + '</i>',
            ),
            0, 0, 2, 1,
        )

        self.forks_radiobutton = self.add_radiobutton(grid2,
            None,
            '   ' + _('Use yt-dlp'),
            0, 1, 1, 1,
        )
        # (Signal connect appears below)

        checkbutton = self.add_checkbutton(grid2,
            _('Install without dependencies') + '\n' \
            + _('(recommended on MS Windows)'),
            self.app_obj.ytdl_fork_no_dependency_flag,
            True,                   # Can be toggled by user
            1, 1, 1, 1,
        )
        # (Signal connect appears below)

        # youtube-dl
        event_box2 = Gtk.EventBox()
        grid.attach(event_box2, 0, 2, 1, 1)
        # (Signal connect appears below)

        frame2 = Gtk.Frame()
        event_box2.add(frame2)
        frame2.set_border_width(self.spacing_size)

        grid3 = Gtk.Grid()
        frame2.add(grid3)
        grid3.set_border_width(self.spacing_size)

        self.add_label(grid3,
            ttutils.tidy_up_long_string(
                '<b>youtube-dl</b>: <i>' \
                + self.app_obj.ytdl_fork_descrip_dict['youtube-dl'] \
                + '</i>',
            ),
            0, 0, 1, 1,
        )

        self.forks_radiobutton2 = self.add_radiobutton(grid3,
            self.forks_radiobutton,
            '   ' + _('Use youtube-dl'),
            0, 1, 1, 1,
        )
        # (Signal connect appears below)

        # Any other fork
        event_box3 = Gtk.EventBox()
        grid.attach(event_box3, 0, 3, 1, 1)
        # (Signal connect appears below)

        frame3 = Gtk.Frame()
        event_box3.add(frame3)
        frame3.set_border_width(self.spacing_size)

        grid4 = Gtk.Grid()
        frame3.add(grid4)
        grid4.set_border_width(self.spacing_size)
        grid4.set_row_spacing(self.spacing_size)

        self.add_label(grid4,
            '<i>' + ttutils.tidy_up_long_string(
                '<b>' + _('Other forks') + ':</b> ' \
                + self.app_obj.ytdl_fork_descrip_dict['custom'],
            ) + '</i>',
            0, 0, 2, 1,
        )

        self.forks_radiobutton3 = self.add_radiobutton(grid4,
            self.forks_radiobutton2,
            '   ' + _('Use this fork (e.g. youtube-dlc):'),
            0, 1, 1, 1,
        )
        # (Signal connect appears below)
        self.forks_radiobutton3.set_hexpand(False)

        self.forks_entry = self.add_entry(grid4,
            None,
            True,
            1, 1, 1, 1,
        )
        self.forks_entry.set_sensitive(True)
        self.forks_entry.set_max_length(32)
        self.forks_entry.set_hexpand(False)
        self.forks_entry.set_icon_from_stock(
            Gtk.EntryIconPosition.PRIMARY,
            'gtk-yes',
        )
        # (Signal connect appears below)

        # Set widgets' initial states
        if self.app_obj.ytdl_fork is None \
        or self.app_obj.ytdl_fork == 'youtube-dl':
            self.forks_radiobutton2.set_active(True)
            checkbutton.set_sensitive(False)
            self.forks_entry.set_sensitive(False)
        elif self.app_obj.ytdl_fork == 'yt-dlp':
            self.forks_radiobutton.set_active(True)
            checkbutton.set_sensitive(True)
            self.forks_entry.set_sensitive(False)
        else:
            self.forks_radiobutton3.set_active(True)
            if self.app_obj.ytdl_fork is not None:
                self.forks_entry.set_text(self.app_obj.ytdl_fork)
            else:
                self.forks_entry.set_text('')
            checkbutton.set_sensitive(False)
            self.forks_entry.set_sensitive(True)

        # (Signal connects from above)
        event_box.connect(
            'button-press-event',
            self.on_ytdl_fork_frame_clicked,
            self.forks_radiobutton,
        )
        event_box2.connect(
            'button-press-event',
            self.on_ytdl_fork_frame_clicked,
            self.forks_radiobutton2,
        )
        event_box3.connect(
            'button-press-event',
            self.on_ytdl_fork_frame_clicked,
            self.forks_radiobutton3,
        )
        self.forks_radiobutton.connect(
            'toggled',
            self.on_ytdl_fork_button_toggled,
            checkbutton,
            'yt-dlp',
        )
        checkbutton.connect('toggled', self.on_ytdlp_install_button_toggled)
        self.forks_radiobutton2.connect(
            'toggled',
            self.on_ytdl_fork_button_toggled,
            checkbutton,
            'youtube-dl',
        )
        self.forks_radiobutton3.connect(
            'toggled',
            self.on_ytdl_fork_button_toggled,
            checkbutton,
        )
        self.forks_entry.connect('changed', self.on_ytdl_fork_changed)

        # Bottom section (always sensitised)
        checkbutton2 = self.add_checkbutton(grid,
            _(
                'When using other downloaders, filter out yt-dlp download' \
                + ' options',
            ),
            self.app_obj.ytdlp_filter_options_flag,
            True,                   # Can be toggled by user
            0, 4, 1, 1,
        )
        checkbutton2.connect('toggled', self.on_filter_options_button_toggled)


    def setup_downloader_paths_tab(self, inner_notebook):

        """Called by self.setup_downloader_tab().

        Sets up the 'File Paths' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Downloaders > File paths'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('File _paths'),
            inner_notebook,
        )
        grid_width = 3

        # Downloader file paths
        self.add_label(grid,
            '<u>' + _('Downloader file paths') + '</u>',
            0, 0, grid_width, 1,
        )

        # youtube-dl file paths
        self.add_label(grid,
            _('Path to the executable'),
            0, 1, 1, 1,
        )

        combo_list = [
            [
                _('Use default path') + ' (' + self.app_obj.ytdl_path_default \
                + ')',
                self.app_obj.ytdl_path_default,
            ],
        ]

        if os.name != 'nt':

            combo_list.append(
                [
                    _('Use local path') + ' (' + self.app_obj.ytdl_bin + ')',
                    self.app_obj.ytdl_bin,
                ],
            )

        if os.name == 'nt':
            msg = _('Use custom path (not recommended on MS Windows)')
        else:
            msg = _('Use custom path')
        combo_list.append(
            [
                msg,
                None,       # Set by the callback
            ],
        )

        if os.name != 'nt':

            combo_list.append(
                [
                    _('Use PyPI path') + ' (' + self.app_obj.ytdl_path_pypi \
                    + ')',
                    self.app_obj.ytdl_path_pypi,
                ],
            )

        self.path_liststore = Gtk.ListStore(str, str)
        for mini_list in combo_list:
            self.path_liststore.append( [ mini_list[0], mini_list[1] ] )

        self.filepaths_combo = Gtk.ComboBox.new_with_model(self.path_liststore)
        grid.attach(self.filepaths_combo, 1, 1, (grid_width - 1), 1)
        renderer_text = Gtk.CellRendererText()
        self.filepaths_combo.pack_start(renderer_text, True)
        self.filepaths_combo.add_attribute(renderer_text, 'text', 0)
        self.filepaths_combo.set_entry_text_column(0)
        # (Signal connect appears below)

        entry = self.add_entry(grid,
            None,
            False,
            1, 2, 1, 1,
        )

        button = Gtk.Button(_('Set'))
        grid.attach(button, 2, 2, 1, 1)
        # (Signal connect appears below)

        # Set up those widgets
        if os.name == 'nt':

            if self.app_obj.ytdl_path_custom_flag:
                self.filepaths_combo.set_active(1)
            else:
                self.filepaths_combo.set_active(0)

        else:

            if self.app_obj.ytdl_path_custom_flag:
                self.filepaths_combo.set_active(2)
            elif self.app_obj.ytdl_path == self.app_obj.ytdl_path_default:
                self.filepaths_combo.set_active(0)
            elif self.app_obj.ytdl_path == self.app_obj.ytdl_path_pypi:
                self.filepaths_combo.set_active(3)
            else:
                self.filepaths_combo.set_active(1)

        if self.app_obj.ytdl_path_custom_flag:

            # (If this window is loaded due to
            #   mainapp.TartubeApp.debug_open_pref_win_flag, this value will be
            #   None)
            if self.app_obj.ytdl_path:
                entry.set_text(self.app_obj.ytdl_path)

        else:
            button.set_sensitive(False)

        # Now set up the next combo
        if not __main__.__pkg_strict_install_flag__:

            self.add_label(grid,
                _('Command for update operations'),
                0, 3, 1, 1,
            )

            self.cmd_liststore = Gtk.ListStore(str, str)
            for item in self.app_obj.ytdl_update_list:
                self.cmd_liststore.append(
                    [item, formats.YTDL_UPDATE_DICT[item]]
                )

            combo2 = Gtk.ComboBox.new_with_model(self.cmd_liststore)
            grid.attach(combo2, 1, 3, (grid_width - 1), 1)

            renderer_text = Gtk.CellRendererText()
            combo2.pack_start(renderer_text, True)
            combo2.add_attribute(renderer_text, 'text', 1)
            combo2.set_entry_text_column(1)

            combo2.set_active(
                self.app_obj.ytdl_update_list.index(
                    self.app_obj.ytdl_update_current,
                ),
            )
            if __main__.__pkg_strict_install_flag__:
                combo2.set_sensitive(False)
            # (Signal connect appears below)

            # Update the combos, so that the youtube-dl fork, rather than
            #   youtube-dl itself, is visible (if applicable)
            self.update_ytdl_combos()

        # (Signal connects from above)
        self.filepaths_combo.connect(
            'changed',
            self.on_ytdl_path_combo_changed,
            entry,
            button,
        )
        button.connect('clicked', self.on_ytdl_path_button_clicked, entry)

        if not __main__.__pkg_strict_install_flag__:
            combo2.connect('changed', self.on_update_combo_changed)


    def setup_downloader_ffmpeg_tab(self, inner_notebook):

        """Called by self.setup_downloader_tab().

        Sets up the 'FFmpeg / AVConv' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Downloaders' \
            + ' > FFmpeg / AVConv'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('FF_mpeg / AVConv'),
            inner_notebook,
        )
        grid_width = 4

        # Post-processor file paths
        self.add_label(grid,
            '<u>' + _('Post-processor file paths') + '</u>',
            0, 0, grid_width, 1,
        )
        self.add_label(grid,
            '<i>' + _(
            'You only need to set these paths if Tartube cannot find' \
            + ' FFmpeg / AVConv automatically'
            ) + '</i>',
            0, 1, grid_width, 1,
        )

        self.add_label(grid,
            _('Path to the FFmpeg executable'),
            0, 2, 1, 1,
        )

        button = Gtk.Button(_('Set'))
        grid.attach(button, 1, 2, 1, 1)
        # (Signal connect appears below)

        button2 = Gtk.Button(_('Reset'))
        grid.attach(button2, 2, 2, 1, 1)
        # (Signal connect appears below)

        button3 = Gtk.Button(_('Use default path'))
        grid.attach(button3, 3, 2, 1, 1)
        # (Signal connect appears below)

        entry = self.add_entry(grid,
            self.app_obj.ffmpeg_path,
            False,
            0, 3, grid_width, 1,
        )
        entry.set_sensitive(False)
        entry.set_editable(False)
        entry.set_hexpand(True)

        if os.name == 'nt':
            entry.set_sensitive(False)
            button.set_sensitive(False)

        # (Signal connects from above)
        button.connect('clicked', self.on_set_ffmpeg_button_clicked, entry)
        button2.connect('clicked', self.on_reset_ffmpeg_button_clicked, entry)
        button3.connect(
            'clicked',
            self.on_default_ffmpeg_button_clicked, entry,
        )

        self.add_label(grid,
            _('Path to the AVConv executable'),
            0, 4, 1, 1,
        )

        button4 = Gtk.Button(_('Set'))
        grid.attach(button4, 1, 4, 1, 1)
        # (Signal connect appears below)

        button5 = Gtk.Button(_('Reset'))
        grid.attach(button5, 2, 4, 1, 1)
        # (Signal connect appears below)

        button6 = Gtk.Button(_('Use default path'))
        grid.attach(button6, 3, 4, 1, 1)
        # (Signal connect appears below)

        entry2 = self.add_entry(grid,
            self.app_obj.ffmpeg_path,
            False,
            0, 5, grid_width, 1,
        )
        entry2.set_sensitive(False)
        entry2.set_editable(False)
        entry2.set_hexpand(True)

        if os.name == 'nt':
            entry2.set_sensitive(False)
            entry2.set_text(_('Not supported on MS Windows'))
            button4.set_sensitive(False)
            button5.set_sensitive(False)
            button6.set_sensitive(False)

        # (Signal connects from above)
        button4.connect('clicked', self.on_set_avconv_button_clicked, entry2)
        button5.connect('clicked', self.on_reset_avconv_button_clicked, entry2)
        button6.connect(
            'clicked',
            self.on_default_avconv_button_clicked,
            entry2,
        )


    def setup_downloader_streamlink_tab(self, inner_notebook):

        """Called by self.setup_downloader_tab().

        Sets up the 'streamlink' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Downloaders > streamlink'
        )
        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'streamlink\' is the name of a Python' \
            + ' module'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_streamlink'),
            inner_notebook,
        )
        grid_width = 3

        # streamlink file path
        self.add_label(grid,
            '<u>' + _('streamlink file path') + '</u>',
            0, 0, grid_width, 1,
        )

        self.add_label(grid,
            _('Path to the streamlink executable'),
            0, 1, 1, 1,
        )

        button = Gtk.Button(_('Set'))
        grid.attach(button, 1, 1, 1, 1)
        # (Signal connect appears below)

        button2 = Gtk.Button(_('Reset'))
        grid.attach(button2, 2, 1, 1, 1)
        # (Signal connect appears below)

        entry = self.add_entry(grid,
            self.app_obj.streamlink_path,
            False,
            0, 2, grid_width, 1,
        )
        entry.set_sensitive(False)
        entry.set_editable(False)
        entry.set_hexpand(True)

        if os.name == 'nt':
            entry.set_sensitive(False)
            entry.set_text(_('Install from main menu'))
            button.set_sensitive(False)
            button2.set_sensitive(False)

        # (Signal connects from above)
        button.connect(
            'clicked',
            self.on_set_streamlink_button_clicked,
            entry,
        )
        button2.connect(
            'clicked',
            self.on_reset_streamlink_button_clicked,
            entry,
        )


    def setup_options_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Options' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Options'
        )

        # Add this tab...
        tab, grid = self.add_notebook_tab('O_ptions')

        # ...and an inner notebook...
        inner_notebook = self.add_inner_notebook(grid)

        # ...with its own tabs
        self.setup_options_dl_list_tab(inner_notebook)
        if not self.app_obj.simple_prefs_flag:
            self.setup_options_dl_prefs_tab(inner_notebook)
        self.setup_options_ffmpeg_list_tab(inner_notebook)


    def setup_options_dl_list_tab(self, inner_notebook):

        """Called by self.setup_options_tab().

        Sets up the 'Download options' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Options' \
            + ' > Download options'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Download options'),
            inner_notebook,
        )
        grid_width = 4

        # List of download options
        self.add_label(grid,
            '<u>' + _('List of download options') + '</u>',
            0, 0, grid_width, 1,
        )

        # (GenericConfigWin.add_treeview() doesn't support multiple columns, so
        #   we'll do everything ourselves)
        frame = Gtk.Frame()
        grid.attach(frame, 0, 1, grid_width, 1)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)

        treeview = Gtk.TreeView()
        scrolled.add(treeview)
        treeview.set_headers_visible(True)

        for i, column_title in enumerate(
            [
                '#', _('Name'), _('Videos tab'), _('Classic Mode'),
                _('Dropzone'), _('Applied to media'),
            ]
        ):
            if i >= 2 and i <= 4:
                renderer_toggle = Gtk.CellRendererToggle()
                column_toggle = Gtk.TreeViewColumn(
                    column_title,
                    renderer_toggle,
                    active=i,
                )
                treeview.append_column(column_toggle)
                column_toggle.set_resizable(False)
            else:
                renderer_text = Gtk.CellRendererText()
                column_text = Gtk.TreeViewColumn(
                    column_title,
                    renderer_text,
                    text=i,
                )
                treeview.append_column(column_text)
                column_text.set_resizable(True)

        self.options_liststore = Gtk.ListStore(int, str, bool, bool, bool, str)
        treeview.set_model(self.options_liststore)

        # Initialise the list
        self.setup_options_dl_list_tab_update_treeview()

        # Add editing buttons
        self.add_label(grid,
            'Manager name',
            0, 2, 1, 1,
        )

        entry = self.add_entry(grid,
            None,
            True,
            1, 2, 1, 1,
        )

        button = Gtk.Button()
        grid.attach(button, 2, 2, 1, 1)
        button.set_label(_('Add'))
        button.connect(
            'clicked',
            self.on_options_add_button_clicked,
            entry,
        )

        button2 = Gtk.Button()
        grid.attach(button2, 3, 2, 1, 1)
        button2.set_label(_('Import'))
        button2.connect(
            'clicked',
            self.on_options_import_button_clicked,
            entry,
        )

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 3, grid_width, 1)

        button3 = Gtk.Button()
        grid2.attach(button3, 0, 0, 1, 1)
        button3.set_label(_('Edit'))
        button3.connect(
            'clicked',
            self.on_options_edit_button_clicked,
            treeview,
        )

        button4 = Gtk.Button()
        grid2.attach(button4, 1, 0, 1, 1)
        button4.set_label(_('Export'))
        button4.connect(
            'clicked',
            self.on_options_export_button_clicked,
            treeview,
        )

        button5 = Gtk.Button()
        grid2.attach(button5, 2, 0, 1, 1)
        button5.set_label(_('Clone'))
        button5.connect(
            'clicked',
            self.on_options_clone_button_clicked,
            treeview,
        )

        button6 = Gtk.Button()
        grid2.attach(button6, 3, 0, 1, 1)
        button6.set_label(_('Use in Classic Mode tab'))
        button6.connect(
            'clicked',
            self.on_options_use_classic_button_clicked,
            treeview,
        )

        button7 = Gtk.Button()
        grid2.attach(button7, 4, 0, 1, 1)
        button7.set_label(_('Delete'))
        button7.connect(
            'clicked',
            self.on_options_delete_button_clicked,
            treeview,
        )

        # (Use an empty label for spacing)
        label = self.add_label(grid2,
            '',
            5, 0, 1, 1,
        )
        label.set_hexpand(True)

        button8 = Gtk.Button()
        grid2.attach(button8, 6, 0, 1, 1)
        button8.set_label(_('Refresh list'))
        button8.connect(
            'clicked',
            self.setup_options_dl_list_tab_update_treeview,
        )


    def setup_options_dl_list_tab_update_treeview(self, button=None):

        """Can be called by anything.

        Fills or updates the treeview.
        """

        self.options_liststore.clear()

        for uid in sorted(self.app_obj.options_reg_dict):
            self.setup_options_dl_list_tab_add_row(
                self.app_obj.options_reg_dict[uid],
            )


    def setup_options_dl_list_tab_add_row(self, options_obj):

        """Can be called by anything.

        Adds a row to the treeview.

        Args:

            options_obj (options.OptionsManager) - The options manager object
                to display on this row

        """

        row_list = []

        row_list.append(options_obj.uid)
        row_list.append(
            ttutils.tidy_up_long_string(
                options_obj.name,
                self.app_obj.main_win_obj.short_string_max_len,
            ),
        )

        if self.app_obj.general_options_obj \
        and self.app_obj.general_options_obj == options_obj:
            row_list.append(True)
        else:
            row_list.append(False)

        if self.app_obj.classic_options_obj \
        and self.app_obj.classic_options_obj == options_obj:
            row_list.append(True)
        else:
            row_list.append(False)

        if options_obj.uid in self.app_obj.classic_dropzone_list:
            row_list.append(True)
        else:
            row_list.append(False)

        if not options_obj.dbid_list:
            row_list.append('')
        else:
            row_list.append(self.get_options_applied_text(options_obj))

        self.options_liststore.append(row_list)


    def setup_options_dl_prefs_tab(self, inner_notebook):

        """Called by self.setup_downloader_tab().

        Sets up the 'Preferences' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Options > Preferences'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Preferences'),
            inner_notebook,
        )
        grid_width = 2

        # Download options preferences
        self.add_label(grid,
            '<u>' + _('Download options preferences') + '</u>',
            0, 0, 1, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _(
            'When applying download options to something, clone the general' \
            + ' download options',
            ),
            self.app_obj.auto_clone_options_flag,
            True,                   # Can be toggled by user
            0, 1, 1, 1,
        )
        checkbutton.connect('toggled', self.on_auto_clone_button_toggled)

        checkbutton2 = self.add_checkbutton(grid,
            _(
            'After downloading a video, destroy its download options',
            ),
            self.app_obj.auto_delete_options_flag,
            True,                   # Can be toggled by user
            0, 2, 1, 1,
        )
        checkbutton2.connect('toggled', self.on_auto_delete_button_toggled)


    def setup_options_ffmpeg_list_tab(self, inner_notebook):

        """Called by self.setup_options_tab().

        Sets up the 'FFmpeg options' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Options > FFmpeg options'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_FFmpeg options'),
            inner_notebook,
        )
        grid_width = 4

        # List of FFmpeg options managers
        self.add_label(grid,
            '<u>' + _('List of FFmpeg options managers') + '</u>',
            0, 0, grid_width, 1,
        )

        # (GenericConfigWin.add_treeview() doesn't support multiple columns, so
        #   we'll do everything ourselves)
        frame = Gtk.Frame()
        grid.attach(frame, 0, 1, grid_width, 1)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_vexpand(True)

        treeview = Gtk.TreeView()
        scrolled.add(treeview)
        treeview.set_headers_visible(True)

        # (Fourth column is empty, to keep the 3rd column at a minimum width)
        for i, column_title in enumerate(
            [ '#', _('Name'), _('Current'), '' ]
        ):
            if i == 2:
                renderer_toggle = Gtk.CellRendererToggle()
                column_toggle = Gtk.TreeViewColumn(
                    column_title,
                    renderer_toggle,
                    active=i,
                )
                treeview.append_column(column_toggle)
                column_toggle.set_resizable(False)
            else:
                renderer_text = Gtk.CellRendererText()
                column_text = Gtk.TreeViewColumn(
                    column_title,
                    renderer_text,
                    text=i,
                )
                treeview.append_column(column_text)
                column_text.set_resizable(True)

        self.ffmpeg_liststore = Gtk.ListStore(int, str, bool, str)
        treeview.set_model(self.ffmpeg_liststore)

        # Initialise the list
        self.setup_options_ffmpeg_list_tab_update_treeview()

        # Add editing buttons
        self.add_label(grid,
            'Manager name',
            0, 2, 1, 1,
        )

        entry = self.add_entry(grid,
            None,
            True,
            1, 2, 1, 1,
        )

        button = Gtk.Button()
        grid.attach(button, 2, 2, 1, 1)
        button.set_label(_('Add'))
        button.connect(
            'clicked',
            self.on_ffmpeg_add_button_clicked,
            entry,
        )

        button2 = Gtk.Button()
        grid.attach(button2, 3, 2, 1, 1)
        button2.set_label(_('Import'))
        button2.connect(
            'clicked',
            self.on_ffmpeg_import_button_clicked,
            entry,
        )

        # (To avoid messing up the neat format of the rows above, add a
        #   secondary grid, and put the next set of widgets inside it)
        grid2 = self.add_secondary_grid(grid, 0, 3, grid_width, 1)

        button3 = Gtk.Button()
        grid2.attach(button3, 0, 0, 1, 1)
        button3.set_label(_('Edit'))
        button3.connect(
            'clicked',
            self.on_ffmpeg_edit_button_clicked,
            treeview,
        )

        button4 = Gtk.Button()
        grid2.attach(button4, 1, 0, 1, 1)
        button4.set_label(_('Export'))
        button4.connect(
            'clicked',
            self.on_ffmpeg_export_button_clicked,
            treeview,
        )

        button5 = Gtk.Button()
        grid2.attach(button5, 2, 0, 1, 1)
        button5.set_label(_('Clone'))
        button5.connect(
            'clicked',
            self.on_ffmpeg_clone_button_clicked,
            treeview,
        )

        button6 = Gtk.Button()
        grid2.attach(button6, 3, 0, 1, 1)
        button6.set_label(_('Use these options'))
        button6.connect(
            'clicked',
            self.on_ffmpeg_use_button_clicked,
            treeview,
        )

        button7 = Gtk.Button()
        grid2.attach(button7, 4, 0, 1, 1)
        button7.set_label(_('Delete'))
        button7.connect(
            'clicked',
            self.on_ffmpeg_delete_button_clicked,
            treeview,
        )

        # (Use an empty label for spacing)
        label = self.add_label(grid2,
            '',
            5, 0, 1, 1,
        )
        label.set_hexpand(True)

        button8 = Gtk.Button()
        grid2.attach(button8, 6, 0, 1, 1)
        button8.set_label(_('Refresh list'))
        button8.connect(
            'clicked',
            self.setup_options_ffmpeg_list_tab_update_treeview,
        )


    def setup_options_ffmpeg_list_tab_update_treeview(self):

        """Can be called by anything.

        Fills or updates the treeview.
        """

        self.ffmpeg_liststore.clear()

        for uid in sorted(self.app_obj.ffmpeg_reg_dict):
            self.setup_options_ffmpeg_list_tab_add_row(
                self.app_obj.ffmpeg_reg_dict[uid],
            )


    def setup_options_ffmpeg_list_tab_add_row(self, options_obj):

        """Can be called by anything.

        Adds a row to the treeview.

        Args:

            options_obj (ffmpeg_tartube.FFmpegOptionsManager): The FFmpeg
                options manager object to display on this row

        """

        row_list = []

        row_list.append(options_obj.uid)
        row_list.append(
            ttutils.tidy_up_long_string(
                options_obj.name,
                self.app_obj.main_win_obj.short_string_max_len,
            ),
        )

        if self.app_obj.ffmpeg_options_obj \
        and self.app_obj.ffmpeg_options_obj == options_obj:
            row_list.append(True)
        else:
            row_list.append(False)

        # (Fourth column is empty, to keep the 3rd column at a minimum width)
        row_list.append('')

        self.ffmpeg_liststore.append(row_list)


    def setup_output_tab(self):

        """Called by self.setup_tabs().

        Sets up the 'Output' tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Output'
        )

        # Add this tab...
        tab, grid = self.add_notebook_tab(_('O_utput'), 0)

        # ...and an inner notebook...
        inner_notebook = self.add_inner_notebook(grid)

        # ...with its own tabs
        self.setup_output_general_tab(inner_notebook)
        self.setup_output_outputtab_tab(inner_notebook)
        if not self.app_obj.simple_prefs_flag:
            self.setup_output_terminal_tab(inner_notebook)
            self.setup_output_log_tab(inner_notebook)


    def setup_output_general_tab(self, inner_notebook):

        """Called by self.setup_output_tab().

        Sets up the 'General' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Output > General'
        )

        tab, grid = self.add_inner_notebook_tab(_('_General'), inner_notebook)

        # General preferences (applies to both the Output tab, terminal window
        #   and downloader log)
        self.add_label(grid,
            '<u>' + _('General preferences') + '</u>',
            0, 0, 1, 1,
        )

        self.add_label(grid,
            '<i>' + _(
                'Applies to Output tab, terminal window and downloader log',
            ) + '</i>',
            0, 1, 1, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Downloader writes verbose output (youtube-dl debugging mode)'),
            self.app_obj.ytdl_write_verbose_flag,
            True,               # Can be toggled by user
            0, 2, 1, 1,
        )
        checkbutton.set_hexpand(False)
        checkbutton.connect('toggled', self.on_ytdl_verbose_button_toggled)


    def setup_output_outputtab_tab(self, inner_notebook):

        """Called by self.setup_output_tab().

        Sets up the 'Output tab' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Output > Output tab'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Output tab'),
            inner_notebook,
        )
        grid_width = 2

        # Output tab preferences
        self.add_label(grid,
            '<u>' + _('Output tab preferences') + '</u>',
            0, 0, grid_width, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Display downloader system commands in the Output tab'),
            self.app_obj.ytdl_output_system_cmd_flag,
            True,               # Can be toggled by user
            0, 1, grid_width, 1,
        )
        checkbutton.set_hexpand(False)
        checkbutton.connect('toggled', self.on_output_system_button_toggled)

        checkbutton2 = self.add_checkbutton(grid,
            _(
            'Display general output (from downloader\'s STDOUT) in the' \
            + ' Output tab',
            ),
            self.app_obj.ytdl_output_stdout_flag,
            True,               # Can be toggled by user
            0, 2, grid_width, 1,
        )
        checkbutton2.set_hexpand(False)
        # (Signal connect appears below)

        checkbutton3 = self.add_checkbutton(grid,
            _('...but don\'t write each video\'s JSON data'),
            self.app_obj.ytdl_output_ignore_json_flag,
            True,               # Can be toggled by user
            0, 3, grid_width, 1,
        )
        checkbutton3.set_hexpand(False)
        checkbutton3.connect('toggled', self.on_output_json_button_toggled)
        if not self.app_obj.ytdl_output_stdout_flag:
            checkbutton3.set_sensitive(False)

        checkbutton4 = self.add_checkbutton(grid,
            _('...but don\'t write each video\'s download progress'),
            self.app_obj.ytdl_output_ignore_progress_flag,
            True,               # Can be toggled by user
            0, 4, grid_width, 1,
        )
        checkbutton4.set_hexpand(False)
        checkbutton4.connect('toggled', self.on_output_progress_button_toggled)
        if not self.app_obj.ytdl_output_stdout_flag:
            checkbutton4.set_sensitive(False)

        # (Signal connect from above)
        checkbutton2.connect(
            'toggled',
            self.on_output_stdout_button_toggled,
            checkbutton3,
            checkbutton4,
        )

        checkbutton5 = self.add_checkbutton(grid,
            _(
            'Display errors/warnings (from downloader\'s STDERR) in the' \
            + ' Output tab',
            ),
            self.app_obj.ytdl_output_stderr_flag,
            True,               # Can be toggled by user
            0, 5, grid_width, 1,
        )
        checkbutton5.set_hexpand(False)
        checkbutton5.connect('toggled', self.on_output_stderr_button_toggled)

        checkbutton6 = self.add_checkbutton(grid,
            _('Limit the size of Output tab pages to'),
            self.app_obj.output_size_apply_flag,
            True,               # Can be toggled by user
            0, 6, 1, 1,
        )
        checkbutton6.set_hexpand(False)
        checkbutton6.connect('toggled', self.on_output_size_button_toggled)

        spinbutton = self.add_spinbutton(grid,
            self.app_obj.output_size_min,
            self.app_obj.output_size_max,
            1,                  # Step
            self.app_obj.output_size_default,
            1, 6, 1, 1,
        )
        spinbutton.connect(
            'value-changed',
            self.on_output_size_spinbutton_changed,
        )

        if not self.app_obj.simple_prefs_flag:

            checkbutton7 = self.add_checkbutton(grid,
                _(
                    'Empty pages in the Output tab at the start of every' \
                    + ' operation',
                ),
                self.app_obj.ytdl_output_start_empty_flag,
                True,               # Can be toggled by user
                0, 7, grid_width, 1,
            )
            checkbutton7.set_hexpand(False)
            checkbutton7.connect(
                'toggled',
                self.on_output_empty_button_toggled,
            )

            checkbutton8 = self.add_checkbutton(grid,
                _(
                'Show a summary of active threads (changes are applied when' \
                + ' Tartube restarts)',
                ),
                self.app_obj.ytdl_output_show_summary_flag,
                True,               # Can be toggled by user
                0, 8, grid_width, 1,
            )
            checkbutton8.set_hexpand(False)
            checkbutton8.connect(
                'toggled',
                self.on_output_summary_button_toggled,
            )

            checkbutton9 = self.add_checkbutton(grid,
                _(
                'During update/info operations, automatically switch to the' \
                + ' Output tab',
                ),
                self.app_obj.auto_switch_output_flag,
                True,                   # Can be toggled by user
                0, 9, grid_width, 1,
            )
            checkbutton9.connect('toggled', self.on_auto_switch_button_toggled)

            checkbutton10 = self.add_checkbutton(grid,
                _(
                'During a refresh operation, show all matching videos in the' \
                + ' Output tab',
                ),
                self.app_obj.refresh_output_videos_flag,
                True,               # Can be toggled by user
                0, 10, grid_width, 1,
            )
            checkbutton10.set_hexpand(False)
            # (Signal connect appears below)

            checkbutton11 = self.add_checkbutton(grid,
                _('...also show all non-matching videos'),
                self.app_obj.refresh_output_verbose_flag,
                True,               # Can be toggled by user
                0, 11, grid_width, 1,
            )
            checkbutton11.set_hexpand(False)
            checkbutton11.connect(
                'toggled',
                self.on_refresh_verbose_button_toggled,
            )
            if not self.app_obj.refresh_output_videos_flag:
                checkbutton10.set_sensitive(False)

            # (Signal connect from above)
            checkbutton10.connect(
                'toggled',
                self.on_refresh_videos_button_toggled,
                checkbutton11,
            )


    def setup_output_terminal_tab(self, inner_notebook):

        """Called by self.setup_output_tab().

        Sets up the 'Terminal window' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Output > Terminal window'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Terminal window'),
            inner_notebook,
        )

        # Terminal window preferences
        self.add_label(grid,
            '<u>' + _('Terminal window preferences') + '</u>',
            0, 0, 1, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Write downloader system commands to the terminal window'),
            self.app_obj.ytdl_write_system_cmd_flag,
            True,               # Can be toggled by user
            0, 1, 1, 1,
        )
        checkbutton.set_hexpand(False)
        checkbutton.connect('toggled', self.on_terminal_system_button_toggled)

        checkbutton2 = self.add_checkbutton(grid,
            _(
            'Write general output (from downloader\'s STDOUT) to the' \
            + ' terminal window',
            ),
            self.app_obj.ytdl_write_stdout_flag,
            True,               # Can be toggled by user
            0, 2, 1, 1,
        )
        checkbutton2.set_hexpand(False)
        # (Signal connect appears below)

        checkbutton3 = self.add_checkbutton(grid,
            _('...but don\'t write each video\'s JSON data'),
            self.app_obj.ytdl_write_ignore_json_flag,
            True,               # Can be toggled by user
            0, 3, 1, 1,
        )
        checkbutton3.set_hexpand(False)
        checkbutton3.connect('toggled', self.on_terminal_json_button_toggled)
        if not self.app_obj.ytdl_write_stdout_flag:
            checkbutton3.set_sensitive(False)

        checkbutton4 = self.add_checkbutton(grid,
            _('...but don\'t write each video\'s download progress'),
            self.app_obj.ytdl_write_ignore_progress_flag,
            True,               # Can be toggled by user
            0, 4, 1, 1,
        )
        checkbutton4.set_hexpand(False)
        checkbutton4.connect(
            'toggled',
            self.on_terminal_progress_button_toggled,
        )
        if not self.app_obj.ytdl_write_stdout_flag:
            checkbutton4.set_sensitive(False)

        # (Signal connect from above)
        checkbutton2.connect(
            'toggled',
            self.on_terminal_stdout_button_toggled,
            checkbutton3,
            checkbutton4,
        )

        checkbutton5 = self.add_checkbutton(grid,
            _(
            'Write errors/warnings (from downloader\'s STDERR) to the' \
            + ' terminal window',
            ),
            self.app_obj.ytdl_write_stderr_flag,
            True,               # Can be toggled by user
            0, 5, 1, 1,
        )
        checkbutton5.set_hexpand(False)
        checkbutton5.connect(
            'toggled',
            self.on_terminal_stderr_button_toggled,
        )


    def setup_output_log_tab(self, inner_notebook):

        """Called by self.setup_output_tab().

        Sets up the 'Log' inner notebook tab.

        Args:

            inner_notebook (Gtk.Notebook): The container for this tab

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Output > Log'
        )

        tab, grid = self.add_inner_notebook_tab(
            _('_Downloader log'),
            inner_notebook,
        )

        # Downloader log preferences
        self.add_label(grid,
            '<u>' + _('Downloader log preferences') + '</u>',
            0, 0, 1, 1,
        )

        self.add_label(grid,
            '<i>' + _(
                'If enabled, the file \'{0}\' is written to Tartube\'s' \
                + ' data folder',
            ).format(self.app_obj.ytdl_log_name) + '</i>',
            0, 1, 1, 1,
        )

        checkbutton = self.add_checkbutton(grid,
            _('Write downloader system commands to the log'),
            self.app_obj.ytdl_log_system_cmd_flag,
            True,               # Can be toggled by user
            0, 2, 1, 1,
        )
        checkbutton.set_hexpand(False)
        checkbutton.connect('toggled', self.on_log_system_button_toggled)

        checkbutton2 = self.add_checkbutton(grid,
            _('Write general output (from downloader\'s STDOUT) to the log'),
            self.app_obj.ytdl_log_stdout_flag,
            True,               # Can be toggled by user
            0, 3, 1, 1,
        )
        checkbutton2.set_hexpand(False)
        # (Signal connect appears below)

        checkbutton3 = self.add_checkbutton(grid,
            _('...but don\'t write each video\'s JSON data'),
            self.app_obj.ytdl_log_ignore_json_flag,
            True,               # Can be toggled by user
            0, 4, 1, 1,
        )
        checkbutton3.set_hexpand(False)
        checkbutton3.connect('toggled', self.on_log_json_button_toggled)
        if not self.app_obj.ytdl_log_stdout_flag:
            checkbutton3.set_sensitive(False)

        checkbutton4 = self.add_checkbutton(grid,
            _('...but don\'t write each video\'s download progress'),
            self.app_obj.ytdl_log_ignore_progress_flag,
            True,               # Can be toggled by user
            0, 5, 1, 1,
        )
        checkbutton4.set_hexpand(False)
        checkbutton4.connect(
            'toggled',
            self.on_log_progress_button_toggled,
        )
        if not self.app_obj.ytdl_log_stdout_flag:
            checkbutton4.set_sensitive(False)

        # (Signal connect from above)
        checkbutton2.connect(
            'toggled',
            self.on_log_stdout_button_toggled,
            checkbutton3,
            checkbutton4,
        )

        checkbutton5 = self.add_checkbutton(grid,
            _('Write errors/warnings (from downloader\'s STDERR) to the log'),
            self.app_obj.ytdl_log_stderr_flag,
            True,               # Can be toggled by user
            0, 6, 1, 1,
        )
        checkbutton5.set_hexpand(False)
        checkbutton5.connect(
            'toggled',
            self.on_log_stderr_button_toggled,
        )


    # Callback class methods


    def on_add_blocked_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_downloads_tab().

        Enables/disables adding blocked videos to the database.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.add_blocked_videos_flag:
            self.app_obj.set_add_blocked_videos_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.add_blocked_videos_flag:
            self.app_obj.set_add_blocked_videos_flag(False)


    def on_add_db_flag_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_clips_tab().

        Enables/disables adding split files to Tartube's database.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.split_video_add_db_flag:
            self.app_obj.set_split_video_add_db_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.split_video_add_db_flag:
            self.app_obj.set_split_video_add_db_flag(False)


    def on_add_from_list_button_toggled(self, checkbutton):

        """Called from callback in self.setup_files_database_tab().

        Enables/disables automatic adding of new Tartube data directories to
        the list of recent directories.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.data_dir_add_from_list_flag:
            self.app_obj.set_data_dir_add_from_list_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.data_dir_add_from_list_flag:
            self.app_obj.set_data_dir_add_from_list_flag(False)


    def on_age_restrict_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_websites_tab().

        Enables/disables ignoring of YouTube age-restriction error messages.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ignore_yt_age_restrict_flag:
            self.app_obj.set_ignore_yt_age_restrict_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ignore_yt_age_restrict_flag:
            self.app_obj.set_ignore_yt_age_restrict_flag(False)


    def on_alt_time_combo_changed(self, combo, type_str):

        """Called from a callback in self.setup_operations_limits_tab().

        Sets the hours or minutes portion of the start or stop time for
        alternative performance limits.

        Args:

            combo (Gtk.ComboBox): The widget clicked

            type_str (str): 'start_hour', 'start_min', 'stop_hour', 'stop_min'

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        value = model[tree_iter][0]

        if type_str == 'start_hour' or type_str == 'start_min':

            if type_str == 'start_hour':
                start_time = value + self.app_obj.alt_start_time[2:5]
            else:
                start_time = self.app_obj.alt_start_time[0:3] + value

            self.app_obj.set_alt_start_time(start_time)

        elif type_str == 'stop_hour' or type_str == 'stop_min':

            if type_str == 'stop_hour':
                stop_time = value + self.app_obj.alt_stop_time[2:5]
            else:
                stop_time = self.app_obj.alt_stop_time[0:3] + value

            self.app_obj.set_alt_stop_time(stop_time)


    def on_alt_days_combo_changed(self, combo):

        """Called from a callback in self.setup_operations_limits_tab().

        Sets the day(s) on which alternative performance limits apply.

        Args:

            combo (Gtk.ComboBox): The widget clicked

            type_str (str): One of the values in formats.SPECIFIED_DAYS_LIST,
                e.g. 'every_day'

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        self.app_obj.set_alt_day_string(model[tree_iter][1])


    def on_archive_button_toggled(self, checkbutton, checkbutton2, radiobutton,
    radiobutton2, radiobutton3, button, button2):

        """Called from callback in self.setup_operations_archive_tab().

        Enables/disables creation of youtube-dl's archive file,
        ytdl-archive.txt.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2 (Gtk.CheckButton): Other widgets to modify

            radiobutton, radiobutton2, radiobutton3 (Gtk.RadioButton): Other
                widgets to modify

            button, button2 (Gtk.Button): Other widgets to modify

        """

        if checkbutton.get_active() \
        and not self.app_obj.allow_ytdl_archive_flag:
            self.app_obj.set_allow_ytdl_archive_flag(True)
            radiobutton.set_sensitive(True)
            radiobutton2.set_sensitive(True)
            radiobutton3.set_sensitive(True)
            if self.app_obj.allow_ytdl_archive_mode == 'custom':
                button.set_sensitive(True)
                button2.set_sensitive(True)
            else:
                button.set_sensitive(False)
                button2.set_sensitive(False)
            checkbutton2.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.allow_ytdl_archive_flag:
            self.app_obj.set_allow_ytdl_archive_flag(False)
            radiobutton.set_sensitive(False)
            radiobutton2.set_sensitive(False)
            radiobutton3.set_sensitive(False)
            button.set_sensitive(False)
            button2.set_sensitive(False)
            checkbutton2.set_sensitive(False)
            checkbutton2.set_active(False)


    def on_archive_classic_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_archive_tab().

        Enables/disables creation of youtube-dl's archive file,
        ytdl-archive.txt, when downloading from the Classic Mode tab. Toggling
        the corresponding Gtk.ToggleButton in the Classic Mode tab sets the IV
        (and makes sure the two buttons have the same status).

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        main_win_obj = self.app_obj.main_win_obj

        other_flag = main_win_obj.classic_archive_button.get_active()
        if (checkbutton.get_active() and not other_flag):
            main_win_obj.classic_archive_button.set_active(True)
        elif (not checkbutton.get_active() and other_flag):
            main_win_obj.classic_archive_button.set_active(False)


    def on_archive_radiobutton_toggled(self, widget, radiobutton, \
    radiobutton2, radiobutton3, entry, button, button2):

        """Called from callback in self.setup_operations_archive_tab().

        Enables/disables creation of youtube-dl's archive file,
        ytdl-archive.txt.

        Args:

            widget (Gtk.RadioButton): The widget clicked

            radiobutton, radiobutton2, radiobutton3 (Gtk.RadioButton): Other
                widgets to check

            entry (Gtk.Entry): A widget to modify

            button, button2 (Gtk.Button): Other widgets to modify

        """

        if radiobutton.get_active():
            self.app_obj.set_allow_ytdl_archive_mode('default')
            entry.set_text('')
            self.app_obj.set_allow_ytdl_archive_path(None)
            button.set_sensitive(False)
            button2.set_sensitive(False)

        elif radiobutton2.get_active():
            self.app_obj.set_allow_ytdl_archive_mode('top')
            entry.set_text('')
            self.app_obj.set_allow_ytdl_archive_path(None)
            button.set_sensitive(False)
            button2.set_sensitive(False)

        elif radiobutton3.get_active():
            self.app_obj.set_allow_ytdl_archive_mode('custom')
            button.set_sensitive(True)
            button2.set_sensitive(True)


    def on_archive_update_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_archive_tab().

        Enables/disables updating the youtube-dl archive file, when a video is
        moved into a media.Folder.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.update_ytdl_archive_on_move_flag:
            self.app_obj.set_update_ytdl_archive_on_move_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.update_ytdl_archive_on_move_flag:
            self.app_obj.set_update_ytdl_archive_on_move_flag(False)


    def on_auto_assign_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_downloads_tab().

        Enables/disables auto-assigning anonymous youtube-dl error/warning
        messages to the most probable video.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.auto_assign_errors_warnings_flag:
            self.app_obj.set_auto_assign_errors_warnings_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.auto_assign_errors_warnings_flag:
            self.app_obj.set_auto_assign_errors_warnings_flag(False)


    def on_auto_clone_button_toggled(self, checkbutton):

        """Called from callback in self.setup_options_prefs().

        Enables/disables auto-cloning of the General Options Manager.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.auto_clone_options_flag:
            self.app_obj.set_auto_clone_options_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.auto_clone_options_flag:
            self.app_obj.set_auto_clone_options_flag(False)


    def on_auto_delete_button_toggled(self, checkbutton):

        """Called from callback in self.setup_options_prefs().

        Enables/disables auto-deleting of download options applied to a
        media.Video, after it has been downloaded.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.auto_delete_options_flag:
            self.app_obj.set_auto_delete_options_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.auto_delete_options_flag:
            self.app_obj.set_auto_delete_options_flag(False)


    def on_auto_delete_flag_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_clips_tab().

        Enables/disables auto-deleting the original video after splitting it
        into smaller pieces.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.split_video_auto_delete_flag:
            self.app_obj.set_split_video_auto_delete_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.split_video_auto_delete_flag:
            self.app_obj.set_split_video_auto_delete_flag(False)


    def on_auto_delete_videos_button_toggled(self, checkbutton, spinbutton,
    checkbutton2, checkbutton3, radiobutton, radiobutton2):

        """Called from callback in self.setup_files_delete_tab().

        Enables/disables automatic deletion of downloaded videos.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            spinbutton (Gtk.SpinButton): A widget to be (de)sensitised

            checkbutton2, checkbutton3 (Gtk.CheckButton): Other widgets to be
                (de)sensitised

            radiobutton, radiobutton2 (Gtk.RadioButton): Other widgets to be
                (de)sensitised

        """

        if checkbutton.get_active() \
        and not self.app_obj.auto_delete_flag:
            self.app_obj.set_auto_delete_flag(True)
            spinbutton.set_sensitive(True)
            checkbutton3.set_sensitive(True)
            radiobutton.set_sensitive(True)
            radiobutton2.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.auto_delete_flag:
            self.app_obj.set_auto_delete_flag(False)
            spinbutton.set_sensitive(False)
            if checkbutton2.get_active():
                checkbutton3.set_sensitive(True)
                radiobutton.set_sensitive(True)
                radiobutton2.set_sensitive(True)
            else:
                checkbutton3.set_active(False)
                checkbutton3.set_sensitive(False)
                radiobutton.set_active(True)
                radiobutton.set_sensitive(False)
                radiobutton2.set_sensitive(False)


    def on_auto_delete_videos_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_files_delete_tab().

        Sets the number of days after which downloaded videos should be
        deleted.

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        self.app_obj.set_auto_delete_days(spinbutton.get_value())


    def on_auto_remove_videos_button_toggled(self, checkbutton, spinbutton,
    checkbutton2, checkbutton3, radiobutton, radiobutton2):

        """Called from callback in self.setup_files_delete_tab().

        Enables/disables automatic removal of downloaded videos.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            spinbutton (Gtk.SpinButton): A widget to be (de)sensitised

            checkbutton2, checkbutton3 (Gtk.CheckButton): Other widgets to be
                (de)sensitised

            radiobutton, radiobutton2 (Gtk.RadioButton): Other widgets to be
                (de)sensitised

        """

        if checkbutton.get_active() \
        and not self.app_obj.auto_remove_flag:
            self.app_obj.set_auto_remove_flag(True)
            spinbutton.set_sensitive(True)
            checkbutton3.set_sensitive(True)
            radiobutton.set_sensitive(True)
            radiobutton2.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.auto_remove_flag:
            self.app_obj.set_auto_remove_flag(False)
            spinbutton.set_sensitive(False)
            if checkbutton2.get_active():
                checkbutton3.set_sensitive(True)
                radiobutton.set_sensitive(True)
                radiobutton2.set_sensitive(True)
            else:
                checkbutton3.set_active(False)
                checkbutton3.set_sensitive(False)
                radiobutton.set_active(True)
                radiobutton.set_sensitive(False)
                radiobutton2.set_sensitive(False)


    def on_auto_remove_videos_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_files_delete_tab().

        Sets the number of days after which downloaded videos should be
        removed.

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        self.app_obj.set_auto_remove_days(spinbutton.get_value())


    def on_auto_open_flag_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_clips_tab().

        Enables/disables auto-opening the destination directory after splitting
        a video.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.split_video_auto_open_flag:
            self.app_obj.set_split_video_auto_open_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.split_video_auto_open_flag:
            self.app_obj.set_split_video_auto_open_flag(False)


    def on_auto_restart_button_toggled(self, checkbutton, spinbutton,
    spinbutton2):

        """Called from callback in self.setup_operations_downloads_tab().

        Enables/disables restarting a stalled download operation.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            spinbutton, spinbutton2 (Gtk.SpinButton): Other widgets to modify

        """

        if checkbutton.get_active() \
        and not self.app_obj.operation_auto_restart_flag:
            self.app_obj.set_operation_auto_restart_flag(True)
            spinbutton.set_sensitive(True)
            spinbutton2.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.operation_auto_restart_flag:
            self.app_obj.set_operation_auto_restart_flag(False)
            spinbutton.set_sensitive(False)
            spinbutton2.set_sensitive(False)


    def on_auto_restart_max_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_operations_downloads_tab().

        Sets the maximum number of restarts after a stalled download.

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        self.app_obj.set_operation_auto_restart_max(spinbutton.get_value())


    def on_auto_restart_time_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_operations_downloads_tab().

        Sets the time after which a stalled download job is restarted.

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        self.app_obj.set_operation_auto_restart_time(spinbutton.get_value())


    def on_auto_switch_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_main_window_tab().

        Enables/disables automatically switching to the Output tab when an
        update operation starts.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.auto_switch_output_flag:
            self.app_obj.set_auto_switch_output_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.auto_switch_output_flag:
            self.app_obj.set_auto_switch_output_flag(False)


    def on_auto_update_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_downloads_tab().

        Enables/disables automatic update operation before every download
        operation.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.operation_auto_update_flag:
            self.app_obj.set_operation_auto_update_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.operation_auto_update_flag:
            self.app_obj.set_operation_auto_update_flag(False)


    def on_autostop_size_button_toggled(self, checkbutton, spinbutton, combo):

        """Called from callback in self.setup_scheduling_stop_tab().

        Enables/disables auto-stopping a download operation after a certain
        amount of disk space.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            spinbutton (Gtk.SpinButton): Another widget to modify

            combo (Gtk.ComboBox): Another widget to modify

        """

        if checkbutton.get_active() \
        and not self.app_obj.autostop_size_flag:
            self.app_obj.set_autostop_size_flag(True)
            spinbutton.set_sensitive(True)
            combo.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.autostop_size_flag:
            self.app_obj.set_autostop_size_flag(False)
            spinbutton.set_sensitive(False)
            combo.set_sensitive(False)


    def on_autostop_size_combo_changed(self, combo):

        """Called from a callback in self.setup_scheduling_stop_tab().

        Sets the disk space unit at which a download operation is auto-stopped.

        Args:

            combo (Gtk.ComboBox): The widget clicked

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        self.app_obj.set_autostop_size_unit(model[tree_iter][0])


    def on_autostop_size_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_scheduling_stop_tab().

        Sets the disk space value at which a download operation is
        auto-stopped.

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        self.app_obj.set_autostop_size_value(spinbutton.get_value())


    def on_autostop_time_button_toggled(self, checkbutton, spinbutton, combo):

        """Called from callback in self.setup_scheduling_stop_tab().

        Enables/disables auto-stopping a download operation after a certain
        time.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            spinbutton (Gtk.SpinButton): Another widget to modify

            combo (Gtk.ComboBox): Another widget to modify

        """

        if checkbutton.get_active() \
        and not self.app_obj.autostop_time_flag:
            self.app_obj.set_autostop_time_flag(True)
            spinbutton.set_sensitive(True)
            combo.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.autostop_time_flag:
            self.app_obj.set_autostop_time_flag(False)
            spinbutton.set_sensitive(False)
            combo.set_sensitive(False)


    def on_autostop_time_combo_changed(self, combo):

        """Called from a callback in self.setup_scheduling_stop_tab().

        Sets the time unit at which a download operation is auto-stopped.

        Args:

            combo (Gtk.ComboBox): The widget clicked

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        self.app_obj.set_autostop_time_unit(model[tree_iter][0])


    def on_autostop_time_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_scheduling_stop_tab().

        Sets the time value at which a download operation is auto-stopped.

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        self.app_obj.set_autostop_time_value(spinbutton.get_value())


    def on_autostop_videos_button_toggled(self, checkbutton, spinbutton):

        """Called from callback in self.setup_scheduling_stop_tab().

        Enables/disables auto-stopping a download operation after a certain
        number of videos.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            spinbutton (Gtk.SpinButton): Another widget to modify

        """

        if checkbutton.get_active() \
        and not self.app_obj.autostop_videos_flag:
            self.app_obj.set_autostop_videos_flag(True)
            spinbutton.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.autostop_videos_flag:
            self.app_obj.set_autostop_videos_flag(False)
            spinbutton.set_sensitive(False)


    def on_autostop_videos_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_scheduling_stop_tab().

        Sets the number of videos at which a download operation is
        auto-stopped.

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        self.app_obj.set_autostop_videos_value(spinbutton.get_value())


    def on_backup_button_toggled(self, radiobutton, value):

        """Called from callback in self.setup_files_backups_tab().

        Updates IVs in the main application.

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

            value (str): The new value of the IV

        """

        if radiobutton.get_active():
            self.app_obj.set_db_backup_mode(value)


    def on_bandwidth_button_toggled(self, checkbutton, alt_flag=False):

        """Called from callback in self.setup_operations_limits_tab().

        Enables/disables the download speed limit. Toggling the corresponding
        Gtk.CheckButton in the Progress tab sets the IV (and makes sure the two
        checkbuttons have the same status).

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            alt_flag (bool): If True, the alternative limit is toggled

        """

        main_win_obj = self.app_obj.main_win_obj

        if not alt_flag:

            other_flag = main_win_obj.bandwidth_checkbutton.get_active()

            if (checkbutton.get_active() and not other_flag):
                main_win_obj.bandwidth_checkbutton.set_active(True)
            elif (not checkbutton.get_active() and other_flag):
                main_win_obj.bandwidth_checkbutton.set_active(False)

        else:

            # Alternative limits. There is no second widget to toggle
            if checkbutton.get_active() \
            and not self.app_obj.alt_bandwidth_apply_flag:
                self.app_obj.set_alt_bandwidth_apply_flag(True)
            elif not checkbutton.get_active() \
            and self.app_obj.alt_bandwidth_apply_flag:
                self.app_obj.set_alt_bandwidth_apply_flag(False)


    def on_bandwidth_spinbutton_changed(self, spinbutton, alt_flag=False):

        """Called from callback in self.setup_operations_limits_tab().

        Sets the simultaneous download limit. Setting the value of the
        corresponding Gtk.SpinButton in the Progress tab sets the IV (and
        makes sure the two spinbuttons have the same value).

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

            alt_flag (bool): If True, the alternative limit is set

        """

        if not alt_flag:

            self.app_obj.main_win_obj.bandwidth_spinbutton.set_value(
                spinbutton.get_value(),
            )

        else:

            # Alternative limits. There is no second widget to toggle
            self.app_obj.set_alt_bandwidth(int(spinbutton.get_value()))


    def on_block_livestreams_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_livestreams_tab().

        Enables/disables checking/downloading livestreams by yt-dlp

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.block_livestreams_flag:
            self.app_obj.set_block_livestreams_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.block_livestreams_flag:
            self.app_obj.set_block_livestreams_flag(False)


    def on_check_comment_fetch_button_toggled(self, checkbutton, checkbutton2):

        """Called from callback in self.setup_operations_comments_tab().

        Enables/disables fetching video comments while checking videos.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2 (Gtk.CheckButton): Another widget to modify

        """

        if checkbutton.get_active() \
        and not self.app_obj.check_comment_fetch_flag:
            self.app_obj.set_check_comment_fetch_flag(True)
            checkbutton2.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.check_comment_fetch_flag:
            self.app_obj.set_check_comment_fetch_flag(False)

            if not self.app_obj.dl_comment_fetch_flag:
                checkbutton2.set_active(False)
                checkbutton2.set_sensitive(False)
            else:
                checkbutton2.set_sensitive(True)


    def on_check_limit_changed(self, entry):

        """Called from callback in self.setup_operations_block_tab().

        Sets the limit at which a download operation will stop checking a
        channel or playlist.

        Args:

            entry (Gtk.Entry): The widget changed

        """

        text = entry.get_text()
        if text.isdigit() and int(text) >= 0:
            self.app_obj.set_operation_check_limit(int(text))


    def on_clickable_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_videos_tab().

        Enables/disables clickable channel/playlist names in the Video
        Catalogue.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.catalogue_clickable_container_flag:
            self.app_obj.set_catalogue_clickable_container_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.catalogue_clickable_container_flag:
            self.app_obj.set_catalogue_clickable_container_flag(False)


    def on_child_process_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_ignore_tab().

        Enables/disables ignoring of child process exit error messages.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ignore_child_process_exit_flag:
            self.app_obj.set_ignore_child_process_exit_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ignore_child_process_exit_flag:
            self.app_obj.set_ignore_child_process_exit_flag(False)


    def on_clear_descrips_button_clicked(self, button):

        """Called from a callback in self.setup_files_update_tab().

        For every video in the database, clears the description (but doesn't
        modify the description file itself).

        Args:

            button (Gtk.Button): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Files > Update'
        )

        video_count = 0
        update_count = 0

        for media_data_obj in self.app_obj.media_reg_dict.values():

            if isinstance(media_data_obj, media.Video):

                video_count += 1

                if media_data_obj.descrip is not None \
                and media_data_obj.descrip != '':
                    update_count += 1

                media_data_obj.reset_video_descrip()

        # Confirm the result
        msg = _('Total videos:') + ' ' + str(video_count) + '\n\n' \
        + _('Videos updated:') + ' ' + str(update_count)

        self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue(
            msg,
            'info',
            'ok',
            self,           # Parent window is this window
        )


    def on_clipboard_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_windows_dialogues_tab().

        Enables/disables copying from the system clipboard in various dialogue
        windows.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.dialogue_copy_clipboard_flag:
            self.app_obj.set_dialogue_copy_clipboard_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.dialogue_copy_clipboard_flag:
            self.app_obj.set_dialogue_copy_clipboard_flag(False)


    def on_clips_dir_button_toggled(self, radiobutton):

        """Called from callback in self.setup_operations_clips_tab().

        Toggles between moving clips to the Video Clips folder.

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

        """

        if radiobutton.get_active():
            self.app_obj.set_split_video_clips_dir_flag(True)
        else:
            self.app_obj.set_split_video_clips_dir_flag(False)


    def on_clips_dl_mode_button_toggled(self, radiobutton):

        """Called from callback in self.setup_operations_clips_tab().

        Toggles between download clips with FFmpeg and yt-dlp.

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

        """

        if radiobutton.get_active():
            self.app_obj.set_video_timestamps_dl_mode('ffmpeg')
        else:
            self.app_obj.set_video_timestamps_dl_mode('downloader')


    def on_close_to_tray_toggled(self, checkbutton, checkbutton2):

        """Called from a callback in self.setup_windows_system_tray_tab().

        Enables/disables closing to the system tray, rather than closing the
        application.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2 (Gtk.CheckButton): Another widget to modify

        """

        if checkbutton.get_active() \
        and not self.app_obj.close_to_tray_flag:
            self.app_obj.set_close_to_tray_flag(True)
            checkbutton2.set_sensitive(True)
        elif not checkbutton.get_active() \
        and self.app_obj.close_to_tray_flag:
            self.app_obj.set_close_to_tray_flag(False)
            checkbutton2.set_active(False)
            checkbutton2.set_sensitive(False)


    def on_comment_store_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_comments_tab().

        Enables/disables storing video comments in the Tartube database.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.comment_store_flag:
            self.app_obj.set_comment_store_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.comment_store_flag:
            self.app_obj.set_comment_store_flag(False)


    def on_complex_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_videos_tab().

        Switches between simple/complex views in the Video Index.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        redraw_flag = False
        if checkbutton.get_active() and not self.app_obj.complex_index_flag:
            self.app_obj.set_complex_index_flag(True)
            redraw_flag = True
        elif not checkbutton.get_active() and self.app_obj.complex_index_flag:
            self.app_obj.set_complex_index_flag(False)
            redraw_flag = True

        if redraw_flag:
            # Redraw the Video Index and the Video Catalogue (since nothing in
            #   the Video Index will be selected)
            self.app_obj.main_win_obj.video_index_catalogue_reset()


    def on_confirm_url_button_toggled(self, checkbutton):

        """Called from callback in self.setup_files_urls_tab().

        Enables/disables prompting user for confirmation before modifying a
        URL.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.url_change_confirm_flag:
            self.app_obj.set_url_change_confirm_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.url_change_confirm_flag:
            self.app_obj.set_url_change_confirm_flag(False)


    def on_container_name_edited(self, widget, path, text, treeview, \
    checkbutton):

        """Called from callback in self.setup_files_urls_tab().

        Updates the name of a channel/playlist, prompting the user for
        confirmation first, if required.

        Args:

            widget (Gtk.CellRendererText): The widget clicked

            path (int): Path to the treeview line that was edited

            text (str): The new contents of the cell

            treeview (Gtk.TreeView): The parent treeview

            checkbutton (Gtk.CheckButton): If active, prompt the user before
                updating URLs

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Files > URLs'
        )

        # Check the entered text is a valid name
        if text == '' \
        or re.search(r'^\s*$', text) \
        or not self.app_obj.check_container_name_is_legal(text):

            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('The name \'{0}\' is not allowed').format(text),
                'error',
                'ok',
                self,           # Parent window is this window
            )

            return

        # Get the dbid for the selected line's channel/playlist
        model = treeview.get_model()
        tree_iter = model.get_iter(path)
        if tree_iter is not None:

            dbid = model[tree_iter][0]
            media_data_obj = self.app_obj.media_reg_dict[dbid]

            if media_data_obj.name == text:
                # No change
                return

            # Check that the parent folder doesn't already have a container
            #   with the same name
            if (
                media_data_obj.parent_obj is not None \
                and self.app_obj.find_duplicate_name_in_container(
                    media_data_obj.parent_obj,
                    text,
                )
            ) or (
                media_data_obj.parent_obj is None \
                and self.app_obj.find_duplicate_name_in_container(None, text)
            ):
                self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                    _(
                        'There is already a channel, playlist or folder' \
                        + ' called \'{0}\'',
                    ).format(text),
                    'error',
                    'ok',
                    self,           # Parent window is this window
                )

                return

            if not checkbutton.get_active():

                # Rename without confirmation
                if self.app_obj.rename_container_silently(
                    media_data_obj,
                    text,
                ):
                    model[tree_iter][3] = text

            else:

                # Seek confirmation before renaming
                if isinstance(media_data_obj, media.Channel):
                    msg = _('Are you sure you want to rename this channel?')
                elif isinstance(media_data_obj, media.Playlist):
                    msg = _('Are you sure you want to rename this playlist?')
                elif isinstance(media_data_obj, media.Folder):
                    msg = _('Are you sure you want to rename this folder?')

                self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                    msg,
                    'question',
                    'yes-no',
                    self,           # Parent window is this window
                    {
                        'yes': 'update_container_name',
                        'data': [model, tree_iter, media_data_obj, text],
                    },
                )


    def on_container_url_edited(self, widget, path, text, treeview, \
    checkbutton):

        """Called from callback in self.setup_files_urls_tab().

        Updates the URL for a channel/playlist, prompting the user for
        confirmation first, if required.

        Args:

            widget (Gtk.CellRendererText): The widget clicked

            path (int): Path to the treeview line that was edited

            text (str): The new contents of the cell

            treeview (Gtk.TreeView): The parent treeview

            checkbutton (Gtk.CheckButton): If active, prompt the user before
                updating URLs

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Files > URLs'
        )

        # Check the entered text is a valid URL
        if not ttutils.check_url(text):
            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('That is not a valid URL'),
                'error',
                'ok',
                self,           # Parent window is this window
            )

            return

        # Get the dbid for the selected line's channel/playlist
        model = treeview.get_model()
        tree_iter = model.get_iter(path)
        if tree_iter is not None:

            dbid = model[tree_iter][0]
            media_data_obj = self.app_obj.media_reg_dict[dbid]

            if not checkbutton.get_active():
                media_data_obj.set_source(text)
                model[tree_iter][3] = text

            else:

                self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                    _('Are you sure you want to update the URL?'),
                    'question',
                    'yes-no',
                    self,           # Parent window is this window
                    {
                        'yes': 'update_container_url',
                        'data': [model, tree_iter, media_data_obj, text],
                    },
                )


    def on_container_url_multiple_edited(self, button, entry, entry2, \
    treeview):

        """Called from callback in self.setup_files_urls_tab().

        Search and replace in the source URLs of the selected channels/
        playlists.

        Args:

            button (Gtk.Button): The widget clicked

            entry, entry2 (Gtk.Entry): Widgets containing the search/replace
                text

            treeview (Gtk.TreeView): The parent treeview

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + 'System preferences > Files > URLs'
        )

        # Check the pattern (in 'entry') is valid ('entry2' can contain any
        #   text, including no text at all)
        pattern = entry.get_text()
        subst = entry2.get_text()

        if not self.app_obj.url_change_regex_flag:

            if pattern == '':
                return

        else:

            try:
                re.compile(pattern)

            except re.error():
                self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                    _('The regex is invalid'),
                    'error',
                    'ok',
                    self,           # Parent window is this window
                )

                return

        # Get the media data objects for each selected line
        media_list = []
        mod_path_list = []

        selection = treeview.get_selection()
        this_tuple = selection.get_selected_rows()
        # (Confusingly, first item in the tuple is the Gtk.ListStore)
        model = this_tuple[0]
        for path in this_tuple[1]:

            tree_iter = model.get_iter(path)
            if tree_iter is not None:

                dbid = model[tree_iter][0]
                if dbid in self.app_obj.media_reg_dict:

                    media_data_obj = self.app_obj.media_reg_dict[dbid]
                    if media_data_obj.source is not None:
                        media_list.append(media_data_obj)
                        mod_path_list.append(path)

        if not media_list:
            # Nothing selected (or channels/playlists removed)
            return

        # Get confirmation, before proceeding
        if not self.app_obj.url_change_confirm_flag:

            self.app_obj.update_container_url_multiple(
                [
                    self,
                    model,
                    mod_path_list,
                    media_list,
                    pattern,
                    subst,
                ],
            )

        else:

            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('Are you sure you want to update these URLs?'),
                'question',
                'yes-no',
                self,           # Parent window is this window
                {
                    'yes': 'update_container_url_multiple',
                    'data': \
                    [
                        self,
                        model,
                        mod_path_list,
                        media_list,
                        pattern,
                        subst,
                    ],
                },
            )


    def on_convert_from_button_toggled(self, radiobutton, mode):

        """Called from callback in self.setup_operations_prefs_tab().

        Set what happens when downloading a media.Video object whose URL
        represents a channel/playlist.

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

            mode (str): The new value for the IV: 'disable', 'multi',
                'channel' or 'playlist'

        """

        if radiobutton.get_active():
            self.app_obj.set_operation_convert_mode(mode)


    def on_copy_thumb_flag_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_clips_tab().

        Enables/disables copying the original video's thumbnail after splitting
        a video.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.split_video_copy_thumb_flag:
            self.app_obj.set_split_video_copy_thumb_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.split_video_copy_thumb_flag:
            self.app_obj.set_split_video_copy_thumb_flag(False)


    def on_copyright_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_websites_tab().

        Enables/disables ignoring of YouTube copyright errors messages.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ignore_yt_copyright_flag:
            self.app_obj.set_ignore_yt_copyright_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ignore_yt_copyright_flag:
            self.app_obj.set_ignore_yt_copyright_flag(False)


    def on_custom_colour_button_clicked(self, colorbutton, key):

        """Called by self.setup_windows_colours_tab_add_row()

        After the colour selection dialogue has closed, update the custom
        background colour used in the Video Catalogue.

        Args:

            colorbutton (Gtk.ColorButton): The widget clicked

            key (str): One of the keys in mainapp.TartubeApp.custom_bg_table

        """

        rgba_obj = colorbutton.get_color()

        # Update IVs. Insert a standard value for alpha, so the user doesn't
        #   have to think about it
        self.app_obj.set_custom_bg(
            key,
            (rgba_obj.red / 65536),
            (rgba_obj.green / 65536),
            (rgba_obj.blue / 65536),
            0.20,
        )

        # Update the custom colour button to show the colour with its true
        #   alpha value
        mini_list = self.app_obj.custom_bg_table[key]
        custom_rgba_obj = Gdk.RGBA(
            mini_list[0],
            mini_list[1],
            mini_list[2],
            mini_list[3],
        )
        colorbutton.set_rgba(custom_rgba_obj)

        # Update the Video Catalogue to show the new colour
        if self.app_obj.main_win_obj.video_index_current_dbid is not None:
            self.app_obj.main_win_obj.video_catalogue_redraw_all(
                self.app_obj.main_win_obj.video_index_current_dbid,
            )


    def on_custom_dl_add_button_clicked(self, button, entry):

        """Called from callback in self.setup_operations_custom_dl_tab().

        Adds a new downloads.CustomDLManager object.

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Widget providiing a name for the new object

        """

        name = entry.get_text()
        if name == '':
            return

        new_obj = self.app_obj.create_custom_dl_manager(name)

        # Update the treeview
        self.setup_operations_custom_dl_tab_update_treeview()
        # Empty the entry box
        entry.set_text('')

        # All other widgets for creating an custom download manager object open
        #   its edit window, so we'll do the same here
        CustomDLEditWin(self.app_obj, new_obj)


    def on_custom_dl_clone_button_clicked(self, button, treeview):

        """Called from callback in self.setup_operations_custom_dl_tab().

        Clones the selected downloads.CustomDLManager object.

        Args:

            button (Gtk.Button): The widget clicked

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if path_list:

            # (Multiple selection is not enabled)
            this_iter = model.get_iter(path_list[0])
            if this_iter is not None:

                uid = model[this_iter][0]
                if uid in self.app_obj.custom_dl_reg_dict:

                    new_obj = self.app_obj.clone_custom_dl_manager(
                        self.app_obj.custom_dl_reg_dict[uid],
                    )

                    # Open an edit window, so the user can set the cloned
                    #   object's name
                    CustomDLEditWin(self.app_obj, new_obj)


    def on_custom_dl_delete_button_clicked(self, button, treeview):

        """Called from callback in self.setup_operations_custom_dl_tab().

        Deletes the selected downloads.CustomDLManager object, if allowed.

        Args:

            button (Gtk.Button): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Operations > Custom'
        )

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if not path_list:

            return

        # (Multiple selection is not enabled)
        this_iter = model.get_iter(path_list[0])
        if this_iter is None:

            return

        uid = model[this_iter][0]
        if not uid in self.app_obj.custom_dl_reg_dict:
            return

        custom_dl_obj = self.app_obj.custom_dl_reg_dict[uid]
        if self.app_obj.general_custom_dl_obj \
        and self.app_obj.general_custom_dl_obj == custom_dl_obj:

            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('The default custom download manager cannot be deleted'),
                'error',
                'ok',
                self,           # Parent window is this window
            )

            return

        # Prompt for confirmation
        self.app_obj.dialogue_manager_obj.show_msg_dialogue(
            _('Are you sure you want to delete this custom download manager?'),
            'question',
            'yes-no',
            self,           # Parent window is this window
            {
                'yes': 'delete_custom_dl_manager',
                # Specified manager
                'data': custom_dl_obj,
            },
        )


    def on_custom_dl_edit_button_clicked(self, button, treeview):

        """Called from callback in self.setup_operations_custom_dl_tab().

        Opens an edit window for the selected downloads.CustomDLManager object.

        Args:

            button (Gtk.Button): The widget clicked

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if path_list:

            # (Multiple selection is not enabled)
            this_iter = model.get_iter(path_list[0])
            if this_iter is not None:

                uid = model[this_iter][0]
                if uid in self.app_obj.custom_dl_reg_dict:

                    CustomDLEditWin(
                        self.app_obj,
                        self.app_obj.custom_dl_reg_dict[uid],
                    )


    def on_custom_dl_export_button_clicked(self, button, treeview):

        """Called from callback in self.setup_operations_custom_dl_tab().

        Exports the selected downloads.CustomDLManager object to a JSON file.

        Args:

            button (Gtk.Button): The widget clicked

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if path_list:

            # (Multiple selection is not enabled)
            this_iter = model.get_iter(path_list[0])
            if this_iter is not None:

                uid = model[this_iter][0]
                if uid in self.app_obj.custom_dl_reg_dict:

                    self.app_obj.export_custom_dl_manager(
                        self.app_obj.custom_dl_reg_dict[uid],
                    )


    def on_custom_dl_import_button_clicked(self, button, entry):

        """Called from callback in self.setup_operations_custom_dl_tab().

        Imports a JSON file and creates a new downloads.CustomDLManager for it.

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Widget providiing a name for the new object.
                If the entry is empty, then the name specified by the JSON file
                itself is used

        """

        self.app_obj.import_custom_dl_manager(entry.get_text())

        # Update the treeview
        self.setup_operations_custom_dl_tab_update_treeview()
        # Empty the entry box
        entry.set_text('')


    def on_custom_dl_use_classic_button_clicked(self, button, treeview):

        """Called from callback in self.setup_operations_custom_dl_tab().

        Applies the selected downloads.CustomDLManager object to the Classic
        Mode tab.

        Args:

            button (Gtk.Button): The widget clicked

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if path_list:

            # (Multiple selection is not enabled)
            this_iter = model.get_iter(path_list[0])
            if this_iter is not None:

                uid = model[this_iter][0]
                if uid in self.app_obj.custom_dl_reg_dict:

                    custom_dl_obj = self.app_obj.custom_dl_reg_dict[uid]

                    # If this is already the custom download manager for the
                    #   Classic Mode tab, then remove it (but don't delete the
                    #   object itself)
                    if self.app_obj.classic_custom_dl_obj \
                    and self.app_obj.classic_custom_dl_obj == custom_dl_obj:

                        self.app_obj.disapply_classic_custom_dl_manager()

                    else:

                        self.app_obj.apply_classic_custom_dl_manager(
                            self.app_obj.custom_dl_reg_dict[uid],
                        )

        # Update the treeview
        self.setup_operations_custom_dl_tab_update_treeview()


    def on_custom_textview_changed(self, textbuffer):

        """Called from callback in self.setup_windows_websites_tab().

        Sets the custom of list of ignorable error messages.

        Args:

            textbuffer (Gtk.TextBuffer): The buffer belonging to the textview
                whose contents has been modified

        """

        text = textbuffer.get_text(
            textbuffer.get_start_iter(),
            textbuffer.get_end_iter(),
            # Don't include hidden characters
            False,
        )

        # Filter out empty lines
        line_list = text.splitlines()
        mod_list = []
        for line in line_list:
            if re.search(r'\S', line):
                mod_list.append(line)

        # Apply the changes
        self.app_obj.set_ignore_custom_msg_list(mod_list)


    def on_custom_title_changed(self, entry):

        """Called from callback in self.setup_operations_clips_tab().

        Sets the custom title for split videos.

        Args:

            entry (Gtk.Entry): The widget clicked

        """

        text = entry.get_text()
        if text == '':
            self.app_obj.set_split_video_custom_title(
                self.app_obj.split_video_generic_title,
            )
        else:
            self.app_obj.set_split_video_custom_title(text)

        entry.set_text(self.app_obj.split_video_custom_title)


    def on_data_block_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_ignore_tab().

        Enables/disables ignoring of data block error messages.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ignore_data_block_error_flag:
            self.app_obj.set_ignore_data_block_error_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ignore_data_block_error_flag:
            self.app_obj.set_ignore_data_block_error_flag(False)


    def on_data_check_button_clicked(self, button):

        """Called from callback in self.setup_files_database_tab().

        Checks the Tartube database for inconsistencies, and fixes them.

        Args:

            button (Gtk.Button): The widget clicked

        """

        self.app_obj.check_integrity_db(
            False,      # Don't run silently; prompt the user before repairing
            self,       # This window, not the main window, is the parent
        )


    def on_data_dir_change_button_clicked(self, button):

        """Called from callback in self.setup_files_database_tab().

        Opens a window in which the user can select Tartube's data directoy.
        If the user actually selects it, call the main application to take
        action.

        Args:

            button (Gtk.Button): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Files > Database'
        )

        dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser(
            _('Please select Tartube\'s data folder'),
            self,
            'folder',
        )

        response = dialogue_win.run()
        if response == Gtk.ResponseType.OK:
            new_path = dialogue_win.get_filename()

        dialogue_win.destroy()

        if response == Gtk.ResponseType.OK:

            dialogue_manager_obj = self.app_obj.dialogue_manager_obj

            # In the past, I accidentally created a new database directory
            #   just inside an existing one, rather than switching to the
            #   existing one
            # If no database file exists, prompt the user to create a new one
            db_path = os.path.abspath(
                os.path.join(new_path, self.app_obj.db_file_name),
            )

            if not os.path.isfile(db_path):

                dialogue_manager_obj.show_msg_dialogue(
                    _(
                        'Are you sure you want to create a new database at' \
                        + ' this location?',
                    ) + '\n\n' + new_path,
                    'question',
                    'yes-no',
                    self,           # Parent window is this window
                    {
                        'yes': 'switch_db',
                        'data': [new_path, self],
                    },
                )

            else:

                # Database file already exists, so try to load it now
                self.try_switch_db(new_path, button)


    def on_data_dir_cursor_changed(self, treeview, button2, button3, button4,
    button5, button6):

        """Called by self.setup_files_database_tab().

        When a data directory in the list is selected, (de)sensitise buttons
        in response.

        Args:

            treeview (Gtk.TreeView): The widget in which a line was selected.

            button2, button3, button4, button5, button6 (Gtk.Button): Other
                widgets to be modified

        """

        selection = treeview.get_selection()
        (model, tree_iter) = selection.get_selected()
        if tree_iter is not None and not self.app_obj.disable_load_save_flag:

            data_dir = model[tree_iter][0]

            if data_dir != self.app_obj.data_dir:
                button2.set_sensitive(True)
                button3.set_sensitive(True)
            else:
                button2.set_sensitive(False)
                button3.set_sensitive(False)

            posn = self.app_obj.data_dir_alt_list.index(data_dir)
            if posn > 0:
                button5.set_sensitive(True)
            else:
                button5.set_sensitive(False)

            if posn < (len(self.app_obj.data_dir_alt_list) - 1):
                button6.set_sensitive(True)
            else:
                button6.set_sensitive(False)

        else:

            button2.set_sensitive(False)
            button3.set_sensitive(False)
            button5.set_sensitive(False)
            button6.set_sensitive(False)

        if len(self.app_obj.data_dir_alt_list) <= 1 \
        or self.app_obj.disable_load_save_flag:
            button4.set_sensitive(False)
        else:
            button4.set_sensitive(True)


    def on_data_dir_forget_button_clicked(self, button, treeview):

        """Called from callback in self.setup_files_database_tab().

        Removes the selected the data directory from the list of alternative
        data directories.

        Args:

            button (Gtk.Button): The widget that was clicked

            treeview (Gtk.TreeView): The widget in which a line was selected.

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Files > Database'
        )

        selection = treeview.get_selection()
        (model, tree_iter) = selection.get_selected()
        if tree_iter is None:

            # Nothing selected
            return

        else:

            data_dir = model[tree_iter][0]

        # Should not be possible to click the button, when the current
        #   directory is selected, but we'll check anyway
        if data_dir == self.app_obj.data_dir:
            return

        # Prompt the user for confirmation. If the user confirms, this window
        #   is reset to update the treeview
        self.app_obj.dialogue_manager_obj.show_msg_dialogue(
            _('Are you sure you want to forget this database?'),
            'question',
            'yes-no',
            self,           # Parent window is this window
            {
                'yes': 'forget_db',
                'data': [data_dir, self],
            },
        )


    def on_data_dir_forget_all_button_clicked(self, button, treeview):

        """Called from callback in self.setup_files_database_tab().

        Removes all data directories from the list of alternatives, except for
        the current one.

        Args:

            button (Gtk.Button): The widget that was clicked

            treeview (Gtk.TreeView): The widget in which a line was selected.

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Files > Database'
        )

        # Should not be possible to click the button, when the list contains
        #   no alternatives but the current one, but we'll check anyway
        if len(self.app_obj.data_dir_alt_list) <= 1:
            return

        # Prompt the user for confirmation. If the user confirms, this window
        #   is reset to update the treeview
        self.app_obj.dialogue_manager_obj.show_msg_dialogue(
            _(
            'Are you sure you want to forget all databases except the' \
            + ' current one?',
            ),
            'question',
            'yes-no',
            self,           # Parent window is this window
            {
                'yes': 'forget_all_db',
                'data': self,
            },
        )


    def on_data_dir_move_down_button_clicked(self, button, treeview, \
    liststore, button2):

        """Called from callback in self.setup_files_database_tab().

        Moves the selected data directory down one position in the list of
        alternative data directories.

        Args:

            button (Gtk.Button): The widget that was clicked (the down button)

            treeview (Gtk.TreeView): The widget in which a line was selected

            liststore (Gtk.ListStore): The treeview's liststore

            button2 (Gtk.Button): The up button

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if not path_list:

            # Nothing selected
            return

        # (Keeping track of the first/last selected items helps us to
        #   (de)sensitise buttons, in a moment)
        first_item = None
        last_item = None

        path_list.reverse()

        for path in path_list:

            this_iter = model.get_iter(path)
            last_item = model[this_iter][0]
            if first_item is None:
                first_item = model[this_iter][0]

            if model.iter_next(this_iter):

                liststore.move_after(
                    this_iter,
                    model.iter_next(this_iter),
                )

            else:

                # If the first item won't move up, then successive items will
                #   be moved above this one (which is not what we want)
                break

        # Update the IV
        dir_list = []
        for row in liststore:
            dir_list.append(row[0])

        self.app_obj.set_data_dir_alt_list(dir_list)

        # (De)sensitise the button(s), if required
        if dir_list.index(first_item) == 0:
            button2.set_sensitive(False)
        else:
            button2.set_sensitive(True)

        if dir_list.index(last_item) == (len(dir_list) - 1):
            button.set_sensitive(False)
        else:
            button.set_sensitive(True)


    def on_data_dir_move_up_button_clicked(self, button, treeview, liststore,
    button2):

        """Called from callback in self.setup_files_database_tab().

        Moves the selected data directory up one position in the list of
        alternative data directories.

        Args:

            button (Gtk.Button): The widget that was clicked (the up button)

            treeview (Gtk.TreeView): The widget in which a line was selected

            liststore (Gtk.ListStore): The treeview's liststore

            button2 (Gtk.Button): The down button

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if not path_list:

            # Nothing selected
            return

        # (Keeping track of the first/last selected items helps us to
        #   (de)sensitise buttons, in a moment)
        first_item = None
        last_item = None

        # Move the selected items up
        for path in path_list:

            this_iter = model.get_iter(path)
            last_item = model[this_iter][0]
            if first_item is None:
                first_item = model[this_iter][0]

            if model.iter_previous(this_iter):

                liststore.move_before(
                    this_iter,
                    model.iter_previous(this_iter),
                )

            else:

                # If the first item won't move up, then successive items will
                #   be moved above this one (which is not what we want)
                break

        # Update the IV
        dir_list = []
        for row in liststore:
            dir_list.append(row[0])

        self.app_obj.set_data_dir_alt_list(dir_list)

        # (De)sensitise the button(s), if required
        if dir_list.index(first_item) == 0:
            button.set_sensitive(False)
        else:
            button.set_sensitive(True)

        if dir_list.index(last_item) == (len(dir_list) - 1):
            button2.set_sensitive(False)
        else:
            button2.set_sensitive(True)


    def on_data_dir_switch_button_clicked(self, button, button2, treeview):

        """Called from callback in self.setup_files_database_tab().

        Changes the Tartube data directory to the one selected in the
        textview.

        Args:

            button (Gtk.Button): The widget clicked

            button2 (Gtk.Button): Another button to be possibly desensitised

            treeview (Gtk.TreeView): A widget in which one file path is
                selected (maybe)

            entry, entry2 (Gtk.Entry): Other widgets to be modified

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Files > Database'
        )

        selection = treeview.get_selection()
        (model, tree_iter) = selection.get_selected()
        if tree_iter is None:

            # Nothing selected
            return

        else:

            data_dir = model[tree_iter][0]

        # Should not be possible to click the button, when the current
        #   directory is selected, but we'll check anyway
        if data_dir == self.app_obj.data_dir:
            return

        # If no database file exists, prompt the user to create a new one
        db_path = os.path.abspath(
            os.path.join(data_dir, self.app_obj.db_file_name),
        )

        if not os.path.isfile(db_path):

            self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue(
                _(
                    'No database exists at this location:',
                ) + '\n\n' + data_dir + '\n\n' + _(
                    'Do you want to create a new one?',
                ),
                'question',
                'yes-no',
                self,           # Parent window is this window
                {
                    'yes': 'switch_db',
                    'data': [data_dir, self],
                },
            )

        else:

            # Database file already exists, so try to load it now
            self.try_switch_db(data_dir, button2)


    def on_delete_asap_button_toggled(self, radiobutton):

        """Called from callback in self.setup_files_delete_tab().

        Enables/disables automatic deletion/removal of videos after every
        download operation.

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

        """

        if not radiobutton.get_active() \
        and not self.app_obj.auto_delete_asap_flag:
            self.app_obj.set_auto_delete_asap_flag(True)
        elif radiobutton.get_active() \
        and self.app_obj.auto_delete_asap_flag:
            self.app_obj.set_auto_delete_asap_flag(False)


    def on_default_avconv_button_clicked(self, button, entry):

        """Called from callback in self.setup_downloader_ffmpeg_tab().

        Sets the path to the avconv binary to the default path.

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Another widget to be modified by this function

        """

        self.app_obj.set_avconv_path(self.app_obj.default_avconv_path)
        entry.set_text(self.app_obj.avconv_path)


    def on_default_colour_button_clicked(self, colorbutton, event_button, \
    colorbutton2, key):

        """Called by self.setup_windows_colours_tab_add_row()

        After clicking the default colour button, don't allow the usual
        colour selection dialogue to open. Instead, update the button showing
        the custom colour.

        Args:

            colorbutton (Gtk.ColorButton): The widget clicked

            event_button (Gdk.EventButton): Ignored

            colorbutton2 (Gtk.ColorButton): Another widget to u pdate

            key (str): One of the keys in mainapp.TartubeApp.custom_bg_table

        """

        # Update IVs
        self.app_obj.reset_custom_bg(key)

        # Update the custom colour button
        mini_list = self.app_obj.custom_bg_table[key]
        custom_rgba_obj = Gdk.RGBA(
            mini_list[0],
            mini_list[1],
            mini_list[2],
            mini_list[3],
        )
        colorbutton2.set_rgba(custom_rgba_obj)

        # Update the Video Catalogue to show the new colour
        if self.app_obj.main_win_obj.video_index_current_dbid is not None:
            self.app_obj.main_win_obj.video_catalogue_redraw_all(
                self.app_obj.main_win_obj.video_index_current_dbid,
            )

        # Return True so the colour selection dialogue is not opened
        return True


    def on_default_ffmpeg_button_clicked(self, button, entry):

        """Called from callback in self.setup_downloader_ffmpeg_tab().

        Sets the path to the ffmpeg binary to the default path.

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Another widget to be modified by this function

        """

        self.app_obj.set_ffmpeg_path(self.app_obj.default_ffmpeg_path)
        entry.set_text(self.app_obj.ffmpeg_path)


    def on_delete_shutdown_button_toggled(self, checkbutton, checkbutton2):

        """Called from callback in self.setup_files_temp_folders_tab().

        Enables/disables emptying temporary folders when Tartube shuts down.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2 (Gtk.CheckButton): Another widget to be modified

        """

        if checkbutton.get_active() \
        and not self.app_obj.delete_on_shutdown_flag:
            self.app_obj.set_delete_on_shutdown_flag(True)
            checkbutton2.set_sensitive(False)

        elif not checkbutton.get_active() \
        and self.app_obj.delete_on_shutdown_flag:
            self.app_obj.set_delete_on_shutdown_flag(False)
            checkbutton2.set_sensitive(True)


    def on_delete_watched_button_toggled(self, checkbutton):

        """Called from callback in self.setup_files_delete_tab().

        Enables/disables automatic deletion/removal of videos, but only those
        that have been watched.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.auto_delete_watched_flag:
            self.app_obj.set_auto_delete_watched_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.auto_delete_watched_flag:
            self.app_obj.set_auto_delete_watched_flag(False)


    def on_dialogue_button_toggled(self, radiobutton, mode):

        """Called from callback in self.setup_operations_actions_tab().

        Sets whether a desktop notification, dialogue window or neither should
        be shown to the user at the end of a download/update/refresh/info/tidy
        operation.

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

            mode (str): The new value for the IV: 'default', 'desktop' or
                'dialogue'

        """

        if radiobutton.get_active():
            self.app_obj.set_operation_dialogue_mode(mode)


    def on_dialogue_disable_toggled(self, checkbutton):

        """Called from a callback in self.setup_windows_dialogues_tab().

        Enables/disables message dialogue windows (for testing purposes).

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.dialogue_disable_msg_flag:
            self.app_obj.set_dialogue_disable_msg_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.dialogue_disable_msg_flag:
            self.app_obj.set_dialogue_disable_msg_flag(False)


    def on_disable_dl_all_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_main_window_tab().

        Enables/disables the 'Download all' buttons in the main window toolbar
        and in the Videos tab.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.disable_dl_all_flag:
            self.app_obj.set_disable_dl_all_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.disable_dl_all_flag:
            self.app_obj.set_disable_dl_all_flag(False)


    def on_disk_stop_button_toggled(self, checkbutton, spinbutton):

        """Called from a callback in self.setup_files_device_tab().

        Enables/disables halting a download operation when the system is
        running out of disk space.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            spinbutton (Gtk.CheckButton): Another widget to be (de)sensitised

        """

        if checkbutton.get_active() \
        and not self.app_obj.disk_space_stop_flag:
            self.app_obj.set_disk_space_stop_flag(True)
            spinbutton.set_sensitive(True)
        elif not checkbutton.get_active() \
        and self.app_obj.disk_space_stop_flag:
            self.app_obj.set_disk_space_stop_flag(False)
            spinbutton.set_sensitive(False)


    def on_disk_stop_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_files_device_tab().

        Sets the amount of free disk space below which download operations
        will be halted.

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        self.app_obj.set_disk_space_stop_limit(spinbutton.get_value())


    def on_disk_warn_button_toggled(self, checkbutton, spinbutton):

        """Called from a callback in self.setup_files_device_tab().

        Enables/disables warnings when the system is running out of disk space.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            spinbutton (Gtk.CheckButton): Another widget to be (de)sensitised

        """

        if checkbutton.get_active() \
        and not self.app_obj.disk_space_warn_flag:
            self.app_obj.set_disk_space_warn_flag(True)
            spinbutton.set_sensitive(True)
        elif not checkbutton.get_active() \
        and self.app_obj.disk_space_warn_flag:
            self.app_obj.set_disk_space_warn_flag(False)
            spinbutton.set_sensitive(False)


    def on_disk_warn_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_files_device_tab().

        Sets the amount of free disk space below which a warning will be
        issued.

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        self.app_obj.set_disk_space_warn_limit(spinbutton.get_value())


    def on_dl_comment_fetch_button_toggled(self, checkbutton, checkbutton2):

        """Called from callback in self.setup_operations_comments_tab().

        Enables/disables fetching video comments while downloading videos.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2 (Gtk.CheckButton): Another widget to modify

        """

        if checkbutton.get_active() \
        and not self.app_obj.dl_comment_fetch_flag:
            self.app_obj.set_dl_comment_fetch_flag(True)
            checkbutton2.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.dl_comment_fetch_flag:
            self.app_obj.set_dl_comment_fetch_flag(False)

            if not self.app_obj.check_comment_fetch_flag:
                checkbutton2.set_active(False)
                checkbutton2.set_sensitive(False)
            else:
                checkbutton2.set_sensitive(True)


    def on_dl_limit_changed(self, entry):

        """Called from callback in self.setup_operations_block_tab().

        Sets the limit at which a download operation will stop downloading a
        channel or playlist.

        Args:

            entry (Gtk.Entry): The widget changed

        """

        text = entry.get_text()
        if text.isdigit() and int(text) >= 0:
            self.app_obj.set_operation_download_limit(int(text))


    def on_drag_error_msg_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_drag_tab().

        Enables/disables transferring the error/warning message when dragging
        and dropping from the Errors/Warnings tab to an external application.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.drag_error_msg_flag:
            self.app_obj.set_drag_error_msg_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.drag_error_msg_flag:
            self.app_obj.set_drag_error_msg_flag(False)


    def on_drag_error_name_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_drag_tab().

        Enables/disables transferring the video/channel/playlist name when
        dragging and dropping from the Errors/Warnings tab to an external
        application.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.drag_error_name_flag:
            self.app_obj.set_drag_error_name_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.drag_error_name_flag:
            self.app_obj.set_drag_error_name_flag(False)


    def on_drag_error_path_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_drag_tab().

        Enables/disables transferring the video/channel/playlist path when
        dragging and dropping from the Errors/Warnings tab to an external
        application.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.drag_error_path_flag:
            self.app_obj.set_drag_error_path_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.drag_error_path_flag:
            self.app_obj.set_drag_error_path_flag(False)


    def on_drag_error_separator_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_drag_tab().

        Enables/disables adding a separator before each video/channel/playlsit
        when dragging and dropping from the Errors/Warnings tab to an external
        application.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.drag_error_separator_flag:
            self.app_obj.set_drag_error_separator_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.drag_error_separator_flag:
            self.app_obj.set_drag_error_separator_flag(False)


    def on_drag_error_source_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_drag_tab().

        Enables/disables transferring the video/channel/playlist URL when
        dragging and dropping from the Errors/Warnings tab to an external
        application.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.drag_error_source_flag:
            self.app_obj.set_drag_error_source_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.drag_error_source_flag:
            self.app_obj.set_drag_error_source_flag(False)


    def on_drag_msg_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_drag_tab().

        Enables/disables transferring of any errors/warnings associated with
        the video when dragging and dropping to an external application.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.drag_video_msg_flag:
            self.app_obj.set_drag_video_msg_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.drag_video_msg_flag:
            self.app_obj.set_drag_video_msg_flag(False)


    def on_drag_name_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_drag_tab().

        Enables/disables transferring the video's name when dragging and
        dropping to an external application.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.drag_video_name_flag:
            self.app_obj.set_drag_video_name_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.drag_video_name_flag:
            self.app_obj.set_drag_video_name_flag(False)


    def on_drag_name_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_drag_tab().

        Enables/disables transferring the video's name when dragging and
        dropping to an external application.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.drag_video_name_flag:
            self.app_obj.set_drag_video_name_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.drag_video_name_flag:
            self.app_obj.set_drag_video_name_flag(False)


    def on_drag_path_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_drag_tab().

        Enables/disables transferring the video's path when dragging and
        dropping to an external application.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.drag_video_path_flag:
            self.app_obj.set_drag_video_path_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.drag_video_path_flag:
            self.app_obj.set_drag_video_path_flag(False)


    def on_drag_separator_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_drag_tab().

        Enables/disables adding a separator before each video, when dragging
        video data into an external application.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.drag_video_separator_flag:
            self.app_obj.set_drag_video_separator_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.drag_video_separator_flag:
            self.app_obj.set_drag_video_separator_flag(False)


    def on_drag_source_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_drag_tab().

        Enables/disables transferring the video's source URL when dragging and
        dropping to an external application.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.drag_video_source_flag:
            self.app_obj.set_drag_video_source_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.drag_video_source_flag:
            self.app_obj.set_drag_video_source_flag(False)


    def on_drag_thumb_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_drag_tab().

        Enables/disables transferring the video thumbnail's path when dragging
        and dropping to an external application.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.drag_thumb_path_flag:
            self.app_obj.set_drag_thumb_path_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.drag_thumb_path_flag:
            self.app_obj.set_drag_thumb_path_flag(False)


    def on_dump_db_button_clicked(self, button, treeview):

        """Called from callback in self.setup_files_database_tab().

        Prompts the user to select a database, then dumps it to JSON.

        The code is here, rather than in mainapp.TartubeApp, so that dialogue
        windows can use this preferences window as the parent.

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeView): The treeview showing recent data folders

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Files > Database'
        )

        # Prompt the user to select a database file
        dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser(
            _('Select a Tartube database file'),
            self,
            'open',
        )

        response = dialogue_win.run()
        if response == Gtk.ResponseType.OK:
            db_path = dialogue_win.get_filename()

        dialogue_win.destroy()

        if response != Gtk.ResponseType.OK or not db_path:
            return

        # Try to load it, ignoring any lockfiles that might be in place
        try:
            fh = open(db_path, 'rb')
            load_dict = pickle.load(fh)
            fh.close()

        except:
            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('This file is not loadable (might be corrupted)'),
                'error',
                'ok',
                self,
            )

            return

        if not 'container_reg_dict' in load_dict \
        or not 'media_reg_dict' in load_dict:
            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('This file does is not compatible with Tartube'),
                'error',
                'ok',
                self,
            )

            return

        elif len(load_dict['container_reg_dict']) == 0:
            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('This file was loaded, but is empty'),
                'error',
                'ok',
                self,
            )

            return

        # Prepare a dictionary in the same form exported by
        #   mainapp.TartubeApp.export_from_db()
        container_reg_dict = load_dict['container_reg_dict']
        local = ttutils.get_local_time()
        db_dict = {}
        count = 0

        for dbid in self.app_obj.container_reg_dict.keys():

            if not dbid in load_dict['media_reg_dict']:
                continue

            media_data_obj = load_dict['media_reg_dict'][dbid]
            # (Don't export folders; just export channels/playlists as a flat
            #   dictionary)
            if isinstance(media_data_obj, media.Channel) \
            or isinstance(media_data_obj, media.Playlist):

                count += 1
                mini_dict = {
                    'dbid': count,
                    'vid': None,
                    'name': media_data_obj.name,
                    'nickname': media_data_obj.nickname,
                    'file': None,
                    'source': media_data_obj.source,
                    'db_dict': {},
                }

                if isinstance(media_data_obj, media.Channel):
                    mini_dict['type'] = 'channel'
                else:
                    mini_dict['type'] = 'playlist'

                db_dict[count] = mini_dict

        export_dict = {
            # Metadata
            'script_name': __main__.__packagename__,
            'script_version': __main__.__version__,
            'save_date': str(local.strftime('%d %b %Y')),
            'save_time': str(local.strftime('%H:%M:%S')),
            'file_type': 'db_export',
            # Data
            'db_dict': db_dict,
        }

        # Try to save the export file
        export_path = os.path.abspath(
            os.path.join(
                os.path.dirname(os.path.realpath(db_path)),
                self.app_obj.export_json_file_name,
            ),
        )

        try:
            with open(export_path, 'w') as outfile:
                json.dump(export_dict, outfile, indent=4)

            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('Database export file saved to:') \
                + '\n\n' + export_path,
                'info',
                'ok',
                self,
            )

        except Exception as e:

            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('Failed to save the database export file:') \
                + '\n\n' + str(e),
                'error',
                'ok',
                self,
            )


    def on_enable_livestreams_button_toggled(self, checkbutton, checkbutton2,
    checkbutton3, spinbutton, spinbutton2):

        """Called from callback in self.setup_operations_livestreams_tab().

        Enables/disables livestream detection.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2, checkbutton3 (Gtk.CheckButton): Other widgets to
                sensitise/desensitise, according to the new value of the flag

            spinbutton, spinbutton2 (Gtk.SpinButton): Another widget to
                sensitise/desensitise, according to the new value of the flag

        """

        if checkbutton.get_active() \
        and not self.app_obj.enable_livestreams_flag:
            self.app_obj.set_enable_livestreams_flag(True)
            checkbutton2.set_sensitive(True)
            checkbutton3.set_sensitive(True)
            spinbutton.set_sensitive(True)
            spinbutton2.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.enable_livestreams_flag:
            self.app_obj.set_enable_livestreams_flag(False)
            checkbutton2.set_active(False)
            checkbutton2.set_sensitive(False)
            checkbutton3.set_active(False)
            checkbutton3.set_sensitive(False)
            spinbutton.set_sensitive(False)
            spinbutton2.set_sensitive(False)


    def on_expand_tree_toggled(self, checkbutton, checkbutton2):

        """Called from callback in self.setup_windows_videos_tab().

        Enables/disables auto-expansion of the Video Index after a folder is
        selected (clicked).

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2 (Gtk.CheckButton): Another widget that must be
                modified

        """

        if checkbutton.get_active() \
        and not self.app_obj.auto_expand_video_index_flag:
            self.app_obj.set_auto_expand_video_index_flag(True)
            checkbutton2.set_sensitive(True)
        elif not checkbutton.get_active() \
        and self.app_obj.auto_expand_video_index_flag:
            self.app_obj.set_auto_expand_video_index_flag(False)
            checkbutton2.set_sensitive(False)


    def on_expand_full_tree_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_videos_tab().

        Enables/disables full auto-expansion of the Video Index after a folder
        is selected (clicked).

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.full_expand_video_index_flag:
            self.app_obj.set_full_expand_video_index_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.full_expand_video_index_flag:
            self.app_obj.set_full_expand_video_index_flag(False)


    def on_extra_livestreams_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_livestreams_tab().

        Enables/disables performing more frequent livestream operations when a
        livestream is due to start.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.scheduled_livestream_extra_flag:
            self.app_obj.set_scheduled_livestream_extra_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.scheduled_livestream_extra_flag:
            self.app_obj.set_scheduled_livestream_extra_flag(False)


    def on_extract_comments_button_clicked(self, button):

        """Called from callback in self.setup_files_update_tab().

        Extracts timestamps from the description of every video in the
        database.

        Args:

            button (Gtk.Button): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Files > Update'
        )

        video_count = 0
        attempt_count = 0
        success_count = 0

        for media_data_obj in self.app_obj.media_reg_dict.values():

            if isinstance(media_data_obj, media.Video):

                video_count += 1

                if not media_data_obj.comment_list:

                    attempt_count += 1
                    self.app_obj.update_video_from_json(
                        media_data_obj,
                        'comments',
                    )

                    if media_data_obj.comment_list:
                        success_count += 1

        # Redraw the Video Catalogue, at its current page
        main_win_obj = self.app_obj.main_win_obj
        main_win_obj.video_catalogue_redraw_all(
            main_win_obj.video_index_current_dbid,
            main_win_obj.catalogue_toolbar_current_page,
        )

        # Confirm the result
        msg = _('Total videos:') + ' ' + str(video_count) + '\n\n' \
        + _('Videos checked:') + ' ' + str(attempt_count) + '\n' \
        + _('Videos updated:') + ' ' + str(success_count)

        self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue(
            msg,
            'info',
            'ok',
            self,           # Parent window is this window
        )


    def on_extract_stamps_button_clicked(self, button):

        """Called from callback in self.setup_files_update_tab().

        Extracts timestamps from the description of every video in the
        database.

        Args:

            button (Gtk.Button): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Files > Update'
        )

        video_count = 0
        attempt_count = 0
        success_count = 0

        for media_data_obj in self.app_obj.media_reg_dict.values():

            if isinstance(media_data_obj, media.Video):

                video_count += 1

                if not media_data_obj.stamp_list \
                and media_data_obj.descrip is not None \
                and media_data_obj.descrip != '':

                    attempt_count += 1

                    media_data_obj.extract_timestamps_from_descrip(
                        self.app_obj,
                    )

                    if media_data_obj.stamp_list:

                        success_count += 1

        # Redraw the Video Catalogue, at its current page
        main_win_obj = self.app_obj.main_win_obj
        main_win_obj.video_catalogue_redraw_all(
            main_win_obj.video_index_current_dbid,
            main_win_obj.catalogue_toolbar_current_page,
        )

        # Confirm the result
        msg = _('Total videos:') + ' ' + str(video_count) + '\n\n' \
        + _('Videos checked:') + ' ' + str(attempt_count) + '\n' \
        + _('Videos updated:') + ' ' + str(success_count)

        self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue(
            msg,
            'info',
            'ok',
            self,           # Parent window is this window
        )


    def on_extract_descrip_flag_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_clips_tab().

        Enables/disables automatically extracting timestamps from the video's
        description file.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.video_timestamps_extract_descrip_flag:
            self.app_obj.set_video_timestamps_extract_descrip_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.video_timestamps_extract_descrip_flag:
            self.app_obj.set_video_timestamps_extract_descrip_flag(False)


    def on_extract_json_flag_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_clips_tab().

        Enables/disables automatically extracting timestamps from the video's
        metadata file.


        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.video_timestamps_extract_json_flag:
            self.app_obj.set_video_timestamps_extract_json_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.video_timestamps_extract_json_flag:
            self.app_obj.set_video_timestamps_extract_json_flag(False)


    def on_ffmpeg_add_button_clicked(self, button, entry):

        """Called from callback in self.setup_options_ffmpeg_list_tab().

        Adds a new ffmpeg_tartube.FFmpegOptionsManager object.

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Widget providiing a name for the new object

        """

        name = entry.get_text()
        if name == '':
            return

        new_obj = self.app_obj.create_ffmpeg_options(name)

        # Update the treeview
        self.setup_options_ffmpeg_list_tab_update_treeview()
        # Empty the entry box
        entry.set_text('')

        # All other widgets for creating an options manager object open its
        #   edit window, so we'll do the same here
        FFmpegOptionsEditWin(self.app_obj, new_obj)


    def on_ffmpeg_clone_button_clicked(self, button, treeview):

        """Called from callback in self.setup_options_ffmpeg_list_tab().

        Clones the selected ffmpeg_tartube.FFmpegOptionsManager object.

        Args:

            button (Gtk.Button): The widget clicked

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if path_list:

            # (Multiple selection is not enabled)
            this_iter = model.get_iter(path_list[0])
            if this_iter is not None:

                uid = model[this_iter][0]
                if uid in self.app_obj.ffmpeg_reg_dict:

                    new_obj = self.app_obj.clone_ffmpeg_options(
                        self.app_obj.ffmpeg_reg_dict[uid],
                    )

                    # Open an edit window, so the user can set the cloned
                    #   object's name
                    FFmpegOptionsEditWin(self.app_obj, new_obj)


    def on_ffmpeg_convert_flag_toggled(self, checkbutton, checkbutton2):

        """Called from callback in self.setup_operations_downloads_tab().

        Enables/disables conversion of .webp thumbnails into .jpg thumbnails.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2 (Gtk.CheckButton): Another widget to be updated

        """

        if checkbutton.get_active() \
        and not self.app_obj.ffmpeg_convert_webp_flag:
            self.app_obj.set_ffmpeg_convert_webp_flag(True)
            checkbutton2.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.ffmpeg_convert_webp_flag:
            self.app_obj.set_ffmpeg_convert_webp_flag(False)
            checkbutton2.set_active(False)
            checkbutton2.set_sensitive(False)


    def on_ffmpeg_delete_button_clicked(self, button, treeview):

        """Called from callback in self.setup_options_ffmpeg_list_tab().

        Deletes the selected ffmpeg_tartube.FFmpegOptionsManager object, if
        allowed.

        Args:

            button (Gtk.Button): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Options > FFmpeg options'
        )

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if not path_list:

            return

        # (Multiple selection is not enabled)
        this_iter = model.get_iter(path_list[0])
        if this_iter is None:

            return

        uid = model[this_iter][0]
        if not uid in self.app_obj.ffmpeg_reg_dict:
            return

        options_obj = self.app_obj.ffmpeg_reg_dict[uid]
        if self.app_obj.ffmpeg_options_obj \
        and self.app_obj.ffmpeg_options_obj == options_obj:

            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('The current options manager cannot be deleted'),
                'error',
                'ok',
                self,           # Parent window is this window
            )

            return

        # Prompt for confirmation
        self.app_obj.dialogue_manager_obj.show_msg_dialogue(
            _('Are you sure you want to delete this options manager?'),
            'question',
            'yes-no',
            self,           # Parent window is this window
            {
                'yes': 'delete_ffmpeg_options',
                # Specified options
                'data': options_obj,
            },
        )


    def on_ffmpeg_edit_button_clicked(self, button, treeview):

        """Called from callback in self.setup_options_ffmpeg_list_tab().

        Opens an edit window for the selected
        ffmpeg_tartube.FFmpegOptionsManager.

        Args:

            button (Gtk.Button): The widget clicked

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if path_list:

            # (Multiple selection is not enabled)
            this_iter = model.get_iter(path_list[0])
            if this_iter is not None:

                uid = model[this_iter][0]
                if uid in self.app_obj.ffmpeg_reg_dict:

                    FFmpegOptionsEditWin(
                        self.app_obj,
                        self.app_obj.ffmpeg_reg_dict[uid],
                    )


    def on_ffmpeg_export_button_clicked(self, button, treeview):

        """Called from callback in self.setup_options_ffmpeg_list_tab().

        Exports the selected ffmpeg_tartube.FFmpegOptionsManager object to a
        JSON file.

        Args:

            button (Gtk.Button): The widget clicked

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if path_list:

            # (Multiple selection is not enabled)
            this_iter = model.get_iter(path_list[0])
            if this_iter is not None:

                uid = model[this_iter][0]
                if uid in self.app_obj.ffmpeg_reg_dict:

                    self.app_obj.export_ffmpeg_options(
                        self.app_obj.ffmpeg_reg_dict[uid],
                    )


    def on_ffmpeg_import_button_clicked(self, button, entry):

        """Called from callback in self.setup_options_ffmpeg_list_tab().

        Imports a JSON file and creates a new
        ffmpeg_tartube.FFmpegOptionsManager for it.

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Widget providiing a name for the new object.
                If the entry is empty, then the name specified by the JSON file
                itself is used

        """

        self.app_obj.import_ffmpeg_options(entry.get_text())

        # Update the treeview
        self.setup_options_ffmpeg_list_tab_update_treeview()
        # Empty the entry box
        entry.set_text('')


    def on_ffmpeg_retain_flag_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_downloads_tab().

        Enables/disables conversion of .webp thumbnails into .jpg thumbnails.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ffmpeg_retain_webp_flag:
            self.app_obj.set_ffmpeg_retain_webp_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ffmpeg_retain_webp_flag:
            self.app_obj.set_ffmpeg_retain_webp_flag(False)


    def on_ffmpeg_use_button_clicked(self, button, treeview):

        """Called from callback in self.setup_options_ffmpeg_list_tab().

        Sets the selected ffmpeg_tartube.FFmpegOptionsManager object as the
        current one.

        Args:

            button (Gtk.Button): The widget clicked

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if path_list:

            # (Multiple selection is not enabled)
            this_iter = model.get_iter(path_list[0])
            if this_iter is not None:

                uid = model[this_iter][0]
                if uid in self.app_obj.ffmpeg_reg_dict:

                    self.app_obj.set_ffmpeg_options_obj(
                        self.app_obj.ffmpeg_reg_dict[uid],
                    )

        # Update the treeview
        self.setup_options_ffmpeg_list_tab_update_treeview()


    def on_filter_options_button_toggled(self, checkbutton):

        """Called from callback in self.setup_downloader_forks_tab().

        Sets the flag to filter out yt-dlp options, when using a fork.

        Args:

            checkbutton (Gtk.Checkbutton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ytdlp_filter_options_flag:
            self.app_obj.set_ytdlp_filter_options_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ytdlp_filter_options_flag:
            self.app_obj.set_ytdlp_filter_options_flag(False)


    def on_hide_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_main_window_tab().

        Enables/disables hiding finishe media data objects in the Progress
        List.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        main_win_obj = self.app_obj.main_win_obj
        other_flag = main_win_obj.hide_finished_checkbutton.get_active()

        if (checkbutton.get_active() and not other_flag):
            main_win_obj.hide_finished_checkbutton.set_active(True)
        elif (not checkbutton.get_active() and other_flag):
            main_win_obj.hide_finished_checkbutton.set_active(False)


    def on_hide_toolbar_button_toggled(self, checkbutton, checkbutton2):

        """Called from callback in self.setup_windows_main_window_tab().

        Enables/disables hiding the main window's main toolbar.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2 (Gtk.CheckButton): Another checkbutton to modify
                (if it exists)

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Windows > Main Window'
        )

        if checkbutton.get_active() \
        and not self.app_obj.toolbar_hide_flag:
            self.app_obj.set_toolbar_hide_flag(True)

            if not self.app_obj.simple_prefs_flag:
                checkbutton2.set_sensitive(False)

        elif not checkbutton.get_active() \
        and self.app_obj.toolbar_hide_flag:
            self.app_obj.set_toolbar_hide_flag(False)

            if not self.app_obj.simple_prefs_flag:
                checkbutton2.set_sensitive(True)

        self.app_obj.dialogue_manager_obj.show_msg_dialogue(
            _('The new setting will be applied when Tartube restarts'),
            'info',
            'ok',
            self,           # Parent window is this window
        )


    def on_http_404_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_ignore_tab().

        Enables/disables ignoring of HTTP 404 error messages.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ignore_http_404_error_flag:
            self.app_obj.set_ignore_http_404_error_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ignore_http_404_error_flag:
            self.app_obj.set_ignore_http_404_error_flag(False)


    def on_invidious_mirror_changed(self, entry):

        """Called from callback in self.on_invidious_mirror_changed().

        Sets the Invidious mirror to use.

        Args:

            entry (Gtk.Entry): The widget changed

        """

        self.app_obj.set_custom_invidious_mirror(entry.get_text())


    def on_json_button_toggled(self, checkbutton, spinbutton, spinbutton2):

        """Called from callback in self.setup_operations_downloads_tab().

        Enables/disables applying a timeout when fetching a video's JSON data.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            spinbutton, spinbutton2 (Gtk.SpinButton): Other widgets to modify

        """

        if checkbutton.get_active() \
        and not self.app_obj.apply_json_timeout_flag:
            self.app_obj.set_apply_json_timeout_flag(True)
            spinbutton.set_sensitive(True)
            spinbutton2.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.apply_json_timeout_flag:
            self.app_obj.set_apply_json_timeout_flag(False)
            spinbutton.set_sensitive(False)
            spinbutton2.set_sensitive(False)


    def on_keep_open_button_toggled(self, checkbutton, checkbutton2):

        """Called from a callback in self.setup_windows_dialogues_tab().

        Enables/disables keeping the dialogue window open when adding channels/
        playlists/folders.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2 (Gtk.CheckButton): Another checkbutton to sensitise/
                desensitise, according to the new value of the flag

        """

        if checkbutton.get_active() \
        and not self.app_obj.dialogue_keep_open_flag:
            self.app_obj.set_dialogue_keep_open_flag(True)
            checkbutton2.set_sensitive(False)

        elif not checkbutton.get_active() \
        and self.app_obj.dialogue_keep_open_flag:
            self.app_obj.set_dialogue_keep_open_flag(False)
            checkbutton2.set_sensitive(True)


    def on_limit_button_toggled(
        self, checkbutton, entry, entry2, checkbutton2
    ):
        """Called from callback in self.setup_operations_block_tab().

        Sets the limit at which a download operation will stop downloading a
        channel or playlist.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            entry, entry2 (Gtk.Entry): The entry boxes which must be
                sensitised/desensitised, according to the new setting of the IV

            checkbutton2 (Gtk.CheckButton): Another widget to be updated

        """

        if checkbutton.get_active() and not self.app_obj.operation_limit_flag:
            self.app_obj.set_operation_limit_flag(True)
            entry.set_sensitive(True)
            entry2.set_sensitive(True)
            checkbutton2.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.operation_limit_flag:
            self.app_obj.set_operation_limit_flag(False)
            entry.set_sensitive(False)
            entry2.set_sensitive(False)
            checkbutton2.set_active(False)
            checkbutton2.set_sensitive(False)


    def on_limit_range_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_block_tab().

        Counts youtube-dl 'date is not in range' messages towards the download
        limit for a channel or playlist.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.operation_limit_include_out_of_range_flag:
            self.app_obj.set_operation_limit_include_out_of_range_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.operation_limit_include_out_of_range_flag:
            self.app_obj.set_operation_limit_include_out_of_range_flag(False)



    def on_livestream_auto_alarm_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_actions_tab().

        Enables/disables sounding an alarm when a livestream starts.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.livestream_auto_alarm_flag:
            self.app_obj.set_livestream_auto_alarm_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.livestream_auto_alarm_flag:
            self.app_obj.set_livestream_auto_alarm_flag(False)


    def on_livestream_auto_dl_start_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_actions_tab().

        Enables/disables downloading a livestream as soon as it starts.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.livestream_auto_dl_start_flag:
            self.app_obj.set_livestream_auto_dl_start_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.livestream_auto_dl_start_flag:
            self.app_obj.set_livestream_auto_dl_start_flag(False)


    def on_livestream_auto_dl_stop_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_actions_tab().

        Enables/disables downloading a livestream as soon as it stops.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.livestream_auto_dl_stop_flag:
            self.app_obj.set_livestream_auto_dl_stop_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.livestream_auto_dl_stop_flag:
            self.app_obj.set_livestream_auto_dl_stop_flag(False)


    def on_livestream_auto_notify_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_actions_tab().

        Enables/disables desktop notifications when a livestream starts.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.livestream_auto_notify_flag:
            self.app_obj.set_livestream_auto_notify_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.livestream_auto_notify_flag:
            self.app_obj.set_livestream_auto_notify_flag(False)


    def on_livestream_auto_open_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_actions_tab().

        Enables/disables opening a livestream in the system's web browser when
        it starts.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.livestream_auto_open_flag:
            self.app_obj.set_livestream_auto_open_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.livestream_auto_open_flag:
            self.app_obj.set_livestream_auto_open_flag(False)


    def on_livestream_colour_button_toggled(self, checkbutton, checkbutton2):

        """Called from callback in self.setup_windows_videos_tab().

        Enables/disables coloured backgrounds for livestream videos in the
        Video Catalogue.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2 (Gtk.CheckButton): Another widget to modify

        """

        if checkbutton.get_active() \
        and not self.app_obj.livestream_use_colour_flag:
            self.app_obj.set_livestream_use_colour_flag(True)
            checkbutton2.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.livestream_use_colour_flag:
            self.app_obj.set_livestream_use_colour_flag(False)
            checkbutton2.set_sensitive(False)

        # Redraw the Video Catalogue, at its current page, to update the
        #   backgrounds
        main_win_obj = self.app_obj.main_win_obj
        main_win_obj.video_catalogue_redraw_all(
            main_win_obj.video_index_current_dbid,
            main_win_obj.catalogue_toolbar_current_page,
        )


    def on_livestream_force_check_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_livestreams_tab().

        Enables/disables force check of a video, before downloading it as a
        livestream.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.livestream_force_check_flag:
            self.app_obj.set_livestream_force_check_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.livestream_force_check_flag:
            self.app_obj.set_livestream_force_check_flag(False)


    def on_livestream_max_days_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_operations_livestreams_tab().

        Sets the time (in days) at which Tartube stops looking for livestreams.

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        self.app_obj.set_livestream_max_days(
            spinbutton.get_value(),
        )


    def on_livestream_mode_button_toggled(self, radiobutton, value):

        """Called from callback in self.setup_operations_livestreams_tab().

        Sets the livestream download mode.

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

            radiobutton2, radiobutton3 (Gtk.RadioButton): Other widgets to
                modify

            value (str): The new value of the IV

        """

        if radiobutton.get_active():
            self.app_obj.set_livestream_dl_mode(value)

            if not self.app_obj.simple_prefs_flag:

                if self.app_obj.livestream_dl_mode == 'streamlink':
                    self.livestream_radiobutton4.set_active(True)
                    self.livestream_radiobutton5.set_sensitive(False)
                else:
                    self.livestream_radiobutton5.set_sensitive(True)


    def on_livestream_replace_button_toggled(self, radiobutton):

        """Called from callback in self.setup_operations_livestreams_tab().

        Enables/disables replacing a previously-downloaded livestream.

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

        """

        if radiobutton.get_active():
            self.app_obj.set_livestream_replace_flag(True)
        else:
            self.app_obj.set_livestream_replace_flag(False)


    def on_livestream_simple_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_videos_tab().

        Enables/disables using the same background colour for livestream and
        debut videos.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.livestream_simple_colour_flag:
            self.app_obj.set_livestream_simple_colour_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.livestream_simple_colour_flag:
            self.app_obj.set_livestream_simple_colour_flag(False)

        # Redraw the Video Catalogue, at its current page, to update the
        #   backgrounds
        main_win_obj = self.app_obj.main_win_obj
        main_win_obj.video_catalogue_redraw_all(
            main_win_obj.video_index_current_dbid,
            main_win_obj.catalogue_toolbar_current_page,
        )


    def on_livestream_stop_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_livestreams_tab().

        Enables/disables marking a stopped livestream as downloaded.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.livestream_stop_is_final_flag:
            self.app_obj.set_livestream_stop_is_final_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.livestream_stop_is_final_flag:
            self.app_obj.set_livestream_stop_is_final_flag(False)


    def on_livestream_timeout_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_operations_livestreams_tab().

        Sets the timeout (in minutes) for livestream downloads.

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        self.app_obj.set_livestream_dl_timeout(
            spinbutton.get_value(),
        )


    def on_load_descrips_button_clicked(self, button, spinbutton):

        """Called from a callback in self.setup_files_update_tab().

        For every video in the database, updates the description from the
        .description file.

        Args:

            button (Gtk.Button): The widget clicked

            spinbutton (Gtk.SpinButton): Widget setting the maximum line
                length

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Files > Update'
        )

        video_count = 0
        update_count = 0

        for media_data_obj in self.app_obj.media_reg_dict.values():

            if isinstance(media_data_obj, media.Video):

                video_count += 1

                old_descrip = media_data_obj.descrip

                media_data_obj.read_video_descrip(
                    self.app_obj,
                    int(spinbutton.get_value()),
                )

                if old_descrip != media_data_obj.descrip:
                    update_count += 1

        # Confirm the result
        msg = _('Total videos:') + ' ' + str(video_count) + '\n\n' \
        + _('Videos updated:') + ' ' + str(update_count)

        self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue(
            msg,
            'info',
            'ok',
            self,           # Parent window is this window
        )


    def on_locale_combo_changed(self, combo, grid):

        """Called from a callback in self.setup_general_language_tab().

        Sets the override locale for Tartube.

        Args:

            combo (Gtk.ComboBox): The widget clicked

            grid (Gtk.Grid): The grid on which this tab's widgets are
                arranged

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > General > Application'
        )

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        locale = model[tree_iter][2]

        if locale == '':
            self.app_obj.reset_override_local()
        else:
            self.app_obj.set_override_local(locale)

        # Add some more widgets to tell the user to restart Tartube
        # As the user might not know the language, show an icon as well as
        #   some text
        # Use an extra grid to avoid messing up the layout of widgets above
        grid2 = self.add_secondary_grid(grid, 0, 4, 3, 1)
        grid2.set_border_width(self.spacing_size * 2)

        frame = self.add_image(grid2,
            self.app_obj.main_win_obj.icon_dict['warning_large'],
            0, 0, 1, 1,
        )
        # (The frame looks cramped without this. The icon itself is 32x32)
        frame.set_size_request(
            32 + (self.spacing_size * 2),
            32 + (self.spacing_size * 2),
        )

        self.add_label(grid2,
            '<i>' + _(
                'The new setting will be applied when Tartube restarts',
            ) + '</i>',
            1, 0, 1, 1,
        )

        self.show_all()


    def on_log_json_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_output_log_tab().

        Enables/disables writing output from youtube-dl's STDOUT to the
        downloader log.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ytdl_log_ignore_json_flag:
            self.app_obj.set_ytdl_log_ignore_json_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ytdl_log_ignore_json_flag:
            self.app_obj.set_ytdl_log_ignore_json_flag(False)


    def on_log_progress_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_output_log_tab().

        Enables/disables writing output from youtube-dl's STDOUT to the
        downloader log.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ytdl_log_ignore_progress_flag:
            self.app_obj.set_ytdl_log_ignore_progress_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ytdl_log_ignore_progress_flag:
            self.app_obj.set_ytdl_log_ignore_progress_flag(False)


    def on_log_stderr_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_output_log_tab().

        Enables/disables writing output from youtube-dl's STDERR to the
        downloader log.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ytdl_log_stderr_flag:
            self.app_obj.set_ytdl_log_stderr_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ytdl_log_stderr_flag:
            self.app_obj.set_ytdl_log_stderr_flag(False)


    def on_log_stdout_button_toggled(self, checkbutton, checkbutton2, \
    checkbutton3):

        """Called from a callback in self.setup_output_log_tab().

        Enables/disables writing output from youtube-dl's STDOUT to the
        downloader log.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2, checkbutton3 (Gtk.CheckButton): Additional
                checkbuttons to sensitise/desensitise, according to the new
                value of the flag

        """

        if checkbutton.get_active() \
        and not self.app_obj.ytdl_log_stdout_flag:
            self.app_obj.set_ytdl_log_stdout_flag(True)
            checkbutton2.set_sensitive(True)
            checkbutton3.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.ytdl_log_stdout_flag:
            self.app_obj.set_ytdl_log_stdout_flag(False)
            checkbutton2.set_sensitive(False)
            checkbutton3.set_sensitive(False)


    def on_log_system_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_output_log_tab().

        Enables/disables writing youtube-dl system commands to the downloader
        log.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ytdl_log_system_cmd_flag:
            self.app_obj.set_ytdl_log_system_cmd_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ytdl_log_system_cmd_flag:
            self.app_obj.set_ytdl_log_system_cmd_flag(False)


    def on_match_button_toggled(self, radiobutton):

        """Called from callback in self.setup_files_video_deletion_tab().

        Updates IVs in the main application and sensities/desensities widgets.

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

        """

        default_val = self.app_obj.match_default_chars

        if radiobutton.get_active():

            if radiobutton == self.radiobutton3:
                self.app_obj.set_match_method('exact_match')
                # (Changing the contents of the widgets automatically updates
                #   mainapp.TartubeApp IVs)
                self.spinbutton3.set_value(default_val)
                self.spinbutton3.set_sensitive(False)
                self.spinbutton4.set_value(default_val)
                self.spinbutton4.set_sensitive(False)

            elif radiobutton == self.radiobutton4:
                self.app_obj.set_match_method('match_first')
                self.spinbutton3.set_sensitive(True)
                self.spinbutton4.set_value(default_val)
                self.spinbutton4.set_sensitive(False)

            else:
                self.app_obj.set_match_method('ignore_last')
                self.spinbutton3.set_value(default_val)
                self.spinbutton3.set_sensitive(False)
                self.spinbutton4.set_sensitive(True)


    def on_match_nickname_button_toggled(self, checkbutton):

        """Called from callback in self.setup_files_videos_tab().

        Enables/disables matching against both media.Video.name and
        media.Video.nickname.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2 (Gtk.CheckButton): Another widget to be modified

        """

        if checkbutton.get_active() \
        and not self.app_obj.match_nickname_flag:
            self.app_obj.set_match_nickname_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.match_nickname_flag:
            self.app_obj.set_match_nickname_flag(False)


    def on_match_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_files_video_deletion_tab().

        Updates IVs in the main application and sensities/desensities widgets.

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        if spinbutton == self.spinbutton3:
            self.app_obj.set_match_first_chars(spinbutton.get_value())
        else:
            self.app_obj.set_match_ignore_chars(spinbutton.get_value())


    def on_merge_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_ignore_tab().

        Enables/disables ignoring of 'Requested formats are incompatible for
        merge and will be merged into mkv' warning messages.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ignore_merge_warning_flag:
            self.app_obj.set_ignore_merge_warning_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ignore_merge_warning_flag:
            self.app_obj.set_ignore_merge_warning_flag(False)


    def on_missing_format_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_ignore_tab().

        Enables/disables ignoring of missing format error messages.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ignore_missing_format_error_flag:
            self.app_obj.set_ignore_missing_format_error_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ignore_missing_format_error_flag:
            self.app_obj.set_ignore_missing_format_error_flag(False)


    def on_missing_time_button_toggled(self, checkbutton, spinbutton):

        """Called from callback in self.setup_operations_prefs_tab().

        Enables/disables a time limit when tracking videos missing from a
        channel/playlist.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            spinbutton (Gtk.SpinButton): Another widget to modify

        """

        if checkbutton.get_active() \
        and not self.app_obj.track_missing_time_flag:

            self.app_obj.set_track_missing_time_flag(True)
            spinbutton.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.track_missing_time_flag:
            self.app_obj.set_track_missing_time_flag(False)
            spinbutton.set_sensitive(False)


    def on_missing_time_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_operations_prefs_tab().

        Sets a time limit when tracking videos missing from a channel/playlist.

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        self.app_obj.set_track_missing_time_days(
            spinbutton.get_value(),
        )


    def on_missing_videos_button_toggled(self, checkbutton, checkbutton2, \
    spinbutton):

        """Called from callback in self.setup_operations_prefs_tab().

        Enables/disables tracking videos missing from a channel/playlist.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2 (Gtk.CheckButton): Another widget to modify

            spinbutton (Gtk.SpinButton): Another widget to modify

        """

        if checkbutton.get_active() \
        and not self.app_obj.track_missing_videos_flag:

            self.app_obj.set_track_missing_videos_flag(True)
            checkbutton2.set_sensitive(True)
            if self.app_obj.track_missing_time_flag:
                spinbutton.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.track_missing_videos_flag:
            checkbutton2.set_active(False)
            self.app_obj.set_track_missing_videos_flag(False)
            checkbutton2.set_sensitive(False)
            spinbutton.set_sensitive(False)


    def on_move_container_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_windows_dialogues_tab().

        Enables/disables prompting the user before moving channels/playlists/
        folders in the Video Index.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.dialogue_move_container_flag:
            self.app_obj.set_dialogue_move_container_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.dialogue_move_container_flag:
            self.app_obj.set_dialogue_move_container_flag(False)


    def on_move_select_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_windows_dialogues_tab().

        Enables/disables selecting a channel/playlist/folder, after media has
        been moved into it.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.dialogue_move_select_flag:
            self.app_obj.set_dialogue_move_select_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.dialogue_move_select_flag:
            self.app_obj.set_dialogue_move_select_flag(False)


    def on_move_video_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_windows_dialogues_tab().

        Enables/disables prompting the user before moving videos in the Video
        Index.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.dialogue_move_video_flag:
            self.app_obj.set_dialogue_move_video_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.dialogue_move_video_flag:
            self.app_obj.set_dialogue_move_video_flag(False)


    def on_moviepy_button_toggled(self, checkbutton):

        """Called from callback in self.setup_general_modules_tab().

        Enables/disables use of the moviepy.editor module.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.use_module_moviepy_flag:
            self.app_obj.set_use_module_moviepy_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.use_module_moviepy_flag:
            self.app_obj.set_use_module_moviepy_flag(False)


    def on_moviepy_timeout_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_general_modules_tab().

        Sets the timeout to apply to threads using the moviepy module.

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        self.app_obj.set_refresh_moviepy_timeout(
            spinbutton.get_value(),
        )


    def on_nickname_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_videos_tab().

        Enables/disables showing video nicknames in the Video Catalogue.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.catalogue_show_nickname_flag:
            self.app_obj.set_catalogue_show_nickname_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.catalogue_show_nickname_flag:
            self.app_obj.set_catalogue_show_nickname_flag(False)


    def on_no_annotations_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_ignore_tab().

        Enables/disables ignoring of the 'no annotations' warning messages.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ignore_no_annotations_flag:
            self.app_obj.set_ignore_no_annotations_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ignore_no_annotations_flag:
            self.app_obj.set_ignore_no_annotations_flag(False)


    def on_no_subtitles_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_ignore_tab().

        Enables/disables ignoring of the 'no subtitles' warning messages.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ignore_no_subtitles_flag:
            self.app_obj.set_ignore_no_subtitles_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ignore_no_subtitles_flag:
            self.app_obj.set_ignore_no_subtitles_flag(False)


    def on_no_descrip_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_ignore_tab().

        Enables/disables ignoring of the 'no playlist description to write'
        warning messages.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ignore_no_descrip_flag:
            self.app_obj.set_ignore_no_descrip_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ignore_no_descrip_flag:
            self.app_obj.set_ignore_no_descrip_flag(False)


    def on_open_desktop_button_toggled(self, checkbutton):

        """Called from callback in self.setup_files_temp_folders_tab().

        Enables/disables opening temporary folders on the desktop when Tartube
        shuts down.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.open_temp_on_desktop_flag:
            self.app_obj.set_open_temp_on_desktop_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.open_temp_on_desktop_flag:
            self.app_obj.set_open_temp_on_desktop_flag(False)


    def on_open_in_tray_toggled(self, checkbutton):

        """Called from a callback in self.setup_windows_system_tray_tab().

        Enables/disables opening Tartube in the system tray.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.open_in_tray_flag:
            self.app_obj.set_open_in_tray_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.open_in_tray_flag:
            self.app_obj.set_open_in_tray_flag(False)


    def on_open_url_clicked(self, button, treeview):

        """Called from a callback in self.setup_files_urls_tab().

        Opens the URL in the selected line.

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeView): The parent treeview

        """

        # Get the media data objects for each selected line
        selection = treeview.get_selection()
        this_tuple = selection.get_selected_rows()
        # (Confusingly, first item in the tuple is the Gtk.ListStore)
        model = this_tuple[0]
        for path in this_tuple[1]:

            tree_iter = model.get_iter(path)
            if tree_iter is not None:

                dbid = model[tree_iter][0]
                if dbid in self.app_obj.media_reg_dict:

                    media_data_obj = self.app_obj.media_reg_dict[dbid]
                    if media_data_obj.source is not None:

                        ttutils.open_file(self.app_obj, media_data_obj.source)


    def on_operation_error_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_errors_warnings_tab().

        Enables/disables operation errors in the 'Errors/Warnings' tab.
        Toggling the corresponding Gtk.CheckButton in the Errors/Warnings tab
        sets the IV (and makes sure the two checkbuttons have the same status).

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        main_win_obj = self.app_obj.main_win_obj
        other_flag = main_win_obj.show_operation_error_checkbutton.get_active()

        main_win_obj = self.app_obj.main_win_obj
        if (checkbutton.get_active() and not other_flag):
            main_win_obj.show_operation_error_checkbutton.set_active(True)
        elif (not checkbutton.get_active() and other_flag):
            main_win_obj.show_operation_error_checkbutton.set_active(False)


    def on_operation_sim_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_downloads_tab().

        Enables/disables ignoring already-checked videos whose parent is a
        media.Folder, if the videos have already been checked.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.operation_sim_shortcut_flag:
            self.app_obj.set_operation_sim_shortcut_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.operation_sim_shortcut_flag:
            self.app_obj.set_operation_sim_shortcut_flag(False)


    def on_operation_warning_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_errors_warnings_tab().

        Enables/disables operation warnings in the 'Errors/Warnings' tab.
        Toggling the corresponding Gtk.CheckButton in the Errors/Warnings tab
        sets the IV (and makes sure the two checkbuttons have the same status).

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        main_win_obj = self.app_obj.main_win_obj
        other_flag \
        = main_win_obj.show_operation_warning_checkbutton.get_active()

        if (checkbutton.get_active() and not other_flag):
            main_win_obj.show_operation_warning_checkbutton.set_active(True)
        elif (not checkbutton.get_active() and other_flag):
            main_win_obj.show_operation_warning_checkbutton.set_active(False)


    def on_options_add_button_clicked(self, button, entry):

        """Called from callback in self.setup_options_dl_list_tab().

        Adds a new options.OptionsManager object.

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Widget providiing a name for the new object

        """

        name = entry.get_text()
        if name == '':
            return

        new_obj = self.app_obj.create_download_options(name)

        # If required, clone download options from the General Options Manager
        #   into the new object
        if self.app_obj.auto_clone_options_flag:
            new_obj.clone_options(self.app_obj.general_options_obj)

        # On the assumption that objects created here will be applied (mostly)
        #   in the Classic Mode tab, disable downloading the description,
        #   annotations (etc) files
        new_obj.set_classic_mode_options()

        # Update the treeview
        self.setup_options_dl_list_tab_update_treeview()
        # Empty the entry box
        entry.set_text('')

        # All other widgets for creating an options manager object open its
        #   edit window, so we'll do the same here
        OptionsEditWin(self.app_obj, new_obj)


    def on_options_clone_button_clicked(self, button, treeview):

        """Called from callback in self.setup_options_dl_list_tab().

        Clones the selected options.OptionsManager object.

        Args:

            button (Gtk.Button): The widget clicked

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if path_list:

            # (Multiple selection is not enabled)
            this_iter = model.get_iter(path_list[0])
            if this_iter is not None:

                uid = model[this_iter][0]
                if uid in self.app_obj.options_reg_dict:

                    new_obj = self.app_obj.clone_download_options(
                        self.app_obj.options_reg_dict[uid],
                    )

                    # Open an edit window, so the user can set the cloned
                    #   object's name
                    OptionsEditWin(self.app_obj, new_obj)


    def on_options_delete_button_clicked(self, button, treeview):

        """Called from callback in self.setup_options_dl_list_tab().

        Deletes the selected options.OptionsManager object, if allowed.

        Args:

            button (Gtk.Button): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Options > Download options'
        )

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if not path_list:

            return

        # (Multiple selection is not enabled)
        this_iter = model.get_iter(path_list[0])
        if this_iter is None:

            return

        uid = model[this_iter][0]
        if not uid in self.app_obj.options_reg_dict:
            return

        options_obj = self.app_obj.options_reg_dict[uid]
        if self.app_obj.general_options_obj \
        and self.app_obj.general_options_obj == options_obj:

            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('The default options manager cannot be deleted'),
                'error',
                'ok',
                self,           # Parent window is this window
            )

            return

        # Prompt for confirmation
        self.app_obj.dialogue_manager_obj.show_msg_dialogue(
            _('Are you sure you want to delete this options manager?'),
            'question',
            'yes-no',
            self,           # Parent window is this window
            {
                'yes': 'delete_download_options',
                # Specified options
                'data': options_obj,
            },
        )


    def on_options_edit_button_clicked(self, button, treeview):

        """Called from callback in self.setup_options_dl_list_tab().

        Opens an edit window for the selected options.OptionsManager object.

        Args:

            button (Gtk.Button): The widget clicked

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if path_list:

            # (Multiple selection is not enabled)
            this_iter = model.get_iter(path_list[0])
            if this_iter is not None:

                uid = model[this_iter][0]
                if uid in self.app_obj.options_reg_dict:

                    OptionsEditWin(
                        self.app_obj,
                        self.app_obj.options_reg_dict[uid],
                    )


    def on_options_export_button_clicked(self, button, treeview):

        """Called from callback in self.setup_options_dl_list_tab().

        Exports the selected options.OptionsManager object to a JSON file.

        Args:

            button (Gtk.Button): The widget clicked

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if path_list:

            # (Multiple selection is not enabled)
            this_iter = model.get_iter(path_list[0])
            if this_iter is not None:

                uid = model[this_iter][0]
                if uid in self.app_obj.options_reg_dict:

                    self.app_obj.export_download_options(
                        self.app_obj.options_reg_dict[uid],
                    )


    def on_options_import_button_clicked(self, button, entry):

        """Called from callback in self.setup_options_dl_list_tab().

        Imports a JSON file and creates a new options.OptionsManager for it.

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Widget providiing a name for the new object.
                If the entry is empty, then the name specified by the JSON file
                itself is used

        """

        self.app_obj.import_download_options(entry.get_text())

        # Update the treeview
        self.setup_options_dl_list_tab_update_treeview()
        # Empty the entry box
        entry.set_text('')


    def on_options_use_classic_button_clicked(self, button, treeview):

        """Called from callback in self.setup_options_dl_list_tab().

        Applies the selected options.OptionsManager object to the Classic Mode
        tab.

        Args:

            button (Gtk.Button): The widget clicked

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if path_list:

            # (Multiple selection is not enabled)
            this_iter = model.get_iter(path_list[0])
            if this_iter is not None:

                uid = model[this_iter][0]
                if uid in self.app_obj.options_reg_dict:

                    options_obj = self.app_obj.options_reg_dict[uid]

                    # If this is already the options manager for the Classic
                    #   Mode tab, then remove it (but don't delete the object
                    #   itself)
                    if self.app_obj.classic_options_obj \
                    and self.app_obj.classic_options_obj == options_obj:

                        self.app_obj.remove_classic_download_options()

                    else:

                        self.app_obj.apply_classic_download_options(
                            self.app_obj.options_reg_dict[uid],
                        )

        # Update the treeview
        self.setup_options_dl_list_tab_update_treeview()


    def on_output_empty_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_output_outputtab_tab().

        Enables/disables emptying pages in the Output tab at the start of every
        operation.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ytdl_output_start_empty_flag:
            self.app_obj.set_ytdl_output_start_empty_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ytdl_output_start_empty_flag:
            self.app_obj.set_ytdl_output_start_empty_flag(False)


    def on_output_json_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_output_outputtab_tab().

        Enables/disables writing output from youtube-dl's STDOUT to the Output
        tab.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ytdl_output_ignore_json_flag:
            self.app_obj.set_ytdl_output_ignore_json_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ytdl_output_ignore_json_flag:
            self.app_obj.set_ytdl_output_ignore_json_flag(False)


    def on_output_progress_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_output_outputtab_tab().

        Enables/disables writing output from youtube-dl's STDOUT to the Output
        tab.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ytdl_output_ignore_progress_flag:
            self.app_obj.set_ytdl_output_ignore_progress_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ytdl_output_ignore_progress_flag:
            self.app_obj.set_ytdl_output_ignore_progress_flag(False)


    def on_output_stderr_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_output_outputtab_tab().

        Enables/disables writing output from youtube-dl's STDERR to the Output
        tab.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ytdl_output_stderr_flag:
            self.app_obj.set_ytdl_output_stderr_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ytdl_output_stderr_flag:
            self.app_obj.set_ytdl_output_stderr_flag(False)


    def on_output_stdout_button_toggled(self, checkbutton, checkbutton2, \
    checkbutton3):

        """Called from a callback in self.setup_output_outputtab_tab().

        Enables/disables writing output from youtube-dl's STDOUT to the Output
        tab.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2, checkbutton3 (Gtk.CheckButton): Additional
                checkbuttons to sensitise/desensitise, according to the new
                value of the flag

        """

        if checkbutton.get_active() \
        and not self.app_obj.ytdl_output_stdout_flag:
            self.app_obj.set_ytdl_output_stdout_flag(True)
            checkbutton2.set_sensitive(True)
            checkbutton3.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.ytdl_output_stdout_flag:
            self.app_obj.set_ytdl_output_stdout_flag(False)
            checkbutton2.set_sensitive(False)
            checkbutton3.set_sensitive(False)


    def on_output_summary_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_output_outputtab_tab().

        Enables/disables displaying a summary page in the Output tab.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ytdl_output_show_summary_flag:
            self.app_obj.set_ytdl_output_show_summary_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ytdl_output_show_summary_flag:
            self.app_obj.set_ytdl_output_show_summary_flag(False)


    def on_output_size_button_toggled(self, checkbutton):

        """Called from callback in self.setup_output_outputtab_tab().

        Enables/disables applying a maximum size to the Output tab pages.
        Toggling the corresponding Gtk.CheckButton in the Output tab sets the
        IV (and makes sure the two checkbuttons have the same status).

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        other_flag \
        = self.app_obj.main_win_obj.output_size_checkbutton.get_active()

        if (checkbutton.get_active() and not other_flag):
            self.app_obj.main_win_obj.output_size_checkbutton.set_active(True)
        elif (not checkbutton.get_active() and other_flag):
            self.app_obj.main_win_obj.output_size_checkbutton.set_active(False)


    def on_output_size_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_output_outputtab_tab().

        Sets the maximum size of the Output tab pages. Setting the value of the
        corresponding Gtk.SpinButton in the Output tab sets the IV (and
        makes sure the two spinbuttons have the same value).

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        self.app_obj.main_win_obj.output_size_spinbutton.set_value(
            spinbutton.get_value(),
        )


    def on_output_system_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_output_outputtab_tab().

        Enables/disables writing youtube-dl system commands to the Output tab.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ytdl_output_system_cmd_flag:
            self.app_obj.set_ytdl_output_system_cmd_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ytdl_output_system_cmd_flag:
            self.app_obj.set_ytdl_output_system_cmd_flag(False)


    def on_page_given_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_ignore_tab().

        Enables/disables ignoring of the 'a channel/user page was given'
        warning messages.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ignore_page_given_flag:
            self.app_obj.set_ignore_page_given_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ignore_page_given_flag:
            self.app_obj.set_ignore_page_given_flag(False)


    def on_payment_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_websites_tab().

        Enables/disables ignoring of payment required error messages.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ignore_yt_payment_flag:
            self.app_obj.set_ignore_yt_payment_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ignore_yt_payment_flag:
            self.app_obj.set_ignore_yt_payment_flag(False)


    def on_pretty_date_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_videos_tab().

        Enables/disables 'today' and 'yesterday' rather than a numerical date
        in the Videos tab.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.show_pretty_dates_flag:
            self.app_obj.set_show_pretty_dates_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.show_pretty_dates_flag:
            self.app_obj.set_show_pretty_dates_flag(False)


    def on_proxy_textview_changed(self, textbuffer):

        """Called from callback in self.setup_operations_proxies_tab().

        Sets the list of proxies.

        Args:

            textbuffer (Gtk.TextBuffer): The buffer belonging to the textview
                whose contents has been modified

        """

        text = textbuffer.get_text(
            textbuffer.get_start_iter(),
            textbuffer.get_end_iter(),
            # Don't include hidden characters
            False,
        )

        # Filter out empty lines
        line_list = text.splitlines()
        mod_list = []
        for line in line_list:
            if re.search(r'\S', line):
                mod_list.append(line)

        # Apply the changes
        self.app_obj.set_dl_proxy_list(mod_list)


    def on_recalculate_stats_button_clicked(self, button, entry, entry2,
    entry3, entry4, entry5, entry6):

        """Called from callback in self.setup_files_statistics_tab().

        Recalculates the number of media data objects in the Tartube database,
        and updates the entry boxes.

        Args:

            button (Gtk.Button): The widget clicked

            entry, entry2, entry3, entry4, entry5, entry6 (Gtk.Entry): The
                entry boxes to update

        """

        self.setup_files_statistics_tab_recalculate(
            entry,
            entry2,
            entry3,
            entry4,
            entry5,
            entry6,
        )


    def on_reextract_stamps_flag_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_clips_tab().

        Enables/disables re-extracting timestamps just before splitting a
        video.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.video_timestamps_re_extract_flag:
            self.app_obj.set_video_timestamps_re_extract_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.video_timestamps_re_extract_flag:
            self.app_obj.set_video_timestamps_re_extract_flag(False)


    def on_refresh_verbose_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_output_outputtab_tab().

        Enables/disables displaying non-matching videos in the Output tab
        during a refresh operation.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.refresh_output_verbose_flag:
            self.app_obj.set_refresh_output_verbose_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.refresh_output_verbose_flag:
            self.app_obj.set_refresh_output_verbose_flag(False)


    def on_refresh_videos_button_toggled(self, checkbutton, checkbutton2):

        """Called from a callback in self.setup_output_outputtab_tab().

        Enables/disables displaying matching videos in the Output tab during a
        refresh operation.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2 (Gtk.CheckButton): A different checkbutton to
                sensitise/desensitise, according to the new value of the flag

        """

        if checkbutton.get_active() \
        and not self.app_obj.refresh_output_videos_flag:
            self.app_obj.set_refresh_output_videos_flag(True)
            checkbutton2.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.refresh_output_videos_flag:
            self.app_obj.set_refresh_output_videos_flag(False)
            checkbutton2.set_sensitive(False)


    def on_regex_button_toggled(self, radiobutton, flag):

        """Called from callback in self.setup_windows_websites_tab().

        Sets whether mainapp.TartubeApp.ignore_custom_msg_list contains
        ordinary strings or regexes.

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

            flag (bool): False for ordinary strings, True for regexes

        """

        if radiobutton.get_active():
            self.app_obj.set_ignore_custom_regex_flag(flag)


    def on_remember_size_button_toggled(self, checkbutton, checkbutton2):

        """Called from callback in self.setup_windows_main_window_tab().

        Enables/disables remembering the size of the main window.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2 (Gtk.CheckButton): Another widget to be modified

        """

        if checkbutton.get_active() \
        and not self.app_obj.main_win_save_size_flag:
            self.app_obj.set_main_win_save_size_flag(True)
            checkbutton2.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.main_win_save_size_flag:
            self.app_obj.set_main_win_save_size_flag(False)
            checkbutton2.set_sensitive(False)
            checkbutton2.set_active(False)


    def on_remember_slider_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_main_window_tab().

        Enables/disables remembering the position of sliders in the main
        window.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.main_win_save_slider_flag:
            self.app_obj.set_main_win_save_slider_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.main_win_save_slider_flag:
            self.app_obj.set_main_win_save_slider_flag(False)


    def on_remember_width_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_windows_main_window_tab().

        Enables/disables remembering the width of some columns in the Progress
        and Classic Mode tabs.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.progress_list_remember_width_flag:
            self.app_obj.set_progress_list_remember_width_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.progress_list_remember_width_flag:
            self.app_obj.set_progress_list_remember_width_flag(False)


    def on_remove_comments_button_clicked(self, button):

        """Called from callback in self.setup_files_update_tab().

        Clears comments from every video in the database.

        Args:

            button (Gtk.Button): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Files > Update'
        )

        video_count = 0
        success_count = 0

        for media_data_obj in self.app_obj.media_reg_dict.values():

            if isinstance(media_data_obj, media.Video):

                video_count += 1
                if media_data_obj.comment_list:

                    success_count += 1
                    media_data_obj.reset_comments()


        # Redraw the Video Catalogue, at its current page
        main_win_obj = self.app_obj.main_win_obj
        main_win_obj.video_catalogue_redraw_all(
            main_win_obj.video_index_current_dbid,
            main_win_obj.catalogue_toolbar_current_page,
        )

        # Confirm the result
        msg = _('Total videos:') + ' ' + str(video_count) + '\n\n' \
        + _('Videos updated:') + ' ' + str(success_count)

        self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue(
            msg,
            'info',
            'ok',
            self,           # Parent window is this window
        )


    def on_remove_container_file_button_toggled(self, checkbutton):

        """Called from callback in self.setup_files_delete_tab().

        Enables/disables removing all files from the filesystem when deleting
        containers manually.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.delete_container_files_flag:
            self.app_obj.set_delete_container_files_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.delete_container_files_flag:
            self.app_obj.set_delete_container_files_flag(False)


    def on_remove_duplicate_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_main_window_tab().

        Enables/disables removeing duplicate URLs from the Classic Mode tab,
        after the 'Add URLs' button is clicked.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.classic_duplicate_remove_flag:
            self.app_obj.set_classic_duplicate_remove_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.classic_duplicate_remove_flag:
            self.app_obj.set_classic_duplicate_remove_flag(False)


    def on_remove_stamps_button_clicked(self, button):

        """Called from callback in self.setup_files_update_tab().

        Clears timestamps from every video in the database.

        Args:

            button (Gtk.Button): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Files > Update'
        )

        video_count = 0
        success_count = 0

        for media_data_obj in self.app_obj.media_reg_dict.values():

            if isinstance(media_data_obj, media.Video):

                video_count += 1
                if media_data_obj.stamp_list:

                    success_count += 1
                    media_data_obj.reset_timestamps()


        # Redraw the Video Catalogue, at its current page
        main_win_obj = self.app_obj.main_win_obj
        main_win_obj.video_catalogue_redraw_all(
            main_win_obj.video_index_current_dbid,
            main_win_obj.catalogue_toolbar_current_page,
        )

        # Confirm the result
        msg = _('Total videos:') + ' ' + str(video_count) + '\n\n' \
        + _('Videos updated:') + ' ' + str(success_count)

        self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue(
            msg,
            'info',
            'ok',
            self,           # Parent window is this window
        )


    def on_remove_video_file_button_toggled(self, checkbutton):

        """Called from callback in self.setup_files_delete_tab().

        Enables/disables removing all files from the filesystem when deleting
        videos manually.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.delete_video_files_flag:
            self.app_obj.set_delete_video_files_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.delete_video_files_flag:
            self.app_obj.set_delete_video_files_flag(False)


    def on_replace_stamps_flag_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_clips_tab().

        Enables/disables replacing timestamps.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.video_timestamps_replace_flag:
            self.app_obj.set_video_timestamps_replace_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.video_timestamps_replace_flag:
            self.app_obj.set_video_timestamps_replace_flag(False)


    def on_reset_archive_button_clicked(self, button, entry):

        """Called from callback in self.setup_operations_archive_tab().

        Resets the path to the youtube-dl archive file.

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Another widget to be modified by this function

        """

        self.app_obj.set_allow_ytdl_archive_path(None)
        entry.set_text('')


    def on_reset_avconv_button_clicked(self, button, entry):

        """Called from callback in self.setup_ytdl_avconv_tab().

        Resets the path to the avconv binary.

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Another widget to be modified by this function

        """

        self.app_obj.set_avconv_path(None)
        entry.set_text('')


    def on_reset_ffmpeg_button_clicked(self, button, entry):

        """Called from callback in self.setup_downloader_ffmpeg_tab().

        Resets the path to the FFmpeg binary.

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Another widget to be modified by this function

        """

        self.app_obj.set_ffmpeg_path(None)
        entry.set_text('')


    def on_reset_invidious_clicked(self, button, entry):

        """Called from callback in self.setup_operations_mirrors_tab().

        Resets the Invidious mirror

        Args:

            entry (Gtk.Entry): The widget changed

            entry (Gtk.Entry): Another widget to update

        """

        self.app_obj.reset_custom_invidious_mirror()
        entry.set_text(self.app_obj.custom_invidious_mirror)


    def on_reset_sblock_clicked(self, button, entry):

        """Called from callback in self.setup_operations_mirrors_tab().

        Resets the URL of the SponsorBlock API.

        Args:

            button (Gtk.Button): The widget that was clicked

            entry (Gtk.Entry): Another widget to update

        """

        self.app_obj.reset_custom_sblock_mirror()
        entry.set_text(self.app_obj.custom_sblock_mirror)


    def on_reset_size_clicked(self, button):

        """Called from a callback in self.setup_windows_main_window_tab().

        Resets the main window to its default size, and repositions sliders to
        their default positions.

        Args:

            button (Gtk.Button): The widget clicked

        """

        self.app_obj.main_win_obj.resize_self(
            self.app_obj.main_win_width,
            self.app_obj.main_win_height,
        )

        self.app_obj.main_win_obj.reset_sliders()

        # Because of Gtk issues, the slider's position must be reset twice, the
        #   second time by mainapp.TartubeApp.script_fast_timer_callback()
        self.app_obj.set_main_win_slider_reset_flag(True)


    def on_reset_streamlink_button_clicked(self, button, entry):

        """Called from callback in self.setup_downloader_streamlink_tab().

        Resets the path to the streamlink binary.

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Another widget to be modified by this function

        """

        self.app_obj.set_streamlink_path(None)
        entry.set_text('')


    def on_restore_from_tray_toggled(self, checkbutton):

        """Called from a callback in self.setup_windows_system_tray_tab().

        Enables/disables restoring the window's position after closing it to
        the system tray.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.restore_posn_from_tray_flag:
            self.app_obj.set_restore_posn_from_tray_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.restore_posn_from_tray_flag:
            self.app_obj.set_restore_posn_from_tray_flag(False)


    def on_reverse_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_main_window_tab().

        Enables/disables display of videos in the Results List in the reverse
        order.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        main_win_obj = self.app_obj.main_win_obj
        other_flag = main_win_obj.reverse_results_checkbutton.get_active()

        if (checkbutton.get_active() and not other_flag):
            main_win_obj.reverse_results_checkbutton.set_active(True)
        elif (not checkbutton.get_active() and other_flag):
            main_win_obj.reverse_results_checkbutton.set_active(False)


    def on_save_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_downloads_tab().

        Enables/disables automatic saving of files at the end of a download/
        update/refresh/info/tidy operation.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() and not self.app_obj.operation_save_flag:
            self.app_obj.set_operation_save_flag(True)
        elif not checkbutton.get_active() and self.app_obj.operation_save_flag:
            self.app_obj.set_operation_save_flag(False)


    def on_sblock_fetch_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_slices_tab().

        Enables/disables contacting the SponsorBlock server.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.sblock_fetch_flag:
            self.app_obj.set_sblock_fetch_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.sblock_fetch_flag:
            self.app_obj.set_sblock_fetch_flag(False)


    def on_sblock_obfuscate_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_slices_tab().

        Enables/disables obfuscating video IDs in requests to the SponsorBlock
        server.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.sblock_obfuscate_flag:
            self.app_obj.set_sblock_obfuscate_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.sblock_obfuscate_flag:
            self.app_obj.set_sblock_obfuscate_flag(False)


    def on_sblock_replace_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_slices_tab().

        Enables/disables replacing any previous set of video slices.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.sblock_replace_flag:
            self.app_obj.set_sblock_replace_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.sblock_replace_flag:
            self.app_obj.set_sblock_replace_flag(False)


    def on_sblock_re_extract_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_slices_tab().

        Enables/disables re-extracting SponsorBlock before removing slices
        from a video.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.sblock_re_extract_flag:
            self.app_obj.set_sblock_re_extract_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.sblock_re_extract_flag:
            self.app_obj.set_sblock_re_extract_flag(False)


    def on_slice_cleanup_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_slices_tab().

        Enables/disables clearing timestamp/slice data from a video, after it
        has had its video slices removed.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.slice_video_cleanup_flag:
            self.app_obj.set_slice_video_cleanup_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.slice_video_cleanup_flag:
            self.app_obj.set_slice_video_cleanup_flag(False)


    def on_sblock_mirror_changed(self, entry):

        """Called from callback in self.setup_operations_mirrors_tab().

        Sets the SponsorBlock API mirror to use.

        Args:

            entry (Gtk.Entry): The widget changed

        """

        self.app_obj.set_custom_sblock_mirror(entry.get_text())


    def on_scheduled_add_button_clicked(self, button, entry):

        """Called from callback in self.setup_scheduling_start_tab().

        Adds a new media.Scheduled object, adds it to the treeview, and opens
        an edit window for it.

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): An entry containing the new object's name

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Scheduling > Start'
        )

        # Check the specified name is valid
        name = entry.get_text()
        if name == '':
            return

        for this_obj in self.app_obj.scheduled_list:
            if this_obj.name == name:

                self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                    _('There is already a scheduled download with that name'),
                    'error',
                    'ok',
                    self,           # Parent window is this window
                )

                return

        # Create a new scheduled download object
        new_obj = media.Scheduled(name, 'real', 'repeat')
        self.app_obj.add_scheduled_list(new_obj)

        # Add it to the treeview
        self.setup_scheduling_start_tab_add_row(new_obj)
        # Open an edit window for it
        ScheduledEditWin(self.app_obj, new_obj)
        # Reset the entry
        entry.set_text('')


    def on_scheduled_delete_button_clicked(self, button, treeview):

        """Called from callback in self.setup_scheduling_start_tab().

        Prompts the user, and then deletes the selected media.Scheduled object.

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeView): The treeview with a selected line

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Scheduling > Start'
        )

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        for path in path_list:

            this_iter = model.get_iter(path)
            name = model[this_iter][0]

            for scheduled_obj in self.app_obj.scheduled_list:
                if scheduled_obj.name == name:

                    # Prompt the user
                    self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                        _(
                        'Are you sure you want to delete this scheduled' \
                        + ' download?',
                        ),
                        'question',
                        'yes-no',
                        self,           # Parent window is this window
                        {
                            'yes': 'del_scheduled_list',
                            'data': [scheduled_obj, self],
                        },
                    )


    def on_scheduled_edit_button_clicked(self, button, treeview):

        """Called from callback in self.setup_scheduling_start_tab().

        Opens an edit window for the selected media.Scheduled object.

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeView): The treeview with a selected line

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        for path in path_list:

            this_iter = model.get_iter(path)
            name = model[this_iter][0]

            for scheduled_obj in self.app_obj.scheduled_list:
                if scheduled_obj.name == name:
                    ScheduledEditWin(self.app_obj, scheduled_obj)
                    break


    def on_scheduled_move_down_button_clicked(self, button, treeview):

        """Called from callback in self.setup_scheduling_start_tab().

        Moves the selected media.Scheduled object down one position in the
        list.

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeView): The treeview with a selected line

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        for path in path_list:

            this_iter = model.get_iter(path)
            if model.iter_next(this_iter):

                name = model[this_iter][0]
                self.app_obj.move_scheduled_list(name, True)

                model.move_after(
                    this_iter,
                    model.iter_next(this_iter),
                )


    def on_scheduled_move_up_button_clicked(self, button, treeview):

        """Called from callback in self.setup_scheduling_start_tab().

        Moves the selected media.Scheduled object up one position in the list.

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeView): The treeview with a selected line

        """

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        for path in path_list:

            this_iter = model.get_iter(path)
            if this_iter is not None:

                name = model[this_iter][0]
                self.app_obj.move_scheduled_list(name, False)

                model.move_before(
                    this_iter,
                    model.iter_previous(this_iter),
                )


    def on_scheduled_livestreams_button_toggled(self, checkbutton,
    checkbutton2, spinbutton):

        """Called from callback in self.setup_operations_livestreams_tab().

        Enables starting the livestream task periodically to check videos
        marked as livestreams.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2 (Gtk.CheckButton): Another widget to sensitise/
                desensitise, according to the new value of the flag

            spinbutton (Gtk.SpinButton): Another widget to sensitise/
                desensitise, according to the new value of the flag

        """

        if checkbutton.get_active() \
        and not self.app_obj.scheduled_livestream_flag:
            self.app_obj.set_scheduled_livestream_flag(True)
            spinbutton.set_sensitive(True)
            checkbutton2.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.scheduled_livestream_flag:
            self.app_obj.set_scheduled_livestream_flag(False)
            spinbutton.set_sensitive(False)
            checkbutton2.set_sensitive(False)
            checkbutton2.set_active(False)


    def on_scheduled_livestreams_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_operations_livestreams_tab().

        Sets the time (in minutes) between scheduled livestream operations.

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        self.app_obj.set_scheduled_livestream_wait_mins(
            spinbutton.get_value(),
        )


    def on_separator_combo_changed(self, combo):

        """Called from a callback in self.setup_files_backups_tab().

        Sets the CSV export separator.

        Args:

            combo (Gtk.ComboBox): The widget clicked

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        self.app_obj.set_export_csv_separator(model[tree_iter][0])


    def on_set_archive_button_clicked(self, button, entry):

        """Called from callback in self.setup_operations_archive_tab().

        Opens a window in which the user can select the path to the directory
        in which the youtube-dl archive file should be stored.

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Another widget to be modified by this function

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Operations > Archive'
        )

        dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser(
            _('Select the location of the archive file'),
            self,
            'folder',
        )

        response = dialogue_win.run()
        if response == Gtk.ResponseType.OK:
            new_path = dialogue_win.get_filename()

        dialogue_win.destroy()

        if response == Gtk.ResponseType.OK and new_path:

            self.app_obj.set_allow_ytdl_archive_path(new_path)
            entry.set_text(self.app_obj.allow_ytdl_archive_path)


    def on_set_avconv_button_clicked(self, button, entry):

        """Called from callback in self.setup_downloader_ffmpeg_tab().

        Opens a window in which the user can select the avconv binary, if it is
        installed (and if the user wants it).

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Another widget to be modified by this function

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Downloaders > FFmpeg / AVConv'
        )

        dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser(
            _('Please select the AVConv executable'),
            self,
            'open',
        )

        response = dialogue_win.run()
        if response == Gtk.ResponseType.OK:
            new_path = dialogue_win.get_filename()

        dialogue_win.destroy()

        if response == Gtk.ResponseType.OK and new_path:

            self.app_obj.set_avconv_path(new_path)
            entry.set_text(self.app_obj.avconv_path)


    def on_set_ffmpeg_button_clicked(self, button, entry):

        """Called from callback in self.setup_downloader_ffmpeg_tab().

        Opens a window in which the user can select the FFmpeg binary, if it is
        installed (and if the user wants it).

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Another widget to be modified by this function

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Downloaders > FFmpeg / AVConv'
        )

        dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser(
            _('Please select the FFmpeg executable'),
            self,
            'open',
        )

        response = dialogue_win.run()
        if response == Gtk.ResponseType.OK:
            new_path = dialogue_win.get_filename()

        dialogue_win.destroy()

        if response == Gtk.ResponseType.OK and new_path:

            self.app_obj.set_ffmpeg_path(new_path)
            entry.set_text(self.app_obj.ffmpeg_path)


    def on_set_streamlink_button_clicked(self, button, entry):

        """Called from callback in self.setup_downloader_streamlink_tab().

        Opens a window in which the user can select the streamlink binary, if
        it is installed (and if the user wants it).

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Another widget to be modified by this function

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Downloaders > streamlink'
        )

        dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser(
            _('Please select the streamlink executable'),
            self,
            'open',
        )

        response = dialogue_win.run()
        if response == Gtk.ResponseType.OK:
            new_path = dialogue_win.get_filename()

        dialogue_win.destroy()

        if response == Gtk.ResponseType.OK and new_path:

            self.app_obj.set_streamlink_path(new_path)
            entry.set_text(self.app_obj.streamlink_path)


    def on_show_classic_mode_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_windows_main_window_tab().

        Enables/disables automatically opening the Classic Mode tab on startup.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.show_classic_tab_on_startup_flag:
            self.app_obj.set_show_classic_tab_on_startup_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.show_classic_tab_on_startup_flag:
            self.app_obj.set_show_classic_tab_on_startup_flag(False)


    def on_show_custom_dl_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_main_window_tab().

        Enables/disables showing the 'Custom download all' button in the Videos
        tab.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.show_custom_dl_button_flag:
            self.app_obj.set_show_custom_dl_button_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.show_custom_dl_button_flag:
            self.app_obj.set_show_custom_dl_button_flag(False)

        # Abuse the main window code to either show or hide the button
        self.app_obj.main_win_obj.hide_progress_bar(True)


    def on_show_custom_icons_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_main_window_tab().

        Enables/disables replacing stock icons with custom icons.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Windows > Main Window'
        )

        if checkbutton.get_active() \
        and not self.app_obj.show_custom_icons_flag:
            self.app_obj.set_show_custom_icons_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.show_custom_icons_flag:
            self.app_obj.set_show_custom_icons_flag(False)

        self.app_obj.dialogue_manager_obj.show_msg_dialogue(
            _('The new setting will be applied when Tartube restarts'),
            'info',
            'ok',
            self,           # Parent window is this window
        )


    def on_show_delete_container_button_toggled(self, checkbutton):

        """Called from callback in self.setup_files_delete_tab().

        Enables/disables prompting the user before deleting containers.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.show_delete_container_dialogue_flag:
            self.app_obj.set_show_delete_container_dialogue_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.show_delete_container_dialogue_flag:
            self.app_obj.set_show_delete_container_dialogue_flag(False)


    def on_show_delete_video_button_toggled(self, checkbutton):

        """Called from callback in self.setup_files_delete_tab().

        Enables/disables prompting the user before deleting videos.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.show_delete_video_dialogue_flag:
            self.app_obj.set_show_delete_video_dialogue_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.show_delete_video_dialogue_flag:
            self.app_obj.set_show_delete_video_dialogue_flag(False)


    def on_show_free_space_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_main_window_tab().

        Enables/disables showing free disk space in the Videos tab.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.show_free_space_flag:
            self.app_obj.set_show_free_space_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.show_free_space_flag:
            self.app_obj.set_show_free_space_flag(False)


    def on_show_selector_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_main_window_tab().

        Enables/disables showing the selector button in each row of the Video
        Index.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.show_marker_in_index_flag:
            self.app_obj.set_show_marker_in_index_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.show_marker_in_index_flag:
            self.app_obj.set_show_marker_in_index_flag(False)


    def on_show_small_icons_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_videos_tab().

        Enables/disables smaller icons in the Video Index.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.show_small_icons_in_index_flag:
            self.app_obj.set_show_small_icons_in_index_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.show_small_icons_in_index_flag:
            self.app_obj.set_show_small_icons_in_index_flag(False)


    def on_show_status_icon_toggled(self, checkbutton, checkbutton2,
    checkbutton3, checkbutton4):

        """Called from a callback in self.setup_windows_system_tray_tab().

        Shows/hides the status icon in the system tray.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2, checkbutton3, checkbutton4 (Gtk.CheckButton): Other
                widgets to modify

        """

        if checkbutton.get_active() \
        and not self.app_obj.show_status_icon_flag:
            self.app_obj.set_show_status_icon_flag(True)
            checkbutton2.set_sensitive(True)
            checkbutton3.set_sensitive(True)
            if self.app_obj.close_to_tray_flag:
                checkbutton4.set_sensitive(True)
            else:
                checkbutton4.set_sensitive(False)

        elif not checkbutton.get_active() \
        and self.app_obj.show_status_icon_flag:
            self.app_obj.set_show_status_icon_flag(False)
            checkbutton2.set_active(False)
            checkbutton2.set_sensitive(False)
            checkbutton3.set_active(False)
            checkbutton3.set_sensitive(False)
            checkbutton4.set_active(False)
            checkbutton4.set_sensitive(False)


    def on_show_tooltips_toggled(self, checkbutton, checkbutton2):

        """Called from callback in self.setup_windows_main_window_tab().

        Enables/disables tooltips for videos/channels/playlists/folders.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2 (Gtk.CheckButton): Another widget to be modified

        """

        if checkbutton.get_active() \
        and not self.app_obj.show_tooltips_flag:
            self.app_obj.set_show_tooltips_flag(True)
            checkbutton2.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.show_tooltips_flag:
            self.app_obj.set_show_tooltips_flag(False)
            checkbutton2.set_sensitive(False)
            checkbutton2.set_active(False)


    def on_show_tooltips_extra_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_videos_tab().

        Enables/disables errors/warnings in tooltips.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.show_tooltips_extra_flag:
            self.app_obj.set_show_tooltips_extra_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.show_tooltips_extra_flag:
            self.app_obj.set_show_tooltips_extra_flag(False)


    def on_simple_prefs_clicked(self, button):

        """Called by callback in self.setup_general_application_tab().

        Args:

            button (Gtk.Button): The widget clicked

        """

        if not self.app_obj.simple_prefs_flag:
            self.app_obj.set_simple_prefs_flag(True)
        else:
            self.app_obj.set_simple_prefs_flag(False)

        self.reset_window()


    def on_slice_keyframe_flag_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_slices_tab().

        Enables/disables forced keyframes at cuts, when removing slices from
        videos.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.slice_video_force_keyframe_flag:
            self.app_obj.set_slice_video_force_keyframe_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.slice_video_force_keyframe_flag:
            self.app_obj.set_slice_video_force_keyframe_flag(False)


    def on_sound_custom_changed(self, combo):

        """Called from callback in self.setup_operations_actions_tab().

        Sets the user's preferred sound effect for livestream alarms.

        Args:

            combo (Gtk.ComboBox): The widget clicked

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        self.app_obj.set_sound_custom(model[tree_iter][0])


    def on_split_keyframe_flag_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_clips_tab().

        Enables/disables forced keyframes at cuts, when splitting videos into
        clips.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.split_video_force_keyframe_flag:
            self.app_obj.set_split_video_force_keyframe_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.split_video_force_keyframe_flag:
            self.app_obj.set_split_video_force_keyframe_flag(False)


    def on_split_mode_combo_changed(self, combo):

        """Called from a callback in self.setup_operations_clips_tab().

        Sets the mode for naming split video files.

        Args:

            combo (Gtk.ComboBox): The widget clicked

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        self.app_obj.set_split_video_name_mode(model[tree_iter][1])


    def on_split_subdir_flag_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_clips_tab().

        Enables/disables moving split files into a sub-directory.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.split_video_subdir_flag:
            self.app_obj.set_split_video_subdir_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.split_video_subdir_flag:
            self.app_obj.set_split_video_subdir_flag(False)


    def on_squeeze_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_main_window_tab().

        Enables/disables labels in the main window's main toolbar.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.toolbar_squeeze_flag:
            self.app_obj.set_toolbar_squeeze_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.toolbar_squeeze_flag:
            self.app_obj.set_toolbar_squeeze_flag(False)


    def on_store_playlist_id_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_downloads_tab().

        Enables/disables storing video's playlist IDs in the parent channel/
        playlist.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.store_playlist_id_flag:
            self.app_obj.set_store_playlist_id_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.store_playlist_id_flag:
            self.app_obj.set_store_playlist_id_flag(False)


    def on_system_container_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_errors_warnings_tab().

        Enables/disables showing container names in the Errors/Warnings tab.
        Toggling the corresponding Gtk.CheckButton in the Errors/Warnings tab
        sets the IV (and makes sure the two checkbuttons have the same status).

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        main_win_obj = self.app_obj.main_win_obj
        other_flag \
        = main_win_obj.show_system_container_checkbutton.get_active()

        if (checkbutton.get_active() and not other_flag):
            main_win_obj.show_system_container_checkbutton.set_active(True)
        elif (not checkbutton.get_active() and other_flag):
            main_win_obj.show_system_container_checkbutton.set_active(False)


    def on_system_date_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_errors_warnings_tab().

        Enables/disables showing dates (as well as times) in the Errors/
        Warnings tab. Toggling the corresponding Gtk.CheckButton in the Errors/
        Warnings tab sets the IV (and makes sure the two checkbuttons have the
        same status).

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        main_win_obj = self.app_obj.main_win_obj
        other_flag \
        = main_win_obj.show_system_date_checkbutton.get_active()

        if (checkbutton.get_active() and not other_flag):
            main_win_obj.show_system_date_checkbutton.set_active(True)
        elif (not checkbutton.get_active() and other_flag):
            main_win_obj.show_system_date_checkbutton.set_active(False)


    def on_system_multi_line_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_errors_warnings_tab().

        Enables/disables showing full error/warning messages in the Errors/
        Warnings tab. Toggling the corresponding Gtk.CheckButton in the Errors/
        Warnings tab sets the IV (and makes sure the two checkbuttons have the
        same status).

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        main_win_obj = self.app_obj.main_win_obj
        other_flag \
        = main_win_obj.show_system_multi_line_checkbutton.get_active()

        if (checkbutton.get_active() and not other_flag):
            main_win_obj.show_system_multi_line_checkbutton.set_active(True)
        elif (not checkbutton.get_active() and other_flag):
            main_win_obj.show_system_multi_line_checkbutton.set_active(False)


    def on_system_video_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_errors_warnings_tab().

        Enables/disables showing video names in the Errors/Warnings tab.
        Toggling the corresponding Gtk.CheckButton in the Errors/Warnings tab
        sets the IV (and makes sure the two checkbuttons have the same status).

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        main_win_obj = self.app_obj.main_win_obj
        other_flag \
        = main_win_obj.show_system_video_checkbutton.get_active()

        if (checkbutton.get_active() and not other_flag):
            main_win_obj.show_system_video_checkbutton.set_active(True)
        elif (not checkbutton.get_active() and other_flag):
            main_win_obj.show_system_video_checkbutton.set_active(False)


    def on_system_error_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_errors_warnings_tab().

        Enables/disables system errors in the 'Errors/Warnings' tab. Toggling
        the corresponding Gtk.CheckButton in the Errors/Warnings tab sets the
        IV (and makes sure the two checkbuttons have the same status).

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        main_win_obj = self.app_obj.main_win_obj
        other_flag = main_win_obj.show_system_error_checkbutton.get_active()

        if (checkbutton.get_active() and not other_flag):
            main_win_obj.show_system_error_checkbutton.set_active(True)
        elif (not checkbutton.get_active() and other_flag):
            main_win_obj.show_system_error_checkbutton.set_active(False)


    def on_system_keep_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_main_window_tab().

        Enables/disables keeping the total number of system messages in the tab
        label until the clear button is explicitly clicked.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.system_msg_keep_totals_flag:
            self.app_obj.set_system_msg_keep_totals_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.system_msg_keep_totals_flag:
            self.app_obj.set_system_msg_keep_totals_flag(False)


    def on_system_warning_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_errors_warnings_tab().

        Enables/disables system warnings in the 'Errors/Warnings' tab. Toggling
        the corresponding Gtk.CheckButton in the Errors/Warnings tab sets the
        IV (and makes sure the two checkbuttons have the same status).

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        main_win_obj = self.app_obj.main_win_obj
        other_flag = main_win_obj.show_system_warning_checkbutton.get_active()

        if (checkbutton.get_active() and not other_flag):
            main_win_obj.show_system_warning_checkbutton.set_active(True)
        elif (not checkbutton.get_active() and other_flag):
            main_win_obj.show_system_warning_checkbutton.set_active(False)


    def on_terminal_json_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_output_terminal_tab().

        Enables/disables writing output from youtube-dl's STDOUT to the
        terminal.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ytdl_write_ignore_json_flag:
            self.app_obj.set_ytdl_write_ignore_json_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ytdl_write_ignore_json_flag:
            self.app_obj.set_ytdl_write_ignore_json_flag(False)


    def on_terminal_progress_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_output_terminal_tab().

        Enables/disables writing output from youtube-dl's STDOUT to the
        terminal.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ytdl_write_ignore_progress_flag:
            self.app_obj.set_ytdl_write_ignore_progress_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ytdl_write_ignore_progress_flag:
            self.app_obj.set_ytdl_write_ignore_progress_flag(False)


    def on_terminal_stderr_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_output_terminal_tab().

        Enables/disables writing output from youtube-dl's STDERR to the
        terminal.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ytdl_write_stderr_flag:
            self.app_obj.set_ytdl_write_stderr_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ytdl_write_stderr_flag:
            self.app_obj.set_ytdl_write_stderr_flag(False)


    def on_terminal_stdout_button_toggled(self, checkbutton, checkbutton2, \
    checkbutton3):

        """Called from a callback in self.setup_output_terminal_tab().

        Enables/disables writing output from youtube-dl's STDOUT to the
        terminal.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            checkbutton2, checkbutton3 (Gtk.CheckButton): Additional
                checkbuttons to sensitise/desensitise, according to the new
                value of the flag

        """

        if checkbutton.get_active() \
        and not self.app_obj.ytdl_write_stdout_flag:
            self.app_obj.set_ytdl_write_stdout_flag(True)
            checkbutton2.set_sensitive(True)
            checkbutton3.set_sensitive(True)

        elif not checkbutton.get_active() \
        and self.app_obj.ytdl_write_stdout_flag:
            self.app_obj.set_ytdl_write_stdout_flag(False)
            checkbutton2.set_sensitive(False)
            checkbutton3.set_sensitive(False)


    def on_terminal_system_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_output_terminal_tab().

        Enables/disables writing youtube-dl system commands to the terminal.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ytdl_write_system_cmd_flag:
            self.app_obj.set_ytdl_write_system_cmd_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ytdl_write_system_cmd_flag:
            self.app_obj.set_ytdl_write_system_cmd_flag(False)


    def on_test_sound_clicked(self, button, combo):

        """Called from callback in self.setup_operations_actions_tab().

        Plays the sound effect selected in the combobox.

        Args:

            button (Gtk.Button): The widget that was clicked

            combo (Gtk.ComboBox): The widget in which a sound effect is
                selected

        """

        self.app_obj.play_sound()


    def on_thumb_404_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_ignore_tab().

        Enables/disables ignoring of the 'Unable to download video thumbnail'
        warning messages.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ignore_thumb_404_flag:
            self.app_obj.set_ignore_thumb_404_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ignore_thumb_404_flag:
            self.app_obj.set_ignore_thumb_404_flag(False)


    def on_timeout_no_comments_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_operations_downloads_tab().

        Sets the JSON timeout when not fetching comments.

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        self.app_obj.set_json_timeout_no_comments_time(spinbutton.get_value())


    def on_timeout_with_comments_spinbutton_changed(self, spinbutton):

        """Called from callback in self.setup_operations_downloads_tab().

        Sets the JSON timeout when fetching comments.

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

        """

        self.app_obj.set_json_timeout_with_comments_time(
            spinbutton.get_value(),
        )


    def on_twitch_live_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_ignore_tab().

        Enables/disables ignoring of the Twitch 'this channel is not live'
        error messages.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ignore_twitch_not_live_flag:
            self.app_obj.set_ignore_twitch_not_live_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ignore_twitch_not_live_flag:
            self.app_obj.set_ignore_twitch_not_live_flag(False)


    def on_update_combo_changed(self, combo):

        """Called from a callback in self.setup_downloader_paths_tab().

        Extracts the value visible in the combobox, converts it into another
        value, and uses that value to update the main application's IV.

        Args:

            combo (Gtk.ComboBox): The widget clicked

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        self.app_obj.set_ytdl_update_current(model[tree_iter][0])


    def on_uploader_button_toggled(self, checkbutton):

        """Called from callback in self.setup_windows_websites_tab().

        Enables/disables ignoring of deletion by uploader error messages.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ignore_yt_uploader_deleted_flag:
            self.app_obj.set_ignore_yt_uploader_deleted_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ignore_yt_uploader_deleted_flag:
            self.app_obj.set_ignore_yt_uploader_deleted_flag(False)


    def on_url_regex_button_toggled(self, checkbutton):

        """Called from callback in self.setup_files_urls_tab().

        Enables/disables treating the pattern as a regex, when searching/
        replacing URLs.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.url_change_regex_flag:
            self.app_obj.set_url_change_regex_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.url_change_regex_flag:
            self.app_obj.set_url_change_regex_flag(False)


    def on_use_first_button_toggled(self, checkbutton):

        """Called from callback in self.setup_files_database_tab().

        Enables/disables automatic loading of the first database file in the
        list.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.data_dir_use_first_flag:
            self.app_obj.set_data_dir_use_first_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.data_dir_use_first_flag:
            self.app_obj.set_data_dir_use_first_flag(False)


    def on_use_list_button_toggled(self, checkbutton):

        """Called from callback in self.setup_files_database_tab().

        Enables/disables automatic loading of an alternative database file, if
        the default one is locked.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.data_dir_use_list_flag:
            self.app_obj.set_data_dir_use_list_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.data_dir_use_list_flag:
            self.app_obj.set_data_dir_use_list_flag(False)


    def on_video_res_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_limits_tab().

        Enables/disables the video resolution limit. Toggling the corresponding
        Gtk.CheckButton in the Progress tab sets the IV (and makes sure the two
        checkbuttons have the same status).

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        other_flag \
        = self.app_obj.main_win_obj.video_res_checkbutton.get_active()

        if (checkbutton.get_active() and not other_flag):
            self.app_obj.main_win_obj.video_res_checkbutton.set_active(True)
        elif (not checkbutton.get_active() and other_flag):
            self.app_obj.main_win_obj.video_res_checkbutton.set_active(False)


    def on_video_res_combo_changed(self, combo):

        """Called from a callback in self.setup_operations_limits_tab().

        Extracts the value visible in the combobox, converts it into another
        value, and uses that value to update the main application's IV.

        Args:

            combo (Gtk.ComboBox): The widget clicked

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        self.app_obj.set_video_res_default(model[tree_iter][0])


    def on_worker_button_toggled(self, checkbutton, alt_flag=False):

        """Called from callback in self.setup_operations_limits_tab().

        Enables/disables the simultaneous download limit. Toggling the
        corresponding Gtk.CheckButton in the Progress tab sets the IV (and
        makes sure the two checkbuttons have the same status).

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            alt_flag (bool): If True, the alternative limit is toggled

        """

        main_win_obj = self.app_obj.main_win_obj

        if not alt_flag:

            other_flag = main_win_obj.num_worker_checkbutton.get_active()

            if (checkbutton.get_active() and not other_flag):
                main_win_obj.num_worker_checkbutton.set_active(True)
            elif (not checkbutton.get_active() and other_flag):
                main_win_obj.num_worker_checkbutton.set_active(False)

        else:

            # Alternative limits. There is no second widget to toggle
            if checkbutton.get_active() \
            and not self.app_obj.alt_num_worker_apply_flag:
                self.app_obj.set_alt_num_worker_apply_flag(True)
            elif not checkbutton.get_active() \
            and self.app_obj.alt_num_worker_apply_flag:
                self.app_obj.set_alt_num_worker_apply_flag(False)


    def on_worker_bypass_button_toggled(self, checkbutton):

        """Called from callback in self.setup_operations_livestreams_tab().

        Enables/disables bypassing the maximum simultaneous downloads limit
        when downloading broadcasting livestreams.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.num_worker_bypass_flag:
            self.app_obj.set_num_worker_bypass_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.num_worker_bypass_flag:
            self.app_obj.set_num_worker_bypass_flag(False)


    def on_worker_spinbutton_changed(self, spinbutton, alt_flag=False):

        """Called from callback in self.setup_operations_limits_tab().

        Sets the simultaneous download limit. Setting the value of the
        corresponding Gtk.SpinButton in the Progress tab sets the IV (and
        makes sure the two spinbuttons have the same value).

        Args:

            spinbutton (Gtk.SpinButton): The widget clicked

            alt_flag (bool): If True, the alternative limit is set

        """

        if not alt_flag:

            self.app_obj.main_win_obj.num_worker_spinbutton.set_value(
                spinbutton.get_value(),
            )

        else:

            # Alternative limits. There is no second widget to toggle
            self.app_obj.set_alt_num_worker(int(spinbutton.get_value()))


    def on_yt_remind_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_windows_dialogues_tab().

        Enables/disables reminding the user about the correct URL when adding
        YouTube channels.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.dialogue_yt_remind_flag:
            self.app_obj.set_dialogue_yt_remind_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.dialogue_yt_remind_flag:
            self.app_obj.set_dialogue_yt_remind_flag(False)


    def on_ytdl_fork_button_toggled(self, radiobutton, checkbutton, \
    fork_type=None):

        """Called from callback in self.setup_downloader_forks_tab().

        Sets the youtube-dl fork to be used. See also
        self.on_ytdl_fork_changed().

        Args:

            radiobutton (Gtk.Radiobutton): The widget clicked

            checkbutton (Gtk.CheckButton): Another widget to be updated

            fork_type (str): 'yt-dlp', 'youtube-dl', or None for any other fork

        """

        if radiobutton.get_active():

            if fork_type is None:

                fork_name = self.forks_entry.get_text()
                # (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.app_obj.set_ytdl_fork(None)
                else:
                    self.app_obj.set_ytdl_fork(fork_name)

                checkbutton.set_sensitive(False)
                self.forks_entry.set_sensitive(True)

            elif fork_type == 'youtube-dl':

                fork_name = fork_type
                self.app_obj.set_ytdl_fork(None)
                checkbutton.set_sensitive(False)
                self.forks_entry.set_text('')
                self.forks_entry.set_sensitive(False)

            elif fork_type == 'yt-dlp':

                fork_name = fork_type
                self.app_obj.set_ytdl_fork(fork_type)
                checkbutton.set_sensitive(True)
                self.forks_entry.set_text('')
                self.forks_entry.set_sensitive(False)

            # If the user has set a custom path to the youtube-dl executable
            #   that does not match 'fork_type', then the path must be reset
            if self.app_obj.ytdl_path_custom_flag:

                directory, fullname = os.path.split(self.app_obj.ytdl_path)
                filename, extension = os.path.splitext(fullname)
                if filename != fork_name:
                    self.filepaths_combo.set_active(0)


        self.update_ytdl_combos()


    def on_ytdl_fork_changed(self, entry):

        """Called from callback in self.setup_downloader_forks_tab().

        Sets the youtube-dl fork to be used. See also
        self.on_ytdl_fork_button_toggled().

        Args:

            entry (Gtk.Entry): The widget changed

        """

        if self.forks_radiobutton3.get_active():

            text = ttutils.strip_whitespace(entry.get_text())
            if text == '':

                self.app_obj.set_ytdl_fork(None)
                entry.set_icon_from_stock(
                    Gtk.EntryIconPosition.PRIMARY,
                    'gtk-yes',
                )

            else:

                # Git 466 - prevent the user from adding an absolute path;
                #   the value we're expecting is omething like 'youtube-dlc'
                if re.search(r'[^\w\-]', text):

                    entry.set_icon_from_stock(
                        Gtk.EntryIconPosition.PRIMARY,
                        'gtk-no',
                    )

                else:

                    self.app_obj.set_ytdl_fork(text)
                    entry.set_icon_from_stock(
                        Gtk.EntryIconPosition.PRIMARY,
                        'gtk-yes',
                    )

            self.update_ytdl_combos()


    def on_ytdl_fork_frame_clicked(self, event_box, event_button, radiobutton):

        """Called from a callback in self.setup_downloader_forks_tab().

        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)


    def on_ytdl_path_button_clicked(self, button, entry):

        """Called from callback in self.setup_downloader_paths_tab().

        Sets a custom path to the youtube-dl executable.

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Another widget to update

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Downloaders > File paths'
        )

        # Prompt the user for the new youtube-dl executable
        dialogue_win = self.app_obj.dialogue_manager_obj.show_file_chooser(
            _('Select the youtube-dl-compatible executable'),
            self,
            'open',
        )

        # (When the user first selects 'Use custom path', using the combobox,
        #   the default youtube-dl path continues to be used until they have
        #   specified a new path)
        if self.app_obj.ytdl_path != self.app_obj.ytdl_path_default:
            dialogue_win.set_current_folder(self.app_obj.ytdl_path)

        # Get the user's response
        response = dialogue_win.run()
        if response == Gtk.ResponseType.OK:
            new_path = dialogue_win.get_filename()

        dialogue_win.destroy()
        if response == Gtk.ResponseType.OK:

            # Update the name of the fork to match the path
            directory, fullname = os.path.split(new_path)
            filename, extension = os.path.splitext(fullname)
            self.app_obj.set_ytdl_fork(filename)

            # Update widgets in the calling ('File paths') tab
            entry.set_text(new_path)
            # Update widgets in the 'Forks' tab
            if filename == 'youtube-dl':
                self.forks_radiobutton.set_active(True)
            elif filename == 'yt-dlp':
                self.forks_radiobutton2.set_active(True)
            else:
                self.forks_radiobutton3.set_active(True)
                self.forks_entry.set_text(filename)

            # Update the path to the executable (this must be done last,
            #   otherwise widget updates will override each other)
            self.app_obj.set_ytdl_path(new_path)
            self.app_obj.ytdl_update_dict['ytdl_update_custom_path'] \
            = ['python3', self.app_obj.ytdl_path, '-U']


    def on_ytdl_path_combo_changed(self, combo, entry, button):

        """Called from a callback in self.setup_downloader_paths_tab().

        Extracts the value visible in the combobox, converts it into another
        value, and uses that value to update the main application's IV.

        Args:

            combo (Gtk.ComboBox): The widget clicked

            entry (Gtk.Entry): Another entry to check

            button (Gtk.Button): Another widget to modify

        """

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        ytdl_path = model[tree_iter][1]

        if ytdl_path is not None:

            self.app_obj.set_ytdl_path(ytdl_path)
            self.app_obj.set_ytdl_path_custom_flag(False)
            entry.set_text('')
            button.set_sensitive(False)

        else:

            # Custom youtube-dl path, set by the entry/button
            # Until the user has selected their own executable, use the default
            #   one
            self.app_obj.set_ytdl_path(self.app_obj.ytdl_path_default)
            self.app_obj.ytdl_update_dict['ytdl_update_custom_path'] \
            = ['python3', self.app_obj.ytdl_path, '-U']
            self.app_obj.set_ytdl_path_custom_flag(True)

            entry.set_text(self.app_obj.ytdl_path)
            button.set_sensitive(True)


    def on_ytdl_verbose_button_toggled(self, checkbutton):

        """Called from a callback in self.setup_output_general_tab().

        Enables/disables writing verbose output (youtube-dl debugging mode).

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ytdl_write_verbose_flag:
            self.app_obj.set_ytdl_write_verbose_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ytdl_write_verbose_flag:
            self.app_obj.set_ytdl_write_verbose_flag(False)


    def on_ytdlp_install_button_toggled(self, checkbutton):

        """Called from callback in self.setup_downloader_forks_tab().

        Sets the flag to install yt-dlp with or without dependencies.

        Args:

            checkbutton (Gtk.Checkbutton): The widget clicked

        """

        if checkbutton.get_active() \
        and not self.app_obj.ytdl_fork_no_dependency_flag:
            self.app_obj.set_ytdl_fork_no_dependency_flag(True)
        elif not checkbutton.get_active() \
        and self.app_obj.ytdl_fork_no_dependency_flag:
            self.app_obj.set_ytdl_fork_no_dependency_flag(False)


    # (Callback support functions)


    def try_switch_db(self, data_dir, button):

        """Called by self.on_data_dir_change_button_clicked() and
        .on_data_dir_switch_button_clicked().

        Having confirmed that a database directory specified by the user
        actually exists, attempt to load the database file inside it.

        Args:

            data_dir (str): The full path to the data directory

            button (Gtk.Button): A button to be possibly desensitised

        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by:' \
            + ' System preferences > Files > Database'
        )

        dialogue_manager_obj = self.app_obj.dialogue_manager_obj

        # Database file already exists, so try to load it now
        if not self.app_obj.switch_db([data_dir, self]):

            # Load failed, and the user chose to shut down Tartube
            if self.app_obj.disable_load_save_lock_flag:

                return self.app_obj.stop()

            # Load failed for any other reason
            elif self.app_obj.disable_load_save_flag:

                button.set_sensitive(False)

                if not self.app_obj.disable_load_save_lock_flag:

                    if self.app_obj.disable_load_save_msg is not None:

                        dialogue_win = dialogue_manager_obj.show_msg_dialogue(
                            self.app_obj.disable_load_save_msg,
                            'error',
                            'ok',
                            self,           # Parent window is this window
                        )

                    else:

                        dialogue_win = dialogue_manager_obj.show_msg_dialogue(
                            _('Database file not loaded'),
                            'error',
                            'ok',
                            self,           # Parent window is this window
                        )

                # When load/save is disabled, this preference window can't be
                #   opened
                # Therefore, if load/save has just been disabled, close this
                #   window after the dialogue window closes
                dialogue_win.set_modal(True)
                dialogue_win.run()
                dialogue_win.destroy()
                if self.app_obj.disable_load_save_flag:
                    self.destroy()

            # Load not attempted
            else:

                dialogue_win = dialogue_manager_obj.show_msg_dialogue(
                    _('Did not try to load the database file'),
                    'error',
                    'ok',
                    self,           # Parent window is this window
                )

        else:

            # Load succeeded. Redraw the preference window, opening it at the
            #   same tab
            self.reset_window()
            self.select_switch_db_tab()

            if self.app_obj.disable_load_save_msg is not None:

                dialogue_manager_obj.show_msg_dialogue(
                    self.app_obj.disable_load_save_msg,
                    'info',
                    'ok',
                    self,           # Parent window is this window
                )

            else:

                dialogue_manager_obj.show_msg_dialogue(
                    _('Database file loaded'),
                    'info',
                    'ok',
                    self,           # Parent window is this window
                )


    def update_ytdl_combos(self):

        """Called initially by self.setup_downloader_paths_tab(), then by
        self.on_ytdl_fork_changed().

        Updates the contents of the two comboboxes in the tab, so that the
        youtube-dl fork is visible, rather than yotube-dl itself (if
        applicable).

        Also updates labels in the Operations > Livestreams tab.
        """

        ignore_me = _(
            'TRANSLATOR\'S NOTE: System preferences > Downloaders > Forks'
        )

        fork = standard = 'youtube-dl'
        if self.app_obj.ytdl_fork is not None:
            fork = self.app_obj.ytdl_fork

        ytdl_path_default = re.sub(
            standard,
            fork,
            self.app_obj.ytdl_path_default,
        )

        # First combo: Path to the youtube-dl executable
        self.path_liststore.set(
            self.path_liststore.get_iter(Gtk.TreePath(0)),
            0,
            _('Use default path') + ' (' + ytdl_path_default + ')',
        )

        ytdl_bin = re.sub(
            standard,
            fork,
            self.app_obj.ytdl_bin,
        )

        if os.name != 'nt':

            self.path_liststore.set(
                self.path_liststore.get_iter(Gtk.TreePath(1)),
                0,
                _('Use local path') + ' (' + ytdl_bin + ')',
            )

            ytdl_path_pypi = re.sub(
                standard,
                fork,
                self.app_obj.ytdl_path_pypi,
            )

            self.path_liststore.set(
                self.path_liststore.get_iter(Gtk.TreePath(3)),
                0,
                _('Use PyPI path') + ' (' + ytdl_path_pypi + ')',
            )

        # Second combo: Command for update operations (not always visible)
        if not __main__.__pkg_strict_install_flag__:

            count = -1
            for item in self.app_obj.ytdl_update_list:

                count += 1
                descrip = re.sub(
                    standard, fork, formats.YTDL_UPDATE_DICT[item]
                )
                self.cmd_liststore.set(
                    self.cmd_liststore.get_iter(Gtk.TreePath(count)),
                    1,
                    descrip,
                )

        # Update labels in the Operations > Livestreams tab
        self.setup_operations_livestreams_tab_update()

