Allow comments to be edited.
[mime-editor.git] / type.py
blob424e341c883373daa3f0b6fad5faa3dc313b3dad
1 import os
2 import rox
3 from rox import g
4 from xml.parsers import expat
5 from xml.dom import XML_NAMESPACE, Node
6 import copy
8 types = {}
10 home_mime = os.path.join(os.environ['HOME'], '.mime')
11 user_override = os.path.join(home_mime, 'packages', 'Override.xml')
13 FREE_NS='http://www.freedesktop.org/standards/shared-mime-info'
15 def data(node):
16 return ''.join([text.nodeValue for text in node.childNodes
17 if text.nodeType == Node.TEXT_NODE])
19 class Invalid(Exception):
20 pass
22 def get_type(name):
23 if name not in types:
24 types[name] = MIME_Type(name)
25 return types[name]
27 class MIME_Type:
28 def __init__(self, name):
29 assert name not in types
30 self.media, self.subtype = name.split('/')
32 self.comments = []
33 self.globs = []
34 self.magic = []
35 self.xml = []
36 self.others = []
38 def add_comment(self, lang, comment, user):
39 self.comments.append((lang, comment, user))
41 def add_xml(self, uri, name, user):
42 self.xml.append((uri, name, user))
44 def add_magic(self, prio, root, user):
45 self.magic.append((prio, root, user))
47 def add_other(self, element, user):
48 self.others.append((element, user))
50 def add_glob(self, pattern, user):
51 self.globs.append((pattern, user))
53 def get_comment(self):
54 best = None
55 for lang, comment, user in self.comments:
56 if not lang:
57 return comment
58 best = comment
59 return best or self.get_name()
61 def get_name(self):
62 return self.media + '/' + self.subtype
64 def make(self, klass, list):
65 return [klass(self, item) for item in list]
67 def get_comments(self): return self.make(Comment, self.comments)
68 def get_globs(self): return self.make(Glob, self.globs)
69 def get_magic(self): return self.make(Magic, self.magic)
70 def get_xml(self): return self.make(XML, self.xml)
71 def get_others(self): return self.make(Other, self.others)
73 def remove_user(self):
74 for list in ['comments', 'globs', 'magic', 'xml', 'others']:
75 setattr(self, list,
76 [x for x in getattr(self, list) if not x[-1]])
78 class Field:
79 "MIME_Type.get_* functions return a list of these."
80 def __init__(self, type, item = None):
81 self.type = type
82 if item is None:
83 self.item = self.get_blank_item()
84 self.user = True
85 self.new = True
86 else:
87 if len(item) == 2:
88 self.item = item[0]
89 else:
90 self.item = item[:-1]
91 self.user = item[-1]
92 self.new = False
93 assert self.user in (True, False)
95 def get_blank_item(self):
96 raise Exception("Can't create fields of this type yet")
98 def __str__(self):
99 return "<%s>" % self.item
101 def add_subtree(self, model, iter, grey):
102 return
104 def __cmp__(self, b):
105 # Note: returns 1 (different) if same and both users set
106 return cmp(str(self), str(b)) or self.user or b.user
108 def delete(self):
109 rox.alert('TODO')
111 def edit(self):
112 "Returns True on success."
113 if not hasattr(self, 'add_edit_widgets'):
114 rox.alert('Sorry, MIME-Editor does not support editing fields of this type')
115 return
116 box = rox.Dialog()
117 self.box = box
118 box.set_has_separator(False)
119 vbox = g.VBox(False, 4)
120 vbox.set_border_width(4)
121 box.vbox.pack_start(vbox, True, True, 0)
122 self.add_edit_widgets(vbox)
123 box.add_button(g.STOCK_CANCEL, g.RESPONSE_CANCEL)
124 box.add_button(g.STOCK_OK, g.RESPONSE_OK)
125 box.set_default_response(g.RESPONSE_OK)
126 box.vbox.show_all()
127 while 1:
128 try:
129 resp = box.run()
130 if resp == g.RESPONSE_OK:
131 try:
132 self.commit_edit(box)
133 except Invalid, e:
134 rox.alert(str(e))
135 continue
136 box.destroy()
137 return True
138 except:
139 rox.report_exception()
140 box.destroy()
141 return False
143 def commit_edit(self, box):
144 raise Invalid('TODO: Not implemented')
146 def delete_from_node(self, type_node):
147 raise Invalid("TODO: Can't delete/edit fields of this type yet")
149 def add_to_node(self, node):
150 raise Invalid("TODO: Can't add/edit fields of this type yet")
152 def delete(self):
153 assert self.user
154 doc, node = get_override_type(self.type.get_name())
155 self.delete_from_node(node)
156 write_override(doc)
158 def commit_edit(self, box):
159 doc, node = get_override_type(self.type.get_name())
160 if not self.new:
161 self.delete_from_node(node)
162 self.add_to_node(node)
163 write_override(doc)
166 class Comment(Field):
167 def get_blank_item(self):
168 return (None, 'Unknown format')
170 def __str__(self):
171 lang, data = self.item
172 if lang:
173 lang = '[' + lang + '] '
174 else:
175 lang = '(default) '
176 return lang + data
178 def add_edit_widgets(self, vbox):
179 vbox.pack_start(
180 g.Label("Enter a brief description of the type, eg 'HTML Page'.\n"
181 "Leave the language blank unless this is a translation, in \n"
182 "which case enter the country code (eg 'fr')."),
183 False, True, 0)
184 hbox = g.HBox(False, 0)
185 vbox.pack_start(hbox, False, True, 0)
187 self.lang = g.Entry()
188 self.lang.set_text(self.item[0] or '')
189 hbox.pack_start(g.Label('Language'), False, True, 0)
190 hbox.pack_start(self.lang, True, True, 0)
191 self.lang.set_activates_default(True)
193 hbox = g.HBox(False, 0)
194 vbox.pack_start(hbox, False, True, 0)
196 self.entry = g.Entry()
197 self.entry.set_text(self.item[1])
198 hbox.pack_start(g.Label('Description'), False, True, 0)
199 hbox.pack_start(self.entry, True, True, 0)
200 self.entry.grab_focus()
201 self.entry.set_activates_default(True)
203 def delete_from_node(self, node):
204 lang = self.item[0] or ''
205 for x in node.childNodes:
206 if x.nodeType != Node.ELEMENT_NODE: continue
207 if x.localName == 'comment' and x.namespaceURI == FREE_NS:
208 if (x.getAttributeNS(XML_NAMESPACE, 'lang') or '') == lang and \
209 data(x) == self.item[1]:
210 x.parentNode.removeChild(x)
211 break
212 else:
213 raise Exception("Can't find this comment in Override.xml!")
215 def add_to_node(self, node):
216 new_lang = self.lang.get_text()
217 new = self.entry.get_text()
218 if not new:
219 raise Invalid("Comment can't be empty")
220 comment = node.ownerDocument.createElementNS(FREE_NS, 'comment')
221 if new_lang:
222 comment.setAttributeNS(XML_NAMESPACE, 'xml:lang', new_lang)
223 data = node.ownerDocument.createTextNode(new)
224 comment.appendChild(data)
225 node.appendChild(comment)
227 class Glob(Field):
228 def get_blank_item(self):
229 return ''
231 def __str__(self):
232 return "Match '%s'" % self.item
234 def add_edit_widgets(self, vbox):
235 vbox.pack_start(
236 g.Label("Enter a glob pattern which matches files of this type.\n"
237 "Special characters are:\n"
238 "? - any one character\n"
239 "* - zero or more characters\n"
240 "[abc] - any character between the brackets\n"
241 "Example: '*.html' matches all files ending in '.html'"),
242 False, True, 0)
243 self.entry = g.Entry()
244 self.entry.set_text(self.item)
245 vbox.pack_start(self.entry, False, True, 0)
246 self.entry.set_activates_default(True)
248 def delete_from_node(self, node):
249 for x in node.childNodes:
250 if x.nodeType != Node.ELEMENT_NODE: continue
251 if x.localName == 'glob' and x.namespaceURI == FREE_NS:
252 if x.getAttributeNS(None, 'pattern') == self.item:
253 x.parentNode.removeChild(x)
254 break
255 else:
256 raise Exception("Can't find this pattern in Override.xml!")
258 def add_to_node(self, node):
259 new = self.entry.get_text()
260 if not new:
261 raise Invalid("Pattern can't be empty")
262 glob = node.ownerDocument.createElementNS(FREE_NS, 'glob')
263 glob.setAttributeNS(None, 'pattern', new)
264 node.appendChild(glob)
266 class Magic(Field):
267 def get_blank_item(self):
268 return (None, Match(None, True))
270 def __str__(self):
271 prio, match = self.item
272 return "Match with priority %s" % prio
274 def add_subtree(self, model, parent, grey):
275 def build(match, parent):
276 for m in match.matches:
277 text = '%s at %s = %s' % (m.type, m.offset, m.value)
278 if m.mask:
279 text += ' masked with ' + m.mask
280 iter = model.append(parent)
281 if match.user:
282 model.set(iter, 0, text, 1, m)
283 else:
284 model.set(iter, 0, text, 1, m, 2, grey)
285 build(m, iter)
286 build(self.item[1], parent)
288 def add_edit_widgets(self, vbox):
289 vbox.pack_start(
290 g.Label("The priority is from 0 (low) to 100 (high).\n"
291 "High priority matches take precedence over low ones."),
292 False, True, 0)
293 prio = self.item[0]
294 if prio is None:
295 prio = 50
296 else:
297 prio = int(prio)
298 self.adj = g.Adjustment(prio, lower = 0, upper = 100, step_incr = 1)
299 spinner = g.SpinButton(self.adj, 1, 0)
300 vbox.pack_start(spinner, False, True, 0)
301 spinner.set_activates_default(True)
303 def __cmp__(self, b):
304 ret = Field.__cmp__(self, b)
305 if ret: return ret
306 return cmp(self.item[1], b.item[1])
308 class XML(Field):
309 def get_blank_item(self):
310 return ('http://example.com', 'documentElement')
312 def __str__(self):
313 return "<%s> with namespace '%s'" % (self.item[1], self.item[0])
315 class Other(Field): pass
317 class FieldParser:
318 def __init__(self, type, attrs):
319 self.type = type
321 def start(self, element, attrs): pass
322 def data(self, data): pass
323 def end(self): pass
325 class CommentParser(FieldParser):
326 def __init__(self, type, attrs, user):
327 FieldParser.__init__(self, type, attrs)
328 self.lang = attrs.get(XML_NAMESPACE + ' lang', None)
329 self.comment = ''
330 self.user = user
332 def data(self, data):
333 self.comment += data
335 def end(self):
336 self.type.add_comment(self.lang, self.comment, self.user)
338 class Match:
339 def __init__(self, parent, user):
340 self.parent = parent
341 self.matches = []
342 self.user = user
344 def __cmp__(self, b):
345 def child_cmp():
346 for x, y in zip(self.matches, b.matches):
347 c = cmp(x, y)
348 if c: return c
349 return 0
351 if not self.parent:
352 return child_cmp()
354 return cmp(self.type, b.type) or cmp(self.offset, b.offset) or \
355 cmp(self.value, b.value) or cmp(self.mask, b.mask) or \
356 child_cmp()
358 def edit(self):
359 rox.alert("Editing of matches isn't currently supported")
361 class MagicParser(FieldParser):
362 def __init__(self, type, attrs, user):
363 FieldParser.__init__(self, type, attrs)
364 self.prio = attrs.get('priority', 50)
365 self.match = Match(None, user)
366 self.user = user
368 def start(self, element, attrs):
369 new = Match(self.match, self.user)
370 new.offset = attrs.get('offset', '?')
371 new.type = attrs.get('type', '?')
372 new.value = attrs.get('value', '?')
373 new.mask = attrs.get('mask', None)
374 self.match.matches.append(new)
376 def end(self):
377 if self.match.parent:
378 self.match = self.match.parent
379 else:
380 self.type.add_magic(self.prio, self.match, self.user)
382 class Scanner:
383 def __init__(self):
384 self.level = 0
385 self.type = None
386 self.handler = None
388 def parse(self, path, user):
389 parser = expat.ParserCreate(namespace_separator = ' ')
390 parser.StartElementHandler = self.start
391 parser.EndElementHandler = self.end
392 parser.CharacterDataHandler = self.data
393 self.user = user
394 parser.ParseFile(file(path))
396 def start(self, element, attrs):
397 self.level += 1
398 if self.level == 1:
399 assert element == FREE_NS + ' mime-info'
400 elif self.level == 2:
401 assert element == FREE_NS + ' mime-type'
402 self.type = get_type(attrs['type'])
403 elif self.level == 3:
404 if element == FREE_NS + ' comment':
405 self.handler = CommentParser(self.type, attrs,
406 self.user)
407 elif element == FREE_NS + ' glob':
408 self.type.add_glob(attrs['pattern'], self.user)
409 elif element == FREE_NS + ' magic':
410 self.handler = MagicParser(self.type, attrs,
411 self.user)
412 elif element == FREE_NS + ' root-XML':
413 self.type.add_xml(attrs['namespaceURI'],
414 attrs['localName'],
415 self.user)
416 else:
417 self.type.add_other(element, self.user)
418 else:
419 assert self.handler
420 self.handler.start(element, attrs)
422 def end(self, element):
423 if self.handler:
424 self.handler.end()
425 self.level -=1
426 if self.level == 1:
427 self.type = None
428 elif self.level == 2:
429 self.handler = None
431 def data(self, data):
432 if self.handler:
433 self.handler.data(data)
435 def scan_file(path, user):
436 if not path.endswith('.xml'): return
437 if not os.path.exists(path):
438 return
439 scanner = Scanner()
440 try:
441 scanner.parse(path, user)
442 except:
443 rox.report_exception()
445 def get_override():
446 from xml.dom import minidom
447 if os.path.exists(user_override):
448 doc = minidom.parse(user_override)
449 else:
450 doc = minidom.Document()
451 node = doc.createElementNS(FREE_NS, 'mime-info')
452 doc.appendChild(node)
453 node.setAttributeNS(XMLNS_NAMESPACE, 'xmlns', FREE_NS)
454 return doc
456 def get_override_type(type_name):
457 doc = get_override()
458 root = doc.documentElement
459 for c in root.childNodes:
460 if c.nodeType != Node.ELEMENT_NODE: continue
461 if c.localName == 'mime-type' and c.namespaceURI == FREE_NS:
462 if c.getAttributeNS(None, 'type') == type_name:
463 return doc, c
464 node = doc.createElementNS(FREE_NS, 'mime-type')
465 node.setAttributeNS(None, 'type', type_name)
466 root.appendChild(node)
467 return doc, node
469 def write_override(doc):
470 home_packages = os.path.join(home_mime, 'packages')
471 if not os.path.isdir(home_packages):
472 os.makedirs(home_packages)
473 path = os.path.join(home_packages, 'Override.xml.new')
474 doc.writexml(file(path, 'w'))
475 os.rename(path, path[:-4])
476 r, w = os.pipe()
477 child = os.fork()
478 if child == 0:
479 # Child
480 try:
481 os.close(r)
482 os.dup2(w, 1)
483 os.dup2(w, 2)
484 os.execlp('update-mime-database', 'update-mime-database', home_mime)
485 finally:
486 os._exit(1)
487 os.close(w)
488 rox.info(os.fdopen(r, 'r').read())
489 os.waitpid(child, 0)
491 import __main__
492 __main__.box.update()
494 def add_type(name):
495 doc = get_override()
497 root = doc.documentElement
498 type_node = doc.createElementNS(FREE_NS, 'mime-type')
499 root.appendChild(type_node)
501 type_node.setAttributeNS(None, 'type', name)
503 write_override(doc)
504 import __main__
505 __main__.box.show_type(name)
507 def delete_type(name):
508 doc = get_override()
509 removed = False
510 for c in doc.documentElement.childNodes:
511 if c.nodeType != Node.ELEMENT_NODE: continue
512 if c.nodeName != 'mime-type': continue
513 if c.namespaceURI != FREE_NS: continue
514 if c.getAttributeNS(None, 'type') != name: continue
515 doc.documentElement.removeChild(c)
516 removed = True
517 if not removed:
518 rox.alert("No user-provided information about this type -- can't remove anything")
519 return
520 if not rox.confirm("Really remove all user-specified information about type %s?" % name,
521 g.STOCK_DELETE):
522 return
523 write_override(doc)
525 # Type names defined even without Override.xml
526 system_types = None
528 def init():
529 "(Re)read the database."
530 global types, system_types
531 if system_types is None:
532 types = {}
533 for mime_dir in ['/usr/share/mime', '/usr/local/share/mime', home_mime]:
534 packages_dir = os.path.join(mime_dir, 'packages')
535 if not os.path.isdir(packages_dir):
536 continue
537 packages = os.listdir(packages_dir)
538 packages.sort()
539 for package in packages:
540 if package == 'Override.xml' and mime_dir is home_mime: continue
541 scan_file(os.path.join(packages_dir, package), False)
542 system_types = types.keys()
543 else:
544 for t in types.keys():
545 if t not in system_types:
546 del types[t]
547 for t in types.values():
548 t.remove_user()
549 scan_file(user_override, True)