coquille--
[minwii.git] / src / minwii / widgets / songfilebrowser.py
1 # -*- coding: utf-8 -*-
2 """
3 Boîte de dialogue pour sélection des chansons.
4
5 $Id$
6 $URL$
7 """
8
9 import pygame
10 from pygame.locals import K_RETURN
11 from pgu.gui import FileDialog
12 import pgu.gui.basic as basic
13 import pgu.gui.input as input
14 import pgu.gui.button as button
15 import pgu.gui.pguglobals as pguglobals
16 import pgu.gui.table as table
17 import pgu.gui.area as area
18 from pgu.gui.const import *
19 from pgu.gui.dialog import Dialog
20 from pgu.gui.app import Desktop
21 import types
22
23 import os
24 import tempfile
25 from xml.etree import ElementTree
26 from minwii.musicxml import musicXml2Song
27
28 INDEX_TXT = 'index.txt'
29 PICTURE_ITEM_SIZE = 64
30
31 def appEventFactory(app, dlg) :
32 # monkey patch de la méthode gestionnaire d'événements :
33 # l'ensemble du Desktop écoute les événements de la roulette de la souris
34 # et les redirige sur la liste déroulante.
35 def _appEvent(self, e) :
36
37 if dlg.list.vscrollbar:
38 if not hasattr(dlg.list.vscrollbar,'value'):
39 return False
40
41 if e.type == pygame.locals.MOUSEBUTTONDOWN:
42 if e.button == 4: #wheel up
43 dlg.list.vscrollbar._click(-1)
44 return True
45 elif e.button == 5: #wheel down
46 dlg.list.vscrollbar._click(1)
47 return True
48 return Desktop.event(self, e)
49
50 return types.MethodType(_appEvent, app)
51
52 class FileOpenDialog(FileDialog):
53
54
55
56 def __init__(self, path):
57 cls1 = 'filedialog'
58 if not path: self.curdir = os.getcwd()
59 else: self.curdir = path
60 self.dir_img = basic.Image(
61 pguglobals.app.theme.get(cls1+'.folder', '', 'image'))
62 self.soundfile_img = basic.Image(
63 pguglobals.app.theme.get(cls1+'.soundfile', '', 'image'))
64 td_style = {'padding_left': 4,
65 'padding_right': 4,
66 'padding_top': 2,
67 'padding_bottom': 2}
68 self.title = basic.Label("Ouvrir une chanson", cls="dialog.title.label")
69 self.body = table.Table()
70 self.list = area.List(width=700, height=250)
71 self.input_dir = input.Input()
72 self.input_file = input.Input()
73 self._current_sort = 'alpha'
74 self._list_dir_()
75 self.button_ok = button.Button("Ouvrir")
76 self.button_sort_alpha = button.Button("A-Z")
77 self.button_sort_alpha.connect(CLICK, self._set_current_sort_, 'alpha')
78 self.button_sort_num = button.Button("0-9")
79 self.button_sort_num.connect(CLICK, self._set_current_sort_, 'num')
80 self.body.tr()
81 self.body.td(basic.Label("Dossier"), style=td_style, align=-1)
82 self.body.td(self.input_dir, style=td_style)
83 self.body.td(self.button_sort_alpha)
84 self.body.td(self.button_sort_num)
85 self.body.tr()
86 self.body.td(self.list, colspan=4, style=td_style)
87 self.list.connect(CHANGE, self._item_select_changed_, None)
88 #self.list.connect(CLICK, self._check_dbl_click_, None)
89 self._last_time_click = pygame.time.get_ticks()
90 self.button_ok.connect(CLICK, self._button_okay_clicked_, None)
91 self.body.tr()
92 self.body.td(basic.Label("Fichier"), style=td_style, align=-1)
93 self.body.td(self.input_file, style=td_style)
94 self.body.td(self.button_ok, style=td_style, colspan=2)
95 self.value = None
96 Dialog.__init__(self, self.title, self.body)
97
98 # monkey patch
99 app = pguglobals.app
100 self.__regularEventMethod = app.event
101 app.event = appEventFactory(app, self)
102
103 def close(self, w=None) :
104 FileDialog.close(self, w)
105 # retrait du monkey patch
106 app = pguglobals.app
107 app.event = self.__regularEventMethod
108
109
110 def _list_dir_(self):
111 self.input_dir.value = self.curdir
112 self.input_dir.pos = len(self.curdir)
113 self.input_dir.vpos = 0
114 dirs = []
115 files = []
116 try:
117 for i in os.listdir(self.curdir):
118 if i.startswith('.') : continue
119 if os.path.isdir(os.path.join(self.curdir, i)): dirs.append(i)
120 else: files.append(i)
121 except:
122 self.input_file.value = "Dossier innacessible !"
123
124 dirs.sort()
125 dirs.insert(0, '..')
126
127 files.sort()
128 for i in dirs:
129 self.list.add(i, image=self.dir_img, value=i)
130
131 xmlFiles = []
132 for i in files:
133 if not i.endswith('.xml') :
134 continue
135 filepath = os.path.join(self.curdir, i)
136 xmlFiles.append(filepath)
137
138 if xmlFiles :
139 printableLines = self.getPrintableLines(xmlFiles)
140 for l in printableLines :
141 imgpath = os.path.splitext(os.path.join(self.curdir, l[1]))[0] + '.jpg'
142 if os.path.exists(imgpath) :
143 img = pygame.image.load(imgpath)
144 iw, ih = img.get_width(), img.get_height()
145 style = {}
146 if iw > ih :
147 style['width'] = PICTURE_ITEM_SIZE
148 style['height'] = int(round(PICTURE_ITEM_SIZE * float(ih) / iw))
149 else :
150 style['heigth'] = PICTURE_ITEM_SIZE
151 style['width'] = int(round(PICTURE_ITEM_SIZE * float(iw) / ih))
152
153 img = basic.Image(img, style=style)
154 else :
155 img = self.soundfile_img
156 self.list.add(l[0], value = l[1], image = img)
157
158 self.list.set_vertical_scroll(0)
159
160 def getPrintableLines(self, xmlFiles) :
161 index = self.getUpdatedIndex(xmlFiles)
162
163 printableLines = []
164 for l in index :
165 l = l.strip()
166 l = l.split('\t')
167 printableLines.append(('%s - %s / %s' % (l[2], l[3], l[4]), l[0]))
168
169 return printableLines
170
171
172 @staticmethod
173 def getSongTitle(file) :
174 it = ElementTree.iterparse(file, ['start', 'end'])
175 creditFound = False
176 title = os.path.basename(file)
177
178 for evt, el in it :
179 if el.tag == 'credit' :
180 creditFound = True
181 if el.tag == 'credit-words' and creditFound:
182 title = el.text
183 break
184 if el.tag == 'part-list' :
185 # au delà de ce tag : aucune chance de trouver un titre
186 break
187 return title
188
189 @staticmethod
190 def getSongMetadata(file) :
191 metadata = {}
192 metadata['title'] = FileOpenDialog.getSongTitle(file).encode('iso-8859-1')
193 metadata['mtime'] = str(os.stat(file).st_mtime)
194 metadata['file'] = os.path.basename(file)
195 song = musicXml2Song(file)
196 metadata['distinctNotes'] = len(song.distinctNotes)
197
198 histo = song.intervalsHistogram
199 coeffInter = reduce(lambda a, b : a + b,
200 [abs(k) * v for k, v in histo.items()])
201
202 totInter = reduce(lambda a, b: a+b, histo.values())
203 totInter = totInter - histo.get(0, 0)
204 difficulty = int(round(float(coeffInter) / totInter, 0))
205 metadata['difficulty'] = difficulty
206
207 return metadata
208
209 def getUpdatedIndex(self, xmlFiles) :
210 indexTxtPath = os.path.join(self.curdir, INDEX_TXT)
211 index = []
212
213 if not os.path.exists(indexTxtPath) :
214 musicXmlFound = False
215 tmp = tempfile.TemporaryFile(mode='r+')
216 for file in xmlFiles :
217 try :
218 metadata = FileOpenDialog.getSongMetadata(file)
219 musicXmlFound = True
220 except ValueError, e :
221 print e
222 if e.args and e.args[0] == 'not a musicxml file' :
223 continue
224
225 line = '%(file)s\t%(mtime)s\t%(title)s\t%(distinctNotes)d\t%(difficulty)d\n' % metadata
226 index.append(line)
227 tmp.write(line)
228
229 if musicXmlFound :
230 tmp.seek(0)
231 indexFile = open(indexTxtPath, 'w')
232 indexFile.write(tmp.read())
233 indexFile.close()
234 tmp.close()
235 else :
236 indexedFiles = {}
237 indexTxt = open(indexTxtPath, 'r')
238
239 # check if index is up to date, and update entries if so.
240 for l in filter(None, indexTxt.readlines()) :
241 parts = l.split('\t')
242 fileBaseName, modificationTime = parts[0], parts[1]
243 filePath = os.path.join(self.curdir, fileBaseName)
244
245 if not os.path.exists(filePath) :
246 continue
247
248 indexedFiles[fileBaseName] = l
249 currentMtime = str(os.stat(filePath).st_mtime)
250
251 # check modification time missmatch
252 if currentMtime != modificationTime :
253 try :
254 metadata = FileOpenDialog.getSongMetadata(filePath)
255 musicXmlFound = True
256 except ValueError, e :
257 print e
258 if e.args and e.args[0] == 'not a musicxml file' :
259 continue
260
261 metadata = FileOpenDialog.getSongMetadata(filePath)
262 line = '%(file)s\t%(mtime)s\t%(title)s\t%(distinctNotes)d\t%(difficulty)d\n' % metadata
263 indexedFiles[fileBaseName] = line
264
265 # check for new files.
266 for file in xmlFiles :
267 fileBaseName = os.path.basename(file)
268 if not indexedFiles.has_key(fileBaseName) :
269 try :
270 metadata = FileOpenDialog.getSongMetadata(filePath)
271 musicXmlFound = True
272 except ValueError, e :
273 print e
274 if e.args and e.args[0] == 'not a musicxml file' :
275 continue
276
277 metadata = FileOpenDialog.getSongMetadata(file)
278 line = '%(file)s\t%(mtime)s\t%(title)s\t%(distinctNotes)d\t%(difficulty)d\n' % metadata
279 indexedFiles[fileBaseName] = line
280
281 # ok, the index is up to date !
282
283 index = indexedFiles.values()
284
285
286 if self._current_sort == 'alpha' :
287 def s(a, b) :
288 da = desacc(a.split('\t')[2]).lower()
289 db = desacc(b.split('\t')[2]).lower()
290 return cmp(da, db)
291
292 elif self._current_sort == 'num' :
293 def s(a, b) :
294 da = int(a.split('\t')[3])
295 db = int(b.split('\t')[3])
296 return cmp(da, db)
297 else :
298 s = cmp
299
300 index.sort(s)
301 return index
302
303 def _set_current_sort_(self, arg) :
304 self._current_sort = arg
305 self.list.clear()
306 self._list_dir_()
307
308 def _check_dbl_click_(self, arg) :
309 if pygame.time.get_ticks() - self._last_time_click < 300 :
310 self._button_okay_clicked_(None)
311 else :
312 self._last_time_click = pygame.time.get_ticks()
313
314 def event(self, e) :
315 FileDialog.event(self, e)
316
317 if e.type == CLICK and \
318 e.button == 1 and \
319 self.list.rect.collidepoint(e.pos) :
320 self._check_dbl_click_(e)
321
322 if e.type == KEYDOWN and e.key == K_RETURN :
323 self._button_okay_clicked_(None)
324
325
326 # utils
327 from unicodedata import decomposition
328 from string import printable
329 _printable = dict([(c, True) for c in printable])
330 isPrintable = _printable.has_key
331
332 def _recurseDecomposition(uc):
333 deco = decomposition(uc).split()
334 fullDeco = []
335 if deco :
336 while (deco) :
337 code = deco.pop()
338 if code.startswith('<') :
339 continue
340 c = unichr(int(code, 16))
341 subDeco = decomposition(c).split()
342 if subDeco :
343 deco.extend(subDeco)
344 else :
345 fullDeco.append(c)
346 fullDeco.reverse()
347 else :
348 fullDeco.append(uc)
349
350 fullDeco = u''.join(filter(lambda c : isPrintable(c), fullDeco))
351 return fullDeco
352
353 def desacc(s, encoding='iso-8859-1') :
354 us = s.decode(encoding, 'ignore')
355 ret = []
356 for uc in us :
357 ret.append(_recurseDecomposition(uc))
358 return u''.join(ret)