Fixed argument expansion for quoted arguments.
[rox-shell.git] / roxshell / directory.py
bloba6f747a1ac53b6081e9feae54bd0d57199e71161
1 """
2 @copyright: (C) 2008, Thomas Leonard
3 @see: U{http://roscidus.com}
4 """
5 from __future__ import with_statement
6 import gobject, stat
7 import _gio as gio
8 import gtk
9 import weakref
10 from logging import warn
12 gtk_theme = gtk.icon_theme_get_default()
14 icon_size = 48
16 def get_themed_icon(name, icon_size):
17 try:
18 return gtk_theme.load_icon('text-x-generic', icon_size, 0)
19 except gobject.GError:
20 return None
22 icon_text_plain = get_themed_icon('text-x-generic', icon_size)
23 icon_dir = get_themed_icon('folder', icon_size)
25 def sort_key(name, info):
26 t = 9 - int(info.get_file_type())
27 return str(t) + name.lower()
29 COLOUR_EXEC = '#008000'
30 COLOUR_NORMAL = '#000000'
31 COLOUR_DIR = '#0000a0'
32 COLOUR_ERROR = '#800000'
34 QUERY = 'standard::*,unix::mode,thumbnail::path'
36 def is_hidden(name):
37 return name.startswith('.')
39 class ErrorInfo:
40 def __init__(self, name):
41 self.name = name
43 def get_name(self):
44 return self.name
46 def get_file_type(self):
47 return gio.FILE_TYPE_UNKNOWN
49 def make_error_row(name, ex):
50 info = ErrorInfo(name)
51 return [name, None, info, sort_key(name, info), COLOUR_ERROR]
53 def make_row(info):
54 name = info.get_name()
55 if is_hidden(name):
56 return None
58 pixbuf = None
59 thumbnail = info.get_attribute_byte_string('thumbnail::path')
60 if thumbnail:
61 try:
62 loader = gtk.gdk.PixbufLoader('png')
63 try:
64 with open(thumbnail) as stream:
65 loader.write(stream.read())
66 finally:
67 loader.close()
68 pixbuf = loader.get_pixbuf()
69 assert pixbuf, "Failed to load PNG thumbnail"
70 except Exception, ex:
71 warn("Failed to load cached PNG thumbnail icon: %s", ex)
72 else:
73 icon = info.get_icon()
74 if icon:
75 gtkicon_info = gtk_theme.choose_icon(icon.get_names(), icon_size, 0)
76 if gtkicon_info:
77 gtkicon_info.get_filename()
78 pixbuf = gtkicon_info.load_icon()
79 mode = info.get_attribute_uint32('unix::mode')
80 if stat.S_ISDIR(mode):
81 colour = COLOUR_DIR
82 elif mode & 0111:
83 colour = COLOUR_EXEC
84 else:
85 colour = COLOUR_NORMAL
86 return [name, pixbuf, info, sort_key(name, info), colour]
88 class DirModel:
89 NAME = 0
90 PIXBUF = 1
91 INFO = 2
92 SORT = 3 # Type-then-name
93 COLOUR = 4
95 model = None
96 monitor = None
98 def __init__(self, file):
99 assert isinstance(file, gio.File), file
100 self.file = file
101 self.users = []
103 def __del__(self):
104 assert not self.users, "DirModel garbage collected while still monitoring! Monitored by " + str(self.users)
106 def add_ref(self, user):
107 user = id(user)
109 if not self.users:
110 # First user...
111 assert not self.monitor
112 assert not self.model
113 self.model = gtk.ListStore(str, gtk.gdk.Pixbuf, object, str, str)
115 # Should really be the view that sorts, but see GTK bug #523724
116 self.model.set_sort_column_id(DirModel.SORT, gtk.SORT_ASCENDING)
118 self.monitor = self.file.monitor_directory(0)
119 weakself = weakref.ref(self)
120 self.monitor.connect('changed', lambda *args: weakself().contents_changed(*args))
121 self.build_contents()
123 self.users.append(user)
125 def del_ref(self, user):
126 user = id(user)
127 self.users.remove(user)
129 if not self.users:
130 # Last user...
131 self.model.clear()
132 self.model = None
133 self.monitor.cancel()
134 self.monitor = None
136 def contents_changed(self, monitor, this_file, other_file, event_type):
137 name = this_file.get_basename()
138 if is_hidden(name):
139 return
140 #print "build", monitor, this_file.get_basename(), other_file, event_type
141 if event_type == gio.FILE_MONITOR_EVENT_CREATED:
142 #print "Create", name
143 i = self.model.append(None)
144 elif event_type in (gio.FILE_MONITOR_EVENT_DELETED, gio.FILE_MONITOR_EVENT_CHANGED, gio.FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED):
145 i = self.model.get_iter_root()
146 while i:
147 if self.model[i][DirModel.NAME] == name:
148 break
149 i = self.model.iter_next(i)
150 else:
151 #print "Note: deleted unknown file", name
152 self.build_contents()
153 return
154 elif event_type == gio.FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
155 return
156 else:
157 print "Unknown event type", event_type, name
158 self.build_contents()
160 if event_type in (gio.FILE_MONITOR_EVENT_CREATED, gio.FILE_MONITOR_EVENT_CHANGED, gio.FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED):
161 #print "Update", name
162 try:
163 info = this_file.query_info(QUERY, 0)
164 except gobject.GError, ex:
165 self.model[i] = make_error_row(name, ex)
166 else:
167 self.model[i] = make_row(info)
168 elif event_type == gio.FILE_MONITOR_EVENT_DELETED:
169 #print "Remove", name
170 self.model.remove(i)
171 else:
172 assert False, "Unreachable! " + str(event_type)
174 def build_contents(self):
175 m = self.model
176 name_to_iter = {}
178 i = m.get_iter_root()
179 while i:
180 name_to_iter[m[i][DirModel.NAME]] = i
181 i = m.iter_next(i)
183 self.error = None
185 files = []
186 try:
187 e = self.file.enumerate_children(QUERY, 0)
188 except gobject.GError, ex:
189 self.error = ex
190 else:
191 if e:
192 while True:
193 info = e.next_file()
194 if not info:
195 break
196 row = make_row(info)
197 if row:
198 files.append(row)
200 # name_to_iter contains the old contents
201 # files contains the desired contents
203 new = []
204 for row in files:
205 name = row[DirModel.NAME]
207 i = name_to_iter.get(name, None)
208 if i:
209 m[i] = row
210 del name_to_iter[name]
211 else:
212 new.append(row)
214 # name_to_iter contains old contents that are no longer desired
215 # new contains rows that are desired but were not present
217 i = m.get_iter_root()
218 while i:
219 if m[i][DirModel.NAME] in name_to_iter:
220 if not m.remove(i):
221 i = None
222 else:
223 i = m.iter_next(i)
225 for row in new:
226 i = m.append(None)
227 m[i] = row
229 dirs = weakref.WeakValueDictionary()
230 def get_dir_model(file):
231 # Might want some caching here...
232 uri = file.get_uri()
233 try:
234 return dirs[uri]
235 except KeyError:
236 dm = DirModel(file)
237 dirs[uri] = dm
238 return dm