# -*- coding: ISO-8859-1 -*-
""" capellaScript -- ©Paul Villiger
>>> Artikulationszeichen setzen

    Dieses Skript wandelt alle Artikulationszeichen (AZ)in Textelemente um und
    positioniert diese unter- oder orberhalb der Noten. Bei einer Hauptstimme
    können die AZ automatisch am Kopfende, unterhalb oder
    oberhalb der Noten positioniert werden. Bei Unter- oder Oberstimmen werden
    die AZ unten oder nur oben gesetzt. Die Zeichen werden
    immer ausserhalb der Notenlinien gesetzt, mit Ausnahme von Staccato und
    Tenuto oder der Kombination von beiden. Es können auch mehrere Zeichen
    kombiniert werden, was Capella nicht erlaubt.

    AZ lassen sich auch in das capella-Format umwandeln. Bedingung: nur einfache AZ oder die
    Kombination von Staccato/Tenuto werden umgewandelt. Andere Kombination bleiben im Textformat.

    Bei markierten Noten gelten folgende Regeln:
    - nur Cursor -> Note hinter Cursor wird bearbeitet
    - Noten innerhalb einer Stimme markiert -> diese Noten werden bearbeitet
    - mehrere Stimmen/Notenzeilen/Systeme markiert -> die ganzen markierten Notenzeile werden bearbeitet

    Bei verbalkten Noten kann die Halsrichtung vom Skript nicht immer eindeutig bestimmt
    werden (fehlende Programmschnittstelle). Dabei kommt es vor, dass die AZ zwischen
    Notenkopf und Balken liegen. Zur Abhilfe kann die Halsrichtung dieser Note fest
    eingegeben werden mit i + <Pfeil auf> oder i + <Pfeil ab>. Danach ist das Skript nochmals aufzurufen.
        

<<<
History:  24.05.05  - Erste Version
          28.05.05  - Einschränkung auf Bereiche
                    - Umwandlung in das capella-Format
                    - Staccato genau über Hals
          29.05.05  - Undo Name korrigiert
                    - Fehler bei Bereichsmarkierung korrigiert
          14.03.11  - Staccatozeichen geändert von 180 auf 46

Rückmeldungen bitte an villpaul(at)bluewin.ch                     

"""


import xml.dom
import new, string
from xml.dom.minidom import NodeList, Node, Element

# default Einstellungen für Dialog
stemLimit = 4
minStemLen = 2


notenString='CDEFGAB'
defaultStemLen = 3.5  # von Capella vorgegeben
actLayout = ''
oberStimme = True
clefOffset = 41       # default

articulationChars = { chr(200):'tenuto',            # Tenuto    
                      chr(201):'staccatissimo',     # Staccatissimo
                      chr(202):'normalAccent',      # normaler Akzent
                      chr(203):'strongAccent',      # starker Akzent
                      chr(204):'weakBeat',          # weicher Schlag
                      chr(205):'strongBeat',        # harter Schlag
                      chr(206):'staccatissimo',     # unten
                      chr(207):'strongBeat',        # harter Schlag unten
                      chr(46) :'staccato',          # staccato (Punkt)
                      chr(180) :'staccato'          # staccato (Fingersymbol)
                    }

articulationsAbove = {#'staccato':       [chr(180),  0.45,  0.5, 0.75],           # char, dx, dy, height
                      'staccato':       [chr(46),   -1,  0.5, 0.75],           # char, dx, dy, height
                      'tenuto':         [chr(200), -0.25, 0.5, 0.5], 
                      'staccatissimo':  [chr(201),  0.25,  0.5, 1.0],     
                      'normalAccent':   [chr(202),  0.0,  0.25, 1.0],      
                      'strongAccent':   [chr(203),  0.25,  0.0, 1.0],      
                      'weakBeat':       [chr(204),  0.0,  0.5, 1.0],          
                      'strongBeat':     [chr(205),  0.5,  0.5, 1.0],        
                      
                    }                           
articulationsBelow = {#'staccato':       [chr(180),  0.45,  0.5, 0.75],
                      'staccato':       [chr(46),  -1,  0.5, 0.75],
                      'tenuto':         [chr(200), -0.25, 0.5, 0.5],
                      'staccatissimo':  [chr(206),  0.25,  0.5, 1.0],     
                      'normalAccent':   [chr(202),  0.0,  0.25, 1.0],      
                      'strongAccent':   [chr(203),  0.25,  0.0, 1.0],      
                      'weakBeat':       [chr(204),  0.0,  0.5, 1.0],          
                      'strongBeat':     [chr(207),  0.5,  0.5, 1.0]       
                                       
                    }

# Die Reihenfolge wie die Artikulationszeichen gesetzt werden
articulationsList = ['staccato','tenuto', 'staccatissimo','normalAccent','strongAccent','weakBeat','strongBeat']
                   

def latin1(u):
    return u.encode('Latin-1')
def latin1d(u):
    return u.decode('Latin-1')

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 getArticulation(chord):
    articulation = []
    for art in chord.getElementsByTagName('articulation'):
        type = art.getAttribute('type')
        articulation.extend(string.split(type))

    for text in chord.getElementsByTagName('text'):
        font = text.gotoChild('font')
        content = text.gotoChild('content')
        if 'capella' in font.getAttribute('face'):
            if content.firstChild:
                cont = latin1(content.firstChild.nodeValue)
                if len(cont) == 1 and cont in articulationChars:
                    articulation.append(articulationChars[cont])
    return articulation

def removeElement(element):
    # removes element and all parentNodes if they contain no more elementsNodes
    cn = element
    pn = cn.parentNode
    pn.removeChild(cn)
    delete = True
    for c in pn.childNodes:
        if c.nodeType == c.ELEMENT_NODE and c.tagName <> 'basic':
            delete = False
    while delete:
        cn = pn    
        pn = cn.parentNode
        pn.removeChild(cn)
        for c in pn.childNodes:
            if c.nodeType == c.ELEMENT_NODE and c.tagName <> 'basic':
                delete = False


def removeArticulation(chord):
    for art in chord.getElementsByTagName('articulation'):
        art.parentNode.removeChild(art)
    for text in chord.getElementsByTagName('text'):
        font = text.gotoChild('font')
        content = text.gotoChild('content')
        if 'capella' in font.getAttribute('face'):
            if content.firstChild:
                cont = latin1(content.firstChild.nodeValue)
                if len(cont) == 1 and cont in articulationChars:
                    removeElement(text)
                        
def setTextArticulation(chord, top, bottom, above, near, articulationList):
    _nearHead = near
    if above: # above chords
        if _nearHead and top >= -1:
            posY = round(top-0.25) - 1 
        else:
            posY = min(top, - 2.0) - 1
            _nearHead = False
        for a in articulationsList:
            if a in articulationList:
                drawObjects = chord.gotoChild('drawObjects')
                drawObj     = drawObjects.gotoChild('drawObj', True)
                text        = drawObj.gotoChild('text')
                if not near and a == 'staccato' and len(articulationList) == 1:
                    text.setAttribute('x',str(articulationsAbove[a][1] + 0.5))
                else:                    
                    text.setAttribute('x',str(articulationsAbove[a][1]))
                text.setAttribute('y',str(posY + articulationsAbove[a][2]))
                font = text.gotoChild('font')
                font.setAttribute('face','capella3')
                font.setAttribute('height','18')
                font.setAttribute('charSet','2')
                font.setAttribute('pitchAndFamily','2')
                content = text.gotoChild('content')
                textNode = doc.createTextNode(latin1d(articulationsAbove[a][0]))
                content.appendChild(textNode)
                if _nearHead:
                    posY = posY - 1
                else:
                    posY = posY - articulationsAbove[a][3]
                    
    else: # below chords
        if _nearHead and bottom <= 1:
            posY = round(bottom+0.25) + 1 
        else:
            posY = max(bottom, 2.0) + 1
            _nearHead = False
        for a in articulationsList:
            if a in articulationList:
                drawObjects = chord.gotoChild('drawObjects')
                drawObj     = drawObjects.gotoChild('drawObj', True)
                text        = drawObj.gotoChild('text')
                if not near and a == 'staccato' and len(articulationList) == 1:
                    text.setAttribute('x',str(articulationsBelow[a][1] - 0.5))
                else:
                    text.setAttribute('x',str(articulationsBelow[a][1]))
                text.setAttribute('y',str(posY - articulationsBelow[a][2]))
                font = text.gotoChild('font')
                font.setAttribute('face','capella3')
                font.setAttribute('height','18')
                font.setAttribute('charSet','2')
                font.setAttribute('pitchAndFamily','2')
                content = text.gotoChild('content')
                textNode = doc.createTextNode(latin1d(articulationsBelow[a][0]))
                content.appendChild(textNode)
                if _nearHead:
                    posY = posY + 1
                else:
                    posY = posY + articulationsBelow[a][3]

def setCapellaArticulation(chord, articulationList):
    articulation = chord.gotoChild('articulation')
    if 'staccato' in articulationList and 'tenuto' in articulationList:
        articulation.setAttribute('type', 'staccato tenuto')
    else:
        articulation.setAttribute('type', articulationList[0])



def getCursorRange():
    sel = curSelection()
    (sy1,st1,vo1,ob1),(sy2,st2,vo2,ob2) = sel
    sel = (min(sy1,sy1), min(st1,st2),min(vo1,vo2),min(ob1,ob2)),(max(sy1,sy1), max(st1,st2),max(vo1,vo2),max(ob1,ob2))
    (sy1,st1,vo1,ob1),(sy2,st2,vo2,ob2) = sel
    
    if ob2 > ob1: # sonst wird Zeichen hinter rechtem Cursor auch bearbeitet
        ob2 -= 1
        sel = (sy1,st1,vo1,ob1),(sy2,st2,vo2,ob2)
    if sel == 0:
        messageBox('Fehler', 'keine aktive Partitur')
        return (0,0,0,0),(0,0,0,0)
    if sel[0][0:3] <> sel[1][0:3]: # mehrere Stimmen markiert -> alle Noten
        ob1 = 0
        ob2 = 999
    if sel[0][0:2] <> sel[1][0:2]: # mehrere Notenzeilen markiert -> alle Stimmen
        vo1 = 0
        vo2 = 999
    if sel[0][0] <> sel[1][0]: # mehrere Systeme markiert -> alle Notenzeilen
        st1 = 0
        st2 = 999
    sel = (sy1,st1,vo1,ob1),(sy2,st2,vo2,ob2)
    return sel

def getMaxPitch(chord):
    maxV = 0
    for head in chord.getElementsByTagName('head'):
        pitch = head.getAttribute('pitch')
        pitchN = notenString.find(pitch[0]) + 7 * long(pitch[1])
        if pitchN > maxV:
            maxV=pitchN
    maxV = - (maxV - clefOffset) / 2.0  # B5 auf Mittellinie
    return maxV
        
def getMinPitch(chord):
    minV = 999
    for head in chord.getElementsByTagName('head'):
        pitch = head.getAttribute('pitch')
        pitchN = notenString.find(pitch[0]) + 7 * long(pitch[1])
        if pitchN < minV:
            minV=pitchN
    minV = - ((minV - clefOffset) / 2.0)  # B5 auf Mittellinie
    return minV

def calculateClefOffset(c):
    global clefOffset
    # Berechnet den Offset der Note, welche bei diesem Schluessel auf der Mittellinie steht
    # Schlüssel umwandeln
    if c == 'treble':
        newC = 'G2'
    elif c == 'bass':
        newC = 'F4'
    elif c == 'alto':
        newC = 'C3'
    elif c == 'tenor':
        newC = 'C4'
    else:
        newC = c
    
    # Schlüssel analysieren
    clef = newC[0]
    line = int(newC[1])
    if newC.find('-') > 0:
        okt = -7
    elif newC.find('+') > 0:
        okt = 7
    else:
        okt = 0
    # Offset berechnen
    offset = - 2 * line + okt
    if clef == 'G':  # G-Schlüssel
        offset += 45
    elif clef == 'F':
        offset += 37 # F-Schlüssel
    elif clef == 'C':
        offset += 41 # C-Schlüssel
    elif clef in  ['N','P']:
        offset += 47 # Schlagzeug & Kein Schlüssel
    else:
        offset = clefOffset
    clefOffset = offset

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 getDialogValues():

    global selectedPos, selectRange
    selectedPos = Radio([' automatisch am Kopfende', ' oberhalb der Notenzeile', 'unterhalb der Notenzeile', 'capella-Format'], value = 0, padding = 8)
    selectRange = CheckBox('Ganze Partitur?' , value = 0)
    
    dlg = Dialog('Artikulationszeichen',
                 VBox([Label('Artikulationszeichen setzen:'),
                      Label(' '),
                      selectedPos,
                      selectRange,
                      Label(' '),
                      Label('Bitte Programmhinweise beachten'),
                      Label(' ')]))

    if dlg.run():
        selectedPos = ['auto','top','bottom','capella'][selectedPos.value()]
        selectRange = selectRange.value()
        return True
    else:
        return False


def setTextArticulationVoice(voice, first, last):
    voiceUp = False
    voiceDown = False
    if voice.hasAttributes:
        stemDir = voice.getAttribute('stemDir')
        voiceUp =   stemDir == 'up'
        voiceDown =   stemDir == 'down'
    objList = getElementObjects(voice.getElementsByTagName('noteObjects')[0].childNodes)
    objNumber = 0
    for obj in objList:
        stemUp = False        
        stemDown = False        
        if obj.tagName == 'clefSign':
            clef = obj.getAttribute('clef')
            calculateClefOffset(clef)
        elif obj.tagName == 'chord':
            articulation = getArticulation(obj)
            if len(articulation) == 0:
                objNumber +=1
                continue

            if first > objNumber or last < objNumber:
                objNumber +=1
                continue

            stemLen = defaultStemLen
            for stem in obj.getElementsByTagName('stem'):
                if stem.hasAttribute('dir'):
                    stemUp = stem.getAttribute('dir') == 'up'
                    stemDown = stem.getAttribute('dir') == 'down'
                if stem.hasAttribute('lengthening'):
                    stemLen = defaultStemLen + float(stem.getAttribute('lengthening'))

            chord = obj
            maxV=getMaxPitch(chord)
            minV=getMinPitch(chord)
            
            # Halsrichtung bestimmen
            if stemUp:
                stemDirUp = True
            elif stemDown:
                stemDirUp = False
            elif voiceUp:
                stemDirUp = True
            elif voiceDown:
                stemDirUp = False
            elif (maxV + minV) > 0:
                stemDirUp = True
            else:
                stemDirUp = False


            # Unter- und Obergrenze der Noten mit Hals berechnen
            if stemDirUp:      # Hals nach oben
                top = maxV - stemLen
                bottom = minV + 0.5
            else: 
                top = maxV - 0.5
                bottom = minV + stemLen

            # calculate placement of articulation signs
            if voiceDown:
                above = False
            elif voiceUp:
                above = True
            elif selectedPos == 'top':
                above = True
            elif selectedPos == 'bottom':
                above = False
            else: # auto placement, always on the head side of the chord
                above = not stemDirUp

            # calculate if sign is near the note head
            nearHead = False
            if above == stemDirUp:  # 
                pass
            elif len(articulation) == 1:
                if 'tenuto' in articulation or 'staccato' in articulation:
                    nearHead = True
            elif len(articulation) == 2:
                if 'tenuto' in articulation and 'staccato' in articulation:
                    nearHead = True

            removeArticulation(chord)

            if selectedPos == 'capella' and len(articulation) == 1:
                setCapellaArticulation(chord, articulation)
            elif selectedPos == 'capella' and nearHead: # len == 2 and staccato and tenuto
                setCapellaArticulation(chord, articulation)
            else:
                setTextArticulation(chord, top, bottom, above, nearHead, articulation)

        objNumber +=1


    
def changeDoc(score):
    global actLayout, doc
    doc = score.parentNode    
    (sy1, st1, vo1, ob1),(sy2, st2, vo2, ob2) = getCursorRange()

    if getDialogValues():
        if selectRange:
            (sy1, st1, vo1, ob1),(sy2, st2, vo2, ob2) = (0,0,0,0),(999,999,999,999)  # ganze Partitur
        sy = 0
        for system in score.getElementsByTagName('system'):
            if sy1 <= sy <= sy2:
                st = 0
                for staff in system.getElementsByTagName('staff'):
                    if st1 <= st <= st2:
                        vo = 0
                        for voice in staff.getElementsByTagName('voice'):
                            if vo1 <= vo <= vo2:
                                setTextArticulationVoice(voice, ob1, ob2)
                            vo += 1
                    st += 1
            sy += 1

# Hauptprogramm:

from caplib.capDOM import ScoreChange
import tempfile

class Halsabschneider(ScoreChange):
    def changeScore(self, score):
        changeDoc(score)

if activeScore():
    activeScore().registerUndo("Artikulationszeichen")
    tempInput = tempfile.mktemp('.capx')
    tempOutput = tempfile.mktemp('.capx')
    activeScore().write(tempInput)
    
    Halsabschneider(tempInput, tempOutput)

    activeScore().read(tempOutput)
    os.remove(tempInput)
    os.remove(tempOutput)
