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