#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2019-2020 A S Lewis
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.


"""Main window classes."""


# Import Gtk modules
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject, Gdk, GdkPixbuf


# Import other modules
import datetime
from gi.repository import Gio
import os
from gi.repository import Pango
import re
import sys
import threading
import time
# (Desktop notifications don't work on MS Windows, so no need to import Notify)
if os.name != 'nt':
    gi.require_version('Notify', '0.7')
    from gi.repository import Notify


# Import our modules
import config
import formats
import html
import __main__
import mainapp
import media
import options
import utils


# 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 91 __init__')

        super(MainWin, self).__init__(
            title=__main__.__packagename__.title() + ' v' \
            + __main__.__version__,
            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_menubar)
        self.menubar = None                     # Gtk.MenuBar
        self.change_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.gen_options_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.export_db_menu_item = None         # Gtk.MenuItem
        self.import_db_menu_item = None         # Gtk.MenuItem
        self.switch_view_menu_item = None       # Gtk.MenuItem
        self.test_menu_item = None              # Gtk.MenuItem
        self.show_hidden_menu_item = None       # Gtk.MenuItem
        self.check_all_menu_item = None         # Gtk.MenuItem
        self.download_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.tidy_up_menu_item = None           # Gtk.MenuItem
        self.stop_operation_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.switch_view_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.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.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_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_button = None       # Gtk.ToolButton
        self.catalogue_filter_entry = None      # Gtk.Entry
        self.catalogue_regex_togglebutton = None
                                                # Gtk.ToggleButton
        self.catalogue_apply_filter_button = None
                                                # Gtk.ToolButton
        self.catalogue_cancel_filter_button = None
                                                # Gtk.ToolButton
        self.catalogue_find_date_button = None  # Gtk.ToolButton
        # (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.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
        # (from self.setup_output_tab)
        self.output_notebook = None             # Gtk.Notebook
        # (from self.setup_errors_tab)
        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.error_list_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
        # Standard size of video thumbnails in the Video Catalogue, in pixels,
        #   assuming that the actual thumbnail file is 1280x720
        self.thumb_width = 128
        self.thumb_height = 76

        # Paths to Tartube standard icon files. Dictionary in the form
        #   key - a string like 'video_both_large'
        #   value - full filepath to the icon file
        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 each window's icon list
        self.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.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)
        # The value is low, because descriptions in ALL CAPS are too big for
        #   the Video Catalogue, otherwise
        self.descrip_line_max_len = 50
        # 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

        # 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 = name of the media data object (stored in its .name IV)
        #   value = Gtk.TreeRowReference
        self.video_index_row_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 name 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 = None
        # Flag set to True when the currently visible item is a private folder
        #   (media.Folder.priv_flag is True), set to False at all other times
        self.video_index_current_priv_flag = False
        # 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 or
        #   mainwin.ComplexCatalogueItem objects (depending on the current
        #   value of self.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 or
        #       mainWin.ComplexCatalogueItem, which matches the dbid of its
        #       media.Video object)
        #   value = the catalogue item itself
        self.video_catalogue_dict = {}
        # Rows are added to the catalogue in a call to
        #   self.video_catalogue_insert_item()
        # 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 = []

        # 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

        # 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
        #   media.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

        # 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': flags from the options.OptionsManager
        #           object used for to download the video (not added to the
        #           dictionary at all for simulated downloads)
        self.results_list_temp_list = []

        # 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 = {}
        # When youtube-dl generates output, that text cannot be displayed in
        #   the Output Tab's pages immediately (because Gtk widgets cannot be
        #   updated from within a thread)
        # Instead, values are appended to this list
        # During a download operation, mainapp.TartubeApp.dl_timer_callback()
        #   calls self.output_tab_update() regularly to display the output in
        #   the Output Tab (which empties the list)
        # List in groups of 3, in the form
        #   (page_number, mssage, type...)
        # ...where 'page_number' matches a key in self.output_textview_dict,
        #   'msg' is a string to display, and 'type' is 'system_cmd' for a
        #   system command (displayed in yellow, by default), 'error_warning'
        #   for an error/warning message (displayed in cyan, by default) and
        #   'default' for everything else
        self.output_tab_insert_list = []
        # 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
        # The number of errors added to the Error List, since this tab was the
        #   visible one (updated by self.errors_list_add_row() or
        #   self.errors_list_add_system_error(), and reset back to zero by
        #   self.on_notebook_switch_page() when the tab becomes the visible one
        #   again)
        self.tab_error_count = 0
        # The number of warnings added to the Error List, since this tab was
        #   the visible one
        self.tab_warning_count = 0
        # 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

        # List of configuration windows (anything inheriting from
        #   config.GenericConfigWin) that are currently open. A download/
        #   update/refresh/info/tidy operation cannot start when one of these
        #   windows are open (and the windows cannot be opened during such an
        #   operation)
        self.config_win_list = []

        # Dialogue window IVs
        # The SetDestinationDialogue dialogue window displays a list of
        #   channels/playlists/folders. When opening it repeatedly, it's handy
        #   to display the previous selection at the top of the list
        # 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

        # Code
        # ----

        # Create GdkPixbuf.Pixbufs for all Tartube standard icons
        self.setup_pixbufs()
        # Set up the main window
        self.setup_win()


    # 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 518 setup_pixbufs')

        # The default location for icons is ../icons. Packagers may have moved
        #   them to one of the directories specified by
        #   __main__.__icon_dir_list__
        # Check all locations, and use the first one which actually exists
        icon_dir_list = __main__.__icon_dir_list__.copy()
        icon_dir_list.insert(
            0,
            os.path.abspath(
                os.path.join(self.app_obj.script_parent_dir, '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

                # (At the moment, the system preference window only uses one
                #   flag, but more may be added later)
                full_path = os.path.abspath(
                    os.path.join(icon_dir_path, 'locale', 'flag_uk.png'),
                )
                self.icon_dict['flag_uk'] = 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),
                    )

                # 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(
            'Missing icons directory; ' + __main__.__prettyname__ \
            + ' cannot start',
            file=sys.stderr,
        )

        self.app_obj.do_shutdown()


    def setup_win(self):

        """Called by self.__init__().

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

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 620 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)

        # 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)
        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 os.name != 'nt':
            Notify.init(__main__.__prettyname__)

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


    # (Create main window widgets)


    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 672 setup_grid')

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


    def setup_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 686 setup_menubar')

        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')

        # 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')

        # 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.export_db_menu_item = Gtk.MenuItem.new_with_mnemonic(
            '_Export from database',
        )
        media_sub_menu.append(self.export_db_menu_item)
        self.export_db_menu_item.set_action_name('app.export_db_menu')

        import_sub_menu = Gtk.Menu()

        import_json_menu_item = Gtk.MenuItem.new_with_mnemonic(
            '_JSON export file',
        )
        import_sub_menu.append(import_json_menu_item)
        import_json_menu_item.set_action_name('app.import_json_menu')

        import_text_menu_item = Gtk.MenuItem.new_with_mnemonic(
            'Plain _text export file',
        )
        import_sub_menu.append(import_text_menu_item)
        import_text_menu_item.set_action_name('app.import_text_menu')

        self.import_db_menu_item = Gtk.MenuItem.new_with_mnemonic(
            '_Import into database'
        )
        self.import_db_menu_item.set_submenu(import_sub_menu)
        media_sub_menu.append(self.import_db_menu_item)

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

        self.switch_view_menu_item = \
        Gtk.MenuItem.new_with_mnemonic('_Switch between views')
        media_sub_menu.append(self.switch_view_menu_item)
        self.switch_view_menu_item.set_action_name('app.switch_view_menu')

        self.show_hidden_menu_item = \
        Gtk.MenuItem.new_with_mnemonic('Show _hidden folders')
        media_sub_menu.append(self.show_hidden_menu_item)
        self.show_hidden_menu_item.set_action_name('app.show_hidden_menu')

        if self.app_obj.debug_test_media_menu_flag:

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

            self.test_menu_item = Gtk.MenuItem.new_with_mnemonic(
                '_Add test media',
            )
            media_sub_menu.append(self.test_menu_item)
            self.test_menu_item.set_action_name('app.test_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_action_name('app.custom_dl_all_menu')

        # 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())

        self.update_ytdl_menu_item = Gtk.MenuItem.new_with_mnemonic(
            'Update _youtube-dl',
        )
        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 youtube-dl...',
        )
        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())

        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',
        )

        # 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',
        )

        # 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')

        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')


    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 936 setup_main_toolbar')

        # 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
        self.main_toolbar = Gtk.Toolbar()
        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 video(s)')
        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:
            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 self.app_obj.debug_test_media_toolbar_flag:

            if not squeeze_flag:
                self.test_toolbutton = Gtk.ToolButton.new(
                    Gtk.Image.new_from_pixbuf(
                        self.pixbuf_dict['tool_test_small'],
                    ),
                )
                self.test_toolbutton.set_label('Test')
                self.test_toolbutton.set_is_important(True)
            else:
                self.test_toolbutton = Gtk.ToolButton.new(
                    Gtk.Image.new_from_pixbuf(
                        self.pixbuf_dict['tool_test_large'],
                    ),
                )

            self.main_toolbar.insert(self.test_toolbutton, -1)
            self.test_toolbutton.set_tooltip_text(
                'Add test media data objects',
            )
            self.test_toolbutton.set_action_name('app.test_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 ' + __main__.__prettyname__)
        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 1184 setup_notebook')

        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)

        # 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 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 1232 setup_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.paned_min_size)
        self.videos_paned.set_wide_handle(True)

        # Left-hand side
        self.video_index_vbox = Gtk.VBox()
        self.videos_paned.add1(self.video_index_vbox)

        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' and 'Download all' buttons
        self.button_box = Gtk.VBox()
        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,
            self.spacing_size,
        )
        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')

        # Right-hand side
        self.video_catalogue_vbox = Gtk.VBox()
        self.videos_paned.add2(self.video_catalogue_vbox)

        # 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)
        label = Gtk.Label('Page  ')
        toolitem.add(label)

        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)
        label2 = Gtk.Label('  of  ')
        toolitem3.add(label2)

        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)

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

        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
        self.catalogue_toolbar.insert(Gtk.SeparatorToolItem(), -1)

        self.catalogue_first_button \
        = Gtk.ToolButton.new_from_stock(Gtk.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',
        )

        self.catalogue_back_button \
        = Gtk.ToolButton.new_from_stock(Gtk.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',
        )

        self.catalogue_forwards_button \
        = Gtk.ToolButton.new_from_stock(Gtk.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',
        )

        self.catalogue_last_button \
        = Gtk.ToolButton.new_from_stock(Gtk.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',
        )

        self.catalogue_scroll_up_button \
        = Gtk.ToolButton.new_from_stock(Gtk.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 up')
        self.catalogue_scroll_up_button.set_action_name(
            'app.scroll_up_toolbutton',
        )

        self.catalogue_scroll_down_button \
        = Gtk.ToolButton.new_from_stock(Gtk.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 down')
        self.catalogue_scroll_down_button.set_action_name(
            'app.scroll_down_toolbutton',
        )

        self.catalogue_show_filter_button \
        = Gtk.ToolButton.new_from_stock(Gtk.STOCK_SORT_ASCENDING)
        self.catalogue_toolbar.insert(self.catalogue_show_filter_button, -1)
        self.catalogue_show_filter_button.set_sensitive(False)
        self.catalogue_show_filter_button.set_tooltip_text(
            'Show filter options',
        )
        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_show_filter_widgets()
        self.catalogue_toolbar2 = Gtk.Toolbar()
        self.catalogue_toolbar2.set_visible(False)

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

        self.catalogue_sort_button \
        = Gtk.ToolButton.new_from_stock(Gtk.STOCK_SPELL_CHECK)
        self.catalogue_toolbar2.insert(self.catalogue_sort_button, -1)
        self.catalogue_sort_button.set_sensitive(False)
        self.catalogue_sort_button.set_tooltip_text('Sort alphabetically')
        self.catalogue_sort_button.set_action_name(
            'app.sort_type_toolbutton',
        )

        # Separator
        self.catalogue_toolbar2.insert(Gtk.SeparatorToolItem(), -1)

        toolitem8 = Gtk.ToolItem.new()
        self.catalogue_toolbar2.insert(toolitem8, -1)
        label5 = Gtk.Label('Filter  ')
        toolitem8.add(label5)

        toolitem9 = Gtk.ToolItem.new()
        self.catalogue_toolbar2.insert(toolitem9, -1)
        self.catalogue_filter_entry = Gtk.Entry()
        toolitem9.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')

        toolitem10 = Gtk.ToolItem.new()
        self.catalogue_toolbar2.insert(toolitem10, -1)
        self.catalogue_regex_togglebutton \
        = Gtk.ToggleButton('Regex')
        toolitem10.add(self.catalogue_regex_togglebutton)
        self.catalogue_regex_togglebutton.set_sensitive(False)
        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',
        )

        self.catalogue_apply_filter_button \
        = Gtk.ToolButton.new_from_stock(Gtk.STOCK_FIND)
        self.catalogue_toolbar2.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',
        )

        self.catalogue_cancel_filter_button \
        = Gtk.ToolButton.new_from_stock(Gtk.STOCK_CANCEL)
        self.catalogue_toolbar2.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',
        )

        # Separator
        self.catalogue_toolbar2.insert(Gtk.SeparatorToolItem(), -1)

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

        self.catalogue_find_date_button \
        = Gtk.ToolButton.new_from_stock(Gtk.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',
        )

        # 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 1559 setup_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.paned_min_size)
        self.progress_paned.set_wide_handle(True)

        # Upper half
        frame = Gtk.Frame()
        self.progress_paned.add1(frame)

        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 disabled by a call to
        #   self.disable_tooltips() after the config file is loaded, if
        #   necessary)
        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,
        )

        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(20)
                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)

        # Lower half
        frame2 = Gtk.Frame()
        self.progress_paned.add2(frame2)

        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
        self.results_list_treeview = Gtk.TreeView()
        self.results_list_scrolled.add(self.results_list_treeview)
        self.results_list_treeview.set_can_focus(False)
        # (Tooltips are initially enabled, and disabled by a call to
        #   self.disable_tooltips() after the config file is loaded, if
        #   necessary)
        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,
        )

        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 column_title == 'File':
                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(20)
                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)

        # 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,
        )
        # (Making this widget expandable guarantees the whole grid is always
        #   full)
        self.bandwidth_checkbutton.set_hexpand(True)
        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,
        )

        self.video_res_checkbutton = Gtk.CheckButton()
        grid.attach(self.video_res_checkbutton, 4, 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)
        grid.attach(self.video_res_combobox, 5, 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)
        self.set_video_res_limit(None)        # Uses default resolution, 720p
        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 active rows after 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 of the list')
        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,
        )


    def setup_output_tab(self):

        """Called by self.setup_win().

        Creates widgets for the Output Tab.
        """

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

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

        # 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()
        vbox.pack_start(self.output_notebook, True, True, 0)
        self.output_notebook.set_border_width(0)

        # 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,
        )


    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 1856 setup_errors_tab')

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

        # Errors List
        frame = Gtk.Frame()
        vbox.pack_start(frame, True, True, 0)

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

        self.errors_list_treeview = Gtk.TreeView()
        self.errors_list_scrolled.add(self.errors_list_treeview)
        self.errors_list_treeview.set_can_focus(False)

        for i, column_title in enumerate(['', '', 'Time', 'Media', '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)

            else:
                renderer_text = Gtk.CellRendererText()
                column_text = Gtk.TreeViewColumn(
                    column_title,
                    renderer_text,
                    text=i,
                )
                self.errors_list_treeview.append_column(column_text)

        self.errors_list_liststore = Gtk.ListStore(
            GdkPixbuf.Pixbuf, GdkPixbuf.Pixbuf,
            str, str, str,
        )
        self.errors_list_treeview.set_model(self.errors_list_liststore)

        # Strip of widgets at the bottom
        hbox = Gtk.HBox()
        vbox.pack_start(hbox, False, False, self.spacing_size)
        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 ' + __main__.__prettyname__ + ' 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 ' + __main__.__prettyname__ + ' 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 server 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 server 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,
        )

        self.error_list_button = Gtk.Button()
        hbox.pack_end(self.error_list_button, False, False, 0)
        self.error_list_button.set_label('Clear list')
        self.error_list_button.connect(
            'clicked',
            self.on_errors_list_clear,
        )


    # (Moodify main window widgets)


    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 1986 toggle_visibility')

        if self.is_visible():
            self.set_visible(False)
        else:
            self.set_visible(True)


    def redraw_main_toolbar(self):

        """Called by mainapp.TartubeApp.load_config(), and also by
        .set_toolbar_squeeze_flag() when the value of the flag is changed.

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

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

        self.setup_main_toolbar()

        if self.app_obj.disable_dl_all_flag:
            self.download_all_menu_item.set_sensitive(False)

        self.show_all()


    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 2031 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.export_db_menu_item.set_sensitive(sens_flag)
        self.import_db_menu_item.set_sensitive(sens_flag)
        self.switch_view_menu_item.set_sensitive(sens_flag)
        self.show_hidden_menu_item.set_sensitive(sens_flag)

        self.check_all_menu_item.set_sensitive(sens_flag)
        self.download_all_menu_item.set_sensitive(sens_flag)
        self.refresh_db_menu_item.set_sensitive(sens_flag)

        if __main__.__debian_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(False)
        else:
            self.install_ffmpeg_menu_item.set_sensitive(sens_flag)

        self.stop_operation_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)
        self.download_all_toolbutton.set_sensitive(sens_flag)
        self.stop_operation_toolbutton.set_sensitive(False)
        self.switch_view_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 self.app_obj.disable_dl_all_flag:
                self.download_media_button.set_sensitive(False)
            else:
                self.download_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)

        # 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)


    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 2120 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 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 2149 sensitise_operation_widgets')

        self.system_prefs_menu_item.set_sensitive(sens_flag)
        self.gen_options_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.check_all_menu_item.set_sensitive(sens_flag)

        if not self.app_obj.disable_dl_all_flag:
            self.download_all_menu_item.set_sensitive(sens_flag)
        else:
            self.download_all_menu_item.set_sensitive(False)

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

        if not self.app_obj.disable_dl_all_flag:
            self.download_all_toolbutton.set_sensitive(sens_flag)
        else:
            self.download_all_toolbutton.set_sensitive(False)

        if not __main__.__debian_install_flag__:
            self.update_ytdl_menu_item.set_sensitive(sens_flag)

        self.test_ytdl_menu_item.set_sensitive(sens_flag)
        self.install_ffmpeg_menu_item.set_sensitive(sens_flag)

        # (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)


    def show_progress_bar(self, operation_type):

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

        At the start of a download/refresh/tidy 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, or
                'tidy' for a tidy operation

        """

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

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

        elif operation_type != 'check' \
        and operation_type != 'download' \
        and operation_type != 'refresh' \
        and operation_type != 'tidy':
            return self.app_obj.system_error(
                202,
                '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
        self.button_box.remove(self.check_media_button)
        self.check_media_button = None
        self.button_box.remove(self.download_media_button)
        self.download_media_button = 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.check_media_button.set_sensitive(False)
        if operation_type == 'check':
            self.check_media_button.set_label('Checking...')
        elif operation_type == 'download':
            self.check_media_button.set_label('Downloading...')
        elif operation_type == 'refresh':
            self.check_media_button.set_label('Refreshing...')
        else:
            self.check_media_button.set_label('Tidying...')

        # (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,
            (self.spacing_size * 2),
        )

        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)
        if operation_type == 'check':
            self.progress_bar.set_text('Checking...')
        elif operation_type == 'download':
            self.progress_bar.set_text('Downloading...')
        elif operation_type == 'refresh':
            self.progress_bar.set_text('Refreshing...')
        else:
            self.progress_bar.set_text('Tidying...')

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


    def hide_progress_bar(self):

        """Called by mainapp.TartubeApp.download_manager_finished(),
        .refresh_manager_finished() and .tidy_manager_finished().

        At the end of a download operation, replace self.progress_list with the
        original button.
        """

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

        if not self.progress_bar:
            return self.app_obj.system_error(
                203,
                '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
        self.button_box.remove(self.check_media_button)
        self.check_media_button = None
        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_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')

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

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


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

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

        During a download/refresh/tidy 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 2301 update_progress_bar')

        if not self.progress_bar:
            return self.app_obj.system_error(
                204,
                '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 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, '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, or None when finish_flag is True

        """

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

        if operation_type is not None \
        and operation_type != 'ffmpeg' and operation_type != 'ytdl' \
        and operation_type != 'formats' and operation_type != 'subs' \
        and operation_type != 'test_ytdl':
            return self.app_obj.system_error(
                205,
                '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
        self.button_box.remove(self.check_media_button)
        self.check_media_button = None
        self.button_box.remove(self.download_media_button)
        self.download_media_button = 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 not finish_flag:

            if operation_type == 'ffmpeg':
                self.check_media_button.set_label('Installing')
                self.download_media_button.set_label('FFmpeg')
            elif operation_type == 'ytdl':
                self.check_media_button.set_label('Updating')
                self.download_media_button.set_label('youtube-dl')
            elif operation_type == 'formats':
                self.check_media_button.set_label('Fetching')
                self.download_media_button.set_label('format list')
            elif operation_type == 'subs':
                self.check_media_button.set_label('Fetching')
                self.download_media_button.set_label('subtitle list')
            else:
                self.check_media_button.set_label('Testing')
                self.download_media_button.set_label('youtube-dl')

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

            self.sensitise_operation_widgets(False, True)

        else:
            self.check_media_button.set_label('Check all')
            self.check_media_button.set_sensitive(True)
            self.check_media_button.set_tooltip_text(
                'Check all videos, channels, playlists and folders',
            )

            self.download_media_button.set_label('Download all')

            self.download_media_button.set_tooltip_text(
                'Download all videos, channels, playlists and folders',
            )

            if not self.app_obj.disable_dl_all_flag:
                self.download_media_button.set_sensitive(True)
            else:
                self.download_media_button.set_sensitive(False)

            self.sensitise_operation_widgets(True, True)

        # Make the widget changes visible
        self.show_all()


    def enable_tooltips(self, update_catalogue_flag=False):

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

        Enables tooltips in the Video Index and Video Catalogue (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 2514 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:
            self.video_catalogue_redraw_all(
                self.video_index_current,
                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,
        )


    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 and Video Catalogue (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 2555 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:
            self.video_catalogue_redraw_all(
                self.video_index_current,
                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)


    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 2583 enable_dl_all_buttons')

        # This setting doesn't apply during a download/update/refresh/info/tidy
        #   operation
        if not self.app_obj.current_manager_obj:
            self.download_all_menu_item.set_sensitive(True)
            self.download_all_toolbutton.set_sensitive(True)
            self.download_media_button.set_sensitive(True)


    def disable_dl_all_buttons(self):

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

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

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

        # This setting doesn't apply during a download/update/refresh/info/tidy
        #   operation
        if not self.app_obj.current_manager_obj:
            self.download_all_menu_item.set_sensitive(False)
            self.download_all_toolbutton.set_sensitive(False)
            self.download_media_button.set_sensitive(False)


    def set_video_res_limit(self, resolution):

        """Called by mainapp.TartubeApp.load_config() and
        self.setup_progress_tab().

        Sets a new video resolution limit. Updates the combobox in the
        Progress Tab, and calls the main application to update its IV.

        Args:

            resolution (str): The new progressive scan resolution; a key in
                formats.VIDEO_RESOLUTION_DICT (e.g. '720p'), or None to use the
                default resolution limit specified by
                formats.VIDEO_RESOLUTION_DEFAULT.

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 2630 set_video_res_limit')

        # Check it's a recognised value
        if not resolution in formats.VIDEO_RESOLUTION_LIST:
            resolution = formats.VIDEO_RESOLUTION_DEFAULT

        self.video_res_combobox.set_active(
            formats.VIDEO_RESOLUTION_LIST.index(resolution),
        )

        self.app_obj.set_video_res_default(resolution)


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

        """Can be called by anything.

        Creates a desktop notification.

        Args:

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

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

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

        """

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

        # Desktop notifications don't work on MS Windows
        if os.name != 'nt':

            if title is None:
                title = __main__.__prettyname__

            if msg is None:
                # Emergency fallback - better than an empty message
                msg = __main__.__prettyname__

            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)
            notify_obj.show()


    def update_show_filter_widgets(self):

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

        The toolbar just below the Video Catalogue consists of two rows, the
        second of which is hidden by default. Show or hide the second row,
        as required.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 2698 update_show_filter_widgets')

        if not self.app_obj.catalogue_show_filter_flag:

            # Hide the second row
            self.catalogue_show_filter_button.set_stock_id(
                Gtk.STOCK_SORT_ASCENDING,
            )

            self.catalogue_show_filter_button.set_tooltip_text(
                'Show filter options',
            )

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

        else:

            # Show the second row
            self.catalogue_show_filter_button.set_stock_id(
                Gtk.STOCK_SORT_DESCENDING,
            )

            self.catalogue_show_filter_button.set_tooltip_text(
                'Hide filter options',
            )

            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.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 is None \
                or not self.video_catalogue_dict:
                    self.catalogue_regex_togglebutton.set_sensitive(False)


    def update_alpha_sort_widgets(self):

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

        Videos in the Video Catalogue can be sorted by date (default), or
        alphabetically. When the user switches between them, update the
        widgets themselves.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 2758 update_alpha_sort_widgets')

        if not self.app_obj.catalogue_alpha_sort_flag:
            self.catalogue_sort_button.set_stock_id(
                Gtk.STOCK_SPELL_CHECK,
            )

            self.catalogue_sort_button.set_tooltip_text('Sort alphabetically')

        else:
            self.catalogue_sort_button.set_stock_id(
                Gtk.STOCK_INDEX,
            )

            self.catalogue_sort_button.set_tooltip_text('Sort by date')


    def update_use_regex_widgets(self):

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

        After loading the config file, toggle the 'Regex' button in the toolbar
        just below the Video Catalogue.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 2784 update_use_regex_widgets')

        if not self.app_obj.catologue_use_regex_flag:
            self.catalogue_regex_togglebutton.set_active(False)
        else:
            self.catalogue_regex_togglebutton.set_active(True)


    # (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.

        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

        Returns:
            -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 2818 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 names of the media data objects on each row
        sort_column, sort_type \
        = self.video_index_sortmodel.get_sort_column_id()
        name1 = treestore.get_value(row_iter1, sort_column)
        name2 = treestore.get_value(row_iter2, sort_column)

        # Get corresponding media data objects
        id1 = self.app_obj.media_name_dict[name1]
        obj1 = self.app_obj.media_reg_dict[id1]

        id2 = self.app_obj.media_name_dict[name2]
        obj2 = self.app_obj.media_reg_dict[id2]

        # Do 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_auto_sort(self, row1, row2, data, notify):

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

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

        Args:

            row1, row2 (mainwin.CatalogueRow): Two rows in the liststore, one
                of which must be sorted before the other

            data (None): Ignored

            notify (False): Ignored

        Returns:
            -1 if row1 comes before row2, 1 if row2 comes before row1, 0 if
                their order should not be changed

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 2911 video_catalogue_auto_sort')

        # Get the media.Video objects displayed on each row
        obj1 = row1.video_obj
        obj2 = row2.video_obj

        # Sort by date
        if not self.app_obj.catalogue_alpha_sort_flag:

            # Sort videos by playlist index (if set), then by upload time, and
            #   then by receive (download) time
            # The video's index is not relevant unless sorting a playlist (and
            #   not relevant in private folders, e.g. 'All Videos')
            if isinstance(obj1.parent_obj, media.Playlist) \
            and not self.video_index_current_priv_flag \
            and obj1.parent_obj == obj2.parent_obj \
            and obj1.index is not None and obj2.index is not None:
                if obj1.index < obj2.index:
                    return -1
                else:
                    return 1
            elif obj1.upload_time is not None and obj2.upload_time is not None:
                if obj1.upload_time > obj2.upload_time:
                    return -1
                elif obj1.upload_time < obj2.upload_time:
                    return 1
                elif obj1.receive_time is not None \
                and obj2.receive_time is not None:
                    # In private folders, the most recently received video goes
                    #   to the top of the list
                    if self.video_index_current_priv_flag:
                        if obj1.receive_time > obj2.receive_time:
                            return -1
                        elif obj1.receive_time < obj2.receive_time:
                            return 1
                        else:
                            return 0
                    # ...but for everything else, the sorting algorithm is the
                    #   same as for media.GenericRemoteContainer.do_sort(), in
                    #   which we assume the website is sending us videos,
                    #   newest first
                    else:
                        if obj1.receive_time < obj2.receive_time:
                            return -1
                        elif obj1.receive_time > obj2.receive_time:
                            return 1
                        else:
                            return 0
                else:
                    return 0
            else:
                return 0

        # Sort alphabetically
        else:
            if obj1.name.lower() < obj2.name.lower():
                return -1
            elif obj1.name.lower() > obj2.name.lower():
                return 1
            else:
                return 0


    # (Popup menu functions for main window widgets)


    def video_index_popup_menu(self, event, name):

        """Called by self.on_video_index_right_click().

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

        Args:

            event (Gdk.EventButton): The mouse click event

            name (str): The name of the clicked media data object

        """

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

        # Find the right-clicked media data object (and a string to describe
        #   its type)
        dbid = self.app_obj.media_name_dict[name]
        media_data_obj = self.app_obj.media_reg_dict[dbid]
        media_type = media_data_obj.get_type()

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

        # Check/download/refresh items
        check_menu_item = Gtk.MenuItem.new_with_mnemonic(
            '_Check ' + media_type,
        )
        check_menu_item.connect(
            'activate',
            self.on_video_index_check,
            media_data_obj,
        )
        if self.app_obj.current_manager_obj \
        or (
            isinstance(media_data_obj, media.Folder) \
            and media_data_obj.priv_flag
        ):
            check_menu_item.set_sensitive(False)
        popup_menu.append(check_menu_item)

        download_menu_item = Gtk.MenuItem.new_with_mnemonic(
            '_Download ' + media_type,
        )
        download_menu_item.connect(
            'activate',
            self.on_video_index_download,
            media_data_obj,
        )
        if self.app_obj.current_manager_obj \
        or (
            isinstance(media_data_obj, media.Folder) \
            and media_data_obj.priv_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 ' + media_type,
        )
        custom_dl_menu_item.connect(
            'activate',
            self.on_video_index_custom_dl,
            media_data_obj,
        )
        if self.app_obj.current_manager_obj \
        or (
            isinstance(media_data_obj, media.Folder) \
            and media_data_obj.priv_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)

        contents_menu_item = Gtk.MenuItem.new_with_mnemonic(
            utils.upper_case_first(media_type) + ' co_ntents',
        )
        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:
            move_top_menu_item.set_sensitive(False)

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

        convert_text = None
        if isinstance(media_data_obj, media.Channel):
            convert_text = '_Convert to playlist'
        elif isinstance(media_data_obj, media.Playlist):
            convert_text = '_Convert to channel'
        else:
            convert_text = None

        if convert_text:

            convert_menu_item = Gtk.MenuItem.new_with_mnemonic(convert_text)
            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:
                convert_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)

        rename_location_menu_item = Gtk.MenuItem.new_with_mnemonic(
            '_Rename ' + media_type + '...',
        )
        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
        ):
            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)

        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,
        )
        actions_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)

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

        export_menu_item = Gtk.MenuItem.new_with_mnemonic(
            '_Export ' + media_type + '...',
        )
        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)

        refresh_menu_item = Gtk.MenuItem.new_with_mnemonic(
            'Re_fresh ' + media_type,
        )
        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)

        tidy_menu_item = Gtk.MenuItem.new_with_mnemonic(
            '_Tidy up ' + media_type,
        )
        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)

        actions_menu_item = Gtk.MenuItem.new_with_mnemonic(
            utils.upper_case_first(media_type) + ' _actions',
        )
        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())

        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)

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

        disable_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
            '_Disable 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_enforce_check,
            media_data_obj,
        )
        downloads_submenu.append(enforce_check_menu_item)
        if self.app_obj.current_manager_obj or media_data_obj.dl_disable_flag \
        or (
            isinstance(media_data_obj, media.Folder) \
            and media_data_obj.fixed_flag
        ):
            enforce_check_menu_item.set_sensitive(False)

        # (Widget sensitivity from above)
        if self.app_obj.current_manager_obj \
        or (
            isinstance(media_data_obj, media.Folder) \
            and media_data_obj.fixed_flag
        ):
            disable_menu_item.set_sensitive(False)
            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)

        # Show
        show_submenu = Gtk.Menu()

        show_properties_menu_item = Gtk.MenuItem.new_with_mnemonic(
            utils.upper_case_first(media_type) + ' _properties...',
        )
        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:
            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
        delete_menu_item = Gtk.MenuItem.new_with_mnemonic(
            'D_elete ' + media_type,
        )
        delete_menu_item.connect(
            'activate',
            self.on_video_index_delete_container,
            media_data_obj,
        )
        if self.app_obj.current_manager_obj:
            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() and
        mainwin.ComplexCatalogueItem.on_right_click_row().

        When the user right-clicks on the Video Catalogue, show 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 3497 video_catalogue_popup_menu')

        # Use a different popup menu for multiple selected rows
        # 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:

            return self.video_catalogue_multi_popup_menu(event, row_list)

        else:

            # Otherwise, right-clicking a row selects (and unselects everything
            #   else)
            self.catalogue_listbox.unselect_all()
            self.catalogue_listbox.select_row(catalogue_item_obj.catalogue_row)

        # 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,
        )
        if self.app_obj.current_manager_obj:
            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 self.app_obj.current_manager_obj:
                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 self.app_obj.current_manager_obj:
                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'
        )
        custom_dl_menu_item.connect(
            'activate',
            self.on_video_catalogue_custom_dl,
            video_obj,
        )
        if self.app_obj.current_manager_obj:
            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, video_obj)

        # 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(
            'Down_loads',
        )
        downloads_menu_item.set_submenu(downloads_submenu)
        popup_menu.append(downloads_menu_item)

        # 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)

        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)

        # Show location/properties
        show_submenu = Gtk.Menu()

        show_location_menu_item = Gtk.MenuItem.new_with_mnemonic(
            'Video _location',
        )
        show_location_menu_item.connect(
            'activate',
            self.on_video_catalogue_show_location,
            video_obj,
        )
        show_submenu.append(show_location_menu_item)

        show_properties_menu_item = Gtk.MenuItem.new_with_mnemonic(
            'Video _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(
            '_Show',
        )
        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 not video_obj.source 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)

        # 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, row_list):

        """Called by self.video_catalogue_popup_menu().

        When multiple rows are selected in the Video Catalogue and the user
        right-clicks one of them, show a context-sensitive popup menu.

        Args:

            event (Gdk.EventButton): The mouse click event

            row_list (list): List of mainwin.CatalogueRow objects that are
                currently selected (each one corresponding to a single
                media.Video object)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 3851 video_catalogue_multi_popup_menu')

        # 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)

        # 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

        # 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,
        )
        if self.app_obj.current_manager_obj:
            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,
        )
        if self.app_obj.current_manager_obj:
            download_menu_item.set_sensitive(False)
        popup_menu.append(download_menu_item)

        custom_dl_menu_item = Gtk.MenuItem.new_with_mnemonic(
            'C_ustom download videos'
        )
        custom_dl_menu_item.connect(
            'activate',
            self.on_video_catalogue_custom_dl_multi,
            video_list,
        )
        if self.app_obj.current_manager_obj:
            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_dl_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 not source_flag \
            or self.app_obj.update_manager_obj \
            or self.app_obj.refresh_manager_obj:
                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)

        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)

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

        # Download to Temporary Videos
        temp_submenu = Gtk.Menu()
        if not video_obj.source \
        or self.app_obj.update_manager_obj \
        or self.app_obj.refresh_manager_obj \
        or temp_folder_flag:
            temp_submenu.set_sensitive(False)

        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)

        # 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,
        )
        if not dl_flag:
            archive_menu_item.set_sensitive(False)
        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,
        )
        if not dl_flag:
            not_archive_menu_item.set_sensitive(False)
        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,
        )
        if not dl_flag:
            bookmark_menu_item.set_sensitive(False)
        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,
        )
        if not dl_flag:
            not_bookmark_menu_item.set_sensitive(False)
        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,
        )
        if not dl_flag:
            fav_menu_item.set_sensitive(False)
        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,
        )
        if not dl_flag:
            not_fav_menu_item.set_sensitive(False)
        mark_videos_submenu.append(not_fav_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,
        )
        if not dl_flag:
            new_menu_item.set_sensitive(False)
        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,
        )
        if not dl_flag:
            not_new_menu_item.set_sensitive(False)
        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,
        )
        if not dl_flag:
            playlist_menu_item.set_sensitive(False)
        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,
        )
        if not dl_flag:
            not_playlist_menu_item.set_sensitive(False)
        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)

        # 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)

        # 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, show 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 4223 progress_list_popup_menu')

        # 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
        video_downloader_obj = None

        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.video_downloader_obj is not None:
                    worker_obj = this_worker_obj
                    video_downloader_obj = this_worker_obj.video_downloader_obj
                    break

        # 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,
            video_downloader_obj,
        )
        popup_menu.append(stop_now_menu_item)
        if not download_manager_obj \
        or video_downloader_obj is None:
            stop_now_menu_item.set_sensitive(False)

        stop_soon_menu_item = Gtk.MenuItem.new_with_mnemonic(
            'Stop after this _video',
        )
        stop_soon_menu_item.connect(
            'activate',
            self.on_progress_list_stop_soon,
            download_item_obj,
            worker_obj,
            video_downloader_obj,
        )
        popup_menu.append(stop_soon_menu_item)
        if not download_manager_obj \
        or video_downloader_obj is None:
            stop_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)
            if utils.is_youtube(media_data_obj.source):

                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)

            else:

                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)

        # 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, dbid):

        """Called by self.on_results_list_right_click().

        When the user right-clicks on the Results List, show a context-
        sensitive popup menu.

        Args:

            event (Gdk.EventButton): The mouse click event

            path (Gtk.TreePath): Path to the clicked row in the treeview

            dbid (int): The dbid of the clicked video object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 4390 results_list_popup_menu')

        # Find the right-clicked video object, and check it still exists
        if not dbid in self.app_obj.media_reg_dict:
            return

        video_obj = self.app_obj.media_reg_dict[dbid]
        if not isinstance(video_obj, media.Video):
            return

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

        # Watch video
        self.add_watch_video_menu_items(popup_menu, video_obj)

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

        # Delete video
        delete_menu_item = Gtk.MenuItem.new_with_mnemonic('_Delete video')
        delete_menu_item.connect(
            'activate',
            self.on_results_list_delete_video,
            video_obj,
            path,
        )
        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 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 4450 video_index_setup_contents_submenu')

        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_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 wai_ting 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)


    def add_watch_video_menu_items(self, popup_menu, video_obj):

        """Called by self.video_catalogue_popup_menu() and
        self.results_list_popup_menu().

        Adds common menu items to the popup menu.

        Args:

            popup_menu (Gtk.Menu): The popup menu

            video_obj (media.Video): The video object that was right-clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 4591 add_watch_video_menu_items')

        # 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 not video_obj.source \
            or self.app_obj.update_manager_obj \
            or self.app_obj.refresh_manager_obj:
                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)

        # Watch video online. For YouTube URLs, offer alternative websites
        if not video_obj.source:

            watch_website_menu_item = Gtk.MenuItem.new_with_mnemonic(
                'Watch on _website',
            )
            watch_website_menu_item.set_sensitive(False)
            popup_menu.append(watch_website_menu_item)

        else:

            if not utils.is_youtube(video_obj.source):

                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)

            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)

                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())

        # 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,
            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 not video_obj.source \
        or self.app_obj.current_manager_obj \
        or (
            isinstance(video_obj.parent_obj, media.Folder)
            and video_obj.parent_obj.temp_flag
        ):
            temp_menu_item.set_sensitive(False)


    # (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 4756 video_index_catalogue_reset')

        video_index_current = self.video_index_current

        # 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 video_index_current is not None:

            dbid = self.app_obj.media_name_dict[video_index_current]
            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 4783 video_index_reset')

        # Reset IVs
        self.video_index_current = None
        if self.video_index_treeview:

            self.video_index_row_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)
        # (Tooltips are initially enabled, and disabled by a call to
        #   self.disable_tooltips() after the config file is loaded, if
        #   necessary)
        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_target_list = [('video index', 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,
            str,
        )
        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)
        self.video_index_sortmodel.set_sort_func(
            1,
            self.video_index_auto_sort,
            None,
        )

        count = -1
        for item in ['hide', 'hide', 'hide', 'pixbuf', 'show']:

            count += 1

            if item == 'pixbuf':

                renderer_pixbuf = Gtk.CellRendererPixbuf()
                column_pixbuf = Gtk.TreeViewColumn(
                    None,
                    renderer_pixbuf,
                    pixbuf=count,
                )
                self.video_index_treeview.append_column(column_pixbuf)

            else:
                renderer_text = Gtk.CellRendererText()
                column_text = Gtk.TreeViewColumn(
                    None,
                    renderer_text,
                    text=count,
                )
                self.video_index_treeview.append_column(column_text)
                if item == 'hide':
                    column_text.set_visible(False)
                else:
                    column_text.set_cell_data_func(
                        renderer_text,
                        self.video_index_render_text,
                    )

        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.self.video_index_add_row().
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 4913 video_index_populate')

        for dbid in self.app_obj.media_top_level_list:

            media_data_obj = self.app_obj.media_reg_dict[dbid]
            if not media_data_obj:
                return self.app_obj.system_error(
                    206,
                    'Video Index initialisation failure',
                )

            else:
                self.video_index_setup_row(media_data_obj, None)

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


    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 4950 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(
                207,
                'Video index setup row request failed sanity check',
            )

        # 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,
                self.video_index_get_text(media_data_obj),
            ],
        )

        # 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),
        )
        self.video_index_row_dict[media_data_obj.name] = tree_ref

        # 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 5013 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(
                208,
                'Video index setup row request failed sanity check',
            )

        # 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.name]
            # ...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,
                    self.video_index_get_text(media_data_obj),
                ],
            )

        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,
                    self.video_index_get_text(media_data_obj),
                ],
            )

        # 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),
        )
        self.video_index_row_dict[media_data_obj.name] = tree_ref

        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 5116 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(
                209,
                '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.name]
        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 (which resets the Video
        #   Catalogue)
        if self.video_index_current is not None \
        and self.video_index_current == media_data_obj.name:

            selection = self.video_index_treeview.get_selection()
            selection.unselect_all()

        # 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 5166 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(
                210,
                '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.name]

            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.name]

        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.

        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 5219 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(
                211,
                '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

        # Because of Gtk issues, we don't update the Video Index during a
        #   download/refresh/tidy operation if the flag is set
        if (
            self.app_obj.gtk_broken_flag \
            or self.app_obj.gtk_emulate_broken_flag
        ) and (
            self.app_obj.download_manager_obj \
            or self.app_obj.refresh_manager_obj \
            or self.app_obj.tidy_manager_obj
        ):
            return

        # Update the treeview row
        tree_ref = self.video_index_row_dict[media_data_obj.name]
        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()


    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.

        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 5274 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(
                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

        # Because of Gtk issues, we don't update the Video Index during a
        #   download/refresh/tidy operation if the flag is set
        if (
            self.app_obj.gtk_broken_flag \
            or self.app_obj.gtk_emulate_broken_flag
        ) and (
            self.app_obj.download_manager_obj \
            or self.app_obj.refresh_manager_obj \
            or self.app_obj.tidy_manager_obj
        ):
            return

        # Update the treeview row
        tree_ref = self.video_index_row_dict[media_data_obj.name]
        model = tree_ref.get_model()
        tree_path = tree_ref.get_path()
        tree_iter = model.get_iter(tree_path)
        model.set(tree_iter, 4, self.video_index_get_text(media_data_obj))

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


    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.

        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 5329 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(
                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

        # Because of Gtk issues, we don't update the Video Index during a
        #   download/refresh/tidy operation if the flag is set
        if (
            self.app_obj.gtk_broken_flag \
            or self.app_obj.gtk_emulate_broken_flag
        ) and (
            self.app_obj.download_manager_obj \
            or self.app_obj.refresh_manager_obj \
            or self.app_obj.tidy_manager_obj
        ):
            return

        # Update the treeview row
        tree_ref = self.video_index_row_dict[media_data_obj.name]
        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()


    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

        Returns:

            A GdkPixbuf or None.

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 5397 video_index_get_icon')

        icon = None
        if not self.app_obj.show_small_icons_in_index:

            # Large icons, bigger selection
            if isinstance(media_data_obj, media.Channel):

                if media_data_obj.fav_flag and media_data_obj.options_obj:
                    icon = 'channel_both_large'
                elif media_data_obj.fav_flag:
                    icon = 'channel_left_large'
                elif media_data_obj.options_obj:
                    icon = 'channel_right_large'
                else:
                    icon = 'channel_none_large'

            elif isinstance(media_data_obj, media.Playlist):

                if media_data_obj.fav_flag and media_data_obj.options_obj:
                    icon = 'playlist_both_large'
                elif media_data_obj.fav_flag:
                    icon = 'playlist_left_large'
                elif media_data_obj.options_obj:
                    icon = 'playlist_right_large'
                else:
                    icon = 'playlist_none_large'

            elif isinstance(media_data_obj, media.Folder):

                if media_data_obj.priv_flag:
                    if media_data_obj.fav_flag and media_data_obj.options_obj:
                        icon = 'folder_private_both_large'
                    elif media_data_obj.fav_flag:
                        icon = 'folder_private_left_large'
                    elif media_data_obj.options_obj:
                        icon = 'folder_private_right_large'
                    else:
                        icon = 'folder_private_none_large'

                elif media_data_obj.temp_flag:
                    if media_data_obj.fav_flag and media_data_obj.options_obj:
                        icon = 'folder_temp_both_large'
                    elif media_data_obj.fav_flag:
                        icon = 'folder_temp_left_large'
                    elif media_data_obj.options_obj:
                        icon = 'folder_temp_right_large'
                    else:
                        icon = 'folder_temp_none_large'

                elif media_data_obj.fixed_flag:
                    if media_data_obj.fav_flag and media_data_obj.options_obj:
                        icon = 'folder_fixed_both_large'
                    elif media_data_obj.fav_flag:
                        icon = 'folder_fixed_left_large'
                    elif media_data_obj.options_obj:
                        icon = 'folder_fixed_right_large'
                    else:
                        icon = 'folder_fixed_none_large'

                else:
                    if media_data_obj.fav_flag and media_data_obj.options_obj:
                        icon = 'folder_both_large'
                    elif media_data_obj.fav_flag:
                        icon = 'folder_left_large'
                    elif media_data_obj.options_obj:
                        icon = 'folder_right_large'
                    else:
                        icon = 'folder_none_large'

        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

        Returns:

            A string.

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 5511 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:

            if media_data_obj.vid_count:
                text += '\nV:' + 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) \
                + ' N:' + str(media_data_obj.new_count) \
                + ' P:' + 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_render_text(self, col, renderer, model, tree_iter, data):

        """Called by self.video_index_reset().

        Cell renderer function. When the text column of the Video Index is
        about to be rendered, set the font to normal, bold or italic, depending
        on the media data object's IVs.

        Args:

            col (Gtk.TreeViewColumn): The treeview column about to be rendered.

            renderer (Gtk.CellRendererText): The Gtk object handling the
                rendering.

            model (Gtk.TreeModelSort): The treeview's row data is stored here.

            tree_iter (Gtk.TreeIter): A pointer to the row containing the cell
                to be rendered.

            data (None): Ignored

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 5573 video_index_render_text')

        # Because of Gtk issues, we don't update the Video Index during a
        #   download/refresh/tidy operation if the flag is set
        if (
            not self.app_obj.gtk_broken_flag \
            and not self.app_obj.gtk_emulate_broken_flag
        ) or (
            not self.app_obj.download_manager_obj \
            and not self.app_obj.refresh_manager_obj \
            and not self.app_obj.tidy_manager_obj
        ):
            dbid = model.get_value(tree_iter, 0)
            media_data_obj = self.app_obj.media_reg_dict[dbid]

            # If marked new (unwatched), show as bold text
            if media_data_obj.new_count:
                renderer.set_property('weight', Pango.Weight.BOLD)
            else:
                renderer.set_property('weight', Pango.Weight.NORMAL)

            # If downloads disabled, show as italic text
            if media_data_obj.dl_disable_flag:
                renderer.set_property('style', Pango.Style.ITALIC)
                renderer.set_property('underline', True)
            elif media_data_obj.dl_sim_flag:
                renderer.set_property('style', Pango.Style.ITALIC)
                renderer.set_property('underline', False)
            else:
                renderer.set_property('style', Pango.Style.NORMAL)
                renderer.set_property('underline', False)

        else:

            # Using default weight/style/underline doesn't seem to cause the
            #   same Gtk issues
            # Forcing normal weight/style prevents the whole Video Index being
            #   drawn bold (occasionally)
            renderer.set_property('weight', Pango.Weight.NORMAL)
            renderer.set_property('style', Pango.Style.NORMAL)
            renderer.set_property('underline', False)


    # (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 5629 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 = {}

        # Set up the widgets
        self.catalogue_scrolled = Gtk.ScrolledWindow()
        self.catalogue_frame.add(self.catalogue_scrolled)
        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)

        self.catalogue_listbox.set_sort_func(
            self.video_catalogue_auto_sort,
            None,
            False,
        )

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


    def video_catalogue_redraw_all(self, name, 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 (corresponding 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 and
        resets IVs.

        Then, it adds new rows to the Gtk.ListBox and creates a new
        mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem object for
        each video on the page.

        Args:

            name (str): The selected media data object's name; one of the keys
                in self.media_name_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): Set to True when called by
                self.on_video_index_selection_changed(). The scrollbars must
                always be reset when switching between channels/playlist/
                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 5716 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 is None \
            or self.video_index_current != name \
            or self.catalogue_toolbar_current_page != page_num:
                reset_scroll_flag = True

        # The parent media data object is a media.Channel, media.playlist or
        #   media.Folder object
        dbid = self.app_obj.media_name_dict[name]
        parent_obj = self.app_obj.media_reg_dict[dbid]

        # Sanity check - the selected item in the Video Index should not be a
        #   media.Video object
        if not parent_obj or (isinstance(parent_obj, media.Video)):
            return self.system_error(
                214,
                'Videos should not appear in the Video Index',
            )

        # 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 parent 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; otherwise, use all child videos
        if self.video_catalogue_filtered_flag:
            child_list = self.video_catalogue_filtered_list.copy()
        else:
            child_list = parent_obj.child_list.copy()

        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 == 'simple_hide_parent' \
                or self.app_obj.catalogue_mode == 'simple_show_parent':
                    catalogue_item_obj = SimpleCatalogueItem(
                        self,
                        child_obj,
                    )

                else:
                    catalogue_item_obj = ComplexCatalogueItem(
                        self,
                        child_obj,
                    )

                self.video_catalogue_dict[catalogue_item_obj.dbid] = \
                catalogue_item_obj

                # 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(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()

        # 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
        self.catalogue_listbox.show_all()


    def video_catalogue_update_row(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).

        If the video is now yet visible in the Video Catalogue, but should be
        drawn on the current page, creates a new mainwin.SimpleCatalogueItem or
        mainwin.ComplexCatalogueItem object and adds a row to the Gtk.ListBox,
        removing an existing catalogue item to make room, if necessary.

        Args:

            video_obj (media.Video) - The video to update

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 5849 video_catalogue_update_row')

        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
        #   displayed in the Video Catalogue
        if self.video_index_current is None:
            return

        # Special measures during a refresh/tidy operation: don't update or
        #   create any new rows while the operation is in progress, if Gtk is
        #   broken
        if (
            self.app_obj.gtk_broken_flag
            or self.app_obj.gtk_emulate_broken_flag
        ) and (
            self.app_obj.refresh_manager_obj
            or self.app_obj.tidy_manager_obj
        ):
            return

        elif self.video_index_current != video_obj.parent_obj.name \
        and self.video_index_current != app_obj.fixed_all_folder.name \
        and (
            self.video_index_current != app_obj.fixed_new_folder.name \
            or video_obj.new_flag
        ) and (
            self.video_index_current != app_obj.fixed_bookmark_folder.name \
            or video_obj.bookmark_flag
        ) and (
            self.video_index_current != app_obj.fixed_fav_folder.name \
            or video_obj.fav_flag
        ) and (
            self.video_index_current != app_obj.fixed_waiting_folder.name \
            or video_obj.waiting_flag
        ):
            return

        # Does a mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem
        #   object already exist for this video?
        if video_obj.dbid in self.video_catalogue_dict:

            # Update the catalogue item object, which updates the widgets in
            #   the Gtk.ListBox
            catalogue_item_obj = self.video_catalogue_dict[video_obj.dbid]
            catalogue_item_obj.update_widgets()

        else:

            # Find the video's position in the parent container's list of
            #   child objects, ignoring any child objects that aren't videos
            # At the same time, count the number of child video object so that
            #   we can update the toolbar widgets
            video_count = 0
            page_num = 1
            current_page_num = self.catalogue_toolbar_current_page
            page_size = app_obj.catalogue_page_size

            dbid = app_obj.media_name_dict[self.video_index_current]
            container_obj = app_obj.media_reg_dict[dbid]

            for child_obj in container_obj.child_list:
                if isinstance(child_obj, media.Video):
                    video_count += 1
                    # If the page size is 0, then all videos are drawn on one
                    #   page
                    if child_obj == video_obj and page_size:
                        page_num = int((video_count - 1) / page_size) + 1

            # If the video should be drawn on the current page, or on any
            #   previous page, and if the current page is already full, then we
            #   might need to remove a catalogue item from this page, and
            #   replace it with another
            if page_num <= current_page_num \
            and len(self.video_catalogue_dict) >= page_size:

                # 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 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
                # Each dictionary should have 0 or 1 entries, but the code will
                #   cope if it's more than that
                missing_dict = {}
                for index in range (
                    (((current_page_num - 1) * page_size) + 1),
                    ((current_page_num * page_size) + 1),
                ):
                    if index <= video_count:
                        child_obj = container_obj.child_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 are
                for dbid in visible_dict:
                    catalogue_item_obj = self.video_catalogue_dict[dbid]
                    self.catalogue_listbox.remove(
                        catalogue_item_obj.catalogue_row,
                    )

                    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_item(missing_obj)

            else:

                # Page is not full, so just create a new catalogue item
                self.video_catalogue_insert_item(video_obj)

            # Update widgets in the toolbar
            self.video_catalogue_toolbar_update(
                self.catalogue_toolbar_current_page,
                video_count,
            )

        # Force the Gtk.ListBox to sort its rows, so that videos are displayed
        #   in the correct order
        # v1.3.112 this call is suspected of causing occasional crashes due to
        #   Gtk issues. Disable it, if a download/refresh/tidy operation is in
        #   progress
        if (
            not app_obj.gtk_broken_flag \
            and not app_obj.gtk_emulate_broken_flag
        ) or (
            not app_obj.download_manager_obj \
            and not app_obj.refresh_manager_obj \
            and not app_obj.tidy_manager_obj
        ):
            self.catalogue_listbox.invalidate_sort()

        # Procedure complete
        self.catalogue_listbox.show_all()


    def video_catalogue_insert_item(self, video_obj):

        """Called by self.video_catalogue_update_row() (only).

        Adds a new mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem
        to the Video Catalogue.

        Args:

            video_obj (media.Video): The video for which a new catalogue item
                should be created

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 6017 video_catalogue_insert_item')

        # Create the new catalogue item
        if self.app_obj.catalogue_mode == 'simple_hide_parent' \
        or self.app_obj.catalogue_mode == 'simple_show_parent':
            catalogue_item_obj = SimpleCatalogueItem(
                self,
                video_obj,
            )

        else:
            catalogue_item_obj = ComplexCatalogueItem(
                self,
                video_obj,
            )

        self.video_catalogue_dict[video_obj.dbid] = catalogue_item_obj

        # 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(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()


    def video_catalogue_retry_insert_items(self):

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

        If an earlier call to self.video_catalogue_insert_item() failed, one
        or more CatalogueRow objects are waiting to be added to the Video
        Catalogue. Add them, if so.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 6068 video_catalogue_retry_insert_items')

        if self.video_catalogue_temp_list:

            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
            # v1.3.112 this call is suspected of causing occasional crashes due
            #   to Gtk issues. Disable it, if a download/refresh/tidy operation
            #   is in progress
            if (
                not self.app_obj.gtk_broken_flag \
                and not self.app_obj.gtk_emulate_broken_flag
            ) or (
                not self.app_obj.download_manager_obj \
                and not self.app_obj.refresh_manager_obj \
                and not self.app_obj.tidy_manager_obj
            ):
                self.catalogue_listbox.invalidate_sort()

            # Procedure complete
            self.catalogue_listbox.show_all()


    def video_catalogue_delete_row(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 or mainwin.ComplexCatalogueItem .

        Args:

            video_obj (media.Video) - The video to remove

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 6117 video_catalogue_delete_row')

        # 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 is None:
            return

        elif self.video_index_current != video_obj.parent_obj.name \
        and self.video_index_current != app_obj.fixed_all_folder.name \
        and (
            self.video_index_current != app_obj.fixed_new_folder.name \
            or video_obj.new_flag
        ) and (
            self.video_index_current != app_obj.fixed_bookmark_folder.name \
            or video_obj.bookmark_flag
        ) and (
            self.video_index_current != app_obj.fixed_fav_folder.name \
            or video_obj.fav_flag
        ) and (
            self.video_index_current != app_obj.fixed_waiting_folder.name \
            or video_obj.waiting_flag
        ):
            return

        # Does a mainwin.SimpleCatalogueItem or mainwin.ComplexCatalogueItem
        #   object exist for this video?
        if video_obj.dbid in self.video_catalogue_dict:

            # Remove the catalogue item object and its mainwin.CatalogueRow
            #   object (the latter being a wrapper for Gtk.ListBoxRow)
            catalogue_item_obj = self.video_catalogue_dict[video_obj.dbid]

            # Remove the row from the Gtk.ListBox
            self.catalogue_listbox.remove(catalogue_item_obj.catalogue_row)

            # 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
            dbid = app_obj.media_name_dict[self.video_index_current]
            container_obj = app_obj.media_reg_dict[dbid]
            video_count = 0

            if self.video_catalogue_dict \
            and self.catalogue_toolbar_current_page \
            < self.catalogue_toolbar_last_page:

                # Get the last mainwin.CatalogueRow object directly from the
                #   Gtk listbox, as it is auto-sorted frequently
                row_list = self.catalogue_listbox.get_children()
                last_row = row_list[-1]
                if last_row:
                    last_obj = last_row.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_obj.dbid:
                            # (Use the next video after this one)
                            next_flag = True

                        elif next_flag == True:
                            # (Use this video)
                            move_obj = child_obj
                            next_flag = False

                # Create the new catalogue item
                if move_obj:
                    self.video_catalogue_update_row(move_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
            self.catalogue_listbox.show_all()


    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 6227 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_sort_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_row() and
        self.video_catalogue_delete_row().

        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 6277 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)

        # 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_sort_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)
        else:
            self.catalogue_sort_button.set_sensitive(True)
            self.catalogue_filter_entry.set_sensitive(True)
            self.catalogue_regex_togglebutton.set_sensitive(True)
            self.catalogue_apply_filter_button.set_sensitive(True)
            self.catalogue_cancel_filter_button.set_sensitive(False)
            self.catalogue_find_date_button.set_sensitive(True)


    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.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 6343 video_catalogue_apply_filter')

        # Sanity check - something must be selected in the Video Index, and it
        #   must not be a media.Video object
        parent_obj = None
        if self.video_index_current is not None:
            dbid = self.app_obj.media_name_dict[self.video_index_current]
            parent_obj = self.app_obj.media_reg_dict[dbid]

        if not parent_obj or (isinstance(parent_obj, media.Video)):
            return self.system_error(
                215,
                '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 == '':
            # Apply 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 doesn't match the filter text
        # Also filter out any videos that don't have an individual name set)
        video_list = []
        regex_flag = self.app_obj.catologue_use_regex_flag
        for child_obj in parent_obj.child_list:
            if isinstance(child_obj, media.Video):

                if child_obj.name != self.app_obj.default_video_name \
                and (
                    (
                        not regex_flag \
                        and child_obj.name.lower().find(search_text.lower()) \
                        > -1
                    ) or (
                        regex_flag \
                        and re.search(
                            search_text,
                            child_obj.name,
                            re.IGNORECASE,
                        )
                    )
                ):
                    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,
            1,          # Display the first page
            True,       # Reset scrollbars
            True,       # This function is the caller
        )

        # Sensitise widgets, as appropriate
        self.catalogue_apply_filter_button.set_sensitive(False)
        self.catalogue_cancel_filter_button.set_sensitive(True)


    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 6419 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)

        # Sensitise widgets, as appropriate
        self.catalogue_apply_filter_button.set_sensitive(True)
        self.catalogue_cancel_filter_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 6446 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 = {}


    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 6487 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 6520 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']
        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,
                ),
            ),
        )
        row_list.append(pixbuf)
        row_list.append(
            utils.shorten_string(
                media_data_obj.name,
                self.medium_string_max_len,
            ),
        )
        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 media.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 6609 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:
            return self.app_obj.system_error(
                216,
                'Missing row in Progress List',
            )

        # 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 6651 progress_list_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.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 object
            # The dictionary is in the standard format described in the
            #   comments to media.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])

            # Update statistics displayed in that row
            # (Columns 0-4 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_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:
                                string = string + '/' \
                                + str(dl_stat_dict['playlist_size'])
                            else:
                                string = string + '/1'

                    else:
                        string = utils.shorten_string(
                            dl_stat_dict[key],
                            self.medium_string_max_len,
                        )

                    self.progress_list_liststore.set(
                        self.progress_list_liststore.get_iter(tree_path),
                        column,
                        string,
                    )


    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 6741 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 6772 progress_list_do_hide_row')

        row_num = self.progress_list_row_dict[item_id]

        # 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

        # Remove the row
        path = Gtk.TreePath(row_num),
        iter = self.progress_list_liststore.get_iter(path)
        self.progress_list_liststore.remove(iter)

        # 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]


    # (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 6814 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 = []


    def results_list_add_row(self, download_item_obj, video_obj, \
    keep_description=None, keep_info=None, keep_annotations=None,
    keep_thumbnail=None):

        """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

            keep_description (True, False, None):
            keep_info (True, False, None):
            keep_annotations (True, False, None):
            keep_thumbnail (bool): Settings from the options.OptionsManager
                object used to download the video (all of them set to 'None'
                for a simulated download)

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 6867 results_list_add_row')

        # Prepare the icons
        if self.app_obj.download_manager_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(
                217,
                '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,
                ),
            ),
        )
        row_list.append(pixbuf)
        row_list.append(
            utils.shorten_string(
                video_obj.nickname,
                self.medium_string_max_len,
            ),
        )

        # (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(
            utils.shorten_string(
                video_obj.parent_obj.name,
                self.medium_string_max_len,
            ),
        )

        # 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,
        }

        if keep_description is not None:
            temp_dict['keep_description'] = keep_description

        if keep_info is not None:
            temp_dict['keep_info'] = keep_info

        if keep_annotations is not None:
            temp_dict['keep_annotations'] = keep_annotations

        if keep_thumbnail is not None:
            temp_dict['keep_thumbnail'] = keep_thumbnail

        # Update IVs
        self.results_list_temp_list.append(temp_dict)
        # (The number of rows has just increased, so increment the IV for the
        #   next call to this function)
        self.results_list_row_count += 1


    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 6988 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.fixed_all_folder.sort_children()
                if video_obj.bookmark_flag:
                    self.app_obj.fixed_bookmark_folder.sort_children()
                if video_obj.fav_flag:
                    self.app_obj.fixed_fav_folder.sort_children()
                if video_obj.new_flag:
                    self.app_obj.fixed_new_folder.sort_children()
                if video_obj.waiting_flag:
                    self.app_obj.fixed_waiting_folder.sort_children()

                # Update the video catalogue in the 'Videos' tab
                self.video_catalogue_update_row(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
                tree_path = Gtk.TreePath(temp_dict['row_num'])
                row_iter = self.results_list_liststore.get_iter(tree_path)

                self.results_list_liststore.set(
                    row_iter,
                    3,
                    utils.shorten_string(
                        video_obj.nickname,
                        self.medium_string_max_len,
                    ),
                )

                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,
                    utils.shorten_string(
                        video_obj.parent_obj.name,
                        self.medium_string_max_len,
                    ),
                )

            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


    # (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 7127 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
        #   mainapp.TartubeApp.num_worker_default during this session (i.e. if
        #   the user reduces its value, we don't remove pages; but we do add
        #   pages if the user increases its value)
        if self.output_page_count < self.app_obj.num_worker_default:

            for num in range(1, (self.app_obj.num_worker_default + 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 7162 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()

        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
        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_provider = self.output_tab_set_textview_css(
            '#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' \
            + '   font-family: monospace, monospace;\n' \
            + '   font-size: 10pt;\n' \
            + '}'
        )

        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

        Returns:

            The Gtk.CssProvider created

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 7271 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. However, it
        can't be displayed immediately, because Gtk widgets can't be updated
        from within a thread.

        Instead, add the received values to a list, and wait for the GObject
        timer mainapp.TartubeApp.dl_timer_id to call self.output_tab_update().

        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_update_pages().

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 7311 output_tab_write_stdout')

        self.output_tab_insert_list.extend( [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. However, it
        can't be displayed immediately, because Gtk widgets can't be updated
        from within a thread.

        Instead, add the received values to a list, and wait for the GObject
        timer mainapp.TartubeApp.dl_timer_id to call self.output_tab_update().

        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_update_pages().

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 7342 output_tab_write_stderr')

        self.output_tab_insert_list.extend( [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). However, they can't be displayed
        immediately, because Gtk widgets can't be updated from within a thread.

        Instead, add the received values to a list, and wait for the GObject
        timer mainapp.TartubeApp.dl_timer_id to call self.output_tab_update().

        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_update_pages().

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 7372 output_tab_write_system_cmd')

        self.output_tab_insert_list.extend( [page_num, msg, 'system_cmd'] )


    def output_tab_update_pages(self):

        """Can be called by anything.

        During a download operation, youtube-dl sends output to STDOUT/STDERR.
        If permitted, this output is displayed in the Output Tab, along with
        any system commands.

        However, the text can't be displayed immediately, because Gtk widgets
        can't be updated from within a thread.

        Instead, the text has been added to self.output_tab_insert_list, and
        can now be displayed (and the list can be emptied).

        Other operations also call this function to display text added to
        self.output_tab_insert_list.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 7396 output_tab_update_pages')

        update_dict = {}

        if self.output_tab_insert_list:

            while self.output_tab_insert_list:

                page_num = self.output_tab_insert_list.pop(0)
                msg = self.output_tab_insert_list.pop(0)
                msg_type = self.output_tab_insert_list.pop(0)

                # Add the output to the textview. STDERR messages and system
                #   commands are displayed in a different colour
                # (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()
                    update_dict[page_num] = textview

                    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
            for textview in update_dict.values():
                textview.show_all()


    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): Ignored

            scrolled (Gtk.ScrolledWindow): The scroller which contains the
                textview

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 7478 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 7500 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())


    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 a download/update/refresh/info/tidy operation, empty
        the pages in the Output Tab (if allowed).
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 7524 output_tab_reset_pages')

        for textview in self.output_textview_dict.values():
            textbuffer = textview.get_buffer()
            textbuffer.set_text('')
            textview.show_all()


    # (Errors Tab)


    def errors_list_reset(self):

        """Can be called by anything.

        Empties the Gtk.TreeView in the Errors List, ready for it to be
        refilled. (There are no IVs to reset.)
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 7544 errors_list_reset')

        # Reset widgets
        self.errors_list_liststore = Gtk.ListStore(
            GdkPixbuf.Pixbuf, GdkPixbuf.Pixbuf,
            str, str, str,
        )
        self.errors_list_treeview.set_model(self.errors_list_liststore)

        self.tab_error_count = 0
        self.tab_warning_count = 0
        self.errors_list_refresh_label()


    def errors_list_add_row(self, media_data_obj):

        """Called by downloads.DownloadWorker.run().

        When a download job generates error and/or warning messages, this
        function is called to display them in the Errors List.

        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.

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 7574 errors_list_add_row')

        # Create a new row for every error and warning message
        # Use the same time on each
        utc = datetime.datetime.utcfromtimestamp(time.time())
        time_string = str(utc.strftime('%H:%M:%S'))

        if self.app_obj.operation_error_show_flag:

            for msg in media_data_obj.error_list:

                # Prepare the icons
                pixbuf = self.pixbuf_dict['error_small']

                if isinstance(media_data_obj, media.Video):
                    pixbuf2 = self.pixbuf_dict['video_small']
                elif isinstance(media_data_obj, media.Channel):
                    pixbuf2 = self.pixbuf_dict['channel_small']
                elif isinstance(media_data_obj, media.Playlist):
                    pixbuf2 = self.pixbuf_dict['playlist_small']
                else:
                    return self.app_obj.system_error(
                        218,
                        'Errors List add row request failed sanity check',
                    )

                # Prepare the new row in the treeview
                row_list = []
                row_list.append(pixbuf)
                row_list.append(pixbuf2)
                row_list.append(time_string)
                row_list.append(
                    utils.shorten_string(
                        media_data_obj.name,
                        self.medium_string_max_len,
                    ),
                )
                row_list.append(utils.tidy_up_long_string(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 != 3:
                    self.tab_error_count += 1

        if self.app_obj.operation_warning_show_flag:

            for msg in media_data_obj.warning_list:

                # Prepare the icons
                pixbuf = self.pixbuf_dict['warning_small']

                if isinstance(media_data_obj, media.Video):
                    pixbuf2 = self.pixbuf_dict['video_small']
                elif isinstance(media_data_obj, media.Channel):
                    pixbuf2 = self.pixbuf_dict['channel_small']
                elif isinstance(media_data_obj, media.Playlist):
                    pixbuf2 = self.pixbuf_dict['playlist_small']
                else:
                    return self.app_obj.system_error(
                        219,
                        'Errors List add row request failed sanity check',
                    )

                # Prepare the new row in the treeview
                row_list = []
                row_list.append(pixbuf)
                row_list.append(pixbuf2)
                row_list.append(time_string)
                row_list.append(
                    utils.shorten_string(
                        media_data_obj.name,
                        self.medium_string_max_len,
                    ),
                )
                row_list.append(utils.tidy_up_long_string(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 != 3:
                    self.tab_warning_count += 1

        # Update the tab's label to show the number of warnings/errors visible
        if self.visible_tab_num != 3:
            self.errors_list_refresh_label()


    def errors_list_add_system_error(self, 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.

        Args:

            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 7688 errors_list_add_system_error')

        if not self.app_obj.system_error_show_flag:
            # Do nothing
            return False

        # Prepare the icons
        pixbuf = self.pixbuf_dict['error_small']
        pixbuf2 = self.pixbuf_dict['system_error_small']

        # Prepare the new row in the treeview
        row_list = []
        utc = datetime.datetime.utcfromtimestamp(time.time())
        time_string = str(utc.strftime('%H:%M:%S'))

        row_list.append(pixbuf)
        row_list.append(pixbuf2)
        row_list.append(time_string)
        row_list.append(__main__.__prettyname__ + ' error')
        row_list.append(
            utils.tidy_up_long_string('#' + str(error_code) + ': ' + 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 != 3:
            self.tab_error_count += 1
            self.errors_list_refresh_label()


    def errors_list_add_system_warning(self, error_code, msg):

        """Can be called by anything. The quickest way is to call
        mainapp.TartubeApp.system_warning(), which acts as a wrapper for this
        function.

        Display a system warning message in the Errors List.

        Args:

            error_code (int): An error code in the range 100-999 (see
                the .system_error() function)

            msg (str): The system warning message to display

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 7741 errors_list_add_system_warning')

        if not self.app_obj.system_warning_show_flag:
            # Do nothing
            return False

        # Prepare the icons
        pixbuf = self.pixbuf_dict['warning_small']
        pixbuf2 = self.pixbuf_dict['system_warning_small']

        # Prepare the new row in the treeview
        row_list = []
        utc = datetime.datetime.utcfromtimestamp(time.time())
        time_string = str(utc.strftime('%H:%M:%S'))

        row_list.append(pixbuf)
        row_list.append(pixbuf2)
        row_list.append(time_string)
        row_list.append(__main__.__prettyname__ + ' warning')
        row_list.append(
            utils.tidy_up_long_string('#' + str(error_code) + ': ' + 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 != 3:
            self.tab_warning_count += 1
            self.errors_list_refresh_label()


    def errors_list_refresh_label(self):

        """Called by self.errors_list_reset(), .errors_list_add_row(),
        .errors_list_add_system_error(), .errors_list_add_system_warning()
         and .on_notebook_switch_page().

        When the Errors / Warnings tab becomes the visible one, reset the
        tab's label (to show 'Errors / Warnings')

        When an error or warning is added to the Error List, refresh the tab's
        label (to show something like 'Errors (4) / Warnings (1)' )
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 7790 errors_list_refresh_label')

        text = '_Errors'
        if self.tab_error_count:
            text += ' (' + str(self.tab_error_count) + ')'

        text += ' / Warnings'
        if self.tab_warning_count:
            text += ' (' + str(self.tab_warning_count) + ')'

        self.errors_label.set_text_with_mnemonic(text)


    # Callback class methods


    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 7823 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(
                220,
                'Callback request denied due to current conditions',
            )

        # 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,
            media_data_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 7863 on_video_index_check')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                221,
                'Callback request denied due to current conditions',
            )

        # Start a download operation
        self.app_obj.download_manager_start('sim', False, [media_data_obj] )


    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 7891 on_video_index_convert_container')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                222,
                '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 7918 on_video_index_custom_dl')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                223,
                'Callback request denied due to current conditions',
            )

        # Start a custom download operation
        self.app_obj.download_manager_start('custom', False, [media_data_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 7946 on_video_index_delete_container')

        self.app_obj.delete_container(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 7967 on_video_index_dl_disable')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                224,
                '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)

        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 7999 on_video_index_download')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                225,
                'Callback request denied due to current conditions',
            )

        # Start a download operation
        self.app_obj.download_manager_start('real', False, [media_data_obj] )


    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 8036 on_video_index_drag_data_received')

        # Must override the usual Gtk handler
        treeview.stop_emission('drag_data_received')

        # Extract the drop destination
        drop_info = treeview.get_dest_row_at_pos(x, y)
        if drop_info is not None:

            # Get the dragged media data object
            old_selection = self.video_index_treeview.get_selection()
            (model, start_iter) = old_selection.get_selected()
            drag_name = model[start_iter][1]

            # Get the destination media data object
            drop_path, drop_posn = drop_info[0], drop_info[1]
            drop_iter = model.get_iter(drop_path)
            dest_name = model[drop_iter][1]

            if drag_name and dest_name:

                drag_id = self.app_obj.media_name_dict[drag_name]
                dest_id = self.app_obj.media_name_dict[dest_name]

                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 8087 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 8115 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(
                226,
                'Callback request denied due to current conditions',
            )

        # Open an edit window
        config.OptionsEditWin(
            self.app_obj,
            media_data_obj.options_obj,
            media_data_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 8146 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_enforce_check(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 8170 on_video_index_enforce_check')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                227,
                '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)

        self.video_index_update_row_text(media_data_obj)


    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 8203 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 8224 on_video_index_hide_folder')

        self.app_obj.mark_folder_hidden(media_data_obj, True)


    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 8251 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 8282 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 8308 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 operation 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_msg_dialogue(
                'The ' + media_data_obj.get_type() + ' contains ' \
                + str(count) + ' items, so this action might take a while.' \
                + '\n\nAre you sure you want to continue?',
                '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 8363 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 operation 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_msg_dialogue(
                'The ' + media_data_obj.get_type() + ' contains ' \
                + str(count) + ' items, so this action might take a while.' \
                + '\n\nAre you sure you want to continue?',
                '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 8423 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 8454 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_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 8486 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 8517 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 8543 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 operation 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_msg_dialogue(
                'The ' + media_data_obj.get_type() + ' contains ' \
                + str(count) + ' items, so this action might take a while.' \
                + '\n\nAre you sure you want to continue?',
                '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 8598 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 operation 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_msg_dialogue(
                'The ' + media_data_obj.get_type() + ' contains ' \
                + str(count) + ' items, so this action might take a while.' \
                + '\n\nAre you sure you want to continue?',
                '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 8653 on_video_index_move_to_top')

        self.app_obj.move_container_to_top(media_data_obj)


    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 8676 on_video_index_refresh')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                228,
                '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 8705 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(
                229,
                '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 8734 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 8758 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 8779 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),
            )

            iter = self.video_index_sortmodel.get_iter(path)
            if iter is not None:
                self.video_index_popup_menu(
                    event,
                    self.video_index_sortmodel[iter][1],
                )


    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 8821 on_video_index_selection_changed')

        (model, iter) = selection.get_selected()
        if iter is None or not model.iter_is_valid(iter):
            return
        else:
            name = model[iter][1]

        # 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 iter is None:
                self.video_index_current = None
                self.video_index_current_priv_flag = False
                self.video_catalogue_reset()

            else:

                # Update IVs
                self.video_index_current = name

                dbid = self.app_obj.media_name_dict[name]
                media_data_obj = self.app_obj.media_reg_dict[dbid]

                if isinstance(media_data_obj, media.Folder) \
                and media_data_obj.priv_flag:
                    self.video_index_current_priv_flag = True
                else:
                    self.video_index_current_priv_flag = False

                # Expand the tree beneath the selected line, if allowed
                if self.app_obj.auto_expand_video_index_flag:
                    self.video_index_treeview.expand_to_path(
                        model.get_path(iter),
                    )

                # Redraw the Video Catalogue, on the first page, and reset its
                #   scrollbars back to the top
                self.video_catalogue_redraw_all(name, 1, 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 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.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 8881 on_video_index_set_destination')

        if isinstance(media_data_obj, media.Video):
            return self.app_obj.system_error(
                230,
                '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
        dbid = dialogue_win.choice
        dialogue_win.destroy()

        if response == Gtk.ResponseType.OK:

            if dbid != media_data_obj.master_dbid:
                media_data_obj.set_master_dbid(self.app_obj, dbid)

            # Update tooltips for this row
            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.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 8922 on_video_index_set_nickname')

        if isinstance(media_data_obj, media.Video):
            return self.app_obj.system_error(
                231,
                '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
            self.video_index_update_row_text(media_data_obj)


    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.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 8966 on_video_index_show_destination')

        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(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 8992 on_video_index_show_location')

        path = media_data_obj.get_default_dir(self.app_obj)
        utils.open_file(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 9014 on_video_index_show_properties')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                232,
                '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 9045 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.Channel):
                The clicked media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 9069 on_video_index_tidy')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                233,
                '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(),
                'del_descrip_flag': dialogue_win.checkbutton6.get_active(),
                'del_json_flag': dialogue_win.checkbutton7.get_active(),
                'del_xml_flag': dialogue_win.checkbutton8.get_active(),
                'del_thumb_flag': dialogue_win.checkbutton9.get_active(),
                'del_archive_flag': dialogue_win.checkbutton10.get_active(),
            }

        # Now destroy the window
        dialogue_win.destroy()

        if response == Gtk.ResponseType.OK:

            # If nothing was selected, then there is nothing to do
            # (Don't need to check 'del_others_flag' here)
            if not choices_dict['corrupt_flag'] \
            and not choices_dict['exist_flag'] \
            and not choices_dict['del_video_flag'] \
            and not choices_dict['del_descrip_flag'] \
            and not choices_dict['del_json_flag'] \
            and not choices_dict['del_xml_flag'] \
            and not choices_dict['del_thumb_flag'] \
            and not choices_dict['del_archive_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_descrip_flag'] \
            or choices_dict['del_json_flag'] \
            or choices_dict['del_xml_flag'] \
            or choices_dict['del_thumb_flag'] \
            or choices_dict['del_archive_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.tidy_manager_start(choices_dict)


    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 9156 on_video_catalogue_apply_options')

        if self.app_obj.current_manager_obj or media_data_obj.options_obj:
            return self.app_obj.system_error(
                234,
                'Callback request denied due to current conditions',
            )

        # Apply download options to the media data object
        media_data_obj.set_options_obj(options.OptionsManager())
        # Update the video catalogue to show the right icon
        self.video_catalogue_update_row(media_data_obj)

        # Open an edit window to show the options immediately
        config.OptionsEditWin(
            self.app_obj,
            media_data_obj.options_obj,
            media_data_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 9192 on_video_catalogue_check')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                235,
                'Callback request denied due to current conditions',
            )

        # Start a download operation
        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 9219 on_video_catalogue_check_multi')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                236,
                'Callback request denied due to current conditions',
            )

        # Start a download operation
        self.app_obj.download_manager_start('sim', False, media_data_list)

        # Standard de-selection of everything in the Video Catalogue
        self.catalogue_listbox.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 9249 on_video_catalogue_custom_dl')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                237,
                'Callback request denied due to current conditions',
            )

        # Start a custom download operation
        self.app_obj.download_manager_start('custom', False, [media_data_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 9276 on_video_catalogue_custom_dl_multi')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                238,
                'Callback request denied due to current conditions',
            )

        # Start a download operation
        self.app_obj.download_manager_start('custom', False, media_data_list)

        # Standard de-selection of everything in the Video Catalogue
        self.catalogue_listbox.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 9306 on_video_catalogue_delete_video')

        self.app_obj.delete_video(media_data_obj, True)


    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 9327 on_video_catalogue_delete_video_multi')

        for media_data_obj in media_data_list:
            self.app_obj.delete_video(media_data_obj, True)

        # Standard de-selection of everything in the Video Catalogue
        self.catalogue_listbox.unselect_all()


    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 9352 on_video_catalogue_dl_and_watch')

        # Can't download the video if it has no source, or if an update/
        #   refresh 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:

            # 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 9385 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 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:

            # 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.catalogue_listbox.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 9425 on_video_catalogue_download')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                239,
                'Callback request denied due to current conditions',
            )

        # Start a download operation
        self.app_obj.download_manager_start('real', False, [media_data_obj] )


    def on_video_catalogue_download_multi(self, menu_item, media_data_list):

        """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

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 9452 on_video_catalogue_download_multi')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                240,
                'Callback request denied due to current conditions',
            )

        # Start a download operation
        self.app_obj.download_manager_start('real', False, media_data_list)

        # Standard de-selection of everything in the Video Catalogue
        self.catalogue_listbox.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 9483 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(
                241,
                'Callback request denied due to current conditions',
            )

        # Open an edit window
        config.OptionsEditWin(
            self.app_obj,
            media_data_obj.options_obj,
            media_data_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 9515 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(
                242,
                '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)

        self.video_catalogue_update_row(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 9552 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)


    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 9579 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)


    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 9606 on_video_catalogue_mark_temp_dl')

        # Can't mark the video for download if it has no source, or if an
        #   update/refresh/tidy 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:

            # Create a new media.Video object in the 'Temporary Videos' folder
            #   (but don't download anything now)
            self.app_obj.add_video(
                self.app_obj.fixed_temp_folder,
                media_data_obj.source,
            )


    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 9642 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 mark the videos for download if they have no source, or if an
        #   update/refresh/tidy 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:

            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,
                )

        # Standard de-selection of everything in the Video Catalogue
        self.catalogue_listbox.unselect_all()


    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 9686 on_video_catalogue_page_entry_activated',
            )

        page_num = utils.strip_whitespace(entry.get_text())

        if self.video_index_current 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,
                int(page_num),
            )


    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 9722 on_video_catalogue_re_download')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                243,
                'Callback request denied due to current conditions',
            )

        # If the file exists, delete it (otherwise youtube-dl won't download
        #   anything)
        # Don't even check media.Video.dl_flag: the file might exist, even if
        #   the flag has not been set
        if media_data_obj.file_name:

            path = media_data_obj.get_actual_path(self.app_obj)

            if os.path.isfile(path):
                os.remove(path)

        # 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. Change
        #   the name of the archive file temporarily; after the download
        #   operation is complete, the file is give its original name
        self.app_obj.set_backup_archive(media_data_obj)

        # Now we're ready to start the download operation
        self.app_obj.download_manager_start('real', False, [media_data_obj] )


    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 9733 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(
                244,
                'Callback request denied due to current conditions',
            )

        # Remove download options from the media data object
        media_data_obj.set_options_obj(None)
        # Update the video catalogue to show the right icon
        self.video_catalogue_update_row(media_data_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 9802 on_video_catalogue_size_entry_activated',
            )

        size = utils.strip_whitespace(entry.get_text())

        if size.isdigit():
            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 is not None:
                self.video_catalogue_redraw_all(self.video_index_current, 1)

        else:
            # Invalid page size, so reinsert the size that's already visible
            entry.set_text(str(self.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 9837 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(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 9860 on_video_catalogue_show_properties')

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                245,
                '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 9889 on_video_catalogue_show_properties_multi',
            )

        if self.app_obj.current_manager_obj:
            return self.app_obj.system_error(
                246,
                '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.catalogue_listbox.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 9922 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_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 9953 on_video_catalogue_temp_dl')

        # Can't download the video if it has no source, or if an update/
        #   refresh/tidy 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:

            # 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:

                # 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 10004 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 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:

            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)

        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.catalogue_listbox.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 10060 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 (source != '' or 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 10113 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)

        self.video_catalogue_update_row(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 10144 on_video_catalogue_toggle_archived_video_multi',
            )

        for media_data_obj in media_data_list:
            media_data_obj.set_archive_flag(archived_flag)
            self.video_catalogue_update_row(media_data_obj)

        # Standard de-selection of everything in the Video Catalogue
        self.catalogue_listbox.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 10172 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 10201 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.catalogue_listbox.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 10228 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 10257 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.catalogue_listbox.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 10282 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 10310 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.catalogue_listbox.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 10337 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 10366 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.catalogue_listbox.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 10391 on_video_catalogue_watch_hooktube')

        # Launch the video
        utils.open_file(
            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 10421 on_video_catalogue_watch_invidious')

        # Launch the video
        utils.open_file(
            utils.convert_youtube_to_invidious(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 10452 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 10481 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.catalogue_listbox.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 10515 on_video_catalogue_watch_website')

        # Launch the video
        utils.open_file(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 10544 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(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.catalogue_listbox.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 10583 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.video_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),
            2,
            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 10630 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.video_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),
            2,
            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 10673 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),
            )

            iter = self.progress_list_liststore.get_iter(path)
            if iter is not None:
                self.progress_list_popup_menu(
                    event,
                    self.progress_list_liststore[iter][0],
                    self.progress_list_liststore[iter][1],
                )


    def on_progress_list_stop_now(self, menu_item, download_item_obj,
    worker_obj, video_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

            video_downloader_obj (downloads.VideoDownloader): The video
                downloader handling checking/downloading this media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 10722 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.video_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)
        video_downloader_obj.stop()


    def on_progress_list_stop_soon(self, menu_item, download_item_obj,
    worker_obj, video_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): The worker currently
                handling checking/downloading this media data object

            video_downloader_obj (downloads.VideoDownloader): The video
                downloader handling checking/downloading this media data object

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 10763 on_progress_list_stop_soon')

        # 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.video_downloader_obj is None:
            # Do nothing
            return

        # Tell the video downloader to stop after the current video check/
        #   download has finished
        video_downloader_obj.stop_soon()


    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 10796 on_progress_list_watch_hooktube')

        if isinstance(media_data_obj, media.Video):

            # Launch the video
            utils.open_file(
                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 10829 on_progress_list_watch_invidious')

        if isinstance(media_data_obj, media.Video):

            # Launch the video
            utils.open_file(
                utils.convert_youtube_to_invidious(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 10861 on_progress_list_watch_website')

        if isinstance(media_data_obj, media.Video) \
        and media_data_obj.source:

            utils.open_file(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 10887 on_results_list_delete_video')

        # Delete the video
        self.app_obj.delete_video(media_data_obj, True)

        # Remove the row from the Results List
        iter = self.results_list_liststore.get_iter(path)
        self.results_list_liststore.remove(iter)


    def on_results_list_right_click(self, treeview, event):

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

        When the user right-clicks an item 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 10913 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),
            )

            iter = self.results_list_liststore.get_iter(path)
            if iter is not None:
                self.results_list_popup_menu(
                    event,
                    path,
                    self.results_list_liststore[iter][0],
                )


    def on_errors_list_clear(self, button):

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

        In the Errors Tab, when the user clicks the 'Clear the list' button,
        clear the Errors List.

        Args:

            button (Gtk.Button): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 10953 on_errors_list_clear')

        self.errors_list_reset()


    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 10973 on_bandwidth_spinbutton_changed')

        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 10995 on_bandwidth_checkbutton_changed')

        self.app_obj.set_bandwidth_apply_flag(
            self.bandwidth_checkbutton.get_active(),
        )


    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 11018 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:

            # Allow the application to close as normal
            return False


    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 11048 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 11073 on_notebook_switch_page')

        self.visible_tab_num = page_num

        if page_num == 2:
            # 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 == 3 and not self.app_obj.system_msg_keep_totals_flag:
            # Update the tab's label
            self.tab_error_count = 0
            self.tab_warning_count = 0
            self.errors_list_refresh_label()


    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 11112 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_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 11135 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_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 11162 on_operation_error_checkbutton_changed',
            )

        self.app_obj.set_operation_error_show_flag(checkbutton.get_active())


    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 11183 on_operation_warning_checkbutton_changed',
            )

        self.app_obj.set_operation_warning_show_flag(checkbutton.get_active())


    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 11209 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_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 11228 on_reverse_results_checkbutton_changed',
            )

        self.app_obj.set_results_list_reverse_flag(checkbutton.get_active())


    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 11248 on_system_error_checkbutton_changed')

        self.app_obj.set_system_error_show_flag(checkbutton.get_active())


    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 11266 on_system_warning_checkbutton_changed')

        self.app_obj.set_system_warning_show_flag(checkbutton.get_active())


    def on_window_drag_data_received(self, widget, 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.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 11281 on_window_drag_data_received')

        if info == 0:
            text = data.get_text()
            if text is not None:

                # Hopefully, 'text' contains one or more valid URLs
                # Decide where to add this video. If a suitable folder is
                #   selected in the Video Index, use that; otherwise, use
                #   'Unsorted Videos'
                parent_obj = None
                if self.video_index_current is not None:
                    dbid \
                    = self.app_obj.media_name_dict[self.video_index_current]
                    parent_obj = self.app_obj.media_reg_dict[dbid]

                    if parent_obj.priv_flag:
                        parent_obj = None

                if not parent_obj:
                    parent_obj = self.app_obj.fixed_misc_folder

                # Split text into a list of lines and filter out invalid URLs
                video_list = []
                duplicate_list = []
                for line in text.split('\n'):

                    # Remove leading/trailing whitespace
                    line = utils.strip_whitespace(line)

                    # Perform checks on the URL. If it passes, remove leading/
                    #   trailing whitespace
                    if utils.check_url(line):
                        video_list.append(utils.strip_whitespace(line))

                    # Check everything in the list against other media.Video
                    #   objects with the same parent folder
                    for line in video_list:
                        if parent_obj.check_duplicate_video(line):
                            duplicate_list.append(line)
                        else:
                            self.app_obj.add_video(parent_obj, line)

                    # 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:

                        msg = 'The following videos are duplicates:'
                        for line in duplicate_list:
                            msg += '\n\n' + line

                        self.app_obj.dialogue_manager_obj.show_msg_dialogue(
                            msg,
                            'warning',
                            'ok',
                        )

        # Without this line, the user's cursor is permanently stuck in drag
        #   and drop mode
        context.finish(True, False, time)


    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 11362 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 11384 on_video_res_checkbutton_changed')

        self.app_obj.set_video_res_apply_flag(
            self.video_res_checkbutton.get_active(),
        )


    # 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 11407 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(
                247,
                'Callback request denied due to current conditions',
            )

        # Update the IV
        self.config_win_list.append(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 11435 del_child_window')

        # Update the IV
        # (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)


    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 11452 set_previous_alt_dest_dbid')

        self.previous_alt_dest_dbid = value


class SimpleCatalogueItem(object):

    """Called by MainWin.video_catalogue_redraw_all() and
    .video_catalogue_insert_item().

    Python class that handles a single row in the Video Catalogue.

    Each mainwin.SimpleCatalogueItem objects 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 11486 __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.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


        # 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


    # Public class methods


    def draw_widgets(self, catalogue_row):

        """Called by mainwin.MainWin.video_catalogue_redraw_all() and
        .video_catalogue_insert_item().

        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 11535 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)

        self.status_image = Gtk.Image()
        self.hbox.pack_start(
            self.status_image,
            False,
            False,
            self.spacing_size,
        )

        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)


    def update_widgets(self):

        """Called by mainwin.MainWin.video_catalogue_redraw_all(),
        .video_catalogue_update_row() and .video_catalogue_insert_item().

        Sets the values displayed by each widget.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 11587 update_widgets')

        self.update_tooltips()
        self.update_status_image()
        self.update_video_name()
        self.update_parent_name()
        self.update_video_stats()


    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 11604 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_image(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 11623 update_status_image')

        # Set the download status
        if 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'],
                )
            else:
                self.status_image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['have_file_small'],
                )
        else:
            self.status_image.set_from_pixbuf(
                self.main_win_obj.pixbuf_dict['no_file_small'],
            )


    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 11649 update_video_name')

        # For videos whose name is unknown, display the URL, rather than the
        #   usual '(video with no name)' string
        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_parent_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 11703 update_parent_name')

        if self.main_win_obj.app_obj.catalogue_mode != 'simple_show_parent':
            return

        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(string + 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 11735 update_video_stats')

        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 is not None:
            string = string + '  -  Size: ' + size
        else:
            string = string + '  -  Size: <i>unknown</i>'

        date = self.video_obj.get_upload_date_string(
            self.main_win_obj.app_obj.show_pretty_dates_flag,
        )

        if date is not None:
            string = string + '  -  Date: ' + date
        else:
            string = string + '  -  Date: <i>unknown</i>'

        self.stats_label.set_markup(string)


    # 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 11782 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_item().

    Python class that handles a single row in the Video Catalogue.

    Each mainwin.ComplexCatalogueItem objects 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 11818 __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_image = None             # Gtk.Image
        self.name_label = None              # Gtk.Label
        self.status_image = None            # Gtk.Image
        self.error_image = None             # Gtk.Image
        self.warning_image = None           # Gtk.Image
        self.descrip_label = None           # Gtk.Label
        self.expand_label = None            # Gtk.Label
        self.stats_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.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_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_new_label = None        # Gtk.Label
        self.marked_playlist_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


    # Public class methods


    def draw_widgets(self, catalogue_row):

        """Called by mainwin.MainWin.video_catalogue_redraw_all() and
        .video_catalogue_insert_item().

        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 11893 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)

        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
        vbox = Gtk.Box(
            orientation=Gtk.Orientation.VERTICAL,
            spacing=0,
        )
        hbox.pack_start(vbox, False, False, 0)

        self.thumb_image = Gtk.Image()
        vbox.pack_start(self.thumb_image, False, False, 0)

        # Everything to the right of the thumbnail is in vbox2
        vbox2 = Gtk.Box(
            orientation=Gtk.Orientation.VERTICAL,
            spacing=0,
        )
        hbox.pack_start(vbox2, True, True, self.spacing_size)

        # First row - video name
        hbox2 = Gtk.Box(
            orientation=Gtk.Orientation.HORIZONTAL,
            spacing=0,
        )
        vbox2.pack_start(hbox2, True, True, 0)

        self.name_label = Gtk.Label('', xalign = 0)
        hbox2.pack_start(self.name_label, True, True, 0)

        # Status/error/warning icons
        self.status_image = Gtk.Image()
        hbox2.pack_end(self.status_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, 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)
        vbox2.pack_start(self.descrip_label, True, True, 0)
        self.descrip_label.connect(
            'activate-link',
            self.on_click_descrip_label,
        )

        # Third row - video stats
        self.stats_label = Gtk.Label('', xalign=0)
        vbox2.pack_start(self.stats_label, True, True, 0)

        # Fourth row - Watch...
        hbox3 = Gtk.Box(
            orientation=Gtk.Orientation.HORIZONTAL,
            spacing=0,
        )
        vbox2.pack_start(hbox3, True, True, 0)

        self.watch_label = Gtk.Label('Watch:   ', xalign=0)
        hbox3.pack_start(self.watch_label, False, False, 0)

        # Watch in player
        self.watch_player_label = Gtk.Label('', xalign=0)
        hbox3.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)
        hbox3.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)
        hbox3.pack_start(self.watch_hooktube_label, False, False, 0)
        self.watch_hooktube_label.connect(
            'activate-link',
            self.on_click_watch_hooktube_label,
        )

        # Watch on Indvidious
        self.watch_invidious_label = Gtk.Label('', xalign=0)
        hbox3.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,
        )

        # Optional rows

        # Fifth row: Temporary...
        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.no_temp_widgets_flag:

            hbox4 = Gtk.Box(
                orientation=Gtk.Orientation.HORIZONTAL,
                spacing=0,
            )
            vbox2.pack_start(hbox4, True, True, 0)

            self.temp_label = Gtk.Label('Temporary:   ', xalign=0)
            hbox4.pack_start(self.temp_label, False, False, 0)

            # Mark for download
            self.temp_mark_label = Gtk.Label('', xalign=0)
            hbox4.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)
            hbox4.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)
            hbox4.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...
        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'
        ):
            hbox5 = Gtk.Box(
                orientation=Gtk.Orientation.HORIZONTAL,
                spacing=0,
            )
            vbox2.pack_start(hbox5, True, True, 0)

            self.marked_label = Gtk.Label('Marked:   ', xalign=0)
            hbox5.pack_start(self.marked_label, False, False, 0)

            # Archived/not archived
            self.marked_archive_label = Gtk.Label('', xalign=0)
            hbox5.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)
            hbox5.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)
            hbox5.pack_start(self.marked_fav_label, False, False, 0)
            self.marked_fav_label.connect(
                'activate-link',
                self.on_click_marked_fav_label,
            )

            # New/not new
            self.marked_new_label = Gtk.Label('', xalign=0)
            hbox5.pack_start(
                self.marked_new_label,
                False,
                False,
                (self.spacing_size * 2),
            )
            self.marked_new_label.connect(
                'activate-link',
                self.on_click_marked_new_label,
            )

            # In waiting list/not in waiting list
            self.marked_playlist_label = Gtk.Label('', xalign=0)
            hbox5.pack_start(self.marked_playlist_label, False, False, 0)
            self.marked_playlist_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_row() and .video_catalogue_insert_item().

        Sets the values displayed by each widget.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12150 update_widgets')

        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 (
            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:
            self.update_temp_labels()

        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'
        ):
            self.update_marked_labels()


    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 12186 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 12206 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
                thumb_flag = True
                self.thumb_image.set_from_pixbuf(
                    self.main_win_obj.app_obj.file_manager_obj.load_to_pixbuf(
                        path,
                        self.main_win_obj.thumb_width,
                        self.main_win_obj.thumb_height,
                   ),
                )

        # No thumbnail file found, so use a standard icon 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['video_both_large'],
                )
            elif self.video_obj.fav_flag:
                self.thumb_image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['video_left_large'],
                )
            elif self.video_obj.options_obj:
                self.thumb_image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['video_right_large'],
                )
            else:
                self.thumb_image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['video_none_large'],
                )


    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 12263 update_video_name')

        # For videos whose name is unknown, display the URL, rather than the
        #   usual '(video with no name)' string
        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 12317 update_status_images')

        # Set the download status
        if 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'],
                )
            else:
                self.status_image.set_from_pixbuf(
                    self.main_win_obj.pixbuf_dict['have_file_small'],
                )
        else:
            self.status_image.set_from_pixbuf(
                self.main_win_obj.pixbuf_dict['no_file_small'],
            )

        # Set an indication of any error/warning messages. If there is an error
        #   but no warning, show the error icon in the warning image (so there
        #   isn't a large gap in the middle)
        if self.video_obj.error_list and self.video_obj.warning_list:

            self.warning_image.set_from_pixbuf(
                self.main_win_obj.pixbuf_dict['warning_small'],
            )

            self.error_image.set_from_pixbuf(
                self.main_win_obj.pixbuf_dict['error_small'],
            )

        elif self.video_obj.error_list:

            self.warning_image.set_from_pixbuf(
                self.main_win_obj.pixbuf_dict['error_small'],
            )

            self.error_image.clear()

        elif self.video_obj.warning_list:

            self.warning_image.set_from_pixbuf(
                self.main_win_obj.pixbuf_dict['warning_small'],
            )

            self.error_image.clear()

        else:

            self.error_image.clear()
            self.warning_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 12378 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.split('\n')

                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">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">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 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 += html.escape(
                utils.shorten_string(
                    self.video_obj.parent_obj.name,
                    self.main_win_obj.very_long_string_max_len,
                ),
                quote=True,
            ) + '\''

            if not self.video_obj.descrip:
                self.descrip_label.set_text(string)

            elif not self.expand_descrip_flag:

                self.descrip_label.set_markup(
                    '<a href="more">More</a>   ' + string,
                )

            else:

                descrip = html.escape(self.video_obj.descrip, quote=True)
                self.descrip_label.set_markup(
                    '<a href="less">Less</a>   ' + string + '\n' + 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.
        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12469 update_video_stats')

        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 is not None:
            string = string + '  -  Size: ' + size
        else:
            string = string + '  -  Size: <i>unknown</i>'

        date = self.video_obj.get_upload_date_string(
            self.main_win_obj.app_obj.show_pretty_dates_flag,
        )

        if date is not None:
            string = string + '  -  Date: ' + date
        else:
            string = string + '  -  Date: <i>unknown</i>'

        self.stats_label.set_markup(string)


    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 12507 update_watch_player')

        if self.video_obj.file_name and self.video_obj.dl_flag:

            # Link clickable
            self.watch_player_label.set_markup(
                '<a href="' \
                + html.escape(
                    self.video_obj.get_actual_path(self.main_win_obj.app_obj),
                ) + '">Player</a>',
            )

        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:

            # Link clickable
            self.watch_player_label.set_markup(
                '<a href="' + html.escape(self.video_obj.source) \
                + '">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 12544 update_watch_web')

        if self.video_obj.source:

            # For YouTube URLs, offer alternative links
            source = self.video_obj.source
            if utils.is_youtube(source):

                # Links clickable
                self.watch_web_label.set_markup(
                    '<a href="' \
                    + html.escape(source, quote=True) \
                    + '">YouTube</a>',
                )

                self.watch_hooktube_label.set_markup(
                    '<a href="' \
                    + html.escape(
                        utils.convert_youtube_to_hooktube(source),
                        quote=True,
                    ) \
                    + '">HookTube</a>',
                )

                self.watch_invidious_label.set_markup(
                    '<a href="' \
                    + html.escape(
                        utils.convert_youtube_to_invidious(source),
                        quote=True,
                    ) \
                    + '">Invidious</a>',
                )

            else:

                self.watch_web_label.set_markup(
                    '<a href="' \
                    + html.escape(source, quote=True) \
                    + '">Website</a>',
                )

                self.watch_hooktube_label.set_text('')
                self.watch_invidious_label.set_text('')

        else:

            # Link not clickable
            self.watch_web_label.set_markup('<i>No weblink</i>')
            self.watch_hooktube_label.set_text('')
            self.watch_invidious_label.set_text('')


    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 12604 update_temp_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 = ''

        # (Video can't be temporarily downloaded if it has no source URL)
        if self.video_obj.source is not None:

            self.temp_mark_label.set_markup(
                '<a href="' + html.escape(link_text) \
                + '">Mark for download</a>',
            )

            self.temp_dl_label.set_markup(
                '<a href="' + html.escape(link_text) + '">Download</a>',
            )

            self.temp_dl_watch_label.set_markup(
                '<a href="' + html.escape(link_text) + '">D/L and 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 12646 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
        if not self.video_obj.archive_flag:

            self.marked_archive_label.set_markup(
                '<a href="' + html.escape(link_text) + '">Archived</a>',
            )

        else:

            self.marked_archive_label.set_markup(
                '<a href="' + html.escape(link_text) + '"><s>Archived</s></a>',
            )

        # Bookmarked/not bookmarked
        if not self.video_obj.bookmark_flag:

            self.marked_bookmark_label.set_markup(
                '<a href="' + html.escape(link_text) + '">Bookmarked</a>',
            )

        else:

            self.marked_bookmark_label.set_markup(
                '<a href="' + html.escape(link_text) \
                + '"><s>Bookmarked</s></a>',
            )

        # Favourite/not favourite
        if not self.video_obj.fav_flag:

            self.marked_fav_label.set_markup(
                '<a href="' + html.escape(link_text) + '">Favourite</a>',
            )

        else:

            self.marked_fav_label.set_markup(
                '<a href="' + html.escape(link_text) \
                + '"><s>Favourite</s></a>',
            )

        # New/not new
        if not self.video_obj.new_flag:

            self.marked_new_label.set_markup(
                '<a href="' + html.escape(link_text) + '">New</a>',
            )

        else:

            self.marked_new_label.set_markup(
                '<a href="' + html.escape(link_text) + '"><s>New</s></a>',
            )

        # In waiting list/not in waiting list
        if not self.video_obj.waiting_flag:

            self.marked_playlist_label.set_markup(
                '<a href="' + html.escape(link_text) + '">In waiting list</a>',
            )

        else:

            self.marked_playlist_label.set_markup(
                '<a href="' + html.escape(link_text) \
                + '"><s>In waiting list</s></a>',
            )


    # 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 12745 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_markup('')
        GObject.timeout_add(0, self.update_video_descrip)


    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

        Returns:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12779 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

        Returns:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12817 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('Not 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

        Returns:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12862 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_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

        Returns:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12907 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

        Returns:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12945 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_playlist_label.set_markup('Not in waiting list')

        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

        Returns:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 12990 on_click_temp_dl_label')

        # Can't download the video if an update/refresh/tidy 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:

            # 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

        Returns:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13044 on_click_temp_dl_watch_label')

        # Can't download the video if an update/refresh/tidy 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:

            # 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

        Returns:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13090 on_click_temp_mark_label')

        # Can't mark the video for download if an update/refresh/tidy 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:

            # 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

        Returns:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13141 on_click_watch_hooktube_label')

        # Launch the video
        utils.open_file(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

        Returns:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13185 on_click_watch_invidious_label')

        # Launch the video
        utils.open_file(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_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

        Returns:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13230 on_click_watch_player_label')

        if 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:

            # 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

        Returns:

            True to show the action has been handled

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13287 on_click_watch_web_label')

        # Launch the video
        utils.open_file(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)
        if utils.is_youtube(self.video_obj.source):
            self.watch_web_label.set_markup('YouTube')
        else:
            self.watch_web_label.set_markup('Website')

        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 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 13331 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 CatalogueRow(Gtk.ListBoxRow):

    """Called by MainWin.video_catalogue_redraw_all() and
    .video_catalogue_insert_item().

    Python class acting as a wrapper for Gtk.ListBoxRow, so that we can
    retrieve the media.Video object displayed in each row.

    Args:

        video_obj (media.Video): The video object displayed on this row

    """


    # Standard class methods


    def __init__(self, video_obj):

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

        super(Gtk.ListBoxRow, self).__init__()

        # IV list - class objects
        # -----------------------

        self.video_obj = video_obj


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 13388 __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 13423 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(__main__.__prettyname__)

        # 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 13447 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 13462 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 a
        download/update/refresh/info/tidy 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 13479 update_icon')

        if self.app_obj.download_manager_obj:
            if self.app_obj.download_manager_obj.operation_type == 'sim':
                icon = formats.STATUS_ICON_DICT['check_icon']
            else:
                icon = formats.STATUS_ICON_DICT['download_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']
        else:
            icon = formats.STATUS_ICON_DICT['default_icon']

        self.set_from_file(
            os.path.abspath(
                os.path.join(
                    self.app_obj.main_win_obj.icon_dir_path,
                    'status',
                    icon,
                ),
            )
        )


    # 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 13530 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 13557 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 13617 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 13636 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().

        Halts the current download operation

        Args:

            menu_item (Gtk.MenuItem): The menu item clicked

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 13655 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()


    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 13686 on_quit_menu_item')

        self.app_obj.stop()


# (Dialogue window classes)


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_name (str): The name 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_name=None,
    dl_sim_flag=False, monitor_flag=False):

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

        # 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 = []
        # 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, 2, 1)
        label2 = Gtk.Label()
        grid.attach(label2, 0, 1, 2, 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, 2, 1)
        self.entry.set_hexpand(True)

        label3 = Gtk.Label('Copy and paste a link to the channel')
        grid.attach(label3, 0, 3, 2, 1)

        self.entry2 = Gtk.Entry()
        grid.attach(self.entry2, 0, 4, 2, 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, 2, 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, 'Temporary Videos' is the first one in the
        #   list
        for name, dbid in main_win_obj.app_obj.media_name_dict.items():
            media_data_obj = main_win_obj.app_obj.media_reg_dict[dbid]

            if isinstance(media_data_obj, media.Folder) \
            and not media_data_obj.fixed_flag \
            and not media_data_obj.restrict_flag \
            and media_data_obj.get_depth() \
            < main_win_obj.app_obj.media_max_level \
            and (
                suggest_parent_name is None
                or suggest_parent_name != media_data_obj.name
            ):
                self.folder_list.append(media_data_obj.name)

        self.folder_list.sort()
        self.folder_list.insert(0, '')
        self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.name)

        if suggest_parent_name is not None:
            self.folder_list.insert(0, suggest_parent_name)

        label4 = Gtk.Label('(Optional) Add this channel inside a folder')
        grid.attach(label4, 0, 6, 2, 1)

        box = Gtk.Box()
        grid.attach(box, 0, 7, 1, 1)
        box.set_border_width(main_win_obj.spacing_size)

        image = Gtk.Image()
        box.add(image)
        image.set_from_pixbuf(main_win_obj.pixbuf_dict['folder_small'])

        listmodel = Gtk.ListStore(str)
        for item in self.folder_list:
            listmodel.append([item])

        combo = Gtk.ComboBox.new_with_model(listmodel)
        grid.attach(combo, 1, 7, 1, 1)
        combo.set_hexpand(True)

        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)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 8, 2, 1)

        self.radiobutton = Gtk.RadioButton.new_with_label_from_widget(
            None,
            'I want to download videos from this channel automatically',
        )
        grid.attach(self.radiobutton, 0, 9, 2, 1)

        self.radiobutton2 = Gtk.RadioButton.new_from_widget(self.radiobutton)
        grid.attach(self.radiobutton2, 0, 10, 2, 1)
        self.radiobutton2.set_label(
            'Don\'t download anything, just check for new videos',
        )
        if dl_sim_flag:
            self.radiobutton2.set_active(True)

        self.checkbutton = Gtk.CheckButton()
        grid.attach(self.checkbutton, 0, 11, 2, 1)
        self.checkbutton.set_label('Monitor the clipboard')
        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()


    # Public 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 13926 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 13958 on_combo_changed')

        self.parent_name = 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 13972 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(),
        )


    # (Callbacks)


    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 13996 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 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_name (str): The name 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_name=None):

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

        # 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 to display in the Gtk.ComboBox
        self.folder_list = []
        # The media.Folder selected in the combobox
        self.parent_name = 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, 2, 1)

        # (Store various widgets as IVs, so the calling function can retrieve
        #   their contents)
        self.entry = Gtk.Entry()
        grid.attach(self.entry, 0, 1, 2, 1)
        self.entry.set_hexpand(True)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 2, 2, 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, 'Temporary Videos' is the first one in the
        #   list
        for name, dbid in main_win_obj.app_obj.media_name_dict.items():
            media_data_obj = main_win_obj.app_obj.media_reg_dict[dbid]

            if isinstance(media_data_obj, media.Folder) \
            and not media_data_obj.fixed_flag \
            and not media_data_obj.restrict_flag \
            and media_data_obj.get_depth() \
            < main_win_obj.app_obj.media_max_level \
            and (
                suggest_parent_name is None
                or suggest_parent_name != media_data_obj.name
            ):
                self.folder_list.append(media_data_obj.name)

        self.folder_list.sort()
        self.folder_list.insert(0, '')
        self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.name)

        if suggest_parent_name is not None:
            self.folder_list.insert(0, suggest_parent_name)

        # Store the combobox's selected item, so the calling function can
        #   retrieve it.
        self.parent_name = self.folder_list[0]

        label4 = Gtk.Label(
            '(Optional) Add this folder inside another folder',
        )
        grid.attach(label4, 0, 3, 2, 1)

        box = Gtk.Box()
        grid.attach(box, 0, 4, 1, 1)
        box.set_border_width(main_win_obj.spacing_size)

        image = Gtk.Image()
        box.add(image)
        image.set_from_pixbuf(main_win_obj.pixbuf_dict['folder_small'])

        listmodel = Gtk.ListStore(str)
        for item in self.folder_list:
            listmodel.append([item])

        combo = Gtk.ComboBox.new_with_model(listmodel)
        grid.attach(combo, 1, 4, 1, 1)
        combo.set_hexpand(True)

        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)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 5, 2, 1)

        self.radiobutton = Gtk.RadioButton.new_with_label_from_widget(
            None,
            'I want to download videos from this folder automatically',
        )
        grid.attach(self.radiobutton, 0, 6, 2, 1)

        self.radiobutton2 = Gtk.RadioButton.new_from_widget(self.radiobutton)
        self.radiobutton2.set_label(
            'Don\'t download anything, just check for new videos',
        )
        grid.attach(self.radiobutton2, 0, 7, 2, 1)

        # Display the dialogue window
        self.show_all()


    # Public 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 14184 on_combo_changed')

        self.parent_name = 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_name (str): The name 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_name=None,
    dl_sim_flag=False, monitor_flag=False):

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

        # 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 = []
        # 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, 2, 1)
        label2 = Gtk.Label()
        grid.attach(label2, 0, 1, 2, 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, 2, 1)
        self.entry.set_hexpand(True)

        label3 = Gtk.Label('Copy and paste a link to the playlist')
        grid.attach(label3, 0, 3, 2, 1)

        self.entry2 = Gtk.Entry()
        grid.attach(self.entry2, 0, 4, 2, 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, 2, 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, 'Temporary Videos' is the first one in the
        #   list
        for name, dbid in main_win_obj.app_obj.media_name_dict.items():
            media_data_obj = main_win_obj.app_obj.media_reg_dict[dbid]

            if isinstance(media_data_obj, media.Folder) \
            and not media_data_obj.fixed_flag \
            and not media_data_obj.restrict_flag \
            and media_data_obj.get_depth() \
            < main_win_obj.app_obj.media_max_level \
            and (
                suggest_parent_name is None
                or suggest_parent_name != media_data_obj.name
            ):
                self.folder_list.append(media_data_obj.name)

        self.folder_list.sort()
        self.folder_list.insert(0, '')
        self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.name)

        if suggest_parent_name is not None:
            self.folder_list.insert(0, suggest_parent_name)

        label4 = Gtk.Label('(Optional) Add this playlist inside a folder')
        grid.attach(label4, 0, 6, 2, 1)

        box = Gtk.Box()
        grid.attach(box, 0, 7, 1, 1)
        box.set_border_width(main_win_obj.spacing_size)

        image = Gtk.Image()
        box.add(image)
        image.set_from_pixbuf(main_win_obj.pixbuf_dict['folder_small'])

        listmodel = Gtk.ListStore(str)
        for item in self.folder_list:
            listmodel.append([item])

        combo = Gtk.ComboBox.new_with_model(listmodel)
        grid.attach(combo, 1, 7, 1, 1)
        combo.set_hexpand(True)

        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)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 8, 2, 1)

        self.radiobutton = Gtk.RadioButton.new_with_label_from_widget(
            None,
            'I want to download videos from this playlist automatically',
        )
        grid.attach(self.radiobutton, 0, 9, 2, 1)

        self.radiobutton2 = Gtk.RadioButton.new_from_widget(self.radiobutton)
        grid.attach(self.radiobutton2, 0, 10, 2, 1)
        self.radiobutton2.set_label(
            'Don\'t download anything, just check for new videos',
        )
        if dl_sim_flag:
            self.radiobutton2.set_active(True)

        self.checkbutton = Gtk.CheckButton()
        grid.attach(self.checkbutton, 0, 11, 2, 1)
        self.checkbutton.set_label('Monitor the clipboard')
        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()


    # Public 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 14420 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 14452 on_combo_changed')

        self.parent_name = 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 14466 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(),
        )


    # (Callbacks)


    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 14490 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 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 14522 __init__')

        # 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 to display in the Gtk.ComboBox
        self.folder_list = []
        # The media.Folder selected in the combobox
        self.parent_name = 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, 2, 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, 2, 1)

        frame = Gtk.Frame()
        grid.attach(frame, 0, 2, 2, 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('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, 2, 1)

        # Prepare a list of folders to display in a combo. The list always
        #   includes the system folders 'Unsorted Videos' 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 = main_win_obj.video_index_current
        if selected:
            dbid = main_win_obj.app_obj.media_name_dict[selected]
            container_obj = main_win_obj.app_obj.media_reg_dict[dbid]
            if isinstance(container_obj, media.Folder) \
            and not container_obj.priv_flag:
                folder_obj = container_obj

        for name, dbid in main_win_obj.app_obj.media_name_dict.items():
            media_data_obj = main_win_obj.app_obj.media_reg_dict[dbid]

            if isinstance(media_data_obj, media.Folder) \
            and not media_data_obj.fixed_flag \
            and not media_data_obj.restrict_flag \
            and (folder_obj is None or media_data_obj != folder_obj):
                self.folder_list.append(media_data_obj.name)

        self.folder_list.sort()
        self.folder_list.insert(0, main_win_obj.app_obj.fixed_misc_folder.name)
        self.folder_list.insert(1, main_win_obj.app_obj.fixed_temp_folder.name)
        if folder_obj:
            self.folder_list.insert(0, folder_obj.name)

        # Store the combobox's selected item, so the calling function can
        #   retrieve it.
        self.parent_name = self.folder_list[0]

        label2 = Gtk.Label('Add the videos to this folder')
        grid.attach(label2, 0, 4, 2, 1)

        box = Gtk.Box()
        grid.attach(box, 0, 5, 1, 1)
        box.set_border_width(main_win_obj.spacing_size)

        image = Gtk.Image()
        box.add(image)
        image.set_from_pixbuf(main_win_obj.pixbuf_dict['folder_small'])

        listmodel = Gtk.ListStore(str)
        for item in self.folder_list:
            listmodel.append([item])

        combo = Gtk.ComboBox.new_with_model(listmodel)
        grid.attach(combo, 1, 5, 1, 1)
        combo.set_hexpand(True)

        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)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 6, 2, 1)

        self.radiobutton = Gtk.RadioButton.new_with_label_from_widget(
            None,
            'I want to download these videos automatically',
        )
        grid.attach(self.radiobutton, 0, 7, 2, 1)

        self.radiobutton2 = Gtk.RadioButton.new_from_widget(self.radiobutton)
        self.radiobutton2.set_label(
            'Don\'t download anything, just check the videos',
        )
        grid.attach(self.radiobutton2, 0, 8, 2, 1)

        self.checkbutton = Gtk.CheckButton()
        grid.attach(self.checkbutton, 0, 9, 2, 1)
        self.checkbutton.set_label('Monitor the clipboard')
        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()


    # Public 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 14746 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 14778 on_combo_changed')

        self.parent_name = 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 14792 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(),
        )


    # (Callbacks)


    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 14817 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 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): 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 14855 __init__')

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_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 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 14939 __init__')

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.button = None                      # Gtk.Button
        self.button2 = None                     # Gtk.Button

        # IV list - other
        # ---------------
        # Number of videos found in the container
        self.video_count = 0


        # Code
        # ----

        # Prepare variables
        pkg_string = __main__.__prettyname__
        media_type = media_data_obj.get_type()
        if media_type == 'video':
            return self.app_obj.system_error(
                248,
                '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:
            title = 'Delete ' + media_type
        else:
            title = 'Empty ' + media_type

        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(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('<b>' + media_data_obj.name + '</b>')

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 1, 1, 1)

        if not total_count:

            if media_type == 'folder':

                label2 = Gtk.Label(
                    'This ' + media_type + ' does not contain any videos,' \
                    + ' channels,\nplaylists or folders (but there might be' \
                    + ' some files\nin ' + pkg_string + '\'s data directory)',
                )

            else:
                label2 = Gtk.Label(
                    'This ' + media_type + ' does not contain any videos' \
                    + ' (but there might\nbe some files in ' + pkg_string \
                    + '\'s data directory)',
                )

            grid.attach(label2, 0, 2, 1, 5)
            label2.set_alignment(0, 0.5)

        else:

            label2 = Gtk.Label('This ' + media_type + ' contains:')
            grid.attach(label2, 0, 2, 1, 1)
            label2.set_alignment(0, 0.5)

            if folder_count == 1:
                label_string = '<b>1</b> folder'
            else:
                label_string = '<b>' + str(folder_count) + '</b> folders'

            label3 = Gtk.Label()
            grid.attach(label3, 0, 3, 1, 1)
            label3.set_markup(label_string)

            if channel_count == 1:
                label_string = '<b>1</b> channel'
            else:
                label_string = '<b>' + str(channel_count) + '</b> channels'

            label4 = Gtk.Label()
            grid.attach(label4, 0, 4, 1, 1)
            label4.set_markup(label_string)

            if playlist_count == 1:
                label_string = '<b>1</b> playlist'
            else:
                label_string = '<b>' + str(playlist_count) + '</b> playlists'

            label5 = Gtk.Label()
            grid.attach(label5, 0, 5, 1, 1)
            label5.set_markup(label_string)

            if self.video_count == 1:
                label_string = '<b>1</b> video'
            else:
                label_string = '<b>' + str(self.video_count) + '</b> videos'

            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:
            label7 = Gtk.Label(
                'Do you want to delete the ' + media_type + ' from ' \
                + pkg_string + '\'s data\ndirectory, deleting all of its' \
                + ' files, or do you just want to\nremove the ' + media_type \
                + ' from this list?',
            )
        else:
            label7 = Gtk.Label(
                'Do you want to empty the ' + media_type + ' in ' \
                + pkg_string + '\'s data\ndirectory, deleting all of its' \
                + ' files, or do you just want to\nempty the ' + media_type \
                + ' in this list?',
            )

        grid.attach(label7, 0, 8, 1, 1)
        label7.set_alignment(0, 0.5)

        if not empty_flag:
            self.button = Gtk.RadioButton.new_with_label_from_widget(
                None,
                'Just remove the ' + media_type + ' from this list',
            )
        else:
            self.button = Gtk.RadioButton.new_with_label_from_widget(
                None,
                'Just empty the ' + media_type + ' in this list',
            )

        grid.attach(self.button, 0, 9, 1, 1)

        self.button2 = Gtk.RadioButton.new_from_widget(self.button)
        self.button2.set_label(
            'Delete all files',
        )
        grid.attach(self.button2, 0, 10, 1, 1)

        # 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 15138 __init__')

        # 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


        # 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
        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 not whole_flag:
            msg = __main__.__prettyname__ \
            + ' is ready to export a partial summary of its\ndatabase,' \
            + ' containing a list of videos, channels,\nplaylists and/or' \
            + ' folders (but not including the\nvideos themselves)'
        else:
            msg = __main__.__prettyname__ \
            + ' is ready to export a summary of its database,\n' \
            + ' containing a list of videos, channels, playlists and/or\n' \
            + ' folders (but not including the videos themselves)'

        label = Gtk.Label(msg)
        grid.attach(label, 0, 0, 1, 1)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 1, 1, 1)

        label = Gtk.Label('Choose what should be included:')
        grid.attach(label, 0, 2, 1, 1)

        # (Store various widgets as IVs, so the calling function can retrieve
        #   their contents)
        self.checkbutton = Gtk.CheckButton()
        grid.attach(self.checkbutton, 0, 3, 1, 1)
        self.checkbutton.set_label('Include lists of videos')
        self.checkbutton.set_active(False)

        self.checkbutton2 = Gtk.CheckButton()
        grid.attach(self.checkbutton2, 0, 4, 1, 1)
        self.checkbutton2.set_label('Include channels')
        self.checkbutton2.set_active(True)

        self.checkbutton3 = Gtk.CheckButton()
        grid.attach(self.checkbutton3, 0, 5, 1, 1)
        self.checkbutton3.set_label('Include playlists')
        self.checkbutton3.set_active(True)

        self.checkbutton4 = Gtk.CheckButton()
        grid.attach(self.checkbutton4, 0, 6, 1, 1)
        self.checkbutton4.set_label('Preserve folder structure')
        self.checkbutton4.set_active(True)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 7, 1, 1)

        self.checkbutton5 = Gtk.CheckButton()
        grid.attach(self.checkbutton5, 0, 8, 1, 1)
        self.checkbutton5.set_label('Export as plain text')
        self.checkbutton5.set_active(False)
        self.checkbutton5.connect('toggled', self.on_checkbutton_toggled)

        # Display the dialogue window
        self.show_all()


    # Public class methods


    def on_checkbutton_toggled(self, checkbutton):

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

        When the specified checkbutton is toggled, modify other widgets in the
        dialogue window.

        Args:

            checkbutton (Gtk.CheckButton): The clicked widget

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 15251 on_checkbutton_toggled')

        if not checkbutton.get_active():
            self.checkbutton.set_sensitive(True)
            self.checkbutton4.set_sensitive(True)
        else:
            self.checkbutton.set_active(False)
            self.checkbutton.set_sensitive(False)
            self.checkbutton4.set_active(False)
            self.checkbutton4.set_sensitive(False)


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 15286 __init__')

        # 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)

        label = Gtk.Label('Choose which items to import')
        grid.attach(label, 0, 0, 4, 1)

        scrolled = Gtk.ScrolledWindow()
        grid.attach(scrolled, 0, 1, 4, 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(
            '',
            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(False)

        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('Deselect 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 textview 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 += '   [ ' + str(mini_dict['video_count']) + ' videos ]'

            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 15493 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
                #   textview
                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


    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 15556 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 15581 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 15603 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 MountDriveDialogue(Gtk.Dialog):

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

    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 15636 __init__')

        # 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_width = 2

        if os.name == 'nt':
            folder = 'folder'
        else:
            folder = 'directory'

        label = Gtk.Label(
            'The ' + __main__.__prettyname__ + ' data ' + folder \
            + ' is set to:',
        )
        grid.attach(label, 0, 0, grid_width, 1)

        label = Gtk.Label()
        grid.attach(label, 0, 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 ' + __main__.__prettyname__ \
                + ' cannot write to this ' + folder,
            )

        grid.attach(label2, 0, 2, grid_width, 1)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 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, 0, 4, grid_width, 1)

        self.radiobutton2 = Gtk.RadioButton.new_with_label_from_widget(
            self.radiobutton,
            'Use this data ' + folder + ':',
        )
        grid.attach(self.radiobutton2, 0, 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, 0, 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, 0, 7, grid_width, 1)

        self.radiobutton4 = Gtk.RadioButton.new_with_label_from_widget(
            self.radiobutton3,
            'Use the default data ' + folder,
        )
        grid.attach(self.radiobutton4, 0, 8, grid_width, 1)

        self.radiobutton5 = Gtk.RadioButton.new_with_label_from_widget(
            self.radiobutton4,
            'Shut down ' + __main__.__prettyname__,
        )
        grid.attach(self.radiobutton5, 0, 9, grid_width, 1)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 10, grid_width, 1)

        button = Gtk.Button.new_with_label('Cancel')
        grid.attach(button, 0, 11, 1, 1)
        button.connect('clicked', self.on_cancel_button_clicked)

        button2 = Gtk.Button.new_with_label('OK')
        grid.attach(button2, 1, 11, 1, 1)
        button2.connect('clicked', self.on_ok_button_clicked)

        # Display the dialogue window
        self.show_all()


    # Public class methods


    # (Callbacks)


    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 15801 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 15842 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 15862 on_radiobutton_toggled')

        if button.get_active():
            self.combo.set_sensitive(True)
        else:
            self.combo.set_sensitive(False)


    # (Callback support functions)


    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 15881 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
            if os.name == 'nt':
                folder = 'folder'
            else:
                folder = 'directory'


            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 15919 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()


class RemoveLockFileDialogue(Gtk.Dialog):

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

    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

    """


    # Standard class methods


    def __init__(self, main_win_obj):

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

        # 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
        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(
            'Failed to load the ' + __main__.__prettyname__ \
            + ' database file, because\nanother instance of ' \
            + __main__.__prettyname__ + ' seems to be using it' \
            + '\n\nIf you are SURE that this is the only instance of\n' \
            + __main__.__prettyname__ + ' running on your system,' \
            + ' click \'Yes\' to\nremove the protection (and then' \
            + ' restart ' + __main__.__prettyname__ + ')' \
            + '\n\nIf you are not sure, then click \'No\'',
        )
        grid.attach(label, 0, 0, grid_width, 1)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 1, grid_width, 1)

        button = Gtk.Button.new_with_label(
            'Yes, I\'m sure',
        )
        grid.attach(button, 0, 2, 1, 1)
        button.set_hexpand(True)
        button.connect('clicked', self.on_yes_button_clicked)

        button2 = Gtk.Button.new_with_label(
            'No, I\'m not sure',
        )
        grid.attach(button2, 0, 3, 1, 1)
        button2.set_hexpand(True)
        button2.connect('clicked', self.on_no_button_clicked)

        # Display the dialogue window
        self.show_all()


    # Public 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 16033 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 16053 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 16082 __init__')

        # 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()

        Gtk.Dialog.__init__(
            self,
            'Rename ' + media_type,
            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(
            'Set the new name for the ' + media_type + ' \'' \
            + media_data_obj.name \
            + '\'\n\nNB This procedure will make changes to your filesystem!',
        )
        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.name)

        # Display the dialogue window
        self.show_all()


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 16161 __init__')

        # 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.main_win_obj = main_win_obj
        self.media_data_obj = media_data_obj
        # Store the user's choice as an IV, so the calling function can
        #   retrieve it
        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
        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 os.name == 'nt':
            dir_name = 'folder'
        else:
            dir_name = 'directory'

        media_type = media_data_obj.get_type()

        label = Gtk.Label(
            'This ' + media_type + ' can store its videos in its own ' \
            + dir_name + ', or it can store\nthem in a different ' \
            + dir_name \
            + '\n\nChoose a different ' + dir_name + ' if:' \
            + '\n\n1. You want to add a channel and its playlists, without' \
            + ' downloading\nthe same video twice' \
            + '\n\n2. A video creator has channels on both YouTube and' \
            + ' BitChute, and\nyou want to add both without downloading the' \
            + ' same video twice',
        )
        grid.attach(label, 0, 0, 1, 1)

        # Separator
        grid.attach(Gtk.HSeparator(), 0, 1, 1, 1)

        radiobutton = Gtk.RadioButton.new_with_label_from_widget(
            None,
            'Use this ' + media_type + '\'s own ' + dir_name,
        )
        grid.attach(radiobutton, 0, 2, 1, 1)
        # Signal connect appears below

        radiobutton2 = Gtk.RadioButton.new_from_widget(radiobutton)
        radiobutton2.set_label('Choose a different ' + dir_name + ':')
        grid.attach(radiobutton2, 0, 3, 1, 1)
        # Signal connect appears below

        # Get a list of channels/playlists/folders
        app_obj = main_win_obj.app_obj
        dbid_list = list(app_obj.media_name_dict.values())

        # 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
        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)

        # 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)
        #   - The most recently-selected alternative download destination, if
        #       any
        #   - media_data_obj itself
        mod_dbid_list = []
        for this_dbid in dbid_list:

            this_obj = app_obj.media_reg_dict[this_dbid]

            if this_dbid != media_data_obj.dbid \
            and (prev_dbid is None or prev_dbid != this_dbid) \
            and this_obj.dbid == this_obj.master_dbid:
                mod_dbid_list.append(this_dbid)

        # Sort the modified list...
        name_list = []
        for this_dbid in mod_dbid_list:
            this_obj = app_obj.media_reg_dict[this_dbid]
            name_list.append(this_obj.name)

        name_list.sort(key=lambda x: x.lower())

        # ...and then add the previous destination, and the media data object
        #   itself, at the top of it
        name_list.insert(0, media_data_obj.name)

        if prev_dbid is not None:
            prev_obj = app_obj.media_reg_dict[prev_dbid]
            name_list.insert(0, prev_obj.name)

        # Add a combo
        store = Gtk.ListStore(GdkPixbuf.Pixbuf, str)

        count = -1

        for name in name_list:
            dbid = app_obj.media_name_dict[name]
            obj = app_obj.media_reg_dict[dbid]

            if isinstance(obj, media.Channel):
                icon_name = 'channel_small'
            elif isinstance(obj, media.Playlist):
                icon_name = 'playlist_small'
            else:
                icon_name = 'folder_small'

            store.append( [main_win_obj.pixbuf_dict[icon_name], name] )

            count += 1

        combo = Gtk.ComboBox.new_with_model(store)
        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, True)
        combo.add_attribute(renderer_text, 'text', 1)

        combo.set_active(0)
        # Signal connect appears below

        if media_data_obj.master_dbid == media_data_obj.dbid:
            combo.set_sensitive(False)
        else:
            radiobutton2.set_active(True)
            combo.set_sensitive(True)

        # Signal connects from above
        radiobutton.connect(
            'toggled',
            self.on_radiobutton_toggled,
            combo,
        )

        radiobutton2.connect(
            'toggled',
            self.on_radiobutton2_toggled,
            combo,
        )

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

        # Display the dialogue window
        self.show_all()


    def on_combo_changed(self, combo, radiobutton2):

        """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 16361 on_combo_changed')

        tree_iter = combo.get_active_iter()
        model = combo.get_model()
        pixbuf, name = model[tree_iter][:2]

        # (Allow for the possibility that the media data object might have
        #   been deleted, since the dialogue window opened)
        if name in self.main_win_obj.app_obj.media_name_dict:
            dbid = self.main_win_obj.app_obj.media_name_dict[name]
            obj = self.main_win_obj.app_obj.media_reg_dict[dbid]
            self.choice = obj.dbid

            if not radiobutton2.get_active():
                self.main_win_obj.set_previous_alt_dest_dbid(None)
            else:
                self.main_win_obj.set_previous_alt_dest_dbid(obj.dbid)


    def on_radiobutton_toggled(self, radiobutton, combo):

        """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): The widget containing the user's choice

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16397 on_radiobutton_toggled')

        if radiobutton.get_active():
            combo.set_sensitive(False)
            self.choice = self.media_data_obj.dbid

            self.main_win_obj.set_previous_alt_dest_dbid(None)


    def on_radiobutton2_toggled(self, radiobutton2, combo):

        """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

        """

        if DEBUG_FUNC_FLAG:
            utils.debug_time('mwn 16423 on_radiobutton2_toggled')

        if radiobutton2.get_active():
            combo.set_sensitive(True)

            tree_iter = combo.get_active_iter()
            model = combo.get_model()
            pixbuf, name = model[tree_iter][:2]

            # (Allow for the possibility that the media data object might have
            #   been deleted, since the dialogue window opened)
            if name in self.main_win_obj.app_obj.media_name_dict:
                dbid = self.main_win_obj.app_obj.media_name_dict[name]
                obj = self.main_win_obj.app_obj.media_reg_dict[dbid]
                self.choice = obj.dbid

                self.main_win_obj.set_previous_alt_dest_dbid(dbid)


class SetDirectoryDialogue_LinuxBSD(Gtk.Dialog):

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

    Python class handling a dialogue window that prompts the user to set the
    directory used as Tartube's data directory.

    Only used after a new installation on Linux/BSD.

    Args:

        main_win_obj (mainwin.MainWin): The parent main window

        default_dir (str): The path to the default data directory, which is the
            current value of mainapp.TartubeApp.data_dir

    """


    # Standard class methods


    def __init__(self, main_win_obj, default_dir):

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

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # IV list - Gtk widgets
        # ---------------------
        self.button = None                      # Gtk.Button
        self.button2 = None                     # Gtk.Button


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            'Welcome to ' + __main__.__prettyname__ + '!',
            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 * 2)

        image = Gtk.Image.new_from_pixbuf(
            main_win_obj.pixbuf_dict['system_icon'],
        )
        grid.attach(image, 0, 0, 1, 3)

        if os.name == 'nt':
            folder = 'folder'
        else:
            folder = 'directory'

        label = Gtk.Label()
        grid.attach(label, 1, 0, 1, 1)
        label.set_markup(
            __main__.__prettyname__ + '\'s data ' + folder \
            + ' will be:\n\n<b>' \
            + html.escape(
                utils.tidy_up_long_string(default_dir, 50, True, True),
            ) + '</b>\n',
        )

        # (Store various widgets as IVs, so the calling function can retrieve
        #   their contents)
        self.button = Gtk.RadioButton.new_with_label_from_widget(
            None,
            'Use this ' + folder
        )
        grid.attach(self.button, 1, 1, 1, 1)

        self.button2 = Gtk.RadioButton.new_from_widget(self.button)
        self.button2.set_label('Choose a different ' + folder)
        grid.attach(self.button2, 1, 2, 1, 1)

        # Display the dialogue window
        self.show_all()


class SetDirectoryDialogue_MSWin(Gtk.Dialog):

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

    Python class handling a dialogue window that prompts the user to set the
    directory used as Tartube's data directory.

    Only used after a new installation on MS Windows.

    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 16563 __init__')

        # IV list - class objects
        # -----------------------
        # Tartube's main window
        self.main_win_obj = main_win_obj


        # Code
        # ----

        Gtk.Dialog.__init__(
            self,
            'Welcome to ' + __main__.__prettyname__ + '!',
            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 * 2)

        image = Gtk.Image.new_from_pixbuf(
            main_win_obj.pixbuf_dict['system_icon'],
        )
        grid.attach(image, 0, 0, 1, 1)

        if os.name == 'nt':
            folder = 'folder'
        else:
            folder = 'directory'

        line_list = [
            'Click OK to create a ' + folder + ' in which ' \
            + __main__.__prettyname__ + ' can store its videos',
            'If you have used ' + __main__.__prettyname__ + ' before,' \
            + ' you can select an existing ' + folder + ' instead of' \
            + ' creating a new one',
        ]

        newline = '\n\n'
        line_list = [
            utils.tidy_up_long_string(
                'Click OK to create a ' + folder + ' in which ' \
                + __main__.__prettyname__ + ' can store its videos',
                40,
            ),
            utils.tidy_up_long_string(
                'If you have used ' + __main__.__prettyname__ + ' before,' \
                + ' you can select an existing ' + folder + ' instead of' \
                + ' creating a new one',
                40,
            ),
        ]

        label = Gtk.Label(newline.join(line_list))
        grid.attach(label, 1, 0, 1, 1)

        # Display the dialogue window
        self.show_all()


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 16658 __init__')

        # 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
        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)

        media_type = media_data_obj.get_type()
        label = Gtk.Label(
            'Set the nickname for the ' + media_type + ' \'' \
            + media_data_obj.name \
            + '\'\n(or leave it blank to reset the nickname)',
        )
        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 SystemCmdDialogue(Gtk.Dialog):

    """Called by 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 16738 __init__')

        # 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(
                upper(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

        Returns:

            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 16847 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_system_cmd(
            self.main_win_obj.app_obj,
            media_data_obj,
            options_list,
        )

        # Display it in the textbuffer
        if cmd_list:
            char = ' '
            system_cmd = char.join(cmd_list)

        else:
            system_cmd = ''


        self.textbuffer.set_text(system_cmd)
        return system_cmd


    # (Callbacks)


    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 16904 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 16932 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 16964 __init__')

        # 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 youtube-dl',
            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)

        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(
            'youtube-dl 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 17063 __init__')

        # 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


        # 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
        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)

        # Left column
        self.checkbutton = Gtk.CheckButton()
        grid.attach(self.checkbutton, 0, 0, 1, 1)
        self.checkbutton.set_label('Check that videos are not corrupted')
        self.checkbutton.connect('toggled', self.on_checkbutton_toggled)

        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, 2)
        self.checkbutton4.set_label(
            'Delete downloaded video files (doesn\'t\nremove videos from ' \
            + utils.upper_case_first(__main__.__packagename__) \
            + '\'s database)',
        )
        self.checkbutton4.connect('toggled', self.on_checkbutton4_toggled)

        self.checkbutton5 = Gtk.CheckButton()
        grid.attach(self.checkbutton5, 0, 5, 1, 1)
        self.checkbutton5.set_label(
            'Also delete all video/audio files with the\nsame name',
        )
        self.checkbutton5.set_sensitive(False)

        # Right column
        self.checkbutton6 = Gtk.CheckButton()
        grid.attach(self.checkbutton6, 1, 0, 1, 1)
        self.checkbutton6.set_label('Delete all description files')

        self.checkbutton7 = Gtk.CheckButton()
        grid.attach(self.checkbutton7, 1, 1, 1, 1)
        self.checkbutton7.set_label('Delete all metadata (JSON) files')

        self.checkbutton8 = Gtk.CheckButton()
        grid.attach(self.checkbutton8, 1, 2, 1, 1)
        self.checkbutton8.set_label('Delete all annotation files')

        self.checkbutton9 = Gtk.CheckButton()
        grid.attach(self.checkbutton9, 1, 3, 1, 1)
        self.checkbutton9.set_label('Delete all thumbnail files')

        self.checkbutton10 = Gtk.CheckButton()
        grid.attach(self.checkbutton10, 1, 4, 1, 1)
        self.checkbutton10.set_label('Delete all youtube-dl archive files')

        # Bottom strip

        button = Gtk.Button.new_with_label('Select all')
        grid.attach(button, 0, 6, 1, 1)
        button.set_hexpand(False)
        button.connect('clicked', self.on_select_all_clicked)

        button = Gtk.Button.new_with_label('Select none')
        grid.attach(button, 1, 6, 1, 1)
        button.set_hexpand(False)
        button.connect('clicked', self.on_select_none_clicked)

        # Display the dialogue window
        self.show_all()


    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 17202 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 17226 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_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 17249 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(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 17276 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)
