Created
October 31, 2012 02:38
-
-
Save gazpachoking/3984489 to your computer and use it in GitHub Desktop.
deluge stable sort
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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