#
# Copyright (C) 2007, 2008 Andrew Resch <andrewresch@gmail.com>
#
# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#

"""The torrent view component that lists all torrents in the session."""

import logging
from locale import strcoll

from gi.repository.Gdk import ModifierType, keyval_name
from gi.repository.GLib import idle_add
from gi.repository.GObject import TYPE_UINT64
from gi.repository.Gtk import EntryIconPosition
from twisted.internet import reactor

import deluge.component as component
from deluge.common import decode_bytes
from deluge.ui.client import client

from . import torrentview_data_funcs as funcs
from .common import cmp
from .listview import ListView
from .removetorrentdialog import RemoveTorrentDialog

log = logging.getLogger(__name__)

try:
    CTRL_ALT_MASK = ModifierType.CONTROL_MASK | ModifierType.MOD1_MASK
except TypeError:
    # Sphinx AutoDoc has a mock issue with Gdk masks.
    pass


def str_nocase_sort(model, iter1, iter2, data):
    """Sort string column data using ISO 14651 in lowercase.

    Uses locale.strcoll which (allegedly) uses ISO 14651. Compares first
    value with second and returns -1, 0, 1 for where it should be placed.

    """
    v1 = model[iter1][data]
    v2 = model[iter2][data]
    # Catch any values of None from model.
    v1 = v1.lower() if v1 else ''
    v2 = v2.lower() if v2 else ''
    return strcoll(v1, v2)


def queue_peer_seed_sort_function(v1, v2):
    if v1 == v2:
        return 0
    if v2 < 0:
        return -1
    if v1 < 0:
        return 1
    if v1 > v2:
        return 1
    if v2 > v1:
        return -1


def queue_column_sort(model, iter1, iter2, data):
    v1 = model[iter1][data]
    v2 = model[iter2][data]
    return queue_peer_seed_sort_function(v1, v2)


def eta_column_sort(model, iter1, iter2, data):
    v1 = model[iter1][data]
    v2 = model[iter2][data]
    if v1 == v2:
        return 0
    if v1 == 0:
        return -1
    if v2 == 0:
        return 1
    if v1 > v2:
        return -1
    if v2 > v1:
        return 1


def seed_peer_column_sort(model, iter1, iter2, data):
    v1 = model[iter1][data]  # num seeds/peers
    v3 = model[iter2][data]  # num seeds/peers
    if v1 == v3:
        v2 = model[iter1][data + 1]  # total seeds/peers
        v4 = model[iter2][data + 1]  # total seeds/peers
        return queue_peer_seed_sort_function(v2, v4)
    return queue_peer_seed_sort_function(v1, v3)


def progress_sort(model, iter1, iter2, sort_column_id):
    progress1 = model[iter1][sort_column_id]
    progress2 = model[iter2][sort_column_id]
    # Progress value is equal, so sort on state
    if progress1 == progress2:
        state1 = model[iter1][sort_column_id + 1]
        state2 = model[iter2][sort_column_id + 1]
        return cmp(state1, state2)
    return cmp(progress1, progress2)


class SearchBox:
    def __init__(self, torrentview):
        self.torrentview = torrentview
        mainwindow = component.get('MainWindow')
        main_builder = mainwindow.get_builder()

        self.visible = False
        self.search_pending = self.prefiltered = None

        self.search_box = main_builder.get_object('search_box')
        self.search_torrents_entry = main_builder.get_object('search_torrents_entry')
        self.close_search_button = main_builder.get_object('close_search_button')
        self.match_search_button = main_builder.get_object('search_torrents_match')
        mainwindow.connect_signals(self)

    def show(self):
        self.visible = True
        self.search_box.show_all()
        self.search_torrents_entry.grab_focus()

    def hide(self):
        self.visible = False
        self.clear_search()
        self.search_box.hide()
        self.search_pending = self.prefiltered = None

    def clear_search(self):
        if self.search_pending and self.search_pending.active():
            self.search_pending.cancel()

        if self.prefiltered:
            filter_column = self.torrentview.columns['filter'].column_indices[0]
            torrent_id_column = self.torrentview.columns['torrent_id'].column_indices[0]
            for row in self.torrentview.liststore:
                torrent_id = row[torrent_id_column]

                if torrent_id in self.prefiltered:
                    # Reset to previous filter state
                    self.prefiltered.pop(self.prefiltered.index(torrent_id))
                    row[filter_column] = not row[filter_column]

        self.prefiltered = None

        self.search_torrents_entry.set_text('')
        if self.torrentview.filter and 'name' in self.torrentview.filter:
            self.torrentview.filter.pop('name', None)
            self.search_pending = reactor.callLater(0.5, self.torrentview.update)

    def set_search_filter(self):
        if self.search_pending and self.search_pending.active():
            self.search_pending.cancel()

        if self.torrentview.filter and 'name' in self.torrentview.filter:
            self.torrentview.filter.pop('name', None)

        elif self.torrentview.filter is None:
            self.torrentview.filter = {}

        search_string = self.search_torrents_entry.get_text()
        if not search_string:
            self.clear_search()
        else:
            if self.match_search_button.get_active():
                search_string += '::match'
            self.torrentview.filter['name'] = search_string
        self.prefilter_torrentview()

    def prefilter_torrentview(self):
        filter_column = self.torrentview.columns['filter'].column_indices[0]
        torrent_id_column = self.torrentview.columns['torrent_id'].column_indices[0]
        torrent_name_column = self.torrentview.columns[_('Name')].column_indices[1]

        match_case = self.match_search_button.get_active()
        if match_case:
            search_string = self.search_torrents_entry.get_text()
        else:
            search_string = self.search_torrents_entry.get_text().lower()

        if self.prefiltered is None:
            self.prefiltered = []

        for row in self.torrentview.liststore:
            torrent_id = row[torrent_id_column]

            if torrent_id in self.prefiltered:
                # Reset to previous filter state
                self.prefiltered.pop(self.prefiltered.index(torrent_id))
                row[filter_column] = not row[filter_column]

            if not row[filter_column]:
                # Row is not visible(filtered out, but not by our filter), skip it
                continue

            if match_case:
                torrent_name = row[torrent_name_column]
            else:
                torrent_name = row[torrent_name_column].lower()

            if search_string in torrent_name and not row[filter_column]:
                row[filter_column] = True
                self.prefiltered.append(torrent_id)
            elif search_string not in torrent_name and row[filter_column]:
                row[filter_column] = False
                self.prefiltered.append(torrent_id)

    def on_close_search_button_clicked(self, widget):
        self.hide()

    def on_search_filter_toggle(self, widget):
        if self.visible:
            self.hide()
        else:
            self.show()

    def on_search_torrents_match_toggled(self, widget):
        if self.search_torrents_entry.get_text():
            self.set_search_filter()
            self.search_pending = reactor.callLater(0.7, self.torrentview.update)

    def on_search_torrents_entry_icon_press(self, entry, icon, event):
        if icon != EntryIconPosition.SECONDARY:
            return
        self.clear_search()

    def on_search_torrents_entry_changed(self, widget):
        self.set_search_filter()
        self.search_pending = reactor.callLater(0.7, self.torrentview.update)


class TorrentView(ListView, component.Component):
    """TorrentView handles the listing of torrents."""

    def __init__(self):
        component.Component.__init__(
            self, 'TorrentView', interval=2, depend=['SessionProxy']
        )
        main_builder = component.get('MainWindow').get_builder()
        # Call the ListView constructor
        ListView.__init__(
            self, main_builder.get_object('torrent_view'), 'torrentview.state'
        )
        log.debug('TorrentView Init..')

        # If we have gotten the state yet
        self.got_state = False

        # This is where status updates are put
        self.status = {}

        # We keep a copy of the previous status to compare for changes
        self.prev_status = {}

        # Register the columns menu with the listview so it gets updated accordingly.
        self.register_checklist_menu(main_builder.get_object('menu_columns'))

        # Add the columns to the listview
        self.add_text_column('torrent_id', hidden=True, unique=True)
        self.add_bool_column('dirty', hidden=True)
        self.add_func_column(
            '#',
            funcs.cell_data_queue,
            [int],
            status_field=['queue'],
            sort_func=queue_column_sort,
        )
        self.add_texticon_column(
            _('Name'),
            status_field=['state', 'name'],
            function=funcs.cell_data_statusicon,
            sort_func=str_nocase_sort,
            default_sort=True,
        )
        self.add_func_column(
            _('Size'),
            funcs.cell_data_size,
            [TYPE_UINT64],
            status_field=['total_wanted'],
        )
        self.add_func_column(
            _('Downloaded'),
            funcs.cell_data_size,
            [TYPE_UINT64],
            status_field=['all_time_download'],
            default=False,
        )
        self.add_func_column(
            _('Uploaded'),
            funcs.cell_data_size,
            [TYPE_UINT64],
            status_field=['total_uploaded'],
            default=False,
        )
        self.add_func_column(
            _('Remaining'),
            funcs.cell_data_size,
            [TYPE_UINT64],
            status_field=['total_remaining'],
            default=False,
        )
        self.add_progress_column(
            _('Progress'),
            status_field=['progress', 'state'],
            col_types=[float, str],
            function=funcs.cell_data_progress,
            sort_func=progress_sort,
        )
        self.add_func_column(
            _('Seeds'),
            funcs.cell_data_peer,
            [int, int],
            status_field=['num_seeds', 'total_seeds'],
            sort_func=seed_peer_column_sort,
            default=False,
        )
        self.add_func_column(
            _('Peers'),
            funcs.cell_data_peer,
            [int, int],
            status_field=['num_peers', 'total_peers'],
            sort_func=seed_peer_column_sort,
            default=False,
        )
        self.add_func_column(
            _('Seeds:Peers'),
            funcs.cell_data_ratio_seeds_peers,
            [float],
            status_field=['seeds_peers_ratio'],
            default=False,
        )
        self.add_func_column(
            _('Down Speed'),
            funcs.cell_data_speed_down,
            [int],
            status_field=['download_payload_rate'],
        )
        self.add_func_column(
            _('Up Speed'),
            funcs.cell_data_speed_up,
            [int],
            status_field=['upload_payload_rate'],
        )
        self.add_func_column(
            _('Down Limit'),
            funcs.cell_data_speed_limit_down,
            [float],
            status_field=['max_download_speed'],
            default=False,
        )
        self.add_func_column(
            _('Up Limit'),
            funcs.cell_data_speed_limit_up,
            [float],
            status_field=['max_upload_speed'],
            default=False,
        )
        self.add_func_column(
            _('ETA'),
            funcs.cell_data_time,
            [int],
            status_field=['eta'],
            sort_func=eta_column_sort,
        )
        self.add_func_column(
            _('Ratio'),
            funcs.cell_data_ratio_ratio,
            [float],
            status_field=['ratio'],
            default=False,
        )
        self.add_func_column(
            _('Avail'),
            funcs.cell_data_ratio_avail,
            [float],
            status_field=['distributed_copies'],
            default=False,
        )
        self.add_func_column(
            _('Added'),
            funcs.cell_data_date_added,
            [int],
            status_field=['time_added'],
            default=False,
        )
        self.add_func_column(
            _('Completed'),
            funcs.cell_data_date_completed,
            [int],
            status_field=['completed_time'],
            default=False,
        )
        self.add_func_column(
            _('Complete Seen'),
            funcs.cell_data_date_or_never,
            [int],
            status_field=['last_seen_complete'],
            default=False,
        )
        self.add_func_column(
            _('Last Transfer'),
            funcs.cell_data_time,
            [int],
            status_field=['time_since_transfer'],
            default=False,
        )
        self.add_texticon_column(
            _('Tracker'),
            function=funcs.cell_data_trackericon,
            status_field=['tracker_host', 'tracker_host'],
            default=False,
        )
        self.add_text_column(
            _('Download Folder'), status_field=['download_location'], default=False
        )
        self.add_text_column(_('Owner'), status_field=['owner'], default=False)
        self.add_bool_column(
            _('Shared'),
            status_field=['shared'],
            default=False,
            tooltip=_('Torrent is shared between other Deluge users or not.'),
        )
        self.restore_columns_order_from_state()

        # Set filter to None for now
        self.filter = None

        # Connect Signals #
        # Connect to the 'button-press-event' to know when to bring up the
        # torrent menu popup.
        self.treeview.connect('button-press-event', self.on_button_press_event)
        # Connect to the 'key-press-event' to know when the bring up the
        # torrent menu popup via keypress.
        self.treeview.connect('key-release-event', self.on_key_press_event)
        # Connect to the 'changed' event of TreeViewSelection to get selection
        # changes.
        self.treeview.get_selection().connect('changed', self.on_selection_changed)

        self.treeview.connect('drag-drop', self.on_drag_drop)
        self.treeview.connect('drag_data_received', self.on_drag_data_received)
        self.treeview.connect('key-press-event', self.on_key_press_event)
        self.treeview.connect('columns-changed', self.on_columns_changed_event)

        self.search_box = SearchBox(self)
        self.permanent_status_keys = ['owner']
        self.columns_to_update = []

    def start(self):
        """Start the torrentview"""
        # We need to get the core session state to know which torrents are in
        # the session so we can add them to our list.
        # Only get the status fields required for the visible columns
        status_fields = []
        for listview_column in self.columns.values():
            if listview_column.column.get_visible():
                if not listview_column.status_field:
                    continue
                status_fields.extend(listview_column.status_field)
        component.get('SessionProxy').get_torrents_status(
            {}, status_fields
        ).addCallback(self._on_session_state)

        client.register_event_handler(
            'TorrentStateChangedEvent', self.on_torrentstatechanged_event
        )
        client.register_event_handler('TorrentAddedEvent', self.on_torrentadded_event)
        client.register_event_handler(
            'TorrentRemovedEvent', self.on_torrentremoved_event
        )
        client.register_event_handler('SessionPausedEvent', self.on_sessionpaused_event)
        client.register_event_handler(
            'SessionResumedEvent', self.on_sessionresumed_event
        )
        client.register_event_handler(
            'TorrentQueueChangedEvent', self.on_torrentqueuechanged_event
        )

    def _on_session_state(self, state):
        self.add_rows(state)
        self.got_state = True
        # Update the view right away with our status
        self.status = state
        self.set_columns_to_update()
        self.update_view(load_new_list=True)
        self.select_first_row()

    def stop(self):
        """Stops the torrentview"""
        client.deregister_event_handler(
            'TorrentStateChangedEvent', self.on_torrentstatechanged_event
        )
        client.deregister_event_handler('TorrentAddedEvent', self.on_torrentadded_event)
        client.deregister_event_handler(
            'TorrentRemovedEvent', self.on_torrentremoved_event
        )
        client.deregister_event_handler(
            'SessionPausedEvent', self.on_sessionpaused_event
        )
        client.deregister_event_handler(
            'SessionResumedEvent', self.on_sessionresumed_event
        )
        client.deregister_event_handler(
            'TorrentQueueChangedEvent', self.on_torrentqueuechanged_event
        )

        if self.treeview.get_selection():
            self.treeview.get_selection().unselect_all()

        # Save column state before clearing liststore
        # so column sort details are correctly saved.
        self.save_state()
        self.liststore.clear()
        self.prev_status = {}
        self.filter = None
        self.search_box.hide()

    def shutdown(self):
        """Called when GtkUi is exiting"""
        pass

    def save_state(self):
        """
        Saves the state of the torrent view.
        """
        if component.get('MainWindow').visible():
            ListView.save_state(self, 'torrentview.state')

    def remove_column(self, header):
        """Removes the column with the name 'header' from the torrentview"""
        self.save_state()
        ListView.remove_column(self, header)

    def set_filter(self, filter_dict):
        """
        Sets filters for the torrentview..

        see: core.get_torrents_status
        """
        search_filter = self.filter and self.filter.get('name', None) or None
        self.filter = dict(filter_dict)  # Copied version of filter_dict.
        if search_filter and 'name' not in filter_dict:
            self.filter['name'] = search_filter
        self.update(select_row=True)

    def set_columns_to_update(self, columns=None):
        status_keys = []
        self.columns_to_update = []

        if columns is None:
            # We need to iterate through all columns
            columns = list(self.columns)

        # Iterate through supplied list of columns to update
        for column in columns:
            # Make sure column is visible and has 'status_field' set.
            # If not, we can ignore it.
            if (
                self.columns[column].column.get_visible() is True
                and self.columns[column].hidden is False
                and self.columns[column].status_field is not None
            ):
                for field in self.columns[column].status_field:
                    status_keys.append(field)
                    self.columns_to_update.append(column)

        # Remove duplicates
        self.columns_to_update = list(set(self.columns_to_update))
        status_keys = list(set(status_keys + self.permanent_status_keys))
        return status_keys

    def send_status_request(self, columns=None, select_row=False):
        # Store the 'status_fields' we need to send to core
        status_keys = self.set_columns_to_update(columns)

        # If there is nothing in status_keys then we must not continue
        if status_keys == []:
            return

        # Remove duplicates from status_key list
        status_keys = list(set(status_keys))

        # Request the statuses for all these torrent_ids, this is async so we
        # will deal with the return in a signal callback.
        d = (
            component.get('SessionProxy')
            .get_torrents_status(self.filter, status_keys)
            .addCallback(self._on_get_torrents_status)
        )
        if select_row:
            d.addCallback(self.select_first_row)

    def select_first_row(self, ignored=None):
        """
        Set the first row in the list selected if a selection does
        not already exist
        """
        rows = self.treeview.get_selection().get_selected_rows()[1]
        # Only select row if noe rows are selected
        if not rows:
            self.treeview.get_selection().select_path((0,))

    def update(self, select_row=False):
        """
        Sends a status request to core and updates the torrent list with the result.

        :param select_row: if the first row in the list should be selected if
                           no rows are already selected.
        :type select_row: boolean

        """
        if self.got_state:
            if (
                self.search_box.search_pending is not None
                and self.search_box.search_pending.active()
            ):
                # An update request is scheduled, let's wait for that one
                return
            # Send a status request
            idle_add(self.send_status_request, None, select_row)

    def update_view(self, load_new_list=False):
        """Update the torrent view model with data we've received."""
        filter_column = self.columns['filter'].column_indices[0]
        status = self.status

        if not load_new_list:
            # Freeze notications while updating
            self.treeview.freeze_child_notify()

        # Get the columns to update from one of the torrents
        if status:
            torrent_id = list(status)[0]
            fields_to_update = []
            for column in self.columns_to_update:
                column_index = self.get_column_index(column)
                for i, status_field in enumerate(self.columns[column].status_field):
                    # Only use columns that the torrent has in the state
                    if status_field in status[torrent_id]:
                        fields_to_update.append((column_index[i], status_field))

        for row in self.liststore:
            torrent_id = row[self.columns['torrent_id'].column_indices[0]]
            # We expect the torrent_id to be in status and prev_status,
            # as it will be as long as the list isn't changed by the user

            torrent_id_in_status = False
            try:
                torrent_status = status[torrent_id]
                torrent_id_in_status = True
                if torrent_status == self.prev_status[torrent_id]:
                    # The status dict is the same, so do nothing to update for this torrent
                    continue
            except KeyError:
                pass

            if not torrent_id_in_status:
                if row[filter_column] is True:
                    row[filter_column] = False
            else:
                if row[filter_column] is False:
                    row[filter_column] = True

                # Find the fields to update
                to_update = []
                for i, status_field in fields_to_update:
                    row_value = status[torrent_id][status_field]
                    if decode_bytes(row[i]) != row_value:
                        to_update.append(i)
                        to_update.append(row_value)
                # Update fields in the liststore
                if to_update:
                    self.liststore.set(row.iter, *to_update)

        if load_new_list:
            # Create the model filter. This sets the model for the treeview and enables sorting.
            self.create_model_filter()
        else:
            self.treeview.thaw_child_notify()

        component.get('MenuBar').update_menu()
        self.prev_status = status

    def _on_get_torrents_status(self, status, select_row=False):
        """Callback function for get_torrents_status().  'status' should be a
        dictionary of {torrent_id: {key, value}}."""
        self.status = status
        if self.search_box.prefiltered is not None:
            self.search_box.prefiltered = None

        if self.status == self.prev_status and self.prev_status:
            # We do not bother updating since the status hasn't changed
            self.prev_status = self.status
            return
        self.update_view()

    def add_rows(self, torrent_ids):
        """Accepts a list of torrent_ids to add to self.liststore"""
        torrent_id_column = self.columns['torrent_id'].column_indices[0]
        dirty_column = self.columns['dirty'].column_indices[0]
        filter_column = self.columns['filter'].column_indices[0]
        for torrent_id in torrent_ids:
            # Insert a new row to the liststore
            row = self.liststore.append()
            self.liststore.set(
                row,
                torrent_id_column,
                torrent_id,
                dirty_column,
                True,
                filter_column,
                True,
            )

    def remove_row(self, torrent_id):
        """Removes a row with torrent_id"""
        for row in self.liststore:
            if row[self.columns['torrent_id'].column_indices[0]] == torrent_id:
                self.liststore.remove(row.iter)
                # Force an update of the torrentview
                self.update(select_row=True)
                break

    def mark_dirty(self, torrent_id=None):
        for row in self.liststore:
            if (
                not torrent_id
                or row[self.columns['torrent_id'].column_indices[0]] == torrent_id
            ):
                # log.debug('marking %s dirty', torrent_id)
                row[self.columns['dirty'].column_indices[0]] = True
                if torrent_id:
                    break

    def get_selected_torrent(self):
        """Returns a torrent_id or None.  If multiple torrents are selected,
        it will return the torrent_id of the first one."""
        selected = self.get_selected_torrents()
        if selected:
            return selected[0]
        else:
            return selected

    def get_selected_torrents(self):
        """Returns a list of selected torrents or None"""
        torrent_ids = []
        try:
            paths = self.treeview.get_selection().get_selected_rows()[1]
        except AttributeError:
            # paths is likely None .. so lets return []
            return []
        try:
            for path in paths:
                try:
                    row = self.treeview.get_model().get_iter(path)
                except Exception as ex:
                    log.debug('Unable to get iter from path: %s', ex)
                    continue

                child_row = self.treeview.get_model().convert_iter_to_child_iter(row)
                child_row = (
                    self.treeview.get_model()
                    .get_model()
                    .convert_iter_to_child_iter(child_row)
                )
                if self.liststore.iter_is_valid(child_row):
                    try:
                        value = self.liststore.get_value(
                            child_row, self.columns['torrent_id'].column_indices[0]
                        )
                    except Exception as ex:
                        log.debug('Unable to get value from row: %s', ex)
                    else:
                        torrent_ids.append(value)
            if len(torrent_ids) == 0:
                return []

            return torrent_ids
        except (ValueError, TypeError):
            return []

    def get_torrent_status(self, torrent_id):
        """Returns data stored in self.status, it may not be complete"""
        try:
            return self.status[torrent_id]
        except KeyError:
            return {}

    def get_visible_torrents(self):
        return list(self.status)

    # Callbacks #
    def on_button_press_event(self, widget, event):
        """This is a callback for showing the right-click context menu."""
        log.debug('on_button_press_event')
        # We only care about right-clicks
        if event.button == 3 and event.window == self.treeview.get_bin_window():
            x, y = event.get_coords()
            path = self.treeview.get_path_at_pos(int(x), int(y))
            if not path:
                return
            row = self.model_filter.get_iter(path[0])

            if self.get_selected_torrents():
                if (
                    self.model_filter.get_value(
                        row, self.columns['torrent_id'].column_indices[0]
                    )
                    not in self.get_selected_torrents()
                ):
                    self.treeview.get_selection().unselect_all()
                    self.treeview.get_selection().select_iter(row)
            else:
                self.treeview.get_selection().select_iter(row)
            torrentmenu = component.get('MenuBar').torrentmenu
            torrentmenu.popup(None, None, None, None, event.button, event.time)
            return True

    def on_selection_changed(self, treeselection):
        """This callback is know when the selection has changed."""
        log.debug('on_selection_changed')
        component.get('TorrentDetails').update()
        component.get('MenuBar').update_menu()

    def on_drag_drop(self, widget, drag_context, x, y, timestamp):
        widget.stop_emission('drag-drop')

    def on_drag_data_received(
        self, widget, drag_context, x, y, selection_data, info, timestamp
    ):
        widget.stop_emission('drag_data_received')

    def on_columns_changed_event(self, treeview):
        log.debug('Treeview Columns Changed')
        self.save_state()

    def on_torrentadded_event(self, torrent_id, from_state):
        self.add_rows([torrent_id])
        self.update(select_row=True)

    def on_torrentremoved_event(self, torrent_id):
        self.remove_row(torrent_id)

    def on_torrentstatechanged_event(self, torrent_id, state):
        # Update the torrents state
        for row in self.liststore:
            if torrent_id != row[self.columns['torrent_id'].column_indices[0]]:
                continue

            for name in self.columns_to_update:
                if not self.columns[name].status_field:
                    continue
                for idx, status_field in enumerate(self.columns[name].status_field):
                    # Update all columns that use the state field to current state
                    if status_field != 'state':
                        continue
                    row[self.get_column_index(name)[idx]] = state

            if self.filter.get('state', None) is not None:
                # We have a filter set, let's see if theres anything to hide
                # and remove from status
                if (
                    torrent_id in self.status
                    and self.status[torrent_id]['state'] != state
                ):
                    row[self.columns['filter'].column_indices[0]] = False
                    del self.status[torrent_id]

        self.mark_dirty(torrent_id)

    def on_sessionpaused_event(self):
        self.mark_dirty()
        self.update()

    def on_sessionresumed_event(self):
        self.mark_dirty()
        self.update(select_row=True)

    def on_torrentqueuechanged_event(self):
        self.mark_dirty()
        self.update()

    # Handle keyboard shortcuts
    def on_key_press_event(self, widget, event):
        keyname = keyval_name(event.keyval)
        if keyname is not None:
            func = getattr(self, 'keypress_' + keyname.lower(), None)
            if func:
                return func(event)

    def keypress_up(self, event):
        """Handle any Up arrow keypresses"""
        log.debug('keypress_up')
        torrents = self.get_selected_torrents()
        if not torrents:
            return

        # Move queue position up with Ctrl+Alt or Ctrl+Alt+Shift
        if event.get_state() & CTRL_ALT_MASK:
            if event.get_state() & ModifierType.SHIFT_MASK:
                client.core.queue_top(torrents)
            else:
                client.core.queue_up(torrents)

    def keypress_down(self, event):
        """Handle any Down arrow keypresses"""
        log.debug('keypress_down')
        torrents = self.get_selected_torrents()
        if not torrents:
            return

        # Move queue position down with Ctrl+Alt or Ctrl+Alt+Shift
        if event.get_state() & CTRL_ALT_MASK:
            if event.get_state() & ModifierType.SHIFT_MASK:
                client.core.queue_bottom(torrents)
            else:
                client.core.queue_down(torrents)

    def keypress_delete(self, event):
        log.debug('keypress_delete')
        torrents = self.get_selected_torrents()
        if torrents:
            if event.get_state() & ModifierType.SHIFT_MASK:
                RemoveTorrentDialog(torrents, delete_files=True).run()
            else:
                RemoveTorrentDialog(torrents).run()

    def keypress_menu(self, event):
        log.debug('keypress_menu')
        if not self.get_selected_torrent():
            return

        torrentmenu = component.get('MenuBar').torrentmenu
        torrentmenu.popup(None, None, None, None, 3, event.time)
        return True
