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