coquille--
[minwii.git] / src / minwii / widgets / songfilebrowser.py
index 99f81da..aa34c65 100755 (executable)
@@ -6,21 +6,107 @@ $Id$
 $URL$
 """
 
+import pygame
+from pygame.locals import K_RETURN
 from pgu.gui import FileDialog
+import pgu.gui.basic as basic
+import pgu.gui.input as input
+import pgu.gui.button as button
+import pgu.gui.pguglobals as pguglobals
+import pgu.gui.table as table
+import pgu.gui.area as area
+from pgu.gui.const import *
+from pgu.gui.dialog import Dialog
+from pgu.gui.app import Desktop
+import types
+
 import os
+import tempfile
 from xml.etree import ElementTree
+from minwii.musicxml import musicXml2Song
+
+INDEX_TXT = 'index.txt'
+PICTURE_ITEM_SIZE = 64
+
+def appEventFactory(app, dlg) :
+    # monkey patch de la méthode gestionnaire d'événements :
+    # l'ensemble du Desktop écoute les événements de la roulette de la souris
+    # et les redirige sur la liste déroulante.
+    def _appEvent(self, e) :
+        
+        if dlg.list.vscrollbar:
+            if not hasattr(dlg.list.vscrollbar,'value'): 
+                return False
+
+            if e.type == pygame.locals.MOUSEBUTTONDOWN:
+                if e.button == 4: #wheel up
+                    dlg.list.vscrollbar._click(-1)
+                    return True
+                elif e.button == 5: #wheel down
+                    dlg.list.vscrollbar._click(1)
+                    return True
+        return Desktop.event(self, e)
+
+    return types.MethodType(_appEvent, app)
 
 class FileOpenDialog(FileDialog):
     
     
     
     def __init__(self, path):
-        FileDialog.__init__(self, 
-                            title_txt="Ouvrir une chanson",
-                            button_txt="Ouvrir",
-                            path=path,
-                            )
+        cls1 = 'filedialog'
+        if not path: self.curdir = os.getcwd()
+        else: self.curdir = path
+        self.dir_img = basic.Image(
+            pguglobals.app.theme.get(cls1+'.folder', '', 'image'))
+        self.soundfile_img = basic.Image(
+            pguglobals.app.theme.get(cls1+'.soundfile', '', 'image'))
+        td_style = {'padding_left': 4,
+                    'padding_right': 4,
+                    'padding_top': 2,
+                    'padding_bottom': 2}
+        self.title = basic.Label("Ouvrir une chanson", cls="dialog.title.label")
+        self.body = table.Table()
+        self.list = area.List(width=700, height=250)
+        self.input_dir = input.Input()
+        self.input_file = input.Input()
+        self._current_sort = 'alpha'
+        self._list_dir_()
+        self.button_ok = button.Button("Ouvrir")
+        self.button_sort_alpha = button.Button("A-Z")
+        self.button_sort_alpha.connect(CLICK, self._set_current_sort_, 'alpha')
+        self.button_sort_num = button.Button("0-9")
+        self.button_sort_num.connect(CLICK, self._set_current_sort_, 'num')
+        self.body.tr()
+        self.body.td(basic.Label("Dossier"), style=td_style, align=-1)
+        self.body.td(self.input_dir, style=td_style)
+        self.body.td(self.button_sort_alpha)
+        self.body.td(self.button_sort_num)
+        self.body.tr()
+        self.body.td(self.list, colspan=4, style=td_style)
+        self.list.connect(CHANGE, self._item_select_changed_, None)
+        #self.list.connect(CLICK, self._check_dbl_click_, None)
+        self._last_time_click = pygame.time.get_ticks()
+        self.button_ok.connect(CLICK, self._button_okay_clicked_, None)
+        self.body.tr()
+        self.body.td(basic.Label("Fichier"), style=td_style, align=-1)
+        self.body.td(self.input_file, style=td_style)
+        self.body.td(self.button_ok, style=td_style, colspan=2)
+        self.value = None
+        Dialog.__init__(self, self.title, self.body)
+        
+        # monkey patch
+        app = pguglobals.app
+        self.__regularEventMethod = app.event
+        app.event = appEventFactory(app, self)
     
+    def close(self, w=None) :
+        FileDialog.close(self, w)
+        # retrait du monkey patch
+        app = pguglobals.app
+        app.event = self.__regularEventMethod
+        
+            
     def _list_dir_(self):
         self.input_dir.value = self.curdir
         self.input_dir.pos = len(self.curdir)
@@ -41,25 +127,232 @@ class FileOpenDialog(FileDialog):
         files.sort()
         for i in dirs:
             self.list.add(i, image=self.dir_img, value=i)
-
+        
+        xmlFiles = []
         for i in files:
-            if not i.endswith('.xml') : 
+            if not i.endswith('.xml') :
                 continue
             filepath = os.path.join(self.curdir, i)
-            self.list.add(FileOpenDialog.getSongTitle(filepath), value=i)
+            xmlFiles.append(filepath)
+        
+        if xmlFiles :
+            printableLines = self.getPrintableLines(xmlFiles)
+            for l in printableLines :
+                imgpath = os.path.splitext(os.path.join(self.curdir, l[1]))[0] + '.jpg'
+                if os.path.exists(imgpath) :
+                    img = pygame.image.load(imgpath)
+                    iw, ih = img.get_width(), img.get_height()
+                    style = {}
+                    if iw > ih :
+                        style['width'] = PICTURE_ITEM_SIZE
+                        style['height'] = int(round(PICTURE_ITEM_SIZE * float(ih) / iw))
+                    else :
+                        style['heigth'] = PICTURE_ITEM_SIZE
+                        style['width'] = int(round(PICTURE_ITEM_SIZE * float(iw) / ih))
+                        
+                    img = basic.Image(img, style=style)
+                else :
+                    img = self.soundfile_img
+                self.list.add(l[0], value = l[1], image = img)
  
         self.list.set_vertical_scroll(0)
     
+    def getPrintableLines(self, xmlFiles) :
+        index = self.getUpdatedIndex(xmlFiles)
+
+        printableLines = []
+        for l in index :
+            l = l.strip()
+            l = l.split('\t')
+            printableLines.append(('%s - %s / %s' % (l[2], l[3], l[4]), l[0]))
+        
+        return printableLines
+    
+    
     @staticmethod
     def getSongTitle(file) :
         it = ElementTree.iterparse(file, ['start', 'end'])
         creditFound = False
+        title = os.path.basename(file)
         
         for evt, el in it :
             if el.tag == 'credit' :
                 creditFound = True
             if el.tag == 'credit-words' and creditFound:
-                return el.text.encode('iso-8859-1')
+                title = el.text
+                break
             if el.tag == 'part-list' :
-                # plus de chance de trouver un titre
-                return os.path.basename(file)
\ No newline at end of file
+                # au delà de ce tag : aucune chance de trouver un titre
+                break
+        return title
+    
+    @staticmethod
+    def getSongMetadata(file) :
+        metadata = {}
+        metadata['title'] = FileOpenDialog.getSongTitle(file).encode('iso-8859-1')
+        metadata['mtime'] = str(os.stat(file).st_mtime)
+        metadata['file'] = os.path.basename(file)
+        song = musicXml2Song(file)
+        metadata['distinctNotes'] = len(song.distinctNotes)
+        
+        histo = song.intervalsHistogram
+        coeffInter = reduce(lambda a, b : a + b,
+                            [abs(k) * v for k, v in histo.items()])
+
+        totInter = reduce(lambda a, b: a+b, histo.values())
+        totInter = totInter - histo.get(0, 0)
+        difficulty = int(round(float(coeffInter) / totInter, 0))
+        metadata['difficulty'] = difficulty
+
+        return metadata
+    
+    def getUpdatedIndex(self, xmlFiles) :
+        indexTxtPath = os.path.join(self.curdir, INDEX_TXT)
+        index = []
+        
+        if not os.path.exists(indexTxtPath) :
+            musicXmlFound = False
+            tmp = tempfile.TemporaryFile(mode='r+')
+            for file in xmlFiles :
+                try :
+                    metadata = FileOpenDialog.getSongMetadata(file)
+                    musicXmlFound = True
+                except ValueError, e :
+                    print e
+                    if e.args and e.args[0] == 'not a musicxml file' :
+                        continue
+                
+                line = '%(file)s\t%(mtime)s\t%(title)s\t%(distinctNotes)d\t%(difficulty)d\n' % metadata
+                index.append(line)
+                tmp.write(line)
+            
+            if musicXmlFound :
+                tmp.seek(0)
+                indexFile = open(indexTxtPath, 'w')
+                indexFile.write(tmp.read())
+                indexFile.close()
+            tmp.close()
+        else :
+            indexedFiles = {}
+            indexTxt = open(indexTxtPath, 'r')
+
+            # check if index is up to date, and update entries if so.
+            for l in filter(None, indexTxt.readlines()) :
+                parts = l.split('\t')
+                fileBaseName, modificationTime = parts[0], parts[1]
+                filePath = os.path.join(self.curdir, fileBaseName)
+
+                if not os.path.exists(filePath) :
+                    continue
+
+                indexedFiles[fileBaseName] = l
+                currentMtime = str(os.stat(filePath).st_mtime)
+                
+                # check modification time missmatch
+                if currentMtime != modificationTime :
+                    try :
+                        metadata = FileOpenDialog.getSongMetadata(filePath)
+                        musicXmlFound = True
+                    except ValueError, e :
+                        print e
+                        if e.args and e.args[0] == 'not a musicxml file' :
+                            continue
+                    
+                    metadata = FileOpenDialog.getSongMetadata(filePath)
+                    line = '%(file)s\t%(mtime)s\t%(title)s\t%(distinctNotes)d\t%(difficulty)d\n' % metadata
+                    indexedFiles[fileBaseName] = line
+            
+            # check for new files.
+            for file in xmlFiles :
+                fileBaseName = os.path.basename(file)
+                if not indexedFiles.has_key(fileBaseName) :
+                    try :
+                        metadata = FileOpenDialog.getSongMetadata(filePath)
+                        musicXmlFound = True
+                    except ValueError, e :
+                        print e
+                        if e.args and e.args[0] == 'not a musicxml file' :
+                            continue
+                
+                    metadata = FileOpenDialog.getSongMetadata(file)
+                    line = '%(file)s\t%(mtime)s\t%(title)s\t%(distinctNotes)d\t%(difficulty)d\n' % metadata
+                    indexedFiles[fileBaseName] = line
+            
+            # ok, the index is up to date !
+            
+            index = indexedFiles.values()
+            
+
+        if self._current_sort == 'alpha' :
+            def s(a, b) :
+                da = desacc(a.split('\t')[2]).lower()
+                db = desacc(b.split('\t')[2]).lower()
+                return cmp(da, db)
+                
+        elif self._current_sort == 'num' :
+            def s(a, b) :
+                da = int(a.split('\t')[3])
+                db = int(b.split('\t')[3])
+                return cmp(da, db)
+        else :
+            s = cmp
+        
+        index.sort(s)
+        return index
+    
+    def _set_current_sort_(self, arg) :
+        self._current_sort = arg
+        self.list.clear()
+        self._list_dir_()
+    
+    def _check_dbl_click_(self, arg) :
+        if pygame.time.get_ticks() - self._last_time_click < 300 :
+            self._button_okay_clicked_(None)
+        else :
+            self._last_time_click = pygame.time.get_ticks()
+    
+    def event(self, e) :
+        FileDialog.event(self, e)
+        
+        if e.type == CLICK and \
+           e.button == 1 and \
+           self.list.rect.collidepoint(e.pos) :
+            self._check_dbl_click_(e)
+        
+        if e.type == KEYDOWN and e.key == K_RETURN :
+            self._button_okay_clicked_(None)
+            
+
+# utils
+from unicodedata import decomposition
+from string import printable
+_printable = dict([(c, True) for c in printable])
+isPrintable = _printable.has_key
+
+def _recurseDecomposition(uc):
+    deco = decomposition(uc).split()
+    fullDeco = []
+    if deco :
+        while (deco) :
+            code = deco.pop()
+            if code.startswith('<') :
+                continue
+            c = unichr(int(code, 16))
+            subDeco = decomposition(c).split()
+            if subDeco :
+                deco.extend(subDeco)
+            else :
+                fullDeco.append(c)
+        fullDeco.reverse()
+    else :
+        fullDeco.append(uc)
+    
+    fullDeco = u''.join(filter(lambda c : isPrintable(c), fullDeco))
+    return fullDeco
+
+def desacc(s, encoding='iso-8859-1') :
+    us = s.decode(encoding, 'ignore')
+    ret = []
+    for uc in us :
+        ret.append(_recurseDecomposition(uc))
+    return u''.join(ret)
\ No newline at end of file