├── .gitignore ├── README.md ├── genlib.py ├── ibdump.py ├── ibgen.py ├── ibtool.py ├── nibencoding.py └── xibparser.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swp 3 | *.pyc 4 | 5 | # Ignore IB files we put in this directory. 6 | *.xib 7 | *.nib 8 | *.storyboard 9 | *.storyboardc 10 | *.txt 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ibtool 2 | My attempt to reverse engineer the iOS Nib format (used for storing compiled 3 | interface files) and create a tool to do similar things as Apple's ibtool. 4 | 5 | ## Usage 6 | Currently, ibtool supports only compiling XIB and storyboard files and 7 | printing NIB files in a readable way. (Only works with Interface Builder 8 | documents for iOS, not OS X.) 9 | 10 | Usage: ibtool.py [OPTIONS] input-file 11 | --dump dump the contents of a NIB file in a readable format 12 | --compile compile a XIB or storyboard file to a binary format 13 | -e show type encodings when dumping a NIB file 14 | 15 | If no command is specified, ibtool will assume --dump, 16 | i.e. `ibtool.py --dump somefile.nib` and `ibtool.py somefile.nib` are equivalent. 17 | 18 | ## Notes 19 | The set of Interface Builder features supported by this application is very limited, 20 | and requires specific functionalities to be manually added, so certain usages of 21 | unimplemented views, scenes, layout constraints, or size classes may fail to compile 22 | or result in NIBs that are missing functionality. 23 | -------------------------------------------------------------------------------- /genlib.py: -------------------------------------------------------------------------------- 1 | import nibencoding 2 | import struct 3 | 4 | ''' Base classes for Nib encoding ''' 5 | 6 | class NibObject: 7 | 8 | _total = 1000 9 | 10 | def __init__(self, classnme = "NSObject"): 11 | self._classname = classnme 12 | self._serial = NibObject._total 13 | NibObject._total += 1 14 | self.properties = { } 15 | self._nibidx = -1 16 | self._repr = None 17 | pass 18 | 19 | def setclassname(self, newname): 20 | self._classname = newname 21 | 22 | def classname(self): 23 | return self._classname 24 | 25 | def repr(self): 26 | return self._repr 27 | 28 | def setrepr(self, r): 29 | self._repr = r 30 | 31 | def nibidx(self): 32 | return self._nibidx 33 | 34 | def serial(self): 35 | return self._serial 36 | 37 | def get(self, key): 38 | return self.properties.get(key) 39 | 40 | def setIfEmpty(self, key, value): 41 | if key not in self.properties: 42 | self[key] = value 43 | def setIfNotDefault(self, key, value, default): 44 | if value != default: 45 | self[key] = value 46 | def append(self, key, value): 47 | if key in self.properties: 48 | assert(isinstance(self[key], list)) 49 | self[key].append(value) 50 | else: 51 | self[key] = [ value ] 52 | def extend(self, key, values): 53 | if key in self.properties: 54 | assert(isinstance(self[key], list)) 55 | self[key].extend(values) 56 | else: 57 | self[key] = list(values) 58 | 59 | def appendkv(self, dictKeyName, key, value): 60 | if not dictKeyName: 61 | return 62 | d = self.get(dictKeyName) 63 | if d is not None and not isinstance(d, dict): 64 | raise Exception("extendkv called non-dictionary NibObject property key") 65 | if d is None: 66 | d = { } 67 | self[dictKeyName] = d 68 | d[key] = value 69 | 70 | 71 | def __getitem__(self, key): 72 | return self.properties[key] 73 | 74 | def __setitem__(self, key, item): 75 | if item is None: 76 | return 77 | self.properties[key] = item 78 | 79 | def __delitem__(self, item): 80 | del self.properties[item] 81 | 82 | # Returns a list of tuples 83 | def getKeyValuePairs(self): 84 | return self.properties.items() 85 | 86 | class NibString(NibObject): 87 | def __init__(self, text = "Hello World"): 88 | NibObject.__init__(self, "NSString") 89 | self._text = text 90 | def getKeyValuePairs(self): 91 | return [("NS.bytes", self._text)] 92 | def __repr__(self): 93 | return "%s %s" % (object.__repr__(self), self._text) 94 | 95 | class NibData(NibObject): 96 | def __init__(self, data): 97 | NibObject.__init__(self, "NSData") 98 | self._data = data 99 | def getKeyValuePairs(self): 100 | # print "MARCO YOLO", type(self._data) 101 | # raise Exception("EVERYTHING IS OK") 102 | return [("NS.bytes", self._data)] 103 | 104 | class NibInlineString: 105 | def __init__(self, text = ""): 106 | self._text = text 107 | 108 | def text(self): 109 | return self._text 110 | 111 | class NibByte: 112 | def __init__(self, val = 0): 113 | self._val = val 114 | def val(self): 115 | return self._val 116 | 117 | class NibNil: 118 | def __init__(self): 119 | pass 120 | 121 | def NibFloatToWord(num): 122 | bytes = struct.pack("= 0 and val < 256: 155 | return [('NS.intval', NibByte(val))] 156 | return ('NS.intval', val) 157 | 158 | #TODO: Have more stuff use this. 159 | #TODO: Make this recursive. 160 | # Is this only for dictionaries? 161 | def convertToNibObject(obj): 162 | if isinstance(obj, NibObject): 163 | return obj # Yep, here is where we would put recursion. IF WE HAD ANY. 164 | elif isinstance(obj, basestring): 165 | return NibString(obj) 166 | elif isinstance(obj, int) or isinstance(obj, float): 167 | return NibNSNumber(obj) 168 | elif isinstance(obj, NibByte): 169 | return NibNSNumber(obj.val()) 170 | return obj 171 | 172 | class NibDictionaryImpl(NibObject): 173 | def __init__(self, objects): 174 | NibObject.__init__(self, "NSDictionary") 175 | if isinstance(objects, dict): 176 | t = [] 177 | for k,v in objects.iteritems(): 178 | k = convertToNibObject(k) 179 | v = convertToNibObject(v) 180 | t.extend([k,v]) 181 | objects = t 182 | self._objects = objects 183 | 184 | def getKeyValuePairs(self): 185 | pairs = [ ( 'NSInlinedValue', True ) ] 186 | pairs.extend([ ('UINibEncoderEmptyKey', obj ) for obj in self._objects ]) 187 | return pairs 188 | 189 | ''' Convenience Classes ''' 190 | 191 | class NibProxyObject(NibObject): 192 | def __init__(self, identifier): 193 | NibObject.__init__(self, "UIProxyObject") 194 | self['UIProxiedObjectIdentifier'] = identifier 195 | 196 | ''' Conversion Stuff ''' 197 | 198 | 199 | class CompilationContext(): 200 | def __init__(self): 201 | self.class_set = set() 202 | self.serial_set = set() # a set of serial numbers for objects that have been added to the object list. 203 | 204 | self.object_list = [] 205 | 206 | def addBinObject(self, obj): 207 | pass 208 | 209 | def addObjects(self, objects): 210 | for o in objects: 211 | self.addObject(o) 212 | 213 | def addObject(self, obj): 214 | 215 | if not isinstance(obj, NibObject): 216 | print "CompilationContext.addObject: Non-NibObject value:", obj 217 | raise Exception("Not supported.") 218 | 219 | serial = obj.serial() 220 | if serial in self.serial_set: 221 | return 222 | self.serial_set.add(serial) 223 | 224 | cls = obj.classname() 225 | if cls not in self.class_set: 226 | self.class_set.add(cls) 227 | 228 | obj._nibidx = len(self.object_list) 229 | self.object_list.append(obj) 230 | 231 | # Determine the set of objects to convert/add 232 | keyset = None 233 | objectset = None 234 | 235 | if isinstance(obj, NibDictionaryImpl): 236 | # objects = obj._objects 237 | # for i in range(0, len(objects)) 238 | # self.addObjects(obj._objects) 239 | 240 | keyset = range(0, len(obj._objects)) 241 | objectset = obj._objects 242 | 243 | elif isinstance(obj, NibList): 244 | # self.addObjects(obj._items) 245 | 246 | keyset = range(0, len(obj._items)) 247 | objectset = obj._items 248 | 249 | else: 250 | keyset = list(obj.properties.keys()) 251 | objectset = obj.properties 252 | 253 | # Add all the subobjects to the object set. 254 | 255 | for key in keyset: 256 | value = objectset[key] 257 | 258 | if isinstance(value, NibObject): 259 | self.addObject(value) 260 | elif isinstance(value, list): 261 | for itm in value: 262 | self.addObject(itm) 263 | value = NibList(value) 264 | self.addObject(value) 265 | objectset[key] = value 266 | elif isinstance(value, basestring): 267 | value = NibString(value) 268 | self.addObject(value) 269 | objectset[key] = value 270 | elif isinstance(value, dict): 271 | value = NibDictionaryImpl(value) 272 | self.addObject(value) 273 | objectset[key] = value 274 | 275 | 276 | 277 | 278 | def makeTuples(self): 279 | 280 | out_objects = [] 281 | out_keys = [] 282 | out_values = [] 283 | out_classes = [] 284 | 285 | def idx_of_class(cls): 286 | if cls in out_classes: 287 | return out_classes.index(cls) 288 | out_classes.append(cls) 289 | return len(out_classes) - 1 290 | def idx_of_key(key): 291 | if key in out_keys: 292 | return out_keys.index(key) 293 | out_keys.append(key) 294 | return len(out_keys) - 1 295 | 296 | for object in self.object_list: 297 | 298 | obj_values_start = len(out_values) 299 | kvpairs = object.getKeyValuePairs() 300 | for k,v in kvpairs: 301 | 302 | if isinstance(v, NibObject): 303 | key_idx = idx_of_key(k) 304 | vtuple = (key_idx, nibencoding.NIB_TYPE_OBJECT, v.nibidx(), v) 305 | out_values.append(vtuple) 306 | elif isinstance(v, basestring) or isinstance(v, bytearray): 307 | key_idx = idx_of_key(k) 308 | vtuple = (key_idx, nibencoding.NIB_TYPE_STRING, v) 309 | out_values.append(vtuple) 310 | elif isinstance(v, NibInlineString): 311 | key_idx = idx_of_key(k) 312 | vtuple = (key_idx, nibencoding.NIB_TYPE_STRING, v.text()) 313 | out_values.append(vtuple) 314 | elif isinstance(v, NibByte): 315 | out_values.append((idx_of_key(k), nibencoding.NIB_TYPE_BYTE, v.val())) 316 | elif v is True: 317 | out_values.append((idx_of_key(k), nibencoding.NIB_TYPE_TRUE)) 318 | elif v is False: 319 | out_values.append((idx_of_key(k), nibencoding.NIB_TYPE_FALSE)) 320 | elif isinstance(v, float): 321 | out_values.append((idx_of_key(k), nibencoding.NIB_TYPE_DOUBLE, v)) 322 | elif isinstance(v, int): 323 | if v < 0: 324 | raise Exception("Encoding negative integers is not supported yet.") 325 | elif v < 0x100: 326 | out_values.append((idx_of_key(k), nibencoding.NIB_TYPE_BYTE, v)) 327 | elif v < 0x10000: 328 | out_values.append((idx_of_key(k), nibencoding.NIB_TYPE_SHORT, v)) 329 | else: 330 | raise Exception("Encoding integers larger than short is not supported yet.") 331 | 332 | elif isinstance(v, tuple): 333 | for el in v: 334 | if not isinstance(el, float): 335 | raise Exception("Only tuples of floats are supported now. Type = " + str(type(el))) 336 | data = bytearray() 337 | data.append(0x07) 338 | data.extend(struct.pack('<' + 'd' * len(v), *v)) 339 | out_values.append((idx_of_key(k), nibencoding.NIB_TYPE_STRING, data)) 340 | 341 | obj_values_end = len(out_values) 342 | class_idx = idx_of_class(object.classname()) 343 | out_objects.append((class_idx, obj_values_start, obj_values_end - obj_values_start)) 344 | 345 | return (out_objects, out_keys, out_values, out_classes) 346 | 347 | 348 | 349 | ''' 350 | This really has (at least) two phases. 351 | 1. Traverse/examine the object graph to find the objects/keys/values/classes that need to be encoded. 352 | 2. Once those lists are built and resolved, convert them into binary format. 353 | ''' 354 | def CompileNibObjects(objects): 355 | 356 | ctx = CompilationContext() 357 | ctx.addObjects(objects) 358 | t = ctx.makeTuples() 359 | 360 | return nibencoding.WriteNib(t) 361 | -------------------------------------------------------------------------------- /ibdump.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import os 4 | import sys 5 | import struct 6 | 7 | def rword(bytes): 8 | return struct.unpack(" 30: 32 | raise Exception("Flex number invalid or too large.") 33 | return (number, ptr - addr) 34 | 35 | def readHeader(bytes, start): 36 | hsize = rword(bytes[start : start+4]) 37 | # print "Header size (words): " + str(hsize) 38 | sections = [] 39 | sectionDataStart = start + 4 40 | for section in range(0, (hsize - 1)/2): 41 | objcount = rword(bytes[sectionDataStart + section * 8 : sectionDataStart + section * 8 + 4]) 42 | address = rword(bytes[sectionDataStart + section * 8 + 4 : sectionDataStart + section * 8 + 8]) 43 | sections += [(objcount, address)] 44 | return sections 45 | 46 | def readKeys(bytes, keysSection): 47 | count, ptr = keysSection 48 | keys = [] 49 | for i in range(0, count): 50 | 51 | rd = readFlexNumber(bytes, ptr) 52 | length = rd[0] 53 | ptr += rd[1] 54 | 55 | keys.append(str(bytes[ptr : ptr + length])) 56 | ptr += length 57 | return keys 58 | 59 | def readObjects(bytes, objectsSection): 60 | count, ptr = objectsSection 61 | objects = [] 62 | for i in range(0, count): 63 | r0 = readFlexNumber(bytes, ptr) 64 | r1 = readFlexNumber(bytes, ptr + r0[1]) 65 | r2 = readFlexNumber(bytes, ptr + r0[1] + r1[1]) 66 | 67 | class_idx = r0[0] 68 | start_idx = r1[0] 69 | size = r2[0] 70 | 71 | ptr += r0[1] + r1[1] + r2[1] 72 | 73 | objects.append((class_idx, start_idx, size)) 74 | return objects 75 | 76 | def readClasses(bytes, classSection): 77 | count, addr = classSection 78 | classes = [] 79 | ptr = addr 80 | for i in range(0, count): 81 | r = readFlexNumber(bytes, ptr) 82 | length = r[0] 83 | ptr += r[1] 84 | 85 | tp = ord(bytes[ptr]) 86 | ptr += 1 87 | 88 | unknown = None 89 | assert(tp in [0x80, 0x81]) 90 | if tp == 0x81: 91 | unknown = rword(bytes[ptr : ptr + 4]) 92 | ptr += 4 93 | print 'readClasses: Mystery value:', unknown, '(', 94 | 95 | classes.append(str(bytes[ptr : ptr + length - 1])) 96 | 97 | if unknown: 98 | print classes[-1], ')' 99 | 100 | ptr += length 101 | 102 | return classes 103 | 104 | def readValues(bytes, valuesSection, debugKeys = []): 105 | count, addr = valuesSection 106 | values = [] 107 | ptr = addr 108 | for i in range(0, count): 109 | r = readFlexNumber(bytes, ptr) 110 | key_idx = r[0] 111 | ptr += r[1] 112 | 113 | encoding = ord(bytes[ptr]) 114 | ptr += 1 115 | 116 | value = None 117 | if encoding == 0x00: # single byte 118 | value = ord(bytes[ptr]) 119 | ptr += 1 120 | elif encoding == 0x01: # short 121 | value = struct.unpack(" 40 and v_str.startswith('NIBArchive') 181 | 182 | if printSubNib: 183 | print prefix + '\t' + k_str + " = Encoded NIB Archive" 184 | nib = readNibSectionsFromBytes(v[1]) 185 | fancyPrintObjects(nib, prefix + "\t", showencoding) 186 | 187 | else: # Boring regular data. 188 | if showencoding: 189 | print prefix + '\t' + k_str + ' = (' + str(v[2]) + ')', v_str 190 | else: 191 | print prefix + '\t' + k_str + ' =', v_str 192 | 193 | # if k_str == 'NS.bytes' and len(v_str) > 200: 194 | # with open('embedded.nib', 'wb') as f: 195 | # f.write(v[1]) 196 | 197 | def readNibSectionsFromBytes(bytes): 198 | sections = readHeader(bytes, 14) 199 | # print sections 200 | classes = readClasses(bytes, sections[3]) 201 | # print classes 202 | objects = readObjects(bytes, sections[0]) 203 | # print objects 204 | keys = readKeys(bytes, sections[1]) 205 | # print keys 206 | values = readValues(bytes, sections[2]) 207 | # print values 208 | return (objects, keys, values, classes) 209 | 210 | def ibdump(filename, showencoding=None): 211 | with open(filename, 'rb') as file: 212 | filebytes = file.read() 213 | 214 | pfx = filebytes[0:10] 215 | print "Prefix: " + pfx 216 | 217 | headers = filebytes[10:10+4] 218 | headers = rword(headers) 219 | print "Headers: " + str(headers) 220 | 221 | if str(pfx) != "NIBArchive": 222 | print "\"%s\" is not a NIBArchive file." % (filename) 223 | return 224 | 225 | nib = readNibSectionsFromBytes(filebytes) 226 | fancyPrintObjects(nib, showencoding=showencoding) 227 | 228 | if __name__ == '__main__': 229 | ibdump(filename = sys.argv[1]) -------------------------------------------------------------------------------- /ibgen.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import os 4 | import sys 5 | import struct 6 | 7 | from genlib import * 8 | 9 | 10 | 11 | 12 | view = NibObject('UIView') 13 | viewController = NibObject('UINavigationController') 14 | 15 | viewController['UIView'] = view 16 | 17 | root = NibObject('NSObject') 18 | firstResponder = NibObject('UIProxyObject') 19 | firstResponder['UIProxiedObjectIdentifier'] = "IBFirstResponder" 20 | filesOwner = NibObject('UIProxyObject') 21 | filesOwner['UIProxiedObjectIdentifier'] = "IBFilesOwner" 22 | root['UINibTopLevelObjectsKey'] = [ filesOwner, firstResponder, viewController ] 23 | root['UINibObjectsKey'] = [ firstResponder, filesOwner, viewController ] 24 | root['UINibConnectionsKey'] = [ ] 25 | root['UINibVisibleWindowsKey'] = [] 26 | root['UINibAccessibilityConfigurationsKey'] = [ ] 27 | root['UINibTraitStorageListsKey'] = [ ] 28 | root['UINibKeyValuePairsKey'] = [ ] 29 | 30 | 31 | 32 | 33 | 34 | 35 | CompileNibObjects([root]) 36 | 37 | -------------------------------------------------------------------------------- /ibtool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | import xml.etree.ElementTree as ET 5 | import xibparser 6 | import genlib 7 | import getopt 8 | import ibdump 9 | 10 | class IBCommands(object): 11 | Compile = 0 12 | Dump = 1 13 | 14 | def main(): 15 | 16 | ops, args = getopt.getopt(sys.argv[1:], 'e', ['compile=', 'write=', 'dump']) 17 | 18 | # print ops 19 | # print args 20 | 21 | if len(args) == 0: 22 | print "Error: No input file given." 23 | sys.exit(1) 24 | elif len(args) > 1: 25 | print "Error: ibtool currently only supports one input file." 26 | sys.exit(1) 27 | 28 | command = IBCommands.Dump 29 | inpath = args[0] 30 | 31 | _write = None 32 | _compile = None 33 | shortflags = [] 34 | 35 | for option, value in ops: 36 | if option == '--compile': 37 | command = IBCommands.Compile 38 | _compile = value 39 | elif option == '--write': 40 | _write = value 41 | elif option == '--dump': 42 | command = IBCommands.Dump 43 | elif option == '-e': 44 | shortflags.append('e') 45 | 46 | if command is None: 47 | print "Error: No command given." 48 | sys.exit(1) 49 | 50 | if command == IBCommands.Compile: 51 | ib_compile(inpath, _write or _compile) 52 | elif command == IBCommands.Dump: 53 | ib_dump(inpath, shortflags) 54 | 55 | 56 | def ib_compile(inpath, outpath): 57 | def die_if(condition, message): 58 | if condition: 59 | print message 60 | exit(1) 61 | 62 | die_if(not outpath, "ib_compile: No input path given") 63 | 64 | suffix = None 65 | if inpath.endswith(".xib"): 66 | suffix = "xib" 67 | elif inpath.endswith(".storyboard"): 68 | suffix = "storyboard" 69 | 70 | die_if(suffix is None, "ib_compile: Only .xib and .storyboard files are currently supported.") 71 | if suffix == 'xib': 72 | ib_compile_xib(inpath, outpath) 73 | elif suffix == 'storyboard': 74 | ib_compile_storyboard(inpath, outpath) 75 | 76 | def ib_compile_xib(inpath, outpath): 77 | tree = ET.parse(inpath) 78 | root = tree.getroot() 79 | objects = root.iter('objects').next() 80 | nibroot = xibparser.ParseXIBObjects(objects) 81 | outbytes = genlib.CompileNibObjects([nibroot]) 82 | 83 | with open(outpath, 'wb') as fl: 84 | fl.write(outbytes) 85 | 86 | def ib_compile_storyboard(inpath, outpath): 87 | tree = ET.parse(inpath) 88 | xibparser.CompileStoryboard(tree, outpath) 89 | 90 | def ib_dump(inpath, shortflags): 91 | showencoding = 'e' in shortflags 92 | ibdump.ibdump(inpath, showencoding) 93 | 94 | if __name__ == '__main__': 95 | main() -------------------------------------------------------------------------------- /nibencoding.py: -------------------------------------------------------------------------------- 1 | 2 | import struct 3 | 4 | NIB_TYPE_BYTE = 0x00 5 | NIB_TYPE_SHORT = 0x01 6 | NIB_TYPE_FALSE = 0x04 7 | NIB_TYPE_TRUE = 0x05 8 | NIB_TYPE_WORD = 0x06 9 | NIB_TYPE_DOUBLE = 0x07 10 | NIB_TYPE_STRING = 0x08 # Can also be used for tuples. e.g. CGPoint/Size/Rect 11 | NIB_TYPE_OBJECT = 0x0A 12 | 13 | 14 | 15 | # Input: Tuple of the four nib components. (Objects, Keys, Values, Classes) 16 | # Output: A byte array containing the binary representation of the nib archive. 17 | def WriteNib(nib): 18 | bytes = bytearray() 19 | bytes.extend("NIBArchive") 20 | bytes.extend([1,0,0,0]) 21 | bytes.extend([9,0,0,0]) 22 | 23 | objs = nib[0] 24 | keys = nib[1] 25 | vals = nib[2] 26 | clss = nib[3] 27 | 28 | objs_section = _nibWriteObjectsSection(objs) 29 | keys_section = _nibWriteKeysSection(keys) 30 | vals_section = _nibWriteValuesSection(vals) 31 | clss_section = _nibWriteClassesSection(clss) 32 | 33 | header_size = 50 34 | objs_start = header_size 35 | keys_start = objs_start + len(objs_section) 36 | vals_start = keys_start + len(keys_section) 37 | clss_start = vals_start + len(vals_section) 38 | 39 | for num in [ len(objs), objs_start, 40 | len(keys), keys_start, 41 | len(vals), vals_start, 42 | len(clss), clss_start, ]: 43 | bytes.extend(struct.pack("> 7 57 | if not number: 58 | break 59 | btarray.append(cur_byte) 60 | cur_byte |= 0x80 61 | btarray.append(cur_byte) 62 | 63 | def _nibWriteObjectsSection(objects): 64 | bytes = bytearray() 65 | for obj in objects: 66 | _nibWriteFlexNumber(bytes, obj[0]) 67 | _nibWriteFlexNumber(bytes, obj[1]) 68 | _nibWriteFlexNumber(bytes, obj[2]) 69 | return bytes 70 | 71 | def _nibWriteKeysSection(keys): 72 | bytes = bytearray() 73 | for key in keys: 74 | _nibWriteFlexNumber(bytes, len(key)) 75 | bytes.extend(key) 76 | return bytes 77 | 78 | def _nibWriteClassesSection(classes): 79 | bytes = bytearray() 80 | for cls in classes: 81 | _nibWriteFlexNumber(bytes, len(cls) + 1) 82 | bytes.append(0x80) 83 | bytes.extend(cls) 84 | bytes.append(0x00) 85 | return bytes 86 | 87 | def _nibWriteValuesSection(values): 88 | bytes = bytearray() 89 | for value in values: 90 | keyidx = value[0] 91 | encoding_type = value[1] 92 | _nibWriteFlexNumber(bytes, keyidx) 93 | bytes.append(encoding_type) 94 | 95 | if encoding_type == NIB_TYPE_FALSE: 96 | continue 97 | if encoding_type == NIB_TYPE_TRUE: 98 | continue 99 | if encoding_type == NIB_TYPE_OBJECT: 100 | try: 101 | bytes.extend(struct.pack("objects 14 | # For storyboards, this is typically document->scenes->scene->objects 15 | def ParseXIBObjects(element, context = None, resolveConnections = True, parent = None): 16 | 17 | toplevel = [] 18 | 19 | context = context or ArchiveContext() 20 | 21 | for nib_object_element in element: 22 | obj = __xibparser_ParseXIBObject(context, nib_object_element, parent) 23 | if obj: 24 | toplevel.append(obj) 25 | 26 | if resolveConnections: 27 | context.resolveConnections() 28 | 29 | root = NibObject("NSObject") 30 | root['UINibTopLevelObjectsKey'] = toplevel 31 | root['UINibConnectionsKey'] = context.connections# __xibparser_resolveConnections(ib_connections, ib_objects) 32 | root['UINibObjectsKey'] = list(toplevel) 33 | root['UINibObjectsKey'].extend(context.extraNibObjects) 34 | 35 | return root 36 | 37 | 38 | def CompileStoryboard(tree, foldername): 39 | 40 | import os 41 | if os.path.isdir(foldername): 42 | import shutil 43 | shutil.rmtree(foldername) 44 | 45 | os.mkdir (foldername) 46 | 47 | root = tree.getroot() 48 | init = root.attrib.get('initialViewController') 49 | 50 | scenesNode = root.iter('scenes').next() 51 | 52 | identifierMap = { } 53 | idToNibNameMap = { } 54 | idToViewControllerMap = { } 55 | 56 | # Make some constants before 57 | 58 | fowner = NibProxyObject("IBFilesOwner") 59 | sbplaceholder = NibProxyObject('UIStoryboardPlaceholder') 60 | 61 | # A list of tuples containing: 62 | # - The view controller of the scene. 63 | # - The root objects for the scene. 64 | # - The view controller nib name. 65 | # We can't write the scene nibs as we read the scenes, because some things might depend on having 66 | # seen all the scenes. (e.g. Segues, which need to know how to translate ID into storyboardIdentifier) 67 | scenesToWrite = [] 68 | 69 | for sceneNode in scenesNode: 70 | 71 | toplevel = [] 72 | 73 | sceneID = sceneNode.attrib['sceneID'] 74 | objects = sceneNode.iter('objects').next() 75 | viewController = None 76 | viewControllerNibName = None 77 | 78 | context = ArchiveContext() 79 | context.isStoryboard = True 80 | 81 | for elem in objects: 82 | 83 | obj = __xibparser_ParseXIBObject(context, elem, None) 84 | if not obj: 85 | continue 86 | viewNibFilename = None 87 | 88 | toplevel.append(obj) 89 | context.toplevel.append(obj) 90 | 91 | viewController = context.storyboardViewController 92 | if not viewController: 93 | raise Exception("Storyboard scene did not have associated view controller.") 94 | 95 | context.resolveConnections() 96 | 97 | viewControllerNibName = viewController.xibattributes.get('storyboardIdentifier') or "UIViewController-" + viewController.xibattributes['id'] 98 | identifierMap[viewControllerNibName] = viewControllerNibName 99 | idToNibNameMap[viewController.xibattributes['id']] = viewControllerNibName 100 | idToViewControllerMap[viewController.xibattributes['id']] = viewController 101 | view = viewController.properties.get('UIView') 102 | if view: 103 | del viewController.properties['UIView'] 104 | context.extraNibObjects.remove(view) # Don't encode the view in the scene nib's objects. 105 | 106 | view.extend('UISubviews', context.viewControllerLayoutGuides) 107 | 108 | ViewConnection = NibObject('UIRuntimeOutletConnection') 109 | ViewConnection['UILabel'] = 'view' 110 | ViewConnection['UISource'] = fowner 111 | ViewConnection['UIDestination'] = view 112 | 113 | viewNibFilename = "%s-view-%s" % (viewController.xibattributes.get('id'), view.repr().attrib.get('id')) 114 | 115 | root = NibObject('NSObject') 116 | root['UINibTopLevelObjectsKey'] = [ view ] # + context.viewConnections 117 | root['UINibObjectsKey'] = [ view ] # + context.viewConnections 118 | root['UINibConnectionsKey'] = [ ViewConnection ] + context.viewConnections 119 | # root['UINibConnectionsKey'] 120 | 121 | with open("%s/%s%s" % (foldername, viewNibFilename, ".nib"), 'wb') as fl: 122 | fl.write(CompileNibObjects([root])) 123 | 124 | 125 | # Not setting the UINibName key is acceptable. 126 | # I'm guessing things like UINavigationController scenes do that. 127 | print 'viewNibFilename:', viewNibFilename 128 | viewController['UINibName'] = viewNibFilename 129 | 130 | toplevel.append(fowner) 131 | toplevel.append(sbplaceholder) 132 | 133 | FilesOwnerConnection = NibObject('UIRuntimeOutletConnection') 134 | FilesOwnerConnection['UILabel'] = 'sceneViewController' 135 | FilesOwnerConnection['UISource'] = fowner 136 | FilesOwnerConnection['UIDestination'] = viewController 137 | 138 | StoryboardConnection = NibObject('UIRuntimeOutletConnection') 139 | StoryboardConnection['UILabel'] = 'storyboard' 140 | StoryboardConnection['UISource'] = viewController 141 | StoryboardConnection['UIDestination'] = sbplaceholder 142 | viewController.sceneConnections.append(StoryboardConnection) 143 | 144 | nibconnections = [ FilesOwnerConnection, StoryboardConnection ] + context.sceneConnections 145 | 146 | root = NibObject("NSObject") 147 | root['UINibTopLevelObjectsKey'] = toplevel 148 | root['UINibConnectionsKey'] = nibconnections 149 | root['UINibObjectsKey'] = list(toplevel) 150 | root['UINibObjectsKey'].extend(context.extraNibObjects) 151 | 152 | scenesToWrite.append( (viewController, root, viewControllerNibName) ) 153 | 154 | 155 | # Do some additional processing before the scenes are written. 156 | # This includes resolving references for storyboard segues and assigning 157 | # all the appropriate values for relationship segues in the storyboard. 158 | for finalScene in scenesToWrite: 159 | 160 | viewController, root, viewControllerNibName = finalScene 161 | 162 | for segue in viewController.get('UIStoryboardSegueTemplates') or []: 163 | dest = segue['UIDestinationViewControllerIdentifier'] 164 | if isinstance(dest, NibString): 165 | dest = dest._text 166 | if isinstance(dest, basestring): 167 | segue['UIDestinationViewControllerIdentifier'] = idToNibNameMap[dest] 168 | 169 | # This is kinda ugly. It's inspired by the need to set certain properties on the view controller, 170 | # like UIParentViewController, which we only want set when we're including the view controller 171 | # inside another view controller's nib. We make a copy of the properties array, add what we need, 172 | # then put the dict back when we're done. 173 | resetProperties = [ ] 174 | 175 | if viewController.relationshipsegue is not None: 176 | segue = viewController.relationshipsegue 177 | relationship = segue.attrib['relationship'] 178 | if relationship == 'rootViewController': 179 | rootViewController = idToViewControllerMap[segue.attrib['destination']] 180 | viewController['UIChildViewControllers'] = [rootViewController] 181 | viewController['UIViewControllers'] = [rootViewController] 182 | 183 | if viewController.sceneConnections: 184 | root['UINibConnectionsKey'].extend(rootViewController.sceneConnections) 185 | 186 | resetProperties.append( (rootViewController, dict(rootViewController.properties)) ) 187 | 188 | rootViewController['UIParentViewController'] = viewController 189 | # Maybe also set a default UINavigationItem? 190 | 191 | bytes = CompileNibObjects([root]) 192 | with open("%s/%s%s" %(foldername,viewControllerNibName,".nib"), 'wb') as fl: 193 | fl.write(bytes) 194 | 195 | for viewController, oldProperties in resetProperties: 196 | viewController.properties = oldProperties 197 | 198 | 199 | 200 | storyboard_info = { 201 | "UIViewControllerIdentifiersToNibNames": identifierMap, 202 | "UIStoryboardVersion" : 1 203 | } 204 | 205 | if init: 206 | init = idToNibNameMap.get(init) or init 207 | storyboard_info['UIStoryboardDesignatedEntryPointIdentifier'] = init 208 | 209 | print "INIT:", init 210 | 211 | import plistlib 212 | plistlib.writePlist(storyboard_info, foldername + "/Info.plist") 213 | 214 | 215 | def makexibid(): 216 | import random 217 | chars = random.sample('0123456789qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM', 10) 218 | chars[3] = '-' 219 | chars[6] = '-' 220 | return ''.join(chars) 221 | 222 | def makePlaceholderIdentifier(): 223 | return "UpstreamPlaceholder-" + makexibid() 224 | 225 | class ArchiveContext: 226 | def __init__(self): 227 | self.connections = [] 228 | self.objects = { } # When parsing a storyboard, this doesn't include the main view or any of its descendant objects. 229 | self.toplevel = [ ] 230 | 231 | self.extraNibObjects = [ ] 232 | self.isStoryboard = False 233 | 234 | # These are used only for storyboards. 235 | self.storyboardViewController = None 236 | self.isParsingStoryboardView = False 237 | self.viewObjects = { } 238 | self.viewConnections = [] 239 | self.sceneConnections = [] 240 | self.segueConnections = [] 241 | 242 | self.isPrototypeList = False 243 | 244 | 245 | # What I plan on using after the context revision: 246 | 247 | self.upstreamPlaceholders = { } 248 | self.parentContext = None 249 | self.viewReferences = [] # List of tuples ( view id, referencing object, referencing key ) 250 | self.viewControllerLayoutGuides = [ ] 251 | # self.view = None 252 | # self.viewController = None 253 | 254 | def contextForSegues(self): 255 | if self.isPrototypeList: 256 | return self.parentContext 257 | return self 258 | 259 | def addObject(self, objid, obj, forceSceneObject = None): 260 | dct = self.viewObjects if self.isParsingStoryboardView else self.objects 261 | if forceSceneObject is not None: 262 | dct = self.objects if forceSceneObject else self.viewObjects 263 | dct[objid] = obj 264 | 265 | # if self.isParsingStoryboardView: 266 | # self.viewObjects[objid] = obj 267 | # else: 268 | # self.objects[objid] = obj 269 | 270 | # to be used for objects that are known to be in the same context, given a valid document. (For possibly 271 | # unkown values, use getObject) 272 | # Also this meant to be an abstraction around the shitty 'objects' vs 'viewObjects' vs $whatever organization scheme. 273 | def findObject(self, objid): 274 | obj = self.getObject(objid) 275 | if obj is None and objid is not None: 276 | raise Exception("Object with id %s not found in archive context." % (objid)) 277 | return obj 278 | 279 | def getObject(self, objid): 280 | if not objid: 281 | return None 282 | if objid in self.viewObjects: 283 | return self.viewObjects[objid] 284 | if objid in self.objects: 285 | return self.objects[objid] 286 | return None 287 | 288 | # Kinda ugly. If we ever use a separate ArchiveContext for storyboard scenes and their views, we can use just use getObject. 289 | # Basically this is like getObject, but only searches in the right one of 'objects' or 'viewObjects' 290 | def getObjectInCurrentContext(self, objid): 291 | if objid is None: 292 | return None 293 | if self.isParsingStoryboardView: 294 | return self.viewObjects.get(objid) 295 | else: 296 | return self.objects[objid] 297 | return None 298 | 299 | def resolveConnections(self): 300 | if not self.isStoryboard: 301 | self._resolveConnections_xib() 302 | else: 303 | self._resolveConnections_storyboard() 304 | self._resolveViewReferences() 305 | 306 | def _resolveViewReferences(self): 307 | for ref in self.viewReferences: 308 | view_id, obj, key = ref 309 | obj[key] = self.findObject(view_id) 310 | 311 | 312 | def _resolveConnections_xib(self): 313 | result = [] 314 | for con in self.connections: 315 | dst = con['UIDestination'] 316 | if isinstance(dst, NibProxyObject): 317 | result.append(con) 318 | continue 319 | 320 | # How does this happen? 321 | if isinstance(dst, XibObject): 322 | result.append(con) 323 | continue 324 | # I think this resolution code will be obsolete when we start using UpstreamPlaceholder's. 325 | assert isinstance(dst, basestring), "%s is not a string ID" % dst 326 | print 'Resolving standalone xib connection with id', dst 327 | if dst in self.objects: 328 | con['UIDestination'] = self.objects[dst] 329 | result.append(con) 330 | continue 331 | phid = makePlaceholderIdentifier() 332 | con['UIDestination'] = NibProxyObject(phid) 333 | self.upstreamPlaceholders[phid] = dst 334 | result.append(con) 335 | 336 | self.connections = result 337 | 338 | def _resolveConnections_storyboard(self): 339 | view_cons = [] 340 | scene_cons = [] 341 | 342 | upstreamPlaceholderTable = { } # src serial -> tuple( phid, src object ) 343 | cachedProxyObjects = { } 344 | 345 | def placeholderIDForObject(obj): 346 | if obj.serial() in upstreamPlaceholderTable: 347 | phid = upstreamPlaceholderTable[obj.serial()][0] 348 | else: 349 | phid = "UpstreamPlaceholder-" + makexibid() 350 | upstreamPlaceholderTable[obj.serial()] = (phid, obj) 351 | return phid 352 | 353 | def proxyObjectForObject(obj): 354 | phid = placeholderIDForObject(obj) 355 | if cachedProxyObjects.get(phid): 356 | return cachedProxyObjects.get(phid) 357 | prox = NibProxyObject(phid) 358 | cachedProxyObjects[phid] = prox 359 | return prox 360 | 361 | for con in self.connections: 362 | label = con['UILabel'] 363 | src = con['UISource'] 364 | dst = con['UIDestination'] # Get the object ID. 365 | if not isinstance(dst, NibObject): 366 | dst = self.objects.get(dst) or self.viewObjects.get(dst) 367 | assert dst, "Can't find connection destination id %s" % (con['UIDestination']) 368 | con['UIDestination'] = dst 369 | 370 | src_top = src.xibid in self.objects 371 | dst_top = dst.xibid in self.objects 372 | 373 | if not src_top: 374 | assert(src.xibid in self.viewObjects) 375 | 376 | # Something outside the view (typically the view controller) pointing to something in the view. 377 | if (src_top, dst_top) == (True, False): 378 | con['UISource'] = proxyObjectForObject(src) 379 | view_cons.append(con) 380 | 381 | # Something in the view pointing to something not in the view. 382 | elif (src_top, dst_top) == (False, True): 383 | con['UIDestination'] = proxyObjectForObject(dst) 384 | view_cons.append(con) 385 | 386 | elif (src_top, dst_top) == (True, True): 387 | scene_cons.append(con) 388 | 389 | elif (src_top, dst_top) == (False, False): 390 | view_cons.append(con) 391 | 392 | 393 | externObjects = dict(upstreamPlaceholderTable.values()) 394 | 395 | for ph_id, obj_id in self.upstreamPlaceholders.iteritems(): 396 | obj = self.objects[obj_id] 397 | externObjects[ph_id] = obj 398 | 399 | if len(externObjects): 400 | self.storyboardViewController['UIExternalObjectsTableForViewLoading'] = externObjects 401 | 402 | scene_cons.extend(self.segueConnections) 403 | 404 | self.viewConnections = view_cons 405 | self.sceneConnections = scene_cons 406 | 407 | self.storyboardViewController.sceneConnections = scene_cons 408 | 409 | 410 | 411 | 412 | def classSwapper(func): 413 | def inner(ctx, elem, parent, *args, **kwargs): 414 | object = func(ctx, elem, parent, *args, **kwargs) 415 | if object: 416 | customClass = elem.attrib.get("customClass") 417 | if customClass: 418 | object['UIOriginalClassName'] = object.classname() 419 | object['UIClassName'] = customClass 420 | object.setclassname("UIClassSwapper") 421 | 422 | return object 423 | return inner 424 | 425 | def __xibparser_ParseXIBObject(ctx, elem, parent): 426 | tag = elem.tag 427 | fnname = "_xibparser_parse_" + tag 428 | parsefn = globals().get(fnname) 429 | # print "----- PARSETHING: " + tag, parsefn 430 | if parsefn: 431 | obj = parsefn(ctx, elem, parent) 432 | if obj and isinstance(obj, XibObject): 433 | obj.xibid = elem.attrib['id'] 434 | return obj 435 | return None 436 | 437 | def __xibparser_ParseChildren(ctx, elem, obj): 438 | children = [__xibparser_ParseXIBObject(ctx, child_element, obj) for child_element in elem] 439 | return [c for c in children if c] 440 | 441 | def _xibparser_parse_placeholder(ctx, elem, parent): 442 | placeholderid = elem.attrib['placeholderIdentifier'] 443 | obj = NibProxyObject(placeholderid) 444 | __xibparser_ParseChildren(ctx, elem, obj) 445 | ctx.addObject(elem.attrib['id'], obj) 446 | return obj 447 | 448 | 449 | def _xibparser_parse_interfacebuilder_properties(ctx, elem, parent, obj): 450 | 451 | rid = elem.attrib.get('restorationIdentifier') 452 | if rid: 453 | obj['UIRestorationIdentifier'] = rid 454 | 455 | ibid = elem.attrib.get('id') 456 | if ibid: 457 | ctx.addObject(ibid, obj) 458 | 459 | 460 | class XibObject(NibObject): 461 | def __init__(self, classname): 462 | NibObject.__init__(self, classname) 463 | self.xibid = None 464 | 465 | def originalclassname(self): 466 | if not self.classname: 467 | return None 468 | if self.classname != "UIClassSwapper": 469 | return self.classname() 470 | oc = self['UIOriginalClassName'] 471 | return oc 472 | 473 | 474 | class XibViewController(XibObject): 475 | def __init__(self, classname): 476 | XibObject.__init__(self, classname) 477 | self.xibattributes = { } 478 | 479 | # For storyboards: 480 | self.relationshipsegue = None 481 | self.sceneConnections = None # Populated in ArchiveContext.resolveConnections() 482 | 483 | @classSwapper 484 | def _xibparser_parse_viewController(ctx, elem, parent, **kwargs): 485 | obj = XibViewController(kwargs.get("uikit_class") or "UIViewController") 486 | 487 | if elem.attrib.get('sceneMemberID') == 'viewController': 488 | ctx.storyboardViewController = obj 489 | 490 | obj.xibattributes = elem.attrib or { } 491 | __xibparser_ParseChildren(ctx, elem, obj) 492 | _xibparser_parse_interfacebuilder_properties(ctx, elem, parent, obj) 493 | obj['UIStoryboardIdentifier'] = elem.attrib.get('storyboardIdentifier') 494 | 495 | return obj 496 | 497 | def _xibparser_parse_navigationController(ctx, elem, parent): 498 | obj = _xibparser_parse_viewController(ctx, elem, parent, uikit_class = "UINavigationController") 499 | return obj 500 | 501 | def _xibparser_parse_tableViewController(ctx, elem, parent): 502 | obj = _xibparser_parse_viewController(ctx, elem, parent, uikit_class = "UITableViewController") 503 | return obj 504 | 505 | 506 | ''' 507 | List of attributes I've seen on 'view' elements 508 | 509 | Unhandled 510 | 511 | adjustsFontSizeToFit 512 | baselineAdjustment 513 | clipsSubviews 514 | horizontalHuggingPriority 515 | lineBreakMode 516 | opaque 517 | text 518 | userInteractionEnabled 519 | verticalHuggingPriority 520 | 521 | contentHorizontalAlignment="center" 522 | contentVerticalAlignment="center" 523 | buttonType="roundedRect" 524 | 525 | 526 | Started 527 | key - 'view' (for view controllers) 528 | 529 | Done 530 | contentMode - TODO: Make sure the string values we check are correct. 531 | customClass 532 | restorationIdentifier 533 | translatesAutoresizingMaskIntoConstraints 534 | 535 | WontDo 536 | fixedFrame - I think this is only for interface builder. (it gets set on UISearchBar) 537 | id - Not arhived in nib. 538 | ''' 539 | @classSwapper 540 | def _xibparser_parse_view(ctx, elem, parent, **kwargs): 541 | obj = XibObject(kwargs.get("uikit_class") or "UIView") 542 | obj.setrepr(elem) 543 | 544 | key = elem.get('key') 545 | if key == 'view': 546 | parent['UIView'] = obj 547 | elif key == 'tableFooterView': 548 | parent['UITableFooterView'] = obj 549 | parent.append('UISubviews', obj) 550 | elif key == 'tableHeaderView': 551 | parent['UITableHeaderView'] = obj 552 | parent.append('UISubviews', obj) 553 | elif key == 'contentView': 554 | if parent.originalclassname() != "UIVisualEffectView": 555 | print "Unhandled class '%s' to take UIView with key 'contentView'" % (parent.originalclassname()) 556 | else: 557 | parent['UIVisualEffectViewContentView'] = obj 558 | obj.setclassname('_UIVisualEffectContentView') 559 | 560 | isMainView = key == 'view' # and isinstance(parent, XibViewController)? 561 | 562 | if elem.attrib.get('translatesAutoresizingMaskIntoConstraints') == 'NO': 563 | obj['UIViewDoesNotTranslateAutoresizingMaskIntoConstraints'] = True 564 | 565 | if 'contentMode' in elem.attrib.keys(): 566 | mode = elem.attrib['contentMode'] 567 | enum = [ 'scaleToFill', 'scaleAspectFit', 'scaleAspectFill', 'redraw', 'center', 'top', 'bottom', 'left', 'right', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight' ] 568 | idx = enum.index(mode) 569 | if idx: # It doesn't encode the default value. 570 | obj['UIContentMode'] = NibByte(idx) 571 | 572 | obj['UIClipsToBounds'] = elem.attrib.get('clipsSubviews') == 'YES' 573 | 574 | # Default Values? 575 | obj['UIAutoresizingMask'] = NibByte(36) # Flexible right + bottom margin. 576 | obj['UIAutoresizeSubviews'] = True 577 | 578 | 579 | val = elem.attrib.get('text') 580 | if val: 581 | obj['UIText'] = val 582 | 583 | if isMainView: 584 | ctx.isParsingStoryboardView = True 585 | 586 | ctx.extraNibObjects.append(obj) 587 | 588 | # Parse these props first, in case any of our children point to us. 589 | _xibparser_parse_interfacebuilder_properties(ctx, elem, parent, obj) 590 | __xibparser_ParseChildren(ctx, elem, obj) 591 | 592 | 593 | if isMainView: 594 | ctx.isParsingStoryboardView = False 595 | 596 | return obj 597 | 598 | def _xibparser_parse_searchBar(ctx, elem, parent): 599 | return _xibparser_parse_view(ctx, elem, parent, uikit_class = "UISearchBar") 600 | 601 | def _xibparser_parse_imageView(ctx, elem, parent): 602 | return _xibparser_parse_view(ctx, elem, parent, uikit_class = "UIImageView") 603 | 604 | def _xibparser_parse_textView(ctx, elem, parent): 605 | return _xibparser_parse_view(ctx, elem, parent, uikit_class = "UITextView") 606 | 607 | def _xibparser_parse_label(ctx, elem, parent): 608 | cls = "UILabel" 609 | if ctx.isPrototypeList: 610 | cls = "UITableViewLabel" 611 | label = _xibparser_parse_view(ctx, elem, parent, uikit_class = cls) 612 | label.setIfEmpty("UIUserInteractionDisabled", True) 613 | label.setIfEmpty("UIViewContentHuggingPriority", "{251, 251}") 614 | return label 615 | 616 | def _xibparser_parse_button(ctx, elem, parent): 617 | button = _xibparser_parse_view(ctx, elem, parent, uikit_class = "UIButton") 618 | 619 | btn_type = elem.attrib.get('buttonType') 620 | if btn_type: 621 | # Todo: Verify these string constants. 622 | idx = [ 'custom', 'system', 'detailDisclosure', 'infoLight', 'infoDark', 'contactAdd', 'roundedRect' ].index(btn_type) 623 | # From iOS 7 onward, roundedRect buttons become system buttons. 624 | idx = 1 if idx == 6 else idx 625 | button['UIButtonType'] = NibByte(idx) 626 | 627 | content = elem.attrib.get('') 628 | 629 | button['UIAdjustsImageWhenHighlighted'] = True 630 | button['UIAdjustsImageWhenDisabled'] = True 631 | # Todo: Default button font. 632 | 633 | # UIButtonStatefulContent = (10) @23 634 | # { 635 | # 0 : @35: UIButtonContent 636 | # UITitle = @12 Shout! 637 | # UIShadowColor = @55 ( 0.5 0.5 0.5 1) 638 | # } 639 | return button 640 | 641 | def _xibparser_parse_navigationBar(ctx, elem, parent): 642 | bar = _xibparser_parse_view(ctx, elem, parent, uikit_class = "UINavigationBar") 643 | if elem.attrib.get('key') == 'navigationBar': 644 | parent['UINavigationBar'] = bar 645 | bar['UIDelegate'] = parent 646 | 647 | translucent = elem.attrib.get('translucent') != 'NO' 648 | bar['UIBarTranslucence'] = 1 if translucent else 2 649 | 650 | if elem.attrib.get('barStyle') == 'black': 651 | bar['UIBarStyle'] = 1 652 | 653 | return bar 654 | 655 | def _xibparser_parse_visualEffectView(ctx, elem, parent): 656 | view = _xibparser_parse_view(ctx, elem, parent, uikit_class = "UIVisualEffectView") 657 | assert view.get('UIVisualEffectViewEffect') 658 | # view['UIVisualEffectViewGroupName'] = NibNil() 659 | return view 660 | 661 | def _xibparser_parse_blurEffect(ctx, elem, parent): 662 | obj = NibObject('UIBlurEffect') 663 | obj['UIBlurEffectStyle'] = ['extraLight', 'light', 'dark'].index(elem.attrib['style']) 664 | 665 | if parent.originalclassname() == 'UIVisualEffectView': 666 | parent['UIVisualEffectViewEffect'] = obj 667 | elif parent.originalclassname() == 'UIVibrancyEffect': 668 | parent['UIVibrancyEffectBlurStyle'] = obj['UIBlurEffectStyle'] #['extraLight', 'light', 'dark'].index(elem.attrib['style']) 669 | 670 | def _xibparser_parse_vibrancyEffect(ctx, elem, parent): 671 | obj = XibObject("UIVibrancyEffect") 672 | __xibparser_ParseChildren(ctx, elem, obj) 673 | parent['UIVisualEffectViewEffect'] = obj 674 | 675 | 676 | ''' 677 | clipsSubviews 678 | contentMode="scaleToFill" 679 | alwaysBounceVertical="YES" 680 | dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="22" sectionFooterHeight="22 681 | 682 | 683 | 684 | Default UITableView in UITableViewController: 685 | 686 | UIBounds = (8) (0.0, 0.0, 600.0, 600.0) 687 | UICenter = (8) (300.0, 300.0) 688 | UIBackgroundColor = (10) @2 689 | UIOpaque = (5) True 690 | UIAutoresizeSubviews = (5) True 691 | UIAutoresizingMask = (0) 36 692 | UIClipsToBounds = (5) True 693 | UIBouncesZoom = (5) True 694 | UIAlwaysBounceVertical = (5) True 695 | UIContentSize = (8) (600.0, 0.0) 696 | UISeparatorStyle = (0) 1 697 | UISeparatorStyleIOS5AndLater = (0) 1 698 | UISectionHeaderHeight = (6) 22.0 699 | UISectionFooterHeight = (6) 22.0 700 | UIShowsSelectionImmediatelyOnTouchBegin = (5) True 701 | 702 | 703 | ''' 704 | def _xibparser_parse_tableView(ctx, elem, parent): 705 | table = _xibparser_parse_view(ctx, elem, parent, uikit_class = "UITableView") 706 | 707 | sepstylemap = { 708 | 'default' : (1, 1), 709 | None : (1, 1), 710 | 'singleLine' : (1, 1), 711 | 'none' : None, 712 | 'singleLineEtched' : (1, 2) 713 | } 714 | sepstyle = sepstylemap[elem.attrib.get('separatorStyle')] 715 | if sepstyle: 716 | table['UISeparatorStyle'] = sepstyle[0] 717 | table['UISeparatorStyleIOS5AndLater'] = sepstyle[1] 718 | 719 | rowHeight = elem.attrib.get('rowHeight') 720 | table['UIRowHeight'] = rowHeight and float(rowHeight) 721 | 722 | return table 723 | 724 | 725 | def _xibparser_parse_state(ctx, elem, parent): 726 | if parent.originalclassname() != "UIButton": 727 | print "'state' tag currently only supported for UIButtons. given", parent.originalclassname() 728 | return None 729 | 730 | content = NibObject("UIButtonContent") 731 | content['UITitle'] = elem.attrib.get('title') 732 | # content['UIShadowColor'] = XibColor.fromrgb(0.5, 0.5, 0.5) 733 | 734 | # Todo: Verify these constants. 735 | statevalue = ['normal', 'highlighted', 'disabled', 'selected' ].index(elem.attrib['key']) 736 | if statevalue: 737 | statevalue = 1 << (statevalue - 1) # Translates 0, 1, 2, 3 to 0, 1 << 0, 1 << 1, 1 << 2 738 | 739 | buttonstates = parent.get('UIButtonStatefulContent') 740 | if not buttonstates: 741 | buttonstates = { } 742 | parent['UIButtonStatefulContent'] = buttonstates 743 | buttonstates[statevalue] = content 744 | 745 | __xibparser_ParseChildren(ctx, elem, content) 746 | 747 | 748 | def _xibparser_parse_subviews(ctx, elem, parent): 749 | # Do we need to pass 'parent' here? Is there anything in XIBs where any of the subviews have a "key" attribute. Table views maybe? 750 | views = __xibparser_ParseChildren(ctx, elem, parent) 751 | parent.extend('UISubviews', views) 752 | 753 | def _xibparser_parse_prototypes(ctx, elem, parent): 754 | 755 | prototypes = { } 756 | prototypeExternalObjects = { } 757 | 758 | for tableViewCell in elem: 759 | rid = tableViewCell.attrib.get('reuseIdentifier') 760 | if not rid: 761 | print "Prototype cell %s has no reuseIdentifier. Skipping." % (tableViewCell.attrib['id']) 762 | continue 763 | 764 | subcontext = ArchiveContext() 765 | subcontext.isPrototypeList = True 766 | subcontext.parentContext = ctx 767 | root = ParseXIBObjects([tableViewCell], subcontext) 768 | externObjects = { } 769 | for ph_id, obj_id in subcontext.upstreamPlaceholders.iteritems(): 770 | obj = ctx.getObjectInCurrentContext(obj_id) 771 | if obj: 772 | externObjects[ph_id] = obj 773 | continue 774 | 775 | phid_to_parent = makePlaceholderIdentifier() 776 | externObjects[ph_id] = NibProxyObject(phid_to_parent) 777 | ctx.upstreamPlaceholders[phid_to_parent] = obj_id 778 | 779 | if len(externObjects): 780 | prototypeExternalObjects[rid] = externObjects 781 | 782 | prototypeNibData = CompileNibObjects([root]) 783 | 784 | prototypeNib = NibObject("UINib") 785 | prototypeNib['captureEnclosingNIBBundleOnDecode'] = True 786 | prototypeNib['archiveData'] = NibData(prototypeNibData) 787 | 788 | 789 | prototypes[tableViewCell.attrib['reuseIdentifier']] = prototypeNib 790 | 791 | if len(prototypeExternalObjects): 792 | parent['UITableViewCellPrototypeNibExternalObjects'] = prototypeExternalObjects 793 | 794 | if not len(prototypes): 795 | return 796 | 797 | parent['UITableViewCellPrototypeNibs'] = prototypes 798 | 799 | def _xibparser_parse_tableViewCell(ctx, elem, parent): 800 | stylemap = { 801 | 'IBUITableViewCellStyleDefault' : None, 802 | 'IBUITableViewCellStyleValue1' : 1, 803 | 'IBUITableViewCellStyleValue2' : 2, 804 | 'IBUITableViewCellStyleSubtitle' : 3, 805 | } 806 | selectmap = { 807 | 'none' : 0, 808 | 'blue' : 1, 809 | 'gray' : 2, 810 | 'default' : None, 811 | } 812 | accmap = { 813 | 'disclosureIndicator' : 1, 814 | 'detailDisclosureButton' : 2, 815 | 'checkmark' : 3, 816 | 'detailButton' : 4, 817 | } 818 | 819 | cell = _xibparser_parse_view(ctx, elem, parent, uikit_class='UITableViewCell') 820 | cell['UITextLabel'] = ctx.findObject(elem.attrib.get('textLabel')) 821 | cell['UIDetailTextLabel'] = ctx.findObject(elem.attrib.get('detailTextLabel')) 822 | cell['UIImageView'] = ctx.findObject(elem.attrib.get('imageView')) 823 | cell['UIReuseIdentifier'] = elem.attrib.get('reuseIdentifier') 824 | 825 | cell['UITableViewCellStyle'] = stylemap.get(elem.attrib.get('style')) 826 | cell['UISelectionStyle'] = selectmap.get(elem.attrib.get('selectionStyle')) 827 | cell.setIfNotDefault('UIIndentationWidth', float(elem.attrib.get('indentationWidth') or 0), 10.0) 828 | cell.setIfNotDefault('UIIndentationLevel', int(elem.attrib.get('indentationLevel') or 0), 0) 829 | cell['UIAccessoryType'] = accmap.get(elem.attrib.get('accessoryType')) 830 | cell['UIEditingAccessoryType'] = accmap.get(elem.attrib.get('editingAccessoryType')) 831 | cell['UIShowsReorderControl'] = elem.attrib.get('showsReorderControl') == 'YES' or None 832 | 833 | # I can't seem to see what effect shouldIndentWhileEditing="NO" has on the nib. 834 | 835 | return cell 836 | 837 | def _xibparser_parse_tableViewCellContentView(ctx, elem, parent): 838 | view = _xibparser_parse_view(ctx, elem, parent, uikit_class = 'UITableViewCellContentView') 839 | parent['UIContentView'] = view 840 | parent['UISubviews'] = [ view ] 841 | return view 842 | 843 | 844 | # Types of connections: outlet, action, segue, *outletConnection (any more?) 845 | def _xibparser_parse_connections(ctx, elem, parent): 846 | __xibparser_ParseChildren(ctx, elem, parent) 847 | 848 | def _xibparser_parse_outlet(ctx, elem, parent): 849 | con = NibObject("UIRuntimeOutletConnection") 850 | con['UILabel'] = elem.attrib.get('property') 851 | con['UISource'] = parent 852 | con['UIDestination'] = elem.attrib.get('destination') 853 | con.xibid = elem.attrib['id'] 854 | 855 | # Add this to the list of connections we'll have to resolve later. 856 | ctx.connections.append(con) 857 | 858 | def _xibparser_parse_action(ctx, elem, parent): 859 | 860 | etype = elem.attrib.get('eventType') 861 | 862 | # @31: UIRuntimeEventConnection 863 | # UILabel = (10) @48 "shout:" 864 | # UISource = (10) @51 UIButton instance 865 | # UIDestination = (10) @16 UIProxyObject "UpstreamPlaceholder-cnh-Gb-aGf" 866 | # UIEventMask = (0) 64 UIControlEventTouchUpInside 867 | 868 | maskmap = { 869 | None : None, 870 | 871 | "touchDown" : 1 << 0, 872 | "touchDownRepeat" : 1 << 1, 873 | "touchDragInside" : 1 << 2, 874 | "touchDragOutside" : 1 << 3, 875 | "touchDragEnter" : 1 << 4, 876 | "touchDragExit" : 1 << 5, 877 | "touchUpInside" : 1 << 6, 878 | "touchUpOutside" : 1 << 7, 879 | "touchCancel" : 1 << 8, 880 | 881 | "valueChanged" : 1 << 12, 882 | 883 | "editingDidBegin" : 1 << 16, 884 | "editingChanged" : 1 << 17, 885 | "editingDidEnd" : 1 << 18, 886 | "editingDidEndOnExit" : 1 << 19, 887 | } 888 | 889 | mask = maskmap[etype] 890 | 891 | con = NibObject("UIRuntimeEventConnection") 892 | con['UILabel'] = elem.attrib['selector'] 893 | con['UISource'] = parent 894 | con['UIDestination'] = elem.attrib['destination'] 895 | con['UIEventMask'] = mask 896 | 897 | ctx.connections.append(con) 898 | 899 | def _xibparser_parse_segue(ctx, elem, parent): 900 | 901 | template = XibObject("") 902 | template.xibid = elem.attrib['id'] 903 | template['UIIdentifier'] = elem.attrib.get('identifier') 904 | template['UIDestinationViewControllerIdentifier'] = elem.attrib['destination'] 905 | 906 | kind = elem.attrib['kind'] 907 | 908 | if kind in ['presentation','modal']: 909 | if elem.attrib.get('modalPresentationStyle'): 910 | enum = { "fullScreen" : 0, "pageSheet" : 1, "formSheet" : 2, "currentContext" : 3, "overFullScreen" : 5, "overCurrentContext" : 6 } 911 | segue['UIModalPresentationStyle'] = enum.get(elem.attrib['modalPresentationStyle']) 912 | 913 | if elem.attrib.get('modalTransitionStyle'): 914 | enum = { "coverVertical" : 0, "flipHorizontal" : 1, "crossDissolve" : 2, "partialCurl" : 3 } 915 | segue['UIModalTransitionStyle'] = enum.get(elem.attrib['modalTransitionStyle']) 916 | 917 | if elem.attrib.get('animates') == 'NO': 918 | segue['UIAnimates'] = False 919 | 920 | if kind == 'show': 921 | template.setclassname('UIStoryboardShowSegueTemplate') 922 | template['UIActionName'] = "showViewController:sender:" 923 | 924 | elif kind == 'showDetail': 925 | template.setclassname('UIStoryboardShowSegueTemplate') 926 | template['UIActionName'] = "showDetailViewController:sender:" 927 | 928 | elif kind == 'presentation': 929 | template.setclassname('UIStoryboardPresentationSegueTemplate') 930 | 931 | 932 | ## Deprecated segue types 933 | 934 | elif kind == 'push': 935 | template.setclassname("UIStoryboardPushSegueTemplate") 936 | template['UIDestinationContainmentContext'] = 0 937 | template['UISplitViewControllerIndex'] = 0 938 | 939 | elif kind == 'modal': 940 | template.setclassname("UIStoryboardModalSegueTemplate") 941 | 942 | elif kind == 'replace': 943 | template.setclassname("UIStoryboardReplaceSegueTemplate") 944 | 945 | template['UIDestinationContainmentContext'] = 1 946 | template['UISplitViewControllerIndex'] = elem.attrib.get('splitViewControllerTargetIndex') 947 | 948 | ## Custom segue 949 | 950 | elif kind == 'custom': 951 | template.setclassname("UIStoryboardSegueTemplate") 952 | template['UISegueClassName'] = elem.attrib.get('customClass') 953 | 954 | elif kind == 'relationship': 955 | parent.relationshipsegue = elem 956 | return 957 | 958 | else: 959 | print 'Unknown segue kind', kind 960 | return 961 | 962 | 963 | # Get the context to install the segue in. 964 | sctx = ctx.contextForSegues() 965 | 966 | controller = sctx.storyboardViewController 967 | templateList = controller.get('UIStoryboardSegueTemplates') 968 | if not templateList: 969 | templateList = [ ] 970 | controller['UIStoryboardSegueTemplates'] = templateList 971 | templateList.append(template) 972 | sctx.addObject(template.xibid, template, True) 973 | 974 | vcConnection = NibObject('UIRuntimeOutletConnection') 975 | vcConnection['UILabel'] = 'viewController' 976 | vcConnection['UISource'] = template 977 | vcConnection['UIDestination'] = controller 978 | 979 | sctx.segueConnections.append(vcConnection) 980 | sctx.extraNibObjects.append(template) 981 | 982 | # TODO: What other types of IB objects can trigger segues? 983 | if parent.originalclassname() == 'UIButton': 984 | con = NibObject("UIRuntimeEventConnection") 985 | 986 | segue_phid = makePlaceholderIdentifier() 987 | 988 | con['UILabel'] = 'perform:' 989 | con['UISource'] = parent 990 | con['UIDestination'] = NibProxyObject(segue_phid) 991 | con['UIEventMask'] = 64 992 | ctx.connections.append(con) 993 | ctx.upstreamPlaceholders[segue_phid] = template.xibid 994 | 995 | elif parent.originalclassname() == 'UITableViewCell': 996 | 997 | label = 'selectionSegueTemplate' 998 | if elem.attrib.get('trigger') == "accessoryAction": 999 | label = 'accessoryActionSegueTemplate' 1000 | 1001 | segue_phid = makePlaceholderIdentifier() 1002 | 1003 | con = NibObject("UIRuntimeOutletConnection") 1004 | con['UILabel'] = label 1005 | con['UISource'] = parent 1006 | con['UIDestination'] = NibProxyObject(segue_phid) 1007 | ctx.connections.append(con) 1008 | ctx.upstreamPlaceholders[segue_phid] = template.xibid 1009 | 1010 | def _xibparser_parse_layoutGuides(ctx, elem, parent): 1011 | __xibparser_ParseChildren(ctx, elem, parent) 1012 | 1013 | def _xibparser_parse_viewControllerLayoutGuide(ctx, elem, parent): 1014 | obj = XibObject("_UILayoutGuide") 1015 | obj.xibid = elem.attrib['id'] 1016 | obj['UIOpaque'] = True 1017 | obj['UIHidden'] = True 1018 | obj['UIAutoresizeSubviews'] = True 1019 | obj['UIViewDoesNotTranslateAutoresizingMaskIntoConstraints'] = True 1020 | #UIViewAutolayoutConstraints = (10) @21 1021 | #_UILayoutGuideConstraintsToRemove = (10) @30 1022 | 1023 | if elem.attrib.get('type') == 'bottom': 1024 | obj['_UILayoutGuideIdentifier'] = "_UIViewControllerBottom" 1025 | elif elem.attrib.get('type') == 'top': 1026 | obj['_UILayoutGuideIdentifier'] = "_UIViewControllerTop" 1027 | 1028 | ctx.addObject(obj.xibid, obj) 1029 | 1030 | ctx.viewControllerLayoutGuides.append(obj) 1031 | 1032 | 1033 | def _xibparser_parse_constraints(ctx, elem, parent): 1034 | __xibparser_ParseChildren(ctx, elem, parent) 1035 | 1036 | def _xibparser_parse_constraint(ctx, elem, parent): 1037 | constraint = _xibparser_get_constraint(ctx, elem, parent) 1038 | if constraint: 1039 | parent.append('UIViewAutolayoutConstraints', constraint) 1040 | ctx.addObject(constraint.xibid, constraint) 1041 | 1042 | item = constraint['NSFirstItem'] 1043 | if isinstance(item, basestring): 1044 | ctx.viewReferences.append( (item, constraint, 'NSFirstItem') ) 1045 | 1046 | item = constraint.get('NSSecondItem') 1047 | if item and isinstance(item, basestring): 1048 | ctx.viewReferences.append( (item, constraint, 'NSSecondItem') ) 1049 | 1050 | def _xibparser_get_constraint(ctx, elem, parent): 1051 | attributes = { 1052 | None : 0, 1053 | 'left' : 1, 1054 | 'right' : 2, 1055 | 'top' : 3, 1056 | 'bottom' : 4, 1057 | 'leading' : 5, 1058 | 'trailing' : 6, 1059 | 'width' : 7, 1060 | 'height' : 8, 1061 | 1062 | # todo: verify these constants. 1063 | 'centerX' : 9, 1064 | 'centerY' : 10, 1065 | 'baseline' : 11, 1066 | } 1067 | 1068 | attribute1 = attributes.get(elem.attrib.get('firstAttribute')) 1069 | attribute2 = attributes.get(elem.attrib.get('secondAttribute')) 1070 | constant = float(elem.attrib.get('constant') or 0) 1071 | firstItem = elem.attrib.get('firstItem') or parent 1072 | secondItem = elem.attrib.get('secondItem') 1073 | priority = elem.attrib.get('priority') 1074 | priority = priority and int(priority) 1075 | 1076 | con = XibObject('NSLayoutConstraint') 1077 | con.xibid = elem.attrib['id'] 1078 | con['NSFirstItem'] = firstItem 1079 | con['NSFirstAttribute'] = attribute1 1080 | con['NSFirstAttributeV2'] = attribute1 1081 | con['NSSecondAttribute'] = attribute2 1082 | con['NSSecondAttributeV2'] = attribute2 1083 | con['NSConstant'] = constant 1084 | con['NSConstantV2'] = constant 1085 | con['NSShouldBeArchived'] = True 1086 | con['NSPriority'] = priority 1087 | con['NSSecondItem'] = secondItem 1088 | return con 1089 | 1090 | def _xibparser_parse_items(ctx, elem, parent): 1091 | if parent.originalclassname() != 'UINavigationBar': 1092 | print "'items' tag only supported for UINavigationBar." 1093 | return 1094 | items = __xibparser_ParseChildren(ctx, elem, None) 1095 | parent['UIItems'] = items 1096 | 1097 | def _xibparser_parse_navigationItem(ctx, elem, parent): 1098 | 1099 | item = XibObject("UINavigationItem") 1100 | item['UITitle'] = elem.attrib.get('title') 1101 | 1102 | if elem.attrib.get('key') == "navigationItem": 1103 | parent['UINavigationItem'] = item 1104 | __xibparser_ParseChildren(ctx, elem, item) 1105 | return item 1106 | 1107 | def _xibparser_parse_barButtonItem(ctx, elem, parent): 1108 | item = XibObject("UIBarButtonItem") 1109 | item.xibid = elem.attrib['id'] 1110 | ctx.addObject(item.xibid, item) 1111 | 1112 | item['UIStyle'] = 1 # Plain? 1113 | item['UIEnabled'] = True 1114 | item['UITitle'] = elem.attrib.get('title') 1115 | 1116 | sysItem = elem.attrib.get('systemItem') 1117 | if sysItem: 1118 | # TODO: Verify these constants. 1119 | allSysItems = [ 'done', 'cancel', 'edit', 'save', 'add', 'flexibleSpace', 'fixedSpace', 'compose', 'reply', 1120 | 'action', 'organize', 'bookmarks', 'search', 'refresh', 'stop', 'camera', 'trash', 'play', 'pause', 1121 | 'rewind', 'fastForward', 'undo', 'redo', 'pageCurl' ] 1122 | 1123 | item['UIIsSystemItem'] = True 1124 | item['UISystemItem'] = allSysItems.index(sysItem) 1125 | 1126 | keymap = { 1127 | 'backBarButtonItem' : 'UIBackBarButtonItem', 1128 | 'rightBarButtonItem' : 'UIRightBarButtonItem', 1129 | } 1130 | 1131 | key = elem.attrib.get('key') 1132 | if key in keymap: 1133 | parent[key] = item 1134 | 1135 | if key == 'rightBarButtonItem': 1136 | parent.append('UIRightBarButtonItems', item) 1137 | 1138 | # Parse children here. 1139 | __xibparser_ParseChildren(ctx, elem, item) 1140 | 1141 | 1142 | # TODO: Finish getting the rest of the system colors. 1143 | def _xibparser_get_color(elem): 1144 | obj = NibObject("UIColor") 1145 | 1146 | presets = { 1147 | 'darkTextColor' : (0.0,0.0,0.0,1.0) 1148 | } 1149 | 1150 | r = None 1151 | a = None 1152 | scolor = elem.attrib.get('cocoaTouchSystemColor') 1153 | if scolor: 1154 | preset = presets.get(scolor) 1155 | if preset: 1156 | r,g,b,a = preset 1157 | if r is None: 1158 | if scolor == "groupTableViewBackgroundColor": 1159 | obj['UISystemColorName'] = 'groupTableViewBackgroundColor' 1160 | obj['UIPatternSelector'] = 'groupTableViewBackgroundColor' 1161 | return obj 1162 | 1163 | if 'white' in elem.attrib.keys(): 1164 | r = g = b = float(elem.attrib['white']) 1165 | 1166 | if r is None: 1167 | r = float(elem.attrib.get("red") or 0.0) 1168 | g = float(elem.attrib.get("green") or 0.0) 1169 | b = float(elem.attrib.get("blue") or 0.0) 1170 | 1171 | if a is None: 1172 | a = float(elem.attrib.get("alpha") or 1.0) 1173 | 1174 | obj['UIColorComponentCount'] = NibByte(4) 1175 | # obj['UIRed'] = NibFloatToWord(r) 1176 | # obj['UIGreen'] = NibFloatToWord(g) 1177 | # obj['UIBlue'] = NibFloatToWord(b) 1178 | # obj['UIAlpha'] = NibFloatToWord(a) 1179 | obj['UIRed'] = r 1180 | obj['UIGreen'] = g 1181 | obj['UIBlue'] = b 1182 | obj['UIAlpha'] = a 1183 | obj['UIColorSpace'] = NibByte(2) #todo: figure out what color spaces there are. 1184 | obj['NSRGB'] = NibInlineString("%.3f %.3f %.3f" % (r, g, b)) 1185 | return obj 1186 | 1187 | ''' Maybe NSColorSpace 4 is calibratedWhiteColorSpace ? 1188 | 25: UIColor 1189 | UISystemColorName = (10) @34 1190 | UIColorComponentCount = (0) 2 1191 | UIWhite = (6) 0.0 1192 | UIAlpha = (6) 1.0 1193 | NSWhite = (8) 0 1194 | NSColorSpace = (0) 4 1195 | ''' 1196 | def _xibparser_parse_color(ctx, elem, parent): 1197 | 1198 | color = _xibparser_get_color(elem) 1199 | 1200 | # TODO: We could move this key handling somewhere else. 1201 | key = elem.attrib.get('key') 1202 | if key: 1203 | XibToNib = { 1204 | "backgroundColor" : "UIBackgroundColor", 1205 | "textColor" : "UITextColor", 1206 | "titleShadowColor" : "UIShadowColor", 1207 | "titleColor" : "UITitleColor", 1208 | "barTintColor" : "UIBarTintColor", 1209 | "separatorColor" : "UISeparatorColor" 1210 | } 1211 | 1212 | key = XibToNib.get(key) 1213 | if key: 1214 | parent[key] = color 1215 | 1216 | return object 1217 | 1218 | # TODO: I think this function might need more logic when the bounds aren't set at 0, 0 1219 | def _xibparser_parse_rect(ctx, elem, parent): 1220 | x = float(elem.attrib.get('x')) 1221 | y = float(elem.attrib.get('y')) 1222 | w = float(elem.attrib.get('width')) 1223 | h = float(elem.attrib.get('height')) 1224 | 1225 | key = elem.attrib.get('key') 1226 | if key == 'frame': 1227 | 1228 | cx = float(x + w / 2) 1229 | cy = float(y + h / 2) 1230 | bx = float(0) 1231 | by = float(0) 1232 | bw = float(w) 1233 | bh = float(h) 1234 | center = ( cx, cy ) 1235 | bounds = ( bx, by, bw, bh ) 1236 | parent['UICenter'] = center 1237 | parent['UIBounds'] = bounds 1238 | 1239 | def _xibparser_parse_inset(ctx, elem, parent): 1240 | key = elem.attrib.get('key') 1241 | if key != 'separatorInset': 1242 | print "'inset' tag only supported for key 'separatorInset'." 1243 | return 1244 | minX = float(elem.attrib['minX']) 1245 | maxX = float(elem.attrib['maxX']) 1246 | minY = float(elem.attrib['minY']) 1247 | maxY = float(elem.attrib['maxY']) 1248 | 1249 | inset = (minY, minX, maxY, maxX) 1250 | 1251 | # TODO: Uncomment this after we start honoring separator style for UITableView. 1252 | if key == 'separatorInset': 1253 | parent['UISeparatorInset'] = inset 1254 | 1255 | def _xibparser_parse_autoresizingMask(ctx, elem, parent): 1256 | 1257 | if elem.attrib.get('key') != "autoresizingMask": 1258 | return 1259 | 1260 | flex_left = elem.attrib.get('flexibleMinX') == 'YES' 1261 | flex_width = elem.attrib.get('widthSizable') == 'YES' 1262 | flex_right = elem.attrib.get('flexibleMaxX') == 'YES' 1263 | flex_top = elem.attrib.get('flexibleMinY') == 'YES' 1264 | flex_height = elem.attrib.get('heightSizable') == 'YES' 1265 | flex_bottom = elem.attrib.get('flexibleMaxY') == 'YES' 1266 | 1267 | mask = 0 1268 | for idx, value in enumerate([flex_left, flex_width, flex_right, flex_top, flex_height, flex_bottom]): 1269 | if value: 1270 | mask = mask | (1 << idx) 1271 | 1272 | parent['UIAutoresizingMask'] = NibByte(mask) 1273 | 1274 | def _xibparser_parse_textInputTraits(ctx, elem, parent): 1275 | if elem.attrib.get('key') != 'textInputTraits': 1276 | return 1277 | 1278 | values = { 1279 | 'UIReturnKeyType' : NibByte(6), 1280 | 'UIEnablesReturnKeyAutomatically' : True, 1281 | 'UISecureTextEntry' : False, 1282 | } 1283 | 1284 | # TODO: Read the traits object for overriddden values. 1285 | for k,v, in values.iteritems(): 1286 | parent[k] = v 1287 | 1288 | def _xibparser_parse_point(ctx, elem, parent): 1289 | point = ( float(elem.attrib['x']), float(elem.attrib['y']) ) 1290 | 1291 | #TODO: 1292 | def _xibparser_parse_fontDescription(ctx, elem, parent): 1293 | if elem.attrib.get('key') != 'fontDescription': 1294 | return 1295 | 1296 | size = float(elem.attrib.get('pointSize') or 0.0) 1297 | fpsize = size 1298 | fonttype = elem.attrib.get('type') 1299 | fontstyle = elem.attrib.get('style') 1300 | 1301 | font = NibObject("UIFont") 1302 | font['UIFontTraits'] = NibByte(0) 1303 | name = None 1304 | 1305 | 1306 | if fontstyle: 1307 | if fontstyle == 'UICTFontTextStyleBody': 1308 | name = ".HelveticaNeueInterface-Regular" 1309 | font['UIIBTextStyle'] = 'UICTFontTextStyleBody' 1310 | size = 16.0 1311 | font['UISystemFont'] = True 1312 | elif fontstyle == 'UICTFontTextStyleCaption1': 1313 | name = ".HelveticaNeueInterface-Regular" 1314 | font['UIIBTextStyle'] = 'UICTFontTextStyleCaption1' 1315 | size = 11.0 1316 | font['UISystemFont'] = True 1317 | elif fontstyle == 'UICTFontTextStyleCaption2': 1318 | name = ".HelveticaNeueInterface-Regular" 1319 | font['UIIBTextStyle'] = 'UICTFontTextStyleCaption2' 1320 | size = 11.0 1321 | font['UISystemFont'] = True 1322 | elif fontstyle == 'UICTFontTextStyleFootnote': 1323 | name = ".HelveticaNeueInterface-Regular" 1324 | font['UIIBTextStyle'] = 'UICTFontTextStyleCaption2' 1325 | size = 12.0 1326 | font['UISystemFont'] = True 1327 | elif fontstyle == 'UICTFontTextStyleHeadline': 1328 | name = ".HelveticaNeueInterface-MediumP4" 1329 | font['UIIBTextStyle'] = 'UICTFontTextStyleHeadline' 1330 | size = 16.0 1331 | font['UISystemFont'] = True 1332 | font['UIFontTraits'] = NibByte(2) 1333 | elif fontstyle == 'UICTFontTextStyleSubhead': 1334 | name = ".HelveticaNeueInterface-Regular" 1335 | font['UIIBTextStyle'] = 'UICTFontTextStyleSubhead' 1336 | size = 14.0 1337 | font['UISystemFont'] = True 1338 | 1339 | elif fonttype == 'custom' or fonttype == None: 1340 | # family = elem.attrib.get('family') 1341 | name = elem.attrib['name'] 1342 | font['UISystemFont'] = False 1343 | descriptor = NibObject('UIFontDescriptor') 1344 | descriptor['UIFontDescriptorAttributes'] = { 1345 | 'NSFontSizeAttribute' : NibNSNumber(elem.attrib.get('pointSize')), 1346 | 'NSFontNameAttribute' : name 1347 | } 1348 | font['UIFontDescriptor'] = descriptor 1349 | elif fonttype: 1350 | 1351 | descriptor = NibObject("UIFontDescriptor") 1352 | attrs = { 'NSFontSizeAttribute' : size } 1353 | 1354 | if fonttype == 'system': 1355 | name = '.HelveticaNeueInterface-Regular' 1356 | font['UISystemFont'] = True 1357 | attrs['NSCTFontUIUsageAttribute'] = 'CTFontRegularUsage' 1358 | 1359 | elif fonttype == 'boldSystem': 1360 | name = '.HelveticaNeueInterface-MediumP4' 1361 | font['UISystemFont'] = True 1362 | font['UIFontTraits'] = NibByte(2) 1363 | attrs['NSCTFontUIUsageAttribute'] = 'CTFontEmphasizedUsage' 1364 | 1365 | elif fonttype == 'italicSystem': 1366 | name = '.HelveticaNeueInterface-Italic' 1367 | font['UISystemFont'] = True 1368 | font['UIFontTraits'] = NibByte(1) 1369 | attrs['NSCTFontUIUsageAttribute'] = 'CTFontObliqueUsage' 1370 | 1371 | descriptor['UIFontDescriptorAttributes'] = attrs 1372 | 1373 | if not name: 1374 | print "Couldn't find font name." 1375 | return 1376 | 1377 | font['UIFontName'] = name 1378 | font['NSName'] = name 1379 | font['UIFontPointSize'] = fpsize 1380 | font['NSSize'] = size 1381 | 1382 | 1383 | parent['UIFont'] = font 1384 | 1385 | --------------------------------------------------------------------------------