# -*- coding: ISO-8859-1 -*-
""" capellaScript -- (c) Paul Villiger
>>> Grafische Taktierung

    Mit diesem Skript können grafische Taktierungen erstellt werden.
    Bereich:    Einzelne Notenzeilen oder ganze Partitur
    Schriftart: beliebig (Skript lernt)
    Vertikale Position:
        - auf der Notenzeile
        - unter/oberhalb Notenzeile
        - zwischen Notenzeilen

<<<

History:    16.09.2006 - Erstausgabe
            20.09.2006 - Nur der letzte Fonteintrag wurde verwendet
"""

from xml.dom.minidom import parseString, NodeList, Node, Element
from caplib.capDOM import ScoreChange
import tempfile, string, new

scriptTag = '21793-20'

def latin1(u):
    return u.encode('Latin-1')

doc = parseString('<score/>')

def gotoChild(self, name, new=False):
    newEl = None
    if new:
        pass
    else:
        for child in self.childNodes:
            if child.nodeType == child.ELEMENT_NODE and child.tagName == name:
                newEl = child
                break
    if newEl == None:
        newEl = doc.createElement(name)
        self.appendChild(newEl)
    return newEl
Node.gotoChild = new.instancemethod(gotoChild,None,Node)

def getElementObjects(objList):  # returns a List
    newList = NodeList()
    for n in range(objList.length):
        if objList[n].nodeType == objList[n].ELEMENT_NODE:
            newList.append(objList[n])
    return newList

def getCursor():
    sel = curSelection()
    result = None
    if sel == 0:
        messageBox('Fehler', 'keine aktive Partitur')
        return result
    result = sel
    return result




fontDict= {}
def collectFonts(score):
# Diese Prozedur sucht nach allen in der Partitur vorhandenen Schriften
# und speichert diese in fontDict ab

    for font in score.getElementsByTagName('font'):
        face = latin1(font.getAttribute('face'))
        if face and ( 'capella' not in face) :
            fontDict[face] = face

    # Einige Standardschriften laden (kann bei Bedarf erweiter werden)
    fonts = ['Arial',
             'Tahoma',
             'Times New Roman']
    for face in fonts:
        fontDict[face] = face


class settings:
    def __init__(self):
        self.action    = 0      #
        self.range     = 0
        self.face      = ''
        self.faces      = {} 
        self.fontSize  = 24     # 
        self.fontWeight = 400    #
        self.fontItalic = False
        self.offset    = 0      # Abstand von der Mittellinie
     
defaults = settings()
dlgSet = settings()

options = ScriptOptions() 
opt = options.get()


def getOptions():
    global dlgSet
    dlgSet.action   = eval(opt.get('action',str(dlgSet.action)))
    dlgSet.range    = eval(opt.get('range',str(dlgSet.range)))
    dlgSet.faces = eval(opt.get('faces',str(dlgSet.faces)))
    dlgSet.faces.update(fontDict)
    dlgSet.fontSize = eval(opt.get('fontSize',str(dlgSet.fontSize)))
    dlgSet.fontWeight = eval(opt.get('fontWeight',str(dlgSet.fontWeight)))
    dlgSet.fontItalic = eval(opt.get('fontItalic',str(dlgSet.fontItalic)))
    dlgSet.offset    = eval(opt.get('offset',str(dlgSet.offset)))
    dlgSet.face = opt.get('face', 'Arial')

def setOptions():
    opt.update(dict(action   = str(dlgSet.action),
                    range    = str(dlgSet.range),
                    faces    = str(dlgSet.faces),
                    face     = str(dlgSet.face),
                    fontSize = str(dlgSet.fontSize),
                    fontWeight = str(dlgSet.fontWeight),
                    fontItalic = str(dlgSet.fontItalic),
                    offset   = str(dlgSet.offset)
                    ))
    options.set(opt)


def getTextSize(cont):
    # Diese Prozedur berechnet die Textgrösse (Breite und Höhe)
    # die Schriftart muss vor dem Aufruf in dlgSet vorhanden sein
    textFont = dict(face    = dlgSet.face,
                    height  = dlgSet.fontSize,
                    weight  = dlgSet.fontWeight,
                    italic  = dlgSet.fontItalic)

    text = dict(type = 'text', content = latin1(cont), font = textFont)
    res = (10,10)           # Default, wenn score keine Note enthaelt
    for noteObject in activeScore().noteObjs():
        if noteObject.isRest() or noteObject.isChord():
            res = noteObject.textSize(text)
            break
    return res


def getUserInput():

    actions =   { 0 : 'Taktierung verbergen',
                  1 : 'Taktierung in Zeile',
                  2 : 'Taktierung zwischen oberen Notenzeilen',
                  3 : 'Taktierung zwischen unteren Notenzeilen',
                  4 : 'Automatische Taktierung'
                }
    
    getAction = Radio(actions.values(), value = dlgSet.action)
    getRange =  Radio(['Aktuelle Notenzeile                                        ',
                       'Ganze Partitur'], value = dlgSet.range, width = 40)
    getOffset = Edit(str(dlgSet.offset), width = 10)

    _fonts = dlgSet.faces.keys()
    _fonts.sort()
    i = v = 0
    for f in _fonts:
        if f == dlgSet.face:
            v = i
            break
        i +=1
    getFont = ComboBox(_fonts, value = i, width = 25)
    getFontHeight = Edit(str(dlgSet.fontSize), width = 10)
    getFontItalic = CheckBox('  kursiv    ', value = dlgSet.fontItalic)
    getFontBold = CheckBox('  fett    ', value = dlgSet.fontWeight == 700)
    

    dlg = Dialog(' Grafische Taktierung ',
                 VBox([VBox([HBox([Label('  '), getAction]),
                             HBox([Label('        Abstand von der Mittellinie    '),
                                   getOffset
                                   ])
                             ], text = 'Aktion'),
                       HBox([Label('  '),getRange], text = 'Bereich', width = 50),
                       VBox([getFont,
                             HBox([getFontHeight,
                                   Label('    Fontgrösse   '),
                                   getFontItalic,
                                   getFontBold])
                             ],
                             text = '  Schrift  '
                            )
                       ],
                       padding = 6)
                 )

    if dlg.run():
        dlgSet.action = getAction.value()
        dlgSet.range = getRange.value()
        try: dlgSet.offset = float(getOffset.value())
        except: dlgSet.offset = 0.0
        dlgSet.face = _fonts[getFont.value()]
        dlgSet.fontSize = float(getFontHeight.value())
        if getFontBold.value():
            dlgSet.fontWeight = 700
        else:
            dlgSet.fontWeight = 400
        dlgSet.fontItalic = getFontItalic.value()
    else:
        dlgSet.action = -1
        

def removeGraficTimeSign(voice):
    # entfernt alle vom Script erstellten Grafikobjekte anhand des Tag's
    for drawObj in voice.getElementsByTagName('drawObj'):
        for basic in drawObj.getElementsByTagName('basic'):
            if basic.getAttribute('tag') == scriptTag:
                drawObj.parentNode.removeChild(drawObj)
                continue
    # drawObjects müssen auch entfernt werden, sonst stürzt capella u.U. ab
    for drawObjects in voice.getElementsByTagName('drawObjects'):
        if not drawObjects.getElementsByTagName('drawObj'):
            drawObjects.parentNode.removeChild(drawObjects)


def getStaffDistances(score, voice):
    #   diese Prozedur berechnet die Abstände zur vorhergehenden und
    #   nachfolgenden Notenzeile in einem System. Der Abstand zum Nachbarsystem
    #   wird nicht berechnet und erhält den Wert 0.
    
    staff = voice.parentNode.parentNode

    # Abstände aus dem Mustersystem auslesen und in Dict layoutDist speichern
    layout = score.gotoChild('layout')
    layoutDist = {'between' : '0', 'top' : '0' }
    for staffLayout in layout.getElementsByTagName('staffLayout'):
        for distances in staffLayout.getElementsByTagName('distances'):
            top = distances.getAttribute('top')
            bottom = distances.getAttribute('bottom')
            if distances.getAttribute('group'):
                bottom = str(int(bottom) + int(distances.getAttribute('group')))
            layoutDist[latin1(staffLayout.getAttribute('description'))] = [top, bottom]


    # Vorgänger- und Nachfolger-Notenzeile bestimmen
    staves = staff.parentNode
    prevStaff = None
    nextStaff = None
    found = False
    for st in staves.getElementsByTagName('staff'):
        if found:
            nextStaff = st
            break
        elif st == staff:
            found = True
        else:
            prevStaff = st

    topDist = 0.0
    bottomDist = 0.0

    if prevStaff:
        topDist += float( layoutDist[latin1(prevStaff.getAttribute('layout'))][1] )
        topDist += float( layoutDist[latin1(staff.getAttribute('layout'))][0] )
        for extraDist in prevStaff.getElementsByTagName('extraDistance'):
            if extraDist.hasAttribute('bottom'):
                topDist += float( extraDist.getAttribute('bottom') )
        for extraDist in staff.getElementsByTagName('extraDistance'):
            if extraDist.hasAttribute('top'):
                topDist += float( extraDist.getAttribute('top') )
    if nextStaff:
        bottomDist += float( layoutDist[latin1(nextStaff.getAttribute('layout'))][0] )
        bottomDist += float( layoutDist[latin1(staff.getAttribute('layout'))][1] )
        for extraDist in staff.getElementsByTagName('extraDistance'):
            if extraDist.hasAttribute('bottom'):
                bottomDist += float( extraDist.getAttribute('bottom') )
        for extraDist in nextStaff.getElementsByTagName('extraDistance'):
            if extraDist.hasAttribute('top'):
                bottomDist += float( extraDist.getAttribute('top') )

    return topDist, bottomDist

def getNoteLines(score, voice):
    staff = voice.parentNode.parentNode
    layout = staff.getAttribute('layout')
    for staffLayout in score.getElementsByTagName('staffLayout'):
        if staffLayout.getAttribute('description') == layout:
            notation = staffLayout.gotoChild('notation')
            notelines = notation.getAttribute('notelines')
            if not notelines:
                notelines = [0,0,0,1,1,1,1,1,0,0,0]
            elif notelines == '1':
                notelines = [0,0,0,0,0,1,0,0,0,0,0]
            else:
                nl = []
                for c in notelines:
                    if c == '_':
                        nl.append(0)
                    if c == '|':
                        nl.append(1)
                notelines = nl
    return notelines


def getTimeStrings(timeSign):
    time0 = timeSign.getAttribute('time')
    if time0 == 'C':
        time0 = '4/4'
    elif time0 == 'allaBreve':
        time0 = '2/2'

    return time0.split('/') # Aufteilen in Zähler Nenner

def setGraficTimeSign(chord, timeSign, pos, yPos):
    time1, time2 = getTimeStrings(timeSign)
    
    size1 = getTextSize(time1)[0]
    size2 = getTextSize(time2)[0]
    tSize = max(size1, size2)
    
    drawObjects = chord.gotoChild('drawObjects')
    drawObj = drawObjects.gotoChild('drawObj', True)
    group = drawObj.gotoChild('group')

    # Set Scripttag for gropup
    basic = drawObj.gotoChild('basic')
    basic.setAttribute('tag', scriptTag)

    # Zähler zeichnen
    drawObj = group.gotoChild('drawObj', True)
    text = drawObj.gotoChild('text')
    text.setAttribute('x', str(pos + tSize/2))
    text.setAttribute('y', str(- yPos - dlgSet.fontSize/256.0))
    text.setAttribute('align', 'center')
                
    font = text.gotoChild('font')
    font.setAttribute('face',dlgSet.face)
    font.setAttribute('height',str(dlgSet.fontSize))
    font.setAttribute('weight', str(dlgSet.fontWeight))
    font.setAttribute('italic', (str(dlgSet.fontItalic == 1).lower()))
    
    content = text.gotoChild('content')
    textNode = doc.createTextNode(time1)
    content.appendChild(textNode)            

    # Nenner zeichnen
    drawObj = group.gotoChild('drawObj', True)
    text = drawObj.gotoChild('text')
    text.setAttribute('x', str(pos + tSize/2))
    text.setAttribute('y', str(- yPos - dlgSet.fontSize/256.0 + dlgSet.fontSize / 6.0))
    text.setAttribute('align', 'center')
                
    font = text.gotoChild('font')
    font.setAttribute('face',dlgSet.face)
    font.setAttribute('height',str(dlgSet.fontSize))
    font.setAttribute('weight', str(dlgSet.fontWeight))
    font.setAttribute('italic', (str(dlgSet.fontItalic == 1).lower()))
    
    content = text.gotoChild('content')
    textNode = doc.createTextNode(time2)
    content.appendChild(textNode)


def hideTimeSigns(score, voice, timeSignPos, setMeter):

    distTop, distBottom = getStaffDistances(score, voice)

    # Notenlinien nachziehen
    count = 0
    prevChord = None
    lastTimeSign = []
    noteObjects = getElementObjects(voice.gotoChild('noteObjects').childNodes)
    for noteObject in noteObjects:
        if noteObject.tagName in ['chord','rest']:
            prevChord = noteObject

        elif noteObject.tagName == 'timeSign':
            lastTimeSign.append(noteObject)

        if prevChord and lastTimeSign:
            for ts in lastTimeSign:
                if timeSignPos[count][1]:
                    pos = timeSignPos[count][0] - timeSignPos[count][1]  # Vorgänger
                else:
                    pos = timeSignPos[count][0] - timeSignPos[count][2]  # Nachfolger

                time1, time2 = getTimeStrings(ts)
                timeLen = max(len(time1), len(time2))
                deltaLeft = 0.2
                charWidth = 1.6 * timeLen 

                drawObjects = prevChord.gotoChild('drawObjects')

                drawObj = drawObjects.gotoChild('drawObj', True)
                group = drawObj.gotoChild('group')
                basic = drawObj.gotoChild('basic')
                basic.setAttribute('tag', scriptTag)

                # Rechteck zeichnen weiss
                drawObj = group.gotoChild('drawObj', True)
                rectangle = drawObj.gotoChild('rectangle')
                rectangle.setAttribute('lineWidth', '0')
                rectangle.setAttribute('filled', 'true')
                rectangle.setAttribute('fillColor', 'FFFFFF')
                rectangle.setAttribute('lineWidth', '0')
                rectangle.setAttribute('x1', str(pos - deltaLeft))
                rectangle.setAttribute('y1', str( -2.0))
                rectangle.setAttribute('x2', str(pos - deltaLeft + charWidth))
                rectangle.setAttribute('y2', str(  2.0))

                nl = getNoteLines(score, voice)
                y = 5
                for n in nl:
                    if n == 1:
                        drawObj = group.gotoChild('drawObj', True)
                        line = drawObj.gotoChild('line')
                        line.setAttribute('x1',str(pos - deltaLeft))
                        line.setAttribute('x2',str(pos - deltaLeft + charWidth))
                        line.setAttribute('y1',str(y))
                        line.setAttribute('y2',str(y))
                    y -= 1
                    
                    
                
                """
                # Notenlinien zeichnen
                drawObj = drawObjects.gotoChild('drawObj', True)
                notelines = drawObj.gotoChild('notelines')
                notelines.setAttribute('x1',str(pos - deltaLeft))
                notelines.setAttribute('x2',str(pos - deltaLeft + charWidth))
                notelines.setAttribute('y','0')
                # Set Scripttag
                basic = drawObj.gotoChild('basic')
                basic.setAttribute('tag', scriptTag)
                """
            
                count += 1

                if setMeter:
                    if dlgSet.action == 1:
                        setGraficTimeSign(prevChord, ts, pos, dlgSet.offset )
                    elif dlgSet.action == 2:
                        setGraficTimeSign(prevChord, ts, pos, distTop / 2 )
                    elif dlgSet.action == 3:
                        setGraficTimeSign(prevChord, ts, pos, - distBottom / 2 )

            lastTimeSign = []

def getTimeSignPos(sy, st, vo):
    # in einer Stimme werden alle Taktpositionen (timeSign) gesucht
    # Die Ausgabe enthält eine Liste mit [Timesign Position, previous Chord Position, following Chord Posistion]
    
    timeSignPos = []
    system = activeScore().system(sy)
    staff = system.staff(st)
    voice = staff.voice(vo)
    prevPos = 0
    for noteObj in voice.noteObjs():
        if noteObj.subType() in [NoteObj.CHORD, NoteObj.REST] :
            prevPos = noteObj.posX()
            if timeSignPos and timeSignPos[-1][-1] == 0:
                timeSignPos[-1][-1] = prevPos
        elif  noteObj.subType() == NoteObj.METER:
            timeSignPos.append([noteObj.posX(), prevPos, 0])
    return timeSignPos



def changeDoc(score):
    collectFonts(score)
    getOptions()
    getUserInput()
    if dlgSet.action == -1:  # Abbruch
        return
    setOptions()
    
    sel = getCursor()
    if not sel:
        return
    (sy1, st1,vo1,ob1), (sy2,st2,vo2,ob2) = sel

    # Namen der selektierte Notenzeile bestimmen
    sy = 0
    for system in score.getElementsByTagName('system'):
        st = 0
        for staff in system.getElementsByTagName('staff'):
            if sy == sy1 and st == st1:
                staffLayout = staff.getAttribute('layout')
            st += 1
        sy += 1

    sy = 0
    for system in score.getElementsByTagName('system'):
        st = 0
        for staff in system.getElementsByTagName('staff'):
            # Ganze Partitur oder selektierte Notenzeile
            if dlgSet.range == 1 or staffLayout == staff.getAttribute('layout'):
                vo = 0
                for voice in staff.getElementsByTagName('voice'):
                    removeGraficTimeSign(voice)
                    if dlgSet.action <> 4:
                        timeSignPos = getTimeSignPos(sy, st, vo)
                        setMeter = dlgSet.action > 0 and vo == 0
                        hideTimeSigns(score, voice, timeSignPos, setMeter)                             
                    vo += 1
            st += 1
        sy += 1
                
    return



class ScoreChange(ScoreChange):

    def changeScore(self, score):
        changeDoc(score)
        

if activeScore():

    activeScore().registerUndo("Grafische Taktierung")
    tempInput = tempfile.mktemp('.capx')
    tempOutput = tempfile.mktemp('.capx')
    activeScore().write(tempInput)

    ScoreChange(tempInput, tempOutput)

    activeScore().read(tempOutput)
    os.remove(tempInput)
    os.remove(tempOutput)



