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