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