Skip to content

Instantly share code, notes, and snippets.

@gazpachoking
Created October 31, 2012 02:38
Show Gist options
  • Save gazpachoking/3984489 to your computer and use it in GitHub Desktop.
Save gazpachoking/3984489 to your computer and use it in GitHub Desktop.
deluge stable sort
Index: deluge/ui/gtkui/listview.py
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
Subsystem: com.intellij.openapi.diff.impl.patch.BaseRevisionTextPatchEP
<+># -*- coding: utf-8 -*-\n#\n# listview.py\n#\n# Copyright (C) 2007, 2008 Andrew Resch <[email protected]>\n#\n# Deluge is free software.\n#\n# You may redistribute it and/or modify it under the terms of the\n# GNU General Public License, as published by the Free Software\n# Foundation; either version 3 of the License, or (at your option)\n# any later version.\n#\n# deluge is distributed in the hope that it will be useful,\n# but WITHOUT ANY WARRANTY; without even the implied warranty of\n# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n# See the GNU General Public License for more details.\n#\n# You should have received a copy of the GNU General Public License\n# along with deluge. If not, write to:\n# The Free Software Foundation, Inc.,\n# 51 Franklin Street, Fifth Floor\n# Boston, MA 02110-1301, USA.\n#\n# In addition, as a special exception, the copyright holders give\n# permission to link the code of portions of this program with the OpenSSL\n# library.\n# You must obey the GNU General Public License in all respects for all of\n# the code used other than OpenSSL. If you modify file(s) with this\n# exception, you may extend this exception to your version of the file(s),\n# but you are not obligated to do so. If you do not wish to do so, delete\n# this exception statement from your version. If you delete this exception\n# statement from all source files in the program, then also delete it here.\n#\n#\n\n\nimport cPickle\nimport os.path\n\nimport pygtk\npygtk.require('2.0')\nimport gtk\nimport gettext\n\nfrom deluge.configmanager import ConfigManager\nimport deluge.configmanager\nimport deluge.common\nfrom deluge.log import LOG as log\n\nfrom gobject import signal_new, SIGNAL_RUN_LAST, TYPE_NONE\nfrom gtk import gdk\nsignal_new('button-press-event', gtk.TreeViewColumn,\n SIGNAL_RUN_LAST, TYPE_NONE, (gdk.Event,))\n\n\n# Cell data functions to pass to add_func_column()\ndef cell_data_speed(column, cell, model, row, data):\n \"\"\"Display value as a speed, eg. 2 KiB/s\"\"\"\n speed = model.get_value(row, data)\n speed_str = \"\"\n if speed > 0:\n speed_str = deluge.common.fspeed(speed)\n\n cell.set_property('text', speed_str)\n\ndef cell_data_size(column, cell, model, row, data):\n \"\"\"Display value in terms of size, eg. 2 MB\"\"\"\n size = model.get_value(row, data)\n size_str = deluge.common.fsize(size)\n cell.set_property('text', size_str)\n\ndef cell_data_peer(column, cell, model, row, data):\n \"\"\"Display values as 'value1 (value2)'\"\"\"\n (first, second) = model.get(row, *data)\n # Only display a (total) if second is greater than -1\n if second > -1:\n cell.set_property('text', '%d (%d)' % (first, second))\n else:\n cell.set_property('text', '%d' % first)\n\ndef cell_data_time(column, cell, model, row, data):\n \"\"\"Display value as time, eg 1m10s\"\"\"\n time = model.get_value(row, data)\n if time <= 0:\n time_str = \"\"\n else:\n time_str = deluge.common.ftime(time)\n cell.set_property('text', time_str)\n\ndef cell_data_ratio(column, cell, model, row, data):\n \"\"\"Display value as a ratio with a precision of 3.\"\"\"\n ratio = model.get_value(row, data)\n if ratio < 0:\n ratio_str = \"∞\"\n else:\n ratio_str = \"%.3f\" % ratio\n\n cell.set_property('text', ratio_str)\n\ndef cell_data_date(column, cell, model, row, data):\n \"\"\"Display value as date, eg 05/05/08\"\"\"\n cell.set_property('text', deluge.common.fdate(model.get_value(row, data)))\n\ndef cell_data_speed_limit(column, cell, model, row, data):\n \"\"\"Display value as a speed, eg. 2 KiB/s\"\"\"\n speed = model.get_value(row, data)\n speed_str = \"\"\n if speed > 0:\n speed_str = deluge.common.fspeed(speed * 1024)\n\n cell.set_property('text', speed_str)\n\nclass ListViewColumnState:\n \"\"\"Used for saving/loading column state\"\"\"\n def __init__(self, name, position, width, visible, sort, sort_order):\n self.name = name\n self.position = position\n self.width = width\n self.visible = visible\n self.sort = sort\n self.sort_order = sort_order\n\nclass ListView:\n \"\"\"ListView is used to make custom GtkTreeViews. It supports the adding\n and removing of columns, creating a menu for a column toggle list and\n support for 'status_field's which are used while updating the columns data.\n \"\"\"\n\n class ListViewColumn:\n \"\"\"Holds information regarding a column in the ListView\"\"\"\n def __init__(self, name, column_indices):\n # Name is how a column is identified and is also the header\n self.name = name\n # Column_indices holds the indexes to the liststore_columns that\n # this column utilizes. It is stored as a list.\n self.column_indices = column_indices\n # Column is a reference to the GtkTreeViewColumn object\n self.column = None\n # This is the name of the status field that the column will query\n # the core for if an update is called.\n self.status_field = None\n # If column is 'hidden' then it will not be visible and will not\n # show up in any menu listing; it cannot be shown ever.\n self.hidden = False\n # If this is set, it is used to sort the model\n self.sort_func = None\n self.sort_id = None\n\n class TreeviewColumn(gtk.TreeViewColumn):\n \"\"\"\n TreeViewColumn does not signal right-click events, and we need them\n This subclass is equivalent to TreeViewColumn, but it signals these events\n\n Most of the code of this class comes from Quod Libet (http://www.sacredchao.net/quodlibet)\n \"\"\"\n\n def __init__(self, title=None, cell_renderer=None, ** args):\n \"\"\" Constructor, see gtk.TreeViewColumn \"\"\"\n gtk.TreeViewColumn.__init__(self, title, cell_renderer, ** args)\n label = gtk.Label(title)\n self.set_widget(label)\n label.show()\n label.__realize = label.connect('realize', self.onRealize)\n\n def onRealize(self, widget):\n widget.disconnect(widget.__realize)\n del widget.__realize\n button = widget.get_ancestor(gtk.Button)\n if button is not None:\n button.connect('button-press-event', self.onButtonPressed)\n\n def onButtonPressed(self, widget, event):\n self.emit('button-press-event', event)\n\n def __init__(self, treeview_widget=None, state_file=None):\n log.debug(\"ListView initialized..\")\n\n if treeview_widget is not None:\n # User supplied a treeview widget\n self.treeview = treeview_widget\n else:\n self.treeview = gtk.TreeView()\n\n self.treeview.set_enable_search(True)\n self.treeview.set_search_equal_func(self.on_keypress_search_by_name)\n\n if state_file:\n self.load_state(state_file)\n\n self.liststore = None\n self.model_filter = None\n\n self.treeview.set_rules_hint(True)\n self.treeview.set_reorderable(True)\n self.treeview.get_selection().set_mode(gtk.SELECTION_MULTIPLE)\n\n # Dictionary of 'header' or 'name' to ListViewColumn object\n self.columns = {}\n # Column_index keeps track of the order of the visible columns.\n self.column_index = []\n # The column types for the list store.. this may have more entries than\n # visible columns due to some columns utilizing more than 1 liststore\n # column and some columns being hidden.\n self.liststore_columns = []\n # The GtkMenu that is created after every addition, removal or reorder\n self.menu = None\n # A list of menus that self.menu will be a submenu of everytime it is\n # created.\n self.checklist_menus = []\n\n # Store removed columns state. This is needed for plugins that remove\n # their columns prior to having the state list saved on shutdown.\n self.removed_columns_state = []\n\n # Create the model filter and column\n self.add_bool_column(\"filter\", hidden=True)\n\n def create_model_filter(self):\n \"\"\"create new filter-model\n must be called after listview.create_new_liststore\n \"\"\"\n model_filter = self.liststore.filter_new()\n model_filter.set_visible_column(\n self.columns[\"filter\"].column_indices[0])\n sort_info = None\n if self.model_filter:\n sort_info = self.model_filter.get_sort_column_id()\n\n self.model_filter = gtk.TreeModelSort(model_filter)\n if sort_info and sort_info[0] and sort_info[1] > -1:\n self.model_filter.set_sort_column_id(sort_info[0], sort_info[1])\n self.set_sort_functions()\n self.treeview.set_model(self.model_filter)\n\n def set_sort_functions(self):\n for column in self.columns.values():\n if column.sort_func:\n self.model_filter.set_sort_func(\n column.sort_id,\n column.sort_func,\n column.sort_id)\n\n def create_column_state(self, column, position=None):\n if not position:\n # Find the position\n for index, c in enumerate(self.treeview.get_columns()):\n if column.get_title() == c.get_title():\n position = index\n break\n sort = None\n sort_id, order = self.model_filter.get_sort_column_id()\n if self.get_column_name(sort_id) == column.get_title():\n sort = sort_id\n\n return ListViewColumnState(column.get_title(), position,\n column.get_width(), column.get_visible(),\n sort, int(column.get_sort_order()))\n\n def save_state(self, filename):\n \"\"\"Saves the listview state (column positions and visibility) to\n filename.\"\"\"\n # A list of ListViewColumnStates\n state = []\n\n # Workaround for all zero widths after removing column on shutdown\n if not any(c.get_width() for c in self.treeview.get_columns()): return\n\n # Get the list of TreeViewColumns from the TreeView\n for counter, column in enumerate(self.treeview.get_columns()):\n # Append a new column state to the state list\n state.append(self.create_column_state(column, counter))\n\n state += self.removed_columns_state\n\n # Get the config location for saving the state file\n config_location = deluge.configmanager.get_config_dir()\n\n try:\n log.debug(\"Saving ListView state file: %s\", filename)\n state_file = open(os.path.join(config_location, filename), \"wb\")\n cPickle.dump(state, state_file)\n state_file.close()\n except IOError, e:\n log.warning(\"Unable to save state file: %s\", e)\n\n def load_state(self, filename):\n \"\"\"Load the listview state from filename.\"\"\"\n # Get the config location for loading the state file\n config_location = deluge.configmanager.get_config_dir()\n state = None\n\n try:\n log.debug(\"Loading ListView state file: %s\", filename)\n state_file = open(os.path.join(config_location, filename), \"rb\")\n state = cPickle.load(state_file)\n state_file.close()\n except (EOFError, IOError, cPickle.UnpicklingError), e:\n log.warning(\"Unable to load state file: %s\", e)\n\n # Keep the state in self.state so we can access it as we add new columns\n self.state = state\n\n def set_treeview(self, treeview_widget):\n \"\"\"Set the treeview widget that this listview uses.\"\"\"\n self.treeview = treeview_widget\n self.treeview.set_model(self.liststore)\n return\n\n def get_column_index(self, name):\n \"\"\"Get the liststore column indices belonging to this column.\n Will return a list.\n \"\"\"\n return self.columns[name].column_indices\n\n def get_column_name(self, index):\n \"\"\"Get the header name for a liststore column index\"\"\"\n for key, value in self.columns.items():\n if index in value.column_indices:\n return key\n\n def get_state_field_column(self, field):\n \"\"\"Returns the column number for the state field\"\"\"\n for column in self.columns.keys():\n if self.columns[column].status_field == None:\n continue\n\n for f in self.columns[column].status_field:\n if field == f:\n return self.columns[column].column_indices[\n self.columns[column].status_field.index(f)]\n\n def on_menuitem_toggled(self, widget):\n \"\"\"Callback for the generated column menuitems.\"\"\"\n # Get the column name from the widget\n name = widget.get_child().get_text()\n\n # Set the column's visibility based on the widgets active state\n try:\n self.columns[name].column.set_visible(widget.get_active())\n except KeyError:\n self.columns[unicode(name)].column.set_visible(widget.get_active())\n return\n\n def on_treeview_header_right_clicked(self, column, event):\n if event.button == 3:\n self.menu.popup(None, None, None, event.button, event.get_time())\n\n\n def register_checklist_menu(self, menu):\n \"\"\"Register a checklist menu with the listview. It will automatically\n attach any new checklist menu it makes to this menu.\n \"\"\"\n self.checklist_menus.append(menu)\n\n def create_checklist_menu(self):\n \"\"\"Creates a menu used for toggling the display of columns.\"\"\"\n menu = self.menu = gtk.Menu()\n # Iterate through the column_index list to preserve order\n for name in self.column_index:\n column = self.columns[name]\n # If the column is hidden, then we do not want to show it in the\n # menu.\n if column.hidden is True:\n continue\n menuitem = gtk.CheckMenuItem(column.name)\n # If the column is currently visible, make sure it's set active\n # (or checked) in the menu.\n if column.column.get_visible() is True:\n menuitem.set_active(True)\n # Connect to the 'toggled' event\n menuitem.connect(\"toggled\", self.on_menuitem_toggled)\n # Add the new checkmenuitem to the menu\n menu.append(menuitem)\n\n # Attach this new menu to all the checklist_menus\n for _menu in self.checklist_menus:\n _menu.set_submenu(menu)\n _menu.show_all()\n return menu\n\n def create_new_liststore(self):\n \"\"\"Creates a new GtkListStore based on the liststore_columns list\"\"\"\n # Create a new liststore with added column and move the data from the\n # old one to the new one.\n new_list = gtk.ListStore(*tuple(self.liststore_columns))\n\n # This function is used in the liststore.foreach method with user_data\n # being the new liststore and the columns list\n def copy_row(model, path, row, user_data):\n new_list, columns = user_data\n new_row = new_list.append()\n for column in range(len(columns)):\n # Get the current value of the column for this row\n value = model.get_value(row, column)\n # Set the value of this row and column in the new liststore\n new_list.set_value(new_row, column, value)\n\n # Do the actual row copy\n if self.liststore is not None:\n self.liststore.foreach(copy_row, (new_list, self.columns))\n\n self.liststore = new_list\n # Create the model\n self.create_model_filter()\n\n return\n\n def remove_column(self, header):\n \"\"\"Removes the column with the name 'header' from the listview\"\"\"\n # Store a copy of this columns state in case it's re-added\n state = self.create_column_state(self.columns[header].column)\n self.removed_columns_state.append(state)\n\n # Start by removing this column from the treeview\n self.treeview.remove_column(self.columns[header].column)\n # Get the column indices\n column_indices = self.columns[header].column_indices\n # Delete the column\n del self.columns[header]\n self.column_index.remove(header)\n # Shift the column_indices values of those columns effected by the\n # removal. Any column_indices > the one removed.\n for column in self.columns.values():\n if column.column_indices[0] > column_indices[0]:\n # We need to shift this column_indices\n for index in column.column_indices:\n index = index - len(column_indices)\n\n # Remove from the liststore columns list\n for index in column_indices:\n del self.liststore_columns[index]\n\n # Create a new liststore\n self.create_new_liststore()\n\n # Re-create the menu\n self.create_checklist_menu()\n\n return\n\n def add_column(self, header, render, col_types, hidden, position,\n status_field, sortid, text=0, value=0, pixbuf=0, function=None,\n column_type=None, sort_func=None, default=True):\n \"\"\"Adds a column to the ListView\"\"\"\n # Add the column types to liststore_columns\n column_indices = []\n if type(col_types) is list:\n for col_type in col_types:\n self.liststore_columns.append(col_type)\n column_indices.append(len(self.liststore_columns) - 1)\n else:\n self.liststore_columns.append(col_types)\n column_indices.append(len(self.liststore_columns) - 1)\n\n # Add to the index list so we know the order of the visible columns.\n if position is not None:\n self.column_index.insert(position, header)\n else:\n self.column_index.append(header)\n\n # Create a new column object and add it to the list\n self.columns[header] = self.ListViewColumn(header, column_indices)\n\n self.columns[header].status_field = status_field\n self.columns[header].sort_func = sort_func\n self.columns[header].sort_id = column_indices[sortid]\n\n # Create a new list with the added column\n self.create_new_liststore()\n\n column = self.TreeviewColumn(header)\n\n if column_type == \"text\":\n column.pack_start(render)\n column.add_attribute(render, \"text\",\n self.columns[header].column_indices[text])\n elif column_type == \"bool\":\n column.pack_start(render)\n column.add_attribute(render, \"active\",\n self.columns[header].column_indices[0])\n elif column_type == \"func\":\n column.pack_start(render, True)\n if len(self.columns[header].column_indices) > 1:\n column.set_cell_data_func(render, function,\n tuple(self.columns[header].column_indices))\n else:\n column.set_cell_data_func(render, function,\n self.columns[header].column_indices[0])\n elif column_type == \"progress\":\n column.pack_start(render)\n if function is None:\n column.add_attribute(render, \"text\",\n self.columns[header].column_indices[text])\n column.add_attribute(render, \"value\",\n self.columns[header].column_indices[value])\n else:\n column.set_cell_data_func(render, function,\n tuple(self.columns[header].column_indices))\n elif column_type == \"texticon\":\n column.pack_start(render[pixbuf], False)\n if function is not None:\n column.set_cell_data_func(render[pixbuf], function,\n self.columns[header].column_indices[pixbuf])\n column.pack_start(render[text], True)\n column.add_attribute(render[text], \"text\",\n self.columns[header].column_indices[text])\n elif column_type == None:\n return\n\n column.set_sort_column_id(self.columns[header].column_indices[sortid])\n column.set_clickable(True)\n column.set_resizable(True)\n column.set_expand(False)\n column.set_min_width(10)\n column.set_reorderable(True)\n column.set_visible(not hidden)\n column.connect('button-press-event',\n self.on_treeview_header_right_clicked)\n\n # Check for loaded state and apply\n column_in_state = False\n if self.state != None:\n for column_state in self.state:\n if header == column_state.name:\n # We found a loaded state\n column_in_state = True\n if column_state.width > 0:\n column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)\n column.set_fixed_width(column_state.width)\n\n if column_state.sort is not None and column_state.sort > -1:\n self.model_filter.set_sort_column_id(column_state.sort, column_state.sort_order)\n column.set_visible(column_state.visible)\n position = column_state.position\n break\n\n # Set this column to not visible if its not in the state and\n # its not supposed to be shown by default\n if not column_in_state and not default and not hidden:\n column.set_visible(False)\n\n if position is not None:\n self.treeview.insert_column(column, position)\n else:\n self.treeview.append_column(column)\n\n # Set hidden in the column\n self.columns[header].hidden = hidden\n self.columns[header].column = column\n # Re-create the menu item because of the new column\n self.create_checklist_menu()\n\n return True\n\n def add_text_column(self, header, col_type=str, hidden=False, position=None,\n status_field=None, sortid=0, column_type=\"text\",\n sort_func=None, default=True):\n \"\"\"Add a text column to the listview. Only the header name is required.\n \"\"\"\n render = gtk.CellRendererText()\n self.add_column(header, render, col_type, hidden, position,\n status_field, sortid, column_type=column_type,\n sort_func=sort_func, default=default)\n\n return True\n\n def add_bool_column(self, header, col_type=bool, hidden=False,\n position=None, status_field=None, sortid=0,\n column_type=\"bool\", default=True):\n \"\"\"Add a bool column to the listview\"\"\"\n render = gtk.CellRendererToggle()\n self.add_column(header, render, col_type, hidden, position,\n status_field, sortid, column_type=column_type,\n default=default)\n\n def add_func_column(self, header, function, col_types, sortid=0,\n hidden=False, position=None, status_field=None,\n column_type=\"func\", sort_func=None, default=True):\n \"\"\"Add a function column to the listview. Need a header name, the\n function and the column types.\"\"\"\n\n render = gtk.CellRendererText()\n self.add_column(header, render, col_types, hidden, position,\n status_field, sortid, column_type=column_type,\n function=function, sort_func=sort_func, default=default)\n\n return True\n\n def add_progress_column(self, header, col_types=[float, str], sortid=0,\n hidden=False, position=None, status_field=None,\n function=None, column_type=\"progress\",\n default=True):\n \"\"\"Add a progress column to the listview.\"\"\"\n\n render = gtk.CellRendererProgress()\n self.add_column(header, render, col_types, hidden, position,\n status_field, sortid, function=function,\n column_type=column_type, value=0, text=1,\n default=default)\n\n return True\n\n def add_texticon_column(self, header, col_types=[str, str], sortid=1,\n hidden=False, position=None, status_field=None,\n column_type=\"texticon\", function=None,\n default=True):\n \"\"\"Adds a texticon column to the listview.\"\"\"\n render1 = gtk.CellRendererPixbuf()\n render2 = gtk.CellRendererText()\n\n self.add_column(header, (render1, render2), col_types, hidden, position,\n status_field, sortid, column_type=column_type,\n function=function, pixbuf=0, text=1, default=default)\n\n return True\n\n def on_keypress_search_by_name(self, model, columnn, key, iter):\n TORRENT_NAME_COL = 5\n return not model[iter][TORRENT_NAME_COL].lower().startswith(key.lower())\n
===================================================================
--- deluge/ui/gtkui/listview.py (revision 03abe320547e27844e212d20c1f50c11d467805f)
+++ deluge/ui/gtkui/listview.py (revision )
@@ -213,6 +213,9 @@
# their columns prior to having the state list saved on shutdown.
self.removed_columns_state = []
+ # Since gtk TreeModelSort doesn't do stable sort, remember last sort order so we can
+ self.last_sort_order = {}
+
# Create the model filter and column
self.add_bool_column("filter", hidden=True)
@@ -230,16 +233,48 @@
self.model_filter = gtk.TreeModelSort(model_filter)
if sort_info and sort_info[0] and sort_info[1] > -1:
self.model_filter.set_sort_column_id(sort_info[0], sort_info[1])
+ self.model_filter.connect("sort-column-changed", self.on_model_sort_changed)
self.set_sort_functions()
self.treeview.set_model(self.model_filter)
+ def on_model_sort_changed(self, treemodel):
+ self.last_sort_order = {}
+ def record_position(model, path, iter, data):
+ self.last_sort_order[model[iter][1]] = path[0]
+ treemodel.foreach(record_position, None)
+
+ def generic_sort_func(self, model, iter1, iter2, data):
+ return cmp(model[iter1][data], model[iter2][data])
+
+ def stabilize_sort_func(self, sort_func):
+ def stabilized(model, iter1, iter2, data):
+ result = sort_func(model, iter1, iter2, data)
+ if result == 0:
+ hash1 = model[iter1][1]
+ hash2 = model[iter2][1]
+ if hash1 not in self.last_sort_order:
+ return 1
+ elif hash2 not in self.last_sort_order:
+ return -1
+ result = cmp(self.last_sort_order[hash1],
+ self.last_sort_order[hash2])
+ return result
+ return stabilized
+
def set_sort_functions(self):
+ self.model_filter.set_default_sort_func(None)
for column in self.columns.values():
if column.sort_func:
self.model_filter.set_sort_func(
column.sort_id,
- column.sort_func,
+ self.stabilize_sort_func(column.sort_func),
column.sort_id)
+ else:
+ self.model_filter.set_sort_func(
+ column.sort_id,
+ self.stabilize_sort_func(self.generic_sort_func),
+ column.sort_id
+ )
def create_column_state(self, column, position=None):
if not position:
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment