<commit_before># coding=utf-8

import os, time
from AppKit import NSToolbarFlexibleSpaceItemIdentifier, NSURL, NSImageCell, NSImageAlignTop, NSScaleNone, NSImageFrameNone, NSImage, NSObject
import designspaceDocument
from designspaceDocument import *
from defcon import Font
from defconAppKit.windows.progressWindow import ProgressWindow
from vanilla import *
from vanilla.dialogs import getFile, putFile, askYesNo

from mojo.UI import AccordionView
from mojo.roboFont import *

from vanilla import *

#import descriptors



hasPathSymbol = u"✓"

# NSOBject Hack, please remove before release.
def ClassNameIncrementer(clsName, bases, dct):
   import objc
   orgName = clsName
   counter = 0
   while 1:
       try:
           objc.lookUpClass(clsName)
       except objc.nosuchclass_error:
           break
       counter += 1
       clsName = orgName + str(counter)
   return type(clsName, bases, dct)
# /NSOBject Hack, please remove before release.




class KeyedGlyphDescriptor(NSObject):
    __metaclass__ = ClassNameIncrementer
    def __new__(cls):
        self = cls.alloc().init()
        self.glyphName = None
        self.patterns = {}
        return self
    
    def glyphNameKey(self):
        return self.glyphName
    
    def workingKey(self):
        return len(self.patterns)==1
        
class KeyedSourceDescriptor(NSObject):
    __metaclass__ = ClassNameIncrementer
    def __new__(cls):
        self = cls.alloc().init()
        self.dir = None
        self.path = None
        self.name = None
        self.location = None
        self.copyLib = False
        self.copyInfo = False
        self.copyGroups = False
        self.copyFeatures = False
        self.muteKerning = False
        self.muteInfo = False
        self.mutedGlyphNames = []
        self.familyName = None
        self.styleName = None
        self.axisOrder = []
        return self
    
    def setName(self):
        # make a name attribute based on the location
        name = ['source:']
        for k, v in self.location.items():
            name.append("%s_%3.3f"%(k, v))
        self.name = "_".join(name)
        
    def copyThisStuff(self, state=True):
        if state:
            self.copyLib = True
            self.copyInfo = True
            self.copyGroups = True
            self.copyFeatures = True
        else:
            self.copyLib = False
            self.copyInfo = False
            self.copyGroups = False
            self.copyFeatures = False
        
    def setAxisOrder(self, names):
        self.axisOrder = names
        
    def sourceUFONameKey(self):
        return os.path.basename(self.path)
        
    def sourceHasFileKey(self):
        if os.path.exists(self.path):
            return hasPathSymbol
        return ""
    
    def sourceFamilyNameKey(self):
        return self.familyName
    
    def sourceStyleNameKey(self):
        return self.styleName
        
    def getAxisValue(self, axisIndex):
        if 0 <= axisIndex < len(self.axisOrder):
            wantAxisName = self.axisOrder[axisIndex]
            if wantAxisName in self.location:
                v = self.location[wantAxisName]
                if type(v) == tuple:
                    return v[0]
                return v
        return ""

    def sourceAxis_1(self):
        return self.getAxisValue(0)
    def sourceAxis_2(self):
        return self.getAxisValue(1)
    def sourceAxis_3(self):
        return self.getAxisValue(2)
    def sourceAxis_4(self):
        return self.getAxisValue(3)
    def sourceAxis_5(self):
        return self.getAxisValue(4)

    def setValue_forUndefinedKey_(self, value=None, key=None):
        if key == "sourceFamilyNameKey":
            self.familyName = value
        elif key == "sourceStyleNameKey":
            self.stylename = value
        elif key == "sourceAxis_1":
            axisName = self.axisOrder[0]
            try:
                self.location[axisName] = float(value)
            except ValueError:
                NSBeep()
        elif key == "sourceAxis_2":
            axisName = self.axisOrder[1]
            try:
                self.location[axisName] = float(value)
            except ValueError:
                NSBeep()
        elif key == "sourceAxis_3":
            axisName = self.axisOrder[2]
            try:
                self.location[axisName] = float(value)
            except ValueError:
                NSBeep()
        elif key == "sourceAxis_4":
            axisName = self.axisOrder[3]
            try:
                self.location[axisName] = float(value)
            except ValueError:
                NSBeep()
        elif key == "sourceAxis_5":
            axisName = self.axisOrder[4]
            try:
                self.location[axisName] = float(value)
            except ValueError:
                NSBeep()
    
class KeyedInstanceDescriptor(NSObject):
    __metaclass__ = ClassNameIncrementer
    def __new__(cls):
        self = cls.alloc().init()
        self.dir = None
        self.path = None
        self.name = None
        self.location = None
        self.familyName = None
        self.styleName = None
        self.postScriptFontName = None
        self.styleMapFamilyName = None
        self.styleMapStyleName = None
        self.glyphs = {}
        self.axisOrder = []
        self.kerning = True
        self.info = True
        return self

    def setName(self):
        # make a name attribute based on the location
        name = ['instance:']
        for k, v in self.location.items():
            name.append("%s_%3.3f"%(k, v))
        self.name = "_".join(name)
        
    def setAxisOrder(self, names):
        self.axisOrder = names

    def instanceHasFileKey(self):
        if self.path is None:
            return ""
        if os.path.exists(self.path):
            return hasPathSymbol
        return u""
    
    def getAxisValue(self, axisIndex):
        if 0 <= axisIndex < len(self.axisOrder):
            wantAxisName = self.axisOrder[axisIndex]
            if wantAxisName in self.location:
                v = self.location[wantAxisName]
                if type(v) == tuple:
                    return v[0]
                return v
        return ""
        
    def instanceAxis_1(self):
        return self.getAxisValue(0)
    def instanceAxis_2(self):
        return self.getAxisValue(1)
    def instanceAxis_3(self):
        return self.getAxisValue(2)
    def instanceAxis_4(self):
        return self.getAxisValue(3)
    def instanceAxis_5(self):
        return self.getAxisValue(4)

    def instanceUFONameKey(self):
        if self.path is None:
            self.makeUFOPathFromNames()
        return os.path.basename(self.path)
    
    def setPathRelativeTo(self, docFolder, instancesFolderName):
        self.dir = os.path.join(docFolder, instancesFolderName)
        self.makeUFOPathFromNames()

    def makeUFOPathFromNames(self):
        if self.familyName is not None and self.styleName is not None:
            fileName = "%s-%s.ufo"%(self.familyName, self.styleName)
            self.postScriptFontName = "%s-%s"%(self.familyName, self.styleName)
        else:
            fileName = "NewFamily-NewStyle.ufo"
            self.postScriptFontName = "NewFamily-NewStyle"
        if self.dir is None:
            saveDir = ""
        else:
            saveDir = self.dir
        #self.path = "saveDir: %s fileName %s"%(saveDir, fileName)
        self.path = os.path.join(saveDir, fileName)

    def setValue_forUndefinedKey_(self, value=None, key=None):
        if key == "instanceFamilyNameKey":
            self.familyName = value
            self.makeUFOPathFromNames()
        elif key == "instanceStyleNameKey":
            self.styleName = value
            self.makeUFOPathFromNames()
        elif key == "instanceAxis_1":
            axisName = self.axisOrder[0]
            try:
                self.location[axisName] = float(value)
            except ValueError:
                NSBeep()
        elif key == "instanceAxis_2":
            axisName = self.axisOrder[1]
            try:
                self.location[axisName] = float(value)
            except ValueError:
                NSBeep()
        elif key == "instanceAxis_3":
            axisName = self.axisOrder[2]
            try:
                self.location[axisName] = float(value)
            except ValueError:
                NSBeep()
        elif key == "instanceAxis_4":
            axisName = self.axisOrder[3]
            try:
                self.location[axisName] = float(value)
            except ValueError:
                NSBeep()
        elif key == "instanceAxis_5":
            axisName = self.axisOrder[4]
            try:
                self.location[axisName] = float(value)
            except ValueError:
                NSBeep()
            
    def instanceFamilyNameKey(self):
        return self.familyName
    def instanceStyleNameKey(self):
        return self.styleName
    
class KeyedAxisDescriptor(NSObject):
    __metaclass__ = ClassNameIncrementer
    # https://www.microsoft.com/typography/otspec/fvar.htm
    registeredTags = [
        ("italic", "ital"),
        ("optical", "opsz"),
        ("slant", "slnt"),
        ("width", "wdth"),
        ("weight", "wght"),
        ]
    def __new__(cls):
        self = cls.alloc().init()
        self.tag = None     # opentype tag for this axis
        self.name = None    # name of the axis used in locations
        self.labelNames = {} # names for UI purposes, if this is not a standard axis,
        self.minimum = None
        self.maximum = None
        self.default = None
        self.map = []
        return self
    
    def registeredTagKey(self):
        for name, tag in self.registeredTags:
            if name == self.name and tag == self.tag:
                return u"®"
        return ""
        
    def setValue_forUndefinedKey_(self, value=None, key=None):
        if key == "axisNameKey":
            for name, tag in self.registeredTags:
                if name == value:
                    self.tag = tag
            self.name = value
        elif key == "axisTagKey":
            if len(value)!=4:
                return
            self.tag = value
        elif key == "axisMinimumKey":
            try:
                num = float(value)
                self.minimum = num
            except ValueError:
                NSBeep()
        elif key == "axisMaximumKey":
            try:
                num = float(value)
                self.maximum = num
            except ValueError:
                NSBeep()
        elif key == "axisDefaultKey":
            try:
                num = float(value)
                self.default = num
            except ValueError:
                NSBeep()

    def axisNameKey(self):
        return self.name
    def axisTagKey(self):
        return self.tag
    def axisMinimumKey(self):
        return self.minimum
    def axisDefaultKey(self):
        return self.default
    def axisMaximumKey(self):
        return self.maximum


class KeyedDocReader(BaseDocReader):
    axisDescriptorClass = KeyedAxisDescriptor
    sourceDescriptorClass = KeyedSourceDescriptor
    instanceDescriptorClass = KeyedInstanceDescriptor
    
class KeyedDocWriter(BaseDocWriter):
    axisDescriptorClass = KeyedAxisDescriptor
    sourceDescriptorClass = KeyedSourceDescriptor
    instanceDescriptorClass = KeyedInstanceDescriptor

def newImageListCell():
    cell = NSImageCell.alloc().init()
    cell.setImageAlignment_(NSImageAlignTop)
    cell.setImageFrameStyle_(NSImageFrameNone)
    return cell


class DesignSpaceEditor:
    instanceFolderName = "instances"
    def __init__(self, designSpacePath=None):
        self.designSpacePath = designSpacePath
        #imageCell = newImageListCell()
        self.doc = None
        self._newInstanceCounter = 1
        if designSpacePath is None:
            fileNameTitle = "Untitled.designspace"
        else:
            fileNameTitle = os.path.basename(designSpacePath)
        self.w = Window((940, 700), fileNameTitle, minSize=(200,400))
        self._updatingTheAxesNames = False
        toolbarItems = [
            {
                'itemIdentifier': "toolbarSave",
                'label': 'Save',
                'callback': self.save,
                #'imageObject': NSImage.alloc().initWithContentsOfFile_('toolbar_axes_m.pdf')
                'imageObject': NSImage.imageNamed_('toolbar_axes_m')
                
            },
        ]
        self.w.addToolbar("DesignSpaceToolbar", toolbarItems)
        
        fileIconWidth = 30
        ufoNameWidth = 220
        axisValueWidth = 80
        familyNameWidth = 130
        masterColDescriptions = [
                {   'title': '',
                    'key':'sourceHasFileKey',
                    'width':fileIconWidth,
                    'editable':False,
                },
                {   'title': 'UFO',
                    'key':'sourceUFONameKey',
                    'width':ufoNameWidth,
                    'editable':False,
                },
                {   'title': 'Family Name',
                    'key': 'sourceFamilyNameKey',
                    'width':familyNameWidth,
                    'editable':True,
                },
                {   'title': 'Style Name',
                    'key':'sourceStyleNameKey',
                    'width':familyNameWidth,
                    'editable':True,
                },
                {   'title': 'Axis 1',
                    'key':'sourceAxis_1',
                    'width':axisValueWidth,
                    'editable':True,
                },
                {   'title': 'Axis 2',
                    'key':'sourceAxis_2',
                    'width':axisValueWidth,
                    'editable':True,
                },
                {   'title': 'Axis 3',
                    'key':'sourceAxis_3',
                    'width':axisValueWidth,
                    'editable':True,
                },
                {   'title': 'Axis 4',
                    'key':'sourceAxis_4',
                    'width':axisValueWidth,
                    'editable':True,
                },
                {   'title': 'Axis 5',
                    'key':'sourceAxis_5',
                    'width':axisValueWidth,
                    'editable':True,
                },
        ]
        instanceColDescriptions = [
                {   'title': '',
                    'key':'instanceHasFileKey',
                    'width':fileIconWidth,
                    'editable':False,
                },
                {   'title': 'UFO',
                    'key': 'instanceUFONameKey',
                    'width':ufoNameWidth,
                    'editable':False,
                },
                {   'title': 'Family Name',
                    'key': 'instanceFamilyNameKey',
                    'width':familyNameWidth,
                    'editable':True,
                },
                {   'title': 'Style Name',
                    'key':'instanceStyleNameKey',
                    'width':familyNameWidth,
                    'editable':True,
                },

                {   'title': 'Axis 1',
                    'key':'instanceAxis_1',
                    'width':axisValueWidth,
                    'editable':True,
                },
                {   'title': 'Axis 2',
                    'key':'instanceAxis_2',
                    'width':axisValueWidth,
                    'editable':True,
                },
                {   'title': 'Axis 3',
                    'key':'instanceAxis_3',
                    'width':axisValueWidth,
                    'editable':True,
                },
                {   'title': 'Axis 4',
                    'key':'instanceAxis_4',
                    'width':axisValueWidth,
                    'editable':True,
                },
                {   'title': 'Axis 5',
                    'key':'instanceAxis_5',
                    'width':axisValueWidth,
                    'editable':True,
                },
            ]
        axisColDescriptions = [
                {   'title': '',
                    'key':'registeredTagKey',
                    'width':40,
                    'editable':False,
                },
                {   'title': 'Name',
                    'key':'axisNameKey',
                    'width':100,
                    'editable':True,
                },
                {   'title': 'Tag',
                    'key':'axisTagKey',
                    'width':100,
                    'editable':True,
                },
                {   'title': 'Minimum',
                    'key':'axisMinimumKey',
                    'width':100,
                    'editable':True,
                },
                {   'title': 'Default',
                    'key':'axisDefaultKey',
                    'width':100,
                    'editable':True,
                },
                {   'title': 'Maximum',
                    'key':'axisMaximumKey',
                    'width':100,
                    'editable':True,
                },
            ]
        glyphsColDescriptions = [
                {   'title': '',
                    'key':'workingKey',
                    'width':40,
                    'editable':False,
                },
                {   'title': 'Glyphname',
                    'key':'glyphNameKey',
                    'width':40,
                    'editable':False,
                },
        ]
        toolbarHeight = 24
        self.axesGroup = Group((0,0,0,0))
        self.axesItem = List((0, toolbarHeight, -0, -0), [], columnDescriptions=axisColDescriptions, editCallback=self.callbackAxesListEdit)
        self.axesGroup.l = self.axesItem
        buttonMargin = 2
        buttonHeight = 20
        openButtonSize = (48,buttonMargin+1,50,buttonHeight)
        statusTextSize = (105, buttonMargin+4,-10,buttonHeight)
        axisToolDescriptions = [
            {'title': "+", 'width': 20,},
            {'title': "-", 'width': 20}
            ]
        self.axesGroup.tools = SegmentedButton(
            (buttonMargin,buttonMargin,100,buttonHeight),
            segmentDescriptions=axisToolDescriptions,
            selectionStyle="momentary",
            callback=self.callbackAxisTools)
        
        self.mastersGroup = Group((0,0,0,0))
        masterToolDescriptions = [
            {'title': "+", 'width': 20,},
            {'title': "-", 'width': 20},
            ]
        self.mastersItem = List((0, toolbarHeight, -0, -0), [],
            columnDescriptions=masterColDescriptions,
            #doubleClickCallback=self.callbackMastersDblClick
            selectionCallback=self.callbackMasterSelection
            )
        self.mastersGroup.l = self.mastersItem
        self.mastersGroup.tools = SegmentedButton(
            (buttonMargin, buttonMargin,100,buttonHeight),
            segmentDescriptions=masterToolDescriptions,
            selectionStyle="momentary",
            callback=self.callbackMasterTools)
        self.mastersGroup.status = TextBox(statusTextSize, "About the masters", sizeStyle="small")
        self.mastersGroup.openButton = Button(
            openButtonSize, "Open",
            callback=self.callbackOpenMaster,
            sizeStyle="small")
        self.mastersGroup.openButton.enable(False)
        
        self.instancesGroup = Group((0,0,0,0))
        self.instancesItem = List((0, toolbarHeight, -0, -0), [],
            columnDescriptions=instanceColDescriptions,
            selectionCallback=self.callbackInstanceSelection,
        )
        instancesToolDescriptions = [
            {'title': "+", 'width': 20,},
            {'title': "-", 'width': 20},
            ]

        self.instancesGroup.tools = SegmentedButton(
            (buttonMargin, buttonMargin,100,buttonHeight),
            segmentDescriptions=instancesToolDescriptions,
            selectionStyle="momentary",
            callback=self.callbackInstanceTools)
        self.instancesGroup.l = self.instancesItem
        self.instancesGroup.openButton = Button(
            openButtonSize, "Open",
            callback=self.callbackOpenInstance,
            sizeStyle="small")
        self.instancesGroup.openButton.enable(False)
        self.instancesGroup.status = TextBox(statusTextSize, "", sizeStyle="small")
        
        self.glyphsGroup = Group((0,0,0,0))
        self.glyphsItem = List((0, toolbarHeight, -0, -0), [],
            #columnDescriptions=glyphsColDescriptions,
            #selectionCallback=self.callbackInstanceSelection,
        )
        self.glyphsGroup.l = self.glyphsItem
    
        self.reportGroup = Group((0,0,0,0))
        self.reportGroup.text = TextBox((5,5,-5,-5), 'hehe')
        
        descriptions = [
           dict(label="Axes", view=self.axesGroup, size=138, collapsed=False, canResize=False),
           dict(label="Masters", view=self.mastersGroup, size=135, collapsed=False, canResize=True),
           dict(label="Instances", view=self.instancesGroup, size=135, collapsed=False, canResize=True),
           dict(label="Report", view=self.reportGroup, size=170, collapsed=False, canResize=True),
           # this panel will show glyphs and compatibiility.
           #dict(label="Glyphs", view=self.glyphsGroup, size=250, collapsed=False, canResize=True),
        ]
        self.read(designSpacePath)
        self.w.accordionView = AccordionView((0, 0, -0, -0), descriptions)
        self.updateAxesColumns()
        self.enableInstanceList()
        self.reportMasterPathStatus()
        self.w.open()
        if designSpacePath is not None:
            self.w.getNSWindow().setRepresentedURL_(NSURL.fileURLWithPath_(designSpacePath))
    
    def validate(self):
        # validate all data and write a report here.
        report = []
        
        # document and path
        if self.designSpacePath is None:
            report.append("This document has not been saved yet.")
            if len(self.doc.sources)>0:
                masterFolder = self.getSaveDirFromMasters()
                report.append("Make sure to save this document in %s."%masterFolder)
        else:
            report.append("Document source folder:")
            report.append("\t%s"%os.path.dirname(self.designSpacePath))

        # axes
        if len(self.doc.axes)==0:
            report.append("Define one or more axes")
        else:
            report.append("Axes:")
            for axis in self.doc.axes:
                if axis.registeredTagKey():
                    report.append("\t\"%s\" registered OpenType axis name"%axis.name)
                else:
                    report.append("\t\"%s\" is a non-standard axis name"%axis.name)
                
        # masters
        if len(self.doc.sources)==0:
            report.append("Add two or more UFOs.")
        else:
            report.append("Masters:")
            for master in self.doc.sources:
                report.append("\t%s"%master.path)
                if self.designSpacePath is not None:
                    if os.path.dirname(master.path)!=os.path.dirname(self.designSpacePath):
                        report.append("\tPlease move to the same folder as this document.")
        
        # instances
        if len(self.doc.instances)==0:
            report.append("Define an instance.")
        else:
            report.append("Instances:")
            for instance in self.doc.instances:
                report.append("\t%s"%instance.path)
        
        self.reportGroup.text.set("\n".join(report))
            
    def setMasterStatus(self, text):
        self.mastersGroup.status.set(text)
    
    def setInstancesStatus(self, text):
        self.instancesGroup.status.set(text)
        
    def openSelectedItem(self, sender):
        selection = sender.getSelection()
        if selection:
            progress = ProgressWindow("Opening UFO...", len(selection), parentWindow=self.w)
            for i in sender.getSelection():
                key = sender[i]
                if key.path is None:
                    continue
                path = key.path
                try:
                    alreadyOpen = False
                    for f in AllFonts():
                        if f.path == path:
                            thisWindow = f.document().getMainWindow()
                            thisWindow.show()
                            alreadyOpen = True
                            break
                    if not alreadyOpen:
                        font = RFont(path, showUI=False)
                        font.showUI()
                except:
                    print "Bad UFO:", path
                    pass
                progress.update()
            progress.close()            
        
    def callbackInstancesDblClick(self, sender):
        print "callbackInstancesDblClick"
    
    def updateAxesColumns(self):
        # present the axis names above all the columns.
        names = []
        for axis in self.doc.axes:
            names.append(axis.name)
        for descriptor in self.doc.instances:
            descriptor.setAxisOrder(names)
        for descriptor in self.doc.sources:
            descriptor.setAxisOrder(names)
        # clear the old names
        
        columns = self.mastersItem.getNSTableView().tableColumns()
        sourceColumnTitleOffset = 4
        for col in range(sourceColumnTitleOffset, len(columns)):
            column = columns[col]
            try:
                newTitle = names[col-sourceColumnTitleOffset]
            except IndexError:
                newTitle = u""
            column.setTitle_(newTitle)
        self.mastersItem.getNSTableView().reloadData()

        columns = self.instancesItem.getNSTableView().tableColumns()
        instancesColumnTitleOffset = 4
        for col in range(instancesColumnTitleOffset, len(columns)):
            column = columns[col]
            try:
                newTitle = names[col-instancesColumnTitleOffset]
            except IndexError:
                newTitle = u""
            column.setTitle_(newTitle)
        self.instancesItem.getNSTableView().reloadData()
        
    def read(self, designSpacePath):
        self.doc = DesignSpaceDocument(KeyedDocReader, KeyedDocWriter)
        if designSpacePath is not None:
            self.doc.read(designSpacePath)
        self.axesItem.set(self.doc.axes)
        self.mastersItem.set(self.doc.sources)
        self.instancesItem.set(self.doc.instances)
        self.validate()

    def getSaveDirFromMasters(self):
        options = {}
        for master in self.mastersItem:
            if master.path is not None:
                thisFileDir = os.path.dirname(master.path)
                options[thisFileDir] = True
        if len(options)==1:
            # neat and tidy, all masters in a folder
            return options.keys()[0]
        if options:
            paths = options.keys()
            paths.sort()
            return paths[0]
        return None
            
    def save(self, sender):
        if self.designSpacePath is None:
            # get a filepath first
            saveToDir = self.getSaveDirFromMasters()    # near the masters
            putFile(messageText="Save this designspace document",
                directory=saveToDir,
                canCreateDirectories=True,
                fileTypes=['designspace'],
                parentWindow=self.w,
                resultCallback=self.finalizeSave)
        else:
            self.finalizeSave(self.designSpacePath)

    def finalizeSave(self, path=None):
        for item in self.doc.sources:
            item.copyThisStuff(False)
        if self.doc.sources:
            self.doc.sources[0].copyThisStuff(True)
        self.designSpacePath = path
        # so we have the path for this document
        # we need to make sure the instances are all in the right place
        docFolder = os.path.dirname(path)
        for item in self.instancesItem:
            item.setPathRelativeTo(docFolder, self.instanceFolderName)
            item.setName()
        # dump all the paths
        #self.dumpFilePaths()<commit_msg>- Make a better unique name for the source and instance elements. - disable the NSObject hack.<commit_after># coding=utf-8

import os, time
from AppKit import NSToolbarFlexibleSpaceItemIdentifier, NSURL, NSImageCell, NSImageAlignTop, NSScaleNone, NSImageFrameNone, NSImage, NSObject
import designspaceDocument
from designspaceDocument import *
from defcon import Font
from defconAppKit.windows.progressWindow import ProgressWindow
from vanilla import *
from vanilla.dialogs import getFile, putFile, askYesNo

from mojo.UI import AccordionView
from mojo.roboFont import *

from vanilla import *

#import descriptors



hasPathSymbol = u"✓"

# NSOBject Hack, please remove before release.
def ClassNameIncrementer(clsName, bases, dct):
   import objc
   orgName = clsName
   counter = 0
   while 1:
       try:
           objc.lookUpClass(clsName)
       except objc.nosuchclass_error:
           break
       counter += 1
       clsName = orgName + str(counter)
   return type(clsName, bases, dct)
# /NSOBject Hack, please remove before release.




class KeyedGlyphDescriptor(NSObject):
    #__metaclass__ = ClassNameIncrementer
    def __new__(cls):
        self = cls.alloc().init()
        self.glyphName = None
        self.patterns = {}
        return self
    
    def glyphNameKey(self):
        return self.glyphName
    
    def workingKey(self):
        return len(self.patterns)==1
        
class KeyedSourceDescriptor(NSObject):
    #__metaclass__ = ClassNameIncrementer
    def __new__(cls):
        self = cls.alloc().init()
        self.dir = None
        self.path = None
        self.name = None
        self.location = None
        self.copyLib = False
        self.copyInfo = False
        self.copyGroups = False
        self.copyFeatures = False
        self.muteKerning = False
        self.muteInfo = False
        self.mutedGlyphNames = []
        self.familyName = None
        self.styleName = None
        self.axisOrder = []
        return self
    
    def setName(self):
        # make a name attribute based on the location
        # this will overwrite things that the source file might already contain.
        name = ['source', self.familyName, self.styleName]
        for k, v in self.location.items():
            name.append("%s_%3.3f"%(k, v))
        self.name = "_".join(name)
        
    def copyThisStuff(self, state=True):
        if state:
            self.copyLib = True
            self.copyInfo = True
            self.copyGroups = True
            self.copyFeatures = True
        else:
            self.copyLib = False
            self.copyInfo = False
            self.copyGroups = False
            self.copyFeatures = False
        
    def setAxisOrder(self, names):
        self.axisOrder = names
        
    def sourceUFONameKey(self):
        return os.path.basename(self.path)
        
    def sourceHasFileKey(self):
        if os.path.exists(self.path):
            return hasPathSymbol
        return ""
    
    def sourceFamilyNameKey(self):
        return self.familyName
    
    def sourceStyleNameKey(self):
        return self.styleName
        
    def getAxisValue(self, axisIndex):
        if 0 <= axisIndex < len(self.axisOrder):
            wantAxisName = self.axisOrder[axisIndex]
            if wantAxisName in self.location:
                v = self.location[wantAxisName]
                if type(v) == tuple:
                    return v[0]
                return v
        return ""

    def sourceAxis_1(self):
        return self.getAxisValue(0)
    def sourceAxis_2(self):
        return self.getAxisValue(1)
    def sourceAxis_3(self):
        return self.getAxisValue(2)
    def sourceAxis_4(self):
        return self.getAxisValue(3)
    def sourceAxis_5(self):
        return self.getAxisValue(4)

    def setValue_forUndefinedKey_(self, value=None, key=None):
        if key == "sourceFamilyNameKey":
            self.familyName = value
        elif key == "sourceStyleNameKey":
            self.stylename = value
        elif key == "sourceAxis_1":
            axisName = self.axisOrder[0]
            try:
                self.location[axisName] = float(value)
            except ValueError:
                NSBeep()
        elif key == "sourceAxis_2":
            axisName = self.axisOrder[1]
            try:
                self.location[axisName] = float(value)
            except ValueError:
                NSBeep()
        elif key == "sourceAxis_3":
            axisName = self.axisOrder[2]
            try:
                self.location[axisName] = float(value)
            except ValueError:
                NSBeep()
        elif key == "sourceAxis_4":
            axisName = self.axisOrder[3]
            try:
                self.location[axisName] = float(value)
            except ValueError:
                NSBeep()
        elif key == "sourceAxis_5":
            axisName = self.axisOrder[4]
            try:
                self.location[axisName] = float(value)
            except ValueError:
                NSBeep()
    
class KeyedInstanceDescriptor(NSObject):
    #__metaclass__ = ClassNameIncrementer
    def __new__(cls):
        self = cls.alloc().init()
        self.dir = None
        self.path = None
        self.name = None
        self.location = None
        self.familyName = None
        self.styleName = None
        self.postScriptFontName = None
        self.styleMapFamilyName = None
        self.styleMapStyleName = None
        self.glyphs = {}
        self.axisOrder = []
        self.kerning = True
        self.info = True
        return self

    def setName(self):
        # make a name attribute based on the location
        name = ['instance', self.familyName, self.styleName]
        for k, v in self.location.items():
            name.append("%s_%3.3f"%(k, v))
        self.name = "_".join(name)
        
    def setAxisOrder(self, names):
        self.axisOrder = names

    def instanceHasFileKey(self):
        if self.path is None:
            return ""
        if os.path.exists(self.path):
            return hasPathSymbol
        return u""
    
    def getAxisValue(self, axisIndex):
        if 0 <= axisIndex < len(self.axisOrder):
            wantAxisName = self.axisOrder[axisIndex]
            if wantAxisName in self.location:
                v = self.location[wantAxisName]
                if type(v) == tuple:
                    return v[0]
                return v
        return ""
        
    def instanceAxis_1(self):
        return self.getAxisValue(0)
    def instanceAxis_2(self):
        return self.getAxisValue(1)
    def instanceAxis_3(self):
        return self.getAxisValue(2)
    def instanceAxis_4(self):
        return self.getAxisValue(3)
    def instanceAxis_5(self):
        return self.getAxisValue(4)

    def instanceUFONameKey(self):
        if self.path is None:
            self.makeUFOPathFromNames()
        return os.path.basename(self.path)
    
    def setPathRelativeTo(self, docFolder, instancesFolderName):
        self.dir = os.path.join(docFolder, instancesFolderName)
        self.makeUFOPathFromNames()

    def makeUFOPathFromNames(self):
        if self.familyName is not None and self.styleName is not None:
            fileName = "%s-%s.ufo"%(self.familyName, self.styleName)
            self.postScriptFontName = "%s-%s"%(self.familyName, self.styleName)
        else:
            fileName = "NewFamily-NewStyle.ufo"
            self.postScriptFontName = "NewFamily-NewStyle"
        if self.dir is None:
            saveDir = ""
        else:
            saveDir = self.dir
        self.path = os.path.join(saveDir, fileName)

    def setValue_forUndefinedKey_(self, value=None, key=None):
        if key == "instanceFamilyNameKey":
            self.familyName = value
            self.makeUFOPathFromNames()
        elif key == "instanceStyleNameKey":
            self.styleName = value
            self.makeUFOPathFromNames()
        elif key == "instanceAxis_1":
            axisName = self.axisOrder[0]
            try:
                self.location[axisName] = float(value)
            except ValueError:
                NSBeep()
        elif key == "instanceAxis_2":
            axisName = self.axisOrder[1]
            try:
                self.location[axisName] = float(value)
            except ValueError:
                NSBeep()
        elif key == "instanceAxis_3":
            axisName = self.axisOrder[2]
            try:
                self.location[axisName] = float(value)
            except ValueError:
                NSBeep()
        elif key == "instanceAxis_4":
            axisName = self.axisOrder[3]
            try:
                self.location[axisName] = float(value)
            except ValueError:
                NSBeep()
        elif key == "instanceAxis_5":
            axisName = self.axisOrder[4]
            try:
                self.location[axisName] = float(value)
            except ValueError:
                NSBeep()
            
    def instanceFamilyNameKey(self):
        return self.familyName
    def instanceStyleNameKey(self):
        return self.styleName
    
class KeyedAxisDescriptor(NSObject):
    __metaclass__ = ClassNameIncrementer
    # https://www.microsoft.com/typography/otspec/fvar.htm
    registeredTags = [
        ("italic", "ital"),
        ("optical", "opsz"),
        ("slant", "slnt"),
        ("width", "wdth"),
        ("weight", "wght"),
        ]
    def __new__(cls):
        self = cls.alloc().init()
        self.tag = None     # opentype tag for this axis
        self.name = None    # name of the axis used in locations
        self.labelNames = {} # names for UI purposes, if this is not a standard axis,
        self.minimum = None
        self.maximum = None
        self.default = None
        self.map = []
        return self
    
    def registeredTagKey(self):
        for name, tag in self.registeredTags:
            if name == self.name and tag == self.tag:
                return u"®"
        return ""
        
    def setValue_forUndefinedKey_(self, value=None, key=None):
        if key == "axisNameKey":
            for name, tag in self.registeredTags:
                if name == value:
                    self.tag = tag
            self.name = value
        elif key == "axisTagKey":
            if len(value)!=4:
                return
            self.tag = value
        elif key == "axisMinimumKey":
            try:
                num = float(value)
                self.minimum = num
            except ValueError:
                NSBeep()
        elif key == "axisMaximumKey":
            try:
                num = float(value)
                self.maximum = num
            except ValueError:
                NSBeep()
        elif key == "axisDefaultKey":
            try:
                num = float(value)
                self.default = num
            except ValueError:
                NSBeep()

    def axisNameKey(self):
        return self.name
    def axisTagKey(self):
        return self.tag
    def axisMinimumKey(self):
        return self.minimum
    def axisDefaultKey(self):
        return self.default
    def axisMaximumKey(self):
        return self.maximum


class KeyedDocReader(BaseDocReader):
    axisDescriptorClass = KeyedAxisDescriptor
    sourceDescriptorClass = KeyedSourceDescriptor
    instanceDescriptorClass = KeyedInstanceDescriptor
    
class KeyedDocWriter(BaseDocWriter):
    axisDescriptorClass = KeyedAxisDescriptor
    sourceDescriptorClass = KeyedSourceDescriptor
    instanceDescriptorClass = KeyedInstanceDescriptor

def newImageListCell():
    cell = NSImageCell.alloc().init()
    cell.setImageAlignment_(NSImageAlignTop)
    cell.setImageFrameStyle_(NSImageFrameNone)
    return cell


class DesignSpaceEditor:
    instanceFolderName = "instances"
    def __init__(self, designSpacePath=None):
        self.designSpacePath = designSpacePath
        #imageCell = newImageListCell()
        self.doc = None
        self._newInstanceCounter = 1
        if designSpacePath is None:
            fileNameTitle = "Untitled.designspace"
        else:
            fileNameTitle = os.path.basename(designSpacePath)
        self.w = Window((940, 700), fileNameTitle, minSize=(200,400))
        self._updatingTheAxesNames = False
        toolbarItems = [
            {
                'itemIdentifier': "toolbarSave",
                'label': 'Save',
                'callback': self.save,
                #'imageObject': NSImage.alloc().initWithContentsOfFile_('toolbar_axes_m.pdf')
                'imageObject': NSImage.imageNamed_('toolbar_axes_m')
                
            },
        ]
        self.w.addToolbar("DesignSpaceToolbar", toolbarItems)
        
        fileIconWidth = 30
        ufoNameWidth = 220
        axisValueWidth = 80
        familyNameWidth = 130
        masterColDescriptions = [
                {   'title': '',
                    'key':'sourceHasFileKey',
                    'width':fileIconWidth,
                    'editable':False,
                },
                {   'title': 'UFO',
                    'key':'sourceUFONameKey',
                    'width':ufoNameWidth,
                    'editable':False,
                },
                {   'title': 'Family Name',
                    'key': 'sourceFamilyNameKey',
                    'width':familyNameWidth,
                    'editable':True,
                },
                {   'title': 'Style Name',
                    'key':'sourceStyleNameKey',
                    'width':familyNameWidth,
                    'editable':True,
                },
                {   'title': 'Axis 1',
                    'key':'sourceAxis_1',
                    'width':axisValueWidth,
                    'editable':True,
                },
                {   'title': 'Axis 2',
                    'key':'sourceAxis_2',
                    'width':axisValueWidth,
                    'editable':True,
                },
                {   'title': 'Axis 3',
                    'key':'sourceAxis_3',
                    'width':axisValueWidth,
                    'editable':True,
                },
                {   'title': 'Axis 4',
                    'key':'sourceAxis_4',
                    'width':axisValueWidth,
                    'editable':True,
                },
                {   'title': 'Axis 5',
                    'key':'sourceAxis_5',
                    'width':axisValueWidth,
                    'editable':True,
                },
        ]
        instanceColDescriptions = [
                {   'title': '',
                    'key':'instanceHasFileKey',
                    'width':fileIconWidth,
                    'editable':False,
                },
                {   'title': 'UFO',
                    'key': 'instanceUFONameKey',
                    'width':ufoNameWidth,
                    'editable':False,
                },
                {   'title': 'Family Name',
                    'key': 'instanceFamilyNameKey',
                    'width':familyNameWidth,
                    'editable':True,
                },
                {   'title': 'Style Name',
                    'key':'instanceStyleNameKey',
                    'width':familyNameWidth,
                    'editable':True,
                },

                {   'title': 'Axis 1',
                    'key':'instanceAxis_1',
                    'width':axisValueWidth,
                    'editable':True,
                },
                {   'title': 'Axis 2',
                    'key':'instanceAxis_2',
                    'width':axisValueWidth,
                    'editable':True,
                },
                {   'title': 'Axis 3',
                    'key':'instanceAxis_3',
                    'width':axisValueWidth,
                    'editable':True,
                },
                {   'title': 'Axis 4',
                    'key':'instanceAxis_4',
                    'width':axisValueWidth,
                    'editable':True,
                },
                {   'title': 'Axis 5',
                    'key':'instanceAxis_5',
                    'width':axisValueWidth,
                    'editable':True,
                },
            ]
        axisColDescriptions = [
                {   'title': '',
                    'key':'registeredTagKey',
                    'width':40,
                    'editable':False,
                },
                {   'title': 'Name',
                    'key':'axisNameKey',
                    'width':100,
                    'editable':True,
                },
                {   'title': 'Tag',
                    'key':'axisTagKey',
                    'width':100,
                    'editable':True,
                },
                {   'title': 'Minimum',
                    'key':'axisMinimumKey',
                    'width':100,
                    'editable':True,
                },
                {   'title': 'Default',
                    'key':'axisDefaultKey',
                    'width':100,
                    'editable':True,
                },
                {   'title': 'Maximum',
                    'key':'axisMaximumKey',
                    'width':100,
                    'editable':True,
                },
            ]
        glyphsColDescriptions = [
                {   'title': '',
                    'key':'workingKey',
                    'width':40,
                    'editable':False,
                },
                {   'title': 'Glyphname',
                    'key':'glyphNameKey',
                    'width':40,
                    'editable':False,
                },
        ]
        toolbarHeight = 24
        self.axesGroup = Group((0,0,0,0))
        self.axesItem = List((0, toolbarHeight, -0, -0), [], columnDescriptions=axisColDescriptions, editCallback=self.callbackAxesListEdit)
        self.axesGroup.l = self.axesItem
        buttonMargin = 2
        buttonHeight = 20
        openButtonSize = (48,buttonMargin+1,50,buttonHeight)
        statusTextSize = (105, buttonMargin+4,-10,buttonHeight)
        axisToolDescriptions = [
            {'title': "+", 'width': 20,},
            {'title': "-", 'width': 20}
            ]
        self.axesGroup.tools = SegmentedButton(
            (buttonMargin,buttonMargin,100,buttonHeight),
            segmentDescriptions=axisToolDescriptions,
            selectionStyle="momentary",
            callback=self.callbackAxisTools)
        
        self.mastersGroup = Group((0,0,0,0))
        masterToolDescriptions = [
            {'title': "+", 'width': 20,},
            {'title': "-", 'width': 20},
            ]
        self.mastersItem = List((0, toolbarHeight, -0, -0), [],
            columnDescriptions=masterColDescriptions,
            #doubleClickCallback=self.callbackMastersDblClick
            selectionCallback=self.callbackMasterSelection
            )
        self.mastersGroup.l = self.mastersItem
        self.mastersGroup.tools = SegmentedButton(
            (buttonMargin, buttonMargin,100,buttonHeight),
            segmentDescriptions=masterToolDescriptions,
            selectionStyle="momentary",
            callback=self.callbackMasterTools)
        self.mastersGroup.status = TextBox(statusTextSize, "About the masters", sizeStyle="small")
        self.mastersGroup.openButton = Button(
            openButtonSize, "Open",
            callback=self.callbackOpenMaster,
            sizeStyle="small")
        self.mastersGroup.openButton.enable(False)
        
        self.instancesGroup = Group((0,0,0,0))
        self.instancesItem = List((0, toolbarHeight, -0, -0), [],
            columnDescriptions=instanceColDescriptions,
            selectionCallback=self.callbackInstanceSelection,
        )
        instancesToolDescriptions = [
            {'title': "+", 'width': 20,},
            {'title': "-", 'width': 20},
            ]

        self.instancesGroup.tools = SegmentedButton(
            (buttonMargin, buttonMargin,100,buttonHeight),
            segmentDescriptions=instancesToolDescriptions,
            selectionStyle="momentary",
            callback=self.callbackInstanceTools)
        self.instancesGroup.l = self.instancesItem
        self.instancesGroup.openButton = Button(
            openButtonSize, "Open",
            callback=self.callbackOpenInstance,
            sizeStyle="small")
        self.instancesGroup.openButton.enable(False)
        self.instancesGroup.status = TextBox(statusTextSize, "", sizeStyle="small")
        
        self.glyphsGroup = Group((0,0,0,0))
        self.glyphsItem = List((0, toolbarHeight, -0, -0), [],
            #columnDescriptions=glyphsColDescriptions,
            #selectionCallback=self.callbackInstanceSelection,
        )
        self.glyphsGroup.l = self.glyphsItem
    
        self.reportGroup = Group((0,0,0,0))
        self.reportGroup.text = TextBox((5,5,-5,-5), 'hehe')
        
        descriptions = [
           dict(label="Axes", view=self.axesGroup, size=138, collapsed=False, canResize=False),
           dict(label="Masters", view=self.mastersGroup, size=135, collapsed=False, canResize=True),
           dict(label="Instances", view=self.instancesGroup, size=135, collapsed=False, canResize=True),
           dict(label="Report", view=self.reportGroup, size=170, collapsed=False, canResize=True),
           # this panel will show glyphs and compatibiility.
           #dict(label="Glyphs", view=self.glyphsGroup, size=250, collapsed=False, canResize=True),
        ]
        self.read(designSpacePath)
        self.w.accordionView = AccordionView((0, 0, -0, -0), descriptions)
        self.updateAxesColumns()
        self.enableInstanceList()
        self.reportMasterPathStatus()
        self.w.open()
        if designSpacePath is not None:
            self.w.getNSWindow().setRepresentedURL_(NSURL.fileURLWithPath_(designSpacePath))
    
    def validate(self):
        # validate all data and write a report here.
        report = []
        
        # document and path
        if self.designSpacePath is None:
            report.append("This document has not been saved yet.")
            if len(self.doc.sources)>0:
                masterFolder = self.getSaveDirFromMasters()
                report.append("Make sure to save this document in %s."%masterFolder)
        else:
            report.append("Document source folder:")
            report.append("\t%s"%os.path.dirname(self.designSpacePath))

        # axes
        if len(self.doc.axes)==0:
            report.append("Define one or more axes")
        else:
            report.append("Axes:")
            for axis in self.doc.axes:
                if axis.registeredTagKey():
                    report.append("\t\"%s\" registered OpenType axis name"%axis.name)
                else:
                    report.append("\t\"%s\" is a non-standard axis name"%axis.name)
                
        # masters
        if len(self.doc.sources)==0:
            report.append("Add two or more UFOs.")
        else:
            report.append("Masters:")
            for master in self.doc.sources:
                report.append("\t%s"%master.path)
                if self.designSpacePath is not None:
                    if os.path.dirname(master.path)!=os.path.dirname(self.designSpacePath):
                        report.append("\tPlease move to the same folder as this document.")
        
        # instances
        if len(self.doc.instances)==0:
            report.append("Define an instance.")
        else:
            report.append("Instances:")
            for instance in self.doc.instances:
                report.append("\t%s"%instance.path)
        
        self.reportGroup.text.set("\n".join(report))
            
    def setMasterStatus(self, text):
        self.mastersGroup.status.set(text)
    
    def setInstancesStatus(self, text):
        self.instancesGroup.status.set(text)
        
    def openSelectedItem(self, sender):
        selection = sender.getSelection()
        if selection:
            progress = ProgressWindow("Opening UFO...", len(selection), parentWindow=self.w)
            for i in sender.getSelection():
                key = sender[i]
                if key.path is None:
                    continue
                path = key.path
                try:
                    alreadyOpen = False
                    for f in AllFonts():
                        if f.path == path:
                            thisWindow = f.document().getMainWindow()
                            thisWindow.show()
                            alreadyOpen = True
                            break
                    if not alreadyOpen:
                        font = RFont(path, showUI=False)
                        font.showUI()
                except:
                    print "Bad UFO:", path
                    pass
                progress.update()
            progress.close()            
        
    def callbackInstancesDblClick(self, sender):
        print "callbackInstancesDblClick"
    
    def updateAxesColumns(self):
        # present the axis names above all the columns.
        names = []
        for axis in self.doc.axes:
            names.append(axis.name)
        for descriptor in self.doc.instances:
            descriptor.setAxisOrder(names)
        for descriptor in self.doc.sources:
            descriptor.setAxisOrder(names)
        # clear the old names
        
        columns = self.mastersItem.getNSTableView().tableColumns()
        sourceColumnTitleOffset = 4
        for col in range(sourceColumnTitleOffset, len(columns)):
            column = columns[col]
            try:
                newTitle = names[col-sourceColumnTitleOffset]
            except IndexError:
                newTitle = u""
            column.setTitle_(newTitle)
        self.mastersItem.getNSTableView().reloadData()

        columns = self.instancesItem.getNSTableView().tableColumns()
        instancesColumnTitleOffset = 4
        for col in range(instancesColumnTitleOffset, len(columns)):
            column = columns[col]
            try:
                newTitle = names[col-instancesColumnTitleOffset]
            except IndexError:
                newTitle = u""
            column.setTitle_(newTitle)
        self.instancesItem.getNSTableView().reloadData()
        
    def read(self, designSpacePath):
        self.doc = DesignSpaceDocument(KeyedDocReader, KeyedDocWriter)
        if designSpacePath is not None:
            self.doc.read(designSpacePath)
        self.axesItem.set(self.doc.axes)
        self.mastersItem.set(self.doc.sources)
        self.instancesItem.set(self.doc.instances)
        self.validate()

    def getSaveDirFromMasters(self):
        options = {}
        for master in self.mastersItem:
            if master.path is not None:
                thisFileDir = os.path.dirname(master.path)
                options[thisFileDir] = True
        if len(options)==1:
            # neat and tidy, all masters in a folder
            return options.keys()[0]
        if options:
            paths = options.keys()
            paths.sort()
            return paths[0]
        return None
            
    def save(self, sender):
        if self.designSpacePath is None:
            # get a filepath first
            saveToDir = self.getSaveDirFromMasters()    # near the masters
            putFile(messageText="Save this designspace document",
                directory=saveToDir,
                canCreateDirectories=True,
                fileTypes=['designspace'],
                parentWindow=self.w,
                resultCallback=self.finalizeSave)
        else:
            self.finalizeSave(self.designSpacePath)

    def finalizeSave(self, path=None):
        for item in self.doc.sources:
            item.copyThisStuff(False)
        if self.doc.sources:
            self.doc.sources[0].copyThisStuff(True)
        self.designSpacePath = path
        # so we have the path for this document
        # we need to make sure the instances are all in the right place
        docFolder = os.path.dirname(path)
        for item in self.instancesItem:
            item.setPathRelativeTo(docFolder, self.instanceFolderName)
            item.setName()
        for item in self.mastersItem:
            item.setName()
        # dump all the paths
        #self.dumpFilePaths()
{'fraction_non_alphanumeric': 0.09963380331522989, 'fraction_numerical': 0.007783890884002335, 'mean_word_length': 1.5869754244656995, 'pattern_counts': {'":': 38, '<': 11, '<?xml version=': 0, '>': 5, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7547157', 'n_tokens_mistral': 15483, 'n_tokens_neox': 14708, 'n_words': 3050}