#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019-2024 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/>.


"""Main window class and related 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 functools
from gi.repository import Gio
import os
import platform
from gi.repository import Pango
import math
import re
import sys
import threading
import time
import urllib.parse


# Import our modules
import config
import formats
import html
import __main__
import mainapp
import media
import options
import utils
import wizwin
# Use same gettext translations
from mainapp import _

# (Desktop notifications don't work on MS Windows/MacOS, so no need to import
#   Notify)
if mainapp.HAVE_NOTIFY_FLAG:
    gi.require_version('Notify', '0.7')
    from gi.repository import Notify


# Debugging flag (calls utils.debug_time at the start of every function)
DEBUG_FUNC_FLAG = False


# Classes
class MainWin(Gtk.ApplicationWindow):

    """Called by mainapp.TartubeApp.start().

    Python class that handles the main window.

    The main window has three tabs - the Videos tab, the Progress tab and the
    Errors tab.

    In the Videos tab, the Video Index is visible on the left, and the Video
    Catalogue is visible on the right.

    In the Progress tab, the Progress List is visible at the top, and the
    Results List is visible at the bottom.

    In the Errors tab, any errors generated by youtube-dl are displayed. (The
    display is not reset at the beginning of every download operation).

    Args:

        app_obj (mainapp.TartubeApp): The main application object

    """


    # Standard class methods


    def __init__(self, app_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 100 __init__')

        super(MainWin, self).__init__(
            title=__main__.__packagename__.title(),
            application=app_obj
        )

        # IV list - class objects
        # -----------------------
        # The main application
        self.app_obj = app_obj


        # IV list - Gtk widgets
        # ---------------------
        # (from self.setup_grid)
        self.grid = None                        # Gtk.Grid
        # (from self.setup_main_menubar)
        self.menubar = None                     # Gtk.MenuBar
        self.change_db_menu_item = None         # Gtk.MenuItem
        self.check_db_menu_item = None          # Gtk.MenuItem
        self.save_db_menu_item = None           # Gtk.MenuItem
        self.save_all_menu_item = None          # Gtk.MenuItem
        self.system_prefs_menu_item = None      # Gtk.MenuItem
        self.open_msys2_menu_item = None        # Gtk.MenuItem
        self.show_install_menu_item = None      # Gtk.MenuItem
        self.show_script_menu_item = None       # Gtk.MenuItem
        self.change_theme_menu_item = None      # Gtk.MenuItem
        self.add_video_menu_item = None         # Gtk.MenuItem
        self.add_channel_menu_item = None       # Gtk.MenuItem
        self.add_playlist_menu_item = None      # Gtk.MenuItem
        self.add_folder_menu_item = None        # Gtk.MenuItem
        self.add_bulk_menu_item = None          # Gtk.MenuItem
        self.export_db_menu_item = None         # Gtk.MenuItem
        self.import_db_menu_item = None         # Gtk.MenuItem
        self.import_yt_menu_item = None         # Gtk.MenuItem
        self.switch_view_menu_item = None       # Gtk.MenuItem
        self.hide_system_menu_item = None       # Gtk.MenuItem
        self.show_hidden_menu_item = None       # Gtk.MenuItem
        self.show_hide_menu_item = None         # Gtk.MenuItem
        self.switch_profile_menu_item = None    # Gtk.MenuItem
        self.auto_switch_menu_item = None       # Gtk.MenuItem
        self.create_profile_menu_item = None    # Gtk.MenuItem
        self.delete_profile_menu_item = None    # Gtk.MenuItem
        self.profile_menu_item = None           # Gtk.MenuItem
        self.mark_containers_menu_item = None   # Gtk.MenuItem
        self.unmark_containers_menu_item = None # Gtk.MenuItem
        self.test_menu_item = None              # Gtk.MenuItem
        self.test_code_menu_item = None         # Gtk.MenuItem
        self.check_all_menu_item = None         # Gtk.MenuItem
        self.download_all_menu_item = None      # Gtk.MenuItem
        self.custom_dl_all_menu_item = None     # Gtk.MenuItem
        self.refresh_db_menu_item = None        # Gtk.MenuItem
        self.update_ytdl_menu_item = None       # Gtk.MenuItem
        self.test_ytdl_menu_item = None         # Gtk.MenuItem
        self.install_ffmpeg_menu_item = None    # Gtk.MenuItem
        self.install_matplotlib_menu_item = None
                                                # Gtk.MenuItem
        self.install_streamlink_menu_item = None
                                                # Gtk.MenuItem
        self.tidy_up_menu_item = None           # Gtk.MenuItem
        self.stop_operation_menu_item = None    # Gtk.MenuItem
        self.stop_soon_menu_item = None         # Gtk.MenuItem
        self.live_prefs_menu_item = None        # Gtk.MenuItem
        self.update_live_menu_item  = None      # Gtk.MenuItem
        self.cancel_live_menu_item  = None      # Gtk.MenuItem
        # (from self.setup_main_toolbar)
        self.main_toolbar = None                # Gtk.Toolbar
        self.add_video_toolbutton = None        # Gtk.ToolButton
        self.add_channel_toolbutton = None      # Gtk.ToolButton
        self.add_playlist_toolbutton = None     # Gtk.ToolButton
        self.add_folder_toolbutton = None       # Gtk.ToolButton
        self.check_all_toolbutton = None        # Gtk.ToolButton
        self.download_all_toolbutton = None     # Gtk.ToolButton
        self.stop_operation_toolbutton = None   # Gtk.ToolButton
        self.system_preferences_toolbutton = None
                                                # Gtk.ToolButton
        self.general_options_toolbutton = None  # Gtk.ToolButton
        self.switch_view_toolbutton = None      # Gtk.ToolButton
        self.hide_system_toolbutton = None      # Gtk.ToolButton
        self.test_toolbutton = None             # Gtk.ToolButton
        # (from self.setup_notebook)
        self.notebook = None                    # Gtk.Notebook
        self.videos_tab = None                  # Gtk.Box
        self.videos_label = None                # Gtk.Label
        self.progress_tab = None                # Gtk.Box
        self.progress_label = None              # Gtk.Label
        self.classic_tab = None                 # Gtk.Box
        self.classic_label = None               # Gtk.Label
        self.drag_drop_tab = None               # Gtk.Box
        self.drag_drop_label = None             # Gtk.Label
        self.output_tab = None                  # Gtk.Box
        self.output_label = None                # Gtk.Label
        self.errors_tab = None                  # Gtk.Box
        self.errors_label = None                # Gtk.Label
        # (from self.setup_videos_tab)
        self.video_index_vbox = None            # Gtk.VBox
        self.videos_paned = None                # Gtk.HPaned
        self.video_index_scrolled = None        # Gtk.ScrolledWindow
        self.video_index_frame = None           # Gtk.Frame
        self.video_index_treeview = None        # Gtk.TreeView
        self.video_index_treestore = None       # Gtk.TreeStore
        self.video_index_sortmodel = None       # Gtk.TreeModelSort
        self.video_index_tooltip_column = 2
        self.button_box = None                  # Gtk.VBox
        self.check_media_button = None          # Gtk.Button
        self.download_media_button = None       # Gtk.Button
        self.custom_dl_media_button = None      # Gtk.Button
        self.progress_box = None                # Gtk.HBox
        self.progress_bar = None                # Gtk.ProgressBar
        self.progress_label = None              # Gtk.Label
        self.video_catalogue_vbox = None        # Gtk.VBox
        self.catalogue_scrolled = None          # Gtk.ScrolledWindow
        self.catalogue_frame = None             # Gtk.Frame
        self.catalogue_listbox = None           # Gtk.ListBox
        self.catalogue_grid = None              # Gtk.Grid
        self.catalogue_toolbar = None           # Gtk.Toolbar
        self.catalogue_page_entry = None        # Gtk.Entry
        self.catalogue_last_entry = None        # Gtk.Entry
        self.catalogue_size_entry = None        # Gtk.Entry
        self.catalogue_first_button = None      # Gtk.ToolButton
        self.catalogue_back_button = None       # Gtk.ToolButton
        self.catalogue_forwards_button = None   # Gtk.ToolButton
        self.catalogue_last_button = None       # Gtk.ToolButton
        self.catalogue_scroll_up_button = None  # Gtk.ToolButton
        self.catalogue_scroll_down_button = None
                                                # Gtk.ToolButton
        self.catalogue_show_filter_button = None
                                                # Gtk.ToolButton
        self.catalogue_toolbar2 = None          # Gtk.Toolbar
        self.catalogue_sort_combo = None        # Gtk.ComboBox
        self.catalogue_reverse_toolbutton = None
                                                # Gtk.ToolButton
        self.catalogue_resort_button = None     # Gtk.ToolButton
        self.catalogue_find_date_button = None  # Gtk.ToolButton
        self.catalogue_cancel_date_button = None
                                                # Gtk.ToolButton

        self.catalogue_toolbar3 = None          # Gtk.Toolbar
        self.catalogue_filter_entry = None      # Gtk.Entry
        self.catalogue_regex_togglebutton = None
                                                # Gtk.ToggleButton
        self.catalogue_filter_name_button = None
                                                # Gtk.CheckButton
        self.catalogue_filter_descrip_button = None
                                                # Gtk.CheckButton
        self.catalogue_filter_comment_button = None
                                                # Gtk.CheckButton
        self.catalogue_apply_filter_button = None
                                                # Gtk.ToolButton
        self.catalogue_cancel_filter_button = None
                                                # Gtk.ToolButton
        self.catalogue_thumb_combo = None       # Gtk.ComboBox
        self.catalogue_frame_button = None      # Gtk.CheckButton
        self.catalogue_icons_button = None      # Gtk.CheckButton
        self.catalogue_blocked_button = None    # Gtk.CheckButton
        # (from self.setup_progress_tab)
        self.progress_paned = None              # Gtk.VPaned
        self.progress_list_scrolled = None      # Gtk.ScrolledWindow
        self.progress_list_treeview = None      # Gtk.TreeView
        self.progress_list_liststore = None     # Gtk.ListStore
        self.progress_list_tooltip_column = 2
        self.results_list_scrolled = None       # Gtk.Frame
        self.results_list_treeview = None       # Gtk.TreeView
        self.results_list_liststore = None      # Gtk.ListStore
        self.results_list_tooltip_column = 1
        self.num_worker_checkbutton = None      # Gtk.CheckButton
        self.num_worker_spinbutton = None       # Gtk.SpinButton
        self.bandwidth_checkbutton = None       # Gtk.CheckButton
        self.bandwidth_spinbutton = None        # Gtk.SpinButton
        self.alt_limits_frame = None            # Gtk.Frame
        self.alt_limits_image = None            # Gtk.Image
        self.video_res_checkbutton = None       # Gtk.CheckButton
        self.video_res_combobox = None          # Gtk.ComboBox
        self.hide_finished_checkbutton = None   # Gtk.CheckButton
        self.reverse_results_checkbutton = None # Gtk.CheckButton
        self.progress_update_label = None       # Gtk.Label
        # (from self.setup_classic_mode_tab)
        self.classic_paned = None               # Gtk.VPaned
        self.classic_banner_img = None          # Gtk.Image
        self.classic_banner_label = None        # Gtk.Label
        self.classic_banner_label2 = None       # Gtk.Label
        self.classic_menu_button = None         # Gtk.Button
        self.classic_textview = None            # Gtk.TextView
        self.classic_textbuffer = None          # Gtk.TextBuffer
        self.classic_mark_start = None          # Gtk.TextMark
        self.classic_mark_end = None            # Gtk.TextMark
        self.classic_dest_dir_liststore = None  # Gtk.ListStore
        self.classic_dest_dir_combo = None      # Gtk.ComboBox
        self.classic_dest_dir_button = None     # Gtk.Button
        self.classic_dest_dir_open_button = None
                                                # Gtk.Button
        self.classic_format_liststore = None    # Gtk.ListStore
        self.classic_format_combo = None        # Gtk.ComboBox
        self.classic_resolution_liststore = None
                                                # Gtk.ListStore
        self.classic_resolution_combo = None    # Gtk.ComboBox
        self.classic_convert_liststore = None   # Gtk.ListStore
        self.classic_convert_combo = None       # Gtk.ComboBox
        self.classic_livestream_checkbutton = None
                                                # Gtk.CheckButton
        self.classic_sblock_checkbutton = None  # Gtk.CheckButton
        self.classic_add_clips_button = None    # Gtk.Button
        self.classic_add_urls_button = None     # Gtk.Button
        self.classic_progress_treeview = None   # Gtk.TreeView
        self.classic_progress_liststore = None  # Gtk.ListStore
        self.classic_progress_tooltip_column = 1
        self.classic_play_button = None         # Gtk.Button
        self.classic_open_button = None         # Gtk.Button
        self.classic_redownload_button = None   # Gtk.Button
        self.classic_archive_button = None      # Gtk.ToggleButton
        self.classic_stop_button = None         # Gtk.Button
        self.classic_clips_button = None        # Gtk.Button
        self.classic_ffmpeg_button = None       # Gtk.Button
        self.classic_move_up_button = None      # Gtk.Button
        self.classic_move_down_button = None    # Gtk.Button
        self.classic_remove_button = None       # Gtk.Button
        self.classic_download_button = None     # Gtk.Button
        self.classic_clear_button = None        # Gtk.Button
        self.classic_clear_dl_button = None     # Gtk.Button
        # (from self.setup_drag_drop_tab)
        self.drag_drop_menu_button = None       # Gtk.Button
        self.drag_drop_frame = None             # Gtk.Frame
        self.drag_drop_grid = None              # Gtk.Grid
        # (from self.setup_output_tab)
        self.output_notebook = None             # Gtk.Notebook
        self.output_size_checkbutton = None     # Gtk.CheckButton
        self.output_size_spinbutton = None      # Gtk.SpinButton
        # (from self.setup_errors_tab)
        self.errors_list_frame = None           # Gtk.Frame
        self.errors_list_scrolled = None        # Gtk.ScrolledWindow
        self.errors_list_treeview = None        # Gtk.TreeView
        self.errors_list_liststore = None       # Gtk.ListStore
        self.show_system_error_checkbutton = None
                                                # Gtk.CheckButton
        self.show_system_warning_checkbutton = None
                                                # Gtk.CheckButton
        self.show_operation_error_checkbutton = None
                                                # Gtk.CheckButton
        self.show_operation_warning_checkbutton = None
                                                # Gtk.CheckButton
        self.show_system_date_checkbutton = None
                                                # Gtk.CheckButton
        self.show_system_container_checkbutton = None
                                                # Gtk.CheckButton
        self.show_system_video_checkbutton = None
                                                # Gtk.CheckButton
        self.show_system_multi_line_checkbutton = None
                                                # Gtk.CheckButton
        self.error_list_copy_button = None      # Gtk.Button
        self.error_list_entry = None            # Gtk Entry
        self.error_list_togglebutton = None     # Gtk.ToggleButton
        self.error_list_container_checkbutton = None
                                                # Gtk.CheckButton
        self.error_list_video_checkbutton = None
                                                # Gtk.CheckButton
        self.error_list_msg_checkbutton = None  # Gtk.CheckButton
        self.error_list_filter_toolbutton = None
                                                # Gtk.ToolButton
        self.error_list_cancel_toolbutton = None
        self.error_list_clear_button = None     # Gtk.Button


        # IV list - other
        # ---------------
        # Size (in pixels) of gaps between main window widgets
        self.spacing_size = self.app_obj.default_spacing_size

        # IVs used when videos in the Video Index are displayed in a grid. The
        #   size of the grid changes as the window is resized. Each location in
        #   the grid can be occupied by a gridbox (mainwin.CatalogueGridBox),
        #   containing a single video
        # The size of the window, the last time a certain signal connect fired,
        #   so we can spot real changes to its size, when the same signal fires
        #   in the future
        self.win_last_width = None
        self.win_last_height = None
        # Also keep track of the position of the slider in the Video tab's
        #   Gtk.HPaned, so that when the user actually drags the slider, we can
        #   adjust the size of the grid
        self.paned_last_width = None
        # IVs used when the window is closed to the tray, recording its
        #   position on the desktop (so that position can be restored when the
        #   window is opened from the tray). The IVs are reset when the window
        #   becomes visible againH
        # NB Detecting the window's position on the desktop does not work on
        #   Wayland (according to the Gtk documentation)
        self.win_last_xpos = None
        self.win_last_ypos = None
        # Standard minimum column width for the treeviews that are the Progress
        #   List, Results List and Classic Progress List
        self.min_column_width = 20

        # Paths to Tartube standard icon files. Dictionary in the form
        #   key - a string like 'video_both_large'
        #   value - full filepath to the icon file
        # N.B. In this dictionary, composite pixbufs created by
        #   self.setup_composite_pixbufs() use the value 'None'
        self.icon_dict = {}
        # Loading icon files whenever they're neeeded causes frequent Gtk
        #   crashes. Instead, we create a GdkPixbuf.Pixbuf for all standard
        #   icon files at the beginning
        # A dictionary of those pixbufs, created by self.setup_pixbufs()
        # Dictionary in the form
        #   key - a string like 'video_both_large' (the same key set used by
        #       self.icon_dict)
        #   value - A GdkPixbuf.Pixbuf object
        self.pixbuf_dict = {}
        # List of pixbufs used as the main window's icon list
        self.win_pixbuf_list = []
        # List of pixbufs used as other windows' icon lists
        self.config_win_pixbuf_list = []
        # The full path to the directory in which self.setup_pixbufs() found
        #   the icons; stores so that StatusIcon can use it
        self.icon_dir_path = None

        # Standard limits for the length of strings displayed in various
        #   widgets
        self.exceedingly_long_string_max_len = 80
        self.very_long_string_max_len = 64
        self.long_string_max_len = 48
        self.quite_long_string_max_len = 40
        self.medium_string_max_len = 32
        self.short_string_max_len = 24
        self.tiny_string_max_len = 16
        # Use a separate IV for video descriptions (so we can tweak it
        #   specifically). A limit exists because descriptions in ALL CAPS are
        #   too big for the Video Catalogue, otherwise
        self.descrip_line_max_len = 80
        # Use a separate IV for tooltips in the Video Index/Video Catalogue
        self.tooltip_max_len = 60
        # Limits (number of videos) at which the code will prompt the user
        #   before bookmarking videos (etc)
        # Take shortcuts, but don't prompt the user
        self.mark_video_lower_limit = 50
        # Take shortcuts, and prompt the user
        self.mark_video_higher_limit = 1000

        # Dictionary of tabs in the main window's notebook (self.notebook), and
        #   their corresponding page numbers. If __pkg_no_download_flag__ is
        #   set, the Classic Mode tab is not visible, so page numbers will
        #   differ
        # Dictionary in the form
        #   key - the string 'videos', 'progress', 'classic', 'drag_drop',
        #       'output' or 'error'
        #   value - The tab number, in the range 0-5
        self.notebook_tab_dict = {}                # Set below
        # The number of the tab in self.notebook that is currently visible
        #   (only required to test whether the Errors/Warnings tab is the
        #   visible one)
        self.visible_tab_num = 0

        # Videos tab IVs
        # The Video Index is the left-hand side of the main window, and
        #   displays only channels, playlists and folders
        # The Video Index uses a Gtk.TreeView to display media data objects
        #   (channels, playlist and folders, but not videos). This dictionary
        #   keeps track of which row in the Gtk.TreeView is displaying which
        #   media data object
        # Dictionary in the form
        #   key = .dbid of the media data object
        #   value = Gtk.TreeRowReference
        self.video_index_row_dict = {}
        # Dictionary keeping track of which rows have their markers activated
        # A subset of key/value pairs in self.video_index_row_dict. Rows whose
        #   markers are not activated are not in this dictionary
        self.video_index_marker_dict = {}
        # A call to self.video_index_reset() redraws the Video Index, but calls
        #   to other functions repopulate it
        # The call to .video_index_reset() resets self.video_index_marker_dict,
        #   moving its pairs temporarily into this dictionary, so that they can
        #   be retrieved during the subsequent call to
        #   self.video_index_populate()
        self.video_index_old_marker_dict = {}

        # The call to self.video_index_add_row() causes the auto-sorting
        #   function self.video_index_auto_sort() to be called before we're
        #   ready, due to some Gtk problem I don't understand
        # Temporary solution is to disable auto-sorting during calls to that
        #   function
        self.video_index_no_sort_flag = False
        # The .dbid of the channel, playlist or folder currently visible in the
        #   Video Catalogue (None if no channel, playlist or folder is
        #   selected)
        self.video_index_current_dbid = None
        # Don't update the Video Catalogue during certain procedures, such as
        #   removing a row from the Video Index (in which case, this flag will
        #   be set to True
        self.ignore_video_index_select_flag = False

        # The Video Catalogue is the right-hand side of the main window. When
        #   the user clicks on a channel, playlist or folder, all the videos
        #   it contains are displayed in the Video Catalogue (replacing any
        #   previous contents)
        # Dictionary of mainwin.SimpleCatalogueItem,
        #   mainwin.ComplexCatalogueItem or mainwin.GridCatalogueItem objects
        #   (depending on the current value of
        #   mainapp.TartubeApp.catalogue_mode)
        # There is one catalogue item object for each row that's currently
        #   visible in the Video Catalogue
        # Dictionary in the form
        #   key = .dbid (of the mainwin.SimpleCatalogueItem,
        #       mainwin.ComplexCatalogueItem or mainwin.GridCatalogueItem which
        #       matches the dbid of its media.Video object)
        #   value = the catalogue item itself
        self.video_catalogue_dict = {}
        # Catalogue itme objects are added to the catalogue in a call to
        #   self.video_catalogue_insert_video()
        # If Gtk issues a warning, complaining that the Gtk.ListBox is being
        #   sorted, the row (actually a CatalogueRow object) is added to this
        #   list temporarily, and then periodic calls to
        #   self.video_catalogue_retry_insert_items() try again, until the
        #   list is empty
        self.video_catalogue_temp_list = []
        # Flag set to True if a filter is currently applied to the Video
        #   Catalogue, hiding some videos, and showing only videos that match
        #   the search text; False if not
        self.video_catalogue_filtered_flag = False
        # When the filter is applied, a list of video objects to show (may be
        #   an empty list)
        self.video_catalogue_filtered_list = []

        # Dragging videos from the Video Catalogue into the Video Index
        #   requires some trickery. At the start of such a drag, this list is
        #   filled with all media.Video objects involved in the drag; when the
        #   drag ends, it is emptied
        # Therefore, self.on_video_index_drag_data_received() knows that it is
        #   receiving media.Videos when this list is not empty
        self.video_catalogue_drag_list = []
        # (Git #561) Dragging items from the Video Index onto itself causes
        #   problems; self.on_window_drag_data_received() is called correctly,
        #   immediately followed by a useless call to
        #   self.on_window_drag_data_received()
        # An intermediate call to Gtk.drag_cancel(context) does not work, so
        #   we're forced to use this ugly hack, hoping that both functions
        #   receive the same Gtk.DragContext
        self.ignore_drag_context = None

        # Background colours used in the Video Catalogue to highlight
        #   livestream videos (we use different colours for a debut video)
        # Each value is a Gdk.RGBA object, whose initial colours are set (in
        #   the call to self.setup_bg_colour() below) using
        #   mainapp.TartubeApp.custom_bg_table
        self.live_wait_colour = None            # Red
        self.live_now_colour = None             # Green
        self.debut_wait_colour = None           # Yellow
        self.debut_now_colour = None            # Cyan
        # Background colours used in the Video Catalogue, in grid mode, to
        #   highlight selected livestream/debut videos
        self.grid_select_colour = None          # Blue
        self.grid_select_wait_colour = None     # Purple
        self.grid_select_live_colour = None     # Purple
        # Background colours used in the Drag and Drop tab
        self.drag_drop_notify_colour = None     # Purple
        self.drag_drop_odd_colour = None        # Orange
        self.drag_drop_even_colour = None       # Pale orange

        # The Video Catalogue splits its video list into pages (as Gtk
        #   struggles with a list of hundreds, or thousands, of videos)
        # The number of videos per page is specified by
        #   mainapp.TartubeApp.catalogue_page_size
        # The current page number (minimum 1, maximum 9999)
        self.catalogue_toolbar_current_page = 1
        # The number of pages currently in use (minimum 1, maximum 9999)
        self.catalogue_toolbar_last_page = 1

        # The horizontal size of the grid (the number of gridboxes that can
        #   fit on a single row of the grid). This value is set automatically
        #   as the available space changes (for example, when the user resizes
        #   the main window, or when the user drags the paned handled in the
        #   Videos tab)
        self.catalogue_grid_column_count = 1
        # The vertical size of the grid. The Video Catalogue scrolls to
        #   accommodate extra rows, so this value is only set when new
        #   CatalogueGridBox objects are added to the grid (and not in
        #   response to changes in the window size, for example)
        # (The value is only checked when the grid actually contains videos, so
        #   its minimum size is 1 even when the grid is empty)
        self.catalogue_grid_row_count = 1
        # mainapp.TartubeApp defines several thumbnail sizes, from 'tiny' to
        #   'enormous'
        # In order to work out how many gridboxes can fit on a single row of
        #   the grid, we have to know the minimum required size for each
        #   gridbox. That size is different for each thumbnail size
        # After drawing the first gridbox(es), the minimum required size is not
        #   available immediately (for obscure Gtk reasons). Therefore, we
        #   initially prevent each gridbox from expanding horizontally until
        #   the size has been obtained; at that point, the grid is redrawn
        # A dictionary of minimum required sizes, in the form
        #   key: thumbnail size (one of the keys in
        #       mainapp.TartubeApp.thumb_size_dict)
        #   value: The minimum required size for a gridbox (in pixels), or
        #       'None' if that value is not known yet
        self.catalogue_grid_width_dict = {}        # Initialised below
        # Gridboxes may be allowed to expand horizontally to fill the available
        #   space, or not, depending on aesthetic requirements. This flag is
        #   True if gridboxes are allowed to expand, False if not
        self.catalogue_grid_expand_flag = False
        # Flag set to True by self.video_catalogue_grid_set_gridbox_width(), so
        #   that mainapp.TartubeApp.script_fast_timer_callback() knows it must
        #   call self.video_catalogue_grid_check_size()
        self.catalogue_grid_rearrange_flag = False
        # When the grid is visible, the selected gridbox (if any) intercepts
        #   cursor and page up/down keys
        # A dictionary of Gdk.keyval_name values that should be intercepted,
        #   for quick lookup
        self.catalogue_grid_intercept_dict = {
            'Up': None,
            'Down': None,
            'Left': None,
            'Right': None,
            'Page_Up': None,
            'Page_Down': None,
            'a': None,              # Intercepts CTRL+A
        }

        # Progress tab IVs
        # The Progress List uses a Gtk.TreeView display download jobs, whether
        #   they are waiting to start, currently in progress, or finished. This
        #   dictionary keeps track of which row in the Gtk.TreeView is handling
        #   which download job
        # Dictionary in the form
        #   key = The downloads.DownloadItem.item_id for the download item
        #       handling the media data object
        #   value = the row number (0 is the first row)
        self.progress_list_row_dict = {}
        # The number of rows added to the treeview
        self.progress_list_row_count = 0
        # During a download operation, self.progress_list_receive_dl_stats() is
        #   called every time youtube-dl writes some output to STDOUT. This can
        #   happen many times a second
        # Updating data displayed in the Progress List several times a second,
        #   and irregularly, doesn't look very nice. Instead, we only update
        #   the displayed data at fixed intervals
        # Thus, when self.progress_list_receive_dl_stats() is called, it
        #   temporarily stores the download statistics it has received in this
        #   IV. The statistics are received in a dictionary in the standard
        #   format described in the comments to
        #   downloads.VideoDownloader.extract_stdout_data()
        # Then, during calls at fixed intervals to
        #   self.progress_list_display_dl_stats(), those download statistics
        #   are displayed
        # Dictionary of download statistics yet to be displayed, emptied after
        #   every call to self.progress_list_display_dl_stats()
        # Dictionary in the form
        #   key = The downloads.DownloadItem.item_id for the download item
        #       handling the media data object
        #   value = A dictionary of download statistics dictionary in the
        #       standard format
        self.progress_list_temp_dict = {}
        # During a download operation, we keep track of rows that are finished,
        #   so they can be hidden, if required
        # Dictionary in the form
        #   key = The downloads.DownloadItem.item_id for the download item
        #       handling the media data object
        #   value = The time at which it should be hidden (matches time.time())
        # (As soon as a row is hidden, all of these IVs are updated, removing
        #   them from all three dictionaries)
        self.progress_list_finish_dict = {}
        # The time (in seconds) after which a row which can be hidden, should
        #   actually be hidden
        # (The code assumes it is at least twice the value of
        #   mainapp.TartubeApp.dl_timer_time)
        self.progress_list_hide_time = 3
        # Dictionay of download speeds for each active row in the Progress
        #   List, used to produce a rolling average. Inactive (or finished)
        #   rows are removed from the table
        # Dictionary in the form
        #   key = The downloads.DownloadItem.item_id for the download item
        #       handling the media data object
        #   value = a list of mini-lists. Each mini-list records an
        #       instantaneous download speed (as reported by youtube-dl), and
        #       the epoch time at which this speed was displayed
        self.progress_list_average_speed_dict = {}
        # The time (in seconds) after which instantaneous download speeds are
        #   removed from the dictionary
        # N.B. 10 seconds seems to be a reasonable value, given that youtube-dl
        #   typically supplies an instaneous speed at least once a second, and
        #   given that some videos might be downloaded faster than 10 seconds
        self.progress_list_average_speed_length = 10
        # !!! DEBUG Git #479
        # Dictionary recording system errors in
        #   self.progress_list_receive_dl_stats() and
        #   .progress_list_do_hide_row() (due to unresolved issues)
        # Instead of repeating the same system error innumerable times, add an
        #   entry to this dictionary so it can be shown only the first time.
        #   The dictionary is reset every time the Progress List is reset
        # Dictionary in the form
        #   key = The downloads.DownloadItem.item_id for the download item
        #       handling the media data object
        #   value = False
        self.progress_list_broken_dict = {}

        # Whenever a video is downloaded (in reality, or just in simulation),
        #   a row is added to Gtk.TreeView in the Results List
        # The number of rows added to the treeview
        self.results_list_row_count = 0
        # At the instant youtube-dl reports that a video has been downloaded,
        #   the file doesn't yet exist in Tartube's data directory (so the
        #   Python test for the existence of the file fails)
        # Therefore, self.results_list_add_row() adds a temporary entry to this
        #   list. Items in the list are checked by
        #   self.results_list_update_row() and removed from the list, as soon
        #   as the file is confirmed to exist, at which time the Results List
        #   is updated
        # (For simulated downloads, the entry is checked by
        #   self.results_list_update_row() just once. For real downloads, it
        #   is checked many times until either the file exists or the
        #   download operation halts)
        # List of python dictionaries, one for each downloaded video. Each of
        #   those dictionaries are in the form:
        #       'video_obj': a media.Video object
        #       'row_num': the row on the treeview, matching
        #           self.results_list_row_count
        #       'keep_description', 'keep_info', 'keep_annotations',
        #           'keep_thumbnail', 'move_description', 'move_info',
        #           'move_annotations', 'move_thumbnail': flags from the
        #           options.OptionsManager object used for to download the
        #           video ('keep_description', etc, are not not added to the
        #           dictionary at all for simulated downloads)
        self.results_list_temp_list = []
        # When a video is deleted, the row in the Results List containing that
        #   video must be updated. So this can be done efficiently, we also
        #   compile a dictionary of media.Video objects and the rows they
        #   occupy
        # Dictionary in the form
        #   key = The .dbid of the media.Video for the row
        #   value = The row number on the treeview
        self.results_list_row_dict = {}

        # Classic Mode tab IVs
        # During a normal download operation, stats are displayed in the
        #   Progress tab
        # During a download operation launched from the Classic Mode tab, stats
        #   are displayed in the Classic Progress List instead. In addition, we
        #   create a set of dummy media.Video objects, one for each URL to
        #   download. Each dummy media.Video object has a negative .dbid, and
        #   none of them are added to the media data registry
        # The dummy media.Video object's URL may be a single video, or even a
        #   channel or playlist (Tartube doesn't really care which)
        # Dictionary in the form
        #   key = The unique ID (dbid) for the dummy media.Video object
        #       handling the URL
        #   value = The dummy media.Video object itself
        self.classic_media_dict = {}
        # The total number of dummy media.Video objects created since Tartube
        #   started (used to give each one a unique ID)
        self.classic_media_total = 0
        # During a download operation launched from the Classic Mode tab,
        #   incoming stats are stored in this dictionary, just as they are
        #   stored in self.progress_list_temp_dict during a normal download
        #   operation
        # Dictionary in the form
        #   key = The downloads.DownloadItem.item_id for the download item
        #       handling the media data object
        #   value = A dictionary of download statistics dictionary in the
        #       standard format
        self.classic_temp_dict = {}
        # Flag set to True when automatic copy/paste has been enabled (always
        #   disabled on startup)
        self.classic_auto_copy_flag = False
        # Flag set to True when one-click downloads have been enabled (always
        #   disabled on startup)
        self.classic_one_click_dl_flag = False
        # The last text that was copy/pasted from the clipboard. Storing it
        #   here prevents self.classic_mode_tab_timer_callback() from
        #   continually re-pasting the same text (for example, when the user
        #   manually empties the textview)
        self.classic_auto_copy_text = None
        # Temporary flag set to prevent a second call to
        #   self.classic_mode_tab_add_urls() before the first one has finished
        self.classic_auto_copy_check_flag = False
        # Flag set to True just before a call to
        #   self.classic_mode_tab_add_urls() so that it can't call itself
        # IVs for clipboard monitoring, when required
        self.classic_clipboard_timer_id = None
        self.classic_clipboard_timer_time = 250

        # Drag and Drop tab IVs
        # Dictionary of mainwin.DropZoneBox objects that currently exist in
        #   the tab (ignoring any blank ones used to fill space)
        # Dictionary in the form
        #   key = the .uid of the equivalent options.OptionsManager object
        #   value = the mainwin.DropZoneBox object
        self.drag_drop_dict = {}
        # The maximum number of dropzones (minimum value = 1; must not be a
        #   prime number)
        self.drag_drop_max = 16
        # The time (in seconds) after which confirmation messages in each
        #   dropzone should be reset
        self.drag_drop_reset_time = 5

        # Output tab IVs
        # Flag set to True when the summary tab is added, during the first call
        #   to self.output_tab_setup_pages() (might not be added at all, if
        #   mainapp.TartubeApp.ytdl_output_show_summary_flag is False)
        self.output_tab_summary_flag = False
        # The number of pages in the Output tab's notebook (not including the
        #   summary tab). The number matches the highest value of
        #   mainapp.TartubeApp.num_worker_default during this session (i.e. if
        #   the user increases the value, new page(s) are created, but if the
        #   user reduces the value, no pages are destroyed)
        self.output_page_count = 0
        # Dictionary of Gtk.TextView objects created in the Output tab; one for
        #   each page
        # Dictionary in the form
        #   key = The page number (the summary page is #0, the first page for a
        #       thread is #1, regardless of whether the summary page is
        #       visible)
        #   value = The corresponding Gtk.TextView object
        self.output_textview_dict = {}
        # Colours used in the output tab
        self.output_tab_bg_colour = '#000000'
        self.output_tab_text_colour = '#FFFFFF'
        self.output_tab_stderr_colour = 'cyan'
        self.output_tab_system_cmd_colour = 'yellow'

        # Errors / Warnings tab IVs
        # List of error/warning messages available to be shown in the Errors/
        #   Warnings tab (so they can be made visible, or not, as required)
        # Every item in the list is a dictionary of key/value pairs. We don't
        #   store and .dbids, in case the media data object gets deleted in the
        #   meantime (in which case, the error/warning isn't automatically
        #   deleted)
        # The dictionary contains the keys
        #   dict['msg_type'] - 'system_error', 'system_warning',
        #       'operation_error', 'operation_warning'
        #   dict['media_type'] - 'video', 'channel', 'playlist'
        #   dict['date_time'] - date and time at which the message was
        #       generated (a string)
        #   dict['time'] - time at which the message was generated (a string)
        #   dict['container_name'] - name of the parent channel/playlist/folder
        #       (if generated by a video, the name of the parent container)
        #   dict['video_name'] - name of the video. If generated by a channel/
        #       playlist, an empty string
        #   dict['msg'] - The message, formatted into multiple lines with a
        #       maximum line length
        #   dict['short_msg'] - The first line of the formatted message, with
        #       an ellipsis appended if the message is too big for a single
        #       line
        #   dict['orig_msg'] - The original message with no formatting
        #   dict['count_flag'] - True if this message should count towards the
        #       totals displayed in the Errors/Warnings tab label; False if not
        #   dict['drag_path']
        #   dict['drag_source']
        #   dict['drag_name'] - Data for drag and drop operations
        self.error_list_buffer_list = []
        # Settings set when the Error List filter is applied, and reset when
        #   it is cancelled
        self.error_list_filter_flag = False
        self.error_list_filter_text = None
        self.error_list_filter_regex_flag = False
        self.error_list_filter_container_flag = False
        self.error_list_filter_video_flag = False
        self.error_list_filter_msg_flag = False

        # List of configuration windows (anything inheriting from
        #   config.GenericConfigWin) and wizard windows (anything inheriting
        #   from wizwin.GenericWizWin) that are currently open
        # An operation cannot start when one of these windows are open (and the
        #   windows cannot be opened during such an operation)
        self.config_win_list = []
        # In addition. only one wizard window (inheriting wizwin.GenericWizWin)
        #   can be open at a time. The currently-open wizard window, if any
        self.wiz_win_obj = None
        # The number of pages in wizwin.TutorialWizWin
        self.tutorial_page_count = 34

        # Dialogue window IVs
        # The SetDestinationDialogue dialogue window displays a list of
        #   channels/playlists/folders, and an external directory. When opening
        #   it repeatedly, it's handy to display the previous selections
        # The .dbid of the previous channel/playlist/folder selected (or None,
        #   if SetDestinationDialogue hasn't been used yet)
        # The value is set/reset by a call to self.set_previous_alt_dest_dbid()
        self.previous_alt_dest_dbid = None
        # The most recent external directory specified (or None, if
        #   SetDestinationDialogue hasn't been used yet)
        self.previous_external_dir = None

        # Desktop notification IVs
        # The desktop notification has an optional button to click. When the
        #   button is used, we need to retain a reference to the
        #   Notify.Notification, or the callback won't work
        # The number of desktop notifications (with buttons) created during
        #   this session (used to give each one a unique ID)
        self.notify_desktop_count = 0
        # Dictionary of Notify.Notification objects. Each entry is removed when
        #   the notification is closed
        # Dictionary in the form
        #   key: unique ID for the notification (based on
        #       self.notify_desktop_count)
        #   value: the corresponding Notify.Notification object
        self.notify_desktop_dict = {}

        # Other IVs
        # Separator used (optionally) when draggind and dropping into an
        #   external application
        self.drag_drop_separator = '-----'


        # Code
        # ----

        # Set tab numbers for each visible tab in the main window
        self.notebook_tab_dict['videos'] = 0
        self.notebook_tab_dict['progress'] = 1
        if not __main__.__pkg_no_download_flag__:
            self.notebook_tab_dict['classic'] = 2
            self.notebook_tab_dict['drag_drop'] = 3
            self.notebook_tab_dict['output'] = 4
            self.notebook_tab_dict['errors'] = 5
        else:
            self.notebook_tab_dict['classic'] = None
            self.notebook_tab_dict['drag_drop'] = None
            self.notebook_tab_dict['output'] = 2
            self.notebook_tab_dict['errors'] = 3

        # Create GdkPixbuf.Pixbufs for all Tartube standard icons
        self.setup_pixbufs()

        # Set (default) background colours for the Video Catalogue. Custom
        #   colours are set by a later call from mainapp.TartubeApp.load_config
        for key in self.app_obj.custom_bg_table:
            self.setup_bg_colour(key)

        # Initialise minimum sizes for gridboxes
        self.video_catalogue_grid_reset_sizes()


    # Public class methods


    def setup_pixbufs(self):

        """Called by self.__init__().

        Populates self.icon_dict and self.pixbuf.dict from the lists provided
        by formats.py.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 942 setup_pixbufs')

        # The default location for icons is ../icons
        # When installed via PyPI, the icons are moved to ../tartube/icons
        # When installed via a Debian/RPM package, the icons are moved to
        #   /usr/share/tartube/icons
        icon_dir_list = []
        icon_dir_list.append(
            os.path.abspath(
                os.path.join(self.app_obj.script_parent_dir, 'icons'),
            ),
        )

        icon_dir_list.append(
            os.path.abspath(
                os.path.join(
                    os.path.dirname(os.path.realpath(__file__)),
                    'icons',
                ),
            ),
        )

        icon_dir_list.append(
            os.path.join(
                '/', 'usr', 'share', __main__.__packagename__, 'icons',
            )
        )

        for icon_dir_path in icon_dir_list:
            if os.path.isdir(icon_dir_path):

                for key in formats.DIALOGUE_ICON_DICT:
                    rel_path = formats.DIALOGUE_ICON_DICT[key]
                    full_path = os.path.abspath(
                        os.path.join(icon_dir_path, 'dialogue', rel_path),
                    )
                    self.icon_dict[key] = full_path

                for key in formats.TOOLBAR_ICON_DICT:
                    rel_path = formats.TOOLBAR_ICON_DICT[key]
                    full_path = os.path.abspath(
                        os.path.join(icon_dir_path, 'toolbar', rel_path),
                    )
                    self.icon_dict[key] = full_path

                for key in formats.LARGE_ICON_DICT:
                    rel_path = formats.LARGE_ICON_DICT[key]
                    full_path = os.path.abspath(
                        os.path.join(icon_dir_path, 'large', rel_path),
                    )
                    self.icon_dict[key] = full_path

                for key in formats.SMALL_ICON_DICT:
                    rel_path = formats.SMALL_ICON_DICT[key]
                    full_path = os.path.abspath(
                        os.path.join(icon_dir_path, 'small', rel_path),
                    )
                    self.icon_dict[key] = full_path

                for key in formats.THUMB_ICON_DICT:
                    rel_path = formats.THUMB_ICON_DICT[key]
                    full_path = os.path.abspath(
                        os.path.join(icon_dir_path, 'thumbs', rel_path),
                    )
                    self.icon_dict[key] = full_path

                for key in formats.EXTERNAL_ICON_DICT:
                    rel_path = formats.EXTERNAL_ICON_DICT[key]
                    full_path = os.path.abspath(
                        os.path.join(icon_dir_path, 'external', rel_path),
                    )
                    self.icon_dict[key] = full_path

                for key in formats.STOCK_ICON_DICT:
                    rel_path = formats.STOCK_ICON_DICT[key]
                    full_path = os.path.abspath(
                        os.path.join(icon_dir_path, 'stock', rel_path),
                    )
                    self.icon_dict[key] = full_path

                for locale in formats.LOCALE_LIST:
                    full_path = os.path.abspath(
                        os.path.join(
                            icon_dir_path,
                            'locale',
                            'flag_' + locale + '.png',
                        ),
                    )
                    self.icon_dict['flag_' + locale] = full_path

                for i in range (self.tutorial_page_count):
                    full_path = os.path.abspath(
                        os.path.join(
                            icon_dir_path,
                            'tutorial',
                            'tutorial' + str(i) + '.png',
                        ),
                    )
                    self.icon_dict['tutorial' + str(i)] = full_path

                # Now create the pixbufs themselves
                for key in self.icon_dict:
                    full_path = self.icon_dict[key]

                    if not os.path.isfile(full_path):
                        self.pixbuf_dict[key] = None
                    else:
                        self.pixbuf_dict[key] \
                        = GdkPixbuf.Pixbuf.new_from_file(full_path)

                for rel_path in formats.WIN_ICON_LIST:
                    full_path = os.path.abspath(
                        os.path.join(icon_dir_path, 'win', rel_path),
                    )
                    self.win_pixbuf_list.append(
                        GdkPixbuf.Pixbuf.new_from_file(full_path),
                    )

                for rel_path in formats.CONFIG_WIN_ICON_LIST:
                    full_path = os.path.abspath(
                        os.path.join(icon_dir_path, 'win', rel_path),
                    )
                    self.config_win_pixbuf_list.append(
                        GdkPixbuf.Pixbuf.new_from_file(full_path),
                    )

                # Composite icons using a base file and one or more overlays
                self.setup_composite_pixbufs(icon_dir_path)

                # Store the correct icon_dir_path, so that StatusIcon can use
                #   it
                self.icon_dir_path = icon_dir_path

                return

        # No icons directory found; this is a fatal error
        print(
            _('Tartube cannot start because it cannot find its icons folder'),
            file=sys.stderr,
        )

        self.app_obj.do_shutdown()


    def setup_composite_pixbufs(self, icon_dir_path):

        """Called by self.setup_pixbufs().

        Most Tartube icons are loaded from a single file. The Video Index uses
        composite icons, loaded from a base file and some optional overlays.

        The base icons are specified by formats.LARGE_ICON_COMPOSITE_LIST,
        a subset of keys in formats.LARGE_ICON_DICT.

        This function creates the composites, and updates self.icon_dict and
        self.pixbuf_dict.

        Args:

            icon_dir_path (str): Full path to a directory in which Tartube's
                icons are stored (depends on the operating system and the
                installation method)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 1108 setup_composite_pixbufs')

        for base in formats.LARGE_ICON_COMPOSITE_LIST:

            # Produce an image whose name (in self.icon_dict) is in the form
            #   'base_tl_tr_bl_br', where the last four components are optional
            #   and represent overlays adding an icon on the top-left,
            #   top-right, bottom-left and/or bottom-right
            for tl in range(2):
                for tr in range(2):
                    for bl in range(2):
                        for br in range(2):
                            for alt in range(2):

                                icon_name = base
                                pixbuf = GdkPixbuf.Pixbuf.new_from_file(
                                    os.path.abspath(
                                        os.path.join(
                                            icon_dir_path,
                                            'large',
                                            formats.LARGE_ICON_DICT[base],
                                        ),
                                    ),
                                )

                                # Add the top-left overlay when tl = 1, don't
                                #   add it when tl = 0 (etc)
                                if tl:
                                    icon_name += '_tl'
                                    pixbuf = self.apply_pixbuf_overlay(
                                        icon_dir_path,
                                        pixbuf,
                                        '_tl',
                                    )

                                if tr:
                                    icon_name += '_tr'
                                    pixbuf = self.apply_pixbuf_overlay(
                                        icon_dir_path,
                                        pixbuf,
                                        '_tr',
                                    )

                                if bl:

                                    # The bottom-left icon has two variants
                                    if not alt:
                                        icon_name += '_bl'
                                        pixbuf = self.apply_pixbuf_overlay(
                                            icon_dir_path,
                                            pixbuf,
                                            '_bl',
                                        )

                                    else:
                                        icon_name += '_bl_alt'
                                        pixbuf = self.apply_pixbuf_overlay(
                                            icon_dir_path,
                                            pixbuf,
                                            '_bl_alt',
                                        )

                                if br:
                                    icon_name += '_br'
                                    pixbuf = self.apply_pixbuf_overlay(
                                        icon_dir_path,
                                        pixbuf,
                                        '_br',
                                    )

                                # (Composite pixbufs have no file path)
                                self.icon_dict[icon_name] = None
                                self.pixbuf_dict[icon_name] = pixbuf


    def apply_pixbuf_overlay(self, icon_dir_path, base_pixbuf, name):

        """Called by self.setup_composite_pixbufs().

        Creates a composite pixbuf using a base pixbuf and an overlay pixbuf.

        Args:

            icon_dir_path (str): Full path to a directory in which Tartube's
                icons are stored (depends on the operating system and the
                installation method)

            base_pixbuf (GdkPixbuf.Pixbuf): The base pixbuf

            name (str): One of the strings '_tl', '_tr', '_bl', '_bl_alt' or
                'br', represnting icons in the ../icons/overlays directory

        Return values:

            Returns the composite pixbuf

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 1207 apply_pixbuf_overlay')

        overlay_pixbuf = GdkPixbuf.Pixbuf.new_from_file(
            os.path.abspath(
                os.path.join(
                    icon_dir_path,
                    'overlays',
                    'overlay' + name + '.png',
                ),
            ),
        )

        overlay_pixbuf.composite(
            base_pixbuf,
            0,
            0,
            base_pixbuf.props.width,
            base_pixbuf.props.height,
            0,
            0,
            1,
            1,
            GdkPixbuf.InterpType.BILINEAR,
            250,
        )

        return base_pixbuf


    def setup_bg_colour(self, bg_name):

        """Called initially by self.__init__(), and then again by
        mainapp.TartubeApp.load_config(), set_custom_bg() and
        .reset_custom_bg().

        Sets the value of the IVs self.live_wait_colour, etc. The colours are
        used as backgrounds in the Video Catalogue.

        Args:

            bg_name (str): One of the keys in
                mainapp.TartubeApp.custom_bg_table

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 1253 setup_bg_colour')

        if bg_name in self.app_obj.custom_bg_table:

            mini_list = self.app_obj.custom_bg_table[bg_name]
            rgba_obj = Gdk.RGBA(
                mini_list[0],
                mini_list[1],
                mini_list[2],
                mini_list[3],
            )

            if bg_name == 'live_wait':
                self.live_wait_colour = rgba_obj
            elif bg_name == 'live_now':
                self.live_now_colour = rgba_obj
            elif bg_name == 'debut_wait':
                self.debut_wait_colour = rgba_obj
            elif bg_name == 'debut_now':
                self.debut_now_colour = rgba_obj
            elif bg_name == 'select':
                self.grid_select_colour = rgba_obj
            elif bg_name == 'select_wait':
                self.grid_select_wait_colour = rgba_obj
            elif bg_name == 'select_live':
                self.grid_select_live_colour = rgba_obj
            elif bg_name == 'drag_drop_notify':
                self.drag_drop_notify_colour = rgba_obj
            elif bg_name == 'drag_drop_odd':
                self.drag_drop_odd_colour = rgba_obj
            elif bg_name == 'drag_drop_even':
                self.drag_drop_even_colour = rgba_obj


    # (Create main window widgets)


    def setup_win(self):

        """Called by mainapp.TartubeApp.start_continue().

        Sets up the main window, calling various function to create its
        widgets.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 1299 setup_win')

        # Set the default window size
        self.set_default_size(
            self.app_obj.main_win_width,
            self.app_obj.main_win_height,
        )

        # Set the window's Gtk icon list
        self.set_icon_list(self.win_pixbuf_list)

        # Intercept the user's attempts to close the window, so we can close to
        #   the system tray, if required
        self.connect('delete-event', self.on_delete_event)

        # Detect window resize events, so the size of the Video Catalogue grid
        #   (when visible) can be adjusted smoothly
        self.connect('size-allocate', self.on_window_size_allocate)

        # Allow the user to drag-and-drop videos (for example, from the web
        #   browser) into the main window, adding it the currently selected
        #   folder (or to 'Unsorted Videos' if something else is selected, or
        #   into the Classic Mode tab if it is visible)
        self.connect('drag-data-received', self.on_window_drag_data_received)
        # (Without this line, we get Gtk warnings on some systems)
        self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
        # (Continuing)
        self.drag_dest_set_target_list(None)
        self.drag_dest_add_text_targets()

        # Set up desktop notifications. Notifications can be sent by calling
        #   self.notify_desktop()
        if mainapp.HAVE_NOTIFY_FLAG:
            Notify.init('Tartube')

        # Create main window widgets
        self.setup_grid()
        self.setup_main_menubar()
        self.setup_main_toolbar()
        self.setup_notebook()
        self.setup_videos_tab()
        self.setup_progress_tab()
        self.setup_classic_mode_tab()
        self.setup_drag_drop_tab()
        self.setup_output_tab()
        self.setup_errors_tab()


    def setup_grid(self):

        """Called by self.setup_win().

        Sets up a Gtk.Grid on which all the main window's widgets are placed.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 1355 setup_grid')

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


    def setup_main_menubar(self):

        """Called by self.setup_win().

        Sets up a Gtk.Menu at the top of the main window.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 1369 setup_main_menubar')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Main window menu starts here'
        )

        self.menubar = Gtk.MenuBar()
        self.grid.attach(self.menubar, 0, 0, 1, 1)

        # File column
        file_menu_column = Gtk.MenuItem.new_with_mnemonic(_('_File'))
        self.menubar.add(file_menu_column)

        file_sub_menu = Gtk.Menu()
        file_menu_column.set_submenu(file_sub_menu)

        self.change_db_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Database preferences...'),
        )
        file_sub_menu.append(self.change_db_menu_item)
        self.change_db_menu_item.set_action_name('app.change_db_menu')

        self.check_db_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Check database integrity'),
        )
        file_sub_menu.append(self.check_db_menu_item)
        self.check_db_menu_item.set_action_name('app.check_db_menu')

        # Separator
        file_sub_menu.append(Gtk.SeparatorMenuItem())

        self.save_db_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Save database'),
        )
        file_sub_menu.append(self.save_db_menu_item)
        self.save_db_menu_item.set_action_name('app.save_db_menu')

        self.save_all_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Save _all'),
        )
        file_sub_menu.append(self.save_all_menu_item)
        self.save_all_menu_item.set_action_name('app.save_all_menu')

        # Separator
        file_sub_menu.append(Gtk.SeparatorMenuItem())

        close_tray_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Close to _tray'),
        )
        file_sub_menu.append(close_tray_menu_item)
        close_tray_menu_item.set_action_name('app.close_tray_menu')

        quit_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Quit'))
        file_sub_menu.append(quit_menu_item)
        quit_menu_item.set_action_name('app.quit_menu')

        # Edit column
        edit_menu_column = Gtk.MenuItem.new_with_mnemonic(_('_Edit'))
        self.menubar.add(edit_menu_column)

        edit_sub_menu = Gtk.Menu()
        edit_menu_column.set_submenu(edit_sub_menu)

        self.system_prefs_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_System preferences...'),
        )
        edit_sub_menu.append(self.system_prefs_menu_item)
        self.system_prefs_menu_item.set_action_name('app.system_prefs_menu')

        self.gen_options_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_General download options...'),
        )
        edit_sub_menu.append(self.gen_options_menu_item)
        self.gen_options_menu_item.set_action_name('app.gen_options_menu')

        # System column (MS Windows only)
        if os.name == 'nt':

            system_menu_column = Gtk.MenuItem.new_with_mnemonic(_('_System'))
            self.menubar.add(system_menu_column)

            system_sub_menu = Gtk.Menu()
            system_menu_column.set_submenu(system_sub_menu)

            self.open_msys2_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('Open _MSYS2 terminal...'),
            )
            system_sub_menu.append(self.open_msys2_menu_item)
            self.open_msys2_menu_item.set_action_name('app.open_msys2_menu')

            # Separator
            system_sub_menu.append(Gtk.SeparatorMenuItem())

            self.show_install_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('Show Tartube _install folder'),
            )
            system_sub_menu.append(self.show_install_menu_item)
            self.show_install_menu_item.set_action_name(
                'app.show_install_menu',
            )

            self.show_script_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('Show Tartube _script folder'),
            )
            system_sub_menu.append(self.show_script_menu_item)
            self.show_script_menu_item.set_action_name('app.show_script_menu')

            # Separator
            system_sub_menu.append(Gtk.SeparatorMenuItem())

            self.change_theme_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('_Change theme...'),
            )
            system_sub_menu.append(self.change_theme_menu_item)
            self.change_theme_menu_item.set_action_name(
                'app.change_theme_menu',
            )

        # Media column
        media_menu_column = Gtk.MenuItem.new_with_mnemonic(_('_Media'))
        self.menubar.add(media_menu_column)

        media_sub_menu = Gtk.Menu()
        media_menu_column.set_submenu(media_sub_menu)

        self.add_video_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Add _videos...'),
        )
        media_sub_menu.append(self.add_video_menu_item)
        self.add_video_menu_item.set_action_name('app.add_video_menu')

        self.add_channel_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Add _channel...'),
        )
        media_sub_menu.append(self.add_channel_menu_item)
        self.add_channel_menu_item.set_action_name('app.add_channel_menu')

        self.add_playlist_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Add _playlist...'),
        )
        media_sub_menu.append(self.add_playlist_menu_item)
        self.add_playlist_menu_item.set_action_name('app.add_playlist_menu')

        self.add_folder_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Add _folder...'),
        )
        media_sub_menu.append(self.add_folder_menu_item)
        self.add_folder_menu_item.set_action_name('app.add_folder_menu')

        # Separator
        media_sub_menu.append(Gtk.SeparatorMenuItem())

        self.add_bulk_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Add many channels/playlists...'),
        )
        media_sub_menu.append(self.add_bulk_menu_item)
        self.add_bulk_menu_item.set_action_name('app.add_bulk_menu')

        self.reset_container_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Reset channel/playlist names...'),
        )
        media_sub_menu.append(self.reset_container_menu_item)
        self.reset_container_menu_item.set_action_name(
            'app.reset_container_menu',
        )

        # Separator
        media_sub_menu.append(Gtk.SeparatorMenuItem())

        export_import_submenu = Gtk.Menu()

        self.export_db_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Export from database...'),
        )
        export_import_submenu.append(self.export_db_menu_item)
        self.export_db_menu_item.set_action_name('app.export_db_menu')

        self.import_db_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Import into database...'),
        )
        export_import_submenu.append(self.import_db_menu_item)
        self.import_db_menu_item.set_action_name('app.import_db_menu')

        # Separator
        export_import_submenu.append(Gtk.SeparatorMenuItem())

        self.import_yt_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Import _YouTube subscriptions...'),
        )
        export_import_submenu.append(self.import_yt_menu_item)
        self.import_yt_menu_item.set_action_name(
            'app.import_yt_menu',
        )

        export_import_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Export/import'))
        export_import_menu_item.set_submenu(export_import_submenu)
        media_sub_menu.append(export_import_menu_item)

        # Separator
        media_sub_menu.append(Gtk.SeparatorMenuItem())

        switch_view_submenu = Gtk.Menu()

        last_type = None
        # (mainapp.TartubeApp.catalogue_mode_list specifies catalogues modes
        #       in groups of three, in the form [mode, mode_type, description]
        for mini_list in self.app_obj.catalogue_mode_list:

            # Catalogue modes are already sorted by mode type. Place a
            #   separator when we move from one mode type to the next
            if last_type is not None and mini_list[1] != last_type:

                # Separator
                switch_view_submenu.append(Gtk.SeparatorMenuItem())

            last_type = mini_list[1]

            menu_item = Gtk.MenuItem.new_with_mnemonic(mini_list[2])
            switch_view_submenu.append(menu_item)
            menu_item.connect(
                'activate',
                self.on_switch_view,
                mini_list[0],
                mini_list[1],
            )

        self.switch_view_menu_item = \
        Gtk.MenuItem.new_with_mnemonic(_('_Switch between views'))
        self.switch_view_menu_item.set_submenu(switch_view_submenu)
        media_sub_menu.append(self.switch_view_menu_item)

        # Separator
        media_sub_menu.append(Gtk.SeparatorMenuItem())

        show_hide_submenu = Gtk.Menu()

        self.hide_system_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
            _('_Hide (most) system folders'),
        )
        show_hide_submenu.append(self.hide_system_menu_item)
        self.hide_system_menu_item.set_active(
            self.app_obj.toolbar_system_hide_flag,
        )
        self.hide_system_menu_item.set_action_name('app.hide_system_menu')

        self.show_hidden_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Show hidden folders'),
        )
        show_hide_submenu.append(self.show_hidden_menu_item)
        self.show_hidden_menu_item.set_action_name('app.show_hidden_menu')

        self.show_hide_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('S_how/hide'))
        self.show_hide_menu_item.set_submenu(show_hide_submenu)
        media_sub_menu.append(self.show_hide_menu_item)

        profile_submenu = Gtk.Menu()

        self.switch_profile_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Switch profile'),
        )
        profile_submenu.append(self.switch_profile_menu_item)
        self.switch_profile_menu_item.set_submenu(
            self.switch_profile_popup_submenu(),
        )

        self.auto_switch_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
            _('_Remember last profile'))
        profile_submenu.append(self.auto_switch_menu_item)
        self.auto_switch_menu_item.set_active(
            self.app_obj.auto_switch_profile_flag,
        )
        self.auto_switch_menu_item.set_action_name(
            'app.auto_switch_menu',
        )

        # Separator
        profile_submenu.append(Gtk.SeparatorMenuItem())

        self.create_profile_menu_item = \
        Gtk.MenuItem.new_with_mnemonic(
            _('_Create profile'),
        )
        profile_submenu.append(self.create_profile_menu_item)
        self.create_profile_menu_item.set_action_name(
            'app.create_profile_menu',
        )

        self.delete_profile_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Delete profile'),
        )
        profile_submenu.append(self.delete_profile_menu_item)
        self.delete_profile_menu_item.set_submenu(
            self.delete_profile_popup_submenu(),
        )

        # Separator
        profile_submenu.append(Gtk.SeparatorMenuItem())

        self.mark_containers_menu_item = \
        Gtk.MenuItem.new_with_mnemonic(
            _('_Mark all for download'),
        )
        profile_submenu.append(self.mark_containers_menu_item)
        self.mark_containers_menu_item.set_action_name(
            'app.mark_all_menu',
        )

        self.unmark_containers_menu_item = \
        Gtk.MenuItem.new_with_mnemonic(
            _('_Unmark all for download'),
        )
        profile_submenu.append(self.unmark_containers_menu_item)
        self.unmark_containers_menu_item.set_action_name(
            'app.unmark_all_menu',
        )

        self.profile_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Pr_ofiles'))
        self.profile_menu_item.set_submenu(profile_submenu)
        media_sub_menu.append(self.profile_menu_item)

        if self.app_obj.debug_test_media_menu_flag \
        or self.app_obj.debug_test_code_menu_flag:

            # Separator
            media_sub_menu.append(Gtk.SeparatorMenuItem())

        if self.app_obj.debug_test_media_menu_flag:

            self.test_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('Ad_d test media'),
            )
            media_sub_menu.append(self.test_menu_item)
            self.test_menu_item.set_action_name('app.test_menu')

        if self.app_obj.debug_test_code_menu_flag:

            self.test_code_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('Run _test code'),
            )
            media_sub_menu.append(self.test_code_menu_item)
            self.test_code_menu_item.set_action_name('app.test_code_menu')

        # Operations column
        ops_menu_column = Gtk.MenuItem.new_with_mnemonic(_('_Operations'))
        self.menubar.add(ops_menu_column)

        ops_sub_menu = Gtk.Menu()
        ops_menu_column.set_submenu(ops_sub_menu)

        self.check_all_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Check all'),
        )
        ops_sub_menu.append(self.check_all_menu_item)
        self.check_all_menu_item.set_action_name('app.check_all_menu')

        self.download_all_menu_item = \
        Gtk.MenuItem.new_with_mnemonic(_('_Download all'))
        ops_sub_menu.append(self.download_all_menu_item)
        self.download_all_menu_item.set_action_name('app.download_all_menu')

        self.custom_dl_all_menu_item = \
        Gtk.MenuItem.new_with_mnemonic(_('C_ustom download all'))
        ops_sub_menu.append(self.custom_dl_all_menu_item)
        self.custom_dl_all_menu_item.set_submenu(
            self.custom_dl_popup_submenu(),
        )

        # Separator
        ops_sub_menu.append(Gtk.SeparatorMenuItem())

        self.refresh_db_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Refresh database...'),
        )
        ops_sub_menu.append(self.refresh_db_menu_item)
        self.refresh_db_menu_item.set_action_name('app.refresh_db_menu')

        # Separator
        ops_sub_menu.append(Gtk.SeparatorMenuItem())

        downloader = self.app_obj.get_downloader()
        self.update_ytdl_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('U_pdate') + ' ' + downloader,
        )
        ops_sub_menu.append(self.update_ytdl_menu_item)
        self.update_ytdl_menu_item.set_action_name('app.update_ytdl_menu')

        self.test_ytdl_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Test') + ' ' + downloader + '...',
        )
        ops_sub_menu.append(self.test_ytdl_menu_item)
        self.test_ytdl_menu_item.set_action_name('app.test_ytdl_menu')

        # Separator
        ops_sub_menu.append(Gtk.SeparatorMenuItem())

        if os.name == 'nt':

            self.install_ffmpeg_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('Install _FFmpeg...'),
            )
            ops_sub_menu.append(self.install_ffmpeg_menu_item)
            self.install_ffmpeg_menu_item.set_action_name(
                'app.install_ffmpeg_menu',
            )

            self.install_matplotlib_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('Install _matplotlib...'),
            )
            ops_sub_menu.append(self.install_matplotlib_menu_item)
            self.install_matplotlib_menu_item.set_action_name(
                'app.install_matplotlib_menu',
            )

            self.install_streamlink_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('Install _streamlink...'),
            )
            ops_sub_menu.append(self.install_streamlink_menu_item)
            self.install_streamlink_menu_item.set_action_name(
                'app.install_streamlink_menu',
            )

            # Separator
            ops_sub_menu.append(Gtk.SeparatorMenuItem())

        self.tidy_up_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Tidy up _files...'),
        )
        ops_sub_menu.append(self.tidy_up_menu_item)
        self.tidy_up_menu_item.set_action_name(
            'app.tidy_up_menu',
        )

        # Separator
        ops_sub_menu.append(Gtk.SeparatorMenuItem())

        self.stop_operation_menu_item = \
        Gtk.MenuItem.new_with_mnemonic(_('_Stop current operation'))
        ops_sub_menu.append(self.stop_operation_menu_item)
        self.stop_operation_menu_item.set_action_name(
            'app.stop_operation_menu',
        )
        self.stop_operation_menu_item.set_sensitive(False)

        self.stop_soon_menu_item = \
        Gtk.MenuItem.new_with_mnemonic(_('Stop _after current videos'))
        ops_sub_menu.append(self.stop_soon_menu_item)
        self.stop_soon_menu_item.set_action_name(
            'app.stop_soon_menu',
        )
        self.stop_soon_menu_item.set_sensitive(False)

        # Livestreams column
        live_menu_column = Gtk.MenuItem.new_with_mnemonic(_('_Livestreams'))
        self.menubar.add(live_menu_column)

        live_sub_menu = Gtk.Menu()
        live_menu_column.set_submenu(live_sub_menu)

        self.live_prefs_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Livestream preferences...'),
        )
        live_sub_menu.append(self.live_prefs_menu_item)
        self.live_prefs_menu_item.set_action_name('app.live_prefs_menu')

        # Separator
        live_sub_menu.append(Gtk.SeparatorMenuItem())

        self.update_live_menu_item = \
        Gtk.MenuItem.new_with_mnemonic(_('_Update existing livestreams'))
        live_sub_menu.append(self.update_live_menu_item)
        self.update_live_menu_item.set_action_name('app.update_live_menu')

        self.cancel_live_menu_item = \
        Gtk.MenuItem.new_with_mnemonic(_('_Cancel all livestream alerts'))
        live_sub_menu.append(self.cancel_live_menu_item)
        self.cancel_live_menu_item.set_action_name('app.cancel_live_menu')

        # Help column
        help_menu_column = Gtk.MenuItem.new_with_mnemonic(_('_Help'))
        self.menubar.add(help_menu_column)

        help_sub_menu = Gtk.Menu()
        help_menu_column.set_submenu(help_sub_menu)

        about_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_About...'))
        help_sub_menu.append(about_menu_item)
        about_menu_item.set_action_name('app.about_menu')

        # Separator
        help_sub_menu.append(Gtk.SeparatorMenuItem())

        tutorial_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Show _tutorial...')
        )
        help_sub_menu.append(tutorial_menu_item)
        tutorial_menu_item.set_action_name('app.tutorial_menu')

        # Separator
        help_sub_menu.append(Gtk.SeparatorMenuItem())

        check_version_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Check for _updates'),
        )
        help_sub_menu.append(check_version_menu_item)
        check_version_menu_item.set_action_name('app.check_version_menu')

        go_website_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Go to _website'),
        )
        help_sub_menu.append(go_website_menu_item)
        go_website_menu_item.set_action_name('app.go_website_menu')

        # Separator
        help_sub_menu.append(Gtk.SeparatorMenuItem())

        send_feedback_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Send _feedback'),
        )
        help_sub_menu.append(send_feedback_menu_item)
        send_feedback_menu_item.set_action_name('app.send_feedback_menu')


    def setup_main_toolbar(self):

        """Called by self.setup_win(). Also called by
        self.redraw_main_toolbar().

        Sets up a Gtk.Toolbar near the top of the main window, below the menu,
        replacing the previous one, if it exists.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 1904 setup_main_toolbar')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Main window toolbar starts here'
        )

        # If a toolbar already exists, destroy it to make room for the new one
        if self.main_toolbar:
            self.grid.remove(self.main_toolbar)

        # Create a new toolbar (hidden, if required)
        self.main_toolbar = Gtk.Toolbar()
        if not self.app_obj.toolbar_hide_flag:
            self.grid.attach(self.main_toolbar, 0, 1, 1, 1)

        # Toolbar items. If mainapp.TartubeApp.toolbar_squeeze_flag is True,
        #   we don't display labels in the toolbuttons
        squeeze_flag = self.app_obj.toolbar_squeeze_flag

        if not squeeze_flag:
            self.add_video_toolbutton = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_video_small'],
                ),
            )
            self.add_video_toolbutton.set_label(_('Videos'))
            self.add_video_toolbutton.set_is_important(True)
        else:
            self.add_video_toolbutton = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_video_large'],
                ),
            )

        self.main_toolbar.insert(self.add_video_toolbutton, -1)
        self.add_video_toolbutton.set_tooltip_text(_('Add new videos'))
        self.add_video_toolbutton.set_action_name('app.add_video_toolbutton')

        if not squeeze_flag:
            self.add_channel_toolbutton = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_channel_small'],
                ),
            )
            self.add_channel_toolbutton.set_label(_('Channel'))
            self.add_channel_toolbutton.set_is_important(True)
        else:
            self.add_channel_toolbutton = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_channel_large'],
                ),
            )

        self.main_toolbar.insert(self.add_channel_toolbutton, -1)
        self.add_channel_toolbutton.set_tooltip_text(_('Add a new channel'))
        self.add_channel_toolbutton.set_action_name(
            'app.add_channel_toolbutton',
        )

        if not squeeze_flag:
            self.add_playlist_toolbutton = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_playlist_small'],
                ),
            )
            self.add_playlist_toolbutton.set_label(_('Playlist'))
            self.add_playlist_toolbutton.set_is_important(True)
        else:
            self.add_playlist_toolbutton = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_playlist_large'],
                ),
            )

        self.main_toolbar.insert(self.add_playlist_toolbutton, -1)
        self.add_playlist_toolbutton.set_tooltip_text(_('Add a new playlist'))
        self.add_playlist_toolbutton.set_action_name(
            'app.add_playlist_toolbutton',
        )

        if not squeeze_flag:
            self.add_folder_toolbutton = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_folder_small'],
                ),
            )
            self.add_folder_toolbutton.set_label(_('Folder'))
            self.add_folder_toolbutton.set_is_important(True)
        else:
            self.add_folder_toolbutton = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_folder_large'],
                ),
            )

        self.main_toolbar.insert(self.add_folder_toolbutton, -1)
        self.add_folder_toolbutton.set_tooltip_text(_('Add a new folder'))
        self.add_folder_toolbutton.set_action_name('app.add_folder_toolbutton')

#       # (Conversely, if there are no labels, then we have enough room for a
#       #   separator)
#       if squeeze_flag:
#           self.main_toolbar.insert(Gtk.SeparatorToolItem(), -1)

        if not squeeze_flag:
            self.check_all_toolbutton = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_check_small'],
                ),
            )
            self.check_all_toolbutton.set_label(_('Check'))
            self.check_all_toolbutton.set_is_important(True)
        else:
            self.check_all_toolbutton = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_check_large'],
                ),
            )

        self.main_toolbar.insert(self.check_all_toolbutton, -1)
        self.check_all_toolbutton.set_tooltip_text(
            _('Check all videos, channels, playlists and folders'),
        )
        self.check_all_toolbutton.set_action_name('app.check_all_toolbutton')

        if not squeeze_flag:
            self.download_all_toolbutton = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_download_small'],
                ),
            )
            self.download_all_toolbutton.set_label(_('Download'))
            self.download_all_toolbutton.set_is_important(True)
        else:
            self.download_all_toolbutton = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_download_large'],
                ),
            )

        self.main_toolbar.insert(self.download_all_toolbutton, -1)
        self.download_all_toolbutton.set_tooltip_text(
            _('Download all videos, channels, playlists and folders'),
        )
        self.download_all_toolbutton.set_action_name(
            'app.download_all_toolbutton',
        )

#       if squeeze_flag:
#           self.main_toolbar.insert(Gtk.SeparatorToolItem(), -1)

        if not squeeze_flag:
            self.stop_operation_toolbutton = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_stop_small'],
                ),
            )
            self.stop_operation_toolbutton.set_label(_('Stop'))
            self.stop_operation_toolbutton.set_is_important(True)
        else:
            self.stop_operation_toolbutton = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_stop_large'],
                ),
            )

        self.main_toolbar.insert(self.stop_operation_toolbutton, -1)
        self.stop_operation_toolbutton.set_sensitive(False)
        self.stop_operation_toolbutton.set_tooltip_text(
            _('Stop the current operation'),
        )
        self.stop_operation_toolbutton.set_action_name(
            'app.stop_operation_toolbutton',
        )

        if not squeeze_flag:

            # (There's only enough room in the toolbar for buttons to open the
            #   preferences window/download options window, when labels aren't
            #   visible)
            self.system_prefs_toolbutton = None
            self.gen_options_toolbutton = None

        else:

            self.system_prefs_toolbutton = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_preferences_large'],
                ),
            )

            self.main_toolbar.insert(self.system_prefs_toolbutton, -1)
            self.system_prefs_toolbutton.set_tooltip_text(
                _('System preferences'),
            )
            self.system_prefs_toolbutton.set_action_name(
                'app.system_prefs_toolbutton',
            )

            self.gen_options_toolbutton = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_options_large'],
                ),
            )

            self.main_toolbar.insert(self.gen_options_toolbutton, -1)
            self.gen_options_toolbutton.set_tooltip_text(
                _('General download options'),
            )
            self.gen_options_toolbutton.set_action_name(
                'app.gen_options_toolbutton',
            )

        if not squeeze_flag:
            self.switch_view_toolbutton = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_switch_small'],
                ),
            )
            self.switch_view_toolbutton.set_label(_('Switch'))
            self.switch_view_toolbutton.set_is_important(True)
        else:
            self.switch_view_toolbutton = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_switch_large'],
                ),
            )

        self.main_toolbar.insert(self.switch_view_toolbutton, -1)
        self.switch_view_toolbutton.set_tooltip_text(
            _('Switch between simple and complex views'),
        )
        self.switch_view_toolbutton.set_action_name(
            'app.switch_view_toolbutton',
        )

        if not squeeze_flag:
            self.hide_system_toolbutton = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_hide_small'],
                ),
            )
            if not self.app_obj.toolbar_system_hide_flag:
                self.hide_system_toolbutton.set_label(_('Hide'))
            else:
                self.hide_system_toolbutton.set_label(_('Show'))
            self.hide_system_toolbutton.set_is_important(True)
        else:
            self.hide_system_toolbutton = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_hide_large'],
                ),
            )

        self.main_toolbar.insert(self.hide_system_toolbutton, -1)
        if not self.app_obj.toolbar_system_hide_flag:
            self.hide_system_toolbutton.set_tooltip_text(
                _('Hide (most) system folders'),
            )
        else:
            self.hide_system_toolbutton.set_tooltip_text(
                _('Show all system folders'),
            )
        self.hide_system_toolbutton.set_action_name(
            'app.hide_system_toolbutton',
        )

#       if squeeze_flag:
#           self.main_toolbar.insert(Gtk.SeparatorToolItem(), -1)

        if not squeeze_flag:
            quit_button = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_quit_small'],
                ),
            )
            quit_button.set_label(_('Quit'))
            quit_button.set_is_important(True)
        else:
            quit_button = Gtk.ToolButton.new(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['tool_quit_large'],
                ),
            )

        self.main_toolbar.insert(quit_button, -1)
        quit_button.set_tooltip_text(_('Close Tartube'))
        quit_button.set_action_name('app.quit_toolbutton')


    def setup_notebook(self):

        """Called by self.setup_win().

        Sets up a Gtk.Notebook occupying all the space below the menu and
        toolbar. Creates two tabs, the Videos tab and the Progress tab.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 2203 setup_notebook')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Main window tabs are defined here'
        )

        self.notebook = Gtk.Notebook()
        self.grid.attach(self.notebook, 0, 2, 1, 1)
        self.notebook.set_border_width(self.spacing_size)
        self.notebook.connect('switch-page', self.on_notebook_switch_page)

        # Videos tab
        self.videos_tab = Gtk.Box()
        self.videos_label = Gtk.Label.new_with_mnemonic(_('_Videos'))
        self.notebook.append_page(self.videos_tab, self.videos_label)
        self.videos_tab.set_hexpand(True)
        self.videos_tab.set_vexpand(True)
        self.videos_tab.set_border_width(self.spacing_size)

        # Progress tab
        self.progress_tab = Gtk.Box()
        self.progress_label = Gtk.Label.new_with_mnemonic(_('_Progress'))
        self.notebook.append_page(self.progress_tab, self.progress_label)
        self.progress_tab.set_hexpand(True)
        self.progress_tab.set_vexpand(True)
        self.progress_tab.set_border_width(self.spacing_size)

        # Classic Mode tab
        self.classic_tab = Gtk.Box()
        self.classic_label = Gtk.Label.new_with_mnemonic(_('_Classic Mode'))
        if not __main__.__pkg_no_download_flag__:
            self.notebook.append_page(self.classic_tab, self.classic_label)
        self.classic_tab.set_hexpand(True)
        self.classic_tab.set_vexpand(True)
        self.classic_tab.set_border_width(self.spacing_size)

        # Drag and Drop tab
        self.drag_drop_tab = Gtk.Box()
        self.drag_drop_label = Gtk.Label.new_with_mnemonic(_('_Drag and Drop'))
        if not __main__.__pkg_no_download_flag__:
            self.notebook.append_page(self.drag_drop_tab, self.drag_drop_label)
        self.drag_drop_tab.set_hexpand(True)
        self.drag_drop_tab.set_vexpand(True)
        self.drag_drop_tab.set_border_width(self.spacing_size)

        # Output tab
        self.output_tab = Gtk.Box()
        self.output_label = Gtk.Label.new_with_mnemonic(_('_Output'))
        self.notebook.append_page(self.output_tab, self.output_label)
        self.output_tab.set_hexpand(True)
        self.output_tab.set_vexpand(True)
        self.output_tab.set_border_width(self.spacing_size)

        # Errors/Warnings tab
        self.errors_tab = Gtk.Box()
        self.errors_label = Gtk.Label.new_with_mnemonic(
            _('_Errors / Warnings'),
        )
        self.notebook.append_page(self.errors_tab, self.errors_label)
        self.errors_tab.set_hexpand(True)
        self.errors_tab.set_vexpand(True)
        self.errors_tab.set_border_width(self.spacing_size)


    def setup_videos_tab(self):

        """Called by self.setup_win().

        Creates widgets for the Videos tab.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 2275 setup_videos_tab')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Main window\'s Videos tab'
        )

        self.videos_paned = Gtk.HPaned()
        self.videos_tab.pack_start(self.videos_paned, True, True, 0)
        self.videos_paned.set_position(
            self.app_obj.main_win_videos_slider_posn,
        )
        self.videos_paned.set_wide_handle(True)

        # Left-hand side
        self.video_index_vbox = Gtk.VBox.new(False, self.spacing_size)
        self.videos_paned.pack1(self.video_index_vbox, True, False)
        # (Detect the user dragging the paned slider by checking the size of
        #   the vbox)
        self.video_index_vbox.connect(
            'size-allocate',
            self.on_paned_size_allocate,
        )

        self.video_index_frame = Gtk.Frame()
        self.video_index_vbox.pack_start(
            self.video_index_frame,
            True,
            True,
            0,
        )

        self.video_index_scrolled = Gtk.ScrolledWindow()
        self.video_index_frame.add(self.video_index_scrolled)
        self.video_index_scrolled.set_policy(
            Gtk.PolicyType.AUTOMATIC,
            Gtk.PolicyType.AUTOMATIC,
        )

        # Video index
        self.video_index_reset()

        # 'Check all', 'Download all' and 'Custom download all' buttons
        self.button_box = Gtk.VBox.new(True, self.spacing_size)
        self.video_index_vbox.pack_start(self.button_box, False, False, 0)

        self.check_media_button = Gtk.Button()
        self.button_box.pack_start(self.check_media_button, True, True, 0)
        self.check_media_button.set_label(_('Check all'))
        self.check_media_button.set_tooltip_text(
            _('Check all videos, channels, playlists and folders'),
        )
        self.check_media_button.set_action_name('app.check_all_button')

        self.download_media_button = Gtk.Button()
        self.button_box.pack_start(self.download_media_button, True, True, 0)
        self.download_media_button.set_label(_('Download all'))
        self.download_media_button.set_tooltip_text(
            _('Download all videos, channels, playlists and folders'),
        )
        self.download_media_button.set_action_name('app.download_all_button')

        if self.app_obj.show_custom_dl_button_flag:

            self.custom_dl_media_button = Gtk.Button()
            self.button_box.pack_start(
                self.custom_dl_media_button,
                True,
                True,
                0
            )
            self.custom_dl_media_button.set_label(_('Custom download all'))
            self.custom_dl_media_button.set_tooltip_text(
                _(
                'Perform a custom download of all videos, channels,' \
                + ' playlists and folders',
                ),
            )
            self.custom_dl_media_button.set_action_name(
                'app.custom_dl_all_button',
            )

        # Right-hand side
        self.video_catalogue_vbox = Gtk.VBox()
        self.videos_paned.pack2(self.video_catalogue_vbox, True, True)

        # Video catalogue
        self.catalogue_frame = Gtk.Frame()
        self.video_catalogue_vbox.pack_start(
            self.catalogue_frame,
            True,
            True,
            0,
        )

        self.catalogue_scrolled = Gtk.ScrolledWindow()
        self.catalogue_frame.add(self.catalogue_scrolled)
        self.catalogue_scrolled.set_policy(
            Gtk.PolicyType.AUTOMATIC,
            Gtk.PolicyType.AUTOMATIC,
        )

        # (An invisible VBox adds a bit of space between the Video Catalogue
        #   and its toolbar)
        self.video_catalogue_vbox.pack_start(
            Gtk.VBox(),
            False,
            False,
            self.spacing_size / 2,
        )

        # Video catalogue toolbar
        self.catalogue_toolbar_frame = Gtk.Frame()
        self.video_catalogue_vbox.pack_start(
            self.catalogue_toolbar_frame,
            False,
            False,
            0,
        )

        self.catalogue_toolbar_vbox = Gtk.VBox()
        self.catalogue_toolbar_frame.add(self.catalogue_toolbar_vbox)

        self.catalogue_toolbar = Gtk.Toolbar()
        self.catalogue_toolbar_vbox.pack_start(
            self.catalogue_toolbar,
            False,
            False,
            0,
        )

        toolitem = Gtk.ToolItem.new()
        self.catalogue_toolbar.insert(toolitem, -1)
        toolitem.add(Gtk.Label(_('Page') + '  '))

        toolitem2 = Gtk.ToolItem.new()
        self.catalogue_toolbar.insert(toolitem2, -1)
        self.catalogue_page_entry = Gtk.Entry()
        toolitem2.add(self.catalogue_page_entry)
        self.catalogue_page_entry.set_text(
            str(self.catalogue_toolbar_current_page),
        )
        self.catalogue_page_entry.set_width_chars(4)
        self.catalogue_page_entry.set_sensitive(False)
        self.catalogue_page_entry.set_tooltip_text(_('Set visible page'))
        self.catalogue_page_entry.connect(
            'activate',
            self.on_video_catalogue_page_entry_activated,
        )

        toolitem3 = Gtk.ToolItem.new()
        self.catalogue_toolbar.insert(toolitem3, -1)
        toolitem3.add(Gtk.Label('  /  '))

        toolitem4 = Gtk.ToolItem.new()
        self.catalogue_toolbar.insert(toolitem4, -1)
        self.catalogue_last_entry = Gtk.Entry()
        toolitem4.add(self.catalogue_last_entry)
        self.catalogue_last_entry.set_text(
            str(self.catalogue_toolbar_last_page),
        )
        self.catalogue_last_entry.set_width_chars(4)
        self.catalogue_last_entry.set_sensitive(False)
        self.catalogue_last_entry.set_editable(False)

        # Separator. In this instance, empty labels look better than
        #   Gtk.SeparatorToolItem
#        self.catalogue_toolbar.insert(Gtk.SeparatorToolItem(), -1)
        toolitem = Gtk.ToolItem.new()
        self.catalogue_toolbar.insert(toolitem, -1)
        toolitem.add(Gtk.Label('   '))

        toolitem5 = Gtk.ToolItem.new()
        self.catalogue_toolbar.insert(toolitem5, -1)
        toolitem5.add(Gtk.Label('  ' + _('Size') + '  '))

        toolitem6 = Gtk.ToolItem.new()
        self.catalogue_toolbar.insert(toolitem6, -1)
        self.catalogue_size_entry = Gtk.Entry()
        toolitem6.add(self.catalogue_size_entry)
        self.catalogue_size_entry.set_text(
            str(self.app_obj.catalogue_page_size),
        )
        self.catalogue_size_entry.set_width_chars(4)
        self.catalogue_size_entry.set_tooltip_text(_('Set page size'))
        self.catalogue_size_entry.connect(
            'activate',
            self.on_video_catalogue_size_entry_activated,
        )

        # Separator
        toolitem = Gtk.ToolItem.new()
        self.catalogue_toolbar.insert(toolitem, -1)
        toolitem.add(Gtk.Label('   '))

        if not self.app_obj.show_custom_icons_flag:
            self.catalogue_first_button \
            = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GOTO_FIRST)
        else:
            self.catalogue_first_button = Gtk.ToolButton.new()
            self.catalogue_first_button.set_icon_widget(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['stock_goto_first'],
                ),
            )
        self.catalogue_toolbar.insert(self.catalogue_first_button, -1)
        self.catalogue_first_button.set_sensitive(False)
        self.catalogue_first_button.set_tooltip_text(_('Go to first page'))
        self.catalogue_first_button.set_action_name(
            'app.first_page_toolbutton',
        )

        if not self.app_obj.show_custom_icons_flag:
            self.catalogue_back_button \
            = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GO_BACK)
        else:
            self.catalogue_back_button = Gtk.ToolButton.new()
            self.catalogue_back_button.set_icon_widget(
                Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_go_back']),
            )
        self.catalogue_toolbar.insert(self.catalogue_back_button, -1)
        self.catalogue_back_button.set_sensitive(False)
        self.catalogue_back_button.set_tooltip_text(_('Go to previous page'))
        self.catalogue_back_button.set_action_name(
            'app.previous_page_toolbutton',
        )

        if not self.app_obj.show_custom_icons_flag:
            self.catalogue_forwards_button \
            = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GO_FORWARD)
        else:
            self.catalogue_forwards_button = Gtk.ToolButton.new()
            self.catalogue_forwards_button.set_icon_widget(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['stock_go_forward'],
                ),
            )
        self.catalogue_toolbar.insert(self.catalogue_forwards_button, -1)
        self.catalogue_forwards_button.set_sensitive(False)
        self.catalogue_forwards_button.set_tooltip_text(_('Go to next page'))
        self.catalogue_forwards_button.set_action_name(
            'app.next_page_toolbutton',
        )

        if not self.app_obj.show_custom_icons_flag:
            self.catalogue_last_button \
            = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GOTO_LAST)
        else:
            self.catalogue_last_button = Gtk.ToolButton.new()
            self.catalogue_last_button.set_icon_widget(
                Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_goto_last']),
            )
        self.catalogue_toolbar.insert(self.catalogue_last_button, -1)
        self.catalogue_last_button.set_sensitive(False)
        self.catalogue_last_button.set_tooltip_text(_('Go to last page'))
        self.catalogue_last_button.set_action_name(
            'app.last_page_toolbutton',
        )

        if not self.app_obj.show_custom_icons_flag:
            self.catalogue_scroll_up_button \
            = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GO_UP)
        else:
            self.catalogue_scroll_up_button = Gtk.ToolButton.new()
            self.catalogue_scroll_up_button.set_icon_widget(
                Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_go_up']),
            )
        self.catalogue_toolbar.insert(self.catalogue_scroll_up_button, -1)
        self.catalogue_scroll_up_button.set_sensitive(False)
        self.catalogue_scroll_up_button.set_tooltip_text(_('Scroll to top'))
        self.catalogue_scroll_up_button.set_action_name(
            'app.scroll_up_toolbutton',
        )

        if not self.app_obj.show_custom_icons_flag:
            self.catalogue_scroll_down_button \
            = Gtk.ToolButton.new_from_stock(Gtk.STOCK_GO_DOWN)
        else:
            self.catalogue_scroll_down_button = Gtk.ToolButton.new()
            self.catalogue_scroll_down_button.set_icon_widget(
                Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_go_down']),
            )
        self.catalogue_toolbar.insert(self.catalogue_scroll_down_button, -1)
        self.catalogue_scroll_down_button.set_sensitive(False)
        self.catalogue_scroll_down_button.set_tooltip_text(
            _('Scroll to bottom'),
        )
        self.catalogue_scroll_down_button.set_action_name(
            'app.scroll_down_toolbutton',
        )

        if not self.app_obj.show_custom_icons_flag:
            self.catalogue_show_filter_button \
            = Gtk.ToolButton.new_from_stock(Gtk.STOCK_SORT_ASCENDING)
        else:
            self.catalogue_show_filter_button = Gtk.ToolButton.new()
            self.catalogue_show_filter_button.set_icon_widget(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['stock_hide_filter']
                ),
            )
        self.catalogue_toolbar.insert(self.catalogue_show_filter_button, -1)
        if not self.app_obj.catalogue_show_filter_flag:
            self.catalogue_show_filter_button.set_sensitive(False)
        self.catalogue_show_filter_button.set_tooltip_text(
            _('Show more settings'),
        )
        self.catalogue_show_filter_button.set_action_name(
            'app.show_filter_toolbutton',
        )

        # Second toolbar, which is not actually added to the VBox until the
        #   call to self.update_catalogue_filter_widgets()
        self.catalogue_toolbar2 = Gtk.Toolbar()
        self.catalogue_toolbar2.set_visible(False)

        toolitem7 = Gtk.ToolItem.new()
        self.catalogue_toolbar2.insert(toolitem7, -1)
        toolitem7.add(Gtk.Label(_('Sort') + '   '))

        toolitem8 = Gtk.ToolItem.new()
        self.catalogue_toolbar2.insert(toolitem8, -1)

        store = Gtk.ListStore(str, str)
        store.append( [ _('Upload time') , 'default'] )
        store.append( [ _('Name') , 'alpha'] )
        store.append( [ _('Download time') , 'receive'] )
        store.append( [ _('Database ID') , 'dbid'] )

        self.catalogue_sort_combo = Gtk.ComboBox.new_with_model(store)
        toolitem8.add(self.catalogue_sort_combo)
        renderer_text = Gtk.CellRendererText()
        self.catalogue_sort_combo.pack_start(renderer_text, True)
        self.catalogue_sort_combo.add_attribute(renderer_text, 'text', 0)
        self.catalogue_sort_combo.set_entry_text_column(0)
        self.catalogue_sort_combo.set_sensitive(False)
        # (Can't use a named action with a Gtk.ComboBox, so use a callback
        #   instead)
        self.catalogue_sort_combo.connect(
            'changed',
            self.on_video_catalogue_sort_combo_changed,
        )

        if not self.app_obj.catalogue_reverse_sort_flag:
            if not self.app_obj.show_custom_icons_flag:
                self.catalogue_reverse_toolbutton \
                = Gtk.ToolButton.new_from_stock(Gtk.STOCK_SORT_ASCENDING)
            else:
                self.catalogue_reverse_toolbutton = Gtk.ToolButton.new()
                self.catalogue_reverse_toolbutton.set_icon_widget(
                    Gtk.Image.new_from_pixbuf(
                        self.pixbuf_dict['stock_sort_ascending'],
                    ),
                )
            self.catalogue_reverse_toolbutton.set_tooltip_text(
                _('Reverse sort'),
            )
        else:
            if not self.app_obj.show_custom_icons_flag:
                self.catalogue_reverse_toolbutton \
                = Gtk.ToolButton.new_from_stock(Gtk.STOCK_SORT_DESCENDING)
            else:
                self.catalogue_reverse_toolbutton = Gtk.ToolButton.new()
                self.catalogue_reverse_toolbutton.set_icon_widget(
                    Gtk.Image.new_from_pixbuf(
                        self.pixbuf_dict['stock_sort_descending'],
                    ),
                )
            self.catalogue_reverse_toolbutton.set_tooltip_text(
                _('Undo reverse sort'),
            )
        self.catalogue_toolbar2.insert(self.catalogue_reverse_toolbutton, -1)
        self.catalogue_reverse_toolbutton.set_sensitive(False)
        self.catalogue_reverse_toolbutton.set_action_name(
            'app.reverse_sort_toolbutton',
        )

        if not self.app_obj.show_custom_icons_flag:
            self.catalogue_resort_button \
            = Gtk.ToolButton.new_from_stock(Gtk.STOCK_REDO)
        else:
            self.catalogue_resort_button = Gtk.ToolButton.new()
            self.catalogue_resort_button.set_icon_widget(
                Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_redo']),
            )
        self.catalogue_toolbar2.insert(self.catalogue_resort_button, -1)
        self.catalogue_resort_button.set_sensitive(False)
        self.catalogue_resort_button.set_tooltip_text(
            _('Resort videos'),
        )
        self.catalogue_resort_button.set_action_name(
            'app.resort_toolbutton',
        )

        # Separator
        toolitem = Gtk.ToolItem.new()
        self.catalogue_toolbar2.insert(toolitem, -1)
#       toolitem.add(Gtk.Label('          '))
        toolitem.add(Gtk.Label('   '))

        toolitem9 = Gtk.ToolItem.new()
        self.catalogue_toolbar2.insert(toolitem9, -1)
        toolitem9.add(Gtk.Label(_('Find date')))

        if not self.app_obj.show_custom_icons_flag:
            self.catalogue_find_date_button \
            = Gtk.ToolButton.new_from_stock(Gtk.STOCK_FIND)
        else:
            self.catalogue_find_date_button = Gtk.ToolButton.new()
            self.catalogue_find_date_button.set_icon_widget(
                Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_find']),
            )
        self.catalogue_toolbar2.insert(self.catalogue_find_date_button, -1)
        self.catalogue_find_date_button.set_sensitive(False)
        self.catalogue_find_date_button.set_tooltip_text(
            _('Find videos by date'),
        )
        self.catalogue_find_date_button.set_action_name(
            'app.find_date_toolbutton',
        )

        if not self.app_obj.show_custom_icons_flag:
            self.catalogue_cancel_date_button \
            = Gtk.ToolButton.new_from_stock(Gtk.STOCK_CANCEL)
        else:
            self.catalogue_cancel_date_button = Gtk.ToolButton.new()
            self.catalogue_cancel_date_button.set_icon_widget(
                Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_cancel']),
            )
        self.catalogue_toolbar2.insert(self.catalogue_cancel_date_button, -1)
        self.catalogue_cancel_date_button.set_sensitive(False)
        self.catalogue_cancel_date_button.set_tooltip_text(
            _('Cancel find videos by date'),
        )
        self.catalogue_cancel_date_button.set_action_name(
            'app.cancel_date_toolbutton',
        )

        # Separator
        toolitem = Gtk.ToolItem.new()
        self.catalogue_toolbar2.insert(toolitem, -1)
#       toolitem.add(Gtk.Label('          '))
        toolitem.add(Gtk.Label('  '))

        toolitem16 = Gtk.ToolItem.new()
        self.catalogue_toolbar2.insert(toolitem16, -1)
        toolitem16.add(Gtk.Label(_('Thumbnail size') + '   '))

        toolitem17 = Gtk.ToolItem.new()
        self.catalogue_toolbar2.insert(toolitem17, -1)

        store2 = Gtk.ListStore(str, str)
        thumb_size_list = self.app_obj.thumb_size_list.copy()
        while thumb_size_list:
            store2.append( [ thumb_size_list.pop(0), thumb_size_list.pop(0)] )

        self.catalogue_thumb_combo = Gtk.ComboBox.new_with_model(store2)
        toolitem17.add(self.catalogue_thumb_combo)
        renderer_text = Gtk.CellRendererText()
        self.catalogue_thumb_combo.pack_start(renderer_text, True)
        self.catalogue_thumb_combo.add_attribute(renderer_text, 'text', 0)
        self.catalogue_thumb_combo.set_entry_text_column(0)
        self.catalogue_thumb_combo.set_sensitive(False)
        self.catalogue_thumb_combo.connect(
            'changed',
            self.on_video_catalogue_thumb_combo_changed,
        )

        # Third toolbar, which is likewise not added to the VBox until the call
        #   to self.update_catalogue_filter_widgets()
        self.catalogue_toolbar3 = Gtk.Toolbar()
        self.catalogue_toolbar3.set_visible(False)

        toolitem10 = Gtk.ToolItem.new()
        self.catalogue_toolbar3.insert(toolitem10, -1)
        toolitem10.add(Gtk.Label(_('Filter') + '  '))

        toolitem11 = Gtk.ToolItem.new()
        self.catalogue_toolbar3.insert(toolitem11, -1)
        self.catalogue_filter_entry = Gtk.Entry()
        toolitem11.add(self.catalogue_filter_entry)
        self.catalogue_filter_entry.set_width_chars(16)
        self.catalogue_filter_entry.set_sensitive(False)
        self.catalogue_filter_entry.set_tooltip_text(_('Enter search text'))

        toolitem12 = Gtk.ToolItem.new()
        self.catalogue_toolbar3.insert(toolitem12, -1)
        self.catalogue_regex_togglebutton \
        = Gtk.ToggleButton(_('Regex'))
        toolitem12.add(self.catalogue_regex_togglebutton)
        self.catalogue_regex_togglebutton.set_sensitive(False)
        if not self.app_obj.catalogue_use_regex_flag:
            self.catalogue_regex_togglebutton.set_active(False)
        else:
            self.catalogue_regex_togglebutton.set_active(True)
        self.catalogue_regex_togglebutton.set_tooltip_text(
            _('Select if search text is a regex'),
        )
        self.catalogue_regex_togglebutton.set_action_name(
            'app.use_regex_togglebutton',
        )

        # Separator
        toolitem = Gtk.ToolItem.new()
        self.catalogue_toolbar3.insert(toolitem, -1)
        toolitem.add(Gtk.Label('   '))

        toolitem13 = Gtk.ToolItem.new()
        self.catalogue_toolbar3.insert(toolitem13, -1)

        self.catalogue_filter_name_button = Gtk.CheckButton()
        toolitem13.add(self.catalogue_filter_name_button)
        self.catalogue_filter_name_button.set_label(_('Names'))
        self.catalogue_filter_name_button.set_active(
            self.app_obj.catalogue_filter_name_flag,
        )
        self.catalogue_filter_name_button.connect(
            'toggled',
            self.on_filter_name_checkbutton_changed,
        )

        toolitem14 = Gtk.ToolItem.new()
        self.catalogue_toolbar3.insert(toolitem14, -1)

        self.catalogue_filter_descrip_button = Gtk.CheckButton()
        toolitem14.add(self.catalogue_filter_descrip_button)
        self.catalogue_filter_descrip_button.set_label(_('Descriptions'))
        self.catalogue_filter_descrip_button.set_active(
            self.app_obj.catalogue_filter_descrip_flag,
        )
        self.catalogue_filter_descrip_button.connect(
            'toggled',
            self.on_filter_descrip_checkbutton_changed,
        )

        toolitem15 = Gtk.ToolItem.new()
        self.catalogue_toolbar3.insert(toolitem15, -1)

        self.catalogue_filter_comment_button = Gtk.CheckButton()
        toolitem15.add(self.catalogue_filter_comment_button)
        self.catalogue_filter_comment_button.set_label(_('Comments'))
        self.catalogue_filter_comment_button.set_active(
            self.app_obj.catalogue_filter_comment_flag,
        )
        self.catalogue_filter_comment_button.connect(
            'toggled',
            self.on_filter_comment_checkbutton_changed,
        )

        if not self.app_obj.show_custom_icons_flag:
            self.catalogue_apply_filter_button \
            = Gtk.ToolButton.new_from_stock(Gtk.STOCK_FIND)
        else:
            self.catalogue_apply_filter_button = Gtk.ToolButton.new()
            self.catalogue_apply_filter_button.set_icon_widget(
                Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_find']),
            )
        self.catalogue_toolbar3.insert(self.catalogue_apply_filter_button, -1)
        self.catalogue_apply_filter_button.set_sensitive(False)
        self.catalogue_apply_filter_button.set_tooltip_text(
            _('Filter videos'),
        )
        self.catalogue_apply_filter_button.set_action_name(
            'app.apply_filter_toolbutton',
        )

        if not self.app_obj.show_custom_icons_flag:
            self.catalogue_cancel_filter_button \
            = Gtk.ToolButton.new_from_stock(Gtk.STOCK_CANCEL)
        else:
            self.catalogue_cancel_filter_button = Gtk.ToolButton.new()
            self.catalogue_cancel_filter_button.set_icon_widget(
                Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_cancel']),
            )
        self.catalogue_toolbar3.insert(self.catalogue_cancel_filter_button, -1)
        self.catalogue_cancel_filter_button.set_sensitive(False)
        self.catalogue_cancel_filter_button.set_tooltip_text(
            _('Cancel filter'),
        )
        self.catalogue_cancel_filter_button.set_action_name(
            'app.cancel_filter_toolbutton',
        )

        # Fourth toolbar, which is likewise not added to the VBox until the
        #   call to self.update_catalogue_filter_widgets()
        self.catalogue_toolbar4 = Gtk.Toolbar()
        self.catalogue_toolbar4.set_visible(False)

        toolitem18 = Gtk.ToolItem.new()
        self.catalogue_toolbar4.insert(toolitem18, -1)

        self.catalogue_frame_button = Gtk.CheckButton()
        toolitem18.add(self.catalogue_frame_button)
        self.catalogue_frame_button.set_label(_('Draw frames'))
        self.catalogue_frame_button.set_active(
            self.app_obj.catalogue_draw_frame_flag,
        )
        self.catalogue_frame_button.connect(
            'toggled',
            self.on_draw_frame_checkbutton_changed,
        )

        toolitem19 = Gtk.ToolItem.new()
        self.catalogue_toolbar4.insert(toolitem19, -1)

        self.catalogue_icons_button = Gtk.CheckButton()
        toolitem19.add(self.catalogue_icons_button)
        self.catalogue_icons_button.set_label(_('Draw icons'))
        self.catalogue_icons_button.set_active(
            self.app_obj.catalogue_draw_icons_flag,
        )
        self.catalogue_icons_button.connect(
            'toggled',
            self.on_draw_icons_checkbutton_changed,
        )

        toolitem20 = Gtk.ToolItem.new()
        self.catalogue_toolbar4.insert(toolitem20, -1)

        self.catalogue_downloaded_button = Gtk.CheckButton()
        toolitem20.add(self.catalogue_downloaded_button)
        self.catalogue_downloaded_button.set_label(_('Show downloaded'))
        self.catalogue_downloaded_button.set_active(
            self.app_obj.catalogue_draw_downloaded_flag,
        )
        self.catalogue_downloaded_button.connect(
            'toggled',
            self.on_draw_downloaded_checkbutton_changed,
        )

        toolitem21 = Gtk.ToolItem.new()
        self.catalogue_toolbar4.insert(toolitem21, -1)

        self.catalogue_undownloaded_button = Gtk.CheckButton()
        toolitem21.add(self.catalogue_undownloaded_button)
        self.catalogue_undownloaded_button.set_label(
            _('Show undownloaded'),
        )
        self.catalogue_undownloaded_button.set_active(
            self.app_obj.catalogue_draw_undownloaded_flag,
        )
        self.catalogue_undownloaded_button.connect(
            'toggled',
            self.on_draw_undownloaded_checkbutton_changed,
        )

        toolitem22 = Gtk.ToolItem.new()
        self.catalogue_toolbar4.insert(toolitem22, -1)

        self.catalogue_blocked_button = Gtk.CheckButton()
        toolitem22.add(self.catalogue_blocked_button)
        self.catalogue_blocked_button.set_label(_('Show blocked'))
        self.catalogue_blocked_button.set_active(
            self.app_obj.catalogue_draw_blocked_flag,
        )
        self.catalogue_blocked_button.connect(
            'toggled',
            self.on_draw_blocked_checkbutton_changed,
        )

        # Set up the Video catalogue
        self.video_catalogue_reset()


    def setup_progress_tab(self):

        """Called by self.setup_win().

        Creates widgets for the Progress tab.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 2946 setup_progress_tab')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Main window\'s Progress tab'
        )

        vbox = Gtk.VBox()
        self.progress_tab.pack_start(vbox, True, True, 0)

        self.progress_paned = Gtk.VPaned()
        vbox.pack_start(self.progress_paned, True, True, 0)
        self.progress_paned.set_position(
            self.app_obj.main_win_progress_slider_posn,
        )
        self.progress_paned.set_wide_handle(True)

        # Upper half
        frame = Gtk.Frame()
        self.progress_paned.pack1(frame, True, False)

        self.progress_list_scrolled = Gtk.ScrolledWindow()
        frame.add(self.progress_list_scrolled)
        self.progress_list_scrolled.set_policy(
            Gtk.PolicyType.AUTOMATIC,
            Gtk.PolicyType.AUTOMATIC,
        )

        # Progress List
        self.progress_list_treeview = Gtk.TreeView()
        self.progress_list_scrolled.add(self.progress_list_treeview)
        self.progress_list_treeview.set_can_focus(False)
        # (Tooltips are initially enabled, and if necessary are disabled by a
        #   call to self.disable_tooltips() shortly afterwards)
        self.progress_list_treeview.set_tooltip_column(
            self.progress_list_tooltip_column,
        )
        # (Detect right-clicks on the treeview)
        self.progress_list_treeview.connect(
            'button-press-event',
            self.on_progress_list_right_click,
        )

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Ext is short for a file extension, e.g. .EXE',
        )

        for i, column_title in enumerate(
            [
                'hide', 'hide', 'hide', '', _('Source'), '#', _('Status'),
                _('Incoming file'), _('Ext'), '%', _('Speed'), _('ETA'),
                _('Size'),
            ]
        ):
            if not column_title:
                renderer_pixbuf = Gtk.CellRendererPixbuf()
                column_pixbuf = Gtk.TreeViewColumn(
                    '',
                    renderer_pixbuf,
                    pixbuf=i,
                )
                self.progress_list_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,
                )
                self.progress_list_treeview.append_column(column_text)
                column_text.set_resizable(True)
                column_text.set_min_width(self.min_column_width)
                if column_title == 'hide':
                    column_text.set_visible(False)

        self.progress_list_liststore = Gtk.ListStore(
            int, int, str,
            GdkPixbuf.Pixbuf,
            str, str, str, str, str, str, str, str, str,
        )
        self.progress_list_treeview.set_model(self.progress_list_liststore)

        # Set the size of the 'Source' and 'Incoming file' columns. The
        #   others always contain few characters, so let them expand as they
        #   please
        source_column = self.progress_list_treeview.get_column(4)
        if self.app_obj.progress_list_width_source is not None \
        and self.app_obj.progress_list_width_source >= self.min_column_width:
            source_column.set_fixed_width(
                self.app_obj.progress_list_width_source,
            )
        else:
            source_column.set_fixed_width(200)

        incoming_column = self.progress_list_treeview.get_column(7)
        if self.app_obj.progress_list_width_incoming is not None \
        and self.app_obj.progress_list_width_incoming >= self.min_column_width:
            incoming_column.set_fixed_width(
                self.app_obj.progress_list_width_incoming,
            )
        else:
            incoming_column.set_fixed_width(200)

        # Lower half
        frame2 = Gtk.Frame()
        self.progress_paned.pack2(frame2, True, False)

        self.results_list_scrolled = Gtk.ScrolledWindow()
        frame2.add(self.results_list_scrolled)
        self.results_list_scrolled.set_policy(
            Gtk.PolicyType.AUTOMATIC,
            Gtk.PolicyType.AUTOMATIC,
        )

        # Results List. Use a modified Gtk.TreeView which permits drag-and-drop
        #   for multiple rows
        self.results_list_treeview = MultiDragDropTreeView()
        self.results_list_scrolled.add(self.results_list_treeview)
        # (Tooltips are initially enabled, and if necessary are disabled by a
        #   call to self.disable_tooltips() shortly afterwards)
        self.results_list_treeview.set_tooltip_column(
            self.results_list_tooltip_column,
        )
        # (Detect right-clicks on the treeview)
        self.results_list_treeview.connect(
            'button-press-event',
            self.on_results_list_right_click,
        )

        # Allow multiple selection...
        self.results_list_treeview.set_can_focus(True)
        selection = self.results_list_treeview.get_selection()
        selection.set_mode(Gtk.SelectionMode.MULTIPLE)
        # ...and then set up drag and drop from the treeview to an external
        #   application (for example, an FFmpeg batch converter)
        self.results_list_treeview.enable_model_drag_source(
            Gdk.ModifierType.BUTTON1_MASK,
            [],
            Gdk.DragAction.COPY,
        )
        self.results_list_treeview.drag_source_add_text_targets()
        self.results_list_treeview.connect(
            'drag-data-get',
            self.on_results_list_drag_data_get,
        )

        # Set up the treeview's model
        for i, column_title in enumerate(
            [
                'hide', 'hide', '', _('New videos'), _('Duration'), _('Size'),
                _('Date'), _('File'), '', _('Downloaded to'),
            ]
        ):
            if not column_title:
                renderer_pixbuf = Gtk.CellRendererPixbuf()
                column_pixbuf = Gtk.TreeViewColumn(
                    column_title,
                    renderer_pixbuf,
                    pixbuf=i,
                )
                self.results_list_treeview.append_column(column_pixbuf)
                column_pixbuf.set_resizable(False)

            elif i == 7:
                renderer_toggle = Gtk.CellRendererToggle()
                column_toggle = Gtk.TreeViewColumn(
                    column_title,
                    renderer_toggle,
                    active=i,
                )
                self.results_list_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,
                )
                self.results_list_treeview.append_column(column_text)
                column_text.set_resizable(True)
                column_text.set_min_width(self.min_column_width)
                if column_title == 'hide':
                    column_text.set_visible(False)

        self.results_list_liststore = Gtk.ListStore(
            int, str,
            GdkPixbuf.Pixbuf,
            str, str, str, str,
            bool,
            GdkPixbuf.Pixbuf,
            str,
        )
        self.results_list_treeview.set_model(self.results_list_liststore)

        # Set the size of the 'New videos' column. The others always contain
        #   few characters, so let them expand as they please
        videos_column = self.results_list_treeview.get_column(3)
        if self.app_obj.results_list_width_video is not None \
        and self.app_obj.results_list_width_video >= self.min_column_width:
            videos_column.set_fixed_width(
                self.app_obj.results_list_width_video,
            )

        else:
            videos_column.set_fixed_width(300)

        # Strip of widgets at the bottom, arranged in a grid
        grid = Gtk.Grid()
        vbox.pack_start(grid, False, False, 0)
        grid.set_vexpand(False)
        grid.set_border_width(self.spacing_size)
        grid.set_column_spacing(self.spacing_size)
        grid.set_row_spacing(self.spacing_size)

        self.num_worker_checkbutton = Gtk.CheckButton()
        grid.attach(self.num_worker_checkbutton, 0, 0, 1, 1)
        self.num_worker_checkbutton.set_label(_('Max downloads'))
        self.num_worker_checkbutton.set_active(
            self.app_obj.num_worker_apply_flag,
        )
        self.num_worker_checkbutton.connect(
            'toggled',
            self.on_num_worker_checkbutton_changed,
        )

        self.num_worker_spinbutton = Gtk.SpinButton.new_with_range(
            self.app_obj.num_worker_min,
            self.app_obj.num_worker_max,
            1,
        )
        grid.attach(self.num_worker_spinbutton, 1, 0, 1, 1)
        self.num_worker_spinbutton.set_value(self.app_obj.num_worker_default)
        self.num_worker_spinbutton.connect(
            'value-changed',
            self.on_num_worker_spinbutton_changed,
        )

        self.bandwidth_checkbutton = Gtk.CheckButton()
        grid.attach(self.bandwidth_checkbutton, 2, 0, 1, 1)
        self.bandwidth_checkbutton.set_label(_('D/L speed (KiB/s)'))
        self.bandwidth_checkbutton.set_active(
            self.app_obj.bandwidth_apply_flag,
        )
        self.bandwidth_checkbutton.connect(
            'toggled',
            self.on_bandwidth_checkbutton_changed,
        )

        self.bandwidth_spinbutton = Gtk.SpinButton.new_with_range(
            self.app_obj.bandwidth_min,
            self.app_obj.bandwidth_max,
            1,
        )
        grid.attach(self.bandwidth_spinbutton, 3, 0, 1, 1)
        self.bandwidth_spinbutton.set_value(self.app_obj.bandwidth_default)
        self.bandwidth_spinbutton.connect(
            'value-changed',
            self.on_bandwidth_spinbutton_changed,
        )

        # (To stop the remaining widgets on this line from constantly resizing
        #   themselves during a download operation, place them inside an inner
        #   grid)
        grid2 = Gtk.Grid()
        grid.attach(grid2, 4, 0, 1, 1)

        self.alt_limits_frame = Gtk.Frame()
        grid2.attach(self.alt_limits_frame, 0, 0, 1, 1)
        self.alt_limits_frame.set_tooltip_text(
            _('Alternative limits do not currently apply'),
        )

        self.alt_limits_image = Gtk.Image()
        self.alt_limits_frame.add(self.alt_limits_image)
        self.alt_limits_image.set_from_pixbuf(
            self.pixbuf_dict['limits_off_large'],
        )

        self.video_res_checkbutton = Gtk.CheckButton()
        grid2.attach(self.video_res_checkbutton, 1, 0, 1, 1)
        self.video_res_checkbutton.set_label(_('Video resolution'))
        self.video_res_checkbutton.set_active(
            self.app_obj.video_res_apply_flag,
        )
        self.video_res_checkbutton.connect(
            'toggled',
            self.on_video_res_checkbutton_changed,
        )

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

        self.video_res_combobox = Gtk.ComboBox.new_with_model(store)
        grid2.attach(self.video_res_combobox, 2, 0, 1, 1)
        renderer_text = Gtk.CellRendererText()
        self.video_res_combobox.pack_start(renderer_text, True)
        self.video_res_combobox.add_attribute(renderer_text, 'text', 0)
        self.video_res_combobox.set_entry_text_column(0)
        # (Check we're using a recognised value)
        resolution = self.app_obj.video_res_default
        if not resolution in formats.VIDEO_RESOLUTION_LIST:
            resolution = formats.VIDEO_RESOLUTION_DEFAULT
        # (Set the active item)
        self.video_res_combobox.set_active(
            formats.VIDEO_RESOLUTION_LIST.index(resolution),
        )
        self.video_res_combobox.connect(
            'changed',
            self.on_video_res_combobox_changed,
        )

        self.hide_finished_checkbutton = Gtk.CheckButton()
        grid.attach(self.hide_finished_checkbutton, 0, 1, 2, 1)
        self.hide_finished_checkbutton.set_label(
            _('Hide rows when they are finished'),
        )
        self.hide_finished_checkbutton.set_active(
            self.app_obj.progress_list_hide_flag,
        )
        self.hide_finished_checkbutton.connect(
            'toggled',
            self.on_hide_finished_checkbutton_changed,
        )

        self.reverse_results_checkbutton = Gtk.CheckButton()
        grid.attach(self.reverse_results_checkbutton, 2, 1, 4, 1)
        self.reverse_results_checkbutton.set_label(
            _('Add newest videos to the top'))
        self.reverse_results_checkbutton.set_active(
            self.app_obj.results_list_reverse_flag,
        )
        self.reverse_results_checkbutton.connect(
            'toggled',
            self.on_reverse_results_checkbutton_changed,
        )

        self.progress_update_label = Gtk.Label()
        grid.attach(self.progress_update_label, 4, 1, 1, 1)
        self.progress_update_label.set_alignment(0, 0.5)
        self.progress_update_label.set_hexpand(False)


    def setup_classic_mode_tab(self):

        """Called by self.setup_win().

        Creates widgets for the Classic Mode tab.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 3300 setup_classic_mode_tab')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Main window\'s Classic Mode tab'
        )

        self.classic_paned = Gtk.VPaned()
        self.classic_tab.pack_start(self.classic_paned, True, True, 0)
        self.classic_paned.set_position(
            self.app_obj.main_win_classic_slider_posn,
        )
        self.classic_paned.set_wide_handle(True)

        # Upper half
        # ----------
        grid = Gtk.Grid()
        self.classic_paned.pack1(grid, True, False)
        grid.set_column_spacing(self.spacing_size)
        grid.set_row_spacing(self.spacing_size * 2)

        grid_width = 7

        # First row - some decoration, and a button to open a popup menu
        # --------------------------------------------------------------------

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

        # (The youtube-dl-gui icon looks neat, but also solves spacing issues
        #   on this grid row)
        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)

        self.classic_banner_img = Gtk.Image()
        hbox2.pack_start(self.classic_banner_img, False, False, 0)

        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)

        self.classic_banner_label = Gtk.Label()
        vbox.pack_start(self.classic_banner_label, True, True, 0)

        self.classic_banner_label2 = Gtk.Label()
        vbox.pack_start(self.classic_banner_label2, True, True, 0)

        self.update_classic_mode_tab_update_banner()

        if not self.app_obj.show_custom_icons_flag:
            self.classic_menu_button = Gtk.Button.new_from_icon_name(
                Gtk.STOCK_INDEX,
                Gtk.IconSize.BUTTON,
            )
        else:
            self.classic_menu_button = Gtk.Button.new()
            self.classic_menu_button.set_image(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['stock_properties_large'],
                ),
            )
        hbox.pack_start(self.classic_menu_button, False, False, 0)
        self.classic_menu_button.set_action_name(
            'app.classic_menu_button',
        )
        self.classic_menu_button.set_tooltip_text(
            _('Open the Classic Mode menu'),
        )

        # Second row - a textview for entering URLs. If automatic copy/paste is
        #   enabled, URLs are automatically copied into this textview
        # --------------------------------------------------------------------

        label3 = Gtk.Label(
            _(
            'Enter URLs below, choose your settings, click the' \
            + ' \'Add URLs\' button, then click \'Download all\''),
        )
        grid.attach(label3, 0, 1, grid_width, 1)
        label3.set_alignment(0, 0.5)

        frame3 = Gtk.Frame()
        grid.attach(frame3, 0, 2, grid_width, 1)

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

        self.classic_textview = Gtk.TextView()
        scrolled.add(self.classic_textview)
        self.classic_textbuffer = self.classic_textview.get_buffer()

        # (Some callbacks will complain about invalid iterators, if we try to
        #   use Gtk.TextIters, so use Gtk.TextMarks instead)
        self.classic_mark_start = self.classic_textbuffer.create_mark(
            'mark_start',
            self.classic_textbuffer.get_start_iter(),
            True,               # Left gravity
        )
        self.classic_mark_end = self.classic_textbuffer.create_mark(
            'mark_end',
            self.classic_textbuffer.get_end_iter(),
            False,              # Not left gravity
        )

        # (When the user copy-pastes URLs into the textview, insert an
        #   initial newline character, so they don't have to continuously
        #   do that themselves)
        self.classic_textview.connect(
            'paste-clipboard',
            self.on_classic_textview_paste,
        )
        # (If the setting is enabled, start a download operation for any valid
        #   URL(s), or add the URL(s) to an existing download operation)
        self.classic_textbuffer.connect(
            'changed',
            self.on_classic_textbuffer_changed,
        )

        # Third row - widgets to set the download destination and video/audio
        #   format. The user clicks the 'Add URLs' button to create dummy
        #   media.Video objects for each URL. Each object is associated with
        #   the specified destination and format
        # --------------------------------------------------------------------

        # Destination directory
        label4 = Gtk.Label(_('Destination:'))
        grid.attach(label4, 0, 3, 1, 1)
        label4.set_xalign(0)
        label4.set_hexpand(False)

        self.classic_dest_dir_liststore = Gtk.ListStore(str)
        for string in self.app_obj.classic_dir_list:
            self.classic_dest_dir_liststore.append( [string] )

        self.classic_dest_dir_combo = Gtk.ComboBox.new_with_model(
            self.classic_dest_dir_liststore,
        )
        grid.attach(self.classic_dest_dir_combo, 1, 3, 4, 1)
        renderer_text = Gtk.CellRendererText()
        self.classic_dest_dir_combo.pack_start(renderer_text, True)
        self.classic_dest_dir_combo.add_attribute(renderer_text, 'text', 0)
        self.classic_dest_dir_combo.set_entry_text_column(0)
        self.classic_dest_dir_combo.set_active(0)
        self.classic_dest_dir_combo.set_hexpand(True)
        self.classic_dest_dir_combo.connect(
            'changed',
            self.on_classic_dest_dir_combo_changed,
        )

        if not self.app_obj.show_custom_icons_flag:
            self.classic_dest_dir_button = Gtk.Button.new_from_icon_name(
                Gtk.STOCK_ADD,
                Gtk.IconSize.BUTTON,
            )
        else:
            self.classic_dest_dir_button = Gtk.Button.new()
            self.classic_dest_dir_button.set_image(
                Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_add']),
            )
        grid.attach(self.classic_dest_dir_button, 5, 3, 1, 1)
        self.classic_dest_dir_button.set_action_name(
            'app.classic_dest_dir_button',
        )
        self.classic_dest_dir_button.set_tooltip_text(
            _('Add a new destination folder'),
        )
        self.classic_dest_dir_button.set_hexpand(False)

        if not self.app_obj.show_custom_icons_flag:
            self.classic_dest_dir_open_button = Gtk.Button.new_from_icon_name(
                Gtk.STOCK_OPEN,
                Gtk.IconSize.BUTTON,
            )
        else:
            self.classic_dest_dir_open_button = Gtk.Button.new()
            self.classic_dest_dir_open_button.set_image(
                Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_open']),
            )
        grid.attach(self.classic_dest_dir_open_button, 6, 3, 1, 1)
        self.classic_dest_dir_open_button.set_action_name(
            'app.classic_dest_dir_open_button',
        )
        self.classic_dest_dir_open_button.set_tooltip_text(
            _('Open the destination folder'),
        )
        self.classic_dest_dir_open_button.set_hexpand(False)

        # Fourth row
        # --------------------------------------------------------------------

        # Video/audio format
        label5 = Gtk.Label(_('Format:'))
        grid.attach(label5, 0, 4, 1, 1)
        label5.set_xalign(0)
        label5.set_hexpand(False)

        combo_list = [_('Default') + '     ', _('Video:')]
        for item in formats.VIDEO_FORMAT_LIST:
            combo_list.append('  ' + item)

        combo_list.append(_('Audio:'))
        for item in formats.AUDIO_FORMAT_LIST:
            combo_list.append('  ' + item)

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

        self.classic_format_combo = Gtk.ComboBox.new_with_model(
            self.classic_format_liststore,
        )
        grid.attach(self.classic_format_combo, 1, 4, 1, 1)
        renderer_text = Gtk.CellRendererText()
        self.classic_format_combo.pack_start(renderer_text, True)
        self.classic_format_combo.add_attribute(renderer_text, 'text', 0)
        self.classic_format_combo.set_entry_text_column(0)
        self.classic_format_combo.set_hexpand(False)
        # (Signal connect appears below)

        # (The None value represents the first line in the combo, 'Default')
        if self.app_obj.classic_format_selection is None:
            self.classic_format_combo.set_active(0)
        else:
            for i in range(len(combo_list)):
                if combo_list[i] == '  ' \
                + self.app_obj.classic_format_selection:
                    self.classic_format_combo.set_active(i)
                    break

        # Video resolution
        combo_list2 = [_('Highest')]
        for item in formats.VIDEO_RESOLUTION_LIST:
            combo_list2.append('  ' + item)

        self.classic_resolution_liststore = Gtk.ListStore(str)
        for string in combo_list2:
            self.classic_resolution_liststore.append( [string] )

        self.classic_resolution_combo = Gtk.ComboBox.new_with_model(
            self.classic_resolution_liststore,
        )
        grid.attach(self.classic_resolution_combo, 2, 4, 1, 1)
        renderer_text = Gtk.CellRendererText()
        self.classic_resolution_combo.pack_start(renderer_text, True)
        self.classic_resolution_combo.add_attribute(renderer_text, 'text', 0)
        self.classic_resolution_combo.set_entry_text_column(0)
        self.classic_resolution_combo.set_hexpand(False)
        # (Signal connect appears below)

        # (The None value represents the first line in the combo, 'Resolution')
        if self.app_obj.classic_resolution_selection is None:
            self.classic_resolution_combo.set_active(0)
        else:
            for i in range(len(combo_list2)):
                if combo_list2[i] == '  ' \
                + self.app_obj.classic_resolution_selection:
                    self.classic_resolution_combo.set_active(i)
                    break

        # Clarifiers
        combo_list3 = [
            _('Convert to this format'),
            _('Download in this format'),
        ]

        self.classic_convert_liststore = Gtk.ListStore(str)
        for string in combo_list3:
            self.classic_convert_liststore.append( [string] )

        self.classic_convert_combo = Gtk.ComboBox.new_with_model(
            self.classic_convert_liststore,
        )
        grid.attach(self.classic_convert_combo, 3, 4, 1, 1)
        renderer_text = Gtk.CellRendererText()
        self.classic_convert_combo.pack_start(renderer_text, True)
        self.classic_convert_combo.add_attribute(renderer_text, 'text', 0)
        self.classic_convert_combo.set_entry_text_column(0)
        # (Signal connect appears below)
        if self.app_obj.classic_format_convert_flag:
            self.classic_convert_combo.set_active(0)
        else:
            self.classic_convert_combo.set_active(1)
        self.classic_convert_combo.set_hexpand(False)
        if not self.app_obj.classic_format_selection:
            self.classic_convert_combo.set_sensitive(False)

        # (Invisible label for spacing)
        label6 = Gtk.Label('')
        grid.attach(label6, 4, 4, 1, 1)
        label6.set_hexpand(True)

        # (Signal connects from above)
        # If the user selects the 'Default' item, desensitise the radiobuttons
        # If the user selects the 'Video:' or 'Audio:' items, automatically
        #   select the first item below that
        self.classic_format_combo.connect(
            'changed',
            self.on_classic_format_combo_changed,
        )
        self.classic_resolution_combo.connect(
            'changed',
            self.on_classic_resolution_combo_changed,
        )
        self.classic_convert_combo.connect(
            'changed',
            self.on_classic_convert_combo_changed,
        )

        # Add clips button
        self.classic_add_clips_button = Gtk.Button(
            '     ' + _('Add video clips') + '     ',
        )
        grid.attach(self.classic_add_clips_button, 5, 4, 2, 1)
        self.classic_add_clips_button.set_action_name(
            'app.classic_add_clips_button',
        )
        self.classic_add_clips_button.set_tooltip_text(_('Add video clips'))
        self.classic_add_clips_button.set_hexpand(False)

        # Fifth row
        # --------------------------------------------------------------------

        # Other settings
        label7 = Gtk.Label(_('Other:'))
        grid.attach(label7, 0, 5, 1, 1)
        label7.set_xalign(0)
        label7.set_hexpand(False)

        self.classic_livestream_checkbutton = Gtk.CheckButton()
        grid.attach(self.classic_livestream_checkbutton, 1, 5, 1, 1)
        self.classic_livestream_checkbutton.set_label(_('Is a livestream'))
        if self.app_obj.classic_livestream_flag:
            self.classic_livestream_checkbutton.set_active(True)
        self.classic_livestream_checkbutton.connect(
            'toggled',
            self.on_classic_livestream_checkbutton_toggled,
        )

        self.classic_sblock_checkbutton = Gtk.CheckButton()
        grid.attach(self.classic_sblock_checkbutton, 2, 5, 2, 1)
        self.classic_sblock_checkbutton.set_label(
            _('Use SponsorBlock (requires FFmpeg v5.1+)'),
        )
        if self.app_obj.classic_sblock_flag:
            self.classic_sblock_checkbutton.set_active(True)
        self.classic_sblock_checkbutton.connect(
            'toggled',
            self.on_classic_sblock_checkbutton_toggled,
        )

        # Add URLs button
        self.classic_add_urls_button = Gtk.Button(
            '     ' + _('Add URLs') + '     ',
        )
        grid.attach(self.classic_add_urls_button, 5, 5, 2, 1)
        self.classic_add_urls_button.set_action_name(
            'app.classic_add_urls_button',
        )
        self.classic_add_urls_button.set_tooltip_text(_('Add these URLs'))
        self.classic_add_urls_button.set_hexpand(False)

        # Bottom half
        # -----------
        grid2 = Gtk.Grid()
        self.classic_paned.pack2(grid2, True, False)
        grid2.set_column_spacing(self.spacing_size)
        grid2.set_row_spacing(self.spacing_size * 2)

        # Sixth row - the Classic Progress List. A treeview to display the
        #   progress of downloads (in Classic Mode, ongoing download
        #   information is displayed here, rather than in the Progress tab)
        # --------------------------------------------------------------------

        frame4 = Gtk.Frame()
        grid2.attach(frame4, 0, 1, 1, 1)
        frame4.set_hexpand(True)
        frame4.set_vexpand(True)

        scrolled2 = Gtk.ScrolledWindow()
        frame4.add(scrolled2)
        scrolled2.set_policy(
            Gtk.PolicyType.AUTOMATIC,
            Gtk.PolicyType.AUTOMATIC,
        )

        self.classic_progress_treeview = MultiDragDropTreeView()
        scrolled2.add(self.classic_progress_treeview)
        # (Tooltips are initially enabled, and if necessary are disabled by a
        #   call to self.disable_tooltips() shortly afterwards)
        self.classic_progress_treeview.set_tooltip_column(
            self.classic_progress_tooltip_column,
        )
        # (Detect right-clicks on the treeview)
        self.classic_progress_treeview.connect(
            'button-press-event',
            self.on_classic_progress_list_right_click,
        )

        # (Enable selection of multiple lines)
        self.classic_progress_treeview.set_can_focus(True)
        selection = self.classic_progress_treeview.get_selection()
        selection.set_mode(Gtk.SelectionMode.MULTIPLE)
        # ...and then set up drag and drop from the treeview to an external
        #   application (for example, an FFmpeg batch converter)
        self.classic_progress_treeview.enable_model_drag_source(
            Gdk.ModifierType.BUTTON1_MASK,
            [],
            Gdk.DragAction.COPY,
        )
        self.classic_progress_treeview.drag_source_add_text_targets()
        self.classic_progress_treeview.connect(
            'drag-data-get',
            self.on_classic_progress_list_drag_data_get,
        )

        for i, column_title in enumerate(
            [
                'hide', 'hide', _('Source'), '#', _('Status'),
                _('Incoming file'), _('Ext'), '%', _('Speed'), _('ETA'),
                _('Size'),
            ]
        ):
            renderer_text = Gtk.CellRendererText()
            column_text = Gtk.TreeViewColumn(
                column_title,
                renderer_text,
                text=i,
            )
            self.classic_progress_treeview.append_column(column_text)
            column_text.set_resizable(True)
            column_text.set_min_width(self.min_column_width)
            if column_title == 'hide':
                column_text.set_visible(False)

        self.classic_progress_liststore = Gtk.ListStore(
            int, str, str, str, str, str, str, str, str, str, str,
        )
        self.classic_progress_treeview.set_model(
            self.classic_progress_liststore,
        )

        # Set the size of the 'Source' and 'Incoming file' columns. The
        #   others always contain few characters, so let them expand as they
        #   please
        source_column = self.classic_progress_treeview.get_column(2)
        if self.app_obj.classic_progress_list_width_source is not None \
        and self.app_obj.classic_progress_list_width_source \
        >= self.min_column_width:
            source_column.set_fixed_width(
                self.app_obj.classic_progress_list_width_source,
            )

        else:
            source_column.set_fixed_width(200)

        incoming_column = self.classic_progress_treeview.get_column(5)
        if self.app_obj.classic_progress_list_width_incoming is not None \
        and self.app_obj.classic_progress_list_width_incoming \
        >= self.min_column_width:
            incoming_column.set_fixed_width(
                self.app_obj.classic_progress_list_width_incoming,
            )

        else:
            incoming_column.set_fixed_width(200)

        # Seventh row - a strip of buttons that apply to rows in the Classic
        #   Progress List. We use another new hbox to avoid messing up the
        #   grid layout
        # --------------------------------------------------------------------

        hbox3 = Gtk.HBox()
        grid2.attach(hbox3, 0, 2, 1, 1)

        if not self.app_obj.show_custom_icons_flag:
            self.classic_play_button = Gtk.Button.new_from_icon_name(
                Gtk.STOCK_MEDIA_PLAY,
                Gtk.IconSize.BUTTON,
            )
        else:
            self.classic_play_button = Gtk.Button.new()
            self.classic_play_button.set_image(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['stock_media_play'],
                ),
            )
        hbox3.pack_start(self.classic_play_button, False, False, 0)
        self.classic_play_button.set_action_name(
            'app.classic_play_button',
        )
        self.classic_play_button.set_tooltip_text(_('Play video'))

        if not self.app_obj.show_custom_icons_flag:
            self.classic_open_button = Gtk.Button.new_from_icon_name(
                Gtk.STOCK_OPEN,
                Gtk.IconSize.BUTTON,
            )
        else:
            self.classic_open_button = Gtk.Button.new()
            self.classic_open_button.set_image(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['stock_open'],
                ),
            )
        hbox3.pack_start(
            self.classic_open_button,
            False,
            False,
            self.spacing_size,
        )
        self.classic_open_button.set_action_name(
            'app.classic_open_button',
        )
        self.classic_open_button.set_tooltip_text(_('Open destination(s)'))

        if not self.app_obj.show_custom_icons_flag:
            self.classic_stop_button = Gtk.Button.new_from_icon_name(
                Gtk.STOCK_MEDIA_STOP,
                Gtk.IconSize.BUTTON,
            )
        else:
            self.classic_stop_button = Gtk.Button.new()
            self.classic_stop_button.set_image(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['stock_media_stop'],
                ),
            )
        hbox3.pack_start(self.classic_stop_button, False, False, 0)
        self.classic_stop_button.set_action_name(
            'app.classic_stop_button',
        )
        self.classic_stop_button.set_tooltip_text(_('Stop download'))

        if not self.app_obj.show_custom_icons_flag:
            self.classic_redownload_button = Gtk.Button.new_from_icon_name(
                Gtk.STOCK_REFRESH,
                Gtk.IconSize.BUTTON,
            )
        else:
            self.classic_redownload_button = Gtk.Button.new()
            self.classic_redownload_button.set_image(
                Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_refresh']),
            )
        hbox3.pack_start(
            self.classic_redownload_button,
            False,
            False,
            self.spacing_size,
        )
        self.classic_redownload_button.set_action_name(
            'app.classic_redownload_button',
        )
        self.classic_redownload_button.set_tooltip_text(_('Re-download'))

        self.classic_archive_button = Gtk.ToggleButton.new()
        self.classic_archive_button.set_image(
            Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_file']),
        )
        hbox3.pack_start(self.classic_archive_button, False, False, 0)
        self.classic_archive_button.set_action_name(
            'app.classic_archive_button',
        )
        self.classic_archive_button.set_tooltip_text(
            utils.tidy_up_long_string(
                _(
                'Allow downloader to create an archive file (enable this' \
                + ' only when downloading channels and playlists)',
                ),
                self.long_string_max_len,
            ),
        )
        if self.app_obj.classic_ytdl_archive_flag:
            self.classic_archive_button.set_active(True)

        if not self.app_obj.show_custom_icons_flag:
            self.classic_clips_button = Gtk.Button.new_from_icon_name(
                Gtk.STOCK_CUT,
                Gtk.IconSize.BUTTON,
            )
        else:
            self.classic_clips_button = Gtk.Button.new()
            self.classic_clips_button.set_image(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['stock_cut'],
                ),
            )
        hbox3.pack_start(
            self.classic_clips_button,
            False,
            False,
            self.spacing_size,
        )
        self.classic_clips_button.set_action_name(
            'app.classic_clips_button',
        )
        self.classic_clips_button.set_tooltip_text(_('Create video clips'))

        if not self.app_obj.show_custom_icons_flag:
            self.classic_ffmpeg_button = Gtk.Button.new_from_icon_name(
                Gtk.STOCK_EXECUTE,
                Gtk.IconSize.BUTTON,
            )
        else:
            self.classic_ffmpeg_button = Gtk.Button.new()
            self.classic_ffmpeg_button.set_image(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['stock_execute'],
                ),
            )
        hbox3.pack_start(self.classic_ffmpeg_button, False, False, 0)
        self.classic_ffmpeg_button.set_action_name(
            'app.classic_ffmpeg_button',
        )
        self.classic_ffmpeg_button.set_tooltip_text(_('Process with FFmpeg'))

        if not self.app_obj.show_custom_icons_flag:
            self.classic_move_up_button = Gtk.Button.new_from_icon_name(
                Gtk.STOCK_GO_UP,
                Gtk.IconSize.BUTTON,
            )
        else:
            self.classic_move_up_button = Gtk.Button.new()
            self.classic_move_up_button.set_image(
                Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_go_up']),
            )
        hbox3.pack_start(
            self.classic_move_up_button,
            False,
            False,
            self.spacing_size,
        )
        self.classic_move_up_button.set_action_name(
            'app.classic_move_up_button',
        )
        self.classic_move_up_button.set_tooltip_text(_('Move up'))

        if not self.app_obj.show_custom_icons_flag:
            self.classic_move_down_button = Gtk.Button.new_from_icon_name(
                Gtk.STOCK_GO_DOWN,
                Gtk.IconSize.BUTTON,
            )
        else:
            self.classic_move_down_button = Gtk.Button.new()
            self.classic_move_down_button.set_image(
                Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_go_down']),
            )
        hbox3.pack_start(self.classic_move_down_button, False, False, 0)
        self.classic_move_down_button.set_action_name(
            'app.classic_move_down_button',
        )
        self.classic_move_down_button.set_tooltip_text(_('Move down'))

        if not self.app_obj.show_custom_icons_flag:
            self.classic_remove_button = Gtk.Button.new_from_icon_name(
                Gtk.STOCK_DELETE,
                Gtk.IconSize.BUTTON,
            )
        else:
            self.classic_remove_button = Gtk.Button.new()
            self.classic_remove_button.set_image(
                Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_delete']),
            )
        hbox3.pack_start(
            self.classic_remove_button,
            False,
            False,
            self.spacing_size,
        )
        self.classic_remove_button.set_action_name(
            'app.classic_remove_button',
        )
        self.classic_remove_button.set_tooltip_text(_('Remove from list'))

        if not self.app_obj.classic_custom_dl_flag:
            self.classic_download_button = Gtk.Button(
                '     ' + _('Download all') + '     ',
            )
        else:
            self.classic_download_button = Gtk.Button(
                '     ' + _('Custom download all') + '     ',
            )
        hbox3.pack_end(self.classic_download_button, False, False, 0)
        self.classic_download_button.set_action_name(
            'app.classic_download_button',
        )
        if not self.app_obj.classic_custom_dl_flag:
            self.classic_download_button.set_tooltip_text(
                _('Download the URLs above'),
            )
        else:
            self.classic_download_button.set_tooltip_text(
                _('Perform a custom download on the URLs above'),
            )

        self.classic_clear_button = Gtk.Button(
            '     ' + _('Clear all') + '     ',
        )
        hbox3.pack_end(
            self.classic_clear_button,
            False,
            False,
            self.spacing_size,
        )
        self.classic_clear_button.set_action_name(
            'app.classic_clear_button',
        )
        self.classic_clear_button.set_tooltip_text(
            _('Clear the URLs above'),
        )

        self.classic_clear_dl_button = Gtk.Button(
            '     ' + _('Clear downloaded') + '     ',
        )
        hbox3.pack_end(self.classic_clear_dl_button, False, False, 0)
        self.classic_clear_dl_button.set_action_name(
            'app.classic_clear_dl_button',
        )
        self.classic_clear_dl_button.set_tooltip_text(
            _('Clear the URLs above'),
        )


    def setup_drag_drop_tab(self):

        """Called by self.setup_win().

        Creates widgets for the Drag and Drop tab.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 4040 setup_drag_drop_tab')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Main window\'s Drag and Drop tab'
        )

        grid = Gtk.Grid()
        self.drag_drop_tab.pack_start(grid, True, True, 0)
        grid.set_column_spacing(self.spacing_size)
        grid.set_row_spacing(self.spacing_size)

        # Upper strip
        # -----------

        hbox = Gtk.HBox()
        grid.attach(hbox, 0, 0, 1, 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)

        img = Gtk.Image()
        hbox2.pack_start(img, False, False, 0)
        img.set_from_pixbuf(self.pixbuf_dict['cursor_large'])

        frame2 = Gtk.Frame()
        hbox.pack_start(frame2, True, True, self.spacing_size)
        frame2.set_hexpand(True)

        vbox2 = Gtk.VBox()
        frame2.add(vbox2)
        vbox2.set_border_width(self.spacing_size)

        label = Gtk.Label()
        vbox2.pack_start(label, True, True, 0)
        label.set_markup(
            '<b>' + _(
                'When you drag a video here, it is added to the Classic Mode' \
                 + ' tab',
            ) + '</b>',
        )

        label2 = Gtk.Label()
        vbox2.pack_start(label2, True, True, 0)
        label2.set_markup(
            '<b>' + _(
                'Each zone represents a set of download options',
            ) + '</b>',
        )

        if os.name == 'nt':

            label3 = Gtk.Label()
            vbox2.pack_start(label3, True, True, 0)
            label3.set_markup(
                '<b><i>' + _(
                    'Warning: Drag and drop does not work well on MS Windows',
                ) + '</i></b>',
            )

        if not self.app_obj.show_custom_icons_flag:
            self.drag_drop_add_button = Gtk.Button.new_from_icon_name(
                Gtk.STOCK_ADD,
                Gtk.IconSize.BUTTON,
            )
        else:
            self.drag_drop_add_button = Gtk.Button.new()
            self.drag_drop_add_button.set_image(
                Gtk.Image.new_from_pixbuf(
                    self.pixbuf_dict['stock_add'],
                ),
            )
        hbox.pack_start(self.drag_drop_add_button, False, False, 0)
        self.drag_drop_add_button.set_action_name(
            'app.drag_drop_add_button',
        )
        self.drag_drop_add_button.set_tooltip_text(
            _('Add a new dropzone'),
        )

        # Drag and Drop Grid
        # ------------------

        # Use a frame, containing a grid. The frame is more convenient, because
        #   its border can be made invisible, and we can use .get_child() and
        #   .remove()
        self.drag_drop_frame = Gtk.Frame()
        grid.attach(self.drag_drop_frame, 0, 1, 1, 1)
        self.drag_drop_frame.set_border_width(0)
        self.drag_drop_frame.set_hexpand(True)
        self.drag_drop_frame.set_vexpand(True)
        self.drag_drop_frame.set_shadow_type(Gtk.ShadowType.NONE)

        self.drag_drop_grid_reset()


    def setup_output_tab(self):

        """Called by self.setup_win().

        Creates widgets for the Output tab.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 4148 setup_output_tab')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Main window\'s Output tab'
        )

        grid = Gtk.Grid()
        self.output_tab.pack_start(grid, True, True, 0)
        grid.set_column_spacing(self.spacing_size)
        grid.set_row_spacing(self.spacing_size)

        grid_width = 3

        # During a download operation, each page in the Output tab's
        #   Gtk.Notebook displays output from a single downloads.DownloadWorker
        #   object
        # The pages are added later, via a call to
        #   self.output_tab_setup_pages()
        self.output_notebook = Gtk.Notebook()
        grid.attach(self.output_notebook, 0, 0, grid_width, 1)
        self.output_notebook.set_border_width(0)
        self.output_notebook.set_scrollable(True)

        # When the user switches between notebook pages, scroll the visible
        #   page's textview to the bottom (otherwise it gets confusing)
        self.output_notebook.connect(
            'switch-page',
            self.on_output_notebook_switch_page,
        )

        # Strip of widgets at the bottom, visible for all tabs
        self.output_size_checkbutton = Gtk.CheckButton()
        grid.attach(self.output_size_checkbutton, 1, 1, 1, 1)
        self.output_size_checkbutton.set_label(_('Maximum page size'))
        self.output_size_checkbutton.set_active(
            self.app_obj.output_size_apply_flag,
        )
        self.output_size_checkbutton.set_hexpand(False)
        self.output_size_checkbutton.connect(
            'toggled',
            self.on_output_size_checkbutton_changed,
        )

        self.output_size_spinbutton = Gtk.SpinButton.new_with_range(
            self.app_obj.output_size_min,
            self.app_obj.output_size_max,
            1,
        )
        grid.attach(self.output_size_spinbutton, 2, 1, 1, 1)
        self.output_size_spinbutton.set_value(self.app_obj.output_size_default)
        self.output_size_spinbutton.set_hexpand(False)
        self.output_size_spinbutton.connect(
            'value-changed',
            self.on_output_size_spinbutton_changed,
        )

        # (Add an empty label for spacing)
        label = Gtk.Label()
        grid.attach(label, 0, 1, 1, 1)
        label.set_hexpand(True)


    def setup_errors_tab(self):

        """Called by self.setup_win().

        Creates widgets for the Errors/Warnings tab.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 4218 setup_errors_tab')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Main window\'s Errors / Warnings tab'
        )

        vbox = Gtk.VBox()
        self.errors_tab.pack_start(vbox, True, True, 0)

        # Errors List. Use a modified Gtk.TreeView which permits drag-and-drop
        #   for multiple rows
        self.errors_list_frame = Gtk.Frame()
        vbox.pack_start(self.errors_list_frame, True, True, 0)

        self.errors_list_reset()

        # Strips of widgets at the bottom

        # (First row)
        hbox = Gtk.HBox()
        vbox.pack_start(hbox, False, False, 0)
        hbox.set_border_width(self.spacing_size)

        self.show_system_error_checkbutton = Gtk.CheckButton()
        hbox.pack_start(self.show_system_error_checkbutton, False, False, 0)
        self.show_system_error_checkbutton.set_label(
            _('Show Tartube errors'),
        )
        self.show_system_error_checkbutton.set_active(
            self.app_obj.system_error_show_flag,
        )
        self.show_system_error_checkbutton.connect(
            'toggled',
            self.on_system_error_checkbutton_changed,
        )

        self.show_system_warning_checkbutton = Gtk.CheckButton()
        hbox.pack_start(self.show_system_warning_checkbutton, False, False, 0)
        self.show_system_warning_checkbutton.set_label(
            _('Show Tartube warnings'),
        )
        self.show_system_warning_checkbutton.set_active(
            self.app_obj.system_warning_show_flag,
        )
        self.show_system_warning_checkbutton.connect(
            'toggled',
            self.on_system_warning_checkbutton_changed,
        )

        self.show_operation_error_checkbutton = Gtk.CheckButton()
        hbox.pack_start(self.show_operation_error_checkbutton, False, False, 0)
        self.show_operation_error_checkbutton.set_label(
            _('Show operation errors'),
        )
        self.show_operation_error_checkbutton.set_active(
            self.app_obj.operation_error_show_flag,
        )
        self.show_operation_error_checkbutton.connect(
            'toggled',
            self.on_operation_error_checkbutton_changed,
        )

        self.show_operation_warning_checkbutton = Gtk.CheckButton()
        hbox.pack_start(
            self.show_operation_warning_checkbutton,
            False,
            False,
            0,
        )
        self.show_operation_warning_checkbutton.set_label(
            _('Show operation warnings'),
        )
        self.show_operation_warning_checkbutton.set_active(
            self.app_obj.operation_warning_show_flag,
        )
        self.show_operation_warning_checkbutton.connect(
            'toggled',
            self.on_operation_warning_checkbutton_changed,
        )

        # (Second row)
        hbox2 = Gtk.HBox()
        vbox.pack_start(hbox2, False, False, 0)
        hbox2.set_border_width(self.spacing_size)

        self.show_system_date_checkbutton = Gtk.CheckButton()
        hbox2.pack_start(
            self.show_system_date_checkbutton,
            False,
            False,
            0,
        )
        self.show_system_date_checkbutton.set_label(
            _('Show dates'),
        )
        self.show_system_date_checkbutton.set_active(
            self.app_obj.system_msg_show_date_flag,
        )
        self.show_system_date_checkbutton.connect(
            'toggled',
            self.on_system_date_checkbutton_changed,
        )

        self.show_system_container_checkbutton = Gtk.CheckButton()
        hbox2.pack_start(
            self.show_system_container_checkbutton,
            False,
            False,
            0,
        )
        self.show_system_container_checkbutton.set_label(
            _('Show channel/playlist/folder names'),
        )
        self.show_system_container_checkbutton.set_active(
            self.app_obj.system_msg_show_container_flag,
        )
        self.show_system_container_checkbutton.connect(
            'toggled',
            self.on_system_container_checkbutton_changed,
        )

        self.show_system_video_checkbutton = Gtk.CheckButton()
        hbox2.pack_start(
            self.show_system_video_checkbutton,
            False,
            False,
            0,
        )
        self.show_system_video_checkbutton.set_label(
            _('Show video names'),
        )
        self.show_system_video_checkbutton.set_active(
            self.app_obj.system_msg_show_video_flag,
        )
        self.show_system_video_checkbutton.connect(
            'toggled',
            self.on_system_video_checkbutton_changed,
        )

        self.show_system_multi_line_checkbutton = Gtk.CheckButton()
        hbox2.pack_start(
            self.show_system_multi_line_checkbutton,
            False,
            False,
            0,
        )
        self.show_system_multi_line_checkbutton.set_label(
            _('Show full messages'),
        )
        self.show_system_multi_line_checkbutton.set_active(
            self.app_obj.system_msg_show_multi_line_flag,
        )
        self.show_system_multi_line_checkbutton.connect(
            'toggled',
            self.on_system_multi_line_checkbutton_changed,
        )

        self.error_list_copy_button = Gtk.Button()
        hbox2.pack_end(self.error_list_copy_button, False, False, 0)
        self.error_list_copy_button.set_label(_('Copy selected'))
        self.error_list_copy_button.set_tooltip_text(
            _('Copy selected errors/warnings to the clipboard')
        )
        self.error_list_copy_button.connect(
            'clicked',
            self.on_errors_list_copy,
        )

        # (Third row)
        hbox3 = Gtk.HBox()
        vbox.pack_start(hbox3, False, False, 0)
        hbox3.set_border_width(self.spacing_size)

        label = Gtk.Label(_('Filter') + '  ')
        hbox3.pack_start(label, False, False, 0)

        self.error_list_entry = Gtk.Entry()
        hbox3.pack_start(self.error_list_entry, False, False, 0)
        self.error_list_entry.set_width_chars(16)
        self.error_list_entry.set_tooltip_text(_('Enter search text'))

        self.error_list_togglebutton = Gtk.ToggleButton(_('Regex'))
        hbox3.pack_start(self.error_list_togglebutton, False, False, 0)
        self.error_list_togglebutton.set_tooltip_text(
            _('Select if search text is a regex'),
        )

        # (Empty label for spacing)
        label2 = Gtk.Label('   ')
        hbox3.pack_start(label2, False, False, 0)

        self.error_list_container_checkbutton = Gtk.CheckButton()
        hbox3.pack_start(
            self.error_list_container_checkbutton,
            False,
            False,
            0,
        )
        self.error_list_container_checkbutton.set_label('Container names')
        self.error_list_container_checkbutton.set_active(True)

        self.error_list_video_checkbutton = Gtk.CheckButton()
        hbox3.pack_start(self.error_list_video_checkbutton, False, False, 0)
        self.error_list_video_checkbutton.set_label('Video names')
        self.error_list_video_checkbutton.set_active(True)

        self.error_list_msg_checkbutton = Gtk.CheckButton()
        hbox3.pack_start(self.error_list_msg_checkbutton, False, False, 0)
        self.error_list_msg_checkbutton.set_label('Messages')
        self.error_list_msg_checkbutton.set_active(True)

        if not self.app_obj.show_custom_icons_flag:
            self.error_list_filter_toolbutton \
            = Gtk.ToolButton.new_from_stock(Gtk.STOCK_FIND)
        else:
            self.error_list_filter_toolbutton = Gtk.ToolButton.new()
            self.error_list_filter_toolbutton.set_icon_widget(
                Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_find']),
            )
        hbox3.pack_start(self.error_list_filter_toolbutton, False, False, 0)
        self.error_list_filter_toolbutton.set_tooltip_text(
            _('Filter messages'),
        )
        self.error_list_filter_toolbutton.set_action_name(
            'app.apply_error_filter_toolbutton',
        )

        if not self.app_obj.show_custom_icons_flag:
            self.error_list_cancel_toolbutton \
            = Gtk.ToolButton.new_from_stock(Gtk.STOCK_CANCEL)
        else:
            self.error_list_cancel_toolbutton = Gtk.ToolButton.new()
            self.error_list_cancel_toolbutton.set_icon_widget(
                Gtk.Image.new_from_pixbuf(self.pixbuf_dict['stock_cancel']),
            )
        hbox3.pack_start(self.error_list_cancel_toolbutton, False, False, 0)
        self.error_list_cancel_toolbutton.set_sensitive(False)
        self.error_list_cancel_toolbutton.set_tooltip_text(
            _('Cancel filter'),
        )
        self.error_list_cancel_toolbutton.set_action_name(
            'app.cancel_error_filter_toolbutton',
        )

        self.error_list_clear_button = Gtk.Button()
        hbox3.pack_end(self.error_list_clear_button, False, False, 0)
        self.error_list_clear_button.set_label(
            '     ' + _('Clear list') + '     ',
        )
        self.error_list_copy_button.set_tooltip_text(
            _('Clear all visible errors/warnings')
        )
        self.error_list_clear_button.connect(
            'clicked',
            self.on_errors_list_clear,
        )


    # (Moodify main window widgets)


    def desensitise_test_widgets(self):

        """Called by mainapp.TartubeApp.on_menu_test().

        Clicking the Test menu item / toolbutton more than once just adds
        illegal duplicate channels/playlists/folders (and non-illegal duplicate
        videos), so this function is called to just disable both widgets.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 4489 desensitise_test_widgets')

        if self.test_menu_item:
            self.test_menu_item.set_sensitive(False)
        if self.test_toolbutton:
            self.test_toolbutton.set_sensitive(False)


    def disable_dl_all_buttons(self):

        """Called by mainapp.TartubeApp.start() and
        set_disable_dl_all_flag().

        Disables (desensitises) the 'Download all' buttons and menu items.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 4506 disable_dl_all_buttons')

        # This setting doesn't apply during an operation. The calling code
        #   should have checked that mainapp.TartubeApp.disable_dl_all_flag is
        #   True
        if not self.app_obj.current_manager_obj \
        or __main__.__pkg_no_download_flag__:
            self.download_all_menu_item.set_sensitive(False)
            self.custom_dl_all_menu_item.set_sensitive(False)
            self.download_all_toolbutton.set_sensitive(False)
            self.download_media_button.set_sensitive(False)
            if self.custom_dl_media_button:
                self.custom_dl_media_button.set_sensitive(False)


    def disable_tooltips(self, update_catalogue_flag=False):

        """Called by mainapp.TartubeApp.load_config() and
        .set_show_tooltips_flag().

        Disables tooltips in the Video Index, Video Catalogue, Progress List,
        Results List and Classic Mode tab (only).

        Args:

            update_catalogue_flag (bool): True when called by
                .set_show_tooltips_flag(), in which case the Video Catalogue
                must be redrawn

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 4538 disable_tooltips')

        # Update the Video Index. Using a dummy column makes the tooltips
        #   invisible
        self.video_index_treeview.set_tooltip_column(-1)

        # Update the Video Catalogue, if a playlist/channel/folder is selected
        if update_catalogue_flag and self.video_index_current_dbid:
            self.video_catalogue_redraw_all(
                self.video_index_current_dbid,
                self.catalogue_toolbar_current_page,
            )

        # Update the Progress List
        self.progress_list_treeview.set_tooltip_column(-1)

        # Update the Results List
        self.results_list_treeview.set_tooltip_column(-1)

        # Update the Classic Mode tab
        self.classic_progress_treeview.set_tooltip_column(-1)


    def enable_dl_all_buttons(self):

        """Called by mainapp.TartubeApp.set_disable_dl_all_flag().

        Enables (sensitises) the 'Download all' buttons and menu items.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 4569 enable_dl_all_buttons')

        # This setting doesn't apply during an operation. The calling code
        #   should have checked that mainapp.TartubeApp.disable_dl_all_flag is
        #   False
        if not self.app_obj.current_manager_obj \
        and not __main__.__pkg_no_download_flag__:
            self.download_all_menu_item.set_sensitive(True)
            self.custom_dl_all_menu_item.set_sensitive(True)
            self.download_all_toolbutton.set_sensitive(True)
            self.download_media_button.set_sensitive(True)
            if self.custom_dl_media_button:
                self.custom_dl_media_button.set_sensitive(True)


    def enable_tooltips(self, update_catalogue_flag=False):

        """Called by mainapp.TartubeApp.set_show_tooltips_flag().

        Enables tooltips in the Video Index, Video Catalogue, Progress List,
        Results List and Classic Mode tab (only).

        Args:

            update_catalogue_flag (bool): True when called by
                .set_show_tooltips_flag(), in which case the Video Catalogue
                must be redrawn

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 4600 enable_tooltips')

        # Update the Video Index
        self.video_index_treeview.set_tooltip_column(
            self.video_index_tooltip_column,
        )

        # Update the Video Catalogue, if a playlist/channel/folder is selected
        if update_catalogue_flag and self.video_index_current_dbid:
            self.video_catalogue_redraw_all(
                self.video_index_current_dbid,
                self.catalogue_toolbar_current_page,
            )

        # Update the Progress List
        self.progress_list_treeview.set_tooltip_column(
             self.progress_list_tooltip_column,
        )

        # Update the Results List
        self.results_list_treeview.set_tooltip_column(
            self.results_list_tooltip_column,
        )

        # Update the Classic Mode tab
        self.classic_progress_treeview.set_tooltip_column(
            self.classic_progress_tooltip_column,
        )


    def force_invisible(self):

        """Called by mainapp.TartubeApp.start_continue().

        An alternative to self.toggle_visibility(), in which the window is
        made invisible (during startup).

        The calling code must check that Tartube is visible in the system tray,
        or the user will be in big trouble.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 4642 force_invisible')

        self.set_visible(False)


    def hide_progress_bar(self, skip_check_flag=False):

        """Can be called by anything.

        Called after an operation has finished to replace the progress bar in
        the Videos tab with download buttons.

        Called by several ofther functions to update the existing download
        buttons (to avoid Gtk crashes).

        Args:

            skip_check_flag (bool): If True, don't perform a sanity check; just
                remove old widgets and replace them with new ones

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 4665 hide_progress_bar')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Main window\'s Videos tab'
        )

        if not self.progress_bar and not skip_check_flag:
            return self.app_obj.system_error(
                201,
                'Videos tab progress bar is not already visible',
            )

        # Remove existing widgets. In previous code, we simply changed the
        #   label on on self.check_media_button, but this causes frequent
        #   crashes
        # Get around the crashes by destroying the old widget and creating a
        #   new one
        if self.check_media_button:
            self.button_box.remove(self.check_media_button)
            self.check_media_button = None

        if self.download_media_button:
            self.button_box.remove(self.download_media_button)
            self.check_media_button = None

        if self.custom_dl_media_button:
            self.button_box.remove(self.custom_dl_media_button)
            self.custom_dl_media_button = None

        if self.progress_box:
            self.button_box.remove(self.progress_box)
            self.progress_box = None
            self.progress_bar = None

        # Add replacement widgets
        self.check_media_button = Gtk.Button()
        self.button_box.pack_start(self.check_media_button, True, True, 0)
        if not self.video_index_marker_dict:
            self.check_media_button.set_label(_('Check all'))
            self.check_media_button.set_tooltip_text(
                _('Check all videos, channels, playlists and folders'),
            )
        else:
            self.check_media_button.set_label(_('Check marked items'))
            self.check_media_button.set_tooltip_text(
                _('Check marked videos, channels, playlists and folders'),
            )
        self.check_media_button.set_action_name('app.check_all_button')

        self.download_media_button = Gtk.Button()
        self.button_box.pack_start(self.download_media_button, True, True, 0)
        if not self.video_index_marker_dict:
            self.download_media_button.set_label(_('Download all'))
            self.download_media_button.set_tooltip_text(
                _('Download all videos, channels, playlists and folders'),
            )
        else:
            self.download_media_button.set_label(_('Download marked items'))
            self.download_media_button.set_tooltip_text(
                _('Download marked videos, channels, playlists and folders'),
            )
        self.download_media_button.set_action_name('app.download_all_button')

        if self.app_obj.show_custom_dl_button_flag:

            self.custom_dl_media_button = Gtk.Button()
            self.button_box.pack_start(
                self.custom_dl_media_button,
                True,
                True,
                0
            )
            if not self.video_index_marker_dict:
                self.custom_dl_media_button.set_label(_('Custom download all'))
                self.custom_dl_media_button.set_tooltip_text(
                    _(
                    'Perform a custom download of all videos, channels,' \
                    + ' playlists and folders',
                    ),
                )
            else:
                self.custom_dl_media_button.set_label(
                    _('Custom download marked items'),
                )
                self.custom_dl_media_button.set_tooltip_text(
                    _(
                    'Perform a custom download of marked videos, channels,' \
                    + ' playlists and folders',
                    ),
                )
            self.custom_dl_media_button.set_action_name(
                'app.custom_dl_all_button',
            )

        # (For some reason, the button must be desensitised after setting the
        #   action name)
        if __main__.__pkg_no_download_flag__ \
        or self.app_obj.disable_dl_all_flag:
            self.download_media_button.set_sensitive(False)
        else:
            self.download_media_button.set_sensitive(True)

        # Make the changes visible
        self.button_box.show_all()


    def notify_desktop(self, title=None, msg=None, icon_path=None, url=None):

        """Can be called by anything.

        Creates a desktop notification (but not on MS Windows / MacOS)

        Args:

            title (str): The notification title. If None, 'Tartube' is used
                used

            msg (str): The message to show. If None, 'Tartube' is used

            icon_path (str): The absolute path to the icon file to use. If
                None, a default icon is used

            url (str): If specified, a 'Click to open' button is added to the
                desktop notification. Clicking the button opens the URL

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 4793 notify_desktop')

        # Desktop notifications don't work on MS Windows/MacOS
        if mainapp.HAVE_NOTIFY_FLAG:

            if title is None:
                title = 'Tartube'

            if msg is None:
                # Emergency fallback - better than an empty message
                msg = 'Tartube'

            if icon_path is None:
                icon_path = os.path.abspath(
                    os.path.join(
                        self.icon_dir_path,
                        'dialogue',
                        formats.DIALOGUE_ICON_DICT['system_icon'],
                    ),
                )

            notify_obj = Notify.Notification.new(title, msg, icon_path)

            if url is not None:

                # We need to retain a reference to the Notify.Notification, or
                #   the callback won't work
                self.notify_desktop_count += 1
                self.notify_desktop_dict[self.notify_desktop_count] \
                = notify_obj

                notify_obj.add_action(
                    'action_click',
                    'Watch',
                    self.on_notify_desktop_clicked,
                    self.notify_desktop_count,
                    url,
                )

                notify_obj.connect(
                    'closed',
                    self.on_notify_desktop_closed,
                    self.notify_desktop_count,
                )

            # Notification is ready; show it
            notify_obj.show()


    def redraw_main_toolbar(self):

        """Called by mainapp.TartubeApp.set_toolbar_squeeze_flag().

        Redraws the main toolbar, with or without labels, depending on the
        value of the flag.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 4851 redraw_main_toolbar')

        if self.app_obj.toolbar_hide_flag:
            # Toolbar is not visible
            return

        else:

            self.setup_main_toolbar()

            if __main__.__pkg_no_download_flag__ \
            or self.app_obj.disable_dl_all_flag:
                self.download_all_menu_item.set_sensitive(False)
                self.custom_dl_all_menu_item.set_sensitive(False)

            self.show_all()


    def reset_sliders(self):

        """Called by config.SystemPrefWin.setup_windows_main_window_tab().

        Resets paned sliders in various tabs to their default positions.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 4877 reset_sliders')

        self.videos_paned.set_position(
            self.app_obj.paned_default_size,
        )

        self.progress_paned.set_position(
            self.app_obj.paned_default_size,
        )

        self.classic_paned.set_position(
            self.app_obj.paned_default_size + 50,
        )


    def resize_self(self, width, height):

        """Can be called by anything.

        Resizes the main window.

        Args:

            width, height (int): The new size in pixels of the main window.
                If either (or both) values are lower than 100, then 100 is
                used instead

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 4907 resize_self')

        if width < 100:
            width = 100
        if height < 100:
            height = 100

        self.resize(width, height)


    def sensitise_check_dl_buttons(self, finish_flag, operation_type=None):

        """Called by mainapp.TartubeApp.update_manager_start(),
        .update_manager_finished(), .info_manager_start() and
        .info_manager_finished().

        Modify and de(sensitise) widgets during an update or info operation.

        Args:

            finish_flag (bool): False at the start of the update operation,
                True at the end of it

            operation_type (str): 'ffmpeg' for an update operation to install
                FFmpeg, 'matplotlib' for an update operation to install
                matplotlib, 'streamlink' for an update operation to install
                streamlink, 'ytdl' for an update operation to install/update
                youtube-dl, 'formats' for an info operation to fetch available
                video formats, 'subs' for an info operation to fetch
                available subtitles, 'test_ytdl' for an info operation in which
                youtube-dl is tested, 'version' for an info operation to check
                for new Tartube releases, or None when finish_flag is True

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 4943 sensitise_check_dl_buttons')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Main window\'s Videos tab'
        )

        if operation_type is not None \
        and operation_type != 'ffmpeg' and operation_type != 'matplotlib' \
        and operation_type != 'streamlink' and operation_type != 'ytdl' \
        and operation_type != 'formats' and operation_type != 'subs' \
        and operation_type != 'test_ytdl' and operation_type != 'version':
            return self.app_obj.system_error(
                202,
                'Invalid update/info operation argument',
            )

        # Remove existing widgets. In previous code, we simply changed the
        #   label on on self.check_media_button, but this causes frequent
        #   crashes
        # Get around the crashes by destroying the old widgets and creating new
        #   ones
        if self.check_media_button:
            self.button_box.remove(self.check_media_button)
            self.check_media_button = None

        if self.download_media_button:
            self.button_box.remove(self.download_media_button)
            self.download_media_button = None

        if self.custom_dl_media_button:
            self.button_box.remove(self.custom_dl_media_button)
            self.custom_dl_media_button = None

        if self.progress_box:
            self.button_box.remove(self.progress_box)
            self.progress_box = None
            self.progress_bar = None

        # Add replacement widgets
        self.check_media_button = Gtk.Button()
        self.button_box.pack_start(self.check_media_button, True, True, 0)
        self.check_media_button.set_action_name('app.check_all_button')

        self.download_media_button = Gtk.Button()
        self.button_box.pack_start(self.download_media_button, True, True, 0)
        self.download_media_button.set_action_name('app.download_all_button')

        if self.app_obj.show_custom_dl_button_flag:

            self.custom_dl_media_button = Gtk.Button()
            self.button_box.pack_start(
                self.custom_dl_media_button,
                True,
                True,
                0
            )
            self.custom_dl_media_button.set_sensitive(False)

        # Set labels on the replacement buttons
        if not finish_flag:

            downloader = self.app_obj.get_downloader();

            if operation_type == 'ffmpeg':
                msg = _('Installing FFmpeg')
            elif operation_type == 'matplotlib':
                msg = _('Installing matplotlib')
            elif operation_type == 'streamlink':
                msg = _('Installing streamlink')
            elif operation_type == 'ytdl':
                msg = _('Updating downloader')
            elif operation_type == 'formats':
                msg = _('Fetching formats')
            elif operation_type == 'subs':
                msg = _('Fetching subtitles')
            elif operation_type == 'test_ytdl':
                msg = _('Testing downloader')
            else:
                msg = _('Contacting website')

            self.check_media_button.set_label(msg)
            self.download_media_button.set_label('...')
            if self.custom_dl_media_button:
                self.custom_dl_media_button.set_label(msg)

            self.check_media_button.set_sensitive(False)
            self.download_media_button.set_sensitive(False)

            self.sensitise_operation_widgets(False, True)

        else:

            if not self.video_index_marker_dict:
                self.check_media_button.set_label(_('Check all'))
                self.check_media_button.set_tooltip_text(
                    _('Check all videos, channels, playlists and folders'),
                )
            else:
                self.check_media_button.set_label(_('Check marked items'))
                self.check_media_button.set_tooltip_text(
                    _(
                        'Check marked videos, channels, playlists and' \
                        + ' folders',
                    ),
                )

            self.check_media_button.set_sensitive(True)

            if not self.video_index_marker_dict:
                self.download_media_button.set_label('Download all')
                self.download_media_button.set_tooltip_text(
                    _('Download all videos, channels, playlists and folders'),
                )
            else:
                self.download_media_button.set_label('Download marked items')
                self.download_media_button.set_tooltip_text(
                    _(
                        'Download marked videos, channels, playlists and' \
                        + ' folders',
                    ),
                )

            if __main__.__pkg_no_download_flag__ \
            or self.app_obj.disable_dl_all_flag:
                self.download_media_button.set_sensitive(False)
            else:
                self.download_media_button.set_sensitive(True)

            if self.custom_dl_media_button:

                if not self.video_index_marker_dict:
                    self.custom_dl_media_button.set_label(
                        'Custom download all',
                    )
                    self.custom_dl_media_button.set_tooltip_text(
                        _(
                        'Perform a custom download of all videos, channels,' \
                        + ' playlists and folders',
                        ),
                    )
                else:
                    self.custom_dl_media_button.set_label(
                        'Custom download marked items',
                    )
                    self.custom_dl_media_button.set_tooltip_text(
                        _(
                        'Perform a custom download of marked videos, ' \
                        + ' channels, playlists and folders',
                        ),
                    )

                if __main__.__pkg_no_download_flag__ \
                or self.app_obj.disable_dl_all_flag:
                    self.custom_dl_media_button.set_sensitive(False)
                else:
                    self.custom_dl_media_button.set_sensitive(True)

            self.sensitise_operation_widgets(True, True)

        # Make the widget changes visible
        self.show_all()


    def sensitise_operation_widgets(self, sens_flag, \
    not_dl_operation_flag=False):

        """Can by called by anything.

        (De)sensitises widgets that must not be sensitised during a download/
        update/refresh/info/tidy operation.

        Args:

            sens_flag (bool): False to desensitise widget at the start of an
                operation, True to re-sensitise widgets at the end of the
                operation

            not_dl_operation_flag (True, False or None): False when called by
                download operation functions, True when called by everything
                else

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 5127 sensitise_operation_widgets')

        self.system_prefs_menu_item.set_sensitive(sens_flag)
        self.gen_options_menu_item.set_sensitive(sens_flag)
        self.reset_container_menu_item.set_sensitive(sens_flag)
        self.export_db_menu_item.set_sensitive(sens_flag)
        self.import_db_menu_item.set_sensitive(sens_flag)
        self.import_yt_menu_item.set_sensitive(sens_flag)
        self.check_all_menu_item.set_sensitive(sens_flag)

        if __main__.__pkg_no_download_flag__ \
        or self.app_obj.disable_dl_all_flag:
            self.download_all_menu_item.set_sensitive(False)
            self.custom_dl_all_menu_item.set_sensitive(False)
        else:
            self.download_all_menu_item.set_sensitive(sens_flag)
            self.custom_dl_all_menu_item.set_sensitive(sens_flag)

        self.refresh_db_menu_item.set_sensitive(sens_flag)
        self.check_all_toolbutton.set_sensitive(sens_flag)

        if __main__.__pkg_no_download_flag__ \
        or self.app_obj.disable_dl_all_flag:
            self.download_all_toolbutton.set_sensitive(False)
        else:
            self.download_all_toolbutton.set_sensitive(sens_flag)

        if __main__.__pkg_strict_install_flag__:
            self.update_ytdl_menu_item.set_sensitive(False)
        else:
            self.update_ytdl_menu_item.set_sensitive(sens_flag)

        self.test_ytdl_menu_item.set_sensitive(sens_flag)

        if os.name == 'nt':
            self.install_ffmpeg_menu_item.set_sensitive(sens_flag)
            self.install_matplotlib_menu_item.set_sensitive(sens_flag)
            self.install_streamlink_menu_item.set_sensitive(sens_flag)

        if not_dl_operation_flag:
            self.update_live_menu_item.set_sensitive(sens_flag)
        else:
            self.update_live_menu_item.set_sensitive(True)

        # (The 'Add videos', 'Add channel' etc menu items/buttons are
        #   sensitised during a download operation, but desensitised during
        #   other operations)
        if not_dl_operation_flag:
            self.add_video_menu_item.set_sensitive(sens_flag)
            self.add_channel_menu_item.set_sensitive(sens_flag)
            self.add_playlist_menu_item.set_sensitive(sens_flag)
            self.add_folder_menu_item.set_sensitive(sens_flag)
            self.add_video_toolbutton.set_sensitive(sens_flag)
            self.add_channel_toolbutton.set_sensitive(sens_flag)
            self.add_playlist_toolbutton.set_sensitive(sens_flag)
            self.add_folder_toolbutton.set_sensitive(sens_flag)

        # (The 'Change database', etc menu items must remain desensitised if
        #   file load/save is disabled)
        if not self.app_obj.disable_load_save_flag:
            self.change_db_menu_item.set_sensitive(sens_flag)
            self.save_db_menu_item.set_sensitive(sens_flag)
            self.save_all_menu_item.set_sensitive(sens_flag)

        # (The 'Stop' button/menu item are only sensitised during a download/
        #   update/refresh/info/tidy operation)
        if not sens_flag:
            self.stop_operation_menu_item.set_sensitive(True)
            self.stop_operation_toolbutton.set_sensitive(True)
        else:
            self.stop_operation_menu_item.set_sensitive(False)
            self.stop_operation_toolbutton.set_sensitive(False)

        if not not_dl_operation_flag and not sens_flag:
            self.stop_soon_menu_item.set_sensitive(True)
        else:
            self.stop_soon_menu_item.set_sensitive(False)

        # (The 'System preferences' and 'General download options' buttons are
        #   only visible in the toolbar, when labels are not visibel)
        if self.system_prefs_toolbutton:
            self.system_prefs_toolbutton.set_sensitive(sens_flag)
            self.gen_options_toolbutton.set_sensitive(sens_flag)

        # The corresponding buttons in the Classic Mode tab must also be
        #   updated
        self.classic_stop_button.set_sensitive(not sens_flag)
        self.classic_clips_button.set_sensitive(sens_flag)
        self.classic_ffmpeg_button.set_sensitive(sens_flag)
        self.classic_clear_button.set_sensitive(sens_flag)
        self.classic_clear_dl_button.set_sensitive(sens_flag)
        if __main__.__pkg_no_download_flag__:
            self.classic_redownload_button.set_sensitive(False)
            self.classic_download_button.set_sensitive(False)
        elif not not_dl_operation_flag:
            self.classic_redownload_button.set_sensitive(True)
            self.classic_download_button.set_sensitive(sens_flag)
        else:
            self.classic_redownload_button.set_sensitive(sens_flag)
            self.classic_download_button.set_sensitive(sens_flag)


    def sensitise_progress_bar(self, sens_flag):

        """Called by mainapp.TartubeApp.download_manager_continue().

        When a download operation is launched from the Classic Mode tab, we
        don't replace the main Check all/Download all buttons with a progress
        bar; instead, we just (de)sensitise the existing buttons.

        Args:

            sens_flag (bool): True to sensitise the buttons, False to
                desensitise them

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 5245 sensitise_progress_bar')

        self.check_media_button.set_sensitive(sens_flag)
        self.classic_clear_button.set_sensitive(sens_flag)
        self.classic_clear_dl_button.set_sensitive(sens_flag)

        if __main__.__pkg_no_download_flag__:
            self.download_media_button.set_sensitive(False)
            self.classic_download_button.set_sensitive(False)
            self.classic_redownload_button.set_sensitive(False)
            if self.custom_dl_media_button:
                self.custom_dl_media_button.set_sensitive(False)

        elif self.app_obj.disable_dl_all_flag:
            self.download_media_button.set_sensitive(False)
            self.classic_download_button.set_sensitive(False)
            self.classic_redownload_button.set_sensitive(sens_flag)
            if self.custom_dl_media_button:
                self.custom_dl_media_button.set_sensitive(False)

        else:
            self.download_media_button.set_sensitive(sens_flag)
            self.classic_download_button.set_sensitive(sens_flag)
            self.classic_redownload_button.set_sensitive(sens_flag)
            if self.custom_dl_media_button:
                self.custom_dl_media_button.set_sensitive(sens_flag)


    def sensitise_widgets_if_database(self, sens_flag):

        """Called by mainapp.TartubeApp.start(), .load_db(), .save_db() and
        .disable_load_save().

        When no database file has been loaded into memory, most main window
        widgets should be desensitised. This function is called to sensitise
        or desensitise the widgets after a change in state.

        Args:

            sens_flag (bool): True to sensitise most widgets, False to
                desensitise most widgets

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 5290 sensitise_widgets_if_database')

        # Menu items
        self.change_db_menu_item.set_sensitive(sens_flag)
        self.save_db_menu_item.set_sensitive(sens_flag)
        self.save_all_menu_item.set_sensitive(sens_flag)

        self.system_prefs_menu_item.set_sensitive(sens_flag)
        self.gen_options_menu_item.set_sensitive(sens_flag)

        self.add_video_menu_item.set_sensitive(sens_flag)
        self.add_channel_menu_item.set_sensitive(sens_flag)
        self.add_playlist_menu_item.set_sensitive(sens_flag)
        self.add_folder_menu_item.set_sensitive(sens_flag)

        self.add_bulk_menu_item.set_sensitive(sens_flag)
        self.reset_container_menu_item.set_sensitive(sens_flag)

        self.export_db_menu_item.set_sensitive(sens_flag)
        self.import_db_menu_item.set_sensitive(sens_flag)
        self.import_yt_menu_item.set_sensitive(sens_flag)
        self.show_hide_menu_item.set_sensitive(sens_flag)
        self.profile_menu_item.set_sensitive(sens_flag)

        self.check_all_menu_item.set_sensitive(sens_flag)
        if __main__.__pkg_no_download_flag__ \
        or self.app_obj.disable_dl_all_flag:
            self.download_all_menu_item.set_sensitive(False)
            self.custom_dl_all_menu_item.set_sensitive(False)
        else:
            self.download_all_menu_item.set_sensitive(sens_flag)
            self.custom_dl_all_menu_item.set_sensitive(sens_flag)
        self.refresh_db_menu_item.set_sensitive(sens_flag)

        if __main__.__pkg_strict_install_flag__:
            self.update_ytdl_menu_item.set_sensitive(False)
        else:
            self.update_ytdl_menu_item.set_sensitive(sens_flag)

        self.test_ytdl_menu_item.set_sensitive(sens_flag)

        if os.name == 'nt':
            self.install_ffmpeg_menu_item.set_sensitive(sens_flag)
            self.install_matplotlib_menu_item.set_sensitive(sens_flag)
            self.install_streamlink_menu_item.set_sensitive(sens_flag)

        self.stop_operation_menu_item.set_sensitive(False)
        self.stop_soon_menu_item.set_sensitive(False)

        if self.test_menu_item:
            self.test_menu_item.set_sensitive(sens_flag)

        # Toolbuttons
        self.add_video_toolbutton.set_sensitive(sens_flag)
        self.add_channel_toolbutton.set_sensitive(sens_flag)
        self.add_playlist_toolbutton.set_sensitive(sens_flag)
        self.add_folder_toolbutton.set_sensitive(sens_flag)

        self.check_all_toolbutton.set_sensitive(sens_flag)
        if __main__.__pkg_no_download_flag__ \
        or self.app_obj.disable_dl_all_flag:
            self.download_all_toolbutton.set_sensitive(False)
        else:
            self.download_all_toolbutton.set_sensitive(sens_flag)
        self.stop_operation_toolbutton.set_sensitive(False)

        if self.system_prefs_toolbutton:
            self.system_prefs_toolbutton.set_sensitive(sens_flag)
            self.gen_options_toolbutton.set_sensitive(sens_flag)

        self.switch_view_toolbutton.set_sensitive(sens_flag)
        self.hide_system_toolbutton.set_sensitive(sens_flag)

        if self.test_toolbutton:
            self.test_toolbutton.set_sensitive(sens_flag)

        # Videos tab
        if self.check_media_button:
            self.check_media_button.set_sensitive(sens_flag)
        if self.download_media_button:
            if __main__.__pkg_no_download_flag__ \
            or self.app_obj.disable_dl_all_flag:
                self.download_media_button.set_sensitive(False)
            else:
                self.download_media_button.set_sensitive(sens_flag)
        if self.custom_dl_media_button:
            if __main__.__pkg_no_download_flag__ \
            or self.app_obj.disable_dl_all_flag:
                self.custom_dl_media_button.set_sensitive(False)
            else:
                self.custom_dl_media_button.set_sensitive(sens_flag)

        # Progress tab
        self.num_worker_checkbutton.set_sensitive(sens_flag)
        self.num_worker_spinbutton.set_sensitive(sens_flag)
        self.bandwidth_checkbutton.set_sensitive(sens_flag)
        self.bandwidth_spinbutton.set_sensitive(sens_flag)
        self.video_res_checkbutton.set_sensitive(sens_flag)
        self.video_res_combobox.set_sensitive(sens_flag)

        # Classic Mode tab
        self.classic_menu_button.set_sensitive(sens_flag)
        self.classic_stop_button.set_sensitive(False)
        self.classic_archive_button.set_sensitive(sens_flag)
        self.classic_clips_button.set_sensitive(sens_flag)
        self.classic_ffmpeg_button.set_sensitive(sens_flag)
        self.classic_clear_button.set_sensitive(sens_flag)
        self.classic_clear_dl_button.set_sensitive(sens_flag)
        if __main__.__pkg_no_download_flag__:
            self.classic_redownload_button.set_sensitive(False)
            self.classic_download_button.set_sensitive(False)
        else:
            self.classic_redownload_button.set_sensitive(sens_flag)
            self.classic_download_button.set_sensitive(sens_flag)

        # Output tab
        self.output_size_checkbutton.set_sensitive(sens_flag)
        self.output_size_spinbutton.set_sensitive(sens_flag)

        # Errors/Warnings tab
        self.show_system_error_checkbutton.set_sensitive(sens_flag)
        self.show_system_warning_checkbutton.set_sensitive(sens_flag)
        self.show_operation_error_checkbutton.set_sensitive(sens_flag)
        self.show_operation_warning_checkbutton.set_sensitive(sens_flag)
        self.show_system_date_checkbutton.set_sensitive(sens_flag)
        self.show_system_container_checkbutton.set_sensitive(sens_flag)
        self.show_system_video_checkbutton.set_sensitive(sens_flag)
        self.show_system_multi_line_checkbutton.set_sensitive(sens_flag)
        self.error_list_entry.set_sensitive(sens_flag)
        self.error_list_togglebutton.set_sensitive(sens_flag)
        self.error_list_container_checkbutton.set_sensitive(sens_flag)
        self.error_list_video_checkbutton.set_sensitive(sens_flag)
        self.error_list_msg_checkbutton.set_sensitive(sens_flag)
        if self.error_list_filter_flag:
            self.error_list_filter_toolbutton.set_sensitive(False)
            self.error_list_cancel_toolbutton.set_sensitive(sens_flag)
        else:
            self.error_list_filter_toolbutton.set_sensitive(sens_flag)
            self.error_list_cancel_toolbutton.set_sensitive(False)


    def show_progress_bar(self, operation_type):

        """Called by mainapp.TartubeApp.download_manager_continue(),
        .refresh_manager_continue(), .tidy_manager_start(),
        .process_manager_start().

        At the start of a download/refresh/tidy/process operation, replace
        self.download_media_button with a progress bar (and a label just above
        it).

        Args:

            operation_type (str): The type of operation: 'download' for a
                download operation, 'check' for a download operation with
                simulated downloads, 'refresh' for a refresh operation, 'tidy'
                for a tidy operation, or 'process' for a process operation

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 5451 show_progress_bar')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Main window\'s Videos tab'
        )

        if self.progress_bar:
            return self.app_obj.system_error(
                203,
                'Videos tab progress bar is already visible',
            )

        elif operation_type != 'check' \
        and operation_type != 'download' \
        and operation_type != 'refresh' \
        and operation_type != 'tidy' \
        and operation_type != 'process':
            return self.app_obj.system_error(
                204,
                'Invalid operation type supplied to progress bar',
            )

        # Remove existing widgets. In previous code, we simply changed the
        #   label on on self.check_media_button, but this causes frequent
        #   crashes
        # Get around the crashes by destroying the old widgets and creating new
        #   ones
        if self.check_media_button:
            self.button_box.remove(self.check_media_button)
            self.check_media_button = None

        if self.download_media_button:
            self.button_box.remove(self.download_media_button)
            self.download_media_button = None

        if self.custom_dl_media_button:
            self.button_box.remove(self.custom_dl_media_button)
            self.custom_dl_media_button = None

        # Display a holding message in the replacement buttons, and initially
        #   in the progress bar (the latter is replaced after a very short
        #   interval)
        free_msg = ' [' \
        + str(round(utils.disk_get_free_space(self.app_obj.data_dir), 1)) \
        + ' GiB]'

        if operation_type == 'check':
            temp_msg = msg = _('Checking...')
            if self.app_obj.show_free_space_flag:
                msg = _('Checking')  + free_msg

        elif operation_type == 'download':
            temp_msg = msg = _('Downloading...')
            if self.app_obj.show_free_space_flag:
                msg = _('Downloading')  + free_msg

        elif operation_type == 'refresh':
            temp_msg = msg = _('Refreshing...')

        elif operation_type == 'tidy':
            temp_msg = msg = _('Tidying...')

        else:
            temp_msg = msg = _('FFmpeg processing...')

        # Add replacement widgets
        self.check_media_button = Gtk.Button()
        self.button_box.pack_start(self.check_media_button, True, True, 0)
        self.check_media_button.set_action_name('app.check_all_button')
        self.check_media_button.set_sensitive(False)
        self.check_media_button.set_label(msg)

        # (Put the progress bar inside a box, so it doesn't touch the divider,
        #   because that doesn't look nice)
        self.progress_box = Gtk.HBox()
        self.button_box.pack_start(self.progress_box, True, True, 0)

        self.progress_bar = Gtk.ProgressBar()
        self.progress_box.pack_start(
            self.progress_bar,
            True,
            True,
            (self.spacing_size * 2),
        )
        self.progress_bar.set_fraction(0)
        self.progress_bar.set_show_text(True)
        self.progress_bar.set_text(temp_msg)

        # (The 'Custom download all' buttons, if they were visible, are
        #   replaced by empty buttons)
        if self.app_obj.show_custom_dl_button_flag:

            self.custom_dl_media_button = Gtk.Button()
            self.button_box.pack_start(
                self.custom_dl_media_button,
                True,
                True,
                0
            )
            self.custom_dl_media_button.set_label(temp_msg)
            self.custom_dl_media_button.set_sensitive(False)

        # Make the changes visible
        self.button_box.show_all()


    def switch_profile(self,  profile_name):

        """Called from a callback in self.on_switch_profile_menu_select() and
        mainapp.TartubeApp.load_db().

        Switches to the specified profile.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            profile_name (str): The specified profile (a key in
                mainapp.TartubeApp.profile_dict).

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 5574 switch_profile')

        if not profile_name in self.app_obj.profile_dict:

            return self.app_obj.system_error(
                205,
                'Unrecognised profile \'{0}\''.format(profile_name),
            )

        this_dict = self.app_obj.profile_dict[profile_name]

        # Add or remove markers from everything in the Video Index
        for dbid in self.app_obj.container_reg_dict.keys():

            if dbid in this_dict:
                self.video_index_set_marker(dbid)
            else:
                self.video_index_reset_marker(dbid)

        self.app_obj.set_last_profile(profile_name)


    def toggle_alt_limits_image(self, on_flag):

        """Can be called by anything.

        Toggles the icon in the Progress tab.

        Args:

            on_flag (bool): True for a normal image (signifying that
                alternative performance limits currently apply), False for a
                greyed-out image

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 5611 toggle_alt_limits_image')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Main window\'s Progress tab'
        )

        if on_flag:

            self.alt_limits_image.set_from_pixbuf(
                self.pixbuf_dict['limits_on_large'],
            )

            self.alt_limits_frame.set_tooltip_text(
                _('Alternative limits currently apply'),
            )

        else:

            self.alt_limits_image.set_from_pixbuf(
                self.pixbuf_dict['limits_off_large'],
            )

            self.alt_limits_frame.set_tooltip_text(
                _('Alternative limits do not currently apply'),
            )


    def toggle_visibility(self):

        """Called by self.on_delete_event, StatusIcon.on_button_press_event and
        mainapp.TartubeApp.on_menu_close_tray().

        Toggles the main window's visibility (usually after the user has left-
        clicked the status icon in the system tray).
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 5648 toggle_visibility')

        if self.is_visible():

            # Record the window's position, so its position can be restored
            #   when the window is made visible again
            posn = self.get_position()
            self.win_last_xpos = posn.root_x
            self.win_last_ypos = posn.root_y
            # Close the window to the tray
            self.set_visible(False)

        else:

            self.set_visible(True)
            if self.app_obj.restore_posn_from_tray_flag \
            and self.win_last_xpos is not None:
                self.move(self.win_last_xpos, self.win_last_ypos)


    def update_catalogue_filter_widgets(self):

        """Called by mainapp.TartubeApp.start() and .on_button_show_filter().

        The toolbar just below the Video Catalogue consists of three rows. Only
        the first is visible by default. Show or hide the remaining rows, as
        required.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 5678 update_catalogue_filter_widgets')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Main window\'s Videos tab'
        )

        if not self.app_obj.catalogue_show_filter_flag:

            # Hide the second/third rows
            if not self.app_obj.show_custom_icons_flag:
                self.catalogue_show_filter_button.set_stock_id(
                    Gtk.STOCK_ADD,
                )
            else:
                self.catalogue_show_filter_button.set_icon_widget(
                    Gtk.Image.new_from_pixbuf(
                        self.pixbuf_dict['stock_show_filter']
                    ),
                )

            self.catalogue_show_filter_button.set_tooltip_text(
                _('Show more settings'),
            )

            if self.catalogue_toolbar2 \
            in self.catalogue_toolbar_vbox.get_children():
                self.catalogue_toolbar_vbox.remove(self.catalogue_toolbar2)
                self.catalogue_toolbar_vbox.remove(self.catalogue_toolbar3)
                self.catalogue_toolbar_vbox.remove(self.catalogue_toolbar4)
                self.catalogue_toolbar_vbox.show_all()

            # If nothing has been selected in the Video Index, then we can
            #   hide rows, but not reveal them again
            if self.video_index_current_dbid is None:
                self.catalogue_show_filter_button.set_sensitive(False)

        else:

            # Show the second/third rows
            if not self.app_obj.show_custom_icons_flag:
                self.catalogue_show_filter_button.set_stock_id(
                    Gtk.STOCK_CLOSE,
                )
            else:
                self.catalogue_show_filter_button.set_icon_widget(
                    Gtk.Image.new_from_pixbuf(
                        self.pixbuf_dict['stock_hide_filter']
                    ),
                )

            self.catalogue_show_filter_button.set_tooltip_text(
                _('Show fewer settings'),
            )

            if not self.catalogue_toolbar2 \
            in self.catalogue_toolbar_vbox.get_children():

                self.catalogue_toolbar_vbox.pack_start(
                    self.catalogue_toolbar2,
                    False,
                    False,
                    0,
                )

                self.catalogue_toolbar_vbox.pack_start(
                    self.catalogue_toolbar3,
                    False,
                    False,
                    0,
                )

                self.catalogue_toolbar_vbox.pack_start(
                    self.catalogue_toolbar4,
                    False,
                    False,
                    0,
                )

                self.catalogue_toolbar_vbox.show_all()

                # After the parent self.catalogue_toolbar2 is added to its
                #   VBox, the 'Regex' button is not desensitised correctly
                #   (for reasons unknown)
                # Desensitise it, if it should be desensitised
                if self.video_index_current_dbid is None \
                or not self.video_catalogue_dict:
                    self.catalogue_regex_togglebutton.set_sensitive(False)


    def update_catalogue_sort_widgets(self):

        """Called by mainapp.TartubeApp.start().

        Videos in the Video Catalogue can be sorted in various ways. When
        required, set the combobox to its correct state.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 5776 update_catalogue_sort_widgets')

        mode = self.app_obj.catalogue_sort_mode
        if mode == 'default':
            self.catalogue_sort_combo.set_active(0)
        elif mode == 'alpha':
            self.catalogue_sort_combo.set_active(1)
        elif mode == 'receive':
            self.catalogue_sort_combo.set_active(2)
        else:
            self.catalogue_sort_combo.set_active(3)


    def update_catalogue_reverse_sort_widgets(self):

        """Called by mainapp.TartubeApp.start() and
        .on_button_reverse_sort_catalogue()

        Videos in the Video Catalogue can be sorted in various ways. When
        required, set the reverse sort button to its correct state.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 5799 update_catalogue_reverse_sort_widgets')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Main window\'s Videos tab'
        )

        if not self.app_obj.catalogue_reverse_sort_flag:

            if not self.app_obj.show_custom_icons_flag:
                self.catalogue_reverse_toolbutton.set_stock_id(
                    Gtk.STOCK_SORT_ASCENDING,
                )
            else:
                self.catalogue_reverse_toolbutton.set_icon_widget(
                    Gtk.Image.new_from_pixbuf(
                        self.pixbuf_dict['stock_sort_ascending']
                    ),
                )

            self.catalogue_reverse_toolbutton.set_tooltip_text(
                _('Reverse sort'),
            )

        else:

            if not self.app_obj.show_custom_icons_flag:
                self.catalogue_reverse_toolbutton.set_stock_id(
                    Gtk.STOCK_SORT_DESCENDING,
                )
            else:
                self.catalogue_reverse_toolbutton.set_icon_widget(
                    Gtk.Image.new_from_pixbuf(
                        self.pixbuf_dict['stock_sort_descending']
                    ),
                )

            self.catalogue_reverse_toolbutton.set_tooltip_text(
                _('Undo reverse sort'),
            )

        self.catalogue_reverse_toolbutton.show_all()


    def update_catalogue_thumb_widgets(self):

        """Called by mainapp.TartubeApp.start().

        When arranged on a grid, thumbnails in the Video Catalogue can be shown
        in a variety of different sizes. On startup, set the correct value of
        the combobox.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 5852 update_catalogue_thumb_widgets')

        # (IV is in groups of two, in the form [translation, actual value])
        self.catalogue_thumb_combo.set_active(
            int(
                self.app_obj.thumb_size_list.index(
                    self.app_obj.thumb_size_custom,
                ) / 2,
            ),
        )


    def update_classic_mode_tab_update_banner(self):

        """Called initially by self.setup_classic_mode_tab(), and then by
        several callbacks.

        Updates the layout of the banner at the top of the Classic Mode tab,
        according to current settings.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 5874 update_classic_mode_tab_update_banner')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Main window\'s Classic Mode tab'
        )

        if self.app_obj.classic_format_selection is None \
        or self.app_obj.classic_format_convert_flag:

            self.classic_banner_img.set_from_pixbuf(
                self.pixbuf_dict['ytdl_gui'],
            )
            self.classic_banner_label.set_markup(
                '<b>' + _(
                    'This tab emulates the classic youtube-dl-gui interface',
                ) + '</b>',
            )
            self.classic_banner_label2.set_markup(
                '<b>' + _(
                    'Videos downloaded here are not added to Tartube\'s' \
                    + ' database',
                ) + '</b>',
            )

        else:

            self.classic_banner_img.set_from_pixbuf(
                self.pixbuf_dict['warning_large'],
            )
            self.classic_banner_label.set_markup(
                '<b>' + _(
                    'If your preferred formats are not available, the' \
                    + ' download will fail!',
                ) + '</b>',
            )
            self.classic_banner_label2.set_markup(
                '<b>' + _(
                    'If you want a specific format, install FFMpeg and' \
                    + ' select \'Convert to this format\'!',
                ) + '</b>',
            )


    def update_menu(self):

        """Can be called by anything.

        Updates several main menu items after a change in conditions.

        Note that other code modifies the state of the main menu and toolbar;
        for example, see the code in mainapp.TartubeApp.load_db().
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 5928 update_menu')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Main window menu'
        )

        if self.update_ytdl_menu_item is not None:

            downloader = self.app_obj.get_downloader()

            self.update_ytdl_menu_item.set_label(
                ('U_pdate') + ' ' + downloader,
            )

            self.test_ytdl_menu_item.set_label(
                _('_Test') + ' ' + downloader,
            )

        if self.custom_dl_all_menu_item is not None:

            self.custom_dl_all_menu_item.set_submenu(
                self.custom_dl_popup_submenu(),
            )

        if self.switch_profile_menu_item is not None:

            self.switch_profile_menu_item.set_submenu(
                self.switch_profile_popup_submenu(),
            )

        if self.delete_profile_menu_item is not None:

            self.delete_profile_menu_item.set_submenu(
                self.delete_profile_popup_submenu(),
            )

        # Make the changes visible
        if self.menubar:
            self.menubar.show_all()


    def update_window_after_show_hide(self):

        """Called by mainapp.TartubeApp.on_menu_hide_system().

        Shows or hides system folders, as required.

        Updates the appearance of the main window's toolbutton, depending on
        the current setting of mainapp.TartubeApp.toolbar_system_hide_flag.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 5980 update_window_after_show_hide')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Main window toolbar'
        )

        # Update the appearance of the toolbar button
        if not self.app_obj.toolbar_system_hide_flag:

            self.hide_system_toolbutton.set_label(_('Hide'))
            self.hide_system_toolbutton.set_tooltip_text(
                _('Hide (most) system folders'),
            )

        else:

            self.hide_system_toolbutton.set_label(_('Show'))
            self.hide_system_toolbutton.set_tooltip_text(
                _('Show all system folders'),
            )

        # After system folders are revealed/hidden, Gtk helpfully selects a
        #   new channel/playlist/folder in the Video Index for us
        # Not sure how to stop it, other than by temporarily preventing
        #   selections altogether
        selection = self.video_index_treeview.get_selection()
        selection.set_mode(Gtk.SelectionMode.NONE)

        # Show/hide system folders
        for dbid in self.app_obj.container_reg_dict:

            media_data_obj = self.app_obj.media_reg_dict[dbid]

            if isinstance(media_data_obj, media.Folder) \
            and media_data_obj.priv_flag \
            and media_data_obj != self.app_obj.fixed_all_folder:
                self.app_obj.mark_folder_hidden(
                    media_data_obj,
                    self.app_obj.toolbar_system_hide_flag,
                )

        # Re-enable selections, and select the previously-selected channel/
        #   playlist/folder (if any)
        selection = self.video_index_treeview.get_selection()
        selection.set_mode(Gtk.SelectionMode.SINGLE)

        if self.video_index_current_dbid is not None:

            media_data_obj \
            = self.app_obj.media_reg_dict[self.video_index_current_dbid]
            self.video_index_select_row(media_data_obj)


    def update_progress_bar(self, text, count, total):

        """Called by downloads.DownloadManager.run(),
        refresh.RefreshManager.refresh_from_default_destination(),
        .refresh_from_actual_destination(), tidy.TidyManager.tidy_directory()
        and process.Processmanager.process_video().

        During a download/refresh/tidy/process operation, updates the progress
        bar just below the Video Index.

        Args:

            text (str): The text of the progress bar's label, matching the name
                of the media data object which has just been passed to
                youtube-dl

            count (int): The number of media data objects passed to youtube-dl
                so far. Note that a channel or a playlist counts as one media
                data object, as far as youtube-dl is concerned

            total (int): The total number of media data objects to be passed
                to youtube-dl

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 6059 update_progress_bar')

        if not self.progress_bar:
            return self.app_obj.system_error(
                206,
                'Videos tab progress bar is missing and cannot be updated',
            )

        # (The 0.5 guarantees that the progress bar is never empty. If
        #   downloading a single video, the progress bar is half full. If
        #   downloading the first out of 3 videos, it is 16% full, and so on)
        self.progress_bar.set_fraction(float(count - 0.5) / total)
        self.progress_bar.set_text(
            utils.shorten_string(text, self.short_string_max_len) \
            + ' ' + str(count) + '/' + str(total)
        )


    def update_free_space_msg(self, disk_space=None):

        """Called by mainapp.TartubeApp.dl_timer_callback() during a download
        operation to update the amount of free disk space visible in the
        Videos tab.

        Args:

            disk_space (float or None): The amount of free disk space on the
                drive containing Tartube's data directory. If None, the
                button's label is reset to its default state

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 6092 update_free_space_msg')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Main window\'s Videos tab'
        )

        if self.check_media_button is None:
            return

        elif disk_space is None:

            if not self.video_index_marker_dict:
                self.check_media_button.set_label(_('Check all'))
                self.check_media_button.set_tooltip_text(
                    _('Check all videos, channels, playlists and folders'),
                )
            else:
                self.check_media_button.set_label(_('Check marked items'))
                self.check_media_button.set_tooltip_text(
                    _('Check marked videos, channels, playlists and folders'),
                )

        else:

            msg = ' [' + str(round(disk_space, 1)) + ' GiB]'

            if self.app_obj.download_manager_obj \
            and self.app_obj.show_free_space_flag:

                operation_type \
                = self.app_obj.download_manager_obj.operation_type

                if operation_type == 'sim' \
                or operation_type == 'custom_sim' \
                or operation_type == 'classic_sim':
                    self.check_media_button.set_label(_('Checking') + msg)
                else:
                    self.check_media_button.set_label(_('Downloading') + msg)
                self.check_media_button.set_tooltip_text()


    # (Auto-sort functions for main window widgets)


    def video_index_auto_sort(self, treestore, row_iter1, row_iter2, data):

        """Sorting function created by self.video_index_reset().

        Automatically sorts rows in the Video Index, two at a time.

        Args:

            treestore (Gtk.TreeStore): Rows in the Video Index are stored in
                this treestore.

            row_iter1, row_iter2 (Gtk.TreeIter): Iters pointing at two rows
                in the treestore, one of which must be sorted before the other

            data (None): Ignored

        Return values:

            -1 if row_iter1 comes before row_iter2, 1 if row_iter2 comes before
                row_iter1, 0 if their order should not be changed

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 6160 video_index_auto_sort')

        # If auto-sorting is disabled temporarily, we can prevent the list
        #   being sorted by returning -1 for all cases
        if self.video_index_no_sort_flag:
            return -1

        # Get the media data objects on both rows
        obj1 = self.app_obj.media_reg_dict[treestore.get_value(row_iter1, 0)]
        obj2 = self.app_obj.media_reg_dict[treestore.get_value(row_iter2, 0)]

        # Perform the sort
        # Treat media.Channel and media.Playlist objects as the same type of
        #   thing, so that all folders appear first (sorted alphabetically),
        #   followed by all channels/playlists (sorted alphabetically)
        if str(obj1.__class__) == str(obj2.__class__) \
        or (
            isinstance(obj1, media.GenericRemoteContainer) \
            and isinstance(obj2, media.GenericRemoteContainer)
        ):
            # Private folders are shown first, then (public) fixed folders,
            #   then user-created folders
            if isinstance(obj1, media.Folder):
                if obj1.priv_flag and not obj2.priv_flag:
                    return -1
                elif not obj1.priv_flag and obj2.priv_flag:
                    return 1
                elif obj1.fixed_flag and not obj2.fixed_flag:
                    return -1
                elif not obj1.fixed_flag and obj2.fixed_flag:
                    return 1

            # Media data objects can't have the same name, but they might have
            #   the same nickname
            # If two nicknames both start with an index, e.g. '1 Music' and
            #   '11 Comedy' then make sure the one with the lowest index comes
            #   first
            index1_list = re.findall(r'^(\d+)', obj1.nickname)
            index2_list = re.findall(r'^(\d+)', obj2.nickname)
            if index1_list and index2_list:
                if int(index1_list[0]) < int(index2_list[0]):
                    return -1
                else:
                    return 1
            elif obj1.nickname.lower() < obj2.nickname.lower():
                return -1
            else:
                return 1

        else:

            # (Folders displayed first, channels/playlists next, and of course
            #   videos aren't displayed here at all)
            if isinstance(obj1, media.Folder):
                return -1
            elif isinstance(obj2, media.Folder):
                return 1
            else:
                return 0


    def video_catalogue_generic_auto_sort(self, row1, row2, data, notify):

        """Sorting function created by self.video_catalogue_reset(), when
        videos are displayed in a Gtk.ListBox.

        Automatically sorts rows in the Video Catalogue, by upload time
        (default) or alphabetically, depending on settings.

        This is a wrapper function, so that self.video_catalogue_compare() can
        be called, regardless of whether the Video Catalogue is using a listbox
        or a grid.

        Args:

            row1, row2 (mainwin.CatalogueRow): Two rows in the Gtk.ListBox's
                model, one of which must be sorted before the other

            data (None): Ignored

            notify (False): Ignored

        Return values:

            -1 if row1 comes before row2, 1 if row2 comes before row1 (the code
                does not return 0)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 6250 video_catalogue_generic_auto_sort')

        return self.app_obj.video_compare(row1.video_obj, row2.video_obj)


    def video_catalogue_grid_auto_sort(self, gridbox1, gridbox2):

        """Sorting function created by self.video_catalogue_reset(), when
        videos are displayed on a Gtk.Grid.

        Automatically sorts gridboxes in the Video Catalogue, by upload time
        (default) or alphabetically, depending on settings.

        This is a wrapper function, so that self.video_catalogue_compare() can
        be called, regardless of whether the Video Catalogue is using a listbox
        or a grid.

        Args:

            gridbox1, gridbox2 (mainwin.CatalogueGridBox): Two gridboxes, one
                of which must be sorted before the other

        Return values:

            -1 if gridbox1 comes before gridbox2, 1 if gridbox2 comes before
                gridbox1 (the code does not return 0)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 6280 video_catalogue_grid_auto_sort')

        return self.app_obj.video_compare(
            gridbox1.video_obj,
            gridbox2.video_obj,
        )


    # (Popup menu functions for main window widgets)


    def video_index_popup_menu(self, event, dbid):

        """Called by self.on_video_index_right_click().

        When the user right-clicks on the Video Index, shows a
        context-sensitive popup menu.

        Args:

            event (Gdk.EventButton): The mouse click event

            dbid (int): The .dbid of the clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 6307 video_index_popup_menu')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Video Index popup menu starts here. In' \
            + ' the Videos tab, right-click any channel/playlist/folder'
        )

        # Find the right-clicked media data object (and a string to describe
        #   its type)
        media_data_obj = self.app_obj.media_reg_dict[dbid]
        media_type = media_data_obj.get_type()
        # (If an external directory is set, but is not available, many items
        #   must be desensitised)
        if media_data_obj.dbid in self.app_obj.container_unavailable_dict:
            unavailable_flag = True
        else:
            unavailable_flag = False

        # Set up the popup menu
        popup_menu = Gtk.Menu()

        # Check/download/refresh items
        if media_type == 'channel':
            msg = _('_Check channel')
        elif media_type == 'playlist':
            msg = _('_Check playlist')
        else:
            msg = _('_Check folder')

        check_menu_item = Gtk.MenuItem.new_with_mnemonic(msg)
        check_menu_item.connect(
            'activate',
            self.on_video_index_check,
            media_data_obj,
        )
        if (
            self.app_obj.current_manager_obj \
            and not self.app_obj.download_manager_obj
        ) or (
            self.app_obj.download_manager_obj \
            and self.app_obj.download_manager_obj.operation_classic_flag
        ) or (
            isinstance(media_data_obj, media.Folder) \
            and media_data_obj.priv_flag
        ) or (
            not isinstance(media_data_obj, media.Folder) \
            and media_data_obj.source is None
        ) or unavailable_flag \
        or media_data_obj.dl_no_db_flag:
            check_menu_item.set_sensitive(False)
        popup_menu.append(check_menu_item)

        if media_type == 'channel':
            msg = _('_Download channel')
        elif media_type == 'playlist':
            msg = _('_Download playlist')
        else:
            msg = _('_Download folder')

        download_menu_item = Gtk.MenuItem.new_with_mnemonic(msg)
        download_menu_item.connect(
            'activate',
            self.on_video_index_download,
            media_data_obj,
        )
        if __main__.__pkg_no_download_flag__ \
        or (
            self.app_obj.current_manager_obj \
            and not self.app_obj.download_manager_obj
        ) or (
            self.app_obj.download_manager_obj \
            and self.app_obj.download_manager_obj.operation_classic_flag
        ) or (
            isinstance(media_data_obj, media.Folder) \
            and media_data_obj.priv_flag
        ) or (
            not isinstance(media_data_obj, media.Folder) \
            and media_data_obj.source is None
        ) or unavailable_flag:
            download_menu_item.set_sensitive(False)
        popup_menu.append(download_menu_item)

        if media_type == 'channel':
            msg = _('C_ustom download channel')
        elif media_type == 'playlist':
            msg = _('C_ustom download playlist')
        else:
            msg = _('C_ustom download folder')

        custom_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(msg)
        if not self.app_obj.check_custom_download_managers():
            custom_dl_menu_item.connect(
                'activate',
                self.on_video_index_custom_dl,
                media_data_obj,
            )

        else:
            custom_dl_submenu = self.custom_dl_popup_submenu([media_data_obj])
            custom_dl_menu_item.set_submenu(
                self.custom_dl_popup_submenu([media_data_obj]),
            )

        if __main__.__pkg_no_download_flag__ \
        or self.app_obj.current_manager_obj \
        or (
            isinstance(media_data_obj, media.Folder) \
            and media_data_obj.priv_flag
        ) or (
            not isinstance(media_data_obj, media.Folder) \
            and media_data_obj.source is None
        ) or unavailable_flag:
            custom_dl_menu_item.set_sensitive(False)
        popup_menu.append(custom_dl_menu_item)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Contents
        contents_submenu = Gtk.Menu()

        if not isinstance(media_data_obj, media.Folder):

            self.video_index_setup_contents_submenu(
                contents_submenu,
                media_data_obj,
                False,
            )

        else:

            # All contents
            all_contents_submenu = Gtk.Menu()

            self.video_index_setup_contents_submenu(
                all_contents_submenu,
                media_data_obj,
                False,
            )

            # Separator
            all_contents_submenu.append(Gtk.SeparatorMenuItem())

            empty_folder_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('_Empty folder'),
            )
            empty_folder_menu_item.connect(
                'activate',
                self.on_video_index_empty_folder,
                media_data_obj,
            )
            all_contents_submenu.append(empty_folder_menu_item)
            if not media_data_obj.child_list or media_data_obj.priv_flag:
                empty_folder_menu_item.set_sensitive(False)

            all_contents_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('_All contents'),
            )
            all_contents_menu_item.set_submenu(all_contents_submenu)
            contents_submenu.append(all_contents_menu_item)

            # Just folder videos
            just_videos_submenu = Gtk.Menu()

            self.video_index_setup_contents_submenu(
                just_videos_submenu,
                media_data_obj,
                True,
            )

            # Separator
            just_videos_submenu.append(Gtk.SeparatorMenuItem())

            empty_videos_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('_Remove videos'),
            )
            empty_videos_menu_item.connect(
                'activate',
                self.on_video_index_remove_videos,
                media_data_obj,
            )
            just_videos_submenu.append(empty_videos_menu_item)
            if not media_data_obj.child_list or media_data_obj.priv_flag:
                empty_videos_menu_item.set_sensitive(False)

            just_videos_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('_Just folder videos'),
            )
            just_videos_menu_item.set_submenu(just_videos_submenu)
            contents_submenu.append(just_videos_menu_item)

        if media_type == 'channel':
            string = _('Channel c_ontents')
        elif media_type == 'playlist':
            string = _('Playlist c_ontents')
        else:
            string = _('Folder c_ontents')

        contents_menu_item = Gtk.MenuItem.new_with_mnemonic(string)
        contents_menu_item.set_submenu(contents_submenu)
        popup_menu.append(contents_menu_item)
        if not media_data_obj.child_list:
            contents_menu_item.set_sensitive(False)

        # Actions
        actions_submenu = Gtk.Menu()

        move_top_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Move to top level'),
        )
        move_top_menu_item.connect(
            'activate',
            self.on_video_index_move_to_top,
            media_data_obj,
        )
        actions_submenu.append(move_top_menu_item)
        if not media_data_obj.parent_obj \
        or self.app_obj.current_manager_obj \
        or unavailable_flag:
            move_top_menu_item.set_sensitive(False)

        # Separator
        actions_submenu.append(Gtk.SeparatorMenuItem())

        if isinstance(media_data_obj, media.Folder):

            hide_folder_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('_Hide folder'),
            )
            hide_folder_menu_item.connect(
                'activate',
                self.on_video_index_hide_folder,
                media_data_obj,
            )
            actions_submenu.append(hide_folder_menu_item)

        if media_type == 'channel':
            msg = _('_Rename channel...')
        elif media_type == 'playlist':
            msg = _('_Rename playlist...')
        else:
            msg = _('_Rename folder...')

        rename_location_menu_item = Gtk.MenuItem.new_with_mnemonic(msg)
        rename_location_menu_item.connect(
            'activate',
            self.on_video_index_rename_location,
            media_data_obj,
        )
        actions_submenu.append(rename_location_menu_item)
        if self.app_obj.current_manager_obj or self.config_win_list \
        or (
            isinstance(media_data_obj, media.Folder) \
            and media_data_obj.fixed_flag
        ) or unavailable_flag:
            rename_location_menu_item.set_sensitive(False)

        set_nickname_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Set _nickname...'),
        )
        set_nickname_menu_item.connect(
            'activate',
            self.on_video_index_set_nickname,
            media_data_obj,
        )
        actions_submenu.append(set_nickname_menu_item)
        if isinstance(media_data_obj, media.Folder) \
        and media_data_obj.priv_flag:
            set_nickname_menu_item.set_sensitive(False)

        if not isinstance(media_data_obj, media.Folder):

            set_url_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('Set _URL...'),
            )
            set_url_menu_item.connect(
                'activate',
                self.on_video_index_set_url,
                media_data_obj,
            )
            actions_submenu.append(set_url_menu_item)
            if self.app_obj.current_manager_obj:
                set_url_menu_item.set_sensitive(False)

        # Separator
        actions_submenu.append(Gtk.SeparatorMenuItem())

        if media_type == 'channel':

            insert_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('_Insert videos...'),
            )
            insert_menu_item.connect(
                'activate',
                self.on_video_index_insert_videos,
                media_data_obj,
            )
            actions_submenu.append(insert_menu_item)
            if self.app_obj.current_manager_obj:
                insert_menu_item.set_sensitive(False)

            # Separator
            actions_submenu.append(Gtk.SeparatorMenuItem())

        if media_type == 'channel':
            msg = _('_Export channel...')
        elif media_type == 'playlist':
            msg = _('_Export playlist...')
        else:
            msg = _('_Export folder...')

        export_menu_item = Gtk.MenuItem.new_with_mnemonic(msg)
        export_menu_item.connect(
            'activate',
            self.on_video_index_export,
            media_data_obj,
        )
        actions_submenu.append(export_menu_item)
        if self.app_obj.current_manager_obj:
            export_menu_item.set_sensitive(False)

        if media_type == 'channel':
            msg = _('Re_fresh channel')
        elif media_type == 'playlist':
            msg = _('Re_fresh playlist')
        else:
            msg = _('Re_fresh folder')

        refresh_menu_item = Gtk.MenuItem.new_with_mnemonic(msg)
        refresh_menu_item.connect(
            'activate',
            self.on_video_index_refresh,
            media_data_obj,
        )
        if self.app_obj.current_manager_obj \
        or (
            isinstance(media_data_obj, media.Folder) \
            and media_data_obj.priv_flag
        ):
            refresh_menu_item.set_sensitive(False)
        actions_submenu.append(refresh_menu_item)

        if media_type == 'channel':
            msg = _('_Tidy up channel')
        elif media_type == 'playlist':
            msg = _('_Tidy up playlist')
        else:
            msg = _('_Tidy up folder')

        tidy_menu_item = Gtk.MenuItem.new_with_mnemonic(msg)
        tidy_menu_item.connect(
            'activate',
            self.on_video_index_tidy,
            media_data_obj,
        )
        if self.app_obj.current_manager_obj \
        or (
            isinstance(media_data_obj, media.Folder) \
            and media_data_obj.priv_flag
        ):
            tidy_menu_item.set_sensitive(False)
        actions_submenu.append(tidy_menu_item)

        # Separator
        actions_submenu.append(Gtk.SeparatorMenuItem())

        convert_text = None
        if media_type == 'channel':
            msg = _('_Convert to playlist')
        elif media_type == 'playlist':
            msg = _('_Convert to channel')
        else:
            msg = None

        if msg:

            convert_menu_item = Gtk.MenuItem.new_with_mnemonic(msg)
            convert_menu_item.connect(
                'activate',
                self.on_video_index_convert_container,
                media_data_obj,
            )
            actions_submenu.append(convert_menu_item)
            if self.app_obj.current_manager_obj \
            or unavailable_flag:
                convert_menu_item.set_sensitive(False)

        classic_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Add to C_lassic Mode tab'),
        )
        classic_dl_menu_item.connect(
            'activate',
            self.on_video_index_add_classic,
            media_data_obj,
        )
        actions_submenu.append(classic_dl_menu_item)
        if __main__.__pkg_no_download_flag__ \
        or isinstance(media_data_obj, media.Folder) \
        or not media_data_obj.source:
            classic_dl_menu_item.set_sensitive(False)

        if media_type == 'channel':
            msg = _('Channel _actions')
        elif media_type == 'playlist':
            msg = _('Playlist _actions')
        else:
            msg = _('Folder _actions')

        actions_menu_item = Gtk.MenuItem.new_with_mnemonic(msg)
        actions_menu_item.set_submenu(actions_submenu)
        popup_menu.append(actions_menu_item)

        # Apply/remove/edit download options, disable downloads
        downloads_submenu = Gtk.Menu()

        # (Desensitise these menu items, if an edit window is already open)
        no_options_flag = False
        for win_obj in self.config_win_list:
            if isinstance(win_obj, config.OptionsEditWin) \
            and media_data_obj.options_obj == win_obj.edit_obj:
                no_options_flag = True
                break

        if not media_data_obj.options_obj:

            apply_options_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('_Apply download options...'),
            )
            apply_options_menu_item.connect(
                'activate',
                self.on_video_index_apply_options,
                media_data_obj,
            )
            downloads_submenu.append(apply_options_menu_item)
            if no_options_flag or self.app_obj.current_manager_obj \
            or (
                isinstance(media_data_obj, media.Folder)
                and media_data_obj.priv_flag
            ):
                apply_options_menu_item.set_sensitive(False)

        else:

            remove_options_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('_Remove download options'),
            )
            remove_options_menu_item.connect(
                'activate',
                self.on_video_index_remove_options,
                media_data_obj,
            )
            downloads_submenu.append(remove_options_menu_item)
            if no_options_flag or self.app_obj.current_manager_obj \
            or (
                isinstance(media_data_obj, media.Folder)
                and media_data_obj.priv_flag
            ):
                remove_options_menu_item.set_sensitive(False)

        edit_options_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Edit download options...'),
        )
        edit_options_menu_item.connect(
            'activate',
            self.on_video_index_edit_options,
            media_data_obj,
        )
        downloads_submenu.append(edit_options_menu_item)
        if no_options_flag or self.app_obj.current_manager_obj \
        or not media_data_obj.options_obj:
            edit_options_menu_item.set_sensitive(False)

        # Separator
        downloads_submenu.append(Gtk.SeparatorMenuItem())

        add_scheduled_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Add to _scheduled download...'),
        )
        add_scheduled_menu_item.connect(
            'activate',
            self.on_video_index_add_to_scheduled,
            media_data_obj,
        )
        downloads_submenu.append(add_scheduled_menu_item)

        set_destination_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Set _download destination...'),
        )
        set_destination_menu_item.connect(
            'activate',
            self.on_video_index_set_destination,
            media_data_obj,
        )
        downloads_submenu.append(set_destination_menu_item)
        if (
            isinstance(media_data_obj, media.Folder) \
            and media_data_obj.fixed_flag
        ):
            set_destination_menu_item.set_sensitive(False)

        show_system_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Show system _command...'),
        )
        show_system_menu_item.connect(
            'activate',
            self.on_video_index_show_system_cmd,
            media_data_obj,
        )
        downloads_submenu.append(show_system_menu_item)
        if isinstance(media_data_obj, media.Folder) \
        or not media_data_obj.source:
            show_system_menu_item.set_sensitive(False)

        # Separator
        downloads_submenu.append(Gtk.SeparatorMenuItem())

        # Only for the "Recent Videos" folder
        if media_data_obj == self.app_obj.fixed_recent_folder:

            recent_videos_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('Set _removal time...'),
            )
            recent_videos_menu_item.connect(
                'activate',
                self.on_video_index_recent_videos_time,
                media_data_obj,
            )
            downloads_submenu.append(recent_videos_menu_item)

            # Separator
            downloads_submenu.append(Gtk.SeparatorMenuItem())

        marker_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
            _('_Mark for checking/downloading'),
        )
        marker_menu_item.set_active(
            media_data_obj.dbid in self.video_index_marker_dict,
        )
        marker_menu_item.connect(
            'activate',
            self.on_video_index_marker,
            media_data_obj,
        )
        downloads_submenu.append(marker_menu_item)
        if (
            isinstance(media_data_obj, media.Folder)
            and media_data_obj.priv_flag
        ) or media_data_obj.dl_disable_flag:
            marker_menu_item.set_sensitive(False)

        no_db_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
            _('_Don\'t add videos to Tartube\'s database'),
        )
        no_db_menu_item.set_active(media_data_obj.dl_no_db_flag)
        no_db_menu_item.connect(
            'activate',
            self.on_video_index_dl_no_db,
            media_data_obj,
        )
        downloads_submenu.append(no_db_menu_item)
        # (Widget sensitivity set below)

        disable_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
            _('D_isable checking/downloading'),
        )
        disable_menu_item.set_active(media_data_obj.dl_disable_flag)
        disable_menu_item.connect(
            'activate',
            self.on_video_index_dl_disable,
            media_data_obj,
        )
        downloads_submenu.append(disable_menu_item)
        # (Widget sensitivity set below)

        enforce_check_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
            _('_Just disable downloading'),
        )
        enforce_check_menu_item.set_active(media_data_obj.dl_sim_flag)
        enforce_check_menu_item.connect(
            'activate',
            self.on_video_index_dl_sim,
            media_data_obj,
        )
        downloads_submenu.append(enforce_check_menu_item)
        # (Widget sensitivity set below)

        # (Widget sensitivity from above)
        if self.app_obj.current_manager_obj \
        or (
            isinstance(media_data_obj, media.Folder) \
            and media_data_obj.fixed_flag
        ):
            no_db_menu_item.set_sensitive(False)
            disable_menu_item.set_sensitive(False)
            enforce_check_menu_item.set_sensitive(False)

        downloads_menu_item = Gtk.MenuItem.new_with_mnemonic(_('Down_loads'))
        downloads_menu_item.set_submenu(downloads_submenu)
        popup_menu.append(downloads_menu_item)
        if __main__.__pkg_no_download_flag__ \
        or unavailable_flag:
            downloads_menu_item.set_sensitive(False)

        # Show
        show_submenu = Gtk.Menu()

        if media_type == 'channel':
            msg = _('Channel _properties...')
        elif media_type == 'playlist':
            msg = _('Playlist _properties...')
        else:
            msg = _('Folder _properties...')

        show_properties_menu_item = Gtk.MenuItem.new_with_mnemonic(msg)
        show_properties_menu_item.connect(
            'activate',
            self.on_video_index_show_properties,
            media_data_obj,
        )
        show_submenu.append(show_properties_menu_item)
        if self.app_obj.current_manager_obj:
            show_properties_menu_item.set_sensitive(False)

        # Separator
        show_submenu.append(Gtk.SeparatorMenuItem())

        show_location_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Default location'),
        )
        show_location_menu_item.connect(
            'activate',
            self.on_video_index_show_location,
            media_data_obj,
        )
        show_submenu.append(show_location_menu_item)
        if isinstance(media_data_obj, media.Folder) \
        and media_data_obj.priv_flag:
            show_location_menu_item.set_sensitive(False)

        show_destination_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Actual location'),
        )
        show_destination_menu_item.connect(
            'activate',
            self.on_video_index_show_destination,
            media_data_obj,
        )
        show_submenu.append(show_destination_menu_item)
        if (
            isinstance(media_data_obj, media.Folder) \
            and media_data_obj.priv_flag
        ) or unavailable_flag:
            show_destination_menu_item.set_sensitive(False)

        show_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Show'))
        show_menu_item.set_submenu(show_submenu)
        popup_menu.append(show_menu_item)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Delete items
        if media_type == 'channel':
            msg = _('D_elete channel')
        elif media_type == 'playlist':
            msg = _('D_elete playlist')
        else:
            msg = _('D_elete folder')

        delete_menu_item = Gtk.MenuItem.new_with_mnemonic(msg)
        delete_menu_item.connect(
            'activate',
            self.on_video_index_delete_container,
            media_data_obj,
        )
        if self.app_obj.current_manager_obj \
        or (media_type == 'folder' and media_data_obj.fixed_flag) \
        or self.config_win_list:
            delete_menu_item.set_sensitive(False)
        popup_menu.append(delete_menu_item)

        # Create the popup menu
        popup_menu.show_all()
        popup_menu.popup(None, None, None, None, event.button, event.time)


    def video_catalogue_popup_menu(self, event, video_obj):

        """Called by mainwin.SimpleCatalogueItem.on_right_click_row(),
        mainwin.ComplexCatalogueItem.on_right_click_row() or
        mainwin.GridCatalogueItem.on_click_box().

        When the user right-clicks on the Video Catalogue, shows a context-
        sensitive popup menu.

        Args:

            event (Gdk.EventButton): The mouse click event

            video_obj (media.Video): The video object displayed in the clicked
                row

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 7012 video_catalogue_popup_menu')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Video Catalogue popup menu starts here. In' \
            + ' the Videos tab, right-click any video'
        )

        # Use a different popup menu for multiple selected videos
        video_list = []
        if self.app_obj.catalogue_mode_type != 'grid':

            # Because of Gtk weirdness, check that the clicked row is actually
            #   one of those selected
            catalogue_item_obj = self.video_catalogue_dict[video_obj.dbid]
            row_list = self.catalogue_listbox.get_selected_rows()
            if catalogue_item_obj.catalogue_row in row_list \
            and len(row_list) > 1:

                # Convert row_list, a list of mainwin.CatalogueRow objects,
                #   into a list of media.Video objects
                video_list = []
                for row in row_list:
                    video_list.append(row.video_obj)

                return self.video_catalogue_multi_popup_menu(event, video_list)

            else:

                # Otherwise, right-clicking a row selects it (and unselects
                #   everything else)
                self.catalogue_listbox.unselect_all()
                self.catalogue_listbox.select_row(
                    catalogue_item_obj.catalogue_row,
                )

        else:

            # For our custom Gtk.Grid selection code, the same principle
            #   applies
            for catalogue_item_obj in self.video_catalogue_dict.values():
                if catalogue_item_obj.selected_flag:
                    video_list.append(catalogue_item_obj.video_obj)

            if video_obj in video_list and len(video_list) > 1:

                return self.video_catalogue_multi_popup_menu(event, video_list)

            else:

                self.video_catalogue_grid_select(
                    self.video_catalogue_dict[video_obj.dbid],
                    'default',  # Like a left-click, with no SHIFT/CTRL key
                )

        # (If the parent channel/playlist/folder has external directory is set,
        #   but which is not available, many items must be desensitised)
        if video_obj.parent_obj.dbid \
        in self.app_obj.container_unavailable_dict:
            unavailable_flag = True
        else:
            unavailable_flag = False

        # Set up the popup menu
        popup_menu = Gtk.Menu()

        # Check/download videos
        check_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Check video'),
        )
        check_menu_item.connect(
            'activate',
            self.on_video_catalogue_check,
            video_obj,
        )
        # (We can add another video to the downloads.DownloadList object, even
        #   after a download operation has started)
        if (
            self.app_obj.current_manager_obj \
            and not self.app_obj.download_manager_obj
        ) or (
            self.app_obj.download_manager_obj \
            and self.app_obj.download_manager_obj.operation_classic_flag
        ) or video_obj.source is None \
        or unavailable_flag \
        or video_obj.parent_obj.dl_no_db_flag:
            check_menu_item.set_sensitive(False)
        popup_menu.append(check_menu_item)

        if not video_obj.dl_flag:

            download_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('_Download video'),
            )
            download_menu_item.connect(
                'activate',
                self.on_video_catalogue_download,
                video_obj,
            )
            if __main__.__pkg_no_download_flag__ \
            or (
                self.app_obj.current_manager_obj \
                and not self.app_obj.download_manager_obj
            ) or (
                self.app_obj.download_manager_obj \
                and self.app_obj.download_manager_obj.operation_classic_flag
            ) or video_obj.source is None \
            or video_obj.live_mode == 1 \
            or unavailable_flag:
                download_menu_item.set_sensitive(False)
            popup_menu.append(download_menu_item)

        else:

            download_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('Re-_download this video')
            )
            download_menu_item.connect(
                'activate',
                self.on_video_catalogue_re_download,
                video_obj,
            )
            if __main__.__pkg_no_download_flag__ \
            or self.app_obj.current_manager_obj \
            or video_obj.source is None \
            or video_obj.live_mode == 1 \
            or unavailable_flag:
                download_menu_item.set_sensitive(False)
            popup_menu.append(download_menu_item)

        custom_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('C_ustom download video')
        )
        if not self.app_obj.check_custom_download_managers():
            custom_dl_menu_item.connect(
                'activate',
                self.on_video_index_custom_dl,
                video_obj,
            )

        else:
            custom_dl_menu_item.set_submenu(
                self.custom_dl_popup_submenu([ video_obj ]),
            )

        if __main__.__pkg_no_download_flag__ \
        or self.app_obj.current_manager_obj \
        or video_obj.source is None \
        or video_obj.live_mode != 0 \
        or unavailable_flag:
            custom_dl_menu_item.set_sensitive(False)
        popup_menu.append(custom_dl_menu_item)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Watch video in player/download and watch
        if not video_obj.dl_flag and not self.app_obj.current_manager_obj:

            dl_watch_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('Download and _watch'),
            )
            dl_watch_menu_item.connect(
                'activate',
                self.on_video_catalogue_dl_and_watch,
                video_obj,
            )
            popup_menu.append(dl_watch_menu_item)
            if __main__.__pkg_no_download_flag__ \
            or video_obj.source is None \
            or self.app_obj.update_manager_obj \
            or self.app_obj.refresh_manager_obj \
            or self.app_obj.process_manager_obj \
            or video_obj.live_mode != 0 \
            or unavailable_flag:
                dl_watch_menu_item.set_sensitive(False)

        else:

            watch_player_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('Watch in _player'),
            )
            watch_player_menu_item.connect(
                'activate',
                self.on_video_catalogue_watch_video,
                video_obj,
            )
            popup_menu.append(watch_player_menu_item)
            if video_obj.live_mode != 0:
                watch_player_menu_item.set_sensitive(False)

        # Watch video online. For YouTube URLs, offer alternative websites
        enhanced = utils.is_video_enhanced(video_obj)
        if video_obj.source is None or video_obj.live_mode != 0:

            if not enhanced:

                watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic(
                    _('_Watch on website'),
                )
                if video_obj.source is None:
                    watch_website_menu_item.set_sensitive(False)
                popup_menu.append(watch_website_menu_item)

            else:

                pretty = formats.ENHANCED_SITE_DICT[enhanced]['pretty_name']
                watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic(
                    _('_Watch on {0}').format(pretty),
                )
                if video_obj.source is None:
                    watch_website_menu_item.set_sensitive(False)
                popup_menu.append(watch_website_menu_item)

        else:

            if not enhanced:

                watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic(
                    _('_Watch on website'),
                )
                watch_website_menu_item.connect(
                    'activate',
                    self.on_video_catalogue_watch_website,
                    video_obj,
                )
                popup_menu.append(watch_website_menu_item)

            elif enhanced != 'youtube':

                pretty = formats.ENHANCED_SITE_DICT[enhanced]['pretty_name']
                watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic(
                    _('_Watch on {0}').format(pretty),
                )
                watch_website_menu_item.connect(
                    'activate',
                    self.on_video_catalogue_watch_website,
                    video_obj,
                )
                popup_menu.append(watch_website_menu_item)

            else:

                alt_submenu = Gtk.Menu()

                watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic(
                    _('_YouTube'),
                )
                watch_website_menu_item.connect(
                    'activate',
                    self.on_video_catalogue_watch_website,
                    video_obj,
                )
                alt_submenu.append(watch_website_menu_item)

                watch_hooktube_menu_item = Gtk.MenuItem.new_with_mnemonic(
                    _('_HookTube'),
                )
                watch_hooktube_menu_item.connect(
                    'activate',
                    self.on_video_catalogue_watch_hooktube,
                    video_obj,
                )
                alt_submenu.append(watch_hooktube_menu_item)

                watch_invidious_menu_item = Gtk.MenuItem.new_with_mnemonic(
                    _('_Invidious'),
                )
                watch_invidious_menu_item.connect(
                    'activate',
                    self.on_video_catalogue_watch_invidious,
                    video_obj,
                )
                alt_submenu.append(watch_invidious_menu_item)

                ignore_me = _(
                    'TRANSLATOR\'S NOTE: Watch on YouTube, Watch on' \
                    + ' HookTube, etc',
                )

                alt_menu_item = Gtk.MenuItem.new_with_mnemonic(
                    _('W_atch on'),
                )
                alt_menu_item.set_submenu(alt_submenu)
                popup_menu.append(alt_menu_item)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Special
        special_submenu = Gtk.Menu()

        clip_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Create video clips...'),
        )
        clip_menu_item.connect(
            'activate',
            self.on_video_catalogue_process_clip,
            video_obj,
        )
        special_submenu.append(clip_menu_item)
        if self.app_obj.current_manager_obj \
        or (video_obj.dl_flag and video_obj.file_name is None) \
        or video_obj.live_mode \
        or unavailable_flag:
            clip_menu_item.set_sensitive(False)

        slice_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Remove video slices...'),
        )
        slice_menu_item.connect(
            'activate',
            self.on_video_catalogue_process_slice,
            video_obj,
        )
        special_submenu.append(slice_menu_item)
        if self.app_obj.current_manager_obj \
        or (video_obj.dl_flag and video_obj.file_name is None) \
        or video_obj.live_mode \
        or unavailable_flag:
            slice_menu_item.set_sensitive(False)

        process_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Process with FFmpeg...'),
        )
        process_menu_item.connect(
            'activate',
            self.on_video_catalogue_process_ffmpeg,
            video_obj,
        )
        special_submenu.append(process_menu_item)
        if self.app_obj.current_manager_obj \
        or video_obj.file_name is None \
        or unavailable_flag:
            process_menu_item.set_sensitive(False)

        # Separator
        special_submenu.append(Gtk.SeparatorMenuItem())

        if not video_obj.dl_flag:
            output_override_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('Download with _name...'),
            )
        else:
            output_override_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('Re-download with _name...'),
            )
        output_override_menu_item.connect(
            'activate',
            self.on_video_catalogue_output_override,
            video_obj,
        )
        special_submenu.append(output_override_menu_item)
        if not download_menu_item.get_sensitive() \
        or (video_obj.dl_flag and self.app_obj.current_manager_obj):
            output_override_menu_item.set_sensitive(False)

        # Separator
        special_submenu.append(Gtk.SeparatorMenuItem())

        reload_metadata_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Reload _metadata'),
        )
        reload_metadata_menu_item.connect(
            'activate',
            self.on_video_catalogue_reload_metadata,
            video_obj,
        )
        special_submenu.append(reload_metadata_menu_item)
        if self.app_obj.current_manager_obj or self.config_win_list:
            reload_metadata_menu_item.set_sensitive(False)

        special_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Special'),
        )
        special_menu_item.set_submenu(special_submenu)
        popup_menu.append(special_menu_item)
        if self.app_obj.current_manager_obj \
        or unavailable_flag:
            special_menu_item.set_sensitive(False)

        # Add to Classic Mode tab
        classic_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Add to C_lassic Mode tab'),
        )
        classic_dl_menu_item.connect(
            'activate',
            self.on_video_catalogue_add_classic,
            video_obj,
        )
        popup_menu.append(classic_dl_menu_item)
        if __main__.__pkg_no_download_flag__ \
        or video_obj.source is None:
            classic_dl_menu_item.set_sensitive(False)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        if video_obj.live_mode != 0:

            # Livestream
            livestream_submenu = Gtk.Menu()

            auto_notify_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
                _('Auto _notify'),
            )
            if video_obj.dbid in self.app_obj.media_reg_auto_notify_dict:
                auto_notify_menu_item.set_active(True)
            auto_notify_menu_item.connect(
                'activate',
                self.on_video_catalogue_livestream_toggle,
                video_obj,
                'notify',
            )
            livestream_submenu.append(auto_notify_menu_item)
            # Currently disabled on MS Windows
            if os.name == 'nt':
                auto_notify_menu_item.set_sensitive(False)

            auto_alarm_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
                _('Auto _sound alarm'),
            )
            if video_obj.dbid in self.app_obj.media_reg_auto_alarm_dict:
                auto_alarm_menu_item.set_active(True)
            auto_alarm_menu_item.connect(
                'activate',
                self.on_video_catalogue_livestream_toggle,
                video_obj,
                'alarm',
            )
            livestream_submenu.append(auto_alarm_menu_item)
            if not mainapp.HAVE_PLAYSOUND_FLAG:
                auto_alarm_menu_item.set_sensitive(False)

            auto_open_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
                _('Auto _open'),
            )
            if video_obj.dbid in self.app_obj.media_reg_auto_open_dict:
                auto_open_menu_item.set_active(True)
            auto_open_menu_item.connect(
                'activate',
                self.on_video_catalogue_livestream_toggle,
                video_obj,
                'open',
            )
            livestream_submenu.append(auto_open_menu_item)

            auto_dl_start_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
                _('_Download on start'),
            )
            if video_obj.dbid in self.app_obj.media_reg_auto_dl_start_dict:
                auto_dl_start_menu_item.set_active(True)
            auto_dl_start_menu_item.connect(
                'activate',
                self.on_video_catalogue_livestream_toggle,
                video_obj,
                'dl_start',
            )
            livestream_submenu.append(auto_dl_start_menu_item)
            if __main__.__pkg_no_download_flag__:
                auto_dl_start_menu_item.set_sensitive(False)

            auto_dl_stop_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
                _('Download on _stop'),
            )
            if video_obj.dbid in self.app_obj.media_reg_auto_dl_stop_dict:
                auto_dl_stop_menu_item.set_active(True)
            auto_dl_stop_menu_item.connect(
                'activate',
                self.on_video_catalogue_livestream_toggle,
                video_obj,
                'dl_stop',
            )
            livestream_submenu.append(auto_dl_stop_menu_item)
            if __main__.__pkg_no_download_flag__:
                auto_dl_stop_menu_item.set_sensitive(False)

            # Separator
            livestream_submenu.append(Gtk.SeparatorMenuItem())

            not_live_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('Not a _livestream'),
            )
            not_live_menu_item.connect(
                'activate',
                self.on_video_catalogue_not_livestream,
                video_obj,
            )
            livestream_submenu.append(not_live_menu_item)

            finalise_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('_Finalise livestream'),
            )
            finalise_menu_item.connect(
                'activate',
                self.on_video_catalogue_finalise_livestream,
                video_obj,
            )
            livestream_submenu.append(finalise_menu_item)
            if video_obj.dl_flag \
            or video_obj.live_mode == 1 \
            or (video_obj.live_mode == 0 and not video_obj.was_live_flag):
                finalise_menu_item.set_sensitive(False)
            else:
                output_path = video_obj.get_actual_path(self.app_obj)
                if os.path.isfile(output_path) \
                or not os.path.isfile(output_path + '.part'):
                    finalise_menu_item.set_sensitive(False)

            livestream_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('_Livestream'),
            )
            livestream_menu_item.set_submenu(livestream_submenu)
            popup_menu.append(livestream_menu_item)
            if unavailable_flag:
                livestream_menu_item.set_sensitive(False)

        else:

            # Temporary
            temp_submenu = Gtk.Menu()

            mark_temp_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('_Mark for download'))
            mark_temp_dl_menu_item.connect(
                'activate',
                self.on_video_catalogue_mark_temp_dl,
                video_obj,
            )
            temp_submenu.append(mark_temp_dl_menu_item)

            # Separator
            temp_submenu.append(Gtk.SeparatorMenuItem())

            temp_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Download'))
            temp_dl_menu_item.connect(
                'activate',
                self.on_video_catalogue_temp_dl,
                video_obj,
                False,
            )
            temp_submenu.append(temp_dl_menu_item)

            temp_dl_watch_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('Download and _watch'),
            )
            temp_dl_watch_menu_item.connect(
                'activate',
                self.on_video_catalogue_temp_dl,
                video_obj,
                True,
            )
            temp_submenu.append(temp_dl_watch_menu_item)

            temp_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Temporary'))
            temp_menu_item.set_submenu(temp_submenu)
            popup_menu.append(temp_menu_item)
            if __main__.__pkg_no_download_flag__ \
            or video_obj.source is None \
            or self.app_obj.current_manager_obj \
            or (
                isinstance(video_obj.parent_obj, media.Folder)
                and video_obj.parent_obj.temp_flag
            ) or video_obj.live_mode != 0 \
            or unavailable_flag:
                temp_menu_item.set_sensitive(False)

        # Apply/remove/edit download options, show system command, disable
        #   downloads
        downloads_submenu = Gtk.Menu()

        # (Desensitise these menu items, if an edit window is already open)
        no_options_flag = False
        for win_obj in self.config_win_list:
            if isinstance(win_obj, config.OptionsEditWin) \
            and video_obj.options_obj == win_obj.edit_obj:
                no_options_flag = True
                break

        if not video_obj.options_obj:

            apply_options_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('_Apply download options...'),
            )
            apply_options_menu_item.connect(
                'activate',
                self.on_video_catalogue_apply_options,
                video_obj,
            )
            downloads_submenu.append(apply_options_menu_item)
            if no_options_flag or self.app_obj.current_manager_obj:
                apply_options_menu_item.set_sensitive(False)

        else:

            remove_options_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('_Remove download options'),
            )
            remove_options_menu_item.connect(
                'activate',
                self.on_video_catalogue_remove_options,
                video_obj,
            )
            downloads_submenu.append(remove_options_menu_item)
            if no_options_flag or self.app_obj.current_manager_obj:
                remove_options_menu_item.set_sensitive(False)

        edit_options_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Edit download options...'),
        )
        edit_options_menu_item.connect(
            'activate',
            self.on_video_catalogue_edit_options,
            video_obj,
        )
        downloads_submenu.append(edit_options_menu_item)
        if no_options_flag or self.app_obj.current_manager_obj \
        or not video_obj.options_obj:
            edit_options_menu_item.set_sensitive(False)

        # Separator
        downloads_submenu.append(Gtk.SeparatorMenuItem())

        show_system_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Show system command'),
        )
        show_system_menu_item.connect(
            'activate',
            self.on_video_catalogue_show_system_cmd,
            video_obj,
        )
        downloads_submenu.append(show_system_menu_item)

        test_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Test system command'),
        )
        test_dl_menu_item.connect(
            'activate',
            self.on_video_catalogue_test_dl,
            video_obj,
        )
        downloads_submenu.append(test_dl_menu_item)
        if self.app_obj.current_manager_obj:
            test_dl_menu_item.set_sensitive(False)

        # Separator
        downloads_submenu.append(Gtk.SeparatorMenuItem())

        enforce_check_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
            _('_Disable downloads'),
        )
        enforce_check_menu_item.set_active(video_obj.dl_sim_flag)
        enforce_check_menu_item.connect(
            'activate',
            self.on_video_catalogue_enforce_check,
            video_obj,
        )
        downloads_submenu.append(enforce_check_menu_item)
        # (Don't allow the user to change the setting of
        #   media.Video.dl_sim_flag if the video is in a channel or playlist,
        #   since media.Channel.dl_sim_flag or media.Playlist.dl_sim_flag
        #   applies instead)
        if self.app_obj.current_manager_obj \
        or not isinstance(video_obj.parent_obj, media.Folder):
            enforce_check_menu_item.set_sensitive(False)

        downloads_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('D_ownloads'),
        )
        downloads_menu_item.set_submenu(downloads_submenu)
        popup_menu.append(downloads_menu_item)
        if __main__.__pkg_no_download_flag__ \
        or unavailable_flag:
            downloads_menu_item.set_sensitive(False)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Mark video
        mark_video_submenu = Gtk.Menu()

        archive_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
            _('Video is _archived'),
        )
        archive_video_menu_item.set_active(video_obj.archive_flag)
        archive_video_menu_item.connect(
            'toggled',
            self.on_video_catalogue_toggle_archived_video,
            video_obj,
        )
        mark_video_submenu.append(archive_video_menu_item)
        if not video_obj.dl_flag:
            archive_video_menu_item.set_sensitive(False)

        bookmark_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
            _('Video is _bookmarked'),
        )
        bookmark_video_menu_item.set_active(video_obj.bookmark_flag)
        bookmark_video_menu_item.connect(
            'toggled',
            self.on_video_catalogue_toggle_bookmark_video,
            video_obj,
        )
        mark_video_submenu.append(bookmark_video_menu_item)

        fav_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
            _('Video is _favourite'),
        )
        fav_video_menu_item.set_active(video_obj.fav_flag)
        fav_video_menu_item.connect(
            'toggled',
            self.on_video_catalogue_toggle_favourite_video,
            video_obj,
        )
        mark_video_submenu.append(fav_video_menu_item)

        missing_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
            _('Video is _missing'),
        )
        missing_video_menu_item.set_active(video_obj.missing_flag)
        missing_video_menu_item.connect(
            'toggled',
            self.on_video_catalogue_toggle_missing_video,
            video_obj,
        )
        mark_video_submenu.append(missing_video_menu_item)
        if (
            not isinstance(video_obj.parent_obj, media.Channel) \
            and not isinstance(video_obj.parent_obj, media.Playlist)
        ) or not video_obj.dl_flag:
            missing_video_menu_item.set_sensitive(False)

        new_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
            _('Video is _new'),
        )
        new_video_menu_item.set_active(video_obj.new_flag)
        new_video_menu_item.connect(
            'toggled',
            self.on_video_catalogue_toggle_new_video,
            video_obj,
        )
        mark_video_submenu.append(new_video_menu_item)
        if not video_obj.dl_flag:
            new_video_menu_item.set_sensitive(False)

        playlist_video_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
            _('Video is in _waiting list'),
        )
        playlist_video_menu_item.set_active(video_obj.waiting_flag)
        playlist_video_menu_item.connect(
            'toggled',
            self.on_video_catalogue_toggle_waiting_video,
            video_obj,
        )
        mark_video_submenu.append(playlist_video_menu_item)

        mark_video_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Mark video'),
        )
        mark_video_menu_item.set_submenu(mark_video_submenu)
        popup_menu.append(mark_video_menu_item)
        if video_obj.live_mode != 0:
            mark_video_menu_item.set_sensitive(False)

        # Show location/properties
        show_submenu = Gtk.Menu()

        show_location_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Location'),
        )
        show_location_menu_item.connect(
            'activate',
            self.on_video_catalogue_show_location,
            video_obj,
        )
        show_submenu.append(show_location_menu_item)
        if unavailable_flag:
            show_location_menu_item.set_sensitive(False)

        show_properties_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Properties...'),
        )
        show_properties_menu_item.connect(
            'activate',
            self.on_video_catalogue_show_properties,
            video_obj,
        )
        show_submenu.append(show_properties_menu_item)
        if self.app_obj.current_manager_obj:
            show_properties_menu_item.set_sensitive(False)

        show_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('S_how video'),
        )
        show_menu_item.set_submenu(show_submenu)
        popup_menu.append(show_menu_item)

        # Fetch formats/subtitles
        fetch_submenu = Gtk.Menu()

        fetch_formats_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Available _formats'),
        )
        fetch_formats_menu_item.connect(
            'activate',
            self.on_video_catalogue_fetch_formats,
            video_obj,
        )
        fetch_submenu.append(fetch_formats_menu_item)

        fetch_subs_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Available _subtitles'),
        )
        fetch_subs_menu_item.connect(
            'activate',
            self.on_video_catalogue_fetch_subs,
            video_obj,
        )
        fetch_submenu.append(fetch_subs_menu_item)

        fetch_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Fetch'),
        )
        fetch_menu_item.set_submenu(fetch_submenu)
        popup_menu.append(fetch_menu_item)
        if video_obj.source is None or self.app_obj.current_manager_obj:
            fetch_menu_item.set_sensitive(False)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Delete video
        delete_menu_item = Gtk.MenuItem.new_with_mnemonic(_('D_elete video'))
        delete_menu_item.connect(
            'activate',
            self.on_video_catalogue_delete_video,
            video_obj,
        )
        popup_menu.append(delete_menu_item)
        if self.config_win_list:
            delete_menu_item.set_sensitive(False)

        # Create the popup menu
        popup_menu.show_all()
        popup_menu.popup(None, None, None, None, event.button, event.time)


    def video_catalogue_multi_popup_menu(self, event, video_list):

        """Called by self.video_catalogue_popup_menu().

        When multiple videos are selected in the Video Catalogue and the user
        right-clicks one of them, shows a context-sensitive popup menu.

        Args:

            event (Gdk.EventButton): The mouse click event

            video_list (list): List of media.Video objects that are currently
                selected (each one corresponding to a single media.Video
                object)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 7876 video_catalogue_multi_popup_menu')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Video Catalogue popup menu starts here. In' \
            + ' the Videos tab, select two or more videos, then righ-click' \
            + ' them'
        )

        # So we can desensitise some menu items, work out in advance whether
        #   any of the selected videos are marked as downloaded, or have a
        #   source URL, or are in a temporary folder
        dl_flag = False
        for video_obj in video_list:
            if video_obj.dl_flag:
                dl_flag = True
                break

        not_dl_flag = False
        for video_obj in video_list:
            if not video_obj.dl_flag:
                not_dl_flag = True
                break

        not_check_flag = False
        for video_obj in video_list:
            if video_obj.parent_obj.dl_no_db_flag:
                not_check_flag = True
                break

        source_flag = False
        for video_obj in video_list:
            if video_obj.source is not None:
                source_flag = True
                break

        temp_folder_flag = False
        for video_obj in video_list:
            if isinstance(video_obj.parent_obj, media.Folder) \
            and video_obj.parent_obj.temp_flag:
                temp_folder_flag = True
                break

        # For 'missing' videos, work out if the selected videos are all inside
        #   a channel or playlist
        any_folder_flag = False
        for video_obj in video_list:
            if isinstance(video_obj.parent_obj, media.Folder):
                any_folder_flag = True
                break

        # Also work out if any videos are waiting or broadcasting livestreams
        live_flag = False
        live_wait_flag = False
        for video_obj in video_list:
            if video_obj.live_mode == 1:
                live_flag = True
                live_wait_flag = True
                break

        live_broadcast_flag = False
        for video_obj in video_list:
            if video_obj.live_mode == 2:
                live_flag = True
                live_broadcast_flag = True
                break

        # (If the parent channel/playlist/folder has external directory is set,
        #   but which is not available, many items must be desensitised)
        for video_obj in video_list:
            if video_obj.parent_obj.dbid \
            in self.app_obj.container_unavailable_dict:
                unavailable_flag = True
            else:
                unavailable_flag = False

            # (Only need to test one video)
            break

        # Set up the popup menu
        popup_menu = Gtk.Menu()

        # Check/download videos
        check_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Check videos'))
        check_menu_item.connect(
            'activate',
            self.on_video_catalogue_check_multi,
            video_list,
        )
        # (We can add another video to the downloads.DownloadList object, even
        #   after a download operation has started)
        if (
            self.app_obj.current_manager_obj \
            and not self.app_obj.download_manager_obj
        ) or (
            self.app_obj.download_manager_obj \
            and self.app_obj.download_manager_obj.operation_classic_flag
        ) or unavailable_flag \
        or not_check_flag:
            check_menu_item.set_sensitive(False)
        popup_menu.append(check_menu_item)

        download_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Download videos')
        )
        download_menu_item.connect(
            'activate',
            self.on_video_catalogue_download_multi,
            video_list,
            live_wait_flag,
        )
        if __main__.__pkg_no_download_flag__ \
        or (
            self.app_obj.current_manager_obj \
            and not self.app_obj.download_manager_obj
        ) or (
            self.app_obj.download_manager_obj \
            and self.app_obj.download_manager_obj.operation_classic_flag
        ) or live_wait_flag \
        or unavailable_flag:
            download_menu_item.set_sensitive(False)
        popup_menu.append(download_menu_item)

        custom_dl_submenu = self.custom_dl_popup_submenu(video_list)

        custom_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('C_ustom download videos')
        )
        custom_dl_menu_item.set_submenu(custom_dl_submenu)
        if __main__.__pkg_no_download_flag__ \
        or self.app_obj.current_manager_obj \
        or live_flag \
        or unavailable_flag:
            custom_dl_menu_item.set_sensitive(False)
        popup_menu.append(custom_dl_menu_item)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Watch video
        self.add_watch_video_menu_items(
            popup_menu,
            not_dl_flag,
            source_flag,
            live_flag,
            unavailable_flag,
            video_list,
        )

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Special
        special_submenu = Gtk.Menu()

        process_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Process with FFmpeg...'),
        )
        process_menu_item.connect(
            'activate',
            self.on_video_catalogue_process_ffmpeg_multi,
            video_list,
        )
        special_submenu.append(process_menu_item)
        if self.app_obj.current_manager_obj \
        or unavailable_flag:
            process_menu_item.set_sensitive(False)

        # Separator
        special_submenu.append(Gtk.SeparatorMenuItem())

        reload_metadata_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Reload metadata'),
        )
        reload_metadata_menu_item.connect(
            'activate',
            self.on_video_catalogue_reload_metadata_multi,
            video_list,
        )
        special_submenu.append(reload_metadata_menu_item)
        if self.app_obj.current_manager_obj or self.config_win_list:
            reload_metadata_menu_item.set_sensitive(False)

        special_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Special'),
        )
        special_menu_item.set_submenu(special_submenu)
        popup_menu.append(special_menu_item)
        if self.app_obj.current_manager_obj \
        or unavailable_flag:
            special_menu_item.set_sensitive(False)

        # Add to Classic Mode tab
        classic_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Add to C_lassic Mode tab'),
        )
        classic_dl_menu_item.connect(
            'activate',
            self.on_video_catalogue_add_classic_multi,
            video_list,
        )
        popup_menu.append(classic_dl_menu_item)
        if __main__.__pkg_no_download_flag__:
            classic_dl_menu_item.set_sensitive(False)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        if live_flag or live_wait_flag or live_broadcast_flag:

            # Livestream
            livestream_submenu = Gtk.Menu()

            not_live_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('Mark as _not livestreams'),
            )
            not_live_menu_item.connect(
                'activate',
                self.on_video_catalogue_not_livestream_multi,
                video_list,
            )
            livestream_submenu.append(not_live_menu_item)

            finalise_live_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('_Finalise livestreams'),
            )
            finalise_live_menu_item.connect(
                'activate',
                self.on_video_catalogue_finalise_livestream_multi,
                video_list,
            )
            livestream_submenu.append(finalise_live_menu_item)

            livestream_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('_Livestream'),
            )
            livestream_menu_item.set_submenu(livestream_submenu)
            popup_menu.append(livestream_menu_item)
            if unavailable_flag:
                livestream_menu_item.set_sensitive(False)

        # Download to Temporary Videos
        temp_submenu = Gtk.Menu()

        mark_temp_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Mark for download'),
        )
        mark_temp_dl_menu_item.connect(
            'activate',
            self.on_video_catalogue_mark_temp_dl_multi,
            video_list,
        )
        temp_submenu.append(mark_temp_dl_menu_item)

        # Separator
        temp_submenu.append(Gtk.SeparatorMenuItem())

        temp_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Download'))
        temp_dl_menu_item.connect(
            'activate',
            self.on_video_catalogue_temp_dl_multi,
            video_list,
            False,
        )
        temp_submenu.append(temp_dl_menu_item)

        temp_dl_watch_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Download and _watch'),
        )
        temp_dl_watch_menu_item.connect(
            'activate',
            self.on_video_catalogue_temp_dl_multi,
            video_list,
            True,
        )
        temp_submenu.append(temp_dl_watch_menu_item)

        temp_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Temporary'),
        )
        temp_menu_item.set_submenu(temp_submenu)
        popup_menu.append(temp_menu_item)
        if __main__.__pkg_no_download_flag__ \
        or not source_flag \
        or self.app_obj.update_manager_obj \
        or self.app_obj.refresh_manager_obj \
        or self.app_obj.process_manager_obj \
        or temp_folder_flag \
        or live_flag \
        or unavailable_flag:
            temp_menu_item.set_sensitive(False)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Mark videos
        mark_videos_submenu = Gtk.Menu()

        archive_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Archived'),
        )
        archive_menu_item.connect(
            'activate',
            self.on_video_catalogue_toggle_archived_video_multi,
            True,
            video_list,
        )
        mark_videos_submenu.append(archive_menu_item)

        not_archive_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Not a_rchived'),
        )
        not_archive_menu_item.connect(
            'activate',
            self.on_video_catalogue_toggle_archived_video_multi,
            False,
            video_list,
        )
        mark_videos_submenu.append(not_archive_menu_item)

        # Separator
        mark_videos_submenu.append(Gtk.SeparatorMenuItem())

        bookmark_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Bookmarked'),
        )
        bookmark_menu_item.connect(
            'activate',
            self.on_video_catalogue_toggle_bookmark_video_multi,
            True,
            video_list,
        )
        mark_videos_submenu.append(bookmark_menu_item)

        not_bookmark_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Not b_ookmarked'),
        )
        not_bookmark_menu_item.connect(
            'activate',
            self.on_video_catalogue_toggle_bookmark_video_multi,
            False,
            video_list,
        )
        mark_videos_submenu.append(not_bookmark_menu_item)

        # Separator
        mark_videos_submenu.append(Gtk.SeparatorMenuItem())

        fav_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Favourite'),
        )
        fav_menu_item.connect(
            'activate',
            self.on_video_catalogue_toggle_favourite_video_multi,
            True,
            video_list,
        )
        mark_videos_submenu.append(fav_menu_item)

        not_fav_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Not fa_vourite'),
        )
        not_fav_menu_item.connect(
            'activate',
            self.on_video_catalogue_toggle_favourite_video_multi,
            False,
            video_list,
        )
        mark_videos_submenu.append(not_fav_menu_item)

        # Separator
        mark_videos_submenu.append(Gtk.SeparatorMenuItem())

        missing_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Missing'),
        )
        missing_menu_item.connect(
            'activate',
            self.on_video_catalogue_toggle_missing_video_multi,
            True,
            video_list,
        )
        if not dl_flag or any_folder_flag:
            missing_menu_item.set_sensitive(False)
        mark_videos_submenu.append(missing_menu_item)

        not_missing_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Not m_issing'),
        )
        not_missing_menu_item.connect(
            'activate',
            self.on_video_catalogue_toggle_missing_video_multi,
            False,
            video_list,
        )
        if not dl_flag or any_folder_flag:
            not_missing_menu_item.set_sensitive(False)
        mark_videos_submenu.append(not_missing_menu_item)

        # Separator
        mark_videos_submenu.append(Gtk.SeparatorMenuItem())

        new_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_New'),
        )
        new_menu_item.connect(
            'activate',
            self.on_video_catalogue_toggle_new_video_multi,
            True,
            video_list,
        )
        mark_videos_submenu.append(new_menu_item)

        not_new_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Not n_ew'),
        )
        not_new_menu_item.connect(
            'activate',
            self.on_video_catalogue_toggle_new_video_multi,
            False,
            video_list,
        )
        mark_videos_submenu.append(not_new_menu_item)

        # Separator
        mark_videos_submenu.append(Gtk.SeparatorMenuItem())

        playlist_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('In _waiting list'),
        )
        playlist_menu_item.connect(
            'activate',
            self.on_video_catalogue_toggle_waiting_video_multi,
            True,
            video_list,
        )
        mark_videos_submenu.append(playlist_menu_item)

        not_playlist_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Not in w_aiting list'),
        )
        not_playlist_menu_item.connect(
            'activate',
            self.on_video_catalogue_toggle_waiting_video_multi,
            False,
            video_list,
        )
        mark_videos_submenu.append(not_playlist_menu_item)

        mark_videos_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Mark videos'),
        )
        mark_videos_menu_item.set_submenu(mark_videos_submenu)
        popup_menu.append(mark_videos_menu_item)
        if live_flag or not dl_flag:
            mark_videos_menu_item.set_sensitive(False)

        # Show properties
        show_properties_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Show p_roperties...'),
        )
        show_properties_menu_item.connect(
            'activate',
            self.on_video_catalogue_show_properties_multi,
            video_list,
        )
        popup_menu.append(show_properties_menu_item)
        if self.app_obj.current_manager_obj:
            show_properties_menu_item.set_sensitive(False)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Delete videos
        delete_menu_item = Gtk.MenuItem.new_with_mnemonic(_('D_elete videos'))
        delete_menu_item.connect(
            'activate',
            self.on_video_catalogue_delete_video_multi,
            video_list,
        )
        popup_menu.append(delete_menu_item)
        if self.config_win_list:
            delete_menu_item.set_sensitive(False)

        # Create the popup menu
        popup_menu.show_all()
        popup_menu.popup(None, None, None, None, event.button, event.time)


    def progress_list_popup_menu(self, event, item_id, dbid):

        """Called by self.on_progress_list_right_click().

        When the user right-clicks on the Progress List, shows a context-
        sensitive popup menu.

        Args:

            event (Gdk.EventButton): The mouse click event

            item_id (int): The .item_id of the clicked downloads.DownloadItem
                object

            dbid (int): The .dbid of the corresponding media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 8383 progress_list_popup_menu')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Progress List popup menu starts here. In' \
            + ' the Progress tab, in the list in the top half of the tab,' \
            + ' right-click any row'
        )

        # Find the downloads.VideoDownloader which is currently handling the
        #   clicked media data object (if any)
        download_manager_obj = self.app_obj.download_manager_obj
        download_list_obj = None
        download_item_obj = None
        worker_obj = None
        downloader_obj = None
        queuing_flag = False

        if download_manager_obj:

            download_list_obj = download_manager_obj.download_list_obj
            download_item_obj = download_list_obj.download_item_dict[item_id]

            for this_worker_obj in download_manager_obj.worker_list:
                if this_worker_obj.running_flag \
                and this_worker_obj.download_item_obj == download_item_obj \
                and this_worker_obj.downloader_obj is not None:
                    worker_obj = this_worker_obj
                    downloader_obj = this_worker_obj.downloader_obj
                    break

            if not downloader_obj:
                queuing_flag = download_list_obj.is_queuing(
                    download_item_obj.item_id
                )

        if download_manager_obj \
        and (
            download_manager_obj.operation_type == 'custom_sim' \
            or download_manager_obj.operation_type == 'classic_sim'
        ):
            custom_sim_flag = True
        else:
            custom_sim_flag = False

        # Find the media data object itself. If the download operation has
        #   finished, the variables just above will not be set
        media_data_obj = None
        if dbid in self.app_obj.media_reg_dict:
            media_data_obj = self.app_obj.media_reg_dict[dbid]

        # Set up the popup menu
        popup_menu = Gtk.Menu()

        # Stop check/download
        stop_now_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Stop now'))
        stop_now_menu_item.connect(
            'activate',
            self.on_progress_list_stop_now,
            download_item_obj,
            worker_obj,
            downloader_obj,
        )
        popup_menu.append(stop_now_menu_item)
        if not download_manager_obj \
        or downloader_obj is None:
            stop_now_menu_item.set_sensitive(False)

        # N.B. During the checking stage of a custom download (operation types
        #   'custom_sim', 'classic_sim'), this menu option has a slightly
        #   different effect (so uses a diffirent label)
        if custom_sim_flag:
            msg = _('Stop checking _videos')
        elif queuing_flag:
            msg = _('Completely stop after this _video')
        else:
            msg = _('Stop after this _video')

        stop_soon_menu_item = Gtk.MenuItem.new_with_mnemonic(msg)
        stop_soon_menu_item.connect(
            'activate',
            self.on_progress_list_stop_soon,
            download_item_obj,
            worker_obj,
            downloader_obj,
        )
        popup_menu.append(stop_soon_menu_item)
        if not download_manager_obj \
        or (downloader_obj is None and not queuing_flag):
            stop_soon_menu_item.set_sensitive(False)

        # N.B. During the checking stage of a custom download (operation types
        #   'custom_sim', 'classic_sim'), this menu option is redundant (same
        #   effect as the 'Stop now' menu option)
        stop_all_soon_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Completely stop after these v_ideos'),
        )
        stop_all_soon_menu_item.connect(
            'activate',
            self.on_progress_list_stop_all_soon,
        )
        popup_menu.append(stop_all_soon_menu_item)
        if not download_manager_obj or custom_sim_flag:
            stop_all_soon_menu_item.set_sensitive(False)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Check/download next/last
        dl_next_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Download _next'),
        )
        dl_next_menu_item.connect(
            'activate',
            self.on_progress_list_dl_next,
            download_item_obj,
        )
        popup_menu.append(dl_next_menu_item)
        if not download_manager_obj or worker_obj:
            dl_next_menu_item.set_sensitive(False)

        dl_last_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Download _last'),
        )
        dl_last_menu_item.connect(
            'activate',
            self.on_progress_list_dl_last,
            download_item_obj,
        )
        popup_menu.append(dl_last_menu_item)
        if not download_manager_obj or worker_obj:
            dl_last_menu_item.set_sensitive(False)

        # Watch on website
        if media_data_obj \
        and isinstance(media_data_obj, media.Video) \
        and media_data_obj.source:

            # Separator
            popup_menu.append(Gtk.SeparatorMenuItem())

            # For YouTube videos, offer three websites (as usual)
            enhanced = utils.is_video_enhanced(media_data_obj)
            if not enhanced:

                watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic(
                    _('_Watch on Website'),
                )
                watch_website_menu_item.connect(
                    'activate',
                    self.on_progress_list_watch_website,
                    media_data_obj,
                )
                popup_menu.append(watch_website_menu_item)

            elif enhanced != 'youtube':

                pretty = formats.ENHANCED_SITE_DICT[enhanced]['pretty_name']
                watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic(
                    _('_Watch on {0}').format(pretty),
                )
                watch_website_menu_item.connect(
                    'activate',
                    self.on_progress_list_watch_website,
                    media_data_obj,
                )
                popup_menu.append(watch_website_menu_item)

            else:

                watch_youtube_menu_item = Gtk.MenuItem.new_with_mnemonic(
                    _('_Watch on YouTube'),
                )
                watch_youtube_menu_item.connect(
                    'activate',
                    self.on_progress_list_watch_website,
                    media_data_obj,
                )
                popup_menu.append(watch_youtube_menu_item)

                watch_hooktube_menu_item = Gtk.MenuItem.new_with_mnemonic(
                    _('Watch on _HookTube'),
                )
                watch_hooktube_menu_item.connect(
                    'activate',
                    self.on_progress_list_watch_hooktube,
                    media_data_obj,
                )
                popup_menu.append(watch_hooktube_menu_item)

                watch_invidious_menu_item = Gtk.MenuItem.new_with_mnemonic(
                    _('Watch on _Invidious'),
                )
                watch_invidious_menu_item.connect(
                    'activate',
                    self.on_progress_list_watch_invidious,
                    media_data_obj,
                )
                popup_menu.append(watch_invidious_menu_item)

        # Create the popup menu
        popup_menu.show_all()
        popup_menu.popup(None, None, None, None, event.button, event.time)


    def results_list_popup_menu(self, event, path):

        """Called by self.on_results_list_right_click().

        When the user right-clicks on the Results List, shows a context-
        sensitive popup menu.

        Unlike the popup menu functions above, here we use a single function
        for single or multiple selections in the treeview.

        Args:

            event (Gdk.EventButton): The mouse click event

            path (Gtk.TreePath): Path to the clicked row in the treeview

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 8606 results_list_popup_menu')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Results List popup menu starts here. In' \
            + ' the Progress tab, in the list in the bottom half of the tab,' \
            + ' right-click any row'
        )

        # Get the selected media.Video object(s)
        video_list = self.get_selected_videos_in_treeview(
            self.results_list_treeview,
            0,      # Column 0 contains the media.Video's .dbid
        )
        # Any videos which have been deleted (but which are still visible in
        #   the Results List) are not returned, so the list might be empty
        if not video_list:
            return

        # So we can desensitise some menu items, work out in advance whether
        #   any of the selected videos are marked as downloaded, or have a
        #   source URL, or are in a temporary folder
        dl_flag = False
        for video_obj in video_list:
            if video_obj.dl_flag:
                dl_flag = True
                break

        not_dl_flag = False
        for video_obj in video_list:
            if not video_obj.dl_flag:
                not_dl_flag = True
                break

        source_flag = False
        for video_obj in video_list:
            if video_obj.source is not None:
                source_flag = True
                break

        temp_folder_flag = False
        for video_obj in video_list:
            if isinstance(video_obj.parent_obj, media.Folder) \
            and video_obj.parent_obj.temp_flag:
                temp_folder_flag = True
                break

        live_flag = False
        for video_obj in video_list:
            if video_obj.live_mode == 1:
                live_flag = True
                break

        # Set up the popup menu
        popup_menu = Gtk.Menu()

        # Watch video
        self.add_watch_video_menu_items(
            popup_menu,
            not_dl_flag,
            source_flag,
            live_flag,
            False,          # unavailable_flag does not apply here
            video_list,
        )

        show_location_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Open destination(s)'),
        )
        show_location_menu_item.connect(
            'activate',
            self.on_video_catalogue_show_location_multi,
            video_list,
        )
        popup_menu.append(show_location_menu_item)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Process with FFmpeg
        process_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Process with FFmpeg...'),
        )
        process_menu_item.connect(
            'activate',
            self.on_video_catalogue_process_ffmpeg_multi,
            video_list,
        )
        popup_menu.append(process_menu_item)
        if self.app_obj.current_manager_obj:
            process_menu_item.set_sensitive(False)

        # Add to Classic Mode tab
        classic_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Add to C_lassic Mode tab'),
        )
        classic_dl_menu_item.connect(
            'activate',
            self.on_video_catalogue_add_classic_multi,
            video_list,
        )
        popup_menu.append(classic_dl_menu_item)
        if __main__.__pkg_no_download_flag__:
            classic_dl_menu_item.set_sensitive(False)

        # Livestreams
        livestream_submenu = Gtk.Menu()

        not_live_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Mark as _not livestreams'),
        )
        not_live_menu_item.connect(
            'activate',
            self.on_video_catalogue_not_livestream_multi,
            video_list,
        )
        livestream_submenu.append(not_live_menu_item)

        finalise_live_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Finalise livestreams'),
        )
        finalise_live_menu_item.connect(
            'activate',
            self.on_video_catalogue_finalise_livestream_multi,
            video_list,
        )
        livestream_submenu.append(finalise_live_menu_item)

        livestream_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Livestream'),
        )
        livestream_menu_item.set_submenu(livestream_submenu)
        popup_menu.append(livestream_menu_item)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Download to Temporary Videos
        temp_submenu = Gtk.Menu()

        mark_temp_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Mark for download'),
        )
        mark_temp_dl_menu_item.connect(
            'activate',
            self.on_video_catalogue_mark_temp_dl_multi,
            video_list,
        )
        temp_submenu.append(mark_temp_dl_menu_item)

        # Separator
        temp_submenu.append(Gtk.SeparatorMenuItem())

        temp_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Download'))
        temp_dl_menu_item.connect(
            'activate',
            self.on_video_catalogue_temp_dl_multi,
            video_list,
            False,
        )
        temp_submenu.append(temp_dl_menu_item)

        temp_dl_watch_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Download and _watch'),
        )
        temp_dl_watch_menu_item.connect(
            'activate',
            self.on_video_catalogue_temp_dl_multi,
            video_list,
            True,
        )
        temp_submenu.append(temp_dl_watch_menu_item)

        temp_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Temporary'),
        )
        temp_menu_item.set_submenu(temp_submenu)
        popup_menu.append(temp_menu_item)
        if __main__.__pkg_no_download_flag__ \
        or not source_flag \
        or self.app_obj.update_manager_obj \
        or self.app_obj.refresh_manager_obj \
        or self.app_obj.process_manager_obj \
        or temp_folder_flag \
        or live_flag:
            temp_menu_item.set_sensitive(False)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Delete videos
        delete_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Delete video(s)'),
        )
        delete_menu_item.connect(
            'activate',
            self.on_video_catalogue_delete_video_multi,
            video_list,
        )
        popup_menu.append(delete_menu_item)
        if self.app_obj.current_manager_obj:
            delete_menu_item.set_sensitive(False)

        # Create the popup menu
        popup_menu.show_all()
        popup_menu.popup(None, None, None, None, event.button, event.time)


    def classic_popup_menu(self):

        """Called by mainapp.TartubeApp.on_button_classic_menu().

        When the user right-clicks the menu button in the Classic Mode tab,
        shows a context-sensitive popup menu.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 8822 classic_popup_menu')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Classic Mode popup menu starts here. In' \
            + ' the Classic Mode tab, click the button in the top-right' \
            + ' corner'
        )

        # Set up the popup menu
        popup_menu = Gtk.Menu()

        # Automatic copy/paste
        automatic_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
             _('_Enable automatic copy/paste'),
        )
        if self.classic_auto_copy_flag:
            automatic_menu_item.set_active(True)
        automatic_menu_item.connect(
            'toggled',
            self.on_classic_menu_toggle_auto_copy,
        )
        popup_menu.append(automatic_menu_item)

        # One-click downloads
        one_click_dl_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
             _('E_nable one-click downloads'),
        )
        if self.classic_one_click_dl_flag:
            one_click_dl_menu_item.set_active(True)
        one_click_dl_menu_item.connect(
            'toggled',
            self.on_classic_menu_toggle_one_click_dl,
        )
        popup_menu.append(one_click_dl_menu_item)

        # Remember undownloaded URLs
        remember_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
            _('_Remember un-downloaded URLs'),
        )
        if self.app_obj.classic_pending_flag:
            remember_menu_item.set_active(True)
        remember_menu_item.connect(
            'toggled',
            self.on_classic_menu_toggle_remember_urls,
        )
        popup_menu.append(remember_menu_item)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Download options
        set_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Set download options...'),
        )
        set_menu_item.connect(
            'activate',
            self.on_classic_menu_set_options,
        )
        if self.app_obj.current_manager_obj:
            set_menu_item.set_sensitive(False)
        popup_menu.append(set_menu_item)

        default_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Use _default download options'),
        )
        default_menu_item.connect(
            'activate',
            self.on_classic_menu_use_general_options,
        )
        if self.app_obj.current_manager_obj \
        or not self.app_obj.classic_options_obj:
            default_menu_item.set_sensitive(False)
        popup_menu.append(default_menu_item)

        edit_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Edit download _options...'),
        )
        edit_menu_item.connect(
            'activate',
            self.on_classic_menu_edit_options,
        )
        if self.app_obj.current_manager_obj:
            edit_menu_item.set_sensitive(False)
        popup_menu.append(edit_menu_item)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        custom_dl_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
            _('Enable _custom downloads'),
        )
        if self.app_obj.classic_custom_dl_flag:
            custom_dl_menu_item.set_active(True)
        custom_dl_menu_item.connect(
            'toggled',
            self.on_classic_menu_toggle_custom_dl,
        )
        if self.app_obj.current_manager_obj:
            custom_dl_menu_item.set_sensitive(False)
        popup_menu.append(custom_dl_menu_item)

        custom_pref_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Custom downloads _preferences...'),
        )
        custom_pref_menu_item.connect(
            'activate',
            self.on_classic_menu_custom_dl_prefs,
        )
        if self.app_obj.current_manager_obj:
            custom_pref_menu_item.set_sensitive(False)
        popup_menu.append(custom_pref_menu_item)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Update youtube-dl
        update_ytdl_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Update') + ' ' + self.app_obj.get_downloader(),
        )
        update_ytdl_menu_item.connect(
            'activate',
            self.on_classic_menu_update_ytdl,
        )
        popup_menu.append(update_ytdl_menu_item)
        if self.app_obj.current_manager_obj \
        or __main__.__pkg_strict_install_flag__:
            update_ytdl_menu_item.set_sensitive(False)

        # Create the popup menu
        popup_menu.show_all()
        popup_menu.popup(
            None,
            None,
            None,
            None,
            1,
            Gtk.get_current_event_time(),
        )


    def classic_progress_list_popup_menu(self, event, path):

        """Called by self.on_classic_progress_list_right_click().

        When the user right-clicks on the Classic Progress List, shows a
        context-sensitive popup menu.

        Args:

            event (Gdk.EventButton): The mouse click event

            path (Gtk.TreePath): Path to the clicked row in the treeview

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 8978 classic_progress_list_popup_menu')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Classic Progress List popup menu starts' \
            + ' here. In the Classic Mode tab, in the list in the bottom' \
            + ' half of the tab, right-click any row'
        )

        # Get the selected dummy media.Video object(s)
        video_list = self.get_selected_videos_in_classic_treeview()
        # Because of Gtk weirdness, right-clicking a line might not select it
        #   in time for Gtk.Selection to know about it
        if not video_list:
            return

        # Set up the popup menu
        popup_menu = Gtk.Menu()

        # Play video
        play_video_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Play video'),
        )
        play_video_menu_item.connect(
            'activate',
            self.on_classic_progress_list_from_popup,
            'play',
            video_list,
        )
        popup_menu.append(play_video_menu_item)

        # Open destination
        open_destination_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Open destination(s)'),
        )
        open_destination_menu_item.connect(
            'activate',
            self.on_classic_progress_list_from_popup,
            'open',
            video_list,
        )
        popup_menu.append(open_destination_menu_item)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Re-download
        re_download_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Re-download'),
        )
        re_download_menu_item.connect(
            'activate',
            self.on_classic_progress_list_from_popup,
            'redownload',
            video_list,
        )
        popup_menu.append(re_download_menu_item)

        # Stop download
        stop_download_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Stop download'),
        )
        stop_download_menu_item.connect(
            'activate',
            self.on_classic_progress_list_from_popup,
            'stop',
            video_list,
        )
        popup_menu.append(stop_download_menu_item)
        if not self.app_obj.current_manager_obj:
            stop_download_menu_item.set_sensitive(False)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Create video clips
        create_clips_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Create _video clips...'),
        )
        create_clips_menu_item.connect(
            'activate',
            self.on_classic_progress_list_from_popup,
            'clips',
            video_list,
        )
        popup_menu.append(create_clips_menu_item)
        if self.app_obj.current_manager_obj \
        or len(video_list) > 1:
            create_clips_menu_item.set_sensitive(False)

        # Process with FFmpeg
        process_ffmpeg_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Process with _FFmpeg...'),
        )
        process_ffmpeg_menu_item.connect(
            'activate',
            self.on_classic_progress_list_from_popup,
            'ffmpeg',
            video_list,
        )
        popup_menu.append(process_ffmpeg_menu_item)
        if self.app_obj.current_manager_obj:
            process_ffmpeg_menu_item.set_sensitive(False)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Copy file path
        copy_submenu = Gtk.Menu()

        copy_path_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_File path'),
        )
        copy_path_menu_item.connect(
            'activate',
            self.on_classic_progress_list_get_path,
            video_list[0],
        )
        if len(video_list) > 1:
            copy_path_menu_item.set_sensitive(False)
        copy_submenu.append(copy_path_menu_item)

        # Copy URL
        copy_url_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_URL'))
        copy_url_menu_item.connect(
            'activate',
            self.on_classic_progress_list_get_url,
            video_list[0],
        )
        if len(video_list) > 1:
            copy_url_menu_item.set_sensitive(False)
        copy_submenu.append(copy_url_menu_item)

        # Copy system command
        copy_cmd_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_System command'),
        )
        copy_cmd_menu_item.connect(
            'activate',
            self.on_classic_progress_list_get_cmd,
            video_list[0],
        )
        if len(video_list) > 1:
            copy_cmd_menu_item.set_sensitive(False)
        copy_submenu.append(copy_cmd_menu_item)

        copy_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Copy'))
        copy_menu_item.set_submenu(copy_submenu)
        popup_menu.append(copy_menu_item)

        # Move up
        move_submenu = Gtk.Menu()

        move_up_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Up'),
        )
        move_up_menu_item.connect(
            'activate',
            self.on_classic_progress_list_from_popup,
            'move_up',
            video_list,
        )
        move_submenu.append(move_up_menu_item)

        # Move down
        move_down_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Down'),
        )
        move_down_menu_item.connect(
            'activate',
            self.on_classic_progress_list_from_popup,
            'move_down',
            video_list,
        )
        move_submenu.append(move_down_menu_item)

        move_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Move'))
        move_menu_item.set_submenu(move_submenu)
        popup_menu.append(move_menu_item)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Re-insert URL above
        reinsert_url_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Re-_insert URL above'),
        )
        reinsert_url_menu_item.connect(
            'activate',
            self.on_classic_progress_list_reinsert_url,
            video_list,
        )
        popup_menu.append(reinsert_url_menu_item)

        # Remove from list
        remove_from_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Re_move from list'),
        )
        remove_from_menu_item.connect(
            'activate',
            self.on_classic_progress_list_from_popup,
            'remove',
            video_list,
        )
        popup_menu.append(remove_from_menu_item)

        # Create the popup menu
        popup_menu.show_all()
        popup_menu.popup(None, None, None, None, event.button, event.time)


    def add_watch_video_menu_items(self, popup_menu, not_dl_flag, source_flag,
    live_flag, unavailable_flag, video_list):

        """Called by self.video_catalogue_multi_popup_menu() and
        .results_list_popup_menu().

        Adds common menu items to the popup menus.

        Args:

            popup_menu (Gtk.Menu): The popup menu

            not_dl_flag (bool): Flag set to True if any of the media.Video
                objects do not have their .dl_flag IV set

            source_flag (bool): Flag set to True if any of the media.Video
                objects have their .source IV set

            live_flag (bool): Flag set to True if any of the media.Video
                objects have their .live_mode IV set to any value above 0

            unavailable_flag (bool): Flag set to True if the videos' parent
                channel/playlist/folder has an external directory that is
                marked disabled

            video_list (list): List of one or more media.Video objects on
                which this popup menu acts

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 9219 add_watch_video_menu_items')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Extra items for the popup menu in the' \
            + ' Video Catalogue. In the videos tab, right-click any video'
        )

        # Watch video in player/download and watch
        if not_dl_flag or live_flag:

            dl_watch_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('D_ownload and watch'),
            )
            dl_watch_menu_item.connect(
                'activate',
                self.on_video_catalogue_dl_and_watch_multi,
                video_list,
            )
            popup_menu.append(dl_watch_menu_item)
            if __main__.__pkg_no_download_flag__ \
            or not source_flag \
            or self.app_obj.update_manager_obj \
            or self.app_obj.refresh_manager_obj \
            or self.app_obj.process_manager_obj \
            or live_flag \
            or unavailable_flag:
                dl_watch_menu_item.set_sensitive(False)

        else:

            watch_player_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('Watch in _player'),
            )
            watch_player_menu_item.connect(
                'activate',
                self.on_video_catalogue_watch_video_multi,
                video_list,
            )
            popup_menu.append(watch_player_menu_item)

        if len(video_list) > 1 or not source_flag or live_flag:

            watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic(
                _('Watch on _website'),
            )
            watch_website_menu_item.connect(
                'activate',
                self.on_video_catalogue_watch_website_multi,
                video_list,
            )
            if not source_flag:
                watch_website_menu_item.set_sensitive(False)
            popup_menu.append(watch_website_menu_item)

        else:

            video_obj = video_list[0]
            enhanced = utils.is_video_enhanced(video_obj)

            if not enhanced:

                watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic(
                    _('_Watch on website'),
                )
                watch_website_menu_item.connect(
                    'activate',
                    self.on_video_catalogue_watch_website,
                    video_obj,
                )
                popup_menu.append(watch_website_menu_item)

            elif enhanced != 'youtube':

                pretty = formats.ENHANCED_SITE_DICT[enhanced]['pretty_name']
                watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic(
                    _('_Watch on {0}').format(pretty),
                )
                watch_website_menu_item.connect(
                    'activate',
                    self.on_video_catalogue_watch_website,
                    video_obj,
                )
                popup_menu.append(watch_website_menu_item)

            else:

                alt_submenu = Gtk.Menu()

                watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic(
                    _('_YouTube'),
                )
                watch_website_menu_item.connect(
                    'activate',
                    self.on_video_catalogue_watch_website,
                    video_obj,
                )
                alt_submenu.append(watch_website_menu_item)

                watch_hooktube_menu_item = Gtk.MenuItem.new_with_mnemonic(
                    _('_HookTube'),
                )
                watch_hooktube_menu_item.connect(
                    'activate',
                    self.on_video_catalogue_watch_hooktube,
                    video_obj,
                )
                alt_submenu.append(watch_hooktube_menu_item)

                watch_invidious_menu_item = Gtk.MenuItem.new_with_mnemonic(
                    _('_Invidious'),
                )
                watch_invidious_menu_item.connect(
                    'activate',
                    self.on_video_catalogue_watch_invidious,
                    video_obj,
                )
                alt_submenu.append(watch_invidious_menu_item)

                ignore_me = _(
                    'TRANSLATOR\'S NOTE: Watch on YouTube, Watch on' \
                    + ' HookTube, etc',
                )

                alt_menu_item = Gtk.MenuItem.new_with_mnemonic(
                    _('W_atch on'),
                )
                alt_menu_item.set_submenu(alt_submenu)
                popup_menu.append(alt_menu_item)


    def custom_dl_popup_menu(self, media_data_list=[]):

        """Called by mainapp.TartubeApp.on_button_custom_dl_all().

        When the user right-clicks the custom download manager selection button
        in the Videos tab, shows a context-sensitive popup menu.

        Args:

            media_data_list (list): List of media data objects to custom
                download (may be an empty list)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 9364 custom_dl_popup_menu')

        # Set up the popup menu
        popup_menu = self.custom_dl_popup_submenu(media_data_list)

        # Create the popup menu
        popup_menu.show_all()
        popup_menu.popup(
            None,
            None,
            None,
            None,
            1,
            Gtk.get_current_event_time(),
        )


    def custom_dl_popup_submenu(self, media_data_list=[]):

        """Called by several functions to create a sub-menu, within a parent
        popup menu.

        The sub-menu contains a list of downloads.CustomDLManager objects. If
        the user selects one, a custom download is started using settings from
        that object.

        Args:

            media_data_list (list): List of media data objects to custom
                download (may be an empty list)

        Return values:

            The sub-menu created

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 9402 custom_dl_popup_submenu')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Popup menu for the \'Custom download all\'' \
            + ' button in the Videos tab (not always visible)'
        )

        # Set up the popup menu
        popup_menu = Gtk.Menu()

        # Title
        choose_menu_item = Gtk.MenuItem.new_with_label(
             _('Choose a custom download:'),
        )
        popup_menu.append(choose_menu_item)
        choose_menu_item.set_sensitive(False)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # General Custom Download Manager
        general_menu_item = Gtk.MenuItem.new_with_label(
            self.app_obj.general_custom_dl_obj.name,
        )
        popup_menu.append(general_menu_item)
        general_menu_item.connect(
            'activate',
            self.on_custom_dl_menu_select,
            media_data_list,
            self.app_obj.general_custom_dl_obj.uid,
        )

        # The custom download manager usually used in the Classic Mode tab
        #   (but don't use it if it's the same as the previous one)
        if self.app_obj.classic_custom_dl_obj is not None \
        and self.app_obj.classic_custom_dl_obj \
        != self.app_obj.general_custom_dl_obj:

            classic_menu_item = Gtk.MenuItem.new_with_label(
                self.app_obj.classic_custom_dl_obj.name,
            )
            popup_menu.append(classic_menu_item)
            classic_menu_item.connect(
                'activate',
                self.on_custom_dl_menu_select,
                media_data_list,
                self.app_obj.classic_custom_dl_obj.uid,
            )

        # (Get a sorted list of custom download managers, excluding the default
        #   ones)
        manager_list = self.app_obj.compile_custom_dl_manager_list()
        if manager_list:

            # Separator
            popup_menu.append(Gtk.SeparatorMenuItem())

            # Other custom download managers
            for this_obj in manager_list:

                this_menu_item = Gtk.MenuItem.new_with_label(this_obj.name)
                popup_menu.append(this_menu_item)
                this_menu_item.connect(
                    'activate',
                    self.on_custom_dl_menu_select,
                    media_data_list,
                    this_obj.uid,
                )

        return popup_menu


    def delete_profile_popup_submenu(self):

        """Called by several functions to create a sub-menu, within a parent
        popup menu.

        The sub-menu contains a list of profile names (keys in
        mainapp.TartubeApp.profile_dict), If the user selects one, the profile
        is deleted

        Return values:

            The sub-menu created

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 9490 delete_profile_popup_submenu')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Extra items for the main window menu,' \
            + ' can be found in Media > Profiles > Delete profile > ...'
        )

        # Set up the popup menu
        popup_menu = Gtk.Menu()

        # Title
        choose_menu_item = Gtk.MenuItem.new_with_label(
             _('Choose a profile:'),
        )
        popup_menu.append(choose_menu_item)
        choose_menu_item.set_sensitive(False)

        if self.app_obj.profile_dict:

            # Separator
            popup_menu.append(Gtk.SeparatorMenuItem())

            for profile_name in sorted(self.app_obj.profile_dict):

                this_menu_item = Gtk.MenuItem.new_with_label(profile_name)
                popup_menu.append(this_menu_item)
                this_menu_item.connect(
                    'activate',
                    self.on_delete_profile_menu_select,
                    profile_name,
                )

        return popup_menu


    def switch_profile_popup_submenu(self):

        """Called by several functions to create a sub-menu, within a parent
        popup menu.

        The sub-menu contains a list of profile names (keys in
        mainapp.TartubeApp.profile_dict), If the user selects one, we switch
        to that profile, marking or unmarking items in the Video Index
        accordingly.

        Return values:

            The sub-menu created

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 9542 switch_profile_popup_submenu')

        # Set up the popup menu
        popup_menu = Gtk.Menu()

        # Title
        choose_menu_item = Gtk.MenuItem.new_with_label(
             _('Choose a profile:'),
        )
        popup_menu.append(choose_menu_item)
        choose_menu_item.set_sensitive(False)

        if self.app_obj.profile_dict:

            # Separator
            popup_menu.append(Gtk.SeparatorMenuItem())

            for profile_name in sorted(self.app_obj.profile_dict):

                this_menu_item = Gtk.MenuItem.new_with_label(profile_name)
                popup_menu.append(this_menu_item)
                this_menu_item.connect(
                    'activate',
                    self.on_switch_profile_menu_select,
                    profile_name,
                )

        return popup_menu


    def video_index_setup_contents_submenu(self, submenu, media_data_obj,
    only_child_videos_flag=False):

        """Called by self.video_index_popup_menu().

        Sets up a submenu for handling the contents of a channel, playlist
        or folder.

        Args:

            submenu (Gtk.Menu): The submenu to set up, currently empty

            media_data_obj (media.Channel, media.Playlist, media.Folder): The
                channel, playlist or folder whose contents should be modified
                by items in the sub-menu

            only_child_videos_flag (bool): Set to True when only a folder's
                child videos (not anything in its child channels, playlists or
                folders) should be modified by items in the sub-menu; False if
                all child objects should be modified

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 9596 video_index_setup_contents_submenu')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Extra items for the Video Index popup menu.' \
            + ' In the Videos tab, right-click any channel/playlist/folder'
        )

        mark_archived_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Mark as _archived'),
        )
        mark_archived_menu_item.connect(
            'activate',
            self.on_video_index_mark_archived,
            media_data_obj,
            only_child_videos_flag,
        )
        submenu.append(mark_archived_menu_item)

        mark_not_archive_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Mark as not a_rchived'),
        )
        mark_not_archive_menu_item.connect(
            'activate',
            self.on_video_index_mark_not_archived,
            media_data_obj,
            only_child_videos_flag,
        )
        submenu.append(mark_not_archive_menu_item)

        # Separator
        submenu.append(Gtk.SeparatorMenuItem())

        mark_bookmark_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Mark as _bookmarked'),
        )
        mark_bookmark_menu_item.connect(
            'activate',
            self.on_video_index_mark_bookmark,
            media_data_obj,
        )
        submenu.append(mark_bookmark_menu_item)
        if media_data_obj == self.app_obj.fixed_bookmark_folder:
            mark_bookmark_menu_item.set_sensitive(False)

        mark_not_bookmark_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Mark as not b_ookmarked'),
        )
        mark_not_bookmark_menu_item.connect(
            'activate',
            self.on_video_index_mark_not_bookmark,
            media_data_obj,
        )
        submenu.append(mark_not_bookmark_menu_item)

        # Separator
        submenu.append(Gtk.SeparatorMenuItem())

        mark_fav_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Mark as _favourite'),
        )
        mark_fav_menu_item.connect(
            'activate',
            self.on_video_index_mark_favourite,
            media_data_obj,
            only_child_videos_flag,
        )
        submenu.append(mark_fav_menu_item)
        if media_data_obj == self.app_obj.fixed_fav_folder:
            mark_fav_menu_item.set_sensitive(False)

        mark_not_fav_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Mark as not fa_vourite'),
        )
        mark_not_fav_menu_item.connect(
            'activate',
            self.on_video_index_mark_not_favourite,
            media_data_obj,
            only_child_videos_flag,
        )
        submenu.append(mark_not_fav_menu_item)

        # Separator
        submenu.append(Gtk.SeparatorMenuItem())

        mark_missing_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Mark as _missing'),
        )
        mark_missing_menu_item.connect(
            'activate',
            self.on_video_index_mark_missing,
            media_data_obj,
        )
        submenu.append(mark_missing_menu_item)
        # Only videos in channels/playlists can be marked as missing
        if isinstance(media_data_obj, media.Folder):
            mark_missing_menu_item.set_sensitive(False)

        mark_not_missing_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Mark as not m_issing'),
        )
        mark_not_missing_menu_item.connect(
            'activate',
            self.on_video_index_mark_not_missing,
            media_data_obj,
        )
        submenu.append(mark_not_missing_menu_item)
        # Only videos in channels/playlists can be marked as not missing
        #   (exception: the 'Missing Videos' folder)
        if isinstance(media_data_obj, media.Folder) \
        and media_data_obj != self.app_obj.fixed_missing_folder:
            mark_not_missing_menu_item.set_sensitive(False)

        # Separator
        submenu.append(Gtk.SeparatorMenuItem())

        mark_new_menu_item = Gtk.MenuItem.new_with_mnemonic(_('Mark as _new'))
        mark_new_menu_item.connect(
            'activate',
            self.on_video_index_mark_new,
            media_data_obj,
            only_child_videos_flag,
        )
        submenu.append(mark_new_menu_item)
        if media_data_obj == self.app_obj.fixed_new_folder:
            mark_new_menu_item.set_sensitive(False)

        mark_old_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Mark as not n_ew'),
        )
        mark_old_menu_item.connect(
            'activate',
            self.on_video_index_mark_not_new,
            media_data_obj,
            only_child_videos_flag,
        )
        submenu.append(mark_old_menu_item)

        # Separator
        submenu.append(Gtk.SeparatorMenuItem())

        mark_playlist_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Mark as in _waiting list'),
        )
        mark_playlist_menu_item.connect(
            'activate',
            self.on_video_index_mark_waiting,
            media_data_obj,
        )
        submenu.append(mark_playlist_menu_item)
        if media_data_obj == self.app_obj.fixed_waiting_folder:
            mark_playlist_menu_item.set_sensitive(False)

        mark_not_playlist_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('Mark as not in waiting _list'),
        )
        mark_not_playlist_menu_item.connect(
            'activate',
            self.on_video_index_mark_not_waiting,
            media_data_obj,
        )
        submenu.append(mark_not_playlist_menu_item)


    # (Video Index)


    def video_index_catalogue_reset(self, reselect_flag=False):

        """Can be called by anything.

        A convenient way to redraw the Video Index and Video Catalogue with a
        one-line call.

        Args:

            reselect_flag (bool): If True, the currently selected channel/
                playlist/folder in the Video Index is re-selected, which draws
                any child videos in the Video Catalogue

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 9778 video_index_catalogue_reset')

        # Reset the Video Index and Video Catalogue
        self.video_index_reset()
        self.video_catalogue_reset()
        self.video_index_populate()

        # Re-select the old selection, if required
        if reselect_flag and self.video_index_current_dbid is not None:

            dbid = self.video_index_current_dbid
            self.video_index_select_row(self.app_obj.media_reg_dict[dbid])


    def video_index_reset(self):

        """Can be called by anything.

        On the first call, sets up the widgets for the Video Index.

        On subsequent calls, replaces those widgets, ready for them to be
        filled with new data.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 9803 video_index_reset')

        # Reset IVs
        self.video_index_current_dbid = None
        if self.video_index_treeview:
            self.video_index_row_dict = {}
            # (Temporarily move key/value pairs in the 'current' IV into an
            #   'old' one; the subsequent call to self.video_index_populate()
            #   restores them)
            self.video_index_old_marker_dict \
            = self.video_index_marker_dict.copy()
            self.video_index_marker_dict = {}

        # Remove the old widgets
        if self.video_index_frame.get_child():
            self.video_index_frame.remove(
                self.video_index_frame.get_child(),
            )

        # Set up the widgets
        self.video_index_scrolled = Gtk.ScrolledWindow()
        self.video_index_frame.add(self.video_index_scrolled)
        self.video_index_scrolled.set_policy(
            Gtk.PolicyType.AUTOMATIC,
            Gtk.PolicyType.AUTOMATIC,
        )

        self.video_index_treeview = Gtk.TreeView()
        self.video_index_scrolled.add(self.video_index_treeview)
        self.video_index_treeview.set_can_focus(False)
        self.video_index_treeview.set_headers_visible(False)
        if not self.app_obj.show_tooltips_flag:
            self.video_index_treeview.set_tooltip_column(-1)
        else:
            self.video_index_treeview.set_tooltip_column(
                self.video_index_tooltip_column,
            )

        # (Detect right-clicks on the treeview)
        self.video_index_treeview.connect(
            'button-press-event',
            self.on_video_index_right_click,
        )

        # Setup up drag and drop. Drag and drop within the Video Index
        #   (dragging one channel/playlist/folder) is handled by spotting the
        #   selected row. Dragging videos from the Video Catalogue into a
        #   channel/playlist/folder is handled by storing a list of videos
        #   involved, at the start of the drag.
        # Therefore, we accept any incoming drag data, since it is not used
        #   anyway
        drag_target_list = [('text/plain', 0, 0)]
        self.video_index_treeview.enable_model_drag_source(
            # Mask of mouse buttons allowed to start a drag
            Gdk.ModifierType.BUTTON1_MASK,
            # 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.MOVE,
        )
        self.video_index_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,
        )
        self.video_index_treeview.connect(
            'drag-drop',
            self.on_video_index_drag_drop,
        )
        self.video_index_treeview.connect(
            'drag-data-received',
            self.on_video_index_drag_data_received,
        )

        self.video_index_treestore = Gtk.TreeStore(
            int,
            str, str,
            GdkPixbuf.Pixbuf, bool, str,
            int, int, int, bool,       # Column bindings
        )
        self.video_index_sortmodel = Gtk.TreeModelSort(
            self.video_index_treestore
        )
        self.video_index_treeview.set_model(self.video_index_sortmodel)
        self.video_index_sortmodel.set_sort_column_id(1, 0)
        # (Sort by name, by default)
        self.video_index_sortmodel.set_sort_func(
            1,
            self.video_index_auto_sort,
            None,
        )

        # (From https://stackoverflow.com/questions/49836499/
        #   make-only-some-rows-bold-in-a-gtk-treeview)
        # Column #5's properties are bound onto columns #6-#9
        #   6: style (int, Pango.Style.NORMAL or Pango.Style.ITALIC)
        #   7: weight (int, Pango.Weight.NORMAL or Pango.Weight.BOLD)
        #   8: underline (int, Pango.Underline.NONE, Pango.Underline.SINGLE or
        #       Pango.Underline.ERROR)
        #   9: strikethrough (bool)
        count = -1
        for item in [
            'hide', 'hide', 'hide', 'pixbuf', 'mark', 'text', 'bind_int',
            'bind_int', 'bind_int', 'bind_bool',
        ]:
            count += 1

            if item == 'hide' or item == 'bind_int':
                renderer_text = Gtk.CellRendererText()
                column_text = Gtk.TreeViewColumn(
                    None,
                    renderer_text,
                    text=count,
                )
                self.video_index_treeview.append_column(column_text)
                column_text.set_visible(False)

            elif item == 'pixbuf':
                renderer_pixbuf = Gtk.CellRendererPixbuf()
                column_pixbuf = Gtk.TreeViewColumn(
                    None,
                    renderer_pixbuf,
                    pixbuf=count,
                )
                self.video_index_treeview.append_column(column_pixbuf)

            elif item == 'text':
                renderer_text = Gtk.CellRendererText()
                # v2.4.457: 'style_set' etc commented out, as they seem to be
                #   unnecessary (and produce Gtk/Pango warnings on MS Windows)
                column_text = Gtk.TreeViewColumn(
                    None,
                    renderer_text,
                    text=count,
                    style=6,                # Bind italics to column #6
#                   style_set=True,
                    weight=7,               # Bind bold text to column #7
#                   weight_set=True,
                    underline=8,
#                   underline_set=True,     # Bind underline to column #8
                    strikethrough=9,
#                   strikethrough_set=True, # Bind strikethrough to column #9
                )
                self.video_index_treeview.append_column(column_text)

            elif item == 'mark' or item == 'bind_bool':
                renderer_toggle = Gtk.CellRendererToggle()
                column_toggle = Gtk.TreeViewColumn(
                    None,
                    renderer_toggle,
                    active=count,
                )
                self.video_index_treeview.append_column(column_toggle)
                if item == 'bind_bool':
                    column_toggle.set_visible(False)
                else:
                    renderer_toggle.set_sensitive(True)
                    renderer_toggle.set_activatable(True)
                    renderer_toggle.connect(
                        'toggled',
                        self.on_video_index_marker_toggled,
                    )
                    if not self.app_obj.show_marker_in_index_flag:
                        column_toggle.set_visible(False)

        selection = self.video_index_treeview.get_selection()
        selection.connect('changed', self.on_video_index_selection_changed)

        # Make the changes visible
        self.video_index_frame.show_all()


    def video_index_populate(self):

        """Can be called by anything.

        Repopulates the Video Index (assuming that it is already empty, either
        because Tartube has just started, or because of an earlier call to
        self.video_index_reset() ).

        After the call to this function, new rows can be added via a call to
        self.video_index_add_row().
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 9987 video_index_populate')

        for dbid in self.app_obj.container_top_level_list:

            media_data_obj = self.app_obj.media_reg_dict[dbid]
            if not media_data_obj:
                return self.app_obj.system_error(
                    207,
                    'Video Index initialisation failure',
                )

            else:
                self.video_index_setup_row(media_data_obj, None)

        # Make the changes visible
        self.video_index_treeview.show_all()

        # Update IVs. The calls to self.video_index_setup_row() will already
        #   have repopulated self.video_index_marker_dict
        self.video_index_old_marker_dict = {}


    def video_index_setup_row(self, media_data_obj, parent_pointer=None):

        """Called by self.video_index_populate(). Subsequently called by this
        function recursively.

        Adds a row to the Video Index.

        Args:

            media_data_obj (media.Video, media.Channel, media.Playlist,
                media.Folder): The media data object for this row

            parent_pointer (Gtk.TreeIter): None if the media data object has no
                parent. Otherwise, a pointer to the position of the parent
                object in the treeview

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 10028 video_index_setup_row')

        # Don't show a hidden folder, or any of its children
        if isinstance(media_data_obj, media.Folder) \
        and media_data_obj.hidden_flag:
            return

        # Prepare the icon
        pixbuf = self.video_index_get_icon(media_data_obj)
        if not pixbuf:
            return self.app_obj.system_error(
                208,
                'Video Index setup row request failed sanity check',
            )

        # Prepare the text style
        style, weight, underline, strike \
        = self.video_index_get_text_properties(media_data_obj)

        # Prepare the marker
        if not media_data_obj.dbid in self.video_index_marker_dict \
        and not media_data_obj.dbid in self.video_index_old_marker_dict:
            marker_flag = False
        else:
            marker_flag = True

        # Add a row to the treeview
        new_pointer = self.video_index_treestore.append(
            parent_pointer,
            [
                media_data_obj.dbid,
                media_data_obj.name,
                media_data_obj.fetch_tooltip_text(
                    self.app_obj,
                    self.tooltip_max_len,
                ),
                pixbuf,
                marker_flag,
                self.video_index_get_text(media_data_obj),
                style,
                weight,
                underline,
                strike,
            ],
        )

        # Create a reference to the row, so we can find it later
        tree_ref = Gtk.TreeRowReference.new(
            self.video_index_treestore,
            self.video_index_treestore.get_path(new_pointer),
        )

        # Update IVs
        self.video_index_row_dict[media_data_obj.dbid] = tree_ref
        if media_data_obj.dbid in self.video_index_marker_dict:
            self.video_index_marker_dict[media_data_obj.dbid] = tree_ref
        if media_data_obj.dbid in self.video_index_old_marker_dict:
            del self.video_index_old_marker_dict[media_data_obj.dbid]

        # Call this function recursively for any child objects that are
        #   channels, playlists or folders (videos are not displayed in the
        #   Video Index)
        for child_obj in media_data_obj.child_list:

            if not(isinstance(child_obj, media.Video)):
                self.video_index_setup_row(child_obj, new_pointer)


    def video_index_add_row(self, media_data_obj, no_select_flag=False):

        """Can be called by anything.

        Adds a row to the Video Index.

        Args:

            media_data_obj (media.Video, media.Channel, media.Playlist,
                media.Folder): The media data object for this row

            no_select_flag (bool): True if the new row should NOT be
                automatically selected, as if ordinarily would be

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 10113 video_index_add_row')

        # Don't add a hidden folder, or any of its children
        if media_data_obj.is_hidden():
            return

        # Prepare the icon
        pixbuf = self.video_index_get_icon(media_data_obj)
        if not pixbuf:
            return self.app_obj.system_error(
                209,
                'Video Index setup row request failed sanity check',
            )

        # Prepare the text style
        style, weight, underline, strike \
        = self.video_index_get_text_properties(media_data_obj)

        # Prepare the marker
        if not media_data_obj.dbid in self.video_index_marker_dict \
        and not media_data_obj.dbid in self.video_index_old_marker_dict:
            marker_flag = False
        else:
            marker_flag = True

        # Add a row to the treeview
        if media_data_obj.parent_obj:

            # This media data object has a parent, so we add a row inside the
            #   parent's row

            # Fetch the treeview reference to the parent media data object...
            parent_ref \
            = self.video_index_row_dict[media_data_obj.parent_obj.dbid]
            # ...and add the new object inside its parent
            tree_iter = self.video_index_treestore.get_iter(
                parent_ref.get_path(),
            )

            new_pointer = self.video_index_treestore.append(
                tree_iter,
                [
                    media_data_obj.dbid,
                    media_data_obj.name,
                    media_data_obj.fetch_tooltip_text(
                        self.app_obj,
                        self.tooltip_max_len,
                    ),
                    pixbuf,
                    marker_flag,
                    self.video_index_get_text(media_data_obj),
                    style,
                    weight,
                    underline,
                    strike,
                ],
            )

        else:

            # The media data object has no parent, so add a row to the
            #   treeview's top level
            new_pointer = self.video_index_treestore.append(
                None,
                [
                    media_data_obj.dbid,
                    media_data_obj.name,
                    media_data_obj.fetch_tooltip_text(
                        self.app_obj,
                        self.tooltip_max_len,
                    ),
                    pixbuf,
                    marker_flag,
                    self.video_index_get_text(media_data_obj),
                    style,
                    weight,
                    underline,
                    strike,
                ],
            )

        # Create a reference to the row, so we can find it later
        tree_ref = Gtk.TreeRowReference.new(
            self.video_index_treestore,
            self.video_index_treestore.get_path(new_pointer),
        )

        # Update IVs
        self.video_index_row_dict[media_data_obj.dbid] = tree_ref
        if media_data_obj.dbid in self.video_index_marker_dict:
            self.video_index_marker_dict[media_data_obj.dbid] = tree_ref
        if media_data_obj.dbid in self.video_index_old_marker_dict:
            del self.video_index_old_marker_dict[media_data_obj.dbid]

        if media_data_obj.parent_obj:

            # Expand rows to make the new media data object visible...
            self.video_index_treeview.expand_to_path(
                self.video_index_sortmodel.convert_child_path_to_path(
                    parent_ref.get_path(),
                ),
            )

        # Select the row (which clears the Video Catalogue)
        if not no_select_flag:
            selection = self.video_index_treeview.get_selection()
            selection.select_path(
                self.video_index_sortmodel.convert_child_path_to_path(
                    tree_ref.get_path(),
                ),
            )

        # Make the changes visible
        self.video_index_treeview.show_all()


    def video_index_delete_row(self, media_data_obj):

        """Can be called by anything.

        Removes a row from the Video Index.

        Args:

            media_data_obj (media.Video, media.Channel, media.Playlist,
                media.Folder): The media data object for this row

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 10243 video_index_delete_row')

        # Videos can't be shown in the Video Index
        if isinstance(media_data_obj, media.Video):
            return self.app_obj.system_error(
                210,
                'Video Index delete row request failed sanity check',
            )

        # During this procedure, ignore any changes to the selected row (i.e.
        #   don't allow self.on_video_index_selection_changed() to redraw the
        #   catalogue)
        self.ignore_video_index_select_flag = True

        # Remove the treeview row
        tree_ref = self.video_index_row_dict[media_data_obj.dbid]
        tree_path = tree_ref.get_path()
        tree_iter = self.video_index_treestore.get_iter(tree_path)
        self.video_index_treestore.remove(tree_iter)

        self.ignore_video_index_select_flag = False

        # If the deleted row was the previously selected one, the new selected
        #   row is the one just above/below that
        # In this situation, unselect the row and then redraw the Video
        #   Catalogue
        if self.video_index_current_dbid is not None \
        and self.video_index_current_dbid == media_data_obj.dbid:

            selection = self.video_index_treeview.get_selection()
            selection.unselect_all()

            self.video_index_current_dbid = None
            self.video_catalogue_reset()

        # Update IVs
        if media_data_obj.dbid in self.video_index_marker_dict:
            del self.video_index_marker_dict[media_data_obj.dbid]

        # Make the changes visible
        self.video_index_treeview.show_all()


    def video_index_select_row(self, media_data_obj):

        """Can be called by anything.

        Selects a row in the Video Index, as if the user had clicked it.

        Args:

            media_data_obj (media.Channel, media.Playlist or media.Folder):
                The media data object whose row should be selected

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 10300 video_index_select_row')

        # Cannot select a hidden folder, or any of its children
        if isinstance(media_data_obj, media.Video) \
        or media_data_obj.is_hidden():
            return self.app_obj.system_error(
                211,
                'Video Index select row request failed sanity check',
            )

        # Select the row, expanding the treeview path to make it visible, if
        #   necessary
        if media_data_obj.parent_obj:

            # Expand rows to make the new media data object visible...
            parent_ref \
            = self.video_index_row_dict[media_data_obj.parent_obj.dbid]

            self.video_index_treeview.expand_to_path(
                self.video_index_sortmodel.convert_child_path_to_path(
                    parent_ref.get_path(),
                ),
            )

        # Select the row
        tree_ref = self.video_index_row_dict[media_data_obj.dbid]

        selection = self.video_index_treeview.get_selection()
        selection.select_path(
            self.video_index_sortmodel.convert_child_path_to_path(
                tree_ref.get_path(),
            ),
        )


    def video_index_update_row_icon(self, media_data_obj):

        """Can be called by anything.

        The icons used in the Video Index must be changed when a media data
        object is marked (or unmarked) favourite, and when download options
        are applied/removed.

        This function updates a row in the Video Index to show the right icon.

        N.B. Because Gtk is not thread safe, this function must always be
        called from within GObject.timeout_add().

        Args:

            media_data_obj (media.Channel, media.Playlist or media.Folder):
                The media data object whose row should be updated

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 10356 video_index_update_row_icon')

        # Videos can't be shown in the Video Index
        if isinstance(media_data_obj, media.Video):
            return self.app_obj.system_error(
                212,
                'Video Index update row request failed sanity check',
            )

        # If media_data_obj is a hidden folder, then there's nothing to update
        if isinstance(media_data_obj, media.Folder) \
        and media_data_obj.hidden_flag:
            return

        # !!! DEBUG Git #523
        try:
            # Update the treeview row
            tree_ref = self.video_index_row_dict[media_data_obj.dbid]
            model = tree_ref.get_model()
            tree_path = tree_ref.get_path()
            tree_iter = model.get_iter(tree_path)
            model.set(tree_iter, 3, self.video_index_get_icon(media_data_obj))

            # Make the changes visible
            self.video_index_treeview.show_all()

        except:
            return self.app_obj.system_error(
                272,
                'Video Index update row request failed because row missing' \
                + ' from registry',
            )


    def video_index_update_row_text(self, media_data_obj):

        """Can be called by anything.

        The text used in the Video Index must be changed when a media data
        object is updated, including when a child video object is added or
        removed.

        This function updates a row in the Video Index to show the new text.

        N.B. Because Gtk is not thread safe, this function must always be
        called from within GObject.timeout_add().

        Args:

            media_data_obj (media.Channel, media.Playlist or media.Folder):
                The media data object whose row should be updated

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 10411 video_index_update_row_text')

        # Videos can't be shown in the Video Index
        if isinstance(media_data_obj, media.Video):
            return self.app_obj.system_error(
                213,
                'Video Index update row request failed sanity check',
            )

        # If media_data_obj is a hidden folder, then there's nothing to update
        if isinstance(media_data_obj, media.Folder) \
        and media_data_obj.hidden_flag:
            return

        # !!! DEBUG Git #523
        try:
            # Update the treeview row
            tree_ref = self.video_index_row_dict[media_data_obj.dbid]
            model = tree_ref.get_model()
            tree_path = tree_ref.get_path()
            tree_iter = model.get_iter(tree_path)
            model.set(tree_iter, 5, self.video_index_get_text(media_data_obj))

            style, weight, underline, strike \
            = self.video_index_get_text_properties(media_data_obj)
            model.set(tree_iter, 6, style)
            model.set(tree_iter, 7, weight)
            model.set(tree_iter, 8, underline)
            model.set(tree_iter, 9, strike)

            # Make the changes visible
            self.video_index_treeview.show_all()

        except:
            return self.app_obj.system_error(
                273,
                'Video Index update row request failed because row missing' \
                + ' from registry',
            )


    def video_index_update_row_tooltip(self, media_data_obj):

        """Can be called by anything.

        The tooltips used in the Video Index must be changed when a media data
        object is updated.

        This function updates the (hidden) row in the Video Index containing
        the text for tooltips.

        N.B. Because Gtk is not thread safe, this function must always be
        called from within GObject.timeout_add().

        Args:

            media_data_obj (media.Channel, media.Playlist or media.Folder):
                The media data object whose row should be updated

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 10473 video_index_update_row_tooltip')

        # Videos can't be shown in the Video Index
        if isinstance(media_data_obj, media.Video):
            return self.app_obj.system_error(
                214,
                'Video Index update row request failed sanity check',
            )

        # If media_data_obj is a hidden folder, then there's nothing to update
        if isinstance(media_data_obj, media.Folder) \
        and media_data_obj.hidden_flag:
            return

        # !!! DEBUG Git #523
        try:
            # Update the treeview row
            tree_ref = self.video_index_row_dict[media_data_obj.dbid]
            model = tree_ref.get_model()
            tree_path = tree_ref.get_path()
            tree_iter = model.get_iter(tree_path)
            model.set(
                tree_iter,
                2,
                media_data_obj.fetch_tooltip_text(
                    self.app_obj,
                    self.tooltip_max_len,
                ),
            )

            # Make the changes visible
            self.video_index_treeview.show_all()

        except:
            return self.app_obj.system_error(
                274,
                'Video Index update row request failed because row missing' \
                + ' from registry',
            )


    def video_index_get_icon(self, media_data_obj):

        """Called by self.video_index_setup_row(),
        .video_index_add_row() and .video_index_update_row_icon().

        Finds the icon to display on a Video Index row for the specified media
        data object.

        Looks up the GdkPixbuf which has already been created for that icon
        and returns it (or None, if the icon file is missing or if no
        corresponding pixbuf can be found.)

        Args:

            media_data_obj (media.Channel, media.Playlist or media.Folder):
                The media data object whose row should be updated

        Return values:

            A GdkPixbuf or None

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 10538 video_index_get_icon')

        icon = None
        if not self.app_obj.show_small_icons_in_index_flag:

            # (The favourite icon for the red folder is a different colour)
            alt_flag = False

            # Large icons, bigger selection
            if isinstance(media_data_obj, media.Channel):
                icon = 'channel_large'
            elif isinstance(media_data_obj, media.Playlist):
                icon = 'playlist_large'
            elif isinstance(media_data_obj, media.Folder):
                if media_data_obj.priv_flag:
                    icon = 'folder_private_large'
                    alt_flag = True
                elif media_data_obj.temp_flag:
                    icon = 'folder_temp_large'
                elif media_data_obj.fixed_flag:
                    icon = 'folder_fixed_large'
                else:
                    icon = 'folder_large'

            # (Apply overlays)
            if media_data_obj.dbid in self.app_obj.container_unavailable_dict:
                icon += '_tl'
            if media_data_obj.dl_no_db_flag \
            or media_data_obj.dl_disable_flag \
            or media_data_obj.dl_sim_flag:
                icon += '_tr'
            if media_data_obj.fav_flag:
                if not alt_flag:
                    icon += '_bl'
                else:
                    icon += '_bl_alt'
            if media_data_obj.options_obj:
                icon += '_br'

        else:

            # Small icons, smaller selection
            if isinstance(media_data_obj, media.Channel):
                icon = 'channel_small'
            elif isinstance(media_data_obj, media.Playlist):
                icon = 'playlist_small'
            elif isinstance(media_data_obj, media.Folder):
                if media_data_obj.priv_flag:
                    icon = 'folder_red_small'
                elif media_data_obj.temp_flag:
                    icon = 'folder_blue_small'
                elif media_data_obj.fixed_flag:
                    icon = 'folder_green_small'
                else:
                    icon = 'folder_small'

        if icon is not None and icon in self.icon_dict:
            return self.pixbuf_dict[icon]
        else:
            # Invalid 'icon', or file not found
            return None


    def video_index_get_text(self, media_data_obj):

        """Called by self.video_index_setup_row(), .video_index_add_row() and
        .video_index_update_row_text().

        Sets the text to display on a Video Index row for the specified media
        data object.

        Args:

            media_data_obj (media.Channel, media.Playlist or media.Folder):
                A media data object visible in the Video Index

        Return values:

            A string

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 10621 video_index_get_text')

        text = utils.shorten_string(
            media_data_obj.nickname,
            self.short_string_max_len,
        )

        if not self.app_obj.complex_index_flag:

            if media_data_obj.dl_count:
                text += ' (' + str(media_data_obj.new_count) + '/' \
                + str(media_data_obj.dl_count) + ')'

        else:

            ignore_me = _(
                'TRANSLATOR\'S NOTE: V = number of videos B = (number of' \
                + ' videos) bookmarked D = downloaded F = favourite' \
                + ' L = live/livestream M = missing N = new W = in waiting' \
                + ' list E = (number of) errors W = warnings. Choose any' \
                + ' abbreviation you like.',
            )

            if media_data_obj.vid_count:
                text += '\n' + _('V:') + str(media_data_obj.vid_count) \
                + ' ' + _('B:') + str(media_data_obj.bookmark_count) \
                + ' ' + _('D:') + str(media_data_obj.dl_count) \
                + ' ' + _('F:') + str(media_data_obj.fav_count) \
                + ' ' + _('L:') + str(media_data_obj.live_count) \
                + ' ' + _('M:') + str(media_data_obj.missing_count) \
                + ' ' + _('N:') + str(media_data_obj.new_count) \
                + ' ' + _('W:') + str(media_data_obj.waiting_count)

            if not isinstance(media_data_obj, media.Folder) \
            and (media_data_obj.error_list or media_data_obj.warning_list):

                if not media_data_obj.vid_count:
                    text += '\n'
                else:
                    text += ' '

                text += _('E:') + str(len(media_data_obj.error_list)) \
                + ' ' + _('W:') + str(len(media_data_obj.warning_list))

        return text


    def video_index_get_text_properties(self, media_data_obj):

        """Called by self.video_index_setup_row(), .video_index_add_row and
        .video_index_update_row_text().

        Returns a list of text style properties for displaying the name of the
        specified media data object.

        Args:

            media_data_obj (media.Channel, media.Playlist, media.Folder): The
                item selected in the Video Index

        Return values:

            A list in the form (style, weight, underline, strike):

                style (int): Pango.Style.NORMAL or Pango.Style.ITALIC
                weight (int): Pango.Weight.NORMAL or Pango.Weight.BOLD
                underline (int): Pango.Underline.NONE, Pango.Underline.SINGLE
                    or Pango.Underline.ERROR
                strikethrough (bool): True for strikethrough, False otherwise

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 10694 video_index_get_text_properties')

        style = Pango.Style.NORMAL
        weight = Pango.Weight.NORMAL
        underline = Pango.Underline.NONE
        strike = False

        # When large icons are visible, we only change the Pango.Weight
        if not self.app_obj.show_small_icons_in_index_flag:

            # If marked new (unwatched), show as bold text
            if media_data_obj.new_count:
                weight = Pango.Weight.BOLD

        # When smaller icons are visible, use italics and strikethrough
        else:

            # If an external directory is disabled, show as strikethrough
            if media_data_obj.dbid in self.app_obj.container_unavailable_dict:
                strike = True

            else:
                # If marked new (unwatched), show as bold text
                if media_data_obj.new_count:
                    weight = Pango.Weight.BOLD

                # If adding videos to the database or checking/downloading
                #   is disabled, show as italic text (perhaps in addition
                #   to bold text)
                if media_data_obj.dl_no_db_flag:
                    style = Pango.Style.ITALIC
                    underline = Pango.Underline.ERROR
                elif media_data_obj.dl_disable_flag:
                    style = Pango.Style.ITALIC
                    underline = Pango.Underline.SINGLE
                elif media_data_obj.dl_sim_flag:
                    style = Pango.Style.ITALIC

        return style, weight, underline, strike


    def video_index_set_marker(self, dbid=None):

        """Can be called by anything.

        Sets the marker on a specified row of the Video Index (or on all of
        them for which the markes are allowed to be set).

        Args:

            dbid (str): The .dbid of a media.Channel, media.Playlist or
                media.Folder; a key in mainapp.TartubeApp.container_reg_dict

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 10750 video_index_set_marker')

        old_size = len(self.video_index_marker_dict)

        container_list = []
        if dbid is None:

            # Set all markers in the Video Index
            container_list = list(self.video_index_row_dict.keys())

        else:

            # Set the marker on the row for the specified channel/playlist/
            #   folder
            container_list = [ dbid ]

        for this_dbid in container_list:

            # System folders cannot be marked
            # Channels/playlists/folders for which checking and downloading is
            #   disabled can't be marked
            media_data_obj = self.app_obj.media_reg_dict[this_dbid]
            if (
                not isinstance(media_data_obj, media.Folder) \
                or not media_data_obj.priv_flag
            ) and not media_data_obj.dl_disable_flag:

                tree_ref = self.video_index_row_dict[this_dbid]
                model = tree_ref.get_model()
                tree_path = tree_ref.get_path()
                tree_iter = model.get_iter(tree_path)

                model.set(tree_iter, 4, True)

                self.video_index_marker_dict[this_dbid] = tree_ref

        if not old_size and self.video_index_marker_dict:
            # Update labels on the 'Check all' button, etc
            # The True argument skips the check for the existence of a progress
            #   bar
            self.hide_progress_bar(True)


    def video_index_reset_marker(self, dbid=None):

        """Can be called by anything.

        Resets the marker on a specified row of the Video Index (or on all of
        them).

        Args:

            dbid (str): The .dbid of a media.Channel, media.Playlist or
                media.Folder; a key in mainapp.TartubeApp.container_reg_dict

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 10808 video_index_reset_marker')

        old_size = len(self.video_index_marker_dict)

        if dbid is None:

            # Reset all markers in the Video Index
            for tree_ref in self.video_index_row_dict.values():
                model = tree_ref.get_model()
                tree_path = tree_ref.get_path()
                tree_iter = model.get_iter(tree_path)

                model.set(tree_iter, 4, False)

            self.video_index_marker_dict = {}

        elif dbid in self.app_obj.container_reg_dict:

            # Reset the marker on the row for the specified channel/playlist/
            #   folder

            # !!! DEBUG GIT #558
            try:
                tree_ref = self.video_index_row_dict[dbid]
                model = tree_ref.get_model()
                tree_path = tree_ref.get_path()
                tree_iter = model.get_iter(tree_path)

                model.set(tree_iter, 4, False)

            except:
                return self.app_obj.system_error(
                    276,
                    'Video Index reset marker request failed because row' \
                    + ' missing from registry',
                )

            if dbid in self.video_index_marker_dict:
                del self.video_index_marker_dict[dbid]

        if old_size and not self.video_index_marker_dict:

            # Update labels on the 'Check all' button, etc
            # The True argument skips the check for the existence of a progress
            #   bar
            self.hide_progress_bar(True)


    # (Video Catalogue)


    def video_catalogue_reset(self):

        """Can be called by anything.

        On the first call, sets up the widgets for the Video Catalogue. On
        subsequent calls, replaces those widgets, ready for them to be filled
        with new data.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 10869 video_catalogue_reset')

        # If not called by self.setup_videos_tab()...
        if self.catalogue_frame.get_child():
            self.catalogue_frame.remove(self.catalogue_frame.get_child())

        # Reset IVs (when called by anything)
        self.video_catalogue_dict = {}
        self.video_catalogue_temp_list = []
        self.catalogue_listbox = None
        self.catalogue_grid = None
        self.catalogue_grid_expand_flag = False
        # (self.catalogue_grid_column_count is not set here)
        self.catalogue_grid_row_count = 1

        # Set up the widgets
        self.catalogue_scrolled = Gtk.ScrolledWindow()
        self.catalogue_frame.add(self.catalogue_scrolled)

        if self.app_obj.catalogue_mode_type != 'grid':

            self.catalogue_scrolled.set_policy(
                Gtk.PolicyType.AUTOMATIC,
                Gtk.PolicyType.AUTOMATIC,
            )

            self.catalogue_listbox = Gtk.ListBox()
            self.catalogue_scrolled.add(self.catalogue_listbox)
            self.catalogue_listbox.set_can_focus(False)
            self.catalogue_listbox.set_selection_mode(
                Gtk.SelectionMode.MULTIPLE,
            )
            # (Without this line, it's not possible to unselect rows by
            #   clicking on one of them)
            self.catalogue_listbox.set_activate_on_single_click(False)

            # (Drag and drop is now handled by mainwin.CatalogueRow directly)

            # Set up automatic sorting of rows in the listbox
            self.catalogue_listbox.set_sort_func(
                self.video_catalogue_generic_auto_sort,
                None,
                False,
            )

        else:

            # (No horizontal scrolling in grid mode)
            self.catalogue_scrolled.set_policy(
                Gtk.PolicyType.NEVER,
                Gtk.PolicyType.AUTOMATIC,
            )

            self.catalogue_grid = Gtk.Grid()
            self.catalogue_scrolled.add(self.catalogue_grid)
            self.catalogue_grid.set_can_focus(False)
            self.catalogue_grid.set_border_width(self.spacing_size)
            self.catalogue_grid.set_column_spacing(self.spacing_size)
            self.catalogue_grid.set_row_spacing(self.spacing_size)

            # (Video selection is handled by custom code, not calls to Gtk)

            # (Drag and drop is now handled by mainwin.CatalogueGridBox
            #   directly)

            # (Automatic sorting is handled by custom code, not calls to Gtk)

        # Make the changes visible
        self.catalogue_frame.show_all()


    def video_catalogue_redraw_all(self, dbid, page_num=1,
    reset_scroll_flag=False, no_cancel_filter_flag=False):

        """Can be called by anything.

        When the user clicks on a media data object in the Video Index (a
        channel, playlist or folder), this function is called to replace the
        contents of the Video Catalogue with some or all of the video objects
        stored as children in that channel, playlist or folder.

        Depending on the value of self.catalogue_mode, the Video Catalogue
        consists of a list of mainwin.SimpleCatalogueItem or
        mainwin.ComplexCatalogueItem objects, one for each row in the
        Gtk.ListBox; or a mainwin.GridCatalogueItem, one for each gridbox in
        the Gtk.Grid. Each row/gridbox corresponds to a single video.

        The video catalogue splits its video list into pages (as Gtk struggles
        with a list of hundreds, or thousands, of videos). Only videos on the
        specified page (or on the current page, if no page is specified) are
        drawn. If mainapp.TartubeApp.catalogue_page_size is set to zero, all
        videos are drawn on a single page.

        If a filter has been applied, only videos matching the search text
        are visible in the catalogue.

        This function clears the previous contents of the Gtk.ListBox/Gtk.Grid
        and resets IVs.

        Then, it adds new rows to the Gtk.ListBox, or new gridboxes to the
        Gtk.Grid, and creates a new mainwin.SimpleCatalogueItem,
        mainwin.ComplexCatalogueItem or mainwin.GridCatalogueItem object for
        each video on the page.

        Args:

            dbid (str): The selected media data object's .dbid; one of the keys
                in mainapp.TartubeApp.container_reg_dict

            page_num (int): The number of the page to be drawn (a value in the
                range 1 to self.catalogue_toolbar_last_page)

            reset_scroll_flag (bool): True if the vertical scrollbar must be
                reset (for example, when switching between channels/playlists/
                folders)

            no_cancel_filter_flag (bool): By default, if the filter is applied,
                it is cancelled by this function. Set to True if the calling
                function doesn't want that (for example, because it has just
                set up the filter, and wants to show only matching videos)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 10993 video_catalogue_redraw_all')

        # If actually switching to a different channel/playlist/folder, or a
        #   different page on the same channel/playlist/folder, must reset the
        #   scrollbars later in the function
        if not reset_scroll_flag:
            if self.video_index_current_dbid is None \
            or self.video_index_current_dbid != dbid \
            or self.catalogue_toolbar_current_page != page_num:
                reset_scroll_flag = True

        # The item selected in the Video Index is a media.Channel,
        #   media.playlist or media.Folder object
        if not dbid in self.app_obj.container_reg_dict:

            return self.app_obj.system_error(
                215,
                'Cannot redraw Video Catalogue because container is missing' \
                + ' from database',
            )

        container_obj = self.app_obj.media_reg_dict[dbid]

        # Sanity check - the selected item should not be a media.Video object
        if not container_obj or (isinstance(container_obj, media.Video)):
            return self.app_obj.system_error(
                216,
                'Videos should not appear in the Video Index',
            )

        # Formerly, the selected container's children were only re-sorted when
        #   the sort mode had changed
        # Due to continuing and hard-to-diagnose sort problems, just sort each
        #   container's children every time the Video Catalogue is redrawn
        container_obj.sort_children(self.app_obj)

        # Reset the previous contents of the Video Catalogue, if any, and reset
        #   IVs
        self.video_catalogue_reset()
        # Temporarily reset widgets in the Video Catalogue toolbar (in case
        #   something goes wrong, or in case drawing the page takes a long
        #   time)
        self.video_catalogue_toolbar_reset()
        # If a filter had recently been applied, reset IVs to cancel it (unless
        #   the calling function doesn't want that)
        # This makes sure that the filter is always reset when the user clicks
        #   on a different channel/playlist/folder in the Video Index
        if not no_cancel_filter_flag:
            self.video_catalogue_filtered_flag = False
            self.video_catalogue_filtered_list = []

        # The selected media data object has any number of child media data
        #   objects, but this function is only interested in those that are
        #   media.Video objects
        video_count = 0
        page_size = self.app_obj.catalogue_page_size
        # If the filter has been applied, use the prepared list of child videos
        #   specified by the IV...
        if self.video_catalogue_filtered_flag:
            child_list = self.video_catalogue_filtered_list.copy()
        # ...otherwise use all child videos that are downloaded/undownloaded/
        #   blocked (according to current settings)
        else:
            child_list = container_obj.get_visible_videos(self.app_obj)

        for child_obj in child_list:
            if isinstance(child_obj, media.Video):

                # (We need the number of child videos when we update widgets in
                #   the toolbar)
                video_count += 1

                # Only draw videos on this page. If the page size is zero, all
                #   videos are drawn on a single page
                if page_size \
                and (
                    video_count <= ((page_num - 1) * page_size) \
                    or video_count > (page_num * page_size)
                ):
                    # Don't draw the video on this page
                    continue

                # Create a new catalogue item object for the video
                if self.app_obj.catalogue_mode_type == 'simple':
                    catalogue_item_obj = SimpleCatalogueItem(self, child_obj)
                elif self.app_obj.catalogue_mode_type == 'complex':
                    catalogue_item_obj = ComplexCatalogueItem(self, child_obj)
                else:
                    catalogue_item_obj = GridCatalogueItem(self, child_obj)

                # Update IVs
                self.video_catalogue_dict[catalogue_item_obj.dbid] = \
                catalogue_item_obj

                # Add the video to the Video Catalogue
                if self.app_obj.catalogue_mode_type != 'grid':

                    # Add a row to the Gtk.ListBox

                    # Instead of using Gtk.ListBoxRow directly, use a wrapper
                    #   class so we can quickly retrieve the video displayed on
                    #   each row
                    wrapper_obj = CatalogueRow(self, child_obj)
                    self.catalogue_listbox.add(wrapper_obj)

                    # Populate the row with widgets...
                    catalogue_item_obj.draw_widgets(wrapper_obj)
                    # ...and give them their initial appearance
                    catalogue_item_obj.update_widgets()

                else:

                    # Add a gridbox to the Gtk.Grid

                    # Instead of using Gtk.Frame directly, use a wrapper class
                    #   so we can quickly retrieve the video displayed on each
                    #   row
                    wrapper_obj = CatalogueGridBox(self, child_obj)

                    # (Place the first video at 0, 0, so in that case, 'count'
                    #   must be 0)
                    count = len(self.video_catalogue_dict) - 1
                    y_pos = int(count / self.catalogue_grid_column_count)
                    x_pos = count % self.catalogue_grid_column_count
                    self.video_catalogue_grid_attach_gridbox(
                        wrapper_obj,
                        x_pos,
                        y_pos,
                    )

                    # Populate the gridbox with widgets...
                    catalogue_item_obj.draw_widgets(wrapper_obj)
                    # ...and give them their initial appearance
                    catalogue_item_obj.update_widgets()

                    # Gridboxes could be made (un)expandable, depending on the
                    #   number of gridboxes now on the grid
                    self.video_catalogue_grid_check_expand()

        # Update widgets in the toolbar, now that we know the number of child
        #   videos
        self.video_catalogue_toolbar_update(page_num, video_count)

        # In all cases, sensitise the scroll up/down toolbar buttons
        self.catalogue_scroll_up_button.set_sensitive(True)
        self.catalogue_scroll_down_button.set_sensitive(True)
        # Reset the scrollbar, if required
        if reset_scroll_flag:
            self.catalogue_scrolled.get_vadjustment().set_value(0)

        # Procedure complete
        if self.app_obj.catalogue_mode_type != 'grid':
            self.catalogue_listbox.show_all()
        else:
            self.catalogue_grid.show_all()


    def video_catalogue_update_video(self, video_obj):

        """Can be called by anything.

        This function is called with a media.Video object. If that video is
        already visible in the Video Catalogue, updates the corresponding
        mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem (which
        updates the widgets in the Gtk.ListBox), or the corresponding
        mainwin.GridCatalogueItem (which updates the widgets in the Gtk.Grid).

        If the video is now yet visible in the Video Catalogue, but should be
        drawn on the current page, creates a new mainwin.SimpleCatalogueItem,
        mainwin.ComplexCatalogueItem or mainwin.GridCatalogueItem and adds it
        to the Gtk.ListBox or Gtk.Grid, removing an existing catalogue item to
        make room, if necessary.

        N.B. Because Gtk is not thread safe, this function must always be
        called from within GObject.timeout_add().

        Args:

            video_obj (media.Video): The video to update

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 11176 video_catalogue_update_video')

        app_obj = self.app_obj

        # Is the video's parent channel, playlist or folder the one that is
        #   currently selected in the Video Index? If not, the video is not
        #   currently displayed in the Video Catalogue
        if self.video_index_current_dbid is None \
        or video_obj.parent_obj is None \
        or not (
            self.video_index_current_dbid == video_obj.parent_obj.dbid
            or self.video_index_current_dbid == app_obj.fixed_all_folder.dbid
            or (
                self.video_index_current_dbid \
                == app_obj.fixed_bookmark_folder.dbid \
                and video_obj.bookmark_flag
            ) or (
                self.video_index_current_dbid \
                == app_obj.fixed_fav_folder.dbid \
                and video_obj.fav_flag
            ) or (
                self.video_index_current_dbid \
                == app_obj.fixed_live_folder.dbid \
                and video_obj.live_mode
            ) or (
                self.video_index_current_dbid \
                == app_obj.fixed_missing_folder.dbid
                and video_obj.missing_flag
            ) or (
                self.video_index_current_dbid == app_obj.fixed_new_folder.dbid
                and video_obj.new_flag
            ) or (
                self.video_index_current_dbid \
                == app_obj.fixed_recent_folder.dbid
                and video_obj in app_obj.fixed_recent_folder.child_list
            ) or (
                self.video_index_current_dbid \
                == app_obj.fixed_waiting_folder.dbid \
                and video_obj.waiting_flag
            )
        ):
            return

        # Does a mainwin.SimpleCatalogueItem, mainwin.ComplexCatalogueItem or
        #   mainwin.GridCatalogueItem object already exist for this video?
        already_exist_flag = False
        if video_obj.dbid in self.video_catalogue_dict:

            already_exist_flag = True

            # Update the catalogue item object, which updates the widgets in
            #   the Gtk.ListBox/Gtk.Grid
            catalogue_item_obj = self.video_catalogue_dict[video_obj.dbid]
            catalogue_item_obj.update_widgets()

        # Now, deal with the video's position in the catalogue. If a catalogue
        #   item object already existed, its position may have changed
        #   (perhaps staying on the current page, perhaps moving to another)
        container_obj = app_obj.media_reg_dict[self.video_index_current_dbid]

        # Find the Video Catalogue page on which this video should be shown
        page_num = 1
        current_page_num = self.catalogue_toolbar_current_page
        page_size = app_obj.catalogue_page_size
        # At the same time, reduce the parent container's list of children,
        #   eliminating those which are media.Channel, media.Playlist and
        #   media.Folder objects
        # Exclude any downloaded/undownloaded/blocked videos, according to
        #   current settings
        sibling_video_list = []

        # If the filter has been applied, use the prepared list of child videos
        #   specified by the IV...
        if self.video_catalogue_filtered_flag:
            child_list = self.video_catalogue_filtered_list.copy()
        # ...otherwise use all child videos that are downloaded/undownloaded/
        #   blocked (according to current settings)
        else:
            child_list = container_obj.get_visible_videos(self.app_obj)

        for child_obj in child_list:
            if isinstance(child_obj, media.Video) \
            and (
                (
                    app_obj.catalogue_draw_downloaded_flag \
                    and child_obj.dl_flag
                ) or (
                    app_obj.catalogue_draw_undownloaded_flag \
                    and not child_obj.dl_flag
                ) or (
                    app_obj.catalogue_draw_blocked_flag \
                    and child_obj.block_flag
                )
            ):
                sibling_video_list.append(child_obj)

                # (If the page size is 0, then all videos are drawn on one
                #   page, i.e. the current value of page_num, which is 1)
                if child_obj == video_obj and page_size:
                    page_num = int(
                        (len(sibling_video_list) - 1) / page_size
                    ) + 1

        sibling_video_count = len(sibling_video_list)

        # Decide whether to move any catalogue items from this page and, if so,
        #   what (if anything) should be moved into their place
        # If a catalogue item was already visible for this video, then the
        #   video might need to be displayed on a different page, its position
        #   on this page being replaced by a different video
        # If a catalogue item was not already visible for this video, and if
        #   it should be drawn on this page or any previous page, then we
        #   need to remove a catalogue item from this page and replace it with
        #   another
        if (already_exist_flag and page_num != current_page_num) \
        or (not already_exist_flag and page_num <= current_page_num):

            # Compile a dictionary of videos which are currently visible on
            #   this page
            visible_dict = {}
            for catalogue_item in self.video_catalogue_dict.values():
                visible_dict[catalogue_item.video_obj.dbid] \
                = catalogue_item.video_obj

            # Check the videos which should be visible on this page. This
            #   code block leaves us with 'visible_dict' containing videos
            #   that should no longer be visible on the page, and
            #   'missing_dict' containing videos that should be visible on
            #   the page, but are not
            missing_dict = {}
            for index in range (
                ((current_page_num - 1) * page_size),
                (current_page_num * page_size),
            ):
                if index < sibling_video_count:
                    child_obj = sibling_video_list[index]
                    if not child_obj.dbid in visible_dict:
                        missing_dict[child_obj.dbid] = child_obj
                    else:
                        del visible_dict[child_obj.dbid]

            # Remove any catalogue items for videos that shouldn't be
            #   visible, but still are
            for dbid in visible_dict:
                catalogue_item_obj = self.video_catalogue_dict[dbid]

                if self.app_obj.catalogue_mode_type != 'grid':

                    self.catalogue_listbox.remove(
                        catalogue_item_obj.catalogue_row,
                    )

                else:

                    self.catalogue_grid.remove(
                        catalogue_item_obj.catalogue_gridbox,
                    )

                del self.video_catalogue_dict[dbid]

            # Add any new catalogue items for videos which should be
            #   visible, but aren't
            for dbid in missing_dict:

                # Get the media.Video object
                missing_obj = app_obj.media_reg_dict[dbid]

                # Create a new catalogue item
                self.video_catalogue_insert_video(missing_obj)

        # Update widgets in the toolbar
        self.video_catalogue_toolbar_update(
            self.catalogue_toolbar_current_page,
            sibling_video_count,
        )

        # Sort the visible list
        if self.app_obj.catalogue_mode_type != 'grid':

            # Force the Gtk.ListBox to sort its rows, so that videos are
            #   displayed in the correct order
            self.catalogue_listbox.invalidate_sort()

        else:

            # After sorting gridboxes, rearrange them on the Gtk.Grid
            self.video_catalogue_grid_rearrange()

        # Procedure complete
        if self.app_obj.catalogue_mode_type != 'grid':
            self.catalogue_listbox.show_all()
        else:
            self.catalogue_grid.show_all()


    def video_catalogue_insert_video(self, video_obj):

        """Called by self.video_catalogue_update_video() (only).

        Adds a new mainwin.SimpleCatalogueItem, mainwin.ComplexCatalogueItem
        or mainwin.GridCatalogueItem to the Video Catalogue. Each catalogue
        item handles a single video.

        Args:

            video_obj (media.Video): The video for which a new catalogue item
                should be created

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 11386 video_catalogue_insert_video')

        # Create the new catalogue item
        if self.app_obj.catalogue_mode_type == 'simple':
            catalogue_item_obj = SimpleCatalogueItem(self, video_obj)
        elif self.app_obj.catalogue_mode_type == 'complex':
            catalogue_item_obj = ComplexCatalogueItem(self, video_obj)
        else:
            catalogue_item_obj = GridCatalogueItem(self, video_obj)

        self.video_catalogue_dict[video_obj.dbid] = catalogue_item_obj

        if self.app_obj.catalogue_mode_type != 'grid':

            # Add a row to the Gtk.ListBox

            # Instead of using Gtk.ListBoxRow directly, use a wrapper class so
            #   we can quickly retrieve the video displayed on each row
            wrapper_obj = CatalogueRow(self, video_obj)

            # On rare occasions, the line below sometimes causes a warning,
            #   'Accessing a sequence while it is being sorted or seached is
            #   not allowed'
            # If this happens, add it to a temporary list of rows to be added
            #   to the listbox by self.video_catalogue_retry_insert_items()
            try:
                self.catalogue_listbox.add(wrapper_obj)
            except:
                self.video_catalogue_temp_list.append(wrapper_obj)

            # Populate the row with widgets...
            catalogue_item_obj.draw_widgets(wrapper_obj)
            # ...and give them their initial appearance
            catalogue_item_obj.update_widgets()

        else:

            # Add a gridbox to the Gtk.Grid

            # Instead of using Gtk.Frame directly, use a wrapper class so we
            #   can quickly retrieve the video displayed on each row
            wrapper_obj = CatalogueGridBox(self, video_obj)

            # (Place the first video at 0, 0, so in that case, count must be 0)
            count = len(self.video_catalogue_dict) - 1
            y_pos = int(count / self.catalogue_grid_column_count)
            x_pos = count % self.catalogue_grid_column_count
            self.video_catalogue_grid_attach_gridbox(
                wrapper_obj,
                x_pos,
                y_pos,
            )

            # Populate the gridbox with widgets...
            catalogue_item_obj.draw_widgets(wrapper_obj)
            # ...and give them their initial appearance
            catalogue_item_obj.update_widgets()

            # Gridboxes could be made (un)expandable, depending on the number
            #   of gridboxes now on the grid, and the number of columns allowed
            #   in the grid
            self.video_catalogue_grid_check_expand()


    def video_catalogue_retry_insert_items(self):

        """Called by mainapp.TartubeApp.script_fast_timer_callback().

        If an earlier call to self.video_catalogue_insert_video() failed, one
        or more CatalogueRow objects are waiting to be added to the Video
        Catalogue. Add them, if so.

        (Not called when videos are arranged on a grid.)
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 11462 video_catalogue_retry_insert_items')

        while self.video_catalogue_temp_list:

            wrapper_obj = self.video_catalogue_temp_list.pop()

            try:
                self.catalogue_listbox.add(wrapper_obj)
            except:
                # Still can't add the row; try again later
                self.video_catalogue_temp_list.append(wrapper_obj)
                return

        # All items added. Force the Gtk.ListBox to sort its rows, so that
        #   videos are displayed in the correct order
        self.catalogue_listbox.invalidate_sort()

        # Procedure complete
        self.catalogue_listbox.show_all()


    def video_catalogue_delete_video(self, video_obj):

        """Can be called by anything.

        This function is called with a media.Video object. If that video is
        already visible in the Video Catalogue, removes the corresponding
        mainwin.SimpleCatalogueItem, mainwin.ComplexCatalogueItem or
        mainwin.GridCatalogueItem.

        If the current page was already full of videos, create a new
        catalogue item to fill the gap.

        Args:

            video_obj (media.Video): The video to remove

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 11504 video_catalogue_delete_video')

        # Is the video's parent channel, playlist or folder the one that is
        #   currently selected in the Video Index? If not, the video is not
        #   displayed in the Video Catalogue
        app_obj = self.app_obj

        if self.video_index_current_dbid is None:
            return

        elif self.video_index_current_dbid != video_obj.parent_obj.dbid \
        and self.video_index_current_dbid != app_obj.fixed_all_folder.dbid \
        and (
            self.video_index_current_dbid \
            != app_obj.fixed_bookmark_folder.dbid \
            or not video_obj.bookmark_flag
        ) and (
            self.video_index_current_dbid != app_obj.fixed_fav_folder.dbid \
            or not video_obj.fav_flag
        ) and (
            self.video_index_current_dbid != app_obj.fixed_live_folder.dbid \
            or not video_obj.live_mode
        ) and (
            self.video_index_current_dbid \
            != app_obj.fixed_missing_folder.dbid \
            or not video_obj.missing_flag
        ) and (
            self.video_index_current_dbid != app_obj.fixed_new_folder.dbid \
            or not video_obj.new_flag
        ) and (
            self.video_index_current_dbid != app_obj.fixed_recent_folder.dbid \
            or video_obj in app_obj.fixed_recent_folder.child_list
        ) and (
            self.video_index_current_dbid \
            != app_obj.fixed_waiting_folder.dbid \
            or not video_obj.waiting_flag
        ):
            return

        # Does a mainwin.SimpleCatalogueItem, mainwin.ComplexCatalogueItem or
        #   mainwin.GridCatalogueItem object exist for this video?
        if video_obj.dbid in self.video_catalogue_dict:

            # Remove the catalogue item object and its mainwin.CatalogueRow or
            #   mainwin.CatalogueGridBox object (the latter being a wrapper for
            #   Gtk.ListBoxRow or Gtk.Frame)
            catalogue_item_obj = self.video_catalogue_dict[video_obj.dbid]

            # Remove the row from the Gtk.ListBox or Gtk.Grid
            if self.app_obj.catalogue_mode_type != 'grid':

                self.catalogue_listbox.remove(
                    catalogue_item_obj.catalogue_row,
                )

            else:

                self.catalogue_grid.remove(
                    catalogue_item_obj.catalogue_gridbox,
                )

            # Update IVs
            del self.video_catalogue_dict[video_obj.dbid]

            # If the current page is not the last one, we can create a new
            #   catalogue item to replace the removed one
            move_obj = None
            container_obj \
            = app_obj.media_reg_dict[self.video_index_current_dbid]
            video_count = 0

            if self.video_catalogue_dict \
            and self.catalogue_toolbar_current_page \
            < self.catalogue_toolbar_last_page:

                # Get the last catalogue object directly from its parent, as
                #   the parent is auto-sorted frequently
                if self.app_obj.catalogue_mode_type != 'grid':
                    child_list = self.catalogue_listbox.get_children()
                else:
                    child_list = self.catalogue_grid.get_children()

                last_obj = child_list[-1]
                if last_obj:
                    last_video_obj = last_obj.video_obj

                # Find the video object that would be drawn after that, if the
                #   videos were all drawn on a single page
                # At the same time, count the number of remaining child video
                #   objects so we can update the toolbar
                next_flag = False

                for child_obj in container_obj.child_list:
                    if isinstance(child_obj, media.Video):
                        video_count += 1
                        if child_obj.dbid == last_video_obj.dbid:
                            # (Use the next video after this one)
                            next_flag = True

                        elif next_flag == True:
                            # (Use this video)
                            insert_obj = child_obj
                            next_flag = False

                # Create the new catalogue item
                if insert_obj:
                    GObject.timeout_add(
                        0,
                        self.video_catalogue_update_video,
                        insert_obj,
                    )

            else:

                # We're already on the last (or only) page, so no need to
                #   replace anything. Just count the number of remaining child
                #   video objects
                for child_obj in container_obj.child_list:
                    if isinstance(child_obj, media.Video):
                        video_count += 1

            # Update widgets in the Video Catalogue toolbar
            self.video_catalogue_toolbar_update(
                self.catalogue_toolbar_current_page,
                video_count,
            )

            # Procedure complete
            if self.app_obj.catalogue_mode_type != 'grid':

                self.catalogue_listbox.show_all()

            else:

                # Fill in any empty spaces on the grid
                self.video_catalogue_grid_rearrange()
                # Gridboxes could be made (un)expandable, depending on the
                #   number of gridboxes now on the grid, and the number of
                #   columns allowed in the grid
                self.video_catalogue_grid_check_expand()


    def video_catalogue_unselect_all(self):

        """Can be called by anything.

        Standard de-selection of all videos in the Video Catalogue (i.e. all
        mainwin.SimpleCatalogueItem objects, or all
        mainwin.ComplexCatalogueItem, or all
        mainwin.GridCatalogueItem objects).
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 11657 video_catalogue_unselect_all')

        if self.app_obj.catalogue_mode_type != 'grid':

            self.catalogue_listbox.unselect_all()

        else:

            for this_catalogue_obj in self.video_catalogue_dict.values():
                this_catalogue_obj.do_select(False)


    def video_catalogue_force_resort(self):

        """Called by mainapp.TartubeApp.on_button_resort_catalogue().

        In case of incorrect sorting in the Video Catalogue, the user can click
        the button to force a re-sort.

        Children of the visible media.Channel, media.Playlist or media.Folder
        are resorted, then the Video Catalogue is redrawn.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 11681 video_catalogue_force_resort')

        if self.video_index_current_dbid is None:
            return

        else:
            # Redraw the Video Catalogue, switching to the first page
            # (The container's children are automatically sorted during the
            #   call to that function)
            self.video_catalogue_redraw_all(
                self.video_index_current_dbid,
                1,
                True,           # Reset scrollbars
                True,           # Don't cancel the filter, if applied
            )


    def video_catalogue_grid_set_gridbox_width(self, width):

        """Called by CatalogueGridBox.on_size_allocate().

        Used only when the Video Catalogue is displaying videos on a grid.
        Each grid location contains a single gridbox (mainwin.CatalogueGridBox)
        handling a single video.

        As we start to add gridboxes to the grid, the minimum required width
        (comprising the space needed for all widgets) is not immediately
        available. Therefore, initially gridboxes are not allowed to expand
        horizontally, filling all the available space.

        As soon as the width of one of the new gridboxes becomes available,
        its callback calls this function.

        We set the minimum required width for the current thumbnail size
        (specified by mainapp.TartubeApp.thumb_size_custom) and set a flag to
        check the size of the grid, given that minimum width (because it might
        be possible to put more or fewer gridboxes in each row).

        We also hide each gridbox's frame, if it should be hidden. (In order to
        obtain the correct minimum width, the frame it is always visible at
        first.)

        Args:

            width (int): The minimum required width for a gridbox, in pixels

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 11731 video_catalogue_grid_set_gridbox_width',
            )

        # Sanity check: Once the minimum gridbox width for each thumbnail size
        #   has been established, don't change it
        thumb_size = self.app_obj.thumb_size_custom
        if self.catalogue_grid_width_dict[thumb_size] is not None:
            return self.app_obj.system_error(
                217,
                'Redundant setting of minimum gridbox width',
            )

        # Further sanity check: nothing to do if videos aren't arranged on a
        #   grid
        if self.app_obj.catalogue_mode_type == 'grid':

            self.catalogue_grid_width_dict[thumb_size] = width

            # All gridboxes can now be drawn without a frame, if required
            for catalogue_obj in self.video_catalogue_dict.values():

                catalogue_obj.catalogue_gridbox.enable_visible_frame(
                    self.app_obj.catalogue_draw_frame_flag,
                )

            # Gtk is busy, so the horizontal size of the grid cannot be checked
            #   immediately. Set a flag to let Tartube's fast timer do that
            self.catalogue_grid_rearrange_flag = True


    def video_catalogue_grid_check_size(self):

        """Called by self.on_video_catalogue_thumb_combo_changed(),
        self.on_window_size_allocate() and .on_paned_size_allocate().

        Also called by mainapp.TartubeApp.script_fast_timer_callback(), after a
        recent call to self.video_catalogue_grid_set_gridbox_width().

        Used only when the Video Catalogue is displaying videos on a grid.
        Each grid location contains a single gridbox (mainwin.CatalogueGridBox)
        handling a single video.

        Check the available size of the grid. Given the minimum required width
        for a gridbox, increase or decrease the number of columns in the grid,
        if necessary.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 11779 video_catalogue_grid_check_size')

        # When working out the grid's actual width, take into account small
        #   gaps between each gridbox, and around the borders of other widgets
        thumb_size = self.app_obj.thumb_size_custom
        grid_width = self.win_last_width - self.videos_paned.get_position() \
        - (self.spacing_size * (self.catalogue_grid_column_count + 7))

        if self.catalogue_grid_width_dict[thumb_size] is None:
            column_count = 1

        else:

            gridbox_width = self.catalogue_grid_width_dict[thumb_size]
            column_count = int(grid_width / gridbox_width)
            if column_count < 1:
                column_count = 1

        # (The flag is True only when called from
        #   mainapp.TartubeApp.script_fast_timer_callback(), in which case we
        #   need to rearrrange the grid, even if the column count hasn't
        #   changed)
        if self.catalogue_grid_column_count != column_count \
        or self.catalogue_grid_rearrange_flag:

            self.catalogue_grid_rearrange_flag = False

            # Change the number of columns to fit more (of fewer) videos on
            #   each row
            self.catalogue_grid_column_count = column_count

            # Gridboxes could be made (un)expandable, depending on the number
            #   of gridboxes now on the grid
            self.video_catalogue_grid_check_expand()

            # Move video gridboxes to their new positions on the grid
            # (Any gridboxes which are not expandable, will not appear expanded
            #   unless this function is called)
            self.video_catalogue_grid_rearrange()

            # After maximising the window, Gtk refuses to do a redraw, meaning
            #   that the user sees a bigger window, but not a change in the
            #   number of columns
            # Only solution I can find is to adjust the size of the paned
            #   temporarily
            if column_count > 1:
                posn = self.videos_paned.get_position()
                self.videos_paned.set_position(posn + 1)
                self.videos_paned.set_position(posn - 1)


    def video_catalogue_grid_check_expand(self):

        """Called by self.video_catalogue_grid_check_size(),
        .video_catalogue_redraw_all(), .video_catalogue_insert_video(),
        .video_catalogue_delete_video().

        Used only when the Video Catalogue is displaying videos on a grid.
        Each grid location contains a single gridbox (mainwin.CatalogueGridBox)
        handling a single video.

        For aesthetic reasons, gridboxes should expand to fill the available
        space, or not.

        This function checks whether expansion should occur. If there has been
        a change of state, then every gridbox is called to update it (either
        enabling or disabling its horizontal expansion flag).
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 11849 video_catalogue_grid_check_expand')

        thumb_size = self.app_obj.thumb_size_custom

        # (Gridboxes never expand to fill the available space, if the minimum
        #   required width for a gridbox is not yet known)
        if self.catalogue_grid_width_dict[thumb_size] is not None:

            toggle_flag = False
            count = len(self.video_catalogue_dict)

            if (
                count < self.catalogue_grid_column_count
                and self.catalogue_grid_expand_flag
            ):
                self.catalogue_grid_expand_flag = False
                toggle_flag = True

            elif (
                count >= self.catalogue_grid_column_count
                and not self.catalogue_grid_expand_flag
            ):
                self.catalogue_grid_expand_flag = True
                toggle_flag = True

            if toggle_flag:

                # Change of state; update every gridbox
                for catalogue_obj in self.video_catalogue_dict.values():
                    catalogue_obj.catalogue_gridbox.set_expandable(
                        self.catalogue_grid_expand_flag,
                    )


    def video_catalogue_grid_attach_gridbox(self, wrapper_obj, x_pos, y_pos):

        """Called by self.video_catalogue_redraw_all(),
        .video_catalogue_insert_video() and .video_catalogue_grid_rearrange().

        Adds the specified CatalogueGridBox to the Video Catalogue's grid at
        the specified coordinates, and updates IVs.

        Args:

            wrapper_obj (mainwin.CatalogueGridBox): The gridbox to be added to
                the grid

            x_pos, y_pos (int): The coordinates at which to add it

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 11901 video_catalogue_grid_attach_gridbox')

        self.catalogue_grid.attach(
            wrapper_obj,
            x_pos,
            y_pos,
            1,
            1,
        )

        # Update IVs
        if self.catalogue_grid_row_count < (y_pos + 1):
            self.catalogue_grid_row_count = y_pos + 1

        wrapper_obj.set_posn(x_pos, y_pos)


    def video_catalogue_grid_rearrange(self):

        """Called by self.video_catalogue_grid_check_size(),
        .video_catalogue_update_video() and .video_catalogue_delete_video().

        Used only when the Video Catalogue is displaying videos on a grid.
        Each grid location contains a single gridbox (mainwin.CatalogueGridBox)
        handling a single video.

        Removes every gridbox from the grid. Sorts the gridboxes, and then
        puts them back onto the grid, using the number of columns specified by
        self.catalogue_grid_column_count (which may have changed recently), and
        filling any gaps (if a gridboxes have been removed from the grid).
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 11934 video_catalogue_grid_rearrange')

        # Each mainwin.CatalogueGridBox acts as a wrapper for a Gtk.Frame
        wrapper_list = []

        # Remove every gridbox from the grid
        for wrapper_obj in self.catalogue_grid.get_children():
            self.catalogue_grid.remove(wrapper_obj)
            wrapper_list.append(wrapper_obj)

        # (This IV's minimum value is 1, even when the grid is empty)
        self.catalogue_grid_row_count = 1

        # Sort the gridboxes, as if we were sorting the media.Video objects
        #   directly
        wrapper_list.sort(
            key=functools.cmp_to_key(self.video_catalogue_grid_auto_sort),
        )

        # Place gridboxes back on the grid, taking into account that the number
        #   of columns may have changed recently
        x_pos = 0
        y_pos = 0
        for wrapper_obj in wrapper_list:

            self.video_catalogue_grid_attach_gridbox(
                wrapper_obj,
                x_pos,
                y_pos,
            )

            x_pos += 1
            if x_pos >= self.catalogue_grid_column_count:
                x_pos = 0
                y_pos += 1


    def video_catalogue_grid_reset_sizes(self):

        """Called by self.__init__() and
        mainapp.TartubeApp.on_button_switch_view().

        When the Video Catalogue displays videos on a grid, each grid location
        contains a single gridbox (mainwin.CatalogueGridBox) handling a single
        video.

        In that case, we need to know the minimum required space for a gridbox.
        After changing mainapp.TartubeApp.catalogue_mode (i.e., after switching
        between one of the several viewing modes), reset those minimum sizes,
        so they can be calculated afresh the next time they are needed.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 11987 video_catalogue_grid_reset_sizes')

        self.catalogue_grid_width_dict = {}

        for key in self.app_obj.thumb_size_dict:
            self.catalogue_grid_width_dict[key] = None


    def video_catalogue_grid_select(self, catalogue_obj, select_type,
    rev_flag=False):

        """Can be called by anything.

        Used only when the Video Catalogue is displaying videos on a grid.
        Each grid location contains a single gridbox (mainwin.CatalogueGridBox)
        handling a single video.

        Widgets on a Gtk.Grid can't be selected just by clicking them (as we
        might select a row in a Gtk.TreeView or Gtk.ListBox, just by clicking
        it), so Tartube includes custom code to allow gridboxes to be selected
        and unselected.

        Args:

            catalogue_obj (mainwin.GridCatalogueItem): The gridbox that was
                clicked

            select_type (str): 'shift' if the SHIFT key is held down at the
                moment of the click, 'ctrl' if the CTRL button is held down,
                and 'default' if neither of those keys are held down

            rev_flag (bool): True when using the 'up' cursor key, or the page
                up key; False for all other detectable keys

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12024 video_catalogue_grid_select')

        # (Sorting function for the code immediately below)
        def sort_by_grid_posn(obj1, obj2):

            if obj1.y_pos < obj2.y_pos:
                return -1
            elif obj1.y_pos > obj2.y_pos:
                return 1
            elif obj1.x_pos < obj2.x_pos:
                return -1
            else:
                return 1

        # Select/deselect gridboxes
        if select_type == 'shift':

            # The user is holding down the SHIFT key. Select one or more
            #   gridboxes

            # Get a list of gridboxes, and sort them by position on the
            #   Gtk.Grid (top left to bottom right)
            gridbox_list = self.catalogue_grid.get_children()
            gridbox_list.sort(key=functools.cmp_to_key(sort_by_grid_posn))
            if rev_flag:
                gridbox_list.reverse()

            # Find the position of the first and last selected gridbox, and the
            #   position of the gridbox that handles the specified
            #   catalogue_obj
            count = -1
            first_posn = None
            last_posn = None
            specified_posn = None

            for gridbox_obj in gridbox_list:

                count += 1
                this_catalogue_obj \
                = self.video_catalogue_dict[gridbox_obj.video_obj.dbid]

                if this_catalogue_obj.selected_flag:

                    last_posn = count
                    if first_posn is None:
                        first_posn = count

                if gridbox_obj.video_obj == catalogue_obj.video_obj:
                    specified_posn = count

            # Sanity check
            if specified_posn is None:

                return self.app_obj.system_error(
                    218,
                    'Gridbox not found in Video Catalogue',
                )

            # Select/deselect videos, as required
            if not first_posn:

                start_posn = specified_posn
                stop_posn = specified_posn

            elif not catalogue_obj.selected_flag:

                if specified_posn < first_posn:

                    start_posn = specified_posn
                    stop_posn = first_posn

                elif specified_posn == first_posn:

                    start_posn = specified_posn
                    stop_posn = specified_posn

                else:

                    start_posn = first_posn
                    stop_posn = specified_posn

            else:

                if specified_posn < first_posn:

                    start_posn = specified_posn
                    stop_posn = last_posn

                else:

                    start_posn = first_posn
                    stop_posn = specified_posn

            count = -1
            for gridbox_obj in gridbox_list:

                count += 1
                this_catalogue_obj \
                = self.video_catalogue_dict[gridbox_obj.video_obj.dbid]

                if count >= start_posn and count <= stop_posn:
                    this_catalogue_obj.do_select(True)
                else:
                    this_catalogue_obj.do_select(False)

        elif select_type == 'ctrl':

            # The user is holding down the CTRL key. Select this gridbox, in
            #   addition to any gridboxes that are already selected
            catalogue_obj.toggle_select()

        else:

            # The user is holding down neither the SHIFT not CTRL keys. Select
            #   this gridbox, and unselect all other gridboxes
            for this_catalogue_obj in self.video_catalogue_dict.values():

                if this_catalogue_obj == catalogue_obj:
                    this_catalogue_obj.do_select(True)
                else:
                    this_catalogue_obj.do_select(False)


    def video_catalogue_grid_select_all(self):

        """Called by CatalogueGridBox.on_key_press_event().

        When the user presses CTRL+A, select all gridboxes in the grid.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12155 video_catalogue_grid_select_all')

        for catalogue_item_obj in self.video_catalogue_dict.values():
            catalogue_item_obj.do_select(True)


    def video_catalogue_grid_scroll_on_select(self, gridbox_obj, keyval,
    select_type):

        """Called by CatalogueGridBox.on_key_press_event().

        Custom code to do scrolling, and to update the selection, in the
        Video Catalogue grid when the user presses the cursor and page up/down
        keys.

        Args:

            gridbox_obj (mainwin.CatalogueGridBox): The gridbox that
                intercepted the keypress

            keyval (str): One of the keys in self.catalogue_grid_intercept_dict
                (e.g. 'Up', 'Left', 'Page_Up')

            select_type (str): 'shift' if the SHIFT key is held down at the
                moment of the click, 'ctrl' if the CTRL button is held down,
                and 'default' if neither of those keys are held down

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12185 video_catalogue_grid_scroll_on_select')

        # Get the GridCatalogueItem of the gridbox which intercepted the
        #   keypress
        if not gridbox_obj.video_obj.dbid in self.video_catalogue_dict:
            return

        catalogue_item_obj \
        = self.video_catalogue_dict[gridbox_obj.video_obj.dbid]

        # For cursor keys, find the grid location immediately beside this one
        x_pos = gridbox_obj.x_pos
        y_pos = gridbox_obj.y_pos

        # (Adjust values describing the size of the grid, to make the code a
        #   little simpler)
        width = self.catalogue_grid_column_count - 1
        height = self.catalogue_grid_row_count - 1

        # (With the SHIFT key, multiple successive up/page up keypresses won't
        #   have the intended effect. Tell self.video_catalogue_grid_select()
        #   to select individual gridboxes from bottom to top, which fixes the
        #   problem)
        if keyval == 'Up' or keyval == 'Page_Up':
            rev_flag = True
        else:
            rev_flag = False

        # Now interpret the keypress
        if keyval == 'Page_Up' or keyval == 'Page_Down':

            # For page up/down keys, things are a bit trickier. Moving the
            #   scrollbars would be simple enough, but then we wouldn't know
            #   which of the visible gridboxes to select
            # Instead, get the height of the parent scrollbar, then test the
            #   height of the old selected gridbox, and gridboxes above/below
            #   it, so we can decide which gridbox to select

            # Get the size of the parent scroller
            rect = self.catalogue_scrolled.get_allocation()
            scroller_height = rect.height
            if scroller_height <= 1:
                # Allocation not known yet (very unlikely after a selection)
                return

            # Chop away at that height, starting with the height of the current
            #   gridbox
            this_obj = gridbox_obj
            while True:

                this_rect = this_obj.get_allocation()
                this_height = this_rect.height
                if this_height <= 1:
                    return

                scroller_height -= this_height
                if scroller_height < 0:
                    break

                else:

                    # On the next loop, check the gridbox above/below this one
                    if keyval == 'Page_Up':

                        y_pos -= 1
                        if y_pos < 0:
                            y_pos = 0

                    else:

                        y_pos += 1
                        if y_pos > height:
                            y_pos = height

                    check_obj = self.catalogue_grid.get_child_at(x_pos, y_pos)
                    if check_obj:
                        this_obj = check_obj

                    else:
                        # Either we have scrolled 'below' the bottom row, or we
                        #   are at the bottom row, which is not full
                        # Select a video in the bottom row, as close to column
                        #   y_pos as possible)
                        for this_x in range(x_pos, -1, -1):

                            if self.catalogue_grid.get_child_at(this_x, y_pos):
                                x_pos = this_x
                                break

            if this_obj != gridbox_obj:

                self.video_catalogue_grid_select(
                    self.video_catalogue_dict[this_obj.video_obj.dbid],
                    select_type,
                    rev_flag,
                )

        else:

            if keyval == 'Left':

                x_pos -= 1
                if x_pos < 0:
                    y_pos -= 1
                    if y_pos < 0:
                        # (Already in the top left corner)
                        return
                    else:
                        x_pos = width

            elif keyval == 'Right':

                x_pos += 1
                if x_pos > width:
                    y_pos += 1
                    if y_pos > height:
                        # (Already in the bottom right corner)
                        return
                    else:
                        x_pos = 0

            elif keyval == 'Up':

                y_pos -= 1
                if y_pos < 0:
                    # (Already at the top)
                    return

            elif keyval == 'Down':

                y_pos += 1
                grid_count = self.catalogue_grid_column_count \
                * self.catalogue_grid_row_count
                video_count = len(self.video_catalogue_dict.keys())

                if y_pos > height:

                    # (The bottom row is full, and we are already at the
                    #   bottom)
                    return

                elif y_pos == height and grid_count > video_count:

                    # (One row above the bottom one, which is not full. Select
                    #   a video in the bottom row, as close to column y_pos as
                    #   possible)
                    for this_x in range(x_pos, -1, -1):

                        if self.catalogue_grid.get_child_at(this_x, y_pos):
                            x_pos = this_x
                            break

            # Fetch the gridbox at the new location
            new_gridbox_obj = self.catalogue_grid.get_child_at(x_pos, y_pos)
            if not new_gridbox_obj:
                return

            else:

                self.video_catalogue_grid_select(
                    self.video_catalogue_dict[new_gridbox_obj.video_obj.dbid],
                    select_type,
                    rev_flag,
                )


    def video_catalogue_toolbar_reset(self):

        """Called by self.video_catalogue_redraw_all().

        Just before completely redrawing the Video Catalogue, temporarily reset
        widgets in the Video Catalogue toolbar (in case something goes wrong,
        or in case drawing the page takes a long time).
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12361 video_catalogue_toolbar_reset')

        self.catalogue_toolbar_current_page = 1
        self.catalogue_toolbar_last_page = 1

        self.catalogue_page_entry.set_sensitive(True)
        self.catalogue_page_entry.set_text(
            str(self.catalogue_toolbar_current_page),
        )

        self.catalogue_last_entry.set_sensitive(True)
        self.catalogue_last_entry.set_text(
            str(self.catalogue_toolbar_last_page),
        )

        self.catalogue_first_button.set_sensitive(False)
        self.catalogue_back_button.set_sensitive(False)
        self.catalogue_forwards_button.set_sensitive(False)
        self.catalogue_last_button.set_sensitive(False)

        self.catalogue_show_filter_button.set_sensitive(False)

        self.catalogue_reverse_toolbutton.set_sensitive(False)
        self.catalogue_sort_combo.set_sensitive(False)
        self.catalogue_resort_button.set_sensitive(False)
        self.catalogue_thumb_combo.set_sensitive(False)
        self.catalogue_frame_button.set_sensitive(False)
        self.catalogue_icons_button.set_sensitive(False)
        self.catalogue_downloaded_button.set_sensitive(False)
        self.catalogue_undownloaded_button.set_sensitive(False)
        self.catalogue_blocked_button.set_sensitive(False)
        self.catalogue_filter_entry.set_sensitive(False)
        self.catalogue_regex_togglebutton.set_sensitive(False)
        self.catalogue_apply_filter_button.set_sensitive(False)
        self.catalogue_cancel_filter_button.set_sensitive(False)
        self.catalogue_find_date_button.set_sensitive(False)


    def video_catalogue_toolbar_update(self, page_num, video_count):

        """Called by self.video_catalogue_redraw_all(),
        self.video_catalogue_update_video() and
        self.video_catalogue_delete_video().

        After the Video Catalogue is redrawn or updated, update widgets in the
        Video Catalogue toolbar.

        Args:

            page_num (int): The page number to draw (a value in the range 1 to
                self.catalogue_toolbar_last_page)

            video_count (int): The number of videos that are children of the
                selected channel, playlist or folder (may be 0)

         """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12419 video_catalogue_toolbar_update')

        self.catalogue_toolbar_current_page = page_num

        # If the page size is 0, then all videos are drawn on one page
        if not self.app_obj.catalogue_page_size:
            self.catalogue_toolbar_last_page = page_num
        else:
            self.catalogue_toolbar_last_page \
            = int((video_count - 1) / self.app_obj.catalogue_page_size) + 1

        self.catalogue_page_entry.set_sensitive(True)
        self.catalogue_page_entry.set_text(
            str(self.catalogue_toolbar_current_page),
        )

        self.catalogue_last_entry.set_sensitive(True)
        self.catalogue_last_entry.set_text(
            str(self.catalogue_toolbar_last_page),
        )

        if page_num == 1:
            self.catalogue_first_button.set_sensitive(False)
            self.catalogue_back_button.set_sensitive(False)
        else:
            self.catalogue_first_button.set_sensitive(True)
            self.catalogue_back_button.set_sensitive(True)

        if page_num == self.catalogue_toolbar_last_page:
            self.catalogue_forwards_button.set_sensitive(False)
            self.catalogue_last_button.set_sensitive(False)
        else:
            self.catalogue_forwards_button.set_sensitive(True)
            self.catalogue_last_button.set_sensitive(True)

        self.catalogue_show_filter_button.set_sensitive(True)

        if self.video_catalogue_filtered_flag:
            self.catalogue_downloaded_button.set_sensitive(False)
            self.catalogue_undownloaded_button.set_sensitive(False)
            self.catalogue_blocked_button.set_sensitive(False)
        else:
            self.catalogue_downloaded_button.set_sensitive(True)
            self.catalogue_undownloaded_button.set_sensitive(True)
            self.catalogue_blocked_button.set_sensitive(True)

        # These widgets are sensitised when the filter is applied even if
        #   there are no matching videos
        # (If not, the user would not be able to click the 'Cancel filter'
        #   button)
        if not video_count and not self.video_catalogue_filtered_flag:
            self.catalogue_reverse_toolbutton.set_sensitive(False)
            self.catalogue_sort_combo.set_sensitive(False)
            self.catalogue_resort_button.set_sensitive(False)
            self.catalogue_thumb_combo.set_sensitive(False)
            self.catalogue_frame_button.set_sensitive(False)
            self.catalogue_icons_button.set_sensitive(False)
            self.catalogue_filter_entry.set_sensitive(False)
            self.catalogue_regex_togglebutton.set_sensitive(False)
            self.catalogue_apply_filter_button.set_sensitive(False)
            self.catalogue_cancel_filter_button.set_sensitive(False)
            self.catalogue_find_date_button.set_sensitive(False)
            self.catalogue_cancel_date_button.set_sensitive(False)
        else:
            self.catalogue_reverse_toolbutton.set_sensitive(True)
            self.catalogue_sort_combo.set_sensitive(True)
            self.catalogue_resort_button.set_sensitive(True)

            if self.app_obj.catalogue_mode_type != 'grid':
                self.catalogue_thumb_combo.set_sensitive(False)
            else:
                self.catalogue_thumb_combo.set_sensitive(True)

            self.catalogue_frame_button.set_sensitive(True)
            self.catalogue_icons_button.set_sensitive(True)

            self.catalogue_filter_entry.set_sensitive(True)
            self.catalogue_regex_togglebutton.set_sensitive(True)
            if self.video_catalogue_filtered_flag:
                self.catalogue_apply_filter_button.set_sensitive(False)
                self.catalogue_cancel_filter_button.set_sensitive(True)
            else:
                self.catalogue_apply_filter_button.set_sensitive(True)
                self.catalogue_cancel_filter_button.set_sensitive(False)
            self.catalogue_find_date_button.set_sensitive(True)
            self.catalogue_cancel_date_button.set_sensitive(False)


    def video_catalogue_apply_filter(self):

        """Called by mainapp.TartubeApp.on_button_apply_filter().

        Applies a filter, so that all videos not matching the search text are
        hidden in the Video Catalogue.

        Note that when a filter is applied, all matching videos are visible,
        regardless of the value of
        mainapp.TartubeApp.catalogue_draw_downloaded_flag,
        .catalogue_draw_undownloaded_flag and .catalogue_draw_blocked_flag.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12521 video_catalogue_apply_filter')

        # Sanity check - something must be selected in the Video Index
        parent_obj = None
        if self.video_index_current_dbid is not None:
            parent_obj \
            = self.app_obj.media_reg_dict[self.video_index_current_dbid]

        if not parent_obj or (isinstance(parent_obj, media.Video)):
            return self.app_obj.system_error(
                219,
                'Tried to apply filter, but no channel/playlist/folder' \
                + ' selected in the Video Index',
            )

        # Get the search text from the entry box
        search_text = self.catalogue_filter_entry.get_text()
        if search_text is None or search_text == '':
            # Applying an empty filter is the same as clicking the cancel
            #   filter button
            return self.video_catalogue_cancel_filter()

        # Get a list of media.Video objects which are children of the
        #   currently selected channel, playlist or folder
        # Then filter out every video whose name, description and/or comments
        #   don't match the filter text
        # If filtering by name, filter out any videos that don't have an
        #   individual name set
        video_list = []
        regex_flag = self.app_obj.catalogue_use_regex_flag
        lower_text = search_text.lower()

        for child_obj in parent_obj.child_list:

            if isinstance(child_obj, media.Video):

                if (
                    self.app_obj.catalogue_filter_name_flag \
                    and child_obj.name != self.app_obj.default_video_name \
                    and (
                        (
                            not regex_flag \
                            and child_obj.name.lower().find(lower_text) > -1
                        ) or (
                            regex_flag \
                            and re.search(
                                search_text,
                                child_obj.name,
                                re.IGNORECASE,
                            )
                        )
                    )
                ) or (
                    self.app_obj.catalogue_filter_descrip_flag \
                    and (
                        not regex_flag \
                        and child_obj.descrip.lower().find(lower_text) > -1
                    ) or (
                        regex_flag \
                        and re.search(
                            search_text,
                            child_obj.name,
                            re.IGNORECASE,
                        )
                    )
                ) or (
                    self.app_obj.catalogue_filter_comment_flag \
                    and child_obj.contains_comment(search_text, regex_flag)
                ):
                    video_list.append(child_obj)

        # Set IVs...
        self.video_catalogue_filtered_flag = True
        self.video_catalogue_filtered_list = video_list.copy()
        # ...and redraw the Video Catalogue
        self.video_catalogue_redraw_all(
            self.video_index_current_dbid,
            1,          # Display the first page
            True,       # Reset scrollbars
            True,       # Do not cancel the filter we've just applied
        )

        # Sensitise widgets, as appropriate
        self.catalogue_apply_filter_button.set_sensitive(False)
        self.catalogue_cancel_filter_button.set_sensitive(True)
        # (Desensitise these widgets, to make it clear to the user that the
        #   settings don't apply when the filter is applied)
        self.catalogue_downloaded_button.set_sensitive(False)
        self.catalogue_undownloaded_button.set_sensitive(False)
        self.catalogue_blocked_button.set_sensitive(False)


    def video_catalogue_cancel_filter(self):

        """Called by mainapp.TartubeApp.on_button_cancel_filter() and
        self.video_catalogue_apply_filter().

        Cancels the filter, so that all videos which are children of the
        currently selected channel/playlist/folder are shown in the Video
        Catalogue.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12624 video_catalogue_cancel_filter')

        # Reset IVs...
        self.video_catalogue_filtered_flag = False
        self.video_catalogue_filtered_list = []
        # ...and redraw the Video Catalogue
        self.video_catalogue_redraw_all(self.video_index_current_dbid)

        # Sensitise widgets, as appropriate
        self.catalogue_apply_filter_button.set_sensitive(True)
        self.catalogue_cancel_filter_button.set_sensitive(False)
        self.catalogue_downloaded_button.set_sensitive(True)
        self.catalogue_undownloaded_button.set_sensitive(True)
        self.catalogue_blocked_button.set_sensitive(True)


    def video_catalogue_show_date(self, page_num):

        """Called by mainapp.TartubeApp.on_button_find_date().

        Redraw the Video Catalogue to show the page containing the first video
        uploaded on a specified date.

        (De)sensitise widgets, as appropriate.

        Args:

            page_num (int): The Video Catalogue page number to display (unlike
                calls to self.video_catalogue_apply_filter(), no videos are
                filtered out; we just show the first page containing videos
                for the specified date)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12659 video_catalogue_show_date')

        # Sanity check - something must be selected in the Video Index
        parent_obj = None
        if self.video_index_current_dbid is not None:
            parent_obj \
            = self.app_obj.media_reg_dict[self.video_index_current_dbid]

        if not parent_obj or (isinstance(parent_obj, media.Video)):
            return self.app_obj.system_error(
                220,
                'Tried to apply find videos by date, but no channel/' \
                + ' playlist/folder selected in the Video Index',
            )

        # Redraw the Video Catalogue
        self.video_catalogue_redraw_all(
            self.video_index_current_dbid,
            page_num,
            True,       # Reset scrollbars
            True,       # Do not cancel the filter, if one has been applied
        )

        # Sensitise widgets, as appropriate
        self.catalogue_find_date_button.set_sensitive(False)
        self.catalogue_cancel_date_button.set_sensitive(True)


    def video_catalogue_unshow_date(self):

        """Called by mainapp.TartubeApp.on_button_find_date().

        Having redrawn the Video Catalogue to show the page containing the
        first video uploaded on a specified date, redraw it to show the first
        page again.

        (De)sensitise widgets, as appropriate.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12699 video_catalogue_unshow_date')

        # Sanity check - something must be selected in the Video Index
        parent_obj = None
        if self.video_index_current_dbid is not None:
            parent_obj \
            = self.app_obj.media_reg_dict[self.video_index_current_dbid]

        if not parent_obj or (isinstance(parent_obj, media.Video)):
            return self.app_obj.system_error(
                221,
                'Tried to cancel find videos by date, but no channel/' \
                + ' playlist/folder selected in the Video Index',
            )

        # Redraw the Video Catalogue
        self.video_catalogue_redraw_all(
            self.video_index_current_dbid,
            1,
            True,       # Reset scrollbars
            True,       # Do not cancel the filter, if one has been applied
        )

        # Sensitise widgets, as appropriate
        self.catalogue_find_date_button.set_sensitive(True)
        self.catalogue_cancel_date_button.set_sensitive(False)


    # (Progress List)


    def progress_list_reset(self):

        """Can be called by anything.

        Empties the Gtk.TreeView in the Progress List, ready for it to be
        refilled.

        Also resets related IVs.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12741 progress_list_reset')

        # Reset widgets
        self.progress_list_liststore = Gtk.ListStore(
            int, int, str,
            GdkPixbuf.Pixbuf,
            str, str, str, str, str, str, str, str, str,
        )
        self.progress_list_treeview.set_model(self.progress_list_liststore)

        # Reset IVs
        self.progress_list_row_dict = {}
        self.progress_list_row_count = 0
        self.progress_list_temp_dict = {}
        self.progress_list_finish_dict = {}
        self.progress_list_broken_dict = {}


    def progress_list_init(self, download_list_obj):

        """Called by mainapp.TartubeApp.download_manager_continue().

        At the start of the download operation, a downloads.DownloadList
        object is created, listing all the media data objects (channels,
        playlists and videos) from which videos are to be downloaded.

        This function is then called to add each of those media data objects to
        the Progress List.

        As the download operation progresses,
        downloads.DownloadWorker.talk_to_mainwin() calls
        self.progress_list_receive_dl_stats() to update the contents of the
        Progress List.

        Args:

            download_list_obj (downloads.DownloadList): The download list
                object that has just been created

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12783 progress_list_init')

        # For each download item object, add a row to the treeview, and store
        #   the download item's .dbid IV so that
        #   self.progress_list_receive_dl_stats() can update the correct row
        for item_id in download_list_obj.download_item_list:

            download_item_obj = download_list_obj.download_item_dict[item_id]

            self.progress_list_add_row(
                item_id,
                download_item_obj.media_data_obj,
            )


    def progress_list_add_row(self, item_id, media_data_obj):

        """Called by self.progress_list_init(),
        mainapp.TartubeApp.download_watch_videos() and
        downloads.VideoDownloader.convert_video_to_container().

        Adds a row to the Progress List.

        Args:

            item_id (int): The downloads.DownloadItem.item_id

            media_data_obj (media.Video, media.Channel or media.Playlist):
                The media data object for which a row should be added

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12816 progress_list_add_row')

        # Prepare the icon
        if isinstance(media_data_obj, media.Channel):
            pixbuf = self.pixbuf_dict['channel_small']
        elif isinstance(media_data_obj, media.Playlist):
            pixbuf = self.pixbuf_dict['playlist_small']
        elif isinstance(media_data_obj, media.Folder):
            pixbuf = self.pixbuf_dict['folder_small']
        elif media_data_obj.live_mode == 2:
            pixbuf = self.pixbuf_dict['live_now_small']
        elif media_data_obj.live_mode == 1:
            pixbuf = self.pixbuf_dict['live_wait_small']
        else:
            pixbuf = self.pixbuf_dict['video_small']

        # Prepare the new row in the treeview
        row_list = []

        row_list.append(item_id)                        # Hidden
        row_list.append(media_data_obj.dbid)            # Hidden
        row_list.append(                                # Hidden
            html.escape(
                media_data_obj.fetch_tooltip_text(
                    self.app_obj,
                    self.tooltip_max_len,
                    True,                               # Show errors/warnings
                ),
            ),
        )
        row_list.append(pixbuf)
        row_list.append(media_data_obj.name)
        row_list.append(None)
        row_list.append(_('Waiting'))
        row_list.append(None)
        row_list.append(None)
        row_list.append(None)
        row_list.append(None)
        row_list.append(None)
        row_list.append(None)

        # Create a new row in the treeview. Doing the .show_all() first
        #   prevents a Gtk error (for unknown reasons)
        self.progress_list_treeview.show_all()
        self.progress_list_liststore.append(row_list)

        # Store the row's details so we can update it later
        self.progress_list_row_dict[item_id] \
        = self.progress_list_row_count
        self.progress_list_row_count += 1


    def progress_list_receive_dl_stats(self, download_item_obj, dl_stat_dict,
    finish_flag=False):

        """Called by downloads.DownloadWorker.data_callback().

        During a download operation, this function is called every time
        youtube-dl writes some output to STDOUT.

        Updating data displayed in the Progress List several times a second,
        and irregularly, doesn't look very nice. Instead, we only update the
        displayed data at fixed intervals.

        Thus, when this function is called, it is passed a dictionary of
        download statistics in a standard format (the one described in the
        comments to downloads.VideoDownloader.extract_stdout_data() ).

        We store that dictionary temporarily. During periodic calls to
        self.progress_list_display_dl_stats(), the contents of any stored
        dictionaries are displayed and then the dictionaries themselves are
        destroyed.

        Args:

            download_item_obj (downloads.DownloadItem): The download item
                object handling a download for a media data object

            dl_stat_dict (dict): The dictionary of download statistics
                described above

            finish_flag (bool): True if the worker has finished with its
                media data object, meaning that dl_stat_dict is the final set
                of statistics, and that the progress list row can be hidden,
                if required

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12905 progress_list_receive_dl_stats')

        # Check that the Progress List actually has a row for the specified
        #   downloads.DownloadItem object
        if not download_item_obj.item_id in self.progress_list_row_dict:

            # !!! DEBUG Git #479
            # Because of an unresolved issue, only show this message once for
            #   each downloads.DownloadItem object
            if not download_item_obj.item_id in self.progress_list_broken_dict:

                self.app_obj.system_error(
                    222,
                    'Missing row in Progress List',
                )

                self.progress_list_broken_dict[download_item_obj.item_id] \
                = False

            return

        # Temporarily store the dictionary of download statistics
        if not download_item_obj.item_id in self.progress_list_temp_dict:
            new_dl_stat_dict = {}
        else:
            new_dl_stat_dict \
            = self.progress_list_temp_dict[download_item_obj.item_id]

        for key in dl_stat_dict:
            new_dl_stat_dict[key] = dl_stat_dict[key]

        self.progress_list_temp_dict[download_item_obj.item_id] \
        = new_dl_stat_dict

        # If it's the final set of download statistics, set the time at which
        #   the row can be hidden (if required)
        if finish_flag:
            self.progress_list_finish_dict[download_item_obj.item_id] \
            = time.time() + self.progress_list_hide_time


    def progress_list_display_dl_stats(self):

        """Called by downloads.DownloadManager.run() and
        mainapp.TartubeApp.dl_timer_callback().

        As the download operation progresses, youtube-dl writes statistics to
        its STDOUT. Those statistics have been interpreted and stored in
        self.progress_list_temp_dict, waiting for periodic calls to this
        function to display them.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12958 progress_list_display_dl_stats')

        # Import some objects from downloads.py (for convenience)
        dl_obj = self.app_obj.download_manager_obj
        # Import the contents of the IV (in case it gets updated during the
        #   call to this function), and use the imported copy
        temp_dict = self.progress_list_temp_dict
        self.progress_list_temp_dict = {}

        # For each media data object displayed in the Progress List...
        for item_id in temp_dict:

            # Get a dictionary of download statistics for this media data
            #   object
            # The dictionary is in the standard format described in the
            #   comments to downloads.VideoDownloader.extract_stdout_data()
            dl_stat_dict = temp_dict[item_id]

            # Get the corresponding treeview row
            tree_path = Gtk.TreePath(self.progress_list_row_dict[item_id])

            # Get the media data object
            # Git 34 reports that the .get_iter() call causes a crash, when
            #   finished rows are being hidden. This may be a Gtk issue, so
            #   intercept the error directly
            try:
                tree_iter = self.progress_list_liststore.get_iter(tree_path)
                dbid = self.progress_list_liststore[tree_iter][1]
                media_data_obj = self.app_obj.media_reg_dict[dbid]

            except:
                # Don't try to update hidden rows
                return

            # Any downloads.DownloadItem objects not in their
            #   ACTIVE_STAGE_DOWNLOAD stage can be removed from the rolling
            #   average data immediately
            if 'status' in dl_stat_dict \
            and dl_stat_dict['status'] != '' \
            and dl_stat_dict['status'] != formats.ACTIVE_STAGE_DOWNLOAD \
            and item_id in self.progress_list_average_speed_dict:
                del self.progress_list_average_speed_dict[item_id]

            # Otherwise, store the instantaneous download speed, so a rolling
            #   average can be calculated
            elif 'speed' in dl_stat_dict and dl_stat_dict['speed'] != '':

                if item_id in self.progress_list_average_speed_dict:
                    self.progress_list_average_speed_dict[item_id].append([
                        utils.convert_string_to_bytes(
                            dl_stat_dict['speed'],
                        ),
                        int(time.time()),
                    ])

                else:
                    self.progress_list_average_speed_dict[item_id] = [[
                        utils.convert_string_to_bytes(
                            dl_stat_dict['speed'],
                        ),
                        int(time.time()),
                    ]]

            # When the download concludes, instead of overwriting the filename,
            #   show the video's name
            if 'filename' in dl_stat_dict \
            and dl_stat_dict['filename'] == '' \
            and isinstance(media_data_obj, media.Video) \
            and media_data_obj.file_name is not None:
                dl_stat_dict['filename'] = media_data_obj.file_name

            # Update the tooltip
            try:
                tree_iter = self.progress_list_liststore.get_iter(tree_path)
                self.progress_list_liststore.set(
                    tree_iter,
                    self.progress_list_tooltip_column,
                    html.escape(
                        media_data_obj.fetch_tooltip_text(
                            self.app_obj,
                            self.tooltip_max_len,
                            True,           # Show errors/warnings
                        ),
                    ),
                )

            except:
                return

            # Update statistics displayed in this row
            # (Columns 0, 1 and 3 are not modified, once the row has been added
            #   to the treeview)
            column = 4

            for key in (
                'playlist_index',
                'status',
                'filename',
                'extension',
                'percent',
                'speed',
                'eta',
                'filesize',
            ):
                column += 1

                if key in dl_stat_dict:

                    if key == 'playlist_index':

                        if dl_stat_dict['playlist_index'] == 0:
                            msg = ''

                        elif 'dl_sim_flag' in dl_stat_dict \
                        and dl_stat_dict['dl_sim_flag']:
                            # (Don't know how many videos there are in a
                            #   channel/playlist, so ignore value of
                            #   'playlist_size')
                            msg = str(dl_stat_dict['playlist_index'])

                        else:
                            msg = str(dl_stat_dict['playlist_index'])
                            if 'playlist_size' in dl_stat_dict \
                            and dl_stat_dict['playlist_size'] > 0:
                                msg = msg + '/' \
                                + str(dl_stat_dict['playlist_size'])
                            else:
                                msg = msg + '/1'

                    else:
                        msg = dl_stat_dict[key]

                    try:
                        tree_iter = self.progress_list_liststore.get_iter(
                            tree_path
                        )

                        self.progress_list_liststore.set(
                            tree_iter,
                            column,
                            msg,
                        )

                    except:
                        return

        # Display ongoing statistics, including the rolling average d/l speed
        ignore_me = _(
            'TRANSLATOR\'S NOTE: D/L = download'
        )

        if not dl_obj:
            msg = ''
        else:
            msg = '<b>' + _('Check') + ':</b> ' \
            + str(dl_obj.total_sim_count) \
            + '   <b>' + _('D/L') + ':</b> ' \
            + str(dl_obj.total_dl_count)

            if dl_obj.total_clip_count or dl_obj.total_slice_count:
                msg = msg + '   <b>' + _('Other') + ':</b> ' \
                + str(dl_obj.total_clip_count + dl_obj.total_slice_count)

            msg = msg + '   <b>' + _('Size') + ':</b> ' \
            + str(utils.convert_bytes_to_string(dl_obj.total_size_count)) \
            + '   <b>' + _('Speed') + ':</b> ' \
            + str(utils.convert_bytes_to_string(
                self.progress_list_get_rolling_average(),
            ) + '/s')

        self.progress_update_label.set_markup(msg)


    def progress_list_check_hide_rows(self, force_flag=False):

        """Called by mainapp.TartubeApp.download_manager_finished,
        .dl_timer_callback() and .set_progress_list_hide_flag().

        Called only when mainapp.TartubeApp.progress_list_hide_flag is True.

        Any rows in the Progress List which are finished are stored in
        self.progress_list_finish_dict. When a row is finished, it is given a
        time (three seconds afterwards, by default) at which the row can be
        deleted.

        Check each row, and if it's time to delete it, do so.

        Args:

            force_flag (bool): Set to True if all finished rows should be
                hidden immediately, rather than waiting for the (by default)
                three seconds

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13154 progress_list_check_hide_rows')

        current_time = time.time()
        hide_list = []

        for item_id in self.progress_list_finish_dict.keys():
            finish_time = self.progress_list_finish_dict[item_id]

            if force_flag or current_time > finish_time:
                hide_list.append(item_id);

        # Now we've finished walking the dictionary, we can hide rows
        for item_id in hide_list:
            self.progress_list_do_hide_row(item_id)


    def progress_list_do_hide_row(self, item_id):

        """Called by self.progress_list_check_hide_rows().

        If it's time to delete a row in the Progress List, delete the row and
        update IVs.

        Args:

            item_id (int): The downloads.DownloadItem.item_id that was
                displaying statistics in the row to be deleted

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13185 progress_list_do_hide_row')

        row_num = self.progress_list_row_dict[item_id]

        # Remove the row. Very rarely this generates a Python error (for
        #   unknown reasons)
        try:

            path = Gtk.TreePath(row_num)
            tree_iter = self.progress_list_liststore.get_iter(path)
            self.progress_list_liststore.remove(tree_iter)

            # Prepare new values for Progress List IVs. Everything after this
            #   row must have its row number decremented by one
            row_dict = {}
            for this_item_id in self.progress_list_row_dict.keys():
                this_row_num = self.progress_list_row_dict[this_item_id]

                if this_row_num > row_num:
                    row_dict[this_item_id] = this_row_num - 1
                elif this_row_num < row_num:
                    row_dict[this_item_id] = this_row_num

            row_count = self.progress_list_row_count - 1


            # Apply updated IVs
            self.progress_list_row_dict = row_dict.copy()
            if item_id in self.progress_list_temp_dict:
                del self.progress_list_temp_dict[item_id]
            if item_id in self.progress_list_finish_dict:
                del self.progress_list_finish_dict[item_id]

        except:

            # !!! DEBUG Git #479
            # Because of an unresolved issue, only show this message once for
            #   each downloads.DownloadItem object
            if not item_id in self.progress_list_broken_dict:

                self.app_obj.system_error(
                    271,
                    'Cannot remove row in Progress List (row does not exist)',
                )

                self.progress_list_broken_dict[item_id] = False


    def progress_list_update_video_name(self, download_item_obj, video_obj):

        """Called by self.results_list_add_row().

        In the Progress List, an individual video (one inside a media.Folder)
        will be visible using the system's default video name, rather than the
        video's actual name. The final call to
        self.progress_list_display_dl_stats() cannot set the actual name, as it
        might not be available yet.

        The Results List is updated some time after the last call to the
        Progress List. If the video has a non-default name, then display it in
        the Progress List now.

        Args:

            download_item_obj (downloads.DownloadItem): The download item
                object handling a download for a media data object

            video_obj (media.Video): The media data object for the downloaded
                video

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13258 progress_list_update_video_name')

        if download_item_obj.item_id in self.progress_list_row_dict \
        and download_item_obj.media_data_obj == video_obj:

            # Get the Progress List treeview row
            tree_path = Gtk.TreePath(
                self.progress_list_row_dict[download_item_obj.item_id],
            )

            self.progress_list_liststore.set(
                self.progress_list_liststore.get_iter(tree_path),
                4,
                video_obj.name,
            )


    def progress_list_get_column_widths(self):

        """Called by mainapp.TartubeApp.save_config().

        Fetches the width of the 'Source' and 'Incoming file' columns in the
        Progress List.

        Return values:

            The two widths, or None if the list doesn't exist yet

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13289 progress_list_get_column_widths')

        if self.progress_list_treeview is None:
            return None, None

        else:
            source_column = self.progress_list_treeview.get_column(4)
            source_width = source_column.get_width()
            # (Shouldn't be possible to reduce the width below the minimum,
            #   but we'll check anyway)
            if source_width < self.min_column_width:
                source_width = self.min_column_width

            incoming_column = self.progress_list_treeview.get_column(7)
            incoming_width = incoming_column.get_width()
            if incoming_width < self.min_column_width:
                incoming_width = self.min_column_width

            return source_width, incoming_width


    def progress_list_get_rolling_average(self):

        """Called by self.progress_list_display_dl_stats().

        Calculates the rolling average download speed, and updates the
        dictionary storing instantaneous download speed, removing older values.

        Return values:

            Returns the rolling average speed, in bytes

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13324 progress_list_get_rolling_average')

        # Import some objects from downloads.py (for convenience)
        dl_list_obj = self.app_obj.download_manager_obj.download_list_obj

        # Update the data, and calculate a new rolling average
        old_dict = self.progress_list_average_speed_dict
        self.progress_list_average_speed_dict = {}

        current_time = time.time()
        total_speed = 0

        for item_id in old_dict:
            new_list = []
            combined_speed = 0
            combined_values = 0

            for mini_list in old_dict[item_id]:

                # List in the form [instantaneous d/l speed, time received]
                if mini_list[1] \
                >= current_time - self.progress_list_average_speed_length:
                    new_list.append(mini_list)
                    combined_speed += mini_list[0]
                    combined_values += 1

            self.progress_list_average_speed_dict[item_id] = new_list
            # The total average speed is the sum of the average speed for
            #   each channel/playlist/video
            if combined_values:
                total_speed += (combined_speed / combined_values)

        return total_speed


    def progress_list_reset_rolling_average(self):

        """Called by mainapp.TartubeApp.download_manager_finished() at the end
        of a download operation.

        Resets our dictionary of instantaneous download speeds, used to
        calculate the rolling average.

        Also resets the Gtk.Label.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13371 progress_list_reset_rolling_average')

        self.progress_list_average_speed_dict = {}
        self.progress_update_label.set_markup('')


    # (Results List)


    def results_list_reset(self):

        """Can be called by anything.

        Empties the Gtk.TreeView in the Results List, ready for it to be
        refilled. (There are no IVs to reset.)
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13389 results_list_reset')

        # Reset widgets
        self.results_list_liststore = Gtk.ListStore(
            int, str,
            GdkPixbuf.Pixbuf,
            str, str, str, str,
            bool,
            GdkPixbuf.Pixbuf,
            str,
        )
        self.results_list_treeview.set_model(self.results_list_liststore)

        # Reset IVs
        self.results_list_row_count = 0
        self.results_list_temp_list = []
        self.results_list_row_dict = {}


    def results_list_add_row(self, download_item_obj, video_obj, \
    mini_options_dict):

        """Called by mainapp.TartubeApp.announce_video_download().

        At the instant when youtube-dl completes a video download, the standard
        python test for the existence of a file fails.

        Therefore, when this function is called, we display the downloaded
        video in the Results List immediately, but we also add the video to a
        temporary list.

        Thereafter, periodic calls to self.results_list_update_row() check
        whether the file actually exists yet, and updates the Results List
        accordingly.

        Args:

            download_item_obj (downloads.DownloadItem): The download item
                object handling a download for a media data object

            video_obj (media.Video): The media data object for the downloaded
                video

            mini_options_dict (dict): A dictionary containing a subset of
                download options from the the options.OptionsManager object
                used to download the video. It contains zero, some or all of
                the following download options:

                keep_description keep_info keep_annotations keep_thumbnail
                move_description move_info move_annotations move_thumbnail

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13443 results_list_add_row')

        # Prepare the icons
        if video_obj.live_mode == 1:
            if not video_obj.live_debut_flag:
                pixbuf = self.pixbuf_dict['live_wait_small']
            else:
                pixbuf = self.pixbuf_dict['debut_wait_small']
        elif video_obj.live_mode == 2:
            if not video_obj.live_debut_flag:
                pixbuf = self.pixbuf_dict['live_now_small']
            else:
                pixbuf = self.pixbuf_dict['debut_now_small']
        elif video_obj.split_flag:
            pixbuf = self.pixbuf_dict['split_file_small']
        elif download_item_obj.operation_type == 'sim' \
        or download_item_obj.media_data_obj.dl_sim_flag:
            pixbuf = self.pixbuf_dict['check_small']
        else:
            pixbuf = self.pixbuf_dict['download_small']

        if isinstance(video_obj.parent_obj, media.Channel):
            pixbuf2 = self.pixbuf_dict['channel_small']
        elif isinstance(video_obj.parent_obj, media.Playlist):
            pixbuf2 = self.pixbuf_dict['playlist_small']
        elif isinstance(video_obj.parent_obj, media.Folder):
            pixbuf2 = self.pixbuf_dict['folder_small']
        else:
            return self.app_obj.system_error(
                223,
                'Results List add row request failed sanity check',
            )

        # Prepare the new row in the treeview
        row_list = []

        # Set the row's initial contents
        row_list.append(video_obj.dbid)             # Hidden
        row_list.append(                            # Hidden
            html.escape(
                video_obj.fetch_tooltip_text(
                    self.app_obj,
                    self.tooltip_max_len,
                    True,                           # Show errors/warnings
                ),
            ),
        )
        row_list.append(pixbuf)
        row_list.append(video_obj.nickname)

        # (For a simulated download, the video duration (etc) will already be
        #   available, so we can display those values)
        if video_obj.duration is not None:
            row_list.append(
                utils.convert_seconds_to_string(video_obj.duration),
            )
        else:
            row_list.append(None)

        if video_obj.file_size is not None:
            row_list.append(video_obj.get_file_size_string())
        else:
            row_list.append(None)

        if video_obj.upload_time is not None:
            row_list.append(video_obj.get_upload_date_string())
        else:
            row_list.append(None)

        row_list.append(video_obj.dl_flag)
        row_list.append(pixbuf2)
        row_list.append(video_obj.parent_obj.name)

        # Create a new row in the treeview. Doing the .show_all() first
        #   prevents a Gtk error (for unknown reasons)
        self.results_list_treeview.show_all()
        if not self.app_obj.results_list_reverse_flag:
            self.results_list_liststore.append(row_list)
        else:
            self.results_list_liststore.prepend(row_list)

        # Store some information about this download so that periodic calls to
        #   self.results_list_update_row() can retrieve it, and check whether
        #   the file exists yet
        temp_dict = {
            'video_obj': video_obj,
            'row_num': self.results_list_row_count,
        }

        for key in mini_options_dict.keys():
            temp_dict[key] = mini_options_dict[key]

        # Update IVs
        self.results_list_temp_list.append(temp_dict)
        self.results_list_row_dict[video_obj.dbid] \
        = self.results_list_row_count
        # (The number of rows has just increased, so increment the IV for the
        #   next call to this function)
        self.results_list_row_count += 1

        # Special measures for individual videos. The video name may not have
        #   been known when the Progress List was updated for the last time
        #   (but is known now). Update the name displayed in the Progress List,
        #   just to be sure
        self.progress_list_update_video_name(download_item_obj, video_obj)


    def results_list_update_row(self):

        """Called by mainapp.TartubeApp.dl_timer_callback().

        self.results_list_temp_list contains a set of dictionaries, one for
        each video download whose file has not yet been confirmed to exist.

        Go through each of those dictionaries. If the file still doesn't exist,
        re-insert the dictionary back into self.results_list_temp_list, ready
        for it to be checked by the next call to this function.

        If the file does now exist, update the corresponding media.Video
        object. Then update the Video Catalogue and the Progress List.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13566 results_list_update_row')

        new_temp_list = []

        while self.results_list_temp_list:

            temp_dict = self.results_list_temp_list.pop(0)

            # For convenience, retrieve the media.Video object, leaving the
            #   other values in the dictionary until we need them
            video_obj = temp_dict['video_obj']
            # Get the video's full file path now, as we use it several times
            video_path = video_obj.get_actual_path(self.app_obj)

            # Because of the 'Requested formats are incompatible for merge and
            #   will be merged into mkv' warning, we have to check for that
            #   extension, too
            mkv_flag = False
            if not os.path.isfile(video_path) and video_obj.file_ext == '.mp4':

                mkv_flag = True
                video_path = video_obj.get_actual_path_by_ext(
                    self.app_obj,
                    '.mkv',
                )

            # Does the downloaded file now exist on the user's hard drive?
            if os.path.isfile(video_path):

                # Update the media.Video object using the temporary dictionary
                self.app_obj.update_video_when_file_found(
                    video_obj,
                    video_path,
                    temp_dict,
                    mkv_flag,
                )

                # The parent container objects can now be sorted
                video_obj.parent_obj.sort_children(self.app_obj)
                self.app_obj.fixed_all_folder.sort_children(self.app_obj)

                if video_obj.bookmark_flag:
                    self.app_obj.fixed_bookmark_folder.sort_children(
                        self.app_obj,
                    )

                if video_obj.fav_flag:
                    self.app_obj.fixed_fav_folder.sort_children(self.app_obj)

                if video_obj.live_mode:
                    self.app_obj.fixed_live_folder.sort_children(self.app_obj)

                if video_obj.missing_flag:
                    self.app_obj.fixed_missing_folder.sort_children(
                        self.app_obj,
                    )

                if video_obj.new_flag:
                    self.app_obj.fixed_new_folder.sort_children(self.app_obj)

                if video_obj in self.app_obj.fixed_recent_folder.child_list:
                    self.app_obj.fixed_recent_folder.sort_children(
                        self.app_obj,
                    )

                if video_obj.waiting_flag:
                    self.app_obj.fixed_waiting_folder.sort_children(
                        self.app_obj,
                    )

                # Update the video catalogue in the 'Videos' tab
                GObject.timeout_add(
                    0,
                    self.video_catalogue_update_video,
                    video_obj,
                )

                # Prepare icons
                if isinstance(video_obj.parent_obj, media.Channel):
                    pixbuf = self.pixbuf_dict['channel_small']
                elif isinstance(video_obj.parent_obj, media.Channel):
                    pixbuf = self.pixbuf_dict['playlist_small']
                else:
                    pixbuf = self.pixbuf_dict['folder_small']

                # Update the corresponding row in the Results List
                row_num = temp_dict['row_num']
                # New rows are being added to the top, so the real row number
                #   changes on every call to self.results_list_add_row()
                if self.app_obj.results_list_reverse_flag:
                    row_num = self.results_list_row_count - 1 - row_num

                tree_path = Gtk.TreePath(row_num)
                row_iter = self.results_list_liststore.get_iter(tree_path)

                self.results_list_liststore.set(
                    row_iter,
                    self.results_list_tooltip_column,
                    html.escape(
                        video_obj.fetch_tooltip_text(
                            self.app_obj,
                            self.tooltip_max_len,
                            True,           # Show errors/warnings
                        ),
                    ),
                )

                self.results_list_liststore.set(
                    row_iter,
                    3,
                    video_obj.nickname,
                )

                if video_obj.duration is not None:
                    self.results_list_liststore.set(
                        row_iter,
                        4,
                        utils.convert_seconds_to_string(
                            video_obj.duration,
                        ),
                    )

                if video_obj.file_size:
                    self.results_list_liststore.set(
                        row_iter,
                        5,
                        video_obj.get_file_size_string(),
                    )

                if video_obj.upload_time:
                    self.results_list_liststore.set(
                        row_iter,
                        6,
                        video_obj.get_upload_date_string(),
                    )

                self.results_list_liststore.set(row_iter, 7, video_obj.dl_flag)
                self.results_list_liststore.set(row_iter, 8, pixbuf)

                self.results_list_liststore.set(
                    row_iter,
                    9,
                    video_obj.parent_obj.name,
                )

            else:

                # File not found

                # If this was a simulated download, the key 'keep_description'
                #   won't exist in temp_dict
                # For simulated downloads, we only check once (in case the
                #   video file already existed on the user's filesystem)
                # For real downloads, we check again on the next call to this
                #   function
                if 'keep_description' in temp_dict:
                    new_temp_list.append(temp_dict)

        # Any files that don't exist yet must be checked on the next call to
        #   this function
        self.results_list_temp_list = new_temp_list


    def results_list_update_row_on_delete(self, dbid):

        """Called by mainapp.TartubeApp.delete_video().

        When a video is deleted, this function is called. If the video is
        visible in the Results List, we change the icon to mark it as deleted.

        Args:

            dbid (int): The .dbid of the media.Video object which has just been
                deleted

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13744 results_list_update_row_on_delete')

        if dbid in self.results_list_row_dict:

            row_num = self.results_list_row_dict[dbid]
            if self.app_obj.results_list_reverse_flag:
                # New rows are being added to the top, so the real row number
                #   changes on every call to self.results_list_add_row()
                row_num = self.results_list_row_count - 1 - row_num

            tree_path = Gtk.TreePath(row_num)
            row_iter = self.results_list_liststore.get_iter(tree_path)
            if row_iter:

                self.results_list_liststore.set(
                    row_iter,
                    2,
                    self.pixbuf_dict['delete_small'],
                )

                self.results_list_liststore.set(row_iter, 7, False)

                self.results_list_liststore.set(
                    row_iter,
                    8,
                    self.pixbuf_dict['delete_small'],
                )

                self.results_list_liststore.set(row_iter, 9, '')


    def results_list_update_tooltip(self, video_obj):

        """Called by downloads.DownloadWorker.data_callback().

        When downloading a video individually, the tooltips in the Results
        List are only updated when the video file is actually downloaded. This
        function is called to update the tooltips at the end of every download,
        ensuring that any errors/warnings are visible in it.

        Args:

            video_obj (media.Video): The video which has just been downloaded
                individually

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13792 results_list_update_tooltip')

        if video_obj.dbid in self.results_list_row_dict:

            # Update the corresponding row in the Results List
            row_num = self.results_list_row_dict[video_obj.dbid]
            # New rows are being added to the top, so the real row number
            #   changes on every call to self.results_list_add_row()
            if self.app_obj.results_list_reverse_flag:
                row_num = self.results_list_row_count - 1 - row_num

            tree_path = Gtk.TreePath(row_num)
            row_iter = self.results_list_liststore.get_iter(tree_path)

            self.results_list_liststore.set(
                row_iter,
                1,
                html.escape(
                    video_obj.fetch_tooltip_text(
                        self.app_obj,
                        self.tooltip_max_len,
                        True,           # Show errors/warnings
                    ),
                ),
            )


    def results_list_get_column_widths(self):

        """Called by mainapp.TartubeApp.save_config().

        Fetches the width of the 'New videos' column in the Results List.

        Return values:

            The width, or None if the list doesn't exist yet

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13832 results_list_get_column_widths')

        if self.results_list_treeview is None:
            return None, None

        else:
            video_column = self.results_list_treeview.get_column(3)
            video_width = video_column.get_width()
            # (Shouldn't be possible to reduce the width below the minimum,
            #   but we'll check anyway)
            if video_width < self.min_column_width:
                video_width = self.min_column_width

            return video_width


    # (Classic Mode tab)


    def classic_mode_tab_add_dest_dir(self):

        """Called by mainapp.TartubeApp.on_button_classic_dest_dir().

        A new destination directory has been added, so add it to the combobox
        in the Classic Mode tab.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13860 classic_mode_tab_add_dest_dir')

        # Reset the contents of the combobox
        self.classic_dest_dir_liststore = Gtk.ListStore(str)
        for string in self.app_obj.classic_dir_list:
            self.classic_dest_dir_liststore.append( [string] )

        self.classic_dest_dir_combo.set_model(self.classic_dest_dir_liststore)
        self.classic_dest_dir_combo.set_active(0)
        self.show_all()


    def classic_mode_tab_add_row(self, dummy_obj):

        """Called by self.classic_mode_tab_add_urls().

        Adds a row to the Classic Progress List.

        Args:

            dummy_obj (media.Video): The dummy media.Video object handling the
                download of a single URL (which might represent a video,
                channel or playlist)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13887 classic_mode_tab_add_row')

        # Prepare the new row in the treeview
        row_list = []

        row_list.append(dummy_obj.dbid)             # Hidden
        row_list.append(                            # Hidden
            html.escape(
                dummy_obj.fetch_tooltip_text(
                    self.app_obj,
                    self.tooltip_max_len,
                    True,                           # Show errors/warnings
                ),
            ),
        )

        # (Don't display the https:// bit, that's just wasted space
        source = dummy_obj.source
        match = re.search(r'^https?\:\/\/(.*)', source)
        if match:
            source = match.group(1)

        row_list.append(source)
        row_list.append(None)
        row_list.append(_('Waiting'))
        row_list.append(None)
        row_list.append(None)
        row_list.append(None)
        row_list.append(None)
        row_list.append(None)
        row_list.append(None)

        # Create a new row in the treeview. Doing the .show_all() first
        #   prevents a Gtk error (for unknown reasons)
        self.classic_progress_treeview.show_all()
        self.classic_progress_liststore.append(row_list)


    def classic_mode_tab_move_row(self, up_flag):

        """Called by mainapp.TartubeApp.on_button_classic_move_up() and
        .on_button_classic_move_down().

        Moves the selected row(s) up/down in the Classic Progress List.

        Args:

            up_flag (bool): True to move up, False to move down

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13939 classic_mode_tab_move_row')

        selection = self.classic_progress_treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        if not path_list:

            # Nothing selected
            return

        # Move each selected row up (or down)
        if up_flag:

            # Move up
            for path in path_list:

                this_iter = model.get_iter(path)
                if model.iter_previous(this_iter):

                    self.classic_progress_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)
                    return

        else:

            # Move down
            path_list.reverse()

            for path in path_list:

                this_iter = model.get_iter(path)
                if model.iter_next(this_iter):

                    self.classic_progress_liststore.move_after(
                        this_iter,
                        model.iter_next(this_iter),
                    )

                else:

                    return


    def classic_mode_tab_remove_rows(self, dbid_list):

        """Called by mainapp.TartubeApp.on_button_classic_remove and
        .on_button_classic_clear.().

        Removes the selected rows from the Classic Progress List and updates
        IVs.

        Args:

            dbid_list (list): The .dbids for the dummy media.Video object
                corresponding to each selected row

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 14005 classic_mode_tab_remove_rows')

        # (Import IVs for convenience)
        manager_obj = self.app_obj.download_manager_obj

        # Check each row in turn
        for dbid in dbid_list:

            # If there is a current download operation, we need to update it
            if manager_obj:

                # If this dummy media.Video object is the one being downloaded,
                #   halt the download
                for worker_obj in manager_obj.worker_list:

                    if worker_obj.running_flag \
                    and worker_obj.download_item_obj \
                    and worker_obj.download_item_obj.media_data_obj.dbid \
                    == dbid:
                        worker_obj.downloader_obj.stop()

            # Delete the dummy media.Video object
            del self.classic_media_dict[dbid]

            # Remove the row from the treeview
            row_iter = self.classic_mode_tab_find_row_iter(dbid)
            if row_iter:
                self.classic_progress_liststore.remove(row_iter)


    def classic_mode_tab_add_urls(self):

        """Called by mainapp.TartubeApp.on_button_classic_add_urls().

        Also called by self.on_classic_textbuffer_changed().

        In the Classic Mode tab, transfers URLs from the textview into the
        Classic Progress List (a treeview), creating a new dummy media.Video
        object for each URL, and updating IVs.

        Return values:

            Returns a list of URLs added to the Classic Progress List (which
                may be empty)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 14053 classic_mode_tab_add_urls')

        # Get the specified download destination
        tree_iter = self.classic_dest_dir_combo.get_active_iter()
        model = self.classic_dest_dir_combo.get_model()
        dest_dir = model[tree_iter][0]

        # Get the specified video/audio format, leaving the value as None if
        #   the 'Default' item is selected
        tree_iter2 = self.classic_format_combo.get_active_iter()
        model2 = self.classic_format_combo.get_model()
        format_str = model2[tree_iter2][0]
        # (Valid formats begin with whitespace)
        if not re.search(r'^\s', format_str):
            format_str = None
        else:
            format_str = re.sub(r'^\s*', '', format_str)
            # (One last check for a valid video/audio format)
            if not format_str in formats.VIDEO_FORMAT_LIST \
            and not format_str in formats.AUDIO_FORMAT_LIST:
                format_str = None

        # Set the specified resolution, leaving the value as None if the
        #   'Highest' item is selected
        tree_iter3 = self.classic_resolution_combo.get_active_iter()
        model3 = self.classic_resolution_combo.get_model()
        resolution_str = model3[tree_iter3][0]
        # (Selectable resolutions in the combo begin with whitespace)
        if not re.search(r'^\s', resolution_str):
            resolution_str = None
        else:
            resolution_str = utils.strip_whitespace(resolution_str)
            # (One last check for a valid resolution)
            if not resolution_str in formats.VIDEO_RESOLUTION_LIST:
                resolution_str = None

        # If the combobox item is selected, we convert a downloaded video to
        #   the specified format with FFmpeg/AVConv. This is signified by
        #   adding 'convert_' to the beginning of the format string
        tree_iter4 = self.classic_convert_combo.get_active_iter()
        model4 = self.classic_convert_combo.get_model()
        convert_str = model4[tree_iter4][0]
        if format_str is not None \
        and convert_str == _('Convert to this format'):
            format_str = 'convert_' + format_str
        # The resolution, if specified, is added to the end of the format
        #   string
        if resolution_str is not None:
            if format_str is None:
                format_str = resolution_str
            else:
                format_str += '_' + resolution_str

        # Extract a list of URLs from the textview
        url_string = self.classic_textbuffer.get_text(
            self.classic_textbuffer.get_iter_at_mark(self.classic_mark_start),
            self.classic_textbuffer.get_iter_at_mark(self.classic_mark_end),
            False,
        )

        # Split the string into lines, then split each line by whitespace. This
        #   allows us to recognise multiple valid URLs on the same line, and
        #   also to interpret a line containing a URL and miscellaneous text
        url_list = []
        line_list = url_string.splitlines()
        for line in line_list:

            for url in line.split():
                url_list.append(url)

        # Remove initial/final whitespace, and ignore invalid/duplicate links
        mod_list = []
        invalid_url_string = ''
        for url in url_list:

            # Strip whitespace
            mod_url = utils.strip_whitespace(url)

            # Check for duplicates
            invalid_flag = False

            if url in mod_list:
                invalid_flag = True

            else:

                for other_obj in self.classic_media_dict.values():
                    if other_obj.source == url:
                        invalid_flag = True
                        break

            if not invalid_flag and not utils.check_url(mod_url):
                invalid_flag = True

            if not invalid_flag:
                mod_list.append(mod_url)
            else:
                # Invalid links can stay in the textview. Hopefully it's
                #   obvious to the user why an invalid link hasn't been added
                if not invalid_url_string:
                    invalid_url_string = mod_url
                else:
                    invalid_url_string += '\n' + mod_url

        # For each valid link, create a dummy media.Video object. The dummy
        #   objects have negative .dbids, and are not added to the media data
        #   registry
        for url in mod_list:

            self.classic_mode_tab_create_dummy_video(
                url,
                dest_dir,
                format_str,
            )

        # Unless the flag is set, any invalid links remain in the textview (but
        #   in all cases, all valid links are removed from it)
        # When this function is called by self.on_classic_textbuffer_changed(),
        #   Gtk generates a bunch of errors/warnings when we try to
        #   .set_text(); the workaround is to use a timer
        if not self.app_obj.classic_duplicate_remove_flag:
            GObject.timeout_add(
                0,
                self.classic_mode_tab_update_textbuffer,
                invalid_url_string,
            )

        else:
            GObject.timeout_add(
                0,
                self.classic_mode_tab_update_textbuffer,
                '',
            )

        return mod_list


    def classic_mode_tab_insert_url(self, url, options_obj):

        """Called by mainwin.DropZoneBox.on_drag_data_received().

        A modified version of self.classic_mode_tab_add_urls().

        Inserts a single URL into the Classic Progress List, creating a dummy
        media.Video object for it. The URL is downloaded using the specified
        options.OptionsManager object.

        The contents of the 'Destination' box is used, but the contents of
        the 'Format' boxes are ignored.

        Args:

            url (str): The URL to download. This function assumes the calling
                code has already stripped leading/trailing whitespace

            options_obj (options.OptionsManager): Download options for this URL

        Return values:

            True on success, False on failure

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 14217 classic_mode_tab_insert_url')

        # Sanity check
        if url is None \
        or not utils.check_url(url) \
        or options_obj is None:
            self.app_obj.system_error(
                224,
                'Invalid insert URL into Classic Progress List request',
            )

            return False

        # Get the specified download destination
        tree_iter = self.classic_dest_dir_combo.get_active_iter()
        model = self.classic_dest_dir_combo.get_model()
        dest_dir = model[tree_iter][0]

        # Create the dummy media.Video object, which has a negative .dbid, and
        #   is not added to the media data registry
        dummy_obj = self.classic_mode_tab_create_dummy_video(
            url,
            dest_dir,
        )

        if not dummy_obj:
            return False
        else:
            dummy_obj.set_options_obj(options_obj)
            return True


    def classic_mode_tab_update_textbuffer(self, text):

        """Called by a timer in self.classic_mode_tab_add_urls() in order to
        replace the contents of the Gtk.TextView at the top of the tab, without
        generating a bunch of Gtk errors/warnings.

        Args:
            text (str): The new text for the textbuffer

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 14261 classic_mode_tab_update_textbuffer')

        # (Prevent an infinite loop by using this flag)
        self.classic_auto_copy_check_flag = True
        self.classic_textbuffer.set_text(text)
        self.classic_auto_copy_check_flag = False


    def classic_mode_tab_create_dummy_video(self, url, dest_dir, \
    format_str=None, ignore_extras_flag=False):

        """Called by self.classic_mode_tab_add_urls(),
        self.on_video_catalogue_process_clip_classic_mode() or
        mainapp.TartubeApp.download_manager_finished().

        Creates a dummy media.Video object. The dummy object has a negative
        .dbid, and is not added to the media data registry.

        In the Classic Mode tab, adds a line to the Classic Progress List (a
        treeview).

        Args:

            url (str): A URL representing a video, channel or playlist, to be
                stored in the new dummy media.Video object

            dest_dir (str): Full path to the directory into which any videos
                (and other files) are downloaded

            format_str (str or None): A string specifying the media format to
                download, or None if the user didn't specify one. The string
                is made up of three optional components in a fixed order and
                separated by underlines: 'convert', the video/audio format, and
                the video resolution, for example 'mp4', 'mp4_720p',
                'convert_mp4_720p'. Valid values are those specified by
                formats.VIDEO_FORMAT_LIST, formats.AUDIO_FORMAT_LIST and
                formats.VIDEO_RESOLUTION_LIST

            ignore_extras_flag (bool): If True, called from
                self.on_video_catalogue_process_clip_classic_mode(), in which
                livestream/SponsorBlock settings should be ignored

        Return values:

            The dummy media.Video object created

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 14310 classic_mode_tab_create_dummy_video')

        self.classic_media_total += 1

        new_obj = media.Video(
            self.app_obj,
            (self.classic_media_total) * -1,    # Negative .dbid
            self.app_obj.default_video_name,
        )

        new_obj.set_dummy(url, dest_dir, format_str)

        if not ignore_extras_flag:
            if self.app_obj.classic_livestream_flag:
                new_obj.set_live_mode(2)

            if self.app_obj.classic_sblock_flag:
                new_obj.set_dummy_sblock_flag(True)

        # Add a line to the treeview
        self.classic_mode_tab_add_row(new_obj)

        # Update IVs
        self.classic_media_dict[new_obj.dbid] = new_obj

        # If a download operation, generated by the Classic Mode tab, is in
        #   progress, then we can add this URL directly to the
        #   downloads.DownloadList object
        manager_obj = self.app_obj.download_manager_obj

        if manager_obj \
        and manager_obj.operation_classic_flag \
        and manager_obj.running_flag \
        and manager_obj.download_list_obj:
            manager_obj.download_list_obj.create_dummy_item(new_obj)

        return new_obj


    def classic_mode_tab_extract_pending_urls(self):

        """Called by mainapp.TartubeApp.save_config().

        If the user wants to remember undownloaded URLs from a previous
        session, extracts them from the textview at the top of the tab, and
        the treeview at the bottom of it.

        Return values:

            A list of URLs (may be an empty list)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 14364 classic_mode_tab_extract_pending_urls')

        # Extract a list of URLs from the textview
        url_string = self.classic_textbuffer.get_text(
            self.classic_textbuffer.get_iter_at_mark(self.classic_mark_start),
            self.classic_textbuffer.get_iter_at_mark(self.classic_mark_end),
            False,
        )

        # Split the string into lines, then split each line by whitespace. This
        #   allows us to recognise multiple valid URLs on the same line, and
        #   also to interpret a line containing a URL and miscellaneous text
        url_list = []
        line_list = url_string.splitlines()
        for line in line_list:

            for url in line.split():
                url_list.append(url)

        # Remove initial/final whitespace, and ignore invalid/duplicate links
        mod_list = []
        for url in url_list:

            # Strip whitespace
            mod_url = utils.strip_whitespace(url)

            if not mod_url in url_list \
            and utils.check_url(mod_url):
                mod_list.append(mod_url)

        # From the treeview, check each dummy media.Video object, and add the
        #   URL for any undownloaded video (but ignore duplicates)
        for dummy_obj in self.classic_media_dict.values():

            if not dummy_obj.dummy_dl_flag \
            and dummy_obj.source is not None \
            and not dummy_obj.source in mod_list:
                mod_list.append(dummy_obj.source)

        return mod_list


    def classic_mode_tab_restore_urls(self, url_list):

        """Called by mainapp.TartubeApp.start().

        If the user wants to remember undownloaded URLs from a previous
        session, then restore them to the Classic Mode tab's textview.

        Args:

            url_list (list): A list of URLs to restore

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 14420 classic_mode_tab_restore_urls')

        self.classic_textbuffer.set_text('\n'.join(url_list))


    def classic_mode_tab_find_row_iter(self, dbid):

        """Called by self.classic_mode_tab_remove_rows() and
        .classic_mode_tab_display_dl_stats().

        Finds the GtkTreeIter for the Classic Progress List row displaying the
        specified data for the dummy media.Video object.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 14435 classic_mode_tab_find_row_iter')

        for row in self.classic_progress_liststore:
            if self.classic_progress_liststore[row.iter][0] == dbid:
                return row.iter


    def classic_mode_tab_receive_dl_stats(self, download_item_obj,
    dl_stat_dict, finish_flag=False):

        """Called by downloads.DownloadWorker.data_callback().

        A modified form of self.progress_list_receive_dl_stats(), used during
        a download operation launched from the Classic Mode tab.

        Stores download statistics until they can be displayed (as in the
        original function)

        Args:

            download_item_obj (downloads.DownloadItem): The download item
                object handling a download for a dummy media.Video object

            dl_stat_dict (dict): The dictionary of download statistics
                described in the original function

            finish_flag (bool): True if the worker has finished with its
                dummy media.Video object, meaning that dl_stat_dict is the
                final set of statistics, and that the progress list row can be
                hidden, if required

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 14469 classic_mode_tab_receive_dl_stats')

        # Temporarily store the dictionary of download statistics
        if not download_item_obj.item_id in self.classic_temp_dict:
            new_dl_stat_dict = {}
        else:
            new_dl_stat_dict \
            = self.classic_temp_dict[download_item_obj.item_id]

        for key in dl_stat_dict:
            new_dl_stat_dict[key] = dl_stat_dict[key]

        self.classic_temp_dict[download_item_obj.item_id] \
        = new_dl_stat_dict


    def classic_mode_tab_display_dl_stats(self):

        """Called by downloads.DownloadManager.run() and
        mainapp.TartubeApp.dl_timer_callback().

        A modified form of self.progress_list_display_dl_stats(), used during
        a download operation launched from the Classic Mode tab.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 14495 classic_mode_tab_display_dl_stats')

        # Import the contents of the IV (in case it gets updated during the
        #   call to this function), and use the imported copy
        temp_dict = self.classic_temp_dict
        self.classic_temp_dict = {}

        # For each dummy media.Video object displayed in the download list...
        for dbid in temp_dict:

            # Get a dictionary of download statistics for this dummy
            #   media.Video object
            # The dictionary is in the standard format described in the
            #   comments to downloads.VideoDownloader.extract_stdout_data()
            dl_stat_dict = temp_dict[dbid]

            # During pre-processing, make sure a filename from a previous call
            #   is not visible
            if 'status' in dl_stat_dict \
            and dl_stat_dict['status'] == formats.ACTIVE_STAGE_PRE_PROCESS:
                dl_stat_dict['filename'] = ''
                pre_process_flag = True
            else:
                pre_process_flag = False

            # Get the dummy media.Video object itself
            if not dbid in self.classic_media_dict:
                # Row has already been deleted by the user
                continue
            else:
                media_data_obj = self.classic_media_dict[dbid]

            # Get the corresponding treeview row
            row_iter = self.classic_mode_tab_find_row_iter(dbid)
            if not row_iter:
                # Row has already been deleted by the user
                continue
            else:
                row_path = self.classic_progress_liststore.get_path(row_iter)

            # Update the tooltip
            self.classic_progress_liststore.set(
                row_iter,
                self.classic_progress_tooltip_column,
                html.escape(
                    media_data_obj.fetch_tooltip_text(
                        self.app_obj,
                        self.tooltip_max_len,
                        True,           # Show errors/warnings
                    ),
                ),
            )

            # Update statistics displayed in this row
            # (Column 0 is not modified, once the row has been added to the
            #   treeview)
            column = 2

            for key in (
                'playlist_index',
                'status',
                'filename',
                'extension',
                'percent',
                'speed',
                'eta',
                'filesize',
            ):
                column += 1

                if key in dl_stat_dict:

                    if key == 'playlist_index':

                        if dl_stat_dict['playlist_index'] == 0:
                            string = ''

                        elif 'dl_sim_flag' in dl_stat_dict \
                        and dl_stat_dict['dl_sim_flag']:
                            # (Don't know how many videos there are in a
                            #   channel/playlist, so ignore value of
                            #   'playlist_size')
                            string = str(dl_stat_dict['playlist_index'])

                        else:
                            string = str(dl_stat_dict['playlist_index'])
                            if 'playlist_size' in dl_stat_dict \
                            and dl_stat_dict['playlist_size'] > 0:
                                string = string + '/' \
                                + str(dl_stat_dict['playlist_size'])
                            else:
                                string = string + '/1'

                    elif key == 'filename':

                        # Don't overwrite the filename, so that users can more
                        #   easily identify failed downloads
                        # (Exception: when splitting a video into clips,
                        #   always show the clip name)
                        if dl_stat_dict[key] == '' and not pre_process_flag:
                            continue
                        elif media_data_obj.file_name is not None \
                        and not 'clip_flag' in dl_stat_dict:
                            string = media_data_obj.file_name
                        else:
                            string = dl_stat_dict[key]

                    else:
                        string = dl_stat_dict[key]

                    self.classic_progress_liststore.set(
                        self.classic_progress_liststore.get_iter(row_path),
                        column,
                        string,
                    )


    def classic_mode_tab_timer_callback(self):

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

        Periodically checks the system's clipboard, and adds any new URLs to
        the Classic Progress List.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 14621 classic_mode_tab_timer_callback')

        # If the user manually empties the textview, don't re-paste whatever
        #   is currently in the clipboard
        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        cliptext = clipboard.wait_for_text()

        if cliptext != '':

            if self.classic_auto_copy_text is not None \
            and cliptext == self.classic_auto_copy_text:

                # Return 1 to keep the timer going
                return 1

            else:

                self.classic_auto_copy_text = cliptext

        utils.add_links_to_textview_from_clipboard(
            self.app_obj,
            self.classic_textbuffer,
            self.classic_mark_start,
            self.classic_mark_end,
        )

        # Return 1 to keep the timer going
        return 1


    def classic_mode_tab_start_download(self):

        """Called by mainapp.TartubeApp.on_button_classic_download() and
        self.on_classic_textbuffer_changed().

        Starts a download operation for the URLs added to the Classic Progress
        List.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 14661 classic_mode_tab_start_download')

        if self.app_obj.download_manager_obj:

            # Download already in progress
            return

        elif not self.app_obj.classic_custom_dl_flag:

            # Start an (ordinary) download operation
            self.app_obj.download_manager_start('classic_real')

        elif self.app_obj.classic_custom_dl_obj.dl_by_video_flag:

            # If the user has opted to download each video independently of its
            #   channel or playlist, then we have to do a simulated download
            #   first, in order to collect the URLs of each invidual video
            #   ('classic_sim')
            # When that download operation has finished, we can do a (real)
            #   custom download for each video ('classic_custom')
            self.app_obj.download_manager_start(
                'classic_sim',
                False,          # Not called by slow timer
                [],             # Download all URLs
                self.app_obj.classic_custom_dl_obj,
            )

        else:

            # Otherwise, a full custom download can proceed immediately,
            #   without performing the simulated download first
            self.app_obj.download_manager_start(
                'classic_custom',
                False,          # Not called by slow timer
                [],             # Download all URLs
                self.app_obj.classic_custom_dl_obj,
            )


    def classic_mode_tab_get_column_widths(self):

        """Called by mainapp.TartubeApp.save_config().

        Fetches the width of the 'Source' and 'Incoming file' columns in the
        Classic Progress List.

        Return values:

            The two widths, or None if the list doesn't exist yet

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 14714 classic_mode_tab_get_column_widths')

        if self.classic_progress_treeview is None:
            return None, None

        else:
            source_column = self.classic_progress_treeview.get_column(2)
            source_width = source_column.get_width()
            # (Shouldn't be possible to reduce the width below the minimum,
            #   but we'll check anyway)
            if source_width < 20:
                source_width = 20

            incoming_column = self.classic_progress_treeview.get_column(5)
            incoming_width = incoming_column.get_width()
            if incoming_width < 20:
                incoming_width = 20

            return source_width, incoming_width


    # (Drag and Drop tab)


    def drag_drop_grid_empty(self):

        """Can be called by anything.

        Draws an empty grid in the Drag and Drop tab (replacing any grid that
        already exists).
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 14747 drag_drop_grid_empty')

        # If not called by self.setup_videos_tab()...
        if self.drag_drop_frame.get_child():
            self.drag_drop_frame.remove(self.drag_drop_frame.get_child())

        # Replace the grid
        self.drag_drop_grid = Gtk.Grid()
        self.drag_drop_frame.add(self.drag_drop_grid)
        self.drag_drop_grid.set_column_spacing(self.spacing_size)
        self.drag_drop_grid.set_row_spacing(self.spacing_size)
        self.drag_drop_grid.set_column_homogeneous(True)
        self.drag_drop_grid.set_row_homogeneous(True)

        # Procedure complete
        self.drag_drop_grid.show_all()


    def drag_drop_grid_reset(self):

        """Can be called by anything.

        Draws a grid of mainwin.DropZoneBox objects in the Drag and Drop tab
        (replacing any grid that already exists). Each mainwin.DropZoneBox is
        associated with a set of download options (options.OptionsManager).

        The code for the Drag and Drop tab is fairly simple.
        self.drag_drop_add_dropzone() can be called to add a new dropzone, but
        for everything else, we just call this function to reset the grid.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 14779 drag_drop_grid_reset')

        # If not called by self.setup_videos_tab()...
        if self.drag_drop_frame.get_child():
            self.drag_drop_frame.remove(self.drag_drop_frame.get_child())

        # (Temporarily retain the old dropzones, so we can preserve their
        #   confirmation messages and reset times)
        old_dict = self.drag_drop_dict
        self.drag_drop_dict = {}

        # Replace the grid
        self.drag_drop_grid = Gtk.Grid()
        self.drag_drop_frame.add(self.drag_drop_grid)
        self.drag_drop_grid.set_column_spacing(self.spacing_size)
        self.drag_drop_grid.set_row_spacing(self.spacing_size)
        self.drag_drop_grid.set_column_homogeneous(True)
        self.drag_drop_grid.set_row_homogeneous(True)

        # Set up dropzones on the grid. The minimum size is 1x1, maximum is
        #   self.drag_drop_max (we assume it is not a prime number, as
        #   discussed in the comments in self.__init__() )
        # If there aren't enough options.OptionsManager objects to fill a grid,
        #   then we use an empty dropzone (one whose .options_obj IV is set to
        #   None)
        actual_size = grid_size = len(self.app_obj.classic_dropzone_list)
        if grid_size > self.drag_drop_max:
            grid_size = self.drag_drop_max

        # Create the smallest grid possible, checking for the suitability of
        #   grid sizes in the order 1x1, 2x1, 2x2, 3x2, 3x3...
        w = None
        h = None
        dim = 0
        while w is None and h is None:

            dim += 1

            if grid_size <= dim * dim:
                w = dim
                h = dim
            elif grid_size <= dim * (dim + 1):
                w = dim + 1
                h = dim

        # Add drop zones at every location in the grid
        index = -1
        for y_pos in range(h):
            for x_pos in range(w):

                index += 1
                if index < actual_size:
                    uid = self.app_obj.classic_dropzone_list[index]
                    options_obj = self.app_obj.options_reg_dict[uid]
                else:
                    options_obj = None

                # Instead of using Gtk.Frame directly, use a wrapper class so
                #   we can quickly retrieve the options.OptionsManager object
                #   displayed in each dropzone
                if not options_obj \
                or not options_obj.uid in self.app_obj.options_reg_dict \
                or not options_obj.uid in old_dict:
                    update_text = None
                    reset_time = None
                else:
                    # Preserve the previous confirmation message
                    old_wrapper_obj = old_dict[options_obj.uid]
                    update_text = old_wrapper_obj.update_text
                    reset_time = old_wrapper_obj.reset_time

                wrapper_obj = DropZoneBox(
                    self,
                    options_obj,
                    x_pos,
                    y_pos,
                    h,
                    update_text,
                    reset_time,
                )

                if wrapper_obj:
                    self.drag_drop_grid.attach(wrapper_obj, x_pos, y_pos, 1, 1)
                    if options_obj:
                        self.drag_drop_dict[options_obj.uid] = wrapper_obj

        # (De)sensitie the add button, as appropriate
        if actual_size >= self.drag_drop_max:
            self.drag_drop_add_button.set_sensitive(False)
        else:
            self.drag_drop_add_button.set_sensitive(True)

        # Procedure complete
        self.drag_drop_grid.show_all()


    def drag_drop_add_dropzone(self):

        """Called by mainapp.TartubeApp.on_button_drag_drop_add() or by any
        other code.

        Prompts the user to create a new set of download options, or to use
        an existing set.

        Adds a new dropzone to the Drag and Drop tab's grid to accommodate it.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 14887 drag_drop_add_dropzone')

        if len(self.drag_drop_dict) >= self.drag_drop_max:
            return self.app_obj.system_error(
                225,
                'Drag and Drop tab out of space',
            )

        # Prompt the user to select one of existing options.OptionsManager
        #   objects, or to create a new one
        dialogue_win = AddDropZoneDialogue(self)
        response = dialogue_win.run()
        # Get the specified options.OptionsManager object, before
        #   destroying the window
        options_name = dialogue_win.options_name
        options_obj = dialogue_win.options_obj
        clone_flag = dialogue_win.clone_flag
        dialogue_win.destroy()

        edit_win_flag = False

        if response == Gtk.ResponseType.OK \
        and (
            options_name is not None \
            or options_obj is not None \
            or clone_flag
        ):
            if options_name is not None:

                options_obj = self.app_obj.create_download_options(
                    options_name,
                )

                edit_win_flag = True

            elif clone_flag:

                options_obj = self.app_obj.clone_download_options(
                    options_obj,
                )

                edit_win_flag = True

            # Add the new dropzone
            self.app_obj.add_classic_dropzone_list(options_obj.uid)
            # Redraw the grid
            self.drag_drop_grid_reset()

            if edit_win_flag:
                # Open an edit window to show the new/cloned options
                #   immediately
                config.OptionsEditWin(
                    self.app_obj,
                    options_obj,
                )


    # (Output tab)


    def output_tab_setup_pages(self):

        """Called by mainapp.TartubeApp.start() and .set_num_worker_default().

        Makes sure there are enough pages in the Output tab's notebook for
        each simultaneous download allowed (a value specified by
        mainapp.TartubeApp.num_worker_default).
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 14957 output_tab_setup_pages')

        # The first page in the Output tab's notebook shows a summary of what
        #   the threads created by downloads.py are doing
        if not self.output_tab_summary_flag \
        and self.app_obj.ytdl_output_show_summary_flag:
            self.output_tab_add_page(True)
            self.output_tab_summary_flag = True

        # The number of pages in the notebook (not including the summary page)
        #   should match the highest value of these two things during this
        #   session:
        #
        #   - The maximum simultaneous downloads allowed
        #   - If a download operation is in progress, the actual number of
        #       download.DownloadWorker objects created
        #
        # Thus, if the user reduces the maximum, we don't remove pages, but we
        #   do add new pages if the maximum is increased
        # Broadcasting livestreams might be exempt from the maximum, so the
        #   number of workers might be larger than it
        count = self.app_obj.num_worker_default
        if self.app_obj.download_manager_obj:

            worker_count = len(self.app_obj.download_manager_obj.worker_list)
            if worker_count > count:
                count = worker_count

        if self.output_page_count < count:

            for num in range(1, (count + 1)):
                if not num in self.output_textview_dict:
                    self.output_tab_add_page()


    def output_tab_add_page(self, summary_flag=False):

        """Called by self.output_tab_setup_pages().

        Adds a new page to the Output tab's notebook, and updates IVs.

        Args:

            summary_flag (bool): If True, add the (first) summary page to the
                notebook, showing what the threads are doing

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 15006 output_tab_add_page')

        # Each page (except the summary page) corresponds to a single
        #   downloads.DownloadWorker object. The page number matches the
        #   worker's .worker_id. The first worker is numbered #1
        if not summary_flag:
            self.output_page_count += 1

        # Add the new page
        tab = Gtk.Box()

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Thread\' means a computer processor' \
            + ' thread. If you\'re not sure how to translate it, just use' \
            + ' \'Page #\', as in Page #1, Page #2, etc',
        )

        if not summary_flag:
            label = Gtk.Label.new_with_mnemonic(
                _('Thread') + ' #_' + str(self.output_page_count),
            )
        else:
            label = Gtk.Label.new_with_mnemonic(_('_Summary'))

        self.output_notebook.append_page(tab, label)
        tab.set_hexpand(True)
        tab.set_vexpand(True)
        tab.set_border_width(self.spacing_size)

        # Add a textview to the tab, using a css style sheet to provide
        #   monospaced white text on a black background
        # N.B. On MS Windows, the monospace font can't display Japanese/
        #   Korean characters; solve the problem by not specifying a font at
        #   all. (A non-monospace font is visible, but that's better than
        #   gibberish)
        scrolled = Gtk.ScrolledWindow()
        tab.pack_start(scrolled, True, True, 0)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)

        frame = Gtk.Frame()
        scrolled.add_with_viewport(frame)

        style_text = '#css_text_id_' + str(self.output_page_count) \
            + ', textview text {\n' \
            + '   background-color: ' + self.output_tab_bg_colour + ';\n' \
            + '   color: ' + self.output_tab_text_colour + ';\n' \
            + '}\n' \
            + '#css_label_id_' + str(self.output_page_count) \
            + ', textview {\n'

        if os.name != 'nt':
            style_text = style_text + '   font-family: monospace, monospace;\n'

        style_text = style_text + '   font-size: 10pt;\n' \
            + '}'

        style_provider = self.output_tab_set_textview_css(style_text)

        textview = Gtk.TextView()
        frame.add(textview)
        textview.set_wrap_mode(Gtk.WrapMode.WORD)
        textview.set_editable(False)
        textview.set_cursor_visible(False)

        context = textview.get_style_context()
        context.add_provider(style_provider, 600)

        # Reset css properties for the next Gtk.TextView created (for example,
        #   by AddVideoDialogue) so it uses default values, rather than the
        #   white text on black background used above
        # To do that, create a dummy textview, and apply a css style to it
        textview2 = Gtk.TextView()
        style_provider2 = self.output_tab_set_textview_css(
            '#css_text_id_default, textview text {\n' \
            + '   background-color: unset;\n' \
            + '   color: unset;\n' \
            + '}\n' \
            + '#css_label_id_default, textview {\n' \
            + '   font-family: unset;\n' \
            + '   font-size: unset;\n' \
            + '}'
        )

        context = textview2.get_style_context()
        context.add_provider(style_provider2, 600)

        # Set up auto-scrolling
        textview.connect(
            'size-allocate',
            self.output_tab_do_autoscroll,
            scrolled,
        )

        # Make the page visible
        self.show_all()

        # Update IVs
        if not summary_flag:
            self.output_textview_dict[self.output_page_count] = textview
        else:
            self.output_textview_dict[0] = textview


    def output_tab_set_textview_css(self, css_string):

        """Called by self.output_tab_add_page().

        Applies a CSS style to the current screen. Called once to create a
        white-on-black Gtk.TextView, then a second time to create a dummy
        textview with default properties.

        Args:

            css_string (str): The CSS style to apply

        Return values:

            The Gtk.CssProvider created

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 15121 output_tab_set_textview_css')

        style_provider = Gtk.CssProvider()
        style_provider.load_from_data(bytes(css_string.encode()))
        Gtk.StyleContext.add_provider_for_screen(
            Gdk.Screen.get_default(),
            style_provider,
            Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
        )

        return style_provider


    def output_tab_write_stdout(self, page_num, msg):

        """Called by various functions in downloads.py, info.py, refresh.py,
        tidy.py and updates.py.

        During a download operation, youtube-dl sends output to STDOUT. If
        permitted, this output is displayed in the Output tab. Other operations
        also call this function to display text in the default colour.

        Args:

            page_num (int): The page number on which this message should be
                displayed. Matches a key in self.output_textview_dict

            msg (str): The message to display. A newline character will be
                added by self.output_tab_write().

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 15154 output_tab_write_stdout')

        GObject.timeout_add(
            0,
            self.output_tab_write,
            page_num,
            msg,
            'default',
        )


    def output_tab_write_stderr(self, page_num, msg):

        """Called by various functions in downloads.py and info.py.

        During a download operation, youtube-dl sends output to STDERR. If
        permitted, this output is displayed in the Output tab. Other operations
        also call this function to display text in the non-default colour.

        Args:

            page_num (int): The page number on which this message should be
                displayed. Matches a key in self.output_textview_dict

            msg (str): The message to display. A newline character will be
                added by self.output_tab_write().

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 15184 output_tab_write_stderr')

        GObject.timeout_add(
            0,
            self.output_tab_write,
            page_num,
            msg,
            'error_warning',
        )


    def output_tab_write_system_cmd(self, page_num, msg):

        """Called by various functions in downloads.py, info.py and updates.py.

        During a download operation, youtube-dl system commands are displayed
        in the Output tab (if permitted). Other operations also call this
        function to display text in the non-default colour.

        Args:

            page_num (int): The page number on which this message should be
                displayed. Matches a key in self.output_textview_dict

            msg (str): The message to display. A newline character will be
                added by self.output_tab_write().

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 15214 output_tab_write_system_cmd')

        GObject.timeout_add(
            0,
            self.output_tab_write,
            page_num,
            msg,
            'system_cmd',
        )


    def output_tab_write(self, page_num, msg, msg_type):

        """Called by self.output_tab_write_stdout(), .output_tab_write_stderr()
        and .output_tab_write_system_cmd().

        Writes a message to the output tab.

        N.B. Because Gtk is not thread safe, this function must always be
        called from within GObject.timeout_add().

        Args:

            page_num (int): The page number on which this message should be
                displayed. Matches a key in self.output_textview_dict

            msg (str): The message to display. A newline character will be
                added by this function

            msg_type (str): 'default', 'error_warning' or 'system_cmd'

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 15248 output_tab_write')

        # Add the text to the textview. STDERR messages and system commands are
        #   displayed in a different colour
        # (Note that the summary page is not necessarily visible)
        if page_num in self.output_textview_dict:

            textview = self.output_textview_dict[page_num]
            textbuffer = textview.get_buffer()

            # If the buffer is too big, remove the first line to make way for
            #   the new one
            if self.app_obj.output_size_apply_flag \
            and textbuffer.get_line_count() > self.app_obj.output_size_default:
                textbuffer.delete(
                    textbuffer.get_start_iter(),
                    textbuffer.get_iter_at_line_offset(1, 0),
                )

            if msg_type != 'default':

                # The .markup_escape_text() call won't escape curly braces, so
                #   we need to replace those manually
                msg = re.sub('{', '(', msg)
                msg = re.sub('}', ')', msg)

                string = '<span color="{:s}">' \
                + GObject.markup_escape_text(msg) + '</span>\n'

                if msg_type == 'system_cmd':

                    textbuffer.insert_markup(
                        textbuffer.get_end_iter(),
                        string.format(self.output_tab_system_cmd_colour),
                        -1,
                    )

                else:

                    # STDERR
                    textbuffer.insert_markup(
                        textbuffer.get_end_iter(),
                        string.format(self.output_tab_stderr_colour),
                        -1,
                    )

            else:

                # STDOUT
                textbuffer.insert(
                    textbuffer.get_end_iter(),
                    msg + '\n',
                )

        # Make the new output visible, and scroll to the bottom of every
        #   updated page
        self.output_tab_scroll_visible_page(page_num)


    def output_tab_update_page_size(self):

        """Called by mainapp.TartubeApp.set_output_size_default().

        When a page size is applied, count the number of lines in each
        textview, and remove the oldest remaining lines, if necessary.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 15316 output_tab_update_page_size')

        if self.app_obj.output_size_apply_flag:

            for page_num in self.output_textview_dict:

                textview = self.output_textview_dict[page_num]
                textbuffer = textview.get_buffer()
                line_count = textbuffer.get_line_count()

                if line_count >= self.app_obj.output_size_default:
                    textbuffer.delete(
                        textbuffer.get_start_iter(),
                        textbuffer.get_iter_at_line_offset(
                            line_count - self.app_obj.output_size_default - 1,
                            0,
                        ),
                    )


    def output_tab_do_autoscroll(self, textview, rect, scrolled):

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

        When one of the textviews in the Output tab is modified (text added or
        removed), make sure the page is scrolled to the bottom.

        Args:

            textview (Gtk.TextView): The textview to scroll

            rect (Gdk.Rectangle): Object describing the window's new size

            scrolled (Gtk.ScrolledWindow): The scroller which contains the
                textview

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 15355 output_tab_do_autoscroll')

        adj = scrolled.get_vadjustment()
        adj.set_value(adj.get_upper() - adj.get_page_size())


    def output_tab_scroll_visible_page(self, page_num):

        """Called by self.on_output_notebook_switch_page() and
        .on_notebook_switch_page().

        When the user switches between pages in the Output tab, scroll the
        visible textview to the bottom (otherwise it gets confusing).

        Args:

            page_num (int): The page to be scrolled, matching a key in
                self.output_textview_dict

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 15377 output_tab_scroll_visible_page')

        if page_num in self.output_textview_dict:
            textview = self.output_textview_dict[page_num]

            frame = textview.get_parent()
            viewport = frame.get_parent()
            scrolled = viewport.get_parent()

            adj = scrolled.get_vadjustment()
            adj.set_value(adj.get_upper() - adj.get_page_size())

            textview.show_all()


    def output_tab_show_first_page(self):

        """Called by mainapp.TartubeApp.update_manager_start().

        Switches to the first tab of the Output tab (not including the summary
        tab, if it's open).
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 15401 output_tab_show_first_page')

        self.notebook.set_current_page(self.notebook_tab_dict['output'])
        if not self.output_tab_summary_flag:
            self.output_notebook.set_current_page(0)
        else:
            self.output_notebook.set_current_page(1)


    def output_tab_reset_pages(self):

        """Called by mainapp.TartubeApp.download_manager_continue(),
        .update_manager_start(), .refresh_manager_continue(),
        .info_manager_start() and .tidy_manager_start().

        At the start of an operation, empty the pages in the Output tab (if
        allowed).
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 15421 output_tab_reset_pages')

        for textview in self.output_textview_dict.values():
            textbuffer = textview.get_buffer()
            textbuffer.set_text('')
            textview.show_all()


    # (Errors/Warnings tab)


    def errors_list_reset(self):

        """Can be called by anything.

        On the first call, sets up the widgets for the Errors List. On
        subsequent calls, replaces those widgets and re-populates the list,
        making error/warning messages visible or not, depending on settings.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 15442 errors_list_reset')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Extra items for the Errors/Warnings tab'
        )

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

        # If not called by self.setup_errors_tab()...
        if self.errors_list_frame.get_child():
            self.errors_list_frame.remove(self.errors_list_frame.get_child())

        # Set up the widgets
        self.errors_list_scrolled = Gtk.ScrolledWindow()
        self.errors_list_frame.add(self.errors_list_scrolled)
        self.errors_list_scrolled.set_policy(
            Gtk.PolicyType.AUTOMATIC,
            Gtk.PolicyType.AUTOMATIC,
        )

        self.errors_list_treeview = MultiDragDropTreeView()
        self.errors_list_scrolled.add(self.errors_list_treeview)
        # Allow multiple selection...
        self.errors_list_treeview.set_can_focus(True)
        selection = self.errors_list_treeview.get_selection()
        selection.set_mode(Gtk.SelectionMode.MULTIPLE)
        # ...and then set up drag and drop from the treeview to an external
        #   application (for example, an FFmpeg batch converter)
        self.errors_list_treeview.enable_model_drag_source(
            Gdk.ModifierType.BUTTON1_MASK,
            [],
            Gdk.DragAction.COPY,
        )
        self.errors_list_treeview.drag_source_add_text_targets()
        self.errors_list_treeview.connect(
            'drag-data-get',
            self.on_errors_list_drag_data_get,
        )

        # Column list:
        #   0:  [str] [hide] Video's full file path (used for drag and drop)
        #   1:  [str] [hide] Media data object's URL (used for drag and drop)
        #   2:  [str] [hide] Media data object's name (used for drag and drop)
        #   3:  [pibxuf] Message type icon
        #   4:  [pixbuf] Media type icon
        #   5:  [str] [switch] Date and time string
        #   6:  [str] [switch] Date string
        #   7:  [str] [switch] Container name
        #   8:  [str] [switch] Video name
        #   9:  [str] [switch] Full message, formatted across several lines
        #   10: [str] [switch] Shortened (one-line) message
        # We don't use the media data object's .dbid, because the media data
        #   object may have been deleted (but the error message will still be
        #   visible)
        # N.B. If this layout changes, then
        #   self.on_system_container_checkbutton_changed(), etc, must also be
        #   updated
        for i, column_title in enumerate(
            [
                'hide', 'hide', 'hide',
                '', '',
                _('Time'), _('Time'), _('Container'), _('Video'), _('Message'),
                _('Message')
            ],
        ):
            if not column_title:
                renderer_pixbuf = Gtk.CellRendererPixbuf()
                column_pixbuf = Gtk.TreeViewColumn(
                    '',
                    renderer_pixbuf,
                    pixbuf=i,
                )
                self.errors_list_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,
                )
                self.errors_list_treeview.append_column(column_text)
                if i < 3 \
                or i == 5 and not app_obj.system_msg_show_date_flag \
                or i == 6 and app_obj.system_msg_show_date_flag \
                or i == 7 and not app_obj.system_msg_show_container_flag \
                or i == 8 and not app_obj.system_msg_show_video_flag \
                or i == 9 and not app_obj.system_msg_show_multi_line_flag \
                or i == 10 and app_obj.system_msg_show_multi_line_flag:
                    column_text.set_visible(False)
                else:
                    column_text.set_resizable(True)

        # Reset widgets
        self.errors_list_liststore = Gtk.ListStore(
            str, str, str,
            GdkPixbuf.Pixbuf, GdkPixbuf.Pixbuf,
            str, str, str, str, str, str,
        )
        self.errors_list_treeview.set_model(self.errors_list_liststore)

        # Populate the list with any errors/warnings already added
        for mini_dict in self.error_list_buffer_list:
            self.errors_list_insert_row(mini_dict)

        # Update the Errors/Warnings tab label with message counts
        self.errors_list_refresh_label()

        # Make the changes visible
        self.errors_list_frame.show_all()


    def errors_list_add_operation_msg(self, media_data_obj, last_flag=False):

        """Can be called by any operation.

        When an operation generates error and/or warning messages, this
        function is called to display them in the Errors List (if settings
        permit), and to update IVs.

        Args:

            media_data_obj (media.Video, media.Channel or media.Playlist): The
                media data object whose download (real or simulated) generated
                the error/warning messages

            last_flag (bool): If True, only the last error/warning message is
                displayed (useful in case this function might be called several
                times for a single media data object)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 15577 errors_list_add_operation_msg')

        if last_flag and media_data_obj.error_list:
            error_list = [ media_data_obj.error_list[-1] ]
        else:
            error_list = media_data_obj.error_list

        if last_flag and media_data_obj.warning_list:
            warning_list = [ media_data_obj.warning_list[-1] ]
        else:
            warning_list = media_data_obj.warning_list

        # Create a new row for every error and warning message
        # Use the same time on each
        time_str = datetime.datetime.today().strftime('%x %X')
        local = utils.get_local_time()
        short_time_str = str(local.strftime('%H:%M:%S'))

        for msg in error_list:
            mini_dict = self.errors_list_prepare_operation_row(
                media_data_obj,
                'error',
                msg,
                time_str,
                short_time_str,
            )

            # Add the row to the treeview
            self.errors_list_insert_row(mini_dict)

        for msg in warning_list:
            mini_dict = self.errors_list_prepare_operation_row(
                media_data_obj,
                'warning',
                msg,
                time_str,
                short_time_str,
            )

            # Add the row to the treeview
            self.errors_list_insert_row(mini_dict)

        # Update the tab's label to show the number of warnings/errors visible
        if self.visible_tab_num != self.notebook_tab_dict['errors']:
            self.errors_list_refresh_label()


    def errors_list_prepare_operation_row(self, media_data_obj, msg_type, msg,
    time_str, short_time_str):

        """Called by self.errors_list_add_operation_msg() (only).

        Errors/Warnings sent for display in the Error List are stored in an IV,
        so the list can be filtered as required.

        Prepares a dictionary of values for this error/warning message, then
        adds it to the IV.

        Args:

            media_data_obj (media.Video, media.Channel or media.Playlist): The
                media data object whose download (real or simulated) generated
                the error/warning messages

            msg_type (str): 'error' or 'warning'

            msg (str): The text of the message itself

            time_str (str): The current date and time, as a string

            short_time_str (str): The current time, as a string

        Return values:

            The dictionary created

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 15656 errors_list_prepare_operation_row')

        # Prepare the mini-dictionary to be added to the IV
        mini_dict = {}

        if msg_type == 'error':
            mini_dict['msg_type'] = 'operation_error'
        else:
            mini_dict['msg_type'] = 'operation_warning'

        mini_dict['date_time'] = time_str
        mini_dict['time'] = short_time_str

        if isinstance(media_data_obj, media.Video):
            mini_dict['media_type'] = 'video'
            # ('Dummy' media.Video objects don't have a parent)
            if not media_data_obj.parent_obj:
                mini_dict['container_name'] = ''
            else:
                mini_dict['container_name'] = utils.shorten_string(
                    media_data_obj.parent_obj.name,
                    self.long_string_max_len,
                )
            mini_dict['video_name'] = utils.shorten_string(
                media_data_obj.name,
                self.long_string_max_len,
            )
        elif isinstance(media_data_obj, media.Channel):
            mini_dict['media_type'] = 'channel'
            mini_dict['container_name'] = utils.shorten_string(
                media_data_obj.name,
                self.long_string_max_len,
            )
            mini_dict['video_name'] = ''
        else:
            mini_dict['media_type'] = 'playlist'
            mini_dict['container_name'] = utils.shorten_string(
                media_data_obj.name,
                self.long_string_max_len,
            )
            mini_dict['video_name'] = ''

        mini_dict['msg'] = utils.tidy_up_long_string(msg)
        mini_dict['short_msg'] = utils.shorten_string(
            msg,
            self.long_string_max_len,
        )
        mini_dict['orig_msg'] = msg

        if self.visible_tab_num != self.notebook_tab_dict['errors']:
            mini_dict['count_flag'] = True
        else:
            mini_dict['count_flag'] = False

        drag_path, drag_source, drag_name = self.get_media_drag_data_as_list(
            media_data_obj,
        )
        mini_dict['drag_path'] = drag_path
        mini_dict['drag_source'] = drag_source
        mini_dict['drag_name'] = drag_name

        # Sanity check: the treeview will not accept None values
        for key in mini_dict.keys():
            if mini_dict[key] is None:
                mini_dict[key] = ''

        # Update the IV
        self.error_list_buffer_list.append(mini_dict)

        return mini_dict


    def errors_list_add_system_msg(self, error_type, error_code, msg):

        """Can be called by anything. The quickest way is to call
        mainapp.TartubeApp.system_error(), which acts as a wrapper for this
        function.

        Display a system error message in the Errors List.

        N.B. Because Gtk is not thread safe, this function must always be
        called from within GObject.timeout_add().

        Args:

            error_type (str): 'error' or 'warning'

            error_code (int): An error code in the range 100-999 (see the
                .system_error() function)

            msg (str): The system error message to display

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 15751 errors_list_add_system_msg')

        # Create a new row for every error and warning message
        # Use the same time on each
        time_str = datetime.datetime.today().strftime('%x %X')
        local = utils.get_local_time()
        short_time_str = str(local.strftime('%H:%M:%S'))

        # Prepare the mini-dictionary to be added to the IV
        mini_dict = {}

        if error_type == 'error':
            mini_dict['msg_type'] = 'system_error'
            mini_dict['container_name'] = _('Tartube error')
            mini_dict['video_name'] = ''
        elif error_type == 'warning':
            mini_dict['msg_type'] = 'system_warning'
            mini_dict['container_name'] = _('Tartube warning')
            mini_dict['video_name'] = ''
        else:
            # Failsafe
            return

        mini_dict['media_type'] = ''
        mini_dict['date_time'] = time_str
        mini_dict['time'] = short_time_str

        # (When called by mainapp.TartubeApp.system_exception(), preserve the
        #   formatting of the raised exception; otherwise tidy it up)
        if error_code == 901:
            mini_dict['msg'] = msg
        else:
            mini_dict['msg'] = utils.tidy_up_long_string(
                '#' + str(error_code) + ': ' + msg,
            )

        mini_dict['short_msg'] = utils.shorten_string(
            '#' + str(error_code) + ': ' + msg,
            self.long_string_max_len,
        )
        mini_dict['orig_msg'] = msg

        if self.visible_tab_num != self.notebook_tab_dict['errors']:
            mini_dict['count_flag'] = True
        else:
            mini_dict['count_flag'] = False

        mini_dict['drag_path'] = ''
        mini_dict['drag_source'] = ''
        mini_dict['drag_name'] = ''

        # Update the IV
        self.error_list_buffer_list.append(mini_dict)

        # Add the row to the treeview
        self.errors_list_insert_row(mini_dict)


    def errors_list_insert_row(self, mini_dict):

        """Called by self.errors_list_reset(),
        self.errors_list_add_operation_msg() and
        self.errors_list_add_system_msg().

        Called with an error/warning message to be displayed in the Errors
        List.

        Decided whether the message should be filitered out or not, depending
        on settings. If not, adds the message to the treeview.

        Args:

            mini_dict (dict): Dictionary of values (retrieved from
                self.error_list_buffer_list) representing a single error or
                warning message

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 15830 errors_list_insert_row')

        # Emergency fallback - if the error is generated before the
        #   Errors/Warnings tab has been set up, do nothing (this will avoid
        #   an infinite cycle of raised exceptions)
        if self.errors_list_treeview is None:
            return

        # Depending on settings, this row should be visible, or not
        if (
            mini_dict['msg_type'] == 'system_error' \
            and not self.app_obj.system_error_show_flag
        ) or (
            mini_dict['msg_type'] == 'system_warning' \
            and not self.app_obj.system_warning_show_flag
        ) or (
            mini_dict['msg_type'] == 'operation_error' \
            and not self.app_obj.operation_error_show_flag
        ) or (
            mini_dict['msg_type'] == 'operation_warning' \
            and not self.app_obj.operation_warning_show_flag
        ):
            # Not visible
            return

        if self.error_list_filter_flag:

            if self.error_list_filter_text == '':
                # Empty search pattern doesn't match anything
                return

            lower_text = self.error_list_filter_text.lower()
            if not (
                (
                    self.error_list_filter_container_flag \
                    and (
                        (
                            not self.error_list_filter_regex_flag \
                            and mini_dict['container_name'].lower().find(
                                lower_text,
                            ) > -1
                        ) or (
                            self.error_list_filter_regex_flag \
                            and re.search(
                                self.error_list_filter_text,
                                mini_dict['container_name'],
                                re.IGNORECASE,
                            )
                        )
                    )
                ) or (
                    self.error_list_filter_video_flag \
                    and (
                        (
                            not self.error_list_filter_regex_flag \
                            and mini_dict['video_name'].lower().find(
                                lower_text,
                            ) > -1
                        ) or (
                            self.error_list_filter_regex_flag \
                            and re.search(
                                self.error_list_filter_text,
                                mini_dict['video_name'],
                                re.IGNORECASE,
                            )
                        )
                    )
                ) or (
                    self.error_list_filter_msg_flag \
                    and (
                        (
                            not self.error_list_filter_regex_flag \
                            and mini_dict['orig_msg'].lower().find(
                                lower_text,
                            ) > -1
                        ) or (
                            self.error_list_filter_regex_flag \
                            and re.search(
                                self.error_list_filter_text,
                                mini_dict['orig_msg'],
                                re.IGNORECASE,
                            )
                        )
                    )
                )
            ):
                return

        # Prepare the icons
        if mini_dict['msg_type'] == 'system_error':
            pixbuf = self.pixbuf_dict['error_small']
            pixbuf2 = self.pixbuf_dict['system_error_small']

        elif mini_dict['msg_type'] == 'system_warning':
            pixbuf = self.pixbuf_dict['warning_small']
            pixbuf2 = self.pixbuf_dict['system_warning_small']

        else:
            if mini_dict['msg_type'] == 'operation_error':
                pixbuf = self.pixbuf_dict['error_small']
            elif mini_dict['msg_type'] == 'operation_warning':
                pixbuf = self.pixbuf_dict['warning_small']
            else:
                # Failsafe
                return

            if mini_dict['media_type'] == 'video':
                pixbuf2 = self.pixbuf_dict['video_small']
            elif mini_dict['media_type'] == 'channel':
                pixbuf2 = self.pixbuf_dict['channel_small']
            elif mini_dict['media_type'] == 'playlist':
                pixbuf2 = self.pixbuf_dict['playlist_small']
            else:
                # Failsafe
                return

        # Prepare the new row in the treeview, starting with the three
        #   hidden columns
        row_list = [
            mini_dict['drag_path'],
            mini_dict['drag_source'],
            mini_dict['drag_name'],
            pixbuf,
            pixbuf2,
            mini_dict['date_time'],
            mini_dict['time'],
            mini_dict['container_name'],
            mini_dict['video_name'],
            mini_dict['msg'],
            mini_dict['short_msg'],
        ]

        # Create a new row in the treeview. Doing the .show_all() first
        #   prevents a Gtk error (for unknown reasons)
        self.errors_list_treeview.show_all()
        self.errors_list_liststore.append(row_list)

        # (Don't update the Errors/Warnings tab label if it's the
        #   visible tab)
        if self.visible_tab_num != self.notebook_tab_dict['errors']:
            self.errors_list_refresh_label()


    def errors_list_refresh_label(self, reset_flag=False):

        """Called by self.errors_list_reset(),
        .errors_list_add_operation_msg(), .errors_list_insert_row() and
        .on_notebook_switch_page().

        The label for the Errors/Warnings tab can show the number of errors/
        warnings currently visible in the tab, or not, depending on conditions.

        Args:

            reset_flag (bool): True when all errors/warnings should be marked
                as old (so, when counting the number of errors/warnings to
                display in the tab label, they are not counted)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 15991 errors_list_refresh_label')

        error_count = 0
        warning_count = 0

        for mini_dict in self.error_list_buffer_list:

            if reset_flag:
                mini_dict['count_flag'] = False

            elif mini_dict['count_flag']:

                if (
                    mini_dict['msg_type'] == 'system_error' \
                    or mini_dict['msg_type'] == 'operation_error'
                ):
                    error_count += 1
                else:
                    warning_count += 1

        text = _('_Errors')
        if error_count:
            text += ' (' + str(error_count) + ')'

        text += ' / ' + _('Warnings')
        if warning_count:
            text += ' (' + str(warning_count) + ')'

        self.errors_label.set_text_with_mnemonic(text)


    def errors_list_apply_filter(self):

        """Called by mainapp.TartubeApp.on_button_apply_error_filter().

        Applies the filter.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16030 errors_list_apply_filter')

        # Set IVs once, so that multiple calls to self.errors_list_insert_row()
        #   can use them
        self.error_list_filter_flag = True
        self.error_list_filter_text = self.error_list_entry.get_text()
        self.error_list_filter_regex_flag \
        = self.error_list_togglebutton.get_active()
        self.error_list_filter_container_flag \
        = self.error_list_container_checkbutton.get_active()
        self.error_list_filter_video_flag \
        = self.error_list_video_checkbutton.get_active()
        self.error_list_filter_msg_flag \
        = self.error_list_msg_checkbutton.get_active()

        # ... and update the Error List
        self.errors_list_reset()

        # Sensitise widgets, as appropriate
        self.error_list_filter_toolbutton.set_sensitive(False)
        self.error_list_cancel_toolbutton.set_sensitive(True)


    def errors_list_cancel_filter(self):

        """Called by mainapp.TartubeApp.on_button_apply_error_filter().

        Applies the filter.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16061 errors_list_cancel_filter')

        # Reset IVs...
        self.error_list_filter_flag = False
        self.error_list_filter_text = None
        self.error_list_filter_regex_flag = False
        self.error_list_filter_container_flag = False
        self.error_list_filter_video_flag = False
        self.error_list_filter_msg_flag = False

        # ... and update the Error List
        self.errors_list_reset()

        # Sensitise widgets, as appropriate
        self.error_list_filter_toolbutton.set_sensitive(True)
        self.error_list_cancel_toolbutton.set_sensitive(False)


    # Callback class methods


    def on_video_index_add_classic(self, menu_item, media_data_obj):

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

        Adds the channel/playlist URL to the textview in the Classic Mode tab.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel or media.Playlist): The clicked media
                data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16098 on_video_index_add_classic')

        if isinstance(media_data_obj, media.Folder) \
        or not media_data_obj.source:
            return self.app_obj.system_error(
                226,
                'Callback request denied due to current conditions',
            )

        utils.add_links_to_textview(
            self.app_obj,
            [ media_data_obj.source ],
            self.classic_textbuffer,
            self.classic_mark_start,
            self.classic_mark_end,
        )


    def on_video_index_add_to_scheduled(self, menu_item, media_data_obj):

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

        Opens a dialogue window giving a list of scheduled downloads, to which
        the specified media data object can be added.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16132 on_video_index_add_to_scheduled')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by a popup menu.' \
            + ' In the Videos tab, right-click a channel and select' \
            + ' Downloads > Add to scheduled download...'
        )

        # Check that at least one scheduled download exists, that doesn't
        #   already contain the specified media data object
        available_list = []
        for scheduled_obj in self.app_obj.scheduled_list:
            if not media_data_obj.dbid in scheduled_obj.media_list:
                available_list.append(scheduled_obj.name)

        if not available_list:
            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _(
                'There are no scheduled downloads that don\'t already' \
                + ' contain the channel/playlist/folder',
                ),
                'error',
                'ok',
                None,                   # Parent window is main window
            )

            return

        available_list.sort()

        # Show the dialogue window
        dialogue_win = ScheduledDialogue(self, media_data_obj, available_list)
        dialogue_win.run()
        choice = dialogue_win.choice
        dialogue_win.destroy()

        # Check for the possibility that the media data object and/or
        #   scheduled download may have been deleted, since the dialogue window
        #   opened
        if choice is not None \
        and media_data_obj.dbid in self.app_obj.container_reg_dict:

            # Find the selected scheduled download
            match_obj = None
            for this_obj in self.app_obj.scheduled_list:
                if this_obj.name == choice:
                    match_obj = this_obj
                    break

            if match_obj:
                match_obj.add_media(media_data_obj.dbid)


    def on_video_index_apply_options(self, menu_item, media_data_obj):

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

        Adds a set of download options (handled by an
        options.OptionsManager object) to the specified media data object.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16202 on_video_index_apply_options')

        if self.app_obj.current_manager_obj \
        or media_data_obj.options_obj\
        or (
            isinstance(media_data_obj, media.Folder)
            and media_data_obj.priv_flag
        ):
            return self.app_obj.system_error(
                227,
                'Callback request denied due to current conditions',
            )

        # If there are any options manager objects besides the General Options
        #   Manager, and the manager attached to the Classic Mode tab, then we
        #   need to prompt the user to select one
        prompt_flag = False
        for options_obj in self.app_obj.options_reg_dict.values():

            if options_obj != self.app_obj.general_options_obj \
            and (
                self.app_obj.classic_options_obj == None \
                or options_obj != self.app_obj.classic_options_obj
            ):
                prompt_flag = True
                break

        if not prompt_flag:

            # Apply (new) download options to the media data object
            self.app_obj.apply_download_options(media_data_obj)

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

        else:

            # Prompt the user to specify new or existing download options
            dialogue_win = ApplyOptionsDialogue(self)
            response = dialogue_win.run()
            # Get the specified options.OptionsManager object, before
            #   destroying the window
            options_obj = dialogue_win.options_obj
            clone_flag = dialogue_win.clone_flag
            dialogue_win.destroy()

            if response == Gtk.ResponseType.OK:

                if clone_flag:

                    options_obj = self.app_obj.clone_download_options(
                        options_obj,
                    )

                # Apply the specified (or new) download options to the media
                #   data object
                self.app_obj.apply_download_options(
                    media_data_obj,
                    options_obj,
                )

                # Open an edit window to show (new) options immediately
                if not options_obj or clone_flag:
                    config.OptionsEditWin(
                        self.app_obj,
                        media_data_obj.options_obj,
                    )


    def on_video_index_check(self, menu_item, media_data_obj):

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

        Check the right-clicked media data object.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16290 on_video_index_check')

        download_manager_obj = self.app_obj.download_manager_obj

        if (
            self.app_obj.current_manager_obj \
            and not self.app_obj.download_manager_obj
        ) or (
            self.app_obj.download_manager_obj \
            and self.app_obj.download_manager_obj.operation_classic_flag
        ):
            return self.app_obj.system_error(
                228,
                'Callback request denied due to current conditions',
            )

        if not download_manager_obj:

            # Start a new download operation to download this channel/playlist/
            #   folder
            self.app_obj.download_manager_start(
                'sim',
                False,
                [media_data_obj],
            )

            return

        # Download operation already in progress. Check that this channel/
        #   playlist/folder is not already in the download list
        for this_obj \
        in download_manager_obj.download_list_obj.download_item_dict.values():

            if this_obj.media_data_obj == media_data_obj:
                return

        # Add the channel/playlist/folder to the download list
        return_list = download_manager_obj.download_list_obj.create_item(
            media_data_obj,
            None,               # media.Scheduled object
            'sim',              # override_operation_type
            False,              # priority_flag
            False,              # ignore_limits_flag
        )

        for download_item_obj in return_list:

            # Add a row to the Progress List
            self.progress_list_add_row(
                download_item_obj.item_id,
                download_item_obj.media_data_obj,
            )

            # Update the main window's progress bar
            self.app_obj.download_manager_obj.nudge_progress_bar()


    def on_video_index_convert_container(self, menu_item, media_data_obj):

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

        Converts a channel to a playlist, or a playlist to a channel.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16363 on_video_index_convert_container')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                229,
                'Callback request denied due to current conditions',
            )

        self.app_obj.convert_remote_container(media_data_obj)


    def on_video_index_custom_dl(self, menu_item, media_data_obj):

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

        Custom download the right-clicked media data object.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16390 on_video_index_custom_dl')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                230,
                'Callback request denied due to current conditions',
            )

        # Start a custom download operation
        if not self.app_obj.general_custom_dl_obj.dl_by_video_flag \
        or not self.app_obj.general_custom_dl_obj.dl_precede_flag:

            self.app_obj.download_manager_start(
                'custom_real',
                False,
                [media_data_obj],
                self.app_obj.general_custom_dl_obj,
            )

        else:

            self.app_obj.download_manager_start(
                'custom_sim',
                False,
                [media_data_obj],
                self.app_obj.general_custom_dl_obj,
            )


    def on_video_index_delete_container(self, menu_item, media_data_obj):

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

        Deletes the channel, playlist or folder.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Folder):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16435 on_video_index_delete_container')

        self.app_obj.delete_container(media_data_obj)


    def on_video_index_dl_no_db(self, menu_item, media_data_obj):

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

        Set the media data object's flag to disable adding videos to Tartube's
        database.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16457 on_video_index_dl_no_db')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                231,
                'Callback request denied due to current conditions',
            )

        if not media_data_obj.dl_no_db_flag:
            media_data_obj.set_dl_no_db_flag(True)
        else:
            media_data_obj.set_dl_no_db_flag(False)

        GObject.timeout_add(
            0,
            self.video_index_update_row_icon,
            media_data_obj,
        )
        GObject.timeout_add(
            0,
            self.video_index_update_row_text,
            media_data_obj,
        )


    def on_video_index_dl_disable(self, menu_item, media_data_obj):

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

        Set the media data object's flag to disable checking and downloading.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16498 on_video_index_dl_disable')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                232,
                'Callback request denied due to current conditions',
            )

        if not media_data_obj.dl_disable_flag:
            media_data_obj.set_dl_disable_flag(True)
        else:
            media_data_obj.set_dl_disable_flag(False)

        GObject.timeout_add(
            0,
            self.video_index_update_row_icon,
            media_data_obj,
        )
        GObject.timeout_add(
            0,
            self.video_index_update_row_text,
            media_data_obj,
        )


    def on_video_index_dl_sim(self, menu_item, media_data_obj):

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

        Set the media data object's flag to force checking of the channel/
        playlist/folder (disabling actual downloads).

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16540 on_video_index_dl_sim')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                233,
                'Callback request denied due to current conditions',
            )

        if not media_data_obj.dl_sim_flag:
            media_data_obj.set_dl_sim_flag(True)
        else:
            media_data_obj.set_dl_sim_flag(False)

        GObject.timeout_add(
            0,
            self.video_index_update_row_icon,
            media_data_obj,
        )
        GObject.timeout_add(
            0,
            self.video_index_update_row_text,
            media_data_obj,
        )


    def on_video_index_download(self, menu_item, media_data_obj):

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

        Download the right-clicked media data object.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16581 on_video_index_download')

        download_manager_obj = self.app_obj.download_manager_obj

        if __main__.__pkg_no_download_flag__ \
        or (
            self.app_obj.current_manager_obj \
            and not self.app_obj.download_manager_obj
        ) or (
            self.app_obj.download_manager_obj \
            and self.app_obj.download_manager_obj.operation_classic_flag
        ):
            return self.app_obj.system_error(
                234,
                'Callback request denied due to current conditions',
            )

        if not download_manager_obj:

            # Start a new download operation to download this channel/playlist/
            #   folder
            self.app_obj.download_manager_start(
                'real',
                False,
                [media_data_obj],
            )

            return

        # Download operation already in progress. Check that this channel/
        #   playlist/folder is not already in the download list
        for this_obj \
        in download_manager_obj.download_list_obj.download_item_dict.values():

            if this_obj.media_data_obj == media_data_obj:
                return

        # Add the channel/playlist/folder to the download list
        return_list = download_manager_obj.download_list_obj.create_item(
            media_data_obj,
            None,           # media.Scheduled object
            'real',         # override_operation_type
            False,          # priority_flag
            False,          # ignore_limits_flag
        )

        for download_item_obj in return_list:

            # Add a row to the Progress List
            self.progress_list_add_row(
                download_item_obj.item_id,
                download_item_obj.media_data_obj,
            )

            # Update the main window's progress bar
            self.app_obj.download_manager_obj.nudge_progress_bar()


    def on_video_index_drag_data_received(self, treeview, drag_context, x, y, \
    selection_data, info, timestamp):

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

        Retrieve the source and destination media data objects, and pass them
        on to a function in the main application.

        Args:

            treeview (Gtk.TreeView): The Video Index'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

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16664 on_video_index_drag_data_received')

        # Must override the usual Gtk handler
        treeview.stop_emission('drag_data_received')
        # Import the treeview's sorted model (for convenience)
        model = self.video_index_sortmodel

        # Extract the drop destination
        drop_info = treeview.get_dest_row_at_pos(x, y)
        if drop_info is None:
            return

        # Get the destination media data object
        drop_path, drop_posn = drop_info[0], drop_info[1]
        drop_iter = model.get_iter(drop_path)
        dest_id = model[drop_iter][0]
        if dest_id is None or dest_id == '':
            return

        if self.video_catalogue_drag_list:

            # media.Video(s) are being dragged from the Video Catalogue into
            #   the Video Index
            # To avoid any unforeseen problems, retrieve the list and reset the
            #   IV immediately
            video_list = self.video_catalogue_drag_list
            self.video_catalogue_drag_list = []

            # Move the video(s)
            self.app_obj.move_videos(
                self.app_obj.media_reg_dict[dest_id],
                video_list,
            )

            # (Git #561) Dragging items from the Video Index onto itself causes
            #   problems; this function is called correctly, immediately
            #   followed by a useless call to
            #   self.on_window_drag_data_received()
            # An intermediate call to Gtk.drag_cancel(context) does not work,
            #   so we're forced to use this ugly hack, hoping that both
            #   functions receive the same Gtk.DragContext
            self.ignore_drag_context = drag_context

        else:

            # A media.Channel, media.Playlist or media.Folder is being dragged
            #    into another container within the Video Index
            # Get the dragged media data object
            old_selection = self.video_index_treeview.get_selection()
            (model, start_iter) = old_selection.get_selected()
            drag_id = model[start_iter][0]
            if drag_id is None or drag_id == '':
                return

            # On MS Windows, the system helpfully deletes the dragged row
            #   before we've had a chance to show the confirmation dialogue
            # Could redraw the dragged row, but then MS Windows helpfully
            #   selects the row beneath it, again before we've had a chance to
            #   intervene
            # Only way around it is to completely reset the Video Index
            #   (and Video Catalogue)
            if os.name == 'nt':
                self.video_index_catalogue_reset(True)

            # Now proceed with the drag
            self.app_obj.move_container(
                self.app_obj.media_reg_dict[drag_id],
                self.app_obj.media_reg_dict[dest_id],
            )


    def on_video_index_drag_drop(self, treeview, drag_context, x, y, time):

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

        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): The Video Index's treeview

            drag_context (GdkX11.X11DragContext): Data from the drag procedure

            x, y (int): Cell coordinates in the treeview

            time (int): A timestamp

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16756 on_video_index_drag_drop')

        # 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_edit_options(self, menu_item, media_data_obj):

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

        Edit the download options (handled by an
        options.OptionsManager object) for the specified media data object.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16784 on_video_index_edit_options')

        if self.app_obj.current_manager_obj or not media_data_obj.options_obj:
            return self.app_obj.system_error(
                235,
                'Callback request denied due to current conditions',
            )

        # Open an edit window
        config.OptionsEditWin(
            self.app_obj,
            media_data_obj.options_obj,
        )


    def on_video_index_empty_folder(self, menu_item, media_data_obj):

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

        Empties the folder.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Folder): The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16814 on_video_index_empty_folder')

        # The True flag tells the function to empty the container, rather than
        #   delete it
        self.app_obj.delete_container(media_data_obj, True)


    def on_video_index_export(self, menu_item, media_data_obj):

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

        Exports a summary of the database, containing the selected channel/
        playlist/folder and its descendants.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16838 on_video_index_export')

        self.app_obj.export_from_db( [media_data_obj] )


    def on_video_index_hide_folder(self, menu_item, media_data_obj):

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

        Hides the folder in the Video Index.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16859 on_video_index_hide_folder')

        self.app_obj.mark_folder_hidden(media_data_obj, True)


    def on_video_index_insert_videos(self, menu_item, media_data_obj):

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

        Creates a dialogue window to insert one or more videos into a channel.

        This is useful when the new videos are unlisted. Videos can be added to
        a folder in the usual way.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16883 on_video_index_insert_videos')

        # (Code adapated from mainapp.TartubeApp.on_menu_add_video() )

        dialogue_win = InsertVideoDialogue(self, media_data_obj)
        response = dialogue_win.run()

        # Retrieve user choices from the dialogue window...
        text = dialogue_win.textbuffer.get_text(
            dialogue_win.textbuffer.get_start_iter(),
            dialogue_win.textbuffer.get_end_iter(),
            False,
        )

        # ...and halt the timer, if running
        if dialogue_win.clipboard_timer_id:
            GObject.source_remove(dialogue_win.clipboard_timer_id)

        # ...before destroying the dialogue window
        dialogue_win.destroy()

        if response == Gtk.ResponseType.OK:

            # Split text into a list of lines and filter out invalid URLs
            video_list = []
            duplicate_list = []
            for line in text.splitlines():

                for item in line.split():

                    # Remove leading/trailing whitespace
                    item = utils.strip_whitespace(item)

                    # Perform checks on the URL. If it passes, remove leading/
                    #   trailing whitespace
                    if utils.check_url(item):
                        video_list.append(utils.strip_whitespace(item))

            # Check everything in the list against other media.Video objects
            #   with the same parent folder
            for item in video_list:
                if media_data_obj.check_duplicate_video(item):
                    duplicate_list.append(item)
                else:
                    self.app_obj.add_video(media_data_obj, item)

            # In the Video Index, select the parent media data object, which
            #   updates both the Video Index and the Video Catalogue
            self.video_index_select_row(media_data_obj)

            # If any duplicates were found, inform the user
            if duplicate_list:
                dialogue_win = mainwin.DuplicateVideoDialogue(
                    self,
                    duplicate_list,
                )
                dialogue_win.run()
                dialogue_win.destroy()


    def on_video_index_mark_archived(self, menu_item, media_data_obj,
    only_child_videos_flag):

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

        Mark all of the children of this channel, playlist or folder (and all
        of their children, and so on) as archived.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

            only_child_videos_flag (bool): Set to True if only child video
                objects should be marked; False if all descendants should be
                marked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16965 on_video_index_mark_archived')

        self.app_obj.mark_container_archived(
            media_data_obj,
            True,
            only_child_videos_flag,
        )


    def on_video_index_mark_not_archived(self, menu_item, media_data_obj,
    only_child_videos_flag):

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

        Mark all videos in this folder (and in any child channels, playlists
        and folders) as not archived.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

            only_child_videos_flag (bool): Set to True if only child video
                objects should be marked; False if all descendants should be
                marked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16996 on_video_index_mark_not_archived')

        self.app_obj.mark_container_archived(
            media_data_obj,
            False,
            only_child_videos_flag,
        )


    def on_video_index_mark_bookmark(self, menu_item, media_data_obj):

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

        Mark all of the children of this channel, playlist or folder (and all
        of their children, and so on) as bookmarked.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17022 on_video_index_mark_bookmark')

        # In earlier versions of Tartube, this action could take a very long
        #   time (perhaps hours)
        count = len(media_data_obj.child_list)
        if count < self.mark_video_lower_limit:

            # The procedure should be quick
            for child_obj in media_data_obj.child_list:
                if isinstance(child_obj, media.Video):
                    self.app_obj.mark_video_bookmark(child_obj, True)

        elif count < self.mark_video_higher_limit:

            # This will take a few seconds, so don't prompt the user
            self.app_obj.prepare_mark_video(
                ['bookmark', True, media_data_obj],
            )

        else:

            # This might take a few tens of seconds, so prompt the user for
            #   confirmation first
            self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue(
                self.get_take_a_while_msg(media_data_obj, count),
                'question',
                'yes-no',
                None,                   # Parent window is main window
                {
                    'yes': 'prepare_mark_video',
                    # Specified options
                    'data': ['bookmark', True, media_data_obj],
                },
            )


    def on_video_index_mark_not_bookmark(self, menu_item, media_data_obj):

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

        Mark all videos in this folder (and in any child channels, playlists
        and folders) as not bookmarked.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17075 on_video_index_mark_not_bookmark')

        # In earlier versions of Tartube, this action could take a very long
        #   time (perhaps hours)
        count = len(media_data_obj.child_list)
        if count < self.mark_video_lower_limit:

            # The procedure should be quick
            for child_obj in media_data_obj.child_list:
                if isinstance(child_obj, media.Video):
                    self.app_obj.mark_video_bookmark(child_obj, False)

        elif count < self.mark_video_higher_limit:

            # This will take a few seconds, so don't prompt the user
            self.app_obj.prepare_mark_video(
                ['bookmark', False, media_data_obj],
            )

        else:

            # This might take a few tens of seconds, so prompt the user for
            #   confirmation first
            self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue(
                self.get_take_a_while_msg(media_data_obj, count),
                'question',
                'yes-no',
                None,                   # Parent window is main window
                {
                    'yes': 'prepare_mark_video',
                    # Specified options
                    'data': ['bookmark', False, media_data_obj],
                },
            )


    def on_video_index_mark_favourite(self, menu_item, media_data_obj,
    only_child_videos_flag):

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

        Mark all of the children of this channel, playlist or folder (and all
        of their children, and so on) as favourite.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

            only_child_videos_flag (bool): Set to True if only child video
                objects should be marked; False if all descendants should be
                marked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17133 on_video_index_mark_favourite')

        self.app_obj.mark_container_favourite(
            media_data_obj,
            True,
            only_child_videos_flag,
        )


    def on_video_index_mark_not_favourite(self, menu_item, media_data_obj,
    only_child_videos_flag):

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

        Mark all videos in this folder (and in any child channels, playlists
        and folders) as not favourite.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

            only_child_videos_flag (bool): Set to True if only child video
                objects should be marked; False if all descendants should be
                marked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17164 on_video_index_mark_not_favourite')

        self.app_obj.mark_container_favourite(
            media_data_obj,
            False,
            only_child_videos_flag,
        )


    def on_video_index_mark_missing(self, menu_item, media_data_obj):

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

        Mark all of the children of this channel or playlist as missing.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17189 on_video_index_mark_missing')

        if isinstance(media_data_obj, media.Video) \
        or isinstance(media_data_obj, media.Folder):
            return self.app_obj.system_error(
                236,
                'Callback request denied due to current conditions',
            )

        self.app_obj.mark_container_missing(media_data_obj, True)


    def on_video_index_mark_not_missing(self, menu_item, media_data_obj):

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

        Mark all of the children of this channel or playlist as not missing.
        This function can't be called for folders (except for the fixed
        'Missing Videos' folder).

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17219 on_video_index_mark_not_missing')

        if isinstance(media_data_obj, media.Video) \
        or (
            isinstance(media_data_obj, media.Folder) \
            and media_data_obj != self.app_obj.fixed_missing_folder
        ):
            return self.app_obj.system_error(
                237,
                'Callback request denied due to current conditions',
            )

        self.app_obj.mark_container_missing(media_data_obj, False)


    def on_video_index_mark_new(self, menu_item, media_data_obj,
    only_child_videos_flag):

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

        Mark all videos in this channel, playlist or folder (and in any child
        channels, playlists and folders) as new (but only if they have been
        downloaded).

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

            only_child_videos_flag (bool): Set to True if only child video
                objects should be marked; False if all descendants should be
                marked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17257 on_video_index_mark_new')

        self.app_obj.mark_container_new(
            media_data_obj,
            True,
            only_child_videos_flag,
        )


    def on_video_index_mark_not_new(self, menu_item, media_data_obj,
    only_child_videos_flag):

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

        Mark all videos in this channel, playlist or folder (and in any child
        channels, playlists and folders) as not new.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

            only_child_videos_flag (bool): Set to True if only child video
                objects should be marked; False if all descendants should be
                marked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17288 on_video_index_mark_not_new')

        self.app_obj.mark_container_new(
            media_data_obj,
            False,
            only_child_videos_flag,
        )


    def on_video_index_mark_waiting(self, menu_item, media_data_obj):

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

        Mark all of the children of this channel, playlist or folder (and all
        of their children, and so on) as in the waiting list.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17314 on_video_index_mark_waiting')

        # In earlier versions of Tartube, this action could take a very long
        #   time (perhaps hours)
        count = len(media_data_obj.child_list)
        if count < self.mark_video_lower_limit:

            # The procedure should be quick
            for child_obj in media_data_obj.child_list:
                if isinstance(child_obj, media.Video):
                    self.app_obj.mark_video_waiting(child_obj, True)

        elif count < self.mark_video_higher_limit:

            # This will take a few seconds, so don't prompt the user
            self.app_obj.prepare_mark_video(
                ['waiting', True, media_data_obj],
            )

        else:

            # This might take a few tens of seconds, so prompt the user for
            #   confirmation first
            self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue(
                self.get_take_a_while_msg(media_data_obj, count),
                'question',
                'yes-no',
                None,                   # Parent window is main window
                {
                    'yes': 'prepare_mark_video',
                    # Specified options
                    'data': ['waiting', True, media_data_obj],
                },
            )


    def on_video_index_mark_not_waiting(self, menu_item, media_data_obj):

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

        Mark all videos in this folder (and in any child channels, playlists
        and folders) as not in the waiting list.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17367 on_video_index_mark_not_waiting')

        # In earlier versions of Tartube, this action could take a very long
        #   time (perhaps hours)
        count = len(media_data_obj.child_list)
        if count < self.mark_video_lower_limit:

            # The procedure should be quick
            for child_obj in media_data_obj.child_list:
                if isinstance(child_obj, media.Video):
                    self.app_obj.mark_video_waiting(child_obj, False)

        elif count < self.mark_video_higher_limit:

            # This will take a few seconds, so don't prompt the user
            self.app_obj.prepare_mark_video(
                ['waiting', False, media_data_obj],
            )

        else:

            # This might take a few tens of seconds, so prompt the user for
            #   confirmation first
            self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue(
                self.get_take_a_while_msg(media_data_obj, count),
                'question',
                'yes-no',
                None,                   # Parent window is main window
                {
                    'yes': 'prepare_mark_video',
                    # Specified options
                    'data': ['waiting', False, media_data_obj],
                },
            )


    def on_video_index_move_to_top(self, menu_item, media_data_obj):

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

        Moves a channel, playlist or folder to the top level (in other words,
        removes its parent folder).

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17420 on_video_index_move_to_top')

        self.app_obj.move_container_to_top(media_data_obj)


    def on_video_index_recent_videos_time(self, menu_item, media_data_obj):

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

        Opens a dialogue window so the user can set the time after which
        videos are removed from the 'Recent videos' folder.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17441 on_video_index_recent_videos_time')

        # Show the dialogue window
        dialogue_win = RecentVideosDialogue(self, media_data_obj)
        dialogue_win.run()

        if dialogue_win.radiobutton.get_active():
            choice = 0
        else:
            choice = dialogue_win.spinbutton.get_value()

        dialogue_win.destroy()

        self.app_obj.set_fixed_recent_folder_days(int(choice))


    def on_video_index_refresh(self, menu_item, media_data_obj):

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

        Refresh the right-clicked media data object, checking the corresponding
        directory on the user's filesystem against video objects in the
        database.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17475 on_video_index_refresh')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                238,
                'Callback request denied due to current conditions',
            )

        # Start a refresh operation
        self.app_obj.refresh_manager_start(media_data_obj)


    def on_video_index_remove_options(self, menu_item, media_data_obj):

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

        Removes a set of download options (handled by an
        options.OptionsManager object) from the specified media data object.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17504 on_video_index_remove_options')

        if self.app_obj.current_manager_obj \
        or not media_data_obj.options_obj:
            return self.app_obj.system_error(
                239,
                'Callback request denied due to current conditions',
            )

        # Remove download options from the media data object
        self.app_obj.remove_download_options(media_data_obj)


    def on_video_index_remove_videos(self, menu_item, media_data_obj):

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

        Empties all child videos of a folder object, but doesn't remove any
        child channel, playlist or folder objects.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Folder): The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17533 on_video_index_remove_videos')

        for child_obj in media_data_obj.child_list:
            if isinstance(child_obj, media.Video):
                self.app_obj.delete_video(child_obj)


    def on_video_index_rename_location(self, menu_item, media_data_obj):

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

        Renames a channel, playlist or folder. Also renames the corresponding
        directory in Tartube's data directory.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17557 on_video_index_rename_location')

        self.app_obj.rename_container(media_data_obj)


    def on_video_index_right_click(self, treeview, event):

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

        When the user right-clicks an item in the Video Index, create a
        context-sensitive popup menu.

        Args:

            treeview (Gtk.TreeView): The Video Index's treeview

            event (Gdk.EventButton): The event emitting the Gtk signal

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17578 on_video_index_right_click')

        if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3:

            # If the user right-clicked on empty space, the call to
            #   .get_path_at_pos returns None (or an empty list)
            if not treeview.get_path_at_pos(
                int(event.x),
                int(event.y),
            ):
                return

            path, column, cellx, celly = treeview.get_path_at_pos(
                int(event.x),
                int(event.y),
            )

            tree_iter = self.video_index_sortmodel.get_iter(path)
            if tree_iter is not None:

                # Pass the container's .dbid to the popup menu code
                self.video_index_popup_menu(
                    event,
                    self.video_index_sortmodel[tree_iter][0],
                )


    def on_video_index_selection_changed(self, selection):

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

        Also called from callbacks in mainapp.TartubeApp.on_menu_test,
        .on_button_switch_view() and .on_menu_add_video().

        When the user clicks to select an item in the Video Index, call a
        function to update the Video Catalogue.

        Args:

            selection (Gtk.TreeSelection): Data for the selected row

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17622 on_video_index_selection_changed')

        (model, tree_iter) = selection.get_selected()
        if tree_iter is not None:
            if not model.iter_is_valid(tree_iter):
                tree_iter = None
            else:
                dbid = model[tree_iter][0]

        # Don't update the Video Catalogue during certain procedures, such as
        #   removing a row from the Video Index (in which case, the flag will
        #   be set)
        if not self.ignore_video_index_select_flag:

            if tree_iter is None:
                self.video_index_current_dbid = None
                self.video_catalogue_reset()

            else:
                # Update IVs
                self.video_index_current_dbid = dbid
                media_data_obj = self.app_obj.media_reg_dict[dbid]

                # Expand the tree beneath the selected line, if allowed
                if self.app_obj.auto_expand_video_index_flag:
                    if not self.video_index_treeview.row_expanded(
                        model.get_path(tree_iter),
                    ):
                        self.video_index_treeview.expand_row(
                            model.get_path(tree_iter),
                            self.app_obj.full_expand_video_index_flag,
                        )

                    else:
                        self.video_index_treeview.collapse_row(
                            model.get_path(tree_iter),
                        )

                # Redraw the Video Catalogue, on the first page, and reset its
                #   scrollbars back to the top
                self.video_catalogue_redraw_all(
                    dbid,
                    1,              # Display the first page
                    True,           # Reset scrollbars
                )


    def on_video_index_marker(self, menu_item, media_data_obj):

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

        Toggle the Video Index marker for the specified media data object.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17685 on_video_index_marker')

        # (Using self.video_index_treestore)
        tree_ref = self.video_index_row_dict[media_data_obj.dbid]
        tree_path = tree_ref.get_path()
        tree_iter = self.video_index_treestore.get_iter(tree_path)

        self.video_index_treestore[tree_path][4] \
        = not self.video_index_treestore[tree_path][4]

        # The media data object's .dbid is in column 0
        tree_iter = self.video_index_treestore.get_iter(tree_path)
        dbid = self.video_index_treestore[tree_iter][0]
        media_data_obj = self.app_obj.media_reg_dict[dbid]

        if not self.video_index_treestore[tree_path][4]:

            if media_data_obj.dbid in self.video_index_marker_dict:
                del self.video_index_marker_dict[media_data_obj.dbid]

        else:

            self.video_index_marker_dict[media_data_obj.dbid] \
            = self.video_index_row_dict[media_data_obj.dbid]


    def on_video_index_marker_toggled(self, renderer_toggle, sorted_path):

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

        When the user toggles the marker checkbutton on a row, update the
        treeview's model.

        Args:

            renderer_toggle (Gtk.CellRendererToggle): The widget clicked

            sorted_path (Gtk.TreePath): Path to the clicked row (in
                self.video_index_sortmodel)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17728 on_video_index_marker_toggled')

        # (Using self.video_index_sortmodel)
        sorted_iter = self.video_index_sortmodel.get_iter(sorted_path)
        dbid = self.video_index_sortmodel[sorted_iter][0]
        media_data_obj = self.app_obj.media_reg_dict[dbid]

        # System folders cannot be marked
        # Channels/playlists/folders for which checking and downloading is
        #   disabled can't be marked
        if (
            isinstance(media_data_obj, media.Folder) \
            and media_data_obj.priv_flag
        ) or media_data_obj.dl_disable_flag:
            return

        # (Using self.video_index_treestore)
        tree_ref = self.video_index_row_dict[media_data_obj.dbid]
        tree_path = tree_ref.get_path()
        tree_iter = self.video_index_treestore.get_iter(tree_path)

        self.video_index_treestore[tree_path][4] \
        = not self.video_index_treestore[tree_path][4]

        # The media data object's .dbid is in column 0
        tree_iter = self.video_index_treestore.get_iter(tree_path)
        dbid = self.video_index_treestore[tree_iter][0]
        media_data_obj = self.app_obj.media_reg_dict[dbid]

        # Update IVs
        old_size = len(self.video_index_marker_dict)
        if not self.video_index_treestore[tree_path][4]:

            if media_data_obj.dbid in self.video_index_marker_dict:
                del self.video_index_marker_dict[media_data_obj.dbid]

        else:

            self.video_index_marker_dict[media_data_obj.dbid] \
            = self.video_index_row_dict[media_data_obj.dbid]

        if (old_size and not self.video_index_marker_dict) \
        or (not old_size and self.video_index_marker_dict):
            # Update labels on the 'Check all' button, etc
            # The True argument skips the check for the existence of a progress
            #   bar
            self.hide_progress_bar(True)


    def on_video_index_set_destination(self, menu_item, media_data_obj):

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

        Sets (or resets) the alternative download destination, or the external
        directory, for the selected channel, playlist or folder.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Folder):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17794 on_video_index_set_destination')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by a popup menu.' \
            + ' In the Videos tab, right-click a channel and select' \
            + ' Downloads > Set download destination...'
        )

        if isinstance(media_data_obj, media.Video):
            return self.app_obj.system_error(
                240,
                'Cannot set the download destination of a video',
            )

        dialogue_win = SetDestinationDialogue(self, media_data_obj)
        response = dialogue_win.run()

        # Retrieve user choices from the dialogue window, before destroying it
        choice = dialogue_win.choice
        dialogue_win.destroy()

        if response == Gtk.ResponseType.OK:

            if type(choice) == int:

                # 'choice' is a .dbid
                if choice != media_data_obj.master_dbid:
                    media_data_obj.set_master_dbid(self.app_obj, choice)

                media_data_obj.set_external_dir(self.app_obj, None)
                if media_data_obj.dbid \
                in self.app_obj.container_unavailable_dict:
                    self.app_obj.del_container_unavailable_dict(
                        media_data_obj.dbid,
                    )

            else:

                # 'choice' is the full path to an external directory. If it
                #   doesn't exist, create it (and add the semaphore file)
                if media_data_obj.set_external_dir(self.app_obj, choice):

                    media_data_obj.set_master_dbid(
                        self.app_obj,
                        media_data_obj.dbid,
                    )

                    if media_data_obj.dbid \
                    in self.app_obj.container_unavailable_dict:
                        self.app_obj.del_container_unavailable_dict(
                            media_data_obj.dbid,
                        )

                else:

                    if os.name == 'nt':
                        msg = _('The external folder is not available')
                    else:
                        msg = _('The external directory is not available')

                    self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                        msg,
                        'error',
                        'ok',
                        None,                   # Parent window is main window
                    )

            # Update tooltips for this row
            GObject.timeout_add(
                0,
                self.video_index_update_row_tooltip,
                media_data_obj,
            )


    def on_video_index_set_nickname(self, menu_item, media_data_obj):

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

        Sets (or resets) the nickname for the selected channel, playlist or
        folder.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Folder):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17886 on_video_index_set_nickname')

        if isinstance(media_data_obj, media.Video):
            return self.app_obj.system_error(
                241,
                'Cannot set the nickname of a video',
            )

        dialogue_win = SetNicknameDialogue(self, media_data_obj)
        response = dialogue_win.run()

        # Retrieve user choices from the dialogue window, before destroying it
        nickname = dialogue_win.entry.get_text()
        dialogue_win.destroy()

        if response == Gtk.ResponseType.OK:

            # If nickname is an empty string, then the call to .set_nickname()
            #   resets the .nickname IV to match the .name IV
            media_data_obj.set_nickname(nickname)

            # Update the name displayed in the Video Index
            GObject.timeout_add(
                0,
                self.video_index_update_row_text,
                media_data_obj,
            )


    def on_video_index_set_url(self, menu_item, media_data_obj):

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

        Sets (or resets) the URL for the selected channel or playlist.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist): The clicked media
                data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17931 on_video_index_set_url')

        if isinstance(media_data_obj, media.Video):
            return self.app_obj.system_error(
                242,
                'Cannot modify the URL of a video',
            )

        elif isinstance(media_data_obj, media.Folder):
            return self.app_obj.system_error(
                243,
                'Cannot set the URL of a folder',
            )

        dialogue_win = SetURLDialogue(self, media_data_obj)
        response = dialogue_win.run()

        # Retrieve user choices from the dialogue window, before destroying it
        url = dialogue_win.entry.get_text()
        dialogue_win.destroy()

        if response == Gtk.ResponseType.OK:

            # Check the URL is valid, before updating the media.Video object
            if url is None or not utils.check_url(url):

                self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                    _('The URL is not valid'),
                    'error',
                    'ok',
                    None,                   # Parent window is main window
                )

            else:

                media_data_obj.set_source(url)


    def on_video_index_show_destination(self, menu_item, media_data_obj):

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

        Opens the sub-directory into which all files for the specified media
        data object are downloaded (which might be the default sub-directory
        for another media data object, if the media data object's .master_dbid
        has been modified).

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Folder):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 17988 on_video_index_show_destination')

        if media_data_obj.external_dir is not None:
            other_obj = media_data_obj
        else:
            other_obj = self.app_obj.media_reg_dict[media_data_obj.master_dbid]

        path = other_obj.get_actual_dir(self.app_obj)
        utils.open_file(self.app_obj, path)


    def on_video_index_show_location(self, menu_item, media_data_obj):

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

        Opens the sub-directory into which all files for the specified media
        data object are downloaded, by default (which might not be the actual
        sub-directory, if the media data object's .master_dbid has been
        modified).

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 18018 on_video_index_show_location')

        path = media_data_obj.get_default_dir(self.app_obj)
        utils.open_file(self.app_obj, path)


    def on_video_index_show_properties(self, menu_item, media_data_obj):

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

        Opens an edit window for the media data object.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 18040 on_video_index_show_properties')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                244,
                'Callback request denied due to current conditions',
            )

        # Open the edit window immediately
        if isinstance(media_data_obj, media.Folder):
            config.FolderEditWin(self.app_obj, media_data_obj)
        else:
            config.ChannelPlaylistEditWin(self.app_obj, media_data_obj)


    def on_video_index_show_system_cmd(self, menu_item, media_data_obj):

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

        Opens a dialogue window to show the system command that would be used
        to download the clicked channel/playlist/folder.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 18071 on_video_index_show_system_cmd')

        # Show the dialogue window
        dialogue_win = SystemCmdDialogue(self, media_data_obj)
        dialogue_win.run()
        dialogue_win.destroy()


    def on_video_index_tidy(self, menu_item, media_data_obj):

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

        Perform a tidy operation on the right-clicked media data object.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Channel, media.Playlist or media.Folder):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 18095 on_video_index_tidy')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                245,
                'Callback request denied due to current conditions',
            )

        # Prompt the user to specify which actions should be applied to
        #   the media data object's directory
        dialogue_win = TidyDialogue(self, media_data_obj)
        response = dialogue_win.run()

        if response == Gtk.ResponseType.OK:

            # Retrieve user choices from the dialogue window
            choices_dict = {
                'media_data_obj': media_data_obj,
                'corrupt_flag': dialogue_win.checkbutton.get_active(),
                'del_corrupt_flag': dialogue_win.checkbutton2.get_active(),
                'exist_flag': dialogue_win.checkbutton3.get_active(),
                'del_video_flag': dialogue_win.checkbutton4.get_active(),
                'del_others_flag': dialogue_win.checkbutton5.get_active(),
                'remove_no_url_flag': dialogue_win.checkbutton6.get_active(),
                'remove_duplicate_flag': \
                dialogue_win.checkbutton7.get_active(),
                'del_archive_flag': dialogue_win.checkbutton8.get_active(),
                'move_thumb_flag': dialogue_win.checkbutton9.get_active(),
                'del_thumb_flag': dialogue_win.checkbutton10.get_active(),
                'del_webp_flag': dialogue_win.checkbutton11.get_active(),
                'convert_webp_flag': dialogue_win.checkbutton12.get_active(),
                'move_data_flag': dialogue_win.checkbutton13.get_active(),
                'del_descrip_flag': dialogue_win.checkbutton14.get_active(),
                'del_json_flag': dialogue_win.checkbutton15.get_active(),
                'del_xml_flag': dialogue_win.checkbutton16.get_active(),
                'convert_ext_flag': dialogue_win.checkbutton17.get_active(),
            }

        # Now destroy the window
        dialogue_win.destroy()

        if response == Gtk.ResponseType.OK:

            # If nothing was selected, then there is nothing to do
            selected_flag = False
            for key in choices_dict.keys():
                if choices_dict[key]:
                    selected_flag = True
                    break

            if not selected_flag:
                return

            # Prompt the user for confirmation, before deleting any files
            if choices_dict['del_corrupt_flag'] \
            or choices_dict['del_video_flag'] \
            or choices_dict['del_archive_flag'] \
            or choices_dict['del_thumb_flag'] \
            or choices_dict['del_webp_flag'] \
            or choices_dict['del_descrip_flag'] \
            or choices_dict['del_json_flag'] \
            or choices_dict['del_xml_flag']:

                self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                    _(
                    'Files cannot be recovered, after being deleted. Are you' \
                    + ' sure you want to continue?',
                    ),
                    'question',
                    'yes-no',
                    None,                   # Parent window is main window
                    {
                        'yes': 'tidy_manager_start',
                        # Specified options
                        'data': choices_dict,
                    },
                )

            else:

                # Start the tidy operation now
                self.app_obj.tidy_manager_start(choices_dict)


    def on_video_catalogue_add_classic(self, menu_item, media_data_obj):

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

        Adds the selected video's URL to the textview in the Classic Mode tab.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 18194 on_video_catalogue_add_classic')

        if media_data_obj.source:

            utils.add_links_to_textview(
                self.app_obj,
                [ media_data_obj.source ],
                self.classic_textbuffer,
                self.classic_mark_start,
                self.classic_mark_end,
            )


    def on_video_catalogue_add_classic_multi(self, menu_item, media_data_list):

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

        Adds the selected videos' URLs to the textview in the Classic Mode tab.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_list (list): List of one or more media.Video objects

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 18222 on_video_catalogue_add_classic_multi')

        source_list = []
        for media_data_obj in media_data_list:
            if media_data_obj.source:
                source_list.append(media_data_obj.source)

        if source_list:

            utils.add_links_to_textview(
                self.app_obj,
                source_list,
                self.classic_textbuffer,
                self.classic_mark_start,
                self.classic_mark_end,
            )


    def on_video_catalogue_apply_options(self, menu_item, media_data_obj):

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

        Adds a set of download options (handled by an
        options.OptionsManager object) to the specified video object.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 18256 on_video_catalogue_apply_options')

        if self.app_obj.current_manager_obj or media_data_obj.options_obj:
            return self.app_obj.system_error(
                246,
                'Callback request denied due to current conditions',
            )

        # If there are any options manager objects besides the General Options
        #   Manager, and the manager attached to the Classic Mode tab, then we
        #   need to prompt the user to select one
        prompt_flag = False
        for options_obj in self.app_obj.options_reg_dict.values():

            if options_obj != self.app_obj.general_options_obj \
            and (
                self.app_obj.classic_options_obj == None \
                or options_obj != self.app_obj.classic_options_obj
            ):
                prompt_flag = True
                break

        if not prompt_flag:

            # Apply download options to the media data object
            self.app_obj.apply_download_options(media_data_obj)

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

        else:

            # Prompt the user to specify new or existing download options
            dialogue_win = ApplyOptionsDialogue(self)
            response = dialogue_win.run()
            # Get the specified options.OptionsManager object, before
            #   destroying the window
            options_obj = dialogue_win.options_obj
            clone_flag = dialogue_win.clone_flag
            dialogue_win.destroy()

            if response == Gtk.ResponseType.OK:

                if clone_flag:

                    options_obj = self.app_obj.clone_download_options(
                        options_obj,
                    )

                # Apply the specified (or new) download options to the media
                #   data object
                self.app_obj.apply_download_options(
                    media_data_obj,
                    options_obj,
                )

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


    def on_video_catalogue_check(self, menu_item, media_data_obj):

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

        Check the right-clicked media data object.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 18337 on_video_catalogue_check')

        download_manager_obj = self.app_obj.download_manager_obj

        if (
            self.app_obj.current_manager_obj \
            and not download_manager_obj
        ) or (
            download_manager_obj \
            and download_manager_obj.operation_classic_flag
        ):
            return self.app_obj.system_error(
                247,
                'Callback request denied due to current conditions',
            )

        if download_manager_obj:

            # Download operation already in progress. Add this video to its
            #   list
            return_list = download_manager_obj.download_list_obj.create_item(
                media_data_obj,
                None,               # media.Scheduled object
                'sim',              # override_operation_type
                False,              # priority_flag
                False,              # ignore_limits_flag
            )

            for download_item_obj in return_list:

                # Add a row to the Progress List
                self.progress_list_add_row(
                    download_item_obj.item_id,
                    download_item_obj.media_data_obj,
                )

                # Update the main window's progress bar
                self.app_obj.download_manager_obj.nudge_progress_bar()

        else:

            # Start a new download operation to download this video
            self.app_obj.download_manager_start(
                'sim',
                False,
                [media_data_obj],
            )


    def on_video_catalogue_check_multi(self, menu_item, media_data_list):

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

        Check the right-clicked media data object(s).

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_list (list): List of one or more media.Video objects

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 18402 on_video_catalogue_check_multi')

        download_manager_obj = self.app_obj.download_manager_obj

        if (
            self.app_obj.current_manager_obj \
            and not download_manager_obj
        ) or (
            download_manager_obj \
            and download_manager_obj.operation_classic_flag
        ):
            return self.app_obj.system_error(
                248,
                'Callback request denied due to current conditions',
            )

        if download_manager_obj:

            # Download operation already in progress. Add these video to its
            #   list
            return_list = []
            for media_data_obj in media_data_list:

                return_list \
                += download_manager_obj.download_list_obj.create_item(
                    media_data_obj,
                    None,           # media.Scheduled object
                    'sim',          # override_operation_type
                    False,          # priority_flag
                    False,          # ignore_limits_flag
                )

            for download_item_obj in return_list:

                # Add a row to the Progress List
                self.progress_list_add_row(
                    download_item_obj.item_id,
                    download_item_obj.media_data_obj,
                )

                # Update the main window's progress bar
                self.app_obj.download_manager_obj.nudge_progress_bar()

        else:

            # Start a new download operation to download these videos
            self.app_obj.download_manager_start(
                'sim',
                False,
                media_data_list,
            )

        # Standard de-selection of everything in the Video Catalogue
        self.video_catalogue_unselect_all()


    def on_video_catalogue_custom_dl(self, menu_item, media_data_obj):

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

        Custom download the right-clicked media data object.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 18471 on_video_catalogue_custom_dl')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                249,
                'Callback request denied due to current conditions',
            )

        # Start a custom download operation
        if not self.app_obj.general_custom_dl_obj.dl_by_video_flag \
        or not self.app_obj.general_custom_dl_obj.dl_precede_flag:

            self.app_obj.download_manager_start(
                'custom_real',
                False,
                [media_data_obj],
                self.app_obj.general_custom_dl_obj,
            )

        else:

            self.app_obj.download_manager_start(
                'custom_sim',
                False,
                [media_data_obj],
                self.app_obj.general_custom_dl_obj,
            )


    def on_video_catalogue_custom_dl_multi(self, menu_item, media_data_list):

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

        Custom download the right-clicked media data objects(s).

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_list (list): List of one or more media.Video objects

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 18515 on_video_catalogue_custom_dl_multi')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                250,
                'Callback request denied due to current conditions',
            )

        # Start a download operation
        if not self.app_obj.general_custom_dl_obj.dl_by_video_flag \
        or not self.app_obj.general_custom_dl_obj.dl_precede_flag:

            self.app_obj.download_manager_start(
                'custom_real',
                False,
                media_data_list,
                self.app_obj.general_custom_dl_obj,
            )

        else:

            self.app_obj.download_manager_start(
                'custom_sim',
                False,
                media_data_list,
                self.app_obj.general_custom_dl_obj,
            )

        # Standard de-selection of everything in the Video Catalogue
        self.video_catalogue_unselect_all()


    def on_video_catalogue_delete_video(self, menu_item, media_data_obj):

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

        Deletes the right-clicked media data object.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 18562 on_video_catalogue_delete_video')

        if self.app_obj.show_delete_video_dialogue_flag:

            # Prompt the user for confirmation
            dialogue_win = DeleteVideoDialogue(self, [ media_data_obj ])
            response = dialogue_win.run()

            # Retrieve user choices from the dialogue window...
            if dialogue_win.button2.get_active():
                delete_file_flag = True
            else:
                delete_file_flag = False

            if dialogue_win.button3.get_active():
                show_win_flag = True
            else:
                show_win_flag = False

            # ...before destroying it
            dialogue_win.destroy()

            if response == Gtk.ResponseType.OK:

                # Update IVs
                self.app_obj.set_delete_video_files_flag(delete_file_flag)
                self.app_obj.set_show_delete_video_dialogue_flag(show_win_flag)

                # Delete the video
                self.app_obj.delete_video(media_data_obj, delete_file_flag)

        else:

            # Delete the video without prompting
            self.app_obj.delete_video(
                media_data_obj,
                self.app_obj.delete_video_files_flag,
            )


    def on_video_catalogue_delete_video_multi(self, menu_item,
    media_data_list):

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

        Deletes the right-clicked media data objects.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_list (list): List of one or more media.Video objects

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 18618 on_video_catalogue_delete_video_multi')

        if self.app_obj.show_delete_video_dialogue_flag:

            # Prompt the user for confirmation
            dialogue_win = DeleteVideoDialogue(self, media_data_list)
            response = dialogue_win.run()

            # Retrieve user choices from the dialogue window...
            if dialogue_win.button2.get_active():
                delete_file_flag = True
            else:
                delete_file_flag = False

            if dialogue_win.button3.get_active():
                show_win_flag = True
            else:
                show_win_flag = False

            # ...before destroying it
            dialogue_win.destroy()

            if response == Gtk.ResponseType.OK:

                # Update IVs
                self.app_obj.set_delete_container_files_flag(delete_file_flag)
                self.app_obj.set_show_delete_video_dialogue_flag(show_win_flag)

                # Delete the videos
                for media_data_obj in media_data_list:
                    self.app_obj.delete_video(media_data_obj, delete_file_flag)

                # Standard de-selection of everything in the Video Catalogue
                self.video_catalogue_unselect_all()

        else:

            # Delete the videos without prompting
            for media_data_obj in media_data_list:
                self.app_obj.delete_video(
                    media_data_obj,
                    self.app_obj.delete_video_files_flag,
                )


    def on_video_catalogue_dl_and_watch(self, menu_item, media_data_obj):

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

        Downloads a video and then opens it using the system's default media
        player.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 18679 on_video_catalogue_dl_and_watch')

        # Can't download the video if it has no source, or if an update/
        #   refresh/process operation has started since the popup menu was
        #   created
        if not media_data_obj.dl_flag or not media_data_obj.source \
        or self.app_obj.update_manager_obj \
        or self.app_obj.refresh_manager_obj \
        or self.app_obj.process_manager_obj:

            # Download the video, and mark it to be opened in the system's
            #   default media player as soon as the download operation is
            #   complete
            # If a download operation is already in progress, the video is
            #   added to it
            self.app_obj.download_watch_videos( [media_data_obj] )


    def on_video_catalogue_dl_and_watch_multi(self, menu_item,
    media_data_list):

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

        Download the videos and then open them using the system's default media
        player.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_list (list): List of one or more media.Video objects

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 18714 on_video_catalogue_dl_and_watch_multi')

        # Only download videos which have a source URL
        mod_list = []
        for media_data_obj in media_data_list:
            if media_data_obj.source:
                mod_list.append(media_data_obj)

        # Can't download the videos if none have no source, or if an update/
        #   refresh/process operation has started since the popup menu was
        #   created
        if mod_list \
        and not self.app_obj.update_manager_obj \
        or self.app_obj.refresh_manager_obj \
        or self.app_obj.process_manager_obj:

            # Download the videos, and mark them to be opened in the system's
            #   default media player as soon as the download operation is
            #   complete
            # If a download operation is already in progress, the videos are
            #   added to it
            self.app_obj.download_watch_videos(mod_list)

        # Standard de-selection of everything in the Video Catalogue
        self.video_catalogue_unselect_all()


    def on_video_catalogue_download(self, menu_item, media_data_obj):

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

        Download the right-clicked media data object.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 18756 on_video_catalogue_download')

        download_manager_obj = self.app_obj.download_manager_obj

        if (
            self.app_obj.current_manager_obj \
            and not download_manager_obj
        ) or (
            self.app_obj.download_manager_obj \
            and download_manager_obj.operation_classic_flag
        ) or media_data_obj.live_mode == 1:
            return self.app_obj.system_error(
                251,
                'Callback request denied due to current conditions',
            )

        if download_manager_obj:

            # Download operation already in progress. Add this video to its
            #   list
            return_list = download_manager_obj.download_list_obj.create_item(
                media_data_obj,
                None,           # media.Scheduled object
                'real',         # override_operation_type
                False,          # priority_flag
                False,          # ignore_limits_flag
            )

            for download_item_obj in return_list:

                # Add a row to the Progress List
                self.progress_list_add_row(
                    download_item_obj.item_id,
                    download_item_obj.media_data_obj,
                )

                # Update the main window's progress bar
                self.app_obj.download_manager_obj.nudge_progress_bar()

        else:

            # Start a new download operation to download this video
            self.app_obj.download_manager_start(
                'real',
                False,
                [media_data_obj],
            )


    def on_video_catalogue_download_multi(self, menu_item, media_data_list,
    live_wait_flag):

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

        Download the right-clicked media data objects(s).

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_list (list): List of one or more media.Video objects

            live_wait_flag (bool): True if any of the videos in media_data_list
                are livestreams that have not started; False otherwise

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 18825 on_video_catalogue_download_multi')

        download_manager_obj = self.app_obj.download_manager_obj

        if (
            self.app_obj.current_manager_obj \
            and not download_manager_obj
        ) or (
            self.app_obj.download_manager_obj \
            and download_manager_obj.operation_classic_flag
        ) or live_wait_flag:
            return self.app_obj.system_error(
                252,
                'Callback request denied due to current conditions',
            )

        if download_manager_obj:

            # Download operation already in progress. Add these videos to its
            #   list
            return_list = []
            for media_data_obj in media_data_list:

                return_list \
                += download_manager_obj.download_list_obj.create_item(
                    media_data_obj,
                    None,               # media.Scheduled object
                    'real',             # override_operation_type
                    False,              # priority_flag
                    False,              # ignore_limits_flag
                )

            for download_item_obj in return_list:

                # Add a row to the Progress List
                self.progress_list_add_row(
                    download_item_obj.item_id,
                    download_item_obj.media_data_obj,
                )

                # Update the main window's progress bar
                self.app_obj.download_manager_obj.nudge_progress_bar()

        else:

            # Start a new download operation to download this video
            self.app_obj.download_manager_start(
                'real',
                False,
                media_data_list,
            )

        # Standard de-selection of everything in the Video Catalogue
        self.video_catalogue_unselect_all()


    def on_video_catalogue_edit_options(self, menu_item, media_data_obj):

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

        Edit the download options (handled by an
        options.OptionsManager object) for the specified video object.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 18895 on_video_catalogue_edit_options')

        if self.app_obj.current_manager_obj or not media_data_obj.options_obj:
            return self.app_obj.system_error(
                253,
                'Callback request denied due to current conditions',
            )

        # Open an edit window
        config.OptionsEditWin(
            self.app_obj,
            media_data_obj.options_obj,
        )


    def on_video_catalogue_enforce_check(self, menu_item, media_data_obj):

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

        Set the video object's flag to force checking (disabling an actual
        downloads).

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 18926 on_video_catalogue_enforce_check')

        # (Don't allow the user to change the setting of
        #   media.Video.dl_sim_flag if the video is in a channel or playlist,
        #   since media.Channel.dl_sim_flag or media.Playlist.dl_sim_flag
        #   applies instead)
        if self.app_obj.current_manager_obj \
        or not isinstance(media_data_obj.parent_obj, media.Folder):
            return self.app_obj.system_error(
                254,
                'Callback request denied due to current conditions',
            )

        if not media_data_obj.dl_sim_flag:
            media_data_obj.set_dl_sim_flag(True)
        else:
            media_data_obj.set_dl_sim_flag(False)

        GObject.timeout_add(
            0,
            self.video_catalogue_update_video,
            media_data_obj,
        )


    def on_video_catalogue_fetch_formats(self, menu_item, media_data_obj):

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

        Fetches a list of available video/audio formats for the specified
        video, using an info operation.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 18967 on_video_catalogue_fetch_formats')

        # Can't start an info operation if any type of operation has started
        #   since the popup menu was created
        if media_data_obj.source \
        and not self.app_obj.current_manager_obj:

            # Fetch information about the video's available formats
            self.app_obj.info_manager_start('formats', media_data_obj)
            # Automatically switch to the Output tab, for convenience
            if self.app_obj.auto_switch_output_flag:
                self.output_tab_show_first_page()


    def on_video_catalogue_fetch_subs(self, menu_item, media_data_obj):

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

        Fetches a list of available subtitles for the specified video, using an
        info operation.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 18997 on_video_catalogue_fetch_subs')

        # Can't start an info operation if any type of operation has started
        #   since the popup menu was created
        if media_data_obj.source \
        and not self.app_obj.current_manager_obj:

            # Fetch information about the video's available subtitles
            self.app_obj.info_manager_start('subs', media_data_obj)
            # Automatically switch to the Output tab, for convenience
            if self.app_obj.auto_switch_output_flag:
                self.output_tab_show_first_page()


    def on_video_catalogue_finalise_livestream(self, menu_item, \
    media_data_obj):

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

        Marks the specified video, which is a livestream whose download was
        not completed, as a downloaded livestream, removing the .part from the
        end of the video file.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 19029 on_video_catalogue_finalise_livestream',
            )

        expect_path = media_data_obj.get_actual_path(self.app_obj)
        part_path = expect_path + '.part'
        self.app_obj.move_file_or_directory(part_path, expect_path)

        media_data_obj.set_file_from_path(expect_path)
        self.app_obj.mark_video_downloaded(media_data_obj, True)
        self.app_obj.mark_video_live(media_data_obj, 0)

        # Update the catalogue item
        GObject.timeout_add(
            0,
            self.video_catalogue_update_video,
            media_data_obj,
        )


    def on_video_catalogue_finalise_livestream_multi(self, menu_item,
    media_data_list):

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

        Marks the specified videos, which are livestreams whose download was
        not completed, as a downloaded livestreaj, removing the .part from the
        end of the video file.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_list (list): List of one or more media.Video objects

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 19067 on_video_catalogue_finalise_livestream_multi',
            )

        for media_data_obj in media_data_list:

            expect_path = media_data_obj.get_actual_path(self.app_obj)
            part_path = expect_path + '.part'

            if not media_data_obj.dl_flag \
            and (
                media_data_obj.live_mode == 2 \
                or (
                    media_data_obj.live_mode == 0 \
                    and media_data_obj.was_live_flag
                )
            ) and not os.path.isfile(expect_path) \
            and os.path.isfile(part_path):

                self.app_obj.move_file_or_directory(part_path, expect_path)

                media_data_obj.set_file_from_path(expect_path)
                self.app_obj.mark_video_downloaded(media_data_obj, True)
                self.app_obj.mark_video_live(media_data_obj, 0)


    def on_video_catalogue_livestream_toggle(self, menu_item, media_data_obj,
    action):

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

        Toggles one of five livestream action settings.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

            action (str): 'notify', 'alarm', 'open', 'dl_start', 'dl_stop'

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 19111 on_video_catalogue_livestream_toggle')

        # Update the IV
        if action == 'notify':
            if not media_data_obj.dbid \
            in self.app_obj.media_reg_auto_notify_dict:
                self.app_obj.add_auto_notify_dict(media_data_obj)
            else:
                self.app_obj.del_auto_notify_dict(media_data_obj)
        elif action == 'alarm':
            if not media_data_obj.dbid \
            in self.app_obj.media_reg_auto_alarm_dict:
                self.app_obj.add_auto_alarm_dict(media_data_obj)
            else:
                self.app_obj.del_auto_alarm_dict(media_data_obj)
        elif action == 'open':
            if not media_data_obj.dbid \
            in self.app_obj.media_reg_auto_open_dict:
                self.app_obj.add_auto_open_dict(media_data_obj)
            else:
                self.app_obj.del_auto_open_dict(media_data_obj)
        elif action == 'dl_start':
            if not media_data_obj.dbid \
            in self.app_obj.media_reg_auto_dl_start_dict:
                self.app_obj.add_auto_dl_start_dict(media_data_obj)
            else:
                self.app_obj.del_auto_dl_start_dict(media_data_obj)
        elif action == 'dl_stop':
            if not media_data_obj.dbid \
            in self.app_obj.media_reg_auto_dl_stop_dict:
                self.app_obj.add_auto_dl_stop_dict(media_data_obj)
            else:
                self.app_obj.del_auto_dl_stop_dict(media_data_obj)

        # Update the catalogue item
        GObject.timeout_add(
            0,
            self.video_catalogue_update_video,
            media_data_obj,
        )


    def on_video_catalogue_mark_temp_dl(self, menu_item, media_data_obj):

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

        Creates a media.Video object in the 'Temporary Videos' folder. The new
        video object has the same source URL as the specified media_data_obj.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 19169 on_video_catalogue_mark_temp_dl')

        # Can't mark the video for download if it has no source, or if an
        #   update/refresh/tidy/process operation has started since the popup
        #   menu was created
        if media_data_obj.source \
        and not self.app_obj.update_manager_obj \
        and not self.app_obj.refresh_manager_obj \
        and not self.app_obj.tidy_manager_obj \
        and not self.app_obj.process_manager_obj:

            # Create a new media.Video object in the 'Temporary Videos' folder
            #   (but don't download anything now)
            new_media_data_obj = self.app_obj.add_video(
                self.app_obj.fixed_temp_folder,
                media_data_obj.source,
            )

            if new_media_data_obj:

                # We can set the temporary video's name/description, if known
                new_media_data_obj.set_cloned_name(media_data_obj)
                # Remember the name of the original container object, for
                #   display in the Video catalogue
                new_media_data_obj.set_orig_parent(media_data_obj.parent_obj)


    def on_video_catalogue_mark_temp_dl_multi(self, menu_item,
    media_data_list):

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

        Creates new media.Video objects in the 'Temporary Videos' folder. The
        new video objects have the same source URL as the video objects in the
        specified media_data_list.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_list (list): List of one or more media.Video objects

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 19214 on_video_catalogue_mark_temp_dl_multi')

        # Only download videos which have a source URL
        mod_list = []
        for media_data_obj in media_data_list:
            if media_data_obj.source:
                mod_list.append(media_data_obj)

        # Can't mark the videos for download if they have no source, or if an
        #   update/refresh/tidy/process operation has started since the popup
        #   menu was created
        if mod_list \
        and not self.app_obj.update_manager_obj \
        and not self.app_obj.refresh_manager_obj \
        and not self.app_obj.tidy_manager_obj \
        and not self.app_obj.process_manager_obj:

            for media_data_obj in mod_list:

                # Create a new media.Video object in the 'Temporary Videos'
                #   folder
                new_media_data_obj = self.app_obj.add_video(
                    self.app_obj.fixed_temp_folder,
                    media_data_obj.source,
                )

                # We can set the temporary video's name/description, if known
                new_media_data_obj.set_cloned_name(media_data_obj)
                # Remember the name of the original container object, for
                #   display in the Video catalogue
                if new_media_data_obj:
                    new_media_data_obj.set_orig_parent(
                        media_data_obj.parent_obj,
                    )

        # Standard de-selection of everything in the Video Catalogue
        self.video_catalogue_unselect_all()


    def on_video_catalogue_not_livestream(self, menu_item, media_data_obj):

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

        Marks the specified video as not a livestream after all.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 19268 on_video_catalogue_not_livestream')

        # Update the video
        self.app_obj.mark_video_live(
            media_data_obj,
            0,                  # Not a livestream
        )

        # Update the catalogue item
        GObject.timeout_add(
            0,
            self.video_catalogue_update_video,
            media_data_obj,
        )


    def on_video_catalogue_not_livestream_multi(self, menu_item,
    media_data_list):

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

        Marks the specified videos as not livestreams after all.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_list (list): List of one or more media.Video objects

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 19300 on_video_catalogue_not_livestream_multi',
            )

        for media_data_obj in media_data_list:
            if media_data_obj.live_mode:
                self.app_obj.mark_video_live(
                    media_data_obj,
                    0,                  # Not a livestream
                )


    def on_video_catalogue_output_override(self, menu_item, media_data_obj):

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

        Downloads (or re-downloads) the right-clicked media.Video object,
        specifying a video name that overrides youtube-dl's output template.

        Based on code in self.on_video_catalogue_download() and
        self.on_video_catalogue_re_download().

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 19331 on_video_catalogue_output_override')

        # Prompt the user
        dialogue_win = OutputOverrideDialogue(self, media_data_obj)
        response = dialogue_win.run()

        # Get the user's choices, before destroying the window
        name = dialogue_win.new_name
        dialogue_win.destroy()

        if response != Gtk.ResponseType.OK:
            return

        # Basic checks
        download_manager_obj = self.app_obj.download_manager_obj

        if (
            self.app_obj.current_manager_obj \
            and not download_manager_obj
        ) or (
            self.app_obj.download_manager_obj \
            and download_manager_obj.operation_classic_flag
        ) or media_data_obj.live_mode == 1:
            return self.app_obj.system_error(
                275,
                'Callback request denied due to current conditions',
            )

        if name is None or re.search(r'^\s*$', name):
            return

        elif os.name != 'nt' and name in self.app_obj.illegal_name_mswin_list:

            self.dialogue_manager_obj.show_msg_dialogue(
                _('The name \'{0}\' is not allowed').format(name),
                'error',
                'ok',
            )

            return

        # Update IVs
        self.app_obj.add_temp_output_override_dict(
            media_data_obj.dbid,
            name,
        )

        # (Reset the video's nickname to match its name. This looks good when
        #   re-downloading a video, to which an output tempalte override had
        #   been applied)
        media_data_obj.set_nickname(media_data_obj.name)

        # If the video is already downloaded...
        if media_data_obj.dl_flag:

            # ...delete the files associated with the video
            self.app_obj.delete_video_files(media_data_obj)

            # No download operation will start, if the media.Video object is
            #   marked as downloaded
            self.app_obj.mark_video_downloaded(media_data_obj, False)

        # (Re-)download the video
        if download_manager_obj:

            # Download operation already in progress. Add this video to its
            #   list
            return_list = download_manager_obj.download_list_obj.create_item(
                media_data_obj,
                None,           # media.Scheduled object
                'real',         # override_operation_type
                False,          # priority_flag
                False,          # ignore_limits_flag
            )

            for download_item_obj in return_list:

                # Add a row to the Progress List
                self.progress_list_add_row(
                    download_item_obj.item_id,
                    download_item_obj.media_data_obj,
                )

                # Update the main window's progress bar
                self.app_obj.download_manager_obj.nudge_progress_bar()

        else:

            # Start a new download operation to download this video
            self.app_obj.download_manager_start(
                'real',
                False,
                [media_data_obj],
            )


    def on_video_catalogue_page_entry_activated(self, entry):

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

        Switches to a different page in the Video Catalogue (or re-inserts the
        current page number, if the user typed an invalid page number).

        Args:

            entry (Gtk.Entry): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 19442 on_video_catalogue_page_entry_activated',
            )

        page_num = utils.strip_whitespace(entry.get_text())

        if self.video_index_current_dbid is None \
        or not page_num.isdigit() \
        or int(page_num) < 1 \
        or int(page_num) > self.catalogue_toolbar_last_page:
            # Invalid page number, so reinsert the number of the page that's
            #   actually visible
            entry.set_text(str(self.catalogue_toolbar_current_page))

        else:
            # Switch to a different page
            self.video_catalogue_redraw_all(
                self.video_index_current_dbid,
                int(page_num),
            )


    def on_video_catalogue_process_clip(self, menu_item, media_data_obj):

        """Called from a callback in self.video_catalogue_popup_menu() (only).

        Sends the right-clicked media.Video object to FFmpeg for
        post-processing, first prompting the user for a set of timestamps which
        are used to split the video into clips.

        Alternatively, instructs yt-dlp (when available) to download video
        chapters, handling them as clips.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 19484 on_video_catalogue_process_clip')

        # Prompt the user
        dialogue_win = PrepareClipDialogue(self, media_data_obj)
        response = dialogue_win.run()

        # Get the user's choices, before destroying the window
        clip_mode = dialogue_win.clip_mode
        dl_mode = dialogue_win.dl_mode
        start_stamp = dialogue_win.start_stamp
        stop_stamp = dialogue_win.stop_stamp
        clip_title = dialogue_win.clip_title
        stamp_list = dialogue_win.stamp_list
        start_dl_flag = dialogue_win.start_dl_flag

        dialogue_win.destroy()

        if not start_dl_flag:
            return

        # Check the validity of any specified timestamps
        regex = '^' + self.app_obj.timestamp_regex + '$'
        if start_stamp is not None:

            if not re.search(regex, start_stamp) \
            or (
                stop_stamp is not None \
                and not re.search(regex, stop_stamp)
            ) or not utils.timestamp_compare(
                self.app_obj,
                start_stamp,
                stop_stamp,
            ):
                self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                    _('Invalid timestamp(s)'),
                    'error',
                    'ok',
                )

                return

        elif stamp_list:

            # Groups of 3, in the form [start, optional_stop, optional_title]
            for mini_list in stamp_list:

                if not re.search(regex, mini_list[0]) \
                or (
                    mini_list[1] is not None \
                    and not re.search(regex, mini_list[1])
                ) or not utils.timestamp_compare(
                    self.app_obj,
                    mini_list[0],
                    mini_list[1],
                ):
                    self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                        _('Invalid timestamp(s)'),
                        'error',
                        'ok',
                    )

                    return

        # Clip mode: 'downloader' to download clips using the downloader
        #   directly (currently only available for yt-dlp), 'ffmpeg' to
        #   download clips using FFmpeg, or 'create' to create clips using the
        #   already-downloaded video
        # Download mode: 'chapters' to download chapters using yt-dlp directly
        #   (when available), 'single' to download/extract a single clip using
        #   specified timestamp(s), 'multiple' to download/extract multiple
        #   clips
        if clip_mode == 'create':

            if dl_mode == 'single':
                insert_list \
                = ['create', [start_stamp, stop_stamp, clip_title] ]

            elif dl_mode == 'multiple':
                insert_list = ['create']
                for mini_list in stamp_list:
                    insert_list.append(mini_list)

            # Update IVs
            self.app_obj.add_temp_stamp_buffer_dict(
                media_data_obj.dbid,
                insert_list,
            )

            # Start a process operation to split the clip from the already-
            #   downloaded video
            self.app_obj.process_manager_start(
                self.app_obj.ffmpeg_options_obj,
                [media_data_obj],
            )

        else:

            if dl_mode == 'chapters':
                insert_list = ['chapters']

            elif dl_mode == 'single':
                insert_list \
                = [clip_mode, [start_stamp, stop_stamp, clip_title] ]

            elif dl_mode == 'multiple':
                insert_list = [clip_mode]
                for mini_list in stamp_list:
                    insert_list.append(mini_list)

            # Update IVs
            self.app_obj.set_video_timestamps_dl_mode(clip_mode)
            self.app_obj.add_temp_stamp_buffer_dict(
                media_data_obj.dbid,
                insert_list,
            )

            # Start a (custom) download operation to download the clip. We
            #   don't need to specify a downloads.CustomDLManager in this case
            self.app_obj.download_manager_start(
                'custom_real',
                # Not called from .script_slow_timer_callback()
                False,
                [media_data_obj],
            )


    def on_video_catalogue_process_clip_classic_mode(self,
    media_data_obj=None):

        """Called from a callback in
        mainapp.TartubeApp.on_button_classic_add_clips().

        Also called by mainapp.TartubeApp.on_button_classic_clips().

        Handles video clips in the Classic Mode tab, combining code from
        self.on_video_catalogue_process_clip() and
        self.classic_mode_tab_add_urls().

        Opens the usual 'Create video clips' dialogue, then creates new dummy
        media.Video objects for each item, and updates IVs.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video or None): When called in reponse to
                clicking the 'Add clips' button, None. When called from the
                popup menu in the Classic Progress List, a dummy media.Video
                object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 19637 on_video_catalogue_process_clip_classic_mode',
            )

        # Prompt the user
        dialogue_win = PrepareClipDialogue(self, media_data_obj)
        response = dialogue_win.run()

        # Get the user's choices, before destroying the window
        clip_mode = dialogue_win.clip_mode
        dl_mode = dialogue_win.dl_mode
        start_stamp = dialogue_win.start_stamp
        stop_stamp = dialogue_win.stop_stamp
        clip_title = dialogue_win.clip_title
        stamp_list = dialogue_win.stamp_list
        start_dl_flag = dialogue_win.start_dl_flag

        classic_stamp_list = dialogue_win.classic_stamp_list
        classic_url = dialogue_win.classic_url
        classic_video_name = dialogue_win.classic_video_name

        dialogue_win.destroy()

        if not start_dl_flag:
            return

        # Check the validity of any specified timestamps
        regex = '^' + self.app_obj.timestamp_regex + '$'
        if start_stamp is not None:

            if not re.search(regex, start_stamp) \
            or (
                stop_stamp is not None \
                and not re.search(regex, stop_stamp)
            ) or not utils.timestamp_compare(
                self.app_obj,
                start_stamp,
                stop_stamp,
            ):
                self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                    _('Invalid timestamp(s)'),
                    'error',
                    'ok',
                )

                return

        elif stamp_list:

            # Groups of 3, in the form [start, optional_stop, optional_title]
            for mini_list in stamp_list:

                if not re.search(regex, mini_list[0]) \
                or (
                    mini_list[1] is not None \
                    and not re.search(regex, mini_list[1])
                ) or not utils.timestamp_compare(
                    self.app_obj,
                    mini_list[0],
                    mini_list[1],
                ):
                    self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                        _('Invalid timestamp(s)'),
                        'error',
                        'ok',
                    )

                    return

        # Create the dummy media.Video object
        if not media_data_obj:

            # Remove initial/final whitespace from the URL, and reject an
            #   invalid URL
            classic_url = utils.strip_whitespace(classic_url)
            if not utils.check_url(classic_url):
                self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                    _('Invalid URL'),
                    'error',
                    'ok',
                )

                return

            # Get the specified download destination
            tree_iter = self.classic_dest_dir_combo.get_active_iter()
            model = self.classic_dest_dir_combo.get_model()
            dest_dir = model[tree_iter][0]

            # Get the specified video/audio format, leaving the value as None
            #   if the 'Default' item is selected
            tree_iter2 = self.classic_format_combo.get_active_iter()
            model2 = self.classic_format_combo.get_model()
            format_str = model2[tree_iter2][0]
            # (Valid formats begin with whitespace)
            if not re.search(r'^\s', format_str):
                format_str = None
            else:
                format_str = re.sub(r'^\s*', '', format_str)
                # (One last check for a valid video/audio format)
                if not format_str in formats.VIDEO_FORMAT_LIST \
                and not format_str in formats.AUDIO_FORMAT_LIST:
                    format_str = None

            # Set the specified resolution, leaving the value as None if the
            #   'Highest' item is selected
            tree_iter3 = self.classic_resolution_combo.get_active_iter()
            model3 = self.classic_resolution_combo.get_model()
            resolution_str = model3[tree_iter3][0]
            # (Selectable resolutions in the combo begin with whitespace)
            if not re.search(r'^\s', resolution_str):
                resolution_str = None
            else:
                resolution_str = utils.strip_whitespace(resolution_str)
                # (One last check for a valid resolution)
                if not resolution_str in formats.VIDEO_RESOLUTION_LIST:
                    resolution_str = None

            # If the combobox item is selected, we convert a downloaded video
            #   to the specified format with FFmpeg/AVConv. This is signified
            #   by adding 'convert_' to the beginning of the format string
            tree_iter4 = self.classic_convert_combo.get_active_iter()
            model4 = self.classic_convert_combo.get_model()
            convert_str = model4[tree_iter4][0]
            if format_str is not None \
            and convert_str == _('Convert to this format'):
                format_str = 'convert_' + format_str
            # The resolution, if specified, is added to the end of the format
            #   string
            if resolution_str is not None:
                if format_str is None:
                    format_str = resolution_str
                else:
                    format_str += '_' + resolution_str

            # Create the dummy media.Video object. Dummy media.Video objects
            #   have negative .dbids, and are not added to the media data
            #   registry. The True argument tells the function to ignore the
            #   value of mainapp.TartubeApp.classic_sblock_flag
            dummy_video_obj = self.classic_mode_tab_create_dummy_video(
                classic_url,
                dest_dir,
                format_str,
                True,
            )

        else:

            # Create a new dummy media.Video object, with the same properties
            #   as the existing one. The True argument tells the function to
            #   ignore the value of mainapp.TartubeApp.classic_sblock_flag
            dummy_video_obj = self.classic_mode_tab_create_dummy_video(
                media_data_obj.source,
                media_data_obj.dummy_dir,
                media_data_obj.dummy_format,
                True,
            )

        # Set the dummy media.Video object's nickname, if the user provided
        #   one; utils.clip_prepare_title() and
        #   ClipDownloader.do_download_clips_with_downloader() will both look
        #   out for it, and use it in the video's file name
        if classic_video_name is not None:
            dummy_video_obj.set_nickname(classic_video_name)

        # Set the dummy media.Video object's timestamps, and start the
        #   download/process operation
        if clip_mode == 'create':

            if dl_mode == 'single':
                insert_list \
                = ['create', [start_stamp, stop_stamp, clip_title] ]

            elif dl_mode == 'multiple':
                insert_list = ['create']
                for mini_list in stamp_list:
                    insert_list.append(mini_list)

            # Update IVs
            self.app_obj.add_temp_stamp_buffer_dict(
                dummy_video_obj.dbid,
                insert_list,
            )

            # Start a process operation to split the clip from the already-
            #   downloaded video
            self.app_obj.process_manager_start(
                self.app_obj.ffmpeg_options_obj,
                [dummy_video_obj],
            )

        else:

            if dl_mode == 'chapters':
                insert_list = ['chapters']

            elif dl_mode == 'single':
                insert_list \
                = [clip_mode, [start_stamp, stop_stamp, clip_title] ]

            elif dl_mode == 'multiple':
                insert_list = [clip_mode]
                for mini_list in stamp_list:
                    insert_list.append(mini_list)

            # Update IVs
            self.app_obj.set_video_timestamps_dl_mode(clip_mode)
            self.app_obj.add_temp_stamp_buffer_dict(
                dummy_video_obj.dbid,
                insert_list,
            )


    def on_video_catalogue_process_ffmpeg(self, menu_item, media_data_obj):

        """Called from a callback in self.video_catalogue_popup_menu() and
        .results_list_popup_menu().

        Sends the right-clicked media.Video object to FFmpeg for
        post-processing.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 19867 on_video_catalogue_process_ffmpeg')

        # Can't start a process operation if another operation has started
        #   since the popup menu was created, or if the video hasn't been
        #   downloaded
        if not self.app_obj.current_manager_obj \
        and media_data_obj.file_name is not None:

            # (There is a lot of code, so use one function instead of two)
            self.on_video_catalogue_process_ffmpeg_multi(
                menu_item,
                [ media_data_obj ],
            )


    def on_video_catalogue_process_ffmpeg_multi(self, menu_item, \
    media_data_list):

        """Called from a callback in self.video_catalogue_multi_popup_menu().
        For efficiency, also called by
        self.on_video_catalogue_process_ffmpeg().

        Sends the right-clicked media.Video objects to FFmpeg for
        post-processing.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_list (list): List of one or more media.Video objects

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 19901 on_video_catalogue_process_ffmpeg_multi',
            )

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by a popup menu.' \
            + ' In the Videos tab, right-click a video and select' \
            + ' Special > Process with FFmpeg...'
        )

        # Can't start a process operation if another operation has started
        #   since the popup menu was created
        if self.app_obj.current_manager_obj:
            return

        # Filter out any media.Video objects whose filename is not known (so
        #   cannot be processed)
        mod_list = []
        for video_obj in media_data_list:

            if video_obj.file_name is not None:
                mod_list.append(video_obj)

        if not mod_list:

            self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                _('Only checked/downloaded videos can be processed by FFmpeg'),
                'error',
                'ok',
            )

            return

        # Create an edit window for the current FFmpegOptionsManager object.
        #   Supply it with the list of videos, so that the user can start the
        #   process operation from the edit window
        config.FFmpegOptionsEditWin(
            self.app_obj,
            self.app_obj.ffmpeg_options_obj,
            mod_list,
        )


    def on_video_catalogue_process_slice(self, menu_item, media_data_obj):

        """Called from a callback in self.video_catalogue_popup_menu() (only).

        Sends the right-clicked media.Video object to FFmpeg for
        post-processing, first prompting the user for a set of start/stop
        times of slices to remove from the video.

        Alternatively, downloads the video with slices removed.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 19963 on_video_catalogue_process_slice')

        # Prompt the user for start/stop times
        dialogue_win = PrepareSliceDialogue(self, media_data_obj)
        response = dialogue_win.run()

        # Get the specified start/stop times, before destroying the window
        slice_mode = dialogue_win.slice_mode
        dl_mode = dialogue_win.dl_mode
        start_time = utils.strip_whitespace(dialogue_win.start_time)
        stop_time = utils.strip_whitespace(dialogue_win.stop_time)
        slice_list = dialogue_win.slice_list
        start_dl_flag = dialogue_win.start_dl_flag

        dialogue_win.destroy()

        if not start_dl_flag:
            return

        # If the user clicked the button in the 'one slice' section, check the
        #   times are valid
        if dl_mode == 'single':

            # Check times are valid
            try:
                start_time = float(
                    utils.timestamp_convert_to_seconds(
                        self.app_obj,
                        start_time,
                    )
                )

                if stop_time is not None:
                    stop_time = float(
                        utils.timestamp_convert_to_seconds(
                            self.app_obj,
                            stop_time,
                        )
                    )

            except:
                self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                    _('Invalid start/stop times'),
                    'error',
                    'ok',
                )

                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',
                )

                return

            # Compile the mini-dictionary in the format described by
            #   media.Video.__init__()
            mini_dict = {
                'category': 'sponsor',
                'action': 'skip',
                'start_time': start_time,
                'stop_time': stop_time,
                'duration': 0,
            }

            insert_list = [mini_dict]

        else:
            insert_list = slice_list


        # Slice mode: 'ffmpeg' to remove slices using FFmpeg, or 'create' to
        #   remove slices using the already-downloaded video
        # Download mode: 'single' to remove a single slice using specified
        #   timestamps/seconds, 'multiple' to remove multiple slices. Set to
        #   None when slice_mode is 'create'
        if slice_mode == 'create':

            # Update IVs
            insert_list.insert(0, 'create')
            self.app_obj.add_temp_slice_buffer_dict(
                media_data_obj.dbid,
                insert_list,
            )

            # Start a process operation to remove the slices from the
            #   already-downloaded video
            self.app_obj.process_manager_start(
                self.app_obj.ffmpeg_options_obj,
                [media_data_obj],
            )

        else:

            # Update IVs
            insert_list.insert(0, 'default')
            self.app_obj.add_temp_slice_buffer_dict(
                media_data_obj.dbid,
                insert_list,
            )

            # Start a (custom) download operation to download the sliced
            #   video. We don't need to specify a downloads.CustomDLManager
            #   in this case
            self.app_obj.download_manager_start(
                'custom_real',
                # Not called from .script_slow_timer_callback()
                False,
                [media_data_obj],
            )


    def on_video_catalogue_re_download(self, menu_item, media_data_obj):

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

        Re-downloads the right-clicked media data object.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 20093 on_video_catalogue_re_download')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                255,
                'Callback request denied due to current conditions',
            )

        # Delete the files associated with the video
        self.app_obj.delete_video_files(media_data_obj)

        # No download operation will start, if the media.Video object is marked
        #   as downloaded
        self.app_obj.mark_video_downloaded(media_data_obj, False)

        # If mainapp.TartubeApp.allow_ytdl_archive_flag is set, youtube-dl will
        #   have created a ytdl_archive.txt, recording every video ever
        #   downloaded in the parent directory. This will prevent a successful
        #   re-downloading of the video
        # Temporarily block usage of the archive file
        self.app_obj.set_block_ytdl_archive_flag(True)

        # Now we're ready to start the download operation
        self.app_obj.download_manager_start('real', False, [media_data_obj] )


    def on_video_catalogue_reload_metadata(self, menu_item, media_data_obj):

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

        Reloads the .info.json file for the specified video, updating IVs in
        the media.Video object.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 20128 on_video_catalogue_reload_metadata')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by a popup menu.' \
            + ' In the Videos tab, right-click a video and select' \
            + ' Special > Reload metadata...'
        )

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                256,
                'Callback request denied due to current conditions',
            )

        # (Use a confirmation dialogue in the same format as used by
        #   self.on_video_catalogue_reload_metadata_multi)
        success_count = 0
        fail_count = 0

        if media_data_obj.file_name is None \
        or not media_data_obj.check_actual_path_by_ext(
            self.app_obj,
            '.info.json',
        ):
            fail_count = 1

        else:

            # (Just assume success, if the metadata file exists)
            success_count = 1

            # Extract video statistics from the metadata file
            self.app_obj.update_video_from_json(media_data_obj)

            # Set the new file's size, duration, and so on. The True argument
            #   instructs the function to override existing values
            if media_data_obj.dl_flag:
                self.app_obj.update_video_from_filesystem(
                    media_data_obj,
                    media_data_obj.get_actual_path(self.app_obj),
                    True,
                )

            # Redraw the video (which serves as a confirmation, if anything has
            #   changed)
            self.video_catalogue_update_video(media_data_obj)

        self.app_obj.dialogue_manager_obj.show_msg_dialogue(
            _(
            'Files reloaded: {0}, not reloaded: {1}',
            ).format(success_count, fail_count),
            'info',
            'ok',
            None,                   # Parent window is main window
        )


    def on_video_catalogue_reload_metadata_multi(self, menu_item, \
    media_data_list):

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

        Reloads the .info.json file for the specified videos, updating IVs in
        the media.Video objects.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_list (list): List of one or more media.Video objects

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 20202 on_video_catalogue_reload_metadata_multi',
            )

        if self.app_obj.current_manager_obj:
            return

        # Filter out any media.Video objects whose filename is not known (so
        #   the .info.json file is also not known)
        success_count = 0
        fail_count = 0
        for video_obj in media_data_list:

            if video_obj.file_name is None \
            or not video_obj.check_actual_path_by_ext(
                self.app_obj,
                '.info.json',
            ):
                fail_count += 1

            else:

                # (Just assume success, if the metadata file exists)
                success_count += 1

                # Extract video statistics from the metadata file
                self.app_obj.update_video_from_json(video_obj)

                # Set the new file's size, duration, and so on. The True
                #   argument instructs the function to override existing values
                if video_obj.dl_flag:
                    self.app_obj.update_video_from_filesystem(
                        video_obj,
                        video_obj.get_actual_path(self.app_obj),
                        True,
                    )

                # Redraw the video (which serves as a confirmation, if anything
                #   has changed)
                self.video_catalogue_update_video(video_obj)


        self.app_obj.dialogue_manager_obj.show_msg_dialogue(
            _(
            'Files reloaded: {0}, not reloaded: {1}',
            ).format(success_count, fail_count),
            'info',
            'ok',
            None,                   # Parent window is main window
        )


    def on_video_catalogue_remove_options(self, menu_item, media_data_obj):

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

        Removes a set of download options (handled by an
        options.OptionsManager object) from the specified video object.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 20270 on_video_catalogue_remove_options')

        if self.app_obj.current_manager_obj or not media_data_obj.options_obj:
            return self.app_obj.system_error(
                257,
                'Callback request denied due to current conditions',
            )

        # Remove download options from the media data object
        media_data_obj.reset_options_obj()


    def on_video_catalogue_size_entry_activated(self, entry):

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

        Sets the page size, and redraws the Video Catalogue (with the first
        page visible).

        Args:

            entry (Gtk.Entry): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 20296 on_video_catalogue_size_entry_activated',
            )

        size = utils.strip_whitespace(entry.get_text())

        if size.isdigit() and int(size) > 0:
            self.app_obj.set_catalogue_page_size(int(size))

            # Need to completely redraw the video catalogue to take account of
            #   the new page size
            if self.video_index_current_dbid is not None:

                self.video_catalogue_redraw_all(
                    self.video_index_current_dbid,
                    1,
                )

        else:
            # Invalid page size, so reinsert the size that's already visible
            entry.set_text(str(self.app_obj.catalogue_page_size))


    def on_video_catalogue_show_location(self, menu_item, media_data_obj):

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

        Shows the actual sub-directory in which the specified video is stored
        (which might be different from the default sub-directory, if the media
        data object's .master_dbid has been modified).

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 20336 on_video_catalogue_show_location')

        parent_obj = media_data_obj.parent_obj
        other_obj = self.app_obj.media_reg_dict[parent_obj.master_dbid]
        path = other_obj.get_actual_dir(self.app_obj)
        utils.open_file(self.app_obj, path)


    def on_video_catalogue_show_location_multi(self, menu_item, \
    media_data_list):

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

        Shows the actual sub-directory in which the specified videos are stored
        (which might be different from the default sub-directory, if the media
        data object's .master_dbid has been modified).

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 20362 on_video_catalogue_show_location_multi',
            )

        path_list = []

        for media_data_obj in media_data_list:

            parent_obj = media_data_obj.parent_obj
            other_obj = self.app_obj.media_reg_dict[parent_obj.master_dbid]
            path = other_obj.get_actual_dir(self.app_obj)

            # Don't show duplicate download locations
            if not path in path_list:
                utils.open_file(self.app_obj, path)
                path_list.append(path)


    def on_video_catalogue_show_properties(self, menu_item, media_data_obj):

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

        Opens an edit window for the video object.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 20395 on_video_catalogue_show_properties')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                258,
                'Callback request denied due to current conditions',
            )

        # Open the edit window immediately
        config.VideoEditWin(self.app_obj, media_data_obj)


    def on_video_catalogue_show_properties_multi(self, menu_item,
    media_data_list):

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

        Opens an edit window for each video object.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_list (list): List of one or more media.Video objects

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 20423 on_video_catalogue_show_properties_multi',
            )

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                259,
                'Callback request denied due to current conditions',
            )

        # Open the edit window immediately
        for media_data_obj in media_data_list:
            config.VideoEditWin(self.app_obj, media_data_obj)

        # Standard de-selection of everything in the Video Catalogue
        self.video_catalogue_unselect_all()


    def on_video_catalogue_show_system_cmd(self, menu_item, media_data_obj):

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

        Opens a dialogue window to show the system command that would be used
        to download the clicked video.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 20457 on_video_catalogue_show_system_cmd')

        # Show the dialogue window
        dialogue_win = SystemCmdDialogue(self, media_data_obj)
        dialogue_win.run()
        dialogue_win.destroy()


    def on_video_catalogue_sort_combo_changed(self, combo):

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

        In the Video Catalogue, set the sorting method for videos.

        Args:

            combo (Gtk.ComboBox): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 20478 on_video_catalogue_sort_combo_changed')

        tree_iter = self.catalogue_sort_combo.get_active_iter()
        model = self.catalogue_sort_combo.get_model()
        self.app_obj.set_catalogue_sort_mode(model[tree_iter][1])

        # Redraw the Video Catalogue, switching to the first page
        if self.video_index_current_dbid is not None:

            self.video_catalogue_redraw_all(
                self.video_index_current_dbid,
                1,
                True,           # Reset scrollbars
                True,           # Don't cancel the filter, if applied
            )


    def on_video_catalogue_thumb_combo_changed(self, combo):

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

        In the Video Catalogue, when videos are arranged on a grid, set the
        thumbnail size.

        Args:

            combo (Gtk.ComboBox): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 20509 on_video_catalogue_thumb_combo_changed',
            )

        tree_iter = self.catalogue_thumb_combo.get_active_iter()
        model = self.catalogue_thumb_combo.get_model()
        self.app_obj.set_thumb_size_custom(model[tree_iter][1])

        # Redraw the Video Catalogue, retaining the current page (but only when
        #   in grid mode)
        if self.video_index_current_dbid is not None \
        and self.app_obj.catalogue_mode_type == 'grid':

            self.video_catalogue_grid_check_size()
            self.video_catalogue_redraw_all(
                self.video_index_current_dbid,
                self.catalogue_toolbar_current_page,
                True,           # Reset scrollbars
                True,           # Don't cancel the filter, if applied
            )


    def on_video_catalogue_temp_dl(self, menu_item, media_data_obj, \
    watch_flag=False):

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

        Creates a media.Video object in the 'Temporary Videos' folder. The new
        video object has the same source URL as the specified media_data_obj.

        Downloads the video and optionally opens it using the system's default
        media player.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

            watch_flag (bool): If True, the video is opened using the system's
                default media player, after being downloaded

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 20554 on_video_catalogue_temp_dl')

        # Can't download the video if it has no source, or if an update/
        #   refresh/tidy/process operation has started since the popup menu was
        #   created
        if media_data_obj.source \
        and not self.app_obj.update_manager_obj \
        and not self.app_obj.refresh_manager_obj \
        and not self.app_obj.tidy_manager_obj \
        and not self.app_obj.process_manager_obj:

            # Create a new media.Video object in the 'Temporary Videos' folder
            new_media_data_obj = self.app_obj.add_video(
                self.app_obj.fixed_temp_folder,
                media_data_obj.source,
            )

            if new_media_data_obj:

                # We can set the temporary video's name/description, if known
                new_media_data_obj.set_cloned_name(media_data_obj)
                # Remember the name of the original container object, for
                #   display in the Video catalogue
                if new_media_data_obj:
                    new_media_data_obj.set_orig_parent(
                        media_data_obj.parent_obj,
                    )

                # Download the video. If a download operation is already in
                #   progress, the video is added to it
                # Optionally open the video in the system's default media
                #   player
                self.app_obj.download_watch_videos(
                    [new_media_data_obj],
                    watch_flag,
                )


    def on_video_catalogue_temp_dl_multi(self, menu_item,
    media_data_list, watch_flag=False):

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

        Creates new media.Video objects in the 'Temporary Videos' folder. The
        new video objects have the same source URL as the video objects in the
        specified media_data_list.

        Downloads the videos and optionally opens them using the system's
        default media player.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_list (list): List of one or more media.Video objects

            watch_flag (bool): If True, the video is opened using the system's
                default media player, after being downloaded

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 20616 on_video_catalogue_temp_dl_multi')

        # Only download videos which have a source URL
        mod_list = []
        for media_data_obj in media_data_list:
            if media_data_obj.source:
                mod_list.append(media_data_obj)

        # Can't download the videos if none have no source, or if an update/
        #   refresh/tidy/process operation has started since the popup menu was
        #   created
        ready_list = []
        if mod_list \
        and not self.app_obj.update_manager_obj \
        and not self.app_obj.refresh_manager_obj \
        and not self.app_obj.tidy_manager_obj \
        and not self.app_obj.process_manager_obj:

            for media_data_obj in mod_list:

                # Create a new media.Video object in the 'Temporary Videos'
                #   folder
                new_media_data_obj = self.app_obj.add_video(
                    self.app_obj.fixed_temp_folder,
                    media_data_obj.source,
                )

                if new_media_data_obj:

                    ready_list.append(new_media_data_obj)

                    # We can set the temporary video's name/description, if
                    #   known
                    new_media_data_obj.set_cloned_name(media_data_obj)
                    # Remember the name of the original container object, for
                    #   display in the Video catalogue
                    if new_media_data_obj:
                        new_media_data_obj.set_orig_parent(
                            media_data_obj.parent_obj,
                        )

        if ready_list:

            # Download the videos. If a download operation is already in
            #   progress, the videos are added to it
            # Optionally open the videos in the system's default media player
            self.app_obj.download_watch_videos(ready_list, watch_flag)

        # Standard de-selection of everything in the Video Catalogue
        self.video_catalogue_unselect_all()


    def on_video_catalogue_test_dl(self, menu_item, media_data_obj):

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

        Prompts the user to specify a URL and youtube-dl options. If the user
        specifies one or both, launches an info operation to test youtube-dl
        using the specified values.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 20685 on_video_catalogue_test_dl')

        # Can't start an info operation if any type of operation has started
        #   since the popup menu was created
        if not self.app_obj.current_manager_obj:

            # Prompt the user for what should be tested
            dialogue_win = TestCmdDialogue(self, media_data_obj.source)
            response = dialogue_win.run()

            # Retrieve user choices from the dialogue window...
            source = dialogue_win.entry.get_text()
            options_string = dialogue_win.textbuffer.get_text(
                dialogue_win.textbuffer.get_start_iter(),
                dialogue_win.textbuffer.get_end_iter(),
                False,
            )

            # ...before destroying it
            dialogue_win.destroy()

            # If the user specified either (or both) a URL and youtube-dl
            #   options, then we can proceed
            if response == Gtk.ResponseType.OK \
            and (re.search(r'\S', source) or re.search(r'\S', options_string)):
                # Start the info operation, which issues the youtube-dl command
                #   with the specified options
                self.app_obj.info_manager_start(
                    'test_ytdl',
                    None,                 # No media.Video object in this case
                    source,               # Use the source, if specified
                    options_string,       # Use download options, if specified
                )


    def on_video_catalogue_toggle_archived_video(self, menu_item, \
    media_data_obj):

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

        Marks the video as archived or not archived.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 20736 on_video_catalogue_toggle_archived_video',
            )

        if not media_data_obj.archive_flag:
            media_data_obj.set_archive_flag(True)
        else:
            media_data_obj.set_archive_flag(False)

        GObject.timeout_add(
            0,
            self.video_catalogue_update_video,
            media_data_obj,
        )


    def on_video_catalogue_toggle_archived_video_multi(self, menu_item,
    archived_flag, media_data_list):

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

        Mark the videos as archived or not archived.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            archived_flag (bool): True to mark the videos as archived, False to
                mark the videos as not archived

            media_data_list (list): List of one or more media.Video objects

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 20771 on_video_catalogue_toggle_archived_video_multi',
            )

        for media_data_obj in media_data_list:
            media_data_obj.set_archive_flag(archived_flag)
            GObject.timeout_add(
                0,
                self.video_catalogue_update_video,
                media_data_obj,
            )

        # Standard de-selection of everything in the Video Catalogue
        self.video_catalogue_unselect_all()


    def on_video_catalogue_toggle_bookmark_video(self, menu_item, \
    media_data_obj):

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

        Marks the video as bookmarked or not bookmarked.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 20803 on_video_catalogue_toggle_bookmark_video',
            )

        if not media_data_obj.bookmark_flag:
            self.app_obj.mark_video_bookmark(media_data_obj, True)
        else:
            self.app_obj.mark_video_bookmark(media_data_obj, False)


    def on_video_catalogue_toggle_bookmark_video_multi(self, menu_item,
    bookmark_flag, media_data_list):

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

        Mark the videos as bookmarked or not bookmarked.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            bookmark_flag (bool): True to mark the videos as bookmarked, False
                to mark the videos as not bookmarked

            media_data_list (list): List of one or more media.Video objects

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 20832 on_video_catalogue_toggle_bookmark_video_multi',
            )

        for media_data_obj in media_data_list:
            self.app_obj.mark_video_bookmark(media_data_obj, bookmark_flag)

        # Standard de-selection of everything in the Video Catalogue
        self.video_catalogue_unselect_all()


    def on_video_catalogue_toggle_favourite_video(self, menu_item, \
    media_data_obj):

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

        Marks the video as favourite or not favourite.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 20859 on_video_catalogue_toggle_favourite_video',
            )

        if not media_data_obj.fav_flag:
            self.app_obj.mark_video_favourite(media_data_obj, True)
        else:
            self.app_obj.mark_video_favourite(media_data_obj, False)


    def on_video_catalogue_toggle_favourite_video_multi(self, menu_item,
    fav_flag, media_data_list):

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

        Mark the videos as favourite or not favourite.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            fav_flag (bool): True to mark the videos as favourite, False to
                mark the videos as not favourite

            media_data_list (list): List of one or more media.Video objects

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 20888 on_video_catalogue_toggle_favourite_video_multi',
            )

        for media_data_obj in media_data_list:
            self.app_obj.mark_video_favourite(media_data_obj, fav_flag)

        # Standard de-selection of everything in the Video Catalogue
        self.video_catalogue_unselect_all()


    def on_video_catalogue_toggle_missing_video(self, menu_item, \
    media_data_obj):

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

        Marks the video as missing or not missing.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 20915 on_video_catalogue_toggle_missing_video',
            )

        if not media_data_obj.missing_flag:
            self.app_obj.mark_video_missing(media_data_obj, True)
        else:
            self.app_obj.mark_video_missing(media_data_obj, False)


    def on_video_catalogue_toggle_missing_video_multi(self, menu_item,
    missing_flag, media_data_list):

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

        Mark the videos as missing or not missing.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            missing_flag (bool): True to mark the videos as missing, False to
                mark the videos as not missing

            media_data_list (list): List of one or more media.Video objects

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 20944 on_video_catalogue_toggle_missing_video_multi',
            )

        for media_data_obj in media_data_list:
            self.app_obj.mark_video_missing(media_data_obj, missing_flag)

        # Standard de-selection of everything in the Video Catalogue
        self.video_catalogue_unselect_all()


    def on_video_catalogue_toggle_new_video(self, menu_item, media_data_obj):

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

        Marks the video as new (unwatched) or not new (watched).

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 20970 on_video_catalogue_toggle_new_video')

        if not media_data_obj.new_flag:
            self.app_obj.mark_video_new(media_data_obj, True)
        else:
            self.app_obj.mark_video_new(media_data_obj, False)


    def on_video_catalogue_toggle_new_video_multi(self, menu_item,
    new_flag, media_data_list):

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

        Mark the videos as new (unwatched) or not new (watched).

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            new_flag (bool): True to mark the videos as favourite, False to
                mark the videos as not favourite

            media_data_list (list): List of one or more media.Video objects

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 20997 on_video_catalogue_toggle_new_video_multi',
            )

        for media_data_obj in media_data_list:
            self.app_obj.mark_video_new(media_data_obj, new_flag)

        # Standard de-selection of everything in the Video Catalogue
        self.video_catalogue_unselect_all()


    def on_video_catalogue_toggle_waiting_video(self, menu_item, \
    media_data_obj):

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

        Marks the video as in the waiting list or not in the waiting list.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 21024 on_video_catalogue_toggle_waiting_video',
            )

        if not media_data_obj.waiting_flag:
            self.app_obj.mark_video_waiting(media_data_obj, True)
        else:
            self.app_obj.mark_video_waiting(media_data_obj, False)


    def on_video_catalogue_toggle_waiting_video_multi(self, menu_item,
    waiting_flag, media_data_list):

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

        Mark the videos as in the waiting list or not in the waiting list.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            waiting_flag (bool): True to mark the videos as in the waiting
                list, False to mark the videos as not in the waiting list

            media_data_list (list): List of one or more media.Video objects

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 21053 on_video_catalogue_toggle_waiting_video_multi',
            )

        for media_data_obj in media_data_list:
            self.app_obj.mark_video_waiting(media_data_obj, waiting_flag)

        # Standard de-selection of everything in the Video Catalogue
        self.video_catalogue_unselect_all()


    def on_video_catalogue_watch_hooktube(self, menu_item, media_data_obj):

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

        Watch a YouTube video on HookTube.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21079 on_video_catalogue_watch_hooktube')

        # Launch the video
        utils.open_file(
            self.app_obj,
            utils.convert_youtube_to_hooktube(media_data_obj.source),
        )

        # Mark the video as not new (having been watched)
        if media_data_obj.new_flag:
            self.app_obj.mark_video_new(media_data_obj, False)
        # Remove the video from the waiting list (having been watched)
        if media_data_obj.waiting_flag:
            self.app_obj.mark_video_waiting(media_data_obj, False)


    def on_video_catalogue_watch_invidious(self, menu_item, media_data_obj):

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

        Watch a YouTube video on Invidious.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21110 on_video_catalogue_watch_invidious')

        # Launch the video
        utils.open_file(
            self.app_obj,
            utils.convert_youtube_to_invidious(
                self.app_obj,
                media_data_obj.source,
            ),
        )

        # Mark the video as not new (having been watched)
        if media_data_obj.new_flag:
            self.app_obj.mark_video_new(media_data_obj, False)
        # Remove the video from the waiting list (having been watched)
        if media_data_obj.waiting_flag:
            self.app_obj.mark_video_waiting(media_data_obj, False)


    def on_video_catalogue_watch_video(self, menu_item, media_data_obj):

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

        Watch a video using the system's default media player, first checking
        that a file actually exists.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21145 on_video_catalogue_watch_video')

        # Launch the video
        self.app_obj.watch_video_in_player(media_data_obj)

        # Mark the video as not new (having been watched)
        if media_data_obj.new_flag:
            self.app_obj.mark_video_new(media_data_obj, False)
        # Remove the video from the waiting list (having been watched)
        if media_data_obj.waiting_flag:
            self.app_obj.mark_video_waiting(media_data_obj, False)


    def on_video_catalogue_watch_video_multi(self, menu_item, media_data_list):

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

        Watch the videos using the system's default media player, first
        checking that the files actually exist.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_list (list): List of one or more media.Video objects

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21174 on_video_catalogue_watch_video_multi')

        # Only watch videos which are marked as downloaded
        for media_data_obj in media_data_list:
            if media_data_obj.dl_flag:

                self.app_obj.watch_video_in_player(media_data_obj)

                # Mark the video as not new (having been watched)
                if media_data_obj.new_flag:
                    self.app_obj.mark_video_new(media_data_obj, False)
                # Remove the video from the waiting list (having been watched)
                if media_data_obj.waiting_flag:
                    self.app_obj.mark_video_waiting(media_data_obj, False)

        # Standard de-selection of everything in the Video Catalogue
        self.video_catalogue_unselect_all()


    def on_video_catalogue_watch_website(self, menu_item, media_data_obj):

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

        Watch a video on its primary website.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_obj (media.Video): The clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21208 on_video_catalogue_watch_website')

        # Launch the video
        utils.open_file(self.app_obj, media_data_obj.source)

        # Mark the video as not new (having been watched)
        if media_data_obj.new_flag:
            self.app_obj.mark_video_new(media_data_obj, False)
        # Remove the video from the waiting list (having been watched)
        if media_data_obj.waiting_flag:
            self.app_obj.mark_video_waiting(media_data_obj, False)


    def on_video_catalogue_watch_website_multi(self, menu_item,
    media_data_list):

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

        Watch videos on their primary websites.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_list (list): List of one or more media.Video objects

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 21237 on_video_catalogue_watch_website_multi',
            )

        # Only watch videos which have a source URL
        for media_data_obj in media_data_list:
            if media_data_obj.source is not None:

                # Launch the video
                utils.open_file(self.app_obj, media_data_obj.source)

                # Mark the video as not new (having been watched)
                if media_data_obj.new_flag:
                    self.app_obj.mark_video_new(media_data_obj, False)
                # Remove the video from the waiting list (having been watched)
                if media_data_obj.waiting_flag:
                    self.app_obj.mark_video_waiting(media_data_obj, False)

        # Standard de-selection of everything in the Video Catalogue
        self.video_catalogue_unselect_all()


    def on_progress_list_dl_last(self, menu_item, download_item_obj):

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

        Moves the selected media data object to the bottom of the
        downloads.DownloadList, so it is assigned to the last available worker.

        Args:

            menu_item (Gtk.MenuItem): The menu item that was clicked

            download_item_obj (downloads.DownloadItem): The download item
                object for the selected media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21276 on_progress_list_dl_last')

        # Check that, since the popup menu was created, the media data object
        #   hasn't been assigned a worker
        for this_worker_obj in self.app_obj.download_manager_obj.worker_list:
            if this_worker_obj.running_flag \
            and this_worker_obj.download_item_obj == download_item_obj \
            and this_worker_obj.downloader_obj is not None:
                return

        # Assign this media data object to the last available worker
        download_list_obj = self.app_obj.download_manager_obj.download_list_obj
        download_list_obj.move_item_to_bottom(download_item_obj)

        # Change the row's icon to show that it will be checked/downloaded
        #   last
        # (Because of the way the Progress List has been set up, borrowing from
        #   the design in youtube-dl-gui, reordering the rows in the list is
        #   not practial)
        tree_path = Gtk.TreePath(
            self.progress_list_row_dict[download_item_obj.item_id],
        )

        self.progress_list_liststore.set(
            self.progress_list_liststore.get_iter(tree_path),
            3,
            self.pixbuf_dict['arrow_down_small'],
        )


    def on_progress_list_dl_next(self, menu_item, download_item_obj):

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

        Moves the selected media data object to the top of the
        downloads.DownloadList, so it is assigned to the next available worker.

        Args:

            menu_item (Gtk.MenuItem): The menu item that was clicked

            download_item_obj (downloads.DownloadItem): The download item
                object for the selected media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21323 on_progress_list_dl_next')

        # Check that, since the popup menu was created, the media data object
        #   hasn't been assigned a worker
        for this_worker_obj in self.app_obj.download_manager_obj.worker_list:
            if this_worker_obj.running_flag \
            and this_worker_obj.download_item_obj == download_item_obj \
            and this_worker_obj.downloader_obj is not None:
                return

        # Assign this media data object to the next available worker
        download_list_obj = self.app_obj.download_manager_obj.download_list_obj
        download_list_obj.move_item_to_top(download_item_obj)

        # Change the row's icon to show that it will be checked/downloaded
        #   next
        tree_path = Gtk.TreePath(
            self.progress_list_row_dict[download_item_obj.item_id],
        )

        self.progress_list_liststore.set(
            self.progress_list_liststore.get_iter(tree_path),
            3,
            self.pixbuf_dict['arrow_up_small'],
        )


    def on_progress_list_right_click(self, treeview, event):

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

        When the user right-clicks an item in the Progress List, create a
        context-sensitive popup menu.

        Args:

            treeview (Gtk.TreeView): The Progress List's treeview

            event (Gdk.EventButton): The event emitting the Gtk signal

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21366 on_progress_list_right_click')

        if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3:

            # If the user right-clicked on empty space, the call to
            #   .get_path_at_pos returns None (or an empty list)
            if not treeview.get_path_at_pos(
                int(event.x),
                int(event.y),
            ):
                return

            path, column, cellx, celly = treeview.get_path_at_pos(
                int(event.x),
                int(event.y),
            )

            tree_iter = self.progress_list_liststore.get_iter(path)
            if tree_iter is not None:
                self.progress_list_popup_menu(
                    event,
                    self.progress_list_liststore[tree_iter][0],
                    self.progress_list_liststore[tree_iter][1],
                )


    def on_progress_list_stop_all_soon(self, menu_item):

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

        Halts checking/downloading the selected media data object, after the
        current video check/download has finished.

        During the checking stage of a custom download (operation types
        'custom_sim' and 'classic_sim'), skips the remaining videos, and
        proceeds directly to the download stage.

        Args:

            menu_item (Gtk.MenuItem): The menu item that was clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21410 on_progress_list_stop_all_soon')

        # Check that, since the popup menu was created, the download operation
        #   hasn't finished
        if not self.app_obj.download_manager_obj:
            # Do nothing
            return

        # Tell the download manager to continue downloading the current videos
        #   (if any), and then stop
        self.app_obj.download_manager_obj.stop_download_operation_soon()


    def on_progress_list_stop_now(self, menu_item, download_item_obj,
    worker_obj, downloader_obj):

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

        Halts checking/downloading the selected media data object.

        Args:

            menu_item (Gtk.MenuItem): The menu item that was clicked

            download_item_obj (downloads.DownloadItem): The download item
                object for the selected media data object

            worker_obj (downloads.DownloadWorker): The worker currently
                handling checking/downloading this media data object

            downloader_obj (downloads.VideoDownloader or
                downloads.StreamDownloader): The downloader handling checking/
                downloading this media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21447 on_progress_list_stop_now')

        # Check that, since the popup menu was created, the video downloader
        #   hasn't already finished checking/downloading the selected media
        #   data object
        if not self.app_obj.download_manager_obj \
        or not worker_obj.running_flag \
        or worker_obj.download_item_obj != download_item_obj \
        or worker_obj.downloader_obj is None:
            # Do nothing
            return

        # Stop the video downloader (causing the worker to be assigned a new
        #   downloads.DownloadItem, if there are any left)
        downloader_obj.stop()


    def on_progress_list_stop_soon(self, menu_item, download_item_obj,
    worker_obj, downloader_obj):

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

        Halts checking/downloading the selected media data object, after the
        current video check/download has finished.

        Args:

            menu_item (Gtk.MenuItem): The menu item that was clicked

            download_item_obj (downloads.DownloadItem): The download item
                object for the selected media data object

            worker_obj (downloads.DownloadWorker or None): The worker currently
                handling checking/downloading this media data object (None if
                the check/download hasn't started yet)

            downloader_obj (downloads.VideoDownloader,
                downloads.StreamDownloader or None): The downloader handling
                checking/downloading this media data object (None if the check/
                download hasn't started yet)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21491 on_progress_list_stop_soon')

        download_manager_obj = self.app_obj.download_manager_obj

        # Check that, since the popup menu was created, the video downloader
        #   hasn't already finished checking/downloading the selected media
        #   data object
        if not download_manager_obj or (
            worker_obj and (
                not worker_obj.running_flag \
                or worker_obj.download_item_obj != download_item_obj \
                or worker_obj.downloader_obj is None
            )
        ):
            # Do nothing
            return

        if downloader_obj:

            # Media data object is checking/downloading now. Tell the video
            #   downloader to stop after the check/download has finished
            # (If other workers are waiting to start, then they will)
            downloader_obj.stop_soon()

        else:

            # Media data object is waiting to start checking/downloading
            # Tell the download list to empty itself one the check/download
            #   starts, so that no more checks/downloads start after that
            download_manager_obj.download_list_obj.set_final_item(
                download_item_obj.item_id
            )


    def on_progress_list_watch_hooktube(self, menu_item, media_data_obj):

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

        Opens the clicked video, which is a YouTube video, on the HookTube
        website.

        Args:

            menu_item (Gtk.MenuItem): The menu item that was clicked

            media_data_obj (media.Video): The corresponding media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21541 on_progress_list_watch_hooktube')

        if isinstance(media_data_obj, media.Video):

            # Launch the video
            utils.open_file(
                self.app_obj,
                utils.convert_youtube_to_hooktube(media_data_obj.source),
            )

            # Mark the video as not new (having been watched)
            if media_data_obj.new_flag:
                self.app_obj.mark_video_new(media_data_obj, False)
            # Remove the video from the waiting list (having been watched)
            if media_data_obj.waiting_flag:
                self.app_obj.mark_video_waiting(media_data_obj, False)


    def on_progress_list_watch_invidious(self, menu_item, media_data_obj):

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

        Opens the clicked video, which is a YouTube video, on the Invidious
        website.

        Args:

            menu_item (Gtk.MenuItem): The menu item that was clicked

            media_data_obj (media.Video): The corresponding media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21575 on_progress_list_watch_invidious')

        if isinstance(media_data_obj, media.Video):

            # Launch the video
            utils.open_file(
                self.app_obj,
                utils.convert_youtube_to_invidious(
                    self.app_obj,
                    media_data_obj.source,
                ),
            )

            # Mark the video as not new (having been watched)
            if media_data_obj.new_flag:
                self.app_obj.mark_video_new(media_data_obj, False)
            # Remove the video from the waiting list (having been watched)
            if media_data_obj.waiting_flag:
                self.app_obj.mark_video_waiting(media_data_obj, False)


    def on_progress_list_watch_website(self, menu_item, media_data_obj):

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

        Opens the clicked video's source URL in a web browser.

        Args:

            menu_item (Gtk.MenuItem): The menu item that was clicked

            media_data_obj (media.Video): The corresponding media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21611 on_progress_list_watch_website')

        if isinstance(media_data_obj, media.Video) \
        and media_data_obj.source:

            utils.open_file(self.app_obj, media_data_obj.source)


    def on_results_list_delete_video(self, menu_item, media_data_obj, path):

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

        Deletes the video, and removes a row from the Results List.

        Args:

            menu_item (Gtk.MenuItem): The menu item that was clicked

            media_data_obj (media.Video): The video displayed on the clicked
                row

            path (Gtk.TreePath): Path to the clicked row in the treeview

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21637 on_results_list_delete_video')

        # Delete the video
        self.app_obj.delete_video(media_data_obj, True)

        # Remove the row from the Results List
        tree_iter = self.results_list_liststore.get_iter(path)
        self.results_list_liststore.remove(tree_iter)


    def on_results_list_drag_data_get(self, treeview, drag_context, data, info,
    time):

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

        Set the data to be used when the user drags and drops rows from the
        Results List to an external application (for example, an FFmpeg batch
        converter).

        Args:

            treeview (Gtk.TreeView): The Results List treeview

            drag_context (GdkX11.X11DragContext): Data from the drag procedure

            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

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21672 on_results_list_drag_data_get')

        # Get the selected media.Video object(s)
        video_list = self.get_selected_videos_in_treeview(
            self.results_list_treeview,
            0,      # Column 0 contains the media.Video's .dbid
        )

        # Transfer to the external application a single string, containing one
        #   or more full file paths/URLs/video names, separated by newline
        #   characters
        # If the path/URL/name isn't known for any videos, then an empty line
        #   is transferred
        if info == 0:   # TARGET_ENTRY_TEXT
            data.set_text(self.get_video_drag_data(video_list), -1)


    def on_results_list_right_click(self, treeview, event):

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

        When the user right-clicks item(s) in the Results List, create a
        context-sensitive popup menu.

        Args:

            treeview (Gtk.TreeView): The Results List's treeview

            event (Gdk.EventButton): The event emitting the Gtk signal

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21705 on_results_list_right_click')

        if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3:

            # If the user right-clicked on empty space, the call to
            #   .get_path_at_pos returns None (or an empty list)
            if not treeview.get_path_at_pos(
                int(event.x),
                int(event.y),
            ):
                return

            path, column, cellx, celly = treeview.get_path_at_pos(
                int(event.x),
                int(event.y),
            )

            tree_iter = self.results_list_liststore.get_iter(path)
            if tree_iter is not None:
                self.results_list_popup_menu(event, path)


    def on_errors_list_clear(self, button):

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

        In the Errors/Warnings tab, when the user clicks the 'Clear list'
        button, clear the Errors List.

        Args:

            button (Gtk.Button): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21741 on_errors_list_clear')

        self.error_list_buffer_list = []
        self.errors_list_reset()


    def on_errors_list_copy(self, button):

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

        In the Errors/Warnings tab, when the user clicks the 'Copy to
        clipboard' button, copy all selected lines to the clipboard.

        Args:

            button (Gtk.Button): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21761 on_errors_list_copy')

        # Based on code from self.on_errors_list_drag_data_get()
        # For each selected line, retrieve values from the three hidden columns
        text = ''

        selection = self.errors_list_treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        for tree_path in path_list:

            tree_iter = model.get_iter(tree_path)
            if tree_iter:

                file_path = model[tree_iter][0]
                source = model[tree_iter][1]
                name = model[tree_iter][2]
                msg = model[tree_iter][9]

                # If the path, source and name are all empty strings, then it
                #   probably wasn't a media data object that generated the
                #   message on this line
                if file_path !=  '' or source != '' or name != '':

                    if self.app_obj.drag_error_separator_flag:
                        text += self.drag_drop_separator + '\n'

                    if self.app_obj.drag_error_path_flag:
                        if file_path == '':
                            text += '(' + _('unknown path') + ')\n'
                        else:
                            text += file_path + '\n'

                    if self.app_obj.drag_error_source_flag:
                        if source  == '':
                            text += '(' + _('unknown URL') + ')\n'
                        else:
                            text += source + '\n'

                    if self.app_obj.drag_error_name_flag:
                        if name == '':
                            text += self.app_obj.default_video_name + '\n'
                        else:
                            text += name + '\n'

                    if self.app_obj.drag_error_msg_flag:

                        # Strip newline characters; we want the whole message
                        #    on a single line, on this occasion
                        # (name == '' should be impossible, but for
                        #   completeness, we'll check it anyway)
                        if name == '':
                            text += '(' + _('unknown message') + ')\n'
                        else:
                            text += re.sub(r'\n+', ' ', msg) + '\n'

        # Copy the string to the clipboard
        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        clipboard.set_text(text, -1)


    def on_errors_list_drag_data_get(self, treeview, drag_context, data, info,
    time):

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

        Set the data to be used when the user drags and drops rows from the
        Errors List to an external application (for example, an FFmpeg batch
        converter).

        Args:

            treeview (Gtk.TreeView): The Errors List treeview

            drag_context (GdkX11.X11DragContext): Data from the drag procedure

            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

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21846 on_errors_list_drag_data_get')

        # For each selected line, retrieve values from the three hidden columns
        text = ''

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        for tree_path in path_list:

            tree_iter = model.get_iter(tree_path)
            if tree_iter:

                file_path = model[tree_iter][0]
                source = model[tree_iter][1]
                name = model[tree_iter][2]
                msg = model[tree_iter][9]

                # If the path, source and name are all empty strings, then it
                #   probably wasn't a media data object that generated the
                #   message on this line
                if file_path !=  '' or source != '' or name != '':

                    if self.app_obj.drag_error_separator_flag:
                        text += self.drag_drop_separator + '\n'

                    if self.app_obj.drag_error_path_flag:
                        if file_path == '':
                            text += '(' + _('unknown path') + ')\n'
                        else:
                            text += file_path + '\n'

                    if self.app_obj.drag_error_source_flag:
                        if source  == '':
                            text += '(' + _('unknown URL') + ')\n'
                        else:
                            text += source + '\n'

                    if self.app_obj.drag_error_name_flag:
                        if name == '':
                            text += self.app_obj.default_video_name + '\n'
                        else:
                            text += name + '\n'

                    if self.app_obj.drag_error_msg_flag:

                        # Strip newline characters; we want the whole message
                        #    on a single line, on this occasion
                        # (name == '' should be impossible, but for
                        #   completeness, we'll check it anyway)
                        if name == '':
                            text += '(' + _('unknown message') + ')\n'
                        else:
                            text += re.sub(r'\n+', ' ', msg) + '\n'

        # Transfer to the external application a single string, containing one
        #   or more full file paths/URLs/video names, separated by newline
        #   characters
        if info == 0:   # TARGET_ENTRY_TEXT
            data.set_text(text, -1)


    def on_classic_convert_combo_changed(self, combo):

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

        Update IVs.

        Args:

            combo (Gtk.ComboBox): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21920 on_classic_convert_combo_changed')

        tree_iter = self.classic_convert_combo.get_active_iter()
        model = self.classic_convert_combo.get_model()
        text = utils.strip_whitespace(model[tree_iter][0])

        # (Update the IV)
        if text == _('Convert to this format'):
            self.app_obj.set_classic_format_convert_flag(True)
        else:
            self.app_obj.set_classic_format_convert_flag(False)

        # (Update the banner at the top of the tab, according to current
        #   conditions)
        self.update_classic_mode_tab_update_banner()


    def on_classic_dest_dir_combo_changed(self, combo):

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

        In the combobox displaying destination directories, remember the most
        recent directory specified by the user, so it can be restored when
        Tartube restarts.

        Args:

            combo (Gtk.ComboBox): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21952 on_classic_dest_dir_combo_changed')

        tree_iter = self.classic_dest_dir_combo.get_active_iter()
        model = self.classic_dest_dir_combo.get_model()
        self.app_obj.set_classic_dir_previous(model[tree_iter][0])


    def on_classic_format_combo_changed(self, combo):

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

        In the combobox displaying video/audio formats, if the user selects the
        'Default' item, desensitise the second radio button.

        If the user selects the 'Video:' or 'Audio:' item, select the line
        immediately below that (which should be a valid format).

        Update IVs.

        Args:

            combo (Gtk.ComboBox): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 21978 on_classic_format_combo_changed')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Extra items for the \'Format\' drop-down' \
            + ' box in the Classic Mode tab'
        )

        tree_iter = self.classic_format_combo.get_active_iter()
        model = self.classic_format_combo.get_model()
        text = model[tree_iter][0]

        # (Dummy items in the combo)
        default_item = _('Default') + '     '
        video_item = _('Video:')
        audio_item = _('Audio:')

        if text == default_item:
            self.classic_convert_combo.set_active(0)
            self.classic_convert_combo.set_sensitive(False)

        else:
            self.classic_convert_combo.set_sensitive(True)

            if text == video_item or text == audio_item:
                self.classic_format_combo.set_active(
                    self.classic_format_combo.get_active() + 1,
                )

        # (Update the IV)
        tree_iter = self.classic_format_combo.get_active_iter()
        text = model[tree_iter][0]
        # (Should only be possible to set the first of thse items, but we'll
        #   check anyway)
        if text != default_item and text != video_item and text != audio_item:
            # (Ignore the first two space characters)
            self.app_obj.set_classic_format_selection(text[2:])
        else:
            self.app_obj.set_classic_format_selection(None)

        # (If an audio format has been selected, then the resolution combo
        #   must be reset)
        if self.app_obj.classic_format_selection is not None \
        and self.app_obj.classic_format_selection in formats.AUDIO_FORMAT_DICT:
            self.classic_resolution_combo.set_active(0)

        # (Update the banner at the top of the tab, according to current
        #   conditions)
        self.update_classic_mode_tab_update_banner()


    def on_classic_livestream_checkbutton_toggled(self, checkbutton):

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

        Updates IVs.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 22041 on_classic_livestream_checkbutton_toggled',
            )

        self.app_obj.set_classic_livestream_flag(checkbutton.get_active())


    def on_classic_menu_custom_dl_prefs(self, menu_item):

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

        Opens the system preferences window, at the tab for custom download
        preferences.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22062 on_classic_menu_custom_dl_prefs')

        if self.app_obj.current_manager_obj:

            return self.app_obj.system_error(
                260,
                'Callback request denied due to current conditions',
            )

        # Open the system preferences window
        config.SystemPrefWin(self.app_obj, 'custom_dl')


    def on_classic_menu_edit_options(self, menu_item):

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

        Opens an edit window for the options.OptionsManager object currently
        selected for use in the Classic Mode tab.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22089 on_classic_menu_edit_options')

        if self.app_obj.current_manager_obj:

            return self.app_obj.system_error(
                261,
                'Callback request denied due to current conditions',
            )

        # Open an edit window
        if self.app_obj.classic_options_obj is None:

            config.OptionsEditWin(
                self.app_obj,
                self.app_obj.general_options_obj,
            )

        else:

            config.OptionsEditWin(
                self.app_obj,
                self.app_obj.classic_options_obj,
            )


    def on_classic_menu_set_options(self, menu_item):

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

        Sets the options.OptionsManager object for use in Classic Mode tab.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22127 on_classic_menu_set_options')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                262,
                'Callback request denied due to current conditions',
            )

        # Open the preferences window, at the tab showing a lot of download
        #   options. The user can choose one there
        config.SystemPrefWin(self.app_obj, 'options')


    def on_classic_menu_use_general_options(self, menu_item):

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

        Uses the General Options Manager in the Classic Mode tab, instead of
        the other options.OptionsManager object currently set.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22154 on_classic_menu_use_general_options')

        if self.app_obj.current_manager_obj:

            return self.app_obj.system_error(
                263,
                'Callback request denied due to current conditions',
            )

        self.app_obj.remove_classic_download_options()


    def on_classic_menu_toggle_auto_copy(self, menu_item):

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

        Toggles the auto copy/paste button in the Classic Mode tab.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22179 on_classic_menu_toggle_auto_copy')

        if not self.classic_auto_copy_flag:

            # Update IVs
            self.classic_auto_copy_flag = True

            # Start a timer to periodically check the clipboard
            self.classic_clipboard_timer_id = GObject.timeout_add(
                self.classic_clipboard_timer_time,
                self.classic_mode_tab_timer_callback,
            )

        else:

            # Update IVs
            self.classic_auto_copy_flag = False

            # Stop the timer
            GObject.source_remove(self.classic_clipboard_timer_id)
            self.classic_clipboard_timer_id = None


    def on_classic_menu_toggle_custom_dl(self, menu_item):

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

        Toggles custom downloads in the Classic Mode tab.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22215 on_classic_menu_toggle_custom_dl')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Extra items for the buttons at the bottom' \
            + ' of the Classic Mode tab'
        )

        if self.app_obj.current_manager_obj:

            return self.app_obj.system_error(
                264,
                'Callback request denied due to current conditions',
            )

        if not self.app_obj.classic_custom_dl_flag:
            self.app_obj.set_classic_custom_dl_flag(True)
            self.classic_download_button.set_label(
                '     ' + _('Custom download all') + '     ',
            )
            self.classic_download_button.set_tooltip_text(
                _('Perform a custom download on the URLs above'),
            )

        else:
            self.app_obj.set_classic_custom_dl_flag(False)
            self.classic_download_button.set_label(
                '     ' + _('Download all') + '     ',
            )
            self.classic_download_button.set_tooltip_text(
                _('Download the URLs above'),
            )


    def on_classic_menu_toggle_one_click_dl(self, menu_item):

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

        Toggles the one-click download button in the Classic Mode tab.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22261 on_classic_menu_toggle_one_click_dl')

        # Update IVs
        if not self.classic_one_click_dl_flag:
            self.classic_one_click_dl_flag = True
        else:
            self.classic_one_click_dl_flag = False


    def on_classic_menu_toggle_remember_urls(self, menu_item):

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

        Toggles the setting to remember undownloaded URLs, when the config file
        is saved.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22284 on_classic_menu_toggle_remember_urls')

        self.app_obj.toggle_classic_pending_flag()


    def on_classic_menu_update_ytdl(self, menu_item):

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

        Starts an update operation.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22302 on_classic_menu_update_ytdl')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                265,
                'Callback request denied due to current conditions',
            )

        # Start the update operation
        self.app_obj.update_manager_start('ytdl')


    def on_classic_progress_list_drag_data_get(self, treeview, drag_context,
    data, info, time):

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

        Set the data to be used when the user drags and drops rows from the
        Classic Progress List to an external application (for example, an
        FFmpeg batch converter).

        Args:

            treeview (Gtk.TreeView): The Classic Progress List treeview

            drag_context (GdkX11.X11DragContext): Data from the drag procedure

            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

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 22339 on_classic_progress_list_drag_data_get',
            )

        # Get the selected dummy media.Video object(s)
        video_list = self.get_selected_videos_in_classic_treeview()

        # Transfer to the external application a single string, containing one
        #   or more full file paths/URLs/video names, separated by newline
        #   characters
        # If the path/URL/name isn't known for any videos, then an empty line
        #   is transferred
        if info == 0 and video_list:   # TARGET_ENTRY_TEXT

            data.set_text(
                self.get_video_drag_data(
                    video_list,
                    True,       # This is a dummy media.Video object
                ),
                -1,
            )


    def on_classic_progress_list_from_popup(self, menu_item, menu_item_type, \
    video_list):

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

        In the popup menu, some items duplicate the buttons at the bottom of
        the tab. When items in the menu are selected, re-direct the request to
        the code used by the buttons.

        Args:

            menu_item (Gtk.MenuItem): The menu item that was clicked

            menu_item_type (str): Identifies the menu item clicked, one of the
                strings 'play', 'open', 'redownload', 'stop', 'clips',
                'ffmpeg', 'move_up', 'move_down', 'remove'

            video_list (list): List of media.Video objects to which the action
                will apply

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22385 on_classic_progress_list_from_popup')

        # Re-direct the request to the button callbacks, supplying dummy
        #   action/par arguments (which are ignored anyway)
        if menu_item_type == 'play':
            self.app_obj.on_button_classic_play(None, None)
        elif menu_item_type == 'open':
            self.app_obj.on_button_classic_open(None, None)
        elif menu_item_type == 'redownload':
            self.app_obj.on_button_classic_redownload(None, None)
        elif menu_item_type == 'stop':
            self.app_obj.on_button_classic_stop(None, None)
        elif menu_item_type == 'clips':
            self.app_obj.on_button_classic_clips(None, None)
        elif menu_item_type == 'ffmpeg':
            self.app_obj.on_button_classic_ffmpeg(None, None)
        elif menu_item_type == 'move_up':
            self.app_obj.on_button_classic_move_up(None, None)
        elif menu_item_type == 'move_down':
            self.app_obj.on_button_classic_move_down(None, None)
        elif menu_item_type == 'remove':
            self.app_obj.on_button_classic_remove(None, None)


    def on_classic_progress_list_get_cmd(self, menu_item, dummy_obj):

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

        Copies the youtube-dl system command for the specified dummy
        media.Video object to the clipboard.

        Args:

            menu_item (Gtk.MenuItem): The menu item that was clicked

            media_data_obj (media.Video): The dummy media.Video object on the
                clicked row

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22426 on_classic_progress_list_get_cmd')

        # Generate the list of download options for the dummy media.Video
        #   object
        options_parser_obj = options.OptionsParser(self.app_obj)
        options_list = options_parser_obj.parse(
            dummy_obj,
            self.app_obj.general_options_obj,
            'classic',
        )

        # Obtain the system command used to download this media data object
        cmd_list = utils.generate_ytdl_system_cmd(
            self.app_obj,
            dummy_obj,
            options_list,
            False,
            True,                                   # Classic Mode tab
        )

        # Copy it to the clipboard
        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        clipboard.set_text(utils.prepare_system_cmd_for_display(cmd_list), -1)


    def on_classic_progress_list_get_path(self, menu_item, dummy_obj):

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

        Copies the full file path for the specified dummy media.Video object to
        the clipboard.

        Args:

            menu_item (Gtk.MenuItem): The menu item that was clicked

            media_data_obj (media.Video): The dummy media.Video object on the
                clicked row

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22468 on_classic_progress_list_get_path')

        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        clipboard.set_text(dummy_obj.dummy_path, -1)


    def on_classic_progress_list_get_url(self, menu_item, dummy_obj):

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

        Copies the URL for the specified dummy media.Video object to the
        clipboard.

        Args:

            menu_item (Gtk.MenuItem): The menu item that was clicked

            media_data_obj (media.Video): The dummy media.Video object on the
                clicked row

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22491 on_classic_progress_list_get_url')

        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        clipboard.set_text(dummy_obj.source, -1)


    def on_classic_progress_list_reinsert_url(self, menu_item, dummy_list):

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

        Re-inserts the selected rows' URLs into the list at the top of the
        Classic Mode tab, then removes the selected rows from the Classic
        Progress List and updates IVs.

        Based on code in self.classic_mode_tab_remove_rows().

        Args:

            menu_item (Gtk.MenuItem): The menu item that was clicked

            dummy_list (list): List of dummy media.Video objects on the
                clicked row(s)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22517 on_classic_progress_list_reinsert_url')

        # (Import IVs for convenience)
        manager_obj = self.app_obj.download_manager_obj

        # Check each row in turn
        source_list = []
        for dummy_video_obj in dummy_list:

            # If there is a current download operation, we need to update it
            if manager_obj:

                # If this dummy media.Video object is the one being downloaded,
                #   halt the download
                for worker_obj in manager_obj.worker_list:

                    if worker_obj.running_flag \
                    and worker_obj.download_item_obj \
                    and worker_obj.download_item_obj.media_data_obj.dbid \
                    == dummy_video_obj.dbid:
                        worker_obj.downloader_obj.stop()

            # Delete the dummy media.Video object
            del self.classic_media_dict[dummy_video_obj.dbid]

            # Remove the row from the treeview
            row_iter = self.classic_mode_tab_find_row_iter(
                dummy_video_obj.dbid,
            )

            if row_iter:
                self.classic_progress_liststore.remove(row_iter)

            # Prepare to re-insert the URL into the list at the top of the tab
            if dummy_video_obj.source is not None:
                source_list.append(dummy_video_obj.source)

        # Re-insert the URLS
        if source_list:
            utils.add_links_to_textview(
                self.app_obj,
                source_list,
                self.classic_textbuffer,
                self.classic_mark_start,
                self.classic_mark_end,
            )


    def on_classic_progress_list_right_click(self, treeview, event):

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

        When the user right-clicks an item in the Classic Progress List, opens
        a context-sensitive popup menu.

        Args:

            treeview (Gtk.TreeView): The Results List's treeview

            event (Gdk.EventButton): The event emitting the Gtk signal

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22581 on_classic_progress_list_right_click')

        if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3:

            # If the user right-clicked on empty space, the call to
            #   .get_path_at_pos returns None (or an empty list)
            if not treeview.get_path_at_pos(
                int(event.x),
                int(event.y),
            ):
                return

            path, column, cellx, celly = treeview.get_path_at_pos(
                int(event.x),
                int(event.y),
            )

            tree_iter = self.classic_progress_liststore.get_iter(path)
            if tree_iter is not None:
                self.classic_progress_list_popup_menu(event, path)


    def on_classic_resolution_combo_changed(self, combo):

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

        Update IVs.

        Args:

            combo (Gtk.ComboBox): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22616 on_classic_resolution_combo_changed')

        tree_iter = self.classic_resolution_combo.get_active_iter()
        model = self.classic_resolution_combo.get_model()
        text = utils.strip_whitespace(model[tree_iter][0])

        # (Dummy items in the combo)
        ignore_me = _(
            'TRANSLATOR\'S NOTE: Highest video resolution'
        )

        default_item = _('Highest')

        # (Update the IV)
        if text != default_item:
            self.app_obj.set_classic_resolution_selection(text)
        else:
            self.app_obj.set_classic_resolution_selection(None)


    def on_classic_sblock_checkbutton_toggled(self, checkbutton):

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

        Updates IVs.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22649 on_classic_sblock_checkbutton_toggled')

        self.app_obj.set_classic_sblock_flag(checkbutton.get_active())


    def on_classic_textbuffer_changed(self, textbuffer):

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

        If the setting is enabled, start a download operation for any valid
        URL(s), or add the URL(s) to an existing download operation.

        Args:

            textbuffer (Gtk.TextBuffer): The textbuffer for the modified
                Gtk.TextView

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22669 on_classic_textbuffer_changed')

        if self.classic_one_click_dl_flag \
        and not self.classic_auto_copy_check_flag:

            # (A second signal is received by this function, when the call to
            #   self.classic_mode_tab_add_urls() resets the textview. Setting
            #   this flag prevents a second call to that function, before the
            #   first one has finished)
            self.classic_auto_copy_check_flag = True
            url_list = self.classic_mode_tab_add_urls()
            self.classic_auto_copy_check_flag = False

            if url_list and not self.app_obj.download_manager_obj:
                self.classic_mode_tab_start_download()


    def on_classic_textview_paste(self, textview):

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

        When the user copy-pastes URLs into the textview, insert an initial
        newline character, so they don't have to continuously do that
        themselves.

        Args:

            textview (Gtk.TextView): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22701 on_classic_textview_paste')

        # (Don't bother, if the URLs are going to be downloaded immediately)
        if not self.classic_one_click_dl_flag:

            text = self.classic_textbuffer.get_text(
                self.classic_textbuffer.get_iter_at_mark(
                    self.classic_mark_start,
                ),
                self.classic_textbuffer.get_iter_at_mark(
                    self.classic_mark_end,
                ),
                # Don't include hidden characters
                False,
            )

            # (Don't bother inserting the newline if the URLs are going to be
            #   sent straight to the download manager)
            if not (re.search(r'^\S*$', text)) \
            and not (re.search(r'\n+\s*$', text)):
                self.classic_textbuffer.set_text(text + '\n')


    def on_bandwidth_spinbutton_changed(self, spinbutton):

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

        In the Progress tab, when the user sets the bandwidth limit, inform
        mainapp.TartubeApp. The new setting is applied to the next download
        job.

        Args:

            spinbutton (Gtk.SpinButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22739 on_bandwidth_spinbutton_changed')

        if self.bandwidth_checkbutton.get_active():
            self.app_obj.set_bandwidth_default(
                int(self.bandwidth_spinbutton.get_value())
            )


    def on_bandwidth_checkbutton_changed(self, checkbutton):

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

        In the Progress tab, when the user turns the bandwidth limit on/off,
        inform mainapp.TartubeApp. The new setting is applied to the next
        download job.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22762 on_bandwidth_checkbutton_changed')

        if self.bandwidth_checkbutton.get_active():

            self.app_obj.set_bandwidth_apply_flag(True)
            self.app_obj.set_bandwidth_default(
                int(self.bandwidth_spinbutton.get_value())
            )

        else:

            self.app_obj.set_bandwidth_apply_flag(False)


    def on_custom_dl_menu_select(self, menu_item, media_data_list, uid):

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

        Starts a custom download using the specified custom download manager.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            media_data_list (list): List of media.Video, media.Channel,
                media.Playlist and media.Folder objects to download. If an
                empty list, all media data objects are custom downloaded

            uid (int): Unique .uid of the downloads.CustomDLManager to use

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22795 on_custom_dl_menu_select')

        custom_dl_obj = self.app_obj.custom_dl_reg_dict[uid]

        if not custom_dl_obj.dl_by_video_flag \
        or not custom_dl_obj.dl_precede_flag:

            self.app_obj.download_manager_start(
                'custom_real',
                False,          # Not called by the slow timer
                media_data_list,
                custom_dl_obj,
            )

        else:

            self.app_obj.download_manager_start(
                'custom_sim',
                False,          # Not called by the slow timer
                media_data_list,
                custom_dl_obj,
            )


    def on_delete_event(self, widget, event):

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

        If the user click-closes the window, close to the system tray (if
        required), rather than closing the application.

        Args:

            widget (mainwin.MainWin): The main window

            event (Gdk.Event): Ignored

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22835 on_delete_event')

        if self.app_obj.status_icon_obj \
        and self.app_obj.show_status_icon_flag \
        and self.app_obj.close_to_tray_flag \
        and self.is_visible():

            # Close to the system tray
            self.toggle_visibility()
            return True

        else:

            # mainapp.TartubeApp.stop_continue() is not called, so let's save
            #   the config/database file right now
            if not self.app_obj.disable_load_save_flag:
                self.app_obj.save_config()
                self.app_obj.save_db()

            # Allow the application to close as normal
            return False


    def on_delete_profile_menu_select(self, menu_item, profile_name):

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

        Deletes the specified profile

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            profile_name (str): The specified profile (a key in
                mainapp.TartubeApp.profile_dict).

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22874 on_delete_profile_menu_select')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Dialogue window, generated by main window' \
            + ' menu, Media > Profiles > Delete profile'
        )

        # Prompt for confirmation, before deleting
        self.app_obj.dialogue_manager_obj.show_msg_dialogue(
            _(
                'Are you sure you want to delete the profile \'{0}\'',
            ).format(profile_name),
            'question',
            'yes-no',
            None,                   # Parent window is main window
            {
                'yes': 'delete_profile',
                # Specified options
                'data': profile_name,
            },
        )


    def on_draw_blocked_checkbutton_changed(self, checkbutton):

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

        In the Videos tab, when the user toggles the checkbutton, enable/
        disable drawing blocked videos.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22911 on_draw_blocked_checkbutton_changed')

        self.app_obj.set_catalogue_draw_blocked_flag(checkbutton.get_active())
        # Redraw the Video Catalogue
        self.video_catalogue_redraw_all(
            self.video_index_current_dbid,
            1,
            True,           # Reset scrollbars
            True,           # Don't cancel the filter, if applied
        )


    def on_draw_downloaded_checkbutton_changed(self, checkbutton):

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

        In the Videos tab, when the user toggles the checkbutton, enable/
        disable drawing downloaded videos.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 22937 on_draw_downloaded_checkbutton_changed',
            )

        self.app_obj.set_catalogue_draw_downloaded_flag(
            checkbutton.get_active(),
        )
        # Redraw the Video Catalogue
        self.video_catalogue_redraw_all(
            self.video_index_current_dbid,
            1,
            True,           # Reset scrollbars
            True,           # Don't cancel the filter, if applied
        )


    def on_draw_frame_checkbutton_changed(self, checkbutton):

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

        In the Videos tab, when the user toggles the checkbutton, enable/
        disable the visible frame around each video.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 22967 on_draw_frame_checkbutton_changed')

        self.app_obj.set_catalogue_draw_frame_flag(checkbutton.get_active())
        # (No need to redraw the Video Catalogue, just to enable/disable the
        #   visible frame around each video)
        if self.app_obj.catalogue_mode_type == 'complex':

            for catalogue_obj in self.video_catalogue_dict.values():
                catalogue_obj.enable_visible_frame(
                    self.app_obj.catalogue_draw_frame_flag,
                )

        elif self.app_obj.catalogue_mode_type == 'grid':

            for catalogue_obj in self.video_catalogue_dict.values():
                catalogue_obj.catalogue_gridbox.enable_visible_frame(
                    self.app_obj.catalogue_draw_frame_flag,
                )


    def on_draw_icons_checkbutton_changed(self, checkbutton):

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

        In the Videos tab, when the user toggles the checkbutton, enable/
        disable drawing the status icons for each video.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23001 on_draw_icons_checkbutton_changed')

        self.app_obj.set_catalogue_draw_icons_flag(checkbutton.get_active())
        # (No need to redraw the Video Catalogue, just to make the status icons
        #   visible/invisible)
        if self.app_obj.catalogue_mode_type != 'simple':
            for catalogue_obj in self.video_catalogue_dict.values():
                catalogue_obj.update_status_images()


    def on_draw_undownloaded_checkbutton_changed(self, checkbutton):

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

        In the Videos tab, when the user toggles the checkbutton, enable/
        disable drawing undownloaded videos.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 23025 on_draw_undownloaded_checkbutton_changed',
            )

        self.app_obj.set_catalogue_draw_undownloaded_flag(
            checkbutton.get_active(),
        )
        # Redraw the Video Catalogue
        self.video_catalogue_redraw_all(
            self.video_index_current_dbid,
            1,
            True,           # Reset scrollbars
            True,           # Don't cancel the filter, if applied
        )


    def on_filter_comment_checkbutton_changed(self, checkbutton):

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

        In the Videos tab, when the user toggles the checkbutton, enable/
        disable filtering by video comments.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23055 on_filter_comment_checkbutton_changed')

        self.app_obj.set_catalogue_filter_comment_flag(
            checkbutton.get_active(),
        )
        # (No need to redraw the Video Catalogue, just to make the status icons
        #   visible/invisible
        if self.app_obj.catalogue_mode_type != 'simple':
            for catalogue_obj in self.video_catalogue_dict.values():
                catalogue_obj.update_status_images()


    def on_filter_descrip_checkbutton_changed(self, checkbutton):

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

        In the Videos tab, when the user toggles the checkbutton, enable/
        disable filtering by video description.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23081 on_filter_descrip_checkbutton_changed')

        self.app_obj.set_catalogue_filter_descrip_flag(
            checkbutton.get_active(),
        )
        # (No need to redraw the Video Catalogue, just to make the status icons
        #   visible/invisible
        if self.app_obj.catalogue_mode_type != 'simple':
            for catalogue_obj in self.video_catalogue_dict.values():
                catalogue_obj.update_status_images()


    def on_filter_name_checkbutton_changed(self, checkbutton):

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

        In the Videos tab, when the user toggles the checkbutton, enable/
        disable filtering by video name.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23107 on_filter_name_checkbutton_changed')

        self.app_obj.set_catalogue_filter_name_flag(checkbutton.get_active())
        # (No need to redraw the Video Catalogue, just to make the status icons
        #   visible/invisible
        if self.app_obj.catalogue_mode_type != 'simple':
            for catalogue_obj in self.video_catalogue_dict.values():
                catalogue_obj.update_status_images()


    def on_hide_finished_checkbutton_changed(self, checkbutton):

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

        Toggles hiding finished rows in the Progress List.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23130 on_hide_finished_checkbutton_changed')

        self.app_obj.set_progress_list_hide_flag(checkbutton.get_active())


    def on_notebook_switch_page(self, notebook, box, page_num):

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

        The Errors / Warnings tab shows the number of errors/warnings in its
        tab label. When the user switches to this tab, reset the tab label.

        Args:

            notebook (Gtk.Notebook): The main window's notebook, providing
                several tabs

            box (Gtk.Box): The box in which the tab's widgets are placed

            page_num (int): The number of the newly-visible tab (the Videos tab
                is number 0)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23155 on_notebook_switch_page')

        self.visible_tab_num = page_num

        if page_num == self.notebook_tab_dict['output']:

            # Switching between tabs causes pages in the Output tab to scroll
            #   to the top. Make sure they're all scrolled back to the bottom

            # Take into account range()...
            page_count = self.output_page_count + 1
            # ...take into account the summary page, if present
            if self.output_tab_summary_flag:
                page_count += 1

            for page_num in range(1, page_count):
                self.output_tab_scroll_visible_page(page_num)

        elif page_num == self.notebook_tab_dict['errors'] \
        and not self.app_obj.system_msg_keep_totals_flag:

            # Update the tab's label, marking all messages as not counting
            #   towards the total number of errors/warnings displayed in the
            #   future
            self.errors_list_refresh_label(True)


    def on_notify_desktop_clicked(self, notification, action_name, notify_id, \
    url):

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

        When the user clicks the button in a desktop notification, open the
        corresponding URL in the system's web browser.

        Args:

            notification: The Notify.Notification object

            action_name (str): 'action_click'

            notify_id (int): A key in self.notify_desktop_dict

            url (str): The URL to open

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23203 on_notify_desktop_clicked')

        utils.open_file(self.app_obj, url)

        # This callback isn't needed any more, so we don't need to retain a
        #   reference to the Notify.Notification
        if notify_id in self.notify_desktop_dict:
            del self.notify_desktop_dict[notify_id]


    def on_notify_desktop_closed(self, notification, notify_id):

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

        When the desktop notification (which includes a button) is closed,
        we no longer need a reference to the Notify.Notification object, so
        remove it.

        Args:

            notification: The Notify.Notification object

            notify_id (int): A key in self.notify_desktop_dict

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23230 on_notify_desktop_closed')

        if notify_id in self.notify_desktop_dict:
            del self.notify_desktop_dict[notify_id]


    def on_num_worker_checkbutton_changed(self, checkbutton):

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

        In the Progress tab, when the user sets the number of simultaneous
        downloads allowed, inform mainapp.TartubeApp, which in turn informs the
        downloads.DownloadManager object.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23251 on_num_worker_checkbutton_changed')

        if self.num_worker_checkbutton.get_active():

            self.app_obj.set_num_worker_apply_flag(True)
            self.app_obj.set_num_worker_default(
                int(self.num_worker_spinbutton.get_value())
            )

        else:

            self.app_obj.set_num_worker_apply_flag(False)


    def on_num_worker_spinbutton_changed(self, spinbutton):

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

        In the Progress tab, when the user sets the number of simultaneous
        downloads allowed, inform mainapp.TartubeApp, which in turn informs the
        downloads.DownloadManager object.

        Args:

            spinbutton (Gtk.SpinButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23280 on_num_worker_spinbutton_changed')

        if self.num_worker_checkbutton.get_active():
            self.app_obj.set_num_worker_default(
                int(self.num_worker_spinbutton.get_value())
            )


    def on_operation_error_checkbutton_changed(self, checkbutton):

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

        Toggles display of operation error messages in the tab.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 23301 on_operation_error_checkbutton_changed',
            )

        self.app_obj.set_operation_error_show_flag(checkbutton.get_active())
        self.errors_list_reset()


    def on_operation_warning_checkbutton_changed(self, checkbutton):

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

        Toggles display of operation warning messages in the tab.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 23322 on_operation_warning_checkbutton_changed',
            )

        self.app_obj.set_operation_warning_show_flag(checkbutton.get_active())
        self.errors_list_reset()


    def on_output_notebook_switch_page(self, notebook, box, page_num):

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

        When the user switches between pages in the Output tab, scroll the
        visible textview to the bottom (otherwise it gets confusing).

        Args:

            notebook (Gtk.Notebook): The Output tab's notebook, providing
                several pages

            box (Gtk.Box): The box in which the page's widgets are placed

            page_num (int): The number of the newly-visible page (the first
                page is number 0)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23350 on_output_notebook_switch_page')

        # Output tab IVs number the first page as #1, and so on
        self.output_tab_scroll_visible_page(page_num + 1)


    def on_output_size_checkbutton_changed(self, checkbutton):

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

        In the Output tab, when the user (dis)applies the maximum pages size,
        inform mainapp.TartubeApp.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23370 on_output_size_checkbutton_changed')

        if self.output_size_checkbutton.get_active():

            self.app_obj.set_output_size_apply_flag(True)
            self.app_obj.set_output_size_default(
                int(self.output_size_spinbutton.get_value())
            )

        else:

            self.app_obj.set_output_size_apply_flag(False)


    def on_output_size_spinbutton_changed(self, spinbutton):

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

        In the Output tab, when the user sets the maximum page size, inform
        mainapp.TartubeApp.

        Args:

            spinbutton (Gtk.SpinButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23398 on_output_size_spinbutton_changed')

        if self.output_size_checkbutton.get_active():
            self.app_obj.set_output_size_default(
                int(self.output_size_spinbutton.get_value())
            )


    def on_paned_size_allocate(self, widget, rect):

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

        The size of the Video tab's slider affects the size of the Video
        Catalogue grid (when visible). This function is called regularly; if
        the slider has actually moved, then we need to check whether the grid
        size needs to be changed.

        Args:

            widget (mainwin.MainWin): The widget the has been resized

            rect (Gdk.Rectangle): Object describing the window's new size

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23424 on_paned_size_allocate')

        if self.paned_last_width is None \
        or self.paned_last_width != rect.width:

            # Slider position has actually changed
            self.paned_last_width = rect.width

            if self.video_index_current_dbid \
            and self.app_obj.catalogue_mode_type == 'grid':

                # Check whether the grid should be resized and, if so, resize
                #   it
                self.video_catalogue_grid_check_size()


    def on_reverse_results_checkbutton_changed(self, checkbutton):

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

        Toggles reversing the order of the Results List.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 23453 on_reverse_results_checkbutton_changed',
            )

        self.app_obj.set_results_list_reverse_flag(checkbutton.get_active())


    def on_switch_profile_menu_select(self, menu_item, profile_name):

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

        Switches to the specified profile.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            profile_name (str): The specified profile (a key in
                mainapp.TartubeApp.profile_dict).

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23476 on_switch_profile_menu_select')

        self.switch_profile(profile_name)


    def on_switch_view(self, menu_item, catalogue_mode, catalogue_mode_type):

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

        Switches to the new Video Catalogue mode.

        Args:

            menu_item (Gtk.MenuItem): The clicked menu item

            catalogue_mode (str): One of the recognised values for
                mainapp.TartubeApp.catalogue_mode_list, e.g.
                'simple_hide_parent'

            catalogue_mode_type (str): The corresponding mode type, 'simple',
                'complex' or 'grid'

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23501 on_switch_view')

        self.app_obj.set_catalogue_mode(catalogue_mode, catalogue_mode_type)


    def on_system_container_checkbutton_changed(self, checkbutton):

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

        Toggles display of container names in the tab.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 23519 on_system_container_checkbutton_changed',
            )

        self.app_obj.set_system_msg_show_container_flag(
            checkbutton.get_active(),
        )

        name_column = self.errors_list_treeview.get_column(7)
        if not self.app_obj.system_msg_show_container_flag:
            name_column.set_visible(False)
        else:
            name_column.set_visible(True)


    def on_system_date_checkbutton_changed(self, checkbutton):

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

        Toggles display of dates (as well as times) in the tab.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23547 on_system_date_checkbutton_changed')

        self.app_obj.set_system_msg_show_date_flag(checkbutton.get_active())

        long_column = self.errors_list_treeview.get_column(5)
        short_column = self.errors_list_treeview.get_column(6)

        if not self.app_obj.system_msg_show_date_flag:
            long_column.set_visible(False)
            short_column.set_visible(True)
        else:
            long_column.set_visible(True)
            short_column.set_visible(False)


    def on_system_error_checkbutton_changed(self, checkbutton):

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

        Toggles display of system error messages in the tab.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23575 on_system_error_checkbutton_changed')

        self.app_obj.set_system_error_show_flag(checkbutton.get_active())
        self.errors_list_reset()


    def on_system_multi_line_checkbutton_changed(self, checkbutton):

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

        Toggles display of multi-line error/warning messages in the tab.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 23594 on_system_multi_line_checkbutton_changed',
            )

        self.app_obj.set_system_msg_show_multi_line_flag(
            checkbutton.get_active(),
        )

        long_column = self.errors_list_treeview.get_column(9)
        short_column = self.errors_list_treeview.get_column(10)

        if not self.app_obj.system_msg_show_multi_line_flag:
            long_column.set_visible(False)
            short_column.set_visible(True)
        else:
            long_column.set_visible(True)
            short_column.set_visible(False)


    def on_system_video_checkbutton_changed(self, checkbutton):

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

        Toggles display of video names in the tab.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23626 on_system_video_checkbutton_changed')

        self.app_obj.set_system_msg_show_video_flag(checkbutton.get_active())

        name_column = self.errors_list_treeview.get_column(8)
        if not self.app_obj.system_msg_show_video_flag:
            name_column.set_visible(False)
        else:
            name_column.set_visible(True)


    def on_system_warning_checkbutton_changed(self, checkbutton):

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

        Toggles display of system warning messages in the tab.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23650 on_system_warning_checkbutton_changed')

        self.app_obj.set_system_warning_show_flag(checkbutton.get_active())
        self.errors_list_reset()


    def on_video_res_combobox_changed(self, combo):

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

        In the Progress tab, when the user sets the video resolution limit,
        inform mainapp.TartubeApp. The new setting is applied to the next
        download job.

        Args:

            combo (Gtk.ComboBox): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23671 on_video_res_combobox_changed')

        tree_iter = self.video_res_combobox.get_active_iter()
        model = self.video_res_combobox.get_model()
        self.app_obj.set_video_res_default(model[tree_iter][0])


    def on_video_res_checkbutton_changed(self, checkbutton):

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

        In the Progress tab, when the user turns the video resolution limit
        on/off, inform mainapp.TartubeApp. The new setting is applied to the
        next download job.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23693 on_video_res_checkbutton_changed')

        self.app_obj.set_video_res_apply_flag(
            self.video_res_checkbutton.get_active(),
        )


    def on_window_drag_data_received(self, widget, drag_context, x, y, data,
    info, time):

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

        This function is required for detecting when the user drags and drops
        videos (for example, from a web browser) into the main window.

        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

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23727 on_window_drag_data_received')

        # (Git #561) Ugly hack to bypass the issue described in
        #   self.on_video_index_drag_data_received()
        if self.ignore_drag_context is not None \
        and self.ignore_drag_context == drag_context:
            self.ignore_drag_context = None
            return

        # Now continue with the code for actual drag and drops into the
        #   main window
        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

            # On MS Windows, drag and drop from an external application doesn't
            #   seem to work at all, so we don't have to worry about it
            # On Linux, URLs are received as expected, but paths to media
            #   data files are received as 'file://PATH'

            # Split 'text' into two lists, and handle them separately. Filter
            #   out any duplicate paths or duplicate URLs. For URLs, eliminate
            #   any invalid URLs
            path_list = []
            url_list = []
            duplicate_list = []

            for line in text.splitlines():

                for item in line.split():

                    # Remove leading/trailing whitespace
                    item = utils.strip_whitespace(item)

                    match = re.search(r'^file\:\/\/(.*)', item)
                    if match:

                        # (Only accept video/audio files with a supported file
                        #   extension)
                        path = urllib.parse.unquote(match.group(1))
                        name, ext = os.path.splitext(path)
                        # (Take account of the initial . in the extension)
                        if ext[1:] in formats.VIDEO_FORMAT_LIST:

                            if not path in path_list:
                                path_list.append(path)
                            else:
                                duplicate_list.append(path)

                    else:

                        if not item in url_list:
                            if utils.check_url(item):
                                url_list.append(item)
                        else:
                            duplicate_list.append(item)

            # Decide where to add the video(s)
            # If a suitable folder is selected in the Video Index, use
            #   that; otherwise, use 'Unsorted Videos'
            # However, if the Classic Mode tab is selected, copy URL(s) into
            #   its textview (and ignore any file paths)
            classic_tab = self.notebook_tab_dict['classic']
            if classic_tab is not None \
            and self.notebook.get_current_page == classic_tab \
            and url_list:

                # Classic Mode tab is selected. The final argument tells the
                #   called function to use that argument, instead of the
                #   clipboard
                utils.add_links_to_textview_from_clipboard(
                    self.app_obj,
                    self.classic_textbuffer,
                    self.classic_mark_start,
                    self.classic_mark_end,
                    '\n'.join(url_list),
                )

            elif (
                classic_tab is None \
                or self.notebook.get_current_page != classic_tab
            ) and (path_list or url_list):

                # Classic Mode tab is not selected, so behave as if the
                #   Videos tab is selected
                parent_obj = None
                current_dbid = self.video_index_current_dbid
                if current_dbid is not None:

                    parent_obj = self.app_obj.media_reg_dict[current_dbid]
                    if isinstance(parent_obj, media.Folder) \
                    and parent_obj.priv_flag:
                        parent_obj = None

                if not parent_obj:
                    parent_obj = self.app_obj.fixed_misc_folder

                # Add videos by path
                for path in path_list:

                    # Check for duplicate media.Video objects in the same
                    #   folder
                    if parent_obj.check_duplicate_video_by_path(
                        self.app_obj,
                        path,
                    ):
                        duplicate_list.append(path)
                    else:
                        new_video_obj = self.app_obj.add_video(parent_obj)
                        new_video_obj.set_file_from_path(path)

                # Add videos by URL
                for url in url_list:

                    # Check for duplicate media.Video objects in the same
                    #   folder
                    if parent_obj.check_duplicate_video(url):
                        duplicate_list.append(url)
                    else:
                        self.app_obj.add_video(parent_obj, url)

                # In the Video Index, select the parent media data object,
                #   which updates both the Video Index and the Video Catalogue
                self.video_index_select_row(parent_obj)

                # If any duplicates were found, inform the user
                if duplicate_list:

                    ignore_me = _(
                        'TRANSLATOR\'S NOTE: Duplicate URLs dragged into' \
                        + ' the main window\'s Videos tab'
                    )

                    msg = _('The following items are duplicates:')
                    for line in duplicate_list:
                        msg += '\n\n' + line

                    self.app_obj.dialogue_manager_obj.show_simple_msg_dialogue(
                        msg,
                        'warning',
                        'ok',
                    )

        # Without this line, the user's cursor is permanently stuck in drag
        #   and drop mode
        drag_context.finish(True, False, time)


    def on_window_size_allocate(self, widget, rect):

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

        The size of the window affects the size of the Video Catalogue grid
        (when visible). This function is called regularly; if the window size
        has actually changed, then we need to check whether the grid size needs
        to be changed.

        Args:

            widget (mainwin.MainWin): The widget the has been resized

            rect (Gdk.Rectangle): Object describing the window's new size

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23898 on_window_size_allocate')

        if self.win_last_width is None \
        or self.win_last_width != rect.width \
        or self.win_last_height != rect.height:

            # Window size has actually changed
            self.win_last_width = rect.width
            self.win_last_height = rect.height

            if self.video_index_current_dbid \
            and self.app_obj.catalogue_mode_type == 'grid':

                # Check whether the grid should be resized and, if so, resize
                #   it
                self.video_catalogue_grid_check_size()


    # (Callback support functions)


    def get_media_drag_data_as_list(self, media_data_obj):

        """Called by self.errors_list_add_operation_msg().

        When a media data object (video, channel or playlist) generates an
        error, that error can be displayed in the Errors List.

        The user may want to drag-and-drop the error messages to an external
        application, revealing information about the media data object that
        generated the error (e.g. the URL of a video). However, the error
        might still be visible after the media data object has been deleted.

        Therefore, we store any data that we might later want to drag-and-drop
        in three hidden columns of the errors list.

        This function returns a list of three values, one for each column. Each
        value may be an empty string or a useable value.

        Args:

            media_data_obj (media.Video, media.Channel, media.Playlist):
                The media data object that generated an error

        Return values:

            A list of three values, one for each hidden column. Each value is
                a string (which might be empty):

                1. Full file path for a video, or the full path to the
                    directory for a channel/playlist
                2. The media data object's source URL
                3. The media data object's name

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 23955 get_media_drag_data_as_list')

        return_list = []

        # Full file path
        if not self.app_obj.drag_video_path_flag:

            return_list.append('')

        elif isinstance(media_data_obj, media.Video):

            if not media_data_obj.dummy_flag \
            and media_data_obj.file_name is not None:

                return_list.append(
                    media_data_obj.get_actual_path(self.app_obj),
                )

            elif media_data_obj.dummy_flag \
            and media_data_obj.dummy_path is not None:
                return_list.append(media_data_obj.dummy_path)

            else:

                return_list.append('')

        else:

            return_list.append(media_data_obj.get_actual_dir(self.app_obj))

        # Source URL. This function should not receive a media.Folder, but
        #   check for that possibility anyway
        if isinstance(media_data_obj, media.Folder) \
        or media_data_obj.source is None:
            return_list.append('')
        else:
            return_list.append(media_data_obj.source)

        # Name
        return_list.append(media_data_obj.name)

        return return_list


    def get_selected_videos_in_treeview(self, treeview, column):

        """Called by self.on_results_list_drag_data_get() and
        .results_list_popup_menu().

        Retrieves a list of media.Video objects, one for each selected line
        in the treeview.

        Args:

            treeview (Gkt.TreeView): The treeview listing the videos

            column (int): The column containing the media.Video object's .dbid

        Return values:

            A list media.Video objects (may be an empty list)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 24020 get_selected_videos_in_treeview')

        video_list = []

        selection = treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        for tree_path in path_list:

            tree_iter = model.get_iter(tree_path)
            if tree_iter:

                dbid = model[tree_iter][0]

                # (Guard against the possibility, that the video has been
                #   deleted in the meantime)
                if dbid in self.app_obj.media_reg_dict:
                    media_data_obj = self.app_obj.media_reg_dict[dbid]
                    if isinstance(media_data_obj, media.Video):
                        video_list.append(media_data_obj)

        return video_list


    def get_selected_videos_in_classic_treeview(self):

        """Called by self.on_results_list_drag_data_get() and
        .classic_progress_list_popup_menu().

        A modified version of self.get_selected_videos_in_treeview(), to fetch
        a list of dummy media.Video objects, one for each selected line in the
        Classic Progress List's treeview.

        Return values:

            A list media.Video objects (may be an empty list)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time(
                'mwn 24059 get_selected_videos_in_classic_treeview',
            )

        video_list = []

        selection = self.classic_progress_treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        for tree_path in path_list:

            tree_iter = model.get_iter(tree_path)
            if tree_iter:

                dbid = model[tree_iter][0]

                # (Guard against the very unlikely possibility that the row
                #   has been removed in the meantime)
                if dbid in self.classic_media_dict:
                    media_data_obj = self.classic_media_dict[dbid]
                    if isinstance(media_data_obj, media.Video):
                        video_list.append(media_data_obj)

        return video_list


    def get_take_a_while_msg(self, media_data_obj, count):

        """Called by self.on_video_index_mark_bookmark(),
        .on_video_index_mark_not_bookmark(), .on_video_index_mark_waiting(),
        .on_video_index_mark_not_waiting().

        Composes a (translated) message to display in a dialogue window.

        Args:

            media_data_obj (media.Channel, media.Playlist, media.Folder): The
                media data object to be marked/unmarked

            count (int): The number of child media data objects in the
                specified channel, playlist or folder

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 24103 get_take_a_while_msg')

        media_type = media_data_obj.get_type()
        if media_type == 'channel':

            msg = _(
                'The channel contains {0} items, so this action may take' \
                + ' a while',
            ).format(str(count))

        elif media_type == 'playlist':

            msg = _(
                'The playlist contains {0} items, so this action may take' \
                + ' a while',
            ).format(str(count))

        else:

            msg = _(
                'The folder contains {0} items, so this action may take' \
                + ' a while',
            ).format(str(count))

        msg += '\n\n' + _('Are you sure you want to continue?')

        return msg


    def get_video_drag_data(self, video_list, dummy_flag=False):

        """Called by self.on_results_list_drag_data_get(),
        .on_classic_progress_list_drag_data_get(),
        CatalogueRow.on_drag_data_get() and
        CatalogueGridBox.on_drag_data_get().

        Returns the data to be transferred to an external application, when the
        user drags a video there.

        Args:

            video_list (list): List of media.Video objects being dragged

            dummy_flag (bool): If True, these are dummy media.Video objects
                (which are created by the Classic Mode tab, and are not stored
                in mainapp.TartubeApp.media_reg_dict)

        Return values:

            A single string, containing one or more full file paths/URLs/video
                names, separated by newline characters. If the path/URL/name
                isn't known for any videos, then an empty line is added to the
                string

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 24160 get_video_drag_data')

        text = ''
        for video_obj in video_list:

            if self.app_obj.drag_video_separator_flag:
                text += self.drag_drop_separator + '\n'

            if self.app_obj.drag_video_path_flag:

                if not dummy_flag and video_obj.file_name is not None:
                    text += video_obj.get_actual_path(self.app_obj) + '\n'
                elif dummy_flag and video_obj.dummy_path is not None:
                    text += video_obj.dummy_path + '\n'
                else:
                    text += '(' + _('unknown path') + ')\n'

            if self.app_obj.drag_video_source_flag:

                if video_obj.source is not None:
                    text += video_obj.source + '\n'
                else:
                    text += '(' + _('unknown URL') + ')\n'

            if self.app_obj.drag_video_name_flag:

                if video_obj.name is not None:
                    text += video_obj.name  + '\n'
                else:
                    text += self.app_obj.default_video_name + '\n'

            if self.app_obj.drag_video_msg_flag:

                if not video_obj.error_list and not video_obj.warning_list:
                    text += '(' + _('no errors/warnings') + ')\n'

                else:
                    for msg in video_obj.error_list:
                        text += msg + '\n'
                    for msg in video_obj.warning_list:
                        text += msg + '\n'

            if self.app_obj.drag_thumb_path_flag:

                if video_obj.file_name is None or dummy_flag:
                    # (Existing code won't be able to find the thumbnail,
                    #   even if the file has been downloaded)
                    text += '(' + _('unknown thumbnail path') + ')\n'

                else:

                    thumb_path = utils.find_thumbnail(
                        self.app_obj,
                        video_obj,
                        True,
                    )

                    if thumb_path is not None:
                        text += thumb_path + '\n'
                    else:
                        text += '(' + _('unknown thumbnail path') + ')\n'

        return text


    # Set accessors


    def add_child_window(self, config_win_obj):

        """Called by config.GenericConfigWin.setup().

        When a configuration window opens, add it to our list of such windows.

        Args:

            config_win_obj (config.GenericConfigWin): The window to add

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 24241 add_child_window')

        # Check that the window isn't already in the list (unlikely, but check
        #   anyway)
        if config_win_obj in self.config_win_list:
            return self.app_obj.system_error(
                266,
                'Callback request denied due to current conditions',
            )

        # Update IVs
        self.config_win_list.append(config_win_obj)
        if isinstance(config_win_obj, wizwin.GenericWizWin):

            self.wiz_win_obj = config_win_obj


    def del_child_window(self, config_win_obj):

        """Called by config.GenericConfigWin.close().

        When a configuration window closes, remove it to our list of such
        windows.

        Args:

            config_win_obj (config.GenericConfigWin): The window to remove

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 24272 del_child_window')

        # Update IVs
        # (Don't show an error if the window isn't in the list, as it's
        #   conceivable this function might be called twice)
        if config_win_obj in self.config_win_list:

            self.config_win_list.remove(config_win_obj)

            if self.wiz_win_obj is not None \
            and self.wiz_win_obj == config_win_obj:
                self.wiz_win_obj = None


    def reset_video_catalogue_drag_list(self):

        """Can be called by anything.

        Dragging from the Video Catalogue into the Video Index is handled by
        storing a list of videos involved, at the start of the drag. The list
        is no longer required, so reset it.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 24296 reset_video_catalogue_drag_list')

        self.video_catalogue_drag_list = []


    def set_previous_alt_dest_dbid(self, value):

        """Called by functions in SetDestinationDialogue.

        The specified value may be a .dbid, or None.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 24309 set_previous_alt_dest_dbid')

        self.previous_alt_dest_dbid = value


    def set_previous_external_dir(self, value):

        """Called by functions in SetDestinationDialogue.

        The specified value may be a full path to a directory, or None.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 24322 set_previous_external_dir')

        self.previous_external_dir = value


    def set_video_catalogue_drag_list(self, video_list):

        """Called by mainwin.CatalogueRow.on_drag_data_get() and
        mainwin.CatalogueGridBox.on_drag_data_get().

        Dragging from the Video Catalogue into the Video Index is handled by
        storing a list of videos involved, at the start of the drag.

        Args:

            video_list (list): A list of media.Video objects to be dragged

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 24342 set_video_catalogue_drag_list')

        self.video_catalogue_drag_list = video_list.copy()


class SimpleCatalogueItem(object):

    """Called by MainWin.video_catalogue_redraw_all() and
    .video_catalogue_insert_video().

    Python class that handles a single row in the Video Catalogue.

    Each mainwin.SimpleCatalogueItem object stores widgets used in that row,
    and updates them when required.

    This class offers a simple view with a minimum of widgets (for example, no
    video thumbnails). The mainwin.ComplexCatalogueItem class offers a more
    complex view (for example, with video thumbnails).

    Args:

        main_win_obj (mainwin.MainWin): The main window object

        video_obj (media.Video): The media data object itself (always a video)

    """


    # Standard class methods


    def __init__(self, main_win_obj, video_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 24376 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: This section specifies text used to' \
            + ' display videos in the Videos tab. Videos are displayed in' \
            + ' several different formats. To switch format, in the main' \
            + ' menu click Media > Switch between views'
        )

        # IV list - class objects
        # -----------------------
        # The main window object
        self.main_win_obj = main_win_obj
        # The media data object itself (always a video)
        self.video_obj = video_obj


        # IV list - Gtk widgets
        # ---------------------
        self.catalogue_row = None           # mainwin.CatalogueRow
        self.hbox = None                    # Gtk.HBox
        self.status_image = None            # Gtk.Image
        self.name_label = None              # Gtk.Label
        self.parent_label = None            # Gtk.Label
        self.stats_label = None             # Gtk.Label
        self.comment_image = None           # Gtk.Image
        self.subs_image = None              # Gtk.Image
        self.slice_image = None             # Gtk.Image
        self.stamp_image = None             # Gtk.Image
        self.warning_image = None           # Gtk.Image
        self.error_image = None             # Gtk.Image
        self.options_image = None           # Gtk.Image


        # IV list - other
        # ---------------
        # Unique ID for this object, matching the .dbid for self.video_obj (an
        #   integer)
        self.dbid = video_obj.dbid
        # Size (in pixels) of gaps between various widgets
        self.spacing_size = 5

        # Whenever self.draw_widgets() or .update_widgets() is called, the
        #   background colour might be changed
        # This IV shows the value of the self.video_obj.live_mode, the last
        #   time either of those functions was called. If the value has
        #   actually changed, then we ask Gtk to change the background
        #   (otherwise, we don't)
        self.previous_live_mode = 0


    # Public class methods


    def draw_widgets(self, catalogue_row):

        """Called by mainwin.MainWin.video_catalogue_redraw_all() and
        .video_catalogue_insert_video().

        After a Gtk.ListBoxRow has been created for this object, populate it
        with widgets.

        Args:

            catalogue_row (mainwin.CatalogueRow): A wrapper for a
                Gtk.ListBoxRow object, storing the media.Video object displayed
                in that row.

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 24447 draw_widgets')

        self.catalogue_row = catalogue_row

        event_box = Gtk.EventBox()
        self.catalogue_row.add(event_box)
        event_box.connect('button-press-event', self.on_right_click_row)

        self.hbox = Gtk.Box(
            orientation=Gtk.Orientation.HORIZONTAL,
            spacing=0,
        )
        event_box.add(self.hbox)
        self.hbox.set_border_width(0)

        # Highlight livestreams by specifying a background colour
        self.update_background()

        # Status icon
        self.status_image = Gtk.Image()
        self.hbox.pack_start(self.status_image, False, False, 0)

        # Box with two lines of text
        vbox = Gtk.Box(
            orientation=Gtk.Orientation.VERTICAL,
            spacing=0,
        )
        self.hbox.pack_start(vbox, True, True, self.spacing_size)

        # Video name
        self.name_label = Gtk.Label('', xalign = 0)
        vbox.pack_start(self.name_label, True, True, 0)

        # Parent channel/playlist/folder name (if allowed)
        if self.main_win_obj.app_obj.catalogue_mode == 'simple_show_parent':
            self.parent_label = Gtk.Label('', xalign = 0)
            vbox.pack_start(self.parent_label, True, True, 0)

        # Video stats
        self.stats_label = Gtk.Label('', xalign=0)
        vbox.pack_start(self.stats_label, True, True, 0)

        # Remainining status icons
        self.comment_image = Gtk.Image()
        self.hbox.pack_end(self.comment_image, False, False, self.spacing_size)

        self.subs_image = Gtk.Image()
        self.hbox.pack_end(self.subs_image, False, False, 0)

        self.slice_image = Gtk.Image()
        self.hbox.pack_end(self.slice_image, False, False, self.spacing_size)

        self.stamp_image = Gtk.Image()
        self.hbox.pack_end(self.stamp_image, False, False, 0)

        self.warning_image = Gtk.Image()
        self.hbox.pack_end(self.warning_image, False, False, self.spacing_size)

        self.error_image = Gtk.Image()
        self.hbox.pack_end(self.error_image, False, False, 0)

        self.options_image = Gtk.Image()
        self.hbox.pack_end(self.options_image, False, False, self.spacing_size)


    def update_widgets(self):

        """Called by mainwin.MainWin.video_catalogue_redraw_all(),
        .video_catalogue_update_video() and .video_catalogue_insert_video().

        Sets the values displayed by each widget.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 24521 update_widgets')

        self.update_background()
        self.update_tooltips()
        self.update_status_images()
        self.update_video_name()
        self.update_container_name()
        self.update_video_stats()


    def update_background(self):

        """Calledy by self.draw_widgets() and .update_widgets().

        Updates the background colour to show which videos are livestreams
        (but only when a video's livestream mode has changed).
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 24540 update_background')

        if self.previous_live_mode != self.video_obj.live_mode:

            self.previous_live_mode = self.video_obj.live_mode

            if self.video_obj.live_mode == 0 \
            or not self.main_win_obj.app_obj.livestream_use_colour_flag:

                self.hbox.override_background_color(
                    Gtk.StateType.NORMAL,
                    None,
                )

            elif self.video_obj.live_mode == 1:

                if not self.video_obj.live_debut_flag \
                or self.main_win_obj.app_obj.livestream_simple_colour_flag:

                    self.hbox.override_background_color(
                        Gtk.StateType.NORMAL,
                        self.main_win_obj.live_wait_colour,
                    )

                else:

                    self.hbox.override_background_color(
                        Gtk.StateType.NORMAL,
                        self.main_win_obj.debut_wait_colour,
                    )

            elif self.video_obj.live_mode == 2:

                if not self.video_obj.live_debut_flag \
                or self.main_win_obj.app_obj.livestream_simple_colour_flag:

                    self.hbox.override_background_color(
                        Gtk.StateType.NORMAL,
                        self.main_win_obj.live_now_colour,
                    )

                else:

                    self.hbox.override_background_color(
                        Gtk.StateType.NORMAL,
                        self.main_win_obj.debut_now_colour,
                    )


    def update_tooltips(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the tooltips for the Gtk.HBox that contains everything.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 24597 update_tooltips')

        if self.main_win_obj.app_obj.show_tooltips_flag:
            self.hbox.set_tooltip_text(
                self.video_obj.fetch_tooltip_text(
                    self.main_win_obj.app_obj,
                    self.main_win_obj.tooltip_max_len,
                ),
            )


    def update_status_images(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the Gtk.Image widget to display the video's download status.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 24616 update_status_images')

        # Set the download status
        if self.video_obj.live_mode == 1:

            if not self.video_obj.live_debut_flag:

                self.status_image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['live_wait_small'],
                )

            else:

                self.status_image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['debut_wait_small'],
                )

        elif self.video_obj.live_mode == 2:

            if not self.video_obj.live_debut_flag:

                self.status_image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['live_now_small'],
                )

            else:

                self.status_image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['debut_now_small'],
                )

        elif self.video_obj.dl_flag:

            if self.video_obj.archive_flag:

                self.status_image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['archived_small'],
                )

            elif self.video_obj.split_flag:

                self.status_image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['split_file_small'],
                )

            elif self.video_obj.was_live_flag:

                self.status_image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['live_old_small'],
                )

            else:

                self.status_image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['have_file_small'],
                )

        elif self.video_obj.was_live_flag:

            self.status_image.set_from_pixbuf(
                self.main_win_obj.pixbuf_dict['live_old_no_file_small'],
            )

        else:

            self.status_image.set_from_pixbuf(
                self.main_win_obj.pixbuf_dict['no_file_small'],
            )

        # The remaining status icons are not displayed at all, if the flag is
        #   not set
        if not self.main_win_obj.app_obj.catalogue_draw_icons_flag:
            self.status_image.clear()
            self.warning_image.clear()
            self.error_image.clear()

        else:

            # To prevent an unsightly gap between these images, use the first
            #   available Gtk.Image
            image_list = [
                self.comment_image,
                self.subs_image,
                self.slice_image,
                self.stamp_image,
                self.warning_image,
                self.error_image,
                self.options_image,
            ]

            if self.video_obj.comment_list:
                image = image_list.pop(0)
                image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['comment_small'],
                )

            if self.video_obj.subs_list:
                image = image_list.pop(0)
                image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['subs_small'],
                )

            if self.video_obj.slice_list:
                image = image_list.pop(0)
                image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['slice_small'],
                )

            if self.video_obj.stamp_list:
                image = image_list.pop(0)
                image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['stamp_small'],
                )

            if self.video_obj.warning_list:
                image = image_list.pop(0)
                image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['warning_small'],
                )

            if self.video_obj.error_list:
                image = image_list.pop(0)
                image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['error_small'],
                )

            if self.video_obj.options_obj:
                image = image_list.pop(0)
                image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['dl_options_small'],
                )

            for image in image_list:
                image.clear()


    def update_video_name(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the Gtk.Label widget to display the video's current name.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 24760 update_video_name')

        # For videos whose name is unknown, display the URL, rather than the
        #   usual '(video with no name)' string
        if not self.main_win_obj.app_obj.catalogue_show_nickname_flag:
            name = self.video_obj.name
        else:
            name = self.video_obj.nickname

        if name is None \
        or name == self.main_win_obj.app_obj.default_video_name:

            if self.video_obj.source is not None:

                # Using pango markup to display a URL is too risky, so just use
                #   ordinary text
                self.name_label.set_text(
                    utils.shorten_string(
                        self.video_obj.source,
                        self.main_win_obj.very_long_string_max_len,
                    ),
                )

                return

            else:

                # No URL to show, so we're forced to use '(video with no name)'
                name = self.main_win_obj.app_obj.default_video_name

        string = ''
        if self.video_obj.new_flag:
            string += ' font_weight="bold"'

        if self.video_obj.dl_sim_flag:
            string += ' style="italic"'

        self.name_label.set_markup(
            '<span font_size="large"' + string + '>' + \
            html.escape(
                utils.shorten_string(
                    name,
                    self.main_win_obj.very_long_string_max_len,
                ),
                quote=True,
            ) + '</span>'
        )


    def update_container_name(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the Gtk.Label widget to display the name of the parent channel,
        playlist or folder.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 24818 update_container_name')

        if self.main_win_obj.app_obj.catalogue_mode != 'simple_show_parent':
            return

        if self.video_obj.orig_parent is not None:

            string = _('Originally from:') + ' \''

            string2 = html.escape(
                utils.shorten_string(
                    self.video_obj.orig_parent,
                    self.main_win_obj.long_string_max_len,
                ),
                quote=True,
            )

        else:

            if isinstance(self.video_obj.parent_obj, media.Channel):
                string = _('From channel')
            elif isinstance(self.video_obj.parent_obj, media.Playlist):
                string = _('From playlist')
            else:
                string = _('From folder')

            string2 = html.escape(
                utils.shorten_string(
                    self.video_obj.parent_obj.name,
                    self.main_win_obj.long_string_max_len,
                ),
                quote=True,
            )

        self.parent_label.set_markup('<i>' + string + '</i>: ' + string2)


    def update_video_stats(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the Gtk.Label widget to display the video's current side/
        duration/date information.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 24864 update_video_stats')

        if self.video_obj.live_mode:

            if not self.video_obj.live_debut_flag:

                if self.video_obj.live_mode == 2:
                    msg = _('Livestream has started')
                elif self.video_obj.live_msg == '':
                    msg = _('Livestream has not started yet')
                else:
                    msg = self.video_obj.live_msg

            else:

                if self.video_obj.live_mode == 2:
                    msg = _('Debut has started')
                elif self.video_obj.live_msg == '':
                    msg = _('Debut has not started yet')
                else:
                    msg = self.video_obj.live_msg

        else:

            if self.video_obj.duration is not None:
                msg = _('Duration:') + ' ' + utils.convert_seconds_to_string(
                    self.video_obj.duration,
                    True,
                )

            else:
                msg = _('Duration:') + ' <i>' + _('unknown') + '</i>'

            size = self.video_obj.get_file_size_string()
            if size != "":
                msg += '  -  ' + _('Size:') + ' ' + size
            else:
                msg += '  -  ' + _('Size:') + ' <i>' + _('unknown') + '</i>'

            pretty_flag = self.main_win_obj.app_obj.show_pretty_dates_flag
            if self.main_win_obj.app_obj.catalogue_sort_mode == 'receive':
                date = self.video_obj.get_receive_date_string(pretty_flag)
            else:
                date = self.video_obj.get_upload_date_string(pretty_flag)

            if date is not None:
                msg += '  -  ' + _('Date:') + ' ' + date
            else:
                msg += '  -  ' + _('Date:') + ' <i>' + _('unknown') + '</i>'

        self.stats_label.set_markup(msg)


    # Callback methods


    def on_right_click_row(self, event_box, event):

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

        When the user right-clicks an a row, create a context-sensitive popup
        menu.

        Args:

            event_box (Gtk.EventBox), event (Gtk.EventButton): Data from the
                signal emitted by the click

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 24935 on_right_click_row')

        if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3:

            self.main_win_obj.video_catalogue_popup_menu(event, self.video_obj)


class ComplexCatalogueItem(object):

    """Called by MainWin.video_catalogue_redraw_all() and
    .video_catalogue_insert_video().

    Python class that handles a single row in the Video Catalogue.

    Each mainwin.ComplexCatalogueItem object stores widgets used in that row,
    and updates them when required.

    The mainwin.SimpleCatalogueItem class offers a simple view with a minimum
    of widgets (for example, no video thumbnails). This class offers a more
    complex view (for example, with video thumbnails).

    Args:

        main_win_obj (mainwin.MainWin): The main window object

        video_obj (media.Video): The media data object itself (always a video)

    """


    # Standard class methods


    def __init__(self, main_win_obj, video_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 24971 __init__')

        # IV list - class objects
        # -----------------------
        # The main window object
        self.main_win_obj = main_win_obj
        # The media data object itself (always a video)
        self.video_obj = video_obj


        # IV list - Gtk widgets
        # ---------------------
        self.catalogue_row = None           # mainwin.CatalogueRow
        self.frame = None                   # Gtk.Frame
        self.thumb_box = None               # Gtk.Box
        self.thumb_image = None             # Gtk.Image
        self.label_box = None               # Gtk.Box
        self.name_label = None              # Gtk.Label
        self.status_image = None            # Gtk.Image
        self.comment_image = None           # Gtk.Image
        self.subs_image = None              # Gtk.Image
        self.slice_image = None             # Gtk.Image
        self.stamp_image = None             # Gtk.Image
        self.warning_image = None           # Gtk.Image
        self.error_image = None             # Gtk.Image
        self.options_image = None           # Gtk.Image
        self.descrip_label = None           # Gtk.Label
        self.expand_label = None            # Gtk.Label
        self.stats_label = None             # Gtk.Label
        self.live_auto_notify_label = None  # Gtk.Label
        self.live_auto_alarm_label = None   # Gtk.Label
        self.live_auto_open_label = None    # Gtk.Label
        self.live_auto_dl_start_label = None
                                            # Gtk.Label
        self.live_auto_dl_stop_label = None # Gtk.Label
        self.watch_label = None             # Gtk.Label
        self.watch_player_label = None      # Gtk.Label
        self.watch_web_label = None         # Gtk.Label
        self.watch_hooktube_label = None    # Gtk.Label
        self.watch_invidious_label = None   # Gtk.Label
        self.watch_other_label = None       # Gtk.Label
        self.temp_box = None                # Gtk.Box
        self.temp_label = None              # Gtk.Label
        self.temp_mark_label = None         # Gtk.Label
        self.temp_dl_label = None           # Gtk.Label
        self.temp_dl_watch_label = None     # Gtk.Label
        self.marked_box = None              # Gtk.Box
        self.marked_label = None            # Gtk.Label
        self.marked_archive_label = None    # Gtk.Label
        self.marked_bookmark_label = None   # Gtk.Label
        self.marked_fav_label = None        # Gtk.Label
        self.marked_missing_label = None    # Gtk.Label
        self.marked_new_label = None        # Gtk.Label
        self.marked_waiting_label = None    # Gtk.Label


        # IV list - other
        # ---------------
        # Unique ID for this object, matching the .dbid for self.video_obj (an
        #   integer)
        self.dbid = video_obj.dbid
        # Size (in pixels) of gaps between various widgets
        self.spacing_size = 5
        # The state of the More/Less label. False if the video's short
        #   description (or no description at all) is visible, True if the
        #   video's full description is visible
        self.expand_descrip_flag = False
        # Flag set to True if the video's parent folder is a temporary folder,
        #   meaning that some widgets don't need to be drawn at all
        self.no_temp_widgets_flag = False

        # Whenever self.draw_widgets() or .update_widgets() is called, the
        #   background colour might be changed
        # This IV shows the value of the self.video_obj.live_mode, the last
        #   time either of those functions was called. If the value has
        #   actually changed, then we ask Gtk to change the background
        #   (otherwise, we don't)
        self.previous_live_mode = 0
        # Flag set to True when the temporary labels box (self.temp_box) is
        #   visible, False when not
        self.temp_box_visible_flag = False
        # Flag set to True when the marked labels box (self.marked_box) is
        #   visible, False when not
        self.marked_box_visible_flag = False


    # Public class methods


    def draw_widgets(self, catalogue_row):

        """Called by mainwin.MainWin.video_catalogue_redraw_all() and
        .video_catalogue_insert_video().

        After a Gtk.ListBoxRow has been created for this object, populate it
        with widgets.

        Args:

            catalogue_row (mainwin.CatalogueRow): A wrapper for a
                Gtk.ListBoxRow object, storing the media.Video object displayed
                in that row.

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 25077 draw_widgets')

        # If the video's parent folder is a temporary folder, then we don't
        #   need one row of widgets at all
        parent_obj = self.video_obj.parent_obj
        if isinstance(parent_obj, media.Folder) \
        and parent_obj.temp_flag:
            self.no_temp_widgets_flag = True
        else:
            self.no_temp_widgets_flag = False

        # Draw the widgets
        self.catalogue_row = catalogue_row

        event_box = Gtk.EventBox()
        self.catalogue_row.add(event_box)
        event_box.connect('button-press-event', self.on_right_click_row)

        self.frame = Gtk.Frame()
        event_box.add(self.frame)
        self.frame.set_border_width(self.spacing_size)
        self.enable_visible_frame(
            self.main_win_obj.app_obj.catalogue_draw_frame_flag,
        )

        # Highlight livestreams by specifying a background colour
        self.update_background()

        hbox = Gtk.Box(
            orientation=Gtk.Orientation.HORIZONTAL,
            spacing=0,
        )
        self.frame.add(hbox)
        hbox.set_border_width(self.spacing_size)

        # The thumbnail is in its own vbox, so we can keep it in the top-left
        #   when the video's description has multiple lines
        self.thumb_box = Gtk.Box(
            orientation=Gtk.Orientation.VERTICAL,
            spacing=0,
        )
        hbox.pack_start(self.thumb_box, False, False, 0)

        self.thumb_image = Gtk.Image()
        self.thumb_box.pack_start(self.thumb_image, False, False, 0)

        # Everything to the right of the thumbnail is in a second vbox
        self.label_box = Gtk.Box(
            orientation=Gtk.Orientation.VERTICAL,
            spacing=0,
        )
        hbox.pack_start(self.label_box, True, True, self.spacing_size)

        # First row - video name
        hbox2 = Gtk.Box(
            orientation=Gtk.Orientation.HORIZONTAL,
            spacing=0,
        )
        self.label_box.pack_start(hbox2, True, True, 0)

        self.name_label = Gtk.Label('', xalign = 0)
        hbox2.pack_start(self.name_label, True, True, 0)

        # Status icons
        self.status_image = Gtk.Image()
        hbox2.pack_end(self.status_image, False, False, 0)

        self.comment_image = Gtk.Image()
        hbox2.pack_end(self.comment_image, False, False, self.spacing_size)

        self.subs_image = Gtk.Image()
        hbox2.pack_end(self.subs_image, False, False, 0)

        self.slice_image = Gtk.Image()
        hbox2.pack_end(self.slice_image, False, False, self.spacing_size)

        self.stamp_image = Gtk.Image()
        hbox2.pack_end(self.stamp_image, False, False, 0)

        self.warning_image = Gtk.Image()
        hbox2.pack_end(self.warning_image, False, False, self.spacing_size)

        self.error_image = Gtk.Image()
        hbox2.pack_end(self.error_image, False, False, 0)

        self.options_image = Gtk.Image()
        hbox2.pack_end(self.options_image, False, False, self.spacing_size)

        # Second row - video description (incorporating the the More/Less
        #   label), or the name of the parent channel/playlist/folder,
        #   depending on settings
        self.descrip_label = Gtk.Label('', xalign=0)
        self.label_box.pack_start(self.descrip_label, True, True, 0)
        self.descrip_label.connect(
            'activate-link',
            self.on_click_descrip_label,
        )

        # Third row - video stats, or livestream notification options,
        #   depending on settings
        hbox3 = Gtk.Box(
            orientation=Gtk.Orientation.HORIZONTAL,
            spacing=0,
        )
        self.label_box.pack_start(hbox3, True, True, 0)

        # (This label is visible in both situations)
        self.stats_label = Gtk.Label('', xalign=0)
        hbox3.pack_start(self.stats_label, False, False, 0)

        # (These labels are visible only for livestreams)
        # Auto-notify
        self.live_auto_notify_label = Gtk.Label('', xalign=0)
        hbox3.pack_start(
            self.live_auto_notify_label,
            False,
            False,
            0,
        )
        self.live_auto_notify_label.connect(
            'activate-link',
            self.on_click_live_auto_notify_label,
        )

        # Auto-sound alarm
        self.live_auto_alarm_label = Gtk.Label('', xalign=0)
        hbox3.pack_start(
            self.live_auto_alarm_label,
            False,
            False,
            (self.spacing_size * 2),
        )
        self.live_auto_alarm_label.connect(
            'activate-link',
            self.on_click_live_auto_alarm_label,
        )

        # Auto-open
        self.live_auto_open_label = Gtk.Label('', xalign=0)
        hbox3.pack_start(
            self.live_auto_open_label,
            False,
            False,
            0,
        )
        self.live_auto_open_label.connect(
            'activate-link',
            self.on_click_live_auto_open_label,
        )

        # D/L on start
        self.live_auto_dl_start_label = Gtk.Label('', xalign=0)
        hbox3.pack_start(
            self.live_auto_dl_start_label,
            False,
            False,
            (self.spacing_size * 2),
        )
        self.live_auto_dl_start_label.connect(
            'activate-link',
            self.on_click_live_auto_dl_start_label,
        )

        # D/L on stop
        self.live_auto_dl_stop_label = Gtk.Label('', xalign=0)
        hbox3.pack_start(
            self.live_auto_dl_stop_label,
            False,
            False,
            0,
        )
        self.live_auto_dl_stop_label.connect(
            'activate-link',
            self.on_click_live_auto_dl_stop_label,
        )

        # Fourth row - Watch...
        hbox4 = Gtk.Box(
            orientation=Gtk.Orientation.HORIZONTAL,
            spacing=0,
        )
        self.label_box.pack_start(hbox4, True, True, 0)

        self.watch_label = Gtk.Label(_('Watch:') + '   ', xalign=0)
        hbox4.pack_start(self.watch_label, False, False, 0)

        # Watch in player
        self.watch_player_label = Gtk.Label('', xalign=0)
        hbox4.pack_start(self.watch_player_label, False, False, 0)
        self.watch_player_label.connect(
            'activate-link',
            self.on_click_watch_player_label,
        )

        # Watch on website/YouTube
        self.watch_web_label = Gtk.Label('', xalign=0)
        hbox4.pack_start(
            self.watch_web_label,
            False,
            False,
            (self.spacing_size * 2),
        )
        self.watch_web_label.connect(
            'activate-link',
            self.on_click_watch_web_label,
        )

        # Watch on HookTube
        self.watch_hooktube_label = Gtk.Label('', xalign=0)
        hbox4.pack_start(self.watch_hooktube_label, False, False, 0)
        self.watch_hooktube_label.connect(
            'activate-link',
            self.on_click_watch_hooktube_label,
        )

        # Watch on Invidious
        self.watch_invidious_label = Gtk.Label('', xalign=0)
        hbox4.pack_start(
            self.watch_invidious_label,
            False,
            False,
            (self.spacing_size * 2),
        )
        self.watch_invidious_label.connect(
            'activate-link',
            self.on_click_watch_invidious_label,
        )

        # Watch on the other YouTube front-end (specified by the user)
        self.watch_other_label = Gtk.Label('', xalign=0)
        hbox4.pack_start(
            self.watch_other_label,
            False,
            False,
            0,
        )
        self.watch_other_label.connect(
            'activate-link',
            self.on_click_watch_other_label,
        )

        # Optional rows

        # Fifth row: Temporary...
        self.temp_box = Gtk.Box(
            orientation=Gtk.Orientation.HORIZONTAL,
            spacing=0,
        )
        if self.temp_box_is_visible():
            self.label_box.pack_start(self.temp_box, True, True, 0)
            self.temp_box_visible_flag = True

        self.temp_label = Gtk.Label(_('Temporary:') + '   ', xalign=0)
        self.temp_box.pack_start(self.temp_label, False, False, 0)

        # Mark for download
        self.temp_mark_label = Gtk.Label('', xalign=0)
        self.temp_box.pack_start(self.temp_mark_label, False, False, 0)
        self.temp_mark_label.connect(
            'activate-link',
            self.on_click_temp_mark_label,
        )

        # Download
        self.temp_dl_label = Gtk.Label('', xalign=0)
        self.temp_box.pack_start(
            self.temp_dl_label,
            False,
            False,
            (self.spacing_size * 2),
        )
        self.temp_dl_label.connect(
            'activate-link',
            self.on_click_temp_dl_label,
        )

        # Download and watch
        self.temp_dl_watch_label = Gtk.Label('', xalign=0)
        self.temp_box.pack_start(self.temp_dl_watch_label, False, False, 0)
        self.temp_dl_watch_label.connect(
            'activate-link',
            self.on_click_temp_dl_watch_label,
        )

        # Sixth row: Marked...
        self.marked_box = Gtk.Box(
            orientation=Gtk.Orientation.HORIZONTAL,
            spacing=0,
        )
        if self.marked_box_is_visible():
            # (For the sixth row we use .pack_end, so that the fifth row can be
            #   added and removed, without affecting the visible order)
            self.label_box.pack_end(self.marked_box, True, True, 0)
            self.marked_box_visible_flag = True

        self.marked_label = Gtk.Label(_('Marked:') + '   ', xalign=0)
        self.marked_box.pack_start(self.marked_label, False, False, 0)

        # Archived/not archived
        self.marked_archive_label = Gtk.Label('', xalign=0)
        self.marked_box.pack_start(self.marked_archive_label, False, False, 0)
        self.marked_archive_label.connect(
            'activate-link',
            self.on_click_marked_archive_label,
        )

        # Bookmarked/not bookmarked
        self.marked_bookmark_label = Gtk.Label('', xalign=0)
        self.marked_box.pack_start(
            self.marked_bookmark_label,
            False,
            False,
            (self.spacing_size * 2),
        )
        self.marked_bookmark_label.connect(
            'activate-link',
            self.on_click_marked_bookmark_label,
        )

        # Favourite/not favourite
        self.marked_fav_label = Gtk.Label('', xalign=0)
        self.marked_box.pack_start(self.marked_fav_label, False, False, 0)
        self.marked_fav_label.connect(
            'activate-link',
            self.on_click_marked_fav_label,
        )

        # Missing/not missing
        self.marked_missing_label = Gtk.Label('', xalign=0)
        self.marked_box.pack_start(
            self.marked_missing_label,
            False,
            False,
            (self.spacing_size * 2),
        )
        self.marked_missing_label.connect(
            'activate-link',
            self.on_click_marked_missing_label,
        )

        # New/not new
        self.marked_new_label = Gtk.Label('', xalign=0)
        self.marked_box.pack_start(self.marked_new_label, False, False, 0)
        self.marked_new_label.connect(
            'activate-link',
            self.on_click_marked_new_label,
        )

        # In waiting list/not in waiting list
        self.marked_waiting_label = Gtk.Label('', xalign=0)
        self.marked_box.pack_start(
            self.marked_waiting_label,
            False,
            False,
            (self.spacing_size * 2),
        )
        self.marked_waiting_label.connect(
            'activate-link',
            self.on_click_marked_waiting_list_label,
        )


    def update_widgets(self):

        """Called by mainwin.MainWin.video_catalogue_redraw_all(),
        .video_catalogue_update_video() and .video_catalogue_insert_video().

        Sets the values displayed by each widget.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 25448 update_widgets')

        self.update_background()
        self.update_tooltips()
        self.update_thumb_image()
        self.update_video_name()
        self.update_status_images()
        self.update_video_descrip()
        self.update_video_stats()
        self.update_watch_player()
        self.update_watch_web()

        # If the fifth/sixth rows are not currently visible, but need to be
        #   visible, make them visible (and vice-versa)
        if not self.temp_box_is_visible():

            if self.temp_box_visible_flag:
                self.label_box.remove(self.temp_box)
                self.temp_box_visible_flag = False

        else:

            self.update_temp_labels()
            if not self.temp_box_visible_flag:
                self.label_box.pack_start(self.temp_box, True, True, 0)
                self.temp_box_visible_flag = True

        if not self.marked_box_is_visible():

            if self.marked_box_visible_flag:
                self.label_box.remove(self.marked_box)
                self.marked_box_visible_flag = False

        else:

            self.update_marked_labels()
            if not self.marked_box_visible_flag:
                self.label_box.pack_end(self.marked_box, True, True, 0)
                self.marked_box_visible_flag = True


    def update_background(self):

        """Calledy by self.draw_widgets() and .update_widgets().

        Updates the background colour to show which videos are livestreams
        (but only when a video's livestream mode has changed).
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 25498 update_background')

        if self.previous_live_mode != self.video_obj.live_mode:

            self.previous_live_mode = self.video_obj.live_mode

            if self.video_obj.live_mode == 0 \
            or not self.main_win_obj.app_obj.livestream_use_colour_flag:

                self.frame.override_background_color(
                    Gtk.StateType.NORMAL,
                    None,
                )

            elif self.video_obj.live_mode == 1:

                if not self.video_obj.live_debut_flag \
                or self.main_win_obj.app_obj.livestream_simple_colour_flag:

                    self.frame.override_background_color(
                        Gtk.StateType.NORMAL,
                        self.main_win_obj.live_wait_colour,
                    )

                else:

                    self.frame.override_background_color(
                        Gtk.StateType.NORMAL,
                        self.main_win_obj.debut_wait_colour,
                    )

            elif self.video_obj.live_mode == 2:

                if not self.video_obj.live_debut_flag \
                or self.main_win_obj.app_obj.livestream_simple_colour_flag:

                    self.frame.override_background_color(
                        Gtk.StateType.NORMAL,
                        self.main_win_obj.live_now_colour,
                    )

                else:

                    self.frame.override_background_color(
                        Gtk.StateType.NORMAL,
                        self.main_win_obj.debut_now_colour,
                    )


    def update_tooltips(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the tooltips for the Gtk.Frame that contains everything.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 25555 update_tooltips')

        if self.main_win_obj.app_obj.show_tooltips_flag:
            self.frame.set_tooltip_text(
                self.video_obj.fetch_tooltip_text(
                    self.main_win_obj.app_obj,
                    self.main_win_obj.tooltip_max_len,
                ),
            )


    def update_thumb_image(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the Gtk.Image widget to display the video's thumbnail, if
        available.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 25575 update_thumb_image')

        # See if the video's thumbnail file has been downloaded
        thumb_flag = False
        if self.video_obj.file_name:

            # No way to know which image format is used by all websites for
            #   their video thumbnails, so look for the most common ones
            # The True argument means that if the thumbnail isn't found in
            #   Tartube's main data directory, look in the temporary directory
            #   too
            path = utils.find_thumbnail(
                self.main_win_obj.app_obj,
                self.video_obj,
                True,
            )

            if path:

                # Thumbnail file exists, so use it
                app_obj = self.main_win_obj.app_obj
                mini_list = app_obj.thumb_size_dict['tiny']
                # (Returns a tuple, who knows why)
                arglist = app_obj.file_manager_obj.load_to_pixbuf(
                    path,
                    mini_list[0],       # width
                    mini_list[1],       # height
                ),

                if arglist[0]:
                    self.thumb_image.set_from_pixbuf(arglist[0])
                    thumb_flag = True

        # No thumbnail file found, so use a default file
        if not thumb_flag:
            if self.video_obj.fav_flag and self.video_obj.options_obj:
                self.thumb_image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['thumb_both_tiny'],
                )
            elif self.video_obj.fav_flag:
                self.thumb_image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['thumb_left_tiny'],
                )
            elif self.video_obj.options_obj:
                self.thumb_image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['thumb_right_tiny'],
                )
            else:
                self.thumb_image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['thumb_none_tiny'],
                )


    def update_video_name(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the Gtk.Label widget to display the video's current name.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 25635 update_video_name')

        # For videos whose name is unknown, display the URL, rather than the
        #   usual '(video with no name)' string
        if not self.main_win_obj.app_obj.catalogue_show_nickname_flag:
            name = self.video_obj.name
        else:
            name = self.video_obj.nickname

        if name is None \
        or name == self.main_win_obj.app_obj.default_video_name:

            if self.video_obj.source is not None:

                # Using pango markup to display a URL is too risky, so just use
                #   ordinary text
                self.name_label.set_text(
                    utils.shorten_string(
                        self.video_obj.source,
                        self.main_win_obj.quite_long_string_max_len,
                    ),
                )

                return

            else:

                # No URL to show, so we're forced to use '(video with no name)'
                name = self.main_win_obj.app_obj.default_video_name

        string = ''
        if self.video_obj.new_flag:
            string += ' font_weight="bold"'

        if self.video_obj.dl_sim_flag:
            string += ' style="italic"'

        self.name_label.set_markup(
            '<span font_size="large"' + string + '>' + \
            html.escape(
                utils.shorten_string(
                    name,
                    self.main_win_obj.quite_long_string_max_len,
                ),
                quote=True,
            ) + '</span>'
        )


    def update_status_images(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the Gtk.Image widgets to display the video's download status,
        error and warning settings.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 25694 update_status_images')

        # Special case: don't display any images at all, if the flag is not
        #   set
        if not self.main_win_obj.app_obj.catalogue_draw_icons_flag:
            self.status_image.clear()
            self.comment_image.clear()
            self.subs_image.clear()
            self.slice_image.clear()
            self.stamp_image.clear()
            self.warning_image.clear()
            self.error_image.clear()
            self.options_image.clear()

        else:

            # Set the download status
            if self.video_obj.live_mode == 1:

                if not self.video_obj.live_debut_flag:

                    self.status_image.set_from_pixbuf(
                        self.main_win_obj.pixbuf_dict['live_wait_small'],
                    )

                else:

                    self.status_image.set_from_pixbuf(
                        self.main_win_obj.pixbuf_dict['debut_wait_small'],
                    )

            elif self.video_obj.live_mode == 2:

                if not self.video_obj.live_debut_flag:

                    self.status_image.set_from_pixbuf(
                        self.main_win_obj.pixbuf_dict['live_now_small'],
                    )

                else:

                    self.status_image.set_from_pixbuf(
                        self.main_win_obj.pixbuf_dict['debut_now_small'],
                    )

            elif self.video_obj.dl_flag:

                if self.video_obj.archive_flag:

                    self.status_image.set_from_pixbuf(
                        self.main_win_obj.pixbuf_dict['archived_small'],
                    )

                elif self.video_obj.split_flag:

                    self.status_image.set_from_pixbuf(
                        self.main_win_obj.pixbuf_dict['split_file_small'],
                    )

                elif self.video_obj.was_live_flag:

                    self.status_image.set_from_pixbuf(
                        self.main_win_obj.pixbuf_dict['live_old_small'],
                    )

                else:

                    self.status_image.set_from_pixbuf(
                        self.main_win_obj.pixbuf_dict['have_file_small'],
                    )

            elif self.video_obj.was_live_flag:

                self.status_image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['live_old_no_file_small'],
                )

            else:

                self.status_image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['no_file_small'],
                )

            # Set the remaining status icons
            # To prevent an unsightly gap between these images, use the first
            #   available Gtk.Image
            image_list = [
                self.comment_image,
                self.subs_image,
                self.slice_image,
                self.stamp_image,
                self.warning_image,
                self.error_image,
                self.options_image,
            ]

            if self.video_obj.comment_list:
                image = image_list.pop(0)
                image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['comment_small'],
                )

            if self.video_obj.subs_list:
                image = image_list.pop(0)
                image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['subs_small'],
                )

            if self.video_obj.slice_list:
                image = image_list.pop(0)
                image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['slice_small'],
                )

            if self.video_obj.stamp_list:
                image = image_list.pop(0)
                image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['stamp_small'],
                )

            if self.video_obj.warning_list:
                image = image_list.pop(0)
                image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['warning_small'],
                )

            if self.video_obj.error_list:
                image = image_list.pop(0)
                image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['error_small'],
                )

            if self.video_obj.options_obj:
                image = image_list.pop(0)
                image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['dl_options_small'],
                )

            for image in image_list:
                image.clear()


    def update_video_descrip(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the Gtk.Label widget to display the video's current
        description.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 25845 update_video_descrip')

        if self.main_win_obj.app_obj.catalogue_mode == 'complex_hide_parent' \
        or self.main_win_obj.app_obj.catalogue_mode \
        == 'complex_hide_parent_ext':

            # Show the first line of the video description, or all of it,
            #   depending on settings
            if self.video_obj.short:

                # Work with a list of lines, displaying either the fist line,
                #   or all of them, as the user clicks the More/Less button
                line_list = self.video_obj.descrip.splitlines()

                if not self.expand_descrip_flag:

                    string = html.escape(
                        utils.shorten_string(
                            line_list[0],
                            self.main_win_obj.very_long_string_max_len,
                        ),
                        quote=True,
                    )

                    if len(line_list) > 1:
                        self.descrip_label.set_markup(
                            '<a href="more" title="' \
                            + _('Show the full description') \
                            + '">' + _('More') + '</a>   ' + string,
                        )
                    else:
                        self.descrip_label.set_text(string)

                else:

                    descrip = html.escape(self.video_obj.descrip, quote=True)

                    if len(line_list) > 1:
                        self.descrip_label.set_markup(
                            '<a href="less" title="' \
                            + _('Show the short description') \
                            + '">' + _('Less') + '</a>   ' + descrip + '\n',
                        )
                    else:
                        self.descrip_label.set_text(descrip)

            else:
                self.descrip_label.set_markup('<i>No description set</i>')

        else:

            # Show the name of the parent channel/playlist/folder, optionally
            #   followed by the whole video description, depending on settings
            if self.video_obj.orig_parent is not None:

                string = _('Originally from:') + ' \''

                string += html.escape(
                    utils.shorten_string(
                        self.video_obj.orig_parent,
                        self.main_win_obj.very_long_string_max_len,
                    ),
                    quote=True,
                ) + '\''

            else:

                string = '<i>'
                if isinstance(self.video_obj.parent_obj, media.Channel):
                    string += _('From channel')
                elif isinstance(self.video_obj.parent_obj, media.Playlist):
                    string += _('From playlist')
                else:
                    string += _('From folder')

                string += '</i>: ' + html.escape(
                    utils.shorten_string(
                        self.video_obj.parent_obj.name,
                        self.main_win_obj.long_string_max_len,
                    ),
                    quote=True,
                )

            if not self.video_obj.descrip:
                self.descrip_label.set_markup(string)

            elif not self.expand_descrip_flag:

                self.descrip_label.set_markup(
                    '<a href="more" title="' \
                    + _('Show the full description') \
                    + '">' + _('More') + '</a>   ' + string,
                )

            else:

                descrip = html.escape(self.video_obj.descrip, quote=True)
                self.descrip_label.set_markup(
                    '<a href="less" title="' \
                    + _('Show the short description') \
                    + '">' + _('Less') + '</a>   ' + descrip + '\n',
                )


    def update_video_stats(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the Gtk.Label widget to display the video's current side/
        duration/date information.

        For livestreams, instead displays livestream options.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 25960 update_video_stats')

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

        if not self.video_obj.live_mode:

            if self.video_obj.duration is not None:
                string = _('Duration:') + ' ' \
                    + utils.convert_seconds_to_string(
                        self.video_obj.duration,
                        True,
                    )

            else:
                string = _('Duration:') + ' <i>' + _('unknown') + '</i>'

            size = self.video_obj.get_file_size_string()
            if size != "":
                string = string + '  -  ' + _('Size:') + ' ' + size
            else:
                string = string + '  -  ' + _('Size:') + ' <i>' \
                + _('unknown') + '</i>'

            pretty_flag = self.main_win_obj.app_obj.show_pretty_dates_flag
            if app_obj.catalogue_sort_mode == 'receive':

                date = self.video_obj.get_receive_date_string(pretty_flag)
                text = _('Received:')

            else:

                date = self.video_obj.get_upload_date_string(pretty_flag)
                text = _('Date:')

            if date is not None:
                string = string + '  -  ' + text + ' ' + date
            else:
                string = string + '  -  ' + text + ' <i>' + _('unknown') \
                + '</i>'

            self.stats_label.set_markup(string)

            self.live_auto_notify_label.set_text('')
            self.live_auto_alarm_label.set_text('')
            self.live_auto_open_label.set_text('')
            self.live_auto_dl_start_label.set_text('')
            self.live_auto_dl_stop_label.set_text('')

        else:

            name = html.escape(self.video_obj.name)
            dbid = self.video_obj.dbid

            if not self.video_obj.live_debut_flag:

                if self.video_obj.live_mode == 2:
                    self.stats_label.set_markup(_('Live now:') + '   ')
                elif self.video_obj.live_msg == '':
                    self.stats_label.set_markup(_('Live soon:') + '   ')
                else:
                    self.stats_label.set_markup(
                        self.video_obj.live_msg + ':   ',
                    )

            else:

                if self.video_obj.live_mode == 2:
                    self.stats_label.set_markup(_('Debut now:') + '   ')
                elif self.video_obj.live_msg == '':
                    self.stats_label.set_markup(_('Debut soon:') + '   ')
                else:
                    self.stats_label.set_markup(
                        self.video_obj.live_msg + ':   ',
                    )

            if dbid in app_obj.media_reg_auto_notify_dict:
                label = '<s>' + _('Notify') + '</s>'
            else:
                label = _('Notify')

            # Currently disabled on MS Windows
            if os.name == 'nt':
                self.live_auto_notify_label.set_markup(_('Notify'))
            else:
                self.live_auto_notify_label.set_markup(
                    '<a href="' + name + '" title="' \
                    + _('When the livestream starts, notify the user') \
                    + '">' + label + '</a>',
                )

            if not mainapp.HAVE_PLAYSOUND_FLAG:

                self.live_auto_alarm_label.set_markup('Alarm')

            else:

                if dbid in app_obj.media_reg_auto_alarm_dict:
                    label = '<s>' + _('Alarm') + '</s>'
                else:
                    label = _('Alarm')

                self.live_auto_alarm_label.set_markup(
                    '<a href="' + name + '" title="' \
                    + _('When the livestream starts, sound an alarm') \
                    + '">' + label + '</a>',
                )

            if dbid in app_obj.media_reg_auto_open_dict:
                label = '<s>' + _('Open') + '</s>'
            else:
                label = _('Open')

            self.live_auto_open_label.set_markup(
                '<a href="' + name + '" title="' \
                + _('When the livestream starts, open it') \
                + '">' + label + '</a>',
            )

            if __main__.__pkg_no_download_flag__ \
            or self.video_obj.live_mode == 2:

                # (Livestream already broadcasting)
                self.live_auto_dl_start_label.set_markup(_('D/L on start'))

            else:

                if dbid in app_obj.media_reg_auto_dl_start_dict:
                    label = '<s>' + _('D/L on start') + '</s>'
                else:
                    label = _('D/L on start')

                self.live_auto_dl_start_label.set_markup(
                    '<a href="' + name + '" title="' \
                    + _('When the livestream starts, download it') \
                    + '">' + label + '</a>',
                )

            if __main__.__pkg_no_download_flag__:

                self.live_auto_dl_stop_label.set_markup(_('D/L on stop'))

            else:

                if dbid in app_obj.media_reg_auto_dl_stop_dict:
                    label = '<s>' + _('D/L on stop') + '</s>'
                else:
                    label = _('D/L on stop')

                self.live_auto_dl_stop_label.set_markup(
                    '<a href="' + name + '" title="' \
                    + _('When the livestream stops, download it') \
                    + '">' + label + '</a>',
                )


    def update_watch_player(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the clickable Gtk.Label widget for watching the video in an
        external media player.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 26125 update_watch_player')

        if self.video_obj.file_name:
            watch_text = '<a href="' + html.escape(
                self.video_obj.get_actual_path(self.main_win_obj.app_obj),
            ) + '" title="' + _('Watch in your media player') + '">' \
            + _('Player') + '</a>'

        # (Many labels are not clickable when a channel/playlist/folder's
        #   external directory is marked disabled)
        if self.video_obj.parent_obj.dbid \
        in self.main_win_obj.app_obj.container_unavailable_dict:

            # Link not clickable
            self.watch_player_label.set_markup(_('Download'))

        elif __main__.__pkg_no_download_flag__:

            if self.video_obj.file_name and self.video_obj.dl_flag:

                # Link clickable
                self.watch_player_label.set_markup(watch_text)

            else:

                # Link not clickable
                self.watch_player_label.set_markup(_('Download'))

        elif self.video_obj.live_mode == 1:

            # Link not clickable
            self.watch_player_label.set_markup(_('Download'))

        elif self.video_obj.live_mode == 2:

            # Link clickable
            self.watch_player_label.set_markup(
                '<a href="' + html.escape(self.video_obj.source) \
                + '" title="' + _('Download this video') + '">' \
                + _('Download') + '</a>',
            )

        elif self.video_obj.file_name and self.video_obj.dl_flag:

            # Link clickable
            self.watch_player_label.set_markup(watch_text)

        elif self.video_obj.source \
        and not self.main_win_obj.app_obj.update_manager_obj \
        and not self.main_win_obj.app_obj.refresh_manager_obj \
        and not self.main_win_obj.app_obj.process_manager_obj:

            ignore_me = _(
                'TRANSLATOR\'S NOTE: If you want to use &, use &amp;' \
                + ' - if you want to use a different word (e.g. French et)' \
                + ', then just use that word',
            )

            # Link clickable
            self.watch_player_label.set_markup(
                '<a href="' + html.escape(self.video_obj.source) \
                + '" title="' + _('Download and watch in your media player') \
                + '">' + _('Download &amp; watch') + '</a>',
            )

        else:

            # Link not clickable
            self.watch_player_label.set_markup(
                '<i>' + _('Not downloaded') + '</i>',
            )


    def update_watch_web(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the clickable Gtk.Label widget for watching the video in an
        external web browser.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 26207 update_watch_web')

        app_obj = self.main_win_obj.app_obj

        if self.video_obj.source:

            # For YouTube URLs, offer alternative links
            source = self.video_obj.source
            enhanced = utils.is_video_enhanced(self.video_obj)
            if not enhanced:

                # Link clickable
                self.watch_web_label.set_markup(
                    '<a href="' + html.escape(source, quote=True) \
                    + '" title="' + _('Watch on website') + '">' \
                    + _('Website') + '</a>',
                )

                # Links not clickable
                self.watch_hooktube_label.set_text('')
                self.watch_invidious_label.set_text('')
                self.watch_other_label.set_text('')

            elif enhanced != 'youtube':

                pretty = formats.ENHANCED_SITE_DICT[enhanced]['pretty_name']

                # Link clickable
                self.watch_web_label.set_markup(
                    '<a href="' + html.escape(source, quote=True) \
                    + '" title="' + _('Watch on {0}').format(pretty) + '">' \
                    + pretty + '</a>',
                )

                # Links not clickable
                self.watch_hooktube_label.set_text('')
                self.watch_invidious_label.set_text('')
                self.watch_other_label.set_text('')

            else:

                # Link clickable
                self.watch_web_label.set_markup(
                    '<a href="' + html.escape(source, quote=True) \
                    + '" title="' + _('Watch on YouTube') + '">' \
                    + _('YouTube') + '</a>',
                )

                if not self.video_obj.live_mode:

                    # Links clickable
                    self.watch_hooktube_label.set_markup(
                        '<a href="' \
                        + html.escape(
                            utils.convert_youtube_to_hooktube(source),
                            quote=True,
                        ) \
                        + '" title="' + _('Watch on HookTube') + '">' \
                        + _('HookTube') + '</a>',
                    )

                    self.watch_invidious_label.set_markup(
                        '<a href="' \
                        + html.escape(
                            utils.convert_youtube_to_invidious(
                                app_obj,
                                source,
                            ),
                            quote=True,
                        ) \
                        + '" title="' + _('Watch on Invidious') + '">' \
                        + _('Invidious') + '</a>',
                    )

                    if app_obj.general_custom_dl_obj.divert_mode == 'other' \
                    and app_obj.custom_dl_obj.divert_website is not None \
                    and len(app_obj.custom_dl_obj.divert_website) > 2:

                        # Link clickable
                        self.watch_other_label.set_markup(
                            '<a href="' \
                            + html.escape(
                                utils.convert_youtube_to_other(
                                    app_obj,
                                    source,
                                ),
                                quote=True,
                            ) \
                            + '" title="' \
                            + app_obj.custom_dl_obj.divert_website \
                            + '">' + _('Other') + '</a>',
                        )

                    else:

                        # Link not clickable
                        self.watch_other_label.set_text('')

                else:

                    # Links not clickable
                    self.watch_hooktube_label.set_text('')
                    self.watch_invidious_label.set_text('')
                    self.watch_other_label.set_text('')

        else:

            # Links not clickable
            self.watch_web_label.set_markup('<i>' + _('No link') + '</i>')
            self.watch_hooktube_label.set_text('')
            self.watch_invidious_label.set_text('')
            self.watch_other_label.set_text('')


    def update_livestream_labels(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the clickable Gtk.Label widget for video properties.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 26329 update_livestream_labels')

        name = html.escape(self.video_obj.name)
        app_obj = self.main_win_obj.app_obj
        dbid = self.video_obj.dbid

        # (Many labels are not clickable when a channel/playlist/folder's
        #   external directory is marked disabled)
        if self.video_obj.parent_obj.dbid \
        in self.main_win_obj.app_obj.container_unavailable_dict:
            unavailable_flag = True
        else:
            unavailable_flag = False

        # Notify/don't notify
        if not dbid in app_obj.media_reg_auto_notify_dict:
            label = _('Notify')
        else:
            label = '<s>' + _('Notify') + '</s>'

        # Currently disabled on MS Windows
        if os.name == 'nt' or unavailable_flag:
            self.live_auto_notify_label.set_markup(_('Notify'))
        else:
            self.live_auto_notify_label.set_markup(
                '<a href="' + name + '" title="' \
                + _('When the livestream starts, notify the user') \
                + '">' + label + '</a>',
            )

        # Sound alarm/don't sound alarm
        if not dbid in app_obj.media_reg_auto_alarm_dict:
            label = _('Alarm')
        else:
            label = '<s>' + _('Alarm') + '</s>'

        if unavailable_flag:
            self.live_auto_alarm_label.set_markup(_('Alarm'))
        else:
            self.live_auto_alarm_label.set_markup(
                '<a href="' + name + '" title="' \
                + _('When the livestream starts, sound an alarm') \
                + '">' + label + '</a>',
            )

        # Open/don't open
        if not dbid in app_obj.media_reg_auto_open_dict:
            label = _('Open')
        else:
            label = '<s>' + _('Open') + '</s>'

        if unavailable_flag:
            self.live_auto_open_label.set_markup(_('Open'))
        else:
            self.live_auto_open_label.set_markup(
                '<a href="' + name + '" title="' \
                + _('When the livestream starts, open it') \
                + '">' + label + '</a>',
            )

        # D/L on start/Don't download
        if not dbid in app_obj.media_reg_auto_dl_start_dict:
            label = _('D/L on start')
        else:
            label = '<s>' + _('D/L on start') + '</s>'

        if unavailable_flag:
            self.live_auto_dl_start_label.set_markup(_('D/L on start'))
        else:
            self.live_auto_dl_start_label.set_markup(
                '<a href="' + name + '" title="' \
                + _('When the livestream starts, download it') \
                + '">' + label + '</a>',
            )

        # D/L on stop/Don't download
        if not dbid in app_obj.media_reg_auto_dl_stop_dict:
            label = _('D/L on stop')
        else:
            label = '<s>' + _('D/L on stop') + '</s>'

        if unavailable_flag:
            self.live_auto_dl_stop_label.set_markup(_('D/L on stop'))
        else:
            self.live_auto_dl_stop_label.set_markup(
                '<a href="' + name + '" title="' \
                + _('When the livestream stops, download it') \
                + '">' + label + '</a>',
            )


    def update_temp_labels(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the clickable Gtk.Label widget for temporary video downloads.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 26428 update_temp_labels')

        # (Many labels are not clickable when a channel/playlist/folder's
        #   external directory is marked disabled)
        if self.video_obj.parent_obj.dbid \
        in self.main_win_obj.app_obj.container_unavailable_dict:
            unavailable_flag = True
        else:
            unavailable_flag = False

        if self.video_obj.file_name:
            link_text = self.video_obj.get_actual_path(
                self.main_win_obj.app_obj,
            )
        elif self.video_obj.source:
            link_text = self.video_obj.source
        else:
            link_text = ''

        # (Video can't be temporarily downloaded if it has no source URL)
        if self.video_obj.source is not None and not unavailable_flag:

            self.temp_mark_label.set_markup(
                '<a href="' + html.escape(link_text) + '" title="' \
                + _('Download to a temporary folder later') \
                + '">' + _('Mark for download') + '</a>',
            )

            self.temp_dl_label.set_markup(
                '<a href="' + html.escape(link_text) + '" title="' \
                + _('Download to a temporary folder') \
                + '">' + _('Download') + '</a>',
            )

            self.temp_dl_watch_label.set_markup(
                '<a href="' + html.escape(link_text) + '" title="' \
                + _('Download to a temporary folder, then watch') \
                + '">' + _('D/L &amp; watch') + '</a>',
            )

        else:

            self.temp_mark_label.set_text(_('Mark for download'))
            self.temp_dl_label.set_text(_('Download'))
            self.temp_dl_watch_label.set_text(_('D/L and watch'))


    def update_marked_labels(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the clickable Gtk.Label widget for video properties.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 26483 update_marked_labels')

        if self.video_obj.file_name:
            link_text = self.video_obj.get_actual_path(
                self.main_win_obj.app_obj,
            )
        elif self.video_obj.source:
            link_text = self.video_obj.source
        else:
            link_text = ''

        # Archived/not archived
        text = '<a href="' + html.escape(link_text) + '" title="' \
        + _('Prevent automatic deletion of the video') + '">'

        if not self.video_obj.archive_flag:
            self.marked_archive_label.set_markup(
                text + _('Archived') + '</a>',
            )
        else:
            self.marked_archive_label.set_markup(
                text + '<s>' + _('Archived') + '</s></a>',
            )

        # Bookmarked/not bookmarked
        text = '<a href="' + html.escape(link_text) + '" title="' \
        + _('Show video in Bookmarks folder') + '">'

        if not self.video_obj.bookmark_flag:
            self.marked_bookmark_label.set_markup(
#                text + _('Bookmarked') + '</a>',
                text + _('B/mark') + '</a>',
            )
        else:
            self.marked_bookmark_label.set_markup(
#                text + '<s>' + _('Bookmarked') + '</s></a>',
                text + '<s>' + _('B/mark') + '</s></a>',
            )

        # Favourite/not favourite
        text = '<a href="' + html.escape(link_text) + '" title="' \
        + _('Show in Favourite Videos folder') + '">'

        if not self.video_obj.fav_flag:
            self.marked_fav_label.set_markup(
                text + _('Favourite') + '</a>',
            )
        else:
            self.marked_fav_label.set_markup(
                text + '<s>' + _('Favourite') + '</s></a>')

        # Missing/not missing
        text = '<a href="' + html.escape(link_text) + '" title="' \
        + _('Mark video as removed by creator') + '">'

        if not self.video_obj.missing_flag:
            self.marked_missing_label.set_markup(
                text + _('Missing') + '</a>',
            )
        else:
            self.marked_missing_label.set_markup(
                text + '<s>' + _('Missing') + '</s></a>',
            )

        # New/not new
        text = '<a href="' + html.escape(link_text) + '" title="' \
        + _('Mark video as never watched') + '">'

        if not self.video_obj.new_flag:
            self.marked_new_label.set_markup(
                text + _('New') + '</a>',
            )
        else:
            self.marked_new_label.set_markup(
                text + '<s>' + _('New') + '</s></a>',
            )

        # In waiting list/not in waiting list
        text = '<a href="' + html.escape(link_text) + '" title="' \
        + _('Show in Waiting Videos folder') + '">'
        if not self.video_obj.waiting_flag:
            self.marked_waiting_label.set_markup(
                text + _('Waiting') + '</a>',
            )
        else:
            self.marked_waiting_label.set_markup(
                text + '<s>' + _('Waiting') + '</s></a>',
            )


    def enable_visible_frame(self, visible_flag):

        """Called by self.draw_widgets() and
        mainwin.MainWin.on_draw_frame_checkbutton_changed().

        Enables or disables the visible frame around the edge of the video.

        Args:

            visible_flag (bool): True to enable the frame, False to disable it

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 26587 enable_visible_frame')

        # Sanity check: don't let GridCatalogueItem use this inherited method;
        #   when displaying videos in a grid, the frame is visible (or not)
        #   around the mainwin.CatalogueGridBox object
        if not isinstance(self, ComplexCatalogueItem):
            return self.main_win_obj.app_obj.system_error(
                267,
                'CatalogueGridBox has no frame to toggle',
            )

        # (When we're still waiting to set the minimum width for a gridbox,
        #   then the frame must be visible)
        thumb_size = self.main_win_obj.app_obj.thumb_size_custom

        if visible_flag \
        or (
            self.main_win_obj.app_obj.catalogue_mode_type == 'grid'
            and self.main_win_obj.catalogue_grid_width_dict[thumb_size] is None
        ):
            self.frame.set_shadow_type(Gtk.ShadowType.IN)
        else:
            self.frame.set_shadow_type(Gtk.ShadowType.NONE)


    def temp_box_is_visible(self):

        """Called by self.draw_widgets and .update_widgets().

        Checks whether the fifth row of labels (for temporary actions) should
        be visible, or not.

        Return values:

            True if the row should be visible, False if not

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 26626 temp_box_is_visible')

        if __main__.__pkg_no_download_flag__:
            return False
        elif (
            self.main_win_obj.app_obj.catalogue_mode \
            == 'complex_hide_parent_ext' \
            or self.main_win_obj.app_obj.catalogue_mode \
            == 'complex_show_parent_ext'
        ) and not self.no_temp_widgets_flag \
        and not self.video_obj.live_mode:
            return True
        else:
            return False


    def marked_box_is_visible(self):

        """Called by self.draw_widgets and .update_widgets().

        Checks whether the sixth row of labels (for marked video actions)
        should be visible, or not.

        Return values:

            True if the row should be visible, False if not

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 26656 marked_box_is_visible')

        if (
            self.main_win_obj.app_obj.catalogue_mode \
            == 'complex_hide_parent_ext' \
            or self.main_win_obj.app_obj.catalogue_mode \
            == 'complex_show_parent_ext'
        ) and not self.video_obj.live_mode:
            return True
        else:
            return False


    # Callback methods


    def on_click_descrip_label(self, label, uri):

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

        When the user clicks on the More/Less label, show more or less of the
        video's description.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 26688 on_click_descrip_label')

        if not self.expand_descrip_flag:
            self.expand_descrip_flag = True
        else:
            self.expand_descrip_flag = False

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.descrip_label.set_text('')
        GObject.timeout_add(0, self.update_video_descrip)


    def on_click_live_auto_alarm_label(self, label, uri):

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

        Toggles auto-sounding alarms when a livestream starts.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 26722 on_click_live_auto_alarm_label')

        # Toggle the setting
        if not self.video_obj.dbid \
        in self.main_win_obj.app_obj.media_reg_auto_alarm_dict:
            self.main_win_obj.app_obj.add_auto_alarm_dict(self.video_obj)
        else:
            self.main_win_obj.app_obj.del_auto_alarm_dict(self.video_obj)

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.live_auto_alarm_label.set_markup(_('Alarm'))

        GObject.timeout_add(0, self.update_livestream_labels)

        return True


    def on_click_live_auto_dl_start_label(self, label, uri):

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

        Toggles auto-downloading the video when a livestream starts.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 26761 on_click_live_auto_dl_start_label')

        # Toggle the setting
        if not self.video_obj.dbid \
        in self.main_win_obj.app_obj.media_reg_auto_dl_start_dict:
            self.main_win_obj.app_obj.add_auto_dl_start_dict(self.video_obj)
        else:
            self.main_win_obj.app_obj.del_auto_dl_start_dict(self.video_obj)

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.live_auto_dl_start_label.set_markup(_('D/L on start'))

        GObject.timeout_add(0, self.update_livestream_labels)

        return True


    def on_click_live_auto_dl_stop_label(self, label, uri):


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

        Toggles auto-downloading the video when a livestream stops.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 26801 on_click_live_auto_dl_stop_label')

        # Toggle the setting
        if not self.video_obj.dbid \
        in self.main_win_obj.app_obj.media_reg_auto_dl_stop_dict:
            self.main_win_obj.app_obj.add_auto_dl_stop_dict(self.video_obj)
        else:
            self.main_win_obj.app_obj.del_auto_dl_stop_dict(self.video_obj)

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.live_auto_dl_stop_label.set_markup(_('D/L on stop'))

        GObject.timeout_add(0, self.update_livestream_labels)

        return True


    def on_click_live_auto_notify_label(self, label, uri):

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

        Toggles auto-notification when a livestream starts.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 26840 on_click_live_auto_notify_label')

        # Toggle the setting
        if not self.video_obj.dbid \
        in self.main_win_obj.app_obj.media_reg_auto_notify_dict:
            self.main_win_obj.app_obj.add_auto_notify_dict(self.video_obj)
        else:
            self.main_win_obj.app_obj.del_auto_notify_dict(self.video_obj)

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.live_auto_notify_label.set_markup(_('Notify'))

        GObject.timeout_add(0, self.update_livestream_labels)

        return True


    def on_click_live_auto_open_label(self, label, uri):

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

        Toggles auto-opening the video in the system's web browser when a
        livestream starts.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 26880 on_click_live_auto_open_label')

        # Toggle the setting
        if not self.video_obj.dbid \
        in self.main_win_obj.app_obj.media_reg_auto_open_dict:
            self.main_win_obj.app_obj.add_auto_open_dict(self.video_obj)
        else:
            self.main_win_obj.app_obj.del_auto_open_dict(self.video_obj)

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.live_auto_open_label.set_markup(_('Open'))

        GObject.timeout_add(0, self.update_livestream_labels)

        return True


    def on_click_marked_archive_label(self, label, uri):

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

        Mark the video as archived or not archived.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 26919 on_click_marked_archive_label')

        # Mark the video as archived/not archived
        if not self.video_obj.archive_flag:
            self.video_obj.set_archive_flag(True)
        else:
            self.video_obj.set_archive_flag(False)

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.marked_archive_label.set_markup(_('Archived'))

        GObject.timeout_add(0, self.update_marked_labels)

        return True


    def on_click_marked_bookmark_label(self, label, uri):

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

        Mark the video as bookmarked or not bookmarked.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 26957 on_click_marked_bookmark_label')

        # Mark the video as bookmarked/not bookmarked
        if not self.video_obj.bookmark_flag:
            self.main_win_obj.app_obj.mark_video_bookmark(
                self.video_obj,
                True,
            )

        else:
            self.main_win_obj.app_obj.mark_video_bookmark(
                self.video_obj,
                False,
            )

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.marked_bookmark_label.set_markup(_('Bookmarked'))

        GObject.timeout_add(0, self.update_marked_labels)

        return True


    def on_click_marked_fav_label(self, label, uri):

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

        Mark the video as favourite or not favourite.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 27002 on_click_marked_fav_label')

        # Mark the video as favourite/not favourite
        if not self.video_obj.fav_flag:
            self.main_win_obj.app_obj.mark_video_favourite(
                self.video_obj,
                True,
            )

        else:
            self.main_win_obj.app_obj.mark_video_favourite(
                self.video_obj,
                False,
            )

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.marked_fav_label.set_markup(_('Favourite'))

        GObject.timeout_add(0, self.update_marked_labels)

        return True


    def on_click_marked_missing_label(self, label, uri):

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

        Mark the video as missing or not missing.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 27047 on_click_marked_missing_label')

        # Mark the video as missing/not missing
        if not self.video_obj.missing_flag:
            self.main_win_obj.app_obj.mark_video_missing(
                self.video_obj,
                True,
            )

        else:
            self.main_win_obj.app_obj.mark_video_missing(
                self.video_obj,
                False,
            )

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.marked_missing_label.set_markup(_('Missing'))

        GObject.timeout_add(0, self.update_marked_labels)

        return True


    def on_click_marked_new_label(self, label, uri):

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

        Mark the video as new or not new.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 27092 on_click_marked_new_label')

        # Mark the video as new/not new
        if not self.video_obj.new_flag:
            self.main_win_obj.app_obj.mark_video_new(self.video_obj, True)
        else:
            self.main_win_obj.app_obj.mark_video_new(self.video_obj, False)

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.marked_new_label.set_markup(_('New'))

        GObject.timeout_add(0, self.update_marked_labels)

        return True


    def on_click_marked_waiting_list_label(self, label, uri):

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

        Mark the video as in the waiting list or not in the waiting list.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 27130 on_click_marked_waiting_list_label')

        # Mark the video as in waiting list/not in waiting list
        if not self.video_obj.waiting_flag:
            self.main_win_obj.app_obj.mark_video_waiting(
                self.video_obj,
                True,
            )

        else:
            self.main_win_obj.app_obj.mark_video_waiting(
                self.video_obj,
                False,
            )

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.marked_waiting_label.set_markup(_('Waiting'))

        GObject.timeout_add(0, self.update_marked_labels)

        return True


    def on_click_temp_dl_label(self, label, uri):

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

        Download the video into the 'Temporary Videos' folder.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 27175 on_click_temp_dl_label')

        # Can't download the video if an update/refresh/info/tidy/process
        #   operation is in progress
        if not self.main_win_obj.app_obj.update_manager_obj \
        and not self.main_win_obj.app_obj.refresh_manager_obj \
        and not self.main_win_obj.app_obj.info_manager_obj \
        and not self.main_win_obj.app_obj.tidy_manager_obj \
        and not self.main_win_obj.app_obj.process_manager_obj:

            # Create a new media.Video object in the 'Temporary Videos' folder
            new_media_data_obj = self.main_win_obj.app_obj.add_video(
                self.main_win_obj.app_obj.fixed_temp_folder,
                self.video_obj.source,
            )

            if new_media_data_obj:

                # Download the video. If a download operation is already in
                #   progress, the video is added to it
                # Optionally open the video in the system's default media
                #   player
                self.main_win_obj.app_obj.download_watch_videos(
                    [new_media_data_obj],
                    False,
                )

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.temp_dl_label.set_markup(_('Download'))
        GObject.timeout_add(0, self.update_temp_labels)

        return True


    def on_click_temp_dl_watch_label(self, label, uri):

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

        Download the video into the 'Temporary Videos' folder.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 27231 on_click_temp_dl_watch_label')

        # Can't download the video if an update/refresh/tidy/process operation
        #   is in progress
        if not self.main_win_obj.app_obj.update_manager_obj \
        and not self.main_win_obj.app_obj.refresh_manager_obj \
        and not self.main_win_obj.app_obj.tidy_manager_obj \
        and not self.main_win_obj.app_obj.process_manager_obj:

            # Create a new media.Video object in the 'Temporary Videos' folder
            new_media_data_obj = self.main_win_obj.app_obj.add_video(
                self.main_win_obj.app_obj.fixed_temp_folder,
                self.video_obj.source,
            )

            if new_media_data_obj:

                # Download the video. If a download operation is already in
                #   progress, the video is added to it
                # Optionally open the video in the system's default media
                #   player
                self.main_win_obj.app_obj.download_watch_videos(
                    [new_media_data_obj],
                    True,
                )

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.temp_dl_watch_label.set_markup(_('D/L and watch'))
        GObject.timeout_add(0, self.update_temp_labels)

        return True


    def on_click_temp_mark_label(self, label, uri):

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

        Mark the video for download into the 'Temporary Videos' folder.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 27286 on_click_temp_mark_label')

        # Can't mark the video for download if an update/refresh/tidy/process
        #   operation is in progress
        if not self.main_win_obj.app_obj.update_manager_obj \
        and not self.main_win_obj.app_obj.refresh_manager_obj \
        and not self.main_win_obj.app_obj.tidy_manager_obj \
        and not self.main_win_obj.app_obj.process_manager_obj:

            # Create a new media.Video object in the 'Temporary Videos' folder
            new_media_data_obj = self.main_win_obj.app_obj.add_video(
                self.main_win_obj.app_obj.fixed_temp_folder,
                self.video_obj.source,
            )

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.temp_mark_label.set_markup(_('Mark for download'))
        GObject.timeout_add(0, self.update_temp_labels)

        return True


    def on_click_watch_hooktube_label(self, label, uri):

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

        Watch a YouTube video on HookTube.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 27330 on_click_watch_hooktube_label')

        # Launch the video
        utils.open_file(self.main_win_obj.app_obj, uri)

        # Mark the video as not new (having been watched)
        if self.video_obj.new_flag:
            self.main_win_obj.app_obj.mark_video_new(self.video_obj, False)
        # Remove the video from the waiting list (having been watched)
        if self.video_obj.waiting_flag:
            self.main_win_obj.app_obj.mark_video_waiting(
                self.video_obj,
                False,
            )

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.watch_hooktube_label.set_markup(_('HookTube'))
        GObject.timeout_add(0, self.update_watch_web)

        return True


    def on_click_watch_invidious_label(self, label, uri):

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

        Watch a YouTube video on Invidious.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 27374 on_click_watch_invidious_label')

        # Launch the video
        utils.open_file(self.main_win_obj.app_obj, uri)

        # Mark the video as not new (having been watched)
        if self.video_obj.new_flag:
            self.main_win_obj.app_obj.mark_video_new(self.video_obj, False)
        # Remove the video from the waiting list (having been watched)
        if self.video_obj.waiting_flag:
            self.main_win_obj.app_obj.mark_video_waiting(
                self.video_obj,
                False,
            )

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.watch_invidious_label.set_markup(_('Invidious'))
        GObject.timeout_add(0, self.update_watch_web)

        return True


    def on_click_watch_other_label(self, label, uri):

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

        Watch a YouTube video on the other YouTube front end (specified by the
        user).

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 27419 on_click_watch_other_label')

        # Launch the video
        utils.open_file(self.main_win_obj.app_obj, uri)

        # Mark the video as not new (having been watched)
        if self.video_obj.new_flag:
            self.main_win_obj.app_obj.mark_video_new(self.video_obj, False)
        # Remove the video from the waiting list (having been watched)
        if self.video_obj.waiting_flag:
            self.main_win_obj.app_obj.mark_video_waiting(
                self.video_obj,
                False,
            )

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.watch_other_label.set_markup(_('Other'))
        GObject.timeout_add(0, self.update_watch_web)

        return True


    def on_click_watch_player_label(self, label, uri):

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

        Watch a video using the system's default media player, first checking
        that a file actually exists.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 27464 on_click_watch_player_label')

        if self.video_obj.live_mode == 2:

            # Download the video. If a download operation is in progress, the
            #   video is added to it
            app_obj = self.main_win_obj.app_obj

            # If the livestream was downloaded when it was still broadcasting,
            #   then a new download must overwrite the original file
            # As of April 2023, the youtube-dl --yes-overwrites option has been
            #   removed (but yt-dlp provides --force-overwrites)
            # To keep things consistent for all forks, we will rename the
            #   original file (in case the download fails)
            app_obj.prepare_overwrite_video(self.video_obj)

            if not app_obj.download_manager_obj:

                # Start a new download operation
                app_obj.download_manager_start(
                    'real',
                    False,
                    [ self.video_obj ],
                )

            else:

                # Download operation already in progress
                return_list \
                = app_obj.download_manager_obj.download_list_obj.create_item(
                    self.video_obj,
                    None,               # media.Scheduled object
                    'real',             # override_operation_type
                    False,              # priority_flag
                    False,              # ignore_limits_flag
                )

                for download_item_obj in return_list:

                    # Add a row to the Progress List
                    self.main_win_obj.progress_list_add_row(
                        download_item_obj.item_id,
                        download_item_obj.media_data_obj,
                    )

                    # Update the main window's progress bar
                    app_obj.download_manager_obj.nudge_progress_bar()

        elif not self.video_obj.dl_flag and self.video_obj.source \
        and not self.main_win_obj.app_obj.update_manager_obj \
        and not self.main_win_obj.app_obj.refresh_manager_obj \
        and not self.main_win_obj.app_obj.process_manager_obj:

            # Download the video, and mark it to be opened in the system's
            #   default media player as soon as the download operation is
            #   complete
            # If a download operation is already in progress, the video is
            #   added to it
            self.main_win_obj.app_obj.download_watch_videos( [self.video_obj] )

        else:

            # Launch the video in the system's media player
            self.main_win_obj.app_obj.watch_video_in_player(self.video_obj)

            # Mark the video as not new (having been watched)
            if self.video_obj.new_flag:
                self.main_win_obj.app_obj.mark_video_new(self.video_obj, False)
            # Remove the video from the waiting list (having been watched)
            if self.video_obj.waiting_flag:
                self.main_win_obj.app_obj.mark_video_waiting(
                    self.video_obj,
                    False,
                )

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.watch_player_label.set_markup(_('Player'))
        GObject.timeout_add(0, self.update_watch_player)

        return True


    def on_click_watch_web_label(self, label, uri):

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

        Watch a video on its primary website.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 27568 on_click_watch_web_label')

        # Launch the video
        utils.open_file(self.main_win_obj.app_obj, uri)

        # Mark the video as not new (having been watched)
        if self.video_obj.new_flag:
            self.main_win_obj.app_obj.mark_video_new(self.video_obj, False)
        # Remove the video from the waiting list (having been watched)
        if self.video_obj.waiting_flag:
            self.main_win_obj.app_obj.mark_video_waiting(
                self.video_obj,
                False,
            )

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        enhanced = utils.is_video_enhanced(self.video_obj)
        if not enhanced:
            self.watch_web_label.set_markup(_('Website'))
        else:
            self.watch_web_label.set_markup(
                formats.ENHANCED_SITE_DICT[enhanced]['pretty_name'],
            )

        GObject.timeout_add(0, self.update_watch_web)

        return True


    def on_right_click_row(self, event_box, event):

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

        When the user right-clicks an the box comprising this
        ComplexCatalogueItem, create a context-sensitive popup menu.

        When the user right-clicks an a row, create a context-sensitive popup
        menu.

        Args:

            event_box (Gtk.EventBox), event (Gtk.EventButton): Data from the
                signal emitted by the click

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 27618 on_right_click_row')

        if event.type == Gdk.EventType.BUTTON_PRESS and event.button == 3:

            self.main_win_obj.video_catalogue_popup_menu(event, self.video_obj)


class GridCatalogueItem(ComplexCatalogueItem):

    """Called by MainWin.video_catalogue_redraw_all() and
    .video_catalogue_insert_video().

    Python class that handles a single gridbox in the Video Catalogue.

    Each mainwin.GridCatalogueItem object stores widgets used in that gridbox,
    and updates them when required.

    Args:

        main_win_obj (mainwin.MainWin): The main window object

        video_obj (media.Video): The media data object itself (always a video)

    """


    # Standard class methods


    def __init__(self, main_win_obj, video_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 27650 __init__')

        # IV list - class objects
        # -----------------------
        # The main window object
        self.main_win_obj = main_win_obj
        # The media data object itself (always a video)
        self.video_obj = video_obj


        # IV list - Gtk widgets
        # ---------------------
        self.catalogue_gridbox = None       # mainwin.CatalogueGridBox
        self.grid = None                    # Gtk.Grid
        self.thumb_box = None               # Gtk.HBox
        self.thumb_image = None             # Gtk.Image
        self.status_hbox = None             # Gtk.HBox
        self.status_vbox = None             # Gtk.VBox
        self.status_image = None            # Gtk.Image
        self.comment_image = None           # Gtk.Image
        self.subs_image = None              # Gtk.Image
        self.slice_image = None             # Gtk.Image
        self.stamp_image = None             # Gtk.Image
        self.warning_image = None           # Gtk.Image
        self.error_image = None             # Gtk.Image
        self.options_image = None           # Gtk.Image
        self.grid2 = None                   # Gtk.Grid
        self.name_label = None              # Gtk.Label
        self.container_label = None         # Gtk.Label
        self.grid3 = None                   # Gtk.Grid
        self.live_auto_notify_label = None  # Gtk.Label
        self.live_auto_alarm_label = None   # Gtk.Label
        self.live_auto_open_label = None    # Gtk.Label
        self.live_auto_dl_start_label = None
                                            # Gtk.Label
        self.live_auto_dl_stop_label = None # Gtk.Label
        self.grid4 = None                   # Gtk.Grid
        self.watch_player_label = None      # Gtk.Label
        self.watch_web_label = None         # Gtk.Label
        self.watch_hooktube_label = None    # Gtk.Label
        self.watch_invidious_label = None   # Gtk.Label
        self.watch_other_label = None       # Gtk.Label
        self.grid5 = None                   # Gtk.Grid
        self.temp_mark_label = None         # Gtk.Label
        self.temp_dl_label = None           # Gtk.Label
        self.temp_dl_watch_label = None     # Gtk.Label
        self.grid6 = None                   # Gtk.Grid
        self.marked_archive_label = None    # Gtk.Label
        self.marked_bookmark_label = None   # Gtk.Label
        self.marked_fav_label = None        # Gtk.Label
        self.marked_missing_label = None    # Gtk.Label
        self.marked_new_label = None        # Gtk.Label
        self.marked_waiting_label = None    # Gtk.Label


        # IV list - other
        # ---------------
        # Unique ID for this object, matching the .dbid for self.video_obj (an
        #   integer)
        self.dbid = video_obj.dbid
        # Size (in pixels) of gaps between various widgets
        self.spacing_size = 5
        # Flag set to True if the video's parent folder is a temporary folder,
        #   meaning that some widgets don't need to be drawn at all
        self.no_temp_widgets_flag = False

        # Whenever self.draw_widgets() or .update_widgets() is called, the
        #   background colour might be changed
        # This IV shows the value of the self.video_obj.live_mode, the last
        #   time either of those functions was called. If the value has
        #   actually changed, then we ask Gtk to change the background
        #   (otherwise, we don't)
        self.previous_live_mode = 0
        # Flag set to True when the temporary labels box (self.temp_box) is
        #   visible, False when not
        self.temp_box_visible_flag = False
        # Flag set to True when the marked labels box (self.marked_box) is
        #   visible, False when not
        self.marked_box_visible_flag = False

        # We can't select widgets on a Gtk.Grid directly, so Tartube implements
        #   its own 'selection' mechanism
        self.selected_flag = False


    # Public class methods


    def draw_widgets(self, catalogue_gridbox):

        """Called by mainwin.MainWin.video_catalogue_redraw_all() and
        .video_catalogue_insert_video().

        After a Gtk.Frame has been created for this object, populate it with
        widgets.

        Args:

            catalogue_gridbox (mainwin.CatalogueGridBox): A wrapper for a
                Gtk.Frame object, storing the media.Video object displayed in
                that row.

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 27755 draw_widgets')

        # If the video's parent folder is a temporary folder, then we don't
        #   need one row of widgets at all
        parent_obj = self.video_obj.parent_obj
        if isinstance(parent_obj, media.Folder) \
        and parent_obj.temp_flag:
            self.no_temp_widgets_flag = True
        else:
            self.no_temp_widgets_flag = False

        # Draw the widgets
        self.catalogue_gridbox = catalogue_gridbox

        event_box = Gtk.EventBox()
        self.catalogue_gridbox.add(event_box)
        event_box.connect('button-release-event', self.on_click_box)

        self.grid = Gtk.Grid()
        event_box.add(self.grid)
        self.grid.set_border_width(self.spacing_size)

        # Highlight livestreams by specifying a background colour
        self.update_background()

        # First row - thumbnail image and status/error/warning icons
        self.thumb_box = Gtk.HBox()
        self.grid.attach(self.thumb_box, 0, 0, 1, 1)
        self.thumb_box.set_hexpand(True)
        self.thumb_box.set_vexpand(False)
        # (Grid looks better with a small gap under the thumbnail)
        self.thumb_box.set_border_width(self.spacing_size)

        self.thumb_image = Gtk.Image()
        # Add extra spacing to the side of the image, so that the status icons
        #   can be placed there
        self.thumb_box.pack_start(
            self.thumb_image,
            True,
            True,
            (self.spacing_size * 4),
        )

        # Add a second box at the same grid location. This box contains the
        #   status icons, shifted to the far right. In this way, the
        #   thumbnail is still centred in the middle of the gridbox, and is
        #   not drawn over the top of the status icons
        self.status_hbox = Gtk.HBox()
        self.grid.attach(self.status_hbox, 0, 0, 1, 1)
        self.status_hbox.set_hexpand(True)
        self.status_hbox.set_vexpand(False)
        self.status_hbox.set_border_width(self.spacing_size)

        self.status_vbox = Gtk.VBox()
        self.status_hbox.pack_end(self.status_vbox, False, False, 0)

        self.status_image = Gtk.Image()
        self.status_vbox.pack_start(self.status_image,  False, False, 0)
        self.status_image.set_hexpand(False)

        self.comment_image = Gtk.Image()
        self.status_vbox.pack_start(
            self.comment_image,
            False,
            False,
            self.spacing_size,
        )
        self.comment_image.set_hexpand(False)

        self.subs_image = Gtk.Image()
        self.status_vbox.pack_start(self.subs_image, False, False, 0)
        self.subs_image.set_hexpand(False)

        self.slice_image = Gtk.Image()
        self.status_vbox.pack_start(
            self.slice_image,
            False,
            False,
            self.spacing_size,
        )
        self.slice_image.set_hexpand(False)

        self.stamp_image = Gtk.Image()
        self.status_vbox.pack_start(self.stamp_image, False, False, 0)
        self.stamp_image.set_hexpand(False)

        self.warning_image = Gtk.Image()
        self.status_vbox.pack_start(
            self.warning_image,
            False,
            False,
            self.spacing_size,
        )
        self.warning_image.set_hexpand(False)

        self.error_image = Gtk.Image()
        self.status_vbox.pack_start(self.error_image, False, False, 0)
        self.error_image.set_hexpand(False)

        self.options_image = Gtk.Image()
        self.status_vbox.pack_start(
            self.options_image,
            False,
            False,
            self.spacing_size,
        )
        self.options_image.set_hexpand(False)

        # Second row - video name
        # (Use sub-grids on several rows so that their column spacing remains
        #   independent of the others)
        self.grid2 = Gtk.Grid()
        self.grid.attach(self.grid2, 0, 1, 1, 1)
        self.grid2.set_column_spacing(self.spacing_size)

        self.name_label = Gtk.Label('', xalign = 0)
        self.grid2.attach(self.name_label, 0, 0, 1, 1)
        self.name_label.set_hexpand(True)

        # Third row - parent channel/playlist/folder name
        self.container_label = Gtk.Label('', xalign = 0)
        self.grid2.attach(self.container_label, 0, 1, 1, 1)
        self.container_label.set_hexpand(True)

        # Fourth row - video stats, or livestream notification options,
        #   depending on settings
        self.grid3 = Gtk.Grid()
        self.grid.attach(self.grid3, 0, 2, 1, 1)
        self.grid3.set_column_spacing(self.spacing_size)

        # (These labels are visible only for livestreams)
        # Auto-notify (this label doubles up as the label for video stats,
        #   when the video is not a livestream)
        self.live_auto_notify_label = Gtk.Label('', xalign=0)
        self.grid3.attach(self.live_auto_notify_label, 1, 0, 1, 1)
        self.live_auto_notify_label.connect(
            'activate-link',
            self.on_click_live_auto_notify_label,
        )

        # Auto-sound alarm
        self.live_auto_alarm_label = Gtk.Label('', xalign=0)
        self.grid3.attach(self.live_auto_alarm_label, 2, 0, 1, 1)
        self.live_auto_alarm_label.connect(
            'activate-link',
            self.on_click_live_auto_alarm_label,
        )

        # Auto-open
        self.live_auto_open_label = Gtk.Label('', xalign=0)
        self.grid3.attach(self.live_auto_open_label, 3, 0, 1, 1)
        self.live_auto_open_label.connect(
            'activate-link',
            self.on_click_live_auto_open_label,
        )

        # D/L on start
        self.live_auto_dl_start_label = Gtk.Label('', xalign=0)
        self.grid3.attach(self.live_auto_dl_start_label, 4, 0, 1, 1)
        self.live_auto_dl_start_label.connect(
            'activate-link',
            self.on_click_live_auto_dl_start_label,
        )

        # D/L on stop
        self.live_auto_dl_stop_label = Gtk.Label('', xalign=0)
        self.grid3.attach(self.live_auto_dl_stop_label, 5, 0, 1, 1)
        self.live_auto_dl_stop_label.connect(
            'activate-link',
            self.on_click_live_auto_dl_stop_label,
        )

        # Fifth row - Watch...
        self.grid4 = Gtk.Grid()
        self.grid.attach(self.grid4, 0, 3, 1, 1)
        self.grid4.set_column_spacing(self.spacing_size)

        # Watch in player
        self.watch_player_label = Gtk.Label('', xalign=0)
        self.grid4.attach(self.watch_player_label, 0, 0, 1, 1)
        self.watch_player_label.connect(
            'activate-link',
            self.on_click_watch_player_label,
        )

        # Watch on website/YouTube
        self.watch_web_label = Gtk.Label('', xalign=0)
        self.grid4.attach(self.watch_web_label, 1, 0, 1, 1)
        self.watch_web_label.connect(
            'activate-link',
            self.on_click_watch_web_label,
        )

        # Watch on HookTube
        self.watch_hooktube_label = Gtk.Label('', xalign=0)
        self.grid4.attach(self.watch_hooktube_label, 2, 0, 1, 1)
        self.watch_hooktube_label.connect(
            'activate-link',
            self.on_click_watch_hooktube_label,
        )

        # Watch on Invidious
        self.watch_invidious_label = Gtk.Label('', xalign=0)
        self.grid4.attach(self.watch_invidious_label, 3, 0, 1, 1)
        self.watch_invidious_label.connect(
            'activate-link',
            self.on_click_watch_invidious_label,
        )

        # Watch on the other YouTube front-end (specified by the user)
        self.watch_other_label = Gtk.Label('', xalign=0)
        self.grid4.attach(self.watch_other_label, 4, 0, 1, 1)
        self.watch_other_label.connect(
            'activate-link',
            self.on_click_watch_other_label,
        )

        # Optional rows

        # Sixth row: Temporary...
        self.grid5 = Gtk.Grid()
        if self.temp_box_is_visible():
            self.grid.attach(self.grid5, 0, 4, 1, 1)
            self.temp_box_visible_flag = True

        self.grid5.set_column_spacing(self.spacing_size)

        # Mark for download
        self.temp_mark_label = Gtk.Label('', xalign=0)
        self.grid5.attach(self.temp_mark_label, 0, 0, 1, 1)
        self.temp_mark_label.connect(
            'activate-link',
            self.on_click_temp_mark_label,
        )

        # Download
        self.temp_dl_label = Gtk.Label('', xalign=0)
        self.grid5.attach(self.temp_dl_label, 1, 0, 1, 1)
        self.temp_dl_label.connect(
            'activate-link',
            self.on_click_temp_dl_label,
        )

        # Download and watch
        self.temp_dl_watch_label = Gtk.Label('', xalign=0)
        self.grid5.attach(self.temp_dl_watch_label, 2, 0, 1, 1)
        self.temp_dl_watch_label.connect(
            'activate-link',
            self.on_click_temp_dl_watch_label,
        )

        # Seventh row: Marked...
        self.grid6 = Gtk.Grid()
        if self.marked_box_is_visible:
            self.grid.attach(self.grid6, 0, 5, 1, 1)
            self.marked_box_visible_flag = True

        self.grid6.set_column_spacing(self.spacing_size)

        # Archived/not archived
        self.marked_archive_label = Gtk.Label('', xalign=0)
        self.grid6.attach(self.marked_archive_label, 0, 0, 1, 1)
        self.marked_archive_label.connect(
            'activate-link',
            self.on_click_marked_archive_label,
        )

        # Bookmarked/not bookmarked
        self.marked_bookmark_label = Gtk.Label('', xalign=0)
        self.grid6.attach(self.marked_bookmark_label, 1, 0, 1, 1)
        self.marked_bookmark_label.connect(
            'activate-link',
            self.on_click_marked_bookmark_label,
        )

        # Favourite/not favourite
        self.marked_fav_label = Gtk.Label('', xalign=0)
        self.grid6.attach(self.marked_fav_label, 2, 0, 1, 1)
        self.marked_fav_label.connect(
            'activate-link',
            self.on_click_marked_fav_label,
        )

        # Missing/not missing
        self.marked_missing_label = Gtk.Label('', xalign=0)
        self.grid6.attach(self.marked_missing_label, 3, 0, 1, 1)
        self.marked_missing_label.connect(
            'activate-link',
            self.on_click_marked_missing_label,
        )

        # New/not new
        self.marked_new_label = Gtk.Label('', xalign=0)
        self.grid6.attach(self.marked_new_label, 4, 0, 1, 1)
        self.marked_new_label.connect(
            'activate-link',
            self.on_click_marked_new_label,
        )

        # In waiting list/not in waiting list
        self.marked_waiting_label = Gtk.Label('', xalign=0)
        self.grid6.attach(self.marked_waiting_label, 5, 0, 1, 1)
        self.marked_waiting_label.connect(
            'activate-link',
            self.on_click_marked_waiting_list_label,
        )


    def update_widgets(self):

        """Called by mainwin.MainWin.video_catalogue_redraw_all(),
        .video_catalogue_update_video() and .video_catalogue_insert_video().

        Sets the values displayed by each widget.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 28072 update_widgets')

        self.update_background()
        self.update_tooltips()
        self.update_thumb_image()
        self.update_status_images()
        self.update_video_name()
        self.update_container_name()
        self.update_video_stats()
        self.update_watch_player()
        self.update_watch_web()

        # If the fifth/sixth rows are not currently visible, but need to be
        #   visible, make them visible (and vice-versa)
        if not self.temp_box_is_visible():

            if self.temp_box_visible_flag:
                self.grid.remove(self.grid5)
                self.temp_box_visible_flag = False

        else:

            self.update_temp_labels()
            if not self.temp_box_visible_flag:
                self.grid.attach(self.grid5, 0, 4, 1, 1)
                self.temp_box_visible_flag = True

        if not self.marked_box_is_visible():

            if self.marked_box_visible_flag:
                self.grid.remove(self.grid6)
                self.marked_box_visible_flag = False

        else:

            self.update_marked_labels()
            if not self.marked_box_visible_flag:
                self.grid.attach(self.grid6, 0, 5, 1, 1)
                self.marked_box_visible_flag = True


    def update_background(self, force_flag=False):

        """Calledy by self.draw_widgets(), .update_widgets(), .do_select() and
        .toggle_select().

        Updates the background colour to show which videos are livestreams
        (but only when a video's livestream mode has changed).

        Note that calls to self.do_select() can also update the background
        colour.

        Args:

            force_flag (bool): True when called from self.do_select() and
                .toggle_select(), in which case the background is updated,
                regardless of whether the media.Video's .live_mode IV has
                changed

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 28134 update_background')

        if force_flag or self.previous_live_mode != self.video_obj.live_mode:

            self.previous_live_mode = self.video_obj.live_mode

            if not self.selected_flag:

                if self.video_obj.live_mode == 0 \
                or not self.main_win_obj.app_obj.livestream_use_colour_flag:

                    self.catalogue_gridbox.override_background_color(
                        Gtk.StateType.NORMAL,
                        None,
                    )

                elif self.video_obj.live_mode == 1:

                    if not self.video_obj.live_debut_flag \
                    or self.main_win_obj.app_obj.livestream_simple_colour_flag:

                        self.catalogue_gridbox.override_background_color(
                            Gtk.StateType.NORMAL,
                            self.main_win_obj.live_wait_colour,
                        )

                    else:

                        self.catalogue_gridbox.override_background_color(
                            Gtk.StateType.NORMAL,
                            self.main_win_obj.debut_wait_colour,
                        )

                elif self.video_obj.live_mode == 2:

                    if not self.video_obj.live_debut_flag \
                    or self.main_win_obj.app_obj.livestream_simple_colour_flag:

                        self.catalogue_gridbox.override_background_color(
                            Gtk.StateType.NORMAL,
                            self.main_win_obj.live_now_colour,
                        )

                    else:

                        self.catalogue_gridbox.override_background_color(
                            Gtk.StateType.NORMAL,
                            self.main_win_obj.debut_now_colour,
                        )

            else:

                # (For selected gridboxes, simplify the colour scheme by not
                #   distinguishing between debut and non-debut videos)
                if self.video_obj.live_mode == 0 \
                or not self.main_win_obj.app_obj.livestream_use_colour_flag:

                    self.catalogue_gridbox.override_background_color(
                        Gtk.StateType.NORMAL,
                        self.main_win_obj.grid_select_colour,
                    )

                elif self.video_obj.live_mode == 1:

                    self.catalogue_gridbox.override_background_color(
                        Gtk.StateType.NORMAL,
                        self.main_win_obj.grid_select_wait_colour,
                    )

                elif self.video_obj.live_mode == 2:

                    self.catalogue_gridbox.override_background_color(
                        Gtk.StateType.NORMAL,
                        self.main_win_obj.grid_select_live_colour,
                    )


    def update_tooltips(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the tooltips for the Gtk.Frame that contains everything.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 28219 update_tooltips')

        if self.main_win_obj.app_obj.show_tooltips_flag:
            self.catalogue_gridbox.set_tooltip_text(
                self.video_obj.fetch_tooltip_text(
                    self.main_win_obj.app_obj,
                    self.main_win_obj.tooltip_max_len,
                ),
            )


    def update_thumb_image(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the Gtk.Image widget to display the video's thumbnail, if
        available.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 28239 update_thumb_image')

        app_obj = self.main_win_obj.app_obj
        thumb_size = app_obj.thumb_size_custom
        gridbox_min_width \
        = self.main_win_obj.catalogue_grid_width_dict[thumb_size]

        # See if the video's thumbnail file has been downloaded
        thumb_flag = False
        if self.video_obj.file_name:

            # No way to know which image format is used by all websites for
            #   their video thumbnails, so look for the most common ones
            # The True argument means that if the thumbnail isn't found in
            #   Tartube's main data directory, look in the temporary directory
            #   too
            path = utils.find_thumbnail(
                self.main_win_obj.app_obj,
                self.video_obj,
                True,
            )

            if path:

                # Thumbnail file exists, so use it
                mini_list = app_obj.thumb_size_dict[thumb_size]

                # (Returns a tuple, who knows why)
                arglist = app_obj.file_manager_obj.load_to_pixbuf(
                    path,
                    mini_list[0],       # width
                    mini_list[1],       # height
                ),

                if arglist[0]:
                    self.thumb_image.set_from_pixbuf(arglist[0])
                    thumb_flag = True

        # No thumbnail file found, so use a default icon file
        if not thumb_flag:

            if not self.video_obj.block_flag:
                thumb_type = 'default'
            else:
                thumb_type = 'block'

            pixbuf_name = 'thumb_' + thumb_type + '_' + thumb_size
            self.thumb_image.set_from_pixbuf(
                self.main_win_obj.pixbuf_dict[pixbuf_name],
            )


    def update_video_name(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the Gtk.Label widget to display the video's current name.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 28299 update_video_name')

        app_obj = self.main_win_obj.app_obj
        thumb_size = app_obj.thumb_size_custom
        gridbox_min_width \
        = self.main_win_obj.catalogue_grid_width_dict[thumb_size]

        # For videos whose name is unknown, display the URL, rather than the
        #   usual '(video with no name)' string
        if not self.main_win_obj.app_obj.catalogue_show_nickname_flag:
            name = self.video_obj.name
        else:
            name = self.video_obj.nickname

        if name is None or name == app_obj.default_video_name:

            if self.video_obj.source is not None:

                # Using pango markup to display a URL is too risky, so just use
                #   ordinary text
                self.name_label.set_text(
                    utils.shorten_string(
                        self.video_obj.source,
                        self.main_win_obj.quite_long_string_max_len,
                    ),
                )

                return

            else:

                # No URL to show, so we're forced to use '(video with no name)'
                name = app_obj.default_video_name

        string = ''
        if self.video_obj.new_flag:
            string += ' font_weight="bold"'

        if self.video_obj.dl_sim_flag:
            string += ' style="italic"'

        # The video name is split into two lines, if there is enough text.
        #   Set the length of a the lines matching the size of the thumbnail
        if thumb_size == 'tiny':
            max_line_length = self.main_win_obj.medium_string_max_len
        elif thumb_size == 'small':
            max_line_length = self.main_win_obj.quite_long_string_max_len
        elif thumb_size == 'medium':
            max_line_length = self.main_win_obj.long_string_max_len
        elif thumb_size == 'large':
            max_line_length = self.main_win_obj.very_long_string_max_len
        else:
            max_line_length = self.main_win_obj.exceedingly_long_string_max_len

        self.name_label.set_markup(
            '<span font_size="large"' + string + '>' + \
            html.escape(
                utils.shorten_string_two_lines(
                    name,
                    max_line_length,
                ),
                quote=True,
            ) + '</span>'
        )


    def update_container_name(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the Gtk.Label widget to display the parent channel/playlist/
        folder name
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 28374 update_container_name')

        if self.video_obj.orig_parent is not None:
            parent_obj = self.video_obj.orig_parent
        else:
            parent_obj = self.video_obj.parent_obj

        if isinstance(parent_obj, media.Channel):
            string = _('Channel') + ': '
        elif isinstance(parent_obj, media.Playlist):
            string = _('Playlist') + ': '
        else:
            string = _('Folder') + ': '

        string2 = html.escape(
            utils.shorten_string(
                parent_obj.name,
                self.main_win_obj.quite_long_string_max_len,
            ),
            quote=True,
        )

        if isinstance(parent_obj, media.Folder) \
        or parent_obj.source is None \
        or not self.main_win_obj.app_obj.catalogue_clickable_container_flag:

            self.container_label.set_markup(
                '<i>' + string + '</i> ' + string2,
            )

        else:

            self.container_label.set_markup(
                '<i>' + string + '</i> <a href="' \
                + html.escape(parent_obj.source) + '" title="' \
                + _('Click to open') + '">' + string2 + '</a>',
            )


    def update_video_stats(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the Gtk.Label widget to display the video's current side/
        duration/date information.

        For livestreams, instead displays livestream options.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 28424 update_video_stats')

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

        if not self.video_obj.live_mode:

            if self.video_obj.duration is not None:
                string = utils.convert_seconds_to_string(
                    self.video_obj.duration,
                    True,
                )

            else:
                string = _('unknown')

            size = self.video_obj.get_file_size_string()
            if size != "":
                string = string + '  -  '  + size
            else:
                string = string + '  -  ' + _('unknown')

            pretty_flag = self.main_win_obj.app_obj.show_pretty_dates_flag
            if app_obj.catalogue_sort_mode == 'receive':
                date = self.video_obj.get_receive_date_string(pretty_flag)
            else:
                date = self.video_obj.get_upload_date_string(pretty_flag)

            if date is not None:
                string = string + '  -  ' + date
            else:
                string = string + '  -  ' + _('unknown')

            self.live_auto_notify_label.set_markup(string)
            self.live_auto_alarm_label.set_text('')
            self.live_auto_open_label.set_text('')
            self.live_auto_dl_start_label.set_text('')
            self.live_auto_dl_stop_label.set_text('')

        else:

            name = html.escape(self.video_obj.name)
            dbid = self.video_obj.dbid

            if dbid in app_obj.media_reg_auto_notify_dict:
                label = '<s>' + _('Notify') + '</s>'
            else:
                label = _('Notify')

            # Currently disabled on MS Windows
            if os.name == 'nt':
                self.live_auto_notify_label.set_markup(_('Notify'))
            else:
                self.live_auto_notify_label.set_markup(
                    '<a href="' + name + '" title="' \
                    + _('When the livestream starts, notify the user') \
                    + '">' + label + '</a>',
                )

            if not mainapp.HAVE_PLAYSOUND_FLAG:

                self.live_auto_alarm_label.set_markup('Alarm')

            else:

                if dbid in app_obj.media_reg_auto_alarm_dict:
                    label = '<s>' + _('Alarm') + '</s>'
                else:
                    label = _('Alarm')

                self.live_auto_alarm_label.set_markup(
                    '<a href="' + name + '" title="' \
                    + _('When the livestream starts, sound an alarm') \
                    + '">' + label + '</a>',
                )

            if dbid in app_obj.media_reg_auto_open_dict:
                label = '<s>' + _('Open') + '</s>'
            else:
                label = _('Open')

            self.live_auto_open_label.set_markup(
                '<a href="' + name + '" title="' \
                + _('When the livestream starts, open it') \
                + '">' + label + '</a>',
            )

            if __main__.__pkg_no_download_flag__ \
            or self.video_obj.live_mode == 2:

                # (Livestream already broadcasting)
                self.live_auto_dl_start_label.set_markup(_('D/L on start'))

            else:

                if dbid in app_obj.media_reg_auto_dl_start_dict:
                    label = '<s>' + _('D/L on start') + '</s>'
                else:
                    label = _('D/L on start')

                self.live_auto_dl_start_label.set_markup(
                    '<a href="' + name + '" title="' \
                    + _('When the livestream starts, download it') \
                    + '">' + label + '</a>',
                )

            if __main__.__pkg_no_download_flag__:

                self.live_auto_dl_stop_label.set_markup(_('D/L on stop'))

            else:

                if dbid in app_obj.media_reg_auto_dl_stop_dict:
                    label = '<s>' + _('D/L on stop') + '</s>'
                else:
                    label = _('D/L on stop')

                self.live_auto_dl_stop_label.set_markup(
                    '<a href="' + name + '" title="' \
                    + _('When the livestream stops, download it') \
                    + '">' + label + '</a>',
                )


    def update_watch_player(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the clickable Gtk.Label widget for watching the video in an
        external media player.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 28557 update_watch_player')

        if self.video_obj.file_name:
            watch_text = '<a href="' + html.escape(
                self.video_obj.get_actual_path(self.main_win_obj.app_obj),
            ) + '" title="' + _('Watch in your media player') + '">' \
            + _('Player') + '</a>'

        # (Many labels are not clickable when a channel/playlist/folder's
        #   external directory is marked disabled)
        if self.video_obj.parent_obj.dbid \
        in self.main_win_obj.app_obj.container_unavailable_dict:

            # Link not clickable
            self.watch_player_label.set_markup(_('Download'))

        elif __main__.__pkg_no_download_flag__:

            if self.video_obj.file_name and self.video_obj.dl_flag:

                # Link clickable
                self.watch_player_label.set_markup(watch_text)

            else:

                # Link not clickable
                self.watch_player_label.set_markup(_('Download'))

        elif self.video_obj.live_mode == 1:

            if self.video_obj.live_msg == '':

                # Link not clickable
                if not self.video_obj.live_debut_flag:
                    self.watch_player_label.set_markup(_('Live soon:'))
                else:
                    self.watch_player_label.set_markup(_('Debut soon:'))

            else:

                self.watch_player_label.set_markup(
                    self.video_obj.live_msg + ':',
                )

        elif self.video_obj.live_mode == 2:

            ignore_me = _('TRANSLATOR\'S NOTE: D/L means download')

            # Link clickable
            self.watch_player_label.set_markup(
                '<a href="' + html.escape(self.video_obj.source) \
                + '" title="' + _('Download this video') + '">' \
                + _('Download') + '</a>',
            )

        elif self.video_obj.file_name and self.video_obj.dl_flag:

            # Link clickable
            self.watch_player_label.set_markup(watch_text)

        elif self.video_obj.source \
        and not self.main_win_obj.app_obj.update_manager_obj \
        and not self.main_win_obj.app_obj.refresh_manager_obj \
        and not self.main_win_obj.app_obj.process_manager_obj:

            ignore_me = _(
                'TRANSLATOR\'S NOTE: If you want to use &, use &amp;' \
                + ' - if you want to use a different word (e.g. French et)' \
                + ', then just use that word',
            )

            # Link clickable
            self.watch_player_label.set_markup(
                '<a href="' + html.escape(self.video_obj.source) \
                + '" title="' + _('Download and watch in your media player') \
                + '">' + _('D/L &amp; watch') + '</a>',
            )

        else:

            # Link not clickable
            self.watch_player_label.set_markup(
                '<i>' + _('Can\'t D/L') + '</i>',
            )


    def update_marked_labels(self):

        """Called by anything, but mainly called by self.update_widgets().

        Updates the clickable Gtk.Label widget for video properties.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 28651 update_marked_labels')

        if self.video_obj.file_name:
            link_text = self.video_obj.get_actual_path(
                self.main_win_obj.app_obj,
            )
        elif self.video_obj.source:
            link_text = self.video_obj.source
        else:
            link_text = ''

        ignore_me = _(
            'TRANSLATOR\'S NOTE: This section contains shortened' \
            + ' labels: Archive = Archived, B/Mark = Bookmarked,' \
            + ' Waiting: In waiting list',
        )

        # Archived/not archived
        text = '<a href="' + html.escape(link_text) + '" title="' \
        + _('Prevent automatic deletion of the video') + '">'

        if not self.video_obj.archive_flag:
            self.marked_archive_label.set_markup(
                text + _('Archived') + '</a>',
            )
        else:
            self.marked_archive_label.set_markup(
                text + '<s>' + _('Archived') + '</s></a>',
            )

        # Bookmarked/not bookmarked
        text = '<a href="' + html.escape(link_text) + '" title="' \
        + _('Show video in Bookmarks folder') + '">'

        if not self.video_obj.bookmark_flag:
            self.marked_bookmark_label.set_markup(
                text + _('B/mark') + '</a>',
            )
        else:
            self.marked_bookmark_label.set_markup(
                text + '<s>' + _('B/mark') + '</s></a>',
            )

        # Favourite/not favourite
        text = '<a href="' + html.escape(link_text) + '" title="' \
        + _('Show in Favourite Videos folder') + '">'

        if not self.video_obj.fav_flag:
            self.marked_fav_label.set_markup(
                text + _('Favourite') + '</a>',
            )
        else:
            self.marked_fav_label.set_markup(
                text + '<s>' + _('Favourite') + '</s></a>')

        # Missing/not missing
        text = '<a href="' + html.escape(link_text) + '" title="' \
        + _('Mark video as removed by creator') + '">'

        if not self.video_obj.missing_flag:
            self.marked_missing_label.set_markup(
                text + _('Missing') + '</a>',
            )
        else:
            self.marked_missing_label.set_markup(
                text + '<s>' + _('Missing') + '</s></a>',
            )

        # New/not new
        text = '<a href="' + html.escape(link_text) + '" title="' \
        + _('Mark video as never watched') + '">'

        if not self.video_obj.new_flag:
            self.marked_new_label.set_markup(
                text + _('New') + '</a>',
            )
        else:
            self.marked_new_label.set_markup(
                text + '<s>' + _('New') + '</s></a>',
            )

        # In waiting list/not in waiting list
        text = '<a href="' + html.escape(link_text) + '" title="' \
        + _('Show in Waiting Videos folder') + '">'
        if not self.video_obj.waiting_flag:
            self.marked_waiting_label.set_markup(
                text + _('Waiting') + '</a>',
            )
        else:
            self.marked_waiting_label.set_markup(
                text + '<s>' + _('Waiting') + '</s></a>',
            )


    def temp_box_is_visible(self):

        """Called by self.draw_widgets and .update_widgets().

        Checks whether the fifth row of labels (for temporary actions) should
        be visible, or not.

        Return values:

            True if the row should be visible, False if not

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 28759 temp_box_is_visible')

        if __main__.__pkg_no_download_flag__:
            return False
        elif (
            self.main_win_obj.app_obj.catalogue_mode
            == 'grid_show_parent_ext'
        ) and not self.no_temp_widgets_flag \
        and not self.video_obj.live_mode:
            return True
        else:
            return False


    def marked_box_is_visible(self):

        """Called by self.draw_widgets and .update_widgets().

        Checks whether the sixth row of labels (for marked video actions)
        should be visible, or not.

        Return values:

            True if the row should be visible, False if not

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 28787 marked_box_is_visible')

        if (
            self.main_win_obj.app_obj.catalogue_mode \
            == 'grid_show_parent_ext' \
        ) and not self.video_obj.live_mode:
            return True
        else:
            return False


    # (Methods unique to this class)


    def toggle_select(self):

        """Called by mainwin.MainWin.video_catalogue_grid_select().

        Selects/unselects this catalogue object.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 28809 toggle_select')

        if not self.selected_flag:

            self.selected_flag = True
            # (Grabbing keyboard focus enables selection using the cursor and
            #   page up/page down keys to work properly)
            self.catalogue_gridbox.grab_focus()

        else:

            self.selected_flag = False

        # (The True argument marks this function as the caller)
        self.update_background(True)


    def do_select(self, select_flag):

        """Called by mainwin.MainWin.video_catalogue_unselect_all() and
        .video_catalogue_grid_select().

        Selects/unselects this catalogue object, and to instruct the
        CatalogueGridBox to grab focus, if required.

        Args:

            select_flag (bool): True to select the catalogue object, False to
                unselect it

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 28842 do_select')

        if not select_flag:

            self.selected_flag = False

        else:

            self.selected_flag = True
            # (Grabbing keyboard focus enables selection using the cursor and
            #   page up/page down keys to work properly)
            self.catalogue_gridbox.grab_focus()

        # (The True argument marks this function as the caller)
        self.update_background(True)


    # Callback methods


    def on_click_marked_archive_label(self, label, uri):

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

        Mark the video as archived or not archived.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 28881 on_click_marked_archive_label')

        # Mark the video as archived/not archived
        if not self.video_obj.archive_flag:
            self.video_obj.set_archive_flag(True)
        else:
            self.video_obj.set_archive_flag(False)

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.marked_archive_label.set_markup(_('Archived'))

        GObject.timeout_add(0, self.update_marked_labels)

        return True


    def on_click_marked_bookmark_label(self, label, uri):

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

        Mark the video as bookmarked or not bookmarked.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 28919 on_click_marked_bookmark_label')

        # Mark the video as bookmarked/not bookmarked
        if not self.video_obj.bookmark_flag:
            self.main_win_obj.app_obj.mark_video_bookmark(
                self.video_obj,
                True,
            )

        else:
            self.main_win_obj.app_obj.mark_video_bookmark(
                self.video_obj,
                False,
            )

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.marked_bookmark_label.set_markup(_('B/mark'))

        GObject.timeout_add(0, self.update_marked_labels)

        return True


    def on_click_marked_waiting_list_label(self, label, uri):

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

        Mark the video as in the waiting list or not in the waiting list.

        Args:

            label (Gtk.Label): The clicked widget

            uri (str): Ignored

        Return values:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 28964 on_click_marked_waiting_list_label')

        # Mark the video as in waiting list/not in waiting list
        if not self.video_obj.waiting_flag:
            self.main_win_obj.app_obj.mark_video_waiting(
                self.video_obj,
                True,
            )

        else:
            self.main_win_obj.app_obj.mark_video_waiting(
                self.video_obj,
                False,
            )

        # Because of an unexplained Gtk problem, there is usually a crash after
        #   this function returns. Workaround is to make the label unclickable,
        #   then use a Glib timer to restore it (after some small fraction of a
        #   second)
        self.marked_waiting_label.set_markup(_('Waiting'))

        GObject.timeout_add(0, self.update_marked_labels)

        return True


    def on_click_box(self, event_box, event):

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

        When the user left-clicks the box comprising this GridCatalogueItem,
        'select' or 'unselect' it.

        When the user rights the box, create a context-sensitive popup menu..

        Args:

            event_box (Gtk.EventBox), event (Gtk.EventButton): Data from the
                signal emitted by the click

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 29007 on_click_box')

        if event.type == Gdk.EventType.BUTTON_RELEASE:

            if event.button == 1:

                # We can't select widgets on a Gtk.Grid directly, so Tartube
                #   implements its own 'selection' mechanism
                if (event.state & Gdk.ModifierType.SHIFT_MASK):

                    self.main_win_obj.video_catalogue_grid_select(
                        self,
                        'shift',
                    )

                elif (event.state & Gdk.ModifierType.CONTROL_MASK):

                    self.main_win_obj.video_catalogue_grid_select(
                        self,
                        'ctrl',
                    )

                else:

                    self.main_win_obj.video_catalogue_grid_select(
                        self,
                        'default',
                    )

            elif event.button == 3:

                self.main_win_obj.video_catalogue_popup_menu(
                    event,
                    self.video_obj,
                )


class CatalogueRow(Gtk.ListBoxRow):

    """Called by MainWin.video_catalogue_redraw_all() and
    .video_catalogue_insert_video().

    Python class acting as a wrapper for Gtk.ListBoxRow, so that we can
    retrieve the media.Video object displayed in each row.

    Args:

        main_win_obj (mainwin.MainWin): The main window

        video_obj (media.Video): The video object displayed on this row

    """


    # Standard class methods


    def __init__(self, main_win_obj, video_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 29067 __init__')

        super(Gtk.ListBoxRow, self).__init__()

        # IV list - class objects
        # -----------------------
        self.main_win_obj = main_win_obj
        self.video_obj = video_obj


        # Code
        # ----

        # Set up drag and drop from the gridbox to an external application
        #   (for example, an FFmpeg batch converter), and also to the Video
        #   Index
        self.drag_source_set(
            Gdk.ModifierType.BUTTON1_MASK,
            [],
            Gdk.DragAction.COPY,
        )
        self.drag_source_add_text_targets()
        self.connect(
            'drag-data-get',
            self.on_drag_data_get,
        )
        self.connect(
            'drag-end',
            self.on_drag_end,
        )


    # Callback class methods


    def on_drag_data_get(self, widget, drag_context, data, \
    info, time):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 29106 on_drag_data_get')

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

        Set the data to be used when the user drags and drops videos from the
        Video Catalogue to an external application (for example, an FFmpeg
        batch converter).

        Args:

            widget (mainwin.CatalogueRow): The widget handling the video in the
                Video Catalogue

            drag_context (GdkX11.X11DragContext): Data from the drag procedure

            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

        """

        if info == 0:   # TARGET_ENTRY_TEXT

            # If this row is selected, and other rows are also selected, they
            #   are all dragged together. Otherwise, only this row is dragged
            selected_list = []
            for catalogue_item_obj \
            in self.main_win_obj.catalogue_listbox.get_selected_rows():
                selected_list.append(catalogue_item_obj.video_obj)

            if self.video_obj in selected_list:
                video_list = selected_list.copy()
            else:
                video_list = [ self.video_obj ]

            # Transfer to the external application a single string, containing
            #   one or more full file paths/URLs/video names, separated by
            #   newline characters
            # If the path/URL/name isn't known for any videos, then an empty
            #   line is transferred
            if info == 0:   # TARGET_ENTRY_TEXT

                data.set_text(
                    self.main_win_obj.get_video_drag_data(video_list),
                    -1,
                )

            # For the benefit of videos being dragged from the Video Catalogue
            #   into the Video Index, we also store the list of videos in the
            #   main window's IV temporarily
            self.main_win_obj.set_video_catalogue_drag_list(video_list)


    def on_drag_end(self, widget, drag_context):

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

        Resets the main window's list of videos being dragged from the Video
        Catalogue (potentially into the Video Index).

        Args:

            widget (mainwin.CatalogueGridBox): The widget handling the video in
                the Video Catalogue

            drag_context (GdkX11.X11DragContext): Data from the drag procedure

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 29179 on_drag_end')

        # For the benefit of videos being dragged from the Video Catalogue
        #   into the Video Index, reset the main window's IV
        self.main_win_obj.reset_video_catalogue_drag_list()


class CatalogueGridBox(Gtk.Frame):

    """Called by MainWin.video_catalogue_redraw_all() and
    .video_catalogue_insert_video().

    Python class acting as a wrapper for Gtk.Frame, so that we can retrieve the
    media.Video object displayed in each gridbox.

    Args:

        main_win_obj (mainwin.MainWin): The main window

        video_obj (media.Video): The video object displayed in this gridbox

    """


    # Standard class methods


    def __init__(self, main_win_obj, video_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 29209 __init__')

        super(Gtk.Frame, self).__init__()

        # IV list - class objects
        # -----------------------
        self.main_win_obj = main_win_obj
        self.video_obj = video_obj


        # IV list - other
        # ---------------
        # The coordinates of the gridbox on the Gtk.Grid (required so that
        #   selection can be handled correctly)
        self.x_pos = None
        self.y_pos = None


        # Code
        # ----

        self.enable_visible_frame(
            main_win_obj.app_obj.catalogue_draw_frame_flag,
        )

        # When the Tartube main window first opens, we don't know how much
        #   horizontal space will be consumed by a gridbox until some time
        #   after a gridbox is drawn
        # Therefore, don't let gridboxes expand vertically until the minimum
        #   size has been determined
        self.set_vexpand(False)

        thumb_size = main_win_obj.app_obj.thumb_size_custom
        if not main_win_obj.catalogue_grid_expand_flag:
            self.set_hexpand(False)
        else:
            self.set_hexpand(True)

        # This callback will set the size of the first CatalogueGridBox, which
        #   tells us the minimum required size for all future gridboxes
        self.connect('size-allocate', self.on_size_allocate)

        # Intercept cursor and page up/down keys, and in response scroll the
        #   Video Catalogue up/down
        self.set_can_focus(True)
        self.connect('key-press-event', self.on_key_press_event)

        # Set up drag and drop from the gridbox to an external application
        #   (for example, an FFmpeg batch converter), and also to the Video
        #   Index
        self.drag_source_set(
            Gdk.ModifierType.BUTTON1_MASK,
            [],
            Gdk.DragAction.COPY,
        )
        self.drag_source_add_text_targets()
        self.connect(
            'drag-data-get',
            self.on_drag_data_get,
        )
        self.connect(
            'drag-end',
            self.on_drag_end,
        )


    # Public class methods


    def set_posn(self, x_pos, y_pos):

        """Called by mainwin.MainWin.video_catalogue_redraw_all(),
        .video_catalogue_insert_video() and .video_catalogue_grid_rearrange().

        Sets the coordinates of this gridbox on the grid, so that selection
        can be handled properly.

        Args:

            x_pos, y_pos (int): Coordinates on a Gtk.Grid

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 29293 set_posn')

        self.x_pos = x_pos
        self.y_pos = y_pos


    def enable_visible_frame(self, visible_flag):

        """Called by self.__init__(),
        mainwin.MainWin.video_catalogue_grid_set_gridbox_width() and
        .on_draw_frame_checkbutton_changed().

        Enables/disables the visible frame drawn around the edge of the
        gridbox (if allowed).

        Args:

            visible_flag (bool): True to enable the frame, False to disable it

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 29315 enable_visible_frame')

        thumb_size = self.main_win_obj.app_obj.thumb_size_custom

        # (When we're still waiting to set the minimum width for a gridbox,
        #   then the frame must be visible, regardless of the specified flag)
        if visible_flag \
        or self.main_win_obj.catalogue_grid_width_dict[thumb_size] is None:
            self.set_shadow_type(Gtk.ShadowType.IN)
        else:
            self.set_shadow_type(Gtk.ShadowType.NONE)


    # Callback class methods


    def on_drag_data_get(self, widget, drag_context, data, \
    info, time):

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

        Set the data to be used when the user drags and drops videos from the
        Video Catalogue to an external application (for example, an FFmpeg
        batch converter).

        Args:

            widget (mainwin.CatalogueGridBox): The widget handling the video in
                the Video Catalogue

            drag_context (GdkX11.X11DragContext): Data from the drag procedure

            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

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 29357 on_drag_data_get')

        if info == 0:   # TARGET_ENTRY_TEXT

            # If this gridbox is selected, and other gridboxes are also
            #   selected, they are all dragged together. Otherwise, only this
            #   gridbox is dragged
            selected_list = []
            for catalogue_item_obj \
            in self.main_win_obj.video_catalogue_dict.values():
                if catalogue_item_obj.selected_flag:
                    selected_list.append(catalogue_item_obj.video_obj)

            if self.video_obj in selected_list:
                video_list = selected_list.copy()
            else:
                video_list = [ self.video_obj ]

            # Transfer to the external application a single string, containing
            #   one or more full file paths/URLs/video names, separated by
            #   newline characters
            # If the path/URL/name isn't known for any videos, then an empty
            #   line is transferred
            if info == 0:   # TARGET_ENTRY_TEXT

                data.set_text(
                    self.main_win_obj.get_video_drag_data(video_list),
                    -1,
                )

            # For the benefit of videos being dragged from the Video Catalogue
            #   into the Video Index, we also store the list of videos in the
            #   main window's IV temporarily
            self.main_win_obj.set_video_catalogue_drag_list(video_list)


    def on_drag_end(self, widget, drag_context):

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

        Resets the main window's list of videos being dragged from the Video
        Catalogue (potentially into the Video Index).

        Args:

            widget (mainwin.CatalogueGridBox): The widget handling the video in
                the Video Catalogue

            drag_context (GdkX11.X11DragContext): Data from the drag procedure

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 29410 on_drag_end')

        # For the benefit of videos being dragged from the Video Catalogue
        #   into the Video Index, reset the main window's IV
        self.main_win_obj.reset_video_catalogue_drag_list()


    def on_key_press_event(self, widget, event):

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

        Intercept keypresses when this gridbox has keyboard focus.

        The cursor and page up/down keys are passed to the main window so that
        the Video Catalogue can be scrolled; all other keys are ignored.

        Args:

            widget (mainwin.CatalogueGridBox): The clicked widget

            event (Gdk.EventButton): The event emitting the Gtk signal

        Return values:

            True to show the action has been handled, or False if the action
                has been ignored

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 29440 on_key_press_event')

        if event.type != Gdk.EventType.KEY_PRESS:
            return

        # 'Up', 'Left', 'Page_Up', etc. Also 'a' for CTRL+A
        keyval = Gdk.keyval_name(event.keyval)
        if not keyval in self.main_win_obj.catalogue_grid_intercept_dict:
            return False

        else:

            if (event.state & Gdk.ModifierType.SHIFT_MASK):
                select_type = 'shift'
            elif (event.state & Gdk.ModifierType.CONTROL_MASK):
                select_type = 'ctrl'
            else:
                select_type = 'default'

            if keyval == 'a':

                if select_type == 'ctrl':

                    self.main_win_obj.video_catalogue_grid_select_all()

                    # Return True to show that we have interfered with this
                    #   keypress
                    return True

                else:
                    return False

            else:

                self.main_win_obj.video_catalogue_grid_scroll_on_select(
                    self,
                    keyval,
                    select_type,
                )

                # Return True to show that we have interfered with this
                #   keypress
                return True


    def on_size_allocate(self, widget, rect):

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

        When gridboxes are added to the Video Catalogue, the minimum horizontal
        space required to fit all of its widgets is not available.

        When it becomes available, this function is called, so that the size
        can be passed on to the main window's code.

        Args:

            widget (mainwin.CatalogueGridBox): The clicked widget

            rect (Gdk.Rectangle): Object describing the window's new size

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 29504 on_size_allocate')

        thumb_size = self.main_win_obj.app_obj.thumb_size_custom
        min_width = self.main_win_obj.catalogue_grid_width_dict[thumb_size]

        if rect.width > 1 and min_width is None:
            self.main_win_obj.video_catalogue_grid_set_gridbox_width(
                rect.width,
            )


    # Set accessors


    def set_expandable(self, expand_flag):

        """Called by mainwin.MainWin.video_catalogue_grid_check_expand().

        Allows/prevents this gridbox to expand horizontally in its parent
        Gtk.Grid, depending on various aesthetic requirements.

        Args:

            expand_flag (bool): True to allow horizontal expansion, False to
                prevent it

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 29533 set_expandable')

        if not expand_flag:
            self.set_hexpand(False)
        else:
            self.set_hexpand(True)


class DropZoneBox(Gtk.Frame):

    """Called by MainWin.drag_drop_grid_reset().

    Python class acting as a wrapper for Gtk.Frame, so that we can retrieve the
    options.OptionsManager object displayed in each dropzone.

    Args:

        main_win_obj (mainwin.MainWin): The main window

        options_obj (options.OptionsManager or None): The download options
            associated with this dropzone, or None for an empty dropzone

        update_text (str or None): When the grid is re-drawn, any messages
            displayed inside the dropzone are copied across to each
            replacment dropzone

        reset_time (int or None): The same applies to the time at which those
            messages are to be removed

    """


    # Standard class methods


    def __init__(self, main_win_obj, options_obj, x_pos, y_pos, height,
    update_text=None, reset_time=None):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 29572 __init__')

        super(Gtk.Frame, self).__init__()

        # IV list - class objects
        # -----------------------
        self.main_win_obj = main_win_obj
        # (If this is a blank dropzone, then 'options_obj' is None)
        self.options_obj = options_obj


        # IV list - other
        # ---------------
        # Coordinates of this dropzone on the Drag and Drop tab's grid
        self.x_pos = x_pos
        self.y_pos = y_pos
        # Height of the Drag and Drop tab's grid
        self.height = height

        # This dropzone's own Gtk.Grid, on which widgets are drawn
        self.grid = None

        # Current messages displayed in the update label (in case this box is
        #   replaced by a new one, before the message is reset)
        self.update_text = update_text
        # The time (matches time.time() at which those messages are due to be
        #   reset (None if no messages are visible)
        self.reset_time = reset_time

        # Code
        # ----

        # Set up widgets
        self.draw_widgets()
        # Set up drag and drop into this frame
        if self.options_obj:
            self.connect('drag-data-received', self.on_drag_data_received)
            self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
            self.drag_dest_set_target_list(None)
            self.drag_dest_add_text_targets()


    # Public class methods


    def draw_widgets(self):

        """Called by self.__init__().

        Populate the dropzone with widgets.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 29625 draw_widgets')

        self.grid = Gtk.Grid()
        self.add(self.grid)
        self.grid.set_column_spacing(self.main_win_obj.spacing_size)
        self.grid.set_row_spacing(self.main_win_obj.spacing_size * 2)
        self.grid.set_border_width(self.main_win_obj.spacing_size)

        # (Depending on the size of the grid, add spacing between widgets, or
        #   not)
        row = 0

        if self.height < 4:
            # (Empty box for spacing)
            box = Gtk.Box()
            self.grid.attach(box, 0, row, 1, 1)
            box.set_vexpand(True)
            row += 1

        self.name_label = Gtk.Label()
        self.grid.attach(self.name_label, 0, row, 1, 1)
        self.name_label.set_hexpand(True)
        row += 1

        self.descrip_label = Gtk.Label()
        self.grid.attach(self.descrip_label, 0, row, 1, 1)
        self.descrip_label.set_hexpand(True)
        row += 1

        if self.height < 3:
            # (Empty box for spacing)
            box = Gtk.Box()
            self.grid.attach(box, 0, row, 1, 1)
            box.set_vexpand(True)
            row += 1

        self.update_label = Gtk.Label()
        self.grid.attach(self.update_label, 0, row, 1, 1)
        self.update_label.set_hexpand(True)
        row += 1

        # (Empty box for spacing)
        box = Gtk.Box()
        self.grid.attach(box, 0, row, 1, 1)
        box.set_vexpand(True)
        row += 1

        # Strip of buttons at the bottom
        hbox = Gtk.HBox()
        self.grid.attach(hbox, 0, row, 1, 1)
        row += 1

        if self.options_obj:

            if not self.main_win_obj.app_obj.show_custom_icons_flag:
                button = Gtk.Button.new_from_icon_name(
                    Gtk.STOCK_DELETE,
                    Gtk.IconSize.BUTTON,
                )
            else:
                button = Gtk.Button.new()
                button.set_image(
                    Gtk.Image.new_from_pixbuf(
                        self.main_win_obj.pixbuf_dict['stock_delete'],
                    ),
                )
            hbox.pack_end(button, False, False, 0)
            button.connect('clicked', self.on_delete_button_clicked)

            if not self.main_win_obj.app_obj.show_custom_icons_flag:
                button2 = Gtk.Button.new_from_icon_name(
                    Gtk.STOCK_INDEX,
                    Gtk.IconSize.BUTTON,
                )
            else:
                button2 = Gtk.Button.new()
                button2.set_image(
                    Gtk.Image.new_from_pixbuf(
                        self.main_win_obj.pixbuf_dict['stock_properties'],
                    ),
                )
            hbox.pack_end(
                button2,
                False,
                False,
                self.main_win_obj.spacing_size,
            )
            button2.connect('clicked', self.on_edit_button_clicked)

        # Draw text on labels, as necessary
        self.update_widgets()


    def update_widgets(self):

        """Can be called by anything.

        Update the text displayed in the widgets created in the earlier call to
        self.draw_widgets().
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 29727 update_widgets')

        size = len(self.main_win_obj.app_obj.classic_dropzone_list)
        if size <= 1:
            length = self.main_win_obj.exceedingly_long_string_max_len
        elif size <= 4:
            length = self.main_win_obj.very_long_string_max_len
        elif size <= 9:
            length = self.main_win_obj.long_string_max_len
        else:
            length = self.main_win_obj.medium_string_max_len

        if not self.options_obj:

            self.name_label.set_markup('')
            self.descrip_label.set_markup('')
            self.update_label.set_markup('')

            self.override_background_color(
                Gtk.StateType.NORMAL,
                None,
            )

        else:

            self.name_label.set_markup(
                '<b><span font_size="large">' + self.options_obj.name \
                + '</span></b>',
            )
            self.descrip_label.set_markup(
                html.escape(
                    utils.shorten_string_two_lines(
                        self.options_obj.descrip,
                        length,
                    ),
                ),
            )

            if self.update_text is None:

                self.update_label.set_markup('')

                if self.y_pos % 2 == 0:
                    if self.x_pos % 2 == 0:
                        colour = self.main_win_obj.drag_drop_even_colour
                    else:
                        colour = self.main_win_obj.drag_drop_odd_colour
                else:
                    if self.x_pos % 2 == 0:
                        colour = self.main_win_obj.drag_drop_odd_colour
                    else:
                        colour = self.main_win_obj.drag_drop_even_colour

                self.override_background_color(
                    Gtk.StateType.NORMAL,
                    colour,
                )

            else:
                self.update_label.set_markup(
                    '<i>' + html.escape(
                        utils.shorten_string_two_lines(
                            self.update_text,
                            length,
                        ),
                    ) + '</i>',
                )

                self.override_background_color(
                    Gtk.StateType.NORMAL,
                    self.main_win_obj.drag_drop_notify_colour,
                )


    def do_reset(self):

        """Called (several times a second) by
        mainapp.TartubeApp.script_fast_timer_callback().

        It's time to remove the message displayed in the dropzone, so remove it
        and update IVs.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 29811 do_reset')

        self.update_text = None
        self.reset_time = None
        self.update_widgets()


    # Callback class methods


    def on_delete_button_clicked(self, button):

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

        Prompts the user to delete this dropzone and/or its associated
        options.OptionsManager object.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 29832 on_delete_button_clicked')

        app_obj = self.main_win_obj.app_obj

        dialogue_win = DeleteDropZoneDialogue(
            self.main_win_obj,
            self.options_obj,
        )
        response = dialogue_win.run()

        # Get the clicked button, before destroying the window
        del_dropzone_flag = dialogue_win.del_dropzone_flag
        del_both_flag = dialogue_win.del_both_flag
        dialogue_win.destroy()

        if response != Gtk.ResponseType.CANCEL \
        and response != Gtk.ResponseType.DELETE_EVENT:

            if del_dropzone_flag:
                app_obj.del_classic_dropzone_list(self.options_obj.uid)

            elif del_both_flag:
                options_obj = app_obj.options_reg_dict[self.options_obj.uid]
                app_obj.delete_download_options(options_obj)


            # Update the Drag and Drop tab
            self.main_win_obj.drag_drop_grid_reset()


    def on_edit_button_clicked(self, button):

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

        Opens an edit window for this dropzone's options.OptionsManager object.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 29870 on_edit_button_clicked')

        config.OptionsEditWin(
            self.main_win_obj.app_obj,
            self.options_obj,
        )


    def on_drag_data_received(self, window, context, x, y, data, info,
    this_time):

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

        Handles drag-and-drop anywhere in the dropzone, adding a valid and
        non-duplicate URL to the Classic Progress List.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 29888 on_drag_data_received')

        # Sanity check
        if not self.options_obj:
            return

        else:

            url = utils.strip_whitespace(data.get_text())

            # Show a confirmation inside the dropzone
            if not utils.check_url(url):
                self.update_text = _('Invalid URL')

            else:

                duplicate_flag = False
                for other_obj in self.main_win_obj.classic_media_dict.values():
                    if other_obj.source == url:
                        duplicate_flag = True
                        break

                if duplicate_flag:
                    self.update_text = _('Duplicate URL')

                elif not self.main_win_obj.classic_mode_tab_insert_url(
                    url,
                    self.options_obj,
                ):
                    self.update_text = _('Failed to add URL')

                else:
                    self.update_text = url

            self.reset_time = time.time() \
            + self.main_win_obj.drag_drop_reset_time
            self.update_widgets()


class StatusIcon(Gtk.StatusIcon):

    """Called by mainapp.TartubeApp.start().

    Python class acting as a wrapper for Gtk.StatusIcon.

    Args:

        app_obj (mainapp.TartubeApp): The main application

    """


    # Standard class methods


    def __init__(self, app_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 29946 __init__')

        super(Gtk.StatusIcon, self).__init__()

        # IV list - class objects
        # -----------------------
        # The main application
        self.app_obj = app_obj


        # IV list - other
        # ---------------
        # Flag set to True (by self.show_icon() ) when the status icon is
        #   actually visible
        self.icon_visible_flag = False


        # Code
        # ----

        self.setup()


    # Public class methods


    def setup(self):

        """Called by self.__init__.

        Sets up the Gtk widget, and creates signal connects for left- and
        right-clicks on the status icon.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 29981 setup')

        # Display the default status icon, to start with...
        self.update_icon()
        # ...but the status icon isn't visible straight away
        self.set_visible(False)

        # Set the tooltip
        self.set_has_tooltip(True)
        self.set_tooltip_text('Tartube')

        # Signal connects
        self.connect('button-press-event', self.on_button_press_event)
        self.connect('popup-menu', self.on_popup_menu)


    def show_icon(self):

        """Can be called by anything.

        Makes the status icon visible in the system tray (if it isn't already
        visible).
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30006 show_icon')

        if not self.icon_visible_flag:
            self.icon_visible_flag = True
            self.set_visible(True)


    def hide_icon(self):

        """Can be called by anything.

        Makes the status icon invisible in the system tray (if it isn't already
        invisible).
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30022 hide_icon')

        if self.icon_visible_flag:
            self.icon_visible_flag = False
            self.set_visible(False)


    def update_icon(self):

        """Called by self.setup(), and then by mainapp.TartubeApp whenever am
        operation starts or stops.

        Updates the status icon with the correct icon file. The icon file used
        depends on whether an operation is in progress or not, and which one.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30039 update_icon')

        if self.app_obj.download_manager_obj:
            if self.app_obj.download_manager_obj.operation_type == 'sim':
                if not self.app_obj.livestream_manager_obj:
                    icon = formats.STATUS_ICON_DICT['check_icon']
                else:
                    icon = formats.STATUS_ICON_DICT['check_live_icon']
            else:
                if not self.app_obj.livestream_manager_obj:
                    icon = formats.STATUS_ICON_DICT['download_icon']
                else:
                    icon = formats.STATUS_ICON_DICT['download_live_icon']
        elif self.app_obj.update_manager_obj:
            icon = formats.STATUS_ICON_DICT['update_icon']
        elif self.app_obj.refresh_manager_obj:
            icon = formats.STATUS_ICON_DICT['refresh_icon']
        elif self.app_obj.info_manager_obj:
            icon = formats.STATUS_ICON_DICT['info_icon']
        elif self.app_obj.tidy_manager_obj:
            icon = formats.STATUS_ICON_DICT['tidy_icon']
        elif self.app_obj.livestream_manager_obj:
            icon = formats.STATUS_ICON_DICT['livestream_icon']
        elif self.app_obj.process_manager_obj:
            icon = formats.STATUS_ICON_DICT['process_icon']
        else:
            icon = formats.STATUS_ICON_DICT['default_icon']

        # DEBUG: Git #577: this seems to be the source of the
        #   'cairo_surface_reference' crashes, but they are difficult to
        #   reproduce; perhaps this will work
#       self.set_from_file(
#           os.path.abspath(
#               os.path.join(
#                   self.app_obj.main_win_obj.icon_dir_path,
#                   'status',
#                   icon,
#               ),
#           )
#       )

        try:
            self.set_from_file(
                os.path.abspath(
                    os.path.join(
                        self.app_obj.main_win_obj.icon_dir_path,
                        'status',
                        icon,
                    ),
                )
            )

        except Exception as e:
            self.app_obj.system_error(
                277,
                'Disabling system tray icon after Gtk error: ' + str(e)
            )

            self.app_obj.set_show_status_icon_flag(False)
            self.app_obj.set_open_in_tray_flag(False)
            self.app_obj.set_close_to_tray_flag(False)
            self.app_obj.set_restore_posn_from_tray_flag(False)


    # Callback class methods


    # (Clicks on the status icon)


    def on_button_press_event(self, widget, event_button):

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

        When the status icon is left-clicked, toggle the main window's
        visibility.

        Args:

            widget (mainwin.StatusIcon): This object

            event_button (Gdk.EventButton): Ignored

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30100 on_button_press_event')

        if event_button.button == 1:
            self.app_obj.main_win_obj.toggle_visibility()
            return True

        else:
            return False


    def on_popup_menu(self, widget, button, time):

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

        When the status icon is right-clicked, open a popup men.

        Args:

            widget (mainwin.StatusIcon): This object

            button_type (int): Ignored

            time (int): Ignored

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30127 on_popup_menu')

        # Set up the popup menu
        popup_menu = Gtk.Menu()

        # Check all
        check_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Check all'))
        check_menu_item.connect('activate', self.on_check_menu_item)
        popup_menu.append(check_menu_item)
        if self.app_obj.current_manager_obj:
            check_menu_item.set_sensitive(False)

        # Download all
        download_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Download all'))
        download_menu_item.connect('activate', self.on_download_menu_item)
        popup_menu.append(download_menu_item)
        if self.app_obj.current_manager_obj:
            download_menu_item.set_sensitive(False)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Stop current operation
        stop_menu_item = Gtk.MenuItem.new_with_mnemonic(
            _('_Stop current operation'),
        )
        stop_menu_item.connect('activate', self.on_stop_menu_item)
        popup_menu.append(stop_menu_item)
        if not self.app_obj.current_manager_obj:
            stop_menu_item.set_sensitive(False)

        # Separator
        popup_menu.append(Gtk.SeparatorMenuItem())

        # Quit
        quit_menu_item = Gtk.MenuItem.new_with_mnemonic(_('_Quit'))
        quit_menu_item.connect('activate', self.on_quit_menu_item)
        popup_menu.append(quit_menu_item)

        # Create the popup menu
        popup_menu.show_all()
        popup_menu.popup(None, None, None, self, 3, time)


    # (Menu item callbacks)


    def on_check_menu_item(self, menu_item):

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

        Starts the download manager.

        Args:

            menu_item (Gtk.MenuItem): The menu item clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30187 on_check_menu_item')

        if not self.app_obj.current_manager_obj:
            self.app_obj.download_manager_start('sim')


    def on_download_menu_item(self, menu_item):

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

        Starts the download manager.

        Args:

            menu_item (Gtk.MenuItem): The menu item clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30206 on_download_menu_item')

        if not self.app_obj.current_manager_obj:
            self.app_obj.download_manager_start('real')


    def on_stop_menu_item(self, menu_item):

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

        Stops the current operation (but not livestream operations, which run
        in the background and are halted immediately, if a different type of
        operation wants to start).

        Args:

            menu_item (Gtk.MenuItem): The menu item clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30227 on_stop_menu_item')

        if self.app_obj.current_manager_obj:

            self.app_obj.set_operation_halted_flag(True)

            if self.app_obj.download_manager_obj:
                self.app_obj.download_manager_obj.stop_download_operation()
            elif self.app_obj.update_manager_obj:
                self.app_obj.update_manager_obj.stop_update_operation()
            elif self.app_obj.refresh_manager_obj:
                self.app_obj.refresh_manager_obj.stop_refresh_operation()
            elif self.app_obj.info_manager_obj:
                self.app_obj.info_manager_obj.stop_info_operation()
            elif self.app_obj.tidy_manager_obj:
                self.app_obj.tidy_manager_obj.stop_tidy_operation()
            elif self.app_obj.process_manager_obj:
                self.app_obj.processs_manager_obj.stop_process_operation()


    def on_quit_menu_item(self, menu_item):

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

        Close the application.

        Args:

            menu_item (Gtk.MenuItem): The menu item clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30260 on_quit_menu_item')

        self.app_obj.stop()


class MultiDragDropTreeView(Gtk.TreeView):

    """Called by MainWin.setup_progress_tab() and .setup_classic_mode_tab().

    A modified version of Gtk.TreeView by Kevin Mehall, released under the MIT
    license, and slightly modified to work with PyGObject. See:
    https://kevinmehall.net/2010/pygtk_multi_select_drag_drop

    This treeview captures mouse events to make drag and drop work properly.
    """

    # Standard class methods


    def __init__(self):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30282 __init__')

        super(MultiDragDropTreeView, self).__init__()

        # Code
        # ----

        self.connect('button-press-event', self.on_button_press)
        self.connect('button-release-event', self.on_button_release)
        self.defer_select = False


    def on_button_press(self, widget, event):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30297 on_button_press')

        """Intercept mouse clicks on selected items so that we can drag
        multiple items without the click selecting only one.

        Args:

            widget (mainwin.MultiDragDropTreeView): The widget clicked

            event (Gdk.EventButton): The event occuring as a result

        """

        target = self.get_path_at_pos(int(event.x), int(event.y))

        if (
            target
            and event.type == Gdk.EventType.BUTTON_PRESS
            and not (
                event.state \
                & (Gdk.ModifierType.CONTROL_MASK|Gdk.ModifierType.SHIFT_MASK)
            )
            and self.get_selection().path_is_selected(target[0])
        ):
            # Disable selection
            self.get_selection().set_select_function(lambda *ignore: False)
            self.defer_select = target[0]


    def on_button_release(self, widget, event):

        """Re-enable selection.

        Args:

            widget (mainwin.MultiDragDropTreeView): The widget clicked

            event (Gdk.EventButton): The event occuring as a result

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30339 on_button_release')

        self.get_selection().set_select_function(lambda *ignore: True)

        target = self.get_path_at_pos(int(event.x), int(event.y))
        if (
            self.defer_select
            and target
            and self.defer_select == target[0]
            and not (event.x==0 and event.y==0)
        ):
            # Certain drag and drop
            self.set_cursor(target[0], target[1], False)

        self.defer_select=False


# (Dialogue window classes)


class AddBulkDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.on_menu_add_bulk().

    Python class handling a dialogue window that adds channels/playlist to the
    media registry in bulk.

    Much of the code in this dialogue window has been copied from
    mainwin.AddVideoDialogue.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        suggest_parent_dbid (int): The .dbid of the parent media.Folder, or
            None if the channels/playlists shouldn't be added inside a folder

    """


    # Standard class methods


    def __init__(self, main_win_obj, suggest_parent_dbid=None):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30385 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Add many channels/playlists\' dialogue' \
            + ' starts here. In the main window menu, click' \
            + ' Media > Add many channels/playlists...'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.grid = None                        # Gtk.Grid
        self.textbuffer = None                  # Gtk.TextBuffer
        self.mark_start = None                  # Gtk.TextMark
        self.mark_end = None                    # Gtk.TextMark
        self.checkbutton = None                 # Gtk.CheckButton
        self.treeview = None                    # Gtk.TreeView
        self.liststore = None                   # Gtk.ListStore
        self.liststore2 = None                  # Gtk.ListStore
        self.grid2 = None                       # Gtk.Grid


        # IV list - other
        # ---------------
        # List of URLs added so far, for checking duplicates
        self.url_list = []
        # Number of channels/playlists added so far, used to give channels/
        #   playlists an initial name
        # In order to eliminate duplicate names, these counts may be larger
        #   than the actual number added
        self.channel_count = 0
        self.playlist_count = 0

        # A list of media.Folder (.dbid values) whose names should be
        #   displayed in the Gtk.ComboBox
        self.folder_list = []
        # The media.Folder selected in the combobox
        self.parent_dbid = None
        # Set up IVs for clipboard monitoring, if required
        self.clipboard_timer_id = None
        self.clipboard_timer_time = 250


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Add many channels/playlists'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)

        # Set up the dialogue window
        box = self.get_content_area()

        self.grid = Gtk.Grid()
        box.add(self.grid)
        self.grid.set_border_width(main_win_obj.spacing_size)
        self.grid.set_row_spacing(main_win_obj.spacing_size)
        self.grid.set_column_homogeneous(True)

        grid_width = 2

        # Initial widgets
        label = Gtk.Label(_('Enter URLs below'))
        self.grid.attach(label, 0, 0, 1, 1)
        label.set_xalign(0)

        self.checkbutton = Gtk.CheckButton()
        self.grid.attach(self.checkbutton, 1, 0, 1, 1)
        self.checkbutton.set_label(_('Enable automatic copy/paste'))
        self.checkbutton.connect('toggled', self.on_checkbutton_toggled)

        # Add a textview
        frame = Gtk.Frame()
        self.grid.attach(frame, 0, 1, grid_width, 1)
        # (Set enough vertical room for several URLs)
        frame.set_size_request(
            main_win_obj.app_obj.config_win_width - 150,
            120,
        )

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)

        textview = Gtk.TextView()
        scrolled.add(textview)
        textview.set_hexpand(True)
        self.textbuffer = textview.get_buffer()

        # Some callbacks will complain about invalid iterators, if we try to
        #   use Gtk.TextIters, so use Gtk.TextMarks instead
        self.mark_start = self.textbuffer.create_mark(
            'mark_start',
            self.textbuffer.get_start_iter(),
            True,               # Left gravity
        )
        self.mark_end = self.textbuffer.create_mark(
            'mark_end',
            self.textbuffer.get_end_iter(),
            False,              # Not left gravity
        )

        # Drag-and-drop onto the textview inevitably inserts a URL in the
        #   middle of another URL. No way to prevent that, but we can disable
        #   drag-and-drop in the textview altogether, and instead handle it
        #   from the dialogue window itself
        textview.drag_dest_unset()
        self.connect('destroy', self.close, textview)

        self.connect('drag-data-received', self.on_window_drag_data_received)
        self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
        self.drag_dest_set_target_list(None)
        self.drag_dest_add_text_targets()

        # Paste in the contents of the clipboard (if it contains valid URLs)
        if main_win_obj.app_obj.dialogue_copy_clipboard_flag:
            utils.add_links_to_textview_from_clipboard(
                main_win_obj.app_obj,
                self.textbuffer,
                self.mark_start,
                self.mark_end,
            )

        # Buttons to add URLs as channels/playlists
        channel_button = Gtk.Button.new_with_label(_('Add channels'))
        self.grid.attach(channel_button, 0, 2, 1, 1)
        channel_button.connect('clicked', self.on_add_urls_clicked, 'channel')

        playlist_button = Gtk.Button.new_with_label(_('Add playlists'))
        self.grid.attach(playlist_button, 1, 2, 1, 1)
        playlist_button.connect(
            'clicked',
            self.on_add_urls_clicked,
            'playlist',
        )

        # Separator
        self.grid.attach(Gtk.HSeparator(), 0, 4, grid_width, 1)

        # Add a treeview
        label2 = Gtk.Label(
            _('Double-click the names/URLs to customise them'),
        )
        self.grid.attach(label2, 0, 5, grid_width, 1)
        label2.set_xalign(0)

        label3 = Gtk.Label()
        label3.set_markup(
            _(
                '<b>HINT</b>: You can also click <b>Media > Reset channel/' \
                + 'playlist names...</b>',
            ),
        )
        self.grid.attach(label3, 0, 6, grid_width, 1)
        label3.set_xalign(0)

        frame2 = Gtk.Frame()
        self.grid.attach(frame2, 0, 7, grid_width, 1)
        frame2.set_size_request(-1, 150)

        scrolled2 = Gtk.ScrolledWindow()
        frame2.add(scrolled2)
        scrolled2.set_policy(
            Gtk.PolicyType.AUTOMATIC,
            Gtk.PolicyType.AUTOMATIC,
        )
        scrolled2.set_vexpand(True)

        self.treeview = Gtk.TreeView()
        scrolled2.add(self.treeview)
        self.treeview.set_headers_visible(True)
        # (Allow multiple selection)
        self.treeview.set_can_focus(True)
        selection = self.treeview.get_selection()
        selection.set_mode(Gtk.SelectionMode.MULTIPLE)

        for i, column_title in enumerate(
            [ 'hide', _('Type'), _('Name'), _('URL') ],
        ):
            if i == 1:
                renderer_pixbuf = Gtk.CellRendererPixbuf()
                column_pixbuf = Gtk.TreeViewColumn(
                    column_title,
                    renderer_pixbuf,
                    pixbuf=i,
                )
                self.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,
                )
                if i == 0:
                    column_text.set_visible(False)
                else:
                    self.treeview.append_column(column_text)
                    column_text.set_resizable(True)
                    if i == 2:
                        renderer_text.set_property('editable', True)
                        renderer_text.connect(
                            'edited',
                            self.on_container_name_edited,
                        )
                    elif i == 3:
                        renderer_text.set_property('editable', True)
                        renderer_text.connect(
                            'edited',
                            self.on_container_url_edited,
                        )

        self.liststore = Gtk.ListStore(
            str, GdkPixbuf.Pixbuf, str, str,
        )
        self.treeview.set_model(self.liststore)

        # Add more buttons
        switch_button = Gtk.Button.new_with_label(_('Toggle channel/playlist'))
        self.grid.attach(switch_button, 0, 8, 1, 1)
        switch_button.connect('clicked', self.on_convert_line_clicked)

        delete_button = Gtk.Button.new_with_label(_('Delete selected lines'))
        self.grid.attach(delete_button, 1, 8, 1, 1)
        delete_button.connect('clicked', self.on_delete_line_clicked)

        # Separator
        self.grid.attach(Gtk.HSeparator(), 0, 9, grid_width, 1)

        # Prepare a list of folders to display in a combo. The list always
        #   includes the system folder 'Temporary Videos'
        # If a folder is selected in the Video Index, then it is the first item
        #   in the list. If not, 'Temporary Videos' is the first item in the
        #   list
        for media_data_obj in main_win_obj.app_obj.container_reg_dict.values():
            if isinstance(media_data_obj, media.Folder) \
            and not media_data_obj.fixed_flag \
            and media_data_obj.restrict_mode == 'open' \
            and media_data_obj.get_depth() \
            < main_win_obj.app_obj.container_max_level \
            and (
                suggest_parent_dbid is None
                or suggest_parent_dbid != media_data_obj.dbid
            ):
                self.folder_list.append(media_data_obj.dbid)

        self.folder_list.sort()
        self.folder_list.insert(0, None)
        self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.dbid)

        if suggest_parent_dbid is not None:
            self.folder_list.insert(0, suggest_parent_dbid)

        # Store the combobox's selected item, so the calling function can
        #   retrieve it
        self.parent_dbid = self.folder_list[0]

        # (Second grid, to avoid messing up the format of the previous one)
        self.grid2 = Gtk.Grid()
        self.grid.attach(self.grid2, 0, 10, grid_width, 1)
        self.grid2.set_column_spacing(main_win_obj.spacing_size)

        label4 = Gtk.Label(_('Add to this folder:'))
        self.grid2.attach(label4, 0, 0, 1, 1)

        self.liststore2 = Gtk.ListStore(GdkPixbuf.Pixbuf, int, str)
        for dbid in self.folder_list:

            if dbid is None:
                pixbuf = main_win_obj.pixbuf_dict['slice_small']
                self.liststore2.append(
                    [pixbuf, dbid, '  ' + _('No parent folder')]
                )

            elif dbid == main_win_obj.app_obj.fixed_temp_folder.dbid:
                pixbuf = main_win_obj.pixbuf_dict['folder_blue_small']
                this_obj = main_win_obj.app_obj.fixed_temp_folder
                self.liststore2.append( [pixbuf, dbid, '  ' + this_obj.name] )

            else:
                pixbuf = main_win_obj.pixbuf_dict['folder_small']
                this_obj = main_win_obj.app_obj.container_reg_dict[dbid]
                self.liststore2.append( [pixbuf, dbid, '  ' + this_obj.name] )

        combo = Gtk.ComboBox.new_with_model(self.liststore2)
        self.grid2.attach(combo, 1, 0, 1, 1)
        combo.set_hexpand(True)

        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, False)
        combo.add_attribute(renderer_text, 'text', 2)

        combo.set_active(0)
        combo.connect('changed', self.on_combo_changed)

        # Display the dialogue window
        self.show_all()


    def close(self, also_self, textview):

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

        We have disabled drag-and-drop directly into the textview, but this
        generates a Gtk warning when the textview is destroyed.

        Workaround is to re-enable drag-and-drop into the textview immediately
        before it (and its window) are destroyed.

        Args:

            also_self (mainwin.AddBulkDialogue): Another copy of this window

            textview (Gtk.TextView): The textview for which drag-and-drop has
                been disabled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30722 close')

        textview.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)


    # Public class methods


    def get_channel_name(self):

        """Called by self.on_add_urls_clicked().

        Generates an initial channel name that isn't already used by a channel/
        playlist/folder in the media data registry.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30739 get_channel_name')

        while 1:

            self.channel_count += 1
            name = 'channel_' + str(self.channel_count)

            if not self.main_win_obj.app_obj.is_container(name):
                return name


    def get_playlist_name(self):

        """Called by self.on_add_urls_clicked().

        Generates an initial playlist name that isn't already used by a
        channel/playlist/folder in the media data registry.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30759 get_playlist_name')

        while 1:

            self.playlist_count += 1
            name = 'playlist_' + str(self.playlist_count)

            if not self.main_win_obj.app_obj.is_container(name):
                return name


    # Callback class methods


    def on_add_urls_clicked(self, button, add_type):

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

        Moves valid URLs from the textview to the treeview, then empties the
        textview.

        Args:

            button (Gtk.Button): The widget clicked

            add_type (str): 'channel' or 'playlist'

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30789 on_add_urls_clicked')

        # Retrieve the list of URLs added by the user
        text = self.textbuffer.get_text(
            self.textbuffer.get_start_iter(),
            self.textbuffer.get_end_iter(),
            False,
        )

        # Split text into a list of lines and filter out invalid URLs
        new_list = []
        for line in text.splitlines():

            for item in line.split():

                # Remove leading/trailing whitespace
                item = utils.strip_whitespace(item)

                # Perform checks on the URL. If it passes, remove leading/
                #   trailing whitespace
                if utils.check_url(item) \
                and not item in self.url_list:
                    mod_item = utils.strip_whitespace(item)
                    new_list.append(mod_item)
                    self.url_list.append(mod_item)

        # Reset the clipboard...
        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        clipboard.set_text('', -1)
        # ...so we can empty the textview
        self.textbuffer.set_text('')
        self.show_all()

        # Add the URLs to the treeview
        for url in new_list:

            if add_type == 'channel':

                self.liststore.append([
                    'channel',
                    self.main_win_obj.pixbuf_dict['channel_small'],
                    self.get_channel_name(),
                    url,
                ])

            else:

                self.liststore.append([
                    'playlist',
                    self.main_win_obj.pixbuf_dict['playlist_small'],
                    self.get_playlist_name(),
                    url,
                ])


    def on_checkbutton_toggled(self, checkbutton):

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

        Enables/disables clipboard monitoring.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30857 on_checkbutton_toggled')

        if not checkbutton.get_active() \
        and self.clipboard_timer_id is not None:

            # Stop the timer
            GObject.source_remove(self.clipboard_timer_id)
            self.clipboard_timer_id = None

        elif checkbutton.get_active() and self.clipboard_timer_id is None:

            # Start the timer
            self.clipboard_timer_id = GObject.timeout_add(
                self.clipboard_timer_time,
                self.clipboard_timer_callback,
            )


    def on_combo_changed(self, combo):

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

        Updates the combobox's selected item, so the calling function can
        retrieve it.

        Args:

            combo (Gtk.ComboBox): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30889 on_combo_changed')

        self.parent_dbid = self.folder_list[combo.get_active()]


    def on_container_name_edited(self, widget, path, text):

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

        Updates the name of a channel/playlist.

        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

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30911 on_container_name_edited')

        app_obj = self.main_win_obj.app_obj

        # Check the entered text is a valid name
        if text == '' \
        or re.search(r'^\s*$', text) \
        or not app_obj.check_container_name_is_legal(text):
            return

        # Get the dbid for the selected line's channel/playlist
        model = self.treeview.get_model()
        tree_iter = model.get_iter(path)
        if tree_iter is not None and model[tree_iter][2] != text:

            # Check that the parent folder doesn't already have a container
            #   with the same name
            if (
                self.parent_dbid is not None \
                and app_obj.find_duplicate_name_in_container(
                    app_obj.media_reg_dict[self.parent_dbid],
                    text,
                )
            ) or (
                self.parent_dbid is None \
                and app_obj.find_duplicate_name_in_container(None, text)
            ):
                return

            # Otherwise, we can update the channel/playlist name
            model[tree_iter][2] = text


    def on_container_url_edited(self, widget, path, text):

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

        Updates the URL of a channel/playlist.

        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

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30961 on_container_url_edited')

        # Check the entered text is a valid URL
        if not utils.check_url(text):
            return

        # Get the dbid for the selected line's channel/playlist
        model = self.treeview.get_model()
        tree_iter = model.get_iter(path)
        if tree_iter is not None and model[tree_iter][3] != text:
            model[tree_iter][3] = text


    def on_convert_line_clicked(self, button):

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

        Converts the selected channel(s) to playlist(s), or vice-versa.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 30987 on_convert_line_clicked')

        selection = self.treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        for path in path_list:

            this_iter = model.get_iter(path)
            container_type = model[this_iter][0]
            if container_type == 'channel':

                model.set_value(this_iter, 0, 'playlist')
                model.set_value(
                    this_iter,
                    1,
                    self.main_win_obj.pixbuf_dict['playlist_small'],
                )

            else:

                model.set_value(this_iter, 0, 'channel')
                model.set_value(
                    this_iter,
                    1,
                    self.main_win_obj.pixbuf_dict['channel_small'],
                )


    def on_delete_line_clicked(self, button):

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

        Removes the selected line(s) from the treeview.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 31027 on_delete_line_clicked')

        selection = self.treeview.get_selection()
        (model, path_list) = selection.get_selected_rows()
        for path in path_list:
            model.remove(model.get_iter(path))


    def on_window_drag_data_received(self, window, context, x, y, data, info,
    time):

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

        Handles drag-and-drop anywhere in the dialogue window.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 31044 on_window_drag_data_received')

        utils.add_links_to_textview_from_clipboard(
            self.main_win_obj.app_obj,
            self.textbuffer,
            self.mark_start,
            self.mark_end,
            # Specify the drag-and-drop text, so the called function uses that,
            #   rather than the clipboard text
            data.get_text(),
        )


    def clipboard_timer_callback(self):

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

        Periodically checks the system's clipboard, and adds any new URLs to
        the dialogue window's textview.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 31066 clipboard_timer_callback')

        utils.add_links_to_textview_from_clipboard(
            self.main_win_obj.app_obj,
            self.textbuffer,
            self.mark_start,
            self.mark_end,
        )

        # Return 1 to keep the timer going
        return 1


class AddChannelDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.on_menu_add_channel().

    Python class handling a dialogue window that adds a channel to the media
    registry.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        suggest_parent_dbid (int): The .dbid of the new channel's suggested
            parent folder (which the user can change, if required), or None if
            this dialogue window shouldn't suggest a parent folder

        dl_sim_flag (bool): True if the 'Don't download anything' radiobutton
            should be made active immediately

        monitor_flag (bool): True if the 'Monitor the clipboard' checkbutton
            should be selected immediately

    """


    # Standard class methods


    def __init__(self, main_win_obj, suggest_parent_dbid=None,
    dl_sim_flag=False, monitor_flag=False):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 31110 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Add channel\' dialogue starts here. In' \
            + ' the main window toolbar, click the Channel button'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.entry = None                       # Gtk.Entry
        self.entry2 = None                      # Gtk.Entry
        self.frame = None                       # Gtk.Frame
        self.radiobutton = None                 # Gtk.RadioButton
        self.radiobutton2 = None                # Gtk.RadioButton
        self.checkbutton = None                 # Gtk.CheckButton


        # IV list - other
        # ---------------
        # A list of media.Folders (.dbid values) whose names should be
        #   displayed in the Gtk.ComboBox
        self.folder_list = []
        # The media.Folder .dbid selected in the combobox (if any)
        self.parent_dbid = None
        # Set up IVs for clipboard monitoring, if required
        self.clipboard_timer_id = None
        self.clipboard_timer_time = 250
        self.clipboard_ignore_url = None


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Add channel'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)

        label = Gtk.Label(_('Enter the channel name'))
        grid.attach(label, 0, 0, 1, 1)
        label2 = Gtk.Label()
        grid.attach(label2, 0, 1, 1, 1)
        label2.set_markup(
            '<i>' + _('(Use the channel\'s real name or a customised name)') \
            + '</i>',
        )

        self.entry = Gtk.Entry()
        grid.attach(self.entry, 0, 2, 1, 1)
        self.entry.set_hexpand(True)

        label3 = Gtk.Label(_('Copy and paste a link to the channel'))
        grid.attach(label3, 0, 3, 1, 1)

        self.entry2 = Gtk.Entry()
        grid.attach(self.entry2, 0, 4, 1, 1)
        self.entry2.set_hexpand(True)
        self.entry2.connect('changed', self.on_entry2_changed, grid)

        # Drag-and-drop onto the entry inevitably inserts a URL in the
        #   middle of another URL. No way to prevent that, but we can disable
        #   drag-and-drop in the entry altogether, and instead handle it
        #   from the dialogue window itself
        self.entry.drag_dest_unset()
        self.entry2.drag_dest_unset()
        self.connect('drag-data-received', self.on_window_drag_data_received)
        self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
        self.drag_dest_set_target_list(None)
        self.drag_dest_add_text_targets()

        # (The frame and its image are invisible, unless self.entry2 contains
        #   a YouTube URL that doesn't end with /videos or /playlists2)
        self.frame = Gtk.Frame()
        self.frame.set_tooltip_text(
            _(
            'Before adding the URL for a YouTube channel, first click the' \
            + ' Videos tab in your browser!',
            ),
        )

        image = Gtk.Image()
        self.frame.add(image)
        if main_win_obj.app_obj.current_locale == 'es':
            image.set_from_pixbuf(
                main_win_obj.pixbuf_dict['yt_remind_icon_es'],
            )
        elif main_win_obj.app_obj.current_locale == 'fr':
            image.set_from_pixbuf(
                main_win_obj.pixbuf_dict['yt_remind_icon_fr'],
            )
        elif main_win_obj.app_obj.current_locale == 'ko_KR':
            image.set_from_pixbuf(
                main_win_obj.pixbuf_dict['yt_remind_icon_kr'],
            )
        elif main_win_obj.app_obj.current_locale == 'nl_NL':
            image.set_from_pixbuf(
                main_win_obj.pixbuf_dict['yt_remind_icon_nl'],
            )
        elif main_win_obj.app_obj.current_locale == 'ru':
            image.set_from_pixbuf(
                main_win_obj.pixbuf_dict['yt_remind_icon_ru'],
            )
        elif main_win_obj.app_obj.current_locale == 'vi':
            image.set_from_pixbuf(
                main_win_obj.pixbuf_dict['yt_remind_icon_vi'],
            )
        else:
            image.set_from_pixbuf(
                main_win_obj.pixbuf_dict['yt_remind_icon_en'],
            )

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 6, 1, 1)

        # Prepare a list of folders to display in a combo. The list always
        #   includes the system folder 'Temporary Videos'
        # If a folder is selected in the Video Index, then it is the first item
        #   in the list. If not, 'No parent videos' is the first item in the
        #   list
        for media_data_obj in main_win_obj.app_obj.container_reg_dict.values():
            if isinstance(media_data_obj, media.Folder) \
            and not media_data_obj.fixed_flag \
            and media_data_obj.restrict_mode == 'open' \
            and media_data_obj.get_depth() \
            < main_win_obj.app_obj.container_max_level \
            and (
                suggest_parent_dbid is None
                or suggest_parent_dbid != media_data_obj.dbid
            ):
                self.folder_list.append(media_data_obj.dbid)

        self.folder_list.sort()
        self.folder_list.insert(0, None)
        self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.dbid)

        if suggest_parent_dbid is not None:
            self.folder_list.insert(0, suggest_parent_dbid)

        # Store the combobox's selected item, so the calling function can
        #   retrieve it
        self.parent_dbid = self.folder_list[0]

        label4 = Gtk.Label(_('(Optional) Add this channel inside a folder'))
        grid.attach(label4, 0, 7, 1, 1)

        listmodel = Gtk.ListStore(GdkPixbuf.Pixbuf, int, str)
        for dbid in self.folder_list:

            if dbid is None:
                pixbuf = main_win_obj.pixbuf_dict['slice_small']
                listmodel.append(
                    [pixbuf, dbid, '  ' + _('No parent folder')]
                )

            elif dbid == main_win_obj.app_obj.fixed_temp_folder.dbid:
                pixbuf = main_win_obj.pixbuf_dict['folder_blue_small']
                this_obj = main_win_obj.app_obj.fixed_temp_folder
                listmodel.append( [pixbuf, dbid, '  ' + this_obj.name] )

            else:
                pixbuf = main_win_obj.pixbuf_dict['folder_small']
                this_obj = main_win_obj.app_obj.container_reg_dict[dbid]
                listmodel.append( [pixbuf, dbid, '  ' + this_obj.name] )

        combo = Gtk.ComboBox.new_with_model(listmodel)
        grid.attach(combo, 0, 8, 1, 1)
        combo.set_hexpand(True)

        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, False)
        combo.add_attribute(renderer_text, 'text', 2)

        combo.set_active(0)
        combo.connect('changed', self.on_combo_changed)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 9, 1, 1)

        self.radiobutton = Gtk.RadioButton.new_with_label_from_widget(
            None,
            _('Enable video downloads for this channel'),
        )
        grid.attach(self.radiobutton, 0, 10, 1, 1)

        self.radiobutton2 = Gtk.RadioButton.new_from_widget(self.radiobutton)
        grid.attach(self.radiobutton2, 0, 11, 1, 1)
        self.radiobutton2.set_label(
            _('Don\'t download the videos, just check them'),
        )
        if dl_sim_flag:
            self.radiobutton2.set_active(True)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 12, 1, 1)

        self.checkbutton = Gtk.CheckButton()
        grid.attach(self.checkbutton, 0, 13, 1, 1)
        self.checkbutton.set_label(_('Enable automatic copy/paste'))
        self.checkbutton.connect('toggled', self.on_checkbutton_toggled)
        if monitor_flag:

            # Get the URL that would have been added to the Gtk.Entry, if we
            #   had not specified a True argument
            self.clipboard_ignore_url \
            = utils.add_links_to_entry_from_clipboard(
                self.main_win_obj.app_obj,
                self.entry2,
                None,
                None,
                True,
            )

            self.checkbutton.set_active(True)

        # Paste in the contents of the clipboard (if it contains at least one
        #   valid URL)
        if main_win_obj.app_obj.dialogue_copy_clipboard_flag \
        and not main_win_obj.app_obj.dialogue_keep_open_flag:
            utils.add_links_to_entry_from_clipboard(
                main_win_obj.app_obj,
                self.entry2,
                self.clipboard_ignore_url,
            )

        # Display the dialogue window
        self.show_all()


    # Callback class methods


    def on_checkbutton_toggled(self, checkbutton):

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

        Enables/disables clipboard monitoring.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 31380 on_checkbutton_toggled')

        if not checkbutton.get_active() \
        and self.clipboard_timer_id is not None:

            # Stop the timer
            GObject.source_remove(self.clipboard_timer_id)
            self.clipboard_timer_id = None

        elif checkbutton.get_active() and self.clipboard_timer_id is None:

            # Start the timer
            self.clipboard_timer_id = GObject.timeout_add(
                self.clipboard_timer_time,
                self.clipboard_timer_callback,
            )


    def on_combo_changed(self, combo):

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

        Store the combobox's selected item, so the calling function can
        retrieve it.

        Args:

            combo (Gtk.ComboBox): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 31412 on_combo_changed')

        self.parent_dbid = self.folder_list[combo.get_active()]


    def on_entry2_changed(self, entry, grid):

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

        When the entry containing a URL is changed to one containing a YouTube
        URL that doesn't end in /videos or /playlists, make the reminder icon
        visible (and vice-versa).

        Args:

            entry (Gtk.Entry): The clicked widget

            grid (Gtk.Grid): The grid on which the dialogue window is arranged

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 31434 on_entry2_changed')

        url = entry.get_text()
        enhanced = utils.is_enhanced(url)

        if self.main_win_obj.app_obj.dialogue_yt_remind_flag \
        and enhanced == 'youtube' \
        and not re.search(r'\/videos\s*$', url)\
        and not re.search(r'\/playlists\s*$', url):
            grid.attach(self.frame, 0, 5, 2, 1)
        else:
            for widget in grid.get_children():
                if widget == self.frame:
                    grid.remove(self.frame)
                    # The window is now too large vertically; this restores its
                    #   proper size
                    self.resize(1, 1)
                    break

        self.show_all()


    def on_window_drag_data_received(self, window, context, x, y, data, info,
    time):

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

        Handles drag-and-drop anywhere in the dialogue window.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 31465 on_window_drag_data_received')

        utils.add_links_to_entry_from_clipboard(
            self.main_win_obj.app_obj,
            self.entry2,
            self.clipboard_ignore_url,
            # Specify the drag-and-drop text, so the called function uses that,
            #   rather than the clipboard text
            data.get_text(),
        )


    def clipboard_timer_callback(self):

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

        Periodically checks the system's clipboard, and adds any new URLs to
        the dialogue window's entry.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 31486 clipboard_timer_callback')

        utils.add_links_to_entry_from_clipboard(
            self.main_win_obj.app_obj,
            self.entry2,
            self.clipboard_ignore_url,
        )

        # Return 1 to keep the timer going
        return 1


class AddDropZoneDialogue(Gtk.Dialog):

    """Called by mainwin.MainWin.drag_drop_add_dropzone().

    Prompt the user to select an existing options.OptionsManager object or to
    create a new one. The choice, if any, is added to the Drag and Drop tab as
    a mainwin.DropZoneBox.

    Based on code from mainwin.ApplyOptionsDialogue.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

    """


    # Standard class methods


    def __init__(self, main_win_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 31521 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Add dropzone dialogue starts here. In the' \
            + ' Drag and Drop tab, click the button in the top-right corner'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        #   (none)


        # IV list - other
        # ---------------
        # Store the user's choices as IVs, so the calling function can retrieve
        #   them
        self.options_name = None
        self.options_obj = None
        self.clone_flag = False


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Add dropzone'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(True)
        app_obj = self.main_win_obj.app_obj

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)

        radiobutton = Gtk.RadioButton.new_with_label_from_widget(
            None,
            _('Create new download options called'),
        )
        grid.attach(radiobutton, 0, 0, 1, 1)
        # (Signal connect appears below)

        entry = Gtk.Entry()
        grid.attach(entry, 0, 1, 1, 1)
        entry.grab_focus()
        # (Signal connect appears below)

        radiobutton2 = Gtk.RadioButton.new_with_label_from_widget(
            radiobutton,
            _('Use these download options'),
        )
        grid.attach(radiobutton2, 0, 2, 1, 1)
        radiobutton2.set_sensitive(False)
        # (Signal connect appears below)

        # Add a combo, containing any options.OptionsManager objects that are
        #   not already assigned to a mainwin.DropZoneBox
        store = Gtk.ListStore(str, int)

        for uid in sorted(app_obj.options_reg_dict):
            options_obj = app_obj.options_reg_dict[uid]

            if not options_obj.uid in app_obj.classic_dropzone_list:

                store.append([
                    '#' + str(options_obj.uid) + ': ' + options_obj.name,
                    options_obj.uid,
                ])
                radiobutton2.set_sensitive(True)

        combo = Gtk.ComboBox.new_with_model(store)
        grid.attach(combo, 0, 3, 1, 1)
        combo.set_hexpand(True)
        combo.set_sensitive(False)
        # (Signal connect appears below)

        cell = Gtk.CellRendererText()
        combo.pack_start(cell, False)
        combo.add_attribute(cell, 'text', 0)
        combo.set_active(0)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 4, 1, 1)

        radiobutton3 = Gtk.RadioButton.new_with_label_from_widget(
            radiobutton2,
            _('Clone these download options'),
        )
        grid.attach(radiobutton3, 0, 5, 1, 1)
        # (Signal connect appears below)

        # Add a combo, containing all options.OptionsManager objects
        store2 = Gtk.ListStore(str, int)

        for uid in sorted(app_obj.options_reg_dict):

            options_obj = app_obj.options_reg_dict[uid]
            store2.append([
                '#' + str(options_obj.uid) + ': ' + options_obj.name,
                options_obj.uid,
            ])

        combo2 = Gtk.ComboBox.new_with_model(store2)
        grid.attach(combo2, 0, 6, 1, 1)
        combo2.set_hexpand(True)
        combo2.set_sensitive(False)
        # (Signal connect appears below)

        cell = Gtk.CellRendererText()
        combo2.pack_start(cell, False)
        combo2.add_attribute(cell, 'text', 0)
        combo2.set_active(0)

        # (Signal connects from above)
        radiobutton.connect(
            'toggled',
            self.on_radiobutton_toggled,
            entry,
            combo,
            combo2,
        )
        entry.connect('changed', self.on_entry_changed)
        radiobutton2.connect(
            'toggled',
            self.on_radiobutton2_toggled,
            entry,
            combo,
            combo2,
        )
        combo.connect('changed', self.on_combo_changed)
        radiobutton3.connect(
            'toggled',
            self.on_radiobutton3_toggled,
            entry,
            combo,
            combo2,
        )
        combo2.connect('changed', self.on_combo2_changed)

        # Display the dialogue window
        self.show_all()


    # Callback class methods


    def on_combo_changed(self, combo):

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

        Store the combobox's selected item, so the calling function can
        retrieve it.

        Args:

            combo (Gtk.ComboBox): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 31698 on_combo_changed')

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        uid = model[tree_iter][1]

        self.options_obj = self.main_win_obj.app_obj.options_reg_dict[uid]
        self.clone_flag = False


    def on_combo2_changed(self, combo):

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

        Store the combobox's selected item, so the calling function can
        retrieve it.

        Args:

            combo (Gtk.ComboBox): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 31722 on_combo2_changed')

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        uid = model[tree_iter][1]

        self.options_obj = self.main_win_obj.app_obj.options_reg_dict[uid]
        self.clone_flag = True


    def on_entry_changed(self, entry):

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

        Store the entry's text, so the calling function can retrieve it.

        Args:

            entry (Gtk.Entry): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 31745 on_entry_changed')

        name = entry.get_text()
        if name != '':
            self.options_name = name
        else:
            self.options_name = None


    def on_radiobutton_toggled(self, button, entry, combo, combo2):

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

        User has selected to create a new options manager.

        Args:

            button (Gtk.RadioButton): The widget clicked

            entry (Gtk.Entry): Another widget to update

            combo, combo2 (Gtk.ComboBox): Other widgets to update

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 31771 on_radiobutton_toggled')

        if button.get_active():

            self.options_obj = None
            self.clone_flag = False

            entry.set_sensitive(True)
            combo.set_sensitive(False)
            combo2.set_sensitive(False)


    def on_radiobutton2_toggled(self, button, entry, combo, combo2):

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

        User wants to select an existing options manager.

        Args:

            button (Gtk.RadioButton): The widget clicked

            entry (Gtk.Entry): Another widget to update

            combo, combo2 (Gtk.ComboBox): Other widgets to update

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 31800 on_radiobutton2_toggled')

        if button.get_active():

            tree_iter = combo.get_active_iter()
            model = combo.get_model()
            uid = model[tree_iter][1]

            self.options_obj = self.main_win_obj.app_obj.options_reg_dict[uid]
            self.clone_flag = False

            entry.set_text('')
            entry.set_sensitive(False)
            combo.set_sensitive(True)
            combo2.set_sensitive(False)


    def on_radiobutton3_toggled(self, button, entry, combo, combo2):

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

        User wants to clone an existing options manager.

        Args:

            button (Gtk.RadioButton): The widget clicked

            entry (Gtk.Entry): Another widget to update

            combo, combo2 (Gtk.ComboBox): Other widgets to update

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 31834 on_radiobutton3_toggled')

        if button.get_active():

            tree_iter = combo2.get_active_iter()
            model = combo2.get_model()
            uid = model[tree_iter][1]

            self.options_obj = self.main_win_obj.app_obj.options_reg_dict[uid]
            self.clone_flag = True

            entry.set_text('')
            entry.set_sensitive(False)
            combo.set_sensitive(False)
            combo2.set_sensitive(True)


class AddFolderDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.on_menu_add_folder().

    Python class handling a dialogue window that adds a folder to the media
    registry.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        suggest_parent_dbid (int): The .dbid of the new folder's suggested
            parent folder (which the user can change, if required), or None if
            this dialogue window shouldn't suggest a parent folder

    """


    # Standard class methods


    def __init__(self, main_win_obj, suggest_parent_dbid=None):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 31875 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Add folder\' dialogue starts here. In' \
            + ' the main window toolbar, click the Folder button'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.entry = None                       # Gtk.Entry
        self.radiobutton = None                 # Gtk.RadioButton
        self.radiobutton2 = None                # Gtk.RadioButton


        # IV list - other
        # ---------------
        # A list of media.Folders (.dbid values) whose names should be
        #   displayed in the Gtk.ComboBox
        self.folder_list = []
        # The media.Folder .dbid selected in the combobox (if any)
        self.parent_dbid = None


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Add folder'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)

        label = Gtk.Label(_('Enter the folder name'))
        grid.attach(label, 0, 0, 1, 1)

        # (Store various widgets as IVs, so the calling function can retrieve
        #   their contents)
        self.entry = Gtk.Entry()
        grid.attach(self.entry, 0, 1, 1, 1)
        self.entry.set_hexpand(True)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 2, 1, 1)

        # Prepare a list of folders to display in a combo. The list always
        #   includes the system folder 'Temporary Videos'
        # If a folder is selected in the Video Index, then it is the first one
        #   in the list. If not, 'No parent videos' is the first one in the
        #   list
        for media_data_obj in main_win_obj.app_obj.container_reg_dict.values():
            if isinstance(media_data_obj, media.Folder) \
            and not media_data_obj.fixed_flag \
            and media_data_obj.restrict_mode != 'full' \
            and media_data_obj.get_depth() \
            < main_win_obj.app_obj.container_max_level \
            and (
                suggest_parent_dbid is None
                or suggest_parent_dbid != media_data_obj.dbid
            ):
                self.folder_list.append(media_data_obj.dbid)

        self.folder_list.sort()
        self.folder_list.insert(0, None)
        self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.dbid)

        if suggest_parent_dbid is not None:
            self.folder_list.insert(0, suggest_parent_dbid)

        # Store the combobox's selected item, so the calling function can
        #   retrieve it
        self.parent_dbid = self.folder_list[0]

        label4 = Gtk.Label(
            _('(Optional) Add this folder inside another folder'),
        )
        grid.attach(label4, 0, 3, 1, 1)

        listmodel = Gtk.ListStore(GdkPixbuf.Pixbuf, int, str)
        for dbid in self.folder_list:

            if dbid is None:
                pixbuf = main_win_obj.pixbuf_dict['slice_small']
                listmodel.append(
                    [pixbuf, dbid, '  ' + _('No parent folder')]
                )

            elif dbid == main_win_obj.app_obj.fixed_temp_folder.dbid:
                pixbuf = main_win_obj.pixbuf_dict['folder_blue_small']
                this_obj = main_win_obj.app_obj.fixed_temp_folder
                listmodel.append( [pixbuf, dbid, '  ' + this_obj.name] )

            else:
                pixbuf = main_win_obj.pixbuf_dict['folder_small']
                this_obj = main_win_obj.app_obj.container_reg_dict[dbid]
                listmodel.append( [pixbuf, dbid, '  ' + this_obj.name] )

        combo = Gtk.ComboBox.new_with_model(listmodel)
        grid.attach(combo, 0, 4, 1, 1)
        combo.set_hexpand(True)

        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, False)
        combo.add_attribute(renderer_text, 'text', 2)

        combo.set_active(0)
        combo.connect('changed', self.on_combo_changed)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 5, 1, 1)

        self.radiobutton = Gtk.RadioButton.new_with_label_from_widget(
            None,
            _('Enable video downloads for this folder'),
        )
        grid.attach(self.radiobutton, 0, 6, 1, 1)

        self.radiobutton2 = Gtk.RadioButton.new_from_widget(self.radiobutton)
        self.radiobutton2.set_label(
            _('Don\'t download the videos, just check them'),
        )
        grid.attach(self.radiobutton2, 0, 7, 1, 1)

        # Display the dialogue window
        self.show_all()


    # Callback class methods


    def on_combo_changed(self, combo):

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

        Store the combobox's selected item, so the calling function can
        retrieve it.

        Args:

            combo (Gtk.ComboBox): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 32043 on_combo_changed')

        self.parent_dbid = self.folder_list[combo.get_active()]


class AddPlaylistDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.on_menu_add_playlist().

    Python class handling a dialogue window that adds a playlist to the
    media registry.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        suggest_parent_dbid (int): The .dbid of the new playlist's suggested
            parent folder (which the user can change, if required), or None if
            this dialogue window shouldn't suggest a parent folder

        dl_sim_flag (bool): True if the 'Don't download anything' radiobutton
            should be made active immediately

        monitor_flag (bool): True if the 'Monitor the clipboard' checkbutton
            should be selected immediately

    """


    # Standard class methods


    def __init__(self, main_win_obj, suggest_parent_dbid=None,
    dl_sim_flag=False, monitor_flag=False):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 32079 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Add playlist\' dialogue starts here. In' \
            + ' the main window toolbar, click the Playlist button'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.entry = None                       # Gtk.Entry
        self.entry2 = None                      # Gtk.Entry
        self.radiobutton = None                 # Gtk.RadioButton
        self.radiobutton2 = None                # Gtk.RadioButton
        self.checkbutton = None                 # Gtk.CheckButton


        # IV list - other
        # ---------------
        # A list of media.Folders to display in the Gtk.ComboBox
        self.folder_list = []
        # The media.Folder .dbid selected in the combobox (if any)
        self.parent_dbid = None
        # Set up IVs for clipboard monitoring, if required
        self.clipboard_timer_id = None
        self.clipboard_timer_time = 250
        self.clipboard_ignore_url = None


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Add playlist'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)

        label = Gtk.Label(_('Enter the playlist name'))
        grid.attach(label, 0, 0, 1, 1)
        label2 = Gtk.Label()
        grid.attach(label2, 0, 1, 1, 1)
        label2.set_markup(
            '<i>' + _('(Use the playlist\'s real name or a customised name)') \
            + '</i>',
        )

        self.entry = Gtk.Entry()
        grid.attach(self.entry, 0, 2, 1, 1)
        self.entry.set_hexpand(True)

        label3 = Gtk.Label(_('Copy and paste a link to the playlist'))
        grid.attach(label3, 0, 3, 1, 1)

        self.entry2 = Gtk.Entry()
        grid.attach(self.entry2, 0, 4, 1, 1)
        self.entry2.set_hexpand(True)

        # Drag-and-drop onto the entry inevitably inserts a URL in the
        #   middle of another URL. No way to prevent that, but we can disable
        #   drag-and-drop in the entry altogether, and instead handle it
        #   from the dialogue window itself
        self.entry.drag_dest_unset()
        self.entry2.drag_dest_unset()
        self.connect('drag-data-received', self.on_window_drag_data_received)
        self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
        self.drag_dest_set_target_list(None)
        self.drag_dest_add_text_targets()

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 5, 1, 1)

        # Prepare a list of folders to display in a combo. The list always
        #   includes the system folder 'Temporary Videos'
        # If a folder is selected in the Video Index, then it is the first item
        #   in the list. If not, 'No parent videos' is the first item in the
        #   list
        for media_data_obj in main_win_obj.app_obj.container_reg_dict.values():
            if isinstance(media_data_obj, media.Folder) \
            and not media_data_obj.fixed_flag \
            and media_data_obj.restrict_mode == 'open' \
            and media_data_obj.get_depth() \
            < main_win_obj.app_obj.container_max_level \
            and (
                suggest_parent_dbid is None
                or suggest_parent_dbid != media_data_obj.dbid
            ):
                self.folder_list.append(media_data_obj.dbid)

        self.folder_list.sort()
        self.folder_list.insert(0, None)
        self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.dbid)

        if suggest_parent_dbid is not None:
            self.folder_list.insert(0, suggest_parent_dbid)

        # Store the combobox's selected item, so the calling function can
        #   retrieve it
        self.parent_dbid = self.folder_list[0]

        label4 = Gtk.Label(_('(Optional) Add this playlist inside a folder'))
        grid.attach(label4, 0, 6, 1, 1)

        listmodel = Gtk.ListStore(GdkPixbuf.Pixbuf, int, str)
        for dbid in self.folder_list:

            if dbid is None:
                pixbuf = main_win_obj.pixbuf_dict['slice_small']
                listmodel.append(
                    [pixbuf, dbid, '  ' + _('No parent folder')]
                )

            elif dbid == main_win_obj.app_obj.fixed_temp_folder.dbid:
                pixbuf = main_win_obj.pixbuf_dict['folder_blue_small']
                this_obj = main_win_obj.app_obj.fixed_temp_folder
                listmodel.append( [pixbuf, dbid, '  ' + this_obj.name] )

            else:
                pixbuf = main_win_obj.pixbuf_dict['folder_small']
                this_obj = main_win_obj.app_obj.container_reg_dict[dbid]
                listmodel.append( [pixbuf, dbid, '  ' + this_obj.name] )

        combo = Gtk.ComboBox.new_with_model(listmodel)
        grid.attach(combo, 0, 7, 1, 1)
        combo.set_hexpand(True)

        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, False)
        combo.add_attribute(renderer_text, 'text', 2)

        combo.set_active(0)
        combo.connect('changed', self.on_combo_changed)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 8, 1, 1)

        self.radiobutton = Gtk.RadioButton.new_with_label_from_widget(
            None,
            _('Enable video downloads for this playlist'),
        )
        grid.attach(self.radiobutton, 0, 9, 1, 1)

        self.radiobutton2 = Gtk.RadioButton.new_from_widget(self.radiobutton)
        grid.attach(self.radiobutton2, 0, 10, 1, 1)
        self.radiobutton2.set_label(
            _('Don\'t download the videos, just check them'),
        )
        if dl_sim_flag:
            self.radiobutton2.set_active(True)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 11, 1, 1)

        self.checkbutton = Gtk.CheckButton()
        grid.attach(self.checkbutton, 0, 12, 1, 1)
        self.checkbutton.set_label(_('Enable automatic copy/paste'))
        self.checkbutton.connect('toggled', self.on_checkbutton_toggled)
        if monitor_flag:

            # Get the URL that would have been added to the Gtk.Entry, if we
            #   had not specified a True argument
            self.clipboard_ignore_url \
            = utils.add_links_to_entry_from_clipboard(
                self.main_win_obj.app_obj,
                self.entry2,
                None,
                None,
                True,
            )

            self.checkbutton.set_active(True)

        # Paste in the contents of the clipboard (if it contains at least one
        #   valid URL)
        if main_win_obj.app_obj.dialogue_copy_clipboard_flag \
        and not main_win_obj.app_obj.dialogue_keep_open_flag:
            utils.add_links_to_entry_from_clipboard(
                main_win_obj.app_obj,
                self.entry2,
                self.clipboard_ignore_url,
            )

        # Display the dialogue window
        self.show_all()


    # Callback class methods


    def on_checkbutton_toggled(self, checkbutton):

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

        Enables/disables clipboard monitoring.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 32305 on_checkbutton_toggled')

        if not checkbutton.get_active() \
        and self.clipboard_timer_id is not None:

            # Stop the timer
            GObject.source_remove(self.clipboard_timer_id)
            self.clipboard_timer_id = None

        elif checkbutton.get_active() and self.clipboard_timer_id is None:

            # Start the timer
            self.clipboard_timer_id = GObject.timeout_add(
                self.clipboard_timer_time,
                self.clipboard_timer_callback,
            )


    def on_combo_changed(self, combo):

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

        Store the combobox's selected item, so the calling function can
        retrieve it.

        Args:

            combo (Gtk.ComboBox): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 32337 on_combo_changed')

        self.parent_dbid = self.folder_list[combo.get_active()]


    def on_window_drag_data_received(self, window, context, x, y, data, info,
    time):

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

        Handles drag-and-drop anywhere in the dialogue window.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 32351 on_window_drag_data_received')

        utils.add_links_to_entry_from_clipboard(
            self.main_win_obj.app_obj,
            self.entry2,
            self.clipboard_ignore_url,
            # Specify the drag-and-drop text, so the called function uses that,
            #   rather than the clipboard text
            data.get_text(),
        )


    def clipboard_timer_callback(self):

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

        Periodically checks the system's clipboard, and adds any new URLs to
        the dialogue window's entry.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 32372 clipboard_timer_callback')

        utils.add_links_to_entry_from_clipboard(
            self.main_win_obj.app_obj,
            self.entry2,
            self.clipboard_ignore_url,
        )

        # Return 1 to keep the timer going
        return 1


class AddStampDialogue(Gtk.Dialog):

    """Called by config.VideoEditWin.on_copy_stamp_button_clicked().

    Python class handling a dialogue window that allows the user to copy/paste
    timestamp data (for example, from a video's description), which is then
    converted into media.Video.stamp_list.

    Args:

        parent_win_obj (mainwin.MainWin or config.OptionsEditWin): The parent
            window

        main_win_obj (mainwin.MainWin): The main window (parent or not)

    """


    # Standard class methods


    def __init__(self, parent_win_obj, main_win_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 32408 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Reset timestamps\' dialogue starts here.' \
            + ' In the Videos tab, right-click a video and select Show Video' \
            + ' > Properties... > Timestamps > Reset list using copied text'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.parent_win_obj = parent_win_obj
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.grid = None                        # Gtk.Grid
        self.textbuffer = None                  # Gtk.TextBuffer
        self.mark_start = None                  # Gtk.TextMark
        self.mark_end = None                    # Gtk.TextMark


        # IV list - other
        # ---------------
        # (none)


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Reset timestamps'),
            parent_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)

        # Set up the dialogue window
        box = self.get_content_area()

        self.grid = Gtk.Grid()
        box.add(self.grid)
        self.grid.set_border_width(main_win_obj.spacing_size)
        self.grid.set_row_spacing(main_win_obj.spacing_size)

        label = Gtk.Label()
        self.grid.attach(label, 0, 0, 1, 1)
        label.set_markup(
            _('Copy timestamps below, e.g.') \
            + '   <b>0:30 ' + _('Introduction') + '</b>',
        )
        label.set_xalign(0)

        # Add a textview
        frame = Gtk.Frame()
        self.grid.attach(frame, 0, 1, 1, 1)
        # (Use the same textview width as AddBulkDialogue)
        frame.set_size_request(
            main_win_obj.app_obj.config_win_width - 150,
            250,
        )

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)

        textview = Gtk.TextView()
        scrolled.add(textview)
        textview.set_hexpand(True)
        self.textbuffer = textview.get_buffer()

        # Some callbacks will complain about invalid iterators, if we try to
        #   use Gtk.TextIters, so use Gtk.TextMarks instead
        self.mark_start = self.textbuffer.create_mark(
            'mark_start',
            self.textbuffer.get_start_iter(),
            True,               # Left gravity
        )
        self.mark_end = self.textbuffer.create_mark(
            'mark_end',
            self.textbuffer.get_end_iter(),
            False,              # Not left gravity
        )

        # Drag-and-drop onto the textview inevitably inserts text inside
        #   existing text. No way to prevent that, but we can disable
        #   drag-and-drop in the textview altogether, and instead handle it
        #   from the dialogue window itself
        textview.drag_dest_unset()
        self.connect('destroy', self.close, textview)

        self.connect('drag-data-received', self.on_window_drag_data_received)
        self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
        self.drag_dest_set_target_list(None)
        self.drag_dest_add_text_targets()

        # Separator
        self.grid.attach(Gtk.HSeparator(), 0, 2, 1, 1)

        # Display the dialogue window
        self.show_all()


    def close(self, also_self, textview):

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

        We have disabled drag-and-drop directly into the textview, but this
        generates a Gtk warning when the textview is destroyed.

        Workaround is to re-enable drag-and-drop into the textview immediately
        before it (and its window) are destroyed.

        Args:

            also_self (mainwin.AddStampDialogue): Another copy of this window

            textview (Gtk.TextView): The textview for which drag-and-drop has
                been disabled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 32537 close')

        textview.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)


    # Callback class methods


    def on_window_drag_data_received(self, window, context, x, y, data, info,
    time):

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

        Handles drag-and-drop anywhere in the dialogue window.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 32554 on_window_drag_data_received')

        utils.add_links_to_textview_from_clipboard(
            self.main_win_obj.app_obj,
            self.textbuffer,
            self.mark_start,
            self.mark_end,
            # Specify the drag-and-drop text, so the called function uses that,
            #   rather than the clipboard text
            data.get_text(),
        )


class AddVideoDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.on_menu_add_video().

    Python class handling a dialogue window that adds invidual video(s) to
    the media registry.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

    """


    # Standard class methods


    def __init__(self, main_win_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 32587 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Add video\' dialogue starts here. In' \
            + ' the main window toolbar, click the Video button'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.textbuffer = None                  # Gtk.TextBuffer
        self.mark_start = None                  # Gtk.TextMark
        self.mark_end = None                    # Gtk.TextMark
        self.radiobutton = None                 # Gtk.RadioButton
        self.radiobutton2 = None                # Gtk.RadioButton
        self.checkbutton = None                 # Gtk.CheckButton


        # IV list - other
        # ---------------
        # A list of media.Folders (.dbid values) whose names should be
        #   displayed in the Gtk.ComboBox
        self.folder_list = []
        # The media.Folder .dbid selected in the combobox (if any)
        self.parent_dbid = None
        # Set up IVs for clipboard monitoring, if required
        self.clipboard_timer_id = None
        self.clipboard_timer_time = 250


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Add videos'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)

        label = Gtk.Label(_('Copy and paste the links to one or more videos'))
        grid.attach(label, 0, 0, 1, 1)

        if main_win_obj.app_obj.operation_convert_mode == 'channel':

            text = _(
                'Links containing multiple videos will be converted to' \
                + ' a channel',
            )

        elif main_win_obj.app_obj.operation_convert_mode == 'playlist':

            text = _(
                'Links containing multiple videos will be converted to a' \
                + ' playlist',
            )

        elif main_win_obj.app_obj.operation_convert_mode == 'multi':

            text = _(
                'Links containing multiple videos will be downloaded' \
                + ' separately',
            )

        elif main_win_obj.app_obj.operation_convert_mode == 'disable':

            text = _(
                'Links containing multiple videos will not be downloaded'
                + ' at all',
            )

        label = Gtk.Label()
        label.set_markup('<i>' + text + '</i>')
        grid.attach(label, 0, 1, 1, 1)

        frame = Gtk.Frame()
        grid.attach(frame, 0, 2, 1, 1)

        scrolledwindow = Gtk.ScrolledWindow()
        frame.add(scrolledwindow)
        # (Set enough vertical room for at several URLs)
        scrolledwindow.set_size_request(-1, 150)

        textview = Gtk.TextView()
        scrolledwindow.add(textview)
        textview.set_hexpand(True)
        self.textbuffer = textview.get_buffer()

        # Some callbacks will complain about invalid iterators, if we try to
        #   use Gtk.TextIters, so use Gtk.TextMarks instead
        self.mark_start = self.textbuffer.create_mark(
            'mark_start',
            self.textbuffer.get_start_iter(),
            True,               # Left gravity
        )
        self.mark_end = self.textbuffer.create_mark(
            'mark_end',
            self.textbuffer.get_end_iter(),
            False,              # Not left gravity
        )

        # Drag-and-drop onto the textview inevitably inserts a URL in the
        #   middle of another URL. No way to prevent that, but we can disable
        #   drag-and-drop in the textview altogether, and instead handle it
        #   from the dialogue window itself
        textview.drag_dest_unset()
        self.connect('destroy', self.close, textview)

        self.connect('drag-data-received', self.on_window_drag_data_received)
        self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
        self.drag_dest_set_target_list(None)
        self.drag_dest_add_text_targets()

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 3, 1, 1)

        # Prepare a list of folders to display in a combo. The list always
        #   includes the system folders 'Unsorted Videos', 'Video Clips' and
        #   'Temporary Videos'
        # If a folder is selected in the Video Index, then it is the first one
        #   in the list. If not, 'Unsorted Videos' is the first one in the
        #   list
        folder_obj = None
        # The selected item in the Video Index could be a channel, playlist or
        #   folder, but here we only pay attention to folders
        selected_dbid = main_win_obj.video_index_current_dbid
        if selected_dbid:
            container_obj = main_win_obj.app_obj.media_reg_dict[selected_dbid]
            if isinstance(container_obj, media.Folder) \
            and not container_obj.priv_flag:
                folder_obj = container_obj

        for media_data_obj in main_win_obj.app_obj.container_reg_dict.values():
            if isinstance(media_data_obj, media.Folder) \
            and not media_data_obj.fixed_flag \
            and media_data_obj.restrict_mode == 'open' \
            and (folder_obj is None or media_data_obj != folder_obj):
                self.folder_list.append(media_data_obj.dbid)

        self.folder_list.sort()
        self.folder_list.insert(0, main_win_obj.app_obj.fixed_misc_folder.dbid)
        self.folder_list.insert(
            1,
            main_win_obj.app_obj.fixed_clips_folder.dbid,
        )
        self.folder_list.insert(2, main_win_obj.app_obj.fixed_temp_folder.dbid)

        if folder_obj:
            self.folder_list.insert(0, folder_obj.dbid)

        # Store the combobox's selected item, so the calling function can
        #   retrieve it
        self.parent_dbid = self.folder_list[0]

        label2 = Gtk.Label(_('Add the videos to this folder'))
        grid.attach(label2, 0, 4, 1, 1)

        listmodel = Gtk.ListStore(GdkPixbuf.Pixbuf, int, str)
        for dbid in self.folder_list:

            if dbid is None:
                pixbuf = main_win_obj.pixbuf_dict['slice_small']
                listmodel.append(
                    [pixbuf, dbid, '  ' + _('No parent folder')]
                )

            elif dbid == main_win_obj.app_obj.fixed_misc_folder.dbid:
                pixbuf = main_win_obj.pixbuf_dict['folder_green_small']
                this_obj = main_win_obj.app_obj.fixed_misc_folder
                listmodel.append( [pixbuf, dbid, '  ' + this_obj.name] )

            elif dbid == main_win_obj.app_obj.fixed_clips_folder.dbid:
                pixbuf = main_win_obj.pixbuf_dict['folder_green_small']
                this_obj = main_win_obj.app_obj.fixed_clips_folder
                listmodel.append( [pixbuf, dbid, '  ' + this_obj.name] )

            elif dbid == main_win_obj.app_obj.fixed_temp_folder.dbid:
                pixbuf = main_win_obj.pixbuf_dict['folder_blue_small']
                this_obj = main_win_obj.app_obj.fixed_temp_folder
                listmodel.append( [pixbuf, dbid, '  ' + this_obj.name] )

            else:
                pixbuf = main_win_obj.pixbuf_dict['folder_small']
                this_obj = main_win_obj.app_obj.container_reg_dict[dbid]
                listmodel.append( [pixbuf, dbid, '  ' + this_obj.name] )

        combo = Gtk.ComboBox.new_with_model(listmodel)
        grid.attach(combo, 0, 5, 1, 1)
        combo.set_hexpand(True)

        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, False)
        combo.add_attribute(renderer_text, 'text', 2)

        combo.set_active(0)
        combo.connect('changed', self.on_combo_changed)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 6, 1, 1)

        self.radiobutton = Gtk.RadioButton.new_with_label_from_widget(
            None,
            _('I want to download these videos'),
        )
        grid.attach(self.radiobutton, 0, 7, 1, 1)

        self.radiobutton2 = Gtk.RadioButton.new_from_widget(self.radiobutton)
        self.radiobutton2.set_label(
            _('Don\'t download the videos, just check them'),
        )
        grid.attach(self.radiobutton2, 0, 8, 1, 1)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 9, 1, 1)

        self.checkbutton = Gtk.CheckButton()
        grid.attach(self.checkbutton, 0, 10, 1, 1)
        self.checkbutton.set_label(_('Enable automatic copy/paste'))
        self.checkbutton.connect('toggled', self.on_checkbutton_toggled)

        # Paste in the contents of the clipboard (if it contains valid URLs)
        if main_win_obj.app_obj.dialogue_copy_clipboard_flag:
            utils.add_links_to_textview_from_clipboard(
                main_win_obj.app_obj,
                self.textbuffer,
                self.mark_start,
                self.mark_end,
            )

        # Display the dialogue window
        self.show_all()


    def close(self, also_self, textview):

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

        We have disabled drag-and-drop directly into the textview, but this
        generates a Gtk warning when the textview is destroyed.

        Workaround is to re-enable drag-and-drop into the textview immediately
        before it (and its window) are destroyed.

        Args:

            also_self (mainwin.AddVideoDialogue): Another copy of this window

            textview (Gtk.TextView): The textview for which drag-and-drop has
                been disabled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 32863 close')

        textview.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)


    # Callback class methods


    def on_checkbutton_toggled(self, checkbutton):

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

        Enables/disables clipboard monitoring.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 32884 on_checkbutton_toggled')

        if not checkbutton.get_active() \
        and self.clipboard_timer_id is not None:

            # Stop the timer
            GObject.source_remove(self.clipboard_timer_id)
            self.clipboard_timer_id = None

        elif checkbutton.get_active() and self.clipboard_timer_id is None:

            # Start the timer
            self.clipboard_timer_id = GObject.timeout_add(
                self.clipboard_timer_time,
                self.clipboard_timer_callback,
            )


    def on_combo_changed(self, combo):

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

        Updates the combobox's selected item, so the calling function can
        retrieve it.

        Args:

            combo (Gtk.ComboBox): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 32916 on_combo_changed')

        self.parent_dbid = self.folder_list[combo.get_active()]


    def on_window_drag_data_received(self, window, context, x, y, data, info,
    time):

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

        Handles drag-and-drop anywhere in the dialogue window.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 32930 on_window_drag_data_received')

        utils.add_links_to_textview_from_clipboard(
            self.main_win_obj.app_obj,
            self.textbuffer,
            self.mark_start,
            self.mark_end,
            # Specify the drag-and-drop text, so the called function uses that,
            #   rather than the clipboard text
            data.get_text(),
        )


    def clipboard_timer_callback(self):

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

        Periodically checks the system's clipboard, and adds any new URLs to
        the dialogue window's textview.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 32952 clipboard_timer_callback')

        utils.add_links_to_textview_from_clipboard(
            self.main_win_obj.app_obj,
            self.textbuffer,
            self.mark_start,
            self.mark_end,
        )

        # Return 1 to keep the timer going
        return 1


class ApplyOptionsDialogue(Gtk.Dialog):

    """Called by mainwin.MainWin.on_video_index_apply_options() and
    .on_video_catalogue_apply_options().

    Prompt the user to specify whether a new options.OptionsManager object or
    and existing one should be applied to a specified media data object.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

    """


    # Standard class methods


    def __init__(self, main_win_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 32986 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Apply download options\' dialogue starts' \
            + ' here. In the Videos tab, right-click a channel, playlist or' \
            + ' folder and select Downloads > Apply download options...'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        #   (none)


        # IV list - other
        # ---------------
        # Store the user's choices as IVs, so the calling function can retrieve
        #   them
        self.options_obj = None
        self.clone_flag = False


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Apply download options'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(True)
        app_obj = self.main_win_obj.app_obj

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)

        radiobutton = Gtk.RadioButton.new_with_label_from_widget(
            None,
            _('Create new download options'),
        )
        grid.attach(radiobutton, 0, 0, 1, 1)
        # (Signal connect appears below)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 1, 1, 1)

        radiobutton2 = Gtk.RadioButton.new_with_label_from_widget(
            radiobutton,
            _('Use these download options'),
        )
        grid.attach(radiobutton2, 0, 2, 1, 1)
        # (Signal connect appears below)

        # Add a combo, containing all options.OptionsManager objects (besides
        #   the General Options Manager)
        store = Gtk.ListStore(str, int)
        for uid in sorted(app_obj.options_reg_dict):

            options_obj = app_obj.options_reg_dict[uid]
            if options_obj != app_obj.general_options_obj:

                store.append([
                    '#' + str(options_obj.uid) + ': ' + options_obj.name,
                    options_obj.uid,
                ])

        combo = Gtk.ComboBox.new_with_model(store)
        grid.attach(combo, 0, 3, 1, 1)
        combo.set_hexpand(True)
        combo.set_sensitive(False)
        # (Signal connect appears below)

        cell = Gtk.CellRendererText()
        combo.pack_start(cell, False)
        combo.add_attribute(cell, 'text', 0)
        combo.set_active(0)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 4, 1, 1)

        radiobutton3 = Gtk.RadioButton.new_with_label_from_widget(
            radiobutton2,
            _('Clone these download options'),
        )
        grid.attach(radiobutton3, 0, 5, 1, 1)
        # (Signal connect appears below)

        # Add a combo, containing all options.OptionsManager objects
        store2 = Gtk.ListStore(str, int)
        for uid in sorted(app_obj.options_reg_dict):

            options_obj = app_obj.options_reg_dict[uid]
            store2.append([
                '#' + str(options_obj.uid) + ': ' + options_obj.name,
                options_obj.uid,
            ])

        combo2 = Gtk.ComboBox.new_with_model(store2)
        grid.attach(combo2, 0, 6, 1, 1)
        combo2.set_hexpand(True)
        combo2.set_sensitive(False)
        # (Signal connect appears below)

        cell = Gtk.CellRendererText()
        combo2.pack_start(cell, False)
        combo2.add_attribute(cell, 'text', 0)
        combo2.set_active(0)

        # (Signal connects from above)
        radiobutton.connect(
            'toggled',
            self.on_radiobutton_toggled,
            combo,
            combo2,
        )
        radiobutton2.connect(
            'toggled',
            self.on_radiobutton2_toggled,
            combo,
            combo2,
        )
        combo.connect('changed', self.on_combo_changed)
        radiobutton3.connect(
            'toggled',
            self.on_radiobutton3_toggled,
            combo,
            combo2,
        )
        combo2.connect('changed', self.on_combo2_changed)

        # Display the dialogue window
        self.show_all()


    # Callback class methods


    def on_combo_changed(self, combo):

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

        Store the combobox's selected item, so the calling function can
        retrieve it.

        Args:

            combo (Gtk.ComboBox): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 33153 on_combo_changed')

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        uid = model[tree_iter][1]

        self.options_obj = self.main_win_obj.app_obj.options_reg_dict[uid]
        self.clone_flag = False


    def on_combo2_changed(self, combo2):

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

        Store the combobox's selected item, so the calling function can
        retrieve it.

        Args:

            combo2 (Gtk.ComboBox): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 33177 on_combo2_changed')

        tree_iter = combo2.get_active_iter()
        model = combo2.get_model()
        uid = model[tree_iter][1]

        self.options_obj = self.main_win_obj.app_obj.options_reg_dict[uid]
        self.clone_flag = True


    def on_radiobutton_toggled(self, button, combo, combo2):

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

        User has selected to create a new options manager.

        Args:

            button (Gtk.RadioButton): The widget clicked

            combo, combo2 (Gtk.ComboBox): Other widgets to update

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 33202 on_radiobutton_toggled')

        if button.get_active():

            self.options_obj = None
            self.clone_flag = False

            combo.set_sensitive(False)
            combo2.set_sensitive(False)


    def on_radiobutton2_toggled(self, button, combo, combo2):

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

        User wants to select an existing options manager.

        Args:

            button (Gtk.RadioButton): The widget clicked

            combo, combo2 (Gtk.ComboBox): Other widgets to update

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 33228 on_radiobutton2_toggled')

        if button.get_active():

            tree_iter = combo.get_active_iter()
            model = combo.get_model()
            uid = model[tree_iter][1]

            self.options_obj = self.main_win_obj.app_obj.options_reg_dict[uid]
            self.clone_flag = False

            combo.set_sensitive(True)
            combo2.set_sensitive(False)


    def on_radiobutton3_toggled(self, button, combo, combo2):

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

        User wants to clone an existing options manager.

        Args:

            button (Gtk.RadioButton): The widget clicked

            combo, combo2 (Gtk.ComboBox): Other widgets to update

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 33258 on_radiobutton3_toggled')

        if button.get_active():

            tree_iter = combo2.get_active_iter()
            model = combo2.get_model()
            uid = model[tree_iter][1]

            self.options_obj = self.main_win_obj.app_obj.options_reg_dict[uid]
            self.clone_flag = True

            combo.set_sensitive(False)
            combo2.set_sensitive(True)


class CalendarDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.on_button_find_date() and
    config.OptionsEditWin.on_button_set_date_clicked().

    Python class handling a dialogue window that prompts the user to choose a
    date on a calendar

    Args:

        parent_win_obj (mainwin.MainWin or config.OptionsEditWin): The parent
            window

        date (str): A date in the form YYYYMMDD. If set, that date is
            selected in the calendar. If an empty string or None, no date is
            selected

    """


    # Standard class methods


    def __init__(self, parent_win_obj, date=None):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 33299 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Select a date\' dialogue starts here. In' \
            + ' the toolbar at the bottom of the Videos tab, click the' \
            + ' \'Find date\' button'
        )

        # IV list - class objects
        # -----------------------
        self.parent_win_obj = parent_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.calendar = None                    # Gtk.Calendar


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Select a date'),
            parent_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(True)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(parent_win_obj.spacing_size)
        grid.set_row_spacing(parent_win_obj.spacing_size)

        # (Store various widgets as IVs, so the calling function can retrieve
        #   their contents)
        self.calendar = Gtk.Calendar.new()
        grid.attach(self.calendar, 0, 0, 1, 1)

        # If the date was specified, it should be a string in the form YYYYMMDD
        if date:
            year = int(date[0:3])
            month = int(date[4:5])
            day = int(date[6:7])

            if day >= 1 and day <= 31 and month >= 1 and month <= 12 \
            and year >=1:
                self.calendar.select_month(month, year)
                self.calendar.select_day(day)

        # Display the dialogue window
        self.show_all()


class ChangeThemeDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.on_menu_change_theme().

    Python class handling a dialogue window that allows a user on MS Windows to
    change the Gtk theme.

    Args:

        parent_win_obj (mainwin.MainWin): The parent window

    """


    # Standard class methods


    def __init__(self, parent_win_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 33381 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Change window theme\' dialogue starts' \
            + ' here. In the main window menu, click System > Change theme.' \
            + ' Available on MS Windows only'
        )

        # IV list - class objects
        # -----------------------
        self.parent_win_obj = parent_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.grid = None                        # Gtk.Grid
        self.combo = None                       # Gtk.ComboBox


        # IV list - other
        # ---------------
        # Theme name; the name of a sub-folder in ../pack/mswin_themes, e.g.
        #   'basic'
        self.theme_name = 'default'
        # Full path to that folder. If the theme named 'default' is selected,
        #   the path remains set to None (as we don't use a sub-folder with
        #   that name)
        self.theme_path = None


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Change window theme'),
            parent_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)

        # Set up the dialogue window
        box = self.get_content_area()

        self.grid = Gtk.Grid()
        box.add(self.grid)
        self.grid.set_border_width(parent_win_obj.spacing_size)
        self.grid.set_row_spacing(parent_win_obj.spacing_size)

        label = Gtk.Label()
        self.grid.attach(label, 0, 0, 1, 1)
        label.set_markup(
            utils.tidy_up_long_descrip(
                _('Choose a new window theme for Tartube'),
                parent_win_obj.quite_long_string_max_len,
            ),
        )

        # Add a combo, containing all available themes. The 'default' theme
        #   always appears at the top
        store = Gtk.ListStore(str, str)
        store.append([ 'default', '' ])

        theme_dict = self.get_theme_dict()
        for theme_name in sorted(theme_dict):
            store.append([ theme_name, theme_dict[theme_name] ])

        combo = Gtk.ComboBox.new_with_model(store)
        self.grid.attach(combo, 0, 1, 1, 1)
        combo.set_hexpand(True)
        # (Signal connect appears below)

        cell = Gtk.CellRendererText()
        combo.pack_start(cell, False)
        combo.add_attribute(cell, 'text', 0)
        combo.set_active(0)
        combo.connect('changed', self.on_combo_changed)

        label2 = Gtk.Label()
        self.grid.attach(label2, 0, 2, 1, 1)
        label2.set_markup(
            '<i>' + utils.tidy_up_long_descrip(
                _(
                'Hint: choose \'default\' for the normal MS Windows theme',
                ),
                parent_win_obj.quite_long_string_max_len,
            ) + '</i>',
        )

        # Display the dialogue window
        self.show_all()


    # Support functions


    def get_theme_dict(self):

        """Called by self.__init__().

        In the MSYS2 environment, GTK themes can be set using a settings.ini
        file.

        In ../pack/mswin_themes can be found several sub-folders, each one
        named after a theme, and containing a settings.ini that will set that
        theme.

        Compile a dictionary of available themes for the dialogue window to
        use.

        Return values:

            Dictionary whose keys are a theme name like 'dark', and whose
                corresponding values are the full path to the sub-folder with
                that name

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 33505 get_theme_dict')

        app_obj = self.parent_win_obj.app_obj

        themes_path = os.path.abspath(
            os.path.join(
                app_obj.script_parent_dir,
                'pack',
                'mswin_themes',
            ),
        )

        try:
            file_list = os.listdir(path = themes_path)
        except:
            file_list = []

        theme_dict = {}
        for filename in file_list:

            # Ignore any folder called 'default'
            if filename != 'default':

                dir_path = os.path.abspath(
                    os.path.join(
                        app_obj.script_parent_dir,
                        'pack',
                        'mswin_themes',
                        filename,
                    ),
                )
                settings_path = os.path.abspath(
                    os.path.join(dir_path, 'settings.ini'),
                )

                if os.path.isdir(dir_path) \
                and not re.search(r'^\.', dir_path) \
                and os.path.isfile(settings_path):
                    theme_dict[filename] = dir_path

        return theme_dict


    # Callback class methods


    def on_combo_changed(self, combo):

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

        Store the combobox's selected item, so the calling function can
        retrieve it.

        Args:

            combo (Gtk.ComboBox): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 33565 on_combo_changed')

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        theme_name = model[tree_iter][0]
        theme_path = model[tree_iter][1]

        self.theme_name = theme_name
        if theme_name == 'default':
            self.theme_path = None
        else:
            self.theme_path = theme_path


class CreateProfileDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.on_menu_create_profile().

    Python class handling a dialogue window that prompts the user to create a
    profile, remembering which media.Channel, media.Playlist and media.Folder
    objects are currently marked.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

    """


    # Standard class methods


    def __init__(self, main_win_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 33600 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Create profile\' dialogue starts here.' \
            + ' In the main window menu, click Media > Profiles > Create' \
            + ' profile...'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - other
        # ---------------
        # The user's choice of name. Set to None when the entry box is empty
        self.profile_name = None


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Create profile'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(True)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)

        label = Gtk.Label()
        grid.attach(label, 0, 1, 1, 1)
        label.set_markup(_('Items currently marked:'))
        label.set_alignment(0, 0.5)

        scrolled = Gtk.ScrolledWindow()
        grid.attach(scrolled, 0, 2, 1, 1)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_hexpand(True)
        scrolled.set_vexpand(True)
        scrolled.set_size_request(-1, 150)

        frame = Gtk.Frame()
        scrolled.add_with_viewport(frame)

        treeview = Gtk.TreeView()
        frame.add(treeview)
        treeview.set_can_focus(False)

        renderer_pixbuf = Gtk.CellRendererPixbuf()
        column_pixbuf = Gtk.TreeViewColumn(
            _('Type'),
            renderer_pixbuf,
            pixbuf=0,
        )
        treeview.append_column(column_pixbuf)

        renderer_text = Gtk.CellRendererText()
        column_text = Gtk.TreeViewColumn(
            _('Name'),
            renderer_text,
            text=1,
        )
        treeview.append_column(column_text)

        liststore = Gtk.ListStore(GdkPixbuf.Pixbuf, str)
        treeview.set_model(liststore)

        for dbid in sorted(main_win_obj.video_index_marker_dict):
            media_data_obj = main_win_obj.app_obj.media_reg_dict[dbid]

            if isinstance(media_data_obj, media.Channel):
                pixbuf = main_win_obj.pixbuf_dict['channel_small']
            elif isinstance(media_data_obj, media.Playlist):
                pixbuf = main_win_obj.pixbuf_dict['playlist_small']
            else:
                pixbuf = main_win_obj.pixbuf_dict['folder_small']

            liststore.append( [pixbuf, media_data_obj.name] )

        label2 = Gtk.Label()
        grid.attach(label2, 0, 3, 1, 1)
        label2.set_markup(_('Remember these items with a profile named:'))
        label2.set_alignment(0, 0.5)

        entry = Gtk.Entry()
        grid.attach(entry, 0, 4, 1, 1)
        entry.set_hexpand(True)
        entry.grab_focus()
        entry.connect('changed', self.on_entry_changed)

        # Display the dialogue window
        self.show_all()


    # Callback class methods


    def on_entry_changed(self, entry):

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

        Updates IVs.

        Args:

            entry (Gtk.Entry): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 33725 on_entry_changed')

        name = entry.get_text()
        if name == '':
            self.profile_name = None
        else:
            self.profile_name = name


class DeleteContainerDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.delete_container().

    Python class handling a dialogue window that prompts the user for
    confirmation, before removing a media.Channel, media.Playlist or
    media.Folder object.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        media_data_obj (media.Channel, media.Playlist or media.Folder): The
            container media data object to be deleted

        empty_flag (bool): If True, the container media data object is to be
            emptied, rather than being deleted

    """


    # Standard class methods


    def __init__(self, main_win_obj, media_data_obj, empty_flag):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 33761 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Delete channel\' dialogue starts here.' \
            + ' In the Videos tab, right-click a channel and select Delete' \
            + ' channel'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.button = None                      # Gtk.RadioButton
        self.button2 = None                     # Gtk.RadioButton
        self.button3 = None                     # Gtk.CheckButton


        # IV list - other
        # ---------------
        # Number of videos found in the container
        self.video_count = 0


        # Code
        # ----

        # Prepare variables
        spacing_size = self.main_win_obj.spacing_size
        label_length = self.main_win_obj.long_string_max_len

        media_type = media_data_obj.get_type()
        if media_type == 'video':

            return main_win_obj.app_obj.system_error(
                268,
                'Dialogue window setup failed sanity check',
            )

        # Count the container object's children
        total_count, self.video_count, channel_count, playlist_count, \
        folder_count = media_data_obj.count_descendants( [0, 0, 0, 0, 0] )

        # Create the dialogue window
        if not empty_flag:
            if media_type == 'channel':
                title = _('Delete channel')
            elif media_type == 'playlist':
                title = _('Delete playlist')
            else:
                title = _('Delete folder')
        else:
            if media_type == 'channel':
                title = _('Empty channel')
            elif media_type == 'playlist':
                title = _('Empty playlist')
            else:
                title = _('Empty folder')

        Gtk.Dialog.__init__(
            self,
            title,
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)
        self.set_resizable(False)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(spacing_size)
        grid.set_row_spacing(spacing_size)

        label = Gtk.Label()
        grid.attach(label, 0, 0, 1, 1)
        label.set_markup('<b>' + media_data_obj.name + '</b>')

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 1, 1, 1)

        if not total_count:

            if media_type == 'channel':
                string = _('This channel does not contain any videos')
            elif media_type == 'playlist':
                string = _('This playlist does not contain any videos')
            else:
                string = _('This folder doesn\'t contain anything')


            label2 = Gtk.Label(
                utils.tidy_up_long_string(
                    string + ' ' + _(
                        '(but there might be some files in Tartube\'s data'
                        + ' folder)',
                    ),
                    label_length,
                ),
            )

            grid.attach(label2, 0, 2, 1, 5)
            label2.set_alignment(0, 0.5)

        else:

            if media_type == 'channel':
                string = _('This channel contains:')
            elif media_type == 'playlist':
                string = _('This playlist contains:')
            else:
                string = _('This folder contains:')

            label2 = Gtk.Label(string)
            grid.attach(label2, 0, 2, 1, 1)
            label2.set_alignment(0, 0.5)

            if folder_count == 1:
                label_string = _('1 folder')
            else:
                label_string = _('{0} folders').format(str(folder_count))

            label3 = Gtk.Label()
            grid.attach(label3, 0, 3, 1, 1)
            label3.set_markup(label_string)

            if channel_count == 1:
                label_string = _('1 channel')
            else:
                label_string = _('{0} channels').format(str(channel_count))

            label4 = Gtk.Label()
            grid.attach(label4, 0, 4, 1, 1)
            label4.set_markup(label_string)

            if playlist_count == 1:
                label_string = _('1 playlist')
            else:
                label_string = _('{0} playlists').format(str(playlist_count))

            label5 = Gtk.Label()
            grid.attach(label5, 0, 5, 1, 1)
            label5.set_markup(label_string)

            if self.video_count == 1:
                label_string = _('1 video')
            else:
                label_string = _('{0} videos').format(str(self.video_count))

            label6 = Gtk.Label()
            grid.attach(label6, 0, 6, 1, 1)
            label6.set_markup(label_string)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 7, 1, 1)

        if not empty_flag:

            if media_type == 'channel':
                string = _(
                    'Do you want to remove the channel from your' \
                    + '  filesystem, or do you just want to remove the' \
                    + ' channel from this list?',
                )
                string2 = _('Just remove the channel from this list')

            elif media_type == 'playlist':
                string = _(
                    'Do you want to remove the playlist from your' \
                    + '  filesystem, or do you just want to remove the' \
                    + ' playlist from this list?',
                )
                string2 = _('Just remove the playlist from this list')

            else:
                string = _(
                    'Do you want to remove the folder from your' \
                    + '  filesystem, or do you just want to remove the' \
                    + ' folder from this list?',
                )
                string2 = _('Just remove the folder from this list')

        else:

            if media_type == 'channel':
                string = _(
                    'Do you want to empty the channel on your filesystem,' \
                    + ' or do you just want to empty the channel in this' \
                    + ' list?',
                )
                string2 = _('Just empty the channel in this list')

            elif media_type == 'playlist':
                string = _(
                    'Do you want to empty the playlist on your filesystem,' \
                    + ' or do you just want to empty the playlist in this' \
                    + ' list?',
                )
                string2 = _('Just empty the playlist in this list')

            else:
                string = _(
                    'Do you want to empty the folder on your filesystem,' \
                    + ' or do you just want to empty the folder in this' \
                    + ' list?',
                )
                string2 = _('Just empty the folder in this list')

        label7 = Gtk.Label(
            utils.tidy_up_long_string(
                string,
                label_length,
            ),
        )
        grid.attach(label7, 0, 8, 1, 1)
        label7.set_alignment(0, 0.5)

        self.button = Gtk.RadioButton.new_with_label_from_widget(None, string2)
        grid.attach(self.button, 0, 9, 1, 1)

        self.button2 = Gtk.RadioButton.new_from_widget(self.button)
        grid.attach(self.button2, 0, 10, 1, 1)
        self.button2.set_label(_('Delete files on your computer'))
        if main_win_obj.app_obj.delete_container_files_flag:
            self.button2.set_active(True)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 11, 1, 1)

        self.button3 = Gtk.CheckButton.new_with_label(
            _('Always show this window'),
        )
        grid.attach(self.button3, 0, 12, 1, 1)
        if main_win_obj.app_obj.show_delete_container_dialogue_flag:
            self.button3.set_active(True)

        # Display the dialogue window
        self.show_all()


class DeleteDropZoneDialogue(Gtk.Dialog):

    """Called by mainwin.DropZoneBox.on_delete_button_clicked().

    Prompt the user to choose whether to delete just the dropzone, or its
    associated options.OptionsManager object too.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        options_obj (options.OptionsManager): The download options whose
            dropzone is to be deleted

    """


    # Standard class methods


    def __init__(self, main_win_obj, options_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 34033 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Delete dropzone\' dialogue starts here.' \
            + ' In the Drag and Drop tab, click a delete button in the' \
            + ' bottom-right corner of any dropzone'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj
        self.options_obj = options_obj


        # IV list - Gtk widgets
        # ---------------------
        #   (none)


        # IV list - other
        # ---------------
        self.del_dropzone_flag = False
        self.del_both_flag = False


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Delete dropzone'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
            )
        )

        self.set_modal(True)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)

        button = Gtk.Button.new_with_label(_('Just delete the dropzone'))
        grid.attach(button, 0, 0, 1, 1)
        button.set_hexpand(True)
        button.connect('clicked', self.on_dropzone_button_clicked)

        button2 = Gtk.Button.new_with_label(_('Also delete download options'))
        grid.attach(button2, 0, 1, 1, 1)
        button2.set_hexpand(True)
        button2.connect('clicked', self.on_both_button_clicked)
        # Don't delete download options in the middle of a download operation,
        #   or if the options have been applied to a media data object
        # Don't delete general download options
        if self.main_win_obj.app_obj.current_manager_obj \
        or options_obj.dbid_list \
        or options_obj == self.main_win_obj.app_obj.general_options_obj:
            button2.set_sensitive(False)

        # Display the dialogue window
        self.show_all()


    # Callback class methods


    def on_dropzone_button_clicked(self, button):

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

        Deletes the dropzone, but not the options.OptionsManager object.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 34119 on_dropzone_button_clicked')

        self.del_dropzone_flag = True
        self.destroy()


    def on_both_button_clicked(self, button):

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

        Deletes the dropzone and the options.OptionsManager object.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 34138 on_both_button_clicked')

        self.del_both_flag = True
        self.destroy()


class DeleteVideoDialogue(Gtk.Dialog):

    """Called by mainwin.MainWin.on_video_catalogue_delete_video() and
    .on_video_catalogue_delete_video_multi().

    Python class handling a dialogue window that prompts the user for
    confirmation, before removing one or more media.Video objects.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        media_list (list): List of media.Video objects to be deleted

    """


    # Standard class methods


    def __init__(self, main_win_obj, media_list):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 34167 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Delete videos\' dialogue starts here.' \
            + ' In the Videos tab, right-click a video and select Delete video'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.button = None                      # Gtk.RadioButton
        self.button2 = None                     # Gtk.RadioButton
        self.button3 = None                     # Gtk.CheckButton


        # IV list - other
        # ---------------
        # Number of videos found in the container
        self.video_count = 0


        # Code
        # ----

        # Prepare variables
        spacing_size = self.main_win_obj.spacing_size
        label_length = self.main_win_obj.long_string_max_len
        current_dbid = main_win_obj.video_index_current_dbid

        parent_obj = None
        try:
            parent_obj = main_win_obj.app_obj.media_reg_dict[current_dbid]
        except:
            pass

        if parent_obj is None or not media_list:

            return main_win_obj.app_obj.system_error(
                269,
                'Dialogue window setup failed sanity check',
            )

        for media_data_obj in media_list:
            if media_data_obj.get_type() != 'video':

                return main_win_obj.app_obj.system_error(
                    270,
                    'Dialogue window setup failed sanity check',
                )

        # Create the dialogue window
        Gtk.Dialog.__init__(
            self,
            _('Delete videos'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)
        self.set_resizable(False)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(spacing_size)
        grid.set_row_spacing(spacing_size)

        label = Gtk.Label()
        grid.attach(label, 0, 0, 1, 1)
        label.set_markup('<b>' + parent_obj.name + '</b>')

        if len(media_list) == 1:
            label_string = _('1 selected video')
        else:
            label_string \
            = _('{0} selected videos').format(str(self.video_count))

        label2 = Gtk.Label()
        grid.attach(label2, 0, 1, 1, 1)
        label2.set_markup(label_string)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 2, 1, 1)

        parent_type = parent_obj.get_type()
        if parent_type == 'channel':
            string = _(
                'Do you want to remove the video(s) from your filesystem,' \
                + ' or do you just want to remove them from this channel?',
            )
            string2 = _('Just remove the video(s) from this channel')

        elif parent_type == 'playlist':
            string = _(
                'Do you want to remove the video(s) from your filesystem,' \
                + ' or do you just want to remove them from this playlist?',
            )
            string2 = _('Just remove the video(s) from this playlist')

        else:
            string = _(
                'Do you want to remove the video(s) from your filesystem,' \
                + ' or do you just want to remove them from this folder?',
            )
            string2 = _('Just remove the video(s) from this folder')

        label3 = Gtk.Label(
            utils.tidy_up_long_string(
                string,
                label_length,
            ),
        )
        grid.attach(label3, 0, 3, 1, 1)
        label3.set_alignment(0, 0.5)

        self.button = Gtk.RadioButton.new_with_label_from_widget(None, string2)
        grid.attach(self.button, 0, 4, 1, 1)

        self.button2 = Gtk.RadioButton.new_from_widget(self.button)
        grid.attach(self.button2, 0, 5, 1, 1)
        self.button2.set_label(_('Delete files on your computer'))
        if main_win_obj.app_obj.delete_video_files_flag:
            self.button2.set_active(True)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 6, 1, 1)

        self.button3 = Gtk.CheckButton.new_with_label(
            _('Always show this window'),
        )
        grid.attach(self.button3, 0, 7, 1, 1)
        if main_win_obj.app_obj.show_delete_video_dialogue_flag:
            self.button3.set_active(True)

        # Display the dialogue window
        self.show_all()


class DuplicateVideoDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.on_menu_add_video().

    Python class handling a dialogue window that shows the user any video URls
    which are duplicates.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        duplicate_list (list): List of URLs

    """


    # Standard class methods


    def __init__(self, main_win_obj, duplicate_list):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 34338 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Duplicate URLs\' dialogue starts here.' \
            + ' In the main window toolbar, click the Video button. In the' \
            + ' top half of the first dialogue window, add duplicate URLs' \
            + ' and then click the OK button'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        #   (none)


        # IV list - other
        # ---------------
        #   (none)


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Duplicate URLs'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)
        # (Set enough vertical/horizontal room for several URLs)
        self.set_size_request(
            main_win_obj.app_obj.config_win_width - 150,
            300,
        )

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)

        label = Gtk.Label(_('The following URLs are duplicates:'))
        grid.attach(label, 0, 0, 1, 1)

        scrolled = Gtk.ScrolledWindow()
        grid.attach(scrolled, 0, 1, 1, 1)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_hexpand(True)
        scrolled.set_vexpand(True)

        frame = Gtk.Frame()
        scrolled.add_with_viewport(frame)

        textview = Gtk.TextView()
        frame.add(textview)
        textview.set_wrap_mode(Gtk.WrapMode.WORD)
        textview.set_editable(False)
        textview.set_cursor_visible(False)

        textbuffer = textview.get_buffer()
        textbuffer.set_text('\n'.join(duplicate_list) + '\n')

        # Display the dialogue window
        self.show_all()


class ExportDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.export_from_db().

    Python class handling a dialogue window that prompts the user before
    creating a database export.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        whole_flag (bool): True if the whole database is to be exported, False
            if only part of the database is to be exported

    """


    # Standard class methods


    def __init__(self, main_win_obj, whole_flag):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 34439 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Export from database\' dialogue starts' \
            + ' here. In the main window menu, click Media > Export/Import' \
            + ' > Export from database...'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.checkbutton = None                 # Gtk.CheckButton
        self.checkbutton2 = None                # Gtk.CheckButton
        self.checkbutton3 = None                # Gtk.CheckButton
        self.checkbutton4 = None                # Gtk.CheckButton
        self.radiobutton = None                 # Gtk.RadioButton
        self.radiobutton2 = None                # Gtk.RadioButton
        self.radiobutton3 = None                # Gtk.RadioButton


        # IV list - other
        # ---------------
        # The selected CSV separator
        self.separator = None


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Export from database'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)

        # Set up the dialogue window
        spacing_size = self.main_win_obj.spacing_size
        label_length = self.main_win_obj.long_string_max_len

        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(spacing_size)
        grid.set_row_spacing(spacing_size)

        grid_width = 2

        if not whole_flag:
            msg = _(
                'Tartube is ready to export a partial summary of its' \
                + ' database, containing a list of videos, channels,' \
                + ' playlists and/or folders (but not including the videos' \
                + ' themselves)',
            )
        else:
            msg = _(
                'Tartube is ready to export a summary of its database,' \
                + ' containing a list of videos, channels, playlists and/or' \
                + ' folders (but not including the videos themselves)',
            )

        label = Gtk.Label(
            utils.tidy_up_long_string(
                msg,
                label_length,
            ),
        )
        grid.attach(label, 0, 0, grid_width, 1)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 1, grid_width, 1)

        label = Gtk.Label(_('Choose what should be included:'))
        grid.attach(label, 0, 2, grid_width, 1)
        label.set_alignment(0, 0.5)

        # (Store various widgets as IVs, so the calling function can retrieve
        #   their contents)
        self.checkbutton = Gtk.CheckButton()
        grid.attach(self.checkbutton, 0, 3, grid_width, 1)
        self.checkbutton.set_label(_('Include lists of videos'))
        self.checkbutton.set_active(False)

        self.checkbutton2 = Gtk.CheckButton()
        grid.attach(self.checkbutton2, 0, 4, grid_width, 1)
        self.checkbutton2.set_label(_('Include channels'))
        self.checkbutton2.set_active(True)

        self.checkbutton3 = Gtk.CheckButton()
        grid.attach(self.checkbutton3, 0, 5, grid_width, 1)
        self.checkbutton3.set_label(_('Include playlists'))
        self.checkbutton3.set_active(True)

        self.checkbutton4 = Gtk.CheckButton()
        grid.attach(self.checkbutton4, 0, 6, grid_width, 1)
        self.checkbutton4.set_label(_('Preserve folder structure'))
        self.checkbutton4.set_active(True)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 7, grid_width, 1)

        self.radiobutton = Gtk.RadioButton.new_with_label_from_widget(
            None,
            _('Export as JSON'),
        )
        grid.attach(self.radiobutton, 0, 8, grid_width, 1)

        self.radiobutton2 = Gtk.RadioButton.new_with_label_from_widget(
            self.radiobutton,
            _('Export as CSV using separator'),
        )
        grid.attach(self.radiobutton2, 0, 9, 1, 1)
        # (Signal connect appears below)

        # (At the moment, Tartube only offers two choices of CSV separator)
        liststore = Gtk.ListStore(str)
        liststore.append(['|'])
        liststore.append([','])

        combo = Gtk.ComboBox.new_with_model(liststore)
        grid.attach(combo, 1, 9, 1, 1)
        combo.set_hexpand(True)

        cell = Gtk.CellRendererText()
        combo.pack_start(cell, False)
        combo.add_attribute(cell, 'text', 0)
        combo.connect('changed', self.on_combo_changed)
        combo.set_sensitive(False)

        if main_win_obj.app_obj.export_csv_separator == ',':
            combo.set_active(1)
        else:
            combo.set_active(0)

        self.radiobutton3 = Gtk.RadioButton.new_with_label_from_widget(
            self.radiobutton,
            _('Export as plain text'),
        )
        grid.attach(self.radiobutton3, 0, 10, grid_width, 1)

        # (Signal connects from above)
        self.radiobutton2.connect(
            'toggled',
            self.on_radiobutton_toggled,
            combo,
        )

        # Display the dialogue window
        self.show_all()


    # Callback class methods


    def on_combo_changed(self, combo):

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

        Updates the combobox's selected item, so the calling function can
        retrieve it.

        Args:

            combo (Gtk.ComboBox): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 34620 on_combo_changed')

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


    def on_radiobutton_toggled(self, button, combo):

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

        (De)sensitises the combobox, as required.

        Args:

            button (Gtk.RadioButton): The widget clicked

            combo (Gtk.ComboBox): Another widget to update

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 34642 on_radiobutton_toggled')

        if button.get_active():
            combo.set_sensitive(True)
        else:
            combo.set_sensitive(False)


class ExtractorCodeDialogue(Gtk.Dialog):

    """Called by config.OptionsEditWin.on_formats_tab_type_clicked().

    Python class handling a dialogue window that prompts the user to type a
    format extractor code.

    Args:

        parent_win_obj (config.SystemPrefWin): The parent main window

    """


    # Standard class methods


    def __init__(self, parent_win_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 34670 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Type extractor code\' dialogue starts' \
            + ' here. In the main window menu, click Edit > General download' \
            + ' Options... > Formats > Preferred, then click the small' \
            + ' button next to the \'Add format\' button'
        )

        # IV list - class objects
        # -----------------------
        self.parent_win_obj = parent_win_obj


        # IV list - Gtk widgets
        # ---------------------
        #   (none)


        # IV list - other
        # ---------------
        # The extractor code types, or None if nothing typed
        self.extract_code = None


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Type extractor code'),
            parent_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(parent_win_obj.spacing_size)
        grid.set_row_spacing(parent_win_obj.spacing_size)

        # (Get the minimum and maximum numeric values specified in formats.py.
        #   Actually, the user can type a non-numeric value appearing as a key
        #   in formats.VIDEO_OPTION_TYPE_DICT, e.g. '144p' or 'mp4', but we
        #   don't advertise the fact
        min_code = 1
        max_code = 1
        for value in formats.VIDEO_OPTION_TYPE_DICT.keys():

            if value.isdigit():
                if int(value) < min_code:
                    min_code = int(value)
                elif int(value) > max_code:
                    max_code = int(value)

        label = Gtk.Label()
        grid.attach(label, 0, 0, 1, 1)
        label.set_markup(
            _(
            'Type an extractor code in the range {0}-{1}'
            ).format(str(min_code), str(max_code)) \
            + '\n' + _(
            '(mp3, mp4 etc are also acceptable)',
            )
        )

        label2 = Gtk.Label()
        grid.attach(label, 0, 1, 1, 1)
        label2.set_markup(_('e.g. 136 for mp4 720p'))

        entry = Gtk.Entry()
        grid.attach(entry, 0, 2, 1, 1)
        entry.connect('changed', self.on_entry_changed)
        entry.connect('activate', self.on_entry_activated)

        # Display the dialogue window
        self.show_all()


    # Callback class methods


    def on_entry_activated(self, entry):

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

        Sets the extractor code and closes the dialogue window.

        Args:

            entry (Gtk.Entry): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 34773 on_entry_activated')

        value = entry.get_text()
        # (Unspecified extractor codes are stored as None, not empty strings)
        if value == '':
            self.extract_code = None
        else:
            self.extract_code = value

        self.response(Gtk.ResponseType.OK)


    def on_entry_changed(self, entry):

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

        Sets the extractor code.

        Args:

            entry (Gtk.Entry): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 34798 on_entry_changed')

        value = entry.get_text()
        # (Unspecified extractor codes are stored as None, not empty strings)
        if value == '':
            self.extract_code = None
        else:
            self.extract_code = value


class FormatsSubsDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.info_manager_finished().

    Python class handling a dialogue window that prompts the user to set or
    apply download options, having fetched available formats/subtitles for a
    (single) video.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        video_obj (media.Video): The selected video

        info_type (str): The information fetched during the info operation:
            'formats' or 'subs'

    """


    # Standard class methods


    def __init__(self, main_win_obj, video_obj, info_type):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 34834 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Confirmation dialogue for fetching' \
            + ' formats/subtitles. In the Videos tab, right-click a video' \
            + ' and select Fetch > Available formats... or Fetch > Available' \
            + ' subtitles'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        #   (none)


        # IV list - other
        # ---------------
        # The selected video
        self.video_obj = video_obj
        # The information fetched during the info operation: 'formats' or
        #   'subs'
        self.info_type = info_type


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Operation complete'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)

        if info_type == 'formats':
            msg = _('Click the Output tab to see available formats')
        else:
            msg = _('Click the Output tab to see available subtitles')

        label = Gtk.Label(msg)
        grid.attach(label, 0, 0, 1, 1)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 1, 1, 1)

        button = Gtk.Button.new_with_label(
            _('Update general download options'),
        )
        grid.attach(button, 0, 2, 1, 1)
        button.connect('clicked', self.on_general_button_clicked)

        if not video_obj.options_obj:

            button2 = Gtk.Button.new_with_label(
                _('Apply download options to this video only'),
            )
            grid.attach(button2, 0, 3, 1, 1)
            button2.connect('clicked', self.on_apply_button_clicked)

        else:

            button2 = Gtk.Button.new_with_label(
                _('Update this video\'s download options'),
            )
            grid.attach(button2, 0, 3, 1, 1)
            button2.connect('clicked', self.on_update_button_clicked)

        # Display the dialogue window
        self.show_all()


    # Callback class methods


    def on_apply_button_clicked(self, button):

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

        When the button is clicked, applies download options to the selected
        video only.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 34940 on_apply_button_clicked')

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

        # Check the video still exists
        if not self.video_obj.dbid in app_obj.media_reg_dict \
        or app_obj.media_reg_dict[self.video_obj.dbid] != self.video_obj:
            self.destroy()

        else:

            # Clone general download options...
            options_obj = app_obj.clone_download_options(
                app_obj.general_options_obj,
            )

            # ...and apply the cloned options to the video
            app_obj.apply_download_options(self.video_obj, options_obj)

            # Open an edit window to show the options immediately
            win_obj = config.OptionsEditWin(
                app_obj,
                options_obj,
                self.info_type,
            )

            self.destroy()
            win_obj.present()


    def on_general_button_clicked(self, button):

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

        When the button is clicked, open the edit window for the current
        options.OptionsManager object.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 34985 on_general_button_clicked')

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

        # Check the video still exists
        if not self.video_obj.dbid in app_obj.media_reg_dict \
        or app_obj.media_reg_dict[self.video_obj.dbid] != self.video_obj:
            self.destroy()

        else:

            win_obj = config.OptionsEditWin(
                app_obj,
                app_obj.general_options_obj,
                self.info_type,
            )

            self.destroy()
            win_obj.present()


    def on_update_button_clicked(self, button):

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

        When the button is clicked, open the edit window for the video's
        existing options.OptionsManager object.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 35021 on_update_button_clicked')

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

        # Check the video still exists
        if not self.video_obj.dbid in app_obj.media_reg_dict \
        or app_obj.media_reg_dict[self.video_obj.dbid] != self.video_obj:
            self.destroy()

        else:

            win_obj = config.OptionsEditWin(
                app_obj,
                self.video_obj.options_obj,
                self.info_type,
            )

            self.destroy()
            win_obj.present()


class ImportDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.import_into_db().

    Python class handling a dialogue window that prompts the user before
    hanlding an export file, created by mainapp.TartubeApp.export_from_db().

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        db_dict (dict): The imported data, a dictionary described in the
            comments in mainapp.TartubeApp.export_from_db()

    """


    # Standard class methods


    def __init__(self, main_win_obj, db_dict):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 35066 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Import into database\' dialogue starts' \
            + ' here. In the main window menu, click Media > Export/Import' \
            + ' > Import into database...'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.treeview = None                    # Gtk.TreeView
        self.liststore = None                   # Gtk.TreeView
        self.checkbutton = None                 # Gtk.TreeView
        self.checkbutton2 = None                # Gtk.TreeView


        # IV list - other
        # ---------------
        # A flattened dictionary of media data objects
        self.flat_db_dict = {}


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Import into database'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)
        self.set_default_size(
            main_win_obj.app_obj.config_win_width,
            main_win_obj.app_obj.config_win_height,
        )

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)
        grid_width = 4

        label = Gtk.Label(_('Choose which items to import'))
        grid.attach(label, 0, 0, grid_width, 1)

        scrolled = Gtk.ScrolledWindow()
        grid.attach(scrolled, 0, 1, grid_width, 1)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_hexpand(True)
        scrolled.set_vexpand(True)

        frame = Gtk.Frame()
        scrolled.add_with_viewport(frame)

        # (Store various widgets as IVs, so the calling function can retrieve
        #   their contents)
        self.treeview = Gtk.TreeView()
        frame.add(self.treeview)
        self.treeview.set_can_focus(False)

        renderer_toggle = Gtk.CellRendererToggle()
        renderer_toggle.connect('toggled', self.on_checkbutton_toggled)
        column_toggle = Gtk.TreeViewColumn(
            _('Import'),
            renderer_toggle,
            active=0,
        )
        self.treeview.append_column(column_toggle)

        renderer_pixbuf = Gtk.CellRendererPixbuf()
        column_pixbuf = Gtk.TreeViewColumn(
            _('Type'),
            renderer_pixbuf,
            pixbuf=1,
        )
        self.treeview.append_column(column_pixbuf)

        renderer_text = Gtk.CellRendererText()
        column_text = Gtk.TreeViewColumn(
            _('Name'),
            renderer_text,
            text=2,
        )
        self.treeview.append_column(column_text)

        renderer_text2 = Gtk.CellRendererText()
        column_text2 = Gtk.TreeViewColumn(
            'hide',
            renderer_text2,
            text=3,
        )
        column_text2.set_visible(False)
        self.treeview.append_column(column_text2)

        self.liststore = Gtk.ListStore(bool, GdkPixbuf.Pixbuf, str, int)
        self.treeview.set_model(self.liststore)

        self.checkbutton = Gtk.CheckButton()
        grid.attach(self.checkbutton, 0, 2, 1, 1)
        self.checkbutton.set_label(_('Import videos'))
        self.checkbutton.set_active(True)

        self.checkbutton2 = Gtk.CheckButton()
        grid.attach(self.checkbutton2, 1, 2, 1, 1)
        self.checkbutton2.set_label(_('Merge channels/playlists/folders'))
        self.checkbutton2.set_active(False)

        button = Gtk.Button.new_with_label(_('Select all'))
        grid.attach(button, 2, 2, 1, 1)
        button.set_hexpand(False)
        button.connect('clicked', self.on_select_all_clicked)

        button2 = Gtk.Button.new_with_label(_('Unselect all'))
        grid.attach(button2, 3, 2, 1, 1)
        button2.set_hexpand(False)
        button2.connect('clicked', self.on_deselect_all_clicked)

        # The data is imported as a dictionary, perhaps preserving the original
        #   folder structure of the database, or perhaps not
        # The 'db_dict' format is described in the comments in
        #   mainapp.TartubeApp.export_from_db()
        # 'db_dict' contains mini-dictionaries, 'mini_dict', whose format is
        #   also described in that function. Each 'mini_dict' represents a
        #   single media data object
        #
        # Convert 'db_dict' to a list. Each item in the list is a 'mini_dict'.
        #   Each 'mini_dict' has some new key-value pairs (except those
        #   representing videos):
        #
        #   - 'video_count': int (showing the number of videos the channel,
        #       playlist or folder contains)
        #   - 'display_name': str (the channel/playlist/folder name indented
        #       with extra whitespace (so the user can clearly see the folder
        #       structure)
        #   - 'import_flag': bool (True if this channel/playlist/folder should
        #       be imported, False if not)
        converted_list = self.convert_to_list( db_dict, [] )

        # Add a line to the treeview for each channel, playlist and folder
        for mini_dict in converted_list:

            pixbuf = main_win_obj.pixbuf_dict[mini_dict['type'] + '_small']
            text = mini_dict['display_name']
            if mini_dict['video_count'] == 1:
                text += '   [ ' + _('1 video') + ' ]'
            elif mini_dict['video_count']:
                text += '   [ ' \
                + _('{0} videos').format(str(mini_dict['video_count'])) + ' ]'

            self.liststore.append( [True, pixbuf, text, mini_dict['dbid']] )

        # Compile a dictionary, a flattened version of the original 'db_dict'
        #   (i.e. which the original database's folder structure removed)
        # This new dictionary contains a single key-value pair for every
        #   channel, playlist and folder. Dictionary in the form:
        #
        #   key: the channel/playlist/folder dbid
        #   value: the 'mini_dict' for that channel/playlist/folder
        #
        # If the channel/playlist/folder has any child videos, then its
        #   'mini_dict' still has some child 'mini_dicts', one for each video
        for mini_dict in converted_list:
            self.flat_db_dict[mini_dict['dbid']] = mini_dict

        # Display the dialogue window
        self.show_all()


    # Public class methods


    def convert_to_list(self, db_dict, converted_list,
    parent_mini_dict=None, recursion_level=0):

        """Called by self.__init__(). Subsequently called by this function
        recursively.

        Converts the imported 'db_dict' into a list, with each item in the
        list being a 'mini_dict' (the format of both dictionaries is described
        in the comments in mainapp.TartubeApp.export_from_db() ).

        Args:

            db_dict (dict): The dictionary described in self.export_from_db();
                if called from self.__init__(), the original imported
                dictionary; if called recursively, a dictionary from somewhere
                inside the original imported dictionary

            converted_list (list): The converted list so far; this function
                adds more 'mini_dict' items to the list

            parent_mini_dict (dict): The contents of db_dict all represent
                children of the channel/playlist/folder represent by this
                dictionary

            recursion_level (int): The number of recursive calls to this
                function (so far)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 35282 convert_to_list')

        # (Sorting function for the code immediately below)
        def sort_dict_by_name(this_dict):
            return this_dict['name']

        # Deal with importable videos/channels/playlists/folders in
        #   alphabetical order
        for mini_dict in sorted(db_dict.values(), key=sort_dict_by_name):

            if mini_dict['type'] == 'video':

                # Videos are not displayed in the treeview (but we count the
                #   number of videos in each channel/playlist/folder)
                if parent_mini_dict:
                   parent_mini_dict['video_count'] += 1

            else:

                # In the treeview, the channel/playlist/folder name is
                #   indented, so the user can see the folder structure
                mini_dict['display_name'] = (' ' * 3 * recursion_level) \
                + mini_dict['name']

                # Count the number of videos this channel/playlist/folder
                #   contains
                mini_dict['video_count'] = 0

                # Import everything, until the user chooses otherwise
                mini_dict['import_flag'] = True

                # Add this channel/playlist/folder to the list visible in the
                #   treeview
                converted_list.append(mini_dict)
                # Call this function to process any child videos/channels/
                #   playlists/folders
                converted_list = self.convert_to_list(
                    mini_dict['db_dict'],
                    converted_list,
                    mini_dict,
                    recursion_level + 1,
                )

        # Procedure complete
        return converted_list


    # Callback class methods


    def on_checkbutton_toggled(self, checkbutton, path):

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

        Respond when the user selects/deselects an item in the treeview.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            path (int): A number representing the widget's row

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 35347 on_checkbutton_toggled')

        # The user has clicked on the checkbutton widget, so toggle the widget
        #   itself
        self.liststore[path][0] = not self.liststore[path][0]

        # Update the data to be returned (eventually) to the calling
        #   mainapp.TartubeApp.import_into_db() function
        mini_dict = self.flat_db_dict[self.liststore[path][3]]
        mini_dict['import_flag'] = self.liststore[path][0]


    def on_select_all_clicked(self, button):

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

        Mark all channels/playlists/folders to be imported.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 35372 on_select_all_clicked')

        for path in range(0, len(self.liststore)):
            self.liststore[path][0] = True

        for mini_dict in self.flat_db_dict.values():
            mini_dict['import_flag'] = True


    def on_deselect_all_clicked(self, button):

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

        Mark all channels/playlists/folders to be not imported.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 35394 on_deselect_all_clicked')

        for path in range(0, len(self.liststore)):
            self.liststore[path][0] = False

        for mini_dict in self.flat_db_dict.values():
            mini_dict['import_flag'] = False


class InsertVideoDialogue(Gtk.Dialog):

    """Called by mainwin.MainWin.on_video_index_insert_videos().

    Python class handling a dialogue window that inserts invidual video(s)
    into a channel.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        parent_obj (media.Channel, media.Playlist or media.Folder): Name of
            the container into which videos are to be inserted. At the moment,
            no calling code specifies a playlist or folder, but such a call is
            nevertheless permitted

    """


    # Standard class methods


    def __init__(self, main_win_obj, parent_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 35428 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Insert videos\' dialogue starts here.' \
            + ' In the Videos tab, right-click a channel and select' \
            + ' Channel actions > Insert videos...'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.textbuffer = None                  # Gtk.TextBuffer
        self.mark_start = None                  # Gtk.TextMark
        self.mark_end = None                    # Gtk.TextMark
        self.checkbutton = None                 # Gtk.CheckButton


        # IV list - other
        # ---------------
        # The media.Channel or media.Playlist into which videos are to be
        #   inserted
        self.parent_obj = parent_obj
        # Set up IVs for clipboard monitoring, if required
        self.clipboard_timer_id = None
        self.clipboard_timer_time = 250


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Insert videos'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)

        label = Gtk.Label(_('Copy and paste the links to one or more videos'))
        grid.attach(label, 0, 0, 1, 1)

        if main_win_obj.app_obj.operation_convert_mode == 'channel':

            text = _(
                'Links containing multiple videos will be converted to' \
                + ' a channel',
            )

        elif main_win_obj.app_obj.operation_convert_mode == 'playlist':

            text = _(
                'Links containing multiple videos will be converted to a' \
                + ' playlist',
            )

        elif main_win_obj.app_obj.operation_convert_mode == 'multi':

            text = _(
                'Links containing multiple videos will be downloaded' \
                + ' separately',
            )

        elif main_win_obj.app_obj.operation_convert_mode == 'disable':

            text = _(
                'Links containing multiple videos will not be downloaded'
                + ' at all',
            )

        label = Gtk.Label()
        label.set_markup('<i>' + text + '</i>')
        grid.attach(label, 0, 1, 1, 1)

        frame = Gtk.Frame()
        grid.attach(frame, 0, 2, 1, 1)

        scrolledwindow = Gtk.ScrolledWindow()
        frame.add(scrolledwindow)
        # (Set enough vertical room for at several URLs)
        scrolledwindow.set_size_request(-1, 150)

        textview = Gtk.TextView()
        scrolledwindow.add(textview)
        textview.set_hexpand(True)
        self.textbuffer = textview.get_buffer()

        # Some callbacks will complain about invalid iterators, if we try to
        #   use Gtk.TextIters, so use Gtk.TextMarks instead
        self.mark_start = self.textbuffer.create_mark(
            'mark_start',
            self.textbuffer.get_start_iter(),
            True,               # Left gravity
        )
        self.mark_end = self.textbuffer.create_mark(
            'mark_end',
            self.textbuffer.get_end_iter(),
            False,              # Not left gravity
        )

        # Drag-and-drop onto the textview inevitably inserts a URL in the
        #   middle of another URL. No way to prevent that, but we can disable
        #   drag-and-drop in the textview altogether, and instead handle it
        #   from the dialogue window itself
        textview.drag_dest_unset()
        self.connect('destroy', self.close, textview)

        self.connect('drag-data-received', self.on_window_drag_data_received)
        self.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
        self.drag_dest_set_target_list(None)
        self.drag_dest_add_text_targets()

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 3, 1, 1)

        # Display the parent channel/playlist in a combo (so the layout of this
        #   window is the same as that for AddVideoDialogue)
        label2 = Gtk.Label()
        grid.attach(label2, 0, 4, 1, 1)

        if isinstance(parent_obj, media.Channel):
            label2.set_text(_('Insert the videos into this channel:'))
            pixbuf = main_win_obj.pixbuf_dict['channel_small']
        elif isinstance(parent_obj, media.Playlist):
            label2.set_text(_('Insert the videos into this playlist:'))
            pixbuf = main_win_obj.pixbuf_dict['playlist_small']
        else:
            label2.set_text(_('Insert the videos into this folder:'))
            pixbuf = main_win_obj.pixbuf_dict['folder_small']

        listmodel = Gtk.ListStore(GdkPixbuf.Pixbuf, str)
        listmodel.append( [pixbuf, '  ' + self.parent_obj.name] )
        combo = Gtk.ComboBox.new_with_model(listmodel)
        grid.attach(combo, 0, 5, 1, 1)
        combo.set_hexpand(True)

        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, False)
        combo.add_attribute(renderer_text, 'text', 1)

        combo.set_active(0)
#       combo.connect('changed', self.on_combo_changed)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 6, 1, 1)

        self.checkbutton = Gtk.CheckButton()
        grid.attach(self.checkbutton, 0, 7, 1, 1)
        self.checkbutton.set_label(_('Enable automatic copy/paste'))
        self.checkbutton.connect('toggled', self.on_checkbutton_toggled)

        # Paste in the contents of the clipboard (if it contains valid URLs)
        if main_win_obj.app_obj.dialogue_copy_clipboard_flag:
            utils.add_links_to_textview_from_clipboard(
                main_win_obj.app_obj,
                self.textbuffer,
                self.mark_start,
                self.mark_end,
            )

        # Display the dialogue window
        self.show_all()


    def close(self, also_self, textview):

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

        We have disabled drag-and-drop directly into the textview, but this
        generates a Gtk warning when the textview is destroyed.

        Workaround is to re-enable drag-and-drop into the textview immediately
        before it (and its window) are destroyed.

        Args:

            also_self (mainwin.InsertVideoDialogue): Another copy of this
                window

            textview (Gtk.TextView): The textview for which drag-and-drop has
                been disabled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 35634 close')

        textview.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)


    # Callback class methods


    def on_checkbutton_toggled(self, checkbutton):

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

        Enables/disables clipboard monitoring.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 35655 on_checkbutton_toggled')

        if not checkbutton.get_active() \
        and self.clipboard_timer_id is not None:

            # Stop the timer
            GObject.source_remove(self.clipboard_timer_id)
            self.clipboard_timer_id = None

        elif checkbutton.get_active() and self.clipboard_timer_id is None:

            # Start the timer
            self.clipboard_timer_id = GObject.timeout_add(
                self.clipboard_timer_time,
                self.clipboard_timer_callback,
            )


    def on_window_drag_data_received(self, window, context, x, y, data, info,
    time):

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

        Handles drag-and-drop anywhere in the dialogue window.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 35682 on_window_drag_data_received')

        utils.add_links_to_textview_from_clipboard(
            self.main_win_obj.app_obj,
            self.textbuffer,
            self.mark_start,
            self.mark_end,
            # Specify the drag-and-drop text, so the called function uses that,
            #   rather than the clipboard text
            data.get_text(),
        )


    def clipboard_timer_callback(self):

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

        Periodically checks the system's clipboard, and adds any new URLs to
        the dialogue window's textview.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 35704 clipboard_timer_callback')

        utils.add_links_to_textview_from_clipboard(
            self.main_win_obj.app_obj,
            self.textbuffer,
            self.mark_start,
            self.mark_end,
        )

        # Return 1 to keep the timer going
        return 1


class MountDriveDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.start().

    Python class handling a dialogue window that asks the user what to do,
    if the drive containing Tartube's data directory is not mounted or is
    unwriteable.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        unwriteable_flag (bool): True if the data directory is unwriteable;
            False if the data directory is missing altogether

    """


    # Standard class methods


    def __init__(self, main_win_obj, unwriteable_flag=False):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 35741 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Mount drive\' dialogue starts here.' \
            + ' Visible on startup, if Tartube\'s data folder does not' \
            + ' exist'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.radiobutton = None                 # Gtk.RadioButton
        self.radiobutton2 = None                # Gtk.RadioButton
        self.combo = None                       # Gtk.ComboBox
        self.radiobutton3 = None                # Gtk.RadioButton
        self.radiobutton4 = None                # Gtk.RadioButton
        self.radiobutton5 = None                # Gtk.RadioButton


        # IV list - other
        # ---------------
        # Flag set to True if the data directory specified by
        #   mainapp.TartubeApp.data_dir is now available
        self.available_flag = False


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Mount drive'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
        )

        self.set_modal(True)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)
        grid.set_column_spacing(main_win_obj.spacing_size)
        # (Actually, the grid width of the area to the right of the Tartube
        #   logo)
        grid_width = 2

        image = Gtk.Image.new_from_pixbuf(
            main_win_obj.pixbuf_dict['system_icon'],
        )
        grid.attach(image, 0, 0, 1, 3)

        label = Gtk.Label(
            _('The Tartube data folder is set to:'),
        )
        grid.attach(label, 1, 0, grid_width, 1)

        label = Gtk.Label()
        grid.attach(label, 1, 1, grid_width, 1)
        label.set_markup(
            '<b>' \
            + utils.shorten_string(main_win_obj.app_obj.data_dir, 50) \
            + '</b>',
        )

        if not unwriteable_flag:
            label2 = Gtk.Label(_('...but this folder doesn\'t exist'))
        else:
            label2 = Gtk.Label(
                _('...but Tartube cannot write to this folder'),
            )

        grid.attach(label2, 1, 2, grid_width, 1)

        # Separator
        grid.attach(Gtk.HSeparator(), 1, 3, grid_width, 1)

        self.radiobutton = Gtk.RadioButton.new_with_label_from_widget(
            None,
            _('I have mounted the drive, please try again'),
        )
        grid.attach(self.radiobutton, 1, 4, grid_width, 1)

        self.radiobutton2 = Gtk.RadioButton.new_with_label_from_widget(
            self.radiobutton,
            _('Use this data folder:'),
        )
        grid.attach(self.radiobutton2, 1, 5, grid_width, 1)
        # (Signal connect appears below)

        store = Gtk.ListStore(str)
        for item in self.main_win_obj.app_obj.data_dir_alt_list:
            store.append([item])

        self.combo = Gtk.ComboBox.new_with_model(store)
        grid.attach(self.combo, 1, 6, grid_width, 1)
        self.combo.set_hexpand(True)
        renderer_text = Gtk.CellRendererText()
        self.combo.pack_start(renderer_text, True)
        self.combo.add_attribute(renderer_text, 'text', 0)
        self.combo.set_entry_text_column(0)
        self.combo.set_active(0)
        self.combo.set_sensitive(False)

        # (Signal connect from above)
        self.radiobutton2.connect(
            'toggled',
            self.on_radiobutton_toggled,
        )

        self.radiobutton3 = Gtk.RadioButton.new_with_label_from_widget(
            self.radiobutton2,
            _('Select a different data folder'),
        )
        grid.attach(self.radiobutton3, 1, 7, grid_width, 1)

        self.radiobutton4 = Gtk.RadioButton.new_with_label_from_widget(
            self.radiobutton3,
            _('Use the default data folder'),
        )
        grid.attach(self.radiobutton4, 1, 8, grid_width, 1)

        self.radiobutton5 = Gtk.RadioButton.new_with_label_from_widget(
            self.radiobutton4,
            _('Shut down Tartube'),
        )
        grid.attach(self.radiobutton5, 1, 9, grid_width, 1)

        # Separator
        grid.attach(Gtk.HSeparator(), 1, 10, grid_width, 1)

        button = Gtk.Button.new_with_label(_('Cancel'))
        grid.attach(button, 1, 11, 1, 1)
        button.connect('clicked', self.on_cancel_button_clicked)

        button2 = Gtk.Button.new_with_label(_('OK'))
        grid.attach(button2, 2, 11, 1, 1)
        button2.connect('clicked', self.on_ok_button_clicked)

        # Display the dialogue window
        self.show_all()


    # Public class methods


    def do_try_again(self):

        """Called by self.on_ok_button_clicked().

        The user has selected 'I have mounted the drive, please try again'.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 35903 do_try_again')

        app_obj = self.main_win_obj.app_obj

        if os.path.exists(app_obj.data_dir):

            # Data directory exists
            self.available_flag = True
            self.destroy()

        else:

            # Data directory still does not exist. Inform the user
            mini_win = app_obj.dialogue_manager_obj.show_msg_dialogue(
                _(
                'The folder still doesn\'t exist. Please try a' \
                + ' different option',
                ),
                'error',
                'ok',
                self,           # Parent window is this window
            )

            mini_win.set_modal(True)


    def do_select_dir(self):

        """Called by self.on_ok_button_clicked().

        The user has selected 'Select a different data directory'.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 35937 do_select_dir')

        if self.main_win_obj.app_obj.prompt_user_for_data_dir():

            # New data directory selected
            self.available_flag = True
            self.destroy()


    # Callback class methods


    def on_ok_button_clicked(self, button):

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

        When the OK button is clicked, perform the selected action.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 35962 on_ok_button_clicked')

        if self.radiobutton.get_active():
            self.do_try_again()

        elif self.radiobutton2.get_active():

            tree_iter = self.combo.get_active_iter()
            model = self.combo.get_model()
            path = model[tree_iter][0]
            self.main_win_obj.app_obj.set_data_dir(path)
            self.available_flag = True
            self.destroy()

        elif self.radiobutton3.get_active():
            self.do_select_dir()

        elif self.radiobutton4.get_active():

            self.main_win_obj.app_obj.reset_data_dir()
            self.available_flag = True
            self.destroy()

        elif self.radiobutton5.get_active():
            self.available_flag = False
            self.destroy()


    def on_cancel_button_clicked(self, button):

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

        When the Cancel button is clicked, shut down Tartube.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 36003 on_cancel_button_clicked')

        self.available_flag = False
        self.destroy()


    def on_radiobutton_toggled(self, button):

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

        When the radiobutton just above it is toggled, (de)sensitise the
        combobox.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 36023 on_radiobutton_toggled')

        if button.get_active():
            self.combo.set_sensitive(True)
        else:
            self.combo.set_sensitive(False)


class MoveContainerDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.move_container_to_top() and
    move_container().

    Python class handling a dialogue window that prompts the user for
    confirmation, before a media.Channel, media.Playlist or media.Folder object
    in the Video Index.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        source_obj (media.Channel, media.Playlist or media.Folder): The media
            data object to be moved

    Optional args:

        dest_obj (media.Folder or None): The destination container ('None' if
            moving to the top level)

    """


    # Standard class methods


    def __init__(self, main_win_obj, source_obj, dest_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 34168 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Move container\' dialogue starts here.' \
            + ' In the Videos tab, drag a channel, playlist or folder onto' \
            + ' another folder',
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.button = None                      # Gtk.CheckButton
        self.button2 = None                     # Gtk.CheckButton


        # IV list - other
        # ---------------


        # Code
        # ----

        # Create the dialogue window
        Gtk.Dialog.__init__(
            self,
            _('Move container'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_NO, Gtk.ResponseType.NO,
                Gtk.STOCK_YES, Gtk.ResponseType.YES,
            )
        )

        self.set_modal(True)
        self.set_resizable(False)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)

        source_type = source_obj.get_type()
        if source_type == 'channel':
            msg = _('Are you sure you want to move this channel:')
        elif source_type == 'playlist':
            msg = _('Are you sure you want to move this playlist:')
        else:
            msg = _('Are you sure you want to move this folder:')

        msg += '\n\n   ' + source_obj.name

        if dest_obj is not None:

            msg += '\n\n' + _('into this folder:') + '\n\n   ' \
            + dest_obj.name + '\n\n' + _(
                'This procedure will move all downloaded files to the new' \
                + ' location',
            )

        else:
            msg += '\n\n' + _(
                'This procedure will move all downloaded files to the top' \
                + ' level of Tartube\'s data folder',
            )

        label = Gtk.Label(msg)
        grid.attach(label, 0, 0, 2, 1)

        if dest_obj is not None \
        and isinstance(dest_obj, media.Folder) \
        and dest_obj.temp_flag:

            # Separator
            grid.attach(Gtk.HSeparator(), 0, 1, 2, 1)

            image = Gtk.Image.new_from_pixbuf(
                main_win_obj.pixbuf_dict['warning_large'],
            )
            grid.attach(image, 0, 2, 1, 1)

            label2 = Gtk.Label(
                utils.tidy_up_long_string(
                    _(
                    'WARNING: The destination folder is marked as temporary,' \
                    + ' so everything inside it will be DELETED when Tartube' \
                    + ' restarts!',
                    ),
                    main_win_obj.long_string_max_len,
                ),
            )

            grid.attach(label2, 1, 2, 1, 1)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 3, 2, 1)

        self.button = Gtk.CheckButton.new_with_label(
            _('After moving, switch to the destination'),
        )
        self.button.set_alignment(0, 0.5)
        grid.attach(self.button, 0, 4, 2, 1)
        if main_win_obj.app_obj.dialogue_move_select_flag:
            self.button.set_active(True)

        self.button2 = Gtk.CheckButton.new_with_label(
            _('Always show this window before moving containers'),
        )
        self.button2.set_alignment(0, 0.5)
        grid.attach(self.button2, 0, 5, 2, 1)
        if main_win_obj.app_obj.dialogue_move_container_flag:
            self.button2.set_active(True)

        # Display the dialogue window
        self.show_all()


class MoveVideoDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.move_videos().

    Python class handling a dialogue window that prompts the user for
    confirmation, before moving one or more media.Video objects in the Video
    Index.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        dest_obj (media.Channel, media.Playlist or media.Folder): The
            destination container

        media_list (list): List of media.Video objects to be deleted

    """


    # Standard class methods


    def __init__(self, main_win_obj, dest_obj, media_list):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 34169 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Move videos\' dialogue starts here.' \
            + ' In the Videos tab, drag a video onto a channel, playlist or' \
            + ' folder',
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.button = None                      # Gtk.CheckButton
        self.button2 = None                     # Gtk.CheckButton


        # IV list - other
        # ---------------


        # Code
        # ----

        # Create the dialogue window
        Gtk.Dialog.__init__(
            self,
            _('Move videos'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_NO, Gtk.ResponseType.NO,
                Gtk.STOCK_YES, Gtk.ResponseType.YES,
            )
        )

        self.set_modal(True)
        self.set_resizable(False)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)

        if len(media_list) == 1:
            msg = _(
                'Are you sure you want to move the video to \'{0}\'?',
            ).format(dest_obj.name)

        else:

            msg = _(
                'Are you sure you want to move {0} videos to \'{1}\'?',
            ).format(len(media_list), dest_obj.name)

        label = Gtk.Label(
            utils.tidy_up_long_string(
                msg,
                main_win_obj.very_long_string_max_len,
            ),
        )
        grid.attach(label, 0, 0, 2, 1)

        if isinstance(dest_obj, media.Folder) \
        and dest_obj.temp_flag:

            # Separator
            grid.attach(Gtk.HSeparator(), 0, 1, 2, 1)

            image = Gtk.Image.new_from_pixbuf(
                main_win_obj.pixbuf_dict['warning_large'],
            )
            grid.attach(image, 0, 2, 1, 1)

            label2 = Gtk.Label(
                utils.tidy_up_long_string(
                    _(
                    'WARNING: The destination folder is marked as temporary,' \
                    + ' so everything inside it will be DELETED when Tartube' \
                    + ' restarts!',
                    ),
                    main_win_obj.long_string_max_len,
                ),
            )

            grid.attach(label2, 1, 2, 1, 1)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 3, 2, 1)

        self.button = Gtk.CheckButton.new_with_label(
            _('After moving, switch to the destination'),
        )
        self.button.set_alignment(0, 0.5)
        grid.attach(self.button, 0, 4, 2, 1)
        if main_win_obj.app_obj.dialogue_move_select_flag:
            self.button.set_active(True)

        self.button2 = Gtk.CheckButton.new_with_label(
            _('Always show this window before moving videos'),
        )
        self.button2.set_alignment(0, 0.5)
        grid.attach(self.button2, 0, 5, 2, 1)
        if main_win_obj.app_obj.dialogue_move_video_flag:
            self.button2.set_active(True)

        # Display the dialogue window
        self.show_all()


class MSYS2Dialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.on_menu_open_msys2().

    Python class handling a dialogue window that advises users how the MINGW
    terminal is used.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

    """


    # Standard class methods


    def __init__(self, main_win_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 36051 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'MSYS2 terminal\' dialogue starts here.' \
            + ' In the main window menu, click System > Open MSYS2 terminal' \
            + ' (MS Windows only)'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        #   (none)


        # IV list - other
        # ---------------
        #   (none)


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('MSYS2 terminal'),
            None,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(True)
        # Without this line, this dialogue window is partially obscured beneath
        #   the MSYS2 window (the exact opposite of what we want)
        self.set_keep_above(True)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)

        label = Gtk.Label()
        grid.attach(label, 0, 0, 1, 1)
        label.set_markup(
            _('On MS Windows, Tartube runs in the MSYS2 environment.'),
        )
        label.set_xalign(0)

        label2 = Gtk.Label()
        grid.attach(label2, 0, 1, 1, 1)
        label2.set_markup(
            _(
            'Advanced users can use the MSYS2 terminal to make changes to' \
            + ' the\nenvironment (for example, to tweak youtube-dl or FFmpeg)',
            ),
        )
        label2.set_xalign(0)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 2, 1, 1)

        checkbutton = Gtk.CheckButton()
        grid.attach(checkbutton, 0, 3, 1, 1)
        checkbutton.set_label(_('Always show this window'))
        if main_win_obj.app_obj.show_msys2_dialogue_flag:
            checkbutton.set_active(True)
        checkbutton.connect('toggled', self.on_checkbutton_toggled)

        # Display the dialogue window
        self.show_all()


    # Callback class methods


    def on_checkbutton_toggled(self, checkbutton):

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

        Enables/disables showing this dialogue window.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 36148 on_checkbutton_toggled')

        if checkbutton.get_active():
            self.main_win_obj.app_obj.set_show_msys2_dialogue_flag(True)
        else:
            self.main_win_obj.app_obj.set_show_msys2_dialogue_flag(False)


class NewbieDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.download_manager_finished().

    Python class handling a dialogue window that advises a newbie what to do if
    the download operation failed to check/download any videos.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        classic_mode_flag (bool): True if the download operation was launched
            from the Classic Mode tab, False otherwise

    """


    # Standard class methods


    def __init__(self, main_win_obj, classic_mode_flag):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 36179 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: Download failure dialogue starts here.' \
            + ' Visible when the user tries to check/download videos, but' \
            + ' no videos are checked/downloaded'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        #   (none)


        # IV list - other
        # ---------------
        # Flag set to True when various widgets are selected
        self.update_flag = False
        self.config_flag = False
        self.change_flag = False
        self.website_flag = False
        self.issues_flag = False
        self.show_flag = main_win_obj.app_obj.show_newbie_dialogue_flag


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Downloads'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(True)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)
        grid.set_column_spacing(main_win_obj.spacing_size)
        # (Actually, the grid width of the area to the right of the Tartube
        #   logo)
        grid_width = 2

        image = Gtk.Image.new_from_pixbuf(
            main_win_obj.pixbuf_dict['newbie_icon'],
        )
        grid.attach(image, 0, 0, 1, 4)

        label = Gtk.Label()
        grid.attach(label, 1, 0, grid_width, 1)
        label.set_markup(
            '<b>' + _('Nothing happened?') + '</b>',
        )

        # Separator
        grid.attach(Gtk.HSeparator(), 1, 1, grid_width, 1)

        if not classic_mode_flag:
            extra_rows = 0

        else:
            extra_rows = 3

#            label2 = Gtk.Label(
#                _('Check the video/audio file actually exists'),
#            )
            label2 = Gtk.Label(
                _('Check the requested format is actually available'),
            )
            grid.attach(label2, 1, 2, grid_width, 1)

            label3 = Gtk.Label(
                _('(Try converting instead of a direct download)'),
            )
            grid.attach(label3, 1, 3, grid_width, 1)

            frame = Gtk.Frame()
            grid.attach(frame, 1, 4, grid_width, 1)

            image2 = Gtk.Image.new_from_pixbuf(
                main_win_obj.pixbuf_dict['newbie_classic_icon'],
            )
            frame.add(image2)

        label4 = Gtk.Label(
            '   ' + _('Check the downloader is installed and updated') + '   ',
        )
        grid.attach(label4, 1, (extra_rows + 2), grid_width, 1)

        button = Gtk.Button.new_with_label(
            _('Update') + ' ' + self.main_win_obj.app_obj.get_downloader(),
        )
        grid.attach(button, 1, (extra_rows + 3), grid_width, 1)
        button.connect('clicked', self.on_update_button_clicked)

        label5 = Gtk.Label(
            _('Tell Tartube where to find the downloader'),
        )
        grid.attach(label5, 1, (extra_rows + 4), grid_width, 1)

        button2 = Gtk.Button.new_with_label(
            _('Set the downloader\'s file path'),
        )
        grid.attach(button2, 1, (extra_rows + 5), grid_width, 1)
        button2.connect('clicked', self.on_config_button_clicked)

        button3 = Gtk.Button.new_with_label(
            _('Try a different downloader'),
        )
        grid.attach(button3, 1, (extra_rows + 6), grid_width, 1)
        button3.connect('clicked', self.on_change_button_clicked)

        label6 = Gtk.Label(
            _('Find more help'),
        )
        grid.attach(label6, 1, (extra_rows + 7), grid_width, 1)

        button4 = Gtk.Button.new_with_label(
            _('Read the FAQ'),
        )
        grid.attach(button4, 1, (extra_rows + 8), 1, 1)
        button4.connect('clicked', self.on_website_button_clicked)

        button5 = Gtk.Button.new_with_label(
            _('Ask for help'),
        )
        grid.attach(button5, 2, (extra_rows + 8), 1, 1)
        button5.connect('clicked', self.on_issues_button_clicked)

        # Separator
        grid.attach(Gtk.HSeparator(), 1, (extra_rows + 9), grid_width, 1)

        label7 = Gtk.Label()
        grid.attach(label7, 1, (extra_rows + 10), grid_width, 1)
        label7.set_markup(
            _(
                'Don\'t forget to check the <b>Output</b> tab and the\n' \
                '<b>Errors/Warnings</b> tab for error messages!',
            ),
        )

        # Separator
        grid.attach(Gtk.HSeparator(), 1, (extra_rows + 11), grid_width, 1)

        checkbutton = Gtk.CheckButton()
        grid.attach(checkbutton, 1, (extra_rows + 12), grid_width, 1)
        checkbutton.set_label(_('Always show this window'))
        if self.show_flag:
            checkbutton.set_active(True)
        checkbutton.connect('toggled', self.on_checkbutton_toggled)

        # Display the dialogue window
        self.show_all()


    # Callback class methods


    def on_change_button_clicked(self, button):

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

        When the button is clicked, open the preferences window.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 36364 on_change_button_clicked')

        self.change_flag = True
        self.destroy()


    def on_checkbutton_toggled(self, checkbutton):

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

        Enables/disables showing this dialogue window.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 36383 on_checkbutton_toggled')

        if checkbutton.get_active():
            self.show_flag = True
        else:
            self.show_flag = False


    def on_config_button_clicked(self, button):

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

        When the button is clicked, open the preferences window.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 36404 on_config_button_clicked')

        self.config_flag = True
        self.destroy()


    def on_issues_button_clicked(self, button):

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

        When the button is clicked, open the Tartube issues page.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 36423 on_issues_button_clicked')

        self.issues_flag = True
        self.destroy()


    def on_update_button_clicked(self, button):

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

        When the button is clicked, perform an update operation.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 36442 on_update_button_clicked')

        self.update_flag = True
        self.destroy()


    def on_website_button_clicked(self, button):

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

        When the button is clicked, open the Tartube website.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 36461 on_website_button_clicked')

        self.website_flag = True
        self.destroy()


class OutputOverrideDialogue(Gtk.Dialog):

    """Called by mainwin.on_video_catalogue_output_override().

    Python class handling a dialogue window to prompt the user for a video
    name. If specified, the name is used to override youtube-dl's output
    template while downloading the video.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        video_obj (media.Video): The video to be renamed

    """


    # Standard class methods


    def __init__(self, main_win_obj, video_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 36490 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Download with name\' and \'Re-download' \
            + ' with name\' dialogue starts here. Right-click a video and' \
            + ' select Special > Download with name...',
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj
        # The video to be renamed
        self.video_obj = video_obj


        # IV list - Gtk widgets
        # ---------------------
        # (none)


        # IV list - other
        # ---------------
        # The new name specified by the user, or None if no name has been
        #   specified, or if the specified name matches the existing name
        self.new_name = None


        # Code
        # ----

        if not video_obj.dl_flag:
            title = _('Download with name')
        else:
            title = _('Re-download with name')

        Gtk.Dialog.__init__(
            self,
            title,
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)

        label = Gtk.Label(
            _('Override the output template with this name'),
        )
        grid.attach(label, 0, 0, 1, 1)

        entry = Gtk.Entry()
        grid.attach(entry, 0, 1, 1, 1)
        entry.connect('changed', self.on_entry_changed)

        # Display the dialogue window
        self.show_all()


    # Callback class methods


    def on_entry_changed(self, entry):

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

        Updates IVs.

        Args:

            entry (Gtk.Entry): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 36576 on_entry_changed')

        text = entry.get_text()
        if text == '' or text == self.video_obj.name:
            self.new_name = None
        else:
            self.new_name = text


class PrepareClipDialogue(Gtk.Dialog):

    """Called by mainwin.MainWin.on_video_catalogue_process_clip().

    Prompt the user to download or create video clips.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        video_obj (media.Video): The video from which video clips will be
            downloaded or extracted

    """


    # Standard class methods


    def __init__(self, main_win_obj, video_obj=None):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 36607 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Create video clip\' dialogue starts' \
            + ' here. In the Videos tab, right-click a video and select' \
            + ' Special > Create video clips...'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj
        # The media.Video to be used (may be None when called from the Classic
        #   Mode tab)
        self.video_obj = video_obj


        # IV list - Gtk widgets
        # ---------------------
        self.radiobutton = None                     # Gtk.RadioButton
        self.radiobutton2 = None                    # Gtk.RadioButton
        self.radiobutton3 = None                    # Gtk.RadioButton
        self.label5 = None                          # Gtk.Label
        self.button = None                          # Gtk.Button
        self.label9 = None                          # Gtk.Label
        self.timestamp_liststore = None             # Gtk.ListStore
        self.button10 = None                        # Gtk.Button
        self.button11 = None                        # Gtk.Button
        self.button12 = None                        # Gtk.Button


        # IV list - other
        # ---------------
        # Store the user's choices as IVs, so the calling function can
        #   retrieve them

        # Clip mode: 'downloader' to download clips using the downloader
        #   directly (currently only available for yt-dlp), 'ffmpeg' to
        #   download clips using FFmpeg, or 'create' to create clips using the
        #   already-downloaded video
        self.clip_mode = self.main_win_obj.app_obj.video_timestamps_dl_mode
        # Download mode: 'chapters' to download chapters using yt-dlp directly
        #   (when available), 'single' to download/extract a single clip using
        #   specified timestamp(s), 'multiple' to download/extract multiple
        #   clips. Set to None when self.clip_mode is 'create'
        self.dl_mode = None
        # When self.dl_mode is 'chapters', an optional regex to apply. If not
        #   specified, remains set to None
        self.optional_regex = None
        # When self.dl_mode is 'single', the start/stop timestamps and the clip
        #   title. The start timestamp is compulsory, the others are optional,
        #   but timestamps must be in a valid format where specified)
        self.start_stamp = None
        self.stop_stamp = None
        self.clip_title = None
        # When self.dl_mode is 'multiple', a list of timestamps (in groups of
        #   3), used to set mainapp.TartubeApp.temp_stamp_buffer_dict
        self.stamp_list = []

        # When called from the Classic Mode tab, and self.video_obj is None,
        #   an alternative timestamp list, in the same format as
        #   media.Video.stamp_list. Used in the dummy media.Video object, when
        #   it is created
        self.classic_stamp_list = []
        # The URL entered, when the box is visible (set to None rather than an
        #   empty string)
        self.classic_url = None
        # Optional video name; if set, can be used with all clips (depending on
        #   the value of mainapp.TartubeApp.split_video_name_mode)
        self.classic_video_name = None

        # Flag set to true when one of the 'Download' buttons is clicked,
        #   which closes the window (in the absence of an OK button)
        self.start_dl_flag = False

        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Create video clips'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
            )
        )

        self.set_modal(True)
        app_obj = self.main_win_obj.app_obj

        # Set up the dialogue window
        self.set_default_size(
            app_obj.config_win_width,
            -1,
        )

        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)

        grid_width = 2
        h_offset = 0

        if self.video_obj is None:

            label = Gtk.Label()
            grid.attach(label, 0, 0, grid_width, 1)
            label.set_markup('<b>' + _('Enter the video\'s URL:') + '</b>')
            label.set_alignment(0, 0.5)

            entry = Gtk.Entry()
            grid.attach(entry, 0, 1, grid_width, 1)
            entry.connect('changed', self.on_url_entry_changed)

            if app_obj.split_video_name_mode == 'num' \
            or app_obj.split_video_name_mode == 'clip' \
            or app_obj.split_video_name_mode == 'num_clip' \
            or app_obj.split_video_name_mode == 'clip_num':

                h_offset = 3

                # Separator
                grid.attach(Gtk.HSeparator(), 0, 2, grid_width, 1)

            else:

                h_offset = 5

                label2 = Gtk.Label()
                grid.attach(label2, 0, 2, grid_width, 1)
                label2.set_markup(
                    _('Optional video name (will be added to all clips)'),
                )
                label2.set_alignment(0, 0.5)

                entry2 = Gtk.Entry()
                grid.attach(entry2, 0, 3, grid_width, 1)
                entry2.connect('changed', self.on_optional_entry_changed)

                # Separator
                grid.attach(Gtk.HSeparator(), 0, 4, grid_width, 1)

        label3 = Gtk.Label()
        grid.attach(label3, 0, (0 + h_offset), grid_width, 1)
        label3.set_markup('<b>' + _('Select a method:') + '</b>')
        label3.set_alignment(0, 0.5)

        self.radiobutton = Gtk.RadioButton.new_with_label_from_widget(
            None,
            _('Download new clips using yt-dlp'),
        )
        grid.attach(self.radiobutton, 0, (1 + h_offset), 1, 1)
        # (Signal connect appears below)
        if app_obj.ytdl_fork != 'yt-dlp':
            self.radiobutton.set_sensitive(False)

        self.radiobutton2 = Gtk.RadioButton.new_from_widget(self.radiobutton)
        grid.attach(self.radiobutton2, 0, (2 + h_offset), 1, 1)
        # (Signal connect appears below)
        self.radiobutton2.set_label(
            _('Download new clips using FFmpeg'),
        )
        if self.clip_mode == 'ffmpeg':
            self.radiobutton2.set_active(True)

        self.radiobutton3 = Gtk.RadioButton.new_from_widget(self.radiobutton2)
        grid.attach(self.radiobutton3, 1, (1 + h_offset), 1, 1)
        # (Signal connect appears below)
        self.radiobutton3.set_label(
            _('Create clips from the downloaded video'),
        )
        if self.clip_mode == 'create':
            self.radiobutton3.set_active(True)

        if self.video_obj is None or not self.video_obj.dl_flag:
            self.radiobutton3.set_sensitive(False)

        else:
            label4 = Gtk.Label()
            grid.attach(label4, 1, (2 + h_offset), 1, 1)
            label4.set_markup(
                '<i>' + _(
                    'Warning: Downloading new clips is usually MUCH quicker!',
                ) + '</i>',
            )
            label4.set_alignment(0, 0.5)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, (3 + h_offset), grid_width, 1)

        # (Additional grid, to avoid messing up the format of the previous one)
        grid2 = Gtk.Grid()
        grid.attach(grid2, 0, (4 + h_offset), grid_width, 1)
        grid2.set_column_spacing(main_win_obj.spacing_size)
        grid2.set_row_spacing(main_win_obj.spacing_size)

        grid2_width = 4

        self.label5 = Gtk.Label()
        grid2.attach(self.label5, 0, 0, grid2_width, 1)
        if self.clip_mode == 'create':
            self.label5.set_markup(
                '<b>' + _('Create one clip') + '</b>:',
            )
        else:
            self.label5.set_markup(
                '<b>' + _('Download one clip') + '</b>:',
            )
        self.label5.set_alignment(0, 0.5)

        label6 = Gtk.Label()
        grid2.attach(label6, 0, 1, 1, 1)
        label6.set_markup(_('Start timestamp (e.g. 15:29)'))
        label6.set_alignment(0, 0.5)

        entry_width = 12

        entry3 = Gtk.Entry()
        grid2.attach(entry3, 1, 1, 1, 1)
        # (Signal connect appears below)
        entry3.set_width_chars(entry_width)

        label7 = Gtk.Label()
        grid2.attach(label7, 2, 1, 1, 1)
        label7.set_markup(_('Stop timestamp (optional)'))
        label7.set_alignment(0, 0.5)

        entry4 = Gtk.Entry()
        grid2.attach(entry4, 3, 1, 1, 1)
        entry4.set_width_chars(entry_width)

        label8 = Gtk.Label()
        grid2.attach(label8, 0, 2, 1, 1)
        label8.set_markup(_('Clip title (optional)'))
        label8.set_alignment(0, 0.5)

        entry5 = Gtk.Entry()
        grid2.attach(entry5, 1, 2, 2, 1)

        self.button = Gtk.Button.new_with_label('')
        grid2.attach(self.button, 3, 2, 1, 1)
        # (Signal connect appears below)
        if self.clip_mode == 'create':
            self.set_bold_button_text(self.button, _('Create clip'))
        else:
            self.set_bold_button_text(self.button, _('Download clip'))
        self.button.set_hexpand(True)
        self.button.set_sensitive(False)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, (5 + h_offset), grid_width, 1)

        self.label9 = Gtk.Label()
        grid.attach(self.label9, 0, (6 + h_offset), grid_width, 1)
        if self.clip_mode == 'create':
            self.label9.set_markup(
                '<b>' + _('Create multiple clips') + '</b>:',
            )
        else:
            self.label9.set_markup(
                '<b>' + _('Download multiple clips') + '</b>:',
            )
        self.label9.set_alignment(0, 0.5)

        frame = Gtk.Frame()
        grid.attach(frame, 0, (7 + h_offset), grid_width, 1)

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

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

        for i, column_title in enumerate(
            [ _('Marked'), _('Start'), _('Stop'), _('Clip title') ],
        ):
            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.timestamp_liststore = Gtk.ListStore(bool, str, str, str)
        treeview.set_model(self.timestamp_liststore)

        # Initialise the list
        self.update_treeview()

        # (Additional grid, to avoid messing up the format of the previous one)
        grid3 = Gtk.Grid()
        grid.attach(grid3, 0, (8 + h_offset), grid_width, 1)
        grid3.set_column_spacing(main_win_obj.spacing_size)
        grid3.set_row_spacing(main_win_obj.spacing_size)

        grid3_width = 4

        button2 = Gtk.Button.new_with_label(_('Add timestamp from above'))
        grid3.attach(button2, 0, 0, 1, 1)
        # (Signal connect appears below)
        button2.set_hexpand(True)

        button3 = Gtk.Button.new_with_label(_('Delete timestamp'))
        grid3.attach(button3, 1, 0, 1, 1)
        # (Signal connect appears below)
        button3.set_hexpand(True)

        button4 = Gtk.Button.new_with_label(_('Clip preferences'))
        grid3.attach(button4, 2, 0, 1, 1)
        # (Signal connect appears below)
        button4.set_hexpand(True)

        button5 = Gtk.Button.new_with_label(_('Clear list'))
        grid3.attach(button5, 3, 0, 1, 1)
        # (Signal connect appears below)
        button5.set_hexpand(True)

        # (Additional grid, to avoid messing up the format of the previous one)
        grid4 = Gtk.Grid()
        grid.attach(grid4, 0, (9 + h_offset), grid_width, 1)
        grid4.set_column_spacing(main_win_obj.spacing_size)
        grid4.set_row_spacing(main_win_obj.spacing_size)

        grid4_width = 4

        button6 = Gtk.Button.new_with_label(_('Select all'))
        grid4.attach(button6, 0, 0, 1, 1)
        # (Signal connect appears below)
        button6.set_hexpand(True)

        button7 = Gtk.Button.new_with_label(_('Select none'))
        grid4.attach(button7, 1, 0, 1, 1)
        # (Signal connect appears below)
        button7.set_hexpand(True)

        button8 = Gtk.Button.new_with_label(_('Reset list using copied text'))
        grid4.attach(button8, 2, 0, 1, 1)
        # (Signal connect appears below)
        button8.set_hexpand(True)

        button9 = Gtk.Button.new_with_label(
            _('Reset list using video description')
        )
        grid4.attach(button9, 3, 0, 1, 1)
        # (Signal connect appears below)
        button9.set_hexpand(True)
        if self.video_obj is None:
            button9.set_sensitive(False)

        # (Additional grid, to avoid messing up the format of the previous one)
        grid5 = Gtk.Grid()
        grid.attach(grid5, 0, (10 + h_offset), grid_width, 1)
        grid5.set_column_spacing(main_win_obj.spacing_size)
        grid5.set_row_spacing(main_win_obj.spacing_size)

        grid5_width = 3

        self.button10 = Gtk.Button.new_with_label('')
        grid5.attach(self.button10, 0, 0, 1, 1)
        # (Signal connect appears below)
        if self.clip_mode == 'create':
            self.set_bold_button_text(self.button10, _('Create marked clips'))
        else:
            self.set_bold_button_text(
                self.button10,
                _('Download marked clips'),
            )
        self.button10.set_hexpand(True)

        self.button11 = Gtk.Button.new_with_label('')
        grid5.attach(self.button11, 1, 0, 1, 1)
        # (Signal connect appears below)
        if self.clip_mode == 'create':
            self.set_bold_button_text(self.button11, _('Create all clips'))
        else:
            self.set_bold_button_text(self.button11, _('Download all clips'))
        self.button11.set_hexpand(True)

        self.button12 = Gtk.Button.new_with_label('')
        grid5.attach(self.button12, 2, 0, 1, 1)
        # (Signal connect appears below)
        self.set_bold_button_text(self.button12, _('Download all chapters'))
        self.button12.set_hexpand(True)
        if self.clip_mode != 'downloader':
            self.button12.set_sensitive(False)

        # (Signal connects from above)
        self.radiobutton.connect('toggled', self.on_radiobutton_toggled)
        self.radiobutton2.connect('toggled', self.on_radiobutton2_toggled)
        self.radiobutton3.connect('toggled', self.on_radiobutton3_toggled)

        entry3.connect('changed', self.on_start_entry_changed)
        self.button.connect(
            'clicked',
            self.on_dl_single_button_clicked,
            entry3,
            entry4,
            entry5,
        )

        button2.connect(
            'clicked',
            self.on_add_stamp_button_clicked,
            entry3,
            entry4,
            entry5,
        )
        button3.connect(
            'clicked',
            self.on_delete_stamp_button_clicked,
            treeview,
        )
        button4.connect('clicked', self.on_prefs_button_clicked)
        button5.connect('clicked', self.on_clear_stamp_button_clicked)
        button6.connect('clicked', self.on_select_all_button_clicked)
        button7.connect('clicked', self.on_select_none_button_clicked)

        button8.connect('clicked', self.on_copy_stamp_button_clicked)
        button9.connect('clicked', self.on_extract_stamp_button_clicked)

        self.button10.connect('clicked', self.on_dl_marked_button_clicked)
        self.button11.connect('clicked', self.on_dl_all_button_clicked)
        self.button12.connect('clicked', self.on_dl_chapters_button_clicked)

        # Display the dialogue window
        self.show_all()


    # Support functions


    def set_bold_button_text(self, button, text):

        """Can be called by anything.

        Sets bold label text for a Gtk.Button.

        Args:

            button (Gtk.Button): The button to update

            text (str): The (translated) text, which does not need to contain
                <b>...</b> tags

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37081 set_bold_button_text')

        for child in button.get_children():
            child.set_label('<b>' + text + '</b>')
            child.set_use_markup(True)


    def update_treeview(self):

        """Called by self.__init__().

        Fills or updates the treeview. Based on
        config.VideoEditWin.setup_timestamps_tab_update_treeview().
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37097 update_treeview')

        self.timestamp_liststore.clear()

        if self.video_obj is not None:
            stamp_list = self.video_obj.stamp_list
        else:
            stamp_list = self.classic_stamp_list

        # Add each timestamp/title to the treeview, one row at a time
        for mini_list in 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(
                [ False, start_stamp, stop_stamp, clip_title ],
            )


    # Callback class methods


    def on_add_stamp_button_clicked(self, button, entry, entry2, entry3):

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

        Adds a new timestamp to treeview, optionally with a clip title.

        Code adapted from config.VideoEditWin.on_add_stamp_button_clicked().

        Args:

            button (Gtk.Button): The widget clicked

            entry, entry2, entry3 (Gtk.Entry): Other widgets to modify

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37146 on_add_stamp_button_clicked')

        app_obj = self.main_win_obj.app_obj

        start_stamp = utils.strip_whitespace(entry.get_text())
        stop_stamp = utils.strip_whitespace(entry2.get_text())
        clip_title = utils.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 = '^' + app_obj.timestamp_regex + '$'
        if re.search(regex, start_stamp) \
        and (stop_stamp is None or re.search(regex, stop_stamp)) \
        and utils.timestamp_compare(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 = utils.timestamp_format(app_obj, start_stamp)
            if stop_stamp is not None:
                stop_stamp = utils.timestamp_format(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)
            if self.video_obj is not None:

                stamp_list = self.video_obj.stamp_list
                stamp_list.append([ start_stamp, stop_stamp, clip_title ])

                # (The called function will sort the list)
                self.video_obj.set_timestamps(stamp_list)

            else:

                self.classic_stamp_list.append(
                    [ start_stamp, stop_stamp, clip_title ],
                )
                self.classic_stamp_list.sort()

            # (Show changes, and empty entry boxes. The 'stop' timestamp, if
            #   specified, becomes the 'start' timestamp for the next group)
            self.update_treeview()

            if stop_stamp is None:
                entry.set_text('')
            else:
                entry.set_text(
                    utils.timestamp_add_second(app_obj, stop_stamp),
                )

            entry2.set_text('')
            entry3.set_text('')

        else:

#            app_obj.dialogue_manager_obj.show_msg_dialogue(
#                _('Invalid timestamp(s)'),
#                'error',
#                'ok',
#                self,           # Parent window is this window
#            )
            # The intended dialogue window doesn't behave well, so instead,
            #   display a message in the entry boxes
            if not re.search(regex, start_stamp):
                entry.set_text(_('INVALID'))

            if stop_stamp is not None and not re.search(regex, stop_stamp):
                entry2.set_text(_('INVALID'))


    def on_clear_stamp_button_clicked(self, button):

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

        Empties the video's timestamp list.

        Code adapted from config.VideoEditWin.on_clear_stamp_button_clicked().

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37241 on_clear_stamp_button_clicked')

        if self.video_obj is not None:
            self.video_obj.reset_timestamps()
        else:
            self.classic_stamp_list = []

        self.update_treeview()


    def on_copy_stamp_button_clicked(self, button):

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

        Updates the video's timestamp list using text the user has copied and
        pasted into a dialogue window.

        Code adapted from config.VideoEditWin.on_copy_stamp_button_clicked().

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37267 on_copy_stamp_button_clicked')

        # Open the dialogue window
        dialogue_win = AddStampDialogue(
            self,
            self.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 timestamps, if no text was
            #   added to the dialogue window)
            if text != '':

                if self.video_obj is not None:
                    self.video_obj.extract_timestamps_from_descrip(
                        self.main_win_obj.app_obj,
                        text,
                    )

                else:
                    self.classic_stamp_list \
                    = utils.extract_timestamps_from_descrip(
                        self.main_win_obj.app_obj,
                        text,
                    )

                self.update_treeview()

        # ...before destroying the dialogue window
        dialogue_win.destroy()


    def on_delete_stamp_button_clicked(self, button, treeview):

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

        Deletes the selected timestamp(s) from the video's timestamp list.

        Code adapted from config.VideoEditWin.on_delete_stamp_button_clicked().

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeVies): The treeview displaying the timestamp list

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37326 on_delete_stamp_button_clicked')

        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][1]
        stop_stamp = model[this_iter][2]
        clip_title = model[this_iter][3]

        # Timestamps stored in groups of three, in the form
        #   (start_stamp, stop_stamp, clip_title)
        # Walk the list, and delete the first matching group
        if self.video_obj is not None:
            stamp_list = self.video_obj.stamp_list
        else:
            stamp_list = self.classic_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)

        if self.video_obj is not None:
            # (The called function will sort the list)
            self.video_obj.set_timestamps(mod_list)
        else:
            self.classic_stamp_list = mod_list
            self.classic_stamp_list.sort()

        # (Show changes)
        self.update_treeview()


    def on_dl_all_button_clicked(self, button):

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

        Downloads/creates all clips.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37388 on_dl_all_button_clicked')

        self.dl_mode = 'multiple'

        if self.video_obj is not None:
            self.stamp_list = self.video_obj.stamp_list
        else:
            self.stamp_list = self.classic_stamp_list

        if self.stamp_list:
            self.start_dl_flag = True
            self.close()


    def on_dl_chapters_button_clicked(self, button):

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

        Downloads clips as chapters using yt-dlp directly.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37415 on_dl_chapters_button_clicked')

        self.dl_mode = 'chapters'
        self.start_dl_flag = True
        self.close()


    def on_dl_marked_button_clicked(self, button):

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

        Downloads/creates marked clips.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37435 on_dl_marked_button_clicked')

        self.dl_mode = 'multiple'
        self.stamp_list = []

        if self.video_obj is not None:
            this_stamp_list = self.video_obj.stamp_list
        else:
            this_stamp_list = self.classic_stamp_list

        count = -1
        for path in range(0, len(self.timestamp_liststore)):

            count += 1
            if self.timestamp_liststore[path][0] is True:

                start_stamp, stop_stamp, clip_title \
                = utils.clip_extract_data(this_stamp_list, count)

                self.stamp_list.append([start_stamp, stop_stamp, clip_title])

        if self.stamp_list:
            self.start_dl_flag = True
            self.close()


    def on_dl_single_button_clicked(self, button, entry, entry2, entry3):

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

        Downloads/creates a single clip.

        Args:

            button (Gtk.Button): The widget clicked

            entry, entry2, entry3 (Gtk.Entry): Widgets specifying timestamps
                and clip titles

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37477 on_dl_single_button_clicked')

        self.dl_mode = 'single'

        # (First entry is compulsory, others are optional; but timestamps must
        #       be in a valid format where specified)
        start_stamp = entry.get_text()
        if start_stamp != '':
            self.start_stamp = utils.strip_whitespace(start_stamp)

        stop_stamp = entry2.get_text()
        if stop_stamp != '':
            self.stop_stamp = utils.strip_whitespace(stop_stamp)

        clip_title = entry3.get_text()
        if clip_title != '':
            self.clip_title = utils.strip_whitespace(clip_title)

        if self.start_stamp is not None:
            self.start_dl_flag = True
            self.close()


    def on_extract_stamp_button_clicked(self, button):

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

        Updates the video's timestamp list from its description, then displays
        that list in the treeview.

        Code adapted from
        config.VideoEditWin.on_extract_stamp_button_clicked().

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37517 on_extract_stamp_button_clicked')

        self.video_obj.extract_timestamps_from_descrip(
            self.main_win_obj.app_obj,
        )

        self.update_treeview()


    def on_optional_entry_changed(self, entry):

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

        Updates IVs.

        Args:

            entry (Gtk.Entry): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37539 on_optional_entry_changed')

        text = entry.get_text()
        if text == '':
            self.classic_video_name = None
        else:
            self.classic_video_name = text


    def on_prefs_button_clicked(self, button):

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

        Opens the preferences window to show clip settings.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37561 on_prefs_button_clicked')

        # N.B. Destroy this window before opening the preferences window, or
        #   the latter will be hidden behind the main window
        # N.B. If we don't destroy this window first, the preferences window is
        #   non-responsive
        self.destroy()
        config.SystemPrefWin(self.main_win_obj.app_obj, 'clips')


    def on_radiobutton_toggled(self, radiobutton):

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

        Updates widgets in the window, according to the current mode.

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37584 on_radiobutton_toggled')

        if radiobutton.get_active():
            self.clip_mode = 'downloader'

            self.button12.set_sensitive(True)

            self.label5.set_markup('<b>' + _('Download one clip') + '</b>:')
            self.set_bold_button_text(self.button, _('Download clip'))

            self.label9.set_markup(
                '<b>' + _('Download multiple clips') + '</b>:',
            )
            self.set_bold_button_text(
                self.button10,
                _('Download marked clips'),
            )
            self.set_bold_button_text(self.button11, _('Download all clips'))


    def on_radiobutton2_toggled(self, radiobutton):

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

        Updates widgets in the window, according to the current mode.

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37617 on_radiobutton2_toggled')

        if radiobutton.get_active():
            self.clip_mode = 'ffmpeg'

            self.button12.set_sensitive(False)

            self.label5.set_markup('<b>' + _('Download one clip') + '</b>:')
            self.set_bold_button_text(self.button, _('Download clip'))
            self.label9.set_markup(
                '<b>' + _('Download multiple clips') + '</b>:',
            )
            self.set_bold_button_text(
                self.button10,
                _('Download marked clips'),
            )
            self.set_bold_button_text(self.button11, _('Download all clips'))


    def on_radiobutton3_toggled(self, radiobutton):

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

        Updates widgets in the window, according to the current mode.

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37649 on_radiobutton3_toggled')

        if radiobutton.get_active():
            self.clip_mode = 'create'
            self.dl_mode = None

            self.button12.set_sensitive(False)

            self.label5.set_markup('<b>' + _('Create one clip') + '</b>:')
            self.set_bold_button_text(self.button, _('Create clip'))
            self.label9.set_markup(
                '<b>' + _('Create multiple clips') + '</b>:',
            )
            self.set_bold_button_text(self.button10, _('Create marked clips'))
            self.set_bold_button_text(self.button11, _('Create all clips'))


    def on_select_all_button_clicked(self, button):

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

        Selects the checkbutton in every line in the treeview.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37679 on_select_all_button_clicked')

        for path in range(0, len(self.timestamp_liststore)):
            self.timestamp_liststore[path][0] = True


    def on_select_none_button_clicked(self, button):

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

        De-selects the checkbutton in every line in the treeview.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37698 on_select_none_button_clicked')

        for path in range(0, len(self.timestamp_liststore)):
            self.timestamp_liststore[path][0] = False


    def on_start_entry_changed(self, entry):

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

        Sensitises the download button, depending on whether the entry contains
        text, or not.

        Note that the validity of any timestamps the user specifies is checked
        by the calling mainwin.MainWin code.

        Args:

            entry (Gtk.Entry): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37721 on_start_entry_changed')

        text = entry.get_text()
        if text == '':
            self.button.set_sensitive(False)
        else:
            self.button.set_sensitive(True)


    def on_treeview_button_toggled(self, renderer_toggle, tree_path):

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

        Toggles one of the check buttons in the treeview.

        Args:

            renderer_toggle (Gtk.CellRendererToggle): The widget clicked

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

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37745 on_treeview_button_toggled')

        self.timestamp_liststore[tree_path][0] \
        = not self.timestamp_liststore[tree_path][0]


    def on_url_entry_changed(self, entry):

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

        Updates IVs.

        Args:

            entry (Gtk.Entry): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37764 on_url_entry_changed')

        text = entry.get_text()
        if text == '':
            self.classic_url = None
        else:
            self.classic_url = text


class PrepareSliceDialogue(Gtk.Dialog):

    """Called by mainwin.MainWin.on_video_catalogue_process_clip().

    Prompt the user to remove video slices.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        video_obj (media.Video): The video from which video slices will be
            removed

    """


    # Standard class methods


    def __init__(self, main_win_obj, video_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 37795 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Remove video slices\' dialogue starts' \
            + ' here. In the Videos tab, right-click a video and select' \
            + ' Special > Remove video slices...'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj
        # The media.Video to be used
        self.video_obj = video_obj


        # IV list - Gtk widgets
        # ---------------------
        self.radiobutton = None                     # Gtk.RadioButton
        self.radiobutton2 = None                    # Gtk.RadioButton
        self.label3 = None                          # Gtk.Label
        self.button = None                          # Gtk.Button
        self.label6 = None                          # Gtk.Label
        self.slice_liststore = None                 # Gtk.ListStore
        self.button9 = None                         # Gtk.Button
        self.button10 = None                        # Gtk.Button


        # IV list - other
        # ---------------
        # Store the user's choice as an IV, so the calling function can
        #   retrieve it
        # Slice mode: 'ffmpeg' to remove slices using FFmpeg, or 'create' to
        #   remove slices using the already-downloaded video
        # (These values match those used in mainwin.PrepareClipDialogue)
        self.slice_mode = 'ffmpeg'
        # Download mode: 'single' to remove a single slice using specified
        #   timestamps/seconds, 'multiple' to remove multiple slices. Set to
        #   None when self.clip_mode is 'create'
        # (These values match those used in mainwin.PrepareClipDialogue)
        self.dl_mode = None
        # When self.dl_mode is 'single', the start/stop times (timestamps or
        #   seconds). The start time is compulsory, the stop time is optional,
        #   but timestamps must be in a valid format where specified)
        self.start_time = None
        self.stop_time = None
        # When self.dl_mode is 'multiple', a list of dictionaries in the form
        #   described by self.on_add_slice_button_clicked(), each representing
        #   a single slice. The list is used to set
        #   mainapp.TartubeApp.temp_slice_buffer_dict
        self.slice_list = []

        # Flag set to true when one of the 'Download' buttons is clicked,
        #   which closes the window (in the absence of an OK button)
        self.start_dl_flag = False


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Remove video slices'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
            )
        )

        self.set_modal(True)
        app_obj = self.main_win_obj.app_obj

        # Set up the dialogue window
        self.set_default_size(
            app_obj.config_win_width,
            -1,
        )
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)

        grid_width = 2

        label = Gtk.Label()
        grid.attach(label, 0, 0, grid_width, 1)
        label.set_markup('<b>' + _('Select a method:') + '</b>')
        label.set_alignment(0, 0.5)

        self.radiobutton = Gtk.RadioButton.new_with_label_from_widget(
            None,
            _('Download new sliced video'),
        )
        grid.attach(self.radiobutton, 0, 1, 1, 1)

        self.radiobutton2 = Gtk.RadioButton.new_from_widget(self.radiobutton)
        grid.attach(self.radiobutton2, 1, 1, 1, 1)
        # (Signal connect appears below)
        self.radiobutton2.set_label(
            _('Remove slices from the downloaded video'),
        )
        if not self.video_obj.dl_flag:
            self.radiobutton2.set_sensitive(False)

        else:
            label2 = Gtk.Label()
            grid.attach(label2, 0, 2, grid_width, 1)
            label2.set_markup(
                '<i>' + _(
                    'Warning: Downloading new sliced videos is usually MUCH' \
                    + ' quicker!',
                ) + '</i>',
            )
            label2.set_alignment(0, 0.5)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 3, grid_width, 1)

        # (Additional grid, to avoid messing up the format of the previous one)
        grid2 = Gtk.Grid()
        grid.attach(grid2, 0, 4, grid_width, 1)
        grid2.set_column_spacing(main_win_obj.spacing_size)
        grid2.set_row_spacing(main_win_obj.spacing_size)

        grid2_width = 4

        self.label3 = Gtk.Label()
        grid2.attach(self.label3, 0, 0, grid2_width, 1)
        if self.slice_mode == 'create':
            self.label3.set_markup(
                '<b>' + _('Create video with one slice') + '</b>:(',
            )
        else:
            self.label3.set_markup(
                '<b>' + _('Download video with one slice') + '</b>:',
            )
        self.label3.set_alignment(0, 0.5)

        label4 = Gtk.Label()
        grid2.attach(label4, 0, 1, 1, 1)
        label4.set_markup(_('Start (timestamp or seconds)'))
        label4.set_alignment(0, 0.5)

        entry_width = 12

        entry = Gtk.Entry()
        grid2.attach(entry, 1, 1, 1, 1)
        # (Signal connect appears below)
        entry.set_width_chars(entry_width)

        label5 = Gtk.Label()
        grid2.attach(label5, 2, 1, 1, 1)
        label5.set_markup(_('Stop (optional)'))
        label5.set_alignment(0, 0.5)

        entry2 = Gtk.Entry()
        grid2.attach(entry2, 3, 1, 1, 1)
        entry2.set_width_chars(entry_width)

        self.button = Gtk.Button.new_with_label('')
        grid2.attach(self.button, 2, 2, 2, 1)
        # (Signal connect appears below)
        if self.slice_mode == 'create':
            self.set_bold_button_text(self.button, _('Create sliced video'))
        else:
            self.set_bold_button_text(self.button, _('Download sliced video'))
        self.button.set_hexpand(True)
        self.button.set_sensitive(False)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 5, grid_width, 1)

        self.label6 = Gtk.Label()
        grid.attach(self.label6, 0, 6, grid_width, 1)
        if self.slice_mode == 'create':
            self.label6.set_markup(
                '<b>' + _('Create video with multiple slices') + '</b>:',
            )
        else:
            self.label6.set_markup(
                '<b>' + _('Download video with multiple slices') + '</b>:',
            )
        self.label6.set_alignment(0, 0.5)

        frame = Gtk.Frame()
        grid.attach(frame, 0, 7, grid_width, 1)

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

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

        for i, column_title in enumerate([
            _('Marked'),
            _('Category'),
            _('Action type'),
            _('Start'),
            _('Stop'),
        ]):
            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.slice_liststore = Gtk.ListStore(bool, str, str, str, str)
        treeview.set_model(self.slice_liststore)

        # Initialise the list
        self.update_treeview()

        # (Additional grid, to avoid messing up the format of the previous one)
        grid3 = Gtk.Grid()
        grid.attach(grid3, 0, 8, grid_width, 1)
        grid3.set_column_spacing(main_win_obj.spacing_size)
        grid3.set_row_spacing(main_win_obj.spacing_size)

        grid3_width = 4

        label7 = Gtk.Label()
        grid3.attach(label7, 0, 0, 1, 1)
        label7.set_markup(_('Category'))
        label7.set_alignment(0, 0.5)

        combostore = Gtk.ListStore(str)
        for category in formats.SPONSORBLOCK_CATEGORY_LIST:
            combostore.append( [category] )

        combo = Gtk.ComboBox.new_with_model(combostore)
        grid3.attach(combo, 1, 0, 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(0)

        label8 = Gtk.Label()
        grid3.attach(label8, 2, 0, 1, 1)
        label8.set_markup(_('Action type'))
        label8.set_alignment(0, 0.5)

        combostore2 = Gtk.ListStore(str)
        for action in formats.SPONSORBLOCK_ACTION_LIST:
            combostore2.append( [action] )

        combo2 = Gtk.ComboBox.new_with_model(combostore2)
        grid3.attach(combo2, 3, 0, 1, 1)
        combo2.set_hexpand(True)

        renderer_text = Gtk.CellRendererText()
        combo2.pack_start(renderer_text, True)
        combo2.add_attribute(renderer_text, 'text', 0)
        combo2.set_active(0)

        label9 = Gtk.Label()
        grid3.attach(label9, 0, 1, 1, 1)
        label9.set_markup(_('Start (timestamp or seconds)'))
        label9.set_alignment(0, 0.5)

        entry3 = Gtk.Entry()
        grid3.attach(entry3, 1, 1, 1, 1)
        # (Signal connect appears below)
        entry3.set_width_chars(entry_width)

        label10 = Gtk.Label()
        grid3.attach(label10, 2, 1, 1, 1)
        label10.set_markup(_('Stop (optional)'))
        label10.set_alignment(0, 0.5)

        entry4 = Gtk.Entry()
        grid3.attach(entry4, 3, 1, 1, 1)
        # (Signal connect appears below)
        entry4.set_width_chars(entry_width)

        # (Additional grid, to avoid messing up the format of the previous one)
        grid4 = Gtk.Grid()
        grid.attach(grid4, 0, 9, grid_width, 1)
        grid4.set_column_spacing(main_win_obj.spacing_size)
        grid4.set_row_spacing(main_win_obj.spacing_size)

        grid4_width = 4

        button2 = Gtk.Button.new_with_label(_('Add slice'))
        grid4.attach(button2, 0, 0, 1, 1)
        # (Signal connect appears below)
        button2.set_hexpand(True)

        button3 = Gtk.Button.new_with_label(_('Delete slice'))
        grid4.attach(button3, 1, 0, 1, 1)
        # (Signal connect appears below)
        button3.set_hexpand(True)

        button4 = Gtk.Button.new_with_label(_('SponsorBlock settings'))
        grid4.attach(button4, 2, 0, 1, 1)
        # (Signal connect appears below)
        button4.set_hexpand(True)

        button5 = Gtk.Button.new_with_label(_('Clear list'))
        grid4.attach(button5, 3, 0, 1, 1)
        # (Signal connect appears below)
        button5.set_hexpand(True)

        # (Additional grid, to avoid messing up the format of the previous one)
        grid5 = Gtk.Grid()
        grid.attach(grid5, 0, 10, grid_width, 1)
        grid5.set_column_spacing(main_win_obj.spacing_size)
        grid5.set_row_spacing(main_win_obj.spacing_size)

        grid5_width = 4

        button6 = Gtk.Button.new_with_label(_('Select all'))
        grid5.attach(button6, 0, 0, 1, 1)
        # (Signal connect appears below)
        button6.set_hexpand(True)

        button7 = Gtk.Button.new_with_label(_('Select none'))
        grid5.attach(button7, 1, 0, 1, 1)
        # (Signal connect appears below)
        button7.set_hexpand(True)

        button8 = Gtk.Button.new_with_label(
            _('Contact SponsorBlock to reset list'),
        )
        grid5.attach(button8, 2, 0, 2, 1)
        # (Signal connect appears below)
        button8.set_hexpand(True)
        if app_obj.custom_sblock_mirror == '' or self.video_obj.vid is None:
            button8.set_sensitive(False)

        self.button9 = Gtk.Button.new_with_label('')
        grid5.attach(self.button9, 0, 1, 2, 1)
        # (Signal connect appears below)
        if self.slice_mode == 'create':
            self.set_bold_button_text(
                self.button9,
                _('Create video, removing marked slices'),
            )
        else:
            self.set_bold_button_text(
                self.button9,
                _('Download video, removing marked slices'),
            )
        self.button9.set_hexpand(True)

        self.button10 = Gtk.Button.new_with_label('')
        grid5.attach(self.button10, 2, 1, 1, 1)
        # (Signal connect appears below)
        if self.slice_mode == 'create':
            self.set_bold_button_text(
                self.button10,
                _('Create video, removing all slices'),
            )
        else:
            self.set_bold_button_text(
                self.button10,
                _('Download video, removing all slices'),
            )
        self.button10.set_hexpand(True)

        # (Signal connects from above)
        entry.connect('changed', self.on_start_entry_changed)
        self.button.connect(
            'clicked',
            self.on_dl_single_button_clicked,
            entry,
            entry2,
        )

        self.radiobutton.connect('toggled', self.on_radiobutton_toggled)
        self.radiobutton2.connect('toggled', self.on_radiobutton2_toggled)

        button2.connect(
            'clicked',
            self.on_add_slice_button_clicked,
            combo,
            combo2,
            entry3,
            entry4,
        )
        button3.connect(
            'clicked',
            self.on_delete_slice_button_clicked,
            treeview,
        )
        button4.connect('clicked', self.on_prefs_button_clicked)
        button5.connect('clicked', self.on_clear_slice_button_clicked)
        button6.connect('clicked', self.on_select_all_button_clicked)
        button7.connect('clicked', self.on_select_none_button_clicked)
        button8.connect('clicked', self.on_contact_sblock_clicked)

        self.button9.connect('clicked', self.on_dl_marked_button_clicked)
        self.button10.connect('clicked', self.on_dl_all_button_clicked)

        # Display the dialogue window
        self.show_all()


    # Support functions


    def set_bold_button_text(self, button, text):

        """Can be called by anything.

        Sets bold label text for a Gtk.Button.

        Args:

            button (Gtk.Button): The button to update

            text (str): The (translated) text, which does not need to contain
                <b>...</b> tags

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 38240 set_bold_button_text')

        for child in button.get_children():
            child.set_label('<b>' + text + '</b>')
            child.set_use_markup(True)


    def update_treeview(self):

        """Called by self.__init__().

        Fills or updates the treeview. Based on
        config.VideoEditWin.setup_slices_tab_update_treeview().
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 28256 update_treeview')

        self.slice_liststore.clear()

        # Add each timestamp/title to the treeview, one row at a time
        for mini_dict in self.video_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(
                [ False, category, action, str(start_time), str(stop_time) ],
            )


    # Callback class methods


    def on_add_slice_button_clicked(self, button, combo, combo2, entry,
    entry2):

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

        Adds a new slice to the treeview.

        Code adapted from config.VideoEditWin.on_add_slice_button_clicked().

        Args:

            button (Gtk.Button): The widget clicked

            combo, combo2 (Gtk.Entry): Other widgets to modify

            entry, entry2 (Gtk.Entry): Other widgets to modify

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 38312 on_add_slice_button_clicked')

        app_obj = self.main_win_obj.app_obj

        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 = utils.strip_whitespace(entry.get_text())
        stop_time = utils.strip_whitespace(entry2.get_text())

        start_time = float(
            utils.timestamp_convert_to_seconds(app_obj, start_time),
        )

        if stop_time == '':
            stop_time = None
        else:
            stop_time = float(
                utils.timestamp_convert_to_seconds(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.video_obj.slice_list
        slice_list.append(mini_dict)

        # (The called function will sort the list)
        self.video_obj.set_slices(slice_list)

        # Show changes, and empty entry boxes
        self.update_treeview()
        entry.set_text('')
        entry.set_text('')


    def on_clear_slice_button_clicked(self, button):

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

        Empties the video's slice list.

        Code adapted from config.VideoEditWin.on_clear_slice_button_clicked().

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 38401 on_clear_slice_button_clicked')

        self.video_obj.reset_slices()
        self.update_treeview()


    def on_contact_sblock_clicked(self, button):

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

        Contacts SponsorBlock to reset the video's slice list.

        Args:

            button (Gtk.Button): The widget clicked

            entry, entry2, (Gtk.Entry): Widgets specifying timestamps/seconds

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 38422 on_contact_sblock_clicked')

        utils.fetch_slice_data(
            self.main_win_obj.app_obj,
            self.video_obj,
        )

        self.update_treeview()


    def on_delete_slice_button_clicked(self, button, treeview):

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

        Deletes the selected slice(s) from the video's slice list.

        Code adapted from config.VideoEditWin.on_delete_slice_button_clicked().

        Args:

            button (Gtk.Button): The widget clicked

            treeview (Gtk.TreeVies): The treeview displaying the timestamp list

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 38449 on_delete_slice_button_clicked')

        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][1]
        action_type = model[this_iter][2]
        start_time = float(model[this_iter][3])
        stop_time = float(model[this_iter][4])

        # 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.video_obj.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.video_obj.set_slices(mod_list)

        # (Show changes)
        self.update_treeview()


    def on_dl_all_button_clicked(self, button):

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

        Downloads/creates all clips.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 38504 on_dl_all_button_clicked')

        self.dl_mode = 'multiple'
        self.slice_list = self.video_obj.slice_list.copy()

        if self.slice_list:
            self.start_dl_flag = True
            self.close()


    def on_dl_marked_button_clicked(self, button):

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

        Downloads/creates video with marked slice(s) removed.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 38527 on_dl_marked_button_clicked')

        self.dl_mode = 'multiple'
        self.slice_list = []

        count = -1
        for path in range(0, len(self.slice_liststore)):

            count += 1
            if self.slice_liststore[path][0] is True:

                self.slice_list.append(self.video_obj.slice_list[count])

        if self.slice_list:
            self.start_dl_flag = True
            self.close()


    def on_dl_single_button_clicked(self, button, entry, entry2):

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

        Downloads/creates a sliced video.

        Args:

            button (Gtk.Button): The widget clicked

            entry, entry2, (Gtk.Entry): Widgets specifying timestamps/seconds

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 38560 on_dl_single_button_clicked')

        self.dl_mode = 'single'

        # First entry is compulsory, second is optional
        # Values, if specified, can be either a timestamp, or a value in
        #   seconds
        start_time = entry.get_text()
        if start_time != '':
            self.start_time = utils.strip_whitespace(start_time)

        stop_time = entry2.get_text()
        if stop_time != '':
            self.stop_time = utils.strip_whitespace(stop_time)

        if self.start_time is not None:
            self.start_dl_flag = True
            self.close()


    def on_prefs_button_clicked(self, button):

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

        Opens the preferences window to show SponsorBlock settings.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 38593 on_prefs_button_clicked')

        # N.B. Destroy this window before opening the preferences window, or
        #   the latter will be hidden behind the main window
        # N.B. If we don't destroy this window first, the preferences window is
        #   non-responsive
        self.destroy()
        config.SystemPrefWin(self.main_win_obj.app_obj, 'slices')


    def on_radiobutton_toggled(self, radiobutton):

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

        Updates widgets in the window, according to the current mode.

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 38616 on_radiobutton_toggled')

        if radiobutton.get_active():
            self.slice_mode = 'ffmpeg'

            self.label3.set_markup(
                '<b>' + _('Download video with one slice') + '</b>:'
            )
            self.set_bold_button_text(self.button, _('Download sliced video'))
            self.label6.set_markup(
                '<b>' + _('Download video with multiple slices') + '</b>:',
            )
            self.set_bold_button_text(
                self.button9,
                _('Download video, removing marked slices'),
            )
            self.set_bold_button_text(
                self.button10,
                _('Download video, removing all slices'),
            )


    def on_radiobutton2_toggled(self, radiobutton):

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

        Updates widgets in the window, according to the current mode.

        Args:

            radiobutton (Gtk.RadioButton): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 38651 on_radiobutton2_toggled')

        if radiobutton.get_active():
            self.slice_mode = 'create'
            self.dl_mode = None

            self.label3.set_markup(
                '<b>' + _('Create video with one slice') + '</b>:'
            )
            self.set_bold_button_text(self.button, _('Create sliced video'))
            self.label6.set_markup(
                '<b>' + _('Create video with multiple slices') + '</b>:',
            )
            self.set_bold_button_text(
                self.button9,
                _('Create video, removing marked slices'),
            )
            self.set_bold_button_text(
                self.button10,
                _('Create video, removing all slices'),
            )


    def on_select_all_button_clicked(self, button):

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

        Selects the checkbutton in every line in the treeview.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 38687 on_select_all_button_clicked')

        for path in range(0, len(self.slice_liststore)):
            self.slice_liststore[path][0] = True


    def on_select_none_button_clicked(self, button):

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

        De-selects the checkbutton in every line in the treeview.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 38706 on_select_none_button_clicked')

        for path in range(0, len(self.slice_liststore)):
            self.slice_liststore[path][0] = False


    def on_start_entry_changed(self, entry):

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

        Sensitises the download button, depending on whether the entry contains
        text, or not.

        Note that the validity of any timestamps the user specifies is checked
        by the calling mainwin.MainWin code.

        Args:

            entry (Gtk.Entry): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 38729 on_start_entry_changed')

        text = entry.get_text()
        if text == '':
            self.button.set_sensitive(False)
        else:
            self.button.set_sensitive(True)


    def on_treeview_button_toggled(self, renderer_toggle, tree_path):

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

        Toggles one of the check buttons in the treeview.

        Args:

            renderer_toggle (Gtk.CellRendererToggle): The widget clicked

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

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 38753 on_treeview_button_toggled')

        self.slice_liststore[tree_path][0] \
        = not self.slice_liststore[tree_path][0]


class RecentVideosDialogue(Gtk.Dialog):

    """Called by mainwin.MainWin.on_video_index_recent_videos_time().

    Python class handling a dialogue window that prompts the user to set the
    time after which videos are removed from the fixed 'Recent Videos' folder.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        media_data_obj (media.Folder): The 'Recent Videos' folder

    """


    # Standard class methods


    def __init__(self, main_win_obj, media_data_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 38781 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Set removal time\' dialogue starts here.' \
            + ' In the Videos tab, right-click the \'Recent Videos\'' \
            + ' folder and select Downloads > Set removal time...'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.radiobutton = None                 # Gtk.RadioButton
        self.radiobutton2 = None                # Gtk.RadioButton
        self.spinbutton = None                  # Gtk.SpinButton


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Set removal time'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)

        label = Gtk.Label()
        grid.attach(label, 0, 0, 2, 1)
        label.set_markup(
            _(
            'When videos are checked/downloaded, older videos\nare removed' \
            + ' from the Recent Videos folder.',
            ),
        )
        label.set_alignment(0, 0.5)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 1, 2, 1)

        self.radiobutton = Gtk.RadioButton.new_with_label_from_widget(
            None,
            _('Empty the whole folder'),
        )
        grid.attach(self.radiobutton, 0, 2, 2, 1)
        # (Signal connect appears below)

        self.radiobutton2 = Gtk.RadioButton.new_with_label_from_widget(
            self.radiobutton,
            _('Remove videos after days'),
        )
        grid.attach(self.radiobutton2, 0, 3, 1, 1)

        self.spinbutton = Gtk.SpinButton.new_with_range(1, 14, 1)
        grid.attach(self.spinbutton, 1, 3, 1, 1)
        self.spinbutton.set_hexpand(False)

        if not main_win_obj.app_obj.fixed_recent_folder_days:
            self.spinbutton.set_sensitive(False)
        else:
            self.radiobutton2.set_active(True)
            self.spinbutton.set_value(
                main_win_obj.app_obj.fixed_recent_folder_days,
            )

        # (Signal connects from above)
        self.radiobutton.connect(
            'toggled',
            self.on_radiobutton_toggled,
        )

        # Display the dialogue window
        self.show_all()


    # Callback class methods


    def on_radiobutton_toggled(self, radiobutton):

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

        (De)sensitises the spinbutton, depending on which radiobutton is
        selected.

        Args:

            radiobutton (Gtk.RadioButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 38891 on_radiobutton_toggled')

        if radiobutton.get_active():
            self.spinbutton.set_sensitive(False)
        else:
            self.spinbutton.set_sensitive(True)


class RemoveLockFileDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.load_db().

    Python class handling a dialogue window that asks the user what to do,
    if the database file can't be loaded because it's protected by a lockfile.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        switch_flag (bool): False when Tartube starts; True when a database
            had already been loaded, and the user is trying to switch to a
            different one

    """


    # Standard class methods


    def __init__(self, main_win_obj, switch_flag):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 38923 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Stale lockfile\' dialogue starts here.' \
            + ' Visible on startup if the Tartube database file is protected' \
            + ' bu a lockfile'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - other
        # ---------------
        # Flag set to True if the lockfile should be removed
        self.remove_flag = False


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Stale lockfile'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
        )

        self.set_modal(True)

        # Set up the dialogue window
        spacing_size = self.main_win_obj.spacing_size
        label_length = self.main_win_obj.long_string_max_len

        box = self.get_content_area()

        # Tartube logo on the left, widgets on the right
        hbox = Gtk.HBox()
        box.add(hbox)

        # Logo in the top corner
        vbox = Gtk.VBox()
        hbox.pack_start(vbox, False, False, spacing_size)

        image = Gtk.Image.new_from_pixbuf(
            main_win_obj.pixbuf_dict['system_icon'],
        )
        vbox.pack_start(image, False, False, spacing_size)

        grid = Gtk.Grid()
        hbox.pack_start(grid, False, False, spacing_size)
        grid.set_border_width(spacing_size)
        grid.set_row_spacing(spacing_size)
        # (Actually, the grid width of the area to the right of the Tartube
        #   logo)
        grid_width = 2

        label = Gtk.Label(
            utils.tidy_up_long_string(
                _(
                'Failed to load the Tartube database file, because another' \
                + ' copy of Tartube seems to be using it',
                ),
                label_length,
            ) + '\n\n' \
            + utils.tidy_up_long_string(
                _(
                'Do you want to load it anyway?',
                ),
                label_length,
            ) + '\n\n' \
            + utils.tidy_up_long_string(
                _(
                '(Only click \'Yes\' if you are sure that other copies of' \
                + ' Tartube are not using the database right now)',
                ),
                label_length,
            )
        )
        grid.attach(label, 1, 0, grid_width, 1)

        # Separator
        grid.attach(Gtk.HSeparator(), 1, 1, grid_width, 1)

        button = Gtk.Button.new_with_label(
            _('Yes, load the file'),
        )
        grid.attach(button, 1, 2, 1, 1)
        button.set_hexpand(True)
        button.connect('clicked', self.on_yes_button_clicked)

        if not switch_flag:
            msg = _('No, just shut down Tartube')
        else:
            msg = _('No, don\'t load the file')

        button2 = Gtk.Button.new_with_label(msg)
        grid.attach(button2, 1, 3, 1, 1)
        button2.set_hexpand(True)
        button2.connect('clicked', self.on_no_button_clicked)

        # Display the dialogue window
        self.show_all()


    # Callback class methods


    def on_yes_button_clicked(self, button):

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

        When the Yes button is clicked, set a flag for the calling function to
        check, the close the window.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 39047 on_yes_button_clicked')

        self.remove_flag = True
        self.destroy()


    def on_no_button_clicked(self, button):

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

        When the No button is clicked, set a flag for the calling function to
        check, the close the window.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 39067 on_no_button_clicked')

        self.remove_flag = False
        self.destroy()


class RenameContainerDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.rename_container().

    Python class handling a dialogue window that prompts the user to rename
    a channel, playlist or folder.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        media_data_obj (media.Channel, media.Playlist, media.Folder): The media
            data object whose name is to be changed

    """


    # Standard class methods


    def __init__(self, main_win_obj, media_data_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 39096 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Rename container\' dialogue starts here.' \
            + ' In the Videos tab, right-click a channel and select' \
            + ' Channel actions > Rename channel...'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.entry = None                       # Gtk.Entry


        # Code
        # ----

        media_type = media_data_obj.get_type()
        if media_type == 'channel':
            string = _('Rename channel')
        elif media_type == 'playlist':
            string = _('Rename playlist')
        else:
            string = _('Rename folder')

        Gtk.Dialog.__init__(
            self,
            string,
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)

        if media_type == 'channel':
            string = _('Set the new name for the channel:')
        elif media_type == 'playlist':
            string = _('Set the new name for the playlist:')
        else:
            string = _('Set the new name for the folder:')

        label = Gtk.Label()
        grid.attach(label, 0, 0, 1, 1)
        label.set_markup(
            string + '\n\n<b>' + media_data_obj.name + '</b>\n\n' + _(
                'N.B. This procedure will modify your filesystem!\n',
            )
        )

        # (Store various widgets as IVs, so the calling function can retrieve
        #   their contents)
        self.entry = Gtk.Entry()
        grid.attach(self.entry, 0, 1, 1, 1)
        self.entry.set_text(media_data_obj.name)

        # Display the dialogue window
        self.show_all()


class ResetContainerDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.on_menu_reset_container().

    Python class handling a dialogue window to allow the user to reset channel/
    playlist names in Tartube's database, replacing them with names gathered
    from their child video's metadata (i.e. the original channel/playlist names
    used on the site from which the videos were downloaded).

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

    """


    # Standard class methods


    def __init__(self, main_win_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 39194 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Create profile\' dialogue starts here.' \
            + ' In the main window menu, click Media > Reset channel/' \
            + '/playlist names...'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.treeview = None                    # Gtk.TreeView
        self.liststore = None                   # Gtk.TreeView


        # IV list - other
        # ---------------
        # Dictionary of media.Channel and media.Playlist objects whose names
        #   should be reset
        # Dictionary in the form
        #   key: .dbid
        #   value: True to reset, False to not reset
        self.reset_dict = {}

        # Dictionary of media.Channel and media.Playlist objects whose names
        #   should be reset, but not to the original website name, but to a
        #   name the user has typed
        # Dictionary in the form
        #   key: .dbid (a subset of those in self.reset_dict)
        #   value: The new typed name
        self.custom_dict = {}


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Reset channel/playlist names'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)
        self.set_default_size(
            main_win_obj.app_obj.config_win_width,
            main_win_obj.app_obj.config_win_height,
        )

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)
        grid_width = 3

        label = Gtk.Label(
            _(
                'This list is updated whenever channels/playlists are' \
                + ' checked/downloaded',
            ),
        )
        grid.attach(label, 0, 0, grid_width, 1)

        label2 = Gtk.Label(
            _(
                'Select which names should be reset to the names on the' \
                + ' original website',
            ),
        )
        grid.attach(label2, 0, 1, grid_width, 1)

        scrolled = Gtk.ScrolledWindow()
        grid.attach(scrolled, 0, 2, grid_width, 1)
        scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
        scrolled.set_hexpand(True)
        scrolled.set_vexpand(True)

        frame = Gtk.Frame()
        scrolled.add_with_viewport(frame)

        # (Store various widgets as IVs, so the calling function can retrieve
        #   their contents)
        self.treeview = Gtk.TreeView()
        frame.add(self.treeview)
        self.treeview.set_can_focus(False)

        renderer_toggle = Gtk.CellRendererToggle()
        renderer_toggle.connect('toggled', self.on_checkbutton_toggled)
        column_toggle = Gtk.TreeViewColumn(
            _('Reset'),
            renderer_toggle,
            active=0,
        )
        self.treeview.append_column(column_toggle)

        renderer_pixbuf = Gtk.CellRendererPixbuf()
        column_pixbuf = Gtk.TreeViewColumn(
            _('Type'),
            renderer_pixbuf,
            pixbuf=1,
        )
        self.treeview.append_column(column_pixbuf)

        renderer_text = Gtk.CellRendererText()
        column_text = Gtk.TreeViewColumn(
            _('Database name'),
            renderer_text,
            text=2,
        )
        self.treeview.append_column(column_text)

        renderer_text2 = Gtk.CellRendererText()
        column_text2 = Gtk.TreeViewColumn(
            _('Original name'),
            renderer_text2,
            text=3,
        )
        self.treeview.append_column(column_text2)
        renderer_text2.set_property('editable', True)
        renderer_text2.connect(
            'edited',
            self.on_original_name_edited,
        )

        renderer_text3 = Gtk.CellRendererText()
        column_text3 = Gtk.TreeViewColumn(
            'hide',
            renderer_text3,
            text=4,
        )
        column_text3.set_visible(False)
        self.treeview.append_column(column_text3)

        self.liststore = Gtk.ListStore(bool, GdkPixbuf.Pixbuf, str, str, int)
        self.treeview.set_model(self.liststore)

        # Get a sorted list of resettable channel/playlist names...
        app_obj = self.main_win_obj.app_obj
        sorted_list = []
        for dbid in app_obj.media_reset_container_dict.keys():
            sorted_list.append(app_obj.media_reg_dict[dbid])

        sorted_list.sort(key=lambda x: x.name)

        # ...and populate the treeview
        for media_data_obj in sorted_list:

            if isinstance(media_data_obj, media.Channel):
                pixbuf = main_win_obj.pixbuf_dict['channel_small']
            else:
                pixbuf = main_win_obj.pixbuf_dict['playlist_small']

            self.liststore.append([
                True,
                pixbuf,
                media_data_obj.name,
                app_obj.media_reset_container_dict[media_data_obj.dbid],
                media_data_obj.dbid,
            ])

            self.reset_dict[media_data_obj.dbid] = True

        # Strip of widgets at the bottom
        button = Gtk.Button.new_with_label(_('Select all'))
        grid.attach(button, 1, 3, 1, 1)
        button.set_hexpand(False)
        button.connect('clicked', self.on_select_all_clicked)

        button2 = Gtk.Button.new_with_label(_('Unselect all'))
        grid.attach(button2, 2, 3, 1, 1)
        button2.set_hexpand(False)
        button2.connect('clicked', self.on_deselect_all_clicked)

        # Display the dialogue window
        self.show_all()


    # Callback class methods


    def on_checkbutton_toggled(self, checkbutton, path):

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

        Respond when the user selects/deselects an item in the treeview.

        Args:

            checkbutton (Gtk.CheckButton): The widget clicked

            path (int): A number representing the widget's row

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 39401 on_checkbutton_toggled')

        # The user has clicked on the checkbutton widget, so toggle the widget
        #   itself
        self.liststore[path][0] = not self.liststore[path][0]

        # Update the data to be returned (eventually) to the calling
        #   mainapp.TartubeApp.import_into_db() function
        if not self.liststore[path][0]:
            self.reset_dict[self.liststore[path][4]] = False
        else:
            self.reset_dict[self.liststore[path][4]] = True


    def on_original_name_edited(self, widget, path, text):

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

        Replace the channel/playlist's name on the original website with a
        name that the user types.

        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

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 39433 on_original_name_edited')

        app_obj = self.main_win_obj.app_obj

        # Check the entered text is a valid name
        if text == '' \
        or re.search(r'^\s*$', text) \
        or not app_obj.check_container_name_is_legal(text):
            return

        # Check the container still exists
        container_dbid = self.liststore[path][4]
        if not container_dbid in app_obj.container_reg_dict:
            return

        # Check that the parent folder doesn't already have a container
        #   with the same name
        container_obj = app_obj.container_reg_dict[container_dbid]
        if app_obj.find_duplicate_name_in_container(
            container_obj.parent_obj,
            text,
        ):
            return

        # Check that the name is unique within this dialogue window
        for other_text in self.custom_dict.values():
            if other_text == text:
                return

        # Update the column text
        self.liststore[path][3] = text

        # Update the data to be returned (eventually) to the calling
        #   mainapp.TartubeApp.on_menu_reset_container() function
        self.custom_dict[container_dbid] = text


    def on_select_all_clicked(self, button):

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

        Mark all channels/playlists/folders to be reset.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 39483 on_select_all_clicked')

        for path in range(0, len(self.liststore)):
            self.liststore[path][0] = True
            self.reset_dict[self.liststore[path][4]] = True


    def on_deselect_all_clicked(self, button):

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

        Mark all channels/playlists/folders to be not reset.

        Args:

            button (Gtk.Button): The widget clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 39503 on_deselect_all_clicked')

        for path in range(0, len(self.liststore)):
            self.liststore[path][0] = False
            self.reset_dict[self.liststore[path][4]] = False


class ScheduledDialogue(Gtk.Dialog):

    """Called by MainWin.on_video_index_add_to_scheduled().

    Python class handling a dialogue window that prompts the user to choose a
    scheduled download. The specified channel/playlist/folder is added to the
    scheduled download selected by the user (if any).

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        media_data_obj (media.Channel, media.Playlist, media.Folder): The media
            data object to be added to a scheduled download

        available_list (list): List of names of media.Scheduled objects that
            don't already contain the specified media data object

    """


    # Standard class methods


    def __init__(self, main_win_obj, media_data_obj, available_list):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 39537 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Add to scheduled download\' dialogue' \
            + ' starts here. In the Videos tab, right-click a channel and' \
            + ' select Downloads > Add to scheduled download...'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - other
        # ---------------
        # Store the user's choice as an IV, so the calling function can
        #   retrieve it
        self.choice = None


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Add to scheduled download'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)

        # Set up the dialogue window
        spacing_size = self.main_win_obj.spacing_size

        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(spacing_size)
        grid.set_row_spacing(spacing_size)

        media_type = media_data_obj.get_type()
        if media_type == 'channel':
            string = _('Add the channel to this scheduled download:')
        elif media_type == 'playlist':
            string = _('Add the playlist to this scheduled download:')
        else:
            string = _('Add the folder to this scheduled download:')

        label = Gtk.Label(string)
        grid.attach(label, 0, 0, 1, 1)

        # Add a combo
        store = Gtk.ListStore(str)
        for name in available_list:
            store.append( [name] )

        combo = Gtk.ComboBox.new_with_model(store)
        grid.attach(combo, 0, 1, 1, 1)
        combo.set_hexpand(True)

        renderer_text = Gtk.CellRendererText()
        combo.pack_start(renderer_text, True)
        combo.add_attribute(renderer_text, 'text', 0)

        combo.connect('changed', self.on_combo_changed)
        combo.set_active(0)

        # Display the dialogue window
        self.show_all()


    # Callback class methods


    def on_combo_changed(self, combo):

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

        Store the combobox's selected item, so the calling function can
        retrieve it.

        Args:

            combo (Gtk.ComboBox): The clicked widget

            radiobutton2 (Gtk.RadioButton): Another widget to check

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 39634 on_combo_changed')

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


class SetDestinationDialogue(Gtk.Dialog):

    """Called by MainWin.on_video_index_set_destination().

    Python class handling a dialogue window that prompts the user to set the
    alternative download destination for a channel, playlist or folder.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        media_data_obj (media.Channel, media.Playlist, media.Folder): The media
            data object whose download destination is to be changed

    """


    # Standard class methods


    def __init__(self, main_win_obj, media_data_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 39664 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Set download destination\' dialogue' \
            + ' starts here. In the Videos tab, right-click a channel and' \
            + ' select Downloads > Set download destination...'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - other
        # ---------------
        # Store function arguments as IVs, so callback functions can retrieve
        #   them
        self.media_data_obj = media_data_obj
        # Store the user's choice as an IV, so the calling function can
        #   retrieve it
        # The two values can be distinguished, because .external_dir is always
        #   a string, and .master_dbid is always an integer
        if media_data_obj.external_dir:
            self.choice = media_data_obj.external_dir
        else:
            self.choice = media_data_obj.master_dbid


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Set download destination'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)

        # Set up the dialogue window
        spacing_size = self.main_win_obj.spacing_size
        label_length = self.main_win_obj.very_long_string_max_len

        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(spacing_size)
        grid.set_row_spacing(spacing_size)

        grid_width = 3

        # If the alternative download destination selected by this window, the
        #   last time it was opened, has since been deleted, then reset the IV
        #   that stores it
        app_obj = main_win_obj.app_obj
        prev_dbid = main_win_obj.previous_alt_dest_dbid
        if prev_dbid is not None and not prev_dbid in app_obj.media_reg_dict:
            prev_dbid = None
            main_win_obj.set_previous_alt_dest_dbid(None)

        # Likewise, if the previous external directory no longer exists, then
        #   reset the IV that stores it
        prev_external_dir = main_win_obj.previous_external_dir
        if prev_external_dir is not None \
        and not os.path.isdir(prev_external_dir):
            prev_external_dir = None
            main_win_obj.set_previous_external_dir(None)

        # Add widgets
        media_type = media_data_obj.get_type()
        if os.name == 'nt':
            if media_type == 'channel':
                msg = _(
                    'This channel normally downloads videos into its own' \
                    + ' folder',
                )
            elif media_type == 'playlist':
                msg = _(
                    'This playlist normally downloads videos into its own' \
                    + ' folder',
                )
            else:
                msg = _(
                    'This folder normally downloads videos into itself',
                )

        else:
            if media_type == 'channel':
                msg = _(
                    'This channel normally downloads videos into its own' \
                    + ' directory',
                )
            elif media_type == 'playlist':
                msg = _(
                    'This playlist normally downloads videos into its own' \
                    + ' directory',
                )
            else:
                msg = _(
                    'This folder normally downloads videos into its own' \
                    + ' directory',
                )

        label = Gtk.Label(utils.tidy_up_long_string(msg, label_length))
        grid.attach(label, 0, 0, grid_width, 1)
        label.set_xalign(0)

        radiobutton = Gtk.RadioButton.new_with_label_from_widget(
            None,
            _('Use this location'),
        )
        grid.attach(radiobutton, 0, 1, grid_width, 1)
        # (Signal connect appears below)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 2, grid_width, 1)

        msg = _('Choose a different location if:') \
        + '\n\n • ' + utils.tidy_up_long_string(
            _(
                'You want to add a channel and its playlists, without' \
                + ' downloading the same video twice',
            ),
            label_length,
        ) + '\n\n • ' + utils.tidy_up_long_string(
            _(
                'A video creator has channels on both YouTube and' \
                + ' BitChute, and you want to add both without' \
                + ' downloading the same video twice',
            ),
            label_length,
        )

        label2 = Gtk.Label(msg)
        grid.attach(label2, 0, 3, grid_width, 1)
        label2.set_xalign(0)

        radiobutton2 = Gtk.RadioButton.new_from_widget(radiobutton)
        grid.attach(radiobutton2, 0, 4, 1, 1)
        radiobutton2.set_label('Use a different location:')
        radiobutton2.set_hexpand(False)
        # (Signal connect appears below)

        # Get a list of channels/playlists/folders
        dbid_list = list(app_obj.container_reg_dict.keys())

        # From this list, filter out:
        #   - Any channel/playlist/folder which has an alternative download
        #       destination set (a media data object can't have an alternative
        #       destination, and be an alternative destination at the same
        #       time)
        #   - media_data_obj's alternative download destination, if any
        #   - The most recently-selected alternative download destination, if
        #       any
        #   - media_data_obj itself
        obj_list = []
        for this_dbid in dbid_list:

            this_obj = app_obj.media_reg_dict[this_dbid]

            if this_dbid != media_data_obj.dbid \
            and (
                media_data_obj.master_dbid == media_data_obj.dbid \
                or media_data_obj.master_dbid != this_dbid
            ) and (prev_dbid is None or prev_dbid != this_dbid) \
            and this_obj.dbid == this_obj.master_dbid:
                obj_list.append(this_obj)

        # Sort the modified list...
        obj_list.sort(key=lambda x: x.name.lower())

        # ...and then add, at the top of the list, possible destinations that
        #   were filtered out
        obj_list.insert(0, media_data_obj)

        if media_data_obj.master_dbid != media_data_obj.dbid:
            obj_list.insert(
                0,
                app_obj.media_reg_dict[media_data_obj.master_dbid],
            )

        elif prev_dbid is not None:
            prev_obj = app_obj.media_reg_dict[prev_dbid]
            obj_list.insert(0, prev_obj)

        # Add a combo
        store = Gtk.ListStore(GdkPixbuf.Pixbuf, int, str)
        count = -1

        for this_obj in obj_list:
            if isinstance(this_obj, media.Channel):
                icon_name = 'channel_small'
            elif isinstance(this_obj, media.Playlist):
                icon_name = 'playlist_small'
            else:
                icon_name = 'folder_small'

            store.append([
                main_win_obj.pixbuf_dict[icon_name],
                this_obj.dbid,
                this_obj.name,
            ])

            count += 1

        combo = Gtk.ComboBox.new_with_model(store)
        grid.attach(combo, 1, 4, 2, 1)
        combo.set_hexpand(True)

        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_active(0)
        # (Signal connect appears below)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 5, grid_width, 1)

        if os.name == 'nt':
            string = _(
                'Using an external folder is not recommended, in general.' \
                + ' Choose an external folder if:',
            )
        else:
            string = _(
                'Using an external directory is not recommended, in general.' \
                + ' Choose an external directory if:',
            )

        if os.name == 'nt':
            string2 = _(
                'You want a different application to process the' \
                + ' downloaded videos (other applications should not modify' \
                + ' Tartube\'s main data folder)',
            )
        else:
            string2 = _(
                'You want a different application to process the' \
                + ' downloaded videos (other applications should not modify' \
                + ' Tartube\'s main data directory)',
            )

        label3 = Gtk.Label(
            utils.tidy_up_long_string(string, label_length) \
            + '\n\n • ' + utils.tidy_up_long_string(string2, label_length),
        )
        grid.attach(label3, 0, 6, grid_width, 1)
        label3.set_xalign(0)

        radiobutton3 = Gtk.RadioButton.new_from_widget(radiobutton2)
        radiobutton3.set_label('Use an external location:')
        grid.attach(radiobutton3, 0, 7, 2, 1)
        # (Signal connect appears below)

        button = Gtk.Button.new_with_label(_('Set'))
        grid.attach(button, 2, 7, 1, 1)
        button.set_hexpand(False)
        # (Signal connect appears below)

        entry = Gtk.Entry()
        grid.attach(entry, 0, 8, grid_width, 1)
        entry.set_editable(False)
        entry.set_can_focus(False)
        if media_data_obj.external_dir is not None:
            entry.set_text(media_data_obj.external_dir)
        elif prev_external_dir is not None:
            entry.set_text(prev_external_dir)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 9, grid_width, 1)

        # Set widget initial states
        if type(self.choice) == str:
            radiobutton3.set_active(True)
            combo.set_sensitive(False)
        elif self.choice != media_data_obj.dbid:
            radiobutton2.set_active(True)
            button.set_sensitive(False)
        else:
            radiobutton.set_active(True)
            combo.set_sensitive(False)
            button.set_sensitive(False)

        # (Signal connects from above)
        radiobutton.connect(
            'toggled',
            self.on_radiobutton_toggled,
            combo,
            button,
            entry,
        )
        radiobutton2.connect(
            'toggled',
            self.on_radiobutton2_toggled,
            combo,
            button,
            entry,
        )
        radiobutton3.connect(
            'toggled',
            self.on_radiobutton3_toggled,
            combo,
            button,
            entry,
        )
        combo.connect('changed', self.on_combo_changed)
        button.connect('clicked', self.on_button_clicked, entry)

        # Display the dialogue window
        self.show_all()


    # Callback class methods


    def on_button_clicked(self, button, entry):

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

        Prompts the user for a directory path, then stores it, so the calling
        function can retriveve it.

        Args:

            button (Gtk.Button): The widget clicked

            entry (Gtk.Entry): Another widget to update

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 40006 on_button_clicked')

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

        # Show a file chooser
        if os.name == 'nt':
            msg = _('Select an external folder')
        else:
            msg = _('Select an external directory')

        dialogue_win = app_obj.dialogue_manager_obj.show_file_chooser(
            msg,
            self.main_win_obj,
            'folder',
        )

        response = dialogue_win.run()
        dest_dir = dialogue_win.get_filename()
        dialogue_win.destroy()

        if response == Gtk.ResponseType.OK:

            # An external directory is not allowed inside Tartube's data
            #   directory
            if dest_dir[:len(app_obj.data_dir)] == app_obj.data_dir:

                if os.name == 'nt':
                    msg = _(
                        'An external folder must not be inside Tartube\'s' \
                        + ' own data folder',
                    )

                else:
                    msg = _(
                        'An external directory must not be inside Tartube\'s' \
                        + ' own data directory',
                    )

                # (Unfortunately, the new dialogue window is not closeable
                #   unless this dialogue window is closed first)
                self.destroy()

                app_obj.dialogue_manager_obj.show_msg_dialogue(
                    msg,
                    'error',
                    'ok',
                    None,                   # Parent window is main window
                )

            else:
                self.choice = dest_dir
                entry.set_text(dest_dir)
                self.main_win_obj.set_previous_external_dir(dest_dir)


    def on_combo_changed(self, combo):

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

        Store the combobox's selected item, so the calling function can
        retrieve it.

        Args:

            combo (Gtk.ComboBox): The clicked widget

            radiobutton2 (Gtk.RadioButton): Another widget to check

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 40078 on_combo_changed')

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        dbid = model[tree_iter][1]

        # (Allow for the possibility that the media data object might have
        #   been deleted, since the dialogue window opened)
        if dbid in self.main_win_obj.app_obj.container_reg_dict:
            self.choice = dbid
            self.main_win_obj.set_previous_alt_dest_dbid(dbid)


    def on_radiobutton_toggled(self, radiobutton, combo, button, entry):

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

        When the specified radiobutton is toggled, modify other widgets in the
        dialogue window, and set self.choice (the value to be retrieved by the
        calling function)

        Args:

            radiobutton (Gtk.RadioButton): The clicked widget

            combo (Gtk.ComboBox): Another widget to modify

            button (Gtk.Button): Another widget to modify

            entry (Gtk.Entry): Another widget to modify

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 40112 on_radiobutton_toggled')

        if radiobutton.get_active():
            combo.set_sensitive(False)
            button.set_sensitive(False)
            entry.set_sensitive(False)
            self.choice = self.media_data_obj.dbid


    def on_radiobutton2_toggled(self, radiobutton2, combo, button, entry):

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

        When the specified radiobutton is toggled, modify other widgets in the
        dialogue window, and set self.choice (the value to be retrieved by the
        calling function)

        Args:

            radiobutton2 (Gtk.RadioButton): The clicked widget

            combo (Gtk.ComboBox): The widget containing the user's choice

            button (Gtk.Button): Another widget to modify

            entry (Gtk.Entry): Another widget to modify

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 40142 on_radiobutton2_toggled')

        if radiobutton2.get_active():
            combo.set_sensitive(True)
            button.set_sensitive(False)
            entry.set_sensitive(False)

            tree_iter = combo.get_active_iter()
            model = combo.get_model()
            dbid = model[tree_iter][1]

            # (Allow for the possibility that the media data object might have
            #   been deleted, since the dialogue window opened)
            if dbid in self.main_win_obj.app_obj.container_reg_dict:
                self.choice = dbid
                self.main_win_obj.set_previous_alt_dest_dbid(dbid)


    def on_radiobutton3_toggled(self, radiobutton2, combo, button, entry):

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

        When the specified radiobutton is toggled, modify other widgets in the
        dialogue window, and set self.choice (the value to be retrieved by the
        calling function)

        Args:

            radiobutton2 (Gtk.RadioButton): The clicked widget

            combo (Gtk.ComboBox): Another widget to modify

            button (Gtk.Button): Another widget to modify

            entry (Gtk.Entry): The widget containing the user's choice

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 40181 on_radiobutton3_toggled')

        if radiobutton2.get_active():
            combo.set_sensitive(False)
            button.set_sensitive(True)
            entry.set_sensitive(True)

            # self.choice set to its default value, until the user actually
            #   specifies an external directory
            dest_dir = entry.get_text()
            if dest_dir == '':
                self.choice = self.media_data_obj.dbid
            else:
                self.choice = dest_dir

            self.main_win_obj.set_previous_external_dir(dest_dir)


class SetNicknameDialogue(Gtk.Dialog):

    """Called by MainWin.on_video_index_set_nickname().

    Python class handling a dialogue window that prompts the user to set the
    nickname of a channel, playlist or folder.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        media_data_obj (media.Channel, media.Playlist, media.Folder): The media
            data object whose nickname is to be changed

    """


    # Standard class methods


    def __init__(self, main_win_obj, media_data_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 40222 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Set nickname\' dialogue starts here.' \
            + ' In the Videos tab, right-click a channel and select' \
            + ' Channel actions > Set nickname...'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.entry = None                       # Gtk.Entry


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Set nickname'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)

        # Set up the dialogue window
        spacing_size = self.main_win_obj.spacing_size
        label_length = self.main_win_obj.long_string_max_len

        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(spacing_size)
        grid.set_row_spacing(spacing_size)

        media_type = media_data_obj.get_type()
        if media_type == 'channel':
            msg = _(
                'Set a nickname for the channel \'{0}\' (or leave it blank' \
                + ' to reset the nickname)',
            ).format(media_data_obj.name)
        elif media_type == 'playlist':
            msg = _(
                'Set a nickname for the playlist \'{0}\' (or leave it blank' \
                + ' to reset the nickname)',
            ).format(media_data_obj.name)
        else:
            msg = _(
                'Set a nickname for the folder \'{0}\' (or leave it blank' \
                + ' to reset the nickname)',
            ).format(media_data_obj.name)

        label = Gtk.Label(
            utils.tidy_up_long_string(
                msg,
                label_length,
            ),
        )
        grid.attach(label, 0, 0, 1, 1)

        # (Store various widgets as IVs, so the calling function can retrieve
        #   their contents)
        self.entry = Gtk.Entry()
        grid.attach(self.entry, 0, 1, 1, 1)
        self.entry.set_text(media_data_obj.nickname)

        # Display the dialogue window
        self.show_all()


class SetURLDialogue(Gtk.Dialog):

    """Called by MainWin.on_video_index_set_url().

    Python class handling a dialogue window that prompts the user to set the
    source URL of a channel or playlist.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        media_data_obj (media.Channel, media.Playlist): The media data object
            whose source URL is to be changed

    """


    # Standard class methods


    def __init__(self, main_win_obj, media_data_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 40326 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Set URL\' dialogue starts here.' \
            + ' In the Videos tab, right-click a channel and select' \
            + ' Channel actions > Set URL...'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.entry = None                       # Gtk.Entry


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Set URL'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)

        # Set up the dialogue window
        spacing_size = self.main_win_obj.spacing_size
        label_length = self.main_win_obj.long_string_max_len

        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(spacing_size)
        grid.set_row_spacing(spacing_size)

        media_type = media_data_obj.get_type()
        if media_type == 'channel':
            msg = _(
                'Update the URL for the channel \'{0}\'',
            ).format(media_data_obj.name)
        else:
            msg = _(
                'Update the URL for the playlist \'{0}\'',
            ).format(media_data_obj.name)

        label = Gtk.Label(
            utils.tidy_up_long_string(
                msg,
                label_length,
            ),
        )
        grid.attach(label, 0, 0, 1, 1)

        # (Store various widgets as IVs, so the calling function can retrieve
        #   their contents)
        self.entry = Gtk.Entry()
        grid.attach(self.entry, 0, 1, 1, 1)
        self.entry.set_text(media_data_obj.source)

        # Display the dialogue window
        self.show_all()


class SystemCmdDialogue(Gtk.Dialog):

    """Called by mainwin.MainWin.on_video_index_show_system_cmd() and
    .on_video_catalogue_show_system_cmd().

    Python class handling a dialogue window that shows the user the system
    command that would be used in a download operation for a particular
    media.Video, media.Channel, media.Playlist or media.Folder object.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        media_data_obj (media.Video, media.Channel, media.Playlist,
            media.Folder): The media data object in question

    """


    # Standard class methods


    def __init__(self, main_win_obj, media_data_obj):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 40425 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Show system command\' dialogue starts' \
            + ' here. In the Videos tab, right-click a channel and select' \
            + ' Downloads > Show system command...'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.textbuffer = None                  # Gtk.TextBuffer


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Show system command'),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (Gtk.STOCK_OK, Gtk.ResponseType.OK),
        )

        self.set_modal(False)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)

        grid_width = 3

        media_type = media_data_obj.get_type()
        label = Gtk.Label(
            utils.shorten_string(
                utils.upper_case_first(media_type) + ': ' \
                + media_data_obj.name,
                50,
            ),
        )
        grid.attach(label, 0, 0, grid_width, 1)

        frame = Gtk.Frame()
        grid.attach(frame, 0, 1, grid_width, 1)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_size_request(400, 150)

        textview = Gtk.TextView()
        scrolled.add(textview)
        textview.set_wrap_mode(Gtk.WrapMode.WORD)
        textview.set_hexpand(False)
        textview.set_editable(False)

        self.textbuffer = textview.get_buffer()
        # Initialise the textbuffer's contents
        self.update_textbuffer(media_data_obj)

        button = Gtk.Button(_('Update'))
        grid.attach(button, 0, 2, 1, 1)
        button.set_hexpand(True)
        button.connect(
            'clicked',
            self.on_update_clicked,
            media_data_obj,
        )

        button2 = Gtk.Button(_('Copy to clipboard'))
        grid.attach(button2, 1, 2, 1, 1)
        button2.set_hexpand(True)
        button2.connect(
            'clicked',
            self.on_copy_clicked,
            media_data_obj,
        )

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 3, 2, 1)

        # Display the dialogue window
        self.show_all()


    # Public class methods


    def update_textbuffer(self, media_data_obj):

        """Called from self.__init__().

        Initialises the specified textbuffer.

        Args:

            media_data_obj (media.Video, media.Channel, media.Playlist,
                media.Folder): The media data object whose system command is
                displayed in this dialogue window

        Return values:

            A string containing the system command displayed, or an empty
                string if the system command could not be generated

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 40542 update_textbuffer')

        # Get the options.OptionsManager object that applies to this media
        #   data object
        # (The manager might be specified by obj itself, or it might be
        #   specified by obj's parent, or we might use the default
        #   options.OptionsManager)
        options_obj = utils.get_options_manager(
            self.main_win_obj.app_obj,
            media_data_obj,
        )

        # Generate the list of download options for this media data object
        options_parser_obj = options.OptionsParser(self.main_win_obj.app_obj)
        options_list = options_parser_obj.parse(media_data_obj, options_obj)

        # Obtain the system command used to download this media data object
        cmd_list = utils.generate_ytdl_system_cmd(
            self.main_win_obj.app_obj,
            media_data_obj,
            options_list,
        )

        # Display it in the textbuffer
        display_cmd = utils.prepare_system_cmd_for_display(cmd_list)
        self.textbuffer.set_text(display_cmd)
        return display_cmd


    # Callback class methods


    def on_copy_clicked(self, button, media_data_obj):

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

        Updates the contents of the textview, and copies the system command to
        the clipboard.

        Args:

            button (Gtk.Button): The widget clicked

            media_data_obj (media.Video, media.Channel, media.Playlist,
                media.Folder): The media data object whose system command is
                displayed in this dialogue window

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 40592 on_copy_clicked')

        # Obtain the system command used to download this media data object,
        #   and display it in the textbuffer
        system_cmd = self.update_textbuffer(media_data_obj)

        # Copy the system command to the clipboard
        clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        clipboard.set_text(system_cmd, -1)


    def on_update_clicked(self, button, media_data_obj):

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

        Updates the contents of the textview.

        Args:

            button (Gtk.Button): The widget clicked

            media_data_obj (media.Video, media.Channel, media.Playlist,
                media.Folder): The media data object whose system command is
                displayed in this dialogue window

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 40620 on_update_clicked')

        # Obtain the system command used to download this media data object,
        #   and display it in the textbuffer
        self.update_textbuffer(media_data_obj)


class TestCmdDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.on_menu_test_ytdl() and
    MainWin.on_video_catalogue_test_dl()

    Python class handling a dialogue window that prompts the user for a
    URL and youtube-dl options. If the user specifies one or both, they are
    used in an info operation.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        source_url (str): If specified, this URL is added to the Gtk.Entry
            automatically

    """


    # Standard class methods


    def __init__(self, main_win_obj, source_url=None):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 40652 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Test youtube-dl\' dialogue starts here.' \
            + ' In the main window menu, click Operations > Test youtube-dl'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.entry = None                       # Gtk.Entry
        self.textbuffer = None                  # Gtk.TextBuffer


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            _('Test') + ' ' + main_win_obj.app_obj.get_downloader(),
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)

        # Set up the dialogue window
        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(main_win_obj.spacing_size)
        grid.set_row_spacing(main_win_obj.spacing_size)

        label = Gtk.Label(
            _('URL of the video to download (optional)'),
        )
        grid.attach(label, 0, 0, 1, 1)

        self.entry = Gtk.Entry()
        grid.attach(self.entry, 0, 1, 1, 1)
        self.entry.set_hexpand(True)
        if source_url is not None:
            self.entry.set_text(source_url)

        label2 = Gtk.Label(
            _('Command line options (optional)'),
        )
        grid.attach(label2, 0, 2, 1, 1)

        frame = Gtk.Frame()
        grid.attach(frame, 0, 3, 1, 1)

        scrolled = Gtk.ScrolledWindow()
        frame.add(scrolled)
        scrolled.set_size_request(400, 150)

        textview = Gtk.TextView()
        scrolled.add(textview)
        textview.set_wrap_mode(Gtk.WrapMode.WORD)
        textview.set_hexpand(False)
        if source_url is not None:
            # The calling function has already specified a URL, so move the
            #   cursor straight into the textview
            textview.grab_focus()

        self.textbuffer = textview.get_buffer()

        # Display the dialogue window
        self.show_all()


class TidyDialogue(Gtk.Dialog):

    """Called by mainapp.TartubeApp.on_menu_tidy_up() and
    MainWin.on_video_index_tidy().

    Python class handling a dialogue window that prompts the user for which
    actions to perform during a tidy operation. If the user selects at least
    one action, the calling function starts a tidy operation to apply them.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        media_data_obj (media.Channel, media.Playlist or media.Folder): If
            specified, only this media data object (and its children) are
            tidied up

    """


    # Standard class methods


    def __init__(self, main_win_obj, media_data_obj=None):

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 40759 __init__')

        ignore_me = _(
            'TRANSLATOR\'S NOTE: \'Tidy up\' dialogue starts here.' \
            + ' In the main window menu, click Operations > Tidy up files...'
        )

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.checkbutton = None                 # Gtk.CheckButton
        self.checkbutton2 = None                # Gtk.CheckButton
        self.checkbutton3 = None                # Gtk.CheckButton
        self.checkbutton4 = None                # Gtk.CheckButton
        self.checkbutton5 = None                # Gtk.CheckButton
        self.checkbutton6 = None                # Gtk.CheckButton
        self.checkbutton7 = None                # Gtk.CheckButton
        self.checkbutton8 = None                # Gtk.CheckButton
        self.checkbutton9 = None                # Gtk.CheckButton
        self.checkbutton10 = None               # Gtk.CheckButton
        self.checkbutton11 = None               # Gtk.CheckButton
        self.checkbutton12 = None               # Gtk.CheckButton
        self.checkbutton13 = None               # Gtk.CheckButton
        self.checkbutton14 = None               # Gtk.CheckButton
        self.checkbutton15 = None               # Gtk.CheckButton
        self.checkbutton16 = None               # Gtk.CheckButton
        self.checkbutton17 = None               # Gtk.CheckButton


        # Code
        # ----

        if media_data_obj is None:
            title = _('Tidy up files')
        elif isinstance(media_data_obj, media.Channel):
            title = _('Tidy up channel')
        elif isinstance(media_data_obj, media.Channel):
            title = _('Tidy up playlist')
        else:
            title = _('Tidy up folder')

        Gtk.Dialog.__init__(
            self,
            title,
            main_win_obj,
            Gtk.DialogFlags.DESTROY_WITH_PARENT,
            (
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OK, Gtk.ResponseType.OK,
            )
        )

        self.set_modal(False)

        # Set up the dialogue window
        spacing_size = self.main_win_obj.spacing_size
        label_length = self.main_win_obj.quite_long_string_max_len

        box = self.get_content_area()

        grid = Gtk.Grid()
        box.add(grid)
        grid.set_border_width(spacing_size)
        grid.set_row_spacing(spacing_size)

        # Left column
        self.checkbutton = Gtk.CheckButton()
        grid.attach(self.checkbutton, 0, 0, 1, 1)
        self.checkbutton.set_label(_('Check that videos are not corrupted'))
        # (Signal connect appears below)

        self.checkbutton2 = Gtk.CheckButton()
        grid.attach(self.checkbutton2, 0, 1, 1, 1)
        self.checkbutton2.set_label(_('Delete corrupted video files'))
        self.checkbutton2.set_sensitive(False)

        if not mainapp.HAVE_MOVIEPY_FLAG \
        or self.main_win_obj.app_obj.refresh_moviepy_timeout == 0:
            self.checkbutton.set_sensitive(False)
            self.checkbutton2.set_sensitive(False)

        self.checkbutton3 = Gtk.CheckButton()
        grid.attach(self.checkbutton3, 0, 2, 1, 1)
        self.checkbutton3.set_label(_('Check that videos do/don\'t exist'))

        self.checkbutton4 = Gtk.CheckButton()
        grid.attach(self.checkbutton4, 0, 3, 1, 1)
        self.checkbutton4.set_label(
            utils.tidy_up_long_string(
                _(
                'Delete downloaded video files (doesn\'t remove videos from' \
                + ' Tartube\'s database)',
                ),
                label_length,
            ),
        )
        # (Signal connect appears below)

        self.checkbutton5 = Gtk.CheckButton()
        grid.attach(self.checkbutton5, 0, 4, 1, 1)
        self.checkbutton5.set_label(
            utils.tidy_up_long_string(
                _('Also delete all video/audio files with the same name'),
                label_length,
            ),
        )
        self.checkbutton5.set_sensitive(False)

        self.checkbutton6 = Gtk.CheckButton()
        grid.attach(self.checkbutton6, 0, 5, 1, 1)
        self.checkbutton6.set_label(_('Remove no-URL videos from database'))

        self.checkbutton7 = Gtk.CheckButton()
        grid.attach(self.checkbutton7, 0, 6, 1, 1)
        self.checkbutton7.set_label(_('Remove duplicate videos from database'))

        # Right column
        self.checkbutton8 = Gtk.CheckButton()
        grid.attach(self.checkbutton8, 0, 7, 1, 1)
        self.checkbutton8.set_label(_('Delete all archive files'))

        self.checkbutton9 = Gtk.CheckButton()
        grid.attach(self.checkbutton9, 1, 0, 1, 1)
        self.checkbutton9.set_label(_('Move thumbnails into own folder'))
        # (Signal connect appears below)

        self.checkbutton10 = Gtk.CheckButton()
        grid.attach(self.checkbutton10, 1, 1, 1, 1)
        self.checkbutton10.set_label(_('Delete all thumbnail files'))
        # (Signal connect appears below)

        self.checkbutton11 = Gtk.CheckButton()
        grid.attach(self.checkbutton11, 1, 2, 1, 1)
        self.checkbutton11.set_label(_('Delete all .webp thumbnails'))
        # (Signal connect appears below)

        self.checkbutton12 = Gtk.CheckButton()
        grid.attach(self.checkbutton12, 1, 3, 1, 1)
        self.checkbutton12.set_label(
            utils.tidy_up_long_string(
                _('Convert .webp thumbnails to .jpg using FFmpeg'),
                label_length,
            ),
        )

        self.checkbutton13 = Gtk.CheckButton()
        grid.attach(self.checkbutton13, 1, 4, 1, 1)
        self.checkbutton13.set_label(
            utils.tidy_up_long_string(
                _('Move other metadata files into own folder'),
                label_length,
            ),
        )
        # (Signal connect appears below)

        self.checkbutton14 = Gtk.CheckButton()
        grid.attach(self.checkbutton14, 1, 5, 1, 1)
        self.checkbutton14.set_label(_('Delete all description files'))

        self.checkbutton15 = Gtk.CheckButton()
        grid.attach(self.checkbutton15, 1, 6, 1, 1)
        self.checkbutton15.set_label(_('Delete all metadata (JSON) files'))

        self.checkbutton16 = Gtk.CheckButton()
        grid.attach(self.checkbutton16, 1, 7, 1, 1)
        self.checkbutton16.set_label(_('Delete all annotation files'))

        # !!! DEBUG Git #472
        self.checkbutton17 = Gtk.CheckButton()
        grid.attach(self.checkbutton17, 0, 8, 2, 1)
        self.checkbutton17.set_label(
            _(
            'EXPERIMENTAL: convert \'.unknown_video\' file extensions to .mp4'
            ),
        )

        # Bottom strip

        button = Gtk.Button.new_with_label(_('Select all'))
        grid.attach(button, 0, 9, 1, 1)
        button.set_hexpand(False)
        # (Signal connect appears below)

        button2 = Gtk.Button.new_with_label(_('Select none'))
        grid.attach(button2, 1, 9, 1, 1)
        button2.set_hexpand(False)
        # (Signal connect appears below)

        # (Signal connects from above)
        self.checkbutton.connect('toggled', self.on_checkbutton_toggled)
        self.checkbutton4.connect('toggled', self.on_checkbutton4_toggled)
        self.checkbutton9.connect('toggled', self.on_checkbutton9_toggled)
        self.checkbutton10.connect('toggled', self.on_checkbutton10_toggled)
        self.checkbutton11.connect('toggled', self.on_checkbutton11_toggled)
        self.checkbutton13.connect('toggled', self.on_checkbutton13_toggled)
        button.connect('clicked', self.on_select_all_clicked)
        button2.connect('clicked', self.on_select_none_clicked)

        # Display the dialogue window
        self.show_all()


    # Callback class methods


    def on_checkbutton_toggled(self, checkbutton):

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

        When the 'Check that videos are not corrupted' button is toggled,
        update the 'Delete corrupted videos...' button.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 40983 on_checkbutton_toggled')

        if not checkbutton.get_active():
            self.checkbutton2.set_active(False)
            self.checkbutton2.set_sensitive(False)

        else:
            self.checkbutton2.set_sensitive(True)


    def on_checkbutton4_toggled(self, checkbutton):

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

        When the 'Delete downloaded video files' button is toggled, update the
        'Also delete...' button.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 41007 on_checkbutton4_toggled')

        if not checkbutton.get_active():
            self.checkbutton5.set_active(False)
            self.checkbutton5.set_sensitive(False)

        else:
            self.checkbutton5.set_sensitive(True)


    def on_checkbutton9_toggled(self, checkbutton):

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

        When the 'Move thumbnails into to own folder' button is toggled, update
        other widgets.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 41031 on_checkbutton9_toggled')

        if not checkbutton.get_active():
            self.checkbutton10.set_sensitive(True)
            self.checkbutton11.set_sensitive(True)
            self.checkbutton12.set_sensitive(True)

        else:
            self.checkbutton10.set_active(False)
            self.checkbutton10.set_sensitive(False)
            self.checkbutton11.set_active(False)
            self.checkbutton11.set_sensitive(False)
            self.checkbutton12.set_active(False)
            self.checkbutton12.set_sensitive(False)


    def on_checkbutton10_toggled(self, checkbutton):

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

        When the 'Delete all thumbnail files' button is toggled, update other
        widgets.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 41061 on_checkbutton10_toggled')

        if not checkbutton.get_active():
            self.checkbutton11.set_sensitive(True)
            self.checkbutton12.set_sensitive(True)

        else:
            self.checkbutton11.set_active(False)
            self.checkbutton11.set_sensitive(False)
            self.checkbutton12.set_active(False)
            self.checkbutton12.set_sensitive(False)


    def on_checkbutton11_toggled(self, checkbutton):

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

        When the 'Delete all .webp thumbnails' button is toggled, update other
        widgets.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 41088 on_checkbutton11_toggled')

        if not checkbutton.get_active():
            self.checkbutton12.set_sensitive(True)

        else:
            self.checkbutton12.set_active(False)
            self.checkbutton12.set_sensitive(False)


    def on_checkbutton13_toggled(self, checkbutton):

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

        When the 'Move other metadata files into own folder' button is toggled,
        update other widgets.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 41112 on_checkbutton13_toggled')

        if not checkbutton.get_active():

            self.checkbutton14.set_sensitive(True)
            self.checkbutton15.set_sensitive(True)
            self.checkbutton16.set_sensitive(True)

        else:

            self.checkbutton14.set_active(False)
            self.checkbutton15.set_active(False)
            self.checkbutton16.set_active(False)

            self.checkbutton14.set_sensitive(False)
            self.checkbutton15.set_sensitive(False)
            self.checkbutton16.set_sensitive(False)


    def on_select_all_clicked(self, button):

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

        Select all checkbuttons.

        Args:

            button (Gtk.Button): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 41144 on_select_all_clicked')

        self.checkbutton.set_active(True)
        self.checkbutton2.set_active(True)
        self.checkbutton3.set_active(True)
        self.checkbutton4.set_active(True)
        self.checkbutton5.set_active(True)
        self.checkbutton6.set_active(True)
        self.checkbutton7.set_active(True)
        self.checkbutton8.set_active(True)
        self.checkbutton9.set_active(True)
#        self.checkbutton10.set_active(False)
#        self.checkbutton11.set_active(False)
#        self.checkbutton12.set_active(False)
        self.checkbutton13.set_active(True)
#        self.checkbutton14.set_active(False)
#        self.checkbutton15.set_active(False)
#        self.checkbutton16.set_active(False)
        self.checkbutton17.set_active(True)


    def on_select_none_clicked(self, button):

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

        Unselect all checkbuttons.

        Args:

            button (Gtk.Button): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 41178 on_select_none_clicked')

        self.checkbutton.set_active(False)
        self.checkbutton2.set_active(False)
        self.checkbutton3.set_active(False)
        self.checkbutton4.set_active(False)
        self.checkbutton5.set_active(False)
        self.checkbutton6.set_active(False)
        self.checkbutton7.set_active(False)
        self.checkbutton8.set_active(False)
        self.checkbutton9.set_active(False)
        self.checkbutton10.set_active(False)
        self.checkbutton12.set_active(False)
        self.checkbutton13.set_active(False)
        self.checkbutton14.set_active(False)
        self.checkbutton15.set_active(False)
        self.checkbutton16.set_active(False)
        self.checkbutton17.set_active(False)

        if not mainapp.HAVE_MOVIEPY_FLAG \
        or self.main_win_obj.app_obj.refresh_moviepy_timeout == 0:
            self.checkbutton.set_sensitive(False)
            self.checkbutton2.set_sensitive(False)
