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